Slide 1

Slide 1 text

資料はこちら pixiv Inc. KNR 2023.10.28 数十億のレコードを持つ 5年目サービスの 設計と障害解決

Slide 2

Slide 2 text

数十億のレコードを持つ 5年目サービスの設計と障害解決 pixiv Inc. KNR 2023.10.28

Slide 3

Slide 3 text

3 今日の話 ● Palcyチームで起こった失敗話をします ● 失敗を通じて伝えたいのは基本的な3点です ○ 1. キャッシュを使う ○ 2. 不要なデータは使わない ○ 3. テーブルを適切な形に変更する

Slide 4

Slide 4 text

キャッシュを使う 4 1.同一のデータを返すもの Palcyの過去事例を使って発表します

Slide 5

Slide 5 text

5 主な対象はこんな人 ● これからRailsアプリケーションを作る方 ● これから大規模なデータをもつアプリケーションを運用する方

Slide 6

Slide 6 text

発表者とサービスの紹介

Slide 7

Slide 7 text

KNR 7 ピクシブへは2019年中途入社 前職まではフリマアプリのモバイルアプリの立ち上げ、 サーバーレスサービスの運用などを経験。 現在は Rails & AWSをメインにPalcyに関わる。 好きなものは旅行と日本酒。浅草橋はSAKE Streetさ んがあるので最高!

Slide 8

Slide 8 text

8 講談社×ピクシブによる少女・女性マンガアプリ 雑誌発売と同時に最新話を配信&オリジナル作品も 多数 モバイルアプリがメインでAndroid / iOS ● AWS ● Ruby on Rails(6.1) ● MySQL(5.7) ● Redis(6.2) https://palcy.jp/      もしくはアプリストアで 「パルシィ」と検索      

Slide 9

Slide 9 text

9 ● 掲載作品数は2,000作品以上 ● 550万DL/110万MAU/30万DAU ● 2.1億超のテーブルは10テーブル超 をサーバー・インフラ全員で3人のチームで回しています Palcy媒体資料より

Slide 10

Slide 10 text

10 のアーキテクチャ Web(PC) Web API (アプリ) (Aurora) 社内唯一のAWSインフラ のアプリ。認証のない Webと認証のある WebAPIの2系統。

Slide 11

Slide 11 text

11 ● 履歴系テーブルが設計当時から大幅にデータが増えて、スロークエ リや他の問題を産んでいる ○ アプリから履歴参照の必要があり容易に変更できない ● 午前0時になるとアクセスが集中する ○ 作品更新やミッション更新が実施されるのでこのタイミングでアク セスするユーザーがかなり多い の悩み

Slide 12

Slide 12 text

# ここから本題

Slide 13

Slide 13 text

1.キャッシュを使う

Slide 14

Slide 14 text

キャッシュを使う 14 Palcyで利用するキャッシュ ● 1. WebAPIで条件なく同一のデータを返すもの ○ 例:作品情報、特定の画面のJSONレスポンス ● 2. テーブルを参照するとクエリが返ってこなくなるもの ○ 例:履歴を対象とするミッション

Slide 15

Slide 15 text

● Active Model Serializerの利用
 ○ ほぼJSONでレスポンスを返すWebAPIが主で使いやすい
 ○ expire期間も指定できる
 ○ 多少のロジックを実装できる
 ■ 故に内部でDBアクセスする実装を行えばN+1が起こる
 キャッシュを使う 15 1.同一のデータを返すもの

Slide 16

Slide 16 text

キャッシュを使う 16 1.同一のデータを返すもの class ComicV2Serializer < ActiveModel::Serializer cache expires_in: 1.hour, except: [:is_liked, :episode_count …] attributes :id, :title, :ruby, :author, :author_image_url … has_many :tags def author_image_url object.author_image_url.nil? ? 'https://example.com/default.png' : object.author_image_url end … end Serializer側の実装例

Slide 17

Slide 17 text

キャッシュを使う 17 Railsのアップデート中に... 障害が発生 いくつかのAPIで500エラーが発生 Rendering 500 with exception: no implicit conversion of String into Hash 5.1から5.2にアップデートした際発生 キャッシュからの読み込みに失敗 キャッシュキーを変更して回避

Slide 18

Slide 18 text

キャッシュを使う 18 1.同一のデータを返すもの ● 開発環境でも実機確認を経ているのでなぜ見逃したか? ● 開発環境の大事な特性 ○ アクセスがとても少ない ○ アクセスがなければexpireのあるキャッシュは消える

