rabbitmq中消息的存储

转载 2016年05月25日 18:05:46

1. 大概原理:

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

2. 消息的读写及删除:

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

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

-record(msg_location, { msg_id,     %%消息ID
                        ref_count,  %%引用计数
                        file,       %%消息存储的文件名
                        offset,     %%消息在文件中的偏移量
                        total_size  %%消息的大小
                      }).

-record(file_summary, { file,       %%文件名
                        valid_total_size, %%文件有效数据大小
                        left,       %%位于该文件左边的文件
                        right,      %%位于该文件右边的文件
                        file_size,  %%文件总的大小
                        locked,     %%上锁标记 垃圾回收时防止对文件进行操作
                        readers     %%当前读文件的队列数
                      })

3. 垃圾回收:

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


4. 性能考虑:

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

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

client_write(MsgId, Msg, Flow,
             CState=#client_msstate{cur_file_cache_ets=CurFileCacheEts,
                                    client_ref=CRef}) ->
    ok = client_update_flying(+1, MsgId, CState),
    ok = update_msg_cache(CurFileCacheEts, MsgId, Msg),
    ok = server_cast(CState, {write, CRef, MsgId, Flow}).

remove(MsgIds, CState = #client_msstate { client_ref = CRef }) ->
    [client_update_flying(-1, MsgId, CState) || MsgId <- MsgIds],
    server_cast(CState, {remove, CRef, MsgIds}).

client_update_flying(Diff, MsgId,
                     #client_msstate{flying_ets = FlyingEts,
                                     client_ref = CRef}) ->
    Key = {MsgId, CRef},
    case ets:insert_new(FlyingEts, {Key, Diff}) of
        true  ->
            ok;
        false ->
            try ets:update_counter(FlyingEts, Key, {2, Diff}) of
            ...
    end.

handle_cast({write, CRef, MsgId, Flow},
            State = #msstate{cur_file_cache_ets=CurFileCacheEts,
                             clients=Clients}) ->
    ...
    true = 0 =< ets:update_counter(CurFileCacheEts, MsgId, {3, -1}),
    case update_flying(-1, MsgId, CRef, State) of
        process ->
            [{MsgId,Msg,_PWC}]=ets:lookup(CurFileCacheEts, MsgId),
            noreply(write_message(MsgId, Msg, CRef, State));
        ignore ->
            ...
    end;

handle_cast({remove, CRef, MsgIds}, State) ->
    {RemovedMsgIds, State1} =
        lists:foldl(
            fun (MsgId, {Removed, State2}) ->
                case update_flying(+1, MsgId, CRef, State2) of
                    process ->
                        {[MsgId | Removed],
                        remove_message(MsgId, CRef, State2)};
                    ignore ->
                        {Removed, State2}
                end
            end, {[], State}, MsgIds),
    ...

