Save 37% off PRO during our Black Friday Sale! »

Elixirでスケールアウト可能なWebDB開発.pdf

88aae8ed2d2f17cc38bb05d1b300c014?s=47 tuchiro
August 24, 2018
250

 Elixirでスケールアウト可能なWebDB開発.pdf

88aae8ed2d2f17cc38bb05d1b300c014?s=128

tuchiro

August 24, 2018
Tweet

Transcript

  1. Elixir でスケールアウト可能な Web +DB 開発

  2. スケーラビリティとは

  3. スケーラビリティ(拡張性) サービス規模の拡大に応じてシステムを増強できる か? 容易に 低コストで より高性能に

  4. 垂直スケール( スケールアップ) ハードウェアをより高性能なものに置き換える クラウドの普及により以前より容易になった 一定の性能を越えるとコストが急増 単体を入れ替えるので無停止で実施しにくい

  5. 水平スケール( スケールアウト) 同じ機能をもつハードウェアの構成する数を増やす 必要に応じて追加することでコスト最適化 同時に耐障害性が上がる場合も 方式により水平スケールしにくい箇所も(LB 、DB など)

  6. Web+DB 構成における拡張性 設計

  7. Web サーバー ( 一般的に水平) セッション管理の方式検討が必要 LB がボトルネックになる可能性 データベース ( 一般的に垂直)

    リードレプリケーションや、パーティショニング で水平も可能 DB 製品によっては構成を増やすことでりライセン ス料に影響
  8. AWS での試算

  9. 2018 年現在のWeb サーバーとDB とのコスト感を確認 する為に、AWS EC2 と RDS のインスタンス性能を 各々2倍4倍とした場合のコストの伸びを試算。

    基準構成 サービス インスタンス vCPU メモリ 台数 EC2 t2-midium 2 4G 2 RDS(MySQL) db.m3- midium 1 3.75G 1
  10. 比較結果 構成 EC2 DB(RDS MySQL) 合計金額($/month) 基準との比率 基準 t2-midium 2台

    db.m3.medium 147.85 - Web サーバー2倍 t2-midium 4 台 db.m3.medium 210.62 1.4 DB 2倍 t2-midium 2台 db.m3.large 234.45 1.6 Web サーバー4 倍 t2-midium 8 台 db.m3.medium 330.63 2.2 DB4 倍 t2-midium 2台 db.m3.xlarge 417.79 2.8 Web サーバー8 倍 t2-midium 16 台 db.m3.medium 570.65 3.9 DB8 倍 t2-midium 2台 db.m3.2xlarge 769.15 5.2 ※ Amazon EC2 インスタンスタイプ表 ※ Amazon RDS インスタンスタイプ表
  11. 性能負荷を「Web 」に寄せる 実装 複雑な処理をDB で実施しない(結合、集計、置換 etc ) データの保管に特化させる 複雑な加工はWeb サーバー(Elixir+Phoenix)

    で実 施する
  12. Association を用いた結合

  13. defmodule EctoRelationSample.Account.User do use Ecto.Schema import Ecto.Changeset schema "users" do

    field :email, :string field :name, :string has_many :posts, EctoRelationSample.Contents.Post # this was added timestamps() end @doc false def changeset(user, attrs) do user |> cast(attrs, [:name, :email]) |> validate_required([:name, :email]) end end
  14. defmodule EctoRelationSample.Contents.Post do use Ecto.Schema import Ecto.Changeset schema "posts" do

    field :body, :string field :title, :string belongs_to :user, EctoRelationSample.Account.User # this was added timestamps() end @doc false def changeset(post, attrs) do post |> cast(attrs, [:title, :body]) |> validate_required([:title, :body]) end end
  15. iex(1)> user = %User{name: "John Doe", email: "john.doe@example.com"} iex(2)> user

    = Repo.insert!(user) iex(3)> post = Ecto.build_assoc(user, :posts, %{title: "sample ecto relation", body: "ecto assocs example"}) iex(4)> Repo.insert!(post) iex(5)> post = Ecto.build_assoc(user, :posts, %{title: "sample ecto relation2", body: "ecto assocs example section 2"}) iex(6)> Repo.insert!(post)
  16. iex(7)> Repo.all(User) |> Repo.preload(:posts) [ %EctoRelationSample.Account.User{ __meta__: #Ecto.Schema.Metadata<:loaded, "users">, email:

    "john.doe@example.com", id: 1, inserted_at: ~N[2018-08-21 06:12:52.155453], name: "John Doe", posts: [ %EctoRelationSample.Contents.Post{ __meta__: #Ecto.Schema.Metadata<:loaded, "posts">, body: "ecto assocs example", id: 1, inserted_at: ~N[2018-08-21 06:13:22.566436], title: "sample ecto relation", updated_at: ~N[2018-08-21 06:13:22.566447], user: #Ecto.Association.NotLoaded<association :user is not loaded>, user_id: 1 }, %EctoRelationSample.Contents.Post{ __meta__: #Ecto.Schema.Metadata<:loaded, "posts">, body: "ecto assocs example section 2", id: 2, inserted_at: ~N[2018-08-21 06:14:08.199156], title: "sample ecto relation2", updated_at: ~N[2018-08-21 06:14:08.199165], user: #Ecto.Association.NotLoaded<association :user is not loaded>, user_id: 1 } ], updated_at: ~N[2018-08-21 06:12:52.161839] } ]
  17. iex(26)> Repo.all(Post) |> Repo.preload(:user) [ %EctoRelationSample.Contents.Post{ __meta__: #Ecto.Schema.Metadata<:loaded, "posts">, body:

    "ecto assocs example", id: 1, inserted_at: ~N[2018-08-21 06:13:22.566436], title: "sample ecto relation", updated_at: ~N[2018-08-21 06:13:22.566447], user: %EctoRelationSample.Account.User{ __meta__: #Ecto.Schema.Metadata<:loaded, "users">, email: "john.doe@example.com", id: 1, inserted_at: ~N[2018-08-21 06:12:52.155453], name: "John Doe", posts: #Ecto.Association.NotLoaded<association :posts is not loaded>, updated_at: ~N[2018-08-21 06:12:52.161839] }, user_id: 1 } ]
  18. preload で発行されるSQL を確認 2018-08-20 23:25:12.371 PDT [89205] LOG: execute ecto_450:

    SELECT p0."id", p0."body", p0."title", p0."user_id", p0."inserted_at", p0."updated_at" FROM "posts" AS p0 2018-08-20 23:25:12.377 PDT [89206] LOG: execute ecto_482: SELECT u0."id", u0."email", u0."name", u0."inserted_at", u0."updated_at", u0."id" FROM "users" AS u0 WHERE (u0."id" = ANY($1)) 2018-08-20 23:25:12.377 PDT [89206] DETAIL: parameters: $1 = '{3,1}'
  19. Ecto.Query と Ecto.Adapters.SQL による SQL の記述

  20. Ecto.Query を用いた場合 iex(30)> import Ecto.Query Ecto.Query iex(31)> query = from

    u in User, join: p in Post, where: u.id == p.user_id, group_by: u.name, select: {u.name, count(p.id)} iex(32)> Repo.all(query) [debug] QUERY OK source="users" db=2.1ms SELECT u0."name", count(p1."id") FROM "users" AS u0 INNER JOIN "posts" AS p1 ON TRUE WHERE (u0."id" = p1."user_id") GROUP BY u0."name" [] [{"John Doe", 2}, {"nanashi gonbe", 1}] 2018-08-21 00:13:12.521 PDT [89207] LOG: execute ecto_642: SELECT u0."name", count(p1."id") FROM "users" AS u0 INNER JOIN "posts" AS p1 ON TRUE WHERE (u0."id" = p1."user_id") GROUP BY u0."name" 戻りがタプルのリスト・・・ [{"John Doe", 2}, {"nanashi gonbe", 1}]
  21. Ecto.Adapters.SQL を用いた場合 Ecto.Query iex(40)> sql = "select u.name, count(p.*) from

    users u join posts p on u.id = p.user_id group by u.name" iex(41)> Ecto.Adapters.SQL.query(Repo, sql, []) [debug] QUERY OK db=28.0ms queue=0.1ms select u.name, count(p.id) from users u inner join posts p on u.id = p.user_id group by u.name [] {:ok, %Postgrex.Result{ columns: ["name", "count"], command: :select, connection_id: 90367, num_rows: 2, rows: [["John Doe", 2], ["nanashi gonbe", 1]] }}
  22. 戻りがカラム名リストと値のリストをも つ%Postgrex.Result 構造・・・ {:ok, %Postgrex.Result{ columns: ["name", "count"], command: :select,

    connection_id: 90367, num_rows: 2, rows: [["John Doe", 2], ["nanashi gonbe", 1]] }}
  23. Ecto.Adapters.SQL をWrapp した実装 defmodule AppExUtils.Ecto.EctoUtil do import Ecto.Query, warn: false

    @spec query(Repo, string, [list]) :: [list] def query(repo, sql, params) do Ecto.Adapters.SQL.query(repo, sql, params) |> result_to_map_list() end defp result_to_map_list(nil) do # 戻りが無いSQL の場合、nil で処理する nil end defp result_to_map_list({:error, error}) do # エラーはスルー {:error, error} end
  24. defp result_to_map_list({:ok, result}) do columns = result.columns case columns do

    nil -> [num_rows: result.num_rows] _ -> rows = result.rows list_maps = Enum.map(rows, fn row -> row_columns_to_map(row, columns) end end defp row_columns_to_map(row, columns) do map_result = Enum.map(Enum.with_index(row, 0), fn {k, i} -> [Enum.at(columns, i), k] |> Enum.map(fn [a, b] -> {String.to_atom(a), convert(b)} end) |> Map.new() end def convert({{year, month, day}, {hour, minites, sec, msec}}) do # 日時解釈できるものはDateTime に変換 Timex.to_datetime({{year, month, day}, {hour, minites, sec, msec}}) end def convert(attr) do attr end end
  25. iex(1)> alias AppExUtils.Ecto.EctoUtil AppExUtils.Ecto.EctoUtil iex(2)> sql = "select u.name, count(p.id)

    from users u inner join posts p on u.id = p.user_id group by u.name" iex(3)> EctoUtil.query(Repo, sql, []) [%{count: 2, name: "John Doe"}, %{count: 1, name: "nanashi gonbe"}] 2018-08-21 00:29:23.087 PDT [90366] LOG: execute <unnamed>: select u.name, count(p.id) from users u inner join posts p on u.id = p.user_id group by u.name 戻りをmap のリストにする(通常のEcto.Repo 関数の 戻りに近い)ことで実装時の煩わしさを排除 [%{count: 2, name: "John Doe"}, %{count: 1, name: "nanashi gonbe"}]
  26. Enum を使い倒す実装 例えばSQL のgroup by に変わる関数 def group_by(maps, group_by_key_list) do

    maps |> Enum.group_by(fn(map) -> group_by_key_list |> Enum.map(fn(key) -> map[key] end) end) |> Map.to_list() |> Enum.map(fn(group_by_result) -> {group_value_list, maps} = group_by_result group_by_keys_map = group_by_key_list Enum.zip(group_value_list) |> Enum.into(%{}) {group_by_keys_map, maps} end) end
  27. iex(2)> users = [%{name: "hogehoge", address: "aaa", age: 16}, %{name:

    "hogehoge", address: nil, age: 4}, %{name: "fugafuga", address: "aaa",age: 16}] [ %{address: "aaa", age: 16, name: "hogehoge"}, %{address: nil, age: 4, name: "hogehoge"}, %{address: "aaa", age: 16, name: "fugafuga"} ] iex(4)> EnumLikeSqlUtil.group_by(users, [:name]) [ {%{name: "fugafuga"}, [%{address: "aaa", age: 16, name: "fugafuga"}]}, {%{name: "hogehoge"}, [ %{address: "aaa", age: 16, name: "hogehoge"}, %{address: nil, age: 4, name: "hogehoge"} ]} ]
  28. def sum(group_by_results, sum_key_list) do initial_result = sum_key_list |> Enum.reduce(%{}, fn(key,

    initial_result) -> Map.put(initial_result, key, 0) end) sum_result_maps = group_by_results |> Enum.map(fn(group_by_result) -> {group_by_keys_map, maps} = group_by_result sum_result_map = maps |> Enum.reduce(initial_result, fn(map, result_map) -> sum_tmp_map = sum_key_list |> Enum.reduce(result_map, fn(key, tmp_map) -> Map.put(tmp_map, key, tmp_map[key] + map[key]) end) end) Map.merge(group_by_keys_map, sum_result_map) end) end
  29. def count(group_by_results, count_key_list \\ []) do initial_result = count_key_list |>

    Enum.reduce(%{}, fn(key, initial_result) -> Map.put(initial_result, key, 0) end) count_result_maps = group_by_results |> Enum.map(fn(group_by_result) -> {group_by_keys_map, maps} = group_by_result all_count = maps |> Enum.count() count_map = %{all_count: all_count} col_count_map = count_key_list |> Enum.reduce(count_map, fn(count_key, count_map) -> col_count = maps |> Enum.count(fn(map) -> map[count_key] != nil end) Map.put(count_map, count_key, col_count) end) count_map = count_map |> Map.merge(col_count_map) {group_by_keys_map, count_map} end) end
  30. iex(5)> users |> EnumLikeSqlUtil.group_by([:name]) |> EnumLikeSqlUtil.count([:address]) [ {%{name: "fugafuga"}, %{address:

    1, all_count: 1}}, {%{name: "hogehoge"}, %{address: 1, all_count: 2}} ] iex(6)> users |> EnumLikeSqlUtil.group_by([:name]) |> EnumLikeSqlUtil.sum([:age]) [%{age: 16, name: "fugafuga"}, %{age: 20, name: "hogehoge"}]
  31. 実際の実装では・・・ user = Users.list_users() address_count = = users |> EnumLikeSqlUtil.group_by([:name])

    |> EnumLikeSqlUtil.count([:address])
  32. DB からWeb サーバーに寄せる実装 で将来の負荷増加に備えましょう!