設計負載均衡器 — Round Robin、一致性雜湊與健康檢查實戰 | 資料結構與演算法

2026/07/30
設計負載均衡器 — Round Robin、一致性雜湊與健康檢查實戰 | 資料結構與演算法

負載均衡器(Load Balancer) 是分散式系統的核心基礎設施——從 Nginx 反向代理、AWS ALB 雲端負載均衡、到 Kubernetes 的 Service 路由,背後都是負載均衡演算法在將流量智慧分配至後端伺服器池。本文作為「資料結構與演算法」系列的 最終篇(第 047 篇),將從需求分析出發,完整比較 Round Robin加權輪詢最少連線一致性雜湊(Consistent Hashing)IP Hash 五種演算法,搭配 JavaScript/TypeScriptC++ 雙語言完整實作與健康檢查機制,帶你徹底掌握負載均衡器的設計與實戰。

前言

在上一篇文章中,我們實作了

文本差異比對系統(Text Diff)

,學會如何用 Myers Diff 演算法與 Unified Diff 格式化器打造程式碼審查工具。今天我們來到本系列的最終站——負載均衡器(Load Balancer)

想像你的電商網站在雙十一促銷時,瞬間湧入每秒五萬次請求。如果所有請求都打到同一台伺服器,那台機器會在幾秒內被壓垮;如果有 20 台伺服器卻沒有合理分配流量,可能某幾台過載當機、其餘閒置浪費資源。負載均衡器 就是解決這個問題的核心元件——它是後端服務的「交通警察」,智慧地將每一筆請求導向最適合處理的伺服器。

這個主題完美收束了我們 47 篇文章的學習之旅:負載均衡器運用了我們在

第 005 篇雜湊表

中學過的雜湊函數、在

第 016 篇進階雜湊

中深入的一致性雜湊、在

第 008 篇二分搜尋

中掌握的 O(log N) 查找、以及在

第 006 篇堆積

中學過的優先權佇列——這正是整個系列融會貫通的絕佳實戰。

本文你將學到:

  • 負載均衡器的完整需求分析與規模估算
  • 五種核心負載均衡演算法的原理、優缺點與適用場景
  • 完整可運行的 JavaScript/TypeScript 與 C++ 雙語言實作(含健康檢查機制)
  • 生產環境考量:L4 vs L7、Sticky Sessions、Circuit Breaker、DNS-based LB
  • 4 道 LeetCode 相關練習題

1. 需求分析

功能性需求(Functional Requirements)

  • 流量分配:將 HTTP/TCP 請求分散至多台後端伺服器,避免單點過載
  • 健康檢查:定期探測後端節點存活狀態,自動剔除故障節點、恢復復原節點
  • Session Affinity:支援同一用戶的請求路由至同一後端(Sticky Sessions)
  • 動態擴縮容:後端節點增減時,最小化流量重新分配
  • 權重分配:依節點容量(CPU / 記憶體)配置不同流量比例
  • 路由策略可切換:支援多種負載均衡演算法,可依場景選擇

非功能性需求(Non-Functional Requirements)

需求目標說明
低延遲單次路由決策 < 0.1ms不成為請求的效能瓶頸
高吞吐> 100 萬 RPS單台 LB(Nginx/HAProxy 等級)
高可用99.999%(五個九)LB 本身不能是單點故障
故障偵測< 5 秒節點故障後快速從路由池移除
記憶體效率< 1 MB路由狀態佔用極輕量

規模估算

以中型電商平台為參考場景:

場景:中型電商平台負載均衡

用戶數:1,000 萬活躍用戶
尖峰並發請求:50,000 RPS
後端伺服器:20 台(每台最大處理 5,000 RPS)

健康檢查開銷:
  - 20 台節點 × 每 5 秒一次 = 4 次/秒健康探測
  - 健康探測封包:< 1KB(HTTP HEAD 請求)
  - 開銷可忽略不計

路由決策記憶體(Consistent Hashing):
  - 20 節點 × 150 虛擬節點 = 3,000 個雜湊點
  - 每個雜湊點:8 bytes(uint32 hash)+ 指標 = ~16 bytes
  - 總計:3,000 × 16 bytes ≈ 48 KB(極輕量)

連線狀態追蹤(Least Connections):
  - 20 台節點 × 8 bytes(int64 計數器)= 160 bytes
  - 原子操作更新,無鎖競爭

水平擴展能力:
  - 單台 LB(Nginx/HAProxy):~100 萬 RPS
  - 多台 LB + DNS 輪詢或 Anycast:線性擴展至億級 RPS

結論:負載均衡器的路由決策本身極為輕量——48 KB 記憶體、亞毫秒延遲。核心挑戰在於 健康檢查的即時性節點增減時的流量穩定性LB 本身的高可用架構


2. 方案設計——五種負載均衡演算法

2.1 演算法全景與比較

演算法原理時間複雜度優點缺點適用場景
Round Robin依序循環分配O(1)最簡單,零狀態不考慮節點負載差異節點同質化、請求均勻
Weighted Round Robin按權重比例循環O(1)支援異質節點仍不考慮即時負載節點規格不同
Least Connections選當前連線數最少的節點O(N)即時負載感知需維護全局狀態長連線(WebSocket、DB)
IP Hashhash(client_ip) % NO(1)Session Affinity節點增減時全部重映射有狀態服務(傳統方案)
Consistent Hashing雜湊環 + 虛擬節點O(log V)節點變化只影響 1/N 流量實作較複雜快取服務、微服務路由

V = 虛擬節點總數,N = 伺服器數量

2.2 Round Robin(輪詢)

最簡單的負載均衡演算法——維護一個遞增索引,依序將請求分配給每台伺服器。

請求序列:  #1  #2  #3  #4  #5  #6  #7  #8  #9
分配結果:   A   B   C   A   B   C   A   B   C
             ↻ 循環分配,每台伺服器得到 1/3 流量

優勢:實作僅需一行 index++ % N,零狀態、零開銷。 劣勢:假設所有節點等效——如果 Server A 是 2 核、Server B 是 8 核,仍然平均分配,造成 A 過載而 B 閒置。

2.3 Weighted Round Robin(加權輪詢)

Nginx 預設使用的 平滑加權輪詢(Smooth Weighted Round Robin) 演算法,確保按權重比例分配流量,且分配序列盡可能平滑(不會連續命中同一節點)。

