【问题标题】:How to properly handle socket accept returning "Too many open files"如何正确处理套接字接受返回“打开的文件太多”
【发布时间】:2016-08-21 20:35:13
【问题描述】:

我在 tcp 端口上有一个监听套接字。进程本身使用setrlimit(RLIMIT_NOFILE,&...); 来配置进程允许的套接字数量。

对于测试,RLIMIT_NOFILE 设置为 20,当然对于生产,它将设置为一个更大的数字。 20 有利于在测试环境中轻松达到极限。

服务器本身没有描述符泄漏或类似的问题,但试图通过增加 RLIMIT_NOFILE 来解决问题显然是做不到的,因为在现实生活中,无论多高都不能保证不会达到限制设置。

问题是在达到限制后accept 返回Too many open files 并且除非文件或套接字关闭,否则事件循环会立即开始旋转,占用一个内核的 100%。即使客户端关闭连接(例如因为超时),服务器也会循环直到有文件描述符可用于处理并关闭已经死的传入连接。 编辑:另一方面,客户端停顿,没有好方法知道服务器是否超载。

我的问题:在accept 返回Too many open files 之后关闭传入连接是否有一些标准方法来处理这种情况。

我想到了几种肮脏的方法:

  • 关闭并重新打开监听套接字,希望所有挂起的连接都将关闭(这很脏,因为在线程服务器中,其他线程可能会获得释放的文件描述符)
  • 跟踪打开的文件描述符计数(对于具有一些未跟踪文件描述符的外部库,这无法正确完成)
  • 检查文件描述符数量是否接近限制并在情况发生之前开始关闭传入连接(这是特定于实现的,虽然它可以在 Linux 上运行,但不能保证其他操作系统会以相同的方式处理文件描述符方式)

编辑:另一种肮脏和丑陋的方法:

保留一个备用 fd(例如 dup(STDIN_FILENO)open("/dev/null",...)),以防 accept 失败时使用。顺序是:

... accept failed
// stop threads
close(sparefd);
newconnection = accept(...);
close(newconnection);
sparefd = open("/dev/null",...);
// release threads

这种方法的主要缺点是线程同步,以防止其他线程获取刚刚释放的备用 fd。

【问题讨论】:

  • 意外保持文件描述符打开的外部库非常罕见。 syslog 打开一个到syslogd 的套接字,DB 访问库为每个数据库会话都有一个 FD。我希望 10 个 FD 的缓冲区在任何正常的应用程序中都绰绰有余。
  • 我确实说过很少见。您大概知道自己在应用程序中使用了哪些类型的库,因此您应该能够估计它们可能会使用哪些 fds,除非您尝试设计的是通用库。
  • 可能根本就没有好的解决方案。
  • 如果您收到“打开的文件过多”错误,那么不要继续重试?这将解决您通过连续重试获得 100% 的 CPU 使用率这一事实。
  • 如果您无法控制外部库对 fd 的猖獗使用,请将其限制在自己的进程中。也许你可以让主服务器进程除了监听连接之外什么都不做,然后将接受的 fd 传递给一组工作进程中的一个,它们可以处理任何行为不良的库。

标签: c sockets


【解决方案1】:

您不应该使用setrlimit 来控制您的进程可以处理的同时连接数。您的一小段套接字代码对整个应用程序的其余部分说,“我只想一次打开 N 个连接,这是我知道如何做到这一点的唯一方法,所以......没有别的该进程可以有任何文件!”。如果每个人都这样做会发生什么?

做你想做的事的正确方法很简单——跟踪你打开了多少个连接,在你可以处理另一个连接之前不要打电话给accept

【讨论】:

  • rlimit 已经存在,无论我是否更改它。仅跟踪套接字连接不是解决方案,因为可能还有其他文件描述符打开。当然我可以使用边缘触发事件,但是从事件处理中删除监听套接字并通过 open("/dev/null",...) 检查是否可以接受另一个看起来也很难看。
  • @bbonev 如果可能有其他文件描述符打开,使用 rlimit 作为对套接字连接数的控制不可能工作。 所以,你不应该使用它。每当您收到此错误时,您需要关闭一些 FD,如果您正在进行循环而不是不断生成新的 FD,例如这个接受循环,您需要停止它,直到您释放了一些现有的 FD。正如这个答案所说。
  • 认为这个答案完全误解了这个问题。 OP 没有调用setrlimit 来“控制进程可以处理的同时连接数”。 OP 正在调用setrlimit 来测试代码的行为是否会耗尽 fds,并且发现行为很差,并且想知道为什么。
  • @SteveSummit,文件句柄的限制通常很高,文件句柄用完是正常操作中不应该发生的事情,比如内存不足。通常,中止关闭是处理此类情况的合理方法。您可以只是减少允许的连接数,但随后您将没有可用的句柄,这对于进程来说不是安全/正常的状态。
  • @MattTimmermans 理解您的观点,但您确实说“通常”和“合理”。 OP 有可能在高可靠性要求下运行,并且不能对刚刚耗尽和中止的可能性如此漫不经心。
