写点什么

百度搜索稳定性问题分析的故事(上)

发布于: 5 小时前
百度搜索稳定性问题分析的故事(上)

导读:百度搜索系统是百度历史最悠久、规模最大并且对其的使用已经植根在大家日常生活中的系统。坊间有一种有趣的做法:很多人通过打开百度搜索来验证自己的网络是不是通畅的。这种做法说明百度搜索系统在大家心目中是“稳定”的代表,且事实确是如此。百度搜索系统为什么具有如此高的可用性?背后使用了哪些技术?以往的技术文章鲜有介绍。本文立足于大家所熟悉的百度搜索系统本身,为大家介绍其可用性治理中关于“稳定性问题分析”方面使用的精细技术,以历史为线索,介绍稳定性问题分析过程中的困厄之境、破局之道、创新之法。希望给读者带来一些启发,更希望能引起志同道合者的共鸣和探讨。


第 1 章 困境

在大规模微服务系统下,如果故障未发生,应该归功于运气好。但是永远不要指望故障不发生,必须把发生故障当作常态。从故障发生到解除过程遵循的基本模式抽象如下。

可用性治理主要从这 3 个角度着手提升:1. 加强系统韧性;2. 完善止损手段,提升止损有效性,加速止损效率;3. 加速原因定位和解除效率。

以上 3 点,每个都是一项专题,限于篇幅,本文仅从【3】展开。

百度搜索系统的故障原因定位和解除,是一件相当困难的事情,也可能是全公司最具有挑战性的一件事情。困难体现在以下几个方面。

极其复杂的系统 VS. 极端严格的可用性要求

百度搜索系统分为在线和离线两部分。离线系统每天从整个互联网抓取资源,建立索引库,形成倒排、正排和摘要三种重要的数据。然后,在线系统基于这些数据,接收用户的 query,并以极快的速度为用户找到他想要的内容。如下图所示。

百度搜索系统是极其庞大的。让我们通过几个数字直观感受一下它的规模:

百度搜索系统的资源占用量折合成数十万台机器,系统分布在天南海北的 N 大地域,搜索微服务系统包含了数百种服务,包含的数据量达到数十 PB 级别,天级变更次数达到数十万量级,日常的故障种类达到数百种,搜索系统有数百人参与研发,系统每天面临数十亿级的用户搜索请求。

虽然系统是超大规模,但是百度对可用性的要求是极其严格的。百度搜索系统的可用性是在 5 个 9 以上的。这是什么概念呢?如果用可提供服务的时间来衡量,在 5 个 9 的可用性下,系统一年不可用时间只有 5 分钟多,而在 6 个 9 的可用性下,一年不可用的时间只有半分钟左右。所以,可以说百度搜索是不停服的。

一个 query 到达百度搜索系统,要经历上万个节点的处理。下图展示了一个 query 经历的全部节点的一小部分,大概占其经历节点全集的几千分之一。在这种复杂的路径下,所有节点都正常的概率是极其小的,异常是常态。

复杂的系统,意味着故障现场的数据收集和分析是一项浩大的工程。

多样的稳定性问题种类

百度搜索系统向来奉行“全”、“新”、“快”、“准”、“稳”五字诀。日常中的故障主要体现在“快”和“稳”方面,大体可归为三类:

  1. PV 损失故障:未按时、正确向用户返回 query 结果,是最严重的故障。

  2. 搜索效果故障:预期网页未在搜索结果中展现;或未排序在搜索结果的合理位置;搜索结果页面响应速度变慢。

  3. 容量故障:因外部或内部等各种原因,无法保证系统高可用需要的冗余度,甚至容量水位超过临界点造成崩溃宕机等情况,未及时预估、告警、修复。

这些种类繁多、领域各异的问题背后,不变的是对数据采集加工的需求和人工分析经验的自动化抽象。

第 2 章 引进来、本土化:破局

