DEV Community

gumi TECH for gumi TECH Blog

Posted on • Edited on

Ecto 3.0: パフォーマンスと新バージョンへの移行など

本稿はplataformatec社の許諾を得て、開発チームのblog記事「A sneak peek at Ecto 3.0: performance, migrations and more」にもとづき、Ecto 3.0のパフォーマンスとレコードのupsert(追加・更新)および新バージョンへの移行について、おもな改善点をご説明します。2018年10月16日付でリリース候補のEcto v3.0.0-rc.0が公開されましたので、実際に試していただくこともできます。

メモリ使用の改善

Ecto 3.0におけるパフォーマンス向上でもっとも大きいのは、Ectoリポジトリからロードされるスキーマの費やすメモリがより少なくなったことです。

Ecto 3.0のメモリの改善の多くは、スキーマメタデータの優れた管理によります。Ecto.Schema%User{}といったインスタンスすべてが、エントリのライフサイクル情報についてのメタデータフィールドをもっています。たとえば、データベース接頭辞やその状態です(ビルドされたのか、データベースからロードされたのか)。メタデータフィールドは具体的には16語あります。

iex> :erts_debug.size %Ecto.Schema.Metadata{}
16
Enter fullscreen mode Exit fullscreen mode

64ビットマシンの16語は128バイトに当たります。Ecto 2.0を使っていて1,000エントリ読み込んだら、メタデータを保管するだけで128KBのメモリが費やされました。けれど、1,000エントリはまったく同じメタデータが使えるのです。これにより、ロードするエントリが1,000でも100,000でもコストはつねに変わらず、128バイトになります。

Ecto 3.0-rcが公開されてから、実際にアップグレードしたチームがあります。リポジトリにはかなり大きなものもありました。けれど、アップグレードには1日もかかりませんでした。CargoSenseの上級エンジニアBen Wilson氏は、彼らのアプリケーションのひとつをEcto 3.0-rcにしてプロダクションに投入しました。その結果がつぎの図です。

図001■アプリケーションの使用メモリ

memory.png

メモリの使用量が落ちたのは、Ecto 2からEcto 3にデプロイした瞬間です。このアプリケーションは、起動中にたくさんのデータを読み込んでいます。メモリの使用がいかに改善されたかわかるでしょう。システムが安定すると、平均使用メモリは全体で15%下がります。

さらに、Ecto 3.0はErlang VMリテラルプールを用いるようになりました。これで、メタデータがクエリ間で共有できるのです。たとえば、ふたつのクエリがあって、それぞれポストを1,000返すとき、合計2,000のポストは同じメタデータを用います。構造体の割り当てを減らす他の改善も合わせて、Ectoのメモリ使用量は全体として減るでしょう。

Ecto.Repo.insert/update/deleteのステートメントキャッシュ

Ecto 3.0のもうひとつの大きなパフォーマンス改善は、Ecto.Repo.insert/update/deleteで生成されるステートメントが自動的にキャッシュされることです。たとえば、つぎのコードを考えてみましょう。Postは13のフィールドを持つスキーマだとします。

for i <- 1..1000 do
  Repo.insert!(%Post{visits: i})
end
Enter fullscreen mode Exit fullscreen mode

このコードを接続が10あるPostgresデータベースで走らせると、1,000のポストをすべて加えるのに900ミリ秒かかります。Ectoはつねに選択したクエリをキャッシュするので、Ecto.Repo.insert/update/deleteにステートメントキャッシュを加えれば、合計操作時間が610ミリ秒に縮められるのです。

問題は、Repo.insertを呼び出すたびに、Ectoは接続プールから新たな接続を得なければならないということです。そのうえで、挿入を行い、接続を戻します。接続10のプールでつぎに選ぶのが、再利用できる(warm)状態でなく、ステートメントキャッシュは使えないかもしれません。長く接続し続けないことは重要です。データベースのリソースは最大限活用すべきでしょう。そこで、たくさんの操作は連続して実行することが求められます。

このため、Ecto 3.0はRepo.checkoutの操作が備わりました。これで、Ectoリポジトリに同じ接続を用いるよう伝えられます。接続プールはとばして、再利用できる接続を使い続けるのです。コードをつぎのように書き替えると、すべての挿入が平均420ミリ秒で済みました。

Repo.checkout(fn ->
  for i <- 1..1000 do
    Repo.insert!(%Post{visits: i})
  end
end)
Enter fullscreen mode Exit fullscreen mode

使えるテクニックがもうひとつあります。挿入を複数行うとき、Repo.checkoutRepo.transactionに置き替えるのです。トランザクションはひとつの接続をチェックアウトします。けれど、データベース自身がより効率化されます。この書き替えにより、かかる時間は320ミリ秒まで落とせます。さらに速くしたければ、Ecto.Repo.insert_allを用いればよいでしょう。

他のオプションと改善

Ecto 2はupsertに対応しました。Ecto 3はupsert APIに多くの改善を加えています。たとえば、:replace_all_except_primary_keyにより、競合が生じたり特定のフィールドが上書きされるとき、on_conflict: {:replace, [:foo, :bar, baz]}が渡せます。また、:conflict_targetの値に{:unsafe_fragment, "be careful with what goes here"}を渡して、カスタム式ができるのです。

Ecto.Repo APIには、前述のEcto.Repo.checkoutのほかにも、新しいEcto.Repo.exists?など多くの改善が加わりました。

新バージョンへの移行

Ecto(より正確にはEcto.SQL)に加えられた他の大きな改善はバージョンの移行です。

移行テーブルをロックして、複数のマシンで同時に移行できます(Allen Madsen氏の貢献)。これまでは、複数のマシンで移行を実行しようとすると、競合が生じて、失敗することがありました。新バージョンでは、このようなことが起こりません。ロックの種類は、:migration_lockリポジトリ設定で定められます。デフォルトは“FOR UPDATE”で、nilが無効です。

もうひとつ改善されたのは、Ectoの移行を行なっているとき、notice/alert/warningが記録できるようになったことです。これまでのバージョンは、長いインデックス名があると、データベースが切り詰めて、TCP接続から警告を発しました。ところが、この警告は抽出されず、端末に出力されませんでした。Ecto 3.0ではこれはなくなります。

同じように、移行を実行したところ、データベース中により新しいバージョンに移行済みのものがあると、Ectoは警告を示します。たとえば、ひとつの機能について長い間作業を続けていて、masterにマージできる段階に至ったとします。ところが、作業を始めてから、他の機能と移行がすでに本番環境に出荷されていたという場合です。すると、デプロイで問題が生じるでしょう。デプロイできなかったら、データベースをロールバックしなければなりません。直近の移行のタイムスタンプは、今実行したものと異なるはずです。

警告が示されれば、開発者とプロダクションのチームがこうした事態に陥ることを防げます。

最後に、おそらくもっとも重要なことは、EctoのAPIがより安定したことです。詳しくは、Elixir Forumの「Ecto 3.0-rc is out and stable API」をお読みください。

関連記事

Top comments (0)