New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
socket处于close_wait状态无法关闭 #1358
Comments
尝试改了一下。这里有多个问题,其中 1, 2 是遗留问题,之前的版本处理关闭比较简单粗暴,掩盖了它们。3 是最近的修改考虑不周的地方。
|
经过简单的测试,发现通过 socket api 正常在一个服务内处理的关闭流程是 ok 的,但是如果通过 skynet.core 的 redirect 重定向接口交给别的服务处理后续流程包括关闭,依然有概率出现上述无法关闭的情况。暂时没有详细跟踪。等后续有时间了再跟踪看看是不是哪里不一样。谢谢云大。 |
无法关闭的情况,还需要核查 lua 层面最终有没有调用到 socket.close 。过去的版本可以不调用,底层会自动关闭;现在的版本必须调用。ps . 如果服务本身关闭,会由 gc 触发。 |
如果 fd 的所有权在不同服务间传递,需要保证最终被关闭。这部分还需要仔细检查。 |
@rangercyh 想了一下。可能是因为 socket fd 的所有权在服务间转移是一个异步过程,所以可能存在已经转移出去的 fd 收到了 CLOSED / ERR 消息(正好在转移期间发生)。所以库有责任关闭这些 fd 。而转移目标服务,会在 socket.start 时发现 error 。这个时候它也会关闭 fd 。多次关闭同一个 fd ,目前的实现是安全的。(和标准库的行为不同,fd 几乎不会复用) |
运行./skynet ./examples/config,再运行客户端./3rd/lua/lua ./examples/client.lua(nc也一样出现), 关闭后,还是出现相同的现象: 客户端端口处于 FIN_WAIT2 状态,skynet的端口处于 CLOSE_WAIT 状态。
|
我后来也仔细看了下我写的服务为什么会出现无法close的情况。就像云风说的,在fd从一个服务转移到另一个服务的期间,socket收到了FIN信号,而我的服务写法类似gateserver,也是注册了socket类型协议,自己处理socket的那7种类型的消息,我发现以前因为skynet对于关闭的粗暴对待,我使用socketdriver这类api处理起来比较容易,现在因为存在半关闭状态了,直接使用socketdriver接口来编写处理socket类型协议的响应函数就很要小心,一不留神可能就遗漏了。所以我现在都改成直接使用skynet在lua层封装的socket api来处理,至于socket的网络开启收发关闭流程就让这个封装自己处理了。 另外我也思考了一下,像skynet现在这种半关闭的处理流程,其实在大部分网络应用中并不太常见,主要原因还是因为这其实可能造成关闭发起方的资源泄漏,变相成为一种反向的ddos攻击,如果skynet一直不给对方回发FIN包,让对方处于FIN_WAIT2状态,就很容易形成泄漏,而skynet这边自身如何处理自己的端口状态,就看skynet自己的考虑了。我观察大部分的网络应用一般对待tcp连接的关闭还是很谨慎粗暴的,基本是收到FIN包就双向关闭释放资源,类似skynet之前的处理流程。云风因为要应对一些web那种类似long polling的应用想做这种半关闭,总感觉有点危险。。。。如果可能希望能够有个开关来屏蔽掉这个特性。。毕竟大部分skynet应用对面的还是游戏:D |
之前 gateserver 模板做的不完善,我认为需要更妥善的处理。在上一个提交中,我修改为:
|
目前如果不是深度定制的话,还是比较安全的。 假设是使用 gate 模板,那么基本上不会漏掉。模板会兜底。 在使用的时候,只要遵循常规的原则:read 到 0 (收到 FIN) 后,就调用 socket.close 应该就不会出错。 |
fix half close issue, cloudwu#1358
"如果没注册 handler.disconnect ,则主动关闭" 这条有点画蛇添足。在 037c3a5 中去掉。
如果是情况 2 ,不应该越俎代庖去关闭连接。因为这种情况下,可能有复杂业务要处理,或者是 long polling 模式,需要等待到可以回应时再关闭。而发起请求方则可以先发送 FIN ,半关闭连接。 |
使用新版代码,连接网易云的TDSQL-C(mysql兼容版),会大概率出现sql语句执行无限期阻塞等待的情况,看到的现象是:数据库这边看连接已经断开了,skynet断打印的日志看,socket还没断开,所以就等待回包阻塞住了。不知道是不是同个原因,求帮助,游戏卡在这个问题不能上线。 |
目前的办法是每隔30分钟调用一下db:disconnect(),然后再次mysql.connect出一个新的,暂时解决,但是很别扭,不知道会不会有影响 |
tcpdump 看看断开过程的握手情况,两边的 FIN 和 ACK 的交换情况? |
根据描述,是远端(TDSQL-C) 主动断开了连接。如果在 lua 里测试,看看 阻塞住的地方是,提起一个请求长时间没有回应;还是长时间没有提请求,再提请求时没有检测到连接已经断开? |
好的,我再测试一下,之前的现象是:远端断开了连接,但是skynet这边认为还没断开,实际上连接不存在了,在调用mysql:query接口的时候,看到socket.lua中的socket.disconnected返回的是还没断开,所以mysql这边就一直阻塞。 |
我给 socketchannel 增加了收到对端关闭消息(如果无正在处理的请求)就立刻主动把 fd 关闭的机制 4fdf28d 。 @xuzhenyu2020 这并不会修复无法收到对方 FIN 消息的问题,只是提前了收到 FIN 后 close fd 的时机。 你这个问题最好能排查一下对端服务器是否有发送 FIN ,以及底层是否收到。(用 tcpdump) 如果有条件,可以在 C 代码中增加 log 帮助排查问题: https://github.com/cloudwu/skynet/blob/master/skynet-src/socket_server.c#L1801 (epoll 报告断开) 还可以在 Lua 中增加 log: https://github.com/cloudwu/skynet/blob/master/lualib/skynet/socket.lua#L119 (收到底层的 CLOSED 消息) |
好的,我打日志看一下,每一次测试都需要一天,因为不是马上必现,但是一个晚上过后肯定会出现。我先拉到最新的代码,然后按你说的方式打日志看看。 |
云大,最新代码不能运行: [:0000000c] lua call [0 to :c : 0 msgsz = 24] error : ../skynet/lualib/skynet.lua:859: ../skynet/lualib/skynet.lua:330: ../skynet/lualib/skynet/socket.lua:374: assertion failed! |
我暂时先 reset 回去,想个更好的方法来实现。 |
@xuzhenyu2020 https://github.com/cloudwu/skynet/tree/onclose 我把相关修改放在 onclose 分支上,你可以试试。 增加了 socket.onclose 可以注册一个 callback 监控 socket 关闭事件。 |
好的,我试一下,感谢云大。游戏现在还没正式部署,如果有需要,我可以提供服务器权限给云大帮助排查问题。 |
注:今天的修改只是为了可以即时关闭对端断开的连接 fd ,避免连接长期处于半关闭状态。但如果是无法检测到对方关闭,那么还是可能: 1. 底层存在 bug 2. 对方的确没有发送 FIN 。 |
用了onclose版本,观察到现在为止,一切正常(目前看到的都是有FIN的,所以不确定如果没有会怎么样)。将会持续观察,到明天再反馈一下结果。在这之前的测试,因为知识匮乏,不知道tcpdump,所以也没有获取到一手信息,不清楚前面几次出问题的时候到底有没有FIN. |
云大,问题依旧,以下是抓包数据: 有看到这个时间段有[F.]标记,但是skynet没有打印"socket closed by peer : 172.17.0.16 3306",我的理解是mysql这边在这个时间段集中将几个连接断开了,但是不知道什么原因,skynet端依然没有感知到。 |
对了,补充一点,第一次被mysql断开后正常,第二次断开就不正常了。 晚上看电影回来后,测试了一下,发现能重连回mysql,然后第二次被断后就没有感知到socket的断开了。 |
建议在 C 里面加两行 log 帮助排查问题:
主要是看 C 层是否工作正常。其中 1 应该是在交换了 FIN 完全断开时触发,以上情况应该到不了;但 2 收到了 FIN 后,recv 就应该返回 0 。此处若正常工作,应该在对方断开后进入。 到 1489 行后,第一次会继续运行到 1507 ,返回 CLOSED 消息。 |
另外,虽然我觉得可能性不大,但还是需要考虑: 如果 socket 上有大量数据待读,但是服务处理不过来。这个时候底层会暂停(pause) 监听可读事件。 即:处理 mysql 请求的服务特别忙。那么也是可能无法及时处理关闭的。log 中会出现 Pause socket 。 |
好的,我今天在c层打印一下看看。另外针对你说的大量数据待读的问题:我们是一个开放世界生存游戏,玩家可以改变世界中的任何东西,世界的数据是用sproto协议编码存储在blob中,需要落地数据的时候,大概需要写入100k左右的数据,不知道这算不算大。 |
今天处理业务问题太晚了,没时间做测试,先休息了,c层打日志的测试结果睡醒后再弄。抱歉。 |
云大,咨询一下,如何使得在c里面打印的内容出现在skynet的日志中? |
skynet_error fprintf |
现在使用的是fprintf,前台运行能打印,后台运行不能到日志中。请问skynet_error这个函数如何使用,非常感谢。 |
结果反馈: 第一次mysql端断开,分别打印出10对信息(一共启动了10个mysql连接),日志如下。 第二次mysql端断开连接好像有点不正常(貌似之前也都是第一次ok,后面可能会出问题): 此时再次触发sql指令,10个连接均能正常连接回mysql,没有出现之前的阻塞现象,只是就打印了5个信息,不知道这是否属于异常。 |
skynet_error 第一个参数传 NULL 后面和 printf 一样。
少了 5 个 SOCKET_CLOSE 就会 lua 层少 5 个关闭事件。应该是有问题的。但你描述 10 个连接均重新建立连接,我猜测是情况 2 由 SOCKET_ERR 让 lua 层关闭了 fd 。如果只看 C 的 log ,那么还应该在https://github.com/cloudwu/skynet/blob/master/skynet-src/socket_server.c#L1135 |
@xuzhenyu2020 我刚才提交了一个 patch ,用来处理上面提到的情况 2 。你可以更新试试。 它回滚了之前的 forward_message_tcp 的实现,改成增加 SOCKET_MORE 状态,让外层继续处理数据包。。 |
收到,我再跑一晚上,看结果。 |
辛苦云大,感谢🙏 |
纠正一下,有个乌龙,日志打出5对是正常对,刚刚仔细分析了一下日志,出现这个情况是因为: 有关socket的正常日志有看到,mysql重连的时候打印出:socket: disconnect detected 172.17.0.16 3306。除此外没有看到其他的socket关键字的日志。 |
03/18/21 13:57:14.33 [:00000022] KILL self 用了最新的onclose分支,目前位置,一切正常,以上是断开的日志。 |
今天出了大量的以下这种日志(有好接近500条),不知有没有问题,除此之外,mysql连接依然正常,mysql连接不正常的问题好像已经解决。 |
我把这行 log 删掉了。因为现在的处理方式是 "如果 read 刚好是 n 就返回 SOCKET_MORE 再读一次”。如果恰好读完了,那么再读一次就会触发这行 log 。 以前之所以没有,是因为之前的逻辑是等 epoll 再次触发。 看 log ,虽然有 500 条这个 log ,但时间上并不连续,不像是死循环导致的输出。所以我判断新加的 SOCKET_MORE 的流程工作正常(每次 epoll 报告可读后,之多额外尝试一次,进入之前的 EAGAIN 分支)。 ps. 这个 log 是 500 条集中在一起么? |
目前为止mysql依然正常,应该是解决了。EAGAIN这个日志不是连续的,是分散在一些时间段内的(间隔有好几个小时),每次出现集中在1分钟左右时间内,连续报出十几到上百条之间。 |
skynet更新到最新代码遇到了一个问题。
之前版本我经常使用 ehco "help" | nc localhost port 这样的命令来执行一些 debug 命令。因为 nc 运行完会自动退出,所以socket也就断开了,skynet那边也能正常的关闭掉这条连接。但新的skynet代码运行后,发现通过这样的命令开启的socket连接无法正确断开了。nc的端口这边处于 FIN_WAIT2 状态,skynet的端口处于 CLOSE_WAIT 状态,说明 nc 这边发送了 FIN,skynet 收到了FIN,但是 nc 这边没有收到 skynet 回发的 FIN。怀疑是新加的半连接状态哪里处理的有问题。我是用 debug_console 测试的。其他类型服务开启的端口应该也会有类似问题。
The text was updated successfully, but these errors were encountered: