灰度發佈淺析

定義

灰度發佈就是已一種平滑過渡的方式來發布,通過切換線上新舊版本之間的路由權重,逐步從舊版本切換到新版本;比如要上線新功能,首先只是更新少量的服務節點,通過路由權重,讓少部分用戶體驗新版本,如果沒有什麼問題,再更新所有服務節點;這樣可以在出現問題把影響面降到最低,保證了系統的穩定性。

灰度發佈

一個系統往往有接入層比如nginx(Openresty),網關層比如zuul,以及服務層比如各種rpc框架;在這幾層都有路由功能,也就是說這幾層都可以做灰度;接入層可以使用nginx+lua來實現灰度,網關層zuul可以結合ribbon來實現灰度,rpc框架如dubbo本身提供了路由功能可以直接做灰度處理;下面看看具體如何去實現;

接入層灰度

接入層我們這裏使用功能更強大的Openresty,然後使用lua進行路由轉發,相關的路由策略可以配置在分佈式緩存redis裏面,當然也可以持久化到數據庫裏面;

  • 準備

準備一臺Openresty,兩臺web服務器tomcat(端口分別是8081,8082),以及redis;爲了方便模擬在redis裏面配置白名單,如果在白名單裏面就走8082,不在則走8081;

  • Openresty配置

需要在Openresty中配置支持lua,以及相關路由的lua腳本,nginx.conf配置如下:

http {
    ...
    lua_package_path "/lualib/?.lua;;";  #lua 模塊  
    lua_package_cpath "/lualib/?.so;;";  #c模塊   
    
    upstream tomcat1 {
      server 127.0.0.1:8081;
    }
    upstream tomcat2 {
      server 127.0.0.1:8082;
    }

    server {
        listen 80;
        server_name localhost;
        location / {
          content_by_lua_file lua/gray.lua;
        }
        location @tomcat1 {
          proxy_pass http://tomcat1;
        }
        location @tomcat2 {
          proxy_pass http://tomcat2;
        }
    }
    ...
}

配置了所有請求都會經過lua目錄下的gray.lua腳本,如下所示:

local redis = require "resty.redis";
local redis_obj = redis:new();
redis_obj:set_timeout(2000);
local ok,err = redis_obj:connect("127.0.0.1", 6379);

if not ok then
  ngx.say("failed to connect redis ",err);
  return;
end

--獲取請求ip
local_ip = ngx.var.remote_addr;

--redis中獲取白名單
local whitelist = redis_obj:get("whitelist");

--判斷是否在白名單然後轉到對應服務
if string.find(whitelist,local_ip) == nil then
  ngx.exec("@tomcat1");
else
  ngx.exec("@tomcat2");
end
local ok,err = redis_obj:close();

Openresty內置的功能模塊可以直接連接redis,然後從redis裏面取出白名單,看當前的請求ip是否在白名單內,然後做簡單的路由功能;可以動態修改redis裏面的白名單,實時更新。

localhost:0>set whitelist 127.0.0.1
"OK"
localhost:0>get whitelist
"127.0.0.1"
  • 啓動測試

分別啓動tomcat1,tomcat2以及Openresty,訪問http://localhost即可,可以動態修改redis裏面的白名單,然後訪問查看結果驗證。

網關層灰度

網關層已zuul爲例,zuul的灰度需要修改ribbon的負載策略,就是根據eureka的metadata進行自定義元數據,然後修改ribbon的策略規則;

  • 準備

測試服務分別準備兩臺端口分別爲:8765,8766,application.yml配置如下:

server:
  port: 8765
eureka:
  instance:
    metadata-map:
      route: 1

同時準備請求地址/hiGray,返回值爲route1;

server:
  port: 8766
eureka:
  instance:
    metadata-map:
      route: 2

同時準備請求地址/hiGray,返回值爲route2;用於區分是否走了灰度服務器;然後在zuul端需要引入一個插件:

<dependency>
    <groupId>io.jmnarloch</groupId>
    <artifactId>ribbon-discovery-filter-spring-cloud-starter</artifactId>
    <version>2.1.0</version>
