OpenTelemetry 在雲原生 PaaS 中的落地實踐

背景

端點科技是一家 ToB 的軟件產品供應商,在長期自研軟件產品和給規模型企業交付的過程中,逐漸總結沉澱出一款面向多雲的 PaaS 平臺 Erda 來作爲企業數字化的底座。我之前曾在端點擔任 PaaS 平臺架構師的時候,負責設計和實現了其中的監控平臺,這個平臺的初始目標包括:

  1. 1. 監控客戶環境的多雲 Kubernetes 集羣

  2. 2. 監控 PaaS 自身的運行狀態

  3. 3. 監控運行在 PaaS 平臺上面的業務系統性能

爲了實現上面的目標,Erda 的監控平臺也經歷了監控到 APM 到可觀測性平臺的演進過程,接下來我們具體看一下在每個階段做的事情和完成的效果。

監控系統演進

最初,我們基於 Telegraf 作爲數據採集器,並二開了大量 Input 插件來滿足 PaaS 內部對 Mesos 和容器的監控需求( 在 17 年實現第一版 PaaS 的時候,使用了 DC/OS 作爲底層容器平臺,從 19 年開始才逐步切換到 kubernetes ),在後端基於 Goka 實現了數據的消費和實時聚合的能力,然後把數據存儲到 KairosDB 中。這個流程大概如下圖所示:

隨着 PaaS 開始逐步交付到業務部門,這個監控系統的問題也逐漸被暴露出來:

  1. 1. 使用了硬編碼的方式預創建時序數據表,導致每增加一種監控指標就需要代碼實現,版本迭代後才能上線

  2. 2. 用戶開始創建大量告警規則後對 TSDB 的查詢壓力過大

  3. 3. 業務團隊對應用監控的需求日漸強烈等

  4. 4. 在達到千萬級的序列規模後,Goka 和 KairosDB 的性能壓力也開始凸顯出來

爲解決上面的問題,我們開始思考和設計第二代監控系統,和前面的監控系統相比,新的系統解決了下面的幾個問題:

  1. 1. 設計了指標元數據系統和 Metrics 數據標準化,監控系統內部不再感知指標的具體含義,增加新的指標只需要接入方按照數據規範打點即可

  2. 2. 實現了 Trace 模塊,開發 Java Agent 實現業務系統的鏈路追蹤接入,引入 ES 來存儲 Trace 數據

  3. 3. 爲了實現了對 kubernetes 體系的監控,在 Telegraf 上做了大量的定製開發,同時也把 Telegraf 作爲機器內數據聚合和轉發的 One-Agent

  4. 4. 使用 Flink 流計算平臺替代 GoKa,基於 Flink 實現了 DSL 方式的指標聚合和告警規則計算,減輕對存儲的壓力

監控服務架構圖

這個版本的監控系統架構如圖所示,接下來重點介紹一下 Trace 和 集羣採集端兩個模塊的設計。

APM 的 Trace 實現

在 Trace 的實現中,尤其是採集側語言 SDK 中,我們借鑑了很多 SkyWalking 的設計思路。首先我們在 SkyWalking Java Agent 的內核基礎上只保留了它的插件機制,把 Trace 內核和跨進程傳播協議替換爲 OpenTracing,其次參考了 SkyWalking 提出的 STAM 協議在 OT 的 Baggage 裏傳遞更多的上下文信息,得以在 SDK 側而不是 APM 的服務端進行 Trace 的分析。拿 HttpClient 插件舉例,我們可以使用如下的實現:

public class HttpClientExecuteInterceptor implements InstanceMethodsAroundInterceptor {
    
    @Override
    public void beforeMethod(IMethodInterceptContext context, MethodInterceptResult result) throws Throwable {
        ...
        Tracer tracer = TracerManager.currentTracer();
        SpanContext spanContext = tracer.active() != null ? tracer.active().span().getContext() : null;
        Span span = tracer.buildSpan("HTTP " + httpRequest.getRequestLine().getMethod() + " " + url.getPath()).childOf(spanContext).startActive().span();

        ...
        // 在 baggage 裏注入上下游服務的元數據
        span.getContext().getBaggage().putAll(new ServiceMetaBaggage());
        
        TextMapCarrier carrier = new TextMapCarrier();
        tracer.inject(span.getContext(), carrier);
        ...
    }
}

