[关闭]
@levinzhang 2021-10-26T13:40:31.000000Z 字数 23876 阅读 590

通过Istio实现微服务特性

by

摘要:

微服务特性(microservicility)指的是除了业务逻辑之外,服务必须要实现的一组横切性的关注点。这些关注点包括调用、弹性(elasticity)和回弹性(resiliency)等。本文描述了如何像Istio这样的服务网格来实现这些关注点。


核心要点

在微服务架构中,应用程序是由多个相互连接的服务组成的,这些服务协同工作以实现所需的业务功能。所以,一个典型的企业级微服务架构如下所示:

最初,我们可能认为使用微服务架构实现一个应用程序是很容易的事情。但是,要恰当地完成这一点并不容易,因为我们会面临一些新的挑战,而这些挑战是单体架构所未曾遇到的。举例来讲,这样的挑战包括容错、服务发现、扩展性、日志和跟踪等。

为了应对这些挑战,每个微服务都需要实现在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?

服务网格是一个专用的基础设施层,目的在于使得服务与服务之间的通信变得安全、快速和可靠。

服务网格通常以轻量级网络代理的形式实现并且会与服务代码部署在一起,它会拦截服务所有进站/出站的网络流量。

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

我们需要一个安装Istio的Kubernetes集群。就本文来讲,我们会使用Minikube,但是任意其他的Kubernetes集群都是可以的。

运行如下的命令来启动集群:

  1. minikube start -p istio --kubernetes-version='v1.19.0' --vm-driver='virtualbox' --memory=4096
  2. [istio] minikube v1.17.1 on Darwin 11.3
  3. Kubernetes 1.20.2 is now available. If you would like to upgrade, specify: --kubernetes-version=v1.20.2
  4. minikube 1.19.0 is available! Download it: https://github.com/kubernetes/minikube/releases/tag/v1.19.0
  5. To disable this notice, run: 'minikube config set WantUpdateNotification false'

✨  基于已有的profile并使用virtualbox驱动

❗  对于既有的minikube集群,我们无法改变它的内存大小。如果需要的话,请先将该集群删除掉。

  1. Starting control plane node istio in cluster istio
  2. Restarting existing virtualbox VM for "istio" ...
  3. Preparing Kubernetes v1.19.0 on Docker 19.03.12 ...
  4. Verifying Kubernetes components...
  5. Enabled addons: storage-provisioner, default-storageclass
  6. Done! kubectl is now configured to use "istio" cluster and "" namespace by default

Kubernetes集群运行起来之后,我们就可以下载istioctlCLI工具来安装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.

✔ Istio core installed

✔ Istiod installed

✔ Egress gateways installed

✔ Ingress gateways installed

✔ Addons installed

✔ Installation complete

我们要一直等到istio-system命名空间中的所有Pod均处于running状态。

  1. kubectl get pods -n istio-system
  2. NAME READY STATUS RESTARTS AGE
  3. grafana-b54bb57b9-fj6qk 1/1 Running 2 171d
  4. istio-egressgateway-68587b7b8b-m5b58 1/1 Running 2 171d
  5. istio-ingressgateway-55bdff67f-jrhpk 1/1 Running 2 171d
  6. istio-tracing-9dd6c4f7c-9gcx9 1/1 Running 3 171d
  7. istiod-76bf8475c-xphgd 1/1 Running 2 171d
  8. kiali-d45468dc4-4nbl4 1/1 Running 2 171d
  9. 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的部署文件如下所示:

  1. ---
  2. apiVersion: v1
  3. kind: Service
  4. metadata:
  5. labels:
  6. app.kubernetes.io/name: book-service
  7. app.kubernetes.io/version: v1.0.0
  8. name: book-service
  9. spec:
  10. ports:
  11. - name: http
  12. port: 8080
  13. targetPort: 8080
  14. selector:
  15. app.kubernetes.io/name: book-service
  16. app.kubernetes.io/version: v1.0.0
  17. type: LoadBalancer
  18. ---
  19. apiVersion: apps/v1
  20. kind: Deployment
  21. metadata:
  22. labels:
  23. app.kubernetes.io/name: book-service
  24. app.kubernetes.io/version: v1.0.0
  25. name: book-service
  26. spec:
  27. replicas: 1
  28. selector:
  29. matchLabels:
  30. app.kubernetes.io/name: book-service
  31. app.kubernetes.io/version: v1.0.0
  32. template:
  33. metadata:
  34. labels:
  35. app.kubernetes.io/name: book-service
  36. app.kubernetes.io/version: v1.0.0
  37. spec:
  38. containers:
  39. - env:
  40. - name: KUBERNETES_NAMESPACE
  41. valueFrom:
  42. fieldRef:
  43. fieldPath: metadata.namespace
  44. image: quay.io/lordofthejars/book-service:v1.0.0
  45. imagePullPolicy: Always
  46. name: book-service
  47. ports:
  48. - containerPort: 8080
  49. name: http
  50. protocol: TCP

