Apache Pulsar 源碼走讀(二)二進制協議

pulsar 使用protocolBuf 作為二進制協議編寫的工具

文件位置

本文主要說明其中幾個主要的RPC 的作用。
并大致說一下這個幾個RPC會被使用的位置。具體每個字段的含義會在后續的文章中說明。(因為目前代碼還是在逐步熟悉過程中)

這里可能是由于歷史的原因,
最開始定義的消息都不是嚴格一個Request對應一個Response的。
所以看代碼有的時候會感覺非常困惑。

大致的消息類型(截止2.7版本)

message BaseCommand {
    enum Type {
        CONNECT     = 2;
        CONNECTED   = 3;

        // consumer 注冊
        SUBSCRIBE   = 4;

        // producer 注冊
        PRODUCER    = 5;

        // 向topic寫入消息
        SEND        = 6;
        // 寫入的response
        SEND_RECEIPT= 7;
        // 寫入異常的response
        SEND_ERROR  = 8;

        // 發message 給consumer
        MESSAGE     = 9;
        // 確認某個消息是否成功消費
        ACK         = 10;
        // consumer 請求消息
        FLOW        = 11;

        UNSUBSCRIBE = 12;

        // 通用的一個成功的response
        SUCCESS     = 13;
        // 通用的一個異常的response
        ERROR       = 14;

        CLOSE_PRODUCER = 15;
        CLOSE_CONSUMER = 16;

        // Producer 的 response
        PRODUCER_SUCCESS = 17;

        // 網絡層keepAlive 用的
        PING = 18;
        PONG = 19;

        // 
        REDELIVER_UNACKNOWLEDGED_MESSAGES = 20;

        PARTITIONED_METADATA           = 21;
        PARTITIONED_METADATA_RESPONSE  = 22;

        LOOKUP           = 23;
        LOOKUP_RESPONSE  = 24;

        CONSUMER_STATS        = 25;
        CONSUMER_STATS_RESPONSE    = 26;


        // 
        REACHED_END_OF_TOPIC = 27;

        SEEK = 28;

        GET_LAST_MESSAGE_ID = 29;
        GET_LAST_MESSAGE_ID_RESPONSE = 30;

        // 
        ACTIVE_CONSUMER_CHANGE = 31;


        GET_TOPICS_OF_NAMESPACE             = 32;
        GET_TOPICS_OF_NAMESPACE_RESPONSE     = 33;

        GET_SCHEMA = 34;
        GET_SCHEMA_RESPONSE = 35;

        AUTH_CHALLENGE = 36;
        AUTH_RESPONSE = 37;

        ACK_RESPONSE = 38;

        GET_OR_CREATE_SCHEMA = 39;
        GET_OR_CREATE_SCHEMA_RESPONSE = 40;

        // transaction related
        // 事務相關的比較容易理解,下面先忽略了 50 - 61 

    }

    // .....
}

CommandConnect

這里是客戶端與server連接的channel一連上就會發送一個CONNECT 請求
這里會有一些鑒權和協議版本上報的信息。
溝通客戶端版本之后,服務端就知道客戶端支持哪些特性,會做一些兼容處理
相當于kafka 里面的ApiVersionRequest

// org.apache.pulsar.client.impl.ClientCnx

public void channelActive(ChannelHandlerContext ctx) throws Exception {
        super.channelActive(ctx);
        this.timeoutTask = this.eventLoopGroup.scheduleAtFixedRate(() -> checkRequestTimeout(), operationTimeoutMs,
                operationTimeoutMs, TimeUnit.MILLISECONDS);

        if (proxyToTargetBrokerAddress == null) {
            if (log.isDebugEnabled()) {
                log.debug("{} Connected to broker", ctx.channel());
            }
        } else {
            log.info("{} Connected through proxy to target broker at {}", ctx.channel(), proxyToTargetBrokerAddress);
        }
        // Send CONNECT command
        ctx.writeAndFlush(newConnectCommand())
                .addListener(future -> {
                    if (future.isSuccess()) {
                        if (log.isDebugEnabled()) {
                            log.debug("Complete: {}", future.isSuccess());
                        }
                        state = State.SentConnectFrame;
                    } else {
                        log.warn("Error during handshake", future.cause());
                        ctx.close();
                    }
                });
    }

CommandConnected

這里實際上是CommandConnect 的response ,但是換了名字
(很容易對不上號)

