web/cache/rpc框架梳理

一. gee-web

1. 实现目标

Gee Web 是一个极简的 Go 语言 Web 框架,设计目标是为开发者提供一个简单、高效且易于扩展的 Web 框架。它通过 Go 的内置并发特性(goroutine)、接口和反射等机制,实现了基本的路由、请求处理、分组、中间件等功能,帮助开发者快速构建 Web 应用程序。

2. 总的框架结构

Gee Web 的核心包括几个重要部分:路由管理、路由分组、中间件支持、请求上下文管理、静态文件服务、HTTP 错误处理等

  • EngineGee Web 的核心结构体,负责启动 HTTP 服务器,管理路由、分组和中间件。Engine 通过嵌入 RouterGroup 实现了路由分组管理的功能,并持有 routergroups 这些核心属性。

  • Router:实现了路由注册和路由分发。将不同的 HTTP 方法和路径映射到处理函数 (HandlerFunc),并在请求到达时,查找对应的处理函数进行执行。

  • RouterGroup:用于路由的分组管理,可以通过设置不同的路径前缀或中间件,构建层次化的 API 结构。

  • Context:对每个 HTTP 请求的封装,包含请求 (http.Request) 和响应 (http.ResponseWriter) 的处理逻辑。它提供了便捷的方法来处理请求参数、响应输出、设置状态码、管理中间件控制流等。

  • 中间件Gee Web 支持在路由处理前后执行中间件逻辑。通过在 RouterGroup 级别和全局级别注册中间件,可以灵活地控制请求的处理过程。

  • 静态文件服务:通过 http.StripPrefixhttp.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.Requesthttp.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 支持通过 deferrecover 捕获 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.StripPrefixhttp.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.HandlerHandlerFunc)进行路由和中间件解耦,确保了灵活性和扩展性。

  • defer 和 recover:利用 deferrecover 机制处理异常,保证了服务器在发生 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 路径获取 groupkey,查询缓存并返回结果。
    • 如果缓存未命中,通过回调函数从外部数据源获取并更新缓存。
  • 示例代码:
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 传输时的文本数据格式,将数据结构(RequestResponse)序列化为紧凑的二进制格式,减少带宽占用,提升通信效率。
    • 请求体包括 groupkey,响应为字节数组 value,节省了传输开销。

总结

该项目通过以下几个核心组件构建了一个高效的分布式缓存系统:

  1. 使用 LRU 缓存管理最近最常使用的数据。
  2. 通过 Group 管理不同的缓存实例,并提供缓存未命中的回调机制。
  3. 使用 HTTP 和一致性哈希实现了多节点缓存查询的分布式架构。
  4. 通过 singleflight 防止缓存击穿:防止多个并发请求在缓存未命中的情况下重复查询同一数据。singleflight 机制确保只有一个请求访问数据库,其他请求等待第一个请求的结果返回,从而避免对数据库的高并发压力。
  5. 一致性哈希:通过一致性哈希算法分布缓存请求,确保数据的均衡分布和节点扩展的平滑性。每个节点根据哈希值选择处理特定的缓存键值,实现负载均衡。
  6. 并发控制:缓存系统中的每个缓存(cache)和 group 都采用了互斥锁(sync.Mutex)来控制并发访问,确保在多线程环境下的安全性。
  7. 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 中定义了 methodTypeservice 结构体,封装了服务的方法信息,通过反射找到符合 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. 客户端与服务端的通信

通过 ClientServer 实现客户端和服务端的通信逻辑。客户端发起远程调用,服务端处理并返回结果。

  • 服务端接受请求:
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 服务
    • 并发控制:支持多线程并发请求,客户端支持异步调用
    • 负载均衡:实现了随机选择和轮询调度的负载均衡策略
    • 超时控制:客户端和服务端支持超时设置
    • 服务发现与注册中心:实现了简单的服务注册和服务发现
posted @ 2024-10-04 17:28  失控D大白兔  阅读(7)  评论(0编辑  收藏  举报