如何设计一个支撑亿级流量的消息队列

大家可能对亿级流量没有什么概念,我首先以10亿级流量来进行一下设备需求的评估:
每天需要承载10亿+请求流量数据,一天24小时,对于平台来说,晚上12点到凌晨8点这8个小时几乎没多少数据涌入的。这里我们使用「二八法则」来进行预估,也就是80%的数据(8亿)会在剩余的16个小时涌入,且8亿中的80%的数据(约6.4亿)会在这16个小时的20%时间 (约3小时)涌入。
通过上面的场景分析,可以得出如下:

QPS计算公式 = 640000000 ÷ (3 * 60 * 60) = 6万

也就是说高峰期集群需要抗住每秒6万的并发请求。

首先从网络设备来看,目前比较普及的是千兆网卡,首先我们以千兆网卡来进行设备台数的估算:

假设每条数据平均按20kb(生产端有数据汇总)来算,高峰期的时候,1000M网卡不可能打满带宽,我们按80%来算,所以每台机器能处理的请求数是

1000*1024kb%0.8/20kb =4W

再考虑到高可靠性问题,每台机器至少需要3个副本,所以每台机器能处理的请求大约是1W左右。

队列如何设计?

从上面的估算我们得出,一台机器是搞不定10亿请求的,所以我们需要多台机器来解决问题。在大学课本上我们学过,队列具备先进先出的特性,从数据结构上来说,需要一个头尾指针,如果是多机部署,那么我们这个头尾指针就不能记录到一台机器上,所以我们需要一个记录头尾指针的地方。

用队列解决问题,其实是用生产者消费模式来解决问题,生产者不停往队列生产数据,消费者不停进行数据消费。所以两者需要紧密配合,不然有一方速度跟不上都会导致10亿的目标无法实现。

假设生产者消费的过程中生产的速度很快,消费的速度很慢,如果不限制生产者的生产速度那么可能会出现内存的OOM,因为内存是有限制的,但是如果为了避免限制生产者的生产,因为如果限制了就无法达到10亿的目标,所以我们要解决这个问题就不可能用内存去存储数据,要么把数据存储在磁盘上,或者数据库。

总体如何设计?

从以上分析我们可以知道,总体上我们采用分布式的架构,为了存储队列的头尾指针,我们设计一个类似于元数据的东西,就是要分布式存储队列的头尾指针来记录队列的生产和消费情况,同时我们需要一个服务节点来进行分布式协调,我们需要这个服务节点来接受生产者生产的数据,同时将数据传递到消费者手里,同时这个服务节点需要将数据进行分布式存储,要么存储到磁盘,或者是数据库。

通过以上的分析这个架构基本成型,但是我们还需要在细节上去考虑一些东西,不然还是无法实现目标。

首先,我们要考虑消费者如何去服务节点获取数据,一般获取数据方式的有两种,要么是推(push),要么是拉(pull),那么我们应该怎么选择呢?如果采用推模式,那么这个服务器节点必须记录状态,不然无法保证数据的顺序性和连续性消费,我们知道一旦服务器有了状态是很灾难的事情,首先服务器处理起来相当复杂,其次服务器需要记录大量状态并进行状态管理导致资源扩张的问题,因为我们不知道我们的客户即消费者的规模,而导致服务器承担管理相当多的资源,可能是无限的,同时推模式因为有了状态导致服务节点无法做到横向扩展。所以我们只能选择拉模式,将状态和复杂性留着客户端,这样才能做到服务端达到无限水平扩展。

其次,我们要考虑数据如何存储的问题,如果存储在磁盘上,由于非SSD的磁盘对随机读不友好,所以我们需要考虑数据如何写入到磁盘的问题,考虑到性能方法肯定是顺序写是性能最好的,但是如果是写入到磁盘上,我们考虑到海量数据的检索问题,所以需要对文件进行分块存储和进行索引问题。如果是存储到数据库,我们可以利用数据库的天生的分库和自带的索引功能来避免复杂的设计。

如何做到高性能和高可用?

在高性能方面,可以参考我之前写的Netty简介和Dubbo的线程模型两篇文章,总体上来说就是借助于IO多路复用模式同时综合使用线程池和队列等技术来充分压榨服务的CPU和IO来为我们所用,总体原则是不能浪费服务器资源,同时也不能让服务器闲置。

在高可用方面,我们需要考虑考虑服务节点和存储数据的多副本机制,然而既然存在多副本,我们就需要处理多副本的同步问题,如果不想折腾可以直接使用Zookeeper,zk使用zab协议保证多副本的同步问题,对于分布式同步协议可以参考我之前写的分布式协议简介,很多分布式数据为了不依赖于zk,自己实现了Paxos或者Raft实现了副本同步问题。

由于在存储方面我们设计了多分区存储,那么可以利用多分区的特性来加速生产者的生产和消费者的消费,在生产端我们可以让多个生产者同时将数据生产到不同的分区,消费端让消费者消费不同的分区,这样就可以大大提高生产和消费的速度,对于消费端还要考虑横向扩展问题,就是如果有消费者上下线要考虑怎么做重平衡问题,保证消费端的均衡消费。

at least once和exactly once

at least once是对一个队列产品的基本要求,如果满足不了这个要求一般都不是一个合格的队列产品,at least once一般要求客户端能处理重复消费的问题,即保证无状态。exactly once是最难的,要实现这点,我们需要保证消息仅仅被投递一次,那么我们需要在服务端不能丢消息,也不能重复投递消息,为到达这点我们可以参考关系数据库的redo和undo日志的实现来进行设计,同时需要在业务上确定消息的唯一性,我们需要给消息加入唯一标识,而保证消息不被重复投递,这个跟数据库的主键很像。

其他方面

一个队列的开发我们可以使用任何语言,如果使用java的话,我们还可以做其他优化,比如为了避免jvm的垃圾回收机制导致内存频繁的回收,我们可以使用堆外内存设计内存池来加快内存的分配和重复利用,同时可以利用零拷贝技术避免内存数据在用户态和内核态重复的进行拷贝来提高性能。我们也可以使用自定义协议或者压缩技术来实现数据的高效传输等。

总结

设计一个高性能和高可用的队列产品是一个比较有难度和有技术的活,我们可以利用分布式存储技术,分布式协调协议,以及IO多路复用技术等来为我们所用,但是从细节方面来说,要实现一个生产上可用的合格队列产品,需要付出相当的努力才能实现。

你可能感兴趣的:(如何设计一个支撑亿级流量的消息队列)