日前,开源软件Easegress在使用过程中出现了一个引人关注的现象:其核心组件etcd的内存占用量呈现出惊人的增长。这一问题最初在Easegress 1.4.1版本中被用户反馈,表现为即使在无请求的空闲状态下,内存也会从初始的400MB在数小时内迅速膨胀至数GB甚至更高,某些情况下甚至突破10GB,且难以回落。
Easegress作为一款功能全面的API应用网关,其设计理念旨在超越传统反向代理,实现API编排、服务发现、弹性设计以及认证鉴权等高级功能。为支撑这些复杂场景,Easegress核心设计之一便是内置数据存储,用于集群控制与数据共享,而etcd正是承载这一职责的关键组件。项目团队早期曾尝试基于gossip协议进行状态同步,但考虑到复杂性和调试难度,最终在三年前切换至了内嵌的etcd版本,并沿用至今,所有配置信息、统计监控数据乃至用户的自定义数据均存储于etcd中,大大提升了扩展性。
此次内存问题发生在一个特殊的用户场景中:用户配置了上千条处理管道(pipeline)。虽然这种超大规模的配置方式并不常见,但Easegress会为每条pipeline进行数据统计,这些统计信息虽单条不大,但会被合并写入etcd的同一个key中。最终,数百条pipeline的统计数据汇聚成了一个平均大小为2MB的巨大key。
经过深入排查,发现内存消耗的症结并非etcd本身的内存泄漏(Easegress采用的是etcd 3.5版本),而是对其使用方式的误解,特别是etcd的Raft Log机制。etcd为了确保集群数据同步,其Raft Log并非存储在文件系统,而是常驻内存,并且默认会保留至少5000条最新请求记录(DefaultSnapshotCatchUpEntries)。当有大型key重复更新时,这5000条日志会占据巨额内存空间。例如,一个2MB的key若是更新5000次,将直接导致5000 * 2MB = 10GB的内存开销。etcd官方团队曾将此默认值从10000降至5000,也从侧面印证了这一设计对内存的巨大影响。
除了Raft Log,etcd还有其他几项设计也会影响内存占用:其一是高效查询所需的B-tree索引,它会存储键值对及其历史版本,内存开销与key长度和版本数量成正比;其二是利用mmap技术对boltdb进行文件映射,使得数据库文件越大,占用的虚拟内存也越多;其三是大量Watcher客户端和Watch数量也会显著增加内存。
针对该特定用户场景,Easegress团队确认,Raft Log的问题是导致内存飙升的主要原因。他们通过调整数据写入策略成功解决了问题:不再将上千条pipeline的统计数据合并成一个巨大的key写入etcd,而是将其拆分为多个较小的key进行分散存储。这样一来,虽然实际存储的数据总量不变,但每条Raft Log记录的数据量大幅减少。原本5000条2MB的日志会占用10GB内存,现在拆分后,5000条1KB的日志仅需500MB,有效地缓解了内存压力。
此次事件也为etcd的使用者提供了宝贵经验。首先,应避免存储过大尺寸的key和value,以减少Raft Log和B-tree索引的内存消耗。其次,建议定期使用etcd的compact和defreg功能进行数据库压缩和碎片整理,以控制DB文件大小和相关mmap内存。再者,应合理控制Watcher客户端和Watch的数量,避免其造成过大的内存负担。最后,始终保持Go语言和etcd版本更新也至关重要,因为新版本通常会包含内存管理方面的优化(例如Go 1.16将内存回收机制从MADV_FREE改为MADV_DONTNEED,显著改善了常驻内存RSS的表现)。