// org.apache.pulsar.broker.service.ServerCnx
protected void handleConnect(CommandConnect connect) {
        checkArgument(state == State.Start);

        if (log.isDebugEnabled()) {
            log.debug("Received CONNECT from {}, auth enabled: {}:"
                    + " has original principal = {}, original principal = {}",
                remoteAddress,
                service.isAuthenticationEnabled(),
                connect.hasOriginalPrincipal(),
                connect.getOriginalPrincipal());
        }

        String clientVersion = connect.getClientVersion();
        int clientProtocolVersion = connect.getProtocolVersion();
        features = new FeatureFlags();
        if (connect.hasFeatureFlags()) {
            features.copyFrom(connect.getFeatureFlags());
        }

        if (!service.isAuthenticationEnabled()) {
            completeConnect(clientProtocolVersion, clientVersion);
            return;
        }

      // ......
}

CommandSubscribe

這個RPC是consumer用來在服務端注冊的。

具體調用的位置是,在ConsumerImpl構造函數的最后一行會請求服務端和客戶端進行連接,如果拿到了一個Connection,會調用這個連接成功的回調connectionOpened 如果是consumer的話就會發送這個請求,來注冊consumer相關的信息。

如果和上面的CommandConnect請求聯動起來,這個請求是在CommandConnect 之后發送的。

// org.apache.pulsar.client.impl.ConsumerImpl
@Override
    public void connectionOpened(final ClientCnx cnx) {
        // ... 上面做了一大堆的準備參數先忽略

        // 構建一個subscription
        ByteBuf request = Commands.newSubscribe(topic,
                subscription,
                consumerId,
                requestId,
                getSubType(),
                priorityLevel,
                consumerName,
                isDurable,
                startMessageIdData,
                metadata,
                readCompacted,
                conf.isReplicateSubscriptionState(),
                InitialPosition.valueOf(subscriptionInitialPosition.getValue()),
                startMessageRollbackDuration,
                schemaInfo,
                createTopicIfDoesNotExist,
                conf.getKeySharedPolicy());

}

proto定義說明(見注釋)

message CommandSubscribe {
    // 這里對應subscription的4種類型
    enum SubType {
        Exclusive = 0;
        Shared    = 1;
        Failover  = 2;
        Key_Shared = 3;
    }
   
    // topic 名字
    required string topic        = 1;
   // subscription 名字
    required string subscription = 2;
   // subscription 類型
    required SubType subType     = 3;
   // 這個是用來標記這個網絡連接上的consumer標識
    required uint64 consumer_id  = 4;
   // 網絡層的請求標識
    required uint64 request_id   = 5;
   // consumer 名字
    optional string consumer_name = 6;
   // consumer 的優先級,優先級高的consumer 容易先收到請求
    optional int32 priority_level = 7;

   // 這個subsciption是否是持久化的

    // Signal wether the subscription should be backed by a
    // durable cursor or not
    optional bool durable = 8 [default = true];

    // If specified, the subscription will position the cursor
    // markd-delete position  on the particular message id and
    // will send messages from that point
    optional MessageIdData start_message_id = 9;


    // 加了一些consumer 的自定義tag Map<String,String>
    /// Add optional metadata key=value to this consumer
    repeated KeyValue metadata = 10;

    optional bool read_compacted = 11;

    optional Schema schema = 12;

   // 初始化位置從哪里開始,最新還是最舊
    enum InitialPosition {
        Latest   = 0;
        Earliest = 1;
    }
    // Signal whether the subscription will initialize on latest
    // or not -- earliest
    optional InitialPosition initialPosition = 13 [default = Latest];


    // geo-replication 相關,先忽略
    // Mark the subscription as "replicated". Pulsar will make sure
    // to periodically sync the state of replicated subscriptions
    // across different clusters (when using geo-replication).
    optional bool replicate_subscription_state = 14;

    // If true, the subscribe operation will cause a topic to be
    // created if it does not exist already (and if topic auto-creation
    // is allowed by broker.
    // If false, the subscribe operation will fail if the topic
    // does not exist.
    optional bool force_topic_creation = 15 [default = true];

    // 這個是按照時間重置消費進度的時候
    // If specified, the subscription will reset cursor's position back
    // to specified seconds and  will send messages from that point
    optional uint64 start_message_rollback_duration_sec = 16 [default = 0];

    // key_Share 模式使用的,暫時不看
    optional KeySharedMeta keySharedMeta = 17;
}

CommandProducer

這個RPC 和 consumer相對應的,是producer在服務端注冊用的,調用位置也是相同的org.apache.pulsar.client.impl.ProducerImpl.connectionOpened 里面。

