HTTP框架修炼之道 | 青训营

69 阅读5分钟

HTTP框架修炼之道

Snipaste_2023-08-15_16-12-30.png

1.再谈http协议

1.1 HTTP协议是什么

HTTP:超文本传输协议(HyperText Transfer Protocol)

1.1.1 为什么需要协议

需要明确的边界

  • 开始
  • 结束

能够携带信息

  • 什么消息

  • 消息类型

  • 协议开始

  • 协议元数据

  • TEXT

  • 协议结束

一个常见的POST请求在协议层究竟做了什么?

1.2 协议里有什么

Snipaste_2023-08-15_16-15-38.png

Snipaste_2023-08-15_16-15-54.png

Snipaste_2023-08-15_16-17-37.png

常见方法名

GET,HEAD,POST,PUT,DELETE,CONNECT,OPTIONS,TRACE,PATCH

状态码

1xx:信息类

2xx:成功

3xx:重定向

4xx:客户端错误

5xx:服务端错误

1.2.1 一个Demo

image-20230815173135815.png

image-20230815173705663.png

1.3 请求流程

image-20230815173745820.png

1.4 不足与展望

HTTP1 队头阻塞 传输效率低 明文传输不安全

HTTP2 多路复用 头部压缩 二进制协议

HTTP3 基于UDP实现 解决队头阻塞 加密减少握手次数 支持快速启动

2.HTTP框架的设计与实现

2.1 分层设计

image-20230815174535020.png

专注性、拓展性、复用性

image-20230815174744378.png

高内聚、低耦合、易复用、高拓展性

image-20230815174840511.png

​ 一个切实可行的复杂系统势必是从一个切实可行的简单系统发展而来的。从头开始设计的复杂系统根本不切实可行,无法修修补补让它切实可行。你必须由一个切实可行的简单系统重新开始。

​ ——盖尔定律

2.2 应用层设计

提供合理的API

  • 可理解性:如ctx.Body(), ctx.GetBody(),

    ​ 不要用 ctx.BodyA()

  • 简单性:如 ctx.Request.Header.Peek(key)/ctx.GetHeader(key)

  • 冗余性

  • 兼容性

  • 可测性

  • 可见性

不要试图在文档中说明,很多用户不看文档

2.3 中间件设计

中间件需求:

  • 配合Handler实现一个完整的请求处理生命周期
  • 拥有预处理逻辑与后处理逻辑
  • 可以注册多中间件
  • 对上层模块用户逻辑模块易用

洋葱模型

v2-99b874cf305bbfe6ecfcb2df1143dccb_b.jpg

核心逻辑与通用逻辑分离

适用场景:日志记录、性能统计、安全控制、事务处理、异常处理

1.既然要实现预处理和后处理,那这个就很像调用了一个函数

image-20230815182432056.png

2.路由上可以注册多Middleware, 同时也可以满足请求级别有效,只需要将Middleware 设计为和业务和Handler 相同即可。

3.用户如果不主动调用下一个处理函数怎么办?

image-20230815182711547.png

核心:在任何场景下index保证递增

4.出现异常想停止怎么办?

image-20230815182751975.png

调用链:

image-20230815182804704.png

有没有坑?不在一个调用栈上

适用场景:

  • 不调用Next:

初始化逻辑且不需要在同一调用栈

  • 调用Next:

后处理逻辑或需要在同一调用栈上

2.4 路由设计

框架路由实际上就是为URL匹配对应的处理函数(Handlers )

  • 静态路由: /a/b/c、 /a/b/d
  • 参数路由: /a/:id/c (/a/b/c, /a/d/c)、 /*all
  • 路由修复: /a/b <-> /a/b/
  • 冲突路由以及优先级: /a/b、 /:id/c
  • 匹配HTTP方法
  • 多处理函数:
  • 方便添加中间件

青铜:map[string]handlers /a/b/c、/a/b/d /a/:id/c、/*all

黄金:前缀匹配树 /a/b/c、/a/b/d

image-20230815183140800.png

/a/b/c /a/b/d /a/:b/d /a/:c/f

image-20230815183326762.png

2.4.1 如何匹配HTTP方法

image-20230815183450342.png

路由映射表 外层Map:根据method进行初步筛选

2.4.2 如何添加多处理函数

在每个节点上使用一个list 存储handler

image-20230815183633021.png

2.4.3 如何做设计

  1. 明确需求:考虑清楚要解决什么问题、有哪些需求
  2. 业界调研:业界都有哪些解决方案可供参考
  3. 方案权衡:思考不同方案的取舍
  4. 方案评审:相关同学对不同方案做评审
  5. 确定开发:确定最合适的方案进行开发

2.5 协议层设计

image-20230815183747652.png

抽象出合适的接口:

image-20230815183812000.png

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.6 网络层设计

image-20230815183955534.png

2.6.1 BIO NIO

BIO:Blocking IO 同步阻塞IO

image-20230815184017339.png

go net BIO 用户管理buffer

image-20230815184117506.png

NIO:Non-Blocking IO 同步非阻塞IO

image-20230815184026529.png

netpoll NIO 网络库管理buffer

image-20230815184204594.png

image-20230815184231596.png

2.7 总结

  • API设计:可理解性、简单性…
  • 中间件设计:洋葱模型
  • 路由设计:前缀匹配树
  • 协议层设计:抽象出合适的接口
  • 网络层设计:网络模型

3.性能修炼之道

3.1 针对网络库的优化

go net

存下全部Header,减少系统调用次数,能够复用内存,能够多次读

“BIO”

image-20230815184441092.png

go net with bufio 绑定一块缓冲区

image-20230815184514485.png

netpoll

存下全部Header,拷贝出完整的Body netpoll with nocopy peek 分配足够大的buffer,限制最大buffer size

image-20230815184547692.png

不同网络库的优势

go net 流式友好 小包性能高

netpoll 中大包性能好 时延低

3.2 针对协议的优化——Headers 解析

找到Header Line边界:\r\n 先找到\n再看它前一个是不是\r

image-20230815184904033.png

image-20230815184930207.png

image-20230815193329606.png

针对协议相关的Headers快速解析:

  1. 通过Header key首字母快速筛除掉完全不可能的key
  2. 解析对应value到独立字段
  3. 使用byte slice 管理对应header存储,方便复用

请求体中同样处理的Key:User-Agent、Content-Type、 Content-Length. Connection、 Transfer-Encoding

取:核心字段快速解析,使用byte slice存储,额外存储到成员变量中、

舍:普通header性能较低,没有map结构

3.3 针对协议的优化-Header Key 规范化

aaa-bbb ——> Aaa-Bbb

取:超高的转换效率,比net.http 提高40倍

舍:额外的内存开销,变更困难

3.4 热点资源池化

image-20230815194014627.png

与请求一一对应,贯穿一个请求始终

image-20230815194101324.png

取:减少了内存分配,提高了内存复用,降低了GC压力,性能提升

舍:额外的Reset逻辑,请求内有效,问题定位难度增加

3.5 总结

针对网络库的优化:buffer 设计 针对协议的优化:header 解析、热点资源池化

4.企业实践

  • 追求性能
  • 追求易用,减少误用
  • 打通内部生态
  • 文档建设、用户群建设