多重化・負荷分散の自動化

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) )

ものすごく単純だけど、拡張性が高い方法。よく使いそうなチェックメソッドはあらかじめ提供しておけば便利。



本体登場です。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年くらいで作ります。と言うか、作りませんか?