云原生观测性 --OpenTelemetry 之实战篇

本篇以开发实战为主。想从零了解云原生可观测性相关概念及方法论、OpenTelemetry 架构和基础模型、以及内部部分细节的读者,推荐阅读《云原生观测性-OpenTelemetry》,本文涉及到相关内容时将不会过多描述。
从前文可知:
Logs(日志)是以应用程序运行事件为基础的记录。无论是结构化的、非结构化的,抑或是简单直白的文字,都可以直接告诉用户进程内部在某个过程发生了什么事。运维人员当然希望记录这种重要的信息来帮助自己理解应用甚至底层系统内部有什么表现。但并非所有日志都是天然可读的,运维者往往借助一些日志分析工具来对内容和结构做些过滤和整理。
Metrics(指标)是可聚合的数据。这些数据可以来自于基础系统、主机、第三方源等等。如果这些数据源位置的日志并不能被用户拿到,那么指标便可以以他的时序性、所记录的值、甚至事件名称来帮助用户观察和预防一些问题的发生。
Traces(链路)是从头至尾跟踪某一过程的一种行为。这个过程可以是一次 API 调用,也可以是系统内的一个动作。他可以帮助运维人员理解应用内模块与模块、服务与服务之间如何连接,也可以借助 Span 的数据能力为观测、预防、排障提供便利。
01 OpenTelemetry 的数据如何流转?
OpenTelemetry(OTel)以特定协议收集(collect)以上类型的观测数据并转发(export)到目标位置。那这些数据究竟怎样被生产出来呢?结合下面简化版的示意图从头开始看看大致都需要哪些过程。

观测数据生命周期
首先,借助 OTel APIs 组件的能力,将 OTel 的 API 嵌入进所开发的应用,使应用程序有收集观测数据的能力,并且还可以设置应该收集哪些数据。鉴于 OTel APIs 组件的语言相关性,应用程序的语言不同,则具体的操作步骤可能会有所不同,并且也会影响这个动作对应用本身的侵入性大小。(左上图)
第二步,和 OTel APIs 紧密相关的 OTel SDKs 组件为用户提供暂存和传输数据的能力。在这一过程中开发人员可以对数据进行打散、重组、取样、过滤等等操作,来“清洗”数据,还可以将不同数据源但相同上下文的数据结合起来使数据更丰满。最终借助 OTLP 将数据转化成标准格式,发往外部。(右上图)
第三步,从上一步 Exporter 中产出的数据,可以流入到 Collector 组件中进行进一步的过滤、聚合等处理,Collector 中一般也会具有批处理能力,数据被集中处理过后再次借助 Exporter 的能力,将数据发向预先准备好的后端服务。
接下来再换一个角度,进一步分析下上面步骤中涉及的各组件能力以及在应用架构中的布局。

OTel 各组件布局
API – 为应用提供一个接入 OTel 的基础接口。
SDK – API 和 Exporter 的中间桥梁,可以通过增加额外的配置来完成数据的过滤、事件的取样等工作。
In-process Exporter – 这个 Exporter 位于 OTel Library 中,他使应用和数据后端分离的方式,进而使用户具备选择和切换后端的能力,而不至于每当想要切换后端时,就需要重新嵌入新代码。
Collector – 处在整个 OTel Library 的进程之外,Collector 负责接收和进一步过滤、聚合、批处理数据的工作。从技术角度看 Collector 在整个 pipeline 中并不是必须的,但他实际是 OTel 架构中极有帮助的组件,因为他使整个观测架构更灵活,为数据的处理提供了功能和性能上的帮助。
Collector 有两种部署模型:
一个 Agent 部署在应用一侧(same host),例如二进制、DaemonSet、sidecar 等。
一个 Service 独立进程与应用分离,可以根据负载扩容多副本,此模型应该避免大规模应用同时连接。
Exporter – 这个 Exporter 大部分情况下是属于 Collector 的一部分,将能力集成在 Collector 中来完成把观测数据发送至外接后端的工作。
随着上面数据生命周期 pipeline 的顺序来看,API、SDK 以及处理数据的 Processor 和 Exporter 都在应用侧,他们共同组成了 OTel Library。右侧的 Collector + Exporter 独立于应用也并非必须,但提供着更完整的能力。
1.1. OpenTelemetry 的可扩展性
OTel 旨在让观测能力可以深入应用层面,而不是把用户锁定在某一种规范上。所以 OTel 自身也保持了一定的可扩展性,最大限度的支持开发人员在想要赋予应用可观测性时,只要做些小的适配微调即可达到目的,而不是大量的重写重构系统。下面随着下图从右到左,看下 OTel 中可扩展的一些点。

