-
Notifications
You must be signed in to change notification settings - Fork 1
/
15277835170976.html
659 lines (423 loc) · 43.9 KB
/
15277835170976.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
<!doctype html>
<html class="no-js" lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>
蚂蚁通信框架实践 - Junkman
</title>
<link href="atom.xml" rel="alternate" title="Junkman" type="application/atom+xml">
<link rel="stylesheet" href="asset/css/foundation.min.css" />
<link rel="stylesheet" href="asset/css/docs.css" />
<script src="asset/js/vendor/modernizr.js"></script>
<script src="asset/js/vendor/jquery.js"></script>
<script src="asset/highlightjs/highlight.pack.js"></script>
<link href="asset/highlightjs/styles/github.css" media="screen, projection" rel="stylesheet" type="text/css">
<script>hljs.initHighlightingOnLoad();</script>
<script type="text/javascript">
function before_search(){
var searchVal = 'site:panlw.github.io ' + document.getElementById('search_input').value;
document.getElementById('search_q').value = searchVal;
return true;
}
</script>
</head>
<body class="antialiased hide-extras">
<div class="marketing off-canvas-wrap" data-offcanvas>
<div class="inner-wrap">
<nav class="top-bar docs-bar hide-for-small" data-topbar>
<section class="top-bar-section">
<div class="row">
<div style="position: relative;width:100%;"><div style="position: absolute; width:100%;">
<ul id="main-menu" class="left">
<li id=""><a target="self" href="index.html">Home</a></li>
<li id=""><a target="_self" href="archives.html">Archives</a></li>
</ul>
<ul class="right" id="search-wrap">
<li>
<form target="_blank" onsubmit="return before_search();" action="http://google.com/search" method="get">
<input type="hidden" id="search_q" name="q" value="" />
<input tabindex="1" type="search" id="search_input" placeholder="Search"/>
</form>
</li>
</ul>
</div></div>
</div>
</section>
</nav>
<nav class="tab-bar show-for-small">
<a href="javascript:void(0)" class="left-off-canvas-toggle menu-icon">
<span> Junkman</span>
</a>
</nav>
<aside class="left-off-canvas-menu">
<ul class="off-canvas-list">
<li><a href="index.html">HOME</a></li>
<li><a href="archives.html">Archives</a></li>
<li><a href="about.html">ABOUT</a></li>
<li><label>Categories</label></li>
<li><a href="Infra.html">Infra</a></li>
<li><a href="Coding.html">Coding</a></li>
<li><a href="Modeling.html">Modeling</a></li>
<li><a href="Archtecting.html">Archtecting</a></li>
</ul>
</aside>
<a class="exit-off-canvas" href="#"></a>
<section id="main-content" role="main" class="scroll-container">
<script type="text/javascript">
$(function(){
$('#menu_item_index').addClass('is_active');
});
</script>
<div class="row">
<div class="large-8 medium-8 columns">
<div class="markdown-body article-wrap">
<div class="article">
<h1>蚂蚁通信框架实践</h1>
<div class="read-more clearfix">
<span class="date">2018/6/1</span>
<span>posted in </span>
<span class="posted-in"><a href='Network.html'>Network</a></span>
<span class="comments">
</span>
</div>
</div><!-- article -->
<div class="article-content">
<blockquote>
<p><a href="https://mp.weixin.qq.com/s?__biz=MzUzMzU5Mjc1Nw==&mid=2247483812&idx=1&sn=580d9003b01b3c5dab821c04a97b77cb&chksm=faa0ee7ecdd767684e66a7844270ca1d0cc94b05719b39068d161e2e56560d00167efd753859&mpshare=1&scene=23&srcid=0531OaPJuQOHngeGUjgDXvfG%23rd">原文地址</a></p>
</blockquote>
<pre><code>**原创声明**:本文系作者原创,谢绝个人、媒体、公众号或网站未经授权转载,违者追究其法律责任。
</code></pre>
<h2 id="toc_0"><strong>前 言</strong></h2>
<p>互联网领域的通信技术,有各式各样的通信协议可以选择,比如基于 TCP/IP 协议簇的 HTTP(1/2)、SPDY 协议、WebSocket、Google 基于 UDP 的 QUIC 协议等。这些协议,都有完整的报文格式与字段定义,对安全,序列化机制,数据压缩机制,CRC 校验机制等各种通信细节都有较好的设计。能够高效、稳定、且安全地运行在公网环境。</p>
<p>而对于私网环境,比如一个公司的 IDC 内部,如果所有应用的节点间,全部通过标准协议来通信,会有很多问题:比如研发效率方面的影响,我们的研发框架,需要做大量业务数据转化成标准协议的工作;再比如升级兼容性,标准协议的字段众多,版本各异,兼容性也得不到保障;除此还有无用字段的传输,也会造成资源浪费,功能定制也可能不那么灵活。而解决这些问题,比较常见的做法就是自己来设计协议,可以自己来定义字段,制定升级方式,可插拔可开关的特性需求等,我们把这样的协议叫做私有通信协议。</p>
<p>在蚂蚁金服的分布式技术体系下,我们大量的技术产品(非网关类产品),都需要在内网,进行节点间通信。高吞吐、高并发的通信,数量众多的连接管理(C10K 问题),便捷的升级机制,兼容性保障,灵活的线程池模型运用,细致的异常处理与日志埋点等,这些功能都需要在通信协议和实现框架上做文章。本文主要从如下几个方面来对蚂蚁通信框架实践之路进行介绍:</p>
<ol>
<li> 私有通信协议设计</li>
<li> 基础通信功能设计要点分析</li>
<li> 私有通信协议设计举例</li>
<li> 蚂蚁自研通信框架 Bolt</li>
</ol>
<h2 id="toc_1"><strong>私有通信协议设计</strong></h2>
<p>我们的分布式架构,所需要的内部通信模块,采用了私有协议来设计和研发。当然私有协议,也是有很多弊端的,比如在通用性上、公网传输的能力上相比标准协议会有劣势。然而,我们为了最大程度的提升性能,降低成本,提高灵活性与效率,最佳选择还是高度定制化的私有协议:</p>
<ul>
<li> 可以有效地利用协议里的各个字段</li>
<li> 灵活满足各种通信功能需求:比如 CRC 校验,Server Fail-Fast 机制,自定义序列化器</li>
<li> 最大程度满足性能需求:IO 模型与线程模型的灵活运用</li>
</ul>
<p>比如一个典型的 Request-Response 通信场景:</p>
<ol>
<li> 在一个通信节点上,如何把一个请求对象,序列化成字节流,通过怎样的网络传输方式,传递到另一个节点</li>
<li> 在对端的通信节点上,需要高效的读取字节流,并反序列化成原始的请求对象,然后根据请求内容,做一些逻辑处理。处理完成后,响应返回。</li>
<li> 同时,此时要考虑,如何充分利用网络 IO、CPU 以及内存,来保证吞吐和处理效率的最优。</li>
</ol>
<p>文章后面的内容,比较清晰地介绍了这个通信场景的设计与实现方案。</p>
<p><img src="media/15277835170976/15277842914150.jpg" alt=""/><br/>
图1 - 私有协议与必要的功能模块</p>
<p>首先协议设计上,我们需要考虑的几个关键问题:</p>
<p><strong>Protocol</strong></p>
<ul>
<li> 协议应该包括哪些必要字段与主要业务负载字段:协议里设计的每个字段都应该被使用到,避免无效字段;</li>
<li> 需要考虑通信功能特性的支持:比如CRC校验,安全校验,数据压缩机制等;</li>
<li> 需要考虑协议的可扩展性:充分评估现有业务的需求,设计一个通用,扩展性高的协议,避免经常对协议进行修改;</li>
<li> 需要考虑协议的升级机制:毕竟是私有协议,没有长期的验证,字段新增或者修改,是有可能发生的,因此升级机制是必须考虑的;</li>
</ul>
<p><strong>Encoder 与 Decoder</strong></p>
<ul>
<li> 协议相关的编解码方式:私有协议需要有核心的encode与decode过程,并且针对业务负载能支持不同的序列化与反序列化机制。这部分,不同的私有协议,由于字段的差异,核心encode和decode过程是不一样的,因此需要分开考虑</li>
</ul>
<p><strong>Heartbeat</strong></p>
<ul>
<li> 协议相关的心跳触发与处理:不同的协议对心跳的需求,处理逻辑也可能是不同的。因此心跳的触发逻辑,心跳的处理逻辑,也都需要单独考虑。</li>
</ul>
<p><strong>Command 与 Command Handler</strong></p>
<ul>
<li> 可扩展的命令与命令处理器管理
<img src="media/15277835170976/15277843020616.jpg" alt=""/>
图2 - 通信命令设计举例</li>
<li> 负载命令:一般传输的业务的具体数据,比如带着请求参数,响应结果的命令;</li>
<li> 控制命令:一些功能管理命令,心跳命令等,它们通常完成一些复杂的分布式跨节点的协调功能,以此来保证负载命令通信过程的稳定,是必不可少的一部分。</li>
<li> 协议的通信过程,会有各种命令定义,逻辑上,我们把传输业务具体负载的请求对象,叫做负载命令(Payload Command),另一种叫做控制命令(Control Command),比如一些功能管理命令,或者心跳命令。</li>
<li> 定义了通信命令,我们还需要定义命令处理器,用来编写各个命令对应的业务处理逻辑。同时,我们需要保存命令与命令处理器的映射关系,以便在处理阶段,走到正确的处理器。</li>
</ul>
<p>有了私有协议的设计要点,我们接下来分两部分来介绍下实现:基础通信模块与私有协议设计举例。</p>
<p>首先是基础通信功能模块的实现,这部分沉淀了我们的一些优化和最佳实践,可以被不同的私有协议复用。</p>
<h2 id="toc_2"><strong>基础通信功能设计要点分析</strong></h2>
<p>蚂蚁的中间件产品,主要是 Java 语言开发,如果通信产品直接用原生的 Java NIO 接口开发,工作量相当庞大。通常我们会选择一些基础网络编程框架,而在基础网络通信框架上,我们也经历了自研(比如伯岩的 Gecko)、基于 Apache Mina 实现。最终,由于 Netty 在网络编程领域的出色表现,我们逐步切换到了 Netty 上。</p>
<p>Netty 在 2008 年就发布了<code>3.0.0</code> 版本,到现在已经经历了 10 年多的发展。而且从 <code>4.x</code> 之后的版本,把无锁化的设计理念放在第一位,然后针对内存分配,高效的 Queue 队列,高吞吐的超时机制等,做了各种细节优化。同时 Netty 的核心 Committer 与社区非常活跃,如果发现了缺陷能够及时得到修复。所有这些,使得 Netty 性能非常的出色和稳定,成为当下 Java 领域最优秀的网络通信组件。接下来主要介绍我们对 Netty 的学习经验,内部使用上的一些最佳实践。</p>
<h3 id="toc_3">1. 网络 IO 模型与线程模型</h3>
<p><img src="media/15277835170976/15277843394656.jpg" alt=""/><br/>
图3 - Netty与Reactor</p>
<p>如果你对 Java 网络 IO 这个话题感兴趣的话,肯定看过 Doug Lea 的《Scalable IO in Java》,在这个 PPT 里详细介绍了如何使用 Java NIO 的技术来实现 Douglas C. Schmidt 发表的 Reactor 论文里所描述的 IO 模型。针对这个高效的通信模型,Netty 做了非常友好的支持:</p>
<ul>
<li><p><strong>Reactor模型</strong></p>
<ul>
<li><p>我们只需要在初始化 <code>ServerBootstrap</code> 时,提供两个不同的 <code>EventLoopGroup</code> 实例,就实现了 Reactor 的主从模型。我们通常把处理建连事件的线程,叫做 BossGroup,对应 <code>ServerBootstrap</code> 构造方法里的 <code>parentGroup</code> 参数,即我们常说的 Acceptor 线程;处理已创建好的 <code>channel</code> 相关连 IO 事件的线程,叫做 WorkerGroup,对应 <code>ServerBootstrap</code> 构造方法里的 <code>childGroup</code> 参数,即我们常说的 IO 线程。</p></li>
<li><p>最佳实践:通常 <code>bossGroup</code> 只需要设置为 <code>1</code> 即可,因为 <code>ServerSocketChannel</code> 在初始化阶段,只会注册到某一个 <code>eventLoop</code> 上,而这个 <code>eventLoop</code> 只会有一个线程在运行,所以没有必要设置为多线程(什么时候需要多线程呢,可以参考 Norman Maurer 在 StackOverflow 上的这个回答);而 IO 线程,为了充分利用 CPU,同时考虑减少线上下文切换的开销,通常设置为 CPU 核数的两倍,这也是 Netty 提供的默认值。</p></li>
</ul></li>
<li><p><strong>串行化设计理念</strong></p>
<ul>
<li> Netty 从 <code>4.x</code> 的版本之后,所推崇的设计理念是串行化处理一个 <code>Channel</code> 所对应的所有 IO 事件和异步任务,单线程处理来规避并发问题。Netty 里的 <code>Channel</code> 在创建后,会通过 <code>EventLoopGroup</code> 注册到某一个 <code>EventLoop</code> 上,之后该 <code>Channel</code> 所有读写事件,以及经由 <code>ChannelPipeline</code> 里各个 <code>Handler</code> 的处理,都是在这一个线程里。一个 <code>Channel</code> 只会注册到一个 <code>EventLoop</code> 上,而一个 <code>EventLoop</code> 可以注册多个 <code>Channel</code> 。所以我们在使用时,也需要尽可能避免使用带锁的实现,能无锁化就无锁。</li>
<li> 最佳实践:<code>Channel</code> 的实现是线程安全的,因此我们通常在运行时,会保存一个 <code>Channel</code> 的引用,同时为了保持 Netty 的无锁化理念,也应该尽可能避免使用带锁的实现,尤其是在 <code>Handler</code> 里的处理逻辑。举个例子:这里会有一个比较特殊的容易死锁的场景,比如在业务线程提交异步任务前需要先抢占某个锁,<code>Handler</code> 里某个异步任务的处理也需要获取同一把锁。如果某一个时刻业务线程先拿到锁 lock1,同时 <code>Handler</code> 里由于事件机制触发了一个异步任务 A,并在业务线程提交异步任务之前,提交到了 <code>EventLoop</code> 的队列里。之后,业务线程提交任务 B,等待 B 执行完成后才能释放锁 lock1;而任务 A 在队列里排在 B 之前,先被执行,执行过程需要获取锁 lock1 才能完成。这样死锁就发生了,与常见的资源竞争不同,而是任务执行权导致的死锁。要规避这类问题,最好的办法就是不要加锁;如果实在需要用锁,需要格外注意 Netty 的线程模型与任务处理机制。</li>
</ul></li>
<li><p><strong>业务处理</strong></p>
<ul>
<li> IO 密集型的轻计算业务:此时线程的上下文切换消耗,会比 IO 线程的占用消耗更为突出,所以我们通常会建议在 IO 线程来处理请求;</li>
<li> CPU 密集型的计算业务:比如需要做远程调用,操作 DB 的业务,此时 IO 线程的占用远远超过线程上下文切换的消耗,所以我们就会建议在单独的业务线程池里来处理请求,以此来释放 IO 线程的占用。该模式,也是我们蚂蚁微服务,消息通信等最常使用的模型。该模式在后面的 RPC 协议实现举例部分会详细介绍。</li>
<li> 如文章开头所描述的场景,我们需要合理设计,来将硬件的 IO 能力,CPU 计算能力与内存结合起来,发挥最佳的效果。针对不同的业务类型,我们会选择不同的处理方式</li>
<li> 最佳实践:“Never block the event loop, reduce context-swtiching”,引自Netty committer Norman Maurer,另外阿里 HSF 的作者毕玄也有类似的总结。</li>
</ul></li>
<li><p><strong>其他实践建议</strong></p>
<ul>
<li> 最小化线程池,能复用 <code>EventLoopGroup</code> 的地方尽量复用。比如蚂蚁因为历史原因,有过两版 RPC 协议,在两个协议升级过渡期间,我们会复用 Acceptor 线程与 IO 线程在同一个端口处理不同协议的请求;除此,针对多应用合并部署的场景,我们也会复用 IO 线程防止一个进程开过多的 IO 线程。</li>
<li> 对于无状态的 <code>ChannelHandler</code> ,设置成共享模式。比如我们的事件处理器,RPC 处理器都可以设置为共享,减少不同的 <code>Channel</code> 对应的 <code>ChannelPipeline</code> 里生成的对象个数。</li>
<li> 正确使用 <code>ChannelHandlerContext</code> 的 <code>ctx.write()</code> 与 <code>ctx.channel().write()</code> 方法。前者是从当前 <code>Handler</code> 的下一个 <code>Handler</code> 开始处理,而后者会从 tail 开始处理。大多情况下使用 <code>ctx.write()</code> 即可。</li>
<li> 在使用 <code>Channel</code> 写数据之前,建议使用 <code>isWritable()</code> 方法来判断一下当前 <code>ChannelOutboundBuffer</code> 里的写缓存水位,防止 OOM 发生。不过实践下来,正常的通信过程不太会 OOM,但当网络环境不好,同时传输报文很大时,确实会出现限流的情况。</li>
</ul></li>
</ul>
<h3 id="toc_4">2. 连接管理</h3>
<p>为了提高通信效率,我们需要考虑复用连接,减少 TCP 三次握手的次数,因此需要有连接管理的机制。而在业务的通信场景中,我们还识别到一些不得不走硬负载(比如 LVS VIP)的场景,此时如果只建立单链接,可能会出现负载不均衡的问题,此时需要建立多个连接,来缓解负载不均的问题。我们需要设计一个针对某个连接地址(IP 与 Port 唯一确定的地址)建立特定数目连接的实现,同时保存在一个连接池里。该连接池设计了一个通用的 <code>PoolKey</code>不限定 Key 的类型。</p>
<p>需要注意的是,这里的建连过程,有一个并发问题要解,比如客户端在高并发的调用建连接口时,如何保证建立的连接刚好是所设定的个数呢?为了配合 Netty 的无锁理念,我们也采用一个无锁化的建连过程来实现,利用 <code>ConcurrentHashMap</code> 的 <code>putIfAbsent</code> 接口:</p>
<p><img src="media/15277835170976/15277843573310.jpg" alt=""/><br/>
代码1 - 无锁建连代码</p>
<p>除此,我们的连接管理,还要具备定时断连功能,自动重连功能,自定义连接选择算法功能来适用不同的连接场景。</p>
<ul>
<li> 最佳实践:在 Netty 的 4.0.28.Final#3218 里,提供了一种 <code>ChannelPool</code> 的接口类与默认实现,其中 <code>FixedChannelPool</code> 与我们实现的连接池做的事情一样。而 Netty 采用了更巧妙的方式来规避并发问题,即在初始化 <code>FixedChannelPool</code> 时,就将其关联到某一个 <code>eventLoop</code> 上,后续的建连动作,采用经典的 <code>inEventLoop()</code> 方法来判断,如果不在 <code>eventLoop</code> 线程,则入队等待下次调度。如此规避了并发问题。这个功能,我们目前还没有实践过,后续计划采用这个官方实现重构一版。</li>
</ul>
<h3 id="toc_5">3. 基础通信模型</h3>
<p><img src="media/15277835170976/15277843662971.jpg" alt=""/><br/>
图4 - 几种通信模型</p>
<p>如图所示,我们实现了多种通信接口 <code>oneway</code> ,<code>sync</code> ,<code>future</code> ,<code>callback</code> 。图中都是ping/pong模式的通信,蓝色部分表示线程正在执行任务</p>
<ul>
<li> 可以看到 <code>oneway</code> 不关心响应,请求线程不会被阻塞,但使用时需要注意控制调用节奏,防止压垮接收方;</li>
<li> <code>sync</code> 调用会阻塞请求线程,待响应返回后才能进行下一个请求。这是最常用的一种通信模型;</li>
<li> <code>future</code> 调用,在调用过程不会阻塞线程,但获取结果的过程会阻塞线程;</li>
<li> <code>callback</code> 是真正的异步调用,永远不会阻塞线程,结果处理是在异步线程里执行。</li>
</ul>
<h3 id="toc_6">4. 超时控制</h3>
<p><img src="media/15277835170976/15277843806586.jpg" alt=""/><br/>
图5 - 超时控制模型</p>
<p>除了 <code>oneway</code> 模式,其他三种通信模型都需要进行超时控制,我们同样采用 Netty 里针对超时机制,所设计的高效方案 <code>HashedWheelTimer</code> 。如图所示,其原理是首先在发起调用前,我们会新增一个超时任务 <code>timeoutTask</code> 到 <code>MpscQueue</code> (Netty 实现的一种高效的无锁队列)里,然后在循环里,会不断的遍历 Queue 里的这些超时任务(每次最多10万),针对每个任务,会根据其设置的超时时间,来计算该任务所属于的 <code>bucket</code> 位置与剩余轮数 <code>remainingRounds</code> ,然后加入到对应 <code>bucket</code> 的链表结构里。随着 <code>tick++</code> 的进行,时间在不断的增长,每 <code>tick</code> 8 次,就是 1 个时间轮 <code>round</code>。当对应超时任务的<code>remainingRounds</code>减到 <code>0</code> 时,就是触发这个超时任务的时候,此时再执行其 <code>run()</code> 方法,做超时逻辑处理。</p>
<ul>
<li> 最佳实践:通常一个进程使用一个<code>HashedWheelTimer</code>实例,采用单例模型即可。</li>
</ul>
<h3 id="toc_7">5. 批量解包与批量提交</h3>
<p><img src="media/15277835170976/15277843927772.jpg" alt=""/><br/>
图6 - 批量解包与批量提交</p>
<p>Netty 提供了一个方便的解码工具类 <code>ByteToMessageDecoder</code> ,如图上半部分所示,这个类具备 <code>accumulate</code> 批量解包能力,可以尽可能的从 <code>socket</code> 里读取字节,然后同步调用 <code>decode</code> 方法,解码出业务对象,并组成一个 <code>List</code> 。最后再循环遍历该 <code>List</code> ,依次提交到 <code>ChannelPipeline</code> 进行处理。此处我们做了一个细小的改动,如图下半部分所示,即将提交的内容从单个 <code>command</code> ,改为整个 <code>List</code> 一起提交,如此能减少 <code>pipeline</code> 的执行次数,同时提升吞吐量。这个模式在低并发场景,并没有什么优势,而在高并发场景下对提升吞吐量有不小的性能提升。</p>
<ul>
<li> 最佳实践:<code>ByteToMessageDecoder</code> 因为内部的实现有成员变量,不是无状态的,所以一定不能被设置为 <code>@Sharable</code></li>
</ul>
<h3 id="toc_8">6. 其他有用的功能</h3>
<ul>
<li> <strong>事件触发与监听机制</strong>
<ul>
<li> Netty 的 <code>ChannelHandler</code> 完美实现了拦截器模式。在 <code>ChannelHandler</code> 里 <code>hook</code> 了各个IO事件与IO操作的方法,我们可以方便的覆写这些方法,来加一些自定义的逻辑。比如为了把建连,断连事件触发给上层业务,方便做一些准备或者优雅关闭的处理,我们实现一个继承了 <code>ChannelInBoundHandler</code> 与 <code>ChannelOutboundHandler</code> 的处理器,覆盖这些事件所对应的建连与断连方法,然后设计一套业务的 <code>event</code> 感知逻辑即可。</li>
</ul></li>
<li> <strong>双工通信</strong>
<ul>
<li> 我们知道 TCP 是可以提供全双工的通信能力的。因此,当客户端与服务端建立连接后,我们是可以由服务端发起通信请求,客户端来处理的。而为了支持这个功能,我们只需要把可以复用的 <code>inboundHandler</code> 与 <code>outboundHandler</code> 在 客户端的 <code>Bootstrap</code> 与服务端的 <code>ServerBootstrap</code> 里都注册一遍即可</li>
</ul></li>
</ul>
<p>有了私有协议的设计要点,与基础通信模块的实现,我们来看一个私有协议设计的举例,一种典型的 RPC 特征的通信实现。</p>
<h2 id="toc_9"><strong>私有通信协议举例</strong></h2>
<h3 id="toc_10">1. 通信协议的设计</h3>
<p><img src="media/15277835170976/15277844025910.jpg" alt=""/><br/>
图7 - 协议字段举例</p>
<ul>
<li> <code>ProtocolCode</code> :如果一个端口,需要处理多种协议的请求,那么这个字段是必须的。因为需要根据 <code>ProtocolCode</code> 来进入不同的核心编解码器。比如在支付宝,因为曾经使用过基于mina开发的通信框架,当时设计了一版协议。因此,我们在设计新版协议时,需要预留该字段,来适配不同的协议类型。该字段可以在想换协议的时候,方便的进行更换。</li>
<li> <code>ProtocolVersion</code> :确定了某一种通信协议后,我们还需要考虑协议的微小调整需求,因此需要增加一个 <code>version</code> 的字段,方便在协议上追加新的字段</li>
</ul>
<p><img src="media/15277835170976/15277844148635.jpg" alt=""/><br/>
图8 - 协议号与版本号的关系</p>
<ul>
<li> <code>RequestType</code> :请求类型, 比如<code>request</code> <code>response</code> <code>oneway</code></li>
<li> <code>CommandCode</code> :请求命令类型,比如 <code>request</code> 可以分为:负载请求,或者心跳请求。<code>oneway</code> 之所以需要单独设置,是因为在处理响应时,需要做特殊判断,来控制响应是否回传。</li>
<li> <code>CommandVersion</code> :请求命令版本号。该字段用来区分请求命令的不同版本。如果修改 <code>Command</code> 版本,不修改协议,那么就是纯粹代码重构的需求;除此情况,<code>Command</code> 的版本升级,往往会同步做协议的升级。</li>
<li> <code>RequestId</code> :请求 ID,该字段主要用于异步请求时,保留请求存根使用,便于响应回来时触发回调。另外,在日志打印与问题调试时,也需要该字段。</li>
<li> <code>Codec</code> :序列化器。该字段用于保存在做业务的序列化时,使用的是哪种序列化器。通信框架不限定序列化方式,可以方便的扩展。</li>
<li> <code>Switch</code> :协议开关,用于一些协议级别的开关控制,比如 CRC 校验,安全校验等。</li>
<li> <code>Timeout</code> :超时字段,客户端发起请求时,所设置的超时时间。该字段非常有用,在后面会详细讲解用法。</li>
<li> <code>ResponseStatus</code> :响应码。从字段精简的角度,我们不可能每次响应都带上完整的异常栈给客户端排查问题,因此,我们会定义一些响应码,通过编号进行网络传输,方便客户端定位问题。</li>
<li> <code>ClassLen</code> :业务请求类名长度</li>
<li> <code>HeaderLen</code> :业务请求头长度</li>
<li> <code>ContentLen</code> :业务请求体长度</li>
<li> <code>ClassName</code> :业务请求类名。需要注意类名传输的时候,务必指定字符集,不要依赖系统的默认字符集。曾经线上的机器,因为运维误操作,默认的字符集被修改,导致字符的传输出现编解码问题。而我们的通信框架指定了默认字符集,因此躲过一劫。</li>
<li> <code>HeaderContent</code> :业务请求头</li>
<li> <code>BodyContent</code> :业务请求体</li>
<li> <code>CRC32</code> :CRC校验码,这也是通信场景里必不可少的一部分,而我们金融业务属性的特征,这个显得尤为重要。</li>
</ul>
<h3 id="toc_11">2. 灵活的反序列化时机控制</h3>
<p>从上面的协议介绍,可以看到协议的基本字段所占用空间是比较小的,目前只有24个字节。协议上的主要负载就是 <code>ClassName</code> ,<code>HeaderContent</code> , <code>BodyContent</code> 这三部分。这三部分的序列化和反序列化是整个请求响应里最耗时的部分。在请求发送阶段,在调用 Netty 的写接口之前,会在业务线程先做好序列化,这里没有什么疑问。而在请求接收阶段,反序列化的时机就需要考虑一下了。结合上面提到的最佳实践的网络 IO 模型,请求接收阶段,我们有 IO 线程,业务线程两种线程池。为了最大程度的配合业务特性,保证整体吞吐我们设计了精细的开关来控制反序列化时机:</p>
<p><img src="media/15277835170976/15277844266936.jpg" alt=""/><br/>
图9 - 反序列化与业务处理时序图</p>
<p><img src="media/15277835170976/15277844352308.jpg" alt=""/><br/>
表格1 - 反序列化场景具体介绍</p>
<h3 id="toc_12">3. Server Fail-Fast 机制</h3>
<p><img src="media/15277835170976/15277844481916.jpg" alt=""/><br/>
图10 - Server Fail-Fast机制</p>
<p>在协议里,留意到我们有timeout这个字段,这个是把客户端发起调用时,所设置的超时时间通过协议传到了 Server 端。有了这个,我们就可以实现 Fail-Fast 快速失败的机制。比如当客户端设置超时时间 1s,当请求到达 Server 开始计时 <code>arriveTimeStamp</code> ,到任务被线程调度到开始处理时,记录 <code>startToProcessTimestamp</code> ,二者的差值即请求反序列化与线程池排队的时延,如果这个时间间隔已经超过了 1s,那么请求就没有必要被处理了。这个机制,在服务端出现处理抖动时,对于快速恢复会很有用。</p>
<ul>
<li> 最佳实践:不要依赖跨系统的时钟,因为时钟可能会不一致,跨系统就会出现误差,因此是从请求到达 Server 的那一刻,在 Server 的进程里开始计时。</li>
</ul>
<h3 id="toc_13">4. 用户请求处理器(UserProcessor)</h3>
<p>在通用设计部分,我们提到了命令处理器。而为了方便开发者使用,我们还提供了一个用户请求处理器,即在 RPC 的命令处理器中,再增加一层映射关系,保存的是 业务传输对象的 <code>className</code> 与 <code>UserProcessor</code> 的对应关系。此时服务端只需要简单注册一个 <code>className</code> 对应的processor,并提供一个独立的 <code>executor</code> ,就可以实现在业务线程处理请求了。</p>
<p><img src="media/15277835170976/15277844585373.jpg" alt=""/><br/>
图11 - 命令处理器与用户请求处理器的关系</p>
<p>除此,我们还设计了一个 <code>RemotingContext</code> 用于保存请求处理阶段的一些通信层的关键辅助类或者信息,方便通信框架开发者使用;同时还提供了一个 <code>BizContext</code> ,有选择把通信层的信息暴露给框架使用者,方便框架使用者使用。有了用户请求处理器,以及上下文的传递机制,我们就可以方便的把通信层处理逻辑与业务处理逻辑联动起来,比如一些开关的控制,字段的传递等定制功能:</p>
<ul>
<li> 请求超时处理开关:用于开关 Server Fail-Fast 机制。</li>
<li> IO 线程业务处理开关:用户可以选择在 IO 线程处理业务请求;或者在业务线程来处理。</li>
<li> 线程池选择器 <code>ExecutorSelector</code> :用户可以提供多个业务线程池,使用 <code>ExecutorSelector</code> 来实现选择逻辑</li>
<li> 泛化调用的支持:序列化请求与反序列化响应阶段,针对泛化调用,使用特殊的序列化器。而是否开启该功能,需要依赖上下文来传递一些标识。</li>
</ul>
<h3 id="toc_14">5. 其他实现细节</h3>
<ul>
<li><p><strong>可扩展的序列化机制</strong><br/>
针对业务对象里的 <code>HeaderContent</code> 与 <code>BodyContent</code> ,我们提供了用户自定义逻辑:用户可以结合自身的请求内容做定制的序列化和反序列化动作;如果用户没有自定义,那么会默认使用 Bolt 框架当前集成的序列化器,比如 Hessian(默认使用)、FastJson 等。</p></li>
<li><p><strong>埋点与异常处理</strong><br/>
为了精细化请求处理过程,我们会记录请求发送阶段的建连耗时,客户端超时时间,请求到达时间,线程调度等待时间等,然后通过上下文传递机制,连通业务与通信层;同时还会细化各个异常场景,比如请求超时异常,服务端线程池繁忙,序列化异常(请求与响应),反序列化异常(请求与响应)等。有了这些就能方便进行问题排查和快速定位。</p></li>
<li><p><strong>日志打印</strong><br/>
<img src="media/15277835170976/15277844755617.jpg" alt=""/><br/>
图12 - 日志模板</p>
<p>作为通信框架,必要的日志打印也是很重要的。比如可以打印建连与断连的日志,便于排查连接问题;一些关键的异常场景也可以打印出来,方便定位问题;还可以打印一些关键字,来表示程序 BUG,便于框架开发者定位和分析。而打印日志的方式,我们选择依赖日志门面 <code>SLF4J</code> ,然后提供不同的日志实现所需要的配置文件。运行时,根据业务所依赖的日志实现(比如 <code>log4j</code> , <code>log4j2</code> , <code>logback</code> 来动态加载日志配置)。同时默认使用异步 <code>logger</code> 来打印日志。</p></li>
</ul>
<h2 id="toc_15"><strong>蚂蚁通信框架-BOLT</strong></h2>
<ul>
<li><p>为了让 Java 程序员,花更多的时间在一些 Productive 的事情上,而不是纠结底层 NIO 的实现,处理难以调试的网络问题,Netty 应运而生</p></li>
<li><p>为了让中间件的开发者,花更多的时间在中间件的特性实现上,而不是重复地一遍遍制造通信框架的轮子,Bolt 应运而生。</p></li>
</ul>
<p>Bolt 即为本文所描述的方法论的一个实践实现,名字取自迪士尼动画,闪电狗。定位是一个基于 Netty 最佳实践过的,通用、高效、稳定的通信框架。我们希望能把这些年,在 RPC,MSG 在网络通信上碰到的问题与解决方案沉淀到这个基础组件里,不断的优化和完善它。让更多的需要网络通信的场景能够统一受益。目前已经运用在了蚂蚁中间件的微服务,消息中心,分布式事务,分布式开关,配置中心等众多产品上。</p>
<p>除了 Bolt 提供的高效通信能力外,还可以方便的进行协议适配的工作。比如蚂蚁内部之前使用的 RPC 协议是 Tr 协议,是基于 Apache Mina 开发的老版本通信框架,由于年久失修,同时性能逐步落伍,我们重新设计了 Bolt 协议,精简以及新增了一些协议字段,同时切换到了 Netty 上。在新老 RPC 协议的切换期间,我们利用 Bolt 进行了协议适配,开发了 BoltTrAdaptor,最大程度的复用基础通信能力,仅仅把协议相关的部分单独实现,以此来保证新老协议调用的兼容性。</p>
<p>针对蚂蚁内部的新老通信框架,我们进行了细致的压测,如下图所示。我们的压测环境是,4 核10G 的虚拟机,千兆网卡,请求与响应包大小 1024 字节,分别压测了四种场景。由压测结果能看出 Bolt -> Bolt 的场景,整体吞吐量最大,平均RT最小,同时对比了 IO ,CPU 使用率等情况,资源整体利用率上也提升很多。</p>
<p><img src="media/15277835170976/15277844880461.jpg" alt=""/><br/>
图13 - 压测TPS数据</p>
<p><img src="media/15277835170976/15277844973704.jpg" alt=""/><br/>
图14 - 压测平均RT数据</p>
<p>Bolt 在实验室里的极限性能压测,采用的是 32 核物理机,万兆网卡的环境,请求和响应 100 字节负载,服务端收到请求后马上返回响应,瓶颈基本就是业务线程池所使用的 <code>ArrayBlockingQueue</code> <code>LinkedBlockingQueue</code> 的性能瓶颈,压力到了十多万,就会出现较大幅度的毛刺和抖动。纯粹为了压测场景,改成使用 <code>SynchronousQueue</code> 后,毛刺减少了很多,基本能稳定在 30W TPS 的处理能力。</p>
<h2 id="toc_16"><strong>写在最后</strong></h2>
<p>近期我们也在准备开源蚂蚁 Bolt 通信框架,主要是吸取 Netty 的开源精神,回馈社区,与社区共建与完善。如果你也有制造通信框架轮子的需求,或者想适配内部的自有或者开源通信协议(比如 Dubbo 等),可以试一下蚂蚁 Bolt 通信框架,敬请期待!我们有很多想法还在实验室里酝酿,还没有落地到生产环境使用。非常欢迎一起来探讨网络通信问题,参与共建。</p>
<p>最后附上蚂蚁中间件的招聘链接,通信是分布式架构体系的基础设施,欢迎有志之士加盟,打造高效、稳定的通信技术。点击左下角的【阅读原文】获取蚂蚁中间件通信组的招聘信息。</p>
<h2 id="toc_17"><strong>参考</strong></h2>
<ol>
<li> Scalable IO in Java, Slides, by Doug Lea, <a href="http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf">http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf</a></li>
<li> Reactor, Thesis, by Douglas C. Schmidt, <a href="http://www.dre.vanderbilt.edu/%7Eschmidt/PDF/reactor-siemens.pdf">http://www.dre.vanderbilt.edu/~schmidt/PDF/reactor-siemens.pdf</a></li>
<li> Hashed and Hierarchical Timing Wheels, Thesis, by George Varghese and Anthony Lauck, <a href="http://www.cs.columbia.edu/%7Enahum/w6998/papers/ton97-timing-wheels.pdf">http://www.cs.columbia.edu/~nahum/w6998/papers/ton97-timing-wheels.pdf</a></li>
<li> Netty Best Practices, Slides, by Norman Maurer, <a href="http://normanmaurer.me/presentations/2014-facebook-eng-netty/slides.html">http://normanmaurer.me/presentations/2014-facebook-eng-netty/slides.html</a></li>
<li> NSF-RPC的优化过程,博客文章,来自毕玄,<a href="http://bluedavy.me/?p=384">http://bluedavy.me/?p=384</a></li>
<li> Netty 源码分析系列 ,博客文章,来自永顺,<a href="https://segmentfault.com/a/1190000007282628">https://segmentfault.com/a/1190000007282628</a></li>
<li> 《Netty权威指南》,书籍,来自李林锋</li>
<li> 《Netty实战》,书籍,来自Norman Maurer等著,何品翻译</li>
</ol>
<h3 id="toc_18"><strong>附文中提到的一些链接地址信息</strong></h3>
<ol>
<li> Gecko: <a href="https://github.com/killme2008/gecko">https://github.com/killme2008/gecko</a></li>
<li> Mina: <a href="http://mina.apache.org/">http://mina.apache.org/</a></li>
<li> Netty: <a href="http://netty.io/">http://netty.io/</a></li>
<li> Stackoverflow - Do we need more than a single thread for boss group?:<a href="https://stackoverflow.com/questions/22280916/do-we-need-more-than-a-single-thread-for-boss-group">https://stackoverflow.com/questions/22280916/do-we-need-more-than-a-single-thread-for-boss-group</a></li>
<li> [#3218] Add ChannelPool abstraction and implementations:<a href="https://github.com/netty/netty/pull/3607">https://github.com/netty/netty/pull/3607</a></li>
</ol>
</div>
<div class="row">
<div class="large-6 columns">
<p class="text-left" style="padding:15px 0px;">
<a href="15277850962764.html"
title="Previous Post: Leaf——美团点评分布式ID生成系统">« Leaf——美团点评分布式ID生成系统</a>
</p>
</div>
<div class="large-6 columns">
<p class="text-right" style="padding:15px 0px;">
<a href="15277833297034.html"
title="Next Post: 揭秘JDBC超时机制">揭秘JDBC超时机制 »</a>
</p>
</div>
</div>
<div class="comments-wrap">
<div class="share-comments">
<script type="text/javascript" src="//s7.addthis.com/js/300/addthis_widget.js#pubid=ra-5ae58078c0d7b2ab"></script>
</div>
</div>
</div><!-- article-wrap -->
</div><!-- large 8 -->
<div class="large-4 medium-4 columns">
<div class="hide-for-small">
<div id="sidebar" class="sidebar">
<div id="site-info" class="site-info">
<div class="site-a-logo"><img src="./asset/img/logo.jpg" /></div>
<h1>Junkman</h1>
<div class="site-des">“拾荒者”一词来自凯文・凯利的《失控》中关于机器学习的故事(“收集癖好机”如何完成他的收集工作)。</div>
<div class="social">
<a target="_blank" class="github" target="_blank" href="https://github.com/panlw/" title="GitHub">GitHub</a>
<a target="_blank" class="rss" href="atom.xml" title="RSS">RSS</a>
</div>
</div>
<div id="site-categories" class="side-item ">
<div class="side-header">
<h2>Categories</h2>
</div>
<div class="side-content">
<p class="cat-list">
<a href="Infra.html"><strong>Infra</strong></a>
<a href="Coding.html"><strong>Coding</strong></a>
<a href="Modeling.html"><strong>Modeling</strong></a>
<a href="Archtecting.html"><strong>Archtecting</strong></a>
</p>
</div>
</div>
<div id="site-categories" class="side-item">
<div class="side-header">
<h2>Recent Posts</h2>
</div>
<div class="side-content">
<ul class="posts-list">
<li class="post">
<a href="15517999043443.html">The Art of Crafting Architectural Diagrams</a>
</li>
<li class="post">
<a href="15517997955971.html">为什么说我们需要软件架构图?</a>
</li>
<li class="post">
<a href="15516128677869.html">DNS Servers That Offer Privacy and Filtering</a>
</li>
<li class="post">
<a href="15516123108194.html">Airbnb's Migration from Monolith to Services</a>
</li>
<li class="post">
<a href="15516097487470.html">Events As First-Class Citizens</a>
</li>
</ul>
</div>
</div>
</div><!-- sidebar -->
</div><!-- hide for small -->
</div><!-- large 4 -->
</div><!-- row -->
<div class="page-bottom clearfix">
<div class="row">
<p class="copyright">Copyright © 2015
Powered by <a target="_blank" href="http://www.mweb.im">MWeb</a>,
Theme used <a target="_blank" href="http://github.com">GitHub CSS</a>.</p>
</div>
</div>
</section>
</div>
</div>
<script src="asset/js/foundation.min.js"></script>
<script>
$(document).foundation();
function fixSidebarHeight(){
var w1 = $('.markdown-body').height();
var w2 = $('#sidebar').height();
if (w1 > w2) { $('#sidebar').height(w1); };
}
$(function(){
fixSidebarHeight();
})
$(window).load(function(){
fixSidebarHeight();
});
</script>
<script src="asset/chart/all-min.js"></script><script type="text/javascript">$(function(){ var mwebii=0; var mwebChartEleId = 'mweb-chart-ele-'; $('pre>code').each(function(){ mwebii++; var eleiid = mwebChartEleId+mwebii; if($(this).hasClass('language-sequence')){ var ele = $(this).addClass('nohighlight').parent(); $('<div id="'+eleiid+'"></div>').insertAfter(ele); ele.hide(); var diagram = Diagram.parse($(this).text()); diagram.drawSVG(eleiid,{theme: 'simple'}); }else if($(this).hasClass('language-flow')){ var ele = $(this).addClass('nohighlight').parent(); $('<div id="'+eleiid+'"></div>').insertAfter(ele); ele.hide(); var diagram = flowchart.parse($(this).text()); diagram.drawSVG(eleiid); } });});</script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script><script type="text/x-mathjax-config">MathJax.Hub.Config({TeX: { equationNumbers: { autoNumber: "AMS" } }});</script>
</body>
</html>