poolboy——Erlang进程池

poolboy是Erlang进程池,应用广泛,代码短小精悍(400行不到)。PostgreSQL的Erlang客户端就用到了poolboy,其他DB库也有用,比如redis等。由于足够简洁,所以坑不少,这篇文章做了总结。任何进程池的目的都是很明确的——预先创建特定数目的进程,执行反复的一次性工作,随后接受直接提供服务。用Erlang实现进程池更是易如反掌,只需要按照OTP规范写一个监控树就行。

poolboy使用简单,官方README和例子说的很清楚。简要列一下工作流程:

poolboy:start/2(start_link/2) -> poolboy gen_server process
poolboy:init -> poolboy_sup -> N worker_process
poolboy:transaction -> select one worker_process from pool and execute function

下面详细看源码。

   Erlang/OTP, poolboy, Code

Erlang编译相关模块

compile 模块

compile 模块提供编译接口,主要是 file/2 和 forms/2,分别接受文件和Erlang抽象格式(Abstract Format)——Erlang项式解析树的标准表现形式。第二个参数是选项,大致可以分为几类: 测试编译结果、是否产生二进制数据、调试信息、Makefile条目生成、’P’/‘E’/’S’生成中间格式文件、错误和警告信息、宏定义等。具体内容看文档。

‘P’、’E’、’S’ 选项分别生成预处理和解析变换(基本检查、函数是否存在等)、代码转换(导入导出文件处理、宏替换、生成module_info函数)、汇编代码(.S文件即模块对应的Erlang汇编,可以用file和forms函数生成bin)文件。这也暗示了compile模块的几个过程,下面介绍相关的其他模块。

epp 模块

epp 模块解析Erlang源文件,并生成抽象格式。可以用open打开一个EPP句柄,并迭代处理文件,也可以用 parse_file 一次性生成。也提供文件编码相关的函数。

   Erlang/OTP, Code

Erlang和其他语言的交互

Erlang和其他语言(如C和Java)的交互手段一直是我很感兴趣的主题,周末看了下OTP文档,终于大致理清楚了思路。这里先简单总结四种交互手段,也是更进一步学习Erlang的开始。

端口

最简单的方式是调用Erlang模块的 open_port/2 ,创建一个端口。Erlang端口可以被认为是一个外部Erlang进程,交互手段是通过标准IO输入输出,对应C语言里的read和write函数。

-spec open_port(PortName, PortSettings) -> port() when
      PortName :: {spawn, Command :: string() | binary()} |
                  {spawn_driver, Command :: string() | binary()} |
                  {spawn_executable, FileName :: file:name() } |
                  {fd, In :: non_neg_integer(), Out :: non_neg_integer()},
      PortSettings :: [Opt],
      Opt :: {packet, N :: 1 | 2 | 4}
           | stream
           | {line, L :: non_neg_integer()}
           | {cd, Dir :: string() | binary()}
           | {env, Env :: [{Name :: string(), Val :: string() | false}]}
           | {args, [string() | binary()]}
           | {arg0, string() | binary()}
           | exit_status
           | use_stdio
           | nouse_stdio
           | stderr_to_stdout
           | in
           | out
           | binary
           | eof
       | {parallelism, Boolean :: boolean()}
       | hide.

   Erlang/OTP, C

Learn you some Erlang for great good!

《Erlang趣学指南》 ,国内最新引进翻译的Erlang书,实际上13年就已经写好了。虽然如此,依旧是市面上最新的相关书籍。“趣”真说不上,作者的搞笑有点刻意,他的图画的倒是很有趣。这本书比它看起来要难很多,言简意赅,涉及到Erlang的方方面面。例子也都很实际。作者把所有章节都放了出来,可以免费看。

花了两天时间抽空看完,记录一些容易忽略的小知识,也学习它的有用的例子。

   Erlang/OTP, Book, reading

HTTP请求处理cowboy_rest

cowboy_rest以REST方式,允许用户模块介入HTTP请求处理。看过nginx源码的同学都知道,nginx回调模块可以介入http请求处理,依靠请求过程中划分的十几个阶段,实现资源重定向、权限控制等。cowboy的做法略有不同,前一篇提到过中间件的概念。给middlewares设置实现了execute回调的模块,接收到请求头和数据包后介入处理,实现nginx类似功能,官方有个markdown的例子,回头分析。一般来说,我们关注正常请求,在用户回调模块中实现cowboy_rest模块的可选回调即可。

cowboy_handler作为请求处理的最后一环,根据路由规则回调用户模块,如果返回{cowboy_rest, Req, State},就会调用cowboy_rest:upgrade/6:

execute(Req, Env=#{handler := Handler, handler_opts := HandlerOpts}) ->
	try Handler:init(Req, HandlerOpts) of
		...
		{Mod, Req2, State} ->
			Mod:upgrade(Req2, Env, Handler, State, infinity, run);
		...

   Erlang/OTP, code, cowboy

Erlang HTTP服务器cowboy

cowboy是基于ranch的http服务器框架,提供用户自定义路由、REST接口等便利功能。由于ranch是很完善的TCP池,所以在此之上的cowboy代码很容易支持http/https。虽说如此,http毕竟是很复杂的协议,好多细节不参考RFC根本搞不清楚。借此机会详细了解如何实现完整的HTTP服务器。首先了解cowboy完整的流程以及使用方法。

最简单的例子:

start(_Type, _Args) ->
	Dispatch = cowboy_router:compile([
		{'_', [
			{"/", toppage_handler, []}
		]}
	]),
	{ok, _} = cowboy:start_clear(http, 100, [{port, 8080}], #{
		env => #{dispatch => Dispatch}
	}),

   Erlang/OTP, code, cowboy

tcp连接池ranch

ranch 是Erlang/OTP实现的 TCP 连接池,我在看cowboy源码的时候意外发现了这个组件。让人欣喜的是它本身就非常强大,只需要短短几行代码,就可以实现强大的tcp服务器功能,而socket管理的细节都被ranch封装起来。

运行anchapp:start(,_)启动服务,然后就可以使用自己实现的服务。晚上看了一下它的源码,非常短,但是很成熟,是学习的好例子。

ranch的启动大致如下流程:

ranch_app:start(_,_) %% application
-> ranch_sup:start_link() %% supervisor 
-> ranch_sup:init() %% supervisor callback
-> ets:new(ranch_server, ...) %% one_for_one, ranch_server
-> ranch_server:start_link() %% gen_server

ranch_server提供tcp连接池管理,其它模块通过和ranch_server交互,维护自己的连接信息、socket选项等。

   Erlang/OTP, code, ranch

supervisor源码阅读

supervisor是监控树中最重要的组成部分,负责启动、停止和监控子进程,异常时重启等。supervisor模块应该尽可能简单,保持功能单一——负责监控。supervisor模块本身基于gen_server,所以它实现了gen_server的六个主要回调函数,是一个gen_server进程。通过阅读supervisor模块源码,了解实现supervisor回调的过程以及实现进程监控器的步骤。

先写一个简单的worker_sup.erl,实现了supervisor行为模式,配合worker.erl模块使用:

%% supervisor
-module(worker_sup).

-behaviour(supervisor).

-export([start_link/0]).
-export([init/1]).

start_link() ->
    supervisor:start_link(worker_sup, []).

init(_Args) ->
    SupFlags = #{strategy => one_for_one, intensity => 1, period => 5},
    ChildSpecs = [#{id => worker, start => {worker, start_link, []}, restart => permanent, shutdown => brutal_kill, type => worker, modules => [worker]}],
    {ok, {SupFlags, ChildSpecs}}.

   Erlang/OTP, code, supervisor

gen_server源码阅读

gen_server是Erlang/OTP的通用服务器框架,使用最为广泛。源代码很少,只有几百行。抽空详细梳理了一遍流程,以简单的gen_server应用worker为例。

先看我们自己的worker.erl文件,根据OTP设计规范,既是回调模块,又是用户接口。对外导出 start_link/0,1 , query/1 , set/1 , show/1 , *stop/1*,同时实现gen_server行为模式的6个必要的回调函数:

   Erlang/OTP, code, gen_server