乐趣区

关于云原生:终极套娃-20|云原生-PaaS-平台的可观测性实践分享

某个周一上午,小涛像平常一样泡上一杯热咖啡 ☕️,筹备关上我的项目协同开始新一天的工作,忽然隔壁的小文喊道:“快看,用户反对群里炸锅了 …”

用户 A:“Git 服务有点问题,代码提交失败了!”
用户 B:“帮忙看一下,执行流水线报错……”
用户 C:“咱们的零碎明天要上线,当初部署页面都打不开了,都要急坏了!”
用户 D:……

小涛只得先放下手中的咖啡,屏幕切换到堡垒机,登录到服务器上一套行云流水的操作,“哦,原来是上周末上线的代码漏了一个参数验证造成 panic 了”,小涛指着屏幕上一段容器的日志对小文说到。

十分钟后,小文应用修复后的安装包更新了线上的零碎,用户的问题也失去了解决。

尽管故障修复了,然而小涛也陷入了深思,“ 为什么咱们没有在用户之前感知到零碎的异样呢? 当初排查问题还须要登录到堡垒机上看容器的日志, 有没有更快捷的形式和更短的工夫里排查到线上故障产生的起因?

这时,坐在对面的小 L 说道:“咱们都在给用户讲帮忙他们实现零碎的可观测性,是时候 Erda 也须要被观测了。”

小涛:“那要怎么做呢…?”且听咱们娓娓道来~

通常状况下,咱们会搭建独立的分布式追踪、监控和日志零碎来帮助开发团队解决微服务零碎中的诊断和观测问题。但同时 Erda 自身也提供了功能齐全的服务观测能力,而且在社区也有一些追踪零碎(比方 Apache SkyWalking 和 Jaeger)都提供了本身的可观测性,给咱们提供了应用平台能力观测本身的另一种思路。

最终,咱们抉择了在 Erda 平台上实现 Erda 本身的可观测,应用该计划的思考如下:

  • 平台曾经提供了服务观测能力,再引入内部平台造成反复建设,对平台应用的资源老本也有减少
  • 开发团队日常应用本人的平台来排查故障和性能问题,吃本人的狗粮对产品的晋升也有肯定的帮忙
  • 对于可观测性零碎的外围组件比方 Kafka 和 数据计算组件,咱们通过 SRE 团队的巡检工具来旁路笼罩,并在出问题时触发报警音讯

Erda 微服务观测平台提供了 APM、用户体验监控、链路追踪、日志剖析等不同视角的观测和诊断工具,本着物尽其用的准则,咱们也把 Erda 产生的不同观测数据别离进行了解决,具体的实现细节且持续往下看。

OpenTelemetry 数据接入

在之前的文章里咱们介绍了如何在 Erda 上接入 Jaeger Trace,首先咱们想到的也是应用 Jaeger Go SDK 作为链路追踪的实现,但 Jaeger 作为次要实现的 OpenTracing 曾经进行保护,因而咱们把眼光放到了新一代的可观测性规范 OpenTelemetry 下面。

OpenTelemetry 是 CNCF 的一个可观测性我的项目,由 OpenTracing 和 OpenCensus 合并而来,旨在提供可观测性畛域的标准化计划,解决观测数据的数据模型、采集、解决、导出等的标准化问题,提供与三方 vendor 无关的服务。

如下图所示,在 Erda 可观测性平台接入 OpenTelemetry 的 Trace 数据,咱们需要在 gateway 组件实现 otlp 协定的 receiver,并且在数据生产端实现一个新的 span analysis 组件把 otlp 的数据分析为 Erda APM 的可观测性数据模型。


OpenTelemetry 数据接入和解决流程

其中,gateway 组件应用 Golang 轻量级实现,外围的逻辑是解析 otlp 的 proto 数据,并且增加对租户数据的鉴权和限流。

要害代码参考 receivers/opentelemetry

span_analysis 组件基于 Flink 实现,通过 DynamicGap 工夫窗口,把 opentelemetry 的 span 数据聚合剖析后产生如下的 Metrics:

  • service_node 形容服务的节点和实例
  • service_call_* 形容服务和接口的调用指标,包含 HTTP、RPC、DB 和 Cache
  • service_call_*_error 形容服务的异样调用,包含 HTTP、RPC、DB 和 Cache
  • service_relation 形容服务之间的调用关系

同时 span_analysis 也会把 otlp 的 span 转换为 Erda 的 span 规范模型,将下面的 metrics 和转换后的 span 数据流转到 kafka,再被 Erda 可观测性平台的现有数据生产组件生产和存储。

要害代码参考 analyzer/tracing

通过下面的形式,咱们就实现了 Erda 对 OpenTelemetry Trace 数据的接入和解决。

接下来,咱们再来看一下 Erda 本身的服务是如何对接 OpenTelemetry。

Golang 无侵入的调用拦挡

Erda 作为一款云原生 PaaS 平台,也天经地义的应用云原生畛域最风行的 Golang 进行开发实现,但在 Erda 晚期的时候,咱们并没有在任何平台的逻辑中预置追踪的埋点。所以即便在 OpenTelemetry 提供了开箱即用的 Go SDK 的状况下,咱们只在外围逻辑中进行手动的 Span 接入都是一个须要投入微小老本的工作。

