Mit6.5840Lab(2024)

Mit6.5840Lab(2024)

Lab 1: MapReduce

要求

你的任务是实现一个分布式 MapReduce,包括两个程序,协调器和工作者。将只有一个协调器进程,以及一个或多个并行执行的工作者进程。在真实系统中,工作者会运行在许多不同的机器上,但在这个实验中,你将把它们都运行在单台机器上。工作者将通过 RPC 与协调器通信。每个工作者进程都会循环地向协调器请求任务,从一个或多个文件中读取任务的输入,执行任务,将任务的输出写入一个或多个文件,并再次向协调器请求新任务。协调器应该注意到如果一个工作者在合理的时间内(在这个实验中,使用十秒)没有完成其任务,则将相同的任务分配给另一个工作者。

我们为你提供了一些初始代码。协调器和工作者的 “main” 程序位于 main/mrcoordinator.gomain/mrworker.go 中;请勿更改这些文件。你应该将你的实现放在 mr/coordinator.gomr/worker.gomr/rpc.go 中。

MapReduce流程总览

在这里插入图片描述

数据结构设计
type Task struct{
	Type 		int 		// 0-Map 1-Reduce 2-End
	Id 			int			// Id
	Done        bool 		// 0-Begin 1-Running 2-End
	FileName 	string 		// 文件名称
	ReduceNum   int 		// Reduce任务数量
	MapNum      int 		// Map任务数量
	startAt 	time.Time 	// 开始时间
}

type Coordinator struct {
	// Your definitions here.
	mutex        		sync.Mutex
	TaskReduceChan 		[]Task 	// Reduce任务
	TaskMapChan    		[]Task 	// Map任务
	HaveFinMap 			int	   	// 已经完成Map任务数量
	HaveFinReduce 		int		// 已经完成Reduce任务数量
	MapNum      		int 	// Map任务数量
	ReduceNum  			int 	// Reduce任务数量
}
主要实现流程
  • Coordinator
    • GetTask,先派送Map任务,如果检测到Map任务全部完成后开始派送Reduce任务
      • 派送任务时通过DonestartAT字段来判断任务是否可派,超时未完成的任务也可继续派送
    • FinMapTaskFinReduceTask方法,收到Worker完成任务后进行后续处理
  • Worker
    • CallTask RPC方法,获取任务后根据type执行相应的MapReduce方法
    • CallFinMap RPCCallFinReduce RPC方法,通知Coordinator当前Task完成
    • Map方法
      • 首先,打开文件 Task.FileName
      • 然后,使用 ioutil.ReadAll 函数读取文件的全部内容到 content 变量中,关闭已打开的文件,释放资源。
      • 接下来,通过调用 mapf 函数对文件内容进行映射处理,生成键值对数组 kva
      • 接着,代码创建了一个长度为 ReduceNum 的切片 HashedKV,用于存储经过哈希映射后的键值对
      • 遍历 kva 中的键值对,计算每个键对应的哈希值,并通过取余运算将其分配到对应的 Reduce 任务中。分配的方式是将键值对添加到 HashedKV 切片中对应的索引位置。
      • 之后,再次遍历长度为 ReduceNum 的切片,对每个 Reduce 任务创建输出文件,并将对应的键值对编码为 JSON 格式写入文件。文件名以 "mr-" + strconv.Itoa(Task.Id) + "-" + strconv.Itoa(i) 的形式命名,其中 Task.Id 表示任务的唯一标识。
      • 最后CallFinMap(Task.Id)
    • Coordinator方法
      • 首先,定义了一个空的键值对切片 intermediate,用于存储所有 Map 任务产生的中间结果
      • 接着,通过循环遍历所有的 Map 任务产生的文件,文件名格式为 "mr-" + strconv.Itoa(i) + "-" + strconv.Itoa(Task.Id),其中 MapNum 为预定义的常量,表示 Map 的数量,Task.Id 是当前任务的唯一标识。对于每个文件,打开并解码其中的键值对,将其添加到 intermediate 中。
      • 读取所有文件中的键值对后,对 intermediate 中的键值对按照键进行排序,以便后续的 Reduce 操作
      • 然后,创建输出文件,文件名格式为 "mr-out-" + strconv.Itoa(Task.Id),其中 Task.Id 是当前任务的唯一标识。该输出文件将保存 Reduce 操作的结果
      • 在排序后的 intermediate 中进行迭代,对每个不同的键执行 Reduce 函数 reducef,将其对应的值列表作为参数传递给 reducef 函数,并将其返回的结果写入输出文件中。在写入结果时,使用格式化字符串 "%v %v\n",将键和对应的输出值写入文件
      • 最后CallFinReduce(Task.Id)

Lab 2: Key/Value Server

该实验比较容易,不过多介绍

数据结构设计
type Clerk struct {
	server *labrpc.ClientEnd
	id  int64        // client id
	seq int64 // sequence number, increase by 1 for each request
}

type KVServer struct {
	mu sync.Mutex
	mmap map[string]string
	lastClientOp map[int64]op // last operation result for each client
}
主要实现流程
  • Client
    • GetPutAppend方法
      • 通过KeyValueClientIdSeq参数循环向Server发出RPC请求,收到回复后结束
  • Server
    • Get
      • 尝试从mmap中获取key对应的value,获取成功则返回value
    • Put
      • 判断是否重复操作
      • 将key对应value置为参数的value
    • Append
      • 判断是否重复操作
      • 将key对应value追加参数的value

Lab 3: Raft

要求
  • 3A:实现Raft的领导者选举和心跳(AppendEntries RPC,不包含日志条目)。Part 3A的目标是选举出单个领导者,如果没有故障,领导者保持领导地位,如果旧领导者失败或者与旧领导者之间的数据包丢失,则新领导者接管。运行go test -run 3A来测试你的3A代码。
  • 3B:实现领导者和跟随者代码以追加新的日志条目,使得go test -run 3B测试通过。
  • 3C:在raft.go中完成persist()readPersist()函数,通过添加代码保存和恢复持久化状态。你需要将状态编码(或"序列化")为字节数组,以便将其传递给Persister。使用labgob编码器;参考persist()readPersist()中的注释。labgob类似于Go的gob编码器,但如果尝试对具有小写字段名称的结构进行编码,则会打印错误消息。目前,将nil作为第二个参数传递给persister.Save()。在你的实现更改持久化状态的地方插入调用persist()。一旦完成了这一步,如果你的其余实现是正确的,你应该通过所有的3C测试。
  • 3D:实现Snapshot()和InstallSnapshot RPC,以及Raft支持这些变化的更改(例如,操作被修剪的日志)。当你的解决方案通过了3D测试(以及之前的Lab 3测试)时,你的解决方案就是完整的。
Raft consensus algorithm

在这里插入图片描述

数据结构设计
type LogEntry struct {
	Command interface{} 		  //日志记录的命令(用于应用服务的命令)
	Index   int         		  //该日志的索引
	Term    int         		  //该日志被接收的时候的Leader任期
}

type Raft struct {
	mu        sync.Mutex          // Lock to protect shared access to this peer's state
	peers     []*labrpc.ClientEnd // RPC end points of all peers
	persister *Persister          // Object to hold this peer's persisted state
	me        int                 // this peer's index into peers[]
	dead      int32               // set by Kill()
    
	currentTerm 	int
	votedFor    	int
	role      		int
	electionTimer   time.Time
	havevoted		int

	log				[]LogEntry
	commitIndex 	int				// 当前log中的最高索引(从0开始,递增)
	lastApplied		int				// 当前被用到状态机中的日志最高索引(从0开始,递增)
	nextIndex		[]int  			// 发送给每台服务器的下一条日志目录索引(初始值为leader的commitIndex + 1)
	matchIndex		[]int			// 每台服务器已知的已被复制的最高日志条目索引

	applyCh			chan ApplyMsg	// 存储machine state

	lastIncludeIndex int 			// 快照中包含的最后日志条目的索引值
	lastIncludeTerm  int			// 快照中包含的最后日志条目的任期号
}

