zipkin:爲阿里雲Ons/RocketMQ加上鍊路跟蹤(二)

上一篇文章簡單的介紹瞭如何把zipkin server、brave插件跑起來,這篇文章介紹一下如何把阿里雲Ons/RocketMQ添加到鏈路跟蹤裏面來

背景

RocketMQ爲阿里巴巴開源的一款消息中間件,阿里雲的Ons服務可以看做成商業閉源版RocketMQ。爲了敘述方便,下面統稱RocketMQ

zipkin官方出品的brave已經包含了很多中間件插件了,比如dubbookhttp3kafka等,但是在國內比較常用的RocketMQ沒有,尤其是部署在阿里雲上的應用大部分都使用了RocketMQ作爲消息中間件。

作爲鏈路中非常重要的一環,不能讓鏈路信息在此中斷,讓我們研究一下如何爲RocketMQ增加鏈路跟蹤能力吧。恰好RocketMQ爲開發擴展提供了支持。

實現

這一章節就來聊一聊如何給RocketMQ添加鏈路跟蹤支持。不過開始之前先來了解下brave是如何對鏈路信息就行傳播的吧。

brave的傳播邏輯

在介紹brave傳播邏輯之前,先想一下傳遞鏈路信息應該有哪些步驟:

  1. 上游封裝當前鏈路信息並放到載體中
  2. 載體將鏈路信息傳遞到下游
  3. 下游解析載體中的鏈路信息並放到鏈路上下文

注:載體可以是http的header、rpc的attachment等任何可以傳遞鏈路信息的傳遞途徑。

來看一下官方如何爲dubbo鏈路添加追蹤信息的吧:
brave.dubbo.rpc.TracingFilter.java

//提取&解析載體鏈路信息邏輯
TraceContext.Extractor<DubboServerRequest> extractor =  tracing.propagation().extractor(GETTER);
//封裝當前鏈路到載體
TraceContext.Injector<DubboClientRequest> injector = tracing.propagation().injector(SETTER);

@Override 
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcExcept
  if (!isInit) return invoker.invoke(invocation);
  RpcContext rpcContext = RpcContext.getContext();
  //判斷是server還是client端
  Kind kind = rpcContext.isProviderSide() ? Kind.SERVER : Kind.CLIENT;
  final Span span;
  if (kind.equals(Kind.CLIENT)) {
  	//若client端,則對鏈路信息進行提取並封裝到attachment中
    Map<String, String> attachments = RpcContext.getContext().getAttachments();
    DubboClientRequest request = new DubboClientRequest(invocation, attachments);
    span = tracer.nextSpan(clientSampler, request);
    //將鏈路信息注入到載體attachment中
    injector.inject(span.context(), request);
  } else {
  	//若是server端,則解析載體中的鏈路信息並注入到上下文中
    DubboServerRequest request = new DubboServerRequest(invocation, invocation.getAttachme
    TraceContextOrSamplingFlags extracted = extractor.extract(request);
    //解析載體中的鏈路信息
    span = nextSpan(extracted, request);
  }
  if (!span.isNoop()) {
    span.kind(kind);
    String service = invoker.getInterface().getSimpleName();
    String method = RpcUtils.getMethodName(invocation);
    span.name(service + "/" + method);
    parseRemoteAddress(rpcContext, span);
    span.start();
  }
  boolean isOneway = false, deferFinish = false;
  try (CurrentTraceContext.Scope scope = current.newScope(span.context())) {
    Result result = invoker.invoke(invocation);
    isOneway = RpcUtils.isOneway(invoker.getUrl(), invocation);
    if (!span.isNoop()) {
      deferFinish = ensureSpanFinishes(rpcContext, span, result);
    }
    return result;
  } catch (Error | RuntimeException e) {
    onError(e, span);
    throw e;
  } finally {
    if (isOneway) {
      span.flush();
    } else if (!deferFinish) {
      span.finish();
    }
  }
}

代碼不多,慢慢品味一下,就不展開講了。若有哪裏不懂,歡迎留言討論~
主要聊一下傳播這裏的代碼邏輯:GETTER&SETTER

static final Getter<DubboServerRequest, String> GETTER =
    new Getter<DubboServerRequest, String>() {
      @Override public String get(DubboServerRequest request, String key) {
      	//從request中的attachment中進行獲取
        return request.getAttachment(key);
      }
    };
    