// 把 ServiceMetaBaggage 展開看一下

public class ServiceMetaBaggage implements Context.ContextIterator<String> {
    ...
        
    public TransactionMetricContext() { 
        map.put(Constants.Metrics.SOURCE_PROJECT_ID, Configs.ServiceConfig.getProjectId());
        map.put(Constants.Metrics.SOURCE_PROJECT_NAME, Configs.ServiceConfig.getProjectName());
        map.put(Constants.Metrics.SOURCE_APPLICATION_ID, Configs.ServiceConfig.getApplicationId()); 
        map.put(Constants.Metrics.SOURCE_SERVICE_NAME, Configs.ServiceConfig.getServiceName());
        map.put(Constants.Metrics.SOURCE_SERVICE_ID, Configs.ServiceConfig.getServiceId());
        ...
    }
}

在業務的 Server 端被 JavaAgent 攔截之後,從 baggage 中讀取到上游服務的信息記錄到自身的 Tag 中,那麼 Server 端記錄的 Span 數據如下所示:

{
    "name""GET /api/order",
    "span_kind""server",
     ...
    "tags"{
        "src_project_id" : "1",
        "src_app_id" : "1",
        "src_service_id" : "1",
        "src_service_name" : "admin",
        "dest_project_id" : "1",
        "dest_app_id" : "2",
        "dest_service_id" : "3",
        "dest_service_name" : "order"
    }
}

同時我們沒有使用 SDK 直連Collector的發送方式,而是在 JavaAgent 把 Span 數據全量推送到宿主機的 Telegraf 中,通過 Telegraf 將命中採樣的 Span 數據轉發到後端 Collector,並使用一個 Go 開發的 Consumer 組件將數據存儲到 ElasticSearch 中。由於每個 Span 已經附帶了調用上下游的數據,也可以很容易的在 Telegraf 的管道內把 Span 聚合爲如下的 Metrics :

  1. 1. service_node 描述服務的節點和實例

  2. 2. service_call_* 描述服務和接口的調用指標,包括 HTTP、RPC、DB 和 Cache

  3. 3. service_call_*_error 描述服務的異常調用,包括 HTTP、RPC、DB 和 Cache

  4. 4. service_relation 描述服務之間的調用關係

通過上面的方式,我們得以在 APM 系統的資源消耗和性能取得相對平衡的情況下實現展示給用戶全量的拓撲、服務調用次數等數據的能力。

集羣採集端實現

採集端的設計如上圖所示,我們在每個集羣中安裝基於 Telegraf 深度定製的 Cluster Agent,負責採集 Kubernetes 和 PaaS 在集羣中部署組件的監控數據,同時這個 Agent 也可以模擬成一個 Prometheus Server,拉取集羣中被自動發現的 Exporters 數據。每個節點上,也會部署一個 Telegraf 的 DaemonSet 作爲 Node Agent,基於我們定製的 Docker Input 插件自動發現節點上的容器,並把容器識別爲 PaaS 平臺定義的 Service、Job 或者 Addon 組件。在上文中我們提到 Telegraf 也會作爲一個本地 Proxy,接收 Pod 中的業務應用 Java Agent 探針上報的應用請求和 Trace 數據,轉發給後端的 Collector 組件。

還有什麼問題

通過上面的一些方案,我們覆蓋了對宿主機、Kubernetes、容器、業務進程的監控數據採集和對 Java 微服務系統的 Trace,但離我們最初的目標還有些差距:

  1. 1. 監控客戶環境的多雲 Kubernetes 集羣基本實現,但定製較多,還需要自己去兼容不同的容器運行時

  2. 2. 監控 PaaS 自身的運行狀態,未實現

  3. 3. 監控運行在 PaaS 平臺上面的業務系統性能,已經實現對 Java 應用系統的 Trace,但客戶也有其他語言比如 PHP、Go 的監控需求