我们可以看到,在文件中既没有Istio相关的内容,也没有sidecar容器的配置。Istio功能的注入默认会自动进行。

我们把应用部署到Kubernetes集群中:

  1. kubectl apply -f rating-service/src/main/kubernetes/service.yml -n default
  2. kubectl apply -f rating-service/src/main/kubernetes/deployment-v1.yml -n default
  3. kubectl apply -f rating-service/src/main/kubernetes/deployment-v2.yml -n default
  4. kubectl apply -f book-service/src/main/kubernetes/deployment.yml -n default

几秒钟之后,应用就会启动起来了。为了进行校验,我们运行如下的命令并观察Pod所拥有的容器数量:

  1. kubectl get pods -n default
  2. NAME READY STATUS RESTARTS AGE
  3. book-service-5cc59cdcfd-5qhb2 2/2 Running 0 79m
  4. rating-service-v1-64b67cd8d-5bfpf 2/2 Running 0 63m
  5. rating-service-v2-66b55746d-f4hpl 2/2 Running 0 63m

注意,每个Pod都包含了两个正在运行的容器,其中一个是服务本身,另外一个是Istio代理。

如果描述这个Pod的话,我们会发现:

  1. kubectl describe pod rating-service-v2-66b55746d-f4hpl
  2. Name: rating-service-v2-66b55746d-f4hpl
  3. Namespace: default
  4. Containers:
  5. rating-service:
  6. Container ID: docker://cda8d72194ee37e146df7bf0a6b23a184b5bfdb36fed00d2cc105daf6f0d6e85
  7. Image: quay.io/lordofthejars/rating-service:v2.0.0
  8. istio-proxy:
  9. Container ID: docker://7f4a9c1f425ea3a06ccba58c74b2c9c3c72e58f1d805f86aace3d914781e0372
  10. Image: docker.io/istio/proxyv2:1.6.13

因为我们使用了Minikube并且Kubernetes服务是LoadBalancer类型,所以要访问应用需要Minikube的 IP和服务端口。为了找到这些值,可以执行如下命令:

  1. minikube IP -p istio
  2. 192.168.99.116
  3. kubectl get services -n default
  4. NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
  5. book-service LoadBalancer 10.106.237.42 <pending> 8080:31304/TCP 111m
  6. kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 132m
  7. rating LoadBalancer 10.109.106.128 <pending> 8080:31216/TCP 95m

接下来,我们可以对服务执行curl命令:

  1. curl 192.168.99.116:31304/book/1
  2. {"bookId":1,"name":"Book 1","rating":1}
  3. curl 192.168.99.116:31304/book/1
  4. {"bookId":1,"name":"Book 1","rating":3}
  5. curl 192.168.99.116:31304/book/1
  6. {"bookId":1,"name":"Book 1","rating":1}
  7. curl 192.168.99.116:31304/book/1
  8. {"bookId":1,"name":"Book 1","rating":3}

从输出我们可以看到评分值的变化,也就是对于同一个图书的id,评分值会在1和3之间变化。默认情况下,Istio会使用round-robin方式平衡对服务的调用。在本例中,请求会在rating:v1(返回固定的评分值1)和rating:v2(在启动的时候进行随机的评分计算,在本例中,会对ID为1的图书返回3)之间进行平衡。

应用现在已经部署好了,并且实现了“Istio化”,但是到目前为止还没有启用任何的微服务特性。我们首先来创建一些Istio资源,以便于在Istio代理容器上启用和配置微服务特性。

Istio微服务特性

服务发现

Kubernetes Service实现了服务发现的理念。它提供了一种方式将一组Kubernetes Pod(作为一个整体)赋予一个稳定的虚拟IP和DNS名。Pod在访问其他Pod的时候,可以使用Kubernetes Service名作为主机名。这只能允许我们实现基本的服务发现策略,但是我们可能会需要更高级的发现/部署策略,比如金丝雀发布、灰度发布或者镜像流量(shadowing traffic),此时Kubernetes Service就爱莫能助了。

Istio能够让我们很容易地控制服务之间的网络流量,这是通过两个概念来实现的,即DestinationRuleVirtualService

DestinationRule定义了在路由发生之后如何为网络流量提供服务的策略。在destination rule中我们可以配置的内容如下所示:

我们创建一个名为destination-rule-v1-v2.yml的文件来注册两个子集,其中一个用于rating service v1,另外一个用于rating service v2

  1. apiVersion: networking.istio.io/v1alpha3
  2. kind: DestinationRule
  3. metadata:
  4. name: rating
  5. spec:
  6. host: rating
  7. subsets:
  8. - labels:
  9. app.kubernetes.io/version: v1.0.0
  10. name: version-v1
  11. - labels:
  12. app.kubernetes.io/version: v2.0.0
  13. name: version-v2

在这里,我们将host字段设置为rating,因为这是在Kubernetes Service中定义的DNS名。随后,在subsets部分,我们以labels集的形式定义了多个子集,并将它们分组到一个“虚拟的”name。例如,在前面的例子中,我们定义了两个组,其中一个组用于rating service的version 1,另外一个组用于version 2。

  1. kubectl apply -f src/main/kubernetes/destination-rule-v1-v2.yml -n default
  2. destinationrule.networking.istio.io/rating created

VirtualService能够让我们配置请求该如何路由至Istio服务网格的服务中。借助virtual service,实现像A/B测试、蓝/绿部署、金丝雀发布或灰度发布这样的策略就会变得非常简单。

我们创建一个名为virtual-service-v1.yml的文件以发送所有的流量到v1:

  1. apiVersion: networking.istio.io/v1alpha3
  2. kind: VirtualService
  3. metadata:
  4. name: rating
  5. spec:
  6. hosts:
  7. - rating
  8. http:
  9. - route:
  10. - destination:
  11. host: rating
  12. subset: version-v1
  13. weight: 100

在前面的文件中,我们配置所有到达rating主机的请求都会被发送到version-v1子集所属的Pod中。我们需要记住,子集是在DestinationRule文件中创建的。

  1. kubectl apply -f src/main/kubernetes/virtual-service-v1.yml -n default
  2. virtualservice.networking.istio.io/rating created

现在,我们可以再次向服务执行一些curl命令,但是在输出方面最大的差异在于所有的请求都发送到了rating v1中。

  1. curl 192.168.99.116:31304/book/1
  2. {"bookId":1,"name":"Book 1","rating":1}
  3. curl 192.168.99.116:31304/book/1
  4. {"bookId":1,"name":"Book 1","rating":1}
  5. curl 192.168.99.116:31304/book/1
  6. {"bookId":1,"name":"Book 1","rating":1}

显然,我们可以创建另外一个virtual service文件,使其指向rating v2:

  1. apiVersion: networking.istio.io/v1alpha3
  2. kind: VirtualService
  3. metadata:
  4. name: rating
  5. spec:
  6. hosts:
  7. - rating
  8. http:
  9. - route:
  10. - destination:
  11. host: rating
  12. subset: version-v2
  13. weight: 100
  1. kubectl apply -f src/main/kubernetes/virtual-service-v2.yml -n default
  2. virtualservice.networking.istio.io/rating configured

