vivo 微服務(wù) API 網(wǎng)關(guān)架構(gòu)實(shí)踐

一、背景介紹

網(wǎng)關(guān)作為微服務(wù)生態(tài)中的重要一環(huán)懂鸵,由于歷史原因检柬,中間件團(tuán)隊(duì)沒有統(tǒng)一的微服務(wù)API網(wǎng)關(guān)藕咏,為此準(zhǔn)備技術(shù)預(yù)研打造一個(gè)功能齊全、可用性高的業(yè)務(wù)網(wǎng)關(guān)衣撬。

二酿联、技術(shù)選型

常見的開源網(wǎng)關(guān)按照語言分類有如下幾類:

  • Nginx+Lua:OpenResty终息、Kong 等夺巩;
  • Java:Zuul1/Zuul2、Spring Cloud Gateway周崭、gravitee-gateway柳譬、Dromara Soul 等;
  • Go:janus续镇、GoKu API Gateway 等美澳;
  • Node.js:Express Gateway、MicroGateway 等摸航。

由于團(tuán)隊(duì)內(nèi)成員基本上為Java技術(shù)棧制跟,因此并不打算深入研究非Java語言的網(wǎng)關(guān)。接下來我們主要調(diào)研了Zuul1酱虎、Zuul2雨膨、Spring Cloud Gateway、Dromara Soul逢净。

業(yè)界主流的網(wǎng)關(guān)基本上可以分為下面三種:

  • Servlet + 線程池
  • NIO(Tomcat / Jetty) + Servlet 3.0 異步
  • NettyServer + NettyClient

在進(jìn)行技術(shù)選型的時(shí)候哥放,主要考慮功能豐富度、性能爹土、穩(wěn)定性甥雕。在反復(fù)對(duì)比之后,決定選擇基于Netty框架進(jìn)行網(wǎng)關(guān)開發(fā)胀茵;但是考慮到時(shí)間的緊迫性社露,最終選擇為針對(duì) Zuul2 進(jìn)行定制化開發(fā),在 Zuul2 的代碼骨架之上去完善網(wǎng)關(guān)的整個(gè)體系琼娘。

三峭弟、Zuul2 介紹

接下來我們簡(jiǎn)要介紹一下 Zuul2 關(guān)鍵知識(shí)點(diǎn)。

Zuul2 的架構(gòu)圖:

01.png

為了解釋上面這張圖脱拼,接下來會(huì)分別介紹幾個(gè)點(diǎn)

  • 如何解析 HTTP 協(xié)議
  • Zuul2 的數(shù)據(jù)流轉(zhuǎn)
  • 兩個(gè)責(zé)任鏈:Netty ChannelPipeline責(zé)任鏈 + Filter責(zé)任鏈

3.1 如何解析 HTTP 協(xié)議

學(xué)習(xí)Zuul2需要一定的鋪墊知識(shí)瞒瘸,比如:Google Guice、RxJava熄浓、Netflix archaius等情臭,但是更關(guān)鍵的應(yīng)該是:如何解析HTTP協(xié)議,會(huì)影響到后續(xù)Filter責(zé)任鏈的原理解析赌蔑,為此先分析這個(gè)關(guān)鍵點(diǎn)俯在。

首先我們介紹官方文檔中的一段話:

By default Zuul doesn't buffer body content, meaning it streams the received headers to the origin before the body has been received.

This streaming behavior is very efficient and desirable, as long as your filter logic depends on header data.

翻譯成中文:

默認(rèn)情況下Zuul2并不會(huì)緩存請(qǐng)求體,也就意味著它可能會(huì)先發(fā)送接收到的請(qǐng)求Headers到后端服務(wù)娃惯,之后接收到請(qǐng)求體再繼續(xù)發(fā)送到后端服務(wù)跷乐,發(fā)送請(qǐng)求體的時(shí)候,也不是組裝為一個(gè)完整數(shù)據(jù)之后才發(fā)趾浅,而是接收到一部分愕提,就轉(zhuǎn)發(fā)一部分馒稍。

這個(gè)流式行為是高效的,只要Filter過濾的時(shí)候只依賴Headers的數(shù)據(jù)進(jìn)行邏輯處理浅侨,而不需要解析RequestBody筷黔。

上面這段話映射到Netty Handler中,則意味著Zuul2并沒有使用HttpObjectAggregator仗颈。

我們先看一下常規(guī)的Netty Server處理HTTP協(xié)議的樣例:

NettyServer樣例

@Slf4j
public class ConfigServerBootstrap {
 
    public static final int WORKER_THREAD_COUNT = Runtime.getRuntime().availableProcessors();
 
    public void start(){
        int port = 8080;
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup(WORKER_THREAD_COUNT);
 
        final BizServerHandler bizServerHandler = new BizServerHandler();
 
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
 
            serverBootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<Channel>() {
                        @Override
                        protected void initChannel(Channel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast(new IdleStateHandler(10, 10, 0));
                            pipeline.addLast(new HttpServerCodec());
                            pipeline.addLast(new HttpObjectAggregator(500 * 1024 * 1024));
                            pipeline.addLast(bizServerHandler);
                        }
                    });
            log.info("start netty server, port:{}", port);
            serverBootstrap.bind(port).sync();
        } catch (InterruptedException e) {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
            log.error(String.format("start netty server error, port:%s", port), e);
        }
    }
}

這個(gè)例子中的兩個(gè)關(guān)鍵類為:HttpServerCodec佛舱、HttpObjectAggregator。

HttpServerCodec是HttpRequestDecoder挨决、HttpResponseEncoder的組合器请祖。

  • HttpRequestDecoder職責(zé):將輸入的ByteBuf解析成HttpRequest、HttpContent對(duì)象脖祈。
  • HttpResponseEncoder職責(zé):將HttpResponse肆捕、HttpContent對(duì)象轉(zhuǎn)換為ByteBuf,進(jìn)行網(wǎng)絡(luò)二進(jìn)制流的輸出盖高。

HttpObjectAggregator的作用:組裝HttpMessage慎陵、HttpContent為一個(gè)完整的FullHttpRequest或者FullHttpResponse。

當(dāng)你不想關(guān)心chunked分塊傳輸?shù)臅r(shí)候喻奥,使用HttpObjectAggregator是非常有用的席纽。

HTTP協(xié)議通常使用Content-Length來標(biāo)識(shí)body的長度,在服務(wù)器端撞蚕,需要先申請(qǐng)對(duì)應(yīng)長度的buffer润梯,然后再賦值。如果需要一邊生產(chǎn)數(shù)據(jù)一邊發(fā)送數(shù)據(jù)甥厦,就需要使用"Transfer-Encoding: chunked" 來代替Content-Length纺铭,也就是對(duì)數(shù)據(jù)進(jìn)行分塊傳輸。

接下來我們看一下Zuul2為了解析HTTP協(xié)議做了哪些處理刀疙。

Zuul的源碼:https://github.com/Netflix/zuul舶赔,基于v2.1.5。

// com.netflix.zuul.netty.server.BaseZuulChannelInitializer#addHttp1Handlers
protected void addHttp1Handlers(ChannelPipeline pipeline) {
    pipeline.addLast(HTTP_CODEC_HANDLER_NAME, createHttpServerCodec());
 
    pipeline.addLast(new Http1ConnectionCloseHandler(connCloseDelay));
    pipeline.addLast("conn_expiry_handler",
            new Http1ConnectionExpiryHandler(maxRequestsPerConnection, maxRequestsPerConnectionInBrownout, connectionExpiry));
}
// com.netflix.zuul.netty.server.BaseZuulChannelInitializer#createHttpServerCodec
protected HttpServerCodec createHttpServerCodec() {
    return new HttpServerCodec(
            MAX_INITIAL_LINE_LENGTH.get(),
            MAX_HEADER_SIZE.get(),
            MAX_CHUNK_SIZE.get(),
            false
    );
}

通過對(duì)比上面的樣例發(fā)現(xiàn)谦秧,Zuul2并沒有添加HttpObjectAggregator竟纳,也就是需要自行去處理chunked分塊傳輸問題、自行組裝請(qǐng)求體數(shù)據(jù)油够。

為了解決上面說的chunked分塊傳輸問題蚁袭,Zuul2通過判斷是否LastHttpContent征懈,來判斷是否接收完成石咬。

3.2 Zuul2 數(shù)據(jù)流轉(zhuǎn)

image

image

如上圖所示,Netty自帶的HttpServerCodec會(huì)將網(wǎng)絡(luò)二進(jìn)制流轉(zhuǎn)換為Netty的HttpRequest對(duì)象卖哎,再通過ClientRequestReceiver編解碼器將HttpRequest轉(zhuǎn)換為Zuul的請(qǐng)求對(duì)象HttpRequestMessageImpl鬼悠;

請(qǐng)求體RequestBody在Netty自帶的HttpServerCodec中被映射為HttpContent對(duì)象删性,ClientRequestReceiver編解碼器依次接收HttpContent對(duì)象。

完成了上述數(shù)據(jù)的轉(zhuǎn)換之后焕窝,就流轉(zhuǎn)到了最重要的編解碼ZuulFilterChainHandler蹬挺,里面會(huì)執(zhí)行Filter鏈,也會(huì)發(fā)起網(wǎng)絡(luò)請(qǐng)求到真正的后端服務(wù)它掂,這一切都是在ZuulFilterChainHandler中完成的巴帮。

得到了后端服務(wù)的響應(yīng)結(jié)果之后,也經(jīng)過了Outbound Filter的過濾虐秋,接下來就是通過ClientResponseWriter把Zuul自定義的響應(yīng)對(duì)象HttpResponseMessageImpl轉(zhuǎn)換為Netty的HttpResponse對(duì)象榕茧,然后通過HttpServerCodec轉(zhuǎn)換為ByteBuf對(duì)象,發(fā)送網(wǎng)絡(luò)二進(jìn)制流客给,完成響應(yīng)結(jié)果的輸出用押。

這里需要特別說明的是:由于Zuul2默認(rèn)不組裝一個(gè)完整的請(qǐng)求對(duì)象/響應(yīng)對(duì)象,所以Zuul2是分別針對(duì)請(qǐng)求頭+請(qǐng)求Headers靶剑、請(qǐng)求體進(jìn)行Filter過濾攔截的蜻拨,也就是說對(duì)于請(qǐng)求,會(huì)走兩遍前置Filter鏈桩引,對(duì)于響應(yīng)結(jié)果缎讼,也是會(huì)走兩遍后置Filter鏈攔截。

3.3 兩個(gè)責(zé)任鏈

3.3.1 Netty ChannelPipeline責(zé)任鏈

Netty的ChannelPipeline設(shè)計(jì)坑匠,通過往ChannelPipeline中動(dòng)態(tài)增減Handler進(jìn)行定制擴(kuò)展休涤。

接下來看一下Zuul2 Netty Server中的pipeline有哪些Handler?

image

image

接著繼續(xù)看一下Zuul2 Netty Client的Handler有哪些笛辟?

image

image

本文不針對(duì)具體的Handler進(jìn)行詳細(xì)解釋功氨,主要是給大家一個(gè)整體的視圖。

3.3.2 Filter責(zé)任鏈

image

image

請(qǐng)求發(fā)送到Netty Server中手幢,先進(jìn)行Inbound Filters的攔截處理捷凄,接著會(huì)調(diào)用Endpoint Filter,這里默認(rèn)為ProxyEndPoint(里面封裝了Netty Client)围来,發(fā)送請(qǐng)求到真實(shí)后端服務(wù)跺涤,獲取到響應(yīng)結(jié)果之后,再執(zhí)行Outbound Filters监透,最終返回響應(yīng)結(jié)果桶错。

三種類型的Filter之間是通過nextStage屬性來銜接的。

Zuul2存在一個(gè)定時(shí)任務(wù)線程GroovyFilterFileManagerPoller胀蛮,定期掃描特定的目錄院刁,通過比對(duì)文件的更新時(shí)間戳,來判斷是否發(fā)生變化粪狼,如果有變化退腥,則重新編譯并放入到內(nèi)存中任岸。

通過定位任務(wù)實(shí)現(xiàn)了Filter的動(dòng)態(tài)加載。

四狡刘、功能介紹

上面介紹了Zuul2的部分知識(shí)點(diǎn)享潜,接下來介紹網(wǎng)關(guān)的整體功能。

image

image

4.1 服務(wù)注冊(cè)發(fā)現(xiàn)

網(wǎng)關(guān)承擔(dān)了請(qǐng)求轉(zhuǎn)發(fā)的功能嗅蔬,需要一定的方法用于動(dòng)態(tài)發(fā)現(xiàn)后端服務(wù)的機(jī)器列表剑按。

這里提供兩種方式進(jìn)行服務(wù)的注冊(cè)發(fā)現(xiàn):

集成網(wǎng)關(guān)SDK

  • 網(wǎng)關(guān)SDK會(huì)在服務(wù)啟動(dòng)之后,監(jiān)聽ContextRefreshedEvent事件澜术,主動(dòng)操作zk登記信息到zookeeper注冊(cè)中心吕座,這樣網(wǎng)關(guān)服務(wù)、網(wǎng)關(guān)管理后臺(tái)就可以訂閱節(jié)點(diǎn)信息瘪板。
  • 網(wǎng)關(guān)SDK添加了ShutdownHook吴趴,在服務(wù)下線的時(shí)候,會(huì)刪除登記在zk的節(jié)點(diǎn)信息侮攀,用于通知網(wǎng)關(guān)服務(wù)锣枝、網(wǎng)關(guān)管理后臺(tái),節(jié)點(diǎn)已下線兰英。

手工配置服務(wù)的機(jī)器節(jié)點(diǎn)信息

  • 在網(wǎng)關(guān)管理后臺(tái)撇叁,手工添加、刪除機(jī)器節(jié)點(diǎn)畦贸。
  • 在網(wǎng)關(guān)管理后臺(tái)陨闹,手工設(shè)置節(jié)點(diǎn)上線、節(jié)點(diǎn)下線操薄坏。

為了防止zookeeper故障趋厉,網(wǎng)關(guān)管理后臺(tái)已提供HTTP接口用于注冊(cè)、取消注冊(cè)作為兜底措施胶坠。

4.2 動(dòng)態(tài)路由

動(dòng)態(tài)路由分為:機(jī)房就近路由君账、灰度路由(類似于Dubbo的標(biāo)簽路由功能)。

  • 機(jī)房就近路由:請(qǐng)求最好是不要跨機(jī)房沈善,比如請(qǐng)求打到網(wǎng)關(guān)服務(wù)的X機(jī)房乡数,那么也應(yīng)該是將請(qǐng)求轉(zhuǎn)發(fā)給X機(jī)房的后端服務(wù)節(jié)點(diǎn),如果后端服務(wù)不存在X機(jī)房的節(jié)點(diǎn)闻牡,則請(qǐng)求到其他機(jī)房的節(jié)點(diǎn)净赴。
  • 灰度路由:類似于Dubbo的標(biāo)簽路由功能,如果希望對(duì)后端服務(wù)節(jié)點(diǎn)進(jìn)行分組隔離罩润,則需要給后端服務(wù)一個(gè)標(biāo)簽名玖翅,建立"標(biāo)簽名→節(jié)點(diǎn)列表"的映射關(guān)系,請(qǐng)求方攜帶這個(gè)標(biāo)簽名,請(qǐng)求到相應(yīng)的后端服務(wù)節(jié)點(diǎn)烧栋。

網(wǎng)關(guān)管理后臺(tái)支持動(dòng)態(tài)配置路由信息,動(dòng)態(tài)開啟/關(guān)閉路由功能拳球。

4.3 負(fù)載均衡

當(dāng)前支持的負(fù)載均衡策略:加權(quán)隨機(jī)算法审姓、加權(quán)輪詢算法、一致性哈希算法祝峻。

可以通過網(wǎng)關(guān)管理后臺(tái)動(dòng)態(tài)調(diào)整負(fù)載均衡策略魔吐,支持API接口級(jí)別、應(yīng)用級(jí)別的配置莱找。

負(fù)載均衡機(jī)制并未采用Netflix Ribbon酬姆,而是仿造Dubbo負(fù)載均衡的算法實(shí)現(xiàn)的。

4.4 動(dòng)態(tài)配置

API網(wǎng)關(guān)支持一套自洽的動(dòng)態(tài)配置功能奥溺,在不依賴第三方配置中心的條件下辞色,仍然支持實(shí)時(shí)調(diào)整配置項(xiàng),并且配置項(xiàng)分為全局配置浮定、應(yīng)用級(jí)別治理配置相满、API接口級(jí)別治理配置。

在自洽的動(dòng)態(tài)配置功能之外桦卒,網(wǎng)關(guān)服務(wù)也與公司級(jí)別的配置中心進(jìn)行打通立美,支持公司級(jí)配置中心配置相應(yīng)的配置項(xiàng)。

4.5 API管理

API管理支持網(wǎng)關(guān)SDK自動(dòng)掃描上報(bào)方灾,也支持在管理后臺(tái)手工配置建蹄。

