panjf2000 / gnet

🚀 gnet is a high-performance, lightweight, non-blocking, event-driven networking framework written in pure Go.
https://gnet.host
Apache License 2.0
9.7k stars 1.04k forks source link

[Question]:详细的说明文档,说明什么情况下应用程序应该主动调用close方法呢 #512

Closed someview closed 11 months ago

someview commented 1 year ago

Actions I've taken before I'm here

Questions with details

因为线上出现了大量tcp close_wait的bug, 怀疑是我们自己实现的ws协议里面有bug ,目前还在排查中。

  1. 在eventhandler的ontraffic里主动调用gnet.Conn.Close()后然后返回gnet.Close是否会引起bug
  2. 在gnet的asyncwrite或者asyncwritev的回调里面,是否需要主动调用gnet.Close()呢,如果不调用是否其他地方有调用close的机会呢

并不清楚close()方法和return gnet.close混用是否会有问题

Code snippets (optional)

err = ws.AsyncWrite(downMsg.GetFrame(), func(gnet.Conn, error) error {
            return nil
    })

        messages, err := conn.Read()
    if err != nil {
        _ = conn.Close()
        m.logger.PrintfError("trans:ws,desc:read,from:%v,err:%v\n", conn.id54, err)
        return gnet.Close
    }
panjf2000 commented 1 year ago

这种问题请务必附上必要的信息:Go 版本,gnet 版本,OS 系统等等,不然根本无从查起;要不然就请选择 bug 的模板创建 issue

panjf2000 commented 1 year ago

在eventhandler的ontraffic里主动调用gnet.Conn.Close()后然后返回gnet.Close是否会引起bug

不会,但是也不要这么做,如果是想直接关闭当前连接,那么就 return gnet.Close,如果是想在其他的地方随时关闭连接,那么可以选择调用 gnet.Conn.Close(),后者是并发安全且异步的,是为了让用户可以在任意地方任意时间关闭连接用的,前者是让用户可以快速直接抵关闭当前连接用的,是同步的;二者选其一就行了,没必要两个都做,但是即便是两个都做了也不会引起问题,因为内部会在关闭连接前检测其是否已经关闭,如果是则忽略掉。

在gnet的asyncwrite或者asyncwritev的回调里面,是否需要主动调用gnet.Close()呢,如果不调用是否其他地方有调用close的机会呢

我没懂你这问题,是否要关闭连接以及什么要关闭连接取决于你的需求,正如前面所说,如果你想在 event-loop 里直接关闭当前连接,那就在任意的 gnet 回调函数中 return gnet.Close 即可,如果你想在其他地方、特定的时间关闭连接,那就用 gnet.Conn.Close().

someview commented 1 year ago

这种问题请务必附上必要的信息:Go 版本,gnet 版本,OS 系统等等,不然根本无从查起;要不然就请选择 bug 的模板创建 issue

谢谢,因为有一些具体的思路,所以也许提供一点点子就能解决了。 go: 1.20 gnet: v2.0 os: linux 现象: 压测的wsclient -> gnet实现的wsserver突然通讯,然后客户端直接选择关闭大量tcp连接, 发现服务端出现大量close_wait状态的连接

panjf2000 commented 1 year ago

大量 CLOSE_WAIT 通常是服务端没有正确关闭连接导致的,如果是客户端主动关闭连接的话,gnet 会自动在服务端这边关闭对应的连接,通常不需要用户自己手动去关闭,而且会有 OnClose 回调,我建议你在这个回调里记录一下看看是否能和客户端关闭的连接数对应上。

someview commented 1 year ago

在eventhandler的ontraffic里主动调用gnet.Conn.Close()后然后返回gnet.Close是否会引起bug

不会,但是也不要这么做,如果是想直接关闭当前连接,那么就 return gnet.Close,如果是想在其他的地方随时关闭连接,那么可以选择调用 gnet.Conn.Close(),后者是并发安全且异步的,是为了让用户可以在任意地方任意时间关闭连接用的,前者是让用户可以快速直接抵关闭当前连接用的,是同步的;二者选其一就行了,没必要两个都做,但是即便是两个都做了也不会引起问题,因为内部会在关闭连接前检测其是否已经关闭,如果是则忽略掉。

在gnet的asyncwrite或者asyncwritev的回调里面,是否需要主动调用gnet.Close()呢,如果不调用是否其他地方有调用close的机会呢

我没懂你这问题,是否要关闭连接以及什么要关闭连接取决于你的需求,正如前面所说,如果你想在 event-loop 里直接关闭当前连接,那就在任意的 gnet 回调函数中 return gnet.Close 即可,如果你想在其他地方、特定的时间关闭连接,那就用 gnet.Conn.Close().

也就是说conn.Close()和return gnet.Close不会出现异常,效果一样了. asyncwrite写数据的时候,应用程序并不知道会返回什么样的错误啊,也许是连接错误了,也许是其他错误。那应用程序如何知道asyncwrite的回调里面是否应该关闭连接呢. 对于客户端直接关闭tcp连接的情况,是在eventhandler的ontraffic里收到响应,还是在onclose里时收到响应呢

