🔗 关于存储系统的头脑风暴
Squid 的存储管理器效率低下,主要表现在以下几个方面:
🔗 与存储相关的方面
- 将数据存储在 4k 的链表中,这意味着写入对象时需要进行多次复制;
- 内存中的表示与磁盘上的表示几乎相同(不包含磁盘元数据);并且两者都与从网络读取的内容非常相似。
- 这意味着,在 Squid-3 及之前的 Squid-2.6 版本中,客户端会从头开始读取回复,解析回复和标头。这是浪费的。Squid-2 的 HEAD 请求仅使用
MemObject->reply中的解析副本,而不是为每个回复重新解析——但这个解析后的回复实际上是由存储例程解析的,而不是“传递”一个解析后的结构进行存储。 - 它完全无法处理部分对象内容——这部分是存储问题,部分是 HTTP 服务器端问题,因为需要知道如何从内存缓存和网络中满足范围请求。
- 没有中间层。所有的刷新、Vary/ETag 和 Range 逻辑都与客户端处理紧密耦合。
- 无法动态修改/添加标头。在需要更新标头的刷新(304 可能带有更新的标头)以及支持分块编码的拖车标头时是必需的。
🔗 与客户端相关的方面
- 所有数据实际上都是通过 storeClientCopy() 复制出来的,而不是引用。这有点愚蠢,因为数据几乎总是会被写到客户端的文件描述符上。
- 每次调用 storeClientCopy() 从内存缓存中获取数据时,实际上都会从内存对象的开头开始,沿着每个 4k 页面进行遍历,直到找到满足该副本的偏移量。对于小于 4k 几倍的对象来说,这没问题,但如果你运行一个几 GB 的 cache_mem,并且不介意在 RAM 中缓存大于 10MB 的对象,那么事情就会变得有点棘手。
🔗 与磁盘相关的方面
- 存储选择是在对象创建时做出的,这不是最佳时机。这意味着在完全从网络读取对象之前,你无法知道整个对象的大小。Swapout 应该被延迟,直到足够多的对象被读入内存以决定如何处理它。
- 哦,顺便说一句,将数据复制到一个或两个缓冲区然后再写入网络也有些愚蠢。如果代码可以直接访问对象内存并在成功将数据写入磁盘后返回,那会更好。我不知道 writev() 在这里会不会有奇效……
- 是的,以 4k 页面写入也有些愚蠢,特别是对于 COSS 这种我们可能拥有超过 4k 数据的情况。
🔗 怎么办
- 修改 store 客户端 API,不要将标头数据包含在 storeClientCopy() 可访问的区域中。
- 移除隐含的“如果你还没有开始读取服务器端,请开始读取并适当地填充
MemObject->reply中的标头”的逻辑,该逻辑由 storeClientCopy() 启动。将其替换为显式的“GrabReply”异步例程,该例程将执行上述操作(包括从磁盘读取对象数据),并返回回复状态+标头,以及任何可用的数据。 - 这应该意味着我们可以摆脱 seen_offset 机制。据我所知,这仅在尝试解析回复标头时使用。
- 一旦这部分完成(这是一项艰巨的工作!),再次修改 storeClientCopy() API,使其接受一个偏移量并返回一个 (mem_node + offset + size) 来提供所需数据。偏移量是必需的,因为 mem_node 可能包含已处理过的数据;大小是必需的,因为 mem_node 可能尚未填充。
- 一旦这部分完成(这又是另一大块工作!),考虑改变方式,使其不再需要不断地在内存对象中搜索。相反,我们应该分两步进行——一个 seek() 类型的调用来设置当前位置,然后返回页面。完整的页面,或者可能是一个 (mem_node + offset + size)。
- 然后,如果你觉得有能力,也许可以考虑使用可变大小的存储页面,而不是固定的 4k。如果我们知道回复很小(感谢 Content-Length,或者它能够放入一个 read() 缓冲区),那么只分配一个合适的大小。如果回复很大(例如,一个非常大的视频文件),则分配更大的页面。将 TX 套接字缓冲区快速填充会很好,而不是浪费超过 1 次通过 comm 循环来填充缓冲区。
- (如果我们真的能达到这一点,那么能够将 http 服务器端的 read() 缓冲区直接存储到 store 中,并附带相关的偏移量+大小信息(因为该缓冲区的一部分可能不是数据——可能是部分标头信息,可能是 HTTP/1.1 的拖车信息,可能是流中的 TE 相关元数据等),那将更有效率。)
- 如上所述,需要对 swapout 代码进行大量重构。
- 也许可以考虑允许磁盘对象再次成为热对象。考虑到这一点,现在这样做实际上并不“那么”难……不过需要考虑内存缓存的抖动。嗯,ZFS 的双 LFS 类型缓存替换策略(我记不起它的名字了!)以避免这种缓存抖动可能很有用。
🔗 长期注意事项
- 应该有一个中间层负责确定在哪里找到所需数据、刷新、Vary/ETag 等。
- 客户端 API 应呈现为两个数据流。一个流包含状态行和已解析的实体标头(逐跳标头应在协议端过滤),另一个流是稀疏字节流。稀疏是为了支持范围。也许也应该有一个 seek 函数,但由于中间层负责处理范围,所以并不真正需要。
- 存储 API 在读写时也应类似地拆分。这里读取时需要一个 seek 操作,以便能够收集所需的片段来构建范围响应。并且,如果标头可以独立于正文进行重写/更新,以正确支持刷新和将拖车标头作为对象标头的一部分进行存储,那将是有益的。
- 服务器端->中间层和中间层->客户端也需要支持中间 1xx 响应(例如 100 Continue)。这些响应在等待实际响应时只需要转发,永远不会存储。
分类: WantedFeature
导航:站点搜索,站点页面,类别,🔼 向上