最近,项目中使用到了ActiveMQ获取第三方推送过来的数据。具体背景是:公司需要监控全国各地车辆实时运行的GPS数据,但监控本身不是公司做的,而是交给第三方公司做,第三方采集GPS数据后推送给我们。全国各地,近万台车辆,每台车辆每隔几秒就发送一次GPS位置数据,如果我们提供API给第三方公司去调用,显然无论是第三方还是我们这边,服务器都是是扛不住的,这么做也是不合理的,于是,便采取了消息队列,第三方采集到的数据直接推送到消息队列代理服务器,而己方从消息队列服务器取数据处理。以下对项目实践及其中遇到的一些问题及解决进行概要总结。

1、ActiveMQ NMS简介

关于NMS,这里主要谈两点。

NMS API:ActiveMQ定义的一套API接口规范,你可以理解为一个API的接口,它指明了生产者或消费者如何与消息队列服务器通信。

NMS Providers:NMS API的具体实现,基于Windows或ActiveMQ下的各种协议,提供了各种实现,目前提供了ActiveMQ、STOMP、MSMQ、EMS、WCF、AMQP、MQTT、XMS几种实现。具体项目中,我采取的是ActiveMQ实现。

至于消息队列涉及到的其他概念,什么Broker、Queue、Topic、Producer、Consumer,这里不做介绍,各位可以自己查资料,这些 概念本身也不难理解的。

2、关于异常下的Broker重连

  这个异常,可能是由于网络异常,也可能是长时间没有通信,Broker把Client给断掉了,不去管它。起初,这个项目是从一位离职员工的手头接过来的,给的说法是只需要维护就够了,基本上不用调整。当时虽然说是做了重连,后来发现,就跟没做一样。发现这个,是起源于第三方频繁通知,MQ队列有积压,通知我们尽快处理。项目拿到手一看,我勒个去,直接起了一个Timer在那儿定时监控Connection状态,如果状态不对立刻重新打开连接。先不说Socket连接的浪费情况,及Timer这个.NET中近乎Bug的一个东西,这种做法实际中行之无效,因为连接异常情况下再打开,往往是打开失败的,比如上次异常连接没有关闭,状态不对,或者ClientID暂时没被Broker释放等。

  于是,针对重连,开始做第一次优化。查了IConnection元数据,发现有个ExceptionListener可用,于是便想到利用这个事件来监听并重连。改完上线,可第二天一大早过来,发现MQ又挤压了,重连时效了,打开日志看到,记录了ExceptionListener事件日志,但重连没有成功,具体原因,我想可能和优化前是一样的吧。这折腾前后完全没区别。这时候,我想,不能在现有做法里边去整了,必须回到NMS本身去整,堂堂Apache开源项目,一定有更好的重连机制,放着不用自己整,是不是傻。。。

  于是,打开官网,如愿以偿,找到了failover这个东西:

根据描述,链接异常时,随机从配置的Broker列表总选取一个进行重连。这是个好东西,于是,Broker的链接配置,由tcp://183.56.131.224:61616调整为failover:(tcp://183.56.131.224:61616)。这里没有多个Broker,只有一个,所以配置一个也是没有问题的,我们重点是利用failover。与此同时,OpenWire上发现了maxInactivityDuration这个配置项,官网描述如下:

这个也不错,配置Connection闲置多久被Broker断掉。我这里比较狠,反正Broker那个队列出了第三方往里边推,就我这儿一个人在消费,直接配置0算了,永不被杀,出问题了重连,岂不爽哉。于是,Broker进一步调整为:failover:(tcp://183.56.131.224:61616)?wireFormat.maxInactivityDuration=0。此机制我也自己写Demo验证过,无论是Broker突然停掉再开启,还是Producer停掉再开启,Consumer均能成功重连的。至此,MQ的可靠重连问题算是解决了。

3、进程重启导致Consumer链接失败

  具体情境是这样的:MQ消费者进程是寄宿在Windows服务中的,运维那边做测试或维护,会在MQ运行正常的情况下直接重启服务,有时候会重启失败,过阵子启动,又成功了。我过去,打开Windows事件日志,说是ClientID被占用,也就是说瞬间重启时候,Broker端暂时没来得及断开或者释放该ClientID对应的Connection,而我们系统中ClientID是配置死的。我又验证了下,正常运行下,先关闭服务,过几秒再开启,就没这问题,也印证了自己的推断。问题是找到了,但总不能告诉运维,每次先停止服务,再打开,不能用重启吧,哪个开发要是这样跟我说,那他妈也太不靠谱了。

  解决方案就是,ClientID动态生成,每次启动都不一样,这个ClientID仅仅是Broker用来标识一个连接端的,随便什么都无所谓,只要跟上次不一样。项目中取的是当前时分秒字符串。如下:

_connection.ClientId = DateTime.Now.ToString("yyyyMMddHHmmss");

调整完上线,再试验,那问题再无复现。

4、服务启动时间过长的问题

  随着各种奇葩情况继续出现,我这里继续被操。具体场景是:鉴于是跟第三方合作,各种第三方服务器宕机,各种网络不靠谱,你懂的。如果是消费者进程已经启动成功了,那第三方或者网络不靠谱了,我们利用2中的重连机制就已经可以了,无非就是等他们靠谱了我们自动重连上就是了。可问题是,如果第三方不靠谱,或者网络不靠谱时,我们在启动消费者Windows服务,那会出现什么情况呢?给大家实际演示下:

目前,我我的服务安装后,是这样的:

假设正常链接配置是这样的:

failover:(tcp://183.56.131.224:61616)?wireFormat.maxInactivityDuration=0

为了模拟外界异常或不可达的情况,我手动设置为如下:

failover:(tcp://183.56.131.200:61616)?wireFormat.maxInactivityDuration=0

大家注意,那个Broker地址是不可达的。

开启服务,其结果如下:

这个启动界面,你就等着吧,等个两三分钟,结果如下:

更要命的是,点击确定后,服务启动结果如下:

这就比较操蛋了,你启动失败就失败把,别给我整成启动状态啊,这不误导人么。

  一般,这种情况,就属于启动进程一直卡主,当服务启动超时时,就会出现这种情况, 启动强行被Windows终止,但那个标记为启动状态这个就不好理解,也比较坑了。这是必须要处理的事情,否则极易造成误导。对于Windows服务本身启动机制,你是没办法做任何事情的,那只能从MQ链接机制去干事情。最终, 经过查询官网文档, 再次如愿以偿,找到了以下两个配置项:

这两个配置项分别代表,启动时最大重连尝试次数,默认值0,代表无限重连,我们出问题就出现在这里,链接不上时无限重试,无限重试无限连接不上,无限链接不上再无限重试。。。然后,进程阻塞,阻塞到一定时间,Windows服务重启失败。这个我也在Connection open时候打断点调试过,确实阻塞了。那么第二个配置项代表一项操作超时时间。问题找到了,那么自然也就有解决方案了,现把链接配置为如下:

failover:(tcp://183.56.131.200:61616)?wireFormat.maxInactivityDuration=0&transport.startupMaxReconnectAttempts=3&transport.timeout=3000

这里有两点要注意:

1)原本,这里配置应该是failover:(tcp://183.56.131.224:61616)?wireFormat.maxInactivityDuration=0&transport.startupMaxReconnectAttempts=3&transport.timeout=3000,但在配置文件中, &符号是不支持的,必须转义或替换,这里采取了实体替换,具体的是&这个鬼实体符;

2)NMS.ActiveMQ v1.4.0以上版本和以前以及其他语言实现版本不大相同,1.4以上版本配置这两项参数时必须有transport前缀。这里当时也是吃过亏的。

配置调整完毕后,我们再用 这个无效地址启动服务,在经过60S以内的启动时间,画风变成了这样:

点击确定:

这个时间,和transport.timeout、transport.initialReconnectDelay、transport.startupMaxReconnectAttempts等几项配置有关。但起码时间不会像之前那样很久,并且最终Windows服务状态显示为启动了。

5、总结

  鉴于这是公司实际运作项目,就不上传代码了,如果是自己的Demo,一定毫不保留,望各位见谅。实际上,也没什么特别的,大家平时遇到这种难缠的问题,多查官网文档,官网文档搞不定,再查源码,配合动手实践,一般都不会是问题的。幸运的是,虽然很多官网文档都是英文,但绝大部分都通俗易懂,我们看上去,也都不费事儿的。

附:ActiveMQ生产者及消费者示例代码

生产者:

/// <summary>
/// 生产者启动器
/// </summary>
public class ProducerBootstrap
{
#region Private Fields private readonly IConnectionFactory _connectionFactory = null;
private IConnection _connection = null;
private IMessageProducer _producer = null; #endregion #region Constructors public ProducerBootstrap()
{
_connectionFactory = new ConnectionFactory("tcp://localhost:61616");
} #endregion #region Public Methods public void Start()
{
_connection = _connectionFactory.CreateConnection();
_connection.ExceptionListener += _connection_ExceptionListener;
_connection.Start();
ISession sesison = _connection.CreateSession();
_producer = sesison.CreateProducer(new ActiveMQQueue("guokun"));
} public void Stop()
{
_connection.Stop();
_connection.Close();
_connection.Dispose();
} public void SendMessage()
{
while (true)
{
ITextMessage message = _producer.CreateTextMessage();
message.Text = string.Format("数据:{0}", DateTime.Now);
_producer.Send(message);
Thread.Sleep();
Console.WriteLine(message.Text);
}
} #endregion #region Private Methods private void _connection_ExceptionListener(Exception exception)
{
Console.WriteLine("生产者发生异常:{0}", exception);
} #endregion
}

消费者:

public class ConsumerBootstrap
{
#region Private Fields private readonly IConnectionFactory _connectionFactory = null;
private IConnection _connection = null;
private IMessageConsumer _consumer = null; #endregion #region Constructors public ConsumerBootstrap()
{
_connectionFactory = new ConnectionFactory("failover:(tcp://localhost:61616)?wireFormat.maxInactivityDuration=0&transport.timeout=3000&transport.startupMaxReconnectAttempts=2");
} #endregion #region Public Methods public void Start()
{
_connection = _connectionFactory.CreateConnection();
_connection.ClientId = "guokun";
_connection.ExceptionListener += _connection_ExceptionListener;
_connection.Start();
ISession session = _connection.CreateSession();
_consumer = session.CreateConsumer(new ActiveMQQueue("guokun"));
_consumer.Listener += _consumer_Listener; Console.WriteLine("消费者启动成功...");
} public void Stop()
{
_connection.Stop();
_connection.Close();
_connection.Dispose();
} #endregion #region Private Methods /// <summary>
/// 消息监听处理
/// </summary>
/// <param name="message"></param>
private void _consumer_Listener(IMessage message)
{
ITextMessage textMessage = message as ITextMessage;
Console.WriteLine("{0}-{1}", DateTime.Now, textMessage.Text);
} private void _connection_ExceptionListener(Exception exception)
{
Console.WriteLine("生产者发生异常:{0}", exception);
} #endregion
}

ActiveMQ NMS使用过程中的一点经验的更多相关文章

  1. 【UEFI】---记录一次debug过程中的调试经验

    最近在调试一次SMBIOS的动态更新以及I2c设备的配置读取时,遇到了很多问题,特此总结: 1. 第一个是调试一个I2c设备的时候,遇到了一个很奇怪的问题,也由此问题总结了下SMBUS模块的知识,如下 ...

  2. storm - 使用过程中的一点思考

    引子 这几天为了优化原有的数据处理框架,比较系统的学习了storm的一些内容,整理一下心得 1. storm提供的是一种数据处理思想,它不提供具体的解决方案 storm的核心是topo的定义,而top ...

  3. SQL Server 2017 安装过程中的一点说明(有点意思)

    会提到:“安装程序无法与下载服务器联系.请提供 Microsoft 机器学习服务器安装文件的位置,然后单击“下一步”.可从以下位置下载安装文件” 的解决方案 安装过程和2016大体一致,机器学习这款更 ...

  4. 关于teleport_pro使用过程中的一点疑惑

    在我新建工程的时候,有两个选项,一个是"new project wizard"另一个是"new project",然后就纠结了,我应该使用那个呢? 使用第一个的 ...

  5. ubuntu安装过程中遇到问题小结

    一.下载 官网下载地址:https://www.ubuntu.com/download/desktop/contribute?version=16.04.4&architecture=amd6 ...

  6. keil程序在外部RAM中调试的问题总结(个人的一点经验总结)

    keil程序在内部RAM调试的基本步骤网上已经有非常多了,我就不再赘述,大家能够在网上搜到非常多. 可是有些时候内部RAM并不够用,这就须要将程序装入外部RAM中调试,而在这个过程中可能会出现各种各样 ...

  7. SpringCloud整合过程中jar依赖踩坑经验

    今天在搭建SpringCloud Eureka过程中,一直在报pom依赖错误,排查问题总结如下经验. 1.SpringBoot整合SpringCloud两者版本是有严格约束的,详细见SpringBoo ...

  8. VMware虚拟机升级过程中遇到的一点问题

    在将VWware由9.0升级到10.0的过程中,出现如下图的错误:        failed to create the requested registry key Key:Installer e ...

  9. 封装RabbitMQ.NET Library 的一点经验总结

    这篇文章内容会很短,主要是想给大家分享下我最近在做一个简单的rabbitmq客户端类库的封装的经验总结,说是简单其实一点都不简单.为了节省时间我主要按照Library的执行顺序来介绍,在你看来这里仅仅 ...

随机推荐

  1. Virtualbox之Ubuntu虚拟机网络访问设置

    在本机(Win7)中 利用VirtualBox安装了一个Ubuntu虚拟机,由于使用桥接,所以本机和虚拟机处于同一个网络局域网下,,主机能访问虚拟机.可是在Ubuntu更新软件的时候才发现不能联网.首 ...

  2. C++中出现的计算机术语4

    adaptor(适配器) 一种标准库类型.函数或迭代器,使某种标准库类型.函数或迭代器的行为类似于第二种标准库类型.函数或迭代器.系统提供了三种顺序容器适配器:stack(栈).queue(队列)以及 ...

  3. JSON数据转换方法 parse()和stringify()

    将对象转换成JSON格式的文本数据 var str = JSON.stringify(data); 将对象转换成JSON对象的方法 var data = JSON.parse(str);

  4. at System.Data.EntityClient.EntityConnection.GetFactory(String providerString)

    最近在做一个WinForm的项目. 使用vs2013开发. 数据库使用的是oracle. 在本地写了一个webservice .测试正常.发布到服务器的时候.就是提示了错误. 打开服务器上的日志.看到 ...

  5. 【C#版本详情回顾】C#3.0主要功能列表

    隐式类型的本地变量和数组 在与本地变量一起使用时,var 关键字指示编译器根据初始化语句右侧的表达式推断变量或数组元素的类型 对象初始值设定项 支持无需显式调用构造函数即可进行对象初始化 集合初始值设 ...

  6. Oracle中的Union、Union All、Intersect、Minus

    Oracle中的Union.Union All.Intersect.Minus  众所周知的几个结果集集合操作命令,今天详细地测试了一下,发现一些问题,记录备考. 假设我们有一个表Student,包括 ...

  7. Carmichael Numbers - PC110702

    欢迎访问我的新博客:http://www.milkcu.com/blog/ 原文地址:http://www.milkcu.com/blog/archives/uva10006.html 原创:Carm ...

  8. Lex Yacc手册

    Python Lex Yacc手册 本文是PLY (Python Lex-Yacc)的中文翻译版.转载请注明出处.这里有更好的阅读体验. 如果你从事编译器或解析器的开发工作,你可能对lex和yacc不 ...

  9. greenlet微线程

    Greenlet简介 一个 “greenlet” 是一个很小的独立微线程.可以把它想像成一个堆栈帧,栈底是初始调用,而栈顶是当前greenlet的暂停位置.你使用greenlet创建一堆这样的堆 栈, ...

  10. 安装dynamics CRM 2013提示“实例名称必须与计算机名称相同”

    在安装CRM 2013的时候,最后一步一直提示“实例名称必须与计算机名称相同”. 原因是在安装数据库之后,我更改了计算机名称.因此就导致了可这个错. 在安装数据库的时候,数据库会记住计算机的名称,用 ...