「Mobageを支える技術」読了

Mobageを支える技術 ~ソーシャルゲームの舞台裏~ (WEB+DB PRESS plus)

Mobageを支える技術 ~ソーシャルゲームの舞台裏~ (WEB+DB PRESS plus)

  • わくわくしながら読める一冊ですね

Part1

Part2

  • スレーブへの参照分散は、スレーブサーバのホスト名をDNSラウンドロビン
  • DNSはMyDNSで構成変更にも柔軟に対応可能
  • 非同期処理としてのジョブキュー
  • FastCGI
    • マルチプロセスのfork処理軽くない

  • ipvsadmコマンド
    • LVSの管理インターフェース

  • httpdとソケットでFastCGI複数接続
  • Server::Starter Hot Deploy用モジュール
    • サーバーの再起動中もリクエストの受信を継続する
    • プロセスforkにより、ソケットを更新前と更新後のプログラムで共有し、更新後のプログラムの起動が完了した時点で更新前のプログラムをシャットダウンする
    • c.f. Server::Starterから学ぶhot deployの仕組み - $shibayu36->blog;

  • mysqldのswapだけは避けたい
    • メモリの空き部分をファイルキャッシュが占めていた
    • クリーンなキャッシュであれば、Swapが起きる前に解放してくれればいいものを、Linuxではそうはなっていない
    • ファイルキャッシュの優先度を落とす、つまり早めに解放されるように指定するには、posix_fadvice()を使う
    • ファイルディスクリプタに対してPOSIX_FADV_DONTNEEDを使うことで、近いうちに再度アクセスはしないことをアドバイスできる(メモリに載せておく必要性が十分低いことをLinux Kernelに伝えることができる)
    • posix_fadvice()はfd単位なので、全体を対象としたいのであれば/proc/sys/vm/drop_caches(1〜3を指定)
      • クリーンなページを解放

  • NYTProf
    • Perlプロファイラ

  • innodb_flush_method

    • 指定無し
      • data : open(),write+fsync
      • log : open(),write+fsync
    • O_DSYNC
      • data : open(),write+fdatasync
      • log : open(O_SYNC),write(+fsync) (内部的にはwriteしたときに同時にfsyncされるのと同じ)
    • O_DIRECT
      • data : open(O_DIRECT),write(ファイルシステムの機能を使わない書き込み)
      • log : open(),write+fdatasync

  • スレーブの再起動直後はバッファプールが空のため、徐々に参照頻度を上げていくような運用にしたい

  • ストアドプロシージャなどでDBへのネットワークアクセスを一回でも少なくする
  • マスタの更新は並列だが、レプリケーションSQLスレッド(スレーブ上でリレーログを適用するスレッド)が単一スレッドのためレプリケーション遅延が発生する
  • SHOW SLAVE STATUS Seconds_Behind_Master は秒単位(繰り上げ)
  • slave net timeout
    • マスタの時刻をテーブルに登録することで、マスタの時刻をレプリケーションによってスレーブに通知することができ、その値とスレーブの時刻との差分を確認することで、時刻差を認識できる

  • スロークエリログは実行中は出力されない、実行完了後に出力される
  • 実行中のスロークエリはSHOW FULL PROCESSLISTで確認できる
  • Perconaのmk-killでKillだけじゃなくその出力が可能
  • mysqladmin extended-status
  • vmstatコマンド
    • biがRead(ブロックデバイスから受け取ったブロック)で、boがWrite(ブロックデバイスに送ったブロック)
    • iostatコマンドというのもある

  • mutexが起こった時点のスタックトレースを取って、MySQLソースコードに踏み込んで調べる
  • LOAD DATAやALTER TABLEは処理が重いため、一時的にバイナリログを無効にし、マスター・スレーブを一つずつ実行していく
  • バイナリログのRow formatは容量大きいが、SQLを実行しないので構文解析や実行計画が省ける
  • PCIeのSSDは高価で高速
  • マスタの更新処理は並列なのでHDDで大丈夫
  • サービスでは使わない低スペックなスレーブを立てておいて、そのスレーブで早めに遅延を検知して、問題が大きくなる前に対処する
  • 5.6からはデータベース毎にSQLスレッドが立てられる
  • プリフェッチの効果
  • レプリケーション遅延が許容できないときは、参照もマスタへ向けてみる
  • DeNAの開発環境ではレプリケーションにスリープ処理をあえて入れて、運用環境に近づけた状態で開発を行っている
  • スレーブの三層構造では、二層目がクラッシュしたときに、CHANGE MASTER TO でマスタのバイナリログのどの位置かを指定できない
    • これはグローバルトランザクションID(GTID)で解消できるだろう
    • GTIDは、全てのトランザクションに与えられたユニークなIDであり、バイナリログに全てのGTIDが保存される(GTIDはその範囲が表現できるようになっている)

  • 安いSSDプチフリーズに注意
  • pdflush
  • MySQLのバイナリログとリレーログはフラッシュしない
    • sync_binlog=1であればコミット毎にバイナリログがフラッシュ(fdatasync)されるが負荷が高い
    • sync_binlog=0はフラッシュのタイミングをOSにまかせる
  • xfs
    • B+Tree構造の情報管理による検索高速化
    • エクステントを単位としたブロック管理による、シーケンシャルな読み込み・書き込み性能の向上
    • 遅延アロケーションによる、write()の高速化・フラグメンテーションの回避

  • mmap
  • メモリに乗り切っているときはCPUバウンド
    • 構文解析や実行計画、テーブルのオープンクローズの処理が、相対的に無駄になる
    • そこでHandlerSocket
      • MySQL Clusterのコンセプトを踏襲
      • MySQLをNoSQLっぽく使う
    • 単純なレコードキャッシュへの参照はmemcachedよりもHandlerSocket

  • MySQLの書き込みスケールアウトは、アプリケーションの中でデータベースを分けておく
  • ジョインはシャーディングキーが同じ(つまり保存先のデータベースインスタンスが同じ)レコード同士であればこの場合でも可能
  • 剰余方式のシャーディングであっても、冪乗でノードを増やしていけば既存シャードは不変のままスケールアウトが可能
  • コンシステントハッシュ方式がマッピングの変更が少なく有利
  • シャード追加時のマッピングの変更の即時性はマッピングテーブル方式が有利
  • MySQL Cluster, MongoDB, Membase は分散型データベースの自動シャーディング
    • ただし、リシャード(マッピングの変更によるデータのノード間移行)がなくなるわけではないので影響の検証は必要

  • MHA for MySQLによる高速マスターフェイルオーバー
    • ダウンタイム1秒前後!

  • 一回のリクエストで複数のマスターへ更新することが多い
  • 一部のマスターがフェイルオーバーしていて、かつロールバックするようなことになれば不整合となる
    • サービス面から考えれば、不整合が発生してもユーザー側に有利になる順序でアプリケーショントランザクションを組んでおく必要がある

  • 名前解決のキャッシュによりTTLの期限が切れるまで、IPが変更になるまでのタイムラグを回避するために、新旧のマスタのIPをDNSラウンドロビンとして登録
    • 切り替えるまでは新マスタにユーザーを作らない?ユーザを削除する?(よくわからなかった...)
    • max_connectionsを1にする
    • SET GLOBAL read_only=1は実行中のセッションの完了を待つ

  • マスタ障害時の自動フェイルオーバー
    • もっとも進んだスレーブのリレーログを全てに反映する
    • マスターと同期されていない場合はロストの可能性あり

  • リレーログがトランザクションの途中で終わっていることがある
  • その場合はコミット or ロールバックしてやる必要がある(全てのスレーブで実行位置をそろえる)
  • マスターが死んだことを正確に検出しなければいけない
    • もし自動フェイルオーバーしたあとでマスターが生きていたとなると、スプリットブレインとなり事態は深刻になる
    • MHAではデフォルト3秒定期で接続確認、3回以上接続不能でフェイルオーバー実行
    • 複数のネットワーク経由で確認した結果を採用するべき
    • フェイルオーバーしたらマスターの電源を別の方法(ipmitoolなど)で落とす

Part3

  • Admintool catalyst
    • dhcpdのleasesログからサーバのシリアル番号と発行されたIPアドレスを合わせてAdmintoolに登録
    • GNU parallel, diskleas Linux, system-config-netboot-cmd, pxeos ...

  • 故障でいうと、HDDが一番多い
    • HDDのメディアエラーの数を監視して、問題が顕在化する前に検知する
    • RAIDコントローラはBBWC(バッテリー)を長持ちさせるために、BBWCを一度揮発させて再充電する処理が数ヶ月に一回程度発生する
    • このときはディスク上のキャッシュが効かずwrite throughになるのでIO性能が急に落ちる

  • アプリケーションサーバの監視はポートのチェックだけでなく、アプリケーションの機能が動いているかまでチェックする
  • 可用性を高めるためにヘルスチェックのみNGにできる機能をつけた
    • ロードバランサの設定変更なしでサービスアウトが可能

  • DSR構成は、ロードバランサの仮想IPをサーバのループバックインタフェースにも設定し、ロードバランサは宛先MACアドレスでサーバを割り振る
  • スパイクに対する閾値は、時間帯によりオフセットを変える必要がある
    • 統計解析的なアプローチが必要

  • データベースを分けて分散することを垂直分割とよぶ
    • JOINに制限がかかることになる

  • 水平分割はレンジやハッシュで分けることで、これがシャーディング

  • mock化, fixture, MySQL::Sandbox...
  • SIGNAL/RESIGNAL, ストアドプロシージャ
    • トリガやイベントスケジューラから呼び出せる
