当前位置: 永利棋牌 > 小说 > 正文

简单Elixir游戏服设计

时间:2019-10-01 13:19来源:小说
  在分析完gen module(的主体module 了.gen_server 的主体 module 暂不涵括terminate, hibernate,debug trace 相关的内容,这些会单独拉出来分析. 在上一篇关于Emysql pool()的分析的最后提到 之前写了篇关于

 

在分析完gen module ( 的主体module 了.gen_server 的主体 module 暂不涵括terminate, hibernate, debug trace 相关的内容,这些会单独拉出来分析.

在上一篇关于Emysql pool ()的分析的最后提到

之前写了篇关于call还是cast的讨论,实际等要改成call的时候又发生了疑问。

gen_server 主要包括start 初始化部分, MAIN loop. 其中MAIN loop 为gen_server的主体结构, 其中,处理Label 为'$gen_call' (也就是handle_call)的消息使用handle_msg 处理, Label为'$gen_cast'(也就是handle_cast)的消息以及无Label(handle_info)的消息经由handle_msg最终交给try_dispatch 函数.

现在的emysql_conn_mgr gen_server 进程属于单点,也就是所有的pool 的管理调度都是由一个进程来完成.

因为call的确有如下作用:

gen_server start

gen_server start 是由外部进程调用gen_server module 的start/start_link 函数,用以创建新的gen_server behavior 的进程. 在标准的Erlang application 中,一般是由supervisor 角色发起. start 的流程见下图

图片 1

如图中所示, ProcessA 是gen_server start 的调用者, 通过user module 的start/start_link 函数, 调用gen_server module 中的start, 继而调用gen module 中的start. 在gen module中, 由proc_lib:spawn 创建新的进程(ProcessB), 并以此调用init_it(gen); init_it(gen_server) ; init(user module) 完成gen_server behavior 进程的初始化.

都成说,gen_server behavior 的user module init 函数尽可能快的返回, 不要做任何阻塞性的操作.

在gen_server 的init_it 函数中, Mod:init 返回之后, 会调用proc_lib:init_ack/2, 用于向 start 的调用者返回结果

 1 init_it(Starter, self, Name, Mod, Args, Options) ->
 2     init_it(Starter, self(), Name, Mod, Args, Options); %% 注意, 如果使用nolink start 时, Parent 就是自己
 3 init_it(Starter, Parent, Name0, Mod, Args, Options) ->
 4     Name = name(Name0),
 5     Debug = debug_options(Name, Options),
 6     case catch Mod:init(Args) of
 7     {ok, State} ->
 8         proc_lib:init_ack(Starter, {ok, self()}),    %% 向调用者返回结果    
 9         loop(Parent, Name, State, Mod, infinity, Debug);
10     {ok, State, Timeout} ->
11         proc_lib:init_ack(Starter, {ok, self()}),         
12         loop(Parent, Name, State, Mod, Timeout, Debug);
13         …………

所以,尽可能快的返回,不在Mod:init中做任何阻塞以及耗时性的操作.

但是,很多情况下,在Mod:init 处理过程中,是用于与外部资源(如:DB,MQ等)创建链接,而这些操作很难确定其耗时性,咋办?牛逼的Erlang大神 Ferd 在其 Erlang in Anger (国内有翻译版:硝烟中的Erlang——Erlang 生产系统问题诊断、调试、解决指南) 中提供了一种方式

The following code attempts to guarantee a connection as part of the process’ state: 

 1 init(Args) ->
 2     Opts = parse_args(Args),
 3     {ok, Port} = connect(Opts),   %% 这种在init 函数中执行connect的方式不可取        
 4     {ok, #state{sock=Port, opts=Opts}}.
 5 [...]
 6 handle_info(reconnect, S = #state{sock=undefined, opts=Opts}) ->
 7     %% try reconnecting in a loop
 8     case connect(Opts) of
 9         {ok, New} -> {noreply, S#state{sock=New}};
10          _ -> self() ! reconnect, {noreply, S}
11 end;

Instead, consider rewriting it as: 

 1 init(Args) ->
 2     Opts = parse_args(Args),
 3     %% you could try connecting here anyway, for a best
 4     %% effort thing, but be ready to not have a connection.
 5     self() ! reconnect,   %% 给self 发送消息,替代在init 时connect的耗时/阻塞操作
 6     {ok, #state{sock=undefined, opts=Opts}}.
 7 [...]
 8 handle_info(reconnect, S = #state{sock=undefined, opts=Opts}) ->
 9     %% try reconnecting in a loop
10     case connect(Opts) of
11         {ok, New} -> {noreply, S#state{sock=New}};
12         _ -> self() ! reconnect, {noreply, S}
13     end;

注意: 第一种方式不可取.

如果在同一个Erlang node 中管理为数众多的pool,就会存在瓶颈. 对于热点进程而言,提高其process priority 是一个optimize 的方向,但是并不能彻底解决因单点带来的问题. 因此, 应该尝试将单个emysql_conn_mgr gen_server 进程拆分为多个, 而拆分的依据, 就是将pool 的管理optimize 为每个pool 交由一个emysql_conn_mgr 进程管理, 而不是现在的所有的pool 都是由同一个emysql_conn_mgr 管理.

1. 阻塞客户端

gen_server MAIN loop

使用proc_lib:init_ack 之后, gen_server init_it 会调用loop 进入gen_server 的MAIN loop 流程中. MAIN loop 使用receive 用以接收'$gen_call', '$gen_cast' 以及其他的message, 紧接着交由 decode_msg 函数进行处理.

 1 %%% ---------------------------------------------------
 2 %%% The MAIN loop.
 3 %%% ---------------------------------------------------
 4 loop(Parent, Name, State, Mod, hibernate, Debug) ->
 5     %% 这个坑在说hibernate 的时候再填
 6     proc_lib:hibernate(?MODULE,wake_hib,[Parent, Name, State, Mod, Debug]);
 7 loop(Parent, Name, State, Mod, Time, Debug) ->
 8     Msg = receive
 9            Input ->
10                 Input
11          after Time ->
12                timeout
13          end,
14     decode_msg(Msg, Parent, Name, State, Mod, Time, Debug, false).
15 
16 wake_hib(Parent, Name, State, Mod, Debug) ->
17     Msg = receive
18           Input ->
19             Input
20       end,
21     decode_msg(Msg, Parent, Name, State, Mod, hibernate, Debug, true).
22 
23 decode_msg(Msg, Parent, Name, State, Mod, Time, Debug, Hib) ->
24     case Msg of
25         %% 这个坑在说sys trace/get_status 的时候填
26       {system, From, Req} ->
27           sys:handle_system_msg(Req, From, Parent, ?MODULE, Debug,
28                     [Name, State, Mod, Time], Hib);
29     %% 这个坑在说 terminate 的时候填
30       {'EXIT', Parent, Reason} ->
31           terminate(Reason, Name, Msg, Mod, State, Debug);
32       _Msg when Debug =:= [] ->
33           handle_msg(Msg, Parent, Name, State, Mod);
34       _Msg ->
35           Debug1 = sys:handle_debug(Debug, fun print_event/3,
36                         Name, {in, Msg}),
37           handle_msg(Msg, Parent, Name, State, Mod, Debug1)
38     end.

在上面的代码片段中, L8 正是receive self 或外部进程的message, L23 是decode_msg 函数的入口, 在L33和L37 处调用handle_msg 函数进一步对msg消息进行处理.代码比较简单,没必要一行一行分析了.

pool 添加操作

对于pool 的数据结构,保持之前的结构不变.使用一个emysql_pool_mgr gen_server 进程,并在其中维护一个ets table(用以保存poolid 和 emysql_conn_mgr 进程ID).在每一次添加 pool 操作时start_child 一个emysql_conn_mgr 匿名进程,并与pool 的ID进行关联, 写入emysql_pool_mgr 进程维护的ets table 中.

 1 add_pool(#pool{pool_id=PoolId,size=Size,user=User,password=Password,host=Host,port=Port,
 2                database=Database,encoding=Encoding,start_cmds=StartCmds,
 3                connect_timeout=ConnectTimeout,warnings=Warnings}=PoolSettings)->
 4     config_ok(PoolSettings),
 5     case emysql_pool_mgr:has_pool(PoolId) of
 6         true -> 
 7             {error,pool_already_exists};
 8         false ->
 9             Pool = #pool{
10                     pool_id = PoolId,
11                     size = Size,
12                     user = User,
13                     password = Password,
14                     host = Host,
15                     port = Port,
16                     database = Database,
17                     encoding = Encoding,
18                     start_cmds = StartCmds,
19                     connect_timeout = ConnectTimeout,
20                     warnings = Warnings
21                     },
22             Pool2 = case emysql_conn:open_connections(Pool) of
23                 {ok, Pool1} -> Pool1;
24                 {error, Reason} -> throw(Reason)
25             end,
26             {ok, PoolServer} = emysql_pool_mgr:add_pool(PoolId, Pool2),
27             [gen_tcp:controlling_process(Conn#emysql_connection.socket, PoolServer)
28              || Conn <- queue:to_list(Pool2#pool.available)],
29             ok
30     end.

start_child emysql_conn_mgr 匿名进程, 并将poolid 与 emysql_conn_mgr 进程ID关联的操作都在emysql_pool_mgr:add_pool/2函数中实现(L26).

2. 有返回值能确定操作是否成功,并能很好的支持测试

 gen_server multi_call

call 在上一篇blog中已经提到, cast以及abcast 的实质就是调用erlang:send bif, 最终调用erts beam 下的dist.c .

multi_call 牵扯到多node , Erlang stdlib 中pg2 module 就主要使用multi_call 同步各自node 上ets 表的信息. 如:

 1 create(Name) ->
 2     _ = ensure_started(),
 3     case ets:member(pg2_table, {group, Name}) of
 4         false ->
 5             global:trans({{?MODULE, Name}, self()},
 6                          fun() ->
 7                                  gen_server:multi_call(?MODULE, {create, Name})
 8                          end),
 9             ok;
10         true ->
11             ok
12     end.

multi_call 调用 do_multi_call 函数, do_multi_call 使用Middleman process . Middleman process 负责给各node 发送 Label 为 '$gen_call' 的消息并等待各node 的结果返回. 

1 %% Middleman process. Should be unsensitive to regular
2 %% exit signals. The sychronization is needed in case
3 %% the receiver would exit before the caller started
4 %% the monitor.

最终, 通过exit 的方式返回给主调用进程, 而主调用进程会通过monitor/receive {'DOWN' ...} 的方式接收结果.

注意: Middleman process 需要monitor 目标node, 如果nodedown, 即会采取 call 失败的流程进行处理. 

 

参考: 

emysql_pool_mgr module

emysql_pool_mgr module 是一个gen_server 进程, 其中维护了一个ets table, 用于存放{PoolID, EmysqlConnMgrProcessID} 信息. 因为每次execute SQL语句时, 都需要对pool 操作,也就是需要获取与之对应的EmysqlConnMgrProcessID, 如果将关联信息保存在emysql_pool_mgr 进程中, emysql_pool_mgr 进程同样会成为单点, 因此使用ets table 来分担emysql_pool_mgr 进程的压力负担.

emysql_pool_mgr module 提供了一下几个API:

1, has_pool/1

用于判断当前系统中是否存在该pool,输入参数为PoolID

2, add_pool/2

用以start_child emysql_conn_mgr 进程, 并写入{PoolID, EmysqlConnMgrProcessID}信息到ets table,输入参数为PoolID 和 Pool结构

3, remove_pool/1

删除Pool, 并stop emysql_conn_mgr 进程, 输入参数为PoolID

4, get_pool_server/1

根据PoolID 获取对应的EmysqlConnMgrProcessID, 输入参数为PoolID

5, pools/0

当前系统中所有的PoolID以及其对应的EmysqlConnMgrProcessID

6, conns_for_pool/1

根据PoolID 获取对应的pool 中的所有链接, 输入参数为PoolID

3. 保证时序,只有call成功了,才能继续执行下一步

add_pool/2

add_pool/2 函数主要调用supervisor:start_child/2 函数在emysql_conn_pool_sup 监控树下添加emysql_conn_mgr gen_server 进程.

emysql_conn_pool_sup module 的代码片段:

1 start_link(Name, Module) ->
2     supervisor:start_link({local, Name}, ?MODULE, Module).
3 
4 init(Module) ->
5     {ok,
6      { {simple_one_for_one, 10, 1},
7       [{undefined, {Module, start_link, []}, temporary,
8         brutal_kill, worker, [Module]}]} }.

相当常见的使用方式.

可是除此之外,还有其他好处吗?麻烦呢?

emyslq_conn_mgr module

optimize 之后,之前的emysql_conn_mgr module 的代码就必须做一些简单调整, 在调用start_link 函数 init初始化时, 就需要将pool 信息添加到进程 state 信息中.

1 init([Pool]) ->
2     erlang:process_flag(priority, high),
3     {ok, #state{pools = [Pool]}}.

此处提升了emysql_conn_mgr 进程的 priority (L2).

而对于其他函数的调用, 需要添加一个EmysqlConnMgrProcessID 参数,使其在gen_server:call/2 时, 将 "?MODULE" 改为 EmysqlConnMgrProcessID.

如果table_server 只是为一个玩家所用,那么可能是合适的。

pool 使用

在调用execute 函数 执行某SQL 语句时, 代码同样需要做一些调整:

1 execute(PoolId, Query, Args, Timeout) when (is_list(Query) orelse is_binary(Query)) andalso is_list(Args) andalso (is_integer(Timeout) orelse Timeout == infinity) ->
2     PoolServer = emysql_pool_mgr:get_pool_server(PoolId),
3     Connection = emysql_conn_mgr:wait_for_connection(PoolServer, PoolId),
4     monitor_work(PoolServer, Connection, Timeout, [Connection, Query, Args]);

也就是在调用emysql_conn_mgr module 的函数之前,需要先根据PoolID 获取EmysqlConnMgrProcessID(L2), 然后将EmysqlConnMgrProcessID 作为emysql_conn_mgr module 函数的第一个参数进行调用(L3).

可惜不是,table_server 还需要广播信息,而这部分广播信息不可能放在玩家进程做,

initialize_pools

在现今的Emysql 项目中, 有一个初始化pools 的功能. 也就是可以在app start 的时候, 自动加载config 文件中配置好的pool .现在的做法是在emysql_conn_mgr 进程 init 的时候, 调用相关函数, 将结果添加到 进程 state 中.

optimize 之后的结构, emysql_conn_mgr 只有在手动添加一个pool 的时候才会被start_link, 也就不能执行相关函数执行initialize_pools 的操作.

现在调整为在emysql_pool_mgr init 之后, emysql_pool_mgr 发送{initialize_pools} message 给self, 在handle_info callback 函数中进行处理:

 1 handle_info({initialize_pools}, State) ->
 2     %% if the emysql application values are not present in the config      
 3     %% file we will initialize and empty set of pools. Otherwise, the      
 4     %% values defined in the config are used to initialize the state.      
 5     InitializesPools = 
 6         [      
 7             {PoolId, #pool{     
 8                 pool_id = PoolId,      
 9                 size = proplists:get_value(size, Props, 1),        
10                 user = proplists:get_value(user, Props),       
11                 password = proplists:get_value(password, Props),       
12                 host = proplists:get_value(host, Props),       
13                 port = proplists:get_value(port, Props),       
14                 database = proplists:get_value(database, Props),       
15                 encoding = proplists:get_value(encoding, Props),       
16                 start_cmds = proplists:get_value(start_cmds, Props, [])        
17             }} || {PoolId, Props} <- emysql_app:pools()     
18         ],
19     [begin
20         case emysql_conn:open_connections(Pool) of
21             {ok, Pool1} ->
22                 emysql_pool_mgr:add_pool(PoolId, Pool1);
23             {error, Reason} ->
24                 erlang:throw(Reason)
25         end
26     end || {PoolId, Pool} <- InitializesPools],
27     {noreply, State, ?HIBERNATE_TIMEOUT};

总不能optimize 之后, 直接remove 掉一些函数功能啊. :)

这样如果在调用端和被调用端都发消息,就有点冗余。

总结

整体上,对每一个pool 使用与之对应的一个emysql_conn_mgr 进程作为管理.能够尽可能的避免多pool 的管理工作给emysql_conn_mgr 进程带来的压力.

而新引入的emysql_pool_mgr gen_server 进程, 用来维护PoolID 和 EmysqlConnMgrProcessID 关联信息. 同时为了避免emysql_pool_mgr 的单点问题, 使用ets table 来分担emysql_pool_mgr gen_server 进程的负担.

optimize 的branch 为add_pool_mgr, 还未PR(等待小伙伴的测试反馈,欢迎 code review). 

cast 是丢过去不管,有返回自然收到,没有返回的话,对不起你自己看着办重试(看起来更流畅?)

编辑:小说 本文来源:简单Elixir游戏服设计

关键词: