跳至主内容

使用 Elixir 编写 RabbitMQ 插件

·9 分钟阅读
Álvaro Videla

RabbitMQ 是一个非常可扩展的消息代理,允许用户通过编写插件来扩展服务器的功能。许多代理功能甚至作为插件随代理一起安装:例如,管理插件STOMP 支持。虽然这很酷,但插件必须用 Erlang 编写有时会成为一种挑战。我决定看看是否可以用另一种针对 Erlang 虚拟机 (EVM) 的语言编写插件,在这篇文章中我将分享我的进展。

Elixir

在过去的几个月里,我一直在关注一种名为 Elixir 的新编程语言,它以 EVM(Erlang 虚拟机)为目标。上周,随着 Erlang 之父 Joe Armstrong 尝试了该语言并对其大加赞赏,它在 Erlang 社区(以及其他圈子)变得极其流行。所以我对自己说,好吧,让我们试一试。

我不想花太多时间去描述 Elixir 是什么——阅读其官网或 Joe Armstrong 的博客文章会更好。对我来说,它将 Ruby 语言的思想带到了 Erlang 平台。我们基本上可以用一种(对于某些“更好语法”的定义而言)更好的语法来编写 Erlang 程序。

比较这段摘自 Elixir 官网的 Erlang Hello World 程序

-module(module_name).
-compile(export_all).

hello() ->
io:format("~s~n", ["Hello world!"]).

再看看这段同样来自其官网的 Elixir 版本

# module_name.ex

defmodule ModuleName do
def hello do
IO.puts "Hello World"
end
end

虽然这个例子非常简单,但我认为对于从未见过 Erlang 语法的人来说,Elixir 版本更容易阅读。从我的角度来看,我一直很喜欢 Erlang 的语法,因为它让 Erlang 概念的语义脱颖而出,并且非常清楚正在发生的事情,但这依然是我个人的看法。

考虑到人们可能更喜欢用 Elixir 编写 RabbitMQ 插件,让我们看看这该如何实现。

编写 RabbitMQ 插件

要为 RabbitMQ 编写插件,您需要设置开发环境以使用 rabbitmq public umbrella 提供的工具、Makefile 和库。您可以从这里按照设置说明进行操作。一旦克隆了 http://hg.rabbitmq.com/rabbitmq-public-umbrella 项目并安装了所有依赖项,我们就可以开始编写自己的插件了。要使用 Elixir 完成此操作,您首先需要在机器上安装该语言,以便使用 Elixir 编译器 (mix) 和语言库。

编写 RabbitMQ 插件时,您可能希望在插件中使用一些 Erlang 库,为此,您需要将它们也包装成插件,这样当您将某个库声明为项目依赖项时,构建环境就能找到它们。在本例中,我们的新插件将依赖于 Elixir,因此我们需要将语言库包装为插件。我已经完成了这项工作,您只需从 Github 克隆 elixir_wrapper 并按照其 README 中的说明进行安装即可。

现在是创建我们自己的插件的时候了。作为一个示例,我已经将一个名为 recent-history-exchange(最近历史交换机)的插件移植到了 Elixir。顾名思义,它为 RabbitMQ 添加了一种新的交换机类型。交换机是 RabbitMQ 用来决定消息去向的路由算法。如果您想了解更多关于交换机的信息,请点击这里

新交换机的代码可以在 Github 上找到:RabbitMQ Recent History Exchange

新交换机类型的代码位于 lib/rabbit_exchange_type_recent_history.ex 文件中,我们在其中实现了 rabbit_exchange_type 行为(behaviour)。被重写的方法有:route/2delete/3add_binding。此交换机的功能是在消息路由到队列时缓存最后 20 条消息。每当新队列绑定到此交换机时,我们就会将这最后 20 条消息投递给它。最后,当交换机被删除时,我们从数据库中删除这些条目。这有什么用呢?假设您用 RabbitMQ 实现了一个聊天室,并希望加入聊天室的人能获取到最近发送到该房间的消息——这就是实现该功能的一种简单方法。

虽然如果您先阅读了 Elixir 教程,就能理解大部分代码,但仍有一些点值得注意,因为对我来说,如何将它们移植到 Elixir 并不是很明确。如果您想查看我从 Erlang 移植过来的原始项目,请点击这里

模块属性

RabbitMQ 使用启动步骤(boot steps)的概念来启动代理。当代理启动时,这些启动步骤会被扫描,插件会自动被服务器拾取。它们被声明为模块属性,所以我遇到的第一个障碍是如何在 Elixir 中添加模块属性。假设我们有以下模块

defmodule RabbitExchangeTypeRecentHistory do
end

要为其添加像 RabbitMQ 所期望的那样的属性,我们需要这样做

defmodule RabbitExchangeTypeRecentHistory do

Module.register_attribute __MODULE__,
:rabbit_boot_step,
accumulate: true, persist: true