Collector 借助 Exporter 能力可以对接多种多样的 Backend。他独立在 OTel 的进程之外,最大程度做到了和应用无关。开发者可以在任何时候去配置这个后端(无论是应用部署前还是部署后),甚至是在运行的时候做切换操作,也无需去做应用的重新部署。所以这一点是至关重要的可扩展性的体现。
在这一步 Collector 的配置控制着聚合、批处理以及其他过程的逻辑。功能的”可配“也是灵活可扩展的重要体现。
In-process Exporter 也就是 OTel Library SDK 中的 Exporter ,默认情况下是将观测数据发向外部 Collector 的。但一些情况下的部署策略不允许有一个独立的 Collector 程序,所以需要开发人员将 Exporter 的配置改写或替换(很方便)来将观测数据直接发向后端。
SDK 自身允许很多功能扩展:取样、过滤、富集(enrichments)。不过配置这些逻辑在 SDK 中的难度往往大于在 Collector 中去实现,但有时候为了性能、数据安全等原因,开发人员也需要这么做。
最后,最激进的一种可扩展性的体现就是用户甚至可以用其他可实践的方案来完全替代 OTel SDK,此时 OTel 对应用的帮助也就是协议规范上的价值。
02 OpenTelemetry 实战-为应用增加可观测性
Talk is cheap。现在来一步步为 demo 程序接入 OTel 。本文举例 Python 和 Java 两种语言的 demo,并以 Python 为主(因为笔者更擅长 Python)。
如果想跟随文章一起操作,您可能需要到 Docker。
2.1. Python
2.1.1. 准备工作(1/5)
第一步是环境的设置和包的安装。笔者用 mininconda 安装了 Python 3.10.4 之后,用 pip 安装 Otel 所需的包。
opentelemetry-distro 将 API、SDK、opentelemetry-bootstrap、opentelemetry-instrument 这些包集合在一起,稍后都会用到。

2.1.2. 创建 HTTP Server(2/5)
vi app.py

这里将先用 Automatic instrumentation 的方式为这个小 flask 应用接入 OTel。
2.1.3. Automatic instrumentation(3/5)
这种方式取代繁琐操作并自动生成一些基础观测数据,这里用最简单的方式。
这条命令会探测已经安装的库并自动地为开发人员安装 “接入” 所需的相关包。

到这一步应用其实完成了 OTel 的接入,来启动下服务看看效果。
上面这一命令用 instrument agent 的命令代替应用的原启动命令 flask run,并指定了观测数据会被吐到 console 中。

在 opentelemetry-instrument 的帮助中可以看到很多选项,这些选项都一一对应了 OTel 中可以配置的环境变量,并且命令行中输入的变量优先级会高于默认配置的变量。
启动命令执行后,向这个 HTTP Server 发送一个请求,来看看最终效果。


上面分别是一次请求的 traces 和 metrics 信息,这些信息便是 automatic instrumentation 为用户”内置“的观测项。比如跟踪 /greeting 路由请求的生命周期数据,以及 http.server.active_requests、 http.server.duration 等指标数据。但实际生产中为应用构建可观测性这些数据还远远不够。
2.1.4. 增加观测项(Manual)(4/5)
自动的集成为用户提供了一些基础的观测数据,但这些数据多来自系统 ”边缘“,比如带内或带外的 HTTP 请求,并不能给用户展示应用内部的情况,下面在 Automatic instrumentation 的基础上再手动增加一些观测能力。
Traces
编辑 app.py。

初始化出一个 tracer 并创建出一条 trace 数据(也就是 span,他属于自动集成所创建的 span 数据的子 span)。
再一次运行服务并请求。

可以看到手动创建的 greeting span 的 parent_id 和 /greeting 的 span_id 是一样,表示着父子关系。
Metrics
编辑 app.py。

初始化出一个 meter 并创建一个 Counter,来对每一个 user 参数来计数(统计调用次数)。
再次运行服务,这次发送多个不同的请求。

这次发送了两次默认调用,以及一次带 user 参数的调用,这个行为被之前设置的 Counter 统计到并生成了 metrics 数据。
以上步骤就是引入 OTel 以及手动增加 Traces 和 Metrics 数据的初步过程。至于 Logs 模块,因为刚刚选择了 export to console 的启动方式,而将 logs 输出到 console 就和正常打印一条日志的效果相同,所以本节先不演示,接下来结合 OTLP 的内容一起看下 OTel 中 Logs 模块的表现。
2.1.5. 向 Collector 发送 OTLP 协议数据(5/5)
Collector 的关键作用在前面已经介绍过一些,下面列举一些 Collector 可以带来的好处:
Collecter 可以使多种服务共用一个观测数据池,从而减少切换 Exporter 的开销。
Collector 可以将跨主机的、跨服务的同一条 trace 数据聚合到一起。
Collector 提供一个将数据吐到后端前的中转站,提供提前分析过滤数据的能力。
下面来演示对接一个简单的 Collector。
首先,在 app.py 文件的路径下,再创建一个 tmp/ 文件夹,并创建一个 otel-collector-config.yaml 文件。

