推送服务
还记得一年半前,做的一个我的项目须要用到 Android 推送服务。和 iOS 不同,Android 生态中没有对立的推送服务。Google 尽管有 Google Cloud Messaging,然而连国外都没对立,更别说国内了,间接被墙。
所以之前在 Android 上做推送大部分只能靠轮询。而咱们之前在技术调研的时候,搜到了 jPush 的博客,下面介绍了一些他们的技术特点,他们次要做的其实就是挪动网络下的长连贯服务。单机 50W-100W 的连贯确实是吓我一跳!起初咱们也采纳了他们的收费计划,因为是一个受众面很小的产品,所以他们的免费版够咱们用了。一年多下来,运作稳固,十分不错!
时隔两年,换了部门后,居然接到了一项工作,优化公司本人的长连贯服务端。
再次搜寻网上技术材料后才发现,相干的很多难点都被攻破,网上也有了很多的总结文章,单机 50W-100W 的连贯齐全不是梦,其实人人都能够做到。然而光有连贯还不够,QPS 也要一起下来。
所以,这篇文章就是汇总一下利用 Netty 实现长连贯服务过程中的各种难点和可优化点。
Netty 是什么
Netty: http://netty.io/
Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.
官网的解释最精准了,其中最吸引人的就是高性能了。然而很多人会有这样的疑难:间接用 NIO 实现的话,肯定会更快吧?就像我间接手写 JDBC 尽管代码量大了点,然而肯定比 iBatis 快!
然而,如果理解 Netty 后你才会发现,这个还真不肯定!
利用 Netty 而不必 NIO 间接写的劣势有这些:
- 高性能高扩大的架构设计,大部分状况下你只须要关注业务而不须要关注架构
- Zero-Copy 技术尽量减少内存拷贝
- 为 Linux 实现 Native 版 Socket
- 写同一份代码,兼容 java 1.7 的 NIO2 和 1.7 之前版本的 NIO
- Pooled Buffers 大大加重 Buffer 和开释 Buffer 的压力
- ……
个性太多,大家能够去看一下《Netty in Action》这本书理解更多。
另外,Netty 源码是一本很好的教科书!大家在应用的过程中能够多看看它的源码,十分棒!
瓶颈是什么
想要做一个长链服务的话,最终的指标是什么?而它的瓶颈又是什么?
其实指标次要就两个:
- 更多的连贯
- 更高的 QPS
所以,上面就针对这两个指标来说说他们的难点和留神点吧。
更多的连贯
非阻塞 IO
其实无论是用 Java NIO 还是用 Netty,达到百万连贯都没有任何难度。因为它们都是非阻塞的 IO,不须要为每个连贯创立一个线程了。
欲知详情,能够搜寻一下 BIO,NIO,AIO 的相干知识点。
Java NIO 实现百万连贯
ServerSocketChannel ssc = ServerSocketChannel.open();
Selector sel = Selector.open();
ssc.configureBlocking(false);
ssc.socket().bind(new InetSocketAddress(8080));
SelectionKey key = ssc.register(sel, SelectionKey.OP_ACCEPT);
while(true) {sel.select();
Iterator it = sel.selectedKeys().iterator();
while(it.hasNext()) {SelectionKey skey = (SelectionKey)it.next();
it.remove();
if(skey.isAcceptable()) {ch = ssc.accept();
}
}
}
这段代码只会承受连过去的连贯,不做任何操作,仅仅用来测试待机连接数极限。
大家能够看到这段代码是 NIO 的根本写法,没什么特地的。
Netty 实现百万连贯
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workerGroup= new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup);
bootstrap.channel(NioServerSocketChannel.class);
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {@Override protected void initChannel(SocketChannel ch) throws Exception {ChannelPipeline pipeline = ch.pipeline();
//todo: add handler
}});
bootstrap.bind(8080).sync();
这段其实也是非常简单的 Netty 初始化代码。同样,为了实现百万连贯基本没有什么非凡的中央。
瓶颈到底在哪
下面两种不同的实现都非常简单,没有任何难度,那有人必定会问了:实现百万连贯的瓶颈到底是什么?
其实只有 java 中用的是非阻塞 IO(NIO 和 AIO 都算),那么它们都能够用单线程来实现大量的 Socket 连贯。不会像 BIO 那样为每个连贯创立一个线程,因为代码层面不会成为瓶颈。
其实真正的瓶颈是在 Linux 内核配置上,默认的配置会限度全局最大关上文件数 (Max Open Files) 还会限度过程数。所以须要对 Linux 内核配置进行肯定的批改才能够。
这个货色当初看似很简略,依照网上的配置改一下就行了,然而大家肯定不晓得第一个钻研这个人有多难。
如何验证
让服务器反对百万连贯一点也不难,咱们过后很快就搞定了一个测试服务端,然而最大的问题是,我怎么去验证这个服务器能够撑持百万连贯呢?
咱们用 Netty 写了一个测试客户端,它同样用了非阻塞 IO,所以不必开大量的线程。然而一台机器上的端口数是有限度的,用 root 权限的话,最多也就 6W 多个连贯了。所以咱们这里用 Netty 写一个客户端,用尽单机所有的连贯吧。
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
Bootstrap b = new Bootstrap();
b.group(workerGroup);
b.channel(NioSocketChannel.class);
b.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {ChannelPipeline pipeline = ch.pipeline();
//todo:add handler
}
});
for (int k = 0; k < 60000; k++) {
// 请自行批改成服务端的 IP
b.connect(127.0.0.1, 8080);
}
代码同样很简略,只有连上就行了,不须要做任何其余的操作。
这样只有找到一台电脑启动这个程序即可。这里须要留神一点,客户端最好和服务端一样,批改一下 Linux 内核参数配置。
怎么去找那么多机器
依照下面的做法,单机最多能够有 6W 的连贯,百万连贯起码须要 17 台机器!
如何能力冲破这个限度呢?其实这个限度来自于网卡。咱们起初通过应用虚拟机,并且把虚拟机的虚构网卡配置成了桥接模式解决了问题。
依据物理机内存大小,单个物理机起码能够跑 4 - 5 个虚拟机,所以最终百万连贯只有 4 台物理机就够了。
讨巧的做法
除了用虚拟机充沛压迫机器资源外,还有一个十分讨巧的做法,这个做法也是我在验证过程中偶尔发现的。
依据 TCP/IP 协定,任何一方发送 FIN 后就会启动失常的断开流程。而如果遇到网络瞬断的状况,连贯并不会主动断开。
那咱们是不是能够这样做?
- 启动服务端,千万别设置 Socket 的 keep-alive 属性,默认是不设置的
- 用虚拟机连贯服务器
- 强制敞开虚拟机
- 批改虚拟机网卡的 MAC 地址,重新启动并连贯服务器
- 服务端承受新的连贯,并放弃之前的连接不断
咱们要验证的是服务端的极限,所以只有始终让服务端认为有那么多连贯就行了,不是吗?
通过咱们的试验后,这种办法和用实在的机器连贯服务端的体现是一样的,因为服务端只是认为对方网络不好罢了,不会将你断开。
另外,禁用 keep-alive 是因为如果不禁用,Socket 连贯会主动探测连贯是否可用,如果不可用会强制断开。
更高的 QPS
因为 NIO 和 Netty 都是非阻塞 IO,所以无论有多少连贯,都只须要大量的线程即可。而且 QPS 不会因为连接数的增长而升高(在内存足够的前提下)。
而且 Netty 自身设计得足够好了,Netty 不是高 QPS 的瓶颈。那高 QPS 的瓶颈是什么?
是数据结构的设计!
如何优化数据结构
首先要相熟各种数据结构的特点是必须的,然而在简单的我的项目中,不是用了一个汇合就能够搞定的,有时候往往是各种汇合的组合应用。
既要做到高性能,还要做到一致性,还不能有死锁,这里难度真的不小…
我在这里总结的教训是,不要过早优化。优先思考一致性,保证数据的精确,而后再去想方法优化性能。
因为一致性比性能重要得多,而且很多性能问题在量小和量大的时候,瓶颈齐全会在不同的中央。所以,我感觉最佳的做法是,编写过程中以一致性为主,性能为辅;代码实现后再去找那个 TOP1,而后去解决它!
解决 CPU 瓶颈
在做这个优化前,先在测试环境中去狠狠地压你的服务器,量小量大,天壤之别。
有了压力测试后,就须要用工具来发现性能瓶颈了!
我喜爱用的是 VisualVM,关上工具后看抽样器 (Sample),依据自用工夫(Self Time (CPU)) 倒序,排名第一的就是你须要去优化的点了!
备注:Sample 和 Profiler 有什么区别?前者是抽样,数据不是最准然而不影响性能;后者是统计精确,然而十分影响性能。如果你的程序十分耗 CPU,那么尽量用 Sample,否则开启 Profiler 后升高性能,反而会影响准确性。
还记得咱们我的项目第一次发现的瓶颈居然是 ConcurrentLinkedQueue 这个类中的 size()办法。量小的时候没有影响,然而 Queue 很大的时候,它每次都是从头统计总数的,而这个 size()办法咱们又是十分频繁地调用的,所以对性能产生了影响。
size()的实现如下:
public int size() {
int count = 0;
for (Node<E> p = first(); p != null; p = succ(p))
if (p.item != null)
// Collection.size() spec says to max out
if (++count == Integer.MAX_VALUE)
break;
return count;
}
起初咱们通过额定应用一个 AtomicInteger 来计数,解决了问题。然而拆散后岂不是做不到高一致性呢?没关系,咱们的这部分代码关怀最终一致性,所以只有保障最终统一就能够了。
总之,具体案例要具体分析,不同的业务要用不同的实现。
解决 GC 瓶颈
GC 瓶颈也是 CPU 瓶颈的一部分,因为不合理的 GC 会大大影响 CPU 性能。
这里还是在用 VisualVM,然而你须要装一个插件:VisualGC
有了这个插件后,你就能够直观的看到 GC 流动状况了。
依照咱们的了解,在压测的时候,有大量的 New GC 是很失常的,因为有大量的对象在创立和销毁。
然而一开始有很多 Old GC 就有点说不过去了!
起初发现,在咱们压测环境中,因为 Netty 的 QPS 和连接数关联不大,所以咱们只连贯了大量的连贯。内存调配得也不是很多。
而 JVM 中,默认的新生代和老生代的比例是 1:2,所以大量的老生代被节约了,新生代不够用。
通过调整 -XX:NewRatio 后,Old GC 有了显著的升高。
然而,生产环境又不一样了,生产环境不会有那么大的 QPS,然而连贯会很多,连贯相干的对象存活工夫十分长,所以生产环境更应该调配更多的老生代。
总之,GC 优化和 CPU 优化一样,也须要一直调整,一直优化,不是欲速不达的。
其余优化
如果你曾经实现了本人的程序,那么肯定要看看《Netty in Action》作者的这个网站:Netty Best Practices a.k.a Faster == Better。
置信你会受益匪浅,通过外面提到的一些小小的优化后,咱们的整体 QPS 晋升了很多。
最初一点就是,java 1.7 比 java 1.6 性能高很多!因为 Netty 的编写格调是事件机制的,看似是 AIO。可 java 1.6 是没有 AIO 的,java 1.7 是反对 AIO 的,所以如果用 java 1.7 的话,性能也会有显著晋升。
最初成绩
通过几周的一直压测和一直优化了,咱们在一台 16 核、120G 内存 (JVM 只调配 8G) 的机器上,用 java 1.6 达到了 60 万的连贯和 20 万的 QPS。
其实这还不是极限,JVM 只调配了 8G 内存,内存配置再大一点连接数还能够下来;
QPS 看似很高,System Load Average 很低,也就是说明瓶颈不在 CPU 也不在内存,那么应该是在 IO 了!下面的 Linux 配置是为了达到百万连贯而配置的,并没有针对咱们本人的业务场景去做优化。
因为目前性能齐全够用,线上单机 QPS 最多才 1W,所以咱们先把精力放在了其余中央。置信前面咱们还会去持续优化这块的性能,期待 QPS 能有更大的冲破!
起源:dozer.cc/2014/12/netty-long-connection.html