前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >让大象起舞第二弹---HTTPS计算性能优化

让大象起舞第二弹---HTTPS计算性能优化

作者头像
腾讯 架构师
发布2021-07-15 11:23:23
9590
发布2021-07-15 11:23:23
举报
文章被收录于专栏:技术运维分享技术运维分享

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。

那ServerKeyExchange消息为什么需要大量的CPU计算?它需要处理哪些内容呢?主要是如下两步:

  1. 选择ECC(椭圆曲线密码)的曲线类型、基点、曲线系数等参数,并根据这些参数生成公钥。
  2. 对曲线参数和公钥进行RSA签名。

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

RSA密钥交换算法分离

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

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

并行计算

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

  1. 减少单个请求的计算时间。
  2. 提升请求并发计算能力。即能够同时处理更多个请求

减少单个请求的计算时间通过采用更高频率和性能的CPU或者专用硬件加速卡的方案能够解决,本文不多做介绍。

提升请求并发计算能力是指同一时刻使用多个CPU或者多个硬件加速卡方案实现性能的提升。

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

4*T / T1

相比串行计算,性能提升了4倍。

异步请求

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

同步请求的弊端是:

  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代码量大、旧、乱、深。
  • 大。代码行数超过50万行。因为要实现不同协议版本,不同算法组合,还要跨平台,支持各种硬件,所以代码量非常庞大。
  • 旧。openssl有很多历史遗留的无用代码,比如一些过时的算法、系统及加速硬件。
  • 乱。风格不良不统一,充斥着大量宏定义,宏开关,缺少注释等。
  • 深。由于涉及到版本和算法很多,本身就比较难懂,又进行了一系列的高层抽象和封装,比如EVP,ssl23,ssl3系列等。

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

  1. openssl虽然非常重要,但是互联网上关于openssl和HTTPS代码工程方面有深度有价值的参考资料几乎为零。
  2. 需要修改nginx事件框架实现SSL完全握手的优化。nginx虽然代码优良,参考资料也多,但是代码有很多细节设计得比较巧妙,修改事件框架很容易踩坑。

计算架构的变化

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

本机同步计算架构

这里的同步是指上层应用比如nginx必须等待CPU执行完RSA计算后才能返回执行其他工作。

这里需要注意的是,即使将同步模型的CPU换成SSL硬件加速卡,对性能的提升也非常有限,不到30%。

异步代理计算架构

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

openssl协议栈的改造

由于RSA计算和非对称密钥交换有关且发生在SSL完全握手的过程中。同时由于openssl本身只能支持RSA同步计算,不支持异步调用,如果要实现RSA的异步代理计算,必须要对openssl的协议栈进行改造。

前面也提到过openssl的一些问题,事实也被业界诟病了很久,直到heartbleed漏洞爆发后,google和openbsd先后推出了openssl的fork版本,那么我们该如何做出选择呢?

openssl,boringssl, libressl的选择。

boringssl

最开始打算选用boringssl,它是google推出的基于openssl的fork 版本。它的优点是代码风格良好,代码精简(只有不到12W行C代码 + 7W行头文件),代码健壮 。

但是很快我们就放弃了boringssl,原因是:

  1. boringssl虽然是一个开源库,但它只是面向google自家使用,并不提供通用的兼容性和稳定性保证,官方网站明确说明不提供API,ABI的可靠性保证。
  2. Google在安全方面的激进策略和国内产品的保守落后现状容易产生矛盾。比如及早在boringssl中移除SSLv3,RC4,Spdy等的支持。但是国内还有很多客户端只支持SSLV3等协议和ciphersuite。Google一心想推动协议和算法朝着更加安全、高效的方向前进,无奈部分老旧客户端拖了严重后腿,为了考虑这部分用户需求,我们不敢轻易使用boringssl。

总得来说,boringssl适合阅读,学习,但不适合用于面向广大客户端的业务。

libressl

libressl 是openbsd推出的基于openssl的fork版本,从名字也能看出来,它的目的是想取代openssl。

它基本上具备了boringssl的优点,比如代码量精简,风格良好,更加安全等特点。最重要的是,已经有一些关键系统(openbsd, OS X10.11)等,使用了libressl。证明这已经是一个工业级的可靠的开源库了。

所以在前期的部署过程中,我们选用了libressl,但是后来压力测试发现它有一个致命的缺点,ECDHE的性能非常差,只有openssl的1/4左右。为什么会这样呢?我猜测原因可能是跟intel针对ecdhe算法进行了一个很大的优化,它将算法专利捐给了openssl,但是并未捐献给libressl。导致libressl无法直接使用这一优化算法。