對於問題 1 我們提出使用 Prometheus 來替換 telegraf 作爲集羣內的數據採集方案。由於我們已經實現一套完善 Metrics 流分析、存儲和查詢系統,我們只把 Prometheus 作爲數據採集器,通過在 Collector 中實現 Remote Write 協議接收數據,而無需考慮 Prometheus 的存儲高可用問題。這個時候 Kubernetes 集羣的監控數據鏈路:Prometheus -> Collector Remote Write receiver -> Metrics System -> Query & Dashboard。

接下來我們將更多的關注點放在問題 2 和 3 之上。PaaS 平臺的控制面本身也是使用 Golang 語言開發的一個微服務系統,大概包含 20+ 的微服務模塊。通常情況下,我們會搭建獨立的分佈式追蹤、監控和日誌系統來協助開發團隊解決微服務系統中的診斷和觀測問題。但同時 PaaS 本身也提供了功能齊全的服務觀測能力,而且在社區也有一些追蹤系統(比如 Apache SkyWalking 和 Jaeger)都提供了自身的可觀測性,給我們提供了使用平臺能力觀測自身的另一種思路。

最終,我們選擇了在 PaaS 平臺上實現 PaaS 自身的可觀測,使用該方案的考慮如下:

  1. 1. 平臺已經提供了服務觀測能力,再引入外部平臺造成重複建設,對平臺使用的資源成本也有增加

  2. 2. 開發團隊日常使用自己的平臺來排查故障和性能問題,喫自己的狗糧對產品的提升也有一定的幫助

  3. 3. 對於監控系統的核心組件比如 Kafka 和 數據計算組件,我們通過 SRE 團隊的巡檢工具來旁路覆蓋,並在出問題時觸發報警消息

這時如果把問題 2 和 3 放在一起來看,我們要解決的事情就變成了,如何實現多語言的 Trace 和 Metrics 監控。

接入 OpenTelemetry Trace

OpenTelemetry 是 CNCF 的一個可觀測性項目,由 OpenTracing 和 OpenCensus 合併而來,旨在提供可觀測性領域的標準化方案,解決觀測數據的數據模型、採集、處理、導出等的標準化問題,提供與三方 vendor 無關的服務。 https://opentelemetry.io

我們在社區尋找多語言的 Trace 接入方案時,注意到 OpenTelemetry 項目,在經過調研和對比後我們認爲它不僅可以滿足我們目前的需求,我們還認可 OpenTelemetry 在可觀測性方向上的潛力。

