☕ NEW! 完成新手任務即可參加抽獎!LINE 星巴克禮券等你拿,名額有限!        🎉 推廣活動:邀請好友註冊 DevLearn,累積推薦抽 LINE 星巴克禮券! 活動詳情 →        🔥 活動期間 2026/4/1 - 5/31 |已有 0 人參加       
Server 中級

反向代理與負載平衡

正向代理 vs 反向代理

💡 比喻:秘書代接電話

  • 正向代理(Forward Proxy):你請秘書幫你打電話給別人

    • 對方不知道是你打的,只知道是秘書打的
    • 例如:VPN、公司內部代理伺服器
  • 反向代理(Reverse Proxy):公司請秘書幫所有員工接電話

    • 外面的人打電話進來,都先經過秘書
    • 秘書決定轉給哪位員工處理
    • 例如:Nginx、HAProxy

關鍵差別:正向代理代替客戶端,反向代理代替伺服器

圖解差異

// 正向代理(幫客戶端出去)
客戶端 A ─┐
客戶端 B ─┼─→ [正向代理] ─→ 網際網路 ─→ 伺服器   // 代理幫客戶端存取外部
客戶端 C ─┘                                        // 伺服器看不到真正的客戶端

// 反向代理(幫伺服器進來)
客戶端 ─→ 網際網路 ─→ [反向代理] ─┬→ 伺服器 A     // 代理幫伺服器接收請求
                                   ├→ 伺服器 B     // 客戶端看不到真正的伺服器
                                   └→ 伺服器 C     // 代理決定分配給哪台

Nginx 設定反向代理到 Kestrel

安裝 Nginx

# Ubuntu/Debian 安裝 Nginx
sudo apt update                    # 更新套件清單
sudo apt install nginx -y          # 安裝 Nginx,-y 自動確認

# 啟動並設為開機自動啟動
sudo systemctl start nginx         # 立即啟動 Nginx
sudo systemctl enable nginx        # 設定開機自動啟動

# 檢查 Nginx 狀態
sudo systemctl status nginx        # 查看是否正常運行
# 應該看到 active (running)

# 測試設定檔語法是否正確
sudo nginx -t                      # 檢查設定檔有沒有語法錯誤

基本反向代理設定

# /etc/nginx/sites-available/myapp
# 這個設定檔告訴 Nginx 如何轉發請求給 Kestrel

server {                                          # 定義一個虛擬主機
    listen 80;                                    # 監聽 HTTP 80 Port
    server_name myapp.com www.myapp.com;          # 回應這些網域名稱的請求

    location / {                                  # 所有路徑的請求
        proxy_pass http://localhost:5000;          # 轉發到 Kestrel(本機 5000 Port)
        proxy_http_version 1.1;                   # 使用 HTTP 1.1 協定
        proxy_set_header Upgrade $http_upgrade;   # 支援 WebSocket 升級
        proxy_set_header Connection keep-alive;   # 保持連線不斷開
        proxy_set_header Host $host;              # 傳遞原始的主機名稱
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;   # 傳遞客戶端真實 IP
        proxy_set_header X-Forwarded-Proto $scheme;                    # 傳遞原始協定(http/https)
        proxy_cache_bypass $http_upgrade;         # WebSocket 請求不走快取
    }

    location /static/ {                           # 靜態檔案路徑
        alias /var/www/myapp/wwwroot/;            # 直接由 Nginx 提供靜態檔案
        expires 30d;                              # 快取 30 天
        add_header Cache-Control "public";      # 設定為公開快取
    }
}
# 啟用站台設定
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/   # 建立符號連結啟用站台
sudo nginx -t                  # 測試設定檔語法
sudo systemctl reload nginx    # 重新載入設定(不中斷服務)

ASP.NET Core 配合反向代理

// Program.cs 中設定轉發標頭
var builder = WebApplication.CreateBuilder(args);   // 建立建構器

// 設定轉發標頭中介軟體(重要!否則拿不到真實 IP)
builder.Services.Configure<ForwardedHeadersOptions>(options =>  // 設定轉發標頭選項
{
    options.ForwardedHeaders =                       // 指定要處理的標頭
        ForwardedHeaders.XForwardedFor |             // 處理客戶端 IP
        ForwardedHeaders.XForwardedProto;            // 處理原始協定
});

var app = builder.Build();                           // 建構應用程式

app.UseForwardedHeaders();    // 啟用轉發標頭(必須放在其他中介軟體之前)
app.UseHttpsRedirection();    // HTTP 重導向到 HTTPS
app.UseStaticFiles();         // 提供靜態檔案
app.UseAuthorization();       // 授權中介軟體
app.MapControllers();         // 對應 Controller 路由
app.Run();                    // 啟動應用程式

負載平衡演算法

💡 比喻:餐廳帶位 想像你是餐廳經理,要決定把客人帶到哪一桌(伺服器):

  • Round Robin:輪流帶位,1號桌 → 2號桌 → 3號桌 → 回到1號桌
  • Least Connections:看哪桌客人最少就帶到那桌
  • IP Hash:看客人的臉(IP),同一張臉永遠去同一桌

Nginx 負載平衡設定

# /etc/nginx/nginx.conf
# 負載平衡設定範例

# 方法一:Round Robin(預設)—— 輪流分配
upstream myapp_servers {                           # 定義後端伺服器群組
    server 192.168.1.10:5000;                      # 後端伺服器 1
    server 192.168.1.11:5000;                      # 後端伺服器 2
    server 192.168.1.12:5000;                      # 後端伺服器 3
    # 請求會依序分配:1 → 2 → 3 → 1 → 2 → 3...   # 公平輪流
}

# 方法二:Least Connections —— 最少連線優先
upstream myapp_least {                             # 另一個伺服器群組
    least_conn;                                    # 啟用最少連線演算法
    server 192.168.1.10:5000;                      # 連線少的優先接收新請求
    server 192.168.1.11:5000;                      # 適合請求處理時間不均的情況
    server 192.168.1.12:5000;                      # 動態平衡負載
}

# 方法三:IP Hash —— 同 IP 固定到同一台
upstream myapp_sticky {                            # 黏性 Session 群組
    ip_hash;                                       # 根據客戶端 IP 做 Hash
    server 192.168.1.10:5000;                      # 同一個 IP 永遠去同一台
    server 192.168.1.11:5000;                      # 適合需要 Session 的應用
    server 192.168.1.12:5000;                      # 缺點:負載可能不均勻
}

# 方法四:加權 Round Robin —— 效能好的多分一點
upstream myapp_weighted {                          # 加權群組
    server 192.168.1.10:5000 weight=3;             # 這台效能好,分配 3 份
    server 192.168.1.11:5000 weight=2;             # 這台普通,分配 2 份
    server 192.168.1.12:5000 weight=1;             # 這台較弱,分配 1 份
}

server {                                           # 虛擬主機設定
    listen 80;                                     # 監聽 80 Port
    server_name myapp.com;                         # 網域名稱

    location / {                                   # 所有請求
        proxy_pass http://myapp_servers;            # 轉發到伺服器群組
        proxy_set_header Host $host;               # 傳遞主機名稱
        proxy_set_header X-Real-IP $remote_addr;   # 傳遞真實 IP
    }
}

SSL/TLS 終止(SSL Termination)

💡 比喻:門口保安檢查 SSL 終止就像大樓門口的保安:

  • 訪客進來時要出示證件(加密連線)
  • 保安驗證完後,讓訪客進入大樓(解密)
  • 大樓內部就不需要再出示證件了(內部走明文)
  • 這樣每間辦公室(後端伺服器)就不用各自安排保安
# Nginx 處理 SSL/TLS 終止
server {                                              # HTTPS 虛擬主機
    listen 443 ssl;                                   # 監聽 443 Port 並啟用 SSL
    server_name myapp.com;                            # 網域名稱

    ssl_certificate /etc/ssl/certs/myapp.crt;         # SSL 憑證檔案路徑
    ssl_certificate_key /etc/ssl/private/myapp.key;   # SSL 私鑰檔案路徑

    ssl_protocols TLSv1.2 TLSv1.3;                   # 只允許安全的 TLS 版本
    ssl_ciphers HIGH:!aNULL:!MD5;                     # 使用高強度加密套件
    ssl_prefer_server_ciphers on;                     # 優先使用伺服器的加密套件

    # Nginx 負責解密,轉發給 Kestrel 時用 HTTP(明文)
    location / {                                      # 所有請求
        proxy_pass http://localhost:5000;              # 內部用 HTTP 就好(已在同一台機器)
        proxy_set_header X-Forwarded-Proto https;     # 告訴應用程式原始是 HTTPS
    }
}

# HTTP 自動重導向到 HTTPS
server {                                              # HTTP 虛擬主機
    listen 80;                                        # 監聽 80 Port
    server_name myapp.com;                            # 網域名稱
    return 301 https://$host$request_uri;             # 永久重導向到 HTTPS
}
# 使用 Let's Encrypt 免費取得 SSL 憑證
sudo apt install certbot python3-certbot-nginx -y     # 安裝 Certbot 和 Nginx 外掛
sudo certbot --nginx -d myapp.com -d www.myapp.com    # 自動設定 SSL 憑證
# Certbot 會自動修改 Nginx 設定檔                       # 並設定自動續約
sudo certbot renew --dry-run                          # 測試自動續約是否正常

Health Check

Nginx 被動健康檢查

# Nginx 開源版的被動健康檢查
upstream myapp_backend {                                    # 後端伺服器群組
    server 192.168.1.10:5000 max_fails=3 fail_timeout=30s; # 失敗 3 次後暫停 30 秒
    server 192.168.1.11:5000 max_fails=3 fail_timeout=30s; # 同樣的失敗門檻設定
    server 192.168.1.12:5000 backup;                       # 備援伺服器,平時不使用
}
# max_fails=3:連續失敗 3 次就標記為不健康                    // 容錯次數
# fail_timeout=30s:30 秒後再試試看是否恢復                   // 恢復等待時間
# backup:只有其他伺服器都掛了才啟用                          // 最後防線

ASP.NET Core Health Check 端點

// Program.cs 中設定 Health Check
var builder = WebApplication.CreateBuilder(args);   // 建構器

// 加入 Health Check 服務
builder.Services.AddHealthChecks()                  // 註冊健康檢查
    .AddSqlServer(                                  // 檢查 SQL Server 連線
        builder.Configuration.GetConnectionString("DefaultConnection"),  // 連線字串
        name: "database",                         // 檢查項目名稱
        timeout: TimeSpan.FromSeconds(3))           // 逾時時間 3 秒
    .AddRedis(                                      // 檢查 Redis 連線
        "localhost:6379",                         // Redis 連線位址
        name: "redis",                            // 檢查項目名稱
        timeout: TimeSpan.FromSeconds(3))           // 逾時時間 3 秒
    .AddUrlGroup(                                   // 檢查外部 API
        new Uri("https://api.example.com/health"), // 外部 API 的健康端點
        name: "external-api",                     // 檢查項目名稱
        timeout: TimeSpan.FromSeconds(5));          // 逾時時間 5 秒

var app = builder.Build();                          // 建構應用程式
app.MapHealthChecks("/health");                   // 對應到 /health 路徑
app.Run();                                          // 啟動

nginx.conf 完整範例

# /etc/nginx/nginx.conf 完整設定範例

user www-data;                                 # Nginx 執行的使用者身分
worker_processes auto;                         # 工作程序數量(auto 會自動偵測 CPU 核心數)
pid /run/nginx.pid;                            # PID 檔案位置
events {
    worker_connections 1024;                   # 每個工作程序的最大連線數
    multi_accept on;                           # 允許一次接受多個新連線
}