在 2014 年以前,故障原因定位和解除都在和数据较劲,当时所能用到的数据,主要有两种。一是搜索服务在线日志(logging);二是一些分布零散的监控(metrics)。这两类数据,一方面不够翔实,利用效率低,问题追查有死角;另一方面,对它们的使用强依赖于人工,自动化程度低。以一个例子说明。

拒绝问题的分析首先通过中控机上部署的脚本定时扫描线上服务抓取单 PV 各模块日志,展现到一个拒绝分析平台(这个平台在当时已经算是比较强大的拒绝原因分析工具了)页面,如下图所示;然后人工阅读抓取到的日志原文进行分析。这个过程虽然具有一定的自动化能力,但是 PV 收集量较小,数据量不足,很多拒绝的原因无法准确定位;数据平铺展示需要依赖有经验的同学阅读,分析效率极其低下。

在问题追查死角和问题追查效率上,前者显得更为迫切。无死角的问题追查呼吁着更多的可观测数据被收集到。如果在非生产环境,获取这些数据是轻而易举的,虽然会有 query 速度上的损失,但是在非生产环境都能容忍,然而,这个速度损失的代价,在生产环境中是承受不起的。在理论基石《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》的指导下,我们建设了 kepler1.0 系统,它基于 query 抽样,产出调用链和部分 annotation(query 处理过程中的非调用链的 KV 数据)。同时,基于业界开源的 prometheus 方案,我们完善自己的 metrics 系统。它们上线后立即产生了巨大的应用价值,打开了搜索系统可观测性建设和应用的想象空间。

2.1 kepler1.0 简介

系统架构如下图所示。

阶段性使命:kepler1.0 在于完善搜索系统的可观测性,基于开源成熟方案结合公司内组件实现从 0 到 1 的建设,快速完成可观测性能力空白的补齐,具备根据 queryID 查询 query 处理过程的调用链以及途径服务实例日志的能力。

引进来:从 kepler1.0 的架构不难发现,它从数据通路、存储架构等方面完整的参考 zipkin

本土化:引进 zipkin 时数据采集 sdk 只支持 c++,为了满足对非 c++模块的可观测性需求,兼顾 sdk 的多语言维护成本以及 trace 的侵入性,采用了常驻进程通过日志采集输出格式和 c++ sdk 兼容的 trace 数据,即图中的日志采集模块。

2.2 通用 metrics 采集方案初步探索

系统架构如下图所示。

阶段性使命:2015 年前后搜索开始探索大规模在线服务集群容器化混部技术,此时公司内的监控系统对多维度指标汇聚支持较弱,基于机器维度指标的传统容量管理方式已经难以满足容器化混部场景的需求。

引进来:将开源界成熟的 metrics 方案引入搜索在线服务混部集群,实现了符合 prometheus 协议的容器指标 exporter,并依托 prometheus 的灵活多维度指标查询接口以及 grafana 丰富的可视化能力,建设了搜索在线业务混部集群容量管理依赖的底层数据系统。

本土化:容器指标 prometheus-exporter 和搜索在线 PaaS 系统深度对接,将服务元信息输出为 prometheus 的 label,实现了容器元信息的指标索引和汇聚能力,满足容器化混部场景下容量管理的需求。指标和 PaaS 元信息关联是云原生 metrics 系统的初步探索主要成果。

2.3 应用效果初显

场景 1:拒绝、效果问题

阶段性痛点:人工分析强依赖日志,从海量调用链、日志数据中精确检索出某些特定 query,通过 ssh 扫线上机器日志效率很低,且对线上服务存在 home 盘 io 打满导致稳定性风险。

解决情况:对命中常态随机抽样拒绝问题、可复现的效果问题开启强制抽样采集,通过 queryID 直接从平台查询调用链及日志用于人工分析原因,基本满足了这个阶段的 trace 需求。

场景 2:速度问题

阶段性痛点:仅有日志数据,缺乏调用链的精细时间戳;一个 query 激发的调用链长、扇出度大,日志散落广泛,难收集。通过日志几乎无法恢复完整的时序过程。这导致速度的优化呈现黑盒状态。

