Unyablog.

のにれんのブログ

dex と oauth2-proxy と ingress-nginx で OIDC ID token を取得して kubernetes-dashboad にログインする

Kubernetes では kubernetes-dashboard という各種リソースを見るためのダッシュボードが提供されている。

ブラウザ上で色々確認できるので便利なのだが、ログイン時は下のように自身の token か kubeconfig ファイルをアップロードする必要がある。

Kubernetes へのログインで Certificate や Token を使っている場合はそのままアップロードすればいいものの、 OIDC を設定したクラスタで OIDC 経由でログインしている場合は ID token を取得する必要があり、めんどくさい。

そこで、

  • oauth2-proxy で kubernetes 向けの OIDC ID token を dex から取得
  • ingress-nginx で ID token を Authorization header に Bearer token として付与し、 kubernetes-dashboard に送信

することでシームレスに見れるようにする。

dex

kubernetes が使っている client と同じ client id を使うので dex で新しく client を発行する必要はないが、 Callback URL を新たに登録する必要がある。

staticClients:
- id: kubernetes-client
  redirectURIs:
  - ...
  - 'https://{k8s dashboard domain}/oauth2/callback'
  ...

oauth2-proxy

oauth2-proxy は dex から id token を取得し、 ingress-nginx に Authorization Header として渡す役割を果たす。以下のような環境変数を設定すると良い。

      - name: OAUTH2_PROXY_PROVIDER
        value: oidc
      - name: OAUTH2_PROXY_OIDC_ISSUER_URL
        value: {OIDC issuer url}
      - name: OAUTH2_PROXY_COOKIE_DOMAINS
        value: {k8s dashboard domain}
      - name: OAUTH2_PROXY_WHITELIST_DOMAINS
        value:  {k8s dashboard domain}
      # Re-validate OAuth token per 1 hour
      - name: OAUTH2_PROXY_COOKIE_REFRESH
        value: 9m # id token の期限より短くする
      - name: OAUTH2_PROXY_SCOPE
        value: "openid profile groups offline_access email" # offline_access は refresh token の取得に使う。 email は oauth2-proxy の動作に必須
      - name: OAUTH2_PROXY_SET_AUTHORIZATION_HEADER
        value: "true"

ポイントは OAUTH2_PROXY_SET_AUTHORIZATION_HEADER で、これを true にすることで id token を Authorization Header として ingress-nginx に渡すことができる。

ingress-nginx

ingress-nginx は oauth2-proxy にリダイレクト(& reverse proxy)して Authorization Header を取得し、それを付けた上で kubernetes-dashboard に reverse proxy する。

構成としては、oauth2-proxy に reverse proxy するための認証がない Ingress と、kubernetes-dashboard に reverse proxy するための auth_request 付き Ingress を用意することになる。

oauth2-proxy 用 Ingress

oauth2-proxy の認証まわりの endpoint である /oauth2 配下を oauth2-proxy の service にわたす。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress
  annotations:
    kubernetes.io/ingress.class: "nginx"
    nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
spec:
  rules:
  - host: {k8s dashboard host}
    http:
      paths:
      - path: /oauth2/
        pathType: Prefix
        backend:
          service:
            name: kubernetes-dashboard-oauth2-proxy
            port:
              number: 80

kubernetes-dashboardIngress

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress
  annotations:
    kubernetes.io/ingress.class: "nginx"
    nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
    nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
    nginx.ingress.kubernetes.io/auth-response-headers: "Authorization"
    nginx.ingress.kubernetes.io/auth-url: "https://{k8s dashboard host}/oauth2/auth"
    nginx.ingress.kubernetes.io/auth-signin: "https://{k8s dashboard host}/oauth2/start?rd=$scheme://$host$escaped_request_uri"
spec:
  rules:
  - host: kubernetes-dashboard.kmc.gr.jp
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: kubernetes-dashboard
            port:
              number: 443

ポイントは nginx.ingress.kubernetes.io/auth-response-headers: "Authorization" を設定することで、これで oauth2-proxy から来た Authorization Header を Request Header に設定して kubernetes-dashboard にリクエストを送ってくれる。