这样,所有的流量会发送至rating v2:

  1. curl 192.168.99.116:31304/book/1
  2. {"bookId":1,"name":"Book 1","rating":3}
  3. curl 192.168.99.116:31304/book/1
  4. {"bookId":1,"name":"Book 1","rating":3}

现在,rating字段没有被设置为1,这是因为所有的请求都被version 2处理了。

通过修改virtual service的weight字段,我们就能实现金丝雀发布

  1. apiVersion: networking.istio.io/v1alpha3
  2. kind: VirtualService
  3. metadata:
  4. name: rating
  5. spec:
  6. hosts:
  7. - rating
  8. http:
  9. - route:
  10. - destination:
  11. host: rating
  12. subset: version-v1
  13. weight: 75
  14. - destination:
  15. host: rating
  16. subset: version-v2
  17. weight: 25
  1. kubectl apply -f src/main/kubernetes/virtual-service-v1-v2-75-25.yml -n default
  2. virtualservice.networking.istio.io/rating configured

现在,我们对应用执行一些curl命令:

  1. curl 192.168.99.116:31304/book/1
  2. {"bookId":1,"name":"Book 1","rating":1}
  3. curl 192.168.99.116:31304/book/1
  4. {"bookId":1,"name":"Book 1","rating":1}
  5. curl 192.168.99.116:31304/book/1
  6. {"bookId":1,"name":"Book 1","rating":1}
  7. curl 192.168.99.116:31304/book/1
  8. {"bookId":1,"name":"Book 1","rating":3}

rating v1的访问次数要比rating v2更多,这遵循了在weight字段中设置的占比。

现在,我们移除virtual service资源,使其回到默认的行为(也就是round-robin策略):

  1. kubectl delete -f src/main/kubernetes/virtual-service-v1-v2-75-25.yml -n default
  2. virtualservice.networking.istio.io "rating" deleted

回弹性

在微服务架构中,我们在开发时要始终考虑到可能出现的故障,在与其他的服务进行通信时更是如此。在单体应用中,我们的应用会作为一个整体,要么全部处于可用状态,要么全部处于宕机状态,但是在微服务架构中,情况却并非如此,因为有些服务是可用的,而另外一些则可能已经宕机了。回弹性(或称为应用回弹性)是指一个应用/服务能够对面临的问题作出反应的能力,在出现问题的时候,依然能够提供尽可能最好的结果。

接下来我们看一下Istio如何帮助我们实现回弹性策略,以及如何配置它们。

故障

rating service实现了一个特殊的端点,当它被访问后会导致服务开始返回503 HTTP错误码。

执行如下的命令(将Pod名替换为你自己的),使服务rating v2在访问的时候开始出现故障::

  1. kubectl get pods -n default
  2. NAME READY STATUS RESTARTS AGE
  3. book-service-5cc59cdcfd-5qhb2 2/2 Running 4 47h
  4. rating-service-v1-64b67cd8d-5bfpf 2/2 Running 4 47h
  5. rating-service-v2-66b55746d-f4hpl 2/2 Running 4 47h
  6. kubectl exec -ti rating-service-v2-66b55746d-f4hpl -c rating-service -n default curl localhost:8080/rate/misbehave
  7. Ratings endpoint returns 503 error.

重试

目前,Istio配置为没有virtual service,这意味着它会在两个版本之间平衡请求。

我们发送一些请求并校验rating v2会失败:

  1. curl 192.168.99.116:31304/book/1
  2. {"bookId":1,"name":"Book 1","rating":1}
  3. curl 192.168.99.116:31304/book/1
  4. curl 192.168.99.116:31304/book/1
  5. {"bookId":1,"name":"Book 1","rating":1}

其中有个请求没有产生响应,这是因为rating v2没有返回合法的响应,而是产生了错误。

