分布式系统基础学习(04)--Nginx

Nginx不管是在单机部署还是在集群部署下, 都起着非常重要的作用。 底层由C语言编写, 并且采用事件驱动模型对Socket连接进行管理, 所以有着非常高效的请求处理能力, 并且支持海量的并发。 本篇文章不会对Nginx的底层原理进行深究, 而是整理自己在工作中所遇到的一些问题和积累的经验。

1. 进程数与连接数的优化

由于大多数后端服务器均采用centos或者是ubuntu作为服务器操作系统, 所以天然的支持epoll事件模型的使用, 自然而然的, 需要为Nginx在socket处理模型上进行优化。

1
2
3
events {
use epoll;
}

这个配置通常会置于全局的配置文件中, 也就是/etc/nginx/nginx.conf中。 对于epoll这样的事件驱动模型来说, 通常服务端都会有2个进程一起协同配合来完成请求的处理。

在Nginx中, Master进程主要用于处理配置文件的读取以及请求的分发, 并不参与请求的实际处理过程, 所以Master进行通常只有一个, 并且Nginx并没有为我们开放配置Master进程数的配置。 Worker进程通常有一个或者是多个, 在每个Worker进程中, 都会运行epoll事件模型, 因此, Worker进程数量应该与CPU核心数保持一致, 如果是4核CPU的话Worker进程数应该也为4。

1
worker_processes auto;

这里使用了auto配置来自动的配置Worker进程数与CPU核心数保持一致。

接下来是Worker进程最大连接数的优化, 这个配置没有那么简单, 需要对服务器所运行的服务以及服务器配置, 最大并发数等数据进行全方位的了解, 并使用严密的压力测试对配置进行测试, 最终才能够得出结果。

在epoll模型中, 每一个Socket连接都需要消耗一个文件描述符, 而由于内存大小的限制, 一台服务器能够占用的描述符最大数量也是不定的。 首选我们需要对一台服务器能够打开多少个文件句柄要有一个判断:

1
cat /proc/sys/fs/file-max

在一台16G内存的Ubuntu服务器中, 结果为1623585, 核心数为8, 所以从最大句柄的角度来看, worker_connections最大能够配置202948, 大概20万那么个样子。 但是系统中还会有其它的进程运行, 也会占用系统资源, 所以在实际生产中, 8核16G的机器, 是不可能开到这么大的单个进程的连接数的。

保险一点, 这个值给到10万, 那么Nginx最多能够处理80万的Socket文件句柄, 作为反向代理, 所支持的最大瞬时并发数为800000/4=200000, 20万的瞬时并发处理, 在绝大多数场景下都够用, 具体的表现仍然需要看压测的结果。

这里给出一个常规的配置:

1
2
3
4
worker_processes auto;
events {
worker_connections 100000;
}

2. Zero-copy优化

用户在读取一个文件, 修改部分内容, 并将其再写入文件的过程, 有着多次用户态和内核态的切换。 操作系统的作用就是帮助用户管理硬件, 所以, 将文件内容刷盘到磁盘中, 或者是写入到Socket连接中, 这些过程都需要从用户态切换到内核态, 由内核完成这些动作。 在切换的过程中就会有时间上的损耗, 所以需要对其进行优化。

sendfile配置本质上是使用DMA控制器来完成数据的所有拷贝工作, 让CPU处理其它的事情。 首先CPU设置DMA控制器, 让它将数据从磁盘设备中拷贝至内核Buffer中, 然后向SocketBuffer中追加当前要发送的数据在KernelBuffer中的位置和偏移量, DMA gather copy根据偏移量直接从KernelBuffer里面将需要的内容拷贝至网卡或者是磁盘设备中。 这个过程CPU只有极少的参与, 数据拷贝过程完全不需要CPU的过多干涉, 所以能够提升系统性能。

不过虽然sendfile能够实现”无CPU”数据拷贝, 从而提升效率的功能, 但是Nginx在作为反向代理的时候, 这个配置作用不大。 而作为静态资源服务器的时候, 例如图片服务, 小文件的下载等, 能够得到较大效率的提升。

3. gzip优化

gzip配置在前端的HTML, CSS以及JS文件压缩中比较常用, 后端的API服务很少会用到这个配置。 该配置是将请求的文件进行在线压缩, 以达到更快的网络传输。 后端开发者了解一下就好。

4. 日志格式的扩充

1
2
3
4
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" $request_time $upstream_response_time'
'$upstream_addr';

这里额外的补充了请求的响应时间, 负载均衡服务器响应时间以及该请求实际的处理服务器地址, 这些数据在搭建ELK或者是使用Python脚本对access_log进行日志分析时将会提供非常有用的帮助。

5. root和alias的区别

rootalias的区别在网上很少有人讲到, 自己在使用中也是踩到了这个坑。

5.1 root

假设有如下配置:

1
2
3
location /test {
root /var/www/html;
}

/var/www/html目录下新建一个目录test, 再在test目录下新建一个smile.txt文件并修改文件权限, 里面随便写一点儿东西。
访问smile.txt: localhost/test/smile.txt

1
smile

得到了smile.txt文本中的内容。 所以root方式的配置实际请求路径为/var/www/html/test/smile.txt。 那么很明显的, url访问路径, 其实就是服务器中路径的子路径, /test既表示匹配规则, 也表示文件路径。

5.2 alias

修改上面的配置:

1
2
3
location /test {
alias /var/www/html/new_test/;
}

new_test文件夹下新建一个smile.txt文件, 同样的使用localhost/test/smile.txt进行访问, 得到了:

1
new_smile

也就是说访问/test/smile.txt时, 服务器实际上是去访问了/var/www/html/new_test/smile.txt这个文件。 /test仅仅作为nginx的匹配规则, 而不是路径, 具体的资源访问路径由alias来进行确定。

6. nginx: [emerg] host not found in upstream报错问题

在我们配置完nginx配置文件, 并使用nginx -t来检测配置是否正确时, 可能会抛出上述问题。 通常来讲我们的配置是没有问题的, 只不过proxy_pass后面的域名nginx无法进行解析。 例如:

1
2
3
location / {
proxy_pass: https://smartkeyerror.com;
}

然而在浏览器中该域名能够访问, 此时我们需要手动的为该域名指定ip地址。
编辑/etc/hosts文件:

1
2
3
4
5
# 如果该域名的服务就在本机的话
127.0.0.1 localhost smartkeyerror.com

# 如果该域名的服务不在本机
120.33.54.178 smartkeyerror.com

编辑保存后即可。

未完待续…..