-
Notifications
You must be signed in to change notification settings - Fork 0
/
atom.xml
537 lines (322 loc) · 238 KB
/
atom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>大名Dean鼎</title>
<link href="/atom.xml" rel="self"/>
<link href="http://www.deanwangpro.com/"/>
<updated>2020-04-13T02:05:12.548Z</updated>
<id>http://www.deanwangpro.com/</id>
<author>
<name>Dean Wang</name>
</author>
<generator uri="https://hexo.io/">Hexo</generator>
<entry>
<title>MySQL死锁与Spring事务</title>
<link href="http://www.deanwangpro.com/2020/04/12/spring-retry-with-tx/"/>
<id>http://www.deanwangpro.com/2020/04/12/spring-retry-with-tx/</id>
<published>2020-04-11T16:00:00.000Z</published>
<updated>2020-04-13T02:05:12.548Z</updated>
<content type="html"><![CDATA[<p>MySQL死锁从产品之初就偶有发生,算是萦绕在心中的噩梦之一。由于死锁大都伴随着锁等待,所以一般都会拉低服务QPS,在死锁发生时肯定会出现各种意料不到的问题。前期一直采用“指标不治本”的办法,对特别容易产生死锁的方法增加retry。但当retry和事务嵌套在一起时也会出现不可预知的错误。</p><p>对于数据库死锁这个万恶之源,真可谓深恶痛绝,所以这次在解决retry和事务嵌套问题时,将这个元凶也一并解决。</p><a id="more"></a><h2 id="一些关于事务的概念"><a href="#一些关于事务的概念" class="headerlink" title="一些关于事务的概念"></a>一些关于事务的概念</h2><p>为了更好的说明问题,我们先来解释一下基本概念</p><h3 id="隔离级别"><a href="#隔离级别" class="headerlink" title="隔离级别"></a>隔离级别</h3><p>TransactionDefinition 接口中定义了五个表示隔离级别的常量:</p><ul><li><strong>TransactionDefinition.ISOLATION_DEFAULT:</strong> 使用后端数据库默认的隔离级别,Mysql 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别.</li><li><strong>TransactionDefinition.ISOLATION_READ_UNCOMMITTED:</strong> 最低的隔离级别,允许读取尚未提交的数据变更,<strong>可能会导致脏读、幻读或不可重复读</strong></li><li><strong>TransactionDefinition.ISOLATION_READ_COMMITTED:</strong> 允许读取并发事务已经提交的数据,<strong>可以阻止脏读,但是幻读或不可重复读仍有可能发生</strong></li><li><strong>TransactionDefinition.ISOLATION_REPEATABLE_READ:</strong> 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,<strong>可以阻止脏读和不可重复读,但幻读仍有可能发生。</strong></li><li><strong>TransactionDefinition.ISOLATION_SERIALIZABLE:</strong> 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,<strong>该级别可以防止脏读、不可重复读以及幻读</strong>。但是这将严重影响程序的性能。通常情况下也不会用到该级别。</li></ul><p>隔离级别是数据库和应用层(这里就是指Spring)都有的概念,一般来说应用层不应该修改隔离级别,都应默认使用数据库的隔离级别,也就是使用TransactionDefinition.ISOLATION_DEFAULT</p><h3 id="事务传播行为"><a href="#事务传播行为" class="headerlink" title="事务传播行为"></a>事务传播行为</h3><p>当一个事务方法被另一个事务方法调用时,对于已存在的事务如何处理?通过指定事务传播机制来解决。在TransactionDefinition定义中包括了如下几个表示传播行为:</p><p><strong>支持当前事务的情况:</strong></p><ul><li><strong>TransactionDefinition.PROPAGATION_REQUIRED:</strong> 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。</li><li><strong>TransactionDefinition.PROPAGATION_SUPPORTS:</strong> 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。</li><li><strong>TransactionDefinition.PROPAGATION_MANDATORY:</strong> 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。</li></ul><p><strong>不支持当前事务的情况:</strong></p><ul><li><strong>TransactionDefinition.PROPAGATION_REQUIRES_NEW:</strong> 创建一个新的事务,如果当前存在事务,则把当前事务挂起。</li><li><strong>TransactionDefinition.PROPAGATION_NOT_SUPPORTED:</strong> 以非事务方式运行,如果当前存在事务,则把当前事务挂起。</li><li><strong>TransactionDefinition.PROPAGATION_NEVER:</strong> 以非事务方式运行,如果当前存在事务,则抛出异常。</li></ul><p><strong>其他情况:</strong></p><ul><li><strong>TransactionDefinition.PROPAGATION_NESTED:</strong> 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。</li></ul><p>一般常用的就是PROPAGATION_REQUIRED。特别需要注意的是事务传播行为并不是数据库的定义,而是应用层(这里就是指Spring)引入的概念。</p><h3 id="事务状态是如何传递的"><a href="#事务状态是如何传递的" class="headerlink" title="事务状态是如何传递的"></a>事务状态是如何传递的</h3><p>假设默认使用PROPAGATION_REQUIRED,当一个事务方法A调用事务方法B(即都用@Transactional声明的方法),事务方法B为何知道此时已经存在事务了?</p><p>其实spring的事务管理器<code>TransactionManager</code>内部会持有当前线程的事务状态<code>transcationInfo</code>来判断一个事务是否已经开启。每个线程都要有一个状态,聪明的你一定会想到使用ThreadLocal这个框架必备的工具。</p><p><img src="http://img.deanwangpro.com/t/tx_threadlocal.png" alt=""></p><p>那么对于嵌套事务,Spring是怎么实现子事务失败,父事务也失败呢?先来看commit方法,</p><p><img src="http://img.deanwangpro.com/t/tx_commit.png" alt=""></p><p>看标注①的代码,如果TransactionStatus里的rollbackOnly是true,那么就会processRollback。继续跟进标注②的代码,看什么时候会设置这个标志位。在processCommit这个方法内,一旦捕获RuntimeException都会进入doRollbackOnCommitException方法,继续跟进这个方法</p><p><img src="http://img.deanwangpro.com/t/tx_rollback.png" alt=""></p><p>可以看到一个条件分支,如果当前是新开启的事务,直接进行rollback。如果当前已有父事务,那么不会直接发送rollback到数据库,而是执行标注①的<code>doSetRollbackOnly</code>方法,设置<code>TransactionStatus</code>里的rollbackOnly=true。这样父事务在commit的时候就会rollback。</p><p>由于篇幅原因,源码部分就不过多展示了, 如果感兴趣可以自行debug一下就能了解其中的机制。</p><h2 id="当-Spring-Retry-与事务相遇"><a href="#当-Spring-Retry-与事务相遇" class="headerlink" title="当 Spring Retry 与事务相遇"></a>当 Spring Retry 与事务相遇</h2><p>上文谈到我们在处理死锁时采用重试的方式解决,但并不是说任何方法都套上<code>@Retryable</code>就万事大吉了。</p><h3 id="Retry的注意点"><a href="#Retry的注意点" class="headerlink" title="Retry的注意点"></a>Retry的注意点</h3><p>一般来说,在spring中使用retry非常的简单,在注解中定义好重试次数和避让策略就可以了。</p><figure class="highlight java"><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="meta">@Override</span></span><br><span class="line"><span class="meta">@Retryable</span>(maxAttempts = MAX_RETIES, value = {MySQLIntegrityConstraintViolationException<span class="class">.<span class="keyword">class</span>, <span class="title">MySQLTransactionRollbackException</span>.<span class="title">class</span>}, <span class="title">backoff</span> </span>= <span class="meta">@Backoff</span>(delay = <span class="number">100</span>, multiplier = <span class="number">2</span>))</span><br><span class="line"><span class="function"><span class="keyword">public</span> Foo <span class="title">save</span><span class="params">(@Nullable Foo foo)</span> </span>{</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"> <span class="comment">// foo.setXXX(...)</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>值得注意是,如果方法内部会对参数对象进行修改,那么实际上每次retry的逻辑就都不是幂等的了,因为这一次的retry会受上一次的影响。假设这个<code>save</code>方法会根据有没有id来判断处理逻辑是创建还是更新,那么第二次的retry很有可能就会变成更新逻辑,而不是预期的创建逻辑了。如果参数是不可变的(immutable)那么可以很大程序上降低这种复杂度。这也告诉我们,immutable思想非常重要,可以在设计和编译阶段就解决很多“头疼”的问题。</p><p>回到正题,试想一下,如果这个<code>save</code>是个子事务方法会发生什么?按照上一节说的,第一次执行就会设置rollbackOnly=true,那么即使第二次重试成功也并不会将rollbackOnly设置为false,所以父事务依然会失败。换句话说,<strong>retry在子事务方法上是没有意义的</strong>。这点在编码时尤其需要注意。</p><h2 id="数据库死锁"><a href="#数据库死锁" class="headerlink" title="数据库死锁"></a>数据库死锁</h2><p>事务的问题说完,回到万恶之源的数据库死锁问题。</p><h3 id="起因"><a href="#起因" class="headerlink" title="起因"></a>起因</h3><p>其实死锁的困扰已久,一般一周次数在几次到十几次不等。而且死锁的对象都是索引或者主键,查阅了一些资料觉得可能是并发写入比较严重造成,但线下模拟了很久也没有复现。次数倒也不多,也就先用重试策略抵挡一下。当然也就遇到了上文提及的问题。</p><p>近期由于业务增长,死锁发生的频繁愈发频繁,所以“治本”的任务也紧急地提上了日程。</p><h3 id="问题分析"><a href="#问题分析" class="headerlink" title="问题分析"></a>问题分析</h3><p>死锁第一步分析肯定先从MySQL的死锁log入手</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></pre></td><td class="code"><pre><span class="line">------------------------</span><br><span class="line">LATEST DETECTED DEADLOCK</span><br><span class="line">------------------------</span><br><span class="line">2020-03-27 10:35:51 0x7f5124f91700</span><br><span class="line">*** (1) TRANSACTION:</span><br><span class="line">TRANSACTION 860653361, ACTIVE 0 sec inserting</span><br><span class="line">mysql tables in use 1, locked 1</span><br><span class="line">LOCK WAIT 15 lock struct(s), heap size 1136, 7 row lock(s), undo log entries 2</span><br><span class="line">MySQL thread id 11732538, OS thread handle 139984633485056, query id 5318215849 10.0.20.22 linkflow update</span><br><span class="line">INSERT INTO contact_identity (contact_id, external_id, last_updated, tenant_id) VALUES (34868134, 'oPawF5kfWh7BjsgMesjfsndYc548', '2020-03-27 10:35:51.579', 1)</span><br><span class="line">*** (1) WAITING FOR THIS LOCK TO BE GRANTED:</span><br><span class="line">RECORD LOCKS space id 433 page no 911218 n bits 600 index idx_t_contact of table `linkflow`.`contact_identity` trx id 860653361 lock_mode X locks gap before rec insert intention waiting</span><br><span class="line">Record lock, heap no 412 PHYSICAL RECORD: n_fields 3; compact format; info bits 0</span><br><span class="line"> 0: len 8; hex 800000000000016e; asc n;;</span><br><span class="line"> 1: len 8; hex 800000000107ef32; asc 2;;</span><br><span class="line"> 2: len 8; hex 80000000020c7f31; asc 1;;</span><br><span class="line"></span><br><span class="line">*** (2) TRANSACTION:</span><br><span class="line">TRANSACTION 860653362, ACTIVE 0 sec inserting</span><br><span class="line">mysql tables in use 1, locked 1</span><br><span class="line">15 lock struct(s), heap size 1136, 7 row lock(s), undo log entries 2</span><br><span class="line">MySQL thread id 11736226, OS thread handle 139986489382656, query id 5318215869 10.0.20.22 linkflow update</span><br><span class="line">INSERT INTO contact_identity (contact_id, external_id, last_updated, tenant_id) VALUES (34868135, 'oPawF5t1K1_a1i96ZDrJQRx22T8w','2020-03-27 10:35:51.588', 1)</span><br><span class="line">*** (2) HOLDS THE LOCK(S):</span><br><span class="line">RECORD LOCKS space id 433 page no 911218 n bits 600 index idx_t_contact of table `linkflow`.`contact_identity` trx id 860653362 lock mode S locks gap before rec</span><br><span class="line">Record lock, heap no 412 PHYSICAL RECORD: n_fields 3; compact format; info bits 0</span><br><span class="line"> 0: len 8; hex 800000000000016e; asc n;;</span><br><span class="line"> 1: len 8; hex 800000000107ef32; asc 2;;</span><br><span class="line"> 2: len 8; hex 80000000020c7f31; asc 1;;</span><br><span class="line"></span><br><span class="line">*** (2) WAITING FOR THIS LOCK TO BE GRANTED:</span><br><span class="line">RECORD LOCKS space id 433 page no 911218 n bits 600 index idx_t_contact of table `linkflow`.`contact_identity` trx id 860653362 lock_mode X locks gap before rec insert intention waiting</span><br><span class="line">Record lock, heap no 412 PHYSICAL RECORD: n_fields 3; compact format; info bits 0</span><br><span class="line"> 0: len 8; hex 800000000000016e; asc n;;</span><br><span class="line"> 1: len 8; hex 800000000107ef32; asc 2;;</span><br><span class="line"> 2: len 8; hex 80000000020c7f31; asc 1;;</span><br><span class="line"></span><br><span class="line">*** WE ROLL BACK TRANSACTION (2)</span><br></pre></td></tr></table></figure><p>这是两个insert操作,事务2失败,是由于等待索引idx_t_contact的排他锁。</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><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">死锁的索引约束</span><br><span class="line"> KEY `idx_t_contact` (`tenant_id`,`contact_id`),</span><br><span class="line">产生死锁的原因是:</span><br><span class="line">TRANSACTION 860653361,插入的记录是:INSERT INTO contact_identity (contact_id, external_id, last_updated, tenant_id) VALUES (34868134, 'oPawF5kfWh7BjsgMesjfsndYc548', '2020-03-27 10:35:51.579', 1)</span><br><span class="line"></span><br><span class="line">他被阻塞,需要等待这个记录的锁提供:</span><br><span class="line"> 0: len 8; hex 800000000000016e; asc n;;</span><br><span class="line"> 1: len 8; hex 800000000107ef32; asc 2;;</span><br><span class="line"> 2: len 8; hex 80000000020c7f31; asc 1;;</span><br><span class="line"></span><br><span class="line">而这个记录的锁,被事务TRANSACTION 860653362持有了,没释放,没释放的情况下,接着在事务860653361后,又执行了一个插入(在860653362的事务中):</span><br><span class="line">INSERT INTO contact_identity (contact_id, external_id, last_updated, tenant_id) VALUES (34868135, 'oPawF5t1K1_a1i96ZDrJQRx22T8w','2020-03-27 10:35:51.588', 1)</span><br><span class="line"></span><br><span class="line">它也需要这个记录的锁(其实它已经持有了这个记录的锁,但是他需要在队列里等待):</span><br><span class="line"> 0: len 8; hex 800000000000016e; asc n;;</span><br><span class="line"> 1: len 8; hex 800000000107ef32; asc 2;;</span><br><span class="line"> 2: len 8; hex 80000000020c7f31; asc 1;;</span><br><span class="line"> </span><br><span class="line">这样就造成了死循环,产生死锁</span><br></pre></td></tr></table></figure><p>这个就挺奇怪的,两个insert没有涉及到同一个主键或者同一个唯一索引,居然会持有S锁,并造成相同等待。咨询了阿里云的售后DBA,表示很有可能是在插入前有一个产生S锁的操作导致了这个问题,比如锁定行记录,或者是更新操作。但在我们的业务中,在写入之前只有查询操作,查询记录为空后执行插入,按理说是不会产生S锁的。之后我们又逐个的排查了应用日志中输出的SQL,也没有发现会加S锁的操作。</p><p>这一下线索又断了,在与阿里云的DBA沟通了一天后也没什么进展,期间各种查binlog也没有任何发现。好在最后DBA建议我们打开RDS的SQL洞察功能,看看会不会有什么蛛丝马迹。虽然这个功能是收费的,不过也正是由于它我们才找到了真正的元凶。</p><h3 id="转机"><a href="#转机" class="headerlink" title="转机"></a>转机</h3><p>SQL洞察购买以后才会开始收集,所以这时我们只需要静静地等待死锁的再次发生。由于是回顾,我们还是以刚刚死锁日志中的两个事务来看下SQL洞察中的记录。</p><p>事务1</p><p><img src="http://img.deanwangpro.com/t/tx1.png" alt=""></p><p>事务2</p><p><img src="http://img.deanwangpro.com/t/tx2.png" alt="tx2"></p><p>可以看到在两次查询时都执行了<code>SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;</code>,这个就非常奇怪了,而且正是由于这个操作导致了死锁,因为设置隔离级别为串行化以后,所以查询都会加S锁。</p><p>我们来模拟下死锁过程</p><table><thead><tr><th>事务1</th><th>事务2</th></tr></thead><tbody><tr><td>SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;</td><td>SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;</td></tr><tr><td>select * from contact_identity where external_id=”abc123” and tenant_id = 1;</td><td>select * from contact_identity where external_id=”123abc” and tenant_id = 1;</td></tr><tr><td></td><td>INSERT INTO contact_identity (contact_id, external_id, TENANT_ID) VALUES (9999, ‘123abc’, 1);</td></tr><tr><td>INSERT INTO contact_identity (contact_id, external_id, TENANT_ID) VALUES (9999, ‘abc123’, 1);</td><td></td></tr><tr><td>死锁</td><td>成功</td></tr></tbody></table><p>事务1和事务2都开启了串行化,那么在查询时会将满足条件的索引都加上S锁,这里锁的就是tenant_id=1的记录,这个时候其实在事务2的第三步插入时就会产生锁等待,因为它在等待事务1释放S锁。这时事务1也提交一个插入操作,并且contact_id和tenant_id都一样(命中idx_t_contact索引),就会产生死锁,事务1失败回滚, 事务2成功。</p><p><img src="http://img.deanwangpro.com/t/deadlock.png" alt=""></p><h3 id="分析"><a href="#分析" class="headerlink" title="分析"></a>分析</h3><p>既然知道了造成了死锁的原因,但是究竟是什么代码会设置隔离级别为串行化呢?</p><p>我们排查了所有代码依然没有任何发现,直觉告诉我,应该是某些三方库的设置导致的。目前应用中涉及到数据库的三方库也并不是很多,决定逐个摸排。</p><p>这里还得提一个阿里出品的线上调试神器<code>arthas</code>,拿来直接watch mysql驱动内<code>connectionImpl</code>的<code>setTransactionIsolation</code>方法,只要有触发了改变隔离级别的操作,那么立刻就可以看到输出。当然这一切都是在staging环境进行的。</p><p>一起准备就绪后,我就开始通过UI操作可疑的功能。果然在进行数据导出的时候,发现<code>arthas</code>的控制台有输出了,从图上可以看到隔离级别在 2 (READ_COMMITTED) 和 8 (SERIALIZABLE) 之间来回切换。</p><p><img src="http://img.deanwangpro.com/t/arthas_deadlock.png" alt=""></p><p>其实这样的切换和SQL洞察的输出也对的上,在事务结束后会将隔离级别恢复到数据库默认级别。阅读源码得知,设置回READ_COMMITTED是HikariCP的逻辑,在connection还回pool的时候会reset connection到默认级别。</p><h3 id="解决"><a href="#解决" class="headerlink" title="解决"></a>解决</h3><p>原因弄清楚了,接下来着手解决。导出业务使用的是spring batch做异步任务。分析其源码,发现默认隔离级别是<code>ISOLATION_SERIALIZABLE</code>。</p><p><img src="http://img.deanwangpro.com/t/tx_spring_batch.png" alt=""></p><p>所以确实是该三方库导致的。解决很简单,既然都已经有public的setter了,那么初始化时直接设置其隔离级别,<code>setIsolationLevelForCreate("ISOLATION_DEFAULT")</code>,避免使用默认值。修改以后再进行测试,确实没有修改隔离级别的情况出现了。</p><h3 id="寻根溯源"><a href="#寻根溯源" class="headerlink" title="寻根溯源"></a>寻根溯源</h3><p>可还是有疑问,我们的查询业务跟spring batch并没有关系。另一方面,即使在使用spring batch后,SERIALIZABLE的connection也应该被重置回了READ_COMMITTED,那么业务中获得connection时应当还是READ_COMMITTED,不应该出现问题。正好我们有另一个服务也使用spring batch,测试下来发现过程就如上面预想的,一切正常。看来还有更深的秘密等待着我…</p><p>根据<code>arthas</code>给出的调用栈信息,定位到spring-orm的<code>EclipseLinkJpaDialect</code>这个方言适配类(我们使用的是EclipseLink而不是Hibernate)。</p><p><img src="http://img.deanwangpro.com/t/tx_spring_orm_eclipselink.png" alt=""></p><p>问题就出在划红线的这行。<code>uow.getLogin()</code>获得是一个可复用的对象(简单地认为是个单例就好),这个值在设置成SERIALIZABLE后,后续的操作都不能被设置回来。为什么后面的执行代码都不能满足<code>definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT</code>这个条件判断呢?我们来脑补一下步骤:</p><ol><li>执行导出,这时由于使用了spring batch,在获取connection时将其隔离级别设置为SERIALIZABLE (8)。</li><li>执行完成后,将connection放回pool,HikariCP恢复connection的隔离级别为READ_COMMITTED (2)。</li><li>一个非导出业务,需要执行数据库操作,从HikariCP的pool中借出一个connection,此时<code>TransactionDefinition</code>的<code>IsolationLevel</code>是默认值<code>ISOLATION_DEFAULT</code>(<code>TransactionDefinition</code>其实就是方法上<code>@Transactional</code>注解的信息,一般都不会设置<code>IsolationLevel</code>的)。所以无法满足条件判断,<code>uow.getLogin()</code>的隔离级别还是第1步设置的SERIALIZABLE,这也是为什么READ_COMMITTED (2)又会被设置成SERIALIZABLE (8) 的原因。</li><li>这个业务执行完成后,将connection放回pool,HikariCP恢复connection的隔离级别为READ_COMMITTED (2)。</li></ol><p>总算水落石出了,是spring-orm中一个“全局变量”的状态设置出现了问题。我也给spring-orm提了<a href="https://github.com/spring-projects/spring-framework/issues/24802" target="_blank" rel="noopener">issue</a>,并提供了<a href="https://github.com/deanwong/spring-orm-sample" target="_blank" rel="noopener">最简Project</a>来方便复现这个bug。感兴趣的可以看一下这个project。</p><p>其实spring batch也算是躺枪,假设我们手动设置一个事务隔离级别,也会触发这个异常。</p><figure class="highlight java"><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 class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">UserRepository</span> <span class="keyword">extends</span> <span class="title">JpaRepository</span><<span class="title">User</span>, <span class="title">Long</span>> </span>{</span><br><span class="line"> <span class="meta">@Transactional</span>(isolation = Isolation.SERIALIZABLE)</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function">List<User> <span class="title">findAll</span><span class="params">()</span></span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>所以 root casue 并不是spring batch设置了SERIALIZABLE的隔离级别,而是<code>uow.getLogin()</code>的状态处理有bug。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>整个问题解决差不多用了3天,特别感谢阿里云RDS的SQL洞察服务和阿里出品的<code>arthas</code>(真不是软广)。</p><p>全局state设置一直是个难题,无论是客户端代码还是服务端代码都会面临相同的问题,当我们希望用尽量少的开销保存更多的状态时,复杂度就难以避免。即便Pivotal团队的大神也不能保证bug free。Trouble shooting就像侦破案件一般,有时候黑夜中唯一的一束光就能带来真相,但往往需要巨大的耐心等待它的出现。</p>]]></content>
<summary type="html">
<p>MySQL死锁从产品之初就偶有发生,算是萦绕在心中的噩梦之一。由于死锁大都伴随着锁等待,所以一般都会拉低服务QPS,在死锁发生时肯定会出现各种意料不到的问题。前期一直采用“指标不治本”的办法,对特别容易产生死锁的方法增加retry。但当retry和事务嵌套在一起时也会出现不可预知的错误。</p>
<p>对于数据库死锁这个万恶之源,真可谓深恶痛绝,所以这次在解决retry和事务嵌套问题时,将这个元凶也一并解决。</p>
</summary>
<category term="spring" scheme="http://www.deanwangpro.com/tags/spring/"/>
<category term="mysql" scheme="http://www.deanwangpro.com/tags/mysql/"/>
<category term="deadlock" scheme="http://www.deanwangpro.com/tags/deadlock/"/>
<category term="retry" scheme="http://www.deanwangpro.com/tags/retry/"/>
</entry>
<entry>
<title>Full GC (Allocation Failure) 引发的应用僵死</title>
<link href="http://www.deanwangpro.com/2020/02/22/oom-address/"/>
<id>http://www.deanwangpro.com/2020/02/22/oom-address/</id>
<published>2020-02-21T16:00:00.000Z</published>
<updated>2020-03-08T15:38:46.875Z</updated>
<content type="html"><![CDATA[<p>距离上次博文已经有快半年了。原本打算在过年期间写一篇2019年的回顾和总结。但也被这突如其来的疫情打乱。放假期间一直关注相关新闻导致信息过载,一度非常沮丧。</p><p>2月复工以后,停止了疫情信息的摄取,反倒心理上轻松不少。人都是健忘的,可不是吗?</p><p>言归正传,上篇博文说遇到一个服务假死的问题,具体现象就是服务不再接收任何请求,客户端会抛出Broken Pipe。正好最近又重现了,并且找到了root cause。</p><a id="more"></a><h2 id="检查状态"><a href="#检查状态" class="headerlink" title="检查状态"></a>检查状态</h2><h3 id="系统状态"><a href="#系统状态" class="headerlink" title="系统状态"></a>系统状态</h3><p>依然从全局入手,执行top查看发现负载很低,cpu都在10%以下,说明不是计算密集的问题,基本可以感觉是io或者是jvm垃圾回收有异常。另外查询系统情况推荐dstat这个工具,蜜汁好用。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">dstat -lcdngy</span><br></pre></td></tr></table></figure><h3 id="JVM状态"><a href="#JVM状态" class="headerlink" title="JVM状态"></a>JVM状态</h3><p>因为开启了gc log,所以先检查一下是不是有频繁的full gc,当然上手就<code>jstat</code>不犹豫也是没问题的。</p><figure class="highlight shell"><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">2020-02-19T17:32:59.041+0800: 5618.660: Application time: 0.9945668 seconds</span><br><span class="line">{Heap before GC invocations=339 (full 70):</span><br><span class="line"> par new generation total 1415616K, used 1415615K [0x00000006f0800000, 0x0000000750800000, 0x0000000750800000)</span><br><span class="line"> eden space 1258368K, 100% used [0x00000006f0800000, 0x000000073d4e0000, 0x000000073d4e0000)</span><br><span class="line"> from space 157248K, 99% used [0x000000073d4e0000, 0x0000000746e6ffd0, 0x0000000746e70000)</span><br><span class="line"> to space 157248K, 0% used [0x0000000746e70000, 0x0000000746e70000, 0x0000000750800000)</span><br><span class="line"> concurrent mark-sweep generation total 2621440K, used 2621440K [0x0000000750800000, 0x00000007f0800000, 0x00000007f0800000)</span><br><span class="line"> Metaspace used 124199K, capacity 131576K, committed 135808K, reserved 1169408K</span><br><span class="line"> class space used 14564K, capacity 15735K, committed 16256K, reserved 1048576K</span><br><span class="line">2020-02-19T17:32:59.042+0800: 5618.661: [Full GC (Allocation Failure) 2020-02-19T17:32:59.043+0800: 5618.661: [CMS2020-02-19T17:33:00.282+0800: 5619.901: [CMS-concurrent-mark: 2.232/2.235 secs] [Times: user=3.23 sys=0.00, real=2.24 secs] </span><br><span class="line"> (concurrent mode failure): 2621440K->2611778K(2621440K), 7.6795988 secs] 4037055K->2611778K(4037056K), [Metaspace: 124199K->124199K(1169408K)], 7.6798738 secs] [Times: user=7.68 sys=0.00, real=7.68 secs] </span><br><span class="line">Heap after GC invocations=340 (full 71):</span><br><span class="line"> par new generation total 1415616K, used 0K [0x00000006f0800000, 0x0000000750800000, 0x0000000750800000)</span><br><span class="line"> eden space 1258368K, 0% used [0x00000006f0800000, 0x00000006f0800000, 0x000000073d4e0000)</span><br><span class="line"> from space 157248K, 0% used [0x000000073d4e0000, 0x000000073d4e0000, 0x0000000746e70000)</span><br><span class="line"> to space 157248K, 0% used [0x0000000746e70000, 0x0000000746e70000, 0x0000000750800000)</span><br><span class="line"> concurrent mark-sweep generation total 2621440K, used 2611778K [0x0000000750800000, 0x00000007f0800000, 0x00000007f0800000)</span><br><span class="line"> Metaspace used 124199K, capacity 131576K, committed 135808K, reserved 1169408K</span><br><span class="line"> class space used 14564K, capacity 15735K, committed 16256K, reserved 1048576K</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>大概有十几个Full GC,原因都是 Allocation Failure。特别值得注意的是,GC前eden的占用是100%,s0的占用是99%,此时还有Allocation Failure说明old区也满了。eden和s0的对象都无法通过Young GC回收,年轻代对象继续向old区晋升,但是old区也满了,导致 Allocation Failure 触发Full GC,而且耗时7.68 secs,这可是STW啊,整个系统都暂停响应了。再看GC后的情况,年轻代都清零了,说明并不是有内存泄露导致无法回收。而是短时间有大量新对象被创建,Young GC来不及回收,直到新对象不断晋升到老年代导致的STW。</p><p>那么接下来就开始追查什么原因会创建这么多新对象,手头缺一个最好的证据,就是Full GC时的heap dump。开启<code>jstat</code>不停的刷,等到old区快满的时候,抓dump。</p><p><img src="http://img.deanwangpro.com/t/20200222-oom-jstat.jpg" alt=""></p><p>时机正好,通过<code>jmap</code>导出dump文件</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">jmap -dump:format=b,file=/tmp/heap.hprof 1</span><br></pre></td></tr></table></figure><p>一看文件足足有6g,说明真的是待回收的对象太多了。</p><h2 id="分析Dump"><a href="#分析Dump" class="headerlink" title="分析Dump"></a>分析Dump</h2><p>使用mat打开这个dump文件都花了半小时,风扇呼呼的在抗议。一个小tip,记得把mat的Xmx调大,不然会崩掉。另外由于这次并不是内存泄露造成不可回收,所以需要打开mat的<code>Keep unreachable objects</code>去分析不可触达的对象。</p><p><img src="http://img.deanwangpro.com/t/20200222-big-object.jpg" alt=""></p><p>打开lead suspects可以看到最大的是<code>java.lang.Object[]</code>,这也印证了大量对象被创建并被加入数组的猜测。</p><h2 id="Root-cause"><a href="#Root-cause" class="headerlink" title="Root cause"></a>Root cause</h2><p>那么究竟是什么操作引发的呢?在mat的报告中最终发现这个object是一个entity类,说明这是一个数据库操作,查询出了大量结果导致的。</p><p>这种查询量级一定会被记录为了慢查询,打开阿里云的RDS慢日志查询</p><p><img src="http://img.deanwangpro.com/t/20200222-mysql-full-scan.jpg" alt=""></p><p>218w的记录被查询到,没有直接OOM算是走运了。</p><p>但是查询的sql很奇怪,出现了<code>is null</code>的条件语句,由于这个字段是索引字段应该精确匹配的,业务逻辑中不可能出现查询空记录的需求。repository的查询接口是使用spring-data-jpa标准描述的。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">Person <span class="title">findByAAndBAndC</span><span class="params">(String A, String B, String C)</span></span>;</span><br></pre></td></tr></table></figure><p>理论上应该转化为</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">select</span> * <span class="keyword">from</span> person <span class="keyword">where</span> A=A <span class="keyword">and</span> B=B <span class="keyword">and</span> C=C;</span><br></pre></td></tr></table></figure><p>结果却变成了</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">select</span> * <span class="keyword">from</span> person <span class="keyword">where</span> A <span class="keyword">is</span> <span class="literal">null</span> <span class="keyword">and</span> B=B <span class="keyword">and</span> C=C;</span><br></pre></td></tr></table></figure><p>这样以来,就跳开了索引并查询到相当可观的记录。经过试验发现如果未经过判空校验,当A=null时,spring-data-jpa会自动转化为<code>is null</code>的SQL语句。</p><p>最终我们对所有类似调用都加上了<code>Assert.notnull</code>的处理。重新上线后问题解决,再也没有FGC发生了。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>对于程序员来讲,这种“智能”的框架真的是非常可怕,当你面对一个黑盒的时候,灾难往往就在不远处。</p>]]></content>
<summary type="html">
<p>距离上次博文已经有快半年了。原本打算在过年期间写一篇2019年的回顾和总结。但也被这突如其来的疫情打乱。放假期间一直关注相关新闻导致信息过载,一度非常沮丧。</p>
<p>2月复工以后,停止了疫情信息的摄取,反倒心理上轻松不少。人都是健忘的,可不是吗?</p>
<p>言归正传,上篇博文说遇到一个服务假死的问题,具体现象就是服务不再接收任何请求,客户端会抛出Broken Pipe。正好最近又重现了,并且找到了root cause。</p>
</summary>
<category term="java" scheme="http://www.deanwangpro.com/tags/java/"/>
<category term="gc" scheme="http://www.deanwangpro.com/tags/gc/"/>
</entry>
<entry>
<title>记录一次Spring Boot假死诊断</title>
<link href="http://www.deanwangpro.com/2019/07/28/zombie-thread-trouble-shooting/"/>
<id>http://www.deanwangpro.com/2019/07/28/zombie-thread-trouble-shooting/</id>
<published>2019-07-27T16:00:00.000Z</published>
<updated>2019-07-30T12:40:11.000Z</updated>
<content type="html"><![CDATA[<p>这两天遇到一个服务假死的问题,具体现象就是服务不再接收任何请求,客户端会抛出Broken Pipe。</p><a id="more"></a><h2 id="检查系统状态"><a href="#检查系统状态" class="headerlink" title="检查系统状态"></a>检查系统状态</h2><p>执行top,发现CPU和内存占用都不高,但是通过命令</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">netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'</span><br></pre></td></tr></table></figure><p>发现有大量的CLOSE_WAIT端口占用,继续调用该服务的api,等待超时之后发现CLOSE_WAIT的数量也没有上升,也就是说服务几乎完全僵死。</p><h2 id="检查JVM情况"><a href="#检查JVM情况" class="headerlink" title="检查JVM情况"></a>检查JVM情况</h2><p>怀疑可能是线程有死锁,决定先dump一下线程情况,执行</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">jstack <pid> > /tmp/thread.hump</span><br></pre></td></tr></table></figure><p>发现tomcat线程基本也正常,都是parking状态。</p><p><img src="/images/2019-07-28/Thread.jpg" alt=""></p><p>这就比较奇怪了,继续想是不是GC导致了STW,使用<code>jstat</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></pre></td><td class="code"><pre><span class="line">app@server:/tmp$ jstat -gcutil 1 2000 10</span><br><span class="line"> S0 S1 E O M CCS YGC YGCT FGC FGCT GCT</span><br><span class="line"> 0.00 27.79 65.01 15.30 94.75 92.23 1338 44.375 1881 475.064 519.439</span><br></pre></td></tr></table></figure><p>一看吓一跳,FGC的次数居然超过了YGC,时长有475s。一定是有什么原因触发了FGC,好在我们打开了GC log。</p><p><img src="/images/2019-07-28/GC.jpg" alt=""></p><p>发现一段时间内频繁发生Allocation Failure引起的Full GC。而且eden区的使用占比也很大,考虑有频繁新建对象逃逸到老年代造成问题。询问了一下业务的开发,确认有一个外部对接API没有分页,查询后可能会产生大量对象。</p><p>由于外部API暂时无法联系对方修改,所以为了先解决问题,对原有的MaxNewSize进扩容,从192MB扩容到一倍。经过几天的观察,发现gc基本趋于正常</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">S0 S1 E O M CCS YGC YGCT FGC FGCT GCT</span><br><span class="line">0.00 3.37 60.55 8.60 95.08 92.98 87 2.421 0 0.000 2.421</span><br></pre></td></tr></table></figure><p>扩容之前对heap进行了dump</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">jmap -dump:format=b,file=heapDump <PID></span><br></pre></td></tr></table></figure><p>通过MAT分析内存泄露,居然疑似是jdbc中的一个类,但其实整体占用堆容量并不多。</p><p><img src="/images/2019-07-28/mat.jpg" alt=""></p><p>分析了线程数量,大约是240多条,与正常时也并没有很大的出入。而且大量的是在sleep的定时线程。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本次排查其实并未找到真正的原因,间接表象是FGC频繁导致服务假死。而且acturator端口是正常工作的,导致health check进程误认为服务正常,没有触发告警。如果你也遇到类似的情况欢迎一起讨论。</p>]]></content>
<summary type="html">
<p>这两天遇到一个服务假死的问题,具体现象就是服务不再接收任何请求,客户端会抛出Broken Pipe。</p>
</summary>
<category term="spring boot" scheme="http://www.deanwangpro.com/tags/spring-boot/"/>
<category term="tomcat" scheme="http://www.deanwangpro.com/tags/tomcat/"/>
<category term="zombie" scheme="http://www.deanwangpro.com/tags/zombie/"/>
</entry>
<entry>
<title>怎么用好Spring Config</title>
<link href="http://www.deanwangpro.com/2019/07/07/benifit-from-spring-config/"/>
<id>http://www.deanwangpro.com/2019/07/07/benifit-from-spring-config/</id>
<published>2019-07-06T16:00:00.000Z</published>
<updated>2019-07-19T08:14:36.000Z</updated>
<content type="html"><![CDATA[<p>配置其实分为结构和内容两个方面,结构对应的是代码,比如1.0.0新开发的代码上有一个功能开关<code>${feature.switchA}</code>,但master上还没有,这就是结构的变化。另一方面是内容,1.0.0的开发分支有两个测试环境,连着不同的数据库,那么对应的<code>${mysql.url}</code>的内容肯定不同。</p><p>内容的类别上也可以分为三种:业务配置,功能开关,服务配置。</p><p>Spring Cloud的配置中心是Spring Config,经过两年的使用,发现了其中不少的问题,有些是使用问题,有些是Spring Config本身的管理能力导致的问题。</p><p>Spring Config首推基于git的管理方式,提供了两个管理维度,一个是label(即branch),一个是profile。当服务foo在一套代码下要安装多套环境,比如预发布环境有2套,一套在shanghai机房,一套在beijing机房。那么比较自然的管理维度就是利用profile,foo-shanghai.yaml以及foo-beijing.yaml。当生产环境也依然需要2台时,怎么处理呢?这时候就会有两种做法,一种利用增加label维度做区分,一种依然只用profile。</p><a id="more"></a><h2 id="方法一:用label-profile区分"><a href="#方法一:用label-profile区分" class="headerlink" title="方法一:用label + profile区分"></a>方法一:用label + profile区分</h2><table><thead><tr><th>Name</th><th>Branch</th><th>Profile</th></tr></thead><tbody><tr><td>foo-shanghai.yaml</td><td>stg</td><td>shanghai</td></tr><tr><td>foo-beijing.yaml</td><td>stg</td><td>beijing</td></tr><tr><td>foo-shanghai.yaml</td><td>prd</td><td>shanghai</td></tr><tr><td>foo-beijing.yaml</td><td>Prd</td><td>beijing</td></tr><tr><td>branch其实表示的是结构,即对应不同的代码,而profile对应的是内容。</td><td></td><td></td></tr></tbody></table><p>这种方式有什么问题?一般应用都是只有profile来区分环境,比如logback要分环境区分配置也是通过<code><springProfile></code>来指定。一旦采用两个维度来确定唯一的配置,那么所有项目都需要有<code>label</code>这个变量。</p><p>试想如果foo这个应用在线上有个bug需要fix,势必会增加一个hotfix的branch在配置中心,同时还需要增加相应的profile,对应foo的<code>label</code>变量设置为hotfix,<code>profile</code>设置为beijing或者shanghai。</p><p>再考虑另一种情况,foo在prd的代码需要放到stg进行验证如何处理?foo的代码版本肯定是prd的(因为stg的配置结构也许已经变了),但profile需要用stg的环境。这时实际上只能在配置中心的prd分支上新建一个新的profile来临时满足这种需求。</p><h2 id="方法二:只使用profile区分"><a href="#方法二:只使用profile区分" class="headerlink" title="方法二:只使用profile区分"></a>方法二:只使用profile区分</h2><table><thead><tr><th>Name</th><th>Branch</th><th>Profile</th></tr></thead><tbody><tr><td>foo-stg-shanghai.yaml</td><td>master</td><td>stg-shanghai</td></tr><tr><td>foo-stg-beijing.yaml</td><td>master</td><td>stg-beijing</td></tr><tr><td>foo-prd-shanghai.yaml</td><td>master</td><td>prd-shanghai</td></tr><tr><td>foo-prd-beijing.yaml</td><td>master</td><td>prd-beijing</td></tr></tbody></table><p>这种方式可以降低管理维度,即放弃label的维度,只有profile的维度。同样的问题,如果foo这个应用在线上有个bug需要fix,那么需要新增两个profile,hotfix-beijing和hotfix-shanghai。虽然维度降低了,但是管理上却有些麻烦。因为master的这个分支无法保护起来,如果有开发人员直接修改了prd-XXX的环境就会导致线上问题。</p><p>同样的,foo在prd的代码需要放到stg进行验证如何处理?foo的代码版本肯定是prd的(因为stg的配置结构也许已经变了),但profile需要用stg的环境。这时实际上只能再配置中心新建一个profile,比如stg-oldshanghai,来满足这种需求。</p><p>然而我们知道,增加新的profile其实还是挺麻烦的事情,如果代码中有直接比较profile的逻辑,那么往往容易出现问题。</p><p>有没有不临时增加profile的办法呢?其实仔细思考一下,在stg环境验证prd的服务,真正的逻辑是什么?是希望用stg环境的配置内容,以及stg某个历史版本(与prd匹配的)的配置结构。所以纵向维度我们需要的其实是version,profile都是stg-shanghai,而version一个是1.0.0,一个是latest。</p><h2 id="方法三:综合一下"><a href="#方法三:综合一下" class="headerlink" title="方法三:综合一下"></a>方法三:综合一下</h2><p>好了,现在我们来综合一下两种方式,可以使用git的分支作为version,profile依然还是按照方法二来区分。毕竟频繁增加环境的可能性不高。但是如果要同时维护一个profile两个分支,其实还是要来回切换的,比较麻烦,这也是Spring Config为人诟病的管理功能弱。好在Spring Cloud也支持mysql,用mysql同时管理多个label的内容还是方便不少,只是git自带的“后悔药”(history)功能没有了。所以说还是有利有弊。</p><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>如果想要更完善的配置管理工具,建议还是使用Apollo。要想用好Spring Cloud,必须可以忍受它比较弱的管理能力,并且做好前期规划,结合项目特点来使用label和profile的能力。</p>]]></content>
<summary type="html">
<p>配置其实分为结构和内容两个方面,结构对应的是代码,比如1.0.0新开发的代码上有一个功能开关<code>${feature.switchA}</code>,但master上还没有,这就是结构的变化。另一方面是内容,1.0.0的开发分支有两个测试环境,连着不同的数据库,那么对应的<code>${mysql.url}</code>的内容肯定不同。</p>
<p>内容的类别上也可以分为三种:业务配置,功能开关,服务配置。</p>
<p>Spring Cloud的配置中心是Spring Config,经过两年的使用,发现了其中不少的问题,有些是使用问题,有些是Spring Config本身的管理能力导致的问题。</p>
<p>Spring Config首推基于git的管理方式,提供了两个管理维度,一个是label(即branch),一个是profile。当服务foo在一套代码下要安装多套环境,比如预发布环境有2套,一套在shanghai机房,一套在beijing机房。那么比较自然的管理维度就是利用profile,foo-shanghai.yaml以及foo-beijing.yaml。当生产环境也依然需要2台时,怎么处理呢?这时候就会有两种做法,一种利用增加label维度做区分,一种依然只用profile。</p>
</summary>
<category term="spring" scheme="http://www.deanwangpro.com/tags/spring/"/>
<category term="config" scheme="http://www.deanwangpro.com/tags/config/"/>
</entry>
<entry>
<title>类似Github的webhook实现</title>
<link href="http://www.deanwangpro.com/2019/05/31/github-webhook/"/>
<id>http://www.deanwangpro.com/2019/05/31/github-webhook/</id>
<published>2019-05-30T16:00:00.000Z</published>
<updated>2019-05-31T03:05:49.000Z</updated>
<content type="html"><![CDATA[<p>Webhook是一种非常强大的推送机制,如果熟悉WordPress的同学可以类比构建WP生态的各类钩子函数。Githubt通过webhook让开发人员可以监听仓库的变化触发持续集成工具的运作,比如Travis CI。</p><a id="more"></a><h2 id="需求"><a href="#需求" class="headerlink" title="需求"></a>需求</h2><p>大家都看过Github上的webhook,可以对某一个repository设置webhook监听仓库变化,比如push,page_build等event(X-GitHub-Event)。</p><p><img src="/images/201905/github-webhook.jpg" alt="github-webhook"><br>每一次发送都会有个uuid作为标记,并写入到HTTP Header的X-GitHub-Delivery,并且对于发送失败的历史记录,可以点击Redeliver进行重发。</p><p><img src="/images/201905/github-webhook-history.jpg" alt="github-webhook-history"></p><p>如果把该功能作为一个单独的服务提供方,其根本诉求就是要准确记录到此服务与Internet每一次网络交互的过程,包括发送请求数据和响应结果数据。继续细化,</p><ol><li>该服务提供方接收客户端的调用,发送请求到客户端所指定的url并获取响应。</li><li>记录每次客户端的原始请求内容(url, method, header, body)以及response(header, body, code, etc.)</li><li>需要考虑到客户端重试或重复调用的情况,需要记录每个请求的调用次数以及最后一次调用时间。(客户端调用时可能会传入一个clientId用于接收端去重,如未传服务提供方根据请求生成一个唯一uuid)</li><li>提供接口对指定的某个发送历史进行重发。</li></ol><h2 id="思考"><a href="#思考" class="headerlink" title="思考"></a>思考</h2><p>拿到需求首先要思考一下这个服务会与哪些系统有交互?</p><ol><li>请求要发送到指定的URL上,那么第一个交互的系统是某个公网服务。</li><li>发送的历史要能保留,说明数据是需要持久化的。第二个交互的系统是数据库。</li></ol><p>好了,交互的系统确定后,接下来应该考虑顺序问题,是先发送请求到公网服务还是先操作数据库?我们逐个来分析一下</p><ul><li>方案A:先发送请求再将记录写入数据库。问题:如果请求发送了但是数据库写入失败,此时就会造成数据不一致,因为遗漏了发送历史。</li><li>方案B:先写入数据库再发送请求。问题:与A类似的,如果数据库写入成功,请求发送失败,比如网络断开等原因。此时数据也会不一致。虽然有了发送历史,但实际发送是失败的。</li><li>方案C:先写入数据库接着发送请求最后更新数据。这种方案相对来说比A和B要可靠。第一步写入请求的数据并将状态(status)置为<code>sending</code>,发送完成再更新status为<code>success</code>或者<code>failure</code>。</li><li>方案D:先写入数据库,将status设置为<code>sending</code>,启动一个新的线程扫描该表,对status为<code>ready</code>的记录进行发送,发送完成再更新status为<code>success</code>或者<code>failure</code>。</li></ul><p>前两个方案肯定是不可取的,我们来分析一下后两个方案的优劣。方案C的缺点在于第一步写入数据库完成后,发送请求时系统宕机,该记录会一直处于<code>sending</code>状态。好在整体方案会提供一个人工重试(点击Redeliver)的机制,可以事后弥补。优点在于串行化的思维编码比较容易。方案D的优点就是对于一直处于<code>sending</code>状态的历史记录,可以自动进行补发,因为有线程不断扫描。缺点在于这个扫描线程可能会加重数据库的负担。如果要想并行扫描那么又要解决任务分片和编排的问题(参考elestic-job),编码相对较难。</p><p>针对以上所述的利弊,最终我们选择方案C。其实把发送网络请求换成发送消息到MQ,那么方案D就很类似大家所熟知的“本地事务表”的解决方案,是将MQ的事务和本地数据库事务绑定的一种思路。</p><h2 id="实现"><a href="#实现" class="headerlink" title="实现"></a>实现</h2><p>数据结构,定义一个request和response</p><pre><code>@Data@NoArgsConstructorpublic class WebHookRequest { @NotBlank private String url; private String method; private Map<String, String> headers; private String body;}@Data@NoArgsConstructor@AllArgsConstructorpublic class WebHookResponse { private String id; private String data; private int code; public boolean isSuccessful() { return this.code >= 200 && this.code < 300; }}</code></pre><p>发送方法</p><pre><code>public WebHookResponse send(WebHookRequest webHookRequest, String id, boolean retryOnServerError) { //如果traceId为空则根据请求参数生成一个md5的值作为traceId if (StringUtils.isBlank(id)) { //拼接请求参数 String content = webHookRequest.getUrl() .concat(webHookRequest.getMethod()) .concat(jsonMapper.toJson(webHookRequest.getHeaders())) .concat(webHookRequest.getBody()); id = DigestUtils.md5Hex(content); } // 查找发送记录 WebHookRecord webHookRecord = webHookRecordMapper.findById(id, TenantContext.getCurrentTenant()); if (webHookRecord == null) { webHookRecord = createFromRequest(webHookRequest); webHookRecord.setId(id); try { // 新建发送记录 webHookRecordMapper.insert(webHookRecord); } catch (Exception ex) { LOGGER.warn("Duplicate key for [{}]", webHookRecord.getId()); throw new BizException(B_01000, ex); } } else { // 如果历史已经存在,可能是客户端发送重发请求,那么先判断是否可以重发,sending状态在一定时间间隔内不能重发,避免频繁失败 if (this.shouldResend(webHookRecord.getSendTime(), webHookRecord.getStatus())) { // 可以重发那么更新状态为sending this.updateResendStatus(webHookRecord); } else { LOGGER.warn("Should not resend key for [{}]", webHookRecord.getId()); throw new BizException(B_01000); } } // 通过网络发送请求,如果失败会直接更新status为failure并抛出异常,发送过程的异常和得到响应对方服务报异常还是不一样的 WebHookResponse webHookResponse = this.doSend(id, webHookRequest); // 发送完成后更新status this.onResponse(id, webHookResponse); if (retryOnServerError && webHookResponse.getCode() >= 500) { throw new RetryException("server error!"); } return webHookResponse;}/** 判断是否可以进行重发*/private boolean shouldResend(DateTime sendTime, String status) { if (!STATUS_SENDING.equals(status)) { return true; } // 发送状态超过60s可重发 return sendTime.plusSeconds(60).isBefore(DateTime.now());}/** 调用网络接口进行发送*/private WebHookResponse doSend(String id, WebHookRequest webHookRequest) { //调用httpClient String responseBodyString = null; Response response; try { response = webHookIntegrationService.send(webHookRequest.getUrl(), webHookRequest.getMethod(), webHookRequest.getHeaders(), webHookRequest.getBody()); if (response.body() != null) { responseBodyString = response.body().string(); } } catch (Exception ex) { this.onSendFailed(id, ExceptionUtils.getStackTrace(ex)); throw new RetryException("http send error!", ex); } return new WebHookResponse(id, responseBodyString, response.code());}/** 请求正常返回后的处理* 这里有一个乐观锁的问题,如果同时有多个线程调用改办法修改同一个历史请求,只有一个线程会更新成功*/private void onResponse(String id, WebHookResponse webHookResponse) { WebHookRecord webHookRecord = webHookRecordMapper.findById(id, TenantContext.getCurrentTenant()); webHookRecord.setResponse(webHookResponse.getData()); webHookRecord.setResponseCode(webHookResponse.getCode()); //更新状态 if (webHookResponse.isSuccessful()) { webHookRecord.setStatus(STATUS_SUCCESS); } else { webHookRecord.setStatus(STATUS_ERROR); } int count = webHookRecordMapper.updateResponse(webHookRecord); if (count == 0) { LOGGER.warn("Attempt to update WebHook id={} with wrong version ({})", id, webHookRecord.getVersion()); }}/** 再次发送时更新状态 (真正发送网络请求前)*/private void updateResendStatus(WebHookRecord webHookRecord) { webHookRecord.setStatus(STATUS_SENDING); webHookRecord.setSendTime(DateTime.now()); int count = webHookRecordMapper.resend(webHookRecord); if (count == 0) { throw new OptimisticLockingFailureException("Attempt to update WebHook id=" + webHookRecord.getId() + " with wrong version (" + webHookRecord.getVersion() + ")"); }}</code></pre><p>为了避免对于同一个请求有多个线程同时发起重试的问题,我们在<code>updateResendStatus</code>方法上使用了乐观锁,如果其中一个线程更新状态成功,那么其他线程会因乐观锁问题直接失败,不会走到真正发送网络的请求的那步。也就是说在发送网络请求前过滤绝大部分并发问题。</p><p>最后,可以使用一个单元测试,模拟并发请求进行验证。</p><pre><code>@Testpublic void testMultiThreads() throws InterruptedException { //调用send接口 WebHookRequest webHookRequest = new WebHookRequest(); webHookRequest.setUrl("http://www.qq.com"); webHookRequest.setMethod("POST"); Map<String, String> headers = new HashMap<>(1); webHookRequest.setHeaders(headers); int nLoop = 100; String clientId = UUID.randomUUID().toString(); CountDownLatch countDownLatch = new CountDownLatch(nLoop); Runnable task = () -> { try { givenToken().when().body(webHookRequest).post("/webhooks/send?clientId=" + clientId) .then() .statusCode(HttpStatus.OK.value()) .extract() .response(); } finally { countDownLatch.countDown(); } }; ExecutorService executorService = new ThreadPoolBuilder.FixedThreadPoolBuilder().setThreadNamePrefix("thread-webhook").setPoolSize(100).build(); for (int i = 0; i < nLoop; i++) { executorService.execute(task); } countDownLatch.await(); int times = webHookMapper.findById(clientId, 1L).getTimes(); // 验证数据库里记录的发送次数是否 等于 真正调用发送网络请求接口的次数 Mockito.verify(webHookIntegrationService, Mockito.times(times)).send(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());}</code></pre><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>如果想进一步提升性能,可以使用支持异步的httpclient工具包,<code>onResponse</code>在callback中进行处理。</p><p>整体来说,这是个很简单的小需求,但要考虑周全其实还是要费一番功夫的。究其本质就是异构系统间的数据一致性问题。当我们把发送网络请求换成写数据到redis,到MQ,到另一个微服务时,就会发现它们存在的共通性。一次请求涉及多个系统,并且无法包裹进同一个事务,就会产生这样的问题。至于解决方案是二阶段提交,事后补偿,还是自动对账,就要根据自己的业务特点来选择了。</p>]]></content>
<summary type="html">
<p>Webhook是一种非常强大的推送机制,如果熟悉WordPress的同学可以类比构建WP生态的各类钩子函数。Githubt通过webhook让开发人员可以监听仓库的变化触发持续集成工具的运作,比如Travis CI。</p>
</summary>
<category term="CAP" scheme="http://www.deanwangpro.com/tags/CAP/"/>
<category term="webhook" scheme="http://www.deanwangpro.com/tags/webhook/"/>
</entry>
<entry>
<title>小团队微服务落地实践</title>
<link href="http://www.deanwangpro.com/2019/04/19/microservice-practice/"/>
<id>http://www.deanwangpro.com/2019/04/19/microservice-practice/</id>
<published>2019-04-18T16:00:00.000Z</published>
<updated>2019-04-19T13:00:21.000Z</updated>
<content type="html"><![CDATA[<p>我们的产品是一个客户数据平台。产品的一个重要部分类似企业版的”捷径”,让运营人员可以像搭乐高积木一样创建企业的自动化流程,无需编程即可让数据流动起来。从这一点上,我们的业务特点就是聚少成多,把一个个服务连接起来就成了数据的海洋。理念上跟微服务一致,一个个独立的小服务最终实现大功能。当然我们一开始也没有使用微服务,当业务还未成型就开始考虑架构,那么就是”过度设计”。另一方面需要考虑的因素就是”人”,有没有经历过微服务项目的人,团队是否有devops文化等等,综合考量是否需要微服务化。</p><a id="more"></a><h1 id="要不要微服务"><a href="#要不要微服务" class="headerlink" title="要不要微服务"></a>要不要微服务</h1><p>微服务的好处是什么?</p><ul><li>相比于单体应用,每个服务的复杂度会下降,特别是数据层面(数据表关系)更清晰,不会一个应用上百张表,新员工上手快。</li><li>对于稳定的核心业务可以单独成为一个服务,降低该服务的发布频率,也减少测试人员压力。</li><li>可以将不同密集型的服务搭配着放到物理机上,或者单独对某个服务进行扩容,实现硬件资源的充分利用。</li><li>部署灵活,在私有化项目中,如果客户有不需要的业务,那么对应的微服务就不需要部署,节省硬件成本,就像上文提到的乐高积木理念。</li></ul><p>微服务有什么挑战?</p><ul><li>一旦设计不合理,交叉调用,相互依赖频繁,就会出现牵一发动全身的局面。想象单个应用内service层依赖复杂的场面就明白了。</li><li>项目多了,轮子需求也会变多,需要有人专注公共代码的开发。</li><li>开发过程的质量需要通过持续集成(CI)严格把控,提高自动化测试的比例,因为往往一个接口改动会涉及多个项目,光靠人工测试很难覆盖所有情况。</li><li>发布过程会变得复杂,因为微服务要发挥全部能力需要容器化的加持,容器编排就是最大的挑战。</li><li>线上运维,当系统出现问题需要快速定位到某个机器节点或具体服务,监控和链路日志分析都必不可少。</li></ul><p>下面详细说说我们是怎么应对这些挑战的</p><h1 id="开发过程的挑战"><a href="#开发过程的挑战" class="headerlink" title="开发过程的挑战"></a>开发过程的挑战</h1><h2 id="持续集成"><a href="#持续集成" class="headerlink" title="持续集成"></a>持续集成</h2><p>通过CI将开发过程规范化,串联自动化测试和人工Review。</p><p>我们使用Gerrit作为代码&分支管理工具,在流程管理上遵循Gitlab的工作流模型。</p><ul><li>开发人员提交代码至Gerrit的magic分支</li><li>代码Review人员Review代码并给出评分</li><li>对应Repo的Jenkins job监听分支上的变动,触发Build job。经过IT和Sonar的静态代码检查给出评分</li><li>Review和Verify皆通过之后,相应Repo的负责人将代码merge到真实分支上</li><li>若有一项不通过,代码修改后重复过程</li><li>Gerrit将代码实时同步备份至的两个远程仓库中</li></ul><p><img src="/images/2019-04-19/CI-7f63954d-d630-4d00-a138-8cbbe6810812.png" alt=""></p><h2 id="集成测试"><a href="#集成测试" class="headerlink" title="集成测试"></a>集成测试</h2><p>一般来说代码自动执行的都是单元测试(Unit Test),即不依赖任何资源(数据库,消息队列)和其他服务,只测试本系统的代码逻辑。但这种测试需要mock的部分非常多,一是写起来复杂,二是代码重构起来跟着改的测试用例也非常多,显得不够敏捷。而且一旦要求开发团队要达到某个覆盖率,就会出现很多造假的情况。所以我们选择主要针对API进行测试,即针对controller层的测试。另外对于一些公共组件如分布式锁,json序列化模块也会有对应的测试代码覆盖。测试代码在运行时会采用一个随机端口拉起项目,并通过http client对本地API发起请求,测试只会对外部服务做mock,数据库的读写,消息队列的消费等都是真实操作,相当于把Jmeter的事情在Java层面完成一部分。Spring Boot项目可以很容易的启动这样一个测试环境,代码如下:</p><pre><code>@RunWith(SpringRunner.class)@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)</code></pre><p>测试过程的http client推荐使用<code>io.rest-assured:rest-assured</code>支持JsonPath,十分好用。</p><p>测试时需要注意的一个点是测试数据的构造和清理。构造又分为schema的创建和测试数据的创建。</p><ul><li>schema由flyway处理,在启用测试环境前先删除所有表,再进行表的创建。</li><li>测试数据可以通过<code>@Sql</code>读取一个sql文件进行创建,在一个用例结束后再清除这些数据。</li></ul><p>顺带说一下,基于flyway的schema upgrade功能我们封成了独立的项目,每个微服务都有自己的upgrade项目,好处一是支持command-line模式,可以细粒度的控制升级版本,二是也可以支持分库分表以后的schema操作。upgrade项目也会被制作成docker image提交到docker hub。</p><p>测试在每次提交代码后都会执行,Jenkins监听gerrit的提交,通过<code>docker run -rm {upgrade项目的image}</code>先执行一次schema upgrade,然后<code>gradle test</code>执行测试。最终会生成测试报告和覆盖率报告,覆盖率报告采用jacoco的gradle插件生成。如图。</p><p><img src="/images/2019-04-19/-9e423b2c-8c0b-4ae5-80aa-1414394848feuntitled.jpg" alt=""></p><p><img src="/images/2019-04-19/-47c5e4c3-9d49-442f-bd61-1d0f2167e23euntitled.jpg" alt=""></p><p>这里多提一点,除了集成测试,服务之间的接口要保证兼容,实际上还需要一种consumer-driven testing tool,就是说接口消费端先写接口测试用例,然后发布到一个公共区域,接口提供方发布接口时也会执行这个公共区域的用例,一旦测试失败,表示接口出现了不兼容的情况。比较推荐大家使用Pact或是Spring Cloud Contact。我们目前的契约基于”人的信任“,毕竟服务端开发者还不多,所以没有必要使用这样一套工具。</p><p>集成测试的同时还会进行静态代码检查,我们用的是sonar,当所有检查通过后jenkins会+1分,再由reviewer进行代码review。</p><h2 id="自动化测试"><a href="#自动化测试" class="headerlink" title="自动化测试"></a>自动化测试</h2><p>单独拿自动化测试出来说,就是因为它是质量保证的非常重要的一环,上文能在CI中执行的测试都是针对单个微服务的,那么当所有服务(包括前端页面)都在一起工作的时候是否会出现问题,就需要一个更接近线上的环境来进行测试了。</p><p>在自动化测试环节,我们结合Docker提高一定的工作效率并提高测试运行时环境的一致性以及可移植性。在准备好基础的Pyhton镜像以及Webdriver(selenium)之后,我们的自动化测试工作主要由以下主要步骤组成</p><ul><li>测试人员在本地调试测试代码并提交至Gerrit</li><li>Jenkins进行测试运行时环境的镜像制作,主要将引用的各种组件和库打包进一个Python的基础镜像</li><li>通过Jenkins定时或手动触发,调用环境部署的job将专用的自动化测试环境更新,然后拉取自动化测试代码启动一次性的自动化测试运行时环境的Docker容器,将代码和测试报告的路径镜像至容器内</li><li>自动化测试过程将在容器内进行</li><li>测试完成之后,不必手动清理产生的各种多余内容,直接在Jenkins上查看发布出来的测试结果与趋势</li></ul><p><img src="/images/2019-04-19/Test-cce3af46-fa1b-4bdc-996c-69a19fbfe7dc.png" alt=""></p><p><img src="/images/2019-04-19/pasted-image-91c2effb-ad25-424a-acf3-b0b6e46c97ee.png" alt=""></p><p>关于部分性能测试的执行,我们同样也将其集成到Jenkins中,在可以直观的通过一些结果数值来观察版本性能变化情况的回归测试和基础场景,将会很大程度的提高效率、便捷的观察趋势</p><ul><li>测试人员在本地调试测试代码并提交至Gerrit</li><li>通过Jenkins定时或手动触发,调用环境部署的job将专用的性能测试环境更新以及可能的Mock Server更新</li><li>拉取最新的性能测试代码,通过Jenkins的性能测试插件来调用测试脚本</li><li>测试完成之后,直接在Jenkins上查看通过插件发布出来的测试结果与趋势</li></ul><p><img src="/images/2019-04-19/perf-a70ca413-382e-4d98-b696-752dc6a56c90.png" alt=""></p><p><img src="/images/2019-04-19/pasted-image-2-22b160cb-d527-4d8b-a608-28cbb672fe0e.png" alt=""></p><h1 id="发布过程的挑战"><a href="#发布过程的挑战" class="headerlink" title="发布过程的挑战"></a>发布过程的挑战</h1><p>上面提到微服务一定需要结合容器化才能发挥全部优势,容器化就意味线上有一套容器编排平台。我们目前采用是Redhat的Openshift。所以发布过程较原来只是启动jar包相比要复杂的多,需要结合容器编排平台的特点找到合适的方法。</p><h2 id="镜像准备"><a href="#镜像准备" class="headerlink" title="镜像准备"></a>镜像准备</h2><p>公司开发基于gitlab的工作流程,git分支为master,pre-production和prodution三个分支,同时生产版本发布都打上对应的tag。每个项目代码里面都包含dockerfile与jenkinsfile,通过jenkins的多分支pipeline来打包docker镜像并推送到harbor私库上。</p><p><img src="/images/2019-04-19/jenkins-ac588247-2366-4338-9bb7-8ac8b8520187.jpg" alt=""></p><p>docker镜像的命令方式为 <code>项目名/分支名:git_commit_id</code>,如 <code>funnel/production:4ee0b052fd8bd3c4f253b5c2777657424fccfbc9</code>,tag版本的docker镜像命名为 <code>项目名/release:tag名</code>,如 <code>funnel/release:18.10.R1</code></p><p><img src="/images/2019-04-19/harbor1-418e44ad-9782-4137-bd7f-8644082d13b4.png" alt=""></p><p><img src="/images/2019-04-19/harbor2-ca928974-9b42-4262-8272-892a428ebd56.png" alt=""></p><p>在jenkins中执行build docker image job时会在每次pull代码之后调用harbor的api来判断此版本的docker image是否已经存在,如果存在就不执行后续编译打包的stage。在jenkins的发布任务中会调用打包job,避免了重复打包镜像,这样就大大的加快了发布速度。</p><h2 id="数据库Schema升级"><a href="#数据库Schema升级" class="headerlink" title="数据库Schema升级"></a>数据库Schema升级</h2><p>数据库的升级用的是flyway,打包成docker镜像后,在openshift中创建job去执行数据库升级。job可以用最简单的命令行的方式去创建</p><pre><code>oc run upgrade-foo --image=upgrade/production --replicas=1 --restart=OnFailure --command -- java -jar -Dprofile=production /app/upgrade-foo.jar</code></pre><p>脚本升级任务也集成在jenkins中。</p><h2 id="容器发布"><a href="#容器发布" class="headerlink" title="容器发布"></a>容器发布</h2><p>openshift有个特别概念叫DeploymentConfig,原生k8s Deployment与之相似,但openshift的DeploymentConfig功能更多些。</p><p>Deploymentconfig关联了一个叫做ImageStreamTag的东西,而这个ImagesStreamTag和实际的镜像地址做关联,当ImageStreamTag关联的镜像地址发生了变更,就会触发相应的DeploymentConfig重新部署。我们发布是使用了jenkins+openshift插件,只需要将项目对应的ImageStreamTag指向到新生成的镜像上,就触发了部署。</p><p><img src="/images/2019-04-19/deploymentconfig-b8e607bf-23b0-440d-8e8c-f157281c5765.png" alt=""></p><p>如果是服务升级,已经有容器在运行怎么实现平滑替换而不影响业务呢?</p><p>配置Pod的健康检查,Health Check只配置了ReadinessProbe,没有用LivenessProbe。因为LivenessProbe在健康检查失败之后,会将故障的pod直接干掉,故障现场没有保留,不利于问题的排查定位。而ReadinessProbe只会将故障的pod从service中踢除,不接受流量。使用了ReadinessProbe后,可以实现滚动升级不中断业务,只有当pod健康检查成功之后,关联的service才会转发流量请求给新升级的pod,并销毁旧的pod。</p><pre><code>readinessProbe: failureThreshold: 4 httpGet: path: /actuator/metrics port: 8090 scheme: HTTP initialDelaySeconds: 60 periodSeconds: 15 successThreshold: 2 timeoutSeconds: 2</code></pre><h1 id="线上运维的挑战"><a href="#线上运维的挑战" class="headerlink" title="线上运维的挑战"></a>线上运维的挑战</h1><h2 id="服务间调用"><a href="#服务间调用" class="headerlink" title="服务间调用"></a>服务间调用</h2><p>Spring Cloud使用eruka接受服务注册请求,并在内存中维护服务列表。当一个服务作为客户端发起跨服务调用时,会先获取服务提供者列表,再通过某种负载均衡算法取得具体的服务提供者地址(ip + port),即所谓的客户端服务发现。在本地开发环境中我们使用这种方式。</p><p>由于Openshift天然就提供服务端服务发现,即service模块,客户端无需关注服务发现具体细节,只需知道服务的域名就可以发起调用。由于我们有nodejs应用,在实现eureka的注册和去注册的过程中都遇到过一些问题,不能达到生产级别。所以决定直接使用service方式替换掉eureka,也为以后采用service mesh做好铺垫。具体的做法是,配置环境变量<code>EUREKA_CLIENT_ENABLED=false</code>,<code>RIBBON_EUREKA_ENABLED=false</code>,并将服务列表如 <code>FOO_RIBBON_LISTOFSERVERS: '[http://foo:8080](http://foo:8080/)'</code> 写进configmap中,以<code>envFrom: configMapRef</code>方式获取环境变量列表。</p><p>如果一个服务需要暴露到外部怎么办,比如暴露前端的html文件或者服务端的gateway。</p><p>Openshift内置的haproxy router,相当于k8s的ingress,直接在Openshift的web界面里面就可以很方便的配置。我们将前端的资源也作为一个Pod并有对应的Service,当请求进入haproxy符合规则就会转发到ui所在的Service。router支持A/B test等功能,唯一的遗憾是还不支持url rewrite。</p><p><img src="/images/2019-04-19/router-5483dbbd-e127-4905-a5cf-6d0e6db5d79d.png" alt=""></p><p><img src="/images/2019-04-19/router2-52da6dd4-60a7-449f-ba93-73e99f0f04ac.png" alt=""></p><p>对于需要url rewrite的场景怎么办?那么就直接将nginx也作为一个服务,再做一层转发。流程变成 router → nginx pod → 具体提供服务的pod。</p><h2 id="链路跟踪"><a href="#链路跟踪" class="headerlink" title="链路跟踪"></a>链路跟踪</h2><p>开源的全链路跟踪很多,比如spring cloud sleuth + zipkin,国内有美团的CAT等等。其目的就是当一个请求经过多个服务时,可以通过一个固定值获取整条请求链路的行为日志,基于此可以再进行耗时分析等,衍生出一些性能诊断的功能。不过对于我们而言,首要目的就是trouble shooting,出了问题需要快速定位异常出现在什么服务,整个请求的链路是怎样的。</p><p>为了让解决方案轻量,我们在日志中打印RequestId以及TraceId来标记链路。RequestId在gateway生成表示唯一一次请求,TraceId相当于二级路径,一开始与RequestId一样,但进入线程池或者消息队列后,TraceId会增加标记来标识唯一条路径。举个例子,当一次请求会向MQ发送一个消息,那么这个消息可能会被多个消费者消费,此时每个消费线程都会自己生成一个TraceId来标记消费链路。加入TraceId的目的就是为了避免只用RequestId过滤出太多日志。</p><p>实现上,通过ThreadLocal存放APIRequestContext串联单服务内的所有调用,当跨服务调用时,将APIRequestContext信息转化为Http Header,被调用方获取到Http Header后再次构建APIRequestContext放入ThreadLocal,重复循环保证RequestId和TraceId不丢失即可。如果进入MQ,那么APIRequestContext信息转化为Message Header即可(基于Rabbitmq实现)。</p><p>当日志汇总到日志系统后,如果出现问题,只需要捕获发生异常的RequestId或是TraceId即可进行问题定位。</p><p><img src="/images/2019-04-19/Untitled-eedbc8b1-14e7-4a64-b29c-8703fb84ff88.png" alt=""></p><p>经过一年来的使用,基本可以满足绝大多数trouble shooting的场景,一般半小时内即可定位到具体业务。</p><h2 id="容器监控"><a href="#容器监控" class="headerlink" title="容器监控"></a>容器监控</h2><p>容器化前监控用的是telegraf探针,容器化后用的是prometheus,直接安装了openshift自带的cluster-monitoring-operator。自带的监控项目已经比较全面,包括node,pod资源的监控,在新增node后也会自动添加进来。</p><p>Java项目也添加了prometheus的监控端点,只是可惜cluster-monitoring-operator提供的配置是只读的,后期将研究怎么将java的jvm监控这些整合进来。</p><p><img src="/images/2019-04-19/grafana1-d2675f57-eeca-4669-a764-70cb3af9fb68.png" alt=""></p><p><img src="/images/2019-04-19/-5f7ae443-5d3e-48bb-9c67-cc60e717d33cuntitled.jpg" alt=""></p><h1 id="更多的"><a href="#更多的" class="headerlink" title="更多的"></a>更多的</h1><p>开源软件是对中小团队的一种福音,无论是Spring Cloud还是k8s都大大降低了团队在基础设施建设上的时间成本。当然其中有更多的话题,比如服务升降级,限流熔断,分布式任务调度,灰度发布,功能开关等等都需要更多时间来探讨。对于小团队,要根据自身情况选择微服务的技术方案,不可一味追新,适合自己的才是最好的。</p><blockquote><p>本文是2019年3月19日dockone.io的分享,文稿内容由我,leon和arthur共同完成。dockone也进行了整理并发表于公众号。</p></blockquote>]]></content>
<summary type="html">
<p>我们的产品是一个客户数据平台。产品的一个重要部分类似企业版的”捷径”,让运营人员可以像搭乐高积木一样创建企业的自动化流程,无需编程即可让数据流动起来。从这一点上,我们的业务特点就是聚少成多,把一个个服务连接起来就成了数据的海洋。理念上跟微服务一致,一个个独立的小服务最终实现大功能。当然我们一开始也没有使用微服务,当业务还未成型就开始考虑架构,那么就是”过度设计”。另一方面需要考虑的因素就是”人”,有没有经历过微服务项目的人,团队是否有devops文化等等,综合考量是否需要微服务化。</p>
</summary>
<category term="spring-cloud" scheme="http://www.deanwangpro.com/tags/spring-cloud/"/>
<category term="ci" scheme="http://www.deanwangpro.com/tags/ci/"/>
<category term="microservice" scheme="http://www.deanwangpro.com/tags/microservice/"/>
</entry>
<entry>
<title>坑系列之阿里SLB上获取客户IP</title>
<link href="http://www.deanwangpro.com/2019/04/16/slb-real-ip/"/>
<id>http://www.deanwangpro.com/2019/04/16/slb-real-ip/</id>
<published>2019-04-15T16:00:00.000Z</published>
<updated>2019-04-19T12:55:03.000Z</updated>
<content type="html"><![CDATA[<p>好久没更新了,正好上周遇到一个获取不到客户端IP的BUG,开发环境用nginx做反代都是work的。上到生产环境就获取不到。思来想去就是生产上多了一个SLB负载均衡。但这是一个老的功能,之前也都是好的,突然就拿的不对了,非常之诡异。</p><a id="more"></a><h1 id="故障重现"><a href="#故障重现" class="headerlink" title="故障重现"></a>故障重现</h1><p>为了确认不是代码的问题,我们使用tcpdump在服务结点上抓包。</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></pre></td><td class="code"><pre><span class="line">GET /api/foo/bar HTTP/1.1</span><br><span class="line">remoteip: 122.xx.xx.xx</span><br><span class="line">x-forwarded-for: 122.xx.xx.xx, 10.130.0.1</span><br><span class="line">accept: application/json, text/plain, */*</span><br><span class="line">dnt: 1</span><br><span class="line">user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36</span><br><span class="line">referer: https://app.example.com/login</span><br><span class="line">accept-language: zh-CN,zh;q=0.9</span><br><span class="line">x-forwarded-host: app.example.com,app.example.com</span><br><span class="line">x-forwarded-port: 80,80</span><br><span class="line">x-forwarded-proto: http,http</span><br><span class="line">x-request-id: d74323d45afd4609977eb233d59f9a9e</span><br><span class="line">x-trace-id: d74323d45afd4609977eb233d59f9a9e</span><br><span class="line">x-real-ip: 10.130.0.1</span><br><span class="line">x-locale: zh_CN</span><br><span class="line">host: app.example.com</span><br><span class="line">Accept-Encoding: gzip</span><br><span class="line">Content-Length: 0</span><br><span class="line">Connection: Keep-Alive</span><br></pre></td></tr></table></figure><p>发现报文头的X-Real-IP是一个VPC的内网地址,说明在我们的nginx中获取的<code>$remote_addr</code>就是<code>10.130.0.1</code>,是SLB的地址。</p><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><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">http {</span><br><span class="line">server {</span><br><span class="line"> listen 80;</span><br><span class="line"> server_name 192.168.50.88;</span><br><span class="line"> root /usr/local/var/www/html;</span><br><span class="line"></span><br><span class="line"> location /api {</span><br><span class="line"> proxy_set_header X-Real-IP $remote_addr; #将remode_addr写入Http Header</span><br><span class="line"> proxy_pass http://backend_hosts;</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">upstream backend_hosts {</span><br><span class="line"> server 127.0.0.1:8080;</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>配置很简单,没有使用realip模块,直接将<code>remote_addr</code>认定为客户端ip。大家知道<code>remote_addr</code>不是http头,不容易伪造。它是服务端与客户端建立socket连接时,从客户端直接获取的。但是为什么这里获取的ip却是SLB自身的IP呢?</p><h1 id="故障分析"><a href="#故障分析" class="headerlink" title="故障分析"></a>故障分析</h1><p>再仔细分析抓包内容,发现其实报文中是包含客户端原始IP的,分别在<code>x-forwarded-for</code>和<code>remoteip</code>上。这里<code>x-forwarded-for</code>的值引起了我们的注意,如果是用nginx原始的<code>$proxy_add_x_forwarded_for</code>参数,客户端IP应该会放在最后,但是这里在第一位,说明SLB对这个头做过处理。<br>找到devops询问是否更改过SLB的配置,发现确实做过调整。为了直接在SLB实现http到https的重定向,将原本的4层负载均衡(tcp)换成了7层负载均衡(http)。试着将SLB恢复原有配置,可以获取客户端IP。最终问题定位到SLB的配置上。<br>再次阅读<a href="https://www.alibabacloud.com/help/zh/doc-detail/54007.htm" target="_blank" rel="noopener">SLB手册</a>,发现以下描述:</p><blockquote><p>负载均衡提供获取客户端真实IP地址的功能,该功能默认是开启的。<br>四层负载均衡(TCP协议)服务可以直接在后端ECS上获取客户端的真实IP地址,无需进行额外的配置。<br>七层负载均衡(HTTP/HTTPS协议)服务需要对应用服务器进行配置,然后使用X-Forwarded-For的方式获取客户端的真实IP地址。<br>真实的客户端IP会被负载均衡放在HTTP头部的X-Forwarded-For字段,格式如下:<br>X-Forwarded-For: 用户真实IP, 代理服务器1-IP, 代理服务器2-IP,…<br>当使用此方式获取客户端真实IP时,获取的第一个地址就是客户端真实IP。</p></blockquote><p>查看SLB配置页面确实也如文档所说</p><p><img src="/images/2019-04-16/SLB%E5%AE%A2%E6%88%B7IP%E8%AE%BE%E7%BD%AE.jpg" alt="SLB客户IP设置"></p><p>至于<code>remote_addr</code>获取到SLB的IP也就很容易理解了,当没有上级代理没有透传tcp连接时,<code>remote_addr</code>获取的就是上一层代理的ip地址。</p><h1 id="故障恢复"><a href="#故障恢复" class="headerlink" title="故障恢复"></a>故障恢复</h1><p>既然定位到问题了,那么需要着手解决,改回4层LB是不现实的。<br>阿里云其实提供了两个方案:</p><ol><li>按照文档上说的,获取X-Forwarded-For的第一段IP即为客户真实IP</li><li>通过抓包发现SLB会添加一个remoteip的头,直接使用就行</li></ol><p>我们偷个懒,直接用第二种,在nginx将remoteip塞到X-Real-IP上,这样不用打hotfix即可修复问题。</p><h1 id="花絮"><a href="#花絮" class="headerlink" title="花絮"></a>花絮</h1><p>其实在故障恢复的过程中,本想在本地复现的。过程就是用nginx搭建一个4层负载代理到7层负载最终到服务。如下图所示</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">| Client +------> TCP LB +----->+ HTTP LB +----> SERVER |</span><br><span class="line">| | | | | | | |</span><br><span class="line">| | | | | | | |</span><br><span class="line">+------------------+ +----------------+ +---------------+ +----------------+</span><br></pre></td></tr></table></figure><p>tcp负载的配置如下:</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">stream {</span><br><span class="line"> upstream tcp_proxy {</span><br><span class="line"> server 127.0.0.1:80;</span><br><span class="line"> }</span><br><span class="line"> server {</span><br><span class="line"> listen 88;</span><br><span class="line"> proxy_connect_timeout 1s;</span><br><span class="line"> proxy_timeout 300s;</span><br><span class="line"> proxy_pass tcp_proxy;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>最终发现nginx的tcp代理有个巨大的坑,就是无法透传<code>remote_addr</code>,如果tcp代理跳过http直连服务,获取到的remote_addr就是127.0.0.1这个本机地址。<br>翻了翻文档,发现还真有<a href="https://www.nginx.com/blog/ip-transparency-direct-server-return-nginx-plus-transparent-proxy/" target="_blank" rel="noopener">官方说明</a></p><p>简而言之,就是要买nginx-plus,里面有个<code>proxy_bind $remote_addr transparent;</code>可以实现透传功能,满满的套路。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>DevOps有的时候真的会影响到业务,不同环境不同配置造成难以预料的影响。虽然我们的服务都已经实现了容器化。但是对于这些PaaS组件如何统一配置并且将配置代码化,让多个环境(包括开发,测试,staging)保持一致还是挺值得研究的话题。</p>]]></content>
<summary type="html">
<p>好久没更新了,正好上周遇到一个获取不到客户端IP的BUG,开发环境用nginx做反代都是work的。上到生产环境就获取不到。思来想去就是生产上多了一个SLB负载均衡。但这是一个老的功能,之前也都是好的,突然就拿的不对了,非常之诡异。</p>
</summary>
<category term="DevOps" scheme="http://www.deanwangpro.com/tags/DevOps/"/>
</entry>
<entry>
<title>小团队的微服务之路</title>
<link href="http://www.deanwangpro.com/2019/02/18/road-of-microservice/"/>
<id>http://www.deanwangpro.com/2019/02/18/road-of-microservice/</id>
<published>2019-02-17T16:00:00.000Z</published>
<updated>2019-02-18T08:17:36.000Z</updated>
<content type="html"><![CDATA[<p>微服务是否适合小团队是个见仁见智的问题。回归现象看本质,随着业务复杂度的提高,单体应用越来越庞大,就好像一个类的代码行越来越多,分而治之,切成多个类应该是更好的解决方法,所以一个庞大的单体应用分出多个小应用也更符合这种分治的思想。当然微服务架构不应该是一个小团队一开始就该考虑的问题,而是慢慢演化的结果,谨慎过度设计尤为重要。</p><p>公司的背景是提供SaaS服务,对于大客户也会有定制开发以及私有化部署。经过2年不到的时间,技术架构经历了从单体到微服务再到容器化的过程。</p><a id="more"></a><h1 id="单体应用时代"><a href="#单体应用时代" class="headerlink" title="单体应用时代"></a>单体应用时代</h1><p>早期开发只有两个人,考虑微服务之类的都是多余。不过由于受前公司影响,最初就决定了前后端分离的路线,因为不需要考虑SEO的问题,索性就做成了SPA单页应用。多说一句,前后端分离也不一定就不能服务端渲染,例如电商系统或者一些匿名即可访问的系统,加一层薄薄的View层,无论是php还是用Thymeleaf都是不错的选择。</p><p>部署架构上,我们使用Nginx代理前端HTML资源,在接收请求时根据路径反向代理到server的8080端口实现业务。</p><p><img src="/images/2018-02-18/arch_mono.png" alt=""></p><h2 id="接口定义"><a href="#接口定义" class="headerlink" title="接口定义"></a>接口定义</h2><p>接口按照标准的Restful来定义,</p><ul><li>版本,统一跟在 /api/后面,例如 <code>/api/v2</code></li><li>以资源为中心,使用复数表述,例如<code>/api/contacts</code>,也可以嵌套,如<code>/api/groups/1/contacts/100</code></li><li>url中尽量不使用动词,实践中发现做到这一点真的比较难,每个研发人员的思路不一致,起的名字也千奇百怪,都需要在代码Review中覆盖。</li><li>动作支持,<code>POST / PUT / DELELE / GET</code> ,这里有一个坑,PUT和PATCH都是更新,但是PUT是全量更新而PATCH是部分更新,前者如果传入的字段是空(未传也视为空)那么也会被更新到数据库中。目前我们虽然是使用PUT但是忽略空字段和未传字段,本质上是一种部分更新,这也带来了一些问题,比如确有置空的业务需要特殊处理。</li><li>接口通过swagger生成文档供前端同事使用。</li></ul><h2 id="持续集成-CI"><a href="#持续集成-CI" class="headerlink" title="持续集成(CI)"></a>持续集成(CI)</h2><p>团队初始成员之前都有在大团队共事的经历,所以对于质量管控和流程管理都有一些共同的要求。因此在开发之初就引入了集成测试的体系,可以直接开发针对接口的测试用例,统一执行并计算覆盖率。</p><p>一般来说代码自动执行的都是单元测试(Unit Test),我们之所以叫集成测试是因为测试用例是针对API的,并且包含了数据库的读写,MQ的操作等等,除了外部服务的依赖基本都是符合真实生产场景,相当于把Jmeter的事情直接在Java层面做掉了。这在开发初期为我们提供了非常大的便利性。但值得注意的是,由于数据库以及其他资源的引入,数据准备以及数据清理时要考虑的问题就会更多,例如如何控制并行任务之间的测试数据互不影响等等。</p><p>为了让这一套流程可以自动化的运作起来, 引入Jenkins也是理所当然的事情了。</p><p><img src="/images/2018-02-18/ci_mono.png" alt=""></p><p>开发人员提交代码进入gerrit中,Jenkins被触发开始编译代码并执行集成测试,完成后生成测试报告,测试通过再由reviewer进行代码review。在单体应用时代这样的CI架构已经足够好用,由于有集成测试的覆盖,在保持API兼容性的前提下进行代码重构都会变得更有信心。</p><h1 id="微服务时代"><a href="#微服务时代" class="headerlink" title="微服务时代"></a>微服务时代</h1><h2 id="服务拆分原则"><a href="#服务拆分原则" class="headerlink" title="服务拆分原则"></a>服务拆分原则</h2><p>从数据层面看,最简单的方式就是看数据库的表之间是否有比较少的关联。例如最容易分离的一般来说都是用户管理模块。如果从领域驱动设计(DDD)看,其实一个服务就是一个或几个相关联的领域模型,通过少量数据冗余划清服务边界。单个服务内通过领域服务完成多个领域对象协作。当然DDD比较复杂,要求领域对象设计上是充血模型而非贫血模型。从实践角度讲,充血模型对于大部分开发人员来说难度非常高,什么代码应该属于行为,什么属于领域服务,很多时候非常考验人员水平。</p><p>服务拆分是一个大工程,往往需要几个对业务以及数据最熟悉的人一起讨论,甚至要考虑到团队结构,最终的效果是服务边界清晰, 没有环形依赖和避免双向依赖。</p><h2 id="框架选择"><a href="#框架选择" class="headerlink" title="框架选择"></a>框架选择</h2><p>由于之前的单体服务使用的是spring boot,所以框架自然而的选择了spring cloud。其实个人认为微服务框架不应该限制技术与语言,但生产实践中发现无论dubbo还是spring cloud都具有侵入性,我们在将nodejs应用融入spring cloud体系时就发现了许多问题。也许未来的service mesh才是更合理的发展道路。</p><p><img src="/images/2018-02-18/arch_spring_cloud.png" alt=""></p><p>这是典型的Spring Cloud的使用方法,该图取自<a href="https://mp.weixin.qq.com/s/vnWXpH5pv-FAzLZfbgTGvg" target="_blank" rel="noopener">纯洁的微笑公众号</a></p><ul><li>zuul作为gateway,分发不同客户端的请求到具体service</li><li>erueka作为注册中心,完成了服务发现和服务注册</li><li>每个service包括gateway都自带了Hystrix提供的限流和熔断功能</li><li>service之间通过feign和ribbon互相调用,feign实际上是屏蔽了service对erueka的操作</li></ul><p>上文说的一旦要融入异构语言的service,那么服务注册,服务发现,服务调用,熔断和限流都需要自己处理。再有关于zuul要多说几句,Sprin Cloud提供的zuul对Netflix版本的做了裁剪,去掉了动态路由功能(Groovy实现),另外一点就是zuul的性能一般,由于采用同步编程模型,对于IO密集型等后台处理时间长的链路非常容易将servlet的线程池占满,所以如果将zuul与主要service放置在同一台物理机上,在流量大的情况下,zuul的资源消耗非常大。实际测试也发现经过zuul与直接调用service的性能损失在30%左右,并发压力大时更为明显。现在spring cloud gateway是pivotal的主推了,支持异步编程模型,后续架构优化也许会采用,或是直接使用Kong这种基于nginx的网关来提供性能。当然同步模型也有优点,编码更简单,后文将会提到使用ThreadLocal如何建立链路跟踪。</p><h2 id="架构改造"><a href="#架构改造" class="headerlink" title="架构改造"></a>架构改造</h2><p>经过大半年的改造以及新需求的加入,单体服务被不断拆分,最终形成了10余个微服务,并且搭建了Spark用于BI。初步形成两大体系,微服务架构的在线业务系统(OLTP) + Spark大数据分析系统(OLAP)。数据源从只有Mysql增加到了ES和Hive。多数据源之间的数据同步也是值得一说的话题,但内容太多不在此文赘述。</p><p><img src="/images/2018-02-18/arch_microservice.png" alt=""></p><p>服务拆分我们采用直接割接的方式,数据表也是整体迁移。因为几次大改造的升级申请了停服,所以步骤相对简单。如果需要不停服升级,那么应该采用先双写再逐步切换的方式保证业务不受影响。</p><h2 id="自动化部署"><a href="#自动化部署" class="headerlink" title="自动化部署"></a>自动化部署</h2><p>与CI比起来,持续交付(CD)实现更为复杂,在资源不足的情况我们尚未实现CD,只是实现执行了自动化部署。</p><p>由于生产环境需要通过跳板机操作,所以我们通过Jenkins生成jar包传输到跳板机,之后再通过Ansible部署到集群。</p><p><img src="/images/2018-02-18/ci_microservice.png" alt=""></p><p>简单粗暴的部署方式在小规模团队开发时还是够用的,只是需要在部署前保证测试(人工测试 + 自动化测试)到位。</p><h2 id="链路跟踪"><a href="#链路跟踪" class="headerlink" title="链路跟踪"></a>链路跟踪</h2><p>开源的全链路跟踪很多,比如spring cloud sleuth + zipkin,国内有美团的CAT等等。其目的就是当一个请求经过多个服务时,可以通过一个固定值获取整条请求链路的行为日志,基于此可以再进行耗时分析等,衍生出一些性能诊断的功能。不过对于我们而言,首要目的就是trouble shooting,出了问题需要快速定位异常出现在什么服务,整个请求的链路是怎样的。</p><p>为了让解决方案轻量,我们在日志中打印RequestId以及TraceId来标记链路。RequestId在gateway生成表示唯一一次请求,TraceId相当于二级路径,一开始与RequestId一样,但进入线程池或者消息队列后,TraceId会增加标记来标识唯一条路径。举个例子,当一次请求会向MQ发送一个消息,那么这个消息可能会被多个消费者消费,此时每个消费线程都会自己生成一个TraceId来标记消费链路。加入TraceId的目的就是为了避免只用RequestId过滤出太多日志。实现如图所示,</p><p><img src="/images/2018-02-18/seq_trace.png" alt=""></p><p>简单的说,通过ThreadLocal存放APIRequestContext串联单服务内的所有调用,当跨服务调用时,将APIRequestContext信息转化为Http Header,被调用方获取到Http Header后再次构建APIRequestContext放入ThreadLocal,重复循环保证RequestId和TraceId不丢失即可。如果进入MQ,那么APIRequestContext信息转化为Message Header即可(基于Rabbitmq实现)。</p><p>当日志汇总到日志系统后,如果出现问题,只需要捕获发生异常的RequestId或是TraceId即可进行问题定位</p><p><img src="/images/2018-02-18/graylog_trace.png" alt=""></p><p>经过一年来的使用,基本可以满足绝大多数trouble shooting的场景,一般半小时内即可定位到具体业务。</p><h2 id="运维监控"><a href="#运维监控" class="headerlink" title="运维监控"></a>运维监控</h2><p>在容器化之前,采用telegraf + influxdb + grafana的方案。telegraf作为探针收集jvm,system,mysql等资源的信息,写入influxdb,最终通过grafana做数据可视化。spring boot actuator可以配合jolokia暴露jvm的endpoint。整个方案零编码,只需要花时间配置。</p><h1 id="容器化时代"><a href="#容器化时代" class="headerlink" title="容器化时代"></a>容器化时代</h1><h2 id="架构改造-1"><a href="#架构改造-1" class="headerlink" title="架构改造"></a>架构改造</h2><p>因为在做微服务之初就计划了容器化,所以架构并未大动,只是每个服务都会建立一个Dockerfile用于创建docker image</p><p><img src="/images/2018-02-18/arch_docker.png" alt=""></p><p>涉及变化的部分包括:</p><ol><li>CI中多了构建docker image的步骤</li><li>自动化测试过程中将数据库升级从应用中剥离单独做成docker image</li><li>生产中用k8s自带的service替代了eruka</li></ol><p>理由下文一一道来。</p><h2 id="Spring-Cloud与k8s的融合"><a href="#Spring-Cloud与k8s的融合" class="headerlink" title="Spring Cloud与k8s的融合"></a>Spring Cloud与k8s的融合</h2><p>我们使用的是Redhat的Openshift,可以认为是k8s企业版,其本身就有service的概念。一个service下有多个pod,pod内即是一个可服务单元。service之间互相调用时k8s会提供默认的负载均衡控制,发起调用方只需要写被调用方的serviceId即可。这一点和spring cloud fegin使用ribbon提供的功能如出一辙。也就是说服务治理可以通过k8s来解决,那么为什么要替换呢?其实上文提到了,Spring Cloud技术栈对于异构语言的支持问题,我们有许多BFF(Backend for Frontend)是使用nodejs实现的,这些服务要想融合到Spring Cloud中,服务注册,负载均衡,心跳检查等等都要自己实现。如果以后还有其他语言架构的服务加入进来,这些轮子又要重造。基于此类原因综合考量后,决定采用Openshift所提供的网络能力替换eruka。</p><p>由于本地开发和联调过程中依然依赖eruka,所以只在生产上通过配置参数来控制,</p><ul><li><code>eureka.client.enabled</code> 设置为 <code>false</code>,停止各服务的eureka注册</li><li><code>ribbon.eureka.enabled</code> 设置为 <code>false</code>,让ribbon不从eureka获取服务列表</li><li>以服务foo为例,<code>foo.ribbon.listofservers</code> 设置为 <a href="http://foo:8080" target="_blank" rel="noopener"><code>http://foo:8080</code></a>,那么当一个服务需要使用服务foo的时候,就会直接调用到<a href="http://foo:8080" target="_blank" rel="noopener"><code>http://foo:8080</code></a></li></ul><h2 id="CI的改造"><a href="#CI的改造" class="headerlink" title="CI的改造"></a>CI的改造</h2><p>CI的改造主要是多了一部编译docker image并打包到Harbor的过程,部署时会直接从Harbor拉取镜像。另一个就是数据库的升级工具。之前我们使用flyway作为数据库升级工具,当应用启动时自动执行SQL脚本。随着服务实例越来越多,一个服务的多个实例同时升级的情况也时有发生,虽然flyway是通过数据库锁实现了升级过程不会有并发,但会导致被锁服务启动时间变长的问题。从实际升级过程来看,将可能发生的并发升级变为单一进程可能更靠谱。此外后期分库分表的架构也会使随应用启动自动升级数据库变的困难。综合考量,我们将升级任务做了拆分,每个服务都有自己的升级项目并会做容器化。在使用时,作为run once的工具来使用,即<code>docker run -rm</code>的方式。并且后续也支持了设定目标版本的功能,在私有化项目的跨版本升级中起到了非常好的效果。</p><p>至于自动部署,由于服务之间存在上下游关系,例如config,eruka等属于基本服务被其他服务依赖,部署也产生了先后顺序。基于Jenkins做pipeline可以很好的解决这个问题。</p><h1 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h1><p>其实以上的每一点都可以深入的写成一篇文章,微服务的架构演进涉及到开发,测试和运维,要求团队内多工种紧密合作。分治是软件行业解决大系统的不二法门,作为小团队我们并没有盲目追新,而是在发展的过程通过服务化的方式解决问题。从另一方面我们也体会到了微服务对于人的要求,以及对于团队的挑战都比过去要高要大。未来仍需探索,演进仍在路上。</p>]]></content>
<summary type="html">
<p>微服务是否适合小团队是个见仁见智的问题。回归现象看本质,随着业务复杂度的提高,单体应用越来越庞大,就好像一个类的代码行越来越多,分而治之,切成多个类应该是更好的解决方法,所以一个庞大的单体应用分出多个小应用也更符合这种分治的思想。当然微服务架构不应该是一个小团队一开始就该考虑的问题,而是慢慢演化的结果,谨慎过度设计尤为重要。</p>
<p>公司的背景是提供SaaS服务,对于大客户也会有定制开发以及私有化部署。经过2年不到的时间,技术架构经历了从单体到微服务再到容器化的过程。</p>
</summary>
<category term="spring-cloud" scheme="http://www.deanwangpro.com/tags/spring-cloud/"/>
<category term="ci" scheme="http://www.deanwangpro.com/tags/ci/"/>
<category term="microservice" scheme="http://www.deanwangpro.com/tags/microservice/"/>
</entry>
<entry>
<title>Spring Boot 2.1.2 & Spring Cloud Greenwich 升级记录</title>
<link href="http://www.deanwangpro.com/2019/02/02/spring-boot-cloud-geenwich-upgrade/"/>
<id>http://www.deanwangpro.com/2019/02/02/spring-boot-cloud-geenwich-upgrade/</id>
<published>2019-02-01T16:00:00.000Z</published>
<updated>2019-03-20T04:15:15.000Z</updated>
<content type="html"><![CDATA[<p>节前没有新业务代码,正好Greenwich刚发布,于是开始为期四天的框架代码升级。</p><p>之前的版本是 spring boot 1.5.10 , spring cloud Edgware.SR3</p><a id="more"></a><h2 id="依赖升级"><a href="#依赖升级" class="headerlink" title="依赖升级"></a>依赖升级</h2><ul><li>增加依赖管理插件 <code>apply plugin: 'io.spring.dependency-management'</code></li><li>spring-cloud-starter-eureka → spring-cloud-starter-netflix-eureka-client</li><li>spring-cloud-starter-feign → spring-cloud-starter-openfeign</li><li>gradle版本要求4.4</li></ul><h2 id="boot-spring-boot-starter-data-jpa"><a href="#boot-spring-boot-starter-data-jpa" class="headerlink" title="boot : spring-boot-starter-data-jpa"></a>boot : spring-boot-starter-data-jpa</h2><ul><li><p>delete → deleteById</p></li><li><p>findone → findById</p><p> 这个改动确实大,返回值变成了Optional,合理是合理的,只是改的真多。。</p></li></ul><h2 id="boot-spring-boot-starter-data-redis"><a href="#boot-spring-boot-starter-data-redis" class="headerlink" title="boot : spring-boot-starter-data-redis"></a>boot : spring-boot-starter-data-redis</h2><p>Jedis → Lettuce</p><p>还好并没有使用它的autoconfiguration,配置上有一个小坑,Jedis的redis.timeout是表示connection timeout, 而Lettuce是表示command timeout,之前配置成0的,如果set到Lettuce的commandtimeout里面那就要抛异常了。</p><h2 id="配置"><a href="#配置" class="headerlink" title="配置:"></a>配置<strong>:</strong></h2><p>可以在build.gradle中加入,启动时会检查配置是否兼容</p><pre><code>compile "org.springframework.boot:spring-boot-properties-migrator" </code></pre><p><strong>注意:完成迁移后需要删除</strong></p><p><img src="/images/springboot-prop-migrator.png" alt="migrator"></p><p>警告如上图会告知最新的配置格式</p><h2 id="boot-spring-boot-starter-actuator"><a href="#boot-spring-boot-starter-actuator" class="headerlink" title="boot: spring-boot-starter-actuator"></a>boot: spring-boot-starter-actuator</h2><p>endpoint的暴露方式变化,<code>management.endpoints.web.exposure.include = "*"</code> 表示暴露所有endpoints,如果配置了security那么也需要在security的配置中开放访问<code>/actuator</code>路径</p><h2 id="boot-spring-boot-starter-security"><a href="#boot-spring-boot-starter-security" class="headerlink" title="boot: spring-boot-starter-security"></a>boot: spring-boot-starter-security</h2><p>自动注入的<code>AuthenticationManager</code>可能会找不到</p><p>If you want to expose Spring Security’s <code>AuthenticationManager</code> as a bean, override the <code>authenticationManagerBean</code> method on your <code>WebSecurityConfigurerAdapter</code> and annotate it with <code>@Bean</code>.</p><h2 id="cloud-eureka"><a href="#cloud-eureka" class="headerlink" title="cloud : eureka"></a>cloud : eureka</h2><p>各个项目在注册中心里面的客户端实例IP显示不正确,需要修改每个项目的</p><p><strong>bootstarp.yml</strong> </p><ul><li>${spring.cloud.client.ipAddress} → ${spring.cloud.client.ip-address}</li></ul><h2 id="boot-spring-boot-starter-test"><a href="#boot-spring-boot-starter-test" class="headerlink" title="boot: spring-boot-starter-test:"></a>boot: spring-boot-starter-test<strong>:</strong></h2><ul><li>org.mockito.Matchers → org.mockito.ArgumentMatchers 注意build时的warning</li><li>Mock方法时请使用Mocikto.doReturn(…).when(…),不使用when(…).thenReturn(…),否则<code>@spybean</code>的会调用实际方法</li></ul><h2 id="其他问题"><a href="#其他问题" class="headerlink" title="其他问题"></a>其他问题</h2><ol><li><p>版本升级后会有deprecated的类或方法,所以要注意看console中build的warning信息</p></li><li><p>由于spring cloud依赖管理插件强制cuator升级到4.0.1,导致我们使用的elestic-job不能正常工作,只能强行控制版本。</p><pre><code>dependencyManagement { imports { mavenBom "org.springframework.cloud:spring-cloud-dependencies:${SPRING_CLOUD_VERSION}" } dependencies { dependency 'org.apache.curator:curator-framework:2.10.0' dependency 'org.apache.curator:curator-recipes:2.10.0' dependency 'org.apache.curator:curator-client:2.10.0' }}</code></pre></li><li><p>如果启用出现error,报bean重复,首先确认是不是故意覆盖,如重写spring-boot自带的bean,如是,可以在bootstrap.yml加入</p><pre><code>spring.main.allow-bean-definition-overriding=true</code></pre></li><li><p>FeignClient注解增加了contextId属性</p><pre><code>@FeignClient(value = "foo", contextId = "fooFeign")</code></pre><p> 此contextId即表示bean id,所有注入使用时需要</p><pre><code>@AutowriedFooFeign fooFeign</code></pre><p> 如果不写contextId,当多个class都是@FeignClient(“foo”),即会认为是同一个bean而排除上一条所说的warning</p></li></ol>]]></content>
<summary type="html">
<p>节前没有新业务代码,正好Greenwich刚发布,于是开始为期四天的框架代码升级。</p>
<p>之前的版本是 spring boot 1.5.10 , spring cloud Edgware.SR3</p>
</summary>
<category term="spring-boot" scheme="http://www.deanwangpro.com/tags/spring-boot/"/>
<category term="spring-cloud" scheme="http://www.deanwangpro.com/tags/spring-cloud/"/>
</entry>
<entry>
<title>Rabbitmq的性能测试</title>
<link href="http://www.deanwangpro.com/2018/10/04/rabbitmq-performance/"/>
<id>http://www.deanwangpro.com/2018/10/04/rabbitmq-performance/</id>
<published>2018-10-03T16:00:00.000Z</published>
<updated>2018-10-05T03:30:14.000Z</updated>
<content type="html"><![CDATA[<p>在做系统的整体性能测试时发现经常会卡在一个较低的QPS(单机低于100)数值,而且应用服务器的负载不高,检查MQ消费速率只有40左右。接着把目标放在消息发送端上,发现消息发送速率很低,大约40条/s。</p><p>果断搭建一个最小化工程单测Rabbitmq发送性能,发现在启用发送端事务后性能下降非常明显。</p><table><thead><tr><th>消息数量</th><th>开启事务</th><th>未开启事务</th></tr></thead><tbody><tr><td>10w</td><td>320796ms</td><td>10246ms</td></tr></tbody></table><p>本机SSD硬盘测试结果10w条消息未开启事务,大约10s发送完毕;而开启了事务后,需要将近320s,差了30多倍。</p><p>接着翻阅Rabbitmq官网,发现开启事务性能最大损失超过250倍。</p><blockquote><p>Using standard AMQP 0-9-1, the only way to guarantee that a message isn’t lost is by using transactions – make the channel transactional then for each message or set of messages publish, commit. In this case, transactions are unnecessarily heavyweight and decrease throughput by a factor of 250. To remedy this, a confirmation mechanism was introduced. It mimics the consumer acknowledgements mechanism already present in the protocol.</p></blockquote><a id="more"></a><h3 id="事务为什么会慢"><a href="#事务为什么会慢" class="headerlink" title="事务为什么会慢"></a>事务为什么会慢</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">rabbitTemplate.setChannelTransacted(<span class="keyword">true</span>);</span><br></pre></td></tr></table></figure><p>该标志位开启后表示Rabbitmq的发送统一被spring事务管理。当一段代码被<code>@Transactional</code>包裹,那么只有当事务结束后,消息才会正在的发送到Rabbitmq的exchange中。具体代码详见<code>rabbitTemplate.java</code>中的<code>doSend()</code>。</p><p>事务机制是Rabbitmq自身支持的,原理是<code>channel.txSelect()</code>开启事务,<code>channel.txRollback()</code>回滚事务。<code>channel.txCommit()</code>提交事务。当事务开启后,通过抓包会发现网络交互增多。</p><p><img src="/images/2018-10-04/rabbitmq_tx.jpg" alt="rabbitmq_tx"></p><h3 id="是否可以去掉事务呢?"><a href="#是否可以去掉事务呢?" class="headerlink" title="是否可以去掉事务呢?"></a>是否可以去掉事务呢?</h3><p>实践证明,<strong>不行</strong>。</p><p>因为某些消息,特别是实体的新增或者更新消息发出后,消费者有可能会通过API反查,这时如果生产者本地事务未提交。消费者就有可能消费到空数据或者旧数据。所以生产者必须将发送消息的事务包裹在本地数据库事务当中。</p><p>在过去的实践中,有一种解法可以在不开启事务的情况下解决这个问题,就是利用本地消息表,即生产者调用后不发送,而是将消息写入到本地消息表,当事务失败那么此次写入操作也会回滚。真正发送消息到MQ就开启另一个定时线程轮询该本地消息表异步发送消息。这种方法理论上可行,但实际操作非常复杂,当有多个生产者实例时,定时发送线程也会有多个,那么就会遇到各种并发问题。</p><h3 id="最大限度改善性能"><a href="#最大限度改善性能" class="headerlink" title="最大限度改善性能"></a>最大限度改善性能</h3><p>既然无法去除事务,并且也不希望代码异常复杂。那么可以将消息分为两类,一类是changlog即实体的变化,一类是command,即通知消费者可以开始做某事,通常用在同步转异步的场景。对于第一类消息仍然保留事务,对于第二类消息关闭发送事务,采用PublisherConfirm的方式保证消息发送成功。</p><p>再次测试,性能明显提高,但是并未达到预期,通过<code>innotop</code>命令查看MySQL压力,发现只有10K/s上下。检查Rabbitmq所在机器的负载。</p><p><img src="/images/2018-10-04/high_iowa.png" alt="high_iowa"></p><p>iowait非常高,由于该机器上还装有es,同样是io密集型的应用,所以实际性能瓶颈都在磁盘io上了。</p><p>跟Devops确认了机器情况,这台机器恰好是Rabbitmq的磁盘节点。为了快速验证,新增了一块SSD硬盘并将Rabbitmq消息文件都挂载到新加的磁盘上。再次测试,iowait下降明显。</p><p><img src="/images/2018-10-04/low_iowa.png" alt="low_iowa"></p><p>通过<code>innotop</code>命令查看MySQL压力,发现上升了一倍,达到20K/s。基本上把压力都转到了数据库一侧。系统整体性能提升了一个数量级。也许该Rabbitmq节点独占一台机器效果会更好。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>性能优化有时候就像破案,看了jstat没问题,gc没问题,机器负载也不高,就是抓不到“元凶”。需要一点一点的扣,往往一个短板就造成了木桶效应。另外还有一点就是如果硬件能够解决的事情,就不要过度优化软件了,代码复杂度上升往往意味着更多的BUG,在资源有限的情况下多花点钱省点时间还是值得的。</p>]]></content>
<summary type="html">
<p>在做系统的整体性能测试时发现经常会卡在一个较低的QPS(单机低于100)数值,而且应用服务器的负载不高,检查MQ消费速率只有40左右。接着把目标放在消息发送端上,发现消息发送速率很低,大约40条/s。</p>
<p>果断搭建一个最小化工程单测Rabbitmq发送性能,发现在启用发送端事务后性能下降非常明显。</p>
<table>
<thead>
<tr>
<th>消息数量</th>
<th>开启事务</th>
<th>未开启事务</th>
</tr>
</thead>
<tbody><tr>
<td>10w</td>
<td>320796ms</td>
<td>10246ms</td>
</tr>
</tbody></table>
<p>本机SSD硬盘测试结果10w条消息未开启事务,大约10s发送完毕;而开启了事务后,需要将近320s,差了30多倍。</p>
<p>接着翻阅Rabbitmq官网,发现开启事务性能最大损失超过250倍。</p>
<blockquote>
<p>Using standard AMQP 0-9-1, the only way to guarantee that a message isn’t lost is by using transactions – make the channel transactional then for each message or set of messages publish, commit. In this case, transactions are unnecessarily heavyweight and decrease throughput by a factor of 250. To remedy this, a confirmation mechanism was introduced. It mimics the consumer acknowledgements mechanism already present in the protocol.</p>
</blockquote>
</summary>
<category term="performance" scheme="http://www.deanwangpro.com/tags/performance/"/>
<category term="tuning" scheme="http://www.deanwangpro.com/tags/tuning/"/>
<category term="rabbitmq" scheme="http://www.deanwangpro.com/tags/rabbitmq/"/>
</entry>
<entry>
<title>spring-cloud服务网关中的Timeout设置</title>
<link href="http://www.deanwangpro.com/2018/04/13/zuul-hytrix-ribbon-timeout/"/>
<id>http://www.deanwangpro.com/2018/04/13/zuul-hytrix-ribbon-timeout/</id>
<published>2018-04-12T16:00:00.000Z</published>
<updated>2018-04-13T08:59:51.000Z</updated>
<content type="html"><![CDATA[<p>大家在初次使用spring-cloud的gateway的时候,肯定会被里面各种的Timeout搞得晕头转向。hytrix有设置,ribbon也有。我们一开始也是乱设一桶,Github上各种项目里也没几个设置正确的。对Timeout的研究源于一次log中的warning</p><blockquote><p>The Hystrix timeout of 60000 ms for the command “foo” is set lower than the combination of the Ribbon read and connect timeout, 200000ms.</p></blockquote><a id="more"></a><h3 id="hytrix超时时间"><a href="#hytrix超时时间" class="headerlink" title="hytrix超时时间"></a>hytrix超时时间</h3><p>log出自<code>AbstractRibbonCommand.java</code>,那么索性研究一下源码。</p><p>假设:</p><ul><li>这里gateway会请求一个serviceName=foo的服务</li></ul><figure class="highlight java"><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">static</span> <span class="keyword">int</span> <span class="title">getHystrixTimeout</span><span class="params">(IClientConfig config, String commandKey)</span> </span>{</span><br><span class="line"><span class="keyword">int</span> ribbonTimeout = getRibbonTimeout(config, commandKey);</span><br><span class="line">DynamicPropertyFactory dynamicPropertyFactory = DynamicPropertyFactory.getInstance();</span><br><span class="line"></span><br><span class="line"><span class="comment">// 获取默认的hytrix超时时间</span></span><br><span class="line"><span class="keyword">int</span> defaultHystrixTimeout = dynamicPropertyFactory.getIntProperty(<span class="string">"hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds"</span>,</span><br><span class="line"><span class="number">0</span>).get();</span><br><span class="line"><span class="comment">// 获取具体服务的hytrix超时时间,这里应该是hystrix.command.foo.execution.isolation.thread.timeoutInMilliseconds</span></span><br><span class="line"><span class="keyword">int</span> commandHystrixTimeout = dynamicPropertyFactory.getIntProperty(<span class="string">"hystrix.command."</span> + commandKey + <span class="string">".execution.isolation.thread.timeoutInMilliseconds"</span>,</span><br><span class="line"><span class="number">0</span>).get();</span><br><span class="line"><span class="keyword">int</span> hystrixTimeout;</span><br><span class="line"><span class="comment">// hystrixTimeout的优先级是 具体服务的hytrix超时时间 > 默认的hytrix超时时间 > ribbon超时时间</span></span><br><span class="line"><span class="keyword">if</span>(commandHystrixTimeout > <span class="number">0</span>) {</span><br><span class="line">hystrixTimeout = commandHystrixTimeout;</span><br><span class="line">}</span><br><span class="line"><span class="keyword">else</span> <span class="keyword">if</span>(defaultHystrixTimeout > <span class="number">0</span>) {</span><br><span class="line">hystrixTimeout = defaultHystrixTimeout;</span><br><span class="line">} <span class="keyword">else</span> {</span><br><span class="line">hystrixTimeout = ribbonTimeout;</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 如果默认的或者具体服务的hytrix超时时间小于ribbon超时时间就会警告</span></span><br><span class="line"><span class="keyword">if</span>(hystrixTimeout < ribbonTimeout) {</span><br><span class="line">LOGGER.warn(<span class="string">"The Hystrix timeout of "</span> + hystrixTimeout + <span class="string">"ms for the command "</span> + commandKey +</span><br><span class="line"><span class="string">" is set lower than the combination of the Ribbon read and connect timeout, "</span> + ribbonTimeout + <span class="string">"ms."</span>);</span><br><span class="line">}</span><br><span class="line"><span class="keyword">return</span> hystrixTimeout;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line">hystrix:</span><br><span class="line"> command:</span><br><span class="line"> default:</span><br><span class="line"> execution:</span><br><span class="line"> isolation:</span><br><span class="line"> thread:</span><br><span class="line"> timeoutInMilliseconds: 60000</span><br><span class="line"> </span><br><span class="line">ribbon:</span><br><span class="line"> ReadTimeout: 50000</span><br><span class="line"> ConnectTimeout: 50000</span><br><span class="line"> MaxAutoRetries: 0</span><br><span class="line"> MaxAutoRetriesNextServer: 1</span><br></pre></td></tr></table></figure><h3 id="ribbon超时时间"><a href="#ribbon超时时间" class="headerlink" title="ribbon超时时间"></a>ribbon超时时间</h3><p>这里ribbon的超时时间是50000ms,那么为什么log中写的ribbon时间是200000ms?</p><p>继续分析源码:</p><figure class="highlight java"><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">static</span> <span class="keyword">int</span> <span class="title">getRibbonTimeout</span><span class="params">(IClientConfig config, String commandKey)</span> </span>{</span><br><span class="line"><span class="keyword">int</span> ribbonTimeout;</span><br><span class="line"><span class="comment">// 这是比较异常的情况,不说</span></span><br><span class="line"><span class="keyword">if</span> (config == <span class="keyword">null</span>) {</span><br><span class="line">ribbonTimeout = RibbonClientConfiguration.DEFAULT_READ_TIMEOUT + RibbonClientConfiguration.DEFAULT_CONNECT_TIMEOUT;</span><br><span class="line">} <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// 这里获取了四个参数,ReadTimeout,ConnectTimeout,MaxAutoRetries, MaxAutoRetriesNextServer</span></span><br><span class="line"><span class="keyword">int</span> ribbonReadTimeout = getTimeout(config, commandKey, <span class="string">"ReadTimeout"</span>,</span><br><span class="line">IClientConfigKey.Keys.ReadTimeout, RibbonClientConfiguration.DEFAULT_READ_TIMEOUT);</span><br><span class="line"><span class="keyword">int</span> ribbonConnectTimeout = getTimeout(config, commandKey, <span class="string">"ConnectTimeout"</span>,</span><br><span class="line">IClientConfigKey.Keys.ConnectTimeout, RibbonClientConfiguration.DEFAULT_CONNECT_TIMEOUT);</span><br><span class="line"><span class="keyword">int</span> maxAutoRetries = getTimeout(config, commandKey, <span class="string">"MaxAutoRetries"</span>,</span><br><span class="line">IClientConfigKey.Keys.MaxAutoRetries, DefaultClientConfigImpl.DEFAULT_MAX_AUTO_RETRIES);</span><br><span class="line"><span class="keyword">int</span> maxAutoRetriesNextServer = getTimeout(config, commandKey, <span class="string">"MaxAutoRetriesNextServer"</span>,</span><br><span class="line">IClientConfigKey.Keys.MaxAutoRetriesNextServer, DefaultClientConfigImpl.DEFAULT_MAX_AUTO_RETRIES_NEXT_SERVER);</span><br><span class="line"><span class="comment">// 原来ribbonTimeout的计算方法在这里,以上文的设置为例</span></span><br><span class="line"><span class="comment">// ribbonTimeout = (50000 + 50000) * (0 + 1) * (1 + 1) = 200000</span></span><br><span class="line">ribbonTimeout = (ribbonReadTimeout + ribbonConnectTimeout) * (maxAutoRetries + <span class="number">1</span>) * (maxAutoRetriesNextServer + <span class="number">1</span>);</span><br><span class="line">}</span><br><span class="line"><span class="keyword">return</span> ribbonTimeout;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可以看到ribbonTimeout是一个总时间,所以从逻辑上来讲,作者希望hystrixTimeout要大于ribbonTimeout,否则hystrix熔断了以后,ribbon的重试就都没有意义了。</p><h3 id="ribbon单服务设置"><a href="#ribbon单服务设置" class="headerlink" title="ribbon单服务设置"></a>ribbon单服务设置</h3><p>到这里最前面的疑问已经解开了,但是hytrix可以分服务设置timeout,ribbon可不可以? 源码走起,这里看的文件是<code>DefaultClientConfigImpl.java</code></p><figure class="highlight java"><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"><span class="comment">// 这是获取配置的入口方法,如果是null,那么用默认值</span></span><br><span class="line"><span class="comment">// 所有ribbon的默认值的都在该类中设置了,可以自己看一下</span></span><br><span class="line"><span class="keyword">public</span> <T> <span class="function">T <span class="title">get</span><span class="params">(IClientConfigKey<T> key, T defaultValue)</span> </span>{</span><br><span class="line"> T value = get(key);</span><br><span class="line"> <span class="keyword">if</span> (value == <span class="keyword">null</span>) {</span><br><span class="line"> value = defaultValue;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> value;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 这是核心方法 </span></span><br><span class="line"><span class="function"><span class="keyword">protected</span> Object <span class="title">getProperty</span><span class="params">(String key)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (enableDynamicProperties) {</span><br><span class="line"> String dynamicValue = <span class="keyword">null</span>;</span><br><span class="line"> DynamicStringProperty dynamicProperty = dynamicProperties.get(key);</span><br><span class="line"> <span class="comment">// dynamicProperties其实是一个缓存,首次访问foo服务的时候会加载</span></span><br><span class="line"> <span class="keyword">if</span> (dynamicProperty != <span class="keyword">null</span>) {</span><br><span class="line"> dynamicValue = dynamicProperty.get();</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 如果缓存没有,那么就再获取一次,注意这里的getConfigKey(key)是生成key的方法</span></span><br><span class="line"> <span class="keyword">if</span> (dynamicValue == <span class="keyword">null</span>) {</span><br><span class="line"> dynamicValue = DynamicProperty.getInstance(getConfigKey(key)).getString();</span><br><span class="line"> <span class="comment">// 如果还是没有取默认值,getDefaultPropName(key)生成key的方法</span></span><br><span class="line"> <span class="keyword">if</span> (dynamicValue == <span class="keyword">null</span>) {</span><br><span class="line"> dynamicValue = DynamicProperty.getInstance(getDefaultPropName(key)).getString();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (dynamicValue != <span class="keyword">null</span>) {</span><br><span class="line"> <span class="keyword">return</span> dynamicValue;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> properties.get(key);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>以我们的服务为例:<br><code>getConfigKey(key)</code> returns <code>foo.ribbon.ReadTimeout</code><br><code>getDefaultPropName(key)</code> returns <code>ribbon.ReadTimeout</code></p><p>一目了然,<code>{serviceName}.ribbon.{propertyName}</code>就可以了。</p><h3 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h3><p>感觉ribbon和hytrix的配置获取源码略微有点乱,所以也导致大家在设置的时候有些无所适从。<code>spring-cloud</code>的代码一直在迭代,无论github上还是文档可能都相对滞后,这时候阅读源码并且动手debug一下是最能接近事实真相的了。</p>]]></content>
<summary type="html">
<p>大家在初次使用spring-cloud的gateway的时候,肯定会被里面各种的Timeout搞得晕头转向。hytrix有设置,ribbon也有。我们一开始也是乱设一桶,Github上各种项目里也没几个设置正确的。对Timeout的研究源于一次log中的warning</p>
<blockquote>
<p>The Hystrix timeout of 60000 ms for the command “foo” is set lower than the combination of the Ribbon read and connect timeout, 200000ms.</p>
</blockquote>
</summary>
<category term="spring" scheme="http://www.deanwangpro.com/tags/spring/"/>
<category term="spring-cloud" scheme="http://www.deanwangpro.com/tags/spring-cloud/"/>
<category term="timeout" scheme="http://www.deanwangpro.com/tags/timeout/"/>
<category term="ribbon" scheme="http://www.deanwangpro.com/tags/ribbon/"/>
<category term="hytrix" scheme="http://www.deanwangpro.com/tags/hytrix/"/>
</entry>
<entry>
<title>spring-cloud中zuul的两种隔离机制实验</title>
<link href="http://www.deanwangpro.com/2018/04/04/spring-cloud-zuul-threads/"/>
<id>http://www.deanwangpro.com/2018/04/04/spring-cloud-zuul-threads/</id>
<published>2018-04-03T16:00:00.000Z</published>
<updated>2018-04-04T10:39:13.000Z</updated>
<content type="html"><![CDATA[<p>ZuulException REJECTED_SEMAPHORE_EXECUTION 是一个最近在性能测试中经常遇到的异常。查询资料发现是因为zuul默认每个路由直接用信号量做隔离,并且默认值是100,也就是当一个路由请求的信号量高于100那么就拒绝服务了,返回500。</p><a id="more"></a><h3 id="信号量隔离"><a href="#信号量隔离" class="headerlink" title="信号量隔离"></a>信号量隔离</h3><p>既然默认值太小,那么就在gateway的配置提高各个路由的信号量再实验。</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></pre></td><td class="code"><pre><span class="line">routes:</span><br><span class="line"> linkflow:</span><br><span class="line"> path: /api1/**</span><br><span class="line"> serviceId: lf</span><br><span class="line"> stripPrefix: false</span><br><span class="line"> semaphore:</span><br><span class="line"> maxSemaphores: 2000</span><br><span class="line"> oauth:</span><br><span class="line"> path: /api2/**</span><br><span class="line"> serviceId: lf</span><br><span class="line"> stripPrefix: false</span><br><span class="line"> semaphore:</span><br><span class="line"> maxSemaphores: 1000</span><br></pre></td></tr></table></figure><p>两个路由的信号量分开提高到2000和1000。我们再用gatling测试一下。</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">setUp(scn.inject(rampUsers(200) over (3 seconds)).protocols(httpConf))</span><br></pre></td></tr></table></figure><p>这是我们的模型,3s内启动200个用户,顺序访问5个API。所以会有1000个request。机器配置只有2核16G,并且是docker化的数据库。所以整体性能不高。</p><p><img src="/images/15228346573506.jpg" alt="信号量统计"></p><p>看结果仍然有57个KO,但是比之前1000个Request有900个KO的比例好很多了。</p><h3 id="线程隔离"><a href="#线程隔离" class="headerlink" title="线程隔离"></a>线程隔离</h3><p><code>Edgware</code>版本的spring cloud提供了另一种基于线程池的隔离机制。实现起来也非常简单,</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></pre></td><td class="code"><pre><span class="line">zuul:</span><br><span class="line"> ribbon-isolation-strategy: THREAD</span><br><span class="line"> thread-pool:</span><br><span class="line"> use-separate-thread-pools: true</span><br><span class="line"> thread-pool-key-prefix: zuulgw</span><br><span class="line"> </span><br><span class="line">hystrix:</span><br><span class="line"> threadpool:</span><br><span class="line"> default:</span><br><span class="line"> coreSize: 50</span><br><span class="line"> maximumSize: 10000</span><br><span class="line"> allowMaximumSizeToDivergeFromCoreSize: true</span><br><span class="line"> maxQueueSize: -1</span><br><span class="line"> execution:</span><br><span class="line"> isolation:</span><br><span class="line"> thread:</span><br><span class="line"> timeoutInMilliseconds: 60000</span><br></pre></td></tr></table></figure><p><code>use-separate-thread-pools</code>的意思是每个路由都有自己的线程池,而不是共享一个。<br><code>thread-pool-key-prefix</code>会指定一个线程池前缀方便调试。<br><code>hystrix</code>的部分主要设置线程池的大小,这里设置了10000,其实并不是越大越好。线程池越大削峰填谷的效果越显著,也就是时间换空间。系统的整体负载会上升,导致响应时间越来越长,那么当响应时间超过某个限度,其实系统也算是不可用了。后面可以看到数据。</p><p><img src="/images/15228360753126.jpg" alt="线程池统计"></p><p>这次没有500的情况了,1000个Request都正常返回了。</p><h3 id="比较"><a href="#比较" class="headerlink" title="比较"></a>比较</h3><p>从几张图对比下两种隔离的效果,上图是信号量隔离,下图是线程隔离。</p><h4 id="响应时间分布"><a href="#响应时间分布" class="headerlink" title="响应时间分布"></a>响应时间分布</h4><p><img src="/images/15228364522331.jpg" alt="信号量隔离响应时间分布"></p><p><img src="/images/15228364349973.jpg" alt="线程隔离响应时间分布"></p><p>直观上能发现使用线程隔离的分布更好看一些,600ms内的响应会更多一些。</p><h4 id="QPS"><a href="#QPS" class="headerlink" title="QPS"></a>QPS</h4><p><img src="/images/15228368284029.jpg" alt="信号量隔离QPS"></p><p><img src="/images/15228368551165.jpg" alt="线程隔离QPS"></p><p>两张图展示的是同一时刻的Request和Response的数量。</p><p>先看信号量隔离的场景,Response per second是逐步提升的,但是达到一个量级后,gateway开始拒绝服务。猜测是超过了信号量的限制或是超时?</p><p>线程隔离的这张就比较有意思了,可以看到Request per second上升的速度要比上面的快,说明系统是试图接收更多的请求然后分发给线程池。再看在某个时间点Response per second反而开始下降,因为线程不断的创建消耗了大量的系统资源,响应变慢。之后因为请求少了,负载降低,Response又开始抬升。所以线程池也并非越大越好,需要不断调试寻找一个平衡点。</p><h3 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h3><p>线程池提供了比信号量更好的隔离机制,并且从实际测试发现高吞吐场景下可以完成更多的请求。但是信号量隔离的开销更小,对于本身就是10ms以内的系统,显然信号量更合适。</p>]]></content>
<summary type="html">
<p>ZuulException REJECTED_SEMAPHORE_EXECUTION 是一个最近在性能测试中经常遇到的异常。查询资料发现是因为zuul默认每个路由直接用信号量做隔离,并且默认值是100,也就是当一个路由请求的信号量高于100那么就拒绝服务了,返回500。</p>
</summary>
<category term="spring" scheme="http://www.deanwangpro.com/tags/spring/"/>
<category term="test" scheme="http://www.deanwangpro.com/tags/test/"/>
<category term="spring-cloud" scheme="http://www.deanwangpro.com/tags/spring-cloud/"/>
</entry>
<entry>
<title>spring-boot系列之集成测试</title>
<link href="http://www.deanwangpro.com/2018/03/22/spring-boot-integration-test/"/>
<id>http://www.deanwangpro.com/2018/03/22/spring-boot-integration-test/</id>
<published>2018-03-21T16:00:00.000Z</published>
<updated>2018-03-22T09:51:58.000Z</updated>
<content type="html"><![CDATA[<p>如果希望很方便针对API进行测试,并且方便的集成到CI中验证每次的提交,那么spring boot自带的IT绝对是不二选择。</p><a id="more"></a><h3 id="迅速编写一个测试Case"><a href="#迅速编写一个测试Case" class="headerlink" title="迅速编写一个测试Case"></a>迅速编写一个测试Case</h3><figure class="highlight java"><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 class="meta">@RunWith</span>(SpringRunner<span class="class">.<span class="keyword">class</span>)</span></span><br><span class="line"><span class="class">@<span class="title">SpringBootTest</span>(<span class="title">webEnvironment</span> </span>= SpringBootTest.WebEnvironment.RANDOM_PORT)</span><br><span class="line"><span class="meta">@ActiveProfiles</span>({Profiles.ENV_IT})</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">DemoIntegrationTest</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> FooService fooService;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">test</span><span class="params">()</span> </span>{</span><br><span class="line"> System.out.println(<span class="string">"tested"</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>其中<code>SpringBootTest</code>定义了跑IT时的一些配置,上述代码是用了随机端口,当然也可以预定义端口,像这样</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">@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, properties = {"server.port=9990"})</span><br></pre></td></tr></table></figure><p><code>ActiveProfiles</code>强制使用了IT的Profile,从最佳实践上来说IT Profile所配置的数据库或者其他资源组件的地址,应该是与开发或者Staging环境隔离的。因为当一个IT跑完之后很多情况下我们需要清除测试数据。</p><p>你能够发现这样的Case可以使用<code>Autowired</code>注入任何想要的Service。这是因为spring将整个上下文都加载了起来,与实际运行的环境是一样的,包含了数据库,缓存等等组件。如果觉得测试时不需要全部的资源,那么在profile删除对应的配置就可以了。这就是一个完整的运行环境,唯一的区别是当用例跑完会自动shutdown。</p><h3 id="测试一个Rest-API"><a href="#测试一个Rest-API" class="headerlink" title="测试一个Rest API"></a>测试一个Rest API</h3><p>强烈推荐一个库,加入到gradle中</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">testCompile 'io.rest-assured:rest-assured:3.0.3'</span><br></pre></td></tr></table></figure><p>支持JsonPath,十分好用,具体文档戳<a href="https://github.com/rest-assured/rest-assured/wiki/GettingStarted" target="_blank" rel="noopener">这里</a></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></pre></td><td class="code"><pre><span class="line">@Sql(scripts = "/testdata/users.sql")</span><br><span class="line">@Test</span><br><span class="line">public void test001Login() {</span><br><span class="line"> String username = "[email protected]";</span><br><span class="line"> String password = "demo";</span><br><span class="line"></span><br><span class="line"> JwtAuthenticationRequest request = new JwtAuthenticationRequest(username, password);</span><br><span class="line"></span><br><span class="line"> Response response = given().contentType(ContentType.JSON).body(request)</span><br><span class="line"> .when().post("/auth/login").then()</span><br><span class="line"> .statusCode(HttpStatus.OK.value())</span><br><span class="line"> .extract()</span><br><span class="line"> .response();</span><br><span class="line"></span><br><span class="line"> assertThat(response.path("token"), is(IsNull.notNullValue()));</span><br><span class="line"> assertThat(response.path("expiration"), is(IsNull.notNullValue()));</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>@Sql</code>用于在测试前执行sql插入测试数据。注意<code>given().body()</code>中传入的是一个java对象<code>JwtAuthenticationRequest</code>,因为rest-assured会自动帮你用<code>jackson</code>将对象序列化成json字符串。当然也可以将转换好的json放到body,效果是一样的。</p><p>返回结果被一个Response接住,之后就可以用JsonPath获取其中数据进行验证。当然还有一种更直观的办法,可以通过<code>response.asString()</code>获取完整的response,再反序列化成java对象进行验证。</p><p>至此,最基本的IT就完成了。 在Jenkins增加一个step<code>gradle test</code>就可以实现每次提交代码都进行一次测试。</p><h3 id="一些复杂的情况"><a href="#一些复杂的情况" class="headerlink" title="一些复杂的情况"></a>一些复杂的情况</h3><h4 id="数据混杂"><a href="#数据混杂" class="headerlink" title="数据混杂"></a>数据混杂</h4><p>这是最容易发生,一个项目有很多dev,每个dev都会写自己的IT case,那么如果数据之间产生了影响怎么办。很容易理解,比如一个测试批量写的场景,最后验证方式是看写的数据量是不是10w行。那么另外一个dev写了其他的case恰好也新增了一条数据到这张表,结果变成了10w+1行,那么批量写的case就跑不过了。</p><p>为了杜绝这种情况,我们采用每次跑完一个测试Class就将数据清空。既然是基于类的操作,可以写一个基类解决。</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></pre></td><td class="code"><pre><span class="line">@RunWith(SpringRunner.class)</span><br><span class="line">@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)</span><br><span class="line">@ActiveProfiles({Profiles.ENV_IT})</span><br><span class="line">public abstract class BaseIntegrationTest {</span><br><span class="line"></span><br><span class="line"> private static JdbcTemplate jdbcTemplate;</span><br><span class="line"></span><br><span class="line"> @Autowired</span><br><span class="line"> public void setDataSource(DataSource dataSource) {</span><br><span class="line"> jdbcTemplate = new JdbcTemplate(dataSource);</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> @Value("${local.server.port}")</span><br><span class="line"> protected int port;</span><br><span class="line"></span><br><span class="line"> @Before</span><br><span class="line"> public void setupEnv() {</span><br><span class="line"> RestAssured.port = port;</span><br><span class="line"> RestAssured.basePath = "/api";</span><br><span class="line"> RestAssured.baseURI = "http://localhost";</span><br><span class="line"> RestAssured.config = RestAssured.config().httpClient(HttpClientConfig.httpClientConfig().httpMultipartMode(HttpMultipartMode.BROWSER_COMPATIBLE));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> public void tearDownEnv() {</span><br><span class="line"> given().contentType(ContentType.JSON)</span><br><span class="line"> .when().post("/auth/logout");</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> @AfterClass</span><br><span class="line"> public static void cleanDB() throws SQLException {</span><br><span class="line"> Resource resource = new ClassPathResource("/testdata/CleanDB.sql");</span><br><span class="line"> Connection connection = jdbcTemplate.getDataSource().getConnection();</span><br><span class="line"> ScriptUtils.executeSqlScript(connection, resource);</span><br><span class="line"> connection.close();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>@AfterClass</code>中使用了jdbcTemplate执行了一个CleanDB.sql,通过这种方式清除所有测试数据。</p><p><code>@Value("${local.server.port}")</code>也要提一下,因为端口是随机的,那么Rest-Assured不知道请求要发到losthost的哪个端口上,这里使用<code>@Value</code>获取当前的端口号并设置到<code>RestAssured.port</code>就解决了这个问题。</p><h4 id="共有数据怎么处理"><a href="#共有数据怎么处理" class="headerlink" title="共有数据怎么处理"></a>共有数据怎么处理</h4><p>跑一次完整的IT,可能需要经历数十个Class,数百个method,那么如果一些数据是所有case都需要的,只有在所有case都跑完才需要清除怎么办?换句话说,这种数据清理不是基于<strong>类</strong>的,而是基于一次<strong>运行</strong>。比如初始用户数据,城市库等等</p><p>我们耍了个小聪明,借助了<code>flyway</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></pre></td><td class="code"><pre><span class="line">@Configuration</span><br><span class="line">@ConditionalOnClass({DataSource.class})</span><br><span class="line">public class UpgradeAutoConfiguration {</span><br><span class="line"></span><br><span class="line"> public static final String FLYWAY = "flyway";</span><br><span class="line"></span><br><span class="line"> @Bean(name = FLYWAY)</span><br><span class="line"> @Profile({ENV_IT})</span><br><span class="line"> public UpgradeService cleanAndUpgradeService(DataSource dataSource) {</span><br><span class="line"> UpgradeService upgradeService = new FlywayUpgradeService(dataSource);</span><br><span class="line"> try {</span><br><span class="line"> upgradeService.cleanAndUpgrade();</span><br><span class="line"> } catch (Exception ex) {</span><br><span class="line"> LOGGER.error("Flyway failed!", ex);</span><br><span class="line"> }</span><br><span class="line"> return upgradeService;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可以看到当Profile是IT的情况下,<code>flyway</code>会drop掉所有表并重新依次执行每次的upgrade脚本,由此创建完整的数据表,当然都是空的。在项目的test路径下,增加一个版本极大的sql,这样就可以让<code>flyway</code>在最后插入共用的测试数据,例如<code>src/test/resources/db/migration/V999.0.1__Insert_Users.sql</code> ,完美的解决各种数据问题。</p><h3 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h3><p>用Spring boot内置的测试服务可以很快速的验证API,我现在都不用把服务启动再通过人工页面点击来测试自己的API,直接与前端同事沟通好Request的格式,写个Case就可以验证。</p><p>当然这种方式也有一个不足就是不方便对系统进行压力测试,之前在公司的API测试用例都是Jmeter写的,做性能测试的时候会方便很多。</p><p>仍在寻找合适的跑性能的工具,如有推荐欢迎留言。</p>]]></content>
<summary type="html">
<p>如果希望很方便针对API进行测试,并且方便的集成到CI中验证每次的提交,那么spring boot自带的IT绝对是不二选择。</p>
</summary>
<category term="spring" scheme="http://www.deanwangpro.com/tags/spring/"/>
<category term="spring-boot" scheme="http://www.deanwangpro.com/tags/spring-boot/"/>
<category term="test" scheme="http://www.deanwangpro.com/tags/test/"/>
</entry>
<entry>
<title>坑系列之阿里SLB上使用Webscoket</title>
<link href="http://www.deanwangpro.com/2018/03/10/Ali-SLB-for-Websocket/"/>
<id>http://www.deanwangpro.com/2018/03/10/Ali-SLB-for-Websocket/</id>
<published>2018-03-09T16:00:00.000Z</published>
<updated>2018-03-12T01:51:14.000Z</updated>
<content type="html"><![CDATA[<p>Websocket是HTML5之后的一个新事物,可以方便的实现客户端到服务端的长会话,特别适合用于客户端需要接收服务端推送的场景。例如在线客服聊天,提醒推送等等。改变了以往客户端只能通过轮询或者long poll来获取服务端状态的限制。</p><a id="more"></a><h3 id="和HTTP协议有什么关系"><a href="#和HTTP协议有什么关系" class="headerlink" title="和HTTP协议有什么关系"></a>和HTTP协议有什么关系</h3><p>首先我们来看一下Websocket协议和HTTP有什么关系呢?<br>本质上说,Websocket和HTTP就不是一个协议,层级不一样。但是为了兼容现有浏览器的握手规范,必须借助HTTP协议建立连接。</p><p>这是一个Websocket的握手请求</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">GET wss://server.example.com/ HTTP/1.1</span><br><span class="line">Host: server.example.com</span><br><span class="line">Pragma: no-cache</span><br><span class="line">Cache-Control: no-cache</span><br><span class="line">Connection: Upgrade</span><br><span class="line">Upgrade: websocket</span><br><span class="line">Origin: https://server.example.com</span><br><span class="line">Accept-Encoding: gzip, deflate, br</span><br><span class="line">Sec-WebSocket-Version: 13</span><br><span class="line">Sec-WebSocket-Key: fFFIlFcwULSAmQacRAbS2A==</span><br><span class="line">Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits</span><br></pre></td></tr></table></figure><p>这里面有几个和一般HTTP Request不一样的地方,</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">Connection: Upgrade</span><br><span class="line">Upgrade: websocket</span><br><span class="line">Sec-WebSocket-Version: 13</span><br><span class="line">Sec-WebSocket-Key: fFFIlFcwULSAmQacRAbS2A==</span><br><span class="line">Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits</span><br></pre></td></tr></table></figure><p>这是告诉服务端这不是一个普通的请求,而是Websocket协议。Sec-WebSocket-Key 是一个Base64 encode的值,是浏览器随机生成的,用于让服务端知道这是一个全新的socket客户端。</p><p>服务端如果开启了Socket监听,那么就会返回这样的Response</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">HTTP/1.1 101 Switching Protocols</span><br><span class="line">Date: Fri, 09 Mar 2018 16:24:45 GMT</span><br><span class="line">Connection: upgrade</span><br><span class="line">upgrade: websocket</span><br><span class="line">sec-websocket-accept: i/tCy92JmOXIoZwGi8ROh6CgUwk=</span><br></pre></td></tr></table></figure><p>表示接收了请求,并且即将切换到Websocket协议,所以code是101。Sec-WebSocket-Accept 这个则是经过服务器确认,并且加密过后的 Sec-WebSocket-Key。到这里HTTP协议的任务就已经完成,之后的通信都是基于Websocket协议了。</p><h3 id="怎么通过nginx转发Websocket的握手请求"><a href="#怎么通过nginx转发Websocket的握手请求" class="headerlink" title="怎么通过nginx转发Websocket的握手请求"></a>怎么通过nginx转发Websocket的握手请求</h3><p>本质上说握手请求就是一个特殊的HTTP Request,只是需要加一些上文提到的特殊内容,从<a href="https://www.nginx.com/blog/websocket-nginx/" target="_blank" rel="noopener">Nignx官方介绍</a>可以看到</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">location /wsapp/ {</span><br><span class="line"> proxy_pass http://wsbackend;</span><br><span class="line"> proxy_http_version 1.1;</span><br><span class="line"> proxy_set_header Upgrade $http_upgrade;</span><br><span class="line"> proxy_set_header Connection "Upgrade";</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>只是在Request header加了两个属性,并且强制升级到HTTP 1.1,原因是HTTP 1.0不支持keep alive。如果使用HTTP 1.0发握手请求,服务端返回101以后就会直接结束这次HTTP会话了。这一点也为之后的坑埋下了伏笔。</p><h3 id="坑从何来"><a href="#坑从何来" class="headerlink" title="坑从何来"></a>坑从何来</h3><p>自从上线了Websocket服务之后,就会经常发现socket无法建立,获得504的超时响应。</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">HTTP/1.1 504 Gateway Time-out</span><br><span class="line">Date: Fri, 09 Mar 2018 03:34:54 GMT</span><br><span class="line">Content-Type: text/html</span><br><span class="line">Content-Length: 272</span><br><span class="line">Connection: keep-alive</span><br></pre></td></tr></table></figure><p>而且这一响应只有在经过SLB(负载均衡)时才有,如果直接请求到我们自己的nginx是没有问题的。但是基于对阿里的信任,还是觉得问题应该还是我们自己这儿。从code review到nginx配置,折腾了五六个小时。</p><p>最后只有自己搭建的nginx access log上寻找蛛丝马迹,一开始抓到一些响应都是499的返回,并且request_time时间都在60s上下。</p><blockquote><p>[09/Mar/2018:15:04:51 +0800] 100.97.89.10 - - - 10.0.21.11 to: 10.0.20.11:8011: GET /ws/?id=168451&url=<a href="http://server.example.com/" target="_blank" rel="noopener">http://server.example.com/</a> HTTP/1.0 upstream_response_time - msec 1520579091.139 request_time <strong>60.000</strong> status 499 client - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36</p></blockquote><p>就考虑是不是socket服务端建立连接后响应不及时,让SLB发现60s没有报文交互直接就切断请求了。</p><p>但是因为我们在前端是做了心跳的,即使服务端不响应,只要socket建立通过心跳肯定也会在60s内进行交互。不应该出现上面的场景。<br>之后我们把access log中socket建立成功的请求和不成功的请求分开放到一起对比,发现不成功的都是HTTP 1.0的协议。</p><blockquote><p>[09/Mar/2018:15:03:51 +0800] 100.97.88.238 - - - 10.0.20.11 to: 127.0.0.1:8011: GET /ws/?id=168451&url=<a href="http://server.example.com" target="_blank" rel="noopener">http://server.example.com</a> <strong>HTTP/1.1</strong> upstream_response_time 11.069 msec 1520579031.198 request_time 11.|<br>069 status 101 client - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36 |<br>[09/Mar/2018:15:04:32 +0800] 100.97.88.254 - - - 10.0.20.11 to: 127.0.0.1:8011: GET /ws/?id=168451&url=<a href="http://server.example.com" target="_blank" rel="noopener">http://server.example.com</a> <strong>HTTP/1.0</strong> upstream_response_time - msec 1520579072.716 request_time 36.755 s|<br>tatus 499 client - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36</p></blockquote><p>就好像这两个请求,同一个页面发出的,但是一个成功一个失败。失败的正好就是HTTP/1.0,为什么会有两个版本的协议呢,<br>为了证据更加“确凿”,我们对请求进行了抓包分析,并将Sec-WebSocket-Key打印到Nginx的access log中方便trace同一个请求。</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">GET http://server.example.com/ws/ HTTP/1.1</span><br><span class="line">Host: app.linkflowtech.com</span><br><span class="line">Connection: Upgrade</span><br><span class="line">Pragma: no-cache</span><br><span class="line">Cache-Control: no-cache</span><br><span class="line">Upgrade: websocket</span><br><span class="line">Origin: http://server.example.com</span><br><span class="line">Sec-WebSocket-Key: 8+qDYeKJGFTWKB2ov4p5TA==</span><br><span class="line">Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits</span><br></pre></td></tr></table></figure><blockquote><p>[09/Mar/2018:17:07:07 +0800] 100.97.88.252 - - - 10.0.21.11 to: 10.0.20.11:8011: GET /ws/ HTTP/1.0 upstream_response_time - msec 1520586427.537 request_time 59.999 status 499 client - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36 <strong>8+qDYeKJGFTWKB2ov4p5TA==</strong><br>2018-03-09 17:12:04</p></blockquote><p>可以看到都是 <strong>8+qDYeKJGFTWKB2ov4p5TA==</strong> 的请求,但是在经过SLB进入nginx时候协议降级到了1.0.这叫一个酸爽,赶紧给阿里云开了工单,经过大概3~4个小时的交流。最终获得一个链接,里面有这样的描述</p><blockquote><p>如何在阿里云负载均衡上启用WS/WSS支持?<br>无需配置,当选用HTTP监听时,默认支持无加密版本WebSocket协议(WS协议);当选择HTTPS监听时,默认支持加密版本的WebSocket协议(WSS协议)。<br>注意:需要将实例升级为<strong>性能保障型实例</strong>。详细参见如何使用负载均衡性能保障型实例。</p></blockquote><p>这个大坑就在”注意”那一段,我们的SLB是性能共享型而不是性能保障型。看来也不是阿里云的问题,是我们的SLB档次不够高啊。知道原因后,立刻付费升级了保障型。实测一下所有问题都解决了。</p><p>虽然问题解决了,但是其实很难理解厂商的逻辑,为什么性能共享型中某些SLB节点就会降级HTTP协议版本呢,要知道1.0版本已经是一个相当落后的版本了。</p><p>在此记录一下心路历程,为了让其他使用阿里云的同学不要重蹈覆辙。</p>]]></content>
<summary type="html">
<p>Websocket是HTML5之后的一个新事物,可以方便的实现客户端到服务端的长会话,特别适合用于客户端需要接收服务端推送的场景。例如在线客服聊天,提醒推送等等。改变了以往客户端只能通过轮询或者long poll来获取服务端状态的限制。</p>
</summary>
<category term="DevOps" scheme="http://www.deanwangpro.com/tags/DevOps/"/>
</entry>
<entry>
<title>压测工具wrk和Artillery的比较</title>
<link href="http://www.deanwangpro.com/2017/12/09/wrk-and-artillery/"/>
<id>http://www.deanwangpro.com/2017/12/09/wrk-and-artillery/</id>
<published>2017-12-08T16:00:00.000Z</published>
<updated>2017-12-22T06:06:05.000Z</updated>
<content type="html"><![CDATA[<p>这两天抽空使用了一下两款压测工具</p><ul><li>wrk</li><li>Artillery</li></ul><p>并且通过两款工具对产品的两个环境进行了测试</p><a id="more"></a><h3 id="工具比较"><a href="#工具比较" class="headerlink" title="工具比较"></a>工具比较</h3><h4 id="wrk"><a href="#wrk" class="headerlink" title="wrk"></a>wrk</h4><p>wrk自身性能就非常惊人,使用epoll这种多路复用技术,所以可以用少量的线程来跟被测服务创建大量连接,进行压测,同时不占用过多的CPU和内存。</p><p>命令非常简单</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">wrk -t8 -c200 -d30s --latency <span class="string">"http://www.baidu.com"</span></span><br></pre></td></tr></table></figure><p>这样就可以进行最简单的压测。但是真实使用起来肯定会有复杂的场景,比如先要登录取到token再进行下一步。好在wrk支持lua脚本,提供了几个阶段的hook来让用户自定义逻辑,具体可以看github上的官方提供的script sample。</p><p>我这里举一个获取token的例子</p><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></pre></td><td class="code"><pre><span class="line"><span class="comment">-- @Author: wangding</span></span><br><span class="line"><span class="comment">-- @Date: 2017-12-06 15:13:19</span></span><br><span class="line"><span class="comment">-- @Last Modified by: wangding</span></span><br><span class="line"><span class="comment">-- @Last Modified time: 2017-12-06 23:57:49</span></span><br><span class="line"><span class="keyword">local</span> cjson = <span class="built_in">require</span> <span class="string">"cjson"</span></span><br><span class="line"><span class="keyword">local</span> cjson2 = cjson.new()</span><br><span class="line"><span class="keyword">local</span> cjson_safe = <span class="built_in">require</span> <span class="string">"cjson.safe"</span></span><br><span class="line"></span><br><span class="line">token = <span class="literal">nil</span></span><br><span class="line"><span class="built_in">path</span> = <span class="string">"/api/auth/login"</span></span><br><span class="line">method = <span class="string">"POST"</span></span><br><span class="line"></span><br><span class="line">wrk.headers[<span class="string">"Content-Type"</span>] = <span class="string">"application/json"</span></span><br><span class="line"></span><br><span class="line">request = <span class="function"><span class="keyword">function</span><span class="params">()</span></span></span><br><span class="line"> <span class="keyword">return</span> wrk.<span class="built_in">format</span>(method, <span class="built_in">path</span>, <span class="literal">nil</span>, <span class="string">'{"username":"[email protected]","password":"demo"}'</span>)</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br><span class="line">response = <span class="function"><span class="keyword">function</span><span class="params">(status, headers, body)</span></span></span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">not</span> token <span class="keyword">and</span> <span class="built_in">status</span> == <span class="number">200</span> <span class="keyword">then</span></span><br><span class="line"> value = cjson.decode(body)</span><br><span class="line"> token = value[<span class="string">"token"</span>]</span><br><span class="line"> method = <span class="string">"GET"</span></span><br><span class="line"> <span class="built_in">path</span> = <span class="string">"/api/contact?size=20&page=0"</span></span><br><span class="line"> wrk.headers[<span class="string">"Authorization"</span>] = token</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><code>request</code> 和 <code>response</code> 分别是两个hook,每次请求都会调用,那么这里request的逻辑就是一开始就使用<code>POST</code>请求<code>/api/auth/login</code>并且带有body,请求完成进入response,第一次token肯定是nil,所以把repose的token解析出来付给全局变量<code>token</code>,之后改写全局变量为<code>GET</code>请求地址<code>/api/contact</code>并且设置了header包含<code>Authorization</code>。</p><p>这样实际是变通的实现了一个简单scenario的测试,那么问题来了,如果场景更复杂怎么办?写肯定是可以写的,但是并不直观,所以wrk不太适合一个包含有序场景的压力测试。</p><p>再来看一下wrk的report,这一点是我最喜欢的</p><figure class="highlight bash"><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></pre></td><td class="code"><pre><span class="line">wrk -t8 -c200 -d30s -H <span class="string">"Authorization: token"</span> --latency <span class="string">"http://10.0.20.2:8080/api/contact?size=20&page=0"</span></span><br><span class="line">Running 30s <span class="built_in">test</span> @ http://10.0.20.2:8080/api/contact?size=20&page=0</span><br><span class="line"> 8 threads and 200 connections</span><br><span class="line"> Thread Stats Avg Stdev Max +/- Stdev</span><br><span class="line"> Latency 769.49ms 324.43ms 1.99s 72.08%</span><br><span class="line"> Req/Sec 33.37 21.58 131.00 62.31%</span><br><span class="line"> Latency Distribution</span><br><span class="line"> 50% 728.97ms</span><br><span class="line"> 75% 958.69ms</span><br><span class="line"> 90% 1.21s</span><br><span class="line"> 99% 1.74s</span><br><span class="line"> 7606 requests <span class="keyword">in</span> 30.03s, 176.69MB <span class="built_in">read</span></span><br><span class="line"> Socket errors: connect 0, <span class="built_in">read</span> 0, write 0, timeout 38</span><br><span class="line">Requests/sec: 253.31</span><br><span class="line">Transfer/sec: 5.88MB</span><br></pre></td></tr></table></figure><p>开启8线程,每个线程200个连接,持续30s的调用,可以看到报告中直接给出了最关键的指标QPS,这里的值是253.31。平均响应时间是33.37ms。简单直接,非常易懂。</p><p>但是这里面有个坑就是cjson这个lua module的使用,不可以使用lua5.2,必须使用lua5.1而且需要特定的wrk和cjson。我直接使用docker来封装这个运行环境,坏处是docker使用host模式本身性能可能就有影响。</p><h4 id="Artillery"><a href="#Artillery" class="headerlink" title="Artillery"></a>Artillery</h4><p>一开始看到Artillery主要是因为它支持带场景的测试,也就是带有步骤,看一眼获取token再进行下一步的脚本。</p><figure class="highlight yml"><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"><span class="attr">config:</span></span><br><span class="line"> <span class="attr">target:</span> <span class="string">"http://10.0.20.2:8080"</span></span><br><span class="line"> <span class="attr">phases:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">duration:</span> <span class="number">30</span></span><br><span class="line"> <span class="attr">arrivalRate:</span> <span class="number">100</span></span><br><span class="line"><span class="attr">scenarios:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">flow:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">post:</span></span><br><span class="line"> <span class="attr">url:</span> <span class="string">"/api/auth/login"</span></span><br><span class="line"> <span class="attr">json:</span></span><br><span class="line"> <span class="attr">username:</span> <span class="string">"[email protected]"</span></span><br><span class="line"> <span class="attr">password:</span> <span class="string">"demo"</span></span><br><span class="line"> <span class="attr">capture:</span></span><br><span class="line"> <span class="attr">json:</span> <span class="string">"$.token"</span></span><br><span class="line"> <span class="attr">as:</span> <span class="string">"token"</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">log:</span> <span class="string">"Login token: <span class="template-variable">{{ token }}</span>"</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">get:</span></span><br><span class="line"> <span class="attr">url:</span> <span class="string">"/api/contact?size=20&page=0"</span></span><br><span class="line"> <span class="attr">headers:</span></span><br><span class="line"> <span class="attr">Authorization:</span> <span class="string">"<span class="template-variable">{{ token }}</span>"</span></span><br></pre></td></tr></table></figure><p><code>flow</code>就是表示步骤,<code>duration</code>表示持续30s,跟wrk不同的是没有thread的概念,Artillery是nodejs写的,<code>arrivalRate</code>表示每秒模拟100个请求,所以两个参数乘起来就是3000个请求。看一下报告什么样:</p><figure class="highlight bash"><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">All virtual users finished</span><br><span class="line">Summary report @ 12:45:41(+0800) 2017-12-08</span><br><span class="line"> Scenarios launched: 3000</span><br><span class="line"> Scenarios completed: 3000</span><br><span class="line"> Requests completed: 3000</span><br><span class="line"> RPS sent: 98.33</span><br><span class="line"> Request latency:</span><br><span class="line"> min: 15.7</span><br><span class="line"> max: 179.1</span><br><span class="line"> median: 19</span><br><span class="line"> p95: 25.8</span><br><span class="line"> p99: 37.5</span><br><span class="line"> Scenario duration:</span><br><span class="line"> min: 16.4</span><br><span class="line"> max: 191.4</span><br><span class="line"> median: 19.8</span><br><span class="line"> p95: 27</span><br><span class="line"> p99: 44.6</span><br><span class="line"> Scenario counts:</span><br><span class="line"> 0: 3000 (100%)</span><br><span class="line"> Codes:</span><br><span class="line"> 200: 3000</span><br></pre></td></tr></table></figure><p>这里的<code>RPS sent</code>是指前10s平均发送请求数,所以这个和我们常说的QPS还是不一样的。如果想提高request的总数就要增加<code>arrivalRate</code>,比如上文wrk一共发了7606请求,那么这里<code>arrivalRate</code>提高到200一共可以在30s发6000次,但是改完就悲剧了,</p><figure class="highlight bash"><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">Warning: High CPU usage warning.</span><br><span class="line">See https://artillery.io/docs/faq/<span class="comment">#high-cpu-warnings for details.</span></span><br></pre></td></tr></table></figure><p>Artillery一直在不断的告警,说明这个工具自身的局限性导致想要并发发送大量请求的时候,自己就很占CPU。</p><h4 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h4><p>wrk小巧而且性能非常好,报告直观。但是对于带多个步骤的压测场景无力。<br>Artillery太耗资源,而且报告不直观。<strong>不建议采用</strong>。<br>除此之外唯一带场景的测试工具就是Jmeter了,但是Jmeter本身使用JVM是否可以短时间模拟大量并发,还是需要测试,建议与wrk做对比实验。</p><h3 id="附录:简单的性能调优"><a href="#附录:简单的性能调优" class="headerlink" title="附录:简单的性能调优"></a>附录:简单的性能调优</h3><p>在用wrk测试GET请求的时候,发现无论如何提高连接数,QPS都是在250左右,此时CPU和内存都没有占满。怀疑是有其他瓶颈。最后发现Spring Boot内嵌的tomcat线程无法突破200,所以看了一下文档,发现默认最大线程数就是200,对<code>application.yml</code>进行了调整(同时调整了多个服务,包括gateway)</p><figure class="highlight yml"><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 class="attr">server:</span></span><br><span class="line"> <span class="attr">tomcat:</span></span><br><span class="line"> <span class="attr">max-threads:</span> <span class="number">1000</span></span><br><span class="line"> <span class="attr">max-connections:</span> <span class="number">2000</span></span><br></pre></td></tr></table></figure><p>调整之后开启8线程,每个100个连接测试</p><figure class="highlight bash"><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></pre></td><td class="code"><pre><span class="line">Running 30s <span class="built_in">test</span> @ http://10.0.10.4:8769/api/contact?size=20&page=0</span><br><span class="line"> 8 threads and 100 connections</span><br><span class="line"> Thread Stats Avg Stdev Max +/- Stdev</span><br><span class="line"> Latency 235.56ms 267.57ms 1.98s 91.07%</span><br><span class="line"> Req/Sec 72.12 30.19 190.00 68.17%</span><br><span class="line"> Latency Distribution</span><br><span class="line"> 50% 166.46ms</span><br><span class="line"> 75% 281.10ms</span><br><span class="line"> 90% 472.03ms</span><br><span class="line"> 99% 1.45s</span><br><span class="line"> 15714 requests <span class="keyword">in</span> 30.03s, 4.77MB <span class="built_in">read</span></span><br><span class="line">Requests/sec: 523.29</span><br><span class="line">Transfer/sec: 162.56KB</span><br></pre></td></tr></table></figure><p>可以看到QPS达到了500以上直接翻倍了,再尝试提高连接数发现瓶颈就在内存了。</p><p>此外之前用公网做了一次压测,QPS只有10左右,看了一下阿里云的监控原来是出口带宽造成的,只有1MB的出口带宽,连接数调多大也没用。</p><p>未来还需要进行场景的细化,再决定是否使用不同的工具进行测试。</p>]]></content>
<summary type="html">
<p>这两天抽空使用了一下两款压测工具</p>
<ul>
<li>wrk</li>
<li>Artillery</li>
</ul>
<p>并且通过两款工具对产品的两个环境进行了测试</p>
</summary>
<category term="Test" scheme="http://www.deanwangpro.com/tags/Test/"/>
</entry>
<entry>
<title>List Merge的小算法</title>
<link href="http://www.deanwangpro.com/2017/10/04/list-merge/"/>
<id>http://www.deanwangpro.com/2017/10/04/list-merge/</id>
<published>2017-10-03T16:00:00.000Z</published>
<updated>2018-05-16T06:46:07.000Z</updated>
<content type="html"><![CDATA[<p>今天说一个算法小甜点,因为对于算法知之甚少,所以完全是自己揣摩出来的一则,写出来只是个记录,如有更学术派的解法欢迎评论。</p><a id="more"></a><h3 id="场景"><a href="#场景" class="headerlink" title="场景"></a>场景</h3><p>我们系统有一个去重的需求,但是查重的维度是多方面的,举个例子,若干个用户用了用一个手机号,那么我们就认为他们是同一个人,用手机号码这个条件可以查出一组这样的人。同理用Email也是。</p><p>但是存在一个问题就是用手机号查出了5组人,用Email查出了3组人,最后我们可以认为重复的人是8吗?其实是不行的,因为有可能某几个人的手机号和Email都是一样的,那么在两次查询后都会将这些人纳入到统计中。所以最后统计出的结果应该是<=各次查询结果之和的。</p><h3 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h3><p>首先是数据结构,每次查询出来的结构是一个List,那么List里面其实又是一组重复的人。</p><p>为了方便理解,我们可以定义一个最小化结构为Group,</p><figure class="highlight java"><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 class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Group</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> List<Long> duplications = <span class="keyword">new</span> ArrayList<>();</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>里面的duplicates代表原始记录ID列表,举例说就是ID=13和ID=15的两条记录都是用的同一个手机号,那么duplications就是{13,15}。</p><p>每通过一个条件查询可以得到一个List<Group>的返回,很好理解,这个List的size就是说明有多少人用了相同的手机号。那么用Email查询的话结果就是代表多少人用了相同的Email。</p><p>假设ID=13的这条记录,它已经在用手机号查询的结果中被GroupA收录,如果它又在Email查询的结果中被GroupF收录的话,说明了什么问题?说明其实GroupA和GroupF应该取个合集,他们都是代表了同一个人。有点类似消消乐的意思。我们最后其实不管是通过什么条件查出来的,只要是一个Group的集合就好了。</p><figure class="highlight java"><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 class="comment">// 先定义一个篮子</span></span><br><span class="line">List<Group> duplicateBucket = <span class="keyword">new</span> ArrayList<>();</span><br><span class="line"><span class="keyword">for</span> (<span class="comment">// 若干条件) {</span></span><br><span class="line"> List<Group> duplicates = queryProvider.queryDuplications();</span><br><span class="line"> duplicateBucket.addAll(duplicates);</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 对这个篮子做一次去重</span></span><br><span class="line">DeduplicationUtils.intersection(duplicateBucket);</span><br></pre></td></tr></table></figure><p>再看这个去重的逻辑</p><figure class="highlight java"><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">intersection</span><span class="params">(List<Grouop> duplicates)</span> </span>{</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> i = duplicates.size() - <span class="number">2</span>; i >= <span class="number">0</span>; i--) {</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> j = duplicates.size() - <span class="number">1</span>; j > i; j--) {</span><br><span class="line"> Set<Long> setA = duplicates.get(i).toSet();</span><br><span class="line"> Set<Long> setB = duplicates.get(j).toSet();</span><br><span class="line"> Set intersection = Sets.intersection(setA, setB);</span><br><span class="line"> <span class="keyword">if</span> (intersection.size() > <span class="number">0</span>) {</span><br><span class="line"> <span class="comment">// 找出差集做合并</span></span><br><span class="line"> List<Long> differences = <span class="keyword">new</span> ArrayList<>();</span><br><span class="line"> <span class="keyword">for</span> (Long id : duplicates.get(j).getDuplications()) {</span><br><span class="line"> <span class="keyword">if</span> (intersection.contains(id)) {</span><br><span class="line"> <span class="keyword">continue</span>;</span><br><span class="line"> }</span><br><span class="line"> differences.add(id);</span><br><span class="line"> }</span><br><span class="line"> duplicates.get(i).addAllDuplications(differences);</span><br><span class="line"> duplicates.remove(j);</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>这里是关键,实际上就是双层遍历做对比,发现重复条目就做消消乐,让后者融合进前者的集合中去。</p><h3 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h3><p>这样就基本解决了去重的问题,但是效率一般,毕竟有双重循环在。如有更好的办法欢迎留言。</p>]]></content>
<summary type="html">
<p>今天说一个算法小甜点,因为对于算法知之甚少,所以完全是自己揣摩出来的一则,写出来只是个记录,如有更学术派的解法欢迎评论。</p>
</summary>
<category term="算法" scheme="http://www.deanwangpro.com/tags/%E7%AE%97%E6%B3%95/"/>
<category term="Collection" scheme="http://www.deanwangpro.com/tags/Collection/"/>
</entry>
<entry>
<title>Activiti系列一之delegate拦截器</title>
<link href="http://www.deanwangpro.com/2017/09/11/activiti-delegate-interceptor/"/>
<id>http://www.deanwangpro.com/2017/09/11/activiti-delegate-interceptor/</id>
<published>2017-09-10T16:00:00.000Z</published>
<updated>2017-10-04T07:55:50.000Z</updated>
<content type="html"><![CDATA[<p>公司内部的工作流引擎用的是Activiti5, 所以这半年一直在研究这个开源项目,打算针对这个项目做一个系列,说一说使用心得。今天就先做系列一,先说使用场景。</p><a id="more"></a><h3 id="基本原理"><a href="#基本原理" class="headerlink" title="基本原理"></a>基本原理</h3><p>Activiti可以很好的与Spring结合,只需要使用<code>SpringProcessEngineConfiguration</code>配置就可以利用Spring管理Bean,所以在BPMN的标准中Activiti的扩展属性都是可以使用Spring Bean的。</p><p>例如Service Task,</p><figure class="highlight xml"><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"><span class="tag"><<span class="name">serviceTask</span> <span class="attr">id</span>=<span class="string">"N0db890f126c2"</span> <span class="attr">name</span>=<span class="string">"Service Task"</span> <span class="attr">activiti:delegateExpression</span>=<span class="string">"#{serviceTaskDelegate}"</span>></span></span><br><span class="line"><span class="tag"></<span class="name">serviceTask</span>></span></span><br></pre></td></tr></table></figure><p>在<code>activiti:delegateExpression</code>中使用的就是一个Spring Bean,这个Bean实际上是一个JavaDelegate的实现。</p><figure class="highlight java"><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"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ServiceTaskDelegate</span> <span class="keyword">implements</span> <span class="title">JavaDelegate</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> ContactGroupService contactGroupService;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">executeDelegate</span><span class="params">(DelegateExecution execution, ContactDTO contactDTO)</span> </span>{</span><br><span class="line"> LOGGER.info(<span class="string">"start execute action..."</span>); </span><br><span class="line"> LOGGER.info(<span class="string">"end execute add group action..."</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这个Bean和其他Spring Bean没有区别,可以注入其他Service,自己也会在Spring上下文中加载。</p><h3 id="场景"><a href="#场景" class="headerlink" title="场景"></a>场景</h3><p>接下来深入的一步讨论就是如何处理复杂的事务,一个工作流中包含若干个上述的Service Task,那么究竟是一个Task失败,整个流程就回滚还是一个失败之后,流程停止在失败的地方然后重试呢?</p><p>这些都需要根据具体的业务场景来处理,Activiti默认采取第一种办法直接全部回滚。在我们的场景中做了一些改动,当出现重试可解决的异常时全部回滚,整个流程等待若干秒后再次重试,当出现重试也无法解决的异常时,例如超出API调用次数之类的,流程直接失败并记录状态。</p><p>另外在单个ServiceTask的运行过程中,我们采取了新开一个事务的办法,避免与Activiti自身事务互相影响,另一方面也可以复用我们系统多租户的拦截器来处理复杂的数据库查询(非框架程序员不用处理租户相关的代码)。</p><h3 id="解决方法"><a href="#解决方法" class="headerlink" title="解决方法"></a>解决方法</h3><p>仔细阅读Activiti源码,发现在配置类中有一个<code>processEngineConfiguration.setDelegateInterceptor</code>方法,这个拦截器是在具体的Delegate启动之前调用的,所以就给了我们一个时机切入到业务逻辑之前。</p><p>不含糊,直接贴代码</p><figure class="highlight java"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">CustomDelegateInterceptor</span> <span class="keyword">implements</span> <span class="title">DelegateInterceptor</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> Logger LOGGER = LoggerFactory.getLogger(ActivitiWorkflowManager<span class="class">.<span class="keyword">class</span>)</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> TenantResolver tenantResolver;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">CustomDelegateInterceptor</span><span class="params">(TenantResolver tenantResolver)</span> </span>{</span><br><span class="line"> <span class="keyword">this</span>.tenantResolver = tenantResolver;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">handleInvocation</span><span class="params">(DelegateInvocation invocation)</span> <span class="keyword">throws</span> Exception </span>{</span><br><span class="line"> Object target = invocation.getTarget();</span><br><span class="line"> <span class="keyword">if</span> (target <span class="keyword">instanceof</span> JavaDelegate) {</span><br><span class="line"> Field executionField = ReflectionUtils.findField(JavaDelegateInvocation.class, "execution", DelegateExecution.class);</span><br><span class="line"> ReflectionUtils.makeAccessible(executionField);</span><br><span class="line"> DelegateExecution execution = (DelegateExecution) ReflectionUtils.getField(executionField, invocation);</span><br><span class="line"> <span class="comment">// 异步任务需要设置TenantId</span></span><br><span class="line"> LOGGER.info(<span class="string">"Executing activiti service task tenantId [{}]"</span>, execution.getTenantId());</span><br><span class="line"> <span class="keyword">if</span> (StringUtils.isBlank(execution.getTenantId())) {</span><br><span class="line"> LOGGER.error(<span class="string">"do not have tenantId, skipped"</span>);</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> tenantResolver.setCurrentTenant(Long.valueOf(execution.getTenantId()));</span><br><span class="line"> String traceId = execution.getProcessBusinessKey() + <span class="string">":"</span> + execution.getProcessInstanceId();</span><br><span class="line"> LogTraceUtils.beginTrace(traceId);</span><br><span class="line"> invocation.proceed();</span><br><span class="line"> } <span class="keyword">finally</span> {</span><br><span class="line"> tenantResolver.clear();</span><br><span class="line"> LogTraceUtils.endTrace();</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> invocation.proceed();</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>在每个Service Task执行前,都会获取当前这个流程的租户ID并且写入到一个专门管理租户ID的ThreadLocal中。并且切入了一段日志逻辑方便排错。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>有些时候就是这样一个不经意的小函数就可以优雅的实现一个类似AOP的拦截。往往这种小函数在文档中是只字不提的,可见对于开源项目源码阅读的重要性。</p>]]></content>
<summary type="html">
<p>公司内部的工作流引擎用的是Activiti5, 所以这半年一直在研究这个开源项目,打算针对这个项目做一个系列,说一说使用心得。今天就先做系列一,先说使用场景。</p>
</summary>
<category term="BPMN" scheme="http://www.deanwangpro.com/tags/BPMN/"/>
<category term="Activiti" scheme="http://www.deanwangpro.com/tags/Activiti/"/>
</entry>
<entry>
<title>短链接的简单实现</title>
<link href="http://www.deanwangpro.com/2017/08/20/dwz/"/>
<id>http://www.deanwangpro.com/2017/08/20/dwz/</id>
<published>2017-08-19T16:00:00.000Z</published>
<updated>2017-09-11T15:56:56.000Z</updated>
<content type="html"><![CDATA[<p>很久没更新了,加入了创业公司,从无到有的构建一套产品,从一开始的码框架到现在思考如何优化产品架构,适应更多的弹性需求和更大的数据量。时间真的是不够用,几乎快半年没有更新博客。近期产品慢慢走上正轨,又有小伙伴加入,所以抽出点时间总结一下这小半年内遇到的坑和走过的路。</p><a id="more"></a><p>说一说最近做的一个短链服务,短链服务其实很简单,其实就是一张表或者说是一个map:<br>+——-+———————–+<br>| 短链 | 长链 |<br>+——-+———————–+<br>| 7Y65s | <a href="https://www.google.com" target="_blank" rel="noopener">https://www.google.com</a> |<br>+——-+———————–+</p><p>就是这样一个结构,当用户访问 your-domain/7Y65s, 去表里查询一下对应的长链,301到该链接就可以了。</p><h3 id="优化"><a href="#优化" class="headerlink" title="优化"></a>优化</h3><p>基本的思路确定后,我们一点一点来优化。</p><p>首先分析大量查询肯定会出现在通过短链找长链这段逻辑中,如果用数据库肯定影响效率,缓存势在必行,而且一旦短链生成基本是不会变的,所以也不存在失效问题(这里可能会有个批量Archive的处理)。在我的项目中,我选择了redis作为缓存,反正是key-value的其他缓存组件肯定也能做。选用redis的原因是还可以用INCR来统计访问量。</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">redisTemplate.opsForHash().put("short-urls:" + slug, "long_url", url);</span><br><span class="line">redisTemplate.opsForHash().increment("short-urls:" + slug, VISITS_FIELD, 1L);</span><br></pre></td></tr></table></figure><p>查询这一侧做完,到了生成短链这一块,这里实际上是刚刚说的那张表的create操作。这里有个基本逻辑是:当一个长链还没有短链的时候,我们生成它并返回,如果已经存在,直接获取返回。</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><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">package io.naza.urlshortener.generator;</span><br><span class="line"></span><br><span class="line">import org.apache.commons.codec.digest.DigestUtils;</span><br><span class="line"></span><br><span class="line">public class UrlShortHelper {</span><br><span class="line"></span><br><span class="line"> private final static int LENGTH = 6;</span><br><span class="line"></span><br><span class="line"> private static char[] DIGITS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toCharArray();</span><br><span class="line"></span><br><span class="line"> public static String[] shorten(String url) {</span><br><span class="line"> String key = "SECRET"; // 自定义生成MD5加密字符串前的混合KEY</span><br><span class="line"> String hex = DigestUtils.md5Hex(key + url);</span><br><span class="line"> int hexLen = hex.length();</span><br><span class="line"> int subHexLen = hexLen / 8;</span><br><span class="line"> String[] shortStr = new String[subHexLen];</span><br><span class="line"></span><br><span class="line"> for (int i = 0; i < subHexLen; i++) {</span><br><span class="line"> StringBuilder outChars = new StringBuilder();</span><br><span class="line"> int j = i + 1;</span><br><span class="line"> String subHex = hex.substring(i * 8, j * 8);</span><br><span class="line"> long idx = Long.valueOf("3FFFFFFF", 16) & Long.valueOf(subHex, 16);</span><br><span class="line"> for (int k = 0; k < LENGTH; k++) {</span><br><span class="line"> int index = (int) (Long.valueOf("0000003D", 16) & idx);</span><br><span class="line"> outChars.append(DIGITS[index]);</span><br><span class="line"> idx = idx >> 5;</span><br><span class="line"> }</span><br><span class="line"> shortStr[i] = outChars.toString();</span><br><span class="line"> }</span><br><span class="line"> return shortStr;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>项目中按照长链生成一个固定长度为6位的短链接,只要长链是一样的,那么每次生成的短链也一样。</p><p>考虑到每次生成短链前要查询一下长链是不是存在,继续加一个小缓存,这次反过来,长链作key,短链做value。</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">redisTemplate.opsForHash().put("short-urls", url, slug);</span><br></pre></td></tr></table></figure><h3 id="尾声"><a href="#尾声" class="headerlink" title="尾声"></a>尾声</h3><p>用比较简单的方式完成一个短链接服务,在做短链跳转的过程中可以分析用户的各类行为,比如操作系统,浏览器版本,地域等等。最后说两点要注意的:</p><ol><li>redis毕竟是作为缓存使用的,建议数据还是要落一次在DB,比如在生成短链时,写入到DB一份。这样当redis挂了,还可以从DB中全量恢复。</li><li>对于长时间没有PV的短链,比如超过1年没有PV,需要做批量清理,一般一个月一次就可以了。 </li></ol>]]></content>
<summary type="html">
<p>很久没更新了,加入了创业公司,从无到有的构建一套产品,从一开始的码框架到现在思考如何优化产品架构,适应更多的弹性需求和更大的数据量。时间真的是不够用,几乎快半年没有更新博客。近期产品慢慢走上正轨,又有小伙伴加入,所以抽出点时间总结一下这小半年内遇到的坑和走过的路。</p>
</summary>
<category term="Java" scheme="http://www.deanwangpro.com/tags/Java/"/>
<category term="Redis" scheme="http://www.deanwangpro.com/tags/Redis/"/>
</entry>
<entry>
<title>Spring boot监控初探</title>
<link href="http://www.deanwangpro.com/2017/03/22/spring-boot-monitor/"/>
<id>http://www.deanwangpro.com/2017/03/22/spring-boot-monitor/</id>
<published>2017-03-21T16:00:00.000Z</published>
<updated>2017-03-26T14:10:10.000Z</updated>
<content type="html"><![CDATA[<p>最近对devOps这个话题有点兴趣,所以研究了一下monitor相关的开源项目,翻到medium上的<a href="https://medium.com/@brunosimioni/near-real-time-monitoring-charts-with-spring-boot-actuator-jolokia-and-grafana-1ce267c50bcc#.il5xmlnv7" target="_blank" rel="noopener">一篇文章</a>,而且实际项目中也曾看到devOps组的同事搭过类似的监控,就想过把瘾,了解一下监控可视化。</p><a id="more"></a><h3 id="被监控服务配置"><a href="#被监控服务配置" class="headerlink" title="被监控服务配置"></a>被监控服务配置</h3><p>本地正好有spring-boot的项目,并且也依赖了<code>jolokia</code>(主要就是为了把JMX的mbean通过HTTP暴露出去)<br>项目配置也少不了</p><figure class="highlight yml"><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 class="attr">endpoints:</span></span><br><span class="line"> <span class="attr">enabled:</span> <span class="literal">true</span></span><br><span class="line"> <span class="attr">jmx:</span></span><br><span class="line"> <span class="attr">enabled:</span> <span class="literal">true</span></span><br><span class="line"> <span class="attr">jolokia:</span></span><br><span class="line"> <span class="attr">enabled:</span> <span class="literal">true</span></span><br><span class="line"></span><br><span class="line"><span class="attr">management:</span></span><br><span class="line"> <span class="attr">security:</span></span><br><span class="line"> <span class="attr">enabled:</span> <span class="literal">false</span></span><br></pre></td></tr></table></figure><p>访问一下URL看看是不是ok</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">http://localhost:8080/jolokia/read/org.springframework.boot:name=metricsEndpoint,type=Endpoint/Data</span><br></pre></td></tr></table></figure><h3 id="搭建监控系统"><a href="#搭建监控系统" class="headerlink" title="搭建监控系统"></a>搭建监控系统</h3><p>如果能看到数据,说明server端配置没问题了,下面我们怎么搭建Telegraf + InfluxDB + Grafana呢,这个三个组件是这么配合的,Telegraf实际就是收集信息的,比如每隔10s访问一次上面那个URL得到metrics,收集到的数据存到InfluxDB,然后Grafana做数据可视化。<br>但是如果纯手动安装实在太麻烦,求助万能的github,找到一个非常棒的项目(<a href="https://github.com/samuelebistoletti/docker-statsd-influxdb-grafana" target="_blank" rel="noopener">https://github.com/samuelebistoletti/docker-statsd-influxdb-grafana</a>), 直接fork然后修改一些配置就可以为自己的项目服务了。如果你不了解相关配置可以先直接run起来,然后通过ssh进去一探究竟。</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">ssh root@localhost -p 22022</span><br></pre></td></tr></table></figure><p>配置方面,主要是要修改Telegraf的,因为它是对接不同项目的,你需要收集什么样的信息,比如cpu,disk,net等等都要在Telegraf里配。简单起见,我只设置了三个输入。</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"># /etc/telegraf/telegraf.conf</span><br><span class="line">[[inputs.jolokia]]</span><br><span class="line"> context = "/jolokia"</span><br><span class="line"></span><br><span class="line">[[inputs.jolokia.servers]]</span><br><span class="line"> name = "springbootapp"</span><br><span class="line"> host = "{app ip address}"</span><br><span class="line"> port = "8080"</span><br><span class="line"></span><br><span class="line">[[inputs.jolokia.metrics]]</span><br><span class="line"> name = "metrics"</span><br><span class="line"> mbean = "org.springframework.boot:name=metricsEndpoint,type=Endpoint"</span><br><span class="line"> attribute = "Data"</span><br><span class="line"> </span><br><span class="line">[[inputs.jolokia.metrics]]</span><br><span class="line"> name = "tomcat_max_threads"</span><br><span class="line"> mbean = "Tomcat:name=\"http-nio-8080\",type=ThreadPool"</span><br><span class="line"> attribute = "maxThreads"</span><br><span class="line"></span><br><span class="line">[[inputs.jolokia.metrics]]</span><br><span class="line"> name = "tomcat_current_threads_busy"</span><br><span class="line"> mbean = "Tomcat:name=\"http-nio-8080\",type=ThreadPool"</span><br><span class="line"> attribute = "currentThreadsBusy"</span><br></pre></td></tr></table></figure><p>其实就是spring-boot标准的metrics以及tomcat的Threads。<br>完成之后重启服务<code>/etc/init.d/telegraf restart</code></p><h3 id="查看监控数据"><a href="#查看监控数据" class="headerlink" title="查看监控数据"></a>查看监控数据</h3><p>我们访问InfluxDB看看有数据了没有<code>http://localhost:3004/</code>,切换数据库到Telegraf。输入以下命令试试吧</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">SHOW MEASUREMENTS</span><br><span class="line">SELECT * FROM jolokia</span><br><span class="line">SELECT * FROM cpu</span><br><span class="line">SELECT * FROM mem</span><br><span class="line">SELECT * FROM diskio</span><br></pre></td></tr></table></figure><p>比如输入<code>SELECT * FROM jolokia</code>就能看到spring-boot暴露了哪些数据,从time列也可以看出Telegraf是每隔10s收集一次,太频繁了对server也是压力。<br><img src="/images/Jolokia.png" alt="Jolokia"></p><p>上面基本涵盖了cpu,内存和存储的一些metrics。<br>其实也可以配置网络相关的,感兴趣的可以看官方的telegraf.conf,里面有配置[[inputs.net]]的例子。</p><h3 id="数据可视化"><a href="#数据可视化" class="headerlink" title="数据可视化"></a>数据可视化</h3><p>数据有了,下一步就是可视化。<br>按照Github上面说的进入<code>http://localhost:3003/</code>,</p><ol><li>Using the wizard click on <code>Add data source</code></li><li>Choose a <code>name</code> for the source and flag it as <code>Default</code></li><li>Choose <code>InfluxDB</code> as <code>type</code></li><li>Choose <code>direct</code> as <code>access</code></li><li>Fill remaining fields as follows and click on <code>Add</code> without altering other fields</li></ol><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">Url: http://localhost:8086</span><br><span class="line">Database:telegraf</span><br><span class="line">User: telegraf</span><br><span class="line">Password:telegraf</span><br></pre></td></tr></table></figure><p>添加好InfluxDB后,新建一个Dashboard,然后快速的ADD几个Graph来。<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></pre></td><td class="code"><pre><span class="line">SELECT MEAN(usage_system) + MEAN(usage_user) AS cpu_total FROM cpu WHERE $timeFilter GROUP BY time($interval)</span><br><span class="line"></span><br><span class="line">SELECT mean("total") as "total" FROM "mem" WHERE $timeFilter GROUP BY time($interval) fill(null)</span><br><span class="line">SELECT mean("used") as "used" FROM "mem" WHERE $timeFilter GROUP BY time($interval) fill(null)</span><br><span class="line"></span><br><span class="line">SELECT mean("metrics_heap.used") as "heap_usage" FROM "jolokia" WHERE $timeFilter GROUP BY time($interval) fill(null)</span><br></pre></td></tr></table></figure><p>第一张是CPU占用率;第二张是内存占用情况,绿线是Total,黄线是Used;第三张是jolokia提供的jvm heap的使用,可以到看到GC的情况。</p><p><img src="/images/Grafana.png" alt="Grafana"></p><p>刚才还配置了Tomcat的收集,想看Tomcat的Thread情况也是妥妥的。</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">SELECT mean("tomcat_max_threads") FROM "jolokia" WHERE $timeFilter GROUP BY time($interval) fill(null)</span><br><span class="line">SELECT mean("tomcat_current_threads_busy") FROM "jolokia" WHERE $timeFilter GROUP BY time($interval) fill(null)</span><br></pre></td></tr></table></figure><p><img src="/images/tomcat.png" alt="tomcat"></p><h3 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h3><p>可以看到搭建这样一套环境其实很快,原理也并不复杂,监控数据可视化的难点在于</p><ul><li>哪些metrics需要监控</li><li>哪些metrics需要配合起来可以判断问题,比如diskio+net是不是可以判断系统整体IO的瓶颈。</li></ul><p>这都是需要多年的经验总结才能获得的,我还是菜鸟一枚,再接再厉。</p>]]></content>
<summary type="html">
<p>最近对devOps这个话题有点兴趣,所以研究了一下monitor相关的开源项目,翻到medium上的<a href="https://medium.com/@brunosimioni/near-real-time-monitoring-charts-with-spring-boot-actuator-jolokia-and-grafana-1ce267c50bcc#.il5xmlnv7" target="_blank" rel="noopener">一篇文章</a>,而且实际项目中也曾看到devOps组的同事搭过类似的监控,就想过把瘾,了解一下监控可视化。</p>
</summary>
<category term="Spring" scheme="http://www.deanwangpro.com/tags/Spring/"/>
<category term="DevOps" scheme="http://www.deanwangpro.com/tags/DevOps/"/>
</entry>
<entry>
<title>说一说微信第三方平台的初步集成</title>
<link href="http://www.deanwangpro.com/2017/03/20/wechat-open-platform/"/>
<id>http://www.deanwangpro.com/2017/03/20/wechat-open-platform/</id>
<published>2017-03-19T16:00:00.000Z</published>
<updated>2017-03-22T03:17:42.000Z</updated>
<content type="html"><![CDATA[<p>微信火了这么久,这两周第一次从一个开发者的角度来研究微信的生态系统而不是应用本身。现在做国内的项目或者产品难免都需要集成微信,其实现在微信背后的支撑平台已经是非常繁杂的了:</p><ul><li>公众平台(订阅号、服务号、企业号、小程序)</li><li>开放平台(网页应用、移动应用、公众号第三方平台开发)</li><li>商户平台 (支付)</li><li>服务商平台(代支付)</li></ul><a id="more"></a><p>是不是感觉有点懵,这么多平台如何选择,还是需要根据自己系统的业务来看。我今天说的是开放平台的初步集成。</p><p>采用倒序的方式我们一步步来说:</p><ol><li><p>开放平台需要代公众号实现功能,就必须拿到公众号的<code>accessToken</code>,然后再去使用公众平台接口。那么获取<code>accessToken</code>的过程实际就是公众平台授权给你这个第三方开放平台的过程。相当于他给你了一把打开他家门的钥匙。详见<a href="https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1453779503&token=&lang=zh_CN" target="_blank" rel="noopener">官方文档</a>第5步.</p></li><li><p>获得钥匙的过程可是需要一些功夫的,因为老换锁啊,每7200s换一次,所以有位管理员大爷出现了,就是<code>refresh_token</code>,锁换了找大爷拿把新的就行。那么这个管理员大爷怎么找到的呢?是在第一次授权成功后会通过参数返回给你的一个<code>authorization_code</code>这个相当于是地图,能让你第一次在毫无经验的情况下找到第一把钥匙和管理员大爷,也就是官方文档的第4步。</p></li><li><p>想要找到地图,给公众平台的管理员发个链接吧<code>https://mp.weixin.qq.com/cgi-bin/componentloginpage?component_appid=xxxx&pre_auth_code=xxxxx&redirect_uri=xxxx</code>,redirect_uri就是把<code>authorization_code</code>给你的入口。例如你写的是<code>redirect_uri=mydomain.com/authrize/callback</code>,那么一旦授权成功,浏览器就会跳转到<code>mydomain.com/authrize/callback?auth_code=xxxx</code>上来,你就可以获取<code>authorization_code</code>这个地图了。</p></li><li><p>上一步的链接中有一个pre_auth_code那么这个值怎么来的?是通过<code>api_create_preauthcode</code>这个接口获得的。而调用这个接口又需要<code>component_access_token</code>,这个东东就是一个令牌,你作为第三方平台调用微信任何API都必须有这个令牌,获得这个令牌的办法就是用调用<code>api_component_token</code>通过<code>component_verify_ticket</code>去换。(仔细想一想,其实<code>component_verify_ticket</code>=<code>refresh_token</code>, <code>component_access_token</code>=<code>accessToken</code>)</p></li></ol><p>这里面涉及到的变量很多,特别需要注意一些的:</p><ul><li><code>component_verify_ticket</code> 这张门票是微信推送,大概每隔十分钟推一次。</li><li><code>api_component_token</code>刚才说了是用上面的那张门票换的,但是有保质期,2小时,那么能不能每次要调接口都用门票换一下?人家微信是有每天的接口调用次数限定的,所以建议用个cache缓存起来,到了1小时50分的时候让缓存失效,失效再去call API换。可以用<strong>redis</strong>的TTL实现。</li><li>类比的上文第1步提到的公众号的<code>accessToken</code>也是有保质期的,所以一定要保存好对应的<code>refresh_token</code>,到了1小时50分的时候再去换<code>accessToken</code>。</li></ul><p>一旦拿到公众号的<code>accessToken</code>,那么就可以像普通公众号的后台服务那样,比如获取粉丝列表啊,推送文章图片啊等等。</p><p>PS: 最好采用加解密算法来进行消息的接受和推送。这里面有一个坑:</p><blockquote><p>异常java.security.InvalidKeyException:illegal Key Size的解决方案:在官方网站下载JCE无限制权限策略文件</p></blockquote><p>另外根据<a href="http://mp.weixin.qq.com/wiki/17/2d4265491f12608cd170a95559800f2d.html" target="_blank" rel="noopener">官网提示</a>首次验证服务器地址的有效性,必须返回同样的<code>echostr</code>。</p>]]></content>
<summary type="html">
<p>微信火了这么久,这两周第一次从一个开发者的角度来研究微信的生态系统而不是应用本身。现在做国内的项目或者产品难免都需要集成微信,其实现在微信背后的支撑平台已经是非常繁杂的了:</p>
<ul>
<li>公众平台(订阅号、服务号、企业号、小程序)</li>
<li>开放平台(网页应用、移动应用、公众号第三方平台开发)</li>
<li>商户平台 (支付)</li>
<li>服务商平台(代支付)</li>
</ul>
</summary>
<category term="Java" scheme="http://www.deanwangpro.com/tags/Java/"/>
<category term="wechat" scheme="http://www.deanwangpro.com/tags/wechat/"/>
</entry>
</feed>