これで、 kuberentes-dashboard へのリクエストに OIDC で認証された kubernetes において valid な Authorization Header が乗るようになり、ユーザーの権限で Kubenetes dashboard が閲覧・編集できるようになった*1

*1:最初の Token を求める画面は出なくなる

Filebeat の Nginx module でアクセス先パスを正規化する

今回の ISUCON では Filebeat を使って Nginx のログを Elasticsearch に放り込み、 Kibana ダッシュボードで表示していた。

nonylene.hatenablog.jp

なかなか役に立ってよかったものの、 /user/:userID/home のようなパスだと userID に様々な値が入ってしまい、総計では時間がかかっていても上位としてダッシュボードに出てこないという課題があった。

/user/4372904890324/home のようなパスに対しては /user/:userID/home といった正規化したフィールドを作ってから Elasticsearch に保存するようにしたい。

Ingest pipeline をいじる

Filebeat の nginx module ではパース処理は基本的に Elasticsearch の Ingest pipeline で行っており、 URL の情報は Ingest pipeline 側で初めてパースして見れるようになる。そのため、 URL に関した処理を行いたい場合は Ingest pipeline 内の URL パース処理の後に追加する必要がある(ずっと Filebeat 側に Processor を追加しようとして本番中も感想戦でもハマっていた…)。

Filebeat 関係の Ingest Pipeline は Filebeat が直接 Elasticsearch に登録するようになっており(filebeat setup --pipelines コマンドで可能)、その設定は /usr/share/filebeat/module/nginx/access/ingest/pipeline.yml にある。

このファイルには適用する Ingest pipeline の Processor (Filebeat の Processor とはまた別!)の設定が並んでおり、その末尾に正規化関係の処理を追加すれば良い。

...
processors:
...
- set:
    copy_from: url.path
    field: url_path_normalized
- gsub:
    field: url_path_normalized
    pattern: "^/user/[a-zA-Z0-9-_]+/gacha/index$"
    replacement: "/user/:userID/gacha/index"
... (以降正規化したいパスを記載していく)

編集したら filebeat setup --pipelines コマンドで Elasticsearch に Ingest pipeline を再登録する。登録された様子は Kibana の Management でも確認できる。

これで正規化後のパスが url_path_normalized フィールドに追加され、 /user/ 配下の API に時間がかかっていることが一目で分かるようになった。

めでたし。

ISUCON12 本選に出場した (:old_noto_innocent: Team)

ISUCON 12 本選に出場した。結果は 167492 点で 7 位(結果修正後)。そこそこかな?

isucon.net

アプリケーション周りは utgw の記事を見ると分かりやすいです

blog.utgw.net

予選の記事は↓。ブログ賞受賞とのことで、個人スポンサーグッズをもらえるらしい。ありがとうございます!ありがとうございます!

nonylene.hatenablog.jp

やったこと

  • いつものサーバー周り
  • Elastic Stack 関係いろいろ

    • nginx のパスをいい感じに aggreate して表示しようとしたが、競技中は無理だったので諦めた
      • (nginx のログをパースする) ingest pipeline が Elasticseach で処理されるのに気づいておらず、ひたすら Filebeat 側の処理を追加していた
    • Filebeat が Elasticsearch に送らなくなったので全てを再構築した
      • 結局 Filebeat 周りの設定ミスだったりファイルをうまくつかんでいなかったりが原因だった
    • 感想戦では MySQL 周りをブラッシュアップ nonylene.hatenablog.jp
  • バグ探しやレビュー

    • nginx のパス aggregate を諦めた後はサーバー側でやることがなくなったのでアプリを見ていた
    • utgw の記事で言及されている通り、確率的に落ちる不具合が終盤で発生していた
      • はじめは uid が int32 の範囲を超えたときに起きるベンチマーカー or アプリケーションのバグかと疑っていたが、結論としては DB を分割したことに起因して invalid な session が指定された時の挙動が 403 -> 401 に 2/3 の確率で変化したことが原因だった
      • Cloud Trace を眺めることで、エラーとなっているリクエストがだいたいどこまで動いているのかを把握できたことで、競技中のバグ発見につながったと思う。入れててよかったトレーシング
      • そして爆速修正してくれたチームメイトに感謝

