Nginx的系统结构

Nginx包含一个单一的master进程和多个worker进程。所有的这些进程都是单线程,并且设计为同时处理成千上万的连接。worker进程是处理连接的地方,它用于处理客户端请求。master进程负责读取配置文件、处理套接字。派生worker进程、打开日志文件等。总之,master进程是一个可以通过处理信号响应来管理请求的进程。
Nginx性能优化实践 - 图1

Nginx性能参数调优

常规参数讲解

进入/etc/nginx文件夹,编辑nginx.conf,可以看到下面的参数。

  1. # nginx进程数,建议按照cpu数目来指定,一般跟cpu核数相同或为它的倍数。
  2. worker_processes 8;
  3. # 每个worker 进程的最大连接数
  4. worker_connections 1024
  5. #为每个进程分配cpu,上例中将8个进程分配到8个cpu,当然可以写多个,或者将一个进程分配到多个cpu。
  6. worker_cpu_affinity 00000001 00000010 00000100 00001000 00010000 00100000 01000000 10000000;
  7. # 作用于event的I/O多路复用模型
  8. use epoll;
  9. #收到新连接通知后接受尽可能多的连接,作用于event
  10. multi_accept on;

eopll接口作为poll接口的变体,在Linux内核2.5中被引入。相比select实现的多路复用I/O模型,epoll最大的好处在于它不会随着被监控描述符数目的增长而导致效率急速下降。

worker_process number;

每个worker进程都是单线程的进程,他们会调用各个模块以实现多种多样的功能。如果这些模块确认不会出现阻塞式的调用,那么,有多少CPU内核就应该配置多少个进程;反之,如果有可能出现阻塞式调用,那么配置稍微多一些的worker进程。
例如:如果业务方面会致使用户请求大量读取本地磁盘上的静态资源文件,而且服务器上的内存较小,以至于大部分的请求访问静态资源文件时都必须读取磁盘(磁头的寻址是缓慢的),而不是内存中的磁盘缓存,那么磁盘I/O的调用可能会阻塞住worker进程少量时间,进而导致服务整体性能下降。

每个worker进程的最大连接数

  1. 语法:worker_connections number;
  2. 默认:worker_connections 1024

worker_cpu_affinity cpumask[cpumask……]

绑定Nginx worker进程到指定的CPU内核

为什么绑定worker进程到指定的CPU内核呢?假定每一个worker进程都是非常繁忙的,如果多个worker进程都在抢同一个CPU,那么这就会出现同步问题。反之,如果每一个worker进程都独享一个CPU,就在内核的调度上实现了完全的并发。

例如,如果有4颗CPU内核,就可以进行如下配置:

  1. worker_processes 4;
  2. worker_cpu_affinity 1000 0100 0010 0001;

注意worker_cpu_affinity 配置仅对Linux操作系统有效。

  1. 查看当前机器cpu
  2. # 查看当前机器物理cpu数量
  3. cat /proc/cpuinfo|grep "physical id"|sort|uniq|wc -l
  4. # 1
  5. # 查看当前机器物理cpu核心数量
  6. cat /proc/cpuinfo|grep "cpu cores"|uniq
  7. #cpu cores : 2

Nginx worker进程优先设置

  1. 语法:worker_priority nice;
  2. 默认:worker_priority 0;

优先级由静态优先级和内核根据进程执行情况所做的动态调整(目前只有±5的调整)共同决定。nice值是进程的静态优先级,他的取值范围是-20~+19,-20是最高优先级,+19是最低优先级。因此,如果用户希望Nginx占有更多的系统资源,那么可以把nice值配置的更小一些,但不建议比内核进程的nice值(通常为-5)还要小

Nginx worker进程可以打开的最大句柄描述符个数

  1. 语法:worker_rlimit_nofile limit;
  2. 默认:空

