Erlang/OTP 两三事

gen_server

提纲

  • gen_server 概述

  • gen_server 实例

  • gen_server 源码

  • gen_server 陷阱

  • gen_server 技巧

gen_server 概述

gen_server 是Erlang 体系中最常用的behavior

& 使用module:

rpc net_kernel file_server etc.

& 使用场景:

1, gen_server behavior 通常用在负责资源分配的进程, client process 请求server process将会获得相应的资源;

2, 作为long-lived process 角色用于维护特定的资源, 如DB connections, ets table etc;

3, 其他: 并发控制, 锁.

gen_server 概述

server 和client 两种角色:

1, server 负责维护资源和提供接口

2, client 通过server 提供的接口访问资源

 

notice:

1, server 和 client 都属于Erlang process

2, 接口调用通过Erlang message 实现

  

gen_server 概述

提纲

  • gen_server 概述 (X)

  • gen_server 实例

  • gen_server 源码

  • gen_server 陷阱

  • gen_server 技巧

gen_server 实例

NB! 文档

  gen_server:start_link 接口定义

  init/1 callback 定义

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

%% callback
init([]) ->
    {ok, #state{}, ?HIBERNATE_TIMEOUT}.

gen_server 实例

NB! 文档

  gen_server:call/2,3 接口定义

  handle_call 几种返回形式

%% interface 
%% gen_server client need a new ets table
new_ets() ->
	gen_server:call(?SERVER, create_ets_table).
	
%% callback
%% gen_server server process will execute new op and keep the ets table alive
handle_call(create_ets_table, _From, State) ->
	EtsTable = ets:new(ets_table, [public]),
	{reply, EtsTable, State, ?HIBERNATE_TIMEOUT};

gen_server 实例

%% interface
%% gen_server client is very lazy, would not execute logic by itself
check_method_valid(MethodName) ->
	gen_server:call(?SERVER, {if_method_valid, MethodName}).

%% callback
%% then gen_server server help client
handle_call({if_method_valid, MethodName}, _From, State) ->
	case MethodName of
		'call' -> {reply, true, State, ?HIBERNATE_TIMEOUT};
		'cast' -> {reply, true, State, ?HIBERNATE_TIMEOUT};
		_      -> {reply, false, State, ?HIBERNATE_TIMEOUT}
	end;

gen_server 实例

%% interface
%% gen_server client has no resource which server had, so has to call server
push_msg(Target, Msg) when erlang:is_pid(Target) ->
	gen_server:cast(?SERVER, {push_msg, Target, Msg}).

%% callback
%% gen_server server do
handle_cast({push_msg, Target, Msg}, State) ->
	erlang:send(Target, Msg),
	{noreply, State, ?HIBERNATE_TIMEOUT};

NB! 文档

  gen_server:cast/2 接口定义

  handle_cast 几种返回形式

提纲

  • gen_server 概述 (X)

  • gen_server 实例 (X)

  • gen_server 源码

  • gen_server 陷阱

  • gen_server 技巧

gen_server 源码

仅对主要 code 做概要性阐述

若有问题, 可详细交流

 

主要蓝本:

http://www.cnblogs.com/--00/tag/gen_server/

gen_server 源码

gen module 是gen_server, gen_fsm module 的基础

 

主要实现了以下函数:

start/5 用于spawn 匿名gen_server 进程

start/6 用于spawn 命名gen_server 进程

call/3, call/4 用来处理gen_server:call/N 请求

reply/2 函数

gen_server 源码

spec of `start` function from gen module:

 

start(GenMod, LinkP, Name, Mod, Args, Options)
    GenMod  :: gen_server | gen_fsm
    LinkP   :: nolink     | link
    Name    :: {local, atom()} | {global, term()} | {via, atom(), term()} %% 指定的name
    Mod     :: atom() %% 就是使用了gen_server behaviour 的模块名
    Args    :: term() %% Mod 模块 init func 的初始参数
    Options :: [{timeout, Timeout} | {debug, [Flag]} | {spawn_opt, OptionList}]
        %% 这个参数是用来指定创建进程的参数
    Flag    :: trace | log | {logfile, File} | statistics | debug

gen_server 源码

gen_server 的主体module 主要包括:

  • start
  • main loop

其中start 是由gen module 下的init_it 函数调用 gen_server:init_it/6 触发start 的

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

gen_server 源码

gen_server 的主体module start :

gen_server 源码

 执行gen_server:init_it/6 之后, 会进入loop :

gen_server 源码

 触发gen_server terminate 的场景 :

gen_server 源码

long-lived process

优点:

1, 便于维护资源

2, ...

 

缺点:

1, 资源回收

2, 单进程瓶颈

In general, Erlang processes are expected to short-lived and have small amounts of live data. When there is not enough free memory in the heap for a process, it is garbage collected, and if less memory can be freed than required it grows. Each process’ heap is garbage collected independently, when a process exits, its memory is simply reclaimed. 

gen_server 源码

hibernate

主要用于long-lived 进程,最典型的就是gen_server 进程了.

 

short-lived 进程资源会随其退出轻松被回收.

通过整理进程的stack,回收进程的heap 来达到回收内存节省资源的效果

  • 用于某进程可能会在未来短期内空闲时
  • hibernate 会清空进程的stack 然后进程GC

gen_server 源码

hibernate on mochiweb

This sounds reasonable - let's try hibernating after every message and see what happens.

gen_server 源码

hibernate 缺点

Note that emptying the call stack means that any surrounding catch is removed and has to be re-inserted after hibernation. 

需要CPU, 可能带来不必要的empty-reinsert 操作.

gen_server 源码

hibernate on ejabberd

Ejabberd 对hibernate 的使用比较谨慎, 只有在进程未收到任何信息一段时间后, 才使用hibernate .

handle_call(create_ets_table, _From, State) ->
    EtsTable = ets:new(ets_table, [public]),
    {reply, EtsTable, State, ?HIBERNATE_TIMEOUT};
handle_info(timeout, State) ->
    proc_lib:hibernate(gen_server, enter_loop,
               [?MODULE, [], State]),
    {noreply, State, ?HIBERNATE_TIMEOUT};

gen_server 源码

hibernate on ejabberd

实现原理:

handle_msg({'$gen_call', From, Msg}, Parent, Name, State, Mod) ->
    Result = try_handle_call(Mod, Msg, From, State),
    case Result of
        %% ...
        {ok, {reply, Reply, NState, Time1}} ->
            reply(From, Reply),
            loop(Parent, Name, NState, Mod, Time1, []);
        %% ...
        {ok, {noreply, NState, Time1}} ->
            loop(Parent, Name, NState, Mod, Time1, [])
        %% ...        
    end;
loop(Parent, Name, State, Mod, Time, Debug) ->
    Msg = receive
          Input ->
            Input
      after Time ->
          timeout
      end,
    decode_msg(Msg, Parent, Name, State, Mod, Time, Debug, false).

gen_server 源码

hibernate on RabbitMQ

使用更为彻底, 更加谨慎

 

直接在自己实现的gen_server 版本中(gen_server2) 强制使用hibernate, 且与ejabberd的使用方式类似, 都是在进程未收到任何信息一段时间后, 才使用hibernate .

init(Q) ->
    process_flag(trap_exit, true),
    ?store_proc_name(Q#amqqueue.name),
    {ok, init_state(Q#amqqueue{pid = self()}), hibernate,
     {backoff, ?HIBERNATE_AFTER_MIN, ?HIBERNATE_AFTER_MIN, ?DESIRED_HIBERNATE},
    ?MODULE}.

提纲

  • gen_server 概述 (X)

  • gen_server 实例 (X)

  • gen_server 源码 (X)

  • gen_server 陷阱

  • gen_server 技巧

gen_server 陷阱

  • 资源不易回收 (X)
  • 单进程瓶颈

每个Erlang Process 默认获得的资源是相同

gen_server 陷阱

  • 单进程瓶颈  - noblock call

gen_server 陷阱

  • 单进程瓶颈  - noblock call  - rpc
handle_call_call(Mod, Fun, Args, Gleader, To, S) ->
    RpcServer = self(),
    %% Spawn not to block the rpc server.
    {Caller,_} =
        erlang:spawn_monitor(
          fun () ->
                  set_group_leader(Gleader),
                  Reply = 
                      %% in case some sucker rex'es 
                      %% something that throws
                      case catch apply(Mod, Fun, Args) of
                          {'EXIT', _} = Exit ->
                              {badrpc, Exit};
                          Result ->
                              Result
                      end,
                  RpcServer ! {self(), {reply, Reply}}
          end),
    {noreply, gb_trees:insert(Caller, To, S)}.

handle client call via spawn a new process for temporary

gen_server 陷阱

  • 单进程瓶颈  - noblock call  - rpc
handle_info({Caller, {reply, Reply}}, S) ->
    case gb_trees:lookup(Caller, S) of
        {value, To} ->
            receive
                {'DOWN', _, process, Caller, _} -> 
                    gen_server:reply(To, Reply),
                    {noreply, gb_trees:delete(Caller, S)}
            end;
        none ->
            {noreply, S}
    end;

return the result to client by `gen_server:reply/2`

gen_server 陷阱

  • 单进程瓶颈  - noblock call  - net_kernel
handle_call({spawn_opt,M,F,A,O,L,Gleader},{From,Tag},State) when is_pid(From) ->
    do_spawn([L,{From,Tag},M,F,A,Gleader],O,State);

do_spawn(SpawnFuncArgs, SpawnOpts, State) ->
    [_,From|_] = SpawnFuncArgs,
    case catch spawn_opt(?MODULE, spawn_func, SpawnFuncArgs, SpawnOpts) of
      {'EXIT', {Reason,_}} ->
            async_reply({reply, {'EXIT', {Reason,[]}}, State}, From);
      {'EXIT', Reason} ->
          async_reply({reply, {'EXIT', {Reason,[]}}, State}, From);
      _ ->
          {noreply,State}
    end.

spawn_func(link,{From,Tag},M,F,A,Gleader) ->
    link(From),
    gen_server:reply({From,Tag},self()),  %% ahhh
    group_leader(Gleader,self()),
    apply(M,F,A);
spawn_func(_,{From,Tag},M,F,A,Gleader) ->
    gen_server:reply({From,Tag},self()),  %% ahhh
    group_leader(Gleader,self()),
    apply(M,F,A).

gen_server 陷阱

  • 单进程瓶颈  - pool

提纲

  • gen_server 概述 (X)

  • gen_server 实例 (X)

  • gen_server 源码 (X)

  • gen_server 陷阱 (X)

  • gen_server 技巧

gen_server 技巧

  • 使用system msg debug
  • 预分配合适的资源
  • 适当干预资源回收
  • 使用task process
  • gen_server pool

提纲

  • gen_server 概述 (X)

  • gen_server 实例 (X)

  • gen_server 源码 (X)

  • gen_server 陷阱 (X)

  • gen_server 技巧 (X)

参考

Q&A

Erlang/OTP-gen_server

By redink

Erlang/OTP-gen_server

  • 6,182