const (
	Follower          int = 0
	Candidate         int = 1
	Leader            int = 2
	APPLIEDSLEEP 	  int = 20
	// 随机生成投票过期时间范围: MoreVoteTime+MinVoteTime ~ MinVoteTime
	MoreVoteTime 	  int = 120
	MinVoteTime       int = 80
	HeartbeatSleep    int = 35
)
主要实现流程
  • 发起选举

    • type RequestVoteArgs struct {
      	Term 			int
      	LeaderId 		int
      	LastLogIndex  	int
      	LastLogTerm 	int     
      }
      
      type RequestVoteReply struct {
      	Term 			int
      	VoteGranted 	bool
      }
      
    • 起一个goroutine循环检测是否不为LederelectionTimer已过期,进入CandidateTick

    • currentTerm加一重置状态开始进行选举,起多个goroutine向同伴发起RequestVote

    • 如果获得超半数投票,则当选为Leader,设置nextIndexmatchIndex等信息后,开始发起LeaderTick

      • 如果收到回复的Term大于当前Term,停止选举,重置为Follower
      • 有许多细节的重置信息需要反复debug来修正,尤其时votedFor置-1,选举成功不能置为-1,否则会出现脑裂现象
      • 同时注意判断后续选举者状态是否对应选举开始状态
  • 发送心跳

    • type AppendEntriesArgs struct {
      	Term 			int
      	LeaderId 		int
      	PrevLogIndex  	int			//Leader以为的其上条log的index
          PrevLogTerm 	int  		//Leader以为的其上条log的term
      	Entries			[]LogEntry
      	LeaderCommit	int
      }
      
      type AppendEntriesReply struct {
      	Term 			int
      	Success 		bool
      	ConflictIndex	int
      	ConflictTerm	int
      }
      
    • 起一个goroutine循环检测是否为Leder,进入LeaderTick

    • 通过nextIndex来获取需要携带的log信息、PrevLogIndexPrevLogTerm

    • 起多个goroutine向同伴发起AppendEntries

      • 如果收到回复的Term大于当前Term,停止发送心跳,重置为Follower
      • 返回的任期已经落后当前任期,直接return
      • Success时进行更新nextIndexmatchIndex
      • 否则处理日志冲突
        • 如果ConflictTerm为0,直接设置对应nextindexConflictIndex+1
        • ConflictTerm不为0,从后向前搜索logterm等于ConflictTerm的最后一个logindex(冲突快速更新策略)
        • 设置对应nextindex为搜索出的logindex+1
      • 最后通过matchIndex判断是否有log可以进行commit,如果有半数以上同伴同意,更新commit
  • 回复选举

    • 如果当前Term大于选举者的Term,直接false返回

    • 重置信息

    • 如果当前lastIncludeTerm大于选举者,或lastIncludeTerm相等但lastIncludeIndex大于选举者,直接false返回

      如果voteFor为其他人,直接false返回

    • 否则将票投给他,并且更新自身的electionTimer等信息

  • 回复心跳

    • 如果当前Term大于选举者的Term,直接false返回

    • 重置信息。

    • 日志冲突判断

      • 如果PrevLogIndex小于当前commitindex,返回冲突日志indexcommitindex,更新leader的nextindex
      • 如果PrevLogIndex小于当前lastIncludeIndex,返回冲突日志indexlastIncludeIndex,更新leader的nextindex
      • 如果PrevLogIndex大于当前lastlogindex,说明日志缺少,返回冲突日志index为当前lastlogindex,更新leader的nextindex
      • curterm为当前log中PrevLogIndex对应的Term(冲突快速更新策略)
        • 如果PrevLogTerm不等于curterm,反向搜索log中Term不等于curtermindex
        • 返回冲突日志index为搜索出的index,更新leader的nextindex

      将log从PrevLogIndex截断,并append上传来的log

    • 更新当前的commitindexmin(leader的commitindex,lastlogindex)

  • 日志提交

    • 起一个goroutine睡眠循环检测是否lastApplied < commitIndex
    • 将未提交的commitIndex - lastAppliedlog进行提交
    • 注意先将log深拷贝出来,然后释放锁
    • 再向chan里放,否则会出现阻塞问题
  • 持久化

    • log、currentTerm、votedFor、lastIncludeIndex、lastIncludeTerm字段内容进行持久化
    • 注意在每个设置到上述字段修改的地方,都要调用持久化
  • 快照

    • 将log截断存储到snap中,并更新lastIncludeIndexlastIncludeTerm
    • 由于log中内容减少,indexterm的获取要对应进行修正,这点设置许多细节问题,需要注意
    • 在发送心跳的时候如果prelogindex小于lastIncludeIndex,则将自身的Snapshot发送
    • 同时提供了Snapshot接口,通过传入indexsnapshotleader主动进行快照

