LoginSignup
4

More than 5 years have passed since last update.

Djangoで、複数台構成のread replicaなdbサーバに参照する時だけランダムに接続する

Last updated at Posted at 2018-07-20

GAE/pyでcloud sqlに繋ぎに行っていたんですが、
データ参照をread replicaに分散させないと高負荷に耐えられないケースが稀にあるので。

Cloud Spannerを使うだけの予算があったり、
上手くDataStoreで実装できればそれで済むこともあるんですが……
キャッシュを使っていてもなおスペックが足りなかったりと、世の中はままならないので。
DBを参照する際にread replicaなサーバにランダムで繋ぎに行った時の問題と解決方法の備忘録でも。

もっといい方法があるよ!という方がいれば是非とも。

Django公式の説明

Multiple databases

この通り複数台構成の場合の説明はあります。
db_for_readで参照先を、db_for_writeで書き込み先を指定してあげればいいだけのお手軽設定です。
さすがに書き込み先を分散させる……ということは早々ないはずなので、
下のような感じで、db_for_readで接続先をランダムに指定してあげれば、
一つのDBサーバに接続が偏らずに、無事にスケールできそうな予感がします!

def db_for_read(self, model, **hints):
    random.choice(settings.DATABASES.keys())

実装の問題点

  • selectが走る度にrandom.choiceで接続先がランダムで選択される
  • 一度のリクエストでN台のDBサーバへのコネクションが発生してしまった!
  • スケールどころではなく一瞬で死亡!
  • トランザクション中でも、read replicaを参照してしまう!
  • begin -> update -> select -> あれ……!?

解決方法

  • 一度ランダムで選択したものを、保持するようにする
  • db_for_write....つまり、デフォルトの接続先がトランザクション実行中であれば、参照先をそこに切り替えるようにする
  • haproxyを使えない環境なので、read replicaが落ちている場合は自動で他のreplicaに切り替えられるようにpython側でどうにかする

実装方法

という訳で下の内容で実装してみました。
db_for_readの中でトランザクションの実施判定、一度設定した接続先を記憶するようにしています。

from django.db import connections
from django.db.transaction import DEFAULT_DB_ALIAS

class FailOverRouter(object):
    def __init__(self):
        self.default = DEFAULT_DB_ALIAS
        self.read_connection = None

    def is_transaction(self):
        has_connection = hasattr(connections._connections, self.default)

        if has_connection:
            return connections[self.default].in_atomic_block

        return False

    def db_for_read(self, model, **hints):
        if self.is_transaction():
            return self.default

        if self.read_connection is None:
            self.read_connection = self.default

            # read replicaの一覧をshuffleしたものを取得
            read_instance_names = get_read_instances()

            for read_name in read_instance_names:
                # 接続先がダウンしていないかどうかの確認
                if test_connection_to_db(read_name):
                    self.read_connection = read_name
                    break

        return self.read_connection

    def db_for_write(self, model, **hints):
        return self.default

    def allow_migrate(self, db, app_label, **hints):
        return db == self.default

実装してみて

これを利用して、高負荷サービスでも(read replicaが耐えられる範囲までは)問題なくさばけるようになりました。
データベースを追加すれば追加するほど、無限にさばける人数を増やせるようになったかなと。
まぁ、増やせば増やすほどコストも無限にスケールするんですが……

最後に

株式会社ネコカリでは高い負荷を求められるサービスでも、求められないサービスでも、
猫の手をかせるお仕事を合わせて募集しています!

一緒に働くメンバーも募集していますので、よかったら是非!

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
4