くりにっき

フルスタックキュアエンジニアです

Herokuにあった個人アプリを軒並み対応した

背景

Herokuの無料プラン終了のため10個以上あった個人アプリを1ヶ月くらいかけて色々対応してました

blog.heroku.com

対応したと言ってもHerokuから移行しないという判断を下した対応もあるのでその辺含めて書いていきます。

やったこと1. 廃止

これが一番簡単

それなりに利用されてそうだけど諸般の事情*1で移行しないと決めたアプリに関しては下記のように「NO LONGER MAINTAINED !」ってお知らせをだして、GitHubにもお知らせを書いてリポジトリアーカイブしました。

https://github.com/sue445/app-stat-api#no-longer-maintained

具体的には下記が該当

github.com

github.com

github.com

github.com

github.com

実は一番最後のやつに関しては途中までGoogle App Engine(以降、AppEngineと呼称)に移行してたんですが *2、移行作業中にこれを動かしてるTwitterボットのアカウントが凍結してることを知って一気にやる気が消えて移行断念しました...

ボットが凍結されていたらツイート投稿時にエラーになってるはずなんだけどRollbar入れてたのに検知できていなかったっぽくて本当に謎。

やったこと2. GCPに移行

完全に自分の慣れの問題だけど手持ちのHerokuアプリはほぼ全部GCPに移行しました。

僕自身はAWSGCPを両方とも業務で使い倒しているからAWSにも全然移行できたと思います。

この手のプラットフォーム選定は誰しも一家言持っているはずなのでみんなも自分が好きなやつに移行すればいいと思います。

GCPといっても色々ありますが、僕は下記のような選定基準で使い分けました。

ユースケース

URLベースで見たユースケース

  • URLベースだとこんな感じ
    • 今まで作ってきたツールが割とウェブアプリとかwebhookとかが多かったので自分の中ではURLを起点に考えるのが自然でした
  • SlackやGitHubに設定するwebhookであればランダムな文字列が入っていても問題ないと思うのでCloud FunctionsやCloud Runでよい
    • ちなみにGoogle App Engineだと一度起動したインスタンスはリクエストの処理時間が一瞬だったとしても最低15分間の費用が発生するため *3、webhook用途だとあまり向かないというのもあります
    • Cloud Functions *4 やCloud Run *5だといずれも100ミリ秒単位の課金
  • 普通にブラウザからアクセスさせたい場合はそういうランダムな文字列が入っていると見栄えがよろしくないのでGoogle App Engineを使うかカスタムドメインを使うしかなさそう
  • 安いTLDを使えばドメインはそんなに費用かからないしGCPのマネージドSSL証明書も無料なのでカスタムドメインでもええやん説はあるんですが、SSL証明書を紐づけるためのロードバランサーが個人アプリだと高くつくんですよね...

実行環境で見たユースケース

  • 図中の「使いたいDockerイメージがGCPにある」とは、DockerイメージがContainer Registry(gcr.io)かArtifact Registry(pkg.dev)にホスティングされていること
    • Cloud RunやGoogle App Engine Flexible EditionはこれらのDockerイメージしか対応していないので実は重要ポイント
    • Cloud Runで動かしたいためだけにいくつかの個人ツールはArtifact RegistryにDockerイメージをホスティングしました
  • DockerHubやGitHubのContainer Registry (ghcr.io)のイメージを使いたい場合はGKE一択
    • GCEインスタンスdocker-compose up もできなくはないけどマジでサーバの管理やりたくなさすぎる...(2回目)

実際にGCPに移行したアプリ達

Cloud Run

HerokuからCloud Runに移行したのは下記

github.com

github.com

github.com

採用理由としては

  • アプリケーションをDeploy to Herokuで配布してたので、脱Heroku後はDockeriseしてDockerイメージとして配布したかった
  • リリース用のバージョンtagとは別に、mainブランチの内容を常にデプロイして動作確認として使うための環境を考えたらCloud RunかAppEngine(Flexible Edition)が適切だった
  • emoy_webhookは会社アカウントのHerokuでも動かしているのでそっちのHeroku対応必要だったのだが、OSSとして配布してるDockerイメージを会社のGCPプロジェクトのCloud Runで動かすのが一番楽

