nginx根據真實IP分發請求
使用場景
2022年6月份,社保局接收到上級的文件,要求建立統一的門戶系統(所有的用戶都通過門戶系統登錄到子系統,原子系統的用戶、角色、機構、權限等,都交給門戶網站來控制)。於是各個子系統就需要做一個適配性的改造,子系統有機關養老、企業養老、城鄉居民養老、工傷保險、失業保險等。在子系統改造的過程中,除了代碼層面的改造以外我們遇上了幾個關於負載方面的問題。
傳統運行方式
會話保持(低成本方式)解決session問題
在原來的子系統中,為了一定程度上解決服務器壓力的問題,我們其實已經用nginx做了一個代理,兩個負載地址作為雙活部署。目前的系統有個做的不是很好的地方,就是在session里面塞了不少的東西,如果客戶端的請求第一次發到了server1上,第二次發到了server2上,很有可能就因為這兩個會話的不同,導致里面的參數獲取不到,所以我們要考慮session共享或者會話保持的問題。其實這個問題已經解決了,項目組選擇的方式是會話保持。具體的方法就是在配置nginx的時候,請求的分發策略選擇ip-hash,這樣一來,一個ip在兩台服務器都運行正常的情況下,永遠只會發到某一台上。一旦某一台服務器宕機,其實上述的問題還是會出現,但是我們就不考慮這個情況了,畢竟出現的概率比較小,這邊的成本想控制到最低。
# nginx配置如下
upstream jgyl{
zone upstreams 64K;
ip-hash
server 10.40.29.153:8080;
server 10.40.29.126:8080;
}
問題復現
nginx代理失效
在參與門戶系統的集成中,請求的方式發生了如下變化:
原:客戶端請求 → nginx → 負載服務器
現:客戶端請求 → 門戶網關 → 【用戶認證中心 → 門戶網關】 → nginx → 負載服務器
其中訪問用戶認證中心主要是為了獲取用戶認證的標識,即token,有token的時候就不需要括號中的部分了。
在現有的請求方式下,nginx認為所有的請求都來自門戶網關而不是,客戶端,所以所有分發的請求都會發到一台服務器上面,於是nginx的代理在實際層面上就失效了。
解決辦法
在解決這個問題的時候,我們討論了幾種解決方式。在傳統方式的基礎下,session利用redis來共享顯然是不太好的,因為代價比較大,而且redis必須安裝在一個共享的目錄里面。利用數據庫共享看似不錯,但是網上的解決方案完全不夠成熟。所以我們還是想看看有沒有辦法讓它還原到原來的運行模式上,總之百轉千回,終於找到了解決方案。那就是讓nginx根據真實的IP的分發請求。
X-Forwarded-For(XFF)是用來識別通過HTTP代理或負載均衡方式連接到Web服務器的客戶端最原始的IP地址的HTTP請求頭字段,我們的核心思路就是在這個字段里面獲取最早的一個請求,原理如下:
1、客戶端請求(ip0) → 服務器(ip1)。request.getHeader("X-Forwarded-For"); // 空
2、客戶端請求(ip0) → nginx(ip1)→ 服務器(ip2)。request.getHeader("X-Forwarded-For"); // ip0
3、客戶端請求(ip0) → 請求轉發器1(ip1) → 請求轉發器2(ip2) → nginx(ip3)→ 服務器(ip4)。
request.getHeader("X-Forwarded-For"); // ip0,ip1,ip2
其中ip0是最初始的IP請求,ip-hash默認以ip2作為請求IP來分發請求。
所以我們通過這個字段來加工出ip0,之后用它來分發請求就可以了,核心的配置如下。
# nginx配置如下
map $http_x_forwarded_for $clientRealIp {
"" $remote_addr;
~^(?P<firstAddr>[0-9\.]+),?.*$ $firstAddr;
}
upstream jgyl{
zone upstreams 64K;
hash $clientRealIp;
server 10.40.29.153:8080;
server 10.40.29.126:8080;
}
完整的配置
這里要注意X-Forwarded-For雖然是一個可用的標准,但是並非所有途中的轉發器都會增加值,需要每個轉發器都設置上(以nginx舉例):
proxy_set_header Host $host:$server_port; # 傳 header 參數至后端服務
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 設置request header 即客戶端IP地址
#user nobody;
worker_processes auto;
#error_log logs/error.log;
#error_log logs/error.log notice;
error_log logs/error.log info;
pid logs/nginx.pid;
worker_rlimit_nofile 65535;
events {
use epoll;
worker_connections 65535;
}
http {
map $http_x_forwarded_for $clientRealIp {
"" $remote_addr;
~^(?P<firstAddr>[0-9\.]+),?.*$ $firstAddr;
}
upstream jgyl{
zone upstreams 64K;
hash $clientRealIp;
server 10.40.29.153:8080;
server 10.40.29.126:8080;
}
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
# 客戶端請求頭緩沖大小
# nginx 默認會用 client_header_buffer_size 這個 buffer 來讀取 header 值
# 如果 header 過大,它會使用 large_client_header_buffers 來讀取
# 如果設置過小的 HTTP 頭,或 Cookie 過大, 會報400 錯誤 nginx 400 bad request
# 如果超過 buffer,就會報 HTTP 414 錯誤 (URI Too Long)
# nginx 接受最長的 HTTP 頭部大小必須比其中一個 buffer 大
# 否則就會報 400 的 HTTP 錯誤 (Bad Request)
client_header_buffer_size 32k;
large_client_header_buffers 4 32k;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 90;
# gzip模塊設置,使用 gzip 壓縮可以降低網站帶寬消耗,同時提升訪問速度。
# 開啟gzip
gzip on;
# 最小壓縮大小
gzip_min_length 1k;
# 壓縮緩沖區
gzip_buffers 4 16k;
# 壓縮版本
gzip_http_version 1.0;
# 壓縮等級
gzip_comp_level 2;
# 壓縮類型
gzip_types text/plain text/css text/xml text/javascript application/json application/x-javascript application/xml application/xml+rss;
server {
listen 8001;
server_name 17.167.18.18;
fastcgi_connect_timeout 75; #鏈接
fastcgi_read_timeout 90; #讀取
fastcgi_send_timeout 100; #發請求
#charset koi8-r;
#access_log logs/host.access.log main;
proxy_set_header Host $host:$server_port; # 傳 header 參數至后端服務
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 設置request header 即客戶端IP地址
proxy_connect_timeout 60s;
proxy_send_timeout 1800;
proxy_read_timeout 1800;
client_max_body_size 100m;
# 緩沖區代理緩沖用戶端請求的最大字節數
client_body_buffer_size 128k;
# 設置代理服務器(nginx)保存用戶頭信息的緩沖區大小
proxy_buffer_size 4k;
# proxy_buffers緩沖區,網頁平均在32k以下的話,這樣設置
proxy_buffers 4 32k;
# 高負荷下緩沖大小(proxy_buffers*2)
proxy_busy_buffers_size 64k;
# 設定緩存文件夾大小,大於這個值,將從upstream服務器傳
proxy_temp_file_write_size 64k;
location /jgyl {
root html;
index index.html index.htm;
proxy_pass http://jgyl/jgyl;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}
# another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
# listen 8000;
# listen somename:8080;
# server_name somename alias another.alias;
# location / {
# root html;
# index index.html index.htm;
# }
#}
# HTTPS server
#
#server {
# listen 443 ssl;
# server_name localhost;
# ssl_certificate cert.pem;
# ssl_certificate_key cert.key;
# ssl_session_cache shared:SSL:1m;
# ssl_session_timeout 5m;
# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;
# location / {
# root html;
# index index.html index.htm;
# }
#}
}