针对网络库的优化
针对原生的网络库进行了一定的优化,从而提升框架的性能:
1、go net是BIO,用户管理Buffer
2、netpoll是NIO,网络库管理Buffer
1、go net
1、希望有一个地方可以存下全部的Header,http头部它是不知道长度的,那我们需要存下全部header才能够进行一个解析。header有大有小,这个就是一个非常麻烦的点。
2、希望能够减少调用的次数,因为系统调用涉及到内核态和用户态的一个切换。这部分的开销还是非常大的。像这个reader和writer,其实它就是底层就是一个系统调用了,尤其是这个writer。
3、希望能够复用内存,提高一下内存的使用率。内存的分配和这些对于客户来说的开销都是非常大的。虽然它优化了很多,但还是有一定开销的。
4、希望能够多次读,这部分主要体现在一个对header的处理上,那对于一个超大的header,比如我可能第一次把它读不完,我并不知道header是不完整的,那所以说我们就只有发现解析失败了,我们才知道当前的这个header是不足够的。那下一次呢我们希望还能够从头进行一个解析。
采用零拷贝的概念,绑定缓冲区:
针对此,我们在go net提供接口上面绑定1个buffer,就是说对每一个连接都绑定1个buffer,我们发现大部分的包其实都是在4K以下的,所以我们可以绑定一个大小为4K左右的一个缓冲区,这样的话对于连接的压力也不是很大。
对于刚刚提出的网络需求需要再设计一下我们封装到connection上面的这个接口。
1. Peek 接口
-
功能:在读取数据时,不移动读取指针。这意味着你可以查看下一部分数据,但不会影响下一次读取操作的起始位置。下次再读的时候还能够在这里进行读。
-
这个接口方法返回下
n个字节的数据,但不改变读取指针的位置。
2. Discard 接口
-
功能:移动读取指针,丢弃已经查看过的数据。这有助于在处理数据时跳过不需要的部分。
-
这个接口方法会移动读取指针,丢弃下
n个字节的数据,并返回实际丢弃的字节数和可能的错误。
3. Release 接口
-
功能:回收内存,使得下一次请求可以复用之前的空间。这对于减少内存分配和提高性能非常重要。需要回收这样的一块内存,希望在下一次请求能够复用之前的空间。
-
这个接口方法用于释放资源,以便下次使用时可以复用内存。
整体设计
-
这些接口方法可以组合在一起,形成一个完整的
Reader接口,用于处理网络连接中的数据读取操作。 -
在实际实现中,你可能需要考虑并发安全性,确保在多线程环境下这些方法的正确执行。
-
可以使用
bufio包或其他缓冲机制来辅助实现这些接口,以提高数据读取的效率。
这种设计可以有效地优化网络连接中的数据读取操作,减少不必要的内存分配和系统调用,提高整体性能。
1、netpoll
关于netpoll这一部分,同样我们也有同样的诉求,首先是存下全部header,第二个呢是拷贝出完整的body,netpoll是由网络库去管理的buffer。buffer是为了避免锁竞争,采用链表方式,实现无锁化。
链表方式带来一个问题就是跨节点,比如我们这张示意图,我们header其实它是被分配到了两个节点上,那如果我要是想要完整的解析这个header的话,我是需要将这两个header编成一块完整的内存,拷贝到一块完整的内存里,然后再进行一个解析。那同样body也是需要一个完整的body了,所以我们这个body也需要再分配一块内存,然后拷贝进去
这是关于netpoll网络库中缓冲区管理的优化方案。以下是详细的分析和解释:
1. 现有问题
-
跨节点数据处理
- 在
netpoll中,使用链表方式管理缓冲区(buffer)以避免锁竞争。然而,这种方式会导致数据(如header和body)可能被分配到不同的节点上。 - 为了完整地解析
header和body,需要将分散在不同节点的数据拷贝到一块完整的内存中,这增加了数据拷贝的开销。
- 在
2. 优化方案
-
减少数据拷贝
- 直接分配大节点:在底层分配一个足够大的节点,其大小根据历史请求中最大的请求来确定。这样,
header和body可以直接在底层拼接好。 - 减少拷贝操作:框架层直接使用底层返回的切片,避免了将数据从多个节点拷贝到一块完整内存的操作,从而减少了数据拷贝的开销。
- 直接分配大节点:在底层分配一个足够大的节点,其大小根据历史请求中最大的请求来确定。这样,
3. 内存使用率问题
-
问题描述
- 当出现一个大请求时,缓冲区(
buffer)会被分配一个较大的内存空间。这可能导致后续的小请求也会分配到同样大的内存空间,造成内存浪费。
- 当出现一个大请求时,缓冲区(
-
解决方案
- 限制最大缓冲区大小:为了避免这种情况,需要限制最大的缓冲区大小。这样可以防止一个大请求导致整个缓冲区变得过大,从而提高内存的使用率。
总结
-
通过在底层直接分配足够大的节点来减少数据拷贝操作,提高了数据处理的效率。
-
通过限制最大缓冲区大小,避免了大请求导致的内存浪费问题,提高了内存的使用率。
这种优化方案能够有效地解决netpoll网络库中缓冲区管理的一些关键问题,提升整体性能。
1. 接口定义
type Conn interface {
net.Conn
Reader
Writer
}
- 这个接口定义了一个名为
Conn的接口类型。 - 它嵌入了三个其他接口:
net.Conn、Reader和Writer。
2. 接口嵌入的目的
-
兼容标准库生态
- 通过嵌入
net.Conn接口,这个自定义的Conn接口可以兼容标准库中的net.Conn接口。这意味着任何实现了标准库net.Conn接口的类型也实现了这个自定义的Conn接口。 - 这有助于在使用这个网络库时,能够无缝地与标准库中的其他网络相关功能集成。
- 通过嵌入
-
自定义功能
- 嵌入
Reader和Writer接口,表明这个Conn接口还具有特定的读取和写入功能。 - 根据描述,这些功能可能包括能够多次读取和复用缓冲区空间等优化操作。
- 嵌入
3. 可能的实现和使用场景
- 在具体实现这个接口时,需要确保实现类型同时满足
net.Conn、Reader和Writer接口的所有方法。 - 使用场景可能包括网络服务器或客户端的实现,其中需要高效地处理网络连接、多次读取数据以及复用缓冲区空间以提高性能。
1. go net
-
流式友好
- 原理:
go net的read接口由用户态调用。如果用户不调用read,数据会留在 TCP 缓冲区中,不会导致用户态内存爆满。 - 优势:这种设计使得
go net在处理流式数据时非常友好,能够有效避免用户态内存的过度占用。
- 原理:
-
小包性能高
- 原理:
go net在连接上直接绑定了一块内存(通常约为 4K)。对于小于 4K 的小包,无需额外的缓冲区申请和回收操作。 - 优势:这种设计使得
go net在处理小包数据时性能较高,减少了内存管理的开销。
- 原理:
2. netpoll
-
中大包性能高
- 原理:
netpoll由网络库管理缓冲区,并且其接口可以直接发送二维切片。当一个节点的缓冲区不够时,可以挂接另一个节点,并且可以通过一次系统调用发送出去。 - 优势:这种设计使得
netpoll在处理中大包数据时性能较高,减少了系统调用的次数,提高了数据发送的效率。
- 原理:
-
时延低
- 原理:由于
netpoll能够高效地管理和发送数据,减少了系统调用和数据拷贝的次数,从而降低了时延。 - 优势:这种设计使得
netpoll在对时延要求较高的场景下表现出色。
- 原理:由于
根据具体的应用场景和数据类型,可以选择更适合的网络库来优化性能。
针对协议的优化
Headers解析
主要讨论了针对协议的优化,特别是在解析 Headers 方面。以下是详细的解释:
1. Headers 解析的优化
-
问题背景
- 在处理网络协议时,需要解析 Headers。Headers 通常以
\r\n作为行边界。传统的方法是先找到\n,然后再检查前一个字符是否是\r。
- 在处理网络协议时,需要解析 Headers。Headers 通常以
-
现有代码
- 展示了一段 Go 语言代码,用于查找字节切片
b中字符c的索引位置。
- 展示了一段 Go 语言代码,用于查找字节切片
func index(b []byte, c byte) int {
for i := 0; i < len(b); i++ {
if b[i] == c {
return i
}
}
return -1
}
- 这段代码是一个简单的线性搜索函数,用于在字节切片
b中查找字符c的位置。
2. 优化思路
-
SIMD (Single Instruction, Multiple Data)
- SIMD 是一种并行处理技术,允许一条指令处理多个数据元素。在解析 Headers 的场景中,可以利用 SIMD 来加速字符查找过程。
- 例如,可以使用 CPU 的 SIMD 指令集(如 SSE、AVX 等)来同时比较多个字节,从而减少循环次数,提高查找效率。
3. 总结
- 目前的代码采用了简单的线性搜索来查找字符,这种方法在处理大量数据时可能效率较低。
- 可以考虑使用 SIMD 技术来优化字符查找过程,从而提高 Headers 解析的速度。
1. 代码部分
switch s.Key[0] | 0x20 {
case 'h':
if utils.CaseInsensitiveCompare(s.Key, bytestr.StrHost) {
h.SetHostBytes(s.Value)
}
continue
...
- 这段代码看起来是在处理 HTTP 头(Headers)。它检查了一个键(
s.Key)的首字母是否为h(不区分大小写),如果是,则进一步检查键是否为Host,若是,则设置主机字节(SetHostBytes)。
2. 针对协议相关的 Headers 快速解析
-
通过 Header key 首字母快速筛除掉完全不可能的 key
- 这种方法通过检查 Header 键的首字母来快速排除不可能的键。例如,如果一个请求的 Header 键的首字母不是
h,那么它不可能是Host键。
- 这种方法通过检查 Header 键的首字母来快速排除不可能的键。例如,如果一个请求的 Header 键的首字母不是
-
解析对应 value 到独立字段
- 这意味着将 Header 中的值解析到对应的独立字段中,以便于后续处理。例如,将
Host键对应的值解析到一个特定的字段中。
- 这意味着将 Header 中的值解析到对应的独立字段中,以便于后续处理。例如,将
-
使用 byte slice 管理对应 header 存储,方便复用
- 使用字节切片(byte slice)来管理 Header 的存储,可以提高内存使用效率和复用性。
3. 请求体中同样处理的 Key
-
提到了一些在请求体中同样需要处理的键(除了host):
-
User - Agent -
Content - Type -
Content - Length -
Connection -
Transfer - Encoding
-
这些键在处理 HTTP 请求时通常非常重要,需要进行特定的解析和处理。
总结
- 图片中的内容主要围绕如何优化 HTTP 头(Headers)的解析过程,包括通过首字母快速筛选键、解析对应的值到独立字段,以及使用字节切片来管理存储。这些优化措施可以提高协议处理的效率
1. “取”
-
核心内容
- 核心字段快速解析:在解析 Headers 时,对核心字段进行快速解析。这可能涉及到对特定关键字段(如常见的 HTTP 头部字段)进行优先处理,以提高解析效率。
- 使用 byte slice 存储:采用字节切片(byte slice)来存储解析后的结果。这种方式有助于优化内存使用,特别是在处理大量数据时。
- 额外存储到成员变量中:高频的header会将解析后的结果存储到成员变量中,方便后续使用。
2. “舍”
-
核心内容
- 普通 header 性能较低:对于普通的头部信息,其处理性能较低。这可能是因为这些普通头部信息在处理时没有采用优化策略,导致处理速度较慢。
- 没有 map 结构:在处理普通头部信息时,没有采用映射(map)结构。映射结构通常可以提高查找和处理的效率,没有这种结构可能会导致性能下降。
总结
- 这张图片主要对比了在解析 Headers 时,对核心字段和普通字段的不同处理策略。核心字段采用了快速解析、字节切片存储和成员变量存储等优化策略,而普通字段则可能因为缺乏优化措施(如没有使用 map 结构)而导致性能较低。
- 这种优化策略在网络协议处理中非常常见,特别是在处理 HTTP 请求时,可以显著提高处理效率。
Header key规范化
将类似于 “aaa - bbb” 的字符串转换为 “Aaa - Bbb” 的形式。以下是详细的解释:
1. 问题背景
- 在处理网络协议中的 Header key 时,有时需要将字符串规范化,例如将小写字母转换为大写字母。传统的方法可能是使用加减法来转换字符的 ASCII 码,但这种方法效率较低。
2. 优化方法:表映射
-
基本思路
- 创建两个映射表:
toupperTable和tolowerTable。 - 这些表将字符的 ASCII 码值作为索引,直接查找并返回转换后的字符。
- 这种方法的时间复杂度为 O (1),因为查找操作是常数时间。
- 创建两个映射表:
-
映射表的构建
-
图片中展示了
toupperTable和tolowerTable的具体内容。这些表是通过 Go 语言中的字符串常量定义的。 -
例如,
tolowerTable的定义如下:
-
const tolowerTable = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"
- 这个字符串常量实际上是一个 ASCII 码表,其中每个字符的位置对应其 ASCII 码值。
3. 转换过程
- 当需要将一个字符转换为大写或小写时,只需查找该字符在映射表中的位置,并返回对应位置的字符。
- 例如,要将字符 'a' 转换为 'A',可以查找
toupperTable中 'a' 对应的位置,然后返回该位置的字符。
4. 效率对比
- 这种表映射方法比传统的加减法转换方法效率更高。因为加减法方法需要进行算术运算,而表映射方法只需要一次查找操作。
总结
- 通过创建和使用映射表,可以高效地将 Header key 中的字符进行规范化处理,提高协议处理的效率。
热点资源池化
1. 热点资源池化的基本概念
-
请求处理流程
- 从前面的图片可以看到,请求(Request)进入系统后,会从 “RequestContext 池” 中获取一个请求上下文(Request Context)来处理请求。处理完成后,系统会生成一个响应(Response)返回给用户。
- 这种设计通过复用请求上下文,避免了频繁创建和销毁资源,从而提高了系统的处理效率。
2. 改进方法(“取”)
-
减少了内存分配
- 通过资源池化,系统不需要为每个请求都分配新的内存资源。而是从池中获取已经分配好的资源,减少了内存分配的开销。
-
提高了内存复用
- 资源池中的资源可以被多个请求复用,提高了内存的利用率,避免了资源的浪费。
-
降低了 GC 压力
- 由于减少了内存分配和销毁的频率,垃圾回收(GC)的压力也相应降低。这有助于提高系统的整体性能。
-
性能提升
- 综合以上几点,系统的性能得到了显著提升,能够更高效地处理大量请求。
3. 取舍(“舍”)
-
额外的 Reset 逻辑
- 为了确保资源池中的资源能够被正确复用,需要额外的逻辑来重置(Reset)资源的状态。这增加了系统的复杂性。
-
请求内有效
- 资源池中的资源通常是在请求内有效的,这意味着资源的生命周期与请求的生命周期紧密相关。这可能会限制资源的使用方式。
-
问题定位难度增加
- 由于资源池的存在,当出现问题时,定位问题的难度会增加。因为资源可能被多个请求共享,很难确定是哪个请求导致了问题。
总结
- 热点资源池化是一种有效的优化策略,通过复用资源来提高系统的性能。
- 然而,这种方法也带来了一些额外的复杂性和问题,需要在设计和实现时进行权衡。通过合理的设计和管理,可以最大化其优点,同时最小化其缺点。
企业实践
字节的 HTTP 框架 Hertz 1w+服务、3kW+ qpS
- 追求性能
- 追求容易使用、容易上手
- 搭建内部生态、打通内部
- 文档健全
- 社区活跃
问题
1. 为什么HTTP框架要分层设计?
分层设计有哪些优势与劣势。
原因
- 模块化和职责分离:HTTP框架处理的功能众多,包括请求处理、响应生成、资源管理、安全验证等。分层设计可以将这些功能模块分开,每个层专注于特定的任务,便于开发、维护和扩展。
-
- 可复用性:分层后,每一层可以在不同的应用场景或项目中被复用。例如,底层的数据处理层可以在多个不同的上层业务逻辑应用中使用。
优势
- 易维护:当出现问题时,可以快速定位到特定的层进行排查和修复,而不需要在整个代码库中搜索。
- 可扩展性:可以方便地在某一层添加新功能或修改现有功能,而不会对其他层造成太大影响。
- 灵活性:不同的层可以独立替换或升级,例如更换底层的数据库系统,而不影响上层的业务逻辑。 #### 劣势 - 性能开销:多层之间的调用和数据传递可能会带来一定的性能损耗,尤其是在对性能要求极高的场景下。
- 复杂度增加:分层设计本身需要良好的架构设计,如果设计不当,可能会导致层与层之间的关系混乱,反而增加了系统的复杂性。
2. 现有开源社区HTTP框架有哪些优势与不足。
优势
- 成熟稳定:许多开源HTTP框架(如Spring Boot、Django等)经过了大量项目的实践检验,在稳定性方面有保障。
- 社区支持:拥有庞大的开源社区,能够得到及时的技术支持,并且有大量的文档、教程和示例代码可供参考。
- 功能丰富:通常集成了诸如路由、缓存、数据库连接、安全验证等多种功能,减少了开发者的工作量。 #### 不足
- 定制性限制:对于某些特殊的业务需求,开源框架可能无法很好地满足,需要进行大量的定制化工作,甚至可能需要修改框架的核心代码。
- 性能瓶颈:在高并发场景下,一些开源框架可能会暴露出性能问题,需要进行性能优化或者寻找更适合的框架。
- 版本兼容性:随着框架版本的更新,可能会出现与现有项目不兼容的情况,需要花费时间和精力进行升级和适配。
3. 中间件还有没有其他实现方式?可以用伪代码说明。 中间件有多种实现方式,以下是一种基于函数式编程的中间件实现方式(伪代码示例):
python
代码解读
复制代码
# 定义中间件函数类型
Middleware = Callable[[Callable[[], Any]], Callable[[], Any]]
def middleware1(next_func):
def wrapper():
print("Middleware 1: Before")
result = next_func()
print("Middleware 1: After")
return result
return wrapper
def middleware2(next_func):
def wrapper():
print("Middleware 2: Before")
result = next_func()
print("Middleware 2: After")
return result
return wrapper
# 定义目标函数
def target_function():
print("Target function executed")
return "Result"
# 应用中间件
wrapped_function = middleware2(middleware1(target_function))
wrapped_function()
在这个示例中,中间件通过函数嵌套的方式实现,每个中间件可以在目标函数执行前后添加自定义的逻辑。
4. 完成基于前缀路由树的注册与查找功能?可以用伪代码说明。 以下是基于前缀路由树(Trie树)的路由注册与查找的伪代码:
python
代码解读
复制代码
class TrieNode:
def __init__(self):
self.children = {}
self.handler = None
class Router:
def __init__(self):
self.root = TrieNode()
def register(self, route, handler):
current = self.root
for char in route:
if char not in current.children:
current.children[char] = TrieNode()
current = current.children[char]
current.handler = handler
def lookup(self, route):
current = self.root
for char in route:
if char not in current.children:
return None
current = current.children[char]
return current.handler
在上述伪代码中: - TrieNode 表示路由树的节点,包含子节点字典和对应的处理函数。 - Router 类用于管理路由树,包括注册路由(register 方法)和查找路由(lookup 方法)。
5. 路由还有没有其他的实现方式?
基于正则表达式的路由
- 原理:使用正则表达式来匹配请求的URL路径,根据匹配结果找到对应的处理函数。
- 示例:在Python的Flask框架中,可以使用
@app.route装饰器并传入正则表达式来定义路由,例如@app.route(r'/user/<regex("[a - z]+"):username>')来匹配以/user/开头,后面跟着一个或多个小写字母的路径。
基于哈希表的路由
- 原理:将请求的路径作为哈希表的键,对应的处理函数作为值。当请求到来时,通过计算路径的哈希值来快速查找处理函数。
- 示例:
lua
代码解读
复制代码
route_table = {}
def register_route(path, handler):
route_table[path] = handler
def lookup_route(path):
return route_table.get(path)
这种方式在路径数量较少且固定的情况下,查找速度非常快。