-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.xml
564 lines (466 loc) · 84.8 KB
/
index.xml
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
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>kikimo</title>
<link>https://coderatwork.cn/</link>
<description>Recent content on kikimo</description>
<generator>Hugo -- gohugo.io</generator>
<language>en-us</language>
<lastBuildDate>Sat, 04 Mar 2023 14:01:45 +0800</lastBuildDate><atom:link href="https://coderatwork.cn/index.xml" rel="self" type="application/rss+xml" />
<item>
<title>消息乱序对 Raft 快照的影响</title>
<link>https://coderatwork.cn/posts/raft-out-of-order-snap/</link>
<pubDate>Sat, 04 Mar 2023 14:01:45 +0800</pubDate>
<guid>https://coderatwork.cn/posts/raft-out-of-order-snap/</guid>
<description>Raft Leader 向 Follower 传输快照的时候,乱序的消息可以能会导致集群瘫痪,看以下例子:
Leader 向 Follower 先后发送了 sendAppend(130, 132)(发送 index 在 [130, 132] 范围内的日志)和 sendAppend(133, 135) 两条 RPC 这两条消息在网络传输中乱序了,导致 Follower 先收到 sendAppend(133, 135),然后才收到 sendAppend(133, 135); Follower 收到 sendAppend(133, 135) 后 Reject 这条消息,然后又 Accept 了 sendAppend(130, 132) 的消息 Leader 在收到 Follower 的 Reject 回应前把日志压缩到 131 位置,收到 Follower Reject 消息触发 Leader 向 Follower 发送快照 Snap(131)(快照中的 lastAppliedIndex = 131),并把 Follower 状态设置为等待快照接受状态 然后 Leader 收到 sendAppend(130, 132) 消息的 Accept 回应,把 Follower 的 matchIndex 跟新到 132 Follower 收到 snap(131) 更新了本地快照,并把重置日志信息——把最后一条日志设置为 131 之后 Follower 的给 Leader 回应的 commitIndex 都是 131 &lt; 132(matchIndex),Follower 无法从等待快照状态中恢复过来,再也收不到新的日志信息 解决方法:</description>
</item>
<item>
<title>Raft Stale Read 问题分析</title>
<link>https://coderatwork.cn/posts/raft-stale-read/</link>
<pubDate>Tue, 17 May 2022 21:00:15 +0800</pubDate>
<guid>https://coderatwork.cn/posts/raft-stale-read/</guid>
<description>这几天在测试一份 Raft 代码的时候发现一个测试过不了: 客户端读不到刚刚写入成功的数据。 这套 Raft 为了保证先行一致性, 所有的读取操作也通过状态机达成一致, 所以出现这种 stale read 的情况就让人很意外。 加几行日志,把所有 Raft apply log 都打印出来, 这套 Raft 代码会批量 apply committed log, 在日志也打印了一次批量 log apply 的开始和结束。 在日志观察到一个情况略神奇: 出现 stale read 时, 总会在一次批量的 log apply 中发现一条 put 操作紧跟着一条 get 操作, 而这条 put 操作的 key/value 就是 stale read 中 miss 的那条记录。 review 代码,发现,在一次批量 Raft log apply 中, 用的是同一个 WriteBatch, 而这中间是允许读操作插进来的。 所以,导致 stale read 的一个合理解释就是 put 操作和紧连的 get 操作塞到同一个批量 raft log apply 操作中, 且他们操作统一个key/value, put 写完但是 WriteBatch 还没提交, 自然后面的 get 也就读不到数据。</description>
</item>
<item>
<title>RPC Pipeline</title>
<link>https://coderatwork.cn/posts/rpc-pipeline/</link>
<pubDate>Fri, 21 Jan 2022 21:27:19 +0800</pubDate>
<guid>https://coderatwork.cn/posts/rpc-pipeline/</guid>
<description>这篇文章讲 RPC Pipeline,涉及的代码在rpc-pipeline。
在使用 RPC 通信时通常我们会等待一次请求结束后再发起下一个请求。 也就等待上一个请求受到回应后才发起下一个请求开始。 但是很多时候,其实并不需要等收到回应后才开始下一个请求。 比如通过 RPC 向服务端发送大量数据,在网络稳定的情况下,服务一般不会出什么错, 这时候如果不等服务端发来回应就继续发起下一个请求的话可以大幅提升数据传输性能。 RPC 回应可以放到异步回调里处理。
以 Go RPC 为例,我们来开如何实现一个 RPC Pipeline。 首先需要一个客户端,就以最简单的 echo server 当例子:
package server type HelloService struct {} func (p *HelloService) Hello(request string, reply *string) error { *reply = &#34;hello:&#34; + request return nil } func runServer() { rpc.RegisterName(&#34;HelloService&#34;, new(server.HelloService)) listerner, err := net.Listen(&#34;tcp&#34;, &#34;:8848&#34;) if err != nil { glog.Fatalf(&#34;error starting rpc server: %+v&#34;, err) } for { conn, err := listerner.</description>
</item>
<item>
<title>Nebula Storage 无法 Kill 问题分析</title>
<link>https://coderatwork.cn/posts/nebula-kill-fail/</link>
<pubDate>Sun, 09 Jan 2022 16:48:52 +0800</pubDate>
<guid>https://coderatwork.cn/posts/nebula-kill-fail/</guid>
<description>Nebula storage 实例经常出现无法 kill 的情况。 必须使用暴力的kill -9才能强行让它退出。 storage 无法 kill 一例:
# ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.0 1104 4 ? Ss 14:43 0:00 /sbin/docker-init -- /bin/bash -c /root/nebula-cluster/scripts/run-storage.sh root 7 0.0 0.0 3976 3136 ? S 14:43 0:00 /bin/bash /root/nebula-cluster/scripts/run-storage.sh root 11 0.0 0.0 12176 4300 ? Ss 14:43 0:00 sshd: /usr/sbin/sshd [listener] 0 of 10-100 startups root 12 14.</description>
</item>
<item>
<title>Raft 选主问题——及时更新 currentTerm</title>
<link>https://coderatwork.cn/posts/raft-update-current-term/</link>
<pubDate>Sun, 09 Jan 2022 15:28:52 +0800</pubDate>
<guid>https://coderatwork.cn/posts/raft-update-current-term/</guid>
<description>Raft 中的 term 相当于 逻辑时钟,Raft 论文里要求: 当一个 Raft 实例在 RPC Request/Response 中看到比自己 currentTerm 更大的 term 时需要立即更新 currentTerm 并转为 follower。 不及时更新 currentTerm 的会有诸多问题, 我们考虑一种因为 term 更新不及时导致的选举死循环问题, 考虑一种一个五节点的 Raft 集群:
p3 是 term 408 的 leader p4 发起 term 409 的选举,p2、p5 投票给 p4 p4 当选 term 409 的 leader 但是马上被网络隔离 此时 p3 还以为当前 term 是 408, 继续给 p2、p5 发日志。 p2、p5 拒绝它的日志, 但是发给 p3 的 RPC Response 没带自己的 term(此时值为 409) 或者 p3 没有根据 RPC Response 中的 term 更新自己的 currentTerm 和 role。 如果系统实现了 prevote,而且 p3 的日志比 p2、p5 新, 那么 p2、p5 的 prevote 会一直失败,p2、p5 的 prevote 请求失败了所以他们的 term 不会进一步增长,也无法发起正式选举; 另一方面 p3 不会更新自己的 term 且一直以为自己还是 term 408 的 leader, 但是 p2、p5 不认它的日志,这时候……emm……整个集群就悲剧了。</description>
</item>
<item>
<title>Raft 日志持久化</title>
<link>https://coderatwork.cn/posts/raft-persist-log/</link>
<pubDate>Sun, 09 Jan 2022 13:53:52 +0800</pubDate>
<guid>https://coderatwork.cn/posts/raft-persist-log/</guid>
<description>Raft 日志需要持久化,问题是在什么时候持久化? 用户每提交一条日志都要先保证落盘吗? 这种做法可以保证数据的完整性, 但想象一下每次都是单条日志进来,每条日志都触发write()、fsync()等操作, 如此一来性能肯定不理想。 我们可以等一批日志进来后再批量落盘。 但还是要回答一个问题,什么时候需要保证日志数据落盘? 或者我们不妨换个角度考虑这个问题:如果日志数据没有及时落盘会出现什么问题? 我们知道,一条日志包括几种状态:
uncommited:未提交 commited:已提交 applied:已执行 先考虑已提交的日志——如果一条日志已经提交但是没有落盘会出现什么问题? 我们假设在一个五节点的 Raft 集群中出现的一个场景:
p1 是 leader,p1 成功将一条日志 l1 复制到 p2、p3 p2、p3 感知到 l1 被提交,并在本地执行这条日志 p1 尚未执行 l1,且 l1 在 p1 上尚未落盘,这时候 p1 crash p1 重启,p4、p5 两个节点选举 p1 为 leader p1 在原来 l1 日志对应的 index 提交一条新日志 l2 可以看到日志 l2 和原来的 l1 冲突了,而不幸的是 l1 在 p2、p3 上已经被执行了。 通过以上分析可以知道一条日志在提交之前一定要保证落盘, 因为日志的执行在日志提交之后, 所以如果我们保证这一点那也就意味着一条被执行的日志肯定已经落盘。
不考虑快照、日志压缩的情况下,Raft 要保证数据不丢失只要保证提交的日志数据不丢失即可。 因为对于尚未明确需不需要提交的日志数据,及时丢了也不会影响到算法的正确运行。 综上,在实现 Raft 的时, 我们可以在确定一批日志数据可以提交的时候再执行批量落盘操作, 这样既保证算法的正确运行又提升了算法的效率。</description>
</item>
<item>
<title>Nebula Raft 死锁问题分析</title>
<link>https://coderatwork.cn/posts/raft-deadlock/</link>
<pubDate>Wed, 17 Nov 2021 21:59:08 +0800</pubDate>
<guid>https://coderatwork.cn/posts/raft-deadlock/</guid>
<description>最近几周在测试 Nebula 的时候经常碰到 raft storage 节点莫名离线的问题。 具体情况是这样的: 起一个 5storage 节点的 raft 集群, 然后开插边压测程序,同时不停的制造 leader change 的场景, 通常十分钟以内就能看到至少一个 storage 节点变成离线状态, 但是节点对应的进程却还活着的:
(root@nebula) [(none)]&gt; show hosts; +-----------------+-------+-----------+--------------+----------------------+------------------------+ | Host | Port | Status | Leader count | Leader distribution | Partition distribution | +-----------------+-------+-----------+--------------+----------------------+------------------------+ | &#34;192.168.15.11&#34; | 33299 | &#34;OFFLINE&#34; | 0 | &#34;No valid partition&#34; | &#34;ttos_3p3r:1&#34; | +-----------------+-------+-----------+--------------+----------------------+------------------------+ | &#34;192.168.15.11&#34; | 54889 | &#34;ONLINE&#34; | 0 | &#34;No valid partition&#34; | &#34;ttos_3p3r:1&#34; | +-----------------+-------+-----------+--------------+----------------------+------------------------+ | &#34;192.</description>
</item>
<item>
<title>Nebula Raft 死锁问题分析</title>
<link>https://coderatwork.cn/raft-deadlock/</link>
<pubDate>Wed, 17 Nov 2021 21:59:08 +0800</pubDate>
<guid>https://coderatwork.cn/raft-deadlock/</guid>
<description>最近几周在测试 Nebula 的时候经常碰到 raft storage 节点莫名离线的问题。 具体情况是这样的: 起一个 5storage 节点的 raft 集群, 然后开插边压测程序,同时不停的制造 leader change 的场景, 通常十分钟以内就能看到至少一个 storage 节点变成离线状态, 但是节点对应的进程却还活着的:
(root@nebula) [(none)]&gt; show hosts; +-----------------+-------+-----------+--------------+----------------------+------------------------+ | Host | Port | Status | Leader count | Leader distribution | Partition distribution | +-----------------+-------+-----------+--------------+----------------------+------------------------+ | &#34;192.168.15.11&#34; | 33299 | &#34;OFFLINE&#34; | 0 | &#34;No valid partition&#34; | &#34;ttos_3p3r:1&#34; | +-----------------+-------+-----------+--------------+----------------------+------------------------+ | &#34;192.168.15.11&#34; | 54889 | &#34;ONLINE&#34; | 0 | &#34;No valid partition&#34; | &#34;ttos_3p3r:1&#34; | +-----------------+-------+-----------+--------------+----------------------+------------------------+ | &#34;192.</description>
</item>
<item>
<title>为什么 Raft 遇到 higher term 要 update 本地 term</title>
<link>https://coderatwork.cn/posts/raft-update-local-term/</link>
<pubDate>Wed, 17 Nov 2021 21:49:32 +0800</pubDate>
<guid>https://coderatwork.cn/posts/raft-update-local-term/</guid>
<description>我们假设一个三节点 raft 集群中有 r1、r2、r3 三个实例,出现以下情况:
leader r3 宕机了 r2 首选发起 election,但是 r2 上的 log 没有比 r1 更新,所以 r1 不会投票给 r2,r1 也不更新本地的 term 直接返回了 r1 超时发起选举,但是 r1 的 term 一直比 r2 小(r2 先发起的投票,term 增长比 r1 快,同时 r1 收到 r2 的请求也不会 reset 本地 term),所以 r1 也没法当选 leader r1、r2 可能陷入选举死循环永远都没法选举出 leader。</description>
</item>
<item>
<title>Raft 中的选举风暴</title>
<link>https://coderatwork.cn/posts/raft-election-storm/</link>
<pubDate>Sat, 30 Oct 2021 19:23:36 +0800</pubDate>
<guid>https://coderatwork.cn/posts/raft-election-storm/</guid>
<description>A server remains in follower state as long as it receives valid RPCs from a leader or candidate.
Follower 在 election timeout 期间内如果收到合法的心跳包或者选举请求就会继续保持 follower 状态。 那么,什么是合法的选举请求?一个合法的选举请求必须满足一下两个条件:
选举请求中的 term 满足 term &gt;= currentTerm 当前节点中 votedFor 字段为 null 或者等于 candidateId 当 term &gt; currentTerm 时需要把节点转为 follower,同时设置 votedFor 字段为 null。 什么情况下 term == currentTerm &amp;&amp; votedFor == null? 一种场景是某个 raft 实例收到一个 higher term 的 vote 请求,但是因为它的日志更新, 此时它的 term 被 update,但是因为没有投票所以 voteFor 字段是 null, 这时候如果刚好另外一个处于同样 term 的 candidate 的 vote 请求过来, 他就能看到 term == currentTerm &amp;&amp; votedFor == null 的情况。 什么情况下 term == currentTerm &amp;&amp; votedFor == candidteId? 当 raft 实例收到 candidate 重复的 RPC 请求, 可能 term == currentTerm 且 votedFor == candidateId, 此时 follower 也要投票给 candidate。</description>
</item>
<item>
<title>扩展欧几里德算法及其应用</title>
<link>https://coderatwork.cn/posts/egcd/</link>
<pubDate>Sat, 29 May 2021 18:56:21 +0800</pubDate>
<guid>https://coderatwork.cn/posts/egcd/</guid>
<description>利用辗转相除法可以计算两个整数的最大公约数。 我们记两个整数 a, b 的最大公约数为 gcd(a, b),假设 gcd(a, b) = d, 那么必定存在两个整数 x, y 使得 ax + by = d。 x, y 可以用扩展欧几里德算法来计算。 我们记 egcd(a, b) 为扩展欧几里德算法,且 d, x, y = egcd(a, b) 其中 d = gcd(a, b),且 ax + by = d。
令 c = a % b,当 c = 0 时,易知 egcd(a, b) = b, 0, 1。 当 c != 0,那么此时有 gcd(a, b) = gcd(b, c) = d。 我们记录 d, x', y' = egcd(b, c),其中 x&rsquo;b + y&rsquo;c = d。 记录 k = [a / b],那么有 c = a - kb,所以有 x&rsquo;b + y'(a - kb) = d,整理后可得 y&rsquo;a + (x' - ky')b = d, 可知有 x = y', y = x' - ky' 使 ax + by = d。 算法的实现如下:</description>
</item>
<item>
<title>修复一个 kubelet numa 节点设备分配的 bug</title>
<link>https://coderatwork.cn/posts/fix-kubelet-device-numa-allocation-bug/</link>
<pubDate>Sat, 22 May 2021 14:28:17 +0800</pubDate>
<guid>https://coderatwork.cn/posts/fix-kubelet-device-numa-allocation-bug/</guid>
<description>k8s 在 1.18 以后发布了 topology manager 功能,允许用户根据硬件设备的 numa 拓扑来分配资源。 topology manager 支持多种分配策略,其中一种叫 single numa node policy, 这种策略承诺分配给 pod 的硬件都属于同一个 numa 节点。 然而我们发现某些场景下,虽然选择的是 single numa node policy, 但是 kublet 分配的设备却可能跨越多个 numa 节点。 通过排查我们发现这个问题的根源在于 devicemanager 中设备分配的实现算法上,具体问题代码在pkg/kubelet/cm/devicemanager/manager.go:filterByAffinity()
func (m *ManagerImpl) filterByAffinity(podUID, contName, resource string, available sets.String) (sets.String, sets.String, sets.String) { // If alignment information is not available, just pass the available list back. hint := m.topologyAffinityStore.GetAffinity(podUID, contName) if !m.deviceHasTopologyAlignment(resource) || hint.NUMANodeAffinity == nil { return sets.</description>
</item>
<item>
<title>分布式系统中的时间、时钟和事件的排序(一)</title>
<link>https://coderatwork.cn/posts/time-clocks/</link>
<pubDate>Mon, 05 Apr 2021 14:44:54 +0800</pubDate>
<guid>https://coderatwork.cn/posts/time-clocks/</guid>
<description>Time, Clocks, and the Ordering of Events in a Distributed System 是分布式理论中一篇非常经典也非常基础的论文, 这篇文章是该论文的阅读笔记,主要记录文章中关于逻辑时钟和分布式锁的实现算法。 论文还有一部分内容是介绍如何利用物理时钟在有外部系统参与的情况下解决分布式的同步问题。
我们可以通过时钟来知道时间。 那么时间和时钟的本质关系是什么? 当我们看到时钟上的读数便能了解当前的时间。 当时钟上读数显示某个数值时,这是一个特定的事件,本质上我们是通过一系列时间发生的顺序来了解时间。
分布式系统意味着多个节点参与,那么我们如何实现在分布式系统中的过程同步呢? 一种思路是通过时间来记录各个节点上事件发生的时间顺序,从而实现过程同步。 那如何记录事件发生的时间顺序呢?用物理时钟? 使用物理时钟的问题在于,每个节点上都需要一个物理时钟, 而我们很难保证不同物理时钟完全一致。 既然我们是通过事件来认识时间,那么我们也就可以摆脱物理时钟,用一些特定的事件取代时钟事件来认知时间。 这些事件可以形成分布式系统中的一个逻辑时钟。
首先我们来定义进程中时间发生的顺序关系,我们用 a -&gt; b 表示事件 a 发生在事件 b 之前, 当满以下条件时我们认为 a -&gt; b 成立:
a、b 在同一个进程内部,a 发生在 b 之前 a 是一条消息发送事件,b 相应的消息接收事件 可以看到这一关系具有传递性,暨如果 a -&gt; b &amp;&amp; b -&gt; c =&gt; a -&gt; c。 我们记录 C为 a 事件发生的时间,那么如果 a -&gt; b 则必然有 C&lt; C, 但反过来不一定成立(a、b 可能是不同进程中并发执行的事件)。 可以看到这个关系只能给部分事件排序(无法给并发时间做排序),为了进一步得到所有事件的排序, 我们做一步简单的约定,对于并发事件直接根据进程编号来排序,如此一来我们就能给所有事件做排序, 而这些事件的排列变可以用来做我们在分布式系统中的逻辑时钟。</description>
</item>
<item>
<title>kube-router 服务就绪慢问题分析</title>
<link>https://coderatwork.cn/posts/trouble-shooting-kube-router/</link>
<pubDate>Tue, 09 Mar 2021 20:27:18 +0800</pubDate>
<guid>https://coderatwork.cn/posts/trouble-shooting-kube-router/</guid>
<description>1. 问题简介 线下的 kube-router 运行一段时间后我们发现它存在一个令人难以忍受的问题: 每次遇到节点 crash 重启后,pod 的网络都需要很长时间才能就绪。 最近对这个问题做了研究,这篇文章对记录当时排查问题的经过,并提出问题的集中处理方法。
2. 问题分析 因为 kube-router 是基于 BGP 构建的 k8s 网络解决方案, 所以我们在节点上对针对 kube-router 的 BGP 通信做了抓包分析。 在抓包分析中我们发现在宕机超过 90s 后(我们把 BGP graceful restart period 设为 90s), 从 kube-rouer 和交换机建立 peer 关系到它上报第一条路由 update 消息隔了六分钟, 而且这一现象可以稳定复现。
(1) peer 关系建立时间 (2) kube-router 第一条路由上报时间 通过跟踪 kube-router 代码, 我们发现当前场景下 kube-router 的第一条路由 update 消息的发送是通过一个定时器触发的(vendor/github.com/osrg/gobgp/pkg/server/server.go):
1486 // RFC 4724 4.1 1487 // Once the session between the Restarting Speaker and the Receiving 1488 // Speaker is re-established, .</description>
</item>
<item>
<title>Raft 算法笔记(四)——集群节点变更</title>
<link>https://coderatwork.cn/posts/notes-on-raft-4/</link>
<pubDate>Sat, 13 Feb 2021 08:16:42 +0800</pubDate>
<guid>https://coderatwork.cn/posts/notes-on-raft-4/</guid>
<description>集群节点变更需要保证
任意时刻最多只有一个主节点 主节点完备性 直接变更集群节点无法保证任意时刻只有一个主节点,例如:
旧节点 o1、o2、o3 新节点 n4、n5 o3 知悉 n4、n5 且 o3、n4、n5 选出一个主节点 o1、o2、o3 选出另外一个主节点,违背主节点唯一的性质 联合共识(joint consensus)
联合指 C(old) 和 C(new) 配置的联合,用 C(old, new) 表示 配置的两阶段提交算法
联合共识生效阶段 新配置生效阶段 联合共识阶段的规则
所有日志需要同时提交到 C(old) 集群和 C(new) 集群 C(old) 和 C(new) 集群中的节点都可以当选主节点 当选的主节点必须同时获得 C(old) 和 C(new) 的多数票 这一要求保证了系统不会在一个 term 内同时出现两个主节点 联合共识阶段允许节点在任意时刻执行配置变更,而且可以继续为客户端提供服务,那么问题来了:</description>
</item>
<item>
<title>SIGPIPE 引发的悲剧</title>
<link>https://coderatwork.cn/posts/sigpipe-tragedy/</link>
<pubDate>Sat, 06 Feb 2021 12:47:51 +0800</pubDate>
<guid>https://coderatwork.cn/posts/sigpipe-tragedy/</guid>
<description>快放年假了,昨天公司同事突然要求帮忙紧急看个线上问题。 原来线上告警系统的一个核心服务最近一个礼拜突然频繁挂掉, 系统挂掉时毫无征兆,也没有任何日志或异常堆栈, 每次挂都是所有服务节点一起挂,导致线上告警系统完全不可用, 而且只要手工 kill 掉其中给一个服务节点所有其他服务也跟着挂掉。
没有日志,服务节点的各项基础监控数据看起来也没什么异常, 这种情况下,马上想到的就是 strace attach 上去观察。 让同事 kill 一个其他节点上的服务, strace 跟踪的进程也挂了,查看 strace 的输出:
[pid 22772] tgkill(22314, 22772, SIGPIPE &lt;unfinished ...&gt; [pid 22325] &lt;... write resumed&gt; ) = 42 [pid 22326] &lt;... write resumed&gt; ) = 335 [pid 22772] &lt;... tgkill resumed&gt; ) = 0 [pid 22319] futex(0xc0000924c8, FUTEX_WAKE_PRIVATE, 1 &lt;unfinished ...&gt; [pid 22326] write(183, &#34;\2Y\366\327\35\0\1\274\371\25\0\0\0\0\0\1\0\0\0\1\6offset\10\0\0\0\0&#34;..., 36 &lt;unfinished ...&gt; [pid 22772] --- SIGPIPE {si_signo=SIGPIPE, si_code=SI_TKILL, si_pid=22314, si_uid=1000} --- [pid 22325] write(179, &#34;\32\235\313\213\33\0\1\272\371\25\0\0\0\0\0\1\0\0\0\1\4size\10\0\0\0\0\0\0&#34;.</description>
</item>
<item>
<title>k8s GPU 加载失败问题排查</title>
<link>https://coderatwork.cn/posts/survey-on-gpu-failure/</link>
<pubDate>Thu, 04 Feb 2021 20:29:25 +0800</pubDate>
<guid>https://coderatwork.cn/posts/survey-on-gpu-failure/</guid>
<description>最近接连收到了几起线上 GPU 容器实例性能低下的问题反馈。经排查发现出问题的实例并没有成功挂载 GPU 设备。进一步的调查发现这是一个和时间赛跑的随机概率问题,且几乎当前所有线上的 GPU 机器都受该问题影响。下面我们具体描述这个问题。
首先应用都是可以成功发布的,但是发布上去的应用会有一定的概率无法挂载 GPU 设备, 一旦出现这种情况就会导致实例的性能低下(因为实例用 CPU 而不是 GPU 做计算)。 应用只有在服务启动的前几秒钟才能成功挂载 GPU 设备, 如果服务启动较慢, 就会出现无法挂载的情况。 错过这几秒的时间窗口过去, 实例将无法再挂载 GPU。 这个现象(超过特定时间窗口,GPU 设备无法加载)在我们线上的 k8s 实例上几乎 100% 重现。
根据我们的测试, 一种可能的问题解决方法是重启 nvidia 的 k8s device plugin 插件, 我们在新添加的机器上测试发现启动可以解决问题, 针对现有的集群我们还需要做进一步验证。 下面我们具体介绍问题的分析定位。
首先我们通过以下代码来获取实例上的 GPU 信息:
#include &lt;cuda_runtime.h&gt;#include &lt;stdio.h&gt; int main(int argc, char **argv) { printf(&#34;%s Starting...\n&#34;, argv[0]); int deviceCount = 0; cudaError_t error_id = cudaGetDeviceCount(&amp;deviceCount); if (error_id != cudaSuccess) { printf(&#34;cudaGetDeviceCount returned %d\n-&gt; %s\n&#34;, (int)error_id, cudaGetErrorString(error_id)); printf(&#34;Result = FAIL\n&#34;); exit(EXIT_FAILURE); } if (deviceCount == 0) { printf(&#34;There are no available device(s) that support CUDA\n&#34;); } else { printf(&#34;Detected %d CUDA Capable device(s)\n&#34;, deviceCount); } int dev, driverVersion = 0, runtimeVersion = 0; dev =0; cudaSetDevice(dev); cudaDeviceProp deviceProp; cudaGetDeviceProperties(&amp;deviceProp, dev); printf(&#34;Device %d: \&#34;%s\&#34;\n&#34;, dev, deviceProp.</description>
</item>
<item>
<title>Raft 笔记(三)——快照</title>
<link>https://coderatwork.cn/posts/notes-on-raft-3/</link>
<pubDate>Tue, 19 Jan 2021 21:51:01 +0800</pubDate>
<guid>https://coderatwork.cn/posts/notes-on-raft-3/</guid>
<description>Raft 的 log 保存在内存中, 内存无法支撑 log 无限增长, 因此 Raft 用快照技术将某个时间段前的状态转成快照并存储在次盘上, 这个时间段之前的的 log 就可以删掉了。 Raft 快照存储的是从 log 开始到具体某个时间点下的已经提交的日志的状态机执行结果,而不是具体的 log 内容。 除此以外 Raft 快照中还包含两个字断 lastIncludedIndex 和 lastIncludedTerm, 这两个字段在 AppendEntries RPC 调用时用来执行一致性检查, 例如当我们网快照之后的 log append 日志时可能就需要检查快照中最后一条日志的 inddex 和 term 是否匹配。 Raft 中的每个节点都会执行快照转存操作, 因为快照转存针对的都是已经提交的日志, 所以我们不用担心数据不一致的问题(Raft 算法已经保证了节点间已提交日志的一致性)。 节点在什么时候执行快照转存操作? Raft 论文中没有限定具体的执行时机, 但给出一个简单的策略:当内存中 log 日志的大小达到某个固定值的时候便执行快照转存。
Raft 为快照处理添加了一个新的 InstallSnapshot RPC. 通常节点自己做快照转存就可以了, 但在某些特殊情况下, 节点的日志可能大幅落后于主节点, 比如这是一个新增的节点, 或者由于网络延迟导致节点数据落后较多, 这时候主节点就会向这些节点调用 InstallSnapshot RPC。 Leader 如何判断 Follower 的日志落后太多, 也就是它如何具体判断 InstallSnapshot RPC 的调用时机? 当 Leader 已经将部分 Log 丢弃(也就是这部分内容已经转存成快照了)时, 如果它发现 Follower 上还没同步这部分数据, 它就会发起 InstallSnapshot RPC。 InstallSnapshot 可能会把快照分多次发送, 所以 RPC 接口参数里有个done字段表示快照是否已经传输完毕, 除此以外还有一个offset字段表示当前数据的偏移量——InstallSnapshot传输的都是二进制数据。 在实现 InstallSnapshot 的时候一个需要特别留意的地方就是, RPC 可能是乱序抵达的,代码上的实现需要考虑这个问题。 Follower 在收到全部的快照数据后用新的快照替代当前的快照, 并且丢弃日志中已经包含在快照中的数据。</description>
</item>
<item>
<title>Raft 笔记(二)—— 日志复制</title>
<link>https://coderatwork.cn/posts/notes-on-raft-2/</link>
<pubDate>Sat, 16 Jan 2021 21:38:04 +0800</pubDate>
<guid>https://coderatwork.cn/posts/notes-on-raft-2/</guid>
<description>1. 日志复制算法 Raft 笔记(一)—— 选主 中我们提到了 AppendEntries RPC, 当时我们把它当成发送心跳的 RPC, 这个 RPC 的另外一个核心用途是日志复制, 这篇文章主要讲 Raft 三个重要模块中的日志复制模块。
Raft 中的每个节点都维护一个日志列表, 客户端向主节点追加日志, 日志复制暨主节点把日志复制到其他节点上,Raft 会记录每条日志对应的任期信息。 当主节点把一条日志复制到超过半数的节点上后,我们变可以认为这条日志被成功提交了。
Raft 保证日志在复制过程中始终遵循几个性质:
主节点只追加日志:主节点不会覆盖或者删除已有的日志 日志匹配:两个节点上同一个索引位置中的日志,如果他们的 term 一样,那么这两条日志便是一样的,且自这个位置之前的所有日志也都是一样的 主节点完备性:一条被提交的日志也会出现在后续所有的主节点的日志列表中(且位置不变) 主节点的完备性保障了 Raft 分布式共识算法的正确性。 Raft 的主节点除了必须获取超过半数的投票外还有一条非常重要的约束——只有当 candidate 上的 log 至少不比自己旧的时候, follower 才会把票投给它。 Raft 中对这里更新的日志的定义是:1. 最后一条日志的 Term 更大 2. 或者 Term 一样,但是 len(log) 更大。 Raft 论文中提到这条性质可以保证选举出来的主节点总是包含所有被提交的日志。 但是论文中没有论证这个结论。 这里我们提供一下大致的证明思路。 首先我们定义 order() 函数来描述节点上日志的更新成都, 例如 order(S1.log) &gt; order(S2.log) 表示 S1 上的日志比 S2 新。 我们通过反证法来论证, 首先我们假设 order(S1.</description>
</item>
<item>
<title>Raft 笔记(一)—— 选主</title>
<link>https://coderatwork.cn/posts/notes-on-raft-1/</link>
<pubDate>Thu, 07 Jan 2021 23:55:03 +0800</pubDate>
<guid>https://coderatwork.cn/posts/notes-on-raft-1/</guid>
<description>1. Raft 概要和选主 Raft 是一个分布式共识算法, 分布式意味着多节点参与,共识指多个节点对某个值达成共识, 这个定义留到后面我们会再详细解释。
Raft 算法由几个模块组成,分别是:
选主 日志复制 安全性保障 这边文章主要围绕的点是 Raft 中的选主流程。 Raft 算法中的节点有三种角色,分别是:
Follower —— 从节点 Candidate —— 候选节点 Leader —— 主节点 Raft 算法中所有来自客户端的请求都是通过主节点来完成的, 如果请求发送到非主节点,它会被进一步转发到主节点进行处理。 Raft 中的选主暨在一个任期(term)内从一个或多个候选节点选出主节点。 这里有一个任期的概念,初始任期为零,一次新的选举意味着一个新的任期, 它在上个任期的基础上加一。 Raft 中的节点之间通过 RPC 通信,有两个核心的 RPC 分别是:
AppendEntries RequestVote 当前我们可以把 AppendEntries 理解为 Raft 中主节点向其他节点发送心跳包的 RPC, 它通过这个心条包来维持主节点的角色。 当从节点超过一定时限未收到心跳包时便会转变为候选节点, 并开始一次心的选举。 在一次选举中候选节点向所有其他节点发送 RequsstVote RPC 请求投票, 当它获得超过半数节点的投票后便可成为新的主节点。 候选节点总是投票给自己,从节点在一个任期内总是把票投给它第一个收到 RequsstVote RPC 的候选节点。 如果一次投票中出现了平局的结果,那么候选节点会继续开始一场新的选举。 这两个 RPC 的返回值都包含节点当前的任期, 一旦请某个节点发现 RPC 返回的任期比自己当前的任期高它会理解跟新自己当前的任期, 并转为从节点。</description>
</item>
<item>
<title>NUMA 简介</title>
<link>https://coderatwork.cn/posts/numa-intro/</link>
<pubDate>Fri, 11 Dec 2020 09:43:50 +0800</pubDate>
<guid>https://coderatwork.cn/posts/numa-intro/</guid>
<description>本文对 NUMA 做简要介绍,内容主要根据HPC-numa.pdf这一讲义整理。
NUMA 的全称是 None-Uniform Memory Access,即非一致性内存访问。 它是一种访存模型,这里访存的主体是 CPU, 非一致性指的是在多核的场景下不同的 CPU 访问内存的链路、延迟可能不一样。
与 NUMA 相对的是早期的 UMA 访存模型。 UMA 的访存模型比较简单,所有的 CPU 通过 FSB(Front Side Bus)连接北桥(Nothbridge)上的内存控制器来访问内存。 这种场景下所有 CPU 看到的内存是都是一致的,它们的访存延迟都是一样的。
UMA 的问题在于,随着服务器上的核心数越来越多,FSB 很快就成了瓶颈,于是就出现了 NUMA。 NUMA 场景中 CPU 和内存分隔组成不同的 NUMA node。 通常一颗物理 CPU 对应一个 NUMA node,也可能存在对应多个 NUMA node 的情况。 UMA 中内存控制器是做在主板上的北桥中,NUMA 中的内存控制器则在 CPU 中,CPU 可以直接访问内存。 一个 NUMA node 由一组 CPU 和它所直接控制的内存组成,CPU 通过 QPI 协议实现跨 node 间的内存访问。 NUMA 节点内的访存速度通常比跨节点间的内存访问快得多(快多少?两倍?)。
NUMA 的一些疑问 numa 场景下页表的分配采用first touch原则——即分配当前线程运行的 CPU 所属的 numa 节点中的内存, 那么问题来了,如果我们开启了 numa strict 策略,那这个线程是否就不会被调度到其他 numa 节点上的 CPU 来执行了?</description>
</item>
<item>
<title>单例模式和内存屏障</title>
<link>https://coderatwork.cn/posts/singleton-and-mb/</link>
<pubDate>Wed, 09 Dec 2020 12:56:19 +0800</pubDate>
<guid>https://coderatwork.cn/posts/singleton-and-mb/</guid>
<description>单例模式是一个比较简单的设计模式, 但多线程场景下使用延迟加载的单例模式是一个经典的并发问题, 可能会需要使用锁和内存屏障来保障线程安全。 这篇文章主要讲并发场景下使用加载的单例模式的实现。
1. 非线程安全的单例实现 我们先考虑非线程安全的单例模式的实现
#include &lt;stdio.h&gt;#include &lt;stdlib.h&gt; struct singleton_t { int val; }; struct singleton_t *INSTANCE = NULL; struct singleton_t *getInstance() { if (INSTANCE == NULL) { INSTANCE = malloc(sizeof(*INSTANCE)); INSTANCE-&gt;val = 8848; } return INSTANCE; } int main() { struct singleton_t *inst = getInstance(); printf(&#34;hello: %p\n&#34;, inst); return 0; } 以上代码的问题在于, 当多个线程同时调用getInstance()函数时, INSTANCE可呢个会被初始化多次,对此我们考虑使用锁来保障线程安全。
2. 线程安全的单例实现 #include &lt;stdio.h&gt;#include &lt;stdlib.h&gt;#include &lt;pthread.h&gt; struct singleton_t { int val; }; struct singleton_t *INSTANCE = NULL; pthread_mutex_t lock; struct singleton_t *getInstance() { pthread_mutex_lock(&amp;lock); // ignore error handling if (INSTANCE == NULL) { INSTANCE = malloc(sizeof(*INSTANCE)); INSTANCE-&gt;val = 8848; } pthread_mutex_unlock(&amp;lock); return INSTANCE; } int main() { struct singleton_t *inst = NULL; if (pthread_mutex_init(&amp;lock, NULL) !</description>
</item>
<item>
<title>一次机器学习在线推理服务的性能优化</title>
<link>https://coderatwork.cn/posts/optimize-ai-reference-svc/</link>
<pubDate>Sat, 21 Nov 2020 20:43:16 +0800</pubDate>
<guid>https://coderatwork.cn/posts/optimize-ai-reference-svc/</guid>
<description>前段时间在处理一批机器学习在线推理服务的容器化时,发现这些服务可能存在性能问题。 当时通过压测观察到的情况是:
服务进程的 CPU 高,特别是内核态的 CPU 居然跑的比用户态还高 GPU 使用不充分 TODO 我们首先尝试和添加更多的 CPU 资源,但是并没有明显改善,CPU 添加到一定数量后性能反而是下降的。 这些服务几乎都是和图片处理有关的,用户通过 restful api 调用请求, 服务进程首先利用 CPU 对图片做解码操作,然后将解码后的图片数据发送给 GPU 做模型推理。 之前在研究 CPU Manager 的时候,发现k8s 的文档提到通过绑核可以让图片处理的性能得到明显的提升。 所以当时没多想就给给服务做了绑核操作,然后我们观察服务的性能提升了两倍左右。 为什么绑核会带来这样的性能提升? 当时我猜测和缓存命中率有关。 但是在perf stat观察中发现,绑核后的缓存命中率明显比绑核前要差。 另外我们还观察到绑核后内核态的 CPU 使用率明显降低了。 为什么绑核后会令内核态的 CPU 使用率降低呢? 当时猜测是不是线程数开太多了,我们这个服务是利用Python Gunicorn提供的 Web 服务。 但是检查服务配置,发现 gunicorn worker 线程数是使用默认的配置也就是数量只有一, 所以线程数这条线索感觉不是问题的真正方向,但实在又找不出其他可能的原因, 后来就放弃了,反正两倍的性能提升应用方已经很满意了。
过了一段时间,忍不住又想起这个问题。 启动压测然后通过perf top观察到,进程中调用频率最高的是内核中的update_curr()函数, 这是 CFS 调度算法中更新调度实体 vruntime 的函数。 感觉问题可能还是在于线程数的问题上。 因为 Python 中臭名昭著的 GIL, 同时为了最大化的提升 GPU 的利用率,所以我们的 Gunicorn 采用了多进程的模型。 为了简化分析的难度,我首先把 Gunicorn 调整为单进程。 依次给服务进程绑定了不同数量的 CPU,观察到一个很有意思的现象:服务进程的线程数随着绑定核数的升高而增加。 这坚定我对线程数这条线索的信心。 我们之前已经排除了 Gunicorn worker 线程数的问题,那么问题应该在别处。在什么地方呢? 我猜测是不是和我们服务使用的推理框架有关。 我们目前的推理框架采用的是 MXNet,网上翻了下 MXNet 的文档, 在MXNet 环境变量配置中看到诸多和线程数有关的配置。 其中大部分配置的默认值都是常数,感觉和这些配置估计关系不大。 然后又看到另一份文档Some Tips for Improving MXNet Performance。 其中有个变量OMP_NUM_THREADS ,MXNet 用了 OpenMP, 这个变量是用来设置 OpenMP 线程数的,似乎默认被设置为OMP_NUM_THREADS=vCPUs / 2, 也就是 CPU 核数的一半。 感觉这可能就是罪魁祸首。 我把OMP_NUM_THREADS这个变量的值设置成一,然后重新跑压测程序, 好家伙,qps 一下子提升到原来的八倍多,而 rt 值比原来还低。 再看进程的 CPU 使用情况,内核态的 CPU 一下子降到 6% 一下,perf top中update_curr()也没了, GPU 的使用率也一下子提上来了。</description>
</item>
<item>
<title>k8s 网络抖动问题排查(二)</title>
<link>https://coderatwork.cn/posts/troubleshooting-k8s-network-jitter-2/</link>
<pubDate>Thu, 05 Nov 2020 09:32:35 +0800</pubDate>
<guid>https://coderatwork.cn/posts/troubleshooting-k8s-network-jitter-2/</guid>
<description>我们的 k8s 集群很早就出现过网络抖动的问题,见一次 k8s 网络抖动问题排查, 当时的问题是由于 BGP 路由环路导致的,而差不多同一时期我们发现 k8s 集群还存在另外一个网络抖动问题, 即服务 RT p999 的抖动问题。 我们从监控上发现部分服务从虚拟机迁移到容器后 RT p999,几乎翻倍。
当时我们一度怀疑和进程调度、CPU 亲和性等问题有关。 但是使用runqlat 发现调度队列并没有明显的延迟, 我们给 k8s 节点开启了CPU Manager static policy, 当时发现的情况下部分机器的 RT p999 优化到虚拟机的水平,但是剩下的机器还是有很严重的抖动。 我们又怀疑过是网络抖动问题, 从 hping3 测试中似乎发现 k8s 节点的抖动高于虚拟机, 但是重复的抓包显示链路上的数据包往返明没有明显的 rt 延迟, 我艹艹艹,真是感觉黔驴技穷了。
当时没有进一步排查的思路,这个问题就这么一直存在着。 直到最近,在研究 trace 的时候发现一篇文章Kernel trace tools(一):中断和软中断关闭时间过长问题追踪, 它介绍了一个用于排查 hardirq/softirq 阻塞的内核模块工具。 这个工具原理本身很简单,代码也好理解,实际上bpf 已经有类似的 trace 工具。 但看这篇文章的时候我就在想,网络数据包经过中断到 DMA 后是不是通过 softirq 向上层协议栈继续交付数据的? 带着这个问题,我在Professional Linux Kernel Architecture翻到这么一张图:
可以看到 Linux 正是通过 softirq 继续向协议栈交付数据包的。 此时我感觉 softirq 入手去查这个问题是个非常有价值的方向。 在 k8s 测试节点上把这个工具跑起来, 然后欣喜的发现数不清的 softirq 延迟堆栈现场,而其中大部分跟 ipvs 模块的一个 estimation_timer 定时器有关:</description>
</item>
<item>
<title>ipvs 速率统计模块分析</title>
<link>https://coderatwork.cn/posts/ipvs-est-module/</link>
<pubDate>Mon, 02 Nov 2020 13:38:54 +0800</pubDate>
<guid>https://coderatwork.cn/posts/ipvs-est-module/</guid>
<description>ip_vs_est.c 是 ipvs 中负责速率统计的模块,我们以内核 4.19.12 版本的为例,分析这个模块的源码。 这个模块的代码量不多总共就 203 行。 结构上也简单清晰,可以分成两部分来看:
一部分是定时器相关代码,这部分代码启动一个内核定时器,每两秒钟统计一遍 ipvs 相关数据 具体的 ipvs 数据统计代码 定时器相关代码 定时器相关代码主要有以下几个函数组成
定时器清理函数 void __net_exit ip_vs_estimator_net_cleanup(struct netns_ipvs *ipvs) { del_timer_sync(&amp;ipvs-&gt;est_timer); } 定时器初始化函数 int __net_init ip_vs_estimator_net_init(struct netns_ipvs *ipvs) { INIT_LIST_HEAD(&amp;ipvs-&gt;est_list); spin_lock_init(&amp;ipvs-&gt;est_lock); timer_setup(&amp;ipvs-&gt;est_timer, estimation_timer, 0); mod_timer(&amp;ipvs-&gt;est_timer, jiffies + 2 * HZ); return 0; } ipvs 统计数据读取函数 /* Get decoded rates */ void ip_vs_read_estimator(struct ip_vs_kstats *dst, struct ip_vs_stats *stats) { struct ip_vs_estimator *e = &amp;stats-&gt;est; dst-&gt;cps = (e-&gt;cps + 0x1FF) &gt;&gt; 10; dst-&gt;inpps = (e-&gt;inpps + 0x1FF) &gt;&gt; 10; dst-&gt;outpps = (e-&gt;outpps + 0x1FF) &gt;&gt; 10; dst-&gt;inbps = (e-&gt;inbps + 0xF) &gt;&gt; 5; dst-&gt;outbps = (e-&gt;outbps + 0xF) &gt;&gt; 5; } 统计数据清零函数 void ip_vs_zero_estimator(struct ip_vs_stats *stats) { struct ip_vs_estimator *est = &amp;stats-&gt;est; struct ip_vs_kstats *k = &amp;stats-&gt;kstats; /* reset counters, caller must hold the stats-&gt;lock lock */ est-&gt;last_inbytes = k-&gt;inbytes; est-&gt;last_outbytes = k-&gt;outbytes; est-&gt;last_conns = k-&gt;conns; est-&gt;last_inpkts = k-&gt;inpkts; est-&gt;last_outpkts = k-&gt;outpkts; est-&gt;cps = 0; est-&gt;inpps = 0; est-&gt;outpps = 0; est-&gt;inbps = 0; est-&gt;outbps = 0; } 把 ipvs 对象加入统计链表 void ip_vs_start_estimator(struct netns_ipvs *ipvs, struct ip_vs_stats *stats) { struct ip_vs_estimator *est = &amp;stats-&gt;est; INIT_LIST_HEAD(&amp;est-&gt;list); spin_lock_bh(&amp;ipvs-&gt;est_lock); list_add(&amp;est-&gt;list, &amp;ipvs-&gt;est_list); spin_unlock_bh(&amp;ipvs-&gt;est_lock); } 把 ipvs 对象从统计链表中删除 void ip_vs_stop_estimator(struct netns_ipvs *ipvs, struct ip_vs_stats *stats) { struct ip_vs_estimator *est = &amp;stats-&gt;est; spin_lock_bh(&amp;ipvs-&gt;est_lock); list_del(&amp;est-&gt;list); spin_unlock_bh(&amp;ipvs-&gt;est_lock); } ipvs 数据统计代码 执行 ipvs 数据统计的主要有两个函数,estimation_timer()和ip_vs_read_cpu_stats()。 ip_vs_read_cpu_stats()分别统计每个 CPU 上的信息,</description>
</item>
<item>
<title>node_exporter 性能优化</title>
<link>https://coderatwork.cn/posts/node_exporter-perf-optimize/</link>
<pubDate>Sun, 01 Nov 2020 09:48:38 +0800</pubDate>
<guid>https://coderatwork.cn/posts/node_exporter-perf-optimize/</guid>
<description>k8s 集群中通常会部署 node_exporter 用于采集节点的基础信息。 在 k8s 集群运行一段时间后我们发现 node_exporter 占用的 CPU 明显增多。 利用 pidstat 统计 node_exporter 的 CPU 使用率,持续大约 40s 左右
Average: 65534 39269 15.78 3.95 0.00 19.73 - node_exporter 可以看到 cpu 平均使用率大约 19.73%。
对 node_exporter 做下 profile,从火焰图里我们可以看到用的 CPU 消耗可以分为三个部分:
http 接口数据处理 node_exporter 基础数据采集(火焰图里主要是 net class 数据和 ipvs 相关 collector 数据的采集) golang gc 调用基础监控数据采集接口的,发现接口 RT 达到 8s 以上,监控数据有 3.2M。
$ time curl http://127.0.0.1:9100/metrics -o ne.txt % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 3245k 0 3245k 0 0 386k 0 --:--:-- 0:00:08 --:--:-- 737k real 0m8.</description>
</item>
<item>
<title>Docker 性能优化</title>
<link>https://coderatwork.cn/posts/docker-perf-optimization/</link>
<pubDate>Tue, 13 Oct 2020 13:31:07 +0800</pubDate>
<guid>https://coderatwork.cn/posts/docker-perf-optimization/</guid>
<description>Docker 在生产环境跑了一段时间后我们发现 dockerd 进程占用了不少 CPU 资源:
# pidstat -u -p 4265 1 Linux *.x86_64 (*) */*/2020 _x86_64_ (* CPU) 07:05:44 PM UID PID %usr %system %guest %CPU CPU Command 07:05:45 PM 0 4265 100.00 16.00 0.00 100.00 38 dockerd 07:05:46 PM 0 4265 87.00 14.00 0.00 100.00 38 dockerd 07:05:47 PM 0 4265 17.00 4.00 0.00 21.00 38 dockerd 07:05:48 PM 0 4265 36.00 7.00 0.00 43.00 38 dockerd 07:05:49 PM 0 4265 100.</description>
</item>
<item>
<title>etcd 安全通信</title>
<link>https://coderatwork.cn/posts/etcd-secure-transport/</link>
<pubDate>Tue, 13 Oct 2020 12:59:53 +0800</pubDate>
<guid>https://coderatwork.cn/posts/etcd-secure-transport/</guid>
<description>etcd 中的安全通信包括两中场景,客户端和 etcd 服务端之间的安全通信,etcd 集群节点之间的安全通信。 这两种场景使用的都是 ssl 来实现安全通信。 etcd 中的 ssl 可以设置为服务器单向安全通信也可以设置服务器、客户端双向安全通信。
1. 设置服务端安全通信 $ etcd --name infra0 --data-dir infra0 \ --cert-file=/path/to/server.crt --key-file=/path/to/server.key \ --advertise-client-urls=https://127.0.0.1:2379 --listen-client-urls=https://127.0.0.1:2379 客户端链接服务端时需要指定 CA 公钥证书以验证服务端的公钥证书:
$ curl --cacert /path/to/ca.crt https://127.0.0.1:2379/v2/keys/foo -XPUT -d value=bar -v 或者也可用-k参数让 curl 跳过证书验证环节:
$ curl -k https://127.0.0.1:2379/v2/keys/foo -XPUT -d value=bar -v 2. 验证客户端 服务端配置:
etcd --name infra0 --data-dir infra0 \ --client-cert-auth --trusted-ca-file=/path/to/ca.crt --cert-file=/path/to/server.crt --key-file=/path/to/server.key \ --advertise-client-urls https://127.0.0.1:2379 --listen-client-urls https://127.0.0.1:2379 --client-cert-auth开启客户端请求验证, 客户请求,需要指定客户端公钥证书、私钥这两参数,服务端会用它的 CA 证书验证客户端的公钥证书:</description>
</item>
<item>
<title>利用 openssl 生成公钥匙证书</title>
<link>https://coderatwork.cn/posts/gen-cert-by-openssl/</link>
<pubDate>Mon, 12 Oct 2020 21:27:41 +0800</pubDate>
<guid>https://coderatwork.cn/posts/gen-cert-by-openssl/</guid>
<description>SSL 通信需要用到私钥/公钥证书。 SSL 通信的本质是利用非对称加密算法来传输对称算法的密钥,通信的时候利用对称算法加密流量。 对称加密算法的效率高,但前提是能够先安全协商和传输对称加密的密钥。 非对称加密算法的特点是加密效率不高,但它的加密密钥的解密密钥不一样,因此可以利用它的这一特点来实现对称加密密钥的传输。 具体的过程为:客户端利用公钥(这个公钥对外界所有人公开)加密好对称算法的密钥,然后发送给服务端, 这时候只有拥有私钥的服务端能够解密客户端的信息,服务端解密出客户端发送的密钥后就可以用于后续普通流量的加解密了。 这里还有一个带解决的问题是,客户端如何验证它从公开渠道或得的公钥? 目前的解决办法就是引入三方机构,这些三方机构被成为 CA(Certificate Authority), 目前目前大部分操作系统、浏览器都会装有主流 CA 机构的公钥证书。 当某个机构需要提供可信的公钥的时,它首先拿它的公钥生成一份证书签名请求也就是 CSR(Certificate Sign Request), 然后把这份 CSR 发送给某个 CA,CA 利用他们的私钥给需要做验证的公钥做签名,得到的结果就是一份带签名的公钥,也就是公钥证书 cert。 客户端可以用他们系统或浏览器上预装的 CA 机构的公钥来验证证书的合法性。
总结以上介绍中的几个重要概念:
公钥/私钥 CA: 给证书做签名的权威机构 证书签名请求(csr) 证书(cert):签名过的公钥 可以用 openssl 来生成公钥/私钥、csr、证书等一列操作。 非对称加密算法有多种,如 RSA、DSA、ECDSA 等, 这里我以 RSA 算法为例介绍利用 openssl 生成证书的一系列操作。
1. 生成 rsa key $ openssl genrsa -aes128 -out fd.key 2048 -aes-128表示用aes-128加密算法保护密钥,设置了这个选项后系统会让你输入密码,其他可选项还有-aes-256, -aes-192。 2048 是 RSA 密钥的长度,也可以选 1024,一般认为 2048 才足够安全。
查看 rsa key 的内容</description>
</item>
<item>
<title>获取系统 CPU 数量</title>
<link>https://coderatwork.cn/posts/get-sys-cpus/</link>
<pubDate>Mon, 12 Oct 2020 09:12:05 +0800</pubDate>
<guid>https://coderatwork.cn/posts/get-sys-cpus/</guid>
<description>经常碰到需要了解系统 CPU 信息的场景, 例如你登录一台服务器想知道这台服务器大概是什么配置, 或者在应用的场景中我们根据 CPU 数量来配置服务的线程数。 我们可以通过cat /proc/cpuinfo指令来获取机器的 CPU 信息。 对于 Java 应用则可以调用runtime.availableProcessors(), Go 则会自动调用runtime.GOMAXPROCS()获取可以 CPU 信息,且根据这一信息来设置 GC、MPG 中的 Processor 等配置。 通常情况下这些获取 CPU 等方法不会有问题,但在容器环境下,这些办法就有问题了。
首先容器中是没有自己的 proc 文件系统的,所以也就没办法执行cat /proc/cpuinfo。 一个解决的办法是通过 lxcfs 挂在一个伪造的/proc/cpuinfo, 我们甚至可以让这个伪造的/proc/cpuinfo和容器的 CPU 配置一致, 比如容器配置是四核,lxcfs 挂在的/proc/cpuinfo也显示四核 CPU。 所以 lxcfs 就能解决所有问题了吗? 显然事情没这么简单,通过简单的测试,可以发现即便配置了伪造的/proc/cpuinfo, Java、Go 依然获取了错误的 CPU 信息。
public class Docker { public static void main(String[] args) { Runtime runtime = Runtime.getRuntime(); int processors = runtime.availableProcessors(); // long maxMemory = runtime.maxMemory(); System.</description>
</item>
<item>
<title>eBPF 原理简介</title>
<link>https://coderatwork.cn/posts/principle-of-bpf/</link>
<pubDate>Sun, 11 Oct 2020 22:16:53 +0800</pubDate>
<guid>https://coderatwork.cn/posts/principle-of-bpf/</guid>
<description>eBPf 是一种强大的 Linux 跟踪技术,要了解 eBPF 的原理首先需要了解它的前身 BPF 技术。 BPF 是一种网络包过滤技术, 在 BPF 出现之前也存在其他的网络包过滤技术, 例如 SunOS 上的 NIT 技术和 DEC 上的 Ultrix Packet Filter 技术。 相较于之前存在的技术 BPF 在效率上具有非常明显的优势, 具体的性能对比可以参考这篇文章:The BSD Packet Filter: A New Architecture for User-level Packet Capture。 为什么 BPF 能在性能上取得大幅度的优势? 这其中最重要的原因之一在于 BPF 中过滤过滤表达式计算模型上的革新。 BPF 和之前存在的过滤技术都会根据用户输入的过滤表达式计算的结果来判断是否要过滤某个网络包。 BPF 之前的技术使用的是把过滤表达式解析为表达式树,然后通过遍历这颗树来计算判定结果, 这个过程这本质上就使用堆栈模型来计算逆波兰表达式,在性能上它存在两个问题:
过多的访存操作拖慢的速度(堆栈存放于内存中) 存在冗余的计算,例如对于表达式expr1 or expr2, 如果计算出expr1为真那么就可以直接返回了,但是expr2还是会被计算一遍 BPF 的创新点在于它设计了一套中间代码(可与 Java 字节码类比),它首先将过滤表达式编译为 BPF 中间代码,然后通过执行中间代码来计算过滤表达式。 这套字节代码设计的核心有两点:
基于寄存器的设计(区别与之前基于堆栈的计算模型),这使得数据可以方便的存放于类似 x86 CPU 上的寄存器上,效率和得到明显提高 使用 CFG (control flow graph)模型来翻译过滤表达式,这避免了树形模型中的冗余表达式计算的问题,同时也可以避免网络报文内容的重复解析(TODO: 说清楚 how) BPF 诞生于 1992 年,也算历史悠久。 在 2014 年的时候 Alexei Starovoitov 对它进行了大幅度的改进,扩充和改进了它的指令集,使它得以更好的适配 64 位机器,同时具备更强的表达能力。 既然我们可以把网络过滤表达式编译成 BPF 字节码,我们也可以把 C 代码或者它的一个子集合编译成 BPF 字节码。 既然我们可以把 BPF 用于网络包的过滤,我们是否也可把它用于更广泛的用途,比如,Linux kernel trace? 答案使肯定的,我们可以结合 Linux Trace Point、Kprobe 等技术,非常方便的实现内核的跟踪。 后续出现的 bcc 编译器可以直接把类 C 代码编译成 eBPF 字节码然后通过bpf系统调用执行 eBPF 字节码。 这些 bcc 编译的 C 代码可以注册为Linux 跟踪系统某个 trace point 的回调函数,它可以直接操作跟踪点上下文中的信息,比如进程结构体、当前上线文堆栈信息等, 本质上可以理解为一种更强大的过滤程序。</description>
</item>
<item>
<title>ps 指令 hang 死原因分析(二)</title>
<link>https://coderatwork.cn/posts/analysis-of-ps-hang-02/</link>
<pubDate>Tue, 31 Mar 2020 11:13:27 +0800</pubDate>
<guid>https://coderatwork.cn/posts/analysis-of-ps-hang-02/</guid>
<description>上一篇关于 ps 指令 hang 死原因的分析我们提到内核读写锁的补丁, 作者在这个补丁里提到读写锁导致进程死锁的一种情况:
From: Xie Yongji &lt;xieyongji@baidu.com&gt; Our system encountered a problem recently, the khungtaskd detected some process hang on mmap_sem. But the odd thing was that one task which is not on mmap_sem.wait_list still sleeps in rwsem_down_read_failed(). Through code inspection, we found a potential bug can lead to this. Imaging this: Thread 1 Thread 2 down_write(); rwsem_down_read_failed() raw_spin_lock_irq(&amp;sem-&gt;wait_lock); list_add_tail(&amp;waiter.list, &amp;wait_list); raw_spin_unlock_irq(&amp;sem-&gt;wait_lock); __up_write(); rwsem_wake(); __rwsem_mark_wake(); wake_q_add(); list_del(&amp;waiter-&gt;list); waiter-&gt;task = NULL; while (true) { set_current_state(TASK_UNINTERRUPTIBLE); if (!</description>
</item>
<item>
<title>Linux iowait</title>
<link>https://coderatwork.cn/posts/linux-iowait/</link>
<pubDate>Thu, 26 Mar 2020 09:20:16 +0800</pubDate>
<guid>https://coderatwork.cn/posts/linux-iowait/</guid>
<description>本文主要参考 The precise meaning of I/O wait time in Linux
Linux 中 CPU 的使用统计被拆分成多个组成部分, 最常见的如 sys 表示 CPU 在内核态的使用情况,usr CPU 在用户态的使用情况。 iowait 也是其中一种,它表示 CPU 等待 io 操作的时间。 和其他项相比 iowait 有一个非常特殊的地方:
iowait 高的时候,CPU 的使用率也会变高,但此时 CPU 本身并不繁忙 iowait 高的时候表示系统存在 io 瓶颈,但 iowait 低的时候并不代表系统 io 空闲 第一点其实很好理解,iowait 高,表示 CPU 在等待 io 操作,CPU 自己其实正处于休眠状态, 这个时候高 CPU 并不代表 CPU 使用率真的很高。 既然 iowait 高的时候 CPU 本质上是处于休眠状态,此时如果有一个计算密集型的进程需要 CPU 资源, Linux 就可以把进程调度到休眠中的 CPU 上执行, 这会到时该 CPU 的 usr/sys 变高,相应的它的 iowait 值就会降下来了, 所以,当我们看到 iowait 低的时候,并不能得出系统 io 不繁忙的结论。</description>
</item>
<item>
<title>CFS 调度算法中的数据结构</title>
<link>https://coderatwork.cn/posts/cfs-data-structure/</link>
<pubDate>Sat, 21 Mar 2020 16:20:23 +0800</pubDate>
<guid>https://coderatwork.cn/posts/cfs-data-structure/</guid>
<description>CFS 是当前 Linux 内核中默认使用的调度算法,它的全称是 Complete Fair Scheduler,中文的意思是完全公平调度算法。 如这个名字所体现出来的意思,CFS 调度算法尽量公平的为每个进程分配 CPU 时间。 CFS 算法的核心并不发复杂,它跟踪每个进程的 CPU 时间,每次调度总是选择当前 CPU 运行时间最小的进程。 在 CFS 出现之前,Linux 使用 O(1) 调度算法,这种算法为每个进程分配一个 nice value, 然后根据 nice value 分配进程的运行时间片。 这种算法存在诸多问题,其中之一是:优先级低的进程分配的时间片较小, 当系统运行的都是优先级低的进程时将会出现频繁的上下文切换,空耗 CPU 资源。 除此之外,O(1) 调度算法在进程优先级的计算上使用了很多难以理解的经验公式, 这些计算公式也许是有效的,但是人们无法解释他们是如何起作用的,这给系统的维护升级都带来麻烦。 CFS 的出现解决了这些问题,同时也为后来 CGroup 的 CPU 资源隔离打下基础。
早先 Linux 内核中只有一个调度队列,后来为了提高多核场景下的并发效率,改成每个 CPU 单独分配一个调度队列。
/* * This is the main, per-CPU runqueue data structure. * * Locking rule: those places that want to lock multiple runqueues * (such as the load balancing or the thread migration code), lock * acquire operations must be ordered by ascending &amp;runqueue.</description>
</item>
<item>
<title>ANSI 转义码(ANSI escape code)解析</title>
<link>https://coderatwork.cn/posts/parsing-ansi-escape-code/</link>
<pubDate>Tue, 11 Feb 2020 22:54:39 +0800</pubDate>
<guid>https://coderatwork.cn/posts/parsing-ansi-escape-code/</guid>
<description>ANSI 转义序列是命令行终端下用来控制光标位置、字体颜色以及其他终端选项的一项 in-bind signaling 标准。
ANSI escape sequences are a standard for in-band signaling to control the cursor location, color, and other options on video text terminals and terminal emulators.
In telecommunications, in-band signaling is the sending of control information within the same band or channel used for data such as voice or video. This is in contrast to out-of-band signaling which is sent over a different channel, or even over a separate network.</description>
</item>
<item>
<title>Linux 中的 wake_q_add() 函数</title>
<link>https://coderatwork.cn/posts/linux-wake_q_add/</link>
<pubDate>Tue, 04 Feb 2020 11:14:56 +0800</pubDate>
<guid>https://coderatwork.cn/posts/linux-wake_q_add/</guid>
<description>wake_q_add()是 Linux 内代码中的一个函数, 它尝试将一个系统进程放置到等待唤醒的队列中:
static bool __wake_q_add(struct wake_q_head *head, struct task_struct *task) { struct wake_q_node *node = &amp;task-&gt;wake_q; /* * Atomically grab the task, if -&gt;wake_q is !nil already it means * its already queued (either by us or someone else) and will get the * wakeup due to that. * * In order to ensure that a pending wakeup will observe our pending * state, even in the failed case, an explicit smp_mb() must be used.</description>
</item>
<item>
<title>GCC 内联汇编</title>
<link>https://coderatwork.cn/posts/gcc-inline-asm/</link>
<pubDate>Mon, 03 Feb 2020 19:55:21 +0800</pubDate>
<guid>https://coderatwork.cn/posts/gcc-inline-asm/</guid>
<description>GCC 支持内联汇编, 格式如下:
asm ( assembler template : output operands (optional) : input operands (optional) : list of clobbered registers (optional) ); asm 又可以写作 __asm__, __asm__主要用来避免命名冲突。 assembler template 就是内联的汇编代码, output operands, input operands, list of clobbered registers分别指代 输入操作数,输入操作数,修饰寄存器列表。 参数的顺序从左到右使用数字序号来引用, 例如%0表示第一个参数。 以下是几个 GCC 内联汇编的例子:
1. 内存操作数(Memory operand constraint(m)) __asm__(&#34;sidt %0\n&#34; : :&#34;m&#34;(loc)); 以上代码的作用等同于*loc = idt(idt 表示中断向量表), &quot;m&quot;表示操作数位于内存中, 其中%0表示第一个参数也就是loc。
2. 参数序号引用(Matching(Digit) constraints) __asm__(&#34;incl %0&#34; :&#34;=a&#34;(var):&#34;0&#34;(var)); 在这个例子中var既用作输入参数由用作输出参数, =a表示使用eax寄存器来存放变量var, =是修饰符,表示输出变量, 它告诉 GCC 这个变量的值会被覆盖。 &ldquo;0&quot;表示使用和第一个参数一样的存储来作为输出来源, 在这里就是eax寄存器。 这段代码展开后等价于:</description>
</item>
<item>
<title>ps 指令 hang 死原因分析(一)</title>
<link>https://coderatwork.cn/posts/analysis-of-ps-hang-01/</link>
<pubDate>Mon, 03 Feb 2020 14:35:42 +0800</pubDate>
<guid>https://coderatwork.cn/posts/analysis-of-ps-hang-01/</guid>
<description>一台服务器出了问题, 登陆上去执行ps aux命令没有响应, ctrl+c也无法退出。 重新登陆机器执行ps aux又挂住没法操作了。 只能再登陆机器, 跑strace ps aux观察,又卡住了:
# strace ps aux stat(&#34;/proc/180944&#34;, {st_mode=S_IFDIR|0555, st_size=0, ...}) = 0 open(&#34;/proc/180944/stat&#34;, O_RDONLY) = 6 read(6, &#34;180944 (ps) D 1 180804 31034 0 -&#34;..., 2048) = 330 close(6) = 0 open(&#34;/proc/180944/status&#34;, O_RDONLY) = 6 read(6, &#34;Name:\tps\nUmask:\t0022\nState:\tD (d&#34;..., 2048) = 1205 close(6) = 0 open(&#34;/proc/180944/cmdline&#34;, O_RDONLY) = 6 read(6, &#34;ps\0-ef\0&#34;, 131072) = 7 read(6, &#34;&#34;, 131065) = 0 close(6) = 0 stat(&#34;/etc/localtime&#34;, {st_mode=S_IFREG|0644, st_size=388, .</description>
</item>
<item>
<title>Linux 中的 cmpxchg 宏</title>
<link>https://coderatwork.cn/posts/linux-cmpxchg/</link>
<pubDate>Sun, 02 Feb 2020 23:51:12 +0800</pubDate>
<guid>https://coderatwork.cn/posts/linux-cmpxchg/</guid>
<description>cmpxchg 是 intel CPU 指令集中的一条指令, 这条指令经常用来实现原子锁, 我们来看 intel 文档中对这条指令的介绍:
Compares the value in the AL, AX, EAX, or RAX register with the first operand (destination operand). If the two values are equal, the second operand (source operand) is loaded into the destination operand. Otherwise, the destination operand is loaded into the AL, AX, EAX or RAX register. RAX register is available only in 64-bit mode.
This instruction can be used with a LOCK prefix to allow the instruction to be executed atomically.</description>
</item>
<item>
<title>ICMP 和 ping</title>
<link>https://coderatwork.cn/posts/icmp-and-ping/</link>
<pubDate>Sat, 01 Feb 2020 14:55:06 +0800</pubDate>
<guid>https://coderatwork.cn/posts/icmp-and-ping/</guid>
<description>ICMP(internet control management protocol) 是四层的协议。 根据 Understanding LINUX NETWORK INTERNALS Chapter 25 中的介绍,ICMP 的主要作用是交换控制信息:
The Internet Control Message Protocol (ICMP) is a transport protocol used by Internet hosts to exchange control messages, notably error notifications and information requests.
Linux 内核的协议栈中包含了 ICMP, 不过这个协议比较有意思, 它的实现是一半在内核态一半在用户态。 我们经常使用 ping 指令来测试某个节点是否在线, ping 指令用的便是 ICMP 协议, 它向目标机器发送 ICMP echo request 报文,并等待目标机器发回的 ICMP echo response 报文, 这些操作都是在用户态下完成的。 目标节点接收到 ICMP echo request 报文后会自动发送 ICMP echo response 报文, 基本上没人听说过 ping server 应用,因为接收报文和回应报文的操作是内核中的 ICMP 协议自动完成的。</description>
</item>
<item>
<title>为什么 DNS 中应该避免 CNAME 记录和 MX 记录共存</title>
<link>https://coderatwork.cn/posts/dns-cname-mx-record-conflict/</link>
<pubDate>Fri, 31 Jan 2020 21:02:45 +0800</pubDate>
<guid>https://coderatwork.cn/posts/dns-cname-mx-record-conflict/</guid>
<description>DNS 协议不允许 CNAME 记录和 MX 记录共存。 造成这种约束的主要原因在于:
DNS 会对 CNAME 记录走递归解析 CNAME 记录的优先级高于 MX 记录 递归 DNS 服务器在查询某个常规域名记录(非 CNAME 记录)时, 如果在本地 cache 中已有该域名有对应的 CNAME 记录, 则会开始用该别名记录来重启查询, 这样 MX 记录会被 CNAME 别名记录的 MX 记录所覆盖。 这个过程,如果我们把 MX 记录替换成 A 记录理解起来也许就更容易了。 实际上,不只是 MX 记录,CNAME 记录和其他非 CNAME 记录都会造成冲突, 除了特殊的 DNSSEC 记录。
以下摘自 wiki CNAME record
CNAME records are handled specially in the domain name system, and have several restrictions on their use.</description>
</item>
<item>
<title>一次 k8s 网络抖动问题排查</title>
<link>https://coderatwork.cn/posts/2019-05-26-troubleshooting-k8s-network-jitter/</link>
<pubDate>Sun, 26 May 2019 21:48:46 +0800</pubDate>
<guid>https://coderatwork.cn/posts/2019-05-26-troubleshooting-k8s-network-jitter/</guid>
<description>问题描述 某服务 A,最初只部署在虚拟机上,后来扩充部署了部分容器实例。 在扩充容器实例不久后就发现服务 rt 偶尔会出现短暂超时的情况, 而从监控上看,容器实例的性能明显比虚拟机差, 具体而言就是容器实例频繁出现 rt 超时的情况(rt 超时指 rt 大于特定的时间比如 50ms)。 超时时间一般持续 10s 左右,进一步查看监控数据发现所有超时的情况均出现在容器实例中。
问题排查 1. CFS 排查方向 出问题的时候只有容器上的 A 应用,所以猜测网络层面没有问题, 因为如果网络层面有问题的话那受影响的可能就不只是 A 应用。 又考虑到出问题的都是容器实例,自然应该从容器和虚拟机的差异方面入手。 考虑容器和虚拟机最大的区别,很容易想到容器的 CGroup CPU 资源隔离。 k8s 通过 limit、request 参数来设置容器的 CPU 资源上下限。 limit、request 利用 CGroup CPU 参数配置来实现, 它本质上又是通过内核 CFS 调度算法来实现的。 limit 通过一种限流机制来实现对某一进程 CPU 使用的上限。 当时猜测 CFS 中的上限设置算法有 bug,google 一下,居然真的找到相关的 issue, CFS quotas can lead to unnecessary throttling #67577, 这个 issue 描述的问题和当前问题及其相似,于是我们先做了一个尝试,把 k8s pod 上的 limit 参数去掉。 去掉 limit 参数后,参数似乎生效了,通过监控发现容器中 rt 高的数据点较之前少了大概有 1/3, 但是没过多久有发现了容器超时的告警, 这直接说明 CFS 的这个 bug 不是 rt 超时的本质原因。</description>
</item>
<item>
<title>CFS bandwidth control 笔记(二)</title>
<link>https://coderatwork.cn/posts/2018-12-24-notes-on-cfs-bandwidth-control-part-2/</link>
<pubDate>Wed, 30 Jan 2019 23:36:46 +0800</pubDate>
<guid>https://coderatwork.cn/posts/2018-12-24-notes-on-cfs-bandwidth-control-part-2/</guid>
<description>在CFS bandwidth control 笔记(一)中提到一个问题:
两个形成父子关系的调度组, 他们的 CPU 带宽限制具体这么运行?
对于这个问题,CFS Bandwidth Control 中的Hierarchical considerations一节里对进程组的带宽控制有以下描述:
The interface enforces that an individual entity&#39;s bandwidth is always attainable, that is: max(c_i) &lt;= C. However, over-subscription in the aggregate case is explicitly allowed to enable work-conserving semantics within a hierarchy. e.g. \Sum (c_i) may exceed C [ Where C is the parent&#39;s bandwidth, and c_i its children ] There are two ways in which a group may become throttled: a.</description>
</item>
<item>
<title>go Map 并发读写问题处理</title>
<link>https://coderatwork.cn/posts/2019-01-17-troubleshooting-go-concurrent-map-read-write-problem/</link>
<pubDate>Thu, 17 Jan 2019 21:04:40 +0800</pubDate>
<guid>https://coderatwork.cn/posts/2019-01-17-troubleshooting-go-concurrent-map-read-write-problem/</guid>
<description>几个月前碰到一起线上 go 应用崩溃问题, 查看日志,注意到其中有一条信息:
fatal error: concurrent map read and map write 回去查了下,代码里面确实有Map的并发读写, 但是涉及到Map相关的操作都用sync.RWMutex做了读写保护, 我把代码拉出来仔细看了一个晚上,愣是没看出啥问题。 这个问题后来交个另外一个同事去跟进, 同事构造了几个单元测试代码,很快就把问题重现出来了。 但是怎么修这个 bug,他说这个问题是由于三方库引起的, 不好修,所以问题后来就这么一直放着。 过了足足两个月,我感觉这问题不解决有点危险,万一哪天线上又复发就尴尬了, 就专门抽个下午的时间去研究这个问题。 代码拉下来,把同事当时写的单元测试跑起来, 很快程序就又爆出concurrent map read and map write问题。 正准备深入研究一番,然而一看同事写的测试代码顿时就无语了, 挖槽,测试代码里自身就有一个 map 的并发读写,跑起来不报这个问题才怪。 心里暗暗叫苦,TM 还是要从零开始分析。 那还是从场景复现开始做起,从之前的故障堆栈日志来看, 对于由哪个Map数据存在并发访问是比较明显的, 所以准备从代码梳理开始做起,查看哪些地方有涉及到这个Map数据的读、写操作, 然后针对这几处访问点构造并发访问场景。 一开始 review 代码的时候,发现这代码有点奇怪。 这些代码都是我几个月前写的,当时是把所有代码中的静态检查告警都搞定了才上线的, 然而今天却又看到了一堆的告警。 一开始是几个fmt.Printf的告警,顺手改掉了, 然后看到一个让我虎躯一震的静态检查告警:
XXX passes lock by value 仔细检查代码,发现在那个地方,有个结构体参数传值, 结构体中有一个sync.RWMutex字段, 其他地方也有涉及到这个结构体的参数传递,但传的是结构体指针, 那个地方当时眼睛瘸了,没注意到是值传递, 因为不是指针传值,所以和其他地方比它其实是在对不同的sync.RWMutex执行操作, 引起Map并发访问问题也就不奇怪了。
关于XXX passes lock by value问题, Detect locks passed by value in Go 这篇文章有很生动的介绍,其中有段示例代码演示了代码中如何引入这个问题并导致程序死锁的:</description>
</item>
<item>
<title>CFS bandwidth control 笔记(一)</title>
<link>https://coderatwork.cn/posts/2018-12-23-notes-on-cfs-bandwidth-control_part_1/</link>
<pubDate>Sun, 23 Dec 2018 11:54:46 +0800</pubDate>
<guid>https://coderatwork.cn/posts/2018-12-23-notes-on-cfs-bandwidth-control_part_1/</guid>
<description>1. 什么是 CFS bandwidth control CFS bandwidth control [1]是 Linux 内核中用来实现 CPU 带宽控制的一种机制, 它可以设定一个进程组(task group)可使用的 CPU 时间的上限,注意是上限, CPU 的使用上限。
2. 为什么需要 CFS bandwidth control 上面我们已经提到 CFS bandwidth control 的主要目的是设定进程组的 CPU 使用上限, 为什么需要 CFS banwidth control 来控制 CPU 使用上限呢? 原因有两点:
1. 最初的 CFS 调度器不支持设定 CPU 上限 2. 实际应用场景中需要控制进程的的 CPU 使用上限 最初的 CFS 只能控制进程的 CPU 使用下限, 更准确的表述应该是它只能控制进程全速运行时 CPU 的使用下限。 这一点和 CFS 的实现有关。 CFS 是根据权重也就是 cfs.share 参数、进程的运行时间(vruntime)来尽量保证 CPU 时间的公平分配。 假设系统中只运行 A 和 B 进程,其调度权重分别为 100、200, 那么 CFS 可保证 A 至少能占有 1/3 的 CPU, B 占有 2/3 的 CPU 时间。 但是, 如果 B 一直处于休眠状态,那么 CFS 可以一直调度执行 A 让它占有超过 1/3 的 CPU 使用时间。 CFS 的这个特点可以造成 CPU 资源的过度使用。 CPU 资源的过度使用可能会造成诸如:系统负载过高,系统响应延迟等问题。 所以内核需要一种机制来限制进程能使用的 CPU 时间的上限。 CPU bandwidth control 还有一个重要的应用场景就是用户按需购买资源, 譬如用户购买 0.</description>
</item>
<item>
<title>一起内核 hard LOCKUP 问题分析</title>
<link>https://coderatwork.cn/posts/2018-12-14-analysis-of-a-kernel-hard-lockup-problem/</link>
<pubDate>Fri, 14 Dec 2018 19:54:46 +0800</pubDate>
<guid>https://coderatwork.cn/posts/2018-12-14-analysis-of-a-kernel-hard-lockup-problem/</guid>
<description>前段时间碰到一起服务器宕机故障, 机器重启后在 /var/crash 找到了崭新的 crash dump 文件夹, 看来是一起新鲜出炉的 kernel crash 故障。 在 crash 目录下的 vmcore-dmesg.txt 文件中发现一系列如下形式的 kernel hard LOCKUP[1]日志:
[xxxxxx.xxxxxx] NMI watchdog: Watchdog detected hard LOCKUP on cpu x [xxxxxx.xxxxxx] NMI watchdog: Watchdog detected hard LOCKUP on cpu y [xxxxxx.xxxxxx] NMI watchdog: Watchdog detected hard LOCKUP on cpu z 这是一台核数不少的物理机, 日志显示几乎有一半的 CPU 发生了 hard LOCKUP. 开动 crash[2]分析, kernel crash 的堆栈现场如下:
crash&gt; bt PID: 0 TASK: ffff8820d3ad8fd0 CPU: x COMMAND: &#34;swapper/x&#34; #0 [ffff883ffde859f0] machine_kexec at ffffffff8105c4cb #1 [ffff883ffde85a50] __crash_kexec at ffffffff81104a32 #2 [ffff883ffde85b20] panic at ffffffff8169dc5f #3 [ffff883ffde85ba0] nmi_panic at ffffffff8108771f #4 [ffff883ffde85bb0] watchdog_overflow_callback at ffffffff8112fa75 #5 [ffff883ffde85bc8] __perf_event_overflow at ffffffff8116e561 #6 [ffff883ffde85c00] perf_event_overflow at ffffffff811770b4 #7 [ffff883ffde85c10] intel_pmu_handle_irq at ffffffff81009f78 #8 [ffff883ffde85e38] perf_event_nmi_handler at ffffffff816ac06b #9 [ffff883ffde85e58] nmi_handle at ffffffff816ad427 #10 [ffff883ffde85eb0] do_nmi at ffffffff816ad65d #11 [ffff883ffde85ef0] end_repeat_nmi at ffffffff816ac8d3 [exception RIP: tick_nohz_stop_sched_tick+755] RIP: ffffffff810f3483 RSP: ffff8820d3aebe50 RFLAGS: 00000002 RAX: 0000000035696841 RBX: 0002b313f9231dc0 RCX: 000000000000001f RDX: 0000000000000005 RSI: 0002b313e403c95f RDI: ffff883ffde8fe80 RBP: ffff8820d3aebe98 R8: ffff8820d3ae8000 R9: 000000012d45c83d R10: 0000000000000000 R11: 0000000000000000 R12: 0000000000000005 R13: 000000012d45c9a0 R14: ffff883ffde8fe80 R15: ffff8820d3ae8000 ORIG_RAX: ffffffffffffffff CS: 0010 SS: 0018 --- &lt;NMI exception stack&gt; --- #12 [ffff8820d3aebe50] tick_nohz_stop_sched_tick at ffffffff810f3483 #13 [ffff8820d3aebe60] sched_clock at ffffffff81033619 #14 [ffff8820d3aebea0] __tick_nohz_idle_enter at ffffffff810f359f #15 [ffff8820d3aebed0] tick_nohz_idle_enter at ffffffff810f3adf #16 [ffff8820d3aebee0] cpu_startup_entry at ffffffff810e7b1a #17 [ffff8820d3aebf28] start_secondary at ffffffff81051af6 根据堆栈现场可以观察到一下几点:</description>
</item>
<item>
<title>cron 僵尸进程问题分析</title>
<link>https://coderatwork.cn/posts/2018-04-08-analysis-of-a-zombie-process-problem/</link>
<pubDate>Sat, 14 Apr 2018 21:57:46 +0800</pubDate>
<guid>https://coderatwork.cn/posts/2018-04-08-analysis-of-a-zombie-process-problem/</guid>
<description>前段时间遇到一个比较诡异的问题, 一台服务器上突然出现了许多僵尸进程。 多数时候,这些僵尸进程出现在早上,大概到了下午又自动消失。 第二天又重复这样的情况。
查了下,这些僵尸进程的父进程都是一些挂在 cron 下的 bash 进程。
猜测是 cron 每天早上定时执行了某些任务留下了这些僵尸进程. 为什么会出现僵尸进程,以及,为什么这些僵尸进程到了下午又自己消失了? 一开始希望直接从 cron 配置定位产生僵尸进程的任务, 检查发现这台机器上配置了相当数量的 cron 任务,逐一排查过去不现实,只能寻找其他思路.
僵尸进程意味着该进程已经执行结束, 但是父进程还没有调用 wait() 获取它的返回值, 从而导致已结束的进程进程处于僵尸(zombie)进程状态。 正常而言,一个进程结束后通常会在短暂的时间内处于僵尸进程状态. 长时间处于僵尸进程状态, 意味着父进程一直都没有调用 wait(), 所以我们首先检查下这些僵尸进程的父进程也就是 cron 进程在干什么。
可以看到 2584 这个僵尸进程的父进程 2582 处于睡眠状态。 strace 显示 2582 进程在等待在 read() 系统调用。 read()系统调用读取的文件描述符是 6, 利用 lsof 指令来查看这个文件描述符的详细信息。
可以看到这是个管道文件, 对应 inode 是 13865。 进程 2582 处于管道读的一端, 根据这个 inode 信息, 我们可以利用 lsof 进一步找出管道写的一端关联的进程。
根据 lsof 的输出,大致可以断定 2586 和 18826 这两个进程把标准输出和错误输入都重定向到管道 6 写的一端了。 经过检查发现 2586 进程执行的 sleep.</description>
</item>
<item>
<title>ptrace 如何实现单步跟进</title>
<link>https://coderatwork.cn/posts/2017-09-18-how-ptrace-implement-single-step/</link>
<pubDate>Mon, 18 Sep 2017 21:57:46 +0800</pubDate>
<guid>https://coderatwork.cn/posts/2017-09-18-how-ptrace-implement-single-step/</guid>
<description>1. ptrace() 单步跟进(PTRACE_SINGLESTEP)源码分析 ptrace() 是一个重要的 Linux 系统调用,它的代码在 kernel/ptrace.c 文件中。 ptrace() 可以实现诸如暂停进程、观察进程内存数据、进程指令单步执行等功能, 它可以用来实现调试器,例如 gdb 调试器便是基于 ptrace() 实现的。 以 Linux 4.10.17 版本的代码为例,我们来分析 ptrace() 函数单步跟进功能的实现。 ptrace() 函数的定义如下:
1114 SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr, 1115 unsigned long, data) 1116 { 1117 struct task_struct *child; 1118 long ret; ... 1149 ret = arch_ptrace(child, request, addr, data); 1150 if (ret || request != PTRACE_DETACH) 1151 ptrace_unfreeze_traced(child); 1152 1153 out_put_task_struct: 1154 put_task_struct(child); 1155 out: 1156 return ret; 1157 } ptrace() 函数的一些操作是平台相关的, 这些平台相关的操作被放置到 arch_ptrace() 函数中, 对于不同的 CPU,arch_ptrace() 函数有不一样的实现。 我们要分析的单步执行功能就是一个平台相关的操作, 所以接下来要去看 x86 平台上 arch_ptrace() 函数的实现。 x86 的 arch_ptrace() 函数在文件 arch/x86/kernel/ptrace.</description>
</item>
<item>
<title>利用 ptrace 设置硬件断点</title>
<link>https://coderatwork.cn/posts/2017-08-15-setting-hardware-breakpoint-using-ptrace/</link>
<pubDate>Tue, 15 Aug 2017 21:57:46 +0800</pubDate>
<guid>https://coderatwork.cn/posts/2017-08-15-setting-hardware-breakpoint-using-ptrace/</guid>
<description>x86 CPU 为断点调试提供了硬件上的支持。 x86 CPU 上设有专门的调试寄存器, 通过设置这些寄存器, 可以为进程设置代码执行断点和内存读写断点, 这些断点统称为硬件断点。 Linux 上的 ptrace() 系统调用可以用来设置 x86 CPU 上的这些调试寄存器, 所以我们可以利用 ptrace 来给进程设置硬件断点。 这篇文章将介绍如何利用 ptrace() 来设置硬件断点。
1. 硬件断点 x86 CPU 上的共有8个调试寄存器: DR0 - DR7。 其中 DR0 - DR3 为地址寄存器, 这四个寄存器是用来存放断点地址的, 只有四个断点地址寄存器意味着最多可以同时设置四个硬件断点。 DR4 和 DR5 是保留寄存器,并不使用。 DR6 是状态寄存器,设置硬件断点的过程中并没有用到该寄存器,这里不做更多介绍。 DR7 是控制寄存器,这个寄存是上有一系列的标志位。 通过设置 DR7 寄存器上的标志位可以设定某个断点的类型(读写断点或者执行断点),断点是否有效等。 我们来看下 DR7 寄存器中各个标志位具体的含义:
0 - 7 标志位控制 DR0 - DR3 寄存器指定的断点是否处于激活状态。 G 和 L 域分别代表 global 和 local 范围。 Gray Hat Python[1]中说用户态的调试中设定 G 和 L 位没有区别。 看了下 wiki 上的介绍:</description>
</item>
</channel>
</rss>