柔軟な IT インフラとそれを支える技術
こんにちは、インフラストラクチャ本部の大山裕泰です。最近話題の WhiteBox スイッチと、そのミドルウェアについてお話したいと思います。
柔軟な IT インフラを目指して
我々に限らず IT インフラに携わるエンジニアにとって、アプリケーションレイヤの人たちが望むアプリケーション実行環境の構築・運用は主要な目標の一つかと思います。
アプリケーション実行環境の形態は様々ですが、突き詰めてゆくと物理的なネットワーク機器に Ethernet ケーブルで接続された物理サーバの集合になります。やや管理的な話になりますが、こうした物理機器は減価償却してゆくので、これが終了するあるいはサポートが切れるまでの 4~5 年の期間、同じ機器を使い続けることになりますので、機器の選定においては長い目で見る必要があります。
これに対して、アプリケーションやアプリケーションを取り巻く環境は日々目まぐるしい速度で変化してゆきます。数年前まで考えが及ばなかった課題や、これまで想定しなかったニーズへの対応などがその都度求められます。
こうしたケースにおいて、ベンダーに相談を持ちかければ、喜んで話を聞いてくれ数年後に、ベンダーオリジナルの所望の機能が実装された機器が提供されるかもしれません。あるいは、サポート料と引きかえに独自対応をしてくれることでしょう。
このようにお金が無尽蔵にあり、且つ特定のベンダーと自社のインフラの運命を共にする決心をすれば、日々発生する運用課題の解決や、新たな要求への対応も可能かもしれませんが、現実的にこの選択肢は賢明とは言えません。
なので我々インフラエンジニアは、なるべく自力で様々なニーズや課題に対応できる柔軟な IT インフラを作っていかなければいけません。
柔軟なインフラとは
柔軟なインフラについて考える前に、柔軟「でない」インフラとは何でしょうか。柔軟でないインフラは "特定の機器・用途でしかアプリケーションを実行できない環境" と仮定すると、柔軟なインフラとは "汎用の機器で様々な用途でアプリケーションを動かせる環境" と言えると思います。
DC をサーバとネットワークで分けた際、サーバは IBM 互換の IA サーバであれば、どのベンダーのどの機種を入れても統一的に扱えます。また機器の選定は、おおよそハードウェアスペックの良し悪しで判断します。
対してネットワーク機器はというと、機器にベンダーオリジナルのネットワーク OS 及びベンダーオリジナルのプロトコルと実装が組み込まれてる為、凝った機能を使おうとすると、特定のベンダーの特定の機種に揃えなければなりません。また機器の選定は、ハードウェアスペックに加えて、どの機能が使えてどれが使えないのかをよくよく調べる必要があります。
なので今日では、柔軟なネットワーク環境作りがより柔軟なインフラ作りにつながります。
柔軟なインフラの例
柔軟なインフラのモデルケースとして Microsoft や Facebook が有名です。
DC 内ネットワークの運用は、通常 STP やベンダーオリジナルの冗長化プロトコルなど L2 ネットワーク技術で行うところ、Microsoft は BGP を使った L3 の技術でこれを構築・運用してしまいました。これの凄いところは、ベンダーオリジナルな機能は使わずに全てオープンで標準化された技術を使うことで、ネットワーク機器をコモディティハードウェアで構成している事です。
Facebook もまた、マルチベンダーで柔軟なインフラ構築を目指しています。更に Facebook は、スケーラブルで効率的な DC 環境の構築を提唱 し、ハードウェア設計の統一・共通化を図ろうとしています。
WhiteBox スイッチ
こうした背景から、WhiteBox スイッチと呼ばれるベンダー謹製のネットワーク OS を持たないベアメタルスイッチの活用が注目されています。
WhiteBox スイッチは、ベンダーオリジナルのネットワーク OS の代わりに GNU/Linux ベースの OS を入れ、ユーザ側がスイッチに対して様々な機能を実装して使えるのが一つの特徴です。
また、オープンな技術を組み合わせる事で柔軟に利用できるだけでなく、ベンダー製と同じハードウェアスペックの機器を安価に手に入れることもできます。
こうした WhiteBox スイッチを手がけるプレイヤーとして、チップメーカー(Mellanox, Broadcom など)、ホワイトボックスベンダー(Quanta, ACCESS, NCLC など)、そしてソフトウェアベンダー(Cumulus Networks, Big Switch Networks) などがおり、とても活発な動きを見せています。
今回は OCP のネットワーキングショップなどにも参加する Cumulus Networks などで利用されているミドルウェア "Quagga" について、使い方から内部実装まで見てゆきます。
Quagga とは?
Quagga は RIP, OSPF, BGP などのルーティングプロトコルを実装したマルチプロトコルなルーティングソフトウェアで、cisco ライクな CLI 環境が特徴です。他にマルチプロトコルをサポートするルーティングソフトウェアとして BIRD や xorp などがありあます。
また Quagga は、WhiteBox スイッチの GNU/Linux のディストリビューション "Cumulus Linux" において利用されている他、Quagga のコアコンポーネントが ACCESS が出す WhiteBox スイッチ "AEROZ" のミドルウェア ZebOS においても使われています。
Quagga のアーキテクチャ
Quagga は一つのコアデーモン (zebrad) と、複数の zebra クライアントデーモンから成ります。クライアントデーモンは、各 Quagga が動くマシン同士で RIP や OSPF, BGP など、それぞれの通信プロトコルをお喋りするデーモンで、RIP ならば "ripd", OSPF ならば "ospfd" という形で存在しています。またコアデーモンの zebrad は、各クライアントデーモンからの要求に応じて、カーネルのルーティングテーブルの操作を実施します。
それぞれの仕組みや処理の流れについては、後述の "zebra の実装" で見てゆきます。まずは、簡単に Quagga の使い方についてお話します。
Quagga の使い方
概ね Quagga がインストールされた環境それぞれにおいて仕様する通信プロトコルのクライアントデーモンと zebra の設定ファイルを用意してやり、それぞれのデーモンプロセスを起動させるだけで、各ネットワークセグメントに接続するノード間で通信が行えます。
以下の図のネットワーク環境の quagga01 と quagga02 でそれぞれ Quagga を動かしてみます。ここでは、zebrad と ripd クライアントデーモンを起動させ、172.16.10.0/24 と 172.16.20.0/24 のネットワーク間でのルーティングの様子を見てみます。
まずそれぞれのルータマシンにおける zebrad と ripd の設定ファイルを見てみます。ここでは hostname を除き、両マシン設定内容は同じになります。
1 2 3 4 5 6 7 |
ohyama@quagga01:~$ sudo cat /usr/local/etc/zebra.conf | grep -v "^\!" | uniq hostname quagga01 password zebra log file /var/log/quagga/zebra.log log syslog ohyama@quagga01:~$ |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
ohyama@quagga01:~$ sudo cat /usr/local/etc/ripd.conf | grep -v "^\!" | uniq hostname quagga01 password zebra log file /var/log/quagga/ripd.log log stdout log syslog router rip version 2 network 172.16.10.0/24 network 172.16.20.0/24 network 192.168.0.0/16 line vty ohyama@quagga01:~$ |
zebra.conf では、ホスト名と zebra の仮想端末 (vtysh) へのログインパスワードとログ出力の設定だけしています。
ripd.conf では、それぞれ 172.16.10.0/24, 172.16.20.0/24 そして 192.168.0.0/16 のネットワークを有効に設定しています。そして、それぞれの各ノードのインターフェイスに属さないネットワークの情報を rip2 プロトコルによって、どのインターフェイスの先にどれだけ離れて存在するかを調べます。
続いて両ルータマシンで zebra を起動させます。ポート 2601 に対する telnet で zebra の仮想端末 (vty) 環境にログインできます。ここでそれぞれのマシンにおけるルーティングテーブルを見てみます。
どちらも、ルーティングマシンに直接接続しているネットワークのルーティングしか持っていません。ここでそれぞれのマシンで ripd を起動させると、ルーティングテーブルは以下のようになります。
データプレーンを通して RIP エントリが交換され、ルーティングテーブルに 172.16.10.0/24 と 172.16.20.0/24 が追加されました。この状態でそれぞれのネットワークに接続する host1, host2 間で通信できるようになります。
zebra の実装
続いて Quagga のコアプロセス zebra の中身を見ていきます。この辺りの動作を把握することで、zebra のクライアントデーモンを自作する際の理解の助けになると思います。
zebra は外部プロセスに対して二つのインターフェイスを提供しています。
一つは人間がインタラクティブに設定するために提供されているコマンドラインインターフェイス (CLI) 環境で、先ほど使って見せたものになります。デフォルトで 2601 ポートに対して telnet 接続を行うと利用できます。この CLI 環境は、vtysh と呼んでいる仮想端末によって実現されており、各クライアントデーモンも同じ実装のインタラクティブなヒューマンインターフェイスを提供しています。もちろん、各仮想端末環境が提供するコマンドは、zebra や各クライアントデーモンにおいてそれぞれ異なります。zebra で定義しているコマンドは、zebra_init() [ zebra/zserv.c ] で定義しています。ここで "node" と言っているものは、Cisco IOS のコマンドモード (*1) を模した実装したものになります。install_node() 関数でコマンドモードを生成し、install_element() 関数によって既に生成されているコマンドモードに対してコマンドを追加します。尚、すべての環境で共通に用意されているコマンドがあり、この実装は cmd_init() 関数 [lib/command.c] で宣言されており、各デーモンの初期化時にこれらが呼ばれます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
/* Initialisation of zebra and installation of commands. */ void zebra_init (void) { /* Client list init. */ zebrad.client_list = list_new (); /* Install configuration write function. */ install_node (&table_node, config_write_table); install_node (&forwarding_node, config_write_forwarding); install_element (VIEW_NODE, &show_ip_forwarding_cmd); install_element (ENABLE_NODE, &show_ip_forwarding_cmd); install_element (CONFIG_NODE, &ip_forwarding_cmd); install_element (CONFIG_NODE, &no_ip_forwarding_cmd); install_element (ENABLE_NODE, &show_zebra_client_cmd); ... } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
/* Initialize command interface. Install basic nodes and commands. */ void cmd_init (int terminal) { ... /* Install top nodes. */ install_node (&view_node, NULL); install_node (&enable_node, NULL); install_node (&auth_node, NULL); install_node (&auth_enable_node, NULL); install_node (&restricted_node, NULL); install_node (&config_node, config_write_host); /* Each node's basic commands. */ install_element (VIEW_NODE, &show_version_cmd); if (terminal) { install_element (VIEW_NODE, &config_list_cmd); install_element (VIEW_NODE, &config_exit_cmd); install_element (VIEW_NODE, &config_quit_cmd); install_element (VIEW_NODE, &config_help_cmd); install_element (VIEW_NODE, &config_enable_cmd); install_element (VIEW_NODE, &config_terminal_length_cmd); install_element (VIEW_NODE, &config_terminal_no_length_cmd); install_element (VIEW_NODE, &show_logging_cmd); install_element (VIEW_NODE, &echo_cmd); ... } ... } |
(*1) cisco IOS (v12.2) のコマンドモード : http://www.cisco.com/c/en/us/td/docs/ios/12_2/configfun/configuration/guide/ffun_c/fcf019.html
もう一つのインターフェイスは、各クライアントデーモンが zebra と通信するためのもので、tcp ポートの 2600 番を使用します。各クライアントデーモンはそれぞれの通信プロトコルによって設定する経路情報をこれを通して zebra に設定させます。また各クライアントデーモンから zebra に対して設定を送る処理は、zebra client (zclient) によって更に抽象化されており、各クライアントデーモンは zclient が提供する API を叩くことで行えます。現時点で zclient が各クライアントデーモンに対して提供する API は、以下の 11 個になります。
1 2 3 4 5 6 7 8 9 10 11 |
int (*router_id_update) (int, struct zclient *, uint16_t); int (*interface_add) (int, struct zclient *, uint16_t); int (*interface_delete) (int, struct zclient *, uint16_t); int (*interface_up) (int, struct zclient *, uint16_t); int (*interface_down) (int, struct zclient *, uint16_t); int (*interface_address_add) (int, struct zclient *, uint16_t); int (*interface_address_delete) (int, struct zclient *, uint16_t); int (*ipv4_route_add) (int, struct zclient *, uint16_t); int (*ipv4_route_delete) (int, struct zclient *, uint16_t); int (*ipv6_route_add) (int, struct zclient *, uint16_t); int (*ipv6_route_delete) (int, struct zclient *, uint16_t); |
最後に、各クライアントデーモンから zebra に送られる経路設定などの要求をどのように処理するかの実装について見て終わりにしたいと思います。zebra がカーネルのルーティングテーブルや NIC の設定を変更する際には、RTNETLINK と呼ばれる通信手段を利用します。一般的に、ソケットは他のノードと L4 通信を透過的に行うための UNIX オリジナルの仕組みですが、RTNETLINK はカーネルと通信するための仕組みになり、他のソケットとアドレスファミリによって区別されており、ルーティングソケットと呼ばれています。具体的に RTNETLINK は、ソケット通信の体で netlink をベースとしたプロトコルで、カーネルのルーティング設定情報を操作する仕組みを提供しています。ルーティングテーブルや NIC の設定情報など、カーネルの情報にアクセスする Open vSwitch などといったネットワーク関連のソフトウェアの多くはこの仕組みを使っています。
zebra では kernel_init() [zebra/rt_netlink.c] において、"netlink_cmd" と "netlink" の二つのソケットを生成しています。"netlink_cmd" ソケットは、ルーティングテーブルの設定や、NIC に対するアドレス設定などカーネルに対する処理をさせる際に使用します。また "netlink" ソケットはカーネルから送られている rtnetlink メッセージを待ち受けるために使用しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
/* Make socket for Linux netlink interface. */ static int netlink_socket (struct nlsock *nl, unsigned long groups) { ... sock = socket (AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); if (sock < 0) { zlog (NULL, LOG_ERR, "Can't open %s socket: %s", nl->name, safe_strerror (errno)); return -1; } memset (&snl, 0, sizeof snl); snl.nl_family = AF_NETLINK; snl.nl_groups = groups; /* Bind the socket to the netlink structure for anything. */ if (zserv_privs.change (ZPRIVS_RAISE)) { zlog (NULL, LOG_ERR, "Can't raise privileges"); return -1; } ret = bind (sock, (struct sockaddr *) &snl, sizeof snl); ... nl->snl = snl; nl->sock = sock; return ret; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
/* Exported interface function. This function simply calls netlink_socket (). */ void kernel_init (void) { unsigned long groups; groups = RTMGRP_LINK | RTMGRP_IPV4_ROUTE | RTMGRP_IPV4_IFADDR; #ifdef HAVE_IPV6 groups |= RTMGRP_IPV6_ROUTE | RTMGRP_IPV6_IFADDR; #endif /* HAVE_IPV6 */ netlink_socket (&netlink, groups); netlink_socket (&netlink_cmd, 0); .. } |
以上、簡単に zebra の内部処理について見てきました。zserv が zclient からメッセージを受け取り、rtnetlink メッセージに変換して netlink_cmd ソケットに送り出すまでの処理の過程でキューイングしていたりと、若干厚い処理が間に挟まっていますが、おおよそどういった構造でどういった流れで処理されているのかが分かったのではないでしょうか。ここで記した処理を理解することで、自作のクライアントデーモンと zebra を通信させたり、zebra や各クライアントデーモンに新たなコマンドを追加したり、zebra と zebra クライアント間でのメッセージプロトコルに機能を拡張してみたりといった hack が思いのままに出来るようになると思います。
終わりに
現時点で WhiteBox は、そこそこな規模のインフラを持ち、且つ腕に自信のあるサーバ・アプリケーションエンジニアがたくさんいる環境においてのみ使われている特別な存在になっているのが実状ですが、ネットワーク機器に搭載する OS やアプリケーションの選択が自由になることで、いづれハードウェアの規格が共通化され、様々なミドルウェアが整備されてゆくと思います。その過程で、サーバ・アプリケーションエンジニアにとってネットワーク機器がより一般的な存在になってゆくかもしれません。