-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathatom.xml
1068 lines (986 loc) · 151 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
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title><![CDATA[码农的耕地]]></title>
<subtitle><![CDATA[夕阳下,烟柳树,coding]]></subtitle>
<link href="/atom.xml" rel="self"/>
<link href="http://yoursite.com/"/>
<updated>2017-03-31T05:52:56.000Z</updated>
<id>http://yoursite.com/</id>
<author>
<name><![CDATA[cbf]]></name>
<email><![CDATA[[email protected]]]></email>
</author>
<generator uri="http://hexo.io/">Hexo</generator>
<entry>
<title><![CDATA[RPC服务化]]></title>
<link href="http://yoursite.com/2017/03/31/RPC%E6%9C%8D%E5%8A%A1%E5%8C%96/"/>
<id>http://yoursite.com/2017/03/31/RPC服务化/</id>
<published>2017-03-31T05:52:56.000Z</published>
<updated>2017-03-31T05:52:56.000Z</updated>
<content type="html"></content>
<summary type="html">
</summary>
</entry>
<entry>
<title><![CDATA[Disruptor简介]]></title>
<link href="http://yoursite.com/2017/02/28/Disruptor%E7%AE%80%E4%BB%8B/"/>
<id>http://yoursite.com/2017/02/28/Disruptor简介/</id>
<published>2017-02-28T01:57:01.000Z</published>
<updated>2017-04-07T03:41:52.000Z</updated>
<content type="html"><![CDATA[<p>Disruptor 简介<br><a id="more"></a></p>
<h3 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h3><p>在Storm中,bolt发送tuple的时候会把tuple放入一个队列,然后mq从队列中取出来发送到下一个节点。这个队列,从storm0.8.0版本开始改为了 DisruptorQueue (之前是LinkedBlockingQueue)。</p>
<p>Disruptor队列是一个什么队列呢?他是一个非常高效的框架,网上有资料介绍的,这是官方介绍:<a href="http://lmax-exchange.github.io/disruptor/" target="_blank" rel="external">http://lmax-exchange.github.io/disruptor/</a></p>
<p>它是一个生产者消费者队列。与普通的队列不同,它通过数组实现了一个环,用于存放数据,叫做RingBuffer。框架大概如下图所示:<br><img src="/2017/02/28/Disruptor简介/1.png" alt="框架图" title="框架图"></p>
<p>生产者(Producer)把数据塞进RingBuffer,然后消费者(Consumer)从RingBuffer中把数据读出来。</p>
<p>RingBuffer拥有一个序号,这个序号指向数组中下一个可用的元素。随着你不停地填充这个buffer(可能也会有相应的读取),这个序号会一直增长,直到绕过这个环。通过mod操作,可以找到数组中的位置。</p>
<h3 id="读写操作"><a href="#读写操作" class="headerlink" title="读写操作"></a>读写操作</h3><p>Disruptor具体怎么读写的,这些都可以从网上查到,我下边就简单介绍一下它的思路和为什么快的原因。</p>
<p>多线程下其他队列之所以慢,是因为互斥锁的原因。Disruptor就解决了这个问题,Disruptor它是通过Volatile关键字实现。Disruptor的Volatile变量都不会出现同时写的操作,所以Volatile关键字就可以达到要求。</p>
<p>Disruptor 的 RingBuffer 的指针是一个Volatile对象,消费者中的序列号也是volatile类型的。</p>
<p>先说说写操作。</p>
<p>写的时候,有2个指针,分别是:<br>next:第一个可以写的,移动的时候只能移动一个slot;<br>cursor:最后一个已经写了的,移动的时候只能移动一个slot。<br>写的时候,有2阶段步骤:(1)获取slot(nextEntry函数)的id,然后next指针后移;(2)写数据,然后cursor后移。<br>在下边的例子中,一开始的时候next在3,cursor在2,然后以下步骤:<br>(1)线程D先获取id = 3,next指向4;<br>(2)线程E获取id = 4,next指向5;<br>(3)线程E先写,写完后此时需要把cursor置为id 4,但是cursor只能加一,cursor是2不是3,所以就一直等待cursor变成3<br>(4)线程D写,把cursor置为id 3<br>(5)E发现cursor变成了3,然后把cursor变成4,结束。<br>如下图所示:<br><img src="/2017/02/28/Disruptor简介/2.png" alt="写操作图" title="写操作图"></p>
<h3 id="读操作"><a href="#读操作" class="headerlink" title="读操作"></a>读操作</h3><p>下图展示当读写有冲突时的情形。该写12的时候,ProducerBarrier从所有Consumer处获取当前consumer读到的位置,发现consumer1还在读12,于是就等待,直到consumer把12读完了,Producer才可以写12。<br>写的时候,会去读取所有Consumer的当前的序号,这个值是volatile的,读不会出现错误的。(volatile在CPU写的时候会在所有读指令前边加上内存屏障,所以读到的一定是最新的)<br><img src="/2017/02/28/Disruptor简介/3.png" alt="读操作图" title="读操作图"></p>
]]></content>
<summary type="html">
<![CDATA[<p>Disruptor 简介<br>]]>
</summary>
<category term="java" scheme="http://yoursite.com/tags/java/"/>
<category term="disruptor" scheme="http://yoursite.com/tags/disruptor/"/>
</entry>
<entry>
<title><![CDATA[JavaNio]]></title>
<link href="http://yoursite.com/2016/12/23/JavaNio/"/>
<id>http://yoursite.com/2016/12/23/JavaNio/</id>
<published>2016-12-23T09:04:35.000Z</published>
<updated>2017-04-07T03:22:27.000Z</updated>
<content type="html"><![CDATA[<p>NIO是New IO 的简称,为所有的原始类型提供(Buffer)缓存支持,提供了与标准IO不同的IO工作方式。现如今,大型网站多多少少使用了NIO框架(Netty, Mina),这使得了解NIO基础原理以及相应框架实现的必要性大大加强,本篇涉及到NIO的基础原理和实现。<br><a id="more"></a></p>
<h2 id="BIO"><a href="#BIO" class="headerlink" title="BIO"></a>BIO</h2><p>BIO是指jdk1.4之前版本面向流的io,服务端需要对每个请求建立一个线程等待请求,而客户端发送请求后,先咨询服务端是否有线程响应,如果没有则会一直等待或者遭到拒绝,如果有的话,线程会阻塞直到IO结束后才继续执行,如图所示:<br><img src="/2016/12/23/JavaNio/1.png" alt="BIO图" title="BIO图"><br>形象比喻:<br>手接水管,手就是thread,水管就是socket,其中的水就是InputStream等流数据,不管水管中有没有水,手都会接着</p>
<p>缺点:<br>1、 耗费线程资源,每一个请求有线程等待处理<br>2、线程阻塞,需要等IO数据准备完全才可以,期间线程阻塞,资源白白浪费</p>
<h2 id="NIO"><a href="#NIO" class="headerlink" title="NIO"></a>NIO</h2><p>无阻塞io是使用单线程或者只使用少量的多线程,每个连接共用一个线程,当处于等待(没有事件)的时候线程资源可以释放出来处理别的请求,通过事件驱动模型当有accept/read/write等事件发生后通知(唤醒)主线程分配资源来处理相关事件。java.nio.channels.Selector就是在该模型中事件的观察者,可以将多个SocketChannel的事件注册到一个Selector上,当没有事件发生时Selector处于阻塞状态,当SocketChannel有accept/read/write等事件发生时唤醒Selector。如下是一个单线程模型的NIO:<br><img src="/2016/12/23/JavaNio/2.jpg" alt="单线程NIO图" title="单线程NIO图"></p>
<p>而在读取数据方面,不同于BIO从InputStream,OutputStream中逐字节读取,写入,NIO的数据必须读入缓冲区中,或写入缓冲区中,再进行处理,如图:<br><img src="/2016/12/23/JavaNio/3.jpg" alt="NIO读写图" title="NIO读写图"><br>形象比喻:<br>不同于BIO的水管,NIO的channel是带龙头的水管,selector是接水工,buffer是水管下的水桶,现在只需一个接水工就可以接手N个水管(单线程模型),他不断查看这些水管(epoll轮询,如果没有水管有水,则阻塞),当发现已经有水管有水了之后,就去打开龙头,把水放入水桶中(从channel中将数据先写入buffer),如果发现水桶中水已经达到可以使用的情况(io数据准备完毕),就进行相应处理</p>
<h2 id="NIO组件解析"><a href="#NIO组件解析" class="headerlink" title="NIO组件解析"></a>NIO组件解析</h2><p>从上分析可知,NIO中最重要的组件是:<br>1、 Buffer<br>2、Channel<br>3、Selector</p>
<h3 id="buffer"><a href="#buffer" class="headerlink" title="buffer"></a>buffer</h3><p>缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存,Buffer 主要实现和工作原理依赖于三个属性:</p>
<ol>
<li>capacity:作为一个内存块,Buffer有一个固定的大小值,也叫capacity. 你只能往里写capacity个byte、long,char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据</li>
<li>position:当你写数据到Buffer中时,position表示当前的位置。初始的position值为0.当一个byte、long等数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity - 1</li>
<li>limit:在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity; 当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据<img src="/2016/12/23/JavaNio/4.jpg" alt="Buffer读写图" title="Buffer读写图">
涉及操作:<br>flip(): 写模式切换到读模式, position = 0, limit = position<br>clear(): 清空buffer, position = 0, limit = capacity, 数据其实未清除<br>compact(): 有未读数据,并且仍需使用这些数据,将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。limit属性依然像clear()方法一样,设置成capacity<br>mark(), reset(): 对于position的标记和恢复</li>
</ol>
<h3 id="channel-selector"><a href="#channel-selector" class="headerlink" title="channel + selector"></a>channel + selector</h3><p>Channel类似BIO中的流,但有不同:</p>
<ol>
<li>既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的</li>
<li>通道可以异步地读写</li>
<li>通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入</li>
</ol>
<p>Java NIO中主要实现:<br>FileChannel:从文件中读写数据<br>DatagramChannel:能通过UDP读写网络中的数据<br>SocketChannel:能通过TCP读写网络中的数据<br>ServerSocketChannel:可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel</p>
<p>Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件</p>
<p>以ServerSocketChannel为例子,一个简单实现如下:</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></pre></td><td class="code"><pre><span class="line">selector = Selector.open(); </span><br><span class="line">ServerSocketChannel ssc = ServerSocketChannel.open(); </span><br><span class="line">ssc.configureBlocking(<span class="keyword">false</span>); </span><br><span class="line">ssc.socket().bind(<span class="keyword">new</span> InetSocketAddress(port));</span><br><span class="line">ssc.register(selector, SelectionKey.OP_ACCEPT); </span><br><span class="line"><span class="keyword">while</span> (<span class="keyword">true</span>) { </span><br><span class="line"><span class="comment">// select()阻塞,等待有事件发生唤醒</span></span><br><span class="line"><span class="keyword">int</span> selected = selector.select(); </span><br><span class="line"><span class="keyword">if</span> (selected > <span class="number">0</span>) { </span><br><span class="line">Iterator selectedKeys = selector.selectedKeys().iterator(); </span><br><span class="line"><span class="keyword">while</span> (selectedKeys.hasNext()) { </span><br><span class="line">SelectionKey key = selectedKeys.next(); </span><br><span class="line"><span class="keyword">if</span> ((key.readyOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) {</span><br><span class="line"> <span class="comment">// 处理 accept 事件 </span></span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> ((key.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ){ </span><br><span class="line"> <span class="comment">// 处理 read 事件 </span></span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> ((key.readyOps() & SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE) { </span><br><span class="line"> <span class="comment">// 处理 write 事件 </span></span><br><span class="line"> } </span><br><span class="line"> selectedKeys.remove(); </span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br></pre></td></tr></table></figure>
<p>从上述代码可见,实现NIO关键点在于:</p>
<ol>
<li>Selector.open();</li>
<li>Channel.register();</li>
<li>selector.select();</li>
<li>唤醒:通过注册在selector上的socket有事件发生 或者 selector.select(timeOut)超时 或者 selector.wakeup()主动唤醒</li>
</ol>
<h2 id="Selector阻塞和唤醒原理分析"><a href="#Selector阻塞和唤醒原理分析" class="headerlink" title="Selector阻塞和唤醒原理分析"></a>Selector阻塞和唤醒原理分析</h2><p>整体原理图:<br><img src="/2016/12/23/JavaNio/5.jpg" alt="整体流程图" title="整体流程图"><br>从代码入手,进行逐步分析:<br>Selector.open()</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></pre></td><td class="code"><pre><span class="line">Selector.java </span><br><span class="line">----- </span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> Selector <span class="title">open</span><span class="params">()</span> <span class="keyword">throws</span> IOException </span>{</span><br><span class="line"> <span class="keyword">return</span> SelectorProvider.provider().openSelector();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">SelectorProvider.java </span><br><span class="line">----- </span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> SelectorProvider <span class="title">provider</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">synchronized</span> (lock) {</span><br><span class="line"> <span class="keyword">if</span> (provider != <span class="keyword">null</span>)</span><br><span class="line"> <span class="keyword">return</span> provider;</span><br><span class="line"> <span class="keyword">return</span> AccessController.doPrivileged(</span><br><span class="line"> <span class="keyword">new</span> PrivilegedAction<SelectorProvider>() {</span><br><span class="line"> <span class="function"><span class="keyword">public</span> SelectorProvider <span class="title">run</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (loadProviderFromProperty())</span><br><span class="line"> <span class="keyword">return</span> provider;</span><br><span class="line"> <span class="keyword">if</span> (loadProviderAsService())</span><br><span class="line"> <span class="keyword">return</span> provider;</span><br><span class="line"> provider = sun.nio.ch.DefaultSelectorProvider.create();</span><br><span class="line"> <span class="keyword">return</span> provider;</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>(1) provider = sun.nio.ch.DefaultSelectorProvider.create();是根据操作系统返回不同的类,各个操作系统类的实现是不同的,以windows为例,其返回的是WindowsSelectorProvider<br>(2) provider 为单例模式</p>
<p>SelectorProvider.provider().openSelector()返回的是各个操作系统下的selector实现,以windows操作系统下的WindowsSelectorProvider为例:</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></pre></td><td class="code"><pre><span class="line">WindowsSelectorProvider.java ---- </span><br><span class="line"> <span class="function"><span class="keyword">public</span> AbstractSelector <span class="title">openSelector</span><span class="params">()</span> <span class="keyword">throws</span> IOException </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> WindowsSelectorImpl(<span class="keyword">this</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>在WindowsSelectorImpl实现中,主要完成了两件事:</p>
<p>(1)初始化轮询数组</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></pre></td><td class="code"><pre><span class="line">WindowsSelectorImpl.java</span><br><span class="line"> ---- </span><br><span class="line"> WindowsSelectorImpl(SelectorProvider sp) <span class="keyword">throws</span> IOException { </span><br><span class="line">.... </span><br><span class="line">pollWrapper = <span class="keyword">new</span> PollArrayWrapper(INIT_CAP); </span><br><span class="line">.... </span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>轮询数组是NIO的核心,其中记录了各个channel的句柄以及所需要监听的事件,底层操作系统就是通过轮询该数组的中channel句柄来查询是否有相应事件发生,如果有,说明有读写等事件发生,select()方法就会从阻塞->唤醒状态,其后相应线程进行IO处理</p>
<p>pollArray模拟的结构为:</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></pre></td><td class="code"><pre><span class="line">typedef struct pollfd { </span><br><span class="line">SOCKET fd; <span class="comment">// 4 bytes </span></span><br><span class="line"><span class="keyword">short</span> events; <span class="comment">// 2 bytes </span></span><br><span class="line">} pollfd_t;</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><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">PollArrayWrapper</span> </span>{ </span><br><span class="line"><span class="keyword">private</span> AllocatedNativeObject pollArray; </span><br><span class="line"><span class="comment">// The fd array ->轮询数组 </span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">short</span> FD_OFFSET = <span class="number">0</span>; </span><br><span class="line"><span class="comment">// fd offset in pollfd ->一个句柄和其监听事件总以句柄描述符开始 </span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">short</span> EVENT_OFFSET = <span class="number">4</span>; </span><br><span class="line"><span class="comment">// events offset in pollfd ->socket句柄占4字节,所以事件起始位移为4 </span></span><br><span class="line"><span class="comment">// 所发生的事件标志 </span></span><br><span class="line"><span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">short</span> POLLIN = AbstractPollArrayWrapper.POLLIN; <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">short</span> POLLOUT = AbstractPollArrayWrapper.POLLOUT; <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">short</span> POLLERR = AbstractPollArrayWrapper.POLLERR; <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">short</span> POLLHUP = AbstractPollArrayWrapper.POLLHUP; <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">short</span> POLLNVAL = AbstractPollArrayWrapper.POLLNVAL; <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">short</span> POLLREMOVE = AbstractPollArrayWrapper.POLLREMOVE; </span><br><span class="line"><span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">short</span> POLLCONN = <span class="number">0x0002</span>; .... PollArrayWrapper(<span class="keyword">int</span> newSize) { </span><br><span class="line"><span class="keyword">int</span> allocationSize = newSize * SIZE_POLLFD; </span><br><span class="line">pollArray = <span class="keyword">new</span> AllocatedNativeObject(allocationSize, <span class="keyword">true</span>); </span><br><span class="line">pollArrayAddress = pollArray.address(); </span><br><span class="line"><span class="keyword">this</span>.size = newSize; </span><br><span class="line"> } </span><br><span class="line"> .... </span><br><span class="line">} </span><br><span class="line">AllocatedNativeObject.java </span><br><span class="line">---- </span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">AllocatedNativeObject</span> <span class="keyword">extends</span> <span class="title">NativeObject</span> </span>{</span><br><span class="line">.... </span><br><span class="line">AllocatedNativeObject(<span class="keyword">int</span> size, <span class="keyword">boolean</span> pageAligned) { </span><br><span class="line"><span class="keyword">super</span>(size, pageAligned); </span><br><span class="line">.... </span><br><span class="line">} </span><br><span class="line">NativeObject.java </span><br><span class="line">---- </span><br><span class="line"><span class="function"><span class="keyword">protected</span> <span class="title">NativeObject</span><span class="params">(<span class="keyword">int</span> size, <span class="keyword">boolean</span> pageAligned)</span> </span>{ </span><br><span class="line"><span class="keyword">if</span> (!pageAligned) { </span><br><span class="line"><span class="keyword">this</span>.allocationAddress = unsafe.allocateMemory(size); <span class="keyword">this</span>.address = <span class="keyword">this</span>.allocationAddress; </span><br><span class="line">} <span class="keyword">else</span> { </span><br><span class="line"><span class="keyword">int</span> ps = pageSize(); </span><br><span class="line"><span class="keyword">long</span> a = unsafe.allocateMemory(size + ps); </span><br><span class="line"><span class="keyword">this</span>.allocationAddress = a; <span class="keyword">this</span>.address = a + ps - (a & (ps - <span class="number">1</span>)); </span><br><span class="line"> } </span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>long a = unsafe.allocateMemory(size + ps); 表明该轮询数组其实是分配的一块系统内存,并且起始地址被相应记录</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></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">PollArrayWrapper</span> </span>{ </span><br><span class="line">.... </span><br><span class="line"><span class="comment">// Access methods for fd structures </span></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">putDescriptor</span><span class="params">(<span class="keyword">int</span> i, <span class="keyword">int</span> fd)</span></span>{ </span><br><span class="line">pollArray.putInt(SIZE_POLLFD * i + FD_OFFSET, fd); </span><br><span class="line">} </span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">putEventOps</span><span class="params">(<span class="keyword">int</span> i, <span class="keyword">int</span> event)</span> </span>{</span><br><span class="line">pollArray.putShort(SIZE_POLLFD * i + EVENT_OFFSET, (<span class="keyword">short</span>)event); </span><br><span class="line"> } </span><br><span class="line"> .... </span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>这是对于轮询数组的核心代码,其实就是保存channel描述符以及保存其相应的事件标志,其中i表示的是当前已在轮询的channel的数量+1,fd是新加入的channel句柄,event是所监听的事件。</p>
<p>(2) 创建自己监听自己的通道</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></pre></td><td class="code"><pre><span class="line">WindowsSelectorImpl.java </span><br><span class="line">---- </span><br><span class="line">WindowsSelectorImpl(SelectorProvider sp) <span class="keyword">throws</span> IOException { </span><br><span class="line">.... </span><br><span class="line">wakeupPipe = Pipe.open(); </span><br><span class="line">wakeupSourceFd = ((SelChImpl)wakeupPipe.source()).getFDVal(); </span><br><span class="line">SinkChannelImpl sink = (SinkChannelImpl)wakeupPipe.sink(); </span><br><span class="line">(sink.sc).socket().setTcpNoDelay(<span class="keyword">true</span>); </span><br><span class="line">wakeupSinkFd = ((SelChImpl)sink).getFDVal(); </span><br><span class="line">pollWrapper.addWakeupSocket(wakeupSourceFd, <span class="number">0</span>); </span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>管道是两个线程的通信方式,Pipe.open()的实现也是根据操作系统的不同而不同,linux直接使用Pipe实现,而windows则通过两个socketChannel来实现,对于管道来说,一个是sinkChannel,是数据的源通道,而sourceChannel则是数据的接收通道,对于windowsSelectorImpl来说,wakeupSinkFd就是sinkChannel的句柄,wakeupSourceFd则是sourceChannel的句柄,最后调用轮询数组对象的addWakeupSocket的方法:</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><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">addWakeupSocket</span><span class="params">(<span class="keyword">int</span> fdVal, <span class="keyword">int</span> index)</span> </span>{ </span><br><span class="line">putDescriptor(index, fdVal); </span><br><span class="line">putEventOps(index, POLLIN); </span><br><span class="line">} </span><br><span class="line">....</span><br></pre></td></tr></table></figure>
<p>其实就是把该接收通道的句柄放入轮询数组中,并将其监听事件设定为POLLIN,表明有可读数据到达,创建自己监听自己的通道,就是为主动唤醒selector而服务</p>
<h3 id="Channel-register"><a href="#Channel-register" class="headerlink" title="Channel.register()"></a>Channel.register()</h3><p>1、 ServerSocketChannel ssc = ServerSocketChannel.open() 打开通道</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> ServerSocketChannel <span class="title">open</span><span class="params">()</span> <span class="keyword">throws</span> IOException </span>{ </span><br><span class="line"><span class="keyword">return</span> SelectorProvider.provider().openServerSocketChannel(); </span><br><span class="line">} </span><br><span class="line"><span class="function"><span class="keyword">public</span> ServerSocketChannel <span class="title">openServerSocketChannel</span><span class="params">()</span> <span class="keyword">throws</span> IOException </span>{ </span><br><span class="line"><span class="keyword">return</span> <span class="keyword">new</span> ServerSocketChannelImpl(<span class="keyword">this</span>); </span><br><span class="line">} </span><br><span class="line">ServerSocketChannelImpl(SelectorProvider sp) <span class="keyword">throws</span> IOException { </span><br><span class="line"><span class="keyword">super</span>(sp); </span><br><span class="line"><span class="keyword">this</span>.fd = Net.serverSocket(<span class="keyword">true</span>); <span class="keyword">this</span>.fdVal = IOUtil.fdVal(fd); <span class="keyword">this</span>.state = ST_INUSE; </span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>可以看见,操作系统相关的selectorProvider无处不在,而在最后初始化通道时,Net.serverSocket(true);用来打开一个socket通道,并返回其句柄</p>
<p>2)ssc.register(selector, SelectionKey.OP_ACCEPT)注册机制:</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><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line">AbstractSelectableChannel.java ---- </span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">final</span> SelectionKey <span class="title">register</span><span class="params">(Selector sel, <span class="keyword">int</span> ops, Object att)</span> <span class="keyword">throws</span> ClosedChannelException </span>{ </span><br><span class="line"><span class="keyword">if</span> (!isOpen()) </span><br><span class="line"><span class="keyword">throw</span> <span class="keyword">new</span> ClosedChannelException(); </span><br><span class="line"><span class="keyword">if</span> ((ops & ~validOps()) != <span class="number">0</span>) <span class="keyword">throw</span> <span class="keyword">new</span> IllegalArgumentException(); <span class="keyword">synchronized</span> (regLock) { <span class="keyword">if</span> (blocking) <span class="keyword">throw</span> <span class="keyword">new</span> IllegalBlockingModeException(); </span><br><span class="line">SelectionKey k = findKey(sel); </span><br><span class="line"><span class="keyword">if</span> (k != <span class="keyword">null</span>) { </span><br><span class="line">k.interestOps(ops); k.attach(att); </span><br><span class="line">} </span><br><span class="line"><span class="keyword">if</span> (k == <span class="keyword">null</span>) { </span><br><span class="line"><span class="comment">// New registration </span></span><br><span class="line">k = ((AbstractSelector)sel).register(<span class="keyword">this</span>, ops, att); addKey(k); } <span class="keyword">return</span> k; </span><br><span class="line"> } </span><br><span class="line">} </span><br><span class="line">SelectorImpl.java </span><br><span class="line">---- </span><br><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">final</span> SelectionKey <span class="title">register</span><span class="params">(AbstractSelectableChannel ch,<span class="keyword">int</span> ops,Object attachment)</span> </span>{ </span><br><span class="line">.... </span><br><span class="line"><span class="keyword">synchronized</span> (publicKeys) { </span><br><span class="line">implRegister(k); </span><br><span class="line">} </span><br><span class="line">k.interestOps(ops); </span><br><span class="line">.... </span><br><span class="line">} </span><br><span class="line">WindowsSelectorImpl.java </span><br><span class="line">---- </span><br><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">implRegister</span><span class="params">(SelectionKeyImpl ski)</span> </span>{ </span><br><span class="line">growIfNeeded(); </span><br><span class="line">channelArray[totalChannels] = ski; </span><br><span class="line">ski.setIndex(totalChannels); fdMap.put(ski); </span><br><span class="line">keys.add(ski); </span><br><span class="line">pollWrapper.addEntry(totalChannels, ski); </span><br><span class="line">totalChannels++; </span><br><span class="line">} </span><br><span class="line">PollArrayWrapper.java </span><br><span class="line">---- </span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">addEntry</span><span class="params">(<span class="keyword">int</span> index, SelectionKeyImpl ski)</span> </span>{ </span><br><span class="line">putDescriptor(index, ski.channel.getFDVal()); </span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>虽然register是channel的方法,但其中调用的还是selector的register方法,在selector的register方法中,implRegister方法主要是向轮询数组中在正确的位置插入该通道的句柄,而interestOps方法则是向轮询数组中插入该通道对应的需要监听的事件,经过此,通道句柄以及相应事件已保存于轮询数组中,完成了注册功能</p>
<h3 id="selector-select"><a href="#selector-select" class="headerlink" title="selector.select()"></a>selector.select()</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><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></pre></td><td class="code"><pre><span class="line">SelectorImpl.java </span><br><span class="line">---- </span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">select</span><span class="params">(<span class="keyword">long</span> timeout)</span> <span class="keyword">throws</span> IOException </span>{ </span><br><span class="line"><span class="keyword">if</span> (timeout < <span class="number">0</span>) <span class="keyword">throw</span> <span class="keyword">new</span> IllegalArgumentException(<span class="string">"Negative timeout"</span>); </span><br><span class="line"><span class="keyword">return</span> lockAndDoSelect((timeout == <span class="number">0</span>) ? -<span class="number">1</span> : timeout); </span><br><span class="line">} </span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">int</span> <span class="title">lockAndDoSelect</span><span class="params">(<span class="keyword">long</span> timeout)</span> <span class="keyword">throws</span> IOException </span>{ </span><br><span class="line"><span class="keyword">synchronized</span> (<span class="keyword">this</span>) { </span><br><span class="line"><span class="keyword">if</span> (!isOpen()) <span class="keyword">throw</span> <span class="keyword">new</span> ClosedSelectorException(); <span class="keyword">synchronized</span> (publicKeys) { </span><br><span class="line"><span class="keyword">synchronized</span> (publicSelectedKeys) { </span><br><span class="line"><span class="keyword">return</span> doSelect(timeout); </span><br><span class="line"> } </span><br><span class="line"> } </span><br><span class="line"> } </span><br><span class="line">} </span><br><span class="line">WindowsSelectorImpl.java </span><br><span class="line">---- </span><br><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">int</span> <span class="title">doSelect</span><span class="params">(<span class="keyword">long</span> timeout)</span> <span class="keyword">throws</span> IOException </span>{ </span><br><span class="line">..... </span><br><span class="line">subSelector.poll(); </span><br><span class="line">..... </span><br><span class="line">} </span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">int</span> <span class="title">poll</span><span class="params">()</span> <span class="keyword">throws</span> IOException</span>{ </span><br><span class="line"><span class="comment">// poll for the main thread </span></span><br><span class="line"><span class="keyword">return</span> poll0(pollWrapper.pollArrayAddress, Math.min(totalChannels, MAX_SELECTABLE_FDS), readFds, writeFds, exceptFds, timeout); </span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>select()最终调用的是subSelector.poll()方法,而其调用的则是本地方法poll0, 其中pollWrapper.pollArrayAddress就是轮询数组的内存地址,供本地方法使用,本地方法实现:</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></pre></td><td class="code"><pre><span class="line">WindowsSelectorImpl.c </span><br><span class="line">---- Java_sun_nio_ch_WindowsSelectorImpl_00024SubSelector_poll0(JNIEnv *env, jobject <span class="keyword">this</span>, jlong pollAddress, jint numfds, jintArray returnReadFds, jintArray returnWriteFds, jintArray returnExceptFds, jlong timeout) { </span><br><span class="line"><span class="comment">// 代码.... 此处省略一万字 </span></span><br><span class="line"><span class="comment">/* Call select */</span> </span><br><span class="line"><span class="keyword">if</span> ((result = select(<span class="number">0</span> , &readfds, &writefds, &exceptfds, tv)) == SOCKET_ERROR) { </span><br><span class="line"><span class="comment">// 代码.... 此处省略一万字 </span></span><br><span class="line"><span class="keyword">for</span> (i = <span class="number">0</span>; i < numfds; i++) { </span><br><span class="line"><span class="comment">// 代码.... 此处省略一万字 </span></span><br><span class="line"> } </span><br><span class="line"> } </span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>本地方法调用的是C的select方法,各个系统不一样,linux调用的是epoll方法,对于select方法来说,其对应于内核中的sys_select调用,sys_select首先将第二三四个参数指向的fdset拷贝到内核,然后对每个被SET的描述符调用进行poll,并记录在临时结果中(fdset),如果有事件发生,select会将临时结果写到用户空间并返回</p>
<h3 id="selector-wakeup"><a href="#selector-wakeup" class="headerlink" title="selector.wakeup()"></a>selector.wakeup()</h3><p>对于唤醒的实现:<br><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></pre></td><td class="code"><pre><span class="line">WindowsSelectorImpl.java </span><br><span class="line">---- </span><br><span class="line"><span class="function"><span class="keyword">public</span> Selector <span class="title">wakeup</span><span class="params">()</span> </span>{ </span><br><span class="line"><span class="keyword">synchronized</span> (interruptLock) { </span><br><span class="line"><span class="keyword">if</span> (!interruptTriggered) { </span><br><span class="line">setWakeupSocket(); interruptTriggered = <span class="keyword">true</span>; </span><br><span class="line"> } </span><br><span class="line">} </span><br><span class="line"><span class="keyword">return</span> <span class="keyword">this</span>; </span><br><span class="line">} </span><br><span class="line"><span class="comment">// Sets Windows wakeup socket to a signaled state. private</span></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">setWakeupSocket</span><span class="params">()</span> </span>{ </span><br><span class="line">setWakeupSocket0(wakeupSinkFd); } </span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">native</span> <span class="keyword">void</span> <span class="title">setWakeupSocket0</span><span class="params">(</span><br><span class="line"> <span class="keyword">int</span> wakeupSinkFd)</span></span>;</span><br></pre></td></tr></table></figure></p>
<p>唤醒使用了上面讲述过的管道sink通道句柄,其最终调用的是本地方法setWakeupSocket0,而该方法实现如下:</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">WindowsSelectorImpl.c </span><br><span class="line">---- Java_sun_nio_ch_WindowsSelectorImpl_setWakeupSocket0(JNIEnv *env, jclass <span class="keyword">this</span>, jint scoutFd) { </span><br><span class="line"><span class="comment">/* Write one byte into the pipe */</span> </span><br><span class="line">send(scoutFd, (<span class="keyword">char</span>*)&POLLIN, <span class="number">1</span>, <span class="number">0</span>); </span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>这里完成了向最开始建立的pipe的sink端写入了一个字节,source文件描述符就会处于就绪状态,poll方法会返回,从而导致select方法返回,达到了主动唤醒selector的目的</p>
]]></content>
<summary type="html">
<![CDATA[<p>NIO是New IO 的简称,为所有的原始类型提供(Buffer)缓存支持,提供了与标准IO不同的IO工作方式。现如今,大型网站多多少少使用了NIO框架(Netty, Mina),这使得了解NIO基础原理以及相应框架实现的必要性大大加强,本篇涉及到NIO的基础原理和实现。<br>]]>
</summary>
<category term="java" scheme="http://yoursite.com/tags/java/"/>
<category term="nio" scheme="http://yoursite.com/tags/nio/"/>
</entry>
<entry>
<title><![CDATA[memcached 内存管理]]></title>
<link href="http://yoursite.com/2016/09/30/memcached-%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86/"/>
<id>http://yoursite.com/2016/09/30/memcached-内存管理/</id>
<published>2016-09-30T15:29:17.000Z</published>
<updated>2016-10-18T06:01:38.000Z</updated>
<content type="html"><![CDATA[<p>基于1.4.31的memcached 介绍memcached的内存管理<br><a id="more"></a></p>
<h3 id="1-Memcached介绍"><a href="#1-Memcached介绍" class="headerlink" title="1.Memcached介绍"></a>1.Memcached介绍</h3><p> Memcached是一套分布式的内存对象缓存系统,用于系统中减少数据库的负载,提高系统性能。本文介绍的Memcached内存管理方式基于1.4.31。旧版本的Memcached的内存管理方式与该版本会存在一定的不同,本文没有涉及旧版本的Memcached内存管理介绍。</p>
<h3 id="2-Memcached模型"><a href="#2-Memcached模型" class="headerlink" title="2.Memcached模型"></a>2.Memcached模型</h3><p>在具体介绍Memcached内存管理的源码实现之前,我们先介绍一些Memcached内存管理的重要概念。<br>1.Slab<br>Slab是Memcached中分配的一块内存,默认大小是1M。Slab是Memcached内存分配的最小单位。<br>2.Chunk<br>Slab是Memcached中分配的最小单位,而每一个Slab又会进一步划分成一个个的Chunk。Chunk是Memcached存储数据的最小单元,一个Chunk只能存储一个对象。同时,一个Slab中所有的Chunk的大小是相同的。<br>3.Item<br>Item是Memcached中存储的实际数据。Item本身是一个复杂的数据结构,其中除了包含对象的Key-Value键值对,过期时间外,还有其他一些数据结构,稍后会具体介绍。Memcached将Item保存对应的Slab的某个Chunk中。<br>4.SlabClass<br>通过上面几个概念的介绍,我们知道Memcached通过分配Slab并把Slab划分成等大小的Chunk来进行存储。那么不同的Item大小数据应该如何存储呢?SlabClass把Slab划分成不同大小的组合,每一个SlabClass对应一种Slab,在同一个SlabClass中所有的Slab都划分成相同大小的Chunk。</p>
<p>这里举一个还算比较形象的列子。Memcached的内存分配可以类比与我们上学时代的写字本。Slab相当于写字本的一页,Chunk相当于写字本一页中的一个个写字格子,Item相当于我们要写的字,而SlabClass相当于一个写字本。所以每一个写字本中的格子大小都是相同的,我们要写不同的字,只需要找最符合该字大小的写字本就可以了。</p>
<h3 id="3-Memecached数据结构"><a href="#3-Memecached数据结构" class="headerlink" title="3. Memecached数据结构"></a>3. Memecached数据结构</h3><p>Memcached中最为基本的数据结构是slabclass_t,该数据结构记录了memcached中单个slabclass的结构,具体的数据结构源码如下:</p>
<pre><code>typedef struct {
unsigned int size; /* Chunk的大小,固定不变 */
unsigned int perslab; /* 每一个slab中可以存储对象的数量 */
void *slots; /* l链表记录slabclass中的所有空闲Chunk的列表*/
unsigned int sl_curr; /* 总的空闲的item的数量,即slots的数量 */
unsigned int slabs; /* 该slabclass中分配的slab的数量 */
void **slab_list; /*slab指针的数组 */
unsigned int list_size; /* slab的数量 */
size_t requested; /* 已经被分配的大小*/
} slabclass_t;
</code></pre><p>这里重点介绍slots、slabs和slab_list。 slots是记录了该SlabClass中所有空闲的Chunk列表,是Memcached中内存分配的入口。这里的空闲列表主要来自于两个部分:新分配的Slab的空闲Chunk;已经使用的Chunk过期回收的Chunk。slabs记录的目前该SlabClass已经分配到内存的Slab的总数。slab_list是指针的数组,它表示目前该SlabClass中所有Slab的指针,但并不表示所有的Slab都已经分配了内存。<br>下面我们介绍一下Memcached中数据存储对象Item的结构: </p>
<pre><code>typedef struct _stritem {
/* Protected by LRU locks */
struct _stritem *next;/*链表中下一个对象,有可能是指slots中的下一个,也可能是LRU链表中的下一个*/
struct _stritem *prev;
/* Rest are protected by an item lock */
struct _stritem *h_next; /* 相同hash值的hash链表中的下一个 */
rel_time_t time; /* 最近的访问时间 */
rel_time_t exptime; /* 过期时间 */
int nbytes; /* 数据大小 */
unsigned short refcount;
uint8_t nsuffix; /* length of flags-and-length string */
uint8_t it_flags; /* ITEM_* above */
uint8_t slabs_clsid;/* slab class的编号,表示该Item所在的slab class */
uint8_t nkey; /* 键值的长度*/
/* this odd type prevents type-punning issues when we do
* the little shuffle to save space when not using CAS. */
union {
uint64_t cas;
char end;
} data[];
/* if it_flags & ITEM_CAS we have 8 bytes CAS */
/* then null-terminated key */
/* then " flags length\r\n" (no terminating null) */
/* then data with terminating \r\n (no terminating null; it's binary!) */
} item;
</code></pre><p>Item是Memcached存储数据的基本单位,这里我们重点介绍data数组。该数组的组成可以分成4个部分:8bytes的CAS,以’\0’结尾的Key,flags,二进制格式的数据。<br>介绍了SlabClass和Item,接下去介绍一下Memcached的LRU实现。内存的不足肯定会导致数据的换入和换出,而Memcached作为内存存储,采用了LRU的方式来进行数据替换。每一个SlabClass独自维护了一套LRU队列,分别是head和tail变量来LRU队列的头部和尾部,尾部的Item就是最近最少使用的数据,会先被淘汰。 </p>
<img src="/2016/09/30/memcached-内存管理/1.png" alt="整体架构图" title="整体架构图">
<p>上图我们可以比较简单的看出的slabclass的内存的管理方式,但实际LRU实现远比这幅图要更为复杂,LRU策略分的更为细粒度。LRU对于Memcached来说单个SlabClass的LRU,而不是整体Memcached的LRU,这是由其内存的分配和管理方式决定,但是也可以通过内存的重分配来调整SlabClass之间的内存分配。<br>Memcached内存的分配和管理方式,虽然可以避免内存的碎片化,但带来的影响是会造成内存的浪费。举个列子:假设有Item的大小为50K,而可以存该Item的Chunk最合适大小为80K,这样就会造成30K内存的浪费。 </p>
<h3 id="4-源码分析"><a href="#4-源码分析" class="headerlink" title="4. 源码分析"></a>4. 源码分析</h3><p>通过上面内容的介绍,我们已经比较好的了解了Memcached的内存模型,下面我们从代码层面来看看Memcached是如何进行内存的分配的。<br>Slabclass数组的初始化: </p>
<pre><code>void slabs_init(const size_t limit, const double factor, const bool prealloc, const uint32_t *slab_sizes) {
int i = POWER_SMALLEST - 1; //确定SlabClass数组开始分配的起点
unsigned int size = sizeof(item) + settings.chunk_size;
//确定初始化SlabClass可以存储的固定大小,item记录元数据,chunk存储用户数据
mem_limit = limit;
if (prealloc) {
/* Allocate everything in a big chunk with malloc */
mem_base = malloc(mem_limit);
if (mem_base != NULL) {
mem_current = mem_base;
mem_avail = mem_limit;
} else {
fprintf(stderr, "Warning: Failed to allocate requested memory in"
" one large chunk.\nWill allocate in smaller chunks\n");
}
}
memset(slabclass, 0, sizeof(slabclass));
while (++i < MAX_NUMBER_OF_SLAB_CLASSES-1) {
if (slab_sizes != NULL) {
if (slab_sizes[i-1] == 0)
break;
size = slab_sizes[i-1];
} else if (size >= settings.slab_chunk_size_max / factor) {
break;
}
/* 这段代码保证在Memcached中存储的对象都是对齐的,默认是使用8位对齐的*/
if (size % CHUNK_ALIGN_BYTES)
size += CHUNK_ALIGN_BYTES - (size % CHUNK_ALIGN_BYTES);
/* 计算该slabclass存在的item的大小和数量*/
slabclass[i].size = size;
slabclass[i].perslab = settings.slab_page_size / slabclass[i].size;
/* 下一个slab的大小的size是按factor参数规律扩展,可以通过调节factor大小来控制memcached的存储分配 */
if (slab_sizes == NULL)
size *= factor;
if (settings.verbose > 1) {
fprintf(stderr, "slab class %3d: chunk size %9u perslab %7u\n",
i, slabclass[i].size, slabclass[i].perslab);
}
}
/* power_largest表示最后一个slabclass, 大小设置成item_size_max,可以存储我们约定的最大的对象,一个slab只能存一个item,一个slab默认的大小是1M */
power_largest = i;
slabclass[power_largest].size = settings.slab_chunk_size_max;
slabclass[power_largest].perslab = settings.slab_page_size / settings.slab_chunk_size_max;
if (settings.verbose > 1) {
fprintf(stderr, "slab class %3d: chunk size %9u perslab %7u\n",
i, slabclass[i].size, slabclass[i].perslab);
}
/* for the test suite: faking of how much we've already malloc'd */
{
char *t_initial_malloc = getenv("T_MEMD_INITIAL_MALLOC");
if (t_initial_malloc) {
mem_malloced = (size_t)atol(t_initial_malloc);
}
}
/* 如果需要预分配内存,则会执行这段逻辑,进行预分配 */
if (prealloc) {
/* 默认每个slabclass分配一个slab */
slabs_preallocate(power_largest);
}
}
</code></pre><p>从slabclass的初始化代码我们可知,每一个slabclass的可以存储的item大小是固定,不同的slabclass的item大小的增长是由growth_rate来控制(默认是1.25)。growth_rate的大小会影响到内存的使用率,这个需要根据应用来进行调优。<br>在下面介绍了slabclass的初始化中,有不少系统的设置,其定义具体如下: </p>
<pre><code>static void settings_init(void) {
settings.use_cas = true;
settings.access = 0700;
settings.port = 11211;
settings.udpport = 11211;
/* By default this string should be NULL for getaddrinfo() */
settings.inter = NULL;
settings.maxbytes = 64 * 1024 * 1024; /* default is 64MB */
settings.maxconns = 1024; /* to limit connections-related memory to about 5MB */
settings.verbose = 0;
settings.oldest_live = 0;
settings.oldest_cas = 0; /* supplements accuracy of oldest_live */
settings.evict_to_free = 1; /* push old items out of cache when memory runs out */
settings.socketpath = NULL; /* by default, not using a unix socket */
settings.factor = 1.25;
settings.chunk_size = 48; /* 初始化的chunck_size */
settings.num_threads = 4; /* N workers */
settings.num_threads_per_udp = 0;
settings.prefix_delimiter = ':';
settings.detail_enabled = 0;
settings.reqs_per_event = 20;
settings.backlog = 1024;
settings.binding_protocol = negotiating_prot;
settings.item_size_max = 1024 * 1024; /* 1M大小限制,它限制了Memcached缓存对象的最大值. */
settings.slab_page_size = 1024 * 1024; /* chunks are split from 1MB pages. */
settings.slab_chunk_size_max = settings.slab_page_size;
settings.sasl = false;
settings.maxconns_fast = false;
settings.lru_crawler = false;
settings.lru_crawler_sleep = 100;
settings.lru_crawler_tocrawl = 0;
settings.lru_maintainer_thread = false;
settings.hot_lru_pct = 32;
settings.warm_lru_pct = 32;
settings.expirezero_does_not_evict = false;
settings.idle_timeout = 0; /* disabled */
settings.hashpower_init = 0;
settings.slab_reassign = false;
settings.slab_automove = 0;
settings.shutdown_command = false;
settings.tail_repair_time = TAIL_REPAIR_TIME_DEFAULT;
settings.flush_enabled = true;
settings.crawls_persleep = 1000;
settings.logger_watcher_buf_size = LOGGER_WATCHER_BUF_SIZE;
settings.logger_buf_size = LOGGER_BUF_SIZE;
}
</code></pre><p>上面介绍了SlabClass的初始化,这里介绍一下每一个slab是如何进行分配的。</p>
<pre><code>static int do_slabs_newslab(const unsigned int id) {
slabclass_t *p = &slabclass[id];
slabclass_t *g = &slabclass[SLAB_GLOBAL_PAGE_POOL];
/*如果Memcached要进行重分配的话,则默认使用Slab的最大值*/
int len = (settings.slab_reassign || settings.slab_chunk_size_max != settings.slab_page_size)
? settings.slab_page_size
: p->size * p->perslab;
char *ptr;
if ((mem_limit && mem_malloced + len > mem_limit && p->slabs > 0
&& g->slabs == 0)) {
mem_limit_reached = true;
MEMCACHED_SLABS_SLABCLASS_ALLOCATE_FAILED(id);
return 0;
}
/* grow_slab_list方法中实现了slabclass中slab size的重分配,如果slabs<list_size,说明还有空余的slab,否则进行2倍的扩张(默认是16个) */
if ((grow_slab_list(id) == 0) ||
(((ptr = get_page_from_global_pool()) == NULL) &&
((ptr = memory_allocate((size_t)len)) == 0))) {
MEMCACHED_SLABS_SLABCLASS_ALLOCATE_FAILED(id);
return 0;
}
memset(ptr, 0, (size_t)len);
/* 获取一个空闲的Slab,用指针ptr表示 */
split_slab_page_into_freelist(ptr, id);
p->slab_list[p->slabs++] = ptr;
MEMCACHED_SLABS_SLABCLASS_ALLOCATE(id);
return 1;
}
</code></pre><p>SlabClass在进行Slab的申请的过程中,会先判断Slab是否已经达到上限(slabs>=list_size),然后来申请一个Slab的内存。在申请完Slab的内容后,Memcached会对该Slab进行划分,划分成一个个空闲的Chunk。下面我们具体看看空闲slab是如何获取的。</p>
<pre><code>static void do_slabs_free(void *ptr, const size_t size, unsigned int id) {
slabclass_t *p;
item *it;
assert(id >= POWER_SMALLEST && id <= power_largest);
if (id < POWER_SMALLEST || id > power_largest)
return;
MEMCACHED_SLABS_FREE(size, id, ptr);
p = &slabclass[id];
/*把新分配处理的Item添加到空闲列表Slots中去*/
it = (item *)ptr;
if ((it->it_flags & ITEM_CHUNKED) == 0) {
it->it_flags = ITEM_SLABBED;
it->slabs_clsid = 0;
it->prev = 0;
it->next = p->slots;
if (it->next) it->next->prev = it;
p->slots = it;
p->sl_curr++;
p->requested -= size;
} else {
do_slabs_free_chunked(it, size, id, p);
}
return;
}
</code></pre><p>所以memcached每申请一个新的slab,都会把该slab进行item化的划分,并使用链表来记录。到此为止,所有的Memcached的内存都已经初始化了,结果是建立了所有slabclass的数组和每个slabclass中的数量(默认是16个),每个slabclass中的大小固定,每个slab中的数量固定,并初始化了一个Slab中的所有空闲Item。而所有的item在一个slot中是采用链表来记录的,数据结构为slots,这个是垮slab,所有的slab中的item都可以找到,并且是顺序存储的。<br>介绍了上面的slab的分配,下面重点介绍Item是如何获取内存的,可以该方法是Memcached内存分配的最核心入口,代码如下: </p>
<pre><code>item *do_item_alloc(char *key, const size_t nkey, const unsigned int flags,
const rel_time_t exptime, const int nbytes) {
int i;
uint8_t nsuffix;
item *it = NULL;
char suffix[40];
size_t ntotal = item_make_header(nkey + 1, flags, nbytes, suffix, &nsuffix);
if (settings.use_cas) {
ntotal += sizeof(uint64_t);
}
//根据存储的总大小判断存储的slabclass,slabclass从1开始
unsigned int id = slabs_clsid(ntotal);
if (id == 0)
return 0;
/* If no memory is available, attempt a direct LRU juggle/eviction */
/* This is a race in order to simplify lru_pull_tail; in cases where
locked items are on the tail, you want them to fall out and cause
occasional OOM's, rather than internally work around them.
This also gives one fewer code path for slab alloc/free
*/
for (i = 0; i < 10; i++) {
uint64_t total_bytes;
/* 首先进行内存的回收,默认lru_maintainer_thread为false */
if (!settings.lru_maintainer_thread) {
lru_pull_tail(id, COLD_LRU, 0, 0);
}
/*进行了Item内存的分配*/
it = slabs_alloc(ntotal, id, &total_bytes, 0);
if (settings.expirezero_does_not_evict)
total_bytes -= noexp_lru_size(id);
/*根据item是否为空*/
if (it == NULL) {
if (settings.lru_maintainer_thread) {
lru_pull_tail(id, HOT_LRU, total_bytes, 0);
lru_pull_tail(id, WARM_LRU, total_bytes, 0);
if (lru_pull_tail(id, COLD_LRU, total_bytes, LRU_PULL_EVICT) <= 0)
break;
} else {
if (lru_pull_tail(id, COLD_LRU, 0, LRU_PULL_EVICT) <= 0)
break;
}
} else {
break;
}
}
if (i > 0) {
pthread_mutex_lock(&lru_locks[id]);
itemstats[id].direct_reclaims += i;
pthread_mutex_unlock(&lru_locks[id]);
}
/*没有足够的内存来保存*/
if (it == NULL) {
pthread_mutex_lock(&lru_locks[id]);
itemstats[id].outofmemory++;
pthread_mutex_unlock(&lru_locks[id]);
return NULL;
}
assert(it->slabs_clsid == 0);
//assert(it != heads[id]);
/* Refcount is seeded to 1 by slabs_alloc() */
it->next = it->prev = 0;
/* Items are initially loaded into the HOT_LRU. This is '0' but I want at
least a note here. Compiler (hopefully?) optimizes this out.
*/
if (settings.lru_maintainer_thread) {
if (exptime == 0 && settings.expirezero_does_not_evict) {
id |= NOEXP_LRU;
} else {
id |= HOT_LRU;
}
} else {
/* There is only COLD in compat-mode */
id |= COLD_LRU;
}
it->slabs_clsid = id;
DEBUG_REFCNT(it, '*');
it->it_flags |= settings.use_cas ? ITEM_CAS : 0;
it->nkey = nkey;
it->nbytes = nbytes;
memcpy(ITEM_key(it), key, nkey);
it->exptime = exptime;
memcpy(ITEM_suffix(it), suffix, (size_t)nsuffix);
it->nsuffix = nsuffix;
/* Need to shuffle the pointer stored in h_next into it->data. */
if (it->it_flags & ITEM_CHUNKED) {
item_chunk *chunk = (item_chunk *) ITEM_data(it);
chunk->next = (item_chunk *) it->h_next;
chunk->prev = 0;
chunk->head = it;
/* Need to chain back into the head's chunk */
chunk->next->prev = chunk;
chunk->size = chunk->next->size - ((char *)chunk - (char *)it);
chunk->used = 0;
assert(chunk->size > 0);
}
it->h_next = 0;
return it;
}
</code></pre><p>从这段代码中,item内存的分配是先进行LRU的判断,释放LRU中过期的Item,然后通过slab_alloc来进行分配,如果还没有足够的内存,继续进行LRU内存的释放(这次可以通过抛弃还未过期的最近最少使用的对象)。我们先看看lru_pull_tail的实现:</p>
<pre><code>static int lru_pull_tail(const int orig_id, const int cur_lru,
const uint64_t total_bytes, uint8_t flags) {
item *it = NULL;
int id = orig_id;
int removed = 0;
if (id == 0)
return 0;
int tries = 5;
item *search;
item *next_it;
void *hold_lock = NULL;
unsigned int move_to_lru = 0;
uint64_t limit = 0;
id |= cur_lru;
pthread_mutex_lock(&lru_locks[id]);
search = tails[id];
/* We walk up *only* for locked items, and if bottom is expired. */
for (; tries > 0 && search != NULL; tries--, search=next_it) {
/* we might relink search mid-loop, so search->prev isn't reliable */
next_it = search->prev;
if (search->nbytes == 0 && search->nkey == 0 && search->it_flags == 1) {
/* We are a crawler, ignore it. */
if (flags & LRU_PULL_CRAWL_BLOCKS) {
pthread_mutex_unlock(&lru_locks[id]);
return 0;
}
tries++;
continue;
}
uint32_t hv = hash(ITEM_key(search), search->nkey);
/* 尝试lock search item,如果失败,则跳过
*Attempt to hash item lock the "search" item. If locked, no
other callers can incr the refcount. Also skip ourselves. */
if ((hold_lock = item_trylock(hv)) == NULL)
continue;
/* Now see if the item is refcount locked */
if (refcount_incr(&search->refcount) != 2) {
/* Note pathological case with ref'ed items in tail.
Can still unlink the item, but it won't be reusable yet */
itemstats[id].lrutail_reflocked++;
/* In case of refcount leaks, enable for quick workaround. */
/* WARNING: This can cause terrible corruption */
if (settings.tail_repair_time &&
search->time + settings.tail_repair_time < current_time) {
itemstats[id].tailrepairs++;
search->refcount = 1;
/* This will call item_remove -> item_free since refcnt is 1 */
do_item_unlink_nolock(search, hv);
item_trylock_unlock(hold_lock);
continue;
}
}
/* Expired or flushed */
if ((search->exptime != 0 && search->exptime < current_time)
|| item_is_flushed(search)) {
itemstats[id].reclaimed++;
if ((search->it_flags & ITEM_FETCHED) == 0) {
itemstats[id].expired_unfetched++;
}
/* refcnt 2 -> 1 */
do_item_unlink_nolock(search, hv);
/* refcnt 1 -> 0 -> item_free */
do_item_remove(search);
item_trylock_unlock(hold_lock);
removed++;
/* If all we're finding are expired, can keep going */
continue;
}
/* If we're HOT_LRU or WARM_LRU and over size limit, send to COLD_LRU.
If we're COLD_LRU, send to WARM_LRU unless we need to evict
*/
switch (cur_lru) {
case HOT_LRU:
limit = total_bytes * settings.hot_lru_pct / 100;
case WARM_LRU:
if (limit == 0)
limit = total_bytes * settings.warm_lru_pct / 100;
if (sizes_bytes[id] > limit) {
itemstats[id].moves_to_cold++;
move_to_lru = COLD_LRU;
do_item_unlink_q(search);
it = search;
removed++;
break;
} else if ((search->it_flags & ITEM_ACTIVE) != 0) {
/* Only allow ACTIVE relinking if we're not too large. */
itemstats[id].moves_within_lru++;
search->it_flags &= ~ITEM_ACTIVE;
do_item_update_nolock(search);
do_item_remove(search);
item_trylock_unlock(hold_lock);
} else {
/* Don't want to move to COLD, not active, bail out */
it = search;
}
break;
case COLD_LRU:
it = search; /* No matter what, we're stopping */
if (flags & LRU_PULL_EVICT) {
if (settings.evict_to_free == 0) {
/* Don't think we need a counter for this. It'll OOM. */
break;
}
itemstats[id].evicted++;
itemstats[id].evicted_time = current_time - search->time;
if (search->exptime != 0)
itemstats[id].evicted_nonzero++;
if ((search->it_flags & ITEM_FETCHED) == 0) {
itemstats[id].evicted_unfetched++;
}
LOGGER_LOG(NULL, LOG_EVICTIONS, LOGGER_EVICTION, search);
do_item_unlink_nolock(search, hv);
removed++;
if (settings.slab_automove == 2) {
slabs_reassign(-1, orig_id);
}
} else if ((search->it_flags & ITEM_ACTIVE) != 0
&& settings.lru_maintainer_thread) {
itemstats[id].moves_to_warm++;
search->it_flags &= ~ITEM_ACTIVE;
move_to_lru = WARM_LRU;
do_item_unlink_q(search);
removed++;
}
break;
}
if (it != NULL)
break;
}
pthread_mutex_unlock(&lru_locks[id]);
if (it != NULL) {
if (move_to_lru) {
it->slabs_clsid = ITEM_clsid(it);
it->slabs_clsid |= move_to_lru;
item_link_q(it);
}
do_item_remove(it);
item_trylock_unlock(hold_lock);
}
return removed;
}
</code></pre><p><br><br></p>
]]></content>
<summary type="html">
<![CDATA[<p>基于1.4.31的memcached 介绍memcached的内存管理<br>]]>
</summary>
</entry>
<entry>
<title><![CDATA[反作弊实践经验分享]]></title>
<link href="http://yoursite.com/2016/06/30/%E5%8F%8D%E4%BD%9C%E5%BC%8A%E5%AE%9E%E8%B7%B5%E7%BB%8F%E9%AA%8C%E5%88%86%E4%BA%AB/"/>
<id>http://yoursite.com/2016/06/30/反作弊实践经验分享/</id>
<published>2016-06-30T15:12:02.000Z</published>
<updated>2016-06-30T15:20:52.000Z</updated>
<content type="html"><![CDATA[<p>前言<br>互联网产品经常会举行一些活动,数量繁多,形式各异。比如朋友圈集赞送礼,转发微博抽奖,拉新用户送福利,参加活动领取优惠券等等。这些活动,举办方大都有一些明确的目标,比如宣传,拉新,促销等。<br><a id="more"></a><br>然而在活动具体实施过程中,有一些恶意用户使用各种手段来“作弊”,企图在不遵守活动规则的前提下也实现获利。他们的这种作弊行为,对活动举办方会造成很大损害,不但造成经济损失,也损害了其他正常参加活动用户的利益,可能导致举办方信誉受损,最终导致活动效果大打折扣,活动目标无法实现。其中有一些人还自称为“羊毛客”,目的就是来薅(hāo)羊毛。<br>信息安全部在给大量网易产品提供反作弊服务的过程中,也积累了一些经验。本文就尝试罗列一些常见的作弊方法和作弊资源,再给出一些防御工具和注意事项,供大家参考和讨论。<br>具体一个活动中的作弊用户可能有好几拨,不同团队的作弊水平有高有低,差距还可能很大。基本可以按作弊手段里结合黑产资源的多少,也就是作弊成本高低来区分。对于低水平的作弊用户,用简单的规则和手段封堵某些点就可以了。但是对于作弊手段高明的人,就很难用片面的策略来发现了。那么我们在做防御方案的时候,也需要全面的应对,用系统化的方案来应对。</p>
<h2 id="1-常见作弊需要的资源"><a href="#1-常见作弊需要的资源" class="headerlink" title="1. 常见作弊需要的资源"></a>1. 常见作弊需要的资源</h2><p>俗话说“知己知彼,百战百胜”,下面我们就先从作弊者的角度来看看有哪些东西可以利用来作弊。</p>
<h3 id="1-1-帐号"><a href="#1-1-帐号" class="headerlink" title="1.1 帐号"></a>1.1 帐号</h3><p>去某宝上搜一下,各个产品的帐号都有,大量批发还更加便宜。(微博,微信,网易通行证,各种邮箱帐号等等)。直接购买就可以使用(提供帐号密码),省去了注册的流程。</p>
<h3 id="1-2-IP"><a href="#1-2-IP" class="headerlink" title="1.2. IP"></a>1.2. IP</h3><p>某宝上也有大量代理IP出售,价格实惠,甚至还提供包月的服务。当然自己写一些爬虫去收集代理IP也行,更牛的自己掌握一些肉鸡就更方便了。用不同的IP登录不同的帐号是一种比较好的隐藏自己行踪的方法,通常很多产品都会对IP做高频限制(同IP下参与用户数过多)。</p>
<h3 id="1-3-手机短信验证码"><a href="#1-3-手机短信验证码" class="headerlink" title="1.3. 手机短信验证码"></a>1.3. 手机短信验证码</h3><p>类似51验证码等手机短信验证码平台现在也是难以杜绝。这种平台都有一定的卡商资源,并且平台对使用方式上有一定的包装,一般都有相应的api或者工具包。方便程序自动实现获取手机号以及该手机号收到的验证码。绝对的真实有效,保质保量。价格也是根据量不同有不同优惠。一般情况下收一个验证码6分钱。</p>
<p>在这些平台上游的卡商,一般是掌握大量手机卡的人,他们可以利用卡猫等设备将手机卡和电脑相连,再用平台给的工具实现平台接入。各种未经实名认证(可能还未出售)的手机号码就能实现接收验证码短信了。</p>
<h3 id="1-4-打码平台"><a href="#1-4-打码平台" class="headerlink" title="1.4. 打码平台"></a>1.4. 打码平台</h3><p>这个是指人工输入图形验证码。对于简单的验证码,用机器识别即可(成功率相当高),对于复杂度较高的验证码,平台也支持人工打码支持。7*24小时服务,每日结算费用。</p>
<h3 id="1-5-模拟器"><a href="#1-5-模拟器" class="headerlink" title="1.5. 模拟器"></a>1.5. 模拟器</h3><p>模拟器通常是指安卓的模拟器,安卓模拟器非常强大,种类也很多,基本可以模拟一个真实的手机全部功能。比如BlueStacks等。连GPS信息都可以模拟。</p>
<h3 id="1-6-专门工具"><a href="#1-6-专门工具" class="headerlink" title="1.6. 专门工具"></a>1.6. 专门工具</h3><p>专门的黑客技术人员会编写有针对性的工具和程序,结合模拟器以及上面提到的各种黑产资源,可以做出一整套自动化的工具。比如现在很多帐号注册机,就支持更换IP,更换手机号码,对接打码平台等功能。据说有这样技术的高端黑客上游,一个可以支撑数十个下游的黑产团队。</p>
<h2 id="2-一些防御措施和准备工作"><a href="#2-一些防御措施和准备工作" class="headerlink" title="2. 一些防御措施和准备工作"></a>2. 一些防御措施和准备工作</h2><p>前面了解了一些常见作弊可能使用的东西之后,我们再来分析自己能做的事情有哪些。</p>
<h3 id="2-1-基本原则"><a href="#2-1-基本原则" class="headerlink" title="2.1. 基本原则"></a>2.1. 基本原则</h3><p>既然很多作弊都是程序自动化的行为,那么我们就应该尝试做人机识别。其实人机识别做好了所有问题就解决了,不过人机识别这个概念范围太广了,我们还是从一些具体的目前现成的技术手段来说明。由于作弊手段是多样的,所以防御措施也没有一劳永逸的好事,但是我们可以遵循的一个基本防御原则就是:提升作弊成本。</p>
<h3 id="2-2-验证码"><a href="#2-2-验证码" class="headerlink" title="2.2. 验证码"></a>2.2. 验证码</h3><p>人机识别里面性价比较高的方案就是验证码,并且最常见的就是数字字母验证码。可以在信息安全的公共技术库中找到验证码工具。普通的数字字母验证码还是有一定的破解率的,需要定期更换样式保证较低的破解率。汉字验证码防机器人效果是不错,不过用户体验不好。一旦有了验证码,他们需要破解就需要额外增加代码服务,提升了作弊成本。同时我们也在研发各种新形势的验证码,比如拖条,拼图,文字点选等等,后续也会补充到公共技术库中供有需要的部门选用。</p>
<h3 id="2-3-手机短信验证"><a href="#2-3-手机短信验证" class="headerlink" title="2.3. 手机短信验证"></a>2.3. 手机短信验证</h3><p>是手机验证码,虽然有打码平台存在,但是接入也是需要成本,并且是按手机号码个数计费,成本也不低。提升了作弊的成本,羊毛党自然的会去找成本更低,更容易的地方薅羊毛了。</p>
<h3 id="2-4-设备ID-浏览器指纹"><a href="#2-4-设备ID-浏览器指纹" class="headerlink" title="2.4. 设备ID/浏览器指纹"></a>2.4. 设备ID/浏览器指纹</h3><p>记录移动设备ID,浏览器指纹,做高频限制,统计分析。常见的手机MAC地址,IMEI号等有专门的黑客工具做了破解,所以对于设备ID的生成,传输,存储也需要有更加安全的方案。这里有一个对抗的过程,我们推出一些新的设备ID算法(包括指纹算法),经过一段时间后黑客也可能破解其中一二,然后我们就需要准备升级。目前信息安全部的公共技术库中也已经有成熟的设备ID(支持IOS和安卓)和浏览器指纹的SDK。</p>
<p>实践证明设备ID和指纹还是非常有效的手段,那种利用手机验证码平台的用户很多都是被设备ID规则拦截下来的。</p>
<h3 id="2-5-IP规则"><a href="#2-5-IP规则" class="headerlink" title="2.5. IP规则"></a>2.5. IP规则</h3><p>IP也是有限的资源,虽然有很多代理售卖,但也是需要成本的。并且IP的概念比较深入人心,用IP做规则,普通用户也容易接受。IP高频限制,作为最最基本的防御措施,还是需要保留的。</p>
<h3 id="2-6-数据分析"><a href="#2-6-数据分析" class="headerlink" title="2.6. 数据分析"></a>2.6. 数据分析</h3><p>再复杂一点的方案就是数据分析。这个分析分成两个方面,一个方面是分析机器人:收集用户的行为轨迹后分析判断是否真实用户。比如用户的鼠标轨迹,用户各个页面的停留时间等。初期可以直接用简单规则配合判断,比如是否有经过活动设定的页面路径等。另一个方面是分析正常用户:根据用户的历史行为数据分析是否为正常人,从而保护用户免受各种反作弊规则的误判。</p>
<h3 id="2-7-游戏规则"><a href="#2-7-游戏规则" class="headerlink" title="2.7. 游戏规则"></a>2.7. 游戏规则</h3><p> 很多作弊的人被拦截后会投诉并且质问拦截原因,他们希望了解更多的防御措施再做有针对性的破解。那么一个活动在开始之前,先说明游戏规则就很有必要了。良好的措辞可以避免很多不必要的纠纷。参考支付宝的一个通用活动总则:每次活动中,每个用户只限参加一次活动,每个用户只能中奖一次。同一手机、同一联系方式、同一IP地址、同一支付宝账户、同一身份证件、同一银行卡号、同一收货地址、同一终端设备号或其他可以合理显示为同一用户的情形,均视为同一用户。</p>
<p> 我们先把丑话说在前头,当有用户投诉的时候,告诉他们违反了活动规则即可。至于具体的判断依据,可以说是有合理情形判断为同一用户。记住:活动规则很重要。但这里并不是说把这些活动细则一下子全展现给用户。而是加一些链接到更加详细的规则说明页面去。支付宝,京东都是这么做的。</p>
<h3 id="2-8-延时发奖"><a href="#2-8-延时发奖" class="headerlink" title="2.8. 延时发奖"></a>2.8. 延时发奖</h3><p> 建议各种奖励都不要立刻结算,一方面对反作弊的工作压力很大,另外一方面对于作弊的人也容易试探规则,只要看是否领奖成功即可。因为上述各种反作弊的措施,都需要一定的时间来完成,时间上给予一些宽裕,方便反作弊工作的实施,也可以实现更加精确的打击。比如有一些作弊者在一台新机器上第一次使用,设备ID也还是全新的,如果是实时判断的话可能就无法识别,但如果累计了一天后再看,就可以发现这个设备ID下聚集了大量帐号。敌明我暗,成功率就高了很多。</p>
<h3 id="2-9-抽奖"><a href="#2-9-抽奖" class="headerlink" title="2.9. 抽奖"></a>2.9. 抽奖</h3><p> 抽奖是一个减少投诉的好方法,但可能对用户吸引力稍弱一些。但是对于被反作弊拦截的用户,全部都可以用抽奖没抽到为理由回复用户,客服压力会小很多。</p>
<h3 id="2-10-客服准备"><a href="#2-10-客服准备" class="headerlink" title="2.10. 客服准备"></a>2.10. 客服准备</h3><p> 这里指的是客服和活动策划,反作弊人员之间的前期沟通交流和规范制定。比如规则的保密性,应对投诉的说法等。并且反作弊也不能做到100%精确,有可能存在误伤了真实用户,对于投诉过来的,可以及时反查用户情况,误伤的尽量给予补偿。并且根据误伤情况反馈,更新防御规则。</p>
<h2 id="3-总结"><a href="#3-总结" class="headerlink" title="3. 总结"></a>3. 总结</h2><p> 反作弊是一个对抗的过程,本质就是作弊和防御成本的较量。我们想完全抓出每一个作弊的用户是非常困难的,我们能做的只有不断抬高作弊成本,至少比其他产品活动成本要高,作弊用户自然就不会骚扰你的产品了。</p>
]]></content>
<summary type="html">
<![CDATA[<p>前言<br>互联网产品经常会举行一些活动,数量繁多,形式各异。比如朋友圈集赞送礼,转发微博抽奖,拉新用户送福利,参加活动领取优惠券等等。这些活动,举办方大都有一些明确的目标,比如宣传,拉新,促销等。<br>]]>
</summary>
<category term="反作弊" scheme="http://yoursite.com/tags/%E5%8F%8D%E4%BD%9C%E5%BC%8A/"/>
</entry>
<entry>
<title><![CDATA[java数据类型转换引发的血案]]></title>
<link href="http://yoursite.com/2016/06/02/java%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E8%BD%AC%E6%8D%A2%E5%BC%95%E5%8F%91%E7%9A%84%E8%A1%80%E6%A1%88/"/>
<id>http://yoursite.com/2016/06/02/java数据类型转换引发的血案/</id>
<published>2016-06-02T15:18:19.000Z</published>
<updated>2016-06-02T15:24:04.000Z</updated>
<content type="html"><![CDATA[<p>问题的起因<br>数据类型转换,应该是个很基础的问题,我们刚接触编程时就会学习相关的知识,但是有些东西看似基础,就算有一定经验的码农也可能栽在这些很基础的问题上。先说说今天为什么要来讨论下java的数据类型转换。<br><a id="more"></a><br>事情的起因是这样的:某一天,线上用户突然反馈说,歌曲的下载链接似乎不起作用,过期时间怎么不对。原意是想要10年后过期,但是结果似乎只有20天左右。<br>相关的代码如下:<br><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">generatePresignedUrlRequest.setExpiration(<span class="keyword">new</span> Date(System.currentTimeMillis()+<span class="number">10</span>*<span class="number">365</span>*<span class="number">3600</span>*<span class="number">1000</span>*<span class="number">24</span>));</span><br></pre></td></tr></table></figure></p>
<p>也就是说想设置的过期时间是,当前时间后的10年,具体的值:<br><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">System.currentTimeMillis()+<span class="number">10</span>*<span class="number">365</span>*<span class="number">3600</span>*<span class="number">1000</span>*<span class="number">24</span></span><br></pre></td></tr></table></figure></p>
<p>细看这段代码,就一个简单的数值计算,不太可能出问题。但是写一段简单的代码验证一下,问题原因就显露出来了:<br><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">long</span> currentTime = System.currentTimeMillis();</span><br><span class="line">System.out.println(currentTime);</span><br><span class="line"><span class="keyword">long</span> expire = currentTime + <span class="number">10</span>*<span class="number">365</span>*<span class="number">3600</span>*<span class="number">1000</span>*<span class="number">24</span>;</span><br><span class="line">System.out.println(expire);</span><br><span class="line">System.out.println(expire-currentTime);</span><br></pre></td></tr></table></figure></p>
<p>输出结果:<br>1440056798539<br>1441884185931<br>1827387392<br>我们预期expire-currentTime应该是315360000000,为什么实际只有1827387392(正好约等于20天的毫秒数),看起来像是溢出了。<br>为什么会这样<br>在java中数据类型转换分为自动转换和强制转换,强制转换比较显而易见,理解上不容易出问题。今天要探讨的是自动类型转换。自动转换不需要用户提前声明,一般是从位数低的类型向位数高的类型转换,转换的优先级如下:<br>低————————————–>高<br>byte,short,char -> int -> long -> float -> double<br>运算中,不同类型的数据先转化为同一类型,然后进行运算。<br>按上面的叙述,System.currentTimeMillis()+10<em>365</em>3600<em>1000</em>24,后面的10<em>365</em>3600<em>1000</em>24不应该被自动转换成long了吗?那样就不会存在溢出问题了。通过上面的代码验证我们可以看到,事情并没有按我们的预期发展。为什么会这样呢? 问题就出在各运算符是有优先级的,执行运算的时候如此,进行数据类型自动转换时也如此。<br>乘法的运算优先级要高于加法,因此,java会执行完乘法运算后再执行加法运行。而执行乘法运算时,所有的操作数都是int型,因此没有触发自动类型转换,运算的结果自然是int型的。但是,10<em>365</em>3600<em>1000</em>24的结果超过了int型变量的最大值(java中的int是32位的,范围是-2147483684~2147483647),于是乘法操作得到的结果并不是我们预期的大小。因此当再执行加法操作时,虽然乘法的结果数被自动转换成long了,但是由于执行乘法操作时已经溢出,所以最终的结果也就比我们预期的值小很多了。<br>如何解决<br>既然知道了原因,解决起来当然就很简单了,我们可以在乘法运算时进行一次强制类型转换:<br><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">System.currentTimeMillis()+(<span class="number">10L</span>*<span class="number">365</span>*<span class="number">3600</span>*<span class="number">1000</span>*<span class="number">24</span>)</span><br></pre></td></tr></table></figure></p>
<p>测试一下:<br><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">long</span> currentTime = System.currentTimeMillis();</span><br><span class="line">System.out.println(currentTime);</span><br><span class="line"><span class="keyword">long</span> expire = currentTime + <span class="number">10L</span>*<span class="number">365</span>*<span class="number">3600</span>*<span class="number">1000</span>*<span class="number">24</span>;</span><br><span class="line">System.out.println(expire);</span><br><span class="line">System.out.println(expire-currentTime);</span><br></pre></td></tr></table></figure></p>
<p>结果如下:<br>1440062994479<br>1755422994479<br>315360000000</p>
]]></content>
<summary type="html">
<![CDATA[<p>问题的起因<br>数据类型转换,应该是个很基础的问题,我们刚接触编程时就会学习相关的知识,但是有些东西看似基础,就算有一定经验的码农也可能栽在这些很基础的问题上。先说说今天为什么要来讨论下java的数据类型转换。<br>]]>
</summary>
<category term="-java" scheme="http://yoursite.com/tags/java/"/>
</entry>
<entry>
<title><![CDATA[高并发下Java Random类使用]]></title>
<link href="http://yoursite.com/2016/06/01/%E9%AB%98%E5%B9%B6%E5%8F%91%E4%B8%8BJava-Random%E7%B1%BB%E4%BD%BF%E7%94%A8/"/>
<id>http://yoursite.com/2016/06/01/高并发下Java-Random类使用/</id>
<published>2016-06-01T13:00:55.000Z</published>
<updated>2016-06-01T13:15:30.000Z</updated>
<content type="html"><![CDATA[<p>俗话说,常在河边站哪有不湿鞋,java用多了,多多少少会遇到一些坑。 例如String.split方法:<br><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">System.out.println(<span class="string">""</span>.split(<span class="string">","</span>).length); <span class="comment">// 结果神奇的是1。使用commons-lang的StringUtils.split方法代替,会返回0,比较符合正常人思维</span></span><br></pre></td></tr></table></figure></p>
<p>而今天的主角是Random类(java.util.Random)。先声明,这是个线程安全的类。<br><a id="more"></a><br>先说结论</p>
<ul>
<li>在高并发时,不要共用同一个Random实例</li>
<li>在高并发时,不要使用Math.random生成随机数</li>
<li>Java1.7开始,使用ThreadLocalRandom类代替</li>
</ul>
<p>用了会怎样?<br>如果你很好奇,那么我只能告诉你,并发不高时,一切都工作的很好;但并发高了,就像以多线程的方式开启了一堆死循环,会把每一个cpu都跑满!<br>在高并发时,不要共用同一个Random实例?<br>因为在高并发时效率真的是太差了,从jdk1.7开始,Random的javadoc中多了这么一句:</p>
<blockquote>
<p>Instances of java.util.Random are threadsafe. However, the concurrent use of the same java.util.Random instance across threads may encounter contention and consequent poor performance. Consider instead using ThreadLocalRandom in multithreaded designs.</p>
</blockquote>
<p>看看源码,事实上Random.nextXXX方法都调用了Random.next方法,那么我们看看这个方法,就能找到答案:<br><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">int</span> <span class="title">next</span><span class="params">(<span class="keyword">int</span> bits)</span> </span>{</span><br><span class="line"> <span class="keyword">long</span> oldseed, nextseed;</span><br><span class="line"> AtomicLong seed = <span class="keyword">this</span>.seed;</span><br><span class="line"> do {</span><br><span class="line"> oldseed = seed.get();</span><br><span class="line"> nextseed = (oldseed * multiplier + addend) & mask;</span><br><span class="line"> } <span class="keyword">while</span> (!seed.compareAndSet(oldseed, nextseed)); <span class="comment">// 就是这里的,高并发时几乎每次比较都不相等</span></span><br><span class="line"> <span class="keyword">return</span> (<span class="keyword">int</span>)(nextseed >>> (<span class="number">48</span> - bits));</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p>
<p>java.util.Random 从Java1.0开始就存在了。它是一个线程安全类,理论上可以通过它同时在多个线程中获得互不相同的随机数。这样的线程安全是通过AtomicLong实现的。<br>Random 使用 AtomicLong CAS (compare-and-set)操作来更新它的seed,尽管很多非阻塞式算法中使用了非阻塞式原语,CAS在资源高度竞争时的表现依然糟糕。<br>在高并发时,不要使用Math.random生成随机数<br>原因是Math.random方法实际上使用了同一个Random实例!恰恰就是上面的情况。看看Math干了什么:<br><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="class"><span class="keyword">class</span> <span class="title">RandomNumberGeneratorHolder</span> </span>{</span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">final</span> Random randomNumberGenerator = <span class="keyword">new</span> Random(); <span class="comment">// 初始化一个共用的Random实例</span></span><br><span class="line">}</span><br><span class="line">...</span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">double</span> <span class="title">random</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> RandomNumberGeneratorHolder.randomNumberGenerator.nextDouble(); <span class="comment">// 使用共用的Random实例</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure></p>
<p>解决方案</p>
<blockquote>
<p>Instances of java.util.Random are threadsafe. However, the concurrent use of the same java.util.Random instance across threads may encounter contention and consequent poor performance. Consider instead using ThreadLocalRandom in multithreaded designs.</p>
</blockquote>
<p>在Java1.7开始,使用ThreadLocalRandom类代替。这个类实际上使得同一个线程使用同一个Random实例,而不同的线程使用不同的Random实例。使用方法:<br><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">ThreadLocalRandom.current().nextXXX(); <span class="comment">/// XXX代表可选的类型,例如Int</span></span><br></pre></td></tr></table></figure></p>
<p>如果不幸的你还在用老版本的Java(默哀…),那么使用ThreadLocal+Random实现一个类似功能的类;或者每次使用时new一个Random(这个又慢又浪费)吧。</p>
]]></content>
<summary type="html">
<![CDATA[<p>俗话说,常在河边站哪有不湿鞋,java用多了,多多少少会遇到一些坑。 例如String.split方法:<br><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">System.out.println(<span class="string">""</span>.split(<span class="string">","</span>).length); <span class="comment">// 结果神奇的是1。使用commons-lang的StringUtils.split方法代替,会返回0,比较符合正常人思维</span></span><br></pre></td></tr></table></figure></p>
<p>而今天的主角是Random类(java.util.Random)。先声明,这是个线程安全的类。<br>]]>
</summary>
<category term="Java" scheme="http://yoursite.com/tags/Java/"/>
<category term="高并发" scheme="http://yoursite.com/tags/%E9%AB%98%E5%B9%B6%E5%8F%91/"/>
</entry>
<entry>
<title><![CDATA[JVM优化的几个原则]]></title>
<link href="http://yoursite.com/2016/05/31/JVM%E4%BC%98%E5%8C%96%E7%9A%84%E5%87%A0%E4%B8%AA%E5%8E%9F%E5%88%99/"/>
<id>http://yoursite.com/2016/05/31/JVM优化的几个原则/</id>
<published>2016-05-31T04:34:33.000Z</published>
<updated>2016-05-31T05:49:23.000Z</updated>
<content type="html"><![CDATA[<p>本文通过GC日志分析总结JVM优化的一般规则<br><a id="more"></a></p>
<h2 id="JVM调优的一般原则"><a href="#JVM调优的一般原则" class="headerlink" title="JVM调优的一般原则"></a>JVM调优的一般原则</h2><ol>
<li>minor gc回收原则:每次minor gc尽量多的回收垃圾对象</li>
<li>GC内存最大化原则:堆空间越大、垃圾回收效果越好</li>
<li>吞吐量、延迟、内存占用,三选二进行调优</li>
</ol>
<h2 id="调优前后配置对比"><a href="#调优前后配置对比" class="headerlink" title="调优前后配置对比"></a>调优前后配置对比</h2><h3 id="默认的配置:"><a href="#默认的配置:" class="headerlink" title="默认的配置:"></a>默认的配置:</h3><p>-Xms350m<br>-Xmx750m<br>-XX:MaxPermSize=350m<br>-XX:ReservedCodeCacheSize=96m<br>-ea<br>-Dsun.io.useCanonCaches=false<br>-Djava.net.preferIPv4Stack=true<br>-XX:+UseCodeCacheFlushing<br>-XX:+UseConcMarkSweepGC<br>-XX:SoftRefLRUPolicyMSPerMB=50<br>-Dawt.useSystemAAFontSettings=lcd</p>
<h3 id="修改的第一版本配置:"><a href="#修改的第一版本配置:" class="headerlink" title="修改的第一版本配置:"></a>修改的第一版本配置:</h3><p>-Xms750m<br>-Xmx750m<br>-XX:PermSize=350m<br>-XX:MaxPermSize=350m<br>-XX:ReservedCodeCacheSize=96m<br>-XX:+PrintGCTimeStamps<br>-XX:+PrintGCDetails<br>-Xloggc:/tmp/idea.log<br>-ea<br>-Dsun.io.useCanonCaches=false<br>-Djava.net.preferIPv4Stack=true<br>-XX:+UseCodeCacheFlushing<br>-XX:+UseConcMarkSweepGC<br>-XX:SoftRefLRUPolicyMSPerMB=50<br>-Dawt.useSystemAAFontSettings=lcd</p>
<h2 id="实战"><a href="#实战" class="headerlink" title="实战"></a>实战</h2><h3 id="一-内存大小的调整"><a href="#一-内存大小的调整" class="headerlink" title="一:内存大小的调整"></a>一:内存大小的调整</h3><h3 id="gc-日志分析"><a href="#gc-日志分析" class="headerlink" title="gc 日志分析"></a>gc 日志分析</h3><p>完整的gc日志:</p>
<blockquote>
<p>1406.535: [GC 1406.535: [ParNew: 51390K->958K(57536K), 0.0176440 secs] 421864K->371433K(761664K), 0.0178630 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]</p>
</blockquote>
<ol>
<li>GC 1406.535:触发的是minor gc 时间点是jvm启动到现在的时间:1406.535</li>
<li>[ParNew: 51390K->958K(57536K), 0.0176440 secs]:新生代大小是57536,使用的是ParNew算法,从51390K降低到了958K。消耗的时间是:0.0176440秒。回收了50432 ,同时也告诉了我们survivor大小是958K</li>
<li>421864K->371433K(761664K), 0.0178630 secs : 总共的堆大小是761664,一次minor gc后从421864降到了371433,降低的是50431,也就是说有1k的数据从新生代进入了老生代。但是老生代大小没有发生变化,估计是它自己计算错误了。</li>
<li><p>[Times: user=0.02 sys=0.00, real=0.01 secs]:用户态消耗的时间是:0.02 系统态消耗的时间:0.00,实际消耗的时间0.01<br>从上面数据可以看出堆大小:761664K(743M) 新生代:57536K(56M) 老生代:704128K(687M)<br>看一次fullgc的情况</p>
<blockquote>
<p>20.837: [Full GC (System) 20.837: [CMS: 109217K->80428K(704128K), 0.5815740 secs] 126205K->80428K(761664K), [CMS Perm : 113263K->113166K(358400K)], 0.5817070 secs] [Times: user=0.57 sys=0.02, real=0.58 secs]</p>
</blockquote>
</li>
<li><p>[CMS: 109217K->80428K(704128K), 0.5815740 secs]:老生代大小从109217K降到了80428K,总共大小是704128K。之后就再也没有发生full gc,也就是说活跃的数据是80428K</p>
</li>
<li>[CMS Perm : 113263K->113166K(358400K);持久代的大小是358400K,活跃的数据是113166K<br>再看看一些gc情况:<blockquote>
<p>zhaoming@zhaoming:~$ jstat -gcutil 4943 5000 1000<br>S0 S1 E O P YGC YGCT FGC FGCT GCT<br>2.07 0.00 73.92 77.17 64.03 646 15.361 3 0.628 15.989<br>2.07 0.00 84.31 77.17 64.03 646 15.361 3 0.628 15.989<br>2.07 0.00 93.63 77.17 64.03 646 15.361 3 0.628 15.989<br>0.00 2.15 4.29 77.17 64.03 647 15.393 3 0.628 16.020<br>一直发生的是minor gc没有发生full gc的情况了。</p>
</blockquote>
</li>
</ol>
<h3 id="数据分析结论"><a href="#数据分析结论" class="headerlink" title="数据分析结论"></a>数据分析结论</h3><ol>
<li>老生代总大小:704128K(687M),堆大小:761664K(743M),新生代:57536K(56M)</li>
<li>老生代活跃数据大小:80428K,因为整个日志上看,只发生了一次full gc</li>
<li>垃圾回收器是CMS</li>
</ol>
<h3 id="应用优化原则一"><a href="#应用优化原则一" class="headerlink" title="应用优化原则一"></a>应用优化原则一</h3><ol>
<li>-Xmx和-Xms设置为活跃数据大小的3~4倍 这里就是80428的3~4倍(235m~~314M),当前设置的是750M,应该是有余的</li>
<li>PermSize和MaxPermSize设置为活跃数据大小的1.2~1.5倍(93M~~117M) ,当前也是有余的</li>
<li>新生代设置为活跃数据大小的1~1.5倍(78m~~117m),这里默认的新生代大小是不够的,我们可以提升下。</li>
</ol>
<h2 id="二-调优延迟-响应性-–总纲"><a href="#二-调优延迟-响应性-–总纲" class="headerlink" title="二 调优延迟/响应性 –总纲"></a>二 调优延迟/响应性 –总纲</h2><ol>
<li>测量minor gc的持续时间</li>
<li>统计minor的次数</li>
<li>测量Full gc的最差持续时间</li>
<li>统计最差情况下,Full gc的频率</li>
</ol>
<h2 id="三-调优延迟-响应性-–新生代"><a href="#三-调优延迟-响应性-–新生代" class="headerlink" title="三 调优延迟/响应性 –新生代"></a>三 调优延迟/响应性 –新生代</h2><h3 id="配置分析"><a href="#配置分析" class="headerlink" title="配置分析"></a>配置分析</h3><blockquote>
<p>-Xmn120m – 发生改变的数据<br>-Xms750m<br>-Xmx750m<br>-XX:PermSize=350m<br>-XX:MaxPermSize=350m<br>-XX:ReservedCodeCacheSize=96m<br>-XX:+PrintGCTimeStamps<br>-XX:+PrintGCDetails<br>-Xloggc:/tmp/idea.log<br>-ea<br>-Dsun.io.useCanonCaches=false<br>-Djava.net.preferIPv4Stack=true<br>-XX:+UseCodeCacheFlushing<br>-XX:+UseConcMarkSweepGC<br>-XX:SoftRefLRUPolicyMSPerMB=50<br>-Dawt.useSystemAAFontSettings=lcd</p>
</blockquote>
<p>开idea,启用tomcat,发现还是没有fullgc,说明idea中大量的对象都是朝生夕死,活跃数据很稳定。</p>
<blockquote>
<p>2014-04-08T20:39:27.207+0800: 243.821: [GC 243.822: [ParNew: 110592K->10359K(110592K), 0.0855940 secs] 351176K->259675K(755712K), 0.0860110 secs] [Times: user=0.08 sys=0.01, real=0.09 secs]<br>2014-04-08T20:39:29.575+0800: 246.189: [GC 246.189: [ParNew: 108663K->4195K(110592K), 0.0203700 secs] 357979K->253512K(755712K), 0.0209180 secs] [Times: user=0.04 sys=0.00, real=0.02 secs]<br>2014-04-08T20:39:33.517+0800: 250.131: [GC 250.131: [ParNew: 102499K->8590K(110592K), 0.0742860 secs] 351816K->257907K(755712K), 0.0746160 secs] [Times: user=0.10 sys=0.01, real=0.07 secs]<br>2014-04-08T20:39:35.489+0800: 252.103: [GC 252.103: [ParNew: 106894K->12288K(110592K), 0.0735800 secs] 356211K->263572K(755712K), 0.0738440 secs] [Times: user=0.10 sys=0.01, real=0.07 secs]</p>
</blockquote>
<p>平均gc的时间是 0.05s,若是你要求的gc时间是0.04,那么减少新生代,若是要求的时间是0.08s,那么可以扩大一点新生代,在保持总大小不变的情况下,增加或是减少10%的大小。<br>平均的gc频率是2.5s左右,若是你期望的是5s,填充完2048M的新生代需要2.5s,那么要5s的话2048*(5/2.5) = 4096m,这个时候需要扩大新生代的内存大小,总的大小都需要增加2048m</p>
<h3 id="应用优化原则二-–-新生代"><a href="#应用优化原则二-–-新生代" class="headerlink" title="应用优化原则二 – 新生代"></a>应用优化原则二 – 新生代</h3><ol>
<li>老年代空间不应该小于活跃数据的1.5倍</li>
<li>新生代空间至少为java堆大小的10%.通过-Xmx和-Xms可以设置</li>
<li>增大java堆大小时候,不能超过物理堆大小<br>tips:吞吐量和延迟考虑的话 -Xms、-Xmx设置为相同。只有在两者相同的情况下,设置-Xmn才有意义。不然可以通过-XX:MaxNewSize、-XX:NewSize两个参数来设置新生代的大小。</li>
</ol>
<h2 id="四-调优延迟-响应性-–老生代"><a href="#四-调优延迟-响应性-–老生代" class="headerlink" title="四 调优延迟/响应性 –老生代"></a>四 调优延迟/响应性 –老生代</h2><p>若是老生代大小是4096m,活跃数据是1370m,那么空闲数据是2726,minor gc每次提升的数据是21m,minor gc的频率是2.147s,那么每秒提升的数据是21494/2.147 = 10011kb/s,那么填充满2726,需要2726/10 = 272.6s 大约4.5分钟。<br>若是你的full gc时间间隔大于这个数值,那么老生代的大小是可以的,若是小于这个数据,就需要增加老生代的大小了。</p>
<h2 id="五-调优延迟-响应性-–-cms调优"><a href="#五-调优延迟-响应性-–-cms调优" class="headerlink" title="五 调优延迟/响应性 – cms调优"></a>五 调优延迟/响应性 – cms调优</h2><ol>
<li>full gc时候一定会触发一次minor gc,除非你设置了 -XX:ScavengeBeforeFull选项 。</li>
<li>因为是并行的收集,延迟性会有提升</li>
<li>压缩式的gc,就是内存碎片整理会消耗大量的时间</li>
</ol>
<h3 id="关键点"><a href="#关键点" class="headerlink" title="关键点"></a>关键点</h3><ol>
<li>新生代提升到老生代的速率</li>
<li>并行回收老生代空间的速率</li>
<li>老生代的碎片化空间</li>
</ol>
<h3 id="解决第三个问题的手段"><a href="#解决第三个问题的手段" class="headerlink" title="解决第三个问题的手段"></a>解决第三个问题的手段</h3><ol>
<li>足够大的内存,减少stop-the-world的压缩</li>
<li>降低从新生代提升到老生代的速率</li>
</ol>
<p>-XX:SurvivorRatio=8 参数调整<br>在保持新生代大小不变的情况下,增加survivor的大小 -> eden 变小 —> minor gc 变的频繁,那么在满足minor gc的要求下,那么必须保持eden不变,增大整个新生代。<br>增大survivor会加长对象在新生代的时间,但是会加快minor gc的频率<br>晋升阀值的计算依据于每次minor gc后存活的对象同目标survivor空间占用的空间大小。<br>具体通过</p>
<blockquote>
<p>-XX:+PrintTenuringDistribution 打印对象的年龄<br>2014-04-18T18:23:38.843+0800: 67.444: [GC 67.444: [ParNew<br>Desired survivor size 6291456 bytes, new threshold 1 (max 4)<br>- age 1: 6792744 bytes, 6792744 total<br>- age 2: 2366824 bytes, 9159568 total<br>- age 3: 1924344 bytes, 11083912 total</p>
</blockquote>
<p>内部计算的晋升阀值是:1.<br>最大晋升阀值是4,第一次 阀值是通过-XX:MaxTenuringThreshold=n来设置的。在JDK1.6.6版本后,范围是在1~15之间。JDK5中范围是1~31.<br>survivor的空间大小乘以目标存活率得到的大小:6291456<br>目标存活率是:预计空间目标在survivor空间中占有的百分比。JVM在GC之后忍让维持的survivor空间占用,通过-XX:TargetsurivorRatio=50来设置,默认是50,这个数值是经过大量的测试得出的,一般不需要修改。<br>当survivor空间的使用小于或是等于JVM期望维护的数值时,会将最大晋升阀值作为计算的晋升阀值。若是JVM觉得无法维持survivor空间的占有时候,会使用一个低于最大晋升阀值的数值来保证survivor空间的占用。<br>当存活的对象大小大于survivor空间的时候,会加速提升对象到老生代的速度。<br>看两个例子:</p>
<blockquote>
<p>2014-04-18T18:23:38.843+0800: 67.444: [GC 67.444: [ParNew<br>Desired survivor size 6291456 bytes, new threshold 1 (max 4)<br>- age 1: 6792744 bytes, 6792744 total<br>- age 2: 2366824 bytes, 9159568 total<br>- age 3: 1924344 bytes, 11083912 total`</p>
</blockquote>
<p>因为留存的对象大小是11083912,远大于计算预估的的大小6291456,所以需要降低晋升阀值,加快进入老生代的速率,故是1 ,最大是4.若是一直保持这种状态下,那么需要增加survivor空间。</p>
<blockquote>
<p>2014-04-18T18:43:50.378+0800: 1278.979: [GC 1278.980: [ParNew<br>Desired survivor size 6291456 bytes, new threshold 4 (max 4)<br>- age 1: 72032 bytes, 72032 total<br>- age 2: 1408 bytes, 73440 total<br>- age 3: 1352 bytes, 74792 total</p>
</blockquote>
<p>留存的对象大小是74792,小于预计的大小6291456,若是对于晋升的阀值是最大,其实可以加大阀值的数值,将4增大,最大可到15.<br>若是minor gc的持续时间过长,那么就是新生代过大,需要调整下大小。<br>老生代的调优<br>老生代在回收的时候可能会发生并发回收失败,那么因为在回收的时候,有新的数据产生,老生代空间不够来进行回收了。那么可以通过两个参数来进行设置<br>-XX:+UseCMSInitiatingOccupancyOnly 使用手工设置的大小<br>-XX:CMSInitiatingOccupancyFaction 在老生代内存达到多少百分比时候进行回收。这个参数依赖于前面那个参数,若是不设置UseCMSInitiatingOccupancyOnly,只在第一次按照CMSInitiatingOccupancyFaction设置的值来进行回收,第二次还是自我调节的去回收<br>CMSInitiatingOccupancyFaction这个数值的大小必须大于活跃数据的大小,若是活跃数据是350,老生代是1024,那么他们之间的比例是350,那么CMSInitiatingOccupancyFaction必须大于350/1024.<br>CMSInitiatingOccupancyFaction这个值看是否设置合适就在于</p>
<blockquote>
<p>cms-initial-mark stop-world<br>cms-concurrent-mark<br>cms-concurrent-preclean<br>cms-concurrent-abortable-preclean<br>cms-remark stop-world<br>cms-concurrent-sweep<br>cms-concurrent-reset</p>
</blockquote>
<p>这几个阶段之间数值是否变化的不大,若是变化不大的话,那么说明CMSInitiatingOccupancyFaction这个值设置的太小了。<br>若是cms-initial-mark后直接出现full gc,那么说明CMSInitiatingOccupancyFaction设置的太大了。</p>
<p>持久代的回收</p>
<blockquote>
<p>-XX:+CMSPermGenSweepingEnabled<br>-XX:CMSInitiatingPermOccupancyFraction<br>-XX:+CMSClassUnloadingEnabled<br>-XX:+CMSParallelRemarkEnabled</p>
</blockquote>
<p>几个参数来设置</p>
]]></content>
<summary type="html">
<![CDATA[<p>本文通过GC日志分析总结JVM优化的一般规则<br>]]>
</summary>
<category term="java" scheme="http://yoursite.com/tags/java/"/>
<category term="jvm调优" scheme="http://yoursite.com/tags/jvm%E8%B0%83%E4%BC%98/"/>
</entry>
<entry>
<title><![CDATA[性能定位整体思路]]></title>
<link href="http://yoursite.com/2016/05/31/%E6%80%A7%E8%83%BD%E5%AE%9A%E4%BD%8D%E6%95%B4%E4%BD%93%E6%80%9D%E8%B7%AF/"/>
<id>http://yoursite.com/2016/05/31/性能定位整体思路/</id>
<published>2016-05-30T16:13:08.000Z</published>
<updated>2016-05-31T05:45:53.000Z</updated>
<content type="html"><![CDATA[<p>一个请求从客户端发起到最后在数据库中进行查询,中间经历了很多个环节。本文就是针对出现的问题,快速定位到是哪个步骤环节出现了问题,由于笔者也在不断的学习中,希望有问题大家能提出来共同进行学习。<br><a id="more"></a></p>
<h2 id="一、服务器定位"><a href="#一、服务器定位" class="headerlink" title="一、服务器定位"></a>一、服务器定位</h2><p>对于一般的应用,最常用的架构是:<br>客户端——nginx——tomcat等应用服务器——数据库/Memcache/redis。<br><img src="/2016/05/31/性能定位整体思路/1.png" alt="整体架构图" title="整体架构图"><br>下图是一个定位问题的结构图(盗图)。一般先从客户端入手,查看客户端的TPS和响应时间。如果TPS过低或者响应时间过长,可以查看一下客户端是否有错误日志产生,从错误日志中定位问题,若没有错误日志可以从堆栈信息中定位问题,详见下文的1.1.3节;如果TPS和响应时间都正常就可以查看一下客户端的资源利用率的使用情况,详见下文的1.2节。<br><img src="/2016/05/31/性能定位整体思路/2.png" alt="整体流程图" title="整体流程图"></p>
<h3 id="1-1-接口测试"><a href="#1-1-接口测试" class="headerlink" title="1.1 接口测试"></a>1.1 接口测试</h3><h4 id="1-1-1-接口的TPS和响应时间"><a href="#1-1-1-接口的TPS和响应时间" class="headerlink" title="1.1.1 接口的TPS和响应时间"></a>1.1.1 接口的TPS和响应时间</h4><p>在一般的接口测试,可以使用loadrunner或者在可以在测试机上安装grinder客户端,然后运行脚本,在log中可以看到本次执行的接口响应时间和TPS,如下图所示;<br><img src="/2016/05/31/性能定位整体思路/3.png" alt="TPS图" title="TPS图"></p>
<h4 id="1-1-2-查看错误日志"><a href="#1-1-2-查看错误日志" class="headerlink" title="1.1.2 查看错误日志"></a>1.1.2 查看错误日志</h4><p>查看是否有error日志,从error中获取信息,例如下图从测试客户端的error日志中查看到的错误信息:<br><img src="/2016/05/31/性能定位整体思路/4.png" alt="errorlog图" title="errorlog图"><br>错误日志中会有错误说明和错误的提示信息,可以从中获取到定位问题的方法,例如图示中的错误提示为:“A JSONObject text must begin with ‘{‘ at character 1”表示JSON对象的格式不对,可以参考代码做相应的修改。</p>
<h4 id="1-1-3-查看堆栈信息"><a href="#1-1-3-查看堆栈信息" class="headerlink" title="1.1.3 查看堆栈信息"></a>1.1.3 查看堆栈信息</h4><p>在运行的机器中,先用top命令获取进程的PID,然后用命令jstack + PID即可获取该进程号对应的java堆栈信息;获取到的堆栈信息需要先关注其线程状态state:</p>
<ul>
<li>死锁,Deadlock(重点关注)</li>
<li>执行中,Runnable</li>
<li>等待资源,Waiting on condition(重点关注)</li>
<li>等待获取监视器,Waiting on monitor entry(重点关注)</li>
<li>暂停,Suspended</li>
<li>对象等待中,Object.wait() 或 TIMED_WAITING</li>
<li>阻塞,Blocked(重点关注)</li>
<li>停止,Parked</li>
</ul>
<p>实例一:Waiting to lock 和 Blocked<br><img src="/2016/05/31/性能定位整体思路/5.png" alt="lock图" title="lock图"></p>
<ol>
<li>线程状态是 Blocked,阻塞状态。说明线程等待资源超时!</li>
<li>“ waiting to lock <0x00000000acf4d0c0>”指,线程在等待给这个 0x00000000acf4d0c0 地址上锁(英文可描述为:trying to obtain 0x00000000acf4d0c0 lock)。</0x00000000acf4d0c0></li>
<li>在 dump 日志里查找字符串 0x00000000acf4d0c0,发现有大量线程都在等待给这个地址上锁。如果能在日志里找到谁获得了这个锁(如locked < 0x00000000acf4d0c0 >),就可以顺藤摸瓜了。</li>
<li>“waiting for monitor entry”说明此线程通过 synchronized(obj) {……} 申请进入了临界区,从而进入了“Entry Set”队列,但该 obj 对应的 monitor 被其他线程拥有,所以本线程在 Entry Set 队列中等待。</li>
<li>第一行里,”RMI TCP Connection(267865)-172.16.5.25”是 Thread Name 。tid指Java Thread id。nid指native线程的id。prio是线程优先级。[0x00007fd4f8684000]是线程栈起始地址。</li>
</ol>
<p>实例二:Waiting on condition 和 TIMED_WAITING<br><img src="/2016/05/31/性能定位整体思路/6.png" alt="waiting图" title="waiting图"></p>
<ol>
<li>“TIMED_WAITING (parking)”中的 timed_waiting 指等待状态,但这里指定了时间,到达指定的时间后自动退出等待状态;parking指线程处于挂起中。</li>
<li>“waiting on condition”需要与堆栈中的“parking to wait for <0x00000000acd84de8> (a java.util.concurrent.SynchronousQueue$TransferStack)”结合来看。首先,本线程肯定是在等待某个条件的发生,来把自己唤醒。其次,SynchronousQueue 并不是一个队列,只是线程之间移交信息的机制,当我们把一个元素放入到 SynchronousQueue 中时必须有另一个线程正在等待接受移交的任务,因此这就是本线程在等待的条件。</0x00000000acd84de8></li>
</ol>
<p>实例三:in Obejct.wait() 和 TIMED_WAITING<br><img src="/2016/05/31/性能定位整体思路/7.png" alt="objectwaiting图" title="objectwaiting图"></p>
<ol>
<li>“TIMED_WAITING (on object monitor)”,对于本例而言,是因为本线程调用了 java.lang.Object.wait(long timeout) 而进入等待状态。</li>
<li>“Wait Set”中等待的线程状态就是“ in Object.wait() ”。当线程获得了 Monitor,进入了临界区之后,如果发现线程继续运行的条件没有满足,它则调用对象(一般就是被 synchronized 的对象)的 wait() 方法,放弃了 Monitor,进入 “Wait Set”队列。只有当别的线程在该对象上调用了 notify() 或者 notifyAll() ,“ Wait Set”队列中线程才得到机会去竞争,但是只有一个线程获得对象的 Monitor,恢复到运行态。</li>
<li>RMI RenewClean 是 DGCClient 的一部分。DGC 指的是 Distributed GC,即分布式垃圾回收。</li>
</ol>
<h3 id="1-2-机器资源指标"><a href="#1-2-机器资源指标" class="headerlink" title="1.2 机器资源指标"></a>1.2 机器资源指标</h3><p>机器的资源指标可以从机器的CPU、网络、内存、磁盘IO等方面全面系统的监控机器的各个方面。一个有经验的测试人员可以从这些监控命令中获取机器运行的健康情况,从而定位哪里出现了性能瓶颈。Linux下有很多监控命令可以监控到各方面的性能,具体可以参考下图的linux性能监测工具。本文中介绍几种常用的监控工具。<br><img src="/2016/05/31/性能定位整体思路/8.png" alt="linux图" title="linux图"></p>
<h4 id="2-1-1-CPU"><a href="#2-1-1-CPU" class="headerlink" title="2.1.1 CPU"></a>2.1.1 CPU</h4><p>CPU的指标主要用下面两个命令top,mpstat。<br>top:<br><img src="/2016/05/31/性能定位整体思路/9.png" alt="top图" title="top图"></p>
<ol>
<li>红色方框中load表示系统负载(任务队列的平均长度),这三个值分别为1分钟,5分钟,15分钟前到现在的系统负载平均值,一般会小于1,如果持续高于5,要检查是哪个程序影响了系统的运行;一般其数值小于CPU核数,最好不要超过核数的两倍。【 load average数据是每隔5秒钟检查一次活跃的进程数,然后按特定算法计算出的数值,如果这个数除以逻辑CPU的数量的结果高于5,就说明系统在超负荷运转了,可以升级CPU个数】</li>
<li>Tasks——任务(进程),系统现在共有112个进程,其中处于运行中的有1个,111个在休眠状态,stoped状态的有0个,zombie状态(僵尸)的有0个;</li>
<li>%Cpu(s)这一行分别代表:<br>us——用户空间占用CPU百分比(user);<br>sy——内核空间占用CPU百分比(system);<br>ni——用户空间内改变过优先级的进程占用CPU百分比(user nice);<br>id——空闲CPU百分比(idle);<br>wa——等待输入输出CPU时间百分比(io wait);<br>hi——CPU服务于硬件中断所耗费的时间总额(hardware irq );<br>si——CPU服务软中断所耗费的时间总额(software irq);st——StealTime<br>【若用户使用CPU过多,则需要优化用户程序;若系统内核态使用CPU过多,可能是过多的中断以及上下文切换造成的】</li>
<li>KiB Mem: total——“物理内存总量”、used——“已使用的物理内存”、free——“空闲物理内存”、buffers——“内核缓存内存量”<br>【已使用的物理内存(used)指的是现在系统内核控制的内存数,空闲内存总量(free)是内核还未纳入其管控范围的数量。纳入内核管理的内存不见得都在使用中,还包括过去使用过的现在可以被重复利用的内存,内核并不把这些可被重新使用的内存交还到free中去,因此在linux上free内存会越来越少,但不用为此担心。】</li>
<li>KiB Swap: total——“交换区总量”、used——“已使用交互区总量”、free——空闲交换区总量”、cached——“缓冲的交换区总量”<br>【Swap中的used的数值如果在不断变化,说明内核在不断进行内存和swap的数据交换,内存有可能不够用】</li>
<li>PID:进程ID </li>
<li>USER: 进程所有者 </li>
<li>PR: 优先级 </li>
<li>NI: nice值(负值表示高优先级,正值表示低优先级) </li>
<li>VIRT: 进程使用的虚拟内存总量 </li>
<li>RES:进程使用的,未被换出的物理内存大小 </li>
<li>SHR:共享内存大小,单位kb </li>
<li>S: 进程状态—— D=不可中断的睡眠状态 R=运行 S=睡眠 T=跟踪/停止 Z=僵尸进程 </li>
<li>%CPU: 上次更新到现在的CPU时间占用百分比 </li>
<li>%MEM:进程使用的物理内存百分比 </li>
<li>TIME+:进程使用CPU的总时间 </li>
<li>COMMAND: 命令行 </li>
<li>其他技巧:<br>在top的基本视图中,按键盘数字“1”,可监控每个逻辑CPU的状况;<br>top -p + PID 显示指定的PID进程信息;</li>
</ol>
<p>mpstat:<br>用 mpstat -P -ALL 1 (MultiProcessor Statistics)查看多核CPU每个计算核心的使用情况,主要看idle这个值,这个值越大说明CPU的空闲越大,使用的CPU越小<br><img src="/2016/05/31/性能定位整体思路/10.png" alt="mpstat图" title="mpstat图"></p>
<h4 id="2-1-2-CPU"><a href="#2-1-2-CPU" class="headerlink" title="2.1.2 CPU"></a>2.1.2 CPU</h4><p>sar 全称为System Activity Report,是Unix/Linux 下流行的系统资源监控命令,Linux 下如果没有该命令,需要安装 sysstat 包。sar 在网卡上的监控如下图,按照网卡的设置(有些网卡支持每秒采集一次数据、有些可能需要2秒)实时观察网络流量变化,而且通过 -n ETCP 参数可以把重试、错误、重传、RST包都统计出来。如果网络发生抖动,那么从 retrains/txpck 可以观察到重传率,如下图的红色部分。<br><img src="/2016/05/31/性能定位整体思路/11.png" alt="sar图" title="sar图"><br>用 sar -n DEV 1 【sar:System Activity Recorder】主要负责收集、汇报与存储系统运行信息的,sar命令使用-n DEV 选项可以汇报网络设备相关信息,该条命令主要用来查看网卡信息:<br><img src="/2016/05/31/性能定位整体思路/12.png" alt="sardev图" title="sardev图"></p>
<ol>
<li>FACE:就是网络设备的名称;</li>
<li>rxpck/s:每秒钟接收到的包数目</li>
<li>txpck/s:每秒钟发送出去的包数目</li>
<li>rxbyt/s:每秒钟接收到的字节数</li>
<li>txbyt/s:每秒钟发送出去的字节数</li>
<li>rxcmp/s:每秒钟接收到的压缩包数目</li>
<li>txcmp/s:每秒钟发送出去的压缩包数目</li>
<li>rxmcst/s:每秒钟接收到的多播包的包数目</li>
</ol>
<p>netstat 命令用于显示各种网络相关信息,如网络连接,路由表,接口状态 (Interface Statistics),masquerade 连接,多播成员 (Multicast Memberships) 等等。在性能测试中,这个命令主要用来查看现有的端口号等等,从整体上看,netstat的输出结果可以分为两个部分:</p>
<ul>
<li>一个是Active Internet connections,称为有源TCP连接,其中”Recv-Q”和”Send-Q”指%0A的是接收队列和发送队列。这些数字一般都应该是0。如果不是则表示软件包正在队列中堆积。这种情况只能在非常少的情况见到。</li>
<li>另一个是Active UNIX domain sockets,称为有源Unix域套接口(和网络套接字一样,但是只能用于本机通信,性能可以提高一倍)。Proto显示连接使用的协议,RefCnt表示连接到本套接口上的进程号,Types显示套接口的类型,State显示套接口当前的状态,Path表示连接到套接口的其它进程使用的路径名。</li>
</ul>
<h4 id="2-1-3-内存"><a href="#2-1-3-内存" class="headerlink" title="2.1.3 内存"></a>2.1.3 内存</h4><p>物理内存和虚拟内存区别:</p>
<ul>
<li>物理内存就是系统硬件提供的内存大小,是真正的内存,相对于物理内存,在linux下还有一个虚拟内存的概念,虚拟内存就是为了满足物理内存的不足而提出的策略,它是利用磁盘空间虚拟出的一块逻辑内存,用作虚拟内存的磁盘空间被称为交换空间(Swap Space)。</li>
<li>作为物理内存的扩展,linux会在物理内存不足时,使用交换分区的虚拟内存,更详细的说,就是内核会将暂时不用的内存块信息写到交换空间,这样以来,物理内存得到了释放,这块内存就可以用于其它目的,当需要用到原始的内容时,这些信息会被重新从交换空间读入物理内存。</li>
</ul>
<p>用 vmstat 1 【Virtual Meomory Statistics(虚拟内存统计)】命令查看内存使用情况:<br><img src="/2016/05/31/性能定位整体思路/13.png" alt="vmstat图" title="vmstat图"><br>其中</p>
<ul>
<li>Procs(进程):<br>r: 运行队列中进程数量(runnable);<br>b: 等待IO的进程数量(blocked)</li>
<li>Memory(内存):<br>swpd: 使用虚拟内存大小;<br>free: 可用内存大小;<br>buff: 用作缓冲的内存大小;<br>cache: 用作缓存的内存大小</li>
<li>Swap:<br>si: 每秒从交换区写到内存的大小;<br>so: 每秒写入交换区的内存大小;<br>【内存够用的时候,这两个值都为0,若这两个值长期大于0时,系统性能会受到影响】<br>IO:(现在的Linux版本块的大小为1024bytes)<br>bi: 每秒读取的块数;<br>bo: 每秒写入的块数;<br>【bi+bo参考值为1000,若超过1000,且wa较大,表示系统IO有问题,应该提高磁盘的读写性能】</li>
<li>system:<br>in: 每秒中断数,包括时钟中断。<br>cs: 每秒上下文切换数。(content switch)<br>【in与cs越大,内核消耗的CPU时间就越多】<br>CPU(以百分比表示):<br>us: 用户进程执行时间(user time);<br>sy: 系统进程执行时间(system time);<br>id: 空闲时间(包括IO等待时间),中央处理器的空闲时间,以百分比表示;<br>wa: 等待IO时间<br>【us+sy参考值为80%,如果大于80%,说明可能存在CPU资源不足的情况】<br>这个命令中主要看参数: r,b,si,so</li>
</ul>
<h4 id="2-1-4-磁盘IO"><a href="#2-1-4-磁盘IO" class="headerlink" title="2.1.4 磁盘IO"></a>2.1.4 磁盘IO</h4><p>用命令 iostat -x 1查看磁盘IO,注意查看%util这个参数<br><img src="/2016/05/31/性能定位整体思路/14.png" alt="iostat图" title="iostat图"></p>
<ul>
<li>rrqm/s:每秒进行merge的读操作数目。即delta(rmerge)/s</li>
<li>wrqm/s:每秒进行merge的写操作数目。即delta(wmerge)/s</li>
<li>r/s:每秒完成的读I/O设备次数。即delta(rio)/s</li>
<li>w/s:每秒完成的写I/0设备次数。即delta(wio)/s</li>
<li>rsec/s:每秒读扇区数。即delta(rsect)/s</li>
<li>wsec/s:每秒写扇区数。即delta(wsect)/s</li>
<li>rKB/s:每秒读K字节数。是rsec/s的一半,因为每扇区大小为512字节</li>
<li>wKB/s:每秒写K字节数。是wsec/s的一半</li>
<li>avgrq-sz:平均每次设备I/O操作的数据大小(扇区)。即delta(rsect+wsect)/delta(rio+wio)</li>
<li>avgqu-sz:平均I/O队列长度。即delta(aveq)/s/1000(因为aveq的单位为毫秒)</li>
<li>await:平均每次设备I/O操作的等待时间(毫秒)。即delta(ruse+wuse)/delta(rio+wio)</li>
<li>svctm:平均每次设备I/O操作的服务时间(毫秒)。即delta(use)/delta(rio+wio)</li>
<li>%util:一秒中有百分之多少的时间用于I/O操作,可以衡量磁盘设备的繁忙程度,或者说一秒中有多少时间I/O队列是非空的。即delta(usr)/s/1000(因为use的单位为毫秒)</li>
</ul>
<p>如果%util接近100%,说明产生的I/O请求太多,I/O系统已经满负载,该磁盘可能存在瓶颈,同时可以结合vmstat查看查看b参数(等待资源的进程数)和wa参数(I/O等待所占用的CPU时间的百分比,高过30%时I/O压力高)。正常情况下svctm应该小于await,而svctm的大小和磁盘性能有关,CPU、内存的负荷也会对svctm值造成影响,过多的请求也会简介导致svctm值的增加。</p>
<p>await的大小一般取决与svctm的值和I/O队列长度以及I/O请求模式。如果svctm与await很接近,表示几乎没有I/O等待,磁盘性能很好;如果await的值远高于svctm的值,表示I/O队列等待太长,系统上运行的应用程序将变慢,此时可以通过更换更快的硬盘来解决问题。</p>
<h3 id="1-3-Nginx端"><a href="#1-3-Nginx端" class="headerlink" title="1.3 Nginx端"></a>1.3 Nginx端</h3><h4 id="1-3-1-查看访问日志access-log、error-log"><a href="#1-3-1-查看访问日志access-log、error-log" class="headerlink" title="1.3.1 查看访问日志access_.log、error.log"></a>1.3.1 查看访问日志access_.log、error.log</h4><p>access.log日志可以在nginx的./conf文件中进行配置,下面是一条详细的access.log日志,通过访问日志和error日志里的有用信息可以粗略定位到哪里出现问题。</p>
<p>10.165.124.21:37444 - - [29/Jan/2016:09:14:01 +0800] “POST /sdk/mobService/device/bd.do HTTP/1.1” 200 51 “-“ “RPT-HTTPClient/0.3-3E” “-“ 1.735 “mobile.service.reg.163.com” “10.165.136.75:8184” “200” “1.735”</p>
<p>每行对应的含义可以参考如下:<br><img src="/2016/05/31/性能定位整体思路/15.png" alt="nginxlog图" title="nginxlog图"><br>要注意区分其中的$request_time和$upstream_response_time:</p>
<ul>
<li>request_time:指的就是从接受用户请求的第一个字节 到发送完响应数据的时间,即包括接收请求数据时间、 程序响应时间、输出;</li>
<li>upstream_response_time:指从Nginx向后端建立连接 开始到接受完数据然后关闭连接为止的时间</li>
<li>总体来说,request_time是包括upstream_response_time时间的,但是如果两者差异太大,可以用netperf工具来查看</li>
</ul>
<h3 id="1-4-Tomcat"><a href="#1-4-Tomcat" class="headerlink" title="1.4. Tomcat"></a>1.4. Tomcat</h3><h4 id="1-4-1-Tomcat日志"><a href="#1-4-1-Tomcat日志" class="headerlink" title="1.4.1 Tomcat日志"></a>1.4.1 Tomcat日志</h4><p>Tomcat默认的引擎为Catalina,host为localhost,所以从tomcat中日志可以查看catalina日志,来确定tomcat的启动是否有报错,localhost_access_log访问日志查看每个请求的状态;<br>用netstat命令来查看网络相关的信息。如果出现404这样的错误,可以查看nginx和tomcat之间的TIME_WAIT数量是否很多,如果TIME_WAIT数量较多,需要进一步来定位问题。</p>
<h4 id="1-4-2-JVM监控"><a href="#1-4-2-JVM监控" class="headerlink" title="1.4.2 JVM监控"></a>1.4.2 JVM监控</h4><p>JVM的监控在性能测试中非常重要,采用工具对JVM监控并分析监控结果可以得到很多定位问题的有用信息,下图是JVM定位问题时经常需要关注的指标和各个指标之间的关系:<br><img src="/2016/05/31/性能定位整体思路/16.png" alt="jmx图" title="jmx图"><br>针对Tomcat这样的后端服务器的JVM监控,可以采用如下设置。在目录/home/appuser/urs/urs-mobile-bj-perf/tomcat-urs-mobile-perf-Ins1/default中配置jvm的参数(根据自己应用的具体目录而定),一般加上如下两行:<br> -Dcom.sun.management.jmxremote.port=8013 \<br> -Djava.rmi.server.hostname=10.165.124.14 \</p>
<p>Jvm中监控的参数主要包括如下几个方面:</p>
<ol>
<li>CPU及堆内存监控<br>主要是查看CPU使用率和堆内存的使用情况,CPU使用情况可以用来判断当前测试的接口大体的CPU资源使用情况,堆内存可以看到当前JVM分配的最大可用内存和已用内存趋势。<br>如果CPU使用率过高,TPS比较低,或者响应时间比较长,可以通过查看线程状态,生成线程dump进行分析,也有可能是达到应用服务器的瓶颈。堆内存可以判断垃圾回收是否正常,如果发现堆内存使用急剧上升时,也可以用堆dump把当前堆内存的使用情况保存下来进行查看。如果CPU波动比较明显,GC的开销占比比较高,同时监控到堆使用状态异常,转向内存的监控和定位。</li>
<li>线程监控<br>线程选项卡主要是查看当前有没有被阻塞的线程,看一下block和wait的线程情况,并用线程堆栈信息辅助查看线程block的具体位置。<br>线程dump可以查看线程的状态,看是在等待、运行、阻塞的状态,并通过下面的堆栈信息来进一步定位造成线程等待或者阻塞的原因。</li>
<li>热点方法监控<br>对CPU进行抽样,查看自用时间以及自用时间CPU,对于占用CPU较多的,可以进一步使用javosize进行定位。切换到线程CPU时间下面监控线程CPU时间。</li>
<li>装入的类<br>监控装入的类和卸载的类的变化情况,一般情况下,应用运行一段时间后,装入的类的总数基本保持不变,如果总数一直在增加,需要关注下perm区。</li>
<li>jvm常用调优命令<br>jps ,主要用来输出JVM中运行的进程状态信息;<br>jstack,java堆栈跟踪工具;<br>jstat,虚拟机统计信息监视工具;<br>jinfo,java配置信息工具;<br>jmap,java内存映像工具;<br>jhat,虚拟机堆转储快照分析工具;<br>VisualVM,可视化的监控工具</li>
</ol>
<h2 id="二、常见服务器错误"><a href="#二、常见服务器错误" class="headerlink" title="二、常见服务器错误"></a>二、常见服务器错误</h2><h3 id="2-1-404-错误"><a href="#2-1-404-错误" class="headerlink" title="2.1 404 错误"></a>2.1 404 错误</h3><p>一般是因为nginx和后端的连接出现了问题,没有正常连接起来。通常检查思路可以分为如下:</p>
<ol>
<li>nginx配置的upstream的端口号是否有误:查看omad上的端口号,以及在服务器上用netstat 命令查看端口号和对应的PID是否一致;<br>命令: netstata –anp|grep 端口号<br> lsof –I : 8181 查看8181端口是否被占用</li>
<li>查看后端应用是否启动起来;<br>命令: ps –ef |grep tomcat; ps –ef |grep resin;等等</li>
<li>查看nginx端和tomcat端的日志,确定数据库,nkv等是否正常连接;</li>
<li>tomcat启动成功,但是context启动失败,解决办法就是增加在目录/WEB-INF/classes中增加启动日志查看context的启动;</li>
<li>如果是本地访问,确认localhost是否正确配置,一般来说配置nginx的私有ip地址加域名;</li>
<li>tomcat和压测机器的/etc/hosts是否有回调到nginx上去,如果是同一个租户配置私有ip地址,若跨租户要配置为机房网络ip;</li>
<li>配置好了nginx的.conf文件后,访问相应的域名出来的是“404,not found nginx”,去查看nginx的日志,看是否是缺失某些文件</li>
</ol>
<h3 id="2-2-Nginx-500-错误"><a href="#2-2-Nginx-500-错误" class="headerlink" title="2.2 Nginx 500 错误"></a>2.2 Nginx 500 错误</h3><p>如果遇到nginx 出现500错误,根本原因是服务器出现了未捕捉的异常,有两种情况:</p>
<ol>
<li>客户端只返回Internal Service Error,一般是 nginx 错误,查看nginx 的error日志</li>
<li>客户端有完整堆栈信息,一般是upstream 出现未捕捉的异常,此时 nginx error无日志、upstream的应用容器一般有错误日志,可以查看:<br>应用出现运行时错误,比如常见的空指针错误、数组越界等<br>依赖服务异常,应用没有处理,引发运行时错误(比如调用其他服务API,当其他服务部可用可能返回null,但是应用未考虑这种情况,依然进行方法调用引发空指针错误)</li>
</ol>
<h3 id="2-3-Nginx-502-错误"><a href="#2-3-Nginx-502-错误" class="headerlink" title="2.3 Nginx 502 错误"></a>2.3 Nginx 502 错误</h3><p>Nginx 502 错误即Bad Gateway,其原因也分成nginx本身和后端应用,主要有以下几种情况:</p>
<ol>
<li>Nginx本身, 没有给upstream设置 DNS,一般错误日志会有:” no resolver defined to resolve”</li>
<li>Nginx 本身,无法解析upstream 域名,一般错误日志会有:“could not be resolved”</li>
<li>应用挂掉,导致nginx 连接出现连接拒绝;</li>
<li>服务刚启动,启动期间,端口还未监听,或者端口已监听但是应用未启动完成,导致连接拒绝;</li>
<li>突然大批量请求达到,Socket backlog(全连接队列)满,可能会导致内核返回RST,引发连接拒绝;</li>
<li>使用连接线程池,当更新线程/线程重建时,有大批量连接到达导致线程不够用,引发连接拒绝(底层原因同5);</li>