マスター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éさんの紹介している手法だと、all
やupdate
等の、Ecto.Repo
で定義されている関数を全て再定義する必要がありますし、edmzで紹介されている方法だと、既存のコードを大幅に書き換える必要が発生してしまいます。
対策
ということで、追加するコードの量と既存コードの改修をできるだけ最小限にする、ということを目標にして、下記のようなパッケージを作成してみました。
使用方法
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モジュールに、上記のReadRepos
をuse
するコードを追加します。
# 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が存在する環境でも存在しない環境でも、同じコードが使用可能です。