C100K系统设计

本篇只讨论OS 级别的并发优化,不包括语言层面对并发的支持特性,C10 K / C100 K / C1000 K(百万) 都是指单服务器下,能够支持的空闲长连接的数量。

一般情况下服务端达到 C10K 连接时,大多数连接都是空闲连接,少数的活跃连接有数据读写,这种情况下讨论 C10K 的瓶颈在于“如何管理 10K-100K 级别的连接数”

如果 C10K 的连接情况下,每个链接还有大量的读写数据,瓶颈就不仅仅是上面的“单机管理 10k-100k 数量级的 Connection”了,这种情况下,网卡、带宽都可能成为瓶颈,所以本文只考虑上面的情况“连接大多空闲、少数活跃”。

有关 Linux 系统的限制

服务器能支持建立连接数由几个决定:

  • 建立 TCP 连接最大并发数:TCP 连接四元组是由源 IP 地址、源端口、目的 IP 地址和目的端口构成,参考 [[#端口数量限制]]
  • 端口数限制:TCP 协议中使用 Unsigned Short 表示端口,理论最大端口65535,参考 [[#端口数量限制]]
  • 打开文件描述符数量上限:Linux 中每打开一个 Socket 都占用进程的文件描述符,参考 [[#进程最大打开文件数]]
  • 对于多进程/多线程模型的应用,系统能创建的进线程上限也会影响到并发数,参考 [[#最大进线程数]]

  • 建立一条 TCP 连接的内存开销,也影响连接数的上限:Linux 为内核对象申请空间使用 Slab 机制,在 Linux 3.10.0 版本中,创建一个 socket 需要消耗 densty、flip、sock_inode_cache、TCP 四个内核对象。这些对象加起来总共需要消耗大约 3 KB 多一点的内存;

  • 服务器端应用层设置:Nginx、Apache、Tomcat 都有各自的最大并发数限制选项

进程最大打开文件数

Linux 中每打开一个 Socket 都占用进程的文件描述符,使用 ulimit 设置最多能打开文件数:

ulimit 起作用的范围是”当前 Shell”, 并不是作用于”当前用户”, 即使用 ulimit 修改了当前限制,使用同一个用户在新的 shell 中登录,之前修改的值不生效。

  • 查看所有的限制: ulimit -a
  • 设置最大打开文件句柄数: ulimit -n 65535
  • 设置 每个用户的 最大进程数: ulimit -u 32768
  • 设置线程栈的大小: ulimit -s 10240
  • 设置最大线程数数: ulimit -T (在 Unix 上可能不同)
  • 设置产生 core 文件大小: ulimit -c xxx
  • 不限制 core 的大小: ulimit -c unlimited

如要对”用户”级别做限制, 则需要修改系统文件 /etc/security/limits.conf:

# * 表示所有用户, nofile表示限制文件打开数, 限制在100
# 注意 [hard nofile]一定要比 fs.nr_open 要小,否则可能导致用户无法登陆
* soft nofile 55000
* hard nofile 100

如果是针对整个系统, 则需要使用 sysctl 修改, 命令格式为: sysctl -w fs.nr_open=10000000, 每个系统参数对应一个/proc 下的文件, fs.nr_open 对应的文件路径是 /proc/sys/fs/nr_open
系统最大打开文件数相关的参数有两个:

  • fs.nr_open,进程级别
  • fs.file-max,系统级别

至此总结一下, “Linux 系统最多能打开文件数” 有当前 shell, 进程, 用户, 系统三个级别,
shell 级别的更改限制命令是 ulimit,
用户级别更改 limits.conf 文件,
而更改进程/系统级别限制的命令是 sysctl(fs.nr_open, fs.file-max)

限制优先级最大的是 fs.file-max, 假如 fs.file-max 设置为 100 万, ulimit 是不能超过 100万的.

cat /proc/sys/fs/file-nr, 输出 9344 0 592026,分别为:1.已经分配的文件句柄数,2.已经分配但没有使用的文件句柄数,3.最大文件句柄数
file-nr 不是单个进程的限制, 是系统级的, 最后一个数字与 file-max 相同

➤ 系统打开文件数有上限吗?
还没查到靠谱的资料,参考 RH6 手册,对于文件的限制,只有最大文件大小、最多子目录的数量等待,没有提到打开文件数 Red Hat Enterprise Linux 的技术能力和限制 - Red Hat Customer Portal

➤ 如何查看已创建文件描述符数?

  • 某进程打开文件数 ll /proc/1599/fd | wc -l
  • 系统全部打开的文件数 lsof | wc -l
  • 某进程打开 socket 的数量: ll /proc/1599/fd | grep socket | wc -l # nginx 一个 worker 打开了 200-300 个 socket

其他:

  • 系统全部打开的 TCP 连接数 lsof | grep TCP | wc -l
  • 查看 TCP 不同状态连接数: netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
    • 注意处于 TIME_WAIT 的链接, 如果这个数过高会占用大量连接, 应该调整参数尽快的释放 time_wait 连接

在 Nginx 机器上测试:

[@zw_85_63 ~]# netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
TIME_WAIT 37968
SYN_SENT 1
FIN_WAIT1 5
FIN_WAIT2 4
ESTABLISHED 2725
SYN_RECV 18
LAST_ACK 4

端口数量限制

TCP 协议中使用 Unsigned Short 表示端口,所以 TCP 理论最大可用端口数量有 65535 个,但是一般的系统里 1024 以下的端口都是保留的,可用的大约就是 64 k 个. 默认情况下,Linux 只开启了 3 万多个可用端口:

#vi /etc/sysctl.conf
net.ipv4.ip_local_port_range = 5000 65000

但对于服务端,端口数不是问题,因为一个监听的端口可以复用,
对于客户端,端口其实也是可以复用的,但还是有四元组的限制(举例,如果 Client 用一个端口,那么 dst_ip dst_port 的组合不能相同)

四元组相关的代码, 参考 net/ipv4/inet_hashtables.c 下的 INET_MATCH 宏

如果 Client 只连接一个 Server(这种情况下 dst_ip 和 dst_port 被固定了),那么作为 Client 只能一个 port 建立一个连接了,也就受到 TCP 协议端口数上限的限制。如果 Client 启动大量并发线程去连接 Server,这时候可能遇到一个问题:TIME_WAIT 状态的连接过多,当端口资源被耗尽,就无法与这个服务器再建立连接了。

但有个优化选项:打开 net.ipv4.tcp_tw_reuse 这个内核参数,客户端调用 connect 函数时,如果选择到的端口,已经被相同四元组的连接占用的时候,就会判断该连接是否处于 TIME_WAIT 状态,如果该连接处于 TIME_WAIT 状态并且 TIME_WAIT 状态持续的时间超过了 1 秒,那么就会重用这个连接,然后就可以正常使用该端口了。

最大进线程数

某些服务程序(Apache, Tomcat) 采用 “Thread Per Request”, 系统的进线程最大数也会影响并发性能.

Linux 没有 直接限制 每个进程能够创建线程数, 仅限制了系统最大进线程数, 相关的配置有 :

  • 仅对当前 shell 有效: ulimit -u 102400, -u 表示 “max user processes”;
  • 系统级别有效:
    1. 临时生效: echo 102400 > /proc/sys/kernel/threads-maxsysctl -w sys.kernel.threads-max=10240 ;
    2. 永久生效: 修改 /etc/sysctl.conf 文件;

这里的 threads-max 不是指进程, 是 “maximum number of threads that can be created using fork()”,
@ref https://www.kernel.org/doc/Documentation/sysctl/kernel.txt

每个进程能创建的最大线程数, 是由 total virtual memory 和 stack size 共同决定的, number of threads = total virtual memory / stack size
这两个参数分别用 ulimit -v xxxulimit -s xxx 设置

此外系统能创建最大进程数还受 kernel.pid_max 影响:

  • 方式1 运行时限制,临时生效 echo 999999 > /proc/sys/kernel/pid_max
  • 方式2 修改/etc/sysctl.conf,永久生效 sys.kernel.pid_max = 999999

但是线程数没限制不代表”Thread Per Request”可以行得通,如果创建了百万级的线程,上下文切换对 CPU 的调度也是考验

应用程序的设置

首先考虑,服务端程序(Ngx、Redis、Tomcat 等)采用哪种 IO 线程模型?

如果是 BIO(比如较早版本的 Tomcat ,当然现在很少有 BIO 实现的服务了),一般采用的是 Connection pre thread 的方式,意味着需要起 10K-100K 线程,所以 BIO 模式直接 pass

如果是多路复用,那还要看是用的 select ?poll?epoll?
select 和 poll 都无法胜任 C10K,只有 epoll 可以,原因见 [[../21.Operating-System/APUE.07b.网络编程-多路复用epoll]],

当然现在的服务端 Ngx、Redis 、Java 的 Netty 等都用的是 epoll,少数系统上可能会用 select、kqueue 等,Nginx & Redis 的线程模型解析=> [[../21.Operating-System/APUE.07d.服务端常用IO模型]]

nginx

  • worker_rlimit_nofile 65535; // 一个 nginx 进程打开的最多文件描述符数目
  • listen 8080 backlog=168888; // accept 成功队列长度,用于 listen(int sockfd, int backlog),默认值 511

  • worker_connections= //每个 worker 线程能创建的连接数

  • upstream 可以使用 http 1.1的 keepalive //与后端服务器创建的连接池大小
  • worker_processes 8; // nginx 进程数,一般等于 cpu core 数量
  • worker_cpu_affinity 00000001 00000010 00000100 00001000 00010000 00100000; // 每个进程分配到 cpu 的 core 上

redis

@todo

apache

默认是多进程同步处理 request, 所以思路和 Nginx 每个 Core 一个进程 epoll 轮询的方式不同, apache 应该增加”系统创建进程数上限”, 并且减小进程栈内存

tomcat

Tomcat 也提供 NIO 模式(epoll + 非阻塞 IO),但 Tomcat 是个“跑业务代码”的服务,业务代码比较耗费时间(各种 CRUD),所以 Tomcat 注定不能像 Nginx 那样,在 epoll 线程里处理所有。

Tomcat 采用的是 1 个线程 accept,多个线程( core 数的 2 倍个)监听已建连接的 IO 事件,收到一个 Http Req 则扔进线程池处理,线程池大小默认几百。参考 [[../13.JavaEE-Framework/JavaEE.Tomcat]]]

  • maxThreads=500,业务线程池大小,此值限制了 bio 的最大连接数

    一般的当一个进程有 500 个线程在跑的话,那性能已经是很低很低了。Tomcat 默认配置的最大请求数是 150。当某个应用拥有 250 个以上并发的时候,应考虑应用服务器的集群。

  • acceptCount:默认是 100

  • maxConnection=10000: NIO 模式下的默认值是 1w(可以认为所有 Poller 线程用 epoll 监听)可以适当调大,因为大多数连接都已建立但空闲

Tomcat 的 maxThreads 、acceptCount、maxConnection 的解析参考 @ref [[../13.JavaEE-Framework/JavaEE.Tomcat#性能优化]] 性能优化一节。

FastCGI

  • fastcgi_connect_timeout 300;

高并发配置-无废话总结

  1. 应用程序的并发设置: 主要是 timeout, 进/线程数这几类参数
  2. 操作系统打开文件数量限制: ulimit -n 单个 Shell 环境的限制, sysctl -w fs.file-max 修改系统打开文件限制
  3. 操作系统打开端口数量限制: 最大端口数 65535(2^16), 但 1024 以后的端口是给系统用的
  4. sysctl 修改的 TCP 协议栈参数

并发性能测试工具

ab(Apache Bench)

1000并发, 总共20000次请求: ab -n 20000 -c 1000 <url>

http_load

30个并发线程, 共60秒测试: http_load -p 30 -s 60 Url.txt

JMeter

配置

@todo

测试

@todo

报告

在聚合报告中,会显示一行数据,共有 10 个字段,含义分别如下。

  • Label:每个 JMeter 的 element(例如 HTTP Request)都有一个 Name 属性,这里显示的就是 Name 属性的值
  • Samples:表示你这次测试中一共发出了多少个请求,如果模拟 10 个用户,每个用户迭代 10 次,那么这里显示 100
  • Average:平均响应时间——默认情况下是单个 Request 的平均响应时间,当使用了 Transaction Controller 时,也可以以 Transaction 为单位显示平均* 响应时间
  • Median:中位数,也就是 50% 用户的响应时间
  • 90% Line:90% 用户的响应时间
  • Min:最小响应时间
  • Max:最大响应时间
  • Error%:本次测试中出现错误的请求的数量/请求的总数
  • Throughput:吞吐量——默认情况下表示每秒完成的请求数(Request per Second)
  • KB/Sec:每秒从服务器端接收到的数据量,相当于 LoadRunner 中的 Throughput/Sec

参考: 使用JMeter进行负载测试——终极指南 - ImportNew @ref

wrk

wg/wrk: Modern HTTP benchmarking tool

参考