← 返回新闻列表

Easegress API网关etcd内存占用过高问题解析及优化

近期,开源软件Easegress在用户场景中遭遇etcd内存占用异常攀升的问题。尽管etcd中存储的数据量较小,但内存消耗却高达数GB。深入调查发现,该问题主要源于etcd的Raft Log设计,特别是当单个Key的Value过大时,会造成大量内存开销。本文将详细探讨问题根源、etcd相关设计缺陷及其最终解决方案,并提供使用etcd的优化建议。

文 / 编辑部 · 2022/05/05 · 阅读约 4 分钟

分享:
Easegress API网关etcd内存占用过高问题解析及优化

近期,开源API网关Easegress的用户在使用过程中发现一个严峻问题:etcd组件的内存占用异常飙升,有时甚至超过10GB,且长时间无法释放。此次问题发生在一个特定场景下,用户配置了上千条处理管道(pipeline),导致Easegress初始化完成后,内存从400MB逐渐增长,在无请求的情况下,运行80分钟后达到2GB,200分钟后更是攀升至4GB。

Easegress作为一款功能强大的API应用网关,提供API编排、服务发现、弹性设计、认证鉴权等多种高级功能,支持微服务、Service Mesh等云原生架构,并能应对高并发、灰度发布等复杂企业级需求。其核心设计包括无第三方依赖的选主集群能力、类似Linux管道的流式处理插件机制,以及内置的数据存储用于集群控制与数据共享。早期的Easegress曾尝试使用gossip协议同步状态,但考虑到其复杂性及调试难度,最终在三年前切换至内嵌版本etcd,以提升稳定性,并沿用至今。Easegress将所有配置信息、统计监控数据以及用户自定义数据存储在etcd中,以方便用户扩展。

尽管存储在etcd中的数据总量不足10MB,内存却消耗巨大,这起初令人怀疑etcd可能存在内存泄漏。然而,在排查etcd的历史版本问题后,发现Easegress使用的是etcd 3.5最新版本,且内存泄漏问题通常不会达到如此规模。这促使开发团队深入研究etcd的设计机制,以找出是否在使用上存在误区。

经过两天的详细分析,开发团队发现etcd的内存消耗主要集中在以下几个方面:

首先是Raft Log。etcd使用内存作为Raft Log的底层存储,用于帮助追随者节点同步数据。此Log默认至少保留5000条最新的请求。如果单个Key的Value尺寸较大,例如持续更新一个1MB的Key,那么5000条Log可能造成高达5GB的内存开销。尽管etcd官方已将该默认值从10000降低至5000,且此数值在源代码中是硬编码(DefaultSnapshotCatchUpEntries),但并未根本解决大尺寸Value引发的内存问题。

其次,etcd的索引机制也是内存消耗的重要来源。每一个Key-Value对都会在内存中维护一个B-tree索引,其开销与Key的长度及历史版本数量有关。

此外,etcd使用mmap技术将boltdb文件映射到虚拟内存,导致DB文件越大,内存占用越大。

最后,Watcher机制也会显著增加内存占用,尤其当存在大量Watch客户端和Watch连接时。

针对Easegress用户的具体问题,调查显示,问题核心在于Raft Log。用户配置的上千条pipeline,Easegress会为每条pipeline进行数据统计,并将这1000多条统计数据合并写入一个Key中。这些统计数据合并后,导致单个Key的平均Value尺寸达到2MB。因此,5000个内存中的Raft Log条目实际上消耗了5000 * 2MB = 10GB的巨大内存。由于之前未遇到如此大规模pipeline的场景,此内存问题此前并未暴露。

解决方案在于改变数据写入策略。团队不再将所有统计数据合并写入一个大型Value,而是将其拆分为多个小型Key值进行存储。虽然实际保存的数据总量不变,但每条Raft Log中的数据量大幅减少。原先是5000条2MB的日志导致10GB内存,现在拆分后,每个日志条目仅为1KB,从而使得5000条日志的内存占用降低到500MB,从而有效解决了内存膨胀问题。相关的代码修改已提交至PR#542。

总结有效的etcd使用实践包括:

避免使用大尺寸的Key和Value,以减少Raft Log和B-tree索引带来的内存消耗。

定期通过compact和defrag操作对DB进行压缩和碎片整理,控制DB的整体尺寸。

审慎管理Watch Client和Watch数量,避免因Watcher过多而导致内存堆积。

优先使用etcd和Go语言的最新版本,因为新版本通常包含了内存优化和问题修复,例如Go 1.16将内存回收机制从MADV_FREE改为MADV_DONTNEED,能更及时地释放系统内存。etcd 3.4是基于Go 1.12编译的,可能受到旧版本Go内存回收机制的影响。

广告位 · 文末横幅