【解决方案2】:

我了解您的代码在库中。库遇到资源限制事件。通常,我会区分灾难性事件(内存耗尽,无法打开监听套接字)和可能是暂时的事件。灾难性事件很难处理:没有内存,甚至日志记录或有序关闭都可能是不可能的。

相比之下,打开文件过多可能是暂时的,尤其是因为我们是资源大户。幸运的是,临时错误条件很容易处理:通过等待。这是你不应该做的:你应该在accept 返回“打开的文件太多”之后等待一段时间,然后再调用accept。这将解决 100% CPU 负载问题。 (我假设我们的服务器对每个完成的连接执行一些工作,因此我们的库持有的客户端连接的文件描述符最终被关闭。)

仍然存在库无法知道用户代码要求的问题。 (accepts 之间的暂停应该多长时间?1 让连接请求等待是否完全可以接受(原文如此)?我们会在某个时候放弃吗?)它必须将错误报告回用户代码,以便用户代码有机会看到并修复错误。

如果用户代码获取文件描述符,这很容易:返回accept 的错误代码(并确保记录这种可能性)。所以我假设用户代码永远不会看到像文件描述符这样的细节,而是会得到一些数据,例如。甚至可能是该库仅执行副作用,可能同时执行,因此用户代码永远不会看到任何可用于传达错误的返回值。然后库必须提供一些其他方式来向用户代码发出错误情况的信号。这可能会对用户代码如何使用库施加限制:可能在某些函数调用之前或之后,或者只是定期地,必须主动检查错误状态。


1顺便说一句,我也不清楚,即使在阅读accept手册页后,客户端的connect是否失败(因为连接请求已在服务器端出队但无法处理),或者请求是否只是停留在队列中,以便客户端忘记服务器的问题,除了延迟。

【讨论】:

  • 在Linux上测试,结果是内核不知道/检查进程out of fds;结果客户端连接并停止,并且侦听套接字 fd 有挂起的读取事件。即使有问题的客户端等待超时并且消失了,侦听套接字上的事件仍处于未决状态。到目前为止,我没有看到比问题中的 #4 更好的方法......
  • 您的意思是,当另一个 fd 已被释放以使资源“fd”再次可用时,以后(在同一进程中)不可能 accept 那个挂起的连接??
  • 是的,没有“可选”事件可以提供免费的 fd。另一方面,在 epoll_wait 和 accept 之间忙于旋转没有多大意义,从 epoll 事件中删除侦听套接字并在 epoll_wait 超时后检查是否有空闲 fd 将解决服务器的 cpu 问题,但会使未接受的客户端停滞不前。我已经问过这个问题,寻找一种干净便携的方式,可惜似乎没有。
【解决方案3】:

请注意,poll(2) 等多路复用系统调用可以在 accept-ing 套接字(以及连接的套接字或任何其他类型的流文件描述符)上工作(因此请等待而不忙自旋循环)。

所以只需让您的事件循环处理它们(可能使用其他可读和可写的文件描述符)。不想打电话的时候不要打电话给accept(2)

【讨论】:

  • 嗯,他想。也许有点过头了(就像一个没有经验的情人,他走得太快了)。
  • poll 在监听套接字上返回一个事件后,如果没有空闲的 fds,accept 将失败,客户端将停止,并且 poll 将继续在监听套接字上报告读取事件,没有延迟...
  • @bbonev 您可以轮询您的其他文件描述符(数据传输到客户端和从客户端传输)以及接受 fd。如果超过一定数量的打开 fds,则在再次执行接受之前,将首先处理那些起作用的。这样可以防止资源耗尽。
猜你喜欢
  • 2010-10-27
  • 1970-01-01
  • 1970-01-01
  • 2010-12-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多