節點池(權重 = CPU 核心數比例):
  Server A:weight=2
  Server B:weight=4
  Server C:weight=2
  總 weight = 8

平滑加權輪詢過程:
  每輪:所有節點 current_weight += weight
  選 current_weight 最大的節點
  選中後:chosen.current_weight -= total_weight

  → 分配序列:B, A, B, C, B, A, B, C(每 8 次循環)
  → B 得到 50%、A 和 C 各 25%,且分布平滑

2.4 Least Connections(最少連線)

即時追蹤每台節點的活躍連線數,每次選擇連線數最少的節點。

當前狀態:
  Server A: 12 connections
  Server B: 5 connections    ← 選這台
  Server C: 8 connections

新請求到來 → 選 Server B → Server B 連線數變為 6
請求完成 → Server B 連線數回到 5

最佳場景:請求處理時間差異大——有些 API 回應 10ms、有些需要 10 秒。Round Robin 會讓某些節點堆積慢請求;Least Connections 自動「填坑」。

2.5 IP Hash

hash(client_ip) % N → 固定路由至同一台伺服器

192.168.1.100 → hash → 2847561 % 3 = 0 → Server A
192.168.1.100 → hash → 2847561 % 3 = 0 → Server A(一定相同)

問題:N=3 → N=4 時
  2847561 % 3 = 0 → Server A
  2847561 % 4 = 1 → Server B   ← 映射完全改變!
  → 約 75% 的請求被路由到不同伺服器

2.6 Consistent Hashing(一致性雜湊)

第 016 篇進階雜湊

中,我們已經深入介紹過一致性雜湊的理論基礎。這裡我們聚焦在它作為負載均衡演算法的實際運用。

Hash Ring(0 到 2³²-1,首尾相連):

              0
              │
    ┌─────────┴─────────┐
    │    Hash Ring       │
    │                    │
 2³²──Node B(hash=25億)──┤
    │                    Node A(hash=5億)
    │                    │
    │   Node C(hash=15億)─────────────────┤
    │                    │
    └────────────────────┘

路由規則:
  hash(request_key) → 在環上找到該點
  → 順時針方向找到第一個 Node
  → 路由至該 Node

新增 Node D(hash=10億):
  → 只影響原本路由到 Node A 的部分 key
  → 其他節點的流量完全不受影響
  → 理論上只重分配 1/(N+1) 的流量

虛擬節點(Virtual Nodes) 解決負載不均問題——真實節點數太少時,環上的分布可能極不均勻。每個真實節點映射 150 個虛擬節點到環上,統計上近似均勻:

Node A → A-0, A-1, ..., A-149(分散在環上 150 個位置)
Node B → B-0, B-1, ..., B-149
Node C → C-0, C-1, ..., C-149

環上共 450 個點,每個節點約佔 1/3
節點性能差異時,可用不同數量的虛擬節點反映
(Node B 效能是 Node A 的 2 倍 → 分配 2 倍虛擬節點)

3. 核心實作

3.1 TypeScript — 完整負載均衡器系統

以下實作包含五種演算法與健康檢查機制,每個類別都可以獨立使用。

Round Robin

// ================================================================
// Round Robin — 最簡單的負載均衡
// ================================================================

interface ServerNode {
  id: string;
  host: string;
  port: number;
  weight: number; // 1~10,用於加權演算法
}

class RoundRobinBalancer {
  private index = 0;

  constructor(private servers: ServerNode[]) {}

  next(): ServerNode | null {
    if (this.servers.length === 0) return null;
    const node = this.servers[this.index % this.servers.length];
    this.index++;
    return node;
  }

  // 動態新增/移除節點
  addServer(server: ServerNode): void {
    this.servers.push(server);
  }

  removeServer(serverId: string): void {
    this.servers = this.servers.filter((s) => s.id !== serverId);
    this.index = this.index % Math.max(1, this.servers.length);
  }
}

// 使用範例
const servers: ServerNode[] = [
  { id: "server-1", host: "10.0.0.1", port: 8080, weight: 2 },
  { id: "server-2", host: "10.0.0.2", port: 8080, weight: 4 },
  { id: "server-3", host: "10.0.0.3", port: 8080, weight: 2 },
];

const rr = new RoundRobinBalancer(servers);
for (let i = 0; i < 6; i++) {
  console.log(`請求 #${i + 1}${rr.next()?.id}`);
}
// 輸出:
// 請求 #1 → server-1
// 請求 #2 → server-2
// 請求 #3 → server-3
// 請求 #4 → server-1
// 請求 #5 → server-2
// 請求 #6 → server-3

Weighted Round Robin(平滑加權輪詢)

// ================================================================
// Weighted Round Robin — Nginx 使用的平滑加權輪詢
// ================================================================

class WeightedRoundRobinBalancer {
  private currentWeights: number[];

  constructor(private readonly servers: ServerNode[]) {
    this.currentWeights = new Array(servers.length).fill(0);
  }

  next(): ServerNode | null {
    if (this.servers.length === 0) return null;

    const totalWeight = this.servers.reduce((sum, s) => sum + s.weight, 0);

    // 每輪:所有節點 current_weight += weight
    for (let i = 0; i < this.servers.length; i++) {
      this.currentWeights[i] += this.servers[i].weight;
    }

    // 選 current_weight 最大的節點
    let maxIdx = 0;
    for (let i = 1; i < this.servers.length; i++) {
      if (this.currentWeights[i] > this.currentWeights[maxIdx]) {
        maxIdx = i;
      }
    }

    // 選中後減去 total_weight
    this.currentWeights[maxIdx] -= totalWeight;
    return this.servers[maxIdx];
  }
}

// 使用範例:驗證分配比例
const wrr = new WeightedRoundRobinBalancer(servers);
const counts: Record<string, number> = {};
for (let i = 0; i < 800; i++) {
  const node = wrr.next()!;
  counts[node.id] = (counts[node.id] ?? 0) + 1;
}
console.log(counts);
// 輸出:{ 'server-1': 200, 'server-2': 400, 'server-3': 200 }
// → server-2(weight=4)得到 50%,符合權重比例

Least Connections(最少連線)

// ================================================================
// Least Connections — 即時負載感知
// ================================================================

class LeastConnectionsBalancer {
  private connections: Map<string, number>;

  constructor(private servers: ServerNode[]) {
    this.connections = new Map(servers.map((s) => [s.id, 0]));
  }

