Netty 100万级高并发服务器配置

释放双眼,带上耳机,听听看~!

疯狂创客圈,一个Java 高并发研习社群 【博客园 总入口 】

疯狂创客圈,倾力推出:《Netty Zookeeper Redis 高并发实战》 面试必备 + 面试必备 + 面试必备 的 基础原理+实战 书籍


前言

每一种该语言在某些极限情况下的表现一般都不太一样,那么我常用的Java语言,在达到100万个并发连接情况下,会怎么样呢,有些好奇,更有些期盼。
这次使用经常使用的顺手的netty NIO框架(netty-3.6.5.Final),封装的很好,接口很全面,就像它现在的域名 netty.io,专注于网络IO。
整个过程没有什么技术含量,浅显分析过就更显得有些枯燥无聊,准备好,硬着头皮吧。

测试服务器配置

运行在VMWare Workstation 9中,64位Centos 6.2系统,分配14.9G内存左右,4核。
已安装有Java7版本:


1
2
3
4
5
1java version "1.7.0_21"
2Java(TM) SE Runtime Environment (build 1.7.0_21-b11)
3Java HotSpot(TM) 64-Bit Server VM (build 23.21-b01, mixed mode)
4
5

在/etc/sysctl.conf中添加如下配置:


1
2
3
4
5
6
7
8
9
10
1fs.file-max = 1048576
2net.ipv4.ip_local_port_range = 1024 65535
3net.ipv4.tcp_mem = 786432 2097152 3145728
4net.ipv4.tcp_rmem = 4096 4096 16777216
5net.ipv4.tcp_wmem = 4096 4096 16777216
6
7net.ipv4.tcp_tw_reuse = 1
8net.ipv4.tcp_tw_recycle = 1
9
10

在/etc/security/limits.conf中添加如下配置:


1
2
3
4
1     * soft nofile 1048576
2     * hard nofile 1048576
3
4

测试端

测试端无论是配置还是程序和以前一样,翻看前几篇博客就可以看到client5.c的源码,以及相关的配置信息等。

服务器程序

这次也是很简单呐,没有业务功能,客户端HTTP请求,服务端输出chunked编码内容。

入口HttpChunkedServer.java:


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
1package com.test.server;
2
3import static org.jboss.netty.channel.Channels.pipeline;
4import java.net.InetSocketAddress;
5import java.util.concurrent.Executors;
6import org.jboss.netty.bootstrap.ServerBootstrap;
7import org.jboss.netty.channel.ChannelPipeline;
8import org.jboss.netty.channel.ChannelPipelineFactory;
9import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
10import org.jboss.netty.handler.codec.http.HttpChunkAggregator;
11import org.jboss.netty.handler.codec.http.HttpRequestDecoder;
12import org.jboss.netty.handler.codec.http.HttpResponseEncoder;
13import org.jboss.netty.handler.stream.ChunkedWriteHandler;
14
15
16public class HttpChunkedServer {
17
18    private final int port;
19
20    public HttpChunkedServer(intport) {
21        this.port = port;
22    }
23
24    public void run() {
25        // Configure the server.
26        ServerBootstrap bootstrap = new ServerBootstrap(
27                new NioServerSocketChannelFactory(
28                        Executors.newCachedThreadPool(),
29                        Executors.newCachedThreadPool()));
30
31        // Set up the event pipeline factory.
32        bootstrap.setPipelineFactory(newChannelPipelineFactory() {
33            public ChannelPipeline getPipeline ()throws Exception {
34                ChannelPipeline pipeline = pipeline();
35                pipeline.addLast("decoder", new HttpRequestDecoder());
36                pipeline.addLast("aggregator", new HttpChunkAggregator(65536));
37                pipeline.addLast("encoder", new HttpResponseEncoder());
38                pipeline.addLast("chunkedWriter", new ChunkedWriteHandler());
39                pipeline.addLast("handler", new HttpChunkedServerHandler());
40                return pipeline;
41            }
42        });
43
44        bootstrap.setOption("child.reuseAddress", true);
45        bootstrap.setOption("child.tcpNoDelay", true);
46        bootstrap.setOption("child.keepAlive", true);
47        // Bind and start to accept incoming connections.
48        bootstrap.bind(newInetSocketAddress(port));
49    }
50
51    public static void main(String[] args) {
52        int port;
53        if (args.length > 0) {
54            port = Integer.parseInt(args[0]);
55        } else {
56            port = 8080;
57        }
58
59        System.out.format("server start with port %d \n", port);
60        new HttpChunkedServer(port).run();
61    }
62}
63
64

