部署架构

消息存储

存储结构

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,即可开始传输数据。也就找到了对应的消息,所以可以做到消息的快速查询。

pull类型消息中间件-消息服务端(三)的更多相关文章

  1. push类型消息中间件-消息服务端(三)

    1.连接管理 网络架构原来是使用是自己开发的网络框架Gecko,Gecko默认为每个网络连接分配64KB的内存,支持1000个网络连接,就需要大概64MB的内存.后来采用Netty重构了网络服务层. ...

  2. pull类型消息中间件-消息发布者(一)

    消息集群架构 对于发送方来说的关键几要素 topic 消息的主题,由用户定义.类似于知乎的话题,Producer发送消息的时候需要指定发送到某一个topic下面,Consumer从某一个topic下面 ...

  3. pull类型消息中间件-消息消费者(二)

    消费者的实例化 关于consumer的默认实现,metaq有两种: DefaultMQPullConsumer:由业务方主动拉取消息 DefaultMQPushConsumer:通过业务方注册回调方法 ...

  4. 从零讲解搭建一个NIO消息服务端

    本文首发于本博客,如需转载,请申明出处. 假设 假设你已经了解并实现过了一些OIO消息服务端,并对异步消息服务端更有兴趣,那么本文或许能带你更好的入门,并了解JDK部分源码的关系流程,正如题目所说,笔 ...

  5. .net平台 基于 XMPP协议的即时消息服务端简单实现

    .net平台 基于 XMPP协议的即时消息服务端简单实现 昨天抽空学习了一下XMPP,在网上找了好久,中文的资料太少了所以做这个简单的例子,今天才完成.公司也正在准备开发基于XMPP协议的即时通讯工具 ...

  6. push类型消息中间件-消息订阅者(一)

    1.订阅者的声明方式 我们以spring组件化的方式,声明一个消息订阅者,对于消息订阅者关心的主要有: topic: 一级消息类型(又名消息主题).如TRADE 消息类型:二级消息类型,区别同一Top ...

  7. Openstack Ocata 公共服务端(三)

    Openstack Ocata 公共服务端 mysql 安装: yum install mariadb mariadb-server mysql 安装过程省略 rabbit-server 安装包: # ...

  8. js接收对象类型数组的服务端、浏览器端实现

    1.服务端 JSONArray jsonArr = JSONUtil.generateObjList(objList); public static generateObjList(List<O ...

  9. Jquery Ajax处理,服务端三种页面aspx,ashx,asmx的比较

    常规的Jquery Ajax 验证登录,主要有3种服务端页面相应 ,也就是 aspx,ashx,asmx即webserivice . 下面分别用3种方式来(aspx,ashx,asmx)做服务端来处理 ...

随机推荐

  1. [ios2] ios7UI适配 【转】

    http://blog.csdn.net/toss156/article/details/11843873#comments (1)如果应用程序始终隐藏 status bar 那么恭喜呢,你在UI上需 ...

  2. [asp.net] 利用WebClient上传图片到远程服务

    一.客户端 1.页面 1 <form id="Form1" method="post" runat="server" enctype= ...

  3. ng1 http 读取json数据

    在前端开发过程中,有时后端还没开发出接口,需要经常自己构造获取本地mock数据. AngularJS XMLHttpRequest $http 是 AngularJS 中的一个核心服务,用于读取远程服 ...

  4. linux 查看磁盘占用情况

    查看"/usr/local/"路径下,所有文件大小总和.只列出总和,不显示该路径下各子文件的大小. du -sh /usr/local/ 结果显示如下图: 如果要详细显示出各子文件 ...

  5. Logstash利用GeoIP库显示地图以及通过useragent显示浏览器(四)

    我们通过Logstash收集的Nginx Access log中已经包含了客户端IP的数据(remote_addr),但是只有这个IP还不够,要在Kibana的显示请求来源的地理位置还需要借助GeoI ...

  6. php7.0 和 php7.1新特性

    PHP7.1 新特性 1.可为空(Nullable)类型 类型现在允许为空,当启用这个特性时,传入的参数或者函数返回的结果要么是给定的类型,要么是 null .可以通过在类型前面加上一个问号来使之成为 ...

  7. iOS ARC与MRC混编的一些解决方法

    1. ARC & MRC 混合开发 在项目开发中,遇到使用MRC开发的第三方库怎么办? 例如:ASI 1> 尝试使用Xcode的转换工具(失败率比较高) 2> 在编译选项中,为MR ...

  8. react-router的基础知识

    一.基本用法 React Router 安装命令如下. $ npm install -S react-router 使用时,路由器Router就是React的一个组件. import { Router ...

  9. 【IE6的疯狂之五】div遮盖select的解决方案

    IE6以及一下版本下,选择框Select会覆盖Div中的内容一般情况下,可以将显示的内容放到Iframe中,然后再显示框架内的内容.由于Iframe的可以显示在Select上层,就可以解决这个问题.不 ...

  10. Js的Url中传递中文参数乱码的解决

    一:Js的Url中传递中文参数乱码问题,重点:encodeURI编码,decodeURI解码: 1.传参页面Javascript代码: 2. 接收参数页面:test02.html 二:如何获取Url& ...