  // 獲取下一個節點(連線數 +1)
  acquire(): ServerNode | null {
    if (this.servers.length === 0) return null;

    let minConn = Infinity;
    let chosen = this.servers[0];

    for (const server of this.servers) {
      const conn = this.connections.get(server.id) ?? 0;
      if (conn < minConn) {
        minConn = conn;
        chosen = server;
      }
    }

    // 增加連線計數
    this.connections.set(
      chosen.id,
      (this.connections.get(chosen.id) ?? 0) + 1
    );
    return chosen;
  }

  // 請求完成後釋放連線
  release(serverId: string): void {
    const current = this.connections.get(serverId) ?? 0;
    this.connections.set(serverId, Math.max(0, current - 1));
  }

  getConnections(): Map<string, number> {
    return new Map(this.connections);
  }
}

// 使用範例:模擬長短請求混合場景
const lc = new LeastConnectionsBalancer(servers);

const s1 = lc.acquire(); // server-1(連線數 0)
const s2 = lc.acquire(); // server-2(連線數 0)
const s3 = lc.acquire(); // server-3(連線數 0)
console.log("分配後:", Object.fromEntries(lc.getConnections()));
// 輸出:{ 'server-1': 1, 'server-2': 1, 'server-3': 1 }

lc.release(s1!.id); // server-1 釋放連線
const s4 = lc.acquire(); // server-1(連線數最少 = 0)
console.log("server-1 釋放後再分配:", s4?.id);
// 輸出:server-1

IP Hash

// ================================================================
// IP Hash — Session Affinity(同 IP 固定路由)
// ================================================================

class IPHashBalancer {
  constructor(private servers: ServerNode[]) {}

  next(clientIP: string): ServerNode | null {
    if (this.servers.length === 0) return null;
    const hash = this.simpleHash(clientIP);
    return this.servers[hash % this.servers.length];
  }

  private simpleHash(str: string): number {
    let hash = 0;
    for (let i = 0; i < str.length; i++) {
      hash = (hash * 31 + str.charCodeAt(i)) >>> 0; // 保持為 uint32
    }
    return hash;
  }
}

// 使用範例:驗證同 IP 固定路由
const ipHash = new IPHashBalancer(servers);
console.log(ipHash.next("192.168.1.100")?.id); // 固定結果
console.log(ipHash.next("192.168.1.100")?.id); // 同 IP → 同節點
console.log(ipHash.next("10.0.0.55")?.id); // 不同 IP → 可能不同節點

Consistent Hashing(一致性雜湊環)

// ================================================================
// Consistent Hash Ring — 完整實作
// 支援:虛擬節點、加權分配、動態增刪節點、O(log V) 路由
// ================================================================
import * as crypto from "crypto";

class ConsistentHashRing {
  private readonly ring: Map<number, ServerNode> = new Map();
  private readonly sortedKeys: number[] = [];
  private readonly nodes: Map<string, ServerNode> = new Map();
  private readonly virtualNodesPerWeight: number;

  constructor(virtualNodesPerWeight: number = 150) {
    this.virtualNodesPerWeight = virtualNodesPerWeight;
  }

  // 新增節點:根據 weight 決定虛擬節點數量
  addNode(node: ServerNode): void {
    if (this.nodes.has(node.id)) {
      this.removeNode(node.id); // 先移除再重新加入(冪等操作)
    }
    this.nodes.set(node.id, node);

    const virtualCount = this.virtualNodesPerWeight * node.weight;
    for (let i = 0; i < virtualCount; i++) {
      const hashKey = this.hash(`${node.id}#${i}`);
      this.ring.set(hashKey, node);
      this.insertSorted(hashKey);
    }
  }

  // 移除節點:從環上移除所有虛擬節點
  removeNode(nodeId: string): void {
    const node = this.nodes.get(nodeId);
    if (!node) return;

    this.nodes.delete(nodeId);
    const virtualCount = this.virtualNodesPerWeight * node.weight;
    for (let i = 0; i < virtualCount; i++) {
      const hashKey = this.hash(`${nodeId}#${i}`);
      this.ring.delete(hashKey);
      this.removeSorted(hashKey);
    }
  }

  // 路由:O(log V) 二分搜尋找順時針最近節點
  getNode(key: string): ServerNode | null {
    if (this.sortedKeys.length === 0) return null;

    const hashKey = this.hash(key);
    const idx = this.binarySearchCeil(hashKey);

    // 若 hashKey 大於所有點,回到環的起點(第一個節點)
    const ringIdx = idx < this.sortedKeys.length ? idx : 0;
    const targetHash = this.sortedKeys[ringIdx];
    return this.ring.get(targetHash) ?? null;
  }

  // 取得 key 對應的 N 個不重複節點(用於資料複製/備援)
  getNodes(key: string, count: number): ServerNode[] {
    if (count >= this.nodes.size) {
      return [...this.nodes.values()];
    }

    const hashKey = this.hash(key);
    let idx = this.binarySearchCeil(hashKey);
    if (idx >= this.sortedKeys.length) idx = 0;

    const result: ServerNode[] = [];
    const seenIds = new Set<string>();

    while (result.length < count) {
      const node = this.ring.get(this.sortedKeys[idx])!;
      if (!seenIds.has(node.id)) {
        seenIds.add(node.id);
        result.push(node);
      }
      idx = (idx + 1) % this.sortedKeys.length;
    }

    return result;
  }

  // 統計:各節點的虛擬節點分布
  getDistribution(): Map<string, number> {
    const dist = new Map<string, number>();
    for (const node of this.ring.values()) {
      dist.set(node.id, (dist.get(node.id) ?? 0) + 1);
    }
    return dist;
  }

  get nodeCount(): number {
    return this.nodes.size;
  }

  get virtualNodeCount(): number {
    return this.sortedKeys.length;
  }

  // ---- 私有工具函數 ----

  private hash(key: string): number {
    const buf = crypto.createHash("md5").update(key).digest();
    return buf.readUInt32BE(0); // 取前 4 bytes 作為 uint32
  }

  private insertSorted(key: number): void {
    let lo = 0,
      hi = this.sortedKeys.length;
    while (lo < hi) {
      const mid = (lo + hi) >> 1;
      if (this.sortedKeys[mid] < key) lo = mid + 1;
      else hi = mid;
    }
    this.sortedKeys.splice(lo, 0, key);
  }

  private removeSorted(key: number): void {
    const idx = this.binarySearchExact(key);
    if (idx !== -1) this.sortedKeys.splice(idx, 1);
  }