Istio支持重试,这是通过在VirtualService资源中进行配置实现的。创建名为virutal-service-retry.yml的文件,其内容如下所示:

  1. apiVersion: networking.istio.io/v1alpha3
  2. kind: VirtualService
  3. metadata:
  4. name: rating
  5. spec:
  6. hosts:
  7. - rating
  8. http:
  9. - route:
  10. - destination:
  11. host: rating
  12. retries:
  13. attempts: 2
  14. perTryTimeout: 5s
  15. retryOn: 5xx

按照配置,如果rating service(不管哪个版本)返回5XX HTTP错误码的话,会自动进行两次重试。

  1. kubectl apply -f src/main/kubernetes/virtua-service-retry.yml -n default
  2. virtualservice.networking.istio.io/rating created

接下来,我们发送一些请求并检查输出:

  1. curl 192.168.99.116:31304/book/1
  2. {"bookId":1,"name":"Book 1","rating":1}
  3. curl 192.168.99.116:31304/book/1
  4. {"bookId":1,"name":"Book 1","rating":1}
  5. curl 192.168.99.116:31304/book/1
  6. {"bookId":1,"name":"Book 1","rating":1}

现在,我们可以看到,所有的请求都是由rating v1响应的。原因很简单,当对rating service的请求发送至v1,会得到一个合法的响应。但是,如果请求被发送到v2的时候,会出现错误并且会自动执行重试。

因为调用是在两个服务之间进行负载均衡的,所以重试请求会被发送到v1,从而产生一个合法的响应。

基于这样的原因,上述的所有请求都会返回来自v1的响应。

断路器

对于处理网络故障或偶尔出现的错误来说,自动重试是一个很好的方式,但是如果多个并发用户向一个具有自动重试功能的故障系统发送请求时,会发生什么呢?

我们通过使用Siege(一个HTTP负载测试工具)模拟这个场景,但首先,我们使用kubectl命令来探查一下rating v2的日志:

  1. kubectl get pods -n default
  2. NAME READY STATUS RESTARTS AGE
  3. book-service-5cc59cdcfd-5qhb2 2/2 Running 4 47h
  4. rating-service-v1-64b67cd8d-5bfpf 2/2 Running 4 47h
  5. rating-service-v2-66b55746d-f4hpl 2/2 Running 4 47h
  6. kubectl logs rating-service-v2-66b55746d-f4hpl -c rating-service -n default
  7. Request 31
  8. Request 32
  9. Request 33
  10. Request 34

这些日志行展示了该服务所处理的请求数。目前,该服务处理了34个请求。

为了模拟四个并发用户,并且每个用户发送十个请求到应用上,我们可以执行如下的siege命令:

  1. siege -r 10 -c 4 -v -d 1 192.168.99.116:31304/book/1
  2. HTTP/1.1 200 0.04 secs: 39 bytes ==> GET /book/1
  3. HTTP/1.1 200 0.03 secs: 39 bytes ==> GET /book/1
  4. Transactions: 40 hits
  5. Availability: 100.00 %
  6. Elapsed time: 0.51 secs
  7. Data transferred: 0.00 MB
  8. Response time: 0.05 secs
  9. Transaction rate: 78.43 trans/sec
  10. Throughput: 0.00 MB/sec
  11. Concurrency: 3.80
  12. Successful transactions: 40
  13. Failed transactions: 0
  14. Longest transaction: 0.13
  15. Shortest transaction: 0.01

当然,这里没有错误发送给调用者,这是因为有自动重试机制,但是我们再次探测一下rating v2的日志:

  1. kubectl logs rating-service-v2-66b55746d-f4hpl -c rating-service -n default
  2. Request 56
  3. Request 57
  4. Request 58
  5. Request 59

尽管rating v2不能产生一个合法的响应,但是服务依然被访问了25次,这会对应用产生很大的影响,因为:

  1. 如果服务已经处于过载状态的话,发送更多的请求对它的恢复来讲并不是一个好主意。也许,最好的方式是将实例放到一个隔离区中。
  2. 如果服务此时恰好因为某个缺陷出现了故障,那么重试并不会改善这种情况。
  3. 对于每次重试,都会建立一个socket、分配一些文件描述符(file descriptor),还要通过网络发送一些数据包,但最终得到的却是故障。这个过程会影响在同一个节点中其他服务(CPU、内存、文件描述符等)或者网络(增加无用的流量、延迟等)。

