offsetTable由来
offsetTable由来
接上篇文章
广播模式下TrackType为NOT_ONLINE
上篇文章分析并跟踪到源码 出现NOT_ONLINE的最终位置是因为获取不到offsetTable导致源码抛出异常
继续跟进
【MQClientAPIImpl.java】
//获取消费统计
public ConsumeStats getConsumeStats(final String addr, final String consumerGroup, final String topic, final long timeoutMillis)
throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, MQBrokerException {
//创建 获取消费统计请求头
GetConsumeStatsRequestHeader requestHeader = new GetConsumeStatsRequestHeader();
//消费者组
requestHeader.setConsumerGroup(consumerGroup);
//订阅消息名称
requestHeader.setTopic(topic);
//创建 通信对象, code GET_CONSUME_STATS = 208
RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET\_CONSUME\_STATS, requestHeader);
//主要数据这里出来:10911 是broker的端口,这里是使用netty客户端调用,获取的offsetTable
RemotingCommand response = this.remotingClient.invokeSync(
MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr),
request,
timeoutMillis);
switch (response.getCode()) {
case ResponseCode.SUCCESS: {
//返回数据结构中 Map结构 offsetTable
ConsumeStats consumeStats = ConsumeStats.decode(response.getBody(), ConsumeStats.class);
return consumeStats;
}
default:
break;
}
throw new MQBrokerException(response.getCode(), response.getRemark());
}
继续跟进
【broker: AdminBrokerProcessor.java】
@Override
public RemotingCommand processRequest(ChannelHandlerContext ctx,
RemotingCommand request) throws RemotingCommandException {
switch (request.getCode()) {
...
case RequestCode.GET_CONSUME_STATS:
//这里获取消费统计
return this.getConsumeStats(ctx, request);
....
}
继续跟进
【broker: AdminBrokerProcessor.java】
private RemotingCommand getConsumeStats(ChannelHandlerContext ctx,
RemotingCommand request) throws RemotingCommandException {
final RemotingCommand response = RemotingCommand.createResponseCommand(null);
final GetConsumeStatsRequestHeader requestHeader =
(GetConsumeStatsRequestHeader) request.decodeCommandCustomHeader(GetConsumeStatsRequestHeader.class);
ConsumeStats consumeStats = new ConsumeStats();
Set<String> topics = new HashSet<String>();
if (UtilAll.isBlank(requestHeader.getTopic())) {
//当前消费者订阅的 topic列表 这里下文继续分析一下
topics = this.brokerController.getConsumerOffsetManager().whichTopicByConsumer(requestHeader.getConsumerGroup());
} else {
topics.add(requestHeader.getTopic());
}
//在广播模式下 topics 是空的 导致 ConsumeStats 也是没有数据的
for (String topic : topics) {
...
//这是我们要的数据
offsetWrapper.setBrokerOffset(brokerOffset);
...
consumeStats.getOffsetTable().put(mq, offsetWrapper);
}
double consumeTps = this.brokerController.getBrokerStatsManager().tpsGroupGetNums(requestHeader.getConsumerGroup(), topic);
consumeTps += consumeStats.getConsumeTps();
consumeStats.setConsumeTps(consumeTps);
}
byte[] body = consumeStats.encode();
response.setBody(body);
response.setCode(ResponseCode.SUCCESS);
response.setRemark(null);
return response;
}
继续跟进
【ConsumerOffsetManager.java】
/**
* 当前消费者组 能匹配到的topic 列表
* 简而言之:当前消费者订阅的 topic列表
* @param group
* @return
*/
public Set<String> whichTopicByConsumer(final String group) {
Set<String> topics = new HashSet<String>();
//offsetTable 在这里呢,但是通过具体审查后 发现这里面都是集群模式的offsetTable 数据
//也就是说 groupName 咱们是广播模式,参数进来后 无法匹配到
Iterator<Entry<String, ConcurrentMap<Integer, Long>>> it = this.offsetTable.entrySet().iterator();
while (it.hasNext()) {
Entry<String, ConcurrentMap<Integer, Long>> next = it.next();
//格式 topic@consumerGroupName
String topicAtGroup = next.getKey();
String[] arrays = topicAtGroup.split(TOPIC_GROUP_SEPARATOR);
if (arrays.length == 2) {
//当前广播模式 消费者组名 equals 集群模式的消费者组名 ,无一例外 全部匹配不成功
if (group.equals(arrays[1])) {
topics.add(arrays[0]);
}
}
}
return topics;
}
[========]
看到此处其实已经能够发现 其实ONT_ONLINE 真的是设计如此,不是我们消息未消费
但是我们是有上进心的孩子,得看看offsetTabel 数据是怎么加载进来的呀
继续搞
broker启动类 加载集群模式下offsetTable
【BrokerStartup.java】
public static void main(String[] args) {
//探索 broker启动时 加载offsetTable
start(createBrokerController(args));
}
追踪
/**
* 探索 broker启动时 加载offsetTable
* @param args
* @return
*/
public static BrokerController createBrokerController(String\[\] args) {
...
//fixme 探索 broker启动时 加载offsetTable
boolean initResult = controller.initialize();
...
return null;
}
追踪
public boolean initialize() throws CloneNotSupportedException {
boolean result = this.topicConfigManager.load();
//fixme 这里加载了 offsetTable数据 注意这是broker项目 一定是加载的集群模式的offsetTable哈
result = result && this.consumerOffsetManager.load();
追踪
load()方法 流程
1、configFilePath() 获取配置文件路径
ConsumerOffsetManager.java
/**
* fixme 启动时 加载的offset配置文件 地址
* storePathRootDir 系统存储路径 System.getProperty("user.home") / "store";
* getConsumerOffsetPath 拼接 系统存储路径 / "config"/ "consumerOffset.json";
* @return
*/
@Override
public String configFilePath() {
return BrokerPathConfigHelper.getConsumerOffsetPath(this.brokerController.getMessageStoreConfig().getStorePathRootDir());
}
2、decode()
/**
* fixme 解析出来的数据 添加到offsetTable
* @param jsonString
*/
@Override
public void decode(String jsonString) {
if (jsonString != null) {
ConsumerOffsetManager obj = RemotingSerializable.fromJson(jsonString, ConsumerOffsetManager.class);
if (obj != null) {
//这里哈
this.offsetTable = obj.offsetTable;
}
}
}
这不就出来了嘛,offsetTable 数据是存储在broker服务器上的
但是怎么确定 广播模式 是存储在消费者本地的呢?
继续探索呗
广播模式下加载offsetTable
clinet 启动类
【DefaultMQPushConsumer.java】
/**
* This method gets internal infrastructure readily to serve. Instances must call this method after configuration.
* fixme 实例配置后必须调用该方法 ,这个方法熟悉吧,我们在编写代码后 一定会 启动DefaultMQPushConsumer.start();
* @throws MQClientException if there is any client error.
*/
public void start() throws MQClientException {
...
//这里启动实现
this.defaultMQPushConsumerImpl.start();
...
}
继续追踪
public synchronized void start() throws MQClientException {
...
if (this.defaultMQPushConsumer.getOffsetStore() != null) {
this.offsetStore = this.defaultMQPushConsumer.getOffsetStore();
} else {
switch (this.defaultMQPushConsumer.getMessageModel()) {
case BROADCASTING:
//fixme 广播模式 初始化 offset存储路径
this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
break; case CLUSTERING:
//fixme 集群模式
this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
break; default:
break;
}
this.defaultMQPushConsumer.setOffsetStore(this.offsetStore);
}
//fixme 加载offset 注意集群模式的load 是空实现,因为它是broker 加载的
this.offsetStore.load();
...
}
分两步追踪 1、 offset存储路径 怎么得到的 2、怎么加载offset
1、offset存储路径
【LocalFileOffsetStore.java】
public LocalFileOffsetStore(MQClientInstance mQClientFactory, String groupName) {
this.mQClientFactory = mQClientFactory;
this.groupName = groupName;
//初始化 存储路径 user.home / .rocketmq_offsets / getClientId = ip@instanceName / groupName / offsets.json
this.storePath = LOCAL_OFFSET_STORE_DIR + File.separator +
this.mQClientFactory.getClientId() + File.separator +
this.groupName + File.separator +
"offsets.json";
}
2、如何加载offset
【LocalFileOffsetStore.java】
@Override
public void load() throws MQClientException {
// 读取本地 offset
OffsetSerializeWrapper offsetSerializeWrapper = this.readLocalOffset();
if (offsetSerializeWrapper != null && offsetSerializeWrapper.getOffsetTable() != null) {
//添加到offsetTable
offsetTable.putAll(offsetSerializeWrapper.getOffsetTable());
for (MessageQueue mq : offsetSerializeWrapper.getOffsetTable().keySet()) {
AtomicLong offset = offsetSerializeWrapper.getOffsetTable().get(mq);
log.info("load consumer's offset, {} {} {}",
this.groupName,
mq,
offset.get());
}
}
}