首页 > 代码库 > 电商网站HTTPS实践之路(三)——性能优化篇

电商网站HTTPS实践之路(三)——性能优化篇

通过分析TLS握手过程的细节我们会发现HTTPS比HTTP会增加多个RTT网络传输时间,既增加了服务端开销,又拖慢了客户端响应时间。因此,性能优化是必不可少的工作。很多文章都集中在服务端的性能优化上,但对于电商行业而言,大部分的用户流量源于App,因此客户端的性能优化配合服务端才能使收益最大化。

1. HTTPS带来的负担

凡事都有两面性。

1.1 增加的传输延时

使用HTTPS传输增加的开销不仅仅是两次TLS握手的过程。优化性能首先要知己知彼。了解性能损耗在哪里,才能有针对性的进行部署。
对于用户来说,使用HTTP请求,首次请求时只要和服务端TCP三次握手建立连接,便可以开始应用数据传输了。
技术分享
而对于HTTPS而言,事情就不那么简单了。
技术分享
1. 用户习惯于使用HTTP请求你的网站。要保护用户的安全,首先要让用户强制302/301到HTTPS。这次跳转至少增加1个RTT的延时;
2. 302跳转后要再次TCP建连,增加1个RTT的延时;
3. 开始两阶段TLS握手,细节如下图所示,增加至少两个RTT的延时。
技术分享
- Client Hello: 客户端开始新的握手,并将自身支持的功能提供给服务端;
- Server Hello:服务端选择连接参数;
- Certificate*:服务端发送证书链;
- ServerKeyExchange*:服务端发送公钥(public key)等生成主密钥(premaster secrect)的额外信息给客户端;
- ServerHelloDone:服务端通知完成协商过程;
- ClientKeyExchange:客户端发送加密后的主密钥给服务端
- [ChangeChiperSpec]:客户端如果要切换加密方式通知服务端
- Finished:客户端完成
- [ChangeChiperSpec]:服务端如果要切换加密方式通知客户端
- Finished:服务端完成
4. 另外客户端如果第一次获取服务端的证书链信息,还需要通过Oscp来验证证书的吊销状态,又需要至少1个RTT延时。
5. 最终,开始应用层数据的传输。

1.2 服务端额外开销

TLS握手过程中密钥交换和加密对CPU都会产生额外的计算开销。选择不同的算法(身份验证算法、密钥交换算法、加密算法)开销不同。比如,2048位RSA作为密钥交换算法对CPU压力就会很大,而ECDHE_RSA(椭圆曲线密钥交换)开销就小的多,RSA可以仍保留用于身份验证。
当然,不管选用多优化的算法,开销是避免不了的,如下图所示。
技术分享

2. 服务端性能优化

服务端性能优化,主要体现在Web服务器配置的优化,我们以Ngnix 1.11.0版本为例。当然你也可以选择Apache、H2O等。

2.1 HSTS的合理使用

HSTS(HTTP Strict Transport Security, HTTP严格传输安全协议)表明网站已经实现了TLS,要求浏览器对用户明文访问的Url重写成HTTPS,避免了始终强制302重定向的延时开销。
HSTS的实现原理是:当浏览器第一次HTTP请求服务器时,返回的响应头中增加Strict-Transport-Security,告诉浏览器在指定的时间内,这个网站必须通过HTTPS协议来访问。也就是对于这个网站的HTTP地址,浏览器需要现在本地替换为HTTPS之后再发送请求。
其配置如下所示。max-age表明HSTS在浏览器中的缓存时间,includeSubdomainscam参数指定应该在所有子域上启用HSTS,preload参数表示预加载,稍后会具体解释。

add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload"

在CanIUse上我们可以查询HSTS协议的浏览器支持度:
技术分享
在使用HSTS的过程中仍有一些值得注意的问题:
1. HSTS将全部的证书错误视为致命的。因此,一旦主域使用HSTS,浏览器将放弃对域名所有无效证书站点的连接。
2. 首次访问仍然使用HTTP,然后才能激活HSTS。无法保障首次访问的安全性如何解决?可以通过preloading预加载的方式,与浏览器厂商约定好一份支持HSTS的网站清单来缓解。目前Google已经提供了在线注册服务https://hstspreload.appspot.com/
3. 如何撤销HSTS?通过Strict-Transport-Security: max-age=0将缓存设置为0可以撤销HSTS。但是只有当浏览器再次访问网站并且得到响应更新配置时才能生效。

