Spring Cloud中基於Sleuth的參數透傳功能探索

一.需求

微服務環境,有A,B,C,D四個服務,調用關係爲:A->B->C->D。用戶在A的頁面選擇當前「語言」環境爲「英文」,在某些業務場景下,其它幾個服務需獲取到這個「語言」信息。java

二.分析

這個需求仍是很簡單的,相似於「擊鼓傳花」:當前服務從上一個服務中獲取參數,並傳給下一個服務。我的感受基本上全部的RPC框架都會遇到這個問題,只是之前SOA架構下,服務層級比較少,將「語言」、「登錄」等附加信息放在參數列表中並不會帶來太多工做量,因此這個問題並非太突出。而引入了微服務架構思想後,服務調用層級急劇增加,這就須要一個更加優雅的方式來解決附加信息的傳遞問題。git

三.方案探索

3.1 方案一:參數放在接口參數列表中

優勢:思路簡單,開發沒有學習成本github

缺點spring

  • 代碼高度耦合:附加信息卻要每一個接口都顯式維護
  • 升級困難:若是未來再加一個參數,全部層級的接都要改動
  • 引發迷惑:若是B服務的邏輯不須要「語言「參數,可是由於D須要,它也必須維護
  • 太傻了,Big不夠

思考:微服務之間絕大多數狀況是經過HTTP調用的,HTTP的header中也能夠放參數信息。這樣,接口參數中就不用維護這些附加信了。緩存


3.2 方案二:參數放在httpRequest的header中

實現
1.自定義一個Filter,獲取Request中本身須要的附加信息,
2.將這些信息放入ThreadLocal中,
3.實現feign.Client(這裏先忽略RestTemplate)的execute()方法,將附件信息在調用下一層服務前塞入request的header中session

優勢:參數解耦多線程

缺點:若是B在獲取到附加信息後,新起了一個線程」T1「來調用服務C,這時T1就沒法從HhreaLocal拿到附加信息了架構

思考:app

  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中扮演了很重要的角色,我潛意識裏以爲,確定會有更加簡潔的方法或者框架我尚未了解到。但願各位各位讀者老師能不吝珠玉,批評指正

相關文章
相關標籤/搜索