感想

今回は5台あったのでどうなることやらと思ったが、結果的にはいつもの構成とそこまで変わらなかった。考慮する変数が多いのはサーバー担当としてはやりがいがあった。

予選でうまくいった Elastic Stack は、新しく nginx のパスの aggregate を行おうとして失敗したり、 MySQL のクエリが大きすぎてうまくハンドリングできないなど、いくつか課題があった。試行時間も割りと使ってしまったし、その時間があれば上位に行けたかもしれない。次回に向けてブラッシュアップしていきたい。

最近はほとんどアプリを見れていなかったけど、今回は割りとコードを読んでレビューしたり挙動確認したりできた。チームの最終スコアにもつながったのでよかった。

ただ様々な賞に一つも引っかからなかったのはくやしい!次回も頑張りましょう。

今年も ISUCON 楽しかったです。やはり本選は予選で得た反省や知見をすぐ試せるといった点でもいいですね。個人的にも興味のある新しい技術を触るいい機会になりました。チームメイトや運営のみなさん、ありがとうございました!

pt-query-digest の出力結果を Filebeat を使って Elasticsearch に集約し Kibana で出す

今年の ISUCON では Elastic Stack を使って MySQL のスローログを Kibana で可視化したが、いくつか課題があった。

  1. パラメーターによってクエリが分散し、ボトルネックが分かりづらい

    MySQL スローログは生のクエリが記載されていて、Filebeat の MySQL モジュールはそのまま送信するため、 SELECT クエリなどパラメーターが分散しがちなデータはうまく集計することができず上位に現れづらかった。

    Elasticsearch に入れる前に何らかの方法でパラメータを取り除いておきたい。

  2. ログエントリが多い

    スロークエリの設定によっては大量のクエリが記録され、その結果大量の(かつ大きな)エントリを Filebeat で処理して Elasticsearch に登録することになる。Filebeat の負荷が厳しくなってアプリのパフォーマンスに影響が出るほか、Kibana ダッシュボードももっさりしてしまう。

    全てのスロークエリをそのまま入れて毎回 Elasticsearch で計算を行うのではなく、ある程度まとめた結果を投稿するようにしたい。データの前処理ってやつ。

これを解決するために、スローログを pt-query-digest で定期的に解析し、その結果を Filebeat 経由で Elasticsearch に投入するようにすることで、 Kibana でリアルタイムにまとめて見れるようにした。

pt-query-digest を使おう

pt-query-digest は言わずと知れたクエリログの解析プログラムで、各種パラメーターを取り除いていい感じに抽象化し、それを集計して重いクエリを出してくれる。

デフォルトだとヒューマンリーダブルな結果が出力されるが、 --output オプションを変更すると json で出力することができる。Filebeat は json をパースして内容をフィールドに変換してくれる機能がある *1 ので、 json で出力できればパースの手間をずいぶん抑えることができる。

また、pt-query-digest には --iterations というオプションがあり、標準入力を継続的に読み取って定期実行することができる。

この機能を使って、スローログの内容を継続的に読み取って、 pt-query-digest の集計結果を毎分 json で出せるようにした。

$ sudo tail -F -n +1 /var/log/mysql/mysql-slow.log | pt-query-digest --output=json --iterations=0 --run-time=1m

Filebeat との相性問題

Filebeat に出力された json を直接読みとらせるとエントリとして登録はされるが、そのままだとまだ問題がある。

pt-query-digest の結果は一つの json の中に複数のクエリの情報が入っており、下のような構成になっている。

{
  "classes": [ {slow query info}, {slow query info}, ... ],
  "global": { command info }
}

詳細例: pt-query-digestを使用したクエリログの変換について | スマートスタイル TECH BLOG

ここで、Filebeat の ndjson input は 1 行を 1 エントリとして読み込んでしまうので、コマンド結果ごと・複数クエリを内包したエントリになってしまう。それでは解析もやりずらい。

