Unyablog.

のにれんのブログ

k8s: 手元で削除したリソースをクラスタからも消したい

k8syaml を kustomize で管理している。ここからリソースを消しても kustomize はそれを感知しないためクラスタからは削除されない。

どうするか

kpt を使う。これを見つけたのは deprecated になった inventory からリンクが貼られていたため。

kpt はいくつかの機能で構成されているが、そのうち live を使う(それ以外の機能はあまり知らない *1)。

kpt live では apply 時に適用対象のリソースを列挙して ConfigMap に保存する。これによって、次回以降の apply 時に適用するリソースと以前に適用したリソースを比較することができ、不要になったリソースが何であるかを判別することができる。

googlecontainertools.github.io

kpt を使う

kpt をインストールしたあとに、まず ConfigMap を作るためのリソースを作成する。

$ kpt live init .

これで inventory-template.yaml ができる。これを kustomize.yaml に含めておく。

resources:
  - ...
  - inventory-template.yaml

そして build したものを kpt live apply で適用する。

$ kustomize build | kpt live apply

その結果、2回目からは不要なリソースを消してくれるようになる。

以下おまけ

kpt を使わない方法

Kustomize で CommonLabels を使うことで kubectl apply --prune と組み合わせることもできる。

commonLabels:
  app.kubernetes.io/managed-by: kustomize

として

$ kustomize build | kubectl apply -f - --prune -l app.kubernetes.io/managed-by=kustomize

すれば二回目以降は prune される。ただ、

のでやめた。

その他の方法

GitOps 的な方面で色々ある。

  • terraform

    Kustomize integraion がある。ドキュメントを読むとできそう。

    ただそのために terraform を導入するのも面倒なのでやめた。

  • flux2

    Git レポジトリ経由ではなく手元から実行できる方法が分からなかったのでやめた。削除してくれるかまでは見ていない。

  • Argo CD

    Argo CD はパージしてくれる機能がありそう。ローカル実行もできそうだったけど、kpt のほうが(ほぼ)手元で完結してシンプルで良さそうに見えたのでやめた*2

*1:別のコマンドとしてくれたほうが分かりやすいと思ったり

*2:CD がほしいわけではないのです

Vertical Pod Autoscaler の limits 周りの挙動について

Kubernetes で memory の requests を管理するのに Vertical Pod Autoscaler (VPA) を使っている。

github.com

VPA はリソースの使用量の実績に基づいて良い感じに limits と requests を調整してくれるものだが、 limits の設定に関してちょっと困ったのでメモ。

参考: https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler#limits-control

VPA がどう limits を設定するか

VPA が limits を設定するときは、 LimitResources などの制約がない場合、以下のようなアルゴリズムで設定される。

  1. VPA が requests を算出する
  2. 算出した requests に、元々設定されていた requests と limits の比をかけて新しい limits とする

すなわち、メモリの requests を 500Mi, limits を 1000Mi とした Pod に対して、VPA が新しい requests を 700Mi とすれば、新たな limits は 1400Mi となる。

requests を設定していないとき問題

ここで、Pod に limits のみを設定し、requests を設定していない場合はどうなるか。このとき、k8s は requests を limits と同じ量に設定するので、VPA は requests と同量の limits を推奨するようになる *1

requests は普段の使用量ベースで自動設定されるので、limits に同じ値が設定されるとほとんど burst できない Pod となってしまう。 起動時ちょっとだけメモリを多めに使う、みたいな Pod がある場合に OOM killer で落ち続けるようになってしまった。

解決策

解決策としては以下が考えられる。

  • limits だけではなく request を設定する

    request を設定する際には VPA の挙動を考慮する必要がある

  • limits を設定せず request のみを設定する

    こうすると VPA によって limits が減ることはない

今回はめんどくさかったので後者にした。

limits がない結果ノードのメモリが不足する可能性があるが、 requests は普段調整されるためそれほど起こらないはず。もし頻繁に起きるなら、普段は問題ないが Burst したときにメモリが足りないということであり、それは各ノードの余剰メモリが足りないということであるので、そういった問題として対処することにした *2

*1:requests が何らかの理由で設定されない場合も VPA によって同量になる https://github.com/kubernetes/autoscaler/blob/2542e8c884a8e25634d3b8f43243fa8706007f30/vertical-pod-autoscaler/pkg/utils/vpa/limit_and_request_scaling.go#L69-L72

