前言
在不久前入职在生鲜供应链领域深耕 SAAS 平台的新东家,规模不大不小,百来研发人员,有采购、订单、仓储、分拣、企业等等微服务,用的 GRPC 做远程过程调用,K8S 做 CI/CD,Istio&Envoy 做外网网关,Apollo 作为配置中心,完完全全的分布式架构,较为全面。新东家有自己实现的 grpc 脚手架去生成模版代码,但那时候处于业务快速发展系统敏捷迭代阶段,基础设施仍较为薄弱。
在我开发的一个商品溯源的迭代里,遇到了 Kafka 的事件监听去根据下游数据生成溯源业务数据的场景,其中会有一些业务异常导致溯源数据生成失败的情况,这时候我就在想,难道我们要靠用户去发现问题吗?能不能我们先比用户早发现问题?大多数这个时候会选择自己简单的通过企业微信的群机器人发送个告警去解决,但是在一个有较多人员一起参与维护的项目中各写各的代码,缺乏「协议」的制定,对日后的重构、维护是非常不利的。
于是我便主动和组长申请了一条接口告警的技术性 feature。但我深知,链路追踪是前提,我得先把在各服务中的请求数据收集并且串联起来,在这之上,再根据链路数据实现告警。
新东家是有专门的基础设施组的,人数不多,而且都还集中在 CI/CD 的优化上。而我是业务开发组的,所以只能下班时间搞。
深入研究之后,发现细节也是很多,比如一些用于服务检活的 CheckHealth 接口、定时任务发起的如数据同步之类的接口,可能一秒甚至几十毫秒就会调用一次,需要进行采样,否则数据量会非常大并且也无意义。
部分诸如报表生成、数据导入导出之类的接口是不需要告警的,告警规则需要灵活易配置。
一个月后,我完成了接口告警的开发工作,对原有业务代码完全无侵入。组长事后在一次会议上,惊叹这居然是还在试用期内员工实现的东西,把我也惊了。
效果
链路数据查询面板
下面是链路追踪的面板,可以根据参数筛选调用链路列表:
常见的场景,根据 request-id 筛选链路:
查看完整的调用链路:
链路告警
再来就是基于链路调用数据之上的接口告警,我除了调用异常告警之外,还额外实现了超时告警:
采样规则的配置:
参考了 k8s 的 --label,可以根据链路数据的 tag 中的参数灵活匹配。
{
"sampling": {
"verbose": false,
"items": [
{
"label_selector": {
"rpc.method": "^SysSync"
},
"interval": "5m"
},
{
"label_selector": {
"rpc.method": "(ListUnit|GetManyOrderDetail|ListOrderDetail|Check)"
},
"interval": "5m"
}
]
}}
}
告警规则的配置:
"alert": {
"verbose": true,
"preset": {
"duration_limit": "10s",
"whitelists": [
{
"label_selector": {
"rpc.grpc.status_code": "1"
}
},
{
"label_selector": {
"rpc.method": "^SysSync"
}
},
{
"label_selector": {
"rpc.method": "^Sync"
}
}
]
},
"items": [
{
"label_selector": {
"rpc.service": "ceres.asynctask."
},
"duration_limit": "3m",
"whitelists": []
}
]
},
同时,手撸了一个消息发送服务,也是可以根据请求消息发送服务时携带的 label 参数灵活匹配路由,指定目的地以及频控。
比如,生产的全量环境的告警、生产的灰度环境的告警、测试环境的告警发到不同的企业微信群。
同时参考了 Model&View 理念,把告警模版(一般是 Markdown)和告警数据进行分离,告警模版放在配置中可以随时改动,只要请求消息发送服务时携带的 Model 中有需要的数据,就不用重启服务:
{
"storage": {
"notification_index_name": "observability-notification",
"flush_notification_request": "2s",
"process_notification_request": "5s",
"process_notification_batch_size": 10
},
"templates": {
"tpl-tracing-duration-alert": {
"template_content": "IyMg5pyN5Yqh5o6l5Y-j6LaF5pe2CuOAkOivt-axguaXtumXtOOAkToge3sgUGFyc2VEYXRlVGltZVN0ciAuc3Bhbl9zdGFydF90aW1lIH19IFwK44CQTmFtZXNwYWNl44CROiB7eyBpbmRleCAubGFiZWxzICJrOHMubmFtZXNwYWNlLm5hbWUiIH19IFwK44CQUG9k44CROiB7eyBpbmRleCAubGFiZWxzICJrOHMucG9kLm5hbWUiIH19IFwK44CQ5o6l5Y-j44CROiB7eyBpbmRleCAubGFiZWxzICJycGMuc2VydmljZSIgfX0ve3tpbmRleCAubGFiZWxzICJycGMubWV0aG9kIiB9fSBcCuOAkHgtY2xpZW5044CROiB7eyBpbmRleCAubGFiZWxzICJ4LWNsaWVudCIgfX0gXArjgJB4LXJlcXVlc3QtaWTjgJE6IHt7IGluZGV4IC5sYWJlbHMgIngtcmVxdWVzdC1pZCIgfX0gXArjgJBncm91cC1pZOOAkToge3sgaW5kZXggLmxhYmVscyAieC1ncm91cC1pZCIgfX0gXArjgJDlhaXlj4LjgJE6IHt7IGluZGV4IC5sYWJlbHMgInJwYy5tZXRob2QucGFyYW1zIiB8IHByaW50ZiAiJS4yMDBzIC4uLiJ9fSBcCuOAkOiAl-aXtuOAkTogPGZvbnQgY29sb3I9Indhcm5pbmciPnt7IC5kdXJhdGlvbiB9fTwvZm9udD4gPGZvbnQgY29sb3I9ImNvbW1lbnQiPkV4cGVjdCB7eyAuZHVyYXRpb25fbGltaXQgfX08L2ZvbnQ-IFwKW-ivpuaDheivt-afpeeciyBKYWVnZXIg6Z2i5p2_XSh7eyAuamFlZ2VyX3VpX2hvc3QgfX0vdHJhY2Uve3sgLnRyYWNlX2lkIH19KQo=",
"note": "链路追踪「超时调用」的告警模版"
},
"tpl-tracing-error-alert": {
"template_content": "IyMg5pyN5Yqh5o6l5Y-j5byC5bi4Cnt7JGdycGNDb2RlIDo9IGluZGV4IC5sYWJlbHMgInJwYy5ncnBjLnN0YXR1c19jb2RlIiB9fQrjgJDor7fmsYLml7bpl7TjgJE6IHt7IFBhcnNlRGF0ZVRpbWVTdHIgLnNwYW5fc3RhcnRfdGltZSB9fSBcCuOAkE5hbWVzcGFjZeOAkToge3sgaW5kZXggLmxhYmVscyAiazhzLm5hbWVzcGFjZS5uYW1lIiB9fSBcCuOAkFBvZOOAkToge3sgaW5kZXggLmxhYmVscyAiazhzLnBvZC5uYW1lIiB9fSBcCuOAkOaOpeWPo-OAkToge3sgaW5kZXggLmxhYmVscyAicnBjLnNlcnZpY2UiIH19L3t7aW5kZXggLmxhYmVscyAicnBjLm1ldGhvZCIgfX0gXArjgJB4LWNsaWVudOOAkToge3sgaW5kZXggLmxhYmVscyAieC1jbGllbnQiIH19IFwK44CQeC1yZXF1ZXN0LWlk44CROiB7eyBpbmRleCAubGFiZWxzICJ4LXJlcXVlc3QtaWQiIH19IFwK44CQZ3JvdXAtaWTjgJE6IHt7IGluZGV4IC5sYWJlbHMgIngtZ3JvdXAtaWQiIH19IFwK44CQ5YWl5Y-C44CROiB7eyBpbmRleCAubGFiZWxzICJycGMubWV0aG9kLnBhcmFtcyIgfCBwcmludGYgIiUuNDAwcyAuLi4ifX0gXAp7e2lmIGVxICRncnBjQ29kZSAiMSIgfX3jgJDogJfml7bjgJE6IDxmb250IGNvbG9yPSJ3YXJuaW5nIj57eyAuZHVyYXRpb24gfX08L2ZvbnQ-IDxmb250IGNvbG9yPSJjb21tZW50Ij5FeHBlY3Qge3sgLmR1cmF0aW9uX2xpbWl0IH19PC9mb250PiBce3tlbmR9fQrjgJDlvILluLgtQ29kZeOAkToge3sgaW5kZXggLmxhYmVscyAicnBjLmdycGMuc3RhdHVzX2NvZGUiIH19IFwK44CQ5byC5bi4LURldGFpbOOAkToge3sgaW5kZXggLmxhYmVscyAiZXhjZXB0aW9uLm1lc3NhZ2UiIH19IFwK44CQ5byC5bi4LVN0YWNr44CROiB7eyBpbmRleCAubGFiZWxzICJleGNlcHRpb24uc3RhY2t0cmFjZSIgfCBwcmludGYgIiUuNjAwcyAuLi4iIH19Cgpb6K-m5oOF6K-35p-l55yLIEphZWdlciDpnaLmnb9dKHt7IC5qYWVnZXJfdWlfaG9zdCB9fS90cmFjZS97eyAudHJhY2VfaWQgfX0p",
"note": "链路追踪「异常调用」的告警模版"
}
},
"destinations": {
"des-tracing-duration-alert": {
"type": 1,
"webhook": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=bcb4eebf-78c8-4564-bea0-",
"note": "线上环境服务告警群 - 超时告警机器人"
},
"des-tracing-error-alert": {
"type": 1,
"webhook": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=f3ab7837-1fd9-4049-bd9e-",
"note": "线上环境服务告警群 - 异常告警机器人"
},
"des-staging-tracing-duration-alert": {
"type": 1,
"webhook": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=7d3e8500-c3ed-4233-8456-",
"note": "线上灰度环境服务告警群 - 超时告警机器人"
},
"des-staging-tracing-error-alert": {
"type": 1,
"webhook": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=b705b048-0258-4c25-8b40-",
"note": "线上灰度环境服务告警群 - 异常告警机器人"
}
},
"routes": [
{
"label_selector": {
"alert_type": "duration-limit-judger",
"k8s.namespace.name": "env-ceres-production"
},
"destination_name": "des-tracing-duration-alert",
"template_name": "tpl-tracing-duration-alert",
"frequency_control_name": "Ns_Service_Method"
},
{
"label_selector": {
"alert_type": "error-judger",
"k8s.namespace.name": "env-ceres-production"
},
"destination_name": "des-tracing-error-alert",
"template_name": "tpl-tracing-error-alert",
"frequency_control_name": "Ns_Service_Method_Err"
},
{
"label_selector": {
"alert_type": "duration-limit-judger",
"k8s.namespace.name": "env-ceres-staging"
},
"destination_name": "des-staging-tracing-duration-alert",
"template_name": "tpl-tracing-duration-alert",
"frequency_control_name": "Ns_Service_Method"
},
{
"label_selector": {
"alert_type": "error-judger",
"k8s.namespace.name": "env-ceres-staging"
},
"destination_name": "des-staging-tracing-error-alert",
"template_name": "tpl-tracing-error-alert",
"frequency_control_name": "Ns_Service_Method_Err"
}
],
"frequency_controls": {
"Ns_Service_Method": {
"label_keys": [
"alert_type",
"k8s.namespace.name",
"rpc.service",
"rpc.method"
],
"interval": "5m",
"count": 1,
"note": "元组 (告警类型, k8s命名空间,接口) 为键名 5 分钟 1 次"
},
"Ns_Service_Method_Err": {
"label_keys": [
"alert_type",
"k8s.namespace.name",
"rpc.service",
"rpc.method",
"rpc.grpc.status_code"
],
"interval": "5m",
"count": 1,
"note": "元组 (告警类型, k8s命名空间, 接口, 异常类型) 为键名 5 分钟 1 次"
}
}
}
实现
如果把链路追踪分为 frontend 和 backend,
那 frontend 就是 Opentelemetry,backend 则是 Jaeger。
frontend 负责链路数据的协议定制以及采集和导出,backend 则负责链路数据的具体落地,各司其职。
协议定制是什么?打个比方,用于保存一次链路调用过程相关参数的结构体应该如何制定,提供给使用方的 API 应该如何制定,导出数据的 API 应该如何制定,如何在分布式环境下传递链路相关的上下文等等。
链路数据的具体落地是什么?最重要的就是
下面简单介绍一下 Opentelemetry 和 Jaeger。
Opentelemetry & Jaeger
Opentelemetry 的前身是专注于链路追踪的 Opentracing,后面被 CNCF 组织归化,融合了链路追踪 Tracing & 指标 Metrics & 日志 Log 三大模块,称为 Observability 可观测性,旨在更好的对系统进行黑盒观察,不过还处于发展阶段,目前在我看来只有 Tracing 模块是较为完善的。
解释一下上图出现的名词,OTel即 Opentelemetry,OTel Auto.Inst.即一些用于自动化采集链路调用的组件,由开源社区贡献,比如我就使用了otelgrpc 这一采集 grpc 链路调用数据的组件,实现原理其实也就是插桩到 grpc 的拦截器里面。
OTel API即对外暴露给使用方的 API,用于开启一次、结束一次链路调用记录等等。
OTel SDK 是 OTel API 的具体实现,里面详细定义了链路调用记录的结构体等等。
OTLP 全称 Opentelemetry Protocol,可以认为是上面的统称,也即我们的系统通过使用 Opentelemetry 后,对外导出的都是符合 OTLP 规范的数据,只要后端实现了 OTLP,那我们的系统就可以随时接入/切换到不同的后端,比如 Jaeger。
OTel Collector由 Receiver & Processor & Exporter 组成,官方实现的 OTel-Exporter 和 OTel-Receiver 分别可以导出和接收符合 OTLP 规范的数据,Jaeger 就使用了该 Receiver 去接收来自 OTel 的数据;而 Processor 用于处理数据,类似于流式计算里面的 Operator,比较重要的有 BatchProcessor 用于实现流转批处理,使用者也可以自定义自己的 Processor,比如我实现的从 golang 的 ctx 里取出 grpc 的 metadata 记录在链路数据中。