消息,是MetaQ最重要的资源,在分析MetaQ之前必须了解的概念,我们所做的一切都是围绕消息进行的,让我们看看MetaQ中消息的定义是怎样的,MetaQ的类Message定义了消息的格式:
- public class Messageimplements Serializable {
- private long id;
- private String topic;
- private byte[] data;
- private String attribute;
- private int flag;
- private Partition partition;
- private transientboolean readOnly;
- private transientboolean rollbackOnly = false;
- }
- public class Message implements Serializable {
- private long id;
- private String topic;
- private byte[] data;
- private String attribute;
- private int flag;
- private Partition partition;
- private transient boolean readOnly;
- private transient boolean rollbackOnly = false;
- }
从对消息的定义,我们可以看出消息都有一个唯一的ID,并且归属于某个主题,发送到该主题下的某个分区,具有一些基本属性。
MetaQ分为Broker和Client以及Zookeeper,系统结构如下:
下面我们先分析Broker源码。
Broker主要围绕发送消息和消费消息的主线进行,对于Broker来说就是输入、输出流的处理。在该主线下,Broker主要分为如下模块:网络传输模块、消息存储模块、消息统计模块以及事务模块,本篇首先针对独立性较强的消息存储模块进行分析。
在进行存储模块分析之前,我们得了解Broker中的一个重要的类MetaConfig,MetaConfig是Broker配置加载器,通过MetaConfig可以获取到各模块相关的配置,所以MetaConfig是贯穿所有模块的类。MetaConfig实现MetaConfigMBean接口,该接口定义如下:
- public interface MetaConfigMBean {
-
-
-
- public void reload();
-
-
- public void closePartitions(String topic,int start, int end);
-
-
- public void openPartitions(String topic);
- }
- public interface MetaConfigMBean {
-
-
-
- public void reload();
-
-
- public void closePartitions(String topic, int start, int end);
-
-
- public void openPartitions(String topic);
- }
MetaConfig注册到了MBeanServer上,所以可以通过JMX协议重新加载配置以及关闭和打开分区。为了加载的配置立即生效,MetaConfig内置了一个通知机制,可以通过向MetaConfig注册监听器的方式关注相关配置的变化,监听器需实现PropertyChangeListener接口。
- public void addPropertyChangeListener(final String propertyName,finalPropertyChangeListener listener) {
- this.propertyChangeSupport.addPropertyChangeListener(propertyName, listener);
- }
-
- public void removePropertyChangeListener(final PropertyChangeListener listener) {
- this.propertyChangeSupport.removePropertyChangeListener(listener);
- }
- public void addPropertyChangeListener(final String propertyName, final PropertyChangeListener listener) {
- this.propertyChangeSupport.addPropertyChangeListener(propertyName, listener);
- }
-
- public void removePropertyChangeListener(final PropertyChangeListener listener) {
- this.propertyChangeSupport.removePropertyChangeListener(listener);
- }
目前MetaConfig发出的事件通知有三种:配置文件发生变化(configFileChecksum)、主题发生变化(topics,topicConfigMap)以及刷新存储的频率发生变化(unflushInterval),代码如下:
-
- private Ini createIni(final File file)throws IOException, InvalidFileFormatException {
- ……
- this.propertyChangeSupport.firePropertyChange("configFileChecksum",null, null);
- return conf;
- }
-
- public void setConfigFileChecksum(long configFileChecksum) {
- this.configFileChecksum = configFileChecksum;
- this.propertyChangeSupport.firePropertyChange("configFileChecksum",null, null);
- }
-
-
- private void populateTopicsConfig(final Ini conf) {
- ……
- if (!newTopicConfigMap.equals(this.topicConfigMap)) {
- this.topics = newTopics;
- this.topicConfigMap = newTopicConfigMap;
- this.propertyChangeSupport.firePropertyChange("topics",null, null);
- this.propertyChangeSupport.firePropertyChange("topicConfigMap",null, null);
- }
- this.propertyChangeSupport.firePropertyChange("unflushInterval",null, null);
- ……
-
- private Ini createIni(final File file) throws IOException, InvalidFileFormatException {
- ……
- this.propertyChangeSupport.firePropertyChange("configFileChecksum", null, null);
- return conf;
- }
-
- public void setConfigFileChecksum(long configFileChecksum) {
- this.configFileChecksum = configFileChecksum;
- this.propertyChangeSupport.firePropertyChange("configFileChecksum", null, null);
- }
-
-
- private void populateTopicsConfig(final Ini conf) {
- ……
- if (!newTopicConfigMap.equals(this.topicConfigMap)) {
- this.topics = newTopics;
- this.topicConfigMap = newTopicConfigMap;
- this.propertyChangeSupport.firePropertyChange("topics", null, null);
- this.propertyChangeSupport.firePropertyChange("topicConfigMap", null, null);
- }
- this.propertyChangeSupport.firePropertyChange("unflushInterval", null, null);
- ……
需要注意的是,调用reload方法时,只对topic的配置生效,对全局配置不生效,只重载topic的配置。
好吧,废话了许多,让我们正式进入存储模块的分析吧。
Broker的存储模块用于存储Client发送的等待被消费的消息,Broker采用文件存储的方式来存储消息,存储模块类图如下:
MessageSet代表一个消息集合,可能是一个文件也可能是文件的一部分,其定义如下:
-
-
-
- public interface MessageSet {
- public MessageSet slice(long offset,long limit) throws IOException;
-
- public void write(GetCommand getCommand, SessionContext ctx);
-
- public long append(ByteBuffer buff)throws IOException;
-
- public void flush()throws IOException;
-
- public void read(final ByteBuffer bf,long offset) throws IOException;
-
- public void read(final ByteBuffer bf)throws IOException;
-
- public long getMessageCount();
- }
-
-
-
- public interface MessageSet {
- public MessageSet slice(long offset, long limit) throws IOException;
-
- public void write(GetCommand getCommand, SessionContext ctx);
-
- public long append(ByteBuffer buff) throws IOException;
-
- public void flush() throws IOException;
-
- public void read(final ByteBuffer bf, long offset) throws IOException;
-
- public void read(final ByteBuffer bf) throws IOException;
-
- public long getMessageCount();
- }
FileMessageSet实现了MessageSet接口和Closeable接口,实现Closeable接口主要是为了在文件关闭的时候确保文件通道关闭以及内容是否提交到磁盘
- public void close()throws IOException {
- if (!this.channel.isOpen()) {
- return;
- }
-
- if (this.mutable) {
- this.flush();
- }
-
- this.channel.close();
- }
- public void close() throws IOException {
- if (!this.channel.isOpen()) {
- return;
- }
-
- if (this.mutable) {
- this.flush();
- }
-
- this.channel.close();
- }
下面让我们来具体分析一下FileMessageSet这个类,
- public class FileMessageSetimplements MessageSet, Closeable {
- ……
- private final FileChannel channel;
- private final AtomicLong messageCount;
- private final AtomicLong sizeInBytes;
- private final AtomicLong highWaterMark;
- private finallong offset;
- private boolean mutable;
-
- public FileMessageSet(final FileChannel channel,final long offset,final longlimit,final boolean mutable)throws IOException {
- this.channel = channel;
- this.offset = offset;
- this.messageCount = new AtomicLong(0);
- this.sizeInBytes = new AtomicLong(0);
- this.highWaterMark = new AtomicLong(0);
- this.mutable = mutable;
- if (mutable) {
- final long startMs = System.currentTimeMillis();
- final long truncated =this.recover();
- if (this.messageCount.get() >0) {
- log.info("Recovery succeeded in " + (System.currentTimeMillis() - startMs) /1000 + " seconds. " + truncated +" bytes truncated.");
- }
- } else {
- try {
- this.sizeInBytes.set(Math.min(channel.size(), limit) - offset);
- this.highWaterMark.set(this.sizeInBytes.get());
- } catch (final Exception e) {
- log.error("Set sizeInBytes error", e);
- }
- }
- }
-
-
- ……
- public long append(final ByteBuffer buf)throws IOException {
-
- if (!this.mutable) {
- throw new UnsupportedOperationException("Immutable message set");
- }
- final long offset =this.sizeInBytes.get();
- int sizeInBytes = 0;
- while (buf.hasRemaining()) {
- sizeInBytes += this.channel.write(buf);
- }
-
-
- this.sizeInBytes.addAndGet(sizeInBytes); 、
- this.messageCount.incrementAndGet();
- return offset;
- }
-
-
- public void flush()throws IOException {
- this.channel.force(true);
- this.highWaterMark.set(this.sizeInBytes.get());
- }
- ……
- @Override
- public MessageSet slice(finallong offset, finallong limit) throws IOException {
-
- return new FileMessageSet(this.channel, offset, limit,false);
- }
-
- static final Logger transferLog = LoggerFactory.getLogger("TransferLog");
-
- @Override
- public void read(final ByteBuffer bf,final long offset)throws IOException {
-
- int size = 0;
- while (bf.hasRemaining()) {
- final int l =this.channel.read(bf, offset + size);
- if (l < 0) {
- break;
- }
- size += l;
- }
- }
-
- @Override
- public void read(final ByteBuffer bf)throws IOException {
- this.read(bf, this.offset);
- }
-
-
- @Override
- public void write(final GetCommand getCommand,final SessionContext ctx) {
- final IoBuffer buf =this.makeHead(getCommand.getOpaque(),this.sizeInBytes.get());
-
- this.tryToLogTransferInfo(getCommand, ctx.getConnection());
- ctx.getConnection().transferFrom(buf, null,this.channel,this.offset,this.sizeInBytes.get());
- }
-
- public long write(final WritableByteChannel socketChanel)throws IOException {
- try {
- return this.getFileChannel().transferTo(this.offset,this.getSizeInBytes(), socketChanel);
- } catch (final IOException e) {
-
-
- final String message = e.getMessage();
- if (message != null && message.contains("temporarily unavailable")) {
- return 0;
- }
- throw e;
- }
- }
- ……
- private staticboolean fastBoot = Boolean.valueOf(System.getProperty("meta.fast_boot","false"));
-
- private long recover()throws IOException {
-
- if (fastBoot) {
- final long size =this.channel.size();
- this.sizeInBytes.set(size);
- this.highWaterMark.set(size);
- this.messageCount.set(0);
- this.channel.position(size);
- return 0;
- }
- if (!this.mutable) {
- throw new UnsupportedOperationException("Immutable message set");
- }
- final long len =this.channel.size();
- final ByteBuffer buf = ByteBuffer.allocate(MessageUtils.HEADER_LEN);
- long validUpTo = 0L;
- long next = 0L;
- long msgCount = 0;
- do {
- next = this.validateMessage(buf, validUpTo, len);
- if (next >= 0) {
- msgCount++;
- validUpTo = next;
- }
- } while (next >= 0);
- this.channel.truncate(validUpTo);
- this.sizeInBytes.set(validUpTo);
- this.highWaterMark.set(validUpTo);
- this.messageCount.set(msgCount);
- this.channel.position(validUpTo);
- return len - validUpTo;
- }
-
- 消息在磁盘上存储结构如下:
-
-
-
-
-
-
-
-
-
-
-
-
- private long validateMessage(final ByteBuffer buf,final long start,final longlen)throws IOException {
- buf.rewind();
- long read = this.channel.read(buf);
- if (read < MessageUtils.HEADER_LEN) {
- return -1;
- }
- buf.flip();
- final int messageLen = buf.getInt();
- final long next = start + MessageUtils.HEADER_LEN + messageLen;
- if (next > len) {
- return -1;
- }
- final int checksum = buf.getInt();
- if (messageLen < 0) {
-
- return -1;
- }
-
- final ByteBuffer messageBuffer = ByteBuffer.allocate(messageLen);
-
- while (messageBuffer.hasRemaining()) {
- read = this.channel.read(messageBuffer);
- if (read < 0) {
- throw new IOException("文件在recover过程中被修改");
- }
-
- }
- if (CheckSum.crc32(messageBuffer.array()) != checksum) {
-
- return -1;
- } else {
- return next;
- }
- }
- public class FileMessageSet implements MessageSet, Closeable {
- ……
- private final FileChannel channel;
- private final AtomicLong messageCount;
- private final AtomicLong sizeInBytes;
- private final AtomicLong highWaterMark;
- private final long offset;
- private boolean mutable;
-
- public FileMessageSet(final FileChannel channel, final long offset, final long limit, final boolean mutable) throws IOException {
- this.channel = channel;
- this.offset = offset;
- this.messageCount = new AtomicLong(0);
- this.sizeInBytes = new AtomicLong(0);
- this.highWaterMark = new AtomicLong(0);
- this.mutable = mutable;
- if (mutable) {
- final long startMs = System.currentTimeMillis();
- final long truncated = this.recover();
- if (this.messageCount.get() > 0) {
- log.info("Recovery succeeded in " + (System.currentTimeMillis() - startMs) / 1000 + " seconds. " + truncated + " bytes truncated.");
- }
- } else {
- try {
- this.sizeInBytes.set(Math.min(channel.size(), limit) - offset);
- this.highWaterMark.set(this.sizeInBytes.get());
- } catch (final Exception e) {
- log.error("Set sizeInBytes error", e);
- }
- }
- }
-
-
- ……
- public long append(final ByteBuffer buf) throws IOException {
-
- if (!this.mutable) {
- throw new UnsupportedOperationException("Immutable message set");
- }
- final long offset = this.sizeInBytes.get();
- int sizeInBytes = 0;
- while (buf.hasRemaining()) {
- sizeInBytes += this.channel.write(buf);
- }
-
-
- this.sizeInBytes.addAndGet(sizeInBytes); 、
- this.messageCount.incrementAndGet();
- return offset;
- }
-
-
- public void flush() throws IOException {
- this.channel.force(true);
- this.highWaterMark.set(this.sizeInBytes.get());
- }
- ……
- @Override
- public MessageSet slice(final long offset, final long limit) throws IOException {
-
- return new FileMessageSet(this.channel, offset, limit, false);
- }
-
- static final Logger transferLog = LoggerFactory.getLogger("TransferLog");
-
- @Override
- public void read(final ByteBuffer bf, final long offset) throws IOException {
-
- int size = 0;
- while (bf.hasRemaining()) {
- final int l = this.channel.read(bf, offset + size);
- if (l < 0) {
- break;
- }
- size += l;
- }
- }
-
- @Override
- public void read(final ByteBuffer bf) throws IOException {
- this.read(bf, this.offset);
- }
-
-
- @Override
- public void write(final GetCommand getCommand, final SessionContext ctx) {
- final IoBuffer buf = this.makeHead(getCommand.getOpaque(), this.sizeInBytes.get());
-
- this.tryToLogTransferInfo(getCommand, ctx.getConnection());
- ctx.getConnection().transferFrom(buf, null, this.channel, this.offset, this.sizeInBytes.get());
- }
-
- public long write(final WritableByteChannel socketChanel) throws IOException {
- try {
- return this.getFileChannel().transferTo(this.offset, this.getSizeInBytes(), socketChanel);
- } catch (final IOException e) {
-
-
- final String message = e.getMessage();
- if (message != null && message.contains("temporarily unavailable")) {
- return 0;
- }
- throw e;
- }
- }
- ……
- private static boolean fastBoot = Boolean.valueOf(System.getProperty("meta.fast_boot", "false"));
-
- private long recover() throws IOException {
-
- if (fastBoot) {
- final long size = this.channel.size();
- this.sizeInBytes.set(size);
- this.highWaterMark.set(size);
- this.messageCount.set(0);
- this.channel.position(size);
- return 0;
- }
- if (!this.mutable) {
- throw new UnsupportedOperationException("Immutable message set");
- }
- final long len = this.channel.size();
- final ByteBuffer buf = ByteBuffer.allocate(MessageUtils.HEADER_LEN);
- long validUpTo = 0L;
- long next = 0L;
- long msgCount = 0;
- do {
- next = this.validateMessage(buf, validUpTo, len);
- if (next >= 0) {
- msgCount++;
- validUpTo = next;
- }
- } while (next >= 0);
- this.channel.truncate(validUpTo);
- this.sizeInBytes.set(validUpTo);
- this.highWaterMark.set(validUpTo);
- this.messageCount.set(msgCount);
- this.channel.position(validUpTo);
- return len - validUpTo;
- }
-
- 消息在磁盘上存储结构如下:
-
-
-
-
-
-
-
-
-
-
-
-
- private long validateMessage(final ByteBuffer buf, final long start, final long len) throws IOException {
- buf.rewind();
- long read = this.channel.read(buf);
- if (read < MessageUtils.HEADER_LEN) {
- return -1;
- }
- buf.flip();
- final int messageLen = buf.getInt();
- final long next = start + MessageUtils.HEADER_LEN + messageLen;
- if (next > len) {
- return -1;
- }
- final int checksum = buf.getInt();
- if (messageLen < 0) {
-
- return -1;
- }
-
- final ByteBuffer messageBuffer = ByteBuffer.allocate(messageLen);
-
- while (messageBuffer.hasRemaining()) {
- read = this.channel.read(messageBuffer);
- if (read < 0) {
- throw new IOException("文件在recover过程中被修改");
- }
-
- }
- if (CheckSum.crc32(messageBuffer.array()) != checksum) {
-
- return -1;
- } else {
- return next;
- }
- }
FileMessageSet是MetaQ 服务器端存储的一个元素,代表了对一个文件的读写操作,能够对文件内容进行完整性和一致性校验,并能提供统计数据,接下来要介绍的是通过MessageStore怎样的组合将一个个的FileMessageSet,单纯的讲,MetaQ的存储非常值得借鉴。