前言
背景
最近决心开始学习 go 语言, 但是苦于没有实际的应用场景, 学习始终停留在 hello world 层面, 看过的教程和资料印象也不深刻. 于是决定从 go 自带的 rpc 实现开始切入, 了解一下 go 语言在实际场景下是如何使用的, 包括异常处理, 代理和过滤, go routine 的用法等等, 同时也简单了解了一下其他 rpc 的 go 语言实现, 比如 thrift 和 grpc 等等. 一阵走马观花, 稍微加深了印象, 也开始慢慢体会到 go 语言和 java 语言的种种差异和共性. 接下来, 为了进一步巩固学习效果, 也算是为了对自己目前为止的职业生涯做一次复习和汇报, 决定使用 go 语言从零开始构建一个比较完整的 RPC(或者说是微服务)框架.
微服务框架和 RPC 框架
本文中提到 RPC 框架, 指的是提供基础的 RPC 调用支持的框架; 而本文中提到的微服务框架, 指的是包含一些服务治理相关的功能 (比如服务注册发现, 负载均衡, 链路追踪等) 的 RPC 框架.
调研
在动手开始做之前, 需要先了解学习一下其他现有的产品, 可以从中学习一下优秀的经验和方法, 这里列举一下初步了解到的几个框架,:
https://grpc.io/ google 推出的微服务框架, 支持 10 种语言, 支持基于 http2 的双向的流式通讯
https://github.com/micro/go-micro 一个开源的微服务框架, 比较独特的是支持 Async Messaging, 像是 mq 一样的 subpub 功能
thrift-go https://thrift.apache.org/lib/go thrift 是 Facebook 捐献给 apache 的 rpc 框架(不包含服务治理相关的功能), 根据官方文档, thrift 支持 20 种语言根据官方文档的 RPC 调用
http://rpcx.site rpcx 是一个国人开发并开源的微服务框架, 宣传的特性是 "快, 易用却功能强大", 官网上的介绍提到性能是 grpc 的两倍. 这里附上作者 (应该是) 的博客 https://colobu.com/#
以上几个就是目前了解过的几个已有的框架, 比较惭愧的是了解地都不够深入, 后续还会持续学习.
Pluggable Interfaces
值得一提的是除了 thrift, 其他三个称得上微服务框架的产品, 其特性都包含 Pluggable Interfaces, 也就是可以通过插件替换部分功能. 通过插件实现可替换的功能, 实际上在一个微服务框架中基本是最低要求了, 否则后续的功能扩展将会变得十分困难, 相信我, 这里是饱含血泪的经验之谈.
需求分析
在开始着手设计甚至是编写代码以前, 我们首先分析一下我们的需求(来自学习软件工程中的成果). 同时对于一部分可能不太熟悉 RPC 相关细节的同学来说, 对我们后面要做的事情心中也能够有一个大致的概念. 这里就直接列举几个功能性需求:
支持 RPC 调用, 包括同步调用和异步调用
支持服务治理的相关功能, 包括:
服务注册与发现
服务负载均衡
限流和熔断
身份认证
监控和链路追踪
健康检查, 包括端到端的心跳以及注册中心对服务实例的检查
支持插件, 对于有多种实现的功能(比如负载均衡), 需要以插件的形式提供实现, 同时需要支持自定义插件 至于非功能性需求比如性能要好, 要够稳定这类的暂时不重点关注.
系统设计
分层
有了大致的需求, 接下来就可以开始着手设计了. 首先我们将框架划分为若干层, 层与层之间约定通过接口交互. 这里就不要问为什么需要分层了, 非要问就是经验. 分层作为一种经典到不能在经典的设计模式, 几乎在软件开发过程中无处不在, 在 RPC 框架当中也十分适用, 下面画出大致的层次图:
service 是面向用户的接口, 比如客户端和服务端实例的初始化和运行等等
client 和 server 表示客户端和服务端的实例, 它们负责发出请求和返回响应
select 表示负载均衡, 或者叫做 loadbanlancer, 它负责决定具体要向哪个 server 发出请求
registery 表示注册中心, server 在初始化完毕甚至是运行时都要向注册中心注册自身的相关信息, 这样 client 才能从注册中心查找到需要的 server
codec 表示编解码, 也就是将对象和二进制数据互相转换
protocol 表示通信协议, 也就是二进制数据是如何组成的, RPC 框架中很多功能都需要协议层的支持
transport 表示通讯, 它负责具体的网络通讯, 将按照 protocol 组装好的二进制数据通过网络发送出去, 并从网络读取数据然后交给 protocol 进行解析
上面提到的各个层, 除了 servce,client 和 server, 实际上可以提供多种实现, 所以应该都以 plugin 的方式实现.
这样一来, 一个客户端从发出请求到收到响应的流程大概就是这样:
服务端的逻辑比较类似, 这里就不画图了.
过滤器链
通过上面的层次划分可以看到, 一个请求或者响应实际上会依次穿过各个层然后通过网络发送或者到达用户逻辑, 所以我们采用类似过滤器链一样的方式处理请求和响应, 以此来达到对扩展开放, 对修改关闭的效果. 这样一来对于一些附加功能比如熔断降级和限流, 身份认证等功能都可以在过滤器中实现.
消息协议
接下来设计具体的消息协议, 所谓消息协议大概就是两台计算机为了互相通信而做的约定. 举个例子, TCP 协议约定了一个 TCP 数据包的具体格式, 比如前 2 个 byte 表示源端口, 第 3 和第 4 个 byte 表示目标端口, 接下来是序号和确认序号. 而在我们的 RPC 框架中, 也需要定义自己的协议. 一般来说, 网络协议都分为 head 和 body 部分, head 是一些元数据, 是协议自身需要的数据, body 则是上一层传递来的数据, 只需要原封不动的接着传递下去就是了.
接下来我们就试着定义自己的协议:
- -------------------------------------------------------------------------------------------------
- |2byte|1byte |4byte |4byte | header length |(total length - header length - 4byte)|
- -------------------------------------------------------------------------------------------------
- |magic|version|total length|header length| header | body |
- -------------------------------------------------------------------------------------------------
根据上面的协议, 一个消息体由以下几个部分严格按照顺序组成:
两个 byte 的 magic number 开头, 这样一来我们就可以快速的识别出非法的请求
一个 byte 表示协议的版本, 目前可以一律设置为 0
4 个 byte 表示消息体剩余部分的总长度(total length)
2 个 byte 表示消息头的长度(header length)
消息头 (header), 其长度根据前面解析出的长度(header length) 决定
消息体(body), 其长度为前面解析出的总长度减去消息头所占的长度(total length - 4 - header length)
协议中消息头的数据主要是 RPC 调用过程中的元数据, 元数据跟方法参数和响应无关, 主要记录额外的信息以及实现附属功能比如链路追踪, 身份认证等等; 消息体的数据则是由实际的请求参数或者响应编码而来. 在实际的处理中, 消息头在发送端是一个结构体, 在发送时会被编码成二进制添加在消息头的前面, 在接收端接收时有解码成一个结构体. 这里列举消息头包含的各个信息:
- type Header struct {
- Seq int // 序号, 用来唯一标识请求或响应
- MessageType byte // 消息类型, 用来标识一个消息是请求还是响应
- CompressType byte // 压缩类型, 用来标识一个消息的压缩方式
- StatusCode byte // 状态类型, 用来标识一个请求是正常还是异常
- ServiceName string // 服务名
- MethodName string // 方法名
- MetaData map[string]string // 其他元数据
- }
结语
第一篇文章就到此为止了, 主要先做一下准备, 整理一下思路, 如果有不正确或者不合理的部分还请大家多多指教.
来源: https://juejin.im/post/5c7b9967518825470368d8d4