高可用(3)- 设计手段

最后更新:2020-06-26

1. 设计层面

设计高可用方案时,我们需要考虑以下要点。

1.1. 服务冗余(计算高可用)

冗余策略

每一个访问可能都会有多个服务组合而成,每个机器每个服务都可能出现问题,所以第一个考虑到的就是每个服务必须不止一份可以是多份。

所谓多份一致的服务就是服务的冗余,这里说的服务泛指了机器的服务,容器的服务,还有微服务本身的服务。

在机器服务层面需要考虑,各个机器间的冗余是否有在物理空间进行隔离冗余。例如是否所有机器分别部署在不同机房,如果在同一个机房是否做到了部署在不同的机柜。

如果是 Docker 容器是否部署在分别不同的物理机上面。

采取的策略其实也还是根据服务的业务而定,所以需要对服务进行分级评分,从而采取不同的策略。

不同的策略安全程度不同,伴随着的成本也是不同,安全等级更高的服务可能还不止考虑不同机房,还需要把各个机房所处的区域考虑进行。

无状态化

服务的冗余会要求我们可以随时对服务进行扩容或者缩容,有可能我们会从 2 台机器变成 3 台机器。

想要对服务进行随时随地的扩缩容,就要求我们的服务是一个无状态化,所谓无状态化就是每个服务的服务内容和数据都是一致的。

例如,从我们的微服务架构来看,我们总共分水平划分了好几个层,正因为我们每个层都做到了无状态,所以在这个水平架构的扩展是非常的简单。

假设,我们需要对网关进行扩容,我们只需要增加服务就可以,而不需要去考虑网关是否存储了一个额外的数据。

网关不保存任何的 Session 数据,不提供会造成一致性的服务,将不一致的数据进行几种存储,借助更加擅长数据同步的中间件来完成。

这个是目前主流的方案,服务本身尽可能提供逻辑的服务,将数据的一致性保证集中式处理,这样就可以把“状态”抽取出来,让网关保持一个“无状态”。

这里仅仅是举了网关的例子,在微服务基本所有的服务,都应该按照这种思路去做。

如果服务中有状态,就应该把状态抽取出来,让更加擅长处理数据的组件来处理,而不是在微服务中去兼容有数据的状态。

1.2. 数据存储高可用

计算高可用只需要做到无状态既可简单的扩容缩容,但是对于需要存储数据的系统来说,数据本身就是有状态。

跟存储与计算相比,有一个本质的差别:将数据从一台机器搬到另一台机器,需要经过线路进行传输。

网络是不稳定的,特别是跨机房的网络,Ping 的延时可能是几十几百毫秒,虽然毫秒对于人来说几乎没有什么感觉,但是对于高可用系统来说,就是本质上的不同,这意味着整个系统在某个时间点上,数据肯定是不一致的。

按照“数据+逻辑=业务”的公式来看,数据不一致,逻辑一致,最后的业务表现也会不一致。

无论是正常情况下的传输延时,还是异常情况下的传输中断,都会导致系统的数据在某个时间点出现不一致。

而数据的不一致又会导致业务出现问题,但是如果数据不做冗余,系统的高可用无法保证。

所以,存储高可用的难点不在于怎么备份数据,而在于如何减少或者规避数据不一致对业务造成的影响。

存储高可用方案的本质是将数据复制到多个存储设备中,通过数据冗余的方式来实现高可用,其复杂度主要呈现在数据复制的延迟或中断导致数据的不一致性。

我们在设计存储架构时必须考虑到以下几个方面:

  • 数据怎么进行复制
  • 架构中每个节点的职责是什么
  • 数据复制出现延迟怎么处理
  • 当架构中节点出现错误怎么保证高可用

对于数据库我们可以采用主主复制+keepalived的方案

当写库挂了的时候,keepalived能够探测到,会自动的进行故障转移,将流量自动迁移到shadow-db-master,由于使用的是相同的virtual IP,这个切换过程对调用方是透明的。

1.3. 异步化

在每一次调用,时间越长存在超时的风险就越大,逻辑越复杂执行的步骤越多,存在失败的风险也就越大。

如果在业务允许的情况下,用户调用只给用户必须要的结果,而不是需要同步的结果可以放在另外的地方异步去操作,这就减少了超时的风险也把复杂业务进行拆分减低复杂度。

当然异步化的好处是非常多,例如削峰解耦等等,这里只是从可用的角度出发。

