使用 ngx_lua (openresty)正确读取 HTTP 请求 body

之前用 ngx_lua(openresty) 写了一个处理 HTTP 请求的程序,今天发现当发送的 HTTP 请求 body 很大的时候,发现老是报错,最后定位到 ngx.req.get_body_data() 这个函数返回 nil ,而不是真正的 body。

于是我去 ngx_lua 的文档看这个函数的文档,发现文档中说有三种情况下,这个函数的返回值会是 nil:

This function returns nil if

  1. the request body has not been read,
  2. the request body has been read into disk temporary files,
  3. or the request body has zero size.

第一条,我是读了文档的,知道要 Nginx 默认是不会读 body 的,要么打开读 body 的开关,要么显示调用一下 read。所以代码中已经调用了 ngx.req.read_body() 了,不会是这个原因。

第三条,也不可能,我通过 curl 发送的,body 肯定是发出去了(可以抓包验证)。

那么基本上就确定是这个 request body 被读到硬盘的临时文件里了。看到这里我猜应该是 Nginx 是将大的 HTTP 请求放到磁盘中而不是放到内存中。搜了一下文档,发现有这个参数:

当请求体的大小大禹 client_body_buffer_size 的时候,Nginx 将会把它存到一个临时文件中,而不是放到内存中。这个值的大小默认是内存页的两倍:32 位系统上是 8k,64位系统上是16k。

OK,问题找到了,现在解决方案有两个:1)调大这个值,我觉得是不合理的,这样会浪费内存。2)可以想办法读到临时文件中的大 body。ngx_lua 提供了配套的方法 ngx_req.get_body_file,注意这只是获得文件,还要在 lua 代码中打开读取文件。

所以最后,处理一个请求且还能正确处理很大 body 的请求的代码是这样的,其中高亮的部分,是核心的逻辑,先尝试从内存中读 body,如果读不到,就去临时文件中读。

我读这个实际的应用场景是,读出 body,按照 json 解析出来当做路由规则(需求是需要动态设置 HTTP 的路由规则,所以我用 ngx_lua 在 nginx 上新开了个端口监控这样的规则)。现在用很大的规则一测试,发现性能下降很厉害。存到文件、再读出文件是一方面。另一方面是 ngx_lua 是为每一个 HTTP 请求开一个 lua 协程处理,不能共享变量,只能通过 lua_shared_dict 来保存持久的变量。但是 lua_shared_dict 的问题是,这个 dict 只支持 “Lua booleans, numbers, strings, or nil”,解析出来的 json 是一个 lua 的 table,不能保存到 lua_shared_dict 里面,我只好保存一个字符串,然后对每一个 HTTP 请求 json decode 这个字符串了。不知道有没有更好的方法。

 

HTTP 长连接

本文介绍 HTTP 长连接的协议、历史,TCP 长连接,和一些客户端、服务器实现。

Disclaimer: 本文所引用的资料都会给出链接,如有疑问应该去原资料验证,如果存在错误请不吝指出。

HTTP/1.0 会为每一个 HTTP 请求都建立 TCP 连接,这样显然是很低效的,所以就有人想在同一个 TCP 连接上发送多个 HTTP 请求,这样就省掉后面 TCP 连接建立的三次握手了。这就是“长连接”,英文里面也有其他的名字:HTTP Persistent Connection,HTTP Keep-Alive,或 HTTP Connection Reuse。

可以用 nc 来测试一下,这里我用 nc 连接到 httpbin.org 的服务器,然后用 tcpdump 抓包看其中的 TCP 连接情况。

用 nc 打开连接后,输入和输出如下。其中高亮的部分是我的输入,其余是服务器的输出:

可以看到我连续发送了两次 HTTP 请求,nc 都没有结束,并且还在等到我的输入,输入 ^c 之后才关闭,断开了连接。

抓包如下:

可以看到两次 HTTP 请求始终在同一个 TCP 连接上,只不过中间有很多保持心跳的 TCP 包。

HTTP/1.0 到 HTTP/1.1

