Kafka(Go)教程(六)---sarama 客户端 producer 源码分析

第一节:kafka golang sarama初体验 关于Kafka的开发库,以Java的居多,Golang语言的开发库,主要推荐的是sarama项目地址:这个项目之前地址是https://github.com/Shopify/sarama 通过源地址也可以访问本节,我们简单通过golang语言,操作sarama.NewClient对象,查询了kafka集群的topic列表,brokers列表,控制器ID。希望大家对sarama有了初步认识。 阅读详情

来自:指月 https://www.lixueduan.com

原文:https://www.lixueduan.com/post/kafka/06-sarama-producer/

本文主要通过源码分析了 Kafka Go sarama 客户端生产者的实现原理,包括消息分发流程,消息打包处理,以及最终发送到 Kafka 等具体步骤,最后通过分析总结出的常见性能优化手段。

本文基于 sarama v1.29.1

1. 概述

Kafka 系列相关代码见 Github

具体流程如下图:

Sarama Producer 流程.png

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来保证的。

异步生产者 Demo 如下:

func Producer(topic string, limit int) {
	config := sarama.NewConfig()
	// 异步生产者不建议把 Errors 和 Successes 都开启,一般开启 Errors 就行
	// 同步生产者就必须都开启,因为会同步返回发送成功或者失败
	config.Producer.Return.Errors = false   // 设定需要返回错误信息
	config.Producer.Return.Successes = true // 设定需要返回成功信息
	producer, err := sarama.NewAsyncProducer([]string{kafka.HOST}, config)
	if err != nil {
		log.Fatal("NewSyncProducer err:", err)
	}
	defer producer.AsyncClose()
	go func() {
		// [!important] 异步生产者发送后必须把返回值从 Errors 或者 Successes 中读出来 不然会阻塞 sarama 内部处理逻辑 导致只能发出去一条消息
		select {
		case _ = <-producer.Successes():
		case e := <-producer.Errors():
			if e != nil {
				log.Printf("[Producer] err:%v msg:%+v \n", e.Msg, e.Err)
			}
		}
	}()
	// 异步发送
	for i := 0; i < limit; i++ {
		str := strconv.Itoa(int(time.Now().UnixNano()))
		msg := &sarama.ProducerMessage{Topic: topic, Key: nil, Value: sarama.StringEncoder(str)}
		// 异步发送只是写入内存了就返回了,并没有真正发送出去
		// sarama 库中用的是一个 channel 来接收,后台 goroutine 异步从该 channel 中取出消息并真正发送
		producer.Input() <- msg
		atomic.AddInt64(&count, 1)
		if atomic.LoadInt64(&count)%1000 == 0 {
			log.Printf("已发送消息数:%v\n", count)
		}

	}
	log.Printf("发送完毕 总发送消息数:%v\n", limit)
}

可以看到整个 API 使用起来还是非常简单的:

  • 1)NewAsyncProducer() :创建 一个 producer 对象
  • 2)producer.Input() <- msg :发送消息
  • 3)s = <-producer.Successes(),e := <-producer.Errors() :异步获取成功或失败信息

2. 发送流程源码分析

为了便于阅读,省略了部分无关代码。

另外:由于同步生产者和异步生产者逻辑是一致的,只是在异步生产者基础上封装了一层,所以本文主要分析了异步生产者。

// 可以看到 同步生产者其实就是把异步生产者封装了一层
type syncProducer struct {
    producer *asyncProducer
    wg       sync.WaitGroup
}

NewAsyncProducer

首先是构建一个异步生产者对象

func NewAsyncProducer(addrs []string, conf *Config) (AsyncProducer, error) {
	client, err := NewClient(addrs, conf)
	if err != nil {
		return nil, err
	}
	return newAsyncProducer(client)
}

func newAsyncProducer(client Client) (AsyncProducer, error) {
	// ...
	p := &asyncProducer{
		client:     client,
		conf:       client.Config(),
		errors:     make(chan *ProducerError),
		input:      make(chan *ProducerMessage),
		successes:  make(chan *ProducerMessage),
		retries:    make(chan *ProducerMessage),
		brokers:    make(map[*Broker]*brokerProducer),
		brokerRefs: make(map[*brokerProducer]int),
		txnmgr:     txnmgr,
	}

	go withRecover(p.dispatcher)
	go withRecover(p.retryHandler)
}

可以看到在 newAsyncProducer 最后开启了两个 goroutine,一个为 dispatcher,一个为 retryHandler。

retryHandler 主要是处理重试逻辑,暂时先忽略。

dispatcher

主要根据 topic 将消息分发到对应的 channel。

func (p *asyncProducer) dispatcher() {
   handlers := make(map[string]chan<- *ProducerMessage)
   // ...
   for msg := range p.input {
       
	  // 拦截器逻辑
      for _, interceptor := range p.conf.Producer.Interceptors {
         msg.safelyApplyInterceptor(interceptor)
      }
	  // 找到这个Topic对应的Handler
      handler := handlers[msg.Topic]
      if handler == nil {
         // 如果没有这个Topic对应的Handler,那么创建一个
         handler = p.newTopicProducer(msg.Topic)
         handlers[msg.Topic] = handler
      }
	  // 然后把这条消息写进这个Handler中
      handler <- msg
   }
}

具体逻辑:从 p.input 中取出消息并写入到 handler 中,如果 topic 对应的 handler 不存在,则调用 newTopicProducer() 创建。

这里的 handler 是一个 unbuffered channel

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

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

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

在这个时候你可以认为消息已经交付给各个 topic 对应的 topicProducer 了。

还有一个需要注意的是newTopicProducer 的这种写法,内部创建一个 chan 返回到外层,然后通过在内部新开一个 goroutine 来处理该 chan 里的消息,这种写法在后面还会遇到好几次。

相比之下在外部显示创建 chan 之后传递到该函数可能会更容易理解。

topicDispatch

newTopicProducer的最后一行go withRecover(tp.dispatch)又启动了一个 goroutine 用于处理消息。也就是说,到了这一步,对于每一个Topic,都有一个协程来处理消息。

dispatch 具体如下:

func (tp *topicProducer) dispatch() {
	for msg := range tp.input {
		handler := tp.handlers[msg.Partition]
		if handler == nil {
			handler = tp.parent.newPartitionProducer(msg.Topic, msg.Partition)
			tp.handlers[msg.Partition] = handler
		}

		handler <- msg
	}
}

可以看到又是同样的套路:

  • 1)找到这条消息所在的分区对应的 channel,然后把消息丢进去
  • 2)如果不存在则新建 chan

PartitionDispatch

新建的 chan 是通过 newPartitionProducer 返回的,和之前的newTopicProducer又是同样的套路,点进去看一下:

func (p *asyncProducer) newPartitionProducer(topic string, partition int32) chan<- *ProducerMessage {
	input := make(chan *ProducerMessage, p.conf.ChannelBufferSize)
	pp := &partitionProducer{
		parent:    p,
		topic:     topic,
		partition: partition,
		input:     input,

		breaker:    breaker.New(3, 1, 10*time.Second),
		retryState: make([]partitionRetryState, p.conf.Producer.Retry.Max+1),
	}
	go withRecover(pp.dispatch)
	return input
}

果然是这样,有没有一种似曾相识的感觉。

TopicProducer是按照 Topic 进行分发,这里的 PartitionProducer 则是按照 partition 进行分发。

到这里可以认为消息已经交付给对应 topic 下的对应 partition 了。

每个 partition 都会有一个 goroutine 来处理分发给自己的消息。

PartitionProducer

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

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

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

具体 pp.dispatch 代码如下

func (pp *partitionProducer) dispatch() {
	// 找到这个主题和分区的leader所在的broker
	pp.leader, _ = pp.parent.client.Leader(pp.topic, pp.partition)
	if pp.leader != nil {
        // 根据 leader 信息创建一个 BrokerProducer 对象
		pp.brokerProducer = pp.parent.getBrokerProducer(pp.leader)
		pp.parent.inFlight.Add(1) 
		pp.brokerProducer.input <- &ProducerMessage{Topic: pp.topic, Partition: pp.partition, flags: syn}
	}
	// 然后把消息丢进brokerProducer中
	for msg := range pp.input {
		pp.brokerProducer.input <- msg
	}
}

根据之前的套路我们知道,真正的逻辑肯定在pp.parent.getBrokerProducer(pp.leader) 这个方法里面。

BrokerProducer

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

让我们继续跟进pp.parent.getBrokerProducer(pp.leader)这行代码里面的内容。其实就是找到asyncProducer中的brokerProducer,如果不存在,则创建一个。

func (p *asyncProducer) getBrokerProducer(broker *Broker) *brokerProducer {	p.brokerLock.Lock()	defer p.brokerLock.Unlock()	bp := p.brokers[broker]	if bp == nil {		bp = p.newBrokerProducer(broker)		p.brokers[broker] = bp		p.brokerRefs[bp] = 0	}	p.brokerRefs[bp]++	return bp}

又调用了newBrokerProducer(),继续追踪下去:

func (p *asyncProducer) newBrokerProducer(broker *Broker) *brokerProducer {	var (		input     = make(chan *ProducerMessage)		bridge    = make(chan *produceSet)		responses = make(chan *brokerProducerResponse)	)	bp := &brokerProducer{		parent:         p,		broker:         broker,		input:          input,		output:         bridge,		responses:      responses,		stopchan:       make(chan struct{}),		buffer:         newProduceSet(p),		currentRetries: make(map[string]map[int32]error),	}	go withRecover(bp.run)	// minimal bridge to make the network response `select`able	go withRecover(func() {		for set := range bridge {			request := set.buildRequest()			response, err := broker.Produce(request)			responses <- &brokerProducerResponse{				set: set,				err: err,				res: response,			}		}		close(responses)	})	if p.conf.Producer.Retry.Max <= 0 {		bp.abandoned = make(chan struct{})	}	return bp}

这里又启动了两个 goroutine,一个为 run,一个是匿名函数姑且称为 bridge。

bridge 看起来是真正的发送逻辑,那么 batch handle 逻辑应该是在 run 方法里了。

这里先分析 bridge 函数,run 在下一章分析。

buildRequest

buildRequest 方法主要是构建一个标准的 Kafka Request 消息。

根据不同版本、是否配置压缩信息做了额外处理,这里先忽略,只看核心代码:

func (ps *produceSet) buildRequest() *ProduceRequest {		req := &ProduceRequest{		RequiredAcks: ps.parent.conf.Producer.RequiredAcks,		Timeout:      int32(ps.parent.conf.Producer.Timeout / time.Millisecond),	}	for topic, partitionSets := range ps.msgs {		for partition, set := range partitionSets {				rb := set.recordsToSend.RecordBatch				if len(rb.Records) > 0 {					rb.LastOffsetDelta = int32(len(rb.Records) - 1)					for i, record := range rb.Records {						record.OffsetDelta = int64(i)					}				}				req.AddBatch(topic, partition, rb)				continue			}    }}

首先是构建一个 req 对象,然后遍历 ps.msg 中的消息,根据 topic 和 partition 分别写入到 req 中。

Produce
func (b *Broker) Produce(request *ProduceRequest) (*ProduceResponse, error) {	var (		response *ProduceResponse		err      error	)	if request.RequiredAcks == NoResponse {		err = b.sendAndReceive(request, nil)	} else {		response = new(ProduceResponse)		err = b.sendAndReceive(request, response)	}	if err != nil {		return nil, err	}	return response, nil}

最终调用了sendAndReceive()方法将消息发送出去。

如果我们设置了需要 Acks,就会传一个 response 进去接收返回值;如果没设置,那么消息发出去之后,就不管了。

func (b *Broker) sendAndReceive(req protocolBody, res protocolBody) error {    	promise, err := b.send(req, res != nil, responseHeaderVersion)	if err != nil {		return err	}	select {	case buf := <-promise.packets:		return versionedDecode(buf, res, req.version())	case err = <-promise.errors:		return err	}}
func (b *Broker) send(rb protocolBody, promiseResponse bool, responseHeaderVersion int16) (*responsePromise, error) {		req := &request{correlationID: b.correlationID, clientID: b.conf.ClientID, body: rb}	buf, err := encode(req, b.conf.MetricRegistry)	if err != nil {		return nil, err	}	bytes, err := b.write(buf)}

最终通过bytes, err := b.write(buf) 发送出去。

func (b *Broker) write(buf []byte) (n int, err error) {	if err := b.conn.SetWriteDeadline(time.Now().Add(b.conf.Net.WriteTimeout)); err != nil {		return 0, err	}	// 这里就是 net 包中的逻辑了。。	return b.conn.Write(buf)}

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

还有一个比较重要的,消息打包批量发送的逻辑,比较多再下一章讲。

3. 消息打包源码分析

在之前 BrokerProducer 逻辑中启动了两个 goroutine,其中 bridge 从 chan 中取消息并真正发送出去。

那么这个 chan 里的消息是哪里来的呢?

其实这就是另一个 goroutine 的工作了。

func (p *asyncProducer) newBrokerProducer(broker *Broker) *brokerProducer {	var (		input     = make(chan *ProducerMessage)		bridge    = make(chan *produceSet)		responses = make(chan *brokerProducerResponse)	)	bp := &brokerProducer{		parent:         p,		broker:         broker,		input:          input,		output:         bridge,		responses:      responses,		stopchan:       make(chan struct{}),		buffer:         newProduceSet(p),		currentRetries: make(map[string]map[int32]error),	}	go withRecover(bp.run)	// minimal bridge to make the network response `select`able	go withRecover(func() {		for set := range bridge {			request := set.buildRequest()			response, err := broker.Produce(request)			responses <- &brokerProducerResponse{				set: set,				err: err,				res: response,			}		}		close(responses)	})	if p.conf.Producer.Retry.Max <= 0 {		bp.abandoned = make(chan struct{})	}	return bp}

run

func (bp *brokerProducer) run() {	var output chan<- *produceSet	for {		select {		case msg, ok := <-bp.input:            // 1. 检查 buffer 空间是否足够存放当前 msg			if bp.buffer.wouldOverflow(msg) {				if err := bp.waitForSpace(msg, false); err != nil {					bp.parent.retryMessage(msg, err)					continue				}			}			// 2. 将 msg 存入 buffer			if err := bp.buffer.add(msg); err != nil {				bp.parent.returnError(msg, err)				continue			}            // 3. 如果间隔时间到了,也会将消息发出去		case <-bp.timer:			bp.timerFired = true            // 4. 将 buffer 里的数据发送到 局部变量 output chan 里		case output <- bp.buffer:			bp.rollOver()		case response, ok := <-bp.responses:			if ok {				bp.handleResponse(response)			}		} 		// 5.如果发送时间到了 或者消息大小或者条数达到阈值 则表示可以发送了 将  bp.output chan 赋值给局部变量 output		if bp.timerFired || bp.buffer.readyToFlush() {			output = bp.output		} else {			output = nil		}	}}
  • 1)首先检测 buffer 空间
  • 2)将 msg 写入 buffer
  • 3)后面的 3 4 5 步都是在发送消息,或者为发送消息做准备

wouldOverflow

if bp.buffer.wouldOverflow(msg) {    if err := bp.waitForSpace(msg, false); err != nil {        bp.parent.retryMessage(msg, err)        continue    }}

在 add 之前先调用bp.buffer.wouldOverflow(msg) 方法检查 buffer 是否存在足够空间以存放当前消息。

wouldOverflow 比较简单,就是判断当前消息大小或者消息数量是否超过设定值:

func (ps *produceSet) wouldOverflow(msg *ProducerMessage) bool {	switch {	case ps.bufferBytes+msg.byteSize(version) >= int(MaxRequestSize-(10*1024)):		return true	case ps.msgs[msg.Topic] != nil && ps.msgs[msg.Topic][msg.Partition] != nil &&		ps.msgs[msg.Topic][msg.Partition].bufferBytes+msg.byteSize(version) >= ps.parent.conf.Producer.MaxMessageBytes:		return true	case ps.parent.conf.Producer.Flush.MaxMessages > 0 && ps.bufferCount >= ps.parent.conf.Producer.Flush.MaxMessages:		return true	default:		return false	}}

如果不够就要调用bp.waitForSpace() 等待 buffer 腾出空间,其实就是把 buffer 里的消息发到 output chan。

这个 output chan 就是前面匿名函数里的 bridge。

func (bp *brokerProducer) waitForSpace(msg *ProducerMessage, forceRollover bool) error {	for {		select {		case response := <-bp.responses:			bp.handleResponse(response)			if reason := bp.needsRetry(msg); reason != nil {				return reason			} else if !bp.buffer.wouldOverflow(msg) && !forceRollover {				return nil			}		case bp.output <- bp.buffer:			bp.rollOver()			return nil		}	}}

add

接下来是调用bp.buffer.add()把消息添加到 buffer,功能比较简单,把待发送的消息添加到 buffer 中。

func (ps *produceSet) add(msg *ProducerMessage) error {		// 1.消息编码		key, err = msg.Key.Encode()		val, err = msg.Value.Encode()		// 2.添加消息到 set.msgs 数组		set.msgs = append(set.msgs, msg)		// 3.添加到set.recordsToSend		msgToSend := &Message{Codec: CompressionNone, Key: key, Value: val}		if ps.parent.conf.Version.IsAtLeast(V0_10_0_0) {			msgToSend.Timestamp = timestamp			msgToSend.Version = 1		}		set.recordsToSend.MsgSet.addMessage(msgToSend)		// 4. 增加 buffer 大小和 buffer 中的消息条数		ps.bufferBytes += size		ps.bufferCount++}

set.recordsToSend.MsgSet.addMessage也很简单:

func (ms *MessageSet) addMessage(msg *Message) {	block := new(MessageBlock)	block.Msg = msg	ms.Messages = append(ms.Messages, block)}

定时发送

因为异步发送者除了消息数或者消息大小达到阈值会触发一次发送之外,到了一定时间也会触发一次发送,具体逻辑也在这个 run 方法里,这个地方比较有意思。

func (bp *brokerProducer) run() {	var output chan<- *produceSet	for {		select {		case msg, ok := <-bp.input:        // 1.时间到了就将 bp.timerFired 设置为 true		case <-bp.timer:			 bp.timerFired = true        // 3.直接把 buffer 里的消息往局部变量 output 里发		case output <- bp.buffer:			bp.rollOver()		}		// 2.如果时间到了,或者 buffer 里的消息达到阈值后都会触发真正的发送逻辑,这里实现比较有意思,需要发送的时候就把 bp.output 也就是存放真正需要发送的批量消息的 chan 赋值给局部变量 output,如果不需要发送就把局部变量 output 清空		if bp.timerFired || bp.buffer.readyToFlush() {			output = bp.output		} else {			output = nil		}	}}

根据注释中的 1、2、3步骤看来,如果第二步需要发送就会给 output 赋值,这样下一轮 select 的时候case output <- bp.buffer: 这个 case 就可能会执行到,就会把消息发给 output,实际上就是发送给了 bp.output.

如果第二步时不需要发消息,output 就被置空,select 时对应的 case 就不会被执行。

正常写法一般是在启动一个 goroutine 来处理定时发送的功能,但是这样两个 goroutine 之间就会存在竞争,会影响性能。这样处理省去了加解锁过程,性能会高一些,但是随之而来的是代码复杂度的提升。

作者能力实在是有限,文中很有可能会有一些错误的理解。所以当你发现了一些违和的地方,也请不吝指教,谢谢你!

再次感谢你能看到这里!

4. 小结

1)具体流程:见开篇图

2)常见优化手段:批量处理。

异步消费者 Go 实现中做了消息批量发送这个优化,当累积了足够的消息后一次性发送,减少网络请求次数以提升性能。

类似于 Redis Pipeline,将多次命令一次性发送,以减少 RTT。

当然为了避免在消息少的时候很久都凑不齐足够消息,导致的无法发送,一般还会设定一个定时发送阈值,每隔一段时间也会发送一次。

这是一种常见的优化手段,比如 IO 相关的地方肯定会有什么 bufferio 之类的库,在写时先写 buffer,buffer 满了再一次性写入到磁盘。读取也是同样的,先读到 buffer 里,然后应用程序再从 buffer 里一行行读出去。

3)代码复杂度和性能取舍

在分析 Sarama Proudcer 的最后一段可以看到是有一个骚操作的,这种操作可以提升性能,但是随之而来的就是代码复杂度的提升。

最近在看 Go runtime 里面也有很多骚操作。这种底层库、中间件这样写没什么问题,但是平常我们的业务代码就尽量别搞骚炒作了。

"The performance improvement does not materialize from the air,it comes with code complexity increase."一dvyokov

性能不会凭空提升,随之而来的一定是代码复杂度的增加。

Kafka 系列相关代码见 Github

5. 参考

https://github.com/Shopify/sarama

https://cs50mu.github.io/post/2021/01/22/source-code-of-sarama-part-i/

https://www.jianshu.com/p/138e0ac2e1f0

https://juejin.cn/post/6866316565348876296

MATLAB实战:手把手教你用三频四步相移法搞定结构光三维重建(附完整代码) 本文详细介绍了如何使用MATLAB实现三频四步相移法进行高精度结构光三维重建。通过光栅图像预处理、相位计算、多频外差与相位解包裹等步骤,结合完整代码示例,帮助读者掌握从二维图像到三维点云的关键技术。该方法在工业检测和逆向工程中具有抗干扰能力强、高分辨率和鲁棒性等优势。 阅读详情

相关推荐

Sciter教程_00_创建HelloWorld工程

本教程记录Sciter工程的创建和编译运行的过程。

qq_36707072的博客 2278

Golang中如何正确的使用sarama包操作Kafka?

背景 在一些业务系统中,模块之间通过引入Kafka解藕,拿IM举例(图来源): 用户A给B发送消息,msg_gateway收到消息后,投递消息到Kafka后就给A返回发送成功。这个时候,其实还没有持久化到mysql中,虽然最终会保持一致性。所以,试想如果Kafka丢消息了,是不是就出大问题了?A认为给B发送消息成功了,但是在服务器内部消息丢失了B并没有收到。 所以,在使用Kafka的时候,有一些业务对消息丢失问题非常的关注。 同样,常见的问题还有: 重复消费的问题。 乱序的问题。 下面我们来一起看一下

xmcy001122的专栏 4185

从零解析:富斯i6遥控器与STM32的IBUS协议通信实战

本文详细解析了富斯i6遥控器与STM32通过IBUS协议通信的实战指南,涵盖硬件准备、协议解析、驱动实现及常见问题解决。特别适合机器人及智能小车开发者,提供低成本、高可靠性的无线控制方案,助力快速实现多通道控制功能。

weixin_29193359的博客 441

go语言并发实战——日志收集系统(三) 利用sarama包连接KafKa实现消息的生产与消费

由于1.19版本后添加了ztcd压缩算法,需要用到cgo,这里我们为了方便考虑选择下载。不过这个不能实现直接的消费,后续我们会对这个进行补充,这里制作介绍。今天我们所时机的内容需要用到go语言的第三方包。了,但是博主在做的时候发现,这样会直接清除掉。命令来安装第三方包,我们要使用/,所以这里我们不能直接使用。其实这是已经可以使用命令。等待命令运行完毕,打开。这时候再打开终端输入。

qq_73924465的博客 2531

zynq+LWIP 裸机双网口实现(MIO+EMIO)+程序下载

芯片型号: ZYNQ: XC7Z010clg400开发软件:Vivado2022 + Xilinx Vitis 2022网卡芯片:RTL8211FDI(需要根据网口芯片型号修改对应的硬件配置代码。

thinking in 小宇宙 3412

188、AI 虚化渲染:从深度图到光学虚化的模拟,光斑与渐进虚化的算法实现

AI虚化渲染技术解析:从深度图到光学级虚化效果 本文深入探讨了手机AI虚化渲染的关键技术,包括深度图处理、光学虚化模拟和渐进虚化算法。文章指出手机深度图存在边缘精度不足的问题,提出通过引导滤波进行边缘锐化预处理。针对传统高斯模糊的"塑料感",介绍了基于物理光学模型的弥散圆卷积核实现方法,包括光圈形状模拟和光斑模板生成。重点分析了渐进虚化技术,通过分层渲染和混合解决深度断层问题,并给出性能优化方案。最后详细解析了真实感光斑渲染的三个特征:边缘增强、洋葱圈纹理和非相干叠加,提供了完整的实现思路和关键代码片段。文

sundong_81的博客 138

go语言操作kafka

sarama Go语言中连接kafka使用第三方库: github.com/Shopify/sarama。 下载及安装 go get github.com/Shopify/sarama {{% panel status=“notice” title=“注意事项” %}} sarama v1.20之后的版本加入了zstd压缩算法,需要用到cgo,在Windows平台编译时会提示类似如下错误: g...

qq_35857939的博客 710

零基础做AI漫剧:Kimi K2.5写本 + Flux生图 + Seedance 2.0动效的保姆级教程

很多零基础小伙伴想做AI漫剧,却被“写剧本、画分镜、做动效”三大难题劝退——不会写剧情、不会画插画、不会做动态,觉得门槛太高。其实不用慌!今天就给大家带来保姆级教程,全程用Kimi K2.5(写剧本)、Flux(生分镜图)、Seedance 2.0(做动效)三个工具,不用专业技能,不用手动绘画,零基础也能在1小时内做出1分钟完整AI漫剧,全程步骤拆解、示例齐全,跟着做就能出片!

远程部署调试 运行安装 项目调试 二次开发 项目技术新 持续迭代 部分源码免费分享 3044

golang源码分析:sarama kafka client(part I:生产者)

getBrokerProducer依赖于这个partitation的leader,发送消息都是发送到partation的leader,获取leader的方式是通过存储在kafka中的元数据得到的,后面会详细介绍。dispatcher这个协程,不断消费input里面的消息,然后发送给topicProducer的input chanel,这样我们发送消息的时候,值需要不断往peoducer的input里面发送就可以了。代码位于client.go中。第二个协程,就是做了构建请求,发起请求,传递返回结果三件事情。

大叶子不小的博客 858

7 中望CAD二次开发-标注(一)(ZRX C++)

尺寸标注是CAD设计中最常用的功能之一,软件内置了很多标注功能,可以通过使用这些标注工具,对需要标注的长度、角度等一个个进行标注。但这显然效率很低,学习CAD二次开发,就是要把人从重复的劳动中解放出来,实现批量标注。本文介绍,如何通过二次开发进行标注。

cilvsuanna的博客 340

把复杂计算推到数据库里:ABAP CDS View Table Function 从原理到落地实践

摘要:本文深入解析ABAP CDS Table Function技术,重点探讨其在复杂业务场景下的应用价值。当CDS View无法满足窗口函数、递归计算等高级需求时,Table Function通过结合CDS类型系统与AMDP的数据库原生计算能力,成为关键解决方案。文章详细介绍了Table Function的架构设计(CDS实体声明+AMDP实现)、客户端处理机制、开发传输流程中的注意事项,并通过航班数据示例展示最小实现闭环。这项技术特别适用于需要将复杂计算下推到数据库层的场景,能显著提升RAP和OData

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 2303

Kafka(Go)教程(八)---生产者压缩算法详解及源码分析

来自:指月 https://www.lixueduan.com 原文:https://www.lixueduan.com/post/kafka/08-compression/ 本文主要分析了 Kafka 生产者压缩与解压逻辑以及注事事项点,最终通过 Go 客户端 sarama 演示了 压缩算法的具体使用,并通过源码分析了 Go 客户端 sarama 的具体实现。 Kakfa 相关代码见 Github 1. 概述 压缩(compression)它秉承了用时间去换空间的经典 trade-off 思想,具体

探索云原生 1287

Python爬虫实战:利用Selenium与异步技术高效获取雪球财经数据

雪球财经作为国内领先的投资社交平台,汇聚了大量股票数据、财经资讯和投资者观点。对于金融数据分析师、量化投资者和研究人员来说,获取雪球平台的实时数据具有重要价值。然而,雪球网站采用了多种反爬虫机制,包括动态内容加载、请求频率限制和用户行为检测等,使得传统爬虫技术难以有效获取数据。本文将介绍如何使用Python的最新爬虫技术,包括Selenium WebDriver、异步请求处理和智能等待策略,来高效、稳定地爬取雪球财经数据。我们将重点解决以下几个技术难点:动态加载内容的处理登录状态维持与模拟。

2201_76125261的博客 1107

https://www.modb.pro/db/129998

golang源码分析:sarama kafka client(part I:生产者) - 墨天轮 https://github.com/Shopify/sarama 是一个纯go实现的kafka客户端,是gopher学习kafka一个很好的资料。说实话sarama的代码组织很烂,密密麻麻一堆源码文件都在一个目录,让人无从下手,下面列出了一部分: examples mocks tools //基于客户端,实现的kafka客户端工具 tools/kafka-producer-performance

大叶子不小的博客 1904

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

golang kafka Shopify/sarama 消费者重置新增分区偏移量并进行重新消费

在重平衡回调方法中使用redis的无序集合储存当前主题、当前消费者组下的全部分区信息,然后在根据当前消费者会话去和上一代的全部分区信息进行差集对比,对比的结果就是新增的分区。得到新增的分区后将这些分区的偏移量重置为0即重头开始消费,当前存在重复消费的情况,需要你的业务逻辑上做好幂等性。最后再将这些差集新增分区ID更新至redis存储的当前主题、当前消费者组下的全部分区信息内形成了一个闭环,即使一个消费者组下有多个消费者进程也不会出现数据覆盖更新等问题,因为如果存在差集新增分区只会将更新差集分区ID。

Enjun97的博客 2563

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

Kafka知识点

RabbitMQ和Kafka是两个常用的分布式消息队列系统,它们的主要区别包括以下几个方面:架构模式:RabbitMQ采用模式和模式,即消息生产者将消息发送到特定的队列,然后等待消费者来处理这个消息,或者发送到订阅的exchange,由绑定到此exchange的队列接收;而Kafka采用模式,即消息生产者把消息发送给主题(topic),然后订阅这个主题的所有消费者都可以接收到消息。

weixin_42094245的博客 293

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

go之kafka库sarama的使用

saram是一个使用纯go语言编写的kafka库。

专注于Python编程技术的分享与交流,致力于帮助开发者提升编程技能,解决实际问题,探索Python的无限可能。 3151

利用sarama包使用kafka

代码仓库 官方文档 sarama提供了纯Go编写的kafka客户端,最新发行版本支持Go和Kafka的最新两个版本 生产者 sarama提供异步生产者和同步生产者 异步生产者用管道接受信息,并且在背后异步地生产消息。异步生产者会尽可能的提高效率,是sarama推荐的选择 同步生产者提供一个阻塞的方法,直到消息被确认成功生产。显然效率上肯定不如异步生产者,且在某些配置中,确认的消息仍会丢失 type AsyncProducer type AsyncProducer interface { // 关闭

IuSpet的博客 2359

golang kafka客户端 sarama,segmentio/kafka-go ,confluent-kafka-go,franz-go比较

golang kafka客户端 sarama,segmentio/kafka-go ,confluent-kafka-go,franz-go哪个更好

王仁一的博客 2157

Kafka(Go)---Producer-Consumer API 基本使用

本文主要讲解其中的 Producer API 和 Consumer API 在 Go Client sarama 中的使基本使用以及注意事项。 1. 概述 Kakfa 相关代码见Github Kafka 有 5 个核心 API: Producer API Consumer API Stream API Connect API Admin API 在 Go sarama 客户端中暂时只实现了 Producer、Consumer、Admin 3 个API。 其中 Stream API .

dz45693的专栏 2368

Kafka Go客户端--Sarama

在Go语言中,有多个Kafka客户端可供选择,其中Sarama、segmentio/kafka-go和confluent-kafka-go是较为知名的三个。Sarama是目前用户数量最多的客户端,最初由Shopify开发,现由IBM维护。segmentio/kafka-go虽然没有显著缺点,但功能相对基础。confluent-kafka-go则需要启用cgo,跨平台支持较差,且不支持交叉编译。 Sarama提供了命令行工具,可以用于简单的消费者和生产者操作。安装Sarama工具时,可以通过设置Go代理来加速

m0_73180708的博客 2605

go 操作kafka包 sarama 使用(示例)

go 操作kafka包 sarama 使用(一) 截止当前时间,github上golang操作kafka的包主要有两个: Shopify/sarama starts 5.7k confluentinc/confluent-kafka-go starts 2k saram 使用纯go语言编写, confluent-kafka-go 这是包装了c的api...

luslin1711的博客 7万+

golang kafka内存泄露

项目需求将接收到的消息写到kafka中,kafka 客户端用到了比较流行的sarama组件。参考了网上的示例代码,上线后一堆问题,开源代码还是需要理解API底层实现原理,不能简单拿来主义。 一. kafka生产者 import "github.com/Shopify/sarama" var Brokers = []string{ "10.120.9.123:909...

specialsun的专栏 3598

go 操作kafka包 sarama (Producer流程)

go 操作kafka包 sarama (Producer流程) 代码: package main import ( "github.com/Shopify/sarama" "log" "os" "os/signal" "sync" "time" ) func main() { var ( wg sync.WaitGroup success_num, error_num ...

luslin1711的博客 5442

Go集成Kafka

【代码】Go集成Kafka。

u014204444的专栏 332

Telego与Web Apps集成:构建现代化Telegram应用的完整流程

Telego是一个强大的Telegram Bot API库,专为Go语言开发者设计。通过Telego,开发者可以轻松构建功能丰富的Telegram机器人,并与Web Apps无缝集成,打造现代化的用户体验。本文将详细介绍如何使用Telego实现与Web Apps的集成,从环境搭建到功能实现,帮助你快速掌握这一技能。 [![Telego Logo](https://raw.gitcode.com/

gitblog_00068的博客 513
上一篇: Kafka(Go)教程(五)---Producer-Consumer API 基本使用
下一篇: Kafka(Go)教程(七)---生产者分区机制原理剖析及实战
探索云原生
博客等级 码龄9年 730粉丝 131原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值