ISUCON12予選に「Railsへの執着はもはや煩悩(ry」で参加して予選通過しました

今年も@cnosukeさんと@aibouさん一緒に、「Railsへの執着はもはや煩悩の域であり、開発者一同は瞑想したほうがいいと思います。」というチームでISUCONの予選に出ました。

最終スコアは28,080点で、予選通過することができました。

一次募集も二次募集も一瞬で枠が埋まってしまい申込みに間に合わなかったので、出題を担当した回を除いてISUCON3から毎年出ていたISUCON参加がついに途切れてしまうかと思ってしまいましたが、なんとか追加募集の枠で参加することができました。

(追加募集の枠も3分くらいで埋まってしまったので本当に危なかった。。。)

ISUCON10, ISUCON11も同じメンバーで、そのときは予選1週間前くらいに練習していたのですが、今年はタイミングが合わずぶっつけ本番になってしまいました。

なんとなくの分担

cnosukeさんが主にアプリ改修とか、aibouさんがSQLiteMySQLのデータ移行やMySQLのインデックス周りとか、自分はデプロイとか細々とした作業をやる感じで、分担していました。

3年目ということもあって割りとスムーズに進められた気がします。

なんとなくのスケジュール

3人がやったこと全部を雑に書いてます。

時刻 やったこと メモ
09:30 集合
10:00 競技開始
10:15 とりあえず初期状態でベンチ回す 2,800点くらいだった気がする
10:30 nginxとMySQLの設定ファイル群と、webappを、まるっとgitリポジトリに突っ込んだりした /etc/mysqlシンボリックリンクを張るようにしていたがうまく動いてくれなくなったので後でMySQLについてはgit管理を諦めた
10:45 コード読んだりkataribeを導入したりしてた
10:55 1号機をnginx, webapp, SQLiteに、2号機をMySQLに、3号機は使わない構成にして、破壊的な実験とかは3号機で試してもらうような状態にした 特に大きく手を入れてなかったのでMySQLがめちゃくちゃCPUを食ってる状態になった
11:25 MySQLにインデックスを張ったりしていた
13:50 とにかく「GET /api/player/competition/:competition_id/ranking」が遅いことがわかったので対策を考え始める
15:10 SQLiteに入ってたデータを全部MySQLにダンプして、SQLiteは全く使わない状態にした 2,826点
15:55 採番ロジックが複雑そうに見えたのでULIDを使うように変更した 4,267点
16:30 1号機をMySQLに、2号機をwebappに、3号機をnginxという構成にした 7,469点
17:05 いろいろキャッシュを入れたり地道に修正 10,234点
17:15 SQLiteは捨てたのにflockしっぱなしだったのに気づいて修正 20,669点 1号機(MySQL)だけがとにかく重い状態になった
17:30 更にcompetition周りをキャッシュ化 22,037点
17:45 インデックスを更に張ったり再起動試験したりしてたら更に上がったので、もうこれ以降は提出をやめることに 28,080点
18:00 競技終了

最終的な構成図


ベンチが詰まることもなく、その他のトラブルもなくとても快適な競技環境でありがたかったです。

運営の皆様、本当にありがとうございました!!

ISUCON7以来、5年ぶり3回目の本戦出場なので楽しみにしてます!!!!

ISUCON11予選に「Railsへの執着はもはや煩悩(ry」で参加したけどダメでした

今年も@cnosukeさんと@aibouさんと「Railsへの執着はもはや煩悩の域であり、開発者一同は瞑想したほうがいいと思います。」というチームでISUCONの予選に出ました。

3人とも別の会社にいるので、去年同様に前の週にリハーサルを行ってイメージを掴みました。

去年と同じようになんとなくの役割分担として、cnosukeさんがAPIのコード変更を行い、aibouさんがDB・nginx周りのチューニングを行い、自分はデプロイとか設定のgit管理したりと雑多な対応をしていました。

なんとなくのスケジュール

昼食を食べずにぶっ続けでやっていたのでかなり疲れてしまって、あんまり当日の記憶も残っていないのですごく雑です。

時刻 やったこと メモ
09:40 集合
10:00 競技開始
10:11 webapp以下をgitに突っ込む
10:29 1号機をnginx, アプリ、3号機をmariadbにする これだけでちょっとスコアがった
10:49 いくつかmariadbにインデックスを貼った 24,648点出て暫定2位になったのでテンションが上がった
11:52 /assets 以下はnginxが直接返すようにした
11:59 秘蔵のmy.cnfを投入した 実際は 設定ファイルのパーミッションがおかしかった せいで投入できていなかった…
12:39 /api/initialize 以外は全部nginxが直接返すようにした
13:05 アプリのリファクタリングが終わった
13:23 1号機のCPUが辛いので /api/trend だけ2号機に流してみた
16:02 バルクインサートするようにアプリを書き換えた 38,324点まで上がった
16:24 アプリを書き換えたり、更にmariadbにインデックスを張ったりした この頃に最高得点の54,690点が出た
17:13 アプリを直したりmariadbにインデックスを貼ったりトラフィックを1号機と2号機で混ぜるようにしたりした 36,926点までしか出なくて伸び悩む
17:30 ポータルがメンテに入ったので マシンを再起動 してみたり、細かなアプリケーションの改善とかをしてた
18:10 ポータルがメンテ明けになったのでベンチを回してみたところ、なんと1,000点しか出なくなってしまった とても焦った
18:44 nginxの設定がおかしいことに気づいたがもう時間がなくなってしまったので、1号機にnginxとアプリ、3号機にmariadbの構成で出した。 13,982点になってしまった

競技中のトラブル

mariadbの設定が反映されていなかった件

/etc/mysql 以下をすべてgitリポジトリに突っ込んで、 /etc/mysql 自体はそのリポジトリシンボリックリンクにしていたのですが、どうやらパーミッションがおかしい(本来はroot:rootが所有しているのが、isucon:isuconになっちゃった)のが原因で、実は設定が反映されていなかったことに途中で気づいて、気づいたものの対応するのにとても時間がかかってしまいました。

結局、AMIがあったので別のインスタンスを立てて、それの /etc/mysql の構成を真似て構築し直して、gitリポジトリに追加しないようにしたところ使えるように戻りました。

nginxの設定がおかしかった件

ポータルのメンテ後くらいからスコアが1,000点しか出なくなって、なんでだろう…とずっと考えていたのですが気づけず、ISUの「状態」が1行も出ず空っぽになっているのがおかしい?と気づいて、

f:id:rkmathii:20210822224313p:plain

見てみたところ、自分が入れてしまったnginxの設定でIPアドレスを間違っていたのが原因でした。。。

f:id:rkmathii:20210822224124p:plain

リクエストを飛ばす先がないのでエラーになるかなと思ったのですが、今回のルールでは減点にならなかったので、加点もされず減点もされず、どこがおかしいのか気づくのに時間がかかってしまったのがもったいなかったです。。。


予選突破には11万点くらい必要だったので届かなかったかもしれませんが、途中でトラブってしまったのがもったいなかったです(´;ω;`)

問題はとても面白かったし、ポータルも途中不具合があったもののそれ以外は快適に使えましたし、すごく楽しかったです。

来年こそ予選突破したい…!

Kustomizeでvarsの代わりにreplacementsを使う

Kustomize v3.8までなら、varsを使うことで次のように特定の箇所の値を別の箇所に上書きすることができ、例えば

# test.yaml
---
apiVersion: v1
kind: Service
metadata:
  name: test
  labels:
    app.kubernetes.io/name: THIS-IS-TEST-SERVICE-NAME # ☆この値を上書きに使いたい
spec:
  type: NodePort
  ports:
    - nodePort: 32000
      port: 80
  selector:
    run: test
---
apiVersion: v1
kind: Pod
metadata:
  name: test
  labels:
    app.kubernetes.io/name: $(TEST_SERVICE_NAME) # ★ここを上書きする
spec:
  containers:
    - name: nginx
      image: nginx
      env:
        - name: testenv
          value: $(TEST_SERVICE_NAME) # ★ここも上書きする
# kustomization.yaml
resources:
  - test.yml

vars:
  # ☆上書きに使う値の情報
  - name: TEST_SERVICE_NAME
    objref:
      apiVersion: v1
      kind: Service
      name: test
    fieldref:
      fieldpath: metadata.labels.[app.kubernetes.io/name]

というマニフェストファイル群に対してkustomize build .すると、次のように上書きすることができていた。

apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/name: THIS-IS-TEST-SERVICE-NAME # ☆この値を上書きに使う
  name: test
spec:
  ports:
  - nodePort: 32000
    port: 80
  selector:
    run: test
  type: NodePort
---
apiVersion: v1
kind: Pod
metadata:
  labels:
    app.kubernetes.io/name: THIS-IS-TEST-SERVICE-NAME # ★上書きされた
  name: test
spec:
  containers:
  - env:
    - name: testenv
      value: THIS-IS-TEST-SERVICE-NAME # ★上書きされた
    image: nginx
    name: nginx

しかし、Kustomize v3.9以降からはエラーになるようになってしまった。

Error: field specified in var '{TEST_SERVICE_NAME ~G_v1_Service {metadata.labels.[app.kubernetes.io/name]}}' not found in corresponding resource

どうやら、varsで指定するfieldpathに . が含まれているような場合について、解釈できなくなってしまったようだった。

もともと2020年1月くらいからvarsをこれ以上使い続けるのは…みたいなIssueもたっていて、

github.com

最近のKustomizeでは代替になるreplacementsという機能が実装されていたので、それを使ってみることにした。


※以降はKustomize v4.2.0で動作確認した

上に書いたような例をreplacementsで実現しようとすると、次のようになる。

# test.yaml
---
apiVersion: v1
kind: Service
metadata:
  name: test
  labels:
    app.kubernetes.io/name: THIS-IS-TEST-SERVICE-NAME # ☆この値を上書きに使う
spec:
  type: NodePort
  ports:
    - nodePort: 32000
      port: 80
  selector:
    run: test
---
apiVersion: v1
kind: Pod
metadata:
  name: test
  labels:
    app.kubernetes.io/name: TO_BE_REPLACED # ★ここを上書きする (TO_BE_REPLACEDの部分は何でも良い)
spec:
  containers:
    - name: nginx
      image: nginx
      env:
        - name: testenv
          value: TO_BE_REPLACED # ★ここも上書きする (TO_BE_REPLACEDの部分は何でも良い)
# kustomization.yaml
resources:
  - test.yml

replacements:
  - source:
      # ☆上書きに使う値の情報
      version: v1
      kind: Service
      name: test
      fieldPath: metadata.labels.[app.kubernetes.io/name]
    targets:
      # ★上書きされる対象の情報
      - select:
          version: v1
          kind: Pod
          name: test
        fieldPaths:
          - metadata.labels.[app.kubernetes.io/name]
          - spec.containers.0.env.0.value

こうしてkustomize build .を実行すると、冒頭に書いた例と同様に上書きされたマニフェストファイルが生成される。

varsの場合と異なり、「上書きに使う値」「上書きされる対象」のどちらのパスも明記する必要があるので若干めんどくさくなったように思ったが、よく言えば「どの部分が上書きされてしまうのか?」が一覧できるようになったので、慣れればreplacementsのほうが管理しやすくなるのかもしれない。

おうちKubernetesクラスタを組む(後編)

rkmathi.hatenablog.com

の続き。

ノートPC↔デスクトップマシンの通信があんまり安定してくれなくて、たまにパケットが通らなくなったりしてよく分からない感じになっちゃったので、VMからまるっと作り直した。 ただ、作り直してもたま~にパケットが通らなくなってしまうのが起きてしまったので、ルータをちゃんとしたヤツに買い換えないといけないのかもしれない。

Serviceの公開ができるようにする

k8sクラスタ上のPodに対してノートPCからどうやってアクセスするのがいいかな~と考えていたが、Serviceをtype: NodePortで作成するのが一番ラクだと思ったのでそうすることにした。

# nginxのPodをたてる
$ kubectl run nginx --image=nginx

# type: NodePortなServiceを作成する
$ kubectl expose po nginx --name=nginx --type=NodePort --port=80

# 作成したPodがどのNodeで動いているか確認する
$ kubectl get po -o wide
NAME     READY   STATUS    RESTARTS   AGE   IP               NODE    NOMINATED NODE   READINESS GATES
nginx    1/1     Running   0          13h   172.16.168.135   node1   <none>           <none>

# 作成したServiceのnodePortを確認する
$ kubectl get svc
NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
nginx        NodePort    10.104.230.33   <none>        80:30953/TCP   11m

これで「今回作成したnginxはnode1の30953ポートで動いている」ということが分かるので、そこにアクセスすれば見えた。

# node1のIPは192.168.10.191
$ curl -is http://192.168.10.191:30953 | head -2
HTTP/1.1 200 OK
Server: nginx/1.21.0

しかしtype: NodePort(かつexternalTrafficPolicy: Cluster)の場合は、PodがいないNodeに対してアクセスしてしまっても正しいNodeに転送してくれるはずだったのに、なぜか転送されなかった。

# node2のIPは192.168.10.192
$ curl -is http://192.168.10.192:30953 | head -2
(返事がない)

調べてみると、Calicoを使ってかつ、KubernetesクラスタのCIDRをVMのと別のにしてしまうと、うまく転送してくれないとというようなIssueがあったので、その現象にあたってしまったのかもしれない?

github.com

Calicoではなく別のCNIプラグインなら大丈夫らしいので、試しにCiliumに切り替えてみたところ、うまく動いてくれたのでそっちを使うことにした。

Tailscaleで家の外からもk8sクラスタにアクセスできるようにする

次は家の外からでもアクセスしたいな~と思ったので、またまたsuperbrothersさんのを参考にして、Tailscaleを導入してみた。

tailscale.com

アカウントを作成したら、クライアントを各マシンにインストールしただけで、本当にあっさり導入できてしまった。

f:id:rkmathii:20210620004816p:plain

iPhoneにTailscaleのクライアントを入れたら、4G回線からも普通に接続できてしまった。

f:id:rkmathii:20210620012959:plain

TailscaleのMagic DNSを有効にする

Magic DNSを有効にすれば、「<マシン名>.<ユーザ名>.beta.tailscale.net」という名前で引けるようになるので、IPアドレスを覚えたり/etc/hostsに書いたりをせずに済むようになったため更に便利になった。

f:id:rkmathii:20210620004926p:plain

さっきはIPアドレスでアクセスしていたnginxのNodePortの例も、

$ curl http://node0.<アカウント名>.beta.tailscale.net:30953/
$ curl http://node1.<アカウント名>.beta.tailscale.net:30953/
$ curl http://node2.<アカウント名>.beta.tailscale.net:30953/

のようにFQDNでアクセスできるようになった。

また、kubernetes-dashboardについても

apiVersion: v1
kind: Service
metadata:
  name: kubernetes-dashboard-expose
  namespace: kubernetes-dashboard
spec:
  type: NodePort
  selector:
     k8s-app: kubernetes-dashboard
  ports:
    - nodePort: 30000
      port: 443
      targetPort: 8443
      protocol: TCP

というようなtype: NodePortなServiceを作ることで、

$ curl http://node0.<アカウント名>.beta.tailscale.net:30000/
$ curl http://node1.<アカウント名>.beta.tailscale.net:30000/
$ curl http://node2.<アカウント名>.beta.tailscale.net:30000/

でアクセスできるようになった。

f:id:rkmathii:20210620010036p:plain

証明書を設定できていないので警告は出てしまうが。。。


最終的にはこういう感じになった。

f:id:rkmathii:20210619230304p:plain

これで一通り準備ができてそれっぽく使えるようになったので、Kubernetesクラスタで色々実験するのが捗りそう😺

あとTailscaleはすごい!!

おうちKubernetesクラスタを組む(前編)

いままで手元でKubernetes関連のなにかを試そうとすると、基本的にはThinkPad X390(Core i7-8665U 4コア8スレッド, メモリ16GB)のWindowsのWSL2上でkindを起動していたが、WSL2にメモリを12GB当ててしまうとWindowsが使う分が足りなくなってきたり、ノートPCなのでスリープしたりすると復帰時にたまにヘンになったりとあまり快適な状態ではなかったため、superbrothersさんのを参考に小さいデスクトップマシンを買ってみた。

デスクトップマシンの構成

小さめのマシンでいくつか迷ったけど、折角(?)なので使ったことがないRyzenを積んでるASUS Mini PC PN51にしてみた。 CPUはRyzen 5 5500UなのでノートPC用のだけど、基本電源はつけっぱなしにするつもりだったので消費電力が小さそうだし筐体サイズも小さいのでこれで十分かなということにした。

これだけではSSDもメモリもないので、Western Digital WD Blue SN550 NVMe SSD (1TB)と、Crucial CT16G4SFD832A x 2本を買ったので、構成はこうなった。

項目 中身
CPU Ryzen 5 5500U (6コア12スレッド)
メモリ 32GB
SSD 1TB
OS Ubuntu Desktop 20.04

メモリは32GBか64GBかで迷ったけど、VMはいくつか立ち上げるつもりはあるものの、ゲームをするわけでもなく基本SSHかRDP経由で軽くGUIを使くらいの予定なので、32GBでいいやということにした。

消費税と送料込みで、

商品 値段
PN51 47,700円
WDS100T2B0C 12,248円 (セールしてた)
CT16G4SFD832A x 2本 19,040円
合計 78,988円

と結構安く済んでよかった。 (価格ドットコムの価格推移をみた感じだと、もうちょっと前ならメモリもSSDも更に安く買えたかもしれないが…)

とりあえずKubernetesを動かす

家で使っているのは普通の家庭用Wi-fiルータなのであまり機能がないが、DHCP固定割当設定はあるのでそれでIPを固定することにして、SSHやRDPするときに迷わないようにした。

まずはvirt-managerを使って、Control Plane用に1つ(2core, 2GB)、Worker Node用に2つ(2core, 4GB)、Ubuntu ServerのVMを立ち上げた。

f:id:rkmathii:20210616235249p:plain

(いままで使ったことはないが)containerdをインストールして、kubeadmを使ってクラスタを起動して、CalicoをインストールしてReadyにするところまでできた。

f:id:rkmathii:20210616235918p:plain

その後、kubernetes-dashboardを起動させてみたが、ノートPCのWebブラウザから見ようとすると

  • kubernetes-dashboard--kubelet-insecure-tls オプション付きで起動させる。
  • デスクトップマシン上で kubectl proxy --address 0.0.0.0 --accept-hosts='^*$' を実行する。
  • ノートPC上で ssh -L 18001:<デスクトップマシンのIP>:8001 rkmathi@<デスクトップマシンのIP> を実行する。
  • ノートPC上のWebブラウザhttp://localhost:18001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/#/login にアクセスする。

というように面倒かつ安全じゃない感じの手順が必要になってしまったので、今まであまり触れてこなかったネットワーク周りを勉強しながらいい感じに直したい。

f:id:rkmathii:20210617001050p:plain

まずは↓のように家の中のノートPC経由でKubernetes上で動かしているWebアプリにアクセスできるようにしたいが、DNSとかMetalLBとかTLS周りとかがよくわかってないので色々試行錯誤中なので、後編に続く。。。

f:id:rkmathii:20210616234717p:plain

これができたら家の外からもつなげるようにしたい。


続き

rkmathi.hatenablog.com