エントリをクエリごとにするには Filebeat だけでは達成できず*2、Elastic Stack 内で済ます場合は Logstash を追加で使う必要がある。Logstash はヘビーなアプリケーションで大変なので、 今回は jq を使って Filebeat に流す前に array を各行に Flatten する。

$ sudo tail -F -n +1 /var/log/mysql/mysql-slow.log | pt-query-digest --output=json --iterations=0 --run-time=1m | jq -R -c --unbuffered 'fromjson | .classes[]' 2>/dev/null
{"attribute":"fingerprint","checksum":"E3341326DCBBC41D81C9550FEAE6F248","distillate":"SELECT user_present_all_received_history","example":{"Query_time":"0.100072","query":"SELECT * FROM user_present_all_received_history WHERE user_id=1662038388045 AND present_all_id IN (1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28)","ts":"2022-09-01T14:02:35"},"fingerprint":"select * from user_present_all_received_history where user_id=? and present_all_id in(?+)","histograms":{"Query_time":[0,0,0,0,27,1,0,0]},"metrics":{"Lock_time":{"avg":"0.000001","max":"0.000006","median":"0.000001","min":"0.000000","pct":"0.070352","pct_95":"0.000001","stddev":"0.000001","sum":"0.000026"},"Query_length":{"avg":"196","max":"198","median":"192","min":"195","pct":"0","pct_95":"192","stddev":"0","sum":"5508"},"Query_time":{"avg":"0.080501","max":"0.100072","median":"0.078474","min":"0.073565","pct":"0.070352","pct_95":"0.090844","stddev":"0.005999","sum":"2.254020"},"Rows_examined":{"avg":"241698","max":"241773","median":"239140","min":"241638","pct":"0","pct_95":"239140","stddev":"0","sum":"6767570"},"Rows_sent":{"avg":"10","max":"27","median":"0","min":"0","pct":"0","pct_95":"26","stddev":"11","sum":"285"},"db":{"value":"isucon"},"host":{"value":"isucon5"},"user":{"value":"isucon"}},"query_count":28,"tables":[{"create":"SHOW CREATE TABLE `isucon`.`user_present_all_received_history`\\G","status":"SHOW TABLE STATUS FROM `isucon` LIKE 'user_present_all_received_history'\\G"}],"ts_max":"2022-09-01T14:13:51","ts_min":"2022-09-01T14:02:35"}
...

-R と 'fromjson' を使っているのは、時折流れてくる非 json なログがあっても処理を続行するため。

フィールドの型問題

これに Filebeat の ndjson input を使うことで各クエリの情報が Elasticsearch に毎分登録されていくようになったが、 pt-query-digest で出力される数値が json では string になっているため、 Elasticsearch では keyword として登録されてしまうという問題があった。keyword として登録されてしまうと数値として扱えず sum や count, top N ができない。

これに関しては、 Filebeat の convert processor を使って特定のフィールドを数値に変換するようにした。その他細々とした調整をして、最終的に filebeat.yaml では以下のような設定になった。

- type: filestream
  id: mysql-digest-jq
  paths:
    - /var/log/mysql-digest-jq.log
  parsers:
    - ndjson:
        target: "mysql-digest"
        add_error_key: true
        message_key: "fingerprint"
  processors:
    - add_fields:
        target: 'event'
        fields:
          dataset: "mysql-digest"
    - convert:
        fields:
          - from: 'mysql-digest.metrics.Query_time.avg'
            type: 'float'
          # 一度登録したフィールドの type は変わらないので、すでに登録してしまっていたら to で新しいフィールドを指定する
          - from: 'mysql-digest.metrics.Query_time.max'
            type: 'float'
          # その他使うフィールドを変換する
    - timestamp:
        field: 'mysql-digest.ts_max'
        layouts:
          - '2006-01-02T15:04:05'
        timezone: 'Local'

CPU 大量消費問題

下のような Systemd service を作成して、ISUCON の走っているインスタンスで使ってみた。

[Unit]
Description=pt-query-digest json dumper

