58 同城搜索云 -- 云搜核心技术揭秘

58 同城搜索云 -- 云搜核心技术揭秘

来源: 58 技术
作者:卢克

导读 : 10 月 31 日-11 月 1 日,第三届 2020 AI Pioneer Con 技术峰会线下场在杭州成功举行。本次大会聚焦 AI 与 5G 最前沿技术及产业落地,邀请全球顶尖技术企业及全球开发者共同参与。

58 同城高级架构师卢克受邀在本次大会“AI 平台 + 数据科学”专场分享了《58 同城搜索云核心技术揭秘》。

本文根据此演讲实录整理而成,图文详实,欢迎阅读分享。

背景

58 同城是综合生活信息服务平台,主要业务包括房产、招聘、汽车、本地服务等。58 搜索承载着 58 列表页关键词搜索和点击筛选搜索的主要流量,早期的 58 搜索系统是基于 Solr 实现,随着业务和流量增长,Solr 的性能捉襟见肘。

2012 年底自研的 esearch 检索系统上线,近实时分布式搜索系统,很好的支撑了业务的发展。由于其出色的性能,在搜索内部开始广泛使用。

截止 2016 年,内部基于 esearch 检索系统,创建了 30 多个垂直搜索服务,如简历搜索、企业搜索等。站在服务的角度看,各个垂直检索系统性能出色,但站在业务的角度看,迭代还不够快,使用不够方便。

因为每当创建一个业务的搜索服务,都需要搜索开发同学先确认需求,如哪些索引字段,如何排序,然后在基础的搜索系统上适配/构建某个业务的检索服务,业务上线后还需监控和运维等,创建一个业务的搜索服务大概需要一个月时间。

但这些垂搜检索服务,其实逻辑上大同小异,底层都是统一个检索系统,系统整体架构基本一样。为了方便用户更快的接入搜索,服务更多的业务需求,需要在平台化和服务效率上更进一步,基于以上需求,58 云搜索平台(内部简称 WCS)应运而生。

58 云搜是 58 搜索团队基于 k8s(即 kubernetes,下同)和 docker 技术,以及内部的搜索引擎实现的全自助化的搜索服务平台,为各业务开发者提供实时索引、动态摘要、自定义排序、运营监控、运维托管等全套解决方案。使用方仅需通过界面配置索引结构、API 上传源数据两个步骤,即可在数分钟内构建完成一个自有的搜索服务。

2017 年上线至今,已经接入了两百多个搜索业务,实现了为业务快速创建搜索服务。使用方如果需要创建一个搜索服务,只需要登录云搜的管理系统,填写目标流量和索引字段等信息,云搜系统收到请求后,即自动拉取相应镜像进行远程部署,随后一个搜索实例即可上线使用。


云搜整体架构

下面是云搜的整体系统架构。首先,云搜采用 k8s 实现资源管理和自动调度,k8s 是整个系统的大脑。云搜所有的服务都是运行在容器中,通过容器的方式自动调度和维护,k8s 自身也是运行在容器之中。

图中红色方框部分即 k8s 和云搜管理中心,它控制索引创建和管理各服务模块。其次,对外提供统一的接入 API,和系统自助管理功能,屏蔽了内部一个个独立的索引实现,方便用户使用。

第三,在离线索引过程,引入 hdfs 和分布式消息队列等外部通用的基础服务,存储源数据和实现索引流程,简化了系统设计,同时也保证了源数据的可靠性,相当于源数据做了离线备份。

k8s 是整个系统的核心,所以保证 k8s master 系统的高可用非常重要。

云搜的 k8s master 是 3 副本冗余,k8s master 所有的信息都是存储在 etcd 集群中,关于 etcd 部分不单独介绍。

云搜如何实现 master 多副本的负载均衡和高可用性?

我们使用了 haproxy 和 keepalived 两个开源组件,以及虚 ip 技术来做 load balance,实现 master 节点的负载均衡和高可用。

