Trema/PbrLbSwitch
はじめに
を見ていてふと、「あ、これ Simple Router ちょっといじったら作れそうだなあ」と思ったのでやってみた。(性能的な話とかは放置して)
[追記 2013-02-17]
変なところがあったので追記してます→Trema/PbrLbSwitch(2)
環境構成
いつもの KVM 環境で。
- 2セグメント作ります。
- 192.168.11.99 へのリクエストを、ofvm03-05 (10.0.2.3-5) に振り分けます。
- サーバからクライエントには直接通信させます(DSR)
- 振り分けのアルゴリズムは、クライアントの Source Port を使います。
- さくらインターネットの資料みたいに、ポートレンジで分けようかと思ったけど、実装が簡単なので剰余演算にしました。(後述)
基礎設定
DSRとサーバのインタフェース設定
DSR(Direct Server Return)について。
リプライをサーバから直接返すので、サーバに Virtual IP (Flowting IP)と同じ Loopback Interface を設定しておきます。…というのを忘れていてしばらく悩んだりしたので気をつけましょう。
Loopbackの設定はこんな感じで。既存の loopback インタフェースにアドレス追加する形にしてあります。
stereocat@ofvm03:~$ cat /etc/network/interfaces # The loopback network interface auto lo lo:1 iface lo inet loopback iface lo:1 inet static address 192.168.11.99 netmask 255.255.255.255 # The primary network interface auto eth0 #iface eth0 inet dhcp iface eth0 inet static address 10.0.2.3 netmask 255.255.255.0 gateway 10.0.2.254 stereocat@ofvm03:~$ stereocat@ofvm03:~$ ip addr list lo 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 16436 qdisc noqueue state UNKNOWN link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo inet 192.168.11.99/32 scope global lo:1 inet6 ::1/128 scope host valid_lft forever preferred_lft forever stereocat@ofvm03:~$
Webサーバ コンテンツ
ofvm03-05 には apache2 を入れてデフォルトのコンフィグで放置してある状態です。中身に、サーバごとにこういうページを設置しておきます。
<html> <body> <h1>It works! This is ofvm03</h1> <script language="Javascript"> <!-- document.write("<ul>") document.write("<li>protocol = " + window.location.protocol); document.write("<li>host = " + window.location.host); document.write("<li>hostname = " + window.location.hostname); document.write("<li>port = " + window.location.port); document.write("<li>pathname = " + window.location.pathname); document.write("<li>search = " + window.location.search); document.write("<li>hash = " + window.location.hash); document.write("<li>cookie = " + document.cookie); document.write("</ul>") --> </script> </body> </html>
まあ定番ということで。javascript でクライアント側の情報を表示できるようにしておきます。今回は特にこの辺意味があるようなテストをやらないですが、実際の(実機の) LB を使う場合、とくに cookie 周りの処理や URL の書き換えとか L7 周りの処理をやる場合にはこういうのを入れておいてチェックしたりします。……まあ DSR じゃなくて NAT 方式の実装とかやるとこういうの合った方が便利なので。
実装
コードは github においてあります。
以下主要な変更点とか。
設定ファイル
- pbrlbswitch.conf.yml (一部のみ抜粋)
:interface: - :hwaddr: "00:00:00:01:00:01" :ipaddr: "192.168.11.3" :masklen: 24 :segment: "network1" - :hwaddr: "00:00:00:01:00:99" :ipaddr: "192.168.11.99" :masklen: 24 :segment: "network1" :vserver: true - :hwaddr: "00:00:00:02:00:01" :ipaddr: "10.0.2.254" :masklen: 24 :segment: "network2" :pbrlb: :vserver: :ipaddr: "192.168.11.99" :tcp_port: 80 :rservers: - :ipaddr: "10.0.2.3" :tcp_port: 80 - :ipaddr: "10.0.2.4" :tcp_port: 80 - :ipaddr: "10.0.2.5" :tcp_port: 80 :dsr: true
- VIP は Controller 側が持つ論理的な Interface のひとつ、という位置づけにしました。こうすることで、Simple Router のコードをほとんど変えなくても、自動的に VIP に対する ARP の解決とか ICMP の処理とかが流用できます。
- "vserver" というフラグをつけてありますが、これは interface の検索の際に、Gateway として動作するインタフェースより vserver のインタフェースが優先されないように印をつけておいた、というくらいの物です。(通常のルーティング処理で vserver として使うインタフェースの情報が使われてしまうのを避けたい。)
- pbrlb のところで分散処理のためのパラメタ設定をしています。
コード(pbrlbswitch.rb)
def handle_ipv4(dpid, message) info "[PbrLbSwitch::handle_ipv4]" if should_pbrlb?(message) handle_pbrlb_request dpid, message elsif should_forward?(message) forward dpid, message elsif message.icmpv4_echo_request? handle_icmpv4_echo_request dpid, message else # noop. end end
Controller に渡ったパケットは、種類別に処理を振り分けていきます。バランシング対象のパケットかどうか(should_pbrlb?
)を判定し、そうであればパケットの加工と転送処理(handle_pbrlb_request
)を実行します。
def should_pbrlb?(message) vsvr = @lb_table.vserver if message.ipv4_daddr.to_a == vsvr.ipaddr.to_a && message.tcp? && message.tcp_dst_port == vsvr.tcp_port return true end end def handle_pbrlb_request(dpid, message) rserver = @lb_table.balance_rserver(message.tcp_src_port) arp_entry = @arp_table.lookup_by_ipaddr(rserver.ipaddr) interface = @interfaces.find_by_prefix(rserver.ipaddr) if not interface info "handle_pbrlb_request: not found interface for #{ rserver.ipaddr }" return end if arp_entry actions = [ SetEthSrcAddr.new(interface.hwaddr.to_s), SetEthDstAddr.new(arp_entry.hwaddr.to_s), # SetTransportDstPort.new(rserver.tcp_port), SendOutPort.new(arp_entry.port) ] flow_mod dpid, message, actions packet_out dpid, message.data, actions else rsvr = rserver.ipaddr handle_unresolved_packet dpid, message, interface, rsvr end end
コード(pbrlb-table.rb)
def balance_rserver(tcp_src_port) @rservers[tcp_src_port % @rservers.length] end
基本的にはLB用のデータを保持するクラスです。振り分けについては単純にクライアントの送信元ポート番号の剰余を取って、バックエンドのサーバに振り分けると言うだけの処理を行っています。
もちろん、tcp/1-20000は ofvm1...みたいにポートのレンジごとにサーバを振り分けても良いですし、設定ファイルに Weight 情報を持たせて比率を分けるようにしても良いでしょうし、振り分けのアルゴリズムは適宜実装できます。今回はぱっと考え得る範囲で一番単純な実装方法のアルゴリズムにしました。
テスト
正直、そんなにちゃんとテストをしていない……というより、必要最小限のテストしかしていないので、上手く動かないケースがあるかもしれない。まあ、すごく簡単な LB 動作の当たりをつける、くらいのモノなので…。
フロー情報
root@oftest01:~# sudo ovs-ofctl dump-flows br0 | grep 56085 cookie=0x3, duration=1372.846s, table=0, n_packets=5, n_bytes=1340, priority=65535,tcp,in_port=4,vlan_tci=0x0000,dl_src=54:52:00:00:00:03,dl_dst=00:00:00:02:00:01,nw_src=192.168.11.99,nw_dst=192.168.11.1,nw_tos=0,tp_src=80,tp_dst=56085 actions=mod_dl_src:00:00:00:01:00:01,mod_dl_dst:00:50:56:c0:00:01,output:1 cookie=0x2, duration=1373.854s, table=0, n_packets=6, n_bytes=955, priority=65535,tcp,in_port=1,vlan_tci=0x0000,dl_src=00:50:56:c0:00:01,dl_dst=00:00:00:01:00:99,nw_src=192.168.11.1,nw_dst=192.168.11.99,nw_tos=0,tp_src=56085,tp_dst=80 actions=mod_dl_src:00:00:00:02:00:01,mod_dl_dst:54:52:00:00:00:03,output:4 root@oftest01:~#
単純な静的ファイルの Get しかしていないので、時々ブラウザ上でリロードしてやると tcp session が新しいのに変わって(クライアント側の Src Port が変わって)、アクセス先のサーバが変わっていきます。
課題
- 機能拡張
- NAT 方式の実装 : 一応セグメント分けて作ってあるので環境的にはあとコントローラ側でサーバ→クライアントへのパケットの IP/port の書き換えができればいい。
- ついでにやるなら、バックエンド側(rservers)でサービスのポート番号変えられるようにしておくとか、Weight 設定してやるとか、バランシングの方式をもうちょっと複数設定してやるとか、OpenFlow を使ってやれる範囲ってだけでもかなりたくさんある。
- いまのところ、バックエンドサーバの不具合については全くチェックしていないので、Health Check 系は本当はあった方がいいんだろうけどなー
- テスト
*1:vserver = virtual server, rserver = real server