Nginx 性能優(yōu)化實踐
Nginx 的系統(tǒng)結構
Nginx 包含一個單一的 master 進程和多個 worker 進程。所有的這些進程都是單線程,并且設計為同時處理成千上萬個連接。worker 進程是處理連接的地方,它用于處理客戶端請求。master 進程負責讀取配置文件、處理套接字、派生 worker 進程、打開日志文件等??傊?, master 進程是一個可以通過處理信號響應來管理請求的進程。更多內容請查看《Nginx 核心模塊與配置實踐》。

Nginx 性能參數調優(yōu)
常規(guī)參數講解
進入 /etc/nginx 文件夾,編輯 nginx.conf ,可以看到下面的參數。簡單介紹下:
# nginx進程數,建議按照cpu數目來指定,一般跟cpu核數相同或為它的倍數。
worker_processes 8;
# 每個worker 進程的最大連接數
worker_connections 1024;
#為每個進程分配cpu,上例中將8個進程分配到8個cpu,當然可以寫多個,或者將一個進程分配到多個cpu。
worker_cpu_affinity 00000001 00000010 00000100 00001000 00010000 00100000 01000000 10000000;
# 作用于event的I/O多路復用模型
use epoll;
#收到新連接通知后接受盡可能多的連接,作用于event
multi_accept on;
epoll 接口作為 poll 接口的變體在 Linux 內核 2.5 中被引入。相比 select 實現的多路復用 I/O 模型,epoll 最大的好處在于它不會隨著被監(jiān)控描述符數目的增長而導致效率急速下降。
worker_processes number;
每個worker進程都是單線程的進程,它們會調用各個模塊以實現多種多樣的功能。如果這些模塊確認不會出現阻塞式的調用,那么,有多少CPU內核就應該配置多少個進程;反之,如果有可能出現阻塞式調用,那么需要配置稍多一些的worker進程。例如,如果業(yè)務方面會致使用戶請求大量讀取本地磁盤上的靜態(tài)資源文件,而且服務器上的內存較小,以至于大部分的請求訪問靜態(tài)資源文件時都必須讀取磁盤(磁頭的尋址是緩慢的),而不是內存中的磁盤緩存,那么磁盤I/O調用可能會阻塞住worker進程少量時間,進而導致服務整體性能下降。
每個worker 進程的最大連接數
語法:worker_connections number;
默認:worker_connections 1024;
worker_cpu_affinity cpumask[cpumask……]
綁定Nginx worker進程到指定的CPU內核
為什么要綁定worker進程到指定的CPU內核呢?假定每一個worker進程都是非常繁忙的,如果多個worker進程都在搶同一個CPU,那么這就會出現同步問題。反之,如果每一個worker進程都獨享一個CPU,就在內核的調度策略上實現了完全的并發(fā)。
例如,如果有4顆CPU內核,就可以進行如下配置:
worker_processes 4;
worker_cpu_affinity 1000 0100 0010 0001;
注意 worker_cpu_affinity配置僅對Linux操作系統(tǒng)有效。
查看當前機器cpu
# 查看當前機器物理cpu數量
cat /proc/cpuinfo|grep "physical id"|sort|uniq|wc -l
# 1
# 查看當前機器物理cpu核心數量
cat /proc/cpuinfo|grep "cpu cores"|uniq
#cpu cores : 2
Nginx worker 進程優(yōu)先級設置
語法:worker_priority nice;
默認:worker_priority 0;
優(yōu)先級由靜態(tài)優(yōu)先級和內核根據進程執(zhí)行情況所做的動態(tài)調整(目前只有±5的調整)共同決定。nice值是進程的靜態(tài)優(yōu)先級,它的取值范圍是–20~+19,–20是最高優(yōu)先級,+19是最低優(yōu)先級。因此,如果用戶希望Nginx占有更多的系統(tǒng)資源,那么可以把nice值配置得更小一些,但不建議比內核進程的nice值(通常為–5)還要小
Nginx worker進程可以打開的最大句柄描述符個數
語法: worker_rlimit_nofile limit;
默認: 空
更改worker進程的最大打開文件數限制。如果沒設置的話,這個值為操作系統(tǒng)的限制。設置后你的操作系統(tǒng)和Nginx可以處理比“ulimit -a”更多的文件,所以把這個值設高,這樣nginx就不會有“too many open files”問題了。
是否打開accept鎖
語法:accept_mutex[on|off]
默認:accept_mutext on;
accept_mutex是Nginx的負載均衡鎖,當某一個worker進程建立的連接數量達到worker_connections配置的最大連接數的7/8時,會大大地減小該worker進程試圖建立新TCP連接的機會,accept鎖默認是打開的,如果關閉它,那么建立TCP連接的耗時會更短,但worker進程之間的負載會非常不均衡,因此不建議關閉它。
使用accept鎖后到真正建立連接之間的延遲時間
語法:accept_mutex_delay Nms;
默認:accept_mutex_delay 500ms;
在使用accept鎖后,同一時間只有一個worker進程能夠取到accept鎖。這個accept鎖不是堵塞鎖,如果取不到會立刻返回。如果只有一個worker進程試圖取鎖而沒有取到,他至少要等待accept_mutex_delay定義的時間才能再次試圖取鎖。
Nginx 高速緩存實戰(zhàn)
案例分析
某電商平臺商品詳情頁需要實現 700+ QPS,如何著手去做?

