前言
etcd
是一个在Docker相关场景中使用非常流行的服务发现组件,在分布式的场景中,服务发现组件起着重要的作用,通常服务发现服务会扮演如下几种不同的角色。
- 服务发现
在分布式系统的中,以松耦合的方式提供不同组件之间的访问、配置等信息。在某些微服务框架中例如springcloud也可以用来封装进行负载均衡的软实现。 - 分布式锁
在分布式系统中,锁的实现需要依赖中间件进行,而服务发现系统最大的特点在于对小规模、热数据的处理,读写高频。在某些系统中需要有主从节点的场景中,可以借助分布式锁的方式,快速高效的实现。 - 消息订阅
在分布式系统中,系统的水平伸缩依赖于状态的剥离,而状态的剥离是基于数据的分布式共享,通常情况下数据会通过中间件的方式进行存储、传输,,例如:缓存类(例如:redis)、存储类(例如:mysql)、消息类(例如:rabbitmq)的方式存在。但是在某些细分场景中,这些类型的中间件存在他的弊端。例如在分布式配置订阅分发的场景中,服务发现组件会以更简单、更高效的方式替代消息队列完成这个任务。
在Docker相关的场景中,etcd主要会扮演集群的中心存储、配置中心、消息的中转中心。在过去的两年中,通过对etcd的使用、运维,今天希望通过这篇文章分享给大家一些心得。
etcd与其他服务发现系统
服务发现组件中,大名鼎鼎的还有Zookeeper和Consul,为什么在Docker相关的领域大家更倾向于使用etcd而不是后两者。虽然这些CoreOS
这个亲爸有些关系,但是从面向的场景和使用的方式上还是有所差别的。
- Zookeeper
zk是三个之中最悠久的,也具备丰富的特性。但是他的缺点在于是由Java进行编写的,架构复杂性高、性能相比之下较低、SDK的使用方式过于繁琐。 - Consul
Consul 和 etcd类似都是 Go 编写的,具备完成的使用工具链和场景,从功能上来讲,可以说是完整的覆盖了etcd,etcd有的基本上Consul都有,而Consul有的etcd需要通过三方的工具来实现。看上去这是一个完美的替代者,但是时至今日,Consul基本上只会作为etcd的替代方案存在。因为在目前大家使用的服务发现组件的场景已经远远跨越了他的本身能力,在大规模集群中,性能的要求极其苛刻,而这正是Consul不如etcd的地方 - etcd
说到今天的主角,最早开始接触etcd是从0.4的版本开始,那个时候Kubernetes的版本还是0.6的版本,现在Kubernetes已经是1.9的版本,而etcd也升级到了3.2的版本。从etcd v2.3版本开始提供grpc的v3 api,但是同时也可以使用tcp的v2 api,但是v2 api与 v3 api在存储模型、接口设计等等方面是完全不同的。在v2 api中,存储的结构是基于常见的文档存储是基于路径的,而在v3 api中是完全散列Hash的键值对存储。v3的api提供更强大的功能,包括更多的watcher、租约的键值对、更简洁的相应、平键空间等等。
从上面的对比可以阐述,etcd的优势在于性能以及使用的方便。和所有的服务发现系统一样,etcd倾向高频读写小数据的场景。etcd的存储分为wal和snap,snap存储数据某一时刻的快照,而wal存储预写的日志,并通过定期的fsync写到磁盘,因此所有的数据都会缓存到内存中,因此对于如下的场景不适用etcd
- 无限增长的时序key的场景
例如一些递增的场景,比如存储发布的历史等等,这些数据存在时序性,且大部分数据需要归档,这种数据会大量占用etcd的内存与磁盘的存储空间以及IO的写。 - 需要频繁修改存储空间比较大key的场景
数据比较多的大Key本身并不是错,比如存储一个应用的配置,这些都是合理的。但是当你需要频发修改、查询这个大Key的时候,会对etcd造成巨大的压力。当写一个大key的时候,很多时候我们没法一次写到etcd的主节点,对于一个三个节点的集群,我们有三分之二的概率写到从节点上去,而从节点会自动将预写的数据转发给主节点,主节点预写完wal后,会同步给未收到的从节点。直到集群中大多数节点回复预写成功后集群才会认为写成功,因此会造成大key的数据在主节点与从节点中多次传输,会占用大量的同步带宽。 - 过度监听的场景
在v2 api中,我们的存储结构是分路径的,那么对于某些场景,我们需要遍历去侦听一个路径下的所有key,虽然这个行为表面上不会有过大的影响,但是v2 api的返回体会将遍历所有的节点与信息返回回去,这会造成返回体异常巨大,而且当你的watcher在集群中的时候,会有一种无形的放大效应,导致etcd的出网流量暴增。 - 乐观锁的不当使用
这个问题的场景来自Docker Overlay网络对于etcd使用的上,在Overlay网络中IPAM是需要依赖etcd来实现的,每次分配容器会根据对特定key更新mask值来进行分配,而Overlay中的默认实现是乐观锁的实现,这导致当存在一个超过50节点以上的集群进行大量Overlay网络容器调度的时候,会对mask的key存在大量查询与重读的过程,而且节点的数目越大造成的产讯次数越多,且当要修改的Key的大小越大,整体的放大效应越大。
如何运维etcd
升级
- 版本升级
场景的场景是etcd 2.* 升级到 etcd 3.*,v2 api产生的数据不能直接转换为 v3 api的数据,但是某些三方工具可以实现这个能力,不过这个动作基本上和全量查询转换没有什么差别,有极大的风险,非常不建议大家从v2 api升级到v3 api。如果期望使用v3 api的能力,可以考虑老数据依然使用v2 api,而新数据可以使用v3 api。单纯从etcd2.3的版本升级到etcd3.0的版本无需变更任何配置,更新binary包,直接依次重启etcd即可,但是值得注意的是,在etcd2.3升级到etcd3.0的过程中,会涉及一个存储结构的兼容,etcd3.0以上的版本为了支持v3 api会改变原来存储空间的数据结果,此时会进行一次数据转换,而数据转换的过程根据数据大小而变化。通常会在3分钟内完成,升级过程中是有可能出现异常的,因为此时集群中共存2.3的版本和3.0的版本,虽然表面上集群不会有错误报出,但是有可能会出现不同节点同步数据的modifyIndex无法追赶的问题。 每个etcd的key都一个modifyIndex,这个modifyIndex会随着写操作的变更而增加,通常etcd的集群v2的版本会默认缓存最后1000次变更,当次期间的变更超过1000时,会根据wal的相关写日志进行追写,但是在v2数据转换的过程中时间较长,会有部分的可能导致集群中某些几点的ModifyIndex永远无法更新。升级后可以通过如下方式进行检查 (假设集群为三个节点的集群)
curl http://etcd1:2379/v2/keys/some_key_not_exists
curl http://etcd2:2379/v2/keys/some_key_not_exists
curl http://etcd3:2379/v2/keys/some_key_not_exists
此时etcd的server会返回一个不存在的提示,并将当前系统的modifyIndex通过结果反馈回来,如果发现三个节点的modifyIndex有比较大差异,说明与master不同的节点已经损坏,切这种损坏是无法通过wal追写恢复的。好在此时etcd的集群的选主是没有问题的,只需要将-initial-cluster-state设置为existing,并删除当前节点的数据文件,并重启etcd即可恢复。
- 配置升级
配置升级通常是CPU、内存或者磁盘等,而通常这种升级是需要重启机器的,此时需要特别注意的是,etcd的集群是非常稳定的,通常情况下etcd不会给你带来任何麻烦,除非集群中大多数节点变得不健康,因此需要特别注意的是,升级一定要一台一台的进行,且进行升级的过程需要将-initial-cluster-state的状态改为existing,否在如果在系统刚起来且因为网络或者其他原因导致无法同步集群其他节点信息的时候,会使得etcd认为自己需要重新命名自己在集群中的角色,从而此时集群会变成四个节点,主节点和未重启的节点会认为升配的节点没有起来,而升配的节点用新的角色发起投票,该投票会被前两节点直接拒绝,也就是此时这个节点再也无法加入集群。若再升级另外一个从节点,则会立马造成集群失去大多数节点,此时集群已不健康,无法通过api进行管理,必须手动重建,这种情况是所有的etcd故障中最危险的一种。
此外还有一种场景,也非常容易出现问题,如果第一次重启的主节点会造成什么现象呢?如果重启的是主节点,两个从节点会在heat beat timeout的次数超过阈值的时候,进行重新选举,如果选举的过程中有任何一个宕机或者升级,此时集群依然会变成无主的状态,只能手动重建。那么如何升级的方式是最稳妥的?首先,先找到etcd集群中的一个从节点,将从节点的配置-initial-cluster-state的状态改为existing,并重启机器与etcd,等待etcd的日志中开始稳定snapshot以及预写wal的时候,例如出现如下信息的时候,且etcdctl的cluster-health状态正常,则表示升级成功
2018-01-21 17:30:58.715153 I | etcdserver: saved snapshot at index 28420730677
2018-01-21 17:30:58.715428 I | etcdserver: compacted raft log at 28420725677
接下来依次升级另外一个从节点和主节点即可。
- 迁移升级
在云平台上,过保迁移是比较常见的一种现象,有的时候迁移不能保证机器的ip不变,特别是对外暴漏的ip不变,通常我们会将etcd的advertise地址配置在使用etcd的地方,这就带来一个问题,到底是否可以修改暴漏的ip呢,答案是可以的。通常我们会在每个etcd前面挂一个负载均衡器,来进行机器ip与advertise ip的解耦,那么怎么让advertise ip和etcd client解耦呢,常见的方式是通过域名的方式进行解耦,但是对于Docker而言,这里有一个Bug会带来巨大的危害,在Docker内部是使用libkv来使用etcd的,libkv中尝试定期的AutoSync etcd的集群信息来保证集群信息的变更带来的连接更新,但是在此处有一个Bug,一旦etcd某些AutoSync失败则系统将无法再次sync etcd集群信息,除非重启Docker Engine,Bug信息如下,有兴趣的同学可以参考下 https://github.com/docker/libkv/pull/193。
因此迁移升级如果涉及advertise ip的变化,那么需要重点注意下Docker Engine的连接状态。否则有可能会对业务造成宕机的影响。
常见故障
- 少量节点故障
报错的信息为“etcd cluster is unavailable or misconfigured”,这个错误是所有etcd错误中最常见的,这实际上是一个非常宽泛的错误,如果使用的是golang的etcd client,这个错误表明etcd的server返回了一个非200的请求,因此看到这个错误的时候不要过于担心是否是etcd集群有宕机。首先需要检查的是,先检查查询key的状态是否正常,然后再检查集群的健康状态。
etcd的高可用容许少于一半个节点宕机,当宕机的是少量节点的时候,如果节点网络、存储等基础物理资源不存在问题,那么只需要修改-initial-cluster-state的状态改为existing,并重启etcd即可。但是这种可能性是非常小的,因为通常情况下etcd的部署方式为容器化部署或者supervisord进行进程管理,因此对于etcd异常退出的错误,通常情况下会自动拉起来。
少量节点失败的可能性还有一种,是节点失联,失联后身份变更,导致其他的节点拒绝拥有同样advertise ip的节点。此时只需要将当前节点的-initial-cluster-state的状态改为existing,并删除数据文件,然后再重启etcd即可恢复。
- 多数节点故障
多数节点故障的现象是集群已经unhealthy,并且在日志中出现大量选举失败的日志,此时需要纯手工重建,重建的方式如下:1)选择一个节点作为主节点,并停止其他的从节点,如果不知道那个节点的数据是最新,可以在集群中选择一个modifyIndex最大的节点。 2)将这个节点启动状态设置为-force-new-cluster,并将配置中其他peer的信息进行删除,以单节点的方式启动。 3)通过etcdctl或者api,添加一个member,此时修改这个member的配置信息,修改-initial-cluster-state的状态改为existing,设置peer的信息为当前集群中的两个节点,备份并移走原来的数据目录,重启etcd,让当前节点强制以老节点的方式加入到集群中。4)重复第三步,此时peer信息为集群中的三个节点信息,加入当前节点到集群。5)等待集群稳定并开始snapshot,修改第一个节点和第二个节点的启动参数,-initial-cluster-state的状态改为existing,peer信息补全为三个节点的信息,依次重启节点,集群恢复。 - 应用级故障
通常应用级故障是etcd client的TIME_OUT,而这一部分可以通过性能调优来解决。具体方式请看下一部分。 - NTP故障
时序问题会带来etcd同步数据失败,对外表现为写操作失败,是因为Raft协议同步强依赖宿主机的时序,因此需要每台宿主机能够校对同一个ntp源,可以通过安装ntp服务或者crontab ntpdate的方式来解决时序带来的问题。
如何诊断调优
etcd的性能非常强悍,通常4C8G的机器在不使用https的情况下,可以支持上万的QPS,而这一切的前提是使用的正确与恰当。另外所有的性能调优在还不了解系统参数具体行为的时候,不建议大家调整系统参数;不建议大家调整系统参数;不建议大家调整系统参数,重要的话说三遍。一个不当的系统参数调整表面上看缓解了某些症状,但是带来的隐患可能远远超乎你的想象,有的时候在调优山重水复疑无路的时候,查看系统参数的变更可能会让你恍然大悟,问题的根源所在。
- CPU、内存调优
通常etcd的瓶颈不会出现在内存上,因为存取的数据量级比较小的时候,内存的用量会在非常小的谅解,通常这个量级会是snapshot与wal目录存储空间的2倍之内。当一个etcd的存储目录超过1G,你就要开始思考,是否是自己使用etcd的方式不对,或者是否需要集群扩容了。如果内存超过2G的用量,此时需要思考下是否集群中存在大Key频繁读写的场景,因为在etcd的client中,默认使用3s作为client的超时,而在etcd server的超时时间远比这个时间长,所有有可能出现在查询大key的场景下,客户端主动断连,单服务器端直到数据查询完发现socket已经关闭,打印broken pipe。如果客户端因为超时进行重试,这个位置会出现更大的放大效应。
etcd的CPU瓶颈通常出现在解析Key的数据结构和https上,而这两项通常情况下可以减少的部分很少,如果使用strace进行查看,你会发现大部分的CPU时间都是在解析数据以及写盘。因此遇到CPU的瓶颈,建议直接升级配置。
- 磁盘IO调优
etcd的存储目录分为snapshot和wal,他们写入的方式是不同的,snapshot是内存直接dump file。而wal是顺序追加写,对于这两种方式系统调优的方式是不同的,snapshot可以通过增加io平滑写来提高磁盘io能力,而wal可以通过降低pagecache的方式提前写入时序。因此对于不同的场景,可以考虑将snap与wal进行分盘,放在两块SSD盘上,提高整体的IO效率,这种方式可以提升etcd 20%左右的性能 - 网路调优
etcd中比较复杂的是网络的调优,因此大量的网络请求会在peer之间转发,而且整体网络吞吐也很大,但是还是再次强调不建议大家调整系统参数,大家可以通过修改etcd的--heartbeat-interval与--election-timeout启动参数来适当提高高吞吐网络下etcd的集群鲁棒性,通常同步吞吐在100MB左右的集群可以考虑将--heartbeat-interval设置为300ms-500ms,--election-timeout可以设置在5000ms左右。此外官方还有基于TC的网络优先传输方案,也是一个比较适用的调优手段。
tc qdisc add dev eth0 root handle 1: prio bands 3
tc filter add dev eth0 parent 1: protocol ip prio 1 u32 match ip sport 2380 0xffff flowid 1:1
tc filter add dev eth0 parent 1: protocol ip prio 1 u32 match ip dport 2380 0xffff flowid 1:1
tc filter add dev eth0 parent 1: protocol ip prio 2 u32 match ip sport 2739 0xffff flowid 1:1
tc filter add dev eth0 parent 1: protocol ip prio 2 u32 match ip dport 2739 0xffff flowid 1:1
写在最后
安利一个小工具lucas,可以协助你快速掌握Kubernetes中的etcd存储结构,项目仓库:https://github.com/ringtail/lucas。