vlambda博客
学习文章列表

设计高可用的微服务架构


微服务特点

模块化

微服务是一个单一的、可独立部署的软件组件,它实现了一些有用的功能。微服务具有API,为其客户端提供对功能的访问。有两种类型的操作:命令和查询。API由命令、查询和事件组成。在一个典型的订单业务场景中,命令如createOrder()执行操作并更新数据。查询,如findOrderById()检索数据。微服务还发布由其客户端使用的事件,例如OrderCreated。微服务的API封装了其内部实现。与单体架构不同,开发人员无法绕过服务的API直接访问服务内部的方法或数据。因此,微服务架构强制实现了应用程序的模块化。

松耦合

微服务架构的最核心特性是服务之间的松耦合性。服务之间的交互采用API完成,这样做就封装了服务的实现细节。这允许服务在不影响客户端的情况下,对实现方式做出修改。松耦合服务是改善开发效率、提升可维护性和可测试性的关键。小的、松耦合的服务更容易被理解、修改和测试。

服务拆分

要满足模块化和松耦合,针对一个单体式服务我们在设计微服务的时候,第一件事就是做服务拆分。一般来说,我们可以从两个方面入手进行服务拆分:

水平拆分

在单体应用中,我们也会对软件架构进行分层,形成接入层、业务逻辑层、数据访问层,但是这些分层并没有完全隔离,一般都是在同一个应用中进行部署和运行。设计微服务架构的时候,我们需要进一步的进行抽象和隔离。

  • 架构拆分

接入层完全独立并且集成负载、流控等功能形成网关层;业务逻辑层遵循无状态,能够支撑多点部署。例如下图:

16088006803979fvqgi
  • 存储拆分

存储层面的拆分就更好理解了,当数据存储达到一定的量级以后,为了保证查询和执行效率,我们就会进行分库分表存储。

垂直拆分

垂直方向拆分服务, 是按照业务维度进行拆分。

  • 架构拆分

在功能逻辑层面,我们可以拆分为会员服务、商品服务、交易服务、物流服务等等。

  • 存储拆分

在数据存储层面,相应的我们可以按照服务拆分的结果,进行数据库存储分离,形成会员数据库、商品数据库、交易数据库等。

1608797601371vHYTHq

服务治理

服务拆分以后,我们会发现,相对于单体应用,服务与服务间的调用关系更加复杂,调用链路也更长。如何有效的管理服务,是我们要解决的问题。

服务限流

服务限流是微服务集群自我保护的一种常用机制,我们对线上调用比较频繁及资源占用较大的服务都加上了相应的限流举措,并构建了单机限流及集群限流两套限流措施。

首先来看一下单机限流,它有多种限流算法可供选择,最主要的是两种,漏桶算法及令牌桶算法。它们之间有什么区别呢?打个比方,比如有家酒吧已经客满了,保安开始限制客流,一种举措是酒吧中出来一个客人,才放进去一个客人,这样就可以保证酒吧中的客人总数是固定的,人人都有座位,这就是漏桶算法—必须有出去的,才能有进来的;另外一种举措是不管有没有客人出去,保安固定每隔 5 分钟就放一个客人进去,这和春运火车站的波段式限流非常类似,可以保证客流是比较均匀的,但是这种策略也有一定的风险,如果离开的客人不够及时,酒吧中的客人总数可能会升高,导致一部分客人没有座位,这就是令牌桶算法。因此,如果要对线上并发总数进行严格限定的话,漏桶算法可能会更合适一些,这是单机限流机制。

接下来再看看集群限流,集群限流的情况要更复杂一些,首先在各个微服务节点上要有一个计数器,对单位时间片内的调用进行计数,计数值会被定期的汇总到日志中心,由统计分析器进行统一汇总,算出这个时间片的总调用量,集群限流分析器会拿到这个总调用量,并和预先定义的限流阈值进行比对,计算出一个限流比例,这个限流比例会通过服务注册中心下发到各个服务节点上,服务节点基于限流比例会各自算出当前节点对应的最终限流阈值,最后利用单机限流进行流控。

集群容错

集群容错是微服务集群高可用的保障,它有很多策略可供选择,包括:

  • 快速失败(Failfast)

  • 失败转移(Failover)

  • 失败重试(Failback)

  • 聚合调用(Forking)

  • 广播调用(Broadcast)

不管用哪种集群容错策略,一定要注意重试的次数,尤其是线上负载已经很高的时候,这时候如果重试次数太多,一方面,会推高服务被调用方的负载及并发,另外一方面,会导致服务调用方的调用延时增长,双重因素叠加之下,最终极可能导致“服务雪崩”,导致集群被“击穿”。

服务降级

服务降级和服务限流类似,也是微服务集群自我保护的机制,一般在线上动用服务降级手段的时候,都是线上比较危急的时候,生死存亡了,这时候留给你思考和反应的时间已经不多,所以在使用服务降级之前一定要做好预案,你要提前梳理出核心业务链路和非核心业务链路,然后通过降级开关一键把部分或所有非核心链路降级,这样才能救命。

服务降级也有很多手段可以使用,包括:

  • 容错降级

  • 静态返回值降级

  • Mock降级

  • 备用服务降级

我们常说的熔断,本质上也是容错降级策略的一种,只不过它比一般容错降级提供了更为丰富的容错托底策略,支持半开降级及全开降级模式。

链路跟踪

线上的故障定位应该是我们每天做的最多的事情。分布式环境下的故障定位,最有效的工具就是动态调用链路跟踪,这应该是没有疑义的,不管你是使用开源的 Zipkin,SkyWalking、PinPoint、CAT,还是使用商用的听云、AppDynamic 或 NewRelic 等等。

调用链本质上也是基于日志,只不过它比常规的日志更重视日志之间的关系。在一个请求刚发起的时候,调用链会赋予它一个跟踪号(traceID),这个跟踪号会随着请求穿越不同的网络节点,并随着日志落盘,日志被收集后,可以根据 traceID 来对日志做聚合,找到所有的关联日志,并按顺序排序,就能构建出这个请求跨网络的调用链,它能详细描述请求的整个生命周期的状况。

容量规划

容量规划有两种形式,一种是容量预估,另一种是性能压测。

系统或者服务上线之前,首先要进行容量的预估,一般做法是基于经验,同时结合对业务的前景预期,先估算出一个总的调用量,比如 1 亿的 PV,可能会有 10%的流量落在购物车服务上,购物车服务就是 1000 万的 PV,一次购物车访问会产生 2 次数据库调用,那么它的关联数据库就会有 2000 万的一个调用量,这样,基于图上从左至右,层层分解之后,就可以获取到每个服务节点上摊到的访问量,再结合运维部门的单机容量指标,就可以估算出整个集群需要多少的软硬件资源。

系统或者服务一旦上线之后,它的性能就开始处于不断“劣化”的状态,上线前预估的指标会越来越不准,这时候就需要通过线上性能压测来对实时容量进行监控。做线上性能压测也要遵循一定的规律,不是说一上来就做全链路压测。同样是基于上图中的调用关系,线上性能压测首先需要在调用链的末梢,也就是对数据库或者缓存先进行压测,以保证它们不是瓶颈,再对调用数据库或者缓存的上一级节点进行压测,再一级一级往上压测,最终覆盖整个链路,实现全链路压测。

可见,全链路压测的前提是单点压测,需要先把单点压测能力做好,才能做全链路压测。

在压测的时候,由于流量是模拟的,数据也是“伪造”的,所以一定要做好隔离,各种各样的隔离,尤其是数据的隔离,我个人不建议将“染色”的压测数据和真实的线上业务数据共表存储,最好将“染色”数据单独表进行存储,并通过分表策略进行区隔。

以上就是性能规划,包含了容量预估与性能压测两大能力。