って感じだったと思います。

個人アプリを配布したい場合はアプリケーションをDockeriseしてArtifact RegistryでDockerイメージを配布し、それをCloud Runとかで動かせるようにするのが便利ですね

ちなみにCloud Run移行後に Cloud Run Button の存在を思い出したんですが、DockeriseとしとけばCloud Run以外のDocker環境でも動かせるのでこれはこれで対応としてはありだったと思ってます。

Cloud Functions

HerokuからCloud Functionsに移行したのは下記

github.com

Cloud Run移行したアプリと違ってこれはDockerイメージとして配布する必要がなかったのでCloud Functiinsに移行しました。

ちなみにこのアプリは素朴なSinatraアプリなのですが、https://github.com/GoogleCloudPlatform/functions-framework-ruby を使うと下記のような感じでSinatraアプリを手軽にCloud Functions対応できてむっちゃ便利でした。

FunctionsFramework.http("regional-rb-calendar") do |request|
  App.call(request.env)
end

アプリケーションコードだとfunctions-framewor依存はこの3行だけなので、 Sinatra::Base を継承した App に関してはいつも通りにSinatraアプリとしてテストを書けるのが嬉しい。

AppEngine

OSSじゃなくて恐縮ですがサザエ実況用の個人アプリをHerokuからAppEngineに移行してました。

デプロイ時にdb:migrateするために https://github.com/GoogleCloudPlatform/appengine-ruby を使っているのですが、Ruby 3.0だと動かなかったのでパッチを投げています。もし困っている人がいたらこのパッチを使ってください。 *6

github.com

GCP移行した全てに共通してること

下記のような理由で1つのアプリにつきGCPプロジェクト(とそれに対応するTerraformリポジトリ)を1つずつ作ってます

  • 予算アラートを仕込む時はGCPプロジェクト単位でやるのが慣れている
  • 各アプリは完全に独立してるのでGCPプロジェクトも独立してるのが自然
  • AppEngineのアプリやFirestoreのスキーマみたいに1つのGCPプロジェクトで1つしか作れないものがたまにある
    • Firestoreに関してはkeyのprefixを工夫すれば複数アプリ共存させることもできるが、keyのパターンやcollection単位でIAMの権限を制御できないのでGCPプロジェクトを分けるのが確実

正直Terraformリポジトリを量産するのは大変のように思えるかもしれないですが、 https://github.com/sue445/terraform-gcp-template のおかげで1つ辺り10分前後で初期セットアップ(リポジトリ作ってから、git pushしたらGitHub Actionsでapplyされるところまで)できているのでそんなに大変ではなかったです。(自分のいつもやる構成をテンプレートリポジトリにしといて本当に良かった...)

terraform-gcp-templateに関しては下記を参照

sue445.hatenablog.com

やったこと3. CircleCIに移行

下記はCircleCIに移行しました

github.com

  • 雑なボットだとCloud Functionsでもいいんだけど、GCPプロジェクトの管理が手間なのも一理ある
  • ボット実行時の状態管理(通知したかどうかのフラグなど)が不要な場合はCIで動かすのが一番お手軽

というモチベーションでCircleCIに移行しました。

こいつは元々Herokuでボットを動かすついでにちょっとした画面も作っていたんですが、画面はなくてもいいのでこの機会に画面をなくしてCircleCIで動かせるようにしました。

ちなみにみんな大好きGitHub Actionsは https://docs.github.com/ja/site-policy/github-terms/github-terms-for-additional-products-and-features#actions

GitHub でホストされるランナーを使用している場合、GitHub Actions が使用されているリポジトリに関連付けられているソフトウェア プロジェクトの運用、テスト、デプロイ、または公開 とは無関係のその他のアクティビティ。

