前段时间一直在用Python做Web,鼓捣fastapi和sqlalchemy,有点审美疲劳,想尝试点更带劲的。问fzq该做些什么,答曰分布式高并发。正好前段时间看到xfgg做了MIT 6.824,那便来吧,分布式。
久闻MIT 6.824大名,真正上手第一个lab,发现颇有你清fly bitch的风范。讲座在油管上看了一遍,感觉也没讲什么东西。论文在这里,idea给你讲了,用golang去实现一个简单的MapReduce吧!而且我看课表,这个lab只给两周的时间。不愧是真·世一大。
首先是搭建环境。我最近都在工位,用docker开了个容器,安装最新版的go 1.23。把代码下载下来发现,文件结构并不遵循golang module的规则,VSCode打开一大片报错:
我有强迫症,便把目录结构稍微重构了下。之后就可以开始正式开发啦!
文档建议先从Coordinator上手。但把RPC调通后发现worker是比较简单的,复杂度基本都在Coordinator上。而且在调度方面,Map与Reduce没有本质区别。所以最终的实现顺序是:
- Register:Worker进程与Coordinator建立连接。
- Ping:Worker固定帧率向Coordinator发送请求,通知Coordinator自己没有挂掉及自身状态。Coodinator返回分配的任务。
- Worker Map:Worker进行Map operation,结束后通知coordinator。
- Coordinator Map:Coordinator对Map tasks进行调度。
- Timeout:Coordinator固定帧率检查Worker最后一次Ping的时间,如果超时就认为它挂掉了。它进行的任务也将重新分配。这里还比较tricky,worker可能实际上没挂,所以同一个任务被做了两遍。这种情况下我们只保存第一份的结果。
- Worker/Coordinator Reduce:把Map的代码复制一遍。
当然实际过程中涉及多次重构,并不是严格的线性关系。
Worker
Worker至多只有两个线程,几乎没有状态需要同步。主线程不断Ping,收到Task就另开一个goroutine去执行。最开始还想MapReduce结束后,能不能以一种优雅的方式让所有Worker结束。后来嫌麻烦就没管,Coordinator结束Worker RPC请求失败自然就挂掉了。
Coordinator
Coordinator需要维护的状态比较多,分为MapTaskManager、ReduceTaskManger和WorkerManager三类(当然实际代码里没有WorkerManager,懒得改了)。由于我不会channel,索性mutex+condition variable一把梭,十分的不golang。
Debugging
主要debug手段是只开一个Coordinator进程和Worker进程,看结果是否正常。至于线程安全性,通过肉眼观察法保证。最后还用test-mr-many找到一个bug,Reduce漏了一个bucket,似乎是因为我有个地方不小心把reduce_index当作了taskId。把这里改过来后再跑1000遍,没出现同样的问题,暂且当作解决了吧。