前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >etcd源码分析 - 4.【打通核心流程】processInternalRaftRequestOnce四个细节​

etcd源码分析 - 4.【打通核心流程】processInternalRaftRequestOnce四个细节​

作者头像
junedayday
发布于 2022-12-02 11:12:44
发布于 2022-12-02 11:12:44
58900
代码可运行
举报
文章被收录于专栏:Go编程点滴Go编程点滴
运行总次数:0
代码可运行

在上一讲,我们继续梳理了PUT请求到EtcdServer这一层的逻辑,并大概阅读了其中的关键函数processInternalRaftRequestOnce

这个方法里面有不少细节,我们今天就选择其中有价值的四点来看看。

1. entry索引 - appliedIndex与committedIndex

在etcd中,我们将每个客户端的操作(如PUT)抽象为一个日志项(entry)。如果这个操作生效,etcd就将这个entry项同步给其它etcd server,作为数据同步

操作有顺序之分,于是服务端就保存了一个长entry数组,用一个关键的索引index来进行区分entry数组(即一个分界的标志),对entry状态进行分类:

  • entry处于状态A - 小于等于索引的entry项
  • entry处于状态B - 大于索引的entry项

一般状态A和B都是互补的,即是一种二分类状态。

而由于分布式的特性,entry不能立刻完成执行的,于是这里就区分出了两种状态,它们复用一个entry数组:

  • 已应用 - applied
  • 已提交 - committed

对应索引appliedIndexcommittedIndex

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 函数用atomic保证原子性
ai := s.getAppliedIndex()
ci := s.getCommittedIndex()
// 两者的差值,表示已应用但是未提交的entry数,不能太多
if ci > ai+maxGapBetweenApplyAndCommitIndex {
  return nil, ErrTooManyRequests
}

entry数组中的索引的一致性非常重要,尤其是在并发的场景下。而示例中的原子操作,其实是一种乐观锁的实现。

更多的细节就涉及到分布式相关了,这里就不展开。

2.id生成器 - idutil.Generator

Generator数据结构不复杂,它的设计详情都放在了备注里,我们可以自行阅读:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// Generator generates unique identifiers based on counters, timestamps, and
// a node member ID.
//
// The initial id is in this format:
// High order 2 bytes are from memberID, next 5 bytes are from timestamp,
// and low order one byte is a counter.
// | prefix   | suffix              |
// | 2 bytes  | 5 bytes   | 1 byte  |
// | memberID | timestamp | cnt     |

在很多分布式系统中,都需要有一套唯一id生成器。etcd的这个方案相对简单,就是 成员id+时间戳 的组合方案。

关于分布式唯一id,更全面的设计可以参考Snowflake,如 https://segmentfault.com/a/1190000020899379

3.认证模块 - auth.AuthStore

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
authInfo, err := s.AuthInfoFromCtx(ctx)

认证功能在成熟软件中非常常见。在etcd,被独立到了etcd/auth模块中。这个模块的内部调用不复杂,功能就是从context中提取出 用户名+版本信息

这个提取过程中值得注意的是,AuthStore是从grpcmetadata提取出想要的认证信息,而metadata类似于HTTP1协议中的header,是一种用KV形式保存和提取数据的结构。

串联一下我们之前的思路,etcd通过grpc-gateway将HTTP1转化成了gRPC,那么就有一个 HTTP header到grpc metadata的映射过程,有兴趣的可以去研究一下。

总体来说,etcd的认证模块做得很简单,也方便其接入service-mesh。

4.多协程小工具 - wait.Wait

wait.Wait是一个很精巧的小工具,使用起来非常简单:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 示例代码
ch := s.w.Register(id)
s.w.Trigger(id, nil)

我们可以在etcd/pkg/wait目录下看到它的具体实现,我提取了重点

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 通过id,来等待和触发对应的事件。
// 注意使用的顺序:先等待,再触发。
type Wait interface {
  // 等待,即注册一个id
 Register(id uint64) <-chan interface{}
 // 触发,用一个id
 Trigger(id uint64, x interface{})
 IsRegistered(id uint64) bool
}

// 实现:读写锁+map数据结构
type list struct {
 l sync.RWMutex
 m map[uint64]chan interface{}
}

// 注册一个id
func (w *list) Register(id uint64) <-chan interface{} {
 w.l.Lock()
 defer w.l.Unlock()
 ch := w.m[id]
 if ch == nil {
    // go官方建议带buffer的channel尽量设置大小为1
  ch = make(chan interface{}, 1)
  w.m[id] = ch
 } else {
    // 不允许重复
  log.Panicf("dup id %x", id)
 }
 return ch
}

