如果您具有基于 UNIX® 的编程经验,那么您可能会在一定程度上苦恼于如何提高您的网络吞吐量。本文介绍了一些有价值的技术,使用本文中描述的这些方法,您可以最大程度地利用您的带宽,并实现显著的性能提升。
任何具有 UNIX® 系统编程经验的人都会苦恼于如何提高网络吞吐量以及磁盘 I/O(在某些情况下)。本文介绍了协议实现者所使用的一些高级编程技术,它们可以帮助您充分利用现有的带宽。(本文并不打算介绍如何优化您的操作系统 (OS)、配置内核、或者系统调整。)
尽管特定协议的可用带宽受到 Shannon 定律和其他一些因素(如网络使用模式)的限制,但大多数时候是因为低劣的编程或者未经过实验的编码导致网络资源的使用率无法达到最佳状态。
性能增强不仅仅是一种科学,而且还是一门艺术。要获得最佳的端到端吞吐量,您必须使用各种工具以度量性能、识别瓶颈,并消除它们,或者使得它们的影响最小化。通过一些简单且直观的科学方法,您可以快速地获得显著的性能提升。
流水线 是 CPU 所使用的一个众所周知的概念,它用于减少取指令-译码-执行 周期中出现的延迟。在获取每条指令期间会出现某种延迟,可以通过预取指令并对其进行存储以用于后续的执行,从而避免延迟。可以将相同的概念应用于网络,正如本部分中所描述的。
可以提前通知服务器在处理完当前请求后客户所关心的是哪些内容,而不是使服务器按老一套方式处理来自客户的请求。服务器维护一个挂起请求队列,依次地执行它们,而不是先执行一个请求,然后再读取下一个请求,等等。这样做提高了交互式应用程序的响应,并且与任何其他技术相比,可以更有效地减少延迟。
但是,并不总是能够这样做。即使在它可以起到作用的情况下,有时候服务器队列也可能耗尽。这种情况非常少见,并且大多数时候流水线都可以很好地工作。所有常见的协议,包括 HTTP 1.1、NNTP、X11 等,都通过某种方式使用了流水线。
持久的传输控制协议(Transmission Control Protocol,TCP)连接
如果您打算为每个请求或者事务发起不同的 TCP 连接,那么上述的技术很明显会失效。您应该重用现有的连接,当然这并不是唯一的原因。在建立和拆除连接的过程中,TCP 握手可能会带来巨大的开销,当然最好能够避免这个开销。
如果不能正确地关闭 TCP 连接,那么可能很快会给您的 UNIX 系统带来各种各样的麻烦。另一个因素是新的连接中的协议开销。您希望确保网络尽可能用于实际数据,而不是交换 Header 以及其他控制信息。图 1 显示了流水线和普通处理。
如何将这转换为编程实现呢?使用现有的 TCP 连接非常容易,但实现流水线并不是那么简单。协议设计必须考虑跟踪挂起的请求,并确定某个响应对应于哪个请求。
下一个部分将说明能够起到帮助作用的其他机制。
您应该熟悉这两种编程模型:同步和异步处理。本文介绍的流水线技术是使用异步处理以提高性能的示例。同步编程将带来简单的设计、更简单的代码,但有时候性能比较糟糕,这是我们不希望看到的。为了避免这个问题,您必须使用其他的一些技巧,如非阻塞 I/O。
阻塞和非阻塞套接字大致上对应于同步和异步处理,但并不是在网络级别,而是在操作系统级别。在使用阻塞套接字进行典型的 socket write(2)
或者 send(2)
操作时,用户进程必须等待系统调用返回。内核负责将进程转移到睡眠状态、等待套接字做好写入的准备、读取 TCP 状态代码,等等。最后,将控制权返回给应用程序。
在使用非阻塞套接字的情况下,麻烦在于程序员必须确保该套接字是可写入的,并且必须确保正确地写入所有数据。这显然给编程带来了一些不便之处,并且必须了解一种新的习惯用法,但是在掌握了该方法之后,它将成为一种为所有网络代码提供优秀性能的功能强大的工具。
当您的套接字变成非阻塞时,仅使用 read(2)
和 write(2)
,或 recv(2)
或者 send(2)
是不够的。您必须使用附加的系统调用,如 poll(2)
或者 select(2)
,以便确定什么时候可以对套接字进行写入,或者从网络进行读取操作。
其中一种选择是使用 poll(2)
来确定可写入性(因为 select(2)
无法完成这一项任务),并使用 select(2)
来确定另一端的数据何时到达您的套接字。清单 1 详细地介绍了一个非阻塞 I/O 的示例。
1 /****************************************** 2 * Non blocking socket read with poll(2) * 3 * * 4 *****************************************/ 5 5 void 6 poll_wait(int fd, int events) 7 { 8 int n; 9 struct pollfd pollfds[1]; 10 memset((char *) pollfds, 0, sizeof(pollfds)); 11 12 pollfds[0].fd = fd; 13 pollfds[0].events = events; 14 15 n = poll(pollfds, 1, -1); 16 if (n < 0) { 17 perror("poll()"); 18 errx(1, "Poll failed"); 19 } 20 } 21 22 size_t 23 readall(int sock, char *buf, size_t n) { 24 size_t pos = 0; 25 ssize_t res; 26 27 while (n > pos) { 28 res = read (sock, buf + pos, n - pos); 29 switch ((int)res) { 30 case -1: 31 if (errno == EINTR || errno == EAGAIN) 32 continue; 33 return 0; 34 case 0: 35 errno = EPIPE; 36 return pos; 37 default: 38 pos += (size_t)res; 39 } 40 } 41 return (pos); 42 } 43 44 size_t 45 readmore(int sock, char *buf, size_t n) { 46 47 fd_set rfds; 6 48 int ret, bytes; 49 50 51 52 poll_wait(sock,POLLERR | POLLIN ); 53 bytes = readall(sock, buf, n); 54 55 if (0 == bytes) { 56 perror("Connection closed"); 57 errx(1, "Readmore Connection closure"); 58 /* NOT REACHED */ 59 } 60 61 return bytes; 62 } 63 64 /****************************************** 65 * Non blocking socket write with poll(2) * 66 * * 67 *****************************************/ 68 69 70 void 71 poll_wait(int fd, int events) 72 { 73 int n; 74 struct pollfd pollfds[1]; 75 memset((char *) &pollfds, 0, sizeof(pollfds)); 76 77 pollfds[0].fd = fd; 78 pollfds[0].events = events; 79 80 n = poll(pollfds, 1, -1); 81 if (n < 0) { 82 perror("poll()"); 83 errx(1, "Poll problem"); 84 } 85 } 86 87 88 size_t 89 writenw(int fd, char *buf, size_t n) 90 { 7 91 size_t pos = 0; 92 ssize_t res; 93 while (n > pos) { 94 poll_wait(fd, POLLOUT | POLLERR); 95 res = write (fd, buf + pos, n - pos); 96 switch ((int)res) { 97 case -1: 98 if (errno == EINTR || errno == EAGAIN) 99 continue; 100 return 0; 101 case 0: 102 errno = EPIPE; 103 return pos; 104 default: 105 pos += (size_t)res; 106 } 107 } 108 return (pos); 109 110 } 111 112 |
Internet 协议 (IP) 分片以及其他随机的网络影响
Sendfile(2)
是一种避免缓冲区复制开销的技术,并且它直接将数据位从文件系统转移到网络。遗憾的是,这个系统调用在现代的 UNIX 系统中存在可移植性的问题;甚至在 OpenBSD 中没有提供这个系统调用。出于可移植性的考虑,应该避免直接使用这个功能。
因为多余的 memcpy(2)
,所以 Sendfile(2)
可以减少延迟。与非阻塞 I/O 一样,您可以使用这种技术在操作系统级别提高网络代码的性能。
然而,在网络级别上可能存在一些影响因素。您可能听说过 IP 分片。通常,它会对性能产生影响。分片和重组的代价非常高,并且尽管仅由中间路由器执行这项任务,但是它对吞吐量有很大的影响。
路径最大传输单元(Path Maximum Transfer Unit,PMTU)发现技术可以帮助您避免对 IP 包进行分片。使用这种方法,您至少可以了解(或者猜测)在不对 IP 层进行分片的情况下可能通过的 TCP 报文段的大小。幸运的是,OS TCP 层可以负责将协议数据划分为避免 IP 分片的TCP 报文段。因为 TCP 是一种不存在任何消息边界的字节流,所以这种方法非常有效,并且是最好的。但是请注意用户数据报协议(User Datagram Protocol,UDP),对它来说,这一点并不成立。
您还需要确保您的网络不会被一些无用的、并且有害的数据包所占用(将 Windows 计算机隔离为一个单独的虚拟 LAN)。在 UNIX 领域中,tcpdump、iftop 和带宽监视工具(如 wmnet)都是非常有价值的。
本文提供了一些方法,可以帮助您充分地利用带宽。组合使用各种不同的工具,可以提高性能。
请继续关注本系列文章的第 2 部分,其中将介绍实现网络使用率最大化的一些技巧。