4.6 協(xié)議轉(zhuǎn)換

后端的服務(wù)有很多是基于Dubbo框架的,網(wǎng)關(guān)服務(wù)支持HTTP→HTTP的請(qǐng)求轉(zhuǎn)發(fā)裕偿,也支持HTTP→Dubbo的協(xié)議轉(zhuǎn)換洞慎。

同時(shí)C++技術(shù)棧,采用了tars框架嘿棘,網(wǎng)關(guān)服務(wù)也支持HTTP → tras協(xié)議轉(zhuǎn)換拢蛋。

4.7 安全機(jī)制

API網(wǎng)關(guān)提供了IP黑白名單、OAuth認(rèn)證授權(quán)蔫巩、appKey&appSecret驗(yàn)簽谆棱、矛盾加解密、vivo登錄態(tài)校驗(yàn)的功能圆仔。

4.8 監(jiān)控/告警

API網(wǎng)關(guān)通過對(duì)接通用監(jiān)控上報(bào)請(qǐng)求訪問信息垃瞧,對(duì)API接口的QPS、請(qǐng)求響應(yīng)嗎坪郭、請(qǐng)求響應(yīng)時(shí)間等進(jìn)行監(jiān)控與告警个从;

通過對(duì)接基礎(chǔ)監(jiān)控,對(duì)網(wǎng)關(guān)服務(wù)自身節(jié)點(diǎn)進(jìn)行CPU、IO嗦锐、內(nèi)存嫌松、網(wǎng)絡(luò)連接等數(shù)據(jù)進(jìn)行監(jiān)控。

4.9 限流/熔斷

API網(wǎng)關(guān)與限流熔斷系統(tǒng)進(jìn)行打通奕污,可以在限流熔斷系統(tǒng)進(jìn)行API接口級(jí)別的配置萎羔,比如熔斷配置、限流配置碳默,而無需業(yè)務(wù)系統(tǒng)再次對(duì)接限流熔斷組件贾陷。

限流熔斷系統(tǒng)提供了對(duì)Netflix Hystrix、Alibaba Sentinel組件的封裝嘱根。

4.10 無損發(fā)布

業(yè)務(wù)系統(tǒng)的無損發(fā)布髓废,這里分為兩種場(chǎng)景介紹:

  • 集成了網(wǎng)關(guān)SDK:網(wǎng)關(guān)SDK添加了ShutdownHook,會(huì)主動(dòng)從zookeeper刪除登記的節(jié)點(diǎn)信息该抒,從而避免請(qǐng)求打到即將下線的節(jié)點(diǎn)慌洪。
  • 未集成網(wǎng)關(guān)SDK:如果什么都不做,則只能依賴網(wǎng)關(guān)服務(wù)的心跳檢測(cè)功能凑保,會(huì)有15s的流量損失蒋譬。慶幸的是管理后臺(tái)提供了流量摘除、流量恢復(fù)的操作按鈕愉适,支持動(dòng)態(tài)的上線犯助、下線機(jī)器節(jié)點(diǎn)。

網(wǎng)關(guān)集群的無損發(fā)布:我們考慮了后端服務(wù)的無損發(fā)布维咸,但是也需要考慮網(wǎng)關(guān)節(jié)點(diǎn)自身的無損發(fā)布剂买,這里我們不再重復(fù)造輪子,直接使用的是CICD系統(tǒng)的HTTP無損發(fā)布功能(Nginx動(dòng)態(tài)摘除/上線節(jié)點(diǎn))癌蓖。

4.11 網(wǎng)關(guān)集群分組隔離

網(wǎng)關(guān)集群的分組隔離指的是業(yè)務(wù)與業(yè)務(wù)之間的請(qǐng)求應(yīng)該是隔離的瞬哼,不應(yīng)該被部分業(yè)務(wù)請(qǐng)求打垮了網(wǎng)關(guān)服務(wù),從而導(dǎo)致了別的業(yè)務(wù)請(qǐng)求無法處理租副。

這里我們會(huì)對(duì)接入網(wǎng)關(guān)的業(yè)務(wù)進(jìn)行分組歸類坐慰,不同的業(yè)務(wù)使用不同的分組,不同的網(wǎng)關(guān)分組用僧,會(huì)部署獨(dú)立的網(wǎng)關(guān)集群结胀,從而隔離了風(fēng)險(xiǎn),不用再擔(dān)心業(yè)務(wù)之間的互相影響责循。

五糟港、系統(tǒng)架構(gòu)

5.1 模塊交互圖

02.png

5.2 網(wǎng)關(guān)管理后臺(tái)

模塊劃分

image

image

5.3 通信機(jī)制

由于需要?jiǎng)討B(tài)的下發(fā)配置,比如全局開關(guān)院仿、應(yīng)用級(jí)別的治理配置秸抚、接口級(jí)別的治理配置速和,就需要網(wǎng)關(guān)管理后臺(tái)可以與網(wǎng)關(guān)服務(wù)進(jìn)行通信,比如推拉模式剥汤。

兩種設(shè)計(jì)方案

  • 基于注冊(cè)中心的訂閱通知機(jī)制
  • 基于HTTP的推模式 + 定時(shí)拉取

這里并未采用第一種方案颠放,主要是因?yàn)橐韵氯秉c(diǎn):

  • 嚴(yán)重依賴zk集群的穩(wěn)定性
  • 信息不私密(zk集群權(quán)限管控能力較弱、擔(dān)心被誤刪)
  • 無法灰度下發(fā)配置吭敢,比如只對(duì)其中的一臺(tái)網(wǎng)關(guān)服務(wù)節(jié)點(diǎn)配置生效

5.3.1 基于HTTP的推模式

image

image

因?yàn)閆uul2本身就自帶了Netty Server碰凶,同理也可以再多啟動(dòng)一個(gè)Netty Server提供HTTP服務(wù),讓管理后臺(tái)發(fā)送HTTP請(qǐng)求到網(wǎng)關(guān)服務(wù)省有,進(jìn)而發(fā)送配置數(shù)據(jù)到網(wǎng)關(guān)服務(wù)了痒留。

所以圖上的藍(lán)色標(biāo)記Netty Server用于接收客戶端請(qǐng)求轉(zhuǎn)發(fā)到后端節(jié)點(diǎn)谴麦,紫色標(biāo)記Netty Server用于提供HTTP服務(wù)蠢沿,接收配置數(shù)據(jù)。

5.3.2 全量配置拉取

網(wǎng)關(guān)服務(wù)在啟動(dòng)之初匾效,需要發(fā)送HTTP請(qǐng)求到管理后臺(tái)拉取全部的配置數(shù)據(jù)舷蟀,并且也需要拉取歸屬當(dāng)前節(jié)點(diǎn)的灰度配置(只對(duì)這個(gè)節(jié)點(diǎn)生效的試驗(yàn)性配置)。

5.3.3 增量配置定時(shí)拉取

上面提到了"基于HTTP的推模式"進(jìn)行配置的動(dòng)態(tài)推送面哼,也介紹了全局配置拉取野宜,為了保險(xiǎn)起見,網(wǎng)關(guān)服務(wù)還是新增了一個(gè)定時(shí)任務(wù)魔策,用于定時(shí)拉取增量配置匈子。

可以理解為兜底操作,就好比配置中心支持長輪詢獲取數(shù)據(jù)實(shí)時(shí)變更+定時(shí)任務(wù)獲取全部數(shù)據(jù)闯袒。

在拉取到增量配置之后虎敦,會(huì)比對(duì)內(nèi)存中的配置數(shù)據(jù)是否一致,如果一致政敢,則不操作直接丟棄其徙。

5.3.4 灰度配置下發(fā)

上面也提到了"灰度配置"這個(gè)詞,這里詳細(xì)解釋一下什么是灰度配置喷户?

比如當(dāng)編輯了某個(gè)接口的限流信息唾那,希望在某個(gè)網(wǎng)關(guān)節(jié)點(diǎn)運(yùn)行一段時(shí)間,如果沒有問題褪尝,則調(diào)整配置讓全部的網(wǎng)關(guān)服務(wù)節(jié)點(diǎn)生效闹获,如果有問題,則也只是其中一個(gè)網(wǎng)關(guān)節(jié)點(diǎn)的請(qǐng)求流量出問題河哑。

這樣可以降低出錯(cuò)的概率昌罩,當(dāng)某個(gè)比較大的改動(dòng)或者版本上線的時(shí)候,可以控制灰度部署一臺(tái)機(jī)器灾馒,同時(shí)配置也只灰度到這臺(tái)機(jī)器茎用,這樣風(fēng)險(xiǎn)就降低了很多。

灰度配置:可以理解為只在某些網(wǎng)關(guān)節(jié)點(diǎn)生效的配置。

灰度配置下發(fā)其實(shí)也是通過"5.3.1基于HTTP的推模式"來進(jìn)行下發(fā)的轨功。

5.4 網(wǎng)關(guān)SDK

網(wǎng)關(guān)SDK旨在完成后端服務(wù)節(jié)點(diǎn)的注冊(cè)與下線旭斥、API接口列表數(shù)據(jù)上報(bào),通過接入網(wǎng)關(guān)SDK即可減少手工操作古涧。網(wǎng)關(guān)SDK通過 ZooKeeper client操作節(jié)點(diǎn)的注冊(cè)與下線垂券,通過發(fā)起HTTP請(qǐng)求進(jìn)行API接口數(shù)據(jù)的上報(bào)。

支持SpringMVC羡滑、SpringBoot的web接口自動(dòng)掃描菇爪、Dubbo新老版本的Service接口掃描。

Dubbo 接口上報(bào):

  • 舊版Dubbo:自定義BeanPostProcessor柒昏,用于提取到ServiceBean凳宙,放入線程池異步上報(bào)到網(wǎng)關(guān)后臺(tái)。
  • 新版Dubbo:自定義ApplicationListener职祷,用于監(jiān)聽ServiceBeanExportedEvent事件氏涩,提取event信息,上報(bào)到網(wǎng)關(guān)后臺(tái)有梆。

HTTP 接口上報(bào):

  • 自定義BeanPostProcessor是尖,用于提取到Controller、RestController的RequestMapping注解泥耀,放入線程池異步上報(bào)API信息饺汹。

六、改造之路

6.1 動(dòng)態(tài)配置

關(guān)聯(lián)知識(shí)點(diǎn):

Zuul2依賴的動(dòng)態(tài)配置為archaius痰催,通過擴(kuò)展ConcurrentMapConfiguration添加到ConcurrentCompositeConfiguration中兜辞。

新增GatewayConfigConfiguration,用于存儲(chǔ)全局配置陨囊、治理配置弦疮、節(jié)點(diǎn)信息、API數(shù)據(jù)等蜘醋。

@Singleton
public class GatewayConfigConfiguration extends ConcurrentMapConfiguration {
 
    public GatewayConfigConfiguration() {
        /**
         * 設(shè)置這個(gè)值為true胁塞,才可以避免archaius強(qiáng)行去除value的類型,導(dǎo)致獲取報(bào)錯(cuò)
         * see com.netflix.config.ConcurrentMapConfiguration#setPropertyImpl(java.lang.String, java.lang.Object)
         */
        this.setDelimiterParsingDisabled(Boolean.TRUE);
    }
 
}

通過Google Guice控制Bean的加載順序压语,在較早的時(shí)機(jī)啸罢,執(zhí)行ConfigurationManager.getConfigInstance(),獲取到ConcurrentCompositeConfiguration胎食,完成GatewayConfigConfiguration的初始化扰才,然后再插入到第一個(gè)位置。

