「实验记录」MIT 6.824 Raft Lab2C Persist
#Lab2C - Persist
- I. Source
- II. My Code
- III. Motivation
- IV. Solution
- S1 - 实现persist()
- S2 - 实现readPersist()
- S3 - 持久化三字段
- S4 - 在newRaft()中初始化nextIdxs和matchIdxs
- S5 - 适当缩短心跳时间
- V. Result
I. Source
- MIT-6.824 2020 课程官网
- Lab2: Raft 实验主页
- simviso 精品付费翻译 MIT 6.824 课程
- Paper - Raft extended version
II. My Code
- source code 的 Gitee 地址
- Lab2C: Persist 的 Gitee 地址
课程官网提供的 Lab 代码下载地址,我没有访问成功,于是我从 Github 其他用户那里 clone 到干净的源码,有需要可以访问我的 Gitee 获取
III. Motivation
提出 Raft 的主要目的,是为了解决容错问题,即使集群中有一些机器发生了故障,也不影响整体的运作(对外提供的服务)
我用一个 demo 来说明,假设我们的需求一直都是自己的 PC 能够顺利访问云端的资源(HTTP 或数据库)服务器。在服务器稳定在线的情况下,我们去访问它,一点问题都没有
但是,如果那唯一的一台服务器掉线了,那么我们将无法再访问,即对外的服务到此停止。这是我们无法忍受的,我们希望提供服务的一方能够保持稳定,时时刻刻为我提供访问服务。这就是我们的需求
好,现在问题摆在眼前,提供服务的一方怎样保证稳定性?让唯一的那台服务器永远维持稳定的状态,不允许宕机?这非常地不现实,就好比让一个人练成金刚不坏之身
所以,我们只能琢磨是否可以通过添加服务器的数量来确保对外服务的稳定。更近一步,即是现在服务器不再只有一台,扩充到 3 台,这 3 台中有一台是 primary 服务器,也主要由它对外提供服务;其他 2 台是 secondary 服务器(后备力量),拥有和 primary 服务器相同的数据内容
在 primary 服务器出现故障的时候,secondary 服务器顶上去,替代它的位置。这样就可以保持稳定的对外服务了
这就是我们应对资源服务器崩溃的最常用最有效的法子,但是想实现这个想法,首先要解决数据同步的问题,即如何确保 secondary 服务器拥有和 primary 服务器同样的内容?
这个同步问题,在学术上被称为共识算法,最经典的共识算法是 Paxos,但是它太难理解了。于是,斯坦福那帮人想出了更为简便的共识算法,即 Raft
通过 Raft 算法就可以同步集群中服务器的内容。要实现该算法,分三步走,5 - The Raft consensus algorithm 章节中的 Leader Election、Log Replication 和 Safety
本文主要针对第三步,Lab2C: persist 展开讲解,如有 Lab2A: Leader Election 或 Lab2B: Log Replication 的需要,请移步
IV. Solution
Lab2C: persist 主要就是为了解决一个问题,即网络中的复杂情况会导致集群中的机器掉线 OR 机器本身的宕机。我们希望发生此类的情况,Raft 也能很好地应对
论文中的图 2 也提到了要完成持久化操作,我们需要保存哪些字段放在磁盘中,最重要的莫过于 log[] 、curTerm 和 votedFor 三个字段。其他的例如 nextIdxs[] 和 matchIdxs[] 是可以通过这三个字段即时计算出来的,从工程角度上来讲可以不用保存,这样减少了读写磁盘的时间,从一定程度上提高了 Lab2C: persist 的效率
好,话不多说,我们可以直接开始具体的编码工作了,只要在 Lab2B: Log Replication 确保没有问题的情况下,Lab2C: persist 将会容易很多
S1 - 实现persist()
第一步,就是要实现持久化函数,即 raft.go:persist() ,这个函数干的事情,即是将 log[] 、curTerm 和 votedFor 三个字段写入磁盘,具体如何写入,这不是我们操心的事,我们只需要将其序列化交给 Encoder 即可,
func (rf *Raft) persist() {// Your code here (2C).// Example:// w := new(bytes.Buffer)// e := labgob.NewEncoder(w)// e.Encode(rf.xxx)// e.Encode(rf.yyy)// data := w.Bytes()// rf.persister.SaveRaftState(data)w := new(bytes.Buffer)e := gob.NewEncoder(w)e.Encode(rf.curTerm)e.Encode(rf.votedFor)e.Encode(rf.log)data := w.Bytes()rf.persister.SaveRaftState(data)
}
就像这样,按照上面助教已给出的例子,照猫画虎即可
S2 - 实现readPersist()
我们也要实现读的具体操作,同样如何去读取磁盘也不是我们关心的事,我们只需要调用 Decoder 分解序列化即可,
func (rf *Raft) readPersist(data []byte) {if data == nil || len(data) < 1 { // bootstrap without any state?return}// Your code here (2C).// Example:// r := bytes.NewBuffer(data)// d := labgob.NewDecoder(r)// var xxx// var yyy// if d.Decode(&xxx) != nil ||// d.Decode(&yyy) != nil {// error...// } else {// rf.xxx = xxx// rf.yyy = yyy// }r := bytes.NewBuffer(data)d := gob.NewDecoder(r)var curTerm intvar votedFor intvar log []LogEntryif d.Decode(&curTerm) != nil || d.Decode(&votedFor) != nil || d.Decode(&log) != nil {DPrintf("read persist fail\n")} else {rf.curTerm = curTermrf.votedFor = votedForrf.log = log}
}
又是一个照猫画虎,跟着助教的写法来就可以。每一次的 raft.go:Make() 都会调用 readPersist() 读取已持久化的数据用以初始化,所以它不需要我们操心 OR 考虑应该在何处调用,自带的框架已经帮我们安排好了
S3 - 持久化三字段
我们要在 raft.go 、appendEntries.go 和 requestVote.go 中寻找到流程中更新 log[] 、curTerm 和 votedFor 三个字段的地方,然后在它们完成操作之后持久化此类的数据
在 raft.go 中第一处出现在 raft.go:Start() 中,即 client 追加日志条目之后,要持久化,
func (rf *Raft) Start(command interface{}) (int, int, bool) {// Your code here (2B).rf.mu.Lock()defer rf.mu.Unlock()/*------------Lab2C Persist---------------*/defer rf.persist()index := -1term := rf.curTermisLeader := rf.role == Leaderif isLeader {rf.log = append(rf.log, LogEntry{Idx: rf.lastLogIdx() + 1, Term: term, Cmd: command})index = rf.lastLogIdx()}return index, term, isLeader
}
第二处出现在 raft.go:run() 的 candidate 阶段,因为 follower 成为 candidate 之后会更新 curTerm 和 votedFor ,
func (rf *Raft) run() {for !rf.killed() {switch rf.role {case Follower:...case Candidate:rf.mu.Lock()rf.curTerm++rf.votedFor = rf.merf.voteCount = 1rf.persist()rf.mu.Unlock()...}time.Sleep(10 * time.Millisecond)}
}
之后,就是 requestVote.go 中,需要在 RequestVote() 中添加持久化操作,
func (rf *Raft) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply) {// Your code here (2A, 2B).rf.mu.Lock()defer rf.mu.Unlock()/*------------Lab2C Persist---------------*/defer rf.persist()...
}
就像上述一样,调用 defer rf.persist() 即可,这样就可以在离开函数之前将数据写回磁盘,以及在 sendRequestVote() 中,
func (rf *Raft) sendRequestVote(server int, args *RequestVoteArgs, reply *RequestVoteReply) bool {ok := rf.peers[server].Call("Raft.RequestVote", args, reply)rf.mu.Lock()defer rf.mu.Unlock()if !ok {return ok}term := rf.curTerm/* 自身过期的情况下,直接不再唱票 */if rf.role != Candidate || args.Term != term {return ok}/* 碰到一个任期比自己高的人 */if reply.Term > term {rf.curTerm = reply.Termrf.role = Follower /* candidate 主动回滚至 follower */rf.votedFor = NoBodyrf.persist()return ok}if reply.VoteGranted {rf.voteCount++if rf.role == Candidate && rf.voteCount > len(rf.peers)/2 {rf.role = Leader /* 至关重要 */rf.leaderCh <- struct{}{}}}return ok
}
在碰到一个任期比自己高的人之后,candidate 会更新自己的 curTerm 和 votedFor ,这里也需要注意持久化
最后来到 appendEntries.go 中,同样在 AppendEntries() 中添上 defer rf.persist() 即可,
func (rf *Raft) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) {rf.mu.Lock()defer rf.mu.Unlock()/*------------Lab2C Persist---------------*/defer rf.persist()reply.Success = falsereply.Term = rf.curTerm...
}
在 sendAppendEntries() 中碰见更新任期的情况下,也要持久化,
func (rf *Raft) sendAppendEntries(server int, args *AppendEntriesArgs, reply *AppendEntriesReply) bool {ok := rf.peers[server].Call("Raft.AppendEntries", args, reply)rf.mu.Lock()defer rf.mu.Unlock()if !ok {return ok}term := rf.curTerm/* 自身过期的情况下,不需要在维护 nextIdx 了 */if rf.role != Leader || args.Term != term {return ok}/* 仅仅是被动退位,不涉及到需要投票给谁 */if reply.Term > term {rf.curTerm = reply.Termrf.role = Follower /* 主动回滚至 follower */rf.votedFor = NoBodyrf.persist()return ok}/*------------Lab2B Log Replication----------------*/if reply.Success {...} else {...}return ok
}
至此,已经功成大半,但测试的准确率还不是 100%,是因为有一些问题还需要考虑到
S4 - 在newRaft()中初始化nextIdxs和matchIdxs
记得要在 raft.go:Make() 的子函数 newRaft() 中写上定义 nextIdxs[] 和 matchIdxs[] 的代码,不然会在 boatcastAE() 时出现 index out of range 切片越界的问题,我想不懂为什么会这样,因为即使机器掉线了,它重新上线之后,也需要经过选举才能成为 leader,而成为 leader 的第一件事就是初始化 nextIdxs[] 和 matchIdxs[]
按理说,这两个切片不可能在发送心跳包时为空,具体什么 bug,我称之为玄学,看我代码吧,
func newRaft(peers []*labrpc.ClientEnd, me int, persister *Persister, applyCh chan ApplyMsg) *Raft {rf := &Raft{peers: peers,persister: persister,me: me,role: Follower,voteCount: 0,curTerm: 0,votedFor: NoBody,grantVoteCh: make(chan struct{}, ChanCap),leaderCh: make(chan struct{}, ChanCap),heartBeatCh: make(chan struct{}, ChanCap),commitCh: make(chan struct{}, ChanCap),nextIdxs: make([]int, len(peers)),matchIdxs: make([]int, len(peers)),applyCh: applyCh,commitIdx: 0,appliedIdx: 0,}/* 下标从 1 开始,这非常重要,0 号位置存放默认题目 */rf.log = append(rf.log, LogEntry{Idx: 0, Term: 0})for i := range rf.peers {rf.nextIdxs[i] = rf.lastLogIdx() + 1rf.matchIdxs[i] = 0}return rf
}
这样就不会再出现 index out of range 的情况了
S5 - 适当缩短心跳时间
上面的几种手段已经能够解决大部分错误了,测试基本能够到达 200 次只出现一个失败的情况,即 one(%v) fail to agreement
这错误提示的意思就是集群不能在有效的时间内选出 leader,进而完成日志同步的工作
很明显,就是选主不够快,最简单的办法,即是缩短心跳时间,虽然在 Lab2: Raft 实验主页 中建议我们心跳 1 s 中不超过十次,但是按照 100 ms 的频率,从工程角度来看是不行的,我改成了 90 ms 就没有此类的错误了,
ElectionTimeOut = 250 * time.Millisecond /* 要远大于论文中的 150-300 ms 才有意义,当然也要保证在 5 秒之内完成测试 */
HeartBeatTimeOut = 90 * time.Millisecond /* 心跳 1 秒不超过 10 次 *//* 生成随机超时时间,在 250ms~500 ms 范围之内 */
func randElectionTimeOut() time.Duration {r := rand.New(rand.NewSource(time.Now().UnixNano()))t := time.Duration(r.Int63()) % ElectionTimeOutreturn ElectionTimeOut + t
}/* 生成固定的心跳时间,固定值为 90 ms */
func fixedHeartBeatTimeOut() time.Duration {return HeartBeatTimeOut
}
所以,我还是称之为玄学。模型正确,不一定代表实现正确!
至此,已然讲明白了 Lab2C: persist 整个一套流程
V. Result
golang 比较麻烦,它有 GOPATH 模式,也有 GOMODULE 模式,6.824-golabs-2020 采用的是 GOPATH,所以在运行之前,需要将 golang 默认的 GOMODULE 关掉,
$ export GO111MODULE="off"
随后,就可以进入 src/raft 中开始运行测试程序,
$ go test -run 2C
仅此一次的测试远远不够,可以通过 shell 循环,让测试跑个两百次就差不多了
$ for i in {1..200}; go test -run 2C
这样,如果还没错误,那应该是真的通过了。分布式的很多 bug 需要通过反复模拟才能复现出来的,它不像单线程程序那样,永远是幂等的情况。也可以用我写的脚本 test_2c.py,
import osntests = 200
nfails = 0
noks = 0if __name__ == "__main__":for i in range(ntests):print("*************ROUND " + str(i+1) + "/" + str(ntests) + "*************")filename = "out" + str(i+1)os.system("go test -run 2C | tee " + filename)with open(filename) as f:if 'FAIL' in f.read():nfails += 1print("✖️fails, " + str(nfails) + "/" + str(ntests))continueelse:noks += 1print("✔️ok, " + str(noks) + "/" + str(ntests))os.system("rm " + filename)
我已经跑过两百次,无一 FAIL。之后的 Lab3: Fault-tolerant Key/Value Service 和 Lab4: Sharded Key/Value Service 都是基于 Lab2: Raft 的,要确保你实现的 Raft 算法没有 bug,不然 Labs 越做到后面越难受
相关文章:
「实验记录」MIT 6.824 Raft Lab2C Persist
#Lab2C - Persist I. SourceII. My CodeIII. MotivationIV. SolutionS1 - 实现persist()S2 - 实现readPersist()S3 - 持久化三字段S4 - 在newRaft()中初始化nextIdxs和matchIdxsS5 - 适当缩短心跳时间 V. Result I. Source MIT-6.824 2020 课程官网Lab2: Raft 实验主页simviso…...
软件详细设计总复习(三)【太原理工大学】
题型及分值: 选择 30 分,填空 20 分, 判断 10 分,简答 20 分,综合设计 20 分。 文章目录 三、行为型模式1. 命令模式2. 迭代器模式3. 观察者模式4. 状态模式5. 策略模式 三、行为型模式 1. 命令模式 举个例子&#x…...
Vue3(一):创建vue3工程、setup、vue3响应式原理、computed和watch
Vue3:第一章 一、创建Vue3.0工程1.使用vue-cli创建2.使用vite创建 二、Vue3中的响应式1.拉开序幕的setup2.ref函数3.reactive函数4.vue3中响应式的原理(1)vue2中响应式原理(2)Vue3中的Proxy 5.reactive和ref的对比6.se…...
Spring中的@Value注解详解
Spring中的Value注解详解 概述 本文配置文件为yml文件 在使用spring框架的项目中,Value是经常使用的注解之一。其功能是将与配置文件中的键对应的值分配给其带注解的属性。在日常使用中,我们常用的功能相对简单。本文使您系统地了解Value的用法。 Value…...
YSL赢麻了?SMI社媒心智品牌榜Top20公布:YSL破局夺魁,国货品牌现后起之秀
全文速览 1.数说故事联合用户说从美妆、彩妆、护肤三板块全新发布《SMI社媒心智品牌榜》。 2.圣罗兰、兰蔻、欧莱雅等法国高端美妆大牌垄断美妆《SMI社媒心智品牌榜》前三甲。 3.彩妆Top20榜单中,底妆产品稳居前列,色彩美妆占据一席之地。 4.护肤TOP…...
链式哈希,一致性哈希,倒排表
在普通的查询中,通过关键码的比较进行查找,而哈希是根据关键码直接定位到数据项 哈希冲突:同一个关键码经过哈希函数后指向同一个记录集 链式哈希 using namespace std; #define M 13 typedef int KeyType; //typedef struct //{ // KeyTyp…...
Python操作XML教程:读取、写入、修改和保存XML文档
目录 导入所需模块解析XML文档获取元素遍历XML文档写入新的元素修改元素的内容和属性删除元素保存修改后的XML文档示例演示python操作xml的常用方法 XML是一种常见的数据交换格式,在许多应用中都被广泛使用。通过掌握Python操作XML的基础知识,您将能够轻…...
Oracle数据库中了locked1勒索病毒,用友nchome配置文件损坏该如何解除
随着互联网技术的不断发展,网络安全问题也越来越受到人们的关注。其中,勒索病毒是一种比较常见的网络安全威胁。最近很多集团企业在使用Oracle数据库的过程中,遭遇到了locked1勒索病毒的攻击,导致企业的用友nchome配置文件损坏&am…...
leecode 数据库: 602. 好友申请 II :谁有最多的好友
数据导入: Create table If Not Exists RequestAccepted (requester_id int not null, accepter_id int null, accept_date date null); Truncate table RequestAccepted; insert into RequestAccepted (requester_id, accepter_id, accept_date) values (1, 2, 20…...
基于 Prometheus 的 SLO告警实战
Prometheus是一个流行的开源监控系统,它可以帮助我们收集、存储和查询应用程序或系统的时间序列数据。在使用Prometheus进行监控时,通常需要根据服务水平指标(Service Level Objectives,简称SLO)来设置告警规则。 SLO…...
调用百度API实现图像风格转换
目录 1、作者介绍2、基本概念2.1 人工智能云服务与百度智能云2.2 图像风格转换 3、调用百度API实现图像风格转换3.1 配置百度智能云平台3.2 环境配置3.3 完整代码实现3.4 效果展示3.5 问题与分析 1、作者介绍 张元帮,男,西安工程大学电子信息学院&#…...
5个最好的WooCommerce商城自动化动作来增加销售量
您是否正在寻找简单智能的方法来自动执行任务并增加 WooCommerce 商店的销售额? 通过在线商店中的自动化任务,您可以在发展业务和增加销售额的同时节省时间和金钱。 在本文中,我们将向您展示如何使用 WooCommerce商城自动化来增加销售额。 …...
打开数据结构大门——实现小小顺序表
文章目录 前言顺序表的概念及分类搭建项目(Seqlist):apple:搭建一个顺序表结构&&定义所需头文件&&函数:banana:初始化:pear:打印:watermelon:数据个数:smile:检查容量:fireworks:判空:tea:在尾部插入数据:tomato:在尾部删除数据:lemon:在…...
一.RxJava
1.RxJava使用场景 RxJava核心思想 Rx思维:响应式编程,从起点到终点,中途不能断掉,并且可以在中途添加拦截. 生活中的例子: 起点(分发事件,我饿了)->下楼->去餐厅->点餐->终点(吃饭,消费事件) 程序中的例子: 起点(分发事件,点击登录)->登录API->请求服务器-…...
如何使用 VSCode 软件运行C代码
VSCode 的下载和扩展的配置可以参考文章:VSCode 的安装与插件配置。 VSCode 是很好用的编辑器,通过给其配置 MinGW-w64 插件就可以在它上面编译运行C代码了。 在没有配置 MinGW-w64 插件时,在 VSCode 中运行下面的代码后打印如下图所示。 这…...
C# 调用Matlab打包的 DLL文件(傻瓜式操作)
1、准备Matlab代码 2. 打包 在matlab命令行窗口输入deploytool,打开MATLAB Complier,选择Library Compiler 在TYPE中选择.NET Assembly;在EXPORTED FUNCTIONS中选择要打包的文件;可以选择为自己打包的文件自定义NameSpace名称,本例中将NameSpace定义为…...
微信小程序学习实录3(环境部署、百度地图微信小程序、单击更换图标、弹窗信息、导航、支持腾讯百度高德地图调起)
百度地图微信小程序 一、环境部署1.need to be declared in the requiredPrivateInfos2.api.map.baidu.com 不在以下 request 合法域名3.width and heigth of marker id 9 are required 二、核心代码(一)逻辑层index.js(二)渲染层…...
【面试题】中高级前端工程师都需要熟悉的技能--前端缓存
前端缓存 一、前言二、web缓存分类1. HTTP缓存:2. 浏览器缓存:3. Service Worker:4. Web Storage缓存:5. 内存缓存: 三、http缓存详解1、http缓存类型a. 基于有效时间的缓存控制:b. 基于资源标识的缓存&…...
小红书数据分析:首播卖6亿,小红书直播开启新纪元!
5月22日,章小蕙在小红书开启了第一场带货直播。继董洁之后,小红书又迎来一位超级带货KOL。 据千瓜数据显示,相关话题#章小蕙小红书直播#上线不到30天,话题浏览量就高达2814.89万,笔记互动量达22.24万。 图 | 千瓜数据…...
Weex中,关于组件的水平排列竖直排列居中对齐居左对齐居右对齐低部对齐顶部对齐布局对齐说明
容器内子组件排列方向 子组件竖直方向排列(默认) 子组件水平方向排列 <style> .container {flex-direction: row;direction: ltr; } </style>子组件在父组件容器中的对齐方式 我们主要使用两个属性实现子组件在父组件的对齐方式ÿ…...
在软件开发中正确使用MySQL日期时间类型的深度解析
在日常软件开发场景中,时间信息的存储是底层且核心的需求。从金融交易的精确记账时间、用户操作的行为日志,到供应链系统的物流节点时间戳,时间数据的准确性直接决定业务逻辑的可靠性。MySQL作为主流关系型数据库,其日期时间类型的…...
Cursor实现用excel数据填充word模版的方法
cursor主页:https://www.cursor.com/ 任务目标:把excel格式的数据里的单元格,按照某一个固定模版填充到word中 文章目录 注意事项逐步生成程序1. 确定格式2. 调试程序 注意事项 直接给一个excel文件和最终呈现的word文件的示例,…...
日语学习-日语知识点小记-构建基础-JLPT-N4阶段(33):にする
日语学习-日语知识点小记-构建基础-JLPT-N4阶段(33):にする 1、前言(1)情况说明(2)工程师的信仰2、知识点(1) にする1,接续:名词+にする2,接续:疑问词+にする3,(A)は(B)にする。(2)復習:(1)复习句子(2)ために & ように(3)そう(4)にする3、…...
通过Wrangler CLI在worker中创建数据库和表
官方使用文档:Getting started Cloudflare D1 docs 创建数据库 在命令行中执行完成之后,会在本地和远程创建数据库: npx wranglerlatest d1 create prod-d1-tutorial 在cf中就可以看到数据库: 现在,您的Cloudfla…...
关于iview组件中使用 table , 绑定序号分页后序号从1开始的解决方案
问题描述:iview使用table 中type: "index",分页之后 ,索引还是从1开始,试过绑定后台返回数据的id, 这种方法可行,就是后台返回数据的每个页面id都不完全是按照从1开始的升序,因此百度了下,找到了…...
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个医院查看报告小程序
一、开发环境准备 工具安装: 下载安装DevEco Studio 4.0(支持HarmonyOS 5)配置HarmonyOS SDK 5.0确保Node.js版本≥14 项目初始化: ohpm init harmony/hospital-report-app 二、核心功能模块实现 1. 报告列表…...
数据链路层的主要功能是什么
数据链路层(OSI模型第2层)的核心功能是在相邻网络节点(如交换机、主机)间提供可靠的数据帧传输服务,主要职责包括: 🔑 核心功能详解: 帧封装与解封装 封装: 将网络层下发…...
【论文阅读28】-CNN-BiLSTM-Attention-(2024)
本文把滑坡位移序列拆开、筛优质因子,再用 CNN-BiLSTM-Attention 来动态预测每个子序列,最后重构出总位移,预测效果超越传统模型。 文章目录 1 引言2 方法2.1 位移时间序列加性模型2.2 变分模态分解 (VMD) 具体步骤2.3.1 样本熵(S…...
【C++从零实现Json-Rpc框架】第六弹 —— 服务端模块划分
一、项目背景回顾 前五弹完成了Json-Rpc协议解析、请求处理、客户端调用等基础模块搭建。 本弹重点聚焦于服务端的模块划分与架构设计,提升代码结构的可维护性与扩展性。 二、服务端模块设计目标 高内聚低耦合:各模块职责清晰,便于独立开发…...
在WSL2的Ubuntu镜像中安装Docker
Docker官网链接: https://docs.docker.com/engine/install/ubuntu/ 1、运行以下命令卸载所有冲突的软件包: for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done2、设置Docker…...
