一 、稳定大于一切
盒马的线下作业稳定性要求极高,假如门店 pos 无法付款了,排起的支付长队伍能让人把门店闹翻,假如配送员无法揽收了,在家里预定的午餐材料的饥肠辘辘的客户能把投诉电话打爆,甚至会形成广泛的社会舆论。盒马安全生产至关重要,稳定大于一切。
盒马配送智能调度负责将订单指派给骑手,是配送作业实操的第一环节,也是最重要的环节之一,假如调度出现问题,那么会导致大量订单超时履约,导致客户投诉,也会造成客户取消造成的销售资损。理论上系统不变更就会降低稳定性的风险,然而随着盒马配送提效降本的业务诉求和新业务的调度需求,以及配送智能调度上线两年来补丁重重导致的开发运维困难,急需系统架构重构,使得我们必须在高压下砥砺前行,对系统进行不断的重构改造,同时支撑日益发展的业务和提效降本的业务诉求。
配送智能调度系统今年系统完全重构一次,迁移了 o2o、b2c、冷链等重要业务,上线了 mall 调度新需求,支撑了预调度和实时运力调度的提效降本需求,支撑了算法数据白盒化改造,在此基础上将继续耕耘,已产出了策略化运营方案、仿真改造方案和算法结果智能诊断方案,我们用实际行动表明,在系统飞速发展的同时,也能做到了智能调度系统全年 0 故障。
二、 智能调度链路分析
系统稳定性保障前提是要对系统关键链路了如指掌,关键链路包括对外依赖和我们提供的服务,因此在接手智能调度系统升级改造时,我们做了非常全面的智能调度的链路分析,并在不断上线新需求后,及时完善链路,使得我们不管在大促期间或者日常监控,不管是我们自己运维还是给团队伙伴运维 backup 都能了解系统全貌,基于统一链路图进行讨论,并在告警发生时能够分析和解决问题。
配送 O2O 智能调度涉及调度系统、压力系统、基础资料、骑手平台、算法策略、分布式计算、路径规划、单据分发等系统,涉及 DTS、diamond、tair、作业 DB、降级 DB 等存储和中间件,链路非常长,我们绘制了一张 o2o 智能调度时序图,基于同一张大图,产品、技术、测试、算法能在大促和系统变更前评估系统稳定性风险。
三、稳定性因素分析和实践
有了详细的系统调用链路,我们可以把链路中的每条线的稳定性按类别进行梳理。
3.1 DB 依赖
(1)慢 sql
DB 依赖主要分析依赖 DB 的稳定性,首先,DB 有没有慢 SQL,盒马早期大多数故障原因是慢 sql 导致,后来对 DB 的集中治理才使得这块不稳定因素被逐步瓦解,但是慢 SQL 治理是长期的事情,不管是上新业务的 sql 事前分析,还是流量自然增长需要做的 DB 定时 check 都至关重要。
(2)逻辑读行数多
有些 sql 不是慢 sql,但是逻辑读非常大,比如超过 10 万行,那么这些 sql 在业务自然增长时很可能发展为慢 sql,如果不治理,假以时日必定让你“刮目相看”。
(3)CloudDBA 中的属性(cpu、内存、load、qps 趋势)
查看 DB 的 cpu 是否正常,load 是否比较高水位,是否有很多 qps/tps 尖刺。配送智能调度依赖的 walle 库在大促前发现 db 整体水位较低,但是 cpu 尖刺特别高,尖刺有到达 60%,而且非常有规律,经过排查是一个网格任务没有离散导致。该问题咨询过 DBA 如果双 11 大促三倍流量,DB 扛不住,幸好及时发现,紧急发布做了网格任务离散化。
(4)DB 不隔离
DB 不隔离常见有核心与非核心数据在一起,非核心 qps 高或者逻辑读高影响了整个 DB 的稳定性,配送从一个库拆分为核心、次核心、非核心、归档库,就是为了做到业务重要性分层。
冷热数据不隔离,一个不经常访问的超慢 sql 生成的报表使用了核心库,该 case 在盒马发展史上出现多次,常见的有统计报查询、报表导出使用了核心库。
读写不分离,上述报表类需求、前端展示类需求、看板类需求属于读业务,线下实操核心的是写 DB,两者不隔离也会同样产生问题。
(5)DB 降级
在智能调度系统中,DB 依赖核心作业库中的单据,核心作业稳定性高于智能调度,为了保障核心作业稳定,我们为 DB 做了降级策略,用精卫同步一份数据到读库。DB 降级主要衡量 DB 不稳定对该 DB 上业务的影响。
(6)上下游同一业务字段存在 DB 字段大小和类型不一致
上下游 DB 使用了同一业务字段但容量不一样会造成极端场景下数据无法写入。配送关联发货单字段是组合字段,包含批次关联的所有发货单,正常情况下一个批次关联 1~3 个发货单,在大促期间仓为了作业方便,关联了 7 个发货单,导致配送 64 位字符串超长,导致关联发货单无法写入,后来紧急变更,将字段改为 256 位得以解决。商品重量配送的字段是 int,表示毫克,某次大促仓库将一瓶饮料录入为 2 吨(商品重量当时无业务应用场景,预留字段),配送子订单表存储了多瓶饮料的子订单,导致配送无法创建运单。
上下游同一业务字段要保持一致,如果有字段转义,需要做好字段截取、报警、应急预案,在保护好自己的同时,能快速解决异常 case。
(7)DB 容量、索引等
DBA 单表建议小于 500 万,不用索引要删除,以免影响写入性能。
(8)DB 变更导致
改索引、改字段类型、字段扩容会引起锁表,有的会影响主备同步,假如恰好有业务报表依赖备库,会造成业务报表不可用。凌晨做的数据结构变更没有设置停止时间,导致早上 7 点 DB 变更还没结束,影响了业务。数据变更批量改了 gmt_modified 时间,恰好有该字段的索引导致索引重建,影响库性能,所有依赖精卫都产生延迟。
DB 变更要评估变更影响,自己拿不定主意的要找 DBA 确认,多找老司机咨询。
(9)db 表非 utf8mtd
emoji 格式文本存储到非 mtd 类型表后,查询导致 emoj 无法显示。
3.2 HSF 依赖
(1)hsf 服务超时
hsf 超时时间不能设置太长,特别是高 qps 和高可用接口,hsf 超时时间过长会导致 hsf 线程池打满,智能调度算法数据采集中,由于 qps 较高,同时访问 ADB 容易造成抖动,hsf 服务的超时时间设置默认 3 秒导致 hsf 线程池打满,数据采集功能在预发环境中排查出不可用。
(2)hsf 超时重试
由于网络抖动造成的 hsf 超时可通过重试避免,相对短的超时时间+重试机制比默认超时时间更可靠,在揽单上限 hsf 服务接口请求时,由于默认 3 秒超时和没有重试导致每天五百万服务揽单上限请求有 25 次左右失败率,使用了 500ms 重试+2 次重试机制后一周失败量才 1 次。
(3)服务缓存
访问数据相对稳定的接口,并且耗时较长的接口需要设置前置缓存,减少访问依赖,同时保证稳定性。强依赖的接口,容易做缓存的接口需要设置后置缓存,当访问服务失败后能够请求缓存数据,同时每次请求成功需要更新缓存。
(4)服务降级
强依赖接口当服务不可用时需要设置降级机制,比如降级到上述缓存,或者降级到其他接口,降级到 diamond,降级到内存等等,保证服务链路走通的同时,配合接口报警机制,即时发现依赖服务的问题。
(5)服务隔离
核心应用不能依赖非核心服务,同样,非核心应用不能依赖核心应用,两者都会造成核心服务被影响,配送仓配一体化调度和 o2o 智能调度同时使用 walle-grid 计算服务,但是一体化调度重要程度低,只影响调度效果,o2o 服务重要程度高影响指派。我们通过版本号对服务进行隔离,如果有需要可根据分组规则或者自定义路由规则进行隔离。
(6)流量预估和压测
上新功能增加了依赖,或者会有较大新流量的,需要对流量进行预估,并且按流量进行单接口压测。配送批次组相似度打分服务上预调度功能时,预估增加 0.5 倍批次,两两计算的笛卡尔积是 2.25,估计全量开预调度增加 3 倍以内流量,当前系统在不增加机器情况下可以扛住洪峰,实际开启预调度后验证无问题。
3.3 HSF 服务提供
(1)服务超时
相应的服务提供者要提供相对可控的超时时间,以防被人把线程池打满,承诺的超时时间可根据系统压测后得到,或者通过线上鹰眼的统计给出一个相对靠谱的超时时间,并不断优化,默认 3 秒的超时在高稳定要求领域尽量少用。
(2)限流
除了设置相对靠谱的超时时间,还需要对服务能够提供的流量进行限定,核心服务一定要增加 sentinel 做限流。sentinel 可根据单机限流,粒度可以是 qps 或者 hsf 线程数。一般按 qps 限流较多,微服务也可按线程数限流。
智能调度请求骑手服务,正常情况下骑手服务性能很高,简单的 db 的 uk 查询,按理说该服务不会有问题,某次大促时 db 发生了主备切换(DB 当时非单实例,其他 DB 影响了),导致长时间 db 不可用,高 qps 流量过来把骑手服务应用线程池打满,通过限流措施截断流量,这时上游调度系统使用后置缓存正常返回,如果没有限流,骑手服务导致线程池被打满,整体系统将陷入不可用状况。
(3)幂等
上游服务重试,下游保证幂等,幂等包含简单单据幂等,也有比较复杂的幂等逻辑,比如批量请求的接口。和上游约定好幂等逻辑,以及幂等返回值。幂等要返回成功,服务端自己吃掉异常。
(4)服务缓存
服务提供者通过前置缓存提高系统支撑流量,可应用于返回值相对稳定的服务,服务缓存是否设置前置缓存可根据缓存命中率评估,有的为了支撑高 qps 流量但缓存命中率低也可考虑。配送某个打分服务是一个笛卡尔积类型的服务,n 个单据就有 n*(n-1)/2 次调用,假如 20 秒调用一次,这时候设置 1 分钟缓存,理论命中率虽然只有 67%,但提高了 2 倍流量,可有效减少机器数量。
服务后置缓存通常用来做服务降级逻辑,揽单上限这个核心服务,有三级兜底逻辑,分别是骑手值、城市值、内存值,保证服务端的高可用。
(5)服务隔离:同上
(6)流量预估和压测:同上
3.4 tair 依赖
(1)tair 各种产品适用场景
MDB 是常用的缓存,常用扛高 qps,但是 MDB 不保证持久化,MDB 分单集群和独立集群,独立集群有两个集群数据存储完全独立,不能保证数据缓存后能被访问到,MDB 不适合用分布式锁。LDB 持久化存储,非此场景都不需要用。RDB 有同步淘汰策略,当容量不够时会触发该策略,导致写入延迟。批量接口单次请求限制在 1024 个,建议批量接口单次请求不超过 100 个。
(2)缓存容量和 qps
缓存容量和 qps 不足时要扩容,MDB 一般可支持 qps 100 万,RDB10 万。
(3)吞吐量
tair 都任何场景都不适合大 key 和大 value 的情况,key 1k,value 不超过 10k,极端情况下会造成数据被拆分,导致数据丢失,批量写入要减少批次数,skey 不超过 5120。
(4)缓存超时
缓存一定要设置超时时间,单位是秒,新同学有认为单位毫秒导致超时时间过长缓存超容量的情况。
(5)独立集群和单集群
MDB 独立集群两个集群完全独立,na61 只提供给 61 机房的系统访问,62 访问不到,由于独立集群的问题,会导致缓存数据访问不到。
(6)tair 分布式锁
tair 锁使用 put 和 get 方法,锁的 version 从 2 开始,锁要考虑超时情况,可通过重试机制避免超时造成的影响,锁网络超时的 ResultCode 返回值可能 ResultCode.CONNERROR,ResultCode.TIMEOUT,ResultCode.UNKNOW。
(7)缓存数据一致性
简单的一致性问题比如 DB 和缓存,db 更新成功后可多增加几个通道保证 db 执行成功后发出消息,缓存做好幂等,考虑系统挂了的情况可以依赖 db 变更的精卫消息或者 binlog 消息做数据对账。
我们遇到的一致性问题比较隐蔽,某个打分服务做缓存减少笛卡尔积计算,最开始设置缓存 Key 是单据,value 是相似度分数对象,但是算法在使用单据对象的时候,相似度分数类包含了已分配单据对应到站骑手,假如为骑手 1,第二次计算的时候,还是拿到的骑手 1,但是骑手 1 已经离站了,导致一个 NP 问题,后改成单据+骑手的缓存 key 才解决该问题。
总结一下,会被上下文修改的对象不适合做缓存,缓存对象要做到粒度小,且不被上下文改变。
(8)缓存击穿
缓存失效访问 DB 是一件高风险的事情,特别是高 qps 情况下,非常容易把 DB 搞挂,当缓存击穿之时就是故障来临之日。设置热点数据延长过期时间,稳定的数据使用内存缓存兜底,使用加锁策略保护 db,db 访问限流等。
3.5 Metaq 依赖
(1)Metaq consumer 线上未注册
该问题会导致注册后批量消息发送。在消息平台中短信业务接入后忘记去线上订阅导致订阅时短信一起发出去,造成业务大大的黑人问号。可以通过低峰期清理 topic 的消息后,再进行订阅。
(2)metaq size 限制
单条消息限制 128k,超过该限制会发送失败。
(3)metaq 发送失败
虽然 metaq 比较稳定,但是偶尔会有 metaq 发送时失败的 case,可能是 metaq broker 集群抖动,网络问题等,重要的 Metaq 消息发送要做发送失败监控,可通过手动补发,或者通过消息对账,自动重试。
(4)metaq 消费失败
消费最多重试 16 次,最大重试间隔 2 小时,可修改重试间隔减少重试时间,可以设置 Metaq 循环重试,超过 15 次后再发送一条 metaq,形成 metaq 循环,不建议死循环,可通过消费时间进行控制。
(5)metaq qps/tps
发送和消费的单 topic qps/tps 都应小于 3000,某次 topic tps 超过 3000 被 Metaq 同学钉钉通知,最后发现是一些废弃的 blink 任务在写,停止后重启 metaq 恢复。
(6)metaq 堆积监控
metaq 堆积一般是由于业务系统自身处理失败引起的,少部分原因是 metaq 的服务端问题导致部分通道无法消费,metaq 平台可配置消费堆积监控,默认值 1 万条,重要业务可设置相对较少的堆积条数,比如设置 1000 条上限左右。
3.6 精卫依赖
(1)精卫数据传输乱码导致无法写入 DB json 字段
db 有个字段是 json 格式,通过精卫传输到读库时,由于中文解析问题导致 json 格式被破坏,无法写入 db,db 字段慎用 json 格式。
(2)精卫延迟
DB 变更,DB 索引不一致,字段不一致等导致精卫延迟或者精卫自身问题导致延迟,重要业务需要配置延迟报警,在 DBA 和精卫同学双方确认下,可以加大写入并发,扩大精卫同步 tps,追赶延迟数据。
(3)精卫暂停任务
精卫任务暂停可设置检测自动重启,或者订阅精卫 delay 监控,手动重启任务。
3.7 DTS 依赖
(1)DTS 网格任务/并行任务离散化
并行类任务需要考虑下游的承载能力做离散化,否则会造成 qps 热点。
(2)DTS 降级
可实现一个本地分布式调度任务,在 DTS 不可用时可降级。
(3)DTS 监控
设置 DTS 超时监控,DTS 返回失败监控,DTS 没有正常启动监控,除了中间件级别的监控以外,还应该设置 DTS 运行的应用级别调用量监控,在量级稳定的情况下,DTS 每分钟的调度次数应该在阈值之内。
3.8 开关
(1)开关推送检查和监控
开关推送要检查是否生效,推送完开关后要刷新开关页面,查看当前值,最好做一个开关生效日志监控,推送开关后查看日志是否所有机器都生效。
(2)多个 diamond 分批发布开关
多个 diamond 开关分批发布要注意,某个开关分批发布暂停时,其他开关发布无法生效,需等待其他开关发布完成。
(3)开关初始化
开关编码时要注意初始化影响,在系统启动时,被依赖的开关没有初始化会有 NP 风险。
(4)开关多线程影响
我们的开关基本都是单例在多线程环境中使用,开关发布要注意数据的可见性,比较复杂的数组或 map 结构,推荐使用影子实例完成开关状态变更。
(5)开关接入 changefree
开关变更最好接入 changefree,审批人要设置多个,以防紧急开关推送和开关回滚找不到联系人。
3.9 监控
(1)流量监控
流量监控包括提供的服务的 qps,依赖服务的 qps,周期任务的执行次数等,流量监控反应业务稳定期间的大致流量。配置监控包括周同比、天环比、流量阈值。流量监控需要不断评估和探索业务在合理变化范围,可用 odps 统计近一个月的数据,设置相应监控值。
(2)错误数量/成功率监控
有些业务流量较低,重要层度却很高,这时候错误数量和成功率监控是最合适的,可以按照分钟均值来统计,避免网络原因或系统 load 抖动造成的正常服务失败。
(3)错误原因监控
错误原因监控可快速定位业务错误,错误日志按照固定格式打印,错处原因数量按错误分类计数。
(4)RT 监控
RT 是服务时长,反应服务的当前健康度,可按照分钟均值统计,避免网络原因或系统 load 抖动造成的正常服务失败。
(5)大盘
P 级故障具备监控条件的都需要配置在监控大盘,监控大盘能够总览所有系统业务监控指标,快速定位具体问题。
(6)系统异常监控
可对所有系统配置 NP 错误监控、数据越界监控、类型转换错误,此类 runtime 错误肯定是系统 bug 导致,很可能是发布导致的问题,配置该监控能够第一时间发现发布的问题。
3.10 灰度
(1)系统对服务有多次状态依赖不可分布发布
一个链路中两次依赖系统中的状态值(db 或 tair 或内存或其他存储),系统正在发布中,两次依赖导致访问了两个不同的状态值,状态不一致阻塞了系统链路。在很多下线作业场景,灰度的粒度是以业务划分粒度的,而不是以流量,因此某些场景下使用业务单元灰度较合适。
(2)切流前评估流量
在大规模切流前,要评估系统即将承受的流量,在前期切 10%左右流量后,能够近似的根据业务特征对后面 90%流量作出合理评估。评估流量后可使用压测评估机器是否能扛住后续流量。
(3)灰度预期和方案回滚
灰度要有业务预期和系统预期,业务预计比如是否按产品流程执行完成,是否灰度流量覆盖了所有 TC 的 case,系统预期比如是否系统 error 数同比和环比一致,是否有新增的异常,是否有 NP 错误。
灰度不符合预期需要立即回滚,不用考虑任何原因和后果,事后再做分析。预调度灰度期,正好盒马封网,经过层层审批才上线灰度一个站点,灰度开始 10 分钟内业务表现正常,但是系统出现了未曾出现的错误日志,随后出现预调度批次不指派,我们立即进行回滚。事后分析了一整天(只在低峰期出现),是缓存对象被改变了导致 NP 错误。
3.11 测试
(1)预发空跑验证
预发造测试站点流量,验证功能可行性,预发空跑是测试验证前期要完成的,按照 TC 造多个 case,验证是否符合预期。
(2)预发引流验证
预发从线上引导流量,验证产品逻辑,预发从线上引流是为了用大流量验证系统功能。像智能调度如此长的链路,我们用引流可以验证一些数据难造的场景,以及 TC 之外的场景
(3)线上引流比对
可单独为线上站点开起新调度和老调度,并对业务最终指派结果进行比对,从日志上分析行为差异性,是正常的时延差异还是系统 bug。还可按照统计学的方法对系统核心指标进行比对分析。
3.12 应急响应
没有完美设计的系统,bug 总是存在,当出现线上问题的时候如何应急处理呢?为了避免线上问题出现时手忙脚乱,需要做好“天晴修屋顶”,这里的“修”既包含上述架构治理、监控完善,也包含应急预案梳理和故障演练。
(1)应急预案梳理
应急预案包括系统上的开关、限流工具、降级预案等也包含业务测的应急响应措施,在系统短期无法恢复时,也能正常作业下去,比如配送小票作业,能在配送系统无法揽收时,通过小票完成货物揽收和配送。
系统上的预案要做到事前验证,在预案上线后验证预案的有效性;一键推送,通过预案平台能够一键触达,快速生效预案;组织培训,预案要深入人心,让组内每个同学都掌握如何操作,以及何时操作。
(2)故障演练
为了避免故障来临时人心慌乱,提高决策效率,缩短问题定位时间,需要在平常做好故障演练。故障演练要当作线上故障对待,由测试在安全生产环境发起,故障演练要做到真实,演练要保密,推荐测试和开发人员使用红蓝对抗实践故障演练。
为了提高开发应急运维能力,需要有一套故障应对响应机制,盒马配送从组织、发现、决策、善后四个角度出发,沉淀了一套故障应急响应方法,在故障来临之际要组织团队 leader 和团队成员停下手头工作,All In 故障处理,首先建立沟通群,其次从大盘监控发现问题点,然后针对问题点决策执行相应预案,当报警恢复时再评估影响面,是否存在故障引起的遗留问题。
(3)故障处理 经过多次故障演练,能有效培养团队故障发生的应急运维能力,故障发生时切记不要盲目的分析故障原因,而是应以最快速度止血和恢复。80%以上的故障是由变更引起,不管是发布、开关推送、新增配置、上下游有发布等等,少部分是由于流量增长和其他上下文环境变化引起。如果发生故障时存在变更,则需第一时间回滚,如果是其他原因,那么可根据系统症状,决策推预案、重启、扩容等,如果无上述措施,则要分工合作,组织人对外沟通、排查日志、检查 DB、查看服务流量、分析链路 trace 等,以最快的速度定位问题。
四、总结
智能调度在不停发展中,我们接下来还有策略运营、智能诊断、仿真等进行中的项目,除了已有的稳定性作战经验之外,我们还将继续迎接新的稳定性挑战,探索不同环境下稳定性最佳实践。
本文转载自公众号阿里技术(ID:ali_tech)。
原文链接:
https://mp.weixin.qq.com/s/X0jsAgSniwysNvXnUMDAKg
评论