微服務 - Nginx閘道器 · 程式機制 · 限流熔斷 · 效能最佳化 · 動態負載 · 高可用

Sol·wang發表於2023-05-04

系列目錄:

本文的前提需要了解一些基礎的Linux知識。
以下圍繞 Nginx 1.23 的閘道器應用;參考官網:http://nginx.org/
本文沒有對概念性方面做深入的闡述,常見的是一筆帶過,而更多的...是對配置項的解釋。

為什麼需要閘道器

通常後臺提供了不同的應用服務,甚至是叢集,每種服務每個服務,需要維護不同的請求地址,甚至服務認證、跨域等動作,管理起來比較麻煩。因此,需要一個閘道器,介於客戶端和應用服務之間,所有的外部請求都會先經過閘道器,閘道器再把請求分發到目標服務。閘道器對外提供唯一請求入口,作為對外聯絡的視窗,易於管理和維護請求。

作者:[Sol·wang] - 部落格園,原文出處:https://www.cnblogs.com/Sol-wang/

一、Nginx 概述

Nginx 不僅是一個高效能的Web伺服器,還具備訪問代理、負載均衡、內容快取等功能,用於客戶端訪問流量到後臺應用伺服器負載均衡和請求轉發。其基於模組化的程式碼架構及可與其它有效整合的可程式設計特性,使其具有強大的擴充套件能力。Nginx以資源消耗低、高穩定、高效能的併發處理能力著稱。

1.1 Nginx 特性

訪問代理
Nginx 可以透過訪問路徑、URL 關鍵字、客戶端 IP等多種手段實現訪問路由分配。

反向代理
將接收到的請求再轉到後端的目標應用伺服器,並把響應資料返回給客戶端。支援目前絕大多數的網路協議:HTTP/FastCGI/RPC/UDP/TCP等。

負載均衡
透過自身的 upstream 模組支援多種負載均衡演算法,使後端伺服器可以非常方便地進行橫向擴充套件,以應對高併發。

內容快取
Nginx支援靜態站點和後端分離,可以把靜態內容快取起來,也可以將後端變化不大的響應結果快取起來,使整體實現了更高速的相應能力。

可擴充套件性
可定製的模組化架構方式,更多的語言(C/Perl/JavaScript/Lua)支援開發第三方模組並引入,增強可程式設計及擴充套件能力。

1.2 Nginx 程式

首先,程式是CPU管理的執行單元,CPU的單個核心也可以執行多個程式,只不過是交替執行著各個程式,稱為時間片,這種方式速度很快,以至於看上去像在同時執行;多核CPU就能同時執行更多的程式。

Nginx是由多個程式執行,一個主程式Master和多個子程式Worker,主程式負責管理子程式,如:重啟/過載/建立/銷燬等,子程式負責處理具體的請求等業務功能。程式間共享記憶體資料,更多的程式帶來更好的處理能力。

Nginx 程式執行示意圖:
Nginx 程式執行示意圖

1.3 Nginx 過載

Nginx支援配置資訊的過載,並以最新的配置內容執行,當Nginx在高速執行的時候,如何做到平穩過渡呢?

相關命令:nginx -s reload

過載過程:
Nginx Master process 負責 fork 出一個新的 Worker process,最新的Worker使用新的配置資訊執行,這時候就銷燬一箇舊的worker,此時,Worker有新舊之分,新Worker用新配置執行,舊Worker依然用舊配置執行,Master繼續fork出新的Worker。。。以同樣的方式持續替換舊Worker,直到全部替換完成。整個過程中,Nginx 並沒有停止執行,絲滑過渡。

二、安裝配置

2.1 編譯安裝

安裝前提:

yum install gcc -y                # C語言編譯器
yum install pcre pcre-devel -y    # PCRE Library
yum install zlib zlib-devel -y    # zlib Library

編譯安裝:

