背景
做的项目服务上线后,因其为 Python 技术栈,Arms 的实时监控对其支持一直不是很完善,跟不上 Java 那样成熟的监控体系(截止 20241017),但近期因开始考虑一些业务告警的实施,需要对 Arms Python 监控的支持进行研究。
产品介绍
在 Arms 产品体系内,对 Python 的支持为链路追踪,由可观测链路 OpenTelemetry 版产品提供。
什么是 OpenTelemetry
OpenTelemetry 是一个开源的可观测性框架和工具包, 专注于监控数据的采集和上报,而关于存储和呈现则由遵守 OpenTracing 标准的服务来承接即可, 因此众多云厂商的监控均会支持此套标准。
当然市场上也有类似的开源产品, 例如 Skywalking, Jaeger 等。
目前大部分都是选择云厂商部署,所以选择上都优先使用厂商推荐的方案。
如果是内部运维自建平台,倾向使用整套解决方案,例如 Skywalking 等, 运维&使用成本更低。
| 特性 | OpenTelemetry | Apache SkyWalking | Jaeger |
|---|---|---|---|
| OpenTracing 兼容性 | 完全兼容,且是新标准 | 支持 OpenTracing 及 OpenTelemetry | OpenTracing 参考实现,完全兼容 |
| 客户端支持语言 | 支持多种语言(Java、Go、Python 等) | 支持 Java、Go、Python、Node.js 等 | 支持 Java、Go、Python、Node.js 等 |
| 存储 | 与后端工具集成(如 Jaeger、ElasticSearch) | 支持 ElasticSearch、MySQL、H2 | 支持 ElasticSearch、Cassandra、Kafka 等 |
| 传输协议支持 | HTTP、gRPC、OTLP | HTTP、gRPC、Kafka | HTTP、gRPC、Thrift |
| UI 丰富程度 | 依赖集成 Jaeger 或 Grafana 等 UI 工具 | 内置丰富的 UI,包括拓扑图、调用链分析 | 提供基本的 UI,功能相对较简单 |
| 实现方式 - 代码侵入性 | 侵入性低,支持自动集成 | 低侵入性,采用无侵入式代理 | 中等侵入性,需要手动在代码中加入追踪点 |
| 扩展性 | 高扩展性,支持自定义采集器、处理器等 | 高扩展性,支持插件机制及多种中间件 | 较好扩展性,支持自定义采样策略、存储等 |
| Trace 查询 | 依赖 Jaeger 或 Zipkin 查询 | 支持强大的查询功能,支持多维度条件过滤 | 提供强大的查询功能,支持多种查询维度 |
| 告警支持 | 需与外部工具集成(如 Prometheus) | 内置告警机制,支持与外部系统集成 | 不提供,需与 Prometheus 等工具集成 |
| 性能损失 | 低到中,取决于采样率及配置 | 低到中,随着监控范围增加,性能开销上升 | 低到中,主要取决于采样率和存储开销 |
基本概念
Trace : 一个调用链代表一个事务或者流程在(分布式)系统中的执行过程, 在OpenTracing标准中,trace是多个span组成的一个有向无环图(DAG),每一个span代表trace中被命名并计时的连续性的执行片段。
Span : 一个span代表系统中具有开始时间和执行时长的逻辑运行单元。span之间通过嵌套或者顺序排列建立逻辑因果关系。
Metric:统计数据,例如应用、接口、数据库的请求数据、响应时间、异常数等。
使用集成
阿里云接入
接入支持 OpenTelemetry Skywalking Jaeger, 但此部分选择了阿里云推荐的 通过OpenTelemetry上报Python应用数据 自动埋点接入方案。
应用改造
应用自身需要安装 OpenTelemetry 相关依赖,以便支持监控采集应用数据
OpenTelemetry 支持采集的框架可以参考这里 github.com/open-teleme…
# 安装依赖
pip install opentelemetry-distro opentelemetry-exporter-otlp -i https://mirrors.aliyun.com/pypi/
# 解析项目源码, 并安装项目相关埋点扩展库支持
RUN PIP_INDEX_URL=https://mirrors.aliyun.com/pypi/ opentelemetry-bootstrap -a install
服务接入
应用启动时, 需要注入 OTEL_EXPORTER_XXX 等环境变量,并以 opentelemetry-instrument启动。
这里主要配置的是数据上传的协议以及端点,默认的协议为 grpc, 但本文档采用的是 http 协议传输
# 项目名
export OTEL_SERVICE_NAME=<your_app_name>
# 上报协议
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
# traces 上传端点
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=<your_http_trcaes_endpoint>
# metrics 上传端点
export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=<your_http_metrics_endpoint>
# 启动命令,启用自动埋点
opentelemetry-instrument <your_run_command>
数据流转
启用自动埋点后,OpenTelemetry 会自动采集所有内部链路数据,并通过设置的上报端点进行上报。
数据存储由 sls 日志提供服务,看板则由 Arms 可观测链路支持
能力覆盖
基础能力
基于 OpenTelemetry 提供分布式链路追踪能力, 能实现应用运行过程中的切面剖析。
业务监控
基于 OpenTelemetry, Arms 提供了以下指标的监控告警:
- 应用提供服务统计 :基于接口名称提供 次数、错误次数、错误率、响应时间 的监控
- 异常监控 :基于接口名称提供 发生次数 的监控告警
- 数据库指标 : 基于数据库名称提供 慢调用、调用次数、错误次数、响应时间 的监控
- 应用依赖服务统计 : 基于接口调用类型提供 响应时间、调用次数、错误率 的监控
目前 数据库指标 & 应用依赖服务统计 均存在无法过滤问题,阿里云反馈 11 月初修复。
扩展能力
链路日志打通
目前应用的日志中一般会打印 TraceId 来查询问题。而因 OpenTelemetry 主打就是分布式链路追踪,其内部存在一套完整的 TraceId 生成和传递。
作为应用接入, 我们可以打通可观测链路和日志,以提高问题排查的效率。当然官方提供了 如何将TraceId和SpanId写入日志 的接入文档。
但这里面存在两个实际的小问题:
- 全程黑盒,未提供应用获取 TraceId 的方案。
- 如本地环境不接入 OpenTelemetry 时, TraceId 就无效。
因此可以将 本地 TraceId 和 OpenTelemetry TraceId 做代码层面的结合,以解决以上的小问题。
在以下代码片段中 TraceContextVar 是项目中搭建的 TraceId 上下文,我们从 OpenTelemetry 获取 Trace, 并开启一个自定义 Trace 来获取当前的 traceId
注意:
- 上报的 traceId 可能不是代码中获取的原始 traceId,这里做了一个进制转化,同时补齐 32 位与最终可观测链路中的 TraceId 对齐。
- TraceContextVar 中针对外部注入的 trace_id 做了兼容处理, 如为空, 则内部生成一个随机 uuid 来承接
- TraceId 一般是请求入口,未来也可以扩展接收外部传入的 TraceId,完成上下游打通。
import typing
from logger_kit.context.variables import TraceContextVar
from logger_kit.customer_logger import logger
from opentelemetry import trace
from fastapi import Request, Response
async def http_middleware(
request: Request,
call_next: typing.Callable[[Request], typing.Awaitable[Response]],
) -> Response:
"""请求处理中间件"""
tracer = trace.get_tracer(__name__)
# 创建新的span
with tracer.start_as_current_span("http") as http_span:
# 获取 arms opentelemetry 服务提供的 trace_id
_trace_id = ""
if http_span.get_span_context().trace_id != trace.INVALID_TRACE_ID:
# 默认是 10 进制, 需要转为 16 进制 与 arms 对齐
_trace_id = hex(int(http_span.get_span_context().trace_id))[2:]
if _trace_id:
# 另外要补全 32 位(8 位十六进制数),用 '0' 补足
_trace_id = _trace_id.zfill(8)
request.state.trace_id = TraceContextVar.set(_trace_id)
logger.info("%s URL --> %s", request.method, request.url)
resp = await call_next(request)
resp.headers["x_biz_trace_id"] = request.state.trace_id
# reset trace_id
return resp
自定义 Span 上报
基于 Trace Span 的能力, 我们可以自定义 Span 以及上报一些指标数据,用于分析和判断。
from opentelemetry import trace
def call_method():
tracer = trace.get_tracer(__name__)
# 创建新的span
with tracer.start_as_current_span("stream_message") as call_method_span:
call_method_span.set_attribute("app.threads.len", 0)
# 处理自己的业务
···
能力禁用
自动埋点场景下, 如果某些业务存在大量无效上报, 则可以通过禁用模块或者排除 URL 来实现
# 排除某些 URL
export OTEL_PYTHON_EXCLUDED_URLS="client/.*/info,healthcheck"
# 禁用模块
OTEL_PYTHON_DISABLED_INSTRUMENTATIONS=redis,kafka,grpc_client
能力缺失
当然除了链路追踪, 我们一般也会关注性能指标部分,但目前 OpenTelemetry 还无法满足,常见的就有以下几个部分:
- Python 线程池监控
- 业务 thread pool 线程池
- web 服务器接收请求线程池
- 定时任务的监控
- apschedule 定时调度
监控覆盖
当然除了 OpenTelemetry 提供的业务监控,云厂商还有一些其他监控产品可以帮助我们覆盖每个角落,以提高服务的可治理性。