Xline command 去重机制(一)—— RIFL 介绍

为什么要对 command 去重?

在一个接收外部 command 的系统中,通常一个 command 至少要执行一次,我们称其为 at-least-once semantics。如果一个 command 执行失败,系统内部经常会实现一套重试结构来尝试恢复这个问题,这就会引出一个问题:重复命令的提交可能会对系统的状态造成影响。

例如要实现 linearizable semantics (用户的每个操作外部看来是立即执行的、恰好执行一次、在它发起调用和返回之间的某一个时间点执行),我们就需要对 command 去重。在没有 command 去重的 Raft 实现中,一个 command 可能会被执行多次,leader 可能在 commit 之后返回给 client 期间崩溃,client 如果向一个新的 leader 重试相同的 command ,最后就会导致一个 command 被执行了两次。

解决这个问题可以有两种方案:一种是类似 etcd 的方法,区分出可以重试的命令和不可以重试的命令,将不可以重试的命令的错误结果返回给用户,并不提供任何保证,即使这个命令可能已经被系统执行了。另一种方案是实现一套 command 的 track 机制,检查系统中执行了的命令来实现 command 的去重,当系统实现了这种去重机制,可以实现 command 执行的 exactly-once semantics,进而实现更高级别的一致性保证。

Etcd 中的 gRPC client interceptor,重试的实现

