搜索在线服务的存储计算分离

背景 随着网络和存储硬件向着高吞吐低延迟的方向不断发展,存储计算分离成为了集团的一个重要技术方向,在节约成本、简化运维、提高混布能力有着重要的作用:

  1. 无需或者少量分发索引,使在线服务可以快速迁移,从而提升容灾效率。
  2. 对于索引数据较大,访问集中的业务,使用存储计算分离后,可以突破单机内存和磁盘空间限制、并释放大量内存。
  3. 将在线服务的 IO 隔离拆到专有的存储集群,降低了在线服务调度维度。
  4. 计算节点不再需要考虑存储容量与内存容量的比例。计算节点与存储节点使用不同的服务器硬件,并独立地进行定制和发展。
  5. 多个节点上的存储资源能够形成单一的存储池,这能降低存储空间碎化、节点间负载不均衡和空间浪费的风险,存储容量和系统吞吐量也能容易地进行水平扩展。
  6. 数据与计算分离,使在线服务的全图化、算子化更易实现。
现状 搜索在线服务的存储计算分离
文章图片

在管控系统的统一管理下,先是由 BuildService 产出索引到离线 DFS。再经由 DeployExpress 服务,将索引以链式分发的形式复制到所有 Searcher 上,并被 Searcher 加载,以提供服务。搜索的几大引擎,HA3、DII、IGraph,都采用了这种架构。在这个框架下:
  1. 索引分发一般时间较长,特别是长尾业务,制约了 Searcher 的迁移和扩容效率。
  2. 索引分发还会产生大量 IO,由于 SSD 写对读的影响较大,而像IGraph、OpenSearch 这些业务都会在查询过程中访问磁盘,写盘会造成这些业务的抖动,为了避免这种情况,需要进行 IO 隔离和限速,这又大大增加了索引分发的时间。
  3. 为了能够较快的分发索引,一些数据量较大的业务,不得不将索引分成多个 Partition,分发到不同的实例上去。对于表较多,且有一定关联的业务,这会使得他们不得不巧妙设计分列方式,以保证查询性能,使得广播查询不可避免。
  4. 如果索引很大,还要考虑磁盘与内存的比例,增加运维复杂度。
存储计算分离架构 搜索在线服务的存储计算分离
文章图片

这是在线服务的存储计算分离架构,主目标对象是那种索引很大,冷数据很多,Cache 可以挡住大部分的查询的业务场景,这类业务,普遍存在于 IGraph、OpenSearch、DII。这些业务虽然使用了不同的引擎来实现,受益于统一服务框架,只在底层和管控系统进行了改造就得以实现。下面先介绍一下整体架构设计:
  1. 索引仍然由 BuildService 产出到离线 DFS,这个 DFS 可以是盘古、也可以是 HDFS,可以使用机械盘,降低成本。
  2. 相比于分离前,去除了 DeployExpress 链式分发,取而代之的是 Madrox 分布式索引同步服务。
  3. 本地磁盘也被移除,当然,实际情况还是会有一块盘的,配置、日志、Binary、甚至实时落盘,还是要用的。
  4. 索引不再分发到在线 Searcher,而是在线的存储集群:盘古。出于稳定性和性能考虑,同时部署了两套盘古,内容一致,互为备份。
  5. 索引文件以 4KB 为单位划分为若干 Block,查询在需要访问索引时,会先检查 BlockCache 是否存在需求的 Block。仅当 BlockCache 不命中的情况时,才会产生真正的 IO 操作,通过网络读取盘古上的索引文件,并会更新 BlockCache。
由于没有了索引到本地磁盘的分发过程,Searcher 变得轻快无比,想飞到哪就飞到哪。解决了快速迁移和扩容的问题。也没有了本地磁盘 IO 的隔离问题,当然,在线盘古上现在要考虑这个问题了,好在,盘古可以做单盘写限速和 QoS,此外 Madrox 做为唯一的写者,也可以调节并发来控制总量。计算与存储机型可以分开演进,成本自然的下降,存储节点也会形成统一的存储资源池,提高利用率。大索引单 Partition 成为可能,不再受单机容量限制。
盘古集群 搜索在线服务的存储计算分离
文章图片

盘古提供了一种称为 FlatLogFile 的?件接口,对用户呈现顺序写和随机读的语义,类似于本地文件系统的流,不支持随机写。这与我们的全量/增量索引文件用法完全契合,我们的全量/增量索引文件采用的是 Append Only 方式写出,产出后立即封闭,不会再进行任何修改。读取则是以随机方式为主,顺序读取方式为辅。
这使得我们不依赖于块设备,也就不需要使用盘古的 BlockServer,而直接访问 ChunkServer,这样可以节省 BlockServer 到 ChunkServer 的一跳网络延迟开销,以及 BlockServer 的资源开销。
借助 FSLIB 的插件式设计,我们将盘古提供的 Clinet SDK 封装成 FSLIB 的插件,使得在线服务可以直接访问盘古集群。
双盘古集群 这里解释一下双盘古集群设计的考虑。
  • 稳定性
