はじめに
Finagleのクライアントが持っているらしいロードバランサ機能の詳細が不明だったので、調査してみた。
Load Balancer
概要
Finagleのクライアントはロードバランサを備えている。
ロードバランサは、クライアントスタックが持つエンドポイントの集合に対して、タスクをダイナミックに分散する。
ロードバランサの実装は「負荷メトリック」「ディストリビュータ」の2つで構成される。
負荷メトリック
エンドポイントごとの負荷を計測している。未処理のリクエストの数やレイテンシなどを負荷の指標として使用する。
ディストリビュータ
負荷メトリックが計測したエンドポイントごとの負荷に応じて、タスクを書くエンドポイントに分散する。
アルゴリズム
下記4つの分散アルゴリズムが用意されていて、P2Cがデフォルトである。
Heap + Least Loaded
ヒープ(バイナリツリー?)を使用した負荷分散。
ヒープの各ノードで、エンドポイントごとの未処理のリクエストの数を保持し、これを負荷の指標として使用する。
負荷が最小のものから順に並べて、ディストリビュータは負荷の小さいエンドポイントにリクエストを送る。
ノードを入れ替えたりするときに負荷がかかるという弱点がある。
P2C
Finagleのクライアントのロードバランサのデフォルトのアルゴリズム。
ディストリビュータは、ランダムに2つのノードをエンドポイントの集合からピックアップし、2つの負荷のうち小さいほうを選んでリクエストを送る。
ヒープと違って、エンドポイントごとに重みを付けたりもしやすい。
ヒープの弱点を克服している。
P2C+EWMA(Experimental)
P2Cディストリビュータの亜種で、Round Trip Timeの移動平均の値で、未処理のリクエストの数を重みづけしたものを負荷の指標として使用する。
リクエストの処理に時間のかかっているエンドポイントに送らないようにする。
ロングポーリングを行うようなクライアントでは、期待した動作ができない。
Aperture + Least Loaded(Experimental)
これまでの手法は、タスクが十分に並行して(同時に)流れてくるような状況でなければ、ただのランダム選択での負荷分散と同じになってしまう。
Apertureディストリビュータでは、エンドポイントの負荷をもとにしたシンプルなフィードバック制御をかけることで、ディストリビュータはサーバ間の負荷を特定の幅に合わせるようにタスクをバランスする。
(すみません自信なしです・・・。使うことなさそうなので深く追いません。)
動作確認
ローカルにhttpサーバ(ポートは8080,8081)を立てて試してみた。
バージョン6.26.0でないと動作しない。
デフォルト
object BalancerTest extends App {
def main() = {
//カンマ区切りで複数のエンドポイントを指定できる。
val client = Httpx.newService("localhost:8080,localhost:8081")
val request = httpx.Request(httpx.Method.Get, "/")
//複数のエンドポイントにリクエストが分散する。
//なお、一方のエンドポイントが死んでいる場合は、生きている方にリクエストされる。(フェイルオーバー)
val response: Future[httpx.Response] = client(request)
response.onSuccess { resp: httpx.Response =>
println("GET success: " + resp.contentString)
}
Await.ready(response)
}
}
アルゴリズムの変更
ロードバランサのアルゴリズムを変える場合の例。
ほとんどどこにもやり方が載っていなかったので、ソースを追うしかなかった。(基本的にはデフォルトで使えということ?)
細かい設定を行う場合はClientBuilderを使ってクライアントを作る必要があるようだ。
object BalancerTest extends App {
def main() = {
val client = ClientBuilder()
.codec(Http())
.hosts("localhost:8080,localhost:8081")
.hostConnectionLimit(1)
//ロードバランサでheapアルゴリズムを使用する。(デフォルトはP2C)
.loadBalancer(Balancers.heap())
.build()
val request = httpx.Request(httpx.Method.Get, "/")
val response: Future[httpx.Response] = client(request)
response.onSuccess { resp: httpx.Response =>
println("GET success: " + resp.contentString)
client.close()
}
Await.ready(response)
}
}
まとめ
結構簡単にフェールオーバー、ロードバランサが実装できてしまうようだ。
各エンドポイントの負荷状況に応じて、動的にタスクのリクエストの配分を変える処理をシンプルに実装できているのがよさそう。
1つのノード内で負荷メトリックの計測とタスク分散を行う機能が完結しているので、クラスタ内のノード間で負荷情報をやり取りする必要がない点とかも良い気がする。
ただし、どうもこのあたりの機能はバージョンによる挙動とインタフェースの違いがかなり大きいようで、上記のコードもFinagle 6.26では動作するが、Finagle 6.24では動作しない。
フェイルオーバー周りももっと詳しく見ていきたい。