Netty构建分布式消息队列(AvatarMQ)设计指南之架构篇

  目前业界流行的分布式消息队列系统(或者可以叫做消息中间件)种类繁多,比如,基于Erlang的RabbitMQ、基于Java的ActiveMQ/Apache Kafka、基于C/C++的ZeroMQ等等,都能进行大批量的消息路由转发。它们的共同特点是,都有一个消息中转路由节点,按照消息队列里面的专业术语,这个角色应该是broker。整个消息系统通过这个broker节点,进行从消息生产者Producer到消费者Consumer的消息路由。当然了,生产者和消费者可以是多对多的关系。消息路由的时候,可以根据关键字(专业的术语叫topic),进行关键字精确匹配、模糊匹配、广播方式的消息路由。

  简单来说,一个极简的分布式消息队列系统主要的构成模块有:

  Broker:简单来说就是消息队列服务器实体。

  Producer:消息的生产者,主要用来发送消息给消费者。

  Consumer:消息的消费者,主要用来接收生产者的消息。

  Routing Key:路由关键字(Topic),主要用来控制生产者和消费者之间的发送与接收消息的对应关系。

  Channel:消息通道,在客户端的每个连接里,可建立多个channel,每个channel代表一个会话任务。

  到此为止,我们明白了一个分布式消息队列系统的主要构成模块,现在本人就通过Netty,这个优秀的Java NIO网络通讯框架,构建一个支持上述应用场景的分布式消息队列系统,本人把其命名为AvatarMQ。后续我会基于这个开源项目,连载出基于Netty构建分布式消息队列系统系列相关的文章,阐明主要的设计思路、组织结构、模块划分依据、类图结构等等。为了说明方便,后续本文中,如果没有特殊说明,有涉及基于Netty构建的分布式消息队列系统,就是指代AvatarMQ。由于整个开源项目涉及的代码量比较多,所以希望大家在本人编写系列博客文章的基础上,耐心地理解、分析其中的代码模块,相信一定不会让您失望!

  AvatarMQ基于Netty,所以首先,你要能清楚的理解Netty是什么?它能做什么?有兴趣的朋友可以关注一下Netty项目的官网(http://netty.io/),上面有很详细的入门文章介绍。虽然都是英文的,但是这些一手的资料更具权威性,值得花时间深入研究探索,毕竟现在流行的云计算、大数据领域成功的开源项目比如Hadoop、Storm等等,网络通信层这块全部依赖Netty,可见Netty的功能强大。

  基于Netty可以开发定制高性能、高可靠性的Java企业级服务端应用,而本文是我,在继利用Netty构建高性能RPC服务器系列文章之后,又一个基于Netty开发的分布式消息队列系统(AvatarMQ)。此外AvatarMQ还大量使用了Java多线程的相关类库。所以希望在此之前,大家能回忆复习一下,这样理解起来会更加得心应手、事半功倍。

  AvatarMQ是基于Netty构建的分布式消息队列系统,支持多个生产者和多个消费者之间的消息路由、传递。主要特性如下:

  • AvatarMQ基于Java语言进行编写,网络通讯依赖Netty。
  • 生产者和消费者的关系可以是一对多、多对一、多对多的关系。
  • 若干个消费者可以组成消费者集群,生产者可以向这个消费者集群投递消息。
  • 消费者集群对于有共同关注点的消费者支持消息的负载均衡策略。
  • 支持动态新增、删除生产者、消费者。
  • 目前仅仅支持关键字的精确匹配路由,后续会逐渐完善。
  • 消息队列服务器Broker基于Netty的主从事件线程池模型开发设计。
  • 网络消息序列化采用Kryo进行消息的网络序列化传输。
  • Broker的消息派发、负载均衡、应答处理(ACK)基于异步多线程模型进行开发设计。
  • Broker消息的投递,目前支持严格的消息顺序。其中Broker还支持消息的缓冲派发,即Broker会缓存一定数量的消息之后,再批量分配给对此消息感兴趣的消费者。

  AvatarMQ项目开源网址:https://github.com/tang-jie/AvatarMQ

  整个开源项目依赖的jar包请参考:https://github.com/tang-jie/AvatarMQ/blob/master/nbproject/project.properties

  另外,值得注意的是:

  AvatarMQ使用的Netty是基于4.0版本(下载地址:http://dl.bintray.com/netty/downloads/netty-4.0.37.Final.tar.bz2)。

  消息序列化使用的Kryo是基于kryo-3.0.3版本(下载地址:https://github.com/EsotericSoftware/kryo/releases/tag/kryo-parent-3.0.3)。

  请大家自行去官网下载使用。

  现在,现在言归正传,我们先来看下整合AvatarMQ项目的软件架构图:

  

  从上述图例中,我们可以很清楚的看到:生产者和消费者之间是通过Broker进行消息的路由和转发,同时Broker还负责应答生产者和接收消费者的处理应答。

  在了解了,整个AvatarMQ的组织架构之后,我们再来实际运行一下AvatarMQ!

  首先,先启动一下Broker服务器(对应代码:https://github.com/tang-jie/AvatarMQ/blob/master/src/com/newlandframework/avatarmq/spring/AvatarMQServerStartup.java

  如果一切正常,终端控制台会打印如下输出:

  

  接着,我们就来实际验证一下AvatarMQ的消息推送功能。

  1、生产者发送1条消息给关注这条消息的消费者。我们先启动消费者,再启动生产者。

  其中消费者1的测试代码(AvatarMQConsumer1.java)如下所示:

package com.newlandframework.avatarmq.test;

import com.newlandframework.avatarmq.consumer.AvatarMQConsumer;
import com.newlandframework.avatarmq.consumer.ProducerMessageHook;
import com.newlandframework.avatarmq.msg.ConsumerAckMessage;
import com.newlandframework.avatarmq.msg.Message;

/**
 * @filename:AvatarMQConsumer1.java
 * @description:AvatarMQConsumer1功能模块
 * @author tangjie<https://github.com/tang-jie>
 * @blog http://www.cnblogs.com/jietang/
 * @since 2016-8-11
 */
public class AvatarMQConsumer1 {

    private static ProducerMessageHook hook = new ProducerMessageHook() {
        public ConsumerAckMessage hookMessage(Message message) {
            System.out.printf("AvatarMQConsumer1 收到消息编号:%s,消息内容:%s\n", message.getMsgId(), new String(message.getBody()));
            ConsumerAckMessage result = new ConsumerAckMessage();
            result.setStatus(ConsumerAckMessage.SUCCESS);
            return result;
        }
    };

    public static void main(String[] args) {
        AvatarMQConsumer consumer = new AvatarMQConsumer("127.0.0.1:18888", "AvatarMQ-Topic-1", hook);
        consumer.init();
        consumer.setClusterId("AvatarMQCluster");
        consumer.receiveMode();
        consumer.start();
    }
}

  生产者1的测试代码(AvatarMQProducer1.java)如下所示,其含义是发送1条消息,给关注“AvatarMQ-Topic-1”主题的消费者:

package com.newlandframework.avatarmq.test;

import com.newlandframework.avatarmq.msg.Message;
import com.newlandframework.avatarmq.msg.ProducerAckMessage;
import com.newlandframework.avatarmq.producer.AvatarMQProducer;
import org.apache.commons.lang3.StringUtils;

/**
 * @filename:AvatarMQProducer1.java
 * @description:AvatarMQProducer1功能模块
 * @author tangjie<https://github.com/tang-jie>
 * @blog http://www.cnblogs.com/jietang/
 * @since 2016-8-11
 */
public class AvatarMQProducer1 {

    public static void main(String[] args) throws InterruptedException {
        AvatarMQProducer producer = new AvatarMQProducer("127.0.0.1:18888", "AvatarMQ-Topic-1");
        producer.setClusterId("AvatarMQCluster");
        producer.init();
        producer.start();

        System.out.println(StringUtils.center("AvatarMQProducer1 消息发送开始", 50, "*"));

        for (int i = 0; i < 1; i++) {
            Message message = new Message();
            String str = "Hello AvatarMQ From Producer1[" + i + "]";
            message.setBody(str.getBytes());
            ProducerAckMessage result = producer.delivery(message);
            if (result.getStatus() == (ProducerAckMessage.SUCCESS)) {
                System.out.printf("AvatarMQProducer1 发送消息编号:%s\n", result.getMsgId());
            }

            Thread.sleep(100);
        }

        producer.shutdown();
        System.out.println(StringUtils.center("AvatarMQProducer1 消息发送完毕", 50, "*"));
    }
}

  首先我们先来启动消费者,如果一切正常,控制台输出结果为:

  这个时候我们再运行生产者,发送一条消息给消费者。启动生产者之后,控制台输出结果如下:

  那现在,我们切回去看下消费者是否收到生产者的消息了呢?

  非常正确,我们的消费者果然收到了生产者发送过来的消息。

 

  2、生产者发送1条消息给不关注这条消息的消费者。

  首先说明的是,代码样例还是基于上述的AvatarMQConsumer1.java、AvatarMQProducer1.java。只不过这次是生产者发送的主题改成:“AvatarMQ-Topic-Test”,消费者关注的主题改成“AvatarMQ-Topic-1”。然后依次启动消费者、生产者。下面是实际的运行情况:

  生产者成功发送消息:

  那按照要求,消费者应该无法收到生产者的这条消息,实际情况是不是这样呢?事实胜于雄辩,看如下截图所示:

  消费者依然处理启动监听状态,说明完全符合我们的预期。

 

  3、生产者发送N条消息(这里是发送100条消息)给一个消费者集群(有2个消费者组成,并且这2个消费者关注的消息主题topic是相同的)。

  我们先启动2个消费者,再启动生产者。消费者代码参考:

package com.newlandframework.avatarmq.test;

import com.newlandframework.avatarmq.consumer.AvatarMQConsumer;
import com.newlandframework.avatarmq.consumer.ProducerMessageHook;
import com.newlandframework.avatarmq.msg.ConsumerAckMessage;
import com.newlandframework.avatarmq.msg.Message;

/**
 * @filename:AvatarMQConsumer2.java
 * @description:AvatarMQConsumer2功能模块
 * @author tangjie<https://github.com/tang-jie>
 * @blog http://www.cnblogs.com/jietang/
 * @since 2016-8-11
 */
public class AvatarMQConsumer2 {

    private static ProducerMessageHook hook = new ProducerMessageHook() {
        public ConsumerAckMessage hookMessage(Message message) {
            System.out.printf("AvatarMQConsumer2 收到消息编号:%s,消息内容:%s\n", message.getMsgId(), new String(message.getBody()));
            ConsumerAckMessage result = new ConsumerAckMessage();
            result.setStatus(ConsumerAckMessage.SUCCESS);
            return result;
        }
    };

    public static void main(String[] args) {
        AvatarMQConsumer consumer = new AvatarMQConsumer("127.0.0.1:18888", "AvatarMQ-Topic-2", hook);
        consumer.init();
        consumer.setClusterId("AvatarMQCluster2");
        consumer.receiveMode();
        consumer.start();
    }
}

  生产者代码参考(目的是发送100条消息)给消费者集群。

package com.newlandframework.avatarmq.test;

import com.newlandframework.avatarmq.msg.Message;
import com.newlandframework.avatarmq.msg.ProducerAckMessage;
import com.newlandframework.avatarmq.producer.AvatarMQProducer;
import org.apache.commons.lang3.StringUtils;

/**
 * @filename:AvatarMQProducer2.java
 * @description:AvatarMQProducer2功能模块
 * @author tangjie<https://github.com/tang-jie>
 * @blog http://www.cnblogs.com/jietang/
 * @since 2016-8-11
 */
public class AvatarMQProducer2 {

    public static void main(String[] args) throws InterruptedException {
        AvatarMQProducer producer = new AvatarMQProducer("127.0.0.1:18888", "AvatarMQ-Topic-2");
        producer.setClusterId("AvatarMQCluster2");
        producer.init();
        producer.start();

        System.out.println(StringUtils.center("AvatarMQProducer2 消息发送开始", 50, "*"));

        for (int i = 0; i < 100; i++) {
            Message message = new Message();
            String str = "Hello AvatarMQ From Producer2[" + i + "]";
            message.setBody(str.getBytes());
            ProducerAckMessage result = producer.delivery(message);
            if (result.getStatus() == (ProducerAckMessage.SUCCESS)) {
                System.out.printf("AvatarMQProducer2 发送消息编号:%s\n", result.getMsgId());
            }

            Thread.sleep(100);
        }

        producer.shutdown();
        System.out.println(StringUtils.center("AvatarMQProducer2 消息发送完毕", 50, "*"));
    }
}

  我们依次启动消费者AvatarMQConsumer2两次,这个时候终端控制台依次输出:

  这个时候我们再启动生产者,运行截图如下:

  说明生产者发送了100条消息出去,看下我们消费者1接收的情况:

  继续看下我们的消费者2,消息接收的情况,截图如下:

  最终统计一下,消费者1,接收的消息编号都是奇数,一共50个。消费者2,接收到的消息编号都是偶数,一共50个。两个消费者接收的消息总数加起来,刚好等于生产者发送的消息总数100个,完全符合我们的预期!另外消费者1、消费者2都收到了来自生产者的消息,说明Broker进行了消息的路由传递。

  4、多个生产者和多个消费者的消息传递,以及动态新增、删除生产者、消费者。

  这个就交给大家自行测试了,由于篇幅有限,在此本人就不一一阐述。

 

  到目前为止,相信大家对于AvatarMQ所具备的基本功能,有了一个大致的印象。当然,AvatarMQ还有一些美中不足,比如:

  • 不支持消息的刷盘存储,可能由于系统Crash,造成消息的丢失。后续需要接入一个存储系统(基于Java NIO),保证消息的持久序列化。
  • AvatarMQ的生产者、消费者模块,要进一步支持,断网重连Broker的功能,确保在Broker重启的情况下,把在途的消息继续发送、接收完毕。
  • Broker单点的问题,根据高可用性集群HA(High Available)的标准,Broker也要有主节点和从节点机制。在主节点宕机的情况,从节点要能灰度过渡,不至于Broker主节点宕机,整个AvatarMQ消息系统陷入瘫痪状态。
  • 消息应答失败,还未支持重试功能。
  • 当然还有一些未知的bug,有待发现和修复。
  • AvatarMQ的处理性能,未经历过生产系统实际检验,暂时无法保证其安全和可靠性。

  由于代码编写、测试等等工作,都是本人利用工作之余的时间完成,时间点上比较仓促。加上本人的技术水平有限,难免有说的不对及写得不好的地方,或者其中应该有更好的解决方案。欢迎广大同行、爱好者在线下进行学习交流,有什么宝贵的建议和观点,恳请批评指正,不吝赐教。虽然AvatarMQ和业界主流、久经考验的消息队列系统,在处理性能、可靠性上,肯定还有不小的差距。但是可以基于此,加深对分布式消息队列的理解,做到知其然知其所以然,何乐而不为?

  最后,本人后续会逐渐推出“基于Netty构建的分布式消息队列系统(AvatarMQ)”,架构设计、原理分析的详解连载文章,敬请期待!

  PS:目前AvatarMQ已经开源,整个项目托管到github,对应的网址为:https://github.com/tang-jie/AvatarMQ,欢迎有兴趣的同行朋友、爱好者关注支持。如果觉得还不错,可以点击Star收藏、关注。当然,你还可以点击推荐本文,也算是对我辛苦付出的一点支持和回报,谢谢大家!

posted @ 2016-08-25 23:17  Newland  阅读(17959)  评论(9编辑  收藏  举报