LoginSignup
19
20

More than 5 years have passed since last update.

Phoenix/Ectoでデータベースのmaster/slave構成に対応する方法

Last updated at Posted at 2016-04-01

マスター1台、スレーブ複数台という構成は、DBサーバーの負荷分散方式として一般的だと思いますが、Phoenix/Ectoでこの構成に対応する方法を調査/検討してみました。

調査

こちらのissue内では、Elixirの作者であるJosé Valimさんが下記のようにコメントしています。

You just define multiple repositories: MyApp.Repo, MyApp.Repo.ReadSlave1, MyApp.Repo.ReadSlave2 up to 4 (or any power of 2). Their configuration is going to be similar and you can easily share them in the config/config.exs file.

Now, every time you need to do a read, you just need to do:

def repo do
  # size needs to be power of 2 that you care about
  <<at::integer-size(2), _::bits>> = :crypto.rand_bytes(1)
  Enum.fetch! [MyApp.Repo.ReadSlave1, ..., ], at
end

Alternatively you can create a facade repository that does exactly that:

defmodule Sample.Repo do
  def all(query, options \\ []) do
    read_repos.all(query, options) # Read goes through the random above
  end
  def update(changes, options \\ []) do
    Sample.Repo.Master.update(changes, options) # Those go to master
  end
end

またこちらの記事では、新たにRepoを作成する場合はconfigファイルとsupervisionツリーにそれぞれ設定を追加する必要があるということが説明されています。

I added the credentials to dev.exs for the other database:

# notice the module name
config :dual_ecto, DualEcto.RepoReadOnly,
  adapter: Ecto.Adapters.MySQL,
  username: "readonly_user",
  password: "foo",
  database: "data",
  hostname: "slave_server",
  pool_size: 10

First, add the module to the supervision tree of your main module, which in my case is called DualEcto:

children = [
  # Start the endpoint when the application starts
  supervisor(DualEcto.Endpoint, []),
  # Start the Ecto repository
  supervisor(DualEcto.Repo, []),
  supervisor(DualEcto.RepoReadOnly, []), # <- new line!
  # Here you could define other workers and supervisors as children
  # worker(DualEcto.Worker, [arg1, arg2, arg3]),
]

どうやらRepoを追加すること自体は比較的簡単なようです。

しかし、Joséさんの紹介している手法だと、allupdate等の、Ecto.Repoで定義されている関数を全て再定義する必要がありますし、edmzで紹介されている方法だと、既存のコードを大幅に書き換える必要が発生してしまいます。

対策

ということで、追加するコードの量と既存コードの改修をできるだけ最小限にする、ということを目標にして、下記のようなパッケージを作成してみました。

read_repos

使用方法

configファイル(dev.exs等)に設定を追加します。

# config/dev.exs
config :my_app, MyApp.ReadRepo0,
  adapter: Ecto.Adapters.MySQL,
  database: "my_app",
  hostname: "192.168.0.2",
  ...

config :my_app, MyApp.ReadRepo1,
  adapter: Ecto.Adapters.MySQL,
  database: "my_app",
  hostname: "192.168.0.3",
  ...

アプリケーションのsupervisionツリーに、動的に生成されるslave用のRepoを挿入するコードを追加します。

# lib/my_app.ex
defmodule MyApp do
  use Application

  def start(_type, _args) do
    children = [
      ...
    ]
    # add
    children = children ++ Enum.map(MyApp.Repo.slaves, &supervisor(&1, []))
    ...
  end
end

マスター用のRepoモジュールに、上記のReadReposuseするコードを追加します。

# lib/my_app/repo.ex
defmodule MyApp.Repo do
  use Ecto.Repo, otp_app: :my_app
  # add
  use ReadRepos
end

この設定だけで、下記のようにスレーブ用Repoを使用してDBにアクセスすることが出来ます。configファイルにスレーブDBの設定が複数記述されている場合、アクセス先のDBは毎回ランダムに選択されます。

MyApp.Entry |> MyApp.Repo.slave.all
MyApp.Entry |> MyApp.Repo.slave.get(1)

マスターDB用のRepoは通常通りMyApp.Repo.insertのようなコードで関数が実行出来ますので、スレーブ用DBを使用したい箇所だけMyApp.Repo.slaveを使うようにコードを書き換えるだけでオッケーです。

スレーブDB用の設定がconfigファイルに存在しない場合、MyApp.Repo.slaveは自動的にマスター用Repoを返しますので、スレーブ用DBが存在する環境でも存在しない環境でも、同じコードが使用可能です。

19
20
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
19
20