为什么总是有5个连接没有附加程序?

5
这个问题类似于网络端口已打开,但没有进程附加?netstat显示一个监听端口,但没有pid,但lsof不是。但它们的答案不能解决我的问题,因为它很奇怪。
我有一个名为lps的服务器应用程序,在8588端口等待tcp连接。
[root@centos63 lcms]# netstat -lnp | grep 8588   
tcp        0      0 0.0.0.0:8588                0.0.0.0:*                   LISTEN          6971/lps

如您所见,监听套接字没有问题,但是当我连接数千个测试客户端(由另一位同事编写)到服务器上时,无论是2000、3000还是4000,总会有5个客户端(也是随机的)连接并向服务器发送登录请求,但无法接收任何响应。以3000个客户端为例,以下是netstat命令的输出:
[root@centos63 lcms]# netstat -nap | grep 8588 | grep ES | wc -l
3000

这是 lsof 命令的输出:

[root@centos63 lcms]# lsof -i:8588 | grep ES | wc -l
2995

这里有5个连接:

[root@centos63 lcms]# netstat -nap | grep 8588 | grep -v 'lps'                   
tcp    92660      0 192.168.0.235:8588          192.168.0.241:52658         ESTABLISHED -                   
tcp    92660      0 192.168.0.235:8588          192.168.0.241:52692         ESTABLISHED -                   
tcp    92660      0 192.168.0.235:8588          192.168.0.241:52719         ESTABLISHED -                   
tcp    92660      0 192.168.0.235:8588          192.168.0.241:52721         ESTABLISHED -                   
tcp    92660      0 192.168.0.235:8588          192.168.0.241:52705         ESTABLISHED -                   

上面的5个连接显示它们连接到8588端口的服务器,但没有程序附加。第二列(即RECV-Q)随着客户端发送请求而不断增加。
上述链接提到了NFS挂载和RPC。关于RPC,我使用了命令rcpinfo -p,结果与8588端口无关。而对于NFS挂载,nfssta输出显示Error: No Client Stats (/proc/net/rpc/nfs: No such file or directory)
问题:这是怎么发生的?总是来自5个不同的客户端,我不认为这是端口冲突,因为其他客户端也连接到同一个服务器IP和端口,并且都被服务器正确处理。
注意:我正在使用Linux epoll接受客户端请求。我还在我的程序中编写了调试代码,并记录了每个accept返回的套接字(以及客户端的信息),但找不到这5个连接。这是uname -a的输出:
Linux centos63 2.6.32-279.el6.x86_64 #1 SMP Fri Jun 22 12:19:21 UTC 2012 x86_64 x86_64 x86_64 GNU/Linux

谢谢您的帮助!我真的很困惑。


< p > < em > < strong >更新 2013-06-08: 将系统升级到CentOS 6.4后,出现了同样的问题。最终我返回到使用epoll,并在this page上发现,将监听fd设置为非阻塞,并accept直到返回EAGAINEWOULDBLOCK错误即可解决问题。是的,它有效了。不再有未处理连接了。但为什么呢?Unix网络编程第一卷说道:

accept is called by a TCP server to return the next completed connection from the 
front of the completed connection queue. If the completed connection queue is empty,
the process is put to sleep (assuming the default of a blocking socket).

所以如果队列中仍有已完成的连接,为什么进程会被置于睡眠状态?
更新 2013-7-1: 在添加监听套接字时我使用了 EPOLLET,因此如果不保持 accept 直到遇到 EAGAIN,就无法全部接受。我刚意识到这个问题。是我的错。记住:如果使用 EPOLLET,即使是监听套接字,也必须 read 或 accept 直到出现 EAGAIN。再次感谢 Matthew 提供测试程序。

你们的环境中,IP地址192.168.0.241有什么特殊之处吗? - Nils
另外再加上@Nils,我不认为这是IP 192.168.0.241的问题。我们有几个测试虚拟机,这5个可能来自不同的主机。 - leowang
等一下。这个服务器 lps 是你正在编写的程序吗? - Michael Hampton
@MichaelHampton 是的,没错。 - leowang
1个回答

1

