Linux Virtual Sever (LVS)は、Linuxオペレーションシステム上で実行されている負荷分散ソリューションの一種で、1998年5月、Wensong ZhangによってOSSプロジェクトとして開始されました。
LVSは、実サーバのクラスタ上に構築されたコンピュータ・クラスター技術を用いて高性能かつ高可用なLinuxサーバを構築し、スケーラビリティや信頼性、保守性を向上させます。また、LVSを利用することで、Webサービスや電子メールサービス、メルチメディア、VoIPサービスなどの高信頼ネットワークサービスの構築が可能となります。

VA LinuxはLVSに積極的な貢献をしており、新機能の実装や現バージョンへの移植作業・管理に協力しています。2010年10月には、netfilter (Linuxのカーネルモジュールとして実装されているパケットフィルタリングとNAT機能)の開発者が集まる招待者限定の技術会議にLVSメンテナメンバーとして出席しており、それらの経験を基に作成した技術調査資料の一部を、2回に分けて紹介します。

はじめに

Linux Virtual Server(LVS)とは、Linuxカーネルに組み込まれたレイヤ4負荷分散の実装であるIPVSを使用したクラスタの名称です。
IPVSの最初のリリースは1998年5月のことで、Linux 2.0カーネルに対するパッチという形でした。そして2003年末には、バージョン2.4.23および2.6.0のメインラインカーネルにIPVSが組み込まれました。その後しばらくは開発が止まり、コードは事実上メンテナンスモードの状態にありましたが、ここ数年は開発が目に見えて活発になり、さまざまな新機能が追加されてきました。
IPVSにはFreeBSD用の実装もありますが、本編ではLinuxの実装を紹介します。

LVSのアーキテクチャ

用語

クラスタというものはその本質上、多数の構成要素が関係するが、LVSクラスタも例外ではありません。LVSについて誤解の余地がないように説明するためには、一貫性のある名前でそれぞれの構成要素を表すことが欠かせません。
LVS HOWTOで使われている用語は以下の通りです。

中心的な要素の名前

  • IPVS(IP Virtual Service):カーネルに組み込まれているL4負荷分散の実装
  • LVS(Linux Virtual Server) :IPVSを使用したクラスタ
  • ipvsadm:IPVSの設定に使うユーティリティ

ホストの名前

  • ディレクター:IPVSが動作しているホスト
  • 実サーバ:ディレクターが転送した接続を受け入れるホスト
  • クライアント:ディレクターに接続するホスト
  • 仮想サービス:クライアントの接続先のサービス
    (1つまたは複数のIPアドレスとポート)

IPアドレスの名前

  • CIP(クライアントIP):接続を行うときにクライアントが使うIPアドレス
  • VIP(仮想IP):仮想サービスのIPアドレス(クライアントの接続先のIPアドレス)
  • DIP(ディレクターIP):ディレクターの統轄用IPアドレス
  • RIP(実サーバIP):実サーバのIPアドレス(ディレクターの接続先のIPアドレス)
  • DGW(ディレクターゲートウェイ):ディレクターのゲートウェイルーターのIPアドレス

ネットワークの名前

  • DRIPネットワーク:DIPとRIPが属しているネットワーク
  • 外部ネットワーク:ディレクターがクライアントからパケットを受信するネットワーク

メカニズムの名前

  • 転送方法:実サーバおよびクライアントへのパケットの転送でディレクターが使うメカニズム
  • スケジューリング:接続する実サーバの選択でディレクターが使うアルゴリズム
  • 持続性:同じクライアントを同じ実サーバに一貫してスケジューリングすること

LVSクラスタ

LVSクラスタはディレクターと実サーバで構成されていて、クライアントがアクセスする対象となります。ディレクターは、1台のみ場合もあれば、2台のディレクターをマスター/スタンバイ構成で稼動させる場合もあります。
後者の場合は、2台の間で接続を同期させておくのが一般的です。そうしておけば、アクティブなディレクターがマスターからスタンバイに切り替わるというフェイルオーバーが発生したときにも、それまでの接続を続行できます。
実サーバは少なくとも1台は必要です。ディレクターが実サーバを兼任するような構成も可能ですが、複数の実サーバを個別に用意する方が一般的です。こうすれば、IPVSの負荷分散の仕組みを利用して、サービスの拡大や縮小が簡単になります。

カーネルとユーザー空間の分離

LVSクラスタではIPVSのレイヤ4負荷分散の実装を利用しています。この実装はLinuxカーネルの中にあり、高速なパケット処理が可能です。つまり、負荷分散の仕組みはカーネルに組み込まれています。
設定はユーザー空間で行われ、通常はipvsadmを使うか、またはipvsadmのラッパーとして機能するツールを利用します。最近のバージョンのIPVSでは、netlinkを利用してカーネルとユーザー空間の構成を行っています。このため、IPVSが提供する仕組みへのアクセスを実現するためのカーネル/ユーザー空間APIが用意されています。
ldirectordやkeepalivedなどの高レベルなツールには、ヘルスチェックの機能や、所定のポリシーに従ってIPVSを設定する機能があります。
カーネル内のIPVSに加えた設定は、カーネルレベルの他の設定と同様、再起動すると元に戻ります。

高レベルな監視

高レベルな監視とは次の2つの処理を表し、どちらもユーザー空間で行われます(設定されている場合)。

1. 実サーバの監視

ヘルスチェックやその他のパラメーターを使って、実サーバの可用性を動的に判断し、必要に応じて仮想サービスに実サーバを追加したり削除したりします。

ディレクターの監視

各ディレクターで障害が発生していないかどうかを確認して、アクティブなディレクターからスタンバイのディレクターへのフェイルオーバーを必要に応じて実施します。フェイルオーバーとはアクティブなディレクターを変更することで、通常はVIPを別のディレクターに切り替えることになります。
高レベルな監視にはさまざまな実装があります。よく使われているのは、1つはkeepalived、もう1つはldirectordとlinux-haの組み合わせです。

 

  • keepalivedは比較的軽量な手法です。
    さまざまなプロトコルに応じた実サーバのヘルスチェックが組み込まれており、IPVSの設定を必要に応じて更新します。
    ディレクターの監視とフェイルオーバーの実装にはRFC 5798のVRRPが使われています。
  • ldirectordはlinux-ha用のモジュールです。
    ldirectord自体は、実サーバのヘルスチェックを実装し、IPVSの設定を適切に更新します。
    ディレクターの監視とフェイルオーバーはlinux-haが処理します。linux-haは豊富な機能を備えた高可用性スタックで、独自のハートビートプロトコルとクラスタメンバーシップアルゴリズムを実装しています。

netfilterのフック

