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

mixi engineer blog

ミクシィ・グループで、実際に開発に携わっているエンジニア達が執筆している公式ブログです。様々なサービスの開発や運用を行っていく際に得た技術情報から採用情報まで、有益な情報を幅広く取り扱っています。

mixi大規模障害について その2

memcached mixi

こんにちは。システム本部技術部たんぽぽGの森本です

補足を追記しました (2010/08/20 15時)

先日のmixi大規模障害についての続報です 今回は小ネタはありません

はじめに

まず初めにtwitter/blogなどを通じて今回の問題の解析を行っていただいたみなさんに感謝の言葉を捧げたいと思います
  • kzk_moverさん
  • stanakaさん
  • mala(bulkneets)さん
  • llameradaさん
(順不同) ありがとうございました 書き漏らした人ごめんなさい

memcachedはすごい

今回の件でmemcachedに対して不安感を持たれた方もおられるとお聞きしました 説明不足だったせいで誤解を与えてしまい申し訳ありません きちんと設定および監視を行っていれば通常の使用にはまったく問題はありません 弊社にて -c 30万で起動したmemcachedに対して、先のテストスクリプトにて28万接続を頻繁にくりかえす負荷テストを行いました 19:00から翌朝の10:00まで何の問題もなく1行のエラーも吐くこともなく17時間動作したことを確認しました

詳細

今回のmemcached不具合の内容をまとめました

main thread

  • accept()で新しいクライアント接続を受け取るが失敗する
  • errno は EMFILE なので "Too many open connections" と判断する
  • 新しい接続要求を受け付けなくするためにaccept_new_conns(false); を呼び出し、listen()のbacklog数を0にする
  • update_event(next, 0)経由でevent_set()監視対象イベントも0にする
この状態だと main thread はaccept用socketのイベントは監視せず、timerイベントだけを受け取る状態になります

worker thread

  • クライアントが接続を切断したので conn_close() を呼び出して後始末を行う
  • 1本接続が減ったので新しい接続を受け付けられるはずなのでaccept_new_conns(true) を呼び出す
  • update_event(next, EV_READ | EV_PERSIST)経由で、main threadにaccept用socketのイベント監視を再開させる
ここで排他制御がうまくいかないと、処理待イベント数(event_count)が同時にインクリメントされ、event_countが実際の処理待ちイベント数よりも少ない数を保持することになります その後 main threadがイベントを処理するとデクリメントされ、最終的にevent_count が0になり処理ループを抜けmemcachedが終了していました accept_new_cons()自体はmutexを用いて排他制御が行われていましたが、それ以外の経路で main_base を操作する経路がいくつかありました 一つは clock_handler()、もう一つは accpetソケットに接続要求があった場合にlibeventが呼び出す epoll_dispatch()です event_countに不整合が発生した場合の stack trace を取ったところ event_base_loop => epoll_dispatch => event_queue_insert と呼び出されていました

再発防止策

memcached を -c 30万で設定しました 実際の接続数を監視してしきい値を超えるとアラートメールが飛ぶように設定しました 長期的にはアーキテクチャの変更が必要なので下記の項目について検討中です
  • サービス単位での個別のmemcachedを使用
  • memcached proxyの使用
  • UDPの使用
  • 不揮発なキャッシュシステム

まとめ

私見ですが、memcachedを運用する場合には -c による接続数の制限はできるだけ大きくするのが望ましいと思います 大事なのはmemcachedがきちんとサービスを提供できていることなので、CPU/Load/Memory/cache hit/response timeなどをきちんと監視していれば実際の接続数がいくらなのかは問題ではないかと思います ちなみに現在 mixi では -c 30万で運用中です -c を大きく設定するにはいくつかのカーネルパラメータの変更が必要です 詳しくはググってください また、実際の接続数が大きくなるとネットワークバッファなどのリソースに影響がでますので、適宜監視が必要です

補足

「-c 30万は対症療法」「patchは書かないのか」とのご意見をいただきましたので若干の補足です

-c 30万

今回のmemcachedの不具合は単なる高負荷状態では発生せず、接続限界数に達したときのみ発生します -c で設定した接続数に達して新しい接続が受けられなくなった時点でキャッシュしている値に不整合がでる可能性があるので、memcachedが落ちなくなったとしても、mixiというサービスとしては問題となります 以下のような場合にキャッシュ値に不整合がでます
  • ユーザーがmixiにアクセス
  • httpd Aがニックネーム(たろう)をMySQLから取得
  • httpd Aがmemcachedに保存
  • ユーザーがニックネーム(たろう => 太郎)を変更
  • httpd Bが新しいニックネーム(太郎)をMySQLに保存
  • httpd Bがmemcached上のニックネームを削除または更新
  • 接続限界数に達しているため処理が失敗する 不整合発生
  • ユーザーがmixiにアクセス
  • httpd Aがニックネーム(たろう)をmemcachedから取得 ※MySQL上のニックネームは太郎です

この様に一部のサーバーだけがmemcachedにつながった状態だとキャッシュしている値に不整合がでます
特に接続限界数に達するのが一時的なものであり、その後は正常に接続が行われる場合にはアラートにひっかかりにくく発見するのが困難になります
memcachedが完全に落ちた場合は自動的に再起動が行われるので一時的にMySQLの負荷が上昇するだけですみます

上記のようなことから、そもそも「接続数限界に達すること」自体が(memcachedのではなくmixiサービスを提供する上での)問題だと判断し、「-c 30万」および「十分に余裕を持たせた接続数の監視」をもって解決策としました

patch

memcachedが接続最大数に達した場合にどのように振る舞うのがよいのかを考え中です 現在の動作はlisten(fd,0)と接続待ち行列を0にしてクライアントからの接続要求を即座に拒否する作りになっています しかし、この方法だと上で述べたようにキャッシュ値の不整合が起こる可能性があります 他の方法、例えば古い接続もしくはリクエストが来ていない接続を強制的に切断するほうが、性能は低下するもののキャッシュ値に不整合がでないのではないか、など悶々としております このあたりはmemcached MLで相談してみようとおもいます