论坛的各位大神,章大哥,你们好,请教大家一个nginx 长连接的问题

524 views
Skip to first unread message

xiati...@gmail.com

unread,
Oct 27, 2014, 8:10:28 AM10/27/14
to open...@googlegroups.com, age...@gmail.com
大家好:
    
       客户端发送请求给nginx,nginx解析之后,把数据发给我后端的tcp服务器,用的是tcp协议,现在问题来了,我希望nginx跟我的后端tcp服务器维持一个长连接的通信,那么我使用这几个函数进行长连接到我的tcp服务器。

       
local sock = ngx.socket.tcp()

sock:settimeout(10000)   -- one second

local ok, err = sock:connect("127.0.0.1", 13900)

if not ok then
    ngx.say("failed to connect: ", err)
    return
end

local bytes, err = sock:send("i am nginx")
if not bytes then
    ngx.say("failed to send query: ", err)
    return
end
 
local chunk,err = sock:receive()

if not chunk then
    ngx.say("failed to receive a chunk: ", err)
    return
end
 
ngx.log(ngx.DEBUG,"result: ", chunk)


local ok, err = sock:setkeepalive(0, 1)

if not ok then
    ngx.say("failed to put the connection into pool "
        .. "with pool capacity 500 "
        .. "and maximal idle time 60 sec")
    return
end

我把sock:setkeepalive的连接数设置为1,就是希望连接池里面只有一个长连接进行通信,数据都是通过唯一的一个连接,进行发送和回复。

但是发现我根本不能建立起长连接,就是nginx一段时间后会自动断开和我的服务器进行连接,请问我这种设计方式是否有问题,如果有问题请问我要怎么改进,非常感谢大家在百忙之中给予回复。。。

Yichun Zhang (agentzh)

unread,
Oct 27, 2014, 4:02:13 PM10/27/14
to xiati...@gmail.com, openresty
Hello!

2014-10-27 5:10 GMT-07:00 xiatian1071:
> 我把sock:setkeepalive的连接数设置为1,就是希望连接池里面只有一个长连接进行通信,数据都是通过唯一的一个连接,进行发送和回复。
>
> 但是发现我根本不能建立起长连接,就是nginx一段时间后会自动断开和我的服务器进行连接,

你 100% 地确定是 nginx 一侧主动断开的连接?而不是你的 TCP 服务器或者网络链路上的其他东西断开的连接?

建议在你的 nginx 服务器一侧使用 tcpdump 或者 wireshark 之类的工具在网络层面上监视包的细节情况。

同时临时开启 nginx 的调试日志也可能有助于诊断分析:

http://nginx.org/en/docs/debugging_log.html

Regards,
-agentzh

P.S. 另外,建议发送给 openresty 的邮件最好不要同时再抄送给我的个人信箱。谢谢配合!

xiati...@gmail.com

unread,
Oct 29, 2014, 2:57:24 AM10/29/14
to open...@googlegroups.com, xiati...@gmail.com
章大哥:你好

      在nginx里面我用代理的方式连接到我自己写的tcpserver,后端用upstream keepalive作为长连接的方式,但是很奇怪的是,我给我的tcp server
发数据的时候,nginx发了两次,一次是tcp的数据,一次是发送了0个字节,也就是我的tcp sever总共收到两次数据,一次是tcp的数据,一次是rece是0,请问这是什么问题。

在 2014年10月28日星期二UTC+8上午4时02分13秒,agentzh写道:

liuwei

unread,
Oct 29, 2014, 7:34:13 AM10/29/14
to open...@googlegroups.com
receive 0是说明tcp对端关闭了,并不是nginx发了两次。相当于nginx发给你前面的数据,然后关闭这个tcp连接。你就会先收到数据,再调receive就是返回0。

--
--
邮件来自列表“openresty”,专用于技术讨论!
订阅: 请发空白邮件到 openresty...@googlegroups.com
发言: 请发邮件到 open...@googlegroups.com
退订: 请发邮件至 openresty+...@googlegroups.com
归档: http://groups.google.com/group/openresty
官网: http://openresty.org/
仓库: https://github.com/agentzh/ngx_openresty
教程: http://openresty.org/download/agentzh-nginx-tutorials-zhcn.html

liuwei

unread,
Oct 29, 2014, 12:49:22 PM10/29/14
to open...@googlegroups.com
hi,你好,我测试了你的代码,我发现你是调用 sock:receive() 函数接收后端的数据。对于这个函数,如果你不指定receive的大小,那么后端发过来的数据一定要直到有换行符或者buf满,receive函数才返回。如果你后端返回的数据没有以换行符结尾,函数就会一直不返回,这个连接就可能会因为超时断掉。因此就有了你观察到的ngx主动断开你的连接的现象。