在上文中也提到,我們在實現 Trace 時使用了類似[STAM](https://wu-sheng.github.io/STAM)的傳播協議,OpenTelemetry 的 Trace 協議則沒有包含太多的上下游服務的信息。在這種情況下我們想到兩種方式來做實現,一個是擴展 OpenTelemetry SDK 來實現上文中提到的我們私有的 baggage 注入,另一個方式是使用原生的 OpenTelemetry SDK,在 APM 後端重新實現 OpenTelemetry Trace 數據的聚合和分析。這裏我們使用了方案二,原因如下

1. 我們在之前定製和改造 telegraf 後期難以和社區的版本進行同步,導致我們不得已一直去維護一個分支版本,在引入新的組件時要儘可能的避免再次出現這種情況

  1. 2. 我們觀察到社區越來越多的框架和系統開始集成 OpenTelemetry ,兼容原生 SDK 的數據上報也有利於我們之後更容易的對接其他系統

具體的實現上,如下圖所示,我們在 Collector 組件中實現 otlp 協議的 Receiver,並且在數據消費端實現一個新的 Span Analysis 組件把 otlp 的數據分析爲 PaaS 平臺原有的 Trace 模型和服務 Metrics

其中,Collector (Gateway) 組件使用 Golang 輕量級實現,核心的邏輯是解析 otlp 的 proto 數據,並且添加對租戶數據的鑑權和限流。在 otlp receiver 插件中,我們添加 go.opentelemetry.io/proto/otlp依賴,其中內置了對 otlp proto 數據的解析:

import (
    "github.com/golang/protobuf/proto"
    otlpv1 "go.opentelemetry.io/proto/otlp/trace/v1"
)

...

func ProtoDecoder(req *http.Request, entity interface{}) error {
    contentType := req.Header.Get("Content-Type")
    if _, ok := acceptedFormats[contentType]; !ok {
        return errors.New(fmt.Sprintf("Unsupported content type: %v", html.EscapeString(contentType)))
    }
    if entity, ok := entity.(*pb.PostSpansRequest); ok {
        body, err := readBody(req)
        if err != nil {
            return err
        }
        // 使用proto包的 Unmarshal 函數即可解析otlp上報的數據
        var tracesData otlpv1.TracesData
        err = proto.Unmarshal(body, &tracesData)
        if err != nil {
            return err
        }
        entity.Spans = convertSpans(&tracesData)
    }
    return nil
}

Span_Analysis 組件基於 Flink 實現,通過 DynamicGap 時間窗口,把 OpenTelemetry 的 span 數據聚合分析後產生如下的 Metrics (和上文中提到的在 Agent 端聚合的 Metrics 一樣):

  1. 1. service_node 描述服務的節點和實例

  2. 2. service_call_* 描述服務和接口的調用指標,包括 HTTP、RPC、DB 和 Cache

  3. 3. service_call_*_error 描述服務的異常調用,包括 HTTP、RPC、DB 和 Cache

  4. 4. service_relation 描述服務之間的調用關係

通過上面的方式,我們實現了將 OpenTelemetry 的 Trace 接入到 PaaS 的 APM 系統。

如上文所說,我們把 PaaS 控制面看做一個 Go 開發的微服務系統,那麼以 Go 語言接入爲例,我們可以使用原生的opentelemetry-goSDK 進行接入。首先引入 SDK 依賴:

go get go.opentelemetry.io/otel/sdk

假設 PaaS 部署後暴露的 Collector 接入點爲 https://collector.paas.io/api/otlp/v1/traces,我們在使用 otlp http exporter進行數據上報:

// Init configures an OpenTelemetry exporter and trace provider
func Init(ctx context.Context) *sdktrace.TracerProvider {
    //New otlp exporter
    opts := []otlptracegrpc.Option{
        // 配置上報地址,如 config.yaml 裏已配置,此處可忽略
        otlptracehttp.WithEndpoint("https://collector.paas.io/api/otlp/v1/traces"), 
        otlptracegrpc.WithInsecure(),
    }
    exporter, err := otlptracehttp.New(ctx,opts...)

    tp := sdktrace.NewTracerProvider(
        sdktrace.WithSampler(sdktrace.AlwaysSample()),
        sdktrace.WithBatcher(exporter),
        sdktrace.WithResource(r),
    )
    otel.SetTracerProvider(tp)
    return tp
}

接入的拓撲效果如圖所示 :

鏈路追蹤的效果如圖所示:

總結

Trace 的接入作爲我們的第一步嘗試,在 Opentelemetry 和我們 PaaS 自研的 APM 後端 / 產品功能的集成上的效果還是比較令人滿意的。之後我們也計劃了更多的 Opentelemetry 對接目標(如 Metrics、Log 集成,和使用更推薦的 Opentelemetry Collector Exporter 來上報數據),期望通過 Opentelemetry 來降低上文描述的接入端的架構複雜度和實現可觀測性數據接入的標準化。但可惜的是,之後由於我個人原因的工作變動,上述計劃沒有完成最終的落地。最近受 OpenTelemetry 中文社區的發起人蔣志偉老師約稿,把我在可觀測性系統演進和 OpenTelemetry 落地中的一些經驗分享給社區供大家進行參考,也歡迎大家對本文的內容提出更多的建議和交流。

https://github.com/open-telemetry/docs-cn

歡迎大家關注 “Opentelemetry” 公衆號,這是中國區唯一官方技術公衆號

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/DcTQ3pYU0MOvINJHYWplsg