ISUCON13に1人チームで参加して14位になった

ISUCON13に参加してきた。
ISUCONは8からほぼ毎年参加しており、去年は参加できなかったので5回目の出場。
初出場の頃はスコアを出すこともできなかったものの、今年はついに例年の予選通過ラインを超えれてよかった。

やったこと

リポジトリこちら

githubのコミットを読ませてChatGPTに記事を書いてもらった。

こんにちは、ISUCON13に1人チームで挑戦し、なんと14位になることができました!このブログでは、私がどのようにしてこの成果を達成したのか、具体的な実装の詳細について共有したいと思います。
コンテストの概要
ISUCONは、日本で開催されるプログラミングコンテストで、参加者は与えられたアプリケーションのパフォーマンスを最大限に引き出すことが目標です。通常、チームで参加するのが一般的ですが、今回は1人での挑戦となりました。
実装のアプローチ
私の戦略は、アプリケーションのボトルネックを特定し、それを効率的に解消することに集中することでした。以下は、GitHubのコミット履歴を参考に、私が行った主な改善点です。
1. ロギングの無効化
* コミット: [107004] app log off
* コミット: nginx access log off
アプリケーションとnginxのアクセスログを無効にしました。これにより、ディスクI/Oの負荷を軽減し、全体のレスポンスタイムを改善することができました。
2. インデックスの追加
* コミット: [96150] add indexes
データベースのクエリパフォーマンスを向上させるために、いくつかのテーブルにインデックスを追加しました。これにより、特に読み込みが多いクエリのレスポンスタイムが大幅に改善されました。
3. キャッシングの利用
* コミット: [30078] use cache for tips !!!
頻繁にアクセスされるデータに対してキャッシングを導入しました。これにより、データベースへのアクセス回数を減らし、レスポンスタイムを短縮することができました。
4. メモリ内でのデータ処理
* コミット: [77099] use in memory icons for not found
一部のデータをメモリ内で処理することで、データベースへのアクセスを減らしました。これは、特にアイコンのような小さなデータに対して効果的でした。
結果と学び
これらの改善を通じて、アプリケーションのパフォーマンスは大幅に向上し、最終的に14位という結果を得ることができました。1人チームでの参加は大変でしたが、パフォーマンスチューニングの重要性と、効果的な戦略の選択の重要性を学ぶことができました。
ISUCONは、技術的なスキルだけでなく、戦略的な思考も要求されるコンテストです。今回の経験を活かし、次回のコンテストではさらに上位を目指したいと思います!

このブログは、ISUCON13に参加した経験を基に書かれています。実際のコミット履歴は、GitHubのリポジトリで確認することができます。プログラミングやパフォーマンスチューニングに興味がある方にとって、参考になれば幸いです。

大体合ってる。 コミット履歴を参考にしているので、最後に行ったものから反映されているけど。

もう少しやったことを補足すると

  • 環境構築 & いつものおまじない
    • 計測系ツール(alp, pt-query-digest, pprof, net-data)の導入
    • Cursor (VSCode) Remote Explorer 
1人チームなので、サーバーにCursor入れて直接ソースコード修正
    • ビルドスクリプト作成
    • 再起動試験対策・too many open files 対策

