【问题标题】:Solutions to blocking operations inside an epoll event loopepoll事件循环内阻塞操作的解决方案
【发布时间】:2017-08-23 03:24:02
【问题描述】:

我的 TCP 服务器中有一个 epoll 事件循环来处理客户端连接并从客户端读取数据。

while(1) {
    int n, i;

    n = epoll_wait(efd, events, 64, -1); // This is blocking. It waits till new events arrive

    for(i = 0; i < n; i++) {
        if((events[i].events & EPOLLERR) || (events[i].events & EPOLLHUP) || (!(events[i].events & EPOLLIN))) {
          /* An error has occured on this fd, or the socket is not
             ready for reading */
      dzlog_error("epoll error: %s", strerror(errno));
      close(events[i].data.fd);

      continue;
    } else if(sock == events[i].data.fd) { // Event on the server socket. Accept client connection
            while(1) {
                if((cli = accept(sock, (struct sockaddr *)&their_addr, &addr_size)) == -1) {
                    if((errno == EAGAIN) || (errno == EWOULDBLOCK)) { // We have processed all incoming connections
                        break;
                    } else {
                        dzlog_error("accept: %s", strerror(errno));
                        break;
                    }
                }

                dzlog_info("Client connected: Identifier - %d", cli);

                s = fcntl(cli, F_SETFL, O_NONBLOCK); // Make client socket non-blocking
               if(s == -1) {
                    dzlog_error("Client no block: %s", strerror(errno));
                    close(cli);
                    break;
                }

                event.data.fd = cli;
                event.events = EPOLLIN | EPOLLET;
                s = epoll_ctl (efd, EPOLL_CTL_ADD, cli, &event); // Add the client socket to the list of file descriptors to poll
                if(s == -1) {
                    dzlog_error("epoll_ctl: %s", strerror(errno));
                    close(cli);
                    break;
                }
            }

            continue;
        } else {
            readClientData(events[i].data.fd);
        }
    }
}

当有数据要从客户端套接字读取时,会调用readClientData 函数。让我们假设在该函数内部,我们调用了一个从表中获取一些数据的数据库。如果由于某种原因,对数据库的调用挂起或花费的时间比预期的要长,其他等待连接或发送数据的客户端也将被阻塞。

例如考虑以下场景:

  1. 客户端 1 连接到服务器
  2. 客户端 2 连接到服务器
  3. 客户端 1 向服务器发送数据(这将导致调用 readClientData 函数来处理数据)
  4. readClientData 函数调用数据库并等待响应。 (等待 10 秒或可能无限期挂起)
  5. 客户端 2 发送数据。无法处理此数据,因为服务器仍在等待客户端 1 的 readClientData 完成
  6. 新的客户端 3 尝试连接,但必须等待其连接被接受,因为服务器仍在处理来自客户端 1 的数据

有没有办法解决这个问题?

谢谢

【问题讨论】:

  • 为什么不让服务器在成功连接时分叉进程?
  • @Chirality 这对性能来说太可怕了,这就是我选择均匀驱动方法的原因
  • 我应该澄清一下。你不能 fork readClientData 函数,以便服务器(父进程)继续接受和处理这种事件驱动方法中的连接吗?我不确定您是否可以等待它执行该功能。
  • @Chirality 这和每个连接都有一个子进程不一样吗?如果我有 50k 个客户端都发送数据,我将有 50k 个子进程
  • 我建议学习 node.js 的工作原理——即使你没有在其中编写应用程序。

标签: c sockets concurrency server epoll


【解决方案1】:

您可以将单独的进程专用于等待的操作,例如数据库读取、侦听套接字,以便您也可以使用事件循环来检查数据库进程的发送/接收是否完成

并在主进程中保持事件循环: 从客户端读取,以 nowait 模式写入 DB 处理进程,返回事件循环以检查 DB 进程回复或来自客户端的新请求

【讨论】:

    【解决方案2】:

    不必要的序列化?

    假设您提到readClientData() 进行数据库查询是密切相关的;数据库非常聪明,如今任何半体面的引擎都可以很高兴地一次为多个客户提供服务。如果客户不应该直接与数据库对话并没有特别的原因,那么让他们这样做可能会使系统更快。

    需要多个数据库填充程序

    如果这不是一个选项,使用进程/线程作为主事件循环和数据库之间的垫片(如 Pras 所述)是保持事件循环响应时间较短的一种方法。

    但是,您也可以拥有多个这些,以便发出短请求的客户端不会被刚刚发出长请求的另一个客户端阻止。使用一个 shim,在开始新的短请求之前,它会一直等待长请求完成。

    这开始变得复杂了;你需要多个进程/线程,你必须想办法在垫片之间划分传入的请求,等等。开始编写很多代码。

    ZeroMQ

    幸好有答案;如果您使用 ZeroMQ 进行主事件循环和数据库垫片之间的通信,您可以开始利用它的模式(= 您自己没有编写大量代码)。 PUSH/PULL 浮现在脑海中;可用于在 shims 中神奇地自动分出客户端请求。

    如果您获得正确的高水位标记,新的客户端请求可能会“超过”长时间运行的请求,因为长时间运行的 shim 根本不会获得新的客户端请求。当然,前提是数据库引擎本身实际上可以同时为所有垫片提供服务。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-06-24
      • 1970-01-01
      • 1970-01-01
      • 2019-01-27
      相关资源
      最近更新 更多