*2:LimitRanges ぐらいは設定してもいいかもしれない

Kubernetes ヒヤリハット ― デフォルトに戻すときはちゃんと書く

Kubernetes で Service Account を特定のものから default に戻そうとして、Pod の Template から serviceAccountName を消して apply した。

         spec:
-          serviceAccountName: old-service-account
           restartPolicy: Never
           containers:
           - ...

しかし、適用しても作られる Pod の serviceAccountNameold-service-account のままだった。

serviceAccountName を消すだけでは apply しても unchanged となり、 default に戻るわけではないらしい。

ちゃんと default って書き直しましょう。

         spec:
-          serviceAccountName: old-service-account
+          serviceAccountName: default
           restartPolicy: Never
           containers:
           - ...

教訓: apply に冪等性があると思ってはいけない!

stackoverflow.com

SQLAlchemy で MySQL の JSON_CONTAINS をやる

SQLAlchemy で MySQLJSON_CONTAINS をやるには func.json_contains を用いれば良いのだけど、 JSON_CONTAINS のドキュメント にあるように candidateJSON ドキュメントである必要がある。

以下のようにすると hogeJSON ドキュメントではないためエラーが出る。

from sqlalchemy import func

# Foo.values は JSON の配列である想定

session.query(Foo).filter(
    func.json_contains(Foo.values, 'hoge')
).all()
sqlalchemy.exc.OperationalError: (MySQLdb._exceptions.OperationalError) (3141, 'Invalid JSON text in argument 1 to function json_contains: "Invalid value." at position 1.')

JSON ドキュメントにするには JSON_QUOTE を使うと良い。JSON_QUOTE すると foo"foo" になる。

SQLAlchemy でやるには func.json_quote を挟む。

from sqlalchemy import func

# Foo.values は JSON の配列である想定

session.query(Foo).filter(
    func.json_contains(Foo.values, func.json_quote('hoge'))
).all()

ISUCON 10 本戦に出場した(16位)

チーム 😇😇😇 として ISUCON 10 本戦に参加した。

isucon.net

結果は 14206 点で 16位。くやしい!

レポジトリは以下。よく分からない Author になってるコミットは大体自分によるもの。

github.com

以下はチームメイトのブログ。コードの改善などは基本的にそちらを見てください。

blog.utgw.net

やったことなど

自分はいつもどおりサーバーを触る人だった。恒例になってきた一連の動作とパフォーマンスを除いて、今回特別にやったことや所感を書いていこうと思う。

Envoy

Envoy が入っていて「自分は Envoy の YAML を手で書いたことがあるぞ!!任せろ!!!」って張り切ったんだけど、実際には Envoy らしいことは特にしておらず普通の Reverse Proxy だった。さっさと Nginx に載せ替えても良かったと思う。 Envoy の設定って手で書くものじゃないしね…。

実際静的ファイルの配信をするために Nginx を導入したけど、当日は全てを Nginx に載せ替えることはしなかった。Envoy の設定には困っていなかったし、 ISUCON で使うと面白そうだったので😈。

気になった点としては、

  • 最後のほうで LimitNOFILE に到達して突然死した

    その時は時間がなくて雑に Restart することで解決したけど、後で調査した結果 LimitNOFILE 到達が原因だった。そういえば上げ忘れてたね…。それでも突然死はやめてほしいが、そういう思想らしい。

  • ベンチマーク中 CPU 使用率が 40% と高かった

    今まで ISUCON で Nginx を使ってたときは困ったことがなかったので意外だった。TLS 終端してるとそんなもんだったっけ?

手で設定しにくいということを除いても、普通の Reverse Proxy として使うなら Nginx のほうが扱いやすいなあと思った。

サーバー構成

今回はサーバースペックが3台とも違っていて、

  1. CPU 2コア メモリ 1GB
  2. CPU 2コア メモリ 2GB
  3. CPU 4コア メモリ 1GB

という構成だった。コンテスト中、構成は以下の変遷をたどった。

  1. サーバー1 単体

    初期状態。

  2. サーバー2: MySQL サーバー3: Web, API, Envoy, Nginx

    MySQL といえば InnoDB バッファプール!ということでメモリの大きい方に載せた。サーバー1は開発用。

  3. サーバー2: Web, API, Envoy, Nginx サーバー3: MySQL

    サーバー2を見ていたらメモリは 30% ぐらいしか使っていないのに CPU がサチってたので載せ替え。サーバー3はメモリも CPU も80%程度で良い感じ。

  4. サーバー1: API, Envoy, Nginx サーバー2: Web サーバー3: MySQL

    サーバー2 を見ていたら意外にも Envoy が食っていたので Nginx と API ごと 1 に載せ替えた。