haproxy 是一个高可靠性代理软件,使用它实现对 API server 的负载均衡和流量转发,所有访问 API server 的请求,都会经过 haproxy 进行代理转发。keepalived 是一个高可用的路由软件,用它来对 haproxy 做健康检查和故障切换。集群中的所有 node 节点通过 vip 访问 haproxy,keepalived 负责抢占 vip,两个 keepalived 互为主备,正常情况都会抢占 vip 成功,两个 haproxy 都会均匀的获得流量。当一个 haproxy 节点故障时,keepalived 检测到 haproxy 故障,自动漂移 vip,这样故障 haproxy 上即没有了流量,实现自动故障切换。

随着云搜规模的增长,我们发现开源的网络组件 flannel 有一个 bug,只能支持 100 台物理机,无法进行再扩容。同时我们也考虑到 k8s master 出现故障,整个云搜将出现不可用。

由于当时集群联邦的模式还不可用,我们构建了完全独立的多 k8s 集群来提升系统的可用性。多集群的模式,我们的架构主要的升级点是把原来的 API 层拆分为两层,顶层 API 根据业务 id 把真正的流量转发到各个集群,实现了多集群的升级过程对业务无感知。构建多集群的另外一个好处就是,我们能够通过迁移一个集群的服务到另外一个集群实现滚动升级,这对快速更新的 k8s 非常重要。

截止目前我们已经进行了 2 轮的迁移服务,滚动升级 k8s 或 Linux 内核。

image.png

自动索引流程和故障恢复

我们简单介绍下一般的检索系统是什么样,有了这个基础然后再详细介绍,在环境下怎么构建搜索系统。

一般来说,搜索系统都包含两部分,离线倒排索引构建和在线检索服务

在离线阶段批量的获取所有的文档,通过索引程序生成倒排索引文件,然后把倒排索引文件推送给在线检索程序去加载,检索程序加载倒排文件后对外提供检索服务。离线构建的倒排索引是静态的不能被改变的,但在很多的应用场景中,文档会有实时的更新或者新增,所以会启动一个实时的索引流程,在检索服务内部实时构建小的倒排索引,即增量索引,全量索引和增量索引合在一起,实现了索引的完整性。

image.png

有了前面的基础,我们现在来介绍下,如何利用 k8s 和容器技术,自动化的创建检索服务,让复杂的检索系统通过 k8s 实现自动运维。搜索是有状态的服务,它有如下 3 个特征:(1)服务存储持久化数据,(2)数据有分片,不同分片包含不同数据,(3)数据在持续更新。



所以我们核心问题就是,实现检索服务的容器化部署,同时保证容器中检索服务中的数据的正确性和完整性,从而实现服务的可用性。我们的实现方案是利用 k8s 一个 pod 内部可包含多个容器的特性,多容器分工合作实现自动构建检索服务。

首先,一个检索 pod 启动后,里面是没有倒排索引数据,相应检索服务也还运行,这时先启动初始化容器,初始化容器完成本服务参数配置,同时根据自己分片拉取倒排数据文件,完成配置和数据的初始化。

有了倒排索引数据,第二步启动主检索容器,加载索引文件启动服务。第三步再启动一个数据更新容器,从外部消息队列中,拉取属于本检索分片的数据,发给检索进程实时构建索引,实现数据更新。

3 个容器配合实现了检索服务的自动构建,有状态的服务转换成了无状态的实现,整个实现过程对原来索引、检索模块和 k8s 无侵入、无感知。

扩容过程类似,当需要扩容时,只需在 k8s 中增加副本数,k8s 自动生成新的检索 pod,扩容出来的检索 pod 也是没有数据,这是由初始化容器从其他分片同步过来相应的倒排数据(这样效率更高),然后启动检索服务容器和数据更新容器,实现了扩容副本和原副本的数据最终一致。

再来看下云搜如何实现自动故障恢复。

如果一个物理机出现了故障,该机器上的服务都不可用了。系统的 liveness 探针发现该 pod 不再存活,这个应用可用的 pod 数目和系统设定的数目差了一个,于是就在其他物理机上启动了新的 pod。

新的 pod 启动后里面的索引数据还不完整,不能对外提供服务,pod 内部独立的容器会从其他正常节点和消息队列同步数据,数据同步完成后,k8s 通过 readiness 探针发现 pod 准备就绪,于是将新启动 pod 加入到服务中,完成故障恢复。可以看到,扩容过程本质上和故障自动恢复一样。