# 進入解壓後的目錄中 編譯安裝 [指定使用者/組] [--with-追加自帶模組名稱] 
./configure --prefix=/usr/local/nginx [--user=www --group=www] [--with-http_gzip_static_module]
make && make install

2.2 啟動例項

進入主程式目錄:/usr/local/nginx/bin

nginx            # 啟動
nginx -s stop    # 停止,立即
nginx -s quit    # 退出,處理完現有任務後
nginx -s reopen  # 重啟
nginx -s reload  # 過載配置,交替更新工作程式

docker 執行 nginx 很簡單:

拉取映象:docker pull nginx
啟動容器:docker run -d --name=ngx-a -p 80:80 nginx

瀏覽器開啟主機IP顯示 NGINX 歡迎頁面。

影響 Nginx 的系統關聯項

Firewall/UFW 防火牆:埠的開放
SELinux 許可權的限制:請求後端的許可權

2.3 配置檔案結構

nginx 的配置檔案預設存於 /etc/nginx/nginx.conf,其中透過 include 引入其它目錄子配置檔案。

  • 全域性塊:針對 Nginx 例項的設定,資源及事件的設定。
  • HTTP:從 Client 到 Nginx 的請求設定,請求過程中要處理的各項配置;
  • Upstream:代理轉發的下個目的地列表,後端服務組地址列表,連線與負載均衡的設定。
  • Server:從 Nginx 到 Service 的設定,通常對應前後端某種服務或組;限制設定,代理設定,錯誤機制等。
  • Location:路由匹配轉發通訊,重定向等。

配置模板示例

###### 全域性塊
worker_processes    auto;                   # 工作程式數
error_log  /var/log/nginx/error.log notice; # 錯誤級別記錄

events {
    worker_connections  1024;               # 單個工作程式,可承載的最大連線數
}

http {
    ###### MIME 配置
    include       /etc/nginx/mime.types;    # 副檔名與檔案型別對映表
    default_type  application/octet-stream; # mime.types 不包含時的預設設定
    
    ###### 請求日誌配置
    log_format  log-format-a  '$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" "$http_x_forwarded_for"';
    access_log  /var/log/nginx/access.log  log-format-a;    # 訪問日誌路徑 及引用格式
    
    ###### 後端可用服務配置
    upstream backend-server-name {
        server 192.168.1.101:80;
        server 192.168.1.102:80;
    }
    
    server {
        ###### 請求匹配
        listen       80;                 # 客戶端請求的埠
        server_name  _;                  # 客戶端請求的域名;區分不同服務(可多個空格區分,支援正則)
        
        location / {
            ###### 重寫配置
            rewrite ^<規則>$ <目的地> break;
            
            ###### 轉發到後端配置
            proxy_pass http://backend-server-name$request_uri;    # 轉發到後臺服務地址,來自於 upstream 項
            proxy_http_version 1.1;                               # 指明版本(1.1預設為keep-alive長連線,1.0預設為短連線)
            proxy_set_header Host $host;                          # 保持原來的請求域名
            proxy_ignore_client_abort on;                         # 客戶端斷網時,是否中斷對後端的請求
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 從遠端客戶端IP到服務端的層層代理轉發IP,多IP追加空格分隔
            
            ###### Cookie 域名/路徑
            proxy_cookie_domain {backend-domain} {request-domain};
            proxy_cookie_path {backend-path} {request-path};
        }
        
        ###### 指定檔案拒絕所有訪問
        location ~ ^/(\.user.ini|\.ht|\.git|\.svn|\.project|LICENSE|README.md){
            deny all;
        }
        
        ###### 限制的客戶端
        location \ {
            deny 172.18.0.101;    # 拒絕的ip
            allow 172.18.1.10;    # 允許的ip
        }
        
        ###### 客戶端快取配置
        location ~* \.(js|css|jpg|svg|gif|png)$ {
            if (-f $request_filename) {    # -f:只能是檔案,因為這用-f判斷了
                expires 30d;               # 快取有效時長 30 天
                break;
            }
        }
        
        ###### 防盜鏈配置
        location ~* \.(gif|jpg|png|bmp)$ {  # 指定格式禁止的請求來源:google/baidu
            valid_referers none blocked *.ttlsa.com server_names ~\.google\. ~\.baidu\.;
            if ($invalid_referer) {
                return 403;                 # 狀態碼
                #rewrite ^/ http://www.ttlsa.com/403.jpg;
            }
        }
        
        ###### (前端) 錯誤機制
        error_page  404              /404.html;    # 錯誤碼 轉向的 錯誤頁
        error_page  500 502 503 504  /50x.html;    # 錯誤碼 轉向的 錯誤頁
        location = /50x.html {                     # 錯誤頁 指向的 靜態頁面
            root   /usr/share/nginx/html;
        }
    }
}

