Nginx 性能優(yōu)化實踐

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)/配送至服務
  • 廣告詞服務
  • 預售/秒殺服務
  • 評價服務
  • 試用服務
  • 推薦服務
  • 商品介紹服務
  • 各品類相關的一些特殊服務

解決方案:

  1. 采用Ajax 動態(tài)加載 價格、廣告、庫存等服務

  2. 采用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 如:hosturiis_argsargs 則會以全路徑md5值做做為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"';
部分圖片來源于網絡,版權歸原作者,侵刪。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容