フューチャー技術ブログ

int32 のサロゲートキーが数年でオーバーフローしそうになった件

はじめに

はじめまして、2021/11 にキャリア入社したTIGの穴井です。失敗談をテーマにした連載の2本目です。

Go の O/Rマッパ(Object-relational mapping)である gorm 利用時の構造体にて、サロゲートキーの型が int32であることに起因して、当該フィールドが数年でオーバーフローしそうだった件について、なぜ実装時に気づけなかったのか、記載いたします。

背景

既存システムに機能追加することで、これまでとは比べ物にならないデータ量を扱うシステムとなることがあると思います。私が参画しているプロジェクトでも扱うデータ量が大幅に急増し、結果として Go の構造体の int32 のサロゲートキーが数年でオーバーフローすることが見込まれる状況となりました。

なぜ実装時に気づけなかったのか

システム開発を行う際に、開発者が共通で利用する部品を作り込むと思いますが、
その部品の利用に問題がありました。

私が参画しているプロジェクトは、全てのテーブルでサロゲートキーとシステムカラムを定義するという開発規約があるため、これらを表現する構造体を1つ定義しておき、構造体の埋め込みを利用して O/Rマッパ を利用していました。また、サロゲートキーは ID という名称にするテーブル定義規約があり、ID はデータが insert されるたびに increment されるテーブル定義 (PostgreSQL の serial 型) となっています。

上記をコードで表すと以下の通りとなります。

// BaseColumns 共通カラムの構造体
type BaseColumns struct {
ID int32 `gorm:"primary_key"`

CreatedAt time.Time `gorm:"column:created_at"`
UpdatedAt time.Time `gorm:"column:updated_at"`
PatchedAt *time.Time `gorm:"column:patched_at"`
}
// CreateSampleModel SampleModel を DB に書き込む関数(利用例)
func CreateSampleModel(ctx context.Context, userID string) error {
s := &SampleModel{
UserID: userID,
BaseColumns: BaseColumns{},
}
tx := GetTx(ctx)
err := tx.WithContext(ctx).Create(&s).Error
return err
}

このような規約がある中で実装を愚直に進めた結果、データ量に関する考慮漏れが発生し、
int32 のサロゲートキーを利用してしまい、数年でオーバーフローが見込まれる状況となってしまいました。

今回はサロゲートキーのオーバフロー問題ということで下記に int32, int64 の最大値を示します。

int32 int64
2,147,483,647 9,223,372,036,854,775,807

本事象では、DB で自動採番された ID が 2,147,483,647 を超えた際に Go の構造体にパースできずオーバーフローすることが見込まれました。

機能追加により約21億のデータを扱うシステムとなることは、稀なケースですが、このようなオーバーフロー問題はすべての人が遭遇する可能性がある事象だと思います。本事象に遭遇したことで、改めて、データ量と型設定について見直すいい機会となりました。

※本事象発覚後、int64のサロゲートキーを利用するようソースを修正しました。

さいごに

同じ苦しみを味わう人が出ないよう、本記事にて供養いたします。

次は原さんのGo言語で定数として扱いたいmapを毎回アロケートさせて性能劣化した話です。