Lab 4: Fault-tolerant Key/Value Service

要求
  • 你的第一个任务是实现一个在没有丢失消息和服务器故障的情况下正常工作的解决方案
  • 随意将你的客户端代码从 Lab 2 (kvsrv/client.go) 复制到 kvraft/client.go。你需要添加逻辑来决定将每个 RPC 发送到哪个 kvserver。请注意,Recall that Append() no longer returns a value to the Clerk
  • 你还需要在 server.go 中实现 Put()Append()Get() RPC 处理程序。这些处理程序应该使用 Start() 在 Raft 日志中输入一个 Op;你应该在 server.go 中填写 Op 结构的定义,使其描述一个 Put/Append/Get 操作。每个服务器应该在 Raft 提交它们时执行 Op 命令,即当它们出现在 applyCh 上时。RPC 处理程序应该注意到 Raft 提交其 Op,并回复 RPC
  • 当你能可靠地通过测试套件中的第一个测试:“One client”时,你已经完成了这个任务。 添加代码来处理故障,并处理重复的 Clerk 请求,包括 Clerk 在一个任期中将请求发送到 kvserver leader,等待回复超时,然后将请求重新发送到另一个任期的新 leader 的情况。请求应该只执行一次。这些注释包括关于重复检测的指导。你的代码应该通过 go test -run 4A tests
  • 修改你的 kvserver,使其能够检测到持久化的 Raft 状态增长过大,然后将快照传递给 Raft。当一个 kvserver 重新启动时,它应该从持久化器(persister)中读取快照,并从快照中恢复其状态
数据结构设计
type Clerk struct {
	servers 		[]*labrpc.ClientEnd
	clientid		int64
	seq				int64
	leaderserver	int64
}

type Op struct {
    // Raft start使用的op
	ClientId 	int64
	Seq      	int64
	Do			string
	Key			string
	Value		string
}

type KVServer struct {
	mu     	 	sync.Mutex
	me     	 	int
	rf      	*raft.Raft
	applyCh 	chan raft.ApplyMsg
	dead    	int32 	// set by Kill()

	maxraftstate int 	// snapshot if //log grows this big

	// Your definitions here.
	kvMap		map[string]string
	seqMap		map[int64]int64
	replyMap	map[IndexAndTerm]chan OpAppendReply
	lastApplied int
}

type IndexAndTerm struct {
    // 利用index和term来构造接收结果的chan,要及时删除
    Index int
    Term  int
}
主要实现流程

Client

  • for {
        response := OpAppendReply{}
        ok := ck.servers[ck.leaderserver].Call("KVServer.Op", &args, &response)
        if ok{
            if response.Err == OK{
                return response.Value
            }else if response.Err == ErrNoKey{
                return ""
            }else if response.Err == ErrWrongLeader{
                ck.leaderserver = (ck.leaderserver + 1) % int64(len(ck.servers))
                continue
            }
        }
        ck.leaderserver = (ck.leaderserver + 1) % int64(len(ck.servers))
    }
    
  • 如上将put append get方法统一到了一起,循环发送RPC请求给server,得到回复后结束