更改worker进程的最大打开文件数限制。如果没设置的话,这个值为操作系统的限制。设置后你的操作系统和Nginx可以处理比”ulimit -a“更多的文件,所以把这个值设高,这样nginx就不会有“too many open files” 问题了

是否打开accept锁

  1. 语法:accept_mutex[on|off]
  2. 默认:accept_mutext on;

accept_mutex是Nginx的负载均衡锁,当某一个worker进程建立的连接数量达到work_connections配置的最大连接数的7/8时,会大大地减小该worker进程试图建立新TCP连接的机会,accept锁默认是打开的,如果关闭它,那么建立TCP连接的耗时会更短,但worker进程之间的负载会非常不均衡,因此不建议关闭它。

使用accept锁后到真正建立连接之间的延迟时间

  1. 语法:accept_mutex_delay Nms;
  2. 默认:accept_mutex_delay 500ms;

在使用accept锁后,同一时间只有一个worker进程能够取到accept锁。这个accept锁不是堵塞锁,如果取不到会立刻返回。如果只有1个worker进程试图取锁而没有取到,他至少要等待acept_mutex_delay定义的时间才能再次试图取锁。

Nginx高速缓存实战

某电商平台商品详情页需要实现700+QPS,如何着手去做?
Nginx性能优化实践 - 图2

对于商品详情页涉及了如下主要服务:

  1. 商品详情页HTML页面渲染
  2. 价格服务
  3. 促销服务
  4. 库存状态/配送至服务
  5. 广告词服务
  6. 预售/秒杀服务
  7. 评价服务
  8. 试用服务
  9. 推荐服务
  10. 商品介绍服务
  11. 各品类相关的一些特殊服务

解决方案:

  1. 采用Ajax动态加载价格、广告、库存等服务
  2. 采用key value 缓存详情页主体html

方案架构

Nginx性能优化实践 - 图3

问题

当达到500QPS的时候很难继续压测上去。

分析原因

一个详情页html主体达平均150kb,那么在500QPS已接近千M局域网宽带极限。必须减少内网通信。

基于Nginx静态缓存的解决方案:

Nginx性能优化实践 - 图4

Nginx缓存配置

配置步骤

  1. 客户端、代理请求缓存
  2. 设置缓存空间,存储缓存文件
  3. 在location中使用缓存空间
  4. 打开文件的缓存配置
    1. #客户端请求主体的缓冲区大小
    2. client_body_buffer_size 512k;
    3. #客户端请求头部的缓冲区大小,这个可以根据系统分页大小来设置
    4. client_header_buffer_size 4k;
    5. client_max_body_size 512k;
    6. large_client_header_buffers 2 8k;
    7. proxy_buffer_size 16k;
    8. proxy_buffers 4 64k;
    9. proxy_busy_buffers_size 128k;
    10. proxy_temp_file_write_size 128k;
    11. #指定在何种情况下一个失败的请求应该被发送到下一台后端服务器
    12. proxy_next_upstream http_502 http_504 http_404 error timeout invalid_header;
    13. #设置缓存空间,存储缓存文件
    14. proxy_cache_path /usr/local/nginx/cache levels=1:2 keys_zone=nginx-cache:20m max_size=50g inactive=168h;
    15. #在location中使用缓存空间,pathname是项目的目录,请自定义
    16. location /pathname {
    17. proxy_set_header X-Real-IP $remote_addr;
    18. proxy_cache nginx-cache;
    19. proxy_cache_valid 200 304 302 5d;
    20. proxy_cache_valid any 5d;
    21. proxy_cache_key '$host:$server_port$request_uri';
    22. add_header X-Cache '$upstream_cache_status from $host';
    23. proxy_set_header X-Real-IP $remote_addr;
    24. proxy_pass http://localhost/pathname;
    25. }
    26. #打开文件的缓存配置
    27. #为打开文件指定缓存,默认是没有启用的,max 指定缓存数量,建议和打开文件数一致,inactive 是指经过多长时间文件没被请求后删除缓存。
    28. open_file_cache max=65535 inactive=60s;
    29. #文件描述符一直是在缓存中打开的,如果有一个文件在inactive时间内一次没被使用,它将被移除。
    30. open_file_cache_min_uses 1;
    31. #指定多长时间检查一次缓存的有效信息。
    32. open_file_cache_valid 80s;

