这是我参与「第三届青训营-后端场」笔记创作活动的第14篇笔记。
1. HTTP协议
1.1 HTTP是什么
HTTP(Hypertext Transfer Protocol)超文本传输协议。超文本就是除了文本以外还能传输图片、音乐、视频的数据。
1.2 协议里面有什么
- 需要明确的边界确定协议的开始与结束。
- 需要包含消息类型(元数据)、消息内容
http请求包括:
- 请求行:请求描述(方法名、URL)、版本描述
- 请求头:元数据(包含body长度)
- 请求体:body
http应答包括:
- 状态行:版本描述、状态码+对应状态
- 响应头:元数据
- 响应体:回复信息
常见方法名有:GET、HEAD、POST、PUT、DELETE、CONNECT、OPTIONS、ARACE、PATCH等
PUT全局更新,PATCH局部更新 PUT是幂等的,PATCH不是幂等的
1.3 请求流程
- 业务层:使用api完成业务逻辑
- 服务治理层(依托于中间件层):相当与修饰器
- 路由层:仅在服务端需要考虑
- 协议编(解)码层
- 传输层
1.4 不足与展望
- http1:(TCP)对头阻塞、传输效率低(报头字段太多)、明文传输不安全、不支持多路复用
- http2:支持多路复用、利用头部压缩(把重复的头部在两端存储以减少需要传输的头部大小)、使用二进制协议,队头阻塞、TLS握手开销较大的问题未解决。
- quic:基于UDP实现,解决对头阻塞,加密减少握手次数,支持快速启动
2. HTTP框架的设计与实现
网络分层架构带来了专注性(每个团队只需考虑自己层要做的事)、扩展性(协议更新不用全局改造,而是只改变特定的层)、复用性(可以单独抽出某一层使用)
HTTP内部也是分层的,这使HTTP的结构高内聚、低耦合。易复用。具有高扩展性。
从上到下分为5层。
- 应用层:与用户直接打交道,对请求进行抽象,提供api。
- 中间件层:预处理。
- 路由层:注册、寻址等。
- 协议层:支持不同版本的协议。只要实现对应的抽象接口就可以实现扩展。
- 网络库层
common存放公共逻辑,每一层都会使用。
应用层设计
提供合理的API:
- 可理解性:使用主流意见,让接口命名容易理解
- 简单性:尽量不形成接口链
- 冗余性:不要提供功能相同的接口
- 兼容性:接口能随版本一直使用
- 可测性:接口可以测试
- 可见性:将接口暴露,核心代码透明
不要试图在文档中说明,很多用户不看文档
2.3 中间件设计
中间件需求:
- 配合handler实现一个完整的请求处理生命周期
- 拥有预处理逻辑与后处理逻辑
- 可以注册多个中间件
- 对上层模块用户逻辑易用
洋葱模型(起装饰器作用): 核心逻辑与通用逻辑分离。例如请求进入日志层、监控层、核心层。在核心层处理后,逐步经过监控层(后处理)、日志层(后处理)最后形成响应传出。
适用场景:
- 日志记录
- 性能统计
- 安全控制
- 事物处理
- 异常处理
调用链:
中间件自增编号。每个函数有执行到第k个中间件的标识。调用完中间件后调用Next函数将k加1。如果不调用后续中间件,直接将k赋值为最大。
适用场景:
- 不调用Next:初始化逻辑且不需要在同一调用栈
- 调用Next:后处理逻辑或者需要在同一调用栈上
2.4 路由设计
框架路由实际上就是为URL匹配对应的处理函数(Handlers)
- 静态路由:
/a/b/c,/a/b/d - 参数路由:
/a/:id/c,/*all - 路由修复:
/a/b<->/a/b/ - 冲突路由以及优先级:
/a/b、/:id/c - 匹配HTTP方法(GET、POST......)
- 多处理函数
- ........
version1: map[string]handlers 。使用哈希表实现路由映射。实现简单,但不支持参数路由
version2:前缀匹配树
如何匹配HTTP方法?哈希表。不同方法对应不同前缀树。有的方法不需要路由则无前缀树。
如何实现添加多处理函数?每个节点上使用一个list存储handler。
如何做设计?
- 明确需求:考虑清楚问题与需求
- 业界调研:参考已有的解决方法
- 方案权衡
- 方案评审
- 确定开发:确定最合适的方案进行开发
2.5 协议层设计
抽象出合适的接口:
- 不要将contexts写在函数体里,而是作为第一个参数显示的传入(golang的官方推荐)。
- 需要在连接上读写数据。
2.6 网络层设计
BIO:阻塞型IO,go net是BIO,需要用户管理buffer。
NIO:注册监听器,有足够的写入时再执行读取。netpoll是NIO,网络库管理buffer。
3.性能如何提升
3.1 针对网络库的优化
go net:存下全部Header,系统调用次数多,不能复用内存,不能多次读(header过大,一次读不完)
go net with bufio:对每一个连接绑定一块缓冲区
netpoll:存下全部Header,拷贝出完整的Body
netpoll with nocopy peek:分配足够大的buffer(根据历史值计算),限制最大buffer size。
- go net对于流式友好,小包性能高
- netpoll:由底层进行buffer管理,所以中大包性能高,时延低
3.2 针对协议的优化
找到Header Line边界:\r\n
可以先找到\n再看前一个是不是\r
能不能更快?SIMD,单指令级多数据技术,一次匹配多个字符
针对协议相关的Headers快速解析:
- 通过Header key首字母快速筛选除掉完全不可能的key(本来需要读全字符串进行精确匹配,此处先用首字母初筛,然后对剩余部分精确匹配)
- 解析对应value到独立字段,高频属性不放在流中,而是独立字段
- 使用byte slice管理对应header存储,方便内存复用
请求体中同样处理的key:User-Agent、Content-Type、Connection、Transfer-Encoding
- 优点:核心字段快速解析。使用byte slice存储。额外存储到成员变量中。
- 缺点:普通header性能较低。没有map结构。
3.3 Header Key规范化
文本映射:aaa-bbb -> Aaa-Bbb
使用表映射的方法,比go net计算ASC码快40倍。但需要存两张表,额外的内存开销。表的变更存在困难(一般表不会变更)。
3.4 热点资源池化
- 优点:减少内存分配,提高内存复用,降低GC压力,提升性能
- 缺点:额外的reset逻辑(放回池子前需要将里面的值reset)。请求内有效。问题定位难度增加。
4. 企业实践
追求性能-->追求易用、减少误用-->打通内部生态(提高代码复用)--->文档建设、用户群建设