に該当しそうなので、ボット実行の環境として使うのは危ない予感がして避けました。

  • アプリの定期CIのためにスケジューラを使うのはいいけど、スケジューラ実行が主になるとダメだという認識
  • GitHub Actions が使用されているリポジトリに関連付けられているソフトウェア プロジェクト( = ボットスクリプト)の運用」で解釈すればセーフなんだが、サポートに問い合わせるとやぶ蛇になりそうなので聞きづらい...

CircleCIはこの手の記述はなかったので問題ないはず

付録A. 道のり

付録B. 調査メモ(移行時に参考にしたドキュメントやサービスなど)

無料プラットフォームがまとまってるドキュメント

移行作業中何回も読んだ。

blog.unasuke.com

ElephantSQL (PostgreSQL)

www.elephantsql.com

20MBまでなら無料

www.elephantsql.com

  • 前述のサザエ実況アプリではこれを採用しました
    • 毎週日曜のサザエ実況ツイート数をDBに記録してるんだけど、約10年分のデータで8.6MBしか使ってなかったのであと10年は無料プランで戦えるw
  • AWS, GCP, Azureが選択可能
  • 無料だと東京リージョンが使えるのはAWSだけですが、東京リージョンがあるだけでもかなり嬉しい

PlanetScale (MySQL)

planetscale.com

無料プランだと1 organization辺り1データベースが無料で使える

planetscale.com

  • 1データベースとはいえストレージ5GB使えるので小さいアプリなら1つのデータベースに複数相乗りは可能そう
  • Railsだとschema_migrationsが有るので複数相乗りは厳しい気がする

Redis Enterprise Cloud

redis.com

30MBまで無料

Redis Enterprise Cloud Pricing | Redis

  • クラウド(AWS, Azure, GCP)のリージョンが使える
  • 今回の移行よりも前に別アプリで使ってた

付録C. Redisを雑にFirestoreに置き換えたらクラウド破産しかけた

  • OSSにしてない個人アプリでHerokuとRedisを使っていて、GCP移行時にHerokuのRedisをFirestoreに差し替えた
  • RedisのノリでFirestoreでもgetしまくったらRead Opsがヤバいことになった
    • FirestoreはReadやWriteやDeleteの回数に対して課金が発生する
  • 一番多いときでRead Opsが1日1,600万回(1日9ドル)
  • 色々チューニングして今は1日辺りの無料枠に収まる範囲で運用できるようになっている
  • クラウド破産」は正直釣りタイトルだったと反省してる

2022/09/22 20:45ブコメレス

id:tengo1985

ドメインSSL証明書はcloudflare被せちゃダメなの?とは思った。アプリの用途や利用具合なんかはわかってないからなんとも言えないんだけど。

なるほどー!普段そんなに使ってないから最初から選択肢になかったです。(仕事ではたまに使うけど個人では契約していない)

*1:主に自分のモチベーションの問題

*2:余談ですが「以降」と「移行」をかけたダジャレではありません

*3:https://cloud.google.com/appengine/pricing?hl=ja

*4:https://cloud.google.com/functions/pricing

*5:https://cloud.google.com/run/pricing

*6:appengine:execの処理の実装はserverless-exec-rubyにあるのでこっちをなおす必要がある

emoy_webhookをDockeriseした

emoy_webhookについて

Slackにemojiを追加した時に通知してくれる君です

sue445.hatenablog.com

github.com

プリッカソン以降もRubyやgemのバージョンを定期的に最新にしてそれなりにメンテしていました。

Dockeriseした発端

emoy_webhookは Deploy to Herokuボタン の仕組みを利用して配布していたのですが、Herokuの無料プランが終了するため *1 別の方法で配布する必要が出てきました。

色々考えた結果アプリケーションをDockerイメージとして配布するのがいいと判断してDockeriseしました。

こだわりポイント

Dockerイメージの配布方法

Docker Hubはpull limitがある *2 のでGitHubのContainer Registryを使っています。

普段GCPCloud Run をよく使っているのですが、Cloud Runが(GCPの方の)Container RegistryArtifact Registry のDockerイメージしか対応していないため、Artifact RegistryでもDockerイメージを配布するようにしました。