缓存参数详细说明

Nginx性能优化实践 - 图5

查看缓存目录

上述配置的结果,重启生效后,我们可以看到生成了很多缓存文件在缓存存储路径为:/usr/local/nginx/cache,levels=1:2代表缓存的目录结构为2级目录

如下图,缓存会在/usr/local/nginx/cache目录下生成,包含2级目录,在之下就是缓存文件,测试的时候可以到该目录下查看缓存文件是否生成
Nginx性能优化实践 - 图6

我们打开其中一个文件看看,会发现一些蹊跷,这里存储了请求头的信息。当我们处理一个HTTP请求的时候,他会先从这里读取。

缓存的清除

该功能可以采用第三方模块 ngx_cache_purge 实现。为nginx添加ngx_cache_purge模块

  1. #下载ngx_cache_purge 模块包 ,这里nginx 版本为1.6.2 purge 对应2.0版
  2. wget http://labs.frickle.com/files/ngx_cache_purge-2.3.tar.gz
  3. #查看已安装模块
  4. ./sbin/nginx -V
  5. #进入nginx安装包目录 重新安装 --add-module为模块解压的全路径
  6. ./configure --prefix=/root/svr/nginx --with-http_stub_status_module --with-http_ssl_module --add-module=/root/svr/nginx/models/ngx_cache_purge-2.0
  7. #重新编译
  8. make
  9. #拷贝 安装目录/objs/nginx 文件用于替换原nginx 文件
  10. #检测查看安装是否成功
  11. nginx -t

清除配置

  1. location ~ /clear(/.*) {
  2. #允许访问的IP
  3. allow 127.0.0.1;
  4. allow 192.168.0.193;
  5. #禁止访问的IP
  6. deny all;
  7. #配置清除指定缓存区和路径(与proxy_cache_key一至)
  8. proxy_cache_purge cache_item '$host:$server_port$request_uri';
  9. }

配置好以后直接访问:
这里 192.168.0.193 域名设置为 www.test.com

  1. # 访问生成缓存文件
  2. http://www.test.com/?a=1
  3. # 清除生成的缓存,如果指定缓存不存在 则会报404 错误。
  4. http://www.testcom/clear/?a=1

指定不缓存页面

配置语法

Nginx性能优化实践 - 图7

例子:

  1. ...
  2. # 判断当前路径是否是指定的路径
  3. if($request_uri ~ ^/(url3|login|register|password\/reset)) {
  4. #设置一个变量用来存储是否是需要缓存
  5. set $cookie_nocache 1;
  6. }
  7. location / {
  8. ...
  9. proxy_no_cache $cookie $arg_nocache $arg_comment;
  10. ...
  11. }
  12. ...

缓存命中分析

在http header上增加命中显示

Nginx提供了$upstream_cache_status 这个变量来显示缓存的状态,我们可以在配置中添加一个http头来显示这一状态,达到类似squid的效果。

  1. location / {
  2. proxy_redirect off;
  3. proxy_set_header Host $host;
  4. proxy_set_header X-Real-IP $remote_addr;
  5. proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  6. proxy_connect_timeout 180;
  7. proxy_send_timeout 180;
  8. proxy_read_timeout 180;
  9. proxy_buffer_size 128k;
  10. proxy_buffers 4 128k;
  11. proxy_busy_buffers_size 128k;
  12. proxy_temp_file_write_size 128k;
  13. proxy_cache cache;
  14. proxy_cache_valid 200 304 1h;
  15. proxy_cache_valid 404 1m;
  16. proxy_cache_key '$host:$server_port$request_uri';
  17. add_header Nginx-Cache "$upstream_cache_status";
  18. proxy_pass http://backend;
  19. }

