作者: lancelot

HTTPS主要的计算环节

大象为什么跳不高跑不快?因为它很重。HTTPS为什么访问比较慢为什么消耗CPU资源呢?同样也是因为它很重。HTTPS的重,体现在如下几方面:

1、大量的计算。SSL的每一个字节都涉及到较为复杂的计算。即使是clientHello,也需要在握手完成时做校验。

2、TLS协议的封装和解析。HTTPS所有数据都是按照TLS record格式进行封装和解析的。

3、协议的网络交互。从TLS的握手过程可以看出,即使不需要进行任何计算,TLS的握手也需要至少1个RTT(round trip time)以上的网络交互。

其中第2和第3点不是本文重点。本文侧重为大家分析HTTPS计算方面的原理,计算性能的测试和优化。

总体来说,HTTPS主要有如下计算环节:

1、非对称密钥交换。比如RSA, Diffie-Hellman, ECDHE.这类算法的主要作用就是根据客户端和服务端不对称的信息,经过高强度的密钥生成算法,生成对称密钥,用于加解密后续应用消息。

2、对称加解密。服务端使用密钥A对响应内容进行加密,客户端使用相同的密钥A对加密内容进行解密,反之亦然。

3、消息一致性验证。每一段加密的内容都会附加一个MAC消息,即消息认证码。简单地说就是对内容进行的安全哈希计算,接收方需要校验MAC码。

4、证书签名校验。这个阶段主要发生在客户端校验服务端证书身份时,需要对证书签名进行校验,确保证书的真实性。

上述环节的简单图示如下:

那上述阶段为什么需要消耗CPU呢?简单介绍一下计算原理及算法分析。

HTTPS计算原理及算法分析

上一节提到四个主要的计算环节,事实上每个环节都需要用到指定的算法。下面结合腾讯现在主要使用到的证书签名和密码套件分别介绍一下。

什么是密码套件(cipher suite)?它其实是一套算法的统称,包括密钥交换算法、消息认证码算法、内容加密算法和伪随机数算法。统计线上的cipher suite可以得出一个大概的使用情况,如下表所示:

证书签名算法

证书签名算法主要用于身份校验,现在的签名算法主要是SHA(安全哈希)系列,目前SHA1算法已经不安全,不推荐使用,但由于部分比较老的操作系统只支持SHA1算法,所以目前线上使用到的签名算法主要是SHA256和少量的SHA1。

SHA2和SHA1算法最主要的操作还是位之间的运算,包括AND, OR, XOR,然后进行最多不超过80轮的迭代。所以从算法原理来看,安全哈希的计算速度应该会比较快。

密钥交换算法

密钥交换就是指客户端和服务端通过交换各自的信息完成共同密钥的生成。目前线上使用到的主要密钥交换算法主要是如下三类:

三类算法最主要的数据运算公式如下:

RSA的安全性建立在大数因子分解很困难的基础上。它的核心数学运算公式如下:

Diffie-Hellman的安全性建立在离散对数求解比较困难的基础上,它的核心数学运算公式如下:

ECDHE是在椭圆曲线有限域上实现的Diffie-Hellman算法,具备Diffie-Hellman同样的安全性,它的核心数学运算公式如下:

分析上述运算方程有什么意义呢?

1、可以看出RSA和DH的主要计算都是模幂计算。模幂计算通常进行的轮次比较多,计算量比较大,对于CPU是一个很大的负担。

2、椭圆曲线实际上是一个集合,并且定义了一套计算规则。使用较小的数字就能实现RSA同样的安全强度。如下表所示,ECC使用224位长度的密钥就能实现RSA2048位长度的安全性。

对称加密算法

对称加密算法就是使用相同的密钥对数据进行加解密,常用的对称加密算法如下:

由于对称加密算法最主要的数学运算是XOR,虽然不同的模式对速度有一定的影响,但是由于密钥长度短,同时计算过程简单,对称加密的效率非常高。

消息认证码算法