为了解决这个问题,我们需要有一种方式能够在出现重复执行失败的时候,让调用能够自动地快速失败。断路器(circuit breaker)设计模式和舱壁(bulkhead)模式是这个问题的解决方案。前者提供了在遇到并发错误的时候,快速失败的策略,而后者则能限制并发执行的数量。

现在,创建一个名为destination-rule-circuit-breaker.yml的文件,内容如下所示:

  1. apiVersion: networking.istio.io/v1alpha3
  2. kind: DestinationRule
  3. metadata:
  4. name: rating
  5. spec:
  6. host: rating
  7. subsets:
  8. - labels:
  9. version: v1
  10. name: version-v1
  11. - labels:
  12. version: v2
  13. name: version-v2
  14. trafficPolicy:
  15. connectionPool:
  16. http:
  17. http1MaxPendingRequests: 3
  18. maxRequestsPerConnection: 3
  19. tcp:
  20. maxConnections: 3
  21. outlierDetection:
  22. baseEjectionTime: 3m
  23. consecutive5xxErrors: 1
  24. interval: 1s
  25. maxEjectionPercent: 100

我们要注意的第一件事情就是DestinationRule配置了断路器。除了配置断路器之外,子集也需要指定。对并发连接的限制是在connectionPool字段中实现的。

要配置断路器,我们需要使用outlierDetection。就本例而言,如果在一秒钟的时间窗口中发生了一次错误,断路器将会打开,使服务暂时跳闸(trip)三分钟。在这个时间之后,断路器会处于半开状态,这意味着会执行真实的逻辑。如果再次失败的话,断路器会保持打开的状态,否则的话,它将会关闭。

  1. kubectl apply -f src/main/kubernetes/destination-rule-circuit-breaker.yml
  2. destinationrule.networking.istio.io/rating configured

我们已经在Istio中配置完了断路器模式,接下来,我们再次执行siege命令并探查rating v2 v2的日志。

  1. siege -r 10 -c 4 -v -d 1 192.168.99.116:31304/book/1
  2. HTTP/1.1 200 0.04 secs: 39 bytes ==> GET /book/1
  3. HTTP/1.1 200 0.03 secs: 39 bytes ==> GET /book/1
  4. Transactions: 40 hits
  5. Availability: 100.00 %

再次探查日志。注意,在前面的运行中,我们已经到了Request 59。

  1. kubectl logs rating-service-v2-66b55746d-f4hpl -c rating-service -n default
  2. Request 56
  3. Request 57
  4. Request 58
  5. Request 59
  6. Request 60

_Rating v2 _只接收到了一个请求,因为在第一次处理请求的时候返回了错误,断路器就会打开,因此不会有更多的请求发送到rating v2上。

现在,我们已经看到了如何使用Istio实现回弹性。在这里,我们并没有在服务中实现相关的逻辑,将其与业务逻辑混在一起,而是让sidecar容器实现了这些逻辑。

最后,执行如下的命令,让rating v2服务回到之前的状态。

  1. kubectl exec -ti rating-service-v2-66b55746d-f4hpl -c rating-service curl localhost:8080/rate/behave
  2. Back to normal

认证

在实现微服务架构的时候,我们可能会发现的一个问题就是如何保护内部服务之间的通信。我们是不是要使用mTLS?是不是要对请求进行认证?是否要对请求进行鉴权?所有这些问题的答案都是肯定的!接下来,我们将会一步一步地看一下Istio是如何帮助我们实现这些功能的。

认证

Istio会自动将代理和工作负载之间的所有网络流量升级为mTLS,这个过程不需要修改任何的服务代码。与此同时,作为开发人员,我们会使用HTTP协议实现服务。当服务被“Istio化”的时候,服务之间的通信会采用HTTPS。Istio会负责管理证书,担任证书颁发机构并撤销/更新证书。