2.2 会话恢复的合理使用

会话恢复机制是指在一次完整协商的连接断开时,客户端和服务端会将会话的安全参数保存一段时间。后续的连接,双方使用简单握手恢复之前协商的会话。大大减少了TLS握手的开销。
会话恢复的方案可以分为两种:会话ID(Session ID)和会话票证(Session Ticket)。会话ID通过服务端为会话指定唯一的标识,并缓存会话状态。在第一次完整协商的过程中,ServerHello消息中将会话ID发回客户端。希望恢复会话的客户端在下一次握手中将会话ID放入ClientHello,服务端认可后接着使用之前协商的主密钥进行加密。而会话票证将所有会话状态保持在客户端(类似于HTTP Cookie)。
1. 配置会话票证较为简单:

ssl_session_tickets on;
ssl_session_ticket_key /usr/local/nginx/ssl_cert/session_ticket.key;

生产key的命令通过openssl生成:

openssl rand –out session_ticket.key 48

注意集群情况下key值保持一致。另外注意使用会话票证前需要开启支持前向性加密支持的密钥套件。
2. 配置会话ID需要注意,会话状态是保存在服务器上的,集群状态下如何保证会话ID的命中率?最简单的方式是负载的轮询策略使用IP_HASH,保证同一客户端总是被分发到集群中的相同节点,但这样未免不够灵活。因此需要采用分布式缓存的方式,将会话状态存储在集群共享的redis中。
如何操作Nginx中的TLS会话信息,可以参考openresty中的ssl_session_fetch_by_lua_block 模块。具体见https://github.com/openresty/lua-nginx-module#ssl_session_store_by_lua_file。
技术分享

2.3 Ocsp stapling的合理使用

OCSP(Online Certificate Status Protocol, 在线证书状态协议)用于查询证书的吊销信息。OCSP实时查询会增加客户端的性能开销。因此,可以考虑通过OCSP stapling的方案来解决:OCSP stapling是一种允许在TLS握手中包含吊销信息的协议功能,启用OCSP stapling后,服务端可以代替客户端完成证书吊销状态的检测,并将全部信息在握手过程中返回给客户端。增加的握手信息大小在1KB以内,但省去了用户代理独立验证吊销状态的时间。
启用OCSP stapling的方式有很多种,比如在线校验。此方式需要支持服务器能够主动访问证书校验服务器才能生效,并且在每次重启nginx的时候会主动请求一次,如果网络不通会导致nginx启动缓慢。

# 启用OCSP stapling
ssl_stapling on;
# valid表示缓存5分钟,resolver_timeout表示网络超时时间
resolver 8.8.8.8 8.8.4.4 223.5.5.5 valid=300s;
resolver_timeout 5s;         
# 启用OCSP响应验证,OCSP信息响应适用的证书   
ssl_stapling_verify on;  
ssl_trusted_certificate /usr/local/nginx/ssl_cert/trustchain.crt;      

为了更可靠,你也可以人工负责更新文件内容,设定Nginx直接从文件获取OCSP响应而无需从服务商拉取。

# 启用OCSP stapling
ssl_stapling on;
ssl_stapling_file /usr/local/nginx/oscp/stapling_file.ocsp;            
# 启用OCSP响应验证,OCSP信息响应适用的证书   
ssl_stapling_verify on;  
ssl_trusted_certificate /usr/local/nginx/ssl_cert/trustchain.crt;      

2.4 TLS协议的合理配置

首先要指定TLS协议的版本,不安全的SSL2和SSL3要废弃掉

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

其次,建议启用ssl_prefer_server_ciphers,用来告诉Nginx在TLS握手时启用服务器算法优先,由服务器选择适配算法而不是客户端:

ssl_prefer_server_ciphers on

然后,选择最优的加密套件以及优先顺序,具体可参考Mozilla的https://wiki.mozilla.org/Security/Server_Side_TLS。优先选择支持前向加密的算法,且按照性能的优先顺序排列