// 触发id的channel
func (w *list) Trigger(id uint64, x interface{}) {
 w.l.Lock()
 ch := w.m[id]
 delete(w.m, id)
  // 取出ch后直接Unlock(可以思考一下与defer的区别)
 w.l.Unlock()
  // 如果触发的id不存在map里,就直接跳过这个判断
 if ch != nil {
  ch <- x
  close(ch)
 }
}

了解Wait的实现之后,我们就知道在正常情况下,RegisterTrigger必须一一对应。

但是,我们再往下看processInternalRaftRequestOnce这部分代码,发现了一个异常点:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
select {
  // 异常:没有找到Trigger,难道忘了?
 case x := <-ch:
  return x.(*applyResult), nil
  // 正常:用Trigger退出
 case <-cctx.Done():
  proposalsFailed.Inc()
  s.w.Trigger(id, nil) 
  return nil, s.parseProposeCtxErr(cctx.Err(), start)
  // 正常:整个server停止,此时不用关心单个Trigger了
 case <-s.done:
  return nil, ErrStopped
}

这里,我们可以做个简单的猜测:在另一个goroutine中,这个etcd server进行了一个操作,包括下面两步:

  1. ch这个channel里发送了一个*applyResult结构的消息
  2. 对wait进行了Trigger操作

小结

今天我们进一步阅读了processInternalRaftRequestOnce中的四个细节,加强了etcd server对请求处理的印象。

etcd作为一款优秀的开源项目,其模块设计比较精巧,而阅读源码的同学也要掌握一个技巧:适当控制阅读深度。比如,在阅读PUT请求时,第一阶段阅读到EtcdServerprocessInternalRaftRequestOnce这层即可:

  • 如果继续深入看raftNode等实现,很容易导致你的整体思路变成过程性的调用,学习不成体系
  • 这时,回过头来巩固一下当前学习的部分,通过串联细节来加深印象,会对你梳理整体更有帮助

Github: https://github.com/Junedayday/code_reading Blog: http://junes.tech/ Bilibili: https://space.bilibili.com/293775192

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-07-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Go编程点滴 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
01.线程状态/创建/启动
多线程作为Java中很重要的一个知识点,在此还是有必要总结一下的。 一.线程的生命周期及五种基本状态 关于Java中线程的生命周期,首先看一下下面这张较为经典的图: 上图中基本上囊括了Java中多线程
Java帮帮
2018/03/15
7870
01.线程状态/创建/启动
【Java】多线程初探
 参考书籍:《Java核心技术 卷Ⅰ 》 Java的线程状态 从操作系统的角度看,线程有5种状态:创建, 就绪, 运行, 阻塞, 终止(结束)。如下图所示 而Java定义的线程状态有: 创建(New)