解决情况:补全了调用链的精细时间戳,使 query 的完整时序恢复成为可能。通过调用链可以查找到程序层面耗时长尾阶段或调度层面热点实例等优化点,基于此,孵化并落地了 tcp connect 异步化、业务回调阻塞操作解除等改进项目。

场景 3:容量问题

阶段性痛点:多维度指标信息不足(缺少容器指标、指标和 PaaS 系统脱节);缺少有效的汇聚、加工、组合、对比、挖掘以及可视化手段。

解决情况:建设了搜索在线的容器层面多维度指标数据采集系统,为容器化的容量管理应用提供了重要的基础输出来源,迈出了指标系统云原生化探索的一步。下图为项目上线后通过容器指标进行消耗审计功能的截图。

第 3 章 创新:应用价值的释放

虽然 kepler1.0 和 prometheus 打开了可观测性建设的大门,但是受限于能力,已经难以低成本地获取更多的使用价值了。

3.1 源动力

基于开源方案的实现在资源成本、采集延迟、数据覆盖面等方面无法满足搜索服务和流量规模,这影响了稳定性问题解决的彻底性,特别是在搜索效果问题层面表现尤为严重,诸如无法稳定复现搜索结果异常问题、关键结果在索引库层面未预期召回问题等。

稳定性问题是否得到解决永远是可观测性建设的出发点和落脚点,毫不妥协的数据建设一直是重中之重。从 2016 年起,搜索开始引领可观测性的创新并将它们做到了极致,使各类问题得以切实解决。

3.2 全量采集

因为搜索系统规模太庞大,所以 kepler1.0 只能支持最高 10%的采样率,在实际使用中,资源成本和问题解决彻底性之间存在矛盾。

(1)搜索系统大部分故障都是 query 粒度的。很多 case 无法稳定复现,但又需要分析出历史上某个特定 query 的搜索结果异常的原因。让人无奈的是,当时只有备份下来的日志才能满足任一历史 query 的数据回溯需求,但它面临收集成本高的难题;另外,很多 query 没有命中 kepler1.0 的抽样,其详细的 tracing 数据并未有被激发出来,分析无从下手。能看到任一历史特定 query 的 tracing 和 logging 信息是几乎所有同学的愿望。

(2)公司内部存储服务性价比较低、可维护性不高,通过扩大采样率对上述问题进行覆盖需要的资源成本巨大,实际中无法满足。

对于这个矛盾,业界当时并没有很好的解决方案。于是,我们通过技术创新实现了 kepler2.0 系统。系统从实现上将 tracing 和 logging 两种数据解耦,通过单一职责设计实现了针对每种数据特点极致优化,以极低的资源开销和极少的耗时增长为成本,换取了全量 query 的 tracing 和 logging 能力,天级别数十 PB 的日志和数十万亿量级的调用链可实现秒查。让大多数故障追查面临的问题迎刃而解。

3.2.1 全量日志索引

首先,我们介绍全量日志索引,对应于上图中日志索引模块。

搜索服务的日志都会在线上机器备份相当长一段时间,以往的解决方案都着眼于将日志原文输出到旁路系统,然而,忽略了在线集群天然就是一个日志原文的现成的零成本存储场所。于是,我们创新的提出了一套方案,核心设计理念概括成一句话:原地建索引。

北斗中通过一个四元组定义一条日志的索引,我们叫做 location,它由 4 个字段组成:ip(日志所在机器)+inode(日志所在文件)+offset(日志所在偏移量)+length(日志长度)。这四个字段共计 20 字节,且只和日志条数有关,和日志长度无关,由此实现对海量日志的低成本索引。location 由 log-indexer 模块(部署在搜索在线服务机器上)采集后对原始日志建立索引,索引保存在日志所在容器的磁盘。

北斗本地存储的日志索引逻辑格式如下图所示。

查询时,将 inode、offset、length 发送给索引 ip 所在的机器(即原始日志所在机器),通过机器上日志读取模块,可根据 inode、offset、length 以 O(1)的时间复杂度定点查询返回日志原文,避免了对文件的 scan 过程,减少了不必要的 cpu 和 io 消耗,减小了日志查询对生产环境服务稳定性的影响。

同时,除了支持 location 索引以外,我们还支持了灵活索引,例如将检索词、用户标识等有业务含义的字段为二级索引,方便问题追查时拿不到 queryID 的场景,可支持根据其他灵活索引中的信息进行查询;在索引的使用方式上,除了用于日志查询以外,我们还通过索引推送方式构建了流式处理架构,用于支持对日志流式分析的应用需求。

这里还有一个问题:查询某一 query 的日志时,是不是仍然需要向所有实例广播查询请求?答案是:不会。我们对查询过程做了优化,方法是:通过下文介绍的 callgraph 全量调用链辅助,来确定 query 的日志位于哪些实例上,实现定点发送,避免广播。

3.2.2 全量调用链

在 dapper 论文提供的方案中,同时存在调用链和 annotation 两种类型的数据。经过重新审视,我们发现,annotation 的本质是 logging,可以通过 logging 来表达;而调用链既可以满足分析问题的需要,又因为它具有整齐一致的数据格式而极易创建和压缩,达到资源的高性价比利用。所以,callgraph 系统(kepler2.0 架构图中红色部分)就带着数据最简、最纯洁的特点应运而生。全量调用链的核心使命在于将搜索全部 query 的调用链数据在合理的资源开销下存储下来并高效查询。

在 tracing 的数据逻辑模型中,调用链的核心元素为 span,一个 span 由 4 部分组成:父节点 span_id、本节点 span_id、本节点访问的子节点 ip&port、开始 &结束时间戳。

全量调用链核心技术创新点在于两点:(1)自研 span_id 推导式生成算法,(2)结合数据特征定制压缩算法。相比 kepler1.0,在存储开销上实现了 60%的优化。下面分别介绍这两种技术。

3.2.2.1 span_id 推导式生成算法

说明:下图中共有两个 0 和 1 两个 span,每个 span 由 client 端和 server 端两部分构成,每个方框为向 trace 系统的存储中真实写入的数据。

左图:kepler1.0 随机数算法。为了使得一个 span 的 client 和 server 能拼接起来并且还原出多个 span 之间的父子关系,所有 span 的 server 端必须保存 parent_span_id。因此两个 span 实际需要向存储中写入 4 条数据。

右图:kepler2.0 推导式算法,span_id 自根节点从 0 开始,每调用一次下游就累加该下游实例的 ip 作为其 span_id 并将其传给下游,下游实例递归在此 span_id 上继续累加,这样可以保证一个 query 所有调用的 span_id 是唯一性。实例只需要保存自己的 span_id 和下游的 ip,即可根据算法还原出一个 span 的 client 端和 server 端。由此可见,只需要写入 2 条数据且数据中不需要保存 parent_span_id,因此存储空间得到了节省,从而实现了全量调用链的采集能力。

右图中 ip1:port1 对 ip2:port 的调用链模拟了对同一个实例 ip2:port2 访问多次的场景,该场景在搜索业务中广泛存在(例如:一个 query 在融合层服务会请求同一个排序服务实例两次;调度层面上游请求下游异常重试到同一个实例等),推导式算法均可以保证生成 span_id 在 query 内的唯一性,从而保证了调用链数据的完整性。

3.2.2.2 数据压缩

结合数据特征综合采用多种压缩算法。

(1) 业务层面:结合业务数据特征进行了定制化压缩,而非采用通用算法无脑压缩。

(a) timestamp:使用相对于 base 的差值和 pfordelta 算法。对扇出型服务多子节点时间戳进行了压缩,只需保存第一个开始时间戳以及相对该时间戳的偏移。以搜索在线服务常见高扇出、短时延场景为例,存储偏移比直接存储两个时间戳节省 70%。

(b) ip:搜索内网服务 ip 均为 10.0.0.0/24 网段,故只保存 ip 的后 3 字节,省去第 1 字节的 10,每个 ip 节省 25%。