双盘古互为备份,内容一致,是容灾上的经典解决方案。盘古承诺 99.95% 的稳定性,已经很高了,但搜索在线服务有自己更高的要求。分离之前,索引被分发到本地磁盘,离线索引不可访问,并不会影响到在线服务,一台 Searcher 的问题,不会影响到整体服务。但在存储计算分离的架构下,一但在线盘古故障,不能快速恢复,在线业务就会瘫痪,影响是不敢想象的。单一盘古集群,极容易受到,人为误操作、升级过程中的意外和程序问题的影响,双盘古集群可以有效避免这些意外的发生。
  • 低延迟需求
由于经过了网络和磁盘,长尾是不可避免的。如果我们两个盘古同时发出请求,取最先回来的结果,可以拿到更低、更平稳的延迟。这就是我们的双读盘古策略,主要用于 Query 触发的 IO 请求。
  • 服务能力
有些索引为了性能,需要采用全内存加载方式,如:PrimaryKey 和 Attribute。在那些行数较多的业务中,Open阶段,会同时顺序大块读取相同的文件,造成某些盘压力过大。双盘古轮询策略可以起到分担的作用。这就是我们的单读盘古策略,主要用于全内存索引文件 Open 时的,连续大块一次性读取。
  • 副作用:成本问题
双盘古带来的主要问题是存储成本的上涨,为此我们采用了每盘古 2 副本的方案,加一起 4 副本,相较于单盘古 3 副本,只多 1 个副本的开销。
未来,我们还会采用 EC(Erasure Coding)的方案来进一步降成本。默认 8+3 的 EC 配置将存储的成本降到约 1.375 份,即双盘古 2.7 份。
Madrox 分布式索引分发服务 搜索在线服务的存储计算分离
文章图片

Madrox 用来解决索引从离线 DFS 到在线盘古的分发问题的。
有两个角色:一个是 API Server,也就是 Master,接受管控系统的分发请求,将索引文件的分发任务派给各个 Worker 也就是 Slave,并收集 Slave 的执行情况,以便反馈给管控系统;另一个是Slave,负责将指定的文件复制到两个盘古之上,并通过心跳汇报进度。
这个服务在设计上重点考虑了以下两个因素:
  1. 有限且昂贵的机房间长传带宽
  2. 有限的盘古集群的出带宽
出于这样的限制,slave 被设计到了计算节点上,并且采取的是一读多写的方式:
  • 一读:保证数据在长传带宽上只出现一次。即 Clinet 只从离线 DFS 上读取一次数据。
  • 多写:采用星型写,即从 Client 写到两个盘古的多个 ChunkServer。这样,写出时只会占用盘古的入带宽。
如果采用盘古集群对拷方式,双盘古要么读取离线 DFS 两次,要么链式复制占用其中一个盘古集群的出带宽。
【搜索在线服务的存储计算分离】此外,Madrox 还要考虑一些问题:
  • 大文件并发:需要盘古能提供小文件合并大文件,或者 Chunk 级复制的能力。目前,我们只能通过修改索引结构、增量 Segment 等方式实现。
  • 仅同步有效数据:因为离线 DFS 索引目录中存在大量的中间及不需要分发的文件。粗暴的目录树同步,会浪费带宽和延长分发时间。
  • 双盘古并行写
分析与优化 在线服务不同于离线,延迟极为敏感,对长尾延迟也有很高的要求。我们的目标是:从网络发出请求到收回数据包的平均延迟在 500us 以内,99.9% 的分位数 1ms 以内。对网络、磁盘、盘古软件栈、都提出了很高的要求。平均延迟比较容易达到,长尾延迟这一目标还在努力中。
延迟分析 一次 IO 的延迟开销,主要有这样几个因素:
  1. 读取磁盘,对于 NVME 的 SSD 来说,平均大约在 100us+,受写影响,大块读影响明显。
  2. 网络,对于内核态 TCP 协议栈来说,往返平均大约在 120us+,受网络波动、丢包、交换机排队、机架位置等因素影响严重。
  3. ChunkServer 软件栈、Client 软件栈、线程切换等,平均大约在 50us+,受 IOPS 量影响明显。
