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工具的使用有所帮助。
超级会员免费看
453

被折叠的 条评论
为什么被折叠?