在 HTTP/1.0 中,有人就尝试实现这样的长连接。实现的方式是客户端和服务器协商 Connection: Keep-Alive Header。加入浏览器支持长连接,那么就在 HTTP 请求的 Header 上添加 Connection: Keep-Alive ,如果服务器收到请求,就在 Response 中也加入这个 Header: Connection: Keep-Alive 。这样客户端再发送另一个请求的时候就会使用这个 TCP 连接,而不会新建一个。

然而,HTTP/1.0 的实现是错误的,主要的问题在代理。HTTP/1.0 的 Proxy Server 不懂这个字段,把它原样发给了后面的 Server,导致 Proxy 和 Server 之间建立了长连接,造成了一个 “Hung Connection”。有人提出一种处理的手段(RFC 7230 A.1.2),是想换一个字段来表示 Client 和 Proxy Server 之间的长连接 Proxy-Connection ,但是通常代理并不仅仅是一层,而是层层 HTTP 代理,这样依然就存在前面的问题,所以 Proxy-Connection 这个 Header 在任何情况下都用不到。

所以在 HTTP/1.0 使用长连接要小心,客户端要感知 hung 住的连接,要显示地关闭。因为 HTTP/1.0 的 Proxy 不支持长连接,所以在有 HTTP/1.0 Proxy 的情况下,不能使用长连接

然而,和 Proxy Server 的对话是长连接一个重要的使用场景,所以粗暴地规定不能对 Proxy 使用长连接是不能接受的。所以就需要一种新的机制,要满足以下的需求:1)对于旧版本的 Proxy Server ,忽略了长连接请求不会造成 Hung Connnection. 2)新版本的机制能够让 Real Server 和 Proxy Server 都能正确处理长连接。

HTTP/1.1 提出的方案(RFC2068 19.7.1)是:所有的 HTTP 连接默认都是长连接。当 Header 中添加 Connection: close 的时候,表示不保持长连接。这样可以满足上面说的需求,兼容旧版本的 Proxy Server(不会携带表示长连接的 Header)。

HTTP/1.1 的 Server 也可以跟 HTTP/1.0 的 Client 建立长连接,如果 Client 显式地携带了 Keep-Alive 的话。但是 HTTP/1.0 的客户端不支持 Chunked transfer-coding,所以必须每条信息都带上 Content-Length 表示边界。

另外除了 Connection 这个 Header,还有一个可选的 Header Keep-Alive 可以控制一些有关长连接的参数:

这个字段是可选的,只有带有参数的时候才生效。显而易见,这个 Header 必须和 Connection: Keep-Alive 一起发,毕竟它是控制长连接的,在没有长连接的情况下,这个 Header 就毫无意义。

最重要的一点,通过上面的描述你可能已经意识到了,就是 Connection: Keep-Alive 和下面要将的 Keep-Alive 这两个 Header 都是 hop-by-hop 的,就是说只对通讯的二者起作用,加入第二个人是 Proxy,那么 Proxy 和 Real Server 之间的连接是怎样的,就需要它们二者再自己商量了。

HTTP Keep-Alive Header

Hypertext Transfer Protocol (HTTP) Keep-Alive Header 这个 RFC 定义了 Keep-Alive Header 的一种形式。

为什么需要这些参数呢?

为了节省资源,Host 会选择关闭 idle 的长连接。比如一个连接很长时间没有使用,Host 会选择关闭它(显而易见)。

基于这个原因,很多客户端发送非幂等的请求的时候,会选择不复用现有的 idle 连接。理由如下,假设现在有一个非幂等请求进来,而恰好 Server 认为这个连接空闲了很长时间了,决定要关闭此连接。那么客户端收不到 HTTP 响应(也可能收不到 TCP 的 ACK),客户端就不知道这个非幂等请求到底被处理了没有。可能 Server 收到了这个请求之后又关闭的,也可能请求到达之前 Server 就关闭连接了。

所以很多客户端选择对所有非幂等请求都建立新的连接。但是每次都建立 TCP 连接,是很浪费资源的,也增加了请求的延迟。

假如 Client 知道 Server 的 timeout,客户端就可以在快要 timeout 的时候选择不使用这个已有的连接,或者发送请求来保证不超过 timeout。另外,如果客户端知道 timeout,那么在没有后续请求发送,而 timeout 时间又比较长的时候,客户端可以显示地要求 Server 关闭连接,释放资源。

