経路集約とECMP
集約で含まれる範囲と、フローごとの経路選択を確認します。
説明
経路集約は複数の範囲をまとめて広告する方法です。ECMPは同じコストの複数の次ホップを利用する方法で、どちらも経路を扱いますが目的は異なります。
前半では/24をまとめた範囲と、その内部で宛先が欠けた場合を調べます。後半ではフローの識別情報をハッシュして次ホップを選び、同じ通信の維持、通信ごとの偏り、次ホップの減少による変化を確かめます。
1連続する二つの範囲をまとめる
10.20.0.0/24と10.20.1.0/24は隣接し、/23の境界にも一致しています。
import ipaddressネットワーク範囲を扱う; parts=集約前の二範囲[ipaddress.ip_network("10.20.0.0/24"),ipaddress.ip_network("10.20.1.0/24")]; aggregate=余分な範囲を含めず集約list(ipaddress.collapse_addresses(parts)); aggregate集約結果10.20.0.0/23にまとまり、範囲は10.20.0.0から10.20.1.255です。二つの/24以外を含めずに一つの広告へまとめられます。
2隣り合っていても一つにできない場合
10.20.1.0/24と10.20.2.0/24を同じ方法で処理します。
list(ipaddress.collapse_addresses境界をまたぐ二範囲を集約([ipaddress.ip_network("10.20.1.0/24"),ipaddress.ip_network("10.20.2.0/24")]))二つの/24のままです。一つの/23の境界をまたいでいるため、余分な範囲を含めずまとめることができません。必要な最小の一つの範囲は/22ですが、.0/24と.3/24も含んでしまいます。
3集約内で一部の経路がなくなった場合
10.20.1.0/24を失った後も外部へ/23を広告していると仮定します。
remaining=内部に残った範囲[parts[0]]; target=失った側の宛先ipaddress.ip_address("10.20.1.20"); (target in広告範囲と内部範囲を比較 aggregate[0],any(target in n for n in remaining))広告した/23には含まれるのに、内部の残った経路では届きません。集約点に破棄経路を置く設計では、この穴の宛先を破棄してdefaultへの送り返しによる循環を防ぎます。集約だけで内部の到達性が保証されるわけではありません。
4フローから次ホップを決める
ここからは二つの同コスト次ホップへ振り分けます。識別子はプロトコル・送受信IP・送受信ポートで、モデルのハッシュはSHA-256です。
import hashlib決定的なハッシュを使用; hops=次ホップ二つ["R1","R2"]; flows=8接続の識別情報[("tcp","192.0.2.10",p,"198.51.100.20",443) for p in range(50000,50008)]同じ端末とサーバーでも送信元ポートの違う8接続を用意しました。実機のハッシュ関数や採用する欄は製品と設定によります。ここでは同じ入力なら同じ経路になる仕組みを確認します。
5ハッシュ値を経路数で割る
ハッシュの先頭4バイトを整数にし、経路数で割った余りを使います。
def select(flow,paths):フローと経路集合
key=識別情報から整数を求めるint.from_bytes(hashlib.sha256(repr(flow).encode()).digest()[:4],"big")
return paths余りで次ホップを選ぶ[key%len(paths)]これはmoduloによる小さなモデルです。同じフローと同じ経路集合なら結果が一定になり、パケットごとに交互送信する方式とは異なります。経路集合が変わったときの挙動は次に確認します。
6同じ接続と異なる接続を比較する
一つのフローを3回検索し、その後8接続すべてを検索します。
before=各接続の経路[select(f,hops) for f in flows]; ([select同じフローの再検索(flows[0],hops) for _ in range(3)],list(zipポートと割当ての一覧([f[2] for f in flows],before)))一つの接続は同じ次ホップを使います。別の接続は異なる経路へ割り当てられる場合がありますが、少数接続で半分ずつになる保証はありません。一つの大きな転送が二回線の合計帯域を必ず使えるわけでもありません。
7フロー数とバイト数の偏り
先頭の接続だけ10000バイト、残りを100バイトとしたときの経路別転送量を計算します。
sizes=接続ごとの転送量[10000]+[100]*7; {h:sum経路別のバイト合計(n for route,n in zip(before,sizes) if route==h) for h in hops}フローの件数が近くても、大きい接続がある側のバイト数が多くなります。負荷分散の確認には、件数だけでなく転送量や回線容量も必要です。
8次ホップが一つ失われた場合
R1が利用できなくなり、R2だけで選び直します。
after=残った次ホップで再計算[select(f,["R2"]) for f in flows]; [(f[2]経路が変わった接続,old,new) for f,old,new in zip(flows,before,after) if old!=new]R1を使っていた接続がR2へ変わります。経路変更で遅延やパケット順序が変わる場合があります。実機には経路集合変更時の移動を減らす方式もあるため、このmoduloモデルの結果をすべての装置に当てはめないようにします。
まとめ
集約は境界と包含範囲を確認し、内部の穴を扱う経路も設計します。
ECMPのフロー固定、負荷の偏り、経路集合変更の影響は別々に確認します。
操作履歴と解説を振り返れます。
Python 3
import ipaddress; parts=[ipaddress.ip_network("10.20.0.0/24"),ipaddress.ip_network("10.20.1.0/24")]; aggregate=list(ipaddress.collapse_addresses(parts)); aggregate[IPv4Network('10.20.0.0/23')]list(ipaddress.collapse_addresses([ipaddress.ip_network("10.20.1.0/24"),ipaddress.ip_network("10.20.2.0/24")]))[IPv4Network('10.20.1.0/24'), IPv4Network('10.20.2.0/24')]remaining=[parts[0]]; target=ipaddress.ip_address("10.20.1.20"); (target in aggregate[0],any(target in n for n in remaining))(True, False)
import hashlib; hops=["R1","R2"]; flows=[("tcp","192.0.2.10",p,"198.51.100.20",443) for p in range(50000,50008)]def select(flow,paths):
key=int.from_bytes(hashlib.sha256(repr(flow).encode()).digest()[:4],"big")
return paths[key%len(paths)]before=[select(f,hops) for f in flows]; ([select(flows[0],hops) for _ in range(3)],list(zip([f[2] for f in flows],before)))(['R2', 'R2', 'R2'], [(50000, 'R2'), (50001, 'R1'), (50002, 'R1'), (50003, 'R2'), (50004, 'R1'), (50005, 'R2'), (50006, 'R1'), (50007, 'R1')])
sizes=[10000]+[100]*7; {h:sum(n for route,n in zip(before,sizes) if route==h) for h in hops}{'R1': 500, 'R2': 10200}after=[select(f,["R2"]) for f in flows]; [(f[2],old,new) for f,old,new in zip(flows,before,after) if old!=new][(50001, 'R1', 'R2'), (50002, 'R1', 'R2'), (50004, 'R1', 'R2'), (50006, 'R1', 'R2'), (50007, 'R1', 'R2')]
演習完了