编辑导语
本文使用改进的粒子群算法建立ES集群的调度模型,通过对集群数据节点的定期动态调整,不仅减少了数据节点的投入,降低了 CPU 和内存的使用,还改善了整个集群的查询和写入性能。
概述
随着业务增长,ES 集群规模越来越大,各集群的使用场景不同,机器节点配置不同,业务场景使用 ES 的方式不同, 易出现集群单点 CPU 飙高,内存不足、GC 时间过长以及磁盘使用率不足等问题,从而影响集群的整体性能。
目前 ES 集群的调度大多是通过人工观测 Zabbix 以及 Grafana 手动对集群的节点进行调度,由于依赖人工调节,并不能将 ES 集群机器的利用效率发挥到最大。为了提高集群的利用效率,降低运维成本,本文建立了一种基于粒子群算法的 ES 集群调度模型。
由于不同时间段集群的访问和写入流量不同,业务的使用方式也在不断调整,传统的全局最优策略无法运用到这种变化的场景,因此建立了可滚动的优化调度模型。该模型会在每一个时间段开始前,根据当前环境(内存、CPU、磁盘)调整这一时间段的调度安排,若环境没有发生改变,则进入下一时间段。但这样的调度方案随着计划周期的不断细化,计算量也随之变大,甚至可能出现“维数灾难”等问题。
针对上述问题,本文采用混合粒子群算法来提高搜索效率,利用 Python 语言设计并验证了混合粒子群算法用于 ES 集群调度优化的可行性和有效性。
ES 集群日负荷调度建模
# CPU、内存以及磁盘使用率打分模型
下图是某个线上 ES 集群 CPU 利用率、堆内存使用以及索引存储一天的使用情况。通常情况下,ES 集群的 CPU 利用率、堆内存使用情况会随着一天的不同时段而变化,索引的存储相对于集群磁盘的总体容量来说变化及影响不是很大。
ES集群24H负荷趋势图
对于一个线上运行的 ES 集群而言,CPU 使用率高可能会致使慢查询增多导致业务查询超时;内存使用率过高则会导致 GC 暂停、触发 old GC;磁盘使用率高于水位线时会将写请求 reroute 到其他节点,同时也可能会触发 ES 的熔断机制导致该集群上的索引不可写入。因此,CPU 利用率、内存使用率以及磁盘利用率是衡量 ES 集群整体性能的重要指标。
本文的做法是将备选机器放入资源池,对资源池里的机器进行打分,然后通过粒子群优化算法(PSO)选择合适的机器作为 ES 集群的 data 节点。
# 资源均衡度打分模型
设想一个场景,当面临节假日流量猛增等情况,ES 集群需要进行扩容,这时可能遇到备选机器 CPU 利用率高但内存使用率低或其内存使用率高但 CPU 使用率低等资源不均衡的情况。针对该问题,使用以下公式对备选机器的 CPU 资源以及内存资源的均衡情况进行打分:
结合以上(4)和(5)两个公式,本文使用公式(6)作为粒子群算法的适应值函数:
粒子群算法
# 算法简介
通常 ES 集群的部署是需要在备选机器中选择合适的机器进行节点部署。在一堆机器中寻找集群部署的最佳方案使之既能满足业务需求又不浪费机器资源,这在数学中可以简化为一个最优化问题,即在特定的解空间中搜索最优解。
粒子群算法是受鸟群的社会行为启发建立的一种依赖于随机过程的启发式搜索算法。PSO 通过粒子之间的相互协作,朝着局部最优和全局最优调整。该算法全局搜索能力强,可以融合其他算法的长处设计出适合于实际问题的改进算法。
# 速度位置更新
基本粒子群算法中,解空间的每个粒子代表一个潜在的解,数学描述为:在一个 D 维解空间中,由 m 个粒子组成的种群 X=(X1…Xi…Xm)。其中第 i 个粒子位置为 Xi = (xi1,xi2...xiD)T,其速度为 Vi=(vi1,vi2...viD)T,粒子的个体值为 pi=(pi1,pi2...piD)T,其种群的全局值为 pg=(pg1,pg2...pgD)T。
首先求每个粒子的个体最优解,利用式子(7),(8)来更新每个粒子当前的速度和位置,式中:iter 代表迭代次数,r1,r2 为 [0,1] 之间的随机数,c1,c2 为加速常数, w为惯性权值。
# 计算步骤
- 初始化种群规模和迭代次数;
- 粒子群的初始化:在解空间内产生随机粒子,初始化粒子的位置以及速度;
- 评价每个粒子的个体适应值,淘汰适应值差的粒子;
- 迭代进化:在每一次迭代更新中, 粒子根据两个“极值”: pbest 和 gbest 来更新本身: pbest 是该粒子当前找到的最佳解,gbest是整个种群目前找到的最好解, 称为全局极值点;
- 迭代终止条件:如果当前迭代次数达到开始设置的最大迭代次数或达到最小误差要求,则迭代终止,输出结果;否则继续迭代。
对粒子群算法的改进
# 软约束策略
在 ES 集群基于 PSO 算法调度过程中,决策变量会受到当前集群规模的约束:集群节点(data)个数受当前索引分片数以及分片存储策略(total_shards_per_node)的限制。在此约束下,只要决策变量稍微超出约束范围一点,粒子的最优解就无法到达最优位置。为了得到满足约束条件下可行的最优解,需要对适应值函数进行软化,然后基于软约束策略将约束函数加入适应值函数中。
式中:Nbefore 为未调整前的初始 data 节点数量,Nafter 为推荐的备选 data 节点的总数。如果推荐的备选机器数和初始集群规模不等,则可以进行一定的妥协,这个妥协通过惩罚因子 λ 进行调节,其核心思想为:节点数量变化的增加,权重系数不断增大,并且当这个误差达到最大上限时,惩罚为无穷大。
# 惩罚函数
获取备选机器的端口信息,假如ES集群运行的端口在备选机器中已经被占用则需要进行惩罚。
式中:θ 为惩罚因子。综上,改进后的粒子群适应值函数为:
# 使用环形领域拓扑搜索全局最优解
为了寻找全局最优解(gbest)避免过早收敛陷入局部最优,本文使用环形邻域拓扑的搜索方式,即所有粒子首尾相连构成环形,每个粒子与 k 个邻近粒子相连。假设有 N 个粒子,当 k=1 时,每个粒子只能选择左右两边的一个粒子进行比较;当 k=2 时,每个粒子可以选择左右两边的两个相邻粒子进行比较;而当 k=N-1 时,此时整体拓扑结构为全互联结构。在快速查找邻近点过程中,使用 kd 树可以快速查找任何点的最近邻居,本文使用 scipy.spatial.cKDTree 来计算 k 邻近粒子。
# 迭代滚动优化调度策略
线上集群需要每天持续稳定地提供服务,而每台机器的CPU、内存和磁盘情况都会随着不同时段发生变化,因此需要建立迭代滚动优化机制。即每个时间段根据当前资源池每台机器的情况来调整这一时间段的集群节点安排。本文通过 Airflow 进行定时调度,将每小时作为一个计量点,每天一共 24 个计量点。
仿真实验
# 实验环境
- ES 6.2.4 集群(三主九从,一台备机)
- 粒子群算法的种群规模 30,迭代次数 1000
- 100G 的压测数据、Rally 极限压测脚本
- 根据 baseline 的压测时间大约两小时,在 airflow 中配置半小时调度策略进行模拟
集群角色分布:
- 主节点:ES-216,ES-217,ES-218
- 数据节点 :ES-100,ES-101, ES-102,ES-103,ES-104,ES-105,ES-106,ES-107,ES-108
- 冷备:ES-111
# 实验步骤
- 运行 Rally 脚本对集群进行压测,生成 baseline 的压测结果报告;
- 再次运行 Rally 脚本,每隔半小时进行一次集群调度;
- 每次通过 python 的 paramiko 模块获取冷备机器的 feature,通过 _nodes/stats 获取集群 data 节点机器的 feature;
- 数据清洗,整理成 dataframe 代入算法模型迭代计算;
- 对调度结果中位置为0的机器 ip 逐个执行 exclude 操作,通过 _cat/recovery 查看分片情况;
- 等分片结束后,卸掉 exclude 掉的机器;
- 对调度结果中位置为1且不在集群 data 节点中的机器 ip,通过 paramiko ssh 到目标机器启动该节点;
- 生成 contender 压测结果报告,和 baseline 的压测结果比较分析。
实验结果
- baseline 压测:压测过程中的CPU使用率
- baseline 压测:同一时间,堆内存使用的情况
- contender 压测:最后一次算法调度结果
- contender 压测:适应值函数收敛图
- contender 压测:压测过程中的CPU使用率
- contender 压测:同一时间,堆内存使用的情况
实验结果分析
| Metric | Task | Baseline | Contender | Diff | Unit |
|--------------------------------:|---------------------:|------------:|------------:|---------:|-------:|
| Indexing time | | 1717.08 | 150.959 | -1566.12 | min |
| Indexing throttle time | | 0.00118333 | 0 | -0.00118 | min |
| Merge time | | 316.819 | 132.466 | -184.353 | min |
| Refresh time | | 22.0931 | 8.14423 | -13.9489 | min |
| Flush time | | 6.80153 | 2.20312 | -4.59842 | min |
| Merge throttle time | | 122.167 | 80.5742 | -41.5932 | min |
| Total Young Gen GC | | 23.547 | 35.524 | 11.977 | s |
| Total Old Gen GC | | 0 | 0 | 0 | s |
| Heap used for segments | | 107.607 | 107.648 | 0.04025 | MB |
| Heap used for doc values | | 0.313099 | 0.319702 | 0.0066 | MB |
| Heap used for terms | | 92.3869 | 92.4321 | 0.04518 | MB |
| Heap used for norms | | 0.0146484 | 0.0146484 | 0 | MB |
| Heap used for points | | 4.31084 | 4.31193 | 0.00109 | MB |
| Heap used for stored fields | | 10.5819 | 10.5693 | -0.01262 | MB |
| Segment count | | 165 | 176 | 11 | |
| Min Throughput | index-append | 60512.6 | 84928.4 | 24415.9 | docs/s |
| Median Throughput | index-append | 66679.2 | 88036 | 21356.8 | docs/s |
| Max Throughput | index-append | 68902.5 | 95821.3 | 26918.9 | docs/s |
| 50th percentile latency | index-append | 5610.93 | 4479.08 | -1131.85 | ms |
| 90th percentile latency | index-append | 7822.26 | 6624.56 | -1197.7 | ms |
| 99th percentile latency | index-append | 9950.89 | 9312.28 | -638.611 | ms |
| 99.9th percentile latency | index-append | 11278.8 | 11822.1 | 543.234 | ms |
| 99.99th percentile latency | index-append | 12525.4 | 12557.2 | 31.7495 | ms |
| 100th percentile latency | index-append | 12812 | 12771.4 | -40.6328 | ms |
| 50th percentile service time | index-append | 5610.93 | 4479.08 | -1131.85 | ms |
| 90th percentile service time | index-append | 7822.26 | 6624.56 | -1197.7 | ms |
| 99th percentile service time | index-append | 9950.89 | 9312.28 | -638.611 | ms |
| 99.9th percentile service time | index-append | 11278.8 | 11822.1 | 543.234 | ms |
| 99.99th percentile service time | index-append | 12525.4 | 12557.2 | 31.7495 | ms |
| 100th percentile service time | index-append | 12812 | 12771.4 | -40.6328 | ms |
| error rate | index-append | 0 | 0 | 0 | % |
| Min Throughput | index-stats | 100.05 | 99.9956 | -0.05439 | ops/s |
| Median Throughput | index-stats | 100.117 | 100.153 | 0.0361 | ops/s |
| Max Throughput | index-stats | 100.431 | 100.409 | -0.02195 | ops/s |
| 50th percentile latency | index-stats | 5.32167 | 5.04014 | -0.28154 | ms |
| 90th percentile latency | index-stats | 14.1983 | 7.06293 | -7.13536 | ms |
| 99th percentile latency | index-stats | 162.394 | 82.0803 | -80.3133 | ms |
| 100th percentile latency | index-stats | 185.119 | 107.025 | -78.0942 | ms |
| 50th percentile service time | index-stats | 5.1202 | 4.87297 | -0.24723 | ms |
| 90th percentile service time | index-stats | 6.53896 | 6.42834 | -0.11061 | ms |
| 99th percentile service time | index-stats | 10.3816 | 11.0737 | 0.69204 | ms |
| 100th percentile service time | index-stats | 160.134 | 106.893 | -53.2416 | ms |
| error rate | index-stats | 0 | 0 | 0 | % |
| Min Throughput | node-stats | 74.5094 | 99.4131 | 24.9037 | ops/s |
| Median Throughput | node-stats | 86.4015 | 99.9811 | 13.5795 | ops/s |
| Max Throughput | node-stats | 100.245 | 100.007 | -0.23856 | ops/s |
| 50th percentile latency | node-stats | 25.6097 | 10.4874 | -15.1223 | ms |
| 90th percentile latency | node-stats | 73.5485 | 27.8513 | -45.6972 | ms |
| 99th percentile latency | node-stats | 121.306 | 84.8256 | -36.4807 | ms |
| 100th percentile latency | node-stats | 123.617 | 87.3426 | -36.2746 | ms |
| 50th percentile service time | node-stats | 9.12567 | 8.95502 | -0.17064 | ms |
| 90th percentile service time | node-stats | 11.4099 | 10.9868 | -0.42316 | ms |
| 99th percentile service time | node-stats | 19.1537 | 16.0868 | -3.06691 | ms |
| 100th percentile service time | node-stats | 116.248 | 82.8752 | -33.3725 | ms |
| error rate | node-stats | 0 | 0 | 0 | % |
| Min Throughput | default | 29.8408 | 30.8769 | 1.03611 | ops/s |
| Median Throughput | default | 30.0673 | 31.2035 | 1.13622 | ops/s |
| Max Throughput | default | 30.959 | 31.4685 | 0.50948 | ops/s |
| 50th percentile latency | default | 113358 | 108187 | -5170.31 | ms |
| 90th percentile latency | default | 176614 | 171861 | -4752.97 | ms |
| 99th percentile latency | default | 190912 | 185794 | -5117.6 | ms |
| 99.9th percentile latency | default | 192868 | 187473 | -5395.41 | ms |
| 100th percentile latency | default | 193480 | 188330 | -5150.38 | ms |
| 50th percentile service time | default | 329.174 | 321.413 | -7.76143 | ms |
| 90th percentile service time | default | 393.63 | 375.3 | -18.33 | ms |
| 99th percentile service time | default | 481.852 | 447.837 | -34.0148 | ms |
| 99.9th percentile service time | default | 558.876 | 518.918 | -39.9582 | ms |
| 100th percentile service time | default | 669.889 | 614.75 | -55.1389 | ms |
| error rate | default | 0 | 0 | 0 | % |
| Min Throughput | term | 4882.63 | 3882.41 | -1000.22 | ops/s |
| Median Throughput | term | 5451.94 | 4039.54 | -1412.4 | ops/s |
| Max Throughput | term | 5660.88 | 4142.66 | -1518.22 | ops/s |
| 50th percentile latency | term | 3940.29 | 5632 | 1691.71 | ms |
| 90th percentile latency | term | 7364.53 | 9579.15 | 2214.62 | ms |
| 99th percentile latency | term | 7975.75 | 10352.6 | 2376.85 | ms |
| 99.9th percentile latency | term | 8274.33 | 10485.4 | 2211.06 | ms |
| 99.99th percentile latency | term | 8329.34 | 10567 | 2237.61 | ms |
| 100th percentile latency | term | 8342.35 | 10577.9 | 2235.54 | ms |
| 50th percentile service time | term | 6.40647 | 9.18781 | 2.78134 | ms |
| 90th percentile service time | term | 10.7888 | 13.0989 | 2.31005 | ms |
| 99th percentile service time | term | 21.9541 | 17.3925 | -4.56156 | ms |
| 99.9th percentile service time | term | 192.16 | 285.78 | 93.6194 | ms |
| 99.99th percentile service time | term | 424.427 | 416.389 | -8.03786 | ms |
| 100th percentile service time | term | 426.502 | 424.049 | -2.45351 | ms |
| error rate | term | 0 | 0 | 0 | % |
| Min Throughput | phrase | 54.5565 | 59.8114 | 5.25493 | ops/s |
| Median Throughput | phrase | 55.1897 | 61.0735 | 5.8838 | ops/s |
| Max Throughput | phrase | 57.7741 | 61.4302 | 3.65603 | ops/s |
| 50th percentile latency | phrase | 434220 | 389896 | -44323.4 | ms |
| 90th percentile latency | phrase | 695307 | 653045 | -42262.1 | ms |
| 99th percentile latency | phrase | 752904 | 713536 | -39367.3 | ms |
| 99.9th percentile latency | phrase | 758565 | 720462 | -38102.2 | ms |
| 99.99th percentile latency | phrase | 759755 | 722499 | -37255.8 | ms |
| 100th percentile latency | phrase | 760168 | 722878 | -37289.7 | ms |
| 50th percentile service time | phrase | 681.294 | 653.602 | -27.692 | ms |
| 90th percentile service time | phrase | 869.716 | 813.168 | -56.5479 | ms |
| 99th percentile service time | phrase | 1015.41 | 946.566 | -68.8423 | ms |
| 99.9th percentile service time | phrase | 1121.99 | 1115.74 | -6.25344 | ms |
| 99.99th percentile service time | phrase | 1365.07 | 1310.3 | -54.7704 | ms |
| 100th percentile service time | phrase | 1996.27 | 1427.16 | -569.108 | ms |
| error rate | phrase | 0 | 0 | 0 | % |
| Min Throughput | country_agg_uncached | 151.347 | 154.644 | 3.29641 | ops/s |
| Median Throughput | country_agg_uncached | 158.383 | 161.305 | 2.9224 | ops/s |
| Max Throughput | country_agg_uncached | 161.497 | 164.487 | 2.99012 | ops/s |
| 50th percentile latency | country_agg_uncached | 3180.58 | 3055.8 | -124.781 | ms |
| 90th percentile latency | country_agg_uncached | 5204.84 | 5190.12 | -14.7223 | ms |
| 99th percentile latency | country_agg_uncached | 5659.89 | 5629.04 | -30.8477 | ms |
| 99.9th percentile latency | country_agg_uncached | 5709.79 | 5676.18 | -33.6157 | ms |
| 100th percentile latency | country_agg_uncached | 5714.09 | 5680.6 | -33.488 | ms |
| 50th percentile service time | country_agg_uncached | 5.98006 | 5.74001 | -0.24005 | ms |
| 90th percentile service time | country_agg_uncached | 7.55047 | 7.04123 | -0.50924 | ms |
| 99th percentile service time | country_agg_uncached | 9.86873 | 10.2407 | 0.37198 | ms |
| 99.9th percentile service time | country_agg_uncached | 13.9543 | 73.3752 | 59.4209 | ms |
| 100th percentile service time | country_agg_uncached | 18.0607 | 78.7413 | 60.6806 | ms |
| error rate | country_agg_uncached | 0 | 0 | 0 | % |
| Min Throughput | country_agg_cached | 290.92 | 302.431 | 11.5104 | ops/s |
| Median Throughput | country_agg_cached | 293.239 | 313.174 | 19.9354 | ops/s |
| Max Throughput | country_agg_cached | 294.261 | 317.505 | 23.2441 | ops/s |
| 50th percentile latency | country_agg_cached | 1444.51 | 1322.29 | -122.218 | ms |
| 90th percentile latency | country_agg_cached | 2414.04 | 2149.01 | -265.035 | ms |
| 99th percentile latency | country_agg_cached | 2622.68 | 2321.08 | -301.597 | ms |
| 99.9th percentile latency | country_agg_cached | 2643.27 | 2341.95 | -301.317 | ms |
| 100th percentile latency | country_agg_cached | 2645.59 | 2343.51 | -302.086 | ms |
| 50th percentile service time | country_agg_cached | 3.18955 | 2.92881 | -0.26074 | ms |
| 90th percentile service time | country_agg_cached | 4.26382 | 3.86103 | -0.40279 | ms |
| 99th percentile service time | country_agg_cached | 6.16704 | 5.74578 | -0.42126 | ms |
| 99.9th percentile service time | country_agg_cached | 8.10998 | 9.59255 | 1.48257 | ms |
| 100th percentile service time | country_agg_cached | 38.2704 | 9.95657 | -28.3138 | ms |
| error rate | country_agg_cached | 0 | 0 | 0 | % |
| Min Throughput | scroll | 12.2239 | 12.5268 | 0.30296 | ops/s |
| Median Throughput | scroll | 12.2592 | 12.8011 | 0.54188 | ops/s |
| Max Throughput | scroll | 12.2881 | 12.948 | 0.65985 | ops/s |
| 50th percentile latency | scroll | 1.26197e+06 | 1.20837e+06 | -53600.8 | ms |
| 90th percentile latency | scroll | 1.55174e+06 | 1.48275e+06 | -68985.2 | ms |
| 100th percentile latency | scroll | 1.62458e+06 | 1.54164e+06 | -82944.7 | ms |
| 50th percentile service time | scroll | 80772.3 | 73913.7 | -6858.58 | ms |
| 90th percentile service time | scroll | 81415.8 | 76314 | -5101.83 | ms |
| 100th percentile service time | scroll | 81606.1 | 81817.2 | 211.018 | ms |
| error rate | scroll | 0 | 0 | 0 | % |
-------------------------------
[INFO] SUCCESS (took 8 seconds)
-------------------------------
从结果对比来看,基于改进后的粒子群算法的ES集群调度后,机器数减少 22%,CPU 和内存使用率都有所降低,索引写入、phrase 查询、聚合查询性能有所提高,scroll 拉取时间有所降低,但 Young GC 时间有所增加,term 查询时间增加。
实验总结与展望
在实验过程中,索引分片耗时大约每次在十分钟左右,后续可以考虑增加分片数把索引分片切得更小一点以提高算法整体效果。因为是对线上环境进行测试,因此未使用循环压测脚本把压测时间拉长到一天甚至更久。
另外,本文只选取 CPU、内存、磁盘三个维度作为给机器打分的依据。实际上,io、网络甚至是机器出厂时的指标数据都可以作为 feature 进行计算。
本文主要基于现有的实验资源和环境,验证了改进后的粒子群算法在集群调度中的可行性和有效性。后续当 ES、HBase、HDFS 等存储组件在 k8s 运行后,希望能对机器的降本增效以及存储组件的热点等问题做进一步的探索。
正文完
作者:张刘毅