异步化大致有这三种的实现方式:

  • 服务端接收到请求后,创建新的线程处理业务逻辑,服务端先回应答给客户端。

  • 服务端接收到请求后,服务端先回应答给客户端,再继续处理业务逻辑。

  • 服务端接收到请求后,服务端把信息保存在消息队列或者数据库,回应答给客户端,服务端业务处理进程再从消息队列或者数据库上读取信息处理业务逻辑。

1.4. 柔性化

所谓的柔性化,就是在我们业务中允许的情况下,做不到给予用户百分百可用的,通过降级的手段给到用户尽可能多的服务,而不是非得每次都交出去要么 100 分或 0 分的答卷。

在电商订单的场景中,下单,扣库存,支付是一定要执行的步骤,如果失败则订单失败。但是加积分,发货,售后是可以柔性处理,就算出错也可以通过日志报警让人工去检查,没必要为加积分损失整个下单的可用性。

1.5. 兜底/容错

兜底可能是我们经常谈论的一种降级的方案,方案是用来实施,但是这里兜底可能更多是一种思想,更多的是一种预案,每个操作都可以犯错,我们也可以接受犯错。

但是每个犯错我们都必须有一个兜底的预案,这个兜底的预案其实就是我们的容错或者说最大程度避免更大伤害的措施,实际上也是一个不断降级的过程。

例如我们首页请求的用户个性化推荐商品的接口,发现推荐系统出错,我们不应该去扩大(直接把异常抛给用户)或保持调用接口的错误,而是应该兼容调用接口的错误,做到更加柔性化。这时候可以选择获取之前没有失败接口的缓存数据,如果没有则可以获取通用商品不用个性化推荐,如果也没有可以读取一些静态文字进行展示。

由于我们架构进行了分层,分层 App,网关,业务逻辑层,数据访问层等等,在组织结构也进行了划分,与之对应的是前端组,后端业务逻辑组,甚至有中台组等等。

既然有代码和人员架构的层级划分,那么每一层都必须有这样的思想:包容下一层的错误,为上一层提供尽可能无错的服务。

例如商品的美元售价假设要用商品人民币售价/汇率,这个时候错误发生在低层的数据层,上一层如果直接进行除,肯定就抛出 java.lang.ArithmeticException: / by zero。本着我们对任何一层调用服务都不可信的原则,应该对其进行容错处理,不能让异常扩散,更要保证我们这一层对上一次尽可能的作出最大努力确定的服务。

1.6. 负载均衡

Nginx 负载均衡故障转移

Nginx 根据给定好的负载均衡算法进行调度,当请求到 Tomcat1,Nginx 发现 Tomcat1 出现连接错误(节点失效),Nginx 会根据一定的机制将 Tomcat1 从调用的负载列表中清除。在下一次请求,Nginx 不会分配请求到有问题的 Tomcat1 上面,会将请求转移到其他的 Tomcat 之上。

1.7. 幂等设计

负载均衡的 Failover 策略,就是对失败的服务会进行重试。

一般来说,如果是读操作的服务,重复执行也不会出问题,但想象一下,如果是一个创建订单减库存的操作,第一次调用也 Tomcat1 超时,再重新调用了 Tomcat2。这个时候我们都不能确认超时调用的 Tomcat1 是否真的被调用,有可能根本就调用不成功,有可能已经调用成功但是因为某些原因返回超时而已。所以,很大程度这个接口会被调用 2 次。如果我们没有保证幂等性,就有可能一个订单导致了减少 2 次的库存。

所谓的幂等性,就是得保证在同一个业务中,一个接口被调用了多次,其导致的结果都是一样的。

1.8. 服务限流降级熔断

通过熔断机制,当一个服务挂了,被影响的服务能够及时熔断,使用 Fallback 数据保证流程在非关键服务不可用的情况下,仍然可以进行。

通过线程池和消息队列机制实现异步化,允许服务快速失败,当一个服务因为过慢而阻塞,被影响服务可以在超时后快速失败,不会影响整个调用链路。

当发现整个系统的确负载过高的时候,可以选择降级某些功能或某些调用,保证最重要的交易流程的通过,以及最重要的资源全部用于保证最核心的流程。

当既设置了熔断策略,又设置了降级策略,通过全链路的压力测试,应该能够知道整个系统的支撑能力。制定限流策略,保证系统在测试过的支撑能力范围内进行服务,超出支撑能力范围的,可拒绝服务。

2. 运维层面

2.1. 服务治理

服务模块划分

