【ETCD】【源码阅读】深入探索 ETCD 源码:了解 `Range` 操作的底层实现

ETCD 是一个高可用的分布式键值存储系统,广泛应用于分布式系统中提供服务发现、配置管理等功能。在实际开发中,ETCD 的 Range 操作是我们常用的功能之一,它可以帮助我们实现对键值对的查询操作。然而,很多开发者只停留在使用 API 的层面,往往对其底层实现不甚了解。

今天,我们就来深入探讨 ETCD 中 Range 操作的源码,了解其具体实现流程,揭示 ETCD 在处理键值对查询时的核心机制。

1. Range 操作概述

Range 操作是 ETCD 提供的一种用于查询一段键值范围的接口。根据查询条件,Range 可以返回一个键值对列表,或者仅返回符合条件的键值对数量。该操作会有一些限制,如查询版本(Rev)、数量限制(Limit)、以及是否返回键值对本身等。

在 ETCD 的实现中,Range 操作主要涉及到两个部分:事务写操作 (storeTxnWrite) 和事务读操作 (storeTxnRead)。我们将从这两个部分的源码着手,逐步分析 ETCD 如何处理 Range 查询。

2. 事务写操作中的 Range 方法

我们从 storeTxnWriteRange 方法开始分析:

go 复制代码
func (tw *storeTxnWrite) Range(ctx context.Context, key, end []byte, ro RangeOptions) (r *RangeResult, err error) {
    rev := tw.beginRev
    if len(tw.changes) > 0 {
        rev++
    }
    return tw.rangeKeys(ctx, key, end, rev, ro)
}

在这里,Range 方法首先计算出当前的版本号 rev。如果存在写操作(tw.changes 非空),则将版本号加 1,然后调用 rangeKeys 方法执行具体的查询操作。接下来,我们会深入探讨 rangeKeys 方法的实现。

3. 事务读操作中的 rangeKeys 方法

接下来,我们分析 storeTxnRead 中的 rangeKeys 方法,它负责处理 Range 查询的具体逻辑