Keep-Alive Header 的形式定义如下:

‘timeout’ 参数

timeout 参数代表当一个连接至少 idle 多长时间才会被关闭。它的 value 是一个代表秒数的 int 值。通常,连接上没有数据传输就被认为是 idle 的。但是不同的客户端和服务器实现对 idle 的理解不同,还受网络延迟的影响。所以建议客户端评估 idle 的时候算上网络延迟。

‘max’ 参数

max 参数表示在一个连接上客户端可以发送请求数的最大值。客户端在一个连接上请求的次数到了这个值,Server 就会关闭这个连接。

max 的 value 是一个 int 值,表示请求数。

对于客户端来说,收到了带有这个参数的 Response,就可以限制在同一个连接中发送的请求。举例说,假如客户端用一个队列来存放即将发送的请求,那么根据 max 就可以对队列分段。对于服务器来说,当服务器接收的请求达到了计数值之后就可以关闭连接。

Keep-Alive Extensions

除了这两个参数之外,也可以在 keep-alive-extension 字段中添加自定义的字段,如果服务器不理解这些字段,将忽略。

存在中间人的情况

在本文开头已经看到,HTTP 的长连接是基于 TCP 的长连接的。所以情况要复杂的多,实际情况有可能 TCP 保持长连接的时间要比 HTTP 短,HTTP 长连接 timeout 还没达到的时候,像 Net Address Translation (NAT) 这样的设备就将此 TCP 连接断开了,导致此连接不再可用。

HTTP/1.1 的中间人在转发请求的时候会直接丢掉这个 Header,因为它不需要认识这个 Header,它可以在任何时候断开连接

HTTP/1.0 的中间人,我们前面说过了,会导致错误。

此外,如果客户端(或中间人)感知到后面中间人的中间人(或中间 TCP 转发设备)的 timeout 更短的话,可能会修改这个值。Again,这个 Header 是 hop-by-hop 的。

从以下这个例子可以看出,每一端的连接都是独立协商的。

这里 Client 想要建立一个 timeout 为 10 分钟的长连接,但是 Proxy Server 只支持 120 秒,所以 Client 和 Proxy 之间的长连接最终是 120 秒的 timeout。Proxy 想和 Real Server 建立 1200 timeout 的长连接,但是 Real Server 将其降低到 300s。(PS:上图中 120 这个数字在 RFC 中写的是 5000,这样的话跟 RFC 的解释就冲突了,我不是很理解,我觉得 RFC 这里这个数字可能写错了。如果我理解错了,请指出)

如果 Upgraded HTTP Connections 的情况,就更复杂一些。如果 upgraded 的协议没有指明 timeout,那么会继承长连接初始化时候的 timeout,max 这个参数没有意义,因为升级后的 request 和 subrequest 被视为一个 HTTP 请求。客户端、中间人、服务器对 Upgraded 的策略可能不同,但是这个长连接的各个参数不再像上面一样可以分别独立协商,而是从 Client 到 Proxy 到 Real Server 的连接属性是一样的。

由于 keepalive 这个词被用在很多地方,而意义不尽相同,下面介绍一下 TCP 的 keepalive。

TCP Keepalive

对于 TCP 来说,Keepalive 并不是标准 TCP 协议规定的,所以 TCP 本身并不知道这个东西的存在,这只是在 TCP 之上的一个实现。

简单说,TCP Keepalive 就是设置一个 timer,时间一到就发送一个 probe packet,并设置 ACK。如果对方发送回来一个 ACK,那么那就知道这个 TCP 连接依然是可用的。如果对方没有发送 ACK 回来,那么就知道这个连接已经被断开了(实际的实现,一般会在收不到请求 ACK 的情况下重复发送多次 probe packet)。可以这么做是因为 1) TCP 是面向流的连接,而不是面向包的,所以在这个“流”中插入一个长度为 0 的包不会对这个流的内容造成任何影响。2)TCP 对每一个 packet 都会发送一个 ACK 确认。所以就可以用长度为 0 的一段“stream”来当做 probe。

那这个 TCP 的 Keepalive 有什么用呢?