啦啦啦321
2018/03/30
7260
【Java】多线程初探
Java多线程与并发
答:进程是资源分配的最小单位,线程是CPU调度的最小单位。   1)、进程是资源分配的基本单位,所有与进行相关的资源,都被记录在进程控制块PCB中,以表示该进程拥有这些资源或者正在使用它们。   2)、进程是抢占处理机的调度单位,线程属于某个进程,共享其资源。进程拥有一个完整的虚拟内存地址空间,当进程发生调度的时候,不同的进程拥有不同的虚拟地址空间,而同一进程内不同线程共享同一地址空间,与进程相对应。线程与资源分配无关,它属于某一个进程,并与进程内的其它线程一起共享进程里面的资源。   3)、线程只由堆栈、寄存器、程序计数器和线程计数表TCB组成。
别先生
2020/04/10
1.1K0
Java多线程与并发
一篇文章弄懂Java多线程基础和Java内存模型
写在前面:提起多线程大部门同学可能都会皱起眉头不知道多线程到底是什么、什么时候可以用到、用的时候是不是有共享变量问题等等一大堆问题。本篇文章将分为两部分第一部分是讲解多线程基础、第二部分讲解Java内存模型。
全栈程序员站长
2022/08/31
2550
一篇文章弄懂Java多线程基础和Java内存模型
Java 多线程系列Ⅰ
首先,所有的创建线程的方式都是基于Thread类来实现,每个线程都必须通过 Thread 类的构造方法创建,并实现 run() 方法来执行线程的任务。
终有救赎
2023/12/26
2010
Java 多线程系列Ⅰ
多线程基础知识(全面):创建线程、线程状态如何变化、wait()、notify()、sleep()、停止线程
一直想着抽时间 学习多线程相关知识,目前整理了多线程的基础部分,特在此记录下,并发安全、线程池等后续再补充。
寻求出路的程序媛
2024/05/01
2720
多线程基础知识(全面):创建线程、线程状态如何变化、wait()、notify()、sleep()、停止线程
java基础第十六篇之多线程
1:线程的概念 进程(任务):一个正在运行的程序 进程的调度:CPU来决定什么时候该运行哪个进程 (时间片轮流法) 线程在一个应用程序中,同时,有多个不同的执行路径,是进程中的实际运作单位。 好处是提高程序效率。
海仔
2019/08/05
2950
Java多线程
例如打开你的计算机上的任务管理器,会显示出当前机器的所有进程,QQ,Chrome等,当QQ运行时,就有很多子任务在同时运行。比如,当你边打字发送表情,边好友视频时这些不同的功能都可以同时运行,其中每一项任务都可以理解成“线程”在工作。
用户10358987
2024/04/23
1210
Java多线程
Java 多线程
程序(program)是为完成特定任务、用某种语言编写的一组指令的集合 即指一 段静态的代码,静态对象。
Java_慈祥
2024/08/06
1210
Java 多线程
Java多线程一:基础知识与线程创建的几种方式
进程是系统资源分配的基本单位,线程是CPU调度的基本单位,一个进程可以包含多个线程,同一个进程下面的资源共享很容易,但是进程之间的资源共享相对较难。
全栈学习笔记
2022/04/24
2460
【Java】创建多线程的四种方式
每个线程都有一定的优先级,同优先级线程组成先进先出队列(先到先服务),使用分时调度策略。优先级高的线程采用抢占式策略,获得较多的执行机会。每个线程默认的优先级都与创建它的父线程具有相同的优先级。
CODER-V
2023/03/04
1.4K0
【Java】创建多线程的四种方式
Java进程和线程
1. 进程和线程 进程:进程表示一个运行的程序,程序的代码段,数据段这些都是存放在磁盘中的,在运行时加载到内存中。
翎野君
2023/05/12
7420
Java进程和线程
多线程详解(一)
线程首先得说到进程, 进程:正在执行的应用程序。是系统进行资源分配和调用的独立单元。每一个进程都有他自己的内存空间和系统资源,简单说就是程序进入内存运行变成一个进程,具有一定独立功能。 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。
smallmayi
2022/05/12
2130
☀️苏州程序大白一文解析Java多线程☀️《❤️记得收藏❤️》
程序:是为完成特定任务,用某种语言编写的一组指令的集合,即指一段静态的代码,静态对象。
苏州程序大白
2021/10/13
3650
Java多线程与并发
进程是资源分配的基本单位,所有与进程有关的资源都记录在进程控制块PCB中,以表示进程拥有这些资源或者正在使用它们,进程也是抢占处理机的调度单位,它拥有完整的虚拟内存地址空间,当进程发生调度时,不同的进程拥有不同的地址空间,而同一进程内的不同线程共享同一地址空间。与进程相对应,线程与资源分配无关,它属于某一个进程,并与进程内的其它线程共享进程的资源。
ha_lydms
2023/08/10
2250
Java多线程与并发
1.11 手把手教你从多线程到线程池
单CPU系统中: 每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。 多CPU系统中: 则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序。
ha_lydms
2023/08/09
2500
1.11 手把手教你从多线程到线程池
夯实Java基础系列17:一文搞懂Java多线程使用方式、实现原理以及常见面试题
本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看
Java技术江湖
2019/10/08
1.2K0
【多线程实践】一、为何使用多线程&三种线程创建方式利弊分析
在平常的业务场景中,多线程无疑是比较常用的,而且熟练的使用多线程是开发高并发系统的基础,今天呢,我们就来根据在实际开发中是如何使用多线程的来探讨一下多线程的相关技术,少讲理论多谈实践,以实际开发的角度去总结一下。
灰小猿
2022/10/08
4810
【多线程实践】一、为何使用多线程&三种线程创建方式利弊分析
Java多线程详解
每个运行的程序就是一个进程,当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个进程。
二十三年蝉
2018/08/01
8841
Java多线程详解
java 多线程
就绪,当线程调用了strat()方法的时候,线程就绪,会为其创建方法调用栈和程序计数器。
mySoul
2018/11/19
7930
推荐阅读
相关推荐
01.线程状态/创建/启动
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档