pool模型
探究erlang_mysql_driver对同一时刻大量请求的支持
mysql:fetch 和 mysql_conn
今天看到网络上的一篇文章说,erlang_mysql_driver的连接池实际上是没有意义的。
大概意思是,当我们的业务进程调用 mysql:fetch 去执行sql语句时,mysql:fetch会call一条消息到mysql_dispatcher进程中。所以当我们在同一时刻多进程大量调用mysql:fetch时,mysql_dispatcher会收到多条call消息,并且这些消息会阻塞在mysql_dispatcher的消息队列里。这些消息会按到达的顺序逐个处理。那么后面调用fetch的进程就必须等待,每个请求需要等待上一个请求执行结束后才能开始执行。所以虽然mysql_dispatcher背后有多个连接进程(mysql_conn)但是他们并没有起到并发使用的作用。
乍一看,好像挺有道理的。但是我又觉得不对劲,毕竟作者不至于挖个这么大的坑吧,于是测试了一下。
同一时刻,spawn 10万个进程,每个进程都调用mysql:fetch进行数据库查询。
按上面的说法,那么这个时候应该会有大量的消息阻塞在mysql_dispatcher中,测试发现mysql_dispatcher消息队列里的这些消息很快就处理完了。也就是mysql_dispatcher很快就把这些消息分发给了mysql_conn,这里我建立9个mysql_conn进程。然后大部分的消息(10万个请求)被堆积在9个mysql_conn进程的消息队列中,而且每个mysql_conn收到的消息是平均的。
证明我们的mysql_dispatcher还是能够顺利完成任务的,而且可以看出 mysql_dispatcher处理这些消息肯定只有简单的分发消息,没有涉及数据io过程的。
gen_server:call gen_server:reply
那么上面讲到mysql:fetch不是调用了gen_server:call吗,而gen_server:call确实是会阻塞的。但是这里阻塞的是调用者的进程,也就是我spawn出来的那些进程。而mysql_dispatcher对于这些消息的处理是非常快的,没有涉及到数据的io过程。
fetch_queries(PoolId, From, State, QueryList) ->
with_next_conn(
PoolId, State,
fun(Conn, State1) ->
Pid = Conn#conn.pid,
mysql_conn:fetch(Pid, QueryList, From),
{noreply, State1}
end).
mysql_dispatcher仅仅将消息转发给合适的mysql_conn,然后返回{noreply, NewState}。看好了,这里是noreply,所以业务进程调用mysql:fetch并不能在这里获得返回,这个时候业务进程还属于继续阻塞状态。
那么mysql:fetch的返回结果是从哪里得到?
mysql:fetch获得的返回结果是通过mysql_conn 使用gen_server:reply返回给调用进程的。
%% GenSrvFrom is either a gen_server:call/3 From term(),
%% or a pid if no gen_server was used to make the query
send_reply(GenSrvFrom, Res) when is_pid(GenSrvFrom) ->
%% The query was not sent using gen_server mechanisms
GenSrvFrom ! {fetch_result, self(), Res};
send_reply(GenSrvFrom, Res) ->
gen_server:reply(GenSrvFrom, Res).
这里可能会有一个疑问就是,mysql_conn如何找到mysql:fetch 的调用进程并且正确地将值返回给他,如果在调用进程等待的返回值期间,先收到其他返回值怎么办?
关于这个问题,要查询官方文档上关于gen_server:call的说法。当使用gen_server:call向某一指定的进程发送call消息的时候,收到消息的一方是这样处理的 :Module:handle_call(Request, From, State)。
让我们再看下文档,From is a tuple {Pid, Tag} where pid is the client and Tag is a unique tag.
如果收到handle_call的一方,使用{reply, Reply, State}返回,那么Reply will be given back to From as the return value of call/2,3。但是问题来了,我们的mysql_dispatcher并没有使用常规手段,他直接返回{noreply, NewState}。那么mysql:fetch的调用不是收不到返回值了,不要急,文档说了 if the function returns {noreply, NewState}, Any reply to From must be given explicitly using gen_server:reply/2。
问题又来了,难道mysql_dispatcher没有使用gen_server:reply?确实没有!但是他把From直接传递给了mysql_conn,最终是mysql_conn查询结束后使用gen_server:reply,把结果最终返回给了阻塞在mysql:fetch中的业务进程
所以,在erlang_mysql_driver的连接池中一开始建立多个连接,在面对大量请求的时候,确实是有帮助的,可以多个连接同时执行,最终的io压力会放在这几个连接进程上,mysql_dispatcher顶多就是要维护的进程池有点大罢了。
大并发执行fetch是否会有大量timeout 报错?
这个是题外话了,在测试的时候遇到的问题。因为毕竟只有10个连接来处理10万个请求,那么后面的几万个请求肯定要排队到好久之后的。这个时间一旦超过了设置的timeout时间,那么就会有timeout报错。
然而测试开始的时候,我没有看到timeout报错,一直很疑惑。后来发现是timeout报错导致业务进程直接挂了,已经没法打印报错出来了。
在上面的测试中,只有9个mysql_conn,同一时刻却要处理10万条sql。那么肯定会有其他大量的调用一直处于阻塞状态的,我们使用mysql:fetch(PoolId, Query)的形式查询,而mysql:fetch其实是封装了gen_server call,这个方法默认的timeout时间是5秒。如果在5秒内没有收到返回值,就会扔出一个timeout的错误。而如果不去catch这个错误,进程就直接挂了,那么错误也打印不出来了。
在测试中,使用mysql:fetch(PoolId, Query)的调用,刚开始的进程能收到返回值,但是5秒后,进程就只能收到timeout报错了。
另外 使用mysql:fetch(PoolId, Query, infinity)的调用,进程会一直等待,测试表明虽然有很多的sql查询请求,每个mysql_conn都收到了1万左右的消息,但是最终都能执行并返回结果。