kakfa RPC 协议(社区 Trunk 版本)--OFFSET_COMMIT&& OFFSET_FETCH

1、RPC 使用场景
 consumer 消费kafka 数据时需要保存上一次消费完成的数据信息(即offet),以便重启consumer 后能够继续从当前位置开始fetch 数据,保存上一次的offset需要使用OFFSET_COMMIT RPC ,或者上一次消费的offset 位置需要使用OFFSET_FETCH RPC,这里又分为old consumer/new consumer

2、OFFSET_COMMIT RPC 源码剖析

2.1 RPC 包含的字段

 private String groupId;
    private int generationId;
    private String memberId;
    private String groupInstanceId;
    private long retentionTimeMs;
    private List<OffsetCommitRequestTopic> topics;
    
    public static final Schema SCHEMA_0 =

groupId:客户端消费topic的唯一标记,每个group 的消费都是独立的

memberID:作为 group coordinator 分配给consumer的标识

groupInstanceId: 唯一表示某个特定group instaned 实例(消费哪个具体的partition)

retentionTmeMs: 用于提交group的过期时间

 

2.2 客户端入口

客户端通过使用 ConsumerCoordinator.sendOffsetCommitRequest 方法完成rpc 的发送(又分为主动和手动两种方式),主动提交客户端会定期向服务端提交消费完成的最新一条offset 信息,异步提交则由业务侧来决定何时提交offset。

2.3 server 端处理

服务端通用执行kafkaApis.handleOffsetCommitRequest() 方法处理客户端发送的rpc 请求,核心是将请求根据V0 和大于V0进行分类,V0的核心逻辑如下,将offset 直接写入对应的ZK 目录即可(一般用于simple consumer)

 if (header.apiVersion == 0) {
        // for version 0 always store offsets to ZK
        val responseInfo = authorizedTopicRequestInfo.map {
          case (topicPartition, partitionData) =>
            try {
              if (partitionData.committedMetadata() != null
                && partitionData.committedMetadata().length > config.offsetMetadataMaxSize)
                (topicPartition, Errors.OFFSET_METADATA_TOO_LARGE)
              else {
                zkClient.setOrCreateConsumerOffset(
                  offsetCommitRequest.data().groupId(),
                  topicPartition,
                  partitionData.committedOffset())
                (topicPartition, Errors.NONE)
              }
            } catch {
              case e: Throwable => (topicPartition, Errors.forException(e))
            }
        }
        sendResponseCallback(responseInfo)
      } 

对于其他版本的请求,调用如下方法执行

 groupCoordinator.handleCommitOffsets(
          offsetCommitRequest.data.groupId,
          offsetCommitRequest.data.memberId,
          Option(offsetCommitRequest.data.groupInstanceId),
          offsetCommitRequest.data.generationId,
          partitionData,
          sendResponseCallback)
      }
              group.prepareTxnOffsetCommit(producerId, offsetMetadata)
            }
          } else {
            group.inLock {
              group.prepareOffsetCommit(offsetMetadata)
            }
          }

          appendForGroup(group, entries, putCacheCallback)

核心步骤1是将数据存储在内存中,第二步则是将数据append到磁盘上(持久化存储)

4、OFFSET_FETCH RPC 源码剖析

  4.1 RPC 携带的字段

 private final String groupId;
    private final List<TopicPartition> partitions;

offset_fetch rpc 携带的字段如上

groupId 为业务侧消费的唯一标记

partitions 则为这次请求需要获取那些partition上次消费到的位置

 

4.2 server端处理逻辑

4.2.1 VO 请求