openssl

由于上述两个库的重大缺陷,我们最终还是回归了openssl。

nginx事件机制的改造

nginx的模块功能很丰富也很强大,一共有11个可以介入的阶段(phase),包括起始的读取请求内容的阶段NGX_HTTP_POST_READ_PHASE,到最后一个打印日志阶段NGX_HTTP_LOG_PHASE,其中只有7个phase可以编写插入自定义模块。

出于通用性的考虑,nginx模块实现时机有一个限制,那就是必须在http 的header全部解析完成之后,自定义模块才能够工作。所以存在如下局限:

  1. 模块介入时机有限。比如无法干预TCP及ssl握手阶段的交互。
  2. 部分功能会影响性能。假如我们需要增加一个IP黑名单,事实上在tcp accept时就可以开始禁止该IP连接,不需要等到HTTP的头部数据解析完后才开始工作,因此Accpet之后的所有开销都是浪费的。特别是攻击规模比较大的时候,这部分对性能的影响也比较严重。

也正是由于第一个局限,我们无法通过添加一个自定义模块,而必须对nginx的核心事件代码进行改造,才能完成我们的性能优化目标。

nginx SSL握手事件的改造

nginx官方版本中的的SSL握手事件代码集中在src/event/ngx_event_openssl.c中,当然调用过程还是需要由ngx_http_request.c中发起,TCP握手完成后,调用ngx_http_ssl_handshake开始整个握手过程。

整个SSL握手事件的入口是ngx_ssl_handshake,有两个出口:

  1. ngx_http_close_connection(),握手异常,关闭连接。
  2. ngx_http_wait_request_handler(),握手成功,开始等待应用层的HTTP数据。

改造的ngx ssl握手事件不会改变或者删除原有逻辑,只是增加了远程异步代理计算的逻辑。

RSA异步代理性能优化结论

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

对称加解密的优化

虽然之前性能分析里提到了相比非对称密钥交换算法来讲,对称加密算法的性能非常卓越(好1到2个数量级),但是如果应用层传输内容较大的话,特别是移动端的CPU计算能力较弱,对称加密算法对性能的影响也不容忽视。

如何优化呢?通过异步代理的方式显然不可能。原因是:会极大降低用户访问速度。由于应用层的每一个字节都需要对称加解密,使用异步的方式实现会严重降低加解密的实时性。

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

AES-NI指令

AES-NI是intel推出的针对AES对称加密算法进行优化的一系列指令,通过硬件计算实现计算速度的提升。

如何测试AES-NI的性能呢?通过环境变量。

aes-ni: OPENSSL_ia32cap="~0x200000200000000" openssl speed -elapsed -evp

aes-128-gcm 或者在代码里将 crypto/evp/e_aes.c # define AESNI_CAPABLE

(OPENSSL_ia32cap_P[1]&(1<<(57-32)))进行设置。

aesni对性能的提升约20%, 由4.3W提升到5.1W。

这里需要注意的是,如果需要单独使用openssl的API进行AES对称加解密,最好使用aes evp API,这样才会默认开启AES-NI指令。

chacha20-poly1305

chacha20-poly1305是由Dan Bernstein发明,并且由google推出的一种带身份认证的对称加密算法。其中chacha20是指对称加密算法,poly1305指身份认证算法。这个算法是对没有AES硬件加速功能的移动平台的补充,比如ARM芯片。

从google公布的数据来看,chacha20-poly1305能够提升30%以上的加解密性能,节省移动端耗电量。

当然,如果手机端支持AES-NI指令的话,chacha20就没有优势了。

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

session resume

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

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

  1. session cache
  2. session ticket

session cache

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的命中率。

session ticket

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,都能被正确处理,实现简化握手。

机制

优点

缺点

session ticket

server端不需要保存状态,不需要维护内存

1. 只是 TLS 协议的一个扩展特性,目前的支持率不是很广泛,只有 60% 左右。 2.session ticket 需要维护一个全局的 key 来加解密,需要考虑 KEY 的安全性和部署效率。

session cache

session id 是 TLS 协议的标准字段,市面上的浏览器全部都支持 session cache

1.需要消耗服务端内存来存储 session 内容。2. 目前的开源软件包括 nginx,apache 只支持单机多进程间共享缓存,不支持多机间分布式缓存。

结论

  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。
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2017-06-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 腾讯架构师 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
SSL 证书
腾讯云 SSL 证书(SSL Certificates)为您提供 SSL 证书的申请、管理、部署等服务,为您提供一站式 HTTPS 解决方案。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档