[Service]
Type=simple
ExecStartPre=/usr/sbin/logrotate -f /etc/logrotate.conf
ExecStart=/bin/bash -c "tail -F -n +1 /var/log/mysql/mysql-slow.log | pt-query-digest --output=json --iterations=0 --run-time=1m | jq -R -c --unbuffered 'fromjson | .classes[]' >> /var/log/mysql-digest-jq.log"
Restart=always

[Install]
WantedBy=multi-user.target

みんな大好き bash -c 。これで動きはしたが、ベンチを回している途中に pt-query-digest がガンガン回って CPU を1コア食ってしまうようになった。

その対策としては、 CPUWeight をかなり低く設定し、ベンチ中は MySQL に優先して CPU 資源を使わせるようにした*3

ダッシュボード作成

十分に正規化されたデータを Elasticsearch に入れることができれば、あとはこっちのもの。

Kibana でクエリのいい感じの集約もできて、本当にネックとなっている重いクエリが簡単に分かるようになった。 イベントの量もかなり減って Filebeat のリソース消費で困ることはなくなった。

めでたし。

*1:https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-input-filestream.html#_ndjson

*2:1つのエントリ内の array を複数のエントリに分割することは Filebeat ではできない(多分)ので

*3:ちなみに Nice は全然効果なかった

ISUCON 予選突破のために Elastic Stack と GCP で分析環境を整えた (:old_noto_innocent: Team)

ISUCON 12 予選にいつもの id:utgwkk, id:wass80 と :old_noto_innocent: チームで参戦した*1
結果は 50696 点で本選出場!

ここ数回と同様、自分はインフラと分析・観測を担当していた。ただ、毎年似たようなことをやっていてあまり貢献できていないなあという課題感があったので、今回はログやメトリクスの分析環境をしっかり作ることにした。目標は毎回ベンチ終了のたびにコマンドを叩くのをやめること!

App 周りについてはチームメイトの記事を参照。

blog.utgw.net

memo.wass80.xyz

Elastic Stack でアクセスログ・スローログ可視化

ログを集約・可視化する代表的なアーキテクチャとして、Elastic Stack がある。 もともと Elasticsearch + Logstash + Kibana の組み合わせの "ELK Stack" が有名だが、最近は軽量ログシッパーである Filebeat も合わせて Elastic Stack というらしい。

www.elastic.co

↑ヘラジカの漫画が自己を見つめ直す話でよかったです。

Loki + Promtail + Grafana とどちらにしようか迷った結果、あんまり使ったことのない Elastic Stack を使ってみることにした。 あと Kibana って語感が好き。

ダッシュボード 最高便利

Filebeat から Nginx のログ、 app ログ、 MySQL スローログを Elasticsearch に投げ、それを Kibana で Log stream やダッシュボードで見れる環境を作った(構築については後述)。

Nginx はパスごとのアクセス数とそのレスポンスタイムの装合計、MySQL はスローログのクエリとその時間の総合計などを出すようにしている。

App log stream

Nginx dashboard

MySQL dashboard

特にダッシュボードが便利で、アクセスログやスローログを見るときに特定の時間で絞り込んだ集計が簡単にできる*2ため、ベンチを回す → 集計結果を見る という流れがとてもスムーズにできた。

ログは結局 journalctcl で見るのが早いのであまり使わなかった気がする。次はダッシュボードでエラーログもサクッと見れるようにしてもいいかもしれない。

Google Cloud でトレーシング & プロファイリング

トレーシングやプロファイリングは Google Cloud を使うことにした。Google アカウントは誰でも持っているので、チームメイトへの権限が簡単に配れてオススメ。

Trace

Cloud Trace  |  Google Cloud

前回は OSSJaeger を使ったが、 UI が使いにくかったりインメモリだと遅かったりで微妙だったので、ちゃんとマネージドサービスを使うことにした。

Google Cloud Trace は OpenTelemetry に対応しているので、サードパーティの Trace library を使って構築できて便利だった。予選では MySQL と HTTP リクエストに対してトレースを仕込んだ。

Cloud Trace。 N+1 してそうな様子が見える

Profiler

cloud.google.com

Google Cloud Profiler は

  • 導入がとても簡単
    • go ファイルの最初にちょろっと書くだけ
  • コンソールが見やすい
  • 無料!!

という3拍子揃った神サービスだった。

Cloud Profiler

バージョン番号を変えてデプロイし続けていたら競技途中で1日のデプロイ数 quota (75)に引っかかったので、そこは注意。

悲劇

やってよかった

Slack のヘッダーの様子

…このように今回は色々リアルタイム可視化グッズを用意して挑んでみた。

ISUCON でのこのような集計は pt-query-digestkataribe / alppprof を使うのが定番だが、今回はインストールだけはしたものの一度も使わずに済んだ。コマンドだとどうしても

ログ退避 → ベンチ → 集計コマンド発行 → 結果の共有 → テキストベースの解読

といった作業が必要になるが、 Kibana や GCP コンソールでそれを一気に省略できたのが大きかった。集計結果が Slack チャンネルに流れていかず、チームメイトが見たいときに自分で操作して確認できるのも良いことだと思う。

当日ダッシュボードを触ったりと付け焼き刃なところもあったが、チームメイトからも好評だったので準備したかいがあった。

ちなみに、これらの分析のオーバーヘッドはそこそこある。echo のデバッグモードなども行っていたのでどれが原因かは分からないが、全てを外してベンチを回すと 36k だったスコアが 50k ぐらいになって盛り上がった。

構築編

Elasticsaerch + Kibana 準備

個人の GKE クラスタに強めのノードをたてて*3、そこに Elasticsearch と Kibana を立ち上げた。

Elasticsearch には elastickibana_system ユーザー向けのパスワードを設定する *4

実際にはこんな感じ。

elastic のパスワード

nonylene.hatenablog.jp

kibana_system のパスワード

      - name: password
        image: curlimages/curl
        command: ["sh", "-c", "until curl -X POST -u elastic:${ELASTIC_PASSWORD} -H 'Content-Type: application/json' -k http://elasticsearch:9200/_security/user/kibana_system/_password -d \"{\\\"password\\\":\\\"${KIBANA_PASSWORD}\\\"}\" | grep -q '^{}'; do sleep 10; done;"]

Kibana へのアクセスはちょっとめんどくさくて、

  • 人間が見る用のパス
  • Filebeat から叩く用のパス

がある。人間が叩く用のパスは OIDC を使うのが正規ルート だろうが、今回は既に構築していた oauth2-proxy 背後にしていたので Kibana 自体は anonymous でアクセスできるように した。

ただ、oauth2-proxy 背後だと Filebeat からのアクセスができなくなるので、別途 Basic Auth 付きの endpoint を用意して Filebeat からはそれを叩かせるようにした*5。こうやってポコポコ endpoint 生やせるのは k8s のいいところ。

その他、各種データは永続化する必要がある。今回は Node Pool がスケーリングしないように設定した上で hostPath を使う超雑ソリューションをしてしまった…。作ったダッシュボードとかエクスポートするまでこのインスタンス落とせないじゃん…*6。本選までには PV 使うようにしたい。

Filebeat

Filebeat は競技インスタンスにインストールする。モジュールやプラグインが充実していて、数行設定を変更するだけで journald、 Nginx、 MySQL log それぞれの収集 & メトリック化を有効化できて楽だった。

一点、 Nginx でレスポンスタイムを記録するためには、 Nginx ログフォーマットの変更 + Filebeat のパース設定の変更を行う必要がある。 kataribe 互換のログフォーマット(デフォルトフォーマットの末尾にレスポンスタイムを追加する)の場合、以下のリンクの通りにすると nginx.response.time がメトリクスとして上がるようになった。

discuss.elastic.co

追記(Nginx)

予選・本選で試してみたところ各種 id が入ったパスが分散して表示されてしまう課題があったので、 Filebeat の Nginx module の設定を触って集約して表示できるようにした。

nonylene.hatenablog.jp