在我之前的 Java 和 .NET Core 我的项目教训中,都会应用 AOP 的形式来实现性能和调用链路埋点这类非业务相干的逻辑。尽管 Golang 语言并没有提供相似 Java Agent 的机制容许咱们在程序运行中批改代码逻辑,但咱们仍从 monkey 我的项目中受到了启发,并在对 monkey、pinpoint-apm/go-aop-agent 和 gohook 进行充沛的比照和测试后,咱们抉择了应用 gohook 作为 Erda 的 AOP 实现思路,最终在 erda-infra 中提供了主动追踪埋点的实现。

对于 monkey 的原理能够参考 monkey-patching-in-go

以 http-server 的主动追踪为例,咱们的外围实现如下:

//go:linkname serverHandler net/http.serverHandler
type serverHandler struct {srv *http.Server}

//go:linkname serveHTTP net/http.serverHandler.ServeHTTP
//go:noinline
func serveHTTP(s *serverHandler, rw http.ResponseWriter, req *http.Request)

//go:noinline
func originalServeHTTP(s *serverHandler, rw http.ResponseWriter, req *http.Request) {}

var tracedServerHandler = otelhttp.NewHandler(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {injectcontext.SetContext(r.Context())
  defer injectcontext.ClearContext()
  s := getServerHandler(r.Context())
  originalServeHTTP(s, rw, r)
}), "", otelhttp.WithSpanNameFormatter(func(operation string, r *http.Request) string {
  u := *r.URL
  u.RawQuery = ""
  u.ForceQuery = false
  return r.Method + " " + u.String()}))

type _serverHandlerKey int8

const serverHandlerKey _serverHandlerKey = 0

func withServerHandler(ctx context.Context, s *serverHandler) context.Context {return context.WithValue(ctx, serverHandlerKey, s)
}

func getServerHandler(ctx context.Context) *serverHandler {return ctx.Value(serverHandlerKey).(*serverHandler)
}

//go:noinline
func wrappedHTTPHandler(s *serverHandler, rw http.ResponseWriter, req *http.Request) {req = req.WithContext(withServerHandler(req.Context(), s))
  tracedServerHandler.ServeHTTP(rw, req)
}

func init() {hook.Hook(serveHTTP, wrappedHTTPHandler, originalServeHTTP)
}

在解决了 Golang 的主动埋点后,咱们还遇到的一个辣手问题是在异步的场景中,因为上下文的切换导致 TraceContext 无奈传递到下一个 Goroutine 中。同样在参考了 Java 的 Future 和 C# 的 Task 两种异步编程模型后,咱们也实现了主动传递 Trace 上下文的异步 API:

future1 := parallel.Go(ctx, func(ctx context.Context) (interface{}, error) {req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://www.baidu.com/api_1", nil)
    if err != nil {return nil, err}
    resp, err := http.DefaultClient.Do(req)
    if err != nil {return nil, err}
    defer resp.Body.Close()
    byts, err := ioutil.ReadAll(resp.Body)
    if err != nil {return nil, err}
    return string(byts), nil
  })

  future2 := parallel.Go(ctx, func(ctx context.Context) (interface{}, error) {req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://www.baidu.com/api_2", nil)
    if err != nil {return nil, err}
    resp, err := http.DefaultClient.Do(req)
    if err != nil {return nil, err}
    defer resp.Body.Close()
    byts, err := ioutil.ReadAll(resp.Body)
    if err != nil {return nil, err}
    return string(byts), nil
  }, parallel.WithTimeout(10*time.Second))

  body1, err := future1.Get()
  if err != nil {return nil, err}

  body2, err := future2.Get()
  if err != nil {return nil, err}

  return &pb.HelloResponse{
    Success: true,
    Data:    body1.(string) + body2.(string),
  }, nil

写在最初

在应用 OpenTelemetry 把 Erda 平台调用产生的 Trace 数据接入到 Erda 本身的 APM 中后,咱们首先能失去的收益是能够直观的失去 Erda 的运行时拓扑:


Erda 运行时拓扑

通过该拓扑,咱们可能看到 Erda 本身在架构设计上存在的诸多问题,比方服务的循环依赖、和存在离群服务等。依据本身的观测数据,咱们也能够在每个版本迭代中逐渐去优化 Erda 的调用架构。

对于咱们隔壁的 SRE 团队,也能够依据 Erda APM 主动剖析的调用异样产生的告警音讯,可能第一工夫晓得平台的异样状态:

最初,对于咱们的开发团队,基于观测数据,可能很容易地洞察到平台的慢调用,以及依据 Trace 剖析故障和性能瓶颈:

小 L:“除了下面这些,咱们还能够把平台的日志、页面访问速度等都应用相似的思路接入到 Erda 的可观测性平台。”

小涛豁然开朗道:“我晓得了,原来套娃观测还能够这么玩!当前就能够释怀地喝着咖啡做本人的工作了😄。”


咱们致力于决社区用户在理论生产环境中反馈的问题和需要,
如果您有任何疑难或倡议,
欢送关注【尔达 Erda】公众号给咱们留言,
退出 Erda 用户群参加交换或在 Github 上与咱们探讨!

退出移动版