IPVSは、いくつかのnetfilterフックを登録することによって、Linuxのネットワークスタックからパケットを受信しています。Linuxカーネル3.1の時点では、各トランスポートプロトコルについて5種類のフックがあります。

 

  • ip_vs_reply
    IPVS-NAT仮想サービスでリモートの実サーバからの返信パケットについて送信元IPアドレスに対しNATを逆変換します。

    チェーン:LOCAL_IN、FORWARD
    FORWARDチェーンへの登録では、リモートクライアントへの応答を処理します。
    LOCAL_INチェーンへの登録では、クライアントとして機能しているディレクターへの応答を処理します。
    IPv4のフック関数:ip_vs_reply4() (ip_vs_out()の簡単なラッパー)
    IPv6のフック関数:ip_vs_reply6() (ip_vs_out()の簡単なラッパー)
  • ip_vs_local_reply
    IPVS-NAT仮想サービスの返信パケットについて送信元IPアドレスに対しNATを逆変換します。
    これは、実サーバとして機能しているディレクターからの応答を処理します。

    チェーン:LOCAL_OUT
    IPv4のフック関数:ip_vs_local_reply4() (ip_vs_in()の簡単なラッパー)
    IPv6のフック関数:ip_vs_local_reply6() (ip_vs_in()の簡単なラッパー)
  • ip_vs_remote_request
    ローカルクライアントからのパケットのスケジューリングと転送を行います。

    チェーン:LOCAL_IN
    IPv4のフック関数:ip_vs_remote_request4() (ip_vs_in()の簡単なラッパー)
    IPv6のフック関数:ip_vs_remote_request6() (ip_vs_in()の簡単なラッパー)
  • ip_vs_local_request
    ローカルクライアントからのパケットのスケジューリングと転送を行います。

    チェーン:LOCAL_OUT
    IPv4のフック関数:ip_vs_local_request4() (ip_vs_in()の簡単なラッパー)
    IPv6のフック関数:ip_vs_local_request6() (ip_vs_in()の簡単なラッパー)
  • ip_vs_forward_icmp
    fwmark仮想サービスに送信されたICMPパケット及びICMPv6パケットで、マークがされておらず、FORWARDチェーンを越えないパケットへの返信を処理します。
    IP仮想サービスに対するICMPパケットおよびICMPv6パケットはFORWARDチェーンを越えるため、ip_vs_in()のip_vs_icmp()またはip_vs_in_icmp_v6()を呼び出すことで処理できます。

    チェーン:FORWARD
    IPv4のフック関数:ip_vs_foward_icmp() (ip_vs_in_icmp()の簡単なラッパー)
    IPv6のフック関数:ip_vs_forward_icmp_v6() (ip_vs_in_icmp_v6()の簡単なラッパー)

 

ネットワークプロトコル

以前のIPVSは、ネットワーク(レイヤ3)プロトコルにIPv4のみをサポートしていましたが、現在ではIPv6もサポートしています。
IPv6のサポートの追加にあたっては、実装に関して次のような問題がありました。

 

ネットワークプロトコルのモジュール化の欠如

それまでネットワークプロトコルとしてIPv4のみをサポートしていたため、コードがモジュール化されていませんでした。
しかも、大半のコードはモジュール化が簡単ではありませんでした。
IPv4とIPv6以外のネットワークプロトコルをサポートする計画はないことから、解決策としては、必要に応じてヘルパー関数とラッパー関数を用意し、それ以外はifステートメントのブロックを使ってそれぞれのプロトコルに応じたコードパスを個別に実装するという方法をとりました。

 

条件付きビルド

IPv6に関するコードはすべて、CONFIG_IP_VS_IPV6で区別する必要があります。
これは、カーネルにIPv6をコンパイルしていない組み込みシステムなどでサイズを抑えるためです。
このことと、モジュール型でない手法でネットワークプロトコルを実装したことに伴い、現在のIPVSのコードには、CONFIG_IP_VS_IPV6による条件分岐がさまざまな場所にあります。

 

sysctlインターフェイス

sysctlのユーザー空間/カーネルインターフェイスはIPv4のみをサポートしており、下位互換性のあるやり方で拡張することができません。
現在ではsysctlインターフェイスよりもnetlinkプロトコルが好まれることから、netlinkプロトコルが開発され、sysctlインターフェイスは非推奨となりました。

 

接続の同期

以前の同期プロトコルは、32ビットのアドレス用の領域しか確保していなかったため、IPv4のみをサポートしていました。
この問題は、IPVSにIPv6のサポートを追加する中で、32ビットと128ビットの両方のアドレスに対応した新しい同期プロトコルを実装することで解決されました。新しい同期プロトコルの詳細については、次回の「主な機能概要」で解説します。

 

フック

既に述べたように、IPVSはnetfilterのフックを使ってLinuxのネットワークスタックからパケットを受信します。
IPVSにIPv6のサポートを実装するにあたっては、既に登録しているIPv4のフックに加えて、対応するIPv6のフックを登録する必要がありました。

 

ソースコードの再配置

IPVSのソースコードは、アップストリームのカーネルツリーへの統合以降、IPv6のサポートを追加する前までは、net/ipv4/ipvs/にありました。
しかしIPv6に対応すると、この場所は直感的にわかりにくくなるため、IPv6のサポートを追加するための準備として、現在の場所であるnet/netfilter/ipvs/に移されました。

