Sarama 源码阅读(一): 客户端是如何与 broker 通信的

554 阅读2分钟

概述

在 sarama 中, Broker 结构负责与远程 kafka broker 的实际通信, 本节专注于 sarama 如何实现请求的发送与响应的回收

统一的 API 实现

在 broker.go 中为 Broker 实现了不同的方法, 这些方法对应着 Kafka 的 API, 如 GetMetadata

func (b *Broker) GetMetadata(request *MetadataRequest) (*MetadataResponse, error) {
	response := new(MetadataResponse)

	err := b.sendAndReceive(request, response)
	if err != nil {
		return nil, err
	}

	return response, nil
}

这些方法的流程都是一致的, 传入相应的 request, 调用 sendAndReceive 返回相应的 response

sendAndReceive

func (b *Broker) sendAndReceive(req protocolBody, res protocolBody) error {
	responseHeaderVersion := int16(-1)
	if res != nil {
		responseHeaderVersion = res.headerVersion()
	}

	promise, err := b.send(req, res != nil, responseHeaderVersion)
	if err != nil {
		return err
	}

	if promise == nil {
		return nil
	}

	select {
	case buf := <-promise.packets:
		return versionedDecode(buf, res, req.version())
	case err = <-promise.errors:
		return err
	}
}

sendAndReceive 方法调用 send 方法生成了一个 promise, 之后从 promise.packets 中读取响应体解析到 res 中, 或者从 promise.errors 中读取到 error 返回失败

type versionedDecoder interface {
	decode(pd packetDecoder, version int16) error
}
func versionedDecode(buf []byte, in versionedDecoder, version int16) error {
	if buf == nil {
		return nil
	}

	helper := realDecoder{raw: buf}
	err := in.decode(&helper, version)
	if err != nil {
		return err
	}

	if helper.off != len(buf) {
		return PacketDecodingError{"invalid length"}
	}

	return nil
}

各种 response 结构都实现了 versionedDecoder 接口, 用于从响应体中解析 response 结构, 在 versionedDecode 中, 便调用了不同响应体实现的 decode 方法解析响应体

send

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
	}

	requestTime := time.Now()
	// Will be decremented in responseReceiver (except error or request with NoResponse)
	b.addRequestInFlightMetrics(1)
	bytes, err := b.write(buf)
	b.updateOutgoingCommunicationMetrics(bytes)
	if err != nil {
		b.addRequestInFlightMetrics(-1)
		return nil, err
	}
	b.correlationID++

	if !promiseResponse {
		// Record request latency without the response
		b.updateRequestLatencyAndInFlightMetrics(time.Since(requestTime))
		return nil, nil
	}

	promise := responsePromise{requestTime, req.correlationID, responseHeaderVersion, make(chan []byte), make(chan error)}
	b.responses <- promise

	return &promise, nil
}

send 方法核心流程在于将请求写入与远程 Kafka broker 的连接中, 生成一个 promise 写入 broker.responses, 写入之后将 promise 返回, 而上述的 sendAndReceive 方法正是从返回的 promise 中读取响应或错误, 由此也可以得知, broker.responses 的 buffer 数决定了一个 broker in-flight 的请求数

responseReceiver

func (b *Broker) responseReceiver() {
	var dead error

	for response := range b.responses {
		if dead != nil {
			// This was previously incremented in send() and
			// we are not calling updateIncomingCommunicationMetrics()
			b.addRequestInFlightMetrics(-1)
			response.errors <- dead
			continue
		}

		headerLength := getHeaderLength(response.headerVersion)
		header := make([]byte, headerLength)

		bytesReadHeader, err := b.readFull(header)
		requestLatency := time.Since(response.requestTime)
		if err != nil {
			b.updateIncomingCommunicationMetrics(bytesReadHeader, requestLatency)
			dead = err
			response.errors <- err
			continue
		}

		decodedHeader := responseHeader{}
		err = versionedDecode(header, &decodedHeader, response.headerVersion)
		if err != nil {
			b.updateIncomingCommunicationMetrics(bytesReadHeader, requestLatency)
			dead = err
			response.errors <- err
			continue
		}
		if decodedHeader.correlationID != response.correlationID {
			b.updateIncomingCommunicationMetrics(bytesReadHeader, requestLatency)
			// TODO if decoded ID < cur ID, discard until we catch up
			// TODO if decoded ID > cur ID, save it so when cur ID catches up we have a response
			dead = PacketDecodingError{fmt.Sprintf("correlation ID didn't match, wanted %d, got %d", response.correlationID, decodedHeader.correlationID)}
			response.errors <- dead
			continue
		}

		buf := make([]byte, decodedHeader.length-int32(headerLength)+4)
		bytesReadBody, err := b.readFull(buf)
		b.updateIncomingCommunicationMetrics(bytesReadHeader+bytesReadBody, requestLatency)
		if err != nil {
			dead = err
			response.errors <- err
			continue
		}

		response.packets <- buf
	}
	close(b.done)
}

broker.responses 的读取与处理则实现在 responseReceiver 中, 它被实现为一个 background goroutine, responseReceiver goroutine 不断地从 broker.responses 中读取 promise, 从 promise 中获取响应头的长度读取响应头, 在从响应头中读取响应体的长度读取响应体, 将响应体写入 promise.packets 中, 这样 sendAndReceive 方法就可以从 promise 中读取响应体了

总结

在 sarama 处理与 broker 的通信过程中, send 方法与 responseReceiver goroutine 是核心逻辑, send 方法将请求写入连接, 构造 promise 返回给上层 API 以供读取响应或判断错误, 同时将 promise 传入 broker.reponses 中, 而 responseReceiver goroutine 从 channel 中读取 promise, 从连接中读取响应数据并写入 promise 的 channel 中, 这样上层的 API 就可以读取到响应了

这里利用了 TCP 有序性的保证和 go channel FIFO 的性质, 先写入连接的请求会先被 broker 处理, 响应也会先返回, 同样, 先写入的请求对应的 promise 也是先写入 broker.responses 中被 responseReceiver goroutine 先处理的, 读取到了对应的响应