ssl_ciphers ssl_ciphers "ECDHE-ECDSA-CHACHA20-POLY1305 ECDHE-RSA-CHACHA20-POLY1305 ECDHE-ECDSA-AES128-GCM-SHA256 
        ECDHE-RSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-RSA-AES256-GCM-SHA384 DHE-RSA-AES128-GCM-SHA256 
        DHE-RSA-AES256-GCM-SHA384 ECDHE-ECDSA-AES128-SHA256 ECDHE-RSA-AES128-SHA256 ECDHE-ECDSA-AES128-SHA 
        ECDHE-RSA-AES256-SHA384 ECDHE-RSA-AES128-SHA ECDHE-ECDSA-AES256-SHA384 ECDHE-ECDSA-AES256-SHA ECDHE-RSA-AES256-SHA 
        DHE-RSA-AES128-SHA256 DHE-RSA-AES128-SHA DHE-RSA-AES256-SHA256 DHE-RSA-AES256-SHA ECDHE-ECDSA-DES-CBC3-SHA 
        ECDHE-RSA-DES-CBC3-SHA EDH-RSA-DES-CBC3-SHA AES128-GCM-SHA256 AES256-GCM-SHA384 AES128-SHA256 AES256-SHA256 
        AES128-SHA AES256-SHA DES-CBC3-SHA !DSS";

最后,如果有双向验证的需求,可以开启Nginx的客户端身份验证。Nginx将只接受包含有效客户端证书的请求。如果请求未包含证书或者证书校验失败,Nginx会返回一个400错误响应。

# 要求客户端身份验证
ssl_verify_client on;
# 指定客户端证书到根证书的最大证书路径长度
ssl_verify_depth 3;
# 指定允许签发客户端证书的CA证书
ssl_client_certificate trustchain.crt;
# 完整证书链中需要包含的其他CA证书
ssl_trusted_certificate root-ca.crt;
# 证书吊销列表
ssl_crl revoked-certificates.crl;

2.5 False Start的合理使用

TLS False Start是指客户端在发送ChangeCipherSpec Finished 同时发送应用数据(如HTTP请求),服务端在 TLS 握手完成时直接返回应用数据(如HTTP响应)。这样,应用数据的发送实际上并未等到握手全部完成,故谓之False Start。
技术分享
要实现False Start,服务端必须满足两个条件:
1. 服务端必须支持NPN(Next protocol negotiation, ALPN的前身)或者ALPN(Application layer protocol negotiation, 应用层协议协商);
2. 服务端必须采用支持前向加密的算法。
补充说明下什么是前向加密(perfect forward secrecy)。前向加密要求一个密钥只能访问由它所保护的数据;用来产生密钥的元素一次一换,不能再产生其他的密钥;一个密钥被破解,并不影响其他密钥的安全性。

2.6 SNI功能的合理使用

SNI(Server Name Indicate)允许客户端在发起SSL握手请求时(ClientHello阶段),就提交请求的Host信息,使得服务器能够切换到正确的域并返回相应的证书。通过这种方式解决了一个IP(虚拟机)部署多个域名服务的问题。
Nginx支持SNI的方式并自动开启。当遇到不支持这一特性的客户端用户时,通常情况下,Nginx会返回默认站点的服务器证书。比如下面的情况下,不支持SNI的客户端,Nginx返回serversuning.pem。这样证书是否能正确匹配是无法保障的,会带来不必要的麻烦和困扰。因此,移动端开发都应该要求启用SNI扩展。

server {
    listen 443 ssl default_server;

    ssl_certificate /usr/local/nginx/cert/serversuning.pem;  
    ssl_certificate_key /usr/local/nginx/cert/suning.key; 
    ...
}

server {
    listen 443 ssl;
    server_name sit1.suning.com;

    ssl_certificate /usr/local/nginx/cert/serversuningcom.pem;  
    ssl_certificate_key /usr/local/nginx/cert/suningcom.key; 
    ...
}
server {
    listen 443 ssl default_server;
    server_name sit1.suning.cn;

    ssl_certificate /usr/local/nginx/waf/serversuningcn.pem;  
    ssl_certificate_key /usr/local/nginx/waf/suningcn.key; 
    ...
}

2.7 HTTP 2.0的合理使用

