You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

245 lines
18 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 17 | 性能及稳定性如何优化及扩展etcd性能?
你好,我是唐聪。
我们继续来看如何优化及扩展etcd性能。上一节课里我为你重点讲述了如何提升读的性能今天我将重点为你介绍如何提升写性能和稳定性以及如何基于etcd gRPC Proxy扩展etcd性能。
当你使用etcd写入大量key-value数据的时候是否遇到过etcd server返回"etcdserver: too many requests"错误?这个错误是怎么产生的呢?我们又该如何来优化写性能呢?
这节课我将通过写性能分析链路图为你从上至下分析影响写性能、稳定性的若干因素并为你总结出若干etcd写性能优化和扩展方法。
## 性能分析链路
为什么你写入大量key-value数据的时候会遇到Too Many Request限速错误呢 是写流程中的哪些环节出现了瓶颈?
和读请求类似,我为你总结了一个开启鉴权场景的写性能瓶颈及稳定性分析链路图,并在每个核心步骤数字旁边标识了影响性能、稳定性的关键因素。
![](https://static001.geekbang.org/resource/image/14/0a/14ac1e7f1936f2def67b7fa24914070a.png)
下面我将按照这个写请求链路分析图和你深入分析影响etcd写性能的核心因素和最佳优化实践。
## db quota
首先是流程一。在etcd v3.4.9版本中client会通过clientv3库的Round-robin负载均衡算法从endpoint列表中轮询选择一个endpoint访问发起gRPC调用。
然后进入流程二。etcd收到gRPC写请求后首先经过的是Quota模块它会影响写请求的稳定性若db大小超过配额就无法写入。
![](https://static001.geekbang.org/resource/image/89/e8/89c9ccbf210861836cc3b5929b7ebae8.png)
etcd是个小型的元数据存储默认db quota大小是2G超过2G就只读无法写入。因此你需要根据你的业务场景适当调整db quota大小并配置的合适的压缩策略。
正如我在[11](https://time.geekbang.org/column/article/342891)里和你介绍的etcd支持按时间周期性压缩、按版本号压缩两种策略建议压缩策略不要配置得过于频繁。比如如果按时间周期压缩一般情况下5分钟以上压缩一次比较合适因为压缩过程中会加一系列锁和删除boltdb数据过于频繁的压缩会对性能有一定的影响。
一般情况下db大小尽量不要超过8G过大的db文件和数据量对集群稳定性各方面都会有一定的影响详细你可以参考[13](https://time.geekbang.org/column/article/343245)。
## 限速
通过流程二的Quota模块后请求就进入流程三KVServer模块。在KVServer模块里影响写性能的核心因素是限速。
![](https://static001.geekbang.org/resource/image/78/14/78062ff5b8c5863d8802bdfacf32yy14.png)
KVServer模块的写请求在提交到Raft模块前会进行限速判断。如果Raft模块已提交的日志索引committed index比已应用到状态机的日志索引applied index超过了5000那么它就返回一个"etcdserver: too many requests"错误给client。
那么哪些情况可能会导致committed Index远大于applied index呢?
首先是long expensive read request导致写阻塞。比如etcd 3.4版本之前长读事务会持有较长时间的buffer读锁而写事务又需要升级锁更新buffer因此出现写阻塞乃至超时。最终导致etcd server应用已提交的Raft日志命令到状态机缓慢。堆积过多时则会触发限速。
其次etcd定时批量将boltdb写事务提交的时候需要对B+ tree进行重平衡、分裂并将freelist、dirty page、meta page持久化到磁盘。此过程需要持有boltdb事务锁若磁盘随机写性能较差、瞬间大量写入则也容易写阻塞应用已提交的日志条目缓慢。
最后执行defrag等运维操作时也会导致写阻塞它们会持有相关锁导致写性能下降。
## 心跳及选举参数优化
写请求经过KVServer模块后则会提交到流程四的Raft模块。我们知道etcd写请求需要转发给Leader处理因此影响此模块性能和稳定性的核心因素之一是集群Leader的稳定性。
![](https://static001.geekbang.org/resource/image/66/2c/660a03c960cd56610e3c43e15c14182c.png)
那如何判断Leader的稳定性呢?
答案是日志和metrics。
一方面在你使用etcd过程中你很可能见过如下Leader发送心跳超时的警告日志你可以通过此日志判断集群是否有频繁切换Leader的风险。
另一方面你可以通过etcd\_server\_leader\_changes\_seen\_total metrics来观察已发生Leader切换的次数。
```
21:30:27 etcd3 | {"level":"warn","ts":"2021-02-23T21:30:27.255+0800","caller":"wal/wal.go:782","msg":"slow fdatasync","took":"3.259857956s","expected-duration":"1s"}
21:30:30 etcd3 | {"level":"warn","ts":"2021-02-23T21:30:30.396+0800","caller":"etcdserver/raft.go:390","msg":"leader failed to send out heartbeat on time; took too long, leader is overloaded likely from slow disk","to":"91bc3c398fb3c146","heartbeat-interval":"100ms","expected-duration":"200ms","exceeded-duration":"827.162111ms"}
```
那么哪些因素会导致此日志产生以及发生Leader切换呢?
首先我们知道etcd是基于Raft协议实现数据复制和高可用的各节点会选出一个Leader然后Leader将写请求同步给各个Follower节点。而Follower节点如何感知Leader异常发起选举正是依赖Leader的心跳机制。
在etcd中Leader节点会根据heartbeart-interval参数默认100ms定时向Follower节点发送心跳。如果两次发送心跳间隔超过2\*heartbeart-interval就会打印此警告日志。超过election timeout默认1000msFollower节点就会发起新一轮的Leader选举。
哪些原因会导致心跳超时呢?
一方面可能是你的磁盘IO比较慢。因为etcd从Raft的Ready结构获取到相关待提交日志条目后它需要将此消息写入到WAL日志中持久化。你可以通过观察etcd\_wal\_fsync\_durations\_seconds\_bucket指标来确定写WAL日志的延时。若延时较大你可以使用SSD硬盘解决。
另一方面也可能是CPU使用率过高和网络延时过大导致。CPU使用率较高可能导致发送心跳的goroutine出现饥饿。若etcd集群跨地域部署节点之间RTT延时大也可能会导致此问题。
最后我们应该如何调整心跳相关参数以避免频繁Leader选举呢
etcd默认心跳间隔是100ms较小的心跳间隔会导致发送频繁的消息消耗CPU和网络资源。而较大的心跳间隔又会导致检测到Leader故障不可用耗时过长影响业务可用性。一般情况下为了避免频繁Leader切换建议你可以根据实际部署环境、业务场景将心跳间隔时间调整到100ms到400ms左右选举超时时间要求至少是心跳间隔的10倍。
## 网络和磁盘IO延时
当集群Leader稳定后就可以进入Raft日志同步流程。
我们假设收到写请求的节点就是Leader写请求通过Propose接口提交到Raft模块后Raft模块会输出一系列消息。
etcd server的raftNode goroutine通过Raft模块的输出接口Ready获取到待发送给Follower的日志条目追加消息和待持久化的日志条目。
raftNode goroutine首先通过HTTP协议将日志条目追加消息广播给各个Follower节点也就是流程五。
![](https://static001.geekbang.org/resource/image/8d/eb/8dd9d414eb4ef3ba9a7603fayy991aeb.png)
流程五涉及到各个节点之间网络通信因此节点之间RTT延时对其性能有较大影响。跨可用区、跨地域部署时性能会出现一定程度下降建议你结合实际网络环境使用benchmark工具测试一下。etcd Raft网络模块在实现上也会通过流式发送和pipeline等技术优化来降低延时、提高网络性能。
同时raftNode goroutine也会将待持久化的日志条目追加到WAL中它可以防止进程crash后数据丢失也就是流程六。注意此过程需要同步等待数据落地因此磁盘顺序写性能决定着性能优异。
为了提升写吞吐量etcd会将一批日志条目批量持久化到磁盘。etcd是个对磁盘IO延时非常敏感的服务如果服务对性能、稳定性有较大要求建议你使用SSD盘。
那使用SSD盘的etcd集群和非SSD盘的etcd集群写性能差异有多大呢
下面是SSD盘集群执行如下benchmark命令的压测结果写QPS 51298平均延时189ms。
```
benchmark --endpoints=addr --conns=100 --clients=1000 \
put --key-size=8 --sequential-keys --total=10000000 --
val-size=256
```
![](https://static001.geekbang.org/resource/image/91/14/913e9875ef32df415426a3e5e7cff814.png)
下面是非SSD盘集群执行同样benchmark命令的压测结果写QPS 35255平均延时279ms。
![](https://static001.geekbang.org/resource/image/17/2f/1758a57804be463228e6431a388c552f.png)
## 快照参数优化
在Raft模块中正常情况下Leader可快速地将我们的key-value写请求同步给其他Follower节点。但是某Follower节点若数据落后太多Leader内存中的Raft日志已经被compact了那么Leader只能发送一个快照给Follower节点重建恢复。
在快照较大的时候发送快照可能会消耗大量的CPU、Memory、网络资源那么它就会影响我们的读写性能也就是我们图中的流程七。
![](https://static001.geekbang.org/resource/image/1a/38/1ab7a084e61d84f44b893a0fbbdc0138.png)
一方面, etcd Raft模块引入了流控机制来解决日志同步过程中可能出现的大量资源开销、导致集群不稳定的问题。
另一方面我们可以通过快照参数优化去降低Follower节点通过Leader快照重建的概率使其尽量能通过增量的日志同步保持集群的一致性。
etcd提供了一个名为--snapshot-count的参数来控制快照行为。它是指收到多少个写请求后就触发生成一次快照并对Raft日志条目进行压缩。为了帮助slower Follower赶上Leader进度etcd在生成快照压缩日志条目的时候也会至少保留5000条日志条目在内存中。
那snapshot-count参数设置多少合适呢?
snapshot-count值过大它会消耗较多内存你可以参考15内存篇中Raft日志内存占用分析。过小则的话在某节点数据落后时如果它请求同步的日志条目Leader已经压缩了此时我们就不得不将整个db文件发送给落后节点然后进行快照重建。
快照重建是极其昂贵的操作对服务质量有较大影响因此我们需要尽量避免快照重建。etcd 3.2版本之前snapshot-count参数值是1万比较低短时间内大量写入就较容易触发慢的Follower节点快照重建流程。etcd 3.2版本后将其默认值调大到10万老版本升级的时候你需要注意配置文件是否写死固定的参数值。
## 大value
当写请求对应的日志条目被集群多数节点确认后就可以提交到状态机执行了。etcd的raftNode goroutine就可通过Raft模块的输出接口Ready获取到已提交的日志条目然后提交到Apply模块的FIFO待执行队列。因为它是串行应用执行命令任意请求在应用到状态机时阻塞都会导致写性能下降。
当Raft日志条目命令从FIFO队列取出执行后它会首先通过授权模块校验是否有权限执行对应的写操作对应图中的流程八。影响其性能因素是RBAC规则数和锁。
![](https://static001.geekbang.org/resource/image/53/f6/5303f1b003480d2ddfe7dbd56b05b3f6.png)
然后通过权限检查后写事务则会从treeIndex模块中查找key、更新的key版本号等信息对应图中的流程九影响其性能因素是key数和锁。
更新完索引后我们就可以把新版本号作为boltdb key 把用户key/value、版本号等信息组合成一个value写入到boltdb对应图中的流程十影响其性能因素是大value、锁。
如果你在应用中保存1Mb的value这会给etcd稳定性带来哪些风险呢
首先会导致读性能大幅下降、内存突增、网络带宽资源出现瓶颈等上节课我已和你分享过一个1MB的key-value读性能压测结果QPS从17万骤降到1100多。
那么写性能具体会下降到多少呢?
通过benchmark执行如下命令写入1MB的数据时候集群几乎不可用三节点8核16G非SSD盘事务提交P99延时高达4秒如下图所示。
```
benchmark --endpoints=addr --conns=100 --clients=1000 \
put --key-size=8 --sequential-keys --total=500 --val-
size=1024000
```
![](https://static001.geekbang.org/resource/image/0c/bb/0c2635d617245f5d4084fbe48820e4bb.png)
因此只能将写入的key-value大小调整为100KB。执行后得到如下结果写入QPS 仅为1119/S平均延时高达324ms。
![](https://static001.geekbang.org/resource/image/a7/63/a745af37d76208c08be147ac46018463.png)
其次etcd底层使用的boltdb存储它是个基于COW(Copy-on-write)机制实现的嵌入式key-value数据库。较大的value频繁更新因为boltdb的COW机制会导致boltdb大小不断膨胀很容易超过默认db quota值导致无法写入。
那如何优化呢?
首先如果业务已经使用了大key拆分、改造存在一定客观的困难那我们就从问题的根源之一的写入对症下药尽量不要频繁更新大key这样etcd db大小就不会快速膨胀。
你可以从业务场景考虑判断频繁的更新是否合理能否做到增量更新。之前遇到一个case 一个业务定时更新大量key导致被限速最后业务通过增量更新解决了问题。
如果写请求降低不了, 就必须进行精简、拆分你的数据结构了。将你需要频繁更新的数据拆分成小key进行更新等实现将value值控制在合理范围以内才能让你的集群跑的更稳、更高效。
Kubernetes的Node心跳机制优化就是这块一个非常优秀的实践。早期kubelet会每隔10s上报心跳更新Node资源。但是此资源对象较大导致db大小不断膨胀无法支撑更大规模的集群。为了解决这个问题社区做了数据拆分将经常变更的数据拆分成非常细粒度的对象实现了集群稳定性提升支撑住更大规模的Kubernetes集群。
## boltdb锁
了解完大value对集群性能的影响后我们再看影响流程十的另外一个核心因素boltdb锁。
首先我们回顾下etcd读写性能优化历史它经历了以下流程
* 3.0基于Raft log read实现线性读线性读需要经过磁盘IO性能较差
* 3.1基于ReadIndex实现线性读每个节点只需要向Leader发送ReadIndex请求不涉及磁盘IO提升了线性读性能
* 3.2将访问boltdb的锁从互斥锁优化到读写锁提升了并发读的性能
* 3.4实现全并发读去掉了buffer锁长尾读几乎不再影响写。
并发读特性的核心原理是创建读事务对象时它会全量拷贝当前写事务未提交的buffer数据并发的读写事务不再阻塞在一个buffer资源锁上实现了全并发读。
最重要的是写事务也不再因为expensive read request长时间阻塞有效的降低了写请求的延时详细测试结果你可以参考[并发读特性实现PR](https://github.com/etcd-io/etcd/pull/10523),因篇幅关系就不再详细描述。
## 扩展性能
当然有不少业务场景你即便用最高配的硬件配置etcd可能还是无法解决你所面临的性能问题。etcd社区也考虑到此问题提供了一个名为[gRPC proxy](https://etcd.io/docs/v3.4.0/op-guide/grpc_proxy/)的组件帮助你扩展读、扩展watch、扩展Lease性能的机制如下图所示。
![](https://static001.geekbang.org/resource/image/4a/b1/4a13ec9a4f93931e6e0656c600c2d3b1.png)
### 扩展读
如果你的client比较多etcd集群节点连接数大于2万或者你想平行扩展串行读的性能那么gRPC proxy就是良好一个解决方案。它是个无状态节点为你提供高性能的读缓存的能力。你可以根据业务场景需要水平扩容若干节点同时通过连接复用降低服务端连接数、负载。
它也提供了故障探测和自动切换能力当后端etcd某节点失效后会自动切换到其他正常节点业务client可对此无感知。
### 扩展Watch
大量的watcher会显著增大etcd server的负载导致读写性能下降。etcd为了解决这个问题gRPC proxy组件里面提供了watcher合并的能力。如果多个client Watch同key或者范围如上图三个client Watch同key它会尝试将你的watcher进行合并降低服务端的watcher数。
然后当它收到etcd变更消息时会根据每个client实际Watch的版本号将增量的数据变更版本分发给你的多个client实现watch性能扩展及提升。
### 扩展Lease
我们知道etcd Lease特性提供了一种客户端活性检测机制。为了确保你的key不被淘汰client需要定时发送keepalive心跳给server。当Lease非常多时这就会导致etcd服务端的负载增加。在这种场景下gRPC proxy提供了keepalive心跳连接合并的机制来降低服务端负载。
## 小结
今天我通过从上至下的写请求流程分析介绍了各个流程中可能存在的瓶颈和优化方法、最佳实践。最后我从分层的角度为你总结了一幅优化思路全景图你可以参考一下下面这张图它将我们这两节课讨论的etcd性能优化、扩展问题分为了以下几类
* 业务应用层etcd应用层的最佳实践
* etcd内核层etcd参数最佳实践
* 操作系统层,操作系统优化事项;
* 硬件及网络层不同的硬件设备对etcd性能有着非常大的影响
* 扩展性能基于gRPC proxy扩展读、Watch、Lease的性能。
希望你通过这节课的学习以后在遇到etcd性能问题时能分别从请求执行链路和分层的视角去分析、优化瓶颈让业务和etcd跑得更稳、更快。
![](https://static001.geekbang.org/resource/image/92/87/928a4f1e66200531f5ee73aab000ce87.png)
## 思考题
最后,我还给你留了一个思考题。
watcher较多的情况下会不会对读写请求性能有影响呢如果会是在什么场景呢gRPC proxy能安全的解决watcher较多场景下的扩展性问题吗
欢迎分享你的性能优化经历,感谢你阅读,也欢迎你把这篇文章分享给更多的朋友一起阅读。