【実務・中級編】 LACPのロードバランシング(ハッシュアルゴリズム)の仕組みと設計上の偏り対策 – インフラ・物理&L2/L3ネットワーク基礎実践ガイド

LACPハッシュの深淵:トラフィック偏極(Polarization)を見抜き、ロードバランシングを極める技術

こんにちは。ネットワークの裏側を覗き見ることが何よりの趣味である、シニアインフラアーキテクトの私です。

これまでに幾度となく「リンクアグリゲーション(LACP / IEEE 802.3ad)を組んだのに、なぜか特定の一本だけ帯域が張り付いてパケットロスを起こす」「アクティブ・スタンバイかのような偏ったトラフィックフローになる」という現場の悲鳴を聞いてきました。帯域を倍増させるために2本の10GbEを束ねたはずが、実効スループットが1本の限界値を超えられない。この現象の裏側には、L2/L3スイッチが内部で行っているハッシュ計算のアルゴリズムと、トラフィックの「偏り(Polarization)」という冷徹な数学的現実が隠されています。

今回は、教科書的な「LACPって何?」という基礎飛び越え、パケットがスイッチのASIC(Application-Specific Integrated Circuit)を通過する瞬間に何が起きているのか、その深淵を一緒に覗いていきましょう。実務で即座に使えるハッシュの仕組み、設計上の対策、そしてPythonを用いたトラフィック検証の自動化手法まで、泥臭い知見を余すところなくお伝えします。

—

1. LACPロードバランシングの基本原理:ハッシュ計算のメカニズム

IEEE 802.3ad(現在はIEEE 802.1AXに統合)で規定されるLACPは、複数の物理リンクを仮想的な1本の論理リンク(Port Channel / LAG)に束ねる技術です。しかし、ここで重要なのは、「パケット単位(Packet-by-packet)」ではなく「フロー単位(Flow-based)」でロードバランシングが行われるという点です。

もしパケット単位でロードバランシングを行ってしまうと、次のような大惨事が起きます。
1. フローAの1番目のパケットがリンク1を通る。
2. 2番目のパケットがリンク2を通る。
3. リンク2の方が遅延がわずかに少なかったり、バッファの状態が異なったりした結果、宛先でのパケット到着順序が逆転(Packet Reordering)する。
4. TCPのシーケンス番号が狂い、DUP ACKが頻発してスループットが急降下する。

これを防ぐため、スイッチのハードウェア(ASIC)は、パケットの特定フィールド(ヘッダー情報)を抽出し、それを数学的なハッシュ関数に投入して「どの物理ポートを使うか」を一意に決定します。

ハッシュ計算に使用されるパラメータの階層

一般的なL2/L3/L4スイッチ(Cisco Catalyst/NexusやArista, Linuxのbonding/teamingなど)では、ハッシュの入力(Hash Inputs)として以下のフィールドを組み合わせることができます。

  • L2層: 送信元MACアドレス (Source MAC)、宛先MACアドレス (Destination MAC)
  • L3層: 送信元IPアドレス (Source IP)、宛先IPアドレス (Destination IP)
  • L4層: TCP/UDPの送信元ポート番号 (Source Port)、宛先ポート番号 (Destination Port)

これらを「どの粒度でハッシュに含めるか(port-channel load-balance等の設定)」によって、トラフィックの分散効率が劇的に変わります。

—

2. 実務の罠:なぜ偏り(Polarization)が発生するのか?

「IPとL4ポートまでハッシュに入れているから完璧だ」と思っていませんか? ここにインフラエンジニアを絶望させる「ハッシュの偏極(Polarization)」という魔物が潜んでいます。

シナリオ:1台のロードバランサー(LB)とバックエンド群の悲劇

例えば、次のようなWebシステムを想像してください。

  • フロントにLACP(L3/L4ハッシュ)で接続されたルーター/スイッチがある。
  • その配下に数台のロードバランサー(LB)がおり、さらにその裏に大量のWeb/APIサーバー(バックエンド)がいる。

ここで、インターネットからやってくるクライアントのIPアドレスや送信元L4ポートは多種多様です。しかし、LBからバックエンドサーバーへ向かうトラフィックはどうでしょうか?
LBからバックエンドへ向かう際、送信元IPアドレスは「LBのIPアドレス(1つ、または少数)」に固定されがちです。さらに、バックエンド側の待ち受けポートも多くの場合、TCP/80やTCP/443といった固定のポート番号になります。

結果として、ハッシュ関数の入力値のうち「変動する要素」が極端に減少し、すべての通信が同じハッシュ値を生み出してしまうのです。その結果、4本あるリンクのうち、常に特定の1本の物理リンクだけに全トラフィックが集中し、残りの3本が遊休状態(0%)になるという現象が発生します。

—

3. 対策とトポロジ最適化手法

この偏りを防ぎ、真のロードバランシングを実現するためには、以下の設計アプローチを組み合わせて実装する必要があります。

対策A:ハッシュアルゴリズムの粒度を最大化する(L4ポートの活用)

スイッチ側でL2だけでなく、L3、そして可能な限りL4(ポート番号)までを含めたハッシュを有効にします。Cisco Nexusスイッチでの設定例を以下に示します。

