後端多環境治理的實踐(一)

背景

最近有個業務場景,須要作一個新舊數據的兼容。大體能夠理解爲以前保存到數據庫的數據是一個字符串,因爲業務調整,該字符串要變爲一個json。前端

新的代碼須要判斷該字段是否爲json,若是是json則序列化爲json,若是不是json,則該字符串爲json的某個字段。java

邏輯簡單,我發佈給測試後,測試問我要怎麼測試,我說須要用舊的數據才能測試這段邏輯,可是我發佈了新的代碼後,就不能產生舊的的數據了。spring

數據流以下圖:數據庫

image

測試說這樣很難測試,能不能像前端同窗同樣,搞個多版本控制,一鍵切換版本。json

測試想要的效果目標以下圖:後端

image

我在以前的公司也常常遇到這種場景,可是我通常都叫測試修改代碼的版本,先發布舊的代碼而後生產數據,而後切換到新的版本去驗證這種場景。多線程

這個時候,同事推薦我使用公司的基建服務「多環境治理平臺」。app

1、什麼是多環境治理

在公司內部,通常是多個功能一塊兒開發,同一個微服務並行開發是時常發生的事。可是功能的上線時間多是不一樣的,因此代碼不能合併在同一個分支開發。框架

提測的時候,因爲測試環境只有一個,要不就是都合併到同一個分支,要不就排隊測試。。。ide

image

大夥一塊兒來測試吧

image

測試人員在排隊使用測試環境

合併到一塊兒測試的話,代碼會衝突,並且會致使測試環境與線上環境不一致(由於測試環境混雜了其餘版本的代碼)。

分開測試的話會致使排隊現場,阻塞嚴重。

多環境治理就是爲了解決這個問題****。

一套測試環境,多個後端版本。

測試人員能夠選擇隨意切換後端版本,隨意測試任意一個版本的後端的功能。

2、多環境治理的原理

假設如今有2個featrue功能在開發

featrue1須要修改user和score微服務。

featrue2須要修改user和order微服務。

咱們但願最後的流量調度以下圖。

image

v1的流量優先調用v1版本的微服務,若是找不到v1版本的微服務時,要調用基準版本的微服務。(例如order)

v2的流量優先調用v2版本的微服務,若是找不到v2版本的微服務時,要調用基準版本的微服務。(例如score)

要實現以上流量調度,只要作三件事:

一、**每一個微服務註冊到註冊中心的時候,要帶上一個標記,標記本身當前的版本。

二、**每一個請求都要帶個版本號,並且這個版本號要由網關開始,一直透穿到下游。

三、微服務的調用下游時,實例選擇策略修改成「優先選擇和流量版本相同的實例,若是沒有該版本的實例,則選擇基準版本的實例」。

多環境治理還能低成本搭建預發佈環境(不需要所有應用都發布一遍pre環境)。

調整一下策略,

根據租戶ID選擇實例,就能實現後端租戶ID級別的灰度發佈

根據userID選擇實例,就能實現後端userID級別的灰度發佈

3、多環境治理的實踐

上面說的都是公司給我提供的基建服務,並且是用go語言寫的。

文章前面的小夥伴可能不在大公司,沒有這樣的基建平臺,因此這裏我根據上面說的原理,本身用java,基於springcloud 作一遍樣例給你們。

你們能夠參考我樣子,而後基於本身公司的微服務框架增長系統的多環境治理能力。

下面的代碼例子只會貼出最核心的代碼,詳細的實踐能夠下載個人代碼本身細看。

1、演示工程目錄

image

最終的效果以下:

一、通常的請求會走基準環境的代碼。

二、請求header裏面只要帶version=v1,則調用v1版本的order和user代碼。

三、請求header裏面只要帶version=v2,則調用v2版本的order和基準版本的user代碼。

image

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就是咱們的選擇實例的策略

image

新建文件啓用這個策略

@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 &

image

正常訪問請求

image

帶上v1的版本號後

image

帶上v2的版本號後

image

並且請求返回結果是固定的,不是輪訓default和v1版本的。

4、多環境治理的MQ問題

咱們能夠在微服務調用實例時編寫本身的策略,實現後端的多版本控制。

可是mq消費的時候咱們無法編寫消費策略,這樣多個版本的消息就混雜消費了,作不到版本隔離了。

下一篇文章會教你們解決多環境治理的mq問題。

5、代碼地址:

關注「從零開始的it轉行生」,回覆「多環境」獲取

相關文章
相關標籤/搜索