微服务:服务限流、熔断与 golang 实现

一、服务雪崩

1)微服务架构是将单个应用程序被划分成各种小而连接的服务,每一个服务完成一个单一的业务功能。相对于传统的单体服务,微服务具有隔离性、技术异构性、可扩展性以及简化部署等优点。通常一个应用由多个微服务组成,微服务之间的数据交互需要通过远过程调用的方式完成。

下图是一个微服务之间互相调用的场景:

微服务A调用微服务B、C和D,微服务C又调用微服务E。假设某一时刻,微服务E变为不可用。微服务C需要等待微服务E返回结果,于是请求就会逐渐堆积在微服务C,形成阻塞。随着微服务C堆积的请求不断增加,微服务A也会随之慢慢阻塞。因为服务器所能支撑的并发数有限,所以最终会耗尽服务器资源,从而导致调用链条上更多的微服务不可用,形成雪崩效应。这种由一个服务崩溃导致整条服务链崩溃的情况,我们就称之为服务雪崩。

在微服务架构中,通常有两种情况会导致服务雪崩:

  • 突发性的访问量剧增,超出了服务处理极限
  • 调用链条上某个服务出现故障或者响应慢(延迟)

针对以上这两种情况,产生了对应的解决方案:服务限流和服务熔断。

二、 服务限流

服务限流是指在一定时间段内限制服务的请求量以保护系统,主要用于防止突发流量而导致的服务崩溃,比如秒杀、抢购、双十一等场景,也可以用于安全目的,比如应对外部暴力攻击。

常用的限流算法有以下几种:

1)计数器算法

通过维护一个内部计数器,对一段时间的服务请求进行累计,判断计数器是否达到预先设定的阈值。如果没有达到阈值,就允许请求通过,并且计数器加1;如果达到阈值,则拒绝服务,抛弃请求。进入下一个计时周期后,计数器清零,重新计数。

计数器算法是限流算法中最简单的算法,缺点是有突刺现象,不仅请求通过的速率不均匀,而且请求输出的速率也不均匀,对后续处理的并发要求比较高。比如:设定服务周期为1秒,请求的上限阈值为1000。如果前1ms内已经收到1000个请求,那么剩下的时间都只能拒绝,而且后端服务需要并发处理1000个请求。

2)漏桶算法

漏桶算法的原理可以这样理解,将服务请求想象成流入漏桶的水,漏桶中的水以恒定的速率从桶底流出,当流入漏桶的水速度过快,超过了漏桶容量时,则直接溢出。所以,漏桶算法能够控制服务请求按照固定速率均匀输出,平滑突发流量,实现流量整形,为后续处理提供一个稳定的流量。但是,漏桶算法无法控制请求按照一定的速率均匀输入。

3)牌桶算法

令牌桶算法是速率限制(Rate Limiting)和流量整形(Traffic Shaping)中最常使用的一种算法。典型情况下,令牌桶算法用来控制发送到网络上的数据的数目,并允许突发数据的发送。

首先设定一个可以存放固定数量的令牌桶,使用某种机制以恒定的速度产生令牌。每次请求调用时,都必须去桶中获取令牌,只有拿到令牌,才能放行,否则只能等待可用的令牌,或者直接拒绝。如果桶中的令牌消耗的速度小于产生的速度,令牌就会不断地增多,直至填满,再产生的令牌就会从桶中溢出。

所以,令牌桶算法既可以控制请求均匀输入的速度,又可以控制请求的均匀输出速率。

三、服务熔断

我们在各种场景下都会接触到熔断这两个字。高压电路中,如果某处电压过高,熔断器就会熔断,对电路进行保护。股票交易中,如果股票涨跌幅过大,也会采用熔断机制,暂停交易,来控制交易风险。

同样,在微服务架构中,熔断机制也是起着类似的作用。当调用链路中的某个微服务长时间不可用或者有延迟,响应过慢,系统就会熔断对该节点微服务的调用,快速返回错误信息。当监控到该微服务正常工作后,再次恢复该调用链路。

四、Hystrix

服务熔断和服务限流作为微服务架构中作为服务治理的重要手段,在很多开源框架或者产品中都包含了此类功能。比如阿里dubbo,Netflix Hystrix,go-micro,go-kit等。

Hystrix是Netflix公司的开源项目,它是一个延迟和故障容错库,旨在隔离对远程系统、服务和第三方库的访问点,防止级联故障,并在无法避免发生故障的复杂分布式系统中实现了弹性。

Hystrix项目使用了java语言开发,代码托管地址为:https://github.com/Netflix/Hystrix。另外,有人使用go语言将该项目进行了移植,代码托管地址为:https://github.com/afex/hystrix-go/hystrix

Hystrix可以做到以下事情:

  • 通过控制延迟和故障来保障第三方服务调用的可靠性
  • 在复杂的分布式系统中防止级联故障,防止雪崩
  • 快速失败、快速恢复
  • 回退并优雅降级
  • 提供近实时监控、报警和操作控制

Hystrix 能使你的系统在出现依赖服务失效的时候,通过隔离系统所依赖的服务,防止服务级联失败,同时提供失败回退机制,更优雅地应对失效,并使你的系统能更快地从异常中恢复。

使用Hystrix 非常简单:

package main

import (
	"github.com/afex/hystrix-go/hystrix"
	"log"
)

func main() {
	// 每一类微服务、远程系统、第三方库的调用都被包装为一个命令,
	// 调用之前需要先行设置相关的配置信息
	hystrix.ConfigureCommand(
		"ServiceA", // 命令名称
		hystrix.CommandConfig{
			Timeout:                1000, // 超时时间设置(毫秒)
			MaxConcurrentRequests:  1,    // 最大并发请求数
			ErrorPercentThreshold:  50,   // 错误率阈值,超过阈值将熔断服务
			SleepWindow:            5000, // 服务熔断后,过多长时间,熔断器再次检测是否开启(毫秒)
			RequestVolumeThreshold: 20,   // 服务熔断前,所需的最小请求量,请求阈值  熔断器是否打开首先要满足这个条件;这里的设置表示至少有5个请求才进行ErrorPercentThreshold错误百分比计算
		})

	// 使用hystrix调用远程服务,并根据命令名称启用ConfigureCommand中设置的参数
	_ = hystrix.Do(
		"ServiceA", // 命令名称
		// 运行函数
		func() error {
			// 调用远程服务
			log.Println("invoke a remote service")
			return nil
		},
		// 失败回退函数
		func(err error) error {
			log.Println("fall back")
			return err
		},
	)
}

本文全部源代码位于:https://github.com/wangshizebin/micro-service

下一章:微服务:微服务拆分目的、原则和实践

微服务在最近几年大行其道,很多公司的研发人员都在考虑微服务架构。我们应该按照什么原则将现有的业务进行拆分?是否拆分得越细就越好?本文内容包括:微服务拆分目的、微服务拆分的时机、微服务拆分的指导原则、微服务拆分的粒度、微服务拆分的策略、微服务合并、微服务拆分的风险。