消息认证码算法和证书签名算法的核心操作有点类似,主要是基于安全哈希函数,比如SHA1或者SHA2。所以这里就不做多余介绍。

性能测试

从上一章的数学原理分析可以得出初步的性能结论,RSA类运算应该是最消耗CPU资源的。但是纯数学分析还不能完全代表真正的软件性能。本章我们使用三种方式测试一下HTTPS的性能。

openssl speed

Openssl提供了一个速度测试的工具:openssl speed。顾名思义,它的功能就是测试openssl支持的全部算法的速度。

Openssl speed测试的基本原理是在指定时间内(比如3S或者10S),循环调用指定的密码算法,最后的运算次数就代表了该算法的性能。测试平台的信息如下:

由于线上业务单个请求的平均大小约为4K字节,所以下表统计了openssl加密和安全哈希算法每秒能够处理的字节数以及处理4K字节需要的用户时间。

可以看出,数据加解密和安全哈希的操作时间基本都是10微秒级别以下。其中耗时最多的是SHA256,需要20微秒。下表统计了RSA签名、校验及椭圆曲线的操作时间:

可以看出RSA2048位的签名操作速度很慢,单次操作需要耗时1.4毫秒,ECDSA的操作速度要快一些,大概需要0.5毫秒。椭圆曲线的操作时间统计如下:

椭圆曲线的操作曲线基点选取,耗时大概0.4毫秒。

运行时间分析

Openssl speed只能统计单个算法的性能和执行时间,但这个时间不能代表线上业务真实运行需要消耗的时间,原因是:

1、一次完整请求涉及到不同算法的组合,单个算法无法反映整体时间。

2、HTTPS协议交互时需要协议解析,网络交互,异步异常处理,这里肯定会有一些额外的性能损耗。

3、openssl实现TLS协议栈时需要调用多个应用层函数和系统调用,也会有一定的开销。

所以需要进一步统计真实的运行时间,主要是统计openssl处理各个主要环节的消耗时间。限于篇幅,这里只介绍ECDHE_RSA密钥交换算法的性能,具体的统计函数和时序如下:

从上图所示的数据能够非常明显地看到,ServerKeyExchange消耗了2.4毫秒,要显著超过其他消息的耗时。那ServerKeyExchange消息为什么消耗时间呢?进一步分析如下:

Perf 事件及火焰图分析

Perf是linux内核提供的一个性能分析工具。它的主要功能是能够周期性地采集各个函数的CPU周期数,从而反映性能瓶颈及热点代码。火焰图能更加形象直观地展示perf record记录下来的事件。通过perf record纪录压力测试nginx过程中的perf 事件,分别绘制ecdhe_rsa和rsa密钥交换算法下的火焰图,如下所示:

火焰图是SVG格式的向量图,由于事件数太多,截图无法反映详细的事件和函数关系图。

这里简单的概括一下: 和rsa计算相关的事件占全部采样约75%,ECC相关的计算占比约10%。

简单概括一下:使用RSA密钥交换时,RSA相关的事件采样占整体采样的比例约85%。

性能测试的结论

性能测试的最终目的是为了性能优化。根据之前的测试数据,HTTPS的计算性能优化思路可以总结如下:

1、完全握手对性能的影响非常大,性能降低至普通HTTP性能的 10%以下。应该尽量提升tcp reuse及session resume,减少完全握手的发生。

2、密钥交换过程中的RSA算法对性能的影响非常大,一次正常HTTPS交互过程,RSA计算过程需要消耗整体性能的75%左右。也就是说,如果能够提升RSA的性能,那么整体性能将最多提升4倍。

3、ECC曲线相关的计算约占整体计算量的7%,对整体性能的影响不大。但如果RSA计算优化好了,ECC的性能将逐渐成为瓶颈。

4、对称加解密及MAC计算对性能影响很小(微秒级别),暂时不需要考虑优化。

让大象起舞—-计算性能优化

RSA异步代理计算

