当我刚开始使用OpenDNS时,我的首要任务是弄清楚Nginx的工作方式,并为其编写一个自定义C模块来处理一些业务逻辑。 Nginx将反向代理到Apache Traffic Server(ATS),它将执行实际的正向代理。 这是一个简化图:
事实证明,Nginx易于理解和使用。这与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; } }}
也可以将其配置为基于某些变量使用上游,例如Host标头:
http { server { listen 80; location / { proxy_pass http://$http_host$request_uri; } }}
这实际上工作得很好。主要警告是Host标头可以匹配配置中的预定义上游{}(如果存在):
http { ... upstream foo { server bar; } ... server { listen 80; location / { proxy_pass http://$http_host$request_uri; } }}
然后,这样的请求将匹配foo并被代理到bar:
GET / HTTP/1.1Accept: */*Host: foo
可以通过在自定义模块中使用新变量来扩展此方法,而不是使用内置的$ http_host和$ request_uri进行更好的目标控制,错误处理等。
一切工作都非常好-请注意,这是一个HTTP(端口80)代理,在此我们不考虑HTTPS情况。一方面,Nginx无法识别显式HTTPS代理中使用的CONNECT方法,因此将永远无法工作。正如我在之前的博客文章中提到的那样,我们的智能代理通常采用一种更加非常规的方法。
一个大问题是性能。我们使用ATS进行的初始负载测试得出的数据少于理想值。Nginx的“ hack”对其性能有没有影响?
跳过更详细的信息,我们的设置使用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.62Transfer/sec: 108.78MB
一切看起来都不错,wrk按预期创建了100个连接,并设法每秒挤出3万个请求。
现在,让我们通过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.39Transfer/sec: 66.53MB
这几乎使可能的吞吐量减半。
通过手动请求,我们发现通过Nginx并不会真正增加任何明显的延迟。Nginx工作人员在测试期间获得了接近100%的CPU使用率,但是增加工作人员人数并没有多大帮助。
上游情况如何,在两种情况下会看到什么?
快速更新以打印一些统计信息后,在直接情况下一切看起来都很好-wrk和上游服务器报告的数字符合预期。但是,在查看上游服务器统计信息时,我们在代理情况下发现了一些令人吃惊的事情:
status: 552263 connects, 552263 closes, 30926728 bytes, 552263 packets
看起来Nginx为往上游的每个请求创建了一个新连接,尽管wrk仅向下游进行了100个连接…
深入Nginx核心并更全面地阅读文档,事情开始变得有意义。Nginx是一个负载均衡器,其中“负载”等于请求,而不是连接。连接可以发出任意数量的请求,重要的是在后端之间平均分配这些请求。就目前而言,Nginx在每个请求之后关闭上游连接。上游keepalive模块尝试通过始终保持一定数量的持久连接保持打开状态来对此进行轻微补救。Nginx Plus提供了诸如会话持久性(Session Persistence)之类的额外功能(顺便说一句,还存在一个等效的开源模块)—使请求可以更一致地路由到相同的上游。
我们真正想要的是客户端及其各自上游之间的一对一持久连接映射。在我们的案例中,上游是完全任意的,我们要避免创建不必要的连接,更重要的是,不要以任何方式“共享”上游连接。我们的会议是整个客户连接本身。
该解决方案非常简单,我们已经在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.99Transfer/sec: 103.01MB
上游的数字与wrk的数字匹配:
status: 8600 connects, 8600 closes, 47882016 bytes, 855036 packets
但是,仍然存在问题。有8600个连接,而不仅仅是100个。Nginx决定关闭上游和下游的许多连接。当进行调试以查看原因时,我们最终追溯到“ lingering_close_handler”:
...nginx: _ngx_http_close_request(r=0000000000C260D0) from ngx_http_lingering_close_handler, L: 3218nginx: ngx_http_close_connection(00007FD41B057A48) from _ngx_http_close_request, L: 3358...
由于即使使用此行为,整体效果还是令人满意的,因此我暂时不这样做了。
我们已经将Nginx作为生产中的正向HTTP代理运行了一段时间,几乎没有问题。我们希望继续扩展Nginx的功能,并推动新的界限。请留意未来的博客文章和代码片段/补丁。
*这是一个重写的补丁程序(原始补丁有点笨拙),这个新代码最近才投入生产。如果有任何问题,我将进行任何调整以更新公共补丁。