Netty SSL性能调优
嗯,这篇不长的文章,是一个晚上工作到三点的血泪加班史总结而成。多一个读,可能就少一个人加班。
《 TLS协议分析 与 现代加密通信协议设计》 首先要感谢这篇文章,如果没有它,我可能还要花更多的时间才能完成。文章有点长,能看多少是多少,每句都是收获。
1.背景知识
1.1 协议史
1996: SSL3.0. 写成RFC,开始流行。目前(2015年)已经不安全,必须禁用。
1999: TLS1.0. 互联网标准化组织ISOC接替NetScape公司,发布了SSL的升级版TLS 1.0版。
2006: TLS1.1. 作为 RFC 4346 发布。主要fix了CBC模式相关的如BEAST攻击等漏洞。
2008: TLS1.2. 作为RFC 5246发布 。增进安全性,目前的主流版本。
2015之后: TLS 1.3,还在制订中。
1.2 TLS算法组合
在TLS中,5类算法组合在一起,称为一个CipherSuite:
- 认证算法
- 加密算法
- 消息认证码算法 简称MAC
- 密钥交换算法
- 密钥衍生算法
比较常见的算法组合是 TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA 和 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 都是ECDHE 做密钥交换,使用RSA做认证,SHA256做PRF算法。
一个使用AES128-CBC做加密算法,用HMAC做MAC。
一个使用AES128-GCM做加密算法,MAC由于GCM作为一种AEAD模式并不需要。
两者的差别,在于 Block Cipher+HMAC 类的算法都爆出了各种漏洞,下一代的TLS v1.3干脆只保留了Authenticated-Encryption 类的算法,主要就是AES-GCM,AEAD模式(Authenticated-Encryption With Addtional data)里Encrypt和MAC直接集成为一个算法,在算法内部解决好安全问题。
1.3 Java 对SSL的支持
JDK7的client端只支持TLS1.0,服务端则支持TLS1.2。
JDK8完全支持TLS1.2。
JDK7不支持GCM算法。
JDK8支持GCM算法,但性能极差极差极差,按Netty的说法:
- Java 8u60以前多版本,只能处理1 MB/s。
- Java 8u60 开始,10倍的性能提升,10-20 MB/s。
- 但比起 OpenSSL的 ~200 MB/s,还差着一个量级。
1.4 Netty 对SSL的支持
Netty既支持JDK SSL,也支持Google的boringssl, 这是OpenSSL 的一个fork,更少的代码,更多的功能。
依赖netty-tcnative-boringssl-static-linux-x86_64.jar即可,它里面已包含了相关的so文件,再也不用管Linux里装没装OpenSSL,OpenSSL啥版本了。
2. 性能问题的出现及调优
2.1 性能问题的出现
忘掉前面所有的背景知识,重新来到问题现场:
JDK7的JMeter HTTPS客户端,连接JDK8的Netty服务端时,速度还可以。
JDK8的JMeter HTTPS客户端,则非常慢,非常慢,非常吃客户端的CPU。
按套路,在JMeter端增加启动参数 -Djavax.net.debug=ssl,handshake debug 握手过程。
(OpenSSL那边这个参数加了没用)
*** ClientHello, TLSv1.2,可以看到,Client端先发起协商,带了一堆可选协议
Cipher Suites: [TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, TLS_RSA_WITH_AES_128_CBC_SHA256…]
*** ServerHello, TLSv1.2 然后服务端回选定一个
Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
还可以看到,传输同样的数据,不同客户端/服务端组合下有不同的纪录:
Client: JDK7 JDK SSL + Server: JDK7/8 JDK SSL
**TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
WRITE: TLSv1 Application Data, length = 32
WRITE: TLSv1 Application Data, length = 304
READ: TLSv1 Application Data, length = 32
READ: TLSv1 Application Data, length = 96
READ: TLSv1 Application Data, length = 32
READ: TLSv1 Application Data, length = 10336
Client: JDK8 JDK SSL + Server: JDK8 Open SSL
** TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
Thread Group 1-1, WRITE: TLSv1.2 Application Data, length = 300
Thread Group 1-1, READ: TLSv1.2 Application Data, length = 92
Thread Group 1-1, READ: TLSv1.2 Application Data, length = 10337
2.2 原因分析
带着上面的记录,经过一晚的奋战,得出了文章一开始的背景信息,再回头分析就很好理解了,JMeter Https 用的是JDK8 SSL,很不幸的和服务端的OpenSSL协商出一个JDK8实现超慢的TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256。
对于服务端/客户端都是基于Netty + boringssl的RPC框架,使用TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 仍然是好的,毕竟更安全。
但Https接口,如果不确定对端的是什么,JDK7 SSL or JDK8 SSL or OpenSSL,为免协商出一个超慢的GCM算法,Server端需要通过配置,才决定要不要把GCM放进可选列表里。
2.3 实现
经过一轮学习,平时是这样写的:
SslContext sslContext = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) .sslProvider( SslProvider. OPENSSL).build();
如果想不要开GCM,那把ReferenceCountedOpenSslContext里面的DEFAULT_CIPHERS抄出来,删掉两个GCM的。
List<String> ciphers = Lists.newArrayList(“ECDHE-RSA-AES128-SHA”, “ECDHE-RSA-AES256-SHA”, “AES128-SHA”, “AES256-SHA”, “DES-CBC3-SHA”);
SslContext sslContext = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) .sslProvider( SslProvider. OPENSSL).ciphers(ciphers).build();
3 结论
- OpenSSL(boringssl)在我们的测试用例里,比JDK SSL 快10倍,10倍!!! 所以Netty下尽量都要使用OpenSSL。
- 在确定两端都使用OpenSSL时,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 仍然是好的,毕竟更安全,也是主流。
- 对端如果是JDK8 SSL时,Server端要把GCM算法从可选列表里拿掉。