唯一的自定义处理器HttpChunkedServerHandler.java:


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
1package com.test.server;
2
3import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE;
4import static org.jboss.netty.handler.codec.http.HttpMethod.GET;
5import static org.jboss.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
6import static org.jboss.netty.handler.codec.http.HttpResponseStatus.METHOD_NOT_ALLOWED;
7import static org.jboss.netty.handler.codec.http.HttpResponseStatus.OK;
8import static org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1;
9import java.util.concurrent.atomic.AtomicInteger;
10import org.jboss.netty.buffer.ChannelBuffer;
11import org.jboss.netty.buffer.ChannelBuffers;
12import org.jboss.netty.channel.Channel;
13import org.jboss.netty.channel.ChannelFutureListener;
14import org.jboss.netty.channel.ChannelHandlerContext;
15import org.jboss.netty.channel.ChannelStateEvent;
16import org.jboss.netty.channel.ExceptionEvent;
17import org.jboss.netty.channel.MessageEvent;
18import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
19import org.jboss.netty.handler.codec.frame.TooLongFrameException;
20import org.jboss.netty.handler.codec.http.DefaultHttpChunk;
21import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
22import org.jboss.netty.handler.codec.http.HttpChunk;
23import org.jboss.netty.handler.codec.http.HttpHeaders;
24import org.jboss.netty.handler.codec.http.HttpRequest;
25import org.jboss.netty.handler.codec.http.HttpResponse;
26import org.jboss.netty.handler.codec.http.HttpResponseStatus;
27import org.jboss.netty.util.CharsetUtil;
28
29public class HttpChunkedServerHandlerextends SimpleChannelUpstreamHandler {
30    private static final AtomicInteger count = new AtomicInteger(0);
31
32    private void increment() {
33        System.out.format("online user %d\n", count.incrementAndGet());
34    }
35    
36    private void decrement() {
37        if (count.get() <= 0) {
38            System.out.format("~online user %d\n", 0);
39        } else {
40            System.out.format("~online user %d\n", count.decrementAndGet());
41        }
42    }
43    
44    @Override
45    public void messageReceived(ChannelHandlerContextctx, MessageEvent e)
46            throws Exception {
47        HttpRequest request = (HttpRequest) e.getMessage();
48        if (request.getMethod() != GET) {
49            sendError(ctx, METHOD_NOT_ALLOWED);
50            return;
51        }
52        
53        sendPrepare(ctx);
54        increment();
55    }
56    
57    @Override
58    public void channelDisconnected(ChannelHandlerContextctx,
59                                    ChannelStateEvent e) throws Exception {
60        decrement();
61        super.channelDisconnected(ctx, e);
62    }
63    
64    @Override
65    public void exceptionCaught(ChannelHandlerContextctx, ExceptionEvent e)
66            throws Exception {
67        Throwable cause = e.getCause();
68        if (cause instanceof TooLongFrameException) {
69            sendError(ctx, BAD_REQUEST);
70            return;
71        }
72    }
73    
74    private static void sendError(ChannelHandlerContext ctx,
75                                  HttpResponseStatus status) {
76        HttpResponse response = new DefaultHttpResponse(HTTP_1_1, status);
77        response.setHeader(CONTENT_TYPE, "text/plain; charset=UTF-8");
78        response.setContent(ChannelBuffers.copiedBuffer(
79                "Failure:" + status.toString() + "\r\n", CharsetUtil.UTF_8));
80        
81        // Close the connection as soon as the error message is sent.
82        ctx.getChannel().write(response)
83                .addListener(ChannelFutureListener.CLOSE);
84    }
85    
86    private void sendPrepare(ChannelHandlerContextctx) {
87        HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);
88        response.setChunked(true);
89        response.setHeader(HttpHeaders.Names.CONTENT_TYPE,
90                "text/html; charset=UTF-8");
91        response.addHeader(HttpHeaders.Names.CONNECTION,
92                HttpHeaders.Values.KEEP_ALIVE);
93        response.setHeader(HttpHeaders.Names.TRANSFER_ENCODING,
94                HttpHeaders.Values.CHUNKED);
95        
96        Channel chan = ctx.getChannel();
97        chan.write(response);
98        
99        // 缓冲必须凑够256字节,浏览器端才能够正常接收 ...
100        StringBuilder builder = new StringBuilder();
101        builder.append("");
102        int leftChars = 256 - builder.length();
103        for (int i = 0; i < leftChars; i++) {
104            builder.append("");
105        }
106        
107        writeStringChunk(chan, builder.toString());
108    }
109    
110    private void writeStringChunk(Channelchannel, String data) {
111        ChannelBuffer chunkContent = ChannelBuffers.dynamicBuffer(channel
112                .getConfig().getBufferFactory());
113        chunkContent.writeBytes(data.getBytes());
114        HttpChunk chunk = new DefaultHttpChunk(chunkContent);
115        channel.write(chunk);
116    }
117}
118
119

