IT技术博客大学习 共学习 共进步

从 Nginx 默认不压缩 HTTP/1.0 说起

JerryQu 的小站 2016-03-21 23:42:29 浏览 1,981 次

   临近年关,明显变忙,博客也更新得慢了,以后尽量保证周更吧。今天这篇文章属于计划之外的更新,源自于白天看到的《一个基于 http 协议的优化》。在这篇文章中,作者描述了这样一个现象:

   在移动的 http 请求量和联通不相上下的前提下,移动的 http response 带来的网络流量是联通的 2.5 倍。移动大概有 3 成的请求都没有做压缩,而联通几乎都是经过压缩的。那些没有经过压缩的 http 会话都是走了 1.0 的协议,相反经过压缩的 http 会话都是走了 http1.1 协议。

   也就是说在相同的服务端配置下,移动运营商过来的流量中有 30% 走了 HTTP/1.0,而作者所使用的 HTTP Server,不对 HTTP/1.0 响应启用 GZip。

   为什么在移动运营商网络下会有这么高比例的 HTTP/1.0 请求,本文按下不表,总之这一定是移动的原因。直接看另外一个问题,也就是本文标题所写:Nginx 为什么默认不压缩 HTTP/1.0?

   那篇文章的作者并没有说明他用什么 HTTP Server,我这里直接当成 Nginx 好了。后面会发现这个问题跟 HTTP 协议有关,所有 HTTP Server 都会面临。

   在 Nginx 的官网文档中,有这样一个指令:

   Syntax:  gzip_http_version 1.0 | 1.1;
Default:  gzip_http_version 1.1;
Context:  http, server, location
Sets the minimum HTTP version of a request required to compress a response.

   很明显,这个指令是用来设置 Nginx 启用 GZip 所需的 HTTP 最低版本,默认是 HTTP/1.1。也就是说 Nginx 默认不压缩 HTTP/1.0 是因为这个指令,将它的值改为 1.0 就能解决问题。

   对于文本文件,GZip 的效果非常明显,开启后传输所需流量大约会降至 1/4 ~ 1/3。这么好的事情,Nginx 改一下配置就可以支持,为什么它默认不开启?

   Nginx 对于满足条件(请求头中有 Accept-Encoding: gzip,响应内容的 Content-Type 存在于 gzip_types 列表)的请求会采用即时压缩(On-The-Fly Compression),整个压缩过程在内存中完成,是流式的。也就是说,Nginx 不会等文件 GZip 完成再返回响应,而是边压缩边响应,这样可以显著提高 TTFB(Time To First Byte,首字节时间,WEB 性能优化重要指标)。这样唯一的问题是,Nginx 开始返回响应时,它无法知道将要传输的文件最终有多大,也就是无法给出 Content-Length 这个响应头部。

   我们还知道,HTTP/1.1 默认支持 TCP 持久连接(Persistent Connection),HTTP/1.0 也可以通过显式指定 Connection: keep-alive 来启用持久连接。HTTP 运行在 TCP 连接之上,自然也有着跟 TCP 一样的三次握手、慢启动等特性,为了尽可能的提高 HTTP 性能,使用持久连接就显得尤为重要了。

   明白以上两点,问题就水落石出了。对于 TCP 持久连接上的 HTTP 报文,客户端需要一种机制来准确判断结束位置,而在 HTTP/1.0 中,这种机制只有 Content-Length。于是,前面这种情况只能要么不压缩,要么不启用持久连接(对于非持久连接,TCP 断开就可以认为 HTTP 报文结束),而 Nginx 默认选择的是前者。

   那么在 HTTP/1.1 中,这个问题解决了吗?当然!我在之前的文章中讲过,HTTP/1.1 新增的 Transfer-Encoding: chunked 所对应的分块传输机制可以完美解决这类问题。有兴趣的同学可以查看我的这篇文章:HTTP 协议中的 Transfer-Encoding

   在很久以前,我们为了降低服务端的 CPU 压力,往往会把静态资源预先压缩为 .gz 文件,这样 HTTP Server 不需要即时压缩文件,也就不会遇到本文提到的这个问题。但随着时间的推移,现在几乎没人会这么干了,想要了解细节的同学可以看下 Nginx 的 ngx_http_gzip_static_module 模块。

   理论知识先写到这里,最后用实践来验证一下:

   首先,不启用 Nginx 的 HTTP/1.0 GZip 功能,使用 HTTP/1.0 请求报文测试:

   http/1.0 without gzip

   可以看到,尽管我的请求报文中指明了可以接受 GZip,但是返回的内容依然是未压缩的;同时服务端响应了 Content-LengthConnection: keep-alive,连接并没有断开。也就是说对于 HTTP/1.0 请求,Nginx 为了尽可能启用持久连接,放弃了 GZip,这是 Nginx 的默认策略。

   然后,启用 Nginx 的 HTTP/1.0 GZip 功能,使用 HTTP/1.0 请求报文测试:

   http/1.0 with gzip

   可以看到,这次的请求报文与上次完全一样,但是结果截然不同:虽然返回的内容被压缩了,但是连接也被断开了,服务端返回了 Connection: close。原因就是之前说过的,动态压缩导致无法事先得知响应内容长度,在 HTTP/1.0 中只能依靠断开连接来让客户端知道响应结束了。

   最后,使用 HTTP/1.1 请求报文测试:

   http/1.1 with gzip

   可以看到,由于请求报文是 HTTP/1.1 的,Nginx 能知道这个客户端可以支持 HTTP/1.1 的 Transfer-Encoding: chunked,于是通过分块传输解决了所有问题:既启用了压缩,也启用了持久连接。

   那么,对于 HTTP/1.0 请求,我们是让 Nginx 放弃持久连接好,还是放弃 GZip 好呢?

   实际上,由于 HTML 文档一般都是使用 PHP、Node.js 等动态语言输出,即使不压缩,Nginx 也无法事先得知它的 Content-Length,在 HTTP/1.0 中横竖都无法启用持久连接,这时还不如启用 GZip 省点流量。

   对于 JS、CSS 等事先可以知道大小的静态文本文件,我的建议是,移动端首次访问把重要的 JS、CSS 都内联在 HTML 中,然后存在 localStorage 里,后续不输出;不重要的 JS、CSS 通过外链引入,启用 GZip,牺牲 keep-alive 来达到减少流量的目的。

   本文先写到这里,欢迎来博客原文进行评论、交流。浏览器的 GZip 其实还有很多有趣的小故事,先卖个关子,以后有空再写。

建议继续学习

  1. windows下压缩包在linux解压乱码的解决办法 (阅读 5,300)
  2. php的echo为什么这么慢 (阅读 5,220)
  3. 使用系统命令实现文件的压缩与加密 (阅读 5,182)
  4. 启用memcached压缩注意事项 (阅读 5,121)
  5. Android设计中的.9.png (阅读 4,901)
  6. MySQL从压缩文件恢复数据 (阅读 4,680)
  7. 前端性能优化之Html压缩 (阅读 4,641)
  8. 项目中对模板和js,css文件进行压缩的处理类 (阅读 4,520)
  9. 开源压缩算法Zopfli介绍 (阅读 4,460)
  10. mod_gzip:Apache的HTTP压缩优化 (阅读 4,380)