这是我参与「第五届青训营 」笔记创作活动的第13天
一、课程背景
二、本堂课重点内容
- 再谈 HTTP 协议
- HTTP 框架的设计与实现
- 性能修炼之道
- 企业实践
三、详细知识点介绍
1. 再谈HTTP协议
1.1 HTTP协议是什么
HTTP:超文本传输协议(Hypertext Transfer Protocol)
1.2 为什么需要协议?
需要明确的边界
- 开始
- 结束
能够携带信息
- 什么消息
- 消息类型
一个常见的POST请求在协议层究竟做了什么?
1.3 协议里有什么?
1.4 一个demo
1.5 请求流程
1.6 不足与展望
- HTTP1
- 队头阻塞
- 传输效率低
- 明文传输不安全
- HTTP2
- 多路复用
- 头部压缩
- 二进制协议
- HTTP3
- 基于UDP实现
- 解决队头阻塞
- 加密减少握手次数
- 支持快速启动
2. HTTP的框架设计与实现
2.1 分层设计
- 专注性
- 扩展性
- 复用性
- 高内聚、低耦合
- 易复用
- 高扩展性
一个切实可行的复杂系统势必是从一个切实可行的简单系统发展而来的。从头开始设计的复杂系统根本不切实可行,无法修修补补让他切实可行。你必须由一个切实可行的简单系统重新开始。 ——— 盖尔定律
2.2 应用层设计
提供合理的API
-
可理解性:如 ctx.Body(), ctx.GetBody(), 不要用ctx.BodyA()
-
简单性:如 ctx.Request, Header.Peek(key)/ctx.GetHeader(key)
-
冗余性
-
兼容性
-
可测性
-
可见性
不要试图在文档中说明!很多用户不看文档
2.3 中间件设计
中间件需求:
- 配合 Handler 实现一个完整的请求处理生命周期
- 拥有预处理逻辑与后处理逻辑
- 可以注册多中间件
- 对上层模块用户逻辑模块易用
洋葱模型:
适用场景:
- 日志记录
- 性能统计
- 安全控制
- 事务处理
- 异常处理
e.g:打印每个请求的 request 和 response
- 既然要实现预处理和后处理,那这个就很像调用了一个函数
- 路由上可以注册多Middleware,同时也可以满足请求级别有效,只需要将Middleware设计为和业务和Handler相同即可。
- 如果用户不主动调用下一个处理函数怎么办?
核心:在任何场景下index保证递增
- 出现异常想停止怎么办?
调用链:
适用场景:
- 不调用Next:初始化逻辑且不需要在同一调用栈
- 调用Next:后处理逻辑或需要同一调用栈上
思考:有没有其他实现中间件的方式?
2.4 路由设计
框架路由实际上就是为URL匹配对应的处理函数(Handlers)
- 静态路由:/a/b/c、/a/b/d
- 参数路由:/a/:id/c(/a/b/c, /a/d/c)、/*a||
- 路由修复:/a/b <-> /a/b/
- 冲突路由以及优先级:/a/b、/:id/c
- 匹配HTTP方法
- 多处理函数:方便添加中间件
- ...
e.g:路由页面
- 初级:map[string]handlers
- /a/b/c、/a/b/d
- /a/:id/c、/*a||
- 高级:前缀匹配树
- /a/b/c、/a/b/d
如何处理带参数的路由注册?(处理形如:/a/:id/b类型的路由)
如何匹配HTTP方法?
外层 Map:根据method进行初步筛选
如何实现添加多处理函数?
在每个节点上使用一个 list 存储 handler
思考:如何查找路由
2.5 如何做设计?
- 明确需求:考虑清楚要解决什么问题、有哪些问题
- 业界调研:业界都有哪些解决方案可供参考
- 方案权衡:思考不同方案的取舍
- 方案评审:相关同学对不同方案做评审
- 确定开发:确定最适合的方案进行开发
2.6 协议层设计
抽象出合适的接口:
- Do not store Contexts inside a struct type;instead,pass a Context explicitly to each function that needs it. The Context should be the first parameter.
- 需要在连接上读写数据
2.7 网络层设计
go net "BIO" 用户管理 buffer
netpoll NIO 网络库管理buffer
netpoll地址:github.com/cloudwego/n…
2.8 总结
- API 设计:可理解性、简单性
- 中间件设计:洋葱模型
- 路由设计:前缀匹配树
- 协议层设计:抽象出合适的接口
- 网络层设计:网络模型
3. 性能修炼之道
3.1 针对网络库的优化
go net
存下全部Header、减少系统调用次数、能够复用内存、能够多次读
"BIO"
go net with bufio 绑定一块缓冲区
基于此,我们可以对勾在勾标准库的接口上面封装一层buffer说也就是用一块用一个常用的一种优化手段。就是绑定在这个连接上面,绑定一块缓冲区。那根据我们在内部 的一个调研,也发现大部分的包都是在4k以下的,所以我们可以绑定一块大小为4k左右的一个缓冲区,这样对内存的压力也不是很大。那这个还那我们再设计接那我们这个再设计接口。那首先需要一个我在读的时候让指针不动,我下次还能再这里进行读,也就是Peek;以及说我们既然能够让指针不动,我们就需要一个接口,让读指针进行一个移动,也就是Discard。最后我们还需要回收这块内存,希望下一次请求能够复用之前的空间,也就是Release接口。
netpoll
存下全部Header、拷贝出完整的Body
netpoll with nocopy peek
分配足够大的buffer,限制最大的 buffer size
- go net
- 流式友好
- 小包性能高
- netpoll
- 中大包性能高
- 时延低
3.2 针对协议的优化
3.2.1 Headers 解析
找到 Header Line 边界:\r\n 先找到 \n 再看它前一个是不是 \r
那能不能更快呢?
Sonic:github.com/bytedance/s…
针对协议相关的Headers快速解析:
- 通过Header key 首字母快速筛除掉完全不可能的 key
- 解析对应 value 到独立字段
- 使用 byte slice 管理对应 header 存储,方便复用
请求体中同样处理的Key:
User-Agent、Content-Type、Content_length、Connection、Transfer-Encoding
- 取
- 核心字段快速解析
- 使用 byte slice 存储
- 额外存储到成员变量中
- 舍
- 普通 header 性能较低
- 没有 map 结构
3.2.2 Header key 规范化
aaa-bbb --> Aaa-Bbb
- 取
- 超高的转换效率
- 比 net.http 提高 40倍
- 舍
- 额外的内存开销
- 变更困难
3.3 热点资源池化
- 取
- 减少了内存分配
- 提高了内存复用
- 降低了GC压力
- 性能提升
- 舍
- 额外的Reset逻辑
- 请求内有效
- 问题定位难度增加
4. 企业实践
- 追求性能
- 追求易用,减少误用
- 打通内部生态
- 文档建设、用户群建设
内部HTTP框架:Hertz
1万+的服务,3千万+的QPS
四、课后作业
- 为什么HTTP框架做要分层设计?分层设计有哪些优势与劣势
- 现有开源社区HTTP框架有哪些优势与不足
- 中间件还有没有其他实现方式?可以用伪代码说明
- 完成基于前缀路由树的注册与查找功能?可以用伪代码说明
- 路由还有没有其他的实现方式?