kumo-server が全ノード WAIT になる

kumofs でサーバーを kumoctl detach した時に、kumo-server の全ノードが WAIT 状態になることがあったので調べてみました。というかここ数ヶ月ずっと調べてました。

attach, detach, replace により、kumo-server 間でデータが転送される時は次のような流れで行われます。

この例は server1 から server2 と server3 にデータを送る場合です。

    server1                   server2                   server3
       |                         |                         |
 A     |---- 送ってもいい? ---->|                         |
 B     |---- 送ってもいい? ------------------------------>|
       |                         |                         |
 C     |<------- いいよ ---------|                         |
 D     |<------------------------------- いいよ -----------|
       |                         |                         |
 E     |------ 転送開始 -------->|                         |
       |          :              |                         |
       |-------転送終了 -------->|                         |
       |                         |                         |
 F     |-------転送開始----------------------------------->|
       |          :              |                         |
       |-------転送終了----------------------------------->|
       |                         |                         |

A, B に対する応答 C, D は並列に処理されますが、データ転送 E と F はシーケンシャルに処理されます。
また、A, B 時には C, D の応答を待つためのタイマーが設定されます。デフォルトでは 1280秒*1でタイムアウトします。

通常は server1 が自分が持ってるデータのうち server2, server3 それぞれに送りたいものを /tmp*2 に抽出してから、転送を開始します。これだと大量のデータを転送する必要がある場合、かなり大きな一時ファイルが必要になってしまいます。--replace-memory-limit オプションを指定することで、指定したメモリに達する度に転送することができます。

図にするとこんな感じです。

    server1                   server2                   server3
       |                         |                         |
   (データが溜まった)            |                         |
       |---- 送ってもいい? ---->|                         |
 G     |---- 送ってもいい? ------------------------------>|
       |                         |                         |
       |<------- いいよ ---------|                         |
 H     |<------------------------------- いいよ -----------|
       |                         |                         |
       |------ 転送開始 -------->|                         |
       |          :              |                         |
       |-------転送終了 -------->|                         |
       |                         |                         |
       |-------転送開始----------------------------------->|
       |          :              |                         |
       |-------転送終了----------------------------------->|
       |                         |                         |
       |                         |                         |
   (データが溜まった)            |                         |
       |---- 送ってもいい? ---->|                         |
 I     |---- 送ってもいい? ------------------------------>|
       |                         |                         |
       |<------- いいよ ---------|                         |
       |<------------------------------- いいよ -----------|
       |                         |                         |
       |------ 転送開始 -------->|                         |
 J     |          :              |                         |
       |-------転送終了 -------->|                         |
       |                         |                         |
 K     |-------転送開始----------------------------------->|
       |          :              |                         |
       |-------転送終了----------------------------------->|
       |                         |                         |

問題は G で設定した server3 に対するタイマーが J でタイムアウトすることがあるということです。G に対する応答は H で返っているにもかかわらず、タイマーは生きたままなので、このタイムアウトを I のタイムアウトと勘違いしてしまうようです。その結果 K の処理は行われず、ログに次のように出力されます。

storage offer to server3 is already timed out

おそらく --replace-memory-limit を使用してなくても短期間に attach, detach を何回か実行した場合にも発生するのではないかと思います。


ということで、この現象に対処してみました。

https://github.com/tmtm/kumofs/commit/479e94781362d1880874c7aede16bf311c0df041

kumofs の内部構造をわかってないので上記の説明も間違ってるかもしれませんが、一応うまく動いているようです。

*1:--clock-interval オプションの値(デフォルト: 8) * 160 秒

*2:--offer-tmp オプションで変更可能

kumofs の attach, detach

最近 kumofs の attach, detach 時の動きについて調べてます。

kumo-manager のログ

kumo-manager の「lost node」のログにバイナリコードが含まれてます。

2011-05-14 23:00:00 manager/framework.cc:97: lost node XX 127.0.0.1:19803

この「XX」の部分が 0x01 になっています。この値が何を意味するのかはわかってませんが、たぶんバグだと思います。こんな感じで直りました。
https://github.com/tmtm/kumofs/commit/5d9dc75f9e386620bde72a16a4fedcfde7132770

