被问的最多的问题? 为什么这么课程选择了Go?
6.824这门课程过去选择使用C++
过去学生们花费了太多时间修改跟分布式系统无关的bug,比如他们释放了还在使用的对象。
Go拥有一些特性可以让你更加在集中注意力在分布式系统而不是语言细节
- 类型安全
- 垃圾回收(这样就不存在释放后使用的问题了)
- 很好的支持并发
- 很好的支持RPC
我们喜欢使用Go编程
一门非常容易学习的语言,可以使用教程effective_go
Remote Procedure Call (RPC)
- 分布式系统的关键部分,全面的实验都使用RPC.
- RPC的目的:
- 容易编写网络通信程序
- 隐藏客户端服务器通信的细节
- 客户端调用更加像传统的过程调用
- 服务端处理更加像传统的过程调用
- RPC被广泛的使用!
RPC理想上想把网络通信做的跟函数调用一样
Client:
z = fn(x, y)
Server:
fn(x, y) { compute return z }
RPC设计目标是这种水平的透明度。
Go example:
RPC消息流程图:
Client Server
request--->
<---response
软件架构
client app handlers
stubs dispatcher
RPC lib RPC lib
net ------------ net
一些细节:
- 应该调用哪个服务器函数(handler)?
- 序列化:格式化数据到包中
- 棘手的数组,指针,对象等。
- Go的RPC库非常强大。
- 有些东西你不能传递:比如channels和function。
- 绑定:客户端怎么知道应该跟谁通信?
- 也许客户端使用服务器的hostname。
- 也许使用命名服务,讲服务名字映射到最好的服务器。
- 线程:
- 客户端可能使用多线程,所以多于一个调用没有被处理,对应的处理器可能会是否缓慢,所以 服务器经常将每个请求放置在独立的线程中处理。
RPC问题:怎么处理失败?
- 比如:丢包,网络断线,服务器运行缓慢,服务器崩溃。
错误对RPC客户端意味着什么?
- 客户端没有获取到服务器的回复。
- 客户端不知道服务器是否接收到请求!也许服务器的网络在发生请求前就失败了。
简单的方案:“最少一次”行为
- RPC库等待回复一段时间,如果还是没有回复到达,重新发生请求。重复多次,如果还是没有回复,那么返回错误给应用程序。
Q: "至少一次"容易被应用程序处理吗?
- 至少一次写的简单问题: 客户端发送"deduct $10 from bank account"
Q: 这个客户端程序会出现什么错误?
- Put("k",10) -- 一个RPC调用在数据库服务器中设置键值对。
- Put("k",20) -- 客户端对同一个键设置其他值。
Q: 至少一次每次都可以很好的工作吗?
- 是的:如果回复操作的是OK,比如,只读操作。
- 是的:如果应该程序有自己处理多个写副本的计划。
更好的RPC行为:“最多一次”
- idea:服务器的RPC代码发现重复的请求,返回之前的回复,而不是重写运行。
- Q:如何发现相同的请求?
client让每一个请求带有唯一标示码XID(unique ID),相同请求使用相同的XID重新发送。
server:
if seen[xid]: r = old[xid] else r = handler() old[xid] = r seen[xid] = true
一些关于“最多一次”的复杂性
这些都会断断续续地出现在实验二中
- 怎么确认xid是唯一的?
- 很大的随机数?
- 将唯一的客户端ID(ip address?)和序列号组合起来?
- 服务器最后必须丢弃老的RPC信息?
- 什么时候丢弃是安全的?
- idea:
- 唯一的客户端id
- 上一个rpc请求的序列号
- 客户端的每一个RPC请求包含"seen all replies <=X"
- 类似tcp中的seq和ack
- 或者每次只允许一个RPC调用,到达的是seq+1,那么忽略其他小于seq
- 客户端最多可以尝试5次,服务器会忽略大于5次的请求。
- 当原来的请求还在执行,怎么样处理相同seq的请求?
- 服务器不想运行两次,也不想回复。
- 想法:给每个执行的RPC,pending标识;等待或者忽略。
如果“至多一次”服务器奔溃或者重启会怎么样?
- 如果服务器将副本信息保存在内存中,服务器会忘记请求,同时在重启之后接受相同的请求。
- 也许,你应该将副本信息保存到磁盘?
- 也许,副本服务器应该保存副本信息?
关于“至少执行一次”?
- 至多一次+无限重试+容错服务
Go RPC实现的”最多一次“?
- 打开TCP连接
- 向TCP连接写入请求
- TCP也许会重传,但是服务器的TCP协议栈会过滤重复的信息
- 在Go代码里面不会有重试(即:不会创建第二个TCP连接)
- Go RPC代码当没有获取到回复之后将返回错误
- 也许是TCP连接的超时
- 也许是服务器没有看到请求
- 也许服务器处理了请求,但是在返回回复之前服务器的网络故障
线程
- 线程是基本的服务器构建工具
- 你将会在实验中经常使用
- 线程非常“狡猾”
- 对RPC非常有用
- Go中使用goroutines代替线程
线程 = “控制线程”
- 线程可以使一个程序同时执行很多事情
- 线程共享内存
- 每个线程包含额线程状态:程序计数器、寄存器、栈
线程挑战
共享数据
- 两个线程在同一个时间修改同一个变量?
当一个线程读取数据,同时另一个线程正在修改这个数据?
上面的问题经常被称为竞争,需要在共享数据上面使用Go中的sync.Mutex保护变量
- 线程协调
- 比如:使用Go中的channels等待全部相关的线程完成工作
- 死锁
- 线程1等待线程2
- 线程2等待线程1
- 比竞争容易诊断
- 锁粒度
- 粗粒度 --> 简单,但是更小的并发/并行
- 细粒度 --> 更好的并发,更容易数据竞争和死锁
- 让我们一起看看名为labrpc的RPC包说明这些问题
看看今天的讲义 -- labrpc.go
- 它很像Go的RPC系统,但是带有模拟网络
- 这个模拟的网络会延迟请求和回复
- 这个模拟的网络会丢失请求和回复
- 这个模拟的网络会重新排序请求和回复
- 对之后的实验二测试非常有用
- 说明线程、互斥锁、通道
- 完整的RPC包完全使用Go语言编写
结构
- 网络结构
- 网络描述
- 服务器
- 客户端节点
- 每个Network结构都持有一个sync.Mutex
- 网络描述
RPC概述
- 更多的例子在test_test.go中
比如: TestBasic()函数
- 应用程序调用Call()函数,发生一个RPC请求并等待结果
reply := end.Call("Raft.AppendEntries", args, &reply)
- 服务器端 srv := MakeServer() srv.AddService(svc) // 一个服务器含有多个服务, 比如. Raft and k/v svc := MakeService(receiverObject) // 对象的方法将会处理RPQ请求
服务器结构
- 一个服务器程序支持多个服务
AddService
- 添加一个服务名字
- Q: 为什么上锁?
AddService可能在多个goroutine中被调用
- Q: defer() 函数的作用?
deter的作用是在函数退出前,调用之后的代码,在里面就是添加完新服务后,做解锁操作。
Dispatch
- 分发请求到正确的服务
- Q: 为什么持有锁?
这里应该指的是Server::dispatch函数,
- Q: 为什么不在函数的结尾处持有锁?
Call():
- 使用反射查找参数类型
- 使用“gob”序列化参数(译注:gob是Golang包自带的一个数据结构序列化的编码/解码工具。)
- e.ch是用于发生请求的通道
- 需要一个通道从网上接收回复(<- req.replyCh)
MakeEnd():
- 使用一个线程或者goroutine模拟网络
- 从e.ch中获取请求,然后处理请求
- 每个请求分别在不同的goroutine处理
Q: 一个端点是否可以拥有多个未处理的请求
- Q:为什么使用rn.mu.Lock()?
- Q:锁保护了什么?
ProcessReq():
- 查看服务器端
- 如果网络不可靠,可能会延迟或者丢失请求,在一个新的线程中分发请求。
- 通过读取e.ch等待回复直到时间过去100毫秒。100毫秒只是来看看服务器是否崩溃。
- 最后返回回复 Q: 谁会读取回复?
- Q:ProcessReq没有持有rn锁,是否安全?
Service.dispatch():
- 为请求找到合适的处理方法
- 反序列化参数
- 调用方法
- 序列化回复
- 返回回复
Go的内存模型需要明确的同步去通信
- 下面的代码是错误的
代码很容易写成上面,但是Go的语法会说没有定义,使用channel或者sync.WaitGroup替换var x int done := false go func() { x = f(...); done = true } while done == false { }
学习Go关于goroutine和channel的教程
- 使用Go的竞争诊断器
- https://golang.org/doc/articles/race_detector.html go test --race mypkg