OpenTelemetry Collector 节点宕机场景下的排查与优化
前言
OpenTelemetry Collector 是 OpenTelemetry 的核心组件,但在底层基础设施(如 Kubernetes 节点)故障时,可能暴露出阻塞或延迟问题。本文通过一次因 Sampling 服务节点宕机引发的故障,结合代码分析其原因,并提供临时和长期解决方案。
问题描述
一天,收到告警,OpenTelemetry 出现 Exporter Trace 异常的情况,具体表现为:
- OpenTelemetry Collector 的负载均衡器指标
otelcol_loadbalancer_num_resolutions
变为 0,表明 DNS 解析更新停止。 - OpenTelemetry Collector 的负载均衡器指标
otelcol_exporter_sent_spans
变为 0, 表明所有 Exporter停止发送数据。 - 系统恢复时间异常长,至少需要 15 分钟以上才能恢复。
架构背景
故障发生在一个多层遥测数据处理架构中,具体流程如下:
客户端 -> OpenTelemetry-Collector -> OpenTelemetry-Collector-Sampling -> Trace 后端服务
- 客户端:应用程序或服务,生成并发送 Trace 数据。
- OpenTelemetry-Collector:第一层 Collector,接收客户端数据,使用负载均衡器分发到下游服务。
- OpenTelemetry-Collector-Sampling:第二层 Collector,负责采样处理(基于 Trace ID 进行采样),运行在 Kubernetes 集群中。
- Trace 后端服务:最终存储和分析遥测数据的服务。
初步排查确认,此次故障影响了第一层 Collector 的正常运行。
版本背景
触发本次问题的 OpenTelemetry Collector 版本为 0.73.0(发布于 2023 年初)。此版本的负载均衡器实现存在已知问题,尤其是在处理端点下线时的阻塞行为尚未优化(后续版本通过 PR #31602 修复)。
原因分析
通过日志、指标和代码分析,我们锁定了问题根源: Sampling 服务的一个 Pod 因所在节点宕机下线,节点宕机后,导致第一层 Collector 的 gRPC 连接未正常关闭,Collector 的数据发送和 DNS 解析组件在处理下线端点时发生阻塞。以下是详细故障链条,结合代码剖析:
1. 节点宕机,gRPC 连接未正常关闭
Sampling 服务的一个 Pod 因节点宕机下线。由于是意外故障,gRPC 连接未执行正常关闭流程,客户端(第一层 Collector)gRPC 未立即感知服务器不可用。
2. DNS Resolver 检测到端点变化
dnsResolver
组件每 5 秒解析 DNS,监控 Sampling 服务端点:
func (r *dnsResolver) resolve(ctx context.Context) ([]string, error) { r.shutdownWg.Add(1) defer r.shutdownWg.Done() addrs, err := r.resolver.LookupIPAddr(ctx, r.hostname) if err != nil { _ = stats.RecordWithTags(ctx, resolverSuccessFalseMutators, mNumResolutions.M(1)) return nil, err } _ = stats.RecordWithTags(ctx, resolverSuccessTrueMutators, mNumResolutions.M(1)) var backends []string for _, ip := range addrs { var backend string if ip.IP.To4() != nil { backend = ip.String() } else { backend = fmt.Sprintf("[%s]", ip.String()) } if r.port != "" { backend = fmt.Sprintf("%s:%s", backend, r.port) } backends = append(backends, backend) } sort.Strings(backends) if equalStringSlice(r.endpoints, backends) { return r.endpoints, nil } // **关键点 1:端点变化触发回调** r.updateLock.Lock() r.endpoints = backends r.updateLock.Unlock() _ = stats.RecordWithTags(ctx, resolverSuccessTrueMutators, mNumBackends.M(int64(len(backends)))) r.changeCallbackLock.RLock() for _, callback := range r.onChangeCallbacks { callback(r.endpoints) // 调用 onBackendChanges } r.changeCallbackLock.RUnlock() return r.endpoints, nil }
- 逻辑 :检测到 Sampling 端点减少后,触发
onBackendChanges
更新负载均衡器。 - 指标 :
mNumResolutions
若为 0,表明 5s 一次的resolve
解析被onBackendChanges
阻塞。
3. 负载均衡器更新后端
onBackendChanges
处理端点变化:
func (lb *loadBalancerImp) onBackendChanges(resolved []string) { newRing := newHashRing(resolved) if !newRing.equal(lb.ring) { // **关键点 2:加锁更新** lb.updateLock.Lock() defer lb.updateLock.Unlock() lb.ring = newRing ctx := context.Background() lb.addMissingExporters(ctx, resolved) lb.removeExtraExporters(ctx, resolved) } }
- 逻辑 :在
onBackendChanges
中,加了 updateLock 写锁,因为removeExtraExporters
需要排空队列中的数据,等 updateLock 锁的操作都要被阻塞等待。
4. 移除下线 Exporter
removeExtraExporters
下线多余端点:
func (lb *loadBalancerImp) removeExtraExporters(ctx context.Context, endpoints []string) { endpointsWithPort := make([]string, len(endpoints)) for i, e := range endpoints { endpointsWithPort[i] = endpointWithPort(e) } for existing := range lb.exporters { if !endpointFound(existing, endpointsWithPort) { // **关键点 3:Shutdown 清理积压数据** _ = lb.exporters[existing].Shutdown(ctx) delete(lb.exporters, existing) } } }
- 问题 :在 0.73.0 版本中,
Shutdown
清理boundedMemoryQueue
时,因 gRPC 服务器不可用,每次发送等待超时,加重试逻辑,耗时极长。
5. Trace ID 路由与 Exporter 调用
数据路由到 Sampling 服务的逻辑在 traceExporterImp.consumeTrace
中实现:
func (e *traceExporterImp) consumeTrace(ctx context.Context, td ptrace.Traces) error { var exp component.Component routingIds, err := routingIdentifiersFromTraces(td, e.routingKey) if err != nil { return err } for rid := range routingIds { endpoint := e.loadBalancer.Endpoint([]byte(rid)) // 获取端点 // **关键点 4:根据 endpoint 获取 Exporter** exp, err = e.loadBalancer.Exporter(endpoint) // 获取 Exporter if err != nil { return err } te, ok := exp.(exporter.Traces) if !ok { return fmt.Errorf("unable to export traces, unexpected exporter type: expected exporter.Traces but got %T", exp) } start := time.Now() // **关键点 5:发送数据到 Sampling 服务** err = te.ConsumeTraces(ctx, td) duration := time.Since(start) if err == nil { _ = stats.RecordWithTags( ctx, []tag.Mutator{tag.Upsert(endpointTagKey, endpoint), successTrueMutator}, mBackendLatency.M(duration.Milliseconds())) } else { _ = stats.RecordWithTags( ctx, []tag.Mutator{tag.Upsert(endpointTagKey, endpoint), successFalseMutator}, mBackendLatency.M(duration.Milliseconds())) } } return err }
获取 Exporter 的具体实现
loadBalancer.Exporter
方法负责返回指定端点的 Exporter:
func (lb *loadBalancerImp) Exporter(endpoint string) (component.Component, error) { // NOTE: make rolling updates of next tier of collectors work. currently, this may cause // data loss because the latest batches sent to outdated backend will never find their way out. // for details: https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/1690 // **关键点 6:根据 endpoint 获取 Exporter 时加读锁** lb.updateLock.RLock() exp, found := lb.exporters[endpointWithPort(endpoint)] lb.updateLock.RUnlock() if !found { return nil, fmt.Errorf("couldn't find the exporter for the endpoint %q", endpoint) } return exp, nil }
- 逻辑 :
routingIdentifiersFromTraces
根据 Trace ID 和路由键生成标识符。loadBalancer.Endpoint
使用一致性哈希映射到端点。loadBalancer.Exporter
获取对应 Exporter,使用读锁(RLock
)访问exporters
映射。ConsumeTraces
通过 gRPC 发送数据到 Sampling 服务。
- 与故障的关联 :
- 当
onBackendChanges
调用removeExtraExporters
并持有updateLock
的写锁(Lock
)时,Shutdown
的长时间执行会阻塞写锁释放。 Exporter
方法需要获取读锁(RLock
),但写锁未释放时,读锁无法获取,导致consumeTrace
中的Exporter
调用被阻塞。- 结果是新数据的发送操作(
ConsumeTraces
)无法进行,数据积压加剧。
- 当
6. 阻塞效应与恢复延迟
- 阻塞 :
Shutdown
持有updateLock
写锁,阻塞dnsResolver
(需写锁更新端点)和Exporter
(需读锁获取实例)。 - 影响 :
otelcol_loadbalancer_num_resolutions
为 0,dns 服务发现停止工作,不能及时发现 Sampling 的副本变化(当故障触发时,Sampling 服务因为收不到请求, CPU 掉到非常低,因为 HPA ,副本数会缩到特别少)。otelcol_exporter_sent_spans
为 0 ,所有数据发送停止。 - 耗时:在 0.73.0 版本中,清理积压数据耗时 15 分钟以上。
临时解决方案:调整 gRPC Keepalive 参数
在 OpenTelemetry Collector v0.73.0 的故障场景中,Sampling 服务节点宕机导致 gRPC 连接未正常关闭,Shutdown 操作阻塞了负载均衡器的更新和数据发送,恢复时间长达 15 分钟。为缩短恢复时间,我们调整了 gRPC 的 Keepalive 参数,使 gRPC 客户端更快感知连接异常,从而加速端点下线流程。以下是具体的配置和分析。
gRPC 客户端配置 (OpenTelemetry-Collector)
loadbalancing: protocol: otlp: compression: none tls: insecure: true keepalive: time: 10s timeout: 3s permit_without_stream: true
参数解析
time: 10s
- 作用:客户端每 10 秒发送一次保活 ping 到服务器,检查连接是否存活。
- 调整原因:默认情况下,gRPC 客户端可能使用较长的保活间隔(或未启用),导致感知服务器下线的时间过长。缩短到 10 秒后,Collector 能更快发现 Sampling 服务端点不可用。
- 注意事项:gRPC 客户端强制最小值为 10 秒(低于此值会自动调整为 10 秒),因此这是较优的短期选择。
timeout: 3s
- 作用:发送保活 ping 后,客户端等待 3 秒,若无响应则认为连接已死。
- 调整原因 :默认超时(通常 20 秒)过长,导致每次探测等待时间累积,延长了
Shutdown
的执行时间。3 秒的超时能在网络正常时保证响应,又能在异常时快速失败。 - 效果 :结合
time: 10s
,最坏情况下 13 秒(10s + 3s)内感知异常,远低于默认配置。
permit_without_stream: true
- 作用:允许客户端在没有活跃 RPC 流时仍发送保活 ping。
- 调整原因 :在 Collector 与 Sampling 服务之间,可能存在空闲连接(无数据传输)。若设为
false
,空闲时不会探测,延迟异常检测。设为true
确保所有连接状态实时更新。 - 场景适用性:此架构中,Collector 可能长时间持有连接但不发送数据,启用此参数尤为重要。
gRPC 服务器配置 (OpenTelemetry-Collector-Sampling)
receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:55681 keepalive: enforcement_policy: min_time: 8s permit_without_stream: true
参数解析
enforcement_policy.min_time: 8s
- 作用:服务器要求客户端的保活 ping 间隔不低于 8 秒,若客户端发送过于频繁(例如每 1 秒),服务器会断开连接。
- 调整原因 :客户端的
time: 10s
大于 8 秒,满足服务器要求,避免因违反策略被断开。默认值通常为 5 分钟(300 秒),调整为 8 秒是为了与客户端的短间隔探测兼容。 - 注意事项 :若客户端
time
小于min_time
(例如设为 5 秒),服务器会拒绝连接,客户端需自动加倍time
(如从 5 秒到 10 秒),可能引入额外延迟。
permit_without_stream: true
- 作用:允许服务器接收客户端在无活跃 RPC 流时的保活 ping。
- 调整原因 :与客户端配置保持一致,确保空闲连接也能维持保活探测。若设为
false
,服务器可能因未收到预期 ping 而断开空闲连接。 - 效果:增强了架构中空闲连接的稳定性。
效果与验证
- 测试结果:在 RND 环境中复现故障后,调整 Keepalive 参数将恢复时间从 15 分钟缩短到不到 1 分钟。
- 原理 :
- 客户端每 10 秒探测一次,3 秒超时,最多 13 秒内感知 Sampling 服务端点下线。
- gRPC 连接标记为不可用后,
Shutdown
操作无需等待长时间超时,快速完成boundedMemoryQueue
清理。 updateLock
释放后,dnsResolver
和Exporter
恢复正常,数据发送不再阻塞。
- 指标验证 :
otelcol_loadbalancer_num_resolutions
从 0 恢复到正常值,mBackendLatency
显示发送延迟下降。
长期解决方案:升级 OpenTelemetry Collector
社区通过 PR #31602 优化了 0.73.0 版本的问题:
- 优化 :
Shutdown
异步执行,不阻塞updateLock
,避免影响Exporter
调用。 - 版本:2024 年 3 月后(例如 v0.96.0)。
- 建议:升级并测试。
总结与建议
问题回顾
在 客户端 -> OpenTelemetry-Collector (v0.73.0) -> OpenTelemetry-Collector-Sampling -> Trace 后端服务
架构中,Sampling 节点宕机导致 Collector 侧的 gRPC 连接 异常,Collector 的数据发送和 DNS 解析组件在处理下线端点时发生阻塞。恢复时间达 15 分钟。
解决方案
- 临时:调整 Keepalive 参数,恢复时间缩至 1 分钟。
- 长期:升级到 v0.96.0 或更高版本,异步优化解决问题。
建议
- 短期应用 Keepalive 配置。
- 长期升级 Collector,脱离 0.73.0 的局限。
- 监控
otelcol_loadbalancer_num_resolutions
和mBackendLatency
,没有这些指标,问题无从入手。 - 任何优化手段上线前,模拟故障验证效果。
通过版本背景和代码分析,我们理解了 0.73.0 的问题根源,希望这篇博文为类似场景提供参考!

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
DeepSeek:“国运级创新”,凭啥?
前言: 春节前后,要说国内外科技圈最火热的名字,DeepSeek 绝对算头一个。“开源大模型之光”、“AI 领域新星”、“有望比肩 OpenAI”…… 各种赞誉纷至沓来,甚至有人直接将其冠于“国运级创新”的说法。“国运级创新”? 这个帽子可实在不小。但是有人会质疑,凭啥?本文则试着从产业共识的角度来分析,笔者认为它确实担得起。 1. DeepSeek,是国运级创新! 要探寻 “DeepSeek 是国运级创新” 这一说法的源头,最早的说法来自游戏圈内极具影响力的人物——现象级3A游戏《黑神话:悟空》的制作公司游戏科学创始人冯骥。他在新浪微博上就曾公开表示:“DeepSeek 可能是个国运级别的科技成果”。冯骥以其独到的眼光和对技术趋势的敏锐洞察力而著称,他的这番评价,无疑引发了广泛关注。下图是他的微博截图。 国内安全领域的领军人物,360 集团创始人周鸿祎也表达了对 DeepSeek 的有力支持。 在今年两会期间接受《新京报》记者关于 “人工智能技术开源” 话题的采访时,他 “举双手赞同” 冯骥关于 DeepSeek “国运级别的科技成果” 的评价。他认为开源模式形成了巨大的虹吸效应,一...
- 下一篇
SelectDB 实时分析性能突出,宝舵成本锐减与性能显著提升的双赢之旅
BOCDOP 宝舵早期基于 TiDB 构建实时数仓,随着数据量增长,在数据处理效率、OLAP 能力扩展、功能支持、成本与资源方面存在一定优化空间。为提升数据分析能力并优化成本,宝舵引入 SelectDB,达成写入速度提升 10 倍,成本直降 30% 的显著成效。 本文转录自高瑞军(宝尊科技 高级架构师)在 Doris Summit Asia 2024 上的演讲,经编辑整理。 业务背景 宝尊集团创立于 2007 年,是中国品牌电商服务行业的领导者、先行者及数字商业赋能者。目前宝尊集团约有 8000 名员工,业务遍及东亚、东南亚、欧洲、北美等多个国家和地区,服务全球各行各业超过 450 家品牌,立足为品牌提供面向全球、面向未来的服务与产品。BOCDOP 宝舵(后文简称"宝舵")是宝尊集团商业化独立品牌,目前拥有 1000 余名内部技术工程师,为集团业务提供强大的自主研发系统支持。 宝舵 BBI Cloud 是由宝舵开发的电商全渠道数据采集、整合与分析应用产品,核心功能包括: 多渠道数据采集与自助取数: 支持 100+ 模块的多渠道数据实时接入,覆盖多渠道店铺运营业务。提供一站式拖拉拽与透视...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7,CentOS8安装Elasticsearch6.8.6
- CentOS关闭SELinux安全模块
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- CentOS6,CentOS7官方镜像安装Oracle11G
- Docker安装Oracle12C,快速搭建Oracle学习环境
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作