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:~$

ref. [ubuntu] Multiple loopback interfaces - Ubuntu Forums

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 のところで分散処理のためのパラメタ設定をしています。
    • VIP: 192.168.11.99 (tcp/80) へのリクエストを、バックエンドのサーバ (rservers, ofvm03-05) に分散します。*1
    • DSR じゃなくて NAT 方式への拡張とかも頭の片隅に入れつつ、ポート番号の情報とかも設定していますが、今のところ使っていません。
コード(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
  • VIP宛て、tcp/80 のパケットを LB 対象としています。
  • パケットの送信元ポートを元に、振り分け先のサーバを求めます。(@lb_table.balance_rserver(message.tcp_src_port))
  • DSR のバランシング処理では、基本的に MAC アドレスを付け替えるだけでL3以上の情報はいじりません。
    • なので、サーバ側で VIP(Dst IP) を受け取って処理できるようにインタフェース(Loopback)の設定が必要です。
コード(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 系は本当はあった方がいいんだろうけどなー
  • テスト
    • そもそものテスト環境っていう方向で。LB動作とか、チェック方法ってもうちょっと何か上手いことやれるものがないんだろうか。ここで、IXIA とかみたいなパケットジェネレータがあればいろいろテストもできるんだけど、OSSでそういうのがやれるモノがあるといいんだけどなあ。手動でぽちぽちリロードしてやる程度でしか今は見ていないです。
    • Jmeter 使えって話か。うーん、L3/L4レベルまででいいのでもうちょっとシンプルなツールとか欲しい。

*1:vserver = virtual server, rserver = real server