SET GLOBAL event_scheduler = ON
CREATE EVENT XXX ON SCHEDULE EVERY @interval HOUR STARTS @time
DO BEGIN
...
END
  • イベントスケジューラの注意点
    • エラーハンドリングができない
    • バイナリログに書かれる
    • SLAVESIDE_DISABLED

  • memcached udf
  • OAuth
  • Facebook Graph API Explorer
  • オーバーロードPOST
  • TheSchwarts
    • Perlのジョブキューライブラリ、キューの処理結果をクライアントに返すことはない

  • Q4M
    • MySQLのストレージエンジン
    • INSERTはキューへのPUSHであり、queue_wait()しているワーカーがいればPOPされる
    • queue_wait() -> オーナーモードへ、キューが空ならデフォルト60秒ブロックして待つ、返る値は指定したテーブルのインデックス値
    • SELECT -> 先頭POP (ここで削除はしない)
    • queue_end() -> 先頭削除
    • オーナーモードでのINSERTは可能?

  • Pararell::Preforkモジュール
  • Perlのeval-$@はtry-catch構文です
  • キューの処理に失敗した場合、その処理は異常終了となるだけ
    • 少し遅延させたあとリトライするといったことができない
    • そこでexponential back off
    • 遅延時間を冪乗で増やしていくからexponential

  • Perlの%hash+ +{...}は、波括弧でくくった部分がハッシュのリファレンスであることを明示するためのものです
  • scalar は引数を強制的にスカラコンテキストで解釈してスカラ値を返す(配列に対して使うとlengthと同じ意味)ものです
  • Prefork workflow(hook point) : before_fork -> start -> after_fork -> on_child_reap
  • Memcached
    • Memcachedのデフォルトの接続タイムアウトは250msec
    • Memcachedをローカルに持つことで、キャッシュのキャッシュとして使う
    • ローカル限定のソケット経由のアクセスは、Unixドメインソケットを使う方が高性能
    • フォールバックフローとして、local -> cache miss -> master
    • Perlのcallbackは、subをスカラとして渡し、$func->()として呼び出すものです
  • Slab allocator
  • データの更新がある場合もlocalのexpiresを1秒など極端に短くしておくことでmasterを更新するだけですべてのサーバに1秒後には反映される?(よくわからなかった...)
  • DNSパケットはUDP
    • トラヒックが増えてくるとパケロスが多くなる
    • 再送遅延デフォルトの5秒以上、アプリサーバで待つようになった
    • 回避策として、resolve.confにoptions timeout:1 attempts:2を設定
    • より効率的に回避するには、ルックアップを減らす必要がある
    • そのために名前解決にもmemcachedを活用
    • FQDNをキーとして解決後のIPを保持

  • UserAgentモジュールのかわりにFurl
    • DNSルックアップに任意の関数使える

  • リスト11.13のコード
  • DNSエントリーキャッシュの効率的活用
    • プロセスごとにキャッシュを持っており、別のプロセスがDNSエントリーをキャッシュに持っていても活用できない
    • これを独自inet_atonで解決する
    • つまりローカルmemcachedでプロセス間のキャッシュ共有ができる
    • DNSエントリーのTTLはそのままmemcachedのexpireになる
    • デフォルトのinet_atonは最後の手段としてフォールバック

  • ENGINE=MEMORY
    • テーブルスキーマがなく、KVSのような使い方ができる

  • クエリを集約して減らす
    • 例えば友達リスト100件を20件づつ、アプリテーブルによるフィルタリングにかけていくと、結果として20より少なくなり、例えば10件になる
    • ここでこの10件に対してクエリを実行するよりも、次のループに持ち越して集約した上でクエリを発行した方が良い
    • 例えばインデックスが5でカウントが10の場合、10は[5,15]に収まるので、ループを続ける
    • こうやってフィルタリングした結果を次のフィルタリング処理に渡すことで、発行するクエリ回数を減らすことができる
    • このデザインはGroupedRangeを使って直感的に実装できる
      • GroupedRangeの結果(つまりnextの結果)は、配列リファレンスがrange数分になるかundefになる
      • undefで終了となる
      • return;はundefを返す

  • DBが別のため、アプリ経由で各DBにクエリ発行(ジョインが使えないため)
  • ただし、結局フィルタリングするものだし、何万件もあるデータをアプリのメモリに載せたくない
  • fetch_arrayrefを使えばやってくれる
    • 呼ぶごとに所望の件数取得する
    • 内部処理としてはSQLは一回で全件の結果をlibmysqlclientに持たせておいて少しづつPerlコンテキストにコピーしている

  • memcached get_multi
  • $filtered_ids = []; 配列リファレンスはスカラです
  • バグ早期発見のためのアプリケーション監視
    • GrowthForecast
    • この時点だとFluentdは初期
    • Push型のリソース監視が望まれている、とのこと
      • mackerelですね

  • Data::Dumperの変数に任意の値を指定することで表示方法をカスタマイズできる
  • ログは1出力を1行にすること
  • Komainu
  • ShipIt
    • テスト実行からタグ切りなど

  • Archer