随着腾讯自研上云及公有云用户的迅速增长,一方面,腾讯云容器服务TKE服务数量和核数大幅增长, 另一方面我们提供的容器服务类型(TKE托管及独立集群、EKS弹性集群、edge边缘计算集群、mesh服务网格、serverless knative)也越来越丰富。各类容器服务类型背后的核心都是K8s,K8s核心的存储etcd又统一由我们基于K8s构建的etcd平台进行管理。基于它我们目前管理了千级etcd集群,背后支撑了万级K8s集群。
在万级K8s集群规模下的我们如何高效保障etcd集群的稳定性?
etcd集群的稳定性风险又来自哪里?
我们通过基于业务场景、历史遗留问题、现网运营经验等进行稳定性风险模型分析,风险主要来自旧TKE etcd架构设计不合理、etcd稳定性、etcd性能部分场景无法满足业务、测试用例覆盖不足、变更管理不严谨、监控是否全面覆盖、隐患点是否能自动巡检发现、极端灾难故障数据安全是否能保障。
前面所描述的etcd平台已经从架构设计上、变更管理上、监控及巡检、数据迁移、备份几个方面程度解决了我们管理的各类容器服务的etcd可扩展性、可运维性、可观测性以及数据安全性,因此本文将重点描述我们在万级K8s场景下面临的etcd内核稳定性及性能挑战,比如:
本文将简易描述我们是如何发现、分析、复现、解决以上问题及挑战,以及从以上过程中我们获得了哪些经验及教训,并将之应用到我们的各类容器服务存储稳定性保障中。
同时,我们将解决方案全部贡献、回馈给etcd开源社区, 截止目前我们贡献的30+ pr已全部合并到社区。腾讯云TKE etcd团队是etcd社区2020年上半年最活跃的贡献团队之一, 为etcd的发展贡献我们的一点力量, 在这过程中特别感谢社区AWS、Google、Ali等maintainer的支持与帮助。
从GitLab误删主库丢失部分数据到GitHub数据不一致导致中断24小时,再到号称"不沉航母"的AWS S3故障数小时等,无一例外都是存储服务。稳定性对于一个存储服务、乃至一个公司的口碑而言至关重要,它决定着一个产品生与死。稳定性优化案例我们将从数据不一致的严重性、两个etcd数据不一致的bug、lease内存泄露、mvcc 死锁、wal crash方面阐述,我们是如何发现、分析、复现、解决以上case,并分享我们从每个case中的获得的收获和反思,从中汲取经验,防患于未然。
谈到数据不一致导致的大故障,就不得不详细提下GitHub在18年一次因网络设备的例行维护工作导致的美国东海岸网络中心与东海岸主要数据中心之间的连接断开。虽然网络的连通性在43秒内得以恢复,但是短暂的中断引发了一系列事件,最终导致GitHub 24小时11分钟的服务降级,部分功能不可用。
GitHub使用了大量的MySQL集群存储GitHub的meta data,如issue、pr、page等等,同时做了东西海岸跨城级别的容灾。故障核心原因是网络异常时GitHub的MySQL仲裁服务Orchestrator进行了故障转移,将写入数据定向到美国西海岸的MySQL集群(故障前primary在东海岸),然而美国东海岸的MySQL包含一小段写入,尚未复制到美国西海岸集群,同时故障转移后由于两个数据中心的集群现在都包含另一个数据中心中不存在的写入,因此又无法安全地将主数据库故障转移回美国东海岸。
最终, 为了保证保证用户数据不丢失,GitHub不得不以24小时的服务降级为代价来修复数据一致性。
数据不一致的故障严重性不言而喻,然而etcd是基于raft协议实现的分布式高可靠存储系统,我们也并未做跨城容灾,按理数据不一致这种看起来高大上bug我们是很难遇到的。然而梦想是美好的,现实是残酷的,我们不仅遇到了不可思议的数据不一致bug, 还一踩就是两个,一个是重启etcd有较低的概率触发,一个是升级etcd版本时如果开启了鉴权,在K8s场景下较大概率触发。在详细讨论这两个bug前,我们先看看在K8s场景下etcd数据不一致会导致哪些问题呢?
首先第一个不一致bug是重启etcd过程中遇到的,人工尝试复现多次皆失败,分析、定位、复现、解决这个bug之路几经波折,过程很有趣并充满挑战,最终通过我对关键点增加debug日志,编写chaos monkey模拟各种异常场景、边界条件,实现复现成功。最后的真凶竟然是一个授权接口在重启后重放导致鉴权版本号不一致,然后放大导致多版本数据库不一致, 部分节点无法写入新数据, 影响所有v3版本的3年之久bug。
随后我们提交若干个相关pr到社区, 并全部合并了, 最新的etcd v3.4.9[1],v3.3.22[2]已修复此问题, 同时google的jingyih也已经提K8s issue和pr[3]将K8s 1.19的etcd client及server版本升级到最新的v3.4.9。此bug详细可参考超凡同学写的文章三年之久的 etcd3 数据不一致 bug 分析。
第二个不一致bug是在升级etcd过程中遇到的,因etcd缺少关键的错误日志,故障现场有效信息不多,定位较困难,只能通过分析代码和复现解决。然而人工尝试复现多次皆失败,于是我们通过chaos monkey模拟client行为场景,将测试环境所有K8s集群的etcd分配请求调度到我们复现集群,以及对比3.2与3.3版本差异,在可疑点如lease和txn模块增加大量的关键日志,并对etcd apply request失败场景打印错误日志。
通过以上措施,我们比较快就复现成功了, 最终通过代码和日志发现是3.2版本与3.3版本在revoke lease权限上出现了差异,3.2无权限,3.3需要写权限。当lease过期的时候,如果leader是3.2,那么请求在3.3节点就会因无权限导致失败,进而导致key数量不一致,mvcc版本号不一致,导致txn事务部分场景执行失败等。最新的3.2分支也已合并我们提交的修复方案,同时我们增加了etcd核心过程失败的错误日志以提高数据不一致问题定位效率,完善了升级文档,详细说明了lease会在此场景下引起数据不一致性,避免大家再次采坑。
众所周知etcd是golang写的,而golang自带垃圾回收机制也会内存泄露吗?首先我们得搞清楚golang垃圾回收的原理,它是通过后台运行一个守护线程,监控各个对象的状态,识别并且丢弃不再使用的对象来释放和重用资源,若你迟迟未释放对象,golang垃圾回收不是万能的,不泄露才怪。比如以下场景会导致内存泄露:
接下来看看我们遇到的这个etcd内存泄露属于哪种情况呢?事情起源于3月末的一个周末起床后收到现网3.4集群大量内存超过安全阈值告警,立刻排查了下发现以下现象:
此内存泄露bug属于内存数据结构管理不周导致的,问题修复后,etcd社区立即发布了新的版本(v3.4.6+)以及K8s都立即进行了etcd版本更新。
从这个内存泄露bug中我们获得了以下收获和最佳实践:
死锁是指两个或两个以上的goroutine的执行过程中,由于竞争资源相互等待(一般是锁)或由于彼此通信(chan引起)而造成的一种程序卡死现象,无法对外提供服务。deadlock问题因为往往是在并发状态下资源竞争导致的, 一般比较难定位和复现, 死锁的性质决定着我们必须保留好分析现场,否则分析、复现及其困难。
**那么我们是如何发现解决这个deadlock bug呢?**问题起源于内部团队在压测etcd集群时,发现一个节点突然故障了,而且一直无法恢复,无法正常获取key数等信息。收到反馈后,我通过分析卡住的etcd进程和查看监控,得到以下结论:
这个bug也隐藏了很久,影响所有etcd3版本,在集群中写入量较大,某落后的较多的节点执行了快照重建,同时此时又恰恰在做历史版本压缩,那就会触发。我提交的修复PR目前也已经合并到3.3和3.4分支中,新的版本已经发布(v3.3.21+/v3.4.8+)。
从这个死锁bug中我们获得了以下收获和最佳实践:
panic是指出现严重运行时和业务逻辑错误,导致整个进程退出。panic对于我们而言并不陌生,我们在现网遇到过几次,最早遭遇的不稳定性因素就是集群运行过程中panic了。
虽说我们3节点的etcd集群是可以容忍一个节点故障,但是crash瞬间对用户依然有影响,甚至出现集群拨测连接失败。
我们遇到的第一个crash bug,是发现集群链接数较多的时候有一定的概率出现crash, 然后根据堆栈查看社区已有人报grpc crash(issue)[4], 原因是etcd依赖的组件grpc-go出现了grpc crash(pr)[5],而最近我们遇到的crash bug[6]是v3.4.8/v3.3.21新版本发布引起的,这个版本跟我们有很大关系,**我们贡献了3个PR到这个版本,占了一大半以上, 那么这个crash bug是如何产生以及复现呢?**会不会是我们自己的锅呢?
虽然这个bug是社区用户反馈的,但从这个crash bug中我们获得了以下收获和最佳实践:
etcd面对一些大数据量的查询(expensive read)和写入操作时(expensive write),如全key遍历(full keyspace fetch)、大量event查询, list all Pod, configmap写入等会消耗大量的cpu、内存、带宽资源,极其容易导致过载,乃至雪崩。
然而,etcd目前只有一个极其简单的限速保护,当etcd的commited index大于applied index的阈值大于5000时,会拒绝一切请求,返回Too Many Request,其缺陷很明显,无法精确的对expensive read/write进行限速,无法有效防止集群过载不可用。
为了解决以上挑战,避免集群过载目前我们通过以下方案来保障集群稳定性:
**多维度的集群告警在我们的etcd稳定性保障中发挥了重要作用,多次帮助我们发现用户和我们自身集群组件问题。**用户问题如内部某K8s平台之前出现bug, 写入大量的集群CRD资源和client读写CRD QPS明显偏高。我们自身组件问题如某旧日志组件,当集群规模增大后,因日志组件不合理的频繁调用list Pod,导致etcd集群流量高达3Gbps, 同时apiserver本身也出现5XX错误。
虽然通过以上措施,我们能极大的减少因expensive read导致的稳定性问题,然而从线上实践效果看,目前我们仍然比较依赖集群告警帮助我们定位一些异常client调用行为,无法自动化的对异常client的进行精准智能限速,。etcd层因无法区分是哪个client调用,如果在etcd侧限速会误杀正常client的请求, 因此依赖apiserver精细化的限速功能实现。社区目前已在1.18中引入了一个API Priority and Fairness[7],目前是alpha版本,期待此特性早日稳定。
etcd读写性能决定着我们能支撑多大规模的集群、多少client并发调用,启动耗时决定着我们当重启一个节点或因落后leader太多,收到leader的快照重建时,它重新提供服务需要多久?性能优化案例剖析我们将从启动耗时减少一半、密码鉴权性能提升12倍、查询key数量性能提升3倍等来简单介绍下如何对etcd进行性能优化。
当db size达到4g时,key数量百万级别时,发现重启一个集群耗时竟然高达5分钟, key数量查询也是超时,调整超时时间后,发现高达21秒,内存暴涨6G。同时查询只返回有限的记录数的场景(如业务使用etcd grpc-proxy来减少watch数,etcd grpc proxy在默认创建watch的时候,会发起对watch路径的一次limit读查询),依然耗时很高且有巨大的内存开销。于是周末空闲的时候我对这几个问题进行了深入调查分析,启动耗时到底花在了哪里?是否有优化空间?查询key数量为何如何耗时,内存开销如此之大?
带着这些问题对源码进行了深入分析和定位,首先来看查询key数和查询只返回指定记录数的耗时和内存开销极大的问题,分析结论如下:
再看启动耗时问题过高的问题,通过对启动耗时各阶段增加日志,得到以下结论:
某内部业务服务一直跑的好好的,某天client略微增多后,突然现网etcd集群出现大量超时,各种折腾,切换云盘类型、切换部署环境、调整参数都不发挥作用,收到求助后,索要metrics和日志后,经过一番排查后,得到以下结论:
本文简单描述了我们在管理万级K8s集群和其他业务过程中遇到的etcd稳定性和性能挑战,以及我们是如何定位、分析、复现、解决这些挑战,并将解决方案贡献给社区。
同时,详细描述了我们从这些挑战中收获了哪些宝贵的经验和教训,并将之应用到后续的etcd稳定性保障中,以支持更大规模的单集群和总集群数。
最后我们面对万级K8s集群数, 千级的etcd集群数, 10几个版本分布,其中不少低版本包含重要的潜在可能触发的严重bug, 我们还需要投入大量工作不断优化我们的etcd平台,使其更智能、变更更加高效、安全、可控(如支持自动化、可控的集群升级等), 同时数据安全也至关重要,目前腾讯云TKE托管集群我们已经全面备份,独立集群的用户后续将引导通过应用市场的etcd备份插件开启定时备份到腾讯云对象存储COS上。
未来我们将继续紧密融入etcd的社区,为etcd社区的发展贡献我们的力量,与社区一块提升etcd的各个功能。
[1]v3.4.9: https://github.com/etcd-io/etcd/releases/tag/v3.4.9
[2]v3.3.22: https://github.com/etcd-io/etcd/releases/tag/v3.3.22
[3]K8s issue和pr: https://github.com/kubernetes/kubernetes/issues/91266
[4]grpc crash(issue): https://github.com/etcd-io/etcd/issues/9956
[5]grpc crash(pr): https://github.com/grpc/grpc-go/pull/2695
[6]crash bug : https://github.com/etcd-io/etcd/issues/11918
[7]API Priority and Fairness: https://github.com/kubernetes/enhancements/blob/master/keps/sig-api-machinery/20190228-priority-and-fairness.md