我估计Lua sock这样设计的原因是为了很好地支持异步,receive得到的数据都存放在一个缓冲之中,只有当缓冲满了或者遇到换行符,才返回,否则就继续做其他协程的IO操作。如果你的后端返回的数据不以\n结尾,想要支持二进制的话,我想到的一个办法是你循环调用sock:receive(1),在自己的代码中用一个状态机来检查是否收完了。不知道春哥以及其他同仁们是否有更好的办法用lua的非阻塞sock来接收二进制数据?

xiati...@gmail.com

unread,
Oct 29, 2014, 10:14:54 PM10/29/14
to open...@googlegroups.com, stup...@126.com
hi,liuwei:

     感谢你的回复,确实你说的很对, receive 的时候因为我发来的数据没有结束符,所以一直在那里recv到time out 之后连接就断开了。现在我想请问一下,如果我发的是http协议的数据,那么我希望一次recv的时候收到整个包,recv的之前是不知道http整个包的长度的,我应该用什么结束符来结束recv。是先(/r/n)取到头部,去解析body的长度吗?还有就是春哥的lua sock是否有其他好的建议来接收二进制的协议。

在 2014年10月30日星期四UTC+8上午12时49分22秒,liuwei写道:

Yichun Zhang (agentzh)

unread,
Oct 29, 2014, 11:00:20 PM10/29/14
to openresty
Hello!

2014-10-29 9:49 GMT-07:00 liuwei:
> 如果你的后端返回的数据不以\n结尾,想要支持二进制的话,我想到的一个办法是你循环
> 调用sock:receive(1),在自己的代码中用一个状态机来检查是否收完了。不知道春哥以及
> 其他同仁们是否有更好的办法用lua的非阻塞sock来接收二进制数据?

二进制数据也会有一定的格式,比如 lua-resty-mysql 库和 lua-resyt-dns 库都是使用的 cosocket API
来接受的 mysql 协议和 DNS 协议的二进制数据包。可以参考一下。cosocket 的 API 沿裔自 LuaSocket 的
API,该 API 的理念是避免使用复杂的显式状态机来处理数据包的分段。

当然有一种场景 BSD socket 的 recv() 的语义还是有用的,那就是不考虑数据的格式的 TCP
代码,来一包就转发一个包,而完全不管数据流的末尾和格式。以色列的 aviramc 同学在下面这个 pull request 中为
ngx_lua 模块的 cosocket API 添加了 bsdrecv() 方法正是用于此目的:

https://github.com/openresty/lua-nginx-module/pull/290

我一直没有找到时间合并进主干。

2014-10-29 19:14 GMT-07:00 <xiati...@gmail.com>:
> 感谢你的回复,确实你说的很对, receive 的时候因为我发来的数据没有结束符,所以一
> 直在那里recv到time out 之后连接就断开了。现在我想请问一下,如果我发的是http协议
> 的数据,那么我希望一次recv的时候收到整个包,recv的之前是不知道http整个包的长度
> 的,我应该用什么结束符来结束recv。

HTTP 协议本身清楚地指示了数据流的边界(通过 chunked encoding 或者 Content-Length 响应头)。即使是
HTTP 1.0 协议中不带 Content-Length 响应头的情形,也可以通过 receive("*a")
读到连接断开为止。你可以考虑使用现成的某一个 lua-resty-http* 库,例如 Brian Akins 的
lua-resty-http-simple 库:

https://github.com/bakins/lua-resty-http-simple

Regards,
-agentzh

liuwei

unread,
Oct 29, 2014, 11:05:16 PM10/29/14
to open...@googlegroups.com
我觉得你得先用一个状态机来接收HTTP的头部,然后解析出Content-Length,然后再接收body。你可以参看ngx源码里面http状态机的部分(nginx-xxx/src/htttp/ngx_http_parse.c),然后去自己用实现lua实现。昨天我也拜读了一下春哥的lua_resty_(redis/mysql/memcache)模块,春哥的实现也是去先接收协议头部,然后解析出数据长度,再去接收定长数据,再接收结尾的/r/n。所以我觉得你要么自己去写http状态机,要么去找个lua的http解析器来用。其实既然你用tcp去连到后端的服务,那为什么要用http协议呢?你完全可以自己封装一个简单的协议嘛。

xiati...@gmail.com

