全鏈路中的數(shù)據(jù)透?jìng)?/h1>
在微服務(wù)的應(yīng)用場(chǎng)景下,服務(wù)之間可以通過(guò)各種方式與協(xié)議進(jìn)行交互,同時(shí)整條鏈路也會(huì)變得比較長(zhǎng)。與此同時(shí),我們會(huì)希望一些數(shù)據(jù)在整條鏈路中進(jìn)行透?jìng)?,比如說(shuō)用作對(duì)普通 api 參數(shù)的動(dòng)態(tài)補(bǔ)充、鏈路壓測(cè)標(biāo)識(shí)或者灰度發(fā)布標(biāo)識(shí)等。
關(guān)于 request headers
如果 rpc 采用一些 tcp 協(xié)議,壓根不會(huì)考慮 request headers。但如果 rpc 是基于 http 協(xié)議的背景下,request headers 似乎天生是做透?jìng)鲾?shù)據(jù)載體的料。
在客戶端,rpc 框架提供了 api 上的注解以注入自定義的 request header。在服務(wù)端 spring mvc 的 controller 里,我們也可以通過(guò) HttpServletRequest 來(lái)獲取 header。就算不在 controller 里,我們也能夠通過(guò) spring 提供的方法從任意地方獲取 request 里的 header,當(dāng)然由于使用了 threadLocal,所以前提是在同一線程里。
final RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); if (attributes instanceof ServletRequestAttributes){ final HttpServletRequest request = ((ServletRequestAttributes) attributes).getRequest(); final String headerVal = request.getHeader("headerKey"); }
那么 request header 還有哪些不滿足鏈路數(shù)據(jù)透?jìng)鞯牡胤侥亍?/span>
如果鏈路中有異步線程切換的時(shí)候,我們沒(méi)法再通過(guò) RequestContextHolder 類來(lái)獲取 request 了,意味著除了在 controller 層以為,拿到 request header 都是不容易的事兒,除非將 HttpServletRequest 對(duì)象在所有代碼中傳遞。
request header 仍然是綁定在 http 上的東西,就算大部分業(yè)務(wù)在使用 http 協(xié)議進(jìn)行交互,總有些應(yīng)用會(huì)使用 tcp 協(xié)議的 rpc 框架,例如 thrift。除此之外,還有些許多應(yīng)用間使用 mq 來(lái)解耦交互,但仍然希望數(shù)據(jù)可以透?jìng)鳌?/span>
大多數(shù)情況下,我們只能對(duì) request header 進(jìn)行"取"操作,而很難進(jìn)行"存刪改"操作,因?yàn)?HttpServletRequest 沒(méi)有提供相關(guān)的方法。
數(shù)據(jù)透?jìng)?/span>
我們希望可以有一種類似 header 的載體來(lái)承載需要透?jìng)鞯臄?shù)據(jù),它能夠跨線程進(jìn)行數(shù)據(jù)傳遞,同時(shí)還能兼容不同的通信方式,支持自由存取,最后它需要對(duì)開(kāi)發(fā)者透明。
兼容不同通信方式意味著我們得抽象出一層數(shù)據(jù)上下文 Context 的概念,而在實(shí)現(xiàn)上去兼容各個(gè)實(shí)際的通信方式。

我們看到這里主要包括兩層,即透?jìng)鲾?shù)據(jù)上下文與數(shù)據(jù)透?jìng)鲄f(xié)議實(shí)現(xiàn)層。前者是一層抽象的概念,依附于一個(gè)貫穿整條鏈路的對(duì)象。而后者是依據(jù)各個(gè)通信方式協(xié)議的不同而具體實(shí)現(xiàn)的。
這里業(yè)務(wù)方 A 使用透?jìng)鲾?shù)據(jù)上下文設(shè)置透?jìng)鲾?shù)據(jù)后,在協(xié)議中需要先使用上下文獲得透?jìng)鲾?shù)據(jù),然后各個(gè)協(xié)議自己實(shí)現(xiàn)透?jìng)鲾?shù)據(jù)隨通信傳遞,在通信對(duì)端獲得透?jìng)鲾?shù)據(jù)后重新設(shè)置回透?jìng)魃舷挛闹校?/span>
這樣業(yè)務(wù)方 B 就可以使用上下文獲取到業(yè)務(wù)方 A 設(shè)置的透?jìng)鲾?shù)據(jù)并進(jìn)行使用了。
數(shù)據(jù)上下文
我們知道數(shù)據(jù)上下文本身得是一個(gè)貫穿整條鏈路的對(duì)象,自然不依賴于具體的通信方式以及通信協(xié)議。
很多時(shí)候我們會(huì)直接把 Context 放到 Rpc 框架上去,隨著 Rpc 通信而傳遞。但放在 Rpc 框架上,首先就違背了通信協(xié)議無(wú)關(guān)了,至少違背了通信框架無(wú)關(guān)。
實(shí)際上比較符合條件的還是調(diào)用鏈框架,本身調(diào)用鏈框架針對(duì)各種通信方式就適配了許多插件,包括 Thrift、Kafka 等,同時(shí)針對(duì)異步線程切換的情況也已經(jīng)有一套適配方式。
所以我們選擇的載體就是調(diào)用鏈框架了,把 Context 類放到調(diào)用鏈的核心包中,然后設(shè)置了幾個(gè)簡(jiǎn)單的方法:
Context.put(k,v,option) //一個(gè)簡(jiǎn)單的存儲(chǔ)或者替換操作,option是為了控制是否往下游透?jìng)?/span>Context.get(k) //一個(gè)簡(jiǎn)單的獲取操作Context.del(k) //一個(gè)簡(jiǎn)單的刪除操作
實(shí)際上調(diào)用這些方法的地方在調(diào)用鏈為各個(gè)協(xié)議封裝的的插件中,因?yàn)橐婕暗礁鱾€(gè)協(xié)議的裝包與解包。
數(shù)據(jù)透?jìng)鲗?shí)現(xiàn)層
各個(gè)協(xié)議層需也只需要干兩件通用的事情,1 是將透?jìng)鲾?shù)據(jù)從上下文中取出設(shè)置到協(xié)議中,2 是將透?jìng)鲾?shù)據(jù)從協(xié)議中取出設(shè)置回上下文中,實(shí)現(xiàn)方式依協(xié)議而定。
比如我們目前使用最廣泛的 Rpc 框架仍然是基于 Http 協(xié)議的,那么意味著在客戶端我們需要將透?jìng)鲾?shù)據(jù)從上下文取出設(shè)置到 request headers 中,而在服務(wù)端則是從 request headers 中取出所有頭(可能做一些過(guò)濾)然后設(shè)置回?cái)?shù)據(jù)上下文中。
再比如到 Thrift 框架中,數(shù)據(jù)上下文中的透?jìng)鲾?shù)據(jù)就是依附于 thrift 協(xié)議 header 進(jìn)行傳遞的。
同樣的,kafka 之類的 mq 也是做類似的工作。
異步數(shù)據(jù)上下文
我們之前說(shuō),整條鏈路中可能會(huì)存在很多線程切換的場(chǎng)景,手動(dòng)起的線程池、servlet 3.0 的異步、spring5 的響應(yīng)式、有些應(yīng)用甚至使用的 akka 等。但不管怎樣,在 java 中要處理異步線程的數(shù)據(jù)傳遞的話無(wú)非 2 中方式:
基于對(duì)象傳遞
以 trace 信息為例,我們?cè)谥骶€程將 trace 信息封裝到一個(gè)對(duì)象里,然后再起子線程的時(shí)候顯式將對(duì)象傳遞進(jìn)去,那么我們?cè)谧泳€程里就能拿到主線程的 trace 信息了。當(dāng)然為了對(duì)使用者透明,我們往往采取裝飾類的方式,比如對(duì) taskDecorator、callable、runnable、supplier 等類進(jìn)行裝飾,然后再裝飾類里預(yù)設(shè)異步上下文。所以基于裝飾類對(duì)象的異步數(shù)據(jù)上下文傳遞如下所示:

還有一種方法就是基于 jdk 提供的 InheritableThreadLocal 衍生出的父子線程傳遞了,包括支持線程池池化復(fù)用場(chǎng)景的 Transmittable ThreadLocal。
數(shù)據(jù)透?jìng)鞯氖褂脠?chǎng)景
鏈路的數(shù)據(jù)透?jìng)骺雌饋?lái)好像使用場(chǎng)景比較單一,除了給業(yè)務(wù)方傳遞一些業(yè)務(wù)場(chǎng)景上的數(shù)據(jù)外,其實(shí)數(shù)據(jù)透?jìng)髟诩兗夹g(shù)層面也有比較多的應(yīng)用,這里簡(jiǎn)單介紹 2 個(gè)場(chǎng)景。
第一個(gè)就是在全鏈路壓測(cè)的場(chǎng)景下,我們的壓測(cè)請(qǐng)求與正常請(qǐng)求需要有一定的區(qū)分,從而讓整個(gè)壓測(cè)請(qǐng)求的流轉(zhuǎn)過(guò)程都不至于影響線上環(huán)境與數(shù)據(jù),包括存儲(chǔ)層面我們也會(huì)讓壓測(cè)請(qǐng)求落入"影子庫(kù)"中而不會(huì)產(chǎn)生臟數(shù)據(jù)。區(qū)分的方法往往是對(duì)請(qǐng)求進(jìn)行"打標(biāo)",然后讓標(biāo)識(shí)通過(guò)數(shù)據(jù)上下文在整條鏈路中進(jìn)行透?jìng)鳌2还苕溌分惺欠裼芯€程切換,包括多少種通信方式。
其次就是對(duì)整條鏈路的流量灰發(fā),灰發(fā)是一種比較穩(wěn)妥的部署上線方式,比方說(shuō)一種灰發(fā)規(guī)則是可以針對(duì)某些特定用戶展示最新版本的應(yīng)用,那么這時(shí)我們往往是根據(jù)請(qǐng)求中的類似"user-id"字段來(lái)區(qū)分用戶的。那么這些字段數(shù)據(jù)也需要在整條鏈路中進(jìn)行透?jìng)鳎拍軌驖M足全鏈路灰發(fā)的需求。

往期精彩推薦 騰訊、阿里、滴滴后臺(tái)面試題匯總總結(jié) — (含答案)
面試:史上最全多線程面試題 !
最新阿里內(nèi)推Java后端面試題
JVM難學(xué)?那是因?yàn)槟銢](méi)認(rèn)真看完這篇文章

—END—
關(guān)注作者微信公眾號(hào) —《JAVA爛豬皮》
了解更多java后端架構(gòu)知識(shí)以及最新面試寶典