  private binarySearchCeil(target: number): number {
    let lo = 0,
      hi = this.sortedKeys.length;
    while (lo < hi) {
      const mid = (lo + hi) >> 1;
      if (this.sortedKeys[mid] < target) lo = mid + 1;
      else hi = mid;
    }
    return lo;
  }

  private binarySearchExact(target: number): number {
    let lo = 0,
      hi = this.sortedKeys.length - 1;
    while (lo <= hi) {
      const mid = (lo + hi) >> 1;
      if (this.sortedKeys[mid] === target) return mid;
      if (this.sortedKeys[mid] < target) lo = mid + 1;
      else hi = mid - 1;
    }
    return -1;
  }
}

// 使用範例
const ring = new ConsistentHashRing(150);
servers.forEach((s) => ring.addNode(s));

console.log("虛擬節點總數:", ring.virtualNodeCount);
// 輸出:1200((2+4+2)×150)

console.log("分布:", Object.fromEntries(ring.getDistribution()));
// 輸出:{ 'server-1': 300, 'server-2': 600, 'server-3': 300 }

// 一致性路由測試
const testKeys = ["user-12345", "session-abc", "cache-key-99"];
for (const key of testKeys) {
  console.log(`${key}${ring.getNode(key)?.id}`);
}

// 節點故障模擬
console.log("\n--- 移除 server-2 ---");
ring.removeNode("server-2");
for (const key of testKeys) {
  console.log(`${key}${ring.getNode(key)?.id}`);
}
// → 原本路由到 server-2 的 key 會被重新分配到 server-1 或 server-3
// → 原本路由到 server-1/3 的 key 不受影響

健康檢查管理器

// ================================================================
// Health Checker — 主動健康檢查與自動故障轉移
// ================================================================

interface HealthCheckConfig {
  intervalMs: number; // 檢查間隔(毫秒)
  timeoutMs: number; // 超時時間
  path: string; // 健康檢查路徑(如 "/health")
  unhealthyThreshold: number; // 連續失敗幾次才標記為不健康
  healthyThreshold: number; // 連續成功幾次才標記為健康
}

class HealthChecker {
  private failureCount: Map<string, number> = new Map();
  private successCount: Map<string, number> = new Map();
  private healthyNodes: Set<string>;

  constructor(
    private readonly ring: ConsistentHashRing,
    private readonly allNodes: ServerNode[],
    private readonly config: HealthCheckConfig
  ) {
    this.healthyNodes = new Set(allNodes.map((n) => n.id));
  }

  // 啟動定時健康檢查
  start(): NodeJS.Timeout {
    return setInterval(async () => {
      await Promise.all(this.allNodes.map((node) => this.checkNode(node)));
    }, this.config.intervalMs);
  }

  private async checkNode(node: ServerNode): Promise<void> {
    const url = `http://${node.host}:${node.port}${this.config.path}`;
    try {
      const controller = new AbortController();
      const timeoutId = setTimeout(
        () => controller.abort(),
        this.config.timeoutMs
      );
      const response = await fetch(url, { signal: controller.signal });
      clearTimeout(timeoutId);

      if (response.ok) {
        this.onSuccess(node);
      } else {
        this.onFailure(node);
      }
    } catch {
      this.onFailure(node);
    }
  }

  private onSuccess(node: ServerNode): void {
    this.failureCount.set(node.id, 0);
    const successes = (this.successCount.get(node.id) ?? 0) + 1;
    this.successCount.set(node.id, successes);

    // 節點恢復健康:連續成功次數達到閾值
    if (
      !this.healthyNodes.has(node.id) &&
      successes >= this.config.healthyThreshold
    ) {
      this.healthyNodes.add(node.id);
      this.ring.addNode(node);
      console.log(`[HealthCheck] ✓ ${node.id} is back online`);
    }
  }

  private onFailure(node: ServerNode): void {
    this.successCount.set(node.id, 0);
    const failures = (this.failureCount.get(node.id) ?? 0) + 1;
    this.failureCount.set(node.id, failures);

    // 節點標記為不健康:連續失敗次數達到閾值
    if (
      this.healthyNodes.has(node.id) &&
      failures >= this.config.unhealthyThreshold
    ) {
      this.healthyNodes.delete(node.id);
      this.ring.removeNode(node.id);
      console.log(
        `[HealthCheck] ✗ ${node.id} is DOWN (${failures} consecutive failures)`
      );
    }
  }

  getHealthyNodes(): string[] {
    return [...this.healthyNodes];
  }

  isHealthy(nodeId: string): boolean {
    return this.healthyNodes.has(nodeId);
  }
}

// 使用範例
const healthConfig: HealthCheckConfig = {
  intervalMs: 5000, // 每 5 秒檢查一次
  timeoutMs: 2000, // 超時 2 秒視為失敗
  path: "/health",
  unhealthyThreshold: 3, // 連續 3 次失敗 → 標記不健康
  healthyThreshold: 2, // 連續 2 次成功 → 恢復健康
};

const healthyRing = new ConsistentHashRing(150);
servers.forEach((s) => healthyRing.addNode(s));
const checker = new HealthChecker(healthyRing, servers, healthConfig);

// checker.start();  // 啟動定時健康檢查
console.log("健康節點:", checker.getHealthyNodes());
// 輸出:['server-1', 'server-2', 'server-3']

3.2 C++ — 高效能負載均衡器

以下是完整的 C++ 實作,使用 std::map 維護有序雜湊環,實現 O(log N) 路由決策。

// ================================================================
// 負載均衡器 — C++ 高效能完整實作
// 包含:Consistent Hash Ring、Weighted Round Robin、Least Connections
// ================================================================
#include <iostream>
#include <map>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <string>
#include <atomic>
#include <mutex>
#include <optional>
#include <chrono>

struct ServerNode {
    std::string id;
    std::string host;
    int port;
    int weight;  // 1-10
};

// ================================================================
// 1. Consistent Hash Ring(一致性雜湊環)
// ================================================================
class ConsistentHashRing {
public:
    explicit ConsistentHashRing(int virtualNodesPerWeight = 150)
        : virtualNodesPerWeight_(virtualNodesPerWeight) {}

    void addNode(const ServerNode& node) {
        removeNode(node.id);  // 冪等操作
        nodes_[node.id] = node;

        const int virtualCount = virtualNodesPerWeight_ * node.weight;
        for (int i = 0; i < virtualCount; i++) {
            const std::string key = node.id + "#" + std::to_string(i);
            const uint32_t hash = hashKey(key);
            ring_[hash] = &nodes_[node.id];
        }
    }

    void removeNode(const std::string& nodeId) {
        auto it = nodes_.find(nodeId);
        if (it == nodes_.end()) return;

        const int virtualCount = virtualNodesPerWeight_ * it->second.weight;
        for (int i = 0; i < virtualCount; i++) {
            const std::string key = nodeId + "#" + std::to_string(i);
            ring_.erase(hashKey(key));
        }
        nodes_.erase(it);
    }

    // 路由:O(log V),V = 虛擬節點總數
    std::optional<ServerNode> getNode(const std::string& requestKey) const {
        if (ring_.empty()) return std::nullopt;

        const uint32_t hash = hashKey(requestKey);
        auto it = ring_.lower_bound(hash);  // 找到第一個 >= hash 的位置

        if (it == ring_.end()) it = ring_.begin();  // 環繞
        return *it->second;
    }

    // 取得 N 個不重複節點(用於資料複製)
    std::vector<ServerNode> getNodes(
        const std::string& requestKey, int count
    ) const {
        if (ring_.empty()) return {};
        count = std::min(count, static_cast<int>(nodes_.size()));

        const uint32_t hash = hashKey(requestKey);
        auto it = ring_.lower_bound(hash);
        if (it == ring_.end()) it = ring_.begin();

        std::vector<ServerNode> result;
        std::unordered_set<std::string> seen;
        auto cur = it;

        while (static_cast<int>(result.size()) < count) {
            if (seen.find(cur->second->id) == seen.end()) {
                seen.insert(cur->second->id);
                result.push_back(*cur->second);
            }
            ++cur;
            if (cur == ring_.end()) cur = ring_.begin();
            if (cur == it) break;  // 環繞一圈
        }
        return result;
    }

    // 分布統計
    std::unordered_map<std::string, int> getDistribution() const {
        std::unordered_map<std::string, int> dist;
        for (const auto& [hash, node] : ring_) {
            dist[node->id]++;
        }
        return dist;
    }

    int nodeCount() const { return static_cast<int>(nodes_.size()); }
    int virtualNodeCount() const { return static_cast<int>(ring_.size()); }

private:
    // FNV-1a Hash(快速且分布均勻)
    uint32_t hashKey(const std::string& key) const {
        uint32_t hash = 2166136261u;  // FNV offset basis
        for (char c : key) {
            hash ^= static_cast<uint8_t>(c);
            hash *= 16777619u;  // FNV prime
        }
        return hash;
    }

    std::map<uint32_t, ServerNode*> ring_;              // 有序環
    std::unordered_map<std::string, ServerNode> nodes_;  // 節點儲存
    int virtualNodesPerWeight_;
};

// ================================================================
// 2. Weighted Round Robin(平滑加權輪詢,執行緒安全)
// ================================================================
class WeightedRoundRobin {
public:
    explicit WeightedRoundRobin(std::vector<ServerNode> servers)
        : servers_(std::move(servers)) {
        totalWeight_ = 0;
        for (const auto& s : servers_) totalWeight_ += s.weight;
        currentWeights_.resize(servers_.size(), 0);
    }

    const ServerNode* next() {
        if (servers_.empty()) return nullptr;

        std::lock_guard<std::mutex> lock(mutex_);

        // 所有節點 current_weight += weight
        for (size_t i = 0; i < servers_.size(); i++) {
            currentWeights_[i] += servers_[i].weight;
        }

        // 選最大的
        int maxIdx = 0;
        for (size_t i = 1; i < servers_.size(); i++) {
            if (currentWeights_[i] > currentWeights_[maxIdx]) {
                maxIdx = static_cast<int>(i);
            }
        }

        // 選中後減去 total_weight
        currentWeights_[maxIdx] -= totalWeight_;
        return &servers_[maxIdx];
    }

private:
    std::vector<ServerNode> servers_;
    std::vector<int> currentWeights_;
    int totalWeight_;
    std::mutex mutex_;
};

// ================================================================
// 3. Least Connections(原子計數器,無鎖高效能)
// ================================================================
class LeastConnections {
public:
    explicit LeastConnections(std::vector<ServerNode> servers)
        : servers_(std::move(servers)) {
        connections_.resize(servers_.size());
        for (auto& conn : connections_) conn.store(0);
    }

    LeastConnections(const LeastConnections&) = delete;  // atomic 不可複製

    const ServerNode* acquire() {
        if (servers_.empty()) return nullptr;

        int minIdx = 0;
        int minConn = connections_[0].load(std::memory_order_relaxed);

        for (size_t i = 1; i < servers_.size(); i++) {
            int conn = connections_[i].load(std::memory_order_relaxed);
            if (conn < minConn) {
                minConn = conn;
                minIdx = static_cast<int>(i);
            }
        }

        connections_[minIdx].fetch_add(1, std::memory_order_relaxed);
        return &servers_[minIdx];
    }

    void release(const std::string& serverId) {
        for (size_t i = 0; i < servers_.size(); i++) {
            if (servers_[i].id == serverId) {
                int prev = connections_[i].fetch_sub(
                    1, std::memory_order_relaxed
                );
                if (prev <= 0) connections_[i].store(0);
                return;
            }
        }
    }

    void printConnections() const {
        for (size_t i = 0; i < servers_.size(); i++) {
            std::cout << servers_[i].id << ": "
                      << connections_[i].load() << " connections\n";
        }
    }

private:
    std::vector<ServerNode> servers_;
    std::vector<std::atomic<int>> connections_;
};

