当我刚开始使用OpenDNS时,我的第一个任务是弄清楚Nginx是如何工作的,并为它编写一个自定义C模块来处理一些业务逻辑。Nginx将反向代理到Apache流量服务器(ATS),这将执行实际的向前代理。下面是一个简化的图表:
Nginx很容易理解和使用。这与ATS形成了对比,ATS更大、更复杂,而且一点也不好玩。因此,“为什么我们不直接使用Nginx呢?”成为一个流行的问题,特别是在决定代理将不做任何缓存之后。
转发代理
虽然Nginx是一个反向代理,设计用于明确定义的上游:
http { upstream myapp1 { server srv1.example.com; server srv2.example.com; server srv3.example.com; } server { listen 80; location / { proxy_pass http://myapp1; } } }
也可以配置它使用基于某些变量的上游,如主机标头:
http { server { listen 80; location / { proxy_pass http://$http_host$request_uri; } } }
这实际上很好。主要的警告是主机头可以匹配配置中预先定义的上游{},如果存在:
http { ... upstream foo { server bar; } ... server { listen 80; location / { proxy_pass http://$http_host$request_uri; } } }
然后像这样的请求将匹配foo并代理到bar:
GET / HTTP/1.1 Accept: */* Host: foo
这种方法可以通过在自定义模块中使用新变量进行扩展,而不是使用内置的$http_host和$request_uri来实现更好的目标控制、错误处理等。
这一切都工作得很好-注意,这是一个HTTP(端口80)代理,我们不考虑HTTPS的情况在这里;首先,Nginx不能识别在显式HTTPS代理中使用的CONNECT方法,所以它永远不会工作。正如我在之前的博客文章中提到的,我们的智能代理通常采用一种更非常规的方法。
一个大问题是性能。我们最初使用ATS进行负载测试,得到的结果并不理想。这种Nginx“黑客”对它的表现有什么影响吗?
负载测试
跳过更详细的细节,我们的设置使用wrk作为负载生成器,使用自定义C程序作为上游。定制的上游很基础;它所做的只是接受连接并使用静态二进制blob对任何类似HTTP的请求进行应答。从来不会显式地关闭连接,以消除来自不必要的额外TCP会话的结果中的任何潜在歪斜。
我们首先建立一个基准直接加载上游服务器:
Running 30s test 10 threads and 100 connections Thread Stats Avg Stdev Max +/- Stdev Latency 3.27ms 680.48us 5.04ms 71.95% Req/Sec 3.21k 350.69 4.33k 69.67% 911723 requests in 30.00s, 3.19GB read 100 total connects (of which 0 were reconnects) Requests/sec: 30393.62 Transfer/sec: 108.78MB
一切看起来都很好,wrk按预期创建了100个连接,并设法每秒挤出30k个请求。
现在让我们重复,通过我们的Nginx转发代理(2工人):
Running 30s test 10 threads and 100 connections Thread Stats Avg Stdev Max +/- Stdev Latency 6.42ms 14.37ms 211.84ms 99.50% Req/Sec 1.91k 245.53 2.63k 83.75% 552173 requests in 30.00s, 1.95GB read 5570 total connects (of which 5470 were reconnects) Requests/sec: 18406.39 Transfer/sec: 66.53MB
这几乎是可能的吞吐量的一半。有些事不对劲。
通过一些手动请求,我们可以看到通过Nginx不会增加任何显著的延迟。Nginx工作人员在测试期间的CPU使用率接近100%,但是提高工作人员数量并没有多大帮助。
那么上游呢?在这两种情况下它看到了什么?
在快速更新打印一些统计数据之后,一切看起来都很好——wrk和上游服务器报告的数字与预期相符。但我们发现一些惊人的代理情况时,看看上游服务器的统计:
状态:552263连接,552263关闭,30926728字节,552263数据包
看起来Nginx为上行的每一个请求创建了一个新的连接,即使wrk只创建了100个下行的连接
深入Nginx核心并更彻底地阅读文档,事情开始变得有意义了。Nginx是一个负载均衡器,其中“负载”等于请求,而不是连接。一个连接可以发出任意数量的请求,重要的是在后端之间平均分配这些请求。按照目前的情况,Nginx会在每个请求之后关闭上游连接。上游的keepalive模块通过在任何时候保持一定数量的持续连接处于打开状态来稍微解决这个问题。Nginx Plus提供了额外的功能,比如会话持久性(顺便说一下,还有一个等价的开源模块)——使请求能够更一致地路由到相同的上行流。
我们真正需要的是客户端和它们各自的上行流之间的1对1持久连接映射。在我们的例子中,upstreams是完全任意的,我们希望避免创建不必要的连接,更重要的是不以任何方式“共享”上游连接。我们的会话是整个客户端连接本身。
补丁
解决方案相当简单,我们已经在Github*上提供了它。
重新运行负载测试与此变化,我们得到了更好的结果,概述了保持TCP连接持久和避免那些昂贵的打开/关闭的重要性:
Running 30s test 10 threads and 100 connections Thread Stats Avg Stdev Max +/- Stdev Latency 10.82ms 48.67ms 332.65ms 97.72% Req/Sec 3.00k 505.22 4.46k 95.81% 854946 requests in 30.00s, 3.02GB read 8600 total connects (of which 8500 were reconnects) Requests/sec: 28498.99 Transfer/sec: 103.01MB
上游数量与wrk一致:
状态:8600连接,8600关闭,47882016字节,855036数据包
然而,仍然存在一个问题。有8600个连接,而不是100个;Nginx决定关闭很多上下游连接。在调试的时候,我们会追踪到“lingering_close_handler”:
…
nginx: _ngx_http_close_request(r=0000000000C260D0) from ngx_http_lingering_close_handler, L: 3218
nginx: ngx_http_close_connection(00007FD41B057A48) from _ngx_http_close_request, L: 3358
…
由于即使使用这种行为,总体性能也是令人满意的,所以我暂时把它放在这里。
结束
我们在生产中运行Nginx作为一个转发HTTP代理已经有一段时间了,几乎没有问题。我们希望继续扩展Nginx的能力,并推进新的边界。留意未来的博客文章和代码片段/补丁。
*这是一个重写的补丁(原来的有点粗糙),这个新代码最近才投入生产。如果出现任何问题,我将更新任何调整的公共补丁。
本文:
讨论:请加入知识星球【首席架构师圈】或者飞聊小组【首席架构师智库】
- 登录 发表评论
- 265 次浏览
最新内容
- 2 days 8 hours ago
- 2 days 10 hours ago
- 2 days 10 hours ago
- 5 days 2 hours ago
- 5 days 9 hours ago
- 5 days 10 hours ago
- 5 days 10 hours ago
- 5 days 10 hours ago
- 1 week 2 days ago
- 1 week 2 days ago