14、Elixir分布式系统搭建与Blitzy工具实现

Elixir分布式系统搭建与Blitzy工具实现

在Elixir中,分布式系统的构建是一项强大且重要的功能。下面将详细介绍如何在Elixir中构建分布式集群,并将Blitzy工具进行分布式改造。

1. Elixir分布式基础
1.1 位置透明性

在Elixir/Erlang集群中,进程具有位置透明性。这意味着,只要知道接收进程的进程ID,在单个节点上的进程之间发送消息与在不同节点上的进程之间发送消息同样容易。从开发者的角度来看,本质上没有区别。

1.2 Elixir节点

节点是运行具有给定名称的Erlang VM的系统。节点名称以原子形式表示,如 :justin@bieber.com ,类似于电子邮件地址。节点名称分为短名称和长名称。使用短名称假设所有节点位于同一IP域内,通常这种方式更容易设置。

1.3 创建集群

创建集群的第一步是在分布式模式下启动Erlang系统,并且需要为其命名。以下是具体步骤:
1. 在新的终端中,使用短名称启动 iex

$ iex --sname barry
iex(barry@imac)> 

此时, iex 提示符会显示短名称和本地机器的主机名。可以使用 Kernel.node/0 获取本地机器的节点名称:

iex(barry@imac)> node
:barry@imac

也可以使用 Node.self/0 ,但 node 更简洁。
2. 在另外两个独立的终端窗口中,重复上述过程,但为每个节点赋予不同的名称:

$ iex --sname robin
iex(robin@imac)> 

$ iex --sname maurice
iex(maurice@imac)> 

此时,这些节点彼此孤立,它们并不知道对方的存在。

1.4 连接节点

可以使用 Node.connect/1 函数连接节点:
1. 在 barry 节点上连接 robin 节点:

iex(barry@imac)> Node.connect(:robin@imac)
true

Node.connect/1 在连接成功时返回 true 。使用 Node.list/0 列出 barry 节点连接的所有节点:

iex(barry@imac)> Node.list
[:robin@imac]

注意, Node.list/1 不会列出当前节点,仅列出已连接的节点。
2. 在 robin 节点上再次运行 Node.list/0

iex(robin@imac)> Node.list
[:barry@imac]

节点名称必须唯一,如果启动的节点名称已被注册,VM会报错,并且不能混合使用长名称和短名称。
3. 从 robin 节点连接 maurice 节点:

iex(robin@imac)> Node.connect(:maurice@imac)
true

检查 robin 节点连接的节点:

iex(robin@imac)> Node.list
[:barry@imac, :maurice@imac]

再检查 barry 节点连接的节点:

iex(barry@imac)> Node.list
[:robin@imac, :maurice@imac]

可以发现,节点连接具有传递性。即使没有显式地将 barry 连接到 maurice ,由于 barry 连接到 robin ,而 robin 连接到 maurice ,所以 barry 也与 maurice 连接。

2. 远程执行函数

在连接节点到集群后,可以进行一些有用的操作。首先关闭之前打开的所有 iex 会话,重新创建集群。在 lib/worker.ex start/3 函数中添加一行代码:

defmodule Blitzy.Worker do
  def start(url, func \\ &HTTPoison.get/1) do
    IO.puts "Running on #node-#{node}"
    {timestamp, response} = Duration.measure(fn -> func.(url) end)
    handle_response({Duration.Duration.to_milliseconds(timestamp), response})
  end
  # ... same as before
end

在三个不同的终端中,进入Blitzy的目录,分别启动节点:

% iex --sname barry -S mix
% iex --sname robin -S mix
% iex --sname maurice -S mix

然后在 maurice 节点上连接其他节点:

iex(maurice@imac)> Node.connect(:barry@imac)
true
iex(maurice@imac)> Node.connect(:robin@imac)
true
iex(maurice@imac)> Node.list
[:barry@imac, :robin@imac]

接下来,在所有三个节点上运行 Blitzy.Worker.start 函数:

iex(maurice@imac)> cluster = [node | Node.list]
[:maurice@imac, :barry@imac, :robin@imac]
iex(maurice@imac)> :rpc.multicall(cluster, Blitzy.Worker, :start, ["http://www.bieberfever.com"]) 
"Running on #node-maurice@imac"
"Running on #node-robin@imac"
"Running on #node-barry@imac"

