Persistent eBPF map object with bcc
Linux kernel 4.4以降には,eBPF map/programを"永続化"する仕組みが実装されている.ここではその概要を説明しつつ,iovisor/bccを使って実際にその動作を確かめてみる.
Background: eBPF objectを複数プロセスで共有したい
eBPFにmapというデータ構造があることはXDPの紹介エントリのサンプルコード内で軽く触れた.
通常,mapはそれを作成したプロセスからのみ読み書き可能である.しかし,いくつかのユースケースにおいては,mapを複数のプロセスから読み書きできると都合が良いことがある.たとえば何からの統計情報(例えばパケットカウンタ)をmapに保存してそれを外部に転送したい場合,「統計情報をmapに保存するプロセス」と「収集された統計情報を加工して外部に転送するプロセス」とに分割したほうが,実装の見通しの観点から望ましい.また,mapに何らかの設定情報(例えばIPルーティングテーブル)を保存してパケットを加工したい場合,「ルーティングテーブルを参照してパケットの加工を行うプロセス」と「ルーティングテーブルのアップデートを行うプロセス」とに分割したほうが,安定性の観点から望ましい.
Kernel 4.4以前から,mapのfile descriptorをunix domain socket経由で他プロセスに共有することで,複数プロセス間でmapを共有することは可能ではあった.
この方法は汎用的である反面,決して簡単とは言い難い.そこで登場したのが,今回紹介するeBPF objectに特化した仕組みである.
Solution: eBPF objectをBPF filesystemに"ピン付け"する
Linux kernel 4.4から,eBPF objectをファイルシステム上のパスに割り当てる (“pinning”) する仕組みが実装された.
まず,eBPF objectとファイルシステム上のパスのマッピングを行うファイルシステム (ここでは仮にbpffsと呼ぶことにする) が用意されている.このファイルシステムを任意のパス (一般に/sys/fs/bpf
) にマウントしておく.
linux/inode.c at master · torvalds/linux · GitHub
その上で,bpf(2)
syscallに追加された BPF_OBJ_PIN
コマンドを使ってpinningを行う.ファイルからeBPF objectを取得する際には同様に BPF_OBJ_GET
コマンドを使う.
簡単なサンプルが以下に用意されている:
- Pin: prototype-kernel/bpf.c at master · netoptimizer/prototype-kernel · GitHub
- Get: prototype-kernel/bpf.c at master · netoptimizer/prototype-kernel · GitHub
Example: bccを使って複数プロセス間でeBPF mapの読み書き
C言語のコードから直接syscallを使ってpinningを行う例は既にいくつかある (例1, 例2) ため,ここではbccを使ってpython経由でeBPF mapのpinningと他プロセスからの読み込みを試してみる.例として,受信したパケット数をeBPFプログラムでカウントしてeBPF mapに書き込み,それを他のプロセスから読み込む場合を考える.
Code
counter.py: eBPF mapへの書き込み & pinning
受信したパケット数をカウントしてeBPF map “packet_counter” に書き込むプログラムである.このプログラムはtc-bpf(8)
経由でネットワークインタフェースにアタッチされる.
from bcc import BPF, libbcc import pyroute2 import ctypes import time import sys code = """ #include <uapi/linux/bpf.h> #include <uapi/linux/pkt_cls.h> BPF_TABLE("array", uint32_t, long, packet_counter, 1); int counter(struct __sk_buff *ctx) { uint32_t index = 0; long *value = packet_counter.lookup(&index); if (value) *value += 1; return TC_ACT_OK; } """ def count_packet(iface, pin_path): bpf = BPF(text=code, cflags=["-w"]) func = bpf.load_func("counter", BPF.SCHED_CLS) counter = bpf.get_table("packet_counter") print("map fd: {}".format(counter.map_fd)) ret = libbcc.lib.bpf_obj_pin(counter.map_fd, ctypes.c_char_p(pin_path)) if ret != 0: raise Exception("Failed to pin map") print("Pinned at: {}".format(pin_path)) ip = pyroute2.IPRoute() ipdb = pyroute2.IPDB(nl=ip) idx = ipdb.interfaces[device].index ip.tc("add", "clsact", idx) ip.tc("add-filter", "bpf", idx, ":1", fd=func.fd, name=func.name, parent="ffff:fff2", classid=1, direct_action=True) print("Hit CTRL+C to stop") while True: try: print(counter[0].value) time.sleep(1) except KeyboardInterrupt: print("Removing filter from device") break ip.tc("del", "clsact", idx) ipdb.release() if __name__ == '__main__': iface = device = sys.argv[1] pin_path = sys.argv[2] count_packet(iface, pin_path)
実際にpinningを行っているのは下記の部分.現状,bccのpython bindingに専用のインタフェースが無いため,libbccの関数を直接呼び出している.
counter = bpf.get_table("packet_counter")
ret = libbcc.lib.bpf_obj_pin(counter.map_fd, ctypes.c_char_p(pin_path))
reader.py: pinningされたeBPF mapの読み出し
こちらのコードでmapの値を読み込む.
from bcc import libbcc, table import ctypes import sys class PinnedArray(table.Array): def __init__(self, map_path, keytype, leaftype, max_entries): map_fd = libbcc.lib.bpf_obj_get(ctypes.c_char_p(map_path)) if map_fd < 0: raise ValueError("Failed to open eBPF map") self.map_fd = map_fd self.Key = keytype self.Leaf = leaftype self.max_entries = max_entries def read_counter(map_path): counter = PinnedArray(map_path, ctypes.c_uint32, ctypes.c_long, 1) return counter[0].value if __name__ == '__main__': path = sys.argv[1] print(read_counter(path))
キモは下記の部分.下記コードでmapのファイルディスクリプタが取得できる.上記コードではbcc.table.Array
を再利用している.
map_fd = libbcc.lib.bpf_obj_get(ctypes.c_char_p(map_path))
実行例
まずはbpffsをマウント.
$ sudo mount -t bpf none /sys/fs/bpf $ mount | grep bpf none on /sys/fs/bpf type bpf (rw,relatime)
続いてcounter.py
を実行.
$ sudo python counter.py eth0 /sys/fs/bpf/counter map fd: 4 Pinned at: /sys/fs/bpf/counter Hit CTRL+C to stop 0 37 38 39 70 ...
/sys/fs/bpf/counter
が作成されていることが確認できる.
$ ls -l /sys/fs/bpf/counter -rw-------. 1 root root 0 Apr 8 14:23 /sys/fs/bpf/counter
この状態でreader.py
を実行.counter.py
側の出力とほぼ同じ値が取得できるはず.
$ sudo python reader.py /sys/fs/bpf/counter
253
なお,pinningしたobjectは生成元のプロセス(ここではcounter.py
)を終了したとしても削除されない.削除するには明示的にunlink
する必要がある.
$ ls -l /sys/fs/bpf/counter -rw-------. 1 root root 0 Apr 8 14:23 /sys/fs/bpf/counter $ sudo unlink /sys/fs/bpf/counter $ ls -l /sys/fs/bpf/counter ls: cannot access '/sys/fs/bpf/counter': No such file or directory
References
- Persistent BPF objects [LWN.net]
- Background, solutionのセクションに書いた内容がより詳しく述べられている
- 初期のパッチ(v1; 実際にマージされたのはv2)を元に書かれているため,実際の実装とは内容が一部異なる.(
BPF_PIN_FD
->BPF_OBJ_PIN
,BPF_GET_FD
->BPF_OBJ_GET
)
- bpf: add support for persistent maps/progs
- 初期実装のコミット.LWNの記事同様,背景が詳しく述べられている
- BPFの現在 // Speaker Deck
- Map pinningだけでなく,BPF全般の最近の動向が纏まっている
Maglev Hashing with Python
今更ながら,GoogleのMaglev論文で提案されているMaglev Hashingを手元で実装してみた.
Maglev: A Fast and Reliable Software Network Load Balancer
Maglev Hashingとは
所謂Consitent Hashの一種.Maglevロードバランサにおけるリアルサーバ選択に使用されている.
上記論文のSection 3.4で詳細が説明されている.NSDI'16での発表スライドも併せて眺めると分かりやすい.
Maglev: A Fast and Reliable Software Network Load Balancer | USENIX
Slide: https://www.usenix.org/sites/default/files/conference/protected-files/nsdi16_slides_eisenbud.pdf
実装
Golang実装を参考にしつつ,Pythonで実装してみた.
Example
256エントリを追加した状態で,更に1エントリの追加・削除を行っている.
テーブルサイズを65537としたとき*1,1エントリの追加・削除で影響を受ける割合が 772 / 65537 = 1.2%程度に抑えられていることが分かる.
>>> h = MaglevHash(m=65537) >>> for i in range(256): ... h.add("172.16.1.{}".format(i)) ... >>> h.populate() 65537 >>> h.add("192.168.1.1") >>> h.populate() 772 >>> h.lookup("10.0.0.1") '172.16.1.111' >>> h.remove("172.16.1.111") >>> h.populate() 700 >>> h.lookup("10.0.0.1") '172.16.1.68'
なお,Maglev Hashingの評価は論文のSection 5.3で示されている.
Kargerらの手法*2やRendezvousと比べ,各エントリに均等に分散されることが示されている.また,テーブルサイズ m
が十分に大きければ,エントリの追加・削除によって影響を受ける割合も小さくなることも示されている.ただし,m
の値を大きくすることは計算量の増大に繋がるため,考慮が必要である.
留意点
論文中では実装の詳細が割愛されている.上記実装のうち,下記の点については注意が必要である:
Linuxカーネルの新機能 XDP (eXpress Data Path) を触ってみる
先日netdev 1.2に参加してみたところ,XDP(eXpress Data Path)の話題で持ち切りといった感じだった. というわけで,XDPについて一通り調べつつ,実際に触ってみた.
XDPとは何か?
誤解を恐れずに一言で言うと,「Intel DPDKのような高速パケット処理基盤をLinuxカーネル自身が用意したもの」であると理解している.このスライドでは
A programmable, high performance, specialized application, packet processor in the Linux networking data path
と言っている.
DPDKはユーザランドアプリケーションがNICを直接叩く(=カーネルのネットワークスタックをバイパスする)ことで高速処理を実現している.一方XDPは,カーネル内の最もNICドライバに近い場所でフックしてeBPFを実行することによりパケット処理を実現する.
eBPFはカーネル内でプログラムを実行するための汎用的な仕組みで,パフォーマンス計測やセキュリティ方面で使用されている,らしい.これまでもtc-bpf(8)のようにeBPFでパケット処理を行う仕組み自体は用意されていたが,XDPによってより早い段階でパケット処理を行えるようになった.
XDPの概要についてはiovisor.orgの説明ページや冒頭で触れたスライドが詳しい.
bpf-docs/Express_Data_Path.pdf at master · iovisor/bpf-docs · GitHub
パフォーマンス
パフォーマンスは現時点において,受信したパケットを全てドロップするケースで20Mpps, L3フォワーディングでも10Mpps程度を達成するとされている.
youtu.be (6:46あたりから)
動作環境
XDPを利用するには,カーネル自体に加えてNICドライバでもサポートが必要となる. 先日リリースされた4.8でXDPサポートおよびMallanox mlx4ドライバサポートが取り込まれたので,少なくともこれ以降のカーネルが必要となる.
開発用としてe1000をサポートするパッチも出ているが,まだリリースされていない.
xdp-vagrantでXDPを手軽に試す
ここまでに書いたように,XDPが動作する環境を準備するのは若干敷居が高い.そこで便利なのがxdp-vagrant.
これを使うと,XDPサポート済みカーネル*1 + e1000サポートパッチ + BCC導入済みな環境がvagrantで簡単に手に入る.
BCC(BPF Compiler Collection)はユーザランドで動作するツール群で,XDPプログラムの読み込みやカーネル側のデータの操作を補助してくれる.Pythonバインディングが用意されているので,XDPプログラムはCで書きつつ*2ユーザランド側のマネジメント廻りをpythonで書く,ということが容易に実現できる.
ちなみに,ユーザランドとカーネルの両方をCで書く場合のサンプルコードは下記にある.
linux/xdp1_user.c at master · torvalds/linux · GitHub
linux/xdp2_kern.c at master · torvalds/linux · GitHub
vagrant upまで
Vagrantfileがlibvirt providerを前提に書かれいているので,vagrant-libvirtプラグインが利用可能な環境を事前に作っておく必要がある.更に,vagrant-restartプラグインも必要.
加えて,オリジナルのVagrantfileだと何故か ubuntu/trusty64
boxが指定されているが,このboxはlibvirt pluginに対応していない.自分でlibvirt対応boxを用意するか,他のlibvirt対応boxで代用する必要がある.ここでは s3than/trusty64
を使うことにする.
$ git clone https://github.com/iovisor/xdp-vagrant $ cd xdp-vagrant $ cat <<EOF | patch -p1 diff --git a/Vagrantfile b/Vagrantfile index 2ee7327..c4392c6 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -5,7 +5,7 @@ end Vagrant.configure('2') do |config| - config.vm.box = "ubuntu/trusty64" # Ubuntu 14.04 + config.vm.box = "s3than/trusty64" # Ubuntu 14.04 config.vm.network "private_network", ip: "192.168.50.4" # fix issues with slow dns https://www.virtualbox.org/ticket/13002 EOF $ vagrant plugin install vagrant-libvirt vagrant-reload $ vagrant up $ vagrant ssh -c "uname -a" Linux vagrant-ubuntu-trusty-64 4.7.0-07282016-torvalds+ #28 SMP Thu Jul 28 11:52:33 PDT 2016 x86_64 x86_64 x86_64 GNU/Linux
特定ポート宛パケットをドロップする例
bccに含まれるサンプルコード xdp_drop_count.py を参考に,80/tcpと8080/tcp宛てパケットを全てドロップしつつ,ドロップした総パケット数をポート毎にカウントするプログラムを書いてみた.
パケットが到着するたびに drop_80_8080
が呼ばれる.この関数でXDP_PASSを返せばパケットは通常のネットワークスタックに送られる.XDP_DROPでドロップ,XDP_TXでTX queueに直接送り込まれる.
上記のプログラムを実行した状態でホスト側からvagrant VMのeth1宛にncやcurlでパケットを送ると,ドロップカウンタがカウントアップしていくことが分かる.ドロップカウンタのインクリメントはカーネル側で行い,それをユーザランドのpythonプログラムから参照して出力している.
vagrant$ $ ls -l total 8 -rw-rw-r-- 1 vagrant vagrant 2034 Oct 10 06:10 drop_80_8080.c -rwxrwxr-x 1 vagrant vagrant 807 Oct 10 06:06 loader.py vagrant# ./drop_packet.py eth1 In file included from /virtual/main.c:4: In file included from include/linux/if_ether.h:23: In file included from include/linux/skbuff.h:34: In file included from include/linux/dma-mapping.h:6: In file included from include/linux/device.h:24: In file included from include/linux/pinctrl/devinfo.h:21: In file included from include/linux/pinctrl/consumer.h:17: In file included from include/linux/seq_file.h:10: include/linux/fs.h:2658:9: warning: comparison of unsigned enum expression < 0 is always false [-Wtautological-compare] if (id < 0 || id >= READING_MAX_ID) ~~ ^ ~ 1 warning generated. XDP loaded. 80: 1 80: 2 80: 2 8080: 1 80: 2 8080: 2 80: 2 8080: 2 80: 2 8080: 2
host$ echo | nc 192.168.50.4 80 # timeout as no response host$ echo | nc 192.168.50.4 8080 # timeout as no response host$ echo | nc 192.168.50.4 81 Ncat: Connection refused.
まとめ
Linuxカーネルに最近追加された高速パケット処理基盤XDPを少しだけ触ってみた.まだリリースされて間も無いということもあって利用可能な環境や関連資料は少ないものの,MellanoxをはじめとしたNICベンダも(eBPF hardware offload含め)サポートに積極的であるように見えるので,今後徐々に盛り上がっていくと思われる.
DPDKと比較してどうか,という部分に関しては,既存のネットワークスタックとの共存やeBPF関連ツールの流用が効く,といった点がアプリケーションを書く側にとって嬉しい点になりそう.
何れにしても,今後の展開が楽しみ.
ロードバランサのアーキテクチャいろいろ
少し前に,Facebookのロードバランサが話題になっていた.
このエントリを読んで,各種Webサービス事業者がどういったロードバランスアーキテクチャを採用しているのか気になったので調べてみた. ざっくり検索した限りだと,Microsoft, CloudFlareの事例が見つかったので,Facebookの例も併せてまとめてみた.
アーキテクチャ部分に注目してまとめたので,マネジメント方法や実装方法,ロードバランス以外の機能や最適化手法といった部分の詳細には触れないことにする.
事例1: Microsoft Azure 'Ananta'
MicrosoftのAzureで採用されている(いた?)ロードバランサのアーキテクチャは,下記の論文が詳しい.
Parveen Patel et al., Ananta: cloud scale load balancing. SIGCOMM '13 (スライド)
2013年10月に発表された論文.'Ananta' と名付けられたこのロードバランサは,論文執筆時点でbing.com等の実環境において約2年(すなわち2011年頃から)の運用実績があると述べられている.
一般的なアプライアンス型ロードバランサやLVS+keepalivedの環境が1+1構成でスケールアップが基本戦略になるのと対照的に,AnantaはN+1構成でスケールアウトが容易な点が特徴である.また実装面では,ハイパーバイザのレベルで諸々の処理を完結させることで,VM側にシステムの存在を意識させないようになっている.
Architecture
Anantaのおおまかな構成は下図の通り.図中には記載していないが,全体の動作を統括するAnanta Manager(AM)も存在する.
ロードバランスはRouter, Layer-4 LB, Host Agentの3階層で行われる*1.従来型のロードバランサがLayer 3からLayer 7までを(内部的な実装はともかく)単一の筐体でこなしていたのとは対照的である.なお上図だと各層間を同一L2で繋ぐように描いてしまったものの,当然その必要は無い.
Router
Per-flow Equal Cost Multi Path (ECMP) によるLayer 3レベルでのトラフィック分散を行う.
Routerは,次段のL4 LBからVIPの/32経路をBGPで受信している*2.このとき,コスト(Local PreferenceやMED)が同一であれば,routerはECMPでのパケットフォワーディングを行う. Per-flow ECMP*3を使用すれば,経路に変化が無い限り,すなわちL4 LBが増えたり死んだりしない限り,同一TCPセッションは常に同一のL4LBにフォワーディングされる.これにより,Routerではセッションを管理する必要が無い.
L4 LB (Multiplexer, MUX)
Consistent HashによるLayer 4レベルでのトラフィック分散を行う.論文中ではMultiplexer 'MUX' と呼ばれている.MUXは,自身が管理しているVIPの/32経路をBGPでrouterに広報する.これにより,VIP宛のトラフィックがMUXに引き込まれるようになる.
MUXはアプケーションが稼動しているVMにトラフィックを分散する.このとき単純にConsistent Hashだけでフォワード先を決定してしまうと,VMやHyperVisorの増減が発生した際に既存セッションが誤ったVMにフォワードされてしまう.これを防ぐため,MUXは既存セッションの情報をセッションテーブルで保持・管理する.ただし,この情報はMUX間では共有されていない*4ため,場合によってはセッションが失われてしまう可能性がある.ちなみに,実装はWindows Filtering Platform(WFP)を用いている.
Host Agent (HA)
IPIPのデカプセル化やNATといった処理を透過的に行い,適切なVMにトラフィックが屆くようにする役割を持つ.
VMやその上で動いているアプリケーションのヘルスチェックもここで行う.ヘルスチェックの結果はAnanta Managerに送られたのちMUXに反映される.実装はHyper-Vの仮想スイッチ*5を用いている.
トラフィックフロー
あるアプリケーションApp 2がVIP 192.0.2.2を使うとき,VIP宛の通信は下記のような流れで処理される:
- RouterにVIP 192.0.2.2宛のパケットが屆く.このとき,router上には192.0.2.2/32の等コスト経路が複数存在するので,そのうちいずれか1経路を選択してパケットをフォワードする.
- MUXにパケットが屆く.MUXは自身のセッションテーブルを参照し,セッションが既に存在するか確認する.存在すればそれに従ってフォワード先のVMを決定する.存在しなければhashによって新たにフォワード先のVMを決定し,セッションテーブルに記録する.フォワード先のVMが決定されると,パケットをIPIPでカプセル化してVM宛にフォワードする.
- HAを通過してパケットがVMに屆く.このとき,HAは透過的にIPIPのデカプセル化を行う.
- VMが応答を返す.なお,このときのパケットのソースIPアドレスはVMの実IPアドレスになっている.
- HAを通過してパケットがクライアントに送られる.このとき,HAはsource NATを行い,パケットのソースIPアドレスをVMの実IPアドレスからVIPに書き換える.また,L3DSRなので,レスポンスのパケットはMUXを通過しない.
事例2: Facebook
このエントリの冒頭で挙げたエントリでも詳解されていたもの.
Building a Billion User Load Balancer | USENIX
MicrosoftのAnantaと同様,複数レイヤ分割によってスケールアウトが容易な構成になっている.
Facebookのシステムが優れているのは,単一IDC内におけるロードバランスだけでなく,PoP間トラフィックの最適化やGSLBによるユーザトラフィックの品質向上まで含めて考えられている点である.また実装面では,Microsoftのものと違って特定の環境に依存する要素が無く,よりポータブルになっている*6.
Architecture
PoP間通信やGSLBに関する部分は置いておいて,単一IDC内だけに着目すると,おおまかな構成は下図の通り.Microsoftの例と同じく階層構造である.ロードバランシングはRouter, Layer 4 LB, Layer 7 LBの3階層で行われる.例によって階層間は同一L2である必要は無い.
Router
Per-flow Equal Cost Multi Path (ECMP) によるLayer 3レベルでのトラフィック分散を行う.Microsoftの例におけるrouterと全く同じ役割と動作を行う.
Layer 4 LB
Consistent HashによるLayer 4レベルでのトラフィック分散を行う.これもMicrosoftの例におけるMUXとほぼ同じ役割と動作を行う.実装はLVSとExaBGPを用いている.
Layer 7 LB
コネクションを終端し,アプリケーションに対するLayer 7レベルでのトラフィック分散(reverse proxy)を行う.実装はProxygenを用いている.
上で挙げた各層の要素はコンテナ化され,Kubernetesを使って任意の場所にデプロイされる. 余談だが,発表動画中では気軽に「任意の場所にデプロイ」と言って図まで描いている。だがしかし、ファブリック的ではないネットワークアーキテクチャでこれをそのまま真似すると,router-L4LB間の帯域が足りなくて辛い思いをすることになると予想される.Facebookのデータセンターは末端まで広帯域を用意していることが知られているが,こういうことをやってこそ出来る芸だと思う.
トラフィックフロー
VIP 192.0.2.1があるとしたとき,このVIP宛の通信は下記のような流れで処理される.
- RouterにVIP 192.0.2.1宛のパケットが屆く.このとき,router上には192.0.2.1/32の等コスト経路が複数存在するので,そのうちいずれか1経路を選択してパケットをフォワードする.
- L4 LBにパケットが屆く.L4 LBは自身のセッションテーブルを参照し,セッションが既に存在するか確認する.存在すればそれに従ってフォワード先のL7 LBを決定する.存在しなければhashによって新たにフォワード先のL7 LBを決定し,セッションテーブルに記録する.フォワード先のL7 LBが決定されると,パケットをIPIPでカプセル化してL7 LB宛にフォワードする.
- L7 LBにパケットが屆く.L7 LBはIPIPをデカプセル化した上でコネクションを一旦終端し,upstreamのアプリケーションに対してリバースプロキシを行う.
- アプリケーションがL7 LBにレスポンスを返す.
- L7 LBを経由してパケットがクライアントに送られる.L3DSRなので,レスポンスのパケットはL4 LBを通過しない.
事例3: CloudFlare
CDN事業者であるCloudFlareは,世界中にPoPを設置してコンテンツ配信を行っている. そんなCloudFlareのPoPにおけるロードバランス手法が下記の記事で紹介されている.
CloudFlareのアーキテクチャで特徴的なのは,ネットワークレベルで負荷分散と障害時の切り離しが完結している点である.またインターネットレベルでのIP Anycastによる耐耐障害性確保も実現している.
Architecture
おおまかな構成は下図の通り.
この構成では,ロードバランサにおける負荷分散機能をECMPで行い,分散先の追加・削除機能をBGP自身で行う.MicrosoftのMUXやFacebookのL4/L7 LBに該当するような明確なロードバランサは存在しない.
Router
ECMPによるLayer 3レベルでのトラフィック分散を行う. Routerは,後段のedge serverにインストールされたBGP speakerからの/32経路を受信している.この経路をそのままインターネット(他のAS)に広報することで,edge serverは外部からアクセス可能になる.
Edge server
CDNのキャッシュやプロキシ機能を提供する. 各Edge serverにはBGP speaker*7がインストールされており,routerに対して自身のVIPを広報する.各edge serverが同一のコストで広報を行えば,routerからECMPによって負荷が分散される. また,VIPは複数のPoPで同一のものを使用している.従って,インターネットに対するIP anycastが行われている.
冒頭でも述べた通り,構成の面白い点は障害時の動作である. たとえばEdge serverが死んだ場合,そのサーバからrouterに対する経路広報が止まるため,そのサーバは自ずと負荷分散対象から外される. また,電源障害や上流回線全断などによってPoP自体が死んだ場合,そのPoPからインターネットへの経路広報が止まるため,そのPoPへはアクセスされなくなる.クライアントはIP Anycastによって他のPoPに自動的に迂回される.
トラフィックフロー
VIP 192.0.2.3があるとしたとき,このVIP宛の通信は下記のような流れで処理される.MicrosoftやFacebookのものに比べると非常にシンプル.
- RouterにVIP 192.0.2.3宛のパケットが屆く.このとき,router上には192.0.2.3/32の等コスト経路が複数存在するので,そのうちいずれか1経路を選択してパケットをフォワードする.
- Edge serverにパケットが屆く.パケットは特にカプセル化等はされておらず,通常のものと変わらない.
- Edge serverがレスポンスを返す.
- ルーティングテーブルに従ってパケットがクライアントに送られる.
余談: Google
GoogleのロードバランスといえばGFEだと思うが,明確な資料は見当たらなかった. 見付けられた範囲だと、下記の動画で簡単に言及がある程度である.
少し前にSeesawというものが公開されていたが,これがGoogle内部ではどういう位置付けでどの程度使われているのかよく分からないので何とも言えないところ.
追記 (2016-02-29 19:25)
Googleのロードバランサに関する情報を@rrreeeyyyさんに教えていただいた:
Google の LB は Maglev って名前が付いてて最近論文が公開されてる ( https://t.co/gr16vu5FMn ) / “ロードバランサのアーキテクチャいろいろ - yunazuno.log” https://t.co/GfYs2gJLWy
— れい (Yoshikawa Ryota) (@rrreeeyyy) 2016年2月29日
論文は下記のページに掲載されている.こんどのNSDIで発表される予定.
Maglev: A Fast and Reliable Software Network Load Balancer
Abstractと図をざっくり眺めた程度だと,こちらも複数レイヤ分割構成でECMPやconsistent hashを活用している様子.あとで論文を詳しく読む予定. 情報ありがとうございました!
まとめ
Microsoft, Facebook, CloudFlareにおけるロードバランサのアーキテクチャを調べて簡単にまとめてみた. 最近のクラウド環境であればロードバランサ機能は当然用意されているが,それらの内部では(Azureに限らず)ここで挙げたような技術が活用されていると考えられる.クラウド様々である. また,ロードバランサに限らず,機能ごとのレイヤ分けやBGP+ECMPの活用といった考え方は,様々な局面で参考にできそう.
*1:実際にはHost Agentは負荷分散の機能は無い.
*2:実際には,Routerの負荷を考慮してもう少し集約された経路を使用している,と論文中では述べられている
*3:source ip, source port, destination ip, destination port等のhashによりパスを選択する方式; これに対してパケット毎にランダムにパスを決定するのがper-packet ECMP
*4:少なくとも論文執筆時点では未実装; DHTのようなアプローチで解決できるよ,と筆者らは言っている
*5:恐らくHyper-V Extensible SwitchをWFPで拡張したもの思われる
*6:もちろん,AnantaもHAを必ずしもハイパーバイザに組み込む必要は無くて,MUX-App間のどこかに同じ機能を持たせれば同様のことは実現できそうに見える.
OS X 10.11のDNS64らしき動きをするDNSサーバを作る
OS X 10.11 El Capitanのインターネット共有機能を使うと,IPv6インターネット接続性が無い環境であってもIPv6 only networkを作ることができる,とAppleは言っている.どういう仕組みなのか少し調べてみたところ,AAAA filterに似た仕組みをDNS64に組み込んでいるらしい.
具体的には,あるドメイン名に対するAAAAクエリがDNS64サーバに来た際,AレコードをNAT64形式に変換してAAAAレコードに詰め直したものを返す,という動作をするようである.通常のDNS64ではAAAAレコードが実在すればそれを返すが,こちらはAAAAレコードが実在してもそれを無視してAレコードを参照する,という違いがある.
この仕組みにより,ルータとなるMacがIPv6接続性を持っていなくても,インターネット共有機能を利用している端末に対してIPv6接続性を提供できる.
これと同様の環境をMac以外でも構築できないかと思い,まずはOS XのDNS64と同じような挙動をするDNSサーバを作ってみた.
Code
AAAAクエリが来たら問答無用でそれを握り潰してAクエリを投げ,応答をNAT64形式のアドレスに変換した上でAAAAレコードに詰めて返す. 実際の名前解決廻りの処理にはPythonのTwistedに含まれるNamesを使用している.起動部分のベースはTwistedのサンプルコードから.
A DNS64 resolver with AAAA filter-like behavior (s ...
Example
上で挙げたDNSサーバが127.0.0.1:10053で動いているとする.
$ dig @127.0.0.1 -p 10053 www.facebook.com. A +short star.c10r.facebook.com. 173.252.120.6 $ dig @127.0.0.1 -p 10053 www.facebook.com. AAAA +short star.c10r.facebook.com. 64:ff9b::adfc:7806
普通のDNSサーバの応答と比べると、実在するAAAAレコードが無視されていることが分かる.
$ dig @8.8.8.8 www.facebook.com. A +short star.c10r.facebook.com. 31.13.79.246 $ dig @8.8.8.8 www.facebook.com. AAAA +short star.c10r.facebook.com. 2a03:2880:f00d:1:face:b00c:0:1
あとはTAYGA等でNAT64を行えば,簡易的なテスト用環境として使えるレベルにはなりそう.
Norikraでそこそこ手軽にNetFlow解析
去年のJANOGで別の方が発表された内容とダダ被りではあるものの,折角なのでメモ程度に書いておく.
概要
- データセンタネットワークやバックボーンネットワークの運用をやっていると,インターネットに出入りするトラフィックの内訳 (どのISP向けのトラフィックが多いのか,どの回線にトラフィックが乗っているか,等) を見たいことがよくある
- NetFlowをNorikraで解析し,その結果をGrowthForecastに流し込むと,トラフィックの内訳をそこそこ手軽に見ることができる
- 定常的なモニタリング用途以外に,突発的なトラブルシューティングにも使えていろいろ便利
背景
自前でAS (Autonomous System) を運用している事業者では,トラフィックのコントロールの観点から,「どのAS向けのトラフィックがどのくらいあるのか」「このASとはどの回線を使って通信しているか」といった情報を知りたいケースがよくある (と思う). このような場面では,NetFlowに代表されるトラフィック統計情報を収集する機能が用いられる.NetFlowを使うと,ある機器を通過した通信フローに関する情報 (src/dst IP, src/dst ASなど) が取得できるので,これらの情報を解析すればトラフィックの内訳を知ることができる.
NetFlow解析を行う場合,一般的には次のような選択肢がある.
回線ごとのトラフィック集計を行いたいケースや複数ASを運用しているようなケースでは,柔軟性の観点から自前での構築を選びたくなる.が,一から全てやるのは結構大変なので,解析はNorikraに,結果のグラフ化はGlowthForecastにやってもらうことにする.
実現方法
構成はNorikra+Fluentd+GrowthForecast.NetFlowコレクタはfluent-plugin-netflowを使用する.
Fluentdの設定はこんな感じ.fluent-plugin-forest使ってるので、AS数や回線数によってはメモリの消費に注意.
## <- NetFlow <source> type netflow tag netflow.flow bind 0.0.0.0 port 2055 </source> ## -> Norikra <match netflow.*> type norikra norikra 127.0.0.1:26571 remove_tag_prefix netflow target_map_tag true </match> ## <- Norikra <source> type norikra norikra 127.0.0.1:26571 <fetch> method sweep tag_prefix norikra.query tag field _gf_key interval 1m </fetch> </source> ## -> GrowthForecast <match norikra.query.**> type forest subtype growthforecast remove_prefix norikra.query <template> gfapi_url http://127.0.0.1:5125/api/ graph_path netflow/as${tag_parts[0]}/${tag_parts[1]}_bps_${tag_parts[2]} name_keys traffic_bps </template> </match>
この構成でNorikraに下のようなクエリを登録した後NetFlowを流し込めば,ひとまずAS毎の集計結果がNorikraで出力されるはず (ただしこの時点では全ての回線の合計値が出力される).
select src_as, dst_as, (SUM(in_bytes * sampling_interval) * 8) / 60 as traffic_bps, from flow.win:time_batch(60 sec, 0L) group by src_as, dst_as
次は回線毎の集計.SNMP ifIndexと回線の対応関係やフローの向きのチェックを全てEPLで書くのは辛いので,簡単なUDFを作った.
norikra-udf-netflowでは,NetFlowのデータを扱い易い形式に変換するためのいくつかの関数を定義している.内容は下の表のような感じ.変換に必要な情報はインストール前にあらかじめdefinition.yamlに書いておく必要がある.
Function | Description |
---|---|
NFDirection(src_as, dst_as) | フローの向き ("in" or "out") |
NFOppositeASN(src_as, dst_as, ipv4_src_addr, ipv4_dst_addr) | 対向のAS番号 |
NFRouter(host_ipaddr) | NetFlowの生成元のルータの名称 |
NFCareer(host_ipaddr, ifindex_in, ifindex_out, flow_direction) | フローが通過した回線の名称 |
これらの関数を使って回線毎の集計をやる場合,こんな感じのクエリになる.
Query name: flow_aggregator, Group: LOOPBACK(aggregated_flow)
select NFOppositeASN(src_as, dst_as, ipv4_src_addr, ipv4_dst_addr) as opposite_as, NFCareer(host, input_snmp, output_snmp, NFDirection(src_as, dst_as)) as career, (SUM(in_bytes * sampling_interval) * 8) / 60 as _traffic_bps, NFDirection(src_as, dst_as) as flow_direction from flow.win:time_batch(60 sec, 0L) group by OppositeASN(src_as, dst_as, ipv4_src_addr, ipv4_dst_addr), NWPoPCareer(host,(code) input_snmp, output_snmp, FlowDirection(src_as, dst_as)), FlowDirection(src_as, dst_as)
Query name: traffic_as_per_career
select opposite_as, career, case LAST(_traffic_bps) when null then 0 else LAST(_traffic_bps) end as traffic_bps, (opposite_as || "." || career || "." || flow_direction) as _gf_key from aggregated_flow.win:time_batch(60 sec, 0L) group by opposite_as, career, (opposite_as || "." || career || "." || flow_direction)
一度LOOPBACKで集計済みデータを別target(上の例だとaggregated_flow)に流しているのは,後で突発的に解析用のクエリを投入する時にデータを再利用しやすくするため.NorikraのクエリでFluentdのタグを作ってる点がちょっとアレ. ここまで全て上手く動いていれば,GrowthForecastでグラフが作られているはず.
これまでの運用状況など
Norikra
去年末に投入後,約3ヶ月稼働中.単純な時間平均で 10000 event/sec (=10000 flow/sec) 弱程度のeventを流し込んでいる.JVMが数回突然死したり,CPUコアが多い環境で起動しないトラブルに遭遇した*1以外は概ね安定している.現在heapに30GB弱割り当てていて,GCのタイミングで結構な時間止まっているので何とかしたい.
Flow collector
fluent-plugin-netflowが手元の環境だと1プロセスあたり8000 flow/sec 近辺でsocket bufferが埋まる速度に追い付けなくなった.順当に行けばfluent-plugin-multiprosessでポートを分けて負荷を分散させる場面だと思われる.が,これをやるとルータによってNetFlowの送り先が変わってしまうので後々大変そう,ということになり*2,nfcollectという超簡易的なNetFlow collectorを書いて凌いだ.
nfcollectはフローの到着順を保持しない代わりにそこそこのスループットが出るようになっていて,flow-genで試したところ同じ環境で30000 flow/secぐらいまでは何事も無く捌いてくれた.こちらを使った場合でもある時点でfluent-plugin-netflowと同じ問題が発生することに変わりは無いものの,現状30000 flow/secを越えることをはまず無さそうということで一旦目を瞑っている.
その他諸々
当初は単純な集計用途だけを想定していたが,使い始めてから暫くすると,トラブルシューティング的な用途にも使えそうなことが分かった.例えば本来存在するはずの通信が見えない時や,特定ISPから特定アドレス帯への通信が上手くいかないといった時に,その問題に合わせたクエリをその場で書いてNorikraに投入してしばらく待てば,パケットキャプチャより手軽に調査ができる*3.ちょっとしたクエリのサンプル集のようなものを事前に書いておけば,大抵の人は問題無く扱えると思う.
まとめ
NetFlow解析,割と難しいもの扱いされているという噂を耳にしますが,やってみると色々便利なのでまずは試してみると良いと思います.
モダンなcipher suiteに対応していないHTTPSクライアントを搭載したロードバランサが存在する問題について
実は一般的に知られている問題なのかもしれないものの,最近知ったのでメモ.
概要
- 一部のロードバランサアプライアンスにおいて,搭載されているHTTPSクライアントが所謂"モダンな"暗号スイートに対応していないものが存在する
- このため,特定条件下において意図せずヘルスチェックに失敗し,場合によってはVIPがdownする
- 現時点では直ぐに影響が出る訳では無いものの,気に留めておかないと近い将来痛い目を見そう
背景
DSR構成のロードバランサでSSL(TLS)を使用する場合,SSLの終端はロードバランサではなくリアルサーバの仕事になる*1.
この環境下においてリアルサーバの死活監視にLayer 7方式を使用するパターンを考える.このパターンでは,ロードバランサからリアルサーバにSSLで接続し,HTTP GETないしHEADリクエストを投げて意図したレスポンスが返ってくれば,リアルサーバは正常動作しているとみなされる*2.
問題となるケース
リアルサーバ上で稼働しているWebサーバ(Apache, Nginx等)において,セキュリティの要件上Mozillaが言うところの所謂"モダンな"cipher suiteのみ使用可能にする (=RC4や3DESに依存したCipher suiteを使用不可にする) ケースを考える.
この時,ロードバランサがヘルスチェックに使用するHTTPS Clientが先に述べたモダンなcipher suiteに非対応だと,リアルサーバとのHandshakeに失敗してしまう.その結果,実際は正常動作しているリアルサーバが死んでいると誤認識されてしまい,意図せずリアルサーバないしVIPがdownする.
どう対応するか
ぱっと思い付く対応策はこんな感じ:
- Layer 7ヘルスチェックを諦めてLayer 4ないしLayer 3でのヘルスチェックに切り替える
- 要件を曲げてCipher suiteの制約を緩める
- ロードバランサ側をモダンなCipher suiteに対応させる
- ベンダに機能追加してもらう,対応OSにバージョンアップする,対応LBに入れ替える,など
商用サービスでモダンもの限定にするケースはまだレアだとは思うものの,気に留めておく必要がありそう.特に,現在使用しているCipher suiteがある時点から安全でなくなる可能性を考慮すると,上述したケースでどういう対応を取るか,ぐらいは考えておいたほうが良さそう.
また,今回のHTTPS Client側cipher suiteの問題に限らず,ロードバランサでSSLを終端した場合のCipher suiteやTLSバージョンの対応状況等,ロードバランサ+SSLには諸々のハマりどころが存在するようなので,運用している人は頑張りましょう.