多重化・負荷分散の自動化
keepalivedやiptablesに加えて、動的に拡張できる分散ファイルシステムが揃うと、多重化や負荷分散の設定はすべて自動化できる、はず。
目標は、「DSASを全自動で作る」こと。そしてカスタマイズもできて、動的な拡張も可能。
方法としては、ネットワークブートにVIVER、その上のネットワークの構築に「分散オブジェクト技術」を利用。分散オブジェクトと言うと、CORBAと言う規格が有名らしいですが、Wikipediaの解説を読んでもサッパリ分からないので、dRubyを使います。Rubyでしか使えないですが、Rubyで良い、Rubyが良いと思います。
ネットワークブートは起動すれば良いだけの話なので、VIVERでなくても良いんですが。でもルートファイルシステムがNFSではダメなので、V-FIELDは必要(あるいは全部RAMにぶちまける)
基本的なアイディアはRUNES(VIVERの構成要素の1つ)から引き継いでいて、「プラグイン」の集合で分散・多重化ネットワークを構築する。「プラグイン」は何かのサーバーを設定したり、一緒にAPIを提供したりもする。
プラグインがサーバーそのものではなくて、既存のサーバーソフトウェア(Apacheなどなど)を設定するというところが1つ目のポイント。既存のアプリケーションにパッチを当てるのではなくて、ラッパーを書くイメージ。
「分散〜」というモノはいくら文章を書いても抽象的になってしまってまとめる意味がないので、突然擬似コード。
名前が決まらないので、とりあえDSiAS(Distributed Self-integrating Application Server)という名前で識別してみる。うーむ。名前は重要なので迷う。まぁ単なるdRubyのラッパーです。
- NetworkManagerプラグイン
IP関連の情報を他のプラグインに提供するプラグイン。
class NetworkManager class IPv4 # IPv4アドレスを表現するクラス end class IPv6 # IPv6アドレスを表現するクラス end class NetworkInterface # ネットワークインターフェース(eth0, bond0...)を表現するクラス def initialize(ipv4, ipv6, macaddr, netmask, netaddr, bcast) @ipv4 = ipv4; @ipv6 = ipv6; @macaddr = macaddr; @netaddr = netaddr; @bcast = bcast end attr_reader :ipv4, :ipv6, :macaddr, :netmask, :netaddr, :bcast end def initialize(conf) @conf = conf @interfaces = Hash.new `/sbin/ifconfig -a`.each {|line| # @interfacesを登録 # @interfaces["eth0"] = NetworkInterface.new(IPv4.new("192.168.0.2", ....) } @main = @interfaces[0].clone end def mainInterface return @main end def registerIPChangeNotice(&handler) @change_notice.push(handler) end def changeIP(ifname, enmain = false) oldif = @interfaces[ifname] # ifnameインターフェースの情報を取得 # `/sbin/ifconfig #{ifname}`.each {|line| ... } if enmain # @mainを破壊的に変更 @main.change!( @interfaces[ifname] ) end @change_notice.each {|func| # registerIPChangeNoticeで登録されていたメソッドを呼ぶ func.call(oldif, @interfaces[ifname]) } end end DSiAS.provide( NetworkManager.new(DSiAS.conf) )
自分のIPアドレスやネットワークアドレスなどの情報を提供するメソッドと、自分のIPが変化したときに登録されているメソッドを呼ぶ機能。
どうやって自分のIPが変化したことを知るんだ?というと、IPアドレスはNetworkManagerが変更するのでOK。それがchangeIPメソッド。後でbondingするプラグインを作ると、addInterfaceメソッドも要るかな。
changeIPなどのメソッドは、irbを使えば人が呼ぶこともできる。
ここまでは至って普通。最後にDSiAS.provdeがあるくらい。
- VRRPプラグイン
VRRP(vrrpdかkeepalived)を自動的に設定するプラグイン。
class VRRP def ipchange(oldif, newif) # 自分のIPが変わったときにNetworkManagerから呼ばれる # すべてのvrrpdを再起動 end def initialize(conf) @localvrrp = Hash.new # @localvrrp[:http] = 192.168.0.3 DSiAS.local.NetworkManager.registerIPChangeNotice( method(:ipchange) ) end def getRegistered(service) return @localvrrp[service] end def register(service, priority = nil, vip = nil) registered = Array.new if vip == nil DSiAS.haveServiceAll(:VRRP).each {|node| # VRRPプラグインを持つホストすべてに対して registered.concat( node.VRRP.getRegistered(service) ) # 登録済みのVRRPセットを取得 } registered.comapct! # nilを削除 if registered.empty? # serviceは登録されていないので、新たに作る # ここで仮想IPアドレスを自動生成 # 仮想IPの表現には↓NetworkManager::IPv4クラスを使う # vip = DSiAS.local.NetworkManager.class::IPv4.new("192.168.0.3") else vip = registered[0] end end # 自分のIPアドレスへの参照を取得 me = DSiAS.local.NetworkManager.mainInterface if priority == nil # priorityを自動設定(ロードアベレージとかハードウェアの情報を提供するプラグインが必要かな) end # 仮想IP = vip, 優先度 = priorityで、vrrpdを起動 # `./vrrpd ...` # keepalivedの方が良いかも end end DSiAS.provide( VRRP.new(DSiAS.conf) )
普通じゃなくなってきました。分散プログラムの面白いところ。
DSiAS.haveServiceAll(:VRRP)で、ネットワーク中の全ホストから、VRRPプラグインを持っているホストを検索。そして「そのホストのVRRPプラグイン」の、getRegisteredメソッドを呼ぶ。
もしserviceがもう登録されていたら、そのVRRPの仮想IPに参加。登録されていなければ、新たに作る。同時に2つ作ってしまわないように排他処理を入れていないといけないですが、DSiASで排他処理APIを提供するか、「排他処理プラグイン」を作れば良いのかな?
- SelfCheckプラグイン
VRRPは丸ごと落ちないとサーバーが切り替わらないので、アプリケーションレベルの障害を検知できない。そこで自分自身の動作をチェックして、障害が発生していたら積極的に落ちる。本当は他のホストにチェックしてもらうのが良いのですが、ぱっとやり方が思いつかないので。
class SelfCheck # ...略... def register(id, interval, funcs) # funcs[0]をinterval毎に実行→falseが返ったらfuncs[1]を実行 end def unregister(id) # idを登録解除 end def checkHTTP(ip, path) # http://#{ip}/#{path}をチェックする # if 正常 # return true # else # return false # end end end DSiAS.provide( SelfCheck.new(DSiAS.conf) )
ものすごく単純だけど、拡張性が高い方法。よく使いそうなチェックメソッドはあらかじめ提供しておけば便利。
- Lighttpdプラグイン
本体登場です。VRRPで自動的に自分を多重化。
class Lighttpd def recovery # 再起動処理 # 無限ループしない制限が必要 end def start # 起動処理 # VRRPに登録 DSiAS.local.VRRP.register("http") # SelfCheckプラグインに登録 # RubyでC++のboost::bindみたいなことはどうやるのかな? DSiAS.local.SelfCheck.register(self.class, 10, [DSiAS.local.SelfCheck.method(:checkHTTP), method(:recovery)] ) end def stop # 終了処理 DSiAS.local.VRRP.unregister("http") DSiAS.local.SelfCheck.unregister(self.class) end def restart stop start end def ipchange(oldif, newif) restart end def initialize(conf) @conf = conf start # 開始 # IPが変わったらipchangeを呼んでもらう DSiAS.local.NetworkManager.registerIPChangeNotice( method(:ipchange) ) end end DSiAS.provide( Lighttpd.new(DSiAS.conf) )
このプラグイン群を実行すれば、多重化されたHTTPサーバーが自動的に構築されていくというシナリオ。負荷分散プラグインは、VRRPプラグインと同じ要領で作れるはず。
DocumentRootは共有していないといけないわけですが、これは先のスケールアウトする分散ファイルシステムのマウントポイントをDSiAS.confに指定すればOK。当然スケールアウトする分散ファイルシステムもプラグインで起動できるはずだから、そこまで自動化できるかも。分散ファイルシステムがFUSE対応であれば、そのファイルシステムをMacでマウントしてウェブサイトを更新、なんてこともできるかも。(SVNを入れておけばいいか。SVNサーバーも多重化したりして)
Rubyの驚異的な生産性のおかげで、プラグインはわりとサクサク作れる感じ。例外処理や排他処理を入れると別かもしれませんが。
作ったプラグインは、オープンな場で共有できれば完璧。ソフトウェアだけでなくネットワークまでapt-getでインストールできる時代。
問題は2つ。名前が決まらない、モノができていない。というわけで向こう1年くらいで作ります。と言うか、作りませんか?