你點(diǎn)的每個(gè)好看,我都認(rèn)真當(dāng)成了
看完本文記得給作者點(diǎn)贊+在看哦~~~大家的支持,是作者源源不斷出文的動(dòng)力
作者:fredalxin
地址:https://fredal.xin/all-link-context
瀏覽
98
在微服務(wù)的應(yīng)用場(chǎng)景下,服務(wù)之間可以通過(guò)各種方式與協(xié)議進(jìn)行交互,同時(shí)整條鏈路也會(huì)變得比較長(zhǎng)。與此同時(shí),我們會(huì)希望一些數(shù)據(jù)在整條鏈路中進(jìn)行透?jìng)?,比如說(shuō)用作對(duì)普通 api 參數(shù)的動(dòng)態(tài)補(bǔ)充、鏈路壓測(cè)標(biāo)識(shí)或者灰度發(fā)布標(biāo)識(shí)等。
關(guān)于 request headers
如果 rpc 采用一些 tcp 協(xié)議,壓根不會(huì)考慮 request headers。但如果 rpc 是基于 http 協(xié)議的背景下,request headers 似乎天生是做透?jìng)鲾?shù)據(jù)載體的料。
在客戶端,rpc 框架提供了 api 上的注解以注入自定義的 request header。在服務(wù)端 spring mvc 的 controller 里,我們也可以通過(guò) HttpServletRequest 來(lái)獲取 header。就算不在 controller 里,我們也能夠通過(guò) spring 提供的方法從任意地方獲取 request 里的 header,當(dāng)然由于使用了 threadLocal,所以前提是在同一線程里。
final RequestAttributes attributes = RequestContextHolder.getRequestAttributes();if (attributes instanceof ServletRequestAttributes){final HttpServletRequest request = ((ServletRequestAttributes) attributes).getRequest();final String headerVal = request.getHeader("headerKey");}
那么 request header 還有哪些不滿足鏈路數(shù)據(jù)透?jìng)鞯牡胤侥亍?/span>
如果鏈路中有異步線程切換的時(shí)候,我們沒(méi)法再通過(guò) RequestContextHolder 類來(lái)獲取 request 了,意味著除了在 controller 層以為,拿到 request header 都是不容易的事兒,除非將 HttpServletRequest 對(duì)象在所有代碼中傳遞。
request header 仍然是綁定在 http 上的東西,就算大部分業(yè)務(wù)在使用 http 協(xié)議進(jìn)行交互,總有些應(yīng)用會(huì)使用 tcp 協(xié)議的 rpc 框架,例如 thrift。除此之外,還有些許多應(yīng)用間使用 mq 來(lái)解耦交互,但仍然希望數(shù)據(jù)可以透?jìng)鳌?/span>
大多數(shù)情況下,我們只能對(duì) request header 進(jìn)行"取"操作,而很難進(jìn)行"存刪改"操作,因?yàn)?HttpServletRequest 沒(méi)有提供相關(guān)的方法。
數(shù)據(jù)透?jìng)?/span>
我們希望可以有一種類似 header 的載體來(lái)承載需要透?jìng)鞯臄?shù)據(jù),它能夠跨線程進(jìn)行數(shù)據(jù)傳遞,同時(shí)還能兼容不同的通信方式,支持自由存取,最后它需要對(duì)開(kāi)發(fā)者透明。
兼容不同通信方式意味著我們得抽象出一層數(shù)據(jù)上下文 Context 的概念,而在實(shí)現(xiàn)上去兼容各個(gè)實(shí)際的通信方式。
我們看到這里主要包括兩層,即透?jìng)鲾?shù)據(jù)上下文與數(shù)據(jù)透?jìng)鲄f(xié)議實(shí)現(xiàn)層。前者是一層抽象的概念,依附于一個(gè)貫穿整條鏈路的對(duì)象。而后者是依據(jù)各個(gè)通信方式協(xié)議的不同而具體實(shí)現(xiàn)的。
這里業(yè)務(wù)方 A 使用透?jìng)鲾?shù)據(jù)上下文設(shè)置透?jìng)鲾?shù)據(jù)后,在協(xié)議中需要先使用上下文獲得透?jìng)鲾?shù)據(jù),然后各個(gè)協(xié)議自己實(shí)現(xiàn)透?jìng)鲾?shù)據(jù)隨通信傳遞,在通信對(duì)端獲得透?jìng)鲾?shù)據(jù)后重新設(shè)置回透?jìng)魃舷挛闹校?/span>
這樣業(yè)務(wù)方 B 就可以使用上下文獲取到業(yè)務(wù)方 A 設(shè)置的透?jìng)鲾?shù)據(jù)并進(jìn)行使用了。
數(shù)據(jù)上下文
我們知道數(shù)據(jù)上下文本身得是一個(gè)貫穿整條鏈路的對(duì)象,自然不依賴于具體的通信方式以及通信協(xié)議。
很多時(shí)候我們會(huì)直接把 Context 放到 Rpc 框架上去,隨著 Rpc 通信而傳遞。但放在 Rpc 框架上,首先就違背了通信協(xié)議無(wú)關(guān)了,至少違背了通信框架無(wú)關(guān)。
實(shí)際上比較符合條件的還是調(diào)用鏈框架,本身調(diào)用鏈框架針對(duì)各種通信方式就適配了許多插件,包括 Thrift、Kafka 等,同時(shí)針對(duì)異步線程切換的情況也已經(jīng)有一套適配方式。
所以我們選擇的載體就是調(diào)用鏈框架了,把 Context 類放到調(diào)用鏈的核心包中,然后設(shè)置了幾個(gè)簡(jiǎn)單的方法:
Context.put(k,v,option) //一個(gè)簡(jiǎn)單的存儲(chǔ)或者替換操作,option是為了控制是否往下游透?jìng)?/span>Context.get(k) //一個(gè)簡(jiǎn)單的獲取操作Context.del(k) //一個(gè)簡(jiǎn)單的刪除操作
實(shí)際上調(diào)用這些方法的地方在調(diào)用鏈為各個(gè)協(xié)議封裝的的插件中,因?yàn)橐婕暗礁鱾€(gè)協(xié)議的裝包與解包。
數(shù)據(jù)透?jìng)鲗?shí)現(xiàn)層
各個(gè)協(xié)議層需也只需要干兩件通用的事情,1 是將透?jìng)鲾?shù)據(jù)從上下文中取出設(shè)置到協(xié)議中,2 是將透?jìng)鲾?shù)據(jù)從協(xié)議中取出設(shè)置回上下文中,實(shí)現(xiàn)方式依協(xié)議而定。
比如我們目前使用最廣泛的 Rpc 框架仍然是基于 Http 協(xié)議的,那么意味著在客戶端我們需要將透?jìng)鲾?shù)據(jù)從上下文取出設(shè)置到 request headers 中,而在服務(wù)端則是從 request headers 中取出所有頭(可能做一些過(guò)濾)然后設(shè)置回?cái)?shù)據(jù)上下文中。
再比如到 Thrift 框架中,數(shù)據(jù)上下文中的透?jìng)鲾?shù)據(jù)就是依附于 thrift 協(xié)議 header 進(jìn)行傳遞的。
同樣的,kafka 之類的 mq 也是做類似的工作。
異步數(shù)據(jù)上下文
我們之前說(shuō),整條鏈路中可能會(huì)存在很多線程切換的場(chǎng)景,手動(dòng)起的線程池、servlet 3.0 的異步、spring5 的響應(yīng)式、有些應(yīng)用甚至使用的 akka 等。但不管怎樣,在 java 中要處理異步線程的數(shù)據(jù)傳遞的話無(wú)非 2 中方式:
基于對(duì)象傳遞
以 trace 信息為例,我們?cè)谥骶€程將 trace 信息封裝到一個(gè)對(duì)象里,然后再起子線程的時(shí)候顯式將對(duì)象傳遞進(jìn)去,那么我們?cè)谧泳€程里就能拿到主線程的 trace 信息了。當(dāng)然為了對(duì)使用者透明,我們往往采取裝飾類的方式,比如對(duì) taskDecorator、callable、runnable、supplier 等類進(jìn)行裝飾,然后再裝飾類里預(yù)設(shè)異步上下文。所以基于裝飾類對(duì)象的異步數(shù)據(jù)上下文傳遞如下所示:
還有一種方法就是基于 jdk 提供的 InheritableThreadLocal 衍生出的父子線程傳遞了,包括支持線程池池化復(fù)用場(chǎng)景的 Transmittable ThreadLocal。
數(shù)據(jù)透?jìng)鞯氖褂脠?chǎng)景
鏈路的數(shù)據(jù)透?jìng)骺雌饋?lái)好像使用場(chǎng)景比較單一,除了給業(yè)務(wù)方傳遞一些業(yè)務(wù)場(chǎng)景上的數(shù)據(jù)外,其實(shí)數(shù)據(jù)透?jìng)髟诩兗夹g(shù)層面也有比較多的應(yīng)用,這里簡(jiǎn)單介紹 2 個(gè)場(chǎng)景。
第一個(gè)就是在全鏈路壓測(cè)的場(chǎng)景下,我們的壓測(cè)請(qǐng)求與正常請(qǐng)求需要有一定的區(qū)分,從而讓整個(gè)壓測(cè)請(qǐng)求的流轉(zhuǎn)過(guò)程都不至于影響線上環(huán)境與數(shù)據(jù),包括存儲(chǔ)層面我們也會(huì)讓壓測(cè)請(qǐng)求落入"影子庫(kù)"中而不會(huì)產(chǎn)生臟數(shù)據(jù)。區(qū)分的方法往往是對(duì)請(qǐng)求進(jìn)行"打標(biāo)",然后讓標(biāo)識(shí)通過(guò)數(shù)據(jù)上下文在整條鏈路中進(jìn)行透?jìng)鳌2还苕溌分惺欠裼芯€程切換,包括多少種通信方式。
其次就是對(duì)整條鏈路的流量灰發(fā),灰發(fā)是一種比較穩(wěn)妥的部署上線方式,比方說(shuō)一種灰發(fā)規(guī)則是可以針對(duì)某些特定用戶展示最新版本的應(yīng)用,那么這時(shí)我們往往是根據(jù)請(qǐng)求中的類似"user-id"字段來(lái)區(qū)分用戶的。那么這些字段數(shù)據(jù)也需要在整條鏈路中進(jìn)行透?jìng)鳎拍軌驖M足全鏈路灰發(fā)的需求。

騰訊、阿里、滴滴后臺(tái)面試題匯總總結(jié) — (含答案)
面試:史上最全多線程面試題 !
最新阿里內(nèi)推Java后端面試題
JVM難學(xué)?那是因?yàn)槟銢](méi)認(rèn)真看完這篇文章

關(guān)注作者微信公眾號(hào) —《JAVA爛豬皮》
了解更多java后端架構(gòu)知識(shí)以及最新面試寶典


看完本文記得給作者點(diǎn)贊+在看哦~~~大家的支持,是作者源源不斷出文的動(dòng)力
作者:fredalxin
地址:https://fredal.xin/all-link-context
