rabbitmq中消息的存储

 

1. 大概原理:

所有队列中的消息都以append的方式写到一个文件中,当这个文件的大小超过指定的限制大小后,关闭这个文件再创建一个新的文件供消息的写入。文件名(*.rdq)从0开始然后依次累加。当某个消息被删除时,并不立即从文件中删除相关信息,而是做一些记录,当垃圾数据达到一定比例时,启动垃圾回收处理,将逻辑相邻的文件中的数据合并到一个文件中。

2. 消息的读写及删除:

rabbitmq在启动时会创建msg_store_persistent,msg_store_transient两个进程,一个用于持久消息的存储,一个用于内存不够时,将存储在内存中的非持久化数据转存到磁盘中。所有队列的消息的写入和删除最终都由这两个进程负责处理,而消息的读取则可能是队列本身直接打开文件进行读取,也可能是发送请求由msg_store_persisteng/msg_store_transient进程进行处理。

在进行消息的存储时,rabbitmq会在ets表中记录消息在文件中的映射,以及文件的相关信息。消息读取时,根据消息ID找到该消息所存储的文件,在文件中的偏移量,然后打开文件进行读取。消息的删除只是从ets表删除指定消息的相关信息,同时更新消息对应存储的文件的相关信息(更新文件有效数据大小)。

 
  1. -record(msg_location, { msg_id, %%消息ID

  2. ref_count, %%引用计数

  3. file, %%消息存储的文件名

  4. offset, %%消息在文件中的偏移量

  5. total_size %%消息的大小

  6. }).

  7.  
  8. -record(file_summary, { file, %%文件名

  9. valid_total_size, %%文件有效数据大小

  10. left, %%位于该文件左边的文件

  11. right, %%位于该文件右边的文件

  12. file_size, %%文件总的大小

  13. locked, %%上锁标记 垃圾回收时防止对文件进行操作

  14. readers %%当前读文件的队列数

  15. })

3. 垃圾回收:

由于执行消息删除操作时,并不立即对在文件中对消息进行删除,也就是说消息依然在文件中,仅仅是垃圾数据而已。当垃圾数据超过一定比例后(默认比例为50%),并且至少有三个及以上的文件时,rabbitmq触发垃圾回收。垃圾回收会先找到符合要求的两个文件(根据#file_summary{}中left,right找逻辑上相邻的两个文件,并且两个文件的有效数据可在一个文件中存储),然后锁定这两个文件,并先对左边文件的有效数据进行整理,再将右边文件的有效数据写入到左边文件,同时更新消息的相关信息(存储的文件,文件中的偏移量),文件的相关信息(文件的有效数据,左边文件,右边文件),最后将右边的文件删除。

4. 性能考虑:

(1)操作引用计数(flying_ets)

队列在进行消息的写入和删除操作前,会在flying_ets表里通过+1,-1的方式进行计数,然后投递请求给msg_store_persistent/msg_store_transient进程进行处理,进程在真正写操作或者删除之前会再次判断flying_ets中对应消息的计数决定是否需要进行相应操作。这样,对于频繁写入和删除的操作,概率减少实际的写入和删除。

 
  1. client_write(MsgId, Msg, Flow,

  2. CState=#client_msstate{cur_file_cache_ets=CurFileCacheEts,

  3. client_ref=CRef}) ->

  4. ok = client_update_flying(+1, MsgId, CState),

  5. ok = update_msg_cache(CurFileCacheEts, MsgId, Msg),

  6. ok = server_cast(CState, {write, CRef, MsgId, Flow}).

  7.  
  8. remove(MsgIds, CState = #client_msstate { client_ref = CRef }) ->

  9. [client_update_flying(-1, MsgId, CState) || MsgId <- MsgIds],

  10. server_cast(CState, {remove, CRef, MsgIds}).

  11.  
  12. client_update_flying(Diff, MsgId,

  13. #client_msstate{flying_ets = FlyingEts,

  14. client_ref = CRef}) ->

  15. Key = {MsgId, CRef},

  16. case ets:insert_new(FlyingEts, {Key, Diff}) of

  17. true ->

  18. ok;

  19. false ->

  20. try ets:update_counter(FlyingEts, Key, {2, Diff}) of

  21. ...

  22. end.

  23.  
  24. handle_cast({write, CRef, MsgId, Flow},

  25. State = #msstate{cur_file_cache_ets=CurFileCacheEts,

  26. clients=Clients}) ->

  27. ...

  28. true = 0 =< ets:update_counter(CurFileCacheEts, MsgId, {3, -1}),

  29. case update_flying(-1, MsgId, CRef, State) of

  30. process ->

  31. [{MsgId,Msg,_PWC}]=ets:lookup(CurFileCacheEts, MsgId),

  32. noreply(write_message(MsgId, Msg, CRef, State));

  33. ignore ->

  34. ...

  35. end;

  36.  
  37. handle_cast({remove, CRef, MsgIds}, State) ->

  38. {RemovedMsgIds, State1} =

  39. lists:foldl(

  40. fun (MsgId, {Removed, State2}) ->

  41. case update_flying(+1, MsgId, CRef, State2) of

  42. process ->

  43. {[MsgId | Removed],

  44. remove_message(MsgId, CRef, State2)};

  45. ignore ->

  46. {Removed, State2}

  47. end

  48. end, {[], State}, MsgIds),

  49. ...

  50.  
  51. update_flying(Diff,MsgId,CRef,#msstate{flying_ets = FlyingEts }) ->

  52. Key = {MsgId, CRef},

  53. NDiff = -Diff,

  54. case ets:lookup(FlyingEts, Key) of

  55. [] ->

  56. ignore;

  57. [{_, Diff}] ->

  58. ignore;

  59. [{_, NDiff}] ->

  60. ets:update_counter(FlyingEts, Key, {2, Diff}),

  61. true = ets:delete_object(FlyingEts, {Key, 0}),

  62. process;

  63. [{_, 0}] ->

  64. true = ets:delete_object(FlyingEts, {Key, 0}),

  65. ignore;

  66. [{_, Err}] ->

  67. throw({bad_flying_ets_record, Diff, Err, Key})

  68. end.

(2)尽可能的并发读

在读取消息的时候,都先根据消息ID找到对应存储的文件,如果文件存在并且未被锁住,则直接打开文件,从指定位置读取消息的内容。

如果消息存储的文件被锁住了,或者对应的文件不存在了,则发送请求,由msg_store_persistent/msg_store_transient进程进行处理。

(3)消息缓存

1)利用ets表进行缓存 