而通过curl或浏览器查看到的header如下:

  1. HTTP/1.1 200 OK
  2. Date: Mon, 22 Apr 2013 02:10:02 GMT
  3. Server: nginx
  4. Content-Type: image/jpeg
  5. Content-Length: 23560
  6. Last-Modified: Thu, 18 Apr 2013 11:05:43 GMT
  7. Nginx-Cache: HIT
  8. Accept-Ranges: bytes
  9. Vary: User-Agent

$upstream_cache_status 包含以下几种状态

  1. MISS 未命中,请求被传送到后端
  2. HIT 缓存命中
  3. EXPIRED 缓存已过期,请求被传送带后端
  4. UPDATING 正在更新缓存,将使用旧的应答
  5. STALE 后端将得到过期的应答

Nginx cache命中率统计

既然nginx为我们提供了$upstream_cache_status 函数,自然可以将命中状态写入到日志中。具体可以如下定义日志格式

  1. # crontab -l
  2. 1 0 * * * /opt/<a href="http://www.test.com/" title="shell"target="_blank">shell</a>/nginx_cache_hit >> /usr/local/nginx/logs/hit

该脚本的内容为

  1. #!/bin/bash
  2. LOG_FILE='/usr/local/nginx/logs/access.log.1'
  3. LAST_DAY=$(date +%F -d "-1 day")
  4. awk '{if($NF==""HIT"") hit++} END {printf "'$LAST_DAY': %d %d %.2f%n", hit,NR,hit/NR}' $LOG_FILE

Nginx其他优化措施

动静分离

将静态文件存储到/usr/share/nginx/html文件夹中,配置静态文件拦截规则。单个项目可以直接放在/usr/share/nginx/html根目录下,如果是多个项目,则要根据项目的根目录创建文件夹来保存静态文件。

配置拦截规则如下:

  1. location ~ .*\.(eot|svg|ttf|woff|jpg|jpeg|gif|png|ico|cur|gz|svgz|mp4|ogg|ogv|webm) {
  2. #所有静态文件直接读取硬盘
  3. root /usr/share/nginx/html;
  4. expires 30d; #缓存30天
  5. }
  6. location ~ .*\.(js|css)?${
  7. #所有静态文件直接读取硬盘
  8. root /usr/share/nginx/html;
  9. expires 12h;
  10. }

结果:整个网站的访问速度都会减少,大部分内容都将从静态文件目录或硬盘中读取

连接超时

长时间占着连接资源不释放,最终会导致请求的堆积,Nginx处理请求效率大大降低。所以我们对连接的控制都要注意设置超时时间,通过超时机制自动回收资源,避免资源浪费

  1. #客户端、服务端设置
  2. server_names_hash_bucket_size 128;
  3. server_names_hash_max_size 512;
  4. # 长连接超时配置
  5. keepalive_timeout 65;
  6. client_header_timeout 15s;
  7. client_body_timeout 15s;
  8. send_timeout 60s;
  9. #代理设置
  10. #与后端服务器建立连接的超时时间。注意这个一般不能大于75秒
  11. proxy_connect_timeout 30s;
  12. proxy_send_timeout 120s;
  13. #从后端服务器读取响应的超时
  14. proxy_read_timeout 120s;

GZIP压缩

在我们进行gzip打包压缩之前,最好将一些静态文件先进行压缩为min文件,请求的时候合并为同一文件。再通过gzip压缩后,你会发现网站加载又加速了。

  1. #开启gzip,减少我们发送的数据量
  2. gzip on;
  3. #允许压缩的最小字节数
  4. gzip_min_length 1k;
  5. #4个单位为16k的内存作为压缩结果流缓存
  6. gzip_buffers 4 16k;
  7. #设置识别HTTP协议版本,默认是1.1
  8. gzip_http_version 1.1;
  9. #gzip压缩比,可在1~9中设置,1压缩比最小,速度最快,9压缩比最大,速度最慢,消耗CPU
  10. gzip_comp_level 4;
  11. #压缩的类型
  12. gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
  13. #给代理服务器用的,有的浏览器支持压缩,有的不支持,所以避免浪费不支持的也压缩,所以根据客户端的HTTP头来判断,是否需要压缩
  14. gzip_vary on;
  15. #禁用IE6以下的gzip压缩,IE6的某些版本对gzip的压缩支持很不好
  16. gzip_disable "MSIE [1-6].";