對于商品詳情頁涉及了如下主要服務:
- 商品詳情頁HTML頁面渲染
- 價格服務
- 促銷服務
- 庫存狀態(tài)/配送至服務
- 廣告詞服務
- 預售/秒殺服務
- 評價服務
- 試用服務
- 推薦服務
- 商品介紹服務
- 各品類相關的一些特殊服務
解決方案:
采用Ajax 動態(tài)加載 價格、廣告、庫存等服務
采用key value 緩存詳情頁主體html。
方案架構:

問題:
當達到500QPS 的時候很難繼續(xù)壓測上去。
分析原因:
一個詳情頁html 主體達平均150 kb 那么在500QPS 已接近千M局域網寬帶極限。必須減少內網通信。
基于Nginx 靜態(tài)緩存的解決方案:

緩存配置
配置步驟
- 客戶端、代理請求緩存
- 設置緩存空間,存儲緩存文件
- 在 location 中使用緩存空間
- 打開文件的緩存配置
#客戶端請求主體的緩沖區(qū)大小
client_body_buffer_size 512k;
#客戶端請求頭部的緩沖區(qū)大小,這個可以根據系統(tǒng)分頁大小來設置
client_header_buffer_size 4k;
client_max_body_size 512k;
large_client_header_buffers 2 8k;
proxy_buffer_size 16k;
proxy_buffers 4 64k;
proxy_busy_buffers_size 128k;
proxy_temp_file_write_size 128k;
#指定在何種情況下一個失敗的請求應該被發(fā)送到下一臺后端服務器
proxy_next_upstream http_502 http_504 http_404 error timeout invalid_header;
#設置緩存空間,存儲緩存文件
proxy_cache_path /usr/local/nginx/cache levels=1:2 keys_zone=nginx-cache:20m max_size=50g inactive=168h;
#在location中使用緩存空間,pathname是項目的目錄,請自定義
location /pathname {
proxy_set_header X-Real-IP $remote_addr;
proxy_cache nginx-cache;
proxy_cache_valid 200 304 302 5d;
proxy_cache_valid any 5d;
proxy_cache_key '$host:$server_port$request_uri';
add_header X-Cache '$upstream_cache_status from $host';
proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://localhost/pathname;
}
#打開文件的緩存配置
#為打開文件指定緩存,默認是沒有啟用的,max 指定緩存數量,建議和打開文件數一致,inactive 是指經過多長時間文件沒被請求后刪除緩存。
open_file_cache max=65535 inactive=60s;
#文件描述符一直是在緩存中打開的,如果有一個文件在inactive時間內一次沒被使用,它將被移除。
open_file_cache_min_uses 1;
#指定多長時間檢查一次緩存的有效信息。
open_file_cache_valid 80s;
緩存參數詳細說明
| 父元素 | 名稱 | 描述 |
|---|---|---|
| http | proxy_cache_path | 指定緩存區(qū)的根路徑 |
| levels | 緩存目錄層級最高三層,每層1~2個字符表示。如1:1:2 表示三層。 | |
| keys_zone | 緩存塊名稱 及內存塊大小。如 cache_item:500m 。表示聲明一個名為cache_item 大小為500m。超出大小后最早的數據將會被清除。 | |
| inactive | 最長閑置時間 如:10d 如果一個數據被閑置10天將會被清除 | |
| max_size | 緩存區(qū)硬盤最大值。超出閑置數據將會被清除 | |
| location | proxy_cache | 指定緩存區(qū),對應keys_zone 中設置的值 |
| proxy_cache_key | 通過參數拼裝緩存key 如: |
|
| proxy_cache_valid | 為不同的狀態(tài)碼設置緩存有效期 |
查看緩存目錄
上述配置的結果,重啟生效后,我們可以看到生成了很多緩存文件在 緩存存儲路徑為:/usr/local/nginx/cache,levels=1:2代表緩存的目錄結構為2級目錄
如下圖,緩存會在/usr/local/nginx/cache目錄下生成,包含2級目錄,在之下就是緩存文件,測試的時候可以到該目錄下查看緩存文件是否生成。