服务模块与服务模块之间有着千丝万缕的关系,但服务模块在业务中各有权重。例如订单模块可能是一家电商公司的重中之重,如果出问题将会直接影响整个公司的营收。而一个后台的查询服务模块可能也重要,但它的重要等级绝对是没有像订单这么重要。所以,在做服务治理时,必须明确各个服务模块的重要等级,这样才能更好的做好监控,分配好资源。

这个在各个公司有各个公司的一个标准,例如在电商公司,确定服务的级别可能会更加倾向对用户请求数和营收相关的作为指标。

可能真正的划分要比这个更为复杂,必须根据具体业务去定,这个可以从平时服务模块的访问量和流量去预估。往往更重要的模块也会提供更多的资源,所以不仅要对技术架构了如指掌,还要对公司各种业务形态了然于心才可以。

服务分级不仅仅在故障界定起到重要主要,而且决定了服务监控的力度,服务监控在高可用中起到了一个保障的作用。它不仅可以保留服务崩溃的现场以等待日后复盘,更重要的是它可以起到一个先知,先行判断的角色,很多时候可以预先判断危险,防范于未然。

2.2. 服务监控

服务监控是微服务治理的一个重要环节,监控系统的完善程度直接影响到我们微服务质量的好坏。

我们的微服务在线上运行的时候有没有一套完善的监控体系能去了解到它的健康情况,对整个系统的可靠性和稳定性是非常重要,可靠性和稳定性是高可用的一个前提保证。

服务的监控更多是对于风险的预判,在出现不可用之间就提前的发现问题,如果系统获取监控报警系统能自我修复则可以将错误消灭在无形,如果系统发现报警无法自我修复则可以通知人员提早进行接入。

一个比较完善的微服务监控体系需要涉及到哪些层次?如下图,大致可以划分为五个层次的监控:

基础设施监控:例如网络,交换机,路由器等低层设备,这些设备的可靠性稳定性就直接影响到上层服务应用的稳定性。所以需要对网络的流量,丢包情况,错包情况,连接数等等这些基础设施的核心指标进行监控。

系统层监控:涵盖了物理机,虚拟机,操作系统这些都是属于系统级别监控的方面,对几个核心指标监控,如 CPU 使用率,内存占用率,磁盘 IO 和网络带宽情况。

应用层监控:例如对 URL 访问的性能,访问的调用数,访问的延迟,还有对服务提供性能进行监控,服务的错误率。对 SQL 也需要进行监控,查看是否有慢 SQL,对于 Cache 来说,需要监控缓存的命中率和性能,每个服务的响应时间和 QPS 等等。

业务监控:比方说一个电商网站,需要关注它的用户登录情况,注册情况,下单情况,支付情况。这些直接影响到实际触发的业务交易情况,这个监控可以提供给运营和公司高管他们需要关注的数据,直接可能对公司战略产生影响。

端用户监控:用户通过浏览器,客户端打开连到到我们的服务,那么在用户端用户的体验是怎么样,用户端的性能是怎么样,有没有产生错误,这些信息也是需要进行监控并记录下来。

如果没有监控,有可能用户因为某些原因出错或者性能问题造成体验非常的差,而我们并没有感知。

2.3. 灰度发布

灰度发布指的是系统的变更不是一次性地推到线上的,而是按照一定比例逐步推进的。一般情况下,灰度发布是以机器维度进行的。比方说,我们先在 10% 的机器上进行变更,同时观察 Dashboard 上的系统性能指标以及错误日志。如果运行了一段时间之后系统指标比较平稳并且没有出现大量的错误日志,那么再推动全量变更。

灰度发布给了开发和运维同学绝佳的机会,让他们能在线上流量上观察变更带来的影响,是保证系统高可用的重要关卡。

要有这样的理念,任何一个新上线的系统,都是不可靠的。

2.3. 故障演练

故障演练指的是对系统进行一些破坏性的手段,观察在出现局部故障时,整体的系统表现是怎样的,从而发现系统中存在的,潜在的可用性问题。

一个复杂的高并发系统依赖了太多的组件,比方说磁盘,数据库,网卡等,这些组件随时随地都可能会发生故障,而一旦它们发生故障,会不会如蝴蝶效应一般造成整体服务不可用呢?我们并不知道,因此,故障演练尤为重要。

3. 参考资料

https://mp.weixin.qq.com/s/oky8g1Nisdr2T4kYG-DFhg

Edgar

Edgar
一个略懂Java的小菜比