❄️

ISUCON12予選問題をUnistore(Hybrid Table)❄️で実装してみた。

2022/08/30に公開

Unistore(Hybrid Table)のPrivatePreview版を使わせていただける機会をいただき、isucon12予選問題をSnowflakeで実装してみました😃

Unistoreとは

Unistoreとは、OLTPデータとOLAPデータを1箇所に保管できるワークロードのことで、Snowflakeが新たに提唱したものです。
SnowflakeでUnistoreを実現したものがHybrid Tableになります。
cf. https://www.snowflake.com/blog/introducing-unistore/?lang=ja

ISUCON12とは

https://isucon.net/archives/56571716.html
Webアプリケーションを「いい感じにスピードアップするコンテスト」で今年で12回目となる有名コンテスト(ハッカソン?)で、受付から数分で参加枠が埋まってしまうくらいの人気でした。(自分もなんとか参加させていただきましたが予選敗退😅)
Snowflakeサミットで話題になったUnistoreを使える機会がいただけるということで、最新のWebアプリケーションの実装で動かしてみたいと思い。予選問題を題材に使わさせていただきました🙏

題材について

https://github.com/isucon/isucon12-qualify
公式リポジトリからソースコードを取得して修正の実装をしていきます。
また、修正に際して「ISUCON12 予選問題の解説と講評」を参考に、予め実装を省力化する対応も同時に入れます。

Unistore(Hybrid Table)に対応した実装

概要

go実装でMySQLを使う部分をHybrid Tableで動くよう対応します。

具体的にはgoの実装部(+環境変数の設定はdocker-compose-go.yaml)に対して、下記の対応を実装していきます。
※記事投稿時点でHybrid Table機能はPrivatePreview版であり、機能のアップデートによって実装方法も変わる可能性があります。

接続を設定する

  • importを追加する
sf "github.com/snowflakedb/gosnowflake"
func connectAdminDB() (*sqlx.DB, error) {
	cfg := &sf.Config{
		Account:   getEnv("SF_UNISTORE_ACCOUNT", "default_account"),
		User:      getEnv("SF_UNISTORE_USER", "default_user"),
		Password:  getEnv("SF_UNISTORE_PASSWORD", "default_password"),
		Database:  getEnv("SF_UNISTORE_DB", "default_db"),
		Schema:    getEnv("SF_UNISTORE_SCHEMA", "default_schema"),
		Role:      getEnv("SF_UNISTORE_ROLE", "default_role"),
		Warehouse: getEnv("SF_UNISTORE_WH", "default_wh"),
	}
	dsn, err := sf.DSN(cfg)
	if err != nil {
		return nil, fmt.Errorf("failed to open snowflake unistore DB: %w", err)
	}
	return sqlx.Open("snowflake", dsn)
}
  • 設定値は環境変数から読み込む形なので、合わせてdocker-compose-go.yamlにも環境変数を追加する

cf.https://pkg.go.dev/github.com/snowflakedb/gosnowflake

行バインドstructの値を大文字へ

TenantRow、その他Hybrid Tableへのクエリから返る行のカラムのバインド設定(sqlx)を大文字へ修正する(小文字のままだとエラーになるため)

type TenantRow struct {
	ID          int64  `db:"ID"`
	Name        string `db:"NAME"`
	DisplayName string `db:"DISPLAY_NAME"`
	CreatedAt   int64  `db:"CREATED_AT"`
	UpdatedAt   int64  `db:"UPDATED_AT"`
}

auto-incrementへ対応する

tenantテーブルの修正

  • createで「AUTOINCREMENT」を付けてtableを生成するも、insert時に値が自動生成されなかった。
