分布式_消息队列

--------------------- 消息队列----------------------------------------

1. 选型:

网络关键信息总结:

(1)ActiveMQ使用PUSH模型,而Kafka使用PULL模型,两者各有利弊,对于PUSH,broker很难控制数据发送给不同消费者的速度,而PULL可以由消费者自己控制,但是PULL模型可能造成消费者在没有消息的情况下盲等;

(2)ActiveMQ(遵循JMS规范-Apache)、Kafka(Apache)、RocketMQ(阿里), kafka是处理性能最好,也是最容易扩展和伸缩的;

Kafka[1] 是一种高吞吐量[2] 的分布式发布订阅消息系统,有如下特性:

  • 通过O(1)的磁盘数据结构提供消息的持久化,这种结构对于即使数以TB的消息存储也能够保持长时间的稳定性能。
  • 高吞吐量[2] :即使是非常普通的硬件Kafka也可以支持每秒数百万[2] 的消息。
  • 支持通过Kafka服务器和消费机集群来分区消息。
  • 支持Hadoop并行数据加载。

kafka:

优点:消费者可以采用pull模式拉取消息,这样容易控制消费速度;

采用分区机制和磁盘顺序存储,处理性能好,单机吞吐量高(上10万);最容易扩展和伸缩;

kafka中消息的Offset是由客户端自己维护,传统的MQ,消息的Offset是由MQ维护;

传统的MQ,消息被消化掉后会被mq删除,而kafka中消息被消化后不会被删除,而是到配置的expire时间后,才删除;

缺点:可靠性低,可能会有消息重复或丢失;

RocketMQ:

优点是可靠性更高,定位阿里的订单,交易,充值,消息推送,日志流式处等;特点是:push模式,消费失败支持定时重试;

详见:

https://www.cnblogs.com/valor-xh/p/6348009.html rabbitMQ、activeMQ、zeroMQ、Kafka、Redis 比较

https://blog.csdn.net/oMaverick1/article/details/51331004 MQ选型对比文档

https://cloud.tencent.com/developer/article/1131893 爬虫架构 | 消息队列应用场景及ActiveMQ、RabbitMQ、RocketMQ、Kafka对比

2. 作用:异步解耦、削峰填谷、广播、最终一致性、提高并发量。。。

3. 消息可靠性设计

(1) 可靠投递:

每当要发生不可靠的事情(RPC等)之前,先将消息落地,然后发送。当失败或者不知道成功失败(比如超时)时,消息状态是待发送,定时任务不停轮询所有待发送消息,最终一定可以送达。

具体来说:

  1. producer往broker发送消息之前,需要做一次落地。
  2. 请求到server后,server确保数据落地后再告诉客户端发送成功。
  3. 支持广播的消息队列需要对每个待发送的endpoint,持久化一个发送状态,直到所有endpoint状态都OK才可删除消息。

对于各种不确定(超时、down机、消息没有送达、送达后数据没落地、数据落地了回复没收到),其实对于发送方来说,都是一件事情,就是消息没有送达。重推消息所面临的问题就是消息重复。重复和丢失就像两个噩梦,你必须要面对一个。好在消息重复还有处理的机会,消息丢失再想找回就难了。

(2) 消息确认:

允许消费者主动进行消费确认是必要的。当然,对于没有特殊逻辑的消息,默认Auto Ack也是可以的,但一定要允许消费方主动ack。

(3) 重复消息:

一般来讲,一个主流消息队列的设计范式里,应该是不丢消息的前提下,尽量减少重复消息,不保证消息的投递顺序。

通常方法:给每个消息标志唯一ID,处理前进行判重,或利用数据库唯一键特性,从而实现消息幂等处理;

特殊情况:由于消息不能被永久存储,所以理论上都存在消息从持久化存储移除的瞬间上游还在投递的可能(上游因种种原因投递失败,不停重试,都到了下游清理消息的时间)。这种事情都是异常情况下才会发生的,毕竟是小众情况。两分钟消息都还没送达,多送一次又能怎样呢?幂等的处理消息是一门艺术,因为种种原因重复消息或者错乱的消息还是来到了,说两种通用的解决方案:

(1)版本号:发送消息是增加版本号,逐步递增,如果想让乱序的消息最后能够正确的被组织,那么就应该只接收比当前版本号大一的消息。

(2)状态机:业务方只需要自己维护一个状态机,定义各种状态的流转关系。例如,"下线"状态只允许接收"上线"消息,“上线”状态只能接收“下线消息”,如果上线收到上线消息,或者下线收到下线消息,在消息不丢失和上游业务正确的前提下。要么是消息发重了,要么是顺序到达反了。这时消费者只需要把“我不能处理这个消息”告诉投递者,要求投递者过一段时间重发即可。而且重发一定要有次数限制,比如5次,避免死循环,就解决了。

(4) 顺序消息:

版本号:发送消息是增加版本号,逐步递增,如果想让乱序的消息最后能够正确的被组织,那么就应该只接收比当前版本号大一的消息。并且在一个session周期内要一直保存各个消息的版本号。如果到来的顺序是21,则先把2存起来,待2到来后,再处理1,这样重复性和顺序性要求就都达到了。

4. pull or push模式消费:

push模式中,broker给consumer推送一堆consumer无法处理的消息,consumer不是reject就是error,然后来回踢皮球。反观pull模式,consumer可以按需消费,不用担心自己处理不了的消息来骚扰自己,而broker堆积消息也会相对简单,无需记录每一个要发送消息的状态,只需要维护所有消息的队列和偏移量就可以了。所以对于建立索引等慢消费,消息量有限且到来的速度不均匀的情况,pull模式比较合适。

消息延迟与忙等:pull模式的缺点,业界较成熟的做法是从短时间开始(不会对broker有太大负担),然后指数级增长等待。比如开始等5ms,然后10ms,然后20ms,然后40ms……直到有消息到来,然后再回到5ms。在阿里的RocketMq里,有一种优化的做法-长轮询,来平衡推拉模型各自的缺点。基本思路是:消费者如果尝试拉取失败,不是直接return,而是把连接挂在那里wait,服务端如果有新的消息到来,把连接notify起来,这也是不错的思路。但海量的长连接block对系统的开销还是不容小觑的,还是要合理的评估时间间隔,给wait加一个时间上限比较好~

顺序消息:如果push模式的消息队列,支持分区,单分区只支持一个消费者消费,并且消费者只有确认一个消息消费后才能push送另外一个消息,还要发送者保证全局顺序唯一,听起来也能做顺序消息,但成本太高了,尤其是必须每个消息消费确认后才能发下一条消息,这对于本身堆积能力和慢消费就是瓶颈的push模式的消息队列,简直是一场灾难。

反观pull模式,如果想做到全局顺序消息,就相对容易很多:

  1. producer对应partition,并且单线程。
  2. consumer对应partition,消费确认(或批量确认),继续消费即可。

所以对于日志push送这种最好全局有序,但允许出现小误差的场景,pull模式非常合适。如果你不想看到通篇乱套的日志~~

Anyway,需要顺序消息的场景还是比较有限的而且成本太高,请慎重考虑。

5. 批量发送和消费

谈到批量就不得不提生产者消费者模型。但生产者消费者模型中最大的痛点是:消费者到底应该何时进行消费。大处着眼来看,消费动作都是事件驱动的。主要事件包括:

  1. 攒够了一定数量。
  2. 到达了一定时间。
  3. 队列里有新的数据到来。

对于及时性要求高的数据,可用采用方式3来完成,比如客户端向服务端投递数据。只要队列有数据,就把队列中的所有数据刷出,否则将自己挂起,等待新数据的到来。

在第一次把队列数据往外刷的过程中,又积攒了一部分数据,第二次又可以形成一个批量。伪代码如下:

Executor executor = Executors.newFixedThreadPool(4); final BlockingQueue<Message> queue = new ArrayBlockingQueue<>(); private Runnable task = new Runnable({//这里由于共享队列,Runnable可以复用,故做成全局的 public void run(){ List<Message> messages = new ArrayList<>(20); queue.drainTo(messages,20); doSend(messages);//阻塞,在这个过程中会有新的消息到来,如果4个线程都占满,队列就有机会囤新的消息 } }); public void send(Message message){ queue.offer(message); executor.submit(task) }

这种方式是消息延迟和批量的一个比较好的平衡,但优先响应低延迟。延迟的最高程度由上一次发送的等待时间决定。但可能造成的问题是发送过快的话批量的大小不够满足性能的极致。

Executor executor = Executors.newFixedThreadPool(4); final BlockingQueue<Message> queue = new ArrayBlockingQueue<>(); volatile long last = System.currentMills(); Executors.newSingleThreadScheduledExecutor().submit(new Runnable(){ flush(); },500,500,TimeUnits.MILLS); private Runnable task = new Runnable({//这里由于共享队列,Runnable可以复用,顾做成全局的。 public void run(){ List<Message> messages = new ArrayList<>(20); queue.drainTo(messages,20); doSend(messages);//阻塞,在这个过程中会有新的消息到来,如果4个线程都占满,队列就有机会屯新的消息。 } }); public void send(Message message){ last = System.currentMills(); queue.offer(message); flush(); } private void flush(){ if(queue.size>200||System.currentMills()-last>200){ executor.submit(task) } }

相反对于可以用适量的延迟来换取高性能的场景来说,用定时/定量二选一的方式可能会更为理想,既到达一定数量才发送,但如果数量一直达不到,也不能干等,有一个时间上限。

具体说来,在上文的submit之前,多判断一个时间和数量,并且Runnable内部维护一个定时器,避免没有新任务到来时旧的任务永远没有机会触发发送条件。对于server端的数据落地,使用这种方式就非常方便。

最后啰嗦几句,曾经有人问我,为什么网络请求小包合并成大包会提高性能?主要原因有两个:

  1. 减少无谓的请求头,如果你每个请求只有几字节,而头却有几十字节,无疑效率非常低下。
  2. 减少回复的ack包个数。把请求合并后,ack包数量必然减少,确认和重发的成本就会降低。

99. 其他

(1)https://blog.csdn.net/qq_31666147/article/details/51853389 消息队列设计精要 -- 好!!!

========================读书笔记《可伸缩服务架构-李艳鹏》======================

kafka客户端klient框架简介:是李艳鹏所在公司做的一个项目框架,作用是为了简化kafka客户端API的使用方法,是kafka生产者客户端和消费者客户端的一种简单易用的框架,具备高效集成、高性能、高稳定等特点;具体详见:http://go.ctolib.com/cloudatee-kclient.html,如下是针对kclient框架的原理介绍,重点关注非功能质量等方面的内容,供学习参考:

1. 消费者消费消息时可使用:

同步线程模型:即在消息消费线程池的线程里消费消息和处理业务,适用于轻量级业务,如缓存查询、本地计算等;

异步线程模型:即在消息消费线程池里负责消费消息,异步业务线程池负责处理业务,适用于重量级业务,如大量IO操作等;

2. 异常处理:

可使用简单易用的打印错误日志方式,然后借助报警或监控系统来后续处理;也可考虑实现异常Listener体系结构;

3. 实现优雅关机:

即考虑在消费者服务器处理消息时如何关机,才不会因为处理中断而丢失消息;

处理方法是:发ctrl+c或kill -2/-15发送退出信号给进程,则JVM在退出时会调用注册的钩子,我们可以注册退出钩子来进行优雅关机;不要直接发送Kill -9给进程无条件强制退出;阻止Daemon线程在JVM退出后被杀掉方法是:JVM退出钩子里等待线程完成手头处理消息再退出JVM,如果非Daemon线程,默认会等待worker线程退出时才退出JVM;

猜你喜欢

转载自blog.csdn.net/zxb448126/article/details/81191454
今日推荐