MIT6.824分布式系统课程 翻译&学习笔记(二)基础架构:RPC和线程【持续更新中】

raesnow · · 570 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

说明

本系列文章是对大名鼎鼎的 MIT6.824分布式系统课程 的翻译补充和学习总结,算是自己一边学习一边记录了。

如有疏漏错误,还请指正:)

持续更新ing。。。

翻译&补充

内容

Go中的线程和RPC,看一下实验

为什么选择Go?

对线程良好的支持
方便的RPC
类型安全
垃圾回收(没有内存释放后再被使用的问题)
线程+垃圾回收非常棒
相对简单
在教程学习后,请阅读 effective go

线程

很有用的构造工具,但是有时候会很棘手
在Go里叫做“goroutine”;其他地方叫做线程

线程 = “执行线程”

线程允许一个程序同时做很多事情
每一个线程依序执行,就像没有线程的普通程序一样
线程之间共享内存
每个线程包括自己的状态:程序计数器,寄存器和栈

为什么选择线程?

它们能够解决并发,在分布式系统中很有用
I/O并发

  • 客户端并行地发送请求到许多服务器,并等待回复;
  • 服务端处理多个客户端的请求,每个请求会阻塞;
  • 在等待客户端X从磁盘读取数据的请求时,会处理客户端Y的请求。

多核的性能:在多个核上并行地执行代码
方便性:在后台,每隔1秒,检查每个worker是否依旧存活。

有没有线程的替代品?

有的:在一个单独的线程里,用代码明确地加入活动;通常称为“事件驱动”。
维持每个活动的状态信息,比如每个客户端请求
一个“事件”的过程如下:

  • 检查每个活动的新输入(比如,服务器的回应)
  • 执行每个活动的下个步骤
  • 更新状态

事件驱动实现I/O并发:

  • 避免线程的消耗(可能会比较大)
  • 但是并没有使用到多核带来的加速
  • 对代码来说很痛苦
线程带来的挑战:

共享数据(临界资源):

  • 比如,如果两个线程同时执行 n = n + 1 会怎么样?或者一个线程增加一个线程读数据?
  • --> 使用锁(Go里的sync.Mutex)
  • --> 避免共享会修改的数据

线程中的协同工作:

  • 比如,一个线程生产数据,另一个线程消费。怎么让消费者等待(释放CPU)?怎么让生产者通知消费者?
  • --> 使用go中的channel,或sync.Cond,或 WaitGroup

死锁:

  • 加锁有环形依赖,或通信有环形依赖(比如:RPC或Go的channel)

让我们看教程中的网络爬虫,把它作为一个线程示例

为什么是网络爬虫?

目前是获取所有的网页,比如:用于建立索引
网页和链接组成图
多个链接指向相同的页面
图中有环形

爬虫带来的挑战:

需要使用I/O并发:

  • 网络延迟比网络容量更受限
  • 需要同时获取多个URL。为了增加每秒的URL获取数目,需要使用并发的多线程

每个URL仅“获取一次”

  • 避免浪费网络带宽
  • 对远程服务器更友好
  • 需要记住每个访问过的URL

知道何时结束

我们看下两种解决方式【课程表页面的crawler.go】
Serial 爬虫:

通过递归Serial调用实现深度遍历
“fetched” map避免了重复,打破环形;一个简单的map,通过传递引用,调用者可以看到并调用者的更新
但是,一个时刻只能获取一个页面;我们可以直接在Serial()函数前加“go”吗?我们可以试下,看看会发生什么?

ConcurrentMutex 爬虫:

创建一个线程用于获取每个页面,启动很多并发的获取,加快获取速度
“go func”创建并执行协程,func可以是匿名方法
多个线程共享“fetched” map,所以同一时刻只有一个线程可以获取任意的指定页面
为什么使用Mutex(Lock()和Unlock())?

  • 原因一:

    * 两个不同的web页面可能包含相同的URL链接
    * 两个线程可能会同步获取这两个页面
    * T1读取fetched[url],T2也读取一样内容
    * 它们会同时发现这个url没有获取(already==false)
    * 它们会同时获取,这是错误的
    * 锁会让检查和更新操作原子化,所以只有一个线程看到already==false
  • 原因二:

    * 从内部来看,map是个复杂的数据结构(tree?就还是可扩展的hash?)
    * 并发更新会破坏内部变量
    * 并发读写会导致读失败
  • 如果我注释掉 Lock()/Unlock(),会怎么样?

    * go run crawler.go会工作吗?
    * go run -race crawler.go 会检测出资源竞争,即使输出是正确的

ConcurrentMutex爬虫怎么判断结束?

  • sync.WaitGroup
  • Wait()会等待所有的Add()数目和Done()一直,即,等待所有的子线程结束[图表:环形URL图上的协程树];每个树的节点上都有一个WaitGroup

这个爬虫会创建多少个并发的线程?

ConcurrentChannel 爬虫:

一个Go channel:

  • 一个channel是一个对象,ch := make(chan int)
  • 一个channel可以实现一个线程向另个一个线程发送数据,ch <- x,发送者等待,直到有协程接收,y := <- ch,for y := range ch,接受者等待,直到有协程发送
  • channel既支持通信又支持同步
  • 多个线程可以在一个channel上通信
  • channel很廉价
  • 记住:发送者会阻塞,直到接受者收到,“synchronnous”,小心死锁

ConcurrentChannel master()

  • master() 创建一个worker协程用来获取每个页面
  • worker() 发送页面的URL切片到channel上,多个wroker在同一个channel上发送
  • master() 从channel上读取URL切片

