Erlang 热代码交换如何在 activity 中间工作?

How does Erlang hot code swapping work in the middle of activity?

我目前正在做一个直播媒体服务器,这将允许普通消费者向我们发送直播视频。在我们当前的环境中,我们已经看到持续数天的广播发送给我们,因此能够在不断开用户连接的情况下修复错误(或添加功能)的想法非常引人注目。

然而,当我编写代码时,我意识到热代码交换没有任何意义,除非我编写每个进程,以便所有状态始终在 gen_server 内完成,并且所有外部模块 gen_server 调用必须尽可能简单。

举个例子:

-module(server_template).
-behaviour(gen_server).

-export([start/1, stop/0]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).

start() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

init([]) -> {ok, {module1:new(), module2:new()}}.

handle_call(Message, From, State) -> {reply, ok, State}.

handle_cast(any_message, {state1, state2}) -> 
    new_state1 = module1:do_something(state1),
    new_state2 = module2:do_something(state2),
    {noreply, {new_state1, new_state2}}.

handle_info(_Message, _Server) -> {noreply, _Server}.

terminate(_Reason, _Server) -> ok.

code_change(_OldVersion, {state1, state2}, _Extra) -> 
    new_state1 = module1:code_change(state1),
    new_state2 = module2:code_change(state2)
    {ok, {new_state1, new_state2}}

据我所知,当新版本的代码加载到当前 运行 运行时而不使用 OTP 系统时,您可以通过将模块作为外部调用来升级到当前代码版本函数调用,所以 my_module:loop(state).

我还看到,当执行热插拔时,code_change/3 函数被调用并升级状态,因此我可以使用它来确保我的每个依赖模块迁移它们提供的最后状态我进入当前代码版本的状态。它这样做是因为主管知道 运行 进程,这允许进程被挂起,以便它可以调用代码更改函数。一切顺利。

但是,如果调用外部模块总是调用该模块的当前版本,那么如果在函数中完成热插拔,这似乎会中断。例如,我的 gen_server 当前正在处理 any_message 转换,比如在 运行 module1:do_something()module2:do_something() 之间。

如果我理解正确,module2:do_something() 现在会调用最新版本的 do_something 函数,这可能意味着我将未迁移的数据传递到新版本的 module2:do_something()。如果它是已更改的记录、包含意外数量元素的数组,或者即使映射缺少代码预期的值,这也很容易导致问题。

我是不是误解了这种情况是如何发生的?如果这是正确的,这似乎表明我必须为任何可能转换模块边界的数据结构跟踪某种类型的版本详细信息,并且每个 public 函数都必须检查该版本号并在必要时执行按需迁移。

这似乎是一个非常高的命令,似乎非常容易出错,所以我想知道我是否遗漏了什么。

是的,你完全正确。没有人说热代码交换很容易。我在一家电信公司工作,所有代码升级都是在实时系统上执行的(这样用户就不会在通话过程中断开连接)。正确的做法意味着仔细考虑您提到的所有这些场景,并为每次失败准备代码,然后进行测试、修复问题、测试等等。要正确测试它,您需要一个系统 运行 负载下的旧版本(例如在测试环境中),然后部署新代码并检查是否有任何崩溃。

在您的问题中提到的这个特定示例中,处理此问题的最简单方法是编写 module2:do_something/1 的两个版本,一个接受旧状态,一个接受新状态。然后相应地处理旧状态,例如将其转换为新状态。

为此,您还需要确保在任何模块有机会使用新状态调用它之前部署 module2 的新版本:

  1. 如果包含 module2 的应用程序是另一个应用程序的依赖项 release_handler 将首先升级该模块。

  2. 否则,您可能需要将部署拆分成两部分,首先升级通用功能使其能够处理新状态,然后部署新版本的gen_servers和其他模块调用 module2.

  3. 如果您不使用发布处理程序,您可以手动指定模块的加载顺序。

这也是为什么在 Erlang 中它在模块之间的函数调用中 advised to avoid circular dependencies 的原因,例如当 modA 调用 modB 中的函数时调用 modA.

中的另一个函数

对于在发布处理程序的帮助下执行的升级,您可以验证 release_handler will upgrade modules on the old system in the relup file that the release_handler generates based on the old and new release 的顺序。它是一个包含所有升级说明的文本文件,例如:remove(删除模块)、load_object_code(加载新模块)、loadpurge

请注意,没有严格的要求所有应用程序都必须遵循 OTP 原则才能使热代码交换正常工作,但是使用 gen_server 和适当的 supervisor 堆栈可以使这项任务变得更加困难开发人员和发布处理程序都更容易处理。

如果您不使用 OTP 版本,则无法使用版本处理程序进行升级,但您仍然可以在系统上强制重新加载模块并将它们升级到新版本。只要您不需要 add/remove Erlang 应用程序,这就可以正常工作,因为为此需要更改发布定义,并且如果没有发布处理程序的支持,就无法在实时系统上完成。

发布处理调用 sys:suspend,它向 gen_server 发送消息。服务器将继续处理请求,直到它处理了挂起消息,此时它基本上只是坐着等待。然后将新模块版本加载到系统中,调用 sys:change_code 告诉服务器调用 code_change 回调来进行升级,然后服务器再次等待。当发布处理程序调用 sys:resume 时,它会向服务器发送一条消息,告诉它重新开始工作并再次开始处理传入的消息。

发布处理同时对依赖于一个模块的所有服务器执行此操作。所以首先全部挂起,然后加载新模块,然后全部升级,最后全部恢复工作。