web/cache/rpc框架梳理
一. gee-web
1. 实现目标
Gee Web
是一个极简的 Go 语言 Web 框架,设计目标是为开发者提供一个简单、高效且易于扩展的 Web 框架。它通过 Go 的内置并发特性(goroutine)、接口和反射等机制,实现了基本的路由、请求处理、分组、中间件等功能,帮助开发者快速构建 Web 应用程序。
2. 总的框架结构
Gee Web
的核心包括几个重要部分:路由管理、路由分组、中间件支持、请求上下文管理、静态文件服务、HTTP 错误处理等。
-
Engine:
Gee Web
的核心结构体,负责启动 HTTP 服务器,管理路由、分组和中间件。Engine
通过嵌入RouterGroup
实现了路由分组管理的功能,并持有router
和groups
这些核心属性。 -
Router:实现了路由注册和路由分发。将不同的 HTTP 方法和路径映射到处理函数 (
HandlerFunc
),并在请求到达时,查找对应的处理函数进行执行。 -
RouterGroup:用于路由的分组管理,可以通过设置不同的路径前缀或中间件,构建层次化的 API 结构。
-
Context:对每个 HTTP 请求的封装,包含请求 (
http.Request
) 和响应 (http.ResponseWriter
) 的处理逻辑。它提供了便捷的方法来处理请求参数、响应输出、设置状态码、管理中间件控制流等。 -
中间件:
Gee Web
支持在路由处理前后执行中间件逻辑。通过在RouterGroup
级别和全局级别注册中间件,可以灵活地控制请求的处理过程。 -
静态文件服务:通过
http.StripPrefix
和http.FileServer
实现了静态文件的便捷处理。
3. 框架的关键设计细节和精巧之处
(1)路由分组设计
Gee Web
通过 RouterGroup
实现了路由的分组管理,这种设计使得开发者可以将具有相同前缀的 API 进行分组,例如 /api/v1
或 /api/v2
。每个分组共享相同的 Engine
实例,并可以在分组中注册不同的中间件。
// Example: 创建新的路由组
group := engine.Group("/v2")
group.GET("/users", usersHandler)
这种分组机制的核心设计是通过 RouterGroup
的嵌套实现的,保证了子路由能够继承父路由的前缀和中间件。
(2)中间件机制
Gee Web
中间件的执行顺序是链式调用的,类似于责任链模式。每个中间件通过 Next()
调用下一个中间件或路由处理函数。这样可以灵活地控制中间件的执行顺序,在请求进入或离开时,添加特定逻辑(如日志记录、认证、跨域控制等)。
func (c *Context) Next() {
c.index++
for ; c.index < len(c.middlewares); c.index++ {
c.middlewares[c.index](c)
}
}
(3)Context 对象封装
Gee Web
通过 Context
将 HTTP 请求和响应封装成一个对象,简化了 http.Request
和 http.ResponseWriter
的处理。同时,Context
支持链式调用和参数解析,如 JSON 响应、设置响应头、状态码等。这样设计大大提高了代码的可读性和可维护性。
func (c *Context) JSON(code int, obj interface{}) {
c.SetHeader("Content-Type", "application/json")
c.Status(code)
encoder := json.NewEncoder(c.Writer)
encoder.Encode(obj)
}
(4)错误恢复机制
Gee Web
支持通过 defer
和 recover
捕获 panic
,防止服务器因为请求处理中的错误而崩溃。在遇到不可恢复的错误时,能够优雅地处理并返回 500 错误,而不会让服务器宕机。
defer func() {
if err := recover(); err != nil {
log.Printf("Recovered from panic: %v", err)
c.Fail(http.StatusInternalServerError, "Internal Server Error")
}
}()
(5)静态文件处理
通过 http.StripPrefix
和 http.FileServer
实现了对静态文件的便捷处理。框架中创建了一个静态文件处理器,将文件路径映射到实际的服务器文件系统中。
func (group *RouterGroup) Static(relativePath string, root string) {
handler := group.createStaticHandler(relativePath, http.Dir(root))
urlPattern := path.Join(relativePath, "/*filepath")
group.GET(urlPattern, handler)
}
(6)链式路由注册
Gee Web
的路由注册通过链式调用,使得 API 注册非常简洁和流畅。比如可以通过 .GET()
、.POST()
等方法在路由分组中添加路由规则。
group := engine.Group("/api")
group.GET("/user", userHandler)
group.POST("/login", loginHandler)
4. 使用的 Go 特性
-
goroutine 并发处理:每个请求由一个独立的 goroutine 处理,充分利用 Go 的并发特性,提升了并发处理的能力。
-
接口与多态:大量使用接口(如
http.Handler
、HandlerFunc
)进行路由和中间件解耦,确保了灵活性和扩展性。 -
defer 和 recover:利用
defer
和recover
机制处理异常,保证了服务器在发生panic
时不会崩溃,提供了基本的错误恢复机制。 -
反射机制:通过 Go 的反射机制,可以对请求参数和响应进行灵活的处理,简化了数据传输和解析。
-
闭包与链式调用:通过闭包实现了中间件的链式处理,每个中间件的执行可以通过
Next()
来控制,保证了灵活的控制流。
6. 总结
- 从主函数流程来看,首先是通过engine实现处理上下文的ServeHTTP接口,方便传入http.ListenAndServe进行触发和并发运行业务处理逻辑,
- 触发响应时新建一个上下文,该上下文记录地址,方法、状态码、匹配地址、所属engine、微服务、当前微服务下标,该上下文会根据请求报文地址,
- 首先扫描所有分组,根据前缀获取微服务,然后engine处理该上下文,实际上就是根据前缀路由树,找到匹配的节点,获取对应哈希映射的函数,接着就是采用链式执行。
- engine是一个特殊的分组,通过匿名嵌套实现,建立分组实例其实就是分组存储对应前缀,来区分分组权限,每个分组可以存储注册的微服务,
- 然后路由前缀树叶子节点存储映射方法,每个engine只有一个路由树,也就是分组的方法注册和匹配也是在该路由树上进行的划分,所有分组共享一个engine,默认对所有方法注册日志微服务和错误恢复追溯微服务
- 访问静态资源通过注册http提供的静态文件服务器映射处理函数,通过返回函数闭包该映射服务器
二. gee-cache
1. LRU 缓存的基本实现
- 数据结构:
使用 Go 的list
实现了一个双向链表,用于 LRU 缓存淘汰策略。cache
结构体封装了这个链表,并且用map
来实现键到链表节点的快速映射。- 核心字段:
maxBytes
: 最大的缓存容量(字节数)。nbytes
: 当前缓存已使用的字节数。ll
: 一个双向链表(list.List
),用来记录访问顺序。cache
: 一个映射表(map
),用于快速找到缓存的节点。
- 操作方法:
Add(key, value)
:添加数据到缓存,更新顺序,如果超出容量,则移除最早未使用的节点。Get(key)
:查找并返回缓存的值,并将该节点移到链表头部,表示最近使用。RemoveOldest()
:删除最久未使用的数据。- 示例代码:
if ele, ok := c.cache[key]; ok { c.ll.MoveToFront(ele) kv := ele.Value.(*entry) c.nbytes += int64(value.Len()) - int64(kv.value.Len()) kv.value = value } else { ele := c.ll.PushFront(&entry{key, value}) c.cache[key] = ele c.nbytes += int64(len(key)) + int64(value.Len()) }
- 核心字段:
2. 并发访问与只读数据封装
- 只读封装:
创建ByteView
结构体用于封装缓存数据,数据只读,避免外部修改缓存中的数据。ByteView
提供了Len()
方法来获取数据的字节大小。ByteSlice()
方法返回数据的副本,避免直接修改缓存中的数据。
- 并发控制:
为cache
添加sync.Mutex
互斥锁,保证并发访问缓存的安全性,防止数据竞争。
3. Group 管理与回调机制
- Group 结构:
Group
结构管理多个缓存,每个缓存属于一个Group
,用于组织和控制不同的缓存逻辑。getter
: 回调函数,用于在缓存未命中时从其他数据源获取数据。- 注册外部提供的回调函数,通过
GetterFunc
实现函数式接口,动态查找数据并更新到缓存。
- 示例代码:
type Group struct {
name string
getter Getter
mainCache cache
}
func (g *Group) Get(key string) (ByteView, error) {
if v, ok := g.mainCache.get(key); ok {
return v, nil
}
return g.load(key)
}
func (g *Group) load(key string) (ByteView, error) {
bytes, err := g.getter.Get(key)
if err != nil {
return ByteView{}, err
}
value := ByteView{b: cloneBytes(bytes)}
g.populateCache(key, value)
return value, nil
}
4. HTTP 服务
- 缓存查询服务:
实现ServeHTTP
方法,处理 HTTP 请求,从指定的缓存Group
中查询数据并返回结果。- 通过 URL 路径获取
group
和key
,查询缓存并返回结果。 - 如果缓存未命中,通过回调函数从外部数据源获取并更新缓存。
- 通过 URL 路径获取
- 示例代码:
func (p *HTTPPool) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.URL.Path, p.basePath) {
http.Error(w, "unexpected path", http.StatusBadRequest)
return
}
parts := strings.SplitN(r.URL.Path[len(p.basePath):], "/", 2)
if len(parts) != 2 {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
groupName, key := parts[0], parts[1]
group := GetGroup(groupName)
if group == nil {
http.Error(w, "no such group", http.StatusNotFound)
return
}
view, err := group.Get(key)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Write(view.ByteSlice())
}
5. 一致性哈希
- 一致性哈希结构:
实现了将节点地址映射到多个虚拟节点(副本)的机制,通过哈希环来实现分布式缓存中节点的负载均衡。- 每个节点注册时,会生成多个哈希副本,存储在哈希环上。
- 当请求到来时,根据
key
进行哈希映射,找到对应的缓存节点处理请求。
- 代码示例:
func (m *Map) Add(keys ...string) {
for _, key := range keys {
for i := 0; i < m.replicas; i++ {
hash := int(m.hash([]byte(strconv.Itoa(i) + key)))
m.keys = append(m.keys, hash)
m.hashMap[hash] = key
}
}
sort.Ints(m.keys)
}
func (m *Map) Get(key string) string {
hash := int(m.hash([]byte(key)))
idx := sort.Search(len(m.keys), func(i int) bool {
return m.keys[i] >= hash
})
return m.hashMap[m.keys[idx%len(m.keys)]]
}
6. 分布式节点互访与缓存一致性
- 分布式节点访问:
每个节点都有一个httpGetter
结构体用于向其他节点请求数据。- 节点通过一致性哈希机制根据
key
找到对应的节点,并向该节点发起 HTTP 请求获取缓存数据。 - 如果请求的节点是本地节点,直接从缓存中查询或通过回调更新缓存。
- 节点通过一致性哈希机制根据
- 示例代码:
func (h *httpGetter) Get(in *pb.Request, out *pb.Response) error {
u := fmt.Sprintf(
"%v%v/%v",
h.baseURL,
url.QueryEscape(in.GetGroup()),
url.QueryEscape(in.GetKey()),
)
res, err := http.Get(u)
if err != nil {
return err
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return err
}
out.Value = body
return nil
}
7. SingleFlight 防止缓存击穿
- 请求合并机制:
使用singleflight
包防止缓存击穿。当多个请求同时查询同一key
时,合并这些请求,确保只有一个请求实际访问数据源,其他请求等待结果。- 实现通过
sync.WaitGroup
来控制并发请求的阻塞与唤醒。 call
结构体保存请求的执行结果,后续的请求通过Wait()
等待第一个请求完成并返回结果。
- 实现通过
- 示例代码:
func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
g.mu.Lock()
if c, ok := g.m[key]; ok {
g.mu.Unlock()
c.wg.Wait() // 等待已存在的请求完成
return c.val, c.err
}
c := new(call)
c.wg.Add(1)
g.m[key] = c
g.mu.Unlock()
c.val, c.err = fn() // 执行实际的请求
c.wg.Done()
g.mu.Lock()
delete(g.m, key)
g.mu.Unlock()
return c.val, c.err
}
8. Protobuf 优化
- 通信效率优化:
使用 Protobuf 代替传统 HTTP 传输时的文本数据格式,将数据结构(Request
和Response
)序列化为紧凑的二进制格式,减少带宽占用,提升通信效率。- 请求体包括
group
和key
,响应为字节数组value
,节省了传输开销。
- 请求体包括
总结
该项目通过以下几个核心组件构建了一个高效的分布式缓存系统:
- 使用 LRU 缓存管理最近最常使用的数据。
- 通过
Group
管理不同的缓存实例,并提供缓存未命中的回调机制。 - 使用 HTTP 和一致性哈希实现了多节点缓存查询的分布式架构。
- 通过 singleflight 防止缓存击穿:防止多个并发请求在缓存未命中的情况下重复查询同一数据。singleflight 机制确保只有一个请求访问数据库,其他请求等待第一个请求的结果返回,从而避免对数据库的高并发压力。
- 一致性哈希:通过一致性哈希算法分布缓存请求,确保数据的均衡分布和节点扩展的平滑性。每个节点根据哈希值选择处理特定的缓存键值,实现负载均衡。
- 并发控制:缓存系统中的每个缓存(cache)和 group 都采用了互斥锁(sync.Mutex)来控制并发访问,确保在多线程环境下的安全性。
- Protobuf 优化通信:通过使用 Protobuf 进行数据的序列化和反序列化,减少了节点间的通信开销。相比于传统的 HTTP 文本格式,Protobuf 提供了更高效的二进制数据传输,节省了带宽并提高了传输速度。
三. gee-rpc
1. 消息的编解码(Codec)
GeeRPC 实现了自定义的消息编解码器,用来序列化和反序列化 RPC 请求和响应。为了支持不同的序列化方式,定义了 Codec
接口,并提供了基于 Gob 的默认实现。
type Header struct {
ServiceMethod string // format "Service.Method"
Seq uint64 // sequence number chosen by client
Error string // error message, if any
}
type Codec interface {
io.Closer
ReadHeader(*Header) error
ReadBody(interface{}) error
Write(*Header, interface{}) error
}
GeeRPC 支持通过自定义的 Codec
实现不同的序列化方式,如 Gob 或 JSON。
- Gob 编码器实现:
type GobCodec struct {
conn io.ReadWriteCloser
buf *bufio.Writer
dec *gob.Decoder
enc *gob.Encoder
}
func NewGobCodec(conn io.ReadWriteCloser) Codec {
buf := bufio.NewWriter(conn)
return &GobCodec{
conn: conn,
buf: buf,
dec: gob.NewDecoder(conn),
enc: gob.NewEncoder(buf),
}
}
2. 服务注册(Service Registration)
通过反射将结构体中的方法注册为服务,支持动态调用服务。service.go
中定义了 methodType
和 service
结构体,封装了服务的方法信息,通过反射找到符合 RPC 规范的方法,并将其注册为可调用服务。
type service struct {
name string
typ reflect.Type
rcvr reflect.Value
method map[string]*methodType
}
func newService(rcvr interface{}) *service {
s := new(service)
s.rcvr = reflect.ValueOf(rcvr)
s.name = reflect.Indirect(s.rcvr).Type().Name()
s.typ = reflect.TypeOf(rcvr)
s.registerMethods()
return s
}
- 反射方法注册:
func (s *service) registerMethods() {
for i := 0; i < s.typ.NumMethod(); i++ {
method := s.typ.Method(i)
mType := method.Type
// 确认方法的入参和返回值符合 RPC 要求
if mType.NumIn() == 3 && mType.NumOut() == 1 && mType.Out(0) == reflect.TypeOf((*error)(nil)).Elem() {
s.method[method.Name] = &methodType{
method: method,
ArgType: mType.In(1),
ReplyType: mType.In(2),
}
}
}
}
3. 客户端与服务端的通信
通过 Client
和 Server
实现客户端和服务端的通信逻辑。客户端发起远程调用,服务端处理并返回结果。
- 服务端接受请求:
func (server *Server) Accept(lis net.Listener) {
for {
conn, err := lis.Accept()
if err != nil {
log.Println("rpc server: accept error:", err)
return
}
go server.ServeConn(conn)
}
}
func (server *Server) ServeConn(conn io.ReadWriteCloser) {
defer conn.Close()
var opt Option
// 首先解码Option
if err := json.NewDecoder(conn).Decode(&opt); err != nil {
log.Println("rpc server: options error: ", err)
return
}
// 根据 Option 的 CodecType,选择相应的解码器
f := codec.NewCodecFuncMap[opt.CodecType]
server.serveCodec(f(conn))
}
- 客户端发起调用:
func (client *Client) Call(serviceMethod string, args, reply interface{}) error {
call := client.Go(serviceMethod, args, reply, make(chan *Call, 1))
return <-call.Done
}
func (client *Client) Go(serviceMethod string, args, reply interface{}, done chan *Call) *Call {
call := &Call{
ServiceMethod: serviceMethod,
Args: args,
Reply: reply,
Done: done,
}
client.send(call)
return call
}
4. 并发与异步调用
支持并发与异步调用,客户端使用 Go
方法进行异步调用,并提供 Done
信道通知调用完成。
func (client *Client) Go(serviceMethod string, args, reply interface{}, done chan *Call) *Call {
if done == nil {
done = make(chan *Call, 10)
}
call := &Call{
ServiceMethod: serviceMethod,
Args: args,
Reply: reply,
Done: done,
}
client.send(call)
return call
}
5. 服务发现与负载均衡
实现了多种负载均衡策略,包括随机选择和轮询算法。支持服务发现机制,客户端根据策略从多个服务实例中选择一个进行调用。
- 服务发现接口:
type Discovery interface {
Refresh() error
Update(servers []string) error
Get(mode SelectMode) (string, error)
GetAll() ([]string, error)
}
- 轮询调度算法:
func (d *MultiServersDiscovery) Get(mode SelectMode) (string, error) {
d.mu.Lock()
defer d.mu.Unlock()
n := len(d.servers)
switch mode {
case RandomSelect:
return d.servers[d.r.Intn(n)], nil
case RoundRobinSelect:
s := d.servers[d.index%n]
d.index = (d.index + 1) % n
return s, nil
default:
return "", errors.New("rpc discovery: not supported select mode")
}
}
6. 超时控制
支持超时控制,避免客户端或服务端因网络问题导致的挂死。
- 客户端超时控制:
func (client *Client) Call(ctx context.Context, serviceMethod string, args, reply interface{}) error {
call := client.Go(serviceMethod, args, reply, make(chan *Call, 1))
select {
case <-ctx.Done():
return errors.New("rpc client: call failed: " + ctx.Err().Error())
case call := <-call.Done:
return call.Error
}
}
7. 注册中心
实现了简易的注册中心,服务端在启动时向注册中心注册,客户端可以从注册中心获取可用的服务列表。
- 注册服务:
func (r *GeeRegistry) putServer(addr string) {
r.mu.Lock()
defer r.mu.Unlock()
s := r.servers[addr]
if s == nil {
r.servers[addr] = &ServerItem{Addr: addr, start: time.Now()}
} else {
s.start = time.Now()
}
}
- 注册中心响应客户端请求:
func (r *GeeRegistry) ServeHTTP(w http.ResponseWriter, req *http.Request) {
switch req.Method {
case "GET":
w.Header().Set("X-Geerpc-Servers", strings.Join(r.aliveServers(), ","))
case "POST":
addr := req.Header.Get("X-Geerpc-Server")
r.putServer(addr)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
总结:
GeeRPC 框架通过设计灵活的模块,逐步实现了消息编解码、服务注册、客户端与服务端的通信、并发与异步调用、负载均衡、超时控制和注册中心等功能。每个功能模块既可以独立使用,也可以组合在一起实现分布式调用框架。
- 技术点总结:
- 编解码:实现了自定义 Codec(Gob、JSON)
- 反射与动态服务注册:通过反射机制注册结构体的方法为 RPC 服务
- 并发控制:支持多线程并发请求,客户端支持异步调用
- 负载均衡:实现了随机选择和轮询调度的负载均衡策略
- 超时控制:客户端和服务端支持超时设置
- 服务发现与注册中心:实现了简单的服务注册和服务发现