この状態でリソース不足が起きることはなくなった。謎エラーがなければもっと伸びたよなぁ~~~~。

Protocol Buffers

今回 Web アプリの API には Protocol Buffers が使われていたので、デバッグのために Protocol Buffers を解読し打てるようにする必要があった。

色々考えた結果 Wireshark でパケットキャプチャして直接眺めることに。

ただ、色々難しいことがあり…

  • WiresharkTLS の解読ができない

    そういえば TLS の中身見れる機能があったな、って秘密鍵だけ持ってきたけど解読できなかった。まあ言われてみれば DH だしね…*1。新たな知見を得た。

    これは Envoy とアプリ間の通信を見ることで解決。

  • Wireshark で Protocol Buffers のパースができなかった

    最新の Wireshark だと見れる機能があるはずなのだけど、うまく行かなかった。Proto を Import するパスが間違っていた?それとも HTTP/2 や gRPC じゃないから見れない?*2

結局パケットキャプチャした内容をそのまま bytes で保存して、 protoc に噛ますことで見れるようになった。

$ cat /tmp/foo.bin | protoc --decode=xsuportal.proto.services.admin.InitializeRequest /Users/nonylene/codes/innocent-team/isucon10f/proto/xsuportal/services/admin/initialize.proto --proto_path /Users/nonylene/codes/innocent-team/isucon10f/proto

めっちゃむずかった!普通に Printf デバッグなりすればよかったね。

デバッグ用のリクエストを作るのも色々手間取ってしまった。初めはこれ gRPC だ!って勘違いしていたのでずっと grpcurl でやろうとして使い方がわからん…と言っていた*3。結局 curlprotoc を組み合わせて解決。

github.com

MySQL 8

予選は MySQL 5.7 だったけど本戦は MySQL 8 だった。

ISUCON 8 本戦で MySQL 8 にめちゃくちゃハマった経験があったけど、そのおかげもあって今回は全く困らなかった。 GRANT 文とユーザー作成を分けなきゃいけなくなったんだな~、ぐらい。

全体の感想とか

結局色々やったものの utgw のブログに上げられているエラーが解決できず、サーバーも本領発揮せずに終わってしまった。みんなで色々見てたけど分からず厳しかった!!

感謝

今年のコンテストも楽しかったです!今年は本戦もリモートとなりましたが、Tシャツや名札が事前に送付されたり、当日に YouTube ライブをしていたりとリモートでも盛り上がるように色々施策があって良かったと思います。問題解説ビデオ最高でした。

www.youtube.com

問題も解きごたえがありました。サーバーによってスペックが違うのも新鮮で、「とりあえず3つに分けとこ」だけではなくちゃんとサーバーのリソースを検討することを求められていて楽しかったです。

今年もチームメイトに助けられました。自分だけだと絶対出れてない(学生一人チームすごいよね…)し、バランスの良いチームだなあと思います。来年もやっていきたい!

*1:じゃあ RSA にするか、パフォーマンスも良くなるだろうし、と思ったけど Envoy での設定方法がわからなかっ上、ベンチマーカーが対応してるのかも不明だったのでやめた

*2:Protocol Buffers のライブラリを使っているだけなので、 Envoy とアプリ間の通信は HTTP/2 や gRPC ではなく HTTP 1.1 だった。

*3:途中で息抜きに外に出て考えたら「あっこれ gRPC じゃないじゃん」って気づいた。休憩は大事。

ISUCON 10 で予選突破した(24位)

ISUCON に今年も出場して、めでたく予選突破できた。チーム名は 😇😇😇 で、チームメイトはいつもと同様 utgwkk と wass80。

isucon.net

チームメイトのエントリは以下。

wass80.hateblo.jp

blog.utgw.net

レポジトリは以下。

github.com

やったこと