</dependency>

然後需要準備一個pre類型的filter,具體如下:

@Configuration
public class GrayFilter extends ZuulFilter {

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        String ip = request.getRemoteAddr();
        //ipv6本地地址,也就是127.0.0.1
        if ("0:0:0:0:0:0:0:1".equals(ip)) {
            RibbonFilterContextHolder.getCurrentContext()
                    .add("route", "1");
        }  else {
            RibbonFilterContextHolder.getCurrentContext()
                    .add("route", "2");
        }
        return null;
    }
    ...
}

以上也是使用白名單爲例子,這裏爲了方便就沒有把白名單配置在redis裏面,配置的白名單地址爲ipv6:0:0:0:0:0:0:0:1,如果是白名單地址則路由到8765端口服務,否則爲8766端口服務;

  • 測試

分別啓動eureka-server,兩個eureka-client,以及zuul網關,訪問網關地址即可;分別通過127.0.0.1和本地ip訪問即可測試;

服務層灰度

服務器已rpc框架dubbo爲例,dubbo本身提供了各種路由規則包括:條件路由,腳本路由等,這裏同樣使用腳本路由爲例,腳本路由規則支持JDK 腳本引擎的所有腳本,比如:javascript, jruby, groovy 等,這裏使用缺省的JavaScript爲例;

  • 準備

註冊中心zookeeper,兩臺Provider可以在本地分別指定端口爲20881和20882,消費者,以及下面重點介紹的路由腳本:

function gray_rule(invokers, context) {
    var tag = context.getAttachment("tag");
    
    var result = new java.util.ArrayList(invokers.size());
    if(tag == "gray"){
        for (i = 0; i < invokers.size(); i ++) {
            if (invokers.get(i).getUrl().getPort()==20881) {
                result.add(invokers.get(i));
            }
        }
    } else {
        for (i = 0; i < invokers.size(); i ++) {
            if (invokers.get(i).getUrl().getPort()==20882) {
                result.add(invokers.get(i));
            }
        }
    }
    return result;
} (invokers,context)

dubbo在運行腳本的時候會傳入三個參數分別是:invokers,Invocation以及RpcContext.getContext();通過在消費端在RpcContext中設置tag:

RpcContext.getContext().setAttachment("tag", "gray");

這樣就可以在腳本中進行判斷,tag爲gray的消費端才走20881端口的服務端,其餘走20882服務端;
以上的腳本需要註冊到zookeeper中,手動註冊代碼如下,當然也可以使用dubbo提供的dubbo-admin來設置路由腳本:

URL registryUrl = URL.valueOf("zookeeper://127.0.0.1:2181");
ZookeeperRegistryFactory zookeeperRegistryFactory = new ZookeeperRegistryFactory();
zookeeperRegistryFactory.setZookeeperTransporter(new CuratorZookeeperTransporter());
Registry zookeeperRegistry = (ZookeeperRegistry) zookeeperRegistryFactory.createRegistry(registryUrl);
URL routerURL = URL.valueOf("script://0.0.0.0/com.dubboApi.DemoService?category=routers&dynamic=false");
routerURL = routerURL.addParameter("rule",
URL.encode("(..JavaScript腳本..)"));
zookeeperRegistry.register(routerURL); // 註冊

具體可以參考官方文檔:舊路由規則

  • 測試

啓動zookeeper,然後分別啓動兩臺生產者,啓動消費者時通過修改tag然後觀察路由;

總結

本文分別從接入層,網關層,服務層這三層簡要的介紹了通過路由規則來實現灰度發佈;已每層比較典型的中間件來介紹具體如何去實現簡單的灰度發佈;總體來說就是使用中間件的路由功能,動態加載外部自定義的一些路由策略腳本,以此來達到灰度發佈的目的。

代碼地址

Dubbo
Spring-Cloud

感謝關注

可以關注微信公衆號 回滾吧代碼 ,第一時間閱讀,文章持續更新;專注Java源碼、架構、算法和麪試
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章