Slide 19

Slide 19 text

キャッシュを使う 19 1.同一のデータを返すもの ● Rspecのテストでは同一の状態でしかテストができないため、発見で きない ● 開発環境上で定期的にキャッシュを作って、キャッシュからモデルを 読み込めるかチェックを実施 ○ エラー時Salckへの通知を行い、本番投入前に異常検知ができ るように整備

Slide 20

Slide 20 text

キャッシュを使う 20 2.テーブルを参照するとクエリが帰ってこなくなるもの ● マンガアプリにおいてはどれくらい読んだかが大事なKPI ○ インセンティブのために「ミッション」機能で、X話を読むと作品が 読める「チケット」を配布するものがあるため ○ 読んだ履歴を参照すると激重クエリが発生するため ○ 数十億レコードのテーブルに対しJOINしているため単純な参照 でもリクエストが返ってこなくなるため

Slide 21

Slide 21 text

キャッシュを使う 21 2.テーブルを参照するとクエリが帰ってこなくなるもの ElastiCache for Redis 履歴テーブル 基本的に書き込みだけ 読み書き両方 user_idと作品IDと日付のセットを配列で保存 ミッションの判定を高速に行えるようになった 特に重い履歴テーブルに関する処理の実装方針

Slide 22

Slide 22 text

22 障害ではなかったが Redisの容量がいっぱいになりかけた

Slide 23

Slide 23 text

23 Redisの容量対応 ● RedisInsightを使い、本番データをコピーした上で調査 ○ https://redis.com/redis-enterprise/redis-insight/ ● expireがなく使い終わったデータを削除 ● 「user_idと作品IDのセットを配列で保存」も制限をかける

Slide 24

Slide 24 text

キャッシュを使う 24 まとめ(キャッシュを使う) ● Active Model SerializerやStringのキャッシュサーバーへの保存 を行い、DB負荷を減らすように設計する ○ ただしデシリアライズ時のエラーなどキャッシュに起因するバグに悩 まされるので、監視体制を作っておく ● キャッシュサーバーを活用するときは不必要にデータを持ちすぎない

Slide 25

Slide 25 text

2.不要なデータは使わない

Slide 26

Slide 26 text

不要なデータは使わない 26 202X年元日 - 新年初障害 障害が発生 RDSのアラートが止まらない ↓からの アプリケーションが応答しない

Slide 27

Slide 27 text

不要なデータは使わない 27 202X年元日 - 新年初障害 この時は冬休みのアクセス増が理由と結論付けてRDSのスペックアップ を実施した。 →一応はエラーが収まったため、障害調査を休みあけに実施した

Slide 28

Slide 28 text

不要なデータは使わない 28 202X年元日 - 新年初障害 特定のユーザーでスロークエリが発生している

Slide 29

Slide 29 text

不要なデータは使わない 29 特定ユーザーのスロークエリ ● スロークエリを眺めていると、「特定のユーザー」でしか発生していない スロークエリがある ● レスポンスタイムを見ていると30秒で張り付いている ○ API Gatewayの最大時間=タイムアウト ● 「特定ユーザー」の共通項は長期間利用しているヘビーユーザー ● スロークエリは0時に多く発生している

Slide 30

Slide 30 text

不要なデータは使わない 30 特定ユーザーのスロークエリ ● 原因はお気に入りの機能 ○ APIでユーザーお気に入り全件と閲覧情報をJOINさせていたこと が原因 ○ 多く読んでいる&多くお気に入りに入れることが原因だったのでヘ ビーユーザーに多かった ○ 0時に多く発生していたのは0時に情報が更新されるため、この時 間にアクセスするユーザーが多かったから 作品メタ情報

Slide 31

Slide 31 text

不要なデータは使わない 31 特定ユーザーのスロークエリ ● 原因はお気に入りの機能 お気に入り作品 作品メタ情報 作品決済情報 作品閲覧履歴 JOIN

Slide 32

Slide 32 text

不要なデータは使わない 32 特定ユーザーのスロークエリ ● お気に入りの制約(1)😭 ○ バッチ更新ができない ■ SQLの中にメタ情報を参照して毎日変わる数値があり、全ユー ザーに対して一斉に情報を更新することが難しい ○ すべての履歴を参照するためデータを移動することで軽くすること も難しい

Slide 33

Slide 33 text

