本文共 10796 字,大约阅读时间需要 35 分钟。
前言
Artemis是一款基于Netty NIO的高性能消息中间件,它的前身为JBoss的HornetQ,2015捐献给了Apache ActiveMQ社区,并命名为Apache Artemis。本文将对Artemis的架构做一个简单的解析,将Artemis的架构拆分为两块:Artemis Broker的解析(也可以说是单点实例的Artemis消息代理内部运行架构解析)和Artemis高可用架构解析。
1.Artemis Broker
如下图为Artemis Broker的架构图,Artemis Broker按其所提供的核心功能及外部工具与接口两部分构成。将Broker做出这样的划分实际上为了本文的结构考虑,核心功能和外部工具与接口两部分都是Artemis Broker的组成部分。1.1 外部工具与接口
1.1.1 命令行工具 Artemis为用户提供了功能丰富的命令行工具,用户可以通过运行Artemis的${INSTANCE}/bin目录下的Artemis脚本加上对应的参数来调用命令行工具,如运行Artemis Broker实例;${INSTANCE}/bin/Artemis run
Artemis命令行工具构建于Airline之上,Airline是一个基于Java注解解析命令行结构的框架。Artemis中所有的命令工具都实现org.apache.activemq.artemis.cli.commands.Action接口。基于Action的命令行工具层级结构:
HelpAddress (address组的帮助)HelpData (data组的帮助)Mask (屏蔽密码操作)HelpQueue (queue组的帮助命令)ActionAbstract (org.apache.activemq.artemis.cli.commands) Migrate1X (迁移1.x版本配置命令) XmlDataImporter (导入XML消息数据命令) InputAbstract (org.apache.activemq.artemis.cli.commands) ConnectionAbstract (org.apache.activemq.artemis.cli.commands.messages) AbstractAction (org.apache.activemq.artemis.cli.commands) QueueAbstract (org.apache.activemq.artemis.cli.commands.queue) UpdateQueue (更新一个队列命令) CreateQueue (创建一个队列或topic命令) DeleteQueue (删除队列命令) AddressAbstract (org.apache.activemq.artemis.cli.commands.address) ShowAddress (获取地址信息命令) CreateAddress (创建地址命令) UpdateAddress (更新地址命令) DeleteAddress (删除地址命令) StatQueue (打印出与队列相关的基本统计数据命令) DestAbstract (org.apache.activemq.artemis.cli.commands.messages) Browse (浏览指定实例消息命令) Producer (发送消息到指定实例命令) Consumer (消费指定实例命令) UserAction (org.apache.activemq.artemis.cli.commands.user) PasswordAction (org.apache.activemq.artemis.cli.commands.user) ResetUser (重置用户密码或角色命令) AddUser (添加一个新用户命令) RemoveUser (删除已存在用户命令) ListUser (列出已存在用户命令) Create (创建Artemis实例命令) Configurable (org.apache.activemq.artemis.cli.commands) Stop (停止Artemis实例命令) Kill (杀死以--allow-kill方式启动的Artemis实例命令) DataAbstract (org.apache.activemq.artemis.cli.commands.tools) LockAbstract (org.apache.activemq.artemis.cli.commands.tools) EncodeJournal (为一组日志文件的编码设置为内部数据格式命令) Run (运行Artemis实例命令) OptionalLocking (org.apache.activemq.artemis.cli.commands.tools) PerfJournal (计算现在使用磁盘的journal-buffer-timeout设置的建议值命令) DBOption (org.apache.activemq.artemis.cli.commands.tools) PrintData (打印数据记录信息命令(注意:在生产服务器运行时不要使用)) XmlDataExporter (以XML格式导出所有消息数据命令) CompactJournal (压缩非运行服务器的日志命令) DecodeJournal (将日志的内部格式解码为新的日志文件集命令)HelpUser (user组的帮助命令)HelpAction (全局帮助命令)
1.1.2 RESTful API
Artemis REST接口允许通过简单的REST/HTTP接口来调用Artemis功能。Artemis的REST接口是基于Artemis JMS API之上构建的,也就是说REST接口其实暴露的是JMS的功能。使用Artemis REST接口可以使用大部分JMS消息功能,如可以非常简单的通过发送和接收HTTP消息来实现生产和消费功能,或者基于JMS消息的管理接口。Artemis REST接口依赖RESTEasy项目构建,部署时需要将其打成WAR包,只能允许在Servlet容器中。安装Artemis REST接口按照其是嵌入式(如在Wildfly中部署),还是使用WAR包独立启动它们的安装配置是有些区别的,需要特别注意。
1.1.3 JMX
Artemis提供了基于JMX(Java Management Extensions,Java管理扩展)的管理API,通过域'org.apache.activemq.artemis'来注册资源,并通过MBean接口来对外提供管理API。Artemis的JMX与使用其他Java应用提供的JMX相同,都可以通过反射或创建MBean代理来实现访问。1.1.4 管理控制页面
为了方便用户能直接通过页面对Artemis进行管理和监控,Artemis提供了一个默认的管理控制台。这个控制台由Hawt.io提供支持的,Hawt.io是一个可以插入式HTML5面板。Artemis的管理控制台使用Jolokia作为JMX中介,在本地JMX上构建基于http协议使用JSON作为数据格式的外部接口,可以看作是远程访问JMX MBean的另一种实现方式。通过控制台页面,用户能直观的对Artemis的运行状况进行监控,如监控连接、会话、队列等情况,并且能通过接口对Artemis进行直接控制操作如配置管理、关闭队列、创建地址等等。可以说JMX提供的管理API在控制台页面非常简单的通过浏览器就能调用。
1.2 Artemis核心功能
1.2.1 接入层(Acceptor) Artemis提供了两种接收器:InVMAcceptor
InVMAcceptor是在同一JVM进程中使用的接收器,可以在broker.xml配置文件中通过<acceptor>元素来声明启动一个VMAcceptor:
<acceptor name="in-vm">vm://clientId?create=false</acceptor>
"vm:"代表要使用VMAcceptor。“clientId”为服务器唯一ID, 也就是用于标识指定的Artemis Broker实例。InVMAcceptor不同于与我们通常认知的基于监听指定端口接收来自网络的请求,它通过模拟监听模型来创建虚拟的通讯体系。同样能建立连接通道,也有消费者和生产者的概念,但是需要注意的,前提条件是必须与Artemis服务器实例处于同一JVM进程中才能建立这个虚拟的连接通道。基于InVMAcceptor所实现的服务,实际上是直接通过调用Artemis中对应的JaveBean而实现的功能。
NettyAcceptor
NettyAcceptor是基于Netty的NIO构建,这也是Artemis高性能原因之一。
NettyAcceptor可以支持单端口多协议功能,如CORE、AMQP、STOMP、MQTT等协议可以使用同一个Acceptor也可以指定不同的Acceptor。同一个Acceptor还可以同时支持HTTP、Websocket协议。这都是归功于基于ChannelHandler消息拦截与处理器,通过拦截每条消息,根据其协议交个不同的handler进行处理。
1.2.2 安全层(Security)
Artemis提供了基于角色的安全模型。为用户的连接、订阅、发送消息等操作权限提供了保障。Artemis通过基于JAAS(Java验证和授权API)的安全管理器ActiveMQJAASSecurityManager进行用户授权。对于如何获取用户信息取决于使用哪种登录模块。
来宾登入模块
来宾登入模块(GuestLoginModule)应用于没有用户凭证的用户访问Artemis。来宾模型通常会与其他登入模型结合使用,为来宾用户定义一个统一的权限,比如来宾用户只有查看权限,没有消费和发布消息的权限。
属性登录模块
属性登入模型(ProperteisLoginModule)通过存储用户数据的属性文件,来为模块提供对应的数据。Artemis默认使用属性登入模块来为其提供简单的用户数据存储。
默认这两个属性文件分别为:artemis-users.properties用于存储用户密码对,artemis-roles.properties用来存储指定权限对应的用户列表。
LDAP登录模块
LDAP登录模块(LDAPLoginModule)基于LDAP(轻量级目录访问协议)使用树形结构来存储用户数据的。使用这个登入模块需要部署一套X.500目录服务器,通过LDAPLoginModule与目录服务器建立连接,用此服务器中的数据与用户的登入数据进行对比来执行身份验证和授权动作。
LDAPLoginModule兼容ActiveMQ 5.x的用户数据结构,它会在允许的范围内将用户数据转换成Artemis所支持的数据。
Kerberos认证
Artmies基于Krb5LoginModule实现了Kerberos登入模块来获取用户凭证,要使用Kerbeoros认证必须要先部署kerberos的基础环境。
1.2.3 协议管理(Protocol Management)
在代码层面其实协议管理模块是先于安全管理被调用的,但是安全管理有在处理具体的协议逻辑的时候被调用,比如连接、发送消息等。这两个模块按代码层次来说其实是纠缠在一起的,不好说谁先谁后。但是按架构层面理解,是应该安全管理在前,有权限才能继续往下继续处理,所以这里把安全层放在协议管理之前。Artemis接入层接收到消息后,对于NettyAcceptor,它启动Netty服务的时注册了一个Handler,协议解析拦截器ProcolDecoder,它会解析客户端使用何种协议与Artemis通讯。
Artemis解析完消息使用何种协议后,通过查找对应协议的ProtocolManager来注册指定协议的编解码处理器和协议逻辑处理器。以MQTT协议为例,MQTTProtocolManager会为在Netty的Handler链中注册MQTT协议的编码器MqttEncoder、×××MqttDecoder、和协议处理器MQTTProtocolHandler。如此Artemis基于ProcolDecoder的动态装载对应协议处理Handler,实现了只需一个端口就能处理多个协议通讯的功能。
1.2.4 服务层(Server)
本文将Artemis的服务层定义为,囊括了非协议处理特定逻辑之外所有与协议处理通用的逻辑之集合(持久化除外,之所以把持久化排查在服务层之外,是因为一般我们在讨论架构的时候通常都会把持久化层抽象为单独的模块,与业务处理逻辑隔离)。如下将主要讲述服务层的三部分功能,分别为地址、队列、检测。
1.2.4.1 检测
对于检测需特别说明下,在代码层次中,各类检测功能其实分散嵌入在对应的模块,并没有在代码层次进行独立封装。此处将检测独立作为一个模块来描绘,是为了方便对各类检测功能进行集中说明,将其嵌入进其他模块进行讲解太过分散,让人不太容易注意到对应的检测功能;
Artemis提供的检测包含:僵尸连接检测、代理健康检测、慢消费者检测、消息超时检测等。
僵尸连接检测
当客户端崩溃或者客户端退出但是未关闭连接,这类连接称为僵尸连接。如果服务端没有主动的连接检测,将会可能使服务端长时间持有大量的僵尸连接,导致服务端资源泄漏,甚至会由于资源耗尽导致服务器崩溃。
检测功能由RemotingServiceImpl中的内部类FailureCheckAndFlushThread定义,FailureCheckAndFlushThread作为一个独立的线程定时轮询所有的连接,检查其连接在指定时间内是否有接收到心跳包,如果没有判定为超时触发连接关闭动作。
这里的连接超时时间可以由客户端在连接到Artemis Broker时,客户端定义好作为协议参数传给Broker,也可以通过Broker中配置connection-ttl-override属性作为全局值使用。
代理健康检测
为了防止Artemis为外部提供坏的服务,Artemis提供服务健康状况自检功能。什么是坏的服务呢,比如客户端能连接到Artemis但是一直发送消息超时,因为磁盘IO阻塞,导致客户端发送的消息阻塞在持久化操作,无法响应应答消息给客户端。
Artemis会在监控检测线程中对队列投递、持久化操作、分页操作进行定期测量,如果响应超时,代理会被认定为不稳定,会根据代理中配置的超时处理策略critical-analyzer-policy来进行对应的操作。默认策略为LOG会对超时情况进行日志输出,也可以将策略配置为HALT暂停服务器、SHUTDOWN停止服务器。
代理监控检测代码封装在CriticalAnalyzer接口中,其实现类有两个:EmptyCriticalAnalyzer和CriticalAnalyzerImpl,代理健康检测功能都在CriticalAnalyzerImpl实现,对于EmptyCriticalAnalyzer是个空类,就是什么都不做的类,仅仅只是继承了CriticalAnalyzer接口,其方法都是什么都不做的空方法。EmptyCriticalAnalyzer是干什么用的,只是作为用户配置关闭代理监控检测功能时调用这个空检测,并且为以后可能会添加检测功能的模块在其初始化的时候传入这个空检测为其插入对应检测功能做预留。
消息超时检测
如果发送到代理中的消息有设置过期时间,消息在存活时间内还未被投递,服务器将丢弃该消息。Artemis也支持定义一个逾期地址,消息逾期后逾期消息会被发送到这个逾期地址中。
逾期消息的功能代码封装于PostOfficeImpl中的内部类ExpiryReaper,逾期检测初始化代理阶段被开启运行。它的逻辑很简单就是通过地址管理获取所有的绑定关系,从绑定中获取队列。遍历所有队列,让各个队列启动自己的扫描线程来扫描队列中的所有消息是否有逾期消息。
慢消费者检测
首先说下慢消费者会对消息代理造成什么样的影响,如果一个队列的消费者由于某些原因消费消息的速度很慢,恰好发送端发送消息比较快,这样就会导致消息在队列中堆积。队列中消息堆积到一定程度后会开启分页功能(这里假设配置了分页)。分页操作对系统资源的损耗很大,存在大量的分页操作可能会导致消息代理的健康检测失败,导致自检测关闭服务。
慢消费者检测就是为了解决这个问题,用户可以自定义一个消费速率阀值,当消费者的消费速率低于这个阀值时,会关闭消费者连接,代理删除其相关订阅队列和队列中所有消息来释放宝贵的服务器资源。
慢消费者检测逻辑封装在QueueImpl的内部类SlowConsumerReaperRunnable中。
1.2.4.2 地址
在Artemis中地址代表了一个消息的端点,每个地址都必须有全局唯一的名称。一个地址可以绑定多个队列,并且可以定义一个路由类型。
Artemis中有两类路由类型,路由类型决定了消息是如何路由到与地址关联的队列的:
Anycast(任播):以点对点的方式匹配地址中的单个队列,通常如果需要负载均衡消费发送到某个地址的消息就用这种类型的地址;
Multicast(广播):以发布/订阅的方式匹配地址中的每个队列。 Artemis中地址相关的基本信息存储在AddressInfo中,通过AddressControlImpl来获取与地址相关一些信息。Binding是用来过滤地址和队列间的绑定关系的。而地址和队列间的消息路由则是封装在PostOfficeImpl中。对于Binding的实现主要由两类实现:LocalQueueBinding和RemoteQueueBindingImpl。LocalQueueBinding主要是针对同节点的绑定关系的路由,RemoteQueueBindingImpl负责集群中不同节点间的绑定关系路由。
1.2.4.3 队列
队列可以认为是一个消息的聚集地,所有待消费的消息都存储在队列中,队列会管理并对消息进行投递。Artemis为队列也定义了两种路由类型,队列的路由类型同地址路由路由类型分为Anycast(任播)和Multicast(广播)。队列的Anycast(任播)路由类型表示多个消费者可以以负载均衡的方式消费队列中的消息;队列的Multicast(广播)路由类型表示消费者以订阅的方式消费消息。
Artemis中为了提升消费的性能,队列中的消息都存储在内存中。同时为了防止某个队列占用过多的内存,导致系统内存资源耗尽,Artemis支持为每个队列定义队列允许使用的最大内存。当队列中堆积消息的大小超过设定的内存阀值,可以触发相应的机制来避免内存资源过量消耗,这是Artemis的一组自我保护机制。Artemis提供了四种策略来手段来处理此种情况:
丢弃消息:当消息数量达到阀值时,Artemis会丢弃消息。客户端无感知。
丢弃消息并向生产者抛出异常:当消息数量达到阀值时,Artemis会丢弃消息,并且向生产者客户端抛出一个异常,客户端可感知。阻塞生产者:当消息数量达到阀值时,Artemis会向生产者发送一条特定消息,让生产者自己阻塞发送,直到Artemis的队列内存释放后。这个功能需要客户端的配合,某些协议的客户端不支持这个功能;消息分页:当消息数量达到阀值时,会触发分页,将内存中的消息分页到磁盘中。这个功能与客户端使用的协议无关,所有协议都可支持,但是触发分页时会消耗磁盘IO并降低消费性能。 以上策略各有优劣,用户可以根据自己实际情况进行选择。Artemis提供了两个特殊队列,这两队列都提供了一下普通队列没有的特殊功能,分别为:
Last-value队列:一个队列被定义为Last-value队列时,任何具有相同属性值的消息只会保留最新的消息,当队列中接收到一个新的消息会去队列中查找是否有遗留的且属性相同的消息,如果有则抛弃遗留消息,将新消息放入队列。Last-Value队列在LastValueQueue中实现。
独占队列:是指一个队列中只允许有一个消费者。这是Artemis为解决顺序消费定义的特殊队列。但是独占队列无法通过增加消费者数量来扩展消费能力。 Artemis提供的Queue接口作为队列的抽象,有两个实现类:QueueImpl:是普通队列和独占队列共有的封装;
LastValueQueue:则为Last-value队列的封装1.2.5 持久化层(Persistence)一个消息中间件的性能,消息持久化的性能优劣是最重要的一环。在计算机中,与CPU、内存、网络效率相比,磁盘IO的效率是远远落后于他们。在应用程序性能优化的场景中,也经常会碰到数据持久化瓶颈,可以说持久化优化是程序扩张之路上不得不解决的问题。
Artemis提供了两种持久方案,一种是针对消息高度优化的并且拥有出色性能的日志存储;另一种是JDBC存储,JDBC存储在2.6.3版本之前还在开发阶段,后续版本会继续完善。下文将对日志存储做一个简单介绍,由于JDBC存储还不完善就不做特别说明。
日志存储
Artemis通过预创建好并且固定大小最初使用0填满的日志文件,对消息的任何增、删、改操作都会记录到日志文件中。写文件的方式为在日志文件末尾追加数据。同时使用日志文件的大小是尽量使文件大小刚好在一个磁盘柱面上,这样为了控制在使用文件最小化磁盘磁头移动,因为磁头移动通常是磁盘中最慢的操作。
Artemis为日志存储提供了三种实现,建议在操作系统支持的情况下选择AIO来与文件系统交互,达到最高性能。
Linux AIO:是基于Linux系统的异步IO库(AIO)来实现与文件系统进行交互,Artemis提供了使用基于C语言封装的调用Linux系统AIO的库,在Artemis中要使用AIO要预先安装好libaio并且运行在Linux 2.6以上的内核版本系统中。使用AIO通常会得到比使用NIO更好的性能,基于AIO的消息持久化日志系统可以达到类比其他消息中间中使用内存存储消息的性能。
Java NIO:使用标准的Java NIO与文件系统进行交互,它提供了非常好的性能。在系统不支持AIO情况下会自动使用NIO作为与文件系统交互的方案。内存映射:支持文件READ_WRITE的内存映射通过系统页面缓存来与文件系统进行交互。这提供了与NIO相当的性能,并且在持久化过程中不会产生堆垃圾,极大的减少了Java垃圾收集的频率与时长。
2.Artemis高可用
Artemis支持当一个或多个服务节点故障后具备将故障服务器节点迁移到健康服务器节点的能力,服务的故障迁移能力是中间件的高可用性的必备条件。单点故障不影响整体程序的服务,也是衡量程序健壮性的一个重要标志。在实际场景中各类后台服务程序都需要考虑高可用,只不过由于各自服务的场景不同所选择的方案有所不同。Artemis提供了两种不同的备份策略来为故障转移提供支持,分别为:主从复制(replication)和共享存储(shared-store)。还有一种默认的策略叫做live-only,这个策略不会提高备份功能,只有单主节点存活。在使用备份时需要注意一个情况,无论是主从复制还是共享存储,它们的备份只对持久化消息有效,对应非持久化消息是无法使用备份功能的。
2.1 主从复制
使用主从复制,主服务器和备份服务器不共享相同的数据目录,所有数据同步都通过网络完成。主服务器收到的消息都会复制到备份服务器。当备份服务器启动时,备份服务器要先全量同步主服务器的数据,并且这个过程虽然不会对当前连接在主服务器的客户端造成阻塞,但是会存在一个时刻备份服务器要确保完全同步了主服务器的数据,这个时刻会阻止所有与持久化相关的操作。并且在数据同步的过程中,备份服务器是完全不可操作的。
当主服务器故障时,备份服务器将接替主服务器对应提供服务。在主备切换过程中,对于是否要进行切换,为了避免脑裂问题,Artemis通过选举策略来尽可能规避脑裂的发生。只有当集群中绝大部分主服务器认为可以进行主备切换时,备服务器才能升级为主服务器对外提供服务,在此之前备份服务器是无法对外提供服务的。
2.2 共享存储
Artemis的共享存储需要一个备份服务器和主服务器都能访问到的共享文件系统。共享存储的优点是主节点和备份节点之间不需要复制同步数据,意味着不会因正常操作期间的复制开销而受到任何性能损失。缺点是需要为它们提供共额外的共享文件系统,并且其持久化性能取决于共享文件系统的性能。
转载于:https://blog.51cto.com/zhangyc/2347998