ここまででちょうと1時間くらい。
だいぶ早くはなったけど、上位のチームは30分くらいで終わってるだろうからまだまだ改善の余地はあるはず

  • If-None-Match対策
    alpでiconがボトルネックだったので、マニュアルに書いてある通りに対応
    Cursorでiconのコード選択して「以下の仕様に沿って修正してください。」とアプリケーションマニュアルのIf-None-Matchの説明部分を送りつけたらGPTが実装してくれた。(手修正する必要なかった)
    https://github.com/yyamada12/isucon13-main/commit/2cbd810adf8101be654c6043ed7786e6819ab089

  • インメモリキャッシュ
    db内のデータを片っ端からメモリ上に乗せていった。 iconに関してはsha256が重かったので、hash結果もキャッシュ。

  • ADMIN PREPARE 対策
    slow query で ADMIN PREPAREが増えてきたので、いつもの設定を追加
    https://github.com/yyamada12/isucon13-main/commit/c01c806966363e8d18a67705d10078c898991bd5

  • MySQLへのINDEX追加
    適宜重くなってきた(slow query 上位に来た)タイミングで対応 DNSへの対応は、このMySQLへのINDEX追加くらい

  • 複数台構成
    本来は1台でなるべくチューニングし切った上でリソースを見てサーバー分割したいところだが、チューニングのペースが芳しくなかった(前半にバグにハマって1~2時間溶かした)ので15時くらいの時点で焦って複数台構成に。
    DNS周りがよくわからないので、DNS + Nginx を3号機のまま、1号機をapp, 2号機を MySQL (for app), 3号機を DNS, MySQL(for DNS), Nginx という構成にした。
    初手では、/api/register が PowerDNSのCLIを直叩きしていたので、 /api/register のみ3号機に残していたが(本来は/api/initilaizeも必要だった)、PowerDNS関連だけで良いよねということで、 /api/initialize_dns/api/register_dns を新規追加して、1号機のappから呼ぶように修正した (initialize, register)

  • 後片付け
    ng_words がボトルネックになった頃に残り時間がなくなったので最後に計測ツールを消して、go application のlog出力も止めて、最高スコアが110461

感想

前半に手間取って1~2時間足踏みしていたのが痛かった。 それもあり、うまくいけば10位以内はいけるのかもと思えたが、3位以内は1人チームのままだと絶望的だなと感じた。 特に1位は強すぎる、、

何はともあれめちゃくちゃ楽しませてもらった。運営の方々はありがとうございました。

gofakeitを用いたダミーデータ生成

go でダミーデータを生成する機会があり、gofakeitgithub.com を使ったので備忘メモ。

基本的な使い方

READMEの通り。
いろいろなダミーデータが用意されているので、用意されているデータであればメソッドを呼び出すだけで生成できる。

gofakeit.Name()             // Markus Moen
gofakeit.Email()            // alaynawuckert@kozey.biz
gofakeit.Phone()            // (570)245-7485
gofakeit.BS()               // front-end
gofakeit.BeerName()         // Duvel
gofakeit.Color()            // MediumOrchid
gofakeit.Company()          // Moen, Pagac and Wuckert
gofakeit.CreditCardNumber() // 4287271570245748
gofakeit.HackerPhrase()     // Connecting the array won't do anything, we need to generate the haptic COM driver!
gofakeit.JobTitle()         // Director
gofakeit.CurrencyShort()    // USD

データを変更する場合

gofakeitでは基本的に、ライブラリ側で英語で用意されたデータセットの中からランダムで生成される。
このデータは、gofakeit/data パッケージの Data という構造体の中に格納されているので、そこを上書きしてあげることで、データセットを変更できる。

package main

import (
    "fmt"

    "github.com/brianvoe/gofakeit/v6"
    "github.com/brianvoe/gofakeit/v6/data"
)

func main() {
    gofakeit.Seed(1)
    fmt.Println(gofakeit.Name()) // Bart Beatty
    fmt.Println(gofakeit.Name()) // Cordia Jacobi

    data.SetSub("person", "first", []string{"太郎", "二郎", "三郎", "四郎", "五郎"})
    data.SetSub("person", "last", []string{"佐藤", "鈴木", "高橋"})

    fmt.Println(gofakeit.Name()) // 二郎 佐藤
    fmt.Println(gofakeit.Name()) // 太郎 高橋
}

カスタムのデータを生成する場合

AddFuncLookup で、カスタムのデータ生成関数を追加できる。
追加した生成関数を使う際には、Generate という関数を利用できる。

package main

import (
    "fmt"
    "math/rand"

    "github.com/brianvoe/gofakeit/v6"
)

func main() {
    gofakeit.Seed(1)

    customIndustries := []string{
        "IT",
        "製造業",
        "医療",
        "金融",
        "教育",
        "エネルギー",
        "不動産",
    }

    gofakeit.AddFuncLookup("industry", gofakeit.Info{
        Category:    "company",
        Description: "Random industry",
        Example:     "IT",
        Output:      "string",
        Generate: func(r *rand.Rand, m *gofakeit.MapParams, info *gofakeit.Info) (interface{}, error) {
            return customIndustries[r.Intn(len(customIndustries))], nil
        },
    })

    fmt.Println(gofakeit.Generate("{industry}")) // 不動産
}

先の例では Generate を利用して生成したが、gofakeit では Struct という関数で構造体ごとデータを生成できる機能があり、これと組み合わせることでより便利に使うことができる。

Struct を使う際には、あらかじめ構造体にfake というタグで生成したいデータを指定する。 参考
カスタムの生成関数を利用したい場合は、 AddFuncLookup の第一引数で渡していたfunctionNameをタグにつければ良い。

package main

import (
    "fmt"
    "math/rand"

    "github.com/brianvoe/gofakeit/v6"
    "github.com/brianvoe/gofakeit/v6/data"
)

type Company struct {
    Name     string `fake:"{company}"`
    Industry string `fake:"{industry}"`
}

func main() {
    gofakeit.Seed(1)

    companies := []string{"トヨタ自動車", "ソニー", "パナソニック"}
    data.SetSub("company", "name", companies)

    customIndustries := []string{
        "IT",
        "製造業",
        "医療",
        "金融",
        "教育",
        "エネルギー",
        "不動産",
    }

    gofakeit.AddFuncLookup("industry", gofakeit.Info{
        Category:    "company",
        Description: "Random industry",
        Example:     "IT",
        Output:      "string",
        Generate: func(r *rand.Rand, m *gofakeit.MapParams, info *gofakeit.Info) (interface{}, error) {
            return customIndustries[r.Intn(len(customIndustries))], nil
        },
    })

    var company Company
    gofakeit.Struct(&company)
    fmt.Printf("%+v", company)
}

構造体の一部でダミーデータ生成が不要な場合

ゼロ値のままにしておきたい場合は、タグとして `fake:"skip"` をつければ良い。

ACID特性 by データ指向アプリケーションデザイン

ふわっと理解して忘れてを繰り返しているACID特性について
自分の理解を固めるために『データ指向アプリケーションデザイン』という書籍での説明(7.1節)を参考にまとめた。

tl;dr

  • A: 原子性(atomic)

  • C: 一貫性(consistency)

    • データベースを利用するアプリケーションの特性であり、データベースだけの責務ではない
    • 原子性や分離性によって支えられる
    • データについて常に真でなければならないことが守られていること
      • 例えば、会計システムで貸方と借方が等しくなければならないこと
  • I: 分離性(isolation)

    • 複数のトランザクションにおいて、並行に実行されることによって生じる問題が起きないこと
    • 直列化可能性(Serializability)を分離性とされることもあるが、実際の実装ではそれよりも弱い分離性が使われている。
  • D: 永続性 (durability)

    • コミットが成功したデータは、障害時にも失われないことを示す。

A: 原子性 (atomic)

まず原子性について、マルチスレッドプログラミングでもアトミックな操作というものが出てくるが、それとACID特性の原子性は似て非なるものであると言われている。

例えばマルチスレッドプログラミングにおいては、あるスレッドがアトミックな処理を実行しているというなら、それは他のスレッドからはその処理の半分だけ完了した途中の状態を見る方法が存在しないことを意味します。 (中略) これに対し、ACIDの文脈における原子性は並行性とは関係ありません。これは複数のプロセスが同じデータに同時にアクセスしようとした時に起こることを記述するものではありません。なぜならこれはACIDのIが表す分離性(isolation) が取り上げていることなのです。

とのこと。
それ以上小さな単位に分割できない、という意味では同じだが、
マルチスレッドプログラミング原子性: 他のスレッドから、途中の状態を見れない
ACIDにおける原子性: 1つのトランザクションが実施された場合、途中の状態で終了することがない
という違いがある。

次に、ACIDにおける原子性について以下のように説明されている。

あるクライアントが複数の書き込みを行おうとして、いくつかの書き込みが処理された後に、例えばプロセスのクラッシュやネットワーク接続の途絶、ディスクフル、あるいは何らかの整合性違反といった障害が発生したような場合に起こることです。仮に複数の書き込みがアトミックなトランザクションにグループ化されており、それらを含むトランザクションが障害のために完了(コミット)しなかったら、そのトランザクションは中断され、データベースはそのトランザクション中でその時点までに行われた書き込みは、すべて破棄もしくは取り消ししなければなりません。