要校验mTLS是否已启用,我们可以使用istioctl工具执行如下的命令:

  1. istioctl experimental authz check book-service-5cc59cdcfd-5qhb2 -a
  2. LISTENER[FilterChain] HTTP ROUTE ALPN mTLS (MODE) AuthZ (RULES)
  3. ...
  4. 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的话,它必须是合法的、没有过期的、由正确的用户颁发的并且没有被篡改。

  1. apiVersion: "security.istio.io/v1beta1"
  2. kind: "RequestAuthentication"
  3. metadata:
  4. name: "bookjwt"
  5. namespace: default
  6. spec:
  7. selector:
  8. matchLabels:
  9. app.kubernetes.io/name: book-service
  10. jwtRules:
  11. - issuer: "testing@secure.istio.io"
  12. jwksUri: "https://gist.githubusercontent.com/lordofthejars/7dad589384612d7a6e18398ac0f10065/raw/ea0f8e7b729fb1df25d4dc60bf17dee409aad204/jwks.json"

其中的关键字段包括:

  1. kubectl apply -f src/main/kubernetes/request-authentication-jwt.yml -n default
  2. requestauthentication.security.istio.io/bookjwt created

我们现在使用一个非法的token来运行curl命令:

  1. curl 192.168.99.116:31304/book/1 -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkRIRmJwb0lVcXJZOHQyenBBMnFYZkNtcjVWTzVaRXI0UnpIVV8tZW52dlEiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjQ2ODU5ODk3MDAsImZvbyI6ImJhciIsImlhdCI6MTUzMjM4OTcwMCwiaXNzIjoidGVzdGluZ0BzZWN1cmUuaXN0aW8uaW8iLCJzdWIiOiJ0ZXN0aW5nQHNlY3VyZS5pc3Rpby5pbyJ9.CfNnxWP2tcnR9q0vxyxweaF3ovQYHYZl82hAUsn21bwQd9zP7c-LS9qd_vpdLG4Tn1A15NxfCjp5f7QNBUo-KC9PJqYpgGbaXhaGx7bEdFWjcwv3nZzvc7M__ZpaCERdwU7igUmJqYGBYQ51vr2njU9ZimyKkfDe3axcyiBZde7G6dabliUosJvvKOPcKIWPccCgefSj_GNfwIip3-SsFdlR7BtbVUcqR-yv-XOxJ3Uc1MI0tz3uMiiZcyPV7sNCU4KRnemRIMHVOfuvHsU60_GhGbiSFzgPTAa9WTltbnarTbxudb_YEOx12JiwYToeX0DCPb43W1tzIBxgm8NxUU"
  2. Jwt verification fails

因为token是非法的,所以请求会被拒绝,并返回HTTP/1.1 401 Unauthorized状态码。

