负载均衡爬坑记

2022-03-29

又是一个值班的晚上,一个人窝在椅子上摸鱼。

OK,到点发布了,我熟练地操作着发布系统,随着发布按钮的按下,服务pod开始滚动重启。观察业务,没有错误。看了眼服务的监控状态,除了发布一开始有些曲线的波动,后续趋于平稳。看来又是稳如老狗的一次发布呢。

继续下一个服务,发布开始的几分钟曲线不太正常,一些pod流量会飙升一些,一些pod流量一开始过低。继续下一个服务。。。居然都是一样的情况。我意识到可能哪里出了问题,赶紧看下报警与业务,貌似一切正常,至于以前,印象中也有发布开始阶段流量不均匀的情况,当时都粗糙地归类于刚启动不稳定了,现在想想没有道理。

我们的流量控制是基于微服务框架,由配置路由规则与负载均衡一起决定最终指向哪个服务器。不可能有人会配置线上pod级别的路由规则,那么应该是负载均衡出了问题。基于追根究底的探索精神(闲的没事),看了下中间件负载均衡的算法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.qunhe.rpc.loadbalancer.rule;

import com.qunhe.rpc.client.config.IClientConfig;
import com.qunhe.rpc.common.logger.Logger;
import com.qunhe.rpc.common.logger.LoggerFactory;
import com.qunhe.rpc.loadbalancer.ILoadBalancer;
import com.qunhe.rpc.loadbalancer.Server;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicInteger;

public class RoundRobinRule extends AbstractLoadBalancerRule {
private AtomicInteger nextIndexAI;
private static final Logger LOG = LoggerFactory.getLogger(RoundRobinRule.class);

public RoundRobinRule() {
this.nextIndexAI = new AtomicInteger(0);
this.predicate = CompositePredicate.withPredicate(new AvailabilityPredicate(this, (IClientConfig)null)).addFallbackPredicate(AbstractServerPredicate.alwaysTrue()).build();
}

public RoundRobinRule(ILoadBalancer lb) {
this.setLoadBalancer(lb);
}

public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
LOG.warn("no load balancer");
return null;
} else {
int maxWeight = 0;
int minWeight = 2147483647;
int weightSum = 0;
LinkedHashMap<Server, RoundRobinRule.IntegerWrapper> weightMap = new LinkedHashMap();
List<Server> svrs = lb.getServerList(true);

int curIndex;
int mod;
for(curIndex = 0; curIndex < svrs.size(); ++curIndex) {
mod = this.getWeight((Server)svrs.get(curIndex));
maxWeight = Math.max(maxWeight, mod);
minWeight = Math.min(minWeight, mod);
if (mod > 0) {
weightMap.put(svrs.get(curIndex), new RoundRobinRule.IntegerWrapper(mod));
weightSum += mod;
}
}

curIndex = this.nextIndexAI.getAndIncrement();
if (maxWeight > 0 && minWeight < maxWeight) {
mod = curIndex % weightSum;

for(int i = 0; i < maxWeight; ++i) {
Iterator var11 = weightMap.entrySet().iterator();

while(var11.hasNext()) {
Entry<Server, RoundRobinRule.IntegerWrapper> entry = (Entry)var11.next();
Server svr = (Server)entry.getKey();
RoundRobinRule.IntegerWrapper w = (RoundRobinRule.IntegerWrapper)entry.getValue();
if (mod == 0 && w.getValue() > 0) {
return svr;
}

if (w.getValue() > 0) {
w.decrement();
--mod;
}
}
}
}

return (Server)svrs.get(curIndex % svrs.size());
}
}

public Server choose(Object key) {
return this.choose(this.getLoadBalancer(), key);
}

public void initWithNiwsConfig(IClientConfig clientConfig) {
}

private static final class IntegerWrapper {
private int value;

public IntegerWrapper(int value) {
this.value = value;
}

public int getValue() {
return this.value;
}

public void setValue(int value) {
this.value = value;
}

public void decrement() {
--this.value;
}
}
}