いつも通り自分は Schema とかアプリケーションはあまり見ずにインフラに徹していた。New Relic も全然見てない。

  • ssh config 書く
  • ユーザー作る
  • pt-query-digest や netdata 入れる
  • レポジトリ整備(スクリプトや symlink)
  • 余計なプロセス消す
  • Nginx で Bot の ban やったり proxy_cache やったり
  • sysctl などのチューニング
  • MySQL のチューニング
    • クエリキャッシュとかスロークエリ出すとか
  • MySQL の複数台分散

いろいろ

Nginx

proxy_cache_valid を設定しないとキャッシュが効かなかったけど本当かな?

MySQL

今回も色々悩まされた。 symlink でやったら何故か上手く行かなくなったり、GRANT で手間取ったり。

一番時間を食われたのはインスタンスをまたいだ接続ができなかったことで、ポートも開いてるし許可もしてるのにサーバー側に Bad handshake エラーが出て全く接続できなかった。

色々見た結果、 最初に MTU を雑に 9000 に設定していたのが原因で、 1500 に戻したら直った。 PMTUD やら TCP MSS やらが動かなかったんかな~。

クエリキャッシュ

今回は Write が少ないためクエリキャッシュがとてもよく効いてくれた。 600点が1500点ぐらいになって一時は2位になってたはず。

query_cache_size だけではダメで、 query_cache_type = ON にしないと動かないことに初めて気づいたのだけど、今までずっとミスし続けていた…。 MySQL 8 にはクエリキャッシュ無いのでこれからはあまり使うこともなさそうな新知見。

複数台分散

今回は MySQLボトルネックだったので複数台分散をどうするか考えていた。結局 *sqlx.DB を複数持って、書込側はレプリケーションせずに両方に並列に書き込む形に、取得側は2台からランダムに取ってくるようにした。

準同期レプリケーションや、nginx の proxy とか LVS を使っても良かったのだけど、アプリケーションがそこまで大変ではなかったのでそちらの変更で済ますことにした。

一見ヤバそうな方針だけど結果的にはちゃんと動いてくれて、スコアを伸ばすことができた。goroutine 最高*1

とはいえ、垂直分割が一番賢いよなぁ。コンテスト中は全く思いつかなかった。

その他

  • Go を使った

    今までずっと RubyPython でやってきたけどバグったときの修正が大変だった。Go は早いし、コンパイル時に型チェックもしてくれるし、IDE は整っているし最高だった。過去の回でよく悩まされてきた謎のエラーもほとんど起きずに、スムーズにスコアを伸ばすことができた。これからも ISUCON では Go を使うと思う。

  • 終盤の Fail

    終盤に複数台分散を入れたら時々落ちることがあったけど、最終的には安定して通るようになって良かった。とっさの reboot が効いたのか、 sysctl を全部戻したのが効いたのか、MySQLinnodb_doublewrite あたりを全部デフォルトに戻したのが効いたのか。

  • Mitigations の無効化

    Mitigations を無効にするカーネルパラメータを入れると早くなるという話題があって、やることも考えていたのだけど、 grub-install が必要で流石に怖くてやめた。全然点数出なかったら最後に試してたかも。

今回も予選にも関わらず複数台構成ということで、インフラ担当の自分としてもやりがいのある問題でした。運営のみなさんありがとうございました!本戦もよろしくお願いします!!

2年ぶり3回目の本戦出場でとても嬉しいし、本戦もこの調子でやっていきたい!

*1:ミスって wass に直してもらったけど😇

eBPF: bpf_skb_store_bytes の BPF_F_RECOMPUTE_CSUM は tc_cls の egress では動かなさそう

最近 bpf についてめっちゃ書いてるけど、ドキュメントが弱く検索しても情報がないからです…

bpf_skb_store_bytes には BPF_F_RECOMPUTE_CSUM というフラグがあって、ドキュメントによると store 後にチェックサムを更新してくれるらしい。

しかし、これを tc_cls (classifier, BPF_PROG_TYPE_SCHED_CLS) の egress に使うとチェックサムが更新されなかった。

理由は(SKB のライフサイクルに関する知識が足りないため)確証まで至っていないけど、コードを見る限り skb->ip_summedCHECKSUM_COMPLETECHECKSUM_PARTIAL の場合に変更するようなコードになっていてそれは egress には関係ないのでは?とか、 skb->csum を tc で更新してもデバイスにわたす直前だから反映されないのではないか?とか考えている。

対処

手動でチェックサムを更新する関数(bpf_l3_csum_replace)があるのでそれを使う。