Blog Details

Linux 作業系統 cutexyz > Blog > Linux 服務架設 > Nginx 的 SSL 憑證& Load balance 演練

Nginx 的 SSL 憑證& Load balance 演練

  1. 啟動多個後端實體:
    使用 Python 腳本,分別啟動在 Port 9090 與 Port 9091(可透過設定檔或參數區分印出標題)。
  2. 建置 Upstream 負載平衡:
    設定 Nginx upstream backend_cluster 包含 127.0.0.1:9090 與 127.0.0.1:9091。
  3. 申請與掛載 SSL 憑證:
    綁定 Domain 名稱(或使用 DuckDNS 等免費 DDNS 網域)。
    執行 sudo certbot --nginx 完成 SSL 憑證部署與 HTTP ➔ HTTPS 自動轉址。
  4. 驗證高可用容錯(Failover):
    在瀏覽器多次重新整理,觀察頁面上的回應 Port 在 9090 與 9091 之間輪流切換。
    模擬故障:直接 kill 掉 9090 的 Python 程序,再次重新整理,確認網頁依然可正常存取(自動切換至 9091)。

    執行 sudo certbot renew --dry-run 確保未來憑證自動續簽測試成功。

以上有關 ssl 的申請, 有幾個前提要滿足才行, 你有一台在 internet 上服務 http 的機器, 另外還需有綁定該機器 ip 的 domain. 這樣才能進行申請與設定, 如你沒有此環境則可以跳過該功能的申請.

演練過程:

Part 1
準備 cluster_node.py 來運作後端的多個 http server 實體, 以下是 python demo code
記得每一個執行個體的 node_name 與 port 都必須要不一樣喔!!!

#!/usr/bin/env python3
import argparse
import http.server
import socketserver
from datetime import datetime


class LoadBalanceHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
    # 類別變數,由外部傳入設定
    node_name = "Node-1"
    port = 9090
    color = "#3498db"  # 預設藍色

    def do_GET(self):
        # 1. 讀取 Nginx 傳遞的真實客戶端 IP
        client_ip = self.headers.get("X-Real-IP")
        if not client_ip:
            client_ip = self.client_address[0]

        # 2. 獲取 Host 與時間
        current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        host_header = self.headers.get("Host", "Unknown")

        # 3. 繪製回應頁面(包含醒目的節點標籤與顏色區隔)
        response_content = f"""<!DOCTYPE html>
<html lang="zh-TW">
<head>
    <meta charset="UTF-8">
    <title>Week 9 Load Balancer Demo - {self.node_name}</title>
    <style>
        body {{ font-family: Arial, sans-serif; background-color: #f0f2f5; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; }}
        .card {{ background: white; padding: 2.5rem; border-radius: 12px; box-shadow: 0 8px 16px rgba(0,0,0,0.1); max-width: 500px; width: 100%; border-top: 8px solid {self.color}; }}
        .node-badge {{ display: inline-block; background-color: {self.color}; color: white; padding: 6px 16px; border-radius: 20px; font-weight: bold; font-size: 1.1rem; margin-bottom: 1rem; }}
        h1 {{ color: #2c3e50; font-size: 1.6rem; margin-top: 0; }}
        p {{ color: #4a5568; line-height: 1.6; font-size: 1.05rem; }}
        .highlight {{ color: #e53e3e; font-weight: bold; background: #fff5f5; padding: 2px 6px; border-radius: 4px; }}
        .code {{ font-family: monospace; background: #edf2f7; padding: 2px 6px; border-radius: 4px; font-weight: bold; color: #2d3748; }}
    </style>
</head>
<body>
    <div class="card">
        <div class="node-badge">運作節點:{self.node_name}</div>
        <h1>⚖️ Nginx 負載平衡測試頁面</h1>
        <p><strong>服務回應 Port:</strong> <span class="code">{self.port}</span></p>
        <p><strong>請求域名/Host:</strong> <span class="code">{host_header}</span></p>
        <p><strong>真實 Client IP:</strong> <span class="highlight">{client_ip}</span></p>
        <p><strong>回應時間:</strong> {current_time}</p>
    </div>
</body>
</html>
"""

        # 4. 發送 HTTP 回應
        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.send_header("Content-Length", str(len(response_content.encode("utf-8"))))
        self.end_headers()
        self.wfile.write(response_content.encode("utf-8"))

        # 5. 印出 Terminal 控制台日誌
        print(f"[{current_time}] [{self.node_name}] 收到來自真實 IP: {client_ip} 的請求")