主要有两个,第一是检查连接是否可用。假设 TCP 的另一端断电了,或者中间的某一个转发的设备断电了,那么通过检查连接的可用性,就可以确保不会在一个已经不能用的连接上发消息,不会有 false-positive。

第二是可以起到类似心跳的作用,防火墙或 NAT 设备的内存只能保存有限的连接数,它们普遍采用的策略是保留最近用到的连接,丢弃最旧没有有消息的连接。通过 Keepalive 的机制,我们可以让 NAT 设备保持我们的连接在可用列表中。

对比一下:HTTP 的 Keep-Alive Header 做的是设置长连接的 idle 时间,超过了这个时间就关闭连接,TCP Keepalive 设置的 timer 到了就发送空的 probe packet。

最后再提一个 Nginx 里面的 keepalive 指令,这个指令就更加迷惑了,跟上面介绍的都不搭边。它表示的是:Nginx 与 upstream 之间保持最多多少个 idle 的长连接。这个 idle 很关键,比如 keepalive 100 ,那么收到 300 个请求的时候,Nginx 和 upstream 建立 300 个长连接,这 300 个请求结束后,又来了 50 个请求,那么只有 50 个长连接是工作的,idle 的连接就有 250 个,Nginx 会关闭 150 个。更具体的例子可以看这里

 

参考资料:

 

SRE&Devops 每周分享 Issue #4 AWS Layer

SRE 监控的黄金指标

服务响应延迟、请求总量、错误数量、系统资源使用率是 Google SRE 提出的“四大黄金监控”指标,之后被很多团队实践。本文介绍了为什么这四个指标是了解系统当前状态最合适的四个指标。也提出了一些其他在提高系统可用性方面的实践。

Observability at Scale: Building Uber’s Alerting Ecosystem

Uber 的官方博客,介绍了它们的监控报警系统。Uber 有两个监控系统,一个是 uMonitor,从时序数据库中检测异常,发出报警(业务层面);一个是 Neris,监控基础设施,比如 CPU/Mem 等。文中分别介绍了这两个监控系统,以及如何报警、如果管理报警。

Why Use K-Means for Time Series Data? (Part One)

K-means 算法处理时序数据教程:

  1. Part1:如何使用统计学函数和 K-means 聚类从时序数据中检测异常
  2. Part2:代码实践,如何将 K-means 算法应用到时序数据,以及一些缺点
  3. Part3:如何使用 K-means 和 InfluxDB 在 EKG 数据中检测异常

Get Application Performance Metrics on Python Flask With Elastic APM on Kibana and Elasticsearch

很多 APM 客户端是开源的,Server 不开源。ELK 架构就很良心了。这是使用 Elastic APM 和 ES 的一个手把手教程。

Garbagedog: How eero does continuous monitoring of Java garbage collection

Eero 开发的一个监控 JVM GC 的组件(Python 编写)。Github

Open-Sourcing Our Incident Response Training

PagerDuty 开源了它们的应急反应培训资料。

Kubernetes Security — Are your Container Doors Open?

本文介绍了一些常见的 Kubernetes 误留下的漏洞,以及检查漏洞的方法。

Amazon Lambda 宣布 Layer 功能

Amazon Lambda 问世 4 年了,本周又宣布了重量级功能:Layer。Layer 可以允许不同语言编写的 Function 互相调用。之前,如果有不同的 Funtion 引用了相同的代码,那么就要将代码同引用代码一同打包发布。现在,你可以将被引用部分的代码单独发布,然后在 Function 中引用。Function 可以带有单独的版本控制,这样升级也不会影响所有的代码。

另外公布了 Runtime API,你可以提供一个可执行文件(比如 Python 解释器),然后 Lambda 会处理 Function 的调用和你的解释器执行代码这一层,你可以使用自己定制的 Runtime,也可以使用 AWS 官方制作的 Runtime。Runtime API 将是 AWS Lambda 将来支持新语言的方式。

这里有一个 awesome-layers 整理了一些 Runtime、可以调用的 Layer Function、以及一些监控的 Layer.

工具 twistlock/cloud-discovery

可以列举出你当前使用的所有云服务,原理就是你给它秘钥,它对接了每一个云平台服务商,会给你查询当前运行的各类云服务。举个例子就很明白了:

此工具以 Docker 形式提供,使用方便。主要可以用来审计,如果用了“弹性”的服务,这个工具就很实用了。我觉得可以和监控平台配合起来,每分钟扫描,记录下实时的资源使用。

另外它只使用读的 API,所以可以放心使用(不放心的话还是去看一下源代码)。

它的另一个功能是可以扫描 IP 端和端口,这样可以管理自建的云服务。

工具 dive

可以分析对比每一层 Docker image 都有哪些 diff,再也不用猜着做出更小的镜像了!

The Human Side of Microservices

InfoQ 对一位软件工程师的采访,涉及微服务、软件架构、devops 等话题。

 

SRE&Devops 每周分享 Issue #3

Scaling Spark Streaming for Logging Event Ingestion

在 Airbnb,用户的每一次搜索、预订都会产生 log。这些 log 可以帮助 Airbnb 更懂客户,为顾客和房东提供更好的服务。这些数据也会驱动业务发展和产品迭代——因为他们会反过来影响机器学习的模型和搜索排名。

Airbnb 的日志处理是近乎实时的。客户端和服务器将事件发往 Kafka,Spark Streaming Job 会持续从 Kafka 读出来然后放到 Hbase 中做去重,然后每小时从 Hbase dump 到 Hive 中。

这些日志是很多图表和控制台的元数据,所以保证这些数据的实时性和 SLA 就很重要。

然而,这些日志事件的量无法预估(由于促销、假期或者其他原因),而且事件会有偏斜(大小不一致),而 Kafka 的 partition 又不能自动水平扩展。这样就有很多挑战(具体见文),比如 HBase down 了一段时间,恢复之后无法给 Spark 分更多的资源,就要花很多时间追平。

另一方面,读日志最终还是存到 Hbase 中,所以时序并不重要,据此可以重新设计模型。Airbnb 开发出负载均衡的 Kafka reader,最终解决了文中描述的所有问题,可以水平扩展 reader。

Linux 系统网络管理员最常用的 32 个 Nmap 命令

Nmap 是一个开源的网络安全软件,它使用 IP 包来探测,可以统计网络上的 Hosts,这些 Hosts 提供的服务,运行的操作系统,安全、防火墙规则等。使用在监控方面非常方便。

Nginxconfig.io

一个可以自动根据你的选择生成 Nginx Conf 文件的网站。

Kubernetes Cheat Sheet

Red Hat 的有关 k8s 的介绍,以及如何在本地安装。

Starting and Scaling DevOps in the Enterprise

Gitlab 首页推荐的一本书,Devops 在大型企业的实践。

Cheat sheet: Linux networking

Linux 常用网络命令速查表

Ansible2.7: reboot plugin

Ansible2.1 就有了 win_reboot,因为 windows 重启的需求比 Linux 更频繁。Linux 的重启也可以通过 shell wait_for 来实现。但是有很多坑需要注意。

被折磨很久之后,reboot for linux 终于出现了。本文介绍了 reboot  这个 plugin 的设计、实现和用法。

Java 微服务:容器与框架的介绍 (By RedHat)

Orchestrating Chaos using Grab’s Experimentation Platform

Grab 做 Chaos 的经验分享。

Capacity planning for Etsy’s web and API clusters

Etsy 做容量规划的经验分享。

The Datacenter As A Computer: Designing Warehouse-Scale Machines, Third Edition

又名:Google 是如何建造数据中心的。第三版。读完可能要花一些力气,不过里面的图很酷炫。

 

SRE&Devops 每周分享 Issue #2

Python in RHEL 8

Red Hat 上周发布了 RHEL8,这是官网发布的一篇博客,讲了在 RHEL8 上面使用 Python 的一些注意事项,非常值得一读。

10 年前,Python 社区决定摆脱技术债,发布一个不向后兼容的版本 Python3,但是他们显然低估了这门语言的流行程度和带来的兼容性问题。

在 RHEL8 上面,Python3.6 是默认版本,但是也没有在系统默认安装,需要 yum install python3 。Python2 也是可以通过 yum 安装的: yum install python2 。