! Cisco Nexusスイッチにおけるポートチャネルのハッシュアルゴリズム設定例
configure terminal
  ! 送信元/宛先IPアドレスとL4のTCP/UDPポート番号をハッシュ計算に含める
  system port-channel load-balance ethernet source-destination port
  
  ! 必要に応じてVLAN IDやトランスポート層の情報をさらに加える場合
  ! system port-channel load-balance ethernet source-destination ip-l4port
end

対策B:ハッシュの偏りを打ち砕く「ローテーション(Salt/Seed)」の導入

最新のハイエンドスイッチやルーター(Cisco NexusやArista EOSなど)では、ハッシュ計算に「ハッシュ・シード(Hash Seed / Salt)」と呼ばれるランダムなオフセット値を付与する機能があります。
これにより、スイッチごとにハッシュ計算の結果を意図的にずらし、多段スイッチ構成であっても上流と下流で同じポートばかりが使われる「偏極」を物理的に防止できます。

! Arista EOSやCisco Nexusでのハッシュシード(ランダム化)設定の概念
! ※プラットフォームのASIC依存しますが、ハッシュの偏りを防ぐための重要機能です
port-channel load-balance hash-seed 54321

—

4. 検証とデバッグ:Pythonを用いたトラフィック分散のシミュレーション

インフラの本番投入前に、実際にどのようなリクエストがどの程度の偏りを生むのか、APIサーバー側(またはクライアント側)からスクリプトで負荷をかけ、スイッチのカウンターを監視することは非常に有益です。

ここでは、Pythonの requests ライブラリ(あるいは標準の urllib)を使い、多重リクエストを投げてバックエンドのLACP分散状況を検証する際の参考スクリプトを紹介します。実務では、このスクリプトを実行しながら、スイッチの show port-channel traffic コマンドを叩いてパケットカウンタの増加具合を確認します。

import concurrent.futures
import time
import urllib.request
import urllib.error

# 検証対象のエンドポイント(APIサーバー等のURL)
TARGET_URL = "http://192.168.100.10/api/v1/health"

def send_request(request_id):
    """
    HTTPリクエストを送信し、LACPのハッシュ偏りをテストするための関数。
    L4ポートの動的な枯渇や、セッションごとの偏りをシミュレートします。
    """
    try:
        # User-Agentやヘッダーを動的に変えることで、
        # アプリケーション層だけでなくリバースプロキシ層での挙動も確認可能
        req = urllib.request.Request(
            TARGET_URL,
            headers={"X-Test-Sequence": str(request_id)}
        )
        with urllib.request.urlopen(req, timeout=3.0) as response:
            return response.status
    except urllib.error.URLError as e:
        return f"Error: {e.reason}"

def load_test_simulation(total_requests=1000, max_workers=50):
    print(f"[*] LACP Hash Load Test開始: 総リクエスト数={total_requests}, 同時実行数={max_workers}")
    start_time = time.time()
    
    success_count = 0
    error_count = 0

    # ThreadPoolExecutorを用いて並列リクエストを送信(L4の送信元ポートを意図的に乱立させる)
    with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
        futures = [executor.submit(send_request, i) for i in range(total_requests)]
        
        for future in concurrent.futures.as_completed(futures):
            res = future.result()
            if res == 200:
                success_count += 1
            else:
                error_count += 1

    end_time = time.time()
    print(f"[*] テスト終了: 経過時間 = {end_time - start_time:.2f}秒")
    print(f"[*] 成功: {success_count}, 失敗/エラー: {error_count}")
    print("[!] 注意: このスクリプト実行中に、スイッチ側で 'show port-channel traffic' を確認してください。")

if __name__ == "__main__":
    # 実務での検証時は、ネットワーク管理者の許可を得て実施してください
    load_test_simulation(total_requests=500, max_workers=20)

スイッチ側での実機確認コマンド(Cisco IOS / Nexus の例)

Pythonスクリプトでトラフィックを流しつつ、以下のコマンドを連打してカウンターの増加スピードを比較します。

! ポートチャネル全体のトラフィック量と、各メンバーポートへの分散状況を確認
show port-channel traffic

! 各ポートのパケット・バイト数の増分をリアルタイムに監視
show interfaces port-channel 1 counters
show interfaces ethernet 1/1 counters
show interfaces ethernet 1/2 counters

もし ethernet 1/1 のカウンターばかりが猛烈な勢いですっ飛び、ethernet 1/2 が全く動いていないのであれば、あなたのLACPハッシュ設計にはまだ改善の余地(偏極の発生)があります。

—

まとめ

LACPのロードバランシングは、単にケーブルを束ねてスイッチの設定を投入すれば終わり、というものではありません。

  • フロー単位のハッシュ計算が行われるという宿命
  • 送信元・宛先のIPやL4ポートの組み合わせによる偏極(Polarization)の罠
  • ハッシュシードの導入や、適切な粒度(IP + L4 Port)への設定チューニングの重要性

これらを深く理解し、パケットの流れを頭の中で完全にトレースできるようになれば、どんなに複雑なマルチティア構成のネットワークであっても、帯域を美しく均等に使い切る高信頼なインフラを構築できます。

日々の運用や設計において、「なぜかここだけ帯域が太いのに詰まる」という現象に出会ったら、ぜひ今回のハッシュの仕組みと偏りの法則を思い出してください。プロトコルの深淵を愛するエンジニアの皆様の健闘を祈っています!

コメント

タイトルとURLをコピーしました