0%

supervisor在我们热更中起到至关重要,而且设计到的内容比较多,需要注意的事项也很多,所以这里特别开了一个章节来说.

指令区别

上一章我们说道同步热更指令有两种:

1
2
{update, Module, {advanced, Extra}}  %% gen_server类指令
{update, Module, supervisor} %% supervisor专用指令

同样是更新进程内部状态,但是为什么要分开两个指令来区分呢?

单从这里,看不出区别来,因此我决定去看看源码实现:

之前我们知道普通指令会在relup阶段转成底层指令然后放入relup文件,然后我找到了这一段,位置在systools_rc:expand_script/1

1
2
3
4
{update, Mod, Change} when is_tuple(Change) -> %% gen_server的指令
{update, Mod, Change, brutal_purge, brutal_purge, []};
{update, Mod, supervisor} -> %% supervisor的指令
{update, Mod, static, default, {advanced,[]}, brutal_purge, brutal_purge, []};

我们发现{update, Mod, Change}被转成了:

1
{update, Mod, Change, brutal_purge, brutal_purge, []};

继续跟踪,上面这条指令在内部还会转换一次,最后是:

1
{update, Mod, dynamic, default, Change, brutal_purge, brutal_purge, []};

而我们的{update, Mod, supervisor}被转成了:

1
{update, Mod, static, default, {advanced,[]}, brutal_purge, brutal_purge, []};

可以看到第三个字段明显不同gen_server的指令的是dynamic, 而supervisor的指令是static:

1
2
{update, Mod, dynamic, default, Change, brutal_purge, brutal_purge, []};
{update, Mod, static, default, {advanced,[]}, brutal_purge, brutal_purge, []};

我们再结合文档来看看:

High-Level Instructions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{update, Mod}
{update, Mod, supervisor}
{update, Mod, Change}
{update, Mod, DepMods}
{update, Mod, Change, DepMods}
{update, Mod, Change, PrePurge, PostPurge, DepMods}
{update, Mod, Timeout, Change, PrePurge, PostPurge, DepMods}
{update, Mod, ModType, Timeout, Change, PrePurge, PostPurge, DepMods}
Mod = atom()
ModType = static | dynamic
Timeout = int()>0 | default | infinity
Change = soft | {advanced,Extra}
Extra = term()
PrePurge = PostPurge = soft_purge | brutal_purge
DepMods = [Mod]

第三个字段的定义为ModType,继续看文档关于ModType的定义解析:

Defaults to dynamic. It specifies if the code is “dynamic”, that is, if a process using the module spontaneously switches to new code, or if it is “static”. When doing an advanced update and upgrade, the new version of a dynamic module is loaded before the process is asked to change code. When downgrading, the process is asked to change code before loading the new version. For static modules, the new version is loaded before the process is asked to change code, both in the case of upgrading and downgrading. Callback modules are dynamic.
默认指定的是dynamic,当进行升级的时候,新版本模块会在执行change code前加载;当进行降级的时候,会先执行change code之后在加载新版本模块
而如果指定static,则升级和降级的时候都是先加载新版本模块再执行change code操作

这里说的是,更新的顺序不同,

指定dynamic的时候:

升级的时候,系统先加载模块代码,然后再执行回调方法change code,让进程更新状态
降级的时候,系统会先执行回调方法change code,然后再加载模块代码,让进程更新状态
指定static的时候:

升级和降级都同样是先加载模块代码,然后再执行回调方法change code,让进程更新状态.

为什么otp要这么设定呢?从这方面我们还是看不出来,接下来我们看看supervisor的更新过程:

supervisor的更新过程

有看过supervisor的源码的同学可能很少会注意到,其实supervisor的behaviour是gen_server:

1
2
3
4
5
6
7
8
9
10
11
12
-module(supervisor).
-behaviour(gen_server).
%% External exports
-export([start_link/2, start_link/3,
start_child/2, restart_child/2,
delete_child/2, terminate_child/2,
which_children/1, count_children/1,
check_childspecs/1, get_childspec/2]).
%% Internal exports
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3, format_status/2]).
-export([try_again_restart/2]).

