灰度發佈就是已一種平滑過渡的方式來發布,經過切換線上新舊版本之間的路由權重,逐步從舊版本切換到新版本;好比要上線新功能,首先只是更新少許的服務節點,經過路由權重,讓少部分用戶體驗新版本,若是沒有什麼問題,再更新全部服務節點;這樣能夠在出現問題把影響面降到最低,保證了系統的穩定性。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中配置支持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而後觀察路由;
本文分別從接入層,網關層,服務層這三層簡要的介紹了經過路由規則來實現灰度發佈;已每層比較典型的中間件來介紹具體如何去實現簡單的灰度發佈;整體來講就是使用中間件的路由功能,動態加載外部自定義的一些路由策略腳本,以此來達到灰度發佈的目的。
能夠關注微信公衆號「 回滾吧代碼」,第一時間閱讀,文章持續更新;專一Java源碼、架構、算法和麪試。