1. 程式人生 > >Spring Cloud中基於Sleuth的引數透傳功能探索

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拿到附加資訊了

思考:

  1. 如果我知道怎麼用無侵入的方式,在當前執行緒”T”建立子孫執行緒”T1”、”T1-1”時,將資料傳給後代,就能解決這個問題了
  2. 微服務呼叫鏈框架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");
    }
}

五.留下的坑

  1. Sleuth通過LazyTraceExecutor解決多執行緒下的問題,但是它並沒有解決給手動建立的Thread傳遞資訊的問題
  2. 有機會試試java位元組碼替換怎麼操作
  3. Sleuth如何重寫RestTemplate的
  4. TraceFeignClient怎麼生成Client的例項

六.後記

因為附加資訊的傳遞在RPC中扮演了很重要的角色,我潛意識裡覺得,肯定會有更加簡潔的方法或者框架我還沒有了解到。希望各位各位讀者老師能不吝