从上一章的分析可以知道,HTTPS协议中最消耗CPU计算资源的就是密钥交换过程中的RSA计算。也是我们优化的最主要对象。如何优化呢?思路如下:

1、算法分离。将最消耗CPU计算的过程分离出来,释放本地CPU,提升整体吞吐性能。

2、并行计算。使用SSL硬件加速卡或者空闲CPU并行计算,提升计算效率。

3、异步代理。算法分离和计算的过程是异步的,不需要同步等待SSL加速计算的结果返回。

下面详细介绍一下上述步骤:

【算法分离】此的核心思想是:分析加密算法的完整过程,将最消耗CPU的计算过程剥离出来,避免在本地CPU上进行同步计算。

需要分离哪些算法呢?

由于我们现在的证书主要使用RSA签名,暂时没有ECDSA签名证书,所以当前设计只针对RSA签名证书。

RSA签名证书最常用的密钥交换算法是ECDHE_RSA,DHE_RSA和RSA,所以我们需要重点解决的就是这三个算法,如下图示:

由于DHE_RSA算法性能较差,所以优先推荐使用ECDHE_RSA和RSA密钥交换算法。下面详细描述一下两个算法的具体分离过程。

【ECDHE_RSA密钥交换算法分离】SSL完全握手过程中,ECDHE_RSA涉及到的最消耗CPU的握手消息为ServerKeyExchange,它在整个握手过程中的位置如下:

其中绿色框标示的Gen_master_key并不是一个握手消息,它表示的是生成Master key的过程,主要是调用SHA256完成计算。红色标示的ServerKeyExchange消息需要大量的CPU计算。那ServerKeyExchange消息为什么需要大量的CPU计算?它需要处理哪些内容呢?主要是如下两步:

1、选择ECC(椭圆曲线密码)的曲线类型、基点、曲线系数等参数,并根据这些参数生成公钥。

2、对曲线参数和公钥进行RSA签名。

由之前的分析得知,这里的RSA签名过程需要使用2048位长度的私钥对数据进行加密,非常消耗CPU。

【RSA密钥交换算法分离】RSA密钥交换算法的过程相对简单,因为没有ECC参数及公钥生成的过程。根据RFC5246描述,客户端使用RSA公钥对premaster内容进行加密,服务端需要使用私钥解密premaster key,从而生成最终的master key。

上述过程图示如下:

其中Gen_master_key主要分为如下两步:

1、私钥RSA私钥解密premaster key。

2、根据premaster key和其他参数生成master key。这一步同样是调用SHA256完成计算。

同样地根据之前的分析,RSA解密相比SHA256计算要消耗更多的CPU计算量。

并行计算

为了提升单位时间T内处理的性能,有两个思路:

1、减少单个请求的计算时间。

2、提升请求并发计算能力。即能够同时处理更多个请求

减少单个请求的计算时间通过采用更高频率和性能的CPU或者专用硬件加速卡的方案能够解决,本文不多做介绍。提升请求并发计算能力是指同一时刻使用多个CPU或者多个硬件加速卡方案实现性能的提升。模型如下:

显然,如果使用更多个数的CPU和硬件加速单元,并行计算能力就得到了显著提升。同样单位时间T内,能够处理的请求数变成了:4*T / T1。相比串行计算,性能提升了4倍。

异步请求

Nginx的当前进程必须等待openssl完成ServerKeyExchange或者premaster secret的处理后才能返回进行其他工作。如下图所示:

上图只是简单说明了RSA签名的同步计算过程,事实上RSA解密的计算过程类似。

假设请求1需要进行RSA签名(RSA_sign)操作,nginx必须等待上图中2 到 7共6个步骤全部完成才能处理下一个请求2。

飘红标示的BN_mod_exp是指大数的模幂计算,是RSA的核心运算过程,由于指数非常大,所以它是个非常消耗CPU的运算。

同步请求的弊端是:

1、由于该过程需要消耗大量的CPU,nginx整体性能受到严重制约。