2.4 前後端分離

把前端站點部署到Nginx:

server {
    listen 80;
    server_name xxx.com;
    
    # 前端配置
    location / {
        # 前端站點路徑
        root /home/vue/dist;
        index index.html
    }
    
    # 後端配置
    location /api {
        proxy_set_header host $HOST;
        proxy_pass http://192.168.1.101:8081;
    }
}

2.5 負載均衡模式

在配置項 upstream 中,負責提供可用的服務地址列表,並可指定負載均衡的實現方式。

輪詢 Round-Robin:將訪問按序依次請求到後端各個伺服器上,能確保平均負載

upstream backend-a {
    server 192.168.1.101:80;
    server 192.168.1.102:80;
}

 權重 Weight:按百分比請求到後端伺服器上,常用到硬體配置不同的場景

upstream backend-b {
    server 192.168.1.101:80 weight=3;
    server 192.168.1.102:80 weight=7;
}

 最少連線 Least-Connect:處理請求少的後端服務優先接收新請求

upstream backend-c {
    least_conn;
    server 192.168.1.101:80;
    server 192.168.1.102:80;
}

IP-Hash:相同的訪問IP落到後端同個伺服器上,所以支援會話保持,但不是絕對平均負載

upstream backend-d {
    ip_hash;
    server 192.168.1.101:80;
    server 192.168.1.102:80;
}

第三方的會話保持 sticky_cookie_insert,同時支援負載均衡。

2.6 限流與熔斷

限流:透過對併發/請求進行限速來保護系統,防止系統過載癱瘓而不能提供服務;為了更好控制整個系統的負載情況,即使阻止了某些請求,系統繼續提供服務。

http_limit_conn:單個IP同時允許的連線限制

http {
    
    # 連線限流定義
    # - $binary_remote_addr:限制物件(客戶端)
    # - zone:限制自定義名稱
    # - 10:記憶體中用10兆空間儲存連線記錄
    limit_conn_zone $binary_remote_addr zone={limits-name}:10m;
                
    server {
        location /search/ {
            
            # 單個IP同時允許建立多少連線(併發限制)
            limit_conn {limits-name} 1;
        }
    }
}

http_limit_req:單個IP請求頻率的限制;次/每秒;

http {
    # 請求限流定義
    # - $binary_remote_addr:限制物件(客戶端)
    # - zone:定義限制(策略)名稱
    # - 10m:用十兆空間記錄訪問次數
    # - rate:每秒10次的請求處理速率
    limit_req_zone $binary_remote_addr zone={limits-name}:10m rate=1r/s;
    # 請求限流定義
    # - $server_name:限制物件,對指定伺服器請求的限制
    limit_req_zone $server_name zone={limits-name}:10m rate=10r/s;
                
    server {
        location /search/ {
            # 引用以上定義的限流策略,做以下設定(漏桶方式)
            # - burst:最多接收5個排隊使用者IP,處於等待處理狀態(容量)
            # - nodelay:超出排隊之外的更多請求,拒絕並返回503(溢位)
            limit_req zone={limits-name} [burst=5] [nodelay];
        }
    }
}