kumo-server のログ

attach, detach 時に以下のようなログが大量に出力されます。

2011-05-14 22:39:55 server/mod_replace.cc:291: current_owners.empty() || current_owners.front() != self

おそらく、自ノードにあるデータのうち、自ノードがプライマリノードでないものがあるとこれを出力しています。
でもノードが保持しているデータの 2/3 は複製データ(プライマリノードでないデータ)なので、これをログに出力する意味はありません。たぶん。
こんな感じで直りました。というか元に戻しただけですが。
https://github.com/tmtm/kumofs/commit/0095ae93b83af1768a5b8b529b8e2faac8322b92

kumoctl status と kumostat の違い

kumo-server の状態を調べるには、kumoctl status コマンドと kumostat コマンドがあります。
当初なんで2つ用意されているのかわからなかったのですが、わかったような気がします。
kumoctl status は kumo-manager が認識しているサーバーの状態を表示するのに対し、kumostat は各サーバーが自分の状態を表示します。

なので、kumoctl status で、あるサーバーが fault になっていても、kumostat では ready になっていることがあり得ます。

監視する時には両方のコマンドを確認した方が良いでしょう。

attach 時の動き

サーバー(ノード)を attach した時の動きを調べてみました。

  1. 自ノードが保持しているデータのうち、自ノードがプライマリノードであるデータで、かつ、新しく attach されたノードも複製を持つべきデータについて、すべてを一時ファイルに作成します。一時ファイルは送るべきノード単位に作られます。(Copying)
  2. 一時ファイルを作成し終えると、新ノードに送信します。
  3. 新ノードは送られたファイルからデータを登録します。
  4. 全ノードが Copying 処理を完了し、新ノードがデータを登録し終えるまで待ちます (WAIT)
  5. 自ノードが保持しているデータのうち、不要になったデータを削除します (Deleting)
  6. 通常の状態になります (ready)

括弧の中は kumotop の status 欄または kumostat replstat で表示されるものです。

detach 時もほぼ同じ動きです。データをコピーする相手が新しいノードではなく、すべての既ノードになるだけです。

少数のサーバーで大量のデータを保持している場合は、一時ファイルがかなり大きくなってしまいます。一時ファイルはデフォルトでは /tmp に作成されますが、/tmp に余裕がない場合は kumo-server の --offer-tmp オプションで別のディレクトリを指定しておくのが良いでしょう。

また、コピーするデータを全部作成してから送るのではなく、少しずつ送る方法もあります。kumo-server の --replace-memory-limit オプションを指定すると、コピーするデータが指定サイズ(MB単位)に達するとその時点で新サーバーに送られるようになります。ただし、このオプションはドキュメントには載っていないようです。

attach 中のデータ参照&登録

kumofs はノードとデータの分散用のハッシュスペースとして、参照用のハッシュスペース(rhs)と、登録用のハッシュスペース(whs)の2つを使用します。

ノードを attach, detach すると、データの再配置が完了するまでの間は、rhs は古いまま、whs は新しいものになります。

既存のデータの参照については rhs を見れば良いので問題ありません。新しいデータは登録時は whs が示すノードに書かれて、参照時は rhs が示すノードを見ます。異なるノードを見ることになって問題はないのでしょうか。

おそらく通常は問題ないと思います。なぜなら、whs のプライマリノードに登録されたデータは、プライマリノードの次のノードにもコピーされますが、このノードは rhs では該当データに関するプライマリノードだったはずだからです。

問題となり得るのは 3台以上のノードを一度に attach する場合です。この場合は whs のプライマリノードとコピーされるノードがすべて新しいノードになってしまう可能性があるためです。もしかしたら kumofs はそういう場合のこともちゃんと考慮して作られているかもしれませんが、わかりませんでした。

さいごに

kumofs の attach, detach の動きについて調べてみました。
自分は C++ を満足に読めないので、kumofs のソースをちゃんと読めている自信はないです。なので、上に書いてあることは間違ってるかもしれません。
もし間違いを見つけたら教えていただければ嬉しいです。

kumofs ストレージ API

kumofs のストレージのAPIについて調べてみました。

