zipkin:自定義鏈路傳播(四)

  • 鏈路傳播定義的是中間件的Server/Producer端將鏈路信息注入到載體的行爲;以及Client/Consumer如何從載體中抽取鏈路信息的行爲;
  • 載體可以是消息體或者請求體。

背景

上一篇提到通過zipkin:實現zipkin-spring-boot-starter(三)降低接入鏈路跟蹤門檻以後,開發同學接入的積極性提高,同時也反饋目前支持鏈路跟蹤的中間件滿足不了需求,比如說無法跟蹤通過redis實現的輕量級mq以及阿里雲的mns等。

但是站在框架維護者的角度考慮:對這類消息中間件的鏈路跟蹤非常棘手,爲什麼? 因爲這類消息中間件沒有擴展機制,開發者發送到消息隊列中的業務消息就是就是消息的全部。若要對消息進行鏈路跟蹤,那麼勢必會對消息體進行侵入:將鏈路信息放到業務消息中。若侵入用戶消息體,那麼就要對業務消息體的格式有強要求,這是不行的、會引發出很多問題。

那RocketMQ爲什麼沒有這個問題呢?因爲RocketMQ將消息體分成了兩個部分:meta信息和業務數據;不管消息體中的業務數據如何變化都可以將鏈路上下文放到meta信息中,從而實現鏈路跟蹤。

所以現在面臨的問題:

  1. 框架維護者的角度:不能以侵入業務消息體爲代價進行鏈路跟蹤
  2. 業務開發者的角度:需要對此類消息中間件進行鏈路跟蹤

現在是一個兩難的局面,只能大家各往後退一步:

  1. 框架維護者:提供鏈路信息的提取和注入工具
  2. 業務開發者:自行在生產端將當前鏈路信息注入到消息體中;在消費端提取消息體中的鏈路信息提取出來

嗯,也是一個不錯的方案!

zipkin鏈路信息

聊鏈路傳播之前,我們來看看鏈路傳播到底傳播了哪些信息,這是從dubbo鏈路提取出來的鏈路信息:

  • X-B3-SpanId=c4d1b2bf028149ec
  • X-B3-ParentSpanId=bfb6542ce3a8f6ca,
  • X-B3-Sampled=1
  • X-B3-TraceId=bfb6542ce3a8f6ca

簡單解釋一下:

  • X-B3-SpanId:當前span的id
  • X-B3-ParentSpanId:當前span的父spanid
  • X-B3-Sampled:採樣標識符,即是否上報的zipkin-server端
  • X-B3-TraceId:當前trace的id

鏈路傳播

在官方的代碼中,鏈路傳播分爲以下幾個步驟:

1. 定義具體的傳播行爲

  • brave.propagation.Propagation.Setter:將鏈路信息set到指定的載體中
  • brave.propagation.Propagation.Getter:將鏈路信息從指定的載體中get出來

2. 提取或者注入鏈路信息

  • brave.propagation.Propagation#injector:與Setter結合使用,將內存中的鏈路信息注入到載體中
  • brave.propagation.Propagation#extractor:與Getter結合使用,將載體中的鏈路信息提取到內存中

ok,進入實操階段,先看看dubbo鏈路傳播是怎樣實現的

官方dubbo鏈路傳播

1. 定義具體的傳播行爲:將請求體中的attachement作爲鏈路信息的載體,把鏈路信息直接put到attachment中

    static final Setter<DubboClientRequest, String> SETTER =
        new Setter<DubboClientRequest, String>() {
            @Override public void put(DubboClientRequest request, String key, String value) {
                request.putAttachment(key, value);
            }
        };

2. 提取或者注入鏈路信息:提取內存中的鏈路信息並放到載體中

	//1. 設置具體的鏈路傳播行爲
	TraceContext.Injector<DubboClientRequest> injector = tracing.propagation().injector(SETTER);

	//2. 從dubbo請求上下文中獲取載體信息,即attachment
    Map<String, String> attachments = RpcContext.getContext().getAttachments();
    DubboClientRequest request = new DubboClientRequest(invocation, attachments);
    
    //3. 將內存中的鏈路信息設置到載體中
    injector.inject(span.context(), request);

