Netty 长链接服务及Netty之JVM调优

DECEMBER 29TH, 2014html

推送服务

还记得一年半前,作的一个项目须要用到 Android 推送服务。和 iOS 不一样,Android 生态中没有统一的推送服务。Google 虽然有 Google Cloud Messaging ,可是连国外都没统一,更别说国内了,直接被墙。java

因此以前在 Android 上作推送大部分只能靠轮询。而咱们以前在技术调研的时候,搜到了 jPush 的博客,上面介绍了一些他们的技术特色,他们主要作的其实就是移动网络下的长链接服务。单机 50W-100W 的链接的确是吓我一跳!后来咱们也采用了他们的免费方案,由于是一个受众面很小的产品,因此他们的免费版够咱们用了。一年多下来,运做稳定,很是不错!linux

时隔两年,换了部门后,居然接到了一项任务,优化公司本身的长链接服务端。bootstrap

再次搜索网上技术资料后才发现,相关的不少难点都被攻破,网上也有了不少的总结文章,单机 50W-100W 的链接彻底不是梦,其实人人均可以作到。可是光有链接还不够,QPS 也要一块儿上去。api

因此,这篇文章就是汇总一下利用 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 源码是一本很好的教科书!你们在使用的过程当中能够多看看它的源码,很是棒!

 

瓶颈是什么

想要作一个长链服务的话,最终的目标是什么?而它的瓶颈又是什么?

其实目标主要就两个:

  1. 更多的链接
  2. 更高的 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 内核配置进行必定的修改才能够。

这个东西如今看似很简单,按照网上的配置改一下就好了,可是你们必定不知道第一个研究这我的有多难。

这里直接贴几篇文章,介绍了相关配置的修改方式:

构建C1000K的服务器

100万并发链接服务器笔记之1M并发链接目标达成

淘宝技术分享 HTTP长链接200万尝试及调优

 

如何验证

让服务器支持百万链接一点也不难,咱们当时很快就搞定了一个测试服务端,可是最大的问题是,我怎么去验证这个服务器能够支撑百万链接呢?

咱们用 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后就会启动正常的断开流程。而若是遇到网络瞬断的状况,链接并不会自动断开。

那咱们是否是能够这样作?

  1. 启动服务端,千万别设置 Socket 的keep-alive属性,默认是不设置的
  2. 用虚拟机链接服务器
  3. 强制关闭虚拟机
  4. 修改虚拟机网卡的 MAC 地址,从新启动并链接服务器
  5. 服务端接受新的链接,并保持以前的链接不断

咱们要验证的是服务端的极限,因此只要一直让服务端认为有那么多链接就好了,不是吗?

通过咱们的试验后,这种方法和用真实的机器链接服务端的表现是同样的,由于服务端只是认为对方网络很差罢了,不会将你断开。

另外,禁用keep-alive是由于若是不由用,Socket 链接会自动探测链接是否可用,若是不可用会强制断开。

 

更高的 QPS

因为 NIO 和 Netty 都是非阻塞 IO,因此不管有多少链接,都只须要少许的线程便可。并且 QPS 不会由于链接数的增加而下降(在内存足够的前提下)。

并且 Netty 自己设计得足够好了,Netty 不是高 QPS 的瓶颈。那高 QPS 的瓶颈是什么?

是数据结构的设计!

 

如何优化数据结构

首先要熟悉各类数据结构的特色是必需的,可是在复杂的项目中,不是用了一个集合就能够搞定的,有时候每每是各类集合的组合使用。

既要作到高性能,还要作到一致性,还不能有死锁,这里难度真的不小…

我在这里总结的经验是,不要过早优化。优先考虑一致性,保证数据的准确,而后再去想办法优化性能。

由于一致性比性能重要得多,并且不少性能问题在量小和量大的时候,瓶颈彻底会在不一样的地方。 因此,我以为最佳的作法是,编写过程当中以一致性为主,性能为辅;代码完成后再去找那个 TOP1,而后去解决它!

 

解决 CPU 瓶颈

在作这个优化前,先在测试环境中去狠狠地压你的服务器,量小量大,天壤之别。

有了压力测试后,就须要用工具来发现性能瓶颈了!

我喜欢用的是 VisualVM,打开工具后看抽样器(Sample),根据自用时间(Self Time (CPU))倒序,排名第一的就是你须要去优化的点了!

备注:Sample 和 Profiler 有什么区别?前者是抽样,数据不是最准可是不影响性能;后者是统计准确,可是很是影响性能。 若是你的程序很是耗 CPU,那么尽可能用 Sample,不然开启 Profiler 后下降性能,反而会影响准确性。

sample

还记得咱们项目第一次发现的瓶颈居然是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

有了这个插件后,你就能够直观的看到 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 创做,采用 知识共享署名-非商业性使用 4.0 国际许可协议 进行许可。

相关文章
相关标签/搜索