分布式消息队列是用来高效传输消息的。RocketMQ由四个部分组成。类比于邮政系统,邮政系统:由发件人,收件人,负责暂存和传输的邮局,和负责管理各个邮局的管理机构四个部分组成;RocketMQ相应地对应于Producer,Consumer,Broker和NameServer四部分组成。
Producer:消息生产者,负责产生消息,一般由业务系统负责产生消息
Consumer:消息消费者,负责消费消息,一般由后台系统负责异步消费
Broker:MQ消息服务器(中转消息,用于消息存储与生产消费转发)
NameServer:管理Broker,接受Broker的register和unregister
1.1 角色和概念
启动RocketMQ的顺序为先启动NameServer,后启动Broker,这时消息队列就可以提供服务了,生产者发送消息,消费者接收消息。
除了引子中提到的四个部分外,RocketMQ还有一些其他的概念:
Topic:将相同类型的消息分为一类,称为Topic,发送消息时即表明发送到哪个Topic
Message Queue:Topic内部设置多个Message Queue,以提高并行处理的效率
Push Consumer:Consumer的一种,需要向Consumer对象注册监听
Pull Consumer:Consumer的一种,需要主动请求Broker拉取消息
Producer Group:生产者集合,一般用于发送一类消息
Consumer Group:消费者集合,一般用于接收一类消息进行消费
1.2 源码结构
在apache rocketmq官网:http://rocketmq.apache.org/docs/quick-start/ 下载源码包后,可以看到结构如下: 其中:
rocketmq-broker:主要的业务逻辑,消息收发,主从同步
rocketmq-client:客户端接口,比如生产者和消费者
rocketmq-example:示例
rocketmq-common:公共的数据结构等
rocketmq-distribution: 编译模块,编译输出等
rocketmq-filter:进行Broker过滤不感兴趣的消息传输,减小带宽压力
rocketmq-logappender,rocketmq-logging:日志相关
rocketmq-namesrv: 用于服务协调
rocketmq-openmessaging:对外提供服务
rocketmq-remoting:远程调用接口,封装netty底层通信
rocketmq-srvutil:提供一些公用的工具方法,比如解析命令行参数
rocketmq-store:消息的存储
参照官网的quickstart进行编译:mvn -Prelease-all -DskipTests clean install -U,编译后就可以参照官网运行mq的nameserver和broker了。这里要注意的是:由于我的电脑是Mac,在/User/我的名字/目录下,产生了logs和stores两个新的文件夹。
1.3 常用命令
参照官网的quick start来做,首先启动nameserver,在安装路径下:/Users/yubuyun/Documents/workspace/rocketmq-all-4.3.2/distribution/target/apache-rocketmq
目录下执行下面的命令:
启动nameserver
nohup shbin/mqnamesrv &
tail -f~/logs/rocketmqlogs/namesrv.log
nohup shbin/mqbroker -nlocalhost:9876 &
tail -f~/logs/rocketmqlogs/broker.log
另外本地机器学习的时候注意,不玩了记得把broker和nameserver关掉,省得一直在后台运行服务:
shbin/mqshutdown broker
shbin/mqshutdown namesrv
publicstaticvoidmain(String[] args) {
main0(args);
}
publicstaticNamesrvControllermain0(String[] args) {
try{
NamesrvControllercontroller=createNamesrvController(args);
start(controller);
returncontroller;
} catch(Throwablee) {
e.printStackTrace();
}
returnnull;
}
创建NamesrvController对象
调用start()方法启动NameServer
2.2.1 创建NamesrvController
createNamesrvController()方法大致实现和注释如下:
publicstaticNamesrvControllercreateNamesrvController(String[] args) throwsIOException, JoranException{
// 解析命令行和携带的参数
commandLine=…
// 创建NameServer配置类
finalNamesrvConfignamesrvConfig=newNamesrvConfig();
// 创建网络配置类
finalNettyServerConfignettyServerConfig=newNettyServerConfig();
nettyServerConfig.setListenPort(9876);
// 启动时参数-c指定配置文件
if(commandLine.hasOption(‘c’)) {
Stringfile=commandLine.getOptionValue(‘c’);
… // 将命令行参数保存到namesrvConfig和nettyServerConfig配置类中
}
// 启动时参数-p打印当前加载的配置属性
if(commandLine.hasOption(‘p’)) {
… // 打印配置属性
}
finalNamesrvControllercontroller=newNamesrvController(namesrvConfig, nettyServerConfig);
returncontroller;
}
2.2.3 NameServer路由注册和故障分析
如果之前学习过Netty看到该start()方法的实现就会感到很熟悉,标准的启动过程。
@Override
public void start() {
// 创建DefaultEventExecutorGroup,作为pipeline中编解码处理的handler
this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(
nettyServerConfig.getServerWorkerThreads(),
new ThreadFactory() {
private AtomicInteger threadIndex = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, “NettyServerCodecThread_” + this.threadIndex.incrementAndGet());
}
});
// ServerBootstrap初始化
ServerBootstrap childHandler =
this.serverBootstrap.group(this.eventLoopGroupBoss, this.eventLoopGroupSelector)
.channel(useEpoll() ? EpollServerSocketChannel.class : NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.option(ChannelOption.SO_REUSEADDR, true)
.option(ChannelOption.SO_KEEPALIVE, false)
.childOption(ChannelOption.TCP_NODELAY, true)
.childOption(ChannelOption.SO_SNDBUF, nettyServerConfig.getServerSocketSndBufSize())
.childOption(ChannelOption.SO_RCVBUF, nettyServerConfig.getServerSocketRcvBufSize())
.localAddress(new InetSocketAddress(this.nettyServerConfig.getListenPort()))
.childHandler(new ChannelInitializer() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
.addLast(defaultEventExecutorGroup, HANDSHAKE_HANDLER_NAME,
new HandshakeHandler(TlsSystemConfig.tlsMode))
.addLast(defaultEventExecutorGroup,
new NettyEncoder(),
new NettyDecoder(),
new IdleStateHandler(0, 0, nettyServerConfig.getServerChannelMaxIdleTimeSeconds()),
new NettyConnectManageHandler(),
new NettyServerHandler()
);
}
});
// 端口绑定
ChannelFuture sync = this.serverBootstrap.bind().sync();
}
主要调用的是我们上面创建的NettyRemotingServer对象的start()方法:
public void start() throws Exception {
this.remotingServer.start();
…
}
下面继续分析NamesrvController的start()方法,看下具体是如何启动的:
2.2.2.2 启动
boss线程池:eventLoopGroupBoss,处理客户端连接
worker线程池:eventLoopGroupSelector,处理IO读写操作
可以看出RocketMQ和Netty中的boss和worker线程池的对应关系如下:
public NettyRemotingServer(final NettyServerConfig nettyServerConfig,
final ChannelEventListener channelEventListener) {
// 创建Netty中的启动类ServerBootstrap
this.serverBootstrap = new ServerBootstrap();
// 创建Netty中处理客户端连接的NioEventLoopGroup
this.eventLoopGroupBoss = new NioEventLoopGroup(1, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread(r, String.format(“NettyBoss_%d”, this.threadIndex.incrementAndGet()));
}
});
// 创建Netty中处理IO的NioEventLoopGroup
this.eventLoopGroupSelector = new NioEventLoopGroup(nettyServerConfig.getServerSelectorThreads(), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread(r, String.format(“NettyServerNIOSelector_%d_%d”, threadTotal, this.threadIndex.incrementAndGet()));
}
});
}
下面着重看下NettyRemotingServer类的构造函数部分源码如下:
public boolean initialize() {
// 创建NettyRemotingServer对象,用于处理网络请求
this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);
…
// 创建定时任务:每隔10s扫描broker,移除不可用的broker
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
NamesrvController.this.routeInfoManager.scanNotActiveBroker();
}
}, 5, 10, TimeUnit.SECONDS);
// 创建定时任务:每隔10min打印KV配置信息
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
NamesrvController.this.kvConfigManager.printAllPeriodically();
}
}, 1, 10, TimeUnit.MINUTES);
return true;
}
NamesrvController的初始化initialize()方法如下:
2.2.2.1 初始化
下面着重分析初始化和启动的实现。
public static NamesrvController start(final NamesrvController controller) throws Exception {
// 初始化
boolean initResult = controller.initialize();
// 如果启动失败则shutdown
if (!initResult) {
controller.shutdown();
System.exit(-3);
}
// 虚拟机退出钩子函数
Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable() {
@Override
public Void call() throws Exception {
controller.shutdown();
return null;
}
}));
// 启动
controller.start();
return controller;
}
start()方法实现如下:
2.2.2 启动NameServer
public NamesrvController(NamesrvConfig namesrvConfig, NettyServerConfig nettyServerConfig) {
this.namesrvConfig = namesrvConfig;
this.nettyServerConfig = nettyServerConfig;
this.kvConfigManager = new KVConfigManager(this);
this.routeInfoManager = new RouteInfoManager();
this.brokerHousekeepingService = new BrokerHousekeepingService(this);
this.configuration = new Configuration(
log,
this.namesrvConfig, this.nettyServerConfig
);
this.configuration.setStorePathFromConfig(this.namesrvConfig, “configStorePath”);
}
可以看见创建NamesrvController的过程就是解析和加载参数的过程,NamesrvController的构造函数如下:
可以看出启动主要做了两件事:
在第一小节讲解了如何使用命令行来启动NameServer,这里分析下NameServer后台启动过程。启动类NamesrvStartup源码如下:
2.2 NameServer启动分析
NameServer本身的高可用可通过部 署多台 NamesServer服务器来实现,但彼此之间 互不通信,也就是 NameServer服务器之间在某一时刻的数据并不会完全相同,但这对消 息发送不会造成任何影响,这也是 RocketMQ NameServer设计的 一个亮点, RocketMQ NameServer设计追求简单高效 。
Broker消息服务器在启动时向所有 NameServer注册,消息生产者(Producer)在发送消 息之前先从 NameServer 获取 Broker 服务器地址列表,然后根据负载算法从列表中选择一 台消息服务器进行消息发送 。 NameServer 与每台 Broker 服务器保持长连接,并间隔 30s 检 测 Broker是否存活,如果检测到 Broker右机 , 则从路由注册表中将其移除 。
NameServer的逻辑部署图如下:
2.1 NameServer架构设计
介绍上述这些命令没有写出具体的参数,使用时可以添加 -h 参数来查看。
创建topic
消息的收发都要有对应的Topic,需要向某个Topic收发消息,所以正式发送和接收消息之前,要先使用updateTopic指令创建Topic。
进入bin目录:/Users/yubuyun/Documents/workspace/rocketmq-all-4.3.2/distribution/target/apache-rocketmq/bin下,这个目录下都是一些sh脚本,可以使用mqadmin工具来创建topic。
shmqadmin updateTopic -tTopicTest -cDefaultCluster -n127.0.0.1:9876
这样就可以进行消息的收发了。
删除topic
删除指令为deleteTopic
创建/修改订阅组
使用Clustering模式消费一个topic里面的消息内容时,可以启动多个消费者并行消费,每个消费者处理topic里面的一部分,提高整体的消费速度,通过订阅组可以将这些消费者分为同一组,同一组的消费者共同消费同一个topic中的消息。命令为:updateSubGroup,示例如下,创建一个名为groupTest的订阅组:
shmqadmin updateSubGroup -n127.0.0.1:9876 -cDefaultCluster -ggroupTest
删除订阅组
删除订阅组使用命令:deleteSubGroup
更改broker配置信息
broker有很多配置信息,在broker启动时可以通过配置文件来指定配置信息。 有些配置信息支持在 Broker运行的时候动态更改,更改指令是:updateBrokerConfig。
更新topic的读写权限
RocketMQ 支持对 Topic 进行权限控制, 主要分为只读的Topic和可读写的Topic,权限可以通过指令:updateTopicPerm 来动态改变。
查询 Topic 的路由信息
Topic 的路由信息指的是某个 Topic 所在的Broker相关信息,客户端可以通过 NameServer来获取这些信息,本命令一般在调试的时候使用,指令是 TopicRoute。
查看 Topic 列表信息
上面提到的 TopicRoute是列出某个 Topic 的相关信息,还有个指令 TopicList 用来列出集群中所有 Topic 的名称 。示例如下:
shmqadmin TopicList -n127.0.0.1:9876
从输出可以看到我们之前添加的TopicTest。
查看 Topic 统计信息
在使用 RocketMQ 的时候,经常需要查看某个 Topic 的状态,看看消息的 数量,有多少未处理等, 此 时可以通过指令 TopicStats 来查询。
根据时间查询消息
一条消息被发送到 RocketMQ后, 默认会带上发送的时间戳, 所以我们可以根据估计的时间来查询消息,指令是 printMsg 。
根据消息 ID 查询消息
根据消息 ID可以精确定位到某条消息,但是消息 ID需要通过其他方式来获取, 比如可以先用时间来查询出一些消息,然后定位到要找 的具体某个消息,指令是queryMsgByld。
查看集群消息
指令 clusterList用来列出集群的状态,看看有哪些 Broker在提供服务。
启动broker
nuhup命令和&配合使用表示不间断的在后台运行脚本,当执行完第一个脚本后,会在同级目录下生成一个nohup.out文件来存储日志,可以在这个日志文件中查看nameserver是否启动成功以及错误日志。