爲了節省篇幅,這裏只分析瞭如何將鏈路信息注入到載體中,如果感興趣可以閱讀zipkin官方dubbo鏈路傳播源碼~

自定義鏈路傳播

1. 定義具體的傳播行爲:考慮到通用性、減少對第三方的依賴,這裏使用java.util.Properties作爲載體

public class CustomPropagation {
    public static final Propagation.Setter<Properties, String> SETTER = (carrier, key, value) -> {
        carrier.setProperty(key, value);
    };

    public static final Propagation.Getter<Properties, String> GETTER = (carrier, key) -> {
        return carrier.getProperty(key);
    };
}

2. 提取或者注入鏈路信息

    /**
     * 將當前鏈路上下文信息注入到開發者業務方法中,適用於redis、mns等鏈路傳遞
     *
     * @param tracing  鏈路核心類
     * @param spanName spanName
     * @param func     開發者的業務方法
     * @param <R>      業務方法的返回值
     * @return 業務方法返回
     */
    public static <R> R injectTraceInfo(Tracing tracing, @NotNull String spanName, Function<Properties, R> func) {
        Tracer tracer = tracing.tracer();
        Span span = tracer.nextSpan().name(spanName).start();
        //設置brave鏈路信息傳播行爲
        TraceContext.Injector<Properties> injector = tracing.propagation().injector(CustomPropagation.SETTER);
        Properties properties = new Properties();
        //將鏈路信息注入到Properties中
        injector.inject(span.context(), properties);
        span.kind(Kind.PRODUCER);
        try (SpanInScope ws = tracer.withSpanInScope(span)) {
        	//執行業務方法
            return func.apply(properties);
        } catch (Exception e) {
            span.error(e);
            throw e;
        } finally {
            span.finish();
        }
    }

    /**
     * 從properties中提取鏈路信息並注入到當前鏈路上下文中,使用redis、mns等鏈路傳遞
     *
     * @param tracing    鏈路核心類
     * @param spanName   spanName
     * @param properties 鏈路信息
     * @param func       開發者的業務方法
     * @param <R>        業務方法的返回值
     * @return 業務方法返回
     */
    public static <R> R extractTraceInfo(Tracing tracing, @NotNull String spanName,
                                         Properties properties, Function<Span, R> func) {
        //設置鏈路傳播行爲
        TraceContext.Extractor<Properties> extracted = tracing.propagation().extractor(CustomPropagation.GETTER);
        //提取載體中的鏈路信息
        TraceContextOrSamplingFlags traceInfo = extracted.extract(properties);
        Tracer tracer = tracing.tracer();
        Span span;
        if (traceInfo != null && traceInfo.context() != null) {
        	//將鏈路信息注入到載體中
            span = tracer.newChild(traceInfo.context()).name(spanName).start();
        } else {
            span = tracer.newTrace().name(spanName).start();
        }
        span.kind(Kind.CONSUMER);
        try (SpanInScope ws = tracer.withSpanInScope(span)) {
            return func.apply(span);
        } catch (Exception e) {
            span.error(e);
            throw e;
        } finally {
            span.finish();
        }
    }

開發者只要在Lambda表達式中傳入業務方法即可將鏈路銜接起來,是不是很方便~

代碼加上註釋不難理解,這裏就不再對代碼單獨解釋了。下面看看RocketMQ如何使用自定義鏈路傳播打通producer和consumer的鏈路

RocketMQ自定義鏈路傳播實戰


    @Override
    public SendResult send(Message message) {
        return traceMessage(message, message1 -> producer.send(message1));
    }

    private <T> T traceMessage(Message message, Function<Message, T> func) {
        return TraceUtil.injectTraceInfo(tracing, "ONS/SEND", properties -> {
            Span span = tracing.tracer().currentSpan();
            //TraceUtil.injectTraceInfo已經將內存中的鏈路信息提取到properties了
            //所以這裏將properties設置到message的元信息中即可
            message.setUserProperties(properties);
            T res = func.apply(message);
            if (res != null && res.getClass() == SendResult.class) {
                String messageId = ((SendResult)res).getMessageId();
                span.tag("mq.msgId", messageId);
            }
            span.tag("mq.topic", message.getTopic());
            return res;
        });
    }

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章