返回结果如下:

{[ok: 2166.561, ok: 3175.567, ok: 2959.726], []}

实际上,也可以不指定集群:

iex(maurice@imac)> :rpc.multicall(Blitzy.Worker, :start, ["http://www.bieberfever.com"])
"Running on #node-maurice@imac"
"Running on #node-barry@imac"
"Running on #node-robin@imac"
{[ok: 1858.212, ok: 737.108, ok: 1038.707], []}

返回值是一个包含两个元素的元组,第一个元素捕获所有成功的调用,第二个参数给出不可达节点的列表。

还可以使用 Task.async/1 Task.await/2 来执行多个工作进程,并聚合结果:

iex(maurice@imac)> :rpc.multicall(Blitzy, :run, [5, "http://www.bieberfever.com"], :infinity)

返回结果是三个列表,每个列表有五个元素:

{[[ok: 92.76, ok: 71.179, ok: 138.284, ok: 78.159, ok: 139.742],
  [ok: 120.909, ok: 75.775, ok: 146.515, ok: 86.986, ok: 129.492],
  [ok: 147.873, ok: 171.228, ok: 114.596, ok: 120.745, ok: 130.114]],
 []}
3. 使Blitzy分布式化
3.1 创建配置文件

创建一个简单的配置文件 config/config.exs ,用于主节点连接集群中的节点:

use Mix.Config
config :blitzy, master_node: :"a@127.0.0.1"
config :blitzy, slave_nodes: [:"b@127.0.0.1",
                              :"c@127.0.0.1",
                              :"d@127.0.0.1"]
3.2 创建命令行界面

Blitzy是一个命令行程序,需要为其构建命令行界面。创建一个新文件 lib/cli.ex ,并按以下方式调用Blitzy:

./blitzy -n [requests] [url]

其中, [requests] 是一个整数,指定要创建的工作进程数量; [url] 是一个字符串,指定端点。如果用户提供的格式不正确,应显示帮助消息。

mix.exs 中修改 project/0 函数,添加 escript 条目:

defmodule Blitzy.Mixfile do
  def project do
    [app: :blitzy,
     version: "0.0.1",
     elixir: "~> 1.1",
     escript: [main_module: Blitzy.CLI],
     deps: deps]
  end
end

lib/cli.ex 中实现 main/1 函数和相关辅助函数:

use Mix.Config
defmodule Blitzy.CLI do
  require Logger
  def main(args) do
    args
      |> parse_args
      |> process_options
  end
  defp parse_args(args) do
    OptionParser.parse(args, aliases: [n: :requests],
                              strict: [requests: :integer])
  end
  defp process_options(options, nodes) do
    case options do
      {[requests: n], [url], []} ->
        # perform action
      _ ->
        do_help
    end
  end
end

parse_args/1 函数使用 OptionParser.parse/2 解析输入参数, process_options/1 函数对解析结果进行模式匹配,若参数格式不符合要求,则调用 do_help 函数显示帮助信息:

defp do_help do
  IO.puts """
  Usage:
  blitzy -n [requests] [url]
  Options:
  -n, [--requests]      # Number of requests
  Example:
  ./blitzy -n 100 http://www.bieberfever.com
  """
  System.halt(0)
end
3.3 连接到节点

lib/cli.ex 中修改 main/1 函数,从配置文件中读取主节点和从节点信息,并进行连接:

defmodule Blitzy.CLI do
  def main(args) do
    Application.get_env(:blitzy, :master_node)
      |> Node.start
    Application.get_env(:blitzy, :slave_nodes)
      |> Enum.each(&Node.connect(&1))
    args
      |> parse_args
      |> process_options([node|Node.list])
  end
end

同时修改 process_options/2 函数,将节点列表传递给 do_requests/3 函数:

defmodule Blitzy.CLI do
  # ...
  defp process_options(options, nodes) do
    case options do
      {[requests: n], [url], []} ->
        do_requests(n, url, nodes)
      _ ->
        do_help
    end
  end
end

do_requests/3 函数是主要的工作函数:

defmodule Blitzy.CLI do
  # ...
  defp do_requests(n_requests, url, nodes) do
    Logger.info "Pummeling #{url} with #{n_requests} requests"
    total_nodes  = Enum.count(nodes)
    req_per_node = div(n_requests, total_nodes)
    nodes
    |> Enum.flat_map(fn node ->
         1..req_per_node |> Enum.map(fn _ ->
           Task.Supervisor.async({Blitzy.TasksSupervisor, node}, Blitzy.Worker, :start, [url])
         end)
       end)
    |> Enum.map(&Task.await(&1, :infinity))
    |> parse_results
  end
end
3.4 任务监督

为了避免任务崩溃导致整个应用程序崩溃,使用 Task.Supervisor 对任务进行监督。创建 lib/supervisor.ex 文件:

defmodule Blitzy.Supervisor do
  use Supervisor
  def start_link(:ok) do
    Supervisor.start_link(__MODULE__, :ok)
  end
  def init(:ok) do
    children = [
      supervisor(Task.Supervisor, [[name: Blitzy.TasksSupervisor]])
    ]
    supervise(children, [strategy: :one_for_one])
  end
end

lib/blitzy.ex 中启动顶级监督器:

defmodule Blitzy do
  use Application
  def start(_type, _args) do
    Blitzy.Supervisor.start_link(:ok)
  end
end
3.5 创建二进制文件

在项目目录中运行以下命令生成二进制文件:

% mix escript.build
3.6 运行Blitzy

在启动二进制文件之前,需要在三个单独的终端中启动从节点:

% iex --name b@127.0.0.1 -S mix
% iex --name c@127.0.0.1 -S mix
% iex --name d@127.0.0.1 -S mix

然后在另一个终端中运行Blitzy命令:

% ./blitzy -n 10000 http://www.bieberfever.com

所有四个终端将显示类似以下的消息:

10:34:17.702 [info]  worker [b@127.0.0.1-#PID<0.2584.0>] completed in 58585.746 msecs

最后,在启动 ./blitzy 命令的终端上会显示结果:

Total workers    : 10000
Successful reqs  : 9795
Failed res       : 205
Average (msecs)  : 31670.991222460456
Longest (msecs)  : 58585.746
Shortest (msecs) : 3141.722

通过以上步骤,我们成功地在Elixir中构建了分布式集群,并将Blitzy工具进行了分布式改造。

Elixir分布式系统搭建与Blitzy工具实现(下半部分)

4. 任务监督器的使用分析

在前面提到了使用 Task.Supervisor 来监督任务,下面详细分析其使用方式和原理。

4.1 Task.Supervisor 的使用方式

do_requests/3 函数中,使用 Task.Supervisor 来启动任务:

nodes
|> Enum.flat_map(fn node ->
     1..req_per_node |> Enum.map(fn _ ->
       Task.Supervisor.async({Blitzy.TasksSupervisor, node}, Blitzy.Worker, :start, [url])
     end)
   end)
|> Enum.map(&Task.await(&1, :infinity))
|> parse_results

这部分代码的执行流程如下:
1. 计算每个节点的任务数量
- total_nodes = Enum.count(nodes) :统计集群中的节点总数。
- req_per_node = div(n_requests, total_nodes) :计算每个节点需要执行的任务数量。
2. 为每个节点启动任务
- 使用 Enum.flat_map 遍历每个节点,对于每个节点,使用 Enum.map 启动 req_per_node 个任务。
- Task.Supervisor.async({Blitzy.TasksSupervisor, node}, Blitzy.Worker, :start, [url]) :在指定节点的 Blitzy.TasksSupervisor 下异步启动 Blitzy.Worker start 函数。
3. 等待任务完成
- Enum.map(&Task.await(&1, :infinity)) :等待所有任务完成,并获取结果。
4. 解析结果
- parse_results :对任务结果进行解析。

4.2 Task.Supervisor Task.async 的区别

Task.Supervisor.async Task.async 类似,但有几个关键区别:
- 任务监督 Task.Supervisor.async 启动的任务会受到 Task.Supervisor 的监督,当任务崩溃时,不会导致整个应用程序崩溃。
- 指定节点 Task.Supervisor.async 可以指定在哪个节点上启动任务,通过 {Blitzy.TasksSupervisor, node} 参数实现。

4.3 Enum.flat_map 的作用

在上述代码中,使用了 Enum.flat_map 而不是 Enum.map ,这是因为 Enum.map 会返回一个嵌套列表,而 Enum.flat_map 会将嵌套列表扁平化。例如:

# 使用Enum.map
nodes
|> Enum.map(fn node ->
     1..req_per_node |> Enum.map(fn _ ->
       Task.Supervisor.async({Blitzy.TasksSupervisor, node}, Blitzy.Worker, :start, [url])
     end)
   end)
# 结果是一个嵌套列表

# 使用Enum.flat_map
nodes
|> Enum.flat_map(fn node ->
     1..req_per_node |> Enum.map(fn _ ->
       Task.Supervisor.async({Blitzy.TasksSupervisor, node}, Blitzy.Worker, :start, [url])
     end)
   end)
# 结果是一个扁平化的列表

使用 Enum.flat_map 可以方便后续对任务结果进行处理。

5. 命令行参数解析分析

在创建命令行界面时,使用了 OptionParser 来解析命令行参数,下面详细分析其使用方式。

5.1 OptionParser.parse 的使用

parse_args/1 函数中,使用 OptionParser.parse 解析参数:

defp parse_args(args) do
  OptionParser.parse(args, aliases: [n: :requests],
                            strict: [requests: :integer])
end

OptionParser.parse 接受两个参数:
- 参数列表 args ,即命令行输入的参数列表。
- 选项 :包含 aliases strict 两个选项。
- aliases :用于指定参数的别名,例如 [n: :requests] 表示 -n --requests 的别名。
- strict :用于指定参数的类型,例如 [requests: :integer] 表示 --requests 参数必须是整数。

5.2 参数解析结果

OptionParser.parse 返回一个包含三个元素的元组:
- 解析后的参数 :一个包含键值对的列表,例如 [requests: 100]
- 剩余的参数 :未被解析的参数列表。
- 无效的参数 :格式不正确的参数列表。

例如:

iex> OptionParser.parse(["-n", "100", "http://www.bieberfever.com"], aliases: [n: :requests], strict: [requests: :integer])
{[requests: 100], ["http://www.bieberfever.com"], []}
5.3 模式匹配处理参数

process_options/2 函数中,使用模式匹配来处理解析后的参数:

defp process_options(options, nodes) do
  case options do
    {[requests: n], [url], []} ->
      do_requests(n, url, nodes)
    _ ->
      do_help
  end
end

这里的模式匹配要求:
- [requests: n] --requests 参数必须存在,且值为整数。
- [url] :必须有一个URL参数。
- [] :不能有无效的参数。
如果参数不符合要求,则调用 do_help 函数显示帮助信息。

6. 总结与展望

通过以上步骤,我们成功地在Elixir中构建了分布式集群,并将Blitzy工具进行了分布式改造。整个过程包括创建集群、连接节点、远程执行函数、创建命令行界面、任务监督等步骤。

在实际应用中,可以根据需要对Blitzy进行进一步的优化和扩展,例如:
- 错误处理 :在任务执行过程中,可能会出现各种错误,如网络错误、请求超时等,可以添加更完善的错误处理机制。
- 性能优化 :可以通过调整任务分配策略、优化网络配置等方式提高性能。
- 功能扩展 :可以添加更多的功能,如统计分析、日志记录等。

以下是整个Blitzy分布式系统的构建流程图:

graph TD;
    A[创建配置文件] --> B[创建命令行界面];
    B --> C[启动主节点];
    C --> D[连接从节点];
    D --> E[解析命令行参数];
    E --> F{参数是否有效};
    F -- 是 --> G[计算每个节点的任务数量];
    G --> H[为每个节点启动任务];
    H --> I[等待任务完成];
    I --> J[解析任务结果];
    F -- 否 --> K[显示帮助信息];

通过这个流程图,可以清晰地看到Blitzy分布式系统的构建和执行流程。希望本文对你理解Elixir分布式系统和Blitzy工具的使用有所帮助。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值