导读:在互联网公司中,业务迭代快,系统变更频繁,初期都是刀耕火种。但随着系统复杂度不断增加,系统稳定性问题会凸显出来,当稳定性问题成为业务发展的掣肘的时候,重新推倒重来所需要的代价可想而知,因此我们的系统架构需要持续优化和演进不断提升稳定性,既要解决当务之急,又要防患未然。本文结合具体实践,对系统高可用建设的方法进行思考和总结。
一、背景介绍
百度商业托管:是百度为了实现营销新生态的建设,以高效连接和投放优化为目标,为商业客户提供一站式的的运营阵地,连接服务和消费者,是百度从流量运营到用户运营的重要转变。代表的产品有基木鱼、度小店等建站和电商平台。随着托管页的业务不断发展,系统的规模和业务复杂度不断增加,系统的可用性面临巨大挑战,本文从可用性建设的方法到实践,深入分析稳定性建设的思路,从规范、监控、冗余、降级、预案等多方面实现系统的高可用。
「可用性指标定义:」 对于系统而言,最理想的情况是系统能提供24小时不间断的提供服务、但由于软件系统的复杂度高,尤其在分布式系统环境中经常会由于系统BUG、软硬件异常、容量不足等导致系统无法提供100%的可用性,因此通常采用N个9来评估系统可用性,此指标也作为一些基础服务的SLA的标准。
二、可用性整体建设思路
系统的高可用建设是一件庞大的工程、需要从不同维度去综合考虑,整体建设思路可以围绕系统故障发生的时间、范围、频率,处理速度等方面来综合考虑。
2.1 故障发现早
从故障的发生时间来看,在用户或客户反馈问题之前,研发人员能够第一时间发现问题是非常重要的,每一次故障发生之后我们都会深入思考一个问题,能不能更早的发现问题,我们有哪些常用的手段和方法,下面就逐一介绍下。
2.1.1 故障发现早-规范化:
- 「日志规范化」规范化的核心思想是通过一定约束来保证整体系统能够协调统一,托管内部的服务是基于统一的微服务框架构建,但由于各个系统和模块的日志千差万别,在开发、测试和运维阶段带来较高的成本。日志规范主要是针对开发过程中关键业务信息的记录,高效的定位问题;在QA测试阶段进行问题排查;在数据统计分析提供有效指导手册。
- 「全局通用规范」:
- 全局上下文采用统一的MDC实现,用中括号和空格分割。
- 所有的logger均需设置addtivity=false,禁止重复打印。
- msg信息需要简明、易懂。
- 相关日志禁止重复打印到console.log中。
- 打印日志使用slf4j门面。
- 「日志分级」:
- 「TRACE」 调试详细信息,线上禁止开启。
- 「DEBUG」 开发调试日志,线上禁止开启。
- 「WARN」 警告日志 日志常用来表示系统模块发生问题,但并不影响系统运行。
- 「INFO」 信息记录 日志级别主要用于记录系统运行状态等关联信息。
- 「ERROR」 错误信息输出 此信息输出后,主体系统核心模块正常工作,需要修复才能正常工作。
-
「日志文件」
「logPattern」
- 「关键日志的格式要求」此处涉及的细则规范较多,不一一列举,主要涉及到贯穿日志的核心上下文,需要包含来源ip,请求路径,状态码,耗时等。
- 「报警规范化」报警规范化主要针对错误日志的报警监控,做到报警的分级监控、定义了分级监控的监控项目名称的定义。针对不同级别的报警,采用不同的采集任务和监控策略,并定义配套的跟进流程。
- 通用服务的性能监控报警。
- 强依赖的性能监控报警。
- 服务异常状态码监控报警。
- 第三方服务耗时监控报警。
- 「值班规范化」针对值班同学的通报、止损、定位、解决等核心规范和流程,保证线上的问题能够第一时间处理和解决。
2.1.2 故障发现早-系统监控
系统监控主要是从问题感知到问题定位的全面能力的建设。前面提到的日志规范化整改是实现自动化问题感知的前提,当系统日志规范完成之后,我们就可以通过一些自动化的方式来建设统一的监控。在问题感知层面主要包含业务指标、业务功能、系统稳定性、数据的正确性、时效性等。
- 「问题感知:」业务指标是指系统关注的核心业务指标,主要是通过实时数据采集的方式能够发现业务指标的变化,能够实时监控到系统问题对业务的影响范围。
业务功能是指针对核心的业务功能分场景的自动化测试和监控能力。
系统稳定性会从多个维度去衡量。会从网关入口来衡量可用性、会从模块自身来看可用性、以及从依赖的第三方的稳定性来衡量系统的稳定性。
数据一致性校验本质上是一种离线或近线的对账场景,对于分布式的微服务来说,绝大部分都是采用补偿来实现最终一致性,因此数据的对账就显得尤为重要。 - 「问题定位:」问题定位主要是结合一些核心业务场景,建设一些异常指标的报警和监控,其中包括流量异常、平响异常、pvlost等。在数据正确性和时效性上面来看,包括数据延时、数据不一致等。
2.1.3 故障发现早-容量评估
容量评估是提前发现系统容量问题的有效手段,尤其是当有一些特定的业务场景的时候,需要工程师或者架构师进行系统的容量评估来判断系统是否需要扩容等操作,在这里需要我们提前做很多准备,常见的容量评估的方式有静态分析和动态评估两种手段。
静态分析是指通过分析现有系统的依赖拓扑,结合在当前流量的情况下,通过理论计算出系统能否承受的最大流量的负载能力和系统瓶颈。静态分析只能提供一种预估的结果,不一定客观和准确。动态评估是指针对线上的服务进行模拟压测,通过系统的实际情况来评估容量情况,此种方式相对客观准确,但线上的全链路压测会有一定风险,而且容易对业务数据带来污染。因此实际在做容量评估的时候可以采用静态分析+动态评估两者结合的方式来进行。
- 「动态评估相关注意事项:」
- 尽量模拟线上真实的流量(流量回放)来进行线上压测,因为不同的分支逻辑可能带来的系统负载不同,例如如果针对某一个相同请求和参数进行压测,极可能命中cache,则会导致压测结果不置信。
- 动态评估之前需要通过静态分析排查可能对业务带来的影响,需要增加相关的开关避免对用户带来干扰,例如:针对下单行为给用户或者商家发短信等。
- 动态评估可能会对线上系统带来影响,因此要在流量低峰期进行,并且能够做到快速启停。
- 动态评估需要业务系统配合做数据打标和清理,避免脏数据对线上业务的影响。
2.2 故障范围小
从故障的范围来看,缩小故障范围的常用方法和核心手段主要就是隔离,隔离强调的是将微服务架构体系中非核心服务导致的故障隔离出去,减少非核心因素对业务核心的稳定性影响,隔离工作做好之后只需要考虑核心服务的稳定性。通俗点讲鸡蛋不能放在同一个篮子里。具体有存储的隔离、服务的隔离、以及权限的隔离。
2.2.1 故障范围小-存储隔离
系统建设初期,为了提升研发效率和节省资源,很多业务都是共用存储的。随着业务的发展,经常会出现以下问题
- A业务的慢sql导致整个集群变慢。影响了B、C、D业务。
- B业务的大表的添加字段,导致主从延时,影响了A、C、D业务。
- C业务线下离线统计分析导致从库CPU100%,影响了A、B、D业务。
解决如下问题的主要方法就是物理集群拆分,避免业务共用底层存储相互影响,提升系统整体稳定性。托管页系统有建站和电商两大业务,由于共享MYSQL集群导致互相影响的线上case出现的频率较高,一般按照业务域去迁移物理集群,主要的拆分方法和步骤见下图:
其中新集群的容量评估、资源申请、以及切换过程中的双写同步都是非常重要的流程和步骤,双写后业务要及时校验数据的准确性。关于其他redis等其他的存储隔离的思路和方法与上述一致。
2.2.2 故障范围小-服务隔离
- 「服务隔离」服务隔离一种方式是从业务视角去看的,此处涉及到微服务的拆分的原则,一般方法和原则如下:
- 将容易变化的,频繁变更的部分隔离出来服务。
- 将高并发等级高的应用与低等级的应用隔离出来。
- 按照组织架构划分将服务进行拆分和隔离(康威定律|垂直拆分)。
- 沉淀底层通用的基础信息和服务,保证通用性(水平拆分)。
另外一种隔离的方式是从冗余的视角去看的,从高可用的角度需要保证我们的服务保证多机房多地域的冗余,保证在某个机房或者某个地域出现故障时候,能否及时切换和止损。冗余解决的是核心服务面对各种环境变化时的稳定性应对,比如服务故障、交换机故障、网络故障、机房故障等,通过各种层次的冗余和流量调度机制,保证业务面对各种硬件和环境变化时仍然可以通过冗余切换提供稳定的服务。
此处的冗余更多的是指接入层和服务的冗余,对于无状态的服务冗余是很容易实现的,但是对于有状态的基础组件和存储服务做多地域冗余成本是很高的,可以分场景去实现,例如对数据一致性要求不高的查询场景,可以采用存储的多地域部署,但是对一致性要求很高的,需要考虑set化来实现,具体可参考阿里的三地五中心架构。
- 「老旧服务清理」由于系统不断变更和迭代,不断会有一些技术项目对现有的系统进行重构或者重写,对于多个版本的接口或者系统并存的情况在互联网公司并不罕见。尤其是对一些底层的基础服务,此处需要程序员或者架构师有高度的敏感度和责任心,对于一些技术的尾巴要及时清理,来保证系统的高可用。
- 对于基础服务的提供方,涉及到老旧版本的升级,需要及时推动上游系统进行升级。
- 对于依赖一些无人维护的老旧服务,需要重新梳理服务依赖拓扑,进行优化替代。
- 对于依赖的基础组件、需要及时进行评估和更新上线,尤其涉及到一些安全问题,性能问题等。
2.2.3 故障范围小-权限隔离
系统故障大多数都是由于变更导致的,在变更管控上重要的一点就是要做到权限隔离,服务发布和上线的权限隔离,此处需要依托于容器化管理平台的能力,但是团队内部需要及时清理相关权限。避免不相关人员误操作导致线上风险。
- 线上数据库的读写权限隔离,IP授权的管控。
- 线上服务的发布和部署权限隔离,分级发布的审核人员名单管控。
- 代码库的权限隔离,保证CR的质量。
- 对于服务的入口层以及管理权限的隔离。
2.3 故障频率低
提到故障频率不得不提及另外一个概念,叫做MTBF(平均故障间隔)
失效时间是指上一次设备恢复正常状态(图中的up time)起,到设备此次失效那一刻(图中的down time)之间间隔的时间。可以将MTBF用如下的数学式表达:
我们面临的是各种复杂的网络环境,故障频率是衡量我们系统自我保护能力的重要指标,接下来介绍下常见的方法和实践。
2.3.1 故障频率低-服务限流
每个系统都有自己的最大承受能力,即在达到某个临界点之前,系统都可以正常提供服务。为了保证系统在面临大量瞬时流量的同时仍然可以对外提供服务,我们就需要采取流控。尤其是针对一些底层基础服务或者被较多应用依赖的业务服务。限流算法常见的有记数法(固定窗口和滑动窗口)令牌桶和漏桶算法。
- 「常见限流算法」
- 令牌桶算法:在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。
- 漏桶算法:漏桶算法这个名字就很形象,算法内部有一个容器,类似生活用到的漏斗,当请求进来时,相当于水倒入漏斗,然后从下端小口慢慢匀速的流出。不管上面流量多大,下面流出的速度始终保持不变。
- 「限流方式」:从托管页系统来看,主要是两大类服务的限流,一种是直接面向用户的web服务或者api,这种通常情况下都会有网关层,例如百度有自己的BFE平台,可以很方便实现限流规则的配置。另一种是RPC服务,这种需要自己来实现限流,目前比较流行的限流方式是RateLimiter - resilience4j(基于令牌桶实现),能够跟Springboot很好的集成,具体实现和使用方法可参考https://resilience4j.readme.io/docs/ratelimiter
在配置和实现限流时需要注意以下几点:
- 限流的难点是如何评估合理的阈值,通常要结合线上的实际情况,和动态压测结果来准确评估。
- 由于我们的服务会提供多个API,需要针对服务进行全局的限流配置以及核心重要API的限流配置,优先级是全局>局部。
- 为保证限流操作的及时性,系统需要支持动态修改配置。
2.3.2 故障频率低-降级熔断
分布式系统的熔断就像家用电路的保险丝一样,当系统超过承载的阈值时,会自动熔断,起到系统保护的作用。尤其在微服务发展迅猛的今天,服务依赖的拓扑越来越复杂,架构师都很难画出来所有的服务依赖拓扑,当出现某一个服务不可用但是没有相应的熔断措施的话,极可能出现雪崩,这种灾难性的故障需要我们通过合理的熔断和降级来保证的。
「强弱依赖梳理」 要做好熔断降级的前提是要梳理好强弱依赖,此处的强弱依赖梳理主要从对业务的影响来评估,例如下单操作,对于商品的库存服务就是强依赖,因为要保证数据的一致性。此处不可降级。但是在商品详情页展示的价格依赖营销算价服务,此处可以定义成弱依赖,因为就算价格服务不可用,商品可以按照原价展示。降级一般来说对业务都有影响,我们核心要做的是降级后预期是什么,会对哪些业务产生影响。
-
「框架选择」 熔断降级框架目前比较常用的是https://resilience4j.readme.io/docs/circuitbreaker。这是一款轻量级的断路器框架(6w行代码),使用简单。(Hystrix停止更新,转入维护模式),同样比较常见的是 Sentinel,网上对比文章较多,此处不再赘述。
与Hystrix相同,Resilience4j熔断器也存在三种状态,即关闭状态(CLOSED)、半开启状态(HALF_OPEN)和开启状态(OPEN),但除此之外,Resilience4j还有两个特殊的状态,不可用状态(DISABLED)和强制开启状态(FORCED_OPEN) Resilience4j使用ring bit buffer 这种数据结构来存储被保护方法的调用结果。一次成功调用,存储1,一次失败调用存储0。ring bit buffer是一个类似bitset的数据结构,其底层是一个long型的数组,仅需16个元素就可以存储1024次调用的结果。
2.3.3 故障频率低-超时设置
超时和重试设置的不合理同样会带来系统故障,托管系统针对超时、容错、池化、等进行了全盘的梳理和整改。主要集中在如下方面
- 「合理的超时设置」
- RPC依赖和HTTP依赖均应设置合理的超时时间,可根据依赖服务线上99分位值,增加30%-50%的buffer。
- 许多框架都有默认的超时时间,需要酌情调整。例如redis连接池默认读写和连接超时为2000ms,okhttp的连接池默认为10s,hikari连接超时默认为30s. 很多默认的超时连接对于并发高的服务和应用都不太合理,需要结合业务场景综合考虑。
- 「容错机制」
- 对于读操作可以选择failover的容错策略,重试次数<=2次。
- 对于写操作的重试需要酌情考虑,要充分考虑下游服务是否能保证幂等性,为风险起见,对于下游无法保证幂等性的情况可以选择 failfast。
- 「池化设置」 线程池、连接池都是我们在程序开发中经常会使用的方式,核心目的就是为了减少频繁创建和销毁带来的系统开销,提升系统的性能,但是不合理的池化配置同样会给系统带来一定的风险。
- 线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式。显示定义线程池核心参数。(阿里编码规范)。
- redis和数据库连接池初始值需要考虑集群规模 以及存储服务允许的最大连接数,不可配置过大,配置的不合理会出现服务启动时就把存储服务打满的情况。
- 线程池和连接池需要设置有区分度的名称,以便于monitor和日志记录。
- 池化大小设置多少合适?要结合吞吐量和平响要求,建议公式:并发量(连接数、线程数)= 每秒请求数 (QPS)* 处理时间。也需要考虑CPU核数,磁盘,内存等综合考虑。建议根据线上压测来实际评估。
2.4 故障处理快
故障处理关键是要快速止损,很多程序员比较爱钻牛角尖,非要定位出根本原因才去解决问题,但随着故障时间的增加,对业务的影响会变得越来越大。因此每个程序员都需要有快速止损的意识,第一时间恢复业务,故障深层次的原因待保留现场事后分析和复盘解决。
- 「快速扩缩容」 快速扩缩容体现了服务和系统的伸缩能力,这里要依赖于容器化的集群伸缩能力。由于历史问题,托管页系统有一些是运行在物理机或者无人维护的老平台上面,对于突发流量的应对简直束手无策,因此paas化是亟待解决的事情,依托于强大的paas平台的快速扩缩容的能力,能够做到快速止损。
- 「常规处理预案」在平时多积累常规的预案是应对突发故障快速处理的有效手段,故障快速定位和止损相对理想的方式是打通故障定位和预案,当出现故障时,相关开发或者运维同学能够快速判断出故障类型并及时执行预案,主要有攻击限流、机房切换、快速扩容、常规紧急case的处理流程等。
预案设计的一些经验TIPS
- 将历史出现过的case进行复盘总结,分类归档到预案。
- 建立预案时尽量方便触发和执行。
- 上线或者变更引起的故障比较常见,每次上线需要有相应的变更回滚的完整方案。
- 对于流量和容量变化引起的故障,需要周期例行化的进行线上容量评估。
- 对于机房、网络、硬件等故障,要通过适当冗余和快速的流量切换保证服务稳定性。
- 「数据备份」 当服务出问题我们可以及时通过流量切换、重启、扩容解决,但是当数据出问题,例如删库,数据丢失等问题,恢复起来成本极高,因此平时我们需要对核心数据进行备份,例如MYSQL集群的核心数据要做到天级备份,并且可以通过binlog实时回溯数据,一般需要业务方和DBA共同确认数据的备份以及快速恢复机制。
三、总结和思考
系统稳定性是一个非常大的课题,本文结合商业托管页的稳定性建设的实践,从宏观层面分类阐述了保障系统稳定性的常见方法。从故障发现、到故障影响,从故障频率到故障处理多个方面进行了总结。稳定性建设需要综合考虑业务、研发、测试、运维多方面的因素,需要各个方面协同配合。由于笔者能力有限,编写仓促,文中难免会有不准确或未能详尽的地方,请读者多多指正。