接下来拉取 OTel 提供的基础 Collector 镜像,启动一个 Collector。
命令不难理解,将配置文件挂载后,监听 Collector 默认的 4317 端口。

然后需要将之前自动及手动抓取的观测数据,通过 OTLP 协议转换为标准格式,发送到 Collector。
这一步需要 OTLP 协议的 Exporter,而 OTel 社区也为用户准备好了安装包。
现在再启动一次应用。
这里已经不再需要指定 --traces_exporter console 等参数,因为这里也借助了 Automatic instrumentation 的能力,instrument agent 会自动的监测到刚刚安装的包,并在下一次启动时,为用户切换到 OTLP/gRPC 协议的 Exporter,默认目标端口也是 4317。
再次请求服务,Collector 进程会接收到 OTLP 标准数据,而原 Flask 进程不会再展示 telemetry 数据。
Traces

Metrics

Logs
Logs 的 SDK 以及相关组件还是开发状态,所以目前接入 app 以及对接 Collector 的过程并不像另外两类观测数据那样便捷。
编辑一下 otel-collector-config.yaml 文件。

再修改一下 app.py。

在上面的步骤中,依次创建了 log_emitter_provider、OTLP 协议的 exporter(这一步对 Collector 接受到应用程序输出的日志很重要)、log_emitter,并最终将用 log_emitter 创建出来的 LoggingHandler 实例加入到了 logging 的 handlers 中(注意在 Flask 应用中,需要将 Stream 类型的 handler 也加入到 logging handler,否则会影响 werkzeug 的信息输出)。
重新运行 Collector 容器和应用程序:
请求 /greeting 接口来看看 Collector 中的结果。

可以注意到 Collector 只收到了 INFO 及以上等级的日志消息,这是因为之前在 otel-collector-config.yaml 文件中设置了 loglevel=INFO。

以 Flask 和 werkzeug 为例,其他库、框架或系统日志也会被发送到 Collector。

在 Span 上下文范围内记录的日志,经过 OTLP 协议的处理会带上 Trace ID 和 Span ID,使所有观测数据互相联结起来。
Python 的 demo 演示至此结束,下面来看下 Java 的演示。
2.2. Java
2.2.1. 准备工作(1/5)
Java 的 demo 代码文章用 gRPC 官方给的例子,不过即使不熟悉 gRPC 也不影响下面的操作。笔者准备了 openjdk 17.0.4 环境,而 gRPC 官网提示需要 JDK 版本在 7 以上。
首先拷贝代码到本地,并进入 examples 文件夹。
测试一下 Hello World。


运行正常则可以先停止服务端
2.2.2. Automatic instrumentation(2/5)
下载 OTel javaagent JAR 包到你想要的目录(记住这个路径)。
配置 OTel javaagent 的方式有许多种,这里例举环境变量的方式,将观测数据输出到 console。
——logging exporter 会将数据输出到 stdout,而 Java 作为集成度较高的语言之一,exporter 已经天然支持 Prometheus、zipkin、jaeger 等类型。
启动服务端并置于后台,观察 console 的输出。

可以看到服务启动后,已经有观测数据被吐到 console 中。OTel javaagent 已经为用户启动了 14 个 metrics,比如图中的 “jvm 缓存区计数统计”。
接下来在同一个终端中,启动客户端。

两条内置的 traces 数据也被打印出来。
结束进程。
至此,完成了对 Java gRPC 服务的 instrumentation。可以注意到整个过程是无侵入的,这极大方便开发者为 Java 应用接入 OTel。
2.2.3. Export to Jaeger(3/5)
上面文章提到 Java 已经支持多种 exporter,这里以 Jaeger 做个示例。
首先部署一套 Jaeger 环境。
在之前的步骤,文章通过 export OTEL_TRACES_EXPORTER=logging 将链路等观测数据定位到标准输出。在这一步,则需要将 logging 改为 jaeger 并加几个新配置。
再次按步骤启动服务端和客户端,并观察 Jaeger UI: http://localhost:16686/。

