关于MQTT协议实现消息推送系统

MQTT协议

国内一些人的总结:https://zhuanlan.zhihu.com/p/20888181

概要

为物联网而生,物联网(Internet of Things,IoT)最近曝光率越来越高。虽然HTTP是网页的事实标准,不过机器之间(Machine-to-Machine,M2M)的大规模沟通需要不同的模式:之前的请求/回答(Request/Response)模式不再合适,取而代之的是发布/订阅(Publish/Subscribe)模式。这就是轻量级、可扩展的MQTT(Message Queuing Telemetry Transport)可以施展拳脚的舞台。
适用背景,MQTT是基于二进制消息的发布/订阅编程模式的消息协议,最早由IBM提出的,如今已经成为OASIS规范。由于规范很简单,非常适合需要低功耗和网络带宽有限的IoT场景,比如:遥感数据;汽车;智能家居;智慧城市;医疗医护。
采用pub/sub模式,与请求/回答这种同步模式不同,发布/定义模式解耦了发布消息的客户(发布者)与订阅消息的客户(订阅者)之间的关系,这意味着发布者和订阅者之间并不需要直接建立联系。
主题,MQTT是通过主题对消息进行分类的,本质上就是一个UTF-8的字符串,不过可以通过反斜杠表示多个层级关系。主题并不需要创建,直接使用就是了。主题还可以通过通配符进行过滤。其中,+可以过滤一个层级,而*只能出现在主题最后表示过滤任意级别的层级。举个例子:building-b/floor-5:代表B楼5层的设备;+/floor-5:代表任何一个楼的5层的设备;building-b/*:代表B楼所有的设备。注意,MQTT允许使用通配符订阅主题,但是并不允许使用通配符广播
服务质量,为了满足不同的场景,MQTT支持三种不同级别的服务质量(Quality of Service,QoS)为不同场景提供消息可靠性:级别0:尽力而为,消息发送者会想尽办法发送消息,但是遇到意外并不会重试(TCP保证);级别1:至少一次,消息接收者如果没有知会或者知会本身丢失(消息推送系统中海量消息推送,这个搞不好会带来,推送速率锐减,一条消息可能重试多次直到接收端回复接受正常?),消息发送者会再次发送以保证消息接收者至少会收到一次,当然可能造成重复消息;级别2:恰好一次。保证这种语义肯待会减少并发或者增加延时,不过丢失或者重复消息是不可接受的时候,级别2是最合适的。
服务质量是个老话题了。级别2所提供的不重不丢很多情况下是最理想的,不过往返多次的确认一定对并发和延迟带来影响。级别1提供的至少一次语义在日志处理这种场景下是完全OK的,所以像Kafka这类的系统利用这一特点减少确认从而大大提高了并发。级别0适合鸡肋数据场景,食之无味弃之可惜,就这么着吧。

报文

1.我们
一共四个字节固定报头,剩下是数据包长度。

2.MQTT(最短7字节数据报头)
由  两个字节的固定包头 + 可变报头 + 有效载荷(消息体)组成。MQTT拥有14种不同的消息类型其中,不管长度有限的消息类型,推送消息是通过PUBLISH类型消息推送到接收端的。PUBLISH控制报文是指从客户端向服务端或者服务端向客户端传输一个应用消息。所以,看一下其组成。
2.1固定报头(2字节长度):
关于MQTT协议实现消息推送系统
2.2可变报头(qos0为5字节,QoS1/2为7字节):
其顺序包含 主题名  和 报文标识符
主题名:用于识别有效载荷数据应该被发布到哪一个信息通道
报文标识符:只有当QoS等级是1或2时,报文标识符(Packet Identifier)字段才能出现在PUBLISH报文
中。
关于MQTT协议实现消息推送系统
2.3有效载荷
有效载荷包含将被发布的应用消息。数据的内容和格式是应用特定的。有效载荷的长度这样计算: 用固定报头中的剩余长度字段的值减去可变报头的长度。包含零长度有效载荷的,PUBLISH报文是合法的

3.总结
所以就按照报文大小来说,我们包更加的小。

性能

1.我们
长连接,在线推送,对于报文有重试机制
2.MQTT
2.1也是长连接,在线推送,对于报文QOS1级别,需要的保证机制,估计比我们的重试机制要重;
2.2为了提供服务质量保证,客户端和服务端有必要存储会话状态。在整个会话期间,客户端和服务端都必须存储会话状态。会话必须至少持续和它的活跃网络连接同样长的时间 
2.3支持tcp/ip,TLS,  WebSocket协议
2.4分发协议关注的是从单个发送者到单个接收者的应用消息。服务端分发应用消息给多个客户端时,每个客户端独立处理。分发给客户端的出站应用消息和入站应用消息的QoS等级可能是不同的。

服务端的保留消息不是会话状态的组成部分, 服务端应该保留那种消息直到客户端删除它。MQTT用户应该评估MQTT客户端和服务端实现的存储容量,确保能满足需求。

关于性能测试的例子:

3.ActiveMQ性能测试参考

测试环境:


关于MQTT协议实现消息推送系统
 硬盘:1T,5400  (效果不佳)

 

 

得出了一个异样的测试结果:

 

持久: 

插入200000条JSON,共消耗:25.175 s

平均:7944.389275074478 条/秒

 

插入200000条JSON,共消耗:34.47 s

平均:5802.146794313896 条/秒

 

插入200000条JSON,共消耗:29.937 s数量:1400000

平均:6680.696128536593 条/秒

 

插入200000条JSON,共消耗:29.094 s

平均:6874.269608854059 条/秒

 

 

 

非持久:

插入200000条JSON,共消耗:11.35 s数量:1800000

平均:17621.14537444934 条/秒

 

插入200000条JSON,共消耗:10.714 s

平均:18667.16445771887 条/秒

 

插入200000条JSON,共消耗:11.153 s

平均:17932.394871335066 条/秒

 

插入200000条JSON,共消耗:10.717 s数量:2400000

平均:18661.93897545955 条/秒

 

 

主要在自己本地测试,最终祸首是硬盘不给力啊;

在进行持久化操作时,ActiveMQ默认是kahadb管理

log的默认大小是32MB,当超过之后会新建一个新的log文件,完成操作后,activeMQ又将旧的log删除了。

 


关于MQTT协议实现消息推送系统
 

 

代码贴上:

Java代码  关于MQTT协议实现消息推送系统
  1. public class Sender {  
  2.   
  3.     static int size = 200000;  
  4.     static Session session;  
  5.     static MessageProducer producer;  
  6.     static Topic topic;  
  7.     static Connection connection;  
  8.     static String str = "[{'flag':'1','value':'8854c92e92404b188e63c4031db0eac9','label':'交换机(虚机)'},{'flag':'1','value':'3f367296c2174b7981342dc6fcb39d64','label':'防火墙'},{'flag':'1','value':'8a3e05eeedf54f8cbed37c6fb38c6385','label':'负载均衡'},{'flag':'1','value':'4f0ebc601dfc40ed854e08953f0cdce8','label':'其他设备'},{'flag':'1','value':'6','label':'路由器'},{'flag':'1','value':'4','label':'交换机'},{'flag':'1','value':'b216ca1af7ec49e6965bac19aadf66da','label':'服务器'},{'flag':'1','value':'7','label':'安全设备'},{'flag':'1','value':'cd8b768a300a4ce4811f5deff91ef700','label':'DWDM\\SDH'},{'flag':'1','value':'5','label':'防火墙(模块)'},{'flag':'1','value':'01748963956649e589a11c644d6c09b5','label':'机箱'}]";  
  9.   
  10.     public static void init_connection() throws Exception {  
  11.         ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory("tcp://localhost:61616");  
  12.         connection = factory.createConnection();  
  13.         connection.start();  
  14.         session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);  
  15.         topic = session.createTopic("java.activemq.tps");  
  16.         producer = session.createProducer(topic);  
  17.         producer.setDeliveryMode(DeliveryMode.PERSISTENT);  
  18.     }  
  19.   
  20.     public static void sendMessage(String msg) {  
  21.         TextMessage message;  
  22.         try {  
  23.             message = session.createTextMessage();  
  24.             message.setText(str);  
  25.             producer.send(message);  
  26.         } catch (JMSException e) {  
  27.             e.printStackTrace();  
  28.         }  
  29.     }  
  30.   
  31.     public static void close() throws Exception {  
  32.         connection.close();  
  33.     }  
  34.   
  35.     public static void main(String[] arg) throws Exception {  
  36.         long start = System.currentTimeMillis();  
  37.         ExecutorService es = Executors.newFixedThreadPool(10);  
  38.         final CountDownLatch cdl = new CountDownLatch(size);  
  39.         init_connection();  
  40.         for (int a = 0; a < size; a++) {  
  41.             es.execute(new Runnable() {  
  42.                 @Override  
  43.                 public void run() {  
  44.                     sendMessage(str);  
  45.                     cdl.countDown();  
  46.                 }  
  47.             });  
  48.         }  
  49.         cdl.await();  
  50.         es.shutdown();  
  51.         long time = System.currentTimeMillis() - start;  
  52.         System.out.println("插入" + size + "条JSON,共消耗:" + (double)time / 1000 + " s");  
  53.         System.out.println("平均:" + size / ((double)time/1000) + " 条/秒");  
  54.         close();  
  55.     }  
  56. }  


消息持久化

最重要的是消息持久化,这个时候再写磁盘或者数据库,就有点晚了(一般都有海量设备不在线),效率肯定要低下来,集群应该可以挽救,但是担心集群复杂度更高,成本比较大。后面有一些已有的集群方案作比较。

MQTT实现推送收集

原始设想

关于MQTT协议实现消息推送系统

流行MQTT服务端


emqttd 收费  Erlang开发   支持集群  开源版稳定性可靠性差
RabbitMQ 免费 Erlang开发  不确定是否支持集群,可能需要自己定制    开源性能都说不好
mosquitto 收费      单线程不支持集群和负载均衡开源
Moquette           java     集群自己定制(借助redis等)  开源问题不少http://blog.csdn.net/educast/article/details/78352953
ActiveMQ   免费java 支持集群 开源

关于架构

关于点对点:http://blog.csdn.net/flonny/article/details/78521634(借助广播主题,并遍历server上链接的client,找到对应的clientid)
http://blog.csdn.net/vsddvsd/article/details/54632913(貌似是按照客户端数量建立queue,跪了)

还得考虑消息持久化(要考虑集群的因素,需要加数据库,加上同步保存集群session),加上上面的点对点,等系统成型,性能不敢保证,下面是分布式集群的方案和测试:

影响qctiveMQ性能的几个要素

影响ActiveMQ性能的几个重要因素

Queue
1、Send/dispatch Async 影响非常大

     同步异步的发送和投递,都非常影响吞吐量。另外,SystemUsage和PFC流控对同步发送有直接影响。
2、Not transacted 去掉了记录redo日志
3、Auto_ACK/Optim_ACK 优化确认

     减少交互次数
4、Non-persistence 持久化消息,跟下面几点有关

    持久化和非持久化,也是数量级的影响,毕竟为了提高可靠性,使用数据库或文件来存消息,开销非常大。
5、pendingQueuePolicy/vmQueueCursor 决定了消息存储+发送模式,影响很大

    内存最快,文件和jdbc方式更安全,但是非常慢。。。
6、producerFlowControl/memoryLimit  可能会直接block掉producer

      vmCursor+非持久时,直接变成一个内存MQ,为了不爆掉jvm,在消息积压到指定数量的时候,PFC会阻止生产消息。
7、fast/slow consumer      决定了消息处理模式

     跟上面几点有关系。

8、在connection或connectionFactory上关闭掉 copyMessageOnSend

<!--StartFragment -->
根据JMS规范,消息是不可变的。send的时候,会自动的添加一些属性。有时候,可能会重用,或者多线程处理。为了不影响消息的不可变性,发送的时候,先复制一份,这样,发送时处理的消息对象和你的代码持有的消息对象,是两个不同对象了。相互之间就不会互相影响了。
一般情况下,这个选项可以关闭,从而获得一定的性能提升。
9、consumer端,获取消息时候的prefetchSize设置。 一定范围情况下,一次预获取越大,总体性能越好。