不要なデータは使わない 33 特定ユーザーのスロークエリ ● お気に入りの制約(2)😭 ○ モバイルアプリの仕様上ページングを付けることが難しい ■ アプリ側の作品更新表示がページングに非対応

Slide 34

Slide 34 text

不要なデータは使わない 34 特定ユーザーのスロークエリ ● お気に入りを2ページ化してスロークエリを減らした ○ 1ページ目を100件にし、2件目は全件表示するように変更 ○ 全部をお気に入りに入れているユーザー(編集長)もいる ○ ただし1日に更新が起きる作品には限りがある ○ 全ユーザーのお気に入り数を調べ、100件程度返せば実用に耐え られる

Slide 35

Slide 35 text

不要なデータは使わない 35 特定ユーザーのスロークエリ 対応の結果... 2ページ目(全件)までアクセスするユーザーは大きく減った🎉 (※スロークエリは解消してないが、発生する数が減ったのでレスポンスも 改善した)

Slide 36

Slide 36 text

不要なデータは使わない 36 まとめ(不要なデータは使わない) ● DBへの問い合わせするときも関連するデータ量が多ければクエリは 遅くなる ○ 仕様も含めてそれでいいのか、ユーザーのアクセスを減らしたり 分割できないか検討を行う

Slide 37

Slide 37 text

3.テーブルを適切な形に変更する

Slide 38

Slide 38 text

テーブルを適切な形に変更する 38 2023年夏 - 障害発生 障害が発生 Rendering 500 with exception: 2147483648 is out of range for ActiveModel::Type::Integer with limit 4 bytes

Slide 39

Slide 39 text

テーブルを適切な形に変更する 39 2023年夏 - 障害発生 障害が発生 Rendering 500 with exception: 2147483648 is out of range for ActiveModel::Type::Integer with limit 4 bytes >ActiveModel::Type::Integer.new.send(:max_value) => 2147483648

Slide 40

Slide 40 text

テーブルを適切な形に変更する 40 障害となった作品閲覧に対するミッション履歴 ● create_table "user_comic_browse_mission_histories" do |t| t.bigint "user_id", null: false t.bigint "mission_id", null: false t.integer "user_comic_browse_history_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["user_id", "mission_id"] end # 作品閲覧に対して達成されたミッションの履歴

Slide 41

Slide 41 text

キャッシュを使う 41 作品の閲覧履歴管理テーブル ● create_table "user_comic_browse_histories" do |t| t.bigint "user_id", null: false t.bigint "comic_id", null: false t.datetime "browse_at", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["user_id", "comic_id"] end https://railsguides.jp/active_record_basics.html

Slide 42

Slide 42 text

テーブルを適切な形に変更する 42 BIGINTの対応漏れ(一次対応) ● ALTER TABLEは可能か? ○ user_comic_browse_mission_histories自体も数十億をゆう に超える ○ 時間が全く予測できない ○ int型→bigint型への変更なのでオンラインDDLは難しい ● 障害対応のため一旦機能自体を止めることになった

Slide 43

Slide 43 text

テーブルを適切な形に変更する 43 BIGINTの対応漏れ(恒久対応&他テーブルへの対応) ● 他にも同様の状況になるテーブルを調査 ○ IDの数値が知りたいのでauto_incrementがどうなっているか調 べ、BIGINT型の必要なテーブルをピックアップした ● いくつかのテーブルで参照元のIDはBIGINT型だが、参照カラムには INTEGER型で定義されているIDが存在していた

Slide 44

Slide 44 text

テーブルを適切な形に変更する 44 BIGINTの対応漏れ(恒久対応&他テーブルへの対応) ● 利用したクエリ SELECT * FROM information_schema.tables ORDER BY auto_increment DESC;

Slide 45

Slide 45 text

テーブルを適切な形に変更する 45 BIGINTの対応漏れ(恒久対応&他テーブルへの対応) 対応が必要なテーブルに対して ● 現在利用中のテーブルも含まれるので機能を止めたくはない ● メンテナンスは最小にしておきたい

Slide 46

Slide 46 text

テーブルを適切な形に変更する 46 BIGINTの対応漏れ(恒久対応&他テーブルへの対応) 対応が必要なテーブルに対して ● 旧来テーブルと、必要なカラムをbigint型にした新テーブルを用意し、 データを新テーブルに順次移行 ● データの同期が完了したら、短時間のメンテナンスを入れ旧来のテーブ ルと新テーブルの名称を入れ替え。この方法なら巨大なレコードの中身 を直接変えることなく対応ができる

