erlang_mysql_driver源码阅读笔记
最近项目里发现了一个比较严重的bug,线上服务器游戏服创建了大量的mysql连接没有释放,轻松达到了设定的连接数上限,因为使用了第三方的驱动erlang_mysql_driver,排查的过程中就研究了下驱动的源码。
结构
代码比较简单,四个文件组成:
mysql.erl:唯一一个behavior为gen_server的模块,主要封装了一些外部调用的api(比如执行sql语句)以及管理连接池。
mysql_conn.erl:负责处理一条与mysql数据库的连接,可单独使用,也可通过mysql模块管理多条连接,甚至可以连接到不同的服务器。sql语句的执行和返回结果也是由这个模块处理的。
mysql_recv.erl:负责实际与mysql的tcp连接,将结果返回给父进程(mysql_conn).
mysql_auth.erl:提供给mysql_conn模块调用的接口,认证相关,可以忽略。
mysql_dispatcher进程启动与初始化
启动
mysql模块提供两种启动方式:start_link和start,实际上都是由start1函数处理,区别就是start1最终调用的是gen_server:start_link还是gen_server:start(前者可以启动在supervisor监督树下,后者则是启动独立的进程)。
要注意的是,mysql启动的进程别名不是mysql而是mysql_dispatcher.
初始化
init([PoolId, Host, Port, User, Password, Database, LogFun, Encoding]) ->
LogFun1 = if LogFun == undefined -> fun log/4; true -> LogFun end,
case mysql_conn:start(Host, Port, User, Password, Database, LogFun1,
Encoding, PoolId) of
{
ok, ConnPid} ->
Conn = new_conn(PoolId, ConnPid, true, Host, Port, User, Password,
Database, Encoding),
State = #state{
log_fun = LogFun1},
{
ok, add_conn(Conn, State)};
{
error, _Reason} ->
?Log(LogFun1, error,
"failed starting first MySQL connection handler, "
"exiting"),
C = #conn{
pool_id = PoolId,
reconnect = true,
host = Host,
port = Port,
user = User,
password = Password,
database = Database,
encoding = Encoding},
start_reconnect(C, LogFun),
{
ok, #state{
log_fun = LogFun1}}
end.
init函数主要做了两件事:
一是启动一个mysql_conn进程,如果正常启动就继续,如果启动失败则调用msql:start_reconnect/2进行重连。
二是如果启动成功,调用mysql:add_conn/2将启动的连接进程信息(比如pid,poolId,端口等)保存到#state{}中。
add_conn(Conn, State) ->
Pid = Conn#conn.pid,
erlang:monitor(process, Conn#conn.pid),
PoolId = Conn#conn.pool_id,
ConnPools = State#state.conn_pools,
NewPool =
case gb_trees:lookup(PoolId, ConnPools) of
none ->
{
[Conn],[]};
{
value, {
Unused, Used}} ->
{
[Conn | Unused], Used}
end,
State#state{
conn_pools =
gb_trees:enter(PoolId, NewPool,
ConnPools),
pids_pools = gb_trees:enter(Pid, PoolId,
State#state.pids_pools)}.
这个add_conn函数值得仔细关注下:
- 当调用add_conn时,mysql_dispatcher首先会调用erlang:monitor建立一个进程的单向监控,目的是当mysql_conn异常挂掉时监控进程mysql_dispatcher收到来自于子进程的‘DOWN’消息.
- mysql_dispatcher进程维护两个数据结构——ConnPools连接池和PidPools进程池。使用gb_trees二叉平衡树存储key-value类型的数据结构:
a. ConnPools:key为PoolId::term(连接池id),value为{Unused::Lists, Used::Lists}元组,其中Unused为未使用的连接列表,Used为使用中的连接列表。
b. PidPools:key为Pid::pid(进程id),value为PoolId::term(连接池id)
mysql_dispather的重连机制
当mysql:init/1启动mysql:start失败或者当收到‘DOWN’消息且#conn.reconnect字段为true时,首先将旧的连接通过mysql:remove_conn/2移除,然后触发调用mysql:start_reconnect/2函数进行重连。
start_reconnect(Conn, LogFun) ->
Pid = spawn(fun () ->
process_flag(trap_exit,