站点可靠性运维综述

VicLai 2022年11月21日

站点可靠性运维更贴近业务,可以说是业务的大管家,每天面对各类业务事务,例如服务上线、报警处理、配合网络调整、服务监控梳理、服务例行排查、预案制定、数据备份等。

网站可靠性工程师起源于Google,在其内部简称为SRE(Site Reliability Engineering,SRE需要掌握很多知识:数据算法、代码编程、大规模分布式架构、网络架构、操作系统和数据库等各个方面,还需要具备较强的故障排查和解决能力。在Google,SRE大多是资深研发工程师转岗,每位SRE都是全栈工程师,甚至是优秀的架构师。但在国内互联网公司多将SRE定义为OP(Operator),主要负责线上服务的各类操作。也正因此,国内能符合Google SRE标准的运维工程师少之又少。在我们看来,SRE的核心价值是作为全公司最了解整体服务的人,能够敏锐地把关各种变更对服务可用性带来的影响,大到业务架构的变化、网络的调整,小到程序升级、配置调整;设计或改进服务架构,使得在业务功能、可靠性、可扩展性、可运维性方面均衡发展;在服务出现问题时,能够从整体进行排查,快速发现问题点并及时恢复服务。

在我们定义的SRE工作中,有两大技术发展方向:高可用运维和运维自动化。

进阶快猫星云是一家云原生智能运维科技公司,由开源监控工具“夜莺监控”的核心开发团队组成。快猫星云打造的云原生监控分析平台——Flashcat平台,旨在解决云原生架构、混合云架构下统一监控难、故障定位慢的问题。Flashcat平台支持云原生架构、公有云、物理机/虚拟机、混合云等多种环境的监控数据统一采集,集告警分析、可视化、数据分析于一体,从业务到应用到基础设施,打通metrics、logging、tracing、event,提供立体的、统一的监控解决方案。

高可用运维

高可用运维的目的是保障业务7×24小时不间断运行,其难度在于业务在不断发展和迭代,在硬件和基础设施的调整、维护、升级过程中,始终要保证用户可以正常地访问和接入。要实现高可用的关键在于整体规划和架构设计,使业务能够支撑功能和性能的扩展,容忍局部故障和异常。SRE在进行整体规划和架构设计时需要做如下工作。

  1. 与网络工程师共同规划可以自冗余的网络架构,进而从根本上减少网站故障。
  2. 规划程序及线上环境的规范,以配合监控、部署等平台,实现自动化的网站维护工作,而不是更依赖人工管理。
  3. 规划和设计服务接入方式,提高服务对抗各类天灾人祸的冗余度,并提供方便、快捷的故障恢复预案。
  4. 辅助设计程序架构,使其拥有更强的容灾容错能力。这是提高网站可靠性、降低人工故障处理成本的不二法则。

可以看到,称职的SRE,应该能作为互联网各项技术工种的黏合剂,通过推进整体规划、程序架构设计、运维自动化的发展,提高网站可靠性并降低维护网站所需的成本。

SRE的工作开展之初,由于各方面建设都不足,各项基础工作会耗费大量的精力。因为既要以规划设计为重点来降低基础工作量,又要保证基础工作的快速推进和当前网站的可靠性。

网站运行是否稳定和可靠,需要用科学、系统的数据指标来衡量,我们称其为可用性指标。它是一个符合产品特性、明确、可自动计算的值。可用性一般用时间来表示,即在单位周期内服务故障时间;当然,也可以通过单位周期损失请求量/总请求量来表示。具体可以根据业务情况来定,这里主要说明一下时间与可用性的关系。

服务可用性常用几个9来表示,一般我们常说服务处于3个9(99.9%)阶段或4个9阶段(99.99%),其最终反映出的是因各种故障导致的服务中断,使得在某个时间范围内业务服务不能被大部分用户所使用。

从下表中可以看到可用性与时间的对应关系。

服务可用性 年故障总时间(分钟)
99% 5256
99.9% 525
99.99% 52.56
99.999% 5.256

进阶:关于SLO和不可用时长更多知识点,可进一步阅读快猫运维白皮书系列文章:《服务稳定性保障的五大误解》

当前国内大部分互联网公司的服务可用性是在3~4个9之间,也有些大型互联网公司在追求5个9的目标。一般电信、银行、证券类系统的服务可用性都是要求达到5个9的。当然,每增加一个9,所面临的技术挑战是巨大的,所付出的成本是昂贵的。这里包括基础设施、基础服务、应用服务等各个层级的稳定性建设。在单数据中心的情况下,基础设施的稳定性要大于业务,才能满足业务稳定性的要求。

举个简单的例子,一个业务的可用性要求是99.96%,而单数据中心考虑到各类硬件的平均可用性是99.8%,那么业务显然不能达到99.96%的要求。为了使业务服务达到稳定性的要求,我们就需要考虑同城多数据中心的建设,能够屏蔽由于单数据中心故障导致的服务中断。如果可用性要求高,避免同地域范围内大面积故障带来的服务中断,我们可能还要考虑建设异地多数据中心。比如,3个以上的数据中心,分布在不同的省市,甚至不同的国家。

下面列举了一些常见业务的服务可用性指标,以供参考。

1.Web服务

1. 请求拒绝率(基于HTTP协议,Nginx软件)。其计算方法主要是在单位时间内,日志中返回5xx状态码(如500、504等)的数量/单位时间内总请求量。这里我们主要使用5xx状态码,因为这个状态码表明了服务端程序有问题。虽然我们有4xx状态码的监控统计,但不作为可用性计算,主要是因为用户可以主动构造或通过一些行为得到4xx状态码。
2. 请求响应时间的Percentile(统计学术语,一般指百分位数)。计算方法为,将单位时间内所有请求的响应时间进行由小到大排序,取对应第99百分位请求的响应时间,用来评价服务整体响应速度。这里没有采用取平均值的方式,原因是平均值可能会受到某些异常Query的影响,将平均值拉高或者拉低,偏离实际情况。而Percentile更接近请求响应的真实状态和影响用户请求的占比情况。

以上两项都是因业务本身问题产生的拒绝,通过相关日志可以统计到。但如果由于公网故障、网络入口故障、接入层故障等,导致用户请求无法到达服务端,这部分损失将无法被日志记录。针对这种情况,一般采用流量环比的方式预估请求损失。也就是当天故障时段的请求数量与上周同一天、同一时段请求数量之差。当然,也会遇到特殊情况。如果是流量高速增长服务,虽然发生了故障,但环比非但没有损失,反而流量还可能有增长。这时候可以按上一小时或前一天同比推算出增长幅度,得到预期增长量,再进行损失预估。

2.存储服务

1. 读/写成功率。计算方法为,单位时间内读/写失败次数/单位时间内读/写次数之和。
2. 数据读/写时间的Percentile。思路同Web服务的请求响应时间的Percentile,这里不再赘述。

3.消息服务

消息发送成功率占比。其定义是,从系统收到消息后,在规定时间内将其成功发送到接收端的数量占比。目前我们规定的时间是1分钟内。由于考虑到用户可能处在各种网络环境中,因此我们要求90%的用户在1分钟内即可。随着系统更加的健壮,规定时间会不断调小。

每个公司不同业务有自己计算可用性的方法,特别是对于复杂系统有很多可用性指标,关键是确定一个能够代表服务整体可用性的指标。但需要注意的是,服务可用性是滞后并且宏观的,它只能反映业务服务一段时间内的整体情况,不代表可以实时和准确地反映服务的故障原因。所以,我们还需要对业务的每个功能,完善详细的服务监控,以便及时发现和定位问题。甚至可以通过对整个业务流进行监控,发现在用户请求处理过程中可能出现的故障等。

服务架构

对一个成功的互联网产品而言,产品的创意和功能是吸引用户的关键。同样的,是否能够持续稳定、快速地访问和使用也是产品成功的关键。试想:如果一个互联网产品经常性地不能提供服务,或者对用户请求的响应非常慢,或者总是丢失用户的数据,那么这个产品也一定会失败。因此,可靠的系统是支撑产品持续发展的基本和前提。可能有人认为,业务架构和设计是研发的事情。其实不然,SRE更应该重点关注并参与其中。在这个过程中,主要关注的是异常,针对系统中每个部分拟订可能发生的各种异常情况,评估系统受到影响的程度和是否可控,是否有可执行的解决方案,最终达到让系统满足高可用的设计要求,并保证落实。在本节中,我们就针对服务如何做到高可用来进行说明。

流量接入

1.多数据中心接入

接入方式分为多数据中心和多接入点两种,大多是通过域名控制接入位置的。 相对来说,多数据中心的成本更高,因为实体服务、数据等都需要在多个物理数据中心部署,相当于一个完整服务的多个备份,需要大量的服务器。出于成本的考虑,我们对于各个数据中心服务器数量的控制是高峰期30%的冗余,而不是100%。这是因为不可能任何故障都发生在高峰期,没必要为偶发情况准备很多空闲资源。另外,就算故障发生在高峰期,我们也会进行适当的服务降级,以此避免投入过多的服务器资源。

多数据中心的优点在于能适应多种故障场景,包括单数据中心的电力故障、内部网络故障、数据中心出口故障、专线故障、基础服务故障,均可通过将流量调度到其他数据中心而规避问题,属于互联网公司比较通用的解决方案。具体结构如图1所示。

图1:多数据中心结构

当A数据中心发生故障时,可以通过调整SmartDNS解析,将用户请求调度到具有独立运行能力的B数据中心。

在软件架构还不能很好地支持多数据中心接入的时候,运维人员可以考虑采用多接入点的方式,快速搭建,能够解决部分基础设施故障带来的问题。具体结构如图2所示。

图2:多接入点结构

多接入点结构在本质上仍然是一个数据中心,只是通过让用户就近接入实现快速连接、静态数据缓存加速和动态数据代理转发的能力。这种结构的优点是成本小、部署灵活,各数据中心只要放置一些代理服务器即可;缺点是对专线依赖相对较重。由于是通过代理进行请求转发的,因此只能应对数据中心出口问题,如果数据中心内部出现故障将无法规避。多点接入使用最多的场景就是通讯类业务,可以让用户就近接入,通过专线将需要转发的请求发往核心数据中心。比如在视频通话业务中,我们在一些地点部署了接入服务器,每个接入点会为一个大的区域提供接入服务,接入服务会将用户请求通过多条线路(专线或公网)向数据中心进行转发,数据中心的服务器只会处理最先到达的请求,次之的将被忽略。

2.接入调度

为了将用户请求分配到我们指定的多个数据中心或多个接入点,还需要有对用户请求进行分配和调度的能力。常见的方式如下。

(1)DNS轮询调度

这是一种最简单的调度方式,DNS就可以支持,只要将A、B两个数据中心服务器的IP地址配置到域名的A记录中即可。DNS在收到查询请求后,会将该域名下这两个数据中心的IP地址都提供给浏览器,浏览器会优先尝试第一个IP地址,如果第一个IP地址在一定时间内无响应,则会尝试第二个IP地址,在更多IP地址的情况下依此类推。理论上各IP地址位于第一个的概率是均等的。这种方式有一个不足,就是只能按流量进行分配。当然,如果你想让A数据中心多得到一些流量,就在A记录里加两个A数据中心IP地址、一个B数据中心IP地址。同时,由于Local DNS的存在,无法保证流量的负载均衡,属于伪轮询模式。 在DNS轮询调度中,还可以根据用户的IP地址归属,进行更精细化的调度。

(2)DNS智能调度

这种调度方式一般根据用户属于哪个运营商、用户在哪个地域进行流量调度。这种基于用户IP地址归属的DNS策略调度系统,我们称为SmartDNS,比较知名的商用DNS服务提供商均支持该功能,如DNSPod。 通过智能解析策略,可以使用户的请求按照用户IP地址属性分配到不同的接入点或数据中心,比如将联通用户落到A数据中心,电信用户落到B数据中心。同样也支持按用户地理位置归属进行调度,如美国的用户调到C数据中心,日本的用户调到D数据中心。

使用SmartDNS解决了区域识别的问题,但同样也存在着不足。SmartDNS是根据用户使用的Local DNS来判别用户的地域属性和运营商属性的,这里面有一个前提假设:用户会设置使用离自己最近的本地运营商Local DNS;但如果用户自己设定了错误的Local DNS,那么就会出现调度错误,出现用户跨网请求的情况,导致访问速度变慢。比如一个用户的DNS设定了8.8.8.8(Google提供的DNS服务),那么SmartDNS就会认为该用户在海外,最终将请求分配到了海外数据中心。所以,如果能够根据用户的真实IP地址进行判别,那么就会比通过Local DNS来判断准确多了。

Google和OpenDNS都支持edns-subnet-clinet协议,它是DNS的扩展协议,会记录用户的IP地址信息传递给在其注册的对应的Name Server服务器(当然,这需要对应的Name Server服务器也支持该协议)。这样Name Server服务器就能获取到用户的IP地址信息,根据用户的真实地域和运营商进行更精确的调度。

无论使用Local DNS还是用户IP地址方式进行判别,IP地址库的准确性都决定了调度结果,所以周期性更新IP地址库,对用户访问服务的效果起着重要的作用。

(3)App/客户端自主调度

随着移动互联网的兴起和发展,大量的业务应用又从B/S模式转换为C/S模式,App/客户端不仅是离用户最近的接入层,同时也是业务逻辑的一部分,因此调度策略也在向客户端进一步延伸。例如,通过HTTP请求先从服务端获取服务IP地址列表,然后按用户所属区域、号段(一般是大的区域划分,比如国家)等客户端属性来选择合适的服务端IP地址。更进一步,客户端定期地对服务端IP地址进行质量评估,包括速度、链路和访问质量等,综合服务质量评估结果来选择最优的服务端IP地址。

(4)私有调度策略

最常见的场景是业务灰度测试,一般不考虑用户的接入点,而是通过内部二次调度,将具备某些特征的用户调度到测试服务器,进行功能测试验证。

比如我们的账号系统,在SmartDNS的IP地址库中增加一个公司IP地址段,将公司内所有用户的账号访问落到测试服务器。

再比如广告系统,将某个地域的有车族的用户(业务分析得出的特征)调度到测试服务器,来评估某广告策略效果。

3.流量负载均衡

所有的用户请求到达接入数据中心后,我们对接入流量有负载均衡处理的能力。对于TCP协议的服务,我们主要使用LVS集群进行负载均衡。首先,这样能够保证业务接入层的可扩展性与高可用性,能够做到对用户透明、方便地伸缩。这是LVS本身的容错机制实现的。另外,外网IP收敛后,其变化的频度也会大幅减小,对应的DNS修改也会减少,因为各级DNS延迟生效所带来的问题也会规避。其次,LVS也起到了安全隔离的作用,将内、外网进行隔离,只需要对LVS集群进行安全加固即可。LVS有一定的半连接攻击防御能力。

而对于UDP协议的支持,目前还没有成形的解决方案。很多公司大都在一些开源框架或开源软件的基础上自行开发。

系统设计

1.多数据中心

多数据中心不仅是数据中心规划的技术问题,对于业务系统架构设计而言,也是一个挑战。 网络、数据中心是保障网站提供正常服务的核心基础设施,一旦出现问题,对上游业务的影响将是巨大的。在单数据中心的情况下,如果网络、IDC的服务可用性不能达到99.9%,那么其所承载的业务就算自身再稳定,也无法做到高可用性。即便达到电信级的99.999%的可用性,也一定会出现问题。这时就需要进行多数据中心建设(多个物理上分开的数据中心,之间通过两条或多条专线进行互联),毕竟两个及两个以上数据中心同时出问题的概率非常小。而在多数据中心结构下,业务系统也需要在软件层面满足应用、数据分区域独立运行的能力,以便做到通过灵活的调度,将故障数据中心屏蔽,使业务服务不过度依赖单数据中心的服务可用性。

对于服务上量比较大的业务,容量扩张也是关键问题。随着流量的不断增长,需要扩充更多的服务器来提升处理能力,而每个数据中心的容量是有限的(很多大厂的服务器数量都在10万台以上),最终结果是单数据中心无法承载全部服务,这时需要向其他数据中心分摊压力。因此,对于这种业务,在运维工程师接手时,要求其必须是多数据中心部署服务,具备同时提供服务的能力。具体要求如下。

(1)服务要有分区域(区域是一个逻辑概念,一般把一个数据中心定义为一个区域)自治能力,即当单一区域出现故障时,其他区域服务不受影响。每个区域有独立的基础设施及服务,如带宽出口、DNS、SNAT、NTP等,业务层面包括ZooKeeper、消息队列、数据存储等。如果数据有一致性要求,则可以定义一个区域作为主区域,数据都写入到这里(单点),其他区域将数据同步到本地,供本区域内读取、查询使用。

(2)尽量规避跨区域的RPC调用。主要有两个原因:一是数据中心间访问网络时延大于同一数据中心;二是数据中心间专线的可靠度差,施工挖断光纤等情况相对比较常见。当然,也可能有些情况不能避免。比如即时通讯类应用,用户是按照组(Partition)区分的,组与组之间的消息通信频繁,就需要保障跨区域交互。研发人员需要认可专线的风险,在出现中断时,系统设计要有能力动态迁移用户。

2.服务解耦

在网站规模由小到大的发展过程中,一般会从功能单一、结构简单逐步发展膨胀为功能众多、结构复杂的巨无霸系统,甚至到后来没人能绘制出系统全貌,最终变成不可运维的状态。特别是当服务扩容时,由于上、下游调用的信息维护不完整,容易导致发生故障。对策应该是在早期就考虑模块拆分、解耦。如果实例间不存在直接的调用关系,那么就不应该耦合在一起,要做到模块的变更不影响上、下游。这里举个解耦例子:利用分布式消息队列来降低系统与系统之间的耦合性。

从图3中可以看到,消息队列是发送者生产消息,一个或N个消费者订阅消息的工作模式。在这里,发送者和消费者之间没有直接的调用关系,发送者只是将消息放到分布式队列中,消费者也只需要从队列中获取消息进行处理,不用关心消息从何而来。因此,上游新增业务产生新的消息,对原有其他业务没有影响;而下游新增消费者,对上游也没有任何影响,只要订阅它想获取的消息即可。我们常用的开源软件有RabbitMQ、Kafka等。

图3:解耦示例示意图

3.业务分离

如果业务较多,最好能按业务类型在物理上进行归类、分离,以免相互影响。分离的原则一般分为三个维度来进行考虑。

(1)按业务类型分离。最常见的就是把动态资源和静态资源分开,这样可以针对不同的类型进行各类优化,让服务质量更好。比如静态资源,我们可以申请大内存或大磁盘容量的服务器,更多地进行本地缓存,或者使用CDN技术;而动态资源如果是HTTPS的,在硬件上我们还会为其配置SSL加速卡。

(2)按安全等级分离。一般来说,大家都比较关注主站,往往忽略了周边小站的安全性,而入侵事件一般都来自于薄弱处,所以需要按安全等级分离。这种分离不仅仅是服务器层面的,甚至在网络层面也需要进行分离。比如我们使用的开源论坛程序,它的安全风险就比较高,如果论坛服务的服务器与内网其他服务器互通,一旦论坛服务被入侵,后果将不堪设想。

(3)大流量服务分离。比如电商类推广、抢购活动、离线计算集群(数据传输)等,它们的业务可能会带来公网、内网的大规模流量负载,和其他业务共享网络带宽时,可能带来影响。而网络QoS并非解决这类问题的最好办法,最简单、可靠的方式仍然是物理分离,为它们规划独立的区域。

4.容量规划

业务分离后,系统内部也需要考虑服务容量、部署以及服务之间是否会相互影响等问题。考虑这些的出发点主要是成本,一般情况下,一个实例是用不满服务器资源的,因此我们希望通过混合部署来最大化利用服务器的各种资源。但混合部署也需要考虑很多因素,混合部署不周反而会导致服务不易管理,致使服务出现异常。在服务混合部署上需要工程师了解4部分信息。

  • (1)不同类型的服务器配置及处理能力。比如磁盘IO,需要知道在顺序读/写与随机读/写的场景下,其性能表现。
  • (2)服务的业务特性,主要消耗哪些系统资源等。
  • (3)业务访问量与系统承载的比例关系。
  • (4)明确知道业务系统中的瓶颈点。

根据以上信息,我们会对服务的混合部署做出合理的规划,这对于规避潜在的风险有着重要的作用。由于互联网产品迭代速度非常快,容量及短板也会因此发生变化,因此需要在测试阶段引入压力测试,以便于了解系统容量变化情况,并适时做出调整。根据以往经验,在混合部署过程中我们遵循以下几个原则。

  • (1)具有相同资源消耗特性的服务不部署在一台服务器上。例如Redis和Kafka,它们消耗内存,也消耗IO;而Nginx和Kafka则可以考虑部署到一起。
  • (2)敏感业务即便资源消耗再少,也要单独部署。比如密钥生成与管理服务、授权认证服务等。
  • (3)在遵循上面原则的情况下,如果服务器容量允许,且属于同一团队管理,则可以尽量将上、下游服务放到同一台服务器上,以减少网络通信的开销。

以上几点主要还是靠人来进行综合评判的,并且需要一定的运维经验。我们正在努力将其转变为系统自动评估,主要依靠虚拟化技术及调度系统来减少人工维护成本。

虚拟化技术主要对资源进行有效隔离,对硬件资源进行划分,并打破路径、端口、账号等资源的限制,工程师不用再根据物理服务器的归属来进行部署规划了。而引入调度系统后,也不再由工程师根据服务器资源使用情况进行部署规划,只要说明每个实例所需要的资源量即可。

5.容错机制

容错机制对集群系统来说是非常重要的保障功能,一方面,服务器经常会出现各种故障,如果集群规模大的话,那么宕机可能是常态的事情;另一方面,互联网的服务每天都会有很多版本更新,这个过程需要重启服务的运行实例。如果集群系统不能对这些异常进行容错,服务可用性将非常低,严重影响用户体验。而且维护成本也将非常大,工程师需要随时准备操作,屏蔽故障服务器与实例。因此容错是集群系统所必需的能力,属于可运维性的最低要求。

线上服务部署,通常是网状交叉互联,需要每一层服务都有容错能力。只有这样才能保证任意层的任意服务实例故障,均可被容错忽略。尽量避免单点服务,由于单点服务往往有状态,因此为其设计高可用方案使其自动恢复的难度也很大。

常见的容错方式有两种。

  • (1)通过定时的心跳检查,判别服务器或实例端口是否存活,如果处于非存活状态,则会将这台服务器或这个实例端口屏蔽,使流量不再分配到这台服务器上。这种方式的优点是可以将故障实例彻底屏蔽,后续请求不会产生过多的开销;缺点是两次心跳检查之间还会有请求落到异常实例上,造成少量损失。
  • (2)将请求分发到下游服务器,如果在规定的时间范围内没有得到下游服务器的返回,则认为下游的这个服务实例有问题,会将本次请求转发给其他服务实例。这种方式的优点是不会造成流量损失;缺点则是由于多次重试,导致负载增加,上游请求易出现堆积,不进行重试次数控制,还可能造成持续压力下的服务雪崩。重试次数上的控制,一般建议2~3次。

结合上述两种方式的特点,还可以设定一些高级的容错策略,如连续3次到这个实例的请求都失败,则暂时屏蔽5分钟;如果5分钟后依然有问题,则再屏蔽5分钟。

6.减少依赖

这里的依赖主要指两种,一种是对运行环境的依赖,如Java、Python、PHP等,由于不同服务可能依赖的基础环境版本不同,如果服务A需要Java 1.5,而服务B需要Java 1.6,那么它们在混合部署时,路径文件及系统变量都会产生冲突,而物理分离又浪费资源。因此,在概念上最好每个服务都能够做到自包含自己运行所需要的依赖,但冲突肯定很难避免。目前的虚拟化以及资源隔离技术可以很好地解决这类依赖、路径、端口等方面的问题。另一种是对基础服务的依赖,如DNS、ZooKeeper等。在日常运维中,为了便于记忆,我们会给每台服务器分配一个服务器名,通过名字就能大致知道其在哪个区域、产品线归属、用途等信息,记忆、管理都比记IP地址要方便很多,包括程序的配置文件中也都配置了服务器名。但同时这也是一把双刃剑,在提高了可管理性的同时,也引入了新的耦合。比如当DNS服务出现问题时,由于服务器名解析失败,很可能导致程序运行或通信的异常。这就需要增加本地缓存机制,以防止类似问题的发生。

7.缓存机制

很多网站都会用到缓存技术,该技术主要是为了提高数据的读取速度。常见的开源缓存系统包括MemCache、Redis。当然,如果私有服务支持缓存的话,也有很多的缓存框架可以选择,甚至还可以做成多级缓存,这就需要看业务的具体需求了。

缓存除了可以提升业务系统本身的性能外,也可以提高对下游有状态服务的故障容忍度,乃至提高整个业务服务的可用性。例如一个有读缓存,且缓存命中率高达90%的业务服务,当下游状态服务出现故障时,仍有90%的请求可以正常服务;一个有写缓存队列的服务,在写缓存满之前,写服务的故障也是可以使用户无感知的。

8.服务保护机制

在服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心功能的正常运行。常见的服务保护机制主要有两种。

  • (1)限流。在网络或应用层面对用户请求进行访问限制,通过这种方式保证一部分用户可以正常访问,而另外一部分请求直接在网络层丢弃,或者在Nginx层被指向到特定的“服务器繁忙”页面。
  • (2)功能关闭。将某个功能的入口临时去掉,或者在程序中将其关闭,从而使该功能不可访问,以保证主功能的正常。比如关闭写数据库的功能,用户只能查看信息。

限流或功能关闭的开关及策略应该做成可配置的,以便根据情况随时调整,一般这些项都体现在程序的配置文件中。为了能够快速、简便地操作,我们对配置进行了一些规范化要求。首先,我们将配置中的内容分为三类,它们的放置位置有所不同。

  • (1)程序自身策略描述。比如读取本地字典、加载数据、设定编码等。这类配置在运维过程中很少关注,跟着服务部署即可。
  • (2)连接类描述。比如配置了连接下游哪些服务器、端口等信息。这类配置和运维直接相关,应该能够很方便地看到,而在服务器和服务数很多的情况下,人工管理又很麻烦,因此在增、删、改上最好能够自动管理。建议注册到ZooKeeper中,当然,如果你有私有的Naming Service更好。如果是开源软件,比如MySQL、MemCache,则可以写个外包程序,负责注册和摘除。
  • (3)限流、功能开关类描述。比如某个功能的开关项、限制连接数配置等。这类配置一般用于异常情况,操作都是紧急的,所以修改需要方便,生效需要快。建议这种配置最好能够存储在ZooKeeper中,工程师可以快速修改内容,并使其生效。对于Nginx这类开源工具,我们还是使用其本地配置进行控制,如连接数、请求数等。

服务保护的方式分为自动和手动两种:

  • (1)自动。程序创建一个特定线程,对所关心的资源进行定期的健康检查,如连接数、内存使用情况等。当某个指标或多个指标达到一定阈值时,则自动启动服务保护策略,开始限流或关闭某些功能。比如我们的即时通讯系统,接入层程序会判断其当前的用户连接数,如果达到上限,则新的连接请求将被拒绝,直到有资源被释放。还有一种是通过客户端程序进行自动服务保护,当客户端收到服务端的错误返回码(如HTTP协议中的500、503等错误)后,会缓存用户的操作,过一段时间再重试。这种通过客户端服务保护的方式主要面向实时性要求不高的功能场景,比如数据同步到云服务上。
  • (2)手动。主要是人工判断服务有风险后,进行相关配置修改,或者通过向程序发信号的方式来进行功能开关控制或限流。比如,Nginx这种开源软件可以通过修改limit_conn等进行限流或限制连接数。

9.故障预案

虽然我们的程序有负载均衡、自动容错,自动服务保护等机制,但还不一定能够应付所有情况。比如数据中心级故障时的切换,就需要人工介入。有时还会遇到程序假死、服务器假死、内部网络丢包等情况,这些情况会让服务运行时表现得时好时坏,上游服务的容错等机制无法有效识别,最终导致整个服务异常。

我们在运维过程中常说的一句话是:“不怕真死,就怕半死不活”。因为在这种状态下产生的不确定情况最多。比如内网抖动,对于配置了自动切换的单点业务来说,可能会出现脑裂让系统产生双主,从而导致数据写入发生混乱。因此,在系统设计阶段,我们要识别自动机制不能覆盖的异常场景,针对这些场景应该有对应的规避方案(工具、操作方案等)。

明确预案的原则,当然,这是理想状态,但我们希望尽量向这个目标靠近。

  • (1)方案执行简单。服务异常时,肯定是越快恢复越好。因此,操作简单才是王道,最好是有工具帮助进行步骤简化。比如主从切换及相关检查动作。当然,对业务也提出了很多要求。例如程序要主动输出重要信息,以满足自动化处理的要求等。
  • (2)一套方案应对各类灾难问题。这里指的是非服务程序bug导致的问题(一般都是基础设施、基础服务故障)。出现故障时,对于运维来说,不是马上找到问题,而是马上规避问题。因此,我们肯定希望尽量少地判断,能够以一种方式以不变应万变最好。比较常见的就是域名切换,不管数据中心内出现了什么问题,影响范围到底有多大,判断只要不是业务自身问题,就将流量引入非故障区域,然后再慢慢定位。
  • (3)所有业务执行方式统一。公司的业务非常多,域名也很多,如果在出现故障时,几个人就能快速完成所有域名的流量牵引工作,就可以把故障损失降到最低。我们的域名管理系统对域名中的每个IP地址做了区域tag标记,当某个数据中心有问题时,只需要一个人执行A tag流量切到B tag,快速完成流量切换。

10.水平扩展

在业务流量增加,当前集群不能负载的时候就需要进行资源扩充,所以在服务设计的前期,服务集群是否能平滑地水平扩展,是运维准入的必要条件。在进行服务容量伸缩调整时,主要会涉及数据、应用,这两部分决定了扩展的效率和是否会对用户有损。

  • (1)应用程序与数据分离

数据的概念比较广泛,在这里主要指程序生成的或程序需要读取的文件(配置文件除外),举几个数据的例子,如字典、索引、用户信息记录文件等。数据对于业务的重要性这里就不多说了。 在我们的运维标准中,程序的数据和程序本身是要分开的,主要是为了服务扩容时快速和便捷。对于数据,我们建议优先使用分布式存储系统,如Hadoop、HBase、MFS等,主要是因为它们的稳定性非常高,并且有专门的研发、运维工程师进行管理,不用每个业务再去过多考虑数据的安全性、完整性等问题,在节省人力的情况下还能提高数据服务的可用性保障。

  • (2)数据读/写分离

以MySQL数据库为例进行说明。一般数据库的配置都是一主一从,或一主多从,甚至是级联式的多从。所谓读/写分离,就是基于这种结构,业务程序在功能上支持并且可配置,增、删、改请求只发向Master服务器,而Select等查询操作发向Slave服务器,且读数据库功能支持前面所说的容错策略。数据读/写分离示意图如图4所示。

图4:数据读/写分离示意图

  • (3)服务状态

在集群系统中,我们希望其中的节点可以随时进行伸缩调整,并且对用户透明。对于程序来说,分为有状态服务和无状态服务两种。

无状态服务最常见的就是Web服务,由于Web服务不负责保存访问时序、用户状态等信息,每次请求都是独立的,所以当实例增、删时,不影响请求的正常处理。这里用一个搜索网站的架构来说明,如图5所示。

图5:数据读/写分离示意图

图5中包括Web Service接入层、页面拼装层(A服务)、数据层(B服务)。Web Service有容错机制,当下游服务异常时,Web Service会尝试将请求发给其他同功能实例进行处理。A服务在伸缩过程中,只需要修改上游Web Service配置即可,这便是无状态服务的特性。

在后来的发展进程中,逐渐在无状态化的过程中加入状态化信息,比如Cookie。服务端在响应客户端的请求时,会向客户端推送一个Cookie,其记录服务端上的一些信息。客户端在后续的请求中,会携带这个Cookie,服务端可以根据这个Cookie判断这个请求的上下文关系。Cookie的存在,是无状态化向状态化过渡的一个手段,它借助外部扩展手段,通过Cookie来维护上下文关系。状态化的服务端有更广阔的应用范围,比如即时通讯类软件、网络游戏、支付等服务。在服务端维护每个连接的状态信息,服务端在接收到每个连接发送的请求时,可以通过本地存储的信息来重现上下文关系。这样,客户端可以很容易使用默认的信息,服务端也可以很容易进行状态管理。比如在即时通讯类软件中,当一个用户登录后,其在线好友能够马上收到该用户上线的通知消息。

无状态服务在处理简单的服务方面有优势,但在复杂的功能方面有很多弊端。状态化服务端在功能实现方面具有更加强大的优势,但由于它需要维护大量的信息和状态,在性能和可维护性方面要稍逊于无状态服务。下面我们用一些应用场景来说明状态化服务端如何满足水平扩展。

试想一款即时通讯类软件,其有上亿用户。由于资源限制和可用性要求,这些用户不可能放在同一台服务器上。最开始的结构是按号段进行划分的,即固定的服务器上承载固定的号段(如A1~A3服务器承载1~1亿号段的用户,B1~B3服务器承载1亿~2亿号段的用户,依此类推),通过人工管理服务器与业务状态的对应关系。这样的架构是经典的垂直扩展,扩展需要人工修改服务器承载的号段范围,而这种修改必然会导致其他号段服务器的配置修改。伸缩过程麻烦,不灵活,易出错。后续我们引入了一致性Hash,通过算法来自动维护号段与服务器的关系,可以自动计算集群中的服务实例个数,根据一致性Hash算法的计算结果,将请求发到对应服务器上。当某个号段的服务器都宕掉时,可以自动重新计算对应关系,进行自动迁移。在这个案例中,我们通过算法解决了状态化服务的扩展问题,如图6所示。

图6: 通过算法解决了状态化服务的扩展问题

再来看一款社区产品。有很多用户上传数据需要存储,并保持时序,比如用户评论信息的展示,在请求落到不同服务器时,展示顺序需要相同。为了满足这样的要求,最开始的系统设计示意图如图7所示。

图7: 最初的系统设计示意图

每个业务服务均具有ID分配的能力,并各自保持请求的时序化。比如一个评论服务,用户提交评论时,直接由评论服务从自己的ID段分配唯一并具备时序的ID,带着顺序ID的评论,在写入多个存储时,就可以保持一致的顺序,不会发生在不同存储中评论顺序不同的问题。但这样的设计,每个服务都维护自己的ID分配状态,也就使每个服务都无法水平扩展,新增实例时都要面对复杂的配置修改和同步ID过程。为了满足运维和服务的高可靠性要求,我们将系统进行了调整,调整后的系统设计示意图如图8所示。

图8: 调整后的系统设计示意图

在这样的设计中,把ID分配抽取成独立服务,由其保持请求时序化,并将ID及其对应数据分发至下游所有业务服务。这样每个业务服务不再维护状态,也就具备了水平扩展能力;而ID分配服务,通过主备等模式做到一定程度的高可用性。

其实解决水平扩展问题就是去状态化,无论是数据分离还是读/写分离,都是通过将状态逻辑进行剥离,使剩余逻辑无状态化,从而具备水平扩展的能力。剥离出来的有状态逻辑可以作为独立运行的系统服务,再通过主备等模式实现其高可用性。通过这种方式实现服务最大化的无状态,最大程度地满足业务可靠性要求。

运维自动化

运维自动化和高可用性是息息相关的,高可用运维会规范化架构设计和业务环境,持续性地提出运维自动化需求,以提高网站可靠性保障,进而降低运维人工成本;运维自动化系统除了提升服务管理的高效和便捷外,反过来还会促进高可用运维的规范化。

我们的自动化运维系统主要包括部署、监控、服务发现、服务管理、服务器生命周期管理、域名管理、资源调度几大基础设施组件。如图9所示是我们的总体规划示意图。每个部分都会有对应的章节进行详细介绍。

图9: 总体规划示意图

在自动化建设过程中,上面提及的这些基础设施组件也不是一次建设完成的,有其规划的先后顺序。首先,我们需要将服务器管理起来,建立服务器及上面所部署服务的关系。然后,需要解决服务的监控问题,如果服务异常都不能及时发现和处理的话,那么更谈不上后续的运维自动了。接着,我们需要重点解决最占日常工作量的事情,即服务变更和部署。解决部署问题能够提升所有技术工程师的工作效率,也方便开展后续的自动化工作,比如服务扩容、服务搬迁等。最后,就需要着手解决服务间关联关系的管理问题,建设调度系统,实现业务的自动伸缩。自动化系统的实现也需要业务服务具备可容错的能力,同时能够配合自动化运维系统,进行关键运维指标的上报。

运维自动化是整个站点可靠性运维工作中的重中之重,运维自动化建设的程度,在很大程度上决定了高可用运维的工作模式,甚至会改变整个业务体系架构。接下来会详细介绍各个运维组件的功能,以及我们的设计思路和实现方式。

本文遵循「知识共享许可协议 CC-BY-NC-SA 4.0 International」,未经作者书面许可,不允许用于商业用途的转载、分发、和演绎。本文内容,由 Wilbur、VicLai 等共同撰写,仅代表相关个人见解和立场,与任何公司、组织无关。

标签: SRE