読者です 読者をやめる 読者になる 読者になる

ひとりでやるRiak Advent Calendar 2012 day22 - disk fullになったとき

Woking vs Ebbsfleet Utd

ふつうのデータベースシステムなら、ディスクフルになった時点でゲームは終了だ。書き込みのトラフィックを止めてデータをリストアして、大きなディスクに移して…や、そもそも共有ディスクのパーティションを他の部署から譲ってもらう交渉をしないといけない。書き込みが止まっている一分一秒に損失は積み上がっていく。たぶん。
だから、ディスクフルにならないように細心の注意を払う。50%を超えたら注意、60%を超えたらはやく更新計画を立てる、70%を超えたらもう休んではいけない、80%を超えたら寝てはいけない、90%を超えたら自分のクビの心配を始める…?

Riakではどうだろうか。ちょっと考えてみた。Riakでも油断しているとディスクフルになることがある。マシンが複数台あるのでそれぞれの容量を見ないといけないのだが、Riakのvnode配置計算は、台数が少ないときは特に必ずしもデータの配置が均等になるわけではない。それは初めての分散環境を構築したときにわかる。試しに分散環境を構築してみよう。

$ git clone git://github.com/basho/riak.git
$ cd riak
$ make stagedevrel  DEVNODES=5
....
$ cd dev
$ dev1/bin/riak start; dev2/bin/riak start; dev3/bin/riak start; dev4/bin/riak start; dev5/bin/riak start; 
$ dev1/bin/riak ping; dev2/bin/riak ping; dev3/bin/riak ping; dev4/bin/riak ping; dev5/bin/riak ping; 
pong
pong
pong
pong
pong
$ dev2/bin/riak-admin cluster join dev1@127.0.0.1
Success: staged join request for 'dev2@127.0.0.1' to 'dev1@127.0.0.1'
$ dev3/bin/riak-admin cluster join dev1@127.0.0.1
Success: staged join request for 'dev3@127.0.0.1' to 'dev1@127.0.0.1'
$ dev4/bin/riak-admin cluster join dev1@127.0.0.1
Success: staged join request for 'dev4@127.0.0.1' to 'dev1@127.0.0.1'
$ dev1/bin/riak-admin cluster plan
(snip)
$ dev1/bin/riak-admin cluster commit
$ dev1/bin/riak-admin member-status
================================= Membership ==================================
Status     Ring    Pending    Node
-------------------------------------------------------------------------------
valid      25.0%      --      'dev1@127.0.0.1'
valid      25.0%      --      'dev2@127.0.0.1'
valid      25.0%      --      'dev3@127.0.0.1'
valid      25.0%      --      'dev4@127.0.0.1'
-------------------------------------------------------------------------------
Valid:4 / Leaving:0 / Exiting:0 / Joining:0 / Down:0

と、4台だとデータ量は均等だ。しかし、ここに5台目を加えると

$ dev5/bin/riak-admin cluster join dev1@127.0.0.1
Success: staged join request for 'dev5@127.0.0.1' to 'dev1@127.0.0.1'
$ dev1/bin/riak-admin cluster plan
=============================== Staged Changes ================================
Action         Nodes(s)
-------------------------------------------------------------------------------
join           'dev5@127.0.0.1'
-------------------------------------------------------------------------------


NOTE: Applying these changes will result in 1 cluster transition

###############################################################################
                         After cluster transition 1/1
###############################################################################

================================= Membership ==================================
Status     Ring    Pending    Node
-------------------------------------------------------------------------------
valid      25.0%     18.8%    'dev1@127.0.0.1'
valid      25.0%     18.8%    'dev2@127.0.0.1'
valid      25.0%     18.8%    'dev3@127.0.0.1'
valid      25.0%     25.0%    'dev4@127.0.0.1'
valid       0.0%     18.8%    'dev5@127.0.0.1'
-------------------------------------------------------------------------------
Valid:5 / Leaving:0 / Exiting:0 / Joining:0 / Down:0

Transfers resulting from cluster changes: 12
  4 transfers from 'dev3@127.0.0.1' to 'dev5@127.0.0.1'
  4 transfers from 'dev2@127.0.0.1' to 'dev5@127.0.0.1'
  4 transfers from 'dev1@127.0.0.1' to 'dev5@127.0.0.1'

このように均等にはならない。ここではvnodeが64なので、 64.0/5/64 = 0.2 で、1台が12〜13のvnodeを持って次のように配置すればもっと均等になるはずだ:

