こんにちは、SREの菅原です。
クックパッドの多くのシステムは AWS 上で稼動しており、そのWebサービスの多くはデータベースにAmazon RDSを使っています。
WebサービスがDBを使う場合、ボトルネックになりやすいDBのパフォーマンスを落とさないためにスロークエリの監視はとても重要です。そこで、Amazon Elasticsearch Serviceを使ったスロークエリの集計・監視システムを構築したので、それについて紹介したいと思います。
※今のところMySQLエンジンのみを対象としています
システム構成
システムの構成は以下のようになります。
また、社内のシステムと完全に同じ訳ではありませんが、同様の構成のSAMプロジェクト(Elasticsearch Serviceに保存するまでの部分)をGitHubで公開しています。
https://github.com/winebarrel/sam-rds-slowquery-to-es
Elasticsearch Service使ったスロークエリの集計はよくある構成ですが
- pt-fingerprintでクエリを正規化して集計しやすくしている
- Elasticsearch Service(Open Distro)のAlerting機能を使って、スロークエリが発生したときにアラートを出すようにしている
- Alertingの設定をGitHubでコードとして管理している
などといったあたりが他のシステムには見られない部分だと思います。
pt-fingerprintを使ったクエリの正規化
「どのようなクエリに時間がかかっているか」「件数が多いのはどのクエリか」などを集計しようと思うと、クエリを正規化して値やフォーマットだけ違うようなクエリも同じものとして扱える必要があります。
mysqldumpslowやpt-query-digestなどのツールを使うとクエリを自動的に正規化して集計してくれますが、Elasticsearchにはそのような機能がないため、Elasticsearchへクエリの投入を行うLambdaファンクション内でpt-fingerprintを実行して、クエリを正規化しています。
クエリを正規化することで、Kibana上でpt-query-digestのようなダッシュボードを作成することができます。
また、クエリにはメールアドレスなどのセンシティブな情報が含まれることもあるため、そのような情報をマスクしてElasticsearchから見られないようにするという意味もあります。
Alerting機能を使った監視と設定の管理
Amazon Elasticsearch Service(Open Distro)には、オリジナルのElasticsearchのX-Packとは別に独自のAlerting機能が使えるようになっています。
Alerting機能を使うと、単位時間あたりのスロークエリの発生件数が閾値を超えた場合にSlackなどにアラートを通知することができます。
このシステムではさらに、Alertingの設定ファイルをGitHubでコードとして管理して、マージされた場合に自動的にElasticsearch Serviceの設定を変更するようにしました。
これにより、モニターの作成や閾値などの変更を容易にすることができました。
設定ファイルは以下のように記述されます。
local action = import '../lib/action.libsonnet'; { type: 'monitor', name: 'my-service', schema_version: 1, enabled: true, schedule: { period: { interval: 1, unit: 'HOURS', }, }, inputs: [ { search: { indices: [ 'aws_rds_cluster_my-service_slowquery-*', ], query: { size: 0, query: { bool: { filter: [ { range: { timestamp: { from: '{{period_end}}||-1h', to: '{{period_end}}', include_lower: true, include_upper: true, format: 'epoch_millis', boost: 1, }, }, }, { bool: { must_not: [ { term: { 'log_stream.keyword': { // バッチ用のDBでサービスには影響が出ないため、このDBへのクエリは無視する value: 'db-batch-001', boost: 1, }, }, }, { term: { 'sql_fingerprint_hash.keyword': { // すぐに修正することが難しいため、このハッシュ値のクエリはいったん無視する // see http://github.com/cookpad/my-service/issue/123 value: 'a43f9b2b1800fc8aa09bbcddcf63eab445b5af87', boost: 1, }, }, }, ], adjust_pure_negative: true, boost: 1, }, }, ], adjust_pure_negative: true, boost: 1, }, }, aggregations: {}, }, }, }, ], triggers: [ { name: 'slowquery-trigger', severity: '1', condition: { script: { source: 'ctx.results[0].hits.total.value > 10', lang: 'painless', }, }, actions: [ action.slowqueryNotifier( 'ap-northeast-1', 'cluster:my-service', std.join( ' AND ', [ 'identifier.keyword:my-service', 'NOT log_stream.keyword:db-batch-001', 'NOT sql_fingerprint_hash.keyword:a43f9b2b1800fc8aa09bbcddcf63eab445b5af87', ] ) ), ], }, ], }
※詳しい書き方についてはOpen Distroのドキュメントを参照してください
Alerting設定はJsonnetで定義され、CodeBuildからElasticsearch ServiceにポストするときにJSONに変換されます。
スロークエリはアラートでは「特定のクエリを無視したい」(例: 深夜の実行でサービスへの影響が少ない・すぐの対応が難しいクエリは無視)「特定のサーバへのクエリを無視したい」(例: バッチ用サーバへのクエリは無視)などといったことがあるので、モニタリング対象の条件にクエリのハッシュ値やサーバ名などを指定できるようにして、ノイズとなるアラートが上がらないようにしています。
そのほかに工夫した点
Lambda上でpt-fingerprintの実行
LambdaではRubyランタイム上でRubyのスクリプトを動かしているのですが、PerlのData::Dumperモジュールが含まれておらず、そのままの状態でpt-fingerprintを動かすことはできませんでした。そのため、pt-fingerprintに以下のようなパッチを適用して、Data::Dumperモジュールを利用しないようにしています。
https://github.com/winebarrel/sam-rds-slowquery-to-es/blob/master/pt-fingerprint.patch
Data::Dumperモジュールはデバッグ出力としての利用だけなので、この変更による動作への影響はないと考えています。
断片的なSQLの無視
CloudWatch Logsに出力されるスロークエリは、基本的に1メッセージに対して 1 つのクエリのログが出力されますが、まれに非常に長いクエリが複数のメッセージにまたがって出力されることがあります。 これを正しくパースするためには分割されたメッセージを一時的にDynamoDBなどに保存し、後続のメッセージがきたタイミングで他の断片と結合してパースする必要があります。
しかし、正しくパースしようとするとシステムが複雑になり運用のコストが上がってしまうため、このシステムでは単純に無視するようにしています。
rdsadminユーザの無視
RDS(MySQL)ではlog_queries_not_using_indexesを有効にすることで、実行時間にかかわらずインデックスを使用していないクエリをスローログに出力することができます。 log_queries_not_using_indexesをただ有効にしただけだと大量のスロークエリが出力されてノイズになってしまうので、min_examined_row_limitを同時に設定することで走査行数の少ないクエリが出力されるのを抑止することができます。
これらの設定を一部のRDSで有効にしたところ、rdsadminユーザによるスロークエリが大量に出力されるようになってしましました。
これは min_examined_row_limitのスコープが「セッション」であるため、既存の接続が切れない限りセッションが維持されており、新しいセッション変数が使われないことが原因でした。 rdsadminユーザはRDS側のシステムが利用するユーザであり、AWS 利用者側からは操作できません。そのため、接続を切る方法をサポートに問い合わせたところ、再起動以外の方法でrdsadminユーザの接続を切る方法はないとのことで、ワークアラウンドとしてrdsadminユーザのスロークエリは無視するようにしました。
rdsadminユーザがスロークエリを発行する可能性がないわけではないので、接続が切れるタイミングがあれば、無視する処理はなくしたいと考えています。
まとめ
RDSのスロークエリをElasticsearch Serviceに流すことで、わかりやすくダッシュボードにまとめることができ、また、アラートの設定によりスロークエリの増大を素早く気づけるようになったと思います。
じわじわと増え続けるスロークエリはすぐにサービスに影響を出すものではないため見落とされがちなのですが、突発的なアクセスの増大などがあると簡単にサービスをダウンさせてしまうものなので、このシステムでスロークエリを減らしていきたいです。