负载均衡策略采用的经典的RoundRobin算法,举个例子,ABC三个实例,权重分别为3,3,2。那么这个算法会尽量生成ABCABCAB这样的调用序列。不过这个源码看着还是太绕了,我们干脆直接测试一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Test
public void run() {
Server s1 = new Server("127.0.0.1", 1);
s1.setAlive(true);
s1.setWeight(100);
Server s2 = new Server("127.0.0.1", 2);
s2.setAlive(true);
s2.setWeight(100);
Server s3 = new Server("127.0.0.1", 3);
s3.setAlive(true);
s3.setWeight(50);

RoundRobinRule rule = new RoundRobinRule();
ILoadBalancer balance = new BaseLoadBalancer(rule);
balance.addServers(Arrays.asList(s1, s2, s3));

Map<Integer, Integer> statics = new HashMap<>();
for (Server it : balance.getServerList(true)) {
statics.put(it.getPort(), 0);
}


for (int i = 0; i < 1000; ++i) {
Server chosen = rule.choose(balance, new Object());
statics.computeIfPresent(chosen.getPort(), (k, v) -> v + 1);
System.out.printf("第%d次, 调用统计: %s\n", i, statics);
}

}

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
第0次, 调用统计: {1=1, 2=0, 3=0}
第1次, 调用统计: {1=1, 2=1, 3=0}
第2次, 调用统计: {1=1, 2=1, 3=1}
第3次, 调用统计: {1=2, 2=1, 3=1}
第4次, 调用统计: {1=2, 2=2, 3=1}
第5次, 调用统计: {1=2, 2=2, 3=2}
第6次, 调用统计: {1=3, 2=2, 3=2}
第7次, 调用统计: {1=3, 2=3, 3=2}
第8次, 调用统计: {1=3, 2=3, 3=3}
第9次, 调用统计: {1=4, 2=3, 3=3}
第10次, 调用统计: {1=4, 2=4, 3=3}
第11次, 调用统计: {1=4, 2=4, 3=4}
第12次, 调用统计: {1=5, 2=4, 3=4}
第13次, 调用统计: {1=5, 2=5, 3=4}
第14次, 调用统计: {1=5, 2=5, 3=5}
第15次, 调用统计: {1=6, 2=5, 3=5}
第16次, 调用统计: {1=6, 2=6, 3=5}
第17次, 调用统计: {1=6, 2=6, 3=6}
第18次, 调用统计: {1=7, 2=6, 3=6}
第19次, 调用统计: {1=7, 2=7, 3=6}
第20次, 调用统计: {1=7, 2=7, 3=7}
第21次, 调用统计: {1=8, 2=7, 3=7}
第22次, 调用统计: {1=8, 2=8, 3=7}
第23次, 调用统计: {1=8, 2=8, 3=8}
第24次, 调用统计: {1=9, 2=8, 3=8}
第25次, 调用统计: {1=9, 2=9, 3=8}
第26次, 调用统计: {1=9, 2=9, 3=9}
第27次, 调用统计: {1=10, 2=9, 3=9}
第28次, 调用统计: {1=10, 2=10, 3=9}
第29次, 调用统计: {1=10, 2=10, 3=10}
第30次, 调用统计: {1=11, 2=10, 3=10}
第31次, 调用统计: {1=11, 2=11, 3=10}
第32次, 调用统计: {1=11, 2=11, 3=11}
第33次, 调用统计: {1=12, 2=11, 3=11}
第34次, 调用统计: {1=12, 2=12, 3=11}
第35次, 调用统计: {1=12, 2=12, 3=12}
第36次, 调用统计: {1=13, 2=12, 3=12}
第37次, 调用统计: {1=13, 2=13, 3=12}
第38次, 调用统计: {1=13, 2=13, 3=13}
第39次, 调用统计: {1=14, 2=13, 3=13}
第40次, 调用统计: {1=14, 2=14, 3=13}
第41次, 调用统计: {1=14, 2=14, 3=14}
第42次, 调用统计: {1=15, 2=14, 3=14}
第43次, 调用统计: {1=15, 2=15, 3=14}
第44次, 调用统计: {1=15, 2=15, 3=15}
第45次, 调用统计: {1=16, 2=15, 3=15}
第46次, 调用统计: {1=16, 2=16, 3=15}
第47次, 调用统计: {1=16, 2=16, 3=16}
第48次, 调用统计: {1=17, 2=16, 3=16}
第49次, 调用统计: {1=17, 2=17, 3=16}
第50次, 调用统计: {1=17, 2=17, 3=17}
第51次, 调用统计: {1=18, 2=17, 3=17}
第52次, 调用统计: {1=18, 2=18, 3=17}
第53次, 调用统计: {1=18, 2=18, 3=18}
第54次, 调用统计: {1=19, 2=18, 3=18}
第55次, 调用统计: {1=19, 2=19, 3=18}
第56次, 调用统计: {1=19, 2=19, 3=19}
第57次, 调用统计: {1=20, 2=19, 3=19}
第58次, 调用统计: {1=20, 2=20, 3=19}
第59次, 调用统计: {1=20, 2=20, 3=20}
第60次, 调用统计: {1=21, 2=20, 3=20}
第61次, 调用统计: {1=21, 2=21, 3=20}
第62次, 调用统计: {1=21, 2=21, 3=21}
第63次, 调用统计: {1=22, 2=21, 3=21}
第64次, 调用统计: {1=22, 2=22, 3=21}
第65次, 调用统计: {1=22, 2=22, 3=22}
第66次, 调用统计: {1=23, 2=22, 3=22}
第67次, 调用统计: {1=23, 2=23, 3=22}
第68次, 调用统计: {1=23, 2=23, 3=23}
第69次, 调用统计: {1=24, 2=23, 3=23}
第70次, 调用统计: {1=24, 2=24, 3=23}
第71次, 调用统计: {1=24, 2=24, 3=24}
第72次, 调用统计: {1=25, 2=24, 3=24}
第73次, 调用统计: {1=25, 2=25, 3=24}
第74次, 调用统计: {1=25, 2=25, 3=25}
第75次, 调用统计: {1=26, 2=25, 3=25}
第76次, 调用统计: {1=26, 2=26, 3=25}
第77次, 调用统计: {1=26, 2=26, 3=26}
第78次, 调用统计: {1=27, 2=26, 3=26}
第79次, 调用统计: {1=27, 2=27, 3=26}
第80次, 调用统计: {1=27, 2=27, 3=27}
第81次, 调用统计: {1=28, 2=27, 3=27}
第82次, 调用统计: {1=28, 2=28, 3=27}
第83次, 调用统计: {1=28, 2=28, 3=28}
第84次, 调用统计: {1=29, 2=28, 3=28}
第85次, 调用统计: {1=29, 2=29, 3=28}
第86次, 调用统计: {1=29, 2=29, 3=29}
第87次, 调用统计: {1=30, 2=29, 3=29}
第88次, 调用统计: {1=30, 2=30, 3=29}
第89次, 调用统计: {1=30, 2=30, 3=30}
第90次, 调用统计: {1=31, 2=30, 3=30}
第91次, 调用统计: {1=31, 2=31, 3=30}
第92次, 调用统计: {1=31, 2=31, 3=31}
第93次, 调用统计: {1=32, 2=31, 3=31}
第94次, 调用统计: {1=32, 2=32, 3=31}
第95次, 调用统计: {1=32, 2=32, 3=32}
第96次, 调用统计: {1=33, 2=32, 3=32}
第97次, 调用统计: {1=33, 2=33, 3=32}
第98次, 调用统计: {1=33, 2=33, 3=33}
第99次, 调用统计: {1=34, 2=33, 3=33}
第100次, 调用统计: {1=34, 2=34, 3=33}
。。。省略。。。
第451次, 调用统计: {1=176, 2=176, 3=100}
第452次, 调用统计: {1=177, 2=176, 3=100}
第453次, 调用统计: {1=177, 2=177, 3=100}
第454次, 调用统计: {1=178, 2=177, 3=100}
第455次, 调用统计: {1=178, 2=178, 3=100}
第456次, 调用统计: {1=179, 2=178, 3=100}
第457次, 调用统计: {1=179, 2=179, 3=100}
第458次, 调用统计: {1=180, 2=179, 3=100}
第459次, 调用统计: {1=180, 2=180, 3=100}
第460次, 调用统计: {1=181, 2=180, 3=100}
第461次, 调用统计: {1=181, 2=181, 3=100}
第462次, 调用统计: {1=182, 2=181, 3=100}
第463次, 调用统计: {1=182, 2=182, 3=100}
第464次, 调用统计: {1=183, 2=182, 3=100}
第465次, 调用统计: {1=183, 2=183, 3=100}
第466次, 调用统计: {1=184, 2=183, 3=100}
第467次, 调用统计: {1=184, 2=184, 3=100}
第468次, 调用统计: {1=185, 2=184, 3=100}
第469次, 调用统计: {1=185, 2=185, 3=100}
第470次, 调用统计: {1=186, 2=185, 3=100}
第471次, 调用统计: {1=186, 2=186, 3=100}
第472次, 调用统计: {1=187, 2=186, 3=100}
第473次, 调用统计: {1=187, 2=187, 3=100}
第474次, 调用统计: {1=188, 2=187, 3=100}
第475次, 调用统计: {1=188, 2=188, 3=100}
第476次, 调用统计: {1=189, 2=188, 3=100}
第477次, 调用统计: {1=189, 2=189, 3=100}
第478次, 调用统计: {1=190, 2=189, 3=100}
第479次, 调用统计: {1=190, 2=190, 3=100}
第480次, 调用统计: {1=191, 2=190, 3=100}
第481次, 调用统计: {1=191, 2=191, 3=100}
第482次, 调用统计: {1=192, 2=191, 3=100}
第483次, 调用统计: {1=192, 2=192, 3=100}
第484次, 调用统计: {1=193, 2=192, 3=100}
第485次, 调用统计: {1=193, 2=193, 3=100}
第486次, 调用统计: {1=194, 2=193, 3=100}
第487次, 调用统计: {1=194, 2=194, 3=100}
第488次, 调用统计: {1=195, 2=194, 3=100}
第489次, 调用统计: {1=195, 2=195, 3=100}
第490次, 调用统计: {1=196, 2=195, 3=100}
第491次, 调用统计: {1=196, 2=196, 3=100}
第492次, 调用统计: {1=197, 2=196, 3=100}
第493次, 调用统计: {1=197, 2=197, 3=100}
第494次, 调用统计: {1=198, 2=197, 3=100}
第495次, 调用统计: {1=198, 2=198, 3=100}
第496次, 调用统计: {1=199, 2=198, 3=100}
第497次, 调用统计: {1=199, 2=199, 3=100}
第498次, 调用统计: {1=200, 2=199, 3=100}
第499次, 调用统计: {1=200, 2=200, 3=100}
第500次, 调用统计: {1=201, 2=200, 3=100}
第501次, 调用统计: {1=201, 2=201, 3=100}
第502次, 调用统计: {1=201, 2=201, 3=101}
第503次, 调用统计: {1=202, 2=201, 3=101}
第504次, 调用统计: {1=202, 2=202, 3=101}
第505次, 调用统计: {1=202, 2=202, 3=102}
第506次, 调用统计: {1=203, 2=202, 3=102}
第507次, 调用统计: {1=203, 2=203, 3=102}
第508次, 调用统计: {1=203, 2=203, 3=103}
第509次, 调用统计: {1=204, 2=203, 3=103}
第510次, 调用统计: {1=204, 2=204, 3=103}
第511次, 调用统计: {1=204, 2=204, 3=104}
第512次, 调用统计: {1=205, 2=204, 3=104}
第513次, 调用统计: {1=205, 2=205, 3=104}
第514次, 调用统计: {1=205, 2=205, 3=105}
第515次, 调用统计: {1=206, 2=205, 3=105}
第516次, 调用统计: {1=206, 2=206, 3=105}
第517次, 调用统计: {1=206, 2=206, 3=106}
第518次, 调用统计: {1=207, 2=206, 3=106}
第519次, 调用统计: {1=207, 2=207, 3=106}
第520次, 调用统计: {1=207, 2=207, 3=107}
第521次, 调用统计: {1=208, 2=207, 3=107}
第522次, 调用统计: {1=208, 2=208, 3=107}
第523次, 调用统计: {1=208, 2=208, 3=108}
第524次, 调用统计: {1=209, 2=208, 3=108}
第525次, 调用统计: {1=209, 2=209, 3=108}
第526次, 调用统计: {1=209, 2=209, 3=109}
第527次, 调用统计: {1=210, 2=209, 3=109}
第528次, 调用统计: {1=210, 2=210, 3=109}
第529次, 调用统计: {1=210, 2=210, 3=110}
第530次, 调用统计: {1=211, 2=210, 3=110}
第531次, 调用统计: {1=211, 2=211, 3=110}
第532次, 调用统计: {1=211, 2=211, 3=111}
第533次, 调用统计: {1=212, 2=211, 3=111}
第534次, 调用统计: {1=212, 2=212, 3=111}
第535次, 调用统计: {1=212, 2=212, 3=112}
第536次, 调用统计: {1=213, 2=212, 3=112}
第537次, 调用统计: {1=213, 2=213, 3=112}
第538次, 调用统计: {1=213, 2=213, 3=113}
第539次, 调用统计: {1=214, 2=213, 3=113}
第540次, 调用统计: {1=214, 2=214, 3=113}
第541次, 调用统计: {1=214, 2=214, 3=114}
第542次, 调用统计: {1=215, 2=214, 3=114}
第543次, 调用统计: {1=215, 2=215, 3=114}
第544次, 调用统计: {1=215, 2=215, 3=115}
第545次, 调用统计: {1=216, 2=215, 3=115}
第546次, 调用统计: {1=216, 2=216, 3=115}
。。。省略。。。
第985次, 调用统计: {1=393, 2=393, 3=200}
第986次, 调用统计: {1=394, 2=393, 3=200}
第987次, 调用统计: {1=394, 2=394, 3=200}
第988次, 调用统计: {1=395, 2=394, 3=200}
第989次, 调用统计: {1=395, 2=395, 3=200}
第990次, 调用统计: {1=396, 2=395, 3=200}
第991次, 调用统计: {1=396, 2=396, 3=200}
第992次, 调用统计: {1=397, 2=396, 3=200}
第993次, 调用统计: {1=397, 2=397, 3=200}
第994次, 调用统计: {1=398, 2=397, 3=200}
第995次, 调用统计: {1=398, 2=398, 3=200}
第996次, 调用统计: {1=399, 2=398, 3=200}
第997次, 调用统计: {1=399, 2=399, 3=200}
第998次, 调用统计: {1=400, 2=399, 3=200}
第999次, 调用统计: {1=400, 2=400, 3=200}