Artifact RegistryだけでDockerイメージを配布することも考えたんですが、Artifact Registryの方をpullされるとその分だけ自分にGCPの費用の発生してアレなのでGitHub Container Registryも使うようにしています。(まあGCPの転送料金も微々たるものだと思うけど...)

複数のアプリのバージョンを一箇所で集中管理してCIから一括バージョンアップできるようにした

モチベーション

  • dependabotだとライブラリの自動バージョンアップはできるのだが、RubyやGoなどのランタイムのバージョンまでは対応してないので一箇所で管理できるようにしたかった
  • 実は一箇所で管理すること自体は既に https://github.com/sue445/ci-config-itamae でやってたのだが、手元に動かす方式だと忘れるのでCIで動かすようにしたかった
  • CI化は割とすぐに終わったのだが、パブリックリポジトリの設定とプライベートリポジトリの設定が混在してたのでこのリポジトリもプライベートリポジトリでずっと運用していた。しかしリポジトリの数が多すぎて*1 ActionsのQuotaを使い切りそうになったのでパブリックリポジトリの設定とプライベートのリポジトリの設定を分離して前者を全公開にした

リポジトリ

github.com

動いてる風景

対応形式

どんな感じで設定するのかはGitHub Actionsの設定ファイルを見ればだいたい分かると思います。

仕組み

  1. 対象のリポジトリgit clone
  2. cloneしたリポジトリに対して Itamae を流し、Gemfile.circleci/config.yml などをいい感じに修正
  3. ファイルが修正されて差分が発生すればPullRequestを投げる

といった一連の流れを全てGitHub Actions上で行っています

リポジトリに適用してるItamaeのレシピファイル

https://github.com/sue445/myapp_version_upgrader/tree/main/cookbooks

完全に自分のアプリの構成に依存してるので他の人が流用したい場合には色々手を入れる必要があると思います。

こだわりポイント

CI用にGitHub Appを作った

GitHub Actionsのデフォルトのトークン( secrets.GITHUB_TOKEN )は自分自身のリポジトリの権限しか持っておらず、かといってパーソナルアクセストークンだと利用するリポジトリの範囲を絞れなくて嫌だったので専用のGitHub Appを作りました。

GitHub Appについて詳しくは下記エントリを参照。

sue445.hatenablog.com

*1:パブリックとプライベート合わせて30リポジトリくらいで大半がパブリックリポジトリ

シレン5の運命の地下を99F踏破した

原始99F踏破した時の記録

sue445.hatenablog.com

運命の地下って?

  • 原始と同様の持ち込み不可&未識別ダンジョン
  • 原始との違いは下記
    • 原始は昼ダンジョンだが運命の地下は昼夜の切り替わりがある
      • 夜だと視界が狭くなったり敵は凶暴になる
    • 原始よりも固定店が少ない
    • 異種合成のマゼルン種が夜にしか出ない

以下、「運命」と呼称

原始99Fクリアするのに半年以上かかったと思うんだけど、運命99Fはそんなに苦労しなかったと思う。(1週間位?)

立ち回り

wiki参照

seesaawiki.jp

技は下記

  • 感電破
  • 自動ナオッテルー
  • ドコ? カイ弾
  • ルームやりすごし閃光
  • ルームカイフク閃光
  • おいでよ混乱破
  • 道具ナレナレ破x2

基本的には

  1. フロア降りた直後においでよ混乱破で敵を呼び寄せる
  2. 感電破で敵を殲滅
  3. ドコ? カイ弾で階段の位置を把握して直行
  4. 道中の敵は他の技でなんとかする
  5. 道中のアイテムは拾うが不用意に探索はしない(感電破で敵を倒してもアイテム入手できるのでそんなに困らなかった)

のような流れ

序盤

  • 松明がほとんど出なくて無灯火で夜フロアを突っ切った
  • 食料もあまり出なくて草で食いつないだ
  • 25Fの固定店でイチかゼロの盾と爆発隠の盾をゲットして38Fのダークマゼモンで異種合成
    • 途中でドラゴン草も見つけたので盾の印にした
    • この辺りが道具欄を圧迫してきつかった...

