概述
服务高可用性(HA),指的是在发生故障时,服务任然能稳定的提供预期功能的能力。常用可用率来衡量系统的高可用性,计算公式为可用时间/总时间。高可用系统通常要求可用率高于99%。
通常情况下,为了实现系统的高可用,我们需要从一下这些方面去考虑。包括冗余部署(如主备切换,集群部署),容错机制(如熔断,限流),故障转移,实时监控和预警等。这些技术可以确保系统在发生故障时能够迅速恢复服务,减少停工时间。
架构设计
在一个复杂的分布式系统中,我们首先要考虑的就是从整体架构上拆分微服务,保证在发生异常时各个微服务间互不影响,且可以单独横向扩展。
这样的拆分有如下优势:
独立开发和部署:每个微服务可以独立的进行开发,测试和部署,提高了开发效率和灵活性。
故障隔离:当一个微服务出现故障时,不会影响到其他微服务的正常运行,从而提高了系统的可用性。
可扩展性: 可以根据需要独立的扩展每个微服务,以满足不同业务场景下的需求。
同时,各个系统间可以采用轻量级的通信和负载均衡机制进行通信,避免单点故障,提高系统的吞吐量和可用性。
冗余设计
通常一个部署上线后部署在生产环境都会有很多机器,这样可以极大的提高服务可用性,某几台机器宕机了也不会影响整个服务。
我所负责的现网业务也采用了冗余部署,同时也是多地域多可用区的部署模式,即使遇到某个机房断电等场景,其余地域也能正常对外提供服务。
但是我们单单提供我们应用的冗余部署还是不够的,因为即使我们的应用可用性非常高,但是所依赖的外部服务,如db,缓存,消息队列等是单节点部署,仍然会出现单点故障的问题。以数据库为例,我们可用采用主从架构,并配置相应的告警和处理机制,当主节点宕机时,可以选择一个合适的从节点作为新的主节点,以此保证高可用。
冗余不光在解决高可用的场景中有重要的意义,在解决其他场景问题时也是一大利器。
例如: 在某大厂的订单系统中,初期业务量较小仅单机的mysql就够用,后续订单量激增切换为分布式db后,自然而然的采用的用户id进行分片的逻辑,这样保证了用户层面查询数据的性能。但是在商家维度如何解决呢?他们就采用了冗余的方式,同样的数据冗余出来另一份,使用商家id进行分片,这样做就解决了商家查询的性能问题,后续也增加了订单id的冗余,解决了订单id查询的问题。
因此,冗余是分布式系统实现高可用的一个重要的方式,在很多地方都能看到它的影子,例如hdfs的多副本,kafka的多副本等等,都是为了解决高可用问题。
服务可用性策略
负载均衡
使用nginx,haproxy等负载均衡器,可以将用户的情况均匀分发到多个后端服务器上,避免单点故障,提高系统的吞吐行和可用性。
我所涉及的业务场景主要是将一批数据按业务逻辑处理后调用下游接口进行推送处理。我的服务采用了多节点的部署,同时底层存储也采用了分布式数据库,使用用户id进行分片。
因此我采用的负载均衡策略是,先查询出当前周期内所有需要处理的用户账号,然后根据账号的数量和机器数量,将所有的账号hash到各个机器上处理,使得每个负载所处理的数据基本一致。当某些产品可能出现大客户的case时,我也可以细化我的分片参数,增加更多的参数,例如 用户账号+地域+可用区分片等。
如果还不能解决大客户的问题,那么对于一些已知的大用户,我们只能采用单独集群的方式,对大客户数据进行负载均衡。
重试与补偿机制
重试
重试主要是体现在远程的rpc调用或网络调用等,受到网络波动,线程资源阻塞等因素影响,请求无法及时相应,那么为了提高可用性,我们可以通过接口重试的方式再次发送请求,尝试获取结果。
重试通常需要跟幂等组合使用,如果一个接口支持了幂等性,那么多次重试也不会有其他影响。
关于幂等大致有如下的解决方案
前端拦截
通过前端页面进行请求拦截,如用户点击提交按钮后,将按钮设置为不可用或隐藏状态,避免用户重复点击。
这种方法简单有效,但存在被绕过的风险,如懂行的程序员可以模拟后端请求直接调用。
数据库层面
悲观锁
通过数据库的事务和锁机制来保证幂等性。例如,在mysql中使select for update 语句锁定数据行。
这种方式实现简答,但可能影响性能,因为会锁定数据行,导致其他操作等待。
乐观锁
通过版本号或时间戳等字段来控制并发更新,在更新数据时,检查版本号或时间戳是否发生变化,若未变化则进行更新,并增加版本号时间戳。
这种方式效率高,适用于读多写少的场景。但数据从未更新到已更新的瞬间,如存在并发更新,则可能产生冲突。
唯一索引
未数据库添加唯一索引,确保数据业务的唯一性。在插入或更新数据时,如果唯一索引字段重复,则操作失败。
这种方式实现简单,效率高,但防重过滤压力在db上,访问量大时可能影响性能。
防重表
新建一个数据库,专门用于记录已经处理过的请求,在处理请求前,先检查该表中是否存在相同请求的记录。
可以根据业务需求定制防重逻辑,但是增加了存储和维护成本。
业务代码层面
状态机
在业务代码中维护状态机,确保操作按预定义的状态顺序执行。
从业务代码层面进行去重,数据库无压力,但实现复杂,需要精确控制状态转换逻辑。
唯一标识符
为每个操作生成一个唯一的标识符,将该标识符作为参数传递给接口。在接受到重复请求时,检查该标识符是否已被处理。
分布式锁
在分布式系统中,使用分布式锁来控制对共享资源的访问,在执行业务操作前,先尝试获取分布式锁,如果获取成功则执行操作,否则放弃或等待。
适用于分布式环境,能够有效防止并发问题,但实现复杂,且可能影响性能。
token机制
客户端在发送请求前,先向服务端请求一个token,服务端在接收到请求时,检查token的有效性,如果有效则处理并删除token,如果无效或已过期,则拒绝请求。
实现简单,但是token的生成,传递和校验过程需要保证安全
补偿
前提:业务能接受短时间的数据不一致
我们知道,不是所有的请求都能收到成功相应。除了上面的重试机制外,我们还可以采用补偿的玩法,实现数据的最终一致性。
业务补偿根据处理的方向主要分为两部分
- 正向:多个操作构成一个分布式事务,如果部分成功,部分失败,我们会通过最大努力机制将失败的任务推进到成功状态。
- 反向:同上道理,我们也可以采用反向操作,将部分成功的任务恢复到初始状态
补偿的实现方式“
- 本地建表,存储相关数据,然后通过定时任务扫描提取,并借助反射机制出发执行。
- 也可以采用简单的消息中间件,构建业务消息体,由下游的消费任务执行。如果失败,可以借助mq的重试机制,多次重试。
在我所负责的业务场景中,待结算数据主要根据不同的结算周期推送下游处理,可以接受一段时间的sla。这里我首先采用了代码层面的重试,调用下游接口失败后,进行重试,最大支持重试5次,若重试5次还未出成功,人工介入进行排查,同时我的待结算数据也包含对应状态信息,并单独拆分一张表,来持久化当前数据的状态,下游接口也能保证幂等性。
那么也存在一些数据,下游接口返回成功,但是由于结算为异步处理的,下游接口也可能结算失败,对于这部分数据,我首先加入了和下游数据的对账能力,在sla前保证我需要让下游处理的数据都已经处理完毕。然后对于一些还存在异常的数据,进行补偿处理。同时下游接口也会将异步结算失败的数据写入异常的kafka中,我们通过监听该kafka也能获取到处理失败的数据,来异步刷新它的状态。
故障处理机制
限流
高并发系统,如遇到流量洪峰,超过了当前系统的承载能力,应该如何处理。
一种方案,照单全收,cpu,内存,负载飙高,最后所有请求都超时无法正常响应。
另一个方案,多余的流量我们直接丢弃。
限流主要分为单机限流和分布式限流
- 单机限流主要借本机内存实现,无需和其他节点统计汇总,性能最高。但是有点也是缺点,无法做到全局统一的限流
- 分布式限流
单机版限流能保护自身节点,但无法保护应用依赖的各种服务,并且在进行节点扩容,缩容时也无法准确控制整个服务的请求限制。而分布式限流,以集群为维度,可以方便控制这个集群的请求限制,从而保护下游以来的各种资源服务。
常见的限流算法有
- 计数器法(固定窗口算法)
原理:将时间划分为固定的时间窗口,每个窗口内允许的请求次数设置限制。如果在一个时间窗口内,请求次数超过了上限,就会触发限流。
特点:实现简单,但在窗口切换时可能会出现流量突增的问题。 - 滑动窗口算法
原理:以当前时间为截止时间,往前取一定的时间(如60秒),计算这个时间段内的请求数量,并与设定的阈值进行比较。如果请求数量超过阈值,则触发限流。
特点:相比固定窗口算法,滑动窗口算法可以更平滑地处理流量,避免窗口切换时的流量突增。 - 漏桶算法
原理:维护一个固定容量的漏桶,请求以不定的速率流入漏桶,而漏桶以固定的速率流出。如果请求到达时漏桶已满,则会触发限流。
特点:可以平滑突发流量,但处理不够灵活,且无法动态调整流量。 - 令牌桶算法
原理:基于一个令牌桶的概念,令牌以固定的速率产生并放入桶中。每个令牌代表一个请求的许可。当请求到达时,需要从令牌桶中获取一个令牌才能通过。
特点:可以平滑突发流量,并允许一定程度的突发流量,灵活性较高,但实现相对复杂。
常见的限流策略有
直接拒绝:当请求超过阈值时,直接拒绝超出的请求。
排队等待:将超出的请求放入队列中,按照一定的规则进行处理。这可能会导致请求处理延迟增加。
延迟处理:将超出的请求延迟到下一个时间窗口处理。
动态调整:根据系统的实际负载情况动态调整限流阈值。
熔断
定义:熔断是一种防止故障扩散的策略。当某个服务出现故障或超时,熔断器会打开并快速失败,拒绝后续的请求,从而避免请求堆积和资源耗尽。熔断器会暂时屏蔽该服务,并在一段时间后尝试恢复。
目的:防止故障服务继续对依赖它的服务造成负面影响,快速失败并保护系统资源。
状态转换:熔断器通常具有三种状态——Closed(关闭)、Open(打开)和Half-Open(半开)。在Closed状态下,请求被正常处理;在Open状态下,请求立即失败;在Half-Open状态下,允许有限数量的请求通过以检测服务是否已恢复。
降级
定义:降级是一种在面对特殊业务或异常情况时保持系统可用的策略。当服务不可用时,降级服务会代替提供一些基本功能或返回预设的默认值,以确保系统依然能够提供有限的功能或服务。
目的:优先保证核心功能的稳定性和可用性,减少对故障或不稳定服务的依赖,提高整体系统的韧性和可用性。
实现方式:降级可以是手动配置的,也可以是根据自动监控和预定义策略进行的。
自动化运维与监控
监控与告警
监控系统用于实时监测系统的各项指标(如CPU使用率、内存占用、网络带宽、请求延迟等),以便及时发现并处理潜在的故障和异常。
告警系统与监控系统紧密配合,当系统指标超过预设阈值时自动触发告警,通知相关人员进行处理。这有助于快速响应系统故障,减少停机时间。
我们也不仅仅只监控我们应用本身的负载,对于所依赖的db,缓存,队列等组件,也应该添加相应的监控告警能力,提高异常的发现能力
自动化运维
自动扩展与缩容技术根据系统的负载情况自动调整资源分配,实现系统的动态扩展和缩容,提高系统的灵活性和可用性,当出现流量激增时,可以动态扩容来提高系统的承载能力。