http_limit_rate:向客戶端傳輸響應的速率限制;位元組/每秒/每連線;0不限制

http {   
    server {
        location /download/ {
            
            # 頻寬限制
            limit_rate_after 5m; # 初始限速5m
            limit_rate 500k;     # 超出後限速500k
        }
    }
}

熔斷:當後端服務發生指定頻率錯誤後,Nginx觸發熔斷措施,不再請求此後端服務,直接返回預設內容到使用者端。

upstream http_backend {
    # 10s內出現3次錯誤,該伺服器將被視為不可用(熔斷)
    server 192.168.1.101:8080 max_fails=3 fail_timeout=10s;
    server 192.168.1.102:8080 max_fails=3 fail_timeout=10s;
}

當然也有容錯機制,Nginx 預設自動轉向其它服務再請求,相關配置:proxy_next_upstream

三、效能最佳化

3.1 全域性最佳化

# 工作程式數
worker_processes auto;           # 建議 CPU核心數|CPU執行緒數

# 最大支援的連線(open-file)數量;最大值受限於 Linux open files (ulimit -n)
# 建議公式:worker_rlimit_nofile > worker_processes * worker_connections
worker_rlimit_nofile    65535;

events {
    use epoll;                   # 高效的IO多路複用(RedHat6+ 都支援 epoll)
    multi_accept on;             # 設定一個程式是否同時接受多個網路連線
    worker_connections  10240;   # 單個工作程式,可承載的最大連線數;
}

3.2 與客戶端之間的最佳化

http {
    
    ###### 零複製技術
    sendfile on;            # 開啟不讀到(應用本身)記憶體,直接透過系統發出資料
    #sendfile_max_chunk 2m; # 每個程式每次呼叫傳輸數量不能大於設定的值,預設0為無上限。
    
    ###### 網路傳輸
    # on:累積到一定容量的資料再發,傳送次數少
    # off:有資料就發,傳送次數多,佔用網路資源
    tcp_nopush on;
    
    ###### 長連線;使用者端比較分散,keepalive 預設值已經足夠,個人不建議重設
    
    ###### 響應資料 開啟壓縮模式
    gzip on;
    gzip_vary on;                                # 為相容老瀏覽器,追加到Header的壓縮標註
    gzip_proxied any;                            # 所有代理請求都壓縮,也可指定型別
    gzip_comp_level 2;                           # 壓縮等級1-9(比例)等級越大 壓縮越高 越耗CPU
    gzip_min_length 128;                         # 壓縮前提,當返回內容大於指定時再壓縮
    gzip_types text/css text/xml image/svg+xml;  # 指定壓縮的檔案型別;更多壓縮項可參考 mime.types
}

3.3 與後端服務之間的最佳化

http {
    upstream backend_A {
        # ...
        
        # 長連線;所有請求匯聚到後端伺服器,併發時是有必要在此基礎上重配 keepalive 的
        # 以下 keepalive 需要 http 1.1 版本,並且 header connection close
        keepalive 100;           # 每個Worker與後端的連線池中,保持指定量的空閒連線數(QPS的10%)
        keepalive_time 1h;       # 每個長連線的(忙碌+空閒)總時長,超出後強制失效
        keepalive_timeout 60s;   # 每個長連線,最大空閒時長,超出後自動關閉長連線
        keepalive_requests 1000; # 每次長連線支援的 Request 次數,超出後自動關閉
    }
    
    server {
        location \ {
            proxy_pass http://backend_A;
            proxy_http_version 1.1;            # 1.1 支援 keep-live
            proxy_set_header connection "";    # 覆蓋客戶端的連線頭
        }
    }
}

3.4 擴充套件最佳化

多級快取:透過 Nginx 實現各種快取的配置,瀏覽器快取、CDN快取、Nginx記憶體、代理快取、後端服務應用快取等。

資源靜態化 ssi 模組:請求結果生成靜態頁,Nginx設定攔截,更多的請求直接讀靜態頁後返回;減少後端請求,定時生成靜態頁。