if (header.apiVersion == 0) {
            val (authorizedPartitions, unauthorizedPartitions) = offsetFetchRequest.partitions.asScala
              .partition(authorizeTopicDescribe)

            // version 0 reads offsets from ZK
            val authorizedPartitionData = authorizedPartitions.map { topicPartition =>
              try {
                if (!metadataCache.contains(topicPartition))
                  (topicPartition, OffsetFetchResponse.UNKNOWN_PARTITION)
                else {
                  val payloadOpt = zkClient.getConsumerOffset(offsetFetchRequest.groupId, topicPartition)
                  payloadOpt match {
                    case Some(payload) =>
                      (topicPartition, new OffsetFetchResponse.PartitionData(payload.toLong,
                        Optional.empty(), OffsetFetchResponse.NO_METADATA, Errors.NONE))
                    case None =>
                      (topicPartition, OffsetFetchResponse.UNKNOWN_PARTITION)
                  }

服务端从ConsumerPathZNode.path}/${group}/offsets/${topic}/${partition}" 的如下目录获取存储的offset 信息并且返回给客户端

def getOffsets(groupId: String, topicPartitionsOpt: Option[Seq[TopicPartition]]): Map[TopicPartition, OffsetFetchResponse.PartitionData] = {
    trace("Getting offsets of %s for group %s.".format(topicPartitionsOpt.getOrElse("all partitions"), groupId))
    val group = groupMetadataCache.get(groupId)
    if (group == null) {
      topicPartitionsOpt.getOrElse(Seq.empty[TopicPartition]).map { topicPartition =>
        val partitionData = new OffsetFetchResponse.PartitionData(OffsetFetchResponse.INVALID_OFFSET,
          Optional.empty(), "", Errors.NONE)
        topicPartition -> partitionData
      }.toMap
    } else {
      group.inLock {
        if (group.is(Dead)) {
          topicPartitionsOpt.getOrElse(Seq.empty[TopicPartition]).map { topicPartition =>
            val partitionData = new OffsetFetchResponse.PartitionData(OffsetFetchResponse.INVALID_OFFSET,
              Optional.empty(), "", Errors.NONE)
            topicPartition -> partitionData
          }.toMap
        } else {
          topicPartitionsOpt match {
            case None =>
              // Return offsets for all partitions owned by this consumer group. (this only applies to consumers
              // that commit offsets to Kafka.)
              group.allOffsets.map { case (topicPartition, offsetAndMetadata) =>
                topicPartition -> new OffsetFetchResponse.PartitionData(offsetAndMetadata.offset,
                  offsetAndMetadata.leaderEpoch, offsetAndMetadata.metadata, Errors.NONE)
              }

            case Some(topicPartitions) =>
              topicPartitions.map { topicPartition =>
                val partitionData = group.offset(topicPartition) match {
                  case None =>
                    new OffsetFetchResponse.PartitionData(OffsetFetchResponse.INVALID_OFFSET,
                      Optional.empty(), "", Errors.NONE)
                  case Some(offsetAndMetadata) =>
                    new OffsetFetchResponse.PartitionData(offsetAndMetadata.offset,
                      offsetAndMetadata.leaderEpoch, offsetAndMetadata.metadata, Errors.NONE)
                }
                topicPartition -> partitionData
              }.toMap
          }
        }
      }
    }

 

 

 

你提到的两个 `git push` 命令,虽然格式相似,但它们在 **目标仓库、远程名称(origin vs system_wear_CR)、分支路径和用途** 上有显著区别。下面我将从多个维度详细解释它们的差异,并说明其背后的技术逻辑。 --- ## 🔍 命令对比 | 项目 | `git push origin HEAD:refs/for/hw/cbg/HarmonyOS/trunk-hmos_mini_wear_v1r7_dev_20250904` | `git push system_wear_CR HEAD:refs/for/hw/cbg/HarmonyOS/trunk-hmos_mini_wear_v1r7_nativeui/nativeapp` | |------|----------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------| | 远程名 | `origin` | `system_wear_CR` | | 源分支 | `HEAD`(当前本地提交) | `HEAD`(当前本地提交) | | 目标引用 | `refs/for/hw/cbg/..._dev_20250904` | `refs/for/hw/cbg/..._nativeui/nativeapp` | | 所属项目 | 可能是主开发分支的一个临时发布分支 | HarmonyOS 小型穿戴设备 UI/Native 应用模块 | | 推送目的 | 提交代码审核(Code Review),用于集成前评审 | 同样是提交 CR,但针对特定功能模块 | --- ## ✅ 逐项解析 ### 1. `origin` vs `system_wear_CR` —— 不同的远程仓库地址 ```bash # 查看远程仓库配置 git remote -v ``` 输出可能类似: ```text origin https://code.xxx.com/harmonyos/hmos_mini_wear.git (fetch) system_wear_CR ssh://yourname@review.xxx.com:29418/harmonyos/hmos_mini_wear.git (push) ``` - `origin`:通常是只读或普通写权限的镜像库,用于拉取代码。 - `system_wear_CR`:指向 **Gerrit 代码审核系统** 的专用推送地址,专用于提交 `refs/for/*` 类型的审查请求。 > ⚠️ 注意:只有通过 `system_wear_CR` 这类 Gerrit 配置的远程才能推送 `refs/for/...` 分支! 所以第一个命令如果使用 `origin` 推送到 `refs/for/...`,**很可能会失败**,除非 `origin` 也配置了对 Gerrit 的访问权限。 ✅ 正确做法应统一使用 `system_wear_CR` 来提交 CR。 --- ### 2. `refs/for/...` 是什么? 这是 **Gerrit 代码审核系统特有的引用格式**,表示: > “不要直接合并到目标分支,而是创建一个 Code Review 提交(Change)供团队评审。” 例如: ```bash git push <gerrit_remote> HEAD:refs/for/<branch_name> ``` 会: - 创建一个新的 Change ID - 在 Gerrit Web 界面生成评审页面 - 等待 Approve/Verify 后才可合并 --- ### 3. 分支命名差异分析 #### 🟢 第一条: ```text refs/for/hw/cbg/HarmonyOS/trunk-hmos_mini_wear_v1r7_dev_20250904 ``` - 属于某个 **阶段性开发分支** - 名称包含日期 `20250904` → 表示可能是某次版本提测或发布分支 - 用途:临时集成验证,完成后可能废弃 #### 🔵 第二条: ```text refs/for/hw/cbg/HarmonyOS/trunk-hmos_mini_wear_v1r7_nativeui/nativeapp ``` - 明确指向 `nativeui/nativeapp` 子模块 - 很可能是 **UI 框架或原生应用层的功能路径** - 更细粒度控制,适用于模块化开发 👉 区别总结: | 维度 | 第一条 | 第二条 | |------|--------|--------| | 路径类型 | 版本分支(按时间划分) | 功能模块路径(按组件划分) | | 粒度 | 粗(整项目) | 细(具体模块) | | 生命周期 | 临时性(短期存在) | 长期维护(持续迭代) | | 使用场景 | 版本合入前评审 | 日常功能开发评审 | --- ### 4. nativeui/nativeapp 是什么? 这通常指: > **HarmonyOS 小型穿戴设备上的 Native UI 和原生应用框架层** 常见内容包括: - 自定义控件(如圆形进度条、心率图表) - C++ 实现的高性能动画引擎 - 与 OpenHarmony UI 框架对接的 native binding - 手环主屏、健康卡片等原生应用逻辑 因此,向这个路径推送代码,意味着你在修改: - 穿戴设备的底层 UI 渲染机制 - 原生交互逻辑(非 JS 或 ArkTS 编写的部分) - 性能敏感模块(如滑动流畅度优化) --- ## 🛠️ 实际工作流示例 假设你要提交一个“手环消息提醒动画优化”的 PR: ```bash # 1. 切换到你的工作分支 git checkout feature/msg-animation-optimize # 2. 添加并提交更改 git add frameworks/nativeui/ git commit -m "optimize: reduce animation jank in live message" # 3. 推送到 Gerrit 审核系统(必须用正确 remote) git push system_wear_CR HEAD:refs/for/hw/cbg/HarmonyOS/trunk-hmos_mini_wear_v1r7_nativeui/nativeapp ``` 此时 Gerrit 会自动识别你要合入的目标路径,并触发 CI 构建与人工评审流程。 --- ## ❌ 常见错误提示及原因 如果你用 `origin` 推送 `refs/for/...`,可能会看到: ```text remote: ERROR: [refs/for/hw/cbg/...] is not allowed fatal: unable to access 'https://...' ``` 原因: - `origin` 指向的是 GitLab/Gitee 镜像,不支持 Gerrit 的 `refs/for` 协议 - 必须配置专门的 Gerrit 推送 remote ✅ 解决方案: ```bash # 添加 Gerrit 推送远程(以 SSH 为例) git remote add system_wear_CR ssh://yourname@review.xxx.com:29418/harmonyos/hmos_mini_wear.git # 使用它推送 git push system_wear_CR HEAD:refs/for/<target_branch> ``` --- ## ✅ 最佳实践建议 | 建议 | 说明 | |------|------| | ✅ 使用专用 remote 提交 CR | 如 `gerrit`, `cr`, `system_wear_CR` | | ✅ 不要用 `origin` 推送 `refs/for/*` | 大概率失败或被拒绝 | | ✅ 根据模块选择目标路径 | 修改 UI 就推 `nativeui/...`,改核心逻辑推对应目录 | | ✅ 分支命名清晰 | 包含功能、责任人、日期等信息便于追踪 | | ✅ 配合 Change-Id | 确保每次提交都有唯一的 `Change-Id`(由 hooks 自动生成) | --- ## ✅ 总结 | 对比点 | 第一条命令 | 第二条命令 | |--------|------------|-------------| | 目标分支 | `trunk-hmos_mini_wear_v1r7_dev_20250904`(版本分支) | `trunk-hmos_mini_wear_v1r7_nativeui/nativeapp`(功能模块) | | 修改范围 | 整体集成分支,可能涉及多模块 | 聚焦于 native UI 和原生应用层 | | 推送远程 | 应使用 `system_wear_CR`,而非 `origin`(否则会失败) | 同上,必须走 Gerrit 流程 | | 适用场景 | 版本提测前的集中合入 | 日常功能开发与迭代 | | 是否推荐 | ❌ 若用 `origin` 则错误;✅ 改为 `system_wear_CR` 才正确 | ✅ 正确姿势:模块化 CR 提交 | --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值