node vnode percentage
dev1 13 20.3%
dev2 13 20.3%
dev3 13 20.3%
dev4 13 20.3%
dev5 12 18.8%
sum 64 -

しかし実際はこうはならない。このあたりに詳しい @jtuple に直接アルゴリズムを一度説明してもらったのだが、ノード追加時にhandoffによるvnode移動量(=ネットワークの負荷)と、データ量の配置の両方の最適化パラメータがあるため、量だけを均等にするアルゴリズムを考えることはできるが…現状は若干heuristicにやっているということだった。

つまり、DHTを使ってデータ量がなるべく均等に配分されるようにしているが、完全に均等にできるわけではなく、5〜6%くらいのズレは起きてしまうことを念頭に入れてディスク容量も設計した方がよいだろう。僕も実際のテスト環境で、まだ容量に余裕があると思って放置していたらディスクフルになってノードが落ちていたことが何度かあった(学べよ)。

一杯になったらどうするか

まず、これはどのようなデータベースでもそうだが、運用を見直すべきだ。それを見なおさずにとりあえず再起動して万が一動いたとしても、また一杯になって運用は破綻していくだろう。だから、基本的には満杯になったマシンがいたら、いくらかマシンなり容量なりを追加するプランを立ててから復旧に望んでほしい。定常的に追加するというのもクラウドっぽくてよいかもしれないが。

ディスクを増やして再起動する場合

Riakは起動時にいくらかデータを更新してしまうので、リードオンリーのような状態でサーバーを起動することはできない。残り200KBとかそれくらいしか残っていないだろうけど、それでは起動できない。もしもRiakのデータが入っているパーティション(多くの場合デフォルトは /var/lib/riak )が何らかの方法で拡張できるなら、拡張して起動するとよい。容量が余っていれば、もう一度 "riak start" とするだけで何事もなかったようにまたクラスタに復活するだろう。
1.2系を使っているのであれば、そのノードが持っていたデータが既に古くなっている可能性もあるので、全てのキーに read repair をかける必要がある。まあ単に全部読みだせばよいだけだが。
1.3系になると、AAEが入っているので特になにもする必要はない。バックグラウンドで自動的に整合性を回復してくれるだろう。

マシンを追加して対処する場合

実はこれは簡単なように見えて、そんなに簡単ではなかった。何はともあれ、新しく追加するマシンを用意して、クラスタにjoin, commitまでさせてしまおう。commit後、彼らが引き受ける全てのvnodeが死んだマシンからtransferされるものでなければ、transferは自動的に進む。
しかし、死んだマシンが持っていたvnodeを新しく入ったマシンが引き受けるプランになってしまうのが普通だろう。そういうときは、新しいマシンにデータを移すために死んだマシンを立ちあげないといけないのだが、前述の通りリードオンリーでも起動することはできない。もしも何も準備しておらず、/var/lib/riak の容量を空ける手段がない(代わりに消すファイルがない)場合は、仕方がないのでvnodeのデータを削除する。危険だと思うかもしれないが、レプリカ数が3から2に落ちるだけでデータは戻ってくるから問題ない。たとえば、 /var/lib/riak/bitcask か、 /var/lib/riak/leveldb の下の適当に大きなディレクトリを削除するとよい。全部消す必要はなくて、数百MBとか、それくらい空けば十分だろう。そうすれば、Riakを起動させるための空き容量を捻出できる。このとき消したデータは、read repairなりAAEなりで復旧されるので心配はしなくてよい。いや心配になるけど。
本当はこんなことしない方がよいので、こういうときのために予め〜1GB程度のスペーサーになるファイルを作っておいて、非常時にそのファイルを消して容量を空けるという手段をとるのがよさそうだ。

さて、なんとか容量を捻出してRiakのノードを再起動できて、transferが再開できたとしても油断してはいけない。このとき、なるべくなら外部からの書き込みのトラフィックを遮断しておくのがよい。遮断しなくてもクラスタは動作するが、書き込みのトラフィックが100%近くなっている当事者のノードに来て、vnodeを他のノードに移譲する前にそのマシンが死んでしまう場合は初めからやり直しになってしまう。

まとめ

  • 用法用量を守って正しく使ってください
  • いくらDHTでも種々の制約により負荷やデータ量が完全に均等になることはありません
  • ディスク満杯になるとプロセスが落ちます
  • マージンは予めスペーサーになるようなファイルを作って確保しておいた方がよいかも