一探究竟新一代可觀測標準 OpenTelemetry
什麼是可觀測
相信大家日常經常使用kibana
、grafana
、jaeger
等平臺觀察系統狀態和定位問題,這些就是可觀測體系的一部分。可觀測主要包括:
-
日誌,實現有
ELK(es/logstash/kibana)
等 -
指標,實現有
grafana
+promthues
等 -
追蹤,基於
opentracing
協議的實現有jaeger
、skywalking
等
我們排查問題過程,一般都會把三者日誌、指標、追蹤結合來看,比如通過接口異常增量指標發現問題 ---> 鏈路追蹤定位異常服務 ---> 排查異常服務日誌,所以關於可觀測我們經常可以看見這個經典的圖片:
什麼是OpenTelemetry
關於可觀測除了上述的各種實現外,還有另一套實現OpenCensus
,於是誕生統一標準OpenTelemetry
且兼容OpenTracing
,OpenCensus
。不過關於 go 語言OpenTelemetry
的統一 sdk 實現還不完善,比如目前還不支持日誌,具體可以查看https://github.com/open-telemetry/opentelemetry-go
。
接下來,我們基於 Go 來看看,原 sdk(也就是未使用OpenTelemetry
) 接入指標和追蹤的方式和基於OpenTelemetry
新體系的指標和追蹤接入方式區別。
可觀測之指標
基於原生promethues
sdk 的指標採集演示
Go 版本 這裏用的 1.14
主要依賴的包:
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
就依賴了兩個包,使用比較簡單:
-
單獨創建一個 server 使用
github.com/prometheus/client_golang/prometheus/promhttp
暴露指標 -
使用
github.com/prometheus/client_golang/prometheus
創建自定義指標,比如NewCounterVec
創建計數器、HistogramVec
創建直方圖等等。 -
WithLabelValues
給自定義指標打標籤
代碼示例如下:
docker 示例:https://github.com/TIGERB/easy-tips/tree/master/docker/grafana-promethues/go-demo/main.go.promethues
package main
import (
"net/http"
// 引入prometheus sdk
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
// 自定義接口請求次數自定義指標
GlobalApiCounter *prometheus.CounterVec
)
func init() {
// 初始化接口請求次數自定義指標
GlobalApiCounter = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "demo_api_request_counter",
Help: "接口請求次數自定義指標",
},
[]string{"domain", "uri"}, // 域名和接口
)
prometheus.MustRegister(GlobalApiCounter)
}
func main() {
go (func() {
// 創建一個獨立的server export暴露Go指標 避免通過業務服務暴露到外網
metricServer := http.NewServeMux()
metricServer.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":2112", metricServer)
})()
// 使用默認server
http.HandleFunc("/v1/demo", func(w http.ResponseWriter, r *http.Request) {
// 自定義計數
GlobalApiCounter.WithLabelValues(r.Host, r.RequestURI).Inc()
w.Write([]byte("test"))
})
// 啓動一個http服務並監聽6060端口 這裏第二個參數可以指定handler
http.ListenAndServe(":6060", nil)
}
訪問接口curl 127.0.0.1:6060/v1/demo
後,查看指標輸出 curl 127.0.0.1:2112/metrics
,如下:
# HELP demo_api_request_counter 接口請求次數自定義指標# TYPE demo_api_request_counter counterdemo_api_request_counter{doamin="127.0.0.1:6060",uri="/v1/demo"} 4
基於OpenTelemetry
sdk 的promethues
指標採集演示
Go 版本 這裏用的 1.19 版本太低報錯
主要依賴的包:
"github.com/prometheus/client_golang/prometheus/promhttp"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/prometheus"
"go.opentelemetry.io/otel/metric"metricsdk "go.opentelemetry.io/otel/sdk/metric"
相對於原生 prome 只使用兩個包,引入的包多幾個:
-
相同點:仍然單獨創建一個 server 使用
github.com/prometheus/client_golang/prometheus/promhttp
暴露指標 -
不同點:使用
go.opentelemetry.io/otel/exporters/prometheus
初始化一個指標對象meter
-
不同點:使用
meter.Int64Counter
初始化計數器、直方圖等 -
不同點:
metric.WithAttributes
打標籤
代碼示例如下:
docker 示例:https://github.com/TIGERB/easy-tips/tree/master/docker/grafana-promethues/go-demo/main.go
package main
import (
"context"
"net/http"
// 引入prometheus sdk
"github.com/prometheus/client_golang/prometheus/promhttp"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/prometheus"
"go.opentelemetry.io/otel/metric"
metricsdk "go.opentelemetry.io/otel/sdk/metric"
)
var meter metric.Meter
func init() {
// 初始化指標meter
mexp, err := prometheus.New()
if err != nil {
panic(err)
}
meter = metricsdk.NewMeterProvider(metricsdk.WithReader(mexp)).Meter("http-demo")
}
func main() {
// 集成指標
// https://github.com/open-telemetry/opentelemetry-go/blob/main/example/prometheus/main.go
// 創建一個接口訪問計數器
urlCouter, _ := meter.Int64Counter("demo_api_request_counter", metric.WithDescription("QPS"))
go (func() {
// 創建一個獨立的server export暴露Go指標 避免通過業務服務暴露到外網
metricServer := http.NewServeMux()
metricServer.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":2112", metricServer)
})()
// 使用默認server
http.HandleFunc("/v1/demo", func(w http.ResponseWriter, r *http.Request) {
// 自定義計數
opt := metric.WithAttributes(attribute.Key("domain").String(r.Host), attribute.Key("uri").String(r.RequestURI))
urlCouter.Add(context.Background(), 1, opt) // 計數
w.Write([]byte("test"))
})
// 啓動一個http服務並監聽6060端口 這裏第二個參數可以指定handler
http.ListenAndServe(":6060", nil)
}
訪問接口curl 127.0.0.1:6060/v1/demo
後,查看指標輸出 curl 127.0.0.1:2112/metrics
,如下:
# HELP demo_api_request_counter_total QPS# TYPE demo_api_request_counter_total counterdemo_api_request_counter_total{domain="127.0.0.1:6060",otel_scope_} 1
可觀測之追蹤
基於OpenTracing
sdk 的jaeger
追蹤演示
Go 版本 這裏用的 1.18
主要依賴的包:
"github.com/grpc-ecosystem/go-grpc-middleware/tracing/opentracing"
"github.com/opentracing/opentracing-go"
"github.com/uber/jaeger-client-go"
"github.com/uber/jaeger-client-go/transport"
使用方式:
-
使用
jaeger.NewTracer
創建一個 tracer 對象tracer
,並指定 jaeger 服務地址 (這裏採用的是非 agent 方式演示) -
使用
tracer
創建一個 spantracer.StartSpan
,並在你想追蹤的代碼段後面span.Finish()
-
這裏上游使用
grpc
服務做測試,使用 opentracing 中間件 SDKgo-grpc-middleware/tracing/opentracing
代碼示例如下:
docker 示例:https://github.com/TIGERB/easy-tips/tree/master/docker/go-jaeger
package main
import (
// 導入net/http包
"context"
"http-demo/demov1"
"io"
"net/http"
"google.golang.org/grpc"
grpc_opentracing "github.com/grpc-ecosystem/go-grpc-middleware/tracing/opentracing"
opentracing "github.com/opentracing/opentracing-go"
jaeger "github.com/uber/jaeger-client-go"
"github.com/uber/jaeger-client-go/transport"
)
var (
// 創建一個tracer對象
tracer opentracing.Tracer
)
func main() {
// 指定上報數據的jaeger服務地址
sender := transport.NewHTTPTransport(
"http://go-jaeger-jaeger-demo:14268/api/traces",
)
var closer io.Closer
tracer, closer = jaeger.NewTracer(
"http-demo",
jaeger.NewConstSampler(true),
jaeger.NewRemoteReporter(sender),
)
defer closer.Close()
http.HandleFunc("/v1/demo", func(w http.ResponseWriter, r *http.Request) {
// 創建一個`span`
span := tracer.StartSpan("demo_span_1")
defer span.Finish()
name, err := demoGrpcReq()
if err != nil {
w.Write([]byte(err.Error()))
}
// 寫入響應內容
w.Write([]byte(name))
})
// 啓動一個http服務並監聽6060端口 這裏第二個參數可以指定handler
http.ListenAndServe(":6060", nil)
}
func demoGrpcReq() (string, error) {
// 使用opentracing中間件SDK go-grpc-middleware/tracing/opentracing
conn, err := grpc.Dial("grpc-demo:1010", grpc.WithInsecure(), grpc.WithUnaryInterceptor(grpc_opentracing.UnaryClientInterceptor(
grpc_opentracing.WithTracer(tracer),
)))
if err != nil {
return "", err
}
defer conn.Close()
client := demov1.NewGreeterClient(conn)
resp, err := client.SayHello(context.TODO(), &demov1.HelloRequest{
Name: "http request",
})
if err != nil {
return "", err
}
return resp.GetMessage(), nil
}
使用docker-compose up -d
啓動演示服務,模擬請求curl 127.0.0.1:6060/v1/demo
,查看 jaeger 後臺http://localhost:16686/
,示例如下:
基於OpenTelemetry
sdk 的jaeger
追蹤演示
Go 版本 這裏用的 1.19 版本太低報錯
主要依賴的包:
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/jaeger"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
"go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/semconv/v1.17.0"
使用方式:
-
使用
"go.opentelemetry.io/otel/exporters/jaeger"
初始化追蹤 tracer,同樣指定 jaeger 服務地址 (這裏採用的是非 agent 方式演示) -
http 服務進行鏈路追蹤,使用包
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
.NewHandler
創建 http handler -
grpc 服務進行鏈路追蹤,使用 grpc otel 追蹤 sdk go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc
代碼示例如下:
docker 示例:https://github.com/TIGERB/easy-tips/tree/master/docker/go-otel
package main
import (
// 導入net/http包
"context"
"http-demo/demov1"
"net/http"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"google.golang.org/grpc"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/jaeger"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
tracesdk "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
)
// 追蹤
var tracer *tracesdk.TracerProvider
func init() {
// 初始化追蹤tracer
// https://github.com/open-telemetry/opentelemetry-go/blob/main/example/jaeger/main.go
// Create the Jaeger exporter
exp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint("http://jaeger-demo:14268/api/traces")))
if err != nil {
panic(err)
return
}
tracer = tracesdk.NewTracerProvider(
tracesdk.WithBatcher(exp),
tracesdk.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String("http-demo"),
)),
)
}
// 常見框架集成opentelemetry SDK
// https://github.com/open-telemetry/opentelemetry-go-contrib/tree/main/instrumentation
func main() {
otel.SetTracerProvider(tracer)
// 關聯上下文
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
defer tracer.Shutdown(ctx)
// /v1/demo接口 業務邏輯handler
demoHandler := func(w http.ResponseWriter, r *http.Request) {
// 調用demo grpc接口
name, err := demoGrpcReq()
if err != nil {
w.Write([]byte(err.Error()))
}
// 寫入響應內容
w.Write([]byte(name))
}
// 集成鏈路追蹤
// https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/instrumentation/net/http/otelhttp/example/server/server.go
otelHandler := otelhttp.NewHandler(http.HandlerFunc(demoHandler), "otelhttp demo test")
http.Handle("/v1/demo", otelHandler)
// 啓動一個http服務並監聽6060端口 這裏第二個參數可以指定handler
http.ListenAndServe(":6060", nil)
}
func demoGrpcReq() (string, error) {
// 使用grpc otel追蹤sdk go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc
conn, err := grpc.Dial("grpc-demo:1010",
grpc.WithInsecure(),
grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),
grpc.WithStreamInterceptor(otelgrpc.StreamClientInterceptor()),
)
if err != nil {
return "", err
}
defer conn.Close()
client := demov1.NewGreeterClient(conn)
resp, err := client.SayHello(context.TODO(), &demov1.HelloRequest{
Name: "http request",
})
if err != nil {
return "", err
}
return resp.GetMessage(), nil
}
使用docker-compose up -d
啓動演示服務,模擬請求curl 127.0.0.1:6060/v1/demo
,查看 jaeger 後臺http://localhost:16686/
,示例如下:
總結
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/P-JmlfDPzcKJjsQ1M1Mo_A