Server

  • Op方法接收处理RPC请求

    • 如果不是Leader,直接返回ErrWrongLeader
    • 构造op结构体,发送raft.Start(op),之后定时等待indexterm构造的chan返回结构
    • 等到了返回结果,没等到超时就返回错误
  • 起一个applyOp() goroutine,监测raftapplyCh时候有命令

    • 根据命令的Do参数去进行不同处理

    • 如果RaftStateSize大于maxraftstate阈值,保存当前snapshot,并通过raftSnapshot接口发送

    • 根据indextermchan写入结果,对应上面

    • 如果是snapshot的话,进行DecodeSnapShot并更新lastApplied

  • PersistSnapShot()

    • kvMapseqMap进行持久化,对对应修改的地方都要进行存储
    • server初始化时如果是重连,要进行DecodeSnapShot
  • 前面一直出现如上错误too slowly

  • 解决方法:在raft中start时,最后直接调用leadertick进行同步

Lab 5: Sharded Key/Value Service

5A要求
  • 你必须在 shardctrler/ 目录下的 client.goserver.go 中实现上述指定的接口。你的 shardctrler 必须是容错的,使用你在实验 3/4 中的 Raft 库。当你通过 shardctrler/ 中的所有测试时,你已经完成了这个任务。
5A数据结构设计
type Config struct {
	Num    int              // config number
	Shards [NShards]int     // shard -> gid
	Groups map[int][]string // gid -> servers[]
}

type Clerk struct {
	servers []*labrpc.ClientEnd
	clientid		int64
	seq				int64
	leaderserver	int64
}

type ShardCtrler struct {
	mu      	sync.Mutex
	me     		int
	rf      	*raft.Raft
	applyCh 	chan raft.ApplyMsg
	configs 	[]Config // indexed by config num
	seqMap		map[int64]int64
	replyMap	map[int]chan Op
}

type Op struct {
	ClientId 	int64				// 用户ID
    Seq      	int64				// cmd的ID
    Do			string				// Query Join Leave Move
	QueryNum	int					// QueryArgs Num
    JoinServers map[int][]string	// JoinArgs Servers
	LeaveGIDs 	[]int				// LeaveArgs GIDs
	MoveShard 	int					// MoveArgs Shard
	MoveGID   	int					// MoveArgs GID
}
5A主要实现流程

本实验实在lab2基础上,对于不同的key进行分片,不同分片交由不同的GID来管理,每个GID又包含多个server,本实验主要实现对分片的管理以及负载均衡

Client

  • QueryJoinLeaveMove方法

  • // Query请求的部分代码
    for {
        response := QueryReply{}
        ok := ck.servers[ck.leaderserver].Call("ShardCtrler.Query", args, &response)
        if ok{
            if response.Err == OK{
                return response.Config
            }else if response.Err == ErrWrongLeader{
                ck.leaderserver = (ck.leaderserver + 1) % int64(len(ck.servers))
                continue
            }
        }
        ck.leaderserver = (ck.leaderserver + 1) % int64(len(ck.servers))
        time.Sleep(100 * time.Millisecond)
    }
    
  • 如上代码循环进行RPC请求,获得回复后退出

    • Query:通过KeySeqNum参数查询Config,成功后返回Config
    • Join:通过KeySeqservers参数,请求服务器将servers加入
    • Leave:通过KeySeqGIDs参数,请求服务器将GIDs中gid删除
    • Move:通过KeySeqShardGID参数,请求服务器将Shard分片分配给GID

Server

  • 对应Client的RPC请求,四个类似的方法来处理
    • 根据收到的不同请求,构造统一个op结构体,然后调用raft.Start(op),等待replyChan返回结果(超时不候)
    • raft同步后传入applyCh
    • applyOp()方法监听applyCh,根据不同的Do参数去分别进行QueryJoinLeaveMove处理
    • 相应方法处理后,将处理结果传入replyChan,上面RPC方法收到结果,然会给Client
  • JoinHandler
    • 将入参中的servers不在config.Group中的加入进去
  • LeaveHandler
    • 将真实存在的入参中的gids删除掉
  • MoveHandler
    • config.Shards[shard] = gid
  • CommonRebalance
    • 上述四个方法执行完成后,都要调用这个方法进行负载均衡,要求不同组负责的分片数量不能相差大于1
    • 首先实现一个找出拥有最多和最少分片的组的方法
    • 找出空闲未分配的分片,每次都分配给当前最少分片的组
    • 循环将拥有最多分片的组分一个给最少的组,直到不满足max_gid_num > (min_gid_num + 1),结束
  • 注意在进行map复制的时候,要再取k、v进行复制,不然直接x=y只是地址一样,会出现错误
