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.

264 lines
20 KiB
Markdown

2 years ago
# 16 | 性能及稳定性如何优化及扩展etcd性能
你好,我是唐聪。
在使用etcd的过程中你是否吐槽过etcd性能差呢 我们知道etcd社区线性读[压测结果](https://etcd.io/docs/v3.4.0/op-guide/performance/)可以达到14w/s那为什么在实际业务场景中有时却只有几千甚至几百、几十还会偶发超时、频繁抖动呢
我相信不少人都遇到过类似的问题。要解决这些问题不仅需要了解症结所在还需要掌握优化和扩展etcd性能的方法对症下药。因为这部分内容比较多所以我分成了两讲内容分别从读性能、写性能和稳定性入手为你详细讲解如何优化及扩展etcd性能及稳定性。
希望通过这两节课的学习能让你在使用etcd的时候设计出良好的业务存储结构遵循最佳实践让etcd稳定、高效地运行获得符合预期的性能。同时当你面对etcd性能瓶颈的时候也能自己分析瓶颈原因、选择合适的优化方案解决它而不是盲目甩锅etcd甚至更换技术方案去etcd化。
今天这节课,我将重点为你介绍如何提升读的性能。
我们说读性能差其实本质是读请求链路中某些环节出现了瓶颈。所以接下来我将通过一张读性能分析链路图为你从上至下分析影响etcd性能、稳定性的若干因素并给出相应的压测数据最终为你总结出一系列的etcd性能优化和扩展方法。
## 性能分析链路
为什么在你的业务场景中读性能不如预期呢? 是读流程中的哪一个环节出现了瓶颈?
在下图中,我为你总结了一个开启密码鉴权场景的读性能瓶颈分析链路图,并在每个核心步骤数字旁边,标出了影响性能的关键因素。我之所以选用密码鉴权的读请求为案例,是因为它使用较广泛并且请求链路覆盖最全,同时它也是最容易遇到性能瓶颈的场景。
![](https://static001.geekbang.org/resource/image/7f/52/7f8c66ded3e151123b18768b880a2152.png)
接下来我将按照这张链路分析图,带你深入分析一个使用密码鉴权的线性读请求,和你一起看看影响它性能表现的核心因素以及最佳优化实践。
## 负载均衡
首先是流程一负载均衡。在[02节](https://time.geekbang.org/column/article/335932)时我和你提到过在etcd 3.4以前client为了节省与server节点的连接数clientv3负载均衡器最终只会选择一个sever节点IP与其建立一个长连接。
但是这可能会导致对应的server节点过载如单节点流量过大出现丢包 其他节点却是低负载最终导致业务无法获得集群的最佳性能。在etcd 3.4后引入了Round-robin负载均衡算法它通过轮询的方式依次从endpoint列表中选择一个endpoint访问(长连接)使server节点负载尽量均衡。
所以如果你使用的是etcd低版本那么我建议你通过Load Balancer访问后端etcd集群。因为一方面Load Balancer一般支持配置各种负载均衡算法如连接数、Round-robin等可以使你的集群负载更加均衡规避etcd client早期的固定连接缺陷获得集群最佳性能。
另一方面当你集群节点需要替换、扩缩容集群节点的时候你不需要去调整各个client访问server的节点配置。
## 选择合适的鉴权
client通过负载均衡算法为请求选择好etcd server节点后client就可调用server的Range RPC方法把请求发送给etcd server。在此过程中如果server启用了鉴权那么就会返回无权限相关错误给client。
如果server使用的是密码鉴权你在创建client时需指定用户名和密码。etcd clientv3库发现用户名、密码非空就会先校验用户名和密码是否正确。
client是如何向sever请求校验用户名、密码正确性的呢
client是通过向server发送Authenticate RPC鉴权请求实现密码认证的也就是图中的流程二。
![](https://static001.geekbang.org/resource/image/9e/61/9e1fb86567b351641db9586081c0e361.png)
根据我们[05](https://time.geekbang.org/column/article/338524)介绍的密码认证原理server节点收到鉴权请求后它会从boltdb获取此用户密码对应的算法版本、salt、cost值并基于用户的请求明文密码计算出一个hash值。
在得到hash值后就可以对比db里保存的hash密码是否与其一致了。如果一致就会返回一个token给client。 这个token是client访问server节点的通行证后续server只需要校验“通行证”是否有效即可无需每次发起昂贵的Authenticate RPC请求。
讲到这里不知道你有没有意识到若你的业务在访问etcd过程中未复用token每次访问etcd都发起一次Authenticate调用这将是一个非常大的性能瓶颈和隐患。因为正如我们05所介绍的为了保证密码的安全性密码认证Authenticate的开销非常昂贵涉及到大量CPU资源。
那这个Authenticate接口究竟有多慢呢
为了得到Authenticate接口的性能我们做过这样一个测试
* 压测集群etcd节点配置是16核32G
* 压测方式是我们通过修改etcd clientv3库、benchmark工具使benchmark工具支持Authenticate接口压测
* 然后设置不同的client和connection参数运行多次观察结果是否稳定获取测试结果。
最终的测试结果非常惊人。etcd v3.4.9之前的版本Authenticate接口性能不到16 QPS并且随着client和connection增多该性能会继续恶化。
当client和connection的数量达到200个的时候性能会下降到8 QPSP99延时为18秒如下图所示。
![](https://static001.geekbang.org/resource/image/bc/c4/bc6336b93de53e6650bd7a5565ef8ec4.png)
对此,我和小伙伴王超凡通过一个[减少锁的范围PR](https://github.com/etcd-io/etcd/pull/11735)该PR已经cherry-pick到了etcd 3.4.9版本将性能优化到了约200 QPS并且P99延时在1秒内如下图所示。
由于导致Authenticate接口性能差的核心瓶颈是在于密码鉴权使用了bcrpt计算hash值因此Authenticate性能已接近极限。
![](https://static001.geekbang.org/resource/image/44/aa/449bb47bef89a7cf1d2fbb1205a15faa.png)
最令人头疼的是Auenticate的调用由clientv3库默默发起的etcd中也没有任何日志记录其耗时等。当大家开启密码鉴权后遇到读写接口超时的时候未详细了解etcd的同学就会非常困惑很难定位超时本质原因。
我曾多次收到小伙伴的求助协助他们排查etcd异常超时问题。通过metrics定位我发现这些问题大都是由比较频繁的Authenticate调用导致只要临时关闭鉴权或升级到etcd v3.4.9版本就可以恢复。
为了帮助大家快速发现Authenticate等特殊类型的expensive request我在etcd 3.5版本中提交了一个PR通过gRPC拦截器的机制当一个请求超过300ms时就会打印整个请求信息。
讲到这里,你应该会有疑问,密码鉴权的性能如此差,可是业务又需要使用它,我们该怎么解决密码鉴权的性能问题呢?对此,我有三点建议。
第一如果你的生产环境需要开启鉴权并且读写QPS较大那我建议你不要图省事使用密码鉴权。最好使用证书鉴权这样能完美避坑认证性能差、token过期等问题性能几乎无损失。
第二确保你的业务每次发起请求时有复用token机制尽可能减少Authenticate RPC调用。
第三如果你使用密码鉴权时遇到性能瓶颈问题可将etcd升级到3.4.9及以上版本,能适当提升密码鉴权的性能。
## 选择合适的读模式
client通过server的鉴权后就可以发起读请求调用了也就是我们图中的流程三。
![](https://static001.geekbang.org/resource/image/58/9a/5832f5da0f916b941b1d832e9fe2e29a.png)
在这个步骤中读模式对性能有着至关重要的影响。我们前面讲过etcd提供了串行读和线性读两种读模式。前者因为不经过ReadIndex模块具有低延时、高吞吐量的特点而后者在牺牲一点延时和吞吐量的基础上实现了数据的强一致性读。这两种读模式分别为不同场景的读提供了解决方案。
关于串行读和线性读的性能对比,下图我给出了一个测试结果,测试环境如下:
* 机器配置client 16核32G三个server节点8核16G、SSD盘client与server节点都在同可用区
* 各节点之间RTT在0.1ms到0.2ms之间;
* etcd v3.4.9版本;
* 1000个client。
执行如下串行读压测命令:
```
benchmark --endpoints=addr --conns=100 --clients=1000 \
range hello --consistency=s --total=500000
```
得到串行读压测结果如下32万 QPS平均延时2.5ms。
![](https://static001.geekbang.org/resource/image/3d/9a/3d18aafb016a93e8d2f07a4193cb6b9a.png)
执行如下线性读压测命令:
```
benchmark --endpoints=addr --conns=100 --clients=1000 \
range hello --consistency=l --total=500000
```
得到线性读压测结果如下19万 QPS平均延时4.9ms。
![](https://static001.geekbang.org/resource/image/83/0d/831338d142bc44999cc6c3b04147yy0d.png)
从两个压测结果图中你可以看到在100个连接时串行读性能比线性读性能高近11万/s串行读请求延时2.5ms比线性读延时约低一半4.9ms)。
**需要注意的是以上读性能数据是在1个key、没有任何写请求、同可用区的场景下压测出来的实际的读性能会随着你的写请求增多而出现显著下降这也是实际业务场景性能与社区压测结果存在非常大差距的原因之一。**所以我建议你使用etcd benchmark工具在你的etcd集群环境中自测一下你也可以参考下面的[etcd社区压测结果](https://etcd.io/docs/v3.4.0/op-guide/performance/)。
![](https://static001.geekbang.org/resource/image/58/ca/58135ebf14a25e3f74004929369867ca.png)
如果你的业务场景读QPS较大但是你又不想通过etcd proxy等机制来扩展性能那你可以进一步评估业务场景对数据一致性的要求高不高。如果你可以容忍短暂的不一致那你可以通过串行读来提升etcd的读性能也可以部署Learner节点给可能会产生expensive read request的业务使用实现cheap/expensive read request隔离。
## 线性读实现机制、网络延时
了解完读模式对性能的影响后我们继续往下分析。在我们这个密码鉴权读请求的性能分析案例中读请求使用的是etcd默认线性读模式。线性读对应图中的流程四、流程五其中流程四对应的是ReadIndex流程五对应的是等待本节点数据追上Leader的进度ApplyWait
![](https://static001.geekbang.org/resource/image/f0/f1/f018b98629360e7c6eef6f9cfb0241f1.png)
在早期的etcd 3.0版本中etcd线性读是基于Raft log read实现的。每次读请求要像写请求一样生成一个Raft日志条目然后提交给Raft一致性模块处理基于Raft日志执行的有序性来实现线性读。因为该过程需要经过磁盘I/O所以性能较差。
为了解决Raft log read的线性读性能瓶颈etcd 3.1中引入了ReadIndex。ReadIndex仅涉及到各个节点之间网络通信因此节点之间的RTT延时对其性能有较大影响。虽然同可用区可获取到最佳性能但是存在单可用区故障风险。如果你想实现高可用区容灾的话那就必须牺牲一点性能了。
跨可用区部署时各个可用区之间延时一般在2毫秒内。如果跨城部署服务性能就会下降较大。所以一般场景下我不建议你跨城部署你可以通过Learner节点实现异地容灾。如果异地的服务对数据一致性要求不高那么你甚至可以通过串行读访问Learner节点来实现就近访问低延时。
各个节点之间的RTT延时是决定流程四ReadIndex性能的核心因素之一。
## 磁盘IO性能、写QPS
到了流程五影响性能的核心因素就是磁盘IO延时和写QPS。
![](https://static001.geekbang.org/resource/image/73/bc/732ec57338e1yy1d932e959ed776c0bc.png)
如下面代码所示流程五是指节点从Leader获取到最新已提交的日志条目索引(rs.Index)后它需要等待本节点当前已应用的Raft日志索引大于等于Leader的已提交索引确保能在本节点状态机中读取到最新数据。
```
if ai := s.getAppliedIndex(); ai < rs.Index {
select {
case <-s.applyWait.Wait(rs.Index):
case <-s.stopping:
return
}
}
// unblock all l-reads requested at indices before rs.Index
nr.notify(nil)
```
而应用已提交日志条目到状态机的过程中又涉及到随机写磁盘,详情可参考我们[03](https://time.geekbang.org/column/article/336766)中介绍过etcd的写请求原理。
因此我们可以知道,**etcd是一个对磁盘IO性能非常敏感的存储系统磁盘IO性能不仅会影响Leader稳定性、写性能表现还会影响读性能。线性读性能会随着写性能的增加而快速下降。如果业务对性能、稳定性有较大要求我建议你尽量使用SSD盘。**
下表我给出了一个8核16G的三节点集群在总key数只有一个的情况下随着写请求增大线性读性能下降的趋势总结基于benchmark工具压测结果你可以直观感受下读性能是如何随着写性能下降。
![](https://static001.geekbang.org/resource/image/40/5a/4069e72370942764ef4905715267c05a.jpg)
当本节点已应用日志条目索引大于等于Leader已提交的日志条目索引后读请求就会接到通知就可通过MVCC模块获取数据。
## RBAC规则数、Auth锁
读请求到了MVCC模块后首先要通过鉴权模块判断此用户是否有权限访问请求的数据路径也就是流程六。影响流程六的性能因素是你的RBAC规则数和锁。
首先是RBAC规则数为了解决快速判断用户对指定key范围是否有权限etcd为每个用户维护了读写权限区间树。基于区间树判断用户访问的范围是否在用户的读写权限区间内时间复杂度仅需要O(logN)。
另外一个因素则是AuthStore的锁。在etcd 3.4.9之前的校验密码接口可能会占用较长时间的锁导致授权接口阻塞。etcd 3.4.9之后合入了缩小锁范围的PR可一定程度降低授权接口被阻塞的问题。
## expensive request、treeIndex锁
通过流程六的授权后则进入流程七从treeIndex中获取整个查询涉及的key列表版本号信息。在这个流程中影响其性能的关键因素是treeIndex的总key数、查询的key数、获取treeIndex锁的耗时。
![](https://static001.geekbang.org/resource/image/9d/da/9dfe22355a9fd841943fb1c4556db9da.png)
首先treeIndex中总key数过多会适当增大我们遍历的耗时。
其次若要访问treeIndex我们必须获取到锁但是可能其他请求如compact操作也会获取锁。早期的时候它需要遍历所有索引然后进行数据压缩工作。这就会导致其他请求阻塞进而增大延时。
为了解决这个性能问题优化方案是compact的时候会将treeIndex克隆一份以空间来换时间尽量降低锁阻塞带来的超时问题。
接下来我重点给你介绍下查询key数较多等expensive read request时对性能的影响。
假设我们链路分析图中的请求是查询一个Kubernetes集群所有Pod当你Pod数一百以内的时候可能对etcd影响不大但是当你Pod数千甚至上万的时候 流程七、八就会遍历大量的key导致请求耗时突增、内存上涨、性能急剧下降。你可结合[13](https://time.geekbang.org/column/article/343245)db大小、[14](https://time.geekbang.org/column/article/343645)延时、[15](https://time.geekbang.org/column/article/344621)内存三节一起看看,这里我就不再重复描述。
如果业务就是有这种expensive read request逻辑我们该如何应对呢
首先我们可以尽量减少expensive read request次数在程序启动的时候只List一次全量数据然后通过etcd Watch机制去获取增量变更数据。比如Kubernetes的Informer机制就是典型的优化实践。
其次在设计上评估是否能进行一些数据分片、拆分等不同场景使用不同的etcd prefix前缀。比如在Kubernetes中不要把Pod全部都部署在default命名空间下尽量根据业务场景按命名空间拆分部署。即便每个场景全量拉取也只需要遍历自己命名空间下的资源数据量上将下降一个数量级。
再次如果你觉得Watch改造大、数据也无法分片开发麻烦你可以通过分页机制按批拉取尽量减少一次性拉取数万条数据。
最后如果以上方式都不起作用的话你还可以通过引入cache实现缓存expensive read request的结果不过应用需维护缓存数据与etcd的一致性。
## 大key-value、boltdb锁
从流程七获取到key列表及版本号信息后我们就可以访问boltdb模块获取key-value信息了。在这个流程中影响其性能表现的除了我们上面介绍的expensive read request还有大key-value和锁。
首先是大key-value。我们知道etcd设计上定位是个小型的元数据存储它没有数据分片机制默认db quota只有2G实践中往往不会超过8G并且针对每个key-value大小它也进行了大小限制默认是1.5MB。
大key-value非常容易导致etcd OOM、server 节点出现丢包、性能急剧下降等。
那么当我们往etcd集群写入一个1MB的key-value时它的线性读性能会从17万QPS具体下降到多少呢?
我们可以执行如下benchmark命令
```
benchmark --endpoints=addr --conns=100 --clients=1000 \
range key --consistency=l --total=10000
```
得到其结果如下从下图你可以看到读取一个1MB的key-value线性读性能QPS下降到1163平均延时上升到818ms可见大key-value对性能的巨大影响。
![](https://static001.geekbang.org/resource/image/a0/c7/a0735af4c2efd4156d392f75yyf132c7.png)
同时从下面的etcd监控图上你也可以看到内存出现了突增若存在大量大key-value时可想而知etcd内存肯定暴涨大概率会OOM。
![](https://static001.geekbang.org/resource/image/95/78/9599ec869c1496e8f9a8e5e54acb5b78.png)
其次是锁etcd为了提升boltdb读的性能从etcd 3.1到etcd 3.4版本,分别进行过几次重大优化,在下一节中我将和你介绍。
以上就是一个开启密码鉴权场景,线性读请求的性能瓶颈分析过程。
## 小结
今天我通过从上至下的请求流程分析,介绍了各个流程中可能存在的瓶颈和优化方法、最佳实践等。
优化读性能的核心思路是首先我们可通过etcd clientv3自带的Round-robin负载均衡算法或者Load Balancer尽量确保整个集群负载均衡。
然后,在开启鉴权场景时,建议你尽量使用证书而不是密码认证,避免校验密码的昂贵开销。
其次根据业务场景选择合适的读模式串行读比线性度性能提高30%以上延时降低一倍。线性读性能受节点之间RTT延时、磁盘IO延时、当前写QPS等多重因素影响。
最容易被大家忽视的就是写QPS对读QPS的影响我通过一系列压测数据整理成一个表格让你更直观感受写QPS对读性能的影响。多可用区部署会导致节点RTT延时增高读性能下降。因此你需要在高可用和高性能上做取舍和平衡。
最后在访问数据前你的读性能还可能会受授权性能、expensive read request、treeIndex及boltdb的锁等影响。你需要遵循最佳实践避免一个请求查询大量key、大key-value等否则会导致读性能剧烈下降。
希望你通过本文当遇到读etcd性能问题时能从请求执行链路去分析瓶颈解决问题让业务和etcd跑得更稳、更快。
## 思考题
你在使用etcd过程中遇到了哪些读性能问题又是如何解决的呢
欢迎分享你的性能优化经历,感谢你阅读,也欢迎你把这篇文章分享给更多的朋友一起阅读。