acl-dev / acl

C/C++ server and network library, including coroutine,redis client,http/https/websocket,mqtt, mysql/postgresql/sqlite client with C/C++ for Linux, Android, iOS, MacOS, Windows, etc..
https://acl-dev.cn
GNU Lesser General Public License v3.0
2.88k stars 936 forks source link

fiber: bugfix recv when signal #275

Closed sidyhe closed 2 years ago

sidyhe commented 2 years ago

仅在linux下出现 (linux mint, gcc 9.4.0)

复现步骤:

  1. 使用mariadb-connector-c 3.1.16, 静态编译
  2. mysql_init, mysql_real_connect, mysql_ping
  3. 会在mysql_ping中死循环

原因: 这个库会切换socket的block状态, 然后循环调用recv/poll以探测数据, 卡死在这里 (pvio_socket_read) 调试后发现是poll异常, fiber库替换了系统实现, 当超时触发acl_fiber_signal后会给fiber置FIBER_F_SIGNALED标志 导致后续调用recv时始终返回EAGAIN (11), 因为在acl_fiber_readacl_fiber_canceled始终成立

解决方法: 所以在signal触发后, fiber switch回来后再把标志去掉

然后发现io count计数不正常, add read 后没有触发read callback, 也是因为上述原因没有触发回调 而io的计数是分散的, 所以改到了一起, 切换前count++切回来count--效果应该是一样的

此PR可能有对acl fiber理解不对的地方, 请作者考虑后再合并

zhengshuxin commented 2 years ago

这个库会切换socket的block状态, 然后循环调用recv/poll以探测数据, 卡死在这里 (pvio_socket_read) 调试后发现是poll异常, fiber库替换了系统实现, 当超时触发acl_fiber_signal后会给fiber置FIBER_F_SIGNALED标志 导致后续调用recv时始终返回EAGAIN (11), 因为在acl_fiber_readacl_fiber_canceled始终成立

请问你那儿如何知道是通过触发了 acl_fiber_signal 后导致的?这个API一般都是一个协程关闭另一个协程时才会被调用.

sidyhe commented 2 years ago

过程

sidyhe commented 2 years ago

部分mariadb代码如下

  while ((r = ma_recv(csock->socket, (void *)buffer, length, read_flags)) == -1)
  {
    int err = socket_errno;
    if ((err != SOCKET_EAGAIN
#ifdef HAVE_SOCKET_EWOULDBLOCK
      && err != SOCKET_EWOULDBLOCK
#endif
      ) || timeout == 0)
      return r;

    if (pvio_socket_wait_io_or_timeout(pvio, TRUE, timeout) < 1)
      return -1;
  }

情况是ma_recv返回-1, 且errno == EAGAIN

ma_recv()recv()的封装 pvio_socket_wait_io_or_timeout()poll()的封装

sidyhe commented 2 years ago

测试环境是单线程单协程, 个人认为没有其他因素的干扰

zhengshuxin commented 2 years ago

我看了一下代码,在 hook/socket.c中 hook 的 setsockopt API 中,确定有一处,就是当应用通过 setsocket 设置读写超时,在这个 hooked setsockopt 中会启用一个定时器 acl_fiber_create_timer,回调为 fiber_timeout,在该回调中会调用 acl_fiber_signal,是不是在 mariadb 中有通过 setsockopt 设置了读写超时的行为?

sidyhe commented 2 years ago

是的, 会用setsockopt设置超时

zhengshuxin commented 2 years ago

我下了mariadb,看到其在开始连接时使用 setsockopt 设置了超时,然后在读时使用poll等待读超时,所以你提的PR应该是正确的,非常感谢!

sidyhe commented 2 years ago

SO_RCVTIMEO和SO_SNDTIMEO的处理是不是过于复杂了? 调用setsockopt的时候就启动了一个定时器, 其实此时并不代表接下来会立马调用send或recv 假设设置recv超时之后, 做了一些其他事情, 会出现意外情况 可能会提前到达超时(提前计时)或者超时无效(超时已触发但还没调用recv)

所以我认为:

sidyhe commented 2 years ago

刚才还想使用CancelIoEx (win32)来优雅的中断, 但是没找到unix对应的方法, 只好搁置

sidyhe commented 2 years ago

SO_RCVTIMEO和SO_SNDTIMEO的处理是不是过于复杂了? 调用setsockopt的时候就启动了一个定时器, 其实此时并不代表接下来会立马调用send或recv 假设设置recv超时之后, 做了一些其他事情, 会出现意外情况 可能会提前到达超时(提前计时)或者超时无效(超时已触发但还没调用recv)

所以我认为:

  • 不需要额外fiber辅助
  • 调用setsockopt时应当记录这两个超时的值
  • 在event_process中找到具有timeout标记的fd, 并取最小值
  • 像event_process_poll/event_process_epoll那样处理timeout的fd并设置错误码

插一点, 在acl_fiber_recv/acl_fiber_send中设置倒计时开始时间

zhengshuxin commented 2 years ago

是的,这是一个优化点,尤其是定时器这块,之前已经把 poll 里超时的定时器做过优化,在大并发时性能提升比较明显。