元々 kumofs は configure 時にストレージを選択できるようになっていて、本体部分とストレージ部分が API によりある程度分離されています。なので独自にストレージを作成することもそんなに難しくなくできると思います。

ストレージ部には21個の関数が必要です。関数は src/storage/interface.h で定義されています。

以下、各関数について説明します。関数名の「xxxxx」は通常ストレージ固有の名前を指定します。たとえば kumofs 標準のストレージである Tokyo Cabinet の Hash の場合は tchdb となっています。

嘘書いてあるかもしれないので、あまり信じない方がいいと思います。

ストレージ層に渡されるキーとデータはクライアントから指定されたものそのままではなく、kumofs 管理用のヘッダがついています。キーのヘッダは 8バイト、データのヘッダは 10バイトです。詳しくは doc/doc.ja.md の FAQ データベースファイルのフォーマットを見てください。

create

static void* kumo_xxxxx_create(void)

初期化。kumo-server 起動時に呼ばれます。
戻り値は任意のポインタです。これ以外の関数の呼び出し時に data として渡されるものです。
NULL を返すと初期化に失敗したことを表し、kumo-server は終了します。

free

static void kumo_xxxxx_free(void* data)

kumo-server 終了時に呼ばれます。

open

static bool kumo_xxxxx_open(void* data, const char* path)

kumo-server が attach された時に呼ばれます。
path は kumo-server の -s オプションで指定されたファイル名です。

close

static void kumo_xxxxx_close(void* data)

kumo-server 終了時に呼ばれます。

get

static const char* kumo_xxxxx_get(void* data,
		const char* key, uint32_t keylen,
		uint32_t* result_vallen,
		msgpack_zone* zone)

データを取り出します。
key, keylen はキーの値と長さを示します。
戻り値で取り出したデータの値のポインタを返します。データが見つからなかった場合、処理に失敗した場合は NULL を返します。
result_vallen はデータの長さを返すポインタです。

データが不要になった場合の処理は次のように登録します。

msgpack_zone_push_finalizer(zone, 関数名, 関数に渡す引数);

msgpack_zone_push_finalizer() が 0 を返した場合は登録に失敗しているので、kumo_xxxxx_get() も NULL を返す必要があります。

データを格納したメモリを malloc() で獲得し、ポインタ val で保持している場合の処理は、通常次のようになります。

if (!msgpack_zone_push_finalizer(zone, free, val)) {
    free(val);
    return NULL;
}

get_header

static int32_t kumo_xxxxx_get_header(void* data,
		const char* key, uint32_t keylen,
		char* result_val, uint32_t vallen)

kumo_xxxxx_get() と同じく値を取り出しますが、返すデータの最大長が指定されています。
key, keylen はキーの値と長さを示します。
取り出した値は result_val に入ります。result_val の大きさは vallen で指定されています。
戻り値は result_val に書き込んだデータの長さです。データが見つからなかった場合は -1 を返します。

set

static bool kumo_xxxxx_set(void* data,
		const char* key, uint32_t keylen,
		const char* val, uint32_t vallen)

キーとデータを格納します。
key, keylen はキーの値と長さを示します。
val, vallen はデータの値と長さを示します。
成功した場合 true, 失敗した場合 false を返します。

del

static bool kumo_xxxxx_del(void* data,
		const char* key, uint32_t keylen,
		kumo_storage_casproc proc, void* casdata)

キーとデータを削除します。
key, keylen はキーの値と長さを示します。
proc と casdata はCAS処理のために使用されます。
指定されたキーが存在する場合、「proc(casdata, 既存データ値, 既存データ長)」を呼び出します。
proc が true を返した場合データを削除して true を返します。
proc が false を返した場合はデータを削除せずに false を返します。

update

static bool kumo_xxxxx_update(void* data,
			const char* key, uint32_t keylen,
			const char* val, uint32_t vallen,
			kumo_storage_casproc proc, void* casdata)

