HTTP 框架修炼之道 | 青训营

83 阅读6分钟

课程背景

Http是前端客户端与服务端通讯地基础协议之一 image.png

1.再谈 HTTP 协议

HTTP 协议是什么

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

为什么需要协议

需要明确的边界:开始与结束 。描述携带信息的类型

协议里有什么

请求

image.png

回复

image.png

请求行:方法名,URL,协议版本

常见方法名包括:GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH,

请求头:协议约定,业务相关

状态行: 协议版本, 状态码, 状态码描述

状态码 1xX:信息类, 2xX:成功, 3xX:重定向, 4xX:客户端错误, 5xx:服务端错误

响应头:协议约定,业务相关

请求流程

image.png 业务层:使用框架提供的API处理业务逻辑
服务治理层:依托于中间件层, 中间件层对每一个请求有先后处理的逻辑,和请求级别绑定的

不足与展望

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

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

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

2.HTTP 框架的设计与实现

分层设计

分层设计可以简化系统的设计。

优点:专注性, 扩展性, 复用性

http主要聚焦于第四层, image.png


在弄分层设计时应该关注:高内聚, 低耦合 ,易复用, 高扩展性 这几点 image.png Application:应用层,处理用户的数据,对请求进行抽象,提供API
middleware:中间件层,对用户有预处理,和后处理的逻辑
route:路由层,有原生的路由事件,类似于注册,寻址
codec:协议层
transport:传输层

应用层设计

提供合理的 API

可理解性: 如 ctx.Body(),ctx.GetBody(),不要用 ctx.BodyA()

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

冗余性:不要有功能重复户的API,不要有一个API是由两个API组成的

兼容性:在开发中尤为重要,要保证所有的API可用

可测性:写出来的API是可测试的

可见性:首先是为了安全性,不让用户拿到核心的代码,方便使用人员

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

中间件设计

中间件需求

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


核心逻辑与通用逻辑分离

洋葱模型

image.png

Request请求首先经过日志中间件的预处理,然后经过Metrics中间件的预处理,然后进行业务逻辑,之后分别在经过Metrics中间件的后处理,日志中间件的后处理,然后返回一个Response给用户。

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

中间件设计

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

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

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

image.png

在任何场景下index保证递增

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

image.png


调用链

image.png

适用场景:

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

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

路由设计

框架路由实际上就是为 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 方法

多处理函数:方便添加中间件


前缀匹配树

/a/b/c、/a/b/d

image.png

如何处理带参数的路由注册?

(处理形如: /a/:id/b类型的路由)

/a/b/c

/a/b/d

/a/:b/d

/a/:c/f

image.png

匹配 HTTP 方法

image.png

外层 Map: 根据 method 进行初步筛选

如何实现添加多处理函数?

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

image.png

如何做设计

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

协议层设计

抽象出合适的接口:

image.png

需要在连接上读写数据

网络层设计

BIO image.png NIO image.png

no net

BI0, 用户管理 buffer

image.png netpoll

NIO, 网络库管理 buffer

image.png netpoll 地址 github.com/cloudwego/n…

image.png

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

3.性能修炼之道

针对网络库的优化

**go net ** 用的都是BIO,让用户进行管理

image.png 存下全部 Header(不知道Header大小), 减少系统调用次数, 能够复用内存, 能够多次读

go net with bufio 绑定一块缓冲区

image.png

那基于此,我们可以对勾在勾标准库的接口上面封装一层 buffer 说也就是用一块用一个常用的一种优化手段。就是绑定在这个连接上面,绑定一块缓冲区。那根据我们在内部的一个调研,也发现大部分的包都是在 4k 以下的,所以我们可以绑定一块大小为 4k 左右的一个缓冲区,这样对内存的压力也不是很大。那这个还那我们再设计接那我们这个再设计接口。那首先需要一个我在读的时候让读指针不动,我下次还能够在这里进行读,也就是 Peek;以及说我们既然就能够让读指针不动,那我们就需要一个接口,让读指针进行一个移动,也就是 Discard。最后呢我们还需要回收这块内存,希望下一次请求能够复用之前的空间,也就是 Release 接口。


netpoll

存下全部 Header, 拷贝出完整的 Body

image.png

netpoll with nocopy peek

分配足够大的 buffer, 限制最大 buffer size

image.png


针对网络库接口 image.png

不同网络库优势

go net: 流式友好 小包性能高

netpoll: 中大包性能高, 时延低

针对协议的优化

Headers 解析

找到 Header Line 边界: \r\n

先找到\n再看它前一个是不是\r

image.png SIMD

Sonic: github.com/bytedance/s…


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

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

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

Header key 规范化

aaa-bbb -> Aaa-Bbb

image.png

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

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

热点资源池化

image.png

image.png

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

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