您的位置:首页 > 数据库 > Mongodb

mongodb-erlang driver Replica Set Secondary节点分配不均衡的问题

2013-12-05 16:08 615 查看
服务端使用Mongodb的Replica Set,一个Primary节点,两个Secondary。 开始一直没注意,前天设置了MMS之后发现两个Secondary节点的查询操作分配明显不平衡,其中一个的op数量似乎总是另一个的2倍。

一开始以为是mongodb配置有问题,各种排查后没有发现问题,而且原理上讲RS并不是将Primary作为proxy进行load balance,也就不可能是因为mongodb的配置导致的分配不均,在使用Java的Driver测试后发现,mongodb的Java Driver并无该问题,所以初步可以肯定问题是出现在erlang的driver里。

为了确定下不是特殊情况,在本机也测试了一下。Primary打开./mongostat, 两个Secondary设置db.setProfilingLevel(2), 单进程测试,每次find_one 200条数据,发现第一个seconday查询数约为第二个的2倍,而且mongostat显示的数据中,command大约有60几。直观感觉难道是200个查询被平均分给了3个节点,主节点又把自己的那1/3数据全部转移给了其中一个从节点?

再来看erlang driver,每次数据操作都调用mongo:do方法:

-spec do (write_mode(), read_mode(), connection() | rs_connection(), db(), action(A)) -> {ok, A} | {failure, failure()}. % IO
%@doc Execute mongo action under given write_mode, read_mode, connection, and db. Return action result or failure.
do (WriteMode, ReadMode, Connection, Database, Action) -> case connection_mode (ReadMode, Connection) of
{error, Reason} -> {failure, {connection_failure, Reason}};
{ok, Conn} ->
%%		io:format("Mongo conn:~p~n", [Conn]),
PrevContext = get (mongo_action_context),
put (mongo_action_context, #context {write_mode = WriteMode, read_mode = ReadMode, dbconn = {Database, Conn}}),
try Action() of
Result -> {ok, Result}
catch
throw: E = {connection_failure, _, _} -> {failure, E};
throw: E = not_master -> {failure, E};
throw: E = unauthorized -> {failure, E};
throw: E = {write_failure, _, _} -> {failure, E};
throw: E = {cursor_expired, _} -> {failure, E}
after
case PrevContext of undefined -> erase (mongo_action_context); _ -> put (mongo_action_context, PrevContext) end
end end.

-spec connection_mode (read_mode(), connection() | rs_connection()) -> {ok, connection()} | {error, reason()}. % IO
%@doc For rs_connection return appropriate primary or secondary connection
connection_mode (_, Conn = {connection, _, _, _}) -> {ok, Conn};
connection_mode (master, RsConn = {rs_connection, _, _, _}) -> mongo_replset:primary (RsConn);
connection_mode (slave_ok, RsConn = {rs_connection, _, _, _}) -> mongo_replset:secondary_ok (RsConn).


通过connection_mode (ReadMode, Connection)获得与数据库的连接,当ReadMode为slave_ok时,调用mongo_replset:secondary_ok (RsConn).

再来看mongo_replset:secondary_ok/1,

-spec secondary_ok (rs_connection()) -> err_or(connection()). % IO
%@doc Return connection to a current secondary in replica set or primary if none
secondary_ok (ReplConn) -> try
{_Conn, Info} = fetch_member_info (ReplConn),
Hosts = lists:map (fun mongo_connect:read_host/1, bson:at (hosts, Info)),
R = random:uniform (length (Hosts)) - 1,
secondary_ok_conn (ReplConn, rotate (R, Hosts))
of Conn -> {ok, Conn}
catch Reason -> {error, Reason} end.


Hosts自然是得到的主从节点信息,形如:

[{"192.168.17.102",27017},
{"192.168.17.102",27018},
{"192.168.17.102",27019}]


这时注意下面,使用了一个随机变量R,看到这里肯定想让到是用户从Hosts中随机取出一个Secondary节点进行读操作。既然这样,那么是不是random:uniform()每次产生的随机数不随机?简单测试发先并非该问题,真正的原因在

secondary_ok_conn (ReplConn, rotate (R, Hosts))


首先看一下secondary_ok_conn:

-spec secondary_ok_conn (rs_connection(), [host()]) -> connection(). % EIO
%@doc Return connection to a live secondaries in replica set, or primary if none
secondary_ok_conn (ReplConn, Hosts) -> try
until_success (Hosts, fun (Host) ->
{Conn, Info} = connect_member (ReplConn, Host),
case bson:at (secondary, Info) of true -> Conn; false -> throw (not_secondary) end end)
catch _ -> primary_conn (2, ReplConn, fetch_member_info (ReplConn)) end.


该函数调用until_success/2,用于依次遍历Hosts列表,直到找到一个节点是secondary为止。当然每次遍历的时候先要connect_member/2得到连接信息,再判断是否为主从节点。这也就解释了为什么primary节点的mongostat数据中有60+的操作(200的1/3)。

而这里的Hosts,就是rotate (R, Hosts)后得到的新的Hosts:

rotate (N, List) ->
{Front, Back} = lists:split (N, List),
Back ++ Front.


不难看出,该函数用于打乱Hosts中各Host的顺序,从而生成新的随机顺序的Hosts列表。配合这样secondary_ok_conn/2函数,达到随机选取一个secondary的目的。

但是该方法存在一个问题,举个例子来说,List=[1,2,3], 对于随机的N(0/1/2),该方法只能产生3中随机组合:

[1,2,3]
[3,1,2]
[2,3,1]


每种组合产生的概率为1/3。这里假设1,2为两个secondary,3为primary。 之前说过until_success/2用于从列表中找到第一个secondary节点,忽略主节点。也就是说,如果遇到第二种Hosts组合,我们选取的节点必然是1。如此来看,最终选择到1的概率为2/3, 2的概率为1/3. 所以就出现了开头所说的情况。

临时写了一个rotate_new替换原来的rotate,用于等概率生成[1,2,3,...N]的所有组合之一,效率有些低,好在这下概率恢复到50%。明天看看再写个效率高点的方法:

rotate_new(RotateHosts, [Host]) ->

[Host | RotateHosts];

rotate_new(RotateHosts, Hosts) ->

R = random:uniform(length(Hosts)),

Host = lists:nth(R, Hosts),

rotate_new([Host | RotateHosts], Hosts--[Host]).

其他一些random sort的方法整理了下:http://blog.csdn.net/huang1196/article/details/17218295

测试发现3个Hosts的情况下这种方法虽然用了--,但是效率还是很高的。

题外话:erlang的driver有1年多没更新了,之前就发现一个严重的bug,就是mongodb_app.erl中有个方法:

next_requestid()-> ets:update_counter (?MODULE, requestid_counter, 1).

对每一个request,都生成一个requestid,每次+1.

然后注意mongo_protocal里对RequestId的使用:

-define (put_header (Opcode), ?put_int32 (RequestId), ?put_int32 (0), ?put_int32 (Opcode)).

-define (get_header (Opcode, ResponseTo), ?get_int32 (_RequestId), ?get_int32 (ResponseTo), ?get_int32 (Opcode)).

所有的RequestId默认是int32的。但是一般企业级的产品,访问量在一段时间内很容易突破int32的范围,也就是update_counter的时候,requestid超过了int32取值范围,导致所有数据查询失效。

目前的解决方法也很简单,直接使用ets:update_counter (?MODULE, requestid_counter, {Pos, 1, 214748364, 0})替换原来的代码,这里214748364是随便设置的一个小于int32的值,超过该值计数器自动置0.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: