混沌工程实践:使用 SMI 和 Linkerd 进行故障注入

应用程序故障注入是混沌工程的一种形式,在微服务应用程序中人为地增加某些服务的错误率,以查看它对整个系统的影响。传统上,需要在服务代码中添加某种类型的故障注入库,才能对应用程序进行故障注入,值得庆幸的是,服务网格提供了一种无需修改或重构服务的应用程序故障注入方法。

结构良好的微服务应用程序有一个特点,它可以优雅地容忍单点服务故障。当这些故障以服务崩溃的形式出现时,Kubernetes 在修复服务崩溃故障方面做的非常好,它可以通过创建新的 Pods 替换崩溃的 Pods 的方式来修复这些故障。然而,故障也可能更加微妙,如导致服务返回的错误率升高。对于这种类型的故障,Kubernetes 就不能自动修复了,仍然会导致部分功能丧失。

使用流量分割 SMI API 注入错误

使用 服务网格接口 (Service Mesh Interface,SMI)的 流量分割 API (Traffic Split API)可以轻松实现应用程序故障注入。这是一种与实现无关且跨服务网格的故障注入方式。

为了实现这种形式的故障注入,首先,我们部署一个只返回错误的新服务。它既可以是一个简单的服务,比如一个配置成返回 HTTP 500 的 NGINX 服务,也可以是一个更复杂的服务,返回我们为了测试某些条件而专门设计的错误。其次,我们创建一个流量分割资源,用该资源来指导服务网格将目标服务流量按照百分比发送到错误服务上。例如,通过将 10%的服务流量发送到错误服务上,来人为地实现向该服务注入 10%的错误率。

我们一起看一个使用 Linkerd 作为服务网格实现的示例。

示例

首先,我们安装 Linkerd CLI,并将它部署到 Kubernetes 集群上:

复制代码

> curl https://run.linkerd.io/install | sh
> export PATH=$PATH:$HOME/.linkerd2/bin
> linkerd install | kubectl apply -f -
> linkerdcheck

然后,我们安装一个“booksapp”示例应用程序:

复制代码

> linkerd inject https://run.linkerd.io/booksapp.yml |kubectlapply-f -

该应用程序的某个服务已经配置了错误率,但这个示例是为了说明,我们不需要任何支持也可以在应用程序中注入错误,所以,需要删除应用程序中配置的错误率:

复制代码

> kubectl edit deploy/authors
# Find and remove these lines:
# - name: FAILURE_RATE
# value: "0.5"

我们看到应用程序可以正常运行了:

复制代码

> linkerd stat deploy
NAME MESHED SUCCESS RPS LATENCY_P50 LATENCY_P95 LATENCY_P99 TCP_CONN
authors1/1100.00%6.6rps3ms58ms92ms6
books1/1100.00%8.0rps4ms81ms119ms6
traffic1/1- - - - - -
webapp3/3100.00%7.7rps24ms91ms117ms9

现在,我们创建一个错误服务。在此,我使用配置成返回 HTTP 500 状态代码的 NGINX,创建一个名为 error-injector.yaml 的文件:

复制代码

apiVersion:apps/v1
kind:Deployment
metadata:
name:error-injector
labels:
app:error-injector
spec:
selector:
matchLabels:
app:error-injector
replicas:1
template:
metadata:
labels:
app:error-injector
spec:
containers:
- name:nginx
image:nginx:alpine
ports:
- containerPort:80
name:nginx
protocol:TCP
volumeMounts:
- name:nginx-config
mountPath:/etc/nginx/nginx.conf
subPath:nginx.conf
volumes:
- name:nginx-config
configMap:
name:error-injector-config
---
apiVersion:v1
kind:Service
metadata:
labels:
app:error-injector
name:error-injector
spec:
clusterIP:None
ports:
- name:service
port:7002
protocol:TCP
targetPort:nginx
selector:
app:error-injector
type:ClusterIP
---
apiVersion:v1
data:
nginx.conf:|2

events{
worker_connections1024;
}

http{
server{
location/{
return500;
}
}
}
kind:ConfigMap
metadata:
name:error-injector-config

部署 error-injector.yaml 文件 :

复制代码

> kubectlapply-ferror-injector.yaml

现在,我们创建一个流量分割资源,它会将 10%的流量从“books”服务重定向到“error-injector”错误服务。该资源文件命名为 error-split.yaml:

复制代码

apiVersion: split.smi-spec.io/v1alpha1
kind: TrafficSplit
metadata:
name: error-split
spec:
service: books
backends:
-service: books
weight:900m
-service: error-injector
weight:100m

部署 error-split.yaml 文件:

复制代码

> kubectlapply-ferror-split.yaml

现在,我们可以看到从 webapp 到 books 的调用错误率为 10%:

复制代码

> linkerd routes deploy/webapp --to service/books
ROUTESERVICESUCCESS RPS LATENCY_P50 LATENCY_P95 LATENCY_P99
[DEFAULT] books 90.66% 6.6rps 5ms 80ms 96ms

我们还可以看到应用程序是如何优雅地处理这些故障:

复制代码

> kubectl port-forward deploy/webapp7000&
> open http://localhost:7000

如果多刷几次页面,我们有时会看到内部服务错误页面。

关于应用程序是如何处理服务错误的,我们已经学到了一些有价值的东西,现在,我们通过简单的删除流量分割资源,来恢复我们的应用程序:

复制代码

> kubectldeletetrafficsplit/error-split

结论

在本文中,通过使用 SMI API(由 Linkerd 提供支持)动态地将一部分流量重定向到简单的“始终失败”的目标服务,我们演示了一种在服务级别快速简便地进行故障注入的方式。这种方式的优势在于,我们仅通过 SMI API 就可实现故障注入,而无需更改任何应用程序代码。

当然,故障注入是一个很泛的话题,还有很多更复杂的故障注入方式,包括路由失败、让匹配特定条件的请求失败、或者在整个应用程序拓扑中传播单个“毒丸”请求。这些类型的故障注入需要比本文所涵盖内容更多的支持机制。

Linkerd 是一个由 云原生计算基金会 (Cloud Native Computing Foundation,CNCF)托管的社区项目。Linkerd 托管在 GitHub 上,在 SlackTwittermailing lists 上社区也很活跃,感兴趣的开发者可以下载试用。