后續(xù)只需要對(duì)GatewayConfigConfiguration進(jìn)行配置的增刪查改操作即可厕怜。

6.2 路由機(jī)制

image

image

路由機(jī)制也是仿造的Dubbo路由機(jī)制衩匣,灰度路由是仿造的Dubbo的標(biāo)簽路由蕾总,就近路由可以理解為同機(jī)房路由。

請(qǐng)求處理過程:

客戶端請(qǐng)求過來的時(shí)候琅捏,網(wǎng)關(guān)服務(wù)會(huì)通過path前綴提取到對(duì)應(yīng)的后端服務(wù)名或者在請(qǐng)求Header中指定傳遞對(duì)應(yīng)的serviceName生百,然后只在匹配到的后端服務(wù)中,繼續(xù)API匹配操作柄延,如果匹配到API蚀浆,則篩選出對(duì)應(yīng)的后端機(jī)器列表,然后進(jìn)行路由搜吧、負(fù)載均衡市俊,最終選中一臺(tái)機(jī)器,將請(qǐng)求轉(zhuǎn)發(fā)過去滤奈。

這里會(huì)有個(gè)疑問摆昧,如果不希望只在某個(gè)后端服務(wù)中進(jìn)行請(qǐng)求路由匹配,是希望在一堆后端服務(wù)中進(jìn)行匹配僵刮,需要怎么操作据忘?

在后面的第七章節(jié)會(huì)解答這個(gè)疑問鹦牛,請(qǐng)耐心閱讀搞糕。

6.2.1 就近路由

當(dāng)請(qǐng)求到網(wǎng)關(guān)服務(wù),會(huì)提取網(wǎng)關(guān)服務(wù)自身的機(jī)房loc屬性值曼追,讀取全局窍仰、應(yīng)用級(jí)別的開關(guān),如果就近路由開關(guān)打開礼殊,則篩選服務(wù)列表的時(shí)候驹吮,會(huì)過濾相同loc的后端機(jī)器,負(fù)載均衡的時(shí)候晶伦,在相同loc的機(jī)器列表中挑選一臺(tái)進(jìn)行請(qǐng)求碟狞。

如果沒有相同loc的后端機(jī)器,則降級(jí)從其他loc的后端機(jī)器中進(jìn)行挑選婚陪。

image

image

其中loc信息就是機(jī)房信息族沃,每個(gè)后端服務(wù)節(jié)點(diǎn)在SDK上報(bào)或者手工錄入的時(shí)候,都會(huì)攜帶這個(gè)值泌参。

6.2.2 灰度路由

灰度路由需要用戶傳遞Header屬性值脆淹,比如gray=canary_gray。

網(wǎng)關(guān)管理后臺(tái)配置灰度路由的時(shí)候沽一,會(huì)建立grayName -> List<Server>映射關(guān)系盖溺,當(dāng)網(wǎng)關(guān)管理后臺(tái)增量推送到網(wǎng)關(guān)服務(wù)之后,網(wǎng)關(guān)服務(wù)就可以通過grayName來提取配置下的后端機(jī)器列表铣缠,然后再進(jìn)行負(fù)載均衡挑選機(jī)器烘嘱。

如下圖所示:

image

image

6.3 API映射匹配

網(wǎng)關(guān)在進(jìn)行請(qǐng)求轉(zhuǎn)發(fā)的時(shí)候昆禽,需要明確知道請(qǐng)求哪一個(gè)服務(wù)的哪一個(gè)API,這個(gè)過程就是API匹配蝇庭。

因?yàn)椴煌暮蠖朔?wù)可能會(huì)擁有相同路徑的API为狸,所以網(wǎng)關(guān)要求請(qǐng)求傳遞serviceName,serviceName可以放置于請(qǐng)求Header或者請(qǐng)求參數(shù)中遗契。

攜帶了serviceName之后辐棒,就可以在后端服務(wù)的API中去匹配了,有一些是相等匹配牍蜂,有些是正則匹配漾根,因?yàn)镽ESTFul協(xié)議,需要支持 /* 通配符匹配鲫竞。

這里會(huì)有人疑問了辐怕,難道請(qǐng)求一定需要顯式傳遞serviceName嗎?

為了解決這個(gè)問題从绘,創(chuàng)建了一個(gè)gateway_origin_mapping表寄疏,用于path前綴或者域名前綴 映射到 serviceName,通過在管理后臺(tái)建立這個(gè)映射關(guān)系僵井,然后推送到網(wǎng)關(guān)服務(wù)陕截,即可解決顯式傳遞serviceName的問題,會(huì)自動(dòng)提取請(qǐng)求的path前綴批什、域名前綴农曲,找到對(duì)應(yīng)的serviceName。

如果不希望是在一個(gè)后端服務(wù)中進(jìn)行API匹配驻债,則需閱讀后面的第七章節(jié)乳规。

6.4 負(fù)載均衡

替換 ribbon 組件,改為仿造 Dubbo 的負(fù)載均衡機(jī)制合呐。

public interface ILoadBalance {
 
    /**
     * 從服務(wù)列表中篩選一臺(tái)機(jī)器進(jìn)行調(diào)用
     * @param serverList
     * @param originName
     * @param requestMessage
     * @return
     */
    DynamicServer select(List<DynamicServer> serverList, String originName, HttpRequestMessage requestMessage);
 
}

替換的理由:ribbon的服務(wù)列表更新只是定期更新暮的,如果不考慮復(fù)雜的篩選過濾,是滿足要求的淌实,但是如果想要靈活的根據(jù)請(qǐng)求頭冻辩、請(qǐng)求參數(shù)進(jìn)行篩選,ribbon則不太適合翩伪。

