我目前正在开发一个实时媒体服务器,这将允许一般消费者向我们发送实时视频.在我们目前的环境中,我们已经看到了在几天内发送给我们的广播,因此能够在不断开用户连接的情况下修复错误(或添加功能)的想法非常引人注目.
然而,当我编写代码时,我意识到热代码交换没有任何意义,除非我编写每个进程以便所有状态总是在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}}
Run Code Online (Sandbox Code Playgroud)
根据我所能找到的,当新版本的代码加载到当前运行的运行时而不使用OTP系统时,您可以通过将模块作为外部函数调用来升级到当前代码版本,因此my_module:loop(state).
我还看到,当执行热交换时,code_change/3函数被调用并升级状态,因此我可以使用它来确保我的每个依赖模块将他们给我的最后状态迁移到当前代码版本的状态.这样做是因为主管知道正在运行的进程,它允许暂停进程,因此可以调用代码更改功能.都好.
但是,如果调用外部模块总是调用该模块的当前版本,那么如果在函数中间进行热交换,这似乎会中断.例如,同样我的gen_server当前正在处理any_message演员表,比如说在run module1:do_something()和module2:do_something().之间.
如果我正确理解事物,module2:do_something()现在会调用do_something函数的新当前版本,这可能意味着我将未迁移的数据传递到新版本的module2:do_something().如果记录已更改,具有意外数量的元素的数组,或者即使映射缺少代码所需的值,这也很容易导致问题.
我误解了这种情况是如何运作的吗?如果这是正确的,这似乎表明我必须跟踪可能转换模块边界的任何数据结构的某些类型的版本详细信息,并且每个公共函数必须检查该版本号并在必要时执行按需迁移.
这似乎是一个非常高的订单,看起来很容易出错,所以我想知道我是否遗漏了一些东西.
是的,你是完全正确的.没有人说热代码交换很容易.我曾在一家电信公司工作,所有代码升级都是在现场系统上进行的(这样用户在通话过程中就不会断开连接).正确的做法意味着仔细考虑您提到的所有场景,并为每次失败准备代码,然后测试,然后修复问题,测试等等.要正确测试它,您需要一个在负载下运行旧版本的系统(例如在测试环境中),然后部署新代码并检查是否有任何崩溃.
在您的问题中提到的这个特定示例中,处理此问题的最简单方法是编写两个版本module2:do_something/1,一个接受旧状态,另一个接受新状态.然后相应地处理旧状态,例如将其转换为新状态.
为此,您还需要确保module2在任何模块有机会使用新状态调用它之前部署新版本:
如果包含的应用程序module2是其他应用程序的依赖项,release_handler则将首先升级该模块.
否则,您可能需要将部署拆分为两部分,首先升级公共函数以便它们可以处理新状态,然后部署新版本gen_servers和其他调用模块module2.
如果您没有使用发布处理程序,则可以手动指定模块的加载顺序.
这也是为什么在Erlang中建议避免模块之间的函数调用中的循环依赖,例如当modA调用函数modB调用另一个函数时modA.
对于在发布处理程序的帮助下执行的升级,您可以验证在基于旧版本和新版本生成release_handler的relup文件中升级旧系统上的模块的顺序.它包含了升级的所有指令的文本文件,如:(删除模块), (加载新模块),等等.release_handlerremoveload_object_codeloadpurge
请注意,没有严格要求所有应用程序必须遵循OTP原则才能使热代码交换工作,但是使用gen_server和适当的管理程序堆栈使开发人员和发布处理程序更容易处理此任务.
如果您未使用OTP版本,则无法使用版本处理程序进行升级,但仍可以强制重新加载系统上的模块并将其升级到新版本.只要您不需要添加/删除Erlang应用程序,这就可以正常工作,因为发布定义需要更改,而且如果没有发布处理程序的支持,则无法在实时系统上完成.