2、除非同步计算的能力非常强,否则即使将过程分离到其他硬件或者CPU完成,由于进程间或者网络间的开销,同步过程也会严重制约nginx的整体性能。

所以上述计算过程需要异步进行,即在openssl进行高强度CPU计算时,比如处理serverKeyExchange或者premaster secret消息,nginx当前进程无需等待计算结果的返回,可以马上执行其他工作。异步过程如下图所示:

1、Nginx接收到请求1后,调用RSA_sign。

2、RSA_sign此时会调用RSA_private_encrypt,然后直接返回,不需等待RSA的签名结果。

3、Nginx此时可以处理其他请求。

4、RSA_private_encrypt是RSA签名的核心函数,主要是使用RSA私钥对哈希值进行加密。它的最主要计算过程还是大数的模幂计算。

5、最消耗CPU的计算由于已经被分离到其他CPU或者硬件加速卡,所以不会消耗本地CPU,同时由于这个过程是异步的,也不会阻塞上层的NGINX。

RSA异步代理计算的工程实现

难点

工程实现的难点主要体现在对openssl和nginx核心代码的掌控上。可以概括成如下几点:

1、需要学习和理解的知识量大。包含 ssl3.0到tls1.2协议,pki体系,pkcs标准,x509标准,ECC标准,光RFC的阅读涉及至少30个以上,常用的比如5246,5280,4492等。

2、openssl代码量大、旧、乱、深。

a)大。代码行数超过50万行。因为要实现不同协议版本,不同算法组合,还要跨平台,支持各种硬件,所以代码量非常庞大。

b)旧。openssl有很多历史遗留的无用代码,比如一些过时的算法、系统及加速硬件。

c)乱。风格不良不统一,充斥着大量宏定义,宏开关,缺少注释等。

d)深。由于涉及到版本和算法很多,本身就比较难懂,又进行了一系列的高层抽象和封装,比如EVP,ssl23,ssl3系列等。

提到了这么多openssl不好的地方,网上甚至有一些文章公开嘲笑甚至辱骂openssl,但是在我的心里却一直认为,一份开源免费却守护着虚拟世界安全的代码,值得每一个人尊敬和崇拜。

3、openssl虽然非常重要,但是互联网上关于openssl和HTTPS代码工程方面有深度有价值的参考资料几乎为零。

4、需要修改nginx事件框架实现SSL完全握手的优化。nginx虽然代码优良,参考资料也多,但是代码有很多细节设计得比较巧妙,修改事件框架很容易踩坑。

计算架构的变化

RSA计算方式的变化必然会导致计算架构的变化。其中现在默认的广泛使用的方式又叫本机CPU同步计算架构。

【本机同步计算架构】这里的同步是指上层应用比如nginx必须等待CPU执行完RSA计算后才能返回执行其他工作。这里需要注意的是,即使将同步模型的CPU换成SSL硬件加速卡,对性能的提升也非常有限,不到30%。

【异步代理计算架构】改造后的架构如下:

异步代理计算架构的特点将最消耗性能的RSA计算分离出来,使用并行计算能力更强的方案替代本机CPU完成计算,同时整个过程是异步的,上层应用程序(NGINX)不需要等待RSA计算结果的返回就能接收其他请求。

RSA异步代理性能优化结论

最终通过RSA异步代理计算,nginx ecdhe_rsa完全握手性能提升了3.5倍,由18000qps提升到了65000qps。

对称加解密的优化

虽然之前性能分析里提到了相比非对称密钥交换算法来讲,对称加密算法的性能非常卓越(好1到2个数量级),但是如果应用层传输内容较大的话,特别是移动端的CPU计算能力较弱,对称加密算法对性能的影响也不容忽视。如何优化呢?通过异步代理的方式显然不可能。原因是:会极大降低用户访问速度。由于应用层的每一个字节都需要对称加解密,使用异步的方式实现会严重降低加解密的实时性。

那有没有同步的优化方式呢?有。类似SSL硬件加速卡,intel针对AES算法实现硬件加速,并将它集成到了CPU指令里。