对于当前正在写的文件,所有消息在写入前都会在cur_file_cache_ets表中存一份,消息读取时会优先从这里进行查找。文件关闭时,会将cur_file_cache_ets表中引用计数为0的消息进行清除。

2)file_handle_cache的写缓存

rabbitmq中对文件的操作封转到了file_handle_cache模块,以写模式打开文件时,默认有1M大小的缓存,即在进行文件的写操作时,是先写入到这个缓存中,当缓存超过大小或者显式刷新,才将缓存中的内容刷入磁盘中。

 
  1. rabbit_msg_store.erl

  2.  
  3. -define(HANDLE_CACHE_BUFFER_SIZE, 1048576). %% 1MB

  4.  
  5. open_file(Dir, FileName, Mode) ->

  6. file_handle_cache:open(form_filename(Dir, FileName),

  7. ?BINARY_MODE ++ Mode,

  8. [{write_buffer,?HANDLE_CACHE_BUFFER_SIZE}]).

  9.  
  10. file_handle_cache.erl

  11.  
  12. append(Ref,Data) ->

  13. with_handles(

  14. [Ref],

  15. fun ([#handle { is_write = false }]) ->

  16. {error, not_open_for_writing};

  17. ([Handle]) ->

  18. case maybe_seek(eof, Handle) of

  19. {{ok, _Offset}, #handle{hdl = Hdl,

  20. offset = Offset,

  21. write_buffer_size_limit = 0,

  22. at_eof = true }= Handle1} ->

  23. Offset1 = Offset + iolist_size(Data),

  24. {prim_file:write(Hdl, Data),

  25. [Handle1#handle{is_dirty=true,offset=Offset1 }]};

  26. {{ok, _Offset},#handle{write_buffer = WriteBuffer,

  27. write_buffer_size = Size,

  28. write_buffer_size_limit= Limit,

  29. at_eof = true } = Handle1} ->

  30. WriteBuffer1 = [Data | WriteBuffer],

  31. Size1 = Size + iolist_size(Data),

  32. Handle2=Handle1#handle{write_buffer=WriteBuffer1,

  33. write_buffer_size=Size1},

  34. case Limit =/= infinity andalso Size1 > Limit of

  35. true ->

  36. {Result,Handle3} = write_buffer(Handle2),

  37. {Result, [Handle3]};

  38. false ->

  39. {ok, [Handle2]}

  40. end;

  41. {{error, _} = Error, Handle1} ->

  42. {Error, [Handle1]}

  43. end

  44. end).

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值