unread,
Oct 31, 2014, 10:32:34 PM10/31/14
to open...@googlegroups.com, stup...@126.com
还有一个问题就是我在content_by_lua的时候都会去连接我的tcp服务器,之后
通过这个函数

local ok, err = sock:setkeepalive(0, 1)

if not ok then
    ngx.say("failed to put the connection into pool "
        .. "with pool capacity 500 "
        .. "and maximal idle time 60 sec")
    return
end

把这个连接放到连接池里面,这个连接池只有一个连接。这样子设计在nginx多进程的模式下是否有问题?长连接是否有问题?

在 2014年10月30日星期四UTC+8上午11时05分16秒,liuwei写道:

Yichun Zhang (agentzh)

unread,
Nov 2, 2014, 4:53:12 PM11/2/14
to openresty
Hello!

2014-10-31 19:32 GMT-07:00 <xiati...@gmail.com>:
> 还有一个问题就是我在content_by_lua的时候都会去连接我的tcp服务器,之后
> 通过这个函数
>
> local ok, err = sock:setkeepalive(0, 1)
>
[...]
>
> 把这个连接放到连接池里面,这个连接池只有一个连接。这样子设计在nginx多进程的模式下是否有问题?长连接是否有问题?
>

我没看出来有什么问题,不过把连接池的最大容量置为 1 似乎过于保守了一些。

值得一提的是,nginx 的多 worker 进程模型并不像 Apache prefork mpm 或者 php-fpm 那样,即并不是一个
worker 进程对应一个下游连接。nginx 的多 worker 进程只是为了用满多个 CPU 核而已,每个 worker
进程可以处理很多的并发连接。ngx_lua 的连接池和 nginx 核心中针对 upstream 模块的连接池一样是在每一个 worker
进程的级别上由许多并发连接共享的。见下面这张图:

http://agentzh.org/misc/slides/nginx-conf-2014/images/pool.png

Regards,
-agentzh

xiati...@gmail.com

unread,
Nov 3, 2014, 4:09:33 AM11/3/14
to open...@googlegroups.com

谢谢各位的回复,

      我也按照这种方式去做,发送包sock:send 之后用sock:receive('\r\n')去接收回复的,但是发送任务正常返回后,我调用set_keepalive(0, 1)想将连接放入连接池,

但是在日志中显示:

failed to set keepalive: connection in dubious state


请问这种问题是什么原因造成的。

在 2014年11月3日星期一UTC+8上午5时53分12秒,agentzh写道:

Yichun Zhang (agentzh)

unread,
Nov 3, 2014, 2:39:16 PM11/3/14
to openresty
Hello!

2014-11-03 1:09 GMT-08:00 xiatian1071:
> 我也按照这种方式去做,发送包sock:send
> 之后用sock:receive('\r\n')去接收回复的,

receive() 方法并不接受 "\r\n" 作为参数,见对应的官方文档:

https://github.com/openresty/lua-nginx-module#tcpsockreceive

> 但是发送任务正常返回后,我调用set_keepalive(0, 1)想将连接放入连接池,
> 但是在日志中显示:
> failed to set keepalive: connection in dubious state
>

请见官方文档中对此错误消息的描述:

https://github.com/openresty/lua-nginx-module#tcpsocksetkeepalive

"When the system receive buffer for the current connection has unread
data, then this method will return the "connection in dubious state"
error message (as the second return value) because the previous
session has unread data left behind for the next session and the
connection is not safe to be reused."

Regards,
-agentzh

xiati...@gmail.com

unread,
Nov 28, 2014, 2:53:44 AM11/28/14
to open...@googlegroups.com

长连接的问题:

大家好,我现在遇到这么一个问题,就是
之后,是可以保持长连接,客户端同时给nginx发送请求的时候,通过tcp像后端tcp服务器发送消息,当同时发送的时候,发现收数据的时候超时,是不是在tcp连接这里需要加锁才行,多谢各位的回答

在 2014年10月27日星期一UTC+8下午8时10分28秒,xiati...@gmail.com写道:

Yichun Zhang (agentzh)

unread,
Nov 28, 2014, 8:55:09 PM11/28/14
to openresty
Hello!

2014-11-27 23:53 GMT-08:00 xiatian1071:
> 之后,是可以保持长连接,客户端同时给nginx发送请求的时候,通过tcp像后端tcp服务器发送消息,当同时发送的时候,发现收数据的时候超时,是不是在tcp连接这里需要加锁才行,多谢各位的回答
>

