HTTP框架修炼之道 | 青训营

57 阅读6分钟

1. 走进HTTP协议

1.1 HTTP协议概念

说到Http协议我们都几乎很清楚了,我们第一个大规模使用的Http协议版本是0.9。

Http又称超文本传输协议(Hypertext Transfer Protocol)。

什么是超文本呢?顾名思义,就是超越文本或不只是文本,比如图片、音频、视频等。

1.2 协议有什么

在网络传输中,传输的都是0、1这样的数据流,只有按照一定的规则,对方才能识别清楚,这样约定好的规则,就可以称为协议。

协议需要明确的边界:开始、结束。需要清楚数据流是从哪一部分开始的到哪一部分结束的。

协议能够携带信息:消息内容、消息类型等

image-20230731110436745.png

1.3 请求流程

一个请求需要经过:业务层、服务治理层、中间件层、协议编解码层、传输层。

image-20230731110557341.png

1.4 不足和展望

下面来对比下Http1、Http2、QUIC都有哪些优缺点:

  • Http1:队头阻塞、传输效率低、明文传输不安全
  • Http2:多路复用、头部压缩、二进制协议
  • QUIC:基于UDP实现、解决队头阻塞问题、加密减少握手次数、支持快速启动

2. HTTP框架设计与实现

2.1 分层设计

OSI是Open System Interconnection的缩写,称为开放式系统互联。

OSI模型把网络通信分为7层,从下到上分别为物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。

OSI分层太多,增加了网络工作的复杂性,后来进行了见后,将一些层进行合并形成了4层结构,从下到上分别为接口层、网络层、传输层和应用层,也就是TCP/IP模型。

image-20230731112338214.png

分层设计可以提高专注性、扩展性、复用性。

在分层设计的时候应该考虑高内聚低耦合、易复用、高扩展。

API分层设计:

image-20230731112445004.png

总结以下API设计:

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

2.2 应用层设计

image-20230731113310909.png

应用层是和用户打交道的一层,提供合理的API。

在设计API时,需要考虑以下几点:

  • 可理解性如:ctx.Body()、ctx.GetBody(),而不是ctx.BodyA()。

  • 简单性:如:ctx.Request.Header.Peek(key)封装为ctx.GetHeader(key)

  • 冗余性

  • 兼容性

  • 可测性

  • 可见性

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

2.3 中间件设计

image-20230731113434918.png 中间件在工作中,常常有以下需求:

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

如下图是一个洋葱模型:

image-20230731113533053.png 洋葱模型的思想是核心逻辑与通用逻辑分离。

中间件要实现预处理和后处理,很像调用了一个函数,如:

func Middleware(some param) {
	// some logic for pre-handler
	Next()
	// some logic for after-hanler
}

路由上可以注册多个中间件,同时也可以满足请求级别有效只需要将中间件设计为和业务和hanlder相同即可。

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

我们可以让用户在使用调用中间件时,先进行初始化,然后中间件处理调用的逻辑,如:

func (ctx *RequestContext) Next() {
    ctx.index++
    for ctx.index < int8(len(ctx.handlers)) {
        ctx.hanlers[ctx.index]()
        ctx.index++
    }
}

核心思想是在任何场景下index保证递增。

如果出现异常想停止怎么办?

只需要给index赋值一个最大值,跳出循环即可,如:

func (ctx *RequestContext) Abort() {
    ctx.index = IndexMax
}

中间件调用链:

image-20230731113808261.png 不适用Next:初始化逻辑且不需要在同一个调用栈

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

2.4 路由设计

image-20230731113940297.png

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

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

路由设计:

青铜:map

黄金:前缀匹配树

image-20230731114145554.png

如何匹配HTTP方法?外层map根据method进行初步筛选,然后使用前缀树匹配路由。

image-20230731114221817.png

如何实现添加多处理函数?在每个节点上使用一个list存储handler。

image-20230731114337553.png

2.5 协议层设计

image-20230731114439808.png

抽象出合适的接口:

image-20230731114543252.png

2.6 网络层设计

image-20230731114606566.png

这里需要预备一个知识点:BIO和NIO

image-20230731114632690.png

BIO使用一个监听器,中途出现堵塞就卡住了。

NIO使用监控器,监控可以使用的IO连接,用于后续的业务。

go net就是BIO,用户管理buffer。

netpoll是NIO,网络库管理buffer

3. 性能修炼之道

总结:

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

3.1 网络库优化

网络库:C10K Problem;Select,Poll,Epoll;Epoll ET、LT 区别;字节跳动自研网络库 netpollnetpoll-examples

go net:优化方案是为每一个连接绑定一块缓冲区buffer,大小为4k,这样对连接的压力也不大,最后记得回收内收。这样的优化可以存下全部 Header、减少系统调用次数、能够复用内存、能够多次读。

netpoll:在底层分配一个大节点,分配足够大的buffer,这样可以存下全部 Header,拷贝出完整的 Body。

image-20230731144710817.png

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

netpoll:中大包性能好,时延低。

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

方案:

  1. SIMD加速解码.
  2. 针对协议相关的 Headers 快速解析

SIMD:即单指令流多数据流,是一种采用一个控制器来控制多个处理器,同时对一组数据(又称“数据向量”)中的每一个分别执行相同的操作从而实现空间上的并行性的技术。简单来说就是一个指令能够同时处理多个数据。

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

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

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

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

缺点:普通 header性能较低,没有 map 结构。

3.3 针对协议优化——Header key 规范化

表映射:将字节转化成ascii码,匹配效率可以达到O(1)

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

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

3.4 热点资源池化

image-20230731145728559.png

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

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

4. 企业实战

追求性能

追求易用,减少误用

打通内部生态

文档建设、用户群建设

字节HTTP框架:Hertz