// unaryClientInterceptor returns a new retrying unary client interceptor.
//
// The default configuration of the interceptor is to not retry *at all*. This behaviour can be
// changed through options (e.g. WithMax) on creation of the interceptor or on call (through grpc.CallOptions).
func (c *Client) unaryClientInterceptor(optFuncs ...retryOption) grpc.UnaryClientInterceptor {intOpts := reuseOrNewWithCallOptions(defaultOptions, optFuncs)return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {ctx = withVersion(ctx)grpcOpts, retryOpts := filterCallOptions(opts)callOpts := reuseOrNewWithCallOptions(intOpts, retryOpts)...var lastErr errorfor attempt := uint(0); attempt < callOpts.max; attempt++ {if err := waitRetryBackoff(ctx, attempt, callOpts); err != nil {return err}...lastErr = invoker(ctx, method, req, reply, cc, grpcOpts...)if lastErr == nil {return nil}...// 在这里基于 callOpts 和 lastErr 判断是否可以重试if !isSafeRetry(c, lastErr, callOpts) {return lastErr}}return lastErr}
}

去重在什么阶段完成?

首先要清楚去重的目的是什么:防止重复 command 对状态机造成影响。那么就会有两个阶段可以对 command 进行去重,一是接收到 command 的阶段,另一个是在应用到状态机时对 command 去重。

无论是哪个方法,我们都需要一个数据结构来追踪已接收并执行的 command 的进度,这样才可以对客户端发来的 command 去重处理。

在应用到状态机阶段时进行 command 的去重处理,自然可以利用状态机的后端存储来获取到之前执行的 command (例如 Log::get_cmd_ids 可以获取 log 中所有的 command id),利用这个 command id set 可以对即将应用到状态机的 command 进行去重处理。

在接收到 command 阶段进行去重处理,就需要额外维护一套数据结构进行处理,通常这套数据结构需要维护在内存中保证高效的读写速度,所以要求这套数据结构不能占用非常大的空间,可能要具备 GC 的机制。

对于第一种方案,首要的问题是 log compaction 机制会让去重机制失效。在 raft 系统中,大家都会实现 log compaction 来避免 log 占用过多内存,如果一个 command log 已经被 compact,那么下次接受到相同的 command 就无法去重了。其次的问题是重复的 command 会在发送到状态机之前还会进行 prepare,speculative execute 处理,消耗额外的 CPU,最后的问题就是读状态机的操作是昂贵的,这种去重手段的性能损耗会很大。

综上,我们可以确定在接受到 command 的阶段就进行去重处理,尽早地拒绝重复 command 的提交。

目前的 command 去重设计的缺陷

Xline curp 内的 command 是一个靠外部实现的 trait,我们没有像 etcd 一样区别对待一些 command 的重试行为,所以我们没有在 command trait 中定义是否具有重试的特征。

目前 curp client 中对所有的 command 都会有重试,所以我们在 curp server 处实现了一套简单的去重机制:

CommandBoard 中有一个 IndexSet,用于记录之前已经执行过的 command 的 ID

/// Command board is a buffer to track cmd states and store notifiers for requests that need to wait for a cmd
#[derive(Debug)]
pub(super) struct CommandBoard<C: Command> {.../// The cmd has been received before, this is used for deduppub(super) sync: IndexSet<ProposeId>,...
}

于是我们可以在 O(1) 的开销下在 propose 阶段对命令进行去重

pub(super) fn handle_propose(&self,cmd: Arc<C>,) -> Result<((Option<ServerId>, u64), Result<bool, ProposeError>), CurpError> {...if !self.ctx.cb.map_write(|mut cb_w| cb_w.sync.insert(id)) {return Ok((info, Err(ProposeError::Duplicated)));}...}

为了保证 CURP 在发生 leadership transfer 时不会丢失当前正在的执行的 command 的 ID,我们在恢复 Speculative Pool 中也会恢复这个结构

/// Recover from all voter's spec poolsfn recover_from_spec_pools(&self,st: &mut State,log: &mut Log<C>,spec_pools: HashMap<ServerId, Vec<PoolEntry<C>>>,) {...for cmd in recovered_cmds {let _ig_sync = cb_w.sync.insert(cmd.id()); // may have been inserted before...}}

最后为了避免这个 IndexSet占用过多内存,之前在 Xline 源码解读文章中提到的 GC 机制会定时清理这个结构。

不过,在极端的网络条件下,client 在发起 command 和收到返回之间的间隔超过了 GC 的间隔,IndexSet 中记录的 ProposeId 被 GC 清理了,client 重试这个命令会导致这个命令的去重失效。

我们在 madsim 的测试中发现了这种极端情况(由于 madsim 的时钟的流速比现实快很多,最后触发了这种问题),需要提出一个新的去重结构来解决问题。本文的后半部分将介绍 RIFL(Reusable Infrastructure for Linearizability) 的工作原理,后续的文章中我们将详细介绍如何在 Xline 中实现 RIFL。

RIFL 介绍

RIFL (Reusable Infrastructure for Linearizability), 是一种在大规模集群中保证 RPC(command) exactly-once semantics 的一套基础设施。为了将 RIFL 的术语和 Xline 系统的术语的统一,本篇章中 RPC 和 Command 具有相同的语义。

RIFL 简介

在 RIFL 中,首先要给每一个 RPC 分配一个 unique identifier,它由一个 64-bit 的 client_id 和在这个 client_id 下分配的 64-bit 的递增 sequence_number 组成。

client_id 需要由一个 system-wide 的结构生成,在 RIFL 中使用了一个全局的 Lease Manager 模块实现,Lease Manager 会为每个 client 分配一个 client_id,并创建一个与之对应的 lease,client 需要不断 keep alive 这个 lease,server 则需要检查这个 lease 来判断 client 是否崩溃。

其次,RIFL 需要一个 RPC 的完成记录和追踪信息持久化,然后,RIFL 在系统迁移时,RPC 的完成记录也需要一并迁移,这样可以保证 RIFL 在系统迁移过程中也能保证 RPC 不会重复执行,重复执行的 RPC 将会直接取出之前的完成记录。

最后 RPC 的完成记录需要在 Client 确认下清除(或者 Client 的奔溃后清除),这样可以安全地清理掉一些不必要的存储。

RIFL 具体组件及功能

下面是 RIFL 中的一些主要组件以及对应的功能:1. Request Tracker: 在 client 端追踪发送出去的命令 a. newSequenceNum(): 为一个 RPC 生成一个递增的序列号 b. firstIncomplete(): 获取当前最小的还未收到 RPC 回复的序列号 c. rpcComplete(sequenceNumber): 标记当前 sequenceNumber 为收到,后续用于更新 firstIncomplete

2. Lease Manager: 一个统一的 Lease Manager 模块,client 使用它为 client_id 续租, server 使用它检查 client_id 的租约是否到期 a. getClientId(): client 获取自己的 client_id,如果不存在的话就询问 lease server 创建一个 b. checkAlive(clientId): server 检查这个 client_id 的租约是否到期来判断该 client 是否存活

3. Result Tracker: 在 server 端追踪接收到的命令,以及 client 确认的进度 a. checkDuplicate(clientId, sequenceNumber): 根据完成记录来判断这个 RPC 是否重复 b. recordCompletion(clientId, sequenceNumber, completionRecord): 在返回给 client 之前标记这个 RPC 为已执行,并存储 completionRecord c. processAck(clientId, firstIncomplete): 为这个 client 回收 firstIncomplete 之前的所有 RPC 的完成记录。

当 Server 收到一个 RPC(client_id, seq_num, first_incomplete) 时,会根据 checkDuplicate 来检查这个 RPC 的状态:1. NEW: 一个新的 RPC,按照正常的逻辑处理请求2. COMPLETED: 一个已经执行完成的 RPC,返回执行完成的记录3. IN_PROGRESS: 一个正在执行的 RPC,返回 IN_PROGRESS 错误4. STALE: 一个已经被 client 确认回收的 RPC,返回 STALE 错误

其次,Server 会根据传来的 RPC 中 first_incomplete 字段来调用 proccessAck,回收掉已经被确认的 RPC 的完成记录。最后,当这个 RPC 执行完成,返回给 Client 之前,会调用 recordCompletion 来将完成记录持久化,并标记这个 RPC 为 COMPLETED。

除此之外,Server 会检查 client_id 的租约是否还有效来判断一个 client 是否还存活,如果失效,则回收掉这个 client_id 下的所有的完成记录。

RIFL 性能分析

在 RIFL 的结构中,很容易发现一处开销来自于 client 和 server 与 Lease Manager 之间通信的开销,RIFL paper 中提到了 server 可以缓存某个 client_id lease 的过期时间,在即将过期时查询 Lease Manager,这样可以省去一些的网络通信。

在上述的过程中 checkDuplicate 或者 proccessAck 中至少会有一个 O(n) 复杂度的操作(按 sequenceNumber 顺序记录进行 checkDuplicate 或者无序记录 sequnceNumber 但需要遍历过滤小于 first_incomplete 进行 processAck),和之前使用 IndexSet 方案的 O(1) 复杂度相比,RIFL 会在这里会有一部分开销。我们可以将 processAck 单独作为一个 RPC 用于通知 server 回收完成记录来优化一些性能。

最后,由于回收的第一种机制仅是检查 first_incomplete,这可能会遇到某个耗时很长的 RPC 阻塞了回收后续 RPC 的完成记录,最后可能导致 server 内存占用过多。RIFL paper 中提到了可以为某个 client 设置最大 inflight RPC 的数量,过多的 RPC 将会被拒绝,另外,也可以考虑提前回收后续的 RPC 完成记录,这样可能会使 RIFL 更加复杂。

小结

以上是 RIFL 为 unary RPC 维护 exactly-once semantics 的机制,paper 中 §6 Implementing Transactions with RIFL 中描述了 RIFL 对多个对象的 transactions 维护 exactly-once semantics 的机制,由于 Xline 系统的 transaction 会作为一个单一 command 发送给 server,所以不需要单独处理,感兴趣的读者可以看看,这里就不展开赘述了。

Summary

本文前半部分从 command 去重机制的契机开始,介绍了去重的必要性以及目前 Xline 的去重机制存在的一些问题。后半部分详细讲解了 RIFL(Reusable Infrastructure for Linearizability) 的工作原理,并对其进行了一些性能分析。后续的文章中将继续介绍我们是如何将 RIFL 应用到我们的 Xline 当中,以及对 RIFL 做了哪些必要的更改与优化。

Xline于2023年6月加入CNCF 沙箱计划,是一个用于元数据管理的分布式KV存储。Xline项目以Rust语言写就。感谢每一位参与的社区伙伴对Xline的帮助和支持,也欢迎更多使用者和开发者参与体验和使用Xline。

GitHub链接:

https://github.com/xline-kv/Xline

Xline官网:www.xline.cloud

Xline Discord: 

https://discord.gg/XyFXGpSfvb

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/588521.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

HTML 基础

文章目录 01-标签语法标签结构 03-HTML骨架04-标签的关系05-注释06-标题标签07-段落标签08-换行和水平线09-文本格式化标签10-图像标签图像属性 11-路径相对路径绝对路径 12-超链接标签13-音频14-视频 01-标签语法 HTML 超文本标记语言——HyperText Markup Language。 超文本…

【分布式配置中心】聊聊Apollo的安装与具体配置变更的原理

【管理设计篇】聊聊分布式配置中心 之前就写过一篇文章&#xff0c;介绍配置中心&#xff0c;但是也只是简单描述了下配置中心的设计点。本篇从apollo的安装到部署架构到核心原理进一步解读&#xff0c;大概看了下apollo的原理&#xff0c;感觉没有必要深究&#xff0c;所以就…

2023年“中银杯”四川省职业院校技能大赛“云计算应用”赛项样题卷③

2023年“中银杯”四川省职业院校技能大赛“云计算应用”赛项&#xff08;高职组&#xff09; 样题&#xff08;第3套&#xff09; 目录 2023年“中银杯”四川省职业院校技能大赛“云计算应用”赛项&#xff08;高职组&#xff09; 样题&#xff08;第3套&#xff09; 模块…

【北亚数据恢复】mysql表被truncate,表数据被delete的数据恢复案例

云服务器数据恢复环境&#xff1a; 华为ECS云服务器&#xff0c;linux操作系统&#xff0c;mysql数据库&#xff08;innodb引擎&#xff09;。作为网站服务器使用。 云服务器故障&#xff1a; 在执行mysql数据库版本更新测试时&#xff0c;误将本应该在测试库上执行的sql脚本执…

亚马逊云科技Amazon Q,一款基于生成式人工智能的新型助手

近日&#xff0c;亚马逊云科技宣布推出Amazon Q&#xff0c;这是一款基于生成式人工智能&#xff08;AI&#xff09;的新型助手&#xff0c;专为辅助工作而设计&#xff0c;可以根据您的业务量身定制。通过连接到公司的信息存储库、代码、数据和企业系统&#xff0c;可以使用Am…

个人游戏启动器 | 游戏数据库 playnite 折腾记录

环境&#xff1a;Windows 11 问题&#xff1a;使用平板串联PC游戏后&#xff0c;需要一个本地的PC启动器 解决办法&#xff1a;使用playnite搭配插件 背景&#xff1a;我是个单机游戏爱好者&#xff0c;因为某些原因&#xff0c;需要串流游玩&#xff0c;需要一个方便手柄操作的…

arkts状态管理使用(@State、@Prop、@Link、@Provide、@Consume、@objectLink和@observed)

一、状态管理 1.在声明式UI中&#xff0c;是以状态驱动视图更新&#xff1a; ①状态&#xff08;State&#xff09;:指驱动视图更新的数据&#xff08;被装饰器标记的变量&#xff09; ②视图&#xff08;View&#xff09;:基于UI描述渲染得到用户界面 注意&#xff1a; ①…

【零基础入门VUE】VueJS - 模板

✍面向读者&#xff1a;所有人 ✍所属专栏&#xff1a;零基础入门VUE专栏https://blog.csdn.net/arthas777/category_12537076.html 我们在前面的章节中学习了如何在屏幕上以文本内容的形式输出。在本章中&#xff0c;我们将学习如何在屏幕上以 HTML 模板的形式获取输出。 为了…

什么是AI PC,又有哪些产品

最近一段时间&#xff0c;AI PC成为一个流行词。Intel在发布Core Ultra处理器的时候&#xff0c;直接使用了AI PC这个词语&#xff0c;而各大厂商发布相应的笔记本产品时&#xff0c;也使用了AI Ready的宣传词。而在Intel之前&#xff0c;AMD在发布自己的新一代APU的时候&#…

Linux中安装了openjdk后jps command not found

一、问题场景 在Linux中用yum安装了openjdk-17&#xff0c;也在.bashrc中配置了环境变量JAVA_HOME以及bin目录的PATH。 但是在运行jps命令时依然报错找不到命令 二、原因分析 进入到$JAVA_HOME/bin目录查看&#xff0c;发现只有寥寥几个命令&#xff0c;压根没有jps命令&…

《现代操作系统》第十二章习题答案

计算机硬件的改进主要归功于更小的晶体管。一些限制因素包括&#xff1a;(a) 光的波动性可能限制传统光刻技术制造集成电路的能力&#xff0c;(b) 固体中个别原子的迁移性可能导致非常薄的半导体、绝缘体和导体层的性能退化&#xff0c;(c) 背景辐射活性可能破坏分子键或影响非…

ARCGIS PRO SDK GeometryEngine处理独立几何图形

1、面积类&#xff1a;pol为Polygon 1).Area&#xff1a;获取几何图形的面积。这是使用二维笛卡尔数学来计算面积的平面测量 double d GeometryEngine.Instance.Area(pol) 2).GeodesicArea:获取几何图形的椭球面积 …

【Redis-08】Redis主从复制的实现原理

在Redis中&#xff0c;可以通过slaveof命令或者设置slaveof选项实现两台Redis服务器的主从复制&#xff0c;比如我们有两个Redis机器&#xff0c;地址分别是 127.0.0.1:6379 和 127.0.0.1:6380&#xff0c;现在我们在前者上面执行&#xff1a; 127.0.0.1:6379 > SLAVEOF 12…

图片预览 element-plus 带页码

vue3、element-plus项目中&#xff0c;点击预览图片&#xff0c;并显示页码效果如图 安装 | Element Plus <div class"image__preview"><el-imagestyle"width: 100px; height: 100px":src"imgListArr[0]":zoom-rate"1.2":max…

菜鸟学习vue3笔记-vue hooks初体验

import { ref } from "vue"; export default function () {let a1 ref(1);let a2 ref(5);let c ref(0);function add() {a1.value;a2.value;}return {add,a1,a2,c,}; }<template><div><p>第一个数字{{ a1 }}</p><p>第二个数字{{ a2…

公共用例库计划--个人版(一)

1、公共用例库计划 1.1、目标 在公司测试管理体系的演变过程中&#xff0c;从禅道过渡到devops再到云效平台&#xff0c;我们已经实现了对bug和用例的有效集中管理。然而&#xff0c;在实际操作中发现&#xff0c;尽管用例管理得到了初步整合&#xff0c;但在面对不同系统和测…

Python高级并发编程的实例详解

更多Python学习内容&#xff1a;ipengtao.com Python中的高效并发编程&#xff0c;有几个重要的概念和工具可以帮助大家充分利用多核处理器和提高程序性能。本文将介绍一些关键的概念和示例代码&#xff0c;以帮助大家更好地理解Python中的高效并发编程。 多线程 vs. 多进程 在…

计算机网络【HTTP 面试题】

HTTP的请求报文结构和响应报文结构 HTTP请求报文主要由请求行、请求头、空行、请求正文&#xff08;Get请求没有请求正文&#xff09;4部分组成。 1、请求行 由三部分组成&#xff0c;分别为&#xff1a;请求方法、URL以及协议版本&#xff0c;之间由空格分隔&#xff1b;请…

全栈架构:从0开始,Vue的搭建与开发

尼恩说在前面 在40岁老架构师 尼恩的读者交流群(50)中&#xff0c;很多小伙伴拿到一线互联网企业、上市企业如阿里、网易、有赞、希音、百度、滴滴的面试资格。 然后&#xff0c;很多小伙伴平时聚焦CRUD&#xff0c;没有亮点项目&#xff0c; 黄金项目。 简历也写得是非常lo…

Generalized Focal Loss V1论文解读

摘要 单级检测器基本上将物体检测表述为密集分类和定位&#xff08;即边界框回归&#xff09;。分类通常通过Focal Loss进行优化&#xff0c;而边界框的定位通常根据Dirac delta分布进行学习。单级检测器的最新趋势是引入一个单独的预测分支来估计定位质量&#xff0c;预测质量…