Skip to content
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

出现大量的 <Error: failed to delete fd=xxx from poller in event-loop(0): epoll_ctl del: no such file or directory> #398

Closed
JemmyH opened this issue Aug 28, 2022 · 15 comments
Assignees
Labels
needs fix pending development Requested PR owner to improve code and waiting for the result waiting for response waiting for the response from commenter

Comments

@JemmyH
Copy link
Contributor

JemmyH commented Aug 28, 2022

Describe the bug
服务突然出现大量的此类错误,然后整个服务不可用。
想问下什么场景会出现此类错误

Expected behavior
A clear and concise description of what you expected to happen.

Screenshots
If applicable, add screenshots to help explain your problem.

System Info (please fill out the following information):

  • OS (e.g. Ubuntu 18.04): Debian 5.4.143
  • Go version (e.g. Go 1.13): go1.18
  • gnet version (e.g. v1.0.0): v2.1.0
@JemmyH JemmyH added the bug Something isn't working label Aug 28, 2022
@panjf2000
Copy link
Owner

这个错误是连接关闭时把 socket fd 从 epoll 里移除时出错,提示 fd 不存在。

@panjf2000
Copy link
Owner

能说明一下具体是在什么情况下发生的吗?服务当时在做什么,如果有相关的代码更好。

@JemmyH
Copy link
Contributor Author

JemmyH commented Sep 2, 2022

我尽量复现一下。

首先我服务启动后有一个后台定时任务,触发某个条件后,会调用 Conn.Close()。

但是此时已经通过 AsyncWrite 提交了相当多的任务在 Poller 的异步队列中,engine 没有结束,所以一直从队列中取出并 asyncWrite:

// connection.go:434
func (c *conn) AsyncWrite(buf []byte, callback AsyncCallback) error {
 ...
 return c.loop.poller.Trigger(c.asyncWrite, &asyncWriteHook{callback, buf})
}

由于conn被关闭,write 的时候出错,会走到 closeConn:

        ...
        var sent int
	if sent, err = unix.Write(c.fd, data); err != nil {
		// A temporary error occurs, append the data to outbound buffer, writing it back to the peer in the next round.
		if err == unix.EAGAIN {
			_, _ = c.outboundBuffer.Write(data)
			err = c.loop.poller.ModReadWrite(c.pollAttachment)
			return
		}
		return -1, c.loop.closeConn(c, os.NewSyscallError("write", err))
	}

        ...

closeConn 时,会调用 epoll ctl 删掉 epoll 中的 socket,同时也会系统调用 close 掉这个 socket:

// eventloop.go:177
...
        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
		}
	}
...

这也是本 issue 标题中的错误大量出现的原因。

一句话描述:write 失败就去 closeConn,恰巧 Poller 中的队列中任务非常多,重复 closeConn 导致。

疑问点:

  1. 真正 write 失败的错误,是直接打印了一个错误,为什么不能回调给业务层?AsyncWrite 几乎没失败过,但 AsyncWrite 写入成功,并不代表者真正的写入成功;writeCallback 的参数只有 Conn,也并不能标识真正 write 的状态。
func (c *conn) asyncWrite(itf interface{}) (err error) {
	if !c.opened {
		return nil
	}

	hook := itf.(*asyncWriteHook)
	_, err = c.write(hook.data)
	if hook.callback != nil {
		_ = hook.callback(c)    // 这个回调要是能加上 error 就好了
	}
	return
}

// internal/netpoll/epoll_default_poller.go:164
for i := 0; i < MaxAsyncTasksAtOneTime; i++ {
    if task = p.asyncTaskQueue.Dequeue(); task == nil {
         break
    }
    switch err = task.Run(task.Arg); err {
    case nil:
    case errors.ErrEngineShutdown:
         return err
    default:
         logging.Warnf("error occurs in user-defined function, %v", err)
    }
    queue.PutTask(task)
}
  1. 另一个诡异的点我还没找到原因:在这一刻,Client 没有异常退出,服务也没有 panic,但是这个 Client 上的所有连接都断了(看表现是每条连接都调用了 OnClose)。