// ================================================================
// 主程式示範
// ================================================================
int main() {
    std::vector<ServerNode> servers = {
        {"server-1", "10.0.0.1", 8080, 2},
        {"server-2", "10.0.0.2", 8080, 4},
        {"server-3", "10.0.0.3", 8080, 2},
    };

    // === Consistent Hash Ring ===
    std::cout << "=== Consistent Hash Ring ===\n";
    ConsistentHashRing ring(150);
    for (const auto& s : servers) ring.addNode(s);

    std::cout << "虛擬節點總數: " << ring.virtualNodeCount() << "\n";
    // 輸出:1200((2+4+2)×150)

    auto dist = ring.getDistribution();
    for (const auto& [id, count] : dist) {
        std::cout << id << ": " << count << " 虛擬節點\n";
    }
    // 輸出:server-1: 300, server-2: 600, server-3: 300

    // 路由測試
    std::vector<std::string> keys = {
        "user-12345", "session-abc", "cache-key-99"
    };
    for (const auto& key : keys) {
        auto node = ring.getNode(key);
        if (node) std::cout << key << " → " << node->id << "\n";
    }

    // 節點移除(僅影響 ~1/3 流量)
    std::cout << "\n移除 server-2 後:\n";
    ring.removeNode("server-2");
    for (const auto& key : keys) {
        auto node = ring.getNode(key);
        if (node) std::cout << key << " → " << node->id << "\n";
    }

    // === Weighted Round Robin ===
    std::cout << "\n=== Weighted Round Robin ===\n";
    WeightedRoundRobin wrr(servers);
    std::unordered_map<std::string, int> counts;
    for (int i = 0; i < 800; i++) {
        const auto* node = wrr.next();
        if (node) counts[node->id]++;
    }
    for (const auto& [id, count] : counts) {
        std::cout << id << ": " << count << "/800 ("
                  << (count * 100 / 800) << "%)\n";
    }
    // 輸出:server-1: 200 (25%), server-2: 400 (50%), server-3: 200 (25%)

    // === Least Connections ===
    std::cout << "\n=== Least Connections ===\n";
    LeastConnections lc(servers);
    const auto* s1 = lc.acquire();  // server-1
    const auto* s2 = lc.acquire();  // server-2
    const auto* s3 = lc.acquire();  // server-3
    lc.printConnections();
    // 輸出:各 1 connection

    if (s1) lc.release(s1->id);
    std::cout << "釋放 " << s1->id << " 後:\n";
    lc.printConnections();
    // 輸出:server-1: 0, server-2: 1, server-3: 1

    return 0;
}

4. 效能分析

時間複雜度

操作複雜度說明
Round Robin next()O(1)索引遞增 + 取餘
Weighted Round Robin next()O(N)掃描所有節點找 max,N 通常很小(< 100)
Least Connections acquire()O(N)掃描所有節點找 min
IP Hash next()O(1)雜湊 + 取餘
Consistent Hashing getNode()O(log V)二分搜尋雜湊環,V = 虛擬節點數
Consistent Hashing addNode()O(W × log V)插入 W 個虛擬節點(W = weight × base)
Consistent Hashing removeNode()O(W × log V)移除 W 個虛擬節點
健康檢查(單次)O(N)並行探測所有 N 個節點

空間複雜度

結構空間說明
Round RobinO(1)只需一個索引
Weighted Round RobinO(N)N 個 current_weight 計數器
Least ConnectionsO(N)N 個連線計數器
IP HashO(1)無狀態
Consistent Hashing(有序環)O(V)V = 所有虛擬節點,存雜湊值 + 指標

效能基準

場景節點數演算法單次路由延遲記憶體
小型服務3-5 台Round Robin< 10ns< 1 KB
中型平台20 台WRR< 50ns< 1 KB
微服務叢集100 台Consistent Hashing< 500ns~240 KB
大型 CDN1,000 台Consistent Hashing< 2μs~24 MB
超大規模10,000 台P2C + Least Connections< 100ns~80 KB

關鍵洞察:在實際生產環境中,路由決策的延遲(奈秒至微秒級)相比網路傳輸延遲(毫秒級)完全可以忽略。選擇演算法的核心考量不是速度,而是 負載均勻性節點變化時的穩定性


5. 生產環境考量

5.1 L4 vs L7 負載均衡

生產環境通常採用多層架構,L4 與 L7 負載均衡器各司其職:

┌────────────────────────────────────────────────────────────┐
│                  DNS 層(Anycast / GeoDNS)                  │
│   用戶 DNS 查詢 → 返回最近 POP(Point of Presence)的 IP    │
└────────────────────────┬───────────────────────────────────┘
                         │
                         ▼
┌────────────────────────────────────────────────────────────┐
│              L4 Load Balancer(傳輸層)                       │
│   基於 IP/Port 的 TCP 轉發,超高吞吐(>1000 萬 PPS)        │
│   代表:Linux IPVS、AWS NLB、Google Cloud Network LB        │
└────────────────────────┬───────────────────────────────────┘
                         │
               ┌─────────┴──────────┐
               ▼                    ▼
┌─────────────────────┐  ┌─────────────────────┐
│   L7 LB Node 1      │  │   L7 LB Node 2      │
│  (Nginx / Envoy)  │  │  (Nginx / Envoy)  │
│   路由算法:         │  │   路由算法:         │
│   Consistent Hash    │  │   Consistent Hash    │
└──────────┬──────────┘  └──────────┬──────────┘
           │                        │
           └────────┬───────────────┘
                    │
      ┌─────────────┼─────────────┐
      ▼             ▼             ▼
┌──────────┐  ┌──────────┐  ┌──────────┐
│ Backend  │  │ Backend  │  │ Backend  │
│ Server A │  │ Server B │  │ Server C │
└──────────┘  └──────────┘  └──────────┘
維度L4(傳輸層)L7(應用層)
解析層級IP + PortHTTP Headers、URL、Cookie
吞吐量> 1000 萬 PPS~100 萬 RPS
功能純轉發路由規則、SSL 終結、重寫
延遲< 10μs< 100μs
代表IPVS、AWS NLBNginx、Envoy、HAProxy
適用場景入口層、高吞吐應用層路由、API Gateway

5.2 Sticky Sessions(黏性會話)

三種 Sticky Sessions 實作方案:

方案 1:Cookie-based Affinity
  首次請求 → LB 選擇後端 → 設定 Cookie:LB_SESSION=server-1
  後續請求 → 讀取 Cookie → 直接路由至 server-1
  問題:server-1 故障後 Cookie 失效,需處理 fallback

方案 2:Consistent Hashing(以 Session ID 為 key)
  hash(session_id) → 一致性路由至同一後端
  節點故障時,只有 1/N 的 Session 受影響
  → 推薦用於需要快取親和性的場景

方案 3:外部化 Session Store(最佳實踐)
  Session → Redis / Memcached(所有節點共享)
  → 任何後端都能處理任何請求
  → 完全消除 Sticky Sessions 需求

5.3 Graceful Shutdown(優雅關閉)

當後端節點需要重啟或部署時,直接斷開連線會導致進行中的請求失敗。優雅關閉的流程:

1. 從 LB 路由池移除該節點(停止分配新請求)
2. 等待所有進行中的請求完成(設定超時時間,如 30 秒)
3. 超時後強制關閉剩餘連線
4. 節點停機、更新、重啟
5. 健康檢查通過後重新加入路由池

在 Kubernetes 中,這對應到 Pod 的 preStop hookterminationGracePeriodSeconds

spec:
  terminationGracePeriodSeconds: 30
  containers:
  - name: app
    lifecycle:
      preStop:
        exec:
          command: ["/bin/sh", "-c", "sleep 5 && kill -SIGTERM 1"]
    # readinessProbe 失敗後 → 從 Service Endpoints 移除
    readinessProbe:
      httpGet:
        path: /health
        port: 8080
      periodSeconds: 5
      failureThreshold: 1

5.4 Circuit Breaker(熔斷器)

當後端節點不是完全當機、而是「慢死」(回應時間急劇增加),健康檢查可能仍然通過,但實際處理能力已大幅下降。熔斷器是解決這個問題的關鍵機制:

熔斷器三種狀態:

  Closed(閉合)→ 正常路由,計數連續錯誤
       │
       │ 連續錯誤 ≥ 閾值
       ▼
  Open(斷開)→ 完全停止路由至此節點
       │           直接返回降級回應或 503
       │
       │ 經過重置時間(如 30 秒)
       ▼
  Half-Open(半開)→ 允許少量試探性請求
       │
       ├── 試探成功 → 回到 Closed
       └── 試探失敗 → 回到 Open

5.5 DNS-based 負載均衡

在負載均衡器之上,DNS 層提供了更大範圍的流量分配:

DNS Round Robin:
  example.com → 1.1.1.1  (數據中心 A)
  example.com → 2.2.2.2  (數據中心 B)
  DNS 交替返回不同 IP,實現跨數據中心分流

GeoDNS:
  亞洲用戶 → 東京數據中心 IP
  歐洲用戶 → 法蘭克福數據中心 IP
  → 降低跨洲延遲

Anycast:
  多個數據中心宣告相同 IP
  BGP 路由自動選擇最近的數據中心
  → 最低延遲 + 天生 DDoS 防護
  代表:Cloudflare、Google Cloud CDN

5.6 Power of Two Choices(P2C)

在大規模叢集(> 100 節點)中,完整的 Least Connections 需要掃描所有節點(O(N)),開銷不小。P2C 是一個巧妙的近似方案:

P2C 演算法:
  1. 隨機選兩個節點
  2. 比較兩者的活躍連線數
  3. 選連線數較少的那個

時間複雜度:O(1)(只需比較 2 個節點)
效果:數學上可以證明,P2C 的負載不均勻度從 O(log N / log log N)
     降低到 O(log log N)——指數級改善!

Envoy proxy 的預設策略就是 P2C + EWMA(指數加權移動平均延遲)

6. LeetCode 練習

以下是與負載均衡器核心演算法相關的練習題目:

題目一:LeetCode 1603 — Design Parking System

項目內容
難度Easy
連結LeetCode 1603
核心設計一個停車系統,管理大、中、小三種車位的容量
提示這是資源管理的基礎練習——停車位就像後端伺服器的容量,每種類型有上限,需要判斷是否還能接受新請求。類似於 Least Connections 中追蹤每台伺服器的連線數是否達到上限

題目二:LeetCode 380 — Insert Delete GetRandom O(1)

項目內容
難度Medium
連結LeetCode 380
核心設計一個支援 O(1) 插入、刪除和隨機取值的資料結構
提示負載均衡器需要 O(1) 動態新增/移除節點,並且能快速隨機選擇節點(P2C 演算法需要隨機選兩個節點)。本題的 HashMap + Array 技巧正是伺服器池管理的核心資料結構

題目三:LeetCode 710 — Random Pick with Blacklist

項目內容
難度Hard
連結LeetCode 710
核心在 [0, N) 範圍內隨機選擇一個不在黑名單中的數字
提示這對應健康檢查後的伺服器選擇問題:從 N 台伺服器中排除不健康的(黑名單),在剩餘的健康伺服器中隨機選擇。需要巧妙的 HashMap 映射,避免每次都重建有效伺服器列表

題目四:LeetCode 528 — Random Pick with Weight

項目內容
難度Medium
連結LeetCode 528
核心根據權重陣列,按權重比例隨機選擇索引
提示這正是 Weighted Round Robin加權 Consistent Hashing 的數學基礎!使用前綴和 + 二分搜尋,按權重比例隨機選擇伺服器。前綴和的二分搜尋技巧與 Consistent Hash Ring 上的 binarySearchCeil 完全一致

練習建議:先完成 1603 理解資源容量管理的基本概念,再做 380 掌握 O(1) 動態集合操作(伺服器池管理)。528 直接對應 Weighted Round Robin 的數學模型,710 對應健康檢查後的排除邏輯。四題串起來就是負載均衡器的完整知識鏈。


7. 總結——系列完結篇

在這篇文章中,我們從需求分析出發,完整設計並實作了一套負載均衡器系統:

  1. 五種核心演算法:Round Robin 的極簡優雅、Weighted Round Robin 的權重感知、Least Connections 的即時負載追蹤、IP Hash 的 Session 親和性、Consistent Hashing 的動態擴縮穩定性——每種演算法都有其最佳適用場景。

  2. 一致性雜湊環:運用了雜湊函數將節點與請求映射到同一個環上,搭配虛擬節點實現均勻分布,透過二分搜尋實現 O(log V) 路由決策。新增或移除節點時只有 1/N 的流量受影響,這是大規模分散式系統擴縮容的關鍵。

  3. 健康檢查機制:主動探測 + 閾值判定 + 自動故障轉移,確保流量不會被路由到故障節點。搭配 Circuit Breaker 熔斷器處理「慢死」場景。

  4. 生產環境架構:L4/L7 多層負載均衡、DNS-based 全球分流、Sticky Sessions 三種方案、Graceful Shutdown 優雅關閉、P2C 大規模近似演算法——這些是從原型到生產的必經之路。


系列回顧——從 Big O 到 Load Balancer 的 47 篇旅程

寫到這裡,「資料結構與演算法」系列的全部 47 篇文章 正式完結。讓我們回顧這段從零到一的學習旅程:

第一階段:基礎資料結構(001-008)

我們從

001 Big O 與複雜度分析

出發,建立了分析演算法效能的共同語言。接著依序學習了

陣列與字串

