pull类型消息中间件-消息服务端(三)

部署架构

消息存储

存储结构

MetaQ的存储结构是一种物理队列+逻辑队列的结构。如下图所示:

Producer生产消息,根据消息的topic选择topic对应某一个分区,然后发送到这个分区对应的Broker;Consumer根据订阅的topic选择去topic的某一个分区拉取消息。

MetaQ将消息存储在本地文件中,每个文件最大大小为1G,如果写入新的消息时,超过当前文件大小,则会自动新建一个文件。文件名称为起始字节大小。以起始字节大小命名并排序这些文件是有诸多好处的,当消费者要抓取某个起始偏移量开始位置的数据,会变的很简单,只要根据传上来的offset二分查找文件列表,定位到具体文件,然后将绝对offset减去文件的起始节点转化为相对offset,即可开始传输数据。假设,每个文件大小为1KB,图中Consumer1 订阅了TopicA,采用pull的方式来拉取消息,刚好Consumer1又被匹配到了TopicA_2分区,Consumer1需要获取{offset=1200,size=200}处的消息。需要经历如下的步骤:

  • 当改pull请求发送到Broker1的时候,Broker1遍历TopicA_2分区(分区就是一些按照文件起始字节大小命名的索引文件,每一个索引文件又包含了多个索引项)找到offset对应的索引项{offset=1200,size=100B,tagHashcode=xxx}。
  • 然后Broker1根据offset值二分查找TopicA_2的commitlog,获取到offset=1200的消息所在的真实文件(0000001024.meta)
  • 根据真实文件的文件名000001024 获取offset=1300的消息所在文件的起始位置=276(1300-1024)
  • 接下来,Broker1从TopicA_2分区的commitlog文件组中0000001024.meta文件的276B个字节开始,读取100B,然后返回给Consumer1。

对于最终用户展现的消息队列只存储Offset,这样使得队列轻量化,单个队列数据量非常少。

这样做的好处如下:

  1. 队列轻量化,单个队列数据量非常少。
  2. 对磁盘的访问串行化,避免磁盘竟争,不会因为队列增加导致IOWAIT增高。

每个方案都有缺点,它的缺点如下:

  1. 写虽然完全是顺序写,但是读却变成了完全的随机读。
  2. 读一条消息,会先读Consume Queue,再读Commit Log,增加了开销。
  3. 要保证Commit Log与Consume Queue完全的一致,增加了编程的复杂度。

以上缺点如何克服:

(1). 随机读,尽可能让读命中PAGECACHE,减少IO读操作,所以内存越大越好。如果系统中堆积的消息过多,读数据要访问磁盘会不会由于随机读导致系统性能急剧下降,答案是否定的。

访问PAGECACHE时,即使只访问1k的消息,系统也会提前预读出更多数据,在下次读时,就可能命中内存。

随机访问Commit Log磁盘数据,系统IO调度算法设置为NOOP(不是ANTICIPATORY吗)方式,会在一定程度上将完全的随机读变成顺序跳跃方式

(2). 由于Consume Queue存储数据量极少,而且是顺序读,在PAGECACHE预读作用下,Consume Queue的读性能几乎与内存一致,即使堆积情况下。所以可认为Consume Queue完全不会阻碍读性能。

(3). Commit Log中存储了所有的元信息,包含消息体,类似于Mysql、Oracle的redolog,所以只要有Commit Log在,Consume Queue即使数据丢失,仍然可以恢复出来。

在读取消息的时候,如何加快读取消息的速度?

传统的read调用会经历内核态-->用户态--->内核态--->网卡缓冲区这样一个复杂的过程。MetaQ使用了mmap的方式,将硬盘文件映射到用内存中,也就是将page cache中的页直接映射到用户进程地址空间中,从而进程可以直接访问自身地址空间的虚拟地址来访问page cache中的页,这样会并不会涉及page cache到用户缓冲区之间的拷贝。对于小文件比较管用

零拷贝

刷盘策略

所有消息都是持久化的,先写入系统 PAGECACHE,然后刷盘,可以保证内存与磁盘都有一份数据,访问时,直接从内存读取。

分区(partition)

同一个topic下有不同分区,每个分区下面会划分为多个文件,只有一个当前文件在写,其他文件只读。当写满一个文件(写满的意思是达到设定值)则切换文件,新建一个当前文件用来写,老的当前文件切换为只读。文件的命名以起始偏移量来命名。

registerTopicsInZk完成向zookeeper注册topic和分区信息功能,有4种类型的根目录

1、/consumers:存放消费者列表及消费记录

2、/brokers/ids:存放Broker列表,如果Broker与Zookeeper失去连接,则会自动注销在/brokers/ids下的broker记录

3、/brokers/topics-pub:存放发布的主题列表及对应的可发送消息的Broker列表

4、/brokers/topics-sub:存放订阅的主题列表及对应可订阅的Broker列表

偏移量(offset)

Offset 消息在 broker 上的每个分区都是组织成一个文件列表,消费者拉取数据需要知道数据在文件中的偏移量,这个偏移量就是所谓 offset。Offset 是绝对偏移量,服务器会将 offset 转化为具体文件的相对偏移量

可靠性

生产者的可靠性保证

消息生产者发送消息后返回SendResult,如果isSuccess返回为true,则表示消息已经确认发送到服务器并被服务器接收存储。整个发送过程是一个同步的过程。保证消息送达服务器并返回结果。

服务器的可靠性保证