因此supervisor同样也实现了code_change回调:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
code_change(_, State, _) ->
case (State#state.module):init(State#state.args) of
{ok, {SupFlags, StartSpec}} ->
case set_flags(SupFlags, State) of
{ok, State1} ->
update_childspec(State1, StartSpec);
{invalid_type, SupFlags} ->
{error, {bad_flags, SupFlags}}; % backwards compatibility
Error ->
{error, Error}
end;
ignore ->
{ok, State};
Error ->
Error
end.

发现当supervisor进程执行code_change回调方法只是简单的执行回调方法Mod:init/1,原来这个init/1并不仅仅是supervisor进程启动的时候才会调用,还会在更新的时候调用.

结合之前的文档我们得到如下的结论:

我们都知道,sup模块的init/1返回的是supervisor的规格定义,而sup模块更新最主要的是更新supervisor内部的state,所以升级的时候必须要先加载新的模块代码,然后才能获得新的规格定义;相反降级的时候必要要先加载旧的代码,然后才能获得旧的规格定义.

解决了一个问题之后,我们也得到了一个更新supervisor进程的注意事项:

sup模块的 init/1 并不仅仅是supervisor进程启动的时候才会调用,还会在更新的时候调用,因此不要在init/1里面执行时间比较长的操作,也不要在里面做一些影响系统状态的操作,只要返回sup的规格定义就行了.

除了这个注意事项,还有其它的~

注意事项

  1. 更新sup模块的规格定义并不会影响到现有的子进程.

  2. 新增的子规格会被加到进去sup的state但是不会自动启动.

  3. 删除的子规格不会被删除,也不会自动关闭.

  4. 如果要实现上述两个功能的,须手动增建apply指令.如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    %% 假定我们要新增一个m1模块到ch_sup:
    {"2",
    [{"1",
    [{update, ch_sup, supervisor},
    {apply, {supervisor, restart_child, [ch_sup, m1]}}
    ]}],
    [{"1",
    [{apply, {supervisor, terminate_child, [ch_sup, m1]}},
    {apply, {supervisor, delete_child, [ch_sup, m1]}},
    {update, ch_sup, supervisor}
    ]}]
    }.

    我们可以看到上面的列子,指令之间的顺序很重要.

如果在版本1升级到2时,在{update, ch_sup, supervisor}先执行{apply, {supervisor, restart_child, [ch_sup, m1]}}是不会成功的,
因为此时ch_sup里并没有m1这个模块的规格定义;
同理,在降级的时候,如果{apply, {supervisor, terminate_child, [ch_sup, m1]}},
{apply, {supervisor, delete_child, [ch_sup, m1]}}在{update, ch_sup, supervisor}后面,也不会成功,
因为ch_sup还原之后就没有了m1这个模块的规格定义了.


其实用之前我们说的appup插件的话,并不需要手动操作上面这一步,插件会在生成阶段动态的去调用前后两个版本sup的init方法,然后通过比对规格定义得到这些指令.
此外,执行apply指令的时候,我们的ch_sup进程的必须有注册名,我们知道pid是动态的,也不能在appup文件里面知道ch_sup的pid,因此必须有注册名,才能查找到ch_sup这个进程.

很多同学可能会有一个疑问,我们到底是升级代码还是升级进程?可能有时候会两个概念混淆了,所以下一章,我们会来看看:升级模块与进程.

插件虽然能帮助我们很方便的生成appup文件,但是如果你想要在升级的时候执行一下你的升级mnesia的record数据结构方法,插件不能分析出来我们要做这个的.

所以插件只能帮我们做其中一部分工作而已,另外一部分需要我们自己去处理,因此我们也需要对appup里面的指令进行进一步的学习.

预备知识

先来看看官方对于appup文件的描述:

The application upgrade file defines how an application is upgraded or downgraded in a running system.
This file is used by the functions in systools when generating a release upgrade file relup.
.appup文件是定义运行中的系统的应用怎么升级和降级,文件后期会被转成.relup 文件

之前我们也看过appup文件的内容了,大概是长这个样子:

1
2
3
4
5
6
7
%% appup generated for rus_rel by rebar3_appup_plugin (2017/09/18 14:46:53)
{ "0.2.0",
[{ "0.1.0",
[{update,rus_gen_server,{advanced,[]},brutal_purge,brutal_purge,[]}] }],
[{ "0.1.0",
[{update,rus_gen_server,{advanced,[]},brutal_purge,brutal_purge,[]}] }]
}.

现在再来看看它的格式定义:

1
2
3
{Vsn,
[{UpFromVsn, Instructions}, ...],
[{DownToVsn, Instructions}, ...]}.

Vsn = string()
Current application version.
UpFromVsn = string() | binary()
An earlier application version to upgrade from. If it is a string, it is interpreted as a specific version number. If it is a binary, it is interpreted as a regular expression that can match multiple version numbers.
DownToVsn = string() | binary()
An earlier application version to downgrade to. If it is a string, it is interpreted as a specific version number. If it is a binary, it is interpreted as a regular expression that can match multiple version numbers.
Instructions
A list of release upgrade instructions, see Release Upgrade Instructions. It is recommended to use high-level instructions only. These are automatically translated to low-level instructions by systools when creating the relup file.

1
2
3
4
{Vsn, %% Vsn 表示的是新版本
[{UpFromVsn, Instructions}, ...], %% 第二个属性表示升级
[{DownToVsn, Instructions}, ...] %% 第三个表示降级
}.

UpFromVsn 表示 从 UpFromVsn 这个版本升级到 Vsn 需要做的指令(Instructions), 可以看到这是一个列表,表示可以支持多个版本

DownToVsn 表示 从 Vsn 这个版本降级到 DownToVsn 需要做的指令(Instructions), 可以看到这也是一个列表,表示可以支持多个版本

下面我们来看一下例子:

1
2
3
4
{"2.0.0",
[{"1.0.0", [{load_module, m}]}], %% 版本"1.0.0"升级到版本"2.0.0", 加载模块m
[{"1.0.0", [{load_module, m}]}] %% 版本"2.0.0"降级到版本"1.0.0", 加载模块m
}.

从文档中可以看到如果UpFromVsn或者DownToVsn的值是binary类型的话,表示可以用正则表达式去匹配版本,可以为多个版本:

1
2
3
4
{"2.0.0",
[{<<"1\\.[0-9]+\\.[0-9]+">>, [{load_module, m}]}], %% 所有版本"1.*.*"升级到版本"2.0.0", 加载模块m
[{<<"1\\.[0-9]+\\.[0-9]+">>, [{load_module, m}]}] %% 版本"2.0.0"降级到所有版本"1.*.*", 加载模块m
}.

另外比较重要的一点是, 官方文档提到:

It is thus recommended that code is changed in as small steps as possible, and always kept backwards compatible. 官方推荐每次更新版本尽可能使用每个小改动版本,用多个版本迭代更新来进行,并且保持向后兼容,利于版本回退.

下面回到正题,来说说指令(Instructions)

指令(Instructions)

OTP supports a set of release handling instructions that are used when creating .appup files. The release handler understands a subset of these, the low-levelinstructions. To make it easier for the user, there are also a number of high-levelinstructions, which are translated to low-level instructions by systools:make_relup.
Some of the most frequently used instructions are described in this section. The complete list of instructions is included in the appup(4) manual page in SASL.

指令分为两种,普通(High-Level)指令和底层(Low-Level)指令的区别: 普通指令是提供给用户使用的,而底层指令是通过普通指令转换而来的.

我们之前生产的*.appup文件, 会在版本发布前的relup阶段,通过调用ystools:make_relup将全部普通指令转换为底层指令.

这个章节我们只是介绍了一些比较常用到的指令,另外还有很多指令大家可以去这个连接获取更加详细的文档.

简单代码热更指令:

1
{load_module, Module}

这个指令的作用就跟我们之前在eshell里面执行l(Module).一样

同步代码热更指令:

1
2
{update, Module, {advanced, Extra}}
{update, Module, supervisor}

可以看到这里有两个:第一个是更新进程内部状态的,而第二个是专门提供给supervisor跟新sup进程内部状态的;之后我会说一下他们的区别.

应用(application)指令:
{add_application, app} - 增加app应用-根据relup自动生成
{remove_application, app} - 删除app应用-根据relup自动生成
{restart_application, app} - 重启app应用
启动关闭进程指令:

1
2
{start, [Mod]}  %% 启动
Mod = atom()

内部通过执行方法supervisor:restart_child/2,启动所有使用Mod的进程.

1
2
{stop, [Mod]}
Mod = atom()

内部通过执行方法supervisor:terminate_child/2,关闭所有使用Mod的进程.

执行指令:

1
2
3
{apply, {M, F, A}}
M = F = atom()
A = [term()]

内部实现就是apply(M, F, A).

更新配置:

这个并不需要指令,但是我们的配置发生变化了怎么办呢?

文档告诉我们:

Module is the application callback module as defined by the mod key in the .appfile.
Changed and New are lists of {Par,Val} for all changed and added configuration parameters, respectively.
Removed is a list of all parameters Par that have been removed.
%% 只要在应用回调模块实现config_change/3方法,在更新的时候,otp就会回调这个方法
Module:config_change(Changed, New, Removed).
%% Changed-有修改的配置 和 New-新增的配置 都是键值对列表[{Par,Val},…]
%% Removed-删除的配置 是所有键的列表[Par,…]
非erlang代码的更新:

官方并没有支持,但是给了一个解决方案,就是在erlang的code_change方法里先关闭旧的port然后再开启新的port.


这一章简单的介绍了一下各个指令的说明,看过官方文档的同学可能知道,supervisor进程更新方面官方文档上有很长的一段描述,所以我特此新开了一章关于supervisor进程的更新说明.

为了不加班,所以程序员一般都会开发一些便捷的工具来提高开发效率,下面就让我来给大家讲讲升级发布版本工具:rebar3_appup_plugin

发布

看前缀就知道,这是一个rebar3的插件,如果对rebar3不熟悉的请先移步去官网看看文档.

那我们先来创建一个rebar3的项目:

1
rebar3 new release rus_rel && cd rus_rel

接下来,当然是添加这个插件到rebar.confg里面:

1
2
3
4
5
6
7
8
9
10
11
12
13
{erl_opts, [debug_info]}.
{plugins, [
rebar3_appup_plugin
]}.
{deps, []}.
{relx, [
{release, { rus_rel, "0.1.0" }, [rus_rel,sasl]},
{sys_config, "./config/sys.config"},
{vm_args, "./config/vm.args"},
{dev_mode, false},
{include_erts, false},
{extended_start_script, true}
]}.

记得relx的配置dev_mode要修改为false(此处是演示,正式设置的时候当然是设置profiles里面的设置)

我们需要增加一个gen_server模块

rus_gen_server.erl :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-module(rus_gen_server).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-export([
start_link/0
]).
-record(state, {}).
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
init([]) ->
{ok, #state{}}.
handle_cast(_Info, State) ->
{noreply, State}.
handle_call(_Info, _From, State) ->
{reply, ok, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, State) ->
{ok, State}.
code_change(OldVsn, State, Extra) ->
io:format("~p ~p ~p~n", [OldVsn, State, Extra]),
{ok, State}.

然后我们在sup里面增加这个模块作为子进程,之后我们会在第二版修改这个进程的内部状态

rus_rel_sup.erl :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-module(rus_rel_sup).
-behaviour(supervisor).
%% API
-export([start_link/0]).
%% Supervisor callbacks
-export([init/1]).
-define(SERVER, ?MODULE).
%%====================================================================
%% API functions
%%====================================================================
start_link() ->
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
%%====================================================================
%% Supervisor callbacks
%%====================================================================
%% Child :: {Id,StartFunc,Restart,Shutdown,Type,Modules}
init([]) ->
Children = [
{rus_gen_server, {rus_gen_server, start_link, []}, permanent, 5000, worker, [rus_gen_server]}
],
{ok, { {one_for_all, 0, 1}, Children} }.

然后我们来发布一个版本:

1
rebar3 release tar

就这么简单,我们0.1.0的版本就发布好了,接下来运行起来:

1
2
3
4
5
$ tar zxf rus_rel-0.1.0.tar.gz // 解压
$ ./bin/rus start // 运行
$ ./bin/rus_rel versions
Installed versions:
* 0.1.0 permanent

ok, 准备工作完成

发布版本升级

接下来我们修改gen_server模块state的数据结构,增加一个字段id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-module(rus_gen_server).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-export([
start_link/0
]).
-record(state, {id}).
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
init([]) ->
{ok, #state{}}.
handle_cast(_Info, State) ->
{noreply, State}.
handle_call(_Info, _From, State) ->
{reply, ok, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, State) ->
{ok, State}.
code_change(OldVsn, {state}, Extra) ->
io:format("~p ~p ~p~n", [OldVsn, State, Extra]),
{ok, #state{id = 1}}.

然后别忘记了修改版本号

rus_rel.app.src修改vsn为0.2.0

1
rebar.config的{ rus_rel, “0.1.0” }修改为{ rus_rel, “0.2.0” }

然后先发布一下:

rebar3 release
之后再运行插件:

1
rebar3 appup generate

这个插件需要之前的版本也在本地目录下面,因为它的运行机制是通过对比两个rel文件夹的beam, 然后根据各自不同自动生成.appup 文件:

1
2
3
4
5
6
7
%% appup generated for rus_rel by rebar3_appup_plugin (2017/09/18 14:46:53)
{ "0.2.0",
[{ "0.1.0",
[{update,rus_gen_server,{advanced,[]},brutal_purge,brutal_purge,[]}] }],
[{ "0.1.0",
[{update,rus_gen_server,{advanced,[]},brutal_purge,brutal_purge,[]}] }]
}.

然后我们再执行relup 和 打包:

1
rebar3 relup tar

这样我们就获得了一个带有更新指令的发布,然后将这个发布包发到运行根目录下的releases文件夹下面, 并执行更新命令:

1
2
3
4
5
6
7
8
9
$ ./bin/rus_rel upgrade 0.2.0
Release 0.2.0 not found, attempting to unpack releases/rus_rel-0.2.0.tar.gz
Unpacked successfully: "0.2.0"
Installed Release: 0.2.0
Made release permanent: "0.2.0"
$ ./bin/rus_rel versions
Installed versions:
* 0.2.0 permanent
* 0.1.0 old

ok,整个更新就完成了,你可以attach进去看一下进程的内部状态,发现已经更新了.

appup插件说明

支持功能:

原理:通过对比两个rel文件夹的beam, 然后根据各自不同自动生成.appup 文件.
支持.appup.src 生成.appup 文件.
当gen_server 存在了-state_record(state),插件会自动注入state变更代码到beam里, More info.
支持根据两个版本sup模块init的返回的规格定义不同,生成相应的升级指令
自动生成模块之间的依赖关系.
使用注意事项:


两个版本必须编译参数里必须都有debug_info ,因为插件需要debug_info 的信息, 担心源码安全的话, 可以通过在项目下增加.erlang.crypt 文件来保护源码.
如果需要手动修改.appup 文件, 则需要编写.appup.src , 但是.appup.src 生成.appup 文件后,插件不会再生成.appup文件,因此建议先用rebar3 appup generate 出 .appup文件,然后再以这个文件的基础去修改,然后作为.appup.src.

Otp的gen_server模块,我想应该是大家在erlang开发中接触最多的模块了,但是我们经常会碰到要修改内部状态(state)的时候,例如原来的state不再适用于新的需求,需要改变state的数据结构,增加或者减少一个字段

遇到这样情况之后,我想很大一部分同学都会选择关闭这个进程,然后加载新代码,然后再开启这个进程,但是面对运行中的环境,要做这个操作只能重启vm了

其实我们并不需要重启vm,只需要多动动手指敲多几行代码就能完成这个代码的热更了


sys模块的api

首先先带大家认识一下这个sys模块的api:

1
2
3
4
sys:get_state(Name) -> State. 获取进程的state
sys:suspend(Name) -> ok. 暂停进程
sys:change_code(Name, Module, OldVsn, Extra) -> ok | {error, Reason}. 更新代码之后告诉进程进行内部状态变更
sys:resume(Name) -> ok. 恢复进程

之后会用到这些api,接下来,我会演示一下怎么用这些api去更新一个进程的内部状态:

实例演示
sync_code_reload.erl (vsn-1):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-module(sync_code_reload).
-behaviour(gen_server).
-export([start/0, vsn/0]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-record(state, {}).
start() ->
gen_server:start(?MODULE, [], []).
vsn() -> 1.
init([]) ->
{ok, #state{}}.
handle_call(_Request, _From, State) ->
{reply, ok, State}.
handle_cast(_Request, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, {state}, _Extra) ->
io:format("old:~p, ex:~p~n", [_OldVsn, _Extra]),
{ok, #state{}}.

这份代码很简单,什么东西都没有处理,这就是第一个版本

先启动进程:

1
1> {ok, P} = sync_code_reload:start().

进程启动完成,之后万恶的的策划给我们加了一个需求,需要增加一个字段id来识别这个进程,而且更新这个还不能重启vm,不然他们就有借口拿我们程序员祭天了,但是这难不倒我们,对模块进行简单修改之后:

sync_code_reload.erl (vsn-2):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-module(sync_code_reload).
-behaviour(gen_server).
-export([start/0, vsn/0]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-record(state, {a}).
start() ->
gen_server:start(?MODULE, [], []).
vsn() -> 2. %% 1 => 2
init([]) ->
{ok, #state{}}.
handle_call(_Request, _From, State) ->
{reply, ok, State}.
handle_cast(_Request, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, {state}, _Extra) ->
io:format("old:~p, ex:~p~n", [_OldVsn, _Extra]),
{ok, #state{id = 1}}. %% 增加 id = 1

我们先来看看这个进程的内部状态,还记得之前说的api吧,现在排上用场了:

1
2
2> sys:get_state(P).
{state}

首先我们需要先暂停这个进程一小会:

1
2
3> sys:suspend(P).
ok

然后加载之前编译好的代码:

1
2
4> l(sync_code_reload).
{module,sync_code_reload}

下一步,我们通知一下进程进行内部状态的变更:

1
2
3
5> sys:change_code(P, sync_code_reload, "0.1.1", []).  
old:"0.1.1", ex:[]
ok

返回ok表示成功,然后我们再来看看状态:

1
2
6> sys:get_state(P).
{state,1}

ok,新的字段已经添加成功了,接下来,我们恢复这个进程的运行:

1
2
7> sys:resume(P).
done!

整个流程下来,我想就算你用了机械键盘,速度很快,很666,DuangDuangDuang,但是你还是会被拿去祭天的,因为,整个过程肯定超过10秒,如果是游戏服,这个进程是游戏进程,那么你的玩家用户就整整10秒没有响应,因为你暂停了呀

其实你可以这样:

1
sys:suspend(P),l(sync_code_reload),sys:change_code(P, sync_code_reload, "0.1.1", []),sys:resume(P).

将操作连在一起操作,肯定不会超过0.1秒,除非你在你的回调方法code_change里执行了timer:sleep(999999).这样做的话,上天也救不了你~

友情提示:
不想被拉去祭天,就别在code_change里面执行耗时的操作!!!
不想被拉去祭天,就别在code_change里面执行耗时的操作!!!
不想被拉去祭天,就别在code_change里面执行耗时的操作!!!

说三遍~你懂得!


另外如果我们有很多个进程,很多个模块需要做进程内部状态的热更呢?

难道每一个进程都做一次上面的操作吗?显然是不可能的~

之后的章节我们会来讲讲怎么用otp的方式来更新一个发布版本!!

代码热更

下面我们会展示两份代码,分别是版本1和版本2

test_load.erl (vsn-1):

1
2
3
4
5
6
7
8
9
10
-module(test_load).
-export([print/0]).
print() ->
io:format("vsn~p~n", [1]).
test_load.erl (vsn-2):

-module(test_load).
-export([print/0]).
print() ->
io:format("vsn~p~n", [2]).

两份代码仅有的差别只是打印的版本号不同,下面我们来看看怎么让运行中版本1代码热更到版本2的代码

1
2
3
4
5
6
7
8
9
10
erlc test_load.erl %% 先编译代码
erl %% 打开eshell
1> test_load:print(). // vsn=1
vsn1
ok
2> l(test_load). // 先在外部执行erlc test_load.erl编译代码 然后再 加载新代码
{ok,test_load}
3> test_load:print(). // vsn=2
vsn2
ok

可以看到,只用通过执行 l(test_load). 就能直接加载新的代码了,很简单对吧,我相信这个知识点大家都懂,因为很多同学在线上紧急修复代码就是这么干的,但是这样做却很容易出问题,而且有很多很多局限性,在以后的章节我们会一起探讨这些问题,现在继续来看看代码版本方面的知识

代码版本

先问大家一个简单问题: 在erlang的vm里面,代码模块能同时存在几个版本?

答案是两个!下面我用实例来演示一遍:

code_replace.erl (vsn-1):

1
2
3
4
5
6
7
8
9
10
11
-module(code_replace).
-export([start/0, loop/0]).
start() ->
spawn(fun loop/0).
loop() ->
receive
_ -> loop()
after 1500 ->
io:format("vsn~p~n", [1]),
loop()
end.

当前我们有一份代码,这个代码主要工作就是开启一个进程,然后这个进程每隔1.5秒打印一下版本号,我们先启动一下这个进程:

1
2
3
4
5
6
erlc code_replace.erl && erl
1> Pid = code_replace:start(). %% 开启进程
vsn1
vsn1
vsn1
...

可以看到我们的进程运行良好,但是突然我开始讨厌打印1了,我想让它打印2

code_replace.erl (vsn-2):

1
2
3
4
5
6
7
8
9
10
11
-module(code_replace).
-export([start/0, loop/0]).
start() ->
spawn(fun loop/0).
loop() ->
receive
_ -> loop()
after 1500 ->
io:format("vsn~p~n", [2]), %% 修改 1 => 2
loop()
end.

先编译:

erlc code_replace.erl

然后加载到vm里:

1
2
3
4
5
2> l(code_replace).
vsn1
vsn1
vsn1
...

虽然我们加载了新代码,但是我们却看到之前我们开启的进程打印的仍然是1,难道是加载代码失败了吗?我们来再开一个进程看看:

1
2
3
4
5
6
3> P2 = code_replace:start().
vsn2
vsn1
vsn2
vsn1
...

可以看到,代码加载是有效的,而我们之前的进程运行的仍然是旧版本的代码,而新的进程运行的是新版本的代码

我是个多变的人~ >_ > 突然又不喜欢打印2了,我想让它打印3:

code_replace.erl (vsn-3):

1
2
3
4
5
6
7
8
9
10
11
-module(code_replace).
-export([start/0, loop/0]).
start() ->
spawn(fun loop/0).
loop() ->
receive
_ -> loop()
after 1500 ->
io:format("vsn~p~n", [3]), %% 修改 2 => 3
loop()
end.

然后加载到vm里:

1
2
3
4
5
6
erlc code_replace.erl
4> l(code_replace).
vsn2
vsn2
vsn2
...

奇怪的事情发生了

原来是vsn1和vsn2都会打印出来,但是加载新版本代码之后,vsn1的打印消失了,我们来看看他是不是被kill了

1
5> erlang:is_process_alive(P). %% false

可以看到我们的第一个进程P已经壮烈牺牲了

接下来,问题就来了,为啥第一个进程死掉了,而第二个进程却还或者呢?

我们去官方文档看看解析:连接

Erlang supports change of code in a running system. Code replacement is done on module level.
The code of a module can exist in two variants in a system: current and old. When a module is loaded into the system for the first time, the code becomes ‘current’. If then a new instance of the module is loaded, the code of the previous instance becomes ‘old’ and the new instance becomes ‘current’.
Both old and current code is valid, and can be evaluated concurrently. Fully qualified function calls always refer to current code. Old code can still be evaluated because of processes lingering in the old code.
If a third instance of the module is loaded, the code server removes (purges) the old code and any processes lingering in it is terminated. Then the third instance becomes ‘current’ and the previously current code becomes ‘old’.
To change from old code to current code, a process must make a fully qualified function call.
众所周知, erlang支持运行时的代码更新,代码更新作用在模块级别.每个代码模块允许存在两个版本在系统中:当前和旧两个版本.当一个模块第一次加载进系统时,这个版本视为当前版本.如果有新的模块加载,之前的版本变为旧的版本,然后新的模块变为当前版本.两个版本都是有效的,可以同时运行.全模块调用总是指向当前版本.旧代码仍然起作用是因为进程仍然在使用着旧代码.如果第三个版本模块加载进系统时,系统会清除旧代码和关闭掉那些仍然在使用旧代码的进程.然后第三个版本会变成当前版本代码,第二个版本会变成旧版本代码.进程必须使用全模块调用才能将旧版本模块代码切换到新版本模块代码.

从这里可以看到我们之前进程做的热更其实是很危险的,万一我们热更的时候,有部分进程的驻留模块用的仍然是旧版本的模块代码,就会造成该进程被杀掉,通常在游戏服里面的表现就是全服玩家集体掉线,因为玩家进程的驻留模块还是旧的呀

那我们怎么来解决这个问题呢?

进程代码版本切换

先来看看这份代码:

code_replace_new.erl (vsn-1):

1
2
3
4
5
6
7
8
9
10
11
12
-module(code_replace_new).
-export([start/0, loop/0]).
start() ->
spawn(fun loop/0).
loop() ->
receive
code_switch -> %% 处理消息
?MODULE:loop()
after 1500 ->
io:format("vsn~p~n", [1]),
loop()
end.

这份代码在原来的基础上加了接受code_switch消息之后,调用一下?MODULE:loop(),这个就是官方说的全模块调用,执行完这一步之后,进程的驻留模块就会从旧版本变更为新版本,下面我们用实际操作来看一下:

启动的方法还是跟之前一样,这里就不重复了

启动之后,我们将打印变为2,然后编译,然后加载,发现还是打印1

1
io:format("vsn~p~n", [2]), %% 1 => 2

不急,我们还没有给进程发送消息让它变更

1
2
3
4
3> P ! code_switch.
vsn2
vsn2
...

更新成功!!

这样我们就成功将进程的驻留模块从版本一替换到版本二了,也很好的解决了进程被杀掉的问题,那是不是我们每一个进程都需要写这样一个代码切换的代码呢,那岂不是很麻烦,

其实otp已经帮我们想好了解决方法,就是gen_server,这个在之后的章节再来详细看看

1.从 Erlang 官网下载源代码

1
$ wget http://erlang.org/download/otp_src_21.0.tar.gz

2.安装依赖

1
$ sudo yum -y install gcc gcc-c++  m4 ncurses-devel openssl-devel perl

3.解压第一步下载的源代码

1
$ tar zxvf otp_src_21.0.tar.gz

4.安装

1
2
$ cd opt_src_21.0
$ ./configure --prefix=/usr/local/erlang --without-javac && make && sudo make install

5.创建链接

1
ln -s /usr/local/erlang/bin/erl /usr/local/bin/erl

6.验证

1
$ erl

一.介绍

在去年的网盘倒闭风波搞怕了不少人吧,转移那么多数据也是头疼,所以不如找几个不怕倒闭的……比如DropBox或者Google Drive。微软的OneDrive虽然国内能直连,但是速度其实也是比较头疼的,倒不如这种在特定环境下反而可以满速的了。

Google Drive在GSuite For Bussiness(或者Education)的版本里可以做到无限容量(起码是现在),所以我拿来做服务器上的数据备份,美滋滋,全球服务器都几乎能满速,而且也不用担心容量问题,棒棒的。那么问题来了,我们怎么使用呢,毕竟我绝大部分是Linux服务器,也没装图形界面啥的。官方提供的是有界面的工具,所以我们需要找CLI工具,也就是能在命令行中直接运行的。

本次我们介绍的是gdrive ——>传送门

该工具是由Go语言编写的,单个二进制文件即可使用,采用Google API方式进行授权,并在本地缓存授权。贴心的作者提供了一大堆各种平台的二进制文件且完全静态编译,无需任何其他依赖,简直牛逼。当然,如果你非想要编译(比如你想要自己修改啥的),那么也是相当简单的(厉害了我的Go)。

二.安装配置

由于全都给你准备好了……所以安装真的太鸡儿简单了

wget -O /usr/bin/gdrive https://docs.google.com/uc?id=0B3X9GlR6EmbnQ0FtZmJJUXEyRTA&export=download

chmod +x /usr/bin/gdrive
然后就好了……接下来就是配置了

gdrive about
然后会出现一串网址并询问验证码

将地址粘贴到浏览器并登陆账号,会返回一串代码

将代码粘贴回去回车,然后会返回你的账户信息

gdrive程序会自动将你的token保存在用户目录下的.gdrive目录中,所以如果不需要了记得把这个文件删掉

当然也可以去看看谷歌授权那个页面——> 传送门 删掉授权,不过这个貌似删掉了其它所有用gdrive的客户端都得重新来,所以除非你完全不需要了,不然还是留着吧

三.操作

到这儿其实你已经可以通过gdrive在命令行中进行正常的上传下载了,我这边简单的介绍几种常用操作,详细的请前往Github查看说明或者使用gdrive help命令查看可用命令

①gdrive的复杂操作需要以表达式的方式进行,这是由于Google Drive的API要求的

所以会存在如下这样的操作

关键字检索

gdrive list -q “name contains ‘关键字’”

列出指定目录下的文件,不支持递归显示

gdrive list -q “‘文件夹ID’ in parents”
②在Google Drive中文件或者是文件夹都以唯一的ID形式存在,所以请善用list命令查看ID

③操作默认入口均为Google Drive根目录,如果需要指定操作的目标位置,需要以-p, –parent 参数来指定父目录

④真·常用操作

从Google Drive根目录处列目录(文件)

gdrive list

从Google Drive根目录处创建文件夹

gdrive mkdir “文件夹名”

上传文件到Google Drive根目录处

gdrive upload /path/to/file

下载指定文件到当前位置(如果需要下载文件夹可以使用-r, –recursive 来实现递归下载)

gdrive download 文件ID

四.附

其实,作为一个CLI工具,它的最大作用是配合脚本实现定时操作

我们可以按照日期来创建目录或是作为文件名来实现压缩备份指定目录并上传

这类脚本网上不少,所以大家自己去找找吧,我这边也不写了

阿里云云服务器优惠拼团活动开启,活动机型2核4G突发性能t5实例,优惠价格269元1年、499元2年、699元3年,可选香港节点,阿里云惠网分享阿里云拼团服务器优惠:
优惠实例规格:突发性能实例t5,30%CPU性能
云服务器配置:2核CPU、4G内存
地域节点:华北1、华北2、华北3、华东1、华东2、华南1、香港;
优惠价格:269元1年、499元2年、699元3年


阿里云服务器优惠699元一年

  1. 代金劵领取
  2. 活动查看

活动规则

一、活动对象
1、阿里云官网注册的个人实名认证用户。其中,新老用户均可作为拼团活动的推荐者,但只有未购买过云产品(即账号下无付费订单记录)的新用户方有资格参与拼团购买。
2、同一用户仅有一次参与拼团推荐或拼团购买指定云服务器的机会。例如:同一用户使用不同账号作为推荐者参与本活动的,仅首个参与活动的账号可获赠;在阿里云官网已有消费的用户重新注册新账号参与拼团购买的,新老账号将被视为同一用户,不具有购买资格。

二、活动时间
2018年8月20日-2018年9月7日。

三、活动规则
1、活动期间,推荐者可推荐新用户通过活动专属页面购买指定云服务器,具体规格如下:
①实例规格:阿里云服务器入门型2核4G 带宽:1M固定带宽 系统盘:40G 高效云盘;
②节点包含华北1、华北2、华北3、华东1、华东2、华南1、香港;
③、阿里云服务器入门型实例说明:
a、入门型实例是以基准CPU计算性能为基础(基准CPU与规格大小正相关),通过CPU实际使用率与基准CPU使用率差值的累计加权获取积分值,CPU积分消耗至0时,实例CPU性能被控制到基准运行;
b、入门型云服务器适用于平时不会持续高压力使用CPU的非企业级轻负载场景,如Web应用服务器、轻负载应用、微服务、开发测试压测服务应用等,不适用于长时间超过性能“基线”的需求场景或企业计算性能需求场景,该类场景推荐使用通用型g5、计算型c5等高性能系列云服务器。
2、接受推荐的每一新用户通过活动专属页面,可以269元/年的拼团价格购买1台指定云服务器,购买时长最少1年起,最长3年封顶。
3、用户应在15分钟内完成拼团订单的支付,否则订单将自动失效。
4、推荐者成功推荐1个或以上新用户拼团购买的指定云服务器,即视为推荐成功,可获赠相同规格的云服务器。
5、奖励推荐者的云服务器时长将以活动期间最终累计成功推荐的参与者数量为准,推荐越多,所赠送的服务器时长越长,最高36个月封顶。具体赠送规则如下:
成功推荐1人,送2个月云服务器;
成功推荐2人,送4个月云服务器;
成功推荐3人,送6个月云服务器;
成功推荐4至10人,送12个月云服务器;
成功推荐11至19人,送24个月云服务器;
成功推荐20人及以上,送36个月云服务器。
6、赠送推荐者的云服务器将以提货券的形式于2018年9月10日发放到推荐者账号中,推荐者可登录阿里云官网后进入“控制台-费用中心-提货券管理”查看并点击激活使用,提货券的激活有效期为60天,过期无法激活使用。提货券使用规则:
a. 提货券仅限新购,不支持升级、续费订单,可以转移给其他阿里云用户使用。
b. 使用提货券购买的实例不支持自定义选择安全组等VPC相关信息。
c. 提货券激活有效期为60天,请于11月8日23:59:59前完成兑换,过期后无法激活使用。

7、推荐成功的组员,在30天内再首购相关云产品组长还将获得其实付金额15%的返现。(通过活动页面购买的云服务器不参与返现)返现规则及产品
8、新用户参与购买的特价云服务器,不支持使用代金券、幸运券或其他优惠券,不支持退订、活动价续费及降配操作。
9、如用户存在作弊、欺诈或通过其他非正常手段获取利益的行为,阿里云有权收回相关权益。

四、名词解释
1、“阿里云官网”,是指包含域名为www.aliyun.com的网站以及阿里云客户端,如APP,但阿里云国际站,包括alibabacloud.com以及所有下属页面和jp.aliyun.com以及所有下属页面除外。
2、“同一用户”,是指根据不同阿里云账号在注册、登录、使用中的关联信息,阿里云判断其实际为同一用户。关联信息举例:同一证件、同一手机号、同一支付账号、同一设备、同一地址等。
3、“成功推荐”,是指通过推荐者页面成功支付购买产品,特殊退款及未付费订单不计为成功推荐。
4、“新用户”,是指其阿里云账户内没有任何付费的云产品订单的阿里云个人用户。
5、“云产品”,是指阿里云官网售卖的全部产品和服务,但不包括域名、虚机等产品。