靜態資源同步 rsync:每臺服務都安裝,監控目錄變化,把生成的靜態頁推送到多臺伺服器。

合併請求 concat 第三方模組:將多個靜態資原始檔 合併為一次請求載入完成,減少併發量;淘寶示例:??xxx.js,yyy.js,zzz.js

四、動態負載

自動更新 upstream 上的可用服務地址,Nginx 的商業版才提供,開源的可選第三方模組;這裡後端叢集管理用的是 Consul;所以這裡使用 Consul 提供的配套工具 Consul-Template。
當後端叢集 consul 上的應用服務有變動時,工具 consul-template 負責拉取 consul 最新的健康應用服務列表,並生成 nginx conf 後再過載 Nginx。

1、下載部署 consul-template

# 從官網 https://releases.hashicorp.com/consul-template/ 下載軟體包
curl -O https://releases.hashicorp.com/consul-template/0.31.0/consul-template_0.31.0_linux_amd64.zip
unzip consul-template_0.31.0_linux_amd64.zip consul-template # unzip解壓軟體包並重新命名
mv consul-template /usr/local/bin/                           # 移動到特定目錄
/usr/local/bin/consul-template -v                            # 驗證安裝效果,顯示版本號

2、建立生成 nginx conf 的模板檔案 ngx-server-conf.tmp,用於生成指定格式的 nginx.conf 檔案;內容示例如下:

為便於集中管理配置檔案,存放於 nginx 預設的配置目錄下 /etc/nginx/conf.d/ngx-server-conf.tmp

########## 自動生成多個 upstream
{{range services}} {{$name := .Name}} {{$service := service .Name}}
upstream {{$name}} {
        least_conn;
        {{range $service}}server {{.Address}}:{{.Port}};
        {{else}}server 127.0.0.1:65535; # force a 502{{end}}
} {{end}}
########## upstream end ##########

server {
        listen 80;
        server_name xxx.com;
        location / {
                root /usr/share/nginx/html/;
                index index.html;
        }

        ########## 自動生成多個 locattion
{{range services}} {{$name := .Name}}
        location /api/{{$name}} {
                proxy_pass http://{{$name}};
                proxy_http_version 1.1;
        }
{{end}} ########## location end ##########
}

更多的模板檔案語法可參考官網說明:consul-template templating language

3、建立 consul-template 的配置檔案 ctmp.hcl,用以設定執行引數。內容示例如下:

為便於集中管理配置檔案,存放於 nginx 容器內的預設配置目錄下 /etc/nginx/conf.d/ctmp.hcl

# 連線 consul 的配置
consul {
    address = "172.18.0.3:8500"    # consul node
}

# 生成 nginx config file 的配置
template {
    source = "/etc/nginx/conf.d/ngx-server-conf.tmp"   # consul-template 的配置模板檔案
    destination = "/etc/nginx/conf.d/default.conf"     # 生成的 nginx-server 配置檔案
    command = ["nginx", "-s", "reload"]                # 執行的命令,以過載 Nginx 配置
}
# 總結步驟:從 address 拉資料,透過格式 source 生成 destination 配置,最後 command 過載Nginx。

以上更多的配置項參考官網說明:consul-template configuration options

4、啟動執行 consul-template

# nginx 容器中啟動 consul-template
/usr/local/bin/consul-template -config /etc/nginx/conf.d/ctmp.hcl
# 驗證效果:可檢視生成的 nginx conf
cat /etc/nginx/conf.d/default.conf

效果:檢視當前 nginx default.conf;之後停掉後端一個應用服務,對比 nginx 前後兩個 default.conf 的內容變化。

五、高可用

高可用 - 雙機熱備:主機和從機透過TCP/IP網路連線,正常情況下主機處於工作狀態,從機處於監視狀態,一旦從機發現主機異常,從機將會在很短的時間之內代替主機,完全實現主機的功能。