HTTP 2笔者在《 HTTP 2.0 原理详细分析》和《 Nginx实现HTTP/2——原理、实践与数据分析》中都有详细地介绍,这里就不再展开。需要提示下,Nginx在1.9.x版本就开始尝试支持http2协议,但每个版本都会有bugfix,仍需要谨慎开启,具体可参考Nginx版本更新日志。

2.8 SSL硬件加速卡合理使用

可以通过SSL硬件加速卡设备来代替CPU进行TLS握手过程中的运算。推荐的有Cavium的加速卡,Cavium引擎可以集成到Nginx模块中,支持物理机和虚拟机环境。同时,虚拟机环境下的测试效果要比物理机好。建议开启Nginx异步请求Cavium引擎模式,更有效提高使用率。
下面是我们压测的CPU到20%情况下,使用TLS 1.2协议、加密套件采用ECDHE-RSA-AES128-SHA256 、HTTPS 短连接的各种环境性能数据,可见使用Cavium,物理机性能提升比:325%,虚拟机性能提升比:588%。

环境 流量类型 TPS 延迟(s)
虚拟机 HTTPS 172 0.066
虚拟机 + Cavium加速卡 HTTPS 1012 0.066
物理机 HTTPS 832 0.066
物理机 + Cavium加速卡 HTTPS 2708 0.059

另外,是否使用硬件加速见仁见智了,其在性能提升上肯定是有效果的,但由于设备价格高昂,很难大规模化,对于大部分互联网公司是一件奢侈品。借用Facebook关于硬件加速的说法:
“我们发现当前基于软件的TLS实现在普通CPU上已经运行的足够快,无需借助专门的加密硬件就能够处理大量的HTTPS请求。我们使用运行于普通硬件上的软件提供全部HTTPS服务。”

最后,我们来总结下服务端Nginx的配置,一个配置模板仅供参考:

server {
    listen 443 ssl http2 default_server;
    server_name  site1.suning.com;
        add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload";

        ssl_certificate /usr/local/nginx/cert/serversuningcom.pem;  
        ssl_certificate_key /usr/local/nginx/cert/suningcom.key;  

        # 分配10MB的共享内存缓存,不同工作进程共享TLS会话信息
        ssl_session_cache shared:SSL:10m;
        # 设置会话缓存过期时间24h
        ssl_session_timeout 1440m;

        ssl_protocols TLSv1 TLSv1.1 TLSv1.2 SSLv3;   
        ssl_prefer_server_ciphers on;  
        ssl_ciphers ssl_ciphers "ECDHE-ECDSA-CHACHA20-POLY1305 ECDHE-RSA-CHACHA20-POLY1305 ECDHE-ECDSA-AES128-GCM-SHA256 
        ECDHE-RSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-RSA-AES256-GCM-SHA384 DHE-RSA-AES128-GCM-SHA256 
        DHE-RSA-AES256-GCM-SHA384 ECDHE-ECDSA-AES128-SHA256 ECDHE-RSA-AES128-SHA256 ECDHE-ECDSA-AES128-SHA 
        ECDHE-RSA-AES256-SHA384 ECDHE-RSA-AES128-SHA ECDHE-ECDSA-AES256-SHA384 ECDHE-ECDSA-AES256-SHA ECDHE-RSA-AES256-SHA 
        DHE-RSA-AES128-SHA256 DHE-RSA-AES128-SHA DHE-RSA-AES256-SHA256 DHE-RSA-AES256-SHA ECDHE-ECDSA-DES-CBC3-SHA 
        ECDHE-RSA-DES-CBC3-SHA EDH-RSA-DES-CBC3-SHA AES128-GCM-SHA256 AES256-GCM-SHA384 AES128-SHA256 AES256-SHA256 
        AES128-SHA AES256-SHA DES-CBC3-SHA !DSS";

        ssl_session_tickets on;
        ssl_session_ticket_key /usr/local/nginx/ssl_cert/session_ticket.key;

        #设置TLS日志格式
        log_format ssl "$time_local $server_name $remote_addr $connection $connnection_requests $ssl_protocol 
        $ssl_cipher $ssl_session_id $ssl_session_reused";
    access_log /usr/local/nginx/logs/access.log ssl;

    ssl_stapling on;
        ssl_stapling_file /usr/local/nginx/oscp/stapling_file.ocsp;            
        ssl_stapling_verify on;  
        ssl_trusted_certificate /usr/local/nginx/ssl_cert/trustchain.crt;      

    root   html;
    index  index.html index.htm;

    location / {
        ...
    }
        error_page 403 /403.html;
        location = /403.html {
            root /usr/local/nginx/waf/403/default;
        }
        error_page 500 502 503 504 /502.html;
        location = /502.html {
            root /usr/local/nginx/waf/403/default;
        }
}