从自动部署和故障恢复,我们就可以看到利用 k8s 实现自动运维的优势和便利性。

image.png

服务质量和资源利用优化

前面也提到了,检索系统包含离线全量倒排索引构建和在线检索服务两个过程。实际线上出现离线全量建索引过程占用资源高,影响在线查询服务的情况。究其原因是调度器非搜索专用,并不知道是在线还是离线。

为了保证在线服务的质量,我们将在线 pod 和离线 pod 进行隔离,不让两种类型的 pod 调度到同一个 node 上。

解决方案,给 node 节点分别打上在线和离线的标签,pod 也打上标签,利用 pod 和 pod 之间的亲和力机制,离线 pod 调度到离线机器,在线 pod 调度到在线机器上。

有时,我们会收到个别机器资源利用率过高的报警,追查之后发现是高流量 pod 堆积在一台节点,造成资源利用率不均。这是因为 scheduler 不了解实时流量,不保证大量 pod 不堆积 。

这里的解决方案,使用了 pod 之间的反亲和力机制,给部分流量比较大的业务打上标签,让大流量 pod 调度到不同机器上。

前面介绍的两种优化的机制,有时候还不能满足我们的需求。

实际线上的情况,仍然会出现会调度到高负载机器上,有的机器负载比较低但是却没有调度过去,造成资源利用不均衡。其原因是调度器是根据已分配出去的资源,来决定新的调度。

但实际情况是,线上大部分业务申请了资源,实际上使用不了那么多,所以分配出去的资源量是不准确的,并不代表真实的使用量。为了解决这个问题,我们决定自定义调度器。

调度器根据特定的调度算法将 pod 调度到合适的 node 节点上去,调度过程看来好像比较简单,但在实际需要考虑的因素很多,非常复杂。

我们采用了调度框架 + 自定义插件方式。k8s 提供可插拔架构的调度框架,使得定制调度器这个任务变得更加的容易。调度框架大概是这样:整个调度分为调度过程和绑定过程,这两个过程合在一起,称之为调度上下文。

调度过程为 pod 选择一个合适的节点,绑定过程则将调度过程的决策应用到集群中(也就是在被选定的节点上运行 pod)。这两个阶段都定义了一些扩展点,用户可以实现扩展点定义的接口来定义自己的调度逻辑。遇到对应的扩展点时,将调用用户注册的扩展。

经过分析,我们选择在打分这个扩展点上定义自己的扩展。根据打分的分值对节点进行排序,选择最合适的 node 节点进行调度。所以这里我们使用了真正的 CPU 和内存使用情况来打分。

刚才说了整个调度过程非常复杂,为了减少影响,保证上线的稳定性,我们选择只针对部分业务应用自定义调度,同时只修改了打分这一个地方。相当于是一个灰度控制,有效保障了自定义调度的安全性。

总结

58 云搜的整体实现,是利用了容器编排和调度技术,在不侵入原有的检索系统(esearch)情况下,将有状态的检索模块转换为无状态的 pod 或者容器化部署,实现检索系统的自动创建和自动维护,极大节省了人力,快速创建一个搜索服务,实现了业务的快速接入,体现了云技术对搜索效率的提升。

同时在云环境下,不断提升服务整体可用性,优化编排调度,不断提升系统资源利用。目前 58 云搜平台已经接入内部搜索业务 200 多个,日均查询量 70 亿,峰值 QPS15 万,我们将持续迭代升级云搜功能,为 58 业务创造价值。

作者介绍:

分享嘉宾:卢克,58 同城搜索技术负责人,高级架构师,10 年专注搜索引擎领域相关技术。2015 年 7 月加入 58 同城,曾带领团队从 0 到 1 构建 58 云搜索平台,目前致力于查询意图理解、自然语言处理技术和搜索引擎,主要负责 58 搜索召回和排序的体验优化,58 云搜索平台建设。曾任腾讯、搜狗高级工程师,参与搜索引擎的研发。2009 年硕士毕业于哈尔滨工业大学,研究方向为自然语言处理。


本文地址:https://www.6aiq.com/article/1604951187388
本文版权归作者和AIQ共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出