AES-NI指令

AES-NI是intel推出的针对AES对称加密算法进行优化的一系列指令,通过硬件计算实现计算速度的提升。如何测试AES-NI的性能呢?通过环境变量

aesni对性能的提升约20%, 由4.3W提升到5.1W。这里需要注意的是,如果需要单独使用openssl的API进行AES对称加解密,最好使用aes evp API,这样才会默认开启AES-NI指令。

1、chacha20-poly1305

chacha20-poly1305是由Dan Bernstein发明,并且由google推出的一种带身份认证的对称加密算法。其中chacha20是指对称加密算法,poly1305指身份认证算法。这个算法是对没有AES硬件加速功能的移动平台的补充,比如ARM芯片。从google公布的数据来看,chacha20-poly1305能够提升30%以上的加解密性能,节省移动端耗电量。当然,如果手机端支持AES-NI指令的话,chacha20就没有优势了。

我们最开始选用libressl的一个重要原因也是它支持chacha20-poly1305,openssl虽然暂时不支持,不过最近发布的版本应该马上就会支持了。

2、session resume

HTTPS最消耗性能的阶段就是完全握手,不管是对用户的访问速度还是CPU资源消耗,避免完全握手的发生都能够极大地提升性能。

SSL协议目前提供两种机制来实现简化握手,避免完全握手的发生:

a)session cache

SSL2.0引入了session identifier机制,如果客户端使用的SSL协议版本大于2.0(全部浏览器都支持,包括IE6),那么server端在收到client hello消息时会生成一个32字节长度的ID(SSL2.0以后ID是0到48字节长度),保存在缓存并且将生成的session id通过server hello消息发送给用户。

客户端在后续的SSL握手请求中通过client hello消息发送session id,server端获取到ID后会从本地或者集群缓存中查找,如果ID查找命中,表明这个session 是可以信任的,能够复用。SSL握手提前完成,不需要继续处理完全握手需要的密钥交换等消耗CPU资源的步骤,同时节省了一个RTT。

【分布式session cache的应用】Session identifier支持得非常广泛。但nginx目前只支持内置缓存及单机进程间共享的session缓存,在多服务器的接入架构下,单机的session缓存几乎是无效的。

针对这种场景,TGW支持四层会话保持,这样在会话保持期间内的client都会落到相同的机器,显著地提升了session cache的命中率。

b)session ticket

session tickets (RFC5077)是一种不需要server端保存session状态信息的session恢复机制。客户端在client hello消息里发送empty session ticket extension表示支持session ticket机制,服务端的nginx在server hello里也会发送一条empty sesson ticket 消息表示支持。这样在完全握手快要结束时,nginx会发送new session ticket消息生成一个新的ticket。

客户端在后续的请求过程中会在client hello包里携带这个ticket,如果nginx能够正确解密这个ticket,标明session能够复用。握手完成,同时发送new session ticket更新ticket。即每次发送请求的ticket都不同。

【分布式session ticket的应用】在多个STGW接入的环境下,同样存在不同用户的session ticket无法被正确处理的问题。为了解决这个问题,STGW配置了全局的session ticket key,即针对全部STGW的nginx,使用相同的key来进行加解密。相同客户端的session ticket,不管下次落到哪台nginx,都能被正确处理,实现简化握手。

结论

1、提升session resume比率,尽量实现分布式session cache及session ticket,减少SSL完全握手的发生。不仅节省网络RTT,提升用户访问速度,也避免了非对称密钥交换的发生,减少了CPU的消耗。

2、通过异步代理完成RSA的私钥计算。ssl完全握手性能由18000qps提升到了63000qps,提升了~3.5倍。节省了接入机器成本,提升了业务的活动运营及防攻击能力。

3、使用性能更高,更安全的对称加密算法,AES-GCM,CHACHA20-POLY1305.

4、开启对称算法加速指令AES-NI。

文章来源于腾讯云开发者社区,点击查看原文