Slide 47

Slide 47 text

テーブルを適切な形に変更する 47 BIGINTの対応漏れ(恒久対応&他テーブルへの対応) class CreateUserComicBrowseMissionBigintHistories < ActiveRecord::Migration[6.1] def change create_table :user_comic_browse_mission_bigint_histories do |t| # テーブル名を変更 t.integer :user_id, null: false t.integer :mission_id, null: false t.bigint :user_comic_browse_history_id, null: false # bigint型に変更 t.timestamp end end end # 下線部分が差分

Slide 48

Slide 48 text

テーブルを適切な形に変更する 48 BIGINTの対応漏れ(恒久対応&他テーブルへの対応) user_comic_browse_mission_bigint_histories user_comic_browse_mission_histories バッチで同一のデータをコピー + after_commitの後同一の内容をbigint_histories側にも保存

Slide 49

Slide 49 text

class TargetTable < ApplicationRecord after_commit :duplicate_bigint_table, on: :create def duplicate_bigint_table TargetBigintTable.create!(self.attributes) end end 49 モデル側の実装

Slide 50

Slide 50 text

class TargetTableMoveTask LIMIT = 10_000 def execute end_id = TargetTable.minimum(:id) start_id = end_id - LIMIT while end_id > 0 target_table = TargetTable.where(id: start_id..end_id) records = target_table.map do |record| TargetBigintTable.new(record.attributes) end TargetBigintTable.import!(records, on_duplicate_key_update: update_attrs) end_id = start_id start_id = end_id - LIMIT start_id = 0 if start_id <= 0 sleep 0.5 end end end 50 バッチの実装

Slide 51

Slide 51 text

テーブルを適切な形に変更する 51 BIGINTの対応漏れ(恒久対応&他テーブルへの対応) user_comic_browse_mission_histories bkp_user_comic_browse_mission_histories テーブル名を変更 対応済みテーブルが参照さ れるようになる user_comic_browse_mission_histories user_comic_browse_mission_bigint_histories class SwapUserComicBrowseMissionBigintHistories < ActiveRecord::Migration[6.1] def change rename_table :user_comic_browse_mission_histories, :bkp_user_comic_browse_mission_histories rename_table :user_comic_browse_mission_bigint_histories, :user_comic_browse_mission_histories end end

Slide 52

Slide 52 text

テーブルを適切な形に変更する 52 BIGINTの対応漏れ(恒久対応&他テーブルへの対応) pt-online-schema-changeではだめだったのか? ● Palcyで以前に実績がある方法を踏襲した ● コードもその時の再利用なので、低コストだった

Slide 53

Slide 53 text

テーブルを適切な形に変更する 53 BIGINTの対応漏れ(恒久対応&他テーブルへの対応) ● 今後同じ事象を防ぐために調査用クエリを使い、自動的に 警告が飛ぶ様に監視バッチを追加

Slide 54

Slide 54 text

テーブルを適切な形に変更する 54 BIGINTの対応漏れ(恒久対応&他テーブルへの対応) def check_auto_increment_result query = <<~SQL SELECT table_name, auto_increment FROM information_schema.tables WHERE 1900000000 < auto_increment AND auto_increment < 2147483648 ORDER BY auto_increment DESC; SQL results = ActiveRecord::Base.connection.execute(query) results.each do |row| notice_alert("int型の上限が近付いています。テーブル名 : #{row[0]} ") notice("table_name: #{row[0]}, auto_increment: #{row[1]}") # Slack通知処理 end end +--------------+------------------------+ | table_name | auto_increment | +--------------+------------------------+ | table_name | (0-9)+| ….

Slide 55

Slide 55 text

テーブルを適切な形に変更する 55 まとめ(テーブルを適切な形にする ) ● Xデーはある日突然やってくる ○ 履歴系のテーブルはBIGINT型を使う ○ ALTER TABLEは無理でもRENAME TABLEは高速。余裕があ るうちにカラムの型を見直しておく ○ auto_incrementの監視機能を実装しておく

Slide 56

Slide 56 text

56 今日のまとめ ● 3つの障害を紹介した ● いずれもサービスインから時間が経てば、障害として現れるものが 多い ● Palcyの失敗を他山の石にして、安定した運用の糧にしてほしい

Slide 57

Slide 57 text

ありがとうございました