我已经尝试使用以下参数来复制您的问题:

  1. 服务器使用epoll来管理连接。
  2. 我建立了3000个连接。
  3. 连接是阻塞的。
  4. 服务器基本上只处理连接,并执行非常少的复杂工作。

我无法复制这个问题。这是我的服务器源代码。

#include <stddef.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>

#include <errno.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>

#include <err.h>
#include <sysexits.h>
#include <string.h>
#include <unistd.h>

struct {
  int numfds;
  int numevents;
  struct epoll_event *events;
} connections = { 0, 0, NULL };

static int create_srv_socket(const char *port) {
  int fd = -1;
  int rc;
  struct addrinfo *ai = NULL, hints;

  memset(&hints, 0, sizeof(hints));
  hints.ai_flags = AI_PASSIVE;

  if ((rc = getaddrinfo(NULL, port, &hints, &ai)) != 0)
    errx(EX_UNAVAILABLE, "Cannot create socket: %s", gai_strerror(rc));

  if ((fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol)) < 0)
    err(EX_OSERR, "Cannot create socket");

  if (bind(fd, ai->ai_addr, ai->ai_addrlen) < 0)
    err(EX_OSERR, "Cannot bind to socket");

  rc = 1;
  if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &rc, sizeof(rc)) < 0)
    err(EX_OSERR, "Cannot setup socket options");

  if (listen(fd, 25) < 0)
    err(EX_OSERR, "Cannot setup listen length on socket");

  return fd;
}

static int create_epoll(void) {
  int fd;
  if ((fd = epoll_create1(0)) < 0)
    err(EX_OSERR, "Cannot create epoll");
  return fd;
}

static bool epoll_join(int epollfd, int fd, int events) { 
  struct epoll_event ev;
  ev.events = events;
  ev.data.fd = fd;

  if ((connections.numfds+1) >= connections.numevents) {
    connections.numevents+=1024;
    connections.events = realloc(connections.events, 
      sizeof(connections.events)*connections.numevents);
    if (!connections.events)
      err(EX_OSERR, "Cannot allocate memory for events list");
  }

  if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev) < 0) {
    warn("Cannot add socket to epoll set");
    return false;
  }

  connections.numfds++;
  return true;
}

static void epoll_leave(int epollfd, int fd) {
  if (epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, NULL) < 0)
    err(EX_OSERR, "Could not remove entry from epoll set");

  connections.numfds--;
}


static void cleanup_old_events(void) {
  if ((connections.numevents - 1024) > connections.numfds) {
    connections.numevents -= 1024;
    connections.events = realloc(connections.events,
      sizeof(connections.events)*connections.numevents);
  }
}


static void disconnect(int fd) {
  shutdown(fd, SHUT_RDWR);
  close(fd);
  return;
}

static bool read_and_reply(int fd) {
  char buf[128];
  int rc;
  memset(buf, 0, sizeof(buf));

  if ((rc = recv(fd, buf, sizeof(buf), 0)) <= 0) {
    rc ? warn("Cannot read from socket") : 1;
    return false;
  }

  if (send(fd, buf, rc, MSG_NOSIGNAL) < 0) {
    warn("Cannot send to socket");
    return false;
  }

  return true;
}

int main()
{
  int srv = create_srv_socket("8558");
  int ep = create_epoll();
  int rc = -1;
  struct epoll_event *ev = NULL;

  if (!epoll_join(ep, srv, EPOLLIN)) 
    err(EX_OSERR, "Server cannot join epollfd");

  while (1) {
    int i, cli;

    rc = epoll_wait(ep, connections.events, connections.numfds, -1);
    if (rc < 0 && errno == EINTR)
      continue;
    else if (rc < 0)
      err(EX_OSERR, "Cannot properly perform epoll wait");

    for (i=0; i < rc; i++) {
      ev = &connections.events[i];

      if (ev->data.fd != srv) {

        if (ev->events & EPOLLIN) {
          if (!read_and_reply(ev->data.fd)) {
            epoll_leave(ep, ev->data.fd);
            disconnect(ev->data.fd);
          }
        } 

        if (ev->events & EPOLLERR || ev->events & EPOLLHUP) {
          if (ev->events & EPOLLERR)
            warn("Error in in fd: %d", ev->data.fd);
          else
            warn("Closing disconnected fd: %d", ev->data.fd);

          epoll_leave(ep, ev->data.fd);
          disconnect(ev->data.fd);
        }

      }
      else {

        if (ev->events & EPOLLIN) {
          if ((cli = accept(srv, NULL, 0)) < 0) {
            warn("Could not add socket");
            continue;
          }

          epoll_join(ep, cli, EPOLLIN);
        }

        if (ev->events & EPOLLERR || ev->events & EPOLLHUP)
          err(EX_OSERR, "Server FD has failed", ev->data.fd);

      }
    }

    cleanup_old_events();
  }

}