中盤

  • 50F前後で動かずの盾と夜の盾を入手
  • 51Fにモンスターハウスがあってフロア移動の罠があったので固定店は無視した

終盤

  • 昼フロアの開幕直後に敵に囲まれた時は正直ヤバいと思ったけど、飛びつきの杖で空中に飛びつき&ワープした先の袋小路の部屋で土塊の杖で入口を塞いで夜待ちした
    • シレンがいる部屋に敵が沸かないという仕様があるため、入口さえ塞げばパコレプキンのような壁抜けがいない限り安全にターン経過できる
    • これ以外でも土塊の杖は夜待ちで重宝したので今回のMVP
  • 夜の99Fで敵を殲滅した直後に昼に変わって敵が復活したのはやばかった
    • 階段まで距離があったので底抜けの壺を割って落とし穴を作って突破した

ISUCON12に1人チームで出て予選敗退した話 #isucon

去年のやつ

sue445.hatenablog.com

今年参加するためにやったこと

自分の得意分野は手の速さよりもツール作成による自動化なので、去年の予選敗退直後から今年のISUCONに向けて準備していました。

途中2ヶ月間くらいはAWS Certified Solutions Architect - Professional(AWS SAP-C01)の試験勉強 *1 をしてたので完全に手が止まってましたが、それ以外の期間はISUCON用のツールを作ったり過去問で素振りをしていたので、準備期間は9~10ヶ月くらいだと思います

作ったもの一覧

rubocop-isucon

github.com

ISUCONのRubyの参照実装に対して静的解析をかけるためのrubocopプラグインです。

去年の予選敗退直後に作り始めたのでこれが開発期間が一番長いです。

ISUCONの過去問のRubyの参照実装を全部読んで、汎用化できそうな改善ポイントをrubocopのcopで実装しています。

手元にISUCONのRubyの参照実装だけを集めたリポジトリを置いて何回も静的解析をかけていました。

rubocopの中でSQLのASTをparseしてN+1クエリを自動修正してる辺りはかなりの面白ポイントなので、来年のRubyKaigiのCFPに応募しようと思っています。(このgemだけで1時間話せるくらいのボリュームはある)

オチ

mysql2-nested_hash_bind

sue445.hatenablog.com

作ったものの今回は特に出番がなかった...

datadog_thread_tracer

sue445.hatenablog.com

雑に並列処理をするのに重宝。

ISUCON終了直前にDatadogを外しやすくするためにこういうラッパを作っていました。

https://github.com/sue445/isucon12-qualify/blob/main/ruby/config/thread_helper.rb

itamae-plugin-recipe-rust

sue445.hatenablog.com

Ruby 3.2のYJITを使うためにRustが必要なので作った。

itamae-plugin-recipe-datadog

github.com

競技環境でDatadogをインストールするためのプラグイン

これだけは唯一僕が0から作ったやつではないのですが、ISUCONの素振りしてて困ったポイントをパッチ投げたら気づいたらメンテナになっていました。

僕がitamae-plugin-recipe-datadogのメンテナになった経緯は下記を参照。

sue445.hatenablog.com

isucon-snippets

github.com

これ自体は去年のISUCONから使ってたのですがprivateリポジトリじゃなくてもよさそうな気がしたのでpublicリポジトリにしました。

1年間の差分はこんな感じ。 https://github.com/sue445/isucon-snippets/compare/isucon11-qualify...isucon12-qualify

チェックリスト

esa-pages.io

  • 前日まで、当日(直前)、当日(競技中)でそれぞれチェックリストを作成
  • チェック項目が93個あるんだけど自動化したことによりいくつか項目が消えたのでこれでもまだ減った方
  • 競技中のチェック項目は競技中に手が止まった時に振り返るために用意してる

Datadog

今までのISUCONはNewRelicを使ってたのですが、業務ではDatadogを使ってるのでISUCONでも使い慣れたツールを使うことにしました。

