1.目前我們用的activemq,面臨一些問題
- activemq高可用基於leveldb的複製,但activemq從5.*開始,leveldb已經不再有任何更新
The LevelDB store has been deprecated and is no longer supported or recommended for use. The recommended store is KahaDB
但官方推薦的KahaDB存儲方案,目前沒有原生的高可用支持。
- 我司使用leveldb過程中,遇到一些怪異的問題:隊列被消費後,日誌不自動清理;刪除queue後日志才清理;日誌手動清理後,activemq記錄的“空間使用百分比”不減少,需要重啓才能顯示正確的值;“空間使用百分比”一旦100%,集羣將不可用。官網上很多人提了類似issue,但都已經不再支持處理。
- leveldb不支持延遲消息。
- leveldb在節點failover時,會出現重啓後“空間使用百分比”一直不減少的情況,只有將整個集羣停止後,清空所有leveldb目錄,重新初始化才能解決(預計整個集羣停服1分鐘)
基於以上原因,我建議考慮新的隊列方案:rabbitmq或者rocketmq。
2.rocketmq介紹
- Nameserver負責broke的自動發現(和dubbo中的zk角色類似);nameserver在rocketmq的早期版本是直接用zk做的,後來自己開發;nameserver無狀態,可任意個節點。
- Producer cluster,可以理解爲一個應用的n個節點,rocketmq中用producer cluster的主要作用是一個produder掛掉後,可以通知別的producer繼續執行其未完成的事務;
- Consumer cluster,也可理解爲一個應用的n個節點,主要是實現n個consumer的負載均衡。
- 1個cluster中可以n個topic,1個topic中可以有n個broken,1個broken中可以有1個master和n個slave。
- Producer向topic 發送消息,n個broken負載均衡,橫向擴展;某個broken中的master宕機後,master被摘除,消息負載到其他broken,但原broken的slave仍然可以提供消費服務。
- Rocketmq中的queue和別的mq不同,不是activemq或rabbitmq中的給特定生產者和消費者配對的邏輯對象,而是爲了提高併發性設置的消息存儲的sharding。
- 生產者向每個queue輪詢發送消息,消費者負載均衡地消費這些queue;每個broken節點中的queue數目默認16,可自定義,數目必須超過consumer數,否則會出現多出的consumer無法消費的情況。
- Rocketmq中會對每個消息加特定的tag,這些tag和別的mq中的queue的概念類似,用於生產者和消費者配對特定的消息。
示例代碼
public class Producer {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("sina-www");
producer.setNamesrvAddr("10.40.20.200:9876,10.40.20.201:9876");
producer.start();
for (int i = 0; i < 100000; i++) {
Message msg = new Message("topic1", "sina.www.abc", ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
}
producer.shutdown();
}
}
public class Consumer {
public static void main(String[] args) throws InterruptedException, MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("sina-www");
consumer.setNamesrvAddr("10.40.20.200:9876,10.40.20.201:9876");
consumer.subscribe("topic1", "sina.www.abc");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,ConsumeConcurrentlyContext context) {
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
}
}
3.rabbitmq介紹
- Exchange的職責類似於交換機,提供數據交換功能,將數據分發到特定的queue。
- Binding: exchange和queue的綁定關係; exchange的不同類型,加上binging配置的routekey,可以實現exchange與queue的任意組合分發規則。
- 一個cluster中每個節點都有相同的exchange和binding信息。
- 每個exchange必須將消息發送至master queue,不管master是否與該exchange在一個節點;Master queue與slave queue自動同步,queue副本數可以指定,可以少於node數;Node宕機後,node上的master queue對應的別的節點的副本會升級爲master。
示例程序
public class Producer {
private static String queueName = "sina.goods.updatestock";
public static void main(String[] args) throws Exception{
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("10.40.20.203");
factory.setUsername("guest");
factory.setPassword("guest");
factory.setVirtualHost("/");
factory.setPort(5672);
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(queueName, true, false, false, null);
channel.queueBind(queueName,"lbexchange", queueName);
for (int i = 0; i < 100000; i++) {
String message = "hello world + " + i;
channel.basicPublish("lbexchange", queueName, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
System.out.println(" [x] Sent '" + i + "' ");
}
channel.close();
connection.close();
}
}
public class Consumer {
private static String queueName = "sina.goods.updatestock";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("10.40.20.203");
factory.setUsername("guest");
factory.setPassword("guest");
factory.setVirtualHost("/");
factory.setPort(5672);
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(queueName, true, false, false, null);
QueueingConsumer consumer = new QueueingConsumer(channel);
int prefetchCount = 1;
channel.basicQos(prefetchCount);
channel.basicConsume(queueName, true, consumer);
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
}
}
}
4.rabbitmq性能測試
因爲rocketmq的性能肯定比rabbitmq強,因此只測試了rabbitmq的性能。
測試場景:10個生產者,10個消費者,消費者自動ACK,2節點集羣,服務器配置8C8G
壓測工具:github.com/rabbitmq/rabbitmq-perf-test
以下分別測試消息大小爲10字節和1K、直連主節點和直連從節點,一共4種組合。
消息大小10字節,直連主節點
Cpu 負載:主65%,從25%
消息大小1K,直連主節點
Cpu 負載:主65%,從25%
消息大小10字節,直連從節點
Cpu 負載:主35%,從40%
消息大小1K,直連從節點
Cpu 負載:主30%,從35%
消息積壓的表現
關閉消費者,讓消息積壓,消息大小1K
因爲內存限制,消息積壓到一定程度要page out到磁盤,因此生產者發送消息會有明顯的抖動(page out時會block)。
5.使用總結
Rocketmq
- Rocketmq爲高併發海量數據設計,架構很先進,支持容量橫向擴展。
- Rocketmq沒有我們傳統意義上的queue對象,而是通過tag實現消息的分類。消息的負載數據(enqueue\dequeue\pending等)無法精確到tag維度,只能到broken或consumer group。
- 控制檯沒有包含到原生項目,而是在rocketmq-externals中;控制檯更新緩慢,2017年6月第一版後再無新的版本;控制檯做得很差
- 沒有完全遵守AMQP,只支持java客戶端;官方文檔簡陋。
Rabbitmq
- 一個2節點的集羣能保守提供6-7K/S的請求量,性能滿足要求(目前我司還沒有單個應用隊列吞吐超過該數量的請求);
- 準守AMQP 協議,支持多種客戶端,如:Python、Ruby、.NET、Java、C、PHP等;
- 控制檯很友好,很強大,能看每個queue最近24小時的生產、消費、積壓走勢圖