追記(MySQL

本選時は init 時の MySQL スロークエリが大きすぎて Filebeat がメモりを食って OOM になる問題があった。 init 関係のクエリは頑張って記録する必要はないので、 max_bytes で一行あたりの最大長を調整し、大きすぎるログはスキップするようにすることで解決した*7

メモリ使用量との格闘の記録。Nice よりも cgroup weight をちゃんと設定したほうがいいです

- module: mysql
   ...
  slowlog:
    enabled: true
    input:
      max_bytes: 100000 # Limit up to 100k bytes

スローログそのままだと本戦ではいくつか問題が出てきたので、pt-query-digest の結果を出せるようにもしてみた。詳しくは↓を参照。発展編って感じ。

nonylene.hatenablog.jp

ダッシュボード

今回 Elastic Stack を選んで最も良かった点は、ダッシュボードが簡単に構築できたこと。 Filebeat と Kibana の連携が整っており、Filebeat のセットアップコマンドで Kibana ダッシュボードを自動生成してくれる。

Filebeat によって自動生成された Kibana のダッシュボード

この機能があったおかげで、自動生成されたダッシュボードを Clone & 参考にすることができ、 Nginx, MySQLダッシュボードを高速に設定できた。

(参考) ダッシュボードを export したものは以下。

github.com

個人的には、 Grafana よりも Kibana のほうが編集画面の UI がしっかりしていてビギナーでも使いやすいと思う。オススメ。

タイムライン

最後に、準備と当日やったことの記録。

前々日

  • 全然準備やってないことに気づく :old_noto_innocent:
  • Elastic Stack のドキュメントを眺め、 Elasticsearch と Kibana の連携までやって力尽きる

前日

  • Filebeat の検証
    • nginx と journal が取れることを確認した
    • nginx のレスポンスタイムと MySQL スローログは当日の課題とした :old_noto_innocent:
  • Kibana でのログ閲覧やダッシュボードの準備
  • トレーシングやプロファイラの試行 w/ チームメイト

当日

だいたいいつも通りで、サーバーの水やり当番をしていた。

  • 30分前に起きて、20分前からライブが始まることを知る
  • カーネル起動パラメーターを変更する
    • 起動しなくなったら最悪なので最初に
  • いつものサーバーもろもろ整備
    • サーバー眺めて不要そうなプロセス止める
    • sysctl チューニングしたり、max open files 変えたり
  • netdata 入れる
    • これはいつもどおり役に立った。ベンチマーク中のリアルタイムな負荷傾向を見るにはこれが一番。
  • filebeat 入れて各種設定
  • ダッシュボード整備(付け焼き刃ポイント)
  • MySQL のメモリ調整
    • 富豪的に設定していたらメモリがなくなってホストへの疎通が失われることが数回あったので、やや控えめに
      • 分割した後は上げといてもよかった。最後余ってたから大差ないだろうけど
  • クエリキャシュ目当てに MySQL 5.7 に戻そうとしたが無理だったので諦める
    • Ubuntu 22.04 に対応したパッケージがなかった
  • MySQL 別ホスト移行、垂直分割準備
    • 今回は初めから % のユーザーがあったので bind 変更するだけだった
  • Nginx キャッシュ
    • 今回は多分関係ない
  • GOGC の調整
    • GOGC=off にしたらメモリを食いまくってホストが死んだ(当たり前すぎる)
    • 最終的に 300 に。1000点分くらいの効果はあった
  • 最後にトレーシングとか全部 purge して、再起動試験
    • Filebeat が purge してもゾンビのように生きていてこわかった(systemctl disable した)
  • 競技終了後に MySQL スローログを切り忘れたのに気づく

最終的に nginx + App 1台、MySQL 2 台の構成になった。App がカツカツだったので、もっと時間があったら or スコアに余裕がなかったら App を増やす作業をしていたと思う。

感想

今回も楽しかったです!SQLite を知ったときはウケました。 本選も楽しみ!!

毎年 id:utgwkk id:wass80 がアプリやクエリ周り見てくれるので大感謝 :pray: 今回もアプリのコードは3分くらいしか見ていないので自分だけだと絶対ムリそう。

サーバー周りは年々整備されていてやらないといけないことが減っている印象があって、サーバー触り担当としてはこういう基盤まわりで貢献していきたいところ。今後の課題としては基盤周りのブラッシュアップと、Redis や Varnish をサクッと触れるようにすることかな。セッション周りも Sticky module に頼った分散から脱却したいと思ったり…。

その他

最近ピノキオピーのデラシネをめっちゃ聞いてます。構築中ずっと流してました。今も流してます。電子音最高です。

open.spotify.com

*1:Slack に登録している :innocent: が Noto emoji だったころのグリフを使った絵文字

*2:アクセス数のグラフを出しておくと、いつベンチが回ったかすぐに分かる。そこでドラッグすると時間の設定が可能

*3:e2-standard-2

*4:kibana_system については、手作業でやるなら Enrollment Token を使えば良さそう。k8s で立ててるとコンテナが作り直されたりするので固定パスワードを設定した https://www.elastic.co/guide/en/elasticsearch/reference/master/create-enrollment-token.html

*5:実際はさらに複雑で、Filebeat から Kibana の API を叩くには Anonymous user だとできなかったので、ingress-nginx で set_proxy_header を行い Filebeat から来たリクエストの Basic auth credential を elastic ユーザーのものに上書きする処理を行っている。

*6:Kibana のダッシュボードなどのデータは Elasticsearch に保存されているので、正確には Elasticsearch の乗ったインスタンスを落とすまで。

*7:本選後に

Elasticsearch の Docker container でパスワードを指定する

環境: Docker の elasticsaerch image 8.3.2

久々に Elasticsearch を Docker で立ち上げると、初期状態でセキュリティ機能が有効になっていて、そのまま curl しても認証が通らず API が打てなかった。

$ curl -k https://localhost:9200/
{"error":{"root_cause":[{"type":"security_exception","reason":"missing authentication credentials for REST request [/]","header":{"WWW-Authenticate":["Basic realm=\"security\" charset=\"UTF-8\"","Bearer realm=\"security\"","ApiKey"]}}],"type":"security_exception","reason":"missing authentication credentials for REST request [/]","header":{"WWW-Authenticate":["Basic realm=\"security\" charset=\"UTF-8\"","Bearer realm=\"security\"","ApiKey"]}},"status":401}

環境変数で認証周りを完全に切ることもできるが、Docker image 経由で立ち上げている場合はパスワードの設定を環境変数経由で行うこともできるのでメモ。

どうするか

ELASTIC_PASSWORD で elastic user のパスワードを指定できる。これは、 Docker image の entrypoint.sh や Elasticsearch の Docker 関係のドキュメントを見ると書いてある。

github.com

www.elastic.co

実際に ELASTIC_PASSWORD を指定すると、下のように elastic user と指定したパスワードを Basic 認証で指定して curl が通ることを確認できる。

$ curl -k https://localhost:9200/ -u 'elastic:yourStrongPassword'
{
  "name" : "elasticsearch-6c77b74fd4-g9688",
  "cluster_name" : "cluster",
...

pip install --user を config でデフォルトにする

最近(2年以上前)、pip がデフォルトで --user でインストールするようになった。

github.com

ただ、この機能が有効になるにはいくつかの条件がある。

  1. The user has not explicitly specified otherwise in the command line, environment variable, or a config file.
  2. The --prefix and --target options are not in use.
  3. The global site-packages dir is not writeable, so doing a non-user install would fail.
  4. User site-packages are enabled - to avoid surprises in isolated environments.

https://github.com/pypa/pip/pull/7002#issue-491155059

ここで、Pythonbrew でインストールすると brew はユーザー権限で入るために 3 が満たせないことが多く、 brew 内のグローバルに入ってしまう状態が続いていた。

確かに権限的にはグローバルでも問題ないが、brew ディレクトリの中はクリーンであって欲しい。brew を使っていてもデフォルトで --user にしたい場合は、pip の config で設定する。

pip.pypa.io

pip の config ではコマンドのオプションのデフォルトを環境変数や config ファイル経由で設定することができる。

今回は config ファイル(~/.config/pip/pip.conf)で以下のように設定した。

[install]
user = true

こうして、環境によらず、また多少古い pip でもデフォルトで --user で入るようになった。