@rabbit_boot_step { __MODULE__,
[{:description, "exchange type x-recent-history"},
{:mfa, {:rabbit_registry, :register,
[:exchange, <<"x-recent-history">>, __MODULE__]}},
{:requires, :rabbit_registry},
{:enables, :kernel_ready}]
end

首先,我们必须通过调用 Module.register_attribute 来注册我们的属性,然后就可以像本例中那样在代码中使用它了。

行为

在我们的模块中声明行为非常简单。我们只需要像这样在模块中添加一个 behaviour 属性

@behaviour :rabbit_exchange_type

Erlang 记录

当您开发 RabbitMQ 插件(可能在与 Erlang 库交互时也是如此)时,您将需要使用库中定义的记录(records)。这并不像我预期的那样直接。我们必须将记录定义导入到 Elixir 中。例如,要使用 RabbitMQ 中的 #exchange 记录,我们必须这样做

defmodule RabbitExchangeTypeRecentHistory do
defrecord :exchange, Record.extract(:exchange, from_lib: "rabbit_common/include/rabbit.hrl")
end

请记住,这里我只是展示了代码片段。您不需要每次都定义 RabbitExchangeTypeRecentHistory 模块。

构建插件

一旦我们完成了插件的实现,我们可能就想构建它,毕竟我们花这么多时间在它上面是有原因的!为此,我们需要在项目文件夹中添加两个 Makefile,以便与 RabbitMQ 的构建系统集成。

第一个名为 Makefile,它只有一行代码

include ../umbrella.mk

第二个稍微复杂一些。在这里,我们指定项目的依赖项,并告诉 RabbitMQ 构建系统如何编译我们的 Elixir 代码。

DEPS:=rabbitmq-server rabbitmq-erlang-client elixir_wrapper
RETAIN_ORIGINAL_VERSION:=true
ORIGINAL_VERSION:=0.1
DO_NOT_GENERATE_APP_FILE:=

CONSTRUCT_APP_PREREQS:=mix-compile
define construct_app_commands
mkdir -p $(APP_DIR)/ebin
cp $(PACKAGE_DIR)/ebin/* $(APP_DIR)/ebin
endef

define package_rules

$(PACKAGE_DIR)/deps/.done:
rm -rf $$(@D)
mkdir -p $$(@D)
@echo [elided] unzip ezs
@cd $$(@D) &amp;&amp; $$(foreach EZ,$$(wildcard $(PACKAGE_DIR)/build/dep-ezs/*.ez),unzip -q $$(abspath $$(EZ)) &amp;&amp;) :
touch $$@

mix-compile: $(PACKAGE_DIR)/deps/.done
mix clean
ERL_LIBS=$(PACKAGE_DIR)/deps mix compile

endef

我不会逐行解释这段代码,只解释有趣的部分。在第一行,我们声明了插件的依赖项。在本例中,我们依赖 rabbitmq serverrabbitmq-erlang-client,以便访问插件所需的所有行为和记录。当然,我们也依赖将随插件一起发布 Elixir 库。

接下来,我们定义一些 make 目标来编译插件并将其打包到 .ez 文件中(RabbitMQ 插件以 .ez 文件形式发布)。mix-compile 目标将构建我们的 Elixir 代码。您可能已经注意到,它将 ERL_LIBS 变量设置为插件的 ./deps 文件夹。为了使之生效,我们首先需要将依赖项解压到那里,因此 make 规则 $(PACKAGE_DIR)/deps/.done 会将预先构建好的依赖项解压到该文件夹中。

最后,我们的 define construct_app_commands 会将我们的 .beam 文件复制到目标文件夹,这样 RabbitMQ 构建系统就能找到它们并将其打包进插件 .ez 文件中。

一旦一切就绪,就可以构建我们的插件了。我们只需在插件文件夹中执行 make 即可。

构建过程完成后,我们可以在 dist 文件夹中找到 .ez 文件

ls dist/
amqp_client-3.3.1.ez
elixir-0.9.2.dev-rmq3.3.1-git7c379aa.ez
rabbit_common-3.3.1.ez
rabbitmq_recent_history_exchange_elixir-0.1-rmq3.3.1.ez

在这些文件中,我们只需要分发 rabbitmq_recent_history_exchange_elixir-0.1-rmq3.3.1.ezelixir-0.9.2.dev-rmq3.3.1-git7c379aa.ez 作为插件的一部分。

安装插件

rabbitmq_recent_history_exchange_elixir-0.1-rmq3.3.1.ezelixir-0.9.2.dev-rmq3.3.1-git7c379aa.ez 文件复制到 RabbitMQ 安装目录的 plugins 文件夹中,然后运行以下命令激活插件

./sbin/rabbitmq-plugins enable rabbitmq_recent_history_exchange_elixir

一旦启动代理,我们可以在管理界面中看到,现在可以添加类型为 x-recent-history 的交换机了。

结语

以上就是全部内容。我们可以使用 Elixir 构建 RabbitMQ 插件!大部分“清理扫尾”(yak shaving)工作已经完成,我们只需要使用 elixir_wrapper 并为我们的插件创建合适的 package.mk 文件。剩下的只需调用 make 即可。请在评论区分享您的想法,如果您使用 Elixir 构建了 RabbitMQ 插件,请分享出来并告诉我。

© . This site is unofficial and not affiliated with VMware.