15.2 声明断路器

15.2 声明断路器

在声明断路器之前,我们需要添加Spring Cloud Netflix Hystrix starter依赖到每个服务的构建文件中。在Maven pom.xml文件中,依赖如下所示:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

作为Spring Cloud套件的一部分,我们需要在构建文件中声明Spring Cloudrelease train的依赖管理。在我编写本书的时候,最新的release train版本为Finchley.SR1。所以,应该将Spring Cloud的版本设置为一个属性,如下的条目应该出现在pom.xml文件的<dependencyManagement>代码块中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<properties>
...
<spring-cloud.version>Finchley.SR1</spring-cloud.version>
</properties>
...
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
注意:在创建项目的时候,starter依赖也可以在Initializr中通过名为Hystrix的复选框来进行添加。如果使用Initializr添加Hystrix到项目的构建文件中,那么依赖管理代码块会自动创建。

Hystrix starter就绪之后,接下来的事情就是启用Hystrix。为了实现这一点,我们可以在应用的主配置类上添加@EnableHystrix。例如,为了在配料服务上启用Hystrix,我们可以按照如下的方式为IngredientServiceApplication添加注解:

1
2
3
4
5
@SpringBootApplication
@EnableHystrix
public class IngredientServiceApplication {
...
}

这样,在我们的应用中就启用Hystrix了,也就意味着声明断路器的所有准备工作都做完了。在我们的代码中还没有声明任何一个断路器,这时@HystrixCommand注解就能够发挥作用了。

任何使用@HystrixCommand注解的方法都会为其声明一个断路器切面。例如,如下的方法使用支持负载均衡的RestTemplate从配料服务中获取一个Ingredient对象的列表:

1
2
3
4
5
6
7
public Iterable<Ingredient> getAllIngredients() {
ParameterizedTypeReference<List<Ingredient>> stringList =
new ParameterizedTypeReference<List<Ingredient>>() {};
return rest.exchange(
"http://ingredient-service/ingredients", HttpMethod.GET,
HttpEntity.EMPTY, stringList).getBody();
}

对exchange()的调用可能会遇到问题。如果Eureka没有注册名为ingredient-service的服务或者由于某种原因请求失败了,那么将会抛出RestClientException(非检查型异常)。因为异常没有在try/catch代码块中进行处理,所以调用者必须要处理这个异常。如果调用者不处理,那么它将沿着调用栈往上抛出;如果它根本没有得到处理,那么这个错误会级联到所有上游微服务或客户端。

在任何应用中,未捕获的异常都是一项艰巨的挑战,在微服务中尤为如此。当遇到失败的时候,微服务应该应用维加斯规则(Vegas Rule):在微服务中发生的事情,就留在微服务中。在getAllIngredients()方法上声明断路器将会满足该规则。

按照最少的要求,我们只需要为该方法添加@HystrixCommand注解并为其提供一个后备方法即可。首先,我们添加@HystrixCommand注解到getAllIngredients()方法上:

1
2
3
4
@HystrixCommand(fallbackMethod="getDefaultIngredients")
public Iterable<Ingredient> getAllIngredients() {
...
}

断路器为getAllIngredients()提供了失败防护,所以在遇到失败时它是安全的。如果由于某种原因getAllIngredients()抛出了未捕获的异常,那么断路器将会捕获它们并将方法调用重定向到名为getDefaultIngredients()的方法上。

你可以让后备方法做任何事情,但是它们的本意是当原始的方法无法履行职责时提供后备行为。后备行为方法的唯一规则是它们要与原始方法具有相同的签名(除了方法名称之外)。

为了满足该要求,getAllIngredients()也不能接受任何参数并要返回List<Ingredient>。如下的getAllIngredients()实现满足该规则,并且返回一个默认的配料列表:

1
2
3
4
5
6
7
8
9
10
private Iterable<Ingredient> getDefaultIngredients() {
List<Ingredient> ingredients = new ArrayList<>();
ingredients.add(new Ingredient(
"FLTO", "Flour Tortilla", Ingredient.Type.WRAP));
ingredients.add(new Ingredient(
"GRBF", "Ground Beef", Ingredient.Type.PROTEIN));
ingredients.add(new Ingredient(
"CHED", "Shredded Cheddar", Ingredient.Type.CHEESE));
return ingredients;
}

现在,如果因为某种原因导致getAllIngredients()失败,那么断路器将会调用备用的getDefaultIngredients(),调用者将会接收到默认的配料列表(尽管非常有限)。

你可能会想,如果备用方法本身有断路器又会怎样呢。尽管按照我们的写法,getDefaultIngredients()几乎不可能会出问题,但是更有意思的是getDefaultIngredients()可能会有潜在的失败点。如果是这样,那么我们可以在getDefaultIngredients()上添加@HystrixCommand注解并提供另一个备用方法。实际上,需要的话,我们可以堆积任意数量的备用方法。唯一的要求就是必须要在后备方法的底部有一个不会失败的方法,该方法不需要使用断路器。

