くりにっき

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

ISUCON 13に1人チームで出場して再起動試験でfailした話

tl;dr;

タイトルが全て

今年の準備

ISUCONの前の週に Google Cloud Next Tokyo ’23 に登壇した関係で今年の準備期間は実質1週間程度でした。

とりあえず勘を取り戻すために下記の素振りをしてました。

github.com

github.com

リポジトリ

競技用のリポジトリ

github.com

ISUCON用の汎用Itamaeレシピ集

github.com

PRとDatadogを見ながら当日やったことを振り返り

当日10:00〜18:00のDatadog晒し

sinatraAPM

競技中はここを見るのが一番多かったです。

サーバ全台

puma

pumaのメモリ以外のメトリクスが後半しかないのは、DatadogのPuma Integrationの設定を入れたのが後半なため。

MySQL

具体的にやったこと

github.com

CloudFormationから起動した直後のインスタンスにベンチマーカーを実行(スコア:4148)

いつものスニペットをコピーして参照実装をGoからRubyに変更(スコア: 3781)

github.com

この時点でRuby 3.3.0-devにしてます。

Sentryとddtrace(DatadogのAPM)を有効化(スコア:3497)

github.com

ここでようやくスタート地点

rubocop-isucon導入(スコア:3389)

github.com

いつもならこの時点でN+1がどれくらいあるかindexを貼る箇所を見つけているのですが、今までとコードの書き方が微妙に違うせいでASTの構成が変わって Isucon/Mysql2部署のcop が全く動かなかったのがショックだった、、、

GET /api/user/:username/icon : DBにある画像を逃がす(スコア:3827)

Datadogを見たら GET /api/user/:username/icon が明らかにボトルネックになってたのでこいつの改善に着手。

エンドポイントを選んでDurationの降順してボトルネックを発見。

小ネタですが https://github.com/sue445/isucon13-20231125/blob/main/ruby/config/ddtrace_init.rb#L55-L65 のようなモンキーパッチをあててSQLの生クエリ(プレースホルダ? が入ってないやつ)をDatadogに送信してexplain取りやすくしてます。(ユーザが入力したパスワードもDatadogに全部生で送信されるので本番アプリには絶対にいれたらダメなパッチ)

GET /api/user/:username/icon : 画像をnginxで返す by sue445 · Pull Request #7 · sue445/isucon13-20231125 · GitHub

ソース見たらDBに画像が保存されてたのでnginxで返そうとしたのですがここで1時間以上手こずったと思います。

ChatGPTに聞きながらやってもいい感じのnginxの設定が書けずに最終的にローカルに保存した画像をsinatraで返すようにしました。

最終的にこんな感じでボトルネック改善したのを確認。

これでもまあ悪くないので1時間もかけずに早いところ諦めるべきだったね、、、(1人チームだとハマった時に無限に時間が溶ける)

fill_user_responseでDBの画像を使わないようにする(スコア:4518)

github.com

最後の方で気づいたけど画像を保存する前に計算したhashをDBに入れた方がよかった。

2台目のサーバをDB専用として投入(スコア:6893)

github.com

ここも微妙にハマったやつ。

デプロイ時に1台目のサーバのMySQLを止めて2台目のサーバのみMySQLを起動してるのに、デプロイ後になぜか1台目のサーバも止めたはずのMySQLが復活してむっちゃわけ分からずにハマってました。

最終的にはserviceファイルのAfterとRequiresが原因とわかって消したんですが、そうしたらPowerDNSが使ってるMySQLにも影響して復活させました

MySQLのスローログを無効化(スコア:7028)

github.com

どうせ見ないので無効化した

index追加祭(スコア:7132)

github.com

github.com

github.com

github.com

POST /api/initialize でやってるTRUNCATE TABLEだとindexが消えなくて、かといってMySQLだと ADD INDEX IF NOT EXISTS のような冪等性のあるDDLが分からなかったのでinitializeで実行されるsqlファイルに入れるのを諦めて手動で投入することにしました。(gitの履歴としては残したいのでこういうファイルだけcommit)

