RPC-Thread
# 为什么选择Go?
- 对线程有非常好的支持
- RPC调用非常方便
- 类型安全
- 垃圾回收机制
- 线程+GC非常有吸引力
- 相对简单
# Threads(线程)
- 一个非常有用的组织工具,但是使用比较困难 在Go语言中称为Goroutines,其他则称为线程 线程允许程序在同一时间做更多的事情
# 每个线程共享内存,每个线程独有:
- 程序计数器/寄存器/栈
- 为什么使用线程?
- 并发I/O,客户端并发的发送非常多的 请求到服务器并等待响应
- 可以利用多核CPU,提高性能
- 非常方便,比如在后台开启一条线程检测worker是否还活着
- 是否有其他方案代替线程?
- 有的,像Nodejs一样使用事件驱动,单线程,把I/O请求、响应事件都放到队列去,使用一个循环来处理事件,事件驱动可以高效处理I/O,但是不能利用多核CPU(当然你可以启动多个进程)
# 使用线程有什么挑战?
- 共享数据问题
- 使用锁(Go的sync.Mutex)
- 避免使用共享的可变数据(mutable data)
- 线程协调
- 使用Go的Channel或者sync.Cond或者WaitGroup
# RPC(远程过程调用)
- 所有分布式系统都需要RPC部件
- 所有的labs都使用RPC处理客户端、服务器端通信
- 隐藏网络通信协议的细节
- 自动数据类型转换(strings、arrays、maps、&c)
//
// Client
//
func connect() *rpc.Client {
client, err := rpc.Dial("tcp", ":1234")
if err != nil {
log.Fatal("dialing:", err)
}
return client
}
func get(key string) string {
client := connect()
args := GetArgs{"subject"}
reply := GetReply{}
err := client.Call("KV.Get", &args, &reply)
if err != nil {
log.Fatal("error:", err)
}
client.Close()
return reply.Value
}
func put(key string, val string) {
client := connect()
args := PutArgs{"subject", "6.824"}
reply := PutReply{}
err := client.Call("KV.Put", &args, &reply)
if err != nil {
log.Fatal("error:", err)
}
client.Close()
}
//
// Server
//
type KV struct {
mu sync.Mutex
data map[string]string
}
func server() {
kv := new(KV)
kv.data = map[string]string{}
rpcs := rpc.NewServer()
rpcs.Register(kv)
l, e := net.Listen("tcp", ":1234")
if e != nil {
log.Fatal("listen error:", e)
}
go func() {
for {
conn, err := l.Accept()
if err == nil {
go rpcs.ServeConn(conn)
} else {
break
}
}
l.Close()
}()
}
func (kv *KV) Get(args *GetArgs, reply *GetReply) error {
kv.mu.Lock()
defer kv.mu.Unlock()
val, ok := kv.data[args.Key]
if ok {
reply.Err = OK
reply.Value = val
} else {
reply.Err = ErrNoKey
reply.Value = ""
}
return nil
}
func (kv *KV) Put(args *PutArgs, reply *PutReply) error {
kv.mu.Lock()
defer kv.mu.Unlock()
kv.data[args.Key] = args.Value
reply.Err = OK
return nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# 使用RPC会遇到的问题
- 比如丢包、网络断开、服务器处理很慢、服务器崩溃
- 解决办法:
- 等待响应一段时间,如果没有回应,则重发,重复几次,然后放弃等待,返回错误这种情况只适合读操作,不管重试多少次都不会修改数据,比如DB检查插入的记录是否成功
- RPC最佳实践:最多执行一次(at most once)
- 服务器检查重复的请求,返回上一次已经执行过了的结果
- 服务器检查重复的客户端请求,并针对当前请求返回上次执行后的结果,而不用重复执行当前请求。
- 客户端可以在发送每个请求的时候加入一个唯一的id给服务器端进行检查。
- 如何设定id的值:
- 每一个客户端有一个独立的id(可能是一个随机的大整数)。
- 每一个客户端的RPC请求都有自己的序号。
- 每一个客户端一次只能发送一个RPC请求,这样根据新请求的序列号,服务器可以丢掉所有之前发过来的序列号小于当前新请求序列号的请求。
- 当原本的请求还在执行时,服务器如何检查重复的请求:
- 可以在执行每个RPC请求时加入标记,确定是等待还是忽略当前的请求。
- 如果服务器崩溃或者重启了:
- 如果重复信息是放在内存里,服务器重启之后会重新处理重复的请求。
- 可以考虑将重复的信息持久化到硬盘上。
- 同时还可以考虑将持久化后的重复信息复制到备份服务器上。
- GO的RPC库采用的就是上述的应对方案的简单形式:
- go的RPC只发送请求一次,所以服务器端看不到重复的请求。
- go的RPC调用如果没有收到响应则会返回错误。
# 参考资料
- Go goroutine理解: https://segmentfault.com/a/1190000018150987
上次更新: 2020/08/23, 11:08:00