@panjf2000
Copy link
Owner

另一个诡异的点我还没找到原因:在这一刻,Client 没有异常退出,服务也没有 panic,但是这个 Client 上的所有连接都断了(看表现是每条连接都调用了 OnClose)。

这个不是你前面说的触发了服务端的某个条件被主动关闭了吗?那这里 client 的连接也是会收到断开的通知啊!

@panjf2000 panjf2000 added the waiting for response waiting for the response from commenter label Sep 7, 2022
@JemmyH
Copy link
Contributor Author

JemmyH commented Sep 11, 2022

另一个诡异的点我还没找到原因:在这一刻,Client 没有异常退出,服务也没有 panic,但是这个 Client 上的所有连接都断了(看表现是每条连接都调用了 OnClose)。

这个不是你前面说的触发了服务端的某个条件被主动关闭了吗?那这里 client 的连接也是会收到断开的通知啊!

不是的,我的服务是一个 client,主动关闭也是 client 的逻辑。client 与多个下游建立了长连接,每条连接都有定时探活的逻辑,探活失败会主动关闭当条连接。但是我理解关闭当条连接导致大量的上述错误打印,不应该影响其他连接,这是我疑惑的点。

@JemmyH
Copy link
Contributor Author

JemmyH commented Sep 11, 2022

关于造成第二点(Client 上所有连接都断了)的原因,昨天找到了——我们基础设施有实例自动迁移,大量的错误触发了自动迁移条件,原有实例被 kill 后又重新拉起,这个场景没有 panic,有报警但是被其他同学关掉了。

@JemmyH
Copy link
Contributor Author

JemmyH commented Sep 11, 2022

简单总结一下:标题不是 gnet 的 bug,是使用过程中 业务异常 + gnet WriteCallback 设计未考虑 write error 一起导致的。

对于 gnet,个人觉得还是有以下可优化的点:

  1. AsyncWrite(buf []byte, callback AsyncCallback) (err error) 中的 AsyncCallback 函数,参数中可以携带真正 write 时 error
  2. 在 Poller 任务队列中取出用户任务执行时遇到的错误,能通过某种回调告知业务层,而不是打印一个 Warnning 日志。

@panjf2000
Copy link
Owner

简单总结一下:标题不是 gnet 的 bug,是使用过程中 业务异常 + gnet WriteCallback 设计未考虑 write error 一起导致的。

对于 gnet,个人觉得还是有以下可优化的点:

  1. AsyncWrite(buf []byte, callback AsyncCallback) (err error) 中的 AsyncCallback 函数,参数中可以携带真正 write 时 error
  2. 在 Poller 任务队列中取出用户任务执行时遇到的错误,能通过某种回调告知业务层,而不是打印一个 Warnning 日志。

我觉得可能得修改下 AsyncCallback 的定义,多接收一个 error 作为入参,不过这样会导致前面的版本有代码不兼容;也可以保持兼容,修改成 type AsyncCallback func(c Conn, err ...error) error,不过这样会导致代码很丑,所以我感觉可能还是直接改成 type AsyncCallback func(c Conn, err error) error 吧,这个地方就破坏了一点兼容性,不过应该也在可接受范围内,到时候 release notes 里标注一下即可,你有时间可以提一个 PR 修改。

@panjf2000 panjf2000 added pending development Requested PR owner to improve code and waiting for the result needs fix and removed bug Something isn't working labels Sep 13, 2022
@JemmyH
Copy link
Contributor Author

JemmyH commented Sep 13, 2022

为了不破坏兼容性,可以多定义一个 callback

type AsyncCallbackWithError func(c Conn, err error) error

asyncWrite时多判断一次:

func (c *conn) asyncWrite(itf interface{}) (err error) {
	if !c.opened {
		return nil
	}

	hook := itf.(*asyncWriteHook)
	_, err = c.write(hook.data)
	if hook.callback != nil {
		_ = hook.callback(c)    
	}
        if hook.callbackWithErr != nil && err != nil {
		_ = hook.callbackWithErr(c, err)   
	}
	return
}

@panjf2000
Copy link
Owner

这样的话,gnet.Conn 接口要新增方法,得不偿失。

@panjf2000
Copy link
Owner

简单总结一下:标题不是 gnet 的 bug,是使用过程中 业务异常 + gnet WriteCallback 设计未考虑 write error 一起导致的。
对于 gnet,个人觉得还是有以下可优化的点:

  1. AsyncWrite(buf []byte, callback AsyncCallback) (err error) 中的 AsyncCallback 函数,参数中可以携带真正 write 时 error
  2. 在 Poller 任务队列中取出用户任务执行时遇到的错误,能通过某种回调告知业务层,而不是打印一个 Warnning 日志。

我觉得可能得修改下 AsyncCallback 的定义,多接收一个 error 作为入参,不过这样会导致前面的版本有代码不兼容;也可以保持兼容,修改成 type AsyncCallback func(c Conn, err ...error) error,不过这样会导致代码很丑,所以我感觉可能还是直接改成 type AsyncCallback func(c Conn, err error) error 吧,这个地方就破坏了一点兼容性,不过应该也在可接受范围内,到时候 release notes 里标注一下即可,你有时间可以提一个 PR 修改。

这个你有兴趣搞吗?如果没时间我这两天就自己提交 commit 了。
@JemmyH

@panjf2000
Copy link
Owner

Related issue: #328

@lesismal
Copy link

lesismal commented Oct 18, 2022

我看了下connection.go里conn的定义,是无锁的,而且Close也是没有判断opend状态直接就trigger的。保持这样的话我估计是无法解决类似问题的,原因我好像之前在其他一些issue里讲到过,但是搜不到了。
那就再大概说一下吧:因为io协程与逻辑协程是不同的协程,无法保证io、多个逻辑并发流中的多个Close/Write的时序。比如应用功能中有多个模块都持有了一个conn,都需要对它Write或者出错时Close,这个时序无法保证。所以为了保证conn操作的正确性,需要锁+状态,比如已经关闭了的,Write/Close也就都是空操作直接返回了。标准库的net.Conn们或者os.File其他这些文件句柄最终到poller部分也都是带锁+状态的,应该也是同样的原因。
不只是Close,甚至可能导致窜号的问题,比如被Close的conn仍然被应用层持有(即使框架已经发出通知,但应用层可能是其他的并发流所以尚未执行清理),这时刚好应用层对这个旧的conn进行了Write,而此时write的fd刚好被accept了分配给了另一个conn,因为都是并发的操作,时序没法保证,并且当前这个fd也能成功写入,但是数据就错乱了。即使Close时把fd赋值成-1之类的也未必能解决,因为没有加锁,赋值的时序也无法保证在其他并发流syscall.Write之前完成,尤其是go 1.14之后抢占调度了即使是纯cpu消耗都可能被随时调度。

@lesismal
Copy link

在我的观念里,无锁应该只适合少量场景下队列的push/pop操作来提高性能。尤其是对于conn这种单个fd上的并发竞争其实很少,这样的话锁也只剩下if+原子操作,并没有太大成本浪费。
trigger可以实现fd操作的无锁有序化,但仍然解决不了应用层的时序问题,尤其是go这种协程数量非常多的场景。

@lesismal
Copy link

github issue的搜索也是有点迷了

0-haha pushed a commit to 0-haha/gnet that referenced this issue Jan 25, 2023
Note: this is a breaking change, the existing codebase using AsyncCallback must be updated accordingly.

Fixes panjf2000#398
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs fix pending development Requested PR owner to improve code and waiting for the result waiting for response waiting for the response from commenter
Projects
None yet
Development

No branches or pull requests

3 participants