someview commented 1 year ago

大量 CLOSE_WAIT 通常是服务端没有正确关闭连接导致的,如果是客户端主动关闭连接的话,gnet 会自动在服务端这边关闭对应的连接,通常不需要用户自己手动去关闭,而且会有 OnClose 回调,我建议你在这个回调里记录一下看看是否能和客户端关闭的连接数对应上。

头疼,。线上环境,复现不了异常情况, 当时查询的时候,是触发不了onclose回调,所以怀疑是不是ontraffic的时候触发的操作. 感谢回复

someview commented 1 year ago

大量 CLOSE_WAIT 通常是服务端没有正确关闭连接导致的,如果是客户端主动关闭连接的话,gnet 会自动在服务端这边关闭对应的连接,通常不需要用户自己手动去关闭,而且会有 OnClose 回调,我建议你在这个回调里记录一下看看是否能和客户端关闭的连接数对应上。

版本是v2 2.2.5是否有release修复bug呢

panjf2000 commented 1 year ago

那应用程序如何知道asyncwrite的回调里面是否应该关闭连接呢.

callback 函数里有传入一个 err,这个值会告诉你在写数据回客户端的时候是否发生了错误,你可以根据这个错误值决定要不要主动关闭连接

对于客户端直接关闭tcp连接的情况,是在eventhandler的ontraffic里收到响应,还是在onclose里时收到响应呢

不管是你主动关闭连接还是被动关闭,都会有 OnClose 回调,不会有 OnTraffic

panjf2000 commented 1 year ago

大量 CLOSE_WAIT 通常是服务端没有正确关闭连接导致的,如果是客户端主动关闭连接的话,gnet 会自动在服务端这边关闭对应的连接,通常不需要用户自己手动去关闭,而且会有 OnClose 回调,我建议你在这个回调里记录一下看看是否能和客户端关闭的连接数对应上。

版本是v2 2.2.5是否有release修复bug呢

现在还不知道是哪里的问题,要先查清楚再说,如果只是客户端大量断开连接就能复现的话,你可以写一个简单的 demo,模拟一下那个场景,看看是否能复现,然后 debug 一下看看是哪里有问题,或者你复现之后把代码贴上来,我来 debug。

someview commented 1 year ago

只能成为悬案了。因为是阿里的mse网关做的ws的代理,并不清楚他们内部的实现细节是如何实现的。如何能测试和模拟tcp断线过程在gnet中的流程呢:

  1. 客户端发送fin1包
  2. 服务端收到fin1包
  3. 服务端发送fin1-ack包
  4. 客户端crash
  5. 服务端发送fin2包 .... 这种情况gnet可以正常处理吗,连接最终能关闭吗
someview commented 1 year ago

大量 CLOSE_WAIT 通常是服务端没有正确关闭连接导致的,如果是客户端主动关闭连接的话,gnet 会自动在服务端这边关闭对应的连接,通常不需要用户自己手动去关闭,而且会有 OnClose 回调,我建议你在这个回调里记录一下看看是否能和客户端关闭的连接数对应上。

版本是v2 2.2.5是否有release修复bug呢

现在还不知道是哪里的问题,要先查清楚再说,如果只是客户端大量断开连接就能复现的话,你可以写一个简单的 demo,模拟一下那个场景,看看是否能复现,然后 debug 一下看看是哪里有问题,或者你复现之后把代码贴上来,我来 debug。

我们自己写的demo或者压测环境一切ok, 中间经过阿里mse的代理后出现了异常.咨询过他们网关的负责人,ws代理的时候是直接关闭tcp连接.

panjf2000 commented 1 year ago

ws代理的时候是直接关闭tcp连接.

这是什么意思?什么时候 MSE 会直接关闭 TCP 连接?

someview commented 1 year ago

ws代理的时候是直接关闭tcp连接.

这是什么意思?什么时候 MSE 会直接关闭 TCP 连接?

wsclient -> mse ws代理 -> wsserver

wsclient 直接ctrl + c 异常结束, mse直接关闭对上游服务的tcp连接. 主要是close_wait恢复不了,几个小时了,都还是大量的close_wait

panjf2000 commented 1 year ago

CLOSE_WAIT 就是服务端没有主动 close 掉连接导致的,这种情况可能是你的 gnet server 没有正确感知到对端的连接断开事件,但这并不应该发生,gnet 是有做正确处理的,你也说了在测试环境没有用代理是没有问题的,感觉还是代理的问题,你能不能在测试环境也搞一个 MSE 然后再 debug 一下呢?

someview commented 1 year ago

CLOSE_WAIT 就是服务端没有主动 close 掉连接导致的,这种情况可能是你的 gnet server 没有正确感知到对端的连接断开事件,但这并不应该发生,gnet 是有做正确处理的,你也说了在测试环境没有用代理是没有问题的,感觉还是代理的问题,你能不能在测试环境也搞一个 MSE 然后再 debug 一下呢?

我本地配置好环境试试。另外发现的一个可疑的点是这样,reuseport选项是否可能导致close_wait呢,如果大量的tcp连接频繁创建销毁

panjf2000 commented 1 year ago

reuseport 可以用来强制重用还处于 TIME_WAIT 的连接,但通常不推荐这么搞,而且和 CLOSE_WAIT 没什么关系。

someview commented 1 year ago

reuseport 可以用来强制重用还处于 TIME_WAIT 的连接,但通常不推荐这么搞,而且和 CLOSE_WAIT 没什么关系。 查看了一下gnet的源码,不太确定是不是某些条件下判断出现的bug:


//  eventloop_unix.go
func (el *eventloop) close(c *conn, err error) (rerr error) {
if addr := c.localAddr; addr != nil && strings.HasPrefix(c.localAddr.Network(), "udp") {
rerr = el.poller.Delete(c.fd)
if c.fd != el.ln.fd {
rerr = unix.Close(c.fd)
el.connections.delConn(c)
}
if el.eventHandler.OnClose(c, err) == Shutdown {
return errorx.ErrEngineShutdown
}
c.release()
return
}
if !c.opened || el.connections.getConn(c.fd) == nil {
    return // ignore stale connections
}

// Send residual data in buffer back to the peer before actually closing the connection.
if !c.outboundBuffer.IsEmpty() {
    for !c.outboundBuffer.IsEmpty() {
        iov := c.outboundBuffer.Peek(0)
        if len(iov) > iovMax {
            iov = iov[:iovMax]
        }
        if n, e := io.Writev(c.fd, iov); e != nil {
            el.getLogger().Warnf("close: error occurs when sending data back to peer, %v", e)
            break
        } else { //nolint:revive
            _, _ = c.outboundBuffer.Discard(n)
        }
    }
}

err0, err1 := el.poller.Delete(c.fd), unix.Close(c.fd)
if err0 != nil {
    rerr = fmt.Errorf("failed to delete fd=%d from poller in event-loop(%d): %v", c.fd, el.idx, err0)
}
if err1 != nil {
    err1 = fmt.Errorf("failed to close fd=%d in event-loop(%d): %v", c.fd, el.idx, os.NewSyscallError("close", err1))
    if rerr != nil {
        rerr = errors.New(rerr.Error() + " & " + err1.Error())
    } else {
        rerr = err1
    }
}

el.connections.delConn(c)
if el.eventHandler.OnClose(c, err) == Shutdown {
    rerr = errorx.ErrEngineShutdown
}
c.release()
return

}

func (c conn) release() { c.ctx = nil c.localAddr = nil c.remoteAddr = nil c.buffer = nil if addr, ok := c.localAddr.(net.TCPAddr); ok && c.localAddr != c.loop.ln.addr && len(addr.Zone) > 0 { bsPool.Put(bs.StringToBytes(addr.Zone)) } if addr, ok := c.remoteAddr.(*net.TCPAddr); ok && len(addr.Zone) > 0 { bsPool.Put(bs.StringToBytes(addr.Zone)) } c.pollAttachment.FD, c.pollAttachment.Callback = 0, nil if !c.isDatagram { c.opened = false c.peer = nil c.inboundBuffer.Done() c.outboundBuffer.Release() } }

在发生异常的情况下,比如,第一次调用close方法,
`err0, err1 := el.poller.Delete(c.fd), unix.Close(c.fd)`
err0执行成功,err1返回的错误是unix.EAGAIN(假设是系统中断引起的),然后执行c.release()将c.opened设置为false,第二次执行close方法的时候,

if !c.opened || el.connections.getConn(c.fd) == nil { return // ignore stale connections }


没有close的机会了.
是否需要在应用中自己处理err为unix.EAGAIN的这种错误
panjf2000 commented 1 year ago

系统调用 close() 不会返回 EAGAIN,最多返回 EINTR,但是这种概率微乎其微,所以在实践中基本上不会专门去处理这种情况,而且就算真的出现这种情况,日志会打印 error occurs in event-loop: %v,你有看到这个日志吗?而且这种情况发生的概率太小了,就算真的有,也应该是极少数,怎么可能有大量的 close() 调用在短时间内都发生了 EINTR 错误, Linux 不至于这么不稳定,以前也没有别人反馈过使用 gnet 遇到过这种情况,我觉得不可能是这里的问题。

someview commented 1 year ago

系统调用 close() 不会返回 EAGAIN,最多返回 EINTR,但是这种概率微乎其微,所以在实践中基本上不会专门去处理这种情况,而且就算真的出现这种情况,日志会打印 error occurs in event-loop: %v,你有看到这个日志吗?而且这种情况发生的概率太小了,就算真的有,也应该是极少数,怎么可能有大量的 close() 调用在短时间内都发生了 EINTR 错误, Linux 不至于这么不稳定,以前也没有别人反馈过使用 gnet 遇到过这种情况,我觉得不可能是这里的问题。

https://www.man7.org/linux/man-pages/man2/close.2.html 确实如此没有err.dragin. 看手册里是推荐出现EINTR错误时,推荐重试关闭文件描述符. 好像没有给gnet添加日志库.没有看到上面的日志