项目被拆分为一个客户端调用RPC获得结果,两个服务器接收调用。
项目client与server流程图

客户端发送数据流程
应用层
限流 - > 服务发现 + 负载均衡 - > 注册熔断器 - > 池化管理网络连接 - > 获取连接 - > 序列化参数 -> 传递请求数据 - > 接收数据记录熔断信息。
传输层
多路复用同一个TCP -> SendAsync:生产请求序列Id,本地缓存一份Id与future的映射,帮助未来接收到数据后根据对应Id找到对应future。发送数据。
TCP建立时维护一个接收协程:readLoop(),阻塞直到接收到信息,接收到信息之后根据Id获得future,并关闭channel通知完成。
数据格式:
bash
magic(2Byte) + HeaderLen(4Byte) + BodyLen(4Byte) + Head + Body
(边界问题)粘包/半包处理:socket的数据一直读,如果有新数据就拼接到本地保存的buffer中,拿到新到达的数据包的HeaderLen + BodyLen,将magic + HeaderLen + BodyLen = 2 + 4 + 4 = 10与获得的HeaderLen + BodyLen长度求和,如果数据超过,就说明一个完整的包已经到达,则切分并传回这个包。
协议处理
Decode编解码问题:拿到data包检查magic,长度正常后,读取头部,反序列出使用的压缩工具,进行解压缩并封装Message返回。
服务端接收数据流程
传输层:
server :Listen监听tcp连接请求,阻塞直到有TCP连接建立。处理连接。并读取信息。
(边界问题)粘包/半包处理:socket的数据一直读,如果有新数据就拼接到本地保存的buffer中,拿到新到达的数据包的HeaderLen + BodyLen,将magic + HeaderLen + BodyLen = 2 + 4 + 4 = 10与获得的HeaderLen + BodyLen长度求和,如果数据超过,就说明一个完整的包已经到达,则切分并传回这个包。
应用层:
处理请求调用invoke,反射获得服务类型,服务名称,进行调用后得到结果返回。对结果序列化后拼接头部返回。
RPC程序设计细节
限流
限流器被New出来时自动开启一条协程每秒装填token。限流器限制了一秒内超过rate的额外请求全被丢弃。
Go
package limiter
import (
"sync"
"time"
)
type TokenBucket struct {
tokens int
rate int
mu sync.Mutex
stopChan chan struct{}
}
func NewTokenBucket(rate int) *TokenBucket {
tb := &TokenBucket{tokens: rate, rate: rate, stopChan: make(chan struct{})}
go func() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
tb.Reset()
case <-tb.stopChan:
return
}
}
}()
return tb
}
func (tb *TokenBucket) Allow() bool {
tb.mu.Lock()
defer tb.mu.Unlock()
if tb.tokens > 0 {
tb.tokens--
return true
}
return false
}
func (tb *TokenBucket) Reset() {
tb.mu.Lock()
defer tb.mu.Unlock()
tb.tokens = tb.rate
}
func (tb *TokenBucket) Stop() {
close(tb.stopChan)
}
服务发现
当寻找服务时首先向ETCD查询一次主机以及对应的服务地址缓存本地
重点服务发现,开启一个协程获得ETCD的通知针对不同类型进行处理
|-----------------|-----------------------|--------------|
| 事件类型 | 触发时机 | 处理 |
| EventTypePut | 新服务注册到了etcd,新server启动 | 添加新服务信息到本地缓存 |
| EventTypeDelete | 服务被移除或宕机 | 从本地缓存删除 |
Go
func (r *Registry) watch(service string) {
key := fmt.Sprintf("%s%s/", r.prefix, service)
for {//建立watch通道,监听服务的所有实例变化
watchChan := r.client.Watch(r.ctx, key, clientv3.WithPrefix())
for watchResp := range watchChan {
for _, event := range watchResp.Events {
switch event.Type {
case clientv3.EventTypePut:
addr := string(event.Kv.Value)
r.mu.Lock()
r.services[service][addr] = Instance{Addr: addr}
r.mu.Unlock()
case clientv3.EventTypeDelete:
deletedKey := string(event.Kv.Key)
addr := strings.TrimPrefix(deletedKey, r.prefix+service+"/")
r.mu.Lock()
delete(r.services[service], addr)
r.mu.Unlock()
}
}
}
// watch 断了,稍后重连
time.Sleep(time.Second)
}
}
负载均衡
负载均衡支持三种算法轮询,顺序是server1 -> server2 -> server3 -> server4 -> server1 ->.... ->
权重轮询,根据权重增加概率增加。以及随机。
以最简单常用的的轮询为例。
实现对外Select接口,让外界可以直接调用。
Go
package loadbalance
import (
"kamaRPC/internal/registry"
"sync/atomic"
)
type RoundRobin struct {
idx uint64
}
func NewRR() *RoundRobin {
r := &RoundRobin{}
return r
}
func (r *RoundRobin) Select(list []registry.Instance) registry.Instance {
i := atomic.AddUint64(&r.idx, 1)
return list[i%uint64(len(list))]
}
熔断机制
什么是熔断?
类似于与下游服务调用的"保险丝",如果某个服务出现故障比如响应超时、错误率过高。自动切断服务调用,防止故障扩散影响整个系统。
作用是:防止雪崩、快速失败、自动恢复(关闭之后进入半开状态,少量请求探测下游服务,如果成功关闭熔断;否则继续保持)。
- 关闭(Closed):正常调用下游服务,同时监控失败率。
- 打开(Open):当失败率达到阈值(如5秒内失败率超过50%),熔断器跳闸,直接拒绝后续请求。
- 半开(Half-Open):经过一段冷却时间后,允许少量请求通过,用于探测服务是否恢复。
Go
package breaker
import (
"log"
"sync"
"time"
)
type State int
const (
Closed State = iota
Open
HalfOpen
)
type CircuitBreaker struct {
mu sync.Mutex
state State
// 统计数据
failureCount int
successCount int
// 配置参数
windowSize int // 统计窗口大小(次数)
failureThreshold float64 // 失败率阈值
openTimeout time.Duration // 熔断持续时间
// 状态控制
lastStateChange time.Time
halfOpenProbe bool // 半开状态下是否已有探测请求
}
func NewCircuitBreaker(windowSize int, failureThreshold float64, openTimeout time.Duration) *CircuitBreaker {
return &CircuitBreaker{
state: Closed,
windowSize: windowSize,
failureThreshold: failureThreshold,
openTimeout: openTimeout,
lastStateChange: time.Now(),
}
}
func (cb *CircuitBreaker) Allow() bool {
cb.mu.Lock()
defer cb.mu.Unlock()
switch cb.state {
case Closed:
return true
case Open:
// 熔断时间到了,进入半开
if time.Since(cb.lastStateChange) > cb.openTimeout {
cb.state = HalfOpen
cb.halfOpenProbe = false
return true
}
return false
case HalfOpen:
// 只允许一个探测请求
if cb.halfOpenProbe {
return false
}
cb.halfOpenProbe = true
return true
}
return true
}
func (cb *CircuitBreaker) RecordSuccess() {
cb.mu.Lock()
defer cb.mu.Unlock()
switch cb.state {
case Closed:
cb.successCount++
case HalfOpen:
// 探测成功 → 恢复
cb.toClosed()
case Open:
//按道理是不会进入这块的
log.Println("理论不发生触发")
}
}
func (cb *CircuitBreaker) RecordFailure() {
cb.mu.Lock()
defer cb.mu.Unlock()
switch cb.state {
case Closed:
cb.failureCount++
total := cb.failureCount + cb.successCount
if total < cb.windowSize {
return
}
rate := float64(cb.failureCount) / float64(total)
if rate >= cb.failureThreshold {
cb.toOpen()
return
}
cb.resetCounts()
case HalfOpen:
// 探测失败 → 重新熔断
cb.toOpen()
case Open:
// 已经熔断,不处理
}
}
func (cb *CircuitBreaker) toOpen() {
cb.state = Open
cb.lastStateChange = time.Now()
cb.resetCounts()
cb.halfOpenProbe = false
}
func (cb *CircuitBreaker) toClosed() {
cb.state = Closed
cb.lastStateChange = time.Now()
cb.resetCounts()
cb.halfOpenProbe = false
}
func (cb *CircuitBreaker) resetCounts() {
cb.failureCount = 0
cb.successCount = 0
}
func (cb *CircuitBreaker) State() State {
cb.mu.Lock()
defer cb.mu.Unlock()
return cb.state
}
池化管理TCP连接
Go
func (c *Client) getPool(addr string) *transport.ConnectionPool {
if pool, ok := c.pools.Load(addr); ok {
return pool.(*transport.ConnectionPool)
}
newPool := transport.NewConnectionPool(addr, 0, 1) //连接池
actual, _ := c.pools.LoadOrStore(addr, newPool)
return actual.(*transport.ConnectionPool)
}
TCP多路复用
本地实现发送序列seq与对应future映射。数据传出后挂起future等待对应回应返回。
Go
func (c *TCPClient) SendAsync(msg *protocol.Message) (*Future, error) {
if atomic.LoadInt32(&c.closed) == 1 {
return nil, errors.New("connection closed")
}
seq := c.nextSeq()
msg.Header.RequestID = seq
future := NewFuture()
c.pending.Store(seq, future)
c.writeMu.Lock()
err := c.conn.Write(msg)
c.writeMu.Unlock()
if err != nil {
c.pending.Delete(seq)
c.fail(err) // 关键:write 失败也要彻底杀死连接(解决之前连接bug)
return nil, err
}
return future, nil
}
开启协程readloop接收响应。
Go
func (c *TCPClient) readLoop() {
for {
msg, err := c.conn.Read()
if err != nil {
c.fail(err)
return
}
seq := msg.Header.RequestID
val, ok := c.pending.LoadAndDelete(seq)
if !ok {
continue
}
future := val.(*Future)
if msg.Header.Error != "" {
future.Done(nil, errors.New(msg.Header.Error))
} else {
future.Done(msg.Body, nil)
}
}
}
粘包/半包边界处理
拿到头部和身体部分长度后拼出一个packet的总长,然后把一个packet总体返回。
Go
func (pb *PacketBuffer) Read() []byte {
pb.lock.Lock()
defer pb.lock.Unlock()
// 最小包头长度校验
if len(pb.buf) < 10 {
return nil
}
headerLen := int(protocol.DecodeHeaderLen(pb.buf[2:6]))
bodyLen := int(protocol.DecodeBodyLen(pb.buf[6:10]))
totalLen := 10 + headerLen + bodyLen
if len(pb.buf) < totalLen {
return nil
}
packet := make([]byte, totalLen)
copy(packet, pb.buf[:totalLen])
// 移动窗口
pb.buf = pb.buf[totalLen:]
return packet
}