方法签名与输入参数**
go 复制代码
func (tr *storeTxnRead) rangeKeys(ctx context.Context, key, end []byte, curRev int64, ro RangeOptions) (*RangeResult, error) {

首先,我们来看 rangeKeys 方法的定义。该方法属于 storeTxnRead 结构体,输入参数包括:

  • ctx:请求的上下文,通常用于控制超时、取消等操作。
  • keyend:查询的键范围。key 是起始键,end 是结束键。
  • curRev:当前版本号,表示查询的基准版本。
  • ro:查询选项,包含了如 RevLimitCount 等查询参数。

该方法返回一个 RangeResult 对象和一个错误对象(error),RangeResult 包含了查询结果的键值对以及查询的版本信息。

版本号处理
go 复制代码
rev := ro.Rev
if rev > curRev {
    return &RangeResult{KVs: nil, Count: -1, Rev: curRev}, ErrFutureRev
}
if rev <= 0 {
    rev = curRev
}
if rev < tr.s.compactMainRev {
    return &RangeResult{KVs: nil, Count: -1, Rev: 0}, ErrCompacted
}

在这一段代码中,方法首先获取了查询选项中的版本号 ro.Rev。如果 rev 大于当前版本 curRev,则返回错误 ErrFutureRev,表示请求的版本比当前版本还要新,无法处理该请求。

如果 rev 小于等于零,则将其设置为当前版本 curRev。同时,如果 rev 小于系统的压缩版本 compactMainRev,也会返回 ErrCompacted,表示该版本的数据已经被压缩。

根据 Count 参数进行统计
go 复制代码
if ro.Count {
    total := tr.s.kvindex.CountRevisions(key, end, rev)
    tr.trace.Step("count revisions from in-memory index tree")
    return &RangeResult{KVs: nil, Count: total, Rev: curRev}, nil
}

在这段代码中,方法判断了 ro.Count 是否为 true,即是否仅需要统计符合条件的键值对数量。如果是,CountRevisions 方法会返回符合条件的版本数量,并将其封装在 RangeResult 中返回。

此时,我们并不关心具体的键值对数据,仅返回统计结果。

从内存索引中获取符合条件的版本数据
go 复制代码
revpairs, total := tr.s.kvindex.Revisions(key, end, rev, int(ro.Limit))
tr.trace.Step("range keys from in-memory index tree")
if len(revpairs) == 0 {
    return &RangeResult{KVs: nil, Count: total, Rev: curRev}, nil
}

如果不只是统计数量,而是需要返回键值对数据,方法会调用 Revisions 获取符合条件的版本数据。Revisions 方法会根据 keyendrev 和查询限制 ro.Limit 返回一个版本对列表 revpairs,以及符合条件的总数 total

revpairs 是一个包含了多个版本对的列表,total 是符合条件的版本总数。如果没有符合条件的版本数据,直接返回空的键值对列表和总数。

设置查询限制并获取键值数据
go 复制代码
limit := int(ro.Limit)
if limit <= 0 || limit > len(revpairs) {
    limit = len(revpairs)
}

kvs := make([]mvccpb.KeyValue, limit)
revBytes := newRevBytes()
for i, revpair := range revpairs[:len(kvs)] {
    select {
    case <-ctx.Done():
        return nil, fmt.Errorf("rangeKeys: context cancelled: %w", ctx.Err())
    default:
    }
    revToBytes(revpair, revBytes)
    _, vs := tr.tx.UnsafeRange(buckets.Key, revBytes, nil, 0)
    if len(vs) != 1 {
        tr.s.lg.Fatal(
            "range failed to find revision pair",
            zap.Int64("revision-main", revpair.main),
            zap.Int64("revision-sub", revpair.sub),
            zap.Int64("revision-current", curRev),
            zap.Int64("range-option-rev", ro.Rev),
            zap.Int64("range-option-limit", ro.Limit),
            zap.Binary("key", key),
            zap.Binary("end", end),
            zap.Int("len-revpairs", len(revpairs)),
            zap.Int("len-values", len(vs)),
        )
    }
    if err := kvs[i].Unmarshal(vs[0]); err != nil {
        tr.s.lg.Fatal(
            "failed to unmarshal mvccpb.KeyValue",
            zap.Error(err),
        )
    }
}

在这段代码中,首先根据查询限制 ro.Limit 确定查询的最大数量。然后,遍历 revpairs,对每一对版本数据进行处理。首先,检查请求是否被取消(通过 ctx.Done())。然后,通过 revToBytes 将版本数据转换为字节形式,并调用 UnsafeRange 获取键值对。

UnsafeRange 返回的值不为 1,表示查询失败,此时会记录详细的日志以便后续排查。

接着,通过 Unmarshal 将返回的值反序列化成 mvccpb.KeyValue 结构体,最终存储在 kvs 数组中。

返回查询结果
go 复制代码
tr.trace.Step("range keys from bolt db")
return &RangeResult{KVs: kvs, Count: total, Rev: curRev}, nil

最后,在完成所有的查询后,方法通过 RangeResult 封装结果并返回。RangeResult 包含了查询到的键值对(KVs)、符合条件的键值对数量(Count)和当前版本号(Rev)。

通过逐步解读 rangeKeys 方法的源码,我们可以看到它是如何处理版本查询、数量统计和键值对返回的。每个步骤都为保证 ETCD 在分布式环境下的高效性与一致性提供了支持。

相关推荐
菜就多练吧1 小时前
JVM 内存分布详解
java·开发语言·jvm
0白露2 小时前
设计模式之工厂方法模式
java·python·设计模式·php·工厂方法模式
triticale2 小时前
【数论】快速幂
java·算法
文牧之3 小时前
PostgreSQL 用户资源管理
运维·数据库·postgresql
爱的叹息3 小时前
【java实现+4种变体完整例子】排序算法中【计数排序】的详细解析,包含基础实现、常见变体的完整代码示例,以及各变体的对比表格
java·算法·排序算法
橘猫云计算机设计5 小时前
基于Springboot的自习室预约系统的设计与实现(源码+lw+部署文档+讲解),源码可白嫖!
java·spring boot·后端·毕业设计
秋书一叶6 小时前
SpringBoot项目打包为window安装包
java·spring boot·后端
碎梦归途6 小时前
23种设计模式-结构型模式之外观模式(Java版本)
java·开发语言·jvm·设计模式·intellij-idea·外观模式
极客先躯6 小时前
高级java每日一道面试题-2025年4月13日-微服务篇[Nacos篇]-Nacos如何处理网络分区情况下的服务可用性问题?
java·服务器·网络·微服务·nacos·高级面试