/// Create a new Producer on a topic, assigning the given producer_id,
/// all messages sent with this producer_id will be persisted on the topic
message CommandProducer {
    // topic 
    required string topic         = 1;
    required uint64 producer_id   = 2;

    // 網絡層的請求編號
    required uint64 request_id    = 3;

    /// If a producer name is specified, the name will be used,
    /// otherwise the broker will generate a unique name
    optional string producer_name = 4;

    // 是否是加密的寫入
    optional bool encrypted       = 5 [default = false];


    // 元數據 Map<String,String>
    /// Add optional metadata key=value to this producer
    repeated KeyValue metadata    = 6;

    optional Schema schema = 7;


    // 這里應該叫producer_epoch
    // If producer reconnect to broker, the epoch of this producer will +1
    optional uint64 epoch = 8 [default = 0];

    // Indicate the name of the producer is generated or user provided
    // Use default true here is in order to be forward compatible with the client
    optional bool user_provided_producer_name = 9 [default = true];


    // 這里是寫入的3種方式

    // Require that this producers will be the only producer allowed on the topic
    optional ProducerAccessMode producer_access_mode = 10 [default = Shared];

    // Topic epoch is used to fence off producers that reconnects after a new
    // exclusive producer has already taken over. This id is assigned by the
    // broker on the CommandProducerSuccess. The first time, the client will
    // leave it empty and then it will always carry the same epoch number on
    // the subsequent reconnections.
    optional uint64 topic_epoch = 11;
}

enum ProducerAccessMode {
    Shared           = 0; // By default multiple producers can publish on a topic
    Exclusive        = 1; // Require exclusive access for producer. Fail immediately if there's already a producer connected.
    WaitForExclusive = 2; // Producer creation is pending until it can acquire exclusive access
}

CommandProducerSuccess

這個是作為CommandProduce 請求的成功response

/// Response from CommandProducer
message CommandProducerSuccess {
    // 網絡層id
    required uint64 request_id    = 1;
    // producer 名字
    required string producer_name = 2;

    // The last sequence id that was stored by this producer in the previous session
    // This will only be meaningful if deduplication has been enabled.
    optional int64  last_sequence_id = 3 [default = -1];
    optional bytes schema_version = 4;

    // The topic epoch assigned by the broker. This field will only be set if we
    // were requiring exclusive access when creating the producer.
    optional uint64 topic_epoch = 5;


    // 這個應該和上面ProducerAccessMode 相關,后面有機會來介紹這個吧
    // If producer is not "ready", the client will avoid to timeout the request
    // for creating the producer. Instead it will wait indefinitely until it gets 
    // a subsequent  `CommandProducerSuccess` with `producer_ready==true`.
    optional bool producer_ready = 6 [default = true];
}

CommandSend

這個是producer 用來發送消息到服務端用的RPC
可以通過org.apache.pulsar.client.impl.ProducerImpl.sendAsync 這個方法一路追到這個調用的位置,一般消息經過batch,加密,分塊等邏輯處理之后,會將消息序列化成這個請求。

具體序列化的格式是下面這個
BaseCommand就是CommandSend

