@acidlemonについて
|'-')/ acidlemonです。鎌倉市在住で、鎌倉で働く普通のITエンジニアです。
30年弱住んだ北海道を離れ、鎌倉でまったりぽわぽわしています。
普段はVSCodeのSSHを使ったリモート開発環境で開発しています。
先週末、出先で急に空き時間が出来たので、さぁなんか滞っている作業(仕事じゃなくて趣味のほう)をやるぞ〜と思ったのですがあいにく手元にPCがない…。そんなときでも最低減の対応ができるようにiPad miniだけ持っているので、iPad miniで出来ることをやるか〜と考えたときに、そういえばVSCodeってWeb版のvscode.devがあるじゃんというのを思い出しました。
とはいえ、Web版のvscode.devはSSHを使ったリモート開発環境への接続はたしかできなかったはず…と思いながらとても久々にvscode.devを開いたら、リモートエクスプローラにRemote Tunnelの選択肢がありました。そういえばそんなのありましたね! ということで、iPad miniからまずサーバにSSHして、VSCodeのCLI版をLinuxにインストールして code tunnel
コマンドを実行してGithubアカウントを紐付けて…とやって無事 vscode.dev からRemote Tunnelで普段のリモート開発環境につながりました。
というところで空き時間が終わってしまったので(あるある…)出先でそれ以上やるのはやめたのですが、帰ってからも「またこういうこともあるかもなぁ」ということでVSCode CLIをsystemdのユーザサービスで登録して常時起動するようにして…というのをやりました。
とりあえずVSCodeのCLIはこの手順でインストールしました。/usr/local/bin
におくのでrootで実行です。
# wget -O vscode-cli.tar.gz "https://code.visualstudio.com/sha/download?build=stable&os=cli-alpine-x64"
# tar zxvf vscode-cli.tar.gz
# rm /usr/local/bin/code
# mv code /usr/local/bin/code
code tunnel
を実行してGithubアカウントなりMicrosoftアカウントなりでDevice Loginをしてアカウントとリモート開発環境を紐付けます。うまくいってれば ~/.vscode/cli/code_tunnel.json
とかができているはず。
で、code tunnel
コマンドを常時起動しておきたいのですが、rootとかで動かすのもなあ…ということで systemctl --user
を使ってユーザ権限で動かすことにしました。Zennにある[Linux] systemdのユーザインスタンスで、サーバ起動後にユーザ固有のジョブを自動実行するという記事を参考にします。まず ~/.config/systemd/user/code-tunnel.service
を作ります。
[Unit]
Description=VSCode Tunnel
After=network.target
[Install]
WantedBy=default.target
[Service]
Type=simple
ExecStart=/usr/local/bin/code tunnel
WorkingDirectory=/home/{{ACCOUNT}}
Restart=always
あとは記事の手順通りにやっていけばOKです。
$ systemctl --user daemon-reload
$ systemctl --user enable code-tunnel.service
$ systemctl --user start code-tunnel.service
うまく起動していれば systemctl --user status code-tunnel.service
で Active: active (running)
になっているはず。VSCodeのRemote Tunnelにでてくるはずです。
そんなわけで、せっかくSystemdの設定までやったんだから普段使いの開発もRemote SSHからRemote Tunnelに変えちゃうか! と思って週明けの今日すこし試したのですが、SSHのほうがやっぱりいいな! という感じでした。この2つはアーキテクチャ的な違いがあって、SSHの場合はVSCodeとリモート開発環境がSSHで直結しているのでレイテンシーがよいです。Remote Tunnelの場合は間にMSのサーバが挟まっていてそこを中継しているというのがレイテンシー的に不利。Remote Tunnelはリモート環境から中継サーバに接続しにいくので、ポートを晒さなくていいというのはあります。
また、Remote SSHの場合はVSCodeのUIはローカル側で処理されるが、Remote TunnelはUIも含めてリモートで処理されるのでそのぶんもっさりするみたいなのもあるようです(入力した文字は通信中灰色になり、その後黒くなります。moshみたいなもんだ)。そのアーキテクチャの違いもあってWeb版のVSCodeでも使えるというのはあるので、iPadOSで使うため、という用途にはRemote Tunnel一択になりますが普通のPC/MacでやるならSSHのほうがパフォーマンス面で有利だなあということが分かりました。
自分の場合はリモート開発環境はEC2になっているので、PC/Macからの接続はSession Manager経由のSSH接続にしてるのと、iPad miniからはVPN接続必須にしてVPN経由の出口だけに22番ポートを空けているのでインターネットフルオープンSSHはもう使ってません。とはいえ意外とSSHはなかなか捨てられないものです。
さて今年も残すところあと3週間。11月末にISUCON13がありまして、その直後からシンガポールに5泊6日で遊びにいってた関係でISUCONのブログを書くのがすっかり遅くなってしまいました。今年のチームメイトは… いません! ISUCON9以来の一人参加です。チーム名はinit Sでした。昔からLinuxをこねくり回して壊していた人にはおなじみのシングルユーザーモードに入るコマンドから取った名前です。acidlemon is entering single user mode. ということです。
ISUCON9のときは「いやー一人でやるのは明らかに手が足りなすぎてしんどすぎる。もう一人ではやらない」って思ってた気がするんですが(実際点数も全然伸びた記憶がない)、人は喉元過ぎれば熱さを忘れる生き物ということでまたもや一人で出てしまいました。
結果から言うと、最終スコアは51172ということで上位30位ラインのボーダー付近だったのですが、最後のこのスコアを出すためにいれたtagsのキャッシュがちょっとミスっており再起動後の試験(目視確認)でNGになる感じで失格という感じでした。まぁ、目視確認やってもらえるレベルのスコアが出てたという点ではそれなりに健闘できたかなというところです。
一人で出るとなにが大変かというと、とにかくチームメイトに組長がいないというのに尽きます。組長いればアカウントつくったりhosts設定したりデプロイ設定したり細かいところキャッシュしたり全部やってもらえる(もちろん自分でやるよりも手が早い)のでデカイボトルネックに集中できるんだよな~ というところなんですが、今回はいつの間にかアドバイザーで運営側に入ってたようなのでしかたない。まこぴーさんもそーだいさんと組んでるし、ということで、初動なにするかとか組長が普段やってくれていることを自分で出来るようにする訓練だけやっておくことにしました。
さくらのクラウドクーポンを作ってISUCON11予選の環境をつくり、ちゃんとインスタンス3台+ベンチ1台を用意して、そこから初動の訓練をちょろっとだけ数日前からやりました。
apt install
で色々入れる(dstat, netdata, percona-toolkit, net-toolsなど)アプリをどう高速かするかとかは練習してもしかたがないので、ひとまずお決まりの頻出インフラ系タスクを当日悩まないように復習したという感じですね。
あとはisucon13リポジトリを作ってお手元にcloneしておいて、当日を迎えました。
まずは初動…ということで、事前に訓練したサーバのセットアップなどなどから作業開始。上に書いたいろいろの他に
sudo dd if=/dev/nvme0n1 of=/dev/null bs=1M
/initialize
を1回isu03に向けたりしたが、後にinit.shみたらちゃんとホスト指定で初期化できることが分かったので戻した10_schema.sql
を流し込んでいなかったので、DROP TABLE IF EXISTSしつつスキーマを毎回initializeで流し込み直すようにするなどをやりつつ、初期のpt-query-digestとかalpを眺めてサクッと張れそうなINDEXを張りました。
最初から3台構成でデプロイできるようにする前提で下準備をしたのでまぁまぁ手間取りつつ、さっそく高速化していきます。
まずは、DBにアイコンをバイナリで突っ込んでいる postIconHandler
がみるからにヤバそうなのでファイルに書くように対処していきます。
rm -rf ../img/user*.jpg
を仕込んでちゃんと消す 02ebcbbdここで、sha256毎回出してるのも無駄じゃろということで、ファイル名にsha256を含めて保存する 171732e8 という小技を使いました。アイコン周りは1時間くらいかけて一旦どうにかなりました。
この手の画像がDBに入る問題はISUCONだと頻出問題な感じですが、今回優しいなあとおもったのは初期データの時点では icons
テーブルがなんと空っぽということで、初期状態のDBから画像データを吸い出してstatic fileのディレクトリに置き直す…というちょっと面倒な作業が発生しなかったというところです。
アイコンが片付いたところで10000点を超えました。
アイコン周りが片付いたところで、DBのヤバそうなクエリが時間使ってるクエリの上位に出てきたので、ちょっとずつそれに対処していきます。まずはstatsから。
このstatsは明らかにループクエリなんですが、usersのJOINをやめるだけでちょっと負荷がさがるので、次にいきます。次に手をつけたのはダークモード関連です。13:30くらいからベンチマーカーが不調になりましたが、ひとまずこれくらいはサクッといけるはずということで動作確認なしで投入していきました。
update users JOIN themes t on t.user_id = users.id SET users.dark_mode = t.dark_mode;
を入れて初期データのthemesのデータをusersテーブルに反映 4337869dおにぎりを食べながらレギュレーションを読んでいると、アイコンについてはGETするときにConditional GETしていて別のAPIで返しておいたicon_hash
と変わってなければ304返すことが出来る というのがあるのでそれに対応しました dc1a2e88。で、これでOKかなとおもってnginxのアクセスログを確認しましたが304を返している形跡がなく、なんでやねんとおもったらダブルクォーテーションで囲まれているのでそれは取り除く必要がある、というやつでした 30354bcf 。ETag的なやつだから囲まれていた…ということですかね。
sqlxは普段ほとんど使わないのでstructに詰めるところを書くのが苦手なんですが、そうも言っていられないということでインラインにstructを定義してN+1クエリを解消してゴリッと1クエリで全部とるようにし、ランキングは元のロジックの集計するように修正しました 0ebf65ba。ハマることなくこれが一発でうごいたので結構自分で自分を褒めています。
この辺まで来たところで、PowerDNSの負荷もだいぶ上がってきたので、いろいろ手を打ちました。
/etc/powerdns/pdns.d/gmysql-host.conf
に設定があるので gmysql-host=192.168.0.12
に書き換えalter table records add index idx_records_name (name);
PowerDNSを1台目のままにしてあった理由はユーザ追加時に pdnsutil add-record
というコマンドを打っているからなので、これもAPIアクセスに変更しました 323293e1。これで理論上はいい感じにプロセスを別サーバに分離できそうでしたが、pdnsのAPIがlocalhostからしかいけなさそうなのでbindするアドレス変更とかまで調べる時間が惜しいのでDBだけ分離したまま1台目にプロセスを置きました。
このほかにもPowerDNSの設定で cache-ttl=90
と query-cache-ttl=90
あたりをいれたり、レコード追加するときのTTLも0じゃなくしたりなどの指定を入れてました。この辺をやっていくと、
という感じになり、WebAppとpdnsが同居している弊害が出始めます。
明らかにまだDB側にヤバイクエリ飛んでいるのにWebApp側のCPUがボトルネックになっているのはマズい、ということで、色々小手先の変更を入れて負荷を下げていきます。
ここまでやってもあまり有効打にならないということで、あえてDB側に負荷が寄るように画像のsha256のキャッシュメカニズムをファイル名からusersテーブルの追加カラムに変更 13c2b2b7というのをやったところ、WebApp側の負荷がガッと下がってまた3台目のisupipe MySQLにボトルネックが移りました。ここでスコアがゴリっと上がったので、「Globでファイル名リスティングするの重かったか~~」となりました。これで17000点くらいから30000点くらいまであがりました。
これで、DNSとアイコンがきちんと片付いた状態になったので、ここからまたisupipe DBに飛んでいるクエリをpt-query-digestで見ていって、足りないINDEXをポチポチ追加していきました。
で、ここまでやったところで17時半、38000点くらい。もうちょっと一押ししたいけどslowlog的にホットになっているもう1個のstatsのN+1クエリを潰している時間はない…ということで、この後に及んで更新がまったくない事に気付いたtagsをオンメモリキャッシュすることにしました。
なぜか何回かFailして、一部キャッシュするのを諦めるとなんとかベンチが通り、50000点に乗りました。今回は競技時間中の最終計測スコアが最終スコアになるというレギュレーションなので、netdataをアンインストールしたり各種ログを全部止めたりして2~3回回すと51172点が出たので、ここで作業終了。再起動して動かなくなる要素はないはずなので再起動試験はなし!で18時を迎えました。
で、結果発表のあとにインスタンスのログをみると、なんか計測してそうな時間に502エラーが出ている…。panicしているところのログをみていくと、「EC2が再起動されたあとに /initialize を呼ばずにtagsを読むとキャッシュに何ものってないのでOut Of Boundsなエラーがでる」というバグを最後に仕込んでしまったことがわかり、上位チームに対する事後整合性チェックでFail扱いになってしまいました。
51,172 init S (再起動後のデータチェックで不整合があったため)
競技が終わった後に、感想戦でtagsはそもそもDBから読むまでもなくGoのファイルにsliceでハードコードしてもいけてしまう、ということがわかり、いやーこれはもったいないことしたな~~ と思いました。そこちゃんと出来ていればギリギリ30位以内に入れたスコアなので、だいぶ惜しかったという感じです。まぁこの辺レビューとかで担保できなかったのは、一人でやっていることのつらさですよね。
Failで終わったわけですが、それにしても一人で特別賞ラインの5万点くらいいくのはまぁがんばったほうなんじゃないか…と思いました。ベンチマークが50分くらい止まったり、その後もキューイングの待たされ方にムラがあったりはしましたが、自分はそれによって大きく時間を無駄にしたりはしなかったのでその点はよかったなというところです。
あとはアレですね、アイコンのハッシュをファイル名に入れるっていうのはちょっとよくばりすぎましたね。最初からシュッとusersテーブルにいれておけばよかったです。その辺最初からusersにいれとけばstatsのN+1バラしはもうちょっと進んだはずなので、あと2~3万点くらいは上乗せできたのではないか…と思いました。そのほか色々思ったのは
ということで、今年のISUCONはおわり! それではまたお会いしましょう~