消息隊列選型:rocketmq or rabbitmq

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

  1. Rocketmq爲高併發海量數據設計,架構很先進,支持容量橫向擴展。
  2. Rocketmq沒有我們傳統意義上的queue對象,而是通過tag實現消息的分類。消息的負載數據(enqueue\dequeue\pending等)無法精確到tag維度,只能到broken或consumer group。
  3. 控制檯沒有包含到原生項目,而是在rocketmq-externals中;控制檯更新緩慢,2017年6月第一版後再無新的版本;控制檯做得很差
  4. 沒有完全遵守AMQP,只支持java客戶端;官方文檔簡陋。

Rabbitmq

  1. 一個2節點的集羣能保守提供6-7K/S的請求量,性能滿足要求(目前我司還沒有單個應用隊列吞吐超過該數量的請求);
  2. 準守AMQP 協議,支持多種客戶端,如:Python、Ruby、.NET、Java、C、PHP等;
  3. 控制檯很友好,很強大,能看每個queue最近24小時的生產、消費、積壓走勢圖
     
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章