链路数据已经被发送到 Jaeger 中,Java exporter 在对接主流后端应用时就是这么轻松。
2.2.4. Manually instrumentation(4/5)
接下来简单介绍一些更高级的使用场景,即用手动插桩的方式为 gRPC 应用增加可观测性。
仍以这个 gRPC 程序为例,先看 client 端。

这是将会使用到的 OTel API。

OTel SDK 应该在最开始就被初始化,然后接下来获取一个 Tracer。

得到一个 Tracer 对象后在客户端像这样创建一个 Root Span。在创建 Span 时指定了名字及类型,当用户想终止一个 Span 时需要调用 span.end()。

通过这两步将 gRPC 请求中分散的上下文信息关联起来。
再来看看服务端。服务端同样要先完成引入 OTel API 以及初始化 OTel SDK 的步骤。

解析在 gRPC metadata 中分散的上下文信息。

这里解析好上下文得到 client 端创建的 Root Span 后,使用 setParent() 方法创建了一个 Child Span。
以上便是手动插桩的大致方法,运行下试试效果吧!文章篇幅有限,Span 的设置还有很多种无法介绍,Java 手动配置指标项、日志的内容也就先省略了,感兴趣的话为大家推荐官方文档和社区仓库,里面有更详尽的解释。
2.2.5. 关于自定义的应用(5/5)
上面以 gRPC 官方示例为例也有一些局限性,当用户想为自己的应用接入 OTel 时,可以参考以下方式。
加一些配置。
总结来看,语言的不同也决定了接入 OTel 方式的不同。Java 发挥语言应用广泛的特性,社区提供着大量的现成 SDK 可以使用,比如 Dubbo、Log4j、mongoDB 等等;而 Python OTel 则继承了 Python 包管理方便的优势,将 SDK、OTLP 协议的 Exporter、API 等包进一步封装进了 Distro 包进行集成,甚至还在 opentelemetry-python 基础上提供了 opentelemetry-python-contrib 来方便开发人员应对更多场景。
03 OpenTelemetry 实战-场景方案分析
这一章节文章讨论一些可能遇到的实际场景,并结合场景分析一些可行方案。
3.1. API 的监控告警
这里 API 的概念涵盖了微服务架构中服务与服务间的内部 API(REST & gRPC)和提供给应用外部的 OpenAPI。API 的可用性是至关重要的,但 API 的使用方对其可用性是不可知的,往往是调用失败了,才知道系统此时有异常,如果发生在高并发的生产环境上,那产生的后果可能会很严重。所以对开发以及运维一个系统来说, API 服务端可用性的监控和及时了解系统发生了什么错误是很重要的。
针对以上场景,假设有以下需求:
各类 API 的使用情况总览监控
接口的流量情况、响应时间
连接数过多导致负载过重而产生的超时错误
所有错误中由系统内部异常导致的错误
影响接口状态的关键资源情况,如内存、磁盘 I/O 等
对异常情况的精准定位和告警
普遍实现大致是这样的:Jaeger 负责链路追踪,Prometheus&AlertManager 负责指标的采集和告警,EFK 组件负责采集日志,有时甚至还要加上文本分析工具来提取关键事件。这种方案有这么几个问题:
这么多的组件所带来的维护负担,包括日志采集及分析的难度。
减弱了应用本身的灵活性,当应用对接了以上组件的 SDK 后,意味着应用架构不能再轻易地替换这些组件工具,否则就意味着新的学习成本和代码重写。
最重要的是这些工具产生出的结果,并不能结合起来马上为相关人员得到结论,仍然需要人工对这些数据进行分析来锁定问题域。
使用了 OTel 之后,这个问题就可以通过 Opentelemetry + Prometheus + Alertmanager + Grafana 的方式实现对 API 的监控告警。他意味着应用只需要对接 OTel SDK,而不关心数据后端怎样实现,甚至一旦数据后端支持了 OTLP 协议,那工具的替换对应用和 OTel 都是零成本的。

开发人员可以用 Manual 的方式将 OTel 接入应用,自定义所需要的 Metrics:
有了这两个指标后,应用便足以计算出接口不同错误类型(4xx、5xx)的错误率、接口的响应时间及分布、接口流量等具体值。后续可以选择在 Prometheus 中添加规则来进行相应告警,也可以直接在 Collector 中进行过滤聚合后直接将结果发到 AlertManager。对于错误的精准定位,还可以借助 Traces 中的信息,包括开发人员自定义的信息,甚至在多数据源的场景下也可以获取到完整上下文,定位到链路的哪一环节出现了问题。再配合 Logs 中的关键信息,明确地指出系统在哪里出现了什么问题。如果需要更进一步的日志审计和存储,那可以再加入 ES 等工具。
3.1.1. 案例