工具 Keepalived,安裝到內網中每個裝有 Nginx 的系統上:
多個 Keepalived 例項形成一個組,組成員有 Master/slave 之分,時時監控組內所有 Keepalived 例項的執行情況;
Keepalived 透過一個虛擬IP加入到 Master 網路卡上,所以透過虛擬IP能夠直接連線到 Master上,也就是其中一個 Nginx;
一個 Keepalived 成員當機後,從 Slave 中選舉出新的 Master,也就是把虛擬IP自動加入到新的 Master上,持續提供服務;
對外僅透過內網的虛擬IP完成與 Nginx 的連線,而不關心使用的 Nginx 在哪臺機上。

Keepalived 官網

5.1 配置執行 Keepalived

安裝 Keepalived:yum install -y keepalived
配置檔案:/etc/keepalived/keepalived.conf
配置模板:可僅留 global_defs,vrrp_instance,virtual_ipaddress

global_defs {
    router_id <key>           # 同組不重複的唯一標識
}

vrrp_instance <Instance-Name> {
    state MASTER              # MASTER/BACKUP;有主優先執行
    interface <net-card-name> # 指定虛擬IP寄存的網路卡
    priority 100              # 相同角色的優先順序,越大越優先
    advert_int 1              # 間隔秒檢測一次成員的執行狀況
    
    authentication {          # 成員間的通訊憑證
        auth_type PASS        # 同組相同的方式
        auth_pass 1111        # 同組相同的編碼,保持成員互通
    }
    
    virtual_ipaddress {       # 追加新IP,對外提供的通訊入口
        192.168.17.200        # 同網段的、未被使用的、虛擬新IP
    }
}

啟動後,可在 Master 中的指定網路卡中看到已追加的虛擬IP;不妨 ping 下你的虛擬IP...
再配置一臺 Keepalived,注意這裡配置的不同項為:router_id / state / priority

瀏覽器中訪問虛擬IP,就直接訪問了 Master 上的 Nginx;
關掉Master伺服器後,Keepalived 將虛擬IP又新增到了備用伺服器上了;
虛擬IP繼續提供正常的 Nginx 服務,瀏覽器正常訪問虛擬IP地址;
當 Master 修復啟動後,Keepalived 又將虛擬IP自動切換到 Master 上,始終以 Master 優先使用

注意

Keepalived 僅檢測自己主程式的執行狀況,並不是檢測 Nginx 的執行狀況;
所以:當 Nginx 錯誤,而 Keepalived 執行正常時,並不能達到 Master 轉移的效果;
方案:用指令碼定時檢測 Nginx 的主程式,Nginx發生錯誤時主動 Kill Keepalived,達到 Master 轉移的效果。

5.2 指令碼檢測 Nginx 服務

建立檢測指令碼檔案 /etc/keepalived/check_nginx.sh 內容示例:

#!/bin/bash
# 檢測 Nginx worker process 執行的個數
ckn=$(ps -C nginx --no-heading | wc -l)
# 當沒有 Nginx worker process 執行時
# 製造keepalived異常,使得啟用備用伺服器
if [ $ckn -eq 0 ]; then
        kill $(pidof keepalived)
fi

並賦予使用者可執行許可權;如:chmod +x /etc/keepalived/check_nginx.sh

Keepalived 指令碼檢測配置內容示例:

# global_defs ...

vrrp_script check_nginx_status {             ### 定義檢測策略
    user root                                # 負責檢測的使用者
    interval 1                               # 檢測間隔秒
    script /etc/keepalived/check_nginx.sh    # 檢測的可執行檔案
}

vrrp_instance <Instance-Name> {
    # ...
    track_script {
        check_nginx_status                   # 引用檢測策略
    }
    # ...
}

在 Master 上停止 Nginx 執行 試試看...??

以上是一個聯動停止執行的方案,那是不是需要一個聯動啟動也更為方便,自行考量...

相關文章