一般并不需要加锁,除非你一个请求里有多个轻量级线程同时往一个 cosocket
对象进行不兼容的操作(比如两个轻线程同时写入,或者同时读取,或者一个读取,一个重新连接,这些都会导致其中一个操作返回 socket busy
这样的错误,但不会超时。值得一提的是,一个线程读另一个线程写同一个 cosocket 是可以的,因为 TCP 是全双工的协议)。

超时一般是因为你错误地使用了 receive() 方法。比如,当不带任何参数调用时,receive() 必须看到换行符才会返回。建议仔细阅读
cosocket 的官方文档:

https://github.com/openresty/lua-nginx-module#tcpsockreceive

注意该函数的语义并不同于经典的 BSD 风格的 socket recv() 函数。

同时可以配合 tcpdump 或者 wireshark 这样的抓包工具在网络层进行检查。

Regards,
-agentzh

xiati...@gmail.com

unread,
Nov 28, 2014, 10:10:25 PM11/28/14
to open...@googlegroups.com, age...@gmail.com
我是在一个请求里面这样发送请求的

local sock = ngx.socket.tcp()

sock:settimeout(100000)  

local ok, err = sock:connect(ip, port)

if not ok then
  ngx.log(ngx.ERR,"failed to connect: ", err)
  return nginx_resp_op.nginx_resp(200,"failed")
end

local data_json = body_data
local data_json_len = string.len(body_data)

ngx.log(ngx.DEBUG,"data_json:"..data_json)

local send_data = "$"..data_len.."\r\n"..data.."\r\n"

local bytes, err = sock:send(send_data)

if not bytes then
  ngx.log(ngx.ERR,"failed to send query: ", err)
  return nginx_resp_op.nginx_resp(200,"failed")
end

ngx.log(ngx.DEBUG,"send data:"..send_data)

local line, err = sock:receive()

if not line then
  if err == "timeout" then
    ngx.log(ngx.ERR,"recv timeout")
    sock:close()
  end
  ngx.log(ngx.ERR,"recv time out")
  return nginx_resp_op.nginx_resp(200,"failed")
end

local tcpdata  = nil
local prefix = byte(line)

ngx.log(ngx.DEBUG,"tcp prefix:"..prefix)

if prefix == 36 then    -- char '$'
  local size = tonumber(sub(line, 2))
  if size < 0 then
    ngx.log(ngx.ERR,"recv size < 0")
    return nginx_resp_op.nginx_resp(200,"failed")
  end
  ngx.log(ngx.DEBUG,"size:"..size)
  tcpdata, err = sock:receive(size)
  if not tcpdata then
    if err == "timeout" then
      sock:close()
    end
    return nginx_resp_op.nginx_resp(200,"failed")
  end
  ngx.log(ngx.DEBUG,"recv from tcp server:"..tcpdata)
  
  local dummy, err = sock:receive(2)
  if not dummy then
    ngx.log(ngx.ERR,"it is not /r/n")
    return nginx_resp_op.nginx_resp(200,"failed")
  end
  ngx.log(ngx.DEBUG,"deal packet finish")
else
   ngx.log(ngx.ERR,"unkown prefix:".. prefix)
   return nginx_resp_op.nginx_resp(200,"failed")
end

local ok, err = sock:setkeepalive(0,1)

if not ok then
  ngx.log(ngx.DEBUG,"keepalive error:"..err)
else
  ngx.log(ngx.DEBUG,"set keepalive success")
end
ngx.log(ngx.DEBUG,"send to client data:"..tcpdata)
return nginx_resp_op.nginx_resp(200,tcpdata)

收的话,像redis协议那样子,去收,如果是一个请求一个请求的发送是没问题,但是在很多请求同时并发的时候,收数据,有时候总是在recv那里停住了,这是怎么回事呢。


在 2014年10月27日星期一UTC+8下午8时10分28秒,xiati...@gmail.com写道:

Yichun Zhang (agentzh)

unread,
Nov 28, 2014, 10:23:18 PM11/28/14
to xiati...@gmail.com, openresty
Hello!

On Fri, Nov 28, 2014 at 7:10 PM, xiatian1071 wrote:
> 收的话,像redis协议那样子,去收,如果是一个请求一个请求的发送是没问题,但是在很多请求同时并发的时候,收数据,有时候总是在recv那里停住了,这是怎么回事呢。
>

也许你的 TCP 后端的吞吐量已经超过极限了。当然,你提供的信息过少,不足以进行任何有意义的诊断,超过后端的吞吐量极限是常见的一种原因。

Regards,
-agentzh
Reply all
Reply to author
Forward
0 new messages