$30 off During Our Annual Pro Sale. View Details »

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

KNR
October 27, 2023
6.8k

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

Kaigi on Rails 2023

KNR

October 27, 2023
Tweet

Transcript

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  6. 発表者とサービスの紹介

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  12. # ここから本題

    View Slide

  13. 1.キャッシュを使う

    View Slide

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

    View Slide

  15. ● Active Model Serializerの利用

    ○ ほぼJSONでレスポンスを返すWebAPIが主で使いやすい

    ○ expire期間も指定できる

    ○ 多少のロジックを実装できる

    ■ 故に内部でDBアクセスする実装を行えばN+1が起こる

    キャッシュを使う
    15
    1.同一のデータを返すもの

    View Slide

  16. キャッシュを使う
    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側の実装例

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  39. テーブルを適切な形に変更する
    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

    View Slide

  40. テーブルを適切な形に変更する
    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
    # 作品閲覧に対して達成されたミッションの履歴

    View Slide

  41. キャッシュを使う
    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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  47. テーブルを適切な形に変更する
    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
    # 下線部分が差分

    View Slide

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

    View Slide

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

    View Slide

  50. 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
    バッチの実装

    View Slide

  51. テーブルを適切な形に変更する
    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

    View Slide

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

    View Slide

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

    View Slide

  54. テーブルを適切な形に変更する
    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)+|
    ….

    View Slide

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

    View Slide

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

    View Slide

  57. ありがとうございました

    View Slide