実際に使ってる設定は https://github.com/sue445/isucon12-itamae/tree/main/cookbooks/datadog/templates/etc/datadog-agent/conf.d ですが、有償プランじゃないと使えない機能を使ってるので注意してください。

今月分はまだ課金が発生してないので分からないけど、先月分のDatadogの費用は462.30ドルだったので個人で使うにはそこそこ勇気がいる金額だと思います。 (素振りしてない時はサーバを完全に止めてDatadogの課金がなるべく発生しないようにしてたけどこの金額になった)

Datadogでalpを実装した。

ISUCONでは https://github.com/tkuchiki/alp を使ってnginxのアクセスログをいい感じに集計するのが定番ですが、1人チームだとログを集計する手間も惜しいのでnginxのログを自動でDatadogに送信して、alpと同等のテーブルをDatadogのダッシュボードで表示するようにしました。

仕組みはいたって簡単で、nginxのアクセスログをfluentdで加工してdogstatsdでDatadogにカスタムメトリクスとして送信しています

デメリット

  • アクセスログの正規化部分で正規表現をゴリゴリ使ってる *2 関係でベンチマーカーからのアクセスでログが大量に出力されるとtd-agentのCPU使用率が高まる。(だいたい20~30%くらい)
  • カスタムメトリクスはFreeプランで使えず、カスタムメトリクス自体もそこそこ費用がかかる *3

Datadogダッシュボード

競技中に複数のダッシュボードを使い分ける余裕は無いと判断して1つのダッシュボードに集約させました。

はてなブログの仕様で巨大スクショのオリジナル画像が見れないので縮小前のスクショのURLも置いておきます。 -> オリジナル画像

詳細なメトリクスは別ページで見れるようにしてるけど基本的にはこのダッシュボードを起点にパフォーマンスチューニングできるようにしてます。

