灰度發佈淺析

定義

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

灰度發佈

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

接入層灰度

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

  • 準備

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

  • Openresty配置

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

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腳本,以下所示:web

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裏面的白名單,而後訪問查看結果驗證。redis

網關層灰度

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

  • 準備

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

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源碼、架構、算法和麪試。
相關文章
相關標籤/搜索