但是不带版本的命令 python 会导致 command not found 。因为现在有两拨人,一波认为默认 Python 应该是 Python2,用 Python3 的话应该显式键入 Python3。但是另一拨人应该向前看,默认 Python 应该换成 Python3 了。话说笔者在这周就遇到一个因为 Python 指向 Python37 导致 build 失败的 bug。在这方面,RHEL8 保留观点,默认的 Python 不指向任何命令,用户必须显示的写明版本(这样脚本也可以在任何版本的 Linux 下工作)。

既然 RHEL8 不自带 Python,那 yum 又是 Python 写的,yum 是怎么运行的呢?这个问题就留给读者自己去阅读此文吧。除此之外,这篇文中还写了如何正确使用 pip,如何正确使用 virtualenv,可以说都是一些“最佳实践”,非常值得一读。

Overload Control for Scaling WeChat Microservices

这是 the morning paper 推荐的一篇 Paper,非常值得一读。介绍了微信微服务的流量过载控制系统 DAGOR。此系统已经微信生产环境中服役 5 年,可谓身经百战。过载控制系统可以在当系统过载的时候有效的保护后端的系统,微服务的开发者很难正确估计真正生产流量,所以将过载控制系统从服务逻辑中解耦出来就非常重要。这篇 Paper 可能在这方面给你一些启发。

Analyzing the GitHub outage

非官方的,一篇有关 Github 10月故障的分析。

Some notes on running new software in production

当你想在生产环境使用新的软件时(我觉得新 lib 也差不多),应该花多少时间去了解你要用的这个东西?应该要了解到什么程度?此文是一篇不错的参考。(ps:Jvns 的博客有口皆碑)

工作、生活、side-project 和学习的平衡

是不是感觉很难做着一份工作的同时还保持学习新的技术?没时间去完成自己的 idea?甚至完全没有自己的生活了?

这篇帖子给出了很多有用的建议:

  1. 尽量减少通勤时间
  2. 保证锻炼和休息的时间
  3. 集中精力
  4. 每天进步一点点
  5. 理智的选择社交生活
  6. 接收事情要花很长时间,不要总是想 ALL IN、把其他事情放到一边。慢慢来,即使某件事要花上十年。不要期望某个事情会从一个时间马上就发生改变,接受事情是慢慢做成的。

Travis CI <3 Honeycomb

 本文介绍 Honeycomb 给 Travis 带来的巨大价值。Travis 一周会运行超过 300 万次 build,在这么大的流量中,如果没有足够的数据的话,调试某一次 build 问题,或者接口问题是很困难的。Travis 在没有 Honeycomb 之前是使用几个 Metric(四条黄金监控)来监控线上的服务状态的。但是这样的问题是,没办法知道某一次 outage 影响了多少用户,耗时高的流量是从哪里来的。也就是粒度不够。第二个资源是日志,日志是原始的信息,动辄好几屏,如果没有好的工具,很难使用日志。

Honeycomb 就是这样的工具,以前无法使用的庞大数据,用 Honeycomb,只需要点几下鼠标就可以了。文章举了一个例子,某一个用户(很可能是 bot)持续访问一个非常慢的 API,拖慢了整个接口耗时。有了 Honeycomb,Travis 很快定位出了这个用户,并确定这个是没有对其他用户造成影响的。要是没有 Honeycomb,可能要排查日志、定位、评估很长时间。

在生产环境中部署 Django Channel

本文介绍了在生产环境中部署 ASGI 应用和 WSGI 应用的方法(分别分开部署的)。包括 Nginx、gunicorn、daphne 的部署等。还有 websocket 的压测方法。放在书签里以后可能会有用:)

Introducing PyEnvDiff-lib and a Hub & Spoke Python Environment Diff Tool

本周正好遇到一个 flake8 的结果本地和 CI 不一致的情况,搞了半天发现是本地有一个全局的 config (啥时候写的我自己都忘了)。所以类似的工具可能可以解决这种问题。

The ultimate DevOps hiring guide

《Devops 雇佣指南》给雇主和寻找工作的 devops 的建议。

Highly Available Microservices with Health Checks and Circuit Breakers

介绍了几种负责均衡的方案,和健康检查、熔断的方法。很好的一篇入门文章,后面介绍了 Kong 的解决方案和使用方法。个人很看好这个项目!