6.5 心跳檢測(cè)

核心思路:當(dāng)網(wǎng)絡(luò)請(qǐng)求正常返回的時(shí)候微猖,心跳檢測(cè)是不需要,此時(shí)后端服務(wù)節(jié)點(diǎn)肯定是正常的缘屹,只需要定期檢測(cè)未被請(qǐng)求的后端節(jié)點(diǎn)凛剥,超過一定的錯(cuò)誤閾值,則標(biāo)記為不可用轻姿,從機(jī)器列表中剔除犁珠。

第一期先實(shí)現(xiàn)簡(jiǎn)單版本:通過定時(shí)任務(wù)定期去異步調(diào)用心跳檢測(cè)Url逻炊,如果超過失敗閾值,則從從負(fù)載均衡列表中剔除犁享。

異步請(qǐng)求采用httpasyncclient組件處理余素。

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpasyncclient</artifactId>
    <version>4.1.4</version>
</dependency>

方案為:HealthCheckScheduledExecutor + HealthCheckTask + HttpAsyncClient。

6.6 日志異步化改造

Zuul2默認(rèn)采用的log4j進(jìn)行日志打印炊昆,是同步阻塞操作桨吊,需要修改為異步化操作,改為使用logback的AsyncAppender凤巨。

日志打印也是影響性能的一個(gè)關(guān)鍵點(diǎn)视乐,需要特別注意,后續(xù)會(huì)衡量是否切換為log4j2敢茁。

6.7 協(xié)議轉(zhuǎn)換

HTTP -> HTTP

Zuul2采用的是ProxyEndpoint用于支持HTTP -> HTTP協(xié)議轉(zhuǎn)發(fā)佑淀。

通過Netty Client的方式發(fā)起網(wǎng)絡(luò)請(qǐng)求到真實(shí)的后端服務(wù)。

HTTP -> Dubbo

采用Dubbo的泛化調(diào)用實(shí)現(xiàn)HTTP -> Dubbo協(xié)議轉(zhuǎn)發(fā)彰檬,可以采用$invokeAsync伸刃。

HTTP → Tars

基于tars-java采用類似于Dubbo的泛化調(diào)用的方式實(shí)現(xiàn)協(xié)議轉(zhuǎn)發(fā),基于https://github.com/TarsCloud/TarsGateway改造而來的逢倍。

6.8 無損發(fā)布

網(wǎng)關(guān)作為請(qǐng)求轉(zhuǎn)發(fā)捧颅,當(dāng)然希望在業(yè)務(wù)后端機(jī)器部署的期間,不應(yīng)該把請(qǐng)求轉(zhuǎn)發(fā)到還未部署完成的節(jié)點(diǎn)瓶堕。

業(yè)務(wù)后端機(jī)器節(jié)點(diǎn)的無損發(fā)布隘道,這里分為兩種場(chǎng)景介紹:

  • 集成了網(wǎng)關(guān)SDK 網(wǎng)關(guān)SDK添加了ShutdownHook症歇,會(huì)主動(dòng)從zookeeper刪除登記的節(jié)點(diǎn)信息郎笆,從而避免請(qǐng)求打到即將下線的節(jié)點(diǎn)。
  • 未集成網(wǎng)關(guān)SDK 如果什么都不做忘晤,則只能依賴網(wǎng)關(guān)服務(wù)的心跳檢測(cè)功能宛蚓,會(huì)有15s的流量損失。慶幸的是管理后臺(tái)提供了流量摘除设塔、流量恢復(fù)的操作按鈕凄吏,支持動(dòng)態(tài)的上線、下線機(jī)器節(jié)點(diǎn)闰蛔。

設(shè)計(jì)方案

我們給后端機(jī)器節(jié)點(diǎn)dynamic_forward_server表新增了一個(gè)字段online痕钢,如果online=1,則代表在線序六,接收流量任连,反之,則代表下線例诀,不接收流量随抠。

網(wǎng)關(guān)服務(wù)gateway-server新增一個(gè)路由:OnlineRouter裁着,從后端機(jī)器列表中篩選online=1的機(jī)器,過濾掉不在線的機(jī)器拱她,則完成了無損發(fā)布的功能二驰。

public interface IRouter {
 
    /**
     * 過濾
     * @param serverList
     * @param originName
     * @param requestMessage
     * @return
     */
    List<DynamicServer> route(List<DynamicServer> serverList, String originName, HttpRequestMessage requestMessage);
 
}

6.9 網(wǎng)關(guān)集群分組隔離

網(wǎng)關(guān)集群的分組隔離指的是業(yè)務(wù)與業(yè)務(wù)之間的請(qǐng)求應(yīng)該是隔離的,不應(yīng)該被部分業(yè)務(wù)請(qǐng)求打垮了網(wǎng)關(guān)服務(wù)秉沼,從而導(dǎo)致了別的業(yè)務(wù)請(qǐng)求無法處理桶雀。

這里我們會(huì)對(duì)接接入網(wǎng)關(guān)的業(yè)務(wù)進(jìn)行分組歸類,不同的業(yè)務(wù)使用不同的分組唬复,不同的網(wǎng)關(guān)分組背犯,會(huì)部署獨(dú)立的網(wǎng)關(guān)集群,從而隔離了風(fēng)險(xiǎn)盅抚,不用再擔(dān)心業(yè)務(wù)之間的互相影響漠魏。

舉例:

金融業(yè)務(wù)在生產(chǎn)環(huán)境存在一個(gè)灰度點(diǎn)檢環(huán)境,為了配合金融業(yè)務(wù)的遷移妄均,這邊也必須有一套獨(dú)立的環(huán)境為之服務(wù)柱锹,那是否重新部署一套全新的系統(tǒng)呢(獨(dú)立的前端+獨(dú)立的管理后臺(tái)+獨(dú)立的網(wǎng)關(guān)集群)

