ISUCON 14 に id:utgwkk, id:wass80 と「ミレニアムサイエンススクール」というチーム名で参加した。
結果は 28875 点で 22 位!去年は fail して最下位だったので反省を活かすことができたのでは?
一昨年に Kibana / Elasticsearch / Filebeat で解析環境を組んでサクセスしていたが、今年は OpenTelemetry (Otel) をベースにモニタリング環境を作り直し、バックエンドは ClickHouse、フロントは Grafana の構成でやってみた。これがなかなか良かったので紹介。
コンテスト当日のアプリ改善周りは utgwkk の記事を見てください。
序: ISUCON で必要な計測
ISUCON ではパフォーマンスを継続的に確認してボトルネックを探しながら改善していくのが王道ルート。 インスタンスの CPU 使用率や RAM 利用量といった一般的なメトリクスについてはベンチ中に netdata なり htop なりを眺めていれば良いが、それだけではアプリケーション内部や各 API パスの具体的な負荷状況は把握できない。
そこで、 Nginx のアクセスログや MySQL のスローログ、さらにはアプリケーションの Trace を出力して集計することが重要になる。
有史以前からこの手のツールはいろいろあり、CLI だと kataribe、pt-query-digest あたりが有名なところ。
そんな中、Nginx のアクセスログや pt-query-digest を常時集計してメトリクス化し、ダッシュボードで一元的に表示できるようにしたのが一昨年の記事だった。
またこれで組んでもいいのだけど、 適当に投入しまくって適当に使うと若干重かったり、クエリが手でぱっと書けなかったり…といった Kibana / Elasticsearch 特有のめんどくささがある(あと飽きた)のも事実なので、今回は別の構成を試してみることにした。
ClickHouse と OpenTelemetry と Grafana でサクセス
Kibana は Elasticsearch と密に紐ついているので、Elasticsearch をやめるとなると Grafana など他の可視化ツールを使うことになる。
いろいろ探していたところ、Grafana と ClickHouse の組み合わせが良さそうということで使ってみた。
また、Nginx 公式の otel module があることを発見したので、これを機に Opentelemetry (Otel) を使って Trace も(Google Cloud Trace ではなく)自力で取り込んでみることにした。
ClickHouse
ClickHouse はもともと Yandex で作られていたものが OSS になり企業としてもスピンアウトしたものだそうで、カラム志向のデータ構造をした OLAP 向けデータウェアハウスとのこと。
Observability についても力が入っており(確かにモニタリングは OLAP の一種と言えそう)、Otel でログや Trace を ClickHouse に流し込み、Grafana の公式プラグインを使って Trace / Metrics / Timeseries の可視化ができる。
各種データの操作は SQL ベースの言語で行う。 Observability の文脈で SQL というのはなかなか珍しいが、Prometheus のわけのわからないクエリよりも100倍分かりやすくて良いと思う*1。
また、データウェアハウスらしくデータの様々な加工・集計・取り出しが高速に行えるので、 Otel の Processor で行うようなデータの前処理・ Grafana で行うような後処理を ClickHouse で完結できるのも良いポイント *2。
立てる
まずは ClickHouse を立てる。今回は公式 Docker image を Kubernetes 上で動かした。MySQL を動かすのと大体同じ感覚。
ユーザーを作る
デフォルトで作られる default
ユーザーは権限最強で危険なので、Grafana と OpenTelemetry Collector (otelcol) 用のユーザーをそれぞれ用意する。
-- Grafana user CREATE USER IF NOT EXISTS grafana IDENTIFIED BY 'password' SETTINGS max_execution_time CHANGEABLE_IN_READONLY, PROFILE `readonly`; GRANT SELECT, SHOW ON *.* TO grafana; -- otel_collector CREATE USER IF NOT EXISTS otel_collector IDENTIFIED BY 'password'; CREATE DATABASE IF NOT EXISTS otel; GRANT CREATE, INSERT, SELECT, dictGet ON otel.* TO otel_collector;
otelcol 経由でデータを投入する
otelcol-contrib は Exporter(送信先)として ClickHouse を設定できる。以下を参考にして otelcol-contrib の設定を行う。
設定して otelcol-contrib を立ち上げると、otel 用の Table を自動的に ClickHouse 側に作成してくれる。
SHOW TABLES Query id: 4a180332-e499-492d-917b-b61ea8e342bb ┌─name──────────────────────────────────┐ 5. │ otel_logs │ 6. │ otel_metrics_exponential_histogram │ 7. │ otel_metrics_gauge │ 8. │ otel_metrics_histogram │ 9. │ otel_metrics_sum │ 10. │ otel_metrics_summary │ 11. │ otel_traces │ 16. │ otel_traces_trace_id_ts │ 17. │ otel_traces_trace_id_ts_mv │ └───────────────────────────────────────┘
その後 otelcol で journald やファイルを読んだり、Trace を受信すると Clickhouse に各種データが投入され、Grafana で確認できる。
Trace も Nginx から送ってみる。Nginx 公式 apt repo から Nginx と otel module を入れ、 otelcol に送るように設定することで簡単に Trace を見ることができた。
スクリーンショットに Trace を取得するための SQL クエリが見えているが、このクエリは Grafana の流儀に合わせてデータを取り出す必要があるのでちょっと複雑。
ただ、基本的なクエリは Grafana ClickHouse Plugin の Query Builder で十分対応できるので直接 1 から書くことはない。Percentile など混み入った関数を使った集計やフィルタをしたくなったら、クエリを適宜編集することになる。
Materialized view を作成する
ドキュメントにも書いてあるが、otelcol によって自動的に作られた Table をそのまま利用することは推奨されていない。というのも、データ構造や貼られている Index は最低限のものであり、実際の利用に適したものにしたほうが効率が良いため。
これは前処理をしっかりしてから入れましょうね、という話ではなく、ClickHouse ではデータはそのまま入れて ClickHouse 側で処理を行うことが推奨されており、その処理を自分に都合の良い Schema でやりましょう、という話である。
We recommend users avoid doing excessive event processing using operators or transform processors. These can incur considerable memory and CPU overhead, especially JSON parsing. It is possible to do all processing in ClickHouse at insert time with materialized views and columns with some exceptions - specifically, context-aware enrichment e.g. adding of k8s metadata. For more details see Extracting structure with SQL.
...
We recommend users disable auto schema creation and create their tables manually. This allows modification of the primary and secondary keys, as well as the opportunity to introduce additional columns for optimizing query performance. For further details see Schema design.
自分で Otel 用の Table から再定義してもいいが、今回は 面倒なので 自動作成される otel_* Table 自体は取り込んだ Trace / ログをそのまま保存する Table として触らずにしておき、 Nginx trace / pt-query-digest log / App trace / App log を集計する際はそれぞれ専用の Table を作成して利用することにした*3。これは ClickHouse の Materialized View 機能を使うことで実現できる。
ClickHouse で Materialized view を使うと、ソースとなるテーブルに Insert があった時にそれを元に加工した行を別の Table に Insert することができる。実質的にデータの前処理の役割を果たし、参照時は高いパフォーマンスで参照することができる。
たとえば Nginx の Trace 専用の Table (nginx_accesses
テーブル) を作り、取り込んだデータ (otel_traces
テーブル) を元にしてその Table に Insert するには以下のように設定する。
CREATE TABLE IF NOT EXISTS nginx_accesses ( Timestamp DateTime64(9) CODEC(Delta, ZSTD(1)), TraceId String CODEC(ZSTD(1)), Operation LowCardinality(String) CODEC(ZSTD(1)), Duration UInt64 CODEC(ZSTD(1)), HTTPStatusCode LowCardinality(String) CODEC(ZSTD(1)), HTTPMethod LowCardinality(String) CODEC(ZSTD(1)), HTTPPath String CODEC(ZSTD(1)), ) ENGINE = MergeTree() PARTITION BY toDate(Timestamp) ORDER BY (HTTPMethod, Operation, toDateTime(Timestamp)); CREATE MATERIALIZED VIEW IF NOT EXISTS nginx_accesses_mv TO nginx_accesses AS SELECT Timestamp, TraceId, concat(SpanAttributes['http.method'], ' ', dictGet("path_regexp_dict", ('normalized'), SpanName)) AS Operation, # パス正規化用 Duration, SpanAttributes['http.status_code'] as HTTPStatusCode, SpanAttributes['http.method'] as HTTPMethod, SpanAttributes['http.target'] as HTTPPath FROM otel_traces WHERE ServiceName = 'nginx';
注: Materialized view は、作成に既にあるデータは反映されない(反映してくれる POPULATE
オプションがあるが、上記のようなケースでは使えない)。そのため、既存のデータを使うには Materialized view と同等のクエリを使って SELECT し、宛先 Table に既存のデータを Insert する必要がある。
Materialized view を変更するたびに宛先の Table の内容を変更しなおすのも面倒なので、変更時は Table / View ごと作り直すようにした。
構文としては SQL 的なので一度慣れたら簡単で、コンテスト中も App(Go で書いていたので Chi)のログに合わせて App ログ用のテーブルを作って otel_logs テーブルから流し込んだりしていた。
Grafana で可視化する
こうして Log も Trace も全部 Otel で ClickHouse に突っ込んで、ClickHouse 内で使いやすい形に整形し、Grafana でクエリして表示できるようになった。
例えばパスごとの p90 を上位から集計してテーブルで表示する(↑における右下のパネル)ためのクエリは以下のようになる。
SELECT Operation, quantile(0.9)(Duration) FROM "otel"."nginx_accesses" WHERE ( Timestamp >= $__fromTime AND Timestamp <= $__toTime ) GROUP BY Operation ORDER BY quantile(0.9)(Duration) DESC LIMIT 10000;
(ClickHouse とは少しずれるが) 今回は Nginx の層で Otel を有効にして Trace id をつけているので、 Nginx のダッシュボードから Trace を辿ると App の Trace も一緒に確認できて便利だった。
前回同様、pt-query-digest も30 秒ごとに結果を集計してリアルタイムで出すようにした。下の記事では Filebeat などで前処理を頑張っているが、今回は Otel から ClickHouse に生ログを流し、ClickHouse 側で JSON をパースするようにしている。
otelcol 側で処理を行わないので、競技インスタンスの負荷も少なく、ベンチマーク中もリアルタイムでリクエストの状況を見ることができた。また、リクエストパスの正規化など前処理の変更があっても各インスタンス上の otelcol を触る必要がないのも楽だった。
Trace もログも同じ Datasource に統一された形で入っているので、それぞれ ClickHouse でパースしてメトリクス的に利用しやすいのも良いポイント。今回はやっていないが、 Trace と Logs を関連づけるのもできそう。
パフォーマンスも申し分なく、時間を広げたり変更してもキビキビ動いてくれた。 Elasticsearch / Kibana のときよりも少ないリソースだったがより快適なモニタリング環境ができたと思う。
その他の点
- ClickHouse
- CLI の出来が良い。補完機能や履歴検索機能などよくある Shell にある機能が充実している。
- エラーログがわかりやすい。 CLI でクエリを打っているときもそうだし、otel でデータを投入するときも権限の不足などエラーの原因がすぐにわかった。
- 概念周りのドキュメントが分かりやすくて充実している。例えば上記に挙げた otel の記事を読むと otel の概要も把握できて便利。ただリファレンス部分は若干機能の追加に追いついてない部分があったりした。
- Elasticsearch でも Ingest pipeline で同じような前処理ができるが、設定のやりやすさがかなり違うと思う
- もちろん万能ではなく、何も考えずに数百万件のログをスキャンして Order by するようなクエリを打つと時間がかかったり RAM が 4GB でも不足したりする。ほとんど困ることはなかったが、適度に Schema / クエリ設計は行うことが求められる。
- Nginx が Otel を吐き出してくれるのがかなり楽だった。ログを解析すると regexp を頑張って書かないといけないので。
なかなか良いものができたと思うので、来年も活用していきたい。Grafana に感じていた壁も解消された気がする。
ISUCON 14 当日の動き
- いつもようにサーバー周りを色々やっていた。 MySQL の isucon user のホスト部がはじめから
%
になっていて感動した。 - プロファイリングは今までと同様 Google Cloud Profiler を利用した。無料で優秀。
- アプリはあまり手を入れていなかったが、終盤に
GET /api/owner/charts
のキャッシュ改善やクエリ改善を少し行っていた。数千点上がったのでヨシ! - デプロイスクリプトで
ISUCON_MATCHING_INTERVAL
周りをちゃんとケアすることができずにトラブってしまったので反省。 - 終了 20 分ほど前にほどほどにいい感じのスコアが出たので、フリーズしてあとはチームメイトに ClickHouse の紹介などをしていた余裕のある回だった。なおスローログもアクセスログも切り忘れたし Nginx の Otel 出力もそのままにしていました。
ということで今回の ISUCON も楽しかった。最近は Observability 関連の知らない技術を触ってみる機会にもなっていて、 Otel 自体もほぼ初めてちゃんと触ることができたのでよかった(Trace しかできないと思っていた)。
問題もしっかり作り込まれていて良かったです。ベンチなどの環境も非常に快適でした。チームのメンバー、そして運営のみなさん、ありがとうございました!