(2) protobuf 层面:业务层面的数据最终持久化存储时采用了 protobuf,灵活运用 protobuf 的序列化特性节省存储。

(a) varint:变长代替原来定长 64 位对所有的整数进行压缩保存,对于 ip、port、时间戳偏移这种不足 64 位的数据实现了无存储浪费。

(b) packed repeated:ip 和 timestamp 均为 repeated 类型,只需要保存一次 field number。packed 默认是不开启的,导致每个 repeated 字段都保存一次 field number,造成了极大浪费。以平均扇出比为 40 的扇出链路为例,开启 packed 可节省了 25%的存储空间(40 字节的 field number)。

最终,一个 span 的逻辑格式(上图)和物理格式(下图)如下:

3.2.3 应用场景的受益

3.2.3.1 时光穿越:历史上任一特定 query 的关键结果在索引库层面未预期召回问题

因为召回层索引库是搜索最大规模的服务集群,kepler1.0 在索引库服务上只支持 0.1%抽样率,使得由于索引库的某个库种和分片故障导致的效果问题追查捉襟见肘。全量调用链采集较好的解决了这一困境。

真实案例:PC 搜索 query=杭州 未展现百度百科结果,首先通过工具查询到该结果的 url 所在数据库 A 的 9 号分片,进一步通过全量调用链调用链查看该 query 对数据库 A 所有请求中丢失了 9 号分片(该分片因重试后仍超时被调度策略丢弃),进一步定位该分片所有副本均无法提供服务导致失败,修复服务后预期结果正常召回。

3.2.3.2 链式分析:有状态服务导致“误中副车”型效果问题

有状态服务效果问题分析复杂性:以最常见的 cache 服务为例。如果没有 cache 只需通过效果异常的 queryID 通过调用链和日志即可定位异常原因。但显然搜索在线系统不可能没有 cache,且通常 cache 数据会辅以异步更新机制,此时对于命中了脏 cache 的 query 只是“受害者”,它的调用链和日志无法用于问题最终定位,需要时序上前一个写 cache 的 query 的调用链和日志进行分析,我们称其为“捣乱者”。

kepler1.0 的局限性:kepler1.0 采样算法是随机比例抽样,“捣乱者”和“受害者”两个 query 是否命中抽样是独立事件,由于“捣乱者”在先,当“受害者”在受到效果影响时,已无法倒流时间触发前者抽样了,导致两个 query 在“时序”维度够成的 trace 链条中断,追查也随之陷入了困境。

kepler2.0 的破解之法:在实现“纵向关联”(某一 query 处理过程中全量调用链和日志信息)基础上,借助全量调用链建设了“横向关联”能力,支持了对时序上多个关联 query 的链式追踪需求。写 cache 时将当前 query 的 TraceId 记录到 cache 结果中,读 cache 的 query 就可通过 cache 结果中的 queryID 找到“捣乱者”。借助全量调用链功能即可对“捣乱者”写脏 cache 的原因进行分析定位。另外,用户界面也对时序追踪的易用性进行了特殊设计,例如,对日志中写 cache 的 queryID 进行飘红,点击该字段可以直接跳转到对应 query 的调用链和日志查询页面。

小结

以上,极致的数据建设解决了问题追查的死角,此时问题分析效率成为主要矛盾,下篇我们为大家带来百度搜索如何通过对人工分析经验进行抽象,实现自动化、智能化的故障问题,从而保障百度搜索稳定性。未完待续,敬请期待……

本期作者 | ZhenZhen;LiDuo;XuZhiMing


文章看完,还不过瘾?

更多精彩内容欢迎关注百度开发者中心公众号


发布于: 5 小时前阅读数: 4
用户头像

关注百度开发者中心,收获一手技术干货。 2018.11.12 加入

汇聚百度所有对外开放技术、平台和服务资源,提供全方位支持,助力开发者加速成功,实现开发者、消费者和百度三方共赢。https://developer.baidu.com/

评论

发布
暂无评论
百度搜索稳定性问题分析的故事(上)