キーに対応するデータを更新します。
key, keylen はキーの値と長さを示します。
val, vallen はデータの値と長さを示します。
proc と casdata はCAS処理のために使用されます。
指定されたキーが存在しない場合データを登録します。
指定されたキーが存在する場合、「proc(casdata, 既存データ値, 既存データ長)」を呼び出します。
proc が true を返した場合データを更新して true を返します。
proc が false を返した場合はデータを更新せずに false を返します。

rnum

static uint64_t kumo_xxxxx_rnum(void* data)

ストレージ内の全データの数を返します。

backup

static bool kumo_xxxxx_backup(void* data, const char* dstpath)

kumoctl backup 時に呼ばれます。
dstpath はバックアップ先のファイル名です。
成功した場合は true, 失敗した場合は false を返します。

error

static const char* kumo_xxxxx_error(void* data)

エラーメッセージを返します。

for_each

static int kumo_xxxxx_for_each(void* data,
		void* user, int (*func)(void* user, void* iterator_data))

ストレージの全データを順番に指定された関数 func に渡します。
func の第一引数は user をそのまま渡します。
func の第二引数は1件のデータを表すポインタで kumo_xxxxx_iterator_ で始まる関数に渡されます。

iterator_key

static const char* kumo_xxxxx_iterator_key(void* iterator_data)

iterator_data からキー値のポインタを取り出します。

iterator_val

static const char* kumo_xxxxx_iterator_val(void* iterator_data)

iterator_data からデータ値のポインタを取り出します。

iterator_keylen

static size_t kumo_xxxxx_iterator_keylen(void* iterator_data)

iterator_data からキーの長さを取り出します。

iterator_vallen

static size_t kumo_xxxxx_iterator_vallen(void* iterator_data)

iterator_data からデータの長さを取り出します。

iterator_release_key

static const char* kumo_xxxxx_iterator_release_key(void* iterator_data, msgpack_zone* zone)

iterator_data からキー値のポインタを取り出します。
メモリ管理をイテレータから zone に移します。キー値のメモリ解放は msgpack_zone_push_finalizer() で zone にまかせます。イテレータ側でメモリを解放してはいけません。
実際にはこの関数は kumofs では使用されていないように見えます。

iterator_release_val

static const char* kumo_xxxxx_iterator_release_val(void* iterator_data, msgpack_zone* zone)

iterator_data からデータ値のポインタを取り出します。
メモリ管理をイテレータから zone に移します。データ値のメモリ解放は msgpack_zone_push_finalizer() で zone にまかせます。イテレータ側でメモリを解放してはいけません。
実際にはこの関数は kumofs では使用されていないように見えます。

iterator_del

static bool kumo_xxxxx_iterator_del(void* iterator_data,
		kumo_storage_casproc proc, void* casdata)

iterator_data が示すデータを削除します。
成功した場合は true, 失敗した場合は false を返します。
proc, casdata は kumo_xxxxx_del() と同様です。

iterator_del_force

static bool kumo_xxxxx_iterator_del_force(void* iterator_data)

iterator_data が示すデータを削除します。

kumo_storage_init

上記各関数のポインタが入ってる kumofs_storage_op 構造体のポインタを返します。kumo-server 起動時に呼ばれます。以降のストレージ処理はこの構造体のメンバ関数を通して処理されます。
次のように使用します。

static kumo_storage_op kumo_xxxxx_op =
{
	kumo_xxxxx_create,
	kumo_xxxxx_free,
	kumo_xxxxx_open,
	kumo_xxxxx_close,
	kumo_xxxxx_get,
	kumo_xxxxx_get_header,
	kumo_xxxxx_set,
	kumo_xxxxx_del,
	kumo_xxxxx_update,
	NULL,
	kumo_xxxxx_rnum,
	kumo_xxxxx_backup,
	kumo_xxxxx_error,
	kumo_xxxxx_for_each,
	kumo_xxxxx_iterator_key,
	kumo_xxxxx_iterator_val,
	kumo_xxxxx_iterator_keylen,
	kumo_xxxxx_iterator_vallen,
	kumo_xxxxx_iterator_release_key,
	kumo_xxxxx_iterator_release_val,
	kumo_xxxxx_iterator_del,
	kumo_xxxxx_iterator_del_force,
};

kumo_storage_op kumo_storage_init(void)
{
	return kumo_xxxxx_op;
}