最近有個業務場景,須要作一個新舊數據的兼容。大體能夠理解爲以前保存到數據庫的數據是一個字符串,因爲業務調整,該字符串要變爲一個json。前端
新的代碼須要判斷該字段是否爲json,若是是json則序列化爲json,若是不是json,則該字符串爲json的某個字段。java
邏輯簡單,我發佈給測試後,測試問我要怎麼測試,我說須要用舊的數據才能測試這段邏輯,可是我發佈了新的代碼後,就不能產生舊的的數據了。spring
數據流以下圖:數據庫
測試說這樣很難測試,能不能像前端同窗同樣,搞個多版本控制,一鍵切換版本。json
測試想要的效果目標以下圖:後端
我在以前的公司也常常遇到這種場景,可是我通常都叫測試修改代碼的版本,先發布舊的代碼而後生產數據,而後切換到新的版本去驗證這種場景。多線程
這個時候,同事推薦我使用公司的基建服務「多環境治理平臺」。app
在公司內部,通常是多個功能一塊兒開發,同一個微服務並行開發是時常發生的事。可是功能的上線時間多是不一樣的,因此代碼不能合併在同一個分支開發。框架
提測的時候,因爲測試環境只有一個,要不就是都合併到同一個分支,要不就排隊測試。。。ide
大夥一塊兒來測試吧
測試人員在排隊使用測試環境
合併到一塊兒測試的話,代碼會衝突,並且會致使測試環境與線上環境不一致(由於測試環境混雜了其餘版本的代碼)。
分開測試的話會致使排隊現場,阻塞嚴重。
多環境治理就是爲了解決這個問題****。
一套測試環境,多個後端版本。
測試人員能夠選擇隨意切換後端版本,隨意測試任意一個版本的後端的功能。
假設如今有2個featrue功能在開發
featrue1須要修改user和score微服務。
featrue2須要修改user和order微服務。
咱們但願最後的流量調度以下圖。
v1的流量優先調用v1版本的微服務,若是找不到v1版本的微服務時,要調用基準版本的微服務。(例如order)
v2的流量優先調用v2版本的微服務,若是找不到v2版本的微服務時,要調用基準版本的微服務。(例如score)
要實現以上流量調度,只要作三件事:
一、**每一個微服務註冊到註冊中心的時候,要帶上一個標記,標記本身當前的版本。
二、**每一個請求都要帶個版本號,並且這個版本號要由網關開始,一直透穿到下游。
三、微服務的調用下游時,實例選擇策略修改成「優先選擇和流量版本相同的實例,若是沒有該版本的實例,則選擇基準版本的實例」。
多環境治理還能低成本搭建預發佈環境(不需要所有應用都發布一遍pre環境)。
調整一下策略,
根據租戶ID選擇實例,就能實現後端租戶ID級別的灰度發佈。
根據userID選擇實例,就能實現後端userID級別的灰度發佈。
上面說的都是公司給我提供的基建服務,並且是用go語言寫的。
文章前面的小夥伴可能不在大公司,沒有這樣的基建平臺,因此這裏我根據上面說的原理,本身用java,基於springcloud 作一遍樣例給你們。
你們能夠參考我樣子,而後基於本身公司的微服務框架增長系統的多環境治理能力。
下面的代碼例子只會貼出最核心的代碼,詳細的實踐能夠下載個人代碼本身細看。
1、演示工程目錄
最終的效果以下:
一、通常的請求會走基準環境的代碼。
二、請求header裏面只要帶version=v1,則調用v1版本的order和user代碼。
三、請求header裏面只要帶version=v2,則調用v2版本的order和基準版本的user代碼。
2、工程搭建
如下代碼基於springcloud-2020.03版本。
(ps:真的感概技術升級太快,以前還在用zuul、ribbon、hystrix,如今基本都升級換代了。因此你們最重要的是懂原理,代碼實踐這些可能過一段時間就不能直接用了。)
一、每一個微服務註冊都註冊中心的時候,要帶上一個標記,標記本身當前的版本。
註冊到springcloud的eureka時,註冊中心容許實例帶個一個map的信息。
在order、user服務加上配置。
eureka.instance.metadata-map.version=${version}
只要加上這個配置,就代表這個實例的"version"字段是「default」。
二、每一個請求都要帶個版本號,並且這個版本號要由網關開始,一直透穿到下游。
爲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) { } }
三、微服務的調用下游時,策略修改成「優先選擇和流量版本相同的實例,若是沒有該版本的實例,則選擇基準版本的實例」。
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); } }
3、驗證
咱們在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問題。
關注「從零開始的it轉行生」,回覆「多環境」獲取