サービス品質の改善効率を高める仕組み

最近うぇぶ業界では、開発効率や構築効率を求める動きが活発のように見受けられますが、ここで改善効率について手を伸ばしてみましょう。

改善効率とは、開発後期やサービス開始後の運用フェーズにおいて、クソコードやクソクエリ、データの蓄積によるレスポンスの悪化などを、自動的に検知し、開発者にオラオラ改修をプッシュするための仕組みのことでございます。



はじめに

ここで紹介する内容はドリコムで実際に運用しているものですが、別にドヤ顔するようなものではなく、中規模以上の企業ならば似たようなことやそれ以上のことをやっているであろう、至極当然な内容です。それでも、それなりに種類が増えてきたことと、それなりの効果を得られていることが実感できているため、いったんまとめてみようと思った次第です。

ウチのサービスのサーバーサイドは Ruby on Rails + MySQL が基本なので、その対策手法になります。WEBアプリケーションはデータを扱ってなんぼなので、プログラミング言語のコーディング と データベースのクエリ の重要性は半々だと思っているのですが、時代的にフロントエンドも忙しいなど相まって、データベースの扱い方がないがしろになりがちです。そのため、レスポンス速度という視点におけるサービス品質の低下の原因の大半が、データベースのクソスキーマ設計やクソクエリであるのが実情です。

そういった品質の低下をいかに手間を掛けずに防ぐか、を課題に色々やってきました。中には自作システムもあり、そのものは公開しないのですが、そこは手法論だけでも紹介したいと思います。


グラフとアラートによる監視

これは当然、何年も前から仕込んでいますが、監視はとりかからなくてはいけないのがわかるだけで、ピンポイントでアプリケーションの改善ポイントがわかるわけではありません。主にミドルウェア~ハードウェアというレイヤーの、閾値設定によるアラートと、多種類グラフによる原因項目の特定によって、キャパシティーオーバーを防ぐために利用します。



使うソフトウェアは、アラート系はNagios, Zabbix、グラフ系はCacti, Ganglia, Collectd と色々ある中から好きなのを選べばいいです。選ぶ基準は、まずインターフェースの見た目。見なくなっては意味が無いので。それと サーバ⇒クライアント に取りにいくのではなく、クライアント⇒サーバ に送る構成であるものを推奨。台数が増えると、サーバ側でいちいち追加するより、クライアントでのChef自動構築に組み込めばいいだけの方が楽だからです。

ウチではCollectdで収集した情報をさらに独自インターフェースで閲覧できるように工夫しています。


Gitでソースコード管理

どちらかというと開発効率の方がメインですが、社内IRCでクソコードを指摘したり、直しました!⇒いいね! の流れを
URL貼り合うだけでスムーズに行えるのは、十分に改善の効率に貢献していると思います。

ちなみにウチは GitLab です。



NewRelicでアクション単位のコード

アプリケーションサーバにNewRelicを仕込むと、1つのHTTPリクエストの中で、何の処理を何回何秒かけて実行しているのかといったログをNewRelicサーバに送ってくれます。すると管理画面にてアクション毎の利用割合/処理時間のワーストランキングを見たり、1つ1つのトランザクションの処理内容を確認できます。


画像:とあるアクションのTraceDetail

これにより、特に時間がかかっているメソッドや、ループ回数が多い処理などがわかるので、悪い順、直しやすい順にリファクタリングしやすくなります。処理が軽いからとネットワークの往復時間を気にせずKVS接続をループしまくってたりしたら指導が入ります。


Sentryでアプリケーションエラー

アプリケーションのエラーログをSentryに集めて、エラーの種類順に多い方から改修したりします。案外おざなりになりがちなログなので、ちゃんと統計的に見れるとゼロに向けての直しがいもでますよね、と。


画像:とあるサービスのエラー数順表示などダッシュボード


画像:とあるエラーの詳細


MySQLのスロークエリログ

古典的ですが、1秒以上かかったクエリをログに残すように設定して、ログゼロを目指して改善。

これも普通に1箇所に集めて簡単に見れるようにすべきですけど、次の施策があるので重要度が下がりました。


MySQLのGeneralログ+EXPLAIN

これは外道父作・銘はGeneralistなのですが、1時間のうち5分間など general_log ON にして得た生クエリログを集計・ユニーク化し、さらにEXPLAIN結果を取得して、クエリごとのクオリティをポイント付するというおこがましいデータになります。アプリケーションは日々開発されていて、次々に新しいクエリが発行されるので、そういったクエリが極端に問題ないか、データの蓄積に対して問題が発生していないか、といったことをクエリ品質の可視化によって、手早く改善することができています。