3.2. AIOps
AIOps,即 Artificial Intelligence for IT Operations –– 智能运维。其实他最初的定义是 Algorithm IT Operations,利用算法来实现运维自动化。随着技术的成熟一步步演化到如今的定义,将人工智能应用于运维领域,运用数据科学和机器学习的手段来进一步解决自动化运维无法解决的问题。AIOps 本身是个大话题,本篇不过多展开。对于 AIOps 可以先简单地了解以下几点:
AIOps 和 DevOps、ChatOps、SRE 等领域相关概念不冲突,而且是更高阶的实现。
以算法为核心,以自动化运维为基石,目的是做到智能告警和全局视图的根因分析,终极目标是做到故障预测和决策执行。
AIOps 依赖大数据级的高质量数据源,通过学习处理逐渐达到识别系统重大事件,甄别冗余告警等能力。
那 AIOps 和可观测性有什么关系呢?先看一些资料。

上图是 GigaOm 2021 输出的 AIOps 供应商 Radar 报告。大家看到这个领域的主要玩家包括:Splunk、New Relic、Dynatrace、Datadog、Elastic 等等。而这几家公司,如果熟悉 OTel 的读者自然会很眼熟,因为他们也是 OpenTelemetry Community 的头部贡献者。

从这一点可以看出 AIOps 一定和可观测性渊源不浅。事实上简单地说,Observability 是支撑 AIOps 得以实现的关键,而 AIOps 是 Observability 可以落地的体现。上面提到的 AIOps 依赖的大数据级高质量数据,指的也就是观测数据,他二者的关系也是另一个大话题,文章也不展开了。那 OpenTelemetry 作为 CNCF 主推的可观测性标准方案,自然可以和 AIOps 平台和系统结合到一起,把 OTel 统一标准、灵活、扩展性强的优势带进这一领域。
3.3. OpenTelemetry in Kubernetes
文章前面一直在讲应用和 OTel 的结合方式和场景,很少提到整个大背景,那就是在云原生环境下。OTel 在 Kubernetes 中的场景,我认为可以从两个角度来看:1. 对集群中已经容器化的应用添加可观测性;2. 对 K8s 集群自身增加可观测性。
3.3.1. 对集群中云原生应用添加可观测性
这个问题的痛点在于,云原生应用都被打包成了镜像,想要对应用进行 OTel 的插桩操作那就需要重新构建这个镜像。而大量已经上云的应用都需要重新构建,实在不能接受。这个问题的解法之一就是 OpenTelemetry–Operator。
OpenTelemetry–Operator 在 v0.38.0 版本(编写文章时已经发布 v0.60.0)引出了 Instrumentation 自定义资源,他里面描述了 OTel SDK 的配置和策略。配合 Instrumentation CRD 的能力,应用工作负载只需要添加几条注解,就可以使其成功对接 OTel。
Instrumentation:
deployment annotation:
配置完成后数据会被输出到 endpoint。另外补充一点 OpenTelemetry–Operator 其实是 Collector 的 Operator,主要实现了 Collector 组件的能力。
3.3.2. 对集群自身增加可观测性
开发人员通常会选择 Prometheus 的 exporters 和相应的 receiver 来抓取一些指标数据来监测集群状态。那 OpenTelemetry–Operator 能不能做到这些呢,答案应该是肯定的。社区中已经有了相关实现,感兴趣可以继续阅读:https://github.com/isItObservable/Otel-Collector-Observek8s
04 OpenTelemetry 社区生态
其实再强调 OTel 在 CNCF 的重要程度和可观测性近年来的种种发展,已经是老生常谈。这里简单放一些社区的现状。

这张图里可以注意到近期排名前几的贡献者,大部分公司在日志能力方面很强势,说明现在社区主力在发展日志相关能力。

Repo 数量已达到 58 个,语言 SDK 基本涉及所有主流语言,其中 Java、.NET 语言 SDK 发展最快最全,其次是 Python,Traces 和 Metrics API 已经 Stable 状态,Logs 在 Experimental,稳定版呼之欲出,Go 语言稍慢一些,Metrics 还在 Alpha 阶段。
最后借用观测领域大佬同事的一句话:“可观测性,无限可能。”
PS:来云原生底座上体验一下可观测性吧!
免费下载 DCE 5.0 社区版:
版权声明: 本文为 InfoQ 作者【Daocloud 道客】的原创文章。
原文链接:【http://xie.infoq.cn/article/2fd502f645ffc8dcd7b0e1b2c】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论