def run():
    # 使用 argparse 解析命令行參數
    parser = argparse.ArgumentParser(description="Week 9 多節點測試用 HTTP Server")
    parser.add_argument("-p", "--port", type=int, default=9090, help="指定監聽 Port (預設: 9090)")
    parser.add_argument("-n", "--name", type=str, default="Node-A", help="指定節點名稱 (預設: Node-A)")
    parser.add_argument("-c", "--color", type=str, default="#3498db", help="指定 UI 色彩 Hex Code (預設: #3498db)")
    args = parser.parse_args()

    # 建立自訂 Handler 並注入參數
    handler = LoadBalanceHTTPRequestHandler
    handler.node_name = args.name
    handler.port = args.port
    handler.color = args.color

    # 綁定 127.0.0.1 本機內部存取,由 Nginx 反向代理進入
    server_address = ("127.0.0.1", args.port)

    with socketserver.TCPServer(server_address, handler) as httpd:
        print(f"🚀 節點【{args.name}】已啟動,監聽於 127.0.0.1:{args.port}")
        print("等待 Nginx 流量分發中...")
        try:
            httpd.serve_forever()
        except KeyboardInterrupt:
            print(f"\n🛑 節點【{args.name}】已停止")


if __name__ == "__main__":
    run()

Part 2
同時啟動兩個(或三個)不同 Port 的節點

# 視窗 1:啟動 Node-A (Port 9090, 藍色)
python3 cluster_node.py -p 9090 -n "Node-A" -c "#3498db"

# 視窗 2:啟動 Node-B (Port 9091, 綠色)
python3 cluster_node.py -p 9091 -n "Node-B" -c "#2ecc71"

# 視窗 3 (可選):啟動 Backup-Node (Port 9092, 橙色)
python3 cluster_node.py -p 9092 -n "Backup-Node" -c "#e67e22"

Part 3
設定 Nginx Upstream
修改 /etc/nginx/sites-available/myapp.conf 設定檔:

upstream backend_cluster {
    # 預設 Round Robin 輪詢
    server 127.0.0.1:9090 max_fails=3 fail_timeout=10s;
    server 127.0.0.1:9091 max_fails=3 fail_timeout=10s;
    
    # 備援節點 (當 9090 與 9091 都掛掉時才會啟用)
    server 127.0.0.1:9092 backup;
}

server {
    listen 80;
    server_name example.com; # 或伺服器 IP

    location / {
        proxy_pass http://backend_cluster;
        
        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_set_header X-Forwarded-Proto $scheme;
    }
}

Part 4
重新載入 Nginx:

sudo nginx -t && sudo systemctl reload nginx

驗證與展示效果

  1. 驗證輪詢分流(Round Robin):
    開啟瀏覽器訪問網站,連續重新整理(Ctrl + R / Cmd + R)。
    觀看視覺體驗:畫面頂部的卡片會隨著每次重新整理,在 【Node-A (藍色 / 9090)】 與 【Node-B (綠色 / 9091)】 之間交替切換。
  2. 驗證故障轉移(Failover / Passive Health Check):
    在視窗 1 按下 Ctrl + C 直接關閉 Node-A (9090)。
    再次在瀏覽器刷頁面,觀察流量是否全數由 【Node-B (9091)】 接手,且完全不會出現 502 錯誤。
  3. 驗證 Backup 備援機制:
    接著按下 Ctrl + C 關閉 Node-B (9091)。

    重新整理頁面,流量將瞬間轉導至 【Backup-Node (橙色 / 9092)】。

以上即可驗證 , Nginx 在前端分配流量到後端可以運作的 server, 避免後端某一台機器掛掉之後服務終止的狀況

Leave A Comment

All fields marked with an asterisk (*) are required