http {                                         # HTTP 區塊設定
    # 基本設定
    sendfile on;                               # 啟用高效檔案傳輸
    tcp_nopush on;                             # 最佳化封包傳送
    tcp_nodelay on;                            # 減少延遲
    keepalive_timeout 65;                      # Keep-Alive 逾時時間 65 秒
    types_hash_max_size 2048;                  # MIME 類型 Hash 表大小
    server_tokens off;                         # 隱藏 Nginx 版本號(安全考量)

    # MIME 類型
    include /etc/nginx/mime.types;             # 載入 MIME 類型對應表
    default_type application/octet-stream;     # 預設 MIME 類型

    # 日誌設定
    access_log /var/log/nginx/access.log;      # 存取日誌路徑
    error_log /var/log/nginx/error.log;        # 錯誤日誌路徑

    # Gzip 壓縮
    gzip on;                                   # 啟用 Gzip 壓縮
    gzip_vary on;                              # 加入 Vary 標頭
    gzip_proxied any;                          # 對所有代理請求壓縮
    gzip_comp_level 6;                         # 壓縮等級(1-9,6 是好的平衡點)
    gzip_types text/plain text/css application/json application/javascript;  # 壓縮的類型

    # 速率限制
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;  # API 限流:每秒 10 個請求

    # 後端伺服器群組
    upstream dotnet_app {                      # 定義後端應用群組
        least_conn;                            # 使用最少連線演算法
        server 127.0.0.1:5000;                 # 本機的 ASP.NET Core 應用
        server 127.0.0.1:5001 backup;          # 備援實例
    }

    server {                                   # 虛擬主機設定
        listen 443 ssl http2;                  # 監聽 HTTPS 並啟用 HTTP/2
        server_name myapp.com;                 # 網域名稱

        ssl_certificate /etc/ssl/certs/myapp.crt;         # SSL 憑證
        ssl_certificate_key /etc/ssl/private/myapp.key;   # SSL 私鑰
        ssl_protocols TLSv1.2 TLSv1.3;                    # 安全的 TLS 版本
        ssl_ciphers HIGH:!aNULL:!MD5;                     # 高強度加密

        # 靜態檔案
        location /static/ {                    # 靜態檔案路徑
            alias /var/www/myapp/wwwroot/;     # 對應到實際目錄
            expires 7d;                        # 快取 7 天
            access_log off;                    # 靜態檔案不記錄存取日誌
        }

        # API 路由(有速率限制)
        location /api/ {                       # API 路徑
            limit_req zone=api burst=20 nodelay;  # 套用速率限制,允許突發 20 個
            proxy_pass http://dotnet_app;       # 轉發到後端群組
            proxy_http_version 1.1;            # HTTP 1.1
            proxy_set_header Upgrade $http_upgrade;         # WebSocket 支援
            proxy_set_header Connection keep-alive;         # 保持連線
            proxy_set_header Host $host;                    # 主機名稱
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;  # 真實 IP
            proxy_set_header X-Forwarded-Proto $scheme;     # 原始協定
        }

        # 其他所有請求
        location / {                           # 預設路徑
            proxy_pass http://dotnet_app;       # 轉發到後端
            proxy_http_version 1.1;            # HTTP 1.1
            proxy_set_header Host $host;       # 主機名稱
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;  # 真實 IP
            proxy_set_header X-Forwarded-Proto $scheme;  # 原始協定
        }
    }

    # HTTP 重導向到 HTTPS
    server {                                   # HTTP 虛擬主機
        listen 80;                             # 監聽 80 Port
        server_name myapp.com;                 # 網域名稱
        return 301 https://$host$request_uri;  # 301 永久重導向
    }
}

🤔 我這樣寫為什麼會錯?

❌ 錯誤一:反向代理後拿不到客戶端真實 IP

// 錯誤:以為 HttpContext.Connection.RemoteIpAddress 是客戶端的 IP
var ip = HttpContext.Connection.RemoteIpAddress;  // 拿到的是 127.0.0.1(Nginx 的 IP)!

// 原因:經過反向代理後,Kestrel 看到的是 Nginx 的 IP
// 真正的客戶端 IP 被放在 X-Forwarded-For 標頭中

// ✅ 正確做法:啟用 ForwardedHeaders 中介軟體
// 在 Program.cs 中加入:
app.UseForwardedHeaders();  // 必須放在 UseAuthorization 之前
// 這樣 RemoteIpAddress 就會自動讀取 X-Forwarded-For

❌ 錯誤二:負載平衡時 Session 遺失

// 錯誤:使用 In-Memory Session,負載平衡時 Session 會遺失
builder.Services.AddDistributedMemoryCache();   // 記憶體快取(只存在單一伺服器)
builder.Services.AddSession();                  // 啟用 Session

// 問題:使用者第一次請求到 Server A 登入
// 第二次請求被分到 Server B,Session 不在 B 上面,被登出!

// ✅ 正確做法:使用分散式 Session(Redis)
builder.Services.AddStackExchangeRedisCache(options =>  // 改用 Redis
{
    options.Configuration = "localhost:6379";           // Redis 連線位址
});
builder.Services.AddSession();  // Session 資料存在 Redis,所有伺服器都能讀取

❌ 錯誤三:SSL 設定不安全

# 錯誤:允許舊版不安全的 SSL/TLS 協定
ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;  # SSLv3 和 TLSv1 有已知漏洞!

# 問題:SSLv3 有 POODLE 攻擊漏洞
# TLSv1.0 和 TLSv1.1 已被主流瀏覽器棄用

# ✅ 正確做法:只允許 TLSv1.2 和 TLSv1.3
ssl_protocols TLSv1.2 TLSv1.3;                        # 只用安全的版本
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;  # 強加密套件
ssl_prefer_server_ciphers on;                          # 優先使用伺服器的加密設定

💡 大家的想法 · 0

載入中...
💬 即時聊天室 🟢 0 人在線
😀 😎 🤓 💻 🎮 🎸 🔥
➕ 新問題
📋 我的工單
💬 LINE 社群
🔒
需要註冊才能使用此功能
註冊帳號即可解鎖測驗、遊戲、簽到、筆記下載等所有功能,完全免費!
免費註冊