鏈結串列

堆疊與佇列

雜湊表

堆積與優先權佇列

排序演算法

二分搜尋

——這些是所有進階主題的基石。

第二階段:進階資料結構(009-016)

我們深入了

二元樹

二元搜尋樹

的世界,學會了

Trie 字典樹

Union-Find 並查集

線段樹

樹狀陣列

進階樹

進階雜湊

——今天的 Consistent Hashing 正是第 016 篇知識的直接應用。

第三階段:基礎演算法(017-024)

遞迴與回溯

教會我們拆解複雜問題,

雙指標

滑動窗口

是陣列高效處理的利器,

動態規劃基礎

進階動態規劃

是面試的核心考點,

貪心演算法

分治法

位元運算

各展所長。

第四階段:圖論與進階(025-032)

圖的表示

出發,

BFS 與 DFS

最短路徑

拓撲排序

最小生成樹

樹演算法

網路流

字串演算法

——圖論是系統設計(如網路拓撲、任務排程)的理論基礎。

第五階段:數學與特化(033-039)

數論

計算幾何

博弈論

隨機化演算法

近似與線上演算法

後綴結構

持久化資料結構

——這些特化主題為我們打開了演算法世界更深邃的大門。

第六階段:實戰應用(040-047)

最後 8 篇文章是整個系列的精華——我們將前面所學的一切融會貫通,設計了真實世界中的系統:

Rate Limiter 限流器

LRU/LFU 快取

搜尋自動補全

任務排程器

短網址系統

推薦系統

文本差異比對

,以及本篇的 負載均衡器


回顧這 47 篇文章,有一個貫穿始終的核心洞見:每一個看似複雜的系統設計,拆解之後都是基礎資料結構與演算法的組合

  • Rate Limiter 用到了雜湊表(O(1) 查找)+ 滑動窗口(時間序列管理)
  • LRU Cache 是雙向鏈結串列 + 雜湊表的經典組合
  • 搜尋自動補全 建立在 Trie 字典樹之上
  • 任務排程器 靠的是堆積(優先權佇列)+ 拓撲排序
  • 短網址系統 利用了 Base62 編碼 + 雜湊碰撞處理
  • 推薦系統 運用了排序演算法 + Bloom Filter + 雜湊表
  • 文本差異比對 是動態規劃(LCS)+ 圖搜尋(Myers Diff)
  • 負載均衡器 結合了一致性雜湊 + 二分搜尋 + 雜湊環

當你扎實地掌握了基礎,面對任何新的系統設計問題,都能從容地拆解為已知的資料結構與演算法,然後逐步組裝出解決方案。

感謝你跟著這個系列走到最後。 從第一篇 Big O 的 O(1) 到最後一篇負載均衡器的 Consistent Hashing,47 篇文章、數百個程式碼範例、上百道 LeetCode 練習——這段旅程的每一步都在為你建立系統性的演算法思維。

希望這個系列能夠成為你在軟體工程道路上的堅實基礎。無論是面試準備、日常開發、還是系統架構設計,這些知識都將伴隨你的職業生涯持續發揮作用。

如有任何問題或建議,歡迎隨時聯繫!


FAQ

Q: Consistent Hashing 和普通 IP Hash 有什麼差別?什麼時候該用 Consistent Hashing?

普通 IP Hash 使用 hash(client_ip) % N 將請求映射到 N 台伺服器之一,問題在於當 N 變化時(新增或移除節點),幾乎所有請求的映射都會改變——假設原本 N=3,擴展為 N=4,約 75% 的請求會被路由到不同的伺服器,導致快取全部失效或 Session 全部中斷。Consistent Hashing 則將節點與請求都映射到同一個雜湊環上(0 到 2³²-1),請求順時針找到最近的節點。新增或移除節點時,只有相鄰區段的流量受影響,理論上只有 1/N 的請求需要重新路由。搭配虛擬節點後,負載分布也能做到近似均勻。當你的系統需要頻繁擴縮容、使用分散式快取、或需要 Session Affinity 時,應該選擇 Consistent Hashing;如果節點數量固定且沒有快取依賴,簡單的 IP Hash 就足夠了。

Q: Round Robin 和 Least Connections 該怎麼選擇?

選擇 Round Robin 還是 Least Connections 取決於你的請求處理模型。Round Robin 適合所有請求處理時間相近、節點規格相同的場景——例如純 API 查詢服務、靜態檔案伺服器。它的優勢是零狀態、零開銷、實作最簡單。然而,如果請求處理時間差異很大(有些 10ms、有些 10 秒),Round Robin 會導致某些節點堆積大量慢請求。Least Connections 會即時追蹤每台節點的活躍連線數,優先分配給最閒的節點,適合 WebSocket 長連線、資料庫連線池、影片串流等處理時間不均勻的場景。代價是需要維護全局的連線計數狀態,在分散式負載均衡器中需要額外的同步機制。實務上,Nginx 預設使用 Round Robin,切換到 Least Connections 只需加一行設定 least_connKubernetes 的 kube-proxy 在 IPVS 模式下也支援 lc(Least Connection)排程。

Q: 生產環境中負載均衡器掛了怎麼辦?如何避免單點故障?

負載均衡器本身成為單點故障(SPOF)是實務中必須解決的問題。解決方案是多層架構:第一層是 DNS 層負載均衡,使用 DNS Round Robin 或 Anycast 將流量分散到多個入口 IP,每個 IP 對應一台 L4 負載均衡器;第二層是 L4 負載均衡器的高可用,使用 VRRP(Virtual Router Redundancy Protocol)實現主備切換——主 LB 掛掉後,備用 LB 在數秒內接管 VIP(Virtual IP),Linux 環境常用 Keepalived + IPVS 實現;第三層是 L7 負載均衡器叢集,多台 Nginx/Envoy 實例由 L4 LB 分配流量。雲端環境更簡單:AWS 的 ALB/NLB、GCP 的 Cloud Load Balancing 都是全託管服務,本身就是跨可用區部署的高可用架構。另外,可以搭配 Circuit Breaker 熔斷器——當檢測到 LB 後方的後端節點大面積故障時,快速返回降級回應而非無限等待,保護上游系統不被拖垮。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。

本週主打

AI 自動化入門包

你每天手動在做的那些煩事,其實 AI 可以自己跑。這份給你 10 個照著做就會的自動化工作流 + 50 個複製即用的提示詞,不用會寫程式。

看看這個產品 →