作ったダッシュボードはJSONにexportしてgistに上げてますが、そのままexportしただけなので細かいところを調整しないとダメかもしれないです。(あと、後述の https://github.com/sue445/isucon12-itamae を適用してること前提)

https://gist.github.com/sue445/05e139f07a6de340e8acac7e337c01ec

ISUCON数日前にDatadogのダッシュボードがバグって困った

DatadogのダッシュボードにService SummaryとしてDatadog APMのメトリクスを埋め込んでいるのですが、ISUCON数日前に急にエラーを出すようになってむっちゃ困りました。

下記のようなスクショを添えてサポートに報告したのですが現時点でまだ治っていません。(つらい)

ISUCON12の予選問題

isucon.net

isucon.net

自分が予選当日に書いたコード

プロビジョニング用のItamae

github.com

事前に作っておいてsshできるようになった瞬間に全台にapplyしてました。

競技用コード

github.com

  • 使用言語:Ruby
  • 最終スコア:2650

スコア用issue

github.com

PRで動作確認をしてマージする直前のスコアをこのissueに貼り付けてるので、実質mainブランチのスコアです

PRベースで振り返り

  • 各PRの冒頭に修正前のスコアを記載してるけどスコアがどれくらい増えたか見やすくて便利
  • 毎回スコアをコピペするのは面倒なんだけどデプロイしてる最中にやってるので時間のロスはなかった

初期セットアップ (3035 -> 1297)

github.com

  • 手元からデプロイできるところまでなので結構コミットは多め
  • 参照実装をGoからRubyに切り替えたことでスコアが下がった
  • この時点でDockerを剥がそうと思ったけど手こずったので別PRにした

rubocopでauto correct (1297 -> 1757)

github.com

  • rubocop-isuconとrubocop-performanceだけ有効化した状態でauto correct
  • rubocop-isuconの機能として、ローカルのMySQLにISUCONのスキーマを入れておけば SELECT * FROM tenantSELECTid,name,display_name,created_at,updated_atFROM tenant みたいな感じで自動変換してくれます
    • 使ってないカラムをSELECTするのは遅いので後から不要なカラムを削りやすくしてる
  • どのcopでどんな変更が行われてるかはCommitsを見てください

Docker剥がし (1757 -> 1536)

github.com

Ruby 3.1.2から3.2.0-devにしたけどスコアそんなに変わらなかった...

DatadogとSentryの有効化 (1536 -> 1842)

github.com

  • ここでスコアが増えたの本当に謎すぎる。(だいたいスコア下がるはず)

sort -> sort_by (1842 -> 1667)

github.com

  • Datadog見て最初に気になったのが GET /api/player/competition/:competition_id/ranking だったので見たところ、sortをsort_byにすることで最適化されるので手始めに実施

sqliteのテーブルにindexを貼った (1667 -> 1652)

github.com

  • sqliteでexplainとる方法がぱっとわからなかったので脳内explainを頼りにindexを貼った

サーバ追加 (1652 -> 2685)

sqliteがある関係でpumaを複数台にできなかったので

  • host01: nginx, MySQL
  • host02: puma
  • host03: auth

の構成になった。MySQLとpumaが分離できたのでMySQLinnodb_buffer_pool_sizeの設定もこの時点で行った

静的ファイルにexpiresを付与 (2685 -> 2542)

github.com

不要なカラムをSELECTしてたので削った (2542 -> 2432)

github.com

id以外使ってないことに気づいたので削った

複数のsqliteへの接続を並列化 (2432 -> 3291)

github.com

他にも並列化できそうな箇所ないか調べたんだけどなかった気がする

visit_historyにindexを追加 (3291 -> 1727)

github.com

  • クエリを見てたらrowsが偉いことになってたのでindexを追加
  • explain結果は改善してるんだけどスコアは下がって本当に謎

JSONエンコーダをojに差し替えた (1727 -> 2067)

github.com

こんなこともあろうかとスニペットに常備してた下記のモンキーパッチを有効化した

app.rb内のhelperを別ファイルに移動

github.com

この時点でsidekiqでの非同期処理を検討してたんだけど、sinatraとsidekiqで同じメソッドを使えるようにsinatraのhelperメソッドをIsuconHelperに移動した

sidekiq導入

github.com

sidekiqの導入と作成したworkerへの処理の移譲を同一PRでやると切り戻しが大変なので第1段階でsidekiqのデプロイだけ実装した

ランキングデータをsidekiqで作るようにしてredisに保存するようにした (2589 -> 1295)

github.com

  • エンドポイント単体だと処理時間が減ってたんだけど結果的にスコアが下がって謎
  • 1時間位かけて頑張って実装したんだけど、しばらくしたらこのPRが原因でbenchmarkerの初期化処理がコケるようになったので戻すことになって本当に悲しい

csvのBULK INSERT化 (1535 -> 2778)

github.com

  • MySQLへのINSERTのN+1クエリならrubocop-isuconで検出できるんだけど、sqliteへのINSERTだったので検出できずに気づくのが遅れた

dispense_idのUUID化 (2778 -> 4939)

github.com

  • むっちゃクエリが発行されてるところをDatadogで見つけたのでUUIDにした

player_scoreをsidekiqで生成してredisにのせる (4939 -> 2135)

github.com

jobが多すぎてqueue詰まりを確認したのでworkerを増やしたんだけど、そうすると今度はsidekiqからToo many open filesが出るようになって厳しい

github.com

SentryとDatadogを無効化しMySQLとnginxのログを止める (2135 -> 2650)

github.com

  • ラスト10分で行った作業
  • ほとんど増えなくて謎

敗因

  • sqlite
    • DatadogのAPMsqliteの処理をトレースしてくれないのでボトルネックが分かりづらかった
      • activerecord経由でsqliteにアクセスしてたらトレースできてた気がする
    • sqliteのdbのせいでアプリ本体を複数台構成にできなかった
      • sqliteを使ってないエンドポイントがあればそこだけ分離する選択肢はあったが、軽く見た感じ全エンドポイントがなんらかの形でsqliteに依存しててえぐかった
      • sidekiqのworkerもsqliteに依存してるのでサーバを分けることができなかった
    • とはいえ来年以降のためにsqlite対策するかと言われるとなあ...
  • ヤマが外れた
    • 今までの経験上予選は割とオーソドックスな構成なのでそこまで変な構成はこないと思いこんでた

良かったこと

  • 去年まではPRを作らずmainブランチに直commitする方針だったが、今年になってからデプロイスクリプトを改修してPRのブランチをデプロイできるようにした
    • 途中で方針転換する時の切り戻しが楽
  • Datadog
    • 普段から使ってるので完全に手になじんでた
    • NewRelicのAPMはmysql2に対応していなくてモンキーパッチを当てないとトレースできないのだが、DatadogのAPMはmysql2に対応してるのでその手のモンキーパッチが不要でよかった
  • 頑張ってDockerを剥がしてRuby 3.2.0-dev + YJITを実戦投入できた
  • Itamae
    • 今回Docker環境だったので競技環境にxbuildが入っていなかったのだが、自分のItamaeレシピでxbuildをインストールしてたので特に困らなかった
    • 1コマンドでいつもの環境が構築できるのが本当に便利
  • rubocop-isucon
    • mysql2系のcopはあまり見せ場がなかったがsinatra系のcop(ログの無効化)は秒で対応できて良かった
    • rubocop-isuconの開発を通してSQLのASTが読めるようになった
  • 色々OSS作ったので来年以降も使える
  • 去年は再起動試験に失敗して0スコアフィニッシュだったけど、今年は正数スコアでフィニッシュできてよかった
  • Copilotを使ったけど自分の脳内を先読みしてスニペットが作成されて便利だった

おまけ:isucon11-finalにパッチを投げた

素振りで去年のISUCON本選問題をやってたらバグを見つけたのでPRを投げておきました

github.com

itamae-plugin-recipe-rustを作った

github.com

モチベーション

Ruby 3.2.0-devでYJITを使うにはRustの処理系が必要なのですが*1Ruby 3.2.0の正式版がリリースされた時に https://github.com/itamae-plugins/itamae-plugin-recipe-rbenv でYJIT付きでビルドするために必要になりそうな気がしたので作りました。

自分のISUCON用Itamaeレシピにあったやつをgem化したのでISUCON合わせプロダクト第n弾の立ち位置です。(最近のISUCON環境だと最初からRustの処理系が入っているが、古い過去問でRuby 3.2.0-devのYJITを使うためにRustの処理系が必要だった)

使い方

itamae-plugin-recipe-rbenvとだいたい同じような使い方です

# `/.cargo/ にインストールする場合
include_recipe "rust::user"

# /usr/local/cargo にインストールする場合
include_recipe "rust::system"

datadog_thread_tracerを作った

ISUCON合わせで作ったやつ第n弾

github.com

モチベーション

ISUCONの素振りでパフォーマンスチューニングのために重い処理を

threads = []

threads << Thread.start do
  # 並列で実行したい処理1
end

threads << Thread.start do
  # 並列で実行したい処理2
end

threads.each(&:join)

みたいにスレッドで並列処理すると、

のようにスレッド内で実行した処理がddtraceでトレースできなくて困ったのでなんとかしたかったのが一番の理由です

実装までの経緯

公式ドキュメントを探してもどうやればスレッド内の呼び出しをトレースできるか見つからなかったのですが、 下記Issueをヒントにして実装しました。

github.com

このIssueだとddtrace v0系だけどv1系でシンタックスが大きく変わってるのでdatadog_thread_tracerはv1系前提で実装してます。

datadog_thread_tracerを使った時

下記のように書くことで

require "datadog_thread_tracer"

DatadogThreadTracer.trace do |t|
  t.trace do
    # 並列で実行したい処理1
  end

  t.trace do
    # 並列で実行したい処理2
  end
end

このようにトレースがDatadogで見れるようになります

頑張りポイント

rbsを書いた

最近のbundlerだと bundle gem した時に sig/ ディレクトリができていてrbsのスケルトンが生成されてるので真面目に書いてみました。

https://github.com/sue445/datadog_thread_tracer/tree/main/sig