基础架构:RPC和线程
最常见的问题:为啥用Go?
- 6.824使用c++很多年
- c++工作良好
- 但是学生花费很多时间追踪指针和alloc/free错误,而且缺乏满意的c++rpc包
- go对我们来说更好
- 并行更好的支持
- rpc更好的支持
- gc垃圾收集
- 类型安全
- 线程安全的gc是很有吸引力的
- go编程是愉快的
- 相对简单且传统
- Threads
- 线程是有用的架构工具
- Go叫他们协程,其他编程语言叫他们线程
- 非常tricky
为啥要threads?
- 我们需要并行,这在分布式系统很常见,io并行
- 当等待另一个服务的响应时候,处理其他的请求
- 线程在多个核心上是并行运行的
线程
- 线程允许一个程序逻辑上一次运行很多事物
- 线程共享内存
- 每个线程共享他们的线程状态
- pc 寄存器 栈
一个程序可以有多少线程?
- 部分是因为结构
- 一个线程一个client 对于后端任务来说
- 一些是因为多核心并行的意愿
- 因此一个核心一个活跃线程
- Go 运行时候自动的调度可执行协程在`可得到的核心上
- 一些是由于对io并行的期望
- 县城数量被容量和潜力所支配
- 保持增长知道规模不再增长
- go 线程是非常简洁的
- 100 1000是很好的,但是几百万线程很糟糕
- 创造i一个线程想对于一次函数调用是昂贵的
线程带来的挑战
- 共享数据
- 一个线程读取另一个线程已经修改的数据
- 例如 两个线程执行count +1
- 这两个线程是竞争关系 通常会产生bug
- 使用互斥
- 避免共享
- 线程之间合作
- 如何等待所有的线程完成
- 使用go channel 或者WaitGroup
- 并发粒度
- 粗糙的-简单,但是很少能并发/并行性能的
- 优秀的-更加并发,但是竞争 死锁情况更多
什么是爬虫?
- 目标是获取网页,例如得到一个索引
- 图结构的网页
- 每个页面有很多链接
- 图是有环的
爬虫的挑战
- io并行的挑战
- 每秒url获取数量的增加
- 网络的潜在因素更多
- 只抓取一个网页
- 避免带宽的浪费
- 对远程服务良好
- 当爬去完成后去要知道哪些url访问过
爬虫策略:爬虫页面的调度方式
串行爬虫
- 获取页面要避免重复,打破环路
- 单向图,递归调用
- 但是一次只爬一个页面 串行的
并行爬虫
- 为没一个抓取页面创建一个线程
- 很多并行的抓取,很高的抓取效率
- 很多线程共享抓取
- 为什么要互斥?/锁?
- 不用锁
- 两个页面包含同样url的链接
- 两个线程同时抓取两个页面
- T1检查了url,T2检查了url ,两个抓取线程都看见了url,最后都没有抓取
- 都抓取了,但是结果错误
- 同时读/写是竞争,而且会导致bug
- bug可能展示不幸的线程的交叉存储的结果(读写顺序错误了)
- 如果我使用了lock和unlock操作
- go run crawler.go
go run -race crawler.go - 锁导致了原子检查和更新
- go run crawler.go
- 如何决定他完成了
- sync.WaitGroup
- 显式等待子线程完成递归的爬取
- 不用锁
并行爬虫
- a Go channel:
- channnel是一个对象,ch : make(chan int)
- channel让一个线程发送一个对象给另一个线程 ch<-x
- sender等待,直到一些协程收到 y:=<=ch for y := range ch
- 接受这等待直到一些协程发送
- 同步
- 几个线程可以在一个channel里面收发信息
- 发送者阻塞知道接受者收到,在发送时候设置锁可能是危险的
- ConcurrentChannel master()
- master()创建一个worker gorountine 来取得每个页面
- worker()发送url给一个channel
- 多个workers在一个channel里面发送
- master()在channnel里面读取url
- 需要给抓取的map加锁,因为这不是共享的
- 有什么共享的数据?
- channel
- channel上面的片段和string
- master()传递给worker()的参数
什么时候使用共享和锁,而不是channel
-
多部分问题可以以任意的方法解决
-
最关键的在于程序员是如何思考的
- state- 共享和锁
- 通信-channels
- 等待事件-channels
-
使用Go 的竞争探测器
-
https://golang.org/doc/articles/race_detector.htmlgo test -race
RPC 远程过程调用
- 分布式系统的关键组件,所有的labs使用rpc
- 目标:让程序的客户服务器更容易通信
RPC 信息的术语
- client server
- request response
rpc尝试模仿本地调用
-
clients z=fn(x,y)
-
server \
-
fn(x,y){ compute return z }
软件结构
- client app handlers
- stubs 票跟 dispatcher调度员
- rpc lib rpc lib
- net net
Go 例子:kv.go 调度页面链接
- 一个玩具kv存储服务器,put(k,v),gey(k.v)
- 使用go rpc 库
- 通常
- 你需要声明参数和返回的结构体为没一个rpc类型
- client
- connect()'s Dial()创建一个tcp连接给服务器
- call()调用rpc库来执行一个调用
- 需要明确函数名字,参数,位置来存放回复
- 库执行参数,发送请求,等待,回复
- 返回值表明是从哪得到的回复
- 通常你需要一个reply.err表明服务器级别的错误
- server
- go需要你声明一个带有对象的方法来作为rpc处理器
- 你之后注册对象给rpc库
- 接收tcp连接,给他们一个rpc库
- rpc库
- 读每个请求
- 创建一个协程来处理请求
- 获取请求的参数
- 调用命名的方法
- 加上参数的回复
- 通过tcp写回回复
- 服务器get 和put处理器
- 必须上锁,因为rpc库给每个请求创建一个协程
- 读参数,修改回复
细节
- 绑定:client如何知道和谁交互
- 对go rpc 服务器名字/端口是一个交流的参数
- 大系统拥有几个名字或者确认服务器
- Marshalling:生成数据
- go rpc传递字符串 数组 对象 map
- go传递指针通过复制
- 不能传输channel 和函数
rpc问题:失败怎么办?
- 丢包,网络故障,服务器慢,服务器崩溃
client rpc 库如何判断失败?
- client无法看见server的response
- client 不能确保server看见了请求
- 可能是服务器无法看见请求
- 可能是服务器执行,在发送回复前崩溃
- 可能服务器执行,但是网络故障,无法发送回复
最简单的错误处理范式:最大交付
- calll() 在一定时间内等待回复
- 如果没有收到,再次发送
- 重复几次
- 仍然失败,放弃,返回错误
最大交互是应用最容易处理的吗?
- 一个典型的坏的情况
- client 执行
- Put(“k”, 10);
Put(“k”, 20); - 都成功了
- Get(“k”)会得到什么?
- 超时,再次发送
- Put(“k”, 10);
- client 执行
最大交付是ok的吗?
- 对于只读操作
- 操作重复不会造成影响的情况
- DB会检查已经插入的记录
最好的rpc行为:最好一次
- server rpc 代码检查重复的请求,返回之前的回复而不是再次执行handler
问题:如何侦测重复的请求?
- 想法:clienta拥有独一无二的Id 给每个request,使用同一个XID来再次发送
server:if seen[xid]:r = old[xid]elser = handler()old[xid] = rseen[xid] = trueserver:
most-once的复杂性
- 体现在lab3中
如何确保XID是唯一的
- 大随机数
- 将ip地址和一个序列数结合
server最终会丢弃老的rpc
- 什么时候丢弃是安全的?
idea
- 每个client是独一无二的id
- 每个client rpc顺序的数字
- client包括seen all replies <= X 给每个rpc
- 更像tcp的ack顺序
- 或者当seq+1序列到达,那么丢弃seq之前的
如何处理重复的req?
- 服务器不知道重复reply
- pending 标记给没一个rpc 等待 或者忽略
一个most-once 服务器崩溃或者重启怎么办?
- 如果at-most-once有重复信息在内存中,服务器将会忘记并接受re-start的重复requests
- 可能他应该写重复信息给磁盘
- 可能重复服务器也需要重复信息
go prc是一个at most once的简单形式
- 开放tcp连接
- 写请求个tcp链接
- gp rpc不需要重复发送请求
- 因此server不会看见重复请求
- go rpc代码返回一个错误值如果他不能得到回复
- 可能因为网络超时
- 肯呢个服务器无法看见请求
- 可能服务器处理请求但是服务器在收到回复前崩溃
exactly once 准确的一次
- 无限次重试
- 重复的检测
- 错误的容忍服务