摘要

在这一篇的文章中,我将从Sarama的同步生产者和异步生产者怎么创建开始讲起,然后我将向你介绍生产者中的各个参数是什么,怎么使用。

然后我将从创建生产者的代码开始,按照代码的调用流程慢慢深入,直到发送消息并接收到响应。

这个过程跟上面的文章说到的kafka各个层次其实是有对应关系的。

1.如何使用

1.1 介绍

在学习如何使用Sarama生产消息之前,我先稍微介绍一下。

Sarama有两种类型的生产者,同步生产者和异步生产者。

To produce messages, use either the AsyncProducer or the SyncProducer. The AsyncProducer accepts messages on a channel and produces them asynchronously in the background as efficiently as possible; it is preferred in most cases. The SyncProducer provides a method which will block until Kafka acknowledges the message as produced. This can be useful but comes with two caveats: it will generally be less efficient, and the actual durability guarantees depend on the configured value of Producer.RequiredAcks. There are configurations where a message acknowledged by the SyncProducer can still sometimes be lost.

官方文档的大致意思是异步生产者使用channel接收(生产成功或失败)的消息,并且也通过channel来发送消息,这样做通常是性能最高的。而同步生产者需要阻塞,直到收到了acks。但是这也带来了两个问题,一是性能变得更差了,而是可靠性是依靠参数acks来保证的。

1.2 异步发送

然后我们直接来看看Sarama是怎么发送异步消息的。

我们先来创建一个最简陋的异步生产者,省略所有的不必要的配置。

注意,为了更容易阅读,我删去了错误处理,并且用省略号替代。

  1. func main() {
  2. config := sarama.NewConfig()
  3. client, err := sarama.NewClient([]string{"localhost:9092"}, config)
  4. ...
  5. producer, err := sarama.NewAsyncProducerFromClient(client)
  6. ...
  7. defer producer.Close()
  8. topic := "topic-test"
  9. for i := 0; i <= 100; i++ {
  10. text := fmt.Sprintf("message %08d", i)
  11. producer.Input() <- &sarama.ProducerMessage{
  12. Topic: topic,
  13. Key: nil,
  14. Value: sarama.StringEncoder(text)}
  15. }
  16. }

可以看出,Sarama发送消息的套路就是先创建一个config,这里更多的config内容我们会在后文提到。

随后根据这个config,和broker地址,创建出生产者客户端。

再然后根据客户端来创建生产者对象(其实在这里用对象不够严谨,但是我认为这么理解是没有问题的)。

最后就可以使用这个生产者对象来发送信息了。

消息的构造过程中我也省略了其他的参数,只保留了最重要也是最必须的两个参数:主题和消息内容。

到了这里,一个简单的异步生产者发送消息的过程就结束了。

1.3 同步发送

在看完了异步发送之后,你可能会有很多的诸如“为什么要这么做”的疑问。

我们先来看看同步发送,再来对比一下:

  1. func main() {
  2. config := sarama.NewConfig()
  3. config.Producer.Return.Successes = true
  4. client, err := sarama.NewClient([]string{"localhost:9092"}, config)
  5. ...
  6. producer, err := sarama.NewSyncProducerFromClient(client)
  7. ...
  8. defer producer.Close()
  9. topic := "topic-test"
  10. for i := 0; i <= 10; i++ {
  11. text := fmt.Sprintf("message %08d", i)
  12. partition, offset, err := producer.SendMessage(
  13. &sarama.ProducerMessage{
  14. Topic: topic,
  15. Key: nil,
  16. Value: sarama.StringEncoder(text)})
  17. ...
  18. log.Println("send message success, partition = ", partition, " offset = ", offset)
  19. }
  20. }

可以看出同步发送跟异步发送的过程是很相似的。

不同的地方在于,同步生产者发送消息,使用的不是channel,并且SendMessage方法有三个返回的值,分别为这条消息的被发送到了哪个partition,处于哪个offset,是否有error

也就是说,只有在消息成功的发送并写入了broker,才会有返回值。

2. 配置

2.1 默认配置

我们顺着源码看一下这一行:

  1. config := sarama.NewConfig()

可以看到Sarama已经返回了一个默认的config了:

  1. // NewConfig returns a new configuration instance with sane defaults.
  2. func NewConfig() *Config {
  3. c := &Config{}
  4. c.Producer.MaxMessageBytes = 1000000
  5. c.Producer.RequiredAcks = WaitForLocal
  6. c.Producer.Timeout = 10 * time.Second
  7. ...
  8. }

2.2 可选配置

我们来看看Config这个结构体,里面有哪些配置项是允许用户自定义的。

因为实在是太长了,限于篇幅以及作者的学识,在这篇文章中不能一一讲解,所以在这篇文章只会选取部分生产者相关的配置进行讲解。

但是无论是Golang客户端,还是Java客户端,都不重要,你只需要知道哪些参数对于你的生产者的生产速度、消息的可靠性等有关系就可以了。

  1. // Config is used to pass multiple configuration options to Sarama's constructors.
  2. type Config struct {
  3. Admin struct {
  4. ...
  5. }
  6. Net struct {
  7. ...
  8. }
  9. Metadata struct {
  10. ...
  11. }
  12. Producer struct {
  13. ...
  14. }
  15. Consumer struct {
  16. ...
  17. }
  18. ClientID string
  19. ...
  20. }

我们可以看出,关于Sarama的配置,分成了很多个部分,我们来具体看一看Producer的这部分。

2.3 重要的生产者参数

在这里我打算介绍一部分我个人认为比较重要的生产者参数。

    1. MaxMessageBytes int

这个参数影响了一条消息的最大字节数,默认是1000000。但是注意,这个参数必须要小于broker中的 message.max.bytes

    1. RequiredAcks RequiredAcks

这个参数影响了消息需要被多少broker写入之后才返回。取值可以是0、1、-1,分别代表了不需要等待broker确认才返回、需要分区的leader确认后才返回、以及需要分区的所有副本确认后返回。

    1. Partitioner PartitionerConstructor

这个是分区器。Sarama默认提供了几种分区器,如果不指定默认使用Hash分区器。

    1. Retry

这个参数代表了重试的次数,以及重试的时间,主要发生在一些可重试的错误中。

    1. Flush

用于设置将消息打包发送,简单来讲就是每次发送消息到broker的时候,不是生产一条消息就发送一条消息,而是等消息累积到一定的程度了,再打包发送。所以里面含有两个参数。一个是多少条消息触发打包发送,一个是累计的消息大小到了多少,然后发送。

2.4 幂等生产者

在聊幂等生产者之前,我们先来看看生产者中另外一个很重要的参数:

    1. MaxOpenRequests int

这个参数代表了允许没有收到acks而可以同时发送的最大batch数。

    1. Idempotent bool

用于幂等生产者,当这一项设置为true的时候,生产者将保证生产的消息一定是有序且精确一次的。

为什么会需要这个选项呢?

当MaxOpenRequests这个参数配置大于1的时候,代表了允许有多个请求发送了还没有收到回应。假设此时的重试次数也设置为了大于1,当同时发送了2个请求,如果第一个请求发送到broker中,broker写入失败了,但是第二个请求写入成功了,那么客户端将重新发送第一个消息的请求,这个时候会造成乱序。

又比如当第一个请求返回acks的时候,因为网络原因,客户端没有收到,所以客户端进行了重发,这个时候就会造成消息的重复。

所以,幂等生产者就是为了保证消息发送到broker中是有序且不重复的。

消息的有序可以通过MaxOpenRequests设置为1来保证,这个时候每个消息必须收到了acks才能发送下一条,所以一定是有序的,但是不能够保证不重复。

而且当MaxOpenRequests设置为1的时候,吞吐量不高。

注意,当启动幂等生产者的时候,Retry次数必须要大于0,ack必须为all。

在Java客户端中,允许MaxOpenRequests小于等于5。

但是在Sarama中有一个很奇怪的地方我也没有研究明白,我们直接看一看这部分的代码:

  1. if c.Producer.Idempotent {
  2. if !c.Version.IsAtLeast(V0_11_0_0) {
  3. return ConfigurationError("Idempotent producer requires Version >= V0_11_0_0")
  4. }
  5. if c.Producer.Retry.Max == 0 {
  6. return ConfigurationError("Idempotent producer requires Producer.Retry.Max >= 1")
  7. }
  8. if c.Producer.RequiredAcks != WaitForAll {
  9. return ConfigurationError("Idempotent producer requires Producer.RequiredAcks to be WaitForAll")
  10. }
  11. if c.Net.MaxOpenRequests > 1 {
  12. return ConfigurationError("Idempotent producer requires Net.MaxOpenRequests to be 1")
  13. }
  14. }

这一部分第一项是版本号,没问题,第二第三项是RetryAcks,也没有问题。问题在于第四项,这里的MaxOpenRequests参数,我想应该等同于Java客户端中的MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION,按照Java客户端中的配置,应该是这个参数小于等于5,即可保证幂等,但是这里必须得设置为1。

检查了Sarama的Issue,有开发者提出了这个问题,但是目前作者还没有打算解决。

3 broker

在这一节的内容中,我将会从代码的层面介绍 Sarama 生产者发送消息的全过程。但是因为代码很多,我将会省略一些内容,包括一些错误处理、重试等。

这些都很重要,也不应该被省略。但是因为篇幅有限,我只能介绍最核心的发送消息这一部分的内容。

我会在贴代码之前,大概的说一下这段代码的思路。随后,我会在代码中加入一些注释,来更详细的进行解释。

然后我们开始吧!

  1. producer, err := sarama.NewAsyncProducer([]string{"localhost:9092"}, config)

一切都从这么一行开始讲起。

我们进去看看。

在这里其实就只有两个部分,先是通过地址配置,构建一个 client

  1. func NewAsyncProducer(addrs []string, conf *Config) (AsyncProducer, error) {
  2. // 构建client
  3. client, err := NewClient(addrs, conf)
  4. if err != nil {
  5. return nil, err
  6. }
  7. // 构建AsyncProducer
  8. return newAsyncProducer(client)
  9. }

3.1 Client的创建

在创建 Client 的过程中,先构建一个 client 结构体。

里面的参数我们先不管,等用到了再进行解释。

然后创建完之后,刷新元数据,并且启动一个协程,在后台进行刷新。

  1. func NewClient(addrs []string, conf *Config) (Client, error) {
  2. ...
  3. // 构建一个client
  4. client := &client{
  5. conf: conf,
  6. closer: make(chan none),
  7. closed: make(chan none),
  8. brokers: make(map[int32]*Broker),
  9. metadata: make(map[string]map[int32]*PartitionMetadata),
  10. metadataTopics: make(map[string]none),
  11. cachedPartitionsResults: make(map[string][maxPartitionIndex][]int32),
  12. coordinators: make(map[string]int32),
  13. }
  14. // 把用户输入的broker地址作为“种子broker”增加到seedBrokers中
  15. // 随后客户端会根据已有的broker地址,自动刷新元数据,以获取更多的broker地址
  16. // 所以称之为种子
  17. random := rand.New(rand.NewSource(time.Now().UnixNano()))
  18. for _, index := range random.Perm(len(addrs)) {
  19. client.seedBrokers = append(client.seedBrokers, NewBroker(addrs[index]))
  20. }
  21. ...
  22. // 启动协程在后台刷新元数据
  23. go withRecover(client.backgroundMetadataUpdater)
  24. return client, nil
  25. }

3.2 元数据的更新

后台更新元数据的设计其实很简单,利用一个 ticker ,按时对元数据进行更新,直到 client 关闭。

这里先提一下我们说的元数据,有哪些内容。

你可以简单的理解为包含了所有 broker 的地址(因为 broker 可能新增,也可能减少),以及包含了哪些 topic ,这些 topic 有哪些 partition 等。

  1. func (client *client) backgroundMetadataUpdater() {
  2. // 按照配置的时间更新元数据
  3. ticker := time.NewTicker(client.conf.Metadata.RefreshFrequency)
  4. defer ticker.Stop()
  5. // 循环获取channel,判断是执行更新操作还是终止
  6. for {
  7. select {
  8. case <-ticker.C:
  9. if err := client.refreshMetadata(); err != nil {
  10. Logger.Println("Client background metadata update:", err)
  11. }
  12. case <-client.closer:
  13. return
  14. }
  15. }
  16. }

然后我们继续来看看 client.refreshMetadata() 这个方法,这个方法是判断了一下需要刷新哪些主题的元数据,还是说全部主题的元数据。

然后我们继续。

在这里也还没有涉及到具体的更新操作。我们看 tryRefreshMetadata 这个方法的参数可以得知,在这里我们设置了需要刷新元数据的主题,重试的次数,超时的时间。

  1. func (client *client) RefreshMetadata(topics ...string) error {
  2. deadline := time.Time{}
  3. if client.conf.Metadata.Timeout > 0 {
  4. deadline = time.Now().Add(client.conf.Metadata.Timeout)
  5. }
  6. // 设置参数
  7. return client.tryRefreshMetadata(topics, client.conf.Metadata.Retry.Max, deadline)
  8. }

然后终于来到了tryRefreshMetadata这个方法。

在这个方法中,会选取已经存在的broker,构造获取元数据的请求。

在收到回应后,如果不存在任何的错误,就将这些元数据用于更新客户端。

  1. func (client *client) tryRefreshMetadata(topics []string, attemptsRemaining int, deadline time.Time) error {
  2. ...
  3. broker := client.any()
  4. for ; broker != nil && !pastDeadline(0); broker = client.any() {
  5. ...
  6. req := &MetadataRequest{
  7. Topics: topics,
  8. // 是否允许创建不存在的主题
  9. AllowAutoTopicCreation: allowAutoTopicCreation
  10. }
  11. response, err := broker.GetMetadata(req)
  12. switch err.(type) {
  13. case nil:
  14. allKnownMetaData := len(topics) == 0
  15. // 对元数据进行更新
  16. shouldRetry, err := client.updateMetadata(response, allKnownMetaData)
  17. if shouldRetry {
  18. Logger.Println("client/metadata found some partitions to be leaderless")
  19. return retry(err)
  20. }
  21. return err
  22. case ...
  23. ...
  24. }
  25. }

然后我们继续往下看看当客户端拿到了 response 之后,是如何更新的。

首先,先对本地保存 broker 进行更新。

然后,对 topic 进行更新,以及这个 topic 下面的那些 partition

  1. func (client *client) updateMetadata(data *MetadataResponse, allKnownMetaData bool) (retry bool, err error) {
  2. ...
  3. // 假设返回了新的broker id,那么保存这些新的broker,这意味着增加了broker、或者下线的broker重新上线了
  4. // 如果返回的id我们已经保存了,但是地址变化了,那么更新地址
  5. // 如果本地保存的一些id没有返回,说明这些broker下线了,那么删除他们
  6. client.updateBroker(data.Brokers)
  7. // 然后对topic也进行元数据的更新
  8. // 主要是更新topic以及topic对应的partition
  9. for _, topic := range data.Topics {
  10. ...
  11. // 更新每个topic以及对应的partition
  12. client.metadata[topic.Name] = make(map[int32]*PartitionMetadata, len(topic.Partitions))
  13. for _, partition := range topic.Partitions {
  14. client.metadata[topic.Name][partition.ID] = partition
  15. ...
  16. }
  17. }

至此,我们元数据的更新就说完了。

下面我们来说一说在更新元数据之前,broker是如何建立连接的,以及请求是如何发送出去,又是如何被broker接收的。

3.3 与Broker建立连接

让我们回到 tryRefreshMetadata 这个方法中。

这个方法里面有这么一行代码:

  1. broker := client.any()

我们进去看看。

在这个方法里, 如果 seedBrokers 存在,那么就打开它,否则的话打开其他的broker。

注意,这里提到的其他的broker,可能是在刷新元数据的时候,获取到的。这就跟上面的内容联系在一起了。

  1. func (client *client) any() *Broker {
  2. ...
  3. if len(client.seedBrokers) > 0 {
  4. _ = client.seedBrokers[0].Open(client.conf)
  5. return client.seedBrokers[0]
  6. }
  7. // 不保证一定是按顺序的
  8. for _, broker := range client.brokers {
  9. _ = broker.Open(client.conf)
  10. return broker
  11. }
  12. return nil
  13. }

然后再让我们看看 Open方法做了什么。

Open方法异步的建立了一个tcp连接,然后创建了一个缓冲大小为MaxOpenRequestschannel

这个名为 responseschannel ,用于接收从 broker发送回来的消息。

其实在 broker 中,用于发送消息跟接收消息的 channel 都设置成了这个大小。

MaxOpenRequests 这个参数你可以理解为是Java客户端中的max.in.flight.requests.per.connection

然后,又启动了一个协程,用于接收消息。

  1. func (b *Broker) Open(conf *Config) error {
  2. if conf == nil {
  3. conf = NewConfig()
  4. }
  5. ...
  6. go withRecover(func() {
  7. ...
  8. dialer := conf.getDialer()
  9. b.conn, b.connErr = dialer.Dial("tcp", b.addr)
  10. ...
  11. b.responses = make(chan responsePromise, b.conf.Net.MaxOpenRequests-1)
  12. ...
  13. go withRecover(b.responseReceiver)
  14. })

3.4 从Broker接收响应

我们来看看 responseReceiver 是怎么工作的。

其实很容易理解,当 broker 收到一个 response 的时候,先解析消息的头部,然后再解析消息的内容。并把这些内容写进 responsepackets 中。

  1. func (b *Broker) responseReceiver() {
  2. for response := range b.responses {
  3. ...
  4. // 先根据Header的版本读取对应长度的Header
  5. var headerLength = getHeaderLength(response.headerVersion)
  6. header := make([]byte, headerLength)
  7. bytesReadHeader, err := b.readFull(header)
  8. decodedHeader := responseHeader{}
  9. err = versionedDecode(header, &decodedHeader, response.headerVersion)
  10. ...
  11. // 解析具体的内容
  12. buf := make([]byte, decodedHeader.length-int32(headerLength)+4)
  13. bytesReadBody, err := b.readFull(buf)
  14. // 省略了一些错误处理,总之,如果发生了错误,就把错误信息写进 response.errors 中
  15. response.packets <- buf
  16. }
  17. }

其实接收响应这部分的代码逻辑很容易理解,就是当 response 这个 channel 有了消息,就读取,然后将读取到的内容写进 response 中。

那么你可能会有一个问题,什么时候才会往response 这个 channel 发送消息呢?

很容易可以猜到,当我们发送了消息给 broker ,就应该要通知 receiver ,准备接受消息了。

既然如此,我们继续刚刚刷新元数据的部分,看看 sarama 是如何把消息发送出去的。

3.5 发送与接受消息

我们回到这一行代码:

  1. response, err := broker.GetMetadata(req)

我们直接进去,发现在这里构造了一个接受返回信息的结构体,然后调用了sendAndReceive方法。

  1. func (b *Broker) GetMetadata(request *MetadataRequest) (*MetadataResponse, error) {
  2. response := new(MetadataResponse)
  3. err := b.sendAndReceive(request, response)
  4. if err != nil {
  5. return nil, err
  6. }
  7. return response, nil
  8. }

我们继续往下。

在这里我们可以看到,先是调用了send方法,然后返回了一个promise。并且当有消息写入这个promise的时候,就得到了结果。

而且回想一下我们在receiver中,是不是把获取到的 response 写进了 packets ,把错误结果写进了 errors 呢,跟这里是一致的对吧?

  1. func (b *Broker) sendAndReceive(req protocolBody, res protocolBody) error {
  2. responseHeaderVersion := int16(-1)
  3. if res != nil {
  4. responseHeaderVersion = res.headerVersion()
  5. }
  6. promise, err := b.send(req, res != nil, responseHeaderVersion)
  7. if err != nil {
  8. return err
  9. }
  10. if promise == nil {
  11. return nil
  12. }
  13. // 这里的promise,是上面send方法返回的
  14. select {
  15. case buf := <-promise.packets:
  16. return versionedDecode(buf, res, req.version())
  17. case err = <-promise.errors:
  18. return err
  19. }
  20. }

带着这个想法,我们看看 send 方法做了什么事。

这个地方很重要,也是我认为 Sarama 设计的特别巧妙的一个地方。

在send方法中,把需要发送的消息通过与broker的tcp连接,同步发送到broker中。

然后构建了一个responsePromise类型的channel,然后直接将这个结构体丢进这个channel中。然后回想一下,我们在responseReceiver这个方法中,不断消费接收到的response。

此时在responseReceiver中,收到了send方法传递的responsePromise,他就会通过conn来读取数据,然后将数据写入这个responsePromise的packets中,或者将错误信息写入errors中。

而此时,再看看send方法,他返回了这个responsePromise的指针。所以,sendAndReceive方法就在等待这个responsePromise内的packets或者errors的channel被写入数据。当responseReceiver接收到了响应并且写入数据的时候,packets或者errors就会被写入消息。

  1. func (b *Broker) send(rb protocolBody, promiseResponse bool, responseHeaderVersion int16) (*responsePromise, error) {
  2. ...
  3. // 将请求的内容封装进 request ,然后发送到Broker中
  4. // 注意一下这里的 b.write(buf)
  5. // 里面做了 b.conn.Write(buf) 这件事情
  6. req := &request{correlationID: b.correlationID, clientID: b.conf.ClientID, body: rb}
  7. buf, err := encode(req, b.conf.MetricRegistry)
  8. bytes, err := b.write(buf)
  9. ...
  10. // 如果我们的response为nil,也就是说当不需要response的时候,是不会放进inflight发送队列的
  11. if !promiseResponse {
  12. // Record request latency without the response
  13. b.updateRequestLatencyAndInFlightMetrics(time.Since(requestTime))
  14. return nil, nil
  15. }
  16. // 构建一个接收响应的 channel ,返回这个channel的指针
  17. // 这个 channel 内部包含了两个 channel,一个用来接收响应,一个用来接收错误
  18. promise := responsePromise{requestTime, req.correlationID, responseHeaderVersion, make(chan []byte), make(chan error)}
  19. b.responses <- promise
  20. // 这里返回指针特别的关键,是把消息的发送跟消息的接收联系在一起了
  21. return &promise, nil
  22. }

让我们来用一张图说明一下上面这个发送跟接收的过程:

这一段比较绕,但这也是Sarama发送与接受消息的核心内容,希望我的解释能够让你理解:)

4 AsyncProcuder

在上一节中,我们已经分析了client的构造全过程,并且在构造client刷新元数据的时候,也解释了sarama是如何发送消息以及接受消息的。

在这一节中,我打算解释一下AsyncProcuder是如何发送消息的。

因为有了上一节的铺垫,这一节的内容应该会比较容易理解。

我们从newAsyncProducer(client)这一行开始讲起。

我们先说说input:make(chan *ProducerMessage),这个事关我们的消息发送。注意到这个channel是没有缓冲的。

也就是说当我们发送一条消息到input中的时候,此时发送方会阻塞,这说明了之后的操作必须不能够被阻塞,否则会影响消息的发送效率。

然后其他字段我们先不管,后面用到了我们再提。

  1. func newAsyncProducer(client Client) (AsyncProducer, error) {
  2. ...
  3. p := &asyncProducer{
  4. client: client,
  5. conf: client.Config(),
  6. errors: make(chan *ProducerError),
  7. input: make(chan *ProducerMessage),
  8. successes: make(chan *ProducerMessage),
  9. retries: make(chan *ProducerMessage),
  10. brokers: make(map[*Broker]*brokerProducer),
  11. brokerRefs: make(map[*brokerProducer]int),
  12. txnmgr: txnmgr,
  13. }
  14. go withRecover(p.dispatcher)
  15. go withRecover(p.retryHandler)
  16. }

4.1 dispatcher

我们往下看看下面协程启动的go withRecover(p.dispatcher)

在这个方法中,首先创建了一个以Topic为key的map,这个map的value是无缓冲的channel。

到这里我们很容易可以推测得出,当通过input发送一条消息的时候,消息会到dispatcher这里,被分配到各个Topic中。

注意,在这个时候,channel还是无缓冲的,所以我们可以推测下一步的操作,依旧是无阻塞的。

  1. func (p *asyncProducer) dispatcher() {
  2. handlers := make(map[string]chan<- *ProducerMessage)
  3. ...
  4. for msg := range p.input {
  5. ...
  6. // 拦截器
  7. for _, interceptor := range p.conf.Producer.Interceptors {
  8. msg.safelyApplyInterceptor(interceptor)
  9. }
  10. ...
  11. // 找到这个Topic对应的Handler
  12. handler := handlers[msg.Topic]
  13. if handler == nil {
  14. // 如果此时还不存在这个Topic对应的Handler,那么创建一个
  15. // 虽然说他叫Handler,但他其实是一个无缓冲的
  16. handler = p.newTopicProducer(msg.Topic)
  17. handlers[msg.Topic] = handler
  18. }
  19. // 然后把这条消息写进这个Handler中
  20. handler <- msg
  21. }
  22. }

然后让我们来handler = p.newTopicProducer(msg.Topic)这一行的代码。

在这里创建了一个缓冲大小为ChannelBufferSize的channel,用于存放发送到这个主题的消息。

然后创建了一个topicProducer,在这个时候你可以认为消息已经交付给各个topic的topicProducer了。

  1. func (p *asyncProducer) newTopicProducer(topic string) chan<- *ProducerMessage {
  2. input := make(chan *ProducerMessage, p.conf.ChannelBufferSize)
  3. tp := &topicProducer{
  4. parent: p,
  5. topic: topic,
  6. input: input,
  7. breaker: breaker.New(3, 1, 10*time.Second),
  8. handlers: make(map[int32]chan<- *ProducerMessage),
  9. partitioner: p.conf.Producer.Partitioner(topic),
  10. }
  11. go withRecover(tp.dispatch)
  12. return input
  13. }

4.2 topicDispatch

然后我们来看看go withRecover(tp.dispatch)这一行代码。

同样是启动了一个协程,来处理消息。

也就是说,到了这一步,对于每一个Topic,都有一个协程来处理消息。

在这个dispatch()方法中,也同样的接收到一条消息,就会去找这条消息所在的分区的channel,然后把消息写进去。

  1. func (tp *topicProducer) dispatch() {
  2. for msg := range tp.input {
  3. ...
  4. // 同样是找到这条消息所在的分区对应的channel,然后把消息丢进去
  5. handler := tp.handlers[msg.Partition]
  6. if handler == nil {
  7. handler = tp.parent.newPartitionProducer(msg.Topic, msg.Partition)
  8. tp.handlers[msg.Partition] = handler
  9. }
  10. handler <- msg
  11. }
  12. }

4.3 PartitionDispatch

我们进tp.parent.newPartitionProducer(msg.Topic, msg.Partition)这里看看。

你可以发现partitionProducer跟topicProducer是很像的。

其实他们就是代表了一条消息的分发,从producer到topic到partition。

注意,这里面的channel缓冲大小,也是ChannelBufferSize。

  1. func (p *asyncProducer) newPartitionProducer(topic string, partition int32) chan<- *ProducerMessage {
  2. input := make(chan *ProducerMessage, p.conf.ChannelBufferSize)
  3. pp := &partitionProducer{
  4. parent: p,
  5. topic: topic,
  6. partition: partition,
  7. input: input,
  8. breaker: breaker.New(3, 1, 10*time.Second),
  9. retryState: make([]partitionRetryState, p.conf.Producer.Retry.Max+1),
  10. }
  11. go withRecover(pp.dispatch)
  12. return input
  13. }

4.4 partitionProducer

到了这一步,我们再来看看消息到了每个partition所在的channel,是如何处理的。

其实在这一步中,主要是做一些错误处理之类的,然后把消息丢进brokerProducer。

可以理解为这一步是业务逻辑层到网络IO层的转变,在这之前我们只关心消息去到了哪个分区,而在这之后,我们需要找到这个分区所在的broker的地址,并使用之前已经建立好的TCP连接,发送这条消息。

  1. func (pp *partitionProducer) dispatch() {
  2. // 找到这个主题和分区的leader所在的broker
  3. pp.leader, _ = pp.parent.client.Leader(pp.topic, pp.partition)
  4. // 如果此时找到了这个leader
  5. if pp.leader != nil {
  6. pp.brokerProducer = pp.parent.getBrokerProducer(pp.leader)
  7. pp.parent.inFlight.Add(1)
  8. // 发送一条消息来表示同步
  9. pp.brokerProducer.input <- &ProducerMessage{Topic: pp.topic, Partition: pp.partition, flags: syn}
  10. }
  11. ...// 各种异常情况
  12. // 然后把消息丢进brokerProducer中
  13. pp.brokerProducer.input <- msg
  14. }

4.5 brokerProducer

到了这里,大概算是整个发送流程最后的一个步骤了。

我们来看看pp.parent.getBrokerProducer(pp.leader)这行代码里面的内容。

其实就是找到asyncProducer中的brokerProducer,如果不存在,则创建一个。

  1. func (p *asyncProducer) getBrokerProducer(broker *Broker) *brokerProducer {
  2. p.brokerLock.Lock()
  3. defer p.brokerLock.Unlock()
  4. bp := p.brokers[broker]
  5. if bp == nil {
  6. bp = p.newBrokerProducer(broker)
  7. p.brokers[broker] = bp
  8. p.brokerRefs[bp] = 0
  9. }
  10. p.brokerRefs[bp]++
  11. return bp
  12. }

那我们就来看看brokerProducer是怎么创建出来的。

看这个方法中启动的第二个协程,我们可以推测bridge这个channel收到消息后,会把收到的消息打包成一个request,然后调用Produce方法。

并且,将返回的结果的指针地址,写进response中。

然后构造好brokerProducerResponse,并且写入responses中。

  1. func (p *asyncProducer) newBrokerProducer(broker *Broker) *brokerProducer {
  2. var (
  3. input = make(chan *ProducerMessage)
  4. bridge = make(chan *produceSet)
  5. responses = make(chan *brokerProducerResponse)
  6. )
  7. bp := &brokerProducer{
  8. parent: p,
  9. broker: broker,
  10. input: input,
  11. output: bridge,
  12. responses: responses,
  13. stopchan: make(chan struct{}),
  14. buffer: newProduceSet(p),
  15. currentRetries: make(map[string]map[int32]error),
  16. }
  17. go withRecover(bp.run)
  18. // minimal bridge to make the network response `select`able
  19. go withRecover(func() {
  20. for set := range bridge {
  21. request := set.buildRequest()
  22. response, err := broker.Produce(request)
  23. responses <- &brokerProducerResponse{
  24. set: set,
  25. err: err,
  26. res: response,
  27. }
  28. }
  29. close(responses)
  30. })
  31. if p.conf.Producer.Retry.Max <= 0 {
  32. bp.abandoned = make(chan struct{})
  33. }
  34. return bp
  35. }

让我们再来看看broker.Produce(request)这一行代码。

是不是很熟悉呢,我们在client部分讲到的sendAndReceive方法。

而且我们可以发现,如果我们设置了需要Acks,就会返回一个response;如果没设置,那么消息发出去之后,就不管了。

此时在获取了response,并且填入了response的内容后,返回这个response的内容。

  1. func (b *Broker) Produce(request *ProduceRequest) (*ProduceResponse, error) {
  2. var (
  3. response *ProduceResponse
  4. err error
  5. )
  6. if request.RequiredAcks == NoResponse {
  7. err = b.sendAndReceive(request, nil)
  8. } else {
  9. response = new(ProduceResponse)
  10. err = b.sendAndReceive(request, response)
  11. }
  12. if err != nil {
  13. return nil, err
  14. }
  15. return response, nil
  16. }

至此,Sarama生产者相关的内容就介绍完毕了。

写在最后

这一篇写的实在是有些久了。

主要是作者这段时间实在是太忙了,还没有完全平衡好目前的学习工作和生活,导致每天花在学习上的时间不多,效率也不高。

另外就是网上我也没有查到有Sarama相关的解析,都是一些API的调用。因为作者恰好开始学习Kafka,为了更好地了解生产者的每一个参数,我选择去研究生产者客户端。

但是,因为作者源码阅读能力实在是有限,在这个过程中很有可能会有一些错误的理解。所以当你发现了一些违和的地方,也请不吝指教,谢谢你!

再次感谢你能看到这里!

PS:如果有其他的问题,也可以在公众号找到我,欢迎来找我玩~

Kafka入门(3):Sarama生产者是如何工作的的更多相关文章

  1. 《OD大数据实战》Kafka入门实例

    官网: 参考文档: Kafka入门经典教程 Kafka工作原理详解 一.安装zookeeper 1. 下载zookeeper-3.4.5-cdh5.3.6.tar.gz 下载地址为: http://a ...

  2. Kafka 入门三问

    目录 1 Kafka 是什么? 1.1 背景 1.2 定位 1.3 产生的原因 1.4 Kafka 有哪些特征 消息和批次 模式 主题和分区 生产者和消费者 broker 和 集群 1.5 Kafka ...

  3. 【转帖】Kafka入门介绍

    Kafka入门介绍 https://www.cnblogs.com/swordfall/p/8251700.html 最近在看hdoop的hdfs 以及看了下kafka的底层存储,发现分布式的技术基本 ...

  4. [转帖]kafka入门:简介、使用场景、设计原理、主要配置及集群搭建

    kafka入门:简介.使用场景.设计原理.主要配置及集群搭建 http://www.aboutyun.com/thread-9341-1-1.html 还没看完 感觉挺好的. 问题导读: 1.zook ...

  5. kafka 入门

    李克华 云计算高级群: 292870151 195907286 交流:Hadoop.NoSQL.分布式.lucene.solr.nutch  kafka入门:简介.使用场景.设计原理.主要配置及集群搭 ...

  6. Kafka入门介绍

    1. Kafka入门介绍 1.1 Apache Kafka是一个分布式的流平台.这到底意味着什么? 我们认为,一个流平台具有三个关键能力: ① 发布和订阅消息.在这方面,它类似一个消息队列或企业消息系 ...

  7. Kafka入门 --安装和简单实用

    一.安装Zookeeper 参考: Zookeeper的下载.安装和启动 Zookeeper 集群搭建--单机伪分布式集群 二.下载Kafka 进入http://kafka.apache.org/do ...

  8. Kafka技术内幕 读书笔记之(一) Kafka入门

    在0.10版本之前, Kafka仅仅作为一个消息系统,主要用来解决应用解耦. 异步消息 . 流量削峰等问题. 在0.10版本之后, Kafka提供了连接器与流处理的能力,它也从分布式的消息系统逐渐成为 ...

  9. 转 Kafka入门经典教程

    Kafka入门经典教程 http://www.aboutyun.com/thread-12882-1-1.html 问题导读 1.Kafka独特设计在什么地方?2.Kafka如何搭建及创建topic. ...

随机推荐

  1. PHP date_create_from_format() 函数

    ------------恢复内容开始------------ 实例 返回一个根据指定格式进行格式化的新的 DateTime 对象: <?php$date=date_create_from_for ...

  2. 牛客练习赛63 牛牛的斐波那契字符串 矩阵乘法 KMP

    LINK:牛牛的斐波那契字符串 虽然sb的事实没有改变 但是 也不会改变. 赛时 看了E和F题 都不咋会写 所以弃疗了. 中午又看了一遍F 发现很水 差分了一下就过了. 这是下午和古队长讨论+看题解的 ...

  3. CF掉分日记 6.6 6.8

    ---恢复内容开始--- 写的效果依旧不好 还没写完前四题比赛就结束了 而且这些普及组的题目 我大多还是缺少简单算法的灵性 总是把问题搞复杂化. 6.5 A 第一道题非常水 简单分析发现是一个快速幂的 ...

  4. Maven 配置编译版本

    pom.xml <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</gro ...

  5. SpringMvc响应数据和结果视图

    响应数据和结果视图 返回值分类 字符串 controller 方法返回字符串可以指定逻辑视图名,通过视图解析器解析为物理视图地址. //指定逻辑视图名,经过视图解析器解析为 jsp 物理路径:/WEB ...

  6. SAFe必备——提高团队敏捷性

    规模化敏捷之于项目群,就像Scrum之于敏捷团队.为了创建高质量业务解决方案,企业需要提高自身能力,提升团队和技术敏捷性,实现真正的规模化敏捷. 敏捷发布火车 实现团队和技术敏捷性,首先需要敏捷团队围 ...

  7. SpringCloud系列之服务容错保护Netflix Hystrix

    1. 什么是雪崩效应? 微服务环境,各服务之间是经常相互依赖的,如果某个不可用,很容易引起连锁效应,造成整个系统的不可用,这种现象称为服务雪崩效应. 如图,引用国外网站的图例:https://www. ...

  8. 1、迭代器 Iterator模式 一个一个遍历 行为型设计模式

    1.Iterator模式 迭代器(iterator)有时又称游标(cursor)是程序设计的软件设计模式,可在容器(container,例如链表或者阵列)上遍访的接口,设计人员无需关心容器的内容. I ...

  9. Vue老项目支持Webpack打包

    1.老的vue项目支持webpack打包 最近在学习Vue.js.版本是2.6,webpack的版本也相对较老,是2.1.0版本.项目脚手架只配置了npm run dev和npm run build. ...

  10. 使用 .NET Core 3.x 构建 RESTFUL Api

    准备工作:在此之前你需要了解关于.NET .Core的基础,前面几篇文章已经介绍:https://www.cnblogs.com/hcyesdo/p/12834345.html 首先需要明确一点的就是 ...