-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathatom.xml
More file actions
581 lines (390 loc) · 244 KB
/
atom.xml
File metadata and controls
581 lines (390 loc) · 244 KB
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
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>ZhiWei Show</title>
<subtitle>Just Show Me</subtitle>
<link href="/atom.xml" rel="self"/>
<link href="https://zhiwei.show/"/>
<updated>2021-03-19T01:31:00.000Z</updated>
<id>https://zhiwei.show/</id>
<author>
<name>ZhiWei</name>
</author>
<generator uri="http://hexo.io/">Hexo</generator>
<entry>
<title>哈夫曼编码</title>
<link href="https://zhiwei.show/78100c4518fd46dabbde08924e39f121/"/>
<id>https://zhiwei.show/78100c4518fd46dabbde08924e39f121/</id>
<published>2021-03-19T01:31:00.000Z</published>
<updated>2021-03-19T01:31:00.000Z</updated>
<content type="html"><![CDATA[<h1 id="什么问题"><a href="#什么问题" class="headerlink" title="什么问题"></a>什么问题</h1><p>解决规模效率的问题, 基本思路是<code>如果数量越多, 就让他的成本更低</code></p><h1 id="举个例子"><a href="#举个例子" class="headerlink" title="举个例子:"></a>举个例子:</h1><p>我们店里有三种一次性商品: 纸杯, 手套, 口罩</p><p>商品的货架位置分别在 1 米(纸杯), 3 米(手套), 5 米(口罩)</p><p>而根据我们出货数量的统计发现</p><p>口罩每天能卖 100 个, 手套 20 个, 纸杯 10 个</p><p>这样我们算一下每天总共需要走多少米</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">100个口罩 * 5米(口罩货架的位置) + 20个手套 * 3米(手套货架的位置) + 10个纸杯 * 1米(纸杯货架的位置)</span><br><span class="line"></span><br><span class="line">100 * 5 + 20 * 3 + 10 *1 = 570米</span><br></pre></td></tr></table></figure><p>总觉得哪里不太对劲啊</p><a id="more"></a><p>为什么口罩购买的那么多, 却放在那么远的<br>而纸杯购买那么的那么少, 反而放的那么近</p><p>假如我们把纸杯和口罩的货架位置转换一下后再来看看</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">100个口罩 * 1米(口罩货架的位置) + 20个手套 * 3米(手套货架的位置) + 10个纸杯 * 5米(纸杯货架的位置)</span><br><span class="line"></span><br><span class="line">100 * 1 + 20 * 3 + 10 *1 = 170米</span><br></pre></td></tr></table></figure><p>看看, 只是的换了一下位置, 就比之前节省了 400 米.</p><h1 id="编码是什么东西"><a href="#编码是什么东西" class="headerlink" title="编码是什么东西"></a>编码是什么东西</h1><p>所谓的编码, 是将我们想要传输的信息转为能够理解的信息格式</p><p>众所周知, 计算机识别二进制, 比如</p><p>000 => 哈<br>001 => 夫<br>010 => 曼<br>011 => 编<br>100 => 码</p><p>这样计算机是不是就能帮助我们传递信息了</p><p>传递<code>哈夫曼编码</code>四个字共需要 15 个比特</p><h1 id="等长编码"><a href="#等长编码" class="headerlink" title="等长编码"></a>等长编码</h1><p>每个编码的位数是一样长的就是等长编码</p><p>比如使用最广的<code>ASCII码</code>用 1 个字节编码</p><p>0000 0000 => a<br>0000 0001 => b<br>0000 0010 => c<br>0000 0011 => d</p><p>这样我传输 4 个字符, 就需要 32 个 bit, 感觉有点浪费啊, 前面的 0 光占坑不干活</p><p>有没有办法能节省下, 我们知道在<code>传输速度</code>不变的情况下, <code>传输总量</code>越小, <code>消耗时间</code>也越少</p><h1 id="变长编码"><a href="#变长编码" class="headerlink" title="变长编码"></a>变长编码</h1><p>比如, 我们用<br>0 => a<br>1 => b<br>01 => c<br>10 => d</p><p>总共就需要传输 6 个 bit, 等等, 等等, 不太对劲.</p><p>计算机怎么识别<code>01</code>代表<code>c</code>还是<code>a</code>和<code>b</code>呢, 貌似识别不出来啊.</p><p>既然这样, 那将有冲突的二进制去掉就可以了.</p><p>0 => a<br>11 => b<br>100 => c<br>101 => d</p><p>这样看来, 发送 4 个字符, 总要需要 9 个 bit</p><p>相比于等长编码的 32 个 bit, 我们节省了 23 个 bit,</p><h1 id="字符的频率"><a href="#字符的频率" class="headerlink" title="字符的频率"></a>字符的频率</h1><p>大致的思路都理解了, 现在我们从一个实际的例子触发</p><p>假设我们传输一段信息, 为了方便, 我们拿英文来举例</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Don't be afraid to shoot a single horse. What about being alone and brave? You can cry all the way, but you can't be angry. You have to go through the days when nobody cares about it to welcome applause and flowers.</span><br></pre></td></tr></table></figure><p>上面这个句子, 我们统计一下字符的数量, 同样为了方便, 我们只显示 TOP5 的字符</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">' ', 41</span><br><span class="line">'a', 20</span><br><span class="line">'o', 19</span><br><span class="line">'e', 16</span><br><span class="line">'t', 14</span><br><span class="line">...</span><br><span class="line">...</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>这样一来, 是不是我用什么二进制代表什么字符对传输总量有着非常大的影响</p><p>用 0 代表’a’ == 1bit * 20 = 20bit<br>用 101 代表’a’ == 3bit * 20 = 60bit</p><h1 id="哈夫曼树"><a href="#哈夫曼树" class="headerlink" title="哈夫曼树"></a>哈夫曼树</h1><p>哈夫曼编码就是解决这个问题的</p><p><code>利用变长编码, 对频率高的用占bit数少的编码, 对频率低的用占bit多的编码</code></p><p>所以我们如何才能构建这个编码呢? 需要两个信息, 字符和字符出现的频率.</p><p>这就涉及到哈夫曼树的生成</p><p>假使有这么一个数组[(‘ ‘, 41),(‘a’, 20),(‘o’, 19),(‘e’, 16),(‘t’, 14)]</p><p>算法是这样的</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">1. 从数组中取出频率最低的两个数据`('e', 16)`和`('t', 14)`</span><br><span class="line"></span><br><span class="line">2. 将两个频率相加 16+14==30, 然后将衍生出来的数据, 放进数组里, 字符为`NULL`</span><br><span class="line"></span><br><span class="line">[(' ', 41),('a', 20),('o', 19), (NULL, 30)]</span><br><span class="line"></span><br><span class="line">接着重复1,2直到数组中的为空</span><br></pre></td></tr></table></figure><p><img src="/resource/6f060ad57fac497da36f3d2a240acee0.png" alt="f34bf0fa970a6bbc8cdbfe9798a43b4b.png"></p><p>根据生成出来的二叉树, 我们可以得出结论.</p><p>叶子节点对应字符出现的频率(也就是权重)</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">' ' 这个字符出现41次, 用0编码(占用1bit)</span><br><span class="line">'o' 这个字符出现19次, 用100编码(占用3bit)</span><br><span class="line">'a' 这个字符出现20次, 用101编码(占用3bit)</span><br><span class="line">'t' 这个字符出现14次, 用110编码(占用3bit)</span><br><span class="line">'e' 这个字符出现16次, 用111编码(占用3bit)</span><br></pre></td></tr></table></figure><p>妙啊, 妙啊, 既减少了, 传输数据的总量, 又不会出现编码的冲突</p><p>这里总共传输的 bit 数是</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">1bit * 41 + 3bit * 19 + 3bit * 20 + 3bit * 14 + 3bit * 16 = 246bit</span><br></pre></td></tr></table></figure><p>如果我们用等长编码(8bit)那么总共传输的 bit 数</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">8bit * 41 + 8bit * 19 + 8bit * 20 + 8bit * 14 + 8bit * 16 = 880bit</span><br></pre></td></tr></table></figure><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>变长编码, 能将减少传输数据的总量</p><p>哈夫曼编码, 能基于字符出现的频率进行不同长度的编码</p><p>保证在大量数据传输的过程中依然有较好的传输效率</p>]]></content>
<summary type="html">
<h1 id="什么问题"><a href="#什么问题" class="headerlink" title="什么问题"></a>什么问题</h1><p>解决规模效率的问题, 基本思路是<code>如果数量越多, 就让他的成本更低</code></p>
<h1 id="举个例子"><a href="#举个例子" class="headerlink" title="举个例子:"></a>举个例子:</h1><p>我们店里有三种一次性商品: 纸杯, 手套, 口罩</p>
<p>商品的货架位置分别在 1 米(纸杯), 3 米(手套), 5 米(口罩)</p>
<p>而根据我们出货数量的统计发现</p>
<p>口罩每天能卖 100 个, 手套 20 个, 纸杯 10 个</p>
<p>这样我们算一下每天总共需要走多少米</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">100个口罩 * 5米(口罩货架的位置) + 20个手套 * 3米(手套货架的位置) + 10个纸杯 * 1米(纸杯货架的位置)</span><br><span class="line"></span><br><span class="line">100 * 5 + 20 * 3 + 10 *1 = 570米</span><br></pre></td></tr></table></figure>
<p>总觉得哪里不太对劲啊</p>
</summary>
<category term="算法" scheme="https://zhiwei.show/tags/%E7%AE%97%E6%B3%95/"/>
</entry>
<entry>
<title>qcloud的k8s集群界面编辑导致pod无法启动</title>
<link href="https://zhiwei.show/f4b54833899b4b93a4b968318ae4afec/"/>
<id>https://zhiwei.show/f4b54833899b4b93a4b968318ae4afec/</id>
<published>2021-03-11T18:52:18.000Z</published>
<updated>2021-03-12T02:57:04.000Z</updated>
<content type="html"><![CDATA[<h1 id="日志满了"><a href="#日志满了" class="headerlink" title="日志满了"></a>日志满了</h1><p>收到告警 clickhouse-zookeeper 的告警</p><p><img src="/resource/7f64c601114f4f5db48126eb94fef2b8.png" alt="de642b7c366f05b6982eaead18a59758.png"></p><p>zookeeper 其实是有参数可以实现自动清理以前的数据</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"># 间隔多久进行一次清理</span><br><span class="line">autopurge.purgeInterval: int, default=0</span><br><span class="line"># 保留多少个数据, 这里包括日志和快照的数据</span><br><span class="line">autopurge.snapretaincount: int, default=3</span><br></pre></td></tr></table></figure><a id="more"></a><p>zookeeper 上如果没有设置<code>autopurge.purgeInterval</code>参数, 默认是 0, 表示关闭自动清理</p><p>导致数据一直保留着, 把磁盘写满了(其实也就 10G, 小规模的集群)</p><p>因为使用 helm 部署的 zookeeper, 参数都是通过环境变量控制的</p><p>所以需要通过 stateful 的配置来修改这个<code>ZOO_AUTOPURGE_INTERVAL</code>参数</p><h1 id="无法启动-pod"><a href="#无法启动-pod" class="headerlink" title="无法启动 pod"></a>无法启动 pod</h1><p>通过 qcloud k8s 的界面修改完了后, 发现 pod 无法启动</p><p>一直在报这个错误<code>Back-off restarting failed container</code></p><p>根据之前的经验, 都是因为 pod 里面的进程执行错误导致的</p><p>一般都能在日志里面找到相关的错误信息, 但是这次什么都没有.</p><p>而且因为 pod 一直在重启, 也无法登录进 container 里面</p><p>尝试还原修改, 问题依然存在.</p><p>既然如此, 那就修改 pod 的<code>command</code>, 通过<code>tail -f /dev/null</code>让 pod 保持住</p><p>然而当我打开编辑界面时发现不对劲啊, 命令和参数全乱套了.</p><p><img src="/resource/098223c1429d4087907869be1c6a9a91.png" alt="225c9433a810ad36020ee71a09abe055.png"></p><p>我做了啥, 只不过修改了一个环境变量而已</p><p>看来编辑器不可靠, 直接打开了 stateful 的 yaml 文件, 并且找到另一个 zookeeper 的 yaml 文件进行对比</p><p><img src="/resource/38ceb7c23da54ea5a086b12cc5f59113.png" alt="4f2ef247df4c65890c6f0b4e3316c53e.png"></p><p>左边是有问题的, 右边是正常的.</p><p>很明显, 在使用 qcloud k8s 编辑器的时候, 将换行都转成了参数.</p><p>导致 pod 运行不起来.</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><ol><li>以为是小问题, 修改不会有影响, 结果还是坑了自己</li><li>云厂商的也不太可靠啊, 能写 yaml 还是写 yaml 吧</li><li>因为是用来测试的, 所以部署时也没有考虑清理数据的问题</li></ol>]]></content>
<summary type="html">
<h1 id="日志满了"><a href="#日志满了" class="headerlink" title="日志满了"></a>日志满了</h1><p>收到告警 clickhouse-zookeeper 的告警</p>
<p><img src="/resource/7f64c601114f4f5db48126eb94fef2b8.png" alt="de642b7c366f05b6982eaead18a59758.png"></p>
<p>zookeeper 其实是有参数可以实现自动清理以前的数据</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"># 间隔多久进行一次清理</span><br><span class="line">autopurge.purgeInterval: int, default=0</span><br><span class="line"># 保留多少个数据, 这里包括日志和快照的数据</span><br><span class="line">autopurge.snapretaincount: int, default=3</span><br></pre></td></tr></table></figure>
</summary>
<category term="k8s" scheme="https://zhiwei.show/tags/k8s/"/>
<category term="qcloud" scheme="https://zhiwei.show/tags/qcloud/"/>
</entry>
<entry>
<title>BLOG方案的调整</title>
<link href="https://zhiwei.show/2dc0f4142a23440ca1e50cde7c6d02e0/"/>
<id>https://zhiwei.show/2dc0f4142a23440ca1e50cde7c6d02e0/</id>
<published>2021-03-11T00:24:20.000Z</published>
<updated>2021-03-12T02:57:14.000Z</updated>
<content type="html"><![CDATA[<h1 id="Blog-的问题"><a href="#Blog-的问题" class="headerlink" title="Blog 的问题"></a>Blog 的问题</h1><p>之前使用 hexo+github 部署 BLOG 环境</p><p>使用 vscode+markdown 来写 Blog</p><p>但是体验很不好, 主要有两个原因</p><ol><li><p>有一些内容记录在云笔记中, 总是需要而外的花时间转换成 Blog</p></li><li><p>Blog 中涉及到图片的内容处理起来很麻烦</p></li></ol><p>因为这两个原因, 导致 Blog 长时间没更新</p><p>于是想找找看有没其他更好的办法</p><a id="more"></a><h1 id="云笔记的问题"><a href="#云笔记的问题" class="headerlink" title="云笔记的问题"></a>云笔记的问题</h1><p>同时在使用云笔记上也有些麻烦</p><p>使用过 OneNote, 印象笔记, 为知笔记, 有道笔记总觉得不太合适</p><p>因为数据存在别人服务器上, 总觉得万一哪天说涨价就涨价.</p><p>这不是完全被绑架了吗</p><p>所以想着是不是有个产品能将功能和数据分开</p><h1 id="开源云笔记"><a href="#开源云笔记" class="headerlink" title="开源云笔记"></a>开源云笔记</h1><p>在网上找了找, 还真的有这么一个项目(<a href="https://joplinapp.org/" target="_blank" rel="noopener">joplin</a>)</p><p>这是一个跨平台的开源云笔记, 可以使用 Dropbox, OneDrive, S3 存储</p><p>都是国外的云服务, 虽然都有好口碑, 但是国内的网络完全没法法使用</p><p>好在有个国内的坚果云可以使用</p><p>折腾了一晚上, 确实还不错</p><ol><li>跨平台</li><li>数据存储</li><li>数据加密</li><li>图片插入</li></ol><h1 id="Blog-迁移"><a href="#Blog-迁移" class="headerlink" title="Blog 迁移"></a>Blog 迁移</h1><p>既然云笔记本有开源的方案, 那有没有将 Joplin 转为 blog 的项目呢</p><p>答案是肯定的<a href="https://github.com/rxliuli/joplin-blog" target="_blank" rel="noopener">joplin-blog</a></p><p>一个将 joplin 转为 hexo 的项目, 虽然项目还比较年轻</p><p>但基本上够用了, 最重要是, 将笔记转为 Blog 相比以前真的是方便太多</p><p>而且还很好的解决了图片的问题</p><p>再也找不到偷懒的借口了, T.T</p><h1 id="还有问题"><a href="#还有问题" class="headerlink" title="还有问题"></a>还有问题</h1><h2 id="joplin-blog-的问题"><a href="#joplin-blog-的问题" class="headerlink" title="joplin-blog 的问题"></a>joplin-blog 的问题</h2><ol><li>Blog 的置顶不太方便, 需要将文章 ID 记录下来, 然后存在配置中</li></ol><h2 id="joplin-的问题"><a href="#joplin-的问题" class="headerlink" title="joplin 的问题"></a>joplin 的问题</h2><ol><li>在文件变多时(750), 需要分页请求坚果云, 而 joplin 不支持分页, 会导致丢失数据</li><li>有时需要在 joplin 中使用页面替换功能, 发现居然没有</li></ol>]]></content>
<summary type="html">
<h1 id="Blog-的问题"><a href="#Blog-的问题" class="headerlink" title="Blog 的问题"></a>Blog 的问题</h1><p>之前使用 hexo+github 部署 BLOG 环境</p>
<p>使用 vscode+markdown 来写 Blog</p>
<p>但是体验很不好, 主要有两个原因</p>
<ol>
<li><p>有一些内容记录在云笔记中, 总是需要而外的花时间转换成 Blog</p>
</li>
<li><p>Blog 中涉及到图片的内容处理起来很麻烦</p>
</li>
</ol>
<p>因为这两个原因, 导致 Blog 长时间没更新</p>
<p>于是想找找看有没其他更好的办法</p>
</summary>
</entry>
<entry>
<title>clickhouse啊, 你很快嘛</title>
<link href="https://zhiwei.show/b4cc7846cded4ac091be89590b09f75d/"/>
<id>https://zhiwei.show/b4cc7846cded4ac091be89590b09f75d/</id>
<published>2021-03-07T18:09:17.000Z</published>
<updated>2021-03-12T18:53:09.000Z</updated>
<content type="html"><![CDATA[<h1 id="是什么"><a href="#是什么" class="headerlink" title="是什么"></a>是什么</h1><p>从使用角度来看是一个分析型数据库</p><p>从实现角度来看是一个列式数据库系统</p><h2 id="分析型数据库和事务型数据库"><a href="#分析型数据库和事务型数据库" class="headerlink" title="分析型数据库和事务型数据库"></a>分析型数据库和事务型数据库</h2><p>字面意思是为了分析数据的数据库, 特点是需要存储的数据非常多</p><p>但是对数据的一致性没有要求, 因为分析数据后看的是趋势, 小部分数据的错误不影响总体的趋势</p><p>相对应的是事务型数据库</p><h2 id="事务是什么"><a href="#事务是什么" class="headerlink" title="事务是什么"></a>事务是什么</h2><p>简单来说, 是不可分离的多个动作</p><p>讲到事务就不得不讲讲事务四个特性</p><p>原子性, 要么都成功, 要么都失败<br>一致性, 同一个事务中, 多次查询的数据要一致<br>隔离性, 每个事务要尽可能的不影响其他事务<br>持久性, 写入后不能丢失数据</p><p>事务性数据库对一致性要求很高</p><p>分析型数据库对海量数据要求很高</p><p>分析型的数据库一般没有<code>一致性</code>, <code>隔离性</code>, <code>原子性</code>, 但是肯定会有<code>持久性</code></p><h1 id="什么是列式数据库"><a href="#什么是列式数据库" class="headerlink" title="什么是列式数据库"></a>什么是列式数据库</h1><p>比如有三条记录</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">1,clickhouse,database</span><br><span class="line">2,mysql,database</span><br><span class="line">3,nginx,web</span><br></pre></td></tr></table></figure><p>如果我们按照每行进行存储, 那就是行式数据库</p><p>比如这样存储:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">1,clickhouse,database|2,mysql,database|3,nginx,web</span><br></pre></td></tr></table></figure><p>而我们在分析数据的时候经常会选取几列的数据进行统计分析</p><p>如果是行式存储的话, 会将整条的数据也一并读取出来(核心思想是减少</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">1,2,3|clickhouse,mysql,nginx|database,database,web</span><br></pre></td></tr></table></figure><p>而如果是列式数据库, 则只会读取相关列的数据</p><p>通常随着数据的增大, 磁盘的 IO 会首先出现瓶颈.</p><p>采用列式数据库能够的大量减少读取的数据</p><p>并且在因为每列的数据类型都是相同的, 所以每列都能压缩数据, 减少存储大小</p><p>因为总数据量的减少, 所以在磁盘 IO 又进一步的降低了.</p><p>但是涉及到压缩和解压, 导致相比于其他数据库会使用更多的 CPU 资源</p><p>所以, <code>clickhouse大部分情况下都是CPU先跑满, 而磁盘IO方面不会成为瓶颈</code></p><h1 id="分布式数据库"><a href="#分布式数据库" class="headerlink" title="分布式数据库?"></a>分布式数据库?</h1><p>这样说可能不对劲, 因为 clickhouse 的分布式是体现在 table 层的.</p><p>打个比方, 我们有 10 万条数据, 两个节点, 不考虑双副本的情况下(高可用)</p><p>将 5 万条数据存入节点 1, 另 5 万条数据存入节点 2</p><p>这个存储数据的东西叫做本地表, 是保存了实实在在的数据</p><p>另外通过创建分布式表将本地表关联起来, 从而实现分片的效果</p><p>我们查询数据的时候直接实用分布式表名, 分布式表会自动帮我们去每个本地表上查询数据</p><p>然后在进行聚合汇总</p><p>那我们写数据的时候也写入分布式表吗?</p><h2 id="分片和负载均衡"><a href="#分片和负载均衡" class="headerlink" title="分片和负载均衡"></a>分片和负载均衡</h2><p>clickhouse 分片和其他分布式集群有点不太一样.</p><p>我们确实可以将数据写入分布式表</p><p>但是规模(日千亿级别, 百 M/s)上来了后不太推荐这种做法</p><p>主要是数据性能和数据一致性的问题.</p><p>本机的分布式表收到请求后会将数据存放在临时文件</p><p>然后再尝试将数据写入相应的分片, 造成了写放大</p><p>推荐写入数据的方式是, 通过负载均衡将数据写入不同的分片</p><p>从逻辑上来说, 每个分片是相互独立的数据</p><p>具体的数据分布情况要看负载均衡的调度策略, 以及每次写入 batch 的大小</p><p>这个需要在业务使用中多加注意</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>非常适合用来做用户行为分析</p><p>大部分应用的瓶颈都在 IO 上, clickhouse 的架构设计避开了这点</p><p>最大限度的利用了多核的优势, 比如从磁盘读取数据后的并行解压, 以及数据分片的使用</p><p>但是在查询使用过程中需要做一些兼容.</p><h1 id="一个大胆的想法"><a href="#一个大胆的想法" class="headerlink" title="一个大胆的想法"></a>一个大胆的想法</h1><p>既然 clickhouse 能够存储大量的数据并且把数据压缩起来.</p><p>是不是可以将游戏通信中的数据存入 clickhouse 中.</p><p>这样可以分析游戏中的<code>热点请求</code>以及, 帮助游戏<code>排查业务Bug</code></p>]]></content>
<summary type="html">
<h1 id="是什么"><a href="#是什么" class="headerlink" title="是什么"></a>是什么</h1><p>从使用角度来看是一个分析型数据库</p>
<p>从实现角度来看是一个列式数据库系统</p>
<h2 id="分析型数据库和事务型数据
</summary>
<category term="clickhouse" scheme="https://zhiwei.show/tags/clickhouse/"/>
<category term="大数据" scheme="https://zhiwei.show/tags/%E5%A4%A7%E6%95%B0%E6%8D%AE/"/>
</entry>
<entry>
<title>flink启动job随机卡住故障排查</title>
<link href="https://zhiwei.show/34f2635ca876481da35832a0995e4875/"/>
<id>https://zhiwei.show/34f2635ca876481da35832a0995e4875/</id>
<published>2021-02-11T00:52:00.000Z</published>
<updated>2021-02-10T22:52:00.000Z</updated>
<content type="html"><![CDATA[<h1 id="故障表现"><a href="#故障表现" class="headerlink" title="故障表现"></a>故障表现</h1><p>在启动 job 时随机出现 checkpoint 无法完成的情况</p><p>但只要 job 能够正常跑起来就一直都没有问题</p><p>没有任何的错误日志</p><p><img src="/resource/24eab53b368c4632b4220c7fa11e7770.png" alt="ce95965764a3fe6be318253fed181a72.png"></p><p><img src="/resource/0ed70bc5087040f7bd36e520e636e130.png" alt="b6ec6decddba84e326d58318e69d1817.png"></p><a id="more"></a><h1 id="背景环境"><a href="#背景环境" class="headerlink" title="背景环境"></a>背景环境</h1><p>在腾讯云的 k8s 上搭建的 flink standalone in k8s 模式, 版本是 flink:1.11.3</p><p>Flink 的 CheckPoint 存储用的是腾讯云的 NFS</p><h1 id="排查思路"><a href="#排查思路" class="headerlink" title="排查思路"></a>排查思路</h1><h2 id="猜测-怀疑是-NFS-的问题"><a href="#猜测-怀疑是-NFS-的问题" class="headerlink" title="猜测: 怀疑是 NFS 的问题"></a>猜测: 怀疑是 NFS 的问题</h2><p>直接去掉 NFS 挂载, 使用本地文件存储 checkpoint, 问题表现依旧</p><p>故障和 nfs 存储无关</p><h2 id="猜测-怀疑是部署环境导致的"><a href="#猜测-怀疑是部署环境导致的" class="headerlink" title="猜测: 怀疑是部署环境导致的"></a>猜测: 怀疑是部署环境导致的</h2><p>因为我们使用的是 helm 包安装的, 并且 k8s 里面有其他服务</p><p>直接重新购买了一套独立 k8s 集群, 通过官方的 yaml 部署, 问题表现依旧</p><p>故障和配置环境无关</p><p>目前看来故障和配置, 环境, 存储无关</p><h2 id="猜测-job-代码的问题"><a href="#猜测-job-代码的问题" class="headerlink" title="猜测: job 代码的问题"></a>猜测: job 代码的问题</h2><p>从 flink 官方上下载了 Example, 跑了十几次都没有问题</p><p>基本确定是 job 代码的问题</p><h2 id="告警日志的线索"><a href="#告警日志的线索" class="headerlink" title="告警日志的线索"></a>告警日志的线索</h2><p>仔细查看下 taskamanger 的日志, 发现了个重要的线索</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line">2021-03-02 15:49:59,091 WARN org.apache.flink.runtime.taskmanager.Task [] - Task 'Source: 配置广播流,GameId=17 (1/1)' did not react to cancelling signal for 30 seconds, but is stuck in method:</span><br><span class="line"> sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)</span><br><span class="line">sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)</span><br><span class="line">sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)</span><br><span class="line">java.lang.reflect.Constructor.newInstance(Constructor.java:423)</span><br><span class="line">java.lang.Class.newInstance(Class.java:442)</span><br><span class="line">java.util.ServiceLoader$LazyIterator.nextService(ServiceLoader.java:380)</span><br><span class="line">java.util.ServiceLoader$LazyIterator.next(ServiceLoader.java:404)</span><br><span class="line">java.util.ServiceLoader$1.next(ServiceLoader.java:480)</span><br><span class="line">java.sql.DriverManager$2.run(DriverManager.java:603)</span><br><span class="line">java.sql.DriverManager$2.run(DriverManager.java:583)</span><br><span class="line">java.security.AccessController.doPrivileged(Native Method)</span><br><span class="line">java.sql.DriverManager.loadInitialDrivers(DriverManager.java:583)</span><br><span class="line">java.sql.DriverManager.<clinit>(DriverManager.java:101)</span><br><span class="line">org.mariadb.jdbc.Driver.<clinit>(Driver.java:70)</span><br><span class="line">java.lang.Class.forName0(Native Method)</span><br><span class="line">java.lang.Class.forName(Class.java:264)</span><br><span class="line">org.apache.flink.connector.jdbc.internal.connection.SimpleJdbcConnectionProvider.getConnection(SimpleJdbcConnectionProvider.java:52)</span><br><span class="line">com.funjoy.flink.ConfigSourceFunction.open(ConfigSourceFunction.java:52)</span><br><span class="line">org.apache.flink.api.common.functions.util.FunctionUtils.openFunction(FunctionUtils.java:36)</span><br><span class="line">org.apache.flink.streaming.api.operators.AbstractUdfStreamOperator.open(AbstractUdfStreamOperator.java:102)</span><br><span class="line">org.apache.flink.streaming.runtime.tasks.OperatorChain.initializeStateAndOpenOperators(OperatorChain.java:291)</span><br><span class="line">org.apache.flink.streaming.runtime.tasks.StreamTask.lambda$beforeInvoke$1(StreamTask.java:506)</span><br><span class="line">org.apache.flink.streaming.runtime.tasks.StreamTask$$Lambda$621/13894355.run(Unknown Source)</span><br><span class="line">org.apache.flink.streaming.runtime.tasks.StreamTaskActionExecutor$SynchronizedStreamTaskActionExecutor.runThrowing(StreamTaskActionExecutor.java:92)</span><br><span class="line">org.apache.flink.streaming.runtime.tasks.StreamTask.beforeInvoke(StreamTask.java:475)</span><br><span class="line">org.apache.flink.streaming.runtime.tasks.StreamTask.invoke(StreamTask.java:526)</span><br><span class="line">org.apache.flink.runtime.taskmanager.Task.doRun(Task.java:721)</span><br><span class="line">org.apache.flink.runtime.taskmanager.Task.run(Task.java:546)</span><br><span class="line">java.lang.Thread.run(Thread.java:748)</span><br><span class="line"></span><br><span class="line">2021-03-02 15:50:29,065 WARN org.apache.flink.runtime.taskmanager.Task [] - Task 'Source: 日数据流,GameId=17 (1/1)' did not react to cancelling signal for 30 seconds, but is stuck in method:</span><br><span class="line"> ru.yandex.clickhouse.ClickHouseDriver.<clinit>(ClickHouseDriver.java:38)</span><br><span class="line">java.lang.Class.forName0(Native Method)</span><br><span class="line">java.lang.Class.forName(Class.java:264)</span><br><span class="line">org.apache.flink.connector.jdbc.internal.connection.SimpleJdbcConnectionProvider.getConnection(SimpleJdbcConnectionProvider.java:52)</span><br><span class="line">com.funjoy.flink.DailySourceFunction.open(DailySourceFunction.java:69)</span><br><span class="line">org.apache.flink.api.common.functions.util.FunctionUtils.openFunction(FunctionUtils.java:36)</span><br><span class="line">org.apache.flink.streaming.api.operators.AbstractUdfStreamOperator.open(AbstractUdfStreamOperator.java:102)</span><br><span class="line">org.apache.flink.streaming.runtime.tasks.OperatorChain.initializeStateAndOpenOperators(OperatorChain.java:291)</span><br><span class="line">org.apache.flink.streaming.runtime.tasks.StreamTask.lambda$beforeInvoke$1(StreamTask.java:506)</span><br><span class="line">org.apache.flink.streaming.runtime.tasks.StreamTask$$Lambda$621/13894355.run(Unknown Source)</span><br><span class="line">org.apache.flink.streaming.runtime.tasks.StreamTaskActionExecutor$SynchronizedStreamTaskActionExecutor.runThrowing(StreamTaskActionExecutor.java:92)</span><br><span class="line">org.apache.flink.streaming.runtime.tasks.StreamTask.beforeInvoke(StreamTask.java:475)</span><br><span class="line">org.apache.flink.streaming.runtime.tasks.StreamTask.invoke(StreamTask.java:526)</span><br><span class="line">org.apache.flink.runtime.taskmanager.Task.doRun(Task.java:721)</span><br><span class="line">org.apache.flink.runtime.taskmanager.Task.run(Task.java:546)</span><br><span class="line">java.lang.Thread.run(Thread.java:748)</span><br></pre></td></tr></table></figure><p>这么两个 waring, 基本上符合我们的故障现象,</p><h2 id="怀疑是因为建立-clickhouse-连接时卡住了"><a href="#怀疑是因为建立-clickhouse-连接时卡住了" class="headerlink" title="怀疑是因为建立 clickhouse 连接时卡住了"></a>怀疑是因为建立 clickhouse 连接时卡住了</h2><p>因为 job 一直卡住了, 所以在执行 cancel 时无法处理, 超过 30 秒后直接出现 waring</p><p>通过上面的堆栈, 发现是获取 clickhouse 和 mariadb 连接时出现的问题</p><p>将连接 clickhouse 相关的代码注释后就正常了<br>所以基本上确定故障表现和连接 clickhouse 有一定关系.</p><p>在卡住的时候我们通过其他客户端连接 clickhouse 正常, 所以故障和 clickhouse 服务无关</p><p>我们找了阿里云的专家帮忙, 查看了 taskmanager 和 jobmanager 的日志后<br>建议我们在 checkpoint 卡住的时候用<code>jstack</code>查看下线程在做什么</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">Source: 日数据流,GameId=17 (1/1)" #62 prio=5 os_prio=0 tid=0x00007f3da008d000 nid=0x156 in Object.wait() [0x00007f3e48fcb000]</span><br><span class="line"> java.lang.Thread.State: RUNNABLE</span><br><span class="line"> at ru.yandex.clickhouse.ClickHouseDriver.<clinit>(ClickHouseDriver.java:38)</span><br><span class="line"> at java.lang.Class.forName0(Native Method)</span><br><span class="line"> at java.lang.Class.forName(Class.java:264)</span><br><span class="line"> at org.apache.flink.connector.jdbc.internal.connection.SimpleJdbcConnectionProvider.getConnection(SimpleJdbcConnectionProvider.java:52)</span><br><span class="line"> - locked <0x00000007db8a3bf8> (a org.apache.flink.connector.jdbc.internal.connection.SimpleJdbcConnectionProvider)</span><br><span class="line"> at com.funjoy.flink.DailySourceFunction.open(DailySourceFunction.java:69)</span><br><span class="line"> at org.apache.flink.api.common.functions.util.FunctionUtils.openFunction(FunctionUtils.java:36)</span><br><span class="line"> at org.apache.flink.streaming.api.operators.AbstractUdfStreamOperator.open(AbstractUdfStreamOperator.java:102)</span><br><span class="line"> at org.apache.flink.streaming.runtime.tasks.OperatorChain.initializeStateAndOpenOperators(OperatorChain.java:291)</span><br><span class="line"> at org.apache.flink.streaming.runtime.tasks.StreamTask.lambda$beforeInvoke$1(StreamTask.java:506)</span><br><span class="line"> at org.apache.flink.streaming.runtime.tasks.StreamTask$$Lambda$617/1628357580.run(Unknown Source)</span><br><span class="line"> at org.apache.flink.streaming.runtime.tasks.StreamTaskActionExecutor$SynchronizedStreamTaskActionExecutor.runThrowing(StreamTaskActionExecutor.java:92)</span><br><span class="line"> - locked <0x00000007db8a3e00> (a java.lang.Object)</span><br><span class="line"> at org.apache.flink.streaming.runtime.tasks.StreamTask.beforeInvoke(StreamTask.java:475)</span><br><span class="line"> at org.apache.flink.streaming.runtime.tasks.StreamTask.invoke(StreamTask.java:526)</span><br><span class="line"> at org.apache.flink.runtime.taskmanager.Task.doRun(Task.java:721)</span><br><span class="line"> at org.apache.flink.runtime.taskmanager.Task.run(Task.java:546)</span><br><span class="line"> at java.lang.Thread.run(Thread.java:748)</span><br></pre></td></tr></table></figure><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line">Source: 配置广播流,GameId=17 (1/1)" #63 prio=5 os_prio=0 tid=0x00007f3da008f000 nid=0x157 in Object.wait() [0x00007f3e48ec8000]</span><br><span class="line"> java.lang.Thread.State: RUNNABLE</span><br><span class="line"> at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)</span><br><span class="line"> at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)</span><br><span class="line"> at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)</span><br><span class="line"> at java.lang.reflect.Constructor.newInstance(Constructor.java:423)</span><br><span class="line"> at java.lang.Class.newInstance(Class.java:442)</span><br><span class="line"> at java.util.ServiceLoader$LazyIterator.nextService(ServiceLoader.java:380)</span><br><span class="line"> at java.util.ServiceLoader$LazyIterator.next(ServiceLoader.java:404)</span><br><span class="line"> at java.util.ServiceLoader$1.next(ServiceLoader.java:480)</span><br><span class="line"> at java.sql.DriverManager$2.run(DriverManager.java:603)</span><br><span class="line"> at java.sql.DriverManager$2.run(DriverManager.java:583)</span><br><span class="line"> at java.security.AccessController.doPrivileged(Native Method)</span><br><span class="line"> at java.sql.DriverManager.loadInitialDrivers(DriverManager.java:583)</span><br><span class="line"> at java.sql.DriverManager.<clinit>(DriverManager.java:101)</span><br><span class="line"> at org.mariadb.jdbc.Driver.<clinit>(Driver.java:70)</span><br><span class="line"> at java.lang.Class.forName0(Native Method)</span><br><span class="line"> at java.lang.Class.forName(Class.java:264)</span><br><span class="line"> at org.apache.flink.connector.jdbc.internal.connection.SimpleJdbcConnectionProvider.getConnection(SimpleJdbcConnectionProvider.java:52)</span><br><span class="line"> - locked <0x00000007db8a35c0> (a org.apache.flink.connector.jdbc.internal.connection.SimpleJdbcConnectionProvider)</span><br><span class="line"> at com.funjoy.flink.ConfigSourceFunction.open(ConfigSourceFunction.java:52)</span><br><span class="line"> at org.apache.flink.api.common.functions.util.FunctionUtils.openFunction(FunctionUtils.java:36)</span><br><span class="line"> at org.apache.flink.streaming.api.operators.AbstractUdfStreamOperator.open(AbstractUdfStreamOperator.java:102)</span><br><span class="line"> at org.apache.flink.streaming.runtime.tasks.OperatorChain.initializeStateAndOpenOperators(OperatorChain.java:291)</span><br><span class="line"> at org.apache.flink.streaming.runtime.tasks.StreamTask.lambda$beforeInvoke$1(StreamTask.java:506)</span><br><span class="line"> at org.apache.flink.streaming.runtime.tasks.StreamTask$$Lambda$617/1628357580.run(Unknown Source)</span><br><span class="line"> at org.apache.flink.streaming.runtime.tasks.StreamTaskActionExecutor$SynchronizedStreamTaskActionExecutor.runThrowing(StreamTaskActionExecutor.java:92)</span><br><span class="line"> - locked <0x00000007db8a37a0> (a java.lang.Object)</span><br><span class="line"> at org.apache.flink.streaming.runtime.tasks.StreamTask.beforeInvoke(StreamTask.java:475)</span><br><span class="line"> at org.apache.flink.streaming.runtime.tasks.StreamTask.invoke(StreamTask.java:526)</span><br><span class="line"> at org.apache.flink.runtime.taskmanager.Task.doRun(Task.java:721)</span><br><span class="line"> at org.apache.flink.runtime.taskmanager.Task.run(Task.java:546)</span><br><span class="line"> at java.lang.Thread.run(Thread.java:748)</span><br></pre></td></tr></table></figure><h1 id="定位问题"><a href="#定位问题" class="headerlink" title="定位问题"></a>定位问题</h1><p>从上面的堆栈来看和 waring 的信息一样, 可以确认线程确实卡在这了</p><p>而且是和 clickhouse 连接有关系</p><h2 id="猜测-clickhouse-jdbc-连接库的问题"><a href="#猜测-clickhouse-jdbc-连接库的问题" class="headerlink" title="猜测: clickhouse-jdbc 连接库的问题"></a>猜测: clickhouse-jdbc 连接库的问题</h2><p>将 clickhouse-jdbc:0.2.4 升级到 clickhouse-jdbc:0.2.6 后故障依旧</p><p>既然不是 clickhouse 库的问题, 那就再往上一层看看</p><p>查看 SimpleJdbcConnectionProvider 的类是 flink-connector-jdbc 包中定义</p><p>这个包是和 flink 的版本保持一致的, 所以直接将 flink 从 1.11.3 升级到 1.12.1</p><p><code>测试了十几次都表现正常, 应该就是这个问题了</code></p><p>在<a href="https://github.com/apache/flink/commit/84ed65356fe61e6ba74a7e4c4aef0dbd3c63f44a#diff-6f7c06757b08d5926f28b33a47ce6463ce1d221470cf8ce053a207de15f2c156" target="_blank" rel="noopener">github</a>上找到 flink 1.12.1 版本的 SimpleJdbcConnectionProvider 变更记录</p><p>发现确实有这个 bug, 已经有大佬修复了这个问题.</p><p>至此整个故障问题已经确认</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>一开始应该关注日志方面, 但是研发一直纠结是配置和环境的问题<br>所以在配置和环境上花费了很多时间.</p><p>对 java 不太熟悉, 从日志中的两个 waring 应该能定位到问题.</p><p>故障表现起来有一定的随机性, 所有验证起来很浪费时间</p>]]></content>
<summary type="html">
<h1 id="故障表现"><a href="#故障表现" class="headerlink" title="故障表现"></a>故障表现</h1><p>在启动 job 时随机出现 checkpoint 无法完成的情况</p>
<p>但只要 job 能够正常跑起来就一直都没有问题</p>
<p>没有任何的错误日志</p>
<p><img src="/resource/24eab53b368c4632b4220c7fa11e7770.png" alt="ce95965764a3fe6be318253fed181a72.png"></p>
<p><img src="/resource/0ed70bc5087040f7bd36e520e636e130.png" alt="b6ec6decddba84e326d58318e69d1817.png"></p>
</summary>
<category term="故障" scheme="https://zhiwei.show/tags/%E6%95%85%E9%9A%9C/"/>
<category term="flink" scheme="https://zhiwei.show/tags/flink/"/>
</entry>
<entry>
<title>使用openresty构建的外部测试网关</title>
<link href="https://zhiwei.show/78b08057401441c8bd6c141f7665a0da/"/>
<id>https://zhiwei.show/78b08057401441c8bd6c141f7665a0da/</id>
<published>2020-12-10T01:33:00.000Z</published>
<updated>2021-03-10T01:33:00.000Z</updated>
<content type="html"><![CDATA[<h2 id="需求"><a href="#需求" class="headerlink" title="需求"></a>需求</h2><p>H5 游戏研发公司<br>需要将 H5 项目的 URL 给合作方进行测试,</p><h2 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h2><p>避免游戏地址被外部传播</p><h2 id="方案"><a href="#方案" class="headerlink" title="方案"></a>方案</h2><p>使用 openrestry 作为反向代理<br>在后台通过对项目地址, 过期时间等信息进行对称加密.<br>生成一个 url 链接,然后发送给合作方</p><p>合作方使用浏览器打开 url 时, openresty 会收到 http 请求<br>并对 url 参数中的加密信息进行解密, 获取项目地址,过期时间等相关的信息</p><p>如果当前时间大于过期时间则返回相应过期提示<br>如果当前时间小于过期时间则通过 proxy_pass 进行代理请求</p><p>大致的情况就是这样了</p><p>client—-gateway url—->openresty—-project url—->project<br>client<—-response data—-openresty<—-response data—-project</p><a id="more"></a><h2 id="续期"><a href="#续期" class="headerlink" title="续期"></a>续期</h2><p>一直相安无事, 突然有了一个需求<br>过期时间在生成链接时是确定的<br>但是合作方可能还想在体验一下</p><p>简单的方法就是直接重新生成一个新链接<br>但是体验很不好, 因为合作方可能有多个人在测试<br>这样的话, 就需要将新链接通知到 N 个人<br>麻烦且低效</p><h2 id="方案-1"><a href="#方案-1" class="headerlink" title="方案 1"></a>方案 1</h2><p>所以我们需要找到一个方法能对发出去的 url 进行续期操作<br>先想到了一个方法, 对每个生成的 url 都有一个 id<br>续期时直接传送一个参数 id 来定位一个 url, 然后进行相应的操作<br>这样 openresty 这边就需要构建一个数据库将所有的 url 记录下来</p><p>原本 openresty 这只是一个过期的判断<br>如果这样修改则需要生成一堆的数据信息<br>能不能简单点</p><h2 id="方案-2"><a href="#方案-2" class="headerlink" title="方案 2"></a>方案 2</h2><p>我们可以直接将 url 的加密信息作为 key<br>存在 redis 中, 然后设置这个 key 的过期时间<br>这样既能满足需求,逻辑简单,改动也少</p><p>请求 url 代理的逻辑</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">在redis中查找是否有以"加密信息"为key的value</span><br><span class="line">如果存在</span><br><span class="line">进行代理</span><br><span class="line">否则</span><br><span class="line">将加密信息解密</span><br><span class="line">对过期时间进行判断</span><br><span class="line">如果过期</span><br><span class="line">返回过期提示</span><br><span class="line">否则</span><br><span class="line">进行代理</span><br></pre></td></tr></table></figure><p>设置 url 续期逻辑</p><p>直接将 url 的加密信息作为 redis 的 key 存储起来,并设置相应的 ttl</p><h2 id="openresty-请求阶段问题"><a href="#openresty-请求阶段问题" class="headerlink" title="openresty 请求阶段问题"></a>openresty 请求阶段问题</h2><p><img src="/resource/4c9391d9f71d4e42b1632b80bfd725e6.png" alt="4b3e6bc0d0331001c26567cfcc68a8b9.png"></p><p>因为需要使用 pass_proxy 对项目 url 进行代理<br>所以我们在 set_by_lua 阶段对请求的 url 进行解密来获取项目的 URL 地址<br>并且直接在 set_by_lua 中进行判断是否过期(本质上是个时间比较)</p><p>但是在 set_by_lua 阶段不能访问 redis 的外部服务(因为性能)<br>所以最后我们先在 set_by_lua 阶段生成代理的 url<br>然后在 access_by_lua 阶段判断请求的 url 是否过期</p><p>以上是我们在外部测试网关管理的问题和总结</p>]]></content>
<summary type="html">
<h2 id="需求"><a href="#需求" class="headerlink" title="需求"></a>需求</h2><p>H5 游戏研发公司<br>需要将 H5 项目的 URL 给合作方进行测试,</p>
<h2 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h2><p>避免游戏地址被外部传播</p>
<h2 id="方案"><a href="#方案" class="headerlink" title="方案"></a>方案</h2><p>使用 openrestry 作为反向代理<br>在后台通过对项目地址, 过期时间等信息进行对称加密.<br>生成一个 url 链接,然后发送给合作方</p>
<p>合作方使用浏览器打开 url 时, openresty 会收到 http 请求<br>并对 url 参数中的加密信息进行解密, 获取项目地址,过期时间等相关的信息</p>
<p>如果当前时间大于过期时间则返回相应过期提示<br>如果当前时间小于过期时间则通过 proxy_pass 进行代理请求</p>
<p>大致的情况就是这样了</p>
<p>client—-gateway url—-&gt;openresty—-project url—-&gt;project<br>client&lt;—-response data—-openresty&lt;—-response data—-project</p>
</summary>
<category term="openresty" scheme="https://zhiwei.show/tags/openresty/"/>
</entry>
<entry>
<title>游戏业务数据库集中式储存的问题</title>
<link href="https://zhiwei.show/4ec7a130f7bb4ac48dfae71508b462e7/"/>
<id>https://zhiwei.show/4ec7a130f7bb4ac48dfae71508b462e7/</id>
<published>2020-09-26T20:24:00.000Z</published>
<updated>2020-09-26T20:24:00.000Z</updated>
<content type="html"><![CDATA[<h1 id="问题背景"><a href="#问题背景" class="headerlink" title="问题背景"></a>问题背景</h1><p>之前有提到过, 我们将游戏本地的数据库集中起来进行存储.</p><p>在整体方面是有了大幅度的提升, 包括迁移, 监控, 高可用, 以及备份.</p><p>尤其时在迁移方面, 我们将单个服<code>10-30</code>分钟的迁移时间压缩到了<code>1-3</code>分钟(不用迁移数据库).</p><p>然而, 之前的的数据库瓶颈分布在 5,6 台主机上, 现在却集中在一台 4 核 16G 的主机上.</p><p>这明显会产生其他的问题, 比如我们处理过的 CPU 高突发以及 binlog 数据量过大.</p><p>以及目前碰到了二个新问题</p><a id="more"></a><h1 id="新增字段"><a href="#新增字段" class="headerlink" title="新增字段"></a>新增字段</h1><p>游戏启动时, 会对数据库的表进行检测, 如果不存在则进行新增字段</p><p>所以我们更新维护时间会特别长, 之前数据都是分布在游戏服上的</p><p>所以整体上能在一个小时内全部解决.</p><p>但是将数据库独立出来集中存储后, 突然到了某个临界值, 问题就来了.</p><p>因为使用的云数据库, 所以只有看到数据层面的监控信息.</p><p>从监控上看是因为 CPU 过高, 所以当时就觉得是 CPU 瓶颈.</p><p>看了下慢查询的日志, 发现大多数都是 update 全表的 sql 语句.</p><p>找研发问了问什么情况, 得到答复是为了保证数据的一致性.</p><p>在新增字段后后对整表进行一次 update 来初始化字段.</p><p>因为使用的是 BLOB 字段, 所以无法设置初始值.</p><p>当时我还真就以为是全表的 update 导致维护时间边长.</p><p>再三的和研发沟通, 看看是否能避免这个操作, 然而并不能.</p><p>另外, 有个问题一直没想明白.</p><p>我们大概有 100 多个数据库, 同时加字段, 然后全表 update</p><p>从 10:30 一直到 16:30, 都没有弄完.</p><p>我们只能把全停掉, 然后 10 个 10 个一组, 才能正常的完成启动.</p><p>什么原因, 数据的总量是不变的, 而且数据库都是独立的(一台服务器), 所以应该不存在锁的问题.</p><p>大概了解了一下 mysql 加字段原理,</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">1. 首先, 对原始表结构COPY了一份</span><br><span class="line">2. 然后对拷贝出来的表结构进行新增字段</span><br><span class="line">3. 接着将原始表中的数据复制到拷贝出现的新表.</span><br><span class="line">4. 最后锁表, 删除原表, 将新表改名成原表.</span><br></pre></td></tr></table></figure><p>因为云数据库没法看到 I/O 的情况, 所以我们只能在本地进行测试</p><p>发现新增字段的过程涉及到大量的 I/O 操作.</p><p>在<code>stackoverflow</code>上看了下, 有几个解决方案</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">1. pt-online-schema-change</span><br><span class="line"></span><br><span class="line">这个适合在线业务加字段, 能够在不影响业务的情况下实现修改表结构</span><br><span class="line">在copy新表的时候通过insert,update,delete触发器来对新表进行同步.</span><br><span class="line">需要满足一定的条件才能使用, 不适合我们</span><br><span class="line"></span><br><span class="line">2. mysql8.0的instant</span><br><span class="line"></span><br><span class="line">我们使用的云数据库是5.7的, 也不可能升级到8.0, 怕HOLD不住</span><br><span class="line"></span><br><span class="line">3. sql操作前置</span><br><span class="line"></span><br><span class="line">就是将需要新增的字段在人少的时候进行操作, 这个需要更新流程中插入</span><br><span class="line">并且研发也需要提前给出需要新增字段的sql语句, 谈崩.</span><br></pre></td></tr></table></figure><p>最后我们只能将并发的启动改成顺序的启动, 从而避免这个问题.</p><p>但是到底为什么会卡死还没有弄明白, 有机会在研究研究.</p><h1 id="limit-的瓶颈"><a href="#limit-的瓶颈" class="headerlink" title="limit 的瓶颈"></a>limit 的瓶颈</h1><p>需求背景是</p><p>A,B 两个数据库, 要将 A,B 数据库某些表的数据全部取出来, 然后将 B 合并到 A 上.</p><p>需要对冲突的数据进行一些处理, 冲突处理的时间先忽略不计, 数据提取的逻辑是这样的</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">total=select count(*) from A</span><br><span class="line"></span><br><span class="line">for (i=0;i<total;i+=10) {</span><br><span class="line"> offset=$i</span><br><span class="line"> select * from A limit $offset 10</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>因为数据合并很慢, 所以需要找找瓶颈.</p><p>看了下 mysql 的慢日志, 一条语句需要跑 100s</p><p>从云数据库的监控上来看, CPU, qps, 逻辑读和物理读并没有达到瓶颈.</p><p>但是在云数据库上又没法看到 I/O 的情况, 所以当时以为就是 I/O 的问题.</p><p>因为这个 SQL 写法逻辑上就很奇怪, 明明需要获取所有的数据.</p><p>但是却使用 limit 来分批获取, 导致 mysql 每次都要重新读取磁盘的数据.</p><p>和研发沟通几次调整修改, 就是不肯, 非说这是上了云数据库导致的问题.</p><p>要让我们从云数据库上优化, 这就很尴尬了.</p><p>我们只能找云厂商帮忙看看有没其他方法可以优化优化.</p><p>专业的 DBA 一下就看出我们这个 sql 语句有内鬼.</p><p>这里也触及到我的一个知识盲区.</p><p>既然程序一直再跑, 但是 CPU 和 I/O 又没跑满.</p><p>那到底是什么原因导致吞吐量一直上不去.</p><p>就是资源又没跑满, 但是吞吐量却上不去.</p><p>没办法, 只能在本地数据库上进行模拟测试看看.</p><p>经过实践发现, 瓶颈确实在 CPU 上, 之所以 CPU 没跑满</p><p>是因为一条 SQL 语句只占用一个 CPU, 也就是说</p><p>这个这个 sql 分配到一个 CPU 上, 即使这个 SQL 火力全开, 也得 100s 才能执行完成.</p><p>但是执行 sql 又不能利用多核, 毕竟人家不是分布式的数据库.</p><p>那么问题又来了, 总过也才 20000 条的数据.</p><p>将数据从磁盘 load 到内存并没有花费太多的时间</p><p>难道你一个 CPU 在内存遍历 20000 条数据还要按秒算?</p><p>根据数据库的慢日志发现有意思的现象或许可以解释解释</p><p>有 a 表和 b 表, a 表是记录数 20000 左右, b 表记录数 2000000 左右.</p><p>a 是宽表, 单条记录很大, b 表窄表, 单条记录很小</p><p>但是<code>select * from a limit 18000 10</code>比<code>select * from b limit 1800000 10</code>要更慢.</p><p>这就说明 sql 执行的时间不只<code>和offset有关,</code> 还<code>和单条记录的大小有关</code>.</p><p>只是有点想不明白, 记录与记录之间不是应该用链表连接的吗?</p><p>为啥单条记录大, 遍历时间就长, 而单条记录小, 遍历时间就短呢.</p><p>受限于知识水平和时间因素, 暂时搞不明白, 而且还有其他更重要的事, 只能先记录下来.</p><p>扯回来, mysql 的 limit 并不是只消耗取 10 条的数据的时间.</p><p>他要将所有数据全部取出来, 然后过滤掉前 offset 条数据, 只返回接下来的 10 条数据</p><p>可想而知, 当 offset 数越大, 消耗的时候就越多.</p><p>原本是个很简单的问题, 结果研发却硬说是云数据库的问题.</p><p>因为之前在本地数据库上操作是没有这个问题的, 真的是成功的气到我了.</p><p>然后我们只能将数据库复制到本地上, 然后执行同样的合服操作.</p><p>结果证明, 本地的更慢, 确当了问题, 就很好处理了.</p><p>只要避免全表扫描, 就能提高性能, 比较简单的方式是根据主键使用 where 过滤.</p><p>走索引比全表扫描快 N 多倍, 之前一条 sql 需要 100s, 走索引后只需 0.1s</p><p>至于为什么不把数据全部取出来处理, 据说是因为框架不支持.</p><p>我也没细问, 这又是另一个故事了.</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">1. 上云数据库有得有失吧, 感觉是利大于弊, 应该确实暴露出很多不合理的使用</span><br><span class="line">2. 对mysql原理方面还有待提高, 以及计算机系统方面也有些理解的不到位</span><br><span class="line">3. 业务上有很大的提升, 之前数据合并需要1个多小时, 现在可能只需要几分钟</span><br><span class="line">4. 自己也将学到的知识点结合实际进行验证, 但第一次没有对思路进行验证, 这个需要注意</span><br></pre></td></tr></table></figure>]]></content>
<summary type="html">
<h1 id="问题背景"><a href="#问题背景" class="headerlink" title="问题背景"></a>问题背景</h1><p>之前有提到过, 我们将游戏本地的数据库集中起来进行存储.</p>
<p>在整体方面是有了大幅度的提升, 包括迁移, 监控, 高可用, 以及备份.</p>
<p>尤其时在迁移方面, 我们将单个服<code>10-30</code>分钟的迁移时间压缩到了<code>1-3</code>分钟(不用迁移数据库).</p>
<p>然而, 之前的的数据库瓶颈分布在 5,6 台主机上, 现在却集中在一台 4 核 16G 的主机上.</p>
<p>这明显会产生其他的问题, 比如我们处理过的 CPU 高突发以及 binlog 数据量过大.</p>
<p>以及目前碰到了二个新问题</p>
</summary>
<category term="mysql" scheme="https://zhiwei.show/tags/mysql/"/>
</entry>
<entry>
<title>本地数据库迁移至云数据库的复盘总结</title>
<link href="https://zhiwei.show/4da7bf5f40c24ab385e61585da25c951/"/>
<id>https://zhiwei.show/4da7bf5f40c24ab385e61585da25c951/</id>
<published>2020-06-25T21:15:00.000Z</published>
<updated>2020-06-25T21:15:00.000Z</updated>
<content type="html"><![CDATA[<h1 id="前戏"><a href="#前戏" class="headerlink" title="前戏"></a>前戏</h1><p>其实一开始, 写这东西我其实是拒绝的.</p><p>因为感觉没有多少技术含量, 没法体现出我牛逼的地方.</p><p>后来想了想, 很多思想和方法还是挺不错, 可以深挖和借鉴.</p><p>所以就记录下来, 方便以后回顾.</p><p>以上.</p><h1 id="问题背景"><a href="#问题背景" class="headerlink" title="问题背景"></a>问题背景</h1><ol><li>本地数据库使用 5.6 的 MyISAM 引擎.</li><li>业务没有使用事务, 存储过程, join, 之类的常规关系数据查询</li><li>直接粗暴简单的将用户数据序列化 json 后存在多个 BLOBTEXT 字段里.</li><li>每个小时的整点进行锁表备份, 导致 CPU 的间隔性的突发.</li></ol><p>我们需要做的是在<code>效率</code>, <code>安全</code>, <code>成本</code>之间做平衡</p><h2 id="迁移"><a href="#迁移" class="headerlink" title="迁移"></a>迁移</h2><p>在游戏服导量过后的一段时间.<br>需要定期的对游戏服进行迁移.<br>以提高资源使用率从而降低成本.</p><p>数据库在本地时, 迁移需要对数据库进行导入导出操作<br>既不安全, 效率也低.</p><h2 id="备份"><a href="#备份" class="headerlink" title="备份"></a>备份</h2><p>本地的数据库使用是的每小时备份.<br>在备份时会占用本地主机的资源, 影响用户体验.</p><h2 id="性能"><a href="#性能" class="headerlink" title="性能"></a>性能</h2><p>本地数据库都是机械磁盘<br>同时大批量的停止和启动服务时会有大量的数据库操作<br>这些操作大都是磁盘的瓶颈, 拉长了维护的时间</p><h2 id="回档"><a href="#回档" class="headerlink" title="回档"></a>回档</h2><p>当研发需要查询某个时间的数据时只能按每小时回档</p><h2 id="崩溃"><a href="#崩溃" class="headerlink" title="崩溃"></a>崩溃</h2><p>没有对数据库做高可用, 当数据库崩溃时影响业务的数据安全</p><a id="more"></a><h1 id="解决思路"><a href="#解决思路" class="headerlink" title="解决思路"></a>解决思路</h1><p>在之前项目中, 使用高性能的机器, 搭建 mysql 主从来解决数据存储的问题</p><p>现在云设施更完善了, 我们可以通过云数据库来达到目的</p><p>云数据库帮我们解决了高可用, 监控, 备份, 安全</p><p>这其中任何一项拿来出来都够我们喝一壶的</p><h2 id="验证兼容问题"><a href="#验证兼容问题" class="headerlink" title="验证兼容问题"></a>验证兼容问题</h2><p>我们之前使用的是 mysql5.6 的 MyISAM 引擎</p><p>而云数据库上只支持 Innodb</p><p>虽说两者主要是事务的差别, 但不清楚项目使用上是否有什么骚操作</p><p>所以我们先将一些没有活跃用户的小平台迁移至云数据库</p><p>运行了一周, 看看是否有数据库兼容的问题</p><p>这个过程很顺利, 并没有遇到兼容性的问题</p><p>迁移过程没有错误, 服务端后台的日志没有错误, 没有用户反馈数据异常.</p><h2 id="验证性能问题"><a href="#验证性能问题" class="headerlink" title="验证性能问题"></a>验证性能问题</h2><p>一个非常基本的问题.</p><p>如果所有游戏服迁移需要多少个云数据库承载.</p><p>涉及到业务的写入机制, 活跃用户, 业务性能, 数据安全多方面的因素.</p><p>所以我们借助数据库的监控系统来分析业务的性能问题.</p><p>经过上线一周来看, 主要有两个问题</p><ol><li>CPU 间隔性突发</li><li>BINLOG 日志过多</li></ol><h3 id="30-分钟的-CPU-间隔性突发排查"><a href="#30-分钟的-CPU-间隔性突发排查" class="headerlink" title="30 分钟的 CPU 间隔性突发排查"></a>30 分钟的 CPU 间隔性突发排查</h3><p>CPU 的间隔突发, 主要影响承载游戏服的数量</p><p>所以我们需要做的是找到 CPU 突发的原因, 然后削峰</p><p><img src="/resource/046b36a384464532adb1c94d2381ff81.png" alt="917d9b017ca6d96a834732842fed2c70.png"></p><p><img src="/resource/eeaa8901031547cc99e47c7a359278de.png" alt="595778a9c55e48204ac73e691e509b2f.png"></p><p>(左边的低突发事因为迁移游戏服的数据库较少)</p><p>从监控数据来看, 突发都是每半个小时一次</p><p>有个奇怪的地方, 在 03:00-04:00 的时候居然也存在 30 分钟的 CPU 突发</p><p>这种明显是不正常的, 毕竟在线的用户少了, 居然还占用这么多资源.</p><p>应该是类似的于活动之类的, 但我们的活动都是在 30/60 分钟之类的时间点</p><p>我们先找到突发点时间范围的云数据库 sql 的审计(真香), 大量的 sql 语句都是类似于</p><p><code>UPDATE items set field1 = '...' where id = '...'</code></p><p>虽然知道了是哪个表导致的, 但是没法知道是哪个业务系统引起的大量<code>UPDATE</code>语句.</p><p>在数据库层面没什么办法了, 只能从业务代码入手.</p><h4 id="Sql-注释法"><a href="#Sql-注释法" class="headerlink" title="Sql 注释法"></a>Sql 注释法</h4><p>有个思路, 通过 Sql 注释的方式将 Sql 的来源写入数据库.</p><p>类似 <code>UPDATE /* SYSTEM_1 */ TABLES set field1 = '...' where id = '...'</code></p><p>和研发沟通后无法实现, 因为 sql 的执行时在数据层, 业务系统在逻辑层</p><p>在数据层执行 sql 存储时无法获取逻辑层的信息, 改动的范围太大, 成本过高</p><h4 id="打印堆栈法"><a href="#打印堆栈法" class="headerlink" title="打印堆栈法"></a>打印堆栈法</h4><p>简单来说, 在数据层入口函数处打印出调用的堆栈信息.</p><p>然后根据统计堆栈的信息进行排查.</p><p>然而还是有点问题, 每个业务系统调用数据存储的方式都不同</p><p>导致打印出的堆栈有不能反映出我们想要的数据.</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"># 已做混淆</span><br><span class="line"></span><br><span class="line">18543 modules/activity/activityMgr.lua:61</span><br><span class="line"> 6038 player/Player.lua:749:</span><br><span class="line"> 1733 service/DataPack.lua:314:</span><br><span class="line"> 1037 service/DataPack.lua:795:</span><br><span class="line"> 851 modules/global/globalwarMgr.lua:1642:</span><br><span class="line"> 847 service/PlatMgr.lua:28:</span><br><span class="line"> 253 player/PlayerMgr.lua:197:</span><br></pre></td></tr></table></figure><p>从上面的统计来看, 活动模块调用的次数最高.</p><p>然后我们再从<code>activityMgr.lua:61</code>来统计, 看看谁调用的次数最多.</p><p>经过 2,3 次的统计, 终于找到了源头</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"># 已做混淆</span><br><span class="line"></span><br><span class="line">18543 modules/difu/difuMgr.lua:61</span><br><span class="line"> 6038 player/PlayerMgr.lua:749</span><br><span class="line"> 1733 service/DataPack.lua:314</span><br><span class="line"> 1037 service/DataPack.lua:795</span><br><span class="line"> 851 modules/global/globalMgr.lua:1642</span><br><span class="line"> 847 service/PlatMgr.lua:28</span><br><span class="line"> 253 player/PlayerMgr.lua:197</span><br></pre></td></tr></table></figure><p>最终和研发沟通后发现逻辑确实有点问题</p><p>活动每半个小时会遍历所有的玩家, 而业务需求上只需要查询一次即可</p><p>修复之后观察监控数据.</p><p><img src="/resource/074964b97f9e4437b884fbd2f5f5f2d2.png" alt="c0e5a4a027cfe9e9e1b701e8e0ce843c.png"></p><p><img src="/resource/f8900adb2fe74dc9a62e2b1cc136f3cd.png" alt="1fefa382f76e0c0d2533cf01b8f8f319.png"></p><h3 id="10-分钟的-CPU-间隔突发排查"><a href="#10-分钟的-CPU-间隔突发排查" class="headerlink" title="10 分钟的 CPU 间隔突发排查"></a>10 分钟的 CPU 间隔突发排查</h3><p>虽然把 30 分钟 CPU 突发给解决了, 但是在监控上发现, 还是有一些 10 分间隔 CPU 的突发点</p><p>这个问题相对好排查一些, 通过云数据库审计系统统计出的 SQL 语句</p><p>大部分都是<code>UPDATE mails SET content = '...' where id = '...'</code></p><p>从 Sql 来看明显能够定位到具体的业务系统</p><p>通过 Sql 发现大量的邮件内容都是充值 0.01 元导致的</p><p>那么问题来了, 为什么充值 0.01 元会导致 CPU 突发?</p><p>说来也很是蛋疼的很啊, 系统在设计时, 有这么个逻辑</p><p>如果一个玩家登陆, 会将所有的邮件装载到内存里, 避免玩家查邮件因为读数据库卡顿导致操作延迟</p><p>如果这个玩家下线, 那么所有在内存里的数据都要强制进行一次数据库写入操作.</p><p>恰好, 我们有个用来测试的机器人, 每 10 分钟会在每个游戏服上找个长期(1 个月)没有登陆的玩家</p><p>进行登陆测试和充值测试, 这就导致每个游戏服, 每十分钟会有个玩家登陆, 下线, 写数据库.</p><p>并且这个操作是立刻的, 所以当有 100 个游戏服的时候就会有 100 个写入操作.</p><p>我们默认只保留 15 天的邮件, 按机器人 10 分钟 1 封, <code>60 * 24 * 15 / 10 = 2160</code></p><p>一个游戏服一次登陆需要读取 2160 封邮件, 然后再写入 2160 封邮件</p><p>如果有 100 个游戏服, 那就是<code>2160 * 100 = 216000</code>封邮件, CPU 就是这样被玩坏的.</p><p>一开始, 我们想把机器人的测试时间加个随机数, 错开高峰.</p><p>但是想想觉得这并不是一个合理的操作, 测试的邮件都是无用的数据.</p><p>频繁的读取和写入, 大量的浪费资源, 影响性能.</p><p>最后我们决定每次进行 0.01 元充值时, 将机器人登陆邮件全部清空.</p><p>这样就避免的测试时大量的无效操作, 观察监控后, CPU 有所下滑了.</p><p><img src="/resource/89d4e0bc8cc34089a8eb22a7b97011dc.png" alt="3e9f99ac24a8b7409fb3980fed105f3c.png"></p><h3 id="BINLOG-日志过多排查"><a href="#BINLOG-日志过多排查" class="headerlink" title="BINLOG 日志过多排查"></a>BINLOG 日志过多排查</h3><p>CPU 突发的问题我们差不多都解决了</p><p>还有一些突发点是业务需求和修复成本导致的</p><p>所以先观察观察, 接着来看看 BINLOG 日志的问题</p><h4 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h4><p>游戏的架构通常都是在服务内存存储着玩家的数据作为缓存.</p><p>所以的数据更新都在缓存进行, 然后定时同步到数据库里.</p><p>如果在更新缓存后发生了宕机(系统层和应用层), 就会出现传说中的回档.</p><p>缺点就是每次写入数据库的数据比一般的大, 但是没有事务的需求.</p><p>当时我们大概估算了一下, BINLOG 的日志一天大概在 260G 左右.</p><p>保留七天, 也就是 1800G, 1800G - 500G(免费额度) = 1300G,</p><p>腾讯云的 BINLOG 备份日志按 0.008/G/小时算</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">1300 * 0.008 * 24 = 249.6</span><br><span class="line">249.6 * 30 = 7488</span><br></pre></td></tr></table></figure><p>一个月光是日志的费用就需要 7488.</p><p>还没完, 根据活跃用户, 我估算了下, 大概需要 30 台云数据库.</p><p>这样<code>7488 * 30 = 224640</code>, 一个月多了 2W 块的成本, 还只是 BINLOG 日志的.</p><p>所以没办法, 必须得进行优化才能上.</p><h4 id="思路"><a href="#思路" class="headerlink" title="思路"></a>思路</h4><p>我们知道, BINLOG 文件的大小取决于两点<code>Sql次数</code>和<code>Sql大小</code></p><p>所以我们需要根据这两个信息的数据进行判断.</p><p>这里先夸一下, 腾讯云的 Sql 审计系统, 做的还不错(也有可以提高的地方)</p><p>可以直接将取 1 小时范围内的 Sql 语句来统计次数和大小.</p><p><img src="/resource/ec486102f8a14ed4a65184965433cfc4.png" alt="d0d3dec288d3063390e1e58888b75114.png"></p><p>这里有个问题, 腾讯云数据库审计系统上的 Sql 是有部分截取的.</p><p>所以没法知道这个 Sql 语句的大小到底是多少, 只能从时间维度上进行对比</p><p>而 Sql 语句的准确大小我们要根据 BINLOG 里面的 Sql 来计算</p><p><img src="/resource/c60db2bc344448598421af63d7617f12.png" alt="d4faff7f34b93348e2d01e0372186369.png"></p><p>这样我们就能从时间和大小两个维度来比对 BINLOG 大小.</p><p>从而发现到底是哪个表和系统有过多的增删改.</p><p>可以看到, <code>players</code>表的 Update 的大小最多, <code>ranks</code>表的 Insert 最多.</p><p>所以我们需要针对这两个表进行优化.</p><h3 id="解决解决"><a href="#解决解决" class="headerlink" title="解决解决"></a>解决解决</h3><p>再和研发沟通后得知, players 表示每 3 分钟更新一次.</p><p>而 ranks 表示 30 分钟整表清空(不是 delete), 然后 insert.</p><p>经过我们的多次尝试, 最后将 players 表改成 30 分钟写一次库</p><p>而 ranks 表则 2 小时写一次库, players 表的重要性比 ranks 要大.</p><p>所以我们会考虑尽量降低 players 的写入时长, 而提高 ranks 表的写入时长.</p><p>完成后观察了下备份的大小, 还行, 在可接受的范围.</p><p><img src="2020-07-07-22-15-24.png" alt></p><h1 id="复盘总结"><a href="#复盘总结" class="headerlink" title="复盘总结"></a>复盘总结</h1><ol><li>云数据库监控和审计能够帮助定位了很多问题</li><li>对业务开发需要制定限制和标准</li><li>基于数据统计后的执行效果就是快, 准, 狠.</li></ol>]]></content>
<summary type="html">
<h1 id="前戏"><a href="#前戏" class="headerlink" title="前戏"></a>前戏</h1><p>其实一开始, 写这东西我其实是拒绝的.</p>
<p>因为感觉没有多少技术含量, 没法体现出我牛逼的地方.</p>
<p>后来想了想, 很多思想和方法还是挺不错, 可以深挖和借鉴.</p>
<p>所以就记录下来, 方便以后回顾.</p>
<p>以上.</p>
<h1 id="问题背景"><a href="#问题背景" class="headerlink" title="问题背景"></a>问题背景</h1><ol>
<li>本地数据库使用 5.6 的 MyISAM 引擎.</li>
<li>业务没有使用事务, 存储过程, join, 之类的常规关系数据查询</li>
<li>直接粗暴简单的将用户数据序列化 json 后存在多个 BLOBTEXT 字段里.</li>
<li>每个小时的整点进行锁表备份, 导致 CPU 的间隔性的突发.</li>
</ol>
<p>我们需要做的是在<code>效率</code>, <code>安全</code>, <code>成本</code>之间做平衡</p>
<h2 id="迁移"><a href="#迁移" class="headerlink" title="迁移"></a>迁移</h2><p>在游戏服导量过后的一段时间.<br>需要定期的对游戏服进行迁移.<br>以提高资源使用率从而降低成本.</p>
<p>数据库在本地时, 迁移需要对数据库进行导入导出操作<br>既不安全, 效率也低.</p>
<h2 id="备份"><a href="#备份" class="headerlink" title="备份"></a>备份</h2><p>本地的数据库使用是的每小时备份.<br>在备份时会占用本地主机的资源, 影响用户体验.</p>
<h2 id="性能"><a href="#性能" class="headerlink" title="性能"></a>性能</h2><p>本地数据库都是机械磁盘<br>同时大批量的停止和启动服务时会有大量的数据库操作<br>这些操作大都是磁盘的瓶颈, 拉长了维护的时间</p>
<h2 id="回档"><a href="#回档" class="headerlink" title="回档"></a>回档</h2><p>当研发需要查询某个时间的数据时只能按每小时回档</p>
<h2 id="崩溃"><a href="#崩溃" class="headerlink" title="崩溃"></a>崩溃</h2><p>没有对数据库做高可用, 当数据库崩溃时影响业务的数据安全</p>
</summary>
<category term="mysql" scheme="https://zhiwei.show/tags/mysql/"/>
<category term="性能" scheme="https://zhiwei.show/tags/%E6%80%A7%E8%83%BD/"/>
</entry>
<entry>
<title>有关SaltStack的State错误的执行在其他主机上</title>
<link href="https://zhiwei.show/397203e63bb441198edc9333273d2194/"/>
<id>https://zhiwei.show/397203e63bb441198edc9333273d2194/</id>
<published>2020-05-24T02:33:00.000Z</published>
<updated>2020-05-24T02:33:00.000Z</updated>
<content type="html"><![CDATA[<h1 id="背景问题"><a href="#背景问题" class="headerlink" title="背景问题"></a>背景问题</h1><p>我们的运维平台是基于 SaltStack 封装的任务系统.<br>在任务系统上将需要执行多个操作转为不同的 state<br>SaltStack 负责进行调度执行, Minion 执行完了之后, 会将数据结果返回给 master<br>而 master 则将结果保存在 mongodb 里面, 看图</p><a id="more"></a><p><img src="/resource/0b24e2e92df44e9e936c148fe46465f7.png" alt="edbf79382966e44f0cf30fced067c217.png"></p><p>然而, 在我们使用的时候出现个奇怪的问题</p><p><img src="/resource/58b9c29a78f743409de73243f6090d42.png" alt="3cdf9b3845bc75db3b5aa060195e3437.png"></p><p>第一个主机上的 state 执行是成功<br>但是莫名其妙会在第二个个主机上执行 state<br>幸好没执行成功, 不然尽给我瞎整.</p><h1 id="分析排查"><a href="#分析排查" class="headerlink" title="分析排查"></a>分析排查</h1><h2 id="基本测试"><a href="#基本测试" class="headerlink" title="基本测试"></a>基本测试</h2><p>先看看这个主机在 master 上是否能正常连接.<br>执行命令<code>salt "VM_0_7_centos" test.ping</code><br>结果显示无法连接</p><p>再看看 salt-minon 的 key 是否还存在<br>执行命令<code>salt-key -L | grep "VM_0_7_centos"</code><br>结果显示, 这台主机 salt-minion 的 key 并不存在</p><p>这就很尴尬了, 为什么一台不存在的主机会被<code>state.sls</code>执行到.<br>关键还不是必现的问题, 不好排查.</p><h2 id="查看源码"><a href="#查看源码" class="headerlink" title="查看源码"></a>查看源码</h2><p>这个问题奇怪的很, 而且不是必现.<br>网上也找不到什么有用的信息.</p><p>好在 SaltStack 是一个开源的项目.<br>有什么不清楚的, 我们可以直接看源码来排查.</p><p>其实对 SaltStack 不是很了解, 代码太长, 我们直接找到问题的核心代码</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">#</span></span><br><span class="line"><span class="comment"># 我们使用的是salt -S '10.0.0.1' state.sls的方式执行的</span></span><br><span class="line"><span class="comment"># 所以问题肯定出在查找minions的时候</span></span><br><span class="line"><span class="comment"># 直接看_check_ipcidr_minions方法就行了</span></span><br><span class="line"><span class="comment">#</span></span><br><span class="line"><span class="comment"># 从上层调用的方法来看, greedy 参数默认是True</span></span><br><span class="line"><span class="comment"># 所以每次执行必定是`贪婪模式`</span></span><br><span class="line"><span class="comment">#</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">_check_ipcidr_minions</span><span class="params">(self, expr, greedy)</span>:</span></span><br><span class="line"> <span class="string">'''</span></span><br><span class="line"><span class="string"> Return the minions found by looking via ipcidr</span></span><br><span class="line"><span class="string"> '''</span></span><br><span class="line"> cache_enabled = self.opts.get(<span class="string">'minion_data_cache'</span>, <span class="literal">False</span>)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> greedy:</span><br><span class="line"> <span class="comment"># 如果是贪婪模式, 从/etc/salt/pki/minions/里面获取minions</span></span><br><span class="line"> minions = self._pki_minions()</span><br><span class="line"> <span class="keyword">elif</span> cache_enabled:</span><br><span class="line"> <span class="comment"># 如果非贪婪模式, 且打开了缓存, 则从/var/cache/salt/master/minions/获取minions</span></span><br><span class="line"> minions = self.cache.list(<span class="string">'minions'</span>)</span><br><span class="line"> <span class="keyword">else</span>:</span><br><span class="line"> <span class="keyword">return</span> {<span class="string">'minions'</span>: [],</span><br><span class="line"> <span class="string">'missing'</span>: []}</span><br><span class="line"></span><br><span class="line"> <span class="comment"># 从函数整体来看</span></span><br><span class="line"> <span class="comment"># minions是最终返回的列表, 称为返回列表</span></span><br><span class="line"> <span class="comment"># cminions是用来遍历的列表, 称为遍历列表</span></span><br><span class="line"> <span class="comment"># 因为不能一边遍历一个对象一边修改一个对象</span></span><br><span class="line"> <span class="keyword">if</span> cache_enabled:</span><br><span class="line"> <span class="keyword">if</span> greedy:</span><br><span class="line"> cminions = self.cache.list(<span class="string">'minions'</span>)</span><br><span class="line"> <span class="keyword">else</span>:</span><br><span class="line"> cminions = minions</span><br><span class="line"> <span class="keyword">if</span> cminions <span class="keyword">is</span> <span class="literal">None</span>:</span><br><span class="line"> <span class="keyword">return</span> {<span class="string">'minions'</span>: minions,</span><br><span class="line"> <span class="string">'missing'</span>: []}</span><br><span class="line"></span><br><span class="line"> tgt = expr</span><br><span class="line"> <span class="keyword">try</span>:</span><br><span class="line"> <span class="comment"># Target is an address?</span></span><br><span class="line"> tgt = ipaddress.ip_address(tgt)</span><br><span class="line"> <span class="keyword">except</span> Exception:</span><br><span class="line"> <span class="keyword">try</span>:</span><br><span class="line"> <span class="comment"># Target is a network?</span></span><br><span class="line"> tgt = ipaddress.ip_network(tgt)</span><br><span class="line"> <span class="keyword">except</span> Exception:</span><br><span class="line"> log.error(<span class="string">'Invalid IP/CIDR target: %s'</span>, tgt)</span><br><span class="line"> <span class="keyword">return</span> {<span class="string">'minions'</span>: [],</span><br><span class="line"> <span class="string">'missing'</span>: []}</span><br><span class="line"> proto = <span class="string">'ipv{0}'</span>.format(tgt.version)</span><br><span class="line"></span><br><span class="line"> <span class="comment"># 使用set对list类型去重</span></span><br><span class="line"> minions = set(minions)</span><br><span class="line"></span><br><span class="line"> <span class="comment"># 遍历的列表是从`/var/cache/salt/master/minions/`获取</span></span><br><span class="line"> <span class="comment"># 返回的列表如果是贪婪模式</span></span><br><span class="line"> <span class="comment"># 从`/etc/salt/pki/minions/`获取</span></span><br><span class="line"> <span class="comment"># 返回的列表如果非贪婪模式</span></span><br><span class="line"> <span class="comment"># 从`/var/cache/salt/master/minions/`获取</span></span><br><span class="line"> <span class="comment"># 所以在贪婪模式下, 如果遍历列表和返回列表不一致</span></span><br><span class="line"> <span class="comment"># 则最终会导致多出很多不是我们想要的minion</span></span><br><span class="line"> <span class="comment">#</span></span><br><span class="line"> <span class="keyword">for</span> id_ <span class="keyword">in</span> cminions:</span><br><span class="line"> mdata = self.cache.fetch(<span class="string">'minions/{0}'</span>.format(id_), <span class="string">'data'</span>)</span><br><span class="line"> <span class="keyword">if</span> mdata <span class="keyword">is</span> <span class="literal">None</span>:</span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">not</span> greedy:</span><br><span class="line"> minions.remove(id_)</span><br><span class="line"> <span class="keyword">continue</span></span><br><span class="line"> grains = mdata.get(<span class="string">'grains'</span>)</span><br><span class="line"> <span class="keyword">if</span> grains <span class="keyword">is</span> <span class="literal">None</span> <span class="keyword">or</span> proto <span class="keyword">not</span> <span class="keyword">in</span> grains:</span><br><span class="line"> match = <span class="literal">False</span></span><br><span class="line"> <span class="keyword">elif</span> isinstance(tgt, (ipaddress.IPv4Address, ipaddress.IPv6Address)):</span><br><span class="line"> match = six.text_type(tgt) <span class="keyword">in</span> grains[proto]</span><br><span class="line"> <span class="keyword">else</span>:</span><br><span class="line"> match = salt.utils.network.in_subnet(tgt, grains[proto])</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">not</span> match <span class="keyword">and</span> id_ <span class="keyword">in</span> minions:</span><br><span class="line"> minions.remove(id_)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> {<span class="string">'minions'</span>: list(minions),</span><br><span class="line"> <span class="string">'missing'</span>: []}</span><br></pre></td></tr></table></figure><p>最终发现<br><code>/var/cache/salt/master/minions/</code>目录里没有相应 minion 的 cache 信息<br>而<code>/etc/salt/pki/minions/</code>上却有相应的 minion 密钥</p><h1 id="复盘思考"><a href="#复盘思考" class="headerlink" title="复盘思考"></a>复盘思考</h1><h2 id="问题的思考"><a href="#问题的思考" class="headerlink" title="问题的思考"></a>问题的思考</h2><ol><li>因为主机下线后没有清掉相应的 key 文件导致的</li><li>使用了 IP 匹配模式来查找 minion</li><li>IP 匹配模式默认使用贪婪模式</li></ol><p>很明显, 不了解的原理导致的问题, 反省, 反省, 反省.</p><h2 id="设计的思考"><a href="#设计的思考" class="headerlink" title="设计的思考"></a>设计的思考</h2><p>我们原本理解的直接指定 IP 地址效率会更高(主机之间通过 IP 地址通过)<br>而且主机名会变, 但 IP 地址一般来说不会变, 所以我们使用 IP 地址来执行<br>然而从代码上来看并不是这样的</p><p>在 salt-master 寻找 minion 时通过 minion 的 IP 地址的来过滤相应的 minion<br>这个实现的过滤方式还有点奇怪</p><p>一般来说, 我们的实现思路应该是这样的</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">find</span><span class="params">(list, key)</span>:</span></span><br><span class="line"> result = []</span><br><span class="line"> <span class="keyword">for</span> i <span class="keyword">in</span> list:</span><br><span class="line"> <span class="keyword">if</span> key == i:</span><br><span class="line"> result.append(i)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> result</span><br></pre></td></tr></table></figure><p>而 saltstack 上实现的方法则是</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">find</span><span class="params">(list1, list2, key)</span>:</span></span><br><span class="line"> <span class="keyword">for</span> i <span class="keyword">in</span> list1:</span><br><span class="line"> <span class="keyword">if</span> key == i:</span><br><span class="line"> list2.remove(key)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> list2</span><br></pre></td></tr></table></figure><p>这个设计可能是为了满足贪婪模式, 但这个贪婪模式不清楚是为什么.<br>从使用者的角度来看, 感觉这是个欠妥的设计.</p><p>因为使用时已经明确的指定了对某一个 IP 执行某个动作<br>但贪婪模式却尽可能的给我匹配更多的 minion</p><p>在认知和实现上有较大的差距, 虽然这个可能是使用者不当造成的.<br>但在设计时应该尽量避免这种有明显不同的地方.</p>]]></content>
<summary type="html">
<h1 id="背景问题"><a href="#背景问题" class="headerlink" title="背景问题"></a>背景问题</h1><p>我们的运维平台是基于 SaltStack 封装的任务系统.<br>在任务系统上将需要执行多个操作转为不同的 state<br>SaltStack 负责进行调度执行, Minion 执行完了之后, 会将数据结果返回给 master<br>而 master 则将结果保存在 mongodb 里面, 看图</p>
</summary>
<category term="故障" scheme="https://zhiwei.show/tags/%E6%95%85%E9%9A%9C/"/>
<category term="saltstack" scheme="https://zhiwei.show/tags/saltstack/"/>
</entry>
<entry>
<title>错误的HTTP头解析引发的故障</title>
<link href="https://zhiwei.show/ffb30a139e094665af16e5314e55883d/"/>
<id>https://zhiwei.show/ffb30a139e094665af16e5314e55883d/</id>
<published>2020-05-20T23:31:00.000Z</published>
<updated>2020-05-20T23:31:00.000Z</updated>
<content type="html"><![CDATA[<h1 id="问题背景"><a href="#问题背景" class="headerlink" title="问题背景"></a>问题背景</h1><p>我们是一家 H5 的研发公司.<br>运营突然报业务故障, 玩家登录不了(监控没做好, 真的是没人啊..)<br>点击进入游戏时转圈圈, 提示<code>连接超时</code>(找不到图了, 发挥一下想象吧)</p><h1 id="排查思路"><a href="#排查思路" class="headerlink" title="排查思路"></a>排查思路</h1><p>经过我们的沟通, 得出信息</p><ol><li>只有 IOS 的用户有问题</li><li>只有某个特殊渠道有问题</li><li>同样的手机访问其他游戏没问题</li><li>同样的账号换手机登录没问题</li><li>没有更新版本和变动</li></ol><p>这样来看, 肯定是渠道方更新了什么导致的故障</p><p>客户端只有连接超时的信息.<br>服务端上只有一行<code>http hand failed</code>, 没有任何其他信息</p><a id="more"></a><h2 id="代理端-HTTP-数据包分析"><a href="#代理端-HTTP-数据包分析" class="headerlink" title="代理端 HTTP 数据包分析"></a>代理端 HTTP 数据包分析</h2><p>之前忘了说, 我们的架构是这样的.<br>(游戏行业里算是有点奇葩的, 据说是因为 server 的框架不支持 https)</p><p><img src="/resource/2e056ea1100841f998b5dfc55c13d6e1.png" alt="3d4b161bb892d99babd254c5d0cbe4de.png"></p><p>故障的 TCP 数据包</p><p><img src="/resource/e3d456f0e9cc4ea884b0ed0feb657ea8.png" alt="f0b0785b0855afe64696f32b9c526f18.png"></p><p>正常的 TCP 数据包</p><p><img src="/resource/c7782c7799f74fe391f91c823b05acdc.png" alt="968ef4cf35c71f54653f00b2ae6cc6eb.png"></p><p>可以看到, 有故障的 TCP 数据包, 被客户端 REST 断开了.<br>结合服务端日志来看, 应该是在 HTTP 请求的时候出错了.</p><p>因为都是 HTTPS 的连接, 所以无法查看请求的信息.<br>尝试了很多解密的方式都无果.<br>最后没办法, 只能在服务端进行抓包.</p><h2 id="服务端-HTTP-数据包分析"><a href="#服务端-HTTP-数据包分析" class="headerlink" title="服务端 HTTP 数据包分析"></a>服务端 HTTP 数据包分析</h2><p>我们在服务端上抓包, 对比正常的请求, 和异常的请求</p><p>异常的 http 数据包</p><p><img src="/resource/887ad45b8ee943a4916e914f9a6397ff.png" alt="091954544ca7fa83b0a9f03efe911551.png"></p><p>正常的 http 数据包</p><p><img src="/resource/0238fdb31c9f4b6c89b1ccf625fd27dd.png" alt="e2eaee36d91e6054dd6ef670deac61da.png"></p><p>终于找到问题了, 服务端没有返回 http 的 websocket 的请求<br>问题又来了, 为什么没有返回请求?</p><h2 id="服务端-HTTP-请求信息分析"><a href="#服务端-HTTP-请求信息分析" class="headerlink" title="服务端 HTTP 请求信息分析"></a>服务端 HTTP 请求信息分析</h2><p>从服务端的错误信息来看, 肯定是服务端处理 http 请求的时候出错了.<br>所以我们再来对比下两个请求的 http 信息</p><p>左边是异常的 http 请求信息, 右边是正常的 http 请求信息</p><p><img src="/resource/edd1a15bb4664318ae5a5e5f7f1d8932.png" alt="d5473da85f1af5dbd4ed4fcd8c79cb69.png"></p><p>看到一个很奇怪的头信息<code>Cookie</code>, 这个头信息是空的.<br>为什么头信息是空的值得怀疑?<br>因为所有其他的头信息看起来都是正常的<br>所以只能先排除这个最明显的异常.</p><p>我们找了个服务进行测试, 在代理端通过<code>proxy_set_header Cookie "";</code><br>最后 reload 后, 这个服务正常了.</p><p>以上.</p><h1 id="复盘总结"><a href="#复盘总结" class="headerlink" title="复盘总结"></a>复盘总结</h1><h2 id="业务监控"><a href="#业务监控" class="headerlink" title="业务监控"></a>业务监控</h2><p>因为游戏行业有上万个独立的服务, 监控覆盖的成本非常高.<br>而且我们做了日志的监控, 但是这个日志不是标准的错误格式, 所以根本没法捕捉到.</p><h2 id="HTTPS"><a href="#HTTPS" class="headerlink" title="HTTPS"></a>HTTPS</h2><p>对 HTTPS 的知识点不是很熟悉, 需要看一看.<br>HTTPS 的解码对排查问题帮助很大, 如果我们没有 HTTP 请求, 那排查的效率指数上升.</p>]]></content>
<summary type="html">
<h1 id="问题背景"><a href="#问题背景" class="headerlink" title="问题背景"></a>问题背景</h1><p>我们是一家 H5 的研发公司.<br>运营突然报业务故障, 玩家登录不了(监控没做好, 真的是没人啊..)<br>点击进入游戏时转圈圈, 提示<code>连接超时</code>(找不到图了, 发挥一下想象吧)</p>
<h1 id="排查思路"><a href="#排查思路" class="headerlink" title="排查思路"></a>排查思路</h1><p>经过我们的沟通, 得出信息</p>
<ol>
<li>只有 IOS 的用户有问题</li>
<li>只有某个特殊渠道有问题</li>
<li>同样的手机访问其他游戏没问题</li>
<li>同样的账号换手机登录没问题</li>
<li>没有更新版本和变动</li>
</ol>
<p>这样来看, 肯定是渠道方更新了什么导致的故障</p>
<p>客户端只有连接超时的信息.<br>服务端上只有一行<code>http hand failed</code>, 没有任何其他信息</p>
</summary>
<category term="故障" scheme="https://zhiwei.show/tags/%E6%95%85%E9%9A%9C/"/>
<category term="复盘" scheme="https://zhiwei.show/tags/%E5%A4%8D%E7%9B%98/"/>
<category term="tcpdump" scheme="https://zhiwei.show/tags/tcpdump/"/>
<category term="http" scheme="https://zhiwei.show/tags/http/"/>
</entry>
<entry>
<title>Docker-Swarm尝试引发的并发问题</title>
<link href="https://zhiwei.show/b078bce7c017421483ca473014d6f081/"/>
<id>https://zhiwei.show/b078bce7c017421483ca473014d6f081/</id>
<published>2020-05-18T00:50:00.000Z</published>
<updated>2020-05-18T00:50:00.000Z</updated>
<content type="html"><![CDATA[<h1 id="背景问题"><a href="#背景问题" class="headerlink" title="背景问题"></a>背景问题</h1><p>公司需要对用户的行为做分析<br>所以在服务端上通过 HTTP 的方式上报数据</p><p>想要将上报数据的网关容器化了<br>方面以后的维护和管理</p><p>因为节点少, 相对于 K8S 来说太重了.<br>所以准备尝试一下 Docker Swarm</p><p>没想到在测试环境没问题, 一上生产.<br>结果立马出现了一大堆上报失败的错误</p><p>吓的我立马回滚了操作, 再慢慢来排查.</p><a id="more"></a><h1 id="排查思路"><a href="#排查思路" class="headerlink" title="排查思路"></a>排查思路</h1><p>先看看架构图</p><p><img src="/resource/b476f3fded4c4e85966bf3c56ae3aaa4.png" alt="278ebb5695adfc8d3e0c35f679b474c6.png"></p><p>是挺简单的, 从设计上来看不怎么可靠.</p><p>因为在测试环境上进行过测试, 但在生产上就问题.<br>所以只能通过 nginx 的 mirror 模块将生产环境的 HTTP 请求转发给测试环境<br>来排查问题.</p><h2 id="大量-TIMEWAIT-的问题"><a href="#大量-TIMEWAIT-的问题" class="headerlink" title="大量 TIMEWAIT 的问题"></a>大量 TIMEWAIT 的问题</h2><p>查看日志, 发现挺正常的, 请求都一个接着一个.<br>但就是会出现大量的上报失败错误.</p><p>看了 CPU, 内存, IO 都挺正常的<br>因为部分请求是正常的, 所以容器内部肯定会有 TCP 的连接<br>想抓包看看那些不成功请求发生了什么<br>进入容器<code>ss -s</code>一看, 有 6W 多个<code>TIMEWAIT</code><br>关于 TIMEWAIT 的问题, 网上都讲烂了.</p><p>总结一下, 主要为了避免<code>网络上延迟的数据包影响正常的新连接</code></p><p>有两种解决思路.</p><p>第一种<br>开启快速回收, <code>echo 1 > /proc/sys/net/ipv4/tcp_tw_recycle</code><br>开启重复利用, <code>echo 1 > /proc/sys/net/ipv4/tcp_tw_recycle</code></p><p>这两个虽然能快速解决问题, 但是有隐患(往下看)</p><p>第二种<br>避免 TIMEWAIT 的出现.<br>我们知道, 根据 TCP 的四次挥手的过程</p><p><img src="/resource/dd169658678340ee85e7fc054b25ea1a.png" alt="e532ea9cdfec4b174b89770f066f748b.png"></p><p>TCP 的服务端和客户端<code>谁先分手</code>, 谁就有<code>TIMEWAIT</code><br>所以这种解决思路就是由客户端发出断开的数据包</p><p>在 HTTP 的应用层, 有个 HTTP 的头部信息<code>Connection: keep-alive</code><br>这个信息控制双方发送完请求后是否断开 TCP 的连接.</p><p>HTTP1.0 的默认是``Connection: close<code>, 也就是说, 请求完后立马断开连接. HTTP1.1之后默认的都是</code>Connection: keep-alive`, 请求完等待一段时候.</p><p>网上有很多文章介绍, 就不搬运了, 有兴趣的同学自行查找.</p><p>现在来看<br>测试环境上的问题和正常环境无关, 因为正式环境是的请求都是 HTTP1.1<br>服务端不应该会出现大量的<code>TIMEWAIT</code></p><p>一直没想明白, 后来反复的查找资料和实验才找到答案<br>测试环境上大量的的<code>TIMEWAIT</code><br>是因为 nginx 的<code>proxy_http_version</code>指令默认请求的是 HTTP1.0</p><p>这就导致了, 后端服务器处理完请求后就立刻断开连接.<br>根据我们谁先分手原则, 最终服务器有大量的 TIMEWAIT.</p><p>解决方法也很简单</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line"></span><br><span class="line">server {</span><br><span class="line"> ...</span><br><span class="line"></span><br><span class="line"> location / {</span><br><span class="line"> proxy_pass http://backend;</span><br><span class="line"> proxy_http_version 1.1;</span><br><span class="line"> proxy_set_header Connection "";</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>有点奇怪的是<code>proxy_set_header Connection "";</code>是什么东西.<br>为什么要清空 HTTP 的<code>Connection</code>信息.</p><p>带着好奇心, 问了一下 google.<br>原来是避免客户端发起了<code>Connection Close</code>, 导致后端服务会和 nginx 直接断开连接.</p><h2 id="TCP-数据包时间过期问题"><a href="#TCP-数据包时间过期问题" class="headerlink" title="TCP 数据包时间过期问题"></a>TCP 数据包时间过期问题</h2><p>讲了这么多, 还是没有弄清楚生产环境为什么会出问题.<br>因为无法在测试环境上复现, 所以再次切换到生产环境上了.<br>还好不是业务系统, 并且有数据有补报机制, 不然可不敢这么玩.</p><p>切到生产上, 问题依旧, 赶紧使用 tcpdump 抓包, 然后还原</p><p><img src="/resource/f6ed6b40bc9742ae87233e9c7ac8e6b9.png" alt="337e268cba0c3cf777c230eb28b1018e.png"></p><p>(不是原图, 原图几乎全是<code>TCP Out-Of-Order</code>)<br>发现全是<code>TCP Out-Of-Order</code>, 简单来说, 就是收到的 TCP 数据包的顺序不对.</p><p>因为 TCP 收到数据包后需要校验确认, 来保证双方的可靠传输.<br>所以收到的 TCP 数据包顺序不对, 无法判断是真的数据丢失, 还是数据错乱.</p><p>至于为什么会引起数据包顺序错误, 这个问题其实网上也有很多案例了.</p><p>简单来说, 内核文件<code>/proc/sys/net/ipv4/tcp_tw_recycle</code>控制是否快速回收<code>TIMEWAIT</code>的 socket<br>而 TIMEWAIT 则是用来避免网络上残留的数据影响了正常的通信.<br>对于同一个连接, 如果收到了<code>过期</code>的数据包就丢弃掉<br>TCP 使用四元组(源 IP,源 PORT,目的 IP,目的 PORT)来表示一个连接的.</p><p>开启<code>tcp_tw_recycle</code>后, TCP 的四元组信息已经不匹配了, 所以只能通过源 IP 来判断<br>可以通过<code>/proc/sys/net/ipv4/tcp_tw_recycle</code>内核文件来控制是否检查同个源 IP 的数据包时间</p><p>除了上面以外, 还有个关键的地方<br><code>docker swarm</code>使用了<code>ipvs snat</code>将所有的请求都变成一个源 IP<br>而这些源 IP 收到数据包发现时间不同, 自然会产生丢弃, 最终导致数据无法传输.</p><h1 id="复盘总结"><a href="#复盘总结" class="headerlink" title="复盘总结"></a>复盘总结</h1><ol><li>本来查一个问题, 结果发现的问题越来越多.</li><li>nginx 的 mirror 是一个很好的生产排查工具, 就是要理解好</li><li>基础性的原理对于排查的思路非常重要.</li></ol>]]></content>
<summary type="html">
<h1 id="背景问题"><a href="#背景问题" class="headerlink" title="背景问题"></a>背景问题</h1><p>公司需要对用户的行为做分析<br>所以在服务端上通过 HTTP 的方式上报数据</p>
<p>想要将上报数据的网关容器化了<br>方面以后的维护和管理</p>
<p>因为节点少, 相对于 K8S 来说太重了.<br>所以准备尝试一下 Docker Swarm</p>
<p>没想到在测试环境没问题, 一上生产.<br>结果立马出现了一大堆上报失败的错误</p>
<p>吓的我立马回滚了操作, 再慢慢来排查.</p>
</summary>
<category term="tcpdump" scheme="https://zhiwei.show/tags/tcpdump/"/>
<category term="docker" scheme="https://zhiwei.show/tags/docker/"/>
</entry>
<entry>
<title>使用nginx代理跨机房的saltstack使用</title>
<link href="https://zhiwei.show/a4f387bec6bc469db9a65aff81e63b6c/"/>
<id>https://zhiwei.show/a4f387bec6bc469db9a65aff81e63b6c/</id>
<published>2020-05-10T01:37:00.000Z</published>
<updated>2020-05-10T01:37:00.000Z</updated>
<content type="html"><![CDATA[<h1 id="问题背景"><a href="#问题背景" class="headerlink" title="问题背景"></a>问题背景</h1><p>我们是游戏研发公司, 需要和不同的发行商合作运营<br>而且很多发行商要求使用自己的服务器(毕竟自己控制更可靠)<br>所以就出现了这么一种情况</p><p>业务不大, 但是不同云厂和区域的服务器不少.<br>并且我们使用 SaltStack 来管理主机的信息.<br>从主机的注册到作业的执行, 基本上是这样的</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">salt-master<----->nginx-stream<----->salt-minion</span><br></pre></td></tr></table></figure><p>这里为啥使用 nginx-stream, 而不是 salt-syndic<br>主要是因为简单, 稳定, 而且 salt-syndic 无法针对单个 minion 下发 job</p><p>把 nginx 部署在<code>docker swarm</code>上<br>有一个区域就添加一个<code>docker worker</code>, 然后自动的部署上去了</p><p>大概的思路是这样的, 然而在部署好了后使用<code>salt 'h72g9g19vr391.qcloud.hk' test.ping</code><br>有时正常, 有时却返回超时</p><a id="more"></a><h1 id="排查思路"><a href="#排查思路" class="headerlink" title="排查思路"></a>排查思路</h1><h2 id="saltstack-排查"><a href="#saltstack-排查" class="headerlink" title="saltstack 排查"></a>saltstack 排查</h2><p>先打开 salt-master 日志的 debug 模式</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">2020-05-29 16:29:01,272 Sending event: tag = 20200529162901272112; data = {'minions': ['h72g9g19vr391.qcloud.hk'], '_stamp': '2020-05-29T08:29:01.272678'}</span><br><span class="line">2020-05-29 16:29:01,273 Sending event: tag = salt/job/20200529162901272112/new; data = {'jid': '20200529162901272112', 'tgt_type': 'glob', 'tgt': 'h72g9g19vr391.qcloud.hk', 'us</span><br><span class="line">2020-05-29 16:29:01,285 LazyLoaded config.option</span><br><span class="line">2020-05-29 16:29:01,293 [salt.master :2346][INFO ][137] User root Published command test.ping with jid 20200529162901272112</span><br><span class="line">2020-05-29 16:29:01,293 Published command details {'fun': 'test.ping', 'arg': [], 'tgt': 'h72g9g19vr391.qcloud.hk', 'jid': '20200529162901272112', 'ret': '', 'tgt_type': 'glob'</span><br><span class="line">2020-05-29 16:29:01,294 Signing data packet</span><br><span class="line">2020-05-29 16:29:01,294 salt.crypt.get_rsa_key: Loading private key</span><br><span class="line">2020-05-29 16:29:01,294 salt.crypt.sign_message: Signing message.</span><br><span class="line">2020-05-29 16:29:01,296 Sending payload to publish daemon. jid=20200529162901272112 size=452</span><br><span class="line">2020-05-29 16:29:01,297 Connecting to pub server: ipc:///var/run/salt/master/publish_pull.ipc</span><br><span class="line">2020-05-29 16:29:01,297 Sent payload to publish daemon.</span><br><span class="line">2020-05-29 16:29:01,297 Publish daemon received payload. size=452</span><br><span class="line">2020-05-29 16:29:01,298 Closing AsyncZeroMQReqChannel instance</span><br><span class="line">2020-05-29 16:29:01,299 Publish daemon getting data from puller ipc:///var/run/salt/master/publish_pull.ipc</span><br><span class="line">2020-05-29 16:29:01,332 LazyLoaded mongo.get_load</span><br><span class="line">2020-05-29 16:29:01,336 LazyLoaded config.option</span><br><span class="line">2020-05-29 16:29:01,567 get_iter_returns for jid 20200529162901272112 sent to {'h72g9g19vr391.qcloud.hk'} will timeout at 16:29:06.567535</span><br><span class="line">2020-05-29 16:29:06,675 Checking whether jid 20200529162901272112 is still running</span><br></pre></td></tr></table></figure><p>从上面 saltstack 的日志来看, master 已经将 job 信息 publish 到了 zeromq 里面了<br>然而, 在 salt-minion 上并没有看到相关的日志信息.</p><p>也就是说 salt-minion 完全没有收到 salt-master 的 publish 的信息.<br>salt-master 已经发了, 但是 salt-minion 没有收到<br>那就说明数据丢在了 nginx 的转发上面</p><h2 id="nginx-排查"><a href="#nginx-排查" class="headerlink" title="nginx 排查"></a>nginx 排查</h2><p><img src="/resource/a33aa4daf83a47f496f8e2edab102d2b.png" alt="213f9a29b1b06f3565870b7711b86e40.png"></p><p>从上面的抓包数据可以看到, 在第 8 秒时, “客户端”主动发起来断开的数据包(FIN)<br>到底是 salt-minion 发出的 FIN, 还是 nginx 发出的 FIN</p><p>猜测应该是 nginx 发出的 FIN, 理由如下</p><ol><li>直连的 salt-minion 没有问题.</li><li>salt-minion 的 debug 日志里没有任何信息</li><li>对比了正常请求的 salt-master 的 debug 日志, 没有任何关于客户端的错误</li></ol><p>先看看 nginx 的配置</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">stream {</span><br><span class="line"> upstream salt-master {</span><br><span class="line"> hash $remote_addr consistent;</span><br><span class="line"> server 1.1.1.1:4505;</span><br><span class="line"> }</span><br><span class="line"> server {</span><br><span class="line"> listen 4505;</span><br><span class="line"> proxy_connect_timeout 5s;</span><br><span class="line"> proxy_timeout 5s;</span><br><span class="line"> proxy_pass salt-master;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>从配置上来看, 问题出在了<code>proxy_timeout 5s;</code>, 查下文档.</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Syntax:proxy_timeout timeout;</span><br><span class="line">Default:</span><br><span class="line">proxy_timeout 10m;</span><br><span class="line">Context:stream, server</span><br><span class="line">Sets the timeout between two successive read or write operations on client or proxied server connections.</span><br><span class="line">If no data is transmitted within this time, the connection is closed.</span><br></pre></td></tr></table></figure><p>在客户端和代理服务器上设置读取或写入的超时时间, 如果没有数据传输, 则连接断开.<br>把这个配置文件去掉后, 就都正常了.</p><h1 id="复盘总结"><a href="#复盘总结" class="headerlink" title="复盘总结"></a>复盘总结</h1><ol><li>排查的思路很重要, 一直以为是 saltstack 出了问题, 不停的在查两边的日志.</li><li>配置文件是完全复制之前的, 所以出了问题才发现.</li></ol>]]></content>
<summary type="html">
<h1 id="问题背景"><a href="#问题背景" class="headerlink" title="问题背景"></a>问题背景</h1><p>我们是游戏研发公司, 需要和不同的发行商合作运营<br>而且很多发行商要求使用自己的服务器(毕竟自己控制更可靠)<br>所以就出现了这么一种情况</p>
<p>业务不大, 但是不同云厂和区域的服务器不少.<br>并且我们使用 SaltStack 来管理主机的信息.<br>从主机的注册到作业的执行, 基本上是这样的</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">salt-master&lt;-----&gt;nginx-stream&lt;-----&gt;salt-minion</span><br></pre></td></tr></table></figure>
<p>这里为啥使用 nginx-stream, 而不是 salt-syndic<br>主要是因为简单, 稳定, 而且 salt-syndic 无法针对单个 minion 下发 job</p>
<p>把 nginx 部署在<code>docker swarm</code>上<br>有一个区域就添加一个<code>docker worker</code>, 然后自动的部署上去了</p>
<p>大概的思路是这样的, 然而在部署好了后使用<code>salt &#39;h72g9g19vr391.qcloud.hk&#39; test.ping</code><br>有时正常, 有时却返回超时</p>
</summary>
<category term="saltstack" scheme="https://zhiwei.show/tags/saltstack/"/>
<category term="nginx" scheme="https://zhiwei.show/tags/nginx/"/>
</entry>
<entry>
<title>nginx的mirror模块请求返回400状态</title>
<link href="https://zhiwei.show/faa6264186c549e2872823cad0749b68/"/>
<id>https://zhiwei.show/faa6264186c549e2872823cad0749b68/</id>
<published>2020-05-10T01:37:00.000Z</published>
<updated>2020-05-10T01:37:00.000Z</updated>
<content type="html"><![CDATA[<h1 id="问题背景"><a href="#问题背景" class="headerlink" title="问题背景"></a>问题背景</h1><p>突然被抓来协助大数据的上报网关(OpenResty)</p><p>client -> OpenResty -> Kafka -> ClickHouse</p><p>这里的 OpenResty 负责数据的兼容, 过滤, 路由</p><p>因为需要经常的修改 OpenResty 的逻辑配合数据分析这边的需求<br>导致改动容易出现问题, 需要搞个测试环境.<br>通过测试环境的验证后, 在更新到正式环境</p><p>实施方案是使用 nginx 的 mirror 模块将请求镜像到测试站点.<br>同样的代码和配置, 通过环境变量来区分生产和测试环境</p><p>大概是这样的.</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"> (不返回client)</span><br><span class="line">client <-------------> nginx <-------------> mirror</span><br></pre></td></tr></table></figure><p>那么问题来了<br>在实施中, 请求主站点时, 镜像站点也能收到请求<br>但一直显示<code>status_code 400</code>, 从表现上来看像是 POST 的数据丢失了.<br>在 nginx 的 log_format 把<code>$request_body</code>参数加上, 发现确实 POST 数据没有.</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">172.19.0.1 - [08/May/2020:06:35:09] "POST /user_login HTTP/1.0" 400 419 "-"</span><br><span class="line">172.19.0.1 - [08/May/2020:07:01:09] "POST /user_login HTTP/1.0" 400 419 "-"</span><br><span class="line">172.19.0.1 - [08/May/2020:07:02:43] "POST /user_login HTTP/1.0" 400 419 "-"</span><br></pre></td></tr></table></figure><a id="more"></a><h1 id="思路验证"><a href="#思路验证" class="headerlink" title="思路验证"></a>思路验证</h1><h2 id="nginx-的配置问题"><a href="#nginx-的配置问题" class="headerlink" title="nginx 的配置问题?"></a>nginx 的配置问题?</h2><p>难道是的配置有问题?</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line"></span><br><span class="line">mirror /mirror;</span><br><span class="line"></span><br><span class="line">location /mirror {</span><br><span class="line"> internal;</span><br><span class="line"> proxy_pass http://127.0.0.1:8080$request_uri;</span><br><span class="line"> proxy_set_header X-Original-URI $request_uri;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>主站点上日志的 POST 数据是没问题的, 所以很容易怀疑是 mirror 配置的问题</p><p>查看 nginx 的官网发现<code>mirror_request_body</code>是用于控制镜像请求时是否附带 body 数据</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Syntax:mirror_request_body on | off;</span><br><span class="line">Default:</span><br><span class="line">mirror_request_body on;</span><br><span class="line">Context:http, server, location</span><br></pre></td></tr></table></figure><p>但这个参数默认是打开的, 所以不是这个问题.</p><h2 id="OpenResty-的版本问题"><a href="#OpenResty-的版本问题" class="headerlink" title="OpenResty 的版本问题?"></a>OpenResty 的版本问题?</h2><p>因为使用的 docker 来搭建的开发环境<br>并且长时间没有更新, 所以怀疑是不是版本兼容问题.<br>使用<code>docker pull OpenResty/OpenResty</code>更新后发现…<br>也不是这个问题</p><h2 id="业务配置的问题"><a href="#业务配置的问题" class="headerlink" title="业务配置的问题?"></a>业务配置的问题?</h2><p>现在无法确定是主站点上的配置问题, 还是镜像站点上配置的问题.<br>所以使用 nginx 镜像配置了个干净的环境来进行测试.</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line">server {</span><br><span class="line"> listen 8080 ssl http2</span><br><span class="line"></span><br><span class="line"> location / {</span><br><span class="line"> return 200;</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>结果还是一样, 那么镜像站点的配置问题排除.</p><p>同样的使用 nginx 配置一个干净的主站点环境, 测试…<br>结果还是一样…</p><h2 id="抓包确认"><a href="#抓包确认" class="headerlink" title="抓包确认"></a>抓包确认</h2><p>当时就纳闷了, 既然请求通过 mirror 模块发起请求都会出现问题.<br>那就确定了一下, 到底发出来的请求数据有没有问题.</p><p>先将 mirror 的地址改成本机(本地开发环境), 然后开启<code>Wireshark</code>抓包</p><p><img src="/resource/a184ff5524b4456c8ee89270c95ada50.png" alt="0fa3315fc3faac41c1ed40363b1ea14d.png"></p><p>这么看来 mirror 模块的请求的 POST 数据是没问题的.</p><h2 id="问题定位"><a href="#问题定位" class="headerlink" title="问题定位"></a>问题定位</h2><p>现在我们知道了, 请求的数据是没问题的<br>但是通过 mirror 模块请求的 status_code 有问题<br>那就直接请求测试站点看看…</p><p>结果真相来了.</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><html></span><br><span class="line"></span><br><span class="line"><head></span><br><span class="line"><title>400 The plain HTTP request was sent to HTTPS port</title></span><br><span class="line"></head></span><br><span class="line"></span><br><span class="line"><body></span><br><span class="line"><center></span><br><span class="line"><h1>400 Bad Request</h1></span><br><span class="line"></center></span><br><span class="line"><center>The plain HTTP request was sent to HTTPS port</center></span><br><span class="line"><hr></span><br><span class="line"><center>OpenResty/1.15.8.3</center></span><br><span class="line"></body></span><br><span class="line"></span><br><span class="line"></html></span><br></pre></td></tr></table></figure><p>镜像站点的配置和主站点是一致的, 都是基于 ssl 的.<br>而主站点里的 mirror 模块使用的是<code>proxy_pass http://127.0.0.1:8080$request_uri;</code></p><p>至此问题定位完毕, 修复起来也就很简单了.</p><h1 id="复盘总结"><a href="#复盘总结" class="headerlink" title="复盘总结"></a>复盘总结</h1><h2 id="细节问题"><a href="#细节问题" class="headerlink" title="细节问题"></a>细节问题</h2><p>配置文件是直接复制的, 所以没有注意到细节.</p><h2 id="对-http-status-code-理解不深刻"><a href="#对-http-status-code-理解不深刻" class="headerlink" title="对 http status_code 理解不深刻"></a>对 http status_code 理解不深刻</h2><p>之前工作中一直碰到的 400 都是缺少参数导致服务器处理不了</p><p>其实真正的意思是说 client 的请求错误, 不仅仅是参数, 经验主义啊.</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">6.5.1. 400 Bad Request</span><br><span class="line"></span><br><span class="line"> The 400 (Bad Request) status code indicates that the server cannot or</span><br><span class="line"> will not process the request due to something that is perceived to be</span><br><span class="line"> a client error (e.g., malformed request syntax, invalid request</span><br><span class="line"> message framing, or deceptive request routing).</span><br></pre></td></tr></table></figure><h2 id="从设计上来看"><a href="#从设计上来看" class="headerlink" title="从设计上来看"></a>从设计上来看</h2><p>为什么设计上不将 http 请求 https 的错误作为一个独立的 status_code<br>猜测可能是因为将错误信息输出在了 respone text 里<br>所以就没有必要将单独作为一个 status_code, 毕竟这种情况也不常见.</p>]]></content>
<summary type="html">
<h1 id="问题背景"><a href="#问题背景" class="headerlink" title="问题背景"></a>问题背景</h1><p>突然被抓来协助大数据的上报网关(OpenResty)</p>
<p>client -&gt; OpenResty -&gt; Kafka -&gt; ClickHouse</p>
<p>这里的 OpenResty 负责数据的兼容, 过滤, 路由</p>
<p>因为需要经常的修改 OpenResty 的逻辑配合数据分析这边的需求<br>导致改动容易出现问题, 需要搞个测试环境.<br>通过测试环境的验证后, 在更新到正式环境</p>
<p>实施方案是使用 nginx 的 mirror 模块将请求镜像到测试站点.<br>同样的代码和配置, 通过环境变量来区分生产和测试环境</p>
<p>大概是这样的.</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"> (不返回client)</span><br><span class="line">client &lt;-------------&gt; nginx &lt;-------------&gt; mirror</span><br></pre></td></tr></table></figure>
<p>那么问题来了<br>在实施中, 请求主站点时, 镜像站点也能收到请求<br>但一直显示<code>status_code 400</code>, 从表现上来看像是 POST 的数据丢失了.<br>在 nginx 的 log_format 把<code>$request_body</code>参数加上, 发现确实 POST 数据没有.</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">172.19.0.1 - [08/May/2020:06:35:09] &quot;POST /user_login HTTP/1.0&quot; 400 419 &quot;-&quot;</span><br><span class="line">172.19.0.1 - [08/May/2020:07:01:09] &quot;POST /user_login HTTP/1.0&quot; 400 419 &quot;-&quot;</span><br><span class="line">172.19.0.1 - [08/May/2020:07:02:43] &quot;POST /user_login HTTP/1.0&quot; 400 419 &quot;-&quot;</span><br></pre></td></tr></table></figure>
</summary>
<category term="故障" scheme="https://zhiwei.show/tags/%E6%95%85%E9%9A%9C/"/>
<category term="复盘" scheme="https://zhiwei.show/tags/%E5%A4%8D%E7%9B%98/"/>
<category term="nginx" scheme="https://zhiwei.show/tags/nginx/"/>
</entry>
<entry>
<title>客户端业务异常上报监控</title>
<link href="https://zhiwei.show/1a05d579266a40bb9785b91b8de3fe70/"/>
<id>https://zhiwei.show/1a05d579266a40bb9785b91b8de3fe70/</id>
<published>2020-04-03T04:09:00.000Z</published>
<updated>2020-04-03T04:09:00.000Z</updated>
<content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>我们是一家 H5 的游戏公司<br>客户端以网页, 小程序, APP 的方式发布<br>对 CDN 的请求非常的大, 所以 CDN 请求的影响是非常大的.</p><h1 id="前提"><a href="#前提" class="headerlink" title="前提"></a>前提</h1><p>其实一开始是想放在 <a href="/2020/05/02/服务端业务异常日志监控/">服务端业务异常日志监控</a> 一起讲的<br>然而因为这里有一些情况不太一样, 细讲起来差异还挺大的.<br>所以这里就拆开单独写了(真的不是为了凑数!!!)</p><h1 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h1><ol><li>客户端请求 CDN 卡顿</li><li>客户端请求服务端卡顿</li><li>客户端代码异常</li></ol><h1 id="方案"><a href="#方案" class="headerlink" title="方案"></a>方案</h1><p>elasticsearch, 存储检索<br>logstash, 上报格式转换<br>kibana, 统计展示<br>openresty, 上报网关<br>(和”隔壁”的架构差不多, 只是用 openresty 替代了 filebeat)</p><p>看图…</p><a id="more"></a><p><img src="/resource/1635ed2790c7497cbc3d358a79fa9c45.png" alt="4586dc3fb08abfda28c6c04ca69b92b1.png"></p><p>这里之所以没有使用 kafka 还是因为量级的原因, 并且 logstash 自带本地持久化</p><ol><li>对于数据的并发性没有高要求</li><li>对于数据的可靠性没有高要求</li></ol><p>我们是这么考虑的</p><ol><li>目前云主机宕机已经很少了</li><li>而且恰好在宕机的时候把硬盘也搞坏了</li><li>而且恰好硬盘里还存留着没有发送的数据</li><li>最后云主机磁盘的 RAID 也跟着坏了.</li></ol><p>即使这些情况都发生了, 最多就是丢失一些上报数据, 完全不响应大局<br>综上所述, 我们没有上 kafka 避免增加维护成本.</p><h1 id="实施"><a href="#实施" class="headerlink" title="实施"></a>实施</h1><h2 id="ELK"><a href="#ELK" class="headerlink" title="ELK"></a>ELK</h2><p>ELK 老生常谈了, 在我们这个数量级的情况下大同小异<br>没什么特殊的地方, 想看请 <a href="/2020/05/02/服务端业务异常日志监控#实施">出门左转</a></p><h2 id="openresty"><a href="#openresty" class="headerlink" title="openresty"></a>openresty</h2><p>openresty 这块, 特意去翻了翻代码<br>发现也没什么好说的, 就是收到请求后<br>使用 resty.http 模块向 logstash 发送请求</p><p>唯一有点需要注意的是, 我们使用使用 json 格式传输<br>为了不影响用户的体验, 发生事件后, 将事件数据 push 进一个数组<br>然后间隔一分钟将整个数组上报</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">[</span><br><span class="line"> {</span><br><span class="line"> 消息1</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> 消息2</span><br><span class="line"> }</span><br><span class="line">]</span><br></pre></td></tr></table></figure><p>哦哦哦, 对了, 还有一点需要注意.<br>因为面向的是多项目的, 所以可能存在不同版本兼容的问题.<br>可以根据项目对数据格式做解析, 也可以定义版本, 根据版本做解析.<br>这里是根据项目对解析的, 大概是这么个流程</p><p>客户端 -> 网关入口 -> 项目解析 -> 标准格式 -> 请求 logstash</p><h2 id="埋点"><a href="#埋点" class="headerlink" title="埋点"></a>埋点</h2><p>重点来了, 重点来了, 重点来了<br>这里最难的点在于需要和不同项目组的人<code>沟通</code>.</p><ol><li>得先让和他们解释做这个能带来什么<code>帮助</code></li><li>按照文档标准沟通清楚执行并且最后<code>验收</code></li><li>考虑不同项目的情况是否能够<code>实现</code></li></ol><p>先来看看我们是怎么落地的</p><h3 id="CDN-请求超时"><a href="#CDN-请求超时" class="headerlink" title="CDN 请求超时"></a>CDN 请求超时</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">在http请求开始时记录开始时间.</span><br><span class="line">在http请求响应时记录结束时间.</span><br><span class="line"></span><br><span class="line">请求耗时 = 结束时间 - 开始时间</span><br><span class="line"></span><br><span class="line">if 请求耗时 > 5秒 then</span><br><span class="line"> 触发CDN请求超时事件</span><br><span class="line"> (记录耗时时间, 账号ID, 请求资源名等...)</span><br></pre></td></tr></table></figure><h3 id="CDN-请求失败"><a href="#CDN-请求失败" class="headerlink" title="CDN 请求失败"></a>CDN 请求失败</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">当请求HTTP失败时</span><br><span class="line"> 触发CDN请求失败事件</span><br><span class="line"> (记录耗时时间, 账号ID, 错误类型, 请求资源名等...)</span><br></pre></td></tr></table></figure><h3 id="请求服务端异常"><a href="#请求服务端异常" class="headerlink" title="请求服务端异常"></a>请求服务端异常</h3><p>这里和 CDN 请求有些类似, 就不重复了.<br>区别在于, 这个是用于检测用户连接服务器的状态.<br>可能存在服务端系统问题, 服务端代码问题, 服务端网络问题</p><p>同时这个上报也是需要区分项目的<br>如果项目的消息协议是一来一回的, 比如这样</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">client ----> client_request_login ----> server</span><br><span class="line">server ----> client_request_login_return ----> client</span><br></pre></td></tr></table></figure><p>那这个事情就很好办了, 因为客户端知道收到消息和发送消息如何对应</p><p>如果项目的消息协议是一来多回的, 比如这样</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">client ----> client_request_login ----> server</span><br><span class="line">server ----> update_user_coin ----> client</span><br><span class="line">server ----> update_user_info ----> client</span><br><span class="line">server ----> update_user_friends ----> client</span><br></pre></td></tr></table></figure><p>那就没戏, 因为没法知道收到的消息对应哪条发送的消息, 也就没法判断了.</p><p>有个办法, 在协议上添加一个字段用于请求表示<br>发送协议时和返回协议时都有对应的请求标识<br>但是这样改造的成本较高, 而且项目方不一定愿意配合修改<br>因为投入收益比太低, 所以这一块不太好落地</p><h3 id="客户端代码异常"><a href="#客户端代码异常" class="headerlink" title="客户端代码异常"></a>客户端代码异常</h3><p>这个其实也很简单, 当客户端代码执行异常时.<br>触发一个异常事件, 等待下次一起上报.</p><p>有个重点是对上报的异常信息要做<code>错误位置</code>截取<br>这是为了方便统计同一个错误总共发生了多少次<br>从而优先解决出现率高的异常错误</p><p>我们使用 logstash 的 grok 实现的, 虽然效率不高, 但也能满足我们的需求</p><p>举个例子</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">TypeError: Cannot read property 'itemConfig' of null</span><br><span class="line"> at e.IsHas_37TypeItem (https://www.baidu.com/gres/g6/rel/1100/main_6_1100.min.js:2:7760)</span><br><span class="line"> at e.doUpDataItem (https://www.baidu.com/gres/g6/rel/1100/main_6_1100.min.js:2:7140)</span><br><span class="line"> at Function.t.HandlerType (https://www.baidu.com/gres/g6/rel/1100/main_6_1100.min.js:33:14188)</span><br><span class="line"> at Function.e.Dispatch (https://www.baidu.com/gres/g6/rel/1100/main_6_1100.min.js:13:14669)</span><br><span class="line"> at t.UpdateDispatch (https://www.baidu.com/gres/g6/rel/1100/main_6_1100.min.js:222:15347)</span><br><span class="line"> at i.update (https://www.baidu.com/gres/g6/res/start_res/991/merged1.js:4:9583)</span><br><span class="line"> at t (https://www.baidu.com/gres/g6/res/start_res/991/merged1.js:10:11261)</span><br></pre></td></tr></table></figure><p>像上面的报错信息, 我们需要截取<code>main_6_1100.min.js:2:7760</code>来作为错误位置来进行聚合统计<br>可以使用下面这个 grok 的表达式来捕获这个错误位置的信息</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">grok {</span><br><span class="line"> match => {</span><br><span class="line"> "message" => [</span><br><span class="line"> "^.+?(?<fileline>[\w\.]+\.js:[0-9]+:[0-9]+).+$"</span><br><span class="line"> ]</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这样就会在上报的数据中添加了一个 fileline 字段来代表异常错误的位置信息.</p><h2 id="监控"><a href="#监控" class="headerlink" title="监控"></a>监控</h2><p><a href="/2020/05/02/服务端业务异常日志监控#监控检测">出门左转</a></p><h1 id="效果"><a href="#效果" class="headerlink" title="效果"></a>效果</h1><h2 id="请求-CDN-监控效果"><a href="#请求-CDN-监控效果" class="headerlink" title="请求 CDN 监控效果"></a>请求 CDN 监控效果</h2><p><img src="/resource/e710d0ac53724f4392bff6fd8a9a7ce6.png" alt="3f1a8432458d331ceed529efbb7f4a6f.png"><br><img src="/resource/a7ae6e6d3af24da78d4b378b88f06e78.png" alt="92236531cae1cda90fcb45dfb4ea626a.png"></p><h2 id="请求服务端监控效果"><a href="#请求服务端监控效果" class="headerlink" title="请求服务端监控效果"></a>请求服务端监控效果</h2><p><img src="/resource/832916c39a744c05b56a68120e75dac5.png" alt="c6620b1ced4f746d010d986db874e6be.png"><br><img src="/resource/6fbc3ad772df4ad190a8d1690351ff33.png" alt="1f70c7b0bccc7328e117701e4165e1b7.png"></p><p>这里解释下, 为什么上图请求服务端的失败统计表为空.<br>是因为请求服务端失败时记录协议 ID 实现的<code>成本较高</code><br>而根据协议 ID 统计出的信息也不具备<code>指导性意见</code></p><p>所以我们就没有纠结这个问题了</p><h2 id="客户端代码异常监控效果"><a href="#客户端代码异常监控效果" class="headerlink" title="客户端代码异常监控效果"></a>客户端代码异常监控效果</h2><p><img src="/resource/cf00887671d842e6933586d29db7c16e.png" alt="ff97ff82de4fc84b22f2fa4f2027426a.png"></p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>就在写这个 Blog 的同时, 我们业务的 CDN 发生了一次故障.</p><p><img src="/resource/5a5e8bfaeb6e474595311f81451b1317.png" alt="630f8e9976b9768a2a47b2ce55fbd817.png"></p><p><img src="/resource/9a0668eb3bd44c878f5b260dd1fe59f1.png" alt="ec2dbdd7f4cc1e76e3f0a4de2450532b.png"></p><p>然而我们并没有做相应的监控, 导致响应不及时(主要是没时间精力)<br>由此可见这块监控效果如何, 如果我们能及时的监控报警<br>那么整个团队面对问题的响应速度会非常的块, 从而提升我们产品的质量</p><p>说了好的, 总要说说坏的.</p><ol><li>只能被动的发现问题, 但我们的目标是没有蛀牙</li><li>根据项目的不同, 难以实现统一标准, 维护成本过高</li><li>监控的覆盖率较低, 只能发现重大问题, 对于平时的运营帮助较小</li></ol><p>以上</p>]]></content>
<summary type="html">
<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>我们是一家 H5 的游戏公司<br>客户端以网页, 小程序, APP 的方式发布<br>对 CDN 的请求非常的大, 所以 CDN 请求的影响是非常大的.</p>
<h1 id="前提"><a href="#前提" class="headerlink" title="前提"></a>前提</h1><p>其实一开始是想放在 <a href="/2020/05/02/服务端业务异常日志监控/">服务端业务异常日志监控</a> 一起讲的<br>然而因为这里有一些情况不太一样, 细讲起来差异还挺大的.<br>所以这里就拆开单独写了(真的不是为了凑数!!!)</p>
<h1 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h1><ol>
<li>客户端请求 CDN 卡顿</li>
<li>客户端请求服务端卡顿</li>
<li>客户端代码异常</li>
</ol>
<h1 id="方案"><a href="#方案" class="headerlink" title="方案"></a>方案</h1><p>elasticsearch, 存储检索<br>logstash, 上报格式转换<br>kibana, 统计展示<br>openresty, 上报网关<br>(和”隔壁”的架构差不多, 只是用 openresty 替代了 filebeat)</p>
<p>看图…</p>
</summary>
<category term="监控" scheme="https://zhiwei.show/tags/%E7%9B%91%E6%8E%A7/"/>
<category term="elk" scheme="https://zhiwei.show/tags/elk/"/>
</entry>
<entry>
<title>业务监控整理总结</title>
<link href="https://zhiwei.show/993b27b34e7a4165a667a75dcf128296/"/>
<id>https://zhiwei.show/993b27b34e7a4165a667a75dcf128296/</id>
<published>2020-03-18T18:33:00.000Z</published>
<updated>2020-03-18T18:33:00.000Z</updated>
<content type="html"><![CDATA[<h1 id="监控范围"><a href="#监控范围" class="headerlink" title="监控范围"></a>监控范围</h1><ol><li>用户体验跟踪</li><li>应用质量跟踪</li><li>应用性能跟踪</li></ol><p>跟踪相对指标, 不是绝对指标</p><h1 id="监控分层"><a href="#监控分层" class="headerlink" title="监控分层"></a>监控分层</h1><h2 id="用户体验监控-前端"><a href="#用户体验监控-前端" class="headerlink" title="用户体验监控(前端)"></a>用户体验监控(前端)</h2><p>因成本(研发成本)原因忽略用户手机的性能</p><p>只关注<code>请求超时</code>和<code>请求错误</code>的记录</p><p>因为成本(研发成本)原因无法获取所有的记录</p><p>所以我们使用<code>总用户数</code>作为比较的指标</p><a id="more"></a><h3 id="CDN-加载监控"><a href="#CDN-加载监控" class="headerlink" title="CDN 加载监控"></a>CDN 加载监控</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line">基础点位</span><br><span class="line"> 必定加载的资源点->用户id去重->总用户数</span><br><span class="line">加载超时(5s)</span><br><span class="line"> 记录结构</span><br><span class="line"> 超时时间</span><br><span class="line"> 资源名</span><br><span class="line"> 资源大小</span><br><span class="line"> 平台id</span><br><span class="line"> 用户id</span><br><span class="line"> 统计指标</span><br><span class="line"> 超时记录数</span><br><span class="line"> 超时用户数</span><br><span class="line"> 超时资源名Top10</span><br><span class="line"> 趋势指标</span><br><span class="line"> 超时用户比</span><br><span class="line"> 超时用户 / 总用户数</span><br><span class="line"> 每个超时用户的超时次数</span><br><span class="line"> 超时次数 / 超时用户数</span><br><span class="line">加载失败</span><br><span class="line"> 记录结构</span><br><span class="line"> 错误状态(一般是http status_code)</span><br><span class="line"> 资源名</span><br><span class="line"> 资源大小</span><br><span class="line"> 平台id</span><br><span class="line"> 用户id</span><br><span class="line"> 统计指标</span><br><span class="line"> 失败记录数</span><br><span class="line"> 失败用户数</span><br><span class="line"> 失败资源名Top10</span><br><span class="line"> 趋势指标</span><br><span class="line"> 失败用户比</span><br><span class="line"> 失败用户 / 总用户数</span><br><span class="line"> 每个失败用户的失败次数</span><br><span class="line"> 失败次数 / 失败用户数</span><br></pre></td></tr></table></figure><h3 id="应用消息监控"><a href="#应用消息监控" class="headerlink" title="应用消息监控"></a>应用消息监控</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line">基础点位</span><br><span class="line"> 必定加载的资源点->用户id去重->总用户数</span><br><span class="line">消息超时</span><br><span class="line"> 描述</span><br><span class="line"> 用户点击按钮后从 `发起请求->服务器处理->收到响应` 的时间</span><br><span class="line"> 记录方式</span><br><span class="line"> 在客户端进行记录, 当收到响应消息时, 判断是否超时</span><br><span class="line"> 记录结构</span><br><span class="line"> 超时时间</span><br><span class="line"> 超时消息名</span><br><span class="line"> 用户id</span><br><span class="line"> 平台id</span><br><span class="line"> 统计指标</span><br><span class="line"> 超时数量</span><br><span class="line"> 超时用户</span><br><span class="line"> 超时消息Top10</span><br><span class="line"> 趋势指标</span><br><span class="line"> 超时用户比</span><br><span class="line"> 超时用户 / 总用户数</span><br><span class="line"> 每个超时用户的超时次数</span><br><span class="line"> 超时次数 / 超时用户数</span><br><span class="line">消息错误</span><br><span class="line"> 描述</span><br><span class="line"> 经了解, 目前只能监控websocket链接的异常</span><br><span class="line"> 记录方式</span><br><span class="line"> 当出现请求错误时上报错误信息</span><br><span class="line"> 记录结构</span><br><span class="line"> 错误状态</span><br><span class="line"> 错误消息名</span><br><span class="line"> 用户id</span><br><span class="line"> 平台id</span><br><span class="line"> 统计指标</span><br><span class="line"> 错误数量</span><br><span class="line"> 错误用户数</span><br><span class="line"> 错误消息Top10</span><br><span class="line"> 趋势指标</span><br><span class="line"> 错误用户比</span><br><span class="line"> 错误用户 / 总用户数</span><br><span class="line"> 每个错误用户的错误次数</span><br><span class="line"> 错误次数 / 错误用户数</span><br></pre></td></tr></table></figure><h3 id="登录-充值响应监控"><a href="#登录-充值响应监控" class="headerlink" title="登录/充值响应监控"></a>登录/充值响应监控</h3><p>登录/充值记录的数量总体上不大, 所以采用全量的方式采集.</p><p>难点在于对 SDK 的监控, 不一定能够监控, 但却是个非常重要的指标.</p><p>虽然很重要, 但涉及到合作方, 推进成本较高, 所以<code>暂缓不做</code>.</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line">充值SDK</span><br><span class="line"> 描述</span><br><span class="line"> 监控用户点击登录/充值按钮时跟踪SDK的响应时间</span><br><span class="line"> 监控方式</span><br><span class="line"> 当用户打开充值面板后tick一次</span><br><span class="line"> 当充值面板关闭后tick一次</span><br><span class="line"> 记录结构</span><br><span class="line"> 开始期间</span><br><span class="line"> 结束时间</span><br><span class="line"> 用户id</span><br><span class="line"> 平台id</span><br><span class="line"> 统计指标</span><br><span class="line"> 充值SDK响应时间</span><br><span class="line"> 趋势指标</span><br><span class="line"> 充值SDK响应时间</span><br><span class="line">发货记录</span><br><span class="line"> 描述</span><br><span class="line"> 用来监控用户充值后发货的的时间</span><br><span class="line"> 监控方式</span><br><span class="line"> 当用户完全支付后tick一次</span><br><span class="line"> 收到发货物品后tick一次</span><br><span class="line"> 记录结构</span><br><span class="line"> 开始期间</span><br><span class="line"> 结束时间</span><br><span class="line"> 用户id</span><br><span class="line"> 平台id</span><br><span class="line"> 统计指标</span><br><span class="line"> 充值SDK响应时间</span><br><span class="line"> 趋势指标</span><br><span class="line"> 充值SDK响应时间</span><br></pre></td></tr></table></figure><h2 id="服务质量监控-后端"><a href="#服务质量监控-后端" class="headerlink" title="服务质量监控(后端)"></a>服务质量监控(后端)</h2><h3 id="应用质量监控"><a href="#应用质量监控" class="headerlink" title="应用质量监控"></a>应用质量监控</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line">消息超时</span><br><span class="line"> 描述</span><br><span class="line"> 用来监控用户服务内消息请求处理超时</span><br><span class="line"> 监控方式</span><br><span class="line"> 当单个消息执行过长时打印log</span><br><span class="line"> 记录结构</span><br><span class="line"> 消息名</span><br><span class="line"> 时间</span><br><span class="line"> 用户id</span><br><span class="line"> 平台id</span><br><span class="line"> 统计指标</span><br><span class="line"> 超时数</span><br><span class="line"> 消息名TOP10</span><br><span class="line"> 趋势指标</span><br><span class="line"> 超时用户比</span><br><span class="line"> 超时用户数 / 总用户数</span><br><span class="line"> 每个超时用户的超时次数</span><br><span class="line"> 超时次数 / 超时用户数</span><br><span class="line"></span><br><span class="line">消息错误</span><br><span class="line"> 描述</span><br><span class="line"> 当服务端错误时用户出现的卡死情况</span><br><span class="line"> 区别于消息异常, 这种错是服务端可以捕获的</span><br><span class="line"> 监控方式</span><br><span class="line"> 当消息执行出错时打印log</span><br><span class="line"> 记录结构</span><br><span class="line"> 消息名</span><br><span class="line"> 时间</span><br><span class="line"> 用户id</span><br><span class="line"> 平台id</span><br><span class="line"> 统计指标</span><br><span class="line"> 错误数</span><br><span class="line"> 消息名TOP10</span><br><span class="line"> 趋势指标</span><br><span class="line"> 错误用户比</span><br><span class="line"> 错误用户数 / 总用户数</span><br><span class="line"> 每个错误用户的错误次数</span><br><span class="line"> 超时次数 / 错误用户数</span><br><span class="line"></span><br><span class="line">消息异常(后端)</span><br><span class="line"> 描述</span><br><span class="line"> 当服务端异常时用户出现的卡死情况</span><br><span class="line"> 监控方式</span><br><span class="line"> 一般不需处理, 通过log来捕获</span><br><span class="line"> 记录结构</span><br><span class="line"> 记录结构不可控</span><br><span class="line"> 统计指标</span><br><span class="line"> 异常数</span><br><span class="line"> 异常文件TOP10</span><br></pre></td></tr></table></figure><h3 id="中间件质量监控"><a href="#中间件质量监控" class="headerlink" title="中间件质量监控"></a>中间件质量监控</h3><p>对基础设施要求较高, 暂时不做</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">nginx(websocket的跟踪, 有点难)</span><br><span class="line"> 连接数, 请求数, 响应时间</span><br><span class="line">mysql</span><br><span class="line"> 连接数, 慢日志, 错误日志</span><br></pre></td></tr></table></figure><h2 id="服务性能监控-后端"><a href="#服务性能监控-后端" class="headerlink" title="服务性能监控(后端)"></a>服务性能监控(后端)</h2><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">基本思路:</span><br><span class="line"></span><br><span class="line">需要应用暴露接口, 通过定期抓取的方式获取应用的状态</span><br><span class="line"></span><br><span class="line">应用指标, 每秒请求数, 每秒事务数, 响应时间, 吞吐量</span><br><span class="line"></span><br><span class="line">系统指标, CPU占用比, 内存占用比, IO占用比</span><br><span class="line"></span><br><span class="line">收益比低, 暂时不做</span><br></pre></td></tr></table></figure><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>我们监控跟踪的目的有两个</p><ol><li>提升用户的体验</li><li>降低运营的成本</li></ol><p><code>用户体验监控</code>是为了能够长期跟踪用户的体验</p><p><code>服务质量监控</code>是为了找到科学的方式去改进用户的体验</p><p><code>服务性能监控</code>是为了找到服务的瓶颈提高服务的事务能力, 在规模较小, 基础不完善的情况下, 收益比很低, 暂时不做</p>]]></content>
<summary type="html">
<h1 id="监控范围"><a href="#监控范围" class="headerlink" title="监控范围"></a>监控范围</h1><ol>
<li>用户体验跟踪</li>
<li>应用质量跟踪</li>
<li>应用性能跟踪</li>
</ol>
<p>跟踪相对指标, 不是绝对指标</p>
<h1 id="监控分层"><a href="#监控分层" class="headerlink" title="监控分层"></a>监控分层</h1><h2 id="用户体验监控-前端"><a href="#用户体验监控-前端" class="headerlink" title="用户体验监控(前端)"></a>用户体验监控(前端)</h2><p>因成本(研发成本)原因忽略用户手机的性能</p>
<p>只关注<code>请求超时</code>和<code>请求错误</code>的记录</p>
<p>因为成本(研发成本)原因无法获取所有的记录</p>
<p>所以我们使用<code>总用户数</code>作为比较的指标</p>
</summary>
<category term="监控" scheme="https://zhiwei.show/tags/%E7%9B%91%E6%8E%A7/"/>
<category term="复盘" scheme="https://zhiwei.show/tags/%E5%A4%8D%E7%9B%98/"/>
<category term="总结" scheme="https://zhiwei.show/tags/%E6%80%BB%E7%BB%93/"/>
</entry>
<entry>
<title>服务端业务异常日志监控</title>
<link href="https://zhiwei.show/f2293fe655bd4009a7eef9c1fdff8dc5/"/>
<id>https://zhiwei.show/f2293fe655bd4009a7eef9c1fdff8dc5/</id>
<published>2020-03-12T01:03:00.000Z</published>
<updated>2020-03-12T01:03:00.000Z</updated>
<content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>我们是一家 H5 的游戏公司<br>多数项目的服务端使用 SKYNET+LUA 的形式开发<br>协议层走的的 websocket</p><h1 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h1><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">一般情况下我们都是走这样的流程</span><br><span class="line"></span><br><span class="line">1. 业务更新版本</span><br><span class="line">2. 等待用户反馈问题</span><br><span class="line">3. 判断客户端还是服务端</span><br><span class="line">4. 根据用户信息查看日志</span><br><span class="line">5. 修复BUG更新版本</span><br><span class="line"></span><br><span class="line">解决问题被动而且效率低下.</span><br><span class="line">大量体验不好的用户直接流失.</span><br></pre></td></tr></table></figure><a id="more"></a><h1 id="方案"><a href="#方案" class="headerlink" title="方案"></a>方案</h1><p>我们采用目前主流的 ELK 方案来解决这个问题</p><p>使用 elasticsearch 在日志存储<br>使用 logstash 做日志标准化<br>使用 kibana 做统计展示</p><p>另外<br>使用 filebeat 来做日志收集<br>使用使用 Crontab, Python 在实现日志监控<br>使用 DingTalk 接受告警信息</p><p>使用 ELK 是因为主流, 坑少, 资料多(不喜欢 java, 太笨重)<br>使用 filebeat 是因为原配, 小三的不要(漂亮的还是可以考虑的)<br>使用 Crontab, Python 是因为 es 的 watch 不能满足我们(需求第一)<br>使用 DingTalk 就要问老板了(老板第一)</p><p>没有使用 Kafka 是因为没有数据级和可靠性的需求<br>即使后面有了需求, 加入 Kafka 也不是多大的事</p><p>看图…</p><p><img src="/resource/7e01327bb65c4d9785793e471e3ddef2.png" alt="5013d8139a7ef721ed4e2cc6bb7b0020.png"></p><h1 id="实施"><a href="#实施" class="headerlink" title="实施"></a>实施</h1><h2 id="filebeat"><a href="#filebeat" class="headerlink" title="filebeat"></a>filebeat</h2><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">1. 使用Saltstack部署filebeat并且以后主机上线后会自动部署</span><br><span class="line"> 新上线的主机在镜像中已经安装了salt-minion</span><br><span class="line"> 启动后会自动连上salt-master然后获取自己应该执行的任务</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">2. 使用include_lines选项来包括需要监控的关键字</span><br><span class="line"> 我们的日志量很多, 又想节约成本, 所以只传输我们关心的日志.</span><br><span class="line"></span><br><span class="line">3. 使用multiline.pattern来合并多行日志</span><br><span class="line"> 老生常谈的问题, 主要解决日志多行的问题.</span><br><span class="line"></span><br><span class="line">4. 使用output.logstash传输日志到logstash</span><br><span class="line"> # 随机获取不同的主机来传输日志, 既可以实现负载又可以实现高可用</span><br><span class="line"> # 真是完美啊, 胖客户端才是王道!!!</span><br><span class="line"> hosts: ["192.168.1.101:9100", "192.168.1.102:9100", "192.168.1.103:9100"]</span><br><span class="line"> loadbalance: true</span><br></pre></td></tr></table></figure><h2 id="logstash"><a href="#logstash" class="headerlink" title="logstash"></a>logstash</h2><p>先来看看配置文件</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"># 多少个干活的</span><br><span class="line"># 进程相当于产品经理, worker相当于程序员的数量</span><br><span class="line">pipeline.workers: 8</span><br><span class="line"></span><br><span class="line"># 一次处理一批, 效率高啊</span><br><span class="line"># 就像每次吃瓜子都是抓一把</span><br><span class="line">pipeline.batch.size: 10000</span><br><span class="line"></span><br><span class="line"># 收到的数据先写入磁盘</span><br><span class="line"># 不然进程崩了, 系统挂了, 数据就丢失了</span><br><span class="line">queue.type: persisted</span><br><span class="line">path.queue: /usr/share/logstash/queue</span><br><span class="line"></span><br><span class="line"># 以文件的形式存储, 一个文件的大小</span><br><span class="line">queue.page_capacity: 256mb</span><br><span class="line"></span><br><span class="line"># 不限制最大事件数</span><br><span class="line">queue.max_events: 0</span><br><span class="line"></span><br><span class="line"># 限制最大使用80G硬盘</span><br><span class="line"># 别问为什么, 问就是不知道.</span><br><span class="line"># 猜测可能是为了避免出了问题没人发现吧.</span><br><span class="line">queue.max_bytes: 81960mb</span><br></pre></td></tr></table></figure><p>然后是 logstash 的 pipeline<br>这里举一小段例子</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"># 匹配服务端超时的日志</span><br><span class="line">if "Cost Time Too Long" in [message] {</span><br><span class="line"> grok {</span><br><span class="line"> match => {</span><br><span class="line"> "message" => [</span><br><span class="line"> "^.*\[%{TIMESTAMP_ISO8601:timestamp}\].+Cost Time Too Long (?<message_name>\w+) (?<elapsed_time>[0-9.]+) (?<online_time>[0-9]*).*",</span><br><span class="line"> "^.*\[%{TIMESTAMP_ISO8601:timestamp}\].+Cost Time Too Long (?<message_name>\w+) (?<elapsed_time>[0-9.]+).*"</span><br><span class="line"> ]</span><br><span class="line"> }</span><br><span class="line"> add_tag => ["TIMEOUT"]</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"># 匹配服务端用户登录的日志</span><br><span class="line">} else if "Player:Login" in [message] {</span><br><span class="line"> grok {</span><br><span class="line"> match => {</span><br><span class="line"> "message" => [</span><br><span class="line"> "^.*\[%{TIMESTAMP_ISO8601:timestamp}\].+Player:Login \w+ \w+ (?<user_id>[^ ]+).*"</span><br><span class="line"> ]</span><br><span class="line"> }</span><br><span class="line"> add_tag => ["LOGIN"]</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"># 匹配服务端所有错误的日志</span><br><span class="line">} else {</span><br><span class="line"> grok {</span><br><span class="line"> match => {</span><br><span class="line"> "message" => [</span><br><span class="line"> "^.*\[%{TIMESTAMP_ISO8601:timestamp}\].*?stack traceback:.*?/(?<fileline>[^/]*.lua:[0-9]+):.*$"</span><br><span class="line"> ]</span><br><span class="line"> }</span><br><span class="line"> add_tag => ["ERROR"]</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>因为我们的日志格式都是非标准的, 日志等级也是很随便的<br>这就需要我们针对每个项目的格式进行转换和处理<br>以保证最终的格式都是一致的(制定了标准也很难推进)</p><h2 id="elasticsearch"><a href="#elasticsearch" class="headerlink" title="elasticsearch"></a>elasticsearch</h2><p>配置就不说了, 都是标配<br>主要注意的是分片和 index 名字的管理</p><ol><li><p>主分片为节点数(最高写入)</p></li><li><p>副本数为 1(数据不丢)</p></li><li><p>同一个主分片和副本不能在一台机器上, 不然机器挂了就完了</p></li><li><p>单个分片的大小在内存大小左右就行了, 毕竟是要把数据装入内存</p></li><li><p>index 的名字要么使用日期的形式命名要么使用 ilm 来自动管理<br>使用日期可以灵活的对某一个日期的 index 做处理<br>但是需要手动删除过期的数据</p><p>使用 ilm 管理就只能通过 RESTAPI 来进行数据处理<br>好处是不用自己删除过期数据</p></li></ol><h2 id="kibana"><a href="#kibana" class="headerlink" title="kibana"></a>kibana</h2><p>主要使用的是 TSVB 组件和 TABLE 组件<br>这个 TSVB 组件还不错, 就是有些需求实现不了<br>可能是我学艺不精, 毕竟才用了一个月</p><p>table 组件也是奇葩的很, 创建的时候就指定了 index name.<br>每次都只能修改 kibana 的 obejct 也是心累啊</p><h2 id="监控检测"><a href="#监控检测" class="headerlink" title="监控检测"></a>监控检测</h2><p>前面也提到为什么我们没有使用 es 的 watch 功能<br>主要是希望能提高告警的准确性反应出问题<br>我们策略是这样的, 取一小时内的数据</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">当前异常比 = 当前异常数 / 当前用户数</span><br><span class="line">昨日异常比 = 昨日异常数 / 昨日用户数</span><br><span class="line"></span><br><span class="line">相差异常比 = 当前异常比 / 昨日异常比</span><br><span class="line"></span><br><span class="line">if 昨日相差异常比 > 2 and 前时相差异常比</span><br><span class="line"> 报警</span><br></pre></td></tr></table></figure><p>这里还可以优化一下<br>即使当前的相差比昨天的大, 也不能隔一小时报警一次.<br>违背了我们的初心, 所以升级一下策略</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">昨日相差异常比 = 当前异常比 / 昨日异常比</span><br><span class="line">前时相差异常比 = 当前异常比 / 前时异常比</span><br><span class="line"></span><br><span class="line">if 昨日相差异常比 > 2 and 前时相差异常比 > 2</span><br><span class="line"> 报警</span><br></pre></td></tr></table></figure><p>为什么是 > 2, 这个也是拍脑袋想的<br>目前暂时没有想到更好的方法来动态调整.<br>有一个思路是根据<code>异常数的差</code>算出临界值, 只是一种思路.</p><h2 id="报警通知"><a href="#报警通知" class="headerlink" title="报警通知"></a>报警通知</h2><p>我们在 DingTalk 上封装了一层接口</p><ol><li>集成各种报警第三方接口</li><li>实现告警的重复收敛</li></ol><p>目前我们 Crontab 的时间间隔是一小时.<br>这个时间间隔目前来看可能没有太大的问题.</p><p>随着项目的发展, 可能会出现报警不及时的问题.<br>所以需要配合重复收敛功能来实现及时告警, 同时又不会重复的告警<br>完美…</p><h1 id="效果"><a href="#效果" class="headerlink" title="效果"></a>效果</h1><p>先看效果, 再谈疗程(屏蔽业务数据)</p><h2 id="统计效果"><a href="#统计效果" class="headerlink" title="统计效果"></a>统计效果</h2><p><img src="/resource/0ccaae3e3b304fbaafa6148f1c24cd63.png" alt="fd033fc85e475f9df95302130b8688a2.png"></p><h2 id="监控效果"><a href="#监控效果" class="headerlink" title="监控效果"></a>监控效果</h2><p><img src="/resource/1c2e7be552b64d74bb9e98f5823a8d22.png" alt="31312d42e26df1a9d7faf29343949208.png"></p><p>这里说说为啥会有个用户数的指标.<br>我们是这么考虑的</p><p>单纯的看日志的异常数是不准确的.<br>因为日志的异常数是随着玩家的增加而增加的.<br>很容易出现, 同样一个问题, 因为玩家增加而多次报警.<br>形成”狼来了”的感觉<br>所以这里使用用户数来做参考.</p><p>这里的用户数是登录数.<br>其实使用在线用户数效果会更好.<br>只是实现的成本较高, 以后再进行迭代.</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>首先这个系统做下来来, 用过的研发都说好.<br>毕竟研发也没有时间天天去看日志.<br>每次等用户发现 BUG 反馈过来, 太过于被动了.</p><p>其次, 通过聚合统计能够发现影响重大的在哪, 集中发力</p><p>当然也有一些问题存在</p><ol><li><p>目前无法快速的接入项目<br>运维标准化和日志标准化的问题<br>每个项目可能会有不同的监控需求</p></li><li><p>只监控了错误日志, 用户请求的超时并没有监控.<br>研发已经没有精力做体验上的优化了, 卡个几秒大家觉得可以接受了.</p></li><li><p>异常信息挖掘<br>集合其他主机监控, 服务指标, 业务指标来从多维度来快速定位问题.</p></li><li><p>只能发现问题, 没法解决问题<br>如果产品初期管理不善, 那积累起来的历史问题会完全无从下手</p></li></ol><p>以上</p>]]></content>
<summary type="html">
<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>我们是一家 H5 的游戏公司<br>多数项目的服务端使用 SKYNET+LUA 的形式开发<br>协议层走的的 websocket</p>
<h1 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h1><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">一般情况下我们都是走这样的流程</span><br><span class="line"></span><br><span class="line">1. 业务更新版本</span><br><span class="line">2. 等待用户反馈问题</span><br><span class="line">3. 判断客户端还是服务端</span><br><span class="line">4. 根据用户信息查看日志</span><br><span class="line">5. 修复BUG更新版本</span><br><span class="line"></span><br><span class="line">解决问题被动而且效率低下.</span><br><span class="line">大量体验不好的用户直接流失.</span><br></pre></td></tr></table></figure>
</summary>
<category term="监控" scheme="https://zhiwei.show/tags/%E7%9B%91%E6%8E%A7/"/>
<category term="elk" scheme="https://zhiwei.show/tags/elk/"/>
<category term="日志" scheme="https://zhiwei.show/tags/%E6%97%A5%E5%BF%97/"/>
</entry>
<entry>
<title>2019年的工作整理总结</title>
<link href="https://zhiwei.show/c5cdc0da435e494f8037967c4df7af78/"/>
<id>https://zhiwei.show/c5cdc0da435e494f8037967c4df7af78/</id>
<published>2020-02-07T02:27:00.000Z</published>
<updated>2020-02-07T02:27:00.000Z</updated>
<content type="html"><![CDATA[<h1 id="2019-年终整理"><a href="#2019-年终整理" class="headerlink" title="2019 年终整理"></a>2019 年终整理</h1><h2 id="运维平台"><a href="#运维平台" class="headerlink" title="运维平台"></a>运维平台</h2><h3 id="CMDB"><a href="#CMDB" class="headerlink" title="CMDB"></a>CMDB</h3><p>我们目前已经接入了腾讯云的 dntg 项目</p><p>通过 canal+kafka 和额外的监控保证数据的及时性和准确性</p><p>在多项目和多云环境上还需要进行迭代完善</p><h3 id="维护功能"><a href="#维护功能" class="headerlink" title="维护功能"></a>维护功能</h3><p>正常的发展应该是 <code>手工->脚本->平台->自动化</code> 这个流程</p><p>然而我们直接抛开了<code>平台</code>这个阶段, 直接做了自动化.</p><p>导致我们后期的工作不好展开(主要是标准化的问题)</p><p>所以要将标准化的优先级提高</p><a id="more"></a><h3 id="巡检系统"><a href="#巡检系统" class="headerlink" title="巡检系统"></a>巡检系统</h3><p>主要是系统资源和业务状态的巡检</p><p>目前基于 openfalcon 做的有些鸡肋, 无法通过时间筛选</p><p>后期计划通过 openfalcon+elasticsearch 来实现</p><h3 id="账单系统"><a href="#账单系统" class="headerlink" title="账单系统"></a>账单系统</h3><p>主要是基于腾讯云的账单进行分析实现的.</p><p>所以在腾讯云的项目可以实现复用.</p><p>但是在其他云环境上就需要再次开发了.</p><h3 id="定时任务系统"><a href="#定时任务系统" class="headerlink" title="定时任务系统"></a>定时任务系统</h3><p>集中管理服务器的周期任务</p><p>包括周期任务和定时任务</p><p>这一块还在构思</p><h2 id="自动化"><a href="#自动化" class="headerlink" title="自动化"></a>自动化</h2><h3 id="部署"><a href="#部署" class="headerlink" title="部署"></a>部署</h3><p>目前来看已经是最优解</p><p>剩下的是标准化和集成其他项目</p><h3 id="迁移"><a href="#迁移" class="headerlink" title="迁移"></a>迁移</h3><p>目前一个人能在”半小时”内完成迁移计划(无论迁移规模大小)</p><p>后期自动化主要是在于</p><ol><li><p>集成运维平台(需要标准化)</p></li><li><p>钉钉审批</p></li></ol><h3 id="合服"><a href="#合服" class="headerlink" title="合服"></a>合服</h3><p>受限于业务架构和数据风险</p><p>一直没有开始做自动化合服, 主要人力消耗点.</p><p>需要在这块做一些突破.</p><h3 id="更新"><a href="#更新" class="headerlink" title="更新"></a>更新</h3><p>两块工作</p><ol><li><p>目前主要对维护脚本整理和标准化以便接入运维平台</p></li><li><p>通过 jenkins 实现交付, 通过钉钉审批实现协作</p></li></ol><h2 id="告警监控"><a href="#告警监控" class="headerlink" title="告警监控"></a>告警监控</h2><h3 id="告警"><a href="#告警" class="headerlink" title="告警"></a>告警</h3><p>告警这块只是做了个简单的接口自用</p><p>因为春节期间告警的故障导致没有空闲服务器部署.</p><p>所以需要把告警的高可用做起来.</p><h3 id="监控"><a href="#监控" class="headerlink" title="监控"></a>监控</h3><p>监控覆盖率和监控指标, 两个重点</p><ol><li><p>需要梳理下业务的整体架构和监控点.</p></li><li><p>监控系统的扩展性</p></li></ol><h2 id="日志系统"><a href="#日志系统" class="headerlink" title="日志系统"></a>日志系统</h2><h3 id="业务日志"><a href="#业务日志" class="headerlink" title="业务日志"></a>业务日志</h3><p>目前已经搭建了业务的日志, 方便研发运营检索.</p><p>没有对业务的日志进行监控和分析, 优先级较低</p><h3 id="系统日志"><a href="#系统日志" class="headerlink" title="系统日志"></a>系统日志</h3><p>目前系统日志还未开始实施.</p><p>目的是可以对系统日志监控, 发现一些潜在的问题</p><p>及时处理避免业务的故障, 优先级较低</p>]]></content>
<summary type="html">
<h1 id="2019-年终整理"><a href="#2019-年终整理" class="headerlink" title="2019 年终整理"></a>2019 年终整理</h1><h2 id="运维平台"><a href="#运维平台" class="headerlink" title="运维平台"></a>运维平台</h2><h3 id="CMDB"><a href="#CMDB" class="headerlink" title="CMDB"></a>CMDB</h3><p>我们目前已经接入了腾讯云的 dntg 项目</p>
<p>通过 canal+kafka 和额外的监控保证数据的及时性和准确性</p>
<p>在多项目和多云环境上还需要进行迭代完善</p>
<h3 id="维护功能"><a href="#维护功能" class="headerlink" title="维护功能"></a>维护功能</h3><p>正常的发展应该是 <code>手工-&gt;脚本-&gt;平台-&gt;自动化</code> 这个流程</p>
<p>然而我们直接抛开了<code>平台</code>这个阶段, 直接做了自动化.</p>
<p>导致我们后期的工作不好展开(主要是标准化的问题)</p>
<p>所以要将标准化的优先级提高</p>
</summary>
<category term="总结" scheme="https://zhiwei.show/tags/%E6%80%BB%E7%BB%93/"/>
</entry>
<entry>
<title>阿里云ASK踩坑记录</title>
<link href="https://zhiwei.show/f38b2488baed4a5d88966be05d09d788/"/>
<id>https://zhiwei.show/f38b2488baed4a5d88966be05d09d788/</id>
<published>2020-01-11T23:11:00.000Z</published>
<updated>2020-01-11T23:11:00.000Z</updated>
<content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>Ask 是 K8S 的一种特殊扩展, 它屏蔽了 node 层, 按 pod 收费.</p><p>这样可以实现低成本和弹性扩容, 在大规模的伸缩中极大的增加了效率和降低了成本</p><p>弹性扩展, 用多少算多少, 听起来确实很美好.</p><p>Ingress 本质上是一种抽象, 顾名思义, 就是所有请求的入口, 可以对请求进行控制和验证</p><p>常用的是 nginx 实现的 ingress, 通过规则映射, 将站点信息抽象各位一个个的匹配规则</p><p>接着, 我们有一个前端打点上报需求, 准备用阿里云的 ask 来做.</p><p>只是一个很简单的通过 http 请求将数据上报至阿里云 SLS 的服务.</p><p>没有状态, 存储之类的需求, 所以用 golang 写了一个简单的 api 来解决.</p><p>万事俱备, 等待数据, 然而研发同学说不太对劲.</p><a id="more"></a><h1 id="tls-证书错误"><a href="#tls-证书错误" class="headerlink" title="tls 证书错误"></a>tls 证书错误</h1><p>直接甩了个错误给我看.</p><p><img src="/resource/36a1af3073e842f18e5fa62e110c67b5.png" alt="d7233d8360b2c6a84ac4fcfd0bc0b6aa.png"></p><p>一看这信息, 很明显是证书问题.</p><p>然后想起, 好像确实没有在 ingress 里面配置证书.</p><p>阿里云上的 ingress 可以直接在控制台添加证书信息.</p><p>本质上是用了 ingress 的<code>nginx.ingress.kubernetes.io/auth-tls-secret</code>注解来实现</p><p>直接在控制配置相关的 tls 信息即可, 然而现实很骨感.</p><p>错误依旧… 难道是我打开的方式不对?</p><p>再三检查发现后并没有发现错误, 没有办法.</p><p>通过在浏览器里面直接访问域名, 提示证书不安全.</p><p><img src="/resource/6701222d063d42fca51249e4e79d87ff.png" alt="985e414f3cdab51bb3663ac3959dd6cd.png"></p><p>好家伙, 这是啥, <code>ingress controller fake certificate</code>, 明明已经指定了域名的证书.</p><p>咋会出现这么个玩意, 看起来像是 ingress 自己生成的, 但是原生的 ingress 应该是不带这玩意的.</p><p>百思不得其解啊, 最终发现 slb 居然是七层的负载均衡, 这就相当于其实客户端是和 slb 的七层负载均衡进行 tls 加密.</p><p>然后对后端的服务进行了请求, 最终将内容返回给客户端, 所以直接在 slb 层配置证书后解决了问题.</p><h1 id="CORS-问题"><a href="#CORS-问题" class="headerlink" title="CORS 问题"></a>CORS 问题</h1><p>然而现实还不肯放过我这个孩子.</p><p>解决了 slb 后居然又出了一个新的问题, 就是 cors</p><p>为了防止恶意攻击, 所以浏览器限制了客户端的请求.</p><p>我们输入域名访问的站点是主站, 如果请求非主站的域名.</p><p>需要返回<code>Access-Control-Allow-*</code>等信息, 来通知浏览器放行.</p><p>所以我们需要在七层的负载均衡上提供相应的头信息.</p><p>然而悲剧的是阿里云提供的七层负载均衡并不能自定义相关的头信息.</p><h1 id="最终"><a href="#最终" class="headerlink" title="最终"></a>最终</h1><p>在和阿里云的同学沟通的确实七层的负载均衡无法提供自定义的头信息</p><p>所以还是换回了 ack 使用, 而且 ask 的整个架构也和 ack 不太一样.</p><p>ack 的数据流是这样的<code>client->slb->nodeport->ipvs->nginx-ingress->service</code></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">1. client将数据发送给slb的TCP端口上</span><br><span class="line">2. slb将数据包转发给nodeport</span><br><span class="line">3. 数据通过nodeport进入ipvs</span><br><span class="line">4. ipvs再将数据通过不同的策略转给nginx-ingress的pod</span><br><span class="line">5. nginx-ingress最终对相应的反向代理请求service</span><br></pre></td></tr></table></figure><p>ask 的数据流很简单<code>client->alb-ingress->service</code></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">1. 客户端发送请求给alb-ingress</span><br><span class="line">2. alb-ingress通过eni直接进行反向代理请求service</span><br></pre></td></tr></table></figure><p>这也就意味着 alb-ingress 和 service 的网络是互通的, 并且将路由信息直接注册到 alb-ingress 中.</p><p>ask 方案性能最好, 没有过多的网络消耗, 但是无法兼容 nginx-ingress, 有些需求可能无法实现, 比如 cors</p><p>ack 在性能上虽然不是最佳, 但胜在灵活性上, 可以兼容 nginx-ingress 注解灵活的对请求进行控制</p><p>性能和灵活一直都在相爱相杀, 最终还是取决于业务场景.</p><p>以上是我在使用阿里云的 ack 和 ask 碰到的问题和总结.</p>]]></content>
<summary type="html">
<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>Ask 是 K8S 的一种特殊扩展, 它屏蔽了 node 层, 按 pod 收费.</p>
<p>这样可以实现低成本和弹性扩容, 在大规模的伸缩中极大的增加了效率和降低了成本</p>
<p>弹性扩展, 用多少算多少, 听起来确实很美好.</p>
<p>Ingress 本质上是一种抽象, 顾名思义, 就是所有请求的入口, 可以对请求进行控制和验证</p>
<p>常用的是 nginx 实现的 ingress, 通过规则映射, 将站点信息抽象各位一个个的匹配规则</p>
<p>接着, 我们有一个前端打点上报需求, 准备用阿里云的 ask 来做.</p>
<p>只是一个很简单的通过 http 请求将数据上报至阿里云 SLS 的服务.</p>
<p>没有状态, 存储之类的需求, 所以用 golang 写了一个简单的 api 来解决.</p>
<p>万事俱备, 等待数据, 然而研发同学说不太对劲.</p>
</summary>
<category term="aliyun" scheme="https://zhiwei.show/tags/aliyun/"/>
<category term="ack" scheme="https://zhiwei.show/tags/ack/"/>
<category term="ask" scheme="https://zhiwei.show/tags/ask/"/>
</entry>
<entry>
<title>业务的自动化部署</title>
<link href="https://zhiwei.show/f35cc21df4554c23b02a4f88da96fce6/"/>
<id>https://zhiwei.show/f35cc21df4554c23b02a4f88da96fce6/</id>
<published>2019-12-20T02:30:00.000Z</published>
<updated>2019-12-20T02:30:00.000Z</updated>
<content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>游戏行业的滚服需求, 服务之间相互独立<br>每天有大量的服务部署需求<br>并且在保证<code>稳定</code>的前提下尽可能的提高<code>资源利用率</code></p><p>而且还有个问题, 我们没有购买主机的权限<br>需要先由我们下订单, 然后通过邮件通知合作方完成订单的支付<br>这就导致需要预留一定数量的主机, 避免支付响应不及时引发的运营事故</p><h1 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h1><p>我们这里解决问题的是如何实现自动部署<br>可能一些老司机说, 自动部署嘛, 这还不简单<br>写个页面, 然后调用 Ansible 一下子就完成了.</p><p>但是我们想要做的是完全自动化<br>整个流程无人介入(不出问题的情况下)</p><p>PS: 只是部署, 不负责发布上线</p><a id="more"></a><h1 id="方案"><a href="#方案" class="headerlink" title="方案"></a>方案</h1><p>有图有状况…</p><h2 id="功能图"><a href="#功能图" class="headerlink" title="功能图"></a>功能图</h2><p><img src="/resource/82638d85b5644c6faa488e97901c7b15.png" alt="cc21f712d030e9b9fec30ecdc3b3eaef.png"></p><h3 id="新主机自动注册"><a href="#新主机自动注册" class="headerlink" title="新主机自动注册"></a>新主机自动注册</h3><p>使用 SaltStack 实现主机的注册<br>当新云主机启动时会自动连上 Salt-Master<br>进行初始化以及将云主机信息注册到 CMDB</p><h3 id="部署检测和调度"><a href="#部署检测和调度" class="headerlink" title="部署检测和调度"></a>部署检测和调度</h3><p>使用 Crontab 每 10 分钟检测一次(确实 Low, 但是香)<br>如果未使用服务数量不足<br>则根据资源历史使用情况进行筛选部署的主机</p><h3 id="任务执行和查看"><a href="#任务执行和查看" class="headerlink" title="任务执行和查看"></a>任务执行和查看</h3><p>确定了部署的主机后, 通过 HTTP 请求新建一个部署任务<br>任务系统负责将需要执行的任务转化为 SaltStack 的 State<br>SaltStack 将执行完成后的结果保存进 Mongodb 以便查看</p><h3 id="新主机购买通知"><a href="#新主机购买通知" class="headerlink" title="新主机购买通知"></a>新主机购买通知</h3><p>又是 Crontab, 我们基本上一天购买一批主机<br>在固定的时间点, 检测服务部署的失败次数(没有低负载的机器)<br>然后根据失败的服务数量大致的判断需要购买多少主机<br>最后通过 API 接口下订单, 发邮件通知合作方</p><h2 id="架构图"><a href="#架构图" class="headerlink" title="架构图"></a>架构图</h2><p>因为我们整体的规模不是很大<br>所有架构方面做的很简单(维护成本低)</p><p><img src="/resource/6eec0800bed14826b1a4c19ee7699604.png" alt="6bfe34dacc1d1a67ce9a3b3b4a7fae45.png"></p><h3 id="基础数据层"><a href="#基础数据层" class="headerlink" title="基础数据层"></a>基础数据层</h3><p>所有的资源和服务的状态<br>OLD-CMDB 是历史包袱, 研发用于更新和维护的数据<br>Canal, Kafka 是用于实时同步服务的状态(是否上线)<br>CMDB 基于 HTTP 接口为其他系统提供数据支持</p><p>CMDB 就应该是干干净净的<code>配置管理数据库</code><br>其他业务逻辑应该在上层封装避免数据和逻辑的耦合</p><h3 id="业务逻辑层"><a href="#业务逻辑层" class="headerlink" title="业务逻辑层"></a>业务逻辑层</h3><p>封装所有的业务逻辑和任务执行所需要的数据, 所有的业务逻辑都在这层体现<br>Approval 是为了与办公通信软件进行交互的, 我们这里使用 DingTalk 来走流程.<br>(走流程的目的是同步信息避免干完事后发现关键人员毫不知情)</p><h3 id="任务执行层"><a href="#任务执行层" class="headerlink" title="任务执行层"></a>任务执行层</h3><p>在 SaltStack 上封装一层任务系统<br>用来解耦任务状态以及记录任务的执行的数据<br>这里的 SaltStack 也可以替换成 Ansible, Puppet<br>任务执行过程中如果进行干预还没有一个较好的方案<br>一个思路是通过 SaltStack 的 Event 来实现(耦合了)</p><h3 id="资源监控层"><a href="#资源监控层" class="headerlink" title="资源监控层"></a>资源监控层</h3><p>监控主机和服务的资源使用情况<br>同样提供统一的接口给外部使用<br>这里为什么使用了三个不同的监控系统<br>Zabbix 之前的监控, 所以不想改动, 但是性能已经不能满足需求了<br>OpenFalcon 是专为自动化而搭建的监控系统, 设计的 JSON 格式非常灵活, 但存储和接口不太好用<br>Prometheus 只是进行尝试, PromSql 很灵活, 性能优秀, 但可扩展性比较复杂, 更像是一个组件</p><h1 id="实施"><a href="#实施" class="headerlink" title="实施"></a>实施</h1><h2 id="部署检测和调度-1"><a href="#部署检测和调度-1" class="headerlink" title="部署检测和调度"></a>部署检测和调度</h2><h3 id="检测"><a href="#检测" class="headerlink" title="检测"></a>检测</h3><p>上面也提到了, 我们使用 Crontab 定时检测部署的服务是否够用<br>这里有个问题需要解决, 到底预留多少个空闲的服务呢?<br>有些平台开服很快, 一天 10 个服还不够.<br>有些平台开服龟速, 一星期开不到 1 个服.<br>写个常量肯定是不行的, 自己骗自己.</p><p>要不通过配置的方式人工的去调整…<br>一开始确实是这么想的, 先搞起来后面在优化.<br>但有同事表示完全没有人会去主动的做调整.</p><p>最后我们决定通过每个平台的最近开服数来动态判断.<br>这个方案可以是可以, 缺点就是自动部署就依赖了外部的系统.<br>导致扩展性和兼容性很差, 尤其是跨部门的情况下.<br>容易出现对方要改, 我们崩溃, 我们要改, 对方不理.<br>也没更好的办法了, 先这么搞吧…</p><h3 id="调度"><a href="#调度" class="headerlink" title="调度"></a>调度</h3><p>这块是个重点, 详细说起来有点像王大妈的裹脚布<br>简单概括一下, 是这么个思路</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">占用资源最大的服务 = 获取最近上线占用资源最大的服务()</span><br><span class="line"></span><br><span class="line">最低负载的比例 = 70%</span><br><span class="line">for 机器 in 所有机器</span><br><span class="line"> # 需要考虑已经部署, 但是没有上线服务的负载情况</span><br><span class="line"> # 这里懒得写的, 不然就很长很臭了...</span><br><span class="line"> 机器负载 = 获取机器负载(机器)</span><br><span class="line"></span><br><span class="line"> 机器预估负载 = 机器负载 + 占用资源最大的服务</span><br><span class="line"> 机器预估负载比例 = 机器预估负载 / 机器的总内存</span><br><span class="line"></span><br><span class="line"> if 机器预估负载比例 < 最低负载的比例</span><br><span class="line"> 最低负载的比例 = 机器预估负载比例</span><br><span class="line"> 最低负载的机器 = 机器</span><br></pre></td></tr></table></figure><p>然后就能找到了最低负载的机器<br>如果没有的话, 就终止任务, 等待下个检测周期<br>牛奶和机器总会有的…</p><h2 id="任务执行和查看-1"><a href="#任务执行和查看-1" class="headerlink" title="任务执行和查看"></a>任务执行和查看</h2><p>在任务系统将任务的信息转化为 SaltStack 的 State<br>然后通过 SaltStack 的 API 去执行<br>最后将执行的结果保存在 Mongodb 中.</p><p>同时用 Crontab 每分钟跑一个定时脚本<br>找到<code>执行超时</code>的和<code>执行失败</code>的任务并发送报警通知<br>收到报警时, 我们可以查看任务执行时的详细情况来解决隐患</p><p><img src="/resource/77b3b6e412134bdcb22c194ca8c892ba.png" alt="7d7550462ae7370c19872417dc511950.png"></p><h2 id="新主机购买通知-1"><a href="#新主机购买通知-1" class="headerlink" title="新主机购买通知"></a>新主机购买通知</h2><p>先来想想, 什么时候需要购买新的主机.<br>在当部署服务时找不低负载主机, 肯定不能触发购买行为<br>因为, 每次找不到低负载主机就触发, 结果就是一天购买十几次的主机<br>(我们没权限直接购买支付, 只能下单, 由合作方支付, 非常高的协作成本和时间成本)</p><p>这时候就要将服务状态修改成<code>等待资源</code>状态.<br>这个状态是为了让购买主机的程序知道, 到底需要购买多少个主机<br>等待购买主机的任务检测时进行下单并且通知合作方</p><p>即使是服务状态已经是<code>等待资源</code>了, 在部署的流程里依然需要进行尝试部署<br>因为不能说上一次没有部署成功, 这一次就不能部署成功<br>资源的使用情况是会随时改变的.</p><h2 id="新主机自动注册-1"><a href="#新主机自动注册-1" class="headerlink" title="新主机自动注册"></a>新主机自动注册</h2><p>我们在购买云主机的时候, 会使用事先制作好的镜像.<br>将 Salt-Minion 装在镜像里, 启动时会自动的连上 Salt-Master<br>进行初始化以及注册到 CMDB 上</p><p>这里有个问题是用什么来作为主机的唯一标识.<br>实例 ID? 主机名? 内网 IP? 公网 IP? 还是自定义?<br>实例 ID 或许是个好主意, 但是有些奇奇怪怪的问题.<br>比如不同公有云的实例 ID 是否会冲突<br>又比如如果我们使用私有云或者没有实例 ID 的公有云怎么办.</p><p>主机名也有些同样的问题, 主机名是否会冲突<br>如果修改了主机名这个主机还是这个主机吗?</p><p>内网 IP, 这个不用想肯定会冲突.</p><p>公网 IP 又会有没有公网 IP 的主机情况或者是 NAT 模式的情况.</p><p>云厂商+区域+实例 ID 是一个比较好的方案.<br>但是我们用的是 云厂商配置名(云厂商+区域)+内网 IP<br>这种方式, 主要是因为, 当时我们没有找到在主机内获取主机实例 ID 的方式<br>其次云厂商配置名+内网 IP 的方案会更加的通用.</p><h1 id="效果"><a href="#效果" class="headerlink" title="效果"></a>效果</h1><p>全都自动了, 能有啥好看的…</p><p><img src="/resource/2406cf1e4d9d471abb903f466da35492.png" alt="a6f65b3cb4c2b8844aa63142965194c8.png"></p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>总的来说效率不错.<br>解放了人力, 并且保证了质量和效率.<br>期间也修修补补了很多次(调参)</p><p>在兼容性和扩展性的还不是很满意<br>这个涉及到运维基础的标准和项目的不同需求.<br>很难做到一套通用, 只能将通用的部署抽出来.<br>不同的细节通过脚本来定制化.</p>]]></content>
<summary type="html">
<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>游戏行业的滚服需求, 服务之间相互独立<br>每天有大量的服务部署需求<br>并且在保证<code>稳定</code>的前提下尽可能的提高<code>资源利用率</code></p>
<p>而且还有个问题, 我们没有购买主机的权限<br>需要先由我们下订单, 然后通过邮件通知合作方完成订单的支付<br>这就导致需要预留一定数量的主机, 避免支付响应不及时引发的运营事故</p>
<h1 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h1><p>我们这里解决问题的是如何实现自动部署<br>可能一些老司机说, 自动部署嘛, 这还不简单<br>写个页面, 然后调用 Ansible 一下子就完成了.</p>
<p>但是我们想要做的是完全自动化<br>整个流程无人介入(不出问题的情况下)</p>
<p>PS: 只是部署, 不负责发布上线</p>
</summary>
<category term="saltstack" scheme="https://zhiwei.show/tags/saltstack/"/>
<category term="自动化" scheme="https://zhiwei.show/tags/%E8%87%AA%E5%8A%A8%E5%8C%96/"/>
</entry>
<entry>
<title>Lua实现的hash合并功能</title>
<link href="https://zhiwei.show/2ab3d2be0894444e9be040312112088e/"/>
<id>https://zhiwei.show/2ab3d2be0894444e9be040312112088e/</id>
<published>2019-11-08T02:35:00.000Z</published>
<updated>2019-11-08T02:35:00.000Z</updated>
<content type="html"><![CDATA[<h1 id="问题背景"><a href="#问题背景" class="headerlink" title="问题背景"></a>问题背景</h1><p>业务需求要通过事件触发, 来实现两个 table 的数据合并.</p><p>理清了需求和思路并思考下扩展, 记录下来, 用于分享和沉淀.</p><p>因为用 lua 写的业务, 所以这里用 <code>lua</code> 实现的.</p><p>PS: 在 lua 里 table 和 hash 的结构类似</p><h1 id="一层合并"><a href="#一层合并" class="headerlink" title="一层合并"></a>一层合并</h1><h3 id="数据"><a href="#数据" class="headerlink" title="数据"></a>数据</h3><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">local</span> a = {</span><br><span class="line"> [<span class="string">"a"</span>] = {</span><br><span class="line"> [<span class="string">"aa"</span>] = {</span><br><span class="line"> [<span class="string">"aaa"</span>] = <span class="string">"111"</span></span><br><span class="line"> },</span><br><span class="line"> [<span class="string">"bb"</span>] = {</span><br><span class="line"> [<span class="string">"bbb"</span>] = <span class="string">"222"</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">local</span> b = {</span><br><span class="line"> [<span class="string">"a"</span>] = {</span><br><span class="line"> [<span class="string">"aa"</span>] = {</span><br><span class="line"> [<span class="string">"aaa"</span>] = <span class="string">"123123"</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="函数"><a href="#函数" class="headerlink" title="函数"></a>函数</h3><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">table.merge</span><span class="params">(dest, src, cover)</span></span></span><br><span class="line"> <span class="keyword">if</span> <span class="built_in">type</span>(dest) ~= <span class="string">"table"</span> <span class="keyword">or</span> <span class="built_in">type</span>(src) ~= <span class="string">"table"</span> <span class="keyword">then</span></span><br><span class="line"> <span class="keyword">return</span></span><br><span class="line"> <span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">-- don't use 'if not cover then'</span></span><br><span class="line"> <span class="comment">-- because "(not nil or not false) == true"</span></span><br><span class="line"> <span class="comment">-- T.T</span></span><br><span class="line"> <span class="keyword">if</span> cover == <span class="literal">nil</span> <span class="keyword">then</span></span><br><span class="line"> cover = <span class="literal">true</span></span><br><span class="line"> <span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span> k, v <span class="keyword">in</span> <span class="built_in">pairs</span>(src) <span class="keyword">do</span></span><br><span class="line"> <span class="keyword">if</span> dest[k] <span class="keyword">then</span></span><br><span class="line"> <span class="keyword">if</span> cover <span class="keyword">then</span></span><br><span class="line"> dest[k] = v</span><br><span class="line"> <span class="keyword">end</span></span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> dest[k] = v</span><br><span class="line"> <span class="keyword">end</span></span><br><span class="line"> <span class="keyword">end</span></span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure><h3 id="结果"><a href="#结果" class="headerlink" title="结果"></a>结果</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line">["a"] = {</span><br><span class="line">["aa"] = {</span><br><span class="line">["aaa"] = "123123",</span><br><span class="line">},</span><br><span class="line">},</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>你看我想将 table B.a.aa.aaa 的值合并到 table A.a.aa.aaa</p><p>但是直接用 table B.a 将 table A.a 的值给覆盖了</p><p>所以这个函数只能用于一层 hash 函数的合并了</p><h1 id="递归合并"><a href="#递归合并" class="headerlink" title="递归合并"></a>递归合并</h1><h3 id="数据-1"><a href="#数据-1" class="headerlink" title="数据"></a>数据</h3><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">local</span> a = {</span><br><span class="line"> [<span class="string">"a"</span>] = {</span><br><span class="line"> [<span class="string">"aa"</span>] = {</span><br><span class="line"> [<span class="string">"aaa"</span>] = <span class="string">"111"</span></span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> [<span class="string">"b"</span>] = {</span><br><span class="line"> [<span class="string">"bb"</span>] = {</span><br><span class="line"> [<span class="string">"bbb"</span>] = <span class="string">"222"</span></span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> [<span class="string">"c"</span>] = {</span><br><span class="line"> [<span class="string">"cc"</span>] = {</span><br><span class="line"> [<span class="string">"ccc"</span>] = <span class="string">"333"</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">local</span> b = {</span><br><span class="line"> [<span class="string">"a"</span>] = {</span><br><span class="line"> [<span class="string">"aa"</span>] = {</span><br><span class="line"> [<span class="string">"aaa"</span>] = <span class="string">"111a"</span></span><br><span class="line"> [<span class="string">"aaa1"</span>] = <span class="string">"111a"</span></span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> [<span class="string">"b"</span>] = {</span><br><span class="line"> [<span class="string">"bb"</span>] = <span class="number">123123</span></span><br><span class="line"> },</span><br><span class="line"> [<span class="string">"c"</span>] = {</span><br><span class="line"> [<span class="string">"cc"</span>] = {</span><br><span class="line"> [<span class="string">"ccc"</span>] = {</span><br><span class="line"> [<span class="string">"cccc"</span>] = <span class="number">3333</span>,</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="函数-1"><a href="#函数-1" class="headerlink" title="函数"></a>函数</h3><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">table.r_merge</span><span class="params">(dest, src, cover)</span></span></span><br><span class="line"> <span class="keyword">if</span> <span class="built_in">type</span>(dest) ~= <span class="string">"table"</span> <span class="keyword">or</span> <span class="built_in">type</span>(src) ~= <span class="string">"table"</span> <span class="keyword">then</span></span><br><span class="line"> <span class="keyword">return</span></span><br><span class="line"> <span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> cover == <span class="literal">nil</span> <span class="keyword">then</span></span><br><span class="line"> cover = <span class="literal">true</span></span><br><span class="line"> <span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span> k, v <span class="keyword">in</span> <span class="built_in">pairs</span>(src) <span class="keyword">do</span></span><br><span class="line"> <span class="keyword">if</span> <span class="built_in">type</span>(v) == <span class="string">"table"</span> <span class="keyword">then</span></span><br><span class="line"> <span class="keyword">if</span> dest[k] <span class="keyword">and</span> <span class="built_in">type</span>(dest[k]) == <span class="string">"table"</span> <span class="keyword">then</span></span><br><span class="line"> <span class="built_in">table</span>.r_merge(dest[k], v, cover)</span><br><span class="line"> <span class="keyword">elseif</span> cover <span class="keyword">then</span></span><br><span class="line"> dest[k] = v</span><br><span class="line"> <span class="keyword">end</span></span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> <span class="keyword">if</span> cover <span class="keyword">then</span></span><br><span class="line"> dest[k] = v</span><br><span class="line"> <span class="keyword">end</span></span><br><span class="line"> <span class="keyword">end</span></span><br><span class="line"> <span class="keyword">end</span></span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure><h3 id="结果-1"><a href="#结果-1" class="headerlink" title="结果"></a>结果</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> ["a"] = {</span><br><span class="line"> ["aa"] = {</span><br><span class="line"> ["aaa1"] = "111a",</span><br><span class="line"> ["aaa"] = "111a",</span><br><span class="line"> },</span><br><span class="line"> },</span><br><span class="line"> ["b"] = {</span><br><span class="line"> ["bb"] = 123123,</span><br><span class="line"> },</span><br><span class="line"> ["c"] = {</span><br><span class="line"> ["cc"] = {</span><br><span class="line"> ["ccc"] = {</span><br><span class="line"> ["cccc"] = 3333,</span><br><span class="line"> },</span><br><span class="line"> },</span><br><span class="line"> },</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>table B.a.aa.aaa1 追加到了 table A.a.aa 里面</p><p>table B.a.aa.aaa 覆盖了 table A.a.aa.aaa</p><p>table B.b.bb 覆盖了 table A.b.aa</p><p>table B.c.cc.ccc 覆盖了 table A.c.cc.ccc</p><h1 id="层级合并"><a href="#层级合并" class="headerlink" title="层级合并"></a>层级合并</h1><h3 id="扩展思考"><a href="#扩展思考" class="headerlink" title="扩展思考"></a>扩展思考</h3><p>一层合并和递归合并都不灵活</p><p>所以需要有一个提供给调用者想要合并层级的函数</p><h3 id="数据-2"><a href="#数据-2" class="headerlink" title="数据"></a>数据</h3><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">local</span> A = {</span><br><span class="line"> [<span class="string">"a"</span>] = {</span><br><span class="line"> [<span class="string">"aa"</span>] = {</span><br><span class="line"> [<span class="string">"xxx"</span>] = <span class="string">"111"</span></span><br><span class="line"> },</span><br><span class="line"> [<span class="string">"bb"</span>] = {</span><br><span class="line"> [<span class="string">"bbb"</span>] = <span class="string">"222"</span></span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> [<span class="string">"b"</span>] = {</span><br><span class="line"> [<span class="string">"bb"</span>] = {</span><br><span class="line"> [<span class="string">"bbb"</span>] = <span class="string">"222"</span></span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> [<span class="string">"c"</span>] = {</span><br><span class="line"> [<span class="string">"cc"</span>] = {</span><br><span class="line"> [<span class="string">"ccc"</span>] = <span class="string">"333"</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">local</span> B = {</span><br><span class="line"> [<span class="string">"a"</span>] = {</span><br><span class="line"> [<span class="string">"aa"</span>] = {</span><br><span class="line"> [<span class="string">"aaa"</span>] = <span class="string">"111"</span>,</span><br><span class="line"> [<span class="string">"aaa1"</span>] = <span class="string">"111a"</span>,</span><br><span class="line"> [<span class="string">"aaaa"</span>] = <span class="string">"1111"</span>,</span><br><span class="line"> [<span class="string">"aaaaa"</span>] = <span class="string">"11111"</span>,</span><br><span class="line"> },</span><br><span class="line"> [<span class="string">"cc"</span>] = <span class="string">"333"</span>,</span><br><span class="line"> },</span><br><span class="line"> [<span class="string">"b"</span>] = {</span><br><span class="line"> [<span class="string">"bb"</span>] = <span class="number">123123</span></span><br><span class="line"> },</span><br><span class="line"> [<span class="string">"c"</span>] = {</span><br><span class="line"> [<span class="string">"cc"</span>] = {</span><br><span class="line"> [<span class="string">"ccc"</span>] = {</span><br><span class="line"> [<span class="string">"cccc"</span>] = <span class="number">3333</span>,</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="实现"><a href="#实现" class="headerlink" title="实现"></a>实现</h3><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">table.r_merge</span><span class="params">(dest, src, cover, depth)</span></span></span><br><span class="line"> <span class="keyword">if</span> <span class="built_in">type</span>(dest) ~= <span class="string">"table"</span> <span class="keyword">or</span> <span class="built_in">type</span>(src) ~= <span class="string">"table"</span> <span class="keyword">then</span></span><br><span class="line"> <span class="keyword">return</span></span><br><span class="line"> <span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> <span class="built_in">type</span>(depth) == <span class="string">"number"</span> <span class="keyword">then</span></span><br><span class="line"> depth = depth - <span class="number">1</span></span><br><span class="line"> <span class="keyword">elseif</span> depth == <span class="literal">nil</span> <span class="keyword">then</span></span><br><span class="line"> <span class="comment">-- 这里有点意思, 当depth == -1</span></span><br><span class="line"> <span class="comment">-- 再次递归的时候永远都是负数.</span></span><br><span class="line"> <span class="comment">-- 根据下面 depth == 0 的判断.</span></span><br><span class="line"> <span class="comment">-- 就形成了无限递归的条件</span></span><br><span class="line"> depth = <span class="number">-1</span></span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> <span class="keyword">return</span></span><br><span class="line"> <span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> cover == <span class="literal">nil</span> <span class="keyword">then</span></span><br><span class="line"> cover = <span class="literal">true</span></span><br><span class="line"> <span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span> k, v <span class="keyword">in</span> <span class="built_in">pairs</span>(src) <span class="keyword">do</span></span><br><span class="line"> <span class="keyword">if</span> <span class="built_in">type</span>(v) == <span class="string">"table"</span> <span class="keyword">then</span></span><br><span class="line"> <span class="keyword">if</span> cover <span class="keyword">and</span> depth == <span class="number">0</span> <span class="keyword">then</span></span><br><span class="line"> dest[k] = v</span><br><span class="line"> <span class="keyword">elseif</span> dest[k] <span class="keyword">and</span> <span class="built_in">type</span>(dest[k]) == <span class="string">"table"</span> <span class="keyword">then</span></span><br><span class="line"> <span class="built_in">table</span>.r_merge(dest[k], v, cover, depth)</span><br><span class="line"> <span class="keyword">elseif</span> dest[k] <span class="keyword">and</span> cover <span class="keyword">then</span></span><br><span class="line"> dest[k] = v</span><br><span class="line"> <span class="keyword">end</span></span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> <span class="keyword">if</span> cover <span class="keyword">then</span></span><br><span class="line"> dest[k] = v</span><br><span class="line"> <span class="keyword">end</span></span><br><span class="line"> <span class="keyword">end</span></span><br><span class="line"> <span class="keyword">end</span></span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure><p>其实我不喜欢这样的函数.</p><p>因为在调用的时候就要明确的知道层级的范围是多少.</p><p>也就是说层级前和层级后的数据是不同的.</p><p>这种结构的不同是隐性存在的.</p><p>需要调用者自行把控.</p><h3 id="结果-2"><a href="#结果-2" class="headerlink" title="结果"></a>结果</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> ["a"] = {</span><br><span class="line"> ["bb"] = {</span><br><span class="line"> ["bbb"] = "222",</span><br><span class="line"> },</span><br><span class="line"> ["cc"] = "333",</span><br><span class="line"> ["aa"] = {</span><br><span class="line"> ["aaa1"] = "111a",</span><br><span class="line"> ["aaaaa"] = "11111",</span><br><span class="line"> ["aaa"] = "111",</span><br><span class="line"> ["aaaa"] = "1111",</span><br><span class="line"> },</span><br><span class="line"> },</span><br><span class="line"> ["b"] = {</span><br><span class="line"> ["bb"] = 123123,</span><br><span class="line"> },</span><br><span class="line"> ["c"] = {</span><br><span class="line"> ["cc"] = {</span><br><span class="line"> ["ccc"] = {</span><br><span class="line"> ["cccc"] = 3333,</span><br><span class="line"> },</span><br><span class="line"> },</span><br><span class="line"> },</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>table A.a.bb 添加到了 table A.a</p><p>table B.aa 覆盖了 table A.a.aa, A.a.aa.xxx 没有了, 去哪了? ^.^</p><p>table A.a.bb 还在 A 里乖乖的呆着.</p><p>table B.b.bb 覆盖了 table A.b.bb</p><p>table B.c.cc 覆盖了 table A.c.cc</p>]]></content>
<summary type="html">
<h1 id="问题背景"><a href="#问题背景" class="headerlink" title="问题背景"></a>问题背景</h1><p>业务需求要通过事件触发, 来实现两个 table 的数据合并.</p>
<p>理清了需求和思路并思考下扩展, 记录下来, 用
</summary>
<category term="lua" scheme="https://zhiwei.show/tags/lua/"/>
</entry>
</feed>