终极套娃 2.0|云原生 PaaS 平台的可观测性实践分享

本文涉及的产品
可观测可视化 Grafana 版,10个用户账号 1个月
云拨测,每月3000次拨测额度
简介: 如何实现自身的可观测性?实践经验分享一起来看!

某个周一上午,小涛像往常一样泡上一杯热咖啡 ☕️,准备打开项目协同开始新一天的工作,突然隔壁的小文喊道:“快看,用户支持群里炸锅了 …”

用户 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 的可观测性数据模型。

image.png
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 的运行时拓扑:

image.png
Erda 运行时拓扑

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

对于我们隔壁的 SRE 团队,也可以根据 Erda APM 自动分析的调用异常产生的告警消息,能够第一时间知道平台的异常状态:

image.png

最后,对于我们的开发团队,基于观测数据,能够很容易地洞察到平台的慢调用,以及根据 Trace 分析故障和性能瓶颈:

image.png

image.png

小 L:“除了上面这些,我们还可以把平台的日志、页面访问速度等都使用类似的思路接入到 Erda 的可观测性平台。”

小涛恍然大悟道:“我知道了,原来套娃观测还可以这么玩!以后就可以放心地喝着咖啡做自己的工作了😄。”


我们致力于决社区用户在实际生产环境中反馈的问题和需求,
如果您有任何疑问或建议,
欢迎关注【尔达Erda】公众号给我们留言,
加入 Erda 用户群参与交流或在 Github 上与我们讨论!

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
1月前
|
缓存 Java API
【云原生】Spring Cloud Gateway的底层原理与实践方法探究
【云原生】Spring Cloud Gateway的底层原理与实践方法探究
|
3月前
|
存储 SQL Cloud Native
深入了解云原生数据库CockroachDB的概念与实践
作为一种全球领先的分布式SQL数据库,CockroachDB以其高可用性、强一致性和灵活性等特点备受关注。本文将深入探讨CockroachDB的概念、设计思想以及实践应用,并结合实例演示其在云原生环境下的优越表现。
|
3月前
|
监控 Cloud Native 安全
浅谈云原生可观测性
【1月更文挑战第23天】
|
3月前
|
Cloud Native 关系型数据库 大数据
CockroachDB:云原生数据库的新概念与实践
本文将介绍CockroachDB,一种先进的云原生数据库,它具备分布式、强一致性和高可用性等特点。我们将探讨CockroachDB的基本原理、架构设计以及在实际应用中的种种优势和挑战。
|
2月前
|
监控 Cloud Native 网络协议
|
1月前
|
Cloud Native 安全 持续交付
构建未来:云原生架构的演进与实践
【2月更文挑战第30天】 随着数字化转型的深入,企业对于信息技术的需求日益复杂化和动态化。传统的IT架构已难以满足快速迭代、灵活扩展及成本效率的双重要求。云原生技术作为解决这一矛盾的关键途径,通过容器化、微服务、持续集成/持续部署(CI/CD)等手段,实现了应用的快速开发、部署及运维。本文将探讨云原生架构的最新发展,分析其如何助力企业构建更加灵活、高效的业务系统,并结合实际案例,展示云原生转型过程中的最佳实践和面临的挑战。
|
12天前
|
Kubernetes 监控 Cloud Native
构建高效云原生应用:基于Kubernetes的微服务治理实践
【4月更文挑战第13天】 在当今数字化转型的浪潮中,企业纷纷将目光投向了云原生技术以支持其业务敏捷性和可扩展性。本文深入探讨了利用Kubernetes作为容器编排平台,实现微服务架构的有效治理,旨在为开发者和运维团队提供一套优化策略,以确保云原生应用的高性能和稳定性。通过分析微服务设计原则、Kubernetes的核心组件以及实际案例,本文揭示了在多变的业务需求下,如何确保系统的高可用性、弹性和安全性。
16 4
|
2月前
|
IDE Cloud Native 开发工具
云原生之在Docker环境下部署Atheos云IDE平台
【2月更文挑战第3天】云原生之在Docker环境下部署Atheos云IDE平台
365 2
|
2月前
|
Prometheus 监控 Kubernetes
青团社:亿级灵活用工平台的云原生架构实践
青团社:亿级灵活用工平台的云原生架构实践
262348 5
|
2月前
|
人工智能 运维 监控
「云原生可观测团队」获选「InfoQ 年度技术内容贡献奖」
「云原生可观测团队」获选「InfoQ 年度技术内容贡献奖」