框架分层设计
依赖于 OSI 七层模型或者 TCP/IP 四层协议,将框架按照层级来实现,注意每一层功能的专注性以及层与层之间的拓展性和复用性,可以简单分为应用层、中间件层、路由层、协议层、网络层以及通用组件
应用层设计要点
①提供合理的 API,API的命名需要具有可理解性以及简单性,可以通过简洁 API 看出操作意图,避免模糊和冗余,不要试图在文档中说明 API,很多用户并不特别关注文档
中间件设计要点
①需要配合应用层的 handler 实现一个完整的请求处理生命周期,拥有预处理和后处理的逻辑,同时要对应用层模块、用户逻辑模块实现复用,而且能够注册多个中间件,常见的洋葱模型就是一个很好的示范,可以适用于日志记录、事务处理、性能统计等等场景
举例说明,打印每个请求的 request 和 response
func main() {
h := server.new()
h.POST("/login", func(c context.Context, ctx *app.RequestContext){
// print request
logs.Infof("request is %s", **)
// 一些处理逻辑
ctx.json(200, "OK")
// print response
logs.Infof("response is %s", **)
})
}
// 思路1,将打印 request 和 response 作为预处理和后处理,实际相当于调用函数
// 因此可以使用中间件包括要处理的逻辑
func Middleware(some param) {
// some pre-process
nextMiddleware()
// some post-process
}
// 思路2,路由上可以注册多个中间件,同时也满足?请求级别有效?
// 只需要将中间件设计为和业务以及 Handler 相同
func Middleware(some program) {
// some pre-process
Next()
// some post-process
}
// 思路3,用户不主动调用下一个处理函数应该怎么处理
// 核心:保证任何场景下的 index 是递增的?为什么?
func (ctx *RequestContext) Next() {
ctx.index++
for ctx.index < int8(len(ctx.handlers)) {
ctx.handlers[ctx.index]()
ctx.index++
}
}
// 思路4,出现异常想要停止应该怎么处理
// 将 index 设置为最大值,可以保证
func (ctx *RequestContext) Abort() {
ctx.index = IndexMax
}
// 思路5,调用链的问题,调不调用Next会导致调用信息可能出现不在一个调用栈上
路由层设计要点
本质就是为 URL 匹配对应的处理函数 Handlers,常见的问题包括:静态路由、参数路由、路由修复、冲突路由及优先级确定、匹配 HTTP 方法,多处理函数(为了方便添加中间件)
路由匹配的常见思路:①通过map,比如 map[string] handlers,这样的方式适合静态路由,但是复杂场景比如参数路由,路由修复场景就很麻烦,②通过前缀匹配树,可以处理参数路由
匹配 HTTP 方法的常见思路:通过建立路由映射表 map,以 HTTP 的方法作为 键值 key,然后以对应方法的前缀匹配树头节点指针作为 vaue,匹配时先通过外层的 HTTP 方法进行初步筛选,然后再进入前缀匹配树中进一步筛
添加多处理函数的常见思路:在每个节点上使用一个 list 来存储 handler,就可在每个节点上添加执行多个处理函数
协议层设计要点
抽象出合适的接口,不要尝试将 Context 信息存储在一个结构体中,应该作为参数显示的传入处理它的函数中
而且不能在连接上读写数据
网络层设计要点
BIO (Block 阻塞IO)
只要有数据就读取,但是数据不够时就会阻塞在 read 流程
go func() {
for {
conn, _ := listener.Accept()
go func() {
conn.Read(request)
handle...
conn.Write(request)
}
}
}
go net 就是典型的 BIO 模式
通过用户来管理底层的 buffer,当通过 read 或者 write 读写底层 buffer 时,就容易造成阻塞
type Conn interface {
Read(b []byte) (n int, err error)
Write(b []byte) (n int, err error)
...
}
NIO(Non-block 非阻塞 IO)
通过一个监听器监控,当数据量达到一定量时,监听器才开放读取,因此不会阻塞在 read 流程
go func() {
for {
readableConns, _ := Monitor(conns)
for conn := range readbleConns {
go func() {
conn.Read(request)
handle...
conn.Write(request)
}
}
}
}
netpoll 则是 NIO 模式,不再通过用户管理 buffer,而是由网络库来管理,用户并不知道底层数据的读写是何时发生
type Reader interface {
Peek(n int) ([]byte, error) // 表示期望底层返回大小为 n 的数据
...
}
type Writer interface {
Malloc(n int) (buf []byte, err error) // 将数据拷贝到底层大小为 n 的区域
Flush() error // 将 Malloc 拷贝到底层的数据发送出去
}
性能优化
网络库的优化
go net 优化需求:①存下全部 header (因为 http 头部长度不是固定的,必须获取完整的头部才可解析)
②减少系统调用次数 (本身read 和 write 底层都是系统调用,用户态和内核态的切换开销较大)
③能够复用内存 ④能够重复读 (头部的读取是可能失败的,要求失败之后能够再重新解析)
优化实现 go net with buffer:为每一个连接都绑定一块缓冲区(4k,调研大部分包都是小于4k的)
go net 适用场景:①流式友好,因为用户通过 read/write 去读写 buffer,不调用数据就仍在buffer中,这样不会将用户态的内存占爆炸 ②小包性能较高,因为每个连接绑定了一块内存(4k,超过就需要考虑buffer的回收)
type Reader interface {
Peek(n int) ([]byte, error) // 指针不动,保证可以重复读
Discard(n int) (discard int, err error) // 指针移动正常解析
Release() error // 释放绑定的 buffer 缓冲区,让下一次连接可以复用内存
Read(b []byte) (n int, err error)
Write(b []byte) (n int, err error)
...
}
type Writer interface {
Write(p []byte)
Size() int
Flush() error
}
netpoll 优化需求:①同样需要存下全部的 header (netpoll 维护了一个 buffer 节点池,取出节点然后通过链表 (LinkBuffer) 的方式来存储 header 信息和 body 信息,可能节点大小不够,造成 header 和 body 信息存在多个连续节点,需要跨节点解析) ②拷贝出完整的 Body
优化实现 netpoll with nocopy peek:按照历史信息,找到最大的 header + body 占用的空间,然后将 buffer 节点按照这个大小分配,这样就不必跨节点解析,但是也会显示最大的 buffer size ,因为要考虑到内存使用率
netpoll 适用场景:①中大包性能高,因为是底层自动管理buffer ②时延较低
协议的优化
headers 解析:① 找到 Header Line 的边界 \r\n
KMP/BP算法太大材小用了,可以先找到\n然后判断前面一个字符是否是\r,时间复杂度为 O(n) ,工程应用可使用 SIMD(simple instruction multi datastream) 技术来加速查找,一组指令对多组数据同时处理(汇编层面)
②headers 协议相关关键字段快速解析 (HOST\CONTENT-TYPE\CONTENT-LENGTH\CONNECTION...)
通过 header key 首字母快速筛除完全不可能的 key,然后解析相关的关键字 value 单独存储到成员变量中,通过.***来访问,使用 byte slice 管理对应的 header 存储,方便内存复用(因为 byte slice 比 map 更适合做内存管理)
③headers key 规范化 (aaa-bbb -> Aaa-Bbb)
将数据转为 ASCII 码,然后通过映射表匹配的方式来快速实现需求大小写转换 (O(1)的时间复杂度),比常规的 O(n) 的ASCII 加减的差值效率提升更快,但是会有额外的内存开销
④热点资源池化
建立 RequestContext 池,可以实现对于每个 RequestContext 的复用,减少创建销毁 RequestContext 的开销,提高内存复用,内存压力也会降低,提升性能,但是复用也会多出一些额外的比如 Reset 的逻辑实现,而且要求在请求周期内有效,否则很可能出现数据不一致的问题,这类问题很难排查定位