合并读 前面说过,索引文件是按照 4KB Block 来组织的。实际使用中,是难以保证一次请求的全部数据都落在一个 4KB Block 中的。如果强制要求每篇文档的索引数据都按照 4KB 对齐,必然造成大量索引空洞,空间浪费大,格式也会不兼容。因此,跨 Block 的请求、超过 4KB 的大块请求是必然存在的,这些都会产出两次或者两次以上的 IO 请求。
为此我们引入了合并读,即一次请求全部所需的 Block,这样可以节省发送请求的网络及磁盘时延开销。
与此同时,我们还通过改进索引查询流程,引入了预取技术,当一个 Block 需要发送 IO 请求时,先分析出其后续的 Block 是否很快被访问到,如果是的,通过合并读,夹带回来,放进 BlockCache 中,这样下次访问时,避免了再次发送 IO 请求。
异步并发读 相较于同步的串行 IO,异步对提升 IOPS,降低延迟的效果是显而易见的。但由于需要对引擎的查询流程做较大变更,还在规划中,很快就会开始实施。
丢包造成的长尾 丢包的直接表现就是长尾的出现,丢包的因素有很多,比如:
  • 网络设备的硬件问题:网卡、光模块、网络等不时的抖动
  • 交换机连接口的 outdrop:
    • 接口有突发流量,导致交换机buffer打满,这种是交换机硬件架构影响。
    • 访问流量是多打一,也就是多台机器同时访问一台机器时。
    • 万兆的机器访问千兆机器,很容易有outdrop。
    • 报文从高速链路进入交换机,由低速链路转发出去;或者报文从相同速率的多个端口同时进入交换机,由一个相同速率的端口转发出去。
  • 服务器负载、内核参数不恰当等软件问题
丢包在网络上是难以避免的,特别是 TCP 的网络环境,有个不知道哪来的数字,0.03% 的丢包率都是正常的。
对于丢包,我们只能通过重传来解决,当然,有些时候换一台计算节点更加明智。在分析了 tcpdump 的结果后发现,内核默认的重传间隔是 200ms,这源自 rto_min 参数的默认值 200ms。也就是请求发出后,如果数据包丢失,重发请求已经是 200ms 以后的事了。这时 Query 早已经超时返回了。
通过修改 rto_min=10ms 有效的缓解了客户端请求丢包的情况,将重传时间缩小到 10ms。这里贴一张修改 rto_min 前后的对比图,中间密集的部分,是默认 rto_min=200ms 情况。两边是 rto_min=10ms 的情况。可见长尾延迟有明显的降低。当然代码就是重传率明显上升。
搜索在线服务的存储计算分离
文章图片

也可以看出,rto_min 没能完全解决问题,还有一些情况,比如数据包被网络延迟,甚至重传的包也都延迟超过50ms,rto_min也是无能为力的。重传也会造成网络的进一步恶化。
冷热表分离 在实际的使用当中,我们发现,纯粹的存储计算分离,完全依靠 SearchCache/BlockCache 的方案,存在如下问题:
  1. Cache 填充慢、预热时间过长,且受 QPS 影响,如果 QPS 较低,可能需要几十分钟。
  2. 冷启动时,命中率低,延迟过高
  3. 冷启动时,盘古集群压力过大
  4. 缺少降级方案,一旦网络或者盘古故障,服务能力完全丧失。
为解决这些问题,我们在引擎的全图片版本上,实现了冷热表分离方案。
冷热表分离是指,根据提前设定好的规则,在离线构建索引的时候,就将文档分成冷、热两张数据表。热表采用不分离架构,索引分发到本地;冷表则会采用分离架构,在不能命中 SearchCache/BlockCache 的情况下,直接访问盘古的一个折中方案。可以简单的把热表理解成一种静态 Cache,如果规则合理,这个 Cache 可以挡住大部分的访问,极大的降低对存储集群的依赖,从而大幅提高在线服务的性能,并降低延迟。
如果设定规则,是这个方案的难点。
对于主搜索 Summary,由于数据已经做了 Excellent/Good/Bad 的分层,得益于一阶段的查询模式,使用 Excellent 和 Good 做热表,分得能够获得 90%+ 和 98%+ 的命中率。极大的降低了对存储集群的访问压力
对于海神,起初简单的按 KKV 索引的 SKey 链长进行划分,获得了近 80% 的命中率,但热表索引也较大,后来通过与算法团队合作,寻找到了更低索引量情况下,超过 90% 命中率的规则。
冷热分离方案,不仅降低了在线存储集群的访问压力,同时还是一个可靠的降级方案。
分布式Cache 对于行数很多的头部业务,在冷启动时,会对在线存储集群造成较大的冲击,且读取的数据相对集中,由于存储集群规模较小,可能会耗尽存储集群的可用带宽。因此,在计算集群中部署一套分布式Cache,是一个不错的思路。副作用是对于不能命中分布式 Cache 的请求,会增加网络跳数,造成延迟上涨。
展望 搜索在线服务的存储计算分离,还有很长的路要走。特别是需要继续探索,降低存储集群的平均延迟,长尾延迟,提高 IOPS 能力等方面。用好 100G 网络、多核 CPU,引入用户态 TCP、甚至是 RDMA。拓展应用场景,服务好长尾业务。
我们相信,存储计算分离会对搜索架构产生深远的变革性影响。

    推荐阅读