mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-10-19 00:13:43 +08:00
mod
This commit is contained in:
129
极客时间专栏/系统性能调优必知必会/应用层编解码优化/14 | 优化TLS|SSL性能该从何下手?.md
Normal file
129
极客时间专栏/系统性能调优必知必会/应用层编解码优化/14 | 优化TLS|SSL性能该从何下手?.md
Normal file
@@ -0,0 +1,129 @@
|
||||
<audio id="audio" title="14 | 优化TLS/SSL性能该从何下手?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/79/79/797e80fb602ca9d9c4146edef5928279.mp3"></audio>
|
||||
|
||||
你好,我是陶辉。
|
||||
|
||||
从这一讲开始,我们进入应用层协议的处理。
|
||||
|
||||
信息安全在当下越来越重要,绝大多数站点访问时都使用https://替代了http://,这就是在用TLS/SSL协议(下文简称为TLS协议)来保障应用层消息的安全。但另一方面,你会发现很多图片类门户网站,还在使用http://,这是因为TLS协议在对信息加解密的同时,必然会降低性能和用户体验,这些站点在权衡后选择了性能优先。
|
||||
|
||||
实际上,TLS协议由一系列加密算法及规范组成,这些算法的安全性和性能各不相同,甚至与你的系统硬件相关。比如当主机的CPU支持AES-NI指令集时,选择AES对称加密算法便可以大幅提升性能。然而,要想选择合适的算法,需要了解算法所用到的一些数学知识,而很多同学由于忽视了数学原理便难以正确地配置TLS算法。
|
||||
|
||||
同时,TLS协议优化时也需要了解网络和软件工程知识,比如我们可以在网络的不同位置缓存密钥来优化性能。而且,TLS协议还可以优化其他应用层协议的性能,比如从HTTP/1升级到HTTP/2协议便可以通过TLS协议减少1个RTT的时间。
|
||||
|
||||
优化TLS性能究竟该从何下手呢?在我看来主要有两个方向,一是对称加密算法的性能优化,二是如何高效地协商密钥。下面我们来详细看看优化细节。
|
||||
|
||||
## 如何提升对称加密算法的性能?
|
||||
|
||||
如果你用Wireshark等工具对HTTPS请求抓包分析,会发现在TCP传输层之上的消息全是乱码,这是因为TCP之上的TLS层,把HTTP请求用对称加密算法重新进行了编码。**当然,用Chrome浏览器配合Wireshark可以解密消息,帮助你分析TLS协议的细节**(具体操作方法可参考[《Web协议详解与抓包实战》第51课](https://time.geekbang.org/course/detail/175-104932))。
|
||||
|
||||
现代对称加密算法的特点是,即使把加密流程向全社会公开,攻击者也从公网上截获到密文,但只要他没有拿到密钥,就无法从密文中反推出原始明文。如何同步密钥我们稍后在谈,先来看如何优化对称加密算法。
|
||||
|
||||
目前主流的对称加密算法叫做AES(Advanced Encryption Standard),它在性能和安全上表现都很优秀。而且,它不只在访问网站时最为常用,甚至你日常使用的WINRAR等压缩软件也在使用AES算法(见[官方FAQ](https://www.win-rar.com/encryption-faq.html?&L=0))。**因此,AES是我们的首选对称加密算法,**下面来看看AES算法该如何优化。
|
||||
|
||||
**AES只支持3种不同的密钥长度,分别是128位、192位和256位,它们的安全性依次升高,运算时间也更长。**比如,当密钥为128比特位时,需要经过十轮操作,其中每轮要用移位法、替换法、异或操作等对明文做4次变换。而当密钥是192位时,则要经过12轮操作,密钥为256比特位时,则要经过14轮操作,如下图所示。
|
||||
|
||||
[<img src="https://static001.geekbang.org/resource/image/8a/28/8ae363f2b0b8cb722533b596b9201428.png" alt="" title="AES128的10轮加密流程[br]此图由Ahmed Ghanim Wadday上传于www.researchgate.net">](http://www.researchgate.net)
|
||||
|
||||
密钥越长,虽然性能略有下降,但安全性提升很多。比如早先的DES算法只有56位密钥,在1999年便被破解。**在TLS1.2及更早的版本中,仍然允许通讯双方使用DES算法,这是非常不安全的行为,你应该在服务器上限制DES算法套件的使用**(Nginx上限制加密套件的方法,参见《Nginx 核心知识100讲》[第96课](https://time.geekbang.org/course/detail/138-75878) 和[第131课](https://time.geekbang.org/course/detail/138-79618))。也正因为密钥长度对安全性的巨大影响,美国政府才不允许出口256位密钥的AES算法。
|
||||
|
||||
只有数百比特的密钥,到底该如何对任意长度的明文加密呢?主流对称算法会将原始明文分成等长的多组明文,再分别用密钥生成密文,最后把它们拼接在一起形成最终密文。而AES算法是按照128比特(16字节)对明文进行分组的(最后一组不足128位时会填充0或者随机数)。为了防止分组后密文出现明显的规律,造成攻击者容易根据概率破解出原文,我们就需要对每组的密钥做一些变换,**这种分组后变换密钥的算法就叫做分组密码工作模式(下文简称为分组模式),它是影响AES性能的另一个因素。**
|
||||
|
||||
[<img src="https://static001.geekbang.org/resource/image/46/d1/460d594465b9eb9a04426c6ee35da4d1.png" alt="" title="优秀的分组密码工作模式[br]更难以从密文中发现规律,图参见wiki">](https://zh.wikipedia.org/wiki/%E5%88%86%E7%BB%84%E5%AF%86%E7%A0%81%E5%B7%A5%E4%BD%9C%E6%A8%A1%E5%BC%8F)
|
||||
|
||||
比如,CBC分组模式中,只有第1组明文加密完成后,才能对第2组加密,因为第2组加密时会用到第1组生成的密文。因此,CBC必然无法并行计算。在材料科学出现瓶颈、单核频率不再提升的当下,CPU都在向多核方向发展,而CBC分组模式无法使用多核的并行计算能力,性能受到很大影响。**所以,通常我们应选择可以并行计算的GCM分组模式,这也是当下互联网中最常见的AES分组算法。**
|
||||
|
||||
由于AES算法中的替换法、行移位等流程对CPU指令并不友好,所以Intel在2008年推出了支持[AES-NI指令集](https://zh.wikipedia.org/wiki/AES%E6%8C%87%E4%BB%A4%E9%9B%86)的CPU,能够将AES算法的执行速度从每字节消耗28个时钟周期(参见[这里](https://www.cryptopp.com/benchmarks-p4.html)),降低至3.5个时钟周期(参见[这里](https://groups.google.com/forum/#!msg/cryptopp-users/5x-vu0KwFRk/CO8UIzwgiKYJ))。在Linux上你可以用下面这行命令查看CPU是否支持AES-NI指令集:
|
||||
|
||||
```
|
||||
# sort -u /proc/crypto | grep module |grep aes
|
||||
module : aesni_intel
|
||||
|
||||
```
|
||||
|
||||
**因此,如果CPU支持AES-NI特性,那么应选择AES算法,否则可以选择[CHACHA20](https://tools.ietf.org/html/rfc7539) 对称加密算法,它主要使用ARX操作(add-rotate-xor),CPU执行起来更快。**
|
||||
|
||||
说完对称加密算法的优化,我们再来看加密时的密钥是如何传递的。
|
||||
|
||||
## 如何更快地协商出密钥?
|
||||
|
||||
无论对称加密算法有多么安全,一旦密钥被泄露,信息安全就是一纸空谈。所以,TLS建立会话的第1个步骤是在握手阶段协商出密钥。
|
||||
|
||||
早期解决密钥传递的是[RSA](https://en.wikipedia.org/wiki/RSA_(cryptosystem)) 密钥协商算法。当你部署TLS证书到服务器上时,证书文件中包含一对公私钥(参见[非对称加密](https://zh.wikipedia.org/wiki/%E5%85%AC%E5%BC%80%E5%AF%86%E9%92%A5%E5%8A%A0%E5%AF%86)),其中,公钥会在握手阶段传递给客户端。在RSA密钥协商算法中,客户端会生成随机密钥(事实上是生成密钥的种子参数),并使用服务器的公钥加密后再传给服务器。根据非对称加密算法,公钥加密的消息仅能通过私钥解密,这样服务器解密后,双方就得到了相同的密钥,再用它加密应用消息。
|
||||
|
||||
**RSA密钥协商算法的最大问题是不支持前向保密**([Forward Secrecy](https://zh.wikipedia.org/wiki/%E5%89%8D%E5%90%91%E4%BF%9D%E5%AF%86)),一旦服务器的私钥泄露,过去被攻击者截获的所有TLS通讯密文都会被破解。解决前向保密的是[DH(Diffie–Hellman)密钥协商算法](https://zh.wikipedia.org/wiki/%E8%BF%AA%E8%8F%B2-%E8%B5%AB%E7%88%BE%E6%9B%BC%E5%AF%86%E9%91%B0%E4%BA%A4%E6%8F%9B)。
|
||||
|
||||
我们简单看下DH算法的工作流程。通讯双方各自独立生成随机的数字作为私钥,而后依据公开的算法计算出各自的公钥,并通过未加密的TLS握手发给对方。接着,根据对方的公钥和自己的私钥,双方各自独立运算后能够获得相同的数字,这就可以作为后续对称加密时使用的密钥。**即使攻击者截获到明文传递的公钥,查询到公开的DH计算公式后,在不知道私钥的情况下,也是无法计算出密钥的。**这样,DH算法就可以在握手阶段生成随机的新密钥,实现前向保密。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9f/1d/9f5ab0e7f64497c825a927782f58f31d.png" alt="">
|
||||
|
||||
DH算法的计算速度很慢,如上图所示,计算公钥以及最终的密钥时,需要做大量的乘法运算,而且为了保障安全性,这些数字的位数都很长。为了提升DH密钥交换算法的性能,诞生了当下广为使用的[ECDH密钥交换算法](https://zh.wikipedia.org/wiki/%E6%A9%A2%E5%9C%93%E6%9B%B2%E7%B7%9A%E8%BF%AA%E8%8F%B2-%E8%B5%AB%E7%88%BE%E6%9B%BC%E9%87%91%E9%91%B0%E4%BA%A4%E6%8F%9B),**ECDH在DH算法的基础上利用[ECC椭圆曲线](https://zh.wikipedia.org/wiki/%E6%A4%AD%E5%9C%86%E6%9B%B2%E7%BA%BF)特性,可以用更少的计算量计算出公钥以及最终的密钥。**
|
||||
|
||||
依据解析几何,椭圆曲线实际对应一个函数,而不同的曲线便有不同的函数表达式,目前不被任何已知专利覆盖的最快椭圆曲线是[X25519曲线](https://en.wikipedia.org/wiki/Curve25519),它的表达式是y<sup>2</sup> = x<sup>3</sup> + 486662x<sup>2</sup> + x。因此,当通讯双方协商使用X25519曲线用于ECDH算法时,只需要传递X25519这个字符串即可。在Nginx上,你可以使用ssl_ecdh_curve指令配置想使用的曲线:
|
||||
|
||||
```
|
||||
ssl_ecdh_curve X25519:secp384r1;
|
||||
|
||||
```
|
||||
|
||||
选择密钥协商算法是通过ssl_ciphers指令完成的:
|
||||
|
||||
```
|
||||
ssl_ciphers 'EECDH+ECDSA+AES128+SHA:RSA+AES128+SHA';
|
||||
|
||||
```
|
||||
|
||||
可见,ssl_ciphers可以同时配置对称加密算法及密钥强度等信息。注意,当ssl_prefer_server_ciphers设置为on时,ssl_ciphers指定的多个算法是有优先顺序的,**我们应当把性能最快且最安全的算法放在最前面。**
|
||||
|
||||
提升密钥协商速度的另一个思路,是减少密钥协商的次数,主要包括以下3种方式。
|
||||
|
||||
首先,最为简单有效的方式是在一个TLS会话中传输多组请求,对于HTTP协议而言就是使用长连接,在请求中加入Connection: keep-alive头部便可以做到。
|
||||
|
||||
其次,客户端与服务器在首次会话结束后缓存下session密钥,并用唯一的session ID作为标识。这样,下一次握手时,客户端只要把session ID传给服务器,且服务器在缓存中找到密钥后(为了提升安全性,缓存会定期失效),双方就可以加密通讯了。这种方式的问题在于,当N台服务器通过负载均衡提供TLS服务时,客户端命中上次访问过的服务器的概率只有1/N,所以大概率它们还得再次协商密钥。
|
||||
|
||||
session ticket方案可以解决上述问题,它把服务器缓存密钥,改为由服务器把密钥加密后作为ticket票据发给客户端,由客户端缓存密文。其中,集群中每台服务器对session加密的密钥必须相同,这样,客户端携带ticket密文访问任意一台服务器时,都能通过解密ticket,获取到密钥。
|
||||
|
||||
当然,使用session缓存或者session ticket既没有前向安全性,应对[重放攻击](https://en.wikipedia.org/wiki/Replay_attack)也更加困难。提升TLS握手性能的更好方式,是把TLS协议升级到1.3版本。
|
||||
|
||||
## 为什么应当尽快升级到TLS1.3?
|
||||
|
||||
TLS1.3(参见[RFC8446](https://tools.ietf.org/html/rfc8446))对性能的最大提升,在于它把TLS握手时间从2个RTT降为1个RTT。
|
||||
|
||||
在TLS1.2的握手中,先要通过Client Hello和Server Hello消息协商出后续使用的加密算法,再互相交换公钥并计算出最终密钥。**TLS1.3中把Hello消息和公钥交换合并为一步,这就减少了一半的握手时间**,如下图所示:
|
||||
|
||||
[<img src="https://static001.geekbang.org/resource/image/49/20/4924f22447eaf0cc443aac9b2d483020.png" alt="" title="TLS1.3相对TLS1.2,减少了1个RTT的握手时间[br]图片来自www.ssl2buy.com">](https://www.ssl2buy.com/wiki/tls-1-3-protocol-released-move-ahead-to-advanced-security-and-privacy)
|
||||
|
||||
那TLS1.3握手为什么只需要1个RTT就可以完成呢?因为TLS1.3支持的密钥协商算法大幅度减少了,这样,客户端尽可以把常用DH算法的公钥计算出来,并与协商加密算法的HELLO消息一起发送给服务器,服务器也作同样处理,这样仅用1个RTT就可以协商出密钥。
|
||||
|
||||
而且,TLS1.3仅支持目前最安全的几个算法,比如openssl中仅支持下面5种安全套件:
|
||||
|
||||
- TLS_AES_256_GCM_SHA384
|
||||
- TLS_CHACHA20_POLY1305_SHA256
|
||||
- TLS_AES_128_GCM_SHA256
|
||||
- TLS_AES_128_CCM_8_SHA256
|
||||
- TLS_AES_128_CCM_SHA256
|
||||
|
||||
相较起来,TLS1.2支持各种古老的算法,中间人可以利用[降级攻击](https://en.wikipedia.org/wiki/Downgrade_attack),在握手阶段把加密算法替换为不安全的算法,从而轻松地破解密文。如前文提到过的DES算法,由于密钥位数只有56位,很容易破解。
|
||||
|
||||
因此,**无论从性能还是安全角度上,你都应该尽快把TLS版本升级到1.3。**你可以用[这个网址](https://www.ssllabs.com/ssltest/index.html)测试当前站点是否支持TLS1.3。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a8/57/a816a361d7f47303cbfeb10035a96d57.png" alt="">
|
||||
|
||||
如果不支持,还可以参见[每日一课《TLS1.3原理及在Nginx上的应用》](https://time.geekbang.org/dailylesson/detail/100028440),升级Nginx到TLS1.3版本。
|
||||
|
||||
## 小结
|
||||
|
||||
这一讲,我们介绍了TLS协议的优化方法。
|
||||
|
||||
应用消息是通过对称加密算法编码的,而目前AES还是最安全的对称加密算法。不同的分组模式也会影响AES算法的性能,而GCM模式能够充分利用多核CPU的并行计算能力,所以AES_GCM是我们的首选。当你的CPU支持AES-NI指令集时,AES算法的执行会非常快,否则,可以考虑对CPU更友好的CHACHA20算法。
|
||||
|
||||
再来看对称加密算法的密钥是如何传递的,它决定着TLS系统的安全,也对HTTP小对象的传输速度有很大影响。DH密钥协商算法速度并不快,因此目前主要使用基于椭圆曲线的ECDH密钥协商算法,其中,不被任何专利覆盖的X25519椭圆曲线速度最快。为了减少密钥协商次数,我们应当尽量通过长连接来复用会话。在TLS1.2及早期版本中,session缓存和session ticket也能减少密钥协商时的计算量,但它们既没有前向安全性,也更难防御重放攻击,所以为了进一步提升性能,应当尽快升级到TLS1.3。
|
||||
|
||||
TLS1.3将握手时间从2个RTT降为1个RTT,而且它限制了目前已经不再安全的算法,这样中间人就难以用降级攻击来破解密钥。
|
||||
|
||||
密码学的演进越来越快,加密与破解总是在道高一尺、魔高一丈的交替循环中发展,当下安全的算法未必在一年后仍然安全。而且,当量子计算机真正诞生后,它强大的并行计算能力可以轻松地暴力破解当下还算安全的算法。然而,这种划时代的新技术出现时总会有一个时间窗口,而在窗口内也会涌现出能够防御住量子破解的新算法。所以,我们应时常关注密码学的进展,更换更安全、性能也更优秀的新算法。
|
||||
|
||||
## 思考题
|
||||
|
||||
最后,留给你一道思考题,TLS体系中还有许多性能优化点,比如在服务器上部署[OSCP Stapling](https://zh.wikipedia.org/wiki/OCSP%E8%A3%85%E8%AE%A2)(用于更快地发现过期证书)也可以提升网站的访问性能,你还用过哪些方式优化TLS的性能呢?欢迎你在留言区与我探讨。
|
||||
|
||||
感谢阅读,如果你觉得这节课对你有一些启发,也欢迎把它分享给你的朋友。
|
132
极客时间专栏/系统性能调优必知必会/应用层编解码优化/15 | 如何提升HTTP|1.1性能?.md
Normal file
132
极客时间专栏/系统性能调优必知必会/应用层编解码优化/15 | 如何提升HTTP|1.1性能?.md
Normal file
@@ -0,0 +1,132 @@
|
||||
<audio id="audio" title="15 | 如何提升HTTP/1.1性能?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/71/09/71766c707c12801cc505d62333674409.mp3"></audio>
|
||||
|
||||
你好,我是陶辉。
|
||||
|
||||
上一讲介绍了为应用层信息安全保驾护航的TLS/SSL协议,这一讲我们来看看最常用的应用层协议HTTP/1.1该如何优化。
|
||||
|
||||
由于门槛低、易监控、自表达等特点,HTTP/1.1在互联网诞生之初就成为最广泛使用的应用层协议。然而它的性能却很差,最为人诟病的是HTTP头部的传输占用了大量带宽。由于HTTP头部使用ASCII编码方式,这造成它往往达到几KB,而且滥用的Cookie头部进一步增大了体积。与此同时,REST架构的无状态特性还要求每个请求都得重传HTTP头部,这就使消息的有效信息比重难以提高。
|
||||
|
||||
你可能听说过诸如缓存、长连接、图片拼接、资源压缩等优化HTTP协议性能的方式,这些优化方案有些从底层的传输层优化入手,有些从用户使用浏览器的体验入手,有些则从服务器资源的编码入手,五花八门,导致我们没有系统化地优化思路,往往在性能提升上难尽全功。
|
||||
|
||||
那么,如何全面地提升HTTP/1.1协议的性能呢?我认为在不升级协议的情况下,有3种优化思路:首先是通过缓存避免发送HTTP请求;其次,如果不得不发起请求,那么就得思考如何才能减少请求的个数;最后则是减少服务器响应的体积。
|
||||
|
||||
接下来,我们就沿着这3个思路看看具体的优化方法。
|
||||
|
||||
## 通过缓存避免发送HTTP请求
|
||||
|
||||
如果不走网络就能获得HTTP响应,这样性能肯定最高。HTTP协议设计之初就考虑到了这一点,缓存能够让客户端在免于发送HTTP请求的情况下获得服务器的资源。
|
||||
|
||||
缓存到底是如何做到的呢?其实很简单,它从时间维度上做文章,把第1份请求及其响应保存在客户端的本地磁盘上,其中请求的URL作为关键字(部分HTTP头部也会加入关键字,例如确定服务器域名的Host头部),而响应就是值。这样,后续发起相同的请求时,就可以先在本地磁盘上通过关键字查找,如果找到了,就直接将缓存作为服务器响应使用。读取本地磁盘耗时不过几十毫秒,这远比慢了上百倍且不稳定的网络请求快得多。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9d/ab/9dea133d832d8b7ab642bb74b48502ab.png" alt="">
|
||||
|
||||
你可能会问,服务器上的资源更新后,客户端并不知道,它若再使用过期的缓存就会出错,这该如何解决?因此,服务器会在发送响应时预估一个过期时间并在响应头部中告诉客户端,而客户端一旦发现缓存过期则重新发起网络请求。HTTP协议控制缓存过期的头部非常多,而且通常这是在服务器端设置的,我会在本专栏的第4部分“分布式系统优化”结合服务器操作再来介绍,这里暂时略过。
|
||||
|
||||
当然,过期的缓存也仍然可以提升性能,如下图所示,当客户端发现缓存过期后,会取出缓存的摘要(摘要是从第1次请求的响应中拿到的),把它放在请求的Etag头部中再发给服务器。而服务器获取到请求后,会将本地资源的摘要与请求中的Etag相比较,如果不同,那么缓存没有价值,重新发送最新资源即可;如果摘要与Etag相同,那么仅返回不含有包体的304 Not Modified响应,告知客户端缓存仍然有效即可,这就省去传递可能高达千百兆的文件资源。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a3/62/a394b7389d0cdb5f0866223681e19b62.png" alt="">
|
||||
|
||||
至于Etag摘要究竟是怎样生成的,各类Web服务器的处理方式不尽相同,比如Nginx会将文件大小和修改时间拼接为一个字符串作为文件摘要(详见[《Nginx核心知识100讲》第97课](https://time.geekbang.org/course/detail/138-76358))。过期缓存在分布式系统中可以有效提升可用性,[第25课] 还会站在反向代理的角度介绍过期缓存的用法。
|
||||
|
||||
浏览器上的缓存只能为一个用户使用,故称为私有缓存。代理服务器上的缓存可以被许多用户使用,所以称为共享缓存。可见,共享缓存带来的性能收益被庞大的客户端群体放大了。你可以看到在下面的REST架构图中,表示缓存的$符号(缓存的英文名称是cache,由于它的发音与cash很像,所以许多英文文档中用美元符号来表示缓存)既存在于User Agent浏览器中,也存在于Proxy正向代理服务器和Gateway反向代理上。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b6/ff/b6950aa62483c56438c41bc4b2f43bff.png" alt="" title="图片来自网络">
|
||||
|
||||
可见,缓存与互联网世界的网络效率密切相关,用好缓存是提升HTTP性能最重要的手段。
|
||||
|
||||
## 如何降低HTTP请求的次数?
|
||||
|
||||
如果不得不发起请求,就应该尽量减少HTTP请求的次数,这可以从减少重定向次数、合并请求、延迟发送请求等3个方面入手。
|
||||
|
||||
首先来看看什么是重定向请求,一个资源由于迁移、维护等原因从url1移至url2后,在客户端访问原先的url1时,服务器不能简单粗暴地返回错误,而是通过302响应码及Location头部,告诉客户端资源已经改到url2了,而客户端再用url2发起新的HTTP请求。
|
||||
|
||||
可见,重定向增加了请求的数量。尤其客户端在公网中时,由于公网速度慢、成本高、路径长、不稳定,而且为了信息安全性还要用TLS协议加密,这些都降低了网络性能。从上面的REST架构图可以看到,HTTP请求会经过多个代理服务器,如果将重定向工作交由代理服务器完成,就能减少网络消耗,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0d/2a/0d1701737956ce65f3d5ec8fa8009f2a.png" alt="">
|
||||
|
||||
更进一步,客户端还可以缓存重定向响应。RFC规范定义了5个重定向响应码(如下表所示),其中客户端接收到301和308后都可以将重定向响应缓存至本地,之后客户端会自动用url2替代url1访问网络资源。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/85/70/85b55f50434fc1acd2ead603e5c57870.jpg" alt="">
|
||||
|
||||
其次我们来看如何合并请求。当多个访问小文件的请求被合并为一个访问大文件的请求时,这样虽然传输的总资源体积未变,但减少请求就意味着减少了重复发送的HTTP头部,同时也减少了TCP连接的数量,因而省去了TCP握手和慢启动过程消耗的时间(参见第12课)。我们具体看几种合并请求的方式。
|
||||
|
||||
一些WEB页面往往含有几十、上百个小图片,用[CSS Image Sprites技术](https://www.tutorialrepublic.com/css-tutorial/css-sprites.php)可以将它们合成一张大图片,而浏览器获得后可以根据CSS数据把它切割还原为多张小图片,这可以大幅减少网络消耗。
|
||||
|
||||
[<img src="https://static001.geekbang.org/resource/image/e4/ed/e4a760a95542701d99b8a38ccddaebed.png" alt="" title="图片来自[黑染枫林的CSDN博客]">](https://blog.csdn.net/weixin_38055381/article/details/81504716)
|
||||
|
||||
类似地,在服务器端用webpack等打包工具将Javascript、CSS等资源合并为大文件,也能起到同样的效果。
|
||||
|
||||
除此以外,还可以将多媒体资源用base64编码后,以URL的方式嵌入HTML文件中,以减少小请求的个数(参见[RFC2397](https://tools.ietf.org/html/rfc2397))。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/27/fb/27aa119e0104f41a6718987cdf71c8fb.png" alt="">
|
||||
|
||||
用Chrome浏览器开发者工具的Network面板可以轻松地判断各站点是否使用了这种技术。关于Network面板的用法,我们在此就不赘述了(可参考[《Web协议详解与抓包实战》第9课](https://time.geekbang.org/course/detail/175-93594),那里有详细的介绍)。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ab/74/ab75920b0c43e4f7fcbc5933005afb74.png" alt="">
|
||||
|
||||
当然,这种合并请求的方式也会带来一个新问题,即,当其中一个资源发生变化后,客户端必须重新下载完整的大文件,这显然会带来额外的网络消耗。在落后的HTTP/1.1协议中,合并请求还算一个不错的解决方案,在下一讲将介绍的HTTP/2出现后,这种技术就没有用武之地了。
|
||||
|
||||
最后,我们还可以从浏览页面的体验角度上,减少HTTP请求的次数。比如,有些HTML页面上引用的资源,其实在当前页面上用不上,而是供后续页面使用的,这就可以使用懒加载([lazy loading](https://zh.wikipedia.org/wiki/%E6%83%B0%E6%80%A7%E8%BC%89%E5%85%A5)) 技术延迟发起请求。
|
||||
|
||||
当不得不发起请求时,还可以从服务器的角度通过减少响应包体的体积来提升性能。
|
||||
|
||||
## 如何重新编码以减少响应的大小?
|
||||
|
||||
减少资源体积的唯一方式是对资源重新编码压缩,其中又分为[无损压缩](https://zh.wikipedia.org/wiki/%E6%97%A0%E6%8D%9F%E6%95%B0%E6%8D%AE%E5%8E%8B%E7%BC%A9)与[有损压缩](https://zh.wikipedia.org/wiki/%E6%9C%89%E6%8D%9F%E6%95%B0%E6%8D%AE%E5%8E%8B%E7%BC%A9)两种。
|
||||
|
||||
先来看无损压缩,这是指压缩后不会损失任何信息,可以完全恢复到压缩前的原样。因此,文本文件、二进制可执行文件都会使用这类压缩方法。
|
||||
|
||||
源代码也是文本文件,但它有自身的语法规则,所以可以依据语法先做一轮压缩。比如[jQuery](https://jquery.com/download/) 是用javascript语言编写的库,而标准版的jQuery.js为了帮助程序员阅读含有许多空格、回车等符号,但机器解释执行时并不需要这些符号。因此,根据语法规则将这些多余的符号去除掉,就可以将jQuery文件的体积压缩到原先的三分之一。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/de/ba/de0fc2a64a100bc83fc48eb2606fedba.png" alt="">
|
||||
|
||||
接着可以基于信息熵原理进行通用的无损压缩,这需要对原文建立统计模型,将出现频率高的数据用较短的二进制比特序列表示,而将出现频率低的数据用较长的比特序列表示。我们最常见的Huffman算法就是一种执行速度较快的实践,在下一讲的HTTP/2协议中还会用到它。在上图中可以看到,最小版的jQuery文件经过Huffman等算法压缩后,体积还会再缩小三分之二。
|
||||
|
||||
支持无损压缩算法的客户端会在请求中通过Accept-Encoding头部明确地告诉服务器:
|
||||
|
||||
```
|
||||
Accept-Encoding: gzip, deflate, br
|
||||
|
||||
```
|
||||
|
||||
而服务器也会在响应的头部中,告诉客户端包体中的资源使用了何种压缩算法(Nginx开启资源压缩的方式参见《Nginx核心知识100讲》[第131课](https://time.geekbang.org/course/detail/138-79618)和[第134课](https://time.geekbang.org/course/detail/138-79621)):
|
||||
|
||||
```
|
||||
content-encoding: gzip
|
||||
|
||||
```
|
||||
|
||||
虽然目前gzip使用最为广泛,但它的压缩效率以及执行速度其实都很一般,Google于2015年推出的[Brotli](https://zh.wikipedia.org/wiki/Brotli) 算法在这两方面表现都更优秀(也就是上文中的br),其对比数据如下:
|
||||
|
||||
[<img src="https://static001.geekbang.org/resource/image/62/cb/62e01433ad8ef23ab698e7c47b7cc8cb.png" alt="" title="图片来源:[https://quixdb.github.io/squash-benchmark/]">](https://quixdb.github.io/squash-benchmark/)
|
||||
|
||||
再来看有损压缩,它通过牺牲质量来提高压缩比,主要针对的是图片和音视频。HTTP请求可以通过Accept头部中的q质量因子(参见[RFC7231](https://tools.ietf.org/html/rfc7231#section-5.3.2)),告诉服务器期望的资源质量:
|
||||
|
||||
```
|
||||
Accept: audio/*; q=0.2, audio/basic
|
||||
|
||||
```
|
||||
|
||||
先来看图片的压缩。目前压缩比较高的开源算法是Google在2010年推出的[WebP格式](https://zh.wikipedia.org/wiki/WebP),你可以在[这个页面](https://isparta.github.io/compare-webp/index.html#12345)看到它与png格式图片的对比图。对于大量使用图片的网站,使用它代替传统格式会有显著的性能提升。
|
||||
|
||||
动态的音视频压缩比要比表态的图片高很多!由于音视频数据有时序关系,且时间连续的帧之间变化很小,因此可以在静态的关键帧之后,使用增量数据表达后续的帧,因此在质量略有损失的情况下,音频体积可以压缩到原先的几十分之一,视频体积则可以压缩到几百分之一,比图片的压缩比要高得多。因此,对音视频做有损压缩,能够大幅提升网络传输的性能。
|
||||
|
||||
对响应资源做压缩不只用于HTTP/1.1协议,事实上它对任何信息传输场景都有效,消耗一些CPU计算力在事前或者事中做压缩,通常会给性能带来不错的提升。
|
||||
|
||||
## 小结
|
||||
|
||||
这一讲我们从三个方面介绍了HTTP/1.1协议的优化策略。
|
||||
|
||||
首先,客户端缓存响应,可以在有效期内避免发起HTTP请求。即使缓存过期后,如果服务器端资源未改变,仍然可以通过304响应避免发送包体资源。浏览器上的私有缓存、服务器上的共享缓存,都对HTTP协议的性能提升有很大意义。
|
||||
|
||||
其次是降低请求的数量,如将原本由客户端处理的重定向请求,移至代理服务器处理可以减少重定向请求的数量。或者从体验角度,使用懒加载技术延迟加载部分资源,也可以减少请求数量。再比如,将多个文件合并后再传输,能够少传许多HTTP头部,而且减少TCP连接数量后也省去握手和慢启动的消耗。当然合并文件的副作用是小文件的更新,会导致整个合并后的大文件重传。
|
||||
|
||||
最后可以通过压缩响应来降低传输的字节数,选择更优秀的压缩算法能够有效降低传输量,比如用Brotli无损压缩算法替换gzip,或者用WebP格式替换png等格式图片等。
|
||||
|
||||
但其实在HTTP/1.1协议上做优化效果总是有限的,下一讲我们还将介绍在URL、头部等高层语法上向前兼容的HTTP/2协议,它在性能上有大幅度提升,是如gRPC等应用层协议的基础。
|
||||
|
||||
## 思考题
|
||||
|
||||
除了我今天介绍的方法以外,使用KeepAlive长连接替换短连接也能提升性能,你还知道有哪些提升HTTP/1.1性能的方法吗?欢迎你在留言区与我一起探讨。
|
||||
|
||||
感谢阅读,如果你觉得这节课对你有一些启发,也欢迎把它分享给你的朋友。
|
133
极客时间专栏/系统性能调优必知必会/应用层编解码优化/16 | HTTP|2是怎样提升性能的?.md
Normal file
133
极客时间专栏/系统性能调优必知必会/应用层编解码优化/16 | HTTP|2是怎样提升性能的?.md
Normal file
@@ -0,0 +1,133 @@
|
||||
<audio id="audio" title="16 | HTTP/2是怎样提升性能的?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ef/fb/ef86354bab99bd133610c355793d2efb.mp3"></audio>
|
||||
|
||||
你好,我是陶辉。
|
||||
|
||||
上一讲我们从多个角度优化HTTP/1的性能,但获得的收益都较为有限,而直接将其升级到兼容HTTP/1的HTTP/2协议,性能会获得非常大的提升。
|
||||
|
||||
HTTP/2协议既降低了传输时延也提升了并发性,已经被主流站点广泛使用。多数HTTP头部都可以被压缩90%以上的体积,这节约了带宽也提升了用户体验,像Google的高性能协议gRPC也是基于HTTP/2协议实现的。
|
||||
|
||||
目前常用的Web中间件都已支持HTTP/2协议,然而如果你不清楚它的原理,对于Nginx、Tomcat等中间件新增的流、推送、消息优先级等HTTP/2配置项,你就不知是否需要调整。
|
||||
|
||||
同时,许多新协议都会参考HTTP/2优秀的设计,如果你不清楚HTTP/2的性能究竟高在哪里,也就很难对当下其他应用层协议触类旁通。而且,HTTP/2协议也并不是毫无缺点,到2020年3月时它的替代协议[HTTP/3](https://zh.wikipedia.org/wiki/HTTP/3) 已经经历了[27个草案](https://tools.ietf.org/html/draft-ietf-quic-http-27),推出在即。HTTP/3的目标是优化传输层协议,它会保留HTTP/2协议在应用层上的优秀设计。如果你不懂HTTP/2,也就很难学会未来的HTTP/3协议。
|
||||
|
||||
所以,这一讲我们就将介绍HTTP/2对HTTP/1.1协议都做了哪些改进,从消息的编码、传输等角度说清楚性能提升点,这样,你就能理解支持HTTP/2的中间件为什么会提供那些参数,以及如何权衡HTTP/2带来的收益与付出的升级成本。
|
||||
|
||||
## 静态表编码能节约多少带宽?
|
||||
|
||||
HTTP/1.1协议最为人诟病的是ASCII头部编码效率太低,浪费了大量带宽。HTTP/2使用了静态表、动态表两种编码技术(合称为HPACK),极大地降低了HTTP头部的体积,搞清楚编码流程,你自然就会清楚服务器提供的http2_max_requests等配置参数的意义。
|
||||
|
||||
我们以一个具体的例子来观察编码流程。每一个HTTP/1.1请求都会有Host头部,它指示了站点的域名,比如:
|
||||
|
||||
```
|
||||
Host: test.taohui.tech\r\n
|
||||
|
||||
```
|
||||
|
||||
算上冒号空格以及结尾的\r\n,它占用了24字节。**使用静态表及Huffman编码,可以将它压缩为13字节,也就是节约了46%的带宽!**这是如何做到的呢?
|
||||
|
||||
我用Chrome访问站点test.taohui.tech,并用Wireshark工具抓包(关于如何用Wireshark抓HTTP/2协议的报文,如果你还不太清楚,可参见[《Web协议详解与抓包实战》第51课](https://time.geekbang.org/course/detail/175-104932))后,下图高亮的头部就是第1个请求的Host头部,其中每8个蓝色的二进制位是1个字节,报文中用了13个字节表示Host头部。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/09/1f/097e7f4549eb761c96b61368c416981f.png" alt="">
|
||||
|
||||
HTTP/2能够用13个字节编码原先的24个字节,是依赖下面这3个技术。
|
||||
|
||||
首先基于二进制编码,就不需要冒号、空格和\r\n作为分隔符,转而用表示长度的1个字节来分隔即可。比如,上图中的01000001就表示Host,而10001011及随后的11个字节表示域名。
|
||||
|
||||
其次,使用静态表来描述Host头部。什么是静态表呢?HTTP/2将61个高频出现的头部,比如描述浏览器的User-Agent、GET或POST方法、返回的200 SUCCESS响应等,分别对应1个数字再构造出1张表,并写入HTTP/2客户端与服务器的代码中。由于它不会变化,所以也称为静态表。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5c/98/5c180e1119c1c0eb66df03a9c10c5398.png" alt="">
|
||||
|
||||
这样收到01000001时,根据[RFC7541](https://tools.ietf.org/html/rfc7541) 规范,前2位为01时,表示这是不包含Value的静态表头部:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/cd/37/cdf16023ab2c2f4f67f0039b8da47837.png" alt="">
|
||||
|
||||
再根据索引000001查到authority头部(Host头部在HTTP/2协议中被改名为authority)。紧跟的字节表示域名,其中首个比特位表示域名是否经过Huffman编码,而后7位表示了域名的长度。在本例中,10001011表示域名共有11个字节(8+2+1=11),且使用了Huffman编码。
|
||||
|
||||
最后,使用静态Huffman编码,可以将16个字节的test.taohui.tech压缩为11个字节,这是怎么做到的呢?根据信息论,高频出现的信息用较短的编码表示后,可以压缩体积。因此,在统计互联网上传输的大量HTTP头部后,HTTP/2依据统计频率将ASCII码重新编码为一张表,参见[这里](https://tools.ietf.org/html/rfc7541#page-27)。test.taohui.tech域名用到了10个字符,我把这10个字符的编码列在下表中。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/81/de/81d2301553c825a466b1f709924ba6de.jpg" alt="">
|
||||
|
||||
这样,接收端在收到下面这串比特位(最后3位填1补位)后,通过查表(请注意每个字符的颜色与比特位是一一对应的)就可以快速解码为:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/57/50/5707f3690f91fe54045f4d8154fe4e50.jpg" alt="">
|
||||
|
||||
由于8位的ASCII码最小压缩为5位,所以静态Huffman的最大压缩比只有5/8。关于Huffman编码是如何构造的,你可以参见[每日一课《HTTP/2 能带来哪些性能提升?》](https://time.geekbang.org/dailylesson/detail/100028441)。
|
||||
|
||||
## 动态表编码能节约多少带宽?
|
||||
|
||||
虽然静态表已经将24字节的Host头部压缩到13字节,**但动态表可以将它压缩到仅1字节,这就能节省96%的带宽!**那动态表是怎么做到的呢?
|
||||
|
||||
你可能注意到,当下许多页面含有上百个对象,而REST架构的无状态特性,要求下载每个对象时都得携带完整的HTTP头部。如果HTTP/2能在一个连接上传输所有对象,那么只要客户端与服务器按照同样的规则,对首次出现的HTTP头部用一个数字标识,随后再传输它时只传递数字即可,这就可以实现几十倍的压缩率。所有被缓存的头部及其标识数字会构成一张表,它与已经传输过的请求有关,是动态变化的,因此被称为动态表。
|
||||
|
||||
静态表有61项,所以动态表的索引会从62起步。比如下图中的报文中,访问test.taohui.tech的第1个请求有13个头部需要加入动态表。其中,Host: test.taohui.tech被分配到的动态表索引是74(索引号是倒着分配的)。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/69/e0/692a5fad16d6acc9746e57b69b4f07e0.png" alt="">
|
||||
|
||||
这样,后续请求使用到Host头部时,只需传输1个字节11001010即可。其中,首位1表示它在动态表中,而后7位1001010值为64+8+2=74,指向服务器缓存的动态表第74项:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9f/31/9fe864459705513bc361cee5eafd3431.png" alt="">
|
||||
|
||||
静态表、Huffman编码、动态表共同完成了HTTP/2头部的编码,其中,前两者可以将体积压缩近一半,而后者可以将反复传输的头部压缩95%以上的体积!
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c0/0c/c08db9cb2c55cb05293c273b8812020c.png" alt="">
|
||||
|
||||
那么,是否要让一条连接传输尽量多的请求呢?并不是这样。动态表会占用很多内存,影响进程的并发能力,所以服务器都会提供类似http2_max_requests这样的配置,限制一个连接上能够传输的请求数量,通过关闭HTTP/2连接来释放内存。**因此,http2_max_requests并不是越大越好,通常我们应当根据用户浏览页面时访问的对象数量来设定这个值。**
|
||||
|
||||
## 如何并发传输请求?
|
||||
|
||||
HTTP/1.1中的KeepAlive长连接虽然可以传输很多请求,但它的吞吐量很低,因为在发出请求等待响应的那段时间里,这个长连接不能做任何事!而HTTP/2通过Stream这一设计,允许请求并发传输。因此,HTTP/1.1时代Chrome通过6个连接访问页面的速度,远远比不上HTTP/2单连接的速度,具体测试结果你可以参考这个[页面](https://http2.akamai.com/demo)。
|
||||
|
||||
为了理解HTTP/2的并发是怎样实现的,你需要了解Stream、Message、Frame这3个概念。HTTP请求和响应都被称为Message消息,它由HTTP头部和包体构成,承载这二者的叫做Frame帧,它是HTTP/2中的最小实体。Frame的长度是受限的,比如Nginx中默认限制为8K(http2_chunk_size配置),因此我们可以得出2个结论:HTTP消息可以由多个Frame构成,以及1个Frame可以由多个TCP报文构成(TCP MSS通常小于1.5K)。
|
||||
|
||||
再来看Stream流,它与HTTP/1.1中的TCP连接非常相似,当Stream作为短连接时,传输完一个请求和响应后就会关闭;当它作为长连接存在时,多个请求之间必须串行传输。在HTTP/2连接上,理论上可以同时运行无数个Stream,这就是HTTP/2的多路复用能力,它通过Stream实现了请求的并发传输。
|
||||
|
||||
[<img src="https://static001.geekbang.org/resource/image/b0/c8/b01f470d5d03082159e62a896b9376c8.png" alt="" title="图片来源:https://developers.google.com/web/fundamentals/performance/http2">](https://developers.google.com/web/fundamentals/performance/http2)
|
||||
|
||||
虽然RFC规范并没有限制并发Stream的数量,但服务器通常都会作出限制,比如Nginx就默认限制并发Stream为128个(http2_max_concurrent_streams配置),以防止并发Stream消耗过多的内存,影响了服务器处理其他连接的能力。
|
||||
|
||||
HTTP/2的并发性能比HTTP/1.1通过TCP连接实现并发要高。这是因为,**当HTTP/2实现100个并发Stream时,只经历1次TCP握手、1次TCP慢启动以及1次TLS握手,但100个TCP连接会把上述3个过程都放大100倍!**
|
||||
|
||||
HTTP/2还可以为每个Stream配置1到256的权重,权重越高服务器就会为Stream分配更多的内存、流量,这样按照资源渲染的优先级为并发Stream设置权重后,就可以让用户获得更好的体验。而且,Stream间还可以有依赖关系,比如若资源A、B依赖资源C,那么设置传输A、B的Stream依赖传输C的Stream即可,如下图所示:
|
||||
|
||||
[<img src="https://static001.geekbang.org/resource/image/9c/97/9c068895a9d2dc66810066096172a397.png" alt="" title="图片来源:https://developers.google.com/web/fundamentals/performance/http2">](https://developers.google.com/web/fundamentals/performance/http2)
|
||||
|
||||
## 服务器如何主动推送资源?
|
||||
|
||||
HTTP/1.1不支持服务器主动推送消息,因此当客户端需要获取通知时,只能通过定时器不断地拉取消息。HTTP/2的消息推送结束了无效率的定时拉取,节约了大量带宽和服务器资源。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f0/16/f0dc7a3bfc5709adc434ddafe3649316.png" alt="">
|
||||
|
||||
HTTP/2的推送是这么实现的。首先,所有客户端发起的请求,必须使用单号Stream承载;其次,所有服务器进行的推送,必须使用双号Stream承载;最后,服务器推送消息时,会通过PUSH_PROMISE帧传输HTTP头部,并通过Promised Stream ID告知客户端,接下来会在哪个双号Stream中发送包体。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a1/62/a1685cc8e24868831f5f2dd961ad3462.png" alt="">
|
||||
|
||||
在SDK中调用相应的API即可推送消息,而在Web资源服务器中可以通过配置文件做简单的资源推送。比如在Nginx中,如果你希望客户端访问/a.js时,服务器直接推送/b.js,那么可以这么配置:
|
||||
|
||||
```
|
||||
location /a.js {
|
||||
http2_push /b.js;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
服务器同样也会控制并发推送的Stream数量(如http2_max_concurrent_pushes配置),以减少动态表对内存的占用。
|
||||
|
||||
## 小结
|
||||
|
||||
这一讲我们介绍了HTTP/2的高性能是如何实现的。
|
||||
|
||||
静态表和Huffman编码可以将HTTP头部压缩近一半的体积,但这只是连接上第1个请求的压缩比。后续请求头部通过动态表可以压缩90%以上,这大大提升了编码效率。当然,动态表也会导致内存占用过大,影响服务器的总体并发能力,因此服务器会限制HTTP/2连接的使用时长。
|
||||
|
||||
HTTP/2的另一个优势是实现了Stream并发,这节约了TCP和TLS协议的握手时间,并减少了TCP的慢启动阶段对流量的影响。同时,Stream之间可以用Weight权重调节优先级,还可以直接设置Stream间的依赖关系,这样接收端就可以获得更优秀的体验。
|
||||
|
||||
HTTP/2支持消息推送,从HTTP/1.1的拉模式到推模式,信息传输效率有了巨大的提升。HTTP/2推消息时,会使用PUSH_PROMISE帧传输头部,并用双号的Stream来传递包体,了解这一点对定位复杂的网络问题很有帮助。
|
||||
|
||||
HTTP/2的最大问题来自于它下层的TCP协议。由于TCP是字符流协议,在前1字符未到达时,后接收到的字符只能存放在内核的缓冲区里,即使它们是并发的Stream,应用层的HTTP/2协议也无法收到失序的报文,这就叫做队头阻塞问题。解决方案是放弃TCP协议,转而使用UDP协议作为传输层协议,这就是HTTP/3协议的由来。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/38/d1/3862dad08cecc75ca6702c593a3c9ad1.png" alt="">
|
||||
|
||||
## 思考题
|
||||
|
||||
最后,留给你一道思考题。为什么HTTP/2要用静态Huffman查表法对字符串编码,基于连接上的历史数据统计信息做动态Huffman编码不是更有效率吗?欢迎你在留言区与我一起探讨。
|
||||
|
||||
感谢阅读,如果你觉得这节课对你有一些启发,也欢迎把它分享给你的朋友。
|
110
极客时间专栏/系统性能调优必知必会/应用层编解码优化/17 | Protobuf是如何进一步提高编码效率的?.md
Normal file
110
极客时间专栏/系统性能调优必知必会/应用层编解码优化/17 | Protobuf是如何进一步提高编码效率的?.md
Normal file
@@ -0,0 +1,110 @@
|
||||
<audio id="audio" title="17 | Protobuf是如何进一步提高编码效率的?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/8e/20/8e66aeee4819a94f8edbf01c09c26320.mp3"></audio>
|
||||
|
||||
你好,我是陶辉。
|
||||
|
||||
上一讲介绍的HTTP/2协议在编码上拥有非常高的空间利用率,这一讲我们看看,相比其中的HPACK编码技术,Protobuf又是通过哪些新招式进一步提升编码效率的。
|
||||
|
||||
Google在2008年推出的Protobuf,是一个针对具体编程语言的编解码工具。它面向Windows、Linux等多种平台,也支持Java、Python、Golang、C++、Javascript等多种面向对象编程语言。使用Protobuf编码消息速度很快,消耗的CPU计算力也不多,而且编码后的字符流体积远远小于JSON等格式,能够大量节约昂贵的带宽,因此gRPC也把Protobuf作为底层的编解码协议。
|
||||
|
||||
然而,很多同学并不清楚Protobuf到底是怎样做到这一点的。这样,当你希望通过更换通讯协议这个高成本手段,提升整个分布式系统的性能时,面对可供选择的众多通讯协议,仅凭第三方的性能测试报告,你仍将难以作出抉择。
|
||||
|
||||
而且,面对分布式系统中的疑难杂症,往往需要通过分析抓取到的网络报文,确定到底是哪个组件出现了问题。可是由于Protobuf编码太过紧凑,即使对照着Proto消息格式文件,在不清楚编码逻辑时,你也很难解析出消息内容。
|
||||
|
||||
下面,我们将基于上一讲介绍过的HPACK编码技术,看看Protobuf是怎样进一步缩减编码体积的。
|
||||
|
||||
## 怎样用最少的空间编码字段名?
|
||||
|
||||
消息由多个名、值对组成,比如HTTP请求中,头部Host: www.taohui.pub就是一个名值对,其中,Host是字段名称,而www.taohui.pub是字段值。我们先来看Protobuf如何编码字段名。
|
||||
|
||||
对于多达几十字节的HTTP头部,HTTP/2静态表仅用一个数字来表示,其中,映射数字与字符串对应关系的表格,被写死在HTTP/2实现框架中。这样的编码效率非常高,**但通用的HTTP/2框架只能将61个最常用的HTTP头部映射为数字,它能发挥出的作用很有限。**
|
||||
|
||||
动态表可以让更多的HTTP头部编码为数字,在上一讲的例子中,动态表将Host头部减少了96%的体积,效果惊人。但动态表生效得有一个前提:必须在一个会话连接上反复传输完全相同的HTTP头部。**如果消息字段在1个连接上只发送了1次,或者反复传输时字段总是略有变动,动态表就无能为力了。**
|
||||
|
||||
有没有办法既使用静态表的预定义映射关系,又享受到动态表的灵活多变呢?**其实只要把由HTTP/2框架实现的字段名映射关系,交由应用程序自行完成即可。**而Protobuf就是这么做的。比如下面这段39字节的JSON消息,虽然一目了然,但字段名name、id、sex其实都是多余的,因为客户端与服务器的处理代码都清楚字段的含义。
|
||||
|
||||
```
|
||||
{"name":"John","id":1234,"sex":"MALE"}
|
||||
|
||||
```
|
||||
|
||||
Protobuf将这3个字段名预分配了3个数字,定义在proto文件中:
|
||||
|
||||
```
|
||||
message Person {
|
||||
string name = 1;
|
||||
uint32 id = 2;
|
||||
|
||||
enum SexType {
|
||||
MALE = 0;
|
||||
FEMALE = 1;
|
||||
}
|
||||
SexType sex = 3;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
接着,通过protoc程序便可以针对不同平台、编程语言,将它生成编解码类,最后通过类中自动生成的SerializeToString方法将消息序列化,编码后的信息仅有11个字节。其中,报文与字段的对应关系我放在下面这张图中。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/12/b4/12907732b38fd0c0f41330985bb02ab4.png" alt="">
|
||||
|
||||
从图中可以看出,Protobuf是按照字段名、值类型、字段值的顺序来编码的,由于编码极为紧凑,所以分析时必须基于二进制比特位进行。比如红色的00001、00010、00011等前5个比特位,就分别代表着name、id、sex字段。
|
||||
|
||||
图中字段值的编码方式我们后面再解释,这里想必大家会有疑问,如果只有5个比特位表示字段名的值,那不是限制消息最多只有31个(2<sup>5</sup> - 1)字段吗?当然不是,字段名的序号可以从1到536870911(即2<sup>29</sup> - 1),可是,多数消息不过只有几个字段,这意味着可以用很小的序号表示它们。因此,对于小于16的序号,Protobuf仅有5个比特位表示,这样加上3位值类型,只需要1个字节表示字段名。对于大于16小于2027的序号,也只需要2个字节表示。
|
||||
|
||||
Protobuf可以用1到5个字节来表示一个字段名,因此,每个字节的第1个比特位保留,它为0时表示这是字段名的最后一个字节。下表列出了几种典型序号的编码值(请把黑色的二进制位,从右至左排列,比如2049应为000100000000001,即2048+1)。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/43/33/43983f7fcba1d26eeea952dc0934d833.jpg" alt="">
|
||||
|
||||
说完字段名,我们再来看字段值是如何编码的。
|
||||
|
||||
## 怎样高效地编码字段值?
|
||||
|
||||
Protobuf对不同类型的值,采用6种不同的编码方式,如下表所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b2/67/b20120a8bac33d985275b5a2768ad067.jpg" alt="">
|
||||
|
||||
字符串用Length-delimited方式编码,顾名思义,在值长度后顺序添加ASCII字节码即可。比如上文例子中的John,对应的ASCII码如下表所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9f/cb/9f472ea914f98a81c03a7ad309f687cb.jpg" alt="">
|
||||
|
||||
这样,"John"需要5个字节进行编码,如下图所示(绿色表示长度,紫色表示ASCII码):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6e/ae/6e45b5c7bb5e8766f6baef8c0e8b7bae.png" alt="">
|
||||
|
||||
这里需要注意,字符串长度的编码逻辑与字段名相同,当长度小于128(2<sup>7</sup>)时,1个字节就可以表示长度。若长度从128到16384(2<sup>14</sup>),则需要2个字节,以此类推。
|
||||
|
||||
由于字符串编码时未做压缩,所以并不会节约空间,但胜在速度快。**如果你的消息中含有大量字符串,那么使用Huffman等算法压缩后再编码效果更好。**
|
||||
|
||||
我们再来看id:1234这个数字是如何编码的。其实Protobuf中所有数字的编码规则是一致的,字节中第1个比特位仅用于指示由哪些字节编码1个数字。例如图中的1234,将由14个比特位00010011010010表示(1024+128+64+16+2,正好是1234)。
|
||||
|
||||
**由于消息中的大量数字都很小,这种编码方式可以带来很高的空间利用率!**当然,如果你确定数字很大,这种编码方式不但不能节约空间,而且会导致原先4个字节的大整数需要用5个字节来表示时,你也可以使用fixed32、fixed64等类型定义数字。
|
||||
|
||||
Protobuf还可以通过enum枚举类型压缩空间。回到第1幅图,sex: FEMALE仅用2个字节就编码完成,正是枚举值FEMALE使用数字1表示所达到的效果。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c9/c7/c9b6c10399a34d7a0e577a0397cd5ac7.png" alt="">
|
||||
|
||||
而且,由于Protobuf定义了每个字段的默认值,因此,当消息使用字段的默认值时,Protobuf编码时会略过该字段。以sex: MALE为例,由于MALE=0是sex的默认值,因此在第2幅示例图中,这2个字节都省去了。
|
||||
|
||||
另外,当使用repeated语法将多个数字组成列表时,还可以通过打包功能提升编码效率。比如下图中,对numbers字段添加101、102、103、104这4个值后,如果不使用打包功能,共需要8个字节编码,其中每个数字前都需要添加字段名。而使用打包功能后,仅用6个字节就能完成编码,显然列表越庞大,节约的空间越多。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ce/47/ce7ed2695b1e3dd869b59c438ee66147.png" alt="">
|
||||
|
||||
在Protobuf2版本中,需要显式设置 [packed=True] 才能使用打包功能,而在Protobuf3版本中这是默认功能。
|
||||
|
||||
最后,从[这里](https://github.com/protocolbuffers/protobuf/blob/master/docs/performance.md)可以查看Protobuf的编解码性能测试报告,你能看到,在保持高空间利用率的前提下,Protobuf仍然拥有飞快的速度!
|
||||
|
||||
## 小结
|
||||
|
||||
这一讲我们介绍了Protobuf的编码原理。
|
||||
|
||||
通过在proto文件中为每个字段预分配1个数字,编码时就省去了完整字段名占用的空间。而且,数字越小编码时用掉的空间也越小,实际网络中大量传输的是小数字,这带来了很高的空间利用率。Protobuf的枚举类型也通过类似的原理,用数字代替字符串,可以节约许多空间。
|
||||
|
||||
对于字符串Protobuf没有做压缩,因此如果消息中的字符串比重很大时,建议你先压缩后再使用Protobuf编码。对于拥有默认值的字段,Protobuf编码时会略过它。对于repeated列表,使用打包功能可以仅用1个字段前缀描述所有数值,它在列表较大时能带来可观的空间收益。
|
||||
|
||||
## 思考题
|
||||
|
||||
下一讲我将介绍gRPC协议,它结合了HTTP/2与Protobuf的优点,在应用层提供方便而高效的RPC远程调用协议。你也可以提前思考下,既然Protobuf的空间效率远甚过HPACK技术,为什么gRPC还要使用HTTP/2协议呢?
|
||||
|
||||
在Protobuf的性能测试报告中,C++语言还拥有arenas功能,你可以通过option cc_enable_arenas = true语句打开它。请结合[[第2讲]](https://time.geekbang.org/column/article/230221) 的内容,谈谈arenas为什么能提升消息的解码性能?欢迎你在留言区与我一起探讨。
|
||||
|
||||
感谢阅读,如果你觉得这节课对你有一些启发,也欢迎把它分享给你的朋友。
|
147
极客时间专栏/系统性能调优必知必会/应用层编解码优化/18 | 如何通过gRPC实现高效远程过程调用?.md
Normal file
147
极客时间专栏/系统性能调优必知必会/应用层编解码优化/18 | 如何通过gRPC实现高效远程过程调用?.md
Normal file
@@ -0,0 +1,147 @@
|
||||
<audio id="audio" title="18 | 如何通过gRPC实现高效远程过程调用?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e8/e8/e894713ff9692c1c167f23df35b9b8e8.mp3"></audio>
|
||||
|
||||
你好,我是陶辉。
|
||||
|
||||
这一讲我们将以一个实战案例,基于前两讲提到的HTTP/2和ProtoBuf协议,看看gRPC如何将结构化消息编码为网络报文。
|
||||
|
||||
直接操作网络协议编程,容易让业务开发过程陷入复杂的网络处理细节。RPC框架以编程语言中的本地函数调用形式,向应用开发者提供网络访问能力,这既封装了消息的编解码,也通过线程模型封装了多路复用,对业务开发很友好。
|
||||
|
||||
其中,Google推出的gRPC是性能最好的RPC框架之一,它支持Java、JavaScript、Python、GoLang、C++、Object-C、Android、Ruby等多种编程语言,还支持安全验证等特性,得到了广泛的应用,比如微服务中的Envoy、分布式机器学习中的TensorFlow,甚至华为去年推出重构互联网的New IP技术,都使用了gRPC框架。
|
||||
|
||||
然而,网络上教你使用gRPC框架的教程很多,却很少去谈gRPC是如何编码消息的。这样,一旦在大型分布式系统中出现疑难杂症,需要通过网络报文去定位问题发生在哪个系统、主机、进程中时,你就会毫无头绪。即使我们掌握了HTTP/2和Protobuf协议,但若不清楚gRPC的编码规则,还是无法分析抓取到的gRPC报文。而且,gRPC支持单向、双向的流式RPC调用,编程相对复杂一些,定位流式RPC调用引发的bug时,更需要我们掌握gRPC的编码原理。
|
||||
|
||||
这一讲,我就将以gRPC官方提供的example:[data_transmisstion](https://github.com/grpc/grpc/tree/master/examples/python/data_transmission) 为例,介绍gRPC的编码流程。在这一过程中,会顺带回顾HTTP/2和Protobuf协议,加深你对它们的理解。虽然这个示例使用的是Python语言,但基于gRPC框架,你可以轻松地将它们转换为其他编程语言。
|
||||
|
||||
## 如何使用gRPC框架实现远程调用?
|
||||
|
||||
我们先来简单地看下gRPC框架到底是什么。RPC的全称是Remote Procedure Call,即远程过程调用,它通过本地函数调用,封装了跨网络、跨平台、跨语言的服务访问,大大简化了应用层编程。其中,函数的入参是请求,而函数的返回值则是响应。
|
||||
|
||||
gRPC就是一种RPC框架,在你定义好消息格式后,针对你选择的编程语言,gRPC为客户端生成发起RPC请求的Stub类,以及为服务器生成处理RPC请求的Service类(服务器只需要继承、实现类中处理请求的函数即可)。如下图所示,很明显,gRPC主要服务于面向对象的编程语言。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c2/a1/c20e6974a05b5e71823aec618fc824a1.jpg" alt="">
|
||||
|
||||
gRPC支持QUIC、HTTP/1等多种协议,但鉴于HTTP/2协议性能好,应用场景又广泛,因此HTTP/2是gRPC的默认传输协议。gRPC也支持JSON编码格式,但在忽略编码细节的RPC调用中,高效的Protobuf才是最佳选择!因此,这一讲仅基于HTTP/2和Protobuf,介绍gRPC的用法。
|
||||
|
||||
gRPC可以简单地分为三层,包括底层的数据传输层,中间的框架层(框架层又包括C语言实现的核心功能,以及上层的编程语言框架),以及最上层由框架层自动生成的Stub和Service类,如下图所示:
|
||||
|
||||
[<img src="https://static001.geekbang.org/resource/image/2a/4a/2a3f82f3eaabd440bf1ee449e532944a.png" alt="" title="图片来源:https://platformlab.stanford.edu/Seminar%20Talks/gRPC.pdf">](https://platformlab.stanford.edu/Seminar%20Talks/gRPC.pdf)
|
||||
|
||||
接下来我们以官网上的[data_transmisstion](https://github.com/grpc/grpc/tree/master/examples/python/data_transmission) 为例,先看看如何使用gRPC。
|
||||
|
||||
构建Python语言的gRPC环境很简单,你可以参考官网上的[QuickStart](https://grpc.io/docs/quickstart/python/)。
|
||||
|
||||
使用gRPC前,先要根据Protobuf语法,编写定义消息格式的proto文件。在这个例子中只有1种请求和1种响应,且它们很相似,各含有1个整型数字和1个字符串,如下所示:
|
||||
|
||||
```
|
||||
package demo;
|
||||
|
||||
message Request {
|
||||
int64 client_id = 1;
|
||||
string request_data = 2;
|
||||
}
|
||||
|
||||
message Response {
|
||||
int64 server_id = 1;
|
||||
string response_data = 2;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
请注意,这里的包名demo以及字段序号1、2,都与后续的gRPC报文分析相关。
|
||||
|
||||
接着定义service,所有的RPC方法都要放置在service中,这里将它取名为GRPCDemo。GRPCDemo中有4个方法,后面3个流式访问的例子我们呆会再谈,先来看简单的一元访问模式SimpleMethod 方法,它定义了1个请求对应1个响应的访问形式。其中,SimpleMethod的参数Request是请求,返回值Response是响应。注意,分析报文时会用到这里的类名GRPCDemo以及方法名SimpleMethod。
|
||||
|
||||
```
|
||||
service GRPCDemo {
|
||||
rpc SimpleMethod (Request) returns (Response);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
用grpc_tools中的protoc命令,就可以针对刚刚定义的service,生成含有GRPCDemoStub类和GRPCDemoServicer类的demo_pb2_grpc.py文件(实际上还包括完成Protobuf编解码的demo_pb2.py),应用层将使用这两个类完成RPC访问。我简化了官网上的Python客户端代码,如下所示:
|
||||
|
||||
```
|
||||
with grpc.insecure_channel("localhost:23333") as channel:
|
||||
stub = demo_pb2_grpc.GRPCDemoStub(channel)
|
||||
request = demo_pb2.Request(client_id=1,
|
||||
request_data="called by Python client")
|
||||
response = stub.SimpleMethod(request)
|
||||
|
||||
```
|
||||
|
||||
示例中客户端与服务器都在同一台机器上,通过23333端口访问。客户端通过Stub对象的SimpleMethod方法完成了RPC访问。而服务器端的实现也很简单,只需要实现GRPCDemoServicer父类的SimpleMethod方法,返回response响应即可:
|
||||
|
||||
```
|
||||
class DemoServer(demo_pb2_grpc.GRPCDemoServicer):
|
||||
def SimpleMethod(self, request, context):
|
||||
response = demo_pb2.Response(
|
||||
server_id=1,
|
||||
response_data="Python server SimpleMethod Ok!!!!")
|
||||
return response
|
||||
|
||||
```
|
||||
|
||||
可见,gRPC的开发效率非常高!接下来我们分析这次RPC调用中,消息是怎样编码的。
|
||||
|
||||
## gRPC消息是如何编码的?
|
||||
|
||||
**定位复杂的网络问题,都需要抓取、分析网络报文。**如果你在Windows上抓取网络报文,可以使用Wireshark工具(可参考[《Web协议详解与抓包实战》第37课](https://time.geekbang.org/course/detail/175-100973)),如果在Linux上抓包可以使用tcpdump工具(可参考[第87课](https://time.geekbang.org/course/detail/175-118169))。当然,你也可以从[这里](https://github.com/russelltao/geektime_distrib_perf/blob/master/18-gRPC/data_transmission.pkt)下载我抓取好的网络报文,用Wireshark打开它。需要注意,23333不是HTTP常用的80或者443端口,所以Wireshark默认不会把它解析为HTTP/2协议。你需要鼠标右键点击报文,选择“解码为”(Decode as),将23333端口的报文设置为HTTP/2解码器,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/74/f7/743e5038dc22676a0d48b56c453c1af7.png" alt="">
|
||||
|
||||
图中蓝色方框中,TCP连接的建立过程请参见[[第9讲]](https://time.geekbang.org/column/article/237612),而HTTP/2会话的建立可参见[《Web协议详解与抓包实战》第52课](https://time.geekbang.org/course/detail/175-105608)(还是比较简单的,如果你都清楚就可以直接略过)。我们重点看红色方框中的gRPC请求与响应,点开请求,可以看到下图中的信息:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ba/41/ba4d9e9a6ce212e94a4ced829eeeca41.png" alt="">
|
||||
|
||||
先来分析蓝色方框中的HTTP/2头部。请求中有2个关键的HTTP头部,path和content-type,它们决定了RPC方法和具体的消息编码格式。path的值为“/demo.GRPCDemo/SimpleMethod”,通过“/包名.服务名/方法名”的形式确定了RPC方法。content-type的值为“application/grpc”,确定消息编码使用Protobuf格式。如果你对其他头部的含义感兴趣,可以看下这个[文档](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md),注意这里使用了ABNF元数据定义语言(如果你还不了解ABNF,可以看下[《Web协议详解与抓包实战》第4课](https://time.geekbang.org/course/detail/175-93589))。
|
||||
|
||||
HTTP/2包体并不会直接存放Protobuf消息,而是先要添加5个字节的Length-Prefixed Message头部,其中用4个字节明确Protobuf消息的长度(1个字节表示消息是否做过压缩),即上图中的桔色方框。为什么要多此一举呢?这是因为,gRPC支持流式消息,即在HTTP/2的1条Stream中,通过DATA帧发送多个gRPC消息,而Length-Prefixed Message就可以将不同的消息分离开。关于流式消息,我们在介绍完一元模式后,再加以分析。
|
||||
|
||||
最后分析Protobuf消息,这里仅以client_id字段为例,对上一讲的内容做个回顾。在proto文件中client_id字段的序号为1,因此首字节00001000中前5位表示序号为1的client_id字段,后3位表示字段的值类型是varint格式的数字,因此随后的字节00000001表示字段值为1。序号为2的request_data字段请你结合上一讲的内容,试着做一下解析,看看字符串“called by Python client”是怎样编码的。
|
||||
|
||||
再来看服务器发回的响应,点开Wireshark中的响应报文后如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b8/cf/b8e71a1b956286b2def457c2fae78bcf.png" alt="">
|
||||
|
||||
其中DATA帧同样包括Length-Prefixed Message和Protobuf,与RPC请求如出一辙,这里就不再赘述了,我们重点看下HTTP/2头部。你可能留意到,响应头部被拆成了2个部分,其中grpc-status和grpc-message是在DATA帧后发送的,这样就允许服务器在发送完消息后再给出错误码。关于gRPC的官方错误码以及message描述信息是如何取值的,你可以参考[这个文档。](https://github.com/grpc/grpc/blob/master/doc/statuscodes.md)
|
||||
|
||||
这种将部分HTTP头部放在包体后发送的技术叫做Trailer,[RFC7230文档](https://tools.ietf.org/html/rfc7230#page-39)对此有详细的介绍。其中,RPC请求中的TE: trailers头部,就说明客户端支持Trailer头部。在RPC响应中,grpc-status头部都会放在最后发送,因此它的帧flags的EndStream标志位为1。
|
||||
|
||||
可以看到,gRPC中的HTTP头部与普通的HTTP请求完全一致,因此,它兼容当下互联网中各种七层负载均衡,这使得gRPC可以轻松地跨越公网使用。
|
||||
|
||||
## gRPC流模式的协议编码
|
||||
|
||||
说完一元模式,我们再来看流模式RPC调用的编码方式。
|
||||
|
||||
所谓流模式,是指RPC通讯的一方可以在1次RPC调用中,持续不断地发送消息,这对订阅、推送等场景很有用。流模式共有3种类型,包括客户端流模式、服务器端流模式,以及两端双向流模式。在[data_transmisstion](https://github.com/grpc/grpc/tree/master/examples/python/data_transmission) 官方示例中,对这3种流模式都定义了RPC方法,如下所示:
|
||||
|
||||
```
|
||||
service GRPCDemo {
|
||||
rpc ClientStreamingMethod (stream Request) returns Response);
|
||||
|
||||
rpc ServerStreamingMethod (Request) returns (stream Response);
|
||||
|
||||
rpc BidirectionalStreamingMethod (stream Request) returns (stream Response);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
不同的编程语言处理流模式的代码很不一样,这里就不一一列举了,但通讯层的流模式消息编码是一样的,而且很简单。这是因为,HTTP/2协议中每个Stream就是天然的1次RPC请求,每个RPC消息又已经通过Length-Prefixed Message头部确立了边界,这样,在Stream中连续地发送多个DATA帧,就可以实现流模式RPC。我画了一张示意图,你可以对照它理解抓取到的流模式报文。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4b/e0/4b1b9301b5cbf0e0544e522c2a8133e0.jpg" alt="">
|
||||
|
||||
## 小结
|
||||
|
||||
这一讲介绍了gRPC怎样使用HTTP/2和Protobuf协议编码消息。
|
||||
|
||||
在定义好消息格式,以及service类中的RPC方法后,gRPC框架可以为编程语言生成Stub和Service类,而类中的方法就封装了网络调用,其中方法的参数是请求,而方法的返回值则是响应。
|
||||
|
||||
发起RPC调用后,我们可以这么分析抓取到的网络报文。首先,分析应用层最外层的HTTP/2帧,根据Stream ID找出一次RPC调用。客户端HTTP头部的path字段指明了service和RPC方法名,而content-type则指明了消息的编码格式。服务器端的HTTP头部被分成2次发送,其中DATA帧发送完毕后,才会发送grpc-status头部,这样可以明确最终的错误码。
|
||||
|
||||
其次,分析包体时,可以通过Stream中Length-Prefixed Message头部,确认DATA帧中含有多少个消息,因此可以确定这是一元模式还是流式调用。在Length-Prefixed Message头部后,则是Protobuf消息,按照上一讲的内容进行分析即可。
|
||||
|
||||
## 思考题
|
||||
|
||||
最后,留给你一道练习题。gRPC默认并不会压缩字符串,你可以通过在获取channel对象时加入grpc.default_compression_algorithm参数的形式,要求gRPC压缩消息,此时Length-Prefixed Message中1个字节的压缩位将会由0变为1。你可以观察下执行压缩后的gRPC消息有何不同,欢迎你在留言区与大家一起探讨。
|
||||
|
||||
感谢阅读,如果你觉得这节课对你有一些启发,也欢迎把它分享给你的朋友。
|
Reference in New Issue
Block a user