0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【備忘録】MySQLレプリケーション遅延の仕組みとRailsでの対処法

0
Posted at

TL;DR

  • レプリケーション = ソースのデータをレプリカにコピーし続ける仕組み。主な目的は負荷分散
  • レプリケーションは非同期なので、ソースに書いたデータがレプリカに反映されるまでにラグがある
  • 遅延の根本原因は「ソース(並列コミット)vs レプリカ(直列または限定的並列適用)の速度差」
  • Railsでは「書き込み直後に読み取る」場面で踏みやすい。対処は書き込み直後はソースから読む

レプリカとは

MySQLのレプリケーションには2種類のサーバーが登場する。

ソース(source)                  レプリカ(replica)
────────────────                  ─────────────────
書き込み・読み取りができる   →    読み取り専用のコピー
メインのDBサーバー                レプリケーションで同期

※ MySQL 8.0以降は「マスター/スレーブ」から「ソース/レプリカ」に用語が統一された

なぜレプリカが必要か

ソース1台だけだと全トラフィックが集中する。

書き込み ──┐
           ├──→ ソース1台 ← CPU・ディスクIOが1台に集中
読み取り ──┘

一般的なWebサービスは読み取りの方が書き込みより圧倒的に多い。読み取りをレプリカに逃がすことでソースの負荷を下げられる。

書き込み ──────────→ ソース
                      ↓ レプリケーション
読み取り ──→ レプリカ1
読み取り ──→ レプリカ2
読み取り ──→ レプリカ3
用途 内容
読み取り負荷分散 参照クエリをレプリカに分散しソースのCPU・IOを下げる
バッチ処理/分析 重い集計クエリをレプリカに逃がしソースへの影響を防ぐ
バックアップ バックアップ取得中の負荷をレプリカに閉じ込める
フェイルオーバー ソース障害時にレプリカをソースに昇格させて高可用性を実現

なぜレプリケーション遅延が起きるのか

根本原因

ソースは複数スレッドで並列コミットできるが、レプリカの適用がそれに追いつけない。

ソース:
  クライアントA ──→ コミット ┐
  クライアントB ──→ コミット ├─ 並列
  クライアントC ──→ コミット ┘

レプリカ(シングルスレッド方式):
  バイナリログ: [A][B][C][D][E]...
  SQLスレッド:   A → B → C → D → E  ← シリアルにしか処理できない

MTA方式(MySQL 8.0.27〜デフォルト)

SQLスレッドをマルチ化することで並列適用できるようになった。

コーディネータスレッド
    ↓ リレーログを読んで振り分け
┌───┬───┬───┬───┐
W1  W2  W3  W4    ← ワーカースレッド(デフォルト4本)

ただし同時刻にコミットされたトランザクションのみ並列化できる。高トラフィックでも遅延が解消しない場合はreplica_parallel_workersを増やすことを検討する。

遅延が起きる3つの具体的ケース

ケース1:高トラフィック × ワーカー不足

ソース:   100 tps でコミット
レプリカ:  60 tps でしか適用できない
           ↓
差分が積み上がり続ける → 遅延が拡大していく

ケース2:大きなトランザクション

# 100万件を1トランザクションで削除
User.where(inactive: true).delete_all

# コミット時に100万件分のROWイベントが一気に送信される
# → レプリカのSQLスレッドが長時間ブロックされる
# → その間、後続トランザクションの適用が止まる

ケース3:PKなしテーブルへのROWイベント適用

ROWイベントの適用手順:
  1. 「どの行を更新するか」をバイナリログから読み取る
  2. テーブルからその行を探す  ← PKで検索
  3. 更新を適用する

PKがない場合:
  テーブルの全行をスキャンして一致する行を探す
  → データ量が増えるほど遅延が深刻になる

Railsでの対処法

パターン1:書き込み直後に読み取る(一番踏みやすい)

# 典型的な罠
def create
  @order = Order.create!(order_params)  # ソースに書き込み
  redirect_to order_path(@order)        # GET /orders/:id
end

def show
  @order = Order.find(params[:id])
  # ↑ レプリカに向いている場合、まだ存在しない可能性がある
  #   → ActiveRecord::RecordNotFound
end

対処法A:書き込み直後はソースから読む

def show
  ActiveRecord::Base.connected_to(role: :writing) do
    @order = Order.find(params[:id])
  end
end

対処法B:セッションでフラグを立てて猶予期間中はソースから読む

# 書き込み後にセッションにフラグを立てる
def create
  @order = Order.create!(order_params)
  session[:wrote_at] = Time.current.to_f
  redirect_to order_path(@order)
end

# ApplicationControllerで猶予期間中はソースから読む
class ApplicationController < ActionController::Base
  around_action :handle_replica_lag

  private

  def handle_replica_lag
    if within_replica_lag_window?
      ActiveRecord::Base.connected_to(role: :writing) { yield }
    else
      yield
    end
  end

  def within_replica_lag_window?
    return false unless session[:wrote_at]
    # 書き込みから2秒以内はソースから読む
    Time.current.to_f - session[:wrote_at] < 2.0
  end
end

パターン2:大きなバッチ処理でレプリカが詰まる

# NG: 大量件数を1トランザクションで処理
User.where(inactive: true).delete_all

# OK: in_batchesで小さいトランザクションに分割
User.where(inactive: true).in_batches(of: 1000) do |batch|
  batch.delete_all
  sleep(0.1)  # レプリカが追いつく時間を与える
end

パターン3:PKなしテーブルの遅延

# NG: PKなしテーブル
create_table :event_logs, id: false do |t|
  t.string   :event_type
  t.integer  :user_id
  t.datetime :occurred_at
end

# OK: 必ずPKを設定する
create_table :event_logs do |t|  # デフォルトでidカラムが追加される
  t.string   :event_type
  t.integer  :user_id
  t.datetime :occurred_at
end

まとめ

パターン 原因 対処
書き込み直後のRecordNotFound 非同期レプリケーションのラグ 書き込み直後はソースから読む
バッチ処理でレプリカが詰まる 大きいトランザクション in_batchesで分割・sleepを挟む
PKなしテーブルの遅延増大 ROW適用時のフルスキャン 必ずPKを設定する
高トラフィックで遅延が拡大 ワーカースレッド不足 replica_parallel_workersを増やす
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?