什么情况下master在等待?master等待的时候占用CPU时间了吗?
不需要对fetched map加锁,因为它并没有被共享!
master怎么知道结束了?

  • 保持worker的数目在n以内
  • 每个worker只在channel上发送一条数据
为什么多个线程使用相同的channel不会资源竞争呢?
worker线程写入URL切片,master线程读取切片,却不使用锁,会有资源竞争吗?
  • worker只在发送“前”写入切片
  • master只在收到“后”读取切片

所以他们不会同时使用切片

什么时候使用共享和锁,什么时候使用channel?

大多数问题用两种方式都可以解决
使用哪个取决于程序员的思考:

* 状态 -- 共享和锁
* 通信 -- channel

对于6.824的试验,建议使用共享和锁存储状态,使用sync.Cond或channel或time.Sleep()用来等待和通知

远程过程调用(RPC)

分布式系统机制最关键的一块,所有试验都在使用RPC
目标:客户端能和服务器的通信更易于编程
隐藏网络协议的细节
转换string、数组、map、指针等各种数据格式为可以传输的格式

image.png

Go示例:课程表页面的 kv.go

一个键值对存储服务器 -- Put(key,value), Get(key)->value
使用Go的RPC库
Common:

  • 定义每个服务器handler的Args和Reply结构

Client:

  • connect()的Dial()创建一个服务器的TCP连接
  • get()和put()是client的“stubs”
  • Call()向RPC库请求执行调用

    • 需要指定服务器方法名、参数,reply的地址
    • 库会marshall参数,发送请求,等待,并unmarshall回复
    • Call()的返回值说明是否获得回复
    • 通常你需要有reply.Err,用于说明服务级别的失败

Server:

  • Go需要服务器定义一个包含RPC handler方法的对象
  • 服务器使用RPC库注册对象
  • 服务器接受TCP连接,传递给RPC库
  • RPC库执行如下内容:

    • 读取每一个请求
    • 为请求创建一个新的协程
    • unmarshall请求
    • 查找命名的对象(在Register()创建的table中)
    • 调用对象的命名方法(dispatch)
    • marshall回复
    • 将回复写到TCP连接中
  • 服务的Get()和Put() handler

    • 需要加锁,因为RPC库为每个请求创建一个新的协程
    • 读取参数,修改回复
一些细节:

绑定:客户端怎么知道与哪台服务器通信?

  • 对于Go的RPC,服务器 名称/端口 是Dial的参数
  • 大系统一般会有自己的命名服务器或配置服务器

Marshalling:数据格式化并打包

  • Go的RPC库可以传递string、数组、对象、map和指针
  • Go通过传递指针来复制指向的数据
  • 不可以传递channel和方法
RPC问题:对于失败的处理?

例如:丢包,网络断开,服务器响应太慢,服务器崩溃

对于客户端的RPC库,失败看起来是什么样的?

客户端没有收到服务端的回复
客户端不知道服务端是否看到请求!
[各种情况下的失败情况图表]

  • 服务端可能没有看到请求
  • 服务端可能执行了,但是在发送回复前崩溃
  • 服务端可能执行了,但是在发送请求时网络断开
最简单的错误处理机制:“尽最大努力”(“最少一次”)

Call()会等待回应一段时间
如果没有回应达到,重新发送请求
重试几次
如果还是失败,则放弃并返回错误

Q:“尽最大努力”在应用中是不是很容易处理?

一个特殊的糟糕的场景:
客户端执行
Put("k", 10);
Put("k", 20);
都成功了
Get("k")会出现什么情况?
[图表:超时,重新发送,原始请求到达晚了]

Q:“尽最大努力”是不是总能正确工作?

只读操作
幂等操作,例如:DB检查是否数据已经插入

更好的RPC行为是:“最多一次”

策略:服务端RPC检查重复请求,返回前一次回复,而并非重新执行

Q:怎么检测出重复的请求?

客户端发送的请求中包含唯一的ID(XID),重新发送时使用相同XID
服务端:

image.png

一些“最多一次”的复杂性

这些会出现在 lab 3
如果两个客户端使用相同的XID,会怎么样?

  • 大随机数
  • 将唯一的客户端ID(IP地址?)和序列号连接?

服务端最后需要丢弃过时的RPC信息

  • 什么时候丢弃是安全的?
  • 策略:

    • 每一个客户端有一个唯一ID(可能是大随机数)
    • 每一个客户端顺序编号RPC请求
    • 客户端对于每个RPC,都存在一个X,收到所有X编号之前的回应
    • 和TCP的请求与回应有些相似
  • 或者只允许客户端在同一时刻只处理一个RPC,这样,当服务端收到seq+1的请求时,可以丢弃小于等于seq的请求

怎么处理重复的请求,当原始请求正在执行?

  • 服务端暂时不回复
  • 策略:对于正在执行的RPC,使用”pengding“标志;等待或者无视重复请求
“最多一次”的服务端崩溃后重启,怎么办?

如果“最多一次”的重复请求信息存储在内存中,服务端会忘记,并在重启后接受重复请求
或许应该将这些重复请求信息存储到硬盘
或许应该采用备份服务器,来备份这些重复请求信息

Go RPC是一种“最多一次”的简单实现

开启TCP连接
将请求写入TCP连接
Go RPC不重发请求,所以服务端不会看到重复请求
Go RPC如果没有收到回应,则返回错误

  • 可能是超时(TCP)
  • 可能是服务端没有看到请求
  • 可能是服务端处理请求,但是服务端或网络在发送回复前出错
“有且仅有一次”怎么样?

有限的重试 + 重复检测 + 容错服务
Lab 3


有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

570 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传