5B要求
  • 在配置更改期间实现分片迁移。确保副本组中的所有服务器在它们执行的操作序列中的同一点进行迁移,以便它们都接受或拒绝并发客户端请求。
  • 在处理后续测试之前,你应该专注于通过第二个测试(“加入然后离开”)。
  • 在通过测试中除了 TestDelete 之外的所有测试后,你就完成了这个任务。
5B数据结构设计
type Clerk struct {
	sm       		*shardctrler.Clerk
	config   		shardctrler.Config
	make_end 		func(string) *labrpc.ClientEnd
	seq				int64
	leaderserver	int64
	clientid		int64
}

type Op struct {
	// Raft start使用的op
	ClientId 	int64
	Seq      	int64
	Do			string
	Key			string
	Value		string
	Config		shardctrler.Config
	Shard		Shard
	ShardId		int
	SeqMap		map[int64]int64
}

type ShardKV struct {
	mu           sync.Mutex
	migratemu    sync.Mutex
	me           int
	rf           *raft.Raft
	applyCh      chan raft.ApplyMsg
	make_end     func(string) *labrpc.ClientEnd
	gid          int
	ctrlers      []*labrpc.ClientEnd
	maxraftstate int // snapshot if log grows this big
	dead    	 int32 // set by Kill()
    mck          *shardctrler.Clerk // sck is a client used to contact shard master

	// Your definitions here.
	seqMap		map[int64]int64
	replyMap	map[int]chan OpAppendReply
	config		shardctrler.Config 	// 当前的config
	lastconfig	shardctrler.Config  // 上一份config
	shards 		[]Shard				
}

type Shard struct{
	StateMachine map[string]string  // 每个分片的kvMap
	ConfigNum    int 				// version
}
5B主要实现流程

Client

  • 类似Lab4中的getputappend三个操作
  • 开始利用shard := key2shard(key),将该RPC传给对应的Group
  • 每次循环后使用ck.config = ck.sm.Query(-1),更新config

Server

  • 类似Lab4中的对getputappend三个操作RPC回应处理
  • 增加isMatchShard判断该server是否拥有该分片,且kv.shards[id].StateMachine是否不为nil,该操作在start前后都要判断,不通过直接返回ErrWrongGroup,否则在网络错乱情况下会出错
  • Lab4最主要的区别还有要对分片进行管理,起一个DetectConfig() goroutine来对分片进行管理
    • 开始先判断是否有不属于当前的分片还持有
      • 通过新的config,将分片的shard信息和seqMap信息通过AskShard RPC请求发给对应的server
      • 对方Leader收到后,跟get方法类似,起一个raft.start(op),在applyCh接收到命令后真正执行
      • 完成后将自身的shard删除,也是通过起一个raft.start(op)
      • sleepcontinue
    • 再判断是否当前该获取的shard已经获取,否则sleepcontinue
    • 到这里当前shard已经是当前config准确的状态
    • config Num加一,进行Query,如果真的获取到了新的config,进行更新当前config
  • UpdateConfig
    • 如果config num小于等于当前,返回错误
    • lastconfig更新为configconfig更新为new config
    • 将属于自己的分片且之前没被拥有过的,进行初始化
  • AddShard
    • 如果cmd.Seq < int64(kv.config.Num) || kv.shards[cmd.ShardId].StateMachine != nil,直接break
    • 将分片的StateMachine更新,且将SeqMap中大于当前或者不存在更新
  • RemoveShard
    • 如果cmd.Seq < int64(kv.config.Num) || kv.shards[cmd.ShardId].StateMachine == nil,直接break
    • shardStateMachine置为nil
    • shardConfigNum置为int(cmd.Seq)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值