消息生产者发送的消息,meta服务器收到后在做必要的校验和检查之后的第一件事就是写入磁盘,写入成功之后返回应答给生产者。因此,可以确认每条发送结果为成功的消息服务器都是写入磁盘的。
写入磁盘,不意味着数据落到磁盘设备上,毕竟我们还隔着一层os,os对写有缓冲。Meta有两个特性来保证数据落到磁盘上:

  • 每1000条(可配置),即强制调用一次force来写入磁盘设备。
  • 每隔10秒(可配置),强制调用一次force来写入磁盘设备。 因此,Meta通过配置可保证在异常情况下(如磁盘掉电)10秒内最多丢失1000条消息。
  • 服务器通常组织为一个集群,一条从生产者过来的消息可能按照路由规则存储到集群中的某台机器。Metaq还正在实现高可用的HA方案,类似mysql的异步复制,将一台meta服务器的数据完整复制到另一台slave服务器,并且slave服务器还提供消费功能。

消费者的可靠性保证

消息的消费者是一条接着一条地消费消息,只有在成功消费一条消息后才会接着消费下一条。如果在消费某条消息失败(如异常),则会尝试重试消费这条消息(默认最大5次),超过最大次数后仍然无法消费,则将消息存储在消费者的本地磁盘,由后台线程继续做重试。而主线程继续往后走,消费后续的消息。因此,只有在MessageListener确认成功消费一条消息后,meta的消费者才会继续消费另一条消息。由此来保证消息的可靠消费。

消费者的另一个可靠性的关键点是offset的存储,也就是拉取数据的偏移量。目前提供了以下几种存储方案:

  • zookeeper,默认存储在zoopkeeper上,zookeeper通过集群来保证数据的安全性。
  • mysql,可以连接到您使用的mysql数据库,只要建立一张特定的表来存储。完全由数据库来保证数据的可靠性。
  • file,文件存储,将offset信息存储在消费者的本地文件中。 Offset会定期保存,并且在每次重新负载均衡前都会强制保存一次。

Broker负载均衡

生产者

每个broker都可以配置一个topic可以有多少个分区,但是在生产者看来,一个topic在所有broker上的所有分区组成一个分区列表来使用。

在创建producer的时候,客户端会从zookeeper上获取publish的topic对应的broker和分区列表,生产者在发送消息的时候必须选择一台broker上的一个分区来发送消息,默认的策略是一个轮询的路由规则。

生产者在通过zk获取分区列表之后,会按照brokerId和partition的顺序排列组织成一个有序的分区列表,发送的时候按照从头到尾循环往复的方式选择一个分区来发送消息。考虑到我们的broker服务器软硬件配置基本一致,默认的轮询策略已然足够。

在broker因为重启或者故障等因素无法服务的时候,producer通过zookeeper会感知到这个变化,将失效的分区从列表中移除做到fail over。因为从故障到感知变化有一个延迟,可能在那一瞬间会有部分的消息发送失败。

消费者

消费者的负载均衡会相对复杂一些。我们这里讨论的是单个分组内的消费者集群的负载均衡,不同分组的负载均衡互不干扰,没有讨论的必要。

综上所述,单个分组内的消费者集群的负载均衡策略如下:

  • 每个分区针对同一个group只挂载一个消费者
  • 如果同一个group的消费者数目大于分区数目,则多出来的消费者将不参与消费
  • 如果同一个group的消费者数目小于分区数目,则有部分消费者需要额外承担消费任务Meta的客户端会自动帮处理消费者的负载均衡,它会将消费者列表和分区列表分别排序,然后按照上述规则做合理的挂载。 从上述内容来看,合理地设置分区数目至关重要。如果分区数目太小,则有部分消费者可能闲置,如果分区数目太大,则对服务器的性能有影响。

在某个消费者故障或者重启等情况下,其他消费者会感知到这一变化(通过 zookeeper watch消费者列表),然后重新进行负载均衡,保证所有的分区都有消费者进行消费。

扩容

扩容是整个系统中的很重要的一个环节。在保证顺序的情况下进行扩容的难度会更大。基本的策略是让向一个队列写入数据的消息发送者能够知道应该把消息写入迁移到新的队列中,并且需要让消息的订阅者知道,当前的队列消费完数据后需要迁移到新队列去消费消息。关键点如下:

  • 原队列在开始扩容后需要有一个标志,即便有新消息过来,也不再接收。
  • 通知消息发送端新的队列的位置。
  • 对于消息接受端,对原来队列的定位会收到新旧两个位置,当旧队列的数据接受完毕后,则会只关心新队列的位置,完成切换。

那么对于Metaq顺序消息,如何做到不停写扩容呢?我说说自己的看法:
在队列扩容的时候考虑到需要处理最新的消息服务,为了不丢失这部分消息,可以采取让Producer暂存消息在本地磁盘设备中,等扩容完成后再与Broker交互。这是我目前能想到的不停写扩容方式。

消息查询和回溯

Consumer已经消费成功的消息,由于业务上需求需要重新消费,Broker 在向Consumer 投递成功消息后,消息仍然需要保留。重新消费一般是按照时间维度,例如由于 Consumer 系统故障,恢复后需要重新消费 1 小时前的数据,可以按照时间维度来回退消费进度。

这个问题我说下自己的看法,首先Metaq是一个Cache系统,热点数据在内存,冷数据在硬盘,读写速度快。消息在服务器的是按照顺序连续append在一起的,文件的命名以起始偏移量(offset)来命名,对于最终用户展示的消息队列只存储offset,当消费者要抓取某个起始偏移量开始位置的数据,会变的很简单,只要根据传上来的offset二分查找文件列表,定位到具体文件,然后将绝对offset减去文件的起始节点转化为相对offset,即可开始传输数据。也就找到了对应的消息,所以可以做到消息的快速查询。

posted on 2016-08-21 15:00  草千里  阅读(730)  评论(0编辑  收藏  举报

导航