Kafka 延迟操作模块(四):DelayedProduce 与 DelayedFetch

        kafka中有很多运用到延迟操作,比较典型的延时任务实现:DelayedProduce 和 DelayedFetch。

DelayedProduce

当生产者追加消息到集群时(对应 ProduceRequest 请求),实际上是与对应 topic 分区的 leader 副本进行交互,当消息写入 leader 副本成功后,为了保证 leader 节点宕机时消息数据不丢失,一般需要将消息同步到位于 ISR 集合中的全部 follower 副本,只有当 ISR 集合中所有的 follower 副本成功完成对当前消息的记录之后才认为本次消息追加操作是成功的。

这里就存在一个延时任务的适用场景,即当消息被成功追加到 leader 副本之后,我们需要创建一个延时任务等待 ISR 集合中所有的 follower 副本完成同步,并在同步操作完成之后对生产者的请求进行响应,Kafka 定义了 DelayedProduce 类来处理这类需求。

DelayedProduce 继承自 DelayedOperation 抽象类,其字段定义如下:

class DelayedProduce(delayMs: Long, // 延迟时长
                     produceMetadata: ProduceMetadata, // 用于判断 DelayedProduce 是否满足执行条件
                     replicaManager: ReplicaManager, // 副本管理器
                     responseCallback: Map[TopicPartition, PartitionResponse] => Unit, // 回调函数,在任务满足条件或到期时执行
                     lockOpt: Option[Lock] = None)
  extends DelayedOperation(delayMs, lockOpt) {
      
  }

        ProduceMetadata 类记录了当前延时任务所关注的 topic 分区的消息追加状态,DelayedProduce 延时任务在被构造时会依据消息写入对应 topic 分区 leader 副本是否成功来对其进行初始化:

  /** acksPending 标识是否正在等待 ISR 集合中的 follower 副本从 leader 副本同步 requiredOffset 之前的消息 */
  // first update the acks pending variable according to the error code
  produceMetadata.produceStatus.foreach { case (topicPartition, status) =>
    if (status.responseStatus.error == Errors.NONE) {
      // 对应 topic 分区消息写入 leader 副本成功,等待其它副本同步
      // Timeout error state will be cleared when required acks are received
      status.acksPending = true
      status.responseStatus.error = Errors.REQUEST_TIMED_OUT
    } else {
      // 对应 topic 分区消息写入 leader 副本失败,无需等待
      status.acksPending = false
    }

    trace(s"Initial partition status for $topicPartition is $status")
  }

tryComplete 方法

        检测当前延时任务所关注的 topic 分区的运行状态

  override def tryComplete(): Boolean = {
    // check for each partition if it still has pending acks
    // 遍历处理所有的 topic 分区
    produceMetadata.produceStatus.foreach { case (topicPartition, status) =>
      trace(s"Checking produce satisfaction for $topicPartition, current status $status")
      // skip those partitions that have already been satisfied
      // 仅处理正在等待 follower 副本复制的分区
      if (status.acksPending) {
        val (hasEnough, error) = replicaManager.getPartitionOrError(topicPartition, expectLeader = true) match {
          case Left(err) =>
            // Case A
            // 找不到对应的分区对象,说明对应分区的 leader 副本已经不在当前 broker 节点上
            (false, err)

          case Right(partition) =>
            // 检测对应分区本次追加的最后一条消息是否已经被 ISR 集合中所有的 follower 副本同步
            partition.checkEnoughReplicasReachOffset(status.requiredOffset)
        }

        // Case B.1 || B.2
        // 出现异常,或所有的 ISR 副本已经同步完成
        if (error != Errors.NONE || hasEnough) {
          status.acksPending = false // 不再等待
          status.responseStatus.error = error
        }
      }
    }

    // check if every partition has satisfied at least one of case A or B
    // 如果所有的 topic 分区都已经满足了 DelayedProduce 的执行条件,即不存在等待 ack 的分区,则结束本次延时任务
    if (!produceMetadata.produceStatus.values.exists(_.acksPending))
      forceComplete()
    else
      false
  }