トランスポートプロトコル

IPVSは、トランスポート(レイヤ4)プロトコルのサポートをモジュール方式で追加できるフレームワークを備えています。
現在IPVSがサポートしているトランスポートプロトコルは次のとおりです。

 

  • AH(Authentication Header)
  • ESP(Encapsulation Security Protocol)
  • SCTP
  • TCP
  • UDP

トランスポートプロトコルのサポートはモジュール方式で実装されているため、新しいトランスポートプロトコルを追加する場合でも、IPVSの中核部分やトランスポート以外のプロトコルの部分に変更を加える必要はありません。通常、各トランスポートプロトコルは、それぞれ別個のファイルとして、ip_vs_proto_<プロトコル名>.cという名前で実装されています。
ただし、AHとESPについては、実装が共通する部分が多く、コードの大半を共用できるため、ip_vs_proto_ah_esp.cという1つのファイルになっています。
トランスポートプロトコルはip_vs_protocol構造体のインスタンスとして実装されています。この中には、データとコールバックの両方のメンバが含まれています。
データでは、名前、プロトコル番号、プロトコルの状態の値、およびデフラグメンテーションを認めるかどうかが定義されています。
コールバックではプロトコルの動作が定義されています。たとえば新しい接続のスケジューリングです。これは一般に、IPVSのコア部分のコードが提供するip_vs_schedule()関数のラッパーとして実装されています。

接続とスケジューリング

IPVSでは接続を粒度として負荷分散が実装されています。接続とは、TCPとSCTPの場合は、それぞれのプロトコルで定義されている接続を表します。
UDPの場合は、一連のデータグラムの送信元と送信先のアドレスおよびポートが同じときに、そのストリームを接続とみなします。この動作はワンパケットスケジューリングを使うことで変更できます。詳細については、この文書の「主な機能概要」のセクションで説明します。

デフォルトでは、IPVSサービスは接続に持続性がないものとして扱います。つまり、新しい接続のたびに実サーバが選択されます。同じ送信元ホストからの最近の接続がどうであったかは考慮されません。しかし、接続が持続的となるようにサービスを設定している場合には、一定の時間内に同じホストから受けた接続は、同じ実サーバに転送されます。持続性の動作の詳細については、次回の「主な機能概要」で解説します。

通常のIP通信と同様に、接続はパケットで構成されています。以下、パケットの処理の流れ(IPVSによる接続の処理の流れ)を簡単に説明します。

 

  1. サービスに対するパケットを受信したIPVSは、自らのサービスへのパケットかどうかをチェックします。
    該当しないパケットは、通常のパケット処理の対象として、Linuxのネットワークスタックに返します。
  2. チェックに該当したパケットはIPVSの処理対象です。接続ハッシュをルックアップし、既存の接続に属するパケットかどうかを確認します。
    該当する場合は、接続ハッシュで見つけたエントリに従いパケットを転送します。
  3. 既存の接続に該当しない場合は、新しい接続のパケットです。
    新しい接続を処理する実サーバの選択を行います。この処理のことをスケジューリングといいます。

(i.) 持続性がある仮想サービスの場合は、接続ハッシュのルックアップを行って、持続性テンプレートというエントリが存在するかどうかを確認します。
これは同じクライアントから最近接続があったかどうかのチェックに相当します。

最近接続があった場合は、テンプレートに基づいて接続ハッシュに接続エントリを挿入します。つまり、同じクライアントからの最近の接続を処理したのと同じ実サーバを今回の接続にも使うことになります。そして、パケットはこの実サーバに転送されます。

(ii.) 上記に該当しない場合は、仮想サービスに持続性がないか、同じクライアントからの最近の接続がないかのどちらかです。
この場合は、対象の仮想サービスに対応する実サーバのプールの中から、実サーバを選択します。実サーバの選択に使うアルゴリズムのことをスケジューリングアルゴリズムといいます。
実サーバを選択したら、接続ハッシュに接続エントリを挿入し、パケットはこの実サーバに転送されます。