本部長や副本部長もプログラミングを(たまに)することで有名なサイボウズの運用本部長、山本泰宇です。 有名じゃないかもしれませんが、ブログに書いたので有名になるということでご了承ください。
今回は、先日発生した yrmcds に起因する障害の原因と対策を解説します。 yrmcds というのは、サイボウズが開発している memcached 互換のキーバリューストレージです。
問題の理解のため、まず TCP 通信で、通信先の相手の障害にどう対応するか解説します。
データの送信中に相手が落ちるケース
このケースはさらに二つに分かれます。
- 相手の OS は生きているが、通信しているプログラムが落ちるケース
- 相手の OS ごと(あるいはネットワークごと)落ちるケース
1 と 2 の違いは、前者の場合 RST パケットが返ってくるのに対して、後者ではなにも返ってこない点です。後者の場合、ack されない送信データ(unacked data)がカーネルの送信バッファにたまる状態になります。
前者のケースでは、ソケット API (send等)がエラーを返すので、プログラム上の処理は容易です。 後者の場合、対応方法はいくつか考えられます。
カーネルが再送信を諦めるのを待つ
Linux の場合再送信の上限回数は sysctl の
net.ipv4.tcp_retries2
で設定できます。 デフォルトでは 15 回となっており、数十分間は再送を続ける動作となります。アプリケーションレベルでタイムアウトする
一定時間内に通信相手の応答がない場合はエラーとするようにします。 通信プロトコルを自由に設計できる場合は可能な選択肢になります。
ソケットオプションでタイムアウトする
ソケットを非同期にしていない(同期ソケット)場合は、
setsockopt(SO_SNDTIMEO)
でタイムアウト時間を設定できます。 ただしカーネルの送信バッファにまだ余裕がある場合はsend
はすぐ返ってくるため、時間内に相手にデータが届いたかは保証できません。ソケットを非同期にしている場合、
setsockopt(TCP_USER_TIMEOUT)
が利用できる場合があります。 Linux の場合 2.6.37 以降のカーネルで利用できるようです。
ポータビリティや時間の保証という点を考慮すると、2 のアプリケーションレベルでタイムアウトするのが一番確実と言えるかと思います。
データの待機中に相手が落ちるケース
データ受信については、アプリケーションレベルでタイムアウト処理を実装することが多いと思います。 でも中には、無期限にコマンドを待つサーバー実装もあるでしょう。
そういうケースでは、アプリケーションレベルのタイムアウト処理の代わりに、TCP keepaliveという仕組みを利用することが可能です。
TCP keepalive は通信がまったくない場合(ここ重要)に、カーネルがアプリケーションの代わりに通信相手に確認パケットを送ることで、通信相手の存在を自動的に確認してくれる機能です。
Linux の場合 TCP_KEEPIDLE
等のソケットオプションを調整することで、TCP keepalive のタイマーの動作を調整可能です。
修正前の yrmcds の実装
さて、懺悔の時間です。yrmcds はレプリケーションを実装しているので、yrmcds のサーバー間で通信をしています。 レプリケーション方式は非同期ではあるものの、causality (因果律)を保つため、オブジェクトをロックしている間にレプリケーション先にデータを送信するようになっています。
結論から先に言うと、レプリケーション先(スレーブ)が不意に消失したときに、yrmcds は自前でタイムアウト処理をしていませんでした。 また、レプリケーション用のソケットの送信バッファが詰まる場合に備えてアプリケーションレベルでもバッファを確保していたのですが、30 MB の固定サイズとなっており、実際の運用環境では約 15 秒でいっぱいになる状態でした。
結果、スレーブサーバーが不意に落ちたことの検出に、カーネルの再送信がタイムアウト(20分前後)するまでかかってしまい、バッファも不足していたためマスターサーバーの処理が滞留して障害になったものです。
私の勘違いは、TCP keepalive 処理を調整すれば、このような状況でもカーネルが自動検出して速やかにエラーにしてくれるだろうと思い込んでいた点です。yrmcds では TCP keepalive を 5 分程度で動作するようにしていました。実際には、TCP keepalive は再送中のパケットがある場合は動作を開始しないため、送信処理のタイムアウトには利用できないものでした。
yrmcds 1.1.5 の修正内容
yrmcds 1.1.5 でこの問題を修正しました。
具体的には、レプリケーションスレーブがアプリケーションレベルでマスターに ping を飛ばし、マスターは ping が一定時間こないときにエラー処理をするようにしています。また、レプリケーション用のバッファサイズについても可変にし、障害検出にかかる時間の間データを貯めておくのに必要なサイズを確保できるようにしています。
また、レプリケーションバッファがいっぱいになったときはログに警告が出るようにもしています。
教訓
- TCP の障害検出は、なるべくアプリケーションレベルで実装しましょう
- 送信バッファサイズのようなパラメータは実運用に必要なサイズを確保できるよう実装しましょう
ご迷惑をおかけしたお客様には、大変申し訳ありませんでした。