15.2.1 缓解延迟

断路器还能缓解延迟。如果某个方法需要较长的时间才能返回,断路器会将它设置为超时。默认情况下,所有带有@HystrixCommand注解的方法都会在1秒之后超时,并调用它们所声明的后备方法。这意味着,如果因为某种原因配料服务响应缓慢,那么对getAllIngredients()调用会在1秒之后超时,而且会调用getDefaultIngredients()作为替代方案。

1秒超时是一个合理的默认值,适用于大多数的场景。我们也可以通过Hystrix命令属性将其调整为更大或更小的限制值。设置Hystrix命令属性可以通过@HystrixCommand注解的commandProperties属性来实现。commandProperties属性是一个或多个@HystrixProperty注解所组成的数组,指定了要设置的属性名和值1

为了调整断路器的超时值,我们需要设置Hystrix命令属性execution.isolation.thread.timeoutInMilliseconds。例如,为了将getAllIngredients()的超时时间更加严格地设置为0.5秒,那么我们可以将超时设置为500,如下所示:

1
2
3
4
5
6
7
8
9
10
@HystrixCommand(
fallbackMethod="getDefaultIngredients",
commandProperties={
@HystrixProperty(
name="execution.isolation.thread.timeoutInMilliseconds",
value="500")
})
public Iterable<Ingredient> getAllIngredients() {
...
}

这里设置的值是毫秒数。如果我们希望放松限制,那么可以将其设置成一个更大的值。或者,如果你认为这里不应该使用超时功能,那么可以将execution.timeout.enabled属性设置为false,直接将超时功能移除:

1
2
3
4
5
6
7
8
9
10
@HystrixCommand(
fallbackMethod="getDefaultIngredients",
commandProperties={
@HystrixProperty(
name="execution.timeout.enabled",
value="false")
})
public Iterable<Ingredient> getAllIngredients() {
...
}

将execution.timeout.enabled为false的话就没有延迟防护了。在本例中,getAllIngredients()方法不管是耗用1秒、10秒还是30分钟,它都不会超时。这可能会导致级联的延迟效果,所以在禁用执行超时的时候要非常小心。

15.2.2 管理断路器的阈值

默认情况下,如果断路器保护的方法调用超过20次,而且50%以上的调用在10秒的时间内发生失败,那么断路器就会进入打开状态。所有后续的调用都将会由后备方法处理。在5秒之后,断路器进入半开状态,将会再次尝试调用原始的方法。

我们可以通过设置Hystrix命令属性调整失败和重试的阈值。如下的命令属性将会影响断路器的行为。

  • circuitBreaker.requestVolumeThreshold:在给定的时间范围内,方法应该被调用的次数。
  • circuitBreaker.errorThresholdPercentage:在给定的时间范围内,方法调用产生失败的百分比。
  • metrics.rollingStats.timeInMilliseconds:控制请求量和错误百分比的滚动时间周期。
  • circuitBreaker.sleepWindowInMilliseconds:处于打开状态的断路器要经过多长时间才会进入半开状态,进入半开状态之后,将会再次尝试失败的原始方法。

如果在metrics.rollingState.timeInMilliseconds设定的时间范围内超出了circuitBreaker.requestVolumeThreshold和circuitBreaker.errorThresholdPercentage设置的值,那么断路器将会进入打开状态。在circuitBreaker.sleepWindowInMilliseconds限定的时间范围内,它会一直处于打开状态,在此之后将进入半开状态,进入半开状态之后,将会再次尝试失败的原始方法。

例如,我们调整失败的设置:将其变更为在20秒的时间范围内调用超过30次且失败率超过25%。为了实现这一点,我们需要按照如下的方式调整Hystrix命令属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@HystrixCommand(
fallbackMethod="getDefaultIngredients",
commandProperties={
@HystrixProperty(
name="circuitBreaker.requestVolumeThreshold",
value="30"),
@HystrixProperty(
name="circuitBreaker.errorThresholdPercentage",
value="25"),
@HystrixProperty(
name="metrics.rollingStats.timeInMilliseconds",
value="20000")
})
public List<Ingredient> getAllIngredients() {
// ...
}

另外,我们还决定处于打开状态之后断路器必须保持1分钟,然后才进入半开状态,那么我们还需要设置circuitBreaker.sleepWindowInMilliseconds命令属性:

1
2
3
4
5
6
7
8
@HystrixCommand(
fallbackMethod="getDefaultIngredients",
commandProperties={
...
@HystrixProperty(
name="circuitBreaker.sleepWindowInMilliseconds",
value="60000")
})

除了优雅地处理方法调用失败和延迟之外,Hystrix还为应用中的每个断路器提供了一个指标流。接下来,我们看一下如何通过Hystrix流监控启用Hystrix功能的应用的监控状况。