在微服务架构中,应用程序是由多个相互连接的服务组成的,这些服务协同工作以实现所需的业务功能。所以,一个典型的企业级微服务架构如下所示:
最初,我们可能认为使用微服务架构实现一个应用程序是很容易的事情。但是,要恰当地完成这一点并不容易,因为我们会面临一些新的挑战,而这些挑战是单体架构所未曾遇到的。举例来讲,这样的挑战包括容错、服务发现、扩展性、日志和跟踪等。
为了应对这些挑战,每个微服务都需要实现在 Red Hat 被称为“微服务特性(microservicility)”的内容。这个术语指的是除了业务逻辑之外,服务必须要实现的一个横切性关注点的列表。
这些关注点总结起来如下图所示:
业务逻辑可以使用任何语言(Java、Go 或 JavaScript)或任何框架(Spring Boot、Quarkus)来实现,但是围绕着业务逻辑,我们应该实现如下的关注点:
API:服务可以通过一组预先定义的 API 操作进行访问。例如,在采用 RESTful Web API 的情况下,会使用 HTTP 作为协议。此外,API 还可以使用像Swagger这样的工具实现文档化。
发现(Discovery):服务需要发现其他的服务。
调用(Invocation):在服务发现之后,需要使用一组参数来调用它,并且可能会返回一个响应。
弹性(Elasticity):微服务架构很重要的特性之一就是每个服务都是有弹性的,这意味着它可以根据一些参数(比如系统的重要程度或当前的工作负载)独立地进行扩展和伸缩。
回弹性(Resiliency):在微服务架构中,我们在开发时应该要考虑到故障,特别是与其他服务进行通信的时候。在单体架构中,应用会作为一个整体进行启动和关闭。但是,当我们把应用拆分成微服务架构之后,应用就变成由多个服务组成的,所有的服务会通过网络互相连接,这意味着应用的某些部分可能在正常运行,而其他部分可能已经出现了故障。在这种情况下,很重要的一点就是遏制故障,避免错误通过其他的服务进行传播。回弹性(或称为应用回弹性)是指一个应用/服务能够对面临的问题作出反应的能力,在出现问题的时候,依然能够提供尽可能最好的结果。
管道(Pipeline):服务应该能够独立部署,不需要任何形式的部署编排。基于这一点,每个服务应该有自己的部署管道。
认证(Authentication):在微服务架构中,涉及到安全性时,很重要的一个方面就是如何认证/授权内部服务之间的调用。Web token(以及通用的 token)是在内部服务之间声明安全性的首选方式。
日志(Logging):在单体应用中,日志是很简单的事情,因为应用的所有组件都在同一个节点中运行。现在,组件以服务的形式分布在多个节点上,因此,为了全面了解日志跟踪的情况,我们需要一个统一的日志系统/数据收集器。
监控(Monitoring):要保证基于微服务的应用正确运行,很重要的一个方面就是衡量系统的运行情况、理解应用的整体健康状况并在出现问题的时候发出告警。监控是控制应用程序的重要方面。
跟踪(Tracing):跟踪是用来可视化一个程序的流程和数据进展的。当我们需要检查用户在整个应用中的操作时,它对开发人员或运维人员尤其有用。
Kubernetes 正在成为部署微服务的事实标准工具。它是一个开源的系统,用来自动化、编排、扩展和管理容器。
但是在我们提到的十个微服务特性中,通过使用 Kubernetes 只能覆盖其中的三个。
**发现(Discovery)**是通过_Kubernetes Service_理念实现的。它提供了一种将_Kubernetes Pod_(作为一个整体)进行分组的方式,使其具有稳定的虚拟 IP 和 DNS 名。要发现一个服务只需要发送请求的时候使用 Kubernetes 的服务名作为主机名即可。
使用 Kubernetes **调用(Invocation)**服务是非常容易的,因为平台本身提供了所需的网络来调用任意的服务。
弹性(Elasticity)(或者说扩展性)是 Kubernetes 从一开始就考虑到的问题,例如,如果运行kubectl scale deployment myservice --replicas=5
命令的话,myservice deployment 就会扩展至五个副本或实例。Kubernetes 平台会负责寻找合适的节点、部署服务并维持所需数量的副本一直处于运行状态。
但是,剩余的微服务特性该怎么处理呢?Kubernetes 只涵盖了其中的三个,那么我们该如何实现剩余的哪些呢?
在本系列的第一篇文章中,我介绍了一种实现它们的方式,那就是使用 Java 将它们嵌入到服务内部。
在代码内部实现横切性关注点的服务如下图所示:
正如在前面的文章中所阐述的那样,这种方式能够正常运行并且具有很多的优势,但是它也有一些缺点。我们介绍主要的几个问题:
归根到底,我们可能会想,为什么需要实现这些微服务特性呢?
在微服务架构中,应用程序是由相互连接的多个服务组成的,所有的服务相互协作以生成我们所需的业务功能。这些服务都是使用网络互相连接在一起的,所以实际上我们实现了一个分布式计算的模型。由于它是分布式的,可观察性(监控、跟踪、日志)就变得有些复杂了,因为所有的数据分散在多个服务中。因为网络是不可靠的,或者网络延迟不可能为零,所以服务需要在面临故障的时候具备回弹性。
因此,我们可以假定之所以需要微服务特性,是因为在基础设施层(我们需要使用网络的分布式服务通信,而不是单体)所做的决定。那么我们为什么要在应用层面实现这些微服务特性,而不是在基础设施层面实现呢?问题就在这里,这个问题有一个很简单的答案,那就是服务网格。
服务网格是一个专用的基础设施层,目的在于使得服务与服务之间的通信变得安全、快速和可靠。
服务网格通常以轻量级网络代理的形式实现并且会与服务代码部署在一起,它会拦截服务所有进站/出站的网络流量。
Istio是一个适用于 Kubernetes 的开源服务网格实现。Istio 采用的策略是集成一个网络流量代理到 Kubernetes Pod 中,而这个过程是借助sidecar容器实现的。sidecar 容器与服务容器运行在同一个 Pod 中。因为它们运行在系统的 Pod 之中,所以两个容器会共享 IP、生命周期、资源、网络和存储。
Istio 使用Envoy Proxy作为 sidecar 容器中的网络代理,并且会配置 Pod 通过 Envoy 代理(sidecar 容器)发送所有的入站/出站流量。
在使用 Istio 的时候,服务之间的通信并不是直接进行的,而是通过 sidecar 容器(即 Envoy)进行的,当服务 A 请求服务 B 的时候,请求会通过服务 A 的 DNS 发送到它的代理容器上。随后,服务 A 的代理容器会发送请求至服务 B 的代理容器,代理容器最终会调用真正的服务 B。响应过程则会遵循完全相反的路径。
Envoy 代理的 sidecar 容器实现了如下的特性:
通过下图我们可以看出,sidecar 容器实现的特性能够非常好地匹配五个微服务特性:服务发现、回弹性、认证、监控和跟踪。
在容器中实现微服务特性的逻辑有如下几个好处:
但是,Istio 内部是如何运行的,我们为什么需要 Istio,而不是直接使用 Envoy 代理呢?
Envoy 代理是一个轻量级的网络代理,它可以单独使用,但是如果有十个服务要部署的话,我们就需要配置十个 Envoy 代理。这个过程会变得有一些复杂和繁琐。Istio 简化了这一过程。
从架构上来讲,Istio 服务网格是由数据平面(data plane)和控制平面(control plane)组成的。
数据平面是由以 sidecar 形式部署的 Envoy 代理组成的。这个代理会拦截所有网络之间的通信。它还会收集和报告所有网格流量的遥测数据。
控制平面负责管理和配置 Envoy 代理。
下图描述了这两个组件:
我们需要一个安装 Istio 的 Kubernetes 集群。就本文来讲,我们会使用Minikube,但是任意其他的 Kubernetes 集群都是可以的。
运行如下的命令来启动集群:
minikube start -p istio --kubernetes-version='v1.19.0' --vm-driver='virtualbox' --memory=4096
[istio] minikube v1.17.1 on Darwin 11.3
Kubernetes 1.20.2 is now available. If you would like to upgrade, specify: --kubernetes-version=v1.20.2
minikube 1.19.0 is available! Download it: https://github.com/kubernetes/minikube/releases/tag/v1.19.0
To disable this notice, run: 'minikube config set WantUpdateNotification false'
✨ 基于已有的 profile 并使用 virtualbox 驱动
❗ 对于既有的 minikube 集群,我们无法改变它的内存大小。如果需要的话,请先将该集群删除掉。
Starting control plane node istio in cluster istio
Restarting existing virtualbox VM for "istio" ...
Preparing Kubernetes v1.19.0 on Docker 19.03.12 ...
Verifying Kubernetes components...
Enabled addons: storage-provisioner, default-storageclass
Done! kubectl is now configured to use "istio" cluster and "" namespace by default
Kubernetes 集群运行起来之后,我们就可以下载istioctl
CLI 工具来安装 Istio 到集群中了。在本例中,我们会从版本发布页面下载 Istio 1.9.4。
istioctl
工具安装完成之后,我们就可以将 Istio 部署到集群之中了。Istio 自带了不同的profiles,但是就开始学习 Istio 而言,demo profile 是最合适的。
istioctl install --set profile=demo -y
Detected that your cluster does not support third party JWT authentication. Falling back to less secure first party JWT. See https://istio.io/docs/ops/best-practices/security/#configure-third-party-service-account-tokens for details.
<small>✔ Istio core installed</small>
<small>✔ Istiod installed</small>
<small>✔ Egress gateways installed</small>
<small>✔ Ingress gateways installed</small>
<small>✔ Addons installed</small>
<small>✔ Installation complete</small>
我们要一直等到istio-system
命名空间中的所有 Pod 均处于 running 状态。
kubectl get pods -n istio-system
NAME READY STATUS RESTARTS AGE
grafana-b54bb57b9-fj6qk 1/1 Running 2 171d
istio-egressgateway-68587b7b8b-m5b58 1/1 Running 2 171d
istio-ingressgateway-55bdff67f-jrhpk 1/1 Running 2 171d
istio-tracing-9dd6c4f7c-9gcx9 1/1 Running 3 171d
istiod-76bf8475c-xphgd 1/1 Running 2 171d
kiali-d45468dc4-4nbl4 1/1 Running 2 171d
prometheus-74d44d84db-86hdr 2/2 Running 4 171d
为了发挥 Istio 的所有功能,网格中的 Pod 必须运行一个 Istio sidecar 代理。
我们有两种方式将 Istio sidecar 注入到 Pod 中:使用istioctl
命令手动注入或者在将 Pod 部署到配置好的命名空间时自动注入。
为了简单起见,我们通过执行如下命令,为default
命名空间配置默认的自动化 sidecar 注入:
kubectl label namespace default istio-injection=enabled
namespace/default labeled
现在,Istio 已经安装到了 Kubernetes 集群中,并且为在default
命名空间使用做好了准备。
在下面的章节中,我们将会看到如何“Istio 化”应用并部署一个这样的应用。
应用是由两个服务组成的,分别是 book service 和 rating service。Book service 返回一本图书的信息及其评分。Rating service 返回给定图书的评分。我们有 rating service 的两个版本:v1 会为所有的图书返回一个固定的评分(也就是 1),而 v2 会返回一个随机的评分值。
因为已经启用了 sidecar 注入,我们不需要对 Kubernetes 部署文件做任何变更。接下来,我们将这三个服务部署到“Istio 化”的命名空间中。
举例来说,_book service_的部署文件如下所示:
---
apiVersion: v1
kind: Service
metadata:
labels:
app.kubernetes.io/name: book-service
app.kubernetes.io/version: v1.0.0
name: book-service
spec:
ports:
- name: http
port: 8080
targetPort: 8080
selector:
app.kubernetes.io/name: book-service
app.kubernetes.io/version: v1.0.0
type: LoadBalancer
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.kubernetes.io/name: book-service
app.kubernetes.io/version: v1.0.0
name: book-service
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: book-service
app.kubernetes.io/version: v1.0.0
template:
metadata:
labels:
app.kubernetes.io/name: book-service
app.kubernetes.io/version: v1.0.0
spec:
containers:
- env:
- name: KUBERNETES_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
image: quay.io/lordofthejars/book-service:v1.0.0
imagePullPolicy: Always
name: book-service
ports:
- containerPort: 8080
name: http
protocol: TCP
我们可以看到,在文件中既没有 Istio 相关的内容,也没有 sidecar 容器的配置。Istio 功能的注入默认会自动进行。
我们把应用部署到 Kubernetes 集群中:
kubectl apply -f rating-service/src/main/kubernetes/service.yml -n default
kubectl apply -f rating-service/src/main/kubernetes/deployment-v1.yml -n default
kubectl apply -f rating-service/src/main/kubernetes/deployment-v2.yml -n default
kubectl apply -f book-service/src/main/kubernetes/deployment.yml -n default
几秒钟之后,应用就会启动起来了。为了进行校验,我们运行如下的命令并观察 Pod 所拥有的容器数量:
kubectl get pods -n default
NAME READY STATUS RESTARTS AGE
book-service-5cc59cdcfd-5qhb2 2/2 Running 0 79m
rating-service-v1-64b67cd8d-5bfpf 2/2 Running 0 63m
rating-service-v2-66b55746d-f4hpl 2/2 Running 0 63m
注意,每个 Pod 都包含了两个正在运行的容器,其中一个是服务本身,另外一个是 Istio 代理。
如果描述这个 Pod 的话,我们会发现:
kubectl describe pod rating-service-v2-66b55746d-f4hpl
Name: rating-service-v2-66b55746d-f4hpl
Namespace: default
…
Containers:
rating-service:
Container ID: docker://cda8d72194ee37e146df7bf0a6b23a184b5bfdb36fed00d2cc105daf6f0d6e85
Image: quay.io/lordofthejars/rating-service:v2.0.0
…
istio-proxy:
Container ID: docker://7f4a9c1f425ea3a06ccba58c74b2c9c3c72e58f1d805f86aace3d914781e0372
Image: docker.io/istio/proxyv2:1.6.13
因为我们使用了 Minikube 并且 Kubernetes 服务是 LoadBalancer 类型,所以要访问应用需要 Minikube 的 IP 和服务端口。为了找到这些值,可以执行如下命令:
minikube IP -p istio
192.168.99.116
kubectl get services -n default
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
book-service LoadBalancer 10.106.237.42 <pending> 8080:31304/TCP 111m
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 132m
rating LoadBalancer 10.109.106.128 <pending> 8080:31216/TCP 95m
接下来,我们可以对服务执行 curl 命令:
curl 192.168.99.116:31304/book/1
{"bookId":1,"name":"Book 1","rating":1}
curl 192.168.99.116:31304/book/1
{"bookId":1,"name":"Book 1","rating":3}
curl 192.168.99.116:31304/book/1
{"bookId":1,"name":"Book 1","rating":1}
curl 192.168.99.116:31304/book/1
{"bookId":1,"name":"Book 1","rating":3}
从输出我们可以看到评分值的变化,也就是对于同一个图书的 id,评分值会在 1 和 3 之间变化。默认情况下,Istio 会使用 round-robin 方式平衡对服务的调用。在本例中,请求会在rating:v1
(返回固定的评分值 1)和rating:v2
(在启动的时候进行随机的评分计算,在本例中,会对 ID 为 1 的图书返回 3)之间进行平衡。
应用现在已经部署好了,并且实现了“Istio 化”,但是到目前为止还没有启用任何的微服务特性。我们首先来创建一些 Istio 资源,以便于在 Istio 代理容器上启用和配置微服务特性。
Kubernetes Service_实现了服务发现的理念。它提供了一种方式将一组_Kubernetes Pod(作为一个整体)赋予一个稳定的虚拟 IP 和 DNS 名。Pod 在访问其他 Pod 的时候,可以使用_Kubernetes Service_名作为主机名。这只能允许我们实现基本的服务发现策略,但是我们可能会需要更高级的发现/部署策略,比如金丝雀发布、灰度发布或者镜像流量(shadowing traffic),此时 Kubernetes Service 就爱莫能助了。
Istio 能够让我们很容易地控制服务之间的网络流量,这是通过两个概念来实现的,即DestinationRule
和VirtualService
。
DestinationRule
定义了在路由发生之后如何为网络流量提供服务的策略。在 destination rule 中我们可以配置的内容如下所示:
VirtualService
中用到。我们创建一个名为destination-rule-v1-v2.yml
的文件来注册两个子集,其中一个用于_rating service v1_,另外一个用于_rating service v2_:
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: rating
spec:
host: rating
subsets:
- labels:
app.kubernetes.io/version: v1.0.0
name: version-v1
- labels:
app.kubernetes.io/version: v2.0.0
name: version-v2
在这里,我们将host
字段设置为 rating,因为这是在_Kubernetes Service_中定义的 DNS 名。随后,在subsets
部分,我们以labels
集的形式定义了多个子集,并将它们分组到一个“虚拟的”name
。例如,在前面的例子中,我们定义了两个组,其中一个组用于 rating service 的 version 1,另外一个组用于 version 2。
kubectl apply -f src/main/kubernetes/destination-rule-v1-v2.yml -n defaultdestinationrule.networking.istio.io/rating created
VirtualService
能够让我们配置请求该如何路由至 Istio 服务网格的服务中。借助 virtual service,实现像 A/B 测试、蓝/绿部署、金丝雀发布或灰度发布这样的策略就会变得非常简单。
我们创建一个名为virtual-service-v1.yml
的文件以发送所有的流量到 v1:
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: rating
spec:
hosts:
- rating
http:
- route:
- destination:
host: rating
subset: version-v1
weight: 100
在前面的文件中,我们配置所有到达 rating 主机的请求都会被发送到 version-v1 子集所属的 Pod 中。我们需要记住,子集是在DestinationRule
文件中创建的。
kubectl apply -f src/main/kubernetes/virtual-service-v1.yml -n defaultvirtualservice.networking.istio.io/rating created
现在,我们可以再次向服务执行一些curl
命令,但是在输出方面最大的差异在于所有的请求都发送到了_rating v1_中。
curl 192.168.99.116:31304/book/1
{"bookId":1,"name":"Book 1","rating":1}
curl 192.168.99.116:31304/book/1
{"bookId":1,"name":"Book 1","rating":1}
curl 192.168.99.116:31304/book/1
{"bookId":1,"name":"Book 1","rating":1}
显然,我们可以创建另外一个 virtual service 文件,使其指向 rating v2:
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: rating
spec:
hosts:
- rating
http:
- route:
- destination:
host: rating
subset: version-v2
weight: 100
kubectl apply -f src/main/kubernetes/virtual-service-v2.yml -n default
virtualservice.networking.istio.io/rating configured
这样,所有的流量会发送至_rating_ v2:
curl 192.168.99.116:31304/book/1
{"bookId":1,"name":"Book 1","rating":3}
curl 192.168.99.116:31304/book/1
{"bookId":1,"name":"Book 1","rating":3}
现在,rating
字段没有被设置为 1,这是因为所有的请求都被 version 2 处理了。
通过修改 virtual service 的weight
字段,我们就能实现金丝雀发布。
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: rating
spec:
hosts:
- rating
http:
- route:
- destination:
host: rating
subset: version-v1
weight: 75
- destination:
host: rating
subset: version-v2
weight: 25
kubectl apply -f src/main/kubernetes/virtual-service-v1-v2-75-25.yml -n defaultvirtualservice.networking.istio.io/rating configured
现在,我们对应用执行一些curl
命令:
curl 192.168.99.116:31304/book/1
{"bookId":1,"name":"Book 1","rating":1}
curl 192.168.99.116:31304/book/1
{"bookId":1,"name":"Book 1","rating":1}
curl 192.168.99.116:31304/book/1
{"bookId":1,"name":"Book 1","rating":1}
curl 192.168.99.116:31304/book/1
{"bookId":1,"name":"Book 1","rating":3}
rating v1 的访问次数要比 rating v2 更多,这遵循了在weight
字段中设置的占比。
现在,我们移除 virtual service 资源,使其回到默认的行为(也就是 round-robin 策略):
kubectl delete -f src/main/kubernetes/virtual-service-v1-v2-75-25.yml -n defaultvirtualservice.networking.istio.io "rating" deleted
在微服务架构中,我们在开发时要始终考虑到可能出现的故障,在与其他的服务进行通信时更是如此。在单体应用中,我们的应用会作为一个整体,要么全部处于可用状态,要么全部处于宕机状态,但是在微服务架构中,情况却并非如此,因为有些服务是可用的,而另外一些则可能已经宕机了。回弹性(或称为应用回弹性)是指一个应用/服务能够对面临的问题作出反应的能力,在出现问题的时候,依然能够提供尽可能最好的结果。
接下来我们看一下 Istio 如何帮助我们实现回弹性策略,以及如何配置它们。
rating service 实现了一个特殊的端点,当它被访问后会导致服务开始返回 503 HTTP 错误码。
执行如下的命令(将 Pod 名替换为你自己的),使服务 rating v2 在访问的时候开始出现故障::
kubectl get pods -n default
NAME READY STATUS RESTARTS AGE
book-service-5cc59cdcfd-5qhb2 2/2 Running 4 47h
rating-service-v1-64b67cd8d-5bfpf 2/2 Running 4 47h
rating-service-v2-66b55746d-f4hpl 2/2 Running 4 47h
kubectl exec -ti rating-service-v2-66b55746d-f4hpl -c rating-service -n default curl localhost:8080/rate/misbehave
Ratings endpoint returns 503 error.
目前,Istio 配置为没有 virtual service,这意味着它会在两个版本之间平衡请求。
我们发送一些请求并校验 rating v2 会失败:
curl 192.168.99.116:31304/book/1{"bookId":1,"name":"Book 1","rating":1}
curl 192.168.99.116:31304/book/1
curl 192.168.99.116:31304/book/1{"bookId":1,"name":"Book 1","rating":1}
其中有个请求没有产生响应,这是因为_rating v2_没有返回合法的响应,而是产生了错误。
Istio 支持重试,这是通过在VirtualService
资源中进行配置实现的。创建名为virutal-service-retry.yml
的文件,其内容如下所示:
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: rating
spec:
hosts:
- rating
http:
- route:
- destination:
host: rating
retries:
attempts: 2
perTryTimeout: 5s
retryOn: 5xx
按照配置,如果 rating service(不管哪个版本)返回5XX
HTTP 错误码的话,会自动进行两次重试。
kubectl apply -f src/main/kubernetes/virtua-service-retry.yml -n default
virtualservice.networking.istio.io/rating created
接下来,我们发送一些请求并检查输出:
curl 192.168.99.116:31304/book/1{"bookId":1,"name":"Book 1","rating":1}
curl 192.168.99.116:31304/book/1{"bookId":1,"name":"Book 1","rating":1}
curl 192.168.99.116:31304/book/1{"bookId":1,"name":"Book 1","rating":1}
现在,我们可以看到,所有的请求都是由 rating v1 响应的。原因很简单,当对 rating service 的请求发送至 v1,会得到一个合法的响应。但是,如果请求被发送到 v2 的时候,会出现错误并且会自动执行重试。
因为调用是在两个服务之间进行负载均衡的,所以重试请求会被发送到 v1,从而产生一个合法的响应。
基于这样的原因,上述的所有请求都会返回来自 v1 的响应。
对于处理网络故障或偶尔出现的错误来说,自动重试是一个很好的方式,但是如果多个并发用户向一个具有自动重试功能的故障系统发送请求时,会发生什么呢?
我们通过使用Siege(一个 HTTP 负载测试工具)模拟这个场景,但首先,我们使用 kubectl 命令来探查一下 rating v2 的日志:
kubectl get pods -n default
NAME READY STATUS RESTARTS AGE
book-service-5cc59cdcfd-5qhb2 2/2 Running 4 47h
rating-service-v1-64b67cd8d-5bfpf 2/2 Running 4 47h
rating-service-v2-66b55746d-f4hpl 2/2 Running 4 47h
kubectl logs rating-service-v2-66b55746d-f4hpl -c rating-service -n default
…
Request 31
Request 32
Request 33
Request 34
这些日志行展示了该服务所处理的请求数。目前,该服务处理了 34 个请求。
为了模拟四个并发用户,并且每个用户发送十个请求到应用上,我们可以执行如下的 siege 命令:
siege -r 10 -c 4 -v -d 1 192.168.99.116:31304/book/1
HTTP/1.1 200 0.04 secs: 39 bytes ==> GET /book/1
HTTP/1.1 200 0.03 secs: 39 bytes ==> GET /book/1
Transactions: 40 hits
Availability: 100.00 %
Elapsed time: 0.51 secs
Data transferred: 0.00 MB
Response time: 0.05 secs
Transaction rate: 78.43 trans/sec
Throughput: 0.00 MB/sec
Concurrency: 3.80
Successful transactions: 40
Failed transactions: 0
Longest transaction: 0.13
Shortest transaction: 0.01
当然,这里没有错误发送给调用者,这是因为有自动重试机制,但是我们再次探测一下 rating v2 的日志:
kubectl logs rating-service-v2-66b55746d-f4hpl -c rating-service -n default
…
Request 56
Request 57
Request 58
Request 59
尽管 rating v2 不能产生一个合法的响应,但是服务依然被访问了 25 次,这会对应用产生很大的影响,因为:
为了解决这个问题,我们需要有一种方式能够在出现重复执行失败的时候,让调用能够自动地快速失败。断路器(circuit breaker)设计模式和舱壁(bulkhead)模式是这个问题的解决方案。前者提供了在遇到并发错误的时候,快速失败的策略,而后者则能限制并发执行的数量。
现在,创建一个名为destination-rule-circuit-breaker.yml
的文件,内容如下所示:
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: rating
spec:
host: rating
subsets:
- labels:
version: v1
name: version-v1
- labels:
version: v2
name: version-v2
trafficPolicy:
connectionPool:
http:
http1MaxPendingRequests: 3
maxRequestsPerConnection: 3
tcp:
maxConnections: 3
outlierDetection:
baseEjectionTime: 3m
consecutive5xxErrors: 1
interval: 1s
maxEjectionPercent: 100
我们要注意的第一件事情就是DestinationRule
配置了断路器。除了配置断路器之外,子集也需要指定。对并发连接的限制是在connectionPool
字段中实现的。
要配置断路器,我们需要使用outlierDetection
。就本例而言,如果在一秒钟的时间窗口中发生了一次错误,断路器将会打开,使服务暂时跳闸(trip)三分钟。在这个时间之后,断路器会处于半开状态,这意味着会执行真实的逻辑。如果再次失败的话,断路器会保持打开的状态,否则的话,它将会关闭。
kubectl apply -f src/main/kubernetes/destination-rule-circuit-breaker.ymldestinationrule.networking.istio.io/rating configured
我们已经在 Istio 中配置完了断路器模式,接下来,我们再次执行siege
命令并探查_rating v2_ v2 的日志。
siege -r 10 -c 4 -v -d 1 192.168.99.116:31304/book/1
HTTP/1.1 200 0.04 secs: 39 bytes ==> GET /book/1
HTTP/1.1 200 0.03 secs: 39 bytes ==> GET /book/1
Transactions: 40 hits
Availability: 100.00 %
再次探查日志。注意,在前面的运行中,我们已经到了 Request 59。
kubectl logs rating-service-v2-66b55746d-f4hpl -c rating-service -n default
…
Request 56
Request 57
Request 58
Request 59
Request 60
kubectl logs rating-service-v2-66b55746d-f4hpl -c rating-service -n default
…
Request 56
Request 57
Request 58
Request 59
Request 60
_Rating v2 _只接收到了一个请求,因为在第一次处理请求的时候返回了错误,断路器就会打开,因此不会有更多的请求发送到 rating v2 上。
现在,我们已经看到了如何使用 Istio 实现回弹性。在这里,我们并没有在服务中实现相关的逻辑,将其与业务逻辑混在一起,而是让 sidecar 容器实现了这些逻辑。
最后,执行如下的命令,让 rating v2 服务回到之前的状态。
kubectl exec -ti rating-service-v2-66b55746d-f4hpl -c rating-service curl localhost:8080/rate/behaveBack to normal
在实现微服务架构的时候,我们可能会发现的一个问题就是如何保护内部服务之间的通信。我们是不是要使用 mTLS?是不是要对请求进行认证?是否要对请求进行鉴权?所有这些问题的答案都是肯定的!接下来,我们将会一步一步地看一下 Istio 是如何帮助我们实现这些功能的。
Istio 会自动将代理和工作负载之间的所有网络流量升级为 mTLS,这个过程不需要修改任何的服务代码。与此同时,作为开发人员,我们会使用 HTTP 协议实现服务。当服务被“Istio 化”的时候,服务之间的通信会采用 HTTPS。Istio 会负责管理证书,担任证书颁发机构并撤销/更新证书。
要校验 mTLS 是否已启用,我们可以使用 istioctl 工具执行如下的命令:
istioctl experimental authz check book-service-5cc59cdcfd-5qhb2 -a
LISTENER[FilterChain] HTTP ROUTE ALPN mTLS (MODE) AuthZ (RULES)
...
virtualInbound[5] inbound|8080|http|book-service.default.svc.cluster.local istio,istio-http/1.0,istio-http/1.1,istio-h2 noneSDS: default yes (PERMISSIVE) no (none)
…
book-service 托管在了 8080 端口,并且以 permissive 策略配置了 mTLS。
接下来,我们看一下如何使用 JSON Web Token(JWT)格式启用 Istio 的终端用户认证。
我们要做的第一件事情是应用一个RequestAuthentication
资源。这个策略能够确保如果Authorization
头信息包含 JWT token 的话,它必须是合法的、没有过期的、由正确的用户颁发的并且没有被篡改。
apiVersion: "security.istio.io/v1beta1"
kind: "RequestAuthentication"
metadata:
name: "bookjwt"
namespace: default
spec:
selector:
matchLabels:
app.kubernetes.io/name: book-service
jwtRules:
- issuer: "testing@secure.istio.io"
jwksUri: "https://gist.githubusercontent.com/lordofthejars/7dad589384612d7a6e18398ac0f10065/raw/ea0f8e7b729fb1df25d4dc60bf17dee409aad204/jwks.json"
其中的关键字段包括:
issuer
:token 的合法颁发者。如果所提供的 token 没有在iss
JWT 字段指定该颁发者,那么这个 token 就是非法的。jwksUri
:jwks
文件的 URL,它指定了公钥注册的地址,用来校验 token 的签名。kubectl apply -f src/main/kubernetes/request-authentication-jwt.yml -n defaultrequestauthentication.security.istio.io/bookjwt created
我们现在使用一个非法的 token 来运行curl
命令:
curl 192.168.99.116:31304/book/1 -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkRIRmJwb0lVcXJZOHQyenBBMnFYZkNtcjVWTzVaRXI0UnpIVV8tZW52dlEiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjQ2ODU5ODk3MDAsImZvbyI6ImJhciIsImlhdCI6MTUzMjM4OTcwMCwiaXNzIjoidGVzdGluZ0BzZWN1cmUuaXN0aW8uaW8iLCJzdWIiOiJ0ZXN0aW5nQHNlY3VyZS5pc3Rpby5pbyJ9.CfNnxWP2tcnR9q0vxyxweaF3ovQYHYZl82hAUsn21bwQd9zP7c-LS9qd_vpdLG4Tn1A15NxfCjp5f7QNBUo-KC9PJqYpgGbaXhaGx7bEdFWjcwv3nZzvc7M__ZpaCERdwU7igUmJqYGBYQ51vr2njU9ZimyKkfDe3axcyiBZde7G6dabliUosJvvKOPcKIWPccCgefSj_GNfwIip3-SsFdlR7BtbVUcqR-yv-XOxJ3Uc1MI0tz3uMiiZcyPV7sNCU4KRnemRIMHVOfuvHsU60_GhGbiSFzgPTAa9WTltbnarTbxudb_YEOx12JiwYToeX0DCPb43W1tzIBxgm8NxUU"
Jwt verification fails
因为 token 是非法的,所以请求会被拒绝,并返回 HTTP/1.1 401 Unauthorized 状态码。
使用合法的 token 重复前面的请求:
curl 192.168.99.116:31304/book/1 -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkRIRmJwb0lVcXJZOHQyenBBMnFYZkNtcjVWTzVaRXI0UnpIVV8tZW52dlEiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjQ2ODU5ODk3MDAsImZvbyI6ImJhciIsImlhdCI6MTUzMjM4OTcwMCwiaXNzIjoidGVzdGluZ0BzZWN1cmUuaXN0aW8uaW8iLCJzdWIiOiJ0ZXN0aW5nQHNlY3VyZS5pc3Rpby5pbyJ9.CfNnxWP2tcnR9q0vxyxweaF3ovQYHYZl82hAUsn21bwQd9zP7c-LS9qd_vpdLG4Tn1A15NxfCjp5f7QNBUo-KC9PJqYpgGbaXhaGx7bEdFWjcwv3nZzvc7M__ZpaCERdwU7igUmJqYGBYQ51vr2njU9ZimyKkfDe3axcyiBZde7G6dabliUosJvvKOPcKIWPccCgefSj_GNfwIip3-SsFdlR7BtbVUcqR-yv-XOxJ3Uc1MI0tz3uMiiZcyPV7sNCU4KRnemRIMHVOfuvHsU60_GhGbiSFzgPTAa9WTltbnarTbxudb_YEOx12JiwYToeX0DCPb43W1tzIBxgm8NxUg"
{"bookId":1,"name":"Book 1","rating":3}
现在,我们可以看到一个合法的响应了,因为此时 token 是正确的。
到目前为止,我们只是认证了请求(只需要一个合法的 token),其实 Istio 还支持基于角色访问控制(role-based access control,RBAC)模型的授权。我们接下来创建一个AuthorizationPolicy
策略,只允许具有合法 JSON Web Token 并且 claim role 设置为 customer 的请求。创建名为authorization-policy-jwt.yml
的文件:
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: require-jwt
namespace: default
spec:
selector:
matchLabels:
app.kubernetes.io/name: book-service
action: ALLOW
rules:
- from:
- source:
requestPrincipals: ["testing@secure.istio.io/testing@secure.istio.io"]
when:
- key: request.auth.claims[role]
values: ["customer"]
kubectl apply -f src/main/kubernetes/authorization-policy-jwt.ymlauthorizationpolicy.security.istio.io/require-jwt created
然后执行和上面一样的 curl 命令:
curl 192.168.99.116:31304/book/1 -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkRIRmJwb0lVcXJZOHQyenBBMnFYZkNtcjVWTzVaRXI0UnpIVV8tZW52dlEiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjQ2ODU5ODk3MDAsImZvbyI6ImJhciIsImlhdCI6MTUzMjM4OTcwMCwiaXNzIjoidGVzdGluZ0BzZWN1cmUuaXN0aW8uaW8iLCJzdWIiOiJ0ZXN0aW5nQHNlY3VyZS5pc3Rpby5pbyJ9.CfNnxWP2tcnR9q0vxyxweaF3ovQYHYZl82hAUsn21bwQd9zP7c-LS9qd_vpdLG4Tn1A15NxfCjp5f7QNBUo-KC9PJqYpgGbaXhaGx7bEdFWjcwv3nZzvc7M__ZpaCERdwU7igUmJqYGBYQ51vr2njU9ZimyKkfDe3axcyiBZde7G6dabliUosJvvKOPcKIWPccCgefSj_GNfwIip3-SsFdlR7BtbVUcqR-yv-XOxJ3Uc1MI0tz3uMiiZcyPV7sNCU4KRnemRIMHVOfuvHsU60_GhGbiSFzgPTAa9WTltbnarTbxudb_YEOx12JiwYToeX0DCPb43W1tzIBxgm8NxUg"
RBAC: access denied
这一次的响应显然不一样了。尽管 token 是合法的,但是访问被拒绝了,这是因为 token 中并没有值为 customer 的 claim role。
然后,我们使用如下的 token:
curl 192.168.99.116:31304/book/1 -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkRIRmJwb0lVcXJZOHQyenBBMnFYZkNtcjVWTzVaRXI0UnpIVV8tZW52dlEiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjI1NDkwNTY4ODgsImlhdCI6MTU0OTA1Njg4OSwiaXNzIjoidGVzdGluZ0BzZWN1cmUuaXN0aW8uaW8iLCJyb2xlIjoiY3VzdG9tZXIiLCJzdWIiOiJ0ZXN0aW5nQHNlY3VyZS5pc3Rpby5pbyJ9.VM9VOHD2NwDjQ6k7tszB3helfAn5wcldxe950BveiFVg43pp7x5MWTjMtWQRmQc7iYul19PXsmGnSSOiQQobxdn2UnhHJeKeccCdX5YVgX68tR0R9xv_wxeYQWquH3roxHh2Xr2SU3gdt6s7gxKHrW7Zc4Z9bT-fnz3ijRUiyrs-HQN7DBc356eiZy2wS7O539lx3mr-pjM9PQtcDCDOGsnmwq1YdKw9o2VgbesfiHDDjJQlNv40wnsfpq2q4BgSmdsofAGwSNKWtqUE6kU7K2hvV2FvgwjzcB19bbRYMWxRG0gHyqgFy-uM5tsC6Cib-gPAIWxCdXDmLEiqIdjM3w"
{"bookId":1,"name":"Book 1","rating":3}
现在,我们看到了一个合法的响应,因为此时 token 是正确的并且包含了一个合法的 role 值。
Istio 自带了四个组件以适应可观察性的需求:
我们可以在istio-system
命名空间中看到所有的 Pod:
kubectl get pods -n istio-system
NAME READY STATUS RESTARTS AGE
grafana-b54bb57b9-k5qbm 1/1 Running 0 178m
istio-egressgateway-68587b7b8b-vdr67 1/1 Running 0 178m
istio-ingressgateway-55bdff67f-hlnqw 1/1 Running 0 178m
istio-tracing-9dd6c4f7c-44xhk 1/1 Running 0 178m
istiod-76bf8475c-xphgd 1/1 Running 7 177d
kiali-d45468dc4-fl8j4 1/1 Running 0 178m
prometheus-74d44d84db-zmkd7 2/2 Running 0 178m
Istio 集成了Prometheus,用于发送与网络流量和服务相关的各种信息。除此之外,它还提供了一个Grafana实例来可视化所有收集到的数据。
要访问 Grafana,我们可以使用 port-forward 命令来将 Pod 暴露出来:
kubectl port-forward -n istio-system grafana-b54bb57b9-k5qbm 3000:3000
Forwarding from 127.0.0.1:3000 -> 3000
Forwarding from [::1]:3000 -> 3000
打开浏览器并导航至locahost:3000
以访问 Grafana 的仪表盘。
Kiali是另外一个在 Istio 中运行的工具,它能够管理 Istio 并观察服务网格参数,比如服务是如何连接的、它们是如何执行的以及 Istio 资源是如何注册的。
要访问 Kiali,我们可以使用 port-forward 命令来将 Pod 暴露出来:
kubectl port-forward -n istio-system kiali-d45468dc4-fl8j4 20001:20001
Forwarding from 127.0.0.1:20001 -> 20001
Forwarding from [::1]:20001 -> 20001
打开浏览器,访问 Istio 仪表盘,然后导航至 locahost:20001。
跟踪用来可视化一个程序的流程和数据进展。Istio 会拦截所有的请求/响应,并将它们发送至Jaeger。
在这里,我们可以不用 port-forward 命令,而是使用istioctl
来暴露端口并自动打开页面。
istioctl dashboard jaeger
开发和实现微服务架构要比开发单体应用更具挑战性。我们相信,微服务特性能够促使你在应用基础设施方面正确地开发服务。
Istio 在一个 sidecar 容器中实现了一些微服务特性,使得它们能够跨所有的服务进行重用,独立于应用所使用的编程语言。
除此之外,Istio 方式能够让我们在不重新部署服务的前提下改变服务的行为。
如果你计划开发微服务并将它们部署到 Kubernetes 中,那么 Istio 是一个切实可行的方案,因为它能够与 Kubernetes 无缝集成。
本文中所用到的源码可以在 GitHub 的仓库中找到,本系列第一篇文章的源码也可以在 GitHub 的仓库中找到。
作者简介:
Alex Soto 是红帽公司的开发者体验总监。他对 Java 领域、软件自动化充满热情,他相信开源软件模式。Soto 是Manning的《Testing Java Microservices》和O’Reilly的《Quarkus Cookbook》两本书的共同作者,他还是多个开源项目的贡献者。自 2017 年以来,他一直是 Java Champion,是国际演讲者和 Salle URL 大学的教师。你可以在 Twitter 上关注他(Alex Soto ⚛️),随时了解 Kubernetes 和 Java 领域的动态。
原文链接:
领取专属 10元无门槛券
私享最新 技术干货