访问限流

我们构建网站是为了让用户访问他们,我们希望用于合法访问。所以不得不采取一些措施限制滥用访问的用户。这种滥用指的是从同一IP每秒到服务器请求的连接数。因为这可能是在同一时间内,世界各地的多台机器上的爬虫机器人多次尝试爬取网站的内容

  1. #限制用户连接数来预防DOS攻击
  2. limit_conn_zone $binary_remote_addr zone=perip:10m;
  3. limit_conn_zone $server_name zone=perserver:10m;
  4. #限制同一客户端ip最大并发连接数
  5. limit_conn perip 2;
  6. #限制同一server最大并发连接数
  7. limit_conn perserver 20;
  8. #限制下载速度,根据自身服务器带宽配置
  9. limit_rate 300k;

搞数据传输配置

  1. #开启文件的高效传输模式。tcp_nopush和tcp_nodelay可防止网络及磁盘i/o阻塞,提升nginx工作效率;
  2. sendfile on;
  3. #数据包不会马上传送出去,等到数据包最大时,一次性的传输出去,这样有助于解决网络堵塞。
  4. tcp_nopush on;
  5. #只要有数据包产生,不管大小多少,就尽快传输
  6. tcp_nodelay on;

内核参数的优化

编辑/etc/sysctl.conf文件,根据需要调整参数配置

  1. #如果想把timewait降下了就要把tcp_max_tw_buckets值减小,默认是180000
  2. net.ipv4.tcp_max_tw_buckets = 5000
  3. #开启重用功能,允许将TIME-WAIT状态的sockets重新用于新的TCP连接
  4. net.ipv4.tcp_tw_reuse = 1
  5. #系统中最多有多少个TCP套接字不被关联到任何一个用户文件句柄上。如果超过这个数字,孤儿连接将即刻被复位并打印出警告信息。这个限制仅仅是为了防止简单的DoS攻击
  6. net.ipv4.tcp_max_orphans = 262144
  7. #当keepalive起用的时候,TCP发送keepalive消息的频度。缺省是2小时。我们可以调短时间跨度
  8. net.ipv4.tcp_keepalive_time = 30

日志配置

日志文件对于我们日常运维至关重要,如果没有日志排查问题,你将很难判断异常的所在,要解决问题无异于大海捞针。日志的保存是必要的,不可缺少的,我们来看下怎么配置有利于排查问题?

  1. 关键字:其中关键字access_log,error_log不能改变
  2. error_log 错误日志级别:【debug | info | notice | warn | error | crit | alert | emerg】,级别越高记录的信息越少。不要配置info等级较低的级别,会带来大量的磁盘I/O消耗

error_log 生产场景一般是 warn | error | crit 这三个级别之一

  1. #定义日志模板
  2. log_format 日志模板名称 日志格式;
  3. #访问日志
  4. access_log path format gzip[=level] [buffer=size] [flush=time];
  5. 关键字 存放的路径 日志格式模板 gzip压缩,level指压缩级别 存放缓存日志的缓存大小 保存在缓存中的最长时间
  6. #错误日志
  7. error_log <FILE> <LEVEL>;
  8. 关键字 日志文件 错误日志级别
  9. #示例
  10. log_format main '$remote_addr - $remote_user [$time_local] "$request"'
  11. '$status $body_bytes_sent "$http_referer"'
  12. '"$http_user_agent" "$http_x_forwarded_for"';