<h3 id="问题概述">问题概述</h3><p>在一次处理反馈问题过程中发现lua-nginx-module的balancer语法在keepalive场景下存在bug,当nginx通过已经建立的回源连接发送请求给原服务器时,连接被上游服务器主动关闭并保证再次发送这个请求时同样会被关闭(触发waf规则或者其他意外关闭),会造成nginx不断创建新的连接。</p>

验证程序

http server, 模拟上游服务器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import SocketServer
import datetime


def make_resp():
GMT_FORMAT = '%a, %d %b %Y %H:%M:%S GMT'
now = datetime.datetime.utcnow()

content = '''
<html>
<head><title>simpleserver</title></head>
<body>
<h1>Hello, World!</h1>
</body>
</html>
'''


response = '''HTTP/1.1 200 OK
Server: %s
Date: %s
Content-Type: text/html;charset=utf8
Content-Length: %s
Connection: keep-alive

%s''' % (

'simpleserver',
now.strftime(GMT_FORMAT),
len(content),
content
)

return response

count = 0

class MyTCPHandler(SocketServer.BaseRequestHandler):
def handle(self):
global count
while True:
self.request.recv(1024)
count += 1
if count == 1:
resp = make_resp()
self.request.sendall(resp)
else:
print count
break


if __name__ == "__main__":
HOST, PORT = "localhost", 8080
server = SocketServer.TCPServer((HOST, PORT), MyTCPHandler)
server.serve_forever()

nginx 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
http {
upstream backend {
server 0.0.0.1;

balancer_by_lua_block {
local balancer = require "ngx.balancer"
local host = "127.0.0.1"
local port = 8080

local ok, err = balancer.set_current_peer(host, port)
}

keepalive 1;
}

server {
listen 80;
proxy_next_upstream off;
location / {
proxy_pass http://backend$request_uri;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}

}

两次访问127.1触发bug,同时对比了proxy模块的keepalive,相同场景,会返回502.

问题分析与修复

为了便于分析,在配置文件中将 work_processes 设为1,打开gdb调试worker process。发现进程在循环调用

1
ngx_http_upstream_next(ngx_http_request_t *r, ngx_http_upstream_t *u, ngx_uint_t ft_type)

这个函数是在nginx回源请求出错时调用的。单步跟,发现一个重要分支:

1
2
3
if (u->peer.cached && ft_type == NGX_HTTP_UPSTREAM_FT_ERROR
&& (!u->request_sent || !r->request_body_no_buffering))
{

当if条件为真时,nginx会重新发送连接,为假时,会最终调用ngx_http_upstream_finalize_request()返回错误信息。所以明确一下条件判断中的各个变量很有必要。

  • ft_type 代表出错类型,这里由于是上游服务器主动关掉了连接,所以是0x00000002即 NGX_HTTP_UPSTREAM_FT_ERROR。
  • u->request_sent 用来标识是否向上游发送过请求,这里已经发送过所以是1。
  • r->request_body_no_buffering 代表nginx是否缓存请求的主体,我们没有配置,那么默认就是0表示缓存。

那么u->peer.cached的值就决定了程序的流程,它表示的是请求所使用的连接的缓存状态。而使用gdb打印这个值,发现在问题场景下这个值一直为1(表示使用的连接是缓存的,显然这是不对的)导致nginx不断重新连接。如图

继续跟u->peer.cached,直到ngx_http_lua_balancer_get_peer(),此时的函数调用堆栈:

1
2
3
4
5
6
7
8
(gdb) bt
#0 ngx_http_lua_balancer_get_peer (pc=0x2678530, data=0x2678ca0) at ../ngx_lua-0.10.0/src/ngx_http_lua_balancer.c:254
#1 0x00000000004d85f9 in ngx_http_upstream_get_keepalive_peer (pc=0x2678530, data=0x2678c68) at src/http/modules/ngx_http_upstream_keepalive_module.c:222
#2 0x000000000044502e in ngx_event_connect_peer (pc=0x2678530) at src/event/ngx_event_connect.c:25
#3 0x0000000000486862 in ngx_http_upstream_connect (r=0x267f190, u=0x2678520) at src/http/ngx_http_upstream.c:1331
#4 0x000000000048c101 in ngx_http_upstream_next (r=0x267f190, u=0x2678520, ft_type=2) at src/http/ngx_http_upstream.c:3913
#5 0x0000000000488215 in ngx_http_upstream_process_header (r=0x267f190, u=0x2678520) at src/http/ngx_http_upstream.c:2106
#6 0x000000000048627c in ngx_http_upstream_handler (ev=0x269f480) at src/http/ngx_http_upstream.c:1095

ngx_http_upstream_get_keepalive_peer 通过

1
rc = kp->original_get_peer(pc, kp->data);

调用ngx_http_lua_balancer_get_peer(ngx_peer_connection_t pc, void data),pc中保存了与上游服务器的连接信息,打印pc的相关信息,如图:

pc 和 ngx_http_upstream_next 中的 u->peer指向了相同的保存后端连接信息的结构体,而在获取新的连接信息后cached并没有被清空,这就是问题所在。对应的解决方法就是显式的清除前一次的连接信息,参考ngx_http_upstream_get_round_robin_peer(此函数是使用proxy模块的keepalvie时ngx_http_upstream_get_keepalive_peer调用的获取后端连接信息的函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static ngx_int_t
ngx_http_lua_balancer_get_peer(ngx_peer_connection_t *pc, void *data)
{
...

ngx_log_debug1(NGX_LOG_DEBUG_HTTP, pc->log, 0,
"lua balancer peer, try: %ui", pc->tries);

+ pc->cached = 0;
+ pc->connection = NULL;

lscf = bp->conf;

...

重新编译nginx和lua module,再次测试后发现问题解决,nginx返回502错误。
此bug在lua module当前发布的最新版本v0.10.1rc1中依然存在,不过由于之前同事已提交过issue,3月1号作者的提交中已fix了这个bug,见这里,修复方案与文中给出的相同,只不过放在了不同位置。
两者并无区别,bp->get_rr_peer指向的是ngx_http_upstream_get_round_robin_peer,而在这个函数中有对上一次连接信息的清理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ngx_int_t
ngx_http_upstream_get_round_robin_peer(ngx_peer_connection_t *pc, void *data)
{
ngx_http_upstream_rr_peer_data_t *rrp = data;

ngx_int_t rc;
ngx_uint_t i, n;
ngx_http_upstream_rr_peer_t *peer;
ngx_http_upstream_rr_peers_t *peers;

ngx_log_debug1(NGX_LOG_DEBUG_HTTP, pc->log, 0,
"get rr peer, try: %ui", pc->tries);

pc->cached = 0;
pc->connection = NULL;

peers = rrp->peers;
ngx_http_upstream_rr_peers_wlock(peers);
...

}

</div>
<footer>
  
    
    



    
  
  <div class="clearfix"></div>
</footer>