- 細羽啓司 / Keishi Hosoba
- 株式会社ガラパゴス
- スマートフォンアプリとバックエンドの開発
- GitHub: https://github.com/hosopy
iOS・Android・バックエンド
深層学習✕デジタルクリエイティブの研究開発
- O/Rマッパーに関連したやらかし話
- 注) 悪いのはO/Rマッパーではなく、使い方とプロセスです
- integer桁あふれ事件
- unscoped事件
- とあるスマホアプリのバックエンド
- PostgreSQLのinteger型の主キー(id)が桁あふれを起こす
- Sequenceは元気に値(bigint)を生成するが、INSERTがひたすらエラー
- 特定のWebAPIが500連発
PG::NumericValueOutOfRange
PG::NumericValueOutOfRange
PG::NumericValueOutOfRange
PG::NumericValueOutOfRange
PG::NumericValueOutOfRange
PG::NumericValueOutOfRange
PG::NumericValueOutOfRange
PG::NumericValueOutOfRange
...
- iOS/Androidアプリで、一部データのアプリ⇔クラウド同期が失敗
- 同期失敗のインジケーターが表示され続ける
- 特定のWebAPIををnginxでシャットアウト
- テーブルへの読み書きを排除
- 仕様的にサービス全体は止めずに済んだ
- idの型をbigintに変更
- 自動実行とリトライ機構をおかげで、バックエンド復旧後のデータ復旧は自動で解決できた
ALTER TABLE items ALTER COLUMN id TYPE bigint
- 142,099,472 |
- INDEX x 1 |
- UNIQUE INDEX x 1 |
- FOREIGN KEY x 1 |
- RDS(m4.xlarge)のスナップショットで実験 |
- 終わる気配なし |
-- idをbigintにキャストしながらテーブルをコピー
CREATE TABLE items_copy AS SELECT cast(id AS bigint) AS id,... FROM items;
-- 主キー,INDEXを付与
ALTER TABLE items_copy add CONSTRAINT items_copy_pkey PRIMARY KEY(id);
CREATE UNIQUE INDEX index_items_copy_on_... ON items_copy (...);
CREATE INDEX index_items_copy_on_... ON items_copy USING BTREE (...);
-- 各種制約を付与
ALTER TABLE items_copy ADD CONSTRAINT fk_items_copy_user_id FOREIGN KEY (user_id) REFERENCES users(id);
ALTER TABLE items_copy ALTER COLUMN ... set DEFAULT 0;
...
-- テーブルをリネーム
ALTER TABLE items RENAME to items_org;
ALTER TABLE items_copy RENAME TO items;
@[2](COPY : 約13分) @[4](PRIMARY KEY : 約14分) @[5](UNIQUE INDEX : 約2時間) @[6](INDEX : 約8分) @[8](FOREIGN KEY : 約3分) @[9-10](DEFAULT, NOT NULL : 約3分) @[12-13](RENAME : 秒殺)
- 設計時に主キーや型について深く考慮せず
- O/Rマッパー任せであった
class CreateItems < ActiveRecord::Migration
def change
create_table :items do |t|
t.string :title, null: false
end
end
end
dev=# \d items;
Table "public.items"
Column | Type | Modifiers
--------+-------------------+----------------------------------------------------
id | integer | not null default nextval('items_id_seq'::regclass)
title | character varying | not null
Indexes:
"items_pkey" PRIMARY KEY, btree (id)
@5
dev=# \d items_id_seq;
Sequence "public.items_id_seq"
Column | Type | Value
---------------+---------+---------------------
sequence_name | name | items_id_seq
last_value | bigint | 1
start_value | bigint | 1
increment_by | bigint | 1
max_value | bigint | 9223372036854775807
min_value | bigint | 1
cache_value | bigint | 1
log_cnt | bigint | 0
is_cycled | boolean | f
is_called | boolean | f
Owned by: public.items.id
@6-10
class CreateItems < ActiveRecord::Migration
def change
create_table :items, id: :bigserial do |t|
t.string :title, null: false
end
end
end
@[3](id: bigserialでidはbigintとして定義される)
- デフォルトでbigint
- Rails 5.1: Default Primary Keys Are Now BIGINT
- とあるスマホアプリのバックエンド
- 外部キー条件の抜けたSELECTが実行され、意図しないレコードを参照した意図しない集計処理(バッチ)が実行された
- Full Scan発生でRDSのCPU使用率が爆発
- バッチ処理
- フロントで一部の集計データがおかしなことに
- RDSのCPU使用率が爆発したことで、APIレスポンスの遅延が増加
- バグ修正後にバッチ処理を再実行して復旧
- 幸いにしてクリティカルなデータではなかった(相対的に...)
- Relation
- Association
- Scope
- Default Scope
- unscoped
# Model
class User < ActiveRecord::Base
end
# Relation
active_users = User.where(status: 0)
active_users.class
=> Activity::ActiveRecord_Relation
active_users.to_sql
=> SELECT "users".* FROM "users" WHERE "users"."status" = 0
class User < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :user
end
user = User.find(1)
user.posts.to_sql
=> SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 1
class User < ActiveRecord::Base
scope :active, -> { where(status: 0) }
end
User.where(status: 0).to_sql
=> SELECT "users".* FROM "users" WHERE "users"."status" = 0
User.active.to_sql
=> SELECT "users".* FROM "users" WHERE "users"."status" = 0
class User < ActiveRecord::Base
scope :active, -> { where(status: 0) }
scope :male, -> { where(gender: 0) }
end
User.active.male.to_sql
=> SELECT "users".* FROM "users" WHERE "users"."status" = 0
AND "users"."gender" = 0
class User < ActiveRecord::Base
has_many :posts
scope :active, -> { where(status: 0) }
end
class Post < ActiveRecord::Base
belongs_to :user
scope :published, -> { where(published: true) }
end
Post.published.joins(:user).merge(User.active).to_sql
=> SELECT "posts".* FROM "posts" INNER JOIN "users" ON ...
WHERE "posts"."deleted" = 'f' AND "posts"."published" = 't'
AND "users"."status" = 0 AND "users"."gender" = 0
class Post < ActiveRecord::Base
belongs_to :user
default_scope -> { where(deleted: false) }
scope :published, -> { where(published: true) }
end
Post.published
=> SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't'
AND "posts"."deleted" = 'f'
個人的にはDefault Scope否定派
class Post < ActiveRecord::Base
belongs_to :user
default_scope -> { where(deleted: false) }
scope :published, -> { where(published: true) }
end
Post.published
=> SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't'
AND "posts"."deleted" = 'f'
Post.unscoped.published
=> SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't'
user = User.find(1)
user.posts.published.to_sql
=> SELECT "posts".* FROM "posts" WHERE "posts"."deleted" = 'f'
AND "posts"."user_id" = 1 AND "posts"."published" = 't'
user.posts.unscoped.published.to_sql
=> SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't'
# 脳内では...
=> SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 1
AND "posts"."published" = 't'
- コードレビューがパス |
- なんとなく正しそうなんだもん... |
- 単体テストがパス |
- beforeで別ユーザーのデータも作っとけば... |
- ステージング環境でのQAテストもパス |
- 本番デプロイ |
- フロントで一部の表示データがおかしい |
- RDSのCPU使用率が爆発 |
- 常に本番相当(規模)のデータを用意しておくべき
- 少なくともRDSのSlowQueryやCPU使用率の爆発で気づけたはず
- 主キーについて考えよう
- ステージング環境のDBは本番相当規模にしておこう