作者:Grey

原文地址:单机百万连接调优和Netty应用级别调优

说明

本文为深度解析Netty源码的学习笔记。

单机百万连接调优

准备两台Linux服务器,一个充当服务端,一个充当客户端。

服务端

  • 操作系统:CentOS 7

  • 配置:4核8G

  • IP:192.168.118.138

客户端

  • 操作系统:CentOS 7

  • 配置:4核8G

  • IP:192.168.118.139

服务端和客户端均要配置java环境,基于jdk1.8。

如何模拟百万连接

如果服务端只开一个端口,客户端连接的时候,端口号是有数量限制的(非root用户,从1024到65535,大约6w),所以服务端开启一个端口,客户端和服务端的连接最多6w个左右。

为了模拟单机百万连接,我们在服务端开启多个端口,例如8000~8100,一共100个端口,客户端还是6w的连接,但是可以连接服务端的不同端口,所以就可以模拟服务端百万连接的情况。

准备服务端程序

服务端程序的主要逻辑是:

绑定8000端口一直到8099端口,一共100个端口,每2s钟统计一下连接数。

channelActive触发的时候,连接+1, channelInactive触发的时候,连接-1

代码见:Server.java

准备客户端程序

客户端程序的主要逻辑是:

循环连接服务端的端口(从8000一直到8099)。

代码见:Client.java

准备好客户端和服务端的代码后,打包成Client.jarServer.jar并上传到客户端和服务端的/data/app目录下。打包配置参考pom.xml

服务端和客户端在/data/app下分别准备两个启动脚本,其中服务端准备的脚本为startServer.sh, 客户端准备的脚本为startClient.sh,内容如下:

startServer.sh

java -jar server.jar -Xms6.5g -Xmx6.5g -XX:NewSize=5.5g -XX:MaxNewSize=5.5g -XX:MaxDirectMemorySize=1g

startClient.sh

java -jar client.jar -Xms6.5g -Xmx6.5g -XX:NewSize=5.5g -XX:MaxNewSize=5.5g -XX:MaxDirectMemorySize=1g

脚本文件见:startServer.shstartClient.sh

先启动服务端

cd /data/app/ 

./startServer.sh

查看日志,待服务端把100个端口都绑定好以后。

在启动客户端

cd /data/app/

./startClient.sh

然后查看服务端日志,服务端在支撑了3942个端口号以后,报了如下错误:

Caused by: java.io.IOException: Too many open files
at sun.nio.ch.FileDispatcherImpl.init(Native Method)
at sun.nio.ch.FileDispatcherImpl.<clinit>(FileDispatcherImpl.java:35)

突破局部文件句柄限制

使用ulimit -n命令可以查看一个jvm进程最多可以打开的文件个数,这个是局部文件句柄限制,默认是1024,我们可以修改这个值

vi /etc/security/limits.conf

增加如下两行

*               hard    nofile             1000000
* soft nofile 1000000

以上配置表示每个进程可以打开的最大文件数是一百万。

突破全局文件句柄限制

除了突破局部文件句柄数限制,还需要突破全局文件句柄数限制,修改如下配置文件

vi /proc/sys/fs/file-max

将这个数量修改为一百万

echo 1000000 > /proc/sys/fs/file-max

通过这种方式修改的配置在重启后失效,如果要使重启也生效,需要修改如下配置

vi /etc/sysctl.conf

在文件末尾加上

fs.file-max=1000000

服务端和客户端在调整完局部文件句柄限制和全局文件句柄限制后,再次启动服务端,待端口绑定完毕后,启动客户端。

查看服务端日志,可以看到,服务端单机连接数已经达到百万级别。

.....
connections: 434703
connections: 438238
connections: 441195
connections: 444082
connections: 447596
.....
connections: 920435
connections: 920437
connections: 920439
connections: 920442
connections: 920443
connections: 920445
.....

Netty应用级别调优

场景

服务端接受到客户端的数据,进行一些相对耗时的操作(比如数据库查询,数据处理),然后把结果返回给客户端。

