Spring Cloud中基於Sleuth的引數透傳功能探索
一.需求
微服務環境,有A,B,C,D四個服務,呼叫關係為:A->B->C->D。使用者在A的頁面選擇當前“語言”環境為“英文”,在某些業務場景下,其它幾個服務需獲取到這個“語言”資訊。
二.分析
這個需求還是很簡單的,類似於“擊鼓傳花”:當前服務從上一個服務中獲取引數,並傳給下一個服務。個人感覺基本上所有的RPC框架都會遇到這個問題,只是以前SOA架構下,服務層級比較少,將“語言”、“登陸”等附加資訊放在引數列表中並不會帶來太多工作量,所以這個問題並不是太突出。而引入了微服務架構思想後,服務呼叫層級急劇增長,這就需要一個更加優雅的方式來解決附加資訊的傳遞問題。
三.方案探索
3.1 方案一:引數放在介面引數列表中
優點:思路簡單,開發沒有學習成本
缺點:
- 程式碼高度耦合:附加資訊卻要每個介面都顯式維護
- 升級困難:如果將來再加一個引數,所有層級的接都要改動
- 引起迷惑:如果B服務的邏輯不需要“語言“引數,但是因為D需要,它也必須維護
- 太傻了,Big不夠
思考:微服務之間絕大多數情況是通過HTTP呼叫的,HTTP的header中也可以放參數資訊。這樣,介面引數中就不用維護這些附加信了。
3.2 方案二:引數放在httpRequest的header中
實現:
1.自定義一個Filter,獲取Request中自己需要的附加資訊,
2.將這些資訊放入ThreadLocal中,
3.實現feign.Client(這裡先忽略RestTemplate)的execute()方法,將附件資訊在呼叫下一層服務前塞入request的header中
優點:引數解耦
缺點:如果B在獲取到附加資訊後,新起了一個執行緒”T1“來呼叫服務C,這時T1就無法從HhreaLocal拿到附加資訊了
思考:
- 如果我知道怎麼用無侵入的方式,在當前執行緒”T”建立子孫執行緒”T1”、”T1-1”時,將資料傳給後代,就能解決這個問題了
- 微服務呼叫鏈框架Sleuth的核心功能即是跟蹤一次請求從A到D的全過程,它肯定支援多執行緒呼叫下的traceId的傳遞。因此,我可以複用Sleuth的相關功能夾帶私貨
3.3 方案三:修改Sleuth原始碼,將附加資訊跟著TraceId一起往後傳遞
優點:
- 原理簡單,不用考慮底層實現
- 不用考慮相容性等問題,Sleuth都已經實現好
- 快(對,就是這一個字)
缺點: - 維護困難,很容易忘記以前修改了哪些地方,更別提移交給別人維護了
- 升級困難,以後每次Spring或者Sleuth升級,都要重新下載原始碼修改
思考:
目前獲取引數的問題解決了,用Filter,只剩下儲存並傳給下一層的問題
既然Sleuth已經解決了多執行緒下traceId的傳遞問題,那我就直接用traceId來解決我的問題
3.4 方案四:充分利用traceId
實現:
- 自定義Filter(優先順序要低於TraceFilter,因為你要獲取TraceFilter裡的traceId),拿到traceId和附加資訊後,將它們存在本地快取中,traceId為key,附加資訊為value
- 參考方案二的實現3。重寫execute()方法,獲取當前執行緒的traceId(這個Sleuth有介面,不再介紹),然後再通過traceId去本地快取中拿到附加資訊,放進Request的header中
優點:擁有上述方案所有的優點,解決上述方案所有缺點
缺點:看著很完美,但是你忽略了一件事:Sleuth要想傳遞自己的traceId,想必它已經重寫了execute()方法(肯定的,那就是TraceFeignClient),你要想用,那就要想辦法在複用TraceFeignClient.execute()的同時,將自己的私貨帶進去
3.5 方案五:重寫TraceFeignClient
實現:有時候,改動原始碼並不需要直接在原有包裡修改。比如:A->B->C->D,如果你要修改C的原始碼,那就將AB原始碼也copy出,作為A1,B1,C#,然後重寫元件的入口,將元件載入順序變為:A1->B1->C#->D,即可達到重寫原始碼的目的。這時候注意的是,載入A1的條件必須跟載入A的相反。具體可參考我之前重寫Consul的入口例子,示例程式碼如下
@ConditionalOnExpression("${spring.cloud.consul.ribbon.enabled:true}==false")
public class MyRibbonConsulAutoConfiguration {}
// 原有入口:
@ConditionalOnProperty(value = "spring.cloud.consul.ribbon.enabled", matchIfMissing = true)
public class RibbonConsulAutoConfiguration {}
綜上,可以重寫TraceFeigClient的入口 TraceFeignClientAutoConfiguration->TraceFeignObjectWrapper>TraceFeignClient,即可達到自己的目的.
優點:感覺事兒基本就成了
缺點:配置為false生效,使用者會覺得比較怪,Sleuth彷彿知道別人會這麼幹似的,它的類的訪問許可權基本都是default,為了copy過來的幾個類能正常編譯通過,你還要再copy九個它們的依賴類,程式太醜
思考:突然想起來,還有一種改程式碼的方式叫位元組碼替換,如果我能在程式啟動的時,將我的execute()直接替換掉Sleuth的execute(),就一勞永逸了
3.6 方案六:位元組碼替換代原始碼修改
優點:高大上,不在原始碼級替換,卻在位元組碼級替換,虛虛實實
缺點:沒這麼幹過,總覺得說著容易做著難
思考:基本上覺得方案五已經能解決問題了。本著精益求精的態度,去技術群裡問了下,很快有大神發來Demo,看過程式碼後頓覺慚愧:我一直在想怎麼重寫TraceFeignClient的execute(),其實這個execute()真正做http請求時,呼叫的是feign.Client的另外一個實現類,注意那句”this.delegate.execute”,只要想辦法用自己的Client替換掉delegate即可
private static final Log log = LogFactory.getLog(MethodHandles.lookup().lookupClass());
private final Client delegate;
@Override
public Response execute(Request request, Request.Options options) throws IOException {
String spanName = getSpanName(request);
Span span = getTracer().createSpan(spanName);
if (log.isDebugEnabled()) {
log.debug("Created new Feign span " + span);
}
try {
AtomicReference<Request> feignRequest = new AtomicReference<>(request);
spanInjector().inject(span, new FeignRequestTextMap(feignRequest));
span.logEvent(Span.CLIENT_SEND);
addRequestTags(request);
Request modifiedRequest = feignRequest.get();
if (log.isDebugEnabled()) {
log.debug("The modified request equals " + modifiedRequest);
}
Response response = this.delegate.execute(modifiedRequest, options);
logCr();
return response;
} catch (RuntimeException | IOException e) {
logCr();
logError(e);
throw e;
} finally {
closeSpan(span);
}
}
3.7 方案七:替換掉TraceFeigClient的delegate即可
實現:通過再次認真Debug原始碼知道,TraceFeignClient預設會載入你的Client實現類作為delegate(汗!),因此你只要直接實現feign.Client介面即可。我偷懶了一把,自己寫個實現類,直接複用了LoadBalancerFeignClient.execute()
優點:基本什麼都有了吧
缺點:如果你以為只是簡單地重寫個execute()就行,那就大錯特了。因為TraceFeignClient直接用了你的方法post過去,因此你要想辦法把ribbon手動整合進來。如果不覺得麻煩的話,可以好好看下TraceFeignClient怎麼生成Client的例項:TraceFeignObjectWrapper.wrap(Object bean)
思考:既然你可以在程式裡獲取到trace和span,那為何不將你的資訊放到span裡呢。如果span中能放點額外資訊就好了,就不用自己寫這麼多東西。經大神提醒,Sleuth中有個baggage可以試試
3.8 方案八:使用baggage
實現:獲取引數的方式不變,取得的引數放在baggage中
優點:簡單,支援RestTemplate呼叫的情況,跟其他元件相容性好
缺點:Sleuth的缺點
四.專案原始碼
4.1 基於slueth的引數透傳外掛
Github地址:https://github.com/bishion/sleuth-plugin
簡介:微服務下使用,呼叫過程中使用者資訊,頁面語言資訊的透傳
使用方式
bizi:
sleuth:
config:
headers: lang_info #如果由多個,逗號隔開.這裡配置從filter裡需要獲取的headerName
呼叫方式
@Service
public class SessionInfoService {
@Resource
private SessionInfoOperator sessionInfoOperator;
public String getLangInfo(){
return sessionInfoOperator.getSessionInfo("lang_info");
}
public void setUserId(){
sessionInfoOperator.setSessionInfo("user_id","bishion");
}
}
五.留下的坑
- Sleuth通過LazyTraceExecutor解決多執行緒下的問題,但是它並沒有解決給手動建立的Thread傳遞資訊的問題
- 有機會試試java位元組碼替換怎麼操作
- Sleuth如何重寫RestTemplate的
- TraceFeignClient怎麼生成Client的例項
六.後記
因為附加資訊的傳遞在RPC中扮演了很重要的角色,我潛意識裡覺得,肯定會有更加簡潔的方法或者框架我還沒有了解到。希望各位各位讀者老師能不吝