3. 客户端性能优化

App中使用HTTPS请求,建议设计客户端的代理层SDK。代理层的主要目的包括两点:(1)统一以HTTP 2协议向服务端转发请求;(2)调用服务端HttpDns接口,获取准确的地址解析信息。

3.1 移动端HTTP2加速代理

比如Android使用组件OkHttp 3,IOS使用组件NSURLSession,都可以支持HTTP 2.0协议。我也曾翻译介绍过《OkHttp, 安卓和Java应用的HTTP&HTTP2.0客户端》。深入到具体代码开发和使用层面不在本文中展开。只有当客户端和服务端都采用HTTP 2.0进行通信,才能达到加速的效果。通过数据也能印证HTTP 2.0的效果。
技术分享

3.2 HttpsDns解决DNS攻击劫持

Dns劫持通过篡改用户的解析指向,将用户的流量导向第三方,以实现恶性盈利的目的。另外还有一些运营商为了避免网间结算费用,会在内网做站点镜像,再通过Dns劫持的方式,使用户直接访问镜像。
技术分享
当全站实现HTTPS后,由于缺乏证书和私钥等必要信息,能够保证他人Dns劫持用户后无法达到非法目的,但同时也无法正常响应用户的请求。这就是一把双刃剑,因为普通用户只会认为是你的网站加载不出来。
技术分享
所以,全站HTTPS只能避免Dns劫持带来的损失,解决Dns劫持问题还需要另觅良法。从根本上而言Dns劫持的原因是我们无法控制本地的LocalDNS不被黑(毕竟是运营商的东西你懂的),那么有没有可能绕开运营商解析?PC端我们肯定是做不到的,而移动端App我们可以使用HttpsDns的方案。
HttpsDns方案:用Https协议(IP代替域名)向HttpDns集群(权威DNS)的443端口进行请求,代替传统的DNS协议向DNS服务器的53端口进行请求。也就是使用Https协议去进行dns解析请求,将服务器返回的解析结果,也就是域名对应的服务器ip获得,直接向该ip发起对应的api服务请求,代替使用域名。备选情况下(HttpsDns解析失败),再走传统的LocalDNS解析方式。
技术分享
HttpsDns的方案优势在于:
1. 防止了LocalDNS劫持问题
2. 平均访问延迟下降,由于后续请求直接通过IP访问,省去了域名解析时间。并且可以通过一些算法计算出最优性能的服务端IP(Ping延时最小),缓存在客户端本地地址库;
3. 用户连接失败率下降
目前提供HttpDns服务端能力的厂商有中网、dnspod等,基本上是以约定接口的形式供客户端来调用,返回解析结果。比如:
技术分享
客户端的实现并没有想象的简单,用ip替换域名访问,要考虑很多问题,比如:
1. Https场景下ip直连出现的证书校验问题
2. 代理场景下的HttpDns问题
3. ip访问的时候Cookie的问题
在这里我们不再展开,有兴趣可以参考:
《Android 使用OkHttp支持HttpDNS》 http://blog.csdn.net/sbsujjbcy/article/details/50532797
《Android OkHttp实现HttpDns的最佳实践(非拦截器)》 http://blog.csdn.net/sbsujjbcy/article/details/51612832
CNSRE/HTTPDNSLib https://github.com/CNSRE/HTTPDNSLib

以上,针对TLS层的性能优化就已经完结了。然而,这就结束了吗?性能提升就到此为止了吗?当然不是。一方面,我们还应该关注与最大限制地去提升TCP层的性能,来配合TLS的优化。包括初始拥塞窗口调优、防止空闲时慢启动、keep-alive等;
另一方面,关注更新的技术成果和动态,比如追求0RTT损耗的TLS 1.3、QUIC协议等

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    电商网站HTTPS实践之路(三)——性能优化篇