背景

最近在研究service mesh相关的实现, 除了知名的 envoy-proxy(c++写的), go写的知名mesh就是 mosn, 在饿了么, 也实现了mesh的产品: samaritan

设计

整体设计

samaritan 本质上是一个proxy, 通过 unix domain socket 和应用进程交互. samaritan-proxy 的设计有以下几个重要模块:

  1. instance: 一个samaritan-proxy就是一个实例
  2. controller: 负责配置信息的变更和处理, 配置变更会启动一个proc
  3. Proc: 服务名绑定的 流量处理和转发抽象, 每一个 procName 对应一个Proc. 比如 redis的实现 就是一个 Proc. Proc 独立监听 网络端口和网络包处理
  4. admin: 运维管理接口, 比如 获取配置、stats统计、pprof. admin端口是重用的.
  5. config: 动态配置实现, 获取动态配置 放到 event channel, 订阅者监听 event channel 处理配置变更

需要注意的是, proc 的协议相关实现 需要实现注册到 controller#procs 的map中, 注册目前依赖 Bootstrap#StaticServices 的配置信息.

主流程

先关注主流程, 协议启动:

Controller#handleEvent -> handleSvcAdd -> #tryEnsureProc -> newProc(controller.go) -> proc#new: 根据协议获取相应的 builder, 实例化 后端服务, 比如 newRedisProc(redis.go)

每个协议的实现, 会根据config#Listener获取监听的地址, Start 的时候 会启动两个goroutine,

  • listener的监听和读写操作, 这一块是抽象出去的, 所有协议的 listener 实现是一个(uds), 连接的处理 是 实例化listener 注册进去的 (connHandleFn), redis 连接的操作 是 session 机制的, 每个 connection 是一个 session, 连接的读写是全双工的, 读是一个goroutine, 写是一个goroutine. 利用了 unix socket 的全双工操作. 在消息的处理上, 每个消息就是一个 cmd, 每个 cmd 对应一个handler实现, 比如: mset mget eval scan hotkey ping quit info time select, 显然没有pipeline, 为什么没有 pipeline 呢? 同时redis 也会实例一个goroutine 处理 upstream 的操作(redis refresh slot), 这样 redis proxy 就可以下线了.

问题

  • 这里就存在一个问题了, 每一种协议的实现 监听端口的是不一样的吗?

是的, 每个proc 根据config#Listener 信息独立监听 unix domain socket 地址, 彼此之间隔离.

  • 协议复用

每个proc的实现都需要实现自己的 连接处理栈, 不能利用相同协议的服务的实现,比如: 网络连接管理、session管理. 按照配置上的解耦实现来看, 应该是可以规划下的, 比如 redis 协议上不同服务的实现. 可以把 handleRequest 抽象出去.

不错的实现

  1. redis proc 在unix domain socket实现了全双工模型, 每一个连接都是 读写并行的.

  2. 支持热重启, 但是什么情况下需要热重启呢?

  3. 热重启&端口复用

proxy 在升级的时候, 我们希望尽可能的不丢失用户的连接, 在tcp中可以通过 REUSE_PORT 实现, 比如一些 mysql proxy. 在 unix domain 中, 只能通过 sendmessage 和 receivemessage 实现 fd 在新老proxy之间传递, 实现端口上连接的复用, 在fd迁移完成后, 会kill 老的proxy. 这种机制就是热重启/端口复用, 整体的实现在 hotrestart模块 + proxy instance#New + #Run 方法中, 会有以下以下步骤:

  • 启动hotreload模块 (监听新的unix domain socket)
  • 启动controller (根据动态配置的信息, 会启动相应的proc监听 unix domain socket, 这个时候有多个instance 监听同一个socket)
  • 通过 unix domain socket 向之前的proxy 发送暂停admin通知. 老的proxy关闭admin的端口监听
  • 启动本地的admin模块
  • 通过 unix domain socket 向之前的proxy 发送fd迁移通知, 老的proxy循环所有的proc 关闭监听
  • 通过 unix domain socket 向之前的proxy 发送关闭通知. 老的proxy系统调用kill自杀

那么怎么知道老的proxy的端口呢?

在 hack/hot-restart.py 中, 启动的时候, 会将proxy的地址放到环境变量中: __Samaritan_Parent__