说明:测试使用的版本checkout源自https://github.com/jkvor/emysql.git
这个版本在github上已经不再更新了
emysql 也是一个常用的erlang mysql数据库驱动。相比较erlang_mysql_driver,emysql的代码结构更加清晰。emysql的使用非常方便,先执行emysql:start、emysql:add_pool,然后调用emysql:fetch/execute就可以开始执行sql语句了。
网上emysql分析源码的版本众多,发现我下载下来的还跟大部分人下载的不一样,可能该版本修改了很多,我下载了当前最新版本。本来是想分析emysql源码的,测试下发现了几个bug,不知道是不是使用的方式不当。
下面就开始分析下emysql的源码和测试的过程。
(注:使用“业务进程”表示调用emysql:execute的进程)
emysql 启动简要分析
- 1 emysql的启动
emysql:start 以监督树进程的形式,启动了两个子进程,emysql_statements 和 emysql_conn_mgr。
init(_) ->
{ok, {{one_for_one, 10, 10}, [
{emysql_statements, {emysql_statements, start_link, []}, permanent, 5000, worker, [emysql_statements]},
{emysql_conn_mgr, {emysql_conn_mgr, start_link, []}, permanent, 5000, worker, [emysql_conn_mgr]}
]}}.
2 emysql_statements的启动
emysql_statements在启动时并没有做特殊操作,只是初始化了#state。这里emysql_statements的state里有两个元素statements和prepared,数据结构都是采用gb_trees。3 emysql_conn_mgr的启动
emysql_conn_mgr的state有两个元素pools 和 waiting , 如果app文件一开始配置了连接池则会在启动的时候添加,一般来说是[]。
init([]) ->
Pools = initialize_pools(),
Pools1 = [emysql_conn:open_connections(Pool) || Pool <- Pools],
{ok, #state{pools=Pools1}}.
emysql_conn_mgr这个进程类似于erlang_mysql_driver中的mysql_dispatcher,emysql_conn_mgr管理了多个连接池,连接池里放置了多个连接。另外emysql_conn_mgr还管理了waiting的pid,这些pid是等待连接的业务进程。
测试
- 1 测试一
简要分析后,就开始测试了,同一时间spawn了10万个进程执行select语句,主要目的是想测试下,在这种情况下主要的压力放在哪些进程上了。测试表明,刚开始6秒左右2万多的进程能够得到返回,但是在后面的时间就都是connection_wait_timeont报错了。
execute(PoolId, Query, Args, Timeout) when is_atom(PoolId) andalso (is_list(Query) orelse is_binary(Query)) andalso is_list(Args) andalso is_integer(Timeout) ->
Connection = emysql_conn_mgr:wait_for_connection(PoolId),
monitor_work(Connection, Timeout, {emysql_conn, execute, [Connection, Query, Args]});
wait_for_connection是一个阻塞的过程,就算我们的执行时传入的参数Timeout大于5000,execute真正的timeout时间也会受wait_for_connection影响。execute方法把Timeout参数传给了monitor_work,并没有传给wait_for_connection。monitor_work确实会在Timeout时间内返回,但是wait_for_connection就按自己定义的timeout时间了。
这也就是许多进程在5秒后就说到timeout报错的原因,因为wait_for_connection自己定义的timeout时间就是5秒,这里我们看下wait_for_connection的源码。
wait_for_connection(PoolId) when is_atom(PoolId) ->
%% try to lock a connection. if no connections are available then
%% wait to be notified of the next available connection
case lock_connection(PoolId) of
unavailable ->
gen_server:call(?MODULE, start_wait, infinity),
receive
{connection, Connection} -> Connection
after lock_timeout() -> %% 这里就是wait_for_connection自定义的timeout时间,一般来说为5秒
exit(connection_lock_timeout)
end;
Connection ->
Connection
end.
业务进程如果一开始lock_connection的时候没有获取到Connection,则会一直阻塞等待emysql_conn_mgr发来Connection。而阻塞了lock_timeout()的时间后还没收到连接,那么就真的exit了,所以就这个代码而言测试大量请求的情况下很容易受到timeout报错,业务进程会一直阻塞在等待emysql_conn_mgr返回连接的wait_for_connnection中。
而emysql_conn_mgr会在收到start_wait消息的时候,把业务进程的pid存入自己的state.waiting队列中。
上述测试中遇到的问题就是,state的waiting队列一直没有清空!很显然业务进程已经挂了,timeout报错了,但是emysql_conn_mgr中的waiting里一直没有把这些业务进程删除,这是为什么呢。
我们可以看下pass_connection_to_waiting_pid的方法,当一个业务进程已经使用完connection后
- 1 当前无waiting的业务进程,删除pool中locked的connection,并把connection添加到pool的avliable中。
- 2 当前的waiting里有等待执行的业务进程,这个就是我们上面说到的业务进程。该进程调用wait_for_connection时没有马上拿到连接,所以进入阻塞等待了。这种情况下就不把connection放回去了,直接给waiting队列中的进程了,但是很遗憾的情况是:erlang:process_info(Pid, current_function) 时->业务进程已经挂了,并没有在{current_function,{emysql_conn_mgr,wait_for_connection,1}}中。
按道理这个时候,我们应该把waiting里的pid该删除了,然而并没有pass_connection_to_waiting_pid直接返回了原来的State,所以导致State中的无效pid一直没有被删除。每次pass_connection的时候,都必须遍历之前waiting的pid。
这里修改成
pass_connection_to_waiting_pid(State#state{waiting=Waiting1}, Connection, Waiting1)就可以了。
pass_connection_to_waiting_pid(State, Connection, Waiting) ->
%% check if any processes are waiting for a connection
case queue:is_empty(Waiting) of
true ->
%% if no processes are waiting then unlock the connection
case find_pool(Connection#connection.pool_id, State#state.pools, []) of
{Pool, OtherPools} ->
%% find connection in locked tree
case gb_trees:lookup(Connection#connection.id, Pool#pool.locked) of
{value, Conn} ->
%% add it to the available queue and remove from locked tree
Pool1 = Pool#pool{
available = queue:in(Conn#connection{locked_at=undefined}, Pool#pool.available),
locked = gb_trees:delete_any(Connection#connection.id, Pool#pool.locked)
},
{ok, State#state{pools = [Pool1|OtherPools]}};
none ->
{{error, connection_not_found}, State}
end;
undefined ->
{{error, pool_not_found}, State}
end;
false ->
%% if the waiting queue is not empty then remove the head of
%% the queue and check if that process is still waiting
%% for a connection. If so, send the connection. Regardless,
%% update the queue in state once the head has been removed.
{{value, Pid}, Waiting1} = queue:out(Waiting),
case erlang:process_info(Pid, current_function) of
{current_function,{emysql_conn_mgr,wait_for_connection,1}} ->
erlang:send(Pid, {connection, Connection}),
{ok, State#state{waiting = Waiting1}};
_ ->
%% 这里是关键,State又一次原样返回了,导致State里的waiting一直没变
%% 应该改成 pass_connetion_to_waiting_pid(State#state{waiting=Waiting1}, Connection, Waiting1)
pass_connection_to_waiting_pid(State, Connection, Waiting1)
end
end.
- 2 测试二
鉴于上面测试的表现,修改了下代码,允许业务进程一直等待,直到查询完成。
wait_for_connection(PoolId) when is_atom(PoolId) ->
%% try to lock a connection. if no connections are available then
%% wait to be notified of the next available connection
case lock_connection(PoolId) of
unavailable ->
gen_server:call(?MODULE, start_wait, infinity),
receive
{connection, Connection} -> Connection
end;
Connection ->
Connection
end.
这里修改后,wait_for_connection就不会有timeout时间了。那么emysql:monitor_work里timeout时间才是真正的timeout时间。业务进程会阻塞在wait_for_connection中,然后获得连接,获得连接后,spawn子进程去使用connection执行sql。
但是这里又遇到了一个问题,gen_server:call(?MODULE, start_wait, infinity),这个消息发送的时候并没有告诉emysql_conn_mgr,该进程是等待哪个pool的连接的!不同的pool可能就是连接不同数据库的!为此我特意建了两个数据库,两个连接池,结果就会出现一个问题,就是emysql_conn_mgr在pass_connection_to_waiting_pid的时候,对返回的connection不加区分直接就传递给了waitng中的pid。
关于这个问题,修改的地方必须是一开始添waiting pid的时候,该pid加上pool的标志。也就是有多个队列,每个队列对应一个pool,当有空闲的connection出现时,查看connection的pool_id,然后获取相应的waiting queue的pid,将connetion发送给该pid。