其實(shí)不必這么操作,我們只需要部署一套獨(dú)立的網(wǎng)關(guān)集群即可丰包,因?yàn)榫W(wǎng)關(guān)管理后臺(tái)禁熏,可以同時(shí)配置多個(gè)網(wǎng)關(guān)分組的數(shù)據(jù)。

創(chuàng)建一個(gè)新的網(wǎng)關(guān)分組finance-gray邑彪,而新的網(wǎng)關(guān)集群只需要拉取finance-gray分組的配置數(shù)據(jù)即可瞧毙,不會(huì)對(duì)其他網(wǎng)關(guān)集群造成任何影響。

七寄症、.如何快速遷移業(yè)務(wù)

在業(yè)務(wù)接入的時(shí)候宙彪,現(xiàn)有的網(wǎng)關(guān)出現(xiàn)了一個(gè)尷尬的問題,當(dāng)某些業(yè)務(wù)方自行搭建了一套Spring Cloud Gateway網(wǎng)關(guān)有巧,里面的服務(wù)沒有清晰的path前綴释漆、獨(dú)立的域名拆分,雖然是微服務(wù)體系篮迎,但是大家共用一個(gè)域名男图,接口前綴也沒有良好的劃分,混用在一起甜橱。

這個(gè)時(shí)候如果再按照原有的請(qǐng)求處理流程逊笆,則需要業(yè)務(wù)方進(jìn)行Nginx的大量修改,需要在location的地方都顯式傳遞serviceName參數(shù)岂傲,但是業(yè)務(wù)方不愿意進(jìn)行這一個(gè)調(diào)整难裆。

針對(duì)這個(gè)問題,其實(shí)本質(zhì)原因在于請(qǐng)求匹配邏輯的不一致性譬胎,現(xiàn)有的網(wǎng)關(guān)是先匹配服務(wù)應(yīng)用差牛,再進(jìn)行API匹配命锄,這樣效率高一些,而Spring Cloud Gateway則是先API匹配偏化,命中了才知道是哪個(gè)后端服務(wù)脐恩。

為了解決這個(gè)問題,網(wǎng)關(guān)再次建立了一個(gè) "微服務(wù)集" → "微服務(wù)應(yīng)用列表" 的映射關(guān)系侦讨,管理后臺(tái)支持這個(gè)映射關(guān)系的推送驶冒。

一個(gè)網(wǎng)關(guān)分組下面會(huì)有很多應(yīng)用服務(wù),這里可以拆分為子集合韵卤,可以理解為微服務(wù)集就是里面的子集合骗污。

客戶端請(qǐng)求傳遞過來的時(shí)候,需要在請(qǐng)求Header傳遞scTag 參數(shù)沈条,scTag用來標(biāo)記是哪個(gè)微服務(wù)集需忿,然后提取到scTag對(duì)應(yīng)的所有后端服務(wù)應(yīng)用列表,依次去對(duì)應(yīng)的應(yīng)用服務(wù)列表中進(jìn)行API匹配蜡歹,如果命中了屋厘,則代表請(qǐng)求轉(zhuǎn)發(fā)到當(dāng)前應(yīng)用的后端節(jié)點(diǎn),而對(duì)原有的架構(gòu)改造很小月而。

如果不想改動(dòng)客戶端請(qǐng)求汗洒,則需要在業(yè)務(wù)域名的Nginx上進(jìn)行調(diào)整,傳遞scTag請(qǐng)求Header父款。

作者:Lin Chengjun

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末溢谤,一起剝皮案震驚了整個(gè)濱河市坏逢,隨后出現(xiàn)的幾起案子孵睬,更是在濱河造成了極大的恐慌俗冻,老刑警劉巖酝润,帶你破解...
    沈念sama閱讀 221,888評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異固以,居然都是意外死亡滋觉,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門包晰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人炕吸,你說我怎么就攤上這事伐憾。” “怎么了赫模?”我有些...
    開封第一講書人閱讀 168,386評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵树肃,是天一觀的道長。 經(jīng)常有香客問我瀑罗,道長胸嘴,這世上最難降的妖魔是什么雏掠? 我笑而不...
    開封第一講書人閱讀 59,726評(píng)論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮劣像,結(jié)果婚禮上乡话,老公的妹妹穿的比我還像新娘。我一直安慰自己耳奕,他們只是感情好绑青,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,729評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著屋群,像睡著了一般闸婴。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上芍躏,一...
    開封第一講書人閱讀 52,337評(píng)論 1 310
  • 那天邪乍,我揣著相機(jī)與錄音,去河邊找鬼对竣。 笑死溺欧,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的柏肪。 我是一名探鬼主播姐刁,決...
    沈念sama閱讀 40,902評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼烦味!你這毒婦竟也來了聂使?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,807評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤谬俄,失蹤者是張志新(化名)和其女友劉穎柏靶,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體溃论,經(jīng)...
    沈念sama閱讀 46,349評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡屎蜓,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,439評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了钥勋。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片炬转。...
    茶點(diǎn)故事閱讀 40,567評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖算灸,靈堂內(nèi)的尸體忽然破棺而出扼劈,到底是詐尸還是另有隱情,我是刑警寧澤菲驴,帶...
    沈念sama閱讀 36,242評(píng)論 5 350
  • 正文 年R本政府宣布荐吵,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏先煎。R本人自食惡果不足惜贼涩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,933評(píng)論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望薯蝎。 院中可真熱鬧遥倦,春花似錦、人聲如沸良风。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽烟央。三九已至统诺,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間疑俭,已是汗流浹背粮呢。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評(píng)論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留钞艇,地道東北人啄寡。 一個(gè)月前我還...
    沈念sama閱讀 48,995評(píng)論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像哩照,于是被迫代替她去往敵國和親挺物。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,585評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容