背景
最近有個業務場景,需要做一個新舊資料的相容。大致可以理解為之前儲存到資料庫的資料是一個字串,由於業務調整,該字串要變為一個json。
新的程式碼需要判斷該欄位是否為json,如果是json則序列化為json,如果不是json,則該字串為json的某個欄位。
邏輯簡單,我釋出給測試後,測試問我要怎麼測試,我說需要用舊的資料才能測試這段邏輯,但是我釋出了新的程式碼後,就不能產生舊的的資料了。
資料流如下圖:
測試說這樣很難測試,能不能像前端同學一樣,搞個多版本控制,一鍵切換版本。
測試想要的效果目標如下圖:
我在之前的公司也經常遇到這種場景,但是我一般都叫測試修改程式碼的版本,先發布舊的程式碼然後生產資料,然後切換到新的版本去驗證這種場景。
這個時候,同事推薦我使用公司的基建服務“多環境治理平臺”。
一、什麼是多環境治理
在公司內部,一般是多個功能一起開發,同一個微服務並行開發是時常發生的事。但是功能的上線時間可能是不同的,所以程式碼不能合併在同一個分支開發。
提測的時候,由於測試環境只有一個,要不就是都合併到同一個分支,要不就排隊測試。。。
大夥一起來測試吧
測試人員在排隊使用測試環境
合併到一起測試的話,程式碼會衝突,而且會導致測試環境與線上環境不一致(因為測試環境混雜了其他版本的程式碼)。
分開測試的話會導致排隊現場,阻塞嚴重。
多環境治理就是為了解決這個問題****。
一套測試環境,多個後端版本。
測試人員可以選擇隨意切換後端版本,隨意測試任意一個版本的後端的功能。
二、多環境治理的原理
假設現在有2個featrue功能在開發
featrue1需要修改user和score微服務。
featrue2需要修改user和order微服務。
我們希望最後的流量排程如下圖。
v1的流量優先呼叫v1版本的微服務,如果找不到v1版本的微服務時,要呼叫基準版本的微服務。(例如order)
v2的流量優先呼叫v2版本的微服務,如果找不到v2版本的微服務時,要呼叫基準版本的微服務。(例如score)
要實現以上流量排程,只要做三件事:
1、**每個微服務註冊到註冊中心的時候,要帶上一個標記,標記自己當前的版本。
2、**每個請求都要帶個版本號,而且這個版本號要由閘道器開始,一直透穿到下游。
3、微服務的呼叫下游時,例項選擇策略修改為“優先選擇和流量版本相同的例項,如果沒有該版本的例項,則選擇基準版本的例項”。
多環境治理還能低成本搭建預釋出環境(不需要全部應用都發布一遍pre環境)。
調整一下策略,
根據租戶ID選擇例項,就能實現後端租戶ID級別的灰度釋出。
根據userID選擇例項,就能實現後端userID級別的灰度釋出。
三、多環境治理的實踐
上面說的都是公司給我提供的基建服務,而且是用go語言寫的。
文章前面的小夥伴可能不在大公司,沒有這樣的基建平臺,所以這裡我根據上面說的原理,自己用java,基於springcloud 做一遍樣例給大家。
大家可以參考我樣子,然後基於自己公司的微服務框架增加系統的多環境治理能力。
下面的程式碼例子只會貼出最核心的程式碼,詳細的實踐可以下載我的程式碼自己細看。
一、演示工程目錄
最終的效果如下:
1、一般的請求會走基準環境的程式碼。
2、請求header裡面只要帶version=v1,則呼叫v1版本的order和user程式碼。
3、請求header裡面只要帶version=v2,則呼叫v2版本的order和基準版本的user程式碼。
二、工程搭建
以下程式碼基於springcloud-2020.03版本。
(ps:真的感概技術升級太快,之前還在用zuul、ribbon、hystrix,現在基本都升級換代了。所以大家最重要的是懂原理,程式碼實踐這些可能過一段時間就不能直接用了。)
1、每個微服務註冊都註冊中心的時候,要帶上一個標記,標記自己當前的版本。
註冊到springcloud的eureka時,註冊中心允許例項帶個一個map的資訊。
在order、user服務加上配置。
eureka.instance.metadata-map.version=${version}
只要加上這個配置,就表明這個例項的"version"欄位是“default”。
2、每個請求都要帶個版本號,而且這個版本號要由閘道器開始,一直透穿到下游。
為order和user增加一個過濾器。
請求來了之後,在request裡面找出version標記,把該標記放到ThreadLocal物件中。
(ps:ThreacLocal物件是執行緒隔離的,所以多執行緒的情況下,這個version標記會丟,如果想多執行緒也不丟這個version標記,則可以使用阿里開源的TransmittableThreadLocal)
@Slf4j
@Component
public class VersionFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String version = httpServletRequest.getHeader(Constont.VERSION);
Utils.SetVersion(version);
log.info("set version,{}",version);
filterChain.doFilter(servletRequest,servletResponse);
Utils.CleanVersion();
}
@Override
public void destroy() {
Filter.super.destroy();
}
}
呼叫下游的時候把這個標記傳遞下去。
springclud的loadbalancer允許我們呼叫下游時,對請求做一些自定義的修改。
@Slf4j
@Component
public class VersionLoadBalancerLifecycle implements LoadBalancerLifecycle<RequestDataContext,Object,Object>
{
@Override
public void onStart(Request request) {
Object context = request.getContext();
if (context instanceof RequestDataContext) {
RequestDataContext dataContext = (RequestDataContext) context;
String version = Utils.GetVersion();
dataContext.getClientRequest().getHeaders().add(Constont.VERSION,version);
}
}
@Override
public void onStartRequest(Request request, Response lbResponse) {
}
@Override
public void onComplete(CompletionContext completionContext) {
}
}
3、微服務的呼叫下游時,策略修改為“優先選擇和流量版本相同的例項,如果沒有該版本的例項,則選擇基準版本的例項”。
springcloud內建很多的例項選擇策略,有基於zone的區域,有基於健康檢查的,也有基於使用者暗示的。
但是都不滿足我們的需求,這裡我們需要實現自己策略。
新建類檔案
MulEnvServiceInstanceListSupplier繼承
DelegatingServiceInstanceListSupplier
然後重寫他的方法。
public class MulEnvServiceInstanceListSupplier extends DelegatingServiceInstanceListSupplier {
public MulEnvServiceInstanceListSupplier(ServiceInstanceListSupplier delegate) {
super(delegate);
}
@Override
public Flux<List<ServiceInstance>> get() {
return delegate.get();
}
@Override
public Flux<List<ServiceInstance>> get(Request request) {
return delegate.get(request).map(instances -> filteredByVersion(instances, getVersion(request.getContext())));
}
private String getVersion(Object requestContext) {
if (requestContext == null) {
return null;
}
String version = null;
if (requestContext instanceof RequestDataContext) {
version = getHintFromHeader((RequestDataContext) requestContext);
}
return version;
}
private String getHintFromHeader(RequestDataContext context) {
if (context.getClientRequest() != null) {
HttpHeaders headers = context.getClientRequest().getHeaders();
if (headers != null) {
return headers.getFirst(Constont.VERSION);
}
}
return null;
}
private List<ServiceInstance> filteredByVersion(List<ServiceInstance> instances, String version) {
if (!StringUtils.hasText(version)) {
version = Constont.DEFAULT_VERSION;
}
List<ServiceInstance> filteredInstances = new ArrayList<>();
List<ServiceInstance> defaultVersionInstances = new ArrayList<>();
for (ServiceInstance serviceInstance : instances) {
if (serviceInstance.getMetadata().getOrDefault(Constont.VERSION, "").equals(version)) {
filteredInstances.add(serviceInstance);
}
if (serviceInstance.getMetadata().getOrDefault(Constont.VERSION, "").equals(Constont.DEFAULT_VERSION)) {
defaultVersionInstances.add(serviceInstance);
}
}
if (filteredInstances.size() > 0) {
return filteredInstances;
}
return defaultVersionInstances;
}
}
其中的filteredByVersion就是我們的選擇例項的策略
新建檔案啟用這個策略
@LoadBalancerClients(defaultConfiguration = MulEnvSupportConfiguration.class)
public class MulEnvSupportConfiguration {
@Bean
public ServiceInstanceListSupplier MulEnvServiceInstanceListSupplier(
ConfigurableApplicationContext context) {
ServiceInstanceListSupplier base = ServiceInstanceListSupplier.builder().withBlockingDiscoveryClient().build(context);
MulEnvServiceInstanceListSupplier MulEnv = new MulEnvServiceInstanceListSupplier(base);
return ServiceInstanceListSupplier.builder().withBase(MulEnv).build(context);
}
}
三、驗證
我們在user服務寫一個測試介面,介面邏輯是返回本例項的“version”。
@Slf4j
@RestController
public class Controller {
@Autowired
private Environment environment;
@Autowired
private HttpServletRequest httpServletRequest;
String VERSION = "version";
@GetMapping("/demo")
public String demo(){
String header = httpServletRequest.getHeader(VERSION);
log.info("headerVersion:{}",header);
return "user:"+environment.getProperty(VERSION);
}
}
然後在order服務寫一個demo介面,去呼叫user介面。同時返回本例項的“version”。
@RestController
public class Controller {
@Autowired
private UserSerivce userSerivce;
@Autowired
private Environment environment;
@GetMapping("/demo")
public String Demo(){
String order = "order:" + environment.getProperty(Constont.VERSION);
return order+"/"+userSerivce.demo();
}
}
打包+啟動服務
mvn clean install -DskipTests
nohup java -jar -Dserver.port=8761 eureka/target/eureka-0.0.1-SNAPSHOT.jar >null 2>&1 &
nohup java -jar -Dserver.port=5000 gateway/target/gateway-0.0.1-SNAPSHOT.jar >null 2>&1 &
nohup java -jar -Dserver.port=8001 order/target/order-0.0.1-SNAPSHOT.jar >null 2>&1 &
nohup java -jar -Dversion=v1 -Dserver.port=8002 order/target/order-0.0.1-SNAPSHOT.jar >null 2>&1 &
nohup java -jar -Dversion=v2 -Dserver.port=8003 order/target/order-0.0.1-SNAPSHOT.jar >null 2>&1 &
nohup java -jar -Dserver.port=9001 user/target/user-0.0.1-SNAPSHOT.jar >null 2>&1 &
nohup java -jar -Dversion=v1 -Dserver.port=9002 user/target/user-0.0.1-SNAPSHOT.jar >null 2>&1 &
正常訪問請求
帶上v1的版本號後
帶上v2的版本號後
而且請求返回結果是固定的,不是輪訓default和v1版本的。
四、多環境治理的MQ問題
我們可以在微服務呼叫例項時編寫自己的策略,實現後端的多版本控制。
但是mq消費的時候我們沒法編寫消費策略,這樣多個版本的訊息就混雜消費了,做不到版本隔離了。
下一篇文章會教大家解決多環境治理的mq問題。
五、程式碼地址:
關注“從零開始的it轉行生”,回覆“多環境”獲取