つまり、他のトランザクションから途中の状態を見れないことを保証するのではなく、障害が起きて失敗した場合にちゃんと切り戻すことを保証する性質である。

おそらくは中断可能性(abortability)の方が原子性よりも良い言葉だったと思われます

なるほど。

C: 一貫性 (consistency)

一貫性については以下のように説明されている。

ACIDにおける一貫性という概念は、データについて常に真でなければならない何らかの言明(不変性)があるということです。たとえば会計システムの場合、すべてのアカウントでまとめれば常に貸方と借方は等しくならなければなりません。

これはあくまでアプリケーションが担保しなければならないものであり、データベースは原子性や一貫性によってこれを支えるものであると書かれている。

とはいえ、この一貫性の概念はアプリケーション固有の不変性の概念に依存しており、一貫性を保つようにトランザクションを適切に定義することは、アプリケーションの責任になります。これはデータベースが保証できることではありません。

さらには、一貫性はACIDには不要とまで書かれている。

Cは実際にはACIDに属していないのです。

I: 分離性(isolation)

これが有名なトランザクションの分離レベルと関係のある分離性である。

ACIDにおける分離性とは、並行して実行されたトランザクションがお互いから分離されており、お互いのつま先を踏みつけあるようなことがないという意味です。

古典的な教科書では直列化可能性として形式化されているが、実際にはパフォーマンスが悪いので直列化可能な分離性が用いられることはほとんどなく、より弱い分離レベルが使われる。

永続性 (durability)

永続性はわかりやすい。 トランザクションが成功したら、クラッシュしてもデータが守られているという性質。

永続性は、トランザクションのコミットが成功したら、仮にハードウェアの障害やデータベースのクラッシュがあったとしても、そのトランザクションで書き込まれたすべてのデータは失われないことを約束するものです。

感想

ふわっと理解して忘れてを繰り返しているのは、原子性、一貫性、分離性がごっちゃになりやすいからだと思った。 マルチスレッドプログラミングの文脈で、原子性と分離性を混同しやすいし、 一貫性は原子性や分離性によって成り立つものであることも混同の元だった。

この本で原子性はマルチスレッドプログラミングのものとは別物と言い切ってくれたのと、CはACIDではないと言ってくれたおかげで理解が進んだ。

Oracle Database メモ

最近Oracle Database に触れる機会が増えているので、学んだことをメモしていく。

概要

version

現在のLTSは 19cで、21cもイノベーション・リリースとして本番でも使えるレベルのリリースとして提供されている模様。
12cの次は18cに飛んでたりする、その辺りからバージョンを20xx年に合わせているぽい。

リリースの考え方は下記ブログがわかりやすい Oracle Database 19cとは何か? どういう位置づけのリリースなのか? | コーソルDatabaseエンジニアのBlog

エディション

EE とか XE 見かけてたけど、よく分かってなかったので調べてみた。

公式ドキュメント Oracle Databaseのエディション によると、

Enterprise Edition: EE
Standard Edition: SE
Express Edition: XE
Personal Edition: PE

などがあるとのこと。
SEは SEとSE1 と SE2 があるが、最新はSE2になっていくので過去のものはあまり気にしなくて良さそう。
また、XEも過去の11gとかしか使えないので、過去バージョンで許容できれば学習用に使う、という感じっぽい。

現状、本番で使うなら EE or SE2 で、違いは Oracle Databaseライセンスの定義とルールを正しく理解する ~第1回:エディション編~ | アシスト の連載を見るとなんとなくわかる。

資料

公式のマニュアルが結構充実している。

「概要マニュアル」を読んでいくと良さそう。 Oracle Databaseデータベース概要, 19c

追加で、

Oracle Database 2日でデータベース管理者 19cOracle Database 2日で開発者ガイド, 19c に進むと良さそう

また、 Oracle Japan のブログ の連載も良さそう。

https://blogs.oracle.com/otnjp/post/shibacho-index

とか

https://blogs.oracle.com/otnjp/post/tsushima-hakushi-index の連載を追っていきたい

M1 Mac のローカルで実行する方法

Running x86_64 Docker Images on Mac M1 Max (Oracle Database 19c) - DBASolved

