-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlocal-search.xml
672 lines (319 loc) · 451 KB
/
local-search.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
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title>稳定性的模式与反模式</title>
<link href="/posts/stability/"/>
<url>/posts/stability/</url>
<content type="html"><![CDATA[<h1 id="稳定性的模式与反模式"><a href="#稳定性的模式与反模式" class="headerlink" title="稳定性的模式与反模式"></a>稳定性的模式与反模式</h1><blockquote><p>稳定性之于系统,就像健康之于人类,看起来重要不紧急,然而一旦失去,就追悔莫及。</p></blockquote><blockquote><p>稳定性是一切 0 前面的 1。</p></blockquote><p>本文简单介绍下稳定的模式和反模式,大家在设计系统的时候可以多一点思考。</p><h2 id="稳定性的反模式:"><a href="#稳定性的反模式:" class="headerlink" title="稳定性的反模式:"></a>稳定性的反模式:</h2><h3 id="集成点"><a href="#集成点" class="headerlink" title="集成点"></a>集成点</h3><ul><li>每一个依赖点的都有可能有问题。</li><li>利用断路器、超时、中间件解耦和握手等模式进行防御性编程,以防止集成点出现问题。</li></ul><h3 id="同层连累反应"><a href="#同层连累反应" class="headerlink" title="同层连累反应"></a>同层连累反应</h3><ul><li>例如在负载均衡下,一台服务器的停机会波及其余服务器。</li><li>可以配置健康检测,有问题时,启动新的实例。</li></ul><h3 id="层叠失效"><a href="#层叠失效" class="headerlink" title="层叠失效"></a>层叠失效</h3><ul><li>层叠失效有一个将系统失效从一个层级传到另一个层级的机制</li></ul><h3 id="用户"><a href="#用户" class="headerlink" title="用户"></a>用户</h3><ul><li>用户会消耗内存</li><li>用户会做奇怪和随机的事情</li><li>恶意用户</li></ul><h3 id="线程阻塞"><a href="#线程阻塞" class="headerlink" title="线程阻塞"></a>线程阻塞</h3><ul><li>应用程序的失效大多与线程阻塞相关。系统失效形式包括常见的系统逐渐变慢和服务器 停止响应。线程阻塞反模式会导致同层连累反应和层叠失效。</li></ul><h3 id="自黑式攻击"><a href="#自黑式攻击" class="headerlink" title="自黑式攻击"></a>自黑式攻击</h3><ul><li>无状态、自动化扩展</li></ul><h3 id="放大效应"><a href="#放大效应" class="headerlink" title="放大效应"></a>放大效应</h3><ul><li>留意点对点通信</li><li>留意共享资源</li></ul><h3 id="失衡的系统容量"><a href="#失衡的系统容量" class="headerlink" title="失衡的系统容量"></a>失衡的系统容量</h3><ul><li>通过测试发现系统容量失衡</li></ul><h3 id="一窝蜂"><a href="#一窝蜂" class="headerlink" title="一窝蜂"></a>一窝蜂</h3><ul><li>一窝蜂是对系统的集中使用,相比将峰值流量分散开后所需的系统能力,一窝蜂需要一<br>个更高的系统容量峰值。</li><li>不要将所有 cron 作业都设置在午夜或其他任何整点时间执行。用混合的方式设置时间,<br>分散负载。</li><li>固定的重试时间间隔,会集中那段时间的调用方需求。相反,使用退避算法,不同调用<br>方在经过自己的退避时间后,在不同的时间点发起调用。</li></ul><h3 id="做出误判的机器"><a href="#做出误判的机器" class="headerlink" title="做出误判的机器"></a>做出误判的机器</h3><ul><li>基础设施管理工具可以迅速对系统产生巨大的影响,要在其内部构建限制器和防护措施,防止其快速毁掉整个系统。</li></ul><h3 id="缓慢的响应"><a href="#缓慢的响应" class="headerlink" title="缓慢的响应"></a>缓慢的响应</h3><ul><li>一旦陷入响应缓慢,上游系统本身的处理速度也会随之变慢,并且当响应时间超过其自身的超时时间时,会很容易引发稳定性问题。</li><li>快速失败:如果系统能跟踪自己的响应情况,那么就可以知道自己何时变慢。当系统平均响应时间 超出系统所允许的时间时,可以考虑发送一个即时错误响应。至少,当平均响应时间超 过调用方的超时时间时,应该发送这样的响应。</li></ul><h3 id="无限长的结果集"><a href="#无限长的结果集" class="headerlink" title="无限长的结果集"></a>无限长的结果集</h3><ul><li>使用切合实际的数据量。</li><li>在前端发送分页请求。</li><li>不要依赖数据生产者。</li></ul><h2 id="稳定性的模式:"><a href="#稳定性的模式:" class="headerlink" title="稳定性的模式:"></a>稳定性的模式:</h2><p>良好的模式能为开发工程师提供架构和设计方面的指导, 从而减少、消除或缓解系统中的裂纹产生的影响。</p><h3 id="超时"><a href="#超时" class="headerlink" title="超时"></a>超时</h3><ul><li>将超时模式应用于集成点、阻塞线程和缓慢响应。<ul><li>超时模式可以防止对集成点的调用转变为对阻塞线程的调用,从而避免层叠失效。</li></ul></li><li>采用超时模式,从意外系统失效中恢复。<ul><li>当操作时间过长,有时无须明确其原因时,只需要放弃操作并继续做其他事。超时模式 可以帮助我们实现这一点。</li></ul></li><li>考虑延迟重试。<ul><li>大多数超时原因涉及网络或远程系统中的问题。这些问题不会立即被解决。立即重试很 可能会遭遇同样的问题,并导致再次超时。这只会让用户等待更长的时间才能看到错误 消息。大多数情况下,应该把操作任务放入队列,稍后再重试。</li></ul></li></ul><h3 id="断路器"><a href="#断路器" class="headerlink" title="断路器"></a>断路器</h3><ul><li>出现问题,停止调用。<ul><li>断路器是保护系统免受各种集成点问题的基本模式。如果集成点出现问题,停止调用!</li></ul></li><li>与超时模式一起使用。 <ul><li>当集成点出现问题时,断路器就能派上用场,避免继续调用。当使用超时模式时,表明集成点存在问题。</li></ul></li><li>开放、跟踪并报告断路器状态变化情况。 <ul><li>断路器跳闸总是表明出现异常。运维工程师应该注意到这种情况,加以记录和报告,并分析其趋势和相关性。</li></ul></li></ul><h3 id="舱壁"><a href="#舱壁" class="headerlink" title="舱壁"></a>舱壁</h3><ul><li>当灾难发生时,舱壁模式将系统进行分隔,确保部分系统功能可用。</li><li>选择有用的分隔粒度。<ul><li>可以对应用程序内的线程池进行分隔,对服务器的 CPU 进行分隔,或对集群中的服务器进行分隔。</li></ul></li></ul><h3 id="稳态"><a href="#稳态" class="headerlink" title="稳态"></a>稳态</h3><ul><li>便面人为干预生产环境</li><li>应用自己清理带有应用程序逻辑的数据</li><li>限制缓存</li><li>滚动日志</li></ul><h3 id="快速失败"><a href="#快速失败" class="headerlink" title="快速失败"></a>快速失败</h3><ul><li>快速失败,而非缓慢响应</li></ul><h3 id="任其崩溃并替换"><a href="#任其崩溃并替换" class="headerlink" title="任其崩溃并替换"></a>任其崩溃并替换</h3><ul><li>通过组件崩溃保护系统。</li><li>快速重启和重启归队。</li></ul><h3 id="握手"><a href="#握手" class="headerlink" title="握手"></a>握手</h3><ul><li>创建基于合作的需求控制机制。</li><li>考虑健康状况检查</li></ul><h3 id="考验机"><a href="#考验机" class="headerlink" title="考验机"></a>考验机</h3><ul><li>优秀的考验机可以让你模拟现实世界中的各种混乱的系统失效方式。</li></ul><h3 id="中间件解耦"><a href="#中间件解耦" class="headerlink" title="中间件解耦"></a>中间件解耦</h3><ul><li>从同步的“请求回复”到异步的通信方式的转变,需要完全不同的设计。此时就需要考虑转换成本。</li></ul><h3 id="卸下负载"><a href="#卸下负载" class="headerlink" title="卸下负载"></a>卸下负载</h3><ul><li>通过卸下负载避免响应缓慢。</li></ul><h3 id="背压机制"><a href="#背压机制" class="headerlink" title="背压机制"></a>背压机制</h3><ul><li>背压机制通过让消费者放慢工作来实现安全性。<ul><li>消费者的处理速度终究会减慢,此时唯一能做的就是让消费者“提醒”提供者,不要过 快地发送请求。</li></ul></li><li>在系统边界内运用背压机制。<ul><li>如果是跨越系统边界的情况,就要换用卸下负载模式,当用户群是整个互联网时更应如此。</li></ul></li><li>要想获得有限的响应时间,就需要构建有限长度的等待队列。<ul><li>当等待队列已满时只有以下选择(虽然都不令人愉悦):丢弃数据,拒绝工作或将其阻塞。消费者必须当心,不要永久阻塞。</li></ul></li></ul><h3 id="调速器"><a href="#调速器" class="headerlink" title="调速器"></a>调速器</h3><ul><li>放慢自动化工具的工作速度,以便人工干预。</li><li>在不安全的方向上施加阻力。</li><li>考虑使用响应曲线。</li></ul>]]></content>
<tags>
<tag>sre</tag>
<tag>stability</tag>
</tags>
</entry>
<entry>
<title>LIVENESS PROBE 的问题</title>
<link href="/posts/liveness-probe-problem/"/>
<url>/posts/liveness-probe-problem/</url>
<content type="html"><![CDATA[<h2 id="Readiness-and-Liveness-Probes"><a href="#Readiness-and-Liveness-Probes" class="headerlink" title="Readiness and Liveness Probes"></a>Readiness and Liveness Probes</h2><p>Kubernetes 提供了两种功能 <a href="https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/" target="_blank" rel="noopener">Readiness and Liveness Probes</a>,它们可以定期执行操作(例如发出 HTTP 请求,打开 TCP 连接或在您的容器中运行命令),以确认您的应用程序按预期工作。</p><p>Kubernetes 使用 Readiness Probes 来了解容器何时准备开始接受流量。当 Pod 的所有容器都准备就绪时,即视为准备就绪。该功能的一种用法是控制将哪些 Pod 用作 Kubernetes Services(尤其是Ingress)的后端。</p><p>Kubernetes 使用 Liveness Probes 知道何时重新启动容器。例如,Liveness Probes 可能会遇到一个死锁,即应用程序 Hang 死了。在存在这种状态的情况下重新启动容器可以帮助使应用程序在存在错误的情况下快速恢复,但是重新启动也可能导致级联失败。</p><h3 id="Example"><a href="#Example" class="headerlink" title="Example"></a>Example</h3><p>Readiness Probe 通过默认设置(间隔:10 秒,超时:1 秒,成功阈值:1,故障阈值:3)通过 HTTP 访问 /health 路径:</p><pre><code class="hljs yaml"><span class="hljs-comment"># part of a larger deployment/stack definition</span><span class="hljs-attr">podTemplate:</span> <span class="hljs-attr">spec:</span> <span class="hljs-attr">containers:</span> <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">my-container</span> <span class="hljs-comment"># ...</span> <span class="hljs-attr">readinessProbe:</span> <span class="hljs-attr">httpGet:</span> <span class="hljs-attr">path:</span> <span class="hljs-string">/health</span> <span class="hljs-attr">port:</span> <span class="hljs-number">8080</span></code></pre><h2 id="最佳实践"><a href="#最佳实践" class="headerlink" title="最佳实践"></a>最佳实践</h2><h3 id="应该做的"><a href="#应该做的" class="headerlink" title="应该做的"></a>应该做的</h3><ul><li>对于提供 HTTP endpoint 的微服务来说,请始终定义一个 Readiness Probes,以检查您的应用程序(Pod)是否已准备好接收流量</li><li>确保 Readiness Probes 覆盖了实际 Web 服务器端口的就绪状态<ul><li>使用 admin 或 management 端口(例如 9090)进行 readinessProbe 时,请确保只有在主 HTTP 端口(例如 8080)准备好接受流量时,端点才返回 OK</li><li>为 Readiness Probes 设置不同的端口,一般不能检测主端口上的线程拥塞问题(主服务器线程池已满,但运行状况检查仍然可以确定)</li></ul></li><li>确保 Readiness Probes 包括数据库初始化/迁移<ul><li>仅数据库初始化完成之后启动HTTP服务器侦听</li></ul></li><li>使用 <code>httpGet</code> 来探测 Readiness Probes 设置的检测地址(例如 /health)</li><li>了解探针的默认行为(间隔:10s,超时:1s,successThreshold:1,failureThreshold:3)<ul><li>默认值表示 Pod 将在约 30 秒后变为未就绪状态(3 次运行状况检查失败)</li></ul></li><li>如果您的技术栈(例如 Java/Spring)允许此端口将管理运行状况和指标与正常流量分开,请使用其他 admin 或 management 端口</li><li>您可以根据需要使用 Readiness Probe 进行预热/缓存加载,并返回503 状态代码,直到应用容器预热完成</li></ul><h3 id="不应该做的"><a href="#不应该做的" class="headerlink" title="不应该做的"></a>不应该做的</h3><ul><li>Readiness/Liveness Probes 不依赖外部依赖项(例如数据存储),因为这可能会导致级联失败<ul><li>例如,具有 10 个 Pod 的有状态 REST 服务,它依赖于一个 PostgresSQL 数据库:当您的探针依赖于一个有效的数据库连接时,如果数据库/网络出现抖动,则所有 10 个 Pod 都将 down,这通常会使影响变得更糟</li><li>请注意,Spring Data 的默认行为是检查数据库连接</li><li>在这种情况下,“外部”也可能意味着同一应用程序的其他 Pod,即,理想情况下,您的探针不应依赖于同一集群中其他 Pod 的状态以防止级联故障</li></ul></li><li>除非您了解后果以及为什么需要 Liveness Probes,否则请勿对您的 Pods 使用 Liveness Probes<ul><li>Liveness Probes 可以帮助恢复“卡住”的容器,但是当您完全拥有应用程序时,不应期望“卡住”进程和死锁之类的东西-一种更好的选择是故意崩溃以恢复到已知的良好状态</li><li>Liveness Probes 失败将导致容器重新启动,从而有可能使与负载相关的错误的影响更严重:容器重新启动将导致停机(至少您的应用启动时间,例如 30s+),从而导致其他容器接受更多的流量,导致更多的错误</li><li>Liveness Probes 与外部依赖项相结合是导致连锁故障的最坏情况:单个数据库故障将重新启动所有容器</li></ul></li><li>如果您使用 Liveness Probes ,请不要和 Readiness Probes 设置相同的规范<ul><li>您可以使用具有相同运行状况检查的 Liveness Probes,但故障阈值较高(例如,在 3 次尝试后标记为未就绪,而在 10 次尝试后标记为 Liveness Probes 失败)</li></ul></li><li>不要使用 <code>exec</code> 探针,因为已知的问题会导致僵尸进程<ul><li><a href="https://www.youtube.com/watch?v=QKI-JRs2RIE" target="_blank" rel="noopener">failure stories by Datadog</a></li></ul></li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ul><li>使用适用于您的 Web 应用程序的 Readiness Probes 来确定 Pod 何时应接收流量</li><li>仅在有真实的用例的情况下才使用 Liveness Probes</li><li>错误使用 Readiness/Liveness Probes 可能会导致可用性降低和级联故障</li></ul>]]></content>
<tags>
<tag>cloud-native</tag>
<tag>best-practive</tag>
</tags>
</entry>
<entry>
<title>Replacing iptables with eBPF in Kubernetes with Cilium</title>
<link href="/posts/cilium/"/>
<url>/posts/cilium/</url>
<content type="html"><![CDATA[<h2 id="Why-Cilium-is-awesome"><a href="#Why-Cilium-is-awesome" class="headerlink" title="Why Cilium is awesome?"></a>Why Cilium is awesome?</h2><ul><li>It makes disadvantages of iptables disappear. And always gets the best from the Linux kernel.</li><li>Cluster Mesh / multi-cluster.</li><li>Makes Istio faster.</li><li>Offers L7 API Aware filtering as a Kubernetes resource.</li><li>Integrates with the other popular CNI plugins – Calico, Flannel, Weave, Lyft, AWS CNI.</li></ul><object data="./Cilium_FOSDEM_2020.pdf" type="application/pdf" width="100%" height="877px">]]></content>
<tags>
<tag>network</tag>
<tag>cloud-native</tag>
</tags>
</entry>
<entry>
<title>谈谈 ACID、CAP 和 BASE</title>
<link href="/posts/acid-cap-base/"/>
<url>/posts/acid-cap-base/</url>
<content type="html"><![CDATA[<h2 id="背景知识"><a href="#背景知识" class="headerlink" title="背景知识"></a>背景知识</h2><h3 id="ACID"><a href="#ACID" class="headerlink" title="ACID"></a>ACID</h3><p>ACID 是指数据库管理系统在写入或更新资料的过程中,为保证事务(transaction)是正确可靠的,所必须具备的四个特性:</p><ul><li>原子性(atomicity)</li><li>一致性(consistency)</li><li>隔离性(isolation)</li><li>持久性(durability)</li></ul><h3 id="CAP"><a href="#CAP" class="headerlink" title="CAP"></a>CAP</h3><p>来自 Berkerly 的 Eric Brewer 教授提出了一个著名的 <a href="https://en.wikipedia.org/wiki/CAP_theorem" target="_blank" rel="noopener">CAP</a> 理论:一致性(Consistency),可用性(Availability)以及分区容忍性(Partition tolerance)三者不能同时满足。</p><ul><li>一致性:对某个指定的客户端来说,读操作能返回最新的写操作。对于数据分布在不同节点上的数据上来说,如果在某个节点更新了数据,那么在其他节点如果都能读取到这个最新的数据,那么就称为强一致,如果有某个节点没有读取到,那就是分布式不一致;</li><li>可用性:非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)。可用性的两个关键一个是合理的时间,一个是合理的响应。合理的时间指的是请求不能无限被阻塞,应该在合理的时间给出返回。合理的响应指的是系统应该明确返回结果并且结果是正确的,这里的正确指的是比如应该返回 50,而不是返回 40。</li><li>分区容忍性:当出现网络分区后,系统能够继续工作。打个比方,这里个集群有多台机器,有台机器网络出现了问题,但是这个集群仍然可以正常工作。</li></ul><h3 id="BASE"><a href="#BASE" class="headerlink" title="BASE"></a>BASE</h3><p>Eric Brewer 在 1997 发表的论文 <a href="https://courses.cs.washington.edu/courses/cse454/13wi/papers/cluster_scalable.pdf" target="_blank" rel="noopener">Cluster-Based Scalable Network Services</a> 中第一次提出 BASE 的概念;eBay 的架构师 Dan Pritchett 在 2008 年发表文章 <a href="http://dl.acm.org/citation.cfm?id=1394128" target="_blank" rel="noopener">BASE: An AcidAlternative</a> 中第一次明确提出的 BASE 理论。</p><p>BASE 是 Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)三个短语的简写。</p><p>BASE 是对 CAP 中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于 CAP 定理逐步演化而来的,其核心思想是即使无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。</p><h4 id="基本可用"><a href="#基本可用" class="headerlink" title="基本可用"></a>基本可用</h4><p>基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性:</p><ul><li>响应时间上的损失</li><li>功能上的损失(降级页面)</li></ul><h4 id="弱状态"><a href="#弱状态" class="headerlink" title="弱状态"></a>弱状态</h4><p>弱状态也称为软状态,和硬状态相对,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。</p><h4 id="最终一致性"><a href="#最终一致性" class="headerlink" title="最终一致性"></a>最终一致性</h4><p>最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。</p><h2 id="为什么要谈-ACID"><a href="#为什么要谈-ACID" class="headerlink" title="为什么要谈 ACID"></a>为什么要谈 ACID</h2><p>BASE 和 CAP 的提出就是体系架构从单机到分布式的大背景下。</p><p><a href="http://dl.acm.org/citation.cfm?id=1394128" target="_blank" rel="noopener">BASE: An AcidAlternative</a> 文章的背景是讨论数据库分片对分布式事务的需求。文章使用 2PC (两阶段提交)来提供跨越多个数据库实例的 ACID 保证。但是引入 2PC 的直接影响就是可用性下降。假设数据分布在两个数据库实例上,每个数据库实例的可用性是 99.9%,那么数据库分片后可用性为:</p><blockquote><p>100−((100−99.9)+(100−99.9))=99.8</p></blockquote><p>作为商用软件,可用性下降是不可容忍的。因此,作者才引出了 CAP 理论,进而提出 BASE,通过放松 ACID 的严格一致性,获得系统可用性和可扩展性的提升。</p><p>我认为, 目前所有在讨论 CAP 的时候带上 ACID,一方面想说明在分布式环境下,必须在数据一致性和可用性之间做出取舍;另一方面,可能就是想单纯装一下。</p><h2 id="CAP-理论的”三选二”"><a href="#CAP-理论的”三选二”" class="headerlink" title="CAP 理论的”三选二”"></a>CAP 理论的”三选二”</h2><p><img src="cap.png" srcset="/img/loading.gif" alt="图 3 "></p><p>我们把 C、A、P 两两组合起来,可以得到关注点不同的系统:</p><ul><li>CA:这样的系统关注一致性和可用性,它需要非常严格的全体一致性协议,比如上文提到的”两段提交”(2PC)。CA 系统不能容忍网络错误或者节点错误,一旦出现这样的问题,整个系统就会拒绝写请求,因为它并不知道是对面的那个节点宕机还是网络错误。唯一安全的做法就是把自己变成只读的。</li><li>CP:这样的系统关注一致性和分区容忍性。它关注的是系统里大多数人的一致性协议,比如:Paxos 算法 (Quorum 类的算法)。这样的系统只需要保证大多数节点数据一直,而少数的节点会在没有同步到最新版数据时变成不可用的状态。这样能够提供一部分的可用性。</li><li>AP:这样的系统关心可用性和分区容忍性。因此,这样的系统不能达成一致性,需要给出数据冲突,给出数据冲突就需要维护数据版本(Dynamo)。</li></ul><h2 id="CAP-的误解"><a href="#CAP-的误解" class="headerlink" title="CAP 的误解"></a>CAP 的误解</h2><p>现在很多人在进行分布式架构设计时言必谈 CAP,但是还是有很多人对 CAP 理论有误解,连 CAP 理论的作者都直言 CAP 理论的”三选二”约束一直存在着误导性。</p><p>这个约束过分简单化了各性质之间的相互关系。我们有必要辨析其中的细节。因此,我们对自己提出两个问题:</p><ul><li>P是必选项吗?</li><li>CA一定要二选一吗?</li></ul><h3 id="P-是必选项吗"><a href="#P-是必选项吗" class="headerlink" title="P 是必选项吗"></a>P 是必选项吗</h3><p>在分布式系统中,分区是由于网络问题或节点宕机导致的。这就导致程序员们就直面了一种状况:分区不会频繁出现,但是一定会出现。因此分布式系统的分区容忍性是必选项。</p><p>对于分布式系统工程实践,CAP 理论更合适的描述是:在满足分区容错的前提下,没有算法能同时满足数据一致性和服务可用性。</p><h3 id="CA-何如取舍"><a href="#CA-何如取舍" class="headerlink" title="CA 何如取舍"></a>CA 何如取舍</h3><p>但是由于分区很少发生,那么在系统不存在分区的情况下牺牲 C 或 A 我们都会觉得很心疼,怎么办呢?</p><p>CAP 定理证明中的一致性指线性一致性,即强一致性。强一致性要求多节点组成的被调要能像单节点一样运作、操作具备原子性,数据在时间、时序上都有要求。如果放宽这些要求,还有其他一致性类型:</p><ul><li>序列一致性(sequential consistency):不要求时序一致,A 操作先于 B 操作,在 B 操作后如果所有调用端读操作得到 A 操作的结果,满足序列一致性;</li><li>最终一致性(eventual consistency):放宽对时间的要求,在被调完成操作响应后的某个时间点,被调多个节点的数据最终达成一致。</li></ul><p>可用性在 CAP 定理里指所有读写操作必须要能终止,实际应用中从主调、被调两个不同的视角,可用性具有不同的含义。当网络分区出现时,主调可以只支持读操作,通过牺牲部分可用性达成数据一致。</p><p>工程实践中,较常见的做法是通过异步拷贝副本(asynchronous replication)、Quorum/NRW,实现在调用端看来数据强一致、被调端最终一致,在调用端看来服务可用、被调端允许部分节点不可用(或被网络分隔)的效果。</p><p>CAP 理论的这三种性质都可以在程度上衡量,并不是非黑即白的有或无。可用性显然是在 0% 到 100% 之间连续变化的,一致性分很多级别,连分区也可以细分为不同含义,如系统内的不同部分对于是否存在分区可以有不一样的认知。CAP 实践应将目标定为针对具体的应用,在合理范围内最大化数据一致性和可用性。</p><p>这样的思路延伸为如何规划分区期间的操作和分区之后的恢复,从而启架构师和程序员加深对 CAP 的认识,跳出由于 CAP 理论的表述而产生的思维局限。</p><h3 id="无法忽略的网络延迟"><a href="#无法忽略的网络延迟" class="headerlink" title="无法忽略的网络延迟"></a>无法忽略的网络延迟</h3><p>CAP 理论的经典解释,是忽略网络延迟的,但在实际中延迟和分区紧密相关。CAP 从理论到实践落地的场景是如何在出现分区时对待已发生的操作:</p><ul><li>降低系统的可用性取消操作;</li><li>冒着系统损失一致性的风险继续操作。</li></ul><p>依靠多次尝试通信的方法来达到一致性(比如 Paxos 算法或者两阶段事务提交),仅仅是推迟了决策的时间,系统终究要做一个决定。无限期地尝试下去,本身就是选择一致性牺牲可用性的表现。</p><p>因此以实际效果而言,分区相当于对通信提出的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在 C 和 A 之间做出选择。这就从延迟的角度抓住了设计的核心问题:分区两侧是否在无通信的情况下继续其操作?</p><p>从这个实用的观察角度出发可以导出若干重要的推论:</p><ul><li>分区并不是全体节点的一致见解,因为有些节点检测到了分区,有些可能没有;</li><li>检测到分区的节点即进入分区模式——这是优化 C 和 A 的核心环节。</li><li>系统设计者可以根据期望中的响应时间设置时限。但是需要注意:时限越短,系统进入分区模式越频繁,其中有些时候并不一定真的发生了分区的情况,可能只是网络变慢而已。</li></ul><p>因此,有时候在跨区域的系统中放弃强一致性来避免保持数据一致所带来的高延迟是非常有意义的。</p><h3 id="跳出-CAP"><a href="#跳出-CAP" class="headerlink" title="跳出 CAP"></a>跳出 CAP</h3><p>CAP 理论的这三种性质都可以在程度上衡量,并不是非黑即白的有或无。可用性显然是在 0% 到 100% 之间连续变化的,一致性分很多级别,连分区也可以细分为不同含义,如系统内的不同部分对于是否存在分区可以有不一样的认知。CAP 实践应将目标定为针对具体的应用,在合理范围内最大化数据一致性和可用性。</p><p>因为在分区没有出现的时候,我们完全不需要考虑分区容忍性,可以选择 CA;当分区出现之后,我们可以根据需求在 C 和 A 之间进行取舍。</p><p>因此,思路延伸为如何规划分区期间的操作和分区之后的恢复,从而启架构师和程序员加深对 CAP 的认识,跳出由于 CAP 理论的表述而产生的思维局限。例如,Oracle数据库的DataGuard复制组件包含三种模式:</p><ul><li>最大保护模式(Maximum Protection):即强同步复制模式,写操作要求主库先将操作日志(数据库的 redo/undo 日志)同步到至少一个备库才可以返回客户端成功。这种模式保证即使主库出现无法恢复的故障,比如硬盘损坏,也不会丢失数据;</li><li>最大性能模式(Maximum Performance):即异步复制模式,写操作只需要在主库上执行成功就可以返回客户端成功,主库上的后台线程会将重做日志通过异步的方式复制到备库。这种方式保证了性能及可用性,但是可能丢失数据;</li><li>最大可用性模式(Maximum Availability):上述两种模式的折衷。正常情况下相当于最大保护模式,如果主备之间的网络出现故障,切换为最大性能模式。</li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>当系统中存在分区,不应该盲目地牺牲一致性或可用性。运用以上讨论的方法,通过细致地管理分区期间的不变性约束,两方面的性质都可以取得最佳的表现。</p><p>对理论的讨论就到这里为止,但是从理论到实践的落地还有很多工作要做。引入 CAP 实践毕竟不像引入 ACID 事务那么简单,实施的时候需要对过去的策略进行全面的考虑,最佳的实施方案极大地依赖于具体服务的不变性约束和操作细节。多研究现有的优秀分布式系统,分析其设计理念和对 CAP 的实现,可以更快地成长。</p>]]></content>
<tags>
<tag>distributed-system</tag>
</tags>
</entry>
<entry>
<title>Raft 协议介绍</title>
<link href="/posts/raft/"/>
<url>/posts/raft/</url>
<content type="html"><![CDATA[<h1 id="寻找一种易于理解的一致性算法(扩展版)"><a href="#寻找一种易于理解的一致性算法(扩展版)" class="headerlink" title="寻找一种易于理解的一致性算法(扩展版)"></a>寻找一种易于理解的一致性算法(扩展版)</h1><p>论文: <a href="https://ramcloud.atlassian.net/wiki/download/attachments/6586375/raft.pdf" target="_blank" rel="noopener">In Search of an Understandable Consensus Algorithm (Extended Version)</a></p><h2 id="摘要"><a href="#摘要" class="headerlink" title="摘要"></a>摘要</h2><p>Raft 是一种为了管理复制日志的一致性算法。它提供了和 Paxos 算法相同的功能和性能,但是它的算法结构和 Paxos 不同,使得 Raft 算法更加容易理解并且更容易构建实际的系统。为了提升可理解性,Raft 将一致性算法分解成了几个关键模块,例如领导人选举、日志复制和安全性。同时它通过实施一个更强的一致性来减少需要考虑的状态的数量。从一个用户研究的结果可以证明,对于学生而言,Raft 算法比 Paxos 算法更加容易学习。Raft 算法还包括一个新的机制来允许集群成员的动态改变,它利用重叠的大多数来保证安全性。</p><h2 id="1-介绍"><a href="#1-介绍" class="headerlink" title="1 介绍"></a>1 介绍</h2><p>一致性算法允许一组机器像一个整体一样工作,即使其中一些机器出现故障也能够继续工作下去。正因为如此,一致性算法在构建可信赖的大规模软件系统中扮演着重要的角色。在过去的 10 年里,Paxos 算法统治着一致性算法这一领域:绝大多数的实现都是基于 Paxos 或者受其影响。同时 Paxos 也成为了教学领域里讲解一致性问题时的示例。</p><p>但是不幸的是,尽管有很多工作都在尝试降低它的复杂性,但是 Paxos 算法依然十分难以理解。并且,Paxos 自身的算法结构需要进行大幅的修改才能够应用到实际的系统中。这些都导致了工业界和学术界都对 Paxos 算法感到十分头疼。</p><p>和 Paxos 算法进行过努力之后,我们开始寻找一种新的一致性算法,可以为构建实际的系统和教学提供更好的基础。我们的做法是不寻常的,我们的首要目标是可理解性:我们是否可以在实际系统中定义一个一致性算法,并且能够比 Paxos 算法以一种更加容易的方式来学习。此外,我们希望该算法方便系统构建者的直觉的发展。不仅一个算法能够工作很重要,而且能够显而易见的知道为什么能工作也很重要。</p><p>Raft 一致性算法就是这些工作的结果。在设计 Raft 算法的时候,我们使用一些特别的技巧来提升它的可理解性,包括算法分解(Raft 主要被分成了领导人选举,日志复制和安全三个模块)和减少状态机的状态(相对于 Paxos,Raft 减少了非确定性和服务器互相处于非一致性的方式)。一份针对两所大学 43 个学生的研究表明 Raft 明显比 Paxos 算法更加容易理解。在这些学生同时学习了这两种算法之后,和 Paxos 比起来,其中 33 个学生能够回答有关于 Raft 的问题。</p><p>Raft 算法在许多方面和现有的一致性算法都很相似(主要是 Oki 和 Liskov 的 Viewstamped Replication),但是它也有一些独特的特性:</p><ul><li><strong>强领导者</strong>:和其他一致性算法相比,Raft 使用一种更强的领导能力形式。比如,日志条目只从领导者发送给其他的服务器。这种方式简化了对复制日志的管理并且使得 Raft 算法更加易于理解。</li><li><strong>领导选举</strong>:Raft 算法使用一个随机计时器来选举领导者。这种方式只是在任何一致性算法都必须实现的心跳机制上增加了一点机制。在解决冲突的时候会更加简单快捷。</li><li><strong>成员关系调整</strong>:Raft 使用一种共同一致的方法来处理集群成员变换的问题,在这种方法下,处于调整过程中的两种不同的配置集群中大多数机器会有重叠,这就使得集群在成员变换的时候依然可以继续工作。</li></ul><p>我们相信,Raft 算法不论出于教学目的还是作为实践项目的基础都是要比 Paxos 或者其他一致性算法要优异的。它比其他算法更加简单,更加容易理解;它的算法描述足以实现一个现实的系统;它有好多开源的实现并且在很多公司里使用;它的安全性已经被证明;它的效率和其他算法比起来也不相上下。</p><p>接下来,这篇论文会介绍以下内容:复制状态机问题(第 2 节),讨论 Paxos 的优点和缺点(第 3 节),讨论我们为了可理解性而采取的方法(第 4 节),阐述 Raft 一致性算法(第 5-8 节),评价 Raft 算法(第 9 节),以及一些相关的工作(第 10 节)。</p><h2 id="2-复制状态机"><a href="#2-复制状态机" class="headerlink" title="2 复制状态机"></a>2 复制状态机</h2><p>一致性算法是从复制状态机的背景下提出的(参考英文原文引用37)。在这种方法中,一组服务器上的状态机产生相同状态的副本,并且在一些机器宕掉的情况下也可以继续运行。复制状态机在分布式系统中被用于解决很多容错的问题。例如,大规模的系统中通常都有一个集群领导者,像 GFS、HDFS 和 RAMCloud,典型应用就是一个独立的的复制状态机去管理领导选举和存储配置信息并且在领导人宕机的情况下也要存活下来。比如 Chubby 和 ZooKeeper。</p><p><img src="raft-%E5%9B%BE1.png" srcset="/img/loading.gif" alt="图 1 "></p><blockquote><p>图 1 :复制状态机的结构。一致性算法管理着来自客户端指令的复制日志。状态机从日志中处理相同顺序的相同指令,所以产生的结果也是相同的。</p></blockquote><p>复制状态机通常都是基于复制日志实现的,如图 1。每一个服务器存储一个包含一系列指令的日志,并且按照日志的顺序进行执行。每一个日志都按照相同的顺序包含相同的指令,所以每一个服务器都执行相同的指令序列。因为每个状态机都是确定的,每一次执行操作都产生相同的状态和同样的序列。</p><p>保证复制日志相同就是一致性算法的工作了。在一台服务器上,一致性模块接收客户端发送来的指令然后增加到自己的日志中去。它和其他服务器上的一致性模块进行通信来保证每一个服务器上的日志最终都以相同的顺序包含相同的请求,尽管有些服务器会宕机。一旦指令被正确的复制,每一个服务器的状态机按照日志顺序处理他们,然后输出结果被返回给客户端。因此,服务器集群看起来形成一个高可靠的状态机。</p><p>实际系统中使用的一致性算法通常含有以下特性:</p><ul><li>安全性保证(绝对不会返回一个错误的结果):在非拜占庭错误情况下,包括网络延迟、分区、丢包、冗余和乱序等错误都可以保证正确。</li><li>可用性:集群中只要有大多数的机器可运行并且能够相互通信、和客户端通信,就可以保证可用。因此,一个典型的包含 5 个节点的集群可以容忍两个节点的失败。服务器被停止就认为是失败。他们当有稳定的存储的时候可以从状态中恢复回来并重新加入集群。</li><li>不依赖时序来保证一致性:物理时钟错误或者极端的消息延迟只有在最坏情况下才会导致可用性问题。</li><li>通常情况下,一条指令可以尽可能快的在集群中大多数节点响应一轮远程过程调用时完成。小部分比较慢的节点不会影响系统整体的性能。</li></ul><h2 id="3-Paxos-算法的问题"><a href="#3-Paxos-算法的问题" class="headerlink" title="3 Paxos 算法的问题"></a>3 Paxos 算法的问题</h2><p>在过去的 10 年里,Leslie Lamport 的 Paxos 算法几乎已经成为一致性的代名词:Paxos 是在课程教学中最经常使用的算法,同时也是大多数一致性算法实现的起点。Paxos 首先定义了一个能够达成单一决策一致的协议,比如单条的复制日志项。我们把这一子集叫做单决策 Paxos。然后通过组合多个 Paxos 协议的实例来促进一系列决策的达成。Paxos 保证安全性和活性,同时也支持集群成员关系的变更。Paxos 的正确性已经被证明,在通常情况下也很高效。</p><p>不幸的是,Paxos 有两个明显的缺点。第一个缺点是 Paxos 算法特别的难以理解。完整的解释是出了名的不透明;通过极大的努力之后,也只有少数人成功理解了这个算法。因此,有了几次用更简单的术语来解释 Paxos 的尝试。尽管这些解释都只关注了单决策的子集问题,但依然很具有挑战性。在 2012 年 NSDI 的会议中的一次调查显示,很少有人对 Paxos 算法感到满意,甚至在经验老道的研究者中也是如此。我们自己也尝试去理解 Paxos;我们一直没能理解 Paxos 直到我们读了很多对 Paxos 的简化解释并且设计了我们自己的算法之后,这一过程花了近一年时间。</p><p>我们假设 Paxos 的不透明性来自它选择单决策问题作为它的基础。单决策 Paxos 是晦涩微妙的,它被划分成了两种没有简单直观解释和无法独立理解的情景。因此,这导致了很难建立起直观的感受为什么单决策 Paxos 算法能够工作。构成多决策 Paxos 增加了很多错综复杂的规则。我们相信,在多决策上达成一致性的问题(一份日志而不是单一的日志记录)能够被分解成其他的方式并且更加直接和明显。</p><p>Paxos算法的第二个问题就是它没有提供一个足够好的用来构建一个现实系统的基础。一个原因是还没有一种被广泛认同的多决策问题的算法。Lamport 的描述基本上都是关于单决策 Paxos 的;他简要描述了实施多决策 Paxos 的方法,但是缺乏很多细节。当然也有很多具体化 Paxos 的尝试,但是他们都互相不一样,和 Paxos 的概述也不同。例如 Chubby 这样的系统实现了一个类似于 Paxos 的算法,但是大多数的细节并没有被公开。</p><p>而且,Paxos 算法的结构也不是十分易于构建实践的系统;单决策分解也会产生其他的结果。例如,独立的选择一组日志条目然后合并成一个序列化的日志并没有带来太多的好处,仅仅增加了不少复杂性。围绕着日志来设计一个系统是更加简单高效的;新日志条目以严格限制的顺序增添到日志中去。另一个问题是,Paxos 使用了一种对等的点对点的方式作为它的核心(尽管它最终提议了一种弱领导人的方法来优化性能)。在只有一个决策会被制定的简化世界中是很有意义的,但是很少有现实的系统使用这种方式。如果有一系列的决策需要被制定,首先选择一个领导人,然后让他去协调所有的决议,会更加简单快速。</p><p>因此,实际的系统中很少有和 Paxos 相似的实践。每一种实现都是从 Paxos 开始研究,然后发现很多实现上的难题,再然后开发了一种和 Paxos 明显不一样的结构。这样是非常费时和容易出错的,并且理解 Paxos 的难度使得这个问题更加糟糕。Paxos 算法在理论上被证明是正确可行的,但是现实的系统和 Paxos 差别是如此的大,以至于这些证明没有什么太大的价值。下面来自 Chubby 实现非常典型:</p><blockquote><p>在Paxos算法描述和实现现实系统中间有着巨大的鸿沟。最终的系统建立在一种没有经过证明的算法之上。</p></blockquote><p>由于以上问题,我们认为 Paxos 算法既没有提供一个良好的基础给实践的系统,也没有给教学很好的帮助。基于一致性问题在大规模软件系统中的重要性,我们决定看看我们是否可以设计一个拥有更好特性的替代 Paxos 的一致性算法。Raft 算法就是这次实验的结果。</p><h2 id="4-为了可理解性的设计"><a href="#4-为了可理解性的设计" class="headerlink" title="4 为了可理解性的设计"></a>4 为了可理解性的设计</h2><p>设计 Raft 算法我们有几个初衷:它必须提供一个完整的实际的系统实现基础,这样才能大大减少开发者的工作;它必须在任何情况下都是安全的并且在大多数的情况下都是可用的;并且它的大部分操作必须是高效的。但是我们最重要也是最大的挑战是可理解性。它必须保证对于普遍的人群都可以十分容易的去理解。另外,它必须能够让人形成直观的认识,这样系统的构建者才能够在现实中进行必然的扩展。</p><p>在设计 Raft 算法的时候,有很多的点需要我们在各种备选方案中进行选择。在这种情况下,我们评估备选方案基于可理解性原则:解释各个备选方案有多大的难度(例如,Raft 的状态空间有多复杂,是否有微妙的暗示)?对于一个读者而言,完全理解这个方案和暗示是否容易?</p><p>我们意识到对这种可理解性分析上具有高度的主观性;尽管如此,我们使用了两种通常适用的技术来解决这个问题。第一个技术就是众所周知的问题分解:只要有可能,我们就将问题分解成几个相对独立的,可被解决的、可解释的和可理解的子问题。例如,Raft 算法被我们分成领导人选举,日志复制,安全性和角色改变几个部分。</p><p>我们使用的第二个方法是通过减少状态的数量来简化需要考虑的状态空间,使得系统更加连贯并且在可能的时候消除不确定性。特别的,所有的日志是不允许有空洞的,并且 Raft 限制了日志之间变成不一致状态的可能。尽管在大多数情况下我们都试图去消除不确定性,但是也有一些情况下不确定性可以提升可理解性。尤其是,随机化方法增加了不确定性,但是他们有利于减少状态空间数量,通过处理所有可能选择时使用相似的方法。我们使用随机化去简化 Raft 中领导人选举算法。</p><h2 id="5-Raft-一致性算法"><a href="#5-Raft-一致性算法" class="headerlink" title="5 Raft 一致性算法"></a>5 Raft 一致性算法</h2><p>Raft 是一种用来管理章节 2 中描述的复制日志的算法。图 2 为了参考之用,总结这个算法的简略版本,图 3 列举了这个算法的一些关键特性。图中的这些元素会在剩下的章节逐一介绍。</p><p>Raft 通过选举一个高贵的领导人,然后给予他全部的管理复制日志的责任来实现一致性。领导人从客户端接收日志条目,把日志条目复制到其他服务器上,并且当保证安全性的时候告诉其他的服务器应用日志条目到他们的状态机中。拥有一个领导人大大简化了对复制日志的管理。例如,领导人可以决定新的日志条目需要放在日志中的什么位置而不需要和其他服务器商议,并且数据都从领导人流向其他服务器。一个领导人可以宕机,可以和其他服务器失去连接,这时一个新的领导人会被选举出来。</p><p>通过领导人的方式,Raft 将一致性问题分解成了三个相对独立的子问题,这些问题会在接下来的子章节中进行讨论:</p><ul><li><strong>领导选举</strong>:一个新的领导人需要被选举出来,当现存的领导人宕机的时候(章节 5.2)</li><li><strong>日志复制</strong>:领导人必须从客户端接收日志然后复制到集群中的其他节点,并且强制要求其他节点的日志保持和自己相同。</li><li><strong>安全性</strong>:在 Raft 中安全性的关键是在图 3 中展示的状态机安全:如果有任何的服务器节点已经应用了一个确定的日志条目到它的状态机中,那么其他服务器节点不能在同一个日志索引位置应用一个不同的指令。章节 5.4 阐述了 Raft 算法是如何保证这个特性的;这个解决方案涉及到一个额外的选举机制(5.2 节)上的限制。</li></ul><p>在展示一致性算法之后,这一章节会讨论可用性的一些问题和计时在系统的作用。</p><p><strong>状态</strong>:</p><table><thead><tr><th>状态</th><th>所有服务器上持久存在的</th></tr></thead><tbody><tr><td>currentTerm</td><td>服务器最后一次知道的任期号(初始化为 0,持续递增)</td></tr><tr><td>votedFor</td><td>在当前获得选票的候选人的 Id</td></tr><tr><td>log[]</td><td>日志条目集;每一个条目包含一个用户状态机执行的指令,和收到时的任期号</td></tr></tbody></table><table><thead><tr><th>状态</th><th>所有服务器上经常变的</th></tr></thead><tbody><tr><td>commitIndex</td><td>已知的最大的已经被提交的日志条目的索引值</td></tr><tr><td>lastApplied</td><td>最后被应用到状态机的日志条目索引值(初始化为 0,持续递增)</td></tr></tbody></table><table><thead><tr><th>状态</th><th>在领导人里经常改变的 (选举后重新初始化)</th></tr></thead><tbody><tr><td>nextIndex[]</td><td>对于每一个服务器,需要发送给他的下一个日志条目的索引值(初始化为领导人最后索引值加一)</td></tr><tr><td>matchIndex[]</td><td>对于每一个服务器,已经复制给他的日志的最高索引值</td></tr></tbody></table><p><strong>附加日志 RPC</strong>:</p><p>由领导人负责调用来复制日志指令;也会用作heartbeat</p><table><thead><tr><th>参数</th><th>解释</th></tr></thead><tbody><tr><td>term</td><td>领导人的任期号</td></tr><tr><td>leaderId</td><td>领导人的 Id,以便于跟随者重定向请求</td></tr><tr><td>prevLogIndex</td><td>新的日志条目紧随之前的索引值</td></tr><tr><td>prevLogTerm</td><td>prevLogIndex 条目的任期号</td></tr><tr><td>entries[]</td><td>准备存储的日志条目(表示心跳时为空;一次性发送多个是为了提高效率)</td></tr><tr><td>leaderCommit</td><td>领导人已经提交的日志的索引值</td></tr></tbody></table><table><thead><tr><th>返回值</th><th>解释</th></tr></thead><tbody><tr><td>term</td><td>当前的任期号,用于领导人去更新自己</td></tr><tr><td>success</td><td>跟随者包含了匹配上 prevLogIndex 和 prevLogTerm 的日志时为真</td></tr></tbody></table><p>接收者实现:</p><ol><li>如果 <code>term < currentTerm</code> 就返回 false (5.1 节)</li><li>如果日志在 prevLogIndex 位置处的日志条目的任期号和 prevLogTerm 不匹配,则返回 false (5.3 节)</li><li>如果已经存在的日志条目和新的产生冲突(索引值相同但是任期号不同),删除这一条和之后所有的 (5.3 节)</li><li>附加日志中尚未存在的任何新条目</li><li>如果 <code>leaderCommit > commitIndex</code>,令 commitIndex 等于 leaderCommit 和 新日志条目索引值中较小的一个</li></ol><p><strong>请求投票 RPC</strong>:</p><p>由候选人负责调用用来征集选票(5.2 节)</p><table><thead><tr><th>参数</th><th>解释</th></tr></thead><tbody><tr><td>term</td><td>候选人的任期号</td></tr><tr><td>candidateId</td><td>请求选票的候选人的 Id</td></tr><tr><td>lastLogIndex</td><td>候选人的最后日志条目的索引值</td></tr><tr><td>lastLogTerm</td><td>候选人最后日志条目的任期号</td></tr></tbody></table><table><thead><tr><th>返回值</th><th>解释</th></tr></thead><tbody><tr><td>term</td><td>当前任期号,以便于候选人去更新自己的任期号</td></tr><tr><td>voteGranted</td><td>候选人赢得了此张选票时为真</td></tr></tbody></table><p>接收者实现:</p><ol><li>如果<code>term < currentTerm</code>返回 false (5.2 节)</li><li>如果 votedFor 为空或者为 candidateId,并且候选人的日志至少和自己一样新,那么就投票给他(5.2 节,5.4 节)</li></ol><p><strong>所有服务器需遵守的规则</strong>:</p><p>所有服务器:</p><ul><li>如果<code>commitIndex > lastApplied</code>,那么就 lastApplied 加一,并把<code>log[lastApplied]</code>应用到状态机中(5.3 节)</li><li>如果接收到的 RPC 请求或响应中,任期号<code>T > currentTerm</code>,那么就令 currentTerm 等于 T,并切换状态为跟随者(5.1 节)</li></ul><p>跟随者(5.2 节):</p><ul><li>响应来自候选人和领导者的请求</li><li>如果在超过选举超时时间的情况之前没有收到<strong>当前领导人</strong>(即该领导人的任期需与这个跟随者的当前任期相同)的心跳/附加日志,或者是给某个候选人投了票,就自己变成候选人</li></ul><p>候选人(5.2 节):</p><ul><li>在转变成候选人后就立即开始选举过程<ul><li>自增当前的任期号(currentTerm)</li><li>给自己投票</li><li>重置选举超时计时器</li><li>发送请求投票的 RPC 给其他所有服务器</li></ul></li><li>如果接收到大多数服务器的选票,那么就变成领导人</li><li>如果接收到来自新的领导人的附加日志 RPC,转变成跟随者</li><li>如果选举过程超时,再次发起一轮选举</li></ul><p>领导人:</p><ul><li>一旦成为领导人:发送空的附加日志 RPC(心跳)给其他所有的服务器;在一定的空余时间之后不停的重复发送,以阻止跟随者超时(5.2 节)</li><li>如果接收到来自客户端的请求:附加条目到本地日志中,在条目被应用到状态机后响应客户端(5.3 节)</li><li>如果对于一个跟随者,最后日志条目的索引值大于等于 nextIndex,那么:发送从 nextIndex 开始的所有日志条目:<ul><li>如果成功:更新相应跟随者的 nextIndex 和 matchIndex</li><li>如果因为日志不一致而失败,减少 nextIndex 重试</li></ul></li><li>如果存在一个满足<code>N > commitIndex</code>的 N,并且大多数的<code>matchIndex[i] ≥ N</code>成立,并且<code>log[N].term == currentTerm</code>成立,那么令 commitIndex 等于这个 N (5.3 和 5.4 节)</li></ul><p><img src="raft-%E5%9B%BE2.png" srcset="/img/loading.gif" alt="图 2 "></p><blockquote><p>图 2:一个关于 Raft 一致性算法的浓缩总结(不包括成员变换和日志压缩)。</p></blockquote><table><thead><tr><th>特性</th><th>解释</th></tr></thead><tbody><tr><td>选举安全特性</td><td>对于一个给定的任期号,最多只会有一个领导人被选举出来(5.2 节)</td></tr><tr><td>领导人只附加原则</td><td>领导人绝对不会删除或者覆盖自己的日志,只会增加(5.3 节)</td></tr><tr><td>日志匹配原则</td><td>如果两个日志在相同的索引位置的日志条目的任期号相同,那么我们就认为这个日志从头到这个索引位置之间全部完全相同(5.3 节)</td></tr><tr><td>领导人完全特性</td><td>如果某个日志条目在某个任期号中已经被提交,那么这个条目必然出现在更大任期号的所有领导人中(5.4 节)</td></tr><tr><td>状态机安全特性</td><td>如果一个领导人已经将给定的索引值位置的日志条目应用到状态机中,那么其他任何的服务器在这个索引位置不会应用一个不同的日志(5.4.3 节)</td></tr></tbody></table><p><img src="raft-%E5%9B%BE3.png" srcset="/img/loading.gif" alt="图 3 "></p><blockquote><p>图 3:Raft 在任何时候都保证以上的各个特性。</p></blockquote><h3 id="5-1-Raft-基础"><a href="#5-1-Raft-基础" class="headerlink" title="5.1 Raft 基础"></a>5.1 Raft 基础</h3><p>一个 Raft 集群包含若干个服务器节点;通常是 5 个,这允许整个系统容忍 2 个节点的失效。在任何时刻,每一个服务器节点都处于这三个状态之一:领导人、跟随者或者候选人。在通常情况下,系统中只有一个领导人并且其他的节点全部都是跟随者。跟随者都是被动的:他们不会发送任何请求,只是简单的响应来自领导者或者候选人的请求。领导人处理所有的客户端请求(如果一个客户端和跟随者联系,那么跟随者会把请求重定向给领导人)。第三种状态,候选人,是用来在 5.2 节描述的选举新领导人时使用。图 4 展示了这些状态和他们之间的转换关系;这些转换关系会在接下来进行讨论。</p><p><img src="raft-%E5%9B%BE4.png" srcset="/img/loading.gif" alt="图 4 "></p><blockquote><p>图 4:服务器状态。跟随者只响应来自其他服务器的请求。如果跟随者接收不到消息,那么他就会变成候选人并发起一次选举。获得集群中大多数选票的候选人将成为领导者。在一个任期内,领导人一直都会是领导人直到自己宕机了。</p></blockquote><p><img src="raft-%E5%9B%BE5.png" srcset="/img/loading.gif" alt="图 5"></p><blockquote><p>图 5:时间被划分成一个个的任期,每个任期开始都是一次选举。在选举成功后,领导人会管理整个集群直到任期结束。有时候选举会失败,那么这个任期就会没有领导人而结束。任期之间的切换可以在不同的时间不同的服务器上观察到。</p></blockquote><p>Raft 把时间分割成任意长度的<strong>任期</strong>,如图 5。任期用连续的整数标记。每一段任期从一次<strong>选举</strong>开始,就像章节 5.2 描述的一样,一个或者多个候选人尝试成为领导者。如果一个候选人赢得选举,然后他就在接下来的任期内充当领导人的职责。在某些情况下,一次选举过程会造成选票的瓜分。在这种情况下,这一任期会以没有领导人结束;一个新的任期(和一次新的选举)会很快重新开始。Raft 保证了在一个给定的任期内,最多只有一个领导者。</p><p>不同的服务器节点可能多次观察到任期之间的转换,但在某些情况下,一个节点也可能观察不到任何一次选举或者整个任期全程。任期在 Raft 算法中充当逻辑时钟的作用,这会允许服务器节点查明一些过期的信息比如陈旧的领导者。每一个节点存储一个当前任期号,这一编号在整个时期内单调的增长。当服务器之间通信的时候会交换当前任期号;如果一个服务器的当前任期号比其他人小,那么他会更新自己的编号到较大的编号值。如果一个候选人或者领导者发现自己的任期号过期了,那么他会立即恢复成跟随者状态。如果一个节点接收到一个包含过期的任期号的请求,那么他会直接拒绝这个请求。</p><p>Raft 算法中服务器节点之间通信使用远程过程调用(RPCs),并且基本的一致性算法只需要两种类型的 RPCs。请求投票(RequestVote) RPCs 由候选人在选举期间发起(章节 5.2),然后附加条目(AppendEntries)RPCs 由领导人发起,用来复制日志和提供一种心跳机制(章节 5.3)。第 7 节为了在服务器之间传输快照增加了第三种 RPC。当服务器没有及时的收到 RPC 的响应时,会进行重试, 并且他们能够并行的发起 RPCs 来获得最佳的性能。</p><h3 id="5-2-领导人选举"><a href="#5-2-领导人选举" class="headerlink" title="5.2 领导人选举"></a>5.2 领导人选举</h3><p>Raft 使用一种心跳机制来触发领导人选举。当服务器程序启动时,他们都是跟随者身份。一个服务器节点继续保持着跟随者状态只要他从领导人或者候选者处接收到有效的 RPCs。领导者周期性的向所有跟随者发送心跳包(即不包含日志项内容的附加日志项 RPCs)来维持自己的权威。如果一个跟随者在一段时间里没有接收到任何消息,也就是<strong>选举超时</strong>,那么他就会认为系统中没有可用的领导者,并且发起选举以选出新的领导者。</p><p>要开始一次选举过程,跟随者先要增加自己的当前任期号并且转换到候选人状态。然后他会并行的向集群中的其他服务器节点发送请求投票的 RPCs 来给自己投票。候选人会继续保持着当前状态直到以下三件事情之一发生:(a) 他自己赢得了这次的选举,(b) 其他的服务器成为领导者,(c) 一段时间之后没有任何一个获胜的人。这些结果会分别的在下面的段落里进行讨论。</p><p>当一个候选人从整个集群的大多数服务器节点获得了针对同一个任期号的选票,那么他就赢得了这次选举并成为领导人。每一个服务器最多会对一个任期号投出一张选票,按照先来先服务的原则(注意:5.4 节在投票上增加了一点额外的限制)。要求大多数选票的规则确保了最多只会有一个候选人赢得此次选举(图 3 中的选举安全性)。一旦候选人赢得选举,他就立即成为领导人。然后他会向其他的服务器发送心跳消息来建立自己的权威并且阻止新的领导人的产生。</p><p>在等待投票的时候,候选人可能会从其他的服务器接收到声明它是领导人的附加日志项 RPC。如果这个领导人的任期号(包含在此次的 RPC中)不小于候选人当前的任期号,那么候选人会承认领导人合法并回到跟随者状态。 如果此次 RPC 中的任期号比自己小,那么候选人就会拒绝这次的 RPC 并且继续保持候选人状态。</p><p>第三种可能的结果是候选人既没有赢得选举也没有输:如果有多个跟随者同时成为候选人,那么选票可能会被瓜分以至于没有候选人可以赢得大多数人的支持。当这种情况发生的时候,每一个候选人都会超时,然后通过增加当前任期号来开始一轮新的选举。然而,没有其他机制的话,选票可能会被无限的重复瓜分。</p><p>Raft 算法使用随机选举超时时间的方法来确保很少会发生选票瓜分的情况,就算发生也能很快的解决。为了阻止选票起初就被瓜分,选举超时时间是从一个固定的区间(例如 150-300 毫秒)随机选择。这样可以把服务器都分散开以至于在大多数情况下只有一个服务器会选举超时;然后他赢得选举并在其他服务器超时之前发送心跳包。同样的机制被用在选票瓜分的情况下。每一个候选人在开始一次选举的时候会重置一个随机的选举超时时间,然后在超时时间内等待投票的结果;这样减少了在新的选举中另外的选票瓜分的可能性。9.3 节展示了这种方案能够快速的选出一个领导人。</p><p>领导人选举这个例子,体现了可理解性原则是如何指导我们进行方案设计的。起初我们计划使用一种排名系统:每一个候选人都被赋予一个唯一的排名,供候选人之间竞争时进行选择。如果一个候选人发现另一个候选人拥有更高的排名,那么他就会回到跟随者状态,这样高排名的候选人能够更加容易的赢得下一次选举。但是我们发现这种方法在可用性方面会有一点问题(如果高排名的服务器宕机了,那么低排名的服务器可能会超时并再次进入候选人状态。而且如果这个行为发生得足够快,则可能会导致整个选举过程都被重置掉)。我们针对算法进行了多次调整,但是每次调整之后都会有新的问题。最终我们认为随机重试的方法是更加明显和易于理解的。</p><h3 id="5-3-日志复制"><a href="#5-3-日志复制" class="headerlink" title="5.3 日志复制"></a>5.3 日志复制</h3><p>一旦一个领导人被选举出来,他就开始为客户端提供服务。客户端的每一个请求都包含一条被复制状态机执行的指令。领导人把这条指令作为一条新的日志条目附加到日志中去,然后并行的发起附加条目 RPCs 给其他的服务器,让他们复制这条日志条目。当这条日志条目被安全的复制(下面会介绍),领导人会应用这条日志条目到它的状态机中然后把执行的结果返回给客户端。如果跟随者崩溃或者运行缓慢,再或者网络丢包,领导人会不断的重复尝试附加日志条目 RPCs (尽管已经回复了客户端)直到所有的跟随者都最终存储了所有的日志条目。</p><p><img src="raft-%E5%9B%BE6.png" srcset="/img/loading.gif" alt="图 6"></p><blockquote><p>图 6:日志由有序序号标记的条目组成。每个条目都包含创建时的任期号(图中框中的数字),和一个状态机需要执行的指令。一个条目当可以安全的被应用到状态机中去的时候,就认为是可以提交了。</p></blockquote><p>日志以图 6 展示的方式组织。每一个日志条目存储一条状态机指令和从领导人收到这条指令时的任期号。日志中的任期号用来检查是否出现不一致的情况,同时也用来保证图 3 中的某些性质。每一条日志条目同时也都有一个整数索引值来表明它在日志中的位置。</p><p>领导人来决定什么时候把日志条目应用到状态机中是安全的;这种日志条目被称为<strong>已提交</strong>。Raft 算法保证所有已提交的日志条目都是持久化的并且最终会被所有可用的状态机执行。在领导人将创建的日志条目复制到大多数的服务器上的时候,日志条目就会被提交(例如在图 6 中的条目 7)。同时,领导人的日志中之前的所有日志条目也都会被提交,包括由其他领导人创建的条目。5.4 节会讨论某些当在领导人改变之后应用这条规则的隐晦内容,同时他也展示了这种提交的定义是安全的。领导人跟踪了最大的将会被提交的日志项的索引,并且索引值会被包含在未来的所有附加日志 RPCs (包括心跳包),这样其他的服务器才能最终知道领导人的提交位置。一旦跟随者知道一条日志条目已经被提交,那么他也会将这个日志条目应用到本地的状态机中(按照日志的顺序)。</p><p>我们设计了 Raft 的日志机制来维护一个不同服务器的日志之间的高层次的一致性。这么做不仅简化了系统的行为也使得更加可预计,同时他也是安全性保证的一个重要组件。Raft 维护着以下的特性,这些同时也组成了图 3 中的日志匹配特性:</p><ul><li>如果在不同的日志中的两个条目拥有相同的索引和任期号,那么他们存储了相同的指令。</li><li>如果在不同的日志中的两个条目拥有相同的索引和任期号,那么他们之前的所有日志条目也全部相同。</li></ul><p>第一个特性来自这样的一个事实,领导人最多在一个任期里在指定的一个日志索引位置创建一条日志条目,同时日志条目在日志中的位置也从来不会改变。第二个特性由附加日志 RPC 的一个简单的一致性检查所保证。在发送附加日志 RPC 的时候,领导人会把新的日志条目紧接着之前的条目的索引位置和任期号包含在里面。如果跟随者在它的日志中找不到包含相同索引位置和任期号的条目,那么他就会拒绝接收新的日志条目。一致性检查就像一个归纳步骤:一开始空的日志状态肯定是满足日志匹配特性的,然后一致性检查保护了日志匹配特性当日志扩展的时候。因此,每当附加日志 RPC 返回成功时,领导人就知道跟随者的日志一定是和自己相同的了。</p><p>在正常的操作中,领导人和跟随者的日志保持一致性,所以附加日志 RPC 的一致性检查从来不会失败。然而,领导人崩溃的情况会使得日志处于不一致的状态(老的领导人可能还没有完全复制所有的日志条目)。这种不一致问题会在领导人和跟随者的一系列崩溃下加剧。图 7 展示了跟随者的日志可能和新的领导人不同的方式。跟随者可能会丢失一些在新的领导人中有的日志条目,他也可能拥有一些领导人没有的日志条目,或者两者都发生。丢失或者多出日志条目可能会持续多个任期。</p><p><img src="raft-%E5%9B%BE7.png" srcset="/img/loading.gif" alt="图 7"></p><blockquote><p>图 7:当一个领导人成功当选时,跟随者可能是任何情况(a-f)。每一个盒子表示是一个日志条目;里面的数字表示任期号。跟随者可能会缺少一些日志条目(a-b),可能会有一些未被提交的日志条目(c-d),或者两种情况都存在(e-f)。例如,场景 f 可能会这样发生,某服务器在任期 2 的时候是领导人,已附加了一些日志条目到自己的日志中,但在提交之前就崩溃了;很快这个机器就被重启了,在任期 3 重新被选为领导人,并且又增加了一些日志条目到自己的日志中;在任期 2 和任期 3 的日志被提交之前,这个服务器又宕机了,并且在接下来的几个任期里一直处于宕机状态。</p></blockquote><p>在 Raft 算法中,领导人处理不一致是通过强制跟随者直接复制自己的日志来解决了。这意味着在跟随者中的冲突的日志条目会被领导人的日志覆盖。5.4 节会阐述如何通过增加一些限制来使得这样的操作是安全的。</p><p>要使得跟随者的日志进入和自己一致的状态,领导人必须找到最后两者达成一致的地方,然后删除从那个点之后的所有日志条目,发送自己的日志给跟随者。所有的这些操作都在进行附加日志 RPCs 的一致性检查时完成。领导人针对每一个跟随者维护了一个 <strong>nextIndex</strong>,这表示下一个需要发送给跟随者的日志条目的索引地址。当一个领导人刚获得权力的时候,他初始化所有的 nextIndex 值为自己的最后一条日志的 index 加 1(图 7 中的 11)。如果一个跟随者的日志和领导人不一致,那么在下一次的附加日志 RPC 时的一致性检查就会失败。在被跟随者拒绝之后,领导人就会减小 nextIndex 值并进行重试。最终 nextIndex 会在某个位置使得领导人和跟随者的日志达成一致。当这种情况发生,附加日志 RPC 就会成功,这时就会把跟随者冲突的日志条目全部删除并且加上领导人的日志。一旦附加日志 RPC 成功,那么跟随者的日志就会和领导人保持一致,并且在接下来的任期里一直继续保持。</p><p>如果需要的话,算法可以通过减少被拒绝的附加日志 RPCs 的次数来优化。例如,当附加日志 RPC 的请求被拒绝的时候,跟随者可以包含冲突的条目的任期号和自己存储的那个任期的最早的索引地址。借助这些信息,领导人可以减小 nextIndex 越过所有那个任期冲突的所有日志条目;这样就变成每个任期需要一次附加条目 RPC 而不是每个条目一次。在实践中,我们十分怀疑这种优化是否是必要的,因为失败是很少发生的并且也不大可能会有这么多不一致的日志。</p><p>通过这种机制,领导人在获得权力的时候就不需要任何特殊的操作来恢复一致性。他只需要进行正常的操作,然后日志就能自动的在回复附加日志 RPC 的一致性检查失败的时候自动趋于一致。领导人从来不会覆盖或者删除自己的日志(图 3 的领导人只附加特性)。</p><p>日志复制机制展示出了第 2 节中形容的一致性特性:Raft 能够接受,复制并应用新的日志条目只要大部分的机器是工作的;在通常的情况下,新的日志条目可以在一次 RPC 中被复制给集群中的大多数机器;并且单个的缓慢的跟随者不会影响整体的性能。</p><h3 id="5-4-安全性"><a href="#5-4-安全性" class="headerlink" title="5.4 安全性"></a>5.4 安全性</h3><p>前面的章节里描述了 Raft 算法是如何选举和复制日志的。然而,到目前为止描述的机制并不能充分的保证每一个状态机会按照相同的顺序执行相同的指令。例如,一个跟随者可能会进入不可用状态同时领导人已经提交了若干的日志条目,然后这个跟随者可能会被选举为领导人并且覆盖这些日志条目;因此,不同的状态机可能会执行不同的指令序列。</p><p>这一节通过在领导选举的时候增加一些限制来完善 Raft 算法。这一限制保证了任何的领导人对于给定的任期号,都拥有了之前任期的所有被提交的日志条目(图 3 中的领导人完整特性)。增加这一选举时的限制,我们对于提交时的规则也更加清晰。最终,我们将展示对于领导人完整特性的简要证明,并且说明领导人完整性特性是如何引导复制状态机做出正确行为的。</p><h4 id="5-4-1-选举限制"><a href="#5-4-1-选举限制" class="headerlink" title="5.4.1 选举限制"></a>5.4.1 选举限制</h4><p>在任何基于领导人的一致性算法中,领导人都必须存储所有已经提交的日志条目。在某些一致性算法中,例如 Viewstamped Replication,某个节点即使是一开始并没有包含所有已经提交的日志条目,它也能被选为领导者。这些算法都包含一些额外的机制来识别丢失的日志条目并把他们传送给新的领导人,要么是在选举阶段要么在之后很快进行。不幸的是,这种方法会导致相当大的额外的机制和复杂性。Raft 使用了一种更加简单的方法,它可以保证所有之前的任期号中已经提交的日志条目在选举的时候都会出现在新的领导人中,不需要传送这些日志条目给领导人。这意味着日志条目的传送是单向的,只从领导人传给跟随者,并且领导人从不会覆盖自身本地日志中已经存在的条目。</p><p>Raft 使用投票的方式来阻止一个候选人赢得选举除非这个候选人包含了所有已经提交的日志条目。候选人为了赢得选举必须联系集群中的大部分节点,这意味着每一个已经提交的日志条目在这些服务器节点中肯定存在于至少一个节点上。如果候选人的日志至少和大多数的服务器节点一样新(这个新的定义会在下面讨论),那么他一定持有了所有已经提交的日志条目。请求投票 RPC 实现了这样的限制:RPC 中包含了候选人的日志信息,然后投票人会拒绝掉那些日志没有自己新的投票请求。</p><p>Raft 通过比较两份日志中最后一条日志条目的索引值和任期号定义谁的日志比较新。如果两份日志最后的条目的任期号不同,那么任期号大的日志更加新。如果两份日志最后的条目任期号相同,那么日志比较长的那个就更加新。</p><h4 id="5-4-2-提交之前任期内的日志条目"><a href="#5-4-2-提交之前任期内的日志条目" class="headerlink" title="5.4.2 提交之前任期内的日志条目"></a>5.4.2 提交之前任期内的日志条目</h4><p>如同 5.3 节介绍的那样,领导人知道一条当前任期内的日志记录是可以被提交的,只要它被存储到了大多数的服务器上。如果一个领导人在提交日志条目之前崩溃了,未来后续的领导人会继续尝试复制这条日志记录。然而,一个领导人不能断定一个之前任期里的日志条目被保存到大多数服务器上的时候就一定已经提交了。图 8 展示了一种情况,一条已经被存储到大多数节点上的老日志条目,也依然有可能会被未来的领导人覆盖掉。</p><p><img src="raft-%E5%9B%BE8.png" srcset="/img/loading.gif" alt="图 8"></p><blockquote><p>图 8:如图的时间序列展示了为什么领导人无法决定对老任期号的日志条目进行提交。在 (a) 中,S1 是领导者,部分的复制了索引位置 2 的日志条目。在 (b) 中,S1 崩溃了,然后 S5 在任期 3 里通过 S3、S4 和自己的选票赢得选举,然后从客户端接收了一条不一样的日志条目放在了索引 2 处。然后到 (c),S5 又崩溃了;S1 重新启动,选举成功,开始复制日志。在这时,来自任期 2 的那条日志已经被复制到了集群中的大多数机器上,但是还没有被提交。如果 S1 在 (d) 中又崩溃了,S5 可以重新被选举成功(通过来自 S2,S3 和 S4 的选票),然后覆盖了他们在索引 2 处的日志。反之,如果在崩溃之前,S1 把自己主导的新任期里产生的日志条目复制到了大多数机器上,就如 (e) 中那样,那么在后面任期里面这些新的日志条目就会被提交(因为 S5 就不可能选举成功)。 这样在同一时刻就同时保证了,之前的所有老的日志条目就会被提交。</p></blockquote><p>为了消除图 8 里描述的情况,Raft 永远不会通过计算副本数目的方式去提交一个之前任期内的日志条目。只有领导人当前任期里的日志条目通过计算副本数目可以被提交;一旦当前任期的日志条目以这种方式被提交,那么由于日志匹配特性,之前的日志条目也都会被间接的提交。在某些情况下,领导人可以安全的知道一个老的日志条目是否已经被提交(例如,该条目是否存储到所有服务器上),但是 Raft 为了简化问题使用一种更加保守的方法。</p><p>当领导人复制之前任期里的日志时,Raft 会为所有日志保留原始的任期号, 这在提交规则上产生了额外的复杂性。在其他的一致性算法中,如果一个新的领导人要重新复制之前的任期里的日志时,它必须使用当前新的任期号。Raft 使用的方法更加容易辨别出日志,因为它可以随着时间和日志的变化对日志维护着同一个任期编号。另外,和其他的算法相比,Raft 中的新领导人只需要发送更少日志条目(其他算法中必须在他们被提交之前发送更多的冗余日志条目来为他们重新编号)。</p><h4 id="5-4-3-安全性论证"><a href="#5-4-3-安全性论证" class="headerlink" title="5.4.3 安全性论证"></a>5.4.3 安全性论证</h4><p>在给定了完整的 Raft 算法之后,我们现在可以更加精确的讨论领导人完整性特性(这一讨论基于 9.2 节的安全性证明)。我们假设领导人完全性特性是不存在的,然后我们推出矛盾来。假设任期 T 的领导人(领导人 T)在任期内提交了一条日志条目,但是这条日志条目没有被存储到未来某个任期的领导人的日志中。设大于 T 的最小任期 U 的领导人 U 没有这条日志条目。</p><p><img src="raft-%E5%9B%BE9.png" srcset="/img/loading.gif" alt="图 9"></p><blockquote><p>图 9:如果 S1 (任期 T 的领导者)提交了一条新的日志在它的任期里,然后 S5 在之后的任期 U 里被选举为领导人,然后至少会有一个机器,如 S3,既拥有来自 S1 的日志,也给 S5 投票了。</p></blockquote><ol><li>在领导人 U 选举的时候一定没有那条被提交的日志条目(领导人从不会删除或者覆盖任何条目)。</li><li>领导人 T 复制这条日志条目给集群中的大多数节点,同时,领导人 U 从集群中的大多数节点赢得了选票。因此,至少有一个节点(投票者、选民)同时接受了来自领导人 T 的日志条目,并且给领导人 U 投票了,如图 9。这个投票者是产生这个矛盾的关键。</li><li>这个投票者必须在给领导人 U 投票之前先接受了从领导人 T 发来的已经被提交的日志条目;否则他就会拒绝来自领导人 T 的附加日志请求(因为此时他的任期号会比 T 大)。</li><li>投票者在给领导人 U 投票时依然保存有这条日志条目,因为任何中间的领导人都包含该日志条目(根据上述的假设),领导人从不会删除条目,并且跟随者只有在和领导人冲突的时候才会删除条目。</li><li>投票者把自己选票投给领导人 U 时,领导人 U 的日志必须和投票者自己一样新。这就导致了两者矛盾之一。</li><li>首先,如果投票者和领导人 U 的最后一条日志的任期号相同,那么领导人 U 的日志至少和投票者一样长,所以领导人 U 的日志一定包含所有投票者的日志。这是另一处矛盾,因为投票者包含了那条已经被提交的日志条目,但是在上述的假设里,领导人 U 是不包含的。</li><li>除此之外,领导人 U 的最后一条日志的任期号就必须比投票人大了。此外,他也比 T 大,因为投票人的最后一条日志的任期号至少和 T 一样大(他包含了来自任期 T 的已提交的日志)。创建了领导人 U 最后一条日志的之前领导人一定已经包含了那条被提交的日志(根据上述假设,领导人 U 是第一个不包含该日志条目的领导人)。所以,根据日志匹配特性,领导人 U 一定也包含那条被提交的日志,这里产生矛盾。</li><li>这里完成了矛盾。因此,所有比 T 大的领导人一定包含了所有来自 T 的已经被提交的日志。</li><li>日志匹配原则保证了未来的领导人也同时会包含被间接提交的条目,例如图 8 (d) 中的索引 2。</li></ol><p>通过领导人完全特性,我们就能证明图 3 中的状态机安全特性,即如果服务器已经在某个给定的索引值应用了日志条目到自己的状态机里,那么其他的服务器不会应用一个不一样的日志到同一个索引值上。在一个服务器应用一条日志条目到他自己的状态机中时,他的日志必须和领导人的日志,在该条目和之前的条目上相同,并且已经被提交。现在我们来考虑在任何一个服务器应用一个指定索引位置的日志的最小任期;日志完全特性保证拥有更高任期号的领导人会存储相同的日志条目,所以之后的任期里应用某个索引位置的日志条目也会是相同的值。因此,状态机安全特性是成立的。</p><p>最后,Raft 要求服务器按照日志中索引位置顺序应用日志条目。和状态机安全特性结合起来看,这就意味着所有的服务器会应用相同的日志序列集到自己的状态机中,并且是按照相同的顺序。</p><h3 id="5-5-跟随者和候选人崩溃"><a href="#5-5-跟随者和候选人崩溃" class="headerlink" title="5.5 跟随者和候选人崩溃"></a>5.5 跟随者和候选人崩溃</h3><p>到目前为止,我们都只关注了领导人崩溃的情况。跟随者和候选人崩溃后的处理方式比领导人要简单的多,并且他们的处理方式是相同的。如果跟随者或者候选人崩溃了,那么后续发送给他们的 RPCs 都会失败。Raft 中处理这种失败就是简单的通过无限的重试;如果崩溃的机器重启了,那么这些 RPC 就会完整的成功。如果一个服务器在完成了一个 RPC,但是还没有响应的时候崩溃了,那么在他重新启动之后就会再次收到同样的请求。Raft 的 RPCs 都是幂等的,所以这样重试不会造成任何问题。例如一个跟随者如果收到附加日志请求但是他已经包含了这一日志,那么他就会直接忽略这个新的请求。</p><h3 id="5-6-时间和可用性"><a href="#5-6-时间和可用性" class="headerlink" title="5.6 时间和可用性"></a>5.6 时间和可用性</h3><p>Raft 的要求之一就是安全性不能依赖时间:整个系统不能因为某些事件运行的比预期快一点或者慢一点就产生了错误的结果。但是,可用性(系统可以及时的响应客户端)不可避免的要依赖于时间。例如,如果消息交换比服务器故障间隔时间长,候选人将没有足够长的时间来赢得选举;没有一个稳定的领导人,Raft 将无法工作。</p><p>领导人选举是 Raft 中对时间要求最为关键的方面。Raft 可以选举并维持一个稳定的领导人,只要系统满足下面的时间要求:</p><blockquote><p>广播时间(broadcastTime) << 选举超时时间(electionTimeout) << 平均故障间隔时间(MTBF)</p></blockquote><p>在这个不等式中,广播时间指的是从一个服务器并行的发送 RPCs 给集群中的其他服务器并接收响应的平均时间;选举超时时间就是在 5.2 节中介绍的选举的超时时间限制;然后平均故障间隔时间就是对于一台服务器而言,两次故障之间的平均时间。广播时间必须比选举超时时间小一个量级,这样领导人才能够发送稳定的心跳消息来阻止跟随者开始进入选举状态;通过随机化选举超时时间的方法,这个不等式也使得选票瓜分的情况变得不可能。选举超时时间应该要比平均故障间隔时间小上几个数量级,这样整个系统才能稳定的运行。当领导人崩溃后,整个系统会大约相当于选举超时的时间里不可用;我们希望这种情况在整个系统的运行中很少出现。</p><p>广播时间和平均故障间隔时间是由系统决定的,但是选举超时时间是我们自己选择的。Raft 的 RPCs 需要接收方将信息持久化的保存到稳定存储中去,所以广播时间大约是 0.5 毫秒到 20 毫秒,取决于存储的技术。因此,选举超时时间可能需要在 10 毫秒到 500 毫秒之间。大多数的服务器的平均故障间隔时间都在几个月甚至更长,很容易满足时间的需求。</p><h2 id="6-集群成员变化"><a href="#6-集群成员变化" class="headerlink" title="6 集群成员变化"></a>6 集群成员变化</h2><p>到目前为止,我们都假设集群的配置(加入到一致性算法的服务器集合)是固定不变的。但是在实践中,偶尔是会改变集群的配置的,例如替换那些宕机的机器或者改变复制级别。尽管可以通过暂停整个集群,更新所有配置,然后重启整个集群的方式来实现,但是在更改的时候集群会不可用。另外,如果存在手工操作步骤,那么就会有操作失误的风险。为了避免这样的问题,我们决定自动化配置改变并且将其纳入到 Raft 一致性算法中来。</p><p>为了让配置修改机制能够安全,那么在转换的过程中不能够存在任何时间点使得两个领导人同时被选举成功在同一个任期里。不幸的是,任何服务器直接从旧的配置直接转换到新的配置的方案都是不安全的。一次性原子地转换所有服务器是不可能的,所以在转换期间整个集群存在划分成两个独立的大多数群体的可能性(见图 10)。</p><p><img src="raft-%E5%9B%BE10.png" srcset="/img/loading.gif" alt="图 10"></p><blockquote><p>图 10:直接从一种配置转到新的配置是十分不安全的,因为各个机器可能在任何的时候进行转换。在这个例子中,集群配额从 3 台机器变成了 5 台。不幸的是,存在这样的一个时间点,两个不同的领导人在同一个任期里都可以被选举成功。一个是通过旧的配置,一个通过新的配置。</p></blockquote><p>为了保证安全性,配置更改必须使用两阶段方法。目前有很多种两阶段的实现。例如,有些系统在第一阶段停掉旧的配置所以集群就不能处理客户端请求;然后在第二阶段在启用新的配置。在 Raft 中,集群先切换到一个过渡的配置,我们称之为共同一致;一旦共同一致已经被提交了,那么系统就切换到新的配置上。共同一致是老配置和新配置的结合:</p><ul><li>日志条目被复制给集群中新、老配置的所有服务器。</li><li>新、旧配置的服务器都可以成为领导人。</li><li>达成一致(针对选举和提交)需要分别在两种配置上获得大多数的支持。</li></ul><p>共同一致允许独立的服务器在不影响安全性的前提下,在不同的时间进行配置转换过程。此外,共同一致可以让集群在配置转换的过程中依然响应客户端的请求。</p><p>集群配置在复制日志中以特殊的日志条目来存储和通信;图 11 展示了配置转换的过程。当一个领导人接收到一个改变配置从 C-old 到 C-new 的请求,他会为了共同一致存储配置(图中的 C-old,new),以前面描述的日志条目和副本的形式。一旦一个服务器将新的配置日志条目增加到它的日志中,他就会用这个配置来做出未来所有的决定(服务器总是使用最新的配置,无论他是否已经被提交)。这意味着领导人要使用 C-old,new 的规则来决定日志条目 C-old,new 什么时候需要被提交。如果领导人崩溃了,被选出来的新领导人可能是使用 C-old 配置也可能是 C-old,new 配置,这取决于赢得选举的候选人是否已经接收到了 C-old,new 配置。在任何情况下, C-new 配置在这一时期都不会单方面的做出决定。</p><p>一旦 C-old,new 被提交,那么无论是 C-old 还是 C-new,在没有经过他人批准的情况下都不可能做出决定,并且领导人完全特性保证了只有拥有 C-old,new 日志条目的服务器才有可能被选举为领导人。这个时候,领导人创建一条关于 C-new 配置的日志条目并复制给集群就是安全的了。再者,每个服务器在见到新的配置的时候就会立即生效。当新的配置在 C-new 的规则下被提交,旧的配置就变得无关紧要,同时不使用新的配置的服务器就可以被关闭了。如图 11,C-old 和 C-new 没有任何机会同时做出单方面的决定;这保证了安全性。</p><p><img src="raft-%E5%9B%BE11.png" srcset="/img/loading.gif" alt="图 11"></p><blockquote><p>图 11:一个配置切换的时间线。虚线表示已经被创建但是还没有被提交的配置日志条目,实线表示最后被提交的配置日志条目。领导人首先创建了 C-old,new 的配置条目在自己的日志中,并提交到 C-old,new 中(C-old 的大多数和 C-new 的大多数)。然后他创建 C-new 条目并提交到 C-new 中的大多数。这样就不存在 C-new 和 C-old 可以同时做出决定的时间点。</p></blockquote><p>在关于重新配置还有三个问题需要提出。第一个问题是,新的服务器可能初始化没有存储任何的日志条目。当这些服务器以这种状态加入到集群中,那么他们需要一段时间来更新追赶,这时还不能提交新的日志条目。为了避免这种可用性的间隔时间,Raft 在配置更新之前使用了一种额外的阶段,在这个阶段,新的服务器以没有投票权身份加入到集群中来(领导人复制日志给他们,但是不考虑他们是大多数)。一旦新的服务器追赶上了集群中的其他机器,重新配置可以像上面描述的一样处理。</p><p>第二个问题是,集群的领导人可能不是新配置的一员。在这种情况下,领导人就会在提交了 C-new 日志之后退位(回到跟随者状态)。这意味着有这样的一段时间,领导人管理着集群,但是不包括他自己;他复制日志但是不把他自己算作是大多数之一。当 C-new 被提交时,会发生领导人过渡,因为这时是最早新的配置可以独立工作的时间点(将总是能够在 C-new 配置下选出新的领导人)。在此之前,可能只能从 C-old 中选出领导人。</p><p>第三个问题是,移除不在 C-new 中的服务器可能会扰乱集群。这些服务器将不会再接收到心跳,所以当选举超时,他们就会进行新的选举过程。他们会发送拥有新的任期号的请求投票 RPCs,这样会导致当前的领导人回退成跟随者状态。新的领导人最终会被选出来,但是被移除的服务器将会再次超时,然后这个过程会再次重复,导致整体可用性大幅降低。</p><p>为了避免这个问题,当服务器确认当前领导人存在时,服务器会忽略请求投票 RPCs。特别的,当服务器在当前最小选举超时时间内收到一个请求投票 RPC,他不会更新当前的任期号或者投出选票。这不会影响正常的选举,每个服务器在开始一次选举之前,至少等待一个最小选举超时时间。然而,这有利于避免被移除的服务器扰乱:如果领导人能够发送心跳给集群,那么他就不会被更大的任期号废黜。</p><h2 id="7-日志压缩"><a href="#7-日志压缩" class="headerlink" title="7 日志压缩"></a>7 日志压缩</h2><p>Raft 的日志在正常操作中不断的增长,但是在实际的系统中,日志不能无限制的增长。随着日志不断增长,他会占用越来越多的空间,花费越来越多的时间来重置。如果没有一定的机制去清除日志里积累的陈旧的信息,那么会带来可用性问题。</p><p>快照是最简单的压缩方法。在快照系统中,整个系统的状态都以快照的形式写入到稳定的持久化存储中,然后到那个时间点之前的日志全部丢弃。快照技术被使用在 Chubby 和 ZooKeeper 中,接下来的章节会介绍 Raft 中的快照技术。</p><p>增量压缩的方法,例如日志清理或者日志结构合并树,都是可行的。这些方法每次只对一小部分数据进行操作,这样就分散了压缩的负载压力。首先,他们先选择一个已经积累的大量已经被删除或者被覆盖对象的区域,然后重写那个区域还活跃的对象,之后释放那个区域。和简单操作整个数据集合的快照相比,需要增加复杂的机制来实现。状态机可以实现 LSM tree 使用和快照相同的接口,但是日志清除方法就需要修改 Raft 了。</p><p><img src="raft-%E5%9B%BE12.png" srcset="/img/loading.gif" alt="图 12"></p><blockquote><p>图 12:一个服务器用新的快照替换了从 1 到 5 的条目,快照值存储了当前的状态。快照中包含了最后的索引位置和任期号。</p></blockquote><p>图 12 展示了 Raft 中快照的基础思想。每个服务器独立的创建快照,只包括已经被提交的日志。主要的工作包括将状态机的状态写入到快照中。Raft 也包含一些少量的元数据到快照中:<strong>最后被包含索引</strong>指的是被快照取代的最后的条目在日志中的索引值(状态机最后应用的日志),<strong>最后被包含的任期</strong>指的是该条目的任期号。保留这些数据是为了支持快照后紧接着的第一个条目的附加日志请求时的一致性检查,因为这个条目需要前一日志条目的索引值和任期号。为了支持集群成员更新(第 6 节),快照中也将最后的一次配置作为最后一个条目存下来。一旦服务器完成一次快照,他就可以删除最后索引位置之前的所有日志和快照了。</p><p>尽管通常服务器都是独立的创建快照,但是领导人必须偶尔的发送快照给一些落后的跟随者。这通常发生在当领导人已经丢弃了下一条需要发送给跟随者的日志条目的时候。幸运的是这种情况不是常规操作:一个与领导人保持同步的跟随者通常都会有这个条目。然而一个运行非常缓慢的跟随者或者新加入集群的服务器(第 6 节)将不会有这个条目。这时让这个跟随者更新到最新的状态的方式就是通过网络把快照发送给他们。</p><p><strong>安装快照 RPC</strong>:</p><p>由领导人调用以将快照的分块发送给跟随者。领导者总是按顺序发送分块。</p><table><thead><tr><th>参数</th><th>解释</th></tr></thead><tbody><tr><td>term</td><td>领导人的任期号</td></tr><tr><td>leaderId</td><td>领导人的 Id,以便于跟随者重定向请求</td></tr><tr><td>lastIncludedIndex</td><td>快照中包含的最后日志条目的索引值</td></tr><tr><td>lastIncludedTerm</td><td>快照中包含的最后日志条目的任期号</td></tr><tr><td>offset</td><td>分块在快照中的字节偏移量</td></tr><tr><td>data[]</td><td>从偏移量开始的快照分块的原始字节</td></tr><tr><td>done</td><td>如果这是最后一个分块则为 true</td></tr></tbody></table><table><thead><tr><th>结果</th><th>解释</th></tr></thead><tbody><tr><td>term</td><td>当前任期号(currentTerm),便于领导人更新自己</td></tr></tbody></table><p><strong>接收者实现</strong>:</p><ol><li>如果<code>term < currentTerm</code>就立即回复</li><li>如果是第一个分块(offset 为 0)就创建一个新的快照</li><li>在指定偏移量写入数据</li><li>如果 done 是 false,则继续等待更多的数据</li><li>保存快照文件,丢弃具有较小索引的任何现有或部分快照</li><li>如果现存的日志条目与快照中最后包含的日志条目具有相同的索引值和任期号,则保留其后的日志条目并进行回复</li><li>丢弃整个日志</li><li>使用快照重置状态机(并加载快照的集群配置)</li></ol><p><img src="raft-%E5%9B%BE13.png" srcset="/img/loading.gif" alt="图 13 "></p><blockquote><p>图 13:一个关于安装快照的简要概述。为了便于传输,快照都是被分成分块的;每个分块都给了跟随者生命的迹象,所以跟随者可以重置选举超时计时器。</p></blockquote><p>在这种情况下领导人使用一种叫做安装快照的新的 RPC 来发送快照给太落后的跟随者;见图 13。当跟随者通过这种 RPC 接收到快照时,他必须自己决定对于已经存在的日志该如何处理。通常快照会包含没有在接收者日志中存在的信息。在这种情况下,跟随者丢弃其整个日志;它全部被快照取代,并且可能包含与快照冲突的未提交条目。如果接收到的快照是自己日志的前面部分(由于网络重传或者错误),那么被快照包含的条目将会被全部删除,但是快照后面的条目仍然有效,必须保留。</p><p>这种快照的方式背离了 Raft 的强领导人原则,因为跟随者可以在不知道领导人情况下创建快照。但是我们认为这种背离是值得的。领导人的存在,是为了解决在达成一致性的时候的冲突,但是在创建快照的时候,一致性已经达成,这时不存在冲突了,所以没有领导人也是可以的。数据依然是从领导人传给跟随者,只是跟随者可以重新组织他们的数据了。</p><p>我们考虑过一种替代的基于领导人的快照方案,即只有领导人创建快照,然后发送给所有的跟随者。但是这样做有两个缺点。第一,发送快照会浪费网络带宽并且延缓了快照处理的时间。每个跟随者都已经拥有了所有产生快照需要的信息,而且很显然,自己从本地的状态中创建快照比通过网络接收别人发来的要经济。第二,领导人的实现会更加复杂。例如,领导人需要发送快照的同时并行的将新的日志条目发送给跟随者,这样才不会阻塞新的客户端请求。</p><p>还有两个问题影响了快照的性能。首先,服务器必须决定什么时候应该创建快照。如果快照创建的过于频繁,那么就会浪费大量的磁盘带宽和其他资源;如果创建快照频率太低,他就要承受耗尽存储容量的风险,同时也增加了从日志重建的时间。一个简单的策略就是当日志大小达到一个固定大小的时候就创建一次快照。如果这个阈值设置的显著大于期望的快照的大小,那么快照对磁盘压力的影响就会很小了。</p><p>第二个影响性能的问题就是写入快照需要花费显著的一段时间,并且我们还不希望影响到正常操作。解决方案是通过写时复制的技术,这样新的更新就可以被接收而不影响到快照。例如,具有函数式数据结构的状态机天然支持这样的功能。另外,操作系统的写时复制技术的支持(如 Linux 上的 fork)可以被用来创建完整的状态机的内存快照(我们的实现就是这样的)。</p><h2 id="8-客户端交互"><a href="#8-客户端交互" class="headerlink" title="8 客户端交互"></a>8 客户端交互</h2><p>这一节将介绍客户端是如何和 Raft 进行交互的,包括客户端如何发现领导人和 Raft 是如何支持线性化语义的。这些问题对于所有基于一致性的系统都存在,并且 Raft 的解决方案和其他的也差不多。</p><p>Raft 中的客户端发送所有请求给领导人。当客户端启动的时候,他会随机挑选一个服务器进行通信。如果客户端第一次挑选的服务器不是领导人,那么那个服务器会拒绝客户端的请求并且提供他最近接收到的领导人的信息(附加条目请求包含了领导人的网络地址)。如果领导人已经崩溃了,那么客户端的请求就会超时;客户端之后会再次重试随机挑选服务器的过程。</p><p>我们 Raft 的目标是要实现线性化语义(每一次操作立即执行,只执行一次,在他调用和收到回复之间)。但是,如上述,Raft 是可以执行同一条命令多次的:例如,如果领导人在提交了这条日志之后,但是在响应客户端之前崩溃了,那么客户端会和新的领导人重试这条指令,导致这条命令就被再次执行了。解决方案就是客户端对于每一条指令都赋予一个唯一的序列号。然后,状态机跟踪每条指令最新的序列号和相应的响应。如果接收到一条指令,它的序列号已经被执行了,那么就立即返回结果,而不重新执行指令。</p><p>只读的操作可以直接处理而不需要记录日志。但是,在不增加任何限制的情况下,这么做可能会冒着返回脏数据的风险,因为领导人响应客户端请求时可能已经被新的领导人作废了,但是他还不知道。线性化的读操作必须不能返回脏数据,Raft 需要使用两个额外的措施在不使用日志的情况下保证这一点。首先,领导人必须有关于被提交日志的最新信息。领导人完全特性保证了领导人一定拥有所有已经被提交的日志条目,但是在他任期开始的时候,他可能不知道哪些是已经被提交的。为了知道这些信息,他需要在他的任期里提交一条日志条目。Raft 中通过领导人在任期开始的时候提交一个空白的没有任何操作的日志条目到日志中去来实现。第二,领导人在处理只读的请求之前必须检查自己是否已经被废黜了(他自己的信息已经变脏了如果一个更新的领导人被选举出来)。Raft 中通过让领导人在响应只读请求之前,先和集群中的大多数节点交换一次心跳信息来处理这个问题。可选的,领导人可以依赖心跳机制来实现一种租约的机制,但是这种方法依赖时间来保证安全性(假设时间误差是有界的)。</p><h2 id="9-算法实现和评估"><a href="#9-算法实现和评估" class="headerlink" title="9 算法实现和评估"></a>9 算法实现和评估</h2><p>我们已经为 RAMCloud 实现了 Raft 算法作为存储配置信息的复制状态机的一部分,并且帮助 RAMCloud 协调故障转移。这个 Raft 实现包含大约 2000 行 C++ 代码,其中不包括测试、注释和空行。这些代码是开源的。同时也有大约 25 个其他独立的第三方的基于这篇论文草稿的开源实现,针对不同的开发场景。同时,很多公司已经部署了基于 Raft 的系统。</p><p>这一节会从三个方面来评估 Raft 算法:可理解性、正确性和性能。</p><h3 id="9-1-可理解性"><a href="#9-1-可理解性" class="headerlink" title="9.1 可理解性"></a>9.1 可理解性</h3><p>为了和 Paxos 比较 Raft 算法的可理解能力,我们针对高层次的本科生和研究生,在斯坦福大学的高级操作系统课程和加州大学伯克利分校的分布式计算课程上,进行了一次学习的实验。我们分别拍了针对 Raft 和 Paxos 的视频课程,并准备了相应的小测验。Raft 的视频讲课覆盖了这篇论文的所有内容除了日志压缩;Paxos 讲课包含了足够的资料来创建一个等价的复制状态机,包括单决策 Paxos,多决策 Paxos,重新配置和一些实际系统需要的性能优化(例如领导人选举)。小测验测试一些对算法的基本理解和解释一些边角的示例。每个学生都是看完第一个视频,回答相应的测试,再看第二个视频,回答相应的测试。大约有一半的学生先进行 Paxos 部分,然后另一半先进行 Raft 部分,这是为了说明两者从第一部分的算法学习中获得的表现和经验的差异。我们计算参加人员的每一个小测验的得分来看参与者是否在 Raft 算法上更加容易理解。</p><p>我们尽可能的使得 Paxos 和 Raft 的比较更加公平。这个实验偏爱 Paxos 表现在两个方面:43 个参加者中有 15 个人在之前有一些 Paxos 的经验,并且 Paxos 的视频要长 14%。如表格 1 总结的那样,我们采取了一些措施来减轻这种潜在的偏见。我们所有的材料都可供审查。</p><table><thead><tr><th>关心</th><th>缓和偏见采取的手段</th><th>可供查看的材料</th></tr></thead><tbody><tr><td>相同的讲课质量</td><td>两者使用同一个讲师。Paxos 使用的是现在很多大学里经常使用的。Paxos 会长 14%。</td><td>视频</td></tr><tr><td>相同的测验难度</td><td>问题以难度分组,在两个测验里成对出现。</td><td>小测验</td></tr><tr><td>公平评分</td><td>使用评价量规。随机顺序打分,两个测验交替进行。</td><td>评价量规(rubric)</td></tr></tbody></table><blockquote><p>表 1:考虑到可能会存在的偏见,对于每种情况的解决方法,和相应的材料。</p></blockquote><p>参加者平均在 Raft 的测验中比 Paxos 高 4.9 分(总分 60,那么 Raft 的平均得分是 25.7,而 Paxos 是 20.8);图 14 展示了每个参与者的得分。配置t-检验(又称student‘s t-test)表明,在 95% 的可信度下,真实的 Raft 分数分布至少比 Paxos 高 2.5 分。</p><p><img src="raft-%E5%9B%BE14.png" srcset="/img/loading.gif" alt="图 14"></p><blockquote><p>图 14:一个散点图表示了 43 个学生在 Paxos 和 Raft 的小测验中的成绩。在对角线之上的点表示在 Raft 获得了更高分数的学生。</p></blockquote><p>我们也建立了一个线性回归模型来预测一个新的学生的测验成绩,基于以下三个因素:他们使用的是哪个小测验,之前对 Paxos 的经验,和学习算法的顺序。模型预测,对小测验的选择会产生 12.5 分的差别。这显著的高于之前的 4.9 分,因为很多学生在之前都已经有了对于 Paxos 的经验,这相当明显的帮助 Paxos,对 Raft 就没什么太大影响了。但是奇怪的是,模型预测对于先进行 Paxos 小测验的人而言,Raft的得分低了6.3分; 虽然我们不知道为什么,这似乎在统计上是有意义的。</p><p>我们同时也在测验之后调查了参与者,他们认为哪个算法更加容易实现和解释;这个的结果在图 15 上。压倒性的结果表明 Raft 算法更加容易实现和解释(41 人中的 33个)。但是,这种自己报告的结果不如参与者的成绩更加可信,并且参与者可能因为我们的 Raft 更加易于理解的假说而产生偏见。</p><p><img src="raft-%E5%9B%BE15.png" srcset="/img/loading.gif" alt="图 15"></p><blockquote><p>图 15:通过一个 5 分制的问题,参与者(左边)被问哪个算法他们觉得在一个高效正确的系统里更容易实现,右边被问哪个更容易向学生解释。</p></blockquote><p>关于 Raft 用户学习有一个更加详细的讨论。</p><h3 id="9-2-正确性"><a href="#9-2-正确性" class="headerlink" title="9.2 正确性"></a>9.2 正确性</h3><p>在第 5 节,我们已经制定了正式的规范,和对一致性机制的安全性证明。这个正式规范使用 TLA+ 规范语言使图 2 中总结的信息非常清晰。它长约400行,并作为证明的主题。同时对于任何想实现 Raft 的人也是十分有用的。我们通过 TLA 证明系统非常机械的证明了日志完全特性。然而,这个证明依赖的约束前提还没有被机械证明(例如,我们还没有证明规范的类型安全)。而且,我们已经写了一个非正式的证明关于状态机安全性是完备的,并且是相当清晰的(大约 3500 个词)。</p><h3 id="9-3-性能"><a href="#9-3-性能" class="headerlink" title="9.3 性能"></a>9.3 性能</h3><p>Raft 和其他一致性算法例如 Paxos 有着差不多的性能。在性能方面,最重要的关注点是,当领导人被选举成功时,什么时候复制新的日志条目。Raft 通过很少数量的消息包(一轮从领导人到集群大多数机器的消息)就达成了这个目的。同时,进一步提升 Raft 的性能也是可行的。例如,很容易通过支持批量操作和管道操作来提高吞吐量和降低延迟。对于其他一致性算法已经提出过很多性能优化方案;其中有很多也可以应用到 Raft 中来,但是我们暂时把这个问题放到未来的工作中去。</p><p>我们使用我们自己的 Raft 实现来衡量 Raft 领导人选举的性能并且回答两个问题。首先,领导人选举的过程收敛是否快速?第二,在领导人宕机之后,最小的系统宕机时间是多久?</p><p><img src="raft-%E5%9B%BE16.png" srcset="/img/loading.gif" alt="图 16"></p><blockquote><p>图 16:发现并替换一个已经崩溃的领导人的时间。上面的图考察了在选举超时时间上的随机化程度,下面的图考察了最小选举超时时间。每条线代表了 1000 次实验(除了 150-150 毫秒只试了 100 次),和相应的确定的选举超时时间。例如,150-155 毫秒意思是,选举超时时间从这个区间范围内随机选择并确定下来。这个实验在一个拥有 5 个节点的集群上进行,其广播时延大约是 15 毫秒。对于 9 个节点的集群,结果也差不多。</p></blockquote><p>为了衡量领导人选举,我们反复的使一个拥有五个节点的服务器集群的领导人宕机,并计算需要多久才能发现领导人已经宕机并选出一个新的领导人(见图 16)。为了构建一个最坏的场景,在每一的尝试里,服务器都有不同长度的日志,意味着有些候选人是没有成为领导人的资格的。另外,为了促成选票瓜分的情况,我们的测试脚本在终止领导人之前同步的发送了一次心跳广播(这大约和领导人在崩溃前复制一个新的日志给其他机器很像)。领导人均匀的随机的在心跳间隔里宕机,也就是最小选举超时时间的一半。因此,最小宕机时间大约就是最小选举超时时间的一半。</p><p>图 16 中上面的图表明,只需要在选举超时时间上使用很少的随机化就可以大大避免选票被瓜分的情况。在没有随机化的情况下,在我们的测试里,选举过程往往都需要花费超过 10 秒钟由于太多的选票瓜分的情况。仅仅增加 5 毫秒的随机化时间,就大大的改善了选举过程,现在平均的宕机时间只有 287 毫秒。增加更多的随机化时间可以大大改善最坏情况:通过增加 50 毫秒的随机化时间,最坏的完成情况(1000 次尝试)只要 513 毫秒。</p><p>图 16 中下面的图显示,通过减少选举超时时间可以减少系统的宕机时间。在选举超时时间为 12-24 毫秒的情况下,只需要平均 35 毫秒就可以选举出新的领导人(最长的一次花费了 152 毫秒)。然而,进一步降低选举超时时间的话就会违反 Raft 的时间不等式需求:在选举新领导人之前,领导人就很难发送完心跳包。这会导致没有意义的领导人改变并降低了系统整体的可用性。我们建议使用更为保守的选举超时时间,比如 150-300 毫秒;这样的时间不大可能导致没有意义的领导人改变,而且依然提供不错的可用性。</p><h2 id="10-相关工作"><a href="#10-相关工作" class="headerlink" title="10 相关工作"></a>10 相关工作</h2><p>已经有很多关于一致性算法的工作被发表出来,其中很多都可以归到下面的类别中:</p><ul><li>Lamport 关于 Paxos 的原始描述,和尝试描述的更清晰。</li><li>关于 Paxos 的更详尽的描述,补充遗漏的细节并修改算法,使得可以提供更加容易的实现基础。</li><li>实现一致性算法的系统,例如 Chubby,ZooKeeper 和 Spanner。对于 Chubby 和 Spanner 的算法并没有公开发表其技术细节,尽管他们都声称是基于 Paxos 的。ZooKeeper 的算法细节已经发表,但是和 Paxos 着实有着很大的差别。</li><li>Paxos 可以应用的性能优化。</li><li>Oki 和 Liskov 的 Viewstamped Replication(VR),一种和 Paxos 差不多的替代算法。原始的算法描述和分布式传输协议耦合在了一起,但是核心的一致性算法在最近的更新里被分离了出来。VR 使用了一种基于领导人的方法,和 Raft 有很多相似之处。</li></ul><p>Raft 和 Paxos 最大的不同之处就在于 Raft 的强领导特性:Raft 使用领导人选举作为一致性协议里必不可少的部分,并且将尽可能多的功能集中到了领导人身上。这样就可以使得算法更加容易理解。例如,在 Paxos 中,领导人选举和基本的一致性协议是正交的:领导人选举仅仅是性能优化的手段,而且不是一致性所必须要求的。但是,这样就增加了多余的机制:Paxos 同时包含了针对基本一致性要求的两阶段提交协议和针对领导人选举的独立的机制。相比较而言,Raft 就直接将领导人选举纳入到一致性算法中,并作为两阶段一致性的第一步。这样就减少了很多机制。</p><p>像 Raft 一样,VR 和 ZooKeeper 也是基于领导人的,因此他们也拥有一些 Raft 的优点。但是,Raft 比 VR 和 ZooKeeper 拥有更少的机制因为 Raft 尽可能的减少了非领导人的功能。例如,Raft 中日志条目都遵循着从领导人发送给其他人这一个方向:附加条目 RPC 是向外发送的。在 VR 中,日志条目的流动是双向的(领导人可以在选举过程中接收日志);这就导致了额外的机制和复杂性。根据 ZooKeeper 公开的资料看,它的日志条目也是双向传输的,但是它的实现更像 Raft。</p><p>和上述我们提及的其他基于一致性的日志复制算法中,Raft 的消息类型更少。例如,我们数了一下 VR 和 ZooKeeper 使用的用来基本一致性需要和成员改变的消息数(排除了日志压缩和客户端交互,因为这些都比较独立且和算法关系不大)。VR 和 ZooKeeper 都分别定义了 10 种不同的消息类型,相对的,Raft 只有 4 种消息类型(两种 RPC 请求和对应的响应)。Raft 的消息都稍微比其他算法的要信息量大,但是都很简单。另外,VR 和 ZooKeeper 都在领导人改变时传输了整个日志;所以为了能够实践中使用,额外的消息类型就很必要了。</p><p>Raft 的强领导人模型简化了整个算法,但是同时也排斥了一些性能优化的方法。例如,平等主义 Paxos (EPaxos)在某些没有领导人的情况下可以达到很高的性能。平等主义 Paxos 充分发挥了在状态机指令中的交换性。任何服务器都可以在一轮通信下就提交指令,除非其他指令同时被提出了。然而,如果指令都是并发的被提出,并且互相之间不通信沟通,那么 EPaxos 就需要额外的一轮通信。因为任何服务器都可以提交指令,所以 EPaxos 在服务器之间的负载均衡做的很好,并且很容易在 WAN 网络环境下获得很低的延迟。但是,他在 Paxos 上增加了非常明显的复杂性。</p><p>一些集群成员变换的方法已经被提出或者在其他的工作中被实现,包括 Lamport 的原始的讨论,VR 和 SMART。我们选择使用共同一致的方法因为他对一致性协议的其他部分影响很小,这样我们只需要很少的一些机制就可以实现成员变换。Lamport 的基于 α 的方法之所以没有被 Raft 选择是因为它假设在没有领导人的情况下也可以达到一致性。和 VR 和 SMART 相比较,Raft 的重新配置算法可以在不限制正常请求处理的情况下进行;相比较的,VR 需要停止所有的处理过程,SMART 引入了一个和 α 类似的方法,限制了请求处理的数量。Raft 的方法同时也需要更少的额外机制来实现,和 VR、SMART 比较而言。</p><h2 id="11-结论"><a href="#11-结论" class="headerlink" title="11 结论"></a>11 结论</h2><p>算法的设计通常会把正确性,效率或者简洁作为主要的目标。尽管这些都是很有意义的目标,但是我们相信,可理解性也是一样的重要。在开发者把算法应用到实际的系统中之前,这些目标没有一个会被实现,这些都会必然的偏离发表时的形式。除非开发人员对这个算法有着很深的理解并且有着直观的感觉,否则将会对他们而言很难在实现的时候保持原有期望的特性。</p><p>在这篇论文中,我们尝试解决分布式一致性问题,但是一个广为接受但是十分令人费解的算法 Paxos 已经困扰了无数学生和开发者很多年了。我们创造了一种新的算法 Raft,显而易见的比 Paxos 要容易理解。我们同时也相信,Raft 也可以为实际的实现提供坚实的基础。把可理解性作为设计的目标改变了我们设计 Raft 的方式;随着设计的进展,我们发现自己重复使用了一些技术,比如分解问题和简化状态空间。这些技术不仅提升了 Raft 的可理解性,同时也使我们坚信其正确性。</p>]]></content>
<tags>
<tag>distributed-system</tag>
</tags>
</entry>
<entry>
<title>Dockerfile ENV 的问题</title>
<link href="/posts/dockerfile-env/"/>
<url>/posts/dockerfile-env/</url>
<content type="html"><![CDATA[<p>今天遇见一个小问题,大概是这样,我们的镜像是分层的,在 base 镜像里设置了一个 <code>ENV</code>,然后在后面一层镜像的时候 unset 掉了,但是实际运行的时候发现这个 <code>ENV</code> 一直存在,导致应用启动失败。花了一些时间查问题,其实本身也是 dockerfile 的设计,之前没有体系化的去看,所以这篇就记录下关于 <code>ENV</code> 的最佳实践。</p><p><a href="https://docs.docker.com/engine/reference/builder/#env" target="_blank" rel="noopener">Dockerfile reference for the ENV instruction</a></p><p>为了使新软件更易于运行,您可以使用 <code>ENV</code> 为你的容器更新 <code>PATH</code> 环境变量。例如,<code>ENV PATH /usr/local/nginx/bin:$PATH</code> 确保 <code>CMD ["nginx"]</code> 正常工作。</p><p><code>ENV</code> 指令对于提供所需的环境变量也很有用,这些变量特定于您希望容器化的服务,例如 Postgres 的 PGDATA。</p><p>最后,还可以使用 <code>ENV</code> 设置常用的版本号,以便更容易维护版本,如下面的示例所示:</p><pre><code class="hljs dockerfile"><span class="hljs-keyword">ENV</span> PG_MAJOR <span class="hljs-number">9.3</span><span class="hljs-keyword">ENV</span> PG_VERSION <span class="hljs-number">9.3</span>.<span class="hljs-number">4</span><span class="hljs-keyword">RUN</span><span class="bash"> curl -SL http://example.com/postgres-<span class="hljs-variable">$PG_VERSION</span>.tar.xz | tar -xJC /usr/src/postgress && …</span><span class="hljs-keyword">ENV</span> PATH /usr/local/postgres-$PG_MAJOR/bin:$PATH</code></pre><p>类似于在程序中具有常量变量(而不是硬编码的值),这种方法允许您更改单个 <code>ENV</code> 指令,以自动地在容器中加载软件的版本。。</p><p>每条 <code>ENV</code> 行都会创建一个新的中间层,就像 <code>RUN</code> 命令一样。这意味着,即使您在以后的层中取消环境变量,它也仍将保留在该层中,并且其值也无法清掉. 您可以通过创建如下所示的 Dockerfile,然后对其进行构建来进行测试。</p><pre><code class="hljs dockerfile"><span class="hljs-keyword">FROM</span> alpine<span class="hljs-keyword">ENV</span> ADMIN_USER=<span class="hljs-string">"mark"</span><span class="hljs-keyword">RUN</span><span class="bash"> <span class="hljs-built_in">echo</span> <span class="hljs-variable">$ADMIN_USER</span> > ./mark</span><span class="hljs-keyword">RUN</span><span class="bash"> <span class="hljs-built_in">unset</span> ADMIN_USER</span></code></pre><pre><code class="hljs shell"><span class="hljs-meta">$</span><span class="bash"> docker run --rm <span class="hljs-built_in">test</span> sh -c <span class="hljs-string">'echo $ADMIN_USER'</span></span>mark</code></pre><p>为避免这种情况,并真正取消对环境变量的设置,可以使用带有 shell 命令的 <code>RUN</code> 命令,在一个单独的层中设置、使用和取消对环境变量的设置。你可以用 <code>;</code> 和 <code>&&</code> 来分隔命令。如果您使用第二种方法,并且其中一个命令失败,docker 构建也会失败。在 Linux Dockerfiles 中使用 <code>\</code> 作为行连续字符可以提高可读性。您还可以将所有命令放入一个 shell 脚本中,并让 <code>RUN</code> 命令直接运行该 shell 脚本。</p><pre><code class="hljs dockerfile"><span class="hljs-keyword">FROM</span> alpine<span class="hljs-keyword">RUN</span><span class="bash"> <span class="hljs-built_in">export</span> ADMIN_USER=<span class="hljs-string">"mark"</span> \</span><span class="bash"> && <span class="hljs-built_in">echo</span> <span class="hljs-variable">$ADMIN_USER</span> > ./mark \</span><span class="bash"> && <span class="hljs-built_in">unset</span> ADMIN_USER</span><span class="hljs-keyword">CMD</span><span class="bash"> sh</span></code></pre><pre><code class="hljs shell"><span class="hljs-meta">$</span><span class="bash"> docker run --rm <span class="hljs-built_in">test</span> sh -c <span class="hljs-string">'echo $ADMIN_USER'</span></span></code></pre>]]></content>
<tags>
<tag>cloud-native</tag>
<tag>docker</tag>
</tags>
</entry>
<entry>
<title>云原生交付思考</title>
<link href="/posts/cloud-native-delivery/"/>
<url>/posts/cloud-native-delivery/</url>
<content type="html"><![CDATA[<p>当 DevOps 遇上容器时,PPT 上往往有各种遐想的空间。实际在落地的过程中,因为公司已有成熟的发布系统,该发布系统是基于虚拟机时代的场景设计,发布主要是更新下代码和重启服务。为了兼容现有的发布模式,交付做的并不成功,和 k8s 的弹性理念存在冲突。所以我们需要打造一个以 k8s 为核心的新一代云原生 PaaS。</p>]]></content>
<tags>
<tag>cloud native</tag>
</tags>
</entry>
<entry>
<title>关于网卡中断不均衡问题及其解决方案</title>
<link href="/posts/irq-not-balance/"/>
<url>/posts/irq-not-balance/</url>
<content type="html"><![CDATA[<p>前不久生产碰到一个故障,一台宿主机上出现了大量的丢包,对业务造成了比较大的影响,遇到的问题还是蛮值得记录下来,所以简单的整理了下。</p><p>我们先了解几个概念:</p><h2 id="什么是中断"><a href="#什么是中断" class="headerlink" title="什么是中断"></a>什么是中断</h2><p>CPU 工作的模式有两种,一种是中断,由各种设备发起;一种是轮询,由 CPU 主动发起。</p><p>我们主要看下中断。</p><p>中断又分为两种:一种硬中断;一种软中断。硬中断是由硬件产生的,比如,像磁盘,网卡,键盘;软中断是由当前正在运行的进程所产生的。</p><p>中断,是一种由硬件产生的电信号直接发送到中断控制器上,然后由中断控制器向 CPU 发送信号,CPU 检测到该信号后,会中断当前的工作转而去处理中断。然后,处理器会通知内核已经产生中断,这样内核就会对这个中断进行适当的处理。</p><p>举个例子:</p><blockquote><p>当网卡收到数据包时会产生中断请求通知到 CPU,CPU 会中断当前正在运行的任务,然后通知内核有新数据包,内核调用中断处理程序进行响应,把数据包从网卡缓存及时拷贝到内存,否则会因为缓存溢出被丢弃。剩下的处理和操作数据包的工作就会交给软中断。</p></blockquote><h2 id="什么是多队列网卡"><a href="#什么是多队列网卡" class="headerlink" title="什么是多队列网卡"></a>什么是多队列网卡</h2><p>我们已经理解了中断,可是当网卡不断的接收数据包,就会产生很多中断,CPU 又如何能满足需求呢?</p><p>答案就是多队列网卡。</p><p>RSS(Receive Side Scaling)是网卡的硬件特性,实现了多队列。通过多队列网卡驱动加载,获取网卡型号,得到网卡的硬件 queue 的数量,并结合 CPU 核的数量,最终通过 Sum=Min(网卡 queue,CPU core)得出所要激活的网卡 queue 数量。</p><p>然后将各个 queue 中断分布到 CPU 多个核上,实现负载均衡,避免了单个核被占用到 100% 而其他核还处于空闲的情况。同一数据流会始终在同一 CPU 上,避免 TCP 的顺序性和 CPU 的并行性的冲突。基于流的负载均衡,解决了顺序协议和 CPU 并行的冲突以及 cache 热度问题。</p><p>多队列需要网卡硬件的支持。如果服务器的网卡支持 RSS,会在系统中看到网卡对应多个发送和接收队列:</p><pre><code class="hljs text">root@SVR:~ # ls /sys/class/net/eth0/queues/rx-0 rx-13 rx-18 rx-22 rx-27 rx-31 rx-36 rx-5 tx-0 tx-13 tx-18 tx-22 tx-27 tx-31 tx-36 tx-5rx-1 rx-14 rx-19 rx-23 rx-28 rx-32 rx-37 rx-6 tx-1 tx-14 tx-19 tx-23 tx-28 tx-32 tx-37 tx-6rx-10 rx-15 rx-2 rx-24 rx-29 rx-33 rx-38 rx-7 tx-10 tx-15 tx-2 tx-24 tx-29 tx-33 tx-38 tx-7rx-11 rx-16 rx-20 rx-25 rx-3 rx-34 rx-39 rx-8 tx-11 tx-16 tx-20 tx-25 tx-3 tx-34 tx-39 tx-8rx-12 rx-17 rx-21 rx-26 rx-30 rx-35 rx-4 rx-9 tx-12 tx-17 tx-21 tx-26 tx-30 tx-35 tx-4 tx-9</code></pre><h2 id="irq-亲缘绑定"><a href="#irq-亲缘绑定" class="headerlink" title="irq 亲缘绑定"></a>irq 亲缘绑定</h2><p><code>/proc/interrupts</code> 文件中可以看到各个 CPU 上的中断情况。</p><p><code>/proc/irq/[irq_num]/smp_affinity_list</code> 可以查看指定中断当前绑定的 CPU。</p><p>我们主要关注网卡中断的 CPU 情况:</p><pre><code class="hljs shell">cat /proc/interrupts | grep mlx5_comp | cut -d: -f1 | while read i; do echo -ne irq":$i\t bind_cpu: "; cat /proc/irq/$i/smp_affinity_list; done | sort -n -t' ' -k3</code></pre><p>输出是这样的:</p><pre><code class="hljs text">irq:100 bind_cpu: 10-19,30-39irq:101 bind_cpu: 10-19,30-39irq:102 bind_cpu: 10-19,30-39irq:103 bind_cpu: 10-19,30-39irq:112 bind_cpu: 10-19,30-39irq:113 bind_cpu: 10-19,30-39irq:114 bind_cpu: 10-19,30-39irq:115 bind_cpu: 10-19,30-39irq:116 bind_cpu: 10-19,30-39irq:117 bind_cpu: 10-19,30-39irq:118 bind_cpu: 10-19,30-39irq:119 bind_cpu: 10-19,30-39irq:120 bind_cpu: 10-19,30-39irq:121 bind_cpu: 10-19,30-39irq:122 bind_cpu: 10-19,30-39irq:123 bind_cpu: 10-19,30-39......</code></pre><p>可以看到绑定的是一组 CPU。</p><p>我们再查看下具体的中断情况:</p><pre><code class="hljs text"># cat /proc/interrupts | grep mlx5_comp | tr -s ' ' '\t'|cut -f 1-1588:000000000015775794740089:000000000036770078331090:000000000010845943390191:000000000022747845600092:000000000011416020630093:000000000027880535920094:000000000029292942280095:000000000034373042970096:000000000042097294990097:00000000002967475340098:000000000027534156400099:0000000000254258306700100:0000000000396116557500101:0000000000154844174900102:0000000000220575271200103:0000000000328505180200112:0000000000203954402600113:000000000099477686800114:0000000000321243650600115:000000000061560855900116:2000000000138779945100117:0100000000170494148300118:0010000000160543255900119:0001000000393250732100120:0000100000161007307500121:0000010000111463419100122:0000001000333848440100123:0000000100198285388600</code></pre><p>可以看到中断都是被 CPU10 处理的。查看 mellanox 的资料 <a href="https://community.mellanox.com/s/article/what-is-irq-affinity-x" target="_blank" rel="noopener">what-is-irq-affinity-x</a></p><blockquote><p>In case the IRQ affinity is not tuned, it mapped differently. For example, each interrupt to all CPUs you will get something like “FF” while running the command “show_irq_affinity”. This is less recommended, as in most cases, only the lower CPU cores (e.g. CPU0, CPU1) will be used and congested due to high volume of interrupts while the higher CPUs will not get to answer interrupts.</p></blockquote><p>也就是说这里配置了 10-19,30-39,但是只有 CPU10 在处理中断。</p><h2 id="丢包的原因"><a href="#丢包的原因" class="headerlink" title="丢包的原因"></a>丢包的原因</h2><p>上面已经分析了中断都是被 CPU10 处理了,我们再查看下 softnet_stat 的情况:</p><pre><code class="hljs text"># cat /proc/net/softnet_statdcc6cc07 00006e12 000001ef 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000199c3900 00000000 0000001b 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000e576cfcb 00000000 00000016 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000c3c17454 00000000 00000013 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000abb700af 00000000 00000010 00000000 00000000 00000000 00000000 00000000 00000000 00000000 0000000098ceb017 00000000 0000000c 00000000 00000000 00000000 00000000 00000000 00000000 00000000 0000000089c0460b 00000000 0000000c 00000000 00000000 00000000 00000000 00000000 00000000 00000000 000000007d3d0e6a 00000000 0000000e 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000724680ee 00000000 00000008 00000000 00000000 00000000 00000000 00000000 00000000 00000000 0000000068f63700 00000000 00000013 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000b4f004b0 30b8190e 00180514 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000bdfe8473 00037a32 0000022a 00000000 00000000 00000000 00000000 00000000 00000000 00000000 000000008b1c5f73 0001a1ee 000001b5 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000f107894f 0000e7f9 00000181 00000000 00000000 00000000 00000000 00000000 00000000 00000000 000000007663ef78 0000a29f 00000120 00000000 00000000 00000000 00000000 00000000 00000000 00000000 0000000076275161 00008367 000000e2 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000......</code></pre><p>其中: 每一行代表每个 CPU 核的状态统计,从 CPU0 依次往下; 每一列代表一个 CPU 核的各项统计:第一列代表中断处理程序收到的包总数;第二列即代表由于 netdev_max_backlog 队列溢出而被丢弃的包总数。</p><p>可以看到 CPU10 有大量的丢包现象(第二列数据)。</p><p>对 softnet_stat 数据我们做了一些监控,可以看到的定期会有丢包的现象。</p><img src="/posts/irq-not-balance/drop_monitor.png" srcset="/img/loading.gif" class="" title="This is an image"><p>关于丢包,扩展阅读可以看我之前一篇博客 <a href="https://leeweir.github.io/2019/10/28/Linux-%E4%B8%A2%E5%8C%85%E9%82%A3%E4%BA%9B%E4%BA%8B/">linux 丢包那些事</a></p><h2 id="如何处理"><a href="#如何处理" class="headerlink" title="如何处理"></a>如何处理</h2><h3 id="netdev-max-backlog-调优"><a href="#netdev-max-backlog-调优" class="headerlink" title="netdev_max_backlog 调优"></a>netdev_max_backlog 调优</h3><p>netdev_max_backlog 是内核从 NIC 收到包后,交由协议栈(如 IP、TCP )处理之前的缓冲队列。</p><p>从这次现象看,因为超越了设定的 netdev_max_backlog 值,导致数据包被丢弃。netdev_max_backlog 的默认值是 1000,我们可以修改内核参数来调优:</p><pre><code class="hljs text">sysctl -w net.core.netdev_max_backlog=2000</code></pre><h3 id="网卡中断均衡"><a href="#网卡中断均衡" class="headerlink" title="网卡中断均衡"></a>网卡中断均衡</h3><p>调优 netdev_max_backlog 只是一个 workaround 方案,本质还是因为网卡中断分配不均导致,所以我们需要讲中断均衡,方案如下:</p><h4 id="网卡绑定"><a href="#网卡绑定" class="headerlink" title="网卡绑定"></a>网卡绑定</h4><p>网卡绑定就是将网卡中断手动绑定到不同 CPU,前面简单介绍了下 smp_affinity_list,我们再来看下。</p><p><code>/proc/irq/[irq_num]/smp_affinity</code>:</p><p>该文件存放的是 CPU 位掩码(十六进制)。修改该文件中的值可以改变 CPU 和某中断的亲和性。</p><p><code>/proc/irq/[irq_num]/smp_affinity_list</code>:</p><p>该文件存放的是 CPU 列表(十进制)。注意,CPU 核心个数用表示编号从 0 开始,如 CPU0, CPU1 等。</p><p>所以我们手动的对网卡的各个队列进行 CPU 绑定,如下:</p><pre><code class="hljs text">echo 0 > /proc/irq/101/smp_affinity_listecho 0 > /proc/irq/102/smp_affinity_listecho 1 > /proc/irq/103/smp_affinity_listecho 1 > /proc/irq/104/smp_affinity_listecho 2 > /proc/irq/105/smp_affinity_listecho 2 > /proc/irq/106/smp_affinity_list......</code></pre><p>中断绑定后, 我们查看 /proc/interrupts 就可以看到中断会分布在 CPU 多个核上。</p><pre><code class="hljs text">35:49282500000000000036:04211130000000000037:00384418000000000038:00037109100000000039:00003629770000000040:00000352131000000041:00000032660500000042:00000003661710000043:00000000353674000044:00000000032137000045:00000000003427320046:00000000000321248047:000000000000324321</code></pre><h4 id="irqbalance-和-minx-tune"><a href="#irqbalance-和-minx-tune" class="headerlink" title="irqbalance 和 minx_tune"></a>irqbalance 和 minx_tune</h4><p>这两块我想作为扩展阅读,后面提供一些资料,对于 irqbalance 补充一点,irqbalance 是根据系统中断负载的情况,自动迁移中断保持中断的平衡,同时会考虑到省电因素等等。但是在实时系统中会导致中断自动漂移,对性能造成不稳定因素,在高性能的场合建议关闭。</p><p><a href="https://linux.die.net/man/1/irqbalance" target="_blank" rel="noopener">irqbalance- Linux man page</a><br><a href="http://blog.yufeng.info/archives/2422" target="_blank" rel="noopener">霸爷的文章:深度剖析告诉你irqbalance有用吗?</a><br><a href="https://community.mellanox.com/s/article/How-to-Tune-Your-Linux-Server-for-Best-Performance-Using-the-mlnx-tune-Tool" target="_blank" rel="noopener">How to Tune Your Linux Server for Best Performance Using the mlnx tune Tool</a></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>关于 irq 其实是比较常见的问题,在很多优化的场景都需要将 irq 和处理进程绑定到不同的 CPU 上以提供更好的实时响应。博客也介绍了需要关注的几个指标和解决方案,希望记录下来对大家有所帮助。</p>]]></content>
<tags>
<tag>sre</tag>
<tag>linux</tag>
<tag>network</tag>
</tags>
</entry>
<entry>
<title>[译] A deep dive into Kubernetes controllers</title>
<link href="/posts/a-deep-dive-into-kubernetes-controllers/"/>
<url>/posts/a-deep-dive-into-kubernetes-controllers/</url>
<content type="html"><![CDATA[<blockquote><p><a href="https://engineering.bitnami.com/articles/a-deep-dive-into-kubernetes-controllers.html" target="_blank" rel="noopener">原文地址</a></p></blockquote><p>Kubernetes 运行一组控制器,它们负责处理日常任务,以确保集群的期望状态与观察到的状态一致。例如,<a href="https://kubernetes.io/docs/concepts/workloads/controllers/replicationcontroller/" target="_blank" rel="noopener">Replica Sets</a> 维护在集群中运行的正确数量的 pods。<a href="https://kubernetes.io/docs/concepts/architecture/nodes/#node-controller" target="_blank" rel="noopener">Node Controller</a> 查找服务器的状态,并在服务器宕机时做出响应。基本上,每个控制器负责 Kubernetes 世界中的特定资源。对于用户管理他们的集群,用户理解 Kubernetes 中每个控制器的角色是很重要的。然而,你曾经想过 Kubernetes 的控制器是如何工作的吗?更令人兴奋的是,您是否考虑过编写自己的自定义控制器?</p><p>这是两篇系列文章的第一篇,在这篇文章中,我将概述 Kubernetes 控制器的内部结构、基本组件及其工作方式。在第二篇文章中,我将指导您实践如何编写自己的控制器来为集群构建一个简单的通知系统。</p><h2 id="控制器模式"><a href="#控制器模式" class="headerlink" title="控制器模式"></a>控制器模式</h2><p>Kubernetes 控制器的最佳解释可以在 Kubernetes 官方文档网页中找到:</p><blockquote><p>In applications of robotics and automation, a control loop is a non-terminating loop that regulates the state of the system. In Kubernetes, a controller is a control loop that watches the shared state of the cluster through the API server and makes changes attempting to move the current state towards the desired state. Examples of controllers that ship with Kubernetes today are the replication controller, endpoints controller, namespace controller, and serviceaccounts controller.</p></blockquote><blockquote><p>Kubernetes 官方文档, <a href="https://kubernetes.io/docs/admin/kube-controller-manager/" target="_blank" rel="noopener">Kube-controller-manager</a></p></blockquote><p>为了降低复杂性,所有的控制器都打包在一个名为 kube-controller-manager 的守护进程中。控制器最简单的实现是循环:</p><pre><code class="hljs go"><span class="hljs-keyword">for</span> { desired := getDesiredState() current := getCurrentState() makeChanges(desired, current)}</code></pre><h2 id="控制器组件"><a href="#控制器组件" class="headerlink" title="控制器组件"></a>控制器组件</h2><p>一个控制器有两个主要组件: Informer/SharedInformer 和 Workqueue。<br>Informer/SharedInformer 监视 Kubernetes 对象当前状态的变化,并将事件发送到 Workqueue,然后 worker(s) 进行消费处理。</p><h3 id="Informer"><a href="#Informer" class="headerlink" title="Informer"></a>Informer</h3><p>Kubernetes 控制器的关键作用是观察对象的期望状态和实际状态,然后发送指令使实际状态更接近期望状态。为了检索对象的当前状态,控制器会向 Kubernetes API server 发送一个请求。</p><p>但是,从 API server 反复检索信息可能会变得非常昂贵。因此,为了在代码中多次获取和列出对象,Kubernetes 开发人员最终使用了 client-go 库已经提供的缓存。此外,控制器并不想连续发送请求。它只关心对象被创建、修改或删除时的事件。client-go 库提供 Listwatcher 接口,该接口执行初始列表并启动对特定资源的监视:</p><pre><code class="hljs go">lw := cache.NewListWatchFromClient( client, &v1.Pod{}, api.NamespaceAll, fieldSelector)</code></pre><p>这些消息都被 Informer 消费。Informer 的一般结构描述如下:</p><pre><code class="hljs go">store, controller := cache.NewInformer { &cache.ListWatch{}, &v1.Pod{}, resyncPeriod, cache.ResourceEventHandlerFuncs{},</code></pre><p>尽管 Informer 在当前的 Kubernetes 中并没有被大量使用(而是使用 SharedInformer,我将在稍后解释),但它仍然是一个需要理解的基本概念,特别是当您希望编写自定义控制器时。以下是用于构造 Informer 的三种模式:</p><h4 id="LISTWATCHER"><a href="#LISTWATCHER" class="headerlink" title="LISTWATCHER"></a>LISTWATCHER</h4><p>Listwatcher 是针对特定 namespace 中的特定资源的 list 函数和 watch 函数的组合。这有助于控制器只关注它想要查看的特定资源。fieldSelector 是一种筛选器,它缩小了搜索资源的结果范围,就像控制器希望检索与特定字段匹配的资源一样。Listwatcher的结构描述如下:</p><pre><code class="hljs go">cache.ListWatch { listFunc := <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(options metav1.ListOptions)</span> <span class="hljs-params">(runtime.Object, error)</span></span> { <span class="hljs-keyword">return</span> client.Get(). Namespace(namespace). Resource(resource). VersionedParams(&options, metav1.ParameterCodec). FieldsSelectorParam(fieldSelector). Do(). Get() } watchFunc := <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(options metav1.ListOptions)</span> <span class="hljs-params">(watch.Interface, error)</span></span> { options.Watch = <span class="hljs-literal">true</span> <span class="hljs-keyword">return</span> client.Get(). Namespace(namespace). Resource(resource). VersionedParams(&options, metav1.ParameterCodec). FieldsSelectorParam(fieldSelector). Watch() }}</code></pre><h4 id="RESOURCE-EVENT-HANDLER"><a href="#RESOURCE-EVENT-HANDLER" class="headerlink" title="RESOURCE EVENT HANDLER"></a>RESOURCE EVENT HANDLER</h4><p>Resource Event Handler 是控制器接收到变更消息后处理特定资源的地方:</p><pre><code class="hljs go"><span class="hljs-keyword">type</span> ResourceEventHandlerFuncs <span class="hljs-keyword">struct</span> { AddFunc <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(obj <span class="hljs-keyword">interface</span>{})</span></span> UpdateFunc <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(oldObj, newObj <span class="hljs-keyword">interface</span>{})</span></span> DeleteFunc <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(obj <span class="hljs-keyword">interface</span>{})</span></span>}</code></pre><ul><li>AddFunc 在一个新的资源创建时被调用。</li><li>UpdateFunc 在已有的资源被修改时被调用。oldObj 是资源的最后一个已知状态。当重新同步发生时,UpdateFunc 也会被调用,即使没有任何变化,它也会被调用。</li><li>DeleteFunc 在已有资源被删除的时候被调用。它获取资源的最终状态(如果它是已知的)。否则,它将获取类型为 DeletedFinalStateUnknown 的对象。比如 watch 关闭并错过了删除事件,而控制器直到后面重新 watch 才注意到删除事件,就会发生这种情况。</li></ul><h4 id="RESYNCPERIOD"><a href="#RESYNCPERIOD" class="headerlink" title="RESYNCPERIOD"></a>RESYNCPERIOD</h4><p>ResyncPeriod 定义了控制器遍历缓存中所有对象的频率,同时它会调用 UpdateFunc。这提供了一种配置,用于周期性地验证当前状态,并使其和所需的状态保持一致。</p><p>它在控制器可能错过更新或之前的操作失败的情况下非常有用。但是,如果构建自定义控制器,则必须注意 CPU 负载(如果周期太短)。</p><h3 id="SharedInformer"><a href="#SharedInformer" class="headerlink" title="SharedInformer"></a>SharedInformer</h3><p>Informer 创建一组仅由自己使用的资源的本地缓存。但是,在Kubernetes中,有一组控制器在运行并关心多种资源,这意味着会有重叠——一个资源由多个控制器管理。</p><p>在本例中,SharedInformer 帮助在控制器之间创建单个共享缓存。这意味着缓存的资源不会被复制,这样做可以减少系统的内存开销。此外,每个 SharedInformer 只在上游服务器上创建一个表,而不管有多少下游消费者正在消费,这也减少了上游服务器的负载,这对于拥有如此多内部控制器的 kube-controller-manager 是很有用的。</p><p>SharedInformer 已经提供了钩子来接收添加、更新和删除特定资源的通知,它还提供了访问共享缓存和确定何时启动缓存的函数。这为我们节省了与 API Server 的连接、服务器端序列化、反序列化以及缓存的重复开销。</p><pre><code class="hljs go">lw := cache.NewListWatchFromClient(…)sharedInformer := cache.NewSharedInformer(lw, &api.Pod{}, resyncPeriod)</code></pre><h3 id="Workqueue"><a href="#Workqueue" class="headerlink" title="Workqueue"></a>Workqueue</h3><p>SharedInformer 不能跟踪每个控制器的位置(因为它是共享的),所以控制器必须提供自己的队列和重试机制(如果需要)。因此,大多数 Resource Event Handlers 只是将项放置到每个使用者的 Workqueue 中。</p><p>当一个资源更新时,Resource Event Handler 会往 Workqueue 写入一个 key。key 使用 <code><resource_namespace>/<resource_name></code> 格式,如果 <code><resource_namespace></code> 为空,那就是 <code><resource_name></code>。通过这样做,事件通过 key 分解,因此每个使用者都可以使用 worker(s) 来拿到 key,并按顺序处理。这将保证没有多个 workers 在同一时间在同一个 key 上处理。</p><p>Workqueue 在 client-go 中的 <code>client-go/util/workqueue</code>。 现在支持多种 queue 的类型,包括 delayed queue, timed queue 和 rate limiting queue。</p><p>下面是一个创建 rate limiting queue 的例子:</p><pre><code class="hljs go">queue :=workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())</code></pre><p>Workqueue 提供一个便利的函数去管理这些 keys。下面这个图描述 Workqueue 里 key 的生命周期:</p><img src="/posts/a-deep-dive-into-kubernetes-controllers/key-lifecicle-workqueue.png" srcset="/img/loading.gif" class="" title="This is an image"><p>在处理事件失败的情况下,控制器调用 AddRateLimited() 函数,将它的 key 退回到 Workqueue,以便稍后使用预定义的重试次数继续工作。反之,如果进程成功,则可以通过调用 Forget() 函数从 Workqueue 中删除 key。但是,该函数只会阻止 Workqueue 跟踪事件的历史。为了从 Workqueue 中完全删除事件,控制器必须触发 Done() 函数。</p><p>因此,Workqueue 可以处理来自缓存的通知,但问题是,控制器何时应该启动 workers 处理 Workqueue?为了获得最新的状态,控制器应该等待直到缓存完全同步,这有两个原因:</p><ol><li><p>在缓存完成同步之前,列出所有资源是不准确的。</p></li><li><p>缓存/队列会将对单个资源的多次快速更新折叠为最新版本。因此,它必须等到缓存变为空闲后才能实际处理项目,以免浪费中间状态。</p></li></ol><p>下面这段伪代码描述了这个逻辑:</p><pre><code class="hljs go">controller.informer = cache.NewSharedInformer(...)controller.queue = workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())controller.informer.Run(stopCh)<span class="hljs-keyword">if</span> !cache.WaitForCacheSync(stopCh, controller.HasSynched){ log.Errorf(<span class="hljs-string">"Timed out waiting for caches to sync"</span>))}<span class="hljs-comment">// Now start processing</span>controller.runWorker()</code></pre><h2 id="What’s-next"><a href="#What’s-next" class="headerlink" title="What’s next"></a>What’s next</h2><p>到目前为止,我只是给你一个 Kubernetes 控制器的概述:它是什么,它是用来做什么的,它是由什么组件构成的,它是如何工作的。最令人兴奋的是 Kubernetes 允许用户集成他们自己的控制器。第二部分更有趣,我将向您展示一个自定义控制器的用例,并指导您用几行代码编写它:</p><ul><li><a href="https://engineering.bitnami.com/articles/kubewatch-an-example-of-kubernetes-custom-controller.html" target="_blank" rel="noopener">Part II: Kubewatch, an example of Kubernetes custom controller</a></li></ul><p>让我们继续!</p>]]></content>
<tags>
<tag>cloud-native</tag>
</tags>
</entry>
<entry>
<title>Linux 丢包那些事</title>
<link href="/posts/linux-packet-loss/"/>
<url>/posts/linux-packet-loss/</url>
<content type="html"><![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>最近一直在排查一些网络的问题,比如 connect timeout 、read timeout 以及一些丢包的问题,刚好想整理一些东西,方便和团队内及开发分享。</p><p>我们先看下 Linux 系统接收数据包的过程:</p><p><img src="1.png" srcset="/img/loading.gif" alt="图 1 "></p><ol><li>网卡收到数据包。</li><li>将数据包从网卡硬件缓存转移到服务器内存中。</li><li>通知内核处理。</li><li>经过 TCP/IP 协议逐层处理。</li><li>应用程序通过 read() 从 socket buffer 读取数据。</li></ol><h2 id="网卡丢包"><a href="#网卡丢包" class="headerlink" title="网卡丢包"></a>网卡丢包</h2><p>我们先看下ifconfig的输出:</p><pre><code class="hljs text"># ifconfig eth0eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 10.5.224.27 netmask 255.255.255.0 broadcast 10.5.224.255 inet6 fe80::5054:ff:fea4:44ae prefixlen 64 scopeid 0x20<link> ether 52:54:00:a4:44:ae txqueuelen 1000 (Ethernet) RX packets 9525661556 bytes 10963926751740 (9.9 TiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 8801210220 bytes 12331600148587 (11.2 TiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0</code></pre><p>RX(receive) 代表接收报文, TX(transmit) 表示发送报文。</p><ul><li>RX errors: 表示总的收包的错误数量,这包括 too-long-frames 错误,Ring Buffer 溢出错误,crc 校验错误,帧同步错误,fifo overruns 以及 missed pkg 等等。</li><li>RX dropped: 表示数据包已经进入了 Ring Buffer,但是由于内存不够等系统原因,导致在拷贝到内存的过程中被丢弃。</li><li>RX overruns: 表示了 fifo 的 overruns,这是由于 Ring Buffer(aka Driver Queue) 传输的 IO 大于 kernel 能够处理的 IO 导致的,而 Ring Buffer 则是指在发起 IRQ 请求之前的那块 buffer。很明显,overruns 的增大意味着数据包没到 Ring Buffer 就被网卡物理层给丢弃了,而 CPU 无法及时的处理中断是造成 Ring Buffer 满的原因之一,上面那台有问题的机器就是因为 interruprs 分布的不均匀(都压在 core0),没有做 affinity 而造成的丢包。</li><li>RX frame: 表示 misaligned 的 frames。</li></ul><p>对于 TX 的来说,出现上述 counter 增大的原因主要包括 aborted transmission, errors due to carrirer, fifo error, heartbeat erros 以及 windown error,而 collisions 则表示由于 CSMA/CD 造成的传输中断。</p><p>dropped 与 overruns 的区别: dropped,表示这个数据包已经进入到网卡的接收缓存 fifo 队列,并且开始被系统中断处理准备进行数据包拷贝(从网卡缓存 fifo 队列拷贝到系统内存),但由于此时的系统原因(比如内存不够等)导致这个数据包被丢掉,即这个数据包被 Linux 系统丢掉。 overruns,表示这个数据包还没有被进入到网卡的接收缓存 fifo 队列就被丢掉,因此此时网卡的 fifo 是满的。为什么 fifo 会是满的?因为系统繁忙,来不及响应网卡中断,导致网卡里的数据包没有及时的拷贝到系统内存, fifo 是满的就导致后面的数据包进不来,即这个数据包被网卡硬件丢掉。所以,个人觉得遇到 overruns 非0,需要检测cpu负载与cpu中断情况。</p><p><code>netstat -i</code>也会提供每个网卡的接发报文以及丢包的情况:</p><pre><code class="hljs text"># netstat -iKernel Interface tableIface MTU RX-OK RX-ERR RX-DRP RX-OVR TX-OK TX-ERR TX-DRP TX-OVR Flgeth0 1500 9528312730 0 0 0 8803615650 0 0 0 BMRU</code></pre><h2 id="Ring-Buffer-溢出"><a href="#Ring-Buffer-溢出" class="headerlink" title="Ring Buffer 溢出"></a>Ring Buffer 溢出</h2><p>如果硬件或者驱动没有问题,一般网卡丢包是因为设置的缓存区(ring buffer)太小。当网络数据包到达(生产)的速率快于内核处理(消费)的速率时, Ring Buffer 很快会被填满,新来的数据包将被丢弃。</p><p><img src="2.png" srcset="/img/loading.gif" alt="图 2 "></p><p>通过 <code>ethtool</code> 或 <code>/proc/net/dev</code> 可以查看因Ring Buffer满而丢弃的包统计,在统计项中以fifo标识:</p><pre><code class="hljs text"># ethtool -S eth0|grep rx_fiforx_fifo_errors: 0# cat /proc/net/devInter-| Receive | Transmit face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed eth0: 10967216557060 9528860597 0 0 0 0 0 0 12336087749362 8804108661 0 0 0 0 0 0</code></pre><p>如果发现服务器上某个网卡的 fifo 数持续增大,可以去确认 CPU 中断是否分配均匀,也可以尝试增加 Ring Buffer 的大小,通过 ethtool 可以查看网卡设备 Ring Buffer 最大值,修改 Ring Buffer 当前设置:</p><pre><code class="hljs text"># 查看eth0网卡Ring Buffer最大值和当前设置$ ethtool -g eth0Ring parameters for eth0:Pre-set maximums:RX: 4096 RX Mini: 0RX Jumbo: 0TX: 4096 Current hardware settings:RX: 1024 RX Mini: 0RX Jumbo: 0TX: 1024 # 修改网卡eth0接收与发送硬件缓存区大小$ ethtool -G eth0 rx 4096 tx 4096Pre-set maximums:RX: 4096 RX Mini: 0RX Jumbo: 0TX: 4096 Current hardware settings:RX: 4096 RX Mini: 0RX Jumbo: 0TX: 4096</code></pre><h2 id="netdev-max-backlog-溢出"><a href="#netdev-max-backlog-溢出" class="headerlink" title="netdev_max_backlog 溢出"></a>netdev_max_backlog 溢出</h2><p>netdev_max_backlog 是内核从 NIC 收到包后,交由协议栈(如 IP、TCP )处理之前的缓冲队列。每个 CPU 核都有一个 backlog 队列,与 Ring Buffer 同理,当接收包的速率大于内核协议栈处理的速率时, CPU 的 backlog 队列不断增长,当达到设定的 netdev_max_backlog 值时,数据包将被丢弃。</p><pre><code class="hljs text"># cat /proc/net/softnet_stat 2e8f1058 00000000 000000ef 00000000 00000000 00000000 00000000 00000000 00000000 00000000 000000000db6297e 00000000 00000035 00000000 00000000 00000000 00000000 00000000 00000000 00000000 0000000009d4a634 00000000 00000010 00000000 00000000 00000000 00000000 00000000 00000000 00000000 000000000773e4f1 00000000 00000005 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000</code></pre><p>其中: 每一行代表每个 CPU 核的状态统计,从 CPU0 依次往下; 每一列代表一个 CPU 核的各项统计:第一列代表中断处理程序收到的包总数;第二列即代表由于 netdev_max_backlog 队列溢出而被丢弃的包总数。 从上面的输出可以看出,这台服务器统计中,确实有因为 netdev_max_backlog 导致的丢包。</p><p>netdev_max_backlog 的默认值是 1000,在高速链路上,可能会出现上述第二列统计不为 0 的情况,可以通过修改内核参数 net.core.netdev_max_backlog 来解决:</p><pre><code class="hljs shell">sysctl -w net.core.netdev_max_backlog=2000</code></pre><h2 id="Socket-Buffer-溢出"><a href="#Socket-Buffer-溢出" class="headerlink" title="Socket Buffer 溢出"></a>Socket Buffer 溢出</h2><p>Socket 可以屏蔽 linux 内核不同协议的差异,为应用程序提供统一的访问接口。每个 Socket 都有一个读写缓存区。</p><ul><li><p>读缓冲区,缓存远端发来的数据。如果读缓存区已满,就不能再接收新的数据。</p></li><li><p>写缓冲区,缓存了要发出去的数据。如果写缓冲区已满,应用程序的写操作就会阻塞。</p></li></ul><p><img src="5.png" srcset="/img/loading.gif" alt="图 5 "></p><h2 id="半连接队列和全连接队列溢出"><a href="#半连接队列和全连接队列溢出" class="headerlink" title="半连接队列和全连接队列溢出"></a>半连接队列和全连接队列溢出</h2><p>之前有个 <a href="https://leeweir.github.io/2019/10/21/%E4%B8%80%E4%B8%AAconnect-timeout%E6%95%85%E9%9A%9C%E6%8E%92%E6%9F%A5/">connect timeout 的 case</a> ,这篇博客里,我也详细介绍了如何去查看半连接队列和全连接队列,包括如何去优化,这里我不展开写。<br>但是补充一点,在半连接满的情况下,若启用syncookie机制,并不会直接丢弃 SYN 包,而是回复带有 syncookie 的 SYN+ACK 包,设计的目的是防范 SYN Flood 造成正常请求服务不可用。 syncookie 之前也有一篇博客分享,参考 <a href="https://leeweir.github.io/2018/03/23/%E8%B0%88%E8%B0%88syn-cookie%E7%9A%84%E9%97%AE%E9%A2%98/">谈谈 syn-cookie 的问题</a>.</p><h2 id="PAWS"><a href="#PAWS" class="headerlink" title="PAWS"></a>PAWS</h2><p>PAWS 全名 Protect Againest Wrapped Sequence numbers ,目的是解决在高带宽下, TCP 序列号在一次会话中可能被重复使用而带来的问题。</p><p><img src="3.png" srcset="/img/loading.gif" alt="图 3 "></p><p>如上图所示,客户端发送的序列号为 A 的数据包 A1 因某些原因在网络中“迷路”,在一定时间没有到达服务端,客户端超时重传序列号为 A 的数据包 A2 ,接下来假设带宽足够,传输用尽序列号空间,重新使用 A ,此时服务端等待的是序列号为 A 的数据包 A3 ,而恰巧此时前面“迷路”的 A1 到达服务端,如果服务端仅靠序列号A就判断数据包合法,就会将错误的数据传递到用户态程序,造成程序异常。</p><p>PAWS 要解决的就是上述问题,它依赖于 timestamp 机制,理论依据是:在一条正常的 TCP 流中,按序接收到的所有 TCP 数据包中的 timestamp 都应该是单调非递减的,这样就能判断那些 timestamp 小于当前 TCP 流已处理的最大 timestamp 值的报文是延迟到达的重复报文,可以予以丢弃。在上文的例子中,服务器已经处理数据包 Z,而后到来的 A1 包的 timestamp 必然小于 Z 包的 timestamp ,因此服务端会丢弃迟到的 A1 包,等待正确的报文到来。</p><p>PAWS 机制的实现关键是内核保存了 Per-Connection 的最近接收时间戳,如果加以改进,就可以用来优化服务器TIME_WAIT状态的快速回收。</p><p>TIME_WAIT 状态是TCP四次挥手中主动关闭连接的一方需要进入的最后一个状态,并且通常需要在该状态保持 2*MSL (报文最大生存时间),它存在的意义有两个:</p><ol><li><p>可靠地实现 TCP 全双工连接的关闭:关闭连接的四次挥手过程中,最终的 ACK 由主动关闭连接的一方(称为 A )发出,如果这个 ACK 丢失,对端(称为 B )将重发 FIN ,如果 A 不维持连接的 TIME_WAIT 状态,而是直接进入 CLOSED ,则无法重传 ACK , B 端的连接因此不能及时可靠释放。</p></li><li><p>等待“迷路”的重复数据包在网络中因生存时间到期消失:通信双方 A 与 B , A 的数据包因“迷路”没有及时到达 B , A 会重发数据包,当 A 与 B 完成传输并断开连接后,如果 A 不维持 TIME_WAIT 状态 2<em>MSL 时间,便有可能与 B 再次建立相同源端口和目的端口的“新连接”,而前一次连接中“迷路”的报文有可能在这时到达,并被 B 接收处理,造成异常,维持 2</em>MSL 的目的就是等待前一次连接的数据包在网络中消失。</p></li></ol><p>TIME_WAIT 状态的连接需要占用服务器内存资源维持, Linux 内核提供了一个参数来控制 TIME_WAIT 状态的快速回收:tcp_tw_recycle,它的理论依据是:</p><p>在 PAWS 的理论基础上,如果内核保存 Per-Host 的最近接收时间戳,接收数据包时进行时间戳比对,就能避免 TIME_WAIT 意图解决的第二个问题:前一个连接的数据包在新连接中被当做有效数据包处理的情况。这样就没有必要维持 TIME_WAIT 状态 2*MSL 的时间来等待数据包消失,仅需要等待足够的 RTO (超时重传),解决 ACK 丢失需要重传的情况,来达到快速回收 TIME_WAIT 状态连接的目的。</p><p>但上述理论在多个客户端使用 NAT 访问服务器时会产生新的问题:同一个 NAT 背后的多个客户端时间戳是很难保持一致的( timestamp 机制使用的是系统启动相对时间),对于服务器来说,两台客户端主机各自建立的 TCP 连接表现为同一个对端IP的两个连接,按照 Per-Host 记录的最近接收时间戳会更新为两台客户端主机中时间戳较大的那个,而时间戳相对较小的客户端发出的所有数据包对服务器来说都是这台主机已过期的重复数据,因此会直接丢弃。</p><p>通过netstat可以得到因PAWS机制timestamp验证被丢弃的数据包统计:</p><pre><code class="hljs text"># netstat -s |grep -e "passive connections rejected because of time stamp" -e "packets rejects in established connections because of timestamp”387158 passive connections rejected because of time stamp825313 packets rejects in established connections because of timestamp</code></pre><p>通过sysctl查看是否启用了 tcp_tw_recycle 及 tcp_timestamp :</p><pre><code class="hljs text">$ sysctl net.ipv4.tcp_tw_recyclenet.ipv4.tcp_tw_recycle = 1$ sysctl net.ipv4.tcp_timestampsnet.ipv4.tcp_timestamps = 1</code></pre><p>如果服务器作为服务端提供服务,且明确客户端会通过 NAT 网络访问,或服务器之前有7层转发设备会替换客户端源IP时,是不应该开启 tcp_tw_recycle 的,而 timestamps 除了支持 tcp_tw_recycle 外还被其他机制依赖,推荐继续开启:</p><pre><code class="hljs shell">sysctl -w net.ipv4.tcp_tw_recycle=0sysctl -w net.ipv4.tcp_timestamps=1</code></pre><h2 id="包丢在哪里了"><a href="#包丢在哪里了" class="headerlink" title="包丢在哪里了"></a>包丢在哪里了</h2><p>第一个是 dropwatch ,之前 <a href="http://blog.yufeng.info/archives/2497" target="_blank" rel="noopener">霸爷博客</a> 也做过分享。</p><pre><code class="hljs text"># dropwatch -l kasInitalizing kallsyms dbdropwatch> startEnabling monitoring...Kernel monitoring activated.Issue Ctrl-C to stop monitoring1 drops at sk_stream_kill_queues+50 (0xffffffff81687860)1 drops at tcp_v4_rcv+147 (0xffffffff8170b737)1 drops at __brk_limit+1de1308c (0xffffffffa052308c)1 drops at ip_rcv_finish+1b8 (0xffffffff816e3348)1 drops at skb_queue_purge+17 (0xffffffff816809e7)3 drops at sk_stream_kill_queues+50 (0xffffffff81687860)2 drops at unix_stream_connect+2bc (0xffffffff8175a05c)2 drops at sk_stream_kill_queues+50 (0xffffffff81687860)1 drops at tcp_v4_rcv+147 (0xffffffff8170b737)2 drops at sk_stream_kill_queues+50 (0xffffffff81687860)</code></pre><p>第二个是 perf 监视 kfree_skb 事件。</p><pre><code class="hljs text"># perf record -g -a -e skb:kfree_skb^C[ perf record: Woken up 1 times to write data ][ perf record: Captured and wrote 1.212 MB perf.data (388 samples) ]# perf scriptcontainerd 93829 [031] 951470.340275: skb:kfree_skb: skbaddr=0xffff8827bfced700 protocol=0 location=0xffffffff8175a05c 7fff8168279b kfree_skb ([kernel.kallsyms]) 7fff8175c05c unix_stream_connect ([kernel.kallsyms]) 7fff8167650f SYSC_connect ([kernel.kallsyms]) 7fff8167818e sys_connect ([kernel.kallsyms]) 7fff81005959 do_syscall_64 ([kernel.kallsyms]) 7fff81802081 entry_SYSCALL_64_after_hwframe ([kernel.kallsyms]) f908d __GI___libc_connect (/usr/lib64/libc-2.17.so) 13077d __nscd_get_mapping (/usr/lib64/libc-2.17.so) 130c7c __nscd_get_map_ref (/usr/lib64/libc-2.17.so) 0 [unknown] ([unknown])containerd 93829 [031] 951470.340306: skb:kfree_skb: skbaddr=0xffff8827bfcec500 protocol=0 location=0xffffffff8175a05c 7fff8168279b kfree_skb ([kernel.kallsyms]) 7fff8175c05c unix_stream_connect ([kernel.kallsyms]) 7fff8167650f SYSC_connect ([kernel.kallsyms]) 7fff8167818e sys_connect ([kernel.kallsyms]) 7fff81005959 do_syscall_64 ([kernel.kallsyms]) 7fff81802081 entry_SYSCALL_64_after_hwframe ([kernel.kallsyms]) f908d __GI___libc_connect (/usr/lib64/libc-2.17.so) 130ebe __nscd_open_socket (/usr/lib64/libc-2.17.so)</code></pre><p>第三个是tcpdrop,之前我也有一篇 <a href="https://leeweir.github.io/2019/10/25/%E8%AF%91-Linux-bcc-eBPF-tcpdrop/">博客介绍</a>, 它显示了源包和目标包的详细信息,以及 TCP 会话状态(来自内核)、TCP 标志(来自包 TCP 报头)和导致这次丢包的内核堆栈跟踪。</p><pre><code class="hljs text">TIME PID IP SADDR:SPORT > DADDR:DPORT STATE (FLAGS)05:46:07 82093 4 10.74.40.245:50010 > 10.74.40.245:58484 ESTABLISHED (ACK) tcp_drop+0x1 tcp_rcv_established+0x1d5 tcp_v4_do_rcv+0x141 tcp_v4_rcv+0x9b8 ip_local_deliver_finish+0x9b ip_local_deliver+0x6f ip_rcv_finish+0x124 ip_rcv+0x291 __netif_receive_skb_core+0x554 __netif_receive_skb+0x18 process_backlog+0xba net_rx_action+0x265 __softirqentry_text_start+0xf2 irq_exit+0xb6 xen_evtchn_do_upcall+0x30 xen_hvm_callback_vector+0x1af05:46:07 85153 4 10.74.40.245:50010 > 10.74.40.245:58446 ESTABLISHED (ACK) tcp_drop+0x1 tcp_rcv_established+0x1d5 tcp_v4_do_rcv+0x141 tcp_v4_rcv+0x9b8 ip_local_deliver_finish+0x9b ip_local_deliver+0x6f ip_rcv_finish+0x124 ip_rcv+0x291 __netif_receive_skb_core+0x554 __netif_receive_skb+0x18 process_backlog+0xba net_rx_action+0x265 __softirqentry_text_start+0xf2 irq_exit+0xb6 xen_evtchn_do_upcall+0x30 xen_hvm_callback_vector+0x1af</code></pre><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p><img src="4.jpg" srcset="/img/loading.gif" alt="图 4 "></p><p>linux 网络协议栈太深,每一层都有可能出现各种各样的问题,我们需要了解这些原理,同时利用好工具去排查这些问题。同时我们在优化的时候,不要盲目的看别人的优化结果,更重要的是体系化的去了解 linux 协议栈的实现,只有知其所以然,才能结合实际业务特点,得出最合理的优化配置。</p><p>最后我按照倪鹏飞之前的优化,整理了一个表格,方便参考(数值仅供参考,具体配置还需要结合实际场景来调整):</p><p><img src="6.png" srcset="/img/loading.gif" alt="图 6 "></p>]]></content>
<tags>
<tag>sre</tag>
<tag>tcp</tag>
<tag>linux</tag>
</tags>
</entry>
<entry>
<title>[译] Linux bcc/eBPF tcpdrop</title>
<link href="/posts/linux-bcc-tcpdrop/"/>
<url>/posts/linux-bcc-tcpdrop/</url>
<content type="html"><![CDATA[<blockquote><p>最近在看 eBPF 的一些材料,看到 Brendan Gregg 的博客,后面想陆续针对一些主题翻译下,这一篇主要自介绍 tcpdrop,文章比较短,<a href="http://www.brendangregg.com/blog/2018-05-31/linux-tcpdrop.html" target="_blank" rel="noopener">原文地址</a></p></blockquote><p>在调试基于内核的 TCP 数据包丢弃的生产问题时,我记得在Linux 4.7中 Eric Dumazet(Google) 添加了一个名为 tcp_drop() 的新功能,我可以使用 kprobes 和 bcc/eBPF 进行跟踪。</p><p>这需要获得了更多的上下文来解释为什么发生这些丢包。例如:</p><pre><code class="hljs text"># tcpdropTIME PID IP SADDR:SPORT > DADDR:DPORT STATE (FLAGS)05:46:07 82093 4 10.74.40.245:50010 > 10.74.40.245:58484 ESTABLISHED (ACK) tcp_drop+0x1 tcp_rcv_established+0x1d5 tcp_v4_do_rcv+0x141 tcp_v4_rcv+0x9b8 ip_local_deliver_finish+0x9b ip_local_deliver+0x6f ip_rcv_finish+0x124 ip_rcv+0x291 __netif_receive_skb_core+0x554 __netif_receive_skb+0x18 process_backlog+0xba net_rx_action+0x265 __softirqentry_text_start+0xf2 irq_exit+0xb6 xen_evtchn_do_upcall+0x30 xen_hvm_callback_vector+0x1af05:46:07 85153 4 10.74.40.245:50010 > 10.74.40.245:58446 ESTABLISHED (ACK) tcp_drop+0x1 tcp_rcv_established+0x1d5 tcp_v4_do_rcv+0x141 tcp_v4_rcv+0x9b8 ip_local_deliver_finish+0x9b ip_local_deliver+0x6f ip_rcv_finish+0x124 ip_rcv+0x291 __netif_receive_skb_core+0x554 __netif_receive_skb+0x18 process_backlog+0xba net_rx_action+0x265 __softirqentry_text_start+0xf2 irq_exit+0xb6 xen_evtchn_do_upcall+0x30 xen_hvm_callback_vector+0x1af[...]</code></pre><p>这是 <a href="https://github.com/iovisor/bcc/pull/1790" target="_blank" rel="noopener">tcpdrop</a>,一个我为开源 <a href="https://github.com/iovisor/bcc" target="_blank" rel="noopener">bcc</a> 项目编写的新工具。它显示了源包和目标包的详细信息,以及 TCP 会话状态(来自内核)、TCP 标志(来自包 TCP 报头)和导致这次丢包的内核堆栈跟踪。堆栈跟踪有助于回答为什么(您需要查看这些函数背后的代码才能理解它)。这也是网上没有的信息,所以你也不会通过使用包嗅探器( libpcap , tcpdump 等)看到这些。</p><p>我不得不强调Eric补丁中的这个小而重要的变化( <a href="https://patchwork.ozlabs.org/patch/604910/" target="_blank" rel="noopener">tcp: increment sk drop for dropped rx packages</a> )</p><pre><code class="hljs c">@@ <span class="hljs-number">-6054</span>,<span class="hljs-number">7</span> +<span class="hljs-number">6061</span>,<span class="hljs-number">7</span> @@ <span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">tcp_rcv_state_process</span><span class="hljs-params">(struct sock *sk, struct sk_buff *skb)</span></span><span class="hljs-function"> </span><span class="hljs-function"> <span class="hljs-title">if</span> <span class="hljs-params">(!queued)</span> </span>{ discard:- __kfree_skb(skb);+ tcp_drop(sk, skb); } <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;</code></pre><p>从许多路径调用 __kfree_skb() 来释放套接字缓冲区,包括日常的代码路径。跟踪它干扰太多: 你的丢包的代码路径在许多正常路径中很难查找。但是使用新的 tcp_drop() 函数,我可以只跟踪 TCP 丢包。今天在 netcon18 上,我已经对 Eric 提了一些增强的建议,比如在某个地方添加一个 “reason” 参数,以便对丢包的原因进行更人性化的描述。也许 tcp_drop() 也应该成为一个跟踪点。</p><p>这里还有一些值得一提的代码,我的 tcpdrop 工具中的一些 eBPF/C 代码:</p><pre><code class="hljs c">[...] <span class="hljs-comment">// pull in details from the packet headers and the sock struct</span> u16 family = sk->__sk_common.skc_family; <span class="hljs-keyword">char</span> state = sk->__sk_common.skc_state; u16 sport = <span class="hljs-number">0</span>, dport = <span class="hljs-number">0</span>; u8 tcpflags = <span class="hljs-number">0</span>; <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">tcphdr</span> *<span class="hljs-title">tcp</span> = <span class="hljs-title">skb_to_tcphdr</span>(<span class="hljs-title">skb</span>);</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iphdr</span> *<span class="hljs-title">ip</span> = <span class="hljs-title">skb_to_iphdr</span>(<span class="hljs-title">skb</span>);</span> bpf_probe_read(&sport, <span class="hljs-keyword">sizeof</span>(sport), &tcp->source); bpf_probe_read(&dport, <span class="hljs-keyword">sizeof</span>(dport), &tcp->dest); bpf_probe_read(&tcpflags, <span class="hljs-keyword">sizeof</span>(tcpflags), &tcp_flag_byte(tcp)); sport = ntohs(sport); dport = ntohs(dport); <span class="hljs-keyword">if</span> (family == AF_INET) { <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ipv4_data_t</span> <span class="hljs-title">data4</span> = {</span>.pid = pid, .ip = <span class="hljs-number">4</span>}; bpf_probe_read(&data4.saddr, <span class="hljs-keyword">sizeof</span>(u32), &ip->saddr); bpf_probe_read(&data4.daddr, <span class="hljs-keyword">sizeof</span>(u32), &ip->daddr); data4.dport = dport; data4.sport = sport; data4.state = state; data4.tcpflags = tcpflags; data4.stack_id = stack_traces.get_stackid(ctx, <span class="hljs-number">0</span>); ipv4_events.perf_submit(ctx, &data4, <span class="hljs-keyword">sizeof</span>(data4));[...]</code></pre><p>我以前在 bcc 中的 tcp 工具只能使用 struct sock 成员(如 tcplife )。但是这一次我需要数据包信息来查看 TCP 标志和数据包的方向。这是我第一次在 eBPF 中访问 TCP 和 IP 报头。我在 tcpdrop 中添加 skb_to_tcphdr() 和 skb_to_iphdr() 以帮助实现这一点,并为以后的 Python 处理添加了一个新的 tcp bcc 库。我相信随着时间的推移,这些代码会被重用(并得到改进)。</p>]]></content>
<tags>
<tag>sre</tag>
<tag>tcp</tag>
<tag>kernel</tag>
</tags>
</entry>
<entry>
<title>一个 Connect Timeout 故障排查</title>
<link href="/posts/connect-timeout-problem/"/>
<url>/posts/connect-timeout-problem/</url>
<content type="html"><![CDATA[<h2 id="问题描述"><a href="#问题描述" class="headerlink" title="问题描述"></a>问题描述</h2><p>有用户反馈在dubbo的应用发布后,过几分钟之后,调用方会出现大量的connectTimeout。当时在服务端容器上进行了抓包,看到在故障期间,客户端发了syn,但是服务端没有任何响应。</p><img src="/posts/connect-timeout-problem/1.png" srcset="/img/loading.gif" class="" title="This is an image"><h2 id="分析问题"><a href="#分析问题" class="headerlink" title="分析问题"></a>分析问题</h2><p>正常的TCP三次握手:</p><img src="/posts/connect-timeout-problem/2.png" srcset="/img/loading.gif" class="" title="This is an image"><ul><li><p>客户端发送syn给服务端发起握手</p></li><li><p>服务端收到syn后回复syn+ack</p></li><li><p>客户端收到syn+ack后,回复ack给服务端,此时客户端上这个连接,进入established</p></li></ul><p>从问题描述看,客户端发的sys包到服务端后直接没响应了,我初步猜测是syn队列满了,通过netstat -s去查看队列的情况:</p><pre><code class="hljs text">3220 times the listen queue of a socket overflowed3220 SYNs to LISTEN sockets dropped</code></pre><h3 id="syn队列和accept队列"><a href="#syn队列和accept队列" class="headerlink" title="syn队列和accept队列"></a>syn队列和accept队列</h3><p>这里先解释下syn队列和accept队列:</p><img src="/posts/connect-timeout-problem/3.png" srcset="/img/loading.gif" class="" title="This is an image"><p>上图结合三次握手来说看</p><ul><li><p>客户端使用connect()向服务端发起连接请求(发送syn包),此时客户端的TCP的状态为 SYN_SENT</p></li><li><p>服务端在收到SYN包后,将TCP相关信息放到 syn队列中,同时向客户端发送syn+ack。服务端TCP的状态为SYN_RCVD</p></li><li><p>客户端收到服务端的syn+ack后,向服务端发送ack,此时客户端的TCP的状态为 ESTABLISHED。服务端收到ack确认后,从 syn队列里将 TCP 信息取出,并放到 accept队列中,此时服务端的TCP的状态为 ESTABLISHED</p></li></ul><p>我们可以大概了解了syn队列和accept队列,那再看上面的问题,overflowed代表accept队列溢出,droped代表syn队列溢出,发现 3220 SYNs to LISTEN sockets dropped,这个就是代表syn队列溢出吗?</p><h3 id="overflowed和dropped为什么一样多"><a href="#overflowed和dropped为什么一样多" class="headerlink" title="overflowed和dropped为什么一样多"></a>overflowed和dropped为什么一样多</h3><p>这里又引出一个问题,可以看到overflowed和dropped竟然一样多,翻看<a href="https://elixir.bootlin.com/linux/v4.14.67/source/net/ipv4/tcp_ipv4.c#L1414" target="_blank" rel="noopener">内核源码</a>:</p><img src="/posts/connect-timeout-problem/4.png" srcset="/img/loading.gif" class="" title="This is an image"><p>可以看到overflow的时候,tcp dropped也会增加,也就是dropped一定大于等于overflowed。这样看,overflowed和dropped是一样的,只能说明accept队列溢出了,而syn队列溢出为0(3220-3220)。</p><h3 id="进一步分析"><a href="#进一步分析" class="headerlink" title="进一步分析"></a>进一步分析</h3><p>但是按照syn队列和accept队列的设计,accept队列满了应该不影响syn响应,即不影响三次握手。带着这个疑问我们再次翻看了<a href="https://elixir.bootlin.com/linux/v4.14.67/source/net/ipv4/tcp_input.c#L6335" target="_blank" rel="noopener">内核源码</a></p><img src="/posts/connect-timeout-problem/5.png" srcset="/img/loading.gif" class="" title="This is an image"><p>可以看到在建连的时候,会判断accept队列,如果accept队列满了,就会drop,即把这个syn包丢掉了。到这里,基本上原因已经查明了。</p><h3 id="如何优化"><a href="#如何优化" class="headerlink" title="如何优化"></a>如何优化</h3><p>按照之前的分析,大概定位到是因为accept队列满了,我们通过ss -lnt查看下:</p><pre><code class="hljs text">State Recv-Q Send-Q Local Address:Port Peer Address:Port LISTEN 0 50 *:20990 *:*</code></pre><p>上面看到的第二列Send-Q 表示第三列的listen端口上的accept队列最大为50,第一列Recv-Q为accept队列当前使用了多少。</p><blockquote><p>accept队列的大小取决于:min(backlog, somaxconn) . backlog是在socket创建的时候传入的,somaxconn是一个os级别的系统参数。</p></blockquote><blockquote><p>syn队列的大小取决于:max(64, /proc/sys/net/ipv4/tcp_max_syn_backlog)。 不同版本的os会有些差异。</p></blockquote><p>在这个case中,因为用户的程序发布拉入后,有大量的客户端发起请求,从而导致应用的accept队列满了,然后导致syn被丢弃。用户的应用上配置的backlog为50,所以发现这个现象后,通知研发去更新了dubbo应用的backlog,同时进行发布验证,发现问题已经消失了,至此问题解决。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>通过文章对故障的分析,大家了解了syn队列和accept队列的一些知识,同时也了解了如何去观测和优化。其实在生产环境中,这种问题比较常见,也很容易被忽视,希望这篇分析对大家有所帮助。</p><p>最后引用下阿里童鞋的一句话:</p><blockquote><p>每个具体问题都是最好学习的机会,光看书理解肯定是不够深刻的,请珍惜每个具体问题,碰到后能够把来龙去脉弄清楚。</p></blockquote>]]></content>
<tags>
<tag>sre</tag>
<tag>tcp</tag>
</tags>
</entry>
<entry>
<title>MasteringGo 翻译完成</title>
<link href="/posts/masteringgo-translate/"/>
<url>/posts/masteringgo-translate/</url>
<content type="html"><![CDATA[<p>之前,运维或者运维开发很多时候主力语言都是python,python作为动态解释性语言,一方面,较低的运行效率,一些场景无法满足,另一方面,过于灵活的语言特性也导致多人协作和项目维护成本较高。<br>受益于云原生的发展,Golang自然也成为了云计算时代的语言,所以也希望Golang能够替代Python,成为后续团队内的主力开发语言。一方面是自己参加Golang委员会,推动Golang的一些技术落地,另一方面也在团队内,做一些标准规范和技术选型。<br>前段时间,有小伙伴找我一起参与翻译Mastering Go这本书,近期已经快翻译完成了。</p> <img src="/posts/masteringgo-translate/masteringgo.jpg" srcset="/img/loading.gif" class="" title="This is an image"><p>Mastering有很多系列的书籍,这本Mastering Go的内容包括但不限于并发、网络编程、垃圾回收、组合、GO UNIX系统编程、基本数据类型、GO源码、反射,接口,类型方法等高级概念。阅读这本书的时候需要一定的编程基础,最好已经完成了<a href="https://books.studygolang.com/gobyexample/" target="_blank" rel="noopener">Go By Example</a>。</p><p>欢迎大家在<a href="https://wskdsgcf.gitbook.io/mastering-go-zh-cn/" target="_blank" rel="noopener">GitBook: Mastering_Go_ZH_CN</a>上阅读,如果你喜欢本书,你也可以参与到本书的翻译或纠正工作中来,GitHub地址是<a href="https://github.com/hantmac/Mastering_Go_ZH_CN" target="_blank" rel="noopener">Mastering_Go_ZH_CN</a>,具体请联系【Jack E-mail:<a href="mailto:[email protected]">[email protected]</a>】,一同完善本书并帮助壮大 Go 语言在国内的学习群体,给大家提供更好的学习资源。</p>]]></content>
<tags>
<tag>golang</tag>
</tags>
</entry>
<entry>
<title>关于 423 故障</title>
<link href="/posts/about-423/"/>
<url>/posts/about-423/</url>
<content type="html"><![CDATA[<p>其实写这种文章压力很大,因为本身自己写这种总结类文章文采真的很差,刚好昨天聊到了这个话题,我还是想把自己想的一些思考记录下来。</p><p>故障原因和恢复过程就不介绍了,只能说故障确实是暴露问题最充分的渠道,故障后,灾备设计、稳定性工具、容量规划、HA设计等等,都暴露出很多问题。</p>]]></content>
<tags>
<tag>sre</tag>
</tags>
</entry>
<entry>
<title>一台服务器可以发起多少个连接</title>
<link href="/posts/how-many-connections-can-a-server-send/"/>
<url>/posts/how-many-connections-can-a-server-send/</url>
<content type="html"><![CDATA[<p>最近好多同学纠结这个问题,拿出以前分享的ppt发个博客。<br>总结来说,发起多少连接不受源端口限制,只要保证TCP四元组唯一,那么就可以继续建立连接。</p><object data="./1.pdf" type="application/pdf" width="100%" height="877px">]]></content>
<tags>
<tag>tcp</tag>
</tags>
</entry>
<entry>
<title>two-sum</title>
<link href="/posts/two-sum/"/>
<url>/posts/two-sum/</url>
<content type="html"><![CDATA[<p>最近刚好在学golang,所以就拿简单的LeetCode提来练习练习,顺便把结果分享下</p><p>题目描述:</p><p>给定一个整数数组<code>nums</code>和一个目标值<code>target</code>,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。</p><p>你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。</p><p>示例:</p><pre><code class="hljs text">给定 nums = [2, 7, 11, 15], target = 9因为 nums[0] + nums[1] = 2 + 7 = 9所以返回 [0, 1]</code></pre><p>首先第一种办法大家很容易想到,暴力的遍历每一个元素。</p><pre><code class="hljs go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">twoSum</span><span class="hljs-params">(nums []<span class="hljs-keyword">int</span>, target <span class="hljs-keyword">int</span>)</span> []<span class="hljs-title">int</span></span> { <span class="hljs-keyword">var</span> res []<span class="hljs-keyword">int</span> <span class="hljs-keyword">for</span> i1, v1 := <span class="hljs-keyword">range</span> nums { <span class="hljs-keyword">for</span> i2, v2 := <span class="hljs-keyword">range</span> nums[i1+<span class="hljs-number">1</span>:] { <span class="hljs-keyword">if</span> v1+v2 == target { res = []<span class="hljs-keyword">int</span>{i1, i2 + i1 + <span class="hljs-number">1</span>} <span class="hljs-keyword">return</span> res } } } <span class="hljs-keyword">return</span> res}</code></pre><p>暴力法可以看到时间复杂度是O(n<sup>2</sup>),时间复杂度比较高,我们可以用哈希表来优化,通过以空间换取速度的方式,我们可以将查找时间从 O(n)降低到 O(1)。</p><a id="more"></a><pre><code class="hljs go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">twoSum</span><span class="hljs-params">(nums []<span class="hljs-keyword">int</span>, target <span class="hljs-keyword">int</span>)</span> []<span class="hljs-title">int</span></span> { m := <span class="hljs-built_in">make</span>(<span class="hljs-keyword">map</span>[<span class="hljs-keyword">int</span>]<span class="hljs-keyword">int</span>) <span class="hljs-keyword">for</span> i, v := <span class="hljs-keyword">range</span> nums { m[v] = i } <span class="hljs-keyword">for</span> i, v := <span class="hljs-keyword">range</span> nums { v2 := target - v <span class="hljs-keyword">if</span> _, ok := m[v2]; ok && i != m[v2] { res := []<span class="hljs-keyword">int</span>{i, m[v2]} <span class="hljs-keyword">return</span> res } } <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>}</code></pre><p>另外我尝试了先排序后查找的办法,实际测试下来效果也很好。</p><pre><code class="hljs go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">index</span><span class="hljs-params">(nums []<span class="hljs-keyword">int</span>, n <span class="hljs-keyword">int</span>)</span> <span class="hljs-title">int</span></span> { <span class="hljs-keyword">for</span> idx, v := <span class="hljs-keyword">range</span> nums { <span class="hljs-keyword">if</span> n == v { <span class="hljs-keyword">return</span> idx } } <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>}<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">reverseIndex</span><span class="hljs-params">(nums []<span class="hljs-keyword">int</span>, n <span class="hljs-keyword">int</span>)</span> <span class="hljs-title">int</span></span> { <span class="hljs-keyword">for</span> idx := <span class="hljs-built_in">len</span>(nums) - <span class="hljs-number">1</span>; idx >= <span class="hljs-number">0</span>; idx-- { <span class="hljs-keyword">if</span> nums[idx] == n { <span class="hljs-keyword">return</span> idx } } <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>}<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">twoSum</span><span class="hljs-params">(nums []<span class="hljs-keyword">int</span>, target <span class="hljs-keyword">int</span>)</span> []<span class="hljs-title">int</span></span> { sorted := <span class="hljs-built_in">make</span>([]<span class="hljs-keyword">int</span>, <span class="hljs-built_in">len</span>(nums)) <span class="hljs-built_in">copy</span>(sorted, nums) sort.Ints(sorted) i, j, s := <span class="hljs-number">0</span>, <span class="hljs-built_in">len</span>(sorted)<span class="hljs-number">-1</span>, <span class="hljs-number">0</span> <span class="hljs-keyword">for</span> { <span class="hljs-keyword">switch</span> s = sorted[i] + sorted[j]; { <span class="hljs-keyword">case</span> s == target: <span class="hljs-keyword">return</span> []<span class="hljs-keyword">int</span>{index(nums, sorted[i]), reverseIndex(nums, sorted[j])} <span class="hljs-keyword">case</span> s > target: j-- <span class="hljs-keyword">case</span> s < target: i++ } } <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>}</code></pre><p>OK,基本上就是这样,下一篇讲讲golang里的指针吧。</p>]]></content>
<tags>
<tag>golang</tag>
</tags>
</entry>
<entry>
<title>DNS 缓存介绍: NSCD</title>
<link href="/posts/dns-cache-nscd/"/>
<url>/posts/dns-cache-nscd/</url>
<content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>NSCD(name service cache daemon)是我们在linux上最常用的DNS缓存服务,它是glibc网络库的一个组件。基本上来讲我们能见到的一些编程语言和开发框架最终均会调用到glibc的网络解析的函数(如GETHOSTBYNAME or GETHOSTBYADDR等),因此绝大部分程序能够使用NSCD提供的缓存服务。当然如果是应用端自己用socket编写了一个网络client做DNS解析就无法使用NSCD提供的缓存服务,比如DNS领域常见的dig命令不会使用NSCD提供的缓存,相反ping得到的DNS解析结果将使用NSCD提供的缓存。</p><p>当然NSCD不止为DNS提供缓存服务,博客主要介绍DNS(hosts)的缓存。</p><h2 id="NSCD配置"><a href="#NSCD配置" class="headerlink" title="NSCD配置"></a>NSCD配置</h2><p>先看下核心配置:</p><pre><code class="hljs text">reload-count unlimited | number #注意下文会具体说明enable-cache hosts <yes|no> #Enables or disables the specified service cache. The default is no.positive-time-to-live hosts value #success缓存的响应时间,注意,下文会具体说明negative-time-to-live hosts value #非success缓存的响应时间,注意,下文会具体说明</code></pre><h2 id="关于缓存时间"><a href="#关于缓存时间" class="headerlink" title="关于缓存时间"></a>关于缓存时间</h2><p>首先最重要的两个配置:</p><pre><code class="hljs text">positive-time-to-live hosts 60negative-time-to-live hosts 10</code></pre><p>原来我们大部分人都认为,NSCD的缓存是看positive-time-to-live,比如上面配置了60s,那认为DNS会缓存60s。实际查看了代码和测试验证下来,发现配置positive-time-to-live hosts并没有什么用处,代码中hstcache.c主流程中会直接读取DNS报文中的TTL并赋值给需要计算的timeout</p><pre><code class="hljs c"><span class="hljs-comment">/* Compute the timeout time. */</span>dataset->head.ttl = ttl == INT32_MAX ? db->postimeout : ttl;timeout = dataset->head.timeout = t + dataset->head.ttl;</code></pre><p>这说明NSCD的DNS缓存控制是以DNS应答的TTL为主,这个positive-time-to-live配置实际无效。那为什么要有这个配置呢,因为NSCD不止处理DNS同时也处理passwd/group等,其实老版本的NSCD忽略了DNS存在TTL,直到某一个版本(glibc 2.08)中把DNS的TTL给加入了。</p><a id="more"></a><h2 id="关于reload-count"><a href="#关于reload-count" class="headerlink" title="关于reload count"></a>关于reload count</h2><pre><code class="hljs text">reload-count 5</code></pre><p>这个配置其实刚开始看的时候觉得还是比较特别的,我们先看下官方的说明:</p><pre><code class="hljs text">Limit on the number of times a cached entry gets reloaded without being used before it gets removed. The default is 5.</code></pre><p>意思是说,一个域名在不被使用的情况下,NSCD会主动发起DNS请求,如果期间发生解析结果变更会将结果主动更新至NSCD缓存。<br>那多久reload一次呢,再看下源码:</p><pre><code class="hljs c"> <span class="hljs-keyword">if</span> (first && prune_wakeup) { <span class="hljs-comment">/* Perhaps the prune thread for the table is not running in a long</span><span class="hljs-comment"> time. Wake it if necessary. */</span> pthread_mutex_lock (&table->prune_lock); <span class="hljs-keyword">time_t</span> next_wakeup = table->wakeup_time; <span class="hljs-keyword">bool</span> do_wakeup = <span class="hljs-literal">false</span>; <span class="hljs-keyword">if</span> (next_wakeup > packet->timeout + CACHE_PRUNE_INTERVAL){ table->wakeup_time = packet->timeout; do_wakeup = <span class="hljs-literal">true</span>;} pthread_mutex_unlock (&table->prune_lock); <span class="hljs-keyword">if</span> (do_wakeup)pthread_cond_signal (&table->prune_cond); } <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;}</code></pre><p>我们发现是ttl+CACHE_PRUNE_INTERVAL,再看下CACHE_PRUNE_INTERVAL的定义:</p><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> CACHE_PRUNE_INTERVAL 15</span></code></pre><p>所以当一个域名的ttl是60s,那你访问了一次以后,这个缓存会存在 (60 + 15) * (5 + 1) 的时间。</p><p>我们打开NSCD日志验证了下我们的分析:</p><pre><code class="hljs text">Sat 02 Feb 2019 04:27:07 PM CST - 16771: GETHOSTBYNAME (awx.ops.xxx.com)Sat 02 Feb 2019 04:27:07 PM CST - 16771: Haven't found "awx.ops.xxx.com" in hosts cache!Sat 02 Feb 2019 04:27:07 PM CST - 16771: add new entry "awx.ops.xxx.com" of type GETHOSTBYNAME for hosts to cache (first)Sat 02 Feb 2019 04:27:29 PM CST - 16771: considering GETHOSTBYNAME entry "awx.ops.xxx.com", timeout 1549096087Sat 02 Feb 2019 04:28:07 PM CST - 16771: considering GETHOSTBYNAME entry "awx.ops.xxx.com", timeout 1549096087Sat 02 Feb 2019 04:28:22 PM CST - 16771: considering GETHOSTBYNAME entry "awx.ops.xxx.com", timeout 1549096087Sat 02 Feb 2019 04:28:22 PM CST - 16771: Reloading "awx.ops.xxx.com" in hosts cache!Sat 02 Feb 2019 04:29:22 PM CST - 16771: considering GETHOSTBYNAME entry "awx.ops.xxx.com", timeout 1549096162Sat 02 Feb 2019 04:29:37 PM CST - 16771: considering GETHOSTBYNAME entry "awx.ops.xxx.com", timeout 1549096162Sat 02 Feb 2019 04:29:37 PM CST - 16771: Reloading "awx.ops.xxx.com" in hosts cache!Sat 02 Feb 2019 04:29:52 PM CST - 16771: considering GETHOSTBYNAME entry "awx.ops.xxx.com", timeout 1549096237Sat 02 Feb 2019 04:30:07 PM CST - 16771: considering GETHOSTBYNAME entry "awx.ops.xxx.com", timeout 1549096237Sat 02 Feb 2019 04:30:22 PM CST - 16771: considering GETHOSTBYNAME entry "awx.ops.xxx.com", timeout 1549096237Sat 02 Feb 2019 04:30:37 PM CST - 16771: considering GETHOSTBYNAME entry "awx.ops.xxx.com", timeout 1549096237Sat 02 Feb 2019 04:30:52 PM CST - 16771: considering GETHOSTBYNAME entry "awx.ops.xxx.com", timeout 1549096237Sat 02 Feb 2019 04:30:52 PM CST - 16771: Reloading "awx.ops.xxx.com" in hosts cache!Sat 02 Feb 2019 04:31:07 PM CST - 16771: considering GETHOSTBYNAME entry "awx.ops.xxx.com", timeout 1549096312Sat 02 Feb 2019 04:31:22 PM CST - 16771: considering GETHOSTBYNAME entry "awx.ops.xxx.com", timeout 1549096312Sat 02 Feb 2019 04:31:37 PM CST - 16771: considering GETHOSTBYNAME entry "awx.ops.xxx.com", timeout 1549096312Sat 02 Feb 2019 04:31:52 PM CST - 16771: considering GETHOSTBYNAME entry "awx.ops.xxx.com", timeout 1549096312Sat 02 Feb 2019 04:32:07 PM CST - 16771: considering GETHOSTBYNAME entry "awx.ops.xxx.com", timeout 1549096312Sat 02 Feb 2019 04:32:07 PM CST - 16771: Reloading "awx.ops.xxx.com" in hosts cache!Sat 02 Feb 2019 04:32:22 PM CST - 16771: considering GETHOSTBYNAME entry "awx.ops.xxx.com", timeout 1549096387Sat 02 Feb 2019 04:32:37 PM CST - 16771: considering GETHOSTBYNAME entry "awx.ops.xxx.com", timeout 1549096387Sat 02 Feb 2019 04:32:52 PM CST - 16771: considering GETHOSTBYNAME entry "awx.ops.xxx.com", timeout 1549096387Sat 02 Feb 2019 04:33:07 PM CST - 16771: considering GETHOSTBYNAME entry "awx.ops.xxx.com", timeout 1549096387Sat 02 Feb 2019 04:33:22 PM CST - 16771: considering GETHOSTBYNAME entry "awx.ops.xxx.com", timeout 1549096387Sat 02 Feb 2019 04:33:22 PM CST - 16771: Reloading "awx.ops.xxx.com" in hosts cache!Sat 02 Feb 2019 04:33:37 PM CST - 16771: considering GETHOSTBYNAME entry "awx.ops.xxx.com", timeout 1549096462Sat 02 Feb 2019 04:33:52 PM CST - 16771: considering GETHOSTBYNAME entry "awx.ops.xxx.com", timeout 1549096462Sat 02 Feb 2019 04:34:07 PM CST - 16771: considering GETHOSTBYNAME entry "awx.ops.xxx.com", timeout 1549096462Sat 02 Feb 2019 04:34:22 PM CST - 16771: considering GETHOSTBYNAME entry "awx.ops.xxx.com", timeout 1549096462Sat 02 Feb 2019 04:34:37 PM CST - 16771: considering GETHOSTBYNAME entry "awx.ops.xxx.com", timeout 1549096462Sat 02 Feb 2019 04:34:37 PM CST - 16771: remove GETHOSTBYNAME entry "awx.ops.xxx.com"</code></pre><h2 id="关于非success域名的缓存"><a href="#关于非success域名的缓存" class="headerlink" title="关于非success域名的缓存"></a>关于非success域名的缓存</h2><p>查看代码发现对于非success域名的缓存,NSCD会读取配置中的negative-time-to-live,将缓存一个negative-time-to-live + CACHE_PRUNE_INTERVAL的时间</p><pre><code class="hljs c">dataset->head.ttl = ttl == INT32_MAX ? db->negtimeout : ttl; timeout = dataset->head.timeout = t + dataset->head.ttl;</code></pre><p>假如你配置了negative-time-to-live 10,那只能等25秒后NSCD才会发起新的解析。<br>当时当你配置negative-time-to-live 0的时候,默认不会缓存negative cahce,那每次都会向DNS server发起请求,像我们的一些互联网服务场景,强烈要求设置</p><pre><code class="hljs text">negative-time-to-live 0</code></pre><h2 id="关于CNAME-A的结果"><a href="#关于CNAME-A的结果" class="headerlink" title="关于CNAME+A的结果"></a>关于CNAME+A的结果</h2><p>GLIBC的GETHOSTBYNAME/GETHOSTBYADD返回的TTL中直接读取的是A类型的TTL,代码中并没有针对CNAME的TTL做特殊处理,因此在有CNAME+A的级联应答结果中,缓存的timeout将只会读取对应的A记录的TTL。<br>当DNS应答结果只有CNAME时,DNS请求将被判定为失败,这时CNAME的TTL将不起作用,缓存的时间将遵循非success域名的timeout计算。</p><pre><code class="hljs c"><span class="hljs-keyword">return</span> ((qtype == T_A || qtype == T_AAAA) && ap != host_data->aliases ? NSS_STATUS_NOTFOUND : NSS_STATUS_TRYAGAIN);</code></pre><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>使用NSCD对于提升域名解析性能、降低DNS并发数量,对于一部分缺乏DNS缓存的开发框架有辅助作用,但是其也存在明显的缺点:比如域名生效时间要持续一个ttl+15s,对于一部分讲究变更快速生效的域名而言有一定的变更生效延误;做DNS RR的域名将会失去轮询的能力。</p><p>所以对于任何一个服务或者软件,你只有充分的了解它,你才能更好的使用它。</p><p>参考:<br><a href="https://github.com/lattera/glibc" target="_blank" rel="noopener">https://github.com/lattera/glibc</a></p>]]></content>
<tags>
<tag>sre</tag>
<tag>dns</tag>
</tags>
</entry>
<entry>
<title>TLS1.3 初探</title>
<link href="/posts/tls13/"/>
<url>/posts/tls13/</url>
<content type="html"><![CDATA[<p>openssl1.1.1 final正式发布后,准备在线上开始灰度了。虽然之前有一些了解,但是其实没有准确的认知,所以需要再好好的分析一下:</p><h1 id="大体上的区别"><a href="#大体上的区别" class="headerlink" title="大体上的区别"></a>大体上的区别</h1><p>我们先看下tls1.3和tls1.2有什么区别:<br>tls1.3:</p><img src="/posts/tls13/tls1-3-cap.jpg" srcset="/img/loading.gif" class="" title="This is an image"><p>tls1.2:</p><img src="/posts/tls13/tls1-2-cap.jpg" srcset="/img/loading.gif" class="" title="This is an image"><p>从这两张图就可以明显的看出tls1.3和tls1.2的数据包的不同,在tls1.3中,自从ServerHello之后全是Application Data了,甚至一度怀疑是不是wireshark这个软件是不是实现有问题。</p><p>我们看下<a href="https://tools.ietf.org/html/rfc8446" target="_blank" rel="noopener">tls1.3 rfc</a>,如下整理了下<br>tls1.3完整握手:</p><img src="/posts/tls13/tls1-3.jpg" srcset="/img/loading.gif" class="" title="This is an image"><p>tls1.2完整握手:</p><img src="/posts/tls13/tls1-2.jpg" srcset="/img/loading.gif" class="" title="This is an image"><p>通过tls1.3和tls1.2的握手流程图对比,可以明显的看见tls1.3的握手流程比tls1.2的要少两次握手。虽然握手少了两次,但是像服务器证书这些必要的信息并没有减少提供,只是说在tls1.3中通过ClientHello和ServerHello这两步信息交换,就已经完成了tls1.2上的ServerKeyExchange和ClientKeyExchange这两个密钥交换的操作,在tls1.3的ServerHello之后的信息都已经走了加密通道。</p><h1 id="解析ClientHello"><a href="#解析ClientHello" class="headerlink" title="解析ClientHello"></a>解析ClientHello</h1><p>先看一下使用openssl进行tls1.3握手的ClientHello的数据包结构:</p><img src="/posts/tls13/tls1-3-clienthello.jpg" srcset="/img/loading.gif" class="" title="This is an image"><p>看一下rfc中对ClientHello的结构定义:</p><pre><code class="hljs c">uint16 ProtocolVersion; opaque Random[<span class="hljs-number">32</span>]; uint8 CipherSuite[<span class="hljs-number">2</span>]; <span class="hljs-comment">/* Cryptographic suite selector */</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> {</span> ProtocolVersion legacy_version = <span class="hljs-number">0x0303</span>; <span class="hljs-comment">/* TLS v1.2 */</span> Random <span class="hljs-built_in">random</span>; opaque legacy_session_id<<span class="hljs-number">0.</span><span class="hljs-number">.32</span>>; CipherSuite cipher_suites<<span class="hljs-number">2.</span><span class="hljs-number">.2</span>^<span class="hljs-number">16</span><span class="hljs-number">-2</span>>; opaque legacy_compression_methods<<span class="hljs-number">1.</span><span class="hljs-number">.2</span>^<span class="hljs-number">8</span><span class="hljs-number">-1</span>>; Extension extensions<<span class="hljs-number">8.</span><span class="hljs-number">.2</span>^<span class="hljs-number">16</span><span class="hljs-number">-1</span>>; } ClientHello;</code></pre><p>需要注意的是,在rfc文档中已经说明,legacy_version的值必须是0x0303(TLS1.2),并且session_id的长度必须设置为0</p><a id="more"></a><p>现在在看一下tls1.2的ClientHello数据包:</p><img src="/posts/tls13/tls1-2-clienthello.jpg" srcset="/img/loading.gif" class="" title="This is an image"><p>在这里我首先注意到的是两个Version后面的值,这两个值tls1.3和tls1.2是没有区别的,那么又是如何区分出是tls1.2还tls1.3的clientHello呢?在tls1.3的rfc中有说明:</p><pre><code class="hljs text">The "supported_versions" ClientHello extension can be used to negotiate the version of TLS to use, in preference to the legacy_version field of the ClientHello.</code></pre><p>在区分出tls1.3或tls1.2的数据包是通过supported_versions这个扩展字段中区分出来的:</p><img src="/posts/tls13/support-versions.jpg" srcset="/img/loading.gif" class="" title="This is an image"><p>tls1.3的ClientHello增加了很多的扩展,这些扩展先暂时放一边,看一下我感觉最差异的地方:Cipher Suites。</p><h2 id="Cipher-Suites"><a href="#Cipher-Suites" class="headerlink" title="Cipher Suites"></a>Cipher Suites</h2><p>tls1.3现在支持的Cipher_Suites如下:</p><pre><code class="hljs text">+------------------------------+-------------+| Description | Value |+------------------------------+-------------+| TLS_AES_128_GCM_SHA256 | {0x13,0x01} || | || TLS_AES_256_GCM_SHA384 | {0x13,0x02} || | || TLS_CHACHA20_POLY1305_SHA256 | {0x13,0x03} || | || TLS_AES_128_CCM_SHA256 | {0x13,0x04} || | || TLS_AES_128_CCM_8_SHA256 | {0x13,0x05} |+------------------------------+-------------+</code></pre><p>tls1.3 目前定义了这5个密钥套件,从表面上看已经隐藏了密钥交换算法了,因为都是使用ECDH算法。</p><h2 id="更多的扩展"><a href="#更多的扩展" class="headerlink" title="更多的扩展"></a>更多的扩展</h2><p>通过openssl抓包,可以看见多的扩展有这些:</p><pre><code class="hljs text">supported_versionspre_shared_keykey_share</code></pre><h3 id="supported-versions"><a href="#supported-versions" class="headerlink" title="supported_versions"></a>supported_versions</h3><p>客户端使用这个扩展表示它能够支持哪些版本的tls,按照这个扩展中的列举的tls版本进行顺序选择。并且如果该扩展存在那么服务器端必须忽略ClientHello.legacy_version的值(就是那个规定必须写成0x0303的),仅使用supported_versions中的扩展中存在的tls版本,并且忽略该扩展中任何未知的版本。</p><h3 id="pre-shared-key"><a href="#pre-shared-key" class="headerlink" title="pre_shared_key"></a>pre_shared_key</h3><p>该扩展是用来标识在PSK握手中的共享密钥。(ps: 该扩展在使用openssl进行tls1.3握手的时候,没有获取到)</p><h3 id="key-share"><a href="#key-share" class="headerlink" title="key_share"></a>key_share</h3><p>在该扩展中包含端点的密码参数。<br>在rfc中是这样定义的:</p><pre><code class="hljs c"><span class="hljs-class"><span class="hljs-keyword">struct</span> {</span> NamedGroup group; opaque key_exchange<<span class="hljs-number">1.</span><span class="hljs-number">.2</span>^<span class="hljs-number">16</span><span class="hljs-number">-1</span>>; } KeyShareEntry</code></pre><p>在上面的的key_share中选用x25519DH参数。还有可以使用类似secp256r1、secp384r1、secp521r1的DH参数</p><h1 id="解析ServerHello信息"><a href="#解析ServerHello信息" class="headerlink" title="解析ServerHello信息"></a>解析ServerHello信息</h1><p>先看一下openssl响应的ServerHello信息:</p><img src="/posts/tls13/tls1-3-serverhello.jpg" srcset="/img/loading.gif" class="" title="This is an image"><p>在这里可以可以看到服务器端选择了TLS_AES_256_GCM_SHA384这个加密套件,并且只返回了key_share这个扩展信息,并没有pre_shared_key扩展,因此根据rfc文档,那么服务端选择的就是(EC)DHE ,</p><p>在看一眼key_share中的结构,和ClientHello中的key_share结构是一致的,到这里,Client和Server已经完成了密钥交换了,剩下的信息就全部通过加密通道进行传输了。</p><h2 id="如何做密码协商"><a href="#如何做密码协商" class="headerlink" title="如何做密码协商"></a>如何做密码协商</h2><p>Client一般通过ClientHello信息提供这些信息:</p><p>客户端支持的加密套件 (Cipher Suites)<br>客户端支持的椭圆算法:(Supported Groups)<br>客户端支持的签名算法:(Signature_algorithms)<br>客户端支持的可能和PSK一起使用的密钥交换模式:(psk_key_exchange_modes)</p><p>密码协商有两种方式:</p><pre><code class="hljs text">PSKDHE</code></pre><p>如果服务器选择是使用DHE的协商模式,那么,服务器支持的加密套件、椭圆算法和签名算法必须和客户端提供的存在交集(三者都需要)。</p><p>如果服务器选择是使用PSK的协商模式,那么,服务器必须从客户端的psk_key_exchange_modes的扩展中提供的支持的模式。</p><p>如果服务器使用的是PSK模式,那么就会在ServerHello的扩展中添加pre_shared_key这个扩展。</p><p>如果服务器使用的是EC(DHE)模式,那么服务器会在ServerHello的扩展中添加key_share扩展。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>总体介绍大概是这样,后面我会再介绍下session reuse的区别,和如何做到0 RTT。</p>]]></content>
<tags>
<tag>https</tag>
</tags>
</entry>
<entry>
<title>听说你访问了 Ctrip</title>
<link href="/posts/access-ctrip/"/>
<url>/posts/access-ctrip/</url>
<content type="html"><![CDATA[<blockquote><p>昨天和新人交谈的时候,发现他们对网站整体的访问和架构都不清楚,所以想着可以写一篇比较白话一点的技术入门文章,内容上也可以慢慢丰富。</p></blockquote><p>马上就是国庆了,然后你想出去旅游,你打开了Ctrip。下面来看看,当你在浏览器轻轻 <a href="http://www.ctrip.com" target="_blank" rel="noopener">www.ctrip.com</a> 以后发生了什么?</p><p>首先你的浏览器查询了DNS服务器(注:能够使人更方便的访问互联网,而不用去记住能够被机器直接读取的IP地址,例如192.168.1.1),现在DNS服务器将 <a href="http://www.ctrip.com" target="_blank" rel="noopener">www.ctrip.com</a> 转换成IP地址,机器能直接读取了。</p><p>不过浏览器发现,在不同的地区或者不同的网络(电信、联通、移动)的情况下,转换后的IP地址很可能是不一样的,这首先涉及到全局负载均衡【GSLB】(注:相当于几万人的大学,一个食堂不够用,于是学校弄了五个食堂来服务所有的同学,这就叫负载均衡)。第一步,通过DNS解析域名时将你的访问分配到不同的入口,同时尽可能保证你所访问的入口是所有入口中可能较快的一个。</p><p>好了,现在你通过这个入口成功的访问了 <a href="http://www.ctrip.com" target="_blank" rel="noopener">www.ctrip.com</a> 的实际的入口IP地址。这时你产生了一个PV(注: Page View,一次页面访问),每日每个网站的总PV量是形容一个网站规模的重要指标。同时作为一个独立的用户,你这次访问Ctrip的所有页面,均算作一个UV(注:Unique Visitor用户访问)。卖火车票的12306.cn的日PV量最高峰在10亿左右,而UV量却相对比较小,这其中的原因我相信大家都会知道。</p><a id="more"></a><p> 因为同一时刻访问 <a href="http://www.ctrip.com" target="_blank" rel="noopener">www.ctrip.com</a> 的人数过于巨大,所以即便是Ctrip首页页面的服务器,也不可能仅有一台。仅用于生成 <a href="http://www.ctrip.com" target="_blank" rel="noopener">www.ctrip.com</a> 首页的服务器就可能有成百上千台,那么你的一次访问时生成页面给你看的任务便会被分配给其中一台服务器完成。(注:相当于学校有5个食堂,二食堂3窗口老是爆满,因为打菜的是个萌妹子。)</p><p> 这个过程要保证公正、公平、平均(注:这成百上千台服务器每台负担的用户数要差不多,就像食堂不能颠勺),这一很复杂的过程是由几个系统配合完成,其中最关键的便是负载均衡。负载均衡分为L4负载均衡、L7负载均衡。L4负载均衡,商业产品有A10、Netscaler、F5等,开源有LVS;L7负载均衡,比较常用是nginx、haproxy,当然那些商业产品基本上也都支持L7的负载均衡。通过负载均衡,可以把请求公平的分给各个服务器去处理。</p><p>然后经过一系列复杂的逻辑和处理(此处省略…),用于这次给你看的Ctrip首页的内容便生成成功了。</p><p>然后你会发现,首页的加载过程中,浏览器会请求大量的图片和静态资源,可以看到这些资源的是不同的ip返回的response。这便是CDN(Content Delivery Network),即内容分发网络的作用,它利用一些手段保证你访问的地方是离你最近的CDN节点,这样便保证了大流量分散在各地访问的加速节点上,同时用户也获得了更好的访问体验。这些静态资源是缓存在CDN节点上的,当你更新了一张图片的时候,你可以刷新CDN节点上的缓存,这样用户的请求到达CDN节点上后,会向我们的源站发起请求,CDN获取到资源后,会返回给用户,同时把这个资源也缓存起来,再有用户的访问,就直接从缓存里拿。</p>]]></content>
<tags>
<tag>sre</tag>
</tags>
</entry>
<entry>
<title>8.8.8.8 的问题</title>
<link href="/posts/8.8.8.8-problem/"/>
<url>/posts/8.8.8.8-problem/</url>
<content type="html"><![CDATA[<blockquote><p>其实这不是8.8.8.8的问题。</p></blockquote><p>我们的海外站点使用了akamai的海外加速,最近有海外用户报障说访问海外站点缓慢,排查后发现直接访问了国内的源站, 且公共dns使用的是8.8.8.8,可以说非常奇怪。</p><p>相信大家都知道8.8.8.8是谷歌的公共dns地址,我们先来看下技术。</p><p>8.8.8.8完美支持edns-subnet-client(ecs,其实是谷歌自己提出来的协议),怎么理解呢,国内用户设置了8.8.8.8后不必担心访问会被调度到美国,因为8.8.8.8会将用户的ip以ip段的形式携带在自己向权威dns服务器的请求中,大部分cdn厂商的权威dns都支持解析ecs信息,并针对ecs中的用户ip返回用户地区的cdn节点,可以说ecs对cdn特别友好。</p><p>ecs很好,那么海外用户的解析为什么会到国内呢?</p><p>我们再看下8.8.8.8的另外一个技术:anycast,谷歌是有自己的AS号,实现了全球8.8.8.8的全球广播,用户的AS和谷歌的AS的网络距离只有一跳,这是其他dns都无法比拟的。从ipip的ping测试来看,在国外时延为16ms左右,在中国大陆时延为50ms,但是有些地区略有丢包现象(恩啊,你懂的)。</p><p>anycast实质是一种网络技术,它借助于网络中动态路由协议实现服务的负载均衡和冗余,从实现类型上分,可以分为Subnet Anycast和Global Anycast。</p><p>我查看了下谷歌的一些介绍: <a href="https://developers.google.com/speed/public-dns/faq" target="_blank" rel="noopener">谷歌公共DNS</a>,谷歌使用的是Global Anycast,即目标dns server是处于不同网络(自治域)中,用户发送的查询请求会被分配到最优的谷歌边缘网络(<a href="https://peering.google.com/#/infrastructure" target="_blank" rel="noopener">Google Edge Network</a>),然后再向权威发起dns查询(这里的优化就不介绍了)。</p><a id="more"></a><p>我们大概了解了一些技术点,我们再看下问题:海外用户的dns解析请求被指向到了国内。</p><p>咨询了一些专家,初步猜测是海外的一些访问和国内的的访问被分配在同一个自治域中。国内用户的查询后,cdn返回了国内的ip,同时这个请求被当前自治域的公共dns缓存,那当国外用户查询的时候,公共dns就直接返回了缓存的国内ip,从而导致这个问题。</p>]]></content>
<tags>
<tag>sre</tag>
<tag>dns</tag>
</tags>
</entry>
<entry>
<title>Python 格式化工具 Black</title>
<link href="/posts/python-black/"/>
<url>/posts/python-black/</url>
<content type="html"><![CDATA[<p>Black是facebook提供的一个python formatter工具,体验了下确实很不错,现在默认编码格式化就用它了。 名字的是来自福特公司当年说过的一句话:</p><blockquote><p>Any customer can have a car painted any color that he wants so long as it is black.</p></blockquote><p>具体的code style我就不介绍了,可以查看:<a href="https://black.readthedocs.io/en/stable/" target="_blank" rel="noopener">https://black.readthedocs.io/en/stable/</a></p><p>我这边主要讲一下pycharm上的配置:</p><ol><li><p>安装Black</p><pre><code class="hljs plain">$ pip install black</code></pre></li><li><p>查找Black的安装路径</p><pre><code class="hljs plain">(python3) λ which black/c/ProgramData/Anaconda2/envs/python3/Scripts/black</code></pre></li><li><p>打开pycharm,<code>File -> Settings -> Tools -> External Tools</code></p></li></ol><a id="more"></a><ol start="4"><li><p>点击”+”增加tool,具体配置如下:</p><pre><code class="hljs plain">Name: BlackDescription: Black is the uncompromising Python code formatter.Program: c:/ProgramData/Anaconda2/envs/python3/Scripts/blackArguments: $FilePath$ --line-length 79</code></pre><img src="/posts/python-black/pycharm_tool_config.jpg" srcset="/img/loading.gif" class="" title="This is an image"></li><li><p>通过工具栏上的 <code>Tools -> External Tools -> black.</code> 点击format,也可以增加快捷键来操作</p></li></ol><p>效果: format前:</p><pre><code class="hljs python">x = { <span class="hljs-string">'a'</span>:<span class="hljs-number">37</span>,<span class="hljs-string">'b'</span>:<span class="hljs-number">42</span>,<span class="hljs-string">'c'</span>:<span class="hljs-number">927</span>}y = <span class="hljs-string">'hello '</span><span class="hljs-string">'world'</span>z = <span class="hljs-string">'hello '</span>+<span class="hljs-string">'world'</span>a = <span class="hljs-string">'hello {}'</span>.format(<span class="hljs-string">'world'</span>)<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">foo</span> <span class="hljs-params">( object )</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">f</span> <span class="hljs-params">(self )</span>:</span> <span class="hljs-keyword">return</span> <span class="hljs-number">37</span>*-+<span class="hljs-number">2</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">g</span><span class="hljs-params">(self, x,y=<span class="hljs-number">42</span>)</span>:</span> <span class="hljs-keyword">return</span> y<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">f</span> <span class="hljs-params">( a )</span> :</span> <span class="hljs-keyword">return</span> <span class="hljs-number">37</span>+-+a[<span class="hljs-number">42</span>-x : y**<span class="hljs-number">3</span>]</code></pre><p>format后:</p><pre><code class="hljs python">x = {<span class="hljs-string">'a'</span>: <span class="hljs-number">37</span>, <span class="hljs-string">'b'</span>: <span class="hljs-number">42</span>, <span class="hljs-string">'c'</span>: <span class="hljs-number">927</span>}y = <span class="hljs-string">'hello '</span> <span class="hljs-string">'world'</span>z = <span class="hljs-string">'hello '</span> + <span class="hljs-string">'world'</span>a = <span class="hljs-string">'hello {}'</span>.format(<span class="hljs-string">'world'</span>)<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">foo</span><span class="hljs-params">(object)</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">f</span><span class="hljs-params">(self)</span>:</span> <span class="hljs-keyword">return</span> <span class="hljs-number">37</span> * -+<span class="hljs-number">2</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">g</span><span class="hljs-params">(self, x, y=<span class="hljs-number">42</span>)</span>:</span> <span class="hljs-keyword">return</span> y<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">f</span><span class="hljs-params">(a)</span>:</span> <span class="hljs-keyword">return</span> <span class="hljs-number">37</span> + -+a[<span class="hljs-number">42</span> - x:y**<span class="hljs-number">3</span>]</code></pre>]]></content>
<tags>
<tag>python</tag>
</tags>
</entry>
<entry>
<title>skyline timeseries 异常检测算法介绍</title>
<link href="/posts/skyline-timeseries-anomaly-detection/"/>
<url>/posts/skyline-timeseries-anomaly-detection/</url>
<content type="html"><![CDATA[<p>最近重新拾起了异常检测这块内容,所以把skyline预定义的几个算法分析了下,总体来说代码还是比较简单和清楚的,输入是一个timeseries,输出是检测结果(True or False)。</p><h1 id="3-sigma"><a href="#3-sigma" class="headerlink" title="3-sigma"></a>3-sigma</h1><p>一个很直接的异常判定思路是,拿最新3个datapoint的平均值(tail_avg方法)和整个序列比较,看是否偏离总体平均水平太多。怎样算“太多”呢,因为standard deviation表示集合中元素到mean的平均偏移距离,因此最简单就是和它进行比较。这里涉及到3-sigma理论:</p><pre><code class="hljs plain">In statistics, the 68–95–99.7 rule, also known as the three-sigma rule or empirical rule, states that nearly all values lie within 3 standard deviations of the mean in a normal distribution.About 68.27% of the values lie within 1 standard deviation of the mean. Similarly, about 95.45% of the values lie within 2 standard deviations of the mean. Nearly all (99.73%) of the values lie within 3 standard deviations of the mean.</code></pre> <img src="/posts/skyline-timeseries-anomaly-detection/3sigma.png" srcset="/img/loading.gif" class=""><p>简单来说就是:在normal distribution(正态分布)中,99.73%的数据都在偏离mean 3个σ (standard deviation 标准差) 的范围内。如果某些datapoint到mean的距离超过这个范围,则认为是异常的。Skyline初始内置的7个算法几乎都是基于该理论的:</p><h1 id="stddev-from-average"><a href="#stddev-from-average" class="headerlink" title="stddev_from_average"></a>stddev_from_average</h1><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">stddev_from_average</span><span class="hljs-params">(timeseries)</span>:</span> <span class="hljs-string">"""</span><span class="hljs-string"> A timeseries is anomalous if the absolute value of the average of the latest</span><span class="hljs-string"> three datapoint minus the moving average is greater than three standard</span><span class="hljs-string"> deviations of the average. This does not exponentially weight the MA and so</span><span class="hljs-string"> is better for detecting anomalies with respect to the entire series.</span><span class="hljs-string"> """</span> series = pandas.Series([x[<span class="hljs-number">1</span>] <span class="hljs-keyword">for</span> x <span class="hljs-keyword">in</span> timeseries]) mean = series.mean() stdDev = series.std() t = tail_avg(timeseries) <span class="hljs-keyword">return</span> abs(t - mean) > <span class="hljs-number">3</span> * stdDev</code></pre><p>该算法如下:</p><ul><li><p>求timeseries的mean</p></li><li><p>求timeseries的standard deviation</p></li><li><p>求tail_avg到mean的距离,大于3倍的标准差则异常。</p><p>该算法的特点是可以有效屏蔽 “在一个点上突变到很大的异常值但在下一个点回落到正常水平” 的情况,即屏蔽单点毛刺:因为它使用的是末3个点的均值(有效缓和突变),和整个序列比较(均值可能被异常值拉大),导致判断正常。对于需要忽略 “毛刺” 数据的场景而言,该算法比后续的EWMA/mean_subtraction_cumulation等算法更适用(当然也可以改造这些算法,用tail_avg代替last datapoint)。</p></li></ul><a id="more"></a><h1 id="first-hour-average"><a href="#first-hour-average" class="headerlink" title="first_hour_average"></a>first_hour_average</h1><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">first_hour_average</span><span class="hljs-params">(timeseries)</span>:</span> <span class="hljs-string">"""</span><span class="hljs-string"> Calcuate the simple average over one hour, FULL_DURATION seconds ago.</span><span class="hljs-string"> A timeseries is anomalous if the average of the last three datapoints</span><span class="hljs-string"> are outside of three standard deviations of this value.</span><span class="hljs-string"> """</span> last_hour_threshold = time() - (FULL_DURATION - <span class="hljs-number">3600</span>) series = pandas.Series([x[<span class="hljs-number">1</span>] <span class="hljs-keyword">for</span> x <span class="hljs-keyword">in</span> timeseries <span class="hljs-keyword">if</span> x[<span class="hljs-number">0</span>] < last_hour_threshold]) mean = series.mean() stdDev = series.std() t = tail_avg(timeseries) <span class="hljs-keyword">return</span> abs(t - mean) > <span class="hljs-number">3</span> * stdDev</code></pre><p>和上述算法几乎一致,但是不同的是,比对的对象是 最近FULL_DURATION时间段内开始的1小时内 的数据,求出这段datapoint的mean和standard deviation后再用tail_avg进行比较。当FULL_DURATION小于1小时(86400)时,该算法和上一个算法一致。对于那些在一段较长时间内匀速递增/减的metrics,该算法可能会误报。</p><h1 id="stddev-from-moving-average"><a href="#stddev-from-moving-average" class="headerlink" title="stddev_from_moving_average"></a>stddev_from_moving_average</h1><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">stddev_from_moving_average</span><span class="hljs-params">(timeseries)</span>:</span> <span class="hljs-string">"""</span><span class="hljs-string"> A timeseries is anomalous if the absolute value of the average of the latest</span><span class="hljs-string"> three datapoint minus the moving average is greater than three standard</span><span class="hljs-string"> deviations of the moving average. This is better for finding anomalies with</span><span class="hljs-string"> respect to the short term trends.</span><span class="hljs-string"> """</span> series = pandas.Series([x[<span class="hljs-number">1</span>] <span class="hljs-keyword">for</span> x <span class="hljs-keyword">in</span> timeseries]) expAverage = pandas.stats.moments.ewma(series, com=<span class="hljs-number">50</span>) stdDev = pandas.stats.moments.ewmstd(series, com=<span class="hljs-number">50</span>) <span class="hljs-keyword">return</span> abs(series.iget(<span class="hljs-number">-1</span>) - expAverage.iget(<span class="hljs-number">-1</span>)) > <span class="hljs-number">3</span> * stdDev.iget(<span class="hljs-number">-1</span>)</code></pre><p>该算法先求出最后一个datapoint处的EWMA(Exponentially-weighted moving average)mean/std deviation,然后用最后3个datapoint的平均值与之比对,看是否满足3-sigma理论。</p><h1 id="Moving-Average"><a href="#Moving-Average" class="headerlink" title="Moving Average"></a>Moving Average</h1><p>给定一个timeseries和subset长度(比如N天),则datapoint i 的N天 moving average = i之前N天(包括i)的平均值。不停地移动这个长度为N的“窗口”并计算平均值,就得到了一条moving average曲线。</p><p>Moving average常用来消除数据短期内的噪音,显示长期趋势;或者根据已有数据预测未来数据。</p><h2 id="Simple-Moving-Average"><a href="#Simple-Moving-Average" class="headerlink" title="Simple Moving Average"></a>Simple Moving Average</h2><p>这是最简单的moving average,为“窗口”内datapoints的算数平均值(每个datapoint的weight一样):</p><pre><code class="hljs plain">SMA(i) = [p(i) + p(i-1) + … + p(i-n+1) ]/ n</code></pre><p>当计算i+1处的SMA时,一个新的值加入,“窗口”左端的值丢弃,因此可得到递推式:</p><pre><code class="hljs plain">SMA(i) = SMA(i-1) + p(i)/n – p(i-n+1)/n</code></pre><p>实现起来也很容易,只要记录上次SMA和将要丢弃的datapoint即可(最开始的几个是没有SMA的)。Pandas中可用 pandas.stats.moments.rolling_mean 计算SMA。</p><p>SMA由于过去的数据和现在的数据权重是一样的,因此它相对真实数据的走向存在延迟,不太适合预测,更适合观察长期趋势。</p><h2 id="Exponential-moving-average"><a href="#Exponential-moving-average" class="headerlink" title="Exponential moving average"></a>Exponential moving average</h2><p>也称 Exponential-weighted moving average,它和SMA主要有两处不同:</p><p>计算SMA仅“窗口”内的n个datapoint参与计算,而EWMA则是之前所有point;<br>EWMA计算average时每个datapoint的权重是不一样的,最近的datapoint拥有越高的权重,随时间呈指数递减。<br>EWMA的递推公式是:</p><pre><code class="hljs plain">EWMA(1) = p(1) // 有时也会取前若干值的平均值。α越小时EWMA(1)的取值越重要。EWMA(i) = α * p(i) + (1-α) * EWMA(i – 1) // α是一个0-1间的小数,称为smoothing factor.</code></pre><p>可以看到比SMA更容易实现,只要维护上次EWMA即可。</p><p>EWMA 的本质其实是,越老的数据在预测时占的比例越低。扩展以上公式可以看到,从i往前的datapoint,权重依次为α, α(1-α), α(1-α)^2….., α(1-α)^n,呈指数递减,权重的和的极限等于1。</p><p>smoothing factor决定了EWMA的 <strong>时效性</strong> 和 <strong>稳定性</strong>。α越大时效性越好,越能反映出最近数据状态;α越小越平滑,越能吸收瞬时波动,反映出长期趋势。</p><p>EWMA由于其时效性被广泛应用在“根据已有时间序列预测未来数据”的场景中,(在计算机领域)比较典型的应用是在TCP中估计RTT,即从已有的RTT数据计算未来RTT,以确定超时时间。</p><p>虽然EWMA中参与计算的是全部datapoint,但它也有类似SMA “N天EWMA”的概念,此时α由N决定:α = 2/(N+1),关于这个公式的由来参见这里。</p><p>回到Skyline,这里并不是用EWMA来预测未来datapoint,而是类似之前的算法求出整体序列的mean和stdDev,只不过计算时加入了时间的权重(EWMA),越近的数据对结果影响越大,即判断的参照物是最近某段时间而非全序列; 再用last datapoint与之比较。因此它的优势在于:</p><ul><li>可以检测到一个异常较短时间后发生的另一个(不太高的突变型)异常,其他算法很有可能会忽略,因为第一个异常把整体的平均水平和标准差都拉高了</li><li>比其他算法更快对异常作出反应,因为它更多的是参考突变之前的点(低水平),而非总体水平(有可能被某个异常或者出现较多次的较高的统计数据拉高)</li></ul><p>而劣势则是</p><ul><li>对渐进型而非突发型的异常检测能力较弱</li><li>异常持续一段时间后可能被判定为正常</li></ul><h1 id="mean-subtraction-cumulation"><a href="#mean-subtraction-cumulation" class="headerlink" title="mean_subtraction_cumulation"></a>mean_subtraction_cumulation</h1><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">mean_subtraction_cumulation</span><span class="hljs-params">(timeseries)</span>:</span> <span class="hljs-string">"""</span><span class="hljs-string"> A timeseries is anomalous if the value of the next datapoint in the</span><span class="hljs-string"> series is farther than three standard deviations out in cumulative terms</span><span class="hljs-string"> after subtracting the mean from each data point.</span><span class="hljs-string"> """</span> series = pandas.Series([x[<span class="hljs-number">1</span>] <span class="hljs-keyword">if</span> x[<span class="hljs-number">1</span>] <span class="hljs-keyword">else</span> <span class="hljs-number">0</span> <span class="hljs-keyword">for</span> x <span class="hljs-keyword">in</span> timeseries]) series = series - series[<span class="hljs-number">0</span>:len(series) - <span class="hljs-number">1</span>].mean() stdDev = series[<span class="hljs-number">0</span>:len(series) - <span class="hljs-number">1</span>].std() expAverage = pandas.stats.moments.ewma(series, com=<span class="hljs-number">15</span>) <span class="hljs-keyword">return</span> abs(series.iget(<span class="hljs-number">-1</span>)) > <span class="hljs-number">3</span> * stdDev</code></pre><p>算法如下:</p><ul><li>排除全序列(暂称为all)最后一个值(last datapoint),求剩余序列(暂称为rest,0..length-2)的mean;</li><li>rest序列中每个元素减去rest的mean,再求standard deviation;</li><li>求last datapoint到rest mean的距离,即 abs(last datapoint – rest mean);</li><li>判断上述距离是否超过rest序列std. dev.的3倍。</li></ul><p>简单地说,就是用最后一个datapoint和剩余序列比较,比较的过程依然遵循3-sigma。这个算法有2个地方很可疑:</p><ul><li>求剩余序列的std. dev.时先减去mean再求,这一步是多余的,对结果没影响;</li><li>虽然用tail_avg已经很不科学了,这个算法更进一步,只判断最后一个datapoint是否异常,这要求在两次analysis间隔中最多只有一个datapoint被加入,否则就会丢失数据。关于这个问题的讨论,见这篇文章最末。</li></ul><p>和stddev_from_average相比,该算法对于 “毛刺” 判断为异常的概率远大于后者。</p><h1 id="least-squares"><a href="#least-squares" class="headerlink" title="least_squares"></a>least_squares</h1><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">least_squares</span><span class="hljs-params">(timeseries)</span>:</span> <span class="hljs-string">"""</span><span class="hljs-string"> A timeseries is anomalous if the average of the last three datapoints</span><span class="hljs-string"> on a projected least squares model is greater than three sigma.</span><span class="hljs-string"> """</span> x = np.array([t[<span class="hljs-number">0</span>] <span class="hljs-keyword">for</span> t <span class="hljs-keyword">in</span> timeseries]) y = np.array([t[<span class="hljs-number">1</span>] <span class="hljs-keyword">for</span> t <span class="hljs-keyword">in</span> timeseries]) A = np.vstack([x, np.ones(len(x))]).T results = np.linalg.lstsq(A, y) residual = results[<span class="hljs-number">1</span>] m, c = np.linalg.lstsq(A, y)[<span class="hljs-number">0</span>] errors = [] <span class="hljs-keyword">for</span> i, value <span class="hljs-keyword">in</span> enumerate(y): projected = m * x[i] + c error = value - projected errors.append(error) <span class="hljs-keyword">if</span> len(errors) < <span class="hljs-number">3</span>: <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span> std_dev = scipy.std(errors) t = (errors[<span class="hljs-number">-1</span>] + errors[<span class="hljs-number">-2</span>] + errors[<span class="hljs-number">-3</span>]) / <span class="hljs-number">3</span> <span class="hljs-keyword">return</span> abs(t) > std_dev * <span class="hljs-number">3</span> <span class="hljs-keyword">and</span> round(std_dev) != <span class="hljs-number">0</span> <span class="hljs-keyword">and</span> round(t) != <span class="hljs-number">0</span></code></pre><ul><li>用最小二乘法得到一条拟合现有datapoint value的直线;</li><li>用实际value和拟合value的差值组成一个新的序列error;</li><li>求该序列的stdDev,判断序列error的tail_avg是否>3倍的stdDev</li></ul><p>因为最小二乘法的关系,该算法对直线形的metrics比较适用。该算法也有一个问题,在最后判定的时候,不是用tail_avg到error序列的mean的距离,而是直接使用tail_avg的值,这无形中缩小了异常判定范围,也不符合3-sigma。</p><h1 id="grubbs"><a href="#grubbs" class="headerlink" title="grubbs"></a>grubbs</h1><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">grubbs</span><span class="hljs-params">(timeseries)</span>:</span> <span class="hljs-string">"""</span><span class="hljs-string"> A timeseries is anomalous if the Z score is greater than the Grubb's score.</span><span class="hljs-string"> """</span> series = scipy.array([x[<span class="hljs-number">1</span>] <span class="hljs-keyword">for</span> x <span class="hljs-keyword">in</span> timeseries]) stdDev = scipy.std(series) mean = np.mean(series) tail_average = tail_avg(timeseries) z_score = (tail_average - mean) / stdDev len_series = len(series) threshold = scipy.stats.t.isf(<span class="hljs-number">.05</span> / (<span class="hljs-number">2</span> * len_series), len_series - <span class="hljs-number">2</span>) threshold_squared = threshold * threshold grubbs_score = ((len_series - <span class="hljs-number">1</span>) / np.sqrt(len_series)) * np.sqrt( threshold_squared / (len_series - <span class="hljs-number">2</span> + threshold_squared)) <span class="hljs-keyword">return</span> z_score > grubbs_score</code></pre><p>Grubbs测试是一种从样本中找出outlier的方法,所谓outlier,是指样本中偏离平均值过远的数据,他们有可能是极端情况下的正常数据,也有可能是测量过程中的错误数据。使用Grubbs测试需要总体是正态分布的。</p><p>Grubbs测试步骤如下:</p><ul><li>样本从小到大排序;</li><li>求样本的mean和std.dev.;</li><li>计算min/max与mean的差距,更大的那个为可疑值;</li><li>求可疑值的z-score (standard score),如果大于Grubbs临界值,那么就是outlier;</li><li>Grubbs临界值可以查表得到,它由两个值决定:检出水平α(越严格越小),样本数量n</li><li>排除outlier,对剩余序列循环做 1-5 步骤。</li></ul><p>由于这里需要的是异常判定,只需要判断tail_avg是否outlier即可。代码中还有求Grubbs临界值的过程,看不懂。</p><blockquote><p><strong>Z-score (standard score)</strong></p><p>标准分,一个个体到集合mean的偏离,以标准差为单位,表达个体距mean相对“平均偏离水平(std dev表达)”的偏离程度,常用来比对来自不同集合的数据。</p></blockquote><h1 id="histogram-bins"><a href="#histogram-bins" class="headerlink" title="histogram_bins"></a>histogram_bins</h1><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">histogram_bins</span><span class="hljs-params">(timeseries)</span>:</span> <span class="hljs-string">"""</span><span class="hljs-string"> A timeseries is anomalous if the average of the last three datapoints falls</span><span class="hljs-string"> into a histogram bin with less than 20 other datapoints (you'll need to tweak</span><span class="hljs-string"> that number depending on your data)</span><span class="hljs-string"></span><span class="hljs-string"> Returns: the size of the bin which contains the tail_avg. Smaller bin size</span><span class="hljs-string"> means more anomalous.</span><span class="hljs-string"> """</span> series = scipy.array([x[<span class="hljs-number">1</span>] <span class="hljs-keyword">for</span> x <span class="hljs-keyword">in</span> timeseries]) t = tail_avg(timeseries) h = np.histogram(series, bins=<span class="hljs-number">15</span>) bins = h[<span class="hljs-number">1</span>] <span class="hljs-keyword">for</span> index, bin_size <span class="hljs-keyword">in</span> enumerate(h[<span class="hljs-number">0</span>]): <span class="hljs-keyword">if</span> bin_size <= <span class="hljs-number">20</span>: <span class="hljs-comment"># Is it in the first bin?</span> <span class="hljs-keyword">if</span> index == <span class="hljs-number">0</span>: <span class="hljs-keyword">if</span> t <= bins[<span class="hljs-number">0</span>]: <span class="hljs-keyword">return</span> <span class="hljs-literal">True</span> <span class="hljs-comment"># Is it in the current bin?</span> <span class="hljs-keyword">elif</span> bins[index] <= t < bins[index + <span class="hljs-number">1</span>]: <span class="hljs-keyword">return</span> <span class="hljs-literal">True</span> <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span></code></pre><p>该算法和以上都不同,它首先将timeseries划分成15个宽度相等的直方,然后判断tail_avg所在直方内的元素是否<=20,如果是,则异常。</p><p>直方的个数和元素个数判定需要根据自己的metrics调整,不然在数据量小的时候很容易就异常了。</p><h1 id="ks-test"><a href="#ks-test" class="headerlink" title="ks_test"></a>ks_test</h1><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">ks_test</span><span class="hljs-params">(timeseries)</span>:</span> <span class="hljs-string">"""</span><span class="hljs-string"> A timeseries is anomalous if 2 sample Kolmogorov-Smirnov test indicates</span><span class="hljs-string"> that data distribution for last 10 minutes is different from last hour.</span><span class="hljs-string"> It produces false positives on non-stationary series so Augmented</span><span class="hljs-string"> Dickey-Fuller test applied to check for stationarity.</span><span class="hljs-string"> """</span> hour_ago = time() - <span class="hljs-number">3600</span> ten_minutes_ago = time() - <span class="hljs-number">600</span> reference = scipy.array([x[<span class="hljs-number">1</span>] <span class="hljs-keyword">for</span> x <span class="hljs-keyword">in</span> timeseries <span class="hljs-keyword">if</span> hour_ago <= x[<span class="hljs-number">0</span>] < ten_minutes_ago]) probe = scipy.array([x[<span class="hljs-number">1</span>] <span class="hljs-keyword">for</span> x <span class="hljs-keyword">in</span> timeseries <span class="hljs-keyword">if</span> x[<span class="hljs-number">0</span>] >= ten_minutes_ago]) <span class="hljs-keyword">if</span> reference.size < <span class="hljs-number">20</span> <span class="hljs-keyword">or</span> probe.size < <span class="hljs-number">20</span>: <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span> ks_d, ks_p_value = scipy.stats.ks_2samp(reference, probe) <span class="hljs-keyword">if</span> ks_p_value < <span class="hljs-number">0.05</span> <span class="hljs-keyword">and</span> ks_d > <span class="hljs-number">0.5</span>: adf = sm.tsa.stattools.adfuller(reference, <span class="hljs-number">10</span>) <span class="hljs-keyword">if</span> adf[<span class="hljs-number">1</span>] < <span class="hljs-number">0.05</span>: <span class="hljs-keyword">return</span> <span class="hljs-literal">True</span> <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span></code></pre><p>这个算法比较高深,它将timeseries分成两段:最近10min(probe),1 hour前 -> 10 min前这50分钟内(reference),两个样本通过Kolmogorov-Smirnov测试后判断差异是否较大。如果相差较大,则对refercence这段样本进行 Augmented Dickey-Fuller 检验(ADF检验),查看其平稳性,如果是平稳的,说明存在从平稳状态(50分钟)到另一个差异较大状态(10分钟)的突变,序列认为是异常的。</p><p>关于这两个检验过于学术了,以上只是我粗浅的理解。</p><p>Kolmogorov-Smirnov test<br>KS-test有两个典型应用:</p><blockquote><p>判断某个样本是否满足某个已知的理论分布,如正态/指数/均匀/泊松分布;<br>判断两个样本背后的总体是否可能有相同的分布,or 两个样本间是否可能来自同一总> 体, or 两个样本是否有显著差异。<br>检验返回两个值:D,p-value,不太明白他们的具体含义,Skyline里当 p-value < 0.05 && D > 0.5 时,认为差异显著。</p></blockquote><p>Augmented Dickey-Fuller test (ADF test)</p><blockquote><p>用于检测时间序列的平稳性,当返回的p-value小于给定的显著性水平时,序列认为是平稳的,Skyline取的临界值是0.05。</p></blockquote><h1 id="median-absolute-deviation"><a href="#median-absolute-deviation" class="headerlink" title="median_absolute_deviation"></a>median_absolute_deviation</h1><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">median_absolute_deviation</span><span class="hljs-params">(timeseries)</span>:</span> <span class="hljs-string">"""</span><span class="hljs-string"> A timeseries is anomalous if the deviation of its latest datapoint with</span><span class="hljs-string"> respect to the median is X times larger than the median of deviations.</span><span class="hljs-string"> """</span> series = pandas.Series([x[<span class="hljs-number">1</span>] <span class="hljs-keyword">for</span> x <span class="hljs-keyword">in</span> timeseries]) median = series.median() demedianed = np.abs(series - median) median_deviation = demedianed.median() <span class="hljs-comment"># The test statistic is infinite when the median is zero,</span> <span class="hljs-comment"># so it becomes super sensitive. We play it safe and skip when this happens.</span> <span class="hljs-keyword">if</span> median_deviation == <span class="hljs-number">0</span>: <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span> test_statistic = demedianed.iget(<span class="hljs-number">-1</span>) / median_deviation <span class="hljs-comment"># Completely arbitary...triggers if the median deviation is</span> <span class="hljs-comment"># 6 times bigger than the median</span> <span class="hljs-keyword">if</span> test_statistic > <span class="hljs-number">6</span>: <span class="hljs-keyword">return</span> <span class="hljs-literal">True</span></code></pre><p>该算法不是基于mean/ standard deviation的,而是基于median / median of deviations (MAD)。</p><p>Median</p><blockquote><p>大部分情况下我们用mean来表达一个集合的平均水平(average),但是在某些情况下存在少数极大或极小的outlier,拉高或拉低了(skew)整体的mean,造成估计的不准确。此时可以用median(中位数)代替mean描述平均水平。Median的求法很简单,集合排序中间位置即是,如果集合总数为偶数,则取中间二者的平均值。</p></blockquote><p>Median of deviation(MAD)</p><blockquote><p>同mean一样,对于median我们也需要类似standard deviation这样的指标来表达数据的紧凑/分散程度,即偏离average的平均距离,这就是MAD。MAD顾名思义,是deviation的median,而此时的deviation = abs( 个体 – median ),避免了少量outlier对结果的影响,更robust。</p></blockquote><p>现在算法很好理解了:求出序列的MAD,看最后datapoint与MAD的距离是否 > 6个MAD。同样的,这里用最后一个datapoint判定,依然存在之前提到的问题;其次,6 是个“magic number”,需要根据自己metrics数据特点调整。</p><p>该算法的优势在于对异常更加敏感:假设metric突然变很高并保持一段时间,基于标准差的算法可能在异常出现较短时间后即判断为正常,因为少量outlier对标准差的计算是有影响的;而计算MAD时,若异常datapoint较少会直接忽略,因此感知异常的时间会更长。</p><p>但正如Median的局限性,该算法对于由多个cluster组成的数据集,即数据分布在几个差距较大的区间内,效果很差,很容易误判。</p>]]></content>
<tags>
<tag>sre</tag>
<tag>aiops</tag>
</tags>
</entry>
<entry>
<title>测试几个公共 DNS 的性能</title>
<link href="/posts/common-local-dns/"/>
<url>/posts/common-local-dns/</url>
<content type="html"><![CDATA[<p>最近CloudFlare推出了自己的免费DNS解析器:1.1.1.1,可以说CloudFlare是我比较喜欢的一个公司之一,当时在想具体性能怎么样,所以找了个工具本地测试了下。<br>Shell脚本测试公共dns的性能,可以测试各个dns server的解析速度,标准输出如下:</p><pre><code class="hljs shell">{ dnsperftest } master » bash ./dnstest.sh |sort -k 22 -n test1 test2 test3 test4 test5 test6 test7 test8 test9 test10 Averagenorton 32 ms 32 ms 32 ms 32 ms 29 ms 31 ms 31 ms 31 ms 37 ms 33 ms 32.00google 7 ms 50 ms 6 ms 4 ms 62 ms 225 ms 6 ms 48 ms 5 ms 48 ms 46.10opendns 6 ms 73 ms 4 ms 4 ms 71 ms 342 ms 5 ms 74 ms 5 ms 73 ms 65.70neustar 28 ms 117 ms 28 ms 26 ms 112 ms 117 ms 26 ms 116 ms 28 ms 113 ms 71.10quad9 27 ms 199 ms 24 ms 25 ms 182 ms 191 ms 26 ms 185 ms 24 ms 191 ms 107.40cleanbrowsing 4 ms 217 ms 5 ms 5 ms 219 ms 219 ms 4 ms 219 ms 4 ms 219 ms 111.50cloudflare 4 ms 234 ms 4 ms 4 ms 226 ms 226 ms 3 ms 258 ms 3 ms 232 ms 119.40yandex 5 ms 212 ms 4 ms 5 ms 265 ms 251 ms 4 ms 205 ms 5 ms 241 ms 119.70adguard 7 ms 258 ms 7 ms 6 ms 233 ms 242 ms 7 ms 234 ms 5 ms 257 ms 125.60comodo 4 ms 281 ms 4 ms 4 ms 326 ms 327 ms 6 ms 325 ms 6 ms 324 ms 160.70</code></pre><p>具体脚本:</p><pre><code class="hljs shell"><span class="hljs-meta">#</span><span class="bash">!/usr/bin/env bash</span>command -v bc > /dev/null || { echo "bc was not found. Please install bc."; exit 1; }{ command -v drill > /dev/null && dig=drill; } || { command -v dig > /dev/null && dig=dig; } || { echo "dig was not found. Please install dnsutils."; exit 1; }NAMESERVERS=`cat /etc/resolv.conf | grep ^nameserver | cut -d " " -f 2 | sed 's/\(.*\)/&#&/'`PROVIDERS="1.1.1.1#cloudflare4.2.2.1#level38.8.8.8#google9.9.9.9#quad980.80.80.80#freenom208.67.222.123#opendns199.85.126.20#norton185.228.168.168#cleanbrowsing77.88.8.7#yandex176.103.130.132#adguard156.154.70.3#neustar8.26.56.26#comodo"<span class="hljs-meta">#</span><span class="bash"> Domains to <span class="hljs-built_in">test</span>. Duplicated domains are ok</span>DOMAINS2TEST="www.google.com amazon.com facebook.com www.youtube.com www.reddit.com wikipedia.org twitter.com gmail.com www.google.com whatsapp.com"totaldomains=0printf "%-18s" ""for d in $DOMAINS2TEST; do totaldomains=$((totaldomains + 1)) printf "%-8s" "test$totaldomains"doneprintf "%-8s" "Average"echo ""for p in $NAMESERVERS $PROVIDERS; do pip=${p%%#*} pname=${p##*#} ftime=0 printf "%-18s" "$pname" for d in $DOMAINS2TEST; do ttime=`$dig +tries=1 +time=2 +stats @$pip $d |grep "Query time:" | cut -d : -f 2- | cut -d " " -f 2` if [ -z "$ttime" ]; then #let's have time out be 1s = 1000ms ttime=1000 elif [ "x$ttime" = "x0" ]; then ttime=1 fi printf "%-8s" "$ttime ms" ftime=$((ftime + ttime)) done avg=`bc -lq <<< "scale=2; $ftime/$totaldomains"` echo " $avg"doneexit 0;</code></pre><p>参考:<br><a href="https://blog.cloudflare.com/announcing-1111/" target="_blank" rel="noopener">https://blog.cloudflare.com/announcing-1111/</a></p>]]></content>
<tags>
<tag>dns</tag>
</tags>
</entry>
<entry>
<title>谈谈 syn cookie 的问题</title>
<link href="/posts/syn-cookie-problem/"/>
<url>/posts/syn-cookie-problem/</url>
<content type="html"><![CDATA[<h2 id="先讲讲Syn-Flood攻击"><a href="#先讲讲Syn-Flood攻击" class="headerlink" title="先讲讲Syn Flood攻击"></a>先讲讲Syn Flood攻击</h2><p>Syn Flood是常见的一种拒绝服务(DOS)攻击方式,所谓的拒绝服务攻击就是通过攻击,使受害主机或者网络不能提供良好的服务,从而达到攻击的目的。</p><p>SYN Flood攻击利用的是IPv4中TCP协议的三次握手(Three-Way Handshake)过程进行的攻击。TCP服务器收到TCP SYN request包时,在发送TCP SYN + ACK包回客户机前,TCP服务器要先分配好一个数据区专门服务于这个即将形成的TCP连接。一般把收到SYN包而还未收到ACK包时的连接状态称为半打开连接(Half-open Connection)。</p><p>在最常见的SYN Flood攻击中,攻击者在短时间内发送大量的TCP SYN包给受害者。受害者(服务器)为每个TCP SYN包分配一个特定的数据区,只要这些SYN包具有不同的源地址(攻击者很容易伪造)。这将给TCP服务器造成很大的系统负担,最终导致系统不能正常工作。</p><h2 id="再说下Syn-Cookie"><a href="#再说下Syn-Cookie" class="headerlink" title="再说下Syn Cookie"></a>再说下Syn Cookie</h2><p>SYN Cookie是对TCP服务器端的三次握手做一些修改,专门用来防范SYN Flood攻击的一种手段。它的原理是,在TCP服务器接收到TCP SYN包并返回TCP SYN + ACK包时,不分配一个专门的数据区,而是根据这个SYN包计算出一个cookie值。这个cookie作为将要返回的SYN ACK包的初始序列号。当客户端返回一个ACK包时,根据包头信息计算cookie,与返回的确认序列号(初始序列号 + 1)进行对比,如果相同,则是一个正常连接,然后,分配资源,建立连接。<br>SYN Cookie机制的核心就是避免攻击造成的大量构造无用的连接请求块,导致内存耗尽,而无法处理正常的连接请求。即使开启该机制并不意味着所有的连接都是用SYN cookies机制来完成连接的建立,只有在半连接队列已满的情况下才会触发SYN cookies机制。</p><h2 id="看一些问题"><a href="#看一些问题" class="headerlink" title="看一些问题"></a>看一些问题</h2><p>我们有用户报障在上传文件的时候失败,客户端收到大量的reset。对应应用在我们的硬件设备NetScaler上,我们抓包如下:</p> <img src="/posts/syn-cookie-problem/cap1.jpg" srcset="/img/loading.gif" class="" title="This is an image"><p>可以看到在三次握手建连后,因为请求包乱序,NetScaler直接回复了reset。当时第一反应是因为timestamp导致的。但是NetScaler上默认timestamp是关闭的。<br>最后和NetScaler tech一起分析了下,发现是因为Syn Cookie引起,我们线上的NetScaler设备默认启用了Syn Cookie。<br>我们再来看下Syn Cookie的问题,SYN cookies机制本身严重违背TCP协议,不允许使用TCP扩展,所以一般来说,可以使用Timestamp区域来存放这些数据,所以当Syn Cookie开启的时候,一般需要开启TCP Timestamp。</p> <img src="/posts/syn-cookie-problem/cloudflare.jpg" srcset="/img/loading.gif" class="" title="This is an image"><p>另外,和本身NetScaler的处理机制也有问题,默认情况下,NetScaler会在收到HTTP的request之后再给这个request分配资源。大部分HTTP request一个包就能搞定,但是这个请求被分成了多个包,加上乱序,所以导致了NetScaler分配资源时认为收到了非法的包,导致错误。</p><p>参考:<br><a href="https://blog.cloudflare.com/syn-packet-handling-in-the-wild/" target="_blank" rel="noopener">https://blog.cloudflare.com/syn-packet-handling-in-the-wild/</a><br><a href="https://blog.csdn.net/justlinux2010/article/details/12619761" target="_blank" rel="noopener">https://blog.csdn.net/justlinux2010/article/details/12619761</a></p>]]></content>
<tags>
<tag>sre</tag>
<tag>tcp</tag>
</tags>
</entry>
<entry>
<title>谈谈证书链的问题</title>
<link href="/posts/cert-chain/"/>
<url>/posts/cert-chain/</url>
<content type="html"><![CDATA[<p>这两天公司遇到影响比较大的故障,是因为服务端证书给的证书链上配置的中间证书错了,导致一部分android用户访问失败。<br>完整的证书内容一般分为3级,服务端证书-中间证书-根证书。其中Root CA是信任锚点,一条证书链中只能有一个。Intermediate CA可以有多个。Root CA通常不直接签发用户证书,而是签发Intermediate CA,由Intermediate CA来签发终用户书。它们之间的关系如下图所示:</p> <img src="/posts/cert-chain/%E8%AF%81%E4%B9%A6%E9%93%BE%E9%AA%8C%E8%AF%81.png" srcset="/img/loading.gif" class="" title="This is an image"><p>系统或浏览器中预置了一些受信任的根证书颁发机构和某些中间证书颁发机构,ssl证书在被验证时最终要验证其根证书是否可信,网站证书的根证书在浏览器可信任根证书列表里才会被信任或者中间证书颁发机构可信其证书也是可信的,否则浏览器则会报告网站的证书来自未知授权中心。可是证书一般是由三级或多级结构构成,浏览器是不能通过用户证书直接验证其根证书的,这时中间证书即证书链文件起了作用,证书链文件告诉了浏览器用户证书的上级证书机构即中间证书,浏览器再通过中间证书验证其上级根证书是否为可信。</p><a id="more"></a><p>在证书里面,我们可以找到<strong>证书颁发信息访问</strong>,通过里面的URL,我们可以获取到整个中间证书。也就是说我们在部署SSL证书的时候没有把中间证书放进去,浏览器依然可以通过证书上面的url信息访问到中间证书,继而建立完整的信用链。</p> <img src="/posts/cert-chain/%E8%AF%81%E4%B9%A6%E9%A2%81%E5%8F%91%E6%9C%BA%E6%9E%84%E4%BF%A1%E6%81%AF%E8%AE%BF%E9%97%AE.jpg" srcset="/img/loading.gif" class="" title="This is an image"><p>大部分浏览器都能这样建立信用链,但是android手机不支持这种方式获取中间证书,所以android访问的时候就会提示证书不受信任。</p> <img src="/posts/cert-chain/ssl-miui.png" srcset="/img/loading.gif" class="" title="This is an image"><p> 所以考虑到兼容性,我们现在的证书都需要把证书链打包进去,最佳实践是需要包含中间证书+站点证书,不需要把根证书放进去,这样可以提高ssl握手的效率。</p><p>再回到这个case,当时出现异常的时候,我第一反应也是缺少了中间证书,通过openssl查看了下证书链</p><pre><code class="hljs plain">openssl s_client -connect xxx.com -servername xxx.com</code></pre><p>发现是有中间证书,当时排查的时候以为是好的。后来分析的时候发现,xxx.com的站点证书的颁发机构应该是DigiCert SHA2 Secure Server CA,而第二张证书竟然是RapidSSL RSA CA 2018,这就会导致无法建立正确的证书链,从而导致访问时提示证书不受信任。</p><pre><code class="hljs plain">Certificate chain 0 s:/C=CN/L=Shanghai/O=Shanghai XXX Commerce Co., Ltd./OU=IT dept./CN=xxx.com i:/C=US/O=DigiCert Inc/CN=DigiCert SHA2 Secure Server CA 1 s:/C=US/O=DigiCert Inc/OU=www.digicert.com/CN=RapidSSL RSA CA 2018 i:/C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert Global Root CA</code></pre><p>参考:<br><a href="https://developer.android.com/training/articles/security-ssl.html?hl=zh-cn#MissingCa" target="_blank" rel="noopener">https://developer.android.com/training/articles/security-ssl.html?hl=zh-cn#MissingCa</a></p>]]></content>
<tags>
<tag>sre</tag>
<tag>https</tag>
</tags>
</entry>
<entry>
<title>系统设计入门</title>
<link href="/posts/system-design/"/>
<url>/posts/system-design/</url>
<content type="html"><![CDATA[<blockquote><ul><li>原文地址:<a href="https://github.com/donnemartin/system-design-primer" target="_blank" rel="noopener">github.com/donnemartin/system-design-primer</a></li></ul></blockquote><h1 id="系统设计入门"><a href="#系统设计入门" class="headerlink" title="系统设计入门"></a>系统设计入门</h1><p align="center"> <img src="http://i.imgur.com/jj3A5N8.png" srcset="/img/loading.gif"> <br/></p><h2 id="目的"><a href="#目的" class="headerlink" title="目的"></a>目的</h2><blockquote><p>学习如何设计大型系统。</p><p>为系统设计的面试做准备。</p></blockquote><h3 id="学习如何设计大型系统"><a href="#学习如何设计大型系统" class="headerlink" title="学习如何设计大型系统"></a>学习如何设计大型系统</h3><p>学习如何设计可扩展的系统将会有助于你成为一个更好的工程师。</p><p>系统设计是一个很宽泛的话题。在互联网上,<strong>关于系统设计原则的资源也是多如牛毛。</strong></p><p>这个仓库就是这些资源的<strong>组织收集</strong>,它可以帮助你学习如何构建可扩展的系统。</p><h3 id="从开源社区学习"><a href="#从开源社区学习" class="headerlink" title="从开源社区学习"></a>从开源社区学习</h3><p>这是一个不断更新的开源项目的初期的版本。</p><p>欢迎<a href="#贡献">贡献</a>!</p><h3 id="为系统设计的面试做准备"><a href="#为系统设计的面试做准备" class="headerlink" title="为系统设计的面试做准备"></a>为系统设计的面试做准备</h3><p>在很多科技公司中,除了代码面试,系统设计也是<strong>技术面试过程</strong>中的一个<strong>必要环节</strong>。</p><p><strong>实践常见的系统设计面试题</strong>并且把你的答案和<strong>例子的解答</strong>进行<strong>对照</strong>:讨论,代码和图表。</p><p>面试准备的其他主题:</p><ul><li><a href="#学习指引">学习指引</a></li><li><a href="#如何处理一个系统设计的面试题">如何处理一个系统设计的面试题</a></li><li><a href="#系统设计的面试题和解答">系统设计的面试题,<strong>含解答</strong></a></li><li><a href="#面向对象设计的面试问题及解答">面向对象设计的面试题,<strong>含解答</strong></a></li><li><a href="#其它的系统设计面试题">其它的系统设计面试题</a></li></ul><h2 id="抽认卡"><a href="#抽认卡" class="headerlink" title="抽认卡"></a>抽认卡</h2><p align="center"> <img src="http://i.imgur.com/zdCAkB3.png" srcset="/img/loading.gif"> <br/></p><p>这里提供的<a href="https://apps.ankiweb.net/" target="_blank" rel="noopener">抽认卡堆</a>使用间隔重复的方法,帮助你记忆关键的系统设计概念。</p><ul><li><a href="resources/flash_cards/System%20Design.apkg">系统设计的卡堆</a></li><li><a href="resources/flash_cards/System%20Design%20Exercises.apkg">系统设计的练习卡堆</a></li><li><a href="resources/flash_cards/OO%20Design.apkg">面向对象设计的练习卡堆</a></li></ul><p>随时随地都可使用。</p><h3 id="代码资源:互动式编程挑战"><a href="#代码资源:互动式编程挑战" class="headerlink" title="代码资源:互动式编程挑战"></a>代码资源:互动式编程挑战</h3><p>你正在寻找资源以准备<a href="https://github.com/donnemartin/interactive-coding-challenges" target="_blank" rel="noopener"><strong>编程面试</strong></a>吗?</p><p align="center"> <img src="http://i.imgur.com/b4YtAEN.png" srcset="/img/loading.gif"> <br/></p><p>请查看我们的姐妹仓库<a href="https://github.com/donnemartin/interactive-coding-challenges" target="_blank" rel="noopener"><strong>互动式编程挑战</strong></a>,其中包含了一个额外的抽认卡堆:</p><ul><li><a href="https://github.com/donnemartin/interactive-coding-challenges/tree/master/anki_cards/Coding.apkg" target="_blank" rel="noopener">代码卡堆</a></li></ul><h2 id="贡献"><a href="#贡献" class="headerlink" title="贡献"></a>贡献</h2><blockquote><p>从社区中学习。</p></blockquote><p>欢迎提交 PR 提供帮助:</p><ul><li>修复错误</li><li>完善章节</li><li>添加章节</li></ul><p>一些还需要完善的内容放在了<a href="#正在完善中">正在完善中</a>。</p><p>请查看<a href="CONTRIBUTING.md">贡献指南</a>。</p><h2 id="系统设计主题的索引"><a href="#系统设计主题的索引" class="headerlink" title="系统设计主题的索引"></a>系统设计主题的索引</h2><blockquote><p>各种系统设计主题的摘要,包括优点和缺点。<strong>每一个主题都面临着取舍和权衡</strong>。</p><p>每个章节都包含着更的资源的链接。</p></blockquote><p align="center"> <img src="http://i.imgur.com/jrUBAF7.png" srcset="/img/loading.gif"> <br/></p><ul><li><a href="#系统设计主题从这里开始">系统设计主题:从这里开始</a><ul><li><a href="#第一步回顾可扩展性scalability的视频讲座">第一步:回顾可扩展性的视频讲座</a></li><li><a href="#第二步回顾可扩展性文章">第二步: 回顾可扩展性的文章</a></li><li><a href="#接下来的步骤">接下来的步骤</a></li></ul></li><li><a href="#性能与可扩展性">性能与拓展性</a></li><li><a href="#延迟与吞吐量">延迟与吞吐量</a></li><li><a href="#可用性与一致性">可用性与一致性</a><ul><li><a href="#cap-理论">CAP 理论</a><ul><li><a href="#cp--一致性和分区容错性">CP - 一致性和分区容错性</a></li><li><a href="#ap--可用性与分区容错性">AP - 可用性和分区容错性</a></li></ul></li></ul></li><li><a href="#一致性模式">一致模式</a><ul><li><a href="#弱一致性">弱一致性</a></li><li><a href="#最终一致性">最终一致性</a></li><li><a href="#强一致性">强一致性</a></li></ul></li><li><a href="#可用性模式">可用模式</a><ul><li><a href="#故障切换">故障切换</a></li><li><a href="#复制">复制</a></li></ul></li><li><a href="#域名系统">域名系统</a></li><li><a href="#内容分发网络cdn">CDN</a><ul><li><a href="#cdn-推送push">CDN 推送</a></li><li><a href="#cdn-拉取pull">CDN 拉取</a></li></ul></li><li><a href="#负载均衡器">负载均衡器</a><ul><li><a href="#工作到备用切换active-passive">工作到备用切换(Active-passive)</a></li><li><a href="#双工作切换active-active">双工作切换(Active-active)</a></li><li><a href="#四层负载均衡">四层负载均衡</a></li><li><a href="#七层负载均衡器">七层负载均衡</a></li><li><a href="#水平扩展">水平扩展</a></li></ul></li><li><a href="#反向代理web-服务器">反向代理(web 服务器)</a><ul><li><a href="#负载均衡器与反向代理">负载均衡与反向代理</a></li></ul></li><li><a href="#应用层">应用层</a><ul><li><a href="#微服务">微服务</a></li><li><a href="#服务发现">服务发现</a></li></ul></li><li><a href="#数据库">数据库</a><ul><li><a href="#关系型数据库管理系统rdbms">关系型数据库管理系统(RDBMS)</a><ul><li><a href="#主从复制">Master-slave 复制集</a></li><li><a href="#主主复制">Master-master 复制集</a></li><li><a href="#联合">联合</a></li><li><a href="#分片">分片</a></li><li><a href="#非规范化">非规范化</a></li><li><a href="#sql-调优">SQL 调优</a></li></ul></li><li><a href="#nosql">NoSQL</a><ul><li><a href="#键-值存储">Key-value 存储</a></li><li><a href="#文档类型存储">文档存储</a></li><li><a href="#列型存储">宽列存储</a></li><li><a href="#图数据库">图数据库</a></li></ul></li><li><a href="#sql-还是-nosql">SQL 还是 NoSQL</a></li></ul></li><li><a href="#缓存">缓存</a><ul><li><a href="#客户端缓存">客户端缓存</a></li><li><a href="#cdn-缓存">CDN 缓存</a></li><li><a href="#web-服务器缓存">Web 服务器缓存</a></li><li><a href="#数据库缓存">数据库缓存</a></li><li><a href="#应用缓存">应用缓存</a></li><li><a href="#数据库查询级别的缓存">数据库查询级别的缓存</a></li><li><a href="#对象级别的缓存">对象级别的缓存</a></li><li><a href="#何时更新缓存">何时更新缓存</a><ul><li><a href="#缓存模式">缓存模式</a></li><li><a href="#直写模式">直写模式</a></li><li><a href="#回写模式">回写模式</a></li><li><a href="#刷新">刷新</a></li></ul></li></ul></li><li><a href="#异步">异步</a><ul><li><a href="#消息队列">消息队列</a></li><li><a href="#任务队列">任务队列</a></li><li><a href="#背压">背压机制</a></li></ul></li><li><a href="#通讯">通讯</a><ul><li><a href="#传输控制协议tcp">传输控制协议(TCP)</a></li><li><a href="#用户数据报协议udp">用户数据报协议(UDP)</a></li><li><a href="#远程过程调用协议rpc">远程控制调用协议(RPC)</a></li><li><a href="#表述性状态转移rest">表述性状态转移(REST)</a></li></ul></li><li><a href="#安全">安全</a></li><li><a href="#附录">附录</a><ul><li><a href="#2-的次方表">2 的次方表</a></li><li><a href="#每个程序员都应该知道的延迟数">每个程序员都应该知道的延迟数</a></li><li><a href="#其它的系统设计面试题">其它的系统设计面试题</a></li><li><a href="#真实架构">真实架构</a></li><li><a href="#公司的系统架构">公司的系统架构</a></li><li><a href="#公司工程博客">公司工程博客</a></li></ul></li><li><a href="#正在完善中">正在完善中</a></li><li><a href="#致谢">致谢</a></li><li><a href="#联系方式">联系方式</a></li><li><a href="#许可">许可</a></li></ul><a id="more"></a><h2 id="学习指引"><a href="#学习指引" class="headerlink" title="学习指引"></a>学习指引</h2><blockquote><p>基于你面试的时间线(短、中、长)去复习那些推荐的主题。</p></blockquote><p><img src="http://i.imgur.com/OfVllex.png" srcset="/img/loading.gif" alt="Imgur"></p><p><strong>问:对于面试来说,我需要知道这里的所有知识点吗?</strong></p><p><strong>答:不,如果只是为了准备面试的话,你并不需要知道所有的知识点。</strong></p><p>在一场面试中你会被问到什么取决于下面这些因素:</p><ul><li>你的经验</li><li>你的技术背景</li><li>你面试的职位</li><li>你面试的公司</li><li>运气</li></ul><p>那些有经验的候选人通常会被期望了解更多的系统设计的知识。架构师或者团队负责人则会被期望了解更多除了个人贡献之外的知识。顶级的科技公司通常也会有一次或者更多的系统设计面试。</p><p>面试会很宽泛的展开并在几个领域深入。这回帮助你了解一些关于系统设计的不同的主题。基于你的时间线,经验,面试的职位和面试的公司对下面的指导做出适当的调整。</p><ul><li><strong>短期</strong> - 以系统设计主题的<strong>广度</strong>为目标。通过解决<strong>一些</strong>面试题来练习。</li><li><strong>中期</strong> - 以系统设计主题的<strong>广度</strong>和<strong>初级深度</strong>为目标。通过解决<strong>很多</strong>面试题来练习。</li><li><strong>长期</strong> - 以系统设计主题的<strong>广度</strong>和<strong>高级深度</strong>为目标。通过解决<strong>大部分</strong>面试题来联系。</li></ul><table><thead><tr><th></th><th>短期</th><th>中期</th><th>长期</th></tr></thead><tbody><tr><td>阅读 <a href="#系统设计主题的索引">系统设计主题</a> 以获得一个关于系统如何工作的宽泛的认识</td><td>:+1:</td><td>:+1:</td><td>:+1:</td></tr><tr><td>阅读一些你要面试的<a href="#公司工程博客">公司工程博客</a>的文章</td><td>:+1:</td><td>:+1:</td><td>:+1:</td></tr><tr><td>阅读 <a href="#真实架构">真实架构</a></td><td>:+1:</td><td>:+1:</td><td>:+1:</td></tr><tr><td>复习 <a href="#如何处理一个系统设计面试题">如何处理一个系统设计面试题</a></td><td>:+1:</td><td>:+1:</td><td>:+1:</td></tr><tr><td>完成 <a href="#系统设计的面试题和解答">系统设计的面试题和解答</a></td><td>一些</td><td>很多</td><td>大部分</td></tr><tr><td>完成 <a href="#面向对象设计的面试问题及解答">面向对象设计的面试题和解答</a></td><td>一些</td><td>很多</td><td>大部分</td></tr><tr><td>复习 <a href="#其它的系统设计面试题">其它的系统设计面试题</a></td><td>一些</td><td>很多</td><td>大部分</td></tr></tbody></table><h2 id="如何处理一个系统设计的面试题"><a href="#如何处理一个系统设计的面试题" class="headerlink" title="如何处理一个系统设计的面试题"></a>如何处理一个系统设计的面试题</h2><p>系统设计面试是一个<strong>开放式的对话</strong>。他们期望你去主导这个对话。</p><p>你可以使用下面的步骤来指引讨论。为了巩固这个过程,请使用下面的步骤完成<a href="#系统设计的面试题和解答">系统设计的面试题和解答</a>这个章节。</p><h3 id="第一步:描述使用场景,约束和假设"><a href="#第一步:描述使用场景,约束和假设" class="headerlink" title="第一步:描述使用场景,约束和假设"></a>第一步:描述使用场景,约束和假设</h3><p>把所有需要的东西聚集在一起,审视问题。不停的提问,以至于我们可以明确使用场景和约束。讨论假设。</p><ul><li>谁会使用它?</li><li>他们会怎样使用它?</li><li>有多少用户?</li><li>系统的作用是什么?</li><li>系统的输入输出分别是什么?</li><li>我们希望处理多少数据?</li><li>我们希望每秒钟处理多少请求?</li><li>我们希望的读写比率?</li></ul><h3 id="第二步:创造一个高级的设计"><a href="#第二步:创造一个高级的设计" class="headerlink" title="第二步:创造一个高级的设计"></a>第二步:创造一个高级的设计</h3><p>使用所有重要的组件来描绘出一个高级的设计。</p><ul><li>画出主要的组件和连接</li><li>证明你的想法</li></ul><h3 id="第三步:设计核心组件"><a href="#第三步:设计核心组件" class="headerlink" title="第三步:设计核心组件"></a>第三步:设计核心组件</h3><p>对每一个核心组件进行详细深入的分析。举例来说,如果你被问到<a href="solutions/system_design/pastebin/README.md">设计一个 url 缩写服务</a>,开始讨论:</p><ul><li>生成并储存一个完整 url 的 hash<ul><li><a href="solutions/system_design/pastebin/README.md">MD5</a> 和 <a href="solutions/system_design/pastebin/README.md">Base62</a></li><li>Hash 碰撞</li><li>SQL 还是 NoSQL</li><li>数据库模型</li></ul></li><li>将一个 hashed url 翻译成完整的 url<ul><li>数据库查找</li></ul></li><li>API 和面向对象设计</li></ul><h3 id="第四步:度量设计"><a href="#第四步:度量设计" class="headerlink" title="第四步:度量设计"></a>第四步:度量设计</h3><p>确认和处理瓶颈以及一些限制。举例来说就是你需要下面的这些来完成拓展性的议题吗?</p><ul><li>负载均衡</li><li>水平拓展</li><li>缓存</li><li>数据库分片</li></ul><p>论述可能的解决办法和代价。每件事情需要取舍。可以使用<a href="#系统设计主题的索引">可拓展系统的设计原则</a>来处理瓶颈。</p><h3 id="信封背面的计算"><a href="#信封背面的计算" class="headerlink" title="信封背面的计算"></a>信封背面的计算</h3><p>你或许会被要求通过手算进行一些估算。涉及到的<a href="#附录">附录</a>涉及到的是下面的这些资源:</p><ul><li><a href="http://highscalability.com/blog/2011/1/26/google-pro-tip-use-back-of-the-envelope-calculations-to-choo.html" target="_blank" rel="noopener">使用信封的背面做计算</a></li><li><a href="#2-的次方表">2 的次方表</a></li><li><a href="#每个程序员都应该知道的延迟数">每个程序员都应该知道的延迟数</a></li></ul><h3 id="相关资源和延伸阅读"><a href="#相关资源和延伸阅读" class="headerlink" title="相关资源和延伸阅读"></a>相关资源和延伸阅读</h3><p>查看下面的链接以获得我们期望的更好的想法:</p><ul><li><a href="https://www.palantir.com/2011/10/how-to-rock-a-systems-design-interview/" target="_blank" rel="noopener">怎样通过一个系统设计的面试</a></li><li><a href="http://www.hiredintech.com/system-design" target="_blank" rel="noopener">系统设计的面试</a></li><li><a href="https://www.youtube.com/watch?v=ZgdS0EUmn70" target="_blank" rel="noopener">系统架构与设计的面试简介</a></li></ul><h2 id="系统设计的面试题和解答"><a href="#系统设计的面试题和解答" class="headerlink" title="系统设计的面试题和解答"></a>系统设计的面试题和解答</h2><blockquote><p>普通的系统设计面试题和相关事例的论述,代码和图表。</p></blockquote><blockquote><p>与内容有关的解答在 <code>solutions/</code> 文件夹中。</p></blockquote><table><thead><tr><th>问题</th><th></th></tr></thead><tbody><tr><td>设计 Pastebin.com (或者 Bit.ly)</td><td><a href="solutions/system_design/pastebin/README.md">解答</a></td></tr><tr><td>设计 Twitter 时间线和搜索 (或者 Facebook feed 和搜索)</td><td><a href="solutions/system_design/twitter/README.md">解答</a></td></tr><tr><td>设计一个网页爬虫</td><td><a href="solutions/system_design/web_crawler/README.md">解答</a></td></tr><tr><td>设计 Mint.com</td><td><a href="solutions/system_design/mint/README.md">解答</a></td></tr><tr><td>为一个社交网络设计数据结构</td><td><a href="solutions/system_design/social_graph/README.md">解答</a></td></tr><tr><td>为搜索引擎设计一个 key-value 储存</td><td><a href="solutions/system_design/query_cache/README.md">解答</a></td></tr><tr><td>通过分类特性设计 Amazon 的销售排名</td><td><a href="solutions/system_design/sales_rank/README.md">解答</a></td></tr><tr><td>在 AWS 上设计一个百万用户级别的系统</td><td><a href="solutions/system_design/scaling_aws/README.md">解答</a></td></tr><tr><td>添加一个系统设计问题</td><td><a href="#贡献">贡献</a></td></tr></tbody></table><h3 id="设计-Pastebin-com-或者-Bit-ly"><a href="#设计-Pastebin-com-或者-Bit-ly" class="headerlink" title="设计 Pastebin.com (或者 Bit.ly)"></a>设计 Pastebin.com (或者 Bit.ly)</h3><p><a href="solutions/system_design/pastebin/README.md">查看实践与解答</a></p><p><img src="http://i.imgur.com/4edXG0T.png" srcset="/img/loading.gif" alt="Imgur"></p><h3 id="设计-Twitter-时间线和搜索-或者-Facebook-feed-和搜索"><a href="#设计-Twitter-时间线和搜索-或者-Facebook-feed-和搜索" class="headerlink" title="设计 Twitter 时间线和搜索 (或者 Facebook feed 和搜索)"></a>设计 Twitter 时间线和搜索 (或者 Facebook feed 和搜索)</h3><p><a href="solutions/system_design/twitter/README.md">查看实践与解答</a></p><p><img src="http://i.imgur.com/jrUBAF7.png" srcset="/img/loading.gif" alt="Imgur"></p><h3 id="设计一个网页爬虫"><a href="#设计一个网页爬虫" class="headerlink" title="设计一个网页爬虫"></a>设计一个网页爬虫</h3><p><a href="solutions/system_design/web_crawler/README.md">查看实践与解答</a></p><p><img src="http://i.imgur.com/bWxPtQA.png" srcset="/img/loading.gif" alt="Imgur"></p><h3 id="设计-Mint-com"><a href="#设计-Mint-com" class="headerlink" title="设计 Mint.com"></a>设计 Mint.com</h3><p><a href="solutions/system_design/mint/README.md">查看实践与解答</a></p><p><img src="http://i.imgur.com/V5q57vU.png" srcset="/img/loading.gif" alt="Imgur"></p><h3 id="为一个社交网络设计数据结构"><a href="#为一个社交网络设计数据结构" class="headerlink" title="为一个社交网络设计数据结构"></a>为一个社交网络设计数据结构</h3><p><a href="solutions/system_design/social_graph/README.md">查看实践与解答</a></p><p><img src="http://i.imgur.com/cdCv5g7.png" srcset="/img/loading.gif" alt="Imgur"></p><h3 id="为搜索引擎设计一个-key-value-储存"><a href="#为搜索引擎设计一个-key-value-储存" class="headerlink" title="为搜索引擎设计一个 key-value 储存"></a>为搜索引擎设计一个 key-value 储存</h3><p><a href="solutions/system_design/query_cache/README.md">查看实践与解答</a></p><p><img src="http://i.imgur.com/4j99mhe.png" srcset="/img/loading.gif" alt="Imgur"></p><h3 id="设计按类别分类的-Amazon-销售排名"><a href="#设计按类别分类的-Amazon-销售排名" class="headerlink" title="设计按类别分类的 Amazon 销售排名"></a>设计按类别分类的 Amazon 销售排名</h3><p><a href="solutions/system_design/sales_rank/README.md">查看实践与解答</a></p><p><img src="http://i.imgur.com/MzExP06.png" srcset="/img/loading.gif" alt="Imgur"></p><h3 id="在-AWS-上设计一个百万用户级别的系统"><a href="#在-AWS-上设计一个百万用户级别的系统" class="headerlink" title="在 AWS 上设计一个百万用户级别的系统"></a>在 AWS 上设计一个百万用户级别的系统</h3><p><a href="solutions/system_design/scaling_aws/README.md">查看实践与解答</a></p><p><img src="http://i.imgur.com/jj3A5N8.png" srcset="/img/loading.gif" alt="Imgur"></p><h2 id="面向对象设计的面试问题及解答"><a href="#面向对象设计的面试问题及解答" class="headerlink" title="面向对象设计的面试问题及解答"></a>面向对象设计的面试问题及解答</h2><blockquote><p>常见面向对象设计面试问题及实例讨论,代码和图表演示。</p><p>与内容相关的解决方案在 <code>solutions/</code> 文件夹中。</p></blockquote><blockquote><p><strong>注:此节还在完善中</strong></p></blockquote><table><thead><tr><th>问题</th><th></th></tr></thead><tbody><tr><td>设计 hash map</td><td><a href="solutions/object_oriented_design/hash_table/hash_map.ipynb">解决方案</a></td></tr><tr><td>设计 LRU 缓存</td><td><a href="solutions/object_oriented_design/lru_cache/lru_cache.ipynb">解决方案</a></td></tr><tr><td>设计一个呼叫中心</td><td><a href="solutions/object_oriented_design/call_center/call_center.ipynb">解决方案</a></td></tr><tr><td>设计一副牌</td><td><a href="solutions/object_oriented_design/deck_of_cards/deck_of_cards.ipynb">解决方案</a></td></tr><tr><td>设计一个停车场</td><td><a href="solutions/object_oriented_design/parking_lot/parking_lot.ipynb">解决方案</a></td></tr><tr><td>设计一个聊天服务</td><td><a href="solutions/object_oriented_design/online_chat/online_chat.ipynb">解决方案</a></td></tr><tr><td>设计一个环形数组</td><td><a href="#贡献">待解决</a></td></tr><tr><td>添加一个面向对象设计问题</td><td><a href="#贡献">待解决</a></td></tr></tbody></table><h2 id="系统设计主题:从这里开始"><a href="#系统设计主题:从这里开始" class="headerlink" title="系统设计主题:从这里开始"></a>系统设计主题:从这里开始</h2><p>不熟悉系统设计?</p><p>首先,你需要对一般性原则有一个基本的认识,知道它们是什么,怎样使用以及利弊。</p><h3 id="第一步:回顾可扩展性(scalability)的视频讲座"><a href="#第一步:回顾可扩展性(scalability)的视频讲座" class="headerlink" title="第一步:回顾可扩展性(scalability)的视频讲座"></a>第一步:回顾可扩展性(scalability)的视频讲座</h3><p><a href="https://www.youtube.com/watch?v=-W9F__D3oY4" target="_blank" rel="noopener">哈佛大学可扩展性讲座</a></p><ul><li>主题涵盖<ul><li>垂直扩展(Vertical scaling)</li><li>水平扩展(Horizontal scaling)</li><li>缓存</li><li>负载均衡</li><li>数据库复制</li><li>数据库分区</li></ul></li></ul><h3 id="第二步:回顾可扩展性文章"><a href="#第二步:回顾可扩展性文章" class="headerlink" title="第二步:回顾可扩展性文章"></a>第二步:回顾可扩展性文章</h3><p><a href="http://www.lecloud.net/tagged/scalability" target="_blank" rel="noopener">可扩展性</a></p><ul><li>主题涵盖:<ul><li><a href="http://www.lecloud.net/post/7295452622/scalability-for-dummies-part-1-clones" target="_blank" rel="noopener">Clones</a></li><li><a href="http://www.lecloud.net/post/7994751381/scalability-for-dummies-part-2-database" target="_blank" rel="noopener">数据库</a></li><li><a href="http://www.lecloud.net/post/9246290032/scalability-for-dummies-part-3-cache" target="_blank" rel="noopener">缓存</a></li><li><a href="http://www.lecloud.net/post/9699762917/scalability-for-dummies-part-4-asynchronism" target="_blank" rel="noopener">异步</a></li></ul></li></ul><h3 id="接下来的步骤"><a href="#接下来的步骤" class="headerlink" title="接下来的步骤"></a>接下来的步骤</h3><p>接下来,我们将看看高阶的权衡和取舍:</p><ul><li><strong>性能</strong>与<strong>可扩展性</strong></li><li><strong>延迟</strong>与<strong>吞吐量</strong></li><li><strong>可用性</strong>与<strong>一致性</strong></li></ul><p>记住<strong>每个方面都面临取舍和权衡</strong>。</p><p>然后,我们将深入更具体的主题,如 DNS、CDN 和负载均衡器。</p><h2 id="性能与可扩展性"><a href="#性能与可扩展性" class="headerlink" title="性能与可扩展性"></a>性能与可扩展性</h2><p>如果服务<strong>性能</strong>的增长与资源的增加是成比例的,服务就是可扩展的。通常,提高性能意味着服务于更多的工作单元,另一方面,当数据集增长时,同样也可以处理更大的工作单位。<sup><a href="http://www.allthingsdistributed.com/2006/03/a_word_on_scalability.html" target="_blank" rel="noopener">1</a></sup></p><p>另一个角度来看待性能与可扩展性:</p><ul><li>如果你的系统有<strong>性能</strong>问题,对于单个用户来说是缓慢的。</li><li>如果你的系统有<strong>可扩展性</strong>问题,单个用户较快但在高负载下会变慢。</li></ul><h3 id="来源及延伸阅读"><a href="#来源及延伸阅读" class="headerlink" title="来源及延伸阅读"></a>来源及延伸阅读</h3><ul><li><a href="http://www.allthingsdistributed.com/2006/03/a_word_on_scalability.html" target="_blank" rel="noopener">简单谈谈可扩展性</a></li><li><a href="http://www.slideshare.net/jboner/scalability-availability-stability-patterns/" target="_blank" rel="noopener">可扩展性,可用性,稳定性和模式</a></li></ul><h2 id="延迟与吞吐量"><a href="#延迟与吞吐量" class="headerlink" title="延迟与吞吐量"></a>延迟与吞吐量</h2><p><strong>延迟</strong>是执行操作或运算结果所花费的时间。</p><p><strong>吞吐量</strong>是单位时间内(执行)此类操作或运算的数量。</p><p>通常,你应该以<strong>可接受级延迟</strong>下<strong>最大化吞吐量</strong>为目标。</p><h3 id="来源及延伸阅读-1"><a href="#来源及延伸阅读-1" class="headerlink" title="来源及延伸阅读"></a>来源及延伸阅读</h3><ul><li><a href="https://community.cadence.com/cadence_blogs_8/b/sd/archive/2010/09/13/understanding-latency-vs-throughput" target="_blank" rel="noopener">理解延迟与吞吐量</a></li></ul><h2 id="可用性与一致性"><a href="#可用性与一致性" class="headerlink" title="可用性与一致性"></a>可用性与一致性</h2><h3 id="CAP-理论"><a href="#CAP-理论" class="headerlink" title="CAP 理论"></a>CAP 理论</h3><p align="center"> <img src="http://i.imgur.com/bgLMI2u.png" srcset="/img/loading.gif"> <br/> <strong><a href="http://robertgreiner.com/2014/08/cap-theorem-revisited" target="_blank" rel="noopener">来源:再看 CAP 理论</a></strong></p><p>在一个分布式计算系统中,只能同时满足下列的两点:</p><ul><li><strong>一致性</strong> ─ 每次访问都能获得最新数据但可能会收到错误响应</li><li><strong>可用性</strong> ─ 每次访问都能收到非错响应,但不保证获取到最新数据</li><li><strong>分区容错性</strong> ─ 在任意分区网络故障的情况下系统仍能继续运行</li></ul><p><strong>网络并不可靠,所以你应要支持分区容错性,并需要在软件可用性和一致性间做出取舍。</strong></p><h4 id="CP-─-一致性和分区容错性"><a href="#CP-─-一致性和分区容错性" class="headerlink" title="CP ─ 一致性和分区容错性"></a>CP ─ 一致性和分区容错性</h4><p>等待分区节点的响应可能会导致延时错误。如果你的业务需求需要原子读写,CP 是一个不错的选择。</p><h4 id="AP-─-可用性与分区容错性"><a href="#AP-─-可用性与分区容错性" class="headerlink" title="AP ─ 可用性与分区容错性"></a>AP ─ 可用性与分区容错性</h4><p>响应节点上可用数据的最近版本可能并不是最新的。当分区解析完后,写入(操作)可能需要一些时间来传播。</p><p>如果业务需求允许<a href="#最终一致性">最终一致性</a>,或当有外部故障时要求系统继续运行,AP 是一个不错的选择。</p><h3 id="来源及延伸阅读-2"><a href="#来源及延伸阅读-2" class="headerlink" title="来源及延伸阅读"></a>来源及延伸阅读</h3><ul><li><a href="http://robertgreiner.com/2014/08/cap-theorem-revisited/" target="_blank" rel="noopener">再看 CAP 理论</a></li><li><a href="http://ksat.me/a-plain-english-introduction-to-cap-theorem/" target="_blank" rel="noopener">通俗易懂地介绍 CAP 理论</a></li><li><a href="https://github.com/henryr/cap-faq" target="_blank" rel="noopener">CAP FAQ</a></li></ul><h2 id="一致性模式"><a href="#一致性模式" class="headerlink" title="一致性模式"></a>一致性模式</h2><p>有同一份数据的多份副本,我们面临着怎样同步它们的选择,以便让客户端有一致的显示数据。回想 <a href="#cap-理论">CAP 理论</a>中的一致性定义 ─ 每次访问都能获得最新数据但可能会收到错误响应</p><h3 id="弱一致性"><a href="#弱一致性" class="headerlink" title="弱一致性"></a>弱一致性</h3><p>在写入之后,访问可能看到,也可能看不到(写入数据)。尽力优化之让其能访问最新数据。</p><p>这种方式可以 memcached 等系统中看到。弱一致性在 VoIP,视频聊天和实时多人游戏等真实用例中表现不错。打个比方,如果你在通话中丢失信号几秒钟时间,当重新连接时你是听不到这几秒钟所说的话的。</p><h3 id="最终一致性"><a href="#最终一致性" class="headerlink" title="最终一致性"></a>最终一致性</h3><p>在写入后,访问最终能看到写入数据(通常在数毫秒内)。数据被异步复制。</p><p>DNS 和 email 等系统使用的是此种方式。最终一致性在高可用性系统中效果不错。</p><h3 id="强一致性"><a href="#强一致性" class="headerlink" title="强一致性"></a>强一致性</h3><p>在写入后,访问立即可见。数据被同步复制。</p><p>文件系统和关系型数据库(RDBMS)中使用的是此种方式。强一致性在需要记录的系统中运作良好。</p><h3 id="来源及延伸阅读-3"><a href="#来源及延伸阅读-3" class="headerlink" title="来源及延伸阅读"></a>来源及延伸阅读</h3><ul><li><a href="http://snarfed.org/transactions_across_datacenters_io.html" target="_blank" rel="noopener">Transactions across data centers</a></li></ul><h2 id="可用性模式"><a href="#可用性模式" class="headerlink" title="可用性模式"></a>可用性模式</h2><p>有两种支持高可用性的模式: <strong>故障切换(fail-over)</strong>和<strong>复制(replication)</strong>。</p><h3 id="故障切换"><a href="#故障切换" class="headerlink" title="故障切换"></a>故障切换</h3><h4 id="工作到备用切换(Active-passive)"><a href="#工作到备用切换(Active-passive)" class="headerlink" title="工作到备用切换(Active-passive)"></a>工作到备用切换(Active-passive)</h4><p>关于工作到备用的故障切换流程是,工作服务器发送周期信号给待机中的备用服务器。如果周期信号中断,备用服务器切换成工作服务器的 IP 地址并恢复服务。</p><p>宕机时间取决于备用服务器处于“热”待机状态还是需要从“冷”待机状态进行启动。只有工作服务器处理流量。</p><p>工作到备用的故障切换也被称为主从切换。</p><h4 id="双工作切换(Active-active)"><a href="#双工作切换(Active-active)" class="headerlink" title="双工作切换(Active-active)"></a>双工作切换(Active-active)</h4><p>在双工作切换中,双方都在管控流量,在它们之间分散负载。</p><p>如果是外网服务器,DNS 将需要对两方都了解。如果是内网服务器,应用程序逻辑将需要对两方都了解。</p><p>双工作切换也可以称为主主切换。</p><h3 id="缺陷:故障切换"><a href="#缺陷:故障切换" class="headerlink" title="缺陷:故障切换"></a>缺陷:故障切换</h3><ul><li>故障切换需要添加额外硬件并增加复杂性。</li><li>如果新写入数据在能被复制到备用系统之前,工作系统出现了故障,则有可能会丢失数据。</li></ul><h3 id="复制"><a href="#复制" class="headerlink" title="复制"></a>复制</h3><h4 id="主─从复制和主─主复制"><a href="#主─从复制和主─主复制" class="headerlink" title="主─从复制和主─主复制"></a>主─从复制和主─主复制</h4><p>这个主题进一步探讨了<a href="#数据库">数据库</a>部分:</p><ul><li><a href="#主从复制">主─从复制</a></li><li><a href="#主主复制">主─主复制</a></li></ul><h2 id="域名系统"><a href="#域名系统" class="headerlink" title="域名系统"></a>域名系统</h2><p align="center"> <img src="http://i.imgur.com/IOyLj4i.jpg" srcset="/img/loading.gif"> <br/> <strong><a href="http://www.slideshare.net/srikrupa5/dns-security-presentation-issa" target="_blank" rel="noopener">来源:DNS 安全介绍</a></strong></p><p>域名系统是把 <a href="http://www.example.com" target="_blank" rel="noopener">www.example.com</a> 等域名转换成 IP 地址。</p><p>域名系统是分层次的,一些 DNS 服务器位于顶层。当查询(域名) IP 时,路由或 ISP 提供连接 DNS 服务器的信息。较底层的 DNS 服务器缓存映射,它可能会因为 DNS 传播延时而失效。DNS 结果可以缓存在浏览器或操作系统中一段时间,时间长短取决于<a href="https://en.wikipedia.org/wiki/Time_to_live" target="_blank" rel="noopener">存活时间 TTL</a>。</p><ul><li><strong>NS 记录(域名服务)</strong> ─ 指定解析域名或子域名的 DNS 服务器。</li><li><strong>MX 记录(邮件交换)</strong> ─ 指定接收信息的邮件服务器。</li><li><strong>A 记录(地址)</strong> ─ 指定域名对应的 IP 地址记录。</li><li><strong>CNAME(规范)</strong> ─ 一个域名映射到另一个域名或 <code>CNAME</code> 记录( example.com 指向 <a href="http://www.example.com" target="_blank" rel="noopener">www.example.com</a> )或映射到一个 <code>A</code> 记录。</li></ul><p><a href="https://www.cloudflare.com/dns/" target="_blank" rel="noopener">CloudFlare</a> 和 <a href="https://aws.amazon.com/route53/" target="_blank" rel="noopener">Route 53</a> 等平台提供管理 DNS 的功能。某些 DNS 服务通过集中方式来路由流量:</p><ul><li><a href="http://g33kinfo.com/info/archives/2657" target="_blank" rel="noopener">加权轮询调度</a><ul><li>防止流量进入维护中的服务器</li><li>在不同大小集群间负载均衡</li><li>A/B 测试</li></ul></li><li>基于延迟路由</li><li>基于地理位置路由</li></ul><h3 id="缺陷-DNS"><a href="#缺陷-DNS" class="headerlink" title="缺陷:DNS"></a>缺陷:DNS</h3><ul><li>虽说缓存可以减轻 DNS 延迟,但连接 DNS 服务器还是带来了轻微的延迟。</li><li>虽然它们通常由<a href="http://superuser.com/questions/472695/who-controls-the-dns-servers/472729" target="_blank" rel="noopener">政府,网络服务提供商和大公司</a>管理,但 DNS 服务管理仍可能是复杂的。</li><li>DNS 服务最近遭受 <a href="http://dyn.com/blog/dyn-analysis-summary-of-friday-october-21-attack/" target="_blank" rel="noopener">DDoS 攻击</a>,阻止不知道 Twtter IP 地址的用户访问 Twiiter。</li></ul><h3 id="来源及延伸阅读-4"><a href="#来源及延伸阅读-4" class="headerlink" title="来源及延伸阅读"></a>来源及延伸阅读</h3><ul><li><a href="https://technet.microsoft.com/en-us/library/dd197427(v=ws.10).aspx" target="_blank" rel="noopener">DNS 架构</a></li><li><a href="https://en.wikipedia.org/wiki/Domain_Name_System" target="_blank" rel="noopener">Wikipedia</a></li><li><a href="https://support.dnsimple.com/categories/dns/" target="_blank" rel="noopener">关于 DNS 的文章</a></li></ul><h2 id="内容分发网络(CDN)"><a href="#内容分发网络(CDN)" class="headerlink" title="内容分发网络(CDN)"></a>内容分发网络(CDN)</h2><p align="center"> <img src="http://i.imgur.com/h9TAuGI.jpg" srcset="/img/loading.gif"> <br/> <strong><a href="https://www.creative-artworks.eu/why-use-a-content-delivery-network-cdn/" target="_blank" rel="noopener">来源:为什么使用 CDN</a></strong></p><p>内容分发网络(CDN)是一个全球性的代理服务器分布式网络,它从靠近用户的位置提供内容。通常,HTML/CSS/JS,图片和视频等静态内容由 CDN 提供,虽然亚马逊 CloudFront 等也支持动态内容。CDN 的 DNS 解析会告知客户端连接哪台服务器。</p><p>将内容存储在 CDN 上可以从两个方面来提供性能:</p><ul><li>从靠近用户的数据中心提供资源</li><li>通过 CDN 你的服务器不必真的处理请求</li></ul><h3 id="CDN-推送(push)"><a href="#CDN-推送(push)" class="headerlink" title="CDN 推送(push)"></a>CDN 推送(push)</h3><p>当你服务器上内容发生变动时,推送 CDN 接受新内容。直接推送给 CDN 并重写 URL 地址以指向你的内容的 CDN 地址。你可以配置内容到期时间及何时更新。内容只有在更改或新增是才推送,流量最小化,但储存最大化。</p><h3 id="CDN-拉取(pull)"><a href="#CDN-拉取(pull)" class="headerlink" title="CDN 拉取(pull)"></a>CDN 拉取(pull)</h3><p>CDN 拉取是当第一个用户请求该资源时,从服务器上拉取资源。你将内容留在自己的服务器上并重写 URL 指向 CDN 地址。直到内容被缓存在 CDN 上为止,这样请求只会更慢,</p><p><a href="https://en.wikipedia.org/wiki/Time_to_live" target="_blank" rel="noopener">存活时间(TTL)</a>决定缓存多久时间。CDN 拉取方式最小化 CDN 上的储存空间,但如果过期文件并在实际更改之前被拉取,则会导致冗余的流量。</p><p>高流量站点使用 CDN 拉取效果不错,因为只有最近请求的内容保存在 CDN 中,流量才能更平衡地分散。</p><h3 id="缺陷:CDN"><a href="#缺陷:CDN" class="headerlink" title="缺陷:CDN"></a>缺陷:CDN</h3><ul><li>CDN 成本可能因流量而异,可能在权衡之后你将不会使用 CDN。</li><li>如果在 TTL 过期之前更新内容,CDN 缓存内容可能会过时。</li><li>CDN 需要更改静态内容的 URL 地址以指向 CDN。</li></ul><h3 id="来源及延伸阅读-5"><a href="#来源及延伸阅读-5" class="headerlink" title="来源及延伸阅读"></a>来源及延伸阅读</h3><ul><li><a href="http://repository.cmu.edu/cgi/viewcontent.cgi?article=2112&context=compsci" target="_blank" rel="noopener">全球性内容分发网络</a></li><li><a href="http://www.travelblogadvice.com/technical/the-differences-between-push-and-pull-cdns/" target="_blank" rel="noopener">CDN 拉取和 CDN 推送的区别</a></li><li><a href="https://en.wikipedia.org/wiki/Content_delivery_network" target="_blank" rel="noopener">Wikipedia</a></li></ul><h2 id="负载均衡器"><a href="#负载均衡器" class="headerlink" title="负载均衡器"></a>负载均衡器</h2><p align="center"> <img src="http://i.imgur.com/h81n9iK.png" srcset="/img/loading.gif"> <br/> <strong><a href="http://horicky.blogspot.com/2010/10/scalable-system-design-patterns.html" target="_blank" rel="noopener">来源:可扩展的系统设计模式</a></strong></p><p>负载均衡器将传入的请求分发到应用服务器和数据库等计算资源。无论哪种情况,负载均衡器将从计算资源来的响应返回给恰当的客户端。负载均衡器的效用在于:</p><ul><li>防止请求进入不好的服务器</li><li>防止资源过载</li><li>帮助消除单一的故障点</li></ul><p>负载均衡器可以通过硬件(昂贵)或 HAProxy 等软件来实现。<br>增加的好处包括:</p><ul><li><strong>SSL 终结</strong> ─ 解密传入的请求并加密服务器响应,这样的话后端服务器就不必再执行这些潜在高消耗运算了。<ul><li>不需要再每台服务器上安装 <a href="https://en.wikipedia.org/wiki/X.509" target="_blank" rel="noopener">X.509 证书</a>。</li></ul></li><li><strong>Session 留存</strong> ─ 如果 Web 应用程序不追踪会话,发出 cookie 并将特定客户端的请求路由到同一实例。</li></ul><p>通常会设置采用<a href="#工作到备用切换active-passive">工作─备用</a> 或 <a href="#双工作切换active-active">双工作</a> 模式的多个负载均衡器,以免发生故障。</p><p>负载均衡器能基于多种方式来路由流量:</p><ul><li>随机</li><li>最少负载</li><li>Session/cookie</li><li><a href="http://g33kinfo.com/info/archives/2657" target="_blank" rel="noopener">轮询调度或加权轮询调度算法</a></li><li><a href="#四层负载均衡">四层负载均衡</a></li><li><a href="#七层负载均衡">七层负载均衡</a></li></ul><h3 id="四层负载均衡"><a href="#四层负载均衡" class="headerlink" title="四层负载均衡"></a>四层负载均衡</h3><p>四层负载均衡根据监看<a href="#通讯">传输层</a>的信息来决定如何分发请求。通常,这会涉及来源,目标 IP 地址和请求头中的端口,但不包括数据包(报文)内容。四层负载均衡执行<a href="https://www.nginx.com/resources/glossary/layer-4-load-balancing/" target="_blank" rel="noopener">网络地址转换(NAT)</a>来向上游服务器转发网络数据包。</p><h3 id="七层负载均衡器"><a href="#七层负载均衡器" class="headerlink" title="七层负载均衡器"></a>七层负载均衡器</h3><p>七层负载均衡器根据监控<a href="#通讯">应用层</a>来决定怎样分发请求。这会涉及请求头的内容,消息和 cookie。七层负载均衡器终结网络流量,读取消息,做出负载均衡判定,然后传送给特定服务器。比如,一个七层负载均衡器能直接将视频流量连接到托管视频的服务器,同时将更敏感的用户账单流量引导到安全性更强的服务器。</p><p>以损失灵活性为代价,四层负载均衡比七层负载均衡花费更少时间和计算资源,虽然这对现代商用硬件的性能影响甚微。</p><h3 id="水平扩展"><a href="#水平扩展" class="headerlink" title="水平扩展"></a>水平扩展</h3><p>负载均衡器还能帮助水平扩展,提高性能和可用性。使用商业硬件的性价比更高,并且比在单台硬件上<strong>垂直扩展</strong>更贵的硬件具有更高的可用性。相比招聘特定企业系统人才,招聘商业硬件方面的人才更加容易。</p><h4 id="缺陷:水平扩展"><a href="#缺陷:水平扩展" class="headerlink" title="缺陷:水平扩展"></a>缺陷:水平扩展</h4><ul><li>水平扩展引入了复杂度并涉及服务器复制<ul><li>服务器应该是无状态的:它们也不该包含像 session 或资料图片等与用户关联的数据。</li><li>session 可以集中存储在数据库或持久化<a href="#缓存">缓存</a>(Redis、Memcached)的数据存储区中。</li></ul></li><li>缓存和数据库等下游服务器需要随着上游服务器进行扩展,以处理更多的并发连接。</li></ul><h3 id="缺陷:负载均衡器"><a href="#缺陷:负载均衡器" class="headerlink" title="缺陷:负载均衡器"></a>缺陷:负载均衡器</h3><ul><li>如果没有足够的资源配置或配置错误,负载均衡器会变成一个性能瓶颈。</li><li>引入负载均衡器以帮助消除单点故障但导致了额外的复杂性。</li><li>单个负载均衡器会导致单点故障,但配置多个负载均衡器会进一步增加复杂性。</li></ul><h3 id="来源及延伸阅读-6"><a href="#来源及延伸阅读-6" class="headerlink" title="来源及延伸阅读"></a>来源及延伸阅读</h3><ul><li><a href="https://www.nginx.com/blog/inside-nginx-how-we-designed-for-performance-scale/" target="_blank" rel="noopener">NGINX 架构</a></li><li><a href="http://www.haproxy.org/download/1.2/doc/architecture.txt" target="_blank" rel="noopener">HAProxy 架构指南</a></li><li><a href="http://www.lecloud.net/post/7295452622/scalability-for-dummies-part-1-clones" target="_blank" rel="noopener">可扩展性</a></li><li><a href="https://en.wikipedia.org/wiki/Load_balancing_(computing)" target="_blank" rel="noopener">Wikipedia</a></li><li><a href="https://www.nginx.com/resources/glossary/layer-4-load-balancing/" target="_blank" rel="noopener">四层负载平衡</a></li><li><a href="https://www.nginx.com/resources/glossary/layer-7-load-balancing/" target="_blank" rel="noopener">七层负载平衡</a></li><li><a href="http://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-listener-config.html" target="_blank" rel="noopener">ELB 监听器配置</a></li></ul><h2 id="反向代理(web-服务器)"><a href="#反向代理(web-服务器)" class="headerlink" title="反向代理(web 服务器)"></a>反向代理(web 服务器)</h2><p align="center"> <img src="http://i.imgur.com/n41Azff.png" srcset="/img/loading.gif"> <br/> <strong><a href="https://upload.wikimedia.org/wikipedia/commons/6/67/Reverse_proxy_h2g2bob.svg" target="_blank" rel="noopener">资料来源:维基百科</a></strong> <br/></p><p>反向代理是一种可以集中地调用内部服务,并提供统一接口给公共客户的 web 服务器。来自客户端的请求先被反向代理服务器转发到可响应请求的服务器,然后代理再把服务器的响应结果返回给客户端。</p><p>带来的好处包括:</p><ul><li><strong>增加安全性</strong> - 隐藏后端服务器的信息,屏蔽黑名单中的 IP,限制每个客户端的连接数。</li><li><strong>提高可扩展性和灵活性</strong> - 客户端只能看到反向代理服务器的 IP,这使你可以增减服务器或者修改它们的配置。</li><li><strong>本地终结 SSL 会话</strong> - 解密传入请求,加密服务器响应,这样后端服务器就不必完成这些潜在的高成本的操作。<ul><li>免除了在每个服务器上安装 <a href="https://en.wikipedia.org/wiki/X.509" target="_blank" rel="noopener">X.509</a> 证书的需要</li></ul></li><li><strong>压缩</strong> - 压缩服务器响应</li><li><strong>缓存</strong> - 直接返回命中的缓存结果</li><li><strong>静态内容</strong> - 直接提供静态内容<ul><li>HTML/CSS/JS</li><li>图片</li><li>视频</li><li>等等</li></ul></li></ul><h3 id="负载均衡器与反向代理"><a href="#负载均衡器与反向代理" class="headerlink" title="负载均衡器与反向代理"></a>负载均衡器与反向代理</h3><ul><li>当你有多个服务器时,部署负载均衡器非常有用。通常,负载均衡器将流量路由给一组功能相同的服务器上。</li><li>即使只有一台 web 服务器或者应用服务器时,反向代理也有用,可以参考上一节介绍的好处。</li><li>NGINX 和 HAProxy 等解决方案可以同时支持第七层反向代理和负载均衡。</li></ul><h3 id="不利之处:反向代理"><a href="#不利之处:反向代理" class="headerlink" title="不利之处:反向代理"></a>不利之处:反向代理</h3><ul><li>引入反向代理会增加系统的复杂度。</li><li>单独一个反向代理服务器仍可能发生单点故障,配置多台反向代理服务器(如<a href="https://en.wikipedia.org/wiki/Failover" target="_blank" rel="noopener">故障转移</a>)会进一步增加复杂度。</li></ul><h3 id="来源及延伸阅读-7"><a href="#来源及延伸阅读-7" class="headerlink" title="来源及延伸阅读"></a>来源及延伸阅读</h3><ul><li><a href="https://www.nginx.com/resources/glossary/reverse-proxy-vs-load-balancer/" target="_blank" rel="noopener">反向代理与负载均衡</a></li><li><a href="https://www.nginx.com/blog/inside-nginx-how-we-designed-for-performance-scale/" target="_blank" rel="noopener">NGINX 架构</a></li><li><a href="http://www.haproxy.org/download/1.2/doc/architecture.txt" target="_blank" rel="noopener">HAProxy 架构指南</a></li><li><a href="https://en.wikipedia.org/wiki/Reverse_proxy" target="_blank" rel="noopener">Wikipedia</a></li></ul><h2 id="应用层"><a href="#应用层" class="headerlink" title="应用层"></a>应用层</h2><p align="center"> <img src="http://i.imgur.com/yB5SYwm.png" srcset="/img/loading.gif"> <br/> <strong><a href="http://lethain.com/introduction-to-architecting-systems-for-scale/#platform_layer" target="_blank" rel="noopener">资料来源:可缩放系统构架介绍</a></strong></p><p>将 Web 服务层与应用层(也被称作平台层)分离,可以独立缩放和配置这两层。添加新的 API 只需要添加应用服务器,而不必添加额外的 web 服务器。</p><p><strong>单一职责原则</strong>提倡小型的,自治的服务共同合作。小团队通过提供小型的服务,可以更激进地计划增长。</p><p>应用层中的工作进程也有可以实现<a href="#异步">异步化</a>。</p><h3 id="微服务"><a href="#微服务" class="headerlink" title="微服务"></a>微服务</h3><p>与此讨论相关的话题是 <a href="https://en.wikipedia.org/wiki/Microservices" target="_blank" rel="noopener">微服务</a>,可以被描述为一系列可以独立部署的小型的,模块化服务。每个服务运行在一个独立的线程中,通过明确定义的轻量级机制通讯,共同实现业务目标。<sup><a href=https://smartbear.com/learn/api-design/what-are-microservices>1</a></sup></p><p>例如,Pinterest 可能有这些微服务: 用户资料、关注者、Feed 流、搜索、照片上传等。</p><h3 id="服务发现"><a href="#服务发现" class="headerlink" title="服务发现"></a>服务发现</h3><p>像 <a href="https://www.consul.io/docs/index.html" target="_blank" rel="noopener">Consul</a>,<a href="https://coreos.com/etcd/docs/latest" target="_blank" rel="noopener">Etcd</a> 和 <a href="http://www.slideshare.net/sauravhaloi/introduction-to-apache-zookeeper" target="_blank" rel="noopener">Zookeeper</a> 这样的系统可以通过追踪注册名、地址、端口等信息来帮助服务互相发现对方。<a href="https://www.consul.io/intro/getting-started/checks.html" target="_blank" rel="noopener">Health checks</a> 可以帮助确认服务的完整性和是否经常使用一个 <a href="#超文本传输协议http">HTTP</a> 路径。Consul 和 Etcd 都有一个内建的 <a href="#键-值存储">key-value 存储</a> 用来存储配置信息和其他的共享信息。</p><h3 id="不利之处:应用层"><a href="#不利之处:应用层" class="headerlink" title="不利之处:应用层"></a>不利之处:应用层</h3><ul><li>添加由多个松耦合服务组成的应用层,从架构、运营、流程等层面来讲将非常不同(相对于单体系统)。</li><li>微服务会增加部署和运营的复杂度。</li></ul><h3 id="来源及延伸阅读-8"><a href="#来源及延伸阅读-8" class="headerlink" title="来源及延伸阅读"></a>来源及延伸阅读</h3><ul><li><a href="http://lethain.com/introduction-to-architecting-systems-for-scale" target="_blank" rel="noopener">可缩放系统构架介绍</a></li><li><a href="http://www.puncsky.com/blog/2016/02/14/crack-the-system-design-interview/" target="_blank" rel="noopener">破解系统设计面试</a></li><li><a href="https://en.wikipedia.org/wiki/Service-oriented_architecture" target="_blank" rel="noopener">面向服务架构</a></li><li><a href="http://www.slideshare.net/sauravhaloi/introduction-to-apache-zookeeper" target="_blank" rel="noopener">Zookeeper 介绍</a></li><li><a href="https://cloudncode.wordpress.com/2016/07/22/msa-getting-started/" target="_blank" rel="noopener">构建微服务,你所需要知道的一切</a></li></ul><h2 id="数据库"><a href="#数据库" class="headerlink" title="数据库"></a>数据库</h2><p align="center"> <img src="http://i.imgur.com/Xkm5CXz.png" srcset="/img/loading.gif"> <br/> <strong><a href="https://www.youtube.com/watch?v=vg5onp8TU6Q" target="_blank" rel="noopener">资料来源:扩展你的用户数到第一个一千万</a></strong></p><h3 id="关系型数据库管理系统(RDBMS)"><a href="#关系型数据库管理系统(RDBMS)" class="headerlink" title="关系型数据库管理系统(RDBMS)"></a>关系型数据库管理系统(RDBMS)</h3><p>像 SQL 这样的关系型数据库是一系列以表的形式组织的数据项集合。</p><blockquote><p>校对注:这里作者 SQL 可能指的是 MySQL</p></blockquote><p><strong>ACID</strong> 用来描述关系型数据库<a href="https://en.wikipedia.org/wiki/Database_transaction" target="_blank" rel="noopener">事务</a>的特性。</p><ul><li><strong>原子性</strong> - 每个事务内部所有操作要么全部完成,要么全部不完成。</li><li><strong>一致性</strong> - 任何事务都使数据库从一个有效的状态转换到另一个有效状态。</li><li><strong>隔离性</strong> - 并发执行事务的结果与顺序执行事务的结果相同。</li><li><strong>持久性</strong> - 事务提交后,对系统的影响是永久的。</li></ul><p>关系型数据库扩展包括许多技术:<strong>主从复制</strong>、<strong>主主复制</strong>、<strong>联合</strong>、<strong>分片</strong>、<strong>非规范化</strong>和 <strong>SQL调优</strong>。</p><p align="center"> <img src="http://i.imgur.com/C9ioGtn.png" srcset="/img/loading.gif"> <br/> <strong><a href="http://www.slideshare.net/jboner/scalability-availability-stability-patterns/" target="_blank" rel="noopener">资料来源:可扩展性、可用性、稳定性、模式</a></strong></p><h4 id="主从复制"><a href="#主从复制" class="headerlink" title="主从复制"></a>主从复制</h4><p>主库同时负责读取和写入操作,并复制写入到一个或多个从库中,从库只负责读操作。树状形式的从库再将写入复制到更多的从库中去。如果主库离线,系统可以以只读模式运行,直到某个从库被提升为主库或有新的主库出现。</p><h5 id="不利之处:主从复制"><a href="#不利之处:主从复制" class="headerlink" title="不利之处:主从复制"></a>不利之处:主从复制</h5><ul><li>将从库提升为主库需要额外的逻辑。</li><li>参考<a href="#不利之处复制">不利之处:复制</a>中,主从复制和主主复制<strong>共同</strong>的问题。</li></ul><p align="center"> <img src="http://i.imgur.com/krAHLGg.png" srcset="/img/loading.gif"> <br/> <strong><a href="http://www.slideshare.net/jboner/scalability-availability-stability-patterns/" target="_blank" rel="noopener">资料来源:可扩展性、可用性、稳定性、模式</a></strong></p><h4 id="主主复制"><a href="#主主复制" class="headerlink" title="主主复制"></a>主主复制</h4><p>两个主库都负责读操作和写操作,写入操作时互相协调。如果其中一个主库挂机,系统可以继续读取和写入。</p><h5 id="不利之处:-主主复制"><a href="#不利之处:-主主复制" class="headerlink" title="不利之处: 主主复制"></a>不利之处: 主主复制</h5><ul><li>你需要添加负载均衡器或者在应用逻辑中做改动,来确定写入哪一个数据库。</li><li>多数主-主系统要么不能保证一致性(违反 ACID),要么因为同步产生了写入延迟。</li><li>随着更多写入节点的加入和延迟的提高,如何解决冲突显得越发重要。</li><li>参考<a href="#不利之处复制">不利之处:复制</a>中,主从复制和主主复制<strong>共同</strong>的问题。</li></ul><h5 id="不利之处:复制"><a href="#不利之处:复制" class="headerlink" title="不利之处:复制"></a>不利之处:复制</h5><ul><li>如果主库在将新写入的数据复制到其他节点前挂掉,则有数据丢失的可能。</li><li>写入会被重放到负责读取操作的副本。副本可能因为过多写操作阻塞住,导致读取功能异常。</li><li>读取从库越多,需要复制的写入数据就越多,导致更严重的复制延迟。</li><li>在某些数据库系统中,写入主库的操作可以用多个线程并行写入,但读取副本只支持单线程顺序地写入。</li><li>复制意味着更多的硬件和额外的复杂度。</li></ul><h5 id="来源及延伸阅读-9"><a href="#来源及延伸阅读-9" class="headerlink" title="来源及延伸阅读"></a>来源及延伸阅读</h5><ul><li><a href="http://www.slideshare.net/jboner/scalability-availability-stability-patterns/" target="_blank" rel="noopener">扩展性,可用性,稳定性模式</a></li><li><a href="https://en.wikipedia.org/wiki/Multi-master_replication" target="_blank" rel="noopener">多主复制</a></li></ul><h4 id="联合"><a href="#联合" class="headerlink" title="联合"></a>联合</h4><p align="center"> <img src="http://i.imgur.com/U3qV33e.png" srcset="/img/loading.gif"> <br/> <strong><a href="https://www.youtube.com/watch?v=vg5onp8TU6Q" target="_blank" rel="noopener">资料来源:扩展你的用户数到第一个一千万</a></strong></p><p>联合(或按功能划分)将数据库按对应功能分割。例如,你可以有三个数据库:<strong>论坛</strong>、<strong>用户</strong>和<strong>产品</strong>,而不仅是一个单体数据库,从而减少每个数据库的读取和写入流量,减少复制延迟。较小的数据库意味着更多适合放入内存的数据,进而意味着更高的缓存命中几率。没有只能串行写入的中心化主库,你可以并行写入,提高负载能力。</p><h5 id="不利之处:联合"><a href="#不利之处:联合" class="headerlink" title="不利之处:联合"></a>不利之处:联合</h5><ul><li>如果你的数据库模式需要大量的功能和数据表,联合的效率并不好。</li><li>你需要更新应用程序的逻辑来确定要读取和写入哪个数据库。</li><li>用 <a href="http://stackoverflow.com/questions/5145637/querying-data-by-joining-two-tables-in-two-database-on-different-servers" target="_blank" rel="noopener">server link</a> 从两个库联结数据更复杂。</li><li>联合需要更多的硬件和额外的复杂度。</li></ul><h5 id="来源及延伸阅读:联合"><a href="#来源及延伸阅读:联合" class="headerlink" title="来源及延伸阅读:联合"></a>来源及延伸阅读:联合</h5><ul><li><a href="https://www.youtube.com/watch?v=vg5onp8TU6Q" target="_blank" rel="noopener">扩展你的用户数到第一个一千万</a></li></ul><h4 id="分片"><a href="#分片" class="headerlink" title="分片"></a>分片</h4><p align="center"> <img src="http://i.imgur.com/wU8x5Id.png" srcset="/img/loading.gif"> <br/> <strong><a href="http://www.slideshare.net/jboner/scalability-availability-stability-patterns/" target="_blank" rel="noopener">资料来源:可扩展性、可用性、稳定性、模式</a></strong></p><p>分片将数据分配在不同的数据库上,使得每个数据库仅管理整个数据集的一个子集。以用户数据库为例,随着用户数量的增加,越来越多的分片会被添加到集群中。</p><p>类似<a href="#联合">联合</a>的优点,分片可以减少读取和写入流量,减少复制并提高缓存命中率。也减少了索引,通常意味着查询更快,性能更好。如果一个分片出问题,其他的仍能运行,你可以使用某种形式的冗余来防止数据丢失。类似联合,没有只能串行写入的中心化主库,你可以并行写入,提高负载能力。</p><p>常见的做法是用户姓氏的首字母或者用户的地理位置来分隔用户表。</p><h5 id="不利之处:分片"><a href="#不利之处:分片" class="headerlink" title="不利之处:分片"></a>不利之处:分片</h5><ul><li>你需要修改应用程序的逻辑来实现分片,这会带来复杂的 SQL 查询。</li><li>分片不合理可能导致数据负载不均衡。例如,被频繁访问的用户数据会导致其所在分片的负载相对其他分片高。<ul><li>再平衡会引入额外的复杂度。基于<a href="http://www.paperplanes.de/2011/12/9/the-magic-of-consistent-hashing.html" target="_blank" rel="noopener">一致性哈希</a>的分片算法可以减少这种情况。</li></ul></li><li>联结多个分片的数据操作更复杂。</li><li>分片需要更多的硬件和额外的复杂度。</li></ul><h4 id="来源及延伸阅读:分片"><a href="#来源及延伸阅读:分片" class="headerlink" title="来源及延伸阅读:分片"></a>来源及延伸阅读:分片</h4><ul><li><a href="http://highscalability.com/blog/2009/8/6/an-unorthodox-approach-to-database-design-the-coming-of-the.html" target="_blank" rel="noopener">分片时代来临</a></li><li><a href="https://en.wikipedia.org/wiki/Shard_(database_architecture)" target="_blank" rel="noopener">数据库分片架构</a></li><li><a href="http://www.paperplanes.de/2011/12/9/the-magic-of-consistent-hashing.html" target="_blank" rel="noopener">一致性哈希</a></li></ul><h4 id="非规范化"><a href="#非规范化" class="headerlink" title="非规范化"></a>非规范化</h4><p>非规范化试图以写入性能为代价来换取读取性能。在多个表中冗余数据副本,以避免高成本的联结操作。一些关系型数据库,比如 <a href="https://en.wikipedia.org/wiki/PostgreSQL" target="_blank" rel="noopener">PostgreSQl</a> 和 Oracle 支持<a href="https://en.wikipedia.org/wiki/Materialized_view" target="_blank" rel="noopener">物化视图</a>,可以处理冗余信息存储和保证冗余副本一致。</p><p>当数据使用诸如<a href="#联合">联合</a>和<a href="#分片">分片</a>等技术被分割,进一步提高了处理跨数据中心的联结操作复杂度。非规范化可以规避这种复杂的联结操作。</p><p>在多数系统中,读取操作的频率远高于写入操作,比例可达到 100:1,甚至 1000:1。需要复杂的数据库联结的读取操作成本非常高,在磁盘操作上消耗了大量时间。</p><h5 id="不利之处:非规范化"><a href="#不利之处:非规范化" class="headerlink" title="不利之处:非规范化"></a>不利之处:非规范化</h5><ul><li>数据会冗余。</li><li>约束可以帮助冗余的信息副本保持同步,但这样会增加数据库设计的复杂度。</li><li>非规范化的数据库在高写入负载下性能可能比规范化的数据库差。</li></ul><h5 id="来源及延伸阅读:非规范化"><a href="#来源及延伸阅读:非规范化" class="headerlink" title="来源及延伸阅读:非规范化"></a>来源及延伸阅读:非规范化</h5><ul><li><a href="https://en.wikipedia.org/wiki/Denormalization" target="_blank" rel="noopener">非规范化</a></li></ul><h4 id="SQL-调优"><a href="#SQL-调优" class="headerlink" title="SQL 调优"></a>SQL 调优</h4><p>SQL 调优是一个范围很广的话题,有很多相关的<a href="https://www.amazon.com/s/ref=nb_sb_noss_2?url=search-alias%3Daps&field-keywords=sql+tuning" target="_blank" rel="noopener">书</a>可以作为参考。</p><p>利用<strong>基准测试</strong>和<strong>性能分析</strong>来模拟和发现系统瓶颈很重要。</p><ul><li><strong>基准测试</strong> - 用 <a href="http://httpd.apache.org/docs/2.2/programs/ab.html" target="_blank" rel="noopener">ab</a> 等工具模拟高负载情况。</li><li><strong>性能分析</strong> - 通过启用如<a href="http://dev.mysql.com/doc/refman/5.7/en/slow-query-log.html" target="_blank" rel="noopener">慢查询日志</a>等工具来辅助追踪性能问题。</li></ul><p>基准测试和性能分析可能会指引你到以下优化方案。</p><h5 id="改进模式"><a href="#改进模式" class="headerlink" title="改进模式"></a>改进模式</h5><ul><li>为了实现快速访问,MySQL 在磁盘上用连续的块存储数据。</li><li>使用 <code>CHAR</code> 类型存储固定长度的字段,不要用 <code>VARCHAR</code>。<ul><li><code>CHAR</code> 在快速、随机访问时效率很高。如果使用 <code>VARCHAR</code>,如果你想读取下一个字符串,不得不先读取到当前字符串的末尾。</li></ul></li><li>使用 <code>TEXT</code> 类型存储大块的文本,例如博客正文。<code>TEXT</code> 还允许布尔搜索。使用 <code>TEXT</code> 字段需要在磁盘上存储一个用于定位文本块的指针。</li><li>使用 <code>INT</code> 类型存储高达 2^32 或 40 亿的较大数字。</li><li>使用 <code>DECIMAL</code> 类型存储货币可以避免浮点数表示错误。</li><li>避免使用 <code>BLOBS</code> 存储对象,存储存放对象的位置。</li><li><code>VARCHAR(255)</code> 是以 8 位数字存储的最大字符数,在某些关系型数据库中,最大限度地利用字节。</li><li>在适用场景中设置 <code>NOT NULL</code> 约束来<a href="http://stackoverflow.com/questions/1017239/how-do-null-values-affect-performance-in-a-database-search" target="_blank" rel="noopener">提高搜索性能</a>。</li></ul><h5 id="使用正确的索引"><a href="#使用正确的索引" class="headerlink" title="使用正确的索引"></a>使用正确的索引</h5><ul><li>你正查询(<code>SELECT</code>、<code>GROUP BY</code>、<code>ORDER BY</code>、<code>JOIN</code>)的列如果用了索引会更快。</li><li>索引通常表示为自平衡的 <a href="https://en.wikipedia.org/wiki/B-tree" target="_blank" rel="noopener">B 树</a>,可以保持数据有序,并允许在对数时间内进行搜索,顺序访问,插入,删除操作。</li><li>设置索引,会将数据存在内存中,占用了更多内存空间。</li><li>写入操作会变慢,因为索引需要被更新。</li><li>加载大量数据时,禁用索引再加载数据,然后重建索引,这样也许会更快。</li></ul><h5 id="避免高成本的联结操作"><a href="#避免高成本的联结操作" class="headerlink" title="避免高成本的联结操作"></a>避免高成本的联结操作</h5><ul><li>有性能需要,可以进行非规范化。</li></ul><h5 id="分割数据表"><a href="#分割数据表" class="headerlink" title="分割数据表"></a>分割数据表</h5><ul><li>将热点数据拆分到单独的数据表中,可以有助于缓存。</li></ul><h5 id="调优查询缓存"><a href="#调优查询缓存" class="headerlink" title="调优查询缓存"></a>调优查询缓存</h5><ul><li>在某些情况下,<a href="http://dev.mysql.com/doc/refman/5.7/en/query-cache" target="_blank" rel="noopener">查询缓存</a>可能会导致<a href="https://www.percona.com/blog/2014/01/28/10-mysql-performance-tuning-settings-after-installation/" target="_blank" rel="noopener">性能问题</a>。</li></ul><h5 id="来源及延伸阅读-10"><a href="#来源及延伸阅读-10" class="headerlink" title="来源及延伸阅读"></a>来源及延伸阅读</h5><ul><li><a href="http://20bits.com/article/10-tips-for-optimizing-mysql-queries-that-dont-suck" target="_blank" rel="noopener">MySQL 查询优化小贴士</a></li><li><a href="http://stackoverflow.com/questions/1217466/is-there-a-good-reason-i-see-varchar255-used-so-often-as-opposed-to-another-l" target="_blank" rel="noopener">为什么 VARCHAR(255) 很常见?</a></li><li><a href="http://stackoverflow.com/questions/1017239/how-do-null-values-affect-performance-in-a-database-search" target="_blank" rel="noopener">Null 值是如何影响数据库性能的?</a></li><li><a href="http://dev.mysql.com/doc/refman/5.7/en/slow-query-log.html" target="_blank" rel="noopener">慢查询日志</a></li></ul><h3 id="NoSQL"><a href="#NoSQL" class="headerlink" title="NoSQL"></a>NoSQL</h3><p>NoSQL 是<strong>键-值数据库</strong>、<strong>文档型数据库</strong>、<strong>列型数据库</strong>或<strong>图数据库</strong>的统称。数据库是非规范化的,表联结大多在应用程序代码中完成。大多数 NoSQL 无法实现真正符合 ACID 的事务,支持<a href="#最终一致性">最终一致</a>。</p><p><strong>BASE</strong> 通常被用于描述 NoSQL 数据库的特性。相比 <a href="#cap-理论">CAP 理论</a>,BASE 强调可用性超过一致性。</p><ul><li><strong>基本可用</strong> - 系统保证可用性。</li><li><strong>软状态</strong> - 即使没有输入,系统状态也可能随着时间变化。</li><li><strong>最终一致性</strong> - 经过一段时间之后,系统最终会变一致,因为系统在此期间没有收到任何输入。</li></ul><p>除了在 <a href="#sql-还是-nosql">SQL 还是 NoSQL</a> 之间做选择,了解哪种类型的 NoSQL 数据库最适合你的用例也是非常有帮助的。我们将在下一节中快速了解下 <strong>键-值存储</strong>、<strong>文档型存储</strong>、<strong>列型存储</strong>和<strong>图存储</strong>数据库。</p><h4 id="键-值存储"><a href="#键-值存储" class="headerlink" title="键-值存储"></a>键-值存储</h4><blockquote><p>抽象模型:哈希表</p></blockquote><p>键-值存储通常可以实现 O(1) 时间读写,用内存或 SSD 存储数据。数据存储可以按<a href="https://en.wikipedia.org/wiki/Lexicographical_order" target="_blank" rel="noopener">字典顺序</a>维护键,从而实现键的高效检索。键-值存储可以用于存储元数据。</p><p>键-值存储性能很高,通常用于存储简单数据模型或频繁修改的数据,如存放在内存中的缓存。键-值存储提供的操作有限,如果需要更多操作,复杂度将转嫁到应用程序层面。</p><p>键-值存储是如文档存储,在某些情况下,甚至是图存储等更复杂的存储系统的基础。</p><h4 id="来源及延伸阅读-11"><a href="#来源及延伸阅读-11" class="headerlink" title="来源及延伸阅读"></a>来源及延伸阅读</h4><ul><li><a href="https://en.wikipedia.org/wiki/Key-value_database" target="_blank" rel="noopener">键-值数据库</a></li><li><a href="http://stackoverflow.com/questions/4056093/what-are-the-disadvantages-of-using-a-key-value-table-over-nullable-columns-or" target="_blank" rel="noopener">键-值存储的劣势</a></li><li><a href="http://qnimate.com/overview-of-redis-architecture/" target="_blank" rel="noopener">Redis 架构</a></li><li><a href="https://www.adayinthelifeof.nl/2011/02/06/memcache-internals/" target="_blank" rel="noopener">Memcached 架构</a></li></ul><h4 id="文档类型存储"><a href="#文档类型存储" class="headerlink" title="文档类型存储"></a>文档类型存储</h4><blockquote><p>抽象模型:将文档作为值的键-值存储</p></blockquote><p>文档类型存储以文档(XML、JSON、二进制文件等)为中心,文档存储了指定对象的全部信息。文档存储根据文档自身的内部结构提供 API 或查询语句来实现查询。请注意,许多键-值存储数据库有用值存储元数据的特性,这也模糊了这两种存储类型的界限。</p><p>基于底层实现,文档可以根据集合、标签、元数据或者文件夹组织。尽管不同文档可以被组织在一起或者分成一组,但相互之间可能具有完全不同的字段。</p><p>MongoDB 和 CouchDB 等一些文档类型存储还提供了类似 SQL 语言的查询语句来实现复杂查询。DynamoDB 同时支持键-值存储和文档类型存储。</p><p>文档类型存储具备高度的灵活性,常用于处理偶尔变化的数据。</p><h4 id="来源及延伸阅读:文档类型存储"><a href="#来源及延伸阅读:文档类型存储" class="headerlink" title="来源及延伸阅读:文档类型存储"></a>来源及延伸阅读:文档类型存储</h4><ul><li><a href="https://en.wikipedia.org/wiki/Document-oriented_database" target="_blank" rel="noopener">面向文档的数据库</a></li><li><a href="https://www.mongodb.com/mongodb-architecture" target="_blank" rel="noopener">MongoDB 架构</a></li><li><a href="https://blog.couchdb.org/2016/08/01/couchdb-2-0-architecture/" target="_blank" rel="noopener">CouchDB 架构</a></li><li><a href="https://www.elastic.co/blog/found-elasticsearch-from-the-bottom-up" target="_blank" rel="noopener">Elasticsearch 架构</a></li></ul><h4 id="列型存储"><a href="#列型存储" class="headerlink" title="列型存储"></a>列型存储</h4><p align="center"> <img src="http://i.imgur.com/n16iOGk.png" srcset="/img/loading.gif"> <br/> <strong><a href="http://blog.grio.com/2015/11/sql-nosql-a-brief-history.html" target="_blank" rel="noopener">资料来源: SQL 和 NoSQL,一个简短的历史</a></strong></p><blockquote><p>抽象模型:嵌套的 <code>ColumnFamily<RowKey, Columns<ColKey, Value, Timestamp>></code> 映射</p></blockquote><p>类型存储的基本数据单元是列(名/值对)。列可以在列族(类似于 SQL 的数据表)中被分组。超级列族再分组普通列族。你可以使用行键独立访问每一列,具有相同行键值的列组成一行。每个值都包含版本的时间戳用于解决版本冲突。</p><p>Google 发布了第一个列型存储数据库 <a href="http://www.read.seas.harvard.edu/~kohler/class/cs239-w08/chang06bigtable.pdf" target="_blank" rel="noopener">Bigtable</a>,它影响了 Hadoop 生态系统中活跃的开源数据库 <a href="https://www.mapr.com/blog/in-depth-look-hbase-architecture" target="_blank" rel="noopener">HBase</a> 和 Facebook 的 <a href="http://docs.datastax.com/en/archived/cassandra/2.0/cassandra/architecture/architectureIntro_c.html" target="_blank" rel="noopener">Cassandra</a>。像 BigTable,HBase 和 Cassandra 这样的存储系统将键以字母顺序存储,可以高效地读取键列。</p><p>列型存储具备高可用性和高可扩展性。通常被用于大数据相关存储。</p><h5 id="来源及延伸阅读:列型存储"><a href="#来源及延伸阅读:列型存储" class="headerlink" title="来源及延伸阅读:列型存储"></a>来源及延伸阅读:列型存储</h5><ul><li><a href="http://blog.grio.com/2015/11/sql-nosql-a-brief-history.html" target="_blank" rel="noopener">SQL 与 NoSQL 简史</a></li><li><a href="http://www.read.seas.harvard.edu/~kohler/class/cs239-w08/chang06bigtable.pdf" target="_blank" rel="noopener">BigTable 架构</a></li><li><a href="https://www.mapr.com/blog/in-depth-look-hbase-architecture" target="_blank" rel="noopener">Hbase 架构</a></li><li><a href="http://docs.datastax.com/en/archived/cassandra/2.0/cassandra/architecture/architectureIntro_c.html" target="_blank" rel="noopener">Cassandra 架构</a></li></ul><h4 id="图数据库"><a href="#图数据库" class="headerlink" title="图数据库"></a>图数据库</h4><p align="center"> <img src="http://i.imgur.com/fNcl65g.png" srcset="/img/loading.gif"> <br/> <strong><a href="https://en.wikipedia.org/wiki/File:GraphDatabase_PropertyGraph.png" target="_blank" rel="noopener">资料来源:图数据库</a></strong></p><blockquote><p>抽象模型: 图</p></blockquote><p>在图数据库中,一个节点对应一条记录,一个弧对应两个节点之间的关系。图数据库被优化用于表示外键繁多的复杂关系或多对多关系。</p><p>图数据库为存储复杂关系的数据模型,如社交网络,提供了很高的性能。它们相对较新,尚未广泛应用,查找开发工具或者资源相对较难。许多图只能通过 <a href="#表述性状态转移rest">REST API</a> 访问。</p><h5 id="相关资源和延伸阅读:图"><a href="#相关资源和延伸阅读:图" class="headerlink" title="相关资源和延伸阅读:图"></a>相关资源和延伸阅读:图</h5><ul><li><a href="https://en.wikipedia.org/wiki/Graph_database" target="_blank" rel="noopener">图数据库</a></li><li><a href="https://neo4j.com/" target="_blank" rel="noopener">Neo4j</a></li><li><a href="https://blog.twitter.com/2010/introducing-flockdb" target="_blank" rel="noopener">FlockDB</a></li></ul><h4 id="来源及延伸阅读:NoSQL"><a href="#来源及延伸阅读:NoSQL" class="headerlink" title="来源及延伸阅读:NoSQL"></a>来源及延伸阅读:NoSQL</h4><ul><li><a href="http://stackoverflow.com/questions/3342497/explanation-of-base-terminology" target="_blank" rel="noopener">数据库术语解释</a></li><li><a href="https://medium.com/baqend-blog/nosql-databases-a-survey-and-decision-guidance-ea7823a822d#.wskogqenq" target="_blank" rel="noopener">NoSQL 数据库 - 调查及决策指南</a></li><li><a href="http://www.lecloud.net/post/7994751381/scalability-for-dummies-part-2-database" target="_blank" rel="noopener">可扩展性</a></li><li><a href="https://www.youtube.com/watch?v=qI_g07C_Q5I" target="_blank" rel="noopener">NoSQL 介绍</a></li><li><a href="http://horicky.blogspot.com/2009/11/nosql-patterns.html" target="_blank" rel="noopener">NoSQL 模式</a></li></ul><h3 id="SQL-还是-NoSQL"><a href="#SQL-还是-NoSQL" class="headerlink" title="SQL 还是 NoSQL"></a>SQL 还是 NoSQL</h3><p align="center"> <img src="http://i.imgur.com/wXGqG5f.png" srcset="/img/loading.gif"> <br/> <strong><a href="https://www.infoq.com/articles/Transition-RDBMS-NoSQL/" target="_blank" rel="noopener">资料来源:从 RDBMS 转换到 NoSQL</a></strong></p><p>选取 <strong>SQL</strong> 的原因:</p><ul><li>结构化数据</li><li>严格的模式</li><li>关系型数据</li><li>需要复杂的联结操作</li><li>事务</li><li>清晰的扩展模式</li><li>既有资源更丰富:开发者、社区、代码库、工具等</li><li>通过索引进行查询非常快</li></ul><p>选取 <strong>NoSQL</strong> 的原因:</p><ul><li>半结构化数据</li><li>动态或灵活的模式</li><li>非关系型数据</li><li>不需要复杂的联结操作</li><li>存储 TB (甚至 PB)级别的数据</li><li>高数据密集的工作负载</li><li>IOPS 高吞吐量</li></ul><p>适合 NoSQL 的示例数据:</p><ul><li>埋点数据和日志数据</li><li>排行榜或者得分数据</li><li>临时数据,如购物车</li><li>频繁访问的(“热”)表</li><li>元数据/查找表</li></ul><h5 id="来源及延伸阅读:SQL-或-NoSQL"><a href="#来源及延伸阅读:SQL-或-NoSQL" class="headerlink" title="来源及延伸阅读:SQL 或 NoSQL"></a>来源及延伸阅读:SQL 或 NoSQL</h5><ul><li><a href="https://www.youtube.com/watch?v=vg5onp8TU6Q" target="_blank" rel="noopener">扩展你的用户数到第一个千万</a></li><li><a href="https://www.sitepoint.com/sql-vs-nosql-differences/" target="_blank" rel="noopener">SQL 和 NoSQL 的不同</a><h2 id="缓存"><a href="#缓存" class="headerlink" title="缓存"></a>缓存</h2></li></ul><p align="center"> <img src="http://i.imgur.com/Q6z24La.png" srcset="/img/loading.gif"> <br/> <strong><a href="http://horicky.blogspot.com/2010/10/scalable-system-design-patterns.html" target="_blank" rel="noopener">资料来源:可扩展的系统设计模式</a></strong></p><p>缓存可以提高页面加载速度,并可以减少服务器和数据库的负载。在这个模型中,分发器先查看请求之前是否被响应过,如果有则将之前的结果直接返回,来省掉真正的处理。</p><p>数据库分片均匀分布的读取是最好的。但是热门数据会让读取分布不均匀,这样就会造成瓶颈,如果在数据库前加个缓存,就会抹平不均匀的负载和突发流量对数据库的影响。</p><h3 id="客户端缓存"><a href="#客户端缓存" class="headerlink" title="客户端缓存"></a>客户端缓存</h3><p>缓存可以位于客户端(操作系统或者浏览器),<a href="#反向代理web-服务器">服务端</a>或者不同的缓存层。</p><h3 id="CDN-缓存"><a href="#CDN-缓存" class="headerlink" title="CDN 缓存"></a>CDN 缓存</h3><p><a href="#内容分发网络cdn">CDN</a> 也被视为一种缓存。</p><h3 id="Web-服务器缓存"><a href="#Web-服务器缓存" class="headerlink" title="Web 服务器缓存"></a>Web 服务器缓存</h3><p><a href="#反向代理web-服务器">反向代理</a>和缓存(比如 <a href="https://www.varnish-cache.org/" target="_blank" rel="noopener">Varnish</a>)可以直接提供静态和动态内容。Web 服务器同样也可以缓存请求,返回相应结果而不必连接应用服务器。</p><h3 id="数据库缓存"><a href="#数据库缓存" class="headerlink" title="数据库缓存"></a>数据库缓存</h3><p>数据库的默认配置中通常包含缓存级别,针对一般用例进行了优化。调整配置,在不同情况下使用不同的模式可以进一步提高性能。</p><h3 id="应用缓存"><a href="#应用缓存" class="headerlink" title="应用缓存"></a>应用缓存</h3><p>基于内存的缓存比如 Memcached 和 Redis 是应用程序和数据存储之间的一种键值存储。由于数据保存在 RAM 中,它比存储在磁盘上的典型数据库要快多了。RAM 比磁盘限制更多,所以例如 <a href="https://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used" target="_blank" rel="noopener">least recently used (LRU)</a> 的<a href="https://en.wikipedia.org/wiki/Cache_algorithms" target="_blank" rel="noopener">缓存无效算法</a>可以将「热门数据」放在 RAM 中,而对一些比较「冷门」的数据不做处理。</p><p>Redis 有下列附加功能:</p><ul><li>持久性选项</li><li>内置数据结构比如有序集合和列表</li></ul><p>有多个缓存级别,分为两大类:<strong>数据库查询</strong>和<strong>对象</strong>:</p><ul><li>行级别</li><li>查询级别</li><li>完整的可序列化对象</li><li>完全渲染的 HTML</li></ul><p>一般来说,你应该尽量避免基于文件的缓存,因为这使得复制和自动缩放很困难。</p><h3 id="数据库查询级别的缓存"><a href="#数据库查询级别的缓存" class="headerlink" title="数据库查询级别的缓存"></a>数据库查询级别的缓存</h3><p>当你查询数据库的时候,将查询语句的哈希值与查询结果存储到缓存中。这种方法会遇到以下问题:</p><ul><li>很难用复杂的查询删除已缓存结果。</li><li>如果一条数据比如表中某条数据的一项被改变,则需要删除所有可能包含已更改项的缓存结果。</li></ul><h3 id="对象级别的缓存"><a href="#对象级别的缓存" class="headerlink" title="对象级别的缓存"></a>对象级别的缓存</h3><p>将您的数据视为对象,就像对待你的应用代码一样。让应用程序将数据从数据库中组合到类实例或数据结构中:</p><ul><li>如果对象的基础数据已经更改了,那么从缓存中删掉这个对象。</li><li>允许异步处理:workers 通过使用最新的缓存对象来组装对象。</li></ul><p>建议缓存的内容:</p><ul><li>用户会话</li><li>完全渲染的 Web 页面</li><li>活动流</li><li>用户图数据</li></ul><h3 id="何时更新缓存"><a href="#何时更新缓存" class="headerlink" title="何时更新缓存"></a>何时更新缓存</h3><p>由于你只能在缓存中存储有限的数据,所以你需要选择一个适用于你用例的缓存更新策略。</p><h4 id="缓存模式"><a href="#缓存模式" class="headerlink" title="缓存模式"></a>缓存模式</h4><p align="center"> <img src="http://i.imgur.com/ONjORqk.png" srcset="/img/loading.gif"> <br/> <strong><a href="http://www.slideshare.net/tmatyashovsky/from-cache-to-in-memory-data-grid-introduction-to-hazelcast" target="_blank" rel="noopener">资料来源:从缓存到内存数据网格</a></strong></p><p>应用从存储器读写。缓存不和存储器直接交互,应用执行以下操作:</p><ul><li>在缓存中查找记录,如果所需数据不在缓存中</li><li>从数据库中加载所需内容</li><li>将查找到的结果存储到缓存中</li><li>返回所需内容</li></ul><pre><code class="hljs plain">def get_user(self, user_id): user = cache.get("user.{0}", user_id) if user is None: user = db.query("SELECT * FROM users WHERE user_id = {0}", user_id) if user is not None: key = "user.{0}".format(user_id) cache.set(key, json.dumps(user)) return user</code></pre><p><a href="https://memcached.org/" target="_blank" rel="noopener">Memcached</a> 通常用这种方式使用。</p><p>添加到缓存中的数据读取速度很快。缓存模式也称为延迟加载。只缓存所请求的数据,这避免了没有被请求的数据占满了缓存空间。</p><h5 id="缓存的缺点:"><a href="#缓存的缺点:" class="headerlink" title="缓存的缺点:"></a>缓存的缺点:</h5><ul><li>请求的数据如果不在缓存中就需要经过三个步骤来获取数据,这会导致明显的延迟。</li><li>如果数据库中的数据更新了会导致缓存中的数据过时。这个问题需要通过设置 TTL 强制更新缓存或者直写模式来缓解这种情况。</li><li>当一个节点出现故障的时候,它将会被一个新的节点替代,这增加了延迟的时间。</li></ul><h4 id="直写模式"><a href="#直写模式" class="headerlink" title="直写模式"></a>直写模式</h4><p align="center"> <img src="http://i.imgur.com/0vBc0hN.png" srcset="/img/loading.gif"> <br/> <strong><a href="http://www.slideshare.net/jboner/scalability-availability-stability-patterns/" target="_blank" rel="noopener">资料来源:可扩展性、可用性、稳定性、模式</a></strong></p><p>应用使用缓存作为主要的数据存储,将数据读写到缓存中,而缓存负责从数据库中读写数据。</p><ul><li>应用向缓存中添加/更新数据</li><li>缓存同步地写入数据存储</li><li>返回所需内容</li></ul><p>应用代码:</p><pre><code class="hljs plain">set_user(12345, {"foo":"bar"})</code></pre><p>缓存代码:</p><pre><code class="hljs plain">def set_user(user_id, values): user = db.query("UPDATE Users WHERE id = {0}", user_id, values) cache.set(user_id, user)</code></pre><p>由于存写操作所以直写模式整体是一种很慢的操作,但是读取刚写入的数据很快。相比读取数据,用户通常比较能接受更新数据时速度较慢。缓存中的数据不会过时。</p><h5 id="直写模式的缺点:"><a href="#直写模式的缺点:" class="headerlink" title="直写模式的缺点:"></a>直写模式的缺点:</h5><ul><li>由于故障或者缩放而创建的新的节点,新的节点不会缓存,直到数据库更新为止。缓存应用直写模式可以缓解这个问题。</li><li>写入的大多数数据可能永远都不会被读取,用 TTL 可以最小化这种情况的出现。</li></ul><h4 id="回写模式"><a href="#回写模式" class="headerlink" title="回写模式"></a>回写模式</h4><p align="center"> <img src="http://i.imgur.com/rgSrvjG.png" srcset="/img/loading.gif"> <br/> <strong><a href="http://www.slideshare.net/jboner/scalability-availability-stability-patterns/" target="_blank" rel="noopener">资料来源:可扩展性、可用性、稳定性、模式</a></strong></p><p>在回写模式中,应用执行以下操作:</p><ul><li>在缓存中增加或者更新条目</li><li>异步写入数据,提高写入性能。</li></ul><h5 id="回写模式的缺点:"><a href="#回写模式的缺点:" class="headerlink" title="回写模式的缺点:"></a>回写模式的缺点:</h5><ul><li>缓存可能在其内容成功存储之前丢失数据。</li><li>执行直写模式比缓存或者回写模式更复杂。</li></ul><h4 id="刷新"><a href="#刷新" class="headerlink" title="刷新"></a>刷新</h4><p align="center"> <img src="http://i.imgur.com/kxtjqgE.png" srcset="/img/loading.gif"> <br/> <strong><a href=http://www.slideshare.net/tmatyashovsky/from-cache-to-in-memory-data-grid-introduction-to-hazelcast>资料来源:从缓存到内存数据网格</a></strong></p><p>你可以将缓存配置成在到期之前自动刷新最近访问过的内容。</p><p>如果缓存可以准确预测将来可能请求哪些数据,那么刷新可能会导致延迟与读取时间的降低。</p><h5 id="刷新的缺点:"><a href="#刷新的缺点:" class="headerlink" title="刷新的缺点:"></a>刷新的缺点:</h5><ul><li>不能准确预测到未来需要用到的数据可能会导致性能不如不使用刷新。</li></ul><h3 id="缓存的缺点:-1"><a href="#缓存的缺点:-1" class="headerlink" title="缓存的缺点:"></a>缓存的缺点:</h3><ul><li>需要保持缓存和真实数据源之间的一致性,比如数据库根据<a href="https://en.wikipedia.org/wiki/Cache_algorithms" target="_blank" rel="noopener">缓存无效</a>。</li><li>需要改变应用程序比如增加 Redis 或者 memcached。</li><li>无效缓存是个难题,什么时候更新缓存是与之相关的复杂问题。</li></ul><h3 id="相关资源和延伸阅读-1"><a href="#相关资源和延伸阅读-1" class="headerlink" title="相关资源和延伸阅读"></a>相关资源和延伸阅读</h3><ul><li><a href="http://www.slideshare.net/tmatyashovsky/from-cache-to-in-memory-data-grid-introduction-to-hazelcast" target="_blank" rel="noopener">从缓存到内存数据</a></li><li><a href="http://horicky.blogspot.com/2010/10/scalable-system-design-patterns.html" target="_blank" rel="noopener">可扩展系统设计模式</a></li><li><a href="http://lethain.com/introduction-to-architecting-systems-for-scale/" target="_blank" rel="noopener">可缩放系统构架介绍</a></li><li><a href="http://www.slideshare.net/jboner/scalability-availability-stability-patterns/" target="_blank" rel="noopener">可扩展性,可用性,稳定性和模式</a></li><li><a href="http://www.lecloud.net/post/9246290032/scalability-for-dummies-part-3-cache" target="_blank" rel="noopener">可扩展性</a></li><li><a href="http://docs.aws.amazon.com/AmazonElastiCache/latest/UserGuide/Strategies.html" target="_blank" rel="noopener">AWS ElastiCache 策略</a></li><li><a href="https://en.wikipedia.org/wiki/Cache_(computing)" target="_blank" rel="noopener">维基百科</a></li></ul><h2 id="异步"><a href="#异步" class="headerlink" title="异步"></a>异步</h2><p align="center"> <img src="http://i.imgur.com/54GYsSx.png" srcset="/img/loading.gif"> <br/> <strong><a href=http://lethain.com/introduction-to-architecting-systems-for-scale/#platform_layer>资料来源:可缩放系统构架介绍</a></strong></p><p>异步工作流有助于减少那些原本顺序执行的请求时间。它们可以通过提前进行一些耗时的工作来帮助减少请求时间,比如定期汇总数据。</p><h3 id="消息队列"><a href="#消息队列" class="headerlink" title="消息队列"></a>消息队列</h3><p>消息队列接收,保留和传递消息。如果按顺序执行操作太慢的话,你可以使用有以下工作流的消息队列:</p><ul><li>应用程序将作业发布到队列,然后通知用户作业状态</li><li>一个 worker 从队列中取出该作业,对其进行处理,然后显示该作业完成</li></ul><p>不去阻塞用户操作,作业在后台处理。在此期间,客户端可能会进行一些处理使得看上去像是任务已经完成了。例如,如果要发送一条推文,推文可能会马上出现在你的时间线上,但是可能需要一些时间才能将你的推文推送到你的所有关注者那里去。</p><p><strong>Redis</strong> 是一个令人满意的简单的消息代理,但是消息有可能会丢失。</p><p><strong>RabbitMQ</strong> 很受欢迎但是要求你适应「AMQP」协议并且管理你自己的节点。</p><p><strong>Amazon SQS</strong> 是被托管的,但可能具有高延迟,并且消息可能会被传送两次。</p><h3 id="任务队列"><a href="#任务队列" class="headerlink" title="任务队列"></a>任务队列</h3><p>任务队列接收任务及其相关数据,运行它们,然后传递其结果。 它们可以支持调度,并可用于在后台运行计算密集型作业。</p><p><strong>Celery</strong> 支持调度,主要是用 Python 开发的。</p><h3 id="背压"><a href="#背压" class="headerlink" title="背压"></a>背压</h3><p>如果队列开始明显增长,那么队列大小可能会超过内存大小,导致高速缓存未命中,磁盘读取,甚至性能更慢。<a href="http://mechanical-sympathy.blogspot.com/2012/05/apply-back-pressure-when-overloaded.html" target="_blank" rel="noopener">背压</a>可以通过限制队列大小来帮助我们,从而为队列中的作业保持高吞吐率和良好的响应时间。一旦队列填满,客户端将得到服务器忙活着 HTTP 503 状态码,以便稍后重试。客户端可以在稍后时间重试该请求,也许是<a href="https://en.wikipedia.org/wiki/Exponential_backoff" target="_blank" rel="noopener">指数退避</a>。</p><h3 id="异步的缺点:"><a href="#异步的缺点:" class="headerlink" title="异步的缺点:"></a>异步的缺点:</h3><ul><li>简单的计算和实时工作流等用例可能更适用于同步操作,因为引入队列可能会增加延迟和复杂性。</li></ul><h3 id="相关资源和延伸阅读-2"><a href="#相关资源和延伸阅读-2" class="headerlink" title="相关资源和延伸阅读"></a>相关资源和延伸阅读</h3><ul><li><a href="https://www.youtube.com/watch?v=1KRYH75wgy4" target="_blank" rel="noopener">这是一个数字游戏</a></li><li><a href="http://mechanical-sympathy.blogspot.com/2012/05/apply-back-pressure-when-overloaded.html" target="_blank" rel="noopener">超载时应用背压</a></li><li><a href="https://en.wikipedia.org/wiki/Little%27s_law" target="_blank" rel="noopener">利特尔法则</a></li><li><a href="https://www.quora.com/What-is-the-difference-between-a-message-queue-and-a-task-queue-Why-would-a-task-queue-require-a-message-broker-like-RabbitMQ-Redis-Celery-or-IronMQ-to-function" target="_blank" rel="noopener">消息队列与任务队列有什么区别?</a></li></ul><h2 id="通讯"><a href="#通讯" class="headerlink" title="通讯"></a>通讯</h2><p align="center"> <img src="http://i.imgur.com/5KeocQs.jpg" srcset="/img/loading.gif"> <br/> <strong><a href=http://www.escotal.com/osilayer.html>资料来源:OSI 7层模型</a></strong></p><h3 id="超文本传输协议(HTTP)"><a href="#超文本传输协议(HTTP)" class="headerlink" title="超文本传输协议(HTTP)"></a>超文本传输协议(HTTP)</h3><p>HTTP 是一种在客户端和服务器之间编码和传输数据的方法。它是一个请求/响应协议:客户端和服务端针对相关内容和完成状态信息的请求和响应。HTTP 是独立的,允许请求和响应流经许多执行负载均衡,缓存,加密和压缩的中间路由器和服务器。</p><p>一个基本的 HTTP 请求由一个动词(方法)和一个资源(端点)组成。 以下是常见的 HTTP 动词:</p><table><thead><tr><th>动词</th><th>描述</th><th>*幂等</th><th>安全性</th><th>可缓存</th></tr></thead><tbody><tr><td>GET</td><td>读取资源</td><td>Yes</td><td>Yes</td><td>Yes</td></tr><tr><td>POST</td><td>创建资源或触发处理数据的进程</td><td>No</td><td>No</td><td>Yes,如果回应包含刷新信息</td></tr><tr><td>PUT</td><td>创建或替换资源</td><td>Yes</td><td>No</td><td>No</td></tr><tr><td>PATCH</td><td>部分更新资源</td><td>No</td><td>No</td><td>Yes,如果回应包含刷新信息</td></tr><tr><td>DELETE</td><td>删除资源</td><td>Yes</td><td>No</td><td>No</td></tr></tbody></table><p><strong>多次执行不会产生不同的结果</strong>。</p><p>HTTP 是依赖于较低级协议(如 <strong>TCP</strong> 和 <strong>UDP</strong>)的应用层协议。</p><h4 id="来源及延伸阅读:HTTP"><a href="#来源及延伸阅读:HTTP" class="headerlink" title="来源及延伸阅读:HTTP"></a>来源及延伸阅读:HTTP</h4><ul><li><a href="https://www.quora.com/What-is-the-difference-between-HTTP-protocol-and-TCP-protocol" target="_blank" rel="noopener">README</a> +</li><li><a href="https://www.nginx.com/resources/glossary/http/" target="_blank" rel="noopener">HTTP 是什么?</a></li><li><a href="https://www.quora.com/What-is-the-difference-between-HTTP-protocol-and-TCP-protocol" target="_blank" rel="noopener">HTTP 和 TCP 的区别</a></li><li><a href="https://laracasts.com/discuss/channels/general-discussion/whats-the-differences-between-put-and-patch?page=1" target="_blank" rel="noopener">PUT 和 PATCH的区别</a></li></ul><h3 id="传输控制协议(TCP)"><a href="#传输控制协议(TCP)" class="headerlink" title="传输控制协议(TCP)"></a>传输控制协议(TCP)</h3><p align="center"> <img src="http://i.imgur.com/JdAsdvG.jpg" srcset="/img/loading.gif"> <br/> <strong><a href="http://www.wildbunny.co.uk/blog/2012/10/09/how-to-make-a-multi-player-game-part-1/" target="_blank" rel="noopener">资料来源:如何制作多人游戏</a></strong></p><p>TCP 是通过 <a href="https://en.wikipedia.org/wiki/Internet_Protocol" target="_blank" rel="noopener">IP 网络</a>的面向连接的协议。 使用<a href="https://en.wikipedia.org/wiki/Handshaking" target="_blank" rel="noopener">握手</a>建立和断开连接。 发送的所有数据包保证以原始顺序到达目的地,用以下措施保证数据包不被损坏:</p><ul><li>每个数据包的序列号和<a href="https://en.wikipedia.org/wiki/Transmission_Control_Protocol#Checksum_computation" target="_blank" rel="noopener">校验码</a>。</li><li><a href="https://en.wikipedia.org/wiki/Acknowledgement_(data_networks)" target="_blank" rel="noopener">确认包</a>和自动重传</li></ul><p>如果发送者没有收到正确的响应,它将重新发送数据包。如果多次超时,连接就会断开。TCP 实行<a href="https://en.wikipedia.org/wiki/Flow_control_(data)" target="_blank" rel="noopener">流量控制</a>和<a href="https://en.wikipedia.org/wiki/Network_congestion#Congestion_control" target="_blank" rel="noopener">拥塞控制</a>。这些确保措施会导致延迟,而且通常导致传输效率比 UDP 低。</p><p>为了确保高吞吐量,Web 服务器可以保持大量的 TCP 连接,从而导致高内存使用。在 Web 服务器线程间拥有大量开放连接可能开销巨大,消耗资源过多,也就是说,一个 <a href="#memcached">memcached</a> 服务器。<a href="https://en.wikipedia.org/wiki/Connection_pool" target="_blank" rel="noopener">连接池</a> 可以帮助除了在适用的情况下切换到 UDP。</p><p>TCP 对于需要高可靠性但时间紧迫的应用程序很有用。比如包括 Web 服务器,数据库信息,SMTP,FTP 和 SSH。</p><p>以下情况使用 TCP 代替 UDP:</p><ul><li>你需要数据完好无损。</li><li>你想对网络吞吐量自动进行最佳评估。</li></ul><h3 id="用户数据报协议(UDP)"><a href="#用户数据报协议(UDP)" class="headerlink" title="用户数据报协议(UDP)"></a>用户数据报协议(UDP)</h3><p align="center"> <img src="http://i.imgur.com/yzDrJtA.jpg" srcset="/img/loading.gif"> <br/> <strong><a href="http://www.wildbunny.co.uk/blog/2012/10/09/how-to-make-a-multi-player-game-part-1" target="_blank" rel="noopener">资料来源:如何制作多人游戏</a></strong></p><p>UDP 是无连接的。数据报(类似于数据包)只在数据报级别有保证。数据报可能会无序的到达目的地,也有可能会遗失。UDP 不支持拥塞控制。虽然不如 TCP 那样有保证,但 UDP 通常效率更高。</p><p>UDP 可以通过广播将数据报发送至子网内的所有设备。这对 <a href="https://en.wikipedia.org/wiki/Dynamic_Host_Configuration_Protocol" target="_blank" rel="noopener">DHCP</a> 很有用,因为子网内的设备还没有分配 IP 地址,而 IP 对于 TCP 是必须的。</p><p>UDP 可靠性更低但适合用在网络电话、视频聊天,流媒体和实时多人游戏上。</p><p>以下情况使用 UDP 代替 TCP:</p><ul><li>你需要低延迟</li><li>相对于数据丢失更糟的是数据延迟</li><li>你想实现自己的错误校正方法</li></ul><h4 id="来源及延伸阅读:TCP-与-UDP"><a href="#来源及延伸阅读:TCP-与-UDP" class="headerlink" title="来源及延伸阅读:TCP 与 UDP"></a>来源及延伸阅读:TCP 与 UDP</h4><ul><li><a href="http://gafferongames.com/networking-for-game-programmers/udp-vs-tcp/" target="_blank" rel="noopener">游戏编程的网络</a></li><li><a href="http://www.cyberciti.biz/faq/key-differences-between-tcp-and-udp-protocols/" target="_blank" rel="noopener">TCP 与 UDP 的关键区别</a></li><li><a href="http://stackoverflow.com/questions/5970383/difference-between-tcp-and-udp" target="_blank" rel="noopener">TCP 与 UDP 的不同</a></li><li><a href="https://en.wikipedia.org/wiki/Transmission_Control_Protocol" target="_blank" rel="noopener">传输控制协议</a></li><li><a href="https://en.wikipedia.org/wiki/User_Datagram_Protocol" target="_blank" rel="noopener">用户数据报协议</a></li><li><a href="http://www.cs.bu.edu/~jappavoo/jappavoo.github.com/451/papers/memcache-fb.pdf" target="_blank" rel="noopener">Memcache 在 Facebook 的扩展</a></li></ul><h3 id="远程过程调用协议(RPC)"><a href="#远程过程调用协议(RPC)" class="headerlink" title="远程过程调用协议(RPC)"></a>远程过程调用协议(RPC)</h3><p align="center"> <img src="http://i.imgur.com/iF4Mkb5.png" srcset="/img/loading.gif"> <br/> <strong><a href="http://www.puncsky.com/blog/2016/02/14/crack-the-system-design-interview" target="_blank" rel="noopener">Source: Crack the system design interview</a></strong></p><p>在 RPC 中,客户端会去调用另一个地址空间(通常是一个远程服务器)里的方法。调用代码看起来就像是调用的是一个本地方法,客户端和服务器交互的具体过程被抽象。远程调用相对于本地调用一般较慢而且可靠性更差,因此区分两者是有帮助的。热门的 RPC 框架包括 <a href="https://developers.google.com/protocol-buffers/" target="_blank" rel="noopener">Protobuf</a>、<a href="https://thrift.apache.org/" target="_blank" rel="noopener">Thrift</a> 和 <a href="https://avro.apache.org/docs/current/" target="_blank" rel="noopener">Avro</a>。</p><p>RPC 是一个“请求-响应”协议:</p><ul><li><strong>客户端程序</strong> ── 调用客户端存根程序。就像调用本地方法一样,参数会被压入栈中。</li><li><strong>客户端 stub 程序</strong> ── 将请求过程的 id 和参数打包进请求信息中。</li><li><strong>客户端通信模块</strong> ── 将信息从客户端发送至服务端。</li><li><strong>服务端通信模块</strong> ── 将接受的包传给服务端存根程序。</li><li><strong>服务端 stub 程序</strong> ── 将结果解包,依据过程 id 调用服务端方法并将参数传递过去。</li></ul><p>RPC 调用示例:</p><pre><code class="hljs plain">GET /someoperation?data=anIdPOST /anotheroperation{ "data":"anId"; "anotherdata": "another value"}</code></pre><p>RPC 专注于暴露方法。RPC 通常用于处理内部通讯的性能问题,这样你可以手动处理本地调用以更好的适应你的情况。</p><p>当以下情况时选择本地库(也就是 SDK):</p><ul><li>你知道你的目标平台。</li><li>你想控制如何访问你的“逻辑”。</li><li>你想对发生在你的库中的错误进行控制。</li><li>性能和终端用户体验是你最关心的事。</li></ul><p>遵循 <strong>REST</strong> 的 HTTP API 往往更适用于公共 API。</p><h4 id="缺点:RPC"><a href="#缺点:RPC" class="headerlink" title="缺点:RPC"></a>缺点:RPC</h4><ul><li>RPC 客户端与服务实现捆绑地很紧密。</li><li>一个新的 API 必须在每一个操作或者用例中定义。</li><li>RPC 很难调试。</li><li>你可能没办法很方便的去修改现有的技术。举个例子,如果你希望在 <a href="http://www.squid-cache.org/" target="_blank" rel="noopener">Squid</a> 这样的缓存服务器上确保 <a href="http://etherealbits.com/2012/12/debunking-the-myths-of-rpc-rest/" target="_blank" rel="noopener">RPC 被正确缓存</a>的话可能需要一些额外的努力了。</li></ul><h3 id="表述性状态转移(REST)"><a href="#表述性状态转移(REST)" class="headerlink" title="表述性状态转移(REST)"></a>表述性状态转移(REST)</h3><p>REST 是一种强制的客户端/服务端架构设计模型,客户端基于服务端管理的一系列资源操作。服务端提供修改或获取资源的接口。所有的通信必须是无状态和可缓存的。</p><p>RESTful 接口有四条规则:</p><ul><li><strong>标志资源(HTTP 里的 URI)</strong> ── 无论什么操作都使用同一个 URI。</li><li><strong>表示的改变(HTTP 的动作)</strong> ── 使用动作, headers 和 body。</li><li><strong>可自我描述的错误信息(HTTP 中的 status code)</strong> ── 使用状态码,不要重新造轮子。</li><li><strong><a href="http://restcookbook.com/Basics/hateoas/" target="_blank" rel="noopener">HATEOAS</a>(HTTP 中的HTML 接口)</strong> ── 你的 web 服务器应该能够通过浏览器访问。</li></ul><p>REST 请求的例子:</p><pre><code class="hljs plain">GET /someresources/anIdPUT /someresources/anId{"anotherdata": "another value"}</code></pre><p>REST 关注于暴露数据。它减少了客户端/服务端的耦合程度,经常用于公共 HTTP API 接口设计。REST 使用更通常与规范化的方法来通过 URI 暴露资源,<a href="https://github.com/for-GET/know-your-http-well/blob/master/headers.md" target="_blank" rel="noopener">通过 header 来表述</a>并通过 GET、POST、PUT、DELETE 和 PATCH 这些动作来进行操作。因为无状态的特性,REST 易于横向扩展和隔离。</p><h4 id="缺点:REST"><a href="#缺点:REST" class="headerlink" title="缺点:REST"></a>缺点:REST</h4><ul><li>由于 REST 将重点放在暴露数据,所以当资源不是自然组织的或者结构复杂的时候它可能无法很好的适应。举个例子,返回过去一小时中与特定事件集匹配的更新记录这种操作就很难表示为路径。使用 REST,可能会使用 URI 路径,查询参数和可能的请求体来实现。</li><li>REST 一般依赖几个动作(GET、POST、PUT、DELETE 和 PATCH),但有时候仅仅这些没法满足你的需要。举个例子,将过期的文档移动到归档文件夹里去,这样的操作可能没法简单的用上面这几个 verbs 表达。</li><li>为了渲染单个页面,获取被嵌套在层级结构中的复杂资源需要客户端,服务器之间多次往返通信。例如,获取博客内容及其关联评论。对于使用不确定网络环境的移动应用来说,这些多次往返通信是非常麻烦的。</li><li>随着时间的推移,更多的字段可能会被添加到 API 响应中,较旧的客户端将会接收到所有新的数据字段,即使是那些它们不需要的字段,结果它会增加负载大小并引起更大的延迟。</li></ul><h3 id="RPC-与-REST-比较"><a href="#RPC-与-REST-比较" class="headerlink" title="RPC 与 REST 比较"></a>RPC 与 REST 比较</h3><table><thead><tr><th>操作</th><th>RPC</th><th>REST</th></tr></thead><tbody><tr><td>注册</td><td><strong>POST</strong> /signup</td><td><strong>POST</strong> /persons</td></tr><tr><td>注销</td><td><strong>POST</strong> /resign<br/>{<br/>“personid”: “1234”<br/>}</td><td><strong>DELETE</strong> /persons/1234</td></tr><tr><td>读取用户信息</td><td><strong>GET</strong> /readPerson?personid=1234</td><td><strong>GET</strong> /persons/1234</td></tr><tr><td>读取用户物品列表</td><td><strong>GET</strong> /readUsersItemsList?personid=1234</td><td><strong>GET</strong> /persons/1234/items</td></tr><tr><td>向用户物品列表添加一项</td><td><strong>POST</strong> /addItemToUsersItemsList<br/>{<br/>“personid”: “1234”;<br/>“itemid”: “456”<br/>}</td><td><strong>POST</strong> /persons/1234/items<br/>{<br/>“itemid”: “456”<br/>}</td></tr><tr><td>更新一个物品</td><td><strong>POST</strong> /modifyItem<br/>{<br/>“itemid”: “456”;<br/>“key”: “value”<br/>}</td><td><strong>PUT</strong> /items/456<br/>{<br/>“key”: “value”<br/>}</td></tr><tr><td>删除一个物品</td><td><strong>POST</strong> /removeItem<br/>{<br/>“itemid”: “456”<br/>}</td><td><strong>DELETE</strong> /items/456</td></tr></tbody></table><p align="center"> <strong><a href="https://apihandyman.io/do-you-really-know-why-you-prefer-rest-over-rpc" target="_blank" rel="noopener">资料来源:你真的知道你为什么更喜欢 REST 而不是 RPC 吗</a></strong></p><h4 id="来源及延伸阅读:REST-与-RPC"><a href="#来源及延伸阅读:REST-与-RPC" class="headerlink" title="来源及延伸阅读:REST 与 RPC"></a>来源及延伸阅读:REST 与 RPC</h4><ul><li><a href="https://apihandyman.io/do-you-really-know-why-you-prefer-rest-over-rpc/" target="_blank" rel="noopener">你真的知道你为什么更喜欢 REST 而不是 RPC 吗</a></li><li><a href="http://programmers.stackexchange.com/a/181186" target="_blank" rel="noopener">什么时候 RPC 比 REST 更合适?</a></li><li><a href="http://stackoverflow.com/questions/15056878/rest-vs-json-rpc" target="_blank" rel="noopener">REST vs JSON-RPC</a></li><li><a href="http://etherealbits.com/2012/12/debunking-the-myths-of-rpc-rest/" target="_blank" rel="noopener">揭开 RPC 和 REST 的神秘面纱</a></li><li><a href="https://www.quora.com/What-are-the-drawbacks-of-using-RESTful-APIs" target="_blank" rel="noopener">使用 REST 的缺点是什么</a></li><li><a href="http://www.puncsky.com/blog/2016/02/14/crack-the-system-design-interview/" target="_blank" rel="noopener">破解系统设计面试</a></li><li><a href="https://code.facebook.com/posts/1468950976659943/" target="_blank" rel="noopener">Thrift</a></li><li><a href="http://arstechnica.com/civis/viewtopic.php?t=1190508" target="_blank" rel="noopener">为什么在内部使用 REST 而不是 RPC</a></li></ul><h2 id="安全"><a href="#安全" class="headerlink" title="安全"></a>安全</h2><p>这一部分需要更多内容。<a href="#贡献">一起来吧</a>!</p><p>安全是一个宽泛的话题。除非你有相当的经验、安全方面背景或者正在申请的职位要求安全知识,你不需要了解安全基础知识以外的内容:</p><ul><li>在运输和等待过程中加密</li><li>对所有的用户输入和从用户那里发来的参数进行处理以防止 <a href="https://en.wikipedia.org/wiki/Cross-site_scripting" target="_blank" rel="noopener">XSS</a> 和 <a href="https://en.wikipedia.org/wiki/SQL_injection" target="_blank" rel="noopener">SQL 注入</a>。</li><li>使用参数化的查询来防止 SQL 注入。</li><li>使用<a href="https://en.wikipedia.org/wiki/Principle_of_least_privilege" target="_blank" rel="noopener">最小权限原则</a>。</li></ul><h3 id="来源及延伸阅读-12"><a href="#来源及延伸阅读-12" class="headerlink" title="来源及延伸阅读"></a>来源及延伸阅读</h3><ul><li><a href="https://github.com/FallibleInc/security-guide-for-developers" target="_blank" rel="noopener">为开发者准备的安全引导</a></li><li><a href="https://www.owasp.org/index.php/OWASP_Top_Ten_Cheat_Sheet" target="_blank" rel="noopener">OWASP top ten</a></li></ul><h2 id="附录"><a href="#附录" class="headerlink" title="附录"></a>附录</h2><p>一些时候你会被要求做出保守估计。比如,你可能需要估计从磁盘中生成 100 张图片的缩略图需要的时间或者一个数据结构需要多少的内存。<strong>2 的次方表</strong>和<strong>每个开发者都需要知道的一些时间数据</strong>(译注:OSChina 上有这篇文章的<a href="https://www.oschina.net/news/30009/every-programmer-should-know" target="_blank" rel="noopener">译文</a>)都是一些很方便的参考资料。</p><h3 id="2-的次方表"><a href="#2-的次方表" class="headerlink" title="2 的次方表"></a>2 的次方表</h3><pre><code class="hljs plain">Power Exact Value Approx Value Bytes---------------------------------------------------------------7 1288 25610 1024 1 thousand 1 KB16 65,536 64 KB20 1,048,576 1 million 1 MB30 1,073,741,824 1 billion 1 GB32 4,294,967,296 4 GB40 1,099,511,627,776 1 trillion 1 TB</code></pre><h4 id="来源及延伸阅读-13"><a href="#来源及延伸阅读-13" class="headerlink" title="来源及延伸阅读"></a>来源及延伸阅读</h4><ul><li><a href="https://en.wikipedia.org/wiki/Power_of_two" target="_blank" rel="noopener">2 的次方</a></li></ul><h3 id="每个程序员都应该知道的延迟数"><a href="#每个程序员都应该知道的延迟数" class="headerlink" title="每个程序员都应该知道的延迟数"></a>每个程序员都应该知道的延迟数</h3><pre><code class="hljs plain">Latency Comparison Numbers--------------------------L1 cache reference 0.5 nsBranch mispredict 5 nsL2 cache reference 7 ns 14x L1 cacheMutex lock/unlock 100 nsMain memory reference 100 ns 20x L2 cache, 200x L1 cacheCompress 1K bytes with Zippy 10,000 ns 10 usSend 1 KB bytes over 1 Gbps network 10,000 ns 10 usRead 4 KB randomly from SSD* 150,000 ns 150 us ~1GB/sec SSDRead 1 MB sequentially from memory 250,000 ns 250 usRound trip within same datacenter 500,000 ns 500 usRead 1 MB sequentially from SSD* 1,000,000 ns 1,000 us 1 ms ~1GB/sec SSD, 4X memoryDisk seek 10,000,000 ns 10,000 us 10 ms 20x datacenter roundtripRead 1 MB sequentially from 1 Gbps 10,000,000 ns 10,000 us 10 ms 40x memory, 10X SSDRead 1 MB sequentially from disk 30,000,000 ns 30,000 us 30 ms 120x memory, 30X SSDSend packet CA->Netherlands->CA 150,000,000 ns 150,000 us 150 msNotes-----1 ns = 10^-9 seconds1 us = 10^-6 seconds = 1,000 ns1 ms = 10^-3 seconds = 1,000 us = 1,000,000 ns</code></pre><p>基于上述数字的指标:</p><ul><li>从磁盘以 30 MB/s 的速度顺序读取</li><li>以 100 MB/s 从 1 Gbps 的以太网顺序读取</li><li>从 SSD 以 1 GB/s 的速度读取</li><li>以 4 GB/s 的速度从主存读取</li><li>每秒能绕地球 6-7 圈</li><li>数据中心内每秒有 2,000 次往返</li></ul><h4 id="延迟数可视化"><a href="#延迟数可视化" class="headerlink" title="延迟数可视化"></a>延迟数可视化</h4><p><img src="https://camo.githubusercontent.com/77f72259e1eb58596b564d1ad823af1853bc60a3/687474703a2f2f692e696d6775722e636f6d2f6b307431652e706e67" srcset="/img/loading.gif" alt=""></p><h4 id="来源及延伸阅读-14"><a href="#来源及延伸阅读-14" class="headerlink" title="来源及延伸阅读"></a>来源及延伸阅读</h4><ul><li><a href="https://gist.github.com/jboner/2841832" target="_blank" rel="noopener">每个程序员都应该知道的延迟数 — 1</a></li><li><a href="https://gist.github.com/hellerbarde/2843375" target="_blank" rel="noopener">每个程序员都应该知道的延迟数 — 2</a></li><li><a href="http://www.cs.cornell.edu/projects/ladis2009/talks/dean-keynote-ladis2009.pdf" target="_blank" rel="noopener">关于建设大型分布式系统的的设计方案、课程和建议</a></li><li><a href="https://static.googleusercontent.com/media/research.google.com/en//people/jeff/stanford-295-talk.pdf" target="_blank" rel="noopener">关于建设大型可拓展分布式系统的软件工程咨询</a></li></ul><h3 id="其它的系统设计面试题"><a href="#其它的系统设计面试题" class="headerlink" title="其它的系统设计面试题"></a>其它的系统设计面试题</h3><blockquote><p>常见的系统设计面试问题,给出了如何解决的方案链接</p></blockquote><table><thead><tr><th>问题</th><th>引用</th></tr></thead><tbody><tr><td>设计类似于 Dropbox 的文件同步服务</td><td><a href="https://www.youtube.com/watch?v=PE4gwstWhmc" target="_blank" rel="noopener">youtube.com</a></td></tr><tr><td>设计类似于 Google 的搜索引擎</td><td><a href="http://queue.acm.org/detail.cfm?id=988407" target="_blank" rel="noopener">queue.acm.org</a><br/><a href="http://programmers.stackexchange.com/questions/38324/interview-question-how-would-you-implement-google-search" target="_blank" rel="noopener">stackexchange.com</a><br/><a href="http://www.ardendertat.com/2012/01/11/implementing-search-engines/" target="_blank" rel="noopener">ardendertat.com</a><br><a href="http://infolab.stanford.edu/~backrub/google.html" target="_blank" rel="noopener">stanford.edu</a></td></tr><tr><td>设计类似于 Google 的可扩展网络爬虫</td><td><a href="https://www.quora.com/How-can-I-build-a-web-crawler-from-scratch" target="_blank" rel="noopener">quora.com</a></td></tr><tr><td>设计 Google 文档</td><td><a href="https://code.google.com/p/google-mobwrite/" target="_blank" rel="noopener">code.google.com</a><br/><a href="https://neil.fraser.name/writing/sync/" target="_blank" rel="noopener">neil.fraser.name</a></td></tr><tr><td>设计类似 Redis 的建值存储</td><td><a href="http://www.slideshare.net/dvirsky/introduction-to-redis" target="_blank" rel="noopener">slideshare.net</a></td></tr><tr><td>设计类似 Memcached 的缓存系统</td><td><a href="http://www.slideshare.net/oemebamo/introduction-to-memcached" target="_blank" rel="noopener">slideshare.net</a></td></tr><tr><td>设计类似亚马逊的推荐系统</td><td><a href="http://tech.hulu.com/blog/2011/09/19/recommendation-system.html" target="_blank" rel="noopener">hulu.com</a><br/><a href="http://ijcai13.org/files/tutorial_slides/td3.pdf" target="_blank" rel="noopener">ijcai13.org</a></td></tr><tr><td>设计类似 Bitly 的短链接系统</td><td><a href="http://n00tc0d3r.blogspot.com/" target="_blank" rel="noopener">n00tc0d3r.blogspot.com</a></td></tr><tr><td>设计类似 WhatsApp 的聊天应用</td><td><a href="http://highscalability.com/blog/2014/2/26/the-whatsapp-architecture-facebook-bought-for-19-billion.html" target="_blank" rel="noopener">highscalability.com</a></td></tr><tr><td>设计类似 Instagram 的图片分享系统</td><td><a href="http://highscalability.com/flickr-architecture" target="_blank" rel="noopener">highscalability.com</a><br/><a href="http://highscalability.com/blog/2011/12/6/instagram-architecture-14-million-users-terabytes-of-photos.html" target="_blank" rel="noopener">highscalability.com</a></td></tr><tr><td>设计 Facebook 的新闻推荐方法</td><td><a href="http://www.quora.com/What-are-best-practices-for-building-something-like-a-News-Feed" target="_blank" rel="noopener">quora.com</a><br/><a href="http://www.quora.com/Activity-Streams/What-are-the-scaling-issues-to-keep-in-mind-while-developing-a-social-network-feed" target="_blank" rel="noopener">quora.com</a><br/><a href="http://www.slideshare.net/danmckinley/etsy-activity-feeds-architecture" target="_blank" rel="noopener">slideshare.net</a></td></tr><tr><td>设计 Facebook 的时间线系统</td><td><a href="https://www.facebook.com/note.php?note_id=10150468255628920" target="_blank" rel="noopener">facebook.com</a><br/><a href="http://highscalability.com/blog/2012/1/23/facebook-timeline-brought-to-you-by-the-power-of-denormaliza.html" target="_blank" rel="noopener">highscalability.com</a></td></tr><tr><td>设计 Facebook 的聊天系统</td><td><a href="http://www.erlang-factory.com/upload/presentations/31/EugeneLetuchy-ErlangatFacebook.pdf" target="_blank" rel="noopener">erlang-factory.com</a><br/><a href="https://www.facebook.com/note.php?note_id=14218138919&id=9445547199&index=0" target="_blank" rel="noopener">facebook.com</a></td></tr><tr><td>设计类似 Facebook 的图表搜索系统</td><td><a href="https://www.facebook.com/notes/facebook-engineering/under-the-hood-building-out-the-infrastructure-for-graph-search/10151347573598920" target="_blank" rel="noopener">facebook.com</a><br/><a href="https://www.facebook.com/notes/facebook-engineering/under-the-hood-indexing-and-ranking-in-graph-search/10151361720763920" target="_blank" rel="noopener">facebook.com</a><br/><a href="https://www.facebook.com/notes/facebook-engineering/under-the-hood-the-natural-language-interface-of-graph-search/10151432733048920" target="_blank" rel="noopener">facebook.com</a></td></tr><tr><td>设计类似 CloudFlare 的内容传递网络</td><td><a href="http://repository.cmu.edu/cgi/viewcontent.cgi?article=2112&context=compsci" target="_blank" rel="noopener">cmu.edu</a></td></tr><tr><td>设计类似 Twitter 的热门话题系统</td><td><a href="http://www.michael-noll.com/blog/2013/01/18/implementing-real-time-trending-topics-in-storm/" target="_blank" rel="noopener">michael-noll.com</a><br/><a href="http://snikolov.wordpress.com/2012/11/14/early-detection-of-twitter-trends/" target="_blank" rel="noopener">snikolov .wordpress.com</a></td></tr><tr><td>设计一个随机 ID 生成系统</td><td><a href="https://blog.twitter.com/2010/announcing-snowflake" target="_blank" rel="noopener">blog.twitter.com</a><br/><a href="https://github.com/twitter/snowflake/" target="_blank" rel="noopener">github.com</a></td></tr><tr><td>返回一定时间段内次数前 k 高的请求</td><td><a href="https://icmi.cs.ucsb.edu/research/tech_reports/reports/2005-23.pdf" target="_blank" rel="noopener">ucsb.edu</a><br/><a href="http://davis.wpi.edu/xmdv/docs/EDBT11-diyang.pdf" target="_blank" rel="noopener">wpi.edu</a></td></tr><tr><td>设计一个数据源于多个数据中心的服务系统</td><td><a href="http://highscalability.com/blog/2009/8/24/how-google-serves-data-from-multiple-datacenters.html" target="_blank" rel="noopener">highscalability.com</a></td></tr><tr><td>设计一个多人网络卡牌游戏</td><td><a href="http://www.indieflashblog.com/how-to-create-an-asynchronous-multiplayer-game.html" target="_blank" rel="noopener">indieflashblog.com</a><br/><a href="http://buildnewgames.com/real-time-multiplayer/" target="_blank" rel="noopener">buildnewgames.com</a></td></tr><tr><td>设计一个垃圾回收系统</td><td><a href="http://journal.stuffwithstuff.com/2013/12/08/babys-first-garbage-collector/" target="_blank" rel="noopener">stuffwithstuff.com</a><br/><a href="http://courses.cs.washington.edu/courses/csep521/07wi/prj/rick.pdf" target="_blank" rel="noopener">washington.edu</a></td></tr><tr><td>添加更多的系统设计问题</td><td><a href="#贡献">贡献</a></td></tr></tbody></table><h3 id="真实架构"><a href="#真实架构" class="headerlink" title="真实架构"></a>真实架构</h3><blockquote><p>关于现实中真实的系统是怎么设计的文章。</p></blockquote><p align="center"> <img src="http://i.imgur.com/TcUo2fw.png" srcset="/img/loading.gif"> <br/> <strong><a href="https://www.infoq.com/presentations/Twitter-Timeline-Scalability" target="_blank" rel="noopener">Source: Twitter timelines at scale</a></strong></p><p><strong>不要专注于以下文章的细节,专注于以下方面:</strong></p><ul><li>发现这些文章中的共同的原则、技术和模式。</li><li>学习每个组件解决哪些问题,什么情况下使用,什么情况下不适用</li><li>复习学过的文章</li></ul><table><thead><tr><th>类型</th><th>系统</th><th>引用</th></tr></thead><tbody><tr><td>Data processing</td><td><strong>MapReduce</strong> - Google的分布式数据处理</td><td><a href="http://static.googleusercontent.com/media/research.google.com/zh-CN/us/archive/mapreduce-osdi04.pdf" target="_blank" rel="noopener">research.google.com</a></td></tr><tr><td>Data processing</td><td><strong>Spark</strong> - Databricks 的分布式数据处理</td><td><a href="http://www.slideshare.net/AGrishchenko/apache-spark-architecture" target="_blank" rel="noopener">slideshare.net</a></td></tr><tr><td>Data processing</td><td><strong>Storm</strong> - Twitter 的分布式数据处理</td><td><a href="http://www.slideshare.net/previa/storm-16094009" target="_blank" rel="noopener">slideshare.net</a></td></tr><tr><td></td><td></td><td></td></tr><tr><td>Data store</td><td><strong>Bigtable</strong> - Google 的列式数据库</td><td><a href="http://www.read.seas.harvard.edu/~kohler/class/cs239-w08/chang06bigtable.pdf" target="_blank" rel="noopener">harvard.edu</a></td></tr><tr><td>Data store</td><td><strong>HBase</strong> - Bigtable 的开源实现</td><td><a href="http://www.slideshare.net/alexbaranau/intro-to-hbase" target="_blank" rel="noopener">slideshare.net</a></td></tr><tr><td>Data store</td><td><strong>Cassandra</strong> - Facebook 的列式数据库</td><td><a href="http://www.slideshare.net/planetcassandra/cassandra-introduction-features-30103666" target="_blank" rel="noopener">slideshare.net</a></td></tr><tr><td>Data store</td><td><strong>DynamoDB</strong> - Amazon 的文档数据库</td><td><a href="http://www.read.seas.harvard.edu/~kohler/class/cs239-w08/decandia07dynamo.pdf" target="_blank" rel="noopener">harvard.edu</a></td></tr><tr><td>Data store</td><td><strong>MongoDB</strong> - 文档数据库</td><td><a href="http://www.slideshare.net/mdirolf/introduction-to-mongodb" target="_blank" rel="noopener">slideshare.net</a></td></tr><tr><td>Data store</td><td><strong>Spanner</strong> - Google 的全球分布数据库</td><td><a href="http://research.google.com/archive/spanner-osdi2012.pdf" target="_blank" rel="noopener">research.google.com</a></td></tr><tr><td>Data store</td><td><strong>Memcached</strong> - 分布式内存缓存系统</td><td><a href="http://www.slideshare.net/oemebamo/introduction-to-memcached" target="_blank" rel="noopener">slideshare.net</a></td></tr><tr><td>Data store</td><td><strong>Redis</strong> - 能够持久化及具有值类型的分布式内存缓存系统</td><td><a href="http://www.slideshare.net/dvirsky/introduction-to-redis" target="_blank" rel="noopener">slideshare.net</a></td></tr><tr><td></td><td></td><td></td></tr><tr><td>File system</td><td><strong>Google File System (GFS)</strong> - 分布式文件系统</td><td><a href="http://static.googleusercontent.com/media/research.google.com/zh-CN/us/archive/gfs-sosp2003.pdf" target="_blank" rel="noopener">research.google.com</a></td></tr><tr><td>File system</td><td><strong>Hadoop File System (HDFS)</strong> - GFS 的开源实现</td><td><a href="https://hadoop.apache.org/docs/r1.2.1/hdfs_design.html" target="_blank" rel="noopener">apache.org</a></td></tr><tr><td></td><td></td><td></td></tr><tr><td>Misc</td><td><strong>Chubby</strong> - Google 的分布式系统的低耦合锁服务</td><td><a href="http://static.googleusercontent.com/external_content/untrusted_dlcp/research.google.com/en/us/archive/chubby-osdi06.pdf" target="_blank" rel="noopener">research.google.com</a></td></tr><tr><td>Misc</td><td><strong>Dapper</strong> - 分布式系统跟踪基础设施</td><td><a href="http://static.googleusercontent.com/media/research.google.com/en//pubs/archive/36356.pdf" target="_blank" rel="noopener">research.google.com</a></td></tr><tr><td>Misc</td><td><strong>Kafka</strong> - LinkedIn 的发布订阅消息系统</td><td><a href="http://www.slideshare.net/mumrah/kafka-talk-tri-hug" target="_blank" rel="noopener">slideshare.net</a></td></tr><tr><td>Misc</td><td><strong>Zookeeper</strong> - 集中的基础架构和协调服务</td><td><a href="http://www.slideshare.net/sauravhaloi/introduction-to-apache-zookeeper" target="_blank" rel="noopener">slideshare.net</a></td></tr><tr><td></td><td>添加更多</td><td><a href="#贡献">贡献</a></td></tr></tbody></table><h3 id="公司的系统架构"><a href="#公司的系统架构" class="headerlink" title="公司的系统架构"></a>公司的系统架构</h3><table><thead><tr><th>Company</th><th>Reference(s)</th></tr></thead><tbody><tr><td>Amazon</td><td><a href="http://highscalability.com/amazon-architecture" target="_blank" rel="noopener">Amazon 的架构</a></td></tr><tr><td>Cinchcast</td><td><a href="http://highscalability.com/blog/2012/7/16/cinchcast-architecture-producing-1500-hours-of-audio-every-d.html" target="_blank" rel="noopener">每天产生 1500 小时的音频</a></td></tr><tr><td>DataSift</td><td><a href="http://highscalability.com/blog/2011/11/29/datasift-architecture-realtime-datamining-at-120000-tweets-p.html" target="_blank" rel="noopener">每秒实时挖掘 120000 条 tweet</a></td></tr><tr><td>DropBox</td><td><a href="https://www.youtube.com/watch?v=PE4gwstWhmc" target="_blank" rel="noopener">我们如何缩放 Dropbox</a></td></tr><tr><td>ESPN</td><td><a href="http://highscalability.com/blog/2013/11/4/espns-architecture-at-scale-operating-at-100000-duh-nuh-nuhs.html" target="_blank" rel="noopener">每秒操作 100000 次</a></td></tr><tr><td>Google</td><td><a href="http://highscalability.com/google-architecture" target="_blank" rel="noopener">Google 的架构</a></td></tr><tr><td>Instagram</td><td><a href="http://highscalability.com/blog/2011/12/6/instagram-architecture-14-million-users-terabytes-of-photos.html" target="_blank" rel="noopener">1400 万用户,达到兆级别的照片存储</a><br/><a href="http://instagram-engineering.tumblr.com/post/13649370142/what-powers-instagram-hundreds-of-instances" target="_blank" rel="noopener">是什么在驱动 Instagram</a></td></tr><tr><td>Justin.tv</td><td><a href="http://highscalability.com/blog/2010/3/16/justintvs-live-video-broadcasting-architecture.html" target="_blank" rel="noopener">Justin.Tv 的直播广播架构</a></td></tr><tr><td>Facebook</td><td><a href="https://cs.uwaterloo.ca/~brecht/courses/854-Emerging-2014/readings/key-value/fb-memcached-nsdi-2013.pdf" target="_blank" rel="noopener">Facebook 的可扩展 memcached</a><br/><a href="https://cs.uwaterloo.ca/~brecht/courses/854-Emerging-2014/readings/data-store/tao-facebook-distributed-datastore-atc-2013.pdf" target="_blank" rel="noopener">TAO: Facebook 社交图的分布式数据存储</a><br/><a href="https://www.usenix.org/legacy/event/osdi10/tech/full_papers/Beaver.pdf" target="_blank" rel="noopener">Facebook 的图片存储</a></td></tr><tr><td>Flickr</td><td><a href="http://highscalability.com/flickr-architecture" target="_blank" rel="noopener">Flickr 的架构</a></td></tr><tr><td>Mailbox</td><td><a href="http://highscalability.com/blog/2013/6/18/scaling-mailbox-from-0-to-one-million-users-in-6-weeks-and-1.html" target="_blank" rel="noopener">在 6 周内从 0 到 100 万用户</a></td></tr><tr><td>Pinterest</td><td><a href="http://highscalability.com/blog/2013/4/15/scaling-pinterest-from-0-to-10s-of-billions-of-page-views-a.html" target="_blank" rel="noopener">从零到每月数十亿的浏览量</a><br/><a href="http://highscalability.com/blog/2012/5/21/pinterest-architecture-update-18-million-visitors-10x-growth.html" target="_blank" rel="noopener">1800 万访问用户,10 倍增长,12 名员工</a></td></tr><tr><td>Playfish</td><td><a href="http://highscalability.com/blog/2010/9/21/playfishs-social-gaming-architecture-50-million-monthly-user.html" target="_blank" rel="noopener">月用户量 5000 万并在不断增长</a></td></tr><tr><td>PlentyOfFish</td><td><a href="http://highscalability.com/plentyoffish-architecture" target="_blank" rel="noopener">PlentyOfFish 的架构</a></td></tr><tr><td>Salesforce</td><td><a href="http://highscalability.com/blog/2013/9/23/salesforce-architecture-how-they-handle-13-billion-transacti.html" target="_blank" rel="noopener">他们每天如何处理 13 亿笔交易</a></td></tr><tr><td>Stack Overflow</td><td><a href="http://highscalability.com/blog/2009/8/5/stack-overflow-architecture.html" target="_blank" rel="noopener">Stack Overflow 的架构</a></td></tr><tr><td>TripAdvisor</td><td><a href="http://highscalability.com/blog/2011/6/27/tripadvisor-architecture-40m-visitors-200m-dynamic-page-view.html" target="_blank" rel="noopener">40M 访问者,200M 页面浏览量,30TB 数据</a></td></tr><tr><td>Tumblr</td><td><a href="http://highscalability.com/blog/2012/2/13/tumblr-architecture-15-billion-page-views-a-month-and-harder.html" target="_blank" rel="noopener">每月 150 亿的浏览量</a></td></tr><tr><td>Twitter</td><td><a href="http://highscalability.com/scaling-twitter-making-twitter-10000-percent-faster" target="_blank" rel="noopener">Making Twitter 10000 percent faster</a><br/><a href="http://highscalability.com/blog/2011/12/19/how-twitter-stores-250-million-tweets-a-day-using-mysql.html" target="_blank" rel="noopener">每天使用 MySQL 存储2.5亿条 tweet</a><br/><a href="http://highscalability.com/blog/2013/7/8/the-architecture-twitter-uses-to-deal-with-150m-active-users.html" target="_blank" rel="noopener">150M 活跃用户,300K QPS,22 MB/S 的防火墙</a><br/><a href="https://www.infoq.com/presentations/Twitter-Timeline-Scalability" target="_blank" rel="noopener">可扩展时间表</a><br/><a href="https://www.youtube.com/watch?v=5cKTP36HVgI" target="_blank" rel="noopener">Twitter 的大小数据</a><br/><a href="https://www.youtube.com/watch?v=z8LU0Cj6BOU" target="_blank" rel="noopener">Twitter 的行为:规模超过 1 亿用户</a></td></tr><tr><td>Uber</td><td><a href="http://highscalability.com/blog/2015/9/14/how-uber-scales-their-real-time-market-platform.html" target="_blank" rel="noopener">Uber 如何扩展自己的实时化市场</a></td></tr><tr><td>WhatsApp</td><td><a href="http://highscalability.com/blog/2014/2/26/the-whatsapp-architecture-facebook-bought-for-19-billion.html" target="_blank" rel="noopener">Facebook 用 190 亿美元购买 WhatsApp 的架构</a></td></tr><tr><td>YouTube</td><td><a href="https://www.youtube.com/watch?v=w5WVu624fY8" target="_blank" rel="noopener">YouTube 的可扩展性</a><br/><a href="http://highscalability.com/youtube-architecture" target="_blank" rel="noopener">YouTube 的架构</a></td></tr></tbody></table><h3 id="公司工程博客"><a href="#公司工程博客" class="headerlink" title="公司工程博客"></a>公司工程博客</h3><blockquote><p>你即将面试的公司的架构</p><p>你面对的问题可能就来自于同样领域</p></blockquote><ul><li><a href="http://nerds.airbnb.com/" target="_blank" rel="noopener">Airbnb Engineering</a></li><li><a href="https://developer.atlassian.com/blog/" target="_blank" rel="noopener">Atlassian Developers</a></li><li><a href="http://cloudengineering.autodesk.com/blog/" target="_blank" rel="noopener">Autodesk Engineering</a></li><li><a href="https://aws.amazon.com/blogs/aws/" target="_blank" rel="noopener">AWS Blog</a></li><li><a href="http://word.bitly.com/" target="_blank" rel="noopener">Bitly Engineering Blog</a></li><li><a href="https://www.box.com/blog/engineering/" target="_blank" rel="noopener">Box Blogs</a></li><li><a href="http://blog.cloudera.com/blog/" target="_blank" rel="noopener">Cloudera Developer Blog</a></li><li><a href="https://tech.dropbox.com/" target="_blank" rel="noopener">Dropbox Tech Blog</a></li><li><a href="http://engineering.quora.com/" target="_blank" rel="noopener">Engineering at Quora</a></li><li><a href="http://www.ebaytechblog.com/" target="_blank" rel="noopener">Ebay Tech Blog</a></li><li><a href="https://blog.evernote.com/tech/" target="_blank" rel="noopener">Evernote Tech Blog</a></li><li><a href="http://codeascraft.com/" target="_blank" rel="noopener">Etsy Code as Craft</a></li><li><a href="https://www.facebook.com/Engineering" target="_blank" rel="noopener">Facebook Engineering</a></li><li><a href="http://code.flickr.net/" target="_blank" rel="noopener">Flickr Code</a></li><li><a href="http://engineering.foursquare.com/" target="_blank" rel="noopener">Foursquare Engineering Blog</a></li><li><a href="http://githubengineering.com/" target="_blank" rel="noopener">GitHub Engineering Blog</a></li><li><a href="http://googleresearch.blogspot.com/" target="_blank" rel="noopener">Google Research Blog</a></li><li><a href="https://engineering.groupon.com/" target="_blank" rel="noopener">Groupon Engineering Blog</a></li><li><a href="https://engineering.heroku.com/" target="_blank" rel="noopener">Heroku Engineering Blog</a></li><li><a href="http://product.hubspot.com/blog/topic/engineering" target="_blank" rel="noopener">Hubspot Engineering Blog</a></li><li><a href="http://highscalability.com/" target="_blank" rel="noopener">High Scalability</a></li><li><a href="http://instagram-engineering.tumblr.com/" target="_blank" rel="noopener">Instagram Engineering</a></li><li><a href="https://software.intel.com/en-us/blogs/" target="_blank" rel="noopener">Intel Software Blog</a></li><li><a href="https://blogs.janestreet.com/category/ocaml/" target="_blank" rel="noopener">Jane Street Tech Blog</a></li><li><a href="http://engineering.linkedin.com/blog" target="_blank" rel="noopener">LinkedIn Engineering</a></li><li><a href="https://engineering.microsoft.com/" target="_blank" rel="noopener">Microsoft Engineering</a></li><li><a href="https://blogs.msdn.microsoft.com/pythonengineering/" target="_blank" rel="noopener">Microsoft Python Engineering</a></li><li><a href="http://techblog.netflix.com/" target="_blank" rel="noopener">Netflix Tech Blog</a></li><li><a href="https://devblog.paypal.com/category/engineering/" target="_blank" rel="noopener">Paypal Developer Blog</a></li><li><a href="http://engineering.pinterest.com/" target="_blank" rel="noopener">Pinterest Engineering Blog</a></li><li><a href="https://engineering.quora.com/" target="_blank" rel="noopener">Quora Engineering</a></li><li><a href="http://www.redditblog.com/" target="_blank" rel="noopener">Reddit Blog</a></li><li><a href="https://developer.salesforce.com/blogs/engineering/" target="_blank" rel="noopener">Salesforce Engineering Blog</a></li><li><a href="https://slack.engineering/" target="_blank" rel="noopener">Slack Engineering Blog</a></li><li><a href="https://labs.spotify.com/" target="_blank" rel="noopener">Spotify Labs</a></li><li><a href="http://www.twilio.com/engineering" target="_blank" rel="noopener">Twilio Engineering Blog</a></li><li><a href="https://engineering.twitter.com/" target="_blank" rel="noopener">Twitter Engineering</a></li><li><a href="http://eng.uber.com/" target="_blank" rel="noopener">Uber Engineering Blog</a></li><li><a href="http://yahooeng.tumblr.com/" target="_blank" rel="noopener">Yahoo Engineering Blog</a></li><li><a href="http://engineeringblog.yelp.com/" target="_blank" rel="noopener">Yelp Engineering Blog</a></li><li><a href="https://www.zynga.com/blogs/engineering" target="_blank" rel="noopener">Zynga Engineering Blog</a></li></ul><h4 id="来源及延伸阅读-15"><a href="#来源及延伸阅读-15" class="headerlink" title="来源及延伸阅读"></a>来源及延伸阅读</h4><ul><li><a href="https://github.com/kilimchoi/engineering-blogs" target="_blank" rel="noopener">kilimchoi/engineering-blogs</a></li></ul><h2 id="正在完善中"><a href="#正在完善中" class="headerlink" title="正在完善中"></a>正在完善中</h2><p>有兴趣加入添加一些部分或者帮助完善某些部分吗?<a href="#贡献">加入进来吧</a>!</p><ul><li>使用 MapReduce 进行分布式计算</li><li>一致性哈希</li><li>直接存储器访问(DMA)控制器</li><li><a href="#贡献">贡献</a></li></ul><h2 id="致谢"><a href="#致谢" class="headerlink" title="致谢"></a>致谢</h2><p>整个仓库都提供了证书和源</p><p>特别鸣谢:</p><ul><li><a href="http://www.hiredintech.com/system-design/the-system-design-process/" target="_blank" rel="noopener">Hired in tech</a></li><li><a href="https://www.amazon.com/dp/0984782850/" target="_blank" rel="noopener">Cracking the coding interview</a></li><li><a href="http://highscalability.com/" target="_blank" rel="noopener">High scalability</a></li><li><a href="https://github.com/checkcheckzz/system-design-interview" target="_blank" rel="noopener">checkcheckzz/system-design-interview</a></li><li><a href="https://github.com/shashank88/system_design" target="_blank" rel="noopener">shashank88/system_design</a></li><li><a href="https://github.com/mmcgrana/services-engineering" target="_blank" rel="noopener">mmcgrana/services-engineering</a></li><li><a href="https://gist.github.com/vasanthk/485d1c25737e8e72759f" target="_blank" rel="noopener">System design cheat sheet</a></li><li><a href="http://dancres.github.io/Pages/" target="_blank" rel="noopener">A distributed systems reading list</a></li><li><a href="http://www.puncsky.com/blog/2016/02/14/crack-the-system-design-interview/" target="_blank" rel="noopener">Cracking the system design interview</a></li></ul><h2 id="联系方式"><a href="#联系方式" class="headerlink" title="联系方式"></a>联系方式</h2><p>欢迎联系我讨论本文的不足、问题或者意见。</p><p>可以在我的 <a href="https://github.com/donnemartin" target="_blank" rel="noopener">GitHub 主页</a>上找到我的联系方式</p><h2 id="许可"><a href="#许可" class="headerlink" title="许可"></a>许可</h2><pre><code>Creative Commons Attribution 4.0 International License (CC BY 4.0)http://creativecommons.org/licenses/by/4.0/</code></pre>]]></content>
<tags>
<tag>design</tag>
</tags>
</entry>
<entry>
<title>在 OpenSSL 中使用 TLSv1.3</title>
<link href="/posts/use-tls13/"/>
<url>/posts/use-tls13/</url>
<content type="html"><![CDATA[<p>即将到来的OpenSSL 1.1.1版本将支持TLSv1.3。这个新版本将兼容OpenSSL 1.1.0版本的二进制文件和API。理论上,如果你的应用程序支持OpenSSL 1.1.0,那么当更新可用时,TLSv1.3版本也将自动得到支持,你不需要专门进行安装。但有一些问题仍需要应用程序开发人员和部署人员了解。在这篇博客中,我将谈谈其中的一些问题。</p><hr><h2 id="与TLS1-2及更早版本的对比"><a href="#与TLS1-2及更早版本的对比" class="headerlink" title="与TLS1.2及更早版本的对比"></a>与TLS1.2及更早版本的对比</h2><p>TLSv1.3版本是对规范的重大修改。它到底应该叫TLSv2.0还是现在的名字TLSv1.3,还存在一些争论。该版本有重大变化,一些工作方式也非常不同。下面是你可能需要注意的一些问题,简明扼要,不过并不太全面。</p><ul><li>有一些新的密码套件仅在TLSv1.3下工作。一些旧的密码套件无法用于TLSv1.3连接。</li><li>新的密码套件定义方式不同,且并未详细规定证书类型(如RSA、DSA、ECDSA)或密钥交换机制(如DHE或ECHDE)。这对密码套件的配置有暗示作用。</li><li>客户端在客户问候消息(ClientHello)中提供一个“key_share”。这会对“组”配置产生影响。</li><li>直到主握手完成以后,会话才会建立。在握手结束和会话建立之间可能会有一个间隙(理论上,会话可能根本不会建立),并可能对会话恢复代码产生影响。</li><li>在TLSv1.3版本中,重新磋商是不可能的。</li><li>现在大部分握手都会被加密。</li><li>更多类型的消息现在可以有扩展(这对定制扩展API和证书透明系统有影响)。</li><li>在TLSv1.3连接中不再允许使用DSA证书。</li></ul><p>注意,在这一阶段,只支持TLSv1.3。因DTLSv1.3版本的规范刚刚开始制定,目前并不支持OpenSSL。</p><hr><h2 id="TLSv1-3标准目前的状态"><a href="#TLSv1-3标准目前的状态" class="headerlink" title="TLSv1.3标准目前的状态"></a>TLSv1.3标准目前的状态</h2><p>到我写这篇文章时,TLSv1.3仍是一个草案。TLS工作团队定期会发布该标准的新版草案。实际中,需要对草案进行部署来识别他们使用的具体版本,基于不同草案版本的实现之间无法交互。</p><p>OpenSSL 1.1.1至少不会在TLSv1.3发布之前完成。同时,OpenSSL的git主分支包含了我们的TLSv1.3开发代码,可以用于测试(即不用于生产)。你可以在任意OpenSSL版本中通过头文件tls1.h中的宏TLS1_3_VERSION_DRAFT_TXT的值来查看部署的TLSv1.3草案的版本。在该标准的最终版发布后,这个宏会被删除。</p><p>TLSv1.3在最新的开发分支上默认是启用的(不需要单独开启)。如果需要禁用,你必须要在“config”或者“configure”的时候指定“no-tls1.3”选项。</p><p>当前OpenSSL是按照“draft-23”实现的TLSv1.3。其他支持TLSv1.3的应用可能使用老的draft版本。这在交互上有问题,如果两个程序(一个浏览器、一个服务端程序)支持不同的TLSv1.3 draft版本尝试沟通的话,可能都被降级到TLSv1.2</p><hr><h2 id="密码套件"><a href="#密码套件" class="headerlink" title="密码套件"></a>密码套件</h2><p>OpenSSL已支持以下5种TLS 1.3的密码套件:</p><ul><li>TLS13-AES-256-GCM-SHA384</li><li>TLS13-CHACHA20-POLY1305-SHA256</li><li>TLS13-AES-128-GCM-SHA256</li><li>TLS13-AES-128-CCM-8-SHA256</li><li>TLS13-AES-128-CCM-SHA256</li></ul><p>其中,前三个是在默认密码套件组中。这意味着如果你没有主动对密码套件进行配置,那么你会自动使用这三个密码套件,并可以进行TLS 1.3磋商。</p><p>所有TLS 1.3密码套件也都出现在别名HIGH中。正如你预计的那样,CHACHA20、AES、AES128、AES256、AESGCM、AESCCM和AESCCM8这些密码套件别名都像它们的名称一样,且包含这些密码套件的一个子集。密钥交换和认证属性是TLS 1.2及以前版本中密码套件定义的一部分。在TLS 1.3中不再如此,所以密码套件别名(如ECHHE、ECDSA、RSA及其它相似别名)都不包含任何TLS 1.3密码套件。</p><p>如果你主动配置了你的密码套件,那么应该注意确保你没有不小心排除掉所有兼容TLS 1.3的密码套件。如果一个客户端启用了TLS 1.3而未配置TLS 1.3密码套件,那么会立即报错(即使服务器不支持TLS 1.3),出现以下提示:</p><pre><code class="hljs plain">140460646909376:error:141A90B5:SSL routines:ssl_cipher_list_to_bytes:no ciphers available:ssl/statem/statem_clnt.c:3562:No ciphers enabled for max supported SSL/TLS version</code></pre><p>类似地,如果一个服务器启用了TLS 1.3而未配置TLS 1.3密码套件,那么也会立即报错,即使客户端不支持TLS 1.3,提示如下:</p><pre><code class="hljs plain">140547390854592:error:141FC0B5:SSL routines:tls_setup_handshake:no ciphers available:ssl/statem/statem_lib.c:108:No ciphers enabled for max supported SSL/TLS version</code></pre><p>例如,默认设置一个密码套件选择字符串ECDHE:!COMPLEMENTOFDEFAULT,还使用ECDHE进行密钥交换。但是,ECDHE组中没有TLS 1.3密码套件,所以如果启用了TLS 1.3,那么这种密码套件配置在OpenSSL 1.1.1中将会出错。你可能要指定你想使用的TLS 1.3密码套件来避免出现问题。例如:</p><pre><code class="hljs plain">TLS13-CHACHA20-POLY1305-SHA256:TLS13-AES-128-GCM-SHA256:TLS13-AES-256-GCM-SHA384:ECDHE:!COMPLEMENTOFDEFAULT</code></pre><p>你可以使用openssl ciphers -s -v命令来测试,在给定的密码套件选择字符串中包含那个密码套件:</p><pre><code class="hljs plain">$ openssl ciphers -s -v "ECDHE:!COMPLEMENTOFDEFAULT"</code></pre><p>确保至少有一个密码套件支持TLS 1.3</p><hr><h2 id="组"><a href="#组" class="headerlink" title="组"></a>组</h2><p>在TLS 1.3中,客户端选择一个“组”用来进行密钥交换。在我撰文时,OpenSSL仅支持ECDHE组(当OpenSSL 1.1.1自动发布时,可能就会支持DHE组了)。客户端会在客户端问候消息中为其所选的组发送“key_share”信息到服务器。</p><p>支持的组的名单是可配置的。一个客户端可能会选择一个服务器不支持的组。在这种情况下,服务器请求客户端发送一个其新支持的key_share。这意味着仍会建立连接(假设存在一个互相支持的组),会引入一个额外的服务器双向连接——所以会对性能产生影响。在理想情况下,客户端将在第一个实例中选择一个服务器支持的组。</p><p>实际上,多数客户端都会为它们的首个key_share使用 X25519或P-256。为实现性能最大化,建议对服务器进行配置,使其至少支持这两个组,客户端为其首个key_share使用其中一个组。这是默认的情况(OpenSSL客户端将使用X25519)。</p><p>组配置还控制着TLS 1.2及以前版本中允许的组。如果应用程序在OpenSSL 1.1.0中曾配置过它们的组,那么你应该重新检查配置,以确保其对TLS 1.3仍然有效。第一个被指定的组(即最偏好的组)将会被一个OpenSSL客户端在其首次key_share中使用。</p><p>应用程序可以使用SSL_CTX_set1_groups()或一个相似的函数(看<a href="https://www.openssl.org/docs/manmaster/man3/SSL_CTX_set1_groups.html" target="_blank" rel="noopener">这里</a>)配置组列表。如果应用程序使用SSL_CONF风格配置文件,则可以使用Groups或Curves命令(看<a href="https://www.openssl.org/docs/manmaster/man3/SSL_CONF_cmd.html#SUPPORTED-CONFIGURATION-FILE-COMMANDS" target="_blank" rel="noopener">这里</a>)来进行配置。</p><hr><h2 id="会话"><a href="#会话" class="headerlink" title="会话"></a>会话</h2><p>在TLS 1.2及以前版本中,一个会话的建立是握手的一部分。这个会话就可以在后续连接中被使用,以实现一个简单握手。一般,在握手完成后,应用程序可能通过使用SSL_get1_session()函数(或类似函数)从会话上得到一个句柄。更多细节请看<a href="https://www.openssl.org/docs/manmaster/man3/SSL_get1_session.html" target="_blank" rel="noopener">这里</a>。</p><p>在TLS 1.3中,直到主握手完成后,会话才会建立。服务器发送一个独立的握手后消息(包含会话细节)到客户端。通常,这会发生在握手结束后不久,也可能会稍晚一些(甚至根本不发生)。</p><p>按照规范,建议应用程序每次只使用一个会话(即使不是强制性的)。由于这个原因,一些服务器会向一个客户端发送多个会话消息。为执行“每次使用一个”的建议,应用程序可以使用SSL_CTX_remove_session()把一个使用过的会话标记为不可恢复(从缓存中将其删除)。旧的SSL_get1_session()和相似API可能像TLS 1.2及以前版本中一样运行。具体来说,如果一个客户端应用程序在收到包含会话细节的服务器消息之前就调用SSL_get1_session(),那么仍将返回一个SSL_SESSION对象,任何试图恢复的企图都不会成功,而是会产生一个完整的握手。在服务器发送多个会话的情况下,只有最后一个会话被SSL_get1_session()返回。</p><p>客户端应用程序开发者应该考虑使用SSL_CTX_sess_set_new_cb() API(看<a href="https://www.openssl.org/docs/manmaster/man3/SSL_CTX_sess_set_new_cb.html" target="_blank" rel="noopener">这里</a>)。这提供一个回调机制,每次新的会话建立时该机制都会被调用。如果服务器发送多个会话消息,就可以为一个连接调用多次。</p><p>注意,SSL_CTX_sess_set_new_cb()在OpenSSL 1.1.0中仍然可用。已使用的那个API应用程序仍可以工作,但它们可能会发现,回调机制被调用的时机变了,变成了在握手之后发生。</p><p>在主握手完成后,一个OpenSSL服务器将立即尝试发生会话细节到客户端。对服务器应用程序来说,这一握手后的阶段看起来像是主握手的一部分,所以对SSL_get1_session()的调用应该像以前一样继续工作。</p><hr><h2 id="定制扩展和证书透明系统"><a href="#定制扩展和证书透明系统" class="headerlink" title="定制扩展和证书透明系统"></a>定制扩展和证书透明系统</h2><p>在TLS 1.2及以前版本中,首次客户端问候信息和服务器问候信息可以包括“扩展”。这允许基础规定可以被添加很多额外特性,这些特性可能无法在所有情况下使用,或者在制定基础规定时无法预见。OpenSSL支持大量“内置”扩展。</p><p>此外,定制扩展API还为应用程序开发人员提供一些基本功能,以支持没有内置在OpenSSL中的新扩展。</p><p>建立在定制扩展API顶层的是“服务器信息”API。这提供了一个更加基础性的接口,可以在运行时进行配置。例如证书透明系统。OpenSSL为证书透明系统的客户端提供内置支持,但没有内置服务器端支持。但是,用“服务器信息”文件就很容易实现。一个包含证书透明系统信息的服务器信息文件可以在OpenSSL中配置,再被正确地发回客户端。</p><p>在TLS 1.3中,扩展的使用显著增加,有很多消息包含扩展。此外,一些支持TLS 1.2及以前版本的扩展在TLS 1.3中不再被支持,一些扩展从服务器问候消息转移到了加密扩展消息中。旧的定制扩展API没有能力指定扩展应该关联的消息。</p><p>由于这个原因,需要有一个新的定制扩展API。</p><p>旧API仍在工作,但定制扩展将只在TLS 1.2及以前版本环境下被添加。想要为所有TLS版本添加定制扩展,应用程序开发人员就需要把他们的应用程序更新到新的API(更多详情看<a href="https://www.openssl.org/docs/manmaster/man3/SSL_CTX_add_custom_ext.html" target="_blank" rel="noopener">这里</a>)。</p><p>“服务器信息”数据格式也已被更新,以包含额外的关于扩展所关联的消息的信息。使用“服务器信息”文件的应用程序可能需要更新到“版本2”的文件格式,才能在TLS 1.3中运行(更多细节看<a href="https://www.openssl.org/docs/manmaster/man3/SSL_CTX_use_serverinfo.html" target="_blank" rel="noopener">这里</a>和<a href="https://www.openssl.org/docs/manmaster/man3/SSL_CTX_use_serverinfo_file.html" target="_blank" rel="noopener">这里</a>)。</p><hr><h2 id="重新磋商"><a href="#重新磋商" class="headerlink" title="重新磋商"></a>重新磋商</h2><p>TLS 1.3没有重新磋商机制,所以在TLS 1.3环境下,对SSL_renegotiate()和SSL_renegotiate_abbreviated()的调用会立即失败。</p><p>重新磋商最常见的例子是更新连接密钥。再TLS 1.3中,函数SSL_key_update()可以用于的这个目的(看这里)。</p><p>另外一个场景是从客户端请求一个整数,这个可以使用TLSv1.3中的SSL_verify_client_post_handshake()方法完成(获取详细信息点<a href="https://www.openssl.org/docs/manmaster/man3/SSL_verify_client_post_handshake.html" target="_blank" rel="noopener">这里</a>)。</p><hr><h2 id="DSA证书"><a href="#DSA证书" class="headerlink" title="DSA证书"></a>DSA证书</h2><p>TLS 1.3中不再允许DSA证书。如果你的服务器应用程序正在使用DSA证书,那么TLS 1.3连接将会失败,提示如下:</p><pre><code class="hljs plain">140348850206144:error:14201076:SSL routines:tls_choose_sigalg:no suitable signature algorithm:ssl/t1_lib.c:2308:</code></pre><p>请使用ECDSA或RSA证书。</p><hr><h2 id="Middlebox-Compatibility-Mode"><a href="#Middlebox-Compatibility-Mode" class="headerlink" title="Middlebox Compatibility Mode"></a>Middlebox Compatibility Mode</h2><p>During development of the TLSv1.3 standard it became apparent that in some cases, even if a client and server both support TLSv1.3, connections could sometimes still fail. This is because middleboxes on the network between the two peers do not understand the new protocol and prevent the connection from taking place. In order to work around this problem the TLSv1.3 specification introduced a “middlebox compatibility” mode. This made a few optional changes to the protocol to make it appear more like TLSv1.2 so that middleboxes would let it through. Largely these changes are superficial in nature but do include sending some small but unneccessary messages. OpenSSL has middlebox compatibility mode on by default, so most users should not need to worry about this. However applications may choose to switch it off by calling the function SSL_CTX_clear_options() and passing SSL_OP_ENABLE_MIDDLEBOX_COMPAT as an argument (see <a href="https://www.openssl.org/docs/manmaster/man3/SSL_CTX_clear_options.html" target="_blank" rel="noopener">here</a> for further details).</p><p>If the remote peer is not using middlebox compatibility mode and there are problematic middleboxes on the network path then this could cause spurious connection failures.</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>TLS 1.3代表着一次重大进步,它有一些激动人心的特性,但对粗心的人来说,在更新时可能会有一些风险。大部分时候,这些问题都会有直接的解决办法。应用程序开发人员应该重新检查他们的代码,并考虑为更高效地使用TLS 1.3,是否应该安装所有更新。类似地,部署人员也应该重新检查他们的配置。</p><p>参考:<br><a href="https://www.openssl.org/blog/blog/2018/02/08/tlsv1.3/" target="_blank" rel="noopener">https://www.openssl.org/blog/blog/2018/02/08/tlsv1.3/</a></p>]]></content>
<tags>
<tag>https</tag>
</tags>
</entry>
<entry>
<title>HTTP/2 介绍</title>
<link href="/posts/http2/"/>
<url>/posts/http2/</url>
<content type="html"><![CDATA[<p>2017年公司全面切换到了https和http/2,以前也陆续整理了些材料,这里算做一下总结。</p><blockquote><p>HTTP/2 is a replacement for how HTTP is expressed “on the wire.” It is not a ground-up rewrite of the protocol; HTTP methods, status codes and semantics are the same, and it should be possible to use the same APIs as HTTP/1.x (possibly with some small additions) to represent the protocol.</p></blockquote><p>HTTP/2是现行HTTP协议(HTTP/1.x)的替代,但它不是重写,HTTP方法/状态码/语义都与HTTP/1.x一样。HTTP/2基于SPDY3,专注于性能,最大的一个目标是在用户和网站间只用一个连接(connection)。</p><p>HTTP/2由两个规范(Specification)组成:</p><ul><li>Hypertext Transfer Protocol version 2 - RFC7540</li><li>HPACK - Header Compression for HTTP/2 - RFC7541</li></ul><h2 id="为什么需要HTTP-2"><a href="#为什么需要HTTP-2" class="headerlink" title="为什么需要HTTP/2"></a>为什么需要HTTP/2</h2><p>我们知道,影响一个HTTP网络请求的因素主要有两个:带宽和延迟。在今天的网络情况下,带宽一般不再是瓶颈,所以我们主要讨论下延迟。延迟一般有下面几个因素:</p><blockquote><p>浏览器阻塞(Head-Of-Line Blocking):浏览器会因为一些原因阻塞请求。<br>DNS查询。<br>建立连接(Initial connection):HTTP基于 TCP 协议,TCP的3次握手和慢启动极大增加延迟。</p></blockquote><p>说完背景,我们讨论下HTTP/1.x中到底存在哪些问题?</p><h3 id="HTTP-1-x的缺陷"><a href="#HTTP-1-x的缺陷" class="headerlink" title="HTTP/1.x的缺陷"></a>HTTP/1.x的缺陷</h3><h4 id="连接无法复用"><a href="#连接无法复用" class="headerlink" title="连接无法复用"></a>连接无法复用</h4><p>连接无法复用会导致每次请求都经历三次握手和慢启动。三次握手在高延迟的场景下影响较明显,慢启动则对文件类大请求影响较大。</p><ul><li>HTTP/1.0传输数据时,每次都需要重新建立连接,增加延迟。</li><li>1.1虽然加入keep-alive可以复用一部分连接,但域名分片等情况下仍然需要建立多个connection,耗费资源,给服务器带来性能压力。</li></ul><h4 id="Head-Of-Line-Blocking"><a href="#Head-Of-Line-Blocking" class="headerlink" title="Head-Of-Line Blocking"></a>Head-Of-Line Blocking</h4><p>导致带宽无法被充分利用,以及后续健康请求被阻塞。HOLB是指一系列包(package)因为第一个包被阻塞;HTTP/1.x中,由于服务器必须按接受请求的顺序发送响应的规则限制,那么假设浏览器在一个(tcp)连接上发送了两个请求,那么服务器必须等第一个请求响应完毕才能发送第二个响应——HOLB。</p><ul><li>虽然现代浏览器允许每个origin建立6个connection,但大量网页动辄几十个资源,HOLB依然是主要问题。</li></ul><h4 id="协议开销大"><a href="#协议开销大" class="headerlink" title="协议开销大"></a>协议开销大</h4><p>HTTP/1.x中header内容过大(每次请求header基本不怎么变化),增加了传输的成本。</p><h4 id="安全因素"><a href="#安全因素" class="headerlink" title="安全因素"></a>安全因素</h4><p>HTTP/1.x中传输的内容都是明文,客户端和服务端双方无法验证身份。</p><h2 id="HTTP-2的新特性"><a href="#HTTP-2的新特性" class="headerlink" title="HTTP/2的新特性"></a>HTTP/2的新特性</h2><p>因为HTTP/1.x的问题,人们提出了各种解决方案。比如希望复用连接的长链接/http long-polling/websocket等等,解决HOLB的Domain Sharding(域名分片)/inline资源/css sprite等等。</p><p>不过以上优化都绕开了协议,直到谷歌推出SPDY,才算是正式改造HTTP协议本身。降低延迟,压缩header等等,SPDY的实践证明了这些优化的效果,也最终带来HTTP/2的诞生。</p><h3 id="新的二进制格式(Binary-Format)"><a href="#新的二进制格式(Binary-Format)" class="headerlink" title="新的二进制格式(Binary Format)"></a>新的二进制格式(Binary Format)</h3><p>http1.x诞生的时候是明文协议,其格式由三部分组成:start line(request line或者status line),header,body。要识别这3部分就要做协议解析,http1.x的解析是基于文本。基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认0和1的组合。基于这种考虑http2.0的协议解析决定采用二进制格式,实现方便且健壮。</p><img src="/posts/http2/1.png" srcset="/img/loading.gif" class="" title="This is an image"><p>http2的格式定义十分高效且精简。length定义了整个frame的大小,type定义frame的类型(一共10种),flags用bit位定义一些重要的参数,stream id用作流控制,payload就是request的正文。</p><img src="/posts/http2/2.png" srcset="/img/loading.gif" class="" title="This is an image"><h3 id="Header压缩"><a href="#Header压缩" class="headerlink" title="Header压缩"></a>Header压缩</h3><p>http1.x的header由于cookie和user agent很容易膨胀,而且每次都要重复发送。</p><p>http2.0使用encoder来减少需要传输的header大小,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小。高效的压缩算法可以很大的压缩header,减少发送包的数量从而降低延迟。</p><h3 id="流(stream)和多路复用(MultiPlexing)"><a href="#流(stream)和多路复用(MultiPlexing)" class="headerlink" title="流(stream)和多路复用(MultiPlexing)"></a>流(stream)和多路复用(MultiPlexing)</h3><p>什么是stream?</p><blockquote><p>Multiplexing of requests is achieved by having each HTTP request/response exchange associated with its own stream. Streams are largely independent of each other, so a blocked or stalled request or response does not prevent progress on other streams.</p></blockquote><blockquote><p>A “stream” is an independent, bidirectional sequence of frames exchanged between the client and server within an HTTP/2 connection.</p></blockquote><blockquote><p>A client sends an HTTP request on a new stream, using a previously unused stream identifier. A server sends an HTTP response on the same stream as the request.<br>Multiplexing of requests is achieved by having each HTTP request/response exchange associated with its own stream.</p></blockquote><p>翻译下,stream就是在HTTP/2连接上的双向帧序列。每个http request都会新建自己的stream,response在同一个stream上返回。</p><p>多路复用(MultiPlexing),即连接共享。之所以可以复用,是因为每个stream高度独立,堵塞的stream不会影响其它stream的处理。一个连接上可以有多个stream,每个stream的frame可以随机的混杂在一起,接收方可以根据stream id将frame再归属到各自不同的request里面。</p><h3 id="流量控制(Flow-Control)"><a href="#流量控制(Flow-Control)" class="headerlink" title="流量控制(Flow Control)"></a>流量控制(Flow Control)</h3><p>类似TCP协议通过sliding window的算法来做流量控制,http2.0使用 WINDOW_UPDATE frame 来做流量控制。每个stream都有流量控制,这保证了数据接收方可以只让自己需要的数据被传输。</p><img src="/posts/http2/3.png" srcset="/img/loading.gif" class="" title="This is an image"><h3 id="流优先级(Stream-Priority)"><a href="#流优先级(Stream-Priority)" class="headerlink" title="流优先级(Stream Priority)"></a>流优先级(Stream Priority)</h3><p>每个流可以设置优先级。优先级的目标是允许终端高速对端(当对端处理并发流时)怎么分配资源。</p><p>更重要的是,当传输能力有限时,优先级可以用来挑选哪些流(高优先级)优先传输——这是优化浏览器渲染的关键,即服务端负责设置优先级,使重要的资源优先加载,加速页面渲染。</p><h3 id="Server-Push"><a href="#Server-Push" class="headerlink" title="Server Push"></a>Server Push</h3><p>Server Push即服务端能通过push的方式将客户端需要的内容预先推送过去,也叫“cache push”。</p><img src="/posts/http2/4.png" srcset="/img/loading.gif" class="" title="This is an image"><p>参考:<br><a href="https://imququ.com/post/http2-resource.html" target="_blank" rel="noopener">https://imququ.com/post/http2-resource.html</a><br><a href="http://www.alloyteam.com/2016/07/httphttp2-0spdyhttps-reading-this-is-enough/" target="_blank" rel="noopener">http://www.alloyteam.com/2016/07/httphttp2-0spdyhttps-reading-this-is-enough/</a></p>]]></content>
<tags>
<tag>http2</tag>
<tag>https</tag>
</tags>
</entry>
<entry>
<title>DomContentLoaded 和 Load 的区别</title>
<link href="/posts/DomContentLoaded-and-Load/"/>
<url>/posts/DomContentLoaded-and-Load/</url>
<content type="html"><![CDATA[<p>前两天排查了一个用户访问的问题,从而对DomContentLoaded和load进行了一下了解,首先看下chrome上的network:</p><img src="/posts/DomContentLoaded-and-Load/chrome_network.jpg" srcset="/img/loading.gif" class="" title="This is an image"><p>可以看到分别有一根蓝线和红线对应DomContentLoaded和load,那分别什么意思呢<br><strong>DomContentLoaded</strong></p><blockquote><p>The DOMContentLoaded event is fired when the initial HTML document has been completely loaded and parsed, without waiting for stylesheets, images, and subframes to finish loading.</p></blockquote><p><strong>Load</strong></p><blockquote><p>The load event is fired when a resource and its dependent resources have finished loading.</p></blockquote><p>DomContentLoaded表示整个dom内容加载完毕,我们看下浏览器的渲染原理:</p><blockquote><p>当我们在浏览器地址输入URL时,浏览器会发送请求到服务器,服务器将请求的HTML文档发送回浏览器,浏览器将文档下载下来后,便开始从上到下解析,解析完成之后,会生成DOM。如果页面中有css,会根据css的内容形成CSSOM,然后DOM和CSSOM会生成一个渲染树,最后浏览器会根据渲染树的内容计算出各个节点在页面中的确切大小和位置,并将其绘制在浏览器上。</p></blockquote><img src="/posts/DomContentLoaded-and-Load/browser.png" srcset="/img/loading.gif" class="" title="This is an image"><p>再看下具体的执行</p><img src="/posts/DomContentLoaded-and-Load/chrome_performence.jpg" srcset="/img/loading.gif" class="" title="This is an image"><p>看chrome上的performance,我们可以发现:</p><blockquote><p>在解析html的过程中,html的解析会被中断,这是因为javascript会阻塞dom的解析。当解析过程中遇到script标签的时候,便会停止解析过程,转而去处理脚本,如果脚本是内联的,浏览器会先去执行这段内联的脚本,如果是外链的,那么先会去加载脚本,然后执行。在处理完脚本之后,浏览器便继续解析HTML文档。同时javascript的执行会受到标签前面样式文件的影响。如果在标签前面有样式文件,需要样式文件加载并解析完毕后才执行脚本.</p></blockquote><p>所以可以明确DOMContentLoaded所计算的时间,当文档中没有脚本时,浏览器解析完文档便能触发 DOMContentLoaded 事件;如果文档中包含脚本,则脚本会阻塞文档的解析,而脚本需要等位于脚本前面的css加载完才能执行。在任何情况下,DOMContentLoaded 的触发不需要等待图片等其他资源加载完成。而load就是整个资源全部都加载好,包括images,scripts、iframe等等。</p><p>参考:<br><a href="https://developers.google.com/web/tools/chrome-devtools/network-performance/resource-loading?hl=zh-cn" target="_blank" rel="noopener">https://developers.google.com/web/tools/chrome-devtools/network-performance/resource-loading?hl=zh-cn</a><br><a href="https://developer.mozilla.org/en-US/docs/Web/Events/DOMContentLoaded" target="_blank" rel="noopener">https://developer.mozilla.org/en-US/docs/Web/Events/DOMContentLoaded</a><br><a href="https://developer.mozilla.org/en-US/docs/Web/Events/load" target="_blank" rel="noopener">https://developer.mozilla.org/en-US/docs/Web/Events/load</a><br><a href="http://javascript.info/onload-ondomcontentloaded" target="_blank" rel="noopener">http://javascript.info/onload-ondomcontentloaded</a><br><a href="http://www.cnblogs.com/caizhenbo/p/6679478.html" target="_blank" rel="noopener">http://www.cnblogs.com/caizhenbo/p/6679478.html</a></p>]]></content>
<tags>
<tag>sre</tag>
<tag>frontend</tag>
</tags>
</entry>
<entry>
<title>wget 或者 curl 无法正确解析域名,而 ping 可以</title>
<link href="/posts/wget-curl-does-not-resolve-domain-properly/"/>
<url>/posts/wget-curl-does-not-resolve-domain-properly/</url>
<content type="html"><![CDATA[<h2 id="现象"><a href="#现象" class="headerlink" title="现象"></a>现象</h2><p>刚才用户反馈服务器上不能wget一个资源,我这边具体测试了下,现象如下</p><blockquote><p>指定IPV4可以正常访问<br>指定IPV6无法访问<br>不指定的时候看到域名对应的解析地址不对,正确的是10.x.x.51,而这个是10.x.x.110。</p></blockquote><p>经过排查,这个域名在上周做过域名指向调整,从10.x.x.110修改为10.x.x.51</p><pre><code class="hljs text">[admin@xx ~]$ curl -4 -avo /dev/null http://meta.xxx.com/metaserver/servers* About to connect() to meta.xxx.com port 80 (#0)* Trying 10.x.x.51... connected* Connected to meta.xxx.com (10.x.x.51) port 80 (#0)> GET /metaserver/servers HTTP/1.1> User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.13.6.0 zlib/1.2.3 libidn/1.18 libssh2/1.4.2> Host: meta.xxx.com> Accept: */*> % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0< HTTP/1.1 200 OK< Server: Apache-Coyote/1.1< Access-Control-Allow-Origin: *< Access-Control-Allow-Methods: GET, POST, DELETE, PUT< Access-Control-Allow-Headers: X-Requested-With, Content-Type, X-Codingpedia< Content-Type: application/json;charset=utf-8< Content-Length: 405< Date: Thu, 01 Feb 2018 06:15:02 GMT<{ [data not shown]101 405 101 405 0 0 239k 0 --:--:-- --:--:-- --:--:-- 395k* Connection #0 to host meta.xxx.com left intact* Closing connection #0[admin@xx ~]$ curl -6 -avo /dev/null http://meta.xxx.com/metaserver/servers* getaddrinfo(3) failed for meta.xxx.com:80* Couldn't resolve host 'meta.xxx.com'* Closing connection #0curl: (6) Couldn't resolve host 'meta.xxx.com'[admin@xx ~]$ curl -avo /dev/null http://meta.xxx.com/metaserver/servers* About to connect() to meta.xxx.com port 80 (#0)* Trying 10.x.x.110... No route to host* couldn't connect to host* Closing connection #0curl: (7) couldn't connect to host</code></pre><h2 id="排查过程"><a href="#排查过程" class="headerlink" title="排查过程"></a>排查过程</h2><p>网上查了些资料,通过curl(依赖libcurl)的程序,如果开启了IPv6,curl默认会优先解析IPv6,在对应域名没有IPv6的情况下,会等待IPv6dns解析失败timeout之后才按以前的正常流程去找IPv4原因。但是比较奇怪的是,即使是这样,顶多是访问慢,但是为什么会解析到上周已经切换到的一个IP呢?<br>我分别strace了下<br>不指定ipv4/6,直接wget meta.xxx.com</p><pre><code class="hljs text">write(2, "Resolving meta.xxx"..., 42Resolving meta.xxx.com... ) = 42socket(PF_NETLINK, SOCK_RAW, 0) = 3bind(3, {sa_family=AF_NETLINK, pid=0, groups=00000000}, 12) = 0getsockname(3, {sa_family=AF_NETLINK, pid=32303, groups=00000000}, [12]) = 0sendto(3, "\24\0\0\0\26\0\1\3@\301rZ\0\0\0\0\0\0\0\0", 20, 0, {sa_family=AF_NETLINK, pid=0, groups=00000000}, 12) = 20recvmsg(3, {msg_name(12)={sa_family=AF_NETLINK, pid=0, groups=00000000}, msg_iov(1)=[{"0\0\0\0\24\0\2\0@\301rZ/~\0\0\2\10\200\376\1\0\0\0\10\0\1\0\177\0\0\1"..., 4096}], msg_controllen=0, msg_flags=0}, 0) = 108recvmsg(3, {msg_name(12)={sa_family=AF_NETLINK, pid=0, groups=00000000}, msg_iov(1)=[{"@\0\0\0\24\0\2\0@\301rZ/~\0\0\n\200\200\376\1\0\0\0\24\0\1\0\0\0\0\0"..., 4096}], msg_controllen=0, msg_flags=0}, 0) = 128recvmsg(3, {msg_name(12)={sa_family=AF_NETLINK, pid=0, groups=00000000}, msg_iov(1)=[{"\24\0\0\0\3\0\2\0@\301rZ/~\0\0\0\0\0\0\1\0\0\0\24\0\1\0\0\0\0\0"..., 4096}], msg_controllen=0, msg_flags=0}, 0) = 20close(3) = 0socket(PF_FILE, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0) = 3connect(3, {sa_family=AF_FILE, path="/var/run/nscd/socket"}, 110) = 0sendto(3, "\2\0\0\0\r\0\0\0\6\0\0\0hosts\0", 18, MSG_NOSIGNAL, NULL, 0) = 18poll([{fd=3, events=POLLIN|POLLERR|POLLHUP}], 1, 5000) = 1 ([{fd=3, revents=POLLIN|POLLHUP}])recvmsg(3, {msg_name(0)=NULL, msg_iov(2)=[{" PS2\0\0", 6}, {"\201\320;X\0\0\0\0", 8}], msg_controllen=0, msg_flags=MSG_CMSG_CLOEXEC}, MSG_CMSG_CLOEXEC) = 0close(3) = 0socket(PF_FILE, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0) = 3connect(3, {sa_family=AF_FILE, path="/var/run/nscd/socket"}, 110) = 0sendto(3, "\2\0\0\0\16\0\0\0\35\0\0\0meta.xxxip"..., 41, MSG_NOSIGNAL, NULL, 0) = 41poll([{fd=3, events=POLLIN|POLLERR|POLLHUP}], 1, 5000) = 1 ([{fd=3, revents=POLLIN|POLLHUP}])read(3, "\2\0\0\0\1\0\0\0\1\0\0\0\4\0\0\0!\0\0\0\0\0\0\0", 24) = 24read(3, "\n\10\224n\2meta.hermes.fx.sh2.ctripcor"..., 38) = 38close(3) = 0write(2, "10.x.x.110", 1210.x.x.110) = 12write(2, "\n", 1) = 1write(2, "Connecting to meta.xxx"..., 63Connecting to meta.xxx.com|10.x.x.110|:80... ) = 63</code></pre><p>指定ipv4</p><pre><code class="hljs text">write(2, "Resolving meta.xxxipco"..., 42Resolving meta.xxx.com... ) = 42socket(PF_NETLINK, SOCK_RAW, 0) = 3bind(3, {sa_family=AF_NETLINK, pid=0, groups=00000000}, 12) = 0getsockname(3, {sa_family=AF_NETLINK, pid=9267, groups=00000000}, [12]) = 0sendto(3, "\24\0\0\0\26\0\1\3\303\304rZ\0\0\0\0\0\0\0\0", 20, 0, {sa_family=AF_NETLINK, pid=0, groups=00000000}, 12) = 20recvmsg(3, {msg_name(12)={sa_family=AF_NETLINK, pid=0, groups=00000000}, msg_iov(1)=[{"0\0\0\0\24\0\2\0\303\304rZ3$\0\0\2\10\200\376\1\0\0\0\10\0\1\0\177\0\0\1"..., 4096}], msg_controllen=0, msg_flags=0}, 0) = 108recvmsg(3, {msg_name(12)={sa_family=AF_NETLINK, pid=0, groups=00000000}, msg_iov(1)=[{"@\0\0\0\24\0\2\0\303\304rZ3$\0\0\n\200\200\376\1\0\0\0\24\0\1\0\0\0\0\0"..., 4096}], msg_controllen=0, msg_flags=0}, 0) = 128recvmsg(3, {msg_name(12)={sa_family=AF_NETLINK, pid=0, groups=00000000}, msg_iov(1)=[{"\24\0\0\0\3\0\2\0\303\304rZ3$\0\0\0\0\0\0\1\0\0\0\24\0\1\0\0\0\0\0"..., 4096}], msg_controllen=0, msg_flags=0}, 0) = 20close(3) = 0open("/etc/resolv.conf", O_RDONLY) = 3fstat(3, {st_mode=S_IFREG|0644, st_size=43, ...}) = 0mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f15151ad000read(3, "nameserver 10.8.86.1\nnameserver "..., 4096) = 43read(3, "", 4096) = 0close(3) = 0munmap(0x7f15151ad000, 4096) = 0uname({sys="Linux", node="SVR13350HW1288", ...}) = 0socket(PF_FILE, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0) = 3connect(3, {sa_family=AF_FILE, path="/var/run/nscd/socket"}, 110) = 0sendto(3, "\2\0\0\0\r\0\0\0\6\0\0\0hosts\0", 18, MSG_NOSIGNAL, NULL, 0) = 18poll([{fd=3, events=POLLIN|POLLERR|POLLHUP}], 1, 5000) = 1 ([{fd=3, revents=POLLIN|POLLHUP}])recvmsg(3, {msg_name(0)=NULL, msg_iov(2)=[{"\20\337\1\0\3\0", 6}, {"\1\0\0\0\0\0\0\0", 8}], msg_controllen=0, msg_flags=MSG_CMSG_CLOEXEC}, MSG_CMSG_CLOEXEC) = 0close(3) = 0socket(PF_FILE, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0) = 3connect(3, {sa_family=AF_FILE, path="/var/run/nscd/socket"}, 110) = 0sendto(3, "\2\0\0\0\4\0\0\0\35\0\0\0meta.xxxip"..., 41, MSG_NOSIGNAL, NULL, 0) = 41poll([{fd=3, events=POLLIN|POLLERR|POLLHUP}], 1, 5000) = 1 ([{fd=3, revents=POLLIN|POLLHUP}])read(3, "\2\0\0\0\1\0\0\0!\0\0\0\1\0\0\0\2\0\0\0\4\0\0\0\1\0\0\0\0\0\0\0", 32) = 32readv(3, [{"meta.xxxgslb.com"..., 33}, {"\35\0\0\0", 4}, {"\n\10u3", 4}], 3) = 41read(3, "meta.xxx.com\0", 29) = 29close(3) = 0write(2, "10.x.x.51", 1110.x.x.51) = 11write(2, "\n", 1) = 1</code></pre><p>发现指定ipv4的情况下,多了下面这段trace,刚开始猜测是不是ipv4的会发起dns query,而不指定的话不会发起,实际验证下来不是这个原因。<br>最后可以看到都是nscd缓存返回的结果,因为nscd的缓存内容不能直接查看,为了解决问题,我就先把nscd服务重启了下。</p><pre><code class="hljs text">open("/etc/resolv.conf", O_RDONLY) = 3fstat(3, {st_mode=S_IFREG|0644, st_size=43, ...}) = 0mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f15151ad000read(3, "nameserver 10.8.86.1\nnameserver "..., 4096) = 43read(3, "", 4096) = 0close(3) = 0</code></pre><p>然后直接用curl访问,已经可以正常返回结果了</p><pre><code class="hljs text">[admin@xx ~]$ curl -4 -avo /dev/null http://meta.xxx.com/metaserver/servers* About to connect() to meta.xxx.com port 80 (#0)* Trying 10.15.200.27... connected* Connected to meta.xxx.com (10.15.200.27) port 80 (#0)> GET /metaserver/servers HTTP/1.1> User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.13.6.0 zlib/1.2.3 libidn/1.18 libssh2/1.4.2> Host: meta.xxx.com> Accept: */*> % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0< HTTP/1.1 200 OK< Server: Apache-Coyote/1.1< Access-Control-Allow-Origin: *< Access-Control-Allow-Methods: GET, POST, DELETE, PUT< Access-Control-Allow-Headers: X-Requested-With, Content-Type, X-Codingpedia< Content-Type: application/json;charset=utf-8< Content-Length: 405< Date: Thu, 01 Feb 2018 10:02:23 GMT<{ [data not shown]101 405 101 405 0 0 75729 0 --:--:-- --:--:-- --:--:-- 98k* Connection #0 to host meta.xxx.com left intact* Closing connection #0</code></pre><h2 id="根因"><a href="#根因" class="headerlink" title="根因"></a>根因</h2><p>其实这个只是暂时解决了问题,但是不指定协议访问的时候,为什么直接从缓存里返回了结果而不做dns解析,还是要具体从libcurl的实现上去分析,后面有空把这段内容补上。不过说回来,dns client cache确实很坑,缓存了上周的一个ip地址,这些问题都需要再深入排查。</p>]]></content>
<tags>
<tag>sre</tag>
</tags>
</entry>
<entry>
<title>Nginx TLS Session 复用</title>
<link href="/posts/nginx-tls-session-reuse/"/>
<url>/posts/nginx-tls-session-reuse/</url>
<content type="html"><![CDATA[<h2 id="概要"><a href="#概要" class="headerlink" title="概要"></a>概要</h2><p>Session 复用,是指将握手时算出来的对称密钥存起来,后续请求中直接使用。这样可以节省证书传送等开销,也可以将 TLS 握手所需 RTT 减少到一个,如下:</p><img src="/posts/nginx-tls-session-reuse/tls.jpg" srcset="/img/loading.gif" class="" title="This is an image"><p>可以看到 Session 复用的时候,双方不需要重新协商密钥了。</p><blockquote><p>Session Identifier<br>Session Ticket</p></blockquote><p>主要有上面两个方法,我会简单介绍下原理,主要讲 Nginx 的具体配置</p><h2 id="Session-Identifier"><a href="#Session-Identifier" class="headerlink" title="Session Identifier"></a>Session Identifier</h2><p>客户端保存 Session ID,在发起 Client Hello 时将上次使用的 Session ID 发送给服务端,服务端根据收到的 Session ID 找到保存好的对称密钥。这里有个问题,线上服务器都是集群部署的,不止一台,当客户端两次请求没有落到同一台服务器上时就无法使会话复用机制生效。所以想要 TLS 会话复用生效,需要让集群中的多台服务器共享 Session 信息。lua-nginx-redis 可以让我们把 Session id 的内容放到redis中。</p><p>Dependencies:</p><blockquote><p>lua-nginx-module<br>lua-resty-core<br>lua-nginx-redis</p></blockquote><p>具体配置:</p><pre><code class="hljs text">lua_package_path "/home/work/nginx/modules/lua-resty-core/lib/?.lua;/home/work/nginx/modules/lua-resty-redis/lib/?.lua;site-enable/lua/?.lua;;";ssl_session_fetch_by_lua_file site-enable/lua/fetch.lua;ssl_session_store_by_lua_file site-enable/lua/store.lua;ssl_session_timeout 7200m;ssl_session_tickets off;</code></pre><h2 id="Session-Ticket"><a href="#Session-Ticket" class="headerlink" title="Session Ticket"></a>Session Ticket</h2><p>Session Ticket 是用只有服务端知道的安全密钥加密过的会话信息,最终保存在浏览器端。浏览器如果在 ClientHello 时带上了 Session Ticket,只要服务器能成功解密就可以完成快速握手。<br>具体配置:</p><pre><code class="hljs text">ssl_session_tickets on;ssl_session_ticket_key /opt/app/nginx/conf/ticket/sessionTicket.key;</code></pre>]]></content>
<tags>
<tag>https</tag>
<tag>nginx</tag>
</tags>
</entry>
<entry>
<title>解决 fullnat 下获取用户源 IP</title>
<link href="/posts/fullnat-get-sip/"/>
<url>/posts/fullnat-get-sip/</url>
<content type="html"><![CDATA[<h2 id="概要"><a href="#概要" class="headerlink" title="概要"></a>概要</h2><p>在设计L4负载均衡架构的时候,往往选择fnat,它支持LB和RS垮vlan通信。LB位于客户端和后端服务之间,对于客户端的请求报文,将目的地址替换成后端服务的地址,源地址替换成LB的本地地址,对于后端服务的响应报文,将目的地址替换成客户端地址,源地址替换成LB的VIP地址。<br>这样就带来一个问题,后端RS上就看不到客户端的源IP了,业内比较常见的有两种解决方案:</p><blockquote><p>TOA,将客户端源IP插入到TCP Option中<br>Proxy Protocol,将客户端IP插入到TCP payload中</p></blockquote><p>下面分别介绍</p><h2 id="TOA,将客户端源IP插入到TCP-Option中"><a href="#TOA,将客户端源IP插入到TCP-Option中" class="headerlink" title="TOA,将客户端源IP插入到TCP Option中"></a>TOA,将客户端源IP插入到TCP Option中</h2><p>TOA是linux的一个内核模块,在tcp协议里面多添加了个option字段,LB把源ip和端口改为16进制然后加到里面一起传到后端RS,然后后端RS通过内核模块toa就可以让应用层程序获取真实的客户端地址。<br>相对来说这块介绍比较多,我这边就不介绍详细细节,可以参考 <a href="http://www.just4coding.com/blog/2015/11/16/toa/" target="_blank" rel="noopener">LVS FULLNAT模式下客户端真实地址的传递</a></p><p>这里说一下,Toa模块对系统的函数进行了修改,但是tcpdump之类的网络层是读取不到用户原始IP,需要应用层用getpeername函数进行自动识别。</p><h2 id="Proxy-Protocol,将客户端ip插入到TCP-payload中"><a href="#Proxy-Protocol,将客户端ip插入到TCP-payload中" class="headerlink" title="Proxy Protocol,将客户端ip插入到TCP payload中"></a>Proxy Protocol,将客户端ip插入到TCP payload中</h2><p>代理协议即 PROXY protocol,是haproxy的作者Willy Tarreau于2010年开发和设计的一个Internet协议,通过为tcp添加一个很小的头信息,来方便的传递客户端信息(协议栈、源IP、目的IP、源端口、目的端口等),在网络情况复杂又需要获取客户IP时非常有用,具体协议设计如下</p><pre><code class="hljs text">Format - PROXY_STRING + single space + INET_PROTOCOL + single space + CLIENT_IP + single space + PROXY_IP + single space + CLIENT_PORT + single space + PROXY_PORT + "\r\n"IPV4 Sample - PROXY TCP4 198.51.100.22 203.0.113.7 35646 80\r\nIPv6 Sample - PROXY TCP6 2001:DB8::21f:5bff:febf:ce22:8a2e 2001:DB8::12f:8baa:eafc:ce29:6b2e 35646 80\r\n</code></pre><p>目前支持的后端应用</p><ul><li>Elastic Load Balancing, since July 2013, AWS’ Load-Balancer</li><li>haproxy, since 1.5-dev3, reverse-proxy load-balancer</li><li>nginx, since 1.5.12 in HTTP server client side only, Web server, HTTP + Mail reverve-proxy</li><li>Percona DB Server, since 5.6.25-73.0, DataBase server</li><li>postfix, since 2.10, SMTP MTA</li><li>apache HTTPD, web server, use the module myfixip, for both apache 2.2 and 2.4</li><li>varnish, HTTP reverse-proxy cache, since version 4.1 d0a2</li></ul><p>nginx配置参考,可以通过$proxy_protocol_addr获取用户的源ip</p><pre><code class="hljs text">server { listen 80 proxy_protocol; location / { add_header Content-Type text/html; return 200 $proxy_protocol_addr; }}</code></pre><p>有个问题是,你开启了proxy protocol后,你就无法对RS进行直接访问测试了,可以说也是将LB和后端RS做了耦合,解决方案是,可以专门搭建一个代理服务器做proxy protocol来测试后端RS</p>]]></content>
<tags>
<tag>tcp</tag>
</tags>
</entry>
<entry>
<title>django singal post update</title>
<link href="/posts/django-singal-post-update/"/>
<url>/posts/django-singal-post-update/</url>
<content type="html"><![CDATA[<p>前不久一个新人问我如何在 django 的 singal 中实现 post update,可是默认 queryset 的 update 是直接调用 sql 的,不会使用 django orm 中的 save 方法,signal 中默认只有 post save 的方法。最后大致实现如下:</p><p><a href="https://docs.djangoproject.com/en/dev/topics/db/queries/#updating-multiple-objects-at-once" target="_blank" rel="noopener">updating-multiple-objects-at-once</a></p><blockquote><p>update() is converted directly to an SQL statement; it doesn’t call save() on the model instances, and so the pre_save and post_save signals aren’t emitted. If you want your signal receivers to be called, you should loop over the queryset, and for each model instance, make your changes and call save() yourself.</p></blockquote><p>singals.py文件</p><pre><code class="hljs python"><span class="hljs-comment"># 自定义 singal</span><span class="hljs-keyword">from</span> django.dispatch <span class="hljs-keyword">import</span> Signalpost_update = Signal(providing_args=[<span class="hljs-string">"user"</span>])</code></pre><p>models.py文件</p><pre><code class="hljs python"><span class="hljs-comment"># 对某个 model,override 其 queryset 中的 update 方法</span><span class="hljs-comment"># 引入自定义的signal文件</span><span class="hljs-keyword">from</span> tools <span class="hljs-keyword">import</span> signals<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyCustomQuerySet</span><span class="hljs-params">(models.query.QuerySet)</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">update</span><span class="hljs-params">(self, **kwargs)</span>:</span> super(MyCustomQuerySet, self).update(**kwargs) //update被调用时, 发送该singal signals.post_update.send(sender=self.model, user=<span class="hljs-string">"xxx"</span>) print(<span class="hljs-string">"finished!"</span>)<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyCustomManager</span><span class="hljs-params">(models.Manager)</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_queryset</span><span class="hljs-params">(self)</span>:</span> <span class="hljs-keyword">return</span> MyCustomQuerySet(self.model, using=self._db)<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">crontab_ping</span><span class="hljs-params">(models.Model)</span>:</span> name = models.CharField(max_length=<span class="hljs-number">64</span>, blank=<span class="hljs-literal">True</span>, null=<span class="hljs-literal">True</span>) objects = MyCustomManager()</code></pre><p>callback.py文件:</p><pre><code class="hljs python"><span class="hljs-comment">#接收signal,触发操作</span><span class="hljs-keyword">from</span> tools.signals <span class="hljs-keyword">import</span> post_update<span class="hljs-meta">@receiver(post_update)</span><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">post_update_callback</span><span class="hljs-params">(sender, **kwargs)</span>:</span> print(kwargs[<span class="hljs-string">'user'</span>]) print(<span class="hljs-string">"post_update_success"</span>)</code></pre>]]></content>
<tags>
<tag>python</tag>
</tags>
</entry>
</search>