ポイントは3種類用意しており、
  • DQP (DirtyQueryPoints) : 参照クエリの品質ポイントで多いほど悪い
  • DUP (DirtyUpdatePoints) : 更新クエリの品質ポイントで多いほど悪い
  • DIP (DirtyIndexPoints) : インデックスの品質ポイントで最大100で多いほど悪い

  • 参照クエリのポイント付の方法は、QPS値を基本値に、漢(オトコ)のコンピュータ道: MySQLのEXPLAINを徹底解説!! の項目ほぼ全て使って外道父が思ったままに掛算しています。rowsが多いほど、typeが非効率なほど、Extraの悪い項目が多いほど、ポイントを高くします。目安として数千ポイント以上になると改善対象となるようにしていますが、たまにクソクエリや管理画面クエリによってフリーザ様を超えると盛り上がったりします。


    画像:Extraが激しくて参照ポイントが高い例

    更新クエリは当時EXPLAINを打てなかったので無理矢理SELECTに置き換えて、rowsを取得し、更新INDEX列数を数えて、QPSを基本値にポイント付しています。結果的にポイントがグラフのIOPS値とほぼ同じになるという副産物があり、計算方法的にあたりまえだったかな、という後付自信作になっています。

    インデックスは SHOW INDEX から Cardinality を引っこ抜いて、非効率なインデックスカラムを指摘しています。例えば、2カラム目から3カラム目への絞込で1件も対象を減らせていないと 100ポイント、半分なら 50ポイント、という具合です。イメージとしては、インデックスつけるなら最低でも 1/5 以下に絞り込めるようにしようぜ、という感じです。5:95になってるbooleanとか5の方しか意味無いですし……でもそれが必要な時もあるので、それは人が判断するということで。無駄なインデックスやインデックスカラムを減らすと、容量と更新IOPSの削減につながるので重要な最適化になります。ただSHOW INDEXは重いので、遅いストレージだとサービスに若干影響が出かねないというのがネック。


    画像:3種ポイントをグラフ化して、クエリ品質の変化を可視化

    ……というのをMASTER/SLAVEの全サーバ毎に出していたのですが、それを社内の別の人間が集計インターフェースを作ってくれたりと、社内で勝手に派生サービスが作られる良システムになっています。ioDriveを採用する際に、必ずクエリの品質低下が起こるだろうことを想定して作ったもので、見事に役立ってくれています。


    NewRelicデータでアクション単位のクエリに自動改善案

    スロークエリやGeneralistは、クエリ単位の早期改善を目指したものですが、1HTTPリクエストの開始から終了までのクエリの一連の流れに対しても改善を施したくなりました。しかし、これらは取得ログがDBサーバ単位のものであり、アクション単位のクエリの流れを掴むことはできませんでした。DBサーバ上だと、どうしても全アプリサーバで実行されたクエリがごちゃまぜ時系列になったり、垂直・水平分散されたDB群では、あちこちにログがバラまかれてしまいます。アクション単位のクエリログは、アプリケーションサーバ上でしか作成できないのです。

    NewRelicはアクション単位で実際に実行されたクエリとともに、ループや実行時間を表示してくれるのですが、パッと見でここを直せばいい!という判断を的確にできる開発者はごく一部であるという現実がありました。


    画像:NewRelicはクエリをユニーク化して統計は出してくれる


    画像:さらにアクション単位でクエリの詳細を見れる

    で、一度私がここはこう、ここはこう変更すると良くなるかも!という指摘を手動でまとめて結果的に良くはなったのですが、サービスごとにスキーマと機能を把握して、本番データをテストサーバにリストアして、クエリをピックアップして、改善方法まで指摘……するのが面倒臭くなりました。

    そこで、NewRelicのデータを使って、SQLクエリの部分だけ引っこ抜いて、アクション単位で、ユニーク化したクエリの実行回数や、ループ状況、クエリの詳細を解析し、改善対象のクエリとその改善案まで自動指摘するインターフェースを用意しました。それが外道父作・銘はReliqRefactorです。

    改善案の例を上げるとこんな感じ。
  • 一行ずつループ処理している ⇒ IN を使う(SELECT, UPDATE, DELETE)
  • 一行ずつループで同じテーブルにINSERTしている ⇒ BULK INSERT する
  • SELECT ~ FOR UPDATE の後にUPDATEしてない ⇒ SELECTでロックしない
  • xxx_id条件でのCOUNTをループで何度もしている ⇒ IN と GROUP BY で1回で取得する
  • idで一行取得しているのにORDER BY id LIMITがついている ⇒ ORDER BY 以下いらない


  • 画像:基本情報に加えて改善案をピンポイントで指摘。ループ構造は2段階で検知して厳しくBULK化を指摘

    特にループの改善にはチカラを入れていて、コーディングではPRIMARY KEYで100ループする方が綺麗でも、1クエリ平均10msと仮定すると合計1秒になるわけです。かといって、なんでもかんでも全て一行クエリにまとめて処理にすればいいというわけではありません。開発現場的には、ソースコードのメンテナンス性の保持をいかに保持しつつ改良するか、データの増加に対する処理量の増加予測とその抑制、を常に考えながら改善してほしく思っています。

    指摘内容は、分岐やコーディングの都合もありますし、必ずしも正着なわけではないですが、DBAクラスから比較的データベースが苦手なエンジニアにまで広く役立ててもらっています。


    さいごに

    サービスの収益になるのは開発であり、改善は怠ると収益が落ちるものです。どちらも重要ですが、優先順位は当然開発が上なので、改善をいかに楽に実行できるかで組織のリソース配分に大きく影響してきます。サービスはサービスが終了するまで、機能の変化とデータの変化によって、常に品質が変わり続けていきますので、準備に多少時間がかかっても、品質把握を自動化することは長い目でみてとても大きなリソース節約になること間違いなしです。

    また、ここでは書きませんでしたが、テストデータ作成や負荷テストもかなり自動化していっています。

    はるか昔は、テストデータを作って、サイトを遷移してクエリ実行をし、ログ拾って、EXPLAINして、改修して…… を手で地道にやってたりしましたが、今ではテストデータから改善ポイント洗い出しまで多くの部分を自動化できて、本来の開発コーディングに注力してもらうことができています。

    クエリ品質の定量化や、クエリの自動改善案、といったものは最初は厳しいと思っていましたが、人間が順番に考えることはたいていシステム化できるものです。日々、手間暇かけさせられている作業に対して、常に自動化欲求を忘れずに生きていきましょう。そうしましょう。