模拟耗时操作

在服务端,模拟通过sleep方法来模拟耗时操作,规则如下:

  • 90.0%情况下,处理时间为1ms

  • 95.0%情况下,处理时间为10ms

  • 99.0%情况下,处理时间为100ms

  • 99.9%情况下,处理时间为1000ms

代码如下

protected Object getResult(ByteBuf data) {
int level = ThreadLocalRandom.current().nextInt(1, 1000);
int time;
if (level <= 900) {
time = 1;
} else if (level <= 950) {
time = 10;
} else if (level <= 990) {
time = 100;
} else {
time = 1000;
}
try {
Thread.sleep(time);
} catch (InterruptedException e) {
}
return data;
}

客户端统计QPS和AVG逻辑

获取当前时间戳,客户端在和服务端建立连接后,会每隔1s给服务端发送数据,发送的数据就是当前的时间戳,服务端获取到这个时间戳以后,会把这个时间戳再次返回给客户端,所以客户端会拿到发送时候的时间戳,然后客户端用当前时间减去收到的时间戳,就是这个数据包的处理时间,记录下这个时间,然后统计数据包发送的次数,根据这两个变量,可以求出QPS和AVG,其中:

QPS 等于 总的请求量 除以 持续到当前的时间

AVG 等于 总的响应时间除以请求总数

客户端源码参考:Client.java

服务端源码参考:Server.java

服务端在不做任何优化的情况下,关键代码如下

...
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new FixedLengthFrameDecoder(Long.BYTES));
ch.pipeline().addLast(/*businessGroup,*/ ServerBusinessHandler.INSTANCE);
// ch.pipeline().addLast(ServerBusinessThreadPoolHandler.INSTANCE);
}
});
...
@ChannelHandler.Sharable
public class ServerBusinessHandler extends SimpleChannelInboundHandler<ByteBuf> {
public static final ChannelHandler INSTANCE = new ServerBusinessHandler(); @Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) {
ByteBuf data = Unpooled.directBuffer();
data.writeBytes(msg);
Object result = getResult(data);
ctx.channel().writeAndFlush(result);
} protected Object getResult(ByteBuf data) {
int level = ThreadLocalRandom.current().nextInt(1, 1000);
int time;
if (level <= 900) {
time = 1;
} else if (level <= 950) {
time = 10;
} else if (level <= 990) {
time = 100;
} else {
time = 1000;
} try {
Thread.sleep(time);
} catch (InterruptedException e) {
} return data;
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// ignore
}
}

运行服务端和客户端,查看客户端日志

.....
qps: 1466, avg response time: 35.68182
qps: 832, avg response time: 214.28384
qps: 932, avg response time: 352.59363
qps: 965, avg response time: 384.59448
qps: 957, avg response time: 403.33804
qps: 958, avg response time: 424.5246
qps: 966, avg response time: 433.35272
qps: 980, avg response time: 484.2116
qps: 986, avg response time: 478.5395
.....

优化方案一:使用自定义线程池处理耗时逻辑

将服务端代码做如下调整

bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new FixedLengthFrameDecoder(Long.BYTES));
//ch.pipeline().addLast(/*businessGroup,*/ ServerBusinessHandler.INSTANCE);
ch.pipeline().addLast(ServerBusinessThreadPoolHandler.INSTANCE);
}
});

其中ServerBusinessThreadPoolHandler中,使用了自定义的线程池来处理耗时的getResult方法。关键代码如下:

private static ExecutorService threadPool = Executors.newFixedThreadPool(1000);
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) {
ByteBuf data = Unpooled.directBuffer();
data.writeBytes(msg);
threadPool.submit(() -> {
Object result = getResult(data);
ctx.channel().writeAndFlush(result);
}); }

再次运行服务端和客户端,可以查看客户端日志,QPS和AVG指标都有明显的改善