livecomment_modelがnilってNoMethodErrorになったので修正

index追加中にこういうエラーに遭遇したので修正。こういう時sentry使ってると検出できて便利っすね。( https://sentry.io のqueueが詰まってたせいか分からないけど最初の方は全然エラーが通知されなくてつらかったけど)

github.com

github.com

3台目投入(スコア:7228)

github.com

app専用として3台目を投入して負荷分散しようと思ったんですが、appがローカルのPowerDNSに依存しててそのPowerDNSを別サーバに動かすのが手こずりそうだったので実際の負荷分散は後回しにすることに。

17:21頃に再起動試験

appを1台で動かす前提にする(スコア:6949)

この時点で残り時間1時間きってたので危険な変更はしないようにしました

github.com

最後にDatadogとか諸々無効化(スコア:10663)

github.com

最終スコア

isucon.net

10,663 sue445.members.count==1 (再起動後スコア0)

ええ。マジ、、、

競技時間中に再起動試験して問題なかったのに、追試の再起動試験でスコア0になるとは思わなかった。

来年に向けて

ruby-wasm-vdomで雑なオレオレツールを作った

作ったもの

https://sue445.github.io/annict-vod-search/

github.com

モチベーション

最近縁があって Annict の編集者*1 になって、気がついた時に各アニメの配信サイトの情報を登録しています。

その時にいちいち複数の配信サイトでアニメのタイトルを検索するのが大変なので一箇所で検索できるようにしたくて作りました。(製作時間は2〜3時間)

使った技術

いつもだとこの手の雑なページはVue.jsを使うことが多いのですが、今回は面白全部 *2ruby.wasmを使ってみました。

ruby.wasmで仮想DOMを扱うためにruby-wasm-vdomを利用しています。

ruby-wasm-vdomについては下記を参照。

github.com

qiita.com

感想

  • 使い慣れたRubyで書けるのは便利
  • ヒアドキュメントでJSXっぽく書けるのは面白い *3
    • しかし入力値をそのまま出力するとXSSが発生して こういう対応 を自前でやる必要があって大変。(Railsだとデフォルトでいい感じにしてくれるのであまり気にしてなかった)
  • wasmの中でエラーが起きた時にどの行でエラーが出たかとかがぱっと分からないのでデバッグが大変だった
  • あまりドキュメントがなかったけど https://github.com/getty104/ruby-brainfuck-interpreter が参考になった

*1:データベースにアニメ情報を自由に追加できる人

*2:面白半分の2倍

*3:https://github.com/sue445/annict-vod-search/blob/be8a262271f5e26f4cfe0618e41f9e7f8ba6e008/docs/src/app.rb#L24-L42

Google Slidesで作ったスライドをSpeakerDeckに公開するためのTips

前置き

普段発表用の資料は Google Slides で作成し、Google SlidesでPDFにエクスポートした後に https://speakerdeck.com/ にアップロードしています。

長年の暗黙知が積み重なってきたのでメモ。

フォントをM PLUS 1pにする

Google SlidesだとデフォルトのフォントがArialとかになっていますが、このフォントだとPDF化した時にレイアウトが崩れたり文字が掠れるため、PDF化する前提なら初手でM PLUS 1pにするのが無難です。

参考URL

speakerdeck.com

note.com

絵文字は使わない

絵文字が含まれるPDFをSpeakerDeckにアップロードしたらいつまで経ってもスライドが作成されなかったり、作成されたスライドもダウンロードできない(ダウンロードできてもPDFがぶっ壊れていて表示できない)という事象がありました。(今はなおってるかも?)

そのため、自分のスライドではなるべく絵文字は使わないようにしています。(どうしても使いたい場合には絵文字を画像としてスライド貼り付ける)

ソースコードを貼り付ける時にいい感じにシンタックスハイライトを効かせるやつ

いくつかあるので紹介

SlidesCodeHighlighter

一番よく使うやつ。ソースコードをテキストとして貼り付けたい場合にはこれが一番使いやすかった。

romannurik.github.io

日本語の説明

monomonotech.jp

Carbon

carbon.now.sh

オフラインでも動くのと、コードを画像として貼り付けるので見る環境によって変わらないのがポイント。

VSCodeなど

VSCodeなどで書いてリッチテキストでコピペ

個人メモ用のNotionを作った

sue445.notion.site

モチベーション

業務で Notion を使ってるんだけど、データベース機能が圧倒的に強いので個人メモ用にもNotionを使ってみることにした。

maekdownで書ける方が嬉しいので esa は引き続き使い続けるつもりです。

コンテンツ

現時点で作ってるものは下記

アイドルランドプリパラ 楽曲リスト

sue445.notion.site

アプリのリリース以降各種攻略サイトが全然更新されなくて困ったので自分用に作った。(特に楽曲ごとのノーツ数がまとまってるサイトがなくて困った)

プリティーシリーズイベントDB

sue445.notion.site

各演者がどのライブに出演していたかをたまに調べたくなるのと、そのライブのBlu-rayとかが発売されているかどうかがまとまってるページがなかったので作った。

プリティーシリーズのライブ、2021年まではそれなりに円盤が発売されていたんだけど最近は発売されないことが多くてつらい...

Twitter botをAPI v2移行する(Ruby編)

https://github.com/sue445/pribirthdaybotTwitter API v2移行した時のメモ

tl;dr;

移行先のgem

自分が当時調べた範囲だとAPI v2対応しているAPIクライアントのgemは下記の2つしかなかったです。

github.com

github.com

ちなみに前者のsimple_twitterは最近メンテナになりました。

sue445.hatenablog.com

その他に必要になりそうなgem

TwitterのOAuth2のリフレッシュトークンからアクセストークンを取得するのに必要だったgem

github.com

ウェブアプリでTwitterのOAuth2認証したい場合に必要なgem

github.com

botAPI v2対応する時の注意点

アクセストークン(厳密にはベアラートークン)には有効期限がある(約2時間)ので、botで利用する時には実行時にリフレッシュトークンからトークンリフレッシュを行い毎回新しいアクセストークンを生成する必要があるのですが、一度利用したリフレッシュトークンは使えなくなるのでトークンリフレッシュの時に返ってくるリフレッシュトークンをDBとかに保存しておく必要があります。(このリフレッシュトークンを次回実行時に使う)

追記: 2023/07/21 10:40

これ書いた後に知ったんだけどAPI v2を使う上でOAuth2は必須ではない(一部のエンドポイントはOAuth 1.0aのアクセストークンとかでTwitter API v2が使えるらしい *1 )ので、OAuth1.0aであればリフレッシュトークンは考えなくてよさそう

手順

リフレッシュトークンを作成する

一番最初にリフレッシュトークンを取得するところだけは手で作る必要があります。

自分用にはこういうアプリをローカルで動かして認証するようにしていました。

github.com

Twitter Appについているデフォルトのトークンでもいいんですが、Appを管理しているTwitterアカウントとbotTwitterアカウントが異なる場合にはこっちの方がいいと思います。( https://developer.twitter.com/ への登録は電話番号認証が必要なのでなるべく1つのTwitterアカウントに集約させたい)

権限はTwitter botの要件であればこれで十分。 *2

# https://developer.twitter.com/en/docs/authentication/oauth-2-0/authorization-code
TWITTER_V2_SCOPE = "tweet.read tweet.write users.read offline.access"

リフレッシュトークンを使う場合には最低限 offline.access は必須。他は用途に応じて調整してください。

このアプリをローカルで動かせば下記のようなjsonが返ってくるので、ここにあるtokenとrefresh_tokenを使います。

{
    "provider": "twitter2",
    "uid": "14540215",
    "info": {
        "name": "sue445",
        "email": null,
        "nickname": "sue445",
        "description": "フルスタックキュアエンジニア。 プリキュア/サザエさん/プリパラ/プリ☆チャン/プリマジ/オヤジギャグコミッタ 【警告】上級者向けアカウント。フォローすると実況ですごい勢いでTLが埋まるよ!【危険】",
        "image": "https://pbs.twimg.com/profile_images/378800000394619528/fb8a746f6955be08134050ba2adaa1f2_normal.png",
        "urls": {
            "Website": "https://t.co/fZVVmoHpLx",
            "Twitter": "https://twitter.com/sue445"
        }
    },
    "credentials": {
        "token": "XXXXXXXXXXXXXXXXXXXXXX",
        "refresh_token": "XXXXXXXXXXXXXXXXXXXXXX",
        "expires_at": 1689828440,
        "expires": true
    },
    "extra": {
        "raw_info": {
            "data": {
                "verified": true,
                "protected": false,
                "description": "フルスタックキュアエンジニア。 プリキュア/サザエさん/プリパラ/プリ☆チャン/プリマジ/オヤジギャグコミッタ 【警告】上級者向けアカウント。フォローすると実況ですごい勢いでTLが埋まるよ!【危険】",
                "entities": {
                    "url": {
                        "urls": [
                            {
                                "start": 0,
                                "end": 23,
                                "url": "https://t.co/fZVVmoHpLx",
                                "expanded_url": "http://sue445.hatenablog.com/",
                                "display_url": "sue445.hatenablog.com"
                            }
                        ]
                    }
                },
                "name": "sue445",
                "location": "Tokyo / Japan",
                "id": "14540215",
                "profile_image_url": "https://pbs.twimg.com/profile_images/378800000394619528/fb8a746f6955be08134050ba2adaa1f2_normal.png",
                "url": "https://t.co/fZVVmoHpLx",
                "created_at": "2008-04-26T05:33:43.000Z",
                "public_metrics": {
                    "followers_count": 2859,
                    "following_count": 1678,
                    "tweet_count": 456027,
                    "listed_count": 182
                },
                "username": "sue445"
            }
        }
    }
}

botでのAPI v2移行

自分がsimple_twitterのメンテナをしてるってことでsimple_twitterを例に出しますが全体的な流れはtweetkitでもそんなに変わらないと思います。

Gemfile に下記を追加。

gem "simple_twitter"
gem "twitter_oauth2"

こういうclassを追加 *3

# frozen_string_literal: true

class TwitterClient
  API_ENDPOINT = "https://api.twitter.com/2"

  class Error < StandardError
  end

  # @return [Hash]
  def get_me
    simple_twitter_client.get("#{API_ENDPOINT}/users/me")
  end

  # @param [String] text
  def post_tweet(text)
    simple_twitter_client.post("#{API_ENDPOINT}/tweets", json: { text: text })
  end

  private

  # @return [String]
  def access_token
    return @access_token if @access_token

    oauth2 = TwitterOAuth2::Client.new(
      identifier: ENV["TWITTER_V2_CLIENT_ID"],
      secret:     ENV["TWITTER_V2_CLIENT_SECRET"],
    )

    # FIXME: refresh_tokenをDBなどから取得する
    oauth2.refresh_token = load_refresh_token

    ret = oauth2.access_token!

    # FIXME: refresh_tokenをDBなどに保存する
    save_refresh_token(ret.refresh_token)

    @access_token = ret.access_token
  end

  # @return [SimpleTwitter::Client]
  def simple_twitter_client
    @simple_twitter_client ||= SimpleTwitter::Client.new(bearer_token: access_token)
  end
end

ここでポイントになるのがrefresh_tokenの読み込みと保存です。

priborthdaybotだと実行環境にCloud Functionsを使っているのでFirestoreに保存するようにしていますが、ここはアプリの要件によって変わると思います。(だいたいはRDBやKVSになるはず)

具体的な作業内容は https://github.com/sue445/pribirthdaybot/pull/61/files を見てください。

simple_twitterのメンテナになった&v2.0.0をリリースした

tl;dr;

id:yhara さんのsimple_twitterのメンテナになりました。

github.com

simple_twitter については下記エントリを参照

zenn.dev

メンテナになった経緯

https://github.com/sue445/pribirthdaybotTwitter API v2対応をしていて既存のtwitter gemから移行先*1としてsimple_twitterを選んだところ、いくつか動かない箇所があったり挙動に不満があったのでいくつかパッチを投げました。

そうしたらメンテナになってました。

ちなみにこの手のパッチ投げたらメンテナになってたエピソードは1年ぶりn度目くらいです。(たくさんありすぎて数えるのを諦めた)

v2.0.0について

大きいところだと

  • positional argumentからkeywords argumentへの変更 *2
  • Twitter APIからエラーが返されたらRubyでもエラーにする *3

です。

特に前者は悩みがあったんですが、yharaさんから合意が得られたのでメジャーバージョンアップしました。

あとは

  • テストやCIを整備
  • yardで作ったgemのリファレンスをGitHub Pagesで公開
    • 頑張りポイント: getpost など、メタプロで動的生成してるメソッドに対してもリファレンスを書いた *4
  • gemspecの rubygems_mfa_required *5 を有効化

など、自分がいつもやってるやつを追加しました。

詳しくは https://github.com/yhara/simple_twitter/blob/main/CHANGELOG.md#v200-2023-07-17 に全部書いています。

今後の展望

simple_twitterという名の通りシンプルなgemなので、PRが来ない限りは機能追加をする余地はあまりなさそうです。

メソッドをメタプロで動的生成してる関係でIDEでの補完が効きづらいのが不便なのでrbsを追加したいとは思ってます。

(一応途中まではできてるんだけど *6 、利用してる他のgemのrbsがなくてsteepでエラーになるので https://github.com/ruby/gem_rbs_collection にPRを送る必要がありそう)

mainブランチよりも古いブランチでCIを実行できなくしたい

たまにやりたくなるやつメモ

モチベーション

CIでTerraformを動かしていると

  • mainブランチを自動apply
  • PRとかでマージ前に挙動を確認したい場合はPRに出ているブランチを手動apply

というワークフローを作ることがよくあるんですが、最新のmainブランチよりも古いブランチをapplyした時にapply済の構成が巻き戻って事故になるため、そうならないように防止したかったというやつです。

実際のスクリプト

どのCIで動かすかによって多少は変わってくると思うけどだいたいこれで動くと思います。

default_branch=`git remote show origin | grep 'HEAD branch:' | cut -d : -f 2 | tr -d '[[:space:]]'`
commit_sha=`git show --format='%H' --no-patch`

echo '[ERROR] 下記でエラーになった場合、実行中のブランチが古いのが原因なので git rebase を行ってください'
git merge-base --is-ancestor origin/${default_branch} ${commit_sha}

解説

  • デフォルトブランチがmainとかmasterとかdevelopとか気にしたくないので自動で判断するようにしています
  • commit_sha (現在のコミットハッシュ)に関しては、各CIで環境変数があるのでそれを利用するといいでしょう
    • GitHub Actionsだと GITHUB_SHA *1
    • GitLab CIだと CI_COMMIT_SHA *2
  • もし現在のコミットハッシュが最新のmainブランチよりも古かった場合には git merge-base --is-ancestor *3 でエラーになります
  • CIがエラーになったらだいたいみんなログを見に行くと思うので、チェックコマンドの直前に雑にechoでエラーログを出しています

おまけ:実際にGitLab CIで動かしてるやつ

GitLab CIの場合、ジョブ実行時点でremoteのブランチの内容が取ってこれていないため明示的に git fetch しています。

before_script:
  - "default_branch=`git remote show origin | grep 'HEAD branch:' | cut -d : -f 2 | tr -d '[[:space:]]'`"
  - git fetch
  - echo '[ERROR] 下記でエラーになった場合、実行中のブランチが古いのが原因なので git rebase を行ってください'
  - git merge-base --is-ancestor origin/${default_branch} ${CI_COMMIT_SHA}