对于每个服务网格框架来说,处理服务间通信中的通信故障的能力是绝对必要的。它包括超时和 HTTP 错误代码的处理。在本文中,我将展示如何使用 Istio 配置重试和熔断机制。与之前 Kubernetes 上使用 Istio Service Mesh 的文章相同,我们将分析在 Kubernetes 上部署的两个简单 Spring Boot 应用程序之间的通信。但是,我们将讨论更高级的主题,而不是非常基本的示例。
示例
为了演示 Istio 和 Spring Boot 的用法,我在 GitHub 上创建了带有两个示例应用程序的存储库:callme-service 和 caller-service。该存储库的地址为https://github.com/piomin/sample-istio-services.git
。前言中已经提到与 Istio 有关服务网格的第一篇文章使用了相同的存储库。
架构
我们的示例系统的体系结构与上一篇文章非常相似。但是,存在一些差异。我们不是在使用 Istio 组件注入故障或延迟,而是直接在源代码内部的应用程序上注入错误或延迟。为什么?现在,我们将能够直接按照为 callme-service
创建的规则进行处理,而不必像以前那样在客户端进行处理。另外,我们正在运行两个 callme-service
应用程序版本 v2 实例,以测试熔断器对相同服务(或相同 Deployment)实例的作用如何。下图说明了当前描述的体系结构。
Spring Boot 应用
我们从示例应用程序的实现开始。应用程序 callme-service
公开了两个端点,这些端点返回有关版本和实例 ID 的信息。端点 GET /ping-with-random-error
将HTTP 504
错误代码设置为对约 50%请求的响应。端点 GET /ping-with-random-delay
返回的响应具有 0s 和 3s 之间的随机延迟。这是 callme-service
端 @RestController 的实现。
@RestController
@RequestMapping("/callme")
public class CallmeController {private static final Logger LOGGER = LoggerFactory.getLogger(CallmeController.class);
private static final String INSTANCE_ID = UUID.randomUUID().toString();
private Random random = new Random();
@Autowired
BuildProperties buildProperties;
@Value("${VERSION}")
private String version;
@GetMapping("/ping-with-random-error")
public ResponseEntity<String> pingWithRandomError() {int r = random.nextInt(100);
if (r % 2 == 0) {LOGGER.info("Ping with random error: name={}, version={}, random={}, httpCode={}",
buildProperties.getName(), version, r, HttpStatus.GATEWAY_TIMEOUT);
return new ResponseEntity<>("Surprise" + INSTANCE_ID + " " + version, HttpStatus.GATEWAY_TIMEOUT);
} else {LOGGER.info("Ping with random error: name={}, version={}, random={}, httpCode={}",
buildProperties.getName(), version, r, HttpStatus.OK);
return new ResponseEntity<>("I'm callme-service"+ INSTANCE_ID +" " + version, HttpStatus.OK);
}
}
@GetMapping("/ping-with-random-delay")
public String pingWithRandomDelay() throws InterruptedException {int r = new Random().nextInt(3000);
LOGGER.info("Ping with random delay: name={}, version={}, delay={}", buildProperties.getName(), version, r);
Thread.sleep(r);
return "I'm callme-service " + version;
}
}
应用程序 caller-service
公开了两个 GET 端点。它使用 RestTemplate
来调用 callme-service
公开的相应 GET 端点。它还返回 caller-service
的版本,但是只有一个标有 version = v1 的应用程序的部署。
@RestController
@RequestMapping("/caller")
public class CallerController {private static final Logger LOGGER = LoggerFactory.getLogger(CallerController.class);
@Autowired
BuildProperties buildProperties;
@Autowired
RestTemplate restTemplate;
@Value("${VERSION}")
private String version;
@GetMapping("/ping-with-random-error")
public ResponseEntity<String> pingWithRandomError() {LOGGER.info("Ping with random error: name={}, version={}", buildProperties.getName(), version);
ResponseEntity<String> responseEntity =
restTemplate.getForEntity("http://callme-service:8080/callme/ping-with-random-error", String.class);
LOGGER.info("Calling: responseCode={}, response={}", responseEntity.getStatusCode(), responseEntity.getBody());
return new ResponseEntity<>("I'm caller-service "+ version +". Calling... " + responseEntity.getBody(), responseEntity.getStatusCode());
}
@GetMapping("/ping-with-random-delay")
public String pingWithRandomDelay() {LOGGER.info("Ping with random delay: name={}, version={}", buildProperties.getName(), version);
String response = restTemplate.getForObject("http://callme-service:8080/callme/ping-with-random-delay", String.class);
LOGGER.info("Calling: response={}", response);
return "I'm caller-service "+ version +". Calling... " + response;
}
}
Istio 中处理重试
Istio DestinationRule
的定义与我的文章使用 Istio 和 Spring Boot 在 Kubernetes 上的服务网格中的定义相同。为标记为 version = v1 和 version = v2 的实例创建了两个子集。重试和超时可以在 VirtualService
上配置。我们可以设置重试次数和重试条件(枚举字符串列表)。以下配置还为整个请求设置了 3s 超时。这两个设置都可以在 HTTPRoute
对象中使用。我们还需要为每次尝试设置超时。在这种情况下,我设置为 1。在实践中如何运作?我们将通过简单的示例对其进行分析。
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: callme-service-destination
spec:
host: callme-service
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: callme-service-route
spec:
hosts:
- callme-service
http:
- route:
- destination:
host: callme-service
subset: v2
weight: 80
- destination:
host: callme-service
subset: v1
weight: 20
retries:
attempts: 3
perTryTimeout: 1s
retryOn: 5xx
timeout: 3s
在部署示例应用程序之前,我们应该增加日志记录级别。我们可以轻松启用 Istio 访问日志记录。Envoy 代理打印访问日志,并将所有传入请求和传出响应发送到它们的标准输出。日志记录条目的分析将特别用于检测重试尝试。
$ istioctl manifest apply --set profile=default --set meshConfig.accessLogFile="/dev/stdout"
现在,让我们向 HTTP 端点 GET /caller/ping-with-random-delay
发送测试请求。它调用随机延迟的 callme-service
端点GET /callme/ping-with-random-delay
。这是该操作的请求和响应。
看来情况非常清楚。但是,让我们检查一下背后发生了什么。我已经强调了重试的顺序。如您所见,Istio 执行了两次重试,因为前两次尝试的时间比设置为 1s 的 perTryTimoeut
更长。两次尝试均被 Istio 超时,可以在其访问日志中进行验证。第三次尝试成功了,因为它花费了大约 400 毫秒。
重试超时不是 Istio 中唯一可用的重试选项。实际上,我们可以重试所有 5XX 甚至 4XX 代码。用于测试错误代码的 VirtualService 更加简单,因为我们没有配置任何超时。
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: callme-service-route
spec:
hosts:
- callme-service
http:
- route:
- destination:
host: callme-service
subset: v2
weight: 80
- destination:
host: callme-service
subset: v1
weight: 20
retries:
attempts: 3
retryOn: gateway-error,connect-failure,refused-stream
我们将使用 GET /caller/ping-with-random-error
调用 HTTP 端点,即调用 callme-service
公开的端点GET /callme/ping-with-random-error
。它为大约 50%的传入请求返回 HTTP 504。这是带有 200 OK HTTP 代码的请求和成功的响应。
以下是日志,这些日志说明了 callme-service
发生的情况。由于两次第一次尝试均导致 HTTP 错误代码,因此请求已重试了 2 次。
Istio 中处理熔断
在 DestinationRule
对象上配置了断路器。我们为此使用了 TrafficPolicy
。首先,我们不会设置用于先前示例的任何重试,因此我们需要将其从VirtualService
定义中删除。我们还应该在 TrafficPolicy
中的 connectionPool
上禁用任何重试。现在最重要。为了在 Istio 中配置断路器,我们使用 OutlierDetection
对象。Istion 断路器的实现基于下游服务返回的连续错误。后续错误的数量可以使用属性 continuous5xxErrors
或continuousGatewayErrors
进行配置。它们之间的唯一区别在于它们能够处理的 HTTP 错误。尽管 ContinuousGatewayErrors
仅适用于 502、503 和 504,但 continuous5xxErrors
用于 5XX 代码。在以下 c allme-service-destination
的配置中,我在 3 上使用了 set Continuous5xxErrors
的设置。这意味着在发生 3 个错误行后,将从负载平衡中删除应用程序的一个实例(pod)1 分钟(baseEjectionTime = 1m)。因为我们在版本 v2 中运行两个 callme-service
Pod,所以我们还需要将maxEjectionPercent
的默认值覆盖为 100%。该属性的默认值为 10%,它表示负载平衡池中可以弹出的最大主机百分比。
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: callme-service-destination
spec:
host: callme-service
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
trafficPolicy:
connectionPool:
http:
http1MaxPendingRequests: 1
maxRequestsPerConnection: 1
maxRetries: 0
outlierDetection:
consecutive5xxErrors: 3
interval: 30s
baseEjectionTime: 1m
maxEjectionPercent: 100
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: callme-service-route
spec:
hosts:
- callme-service
http:
- route:
- destination:
host: callme-service
subset: v2
weight: 80
- destination:
host: callme-service
subset: v1
weight: 20
部署两个应用程序的最快方法是使用 Jib 和 Skaffold。首先,您进入目录 callme-service
并使用可选的 --port-forward
参数执行 skaffold dev
命令。
$ cd callme-service
$ skaffold dev --port-forward
对 caller-service
做相同的操作:
$ cd caller-service
$ skaffold dev --port-forward
在发送一些测试请求之前,让我们运行callme-service
v2 版本的第二个实例,因为 Deployment 将参数副本设置为 1。为此,我们需要运行以下命令。
$ kubectl scale --replicas=2 deployment/callme-service-v2
现在,让我们验证 Kubernetes 上的部署状态。有 3 个部署。
之后,我们准备发送一些测试请求。我们正在调用由 caller-service
暴露的端点 GET /caller/ping-with-random-error
,即正在调用由callme-service
暴露的端点 GET /callme/ping-with-random-error
。callme-service
公开的端点针对 50%的请求返回 HTTP 504。我已经在 8080 上为 callme-service
设置了端口转发,因此用于调用应用程序的命令是:
curl http://localhost:8080/caller/ping-with-random-error
现在,让我们分析 caller-service
的响应。我已经突出显示了来自版本为 v2 的 callme-service
实例的 HTTP 504 错误代码响应,并生成了 ID 98c068bb-8d02-4d2a-9999-23951bbed6ad。在该实例的行中出现 3 次错误响应后,立即将其从负载平衡池中删除,这导致将所有其他请求发送到 ID 为 00653617-58e1-4d59-9e36-3f98f9d403b8 的 callme-service v2
的第二个实例。当然,仍然有一个 callme-service v1
实例,该实例正在接收 caller-service
发送的请求总数的 20%。
好的,让我们检查一下单个实例 callme-service v1
返回 3 个错误会发生什么情况。我还用以下可见的日志突出显示了这些错误响应。因为池中只有一个 callme-service v1
实例,所以没有机会将传入的流量重定向到其他实例。这就是为什么 Istio 为发送到 callme-service v1
的下一个请求返回 HTTP 503 的原因。自熔断后的 1 分钟内,将返回相同的响应。
PS:本文属于翻译,原文