使用合法的token重复前面的请求:

  1. curl 192.168.99.116:31304/book/1 -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkRIRmJwb0lVcXJZOHQyenBBMnFYZkNtcjVWTzVaRXI0UnpIVV8tZW52dlEiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjQ2ODU5ODk3MDAsImZvbyI6ImJhciIsImlhdCI6MTUzMjM4OTcwMCwiaXNzIjoidGVzdGluZ0BzZWN1cmUuaXN0aW8uaW8iLCJzdWIiOiJ0ZXN0aW5nQHNlY3VyZS5pc3Rpby5pbyJ9.CfNnxWP2tcnR9q0vxyxweaF3ovQYHYZl82hAUsn21bwQd9zP7c-LS9qd_vpdLG4Tn1A15NxfCjp5f7QNBUo-KC9PJqYpgGbaXhaGx7bEdFWjcwv3nZzvc7M__ZpaCERdwU7igUmJqYGBYQ51vr2njU9ZimyKkfDe3axcyiBZde7G6dabliUosJvvKOPcKIWPccCgefSj_GNfwIip3-SsFdlR7BtbVUcqR-yv-XOxJ3Uc1MI0tz3uMiiZcyPV7sNCU4KRnemRIMHVOfuvHsU60_GhGbiSFzgPTAa9WTltbnarTbxudb_YEOx12JiwYToeX0DCPb43W1tzIBxgm8NxUg"
  2. {"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的文件:

  1. apiVersion: security.istio.io/v1beta1
  2. kind: AuthorizationPolicy
  3. metadata:
  4. name: require-jwt
  5. namespace: default
  6. spec:
  7. selector:
  8. matchLabels:
  9. app.kubernetes.io/name: book-service
  10. action: ALLOW
  11. rules:
  12. - from:
  13. - source:
  14. requestPrincipals: ["testing@secure.istio.io/testing@secure.istio.io"]
  15. when:
  16. - key: request.auth.claims[role]
  17. values: ["customer"]
  1. kubectl apply -f src/main/kubernetes/authorization-policy-jwt.yml
  2. authorizationpolicy.security.istio.io/require-jwt created

然后执行和上面一样的curl命令:

  1. curl 192.168.99.116:31304/book/1 -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkRIRmJwb0lVcXJZOHQyenBBMnFYZkNtcjVWTzVaRXI0UnpIVV8tZW52dlEiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjQ2ODU5ODk3MDAsImZvbyI6ImJhciIsImlhdCI6MTUzMjM4OTcwMCwiaXNzIjoidGVzdGluZ0BzZWN1cmUuaXN0aW8uaW8iLCJzdWIiOiJ0ZXN0aW5nQHNlY3VyZS5pc3Rpby5pbyJ9.CfNnxWP2tcnR9q0vxyxweaF3ovQYHYZl82hAUsn21bwQd9zP7c-LS9qd_vpdLG4Tn1A15NxfCjp5f7QNBUo-KC9PJqYpgGbaXhaGx7bEdFWjcwv3nZzvc7M__ZpaCERdwU7igUmJqYGBYQ51vr2njU9ZimyKkfDe3axcyiBZde7G6dabliUosJvvKOPcKIWPccCgefSj_GNfwIip3-SsFdlR7BtbVUcqR-yv-XOxJ3Uc1MI0tz3uMiiZcyPV7sNCU4KRnemRIMHVOfuvHsU60_GhGbiSFzgPTAa9WTltbnarTbxudb_YEOx12JiwYToeX0DCPb43W1tzIBxgm8NxUg"
  2. 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:

  1. kubectl get pods -n istio-system
  2. NAME READY STATUS RESTARTS AGE
  3. grafana-b54bb57b9-k5qbm 1/1 Running 0 178m
  4. istio-egressgateway-68587b7b8b-vdr67 1/1 Running 0 178m
  5. istio-ingressgateway-55bdff67f-hlnqw 1/1 Running 0 178m
  6. istio-tracing-9dd6c4f7c-44xhk 1/1 Running 0 178m
  7. istiod-76bf8475c-xphgd 1/1 Running 7 177d
  8. kiali-d45468dc4-fl8j4 1/1 Running 0 178m
  9. prometheus-74d44d84db-zmkd7 2/2 Running 0 178m

监控

Istio集成了Prometheus,用于发送与网络流量和服务相关的各种信息。除此之外,它还提供了一个Grafana实例来可视化所有收集到的数据。

要访问Grafana,我们可以使用port-forward命令来将Pod暴露出来:

  1. kubectl port-forward -n istio-system grafana-b54bb57b9-k5qbm 3000:3000
  2. Forwarding from 127.0.0.1:3000 -> 3000
  3. Forwarding from [::1]:3000 -> 3000

打开浏览器并导航至locahost:3000以访问Grafana的仪表盘。

Kiali是另外一个在Istio中运行的工具,它能够管理Istio并观察服务网格参数,比如服务是如何连接的、它们是如何执行的以及Istio资源是如何注册的。

要访问Kiali,我们可以使用port-forward命令来将Pod暴露出来:

  1. kubectl port-forward -n istio-system kiali-d45468dc4-fl8j4 20001:20001
  2. Forwarding from 127.0.0.1:20001 -> 20001
  3. 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领域的动态。

查看英文原文:Implementing Microservicilites with Istio

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注