この辺を参考に。
aarch64用のdocker image が用意されていないため、 M1 Macamd64 の docker image を実行できるように colima を導入する必要がある。

基本コマンド

sqlplus で DBに接続

sqlplus <ID>/<パスワード>@<IP or ホスト名>:<ポート番号>/<SID>

例)

sqlplus system/password@localhost:1521/ORCLCDB
sqlplus system/password@localhost:1521/ORCLPDB1

SQL*Plusコマンド一覧

接続中のコンテナの確認

show con_name;

CON_NAME
------------------------------
ORCLPDB1

表領域

表領域の確認

select * from dba_tablespaces;

表領域を構成する データ・ファイルの確認

select * from dba_data_files;

テーブル情報の確認

select * from user_tables where table_name = テーブル名;
desc テーブル名

データ・ディクショナリ

データ・ディクショナリと動的パフォーマンス・ビュー

データベースに関する情報はデータ・ディクショナリという形で提供され、実表とビューがある。

DICT_COLUMNSを利用するとどんなカラムがあるか確認できる

例 )

select * from DICT_COLUMNS where TABLE_NAME='USER_TABLES';

モノリスからマイクロサービスへ: マイクロサービスを選択する理由

モノリスからマイクロサービスへ』を読んだ

マイクロサービスを選択する上での参考になりそうな情報があったのでメモ マイクロサービスを導入するためには明確な理由を持つべきであり、同じ結果を達成できる他の方法も踏まえて検討すべし、とした上で、2.2章でマイクロサービスを選択する代表的な理由と、同じ結果を達成できる他の方法が列挙されている

  1. チームの自律性を高めるため
    マイクロサービスによって、チームの規模を小さく保つ
    代替案 チーム設計が本質で、マイクロサービス化は必須ではない モジュラーモノリスでも実現できる

  2. 市場投入までの時間を減らす
    デプロイ独立性から、各マイクロサービスは他サービスとの調整なくリリースできる
    代替案 リリースを早めるための手法をいくらでもある ボトルネックに効く対策が必要

  3. 負荷への費用対効果が高いスケーリング
    マイクロサービス単位でのスケールができる
    代替案 既存のモノリスのスケールアウト、スケールアップで十分な場合も多い

  4. 堅牢性を改善する
    サービス全体を止めなくても、マイクロサービスを変更できる、という意味合い
    単にマイクロサービスは堅牢性があるというわけではなく、 "むしろ、マイクロサービスはネットワークの分割やサービス停止などに一層耐えられるようなシステムの設計を必要とする"
    代替案 LBによる冗長化や、地理的なインフラの分散などで十分な場合も

  5. 開発者の数を増やす
    マイクロサービスごとにチームができるので開発者のスケールができる
    代替案 モジュラーモノリスでも複数チームで担当を分けることができる
    ただしモジュラーモノリスではデプロイ時はチーム間で調整が必要なため限界がある

  6. 新しい技術を受け入れる
    サービスごとに別の技術を選択できるので、一部のサービスだけ新しい技術を採用する、といったことができる

React の context API があれば redux は不要なのか

React hooksの登場とともに useContextが現れた当時、reduxは必要なくなる?と疑問に思って軽く調べた記憶がある。 当時は、mediumの記事 にたどり着いて
hooksはreduxに取って代わるものではなく、必要であるのであれば変わらずredux導入しなさい、という結論だけそうなのかーと受け入れていたが、 理解が浅く具体どんな場合にreduxが適するのか、が分かっていなかった。
本日改めて記事を読み返してちょっと腹落ちしたのでメモしておく。

本題

useContextとreduxを比べるとき、解決したいこととしては以下だと思う。
- 複数のコンポーネントで同一のstateを利用したい
- だが、親コンポーネントからpropを何度も経由してstateを受け渡すのは辛い

https://zenn.dev/luvmini511/articles/61e8e54853bc13#1.-%E3%83%A1%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%AC%E3%83%BC%E3%82%B8%E3%81%AE%E5%BF%85%E8%A6%81%E6%80%A7

上記の記事の 「1.メインストレージの必要性」の話。

これに対するuseContextとreduxが提供する機能にはそもそも違いがある。