static final Setter<DubboClientRequest, String> SETTER =
    new Setter<DubboClientRequest, String>() {
      @Override public void put(DubboClientRequest request, String key, String value) {
      	//將鏈路信息放到attachment中
        request.putAttachment(key, value);
      }
    };

在上面的代碼中可以看到:brave是如何從載體中提取&注入鏈路信息到載體的。而這裏的載體就是request中的attachment。
在dubbo這個例子中,我們可以窺探到一個完整的鏈路傳遞需要的步驟以及爲實現自定義的鏈路跟蹤打下基礎。下面就讓我們來爲RocketMQ加上鍊路跟蹤吧!

RocketMQ的擴展點及核心生產消費邏輯

在動工之前,先來看看RocketMQ留給開發的擴展點。
注:以下使用到的阿里雲Ons/RocketMQ的maven座標爲

<dependency>
    <groupId>com.aliyun.openservices</groupId>
    <artifactId>ons-client</artifactId>
    <version>1.8.0.Final</version>
</dependency>

擴展點

打開RocketMQ的Message.java源碼可以明顯的看到一個叫做userProperties的Properties成員變量。對,這個就是RocketMQ留給開發的擴展點。消息在生產時放到userProperties中的數據會在被消費時被讀取到,可以利用這一點傳遞鏈路消息。
Message.java

Producer

首先看看生產者常規發送消息的代碼

private String send(String data, String key, String tag) {
    Message msg = new Message(topic, tag, data.getBytes());
    SendResult sendResult = producer.send(msg);
    String messageId = sendResult.getMessageId();
    return messageId;
}

再看看producer的uml圖
producer uml圖
可以看到上述代碼使用的是Producer接口的send(Message)方法,而具體實現則是ProducerImpl類。看到它們的繼承關係後就很容易的想到使用裝飾器模式:在真正調用ProducerImpl#send方法之前把鏈路信息發到userProperties中。核心實現代碼:

private <T> T traceMessage(Message message, Function<Message, T> func) {                               
    Tracer tracer = tracing.tracer();                                                                  
    Span span = tracer.nextSpan().name(OnsConstants.ONS_SEND).start();                                 
    //設置brave鏈路信息傳播行爲                                                                                  
    TraceContext.Injector<Properties> injector = tracing.propagation().injector(OnsPropagation.SETTER);
    Properties properties = new Properties();                                                          
    //將鏈路信息注入到userProperties中                                                                          
    injector.inject(span.context(), properties);                                                       
    message.setUserProperties(properties);                                                             
    span.kind(Kind.PRODUCER);                                                                          
    try (SpanInScope ws = tracer.withSpanInScope(span)){                                               
        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;                                                                                    
    } catch (Exception e){                                                                             
        span.error(e);                                                                                 
        throw e;                                                                                       
    } finally {                                                                                        
        span.finish();                                                                                 
    }                                                                                                  
}                                                                                                      

相信大部分代碼大家都看得懂,但需要注意一下這行代碼

SpanInScope ws = tracer.withSpanInScope(span)

這行代碼非常關鍵,主要有兩個點:

  1. 可以簡單理解爲brave幫忙管理了span的生命週期;
  2. brave會幫忙將traceId、spanId等信息放在MDC中,使業務日誌能夠打印traceId。

Consumer

說完Producer,再來看看Consumer。
有了Producer的鏈路擴展經驗,實現Consumer的鏈路擴展就是分分鐘的事情了。
老套路,先來看看消費消息的常規邏輯代碼:

public void init() {
    Properties pretreatProperties = new Properties();
    pretreatProperties.put(PropertyKeyConst.GROUP_ID, "gid");
    pretreatProperties.put(PropertyKeyConst.AccessKey, "ak");
    pretreatProperties.put(PropertyKeyConst.SecretKey, "sk");
    pretreatProperties.put(PropertyKeyConst.NAMESRV_ADDR, "addr");
    pretreatProperties.put(PropertyKeyConst.ConsumeThreadNums, 10);
    Consumer consumer = ONSFactory.createConsumer(pretreatProperties);
    consumer.subscribe("topic","*", createMessageListener());
    consumer.start();
}

private MessageListener createMessageListener() {
    return (message, context) -> {
        try {
            consumer(message);
            return Action.CommitMessage;
        } catch (Exception e) {
            return Action.ReconsumeLater;
        }
    };
}