update_flying(Diff,MsgId,CRef,#msstate{flying_ets = FlyingEts }) ->
    Key = {MsgId, CRef},
    NDiff = -Diff,
    case ets:lookup(FlyingEts, Key) of
        [] ->
            ignore;
        [{_,  Diff}] ->
            ignore;
        [{_, NDiff}] ->
            ets:update_counter(FlyingEts, Key, {2, Diff}),
            true = ets:delete_object(FlyingEts, {Key, 0}),
            process;
        [{_, 0}] ->
            true = ets:delete_object(FlyingEts, {Key, 0}),
            ignore;
        [{_, Err}] ->
            throw({bad_flying_ets_record, Diff, Err, Key})
    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大小的缓存,即在进行文件的写操作时,是先写入到这个缓存中,当缓存超过大小或者显式刷新,才将缓存中的内容刷入磁盘中。

rabbit_msg_store.erl

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

open_file(Dir, FileName, Mode) ->
    file_handle_cache:open(form_filename(Dir, FileName),
                           ?BINARY_MODE ++ Mode,
                           [{write_buffer,?HANDLE_CACHE_BUFFER_SIZE}]).

file_handle_cache.erl

append(Ref,Data) ->
    with_handles(
        [Ref],
        fun ([#handle { is_write = false }]) ->
           {error, not_open_for_writing};
        ([Handle]) ->
            case maybe_seek(eof, Handle) of
                {{ok, _Offset}, #handle{hdl = Hdl,
                                        offset = Offset,
                                        write_buffer_size_limit = 0,
                                        at_eof = true }= Handle1} ->
                    Offset1 = Offset + iolist_size(Data),
                    {prim_file:write(Hdl, Data),
                    [Handle1#handle{is_dirty=true,offset=Offset1 }]};
                {{ok, _Offset},#handle{write_buffer = WriteBuffer,
                                       write_buffer_size = Size,
                                       write_buffer_size_limit= Limit,
                                       at_eof = true } = Handle1} ->
                    WriteBuffer1 = [Data | WriteBuffer],
                    Size1 = Size + iolist_size(Data),
                    Handle2=Handle1#handle{write_buffer=WriteBuffer1,
                                           write_buffer_size=Size1},
                    case Limit =/= infinity andalso Size1 > Limit of
                        true  ->
                            {Result,Handle3} = write_buffer(Handle2),
                            {Result, [Handle3]};
                        false ->
                            {ok, [Handle2]}
                    end;
                {{error, _} = Error, Handle1} ->
                    {Error, [Handle1]}
            end
        end).


相关文章推荐

关于RabbitMQ性能问题的几点分析

目前对RabbitMQ的使用才刚刚开始,下面提出的问题,也许是由于对服务器的配置或者对客户端API还不了解导致的。欢迎斧正。一. 要避免流控机制触发 服务端默认配置是当内存使用达到40%,磁盘使用...
  • tom06
  • tom06
  • 2017年03月27日 12:07
  • 2013

Rabbitmq 消息堆积测试

1.  问题 在rabbitmq的pub/sub模式中,是否会出现消息堆积现象。比如,生产者向exchange发送了大量消息,而此时consumer不work,无法处理,等到consumer on...

RabbitMQ消息队列

1      什么是RabbitMQ? RabbitMQ是实现AMQP(高级消息队列协议)的消息中间件的一种,最初起源于金融系统,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现...

rabbitmq实现延迟消息

基于RabbitMQ 3.6.5, Erlang 19.0实现,客户端用spring4.2 什么是延迟队列 延迟队列存储的对象肯定是对应的延时消息,所谓”延时消息”是指当消息被发送以后,并不想让消费...

RabbitMQ之队列与消息持久化

队列持久化 在之前的例子中,我们所用的队列都是临时队列,当服务重启后之前创建的队列就都没有了。 队列的持久化是在定义队列时的第二个参数决定的(false为队列不用持久化) ...

从rabbitmq谈谈基于redis的分布式存储系统的开发(1)

传统的数据库系统如mysql,在数据存储的可靠性,以及数据多机房的分布上可以满足,但是大几千甚至几万、几十万每秒的高并发读写请求上,由于硬盘瓶颈,所以性能通常无法满足,因此,在这种高并发请求的需求下,...

初始化rabbitmq用户数据路径以及内存硬盘参数

一.安装er-lang,选择非c盘 二.安装rabbit-mq,选择非c盘 三.修改配置文件 1.修改sbin/rabbitmq-env.bat使得用户目录为rabbit-mq安装目录下的user目录...
  • qsy2000
  • qsy2000
  • 2016年11月18日 11:25
  • 2075

RabbitMQ的元数据重建

1.概述对于RabbitMQ运维层面来说,扩容和迁移是必不可少。扩容比较简单,一般往集群中加入新的机器节点即可,不过新的机器节点中是没有消息的,如果想要新加入的节点能快速的存储消息还是需要做点小手术的...

RabbitMQ持久化机制

之前其实已经写过一篇关于RabbitMQ持久化的 文章 ,但那篇文章侧重代码层面的写入流程,对于持久化操作何时发生以及什么时候会刷新到磁盘等问题其实都没有搞清楚,这篇文章着重于关注这些问题。 消...

redis缓存,rabbitMQ队列

缓存数据库介绍  NoSQL(NoSQL = Not Only SQL ),意即“不仅仅是SQL”,泛指非关系型的数据库,随着互联网web2.0网站的兴起,传统的关系数据库在应付web2.0网站,特...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:rabbitmq中消息的存储
举报原因:
原因补充:

(最多只允许输入30个字)