emysql 源码阅读

本文详细分析了emysql的源码启动过程,包括emysql_conn_mgr和emysql_statements的启动。测试中发现,当并发执行大量SQL时,由于wait_for_connection的timeout问题,可能导致业务进程阻塞并报错。同时,emysql_conn_mgr的waiting队列中可能存在无效的pid,未被正确清理。此外,测试还揭示了gen_server:call(?MODULE, start_wait, infinity)消息传递时未指定连接池的问题。为解决这些问题,提出了相应的代码修改建议。" 45166169,4999653,单片机控制LED灯左移右移实现,"['单片机', '汇编', 'C语言', '控制', '流水灯']

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

说明:测试使用的版本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。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值