....
qps: 1033, avg response time: 17.690498
qps: 1018, avg response time: 17.133448
qps: 1013, avg response time: 15.563113
qps: 1010, avg response time: 15.415672
qps: 1009, avg response time: 16.049961
qps: 1008, avg response time: 16.179882
qps: 1007, avg response time: 16.120466
qps: 1006, avg response time: 15.822202
qps: 1006, avg response time: 15.987518
....

实际生产过程中,Executors.newFixedThreadPool(1000);中配置的数量需要通过压测来验证。

优化方案二:使用Netty原生的线程池优化

我们可以通过Netty提供的线程池来处理耗时的Handler,这样的话,无需调整Handler的逻辑(对原有Handler无代码侵入),关键代码:

bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new FixedLengthFrameDecoder(Long.BYTES));
// ch.pipeline().addLast(ServerBusinessHandler.INSTANCE);
// 使用业务线程池方式
// ch.pipeline().addLast(ServerBusinessThreadPoolHandler.INSTANCE);
// 使用Netty自带线程池方式
ch.pipeline().addLast(businessGroup,ServerBusinessHandler.INSTANCE);
}
});

其中businessGroup是Netty自带的线程池

EventLoopGroup businessGroup = new NioEventLoopGroup(1000);

ServerBusinessHandler中的所有方法,都会在businessGroup中执行。

再次启动服务端和客户端,查看客户端日志

.....
qps: 1027, avg response time: 23.833092
qps: 1017, avg response time: 20.98855
qps: 1014, avg response time: 18.220013
qps: 1012, avg response time: 17.447332
qps: 1010, avg response time: 16.502508
qps: 1010, avg response time: 15.692251
qps: 1009, avg response time: 15.968423
qps: 1008, avg response time: 15.888149
.....

更多优化建议

参考Netty性能调优奇技淫巧还有其他的吗?

1.如果QPS过高,数据传输过快的情况下,调用writeAndFlush可以考虑拆分成多次write,然后单次flush,也就是批量flush操作

2.分配和释放内存尽量在reactor线程内部做,这样内存就都可以在reactor线程内部管理

3.尽量使用堆外内存,尽量减少内存的copy操作,使用CompositeByteBuf可以将多个ByteBuf组合到一起读写

4.外部线程连续调用eventLoop的异步调用方法的时候,可以考虑把这些操作封装成一个task,提交到eventLoop,这样就不用多次跨线程

5.尽量调用ChannelHandlerContext.writeXXX()方法而不是channel.writeXXX()方法,前者可以减少pipeline的遍历

6.如果一个ChannelHandler无数据共享,那么可以搞成单例模式,标注@Shareable,节省对象开销对象

7.如果要做网络代理类似的功能,尽量复用eventLoop,可以避免跨reactor线程

源码

Github

参考资料

深度解析Netty源码

单机百万连接调优和Netty应用级别调优的更多相关文章

  1. 高性能C++网络库libtnet实践:comet单机百万连接挂载测试

    最近在用go语言做一个挂载大量长连接的推送服务器,虽然已经完成,但是内存占用情况让我不怎么满意,于是考虑使用libtnet来重新实现一个.后续我会使用comet来表明推送服务器. 对于comet来说, ...

  2. 单机c1000k连接

    单机c1000k连接即单机实现百万连接,首先要注意的是连接是虚拟的逻辑的,连接最终落于 网卡,清晰概念才能更深入更清晰的想出问题的解决办法. 参考 http://www.ideawu.net/blog ...

  3. [Spark性能调优] 第一章:性能调优的本质、Spark资源使用原理和调优要点分析

    本課主題 大数据性能调优的本质 Spark 性能调优要点分析 Spark 资源使用原理流程 Spark 资源调优最佳实战 Spark 更高性能的算子 引言 我们谈大数据性能调优,到底在谈什么,它的本质 ...

  4. spark 性能调优(一) 性能调优的本质、spark资源使用原理、调优要点分析

    转载:http://www.cnblogs.com/jcchoiling/p/6440709.html 一.大数据性能调优的本质 编程的时候发现一个惊人的规律,软件是不存在的!所有编程高手级别的人无论 ...

  5. 架构师养成记--22.客户端与服务器端保持连接的解决方案,netty的ReadTimeoutHandler

    概述 保持客户端与服务器端连接的方案常用的有3种 1.长连接,也就是客户端与服务器端一直保持连接,适用于客户端比较少的情况. 2.定时段连接,比如在某一天的凌晨建立连接,适用于对实时性要求不高的情况. ...

  6. 开源libco库:单机千万连接、支撑微信8亿用户的后台框架基石

    微信于2013年开源的ibco库,是微信后台大规模使用的c/c++协程库,2013年至今稳定运行在微信后台的数万台机器上.libco在2013年的时候作为腾讯六大开源项目首次开源,ibco支持后台敏捷 ...

  7. netty百万连接跟踪记录

    0. 启动客户端和服务端 # 测试环境: centos7 jdk8 2核16G# 服务端启动nohup java -Xmx8192m -Xms4096m -XX:+UseG1GC -XX:Parall ...

  8. Netty SSL性能调优

    TLS算法组合 在TLS中,5类算法组合在一起,称为一个CipherSuite: 认证算法 加密算法 消息认证码算法 简称MAC 密钥交换算法 密钥衍生算法 比较常见的算法组合是 TLS_ECDHE_ ...

  9. Nginx 单机百万QPS环境搭建

    一.背景 最近公司在做一些物联网产品,物物通信用的是MQTT协议,内部权限与内部关系等业务逻辑准备用HTTP实现.leader要求在本地测试中要模拟出百万用户同时在线的需求.虽然该产品最后不一定有这么 ...

随机推荐

  1. session.flush与transaction.commit

    以session的save方法为例来看一个简单.完整的事务流程,如下是代码片段: ---------------------------- Session session = sessionFacto ...

  2. [题解] SPOJ GSS1 - Can you answer these queries I

    [题解] SPOJ GSS1 - Can you answer these queries I · 题目大意 要求维护一段长度为 \(n\) 的静态序列的区间最大子段和. 有 \(m\) 次询问,每次 ...

  3. Promise.race()

    Promise.race([ ])---race竞赛,只要有一个决议了,就返回一个promise实例(对应resolve()或reject( )中参数值: 1.与Promise.all()对应的,还有 ...

  4. 那些优秀的python代码

    时间:2019-04-18 收藏:PangYuaner 标题:Python如何生成树形图案 地址:https://www.jb51.net/article/132049.htm 标题:用python- ...

  5. springboot系列总结(二)---springboot的常用注解

    上一篇文章我们简单讲了一下@SpringBootApplication这个注解,申明让spring boot自动给程序进行必要的配置,他是一个组合注解,包含了@ComponentScan.@Confi ...

  6. D3之svg transform 与 css3 transform 区别与联系

    D3就不用多介绍了,在数据可视化界属于大佬级别的js库.在这里主要想记录一下在写程序期间遇到的一个问题. 如下图所示,想完成主视图在小地图上的映射,小地图的白色矩形框用来代表当前主视图可见区域,主视图 ...

  7. C++模板简介

    模板是C++支持参数化多态的工具,使用模板可以使用户为类或者函数声明一种一般模式,使得类中的某些数据成员或者成员函数的参数.返回值取得任意类型. 模板是一种对类型进行参数化的工具: 通常有两种形式:函 ...

  8. java交互Scanner类

    用next方法接收 import java.util.Scanner; public class Demo01 { public static void main(String[] args) { / ...

  9. 羽夏笔记——PE结构(不包含.Net)

    写在前面   本笔记是由本人独自整理出来的,图片来源于网络.本人非计算机专业,可能对本教程涉及的事物没有了解的足够深入,如有错误,欢迎批评指正. 如有好的建议,欢迎反馈.码字不易,如果本篇文章有帮助你 ...

  10. Java HashMap工作原理:不仅仅是HashMap

    前言: 几乎所有java程序员都用过hashMap,但会用不一定会说. 近年来hashMap是非常常见的面试题,如何为自己的回答加分?需要从理解开始. "你用过hashMap吗?" ...