CREATE HYBRID TABLE tenant (
  id BIGINT NOT NULL AUTOINCREMENT PRIMARY KEY,
  name VARCHAR(255) NOT NULL,
  display_name VARCHAR(255) NOT NULL,
  created_at BIGINT NOT NULL,
  updated_at BIGINT NOT NULL
);
  • autoincrement的に値を自動生成するために、sequenceを使った。
    ※このシーケンスは「Snowflakeでは、ギャップのないシーケンス番号の生成は保証されません。」とのことで、OLTPでautoincrementと同等の機能を果たすかは不明。
"INSERT INTO tenant SELECT tenant_seq.nextval, ?, ?, ?, ? ",
  • sequence生成句
CREATE sequence tenant_seq start = 101 increment = 1;

cf. https://docs.snowflake.com/ja/sql-reference/sql/create-sequence.html

primary-keyへ対応する

  • visit_historyテーブルの生成
    Hybrid TableではPrimaryKeyが必須となる。
    元visit_historyにはPrimaryKeyにあたる部分がなかったため、surrogate_keyカラムを追加する。
CREATE HYBRID TABLE visit_history (
  surrogate_key varchar(255) NOT NULL PRIMARY KEY,
  player_id VARCHAR(255) NOT NULL,
  tenant_id BIGINT NOT NULL,
  competition_id VARCHAR(255) NOT NULL,
  created_at BIGINT NOT NULL,
  updated_at BIGINT NOT NULL
);
  • visit_historyデータ作成
    surrogate_keyをUniqKeyのカラム連結&md5で生成する。
    同時にinsert or updateにも対応する。
if _, err := adminDB.ExecContext(
    ctx,
    `
    MERGE INTO visit_history t
    USING (
      SELECT MD5(CONCAT_WS('|',sub.*)) AS surrogate_key, sub.*, DATE_PART(epoch_second, current_timestamp) as created_at, DATE_PART(epoch_second, current_timestamp) as updated_at
    		FROM (select ? as player_id, ? as tenant_id, ? as competition_id) sub
	) s ON t.surrogate_key = s.surrogate_key
    WHEN MATCHED THEN UPDATE SET updated_at = s.updated_at
    WHEN NOT MATCHED THEN INSERT (surrogate_key, player_id, tenant_id, competition_id, created_at, updated_at) values (s.surrogate_key, s.player_id, s.tenant_id, s.competition_id, s.created_at, s.updated_at)
    `,
     v.playerID, tenant.ID, competitionID,
);

MySQLからデータを移行する

サーバにて初期DBからtsv出力してSnowflakeへロードする

  • tenantテーブルデータ
mysql -uisucon -pisucon isuports -N -e "select * from tenant" |gzip -c > tenant.tsv.gz
  • visit_historyテーブルデータ
mysql -uisucon -pisucon isuports -N -e 'select md5(concat_ws("|",player_id,tenant_id,competition_id)),player_id,tenant_id,competition_id,min(created_at),min(updated_at) from visit_history group by player_id,tenant_id,competition_id'  | gzip -c > visit_history.tsv.gz

動かしてみる

  • admin.t.isucon.devへアクセス(テナント一覧初期)
  • テナント追加
  • テナント追加後
  • Snowflakeのクエリ履歴

無事Hybrid Tableで動作していることが確認できました😃
(XSかつUSリージョンにもかかわらず、現実的な処理時間)

一通り実装してみて

Hybrid TableはまだまだPrivatePreview版ということで、最新の仕組みで実装されたisucon予選問題で動くようになるのか🤔、結構ハマるかも💦
と思って取り組み始めましたが、やってみたらそれほど大掛かりに修正しなくてもシュッと動くようになりました😃

ただ、現時点では接続の同時接続の設定が機能してないように思えたり(driverの方でまだ対応されてなさそう)、autoincrementが効かなかったりするのでまだ実用でバリバリ使っていくという段階ではないかなとの印象でした。

UnistoreによってOLTPとOLAPが一つになると、Webアプリケーションのデータを、ダイレクトにデータ分析できる世界になる(=ETL(ELT)がなくなる?)というカナリ野心的な挑戦だと思います。
これからのアップデートに期待します😃

Discussion