// org.apache.pulsar.common.protocol.Commands
private static ByteBufPair serializeCommandSendWithSize(BaseCommand cmd, ChecksumType checksumType,
            MessageMetadata msgMetadata, ByteBuf payload) {
        // / Wire format
        // [TOTAL_SIZE] [CMD_SIZE][CMD] [MAGIC_NUMBER][CHECKSUM] [METADATA_SIZE][METADATA] [PAYLOAD]

這里面的protocol格式實際只包含了上面的 [CMD] 部分

message CommandSend {
    required uint64 producer_id = 1;
    required uint64 sequence_id = 2;
    optional int32 num_messages = 3 [default = 1];
    optional uint64 txnid_least_bits = 4 [default = 0];
    optional uint64 txnid_most_bits = 5 [default = 0];

    /// Add highest sequence id to support batch message with external sequence id
    optional uint64 highest_sequence_id = 6 [default = 0];
    optional bool is_chunk     =7 [default = false];
}

CommandSendReceipt

這個是服務端成功處理完消息持久化之后成功的response

message CommandSendReceipt {
    required uint64 producer_id = 1;
    // 這個是用來保證順序的
    required uint64 sequence_id = 2;
    optional MessageIdData message_id = 3;
    // 這個應該是用來去重的
    optional uint64 highest_sequence_id = 4 [default = 0];
}

// 這個是返回的寫入成功的消息id,這個結構會在其他位置復用
message MessageIdData {
    required uint64 ledgerId = 1;
    required uint64 entryId  = 2;
    optional int32 partition = 3 [default = -1];
    // 這里是
    optional int32 batch_index = 4 [default = -1];
    repeated int64 ack_set = 5;
    optional int32 batch_size = 6;
}

CommandSendError

這個是CommandSend 異常的response

message CommandSendError {
    required uint64 producer_id = 1;
    required uint64 sequence_id = 2;
    required ServerError error  = 3;
    required string message     = 4;
}

CommandFlow

這個是用來告知服務端我這個consumer當前可以接受消息的數目
服務端會記錄一個subscription里面每個consumer當前可以接受消息的數目
分配消息給哪個consumer的時候會按照這個數目來確定consumer當前能否接受消息。

目前了解到的位置是在connectionOpened的這個方法成功處理Subscription 注冊之后會發送一個CommandFlow 請求,來讓服務端推送消息。
不過可以想到,如果consumer隊列是空閑的狀態下都會發送這個消息。

message CommandFlow {
    required uint64 consumer_id       = 1;

    // Max number of messages to prefetch, in addition
    // of any number previously specified
    required uint32 messagePermits     = 2;
}

CommandMessage

這里實際上可能是服務端推消息給consumer,服務端會主動發送這個請求給consumer。(這個邏輯在服務端的 subscription 里的 dispatcher里面)

具體的調用位置在 org.apache.pulsar.broker.service.Consumer#sendMessages
這個方法在往上看一層的話都是org.apache.pulsar.broker.service.Dispatcher 這個類調用的。

這里和上面寫入的格式一樣這里的Command 實際上是一個RPC的header后面會加上消息的payload。

//  Wire format
// [TOTAL_SIZE] [CMD_SIZE][CMD] [MAGIC_NUMBER][CHECKSUM] [METADATA_SIZE][METADATA] [PAYLOAD]
//
// metadataAndPayload contains from magic-number to the payload included
message CommandMessage {
    required uint64 consumer_id       = 1;
    // 這里是消息的id
    required MessageIdData message_id = 2;
    // 這個消息重發了多少次
    optional uint32 redelivery_count  = 3 [default = 0];
    // 這個消息里面哪些已經被ack了
    repeated int64 ack_set = 4;
}

CommandAck

這個用來ack成功消費的消息,可以單獨ack一條消息,
也可以累積確認(類似kafka)。
這里為了減少RPC的頻率,在客戶端做了一個batch ack 的優化。
服務端的對應處理一般會更新ManagedCursor里面保存的數據,將這個ack的結果持久化。

message CommandAck {
    
    // ack 類型,是累積確認還是單獨確認
    enum AckType {
        Individual = 0;
        Cumulative = 1;
    }

    required uint64 consumer_id       = 1;
    required AckType ack_type         = 2;

    // 這里類型是repeated類型的可以把ack做batch

    // In case of individual acks, the client can pass a list of message ids
    repeated MessageIdData message_id = 3;

    // Acks can contain a flag to indicate the consumer
    // received an invalid message that got discarded
    // before being passed on to the application.
    enum ValidationError {
        UncompressedSizeCorruption = 0;
        DecompressionError = 1;
        ChecksumMismatch = 2;
        BatchDeSerializeError = 3;
        DecryptionError = 4;
    }

    // 一些異常情況可能也會ack這個消息,這里會記錄一些信息
    optional ValidationError validation_error = 4;


    repeated KeyLongValue properties = 5;

    optional uint64 txnid_least_bits = 6 [default = 0];
    optional uint64 txnid_most_bits = 7 [default = 0];

    // 網絡層請求id
    optional uint64 request_id = 8;
}

CommandRedeliverUnacknowledgedMessages

這個是consumer告訴服務端哪些消息需要重新被投遞的RPC

message CommandRedeliverUnacknowledgedMessages {
    required uint64 consumer_id = 1;
    repeated MessageIdData message_ids = 2;
}

CommandSuccess & CommandError

這個其實是一個公用的response,如果請求沒有特殊需要返回的字段的話,幾乎可以被所有的請求使用。
這里不像Kafka 每個request和response 都帶著一個ApiKey不會嚴格一一對應。

message CommandSuccess {
    required uint64 request_id = 1;
    optional Schema schema = 2;
}
message CommandError {
    required uint64 request_id  = 1;
    required ServerError error = 2;
    required string message    = 3;
}

CommandPing & CommandPong

這2個都是空的,主要作用是用來維護tcp連接應用層的keepAlive
org.apache.pulsar.common.protocol.PulsarHandler#handleKeepAliveTimeout

// Commands to probe the state of connection.
// When either client or broker doesn't receive commands for certain
// amount of time, they will send a Ping probe.
message CommandPing {
}
message CommandPong {
}

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容