这里是客户端:

from socket import *
import time
scks = list()

for i in range(0, 3000):
  s = socket(AF_INET, SOCK_STREAM)
  s.connect(("localhost", 8558))
  scks.append(s)

time.sleep(600)

在我的本地机器上运行时,我使用8558端口得到了6001个套接字(其中1个是监听套接字,3000个是客户端套接字,3000个是服务器端套接字)。
$ ss -ant | grep 8558 | wc -l
6001

当检查客户端连接的IP连接数量时,我得到了3000个。
# lsof -p$(pgrep python) | grep IPv4 | wc -l
3000

我也尝试了在远程机器上测试,也成功了。

我建议您也尝试一下。

此外,为了排除某些连接跟踪问题,尝试完全关闭 iptables。有时,/proc 中的 iptables 选项也可能有所帮助。因此,请尝试 sysctl -w net.netfilter.nf_conntrack_tcp_be_liberal=1

编辑:我进行了另一个测试,产生了您在您这边看到的输出。您的问题是您过早地关闭了服务器端的连接。

我可以通过执行以下操作来复制类似于您所看到的结果:

  • 在我的服务器上读取一些数据后,调用shutdown(fd, SHUT_RD)
  • 在服务器上执行send(fd, buf, sizeof(buf))

执行此操作后,将看到以下行为。

  • 在客户端中,我在netstat/ss中打开了3000个连接,并处于已建立状态。
  • 在lsof输出中,我建立了2880个连接(因为我关闭方式的自然性质)。
  • 其余的连接lsof -i:8558 | grep -v ES处于CLOSE_WAIT状态。

这只发生在半关闭连接上。

因此,我怀疑这是您的客户端或服务器程序中的错误。要么您正在向服务器发送某些服务器不接受的内容,要么服务器无效地关闭连接。

您需要确认“异常”连接处于何种状态(如close_wait或其他状态)。

目前,我认为这是一个编程问题,而不是服务器故障。如果没有看到客户端/服务器源代码的相关部分,任何人都无法追踪故障的原因。尽管我相当有信心,这与操作系统处理连接的方式无关。


感谢您抽出时间编写测试程序。在我的机器上测试结果与您的相同。我将服务器程序修改回阻塞监听文件描述符,也可以接受3000个连接。但是回到原来的例程进行传入数据处理时,那些无法被接受的丢失连接返回了。我也尝试了您建议的关闭iptables和修改sysctl参数,但仍然没有起作用。 - leowang
在处理方面,它是I/O、内存还是CPU负载较重? - Matthew Ife
我已经对我的原始答案进行了更新。我还投票将其迁移到stackoverflow,因为它可能不再适用于此处。 - Matthew Ife
我编写了服务器程序,客户端由另一位同事负责,因此所有发布的信息都在服务器端。问题是netstat没有给出CLOSE_WAIT状态。如上所述,这5个也处于ESTABLISHED状态,但没有附加程序。只有当客户端关闭连接或客户端在几分钟内没有传入数据时,我才会关闭连接。PS:感谢您的迁移,我也认为它与Serverfault关系不大。 - leowang
1
我最近在使用epoll时遇到了一些问题,在多次阅读手册页面后,我发现我们程序之间有一个很大的区别。当添加监听fd时,我使用EPOLLET,而你只是使用默认的水平触发EPOLLIN。这就是为什么我不能接受全部(但我仍然不明白为什么总是五个)。在使用水平触发进行测试后,这些5也没有出现(这也解释了为什么接受直到遇到EAGAIN可行)。 - leowang
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接