最终调用比例的确达到了权重预设的比例,不过一开始是比较均衡的,权重并没有起到作用。感觉有问题?

我转念一想,不对啊,这也不会导致pod之间流量不均匀啊。在对负载均衡模块经过又一轮查看后,没有找到问题所在,我几乎要放弃了。

第二天与同事交流了这个问题,他说是不是给的权重或者别的就已经有问题了。这给了我一些启发,虽然最终答案与这没什么关系。我之前忽略的一点是问题只发生在发布时,而我过于关注负载均衡算法本身了,算法是没问题的,有问题的是来源。

我们知道,重启服务会变更ip(大家应该都上容器了吧),同时会更新rpc注册中心的地址,注册中心又会通知消费者去更新订阅的服务列表。是不是更新这个逻辑有什么玄机?如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
private void refreshInvokers(final List<URL> providerUrls) {
if (providerUrls != null && providerUrls.size() == 1 && providerUrls.get(0) != null &&
Constants.EMPTY_PROTOCOL.equals(providerUrls.get(0).getProtocol())) {
onEmptyProtocol();
} else {
if (CollectionUtils.isEmpty(providerUrls)) {
final List<URL> localCached = this.cachedProviderUrls;
if (localCached != null) {
providerUrls.addAll(localCached);
}
} else {
// 注意这行
this.cachedProviderUrls = providerUrls;
}

final ServiceParams svrCfg = new ServiceParams();
final Map<ApiConfig, List<ApiServer>> apiCaches = buildApiCache(providerUrls, svrCfg);
if (apiCaches.size() > 0) {
final ApiInvokerBuilder apiInvokerBuilder = new ApiInvokerBuilder(this);
final UrlPrefixTree<ApiInvoker> cacheTree = new UrlPrefixTree<>();
for (final Map.Entry<ApiConfig, List<ApiServer>> entry : apiCaches.entrySet()) {
final List<ApiInvoker> invokers = buildApiInvoker(entry.getKey(),
entry.getValue(), svrCfg, apiInvokerBuilder);
for (final ApiInvoker invoker : invokers) {
cacheTree.addApiPattern(invoker);
}
}

final UrlPrefixTree<ApiInvoker> oldPrefixTree = this.prefixTree;
this.prefixTree = cacheTree;
destroyAllInvokers(oldPrefixTree);
apiInvokerBuilder.resetAll();
} else {
final UrlPrefixTree<ApiInvoker> oldPrefixTree = this.prefixTree;
this.prefixTree = null;
destroyAllInvokers(oldPrefixTree);
}
afterApiInvokerBuild(apiCaches);
}
}

注意注释的那行,debug的过程中发现,每次有provider的变更,负载均衡都会重新使用新的订阅服务器列表,而每次从zookeeper拿的服务器列表顺序都是固定的。新起的pod,会放到list后面,前面的是先启动的pod。而从负载均衡算法看,第一次肯定是用列表第一个元素,也就是第一个pod,后面也是顺序轮训。所有在滚动重启或扩容的发布过程中,前几个的流量会相对高,pod变更的越多,差异越大。

询问中间件的同学,他们认为这确实是一个问题,后续会改进负载均衡逻辑。事了拂衣去,深藏功与名~