mediumの記事には以下のようにあり

And while some developers opt to use context to manage their entire application state, that’s not really what it’s designed for. According to the docs,

Context is designed to share data that can be considered “global” for a tree of React components, such as the current authenticated user, theme, or preferred language.

In other words, things that aren’t expected to updated frequently.

developers は contextをアプリケーション全体のstate管理に利用しようとするが、contextはそういった目的で作られた訳ではない。
"contextは、認証されているユーザー、テーマ、優先言語など、Reactコンポーネントのツリーの「グローバル」と見なすことができるデータを共有するように設計されています。"
言い換えると、contextは頻繁に更新されることが期待されていないものである。

ということだった。
つまり、頻繁に更新されるstateを複数コンポーネントで利用するなら、contextよりもreduxの方がより目的に適した手法である。
contextを利用しても実現できるが、以下のようなデメリットもある。
コンポーネントの再利用性が損なわれる
↑ contextのProvider配下のコンポーネントは、context経由でstateを受け取れることを前提に作られ、 contextのProvider配下以外には利用できなくなるため 。 reduxであれば、一元管理のstoreにはどこからでもアクセスできるため、再利用しやすい 。
・contextのProvider配下のコンポーネントの不要な再レンダリングを起こしやすい
↑ 基本的にはcontextで管理しているstateが更新されると、contextのProvider配下のコンポーネントは全て再レンダリングされるため(回避策はあるが)。
reduxであれば、connectされている(またはuseSelectorを利用している)コンポーネントのみが再レンダリングされる 。

また、reduxを使うメリットとしては、複数のコンポーネントでstateを利用できるだけではなく以下のようなものがあり、これもreduxを利用する大きな理由となる。
- middlewareを利用できる
- 開発者ツールやデバッグ機能が充実している

ただし、reduxは学習コストが高い上にコード記述量も増えるので、導入は慎重に行いたいというのが自分の考え。

まとめ

reduxを利用するモチベーションは以下
- 頻繁に更新されるstateを複数コンポーネントで共有したい
- stateの更新に際してmiddlewareを利用したい
- stateの更新に際してreduxの充実した開発者ツールやデバッグ機能を利用したい

これらが、redux導入によるデメリット (学習コスト、コード記述量の増加、バンドルサイズの増加、ライブラリ管理コストなど) に見合っていれば、導入を検討したい。

新規VPC内のEC2をインターネットに繋ぐ

これまでデフォルトVPCを利用したり、既にインフラエンジニアが作成してくれたVPCを利用してきており、 新しくVPC作ってインターネットに繋がるように設定したことがなかったので、自分で手を動かしてやっておく。

今まではこのようなチュートリアル的な内容はやるだけやって満足していたが、世に似たような記事が溢れているので記事を書くことをためらっていたが、 やるだけだと数ヶ月離れると全て忘れてしまうので、自分のために記事に起こして、思い出し工数を少しでも下げてみようという試みから残しておくことにする。

デフォルトVPCを使わず、新規VPCからインターネットに繋がるEC2を立てる手順

ちなみにデフォルトVPCは、AWSアカウントに付いてくるいい感じの設定のVPCであり、新しくVPCをデフォルト設定のまま作成したものとは別物。

VPCおよびサブネット作成.
② インターネットゲートウェイ(IG)作成.
③ 作成したIGをサブネットに紐付け.
④ IGをサブネットに紐づけるだけではインターネットに繋がらず、以下のどちらかでルートテーブルに追加する必要がある.
 ④ -A 作成したVPCにデフォルトで付いてくるメインルートテーブル の設定を編集して作成したインターネットゲートウェイ用のレコードを追加する.
 ④ -B ルートテーブルを新規で作成して、 作成したVPCに紐づけ & ルートテーブルの設定を編集してレコード追加.
⑤ EC2インスタンスを作成したVPC上、 自動割り当てパブリック IP: 有効 として作成.

作成したEC2にsshできればOK。EC2の中からcurlgoogleとかを叩けば繋がることも確認できる。

メインルートテーブル

VPCを作成すると自動的に作成されるもので、明示的にルートテーブルに紐づけていないサブネットは、このメインルートテーブルに自動的に紐づけられる。