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

本文涉及的产品
应用实时监控服务-应用监控,每月50GB免费额度
应用实时监控服务-可观测链路OpenTelemetry版,每月50GB免费额度
可观测监控 Prometheus 版,每月50GB免费额度
简介: 如何实现自身的可观测性?实践经验分享一起来看!

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

用户 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日志并进行多维度分析。
目录
相关文章
|
21天前
|
Cloud Native 持续交付 开发者
云原生技术在现代企业中的应用与实践####
本文深入探讨了云原生技术的核心概念及其在现代企业IT架构转型中的关键作用,通过具体案例分析展示了云原生如何促进企业的敏捷开发、高效运维及成本优化。不同于传统摘要仅概述内容,本部分旨在激发读者对云原生领域的兴趣,强调其在加速数字化转型过程中的不可或缺性,为后续详细论述奠定基础。 ####
|
11天前
|
Cloud Native 安全 Java
铭师堂的云原生升级实践
铭师堂完整经历了云计算应用的四个关键阶段:从”启动上云”到”全量上云”,再到”全栈用云”,最终达到”精益用云”。通过 MSE 云原生网关的落地,为我们的组织带来了诸多收益,SLA 提升至100%,财务成本降低67%,算力成本降低75%,每次请求 RT 减少5ms。
铭师堂的云原生升级实践
|
10天前
|
Cloud Native 安全 Java
杭州铭师堂的云原生升级实践
在短短 2-3 年间,杭州铭师堂完整经历了云计算应用的四个关键阶段:从“启动上云”到“全量上云”,再到“全栈用云”,最终达到“精益用云”。也从云计算的第一次浪潮,迈过了第二次浪潮,顺利的进入到了 第三次浪潮 AI + 云。
|
10天前
|
Cloud Native
邀您参加云原生高可用技术沙龙丨云上高可用体系构建:从理论到实践
云原生高可用技术专场,邀您从理论到实践一起交流,探索云上高可用体系构建!
|
21天前
|
Kubernetes Cloud Native API
云原生入门:从理论到实践的探索之旅
本文旨在为初学者提供一个关于云原生技术的全面介绍,包括其定义、核心原则、关键技术组件以及如何将这些概念应用于实际项目中。我们将通过一个简易的代码示例,展示如何在云原生环境下部署一个简单的应用,从而帮助读者更好地理解云原生技术的实践意义和应用价值。
|
23天前
|
运维 Cloud Native 开发者
云原生技术入门与实践
在云计算的浪潮中,云原生技术以其独特的优势和魅力吸引了越来越多的开发者和企业。本文将从云原生技术的基本概念、核心组件以及实际应用三个方面进行详细介绍,帮助读者更好地理解和掌握这一新兴技术。同时,文章还将分享一些实际案例和经验教训,让读者能够更深入地了解云原生技术的应用场景和发展趋势。
37 5
|
1月前
|
Kubernetes Cloud Native 微服务
云原生入门与实践:Kubernetes的简易部署
云原生技术正改变着现代应用的开发和部署方式。本文将引导你了解云原生的基础概念,并重点介绍如何使用Kubernetes进行容器编排。我们将通过一个简易的示例来展示如何快速启动一个Kubernetes集群,并在其上运行一个简单的应用。无论你是云原生新手还是希望扩展现有知识,本文都将为你提供实用的信息和启发性的见解。
|
1月前
|
Cloud Native 安全 Docker
云原生技术在现代应用部署中的实践与思考
本文深入探讨了云原生技术如何在现代应用部署中发挥关键作用,并提供了具体的代码示例来展示其实现。通过分析云原生的核心概念和优势,我们将了解如何利用这些技术来提高应用的可扩展性、可靠性和安全性。文章还将讨论云原生技术的未来发展趋势,以及如何将其应用于实际项目中,以实现更高效和灵活的应用部署。
|
21天前
|
Cloud Native API 持续交付
云原生架构下的微服务治理策略与实践####
本文旨在探讨云原生环境下微服务架构的治理策略,通过分析当前面临的挑战,提出一系列实用的解决方案。我们将深入讨论如何利用容器化、服务网格(Service Mesh)等先进技术手段,提升微服务系统的可管理性、可扩展性和容错能力。此外,还将分享一些来自一线项目的经验教训,帮助读者更好地理解和应用这些理论到实际工作中去。 ####
36 0
|
28天前
|
Cloud Native 持续交付 云计算
云计算的转型之路:探索云原生架构的崛起与实践####
随着企业数字化转型加速,云原生架构以其高效性、灵活性和可扩展性成为现代IT基础设施的核心。本文深入探讨了云原生技术的关键要素,包括容器化、微服务、持续集成/持续部署(CI/CD)及无服务器架构等,并通过案例分析展示了这些技术如何助力企业实现敏捷开发、快速迭代和资源优化。通过剖析典型企业的转型经历,揭示云原生架构在应对市场变化、提升业务竞争力方面的巨大潜力。 ####
34 0