前言
将使用通用服务器(gen_sever)、FSM(gen_fsm)、事件处理器(gen_event)以及监督者(supervisor)构建应用进行实战
进程池
进程池背后的思想就是用一种通用的方式去管理和限制系统中运行的资源使用。
通过进程池,我们可以限制同时运行的进程数量。当运行的工作者进程数量达到上限时,进程池还可以把任务放到队列中。只要有进程资源被释放,排队的任务就能获得运行,否则任务只能阻塞,用户什么也做不了。
为什么要使用进程池呢?有如下几种可能。
限制并发链接
:进程池可用于限制一个服务器同时处理的连接数量。通过控制进程池中的工作者进程数量,可以确保系统资源不被过度占用,并防止过多的并发连接导致性能下降。限制打开的文件数量
:进程池还可用于限制应用程序可以打开的文件数量。通过将每个任务分配给池中的工作者进程,可以确保打开的文件数量不超过预定限制,从而保护系统免受资源耗尽的风险。分配资源的优先级
:通过为不同子系统分配不同数量的进程资源,可以为发布中的各个子系统分配优先级。例如,可能希望为处理客户请求的进程分配更多资源,而为生成管理报表的进程分配较少资源,以满足不同子系统的需求和优先级。稳定性和负载均衡
:通过将任务排队并在空闲进程可用时立即执行,进程池可以在突发的高负载情况下保持应用程序的稳定性。当系统资源不足以同时处理所有任务时,进程池将任务放入队列,并在有空闲进程时按照排队顺序逐个执行任务,确保任务的有序执行。
进程池监督树
我们该如何组织这些进程池呢?关于此,有两种观点。一种观点是自底向上设计(先编写单个组件,然后按照要求把它们组装起来)
。另外一个观点是自顶向下设计(先假想所有的组件已经就绪,做完高层设计后再去实际构建这些组件)
。两种方法是等效的,具体使用哪种方法,取决于应用场景和个人风格。为了让每项工作都好理解,本例将采用自顶向下的方法。
进程池需要作为整体启动,包含多个进程池和每个进程池中的多个工作者进程。为了实现这个需求,每个进程池需要一个服务器来维护工作者进程的计数和任务队列。但是,谁来监督工作者进程呢?
可使用一个独立的监督者来监督所有的进程池。这个监督者由池服务器和工作者进程监督者组成。池服务器知道工作者进程监督者的存在,并可以请求增加新的工作者进程。为了动态地增加子进程,建议使用simple_one_for_one类型的监督者。
然而,如果某个进程池或服务器在短时间内重启次数过多,会导致其他进程池被终止。为了解决这个问题,可在监督者层再增加一层监督者,处理同时存在多个进程池的情况。
这样的设计使得进程池相互独立,并提供了工作者进程的监督和重启机制,同时解决了重启次数过多的问题,实现了良好的错误隔离性。
实现监督者
它由几个函数构成:start_link/0,用来启动整个应用;stop/0,用来停止应用;start_pool/3,创建一个具体的进程池,以及stop_pool/1,删除一个进程池。init/1,它是监督者行为唯一要求的回调函数。
-module(ppool_supersup).
-behavior(supervisor).
-export([start_link/0, stop/0, start_pool/3, stop_pool/1]).
-export([init/1]).
start_link() ->
supervisor:start_link({local, ppool}, ?MODULE, []).
init([]) ->
MaxRestart = 6,
MaxTime = 3600,
{ok, {{one_for_one, MaxRestart, MaxTime}, []}}.
start_pool(Name, Limit, MFA) ->
ChildSpec = {Name,
{ppool_sup, start_link, [Name, Limit, MFA]},
permanent, 10500, supervisor, [ppool_sup]},
supervisor:start_child(ppool, ChildSpec).
stop_pool(Name) ->
supervisor:terminate_child(ppool, Name),
supervisor:delete_child(ppool, Name).
%% 从技术上讲,不太容易杀死一个监督者
%% 我们采取暴力手段!
stop() ->
case whereis(ppool) of
P when is_pid(P) ->
exit(P, kill);
_ -> ok
end.
进程池服务器把工作者进程的监督者动态地增加到ppool_sup中
-module(ppool_sup).
-export([start_link/3, init/1]).
-behavior(supervisor).
start_link(Name, Limit, MFA) ->
supervisor:start_link(?MODULE, {Name, Limit, MFA}).
init({Name, Limit, MFA}) ->
MaxRestart = 1,
MaxTime = 3600,
{ok, {{one_for_all, MaxRestart, MaxTime},
[{serv,
{ppool_serv, start_link, [Name, Limit, self(), MFA]},
permanent,
5000, % 关闭时间
worker,
[ppool_serv]}]}}.
编写应用中最后一个监督者ppool_worker_sup,它负责管理监督所有的工作者进程。
-module(ppool_worker_sup).
-export([start_link/1, init/1]).
-behavior(supervisor).
start_link(MFA = {_, _, _}) ->
supervisor:start_link(?MODULE, MFA).
init({M, F, A}) ->
MaxRestart = 5,
MaxTime = 3600,
{ok, {{simple_one_for_one, MaxRestart, MaxTime},
[{ppool_worker,
{M, F, A},
temporary, 5000, worker, [M]}]}}.
进程池服务器
下面列出服务器必须支持的操作
- 在进程池中运行一个任务,如果池中的进程个数已满,就要给出无法运行的指示。
- 在进程池中运行一个任务,当进程池尚有空间时立即运行;否则,让调用者进程保持等待,并将任务入队,直到任务可以运行。
- 以异步方式在进程池中运行一个任务,尽量立即运行;如果池中没有空间,就让任务入队,以后运行。
-module(ppool_serv).
-behavior(gen_server).
-export([start/4, start_link/4, run/2, sync_queue/2, async_queue/2, stop/1]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
code_change/3, terminate/2]).
-record(state, {limit = 0,
sup,
refs,
queue = queue:new()}).
%% 我们的监督者朋友是动态启动的!
-define(SPEC(MFA),
{worker_sup,
{ppool_worker_sup, start_link, [MFA]},
permanent,
10000,
supervisor,
[ppool_worker_sup]}).
init({Limit, MFA, Sup}) ->
%% 我们要在此处得到工作者进程监督者的pid,
%% 但是,唉,这需要调用监督者,而它正在等待我们的回应
self() ! {start_worker_supervisor, Sup, MFA},
{ok, #state{limit = Limit, refs = gb_sets:empty()}}.
handle_info({'DOWN', Ref, process, _Pid, _}, S = #state{limit = L, sup = Sup, refs = Refs}) ->
io:format("received down msg~n"),
case gb_sets:is_element(Ref, Refs) of
true ->
handle_down_worker(Ref, S);
false -> %% 不处理这种情况
{noreply, S}
end;
handle_info({start_worker_supervisor, Sup, MFA}, S = #state{}) ->
{ok, Pid} = supervisor:start_child(Sup, ?SPEC(MFA)),
{noreply, S#state{sup = Pid}};
handle_info(Msg, State) ->
io:format("Unknown msg: ~p~n", [Msg]),
{noreply, State}.
handle_call({run, Args}, _From, S = #state{limit = N, sup = Sup, refs = R}) when N > 0 ->
{ok, Pid} = supervisor:start_child(Sup, Args),
Ref = erlang:monitor(process, Pid),
{reply, {ok, Pid}, S#state{limit = N - 1, refs = gb_sets:add(Ref, R)}};
handle_call({run, _Args}, _From, S = #state{limit = N}) when N =< 0 ->
{reply, noalloc, S};
handle_call({sync, Args}, _From, S = #state{limit = N, sup = Sup, refs = R}) when N > 0 ->
{ok, Pid} = supervisor:start_child(Sup, Args),
Ref = erlang:monitor(process, Pid),
{reply, {ok, Pid}, S#state{limit = N - 1, refs = gb_sets:add(Ref, R)}};
handle_call({sync, Args}, From, S = #state{queue = Q}) ->
{noreply, S#state{queue = queue:in({From, Args}, Q)}};
handle_call(stop, _From, State) ->
{stop, normal, ok, State};
handle_call(_Msg, _From, State) ->
{noreply, State}.
handle_cast({async, Args}, S = #state{limit = N, sup = Sup, refs = R}) when N > 0 ->
{ok, Pid} = supervisor:start_child(Sup, Args),
Ref = erlang:monitor(process, Pid),
{noreply, S#state{limit = N - 1, refs = gb_sets:add(Ref, R)}};
handle_cast({async, Args}, S = #state{limit = N, queue = Q}) when N =< 0 ->
{noreply, S#state{queue = queue:in(Args, Q)}};
%% 下面这个无需解释!
handle_cast(_Msg, State) ->
{noreply, State}.
handle_down_worker(Ref, S = #state{limit = L, sup = Sup, refs = Refs}) ->
case queue:out(S#state.queue) of
{{value, {From, Args}}, Q} ->
{ok, Pid} = supervisor:start_child(Sup, Args),
NewRef = erlang:monitor(process, Pid),
NewRefs = gb_sets:insert(NewRef, gb_sets:delete(Ref, Refs)),
gen_server:reply(From, {ok, Pid}),
{noreply, S#state{refs = NewRefs, queue = Q}};
{{value, Args}, Q} ->
{ok, Pid} = supervisor:start_child(Sup, Args),
NewRef = erlang:monitor(process, Pid),
NewRefs = gb_sets:insert(NewRef, gb_sets:delete(Ref, Refs)),
{noreply, S#state{refs = NewRefs, queue = Q}};
{empty, _} ->
{noreply, S#state{limit = L + 1, refs = gb_sets:delete(Ref, Refs)}}
end.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
terminate(_Reason, _State) ->
ok.
start(Name, Limit, Sup, MFA) when is_atom(Name), is_integer(Limit) ->
gen_server:start({local, Name}, ?MODULE, {Limit, MFA, Sup}, []).
start_link(Name, Limit, Sup, MFA) when is_atom(Name), is_integer(Limit) ->
gen_server:start_link({local, Name}, ?MODULE, {Limit, MFA, Sup}, []).
run(Name, Args) ->
gen_server:call(Name, {run, Args}).
sync_queue(Name, Args) ->
gen_server:call(Name, {sync, Args}, infinity).
async_queue(Name, Args) ->
gen_server:cast(Name, {async, Args}).
stop(Name) ->
gen_server:call(Name, stop).
%%% 进程池API模块
-module(ppool).
-export([start_link/0, stop/0, start_pool/3,
run/2, sync_queue/2, async_queue/2, stop_pool/1]).
start_link() ->
ppool_supersup:start_link().
stop() ->
ppool_supersup:stop().
start_pool(Name, Limit, {M, F, A}) ->
ppool_supersup:start_pool(Name, Limit, {M, F, A}).
stop_pool(Name) ->
ppool_supersup:stop_pool(Name).
run(Name, Args) ->
ppool_serv:run(Name, Args).
async_queue(Name, Args) ->
ppool_serv:async_queue(Name, Args).
sync_queue(Name, Args) ->
ppool_serv:sync_queue(Name, Args).
实现工作者
每个任务创建一个工作者
-module(ppool_nagger).
-behavior(gen_server).
-export([start_link/4, stop/1]).
-export([init/1, handle_call/3, handle_cast/2,
handle_info/2, code_change/3, terminate/2]).
start_link(Task, Delay, Max, SendTo) ->
gen_server:start_link(?MODULE, {Task, Delay, Max, SendTo}, []).
stop(Pid) ->
gen_server:call(Pid, stop).
init({Task, Delay, Max, SendTo}) ->
{ok, {Task, Delay, Max, SendTo}, Delay}.
%%% OTP 回调函数
handle_call(stop, _From, State) ->
{stop, normal, ok, State};
handle_call(_Msg, _From, State) ->
{noreply, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(timeout, {Task, Delay, Max, SendTo}) ->
SendTo ! {self(), Task},
if Max =:= infinity ->
{noreply, {Task, Delay, Max, SendTo}, Delay};
Max =< 1 ->
{stop, normal, {Task, Delay, 0, SendTo}};
Max > 1 ->
{noreply, {Task, Delay, Max - 1, SendTo}, Delay}
end.
%% 不要使用下面的handle_info子句:
%% 如果这个子句执行了,计时器会被取消,进程也就基本成为了僵尸
%% 此时,崩溃是最好的选择。
%% handle_info(_Msg, State) ->
%% {noreply, State}.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
terminate(_Reason, _State) -> ok.
测试
10> ppool:start_link().
{ok,<0.93.0>}
11> ppool:start_pool(nagger, 2, {ppool_nagger, start_link, []}).
{ok,<0.95.0>}
12> ppool:run(nagger, ["finish the chapter!", 10000, 10, self()]).
{ok,<0.99.0>}
13> ppool:run(nagger, ["Watch a good movie", 10000, 10, self()]).
{ok,<0.101.0>}
14> flush().
Shell got {<0.99.0>,"finish the chapter!"}
Shell got {<0.99.0>,"finish the chapter!"}
Shell got {<0.101.0>,"Watch a good movie"}
Shell got {<0.99.0>,"finish the chapter!"}
Shell got {<0.101.0>,"Watch a good movie"}
ok
15> ppool:run(nagger, ["clean up a bit", 10000, 10, self()]).
noalloc
16> flush().
Shell got {<0.99.0>,"finish the chapter!"}
Shell got {<0.101.0>,"Watch a good movie"}
Shell got {<0.99.0>,"finish the chapter!"}
Shell got {<0.101.0>,"Watch a good movie"}
Shell got {<0.99.0>,"finish the chapter!"}
Shell got {<0.101.0>,"Watch a good movie"}
Shell got {<0.99.0>,"finish the chapter!"}
Shell got {<0.101.0>,"Watch a good movie"}
ok
对于同步不入队的任务运行,一切正常。启动了进程池,增加了任务,消息也被发送到了正确的目的地。当我们试图运行的任务数超过允许上限时,被拒绝了。
现在来试验一下任务队列的功能(异步调用)0
17> ppool:async_queue(nagger, ["Pay the bills", 30000, 1, self()]).
ok
18> ppool:async_queue(nagger, ["Take a shower", 30000, 1, self()]).
ok
19> ppool:async_queue(nagger, ["Plant a tree", 30000, 1, self()]).
ok
`等待一段时间`
20> received down msg
20> received down msg
20> received down msg
20> flush().
Shell got {<0.99.0>,"finish the chapter!"}
Shell got {<0.101.0>,"Watch a good movie"}
Shell got {<0.99.0>,"finish the chapter!"}
...
Shell got {<0.101.0>,"Watch a good movie"}
Shell got {<0.106.0>,"Pay the bills"}
测试同步队列的行为
22> ppool:sync_queue(nagger, ["Pet a dog", 20000, 1, self()]).
{ok,<0.114.0>}
23> ppool:sync_queue(nagger, ["Make some noise", 20000, 1, self()]).
{ok,<0.116.0>}
24> received down msg
24> ppool:sync_queue(nagger, ["Chase a tornado", 20000, 1, self()]).
{ok,<0.118.0>}
25> received down msg
25> received down msg
25> received down msg
25> flush().
Shell got {<0.114.0>,"Pet a dog"}
Shell got {<0.116.0>,"Make some noise"}
Shell got {<0.118.0>,"Chase a tornado"}
ok
正常
现在关闭
27> ppool:stop_pool(nagger).
ok
28> ppool:stop().
** exception exit: killed