onComplete 方法

        以回调的形式将各个 topic 分区对应的响应状态发送给客户端。
 

  override def onComplete(): Unit = {
    val responseStatus = produceMetadata.produceStatus.map { case (k, status) => k -> status.responseStatus }
    responseCallback(responseStatus)
  }

DelayedFetch


        消费者和 follower 副本均会向目标 topic 分区的 leader 副本所在 broker 节点发送 FetchRequest 请求来拉取消息,从接收到请求到准备消息数据,再到发送响应之间的过程同样适用于延时任务,Kafka 定义了 DelayedFetch 类来处理这类需求。
        DelayedFetch 同样继承自 DelayedOperation 抽象类,其字段定义如下:
  

class DelayedFetch(delayMs: Long, // 延时任务延迟时长
                   fetchMetadata: FetchMetadata, // 记录对应 topic 分区的状态信息,用于判定当前延时任务是否满足执行条件
                   replicaManager: ReplicaManager, // 副本管理器
                   quota: ReplicaQuota,
                   clientMetadata: Option[ClientMetadata],
                   responseCallback: Seq[(TopicPartition, FetchPartitionData)] => Unit  // 响应回调函数
                  )
  extends DelayedOperation(delayMs) {
      
  }

tryComplete 方法

        当满足以下 4 个条件之一时会触发执行延时任务:

  • 对应 topic 分区的 leader 副本不再位于当前 broker 节点上。
  • 请求拉取消息的 topic 分区在当前 broker 节点上找不到。
  • 请求拉取消息的 offset 不位于 activeSegment 对象上,可能已经创建了新的 activeSegment,或者 Log 被截断。
  • 累计读取的字节数已经达到所要求的最小字节数。

  override def tryComplete(): Boolean = {
    var accumulatedSize = 0
    // 遍历处理当前延时任务关注的所有 topic 分区的状态信息
    fetchMetadata.fetchPartitionStatus.foreach {
      case (topicPartition, fetchStatus) =>
        // 获取上次拉取消息的结束 offset
        val fetchOffset = fetchStatus.startOffsetMetadata
        val fetchLeaderEpoch = fetchStatus.fetchInfo.currentLeaderEpoch
        try {
          if (fetchOffset != LogOffsetMetadata.UnknownOffsetMetadata) {
            val partition = replicaManager.getPartitionOrException(topicPartition,
              expectLeader = fetchMetadata.fetchOnlyLeader)
            val offsetSnapshot = partition.fetchOffsetSnapshot(fetchLeaderEpoch, fetchMetadata.fetchOnlyLeader)
            // 依据fetchIsolation来确定拉取消息的结束 offset
            val endOffset = fetchMetadata.fetchIsolation match {
              case FetchLogEnd => offsetSnapshot.logEndOffset
              case FetchHighWatermark => offsetSnapshot.highWatermark
              case FetchTxnCommitted => offsetSnapshot.lastStableOffset
            }

            // Go directly to the check for Case G if the message offsets are the same. If the log segment
            // has just rolled, then the high watermark offset will remain the same but be on the old segment,
            // which would incorrectly be seen as an instance of Case F.
            // 校验上次拉取消息完成之后,endOffset 是否发生变化,如果未发送变化则说明数据不够,没有继续的必要,否则继续执行
            if (endOffset.messageOffset != fetchOffset.messageOffset) {
                
              // endOffset.onOlderSegment(fetchOffset)与 fetchOffset.onOlderSegment(endOffset) 都是请求拉取消息的 offset 不位于 activeSegment 对象上,可能已经创建了新的 activeSegment,或者 Log 被截断。
              if (endOffset.onOlderSegment(fetchOffset)) {
                // Case F, this can happen when the new fetch operation is on a truncated leader
                debug(s"Satisfying fetch $fetchMetadata since it is fetching later segments of partition $topicPartition.")
                // endOffset 相对于 fetchOffset 较小,说明请求不位于当前 activeSegment 上
                return forceComplete()
              } else if (fetchOffset.onOlderSegment(endOffset)) {
                // Case F, this can happen when the fetch operation is falling behind the current segment
                // or the partition has just rolled a new segment
                debug(s"Satisfying fetch $fetchMetadata immediately since it is fetching older segments.")
                // We will not force complete the fetch request if a replica should be throttled.
                // fetchOffset 位于 endOffset 之前,但是 fetchOffset 落在老的 LogSegment 上,而非 activeSegment 上
                if (!replicaManager.shouldLeaderThrottle(quota, topicPartition, fetchMetadata.replicaId))
                  return forceComplete()
              } else if (fetchOffset.messageOffset < endOffset.messageOffset) {
                // we take the partition fetch size as upper bound when accumulating the bytes (skip if a throttled partition)
                // fetchOffset 和 endOffset 位于同一个 LogSegment 上,计算累计读取的字节数
                val bytesAvailable = math.min(endOffset.positionDiff(fetchOffset), fetchStatus.fetchInfo.maxBytes)
                if (!replicaManager.shouldLeaderThrottle(quota, topicPartition, fetchMetadata.replicaId))
                  accumulatedSize += bytesAvailable
              }
            }

            if (fetchMetadata.isFromFollower) {
              // Case H check if the follower has the latest HW from the leader
              if (partition.getReplica(fetchMetadata.replicaId)
                .exists(r => offsetSnapshot.highWatermark.messageOffset > r.lastSentHighWatermark)) {
                return forceComplete()
              }
            }
          }
        } catch {
          // 对应 topic 分区的 leader 副本不再位于当前 broker 节点上
          case _: NotLeaderForPartitionException =>  // Case A
            debug(s"Broker is no longer the leader of $topicPartition, satisfy $fetchMetadata immediately")
            return forceComplete()
          case _: ReplicaNotAvailableException =>  // Case B
            debug(s"Broker no longer has a replica of $topicPartition, satisfy $fetchMetadata immediately")
            return forceComplete()
          // 请求 fetch 的 topic 分区在当前 broker 节点上找不到
          case _: UnknownTopicOrPartitionException => // Case C
            debug(s"Broker no longer knows of partition $topicPartition, satisfy $fetchMetadata immediately")
            return forceComplete()
          case _: KafkaStorageException => // Case D
            debug(s"Partition $topicPartition is in an offline log directory, satisfy $fetchMetadata immediately")
            return forceComplete()
          case _: FencedLeaderEpochException => // Case E
            debug(s"Broker is the leader of partition $topicPartition, but the requested epoch " +
              s"$fetchLeaderEpoch is fenced by the latest leader epoch, satisfy $fetchMetadata immediately")
            return forceComplete()
        }
    }

    // Case G
    // 累计读取的字节数已经达到最小字节限制
    if (accumulatedSize >= fetchMetadata.fetchMinBytes)
       forceComplete()
    else
      false
  }

onComplete 方法    

        如果延时任务满足执行条件,则会触发执行 DelayedFetch#forceComplete 方法,即调用 onComplete 方法以回调的形式将各个 topic 分区对应的响应数据封装成 FetchPartitionData 对象发送给请求方。

  override def onComplete(): Unit = {
    val logReadResults = replicaManager.readFromLocalLog(
      replicaId = fetchMetadata.replicaId,
      fetchOnlyFromLeader = fetchMetadata.fetchOnlyLeader,
      fetchIsolation = fetchMetadata.fetchIsolation,
      fetchMaxBytes = fetchMetadata.fetchMaxBytes,
      hardMaxBytesLimit = fetchMetadata.hardMaxBytesLimit,
      readPartitionInfo = fetchMetadata.fetchPartitionStatus.map { case (tp, status) => tp -> status.fetchInfo },
      clientMetadata = clientMetadata,
      quota = quota)

    val fetchPartitionData = logReadResults.map { case (tp, result) =>
      tp -> FetchPartitionData(result.error, result.highWatermark, result.leaderLogStartOffset, result.info.records,
        result.lastStableOffset, result.info.abortedTransactions, result.preferredReadReplica)
    }

    responseCallback(fetchPartitionData)
  }


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值