我們打開其中一個文件看看,會發(fā)現一些蹊蹺,這里存儲了請求頭的信息。當我們處理一個 HTTP 請求的時候,它會先從這里讀取。
緩存的清除
該功能可以采用第三方模塊 ngx_cache_purge 實現。為nginx 添加 ngx_cache_purge 模塊
#下載ngx_cache_purge 模塊包 ,這里nginx 版本為1.6.2 purge 對應2.0版
wget http://labs.frickle.com/files/ngx_cache_purge-2.3.tar.gz
#查看已安裝模塊
./sbin/nginx -V
#進入nginx安裝包目錄 重新安裝 --add-module為模塊解壓的全路徑
./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
#重新編譯
make
#拷貝 安裝目錄/objs/nginx 文件用于替換原nginx 文件
#檢測查看安裝是否成功
nginx -t
清除配置:
location ~ /clear(/.*) {
#允許訪問的IP
allow 127.0.0.1;
allow 192.168.0.193;
#禁止訪問的IP
deny all;
#配置清除指定緩存區(qū)和路徑(與proxy_cache_key一至)
proxy_cache_purge cache_item '$host:$server_port$request_uri';
}
配置好以后 直接訪問 :
這里 192.168.0.193 域名設置為 www.test.com
# 訪問生成緩存文件
http://www.test.com/?a=1
# 清除生成的緩存,如果指定緩存不存在 則會報404 錯誤。
http://www.testcom/clear/?a=1
指定不緩存頁面
配置語法:
| proxy_no_cache | |
|---|---|
| 語法 | proxy_no_cache string ... |
| 默認 | --- |
| 作用域 | http,server,location |
| 備注 |
例子:
...
# 判斷當前路徑是否是指定的路徑
if($request_uri ~ ^/(url3|login|register|password\/reset)) {
#設置一個變量用來存儲是否是需要緩存
set $cookie_nocache 1;
}
location / {
...
proxy_no_cache $cookie $arg_nocache $arg_comment;
...
}
...
緩存命中分析
在http header上增加命中顯示
Nginx 提供了 $upstream_cache_status 這個變量來顯示緩存的狀態(tài),我們可以在配置中添加一個http頭來顯示這一狀態(tài),達到類似squid的效果。
location / {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_connect_timeout 180;
proxy_send_timeout 180;
proxy_read_timeout 180;
proxy_buffer_size 128k;
proxy_buffers 4 128k;
proxy_busy_buffers_size 128k;
proxy_temp_file_write_size 128k;
proxy_cache cache;
proxy_cache_valid 200 304 1h;
proxy_cache_valid 404 1m;
proxy_cache_key '$host:$server_port$request_uri';
add_header Nginx-Cache "$upstream_cache_status";
proxy_pass http://backend;
}
而通過curl或瀏覽器查看到的header如下:
HTTP/1.1 200 OK
Date: Mon, 22 Apr 2013 02:10:02 GMT
Server: nginx
Content-Type: image/jpeg
Content-Length: 23560
Last-Modified: Thu, 18 Apr 2013 11:05:43 GMT
Nginx-Cache: HIT
Accept-Ranges: bytes
Vary: User-Agent
$upstream_cache_status 包含以下幾種狀態(tài):
- MISS 未命中,請求被傳送到后端
- HIT 緩存命中
- EXPIRED 緩存已經過期請求被傳送到后端
- UPDATING 正在更新緩存,將使用舊的應答
- STALE 后端將得到過期的應答
Nginx cache命中率統(tǒng)計
即然nginx為我們提供了$upstream_cache_status函數,自然可以將命中狀態(tài)寫入到日志中。具體可以如下定義日志格式:
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"'
'"$upstream_cache_status"';
命中率統(tǒng)計方法:用HIT的數量除以日志總量得出緩存命中率:
了解了原理以后,也可以通過crontab腳本將每天的命中率統(tǒng)計到一個日志中,以備查看。
# crontab -l
1 0 * * * /opt/<a title="shell"target="_blank">shell</a>/nginx_cache_hit >> /usr/local/nginx/logs/hit
訪腳本的內容為:
#!/bin/bash
LOG_FILE='/usr/local/nginx/logs/access.log.1'
LAST_DAY=$(date +%F -d "-1 day")
awk '{if($NF==""HIT"") hit++} END {printf "'$LAST_DAY': %d %d %.2f%n", hit,NR,hit/NR}' $LOG_FILE
Nginx 其他優(yōu)化措施
動靜分離
將靜態(tài)文件存儲到 /usr/share/nginx/html 文件夾中,配置靜態(tài)文件攔截規(guī)則。單個項目可以直接放在 /usr/share/nginx/html 根目錄下,如果是多個項目,則要根據項目的根目錄創(chuàng)建文件夾來保存靜態(tài)文件。
配置攔截規(guī)則如下:
location ~ .*\.(eot|svg|ttf|woff|jpg|jpeg|gif|png|ico|cur|gz|svgz|mp4|ogg|ogv|webm) {
#所有靜態(tài)文件直接讀取硬盤
root /usr/share/nginx/html;
expires 30d; #緩存30天
}
location ~ .*\.(js|css)?${
#所有靜態(tài)文件直接讀取硬盤
root /usr/share/nginx/html;
expires 12h;
}
結果:整個網站的訪問速度都會減少,大部分內容都將從靜態(tài)文件目錄或者硬盤中讀取。
連接超時
長時間占著連接資源不釋放,最終會導致請求的堆積,Nginx 處理請求效率大大降低。所以我們對連接的控制都要注意設置超時時間,通過超時機制自動回收資源、避免資源浪費。
#客戶端、服務端設置
server_names_hash_bucket_size 128;
server_names_hash_max_size 512;
# 長連接超時配置
keepalive_timeout 65;
client_header_timeout 15s;
client_body_timeout 15s;
send_timeout 60s;
#代理設置
#與后端服務器建立連接的超時時間。注意這個一般不能大于75秒
proxy_connect_timeout 30s;
proxy_send_timeout 120s;
#從后端服務器讀取響應的超時
proxy_read_timeout 120s;
GZIP 壓縮
在我們進行 gzip 打包壓縮之前,最好將一些靜態(tài)文件先進行壓縮為 min 文件,請求的時候合并為同一文件。再通過 gzip 壓縮后,你會發(fā)現網站加載又加速了。
#開啟gzip,減少我們發(fā)送的數據量
gzip on;
#允許壓縮的最小字節(jié)數
gzip_min_length 1k;
#4個單位為16k的內存作為壓縮結果流緩存
gzip_buffers 4 16k;
#設置識別HTTP協議版本,默認是1.1
gzip_http_version 1.1;
#gzip壓縮比,可在1~9中設置,1壓縮比最小,速度最快,9壓縮比最大,速度最慢,消耗CPU
gzip_comp_level 4;
#壓縮的類型
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
#給代理服務器用的,有的瀏覽器支持壓縮,有的不支持,所以避免浪費不支持的也壓縮,所以根據客戶端的HTTP頭來判斷,是否需要壓縮
gzip_vary on;
#禁用IE6以下的gzip壓縮,IE6的某些版本對gzip的壓縮支持很不好
gzip_disable "MSIE [1-6].";
訪問限流
我們構建網站是為了讓用戶訪問它們,我們希望用于合法訪問。所以不得不采取一些措施限制濫用訪問的用戶。這種濫用指的是從同一IP每秒到服務器請求的連接數。因為這可能是在同一時間內,世界各地的多臺機器上的爬蟲機器人多次嘗試爬取網站的內容。
#限制用戶連接數來預防DOS攻擊
limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn_zone $server_name zone=perserver:10m;
#限制同一客戶端ip最大并發(fā)連接數
limit_conn perip 2;
#限制同一server最大并發(fā)連接數
limit_conn perserver 20;
#限制下載速度,根據自身服務器帶寬配置
limit_rate 300k;
高效數據傳輸配置
#開啟文件的高效傳輸模式。tcp_nopush和tcp_nodelay可防止網絡及磁盤i/o阻塞,提升nginx工作效率;
sendfile on;
#數據包不會馬上傳送出去,等到數據包最大時,一次性的傳輸出去,這樣有助于解決網絡堵塞。
tcp_nopush on;
#只要有數據包產生,不管大小多少,就盡快傳輸
tcp_nodelay on;
內核參數的優(yōu)化
編輯 /etc/sysctl.conf 文件,根據需要調整參數配置。
#如果想把timewait降下了就要把tcp_max_tw_buckets值減小,默認是180000
net.ipv4.tcp_max_tw_buckets = 5000
#開啟重用功能,允許將TIME-WAIT狀態(tài)的sockets重新用于新的TCP連接
net.ipv4.tcp_tw_reuse = 1
#系統(tǒng)中最多有多少個TCP套接字不被關聯到任何一個用戶文件句柄上。如果超過這個數字,孤兒連接將即刻被復位并打印出警告信息。這個限制僅僅是為了防止簡單的DoS攻擊
net.ipv4.tcp_max_orphans = 262144
#當keepalive起用的時候,TCP發(fā)送keepalive消息的頻度。缺省是2小時。我們可以調短時間跨度
net.ipv4.tcp_keepalive_time = 30
日志配置
日志文件對于我們日常運維至關重要,如果沒有日志排查問題,你將很難判斷異常的所在,要解決問題無異于大海撈針。日志的保存時必要的,不可缺少的,我們來看下怎么配置有利于排查問題?
- 關鍵字:其中關鍵字 access_log,error_log 不能改變
- error_log 錯誤日志級別:[debug | info | notice | warn | error | crit | alert | emerg],級別越高記錄的信息越少。不要配置 info 等級較低的級別,會帶來大量的磁盤 I/O 消耗。
error_log 生產場景一般是 warn | error | crit 這三個級別之一
#定義日志模板
log_format 日志模板名稱 日志格式;
#訪問日志
access_log path format gzip[=level] [buffer=size] [flush=time];
關鍵字 存放的路徑 日志格式模板 gzip壓縮,level指壓縮級別 存放緩存日志的緩存大小 保存在緩存中的最長時間
#錯誤日志
error_log <FILE> <LEVEL>;
關鍵字 日志文件 錯誤日志級別
#示例
log_format main '$remote_addr - $remote_user [$time_local] "$request"'
'$status $body_bytes_sent "$http_referer"'
'"$http_user_agent" "$http_x_forwarded_for"';
部分圖片來源于網絡,版權歸原作者,侵刪。