好傢伙,消費消息的核心代碼就在MessageListener接口的實現類中,而具體的實現取決於開發者。那思路就很明瞭了,還是裝飾器模式:對開發同學實現的MessageListener進行包裝,增加解析Producer放在userProperties中的鏈路信息並放在鏈路上下文中。

這裏有一個需要注意的點:怎麼把開發同學的實現的MessageListener給包裝起來?
最好的時機是在調用Consumer#subscribe方法時進行包裝,這麼做的前提需要把subscribe方法進行重寫。換句話說在調用subscribe方法之前將consumer替換成自己的,這個好辦。
貼一下核心代碼:

/**
 * @author liumian  2020/1/7 11:41 上午
 */
public class OnsConsumerDecorator implements Consumer {

    private Consumer consumer;

    private Tracing tracing;
    
    @Override
    public void subscribe(String topic, String subExpression, MessageListener listener) {
        OnsMessageListenerDecorator messageListenerDecorator = new OnsMessageListenerDecorator(tracing,listener);
        consumer.subscribe(topic,subExpression,messageListenerDecorator);
    }
}

/**
 * @author liumian  2020/1/7 11:13 上午
 */
public class OnsMessageListenerDecorator implements MessageListener {

    private Tracing tracing;

    private MessageListener messageListener;

    public OnsMessageListenerDecorator(Tracing tracing, MessageListener messageListener) {
        this.tracing = tracing;
        this.messageListener = messageListener;
    }

    @Override
    public Action consume(Message message, ConsumeContext consumeContext) {
    	//獲取producer的鏈路信息
        Properties properties = message.getUserProperties();
        //設置brave鏈路信息傳播解析規則
        TraceContext.Extractor<Properties> extracted = tracing.propagation().extractor(OnsPropagation.GETTER);
        TraceContextOrSamplingFlags traceInfo = extracted.extract(properties);
        Tracer tracer = tracing.tracer();
        Span span;
        //判斷解析出來的鏈路信息是否存在:若不存在則開啓一條新的鏈路
        if (traceInfo != null && traceInfo.context() != null){
            span = tracer.newChild(traceInfo.context()).name(OnsConstants.ONS_CONSUME).start();
        } else {
            span = tracer.newTrace().name(OnsConstants.ONS_CONSUME).start();
        }
        span.kind(Kind.CONSUMER);
        try(SpanInScope ws = tracer.withSpanInScope(span)){
            Action res = messageListener.consume(message, consumeContext);
            span.tag("mq.consumeResult", res.name());
            return res;
        } catch (Exception e){
            span.error(e);
            throw e;
        } finally {
            span.tag("mq.topic",message.getTopic());
            span.tag("mq.msgId",message.getMsgID());
            span.finish();
        }

    }
}

註釋比較詳細,大家應該能輕鬆看懂,這裏就不展開了。

鏈路傳播行爲

大家別忘了要定義鏈路傳播行爲哦,也就是描述brave如何將鏈路信息注入到載體和從載體中解析鏈路信息的行爲。

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

    public static final Propagation.Getter<Properties, String> GETTER = (carrier, key) -> {
        String value = carrier.getProperty(key);
        if (value == null|| value.isEmpty()){
            return null;
        } else {
            return value;
        }
    };

    OnsPropagation() {
    }
}

至此,爲RocketMQ添加鏈路跟蹤就完成了。整個實現下來還是很輕鬆的,如果需要完成的鏈路代碼,請留言。

最後

留在最後的是吐槽:
公司不僅用了阿里雲Ons服務,在很多地方還用了阿里雲mns服務(也是一款消息中間件,非常輕量級)。於是乎想着給mns服務也加上鍊路跟蹤,但是調研一通下來發現mns壓根就沒考慮到留給開發人員進行擴展:

  1. jar包裏面的類都是final,無法進行繼承(當然可以動態代理解決,更爲致命的是第二條)
  2. 在mns的消息體中沒有RocketMQ的userProperties字段,mns只有且只傳遞messageBody。

第二條造成了開發人員無法優雅的進行擴展,若要擴展則必須對消息體進行侵入。對消息體進行侵入,如果不小心非常容易出生產事故。這是不能接受的。
最後放棄了對mns的鏈路跟蹤,而沒有鏈路跟蹤的mns服務註定在以後的技術選型中佔不到優勢。

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