启动脚本start.sh


1
2
3
4
1set CLASSPATH=.
2nohup java -server -Xmx6G -Xms6G -Xmn600M -XX:PermSize=50M -XX:MaxPermSize=50M -Xss256K -XX:+DisableExplicitGC -XX:SurvivorRatio=1 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSClassUnloadingEnabled -XX:LargePageSizeInBytes=128M -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=80 -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+PrintClassHistogram -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -Xloggc:gc.log -Djava.ext.dirs=lib com.test.server.HttpChunkedServer 8000>server.out 2>&1 &
3
4

达到100万并发连接时的一些信息

每次服务器端达到一百万个并发持久连接之后,然后关掉测试端程序,断开所有的连接,等到服务器端日志输出在线用户为0时,再次重复以上步骤。在这反反复复的情况下,观察内存等信息的一些情况。以某次断开所有测试端为例后,当前系统占用为(设置为list_free_1):


1
2
3
4
5
6
1                  total       used       free     shared    buffers     cached
2     Mem:         15189       7736       7453          0         18        120
3     -/+ buffers/cache:       7597       7592
4     Swap:         4095        948       3147
5
6

通过top观察,其进程相关信息


1
2
3
4
1    PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                                                                                      
2   4925 root      20   0 8206m 4.3g 2776 S  0.3 28.8  50:18.66 java
3
4

在启动脚本start.sh中,我们设置堆内存为6G。

ps aux|grep java命令获得信息:


1
2
3
1  root      4925 38.0 28.8 8403444 4484764 ?     Sl   15:26  50:18 java -server...HttpChunkedServer 8000
2
3

RSS占用内存为4484764K/1024K=4379M

然后再次启动测试端,在服务器接收到online user 1023749时,ps aux|grep java内容为:


1
2
3
1  root      4925 43.6 28.4 8403444 4422824 ?     Sl   15:26  62:53 java -server...
2
3

查看当前网络信息统计


1
2
3
4
5
6
7
8
9
10
11
12
13
1  ss -s
2  Total: 1024050 (kernel 1024084)
3  TCP:   1023769 (estab 1023754, closed 2, orphaned 0, synrecv 0, timewait 0/0), ports 12
4
5  Transport Total     IP        IPv6
6  *    1024084   -         -        
7  RAW     0         0         0        
8  UDP     7         6         1        
9  TCP     1023767   12        1023755  
10  INET    1023774   18        1023756  
11  FRAG    0         0         0    
12
13

通过top查看一下


1
2
3
4
5
6
7
8
9
10
11
12
13
14
1  top -p 4925
2  top - 17:51:30 up  3:02,  4 users,  load average: 1.03, 1.80, 1.19
3  Tasks:   1 total,   0 running,   1 sleeping,   0 stopped,   0 zombie
4  Cpu0  :  0.9%us,  2.6%sy,  0.0%ni, 52.9%id,  1.0%wa, 13.6%hi, 29.0%si,  0.0%st
5  Cpu1  :  1.4%us,  4.5%sy,  0.0%ni, 80.1%id,  1.9%wa,  0.0%hi, 12.0%si,  0.0%st
6  Cpu2  :  1.5%us,  4.4%sy,  0.0%ni, 80.5%id,  4.3%wa,  0.0%hi,  9.3%si,  0.0%st
7  Cpu3  :  1.9%us,  4.4%sy,  0.0%ni, 84.4%id,  3.2%wa,  0.0%hi,  6.2%si,  0.0%st
8  Mem:  15554336k total, 15268728k used,   285608k free,     3904k buffers
9  Swap:  4194296k total,  1082592k used,  3111704k free,    37968k cached
10
11    PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                                                                                      
12   4925 root      20   0 8206m 4.2g 2220 S  3.3 28.4  62:53.66 java
13
14

四核都被占用了,每一个核心不太平均。这是在虚拟机中得到结果,可能真实服务器会更好一些。 因为不是CPU密集型应用,CPU不是问题,无须多加关注。

系统内存状况


1
2
3
4
5
6
7
1  free -m
2               total       used       free     shared    buffers     cached
3  Mem:         15189      14926        263          0          5         56
4  -/+ buffers/cache:      14864        324
5  Swap:         4095       1057       3038
6
7

物理内存已经无法满足要求了,占用了1057M虚拟内存。

查看一下堆内存情况


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
1  jmap -heap 4925
2  Attaching to process ID 4925, please wait...
3  Debugger attached successfully.
4  Server compiler detected.
5  JVM version is 23.21-b01
6
7  using parallel threads in the new generation.
8  using thread-local object allocation.
9  Concurrent Mark-Sweep GC
10
11  Heap Configuration:
12     MinHeapFreeRatio = 40
13     MaxHeapFreeRatio = 70
14     MaxHeapSize      = 6442450944 (6144.0MB)
15     NewSize          = 629145600 (600.0MB)
16     MaxNewSize       = 629145600 (600.0MB)
17     OldSize          = 5439488 (5.1875MB)
18     NewRatio         = 2
19     SurvivorRatio    = 1
20     PermSize         = 52428800 (50.0MB)
21     MaxPermSize      = 52428800 (50.0MB)
22     G1HeapRegionSize = 0 (0.0MB)
23
24  Heap Usage:
25  New Generation (Eden + 1 Survivor Space):
26     capacity = 419430400 (400.0MB)
27     used     = 308798864 (294.49354553222656MB)
28     free     = 110631536 (105.50645446777344MB)
29     73.62338638305664% used
30  Eden Space:
31     capacity = 209715200 (200.0MB)
32     used     = 103375232 (98.5863037109375MB)
33     free     = 106339968 (101.4136962890625MB)
34     49.29315185546875% used
35  From Space:
36     capacity = 209715200 (200.0MB)
37     used     = 205423632 (195.90724182128906MB)
38     free     = 4291568 (4.0927581787109375MB)
39     97.95362091064453% used
40  To Space:
41     capacity = 209715200 (200.0MB)
42     used     = 0 (0.0MB)
43     free     = 209715200 (200.0MB)
44     0.0% used
45  concurrent mark-sweep generation:
46     capacity = 5813305344 (5544.0MB)
47     used     = 4213515472 (4018.321487426758MB)
48     free     = 1599789872 (1525.6785125732422MB)
49     72.48054631000646% used
50  Perm Generation:
51     capacity = 52428800 (50.0MB)
52     used     = 5505696 (5.250640869140625MB)
53     free     = 46923104 (44.749359130859375MB)
54     10.50128173828125% used
55
56  1439 interned Strings occupying 110936 bytes.
57
58

老生代占用内存为72%,较为合理,毕竟系统已经处理100万个连接。

再次断开所有测试端,看看系统内存(free -m)


1
2
3
4
5
6
1               total       used       free     shared    buffers     cached
2  Mem:         15189       7723       7466          0         13        120
3  -/+ buffers/cache:       7589       7599
4  Swap:         4095        950       3145
5
6

记为list_free_2。

list_free_1和list_free_2两次都释放后的内存比较结果,系统可用物理已经内存已经降到7589M,先前可是7597M物理内存。
总之,我们的JAVA测试程序在内存占用方面已经,最低需要7589 + 950 = 8.6G内存为最低需求内存吧。

GC日志

我们在启动脚本处设置的一大串参数,到底是否达到目标,还得从gc日志处获得具体效果,推荐使用GCViewer。

GC事件概览:

其它:

总之:

  • 只进行了一次Full GC,代价太高,停顿了12秒。
  • PartNew成为了停顿大户,导致整个系统停顿了41秒之久,不可接受。
  • 当前JVM调优喜忧参半,还得继续努力等

小结

Java与与Erlang、C相比,比较麻烦的事情,需要在程序一开始就得准备好它的堆栈到底需要多大空间,换个说法就是JVM启动参数设置堆内存大小,设置合适的垃圾回收机制,若以后程序需要更多内存,需停止程序,编辑启动参数,然后再次启动。总之一句话,就是麻烦。单单JVM的调优,就得持续不断的根据检测、信息、日志等进行适当微调。

  • JVM需要提前指定堆大小,相比Erlang/C,这可能是个麻烦
  • GC(垃圾回收),相对比麻烦,需要持续不断的根据日志、JVM堆栈信息、运行时情况进行JVM参数微调
  • 设置一个最大连接目标,多次测试达到顶峰,然后释放所有连接,反复观察内存占用,获得一个较为合适的系统运行内存值
  • Eclipse Memory Analyzer结合jmap导出堆栈DUMP文件,分析内存泄漏,还是很方便的
  • 想修改运行时内容,或者称之为热加载,默认不可能
  • 真实机器上会有更好的反映

吐槽一下:
JAVA OSGI,相对比Erlang来说,需要人转换思路,不是那么原生的东西,总是有些别扭,社区或商业公司对此的修修补补,不过是实现一些面向对象所不具备的热加载的企业特性。

测试源代码,下载just_test。


无编程不创客,无案例不学习。疯狂创客圈,一大波高手正在交流、学习中!

疯狂创客圈 Netty 死磕系列 10多篇深度文章: 【博客园 总入口】 QQ群:104131248

给TA打赏
共{{data.count}}人
人已打赏
安全经验

如何避免Adsense违规封号

2021-10-11 16:36:11

安全经验

安全咨询服务

2022-1-12 14:11:49

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索