Istio-服务网格入门指南-全-
Istio 服务网格入门指南(全)
一、Kubernetes 之旅
Kubernetes 源于希腊语κυβερνήτης,意思是“总督”、“Helm 手”或“领航员”这就是创始人乔·贝达、布伦丹·伯恩斯和克雷格·麦克卢奇的想法。他们希望“驾驶一艘容器船”,从而创建一个容器编排平台,如今该平台已成为在云中运行微服务的事实标准。
在 2013 年末,IaaS 的声明式配置开始超过用于云基础设施的 bash 脚本。尽管像网飞这样的公司正在普及不可变的基础设施,但这也带来了重量级虚拟机映像的成本。Docker 通过提供轻量级容器成为了救星。与重量级虚拟机映像相比,它提供了一种在机器上打包、分发和部署应用的简单方法。但是在一台机器上运行 Docker 容器并不是扩展应用的解决方案,因为扩展应用需要在多台机器上部署 Docker 容器。这就产生了对管弦乐队的需求。
Kubernetes 的开发从关注 orchestrator 的关键特性开始,例如具有负载均衡和服务发现的应用复制,然后是基本的健康检查和修复特性以确保可用性。Kubernetes 还作为 Borg 的开源版本发布,Borg 是谷歌的一个大规模集群管理器,跨集群为不同的应用运行数十万个作业,每个集群有数万台机器。2015 年年中,Kubernetes 致力于 GitHub,并开放给开发者开始投稿。很快,像微软、Red Hat、IBM、Docker、Mesosphere、CoreOS 和 SaltStack 这样的大玩家加入了这个社区并开始做出贡献。随着时间的推移,在 Kubernetes 上开发了多个模块,确保了基本的 orchestrator 是完整的,并随着时间的推移得到了优化。
随着 Kubernetes 在开发人员社区中越来越受欢迎,开发人员开始使部署过程变得更加简单。Helm 是 Kubernetes 的一个包管理器,于 2016 年初推出,旨在简化如何定义、安装和升级复杂的 Kubernetes 应用。2016 年年中的某个时候,Minikube 发布了;Minikube 将 Kubernetes 环境带到了开发人员的本地系统中。我们将在本章后面的示例 Kubernetes 应用中使用 Minikube。Kubernetes 在生产中最受欢迎的应用之一是 PokemonGo。当时,这是 Kubernetes 在 Google Container Engine 上最大的部署之一。他们发布了一个案例研究,解释当应用的流量远远超出预期时,Kubernetes 如何帮助公司扩大规模。
后来,在 2017 年和 2018 年初,像 AWS 和 DigitalOcean 这样的云玩家在他们的栈上为 Kubernetes 腾出了空间。今天的 Kubernetes 是一个用于管理容器化应用的可移植、可扩展的开源平台。它有微型组件,负责 orchestrator 的基本功能。我们先来看看 K8s,这个单词 Kubernetes 的缩写,是由什么组成的。
K8s 架构/组件
Kubernetes 遵循客户端-服务器架构,其中主服务器安装在一台机器上,节点分布在多台机器上,可通过主服务器访问。图 1-1 显示了 Kubernetes 架构的构建模块。K8s master 和 K8s workers 是 Kubernetes 控制面板的一部分,而容器注册中心可能位于控制面板之外。
图 1-1
Kubernetes 体系结构概述
Kubernetes 大师
Kubernetes 主节点是负责管理整个集群的主要节点。K8s 工作者的编排由该节点处理。该节点是可复制的,以避免任何单点故障。控制面板访问主服务器只是为了对集群进行修改。母版由四个主要部分组成。
-
API 服务器:这是一个 Kubernetes 控制面板的前端。它维护 RESTful web 服务来定义和配置 Kubernetes 集群。
-
这是一个高度可用的组件,维护着系统中运行的所有对象的记录。Kubernetes 配置中的任何更改都存储在这里,并且允许查看这些更改以立即采取行动。
-
调度器(Scheduler):它以 pods 的形式调度 Kubernetes workers 上的工作负载。我们将在下一节介绍 pod。调度程序通读每个 pod 的资源要求,并根据可用性将 pod 分布在整个集群中。默认情况下,它还会尝试将 pod 副本分发到不同的节点,以保持高可用性。
-
控制器管理器:它在后台运行负责集群中不同重要任务的控制器。控制器监视 etcd 的配置变化,并将集群置于所需状态;另一方面,控制循环监视集群中的变化,并根据 etcd 努力维持所需的状态。让我们通过几个控制器示例来了解控制器在集群中的作用。
-
节点控制器:它监控集群中的节点,并在节点出现或出现故障时做出响应。这一点很重要,这样调度程序就可以根据节点的可用性调整 pod,并根据 etcd 维护状态。
-
端点控制器:它通过在 API 中创建端点记录来连接服务和 pod,并改变 DNS 配置以返回指向运行服务的 pod 之一的地址。
-
复制控制器:复制是维护应用高可用性的一种常规做法。复制控制器确保所需数量的 pod 副本/拷贝正在集群中运行。
-
在本章的后面,我们将会看到这些控制器的作用。此外,还有一个云控制器管理器,它允许云提供商通过使用插件轻松地与 Kubernetes 集成。
不可思议的工人
现在可能很清楚,实际的应用运行在 worker 节点上。早先这些人也被称为爪牙。术语的爪牙和的节点在一些文档中仍然可以互换使用。每个节点有三个主要组件。
-
Kubelet : Kubelet 是主节点代理,运行在每个节点上,监视节点上的容器是否运行正常。Kubelet 接受一组 PodSpecs,这是一个描述 pod 的 YAML 或 JSON 对象,并且只监视那些 Specs 中描述的容器。注意,除了 PodSpecs 中列出的容器之外,还可以有其他容器在节点上运行,但是 Kubelet 并不监控这些容器。
-
Kube-proxy:Kubernetes 主调度程序通常在一个节点上运行多个服务。Kube-proxy 为这些服务创建了一个网络代理和负载均衡器。它可以跨一组后端执行简单的 TCP、UDP 和 SCTP 流转发或循环 TCP、UDP 和 SCTP 转发。如果进行了配置,它还允许将节点暴露给互联网。
-
pod:pod 是 Kubernetes 对象模型的最小单元,可以被创建、部署或销毁。Kubernetes pod 通常只有一个容器,但也允许包含一组紧密耦合的容器。pod 表示集群上正在运行的进程。它有两种广泛的用途。
-
单容器 pod :这是最常见的 Kubernetes 用例,也称为每个 pod 一个容器。pod 包装容器,并向 Kubernetes 提供一个抽象层来访问或修改容器。
-
多容器容器(multi-container pod):有些情况下,应用需要共享资源的多个紧密耦合的容器。在这样的场景中,一个 pod 在这些容器上构建一个包装器,并将它们作为一个单一的服务对待。一个例子是一个为最终用户提供 REST APIs 的容器,一个侧柜计算实现 API 限制的请求数量。pod 内的容器共享分配给 pod 的相同 IP,并共享相同的存储集。在接下来的章节中,我们将会看到 Istio 的边车。
-
如前所述,部署在每个 pod 内部的容器运行服务。容器的打包和存储依赖于容器的运行时和注册。
-
容器运行时:为了理解这一点,让我们试着理解一下什么是容器。容器是一个代码单元,它与依赖项打包在一起,创建一个可以在不同的计算环境中快速运行的工件。容器运行时通过提供一组基本的资源和库来让用户运行容器,这些资源和库与容器的包结合起来启动应用。容器中的应用可以自由使用自己的环境,包括存储、网络等。,限制每种资源的使用量。容器运行时还管理节点上的容器映像。有多种容器运行时可用,所以让我们来看看其中的几种。
-
Rocket : Rocket,也称为 rkt ,是 coreOS 提供的容器运行时。Rkt 使用了一些与 Kubernetes 类似的术语。pod 是 Rkt 的核心执行单元。但是请注意,这种 POD 不同于 Kubernetes POD。Rocket 允许更细粒度的容器配置;换句话说,可以设置 pod 内部运行的应用的内存限制。Rocket 的容器遵循 app 容器规范,但也支持 Docker 映像。Rocket 带来的主要区别是它以无守护模式运行;启动的容器并不在守护进程的保护伞下运行,而是在基础机器上被赋予单独的进程 id。这允许它在同一个容器中运行多个进程,并在不终止父容器的情况下重启其中任何一个进程。
-
Docker 是目前最流行的容器运行时之一。如前所述,它提供轻量级容器的解决方案是需要编排的原因,这导致了对 Kubernetes 的需求。Docker 社区非常庞大,因为人们可以很容易地在注册表中获得任何作为 Docker 映像的常见包。
-
-
选择哪个容器运行时是个人喜好的问题,也取决于代码库的复杂程度和它所依赖的资源种类。使用 Rocket,您可以将文件描述符从一个进程传递到另一个进程,而文件描述仍然在监听。尽管这种场景并不常见,但在选择容器运行时之前,它们是需要考虑的重要场景。在本书中,我们将使用 Docker 作为我们的容器运行时。
-
容器注册:每一代容器都需要代码开发,从不同的包管理器添加库,并创建运行代码的基本环境。每次部署时都可以构建一个容器,但是每次获取最新的代码、获取新的库和准备环境都是非常耗时的。为了简化这一点,开发人员存储他们曾经创建的容器,并在需要时使用它。容器注册表是允许开发人员保存他们的容器映像并在需要时使用它们的地方。Azure、Docker 和 Google 等独立的提供商有自己的容器注册中心,这些注册中心在具有访问级别限制的高可用性环境中托管映像。
Kubernetes 使用容器运行时接口(CRI)与容器运行时进行交互。从 Kubernetes 1.5 开始,容器运行时被期望实现 CRI,它充当 Kubernetes Kubelet 和容器运行时之间的桥梁。CRI 提供了 Kubernetes 和容器运行时之间的抽象,使 Kubernetes 能够独立于容器运行时运行。
现在您已经理解了 Kubernetes 的架构,让我们试着理解 Kubernetes 中使用的几个重要术语。
库柏术语
有几个术语我们可能会在本书中经常用到。让我们来看一下其中的几个,以避免在将来的参考中出现任何混淆。
-
部署:部署是建立在 pod 上的抽象单元。要部署应用或微服务,需要在 pod 中运行它。为此,创建一个部署配置,其中规定需要部署什么以及应用的副本数量。在将这个配置提交给 Kubernetes 时,部署控制器会生成一组 pods,用配置好的副本部署应用。
-
映像:映像是将要部署在集群上的软件/容器。在本书中,我们将交替使用映像和映像。
-
Kubectl :这是一个与 Kubernetes 集群交互的 CLI。我们将使用它来部署集群,检查它们的状态,并更新我们的集群。
-
Namespace :顾名思义,这是用来对同一个 Kubernetes 实例上的多个虚拟集群进行分组,或者组织同一个集群内的资源。它允许唯一地标识每个资源。
-
Replicaset :这与复制控制器相同,它额外支持基于集合的选择器,而不是基于等式的选择器。这在本章后面的例子中会更清楚。
-
Service :这是对部署在一个或多个 pod 上的应用如何被内部或外部访问的描述。由于 pod 不是永久性的,Kubernetes 可能会根据可用性不时地重新部署 pod,因此不建议依赖直接访问 pod。该服务发现 pods 中运行的应用,并通过端口、负载均衡器或其他机制提供对它们的访问。
-
StatefulSet :这类似于管理 pod 的排序和唯一性的部署。换句话说,如果一个单元死了,StatefulSet 控制器会产生一个新的单元,它具有与死单元相同的身份和资源。
这些并不是本书中使用的所有术语,但是这个列表应该足以让我们开始创建我们的第一个 Kubernetes 集群。在此之前,我们需要设置 Kubernetes 环境。
建立一个坚不可摧的群集
如前所述,Minikube 是一个在本地运行 Kubernetes 集群的工具。因为它是本地的,所以它提供了一个单节点 Kubernetes 集群。Minikube 在虚拟机管理程序上启动自己的服务器。为简单起见,我们将使用 VirtualBox 作为管理程序,它可用于 Windows、Linux 和 macOS。
安装 VirtualBox
开始之前,请确保在系统 BIOS 中启用了 AMD-v 或 VT-x 虚拟化。这允许您在机器上运行 VirtualBox 实例。按照 https://www.virtualbox.org/wiki/Downloads
的步骤下载并安装 VirtualBox。安装完成后,让我们安装 Kubectl。
install kubectl 安装 kubectl
如前所述,Kubectl 是与 Kubernetes 集群交互的 CLI。在不同的平台上设置 Kubectl 会有所不同。让我们一个一个地看。
Linux 安装
Kubectl 的最新版本可以通过以下网址下载:
scurl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl
将下载的文件制作成可执行文件,并将其移动到您的PATH
中。
chmod +x ./kubectl
sudo mv ./kubectl /usr/local/bin/kubectl
macOS 安装
macOS 上的安装类似于 Linux 设置。
curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/darwin/amd64/kubectl
将下载的文件制作成可执行文件,并将其移动到您的PATH
中。
chmod +x ./kubectl
sudo mv ./kubectl /usr/local/bin/kubectl
Windows 安装
使用以下网址下载最新版本:
curl -LO https://storage.googleapis.com/kubernetes-release/release/v1.14.0/bin/windows/amd64/kubectl.exe
将二进制 EXE 文件添加到 Windows 中的PATH
中。
设置迷你库
在不同的操作系统上安装 Minikube 需要不同的步骤。
Linux 安装
Kubectl 的最新版本可以通过以下网址下载:
curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
将下载的文件制作成可执行文件,并将其移动到您的PATH
中。
chmod +x minikube
sudo mv ./minikube /usr/local/bin/minikube
macOS 安装
macOS 上的安装类似于 Linux 设置。
curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-darwin-amd64
将下载的文件制作成可执行文件,并将其移动到您的PATH
中。
chmod +x minikube
sudo mv ./minikube /usr/local/bin/minikube
Windows 安装
对于 Windows,最好使用包管理器来处理安装开销。一个流行的 Windows 包管理器是 Chocolatey ( https://chocolatey.org
)。它允许快速安装 Minikube。安装完 Chocolatey 后,以管理员身份运行choco
。
choco install minikube kubernetes-cli
就这样。
设置 Docker
在您的本地系统上安装 Docker 允许 Minikube 从您的本地系统访问映像。与 Minikube 类似,Docker 的设置对于不同的操作系统也有所不同。
Linux 安装
我们将使用 Ubuntu 库安装 Docker。其他 Linux 安装请访问 https://docs.docker.com/install/linux/docker-ce/centos/
。
-
更新本地存储库。
sudo apt-get update
-
安装依赖包。
> sudo apt-get install \ apt-transport-https \ ca-certificates \ curl \ gnupg-agent \ software-properties-common
-
添加 Docker 的 GPG 键。
> curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
-
将存储库添加到
apt
。> sudo add-apt-repository \ "deb [arch=amd64] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) \ stable"
-
更新本地存储库以提取 Docker 包。
> sudo apt-get update
-
使用以下命令安装 Docker:
> sudo apt-get update
-
安装
docker-ce
和cli
。> sudo apt-get install docker-ce docker-ce-cli containerd.io
macOS 安装
在 macOS 上安装的最简单方法是下载并安装 DMG 文件,可从以下网址获得:
https://hub.docker.com/editions/community/docker-ce-desktop-mac
Windows 安装
与 macOS 类似,Docker 提供了一个安装程序,可从以下网址获得:
https://hub.docker.com/editions/community/docker-ce-desktop-windows
我们将用 Java 和 Python 开发和部署一些应用。因此,我们将需要这两种语言的 SDK。
设置 Python
按照 https://www.python.org/downloads/
的步骤,可以轻松完成 Python 的设置。
设置 Java
与 Python 类似,按照 https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html
的步骤就可以轻松完成 Java 的设置。
我们的第一个 Kubernetes 集群
设置完成后,让我们启动第一个 Kubernetes 服务器。为了简单起见,我们将展示 Ubuntu 终端的输出。
> minikube start
Starting local Kubernetes v1.13.2 cluster...
Starting VM...
Getting VM IP address...
Moving files into cluster...
Setting up certs...
Connecting to cluster...
Setting up kubeconfig...
Stopping extra container runtimes...
Starting cluster components...
Verifying kubelet health ...
Verifying apiserver health ...
Kubectl is now configured to use the cluster.
Loading cached images from config file.
Everything looks great. Please enjoy minikube!
这将在 VirtualBox 上生成一个新的虚拟机,只需一些网络设置,允许基本系统访问它。Kubernetes 集群将被限制在这个虚拟机上。在开发和部署期间的任何时候,如果集群看起来很慢或者您的本地系统开始消耗超过预期的资源,您可以使用以下命令关闭虚拟机:
> minikube stop
此外,您可以从 VirtualBox UI 调整该虚拟机使用的资源。请注意,就本书的范围而言,建议允许该虚拟机至少使用两个 CPU 内核和 4gb RAM。
您可以通过运行以下命令来查看 Kubernetes 集群的简介:
> minikube dashboard
Enabling dashboard ...
Verifying dashboard health ...
Launching proxy ...
Verifying proxy health ...
Opening http://127.0.0.1:58969/api/v1/namespaces/kube-system/services/http:kubernetes-dashboard:/proxy/ in your default browser...
图 1-2 显示了 Minikube 仪表盘。如果您能够看到控制面板,那么您的集群已经启动并运行,可以部署应用了。
图 1-2
Minikube 仪表板
在 Kubernetes 上运行应用
我们已经准备好了 Kubernetes 集群,所以让我们尝试在其上部署一个应用,并了解它是如何发生的。
申请详情
让我们从使用 Flask 在 Python 中创建一个简单的 web 应用开始。pip
是 Python 的一个包管理器。它可以通过以下方式安装:
> curl https://bootstrap.pypa.io/get-pip.py | python
一旦pip
安装好,烧瓶就可以这样安装:
> pip install flask
让我们创建一个名为WebApp
的项目,里面有app.py
来处理 web 请求。应用结构应该是这样的:
.
|____WebApp
| |____app.py
| |____requirement.txt
| |____Dockerfile
编辑app.py
创建一个简单的监听器。文件见清单 1-1 。
from flask import Flask
app = Flask(__name__)
@app.route("/")
def main():
return "Welcome!"
if __name__ == "__main__":
app.run(host='0.0.0.0')
Listing 1-1Web Requests Handler: app.py
让我们创建一个 Dockerfile 文件来封装应用。清单 1-2 解释了容器的创建。
FROM ubuntu:18.04
RUN apt-get update -y && apt-get install -y python-pip python-dev
COPY ./requirement.txt /app/requirement.txt
WORKDIR /app
RUN pip install -r requirement.txt
COPY . /app
ENTRYPOINT [ "python" ]
CMD [ "app.py" ]
Listing 1-2Dockerfile to Containerize the Application
Minikube 的 Docker 环境和存储的映像是不同的。我们将把容器映像直接存储在 Minikube 实例上,而不是将映像存储到我们的本地环境中,发送到注册表,然后再将它们放回 Minikube 上。
> eval $(minikube docker-env)
构建名为web-app
的WebApp
应用容器,并分配版本 1.0。
> docker build -t web-app:1.0 .
图 1-3 显示了创建的新容器映像。
图 1-3
创建 web 应用后,Minikube 服务器上的映像可用
部署应用
让我们创建我们的第一个部署配置。这告诉 Kubernetes 为我们的应用创建一个容器。清单 1-3 显示了我们的 webapp 的webapp-deployment.yaml
文件。
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: webapp-deployment
labels:
app: webapp
spec:
replicas: 1
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
containers:
- name: webapp
image: web-app:1.0
imagePullPolicy: Never
ports:
- containerPort: 5000
Listing 1-3webapp-deployment.yaml File for Web App
让我们试着理解 YAML 文件。
-
apiVersion
:这是用于创建该对象的 API 版本。 -
这说明我们正在创建一个 Kubernetes 部署。
-
metadata
:这指定了部署的名称(一个必需的键)和可能想要放在部署上的可选标签。 -
replicas
:指定要为此部署创建的单元数量。 -
这就是部署如何设法定位 pod 的方法。
-
template.metadata
:从该部署创建的 pod 将从这些标签中命名。 -
containers
:指定需要在这个 pod 中部署的容器。在我们的例子中,我们使用在上一节中创建的映像部署一个容器。由于我们在上一节中确保了该映像对于 Kubernetes 集群是可用的,所以我们没有将该映像上传到任何注册表,因此imagePullPolicy
被设置为Never
。 -
ports
:这是暴露给集群的容器的端口。
让我们使用以下代码在集群上部署应用:
> kubectl apply -f webapp-deployment.yaml
前面的命令在 Kubernetes 集群上应用了 YAML 定义的配置。换句话说,它创建了一个名为webapp-deployment
的部署。图 1-4 显示了集群上的所有部署及其状态以及运行和活动副本的数量。
图 1-4
在 Kubernetes 集群上运行的部署
该图显示有一个 pod 正在运行WebApp
应用。部署产生了一个复制集,它试图保持一个 pod 一直运行的状态。图 1-5 和图 1-6 显示了集群上正在运行的副本集和 pod。
图 1-6
在集群上运行的 pod
图 1-5
副本集从部署开始
pod 详细信息包括 IP 地址。内部网络可以使用该 IP 地址访问 pod,但是如前所述,不鼓励通过 IP 地址直接访问 pod,因为 pod 是消耗品,新的 pod 可能具有不同的 IP 地址。从 IP 地址可以清楚地看出,虽然可以通过 Kubernetes 网络内部的 IP 地址访问 pod,但是可能无法从主机访问它。Kubectl 提供了一种为 pod 使用代理并从主机访问应用的方法。
> kubectl port-forward webapp-deployment-7946f7db77-gtsbg 5000:5000
webapp-deployment-7946f7db77-gtsbg
是我们的 pod 名称(参见图 1-6 ),5000 是 pod 上暴露的用于访问应用的端口。图 1-7 显示端口转发的输出。
图 1-7
端口将主机端口转发到 pod 端口
现在可以从主机上访问应用了。图 1-8 显示了在主机浏览器上运行的应用。
图 1-8
转发到主机并可在浏览器中访问的应用端口
可以使用以下命令从 pod 中访问应用日志:
> kubectl log -f webapp-deployment-7946f7db77-gtsbg 5000:5000
图 1-9 显示了 pod 日志的输出。
图 1-9
通过注销可以看到 Pod 日志
Kubernetes 的服务,Kubernetes 的服务,Kubernetes 的服务,Kubernetes 的服务,Kubernetes 的服务,Kubernetes 的服务,Kubernetes 的服务,Kubernetes 的服务
库伯内特 POD 是可牺牲的。ReplicaSet 在放大和缩小的过程中创建和破坏 POD;因此,通过 IP 地址访问 pod 不是一个可靠的解决方案。那 Kubernetes 内部的微服务是怎么和其他微服务沟通的呢?答案是 Kubernetes 服务。让我们试着理解服务的概念。
Kubernetes 服务提供了一个虚拟的基于 IP 的桥梁来访问 pod。用户可以访问单个 pod,也可以同时访问一组 pod。可以有两种类型的交互。
-
Pods 访问服务
-
公开的服务
在解释这一点之前,让我们通过服务公开我们的 web 应用。清单 1-4 显示了一个简单的服务,选择器指向我们的 webapp。
apiVersion: v1
kind: Service
metadata:
name: webservice
spec:
selector:
app: webapp
ports:
- protocol: TCP
port: 80
targetPort: 5000
Listing 1-4webapp-service.yaml File for Our Web App
该服务被命名为webservice
,并通过选择器app:webapp
指向部署。该服务在端口 80 上公开,并将请求代理到结果窗格的端口 5000。使用以下方式应用服务:
> kubectl apply -f webapp-service.yaml
使用以下命令验证服务是否已成功创建:
> kubectl describe service webservice
图 1-10 显示了创建的服务的描述。
图 1-10
使用选择器应用创建的指向窗格的服务:webapp
该服务被分配一个群集 IP 地址 10.107.243.100。集群中的任何微服务都可以通过端口 80 使用这个 IP 地址访问服务。
现在,让我们试着理解 Kubernetes 集群中可能存在的两种服务交互。
Pods 访问服务
任何微服务架构都需要一个服务来访问私有网络内的多个微服务。通过其他服务的 IP 地址或通过 DNS 请求,可以访问其他服务。Kubernetes 支持他们两个。
图 1-11
定义了服务端点的环境变量
-
DNS :虽然这不是默认设置,但它是可选的,但推荐 Kubernetes 使用。顾名思义,每个服务一创建就为自己注册一个 DNS 记录。DNS 记录遵循模式 <服务名>。<命名空间> 。同一名称空间内的任何 pod 都可以通过 <服务名> 直接访问服务,而名称空间外的 pod 必须包含。<命名空间> 来访问服务。
-
环境变量:当一个 pod 在一个节点中启动时,Kubectl 声明所有正在运行的服务作为 pod 的环境变量被访问。但这迫使人们遵循一个顺序;如果新服务是在第一个服务启动后定义的,则第一个服务不能访问新服务。尝试登录到
webapp
pod 的 Docker 容器并检查环境变量。新服务不可见。如果开发人员删除现有部署并重新创建部署,则服务在环境变量中可见。图 1-11 显示了第二种情况的环境变量。
公开的服务
向外部世界公开服务有多种方式。Kubernetes 提供了多种方法来实现这一点。
ClusterIP(群集 IP)
这允许通过集群的内部 IP 公开服务。如前所述,集群的内部 IP 地址是公开的,可以被集群内部的 pod 访问。
节点端口
这允许在特定端口的节点 IP 地址上公开服务。这允许通过 <节点 IP > : <端口> 地址访问服务。Kubernetes 在内部创建一个 ClusterIP 服务,作为节点 IP 和实际服务之间的连接。端口号可以在 30000 和 32767 之间。每个节点将选定的端口代理到服务箱。
LoadBalancer(负载均衡器)
这将在节点端口上创建一个公共 IP。因此,可以通过公共 IP 访问该服务,该服务被路由到 NodePort,然后被进一步路由到 ClusterIP。它的实现因云提供商而异。对配置的一个小的添加创建了一个LoadBalancer
类型。清单 1-5 展示了在服务中添加一个LoadBalancer
类型。
apiVersion: v1
kind: Service
metadata:
name: webservice
spec:
type: LoadBalancer
selector:
app: webapp
ports:
- protocol: TCP
port: 80
targetPort: 5000
Listing 1-5webapp-service-loadbalancer.yaml File for Our Web App
使用此配置创建服务后,开发人员可以使用以下命令检查服务的外部 IP 地址:
> kubectl get service webservice
图 1-12 显示了我们的例子。我们没有获得外部 IP 地址,因为我们是在 Minikube 上运行我们的应用。在云上,外部 IP 由一个值填充。
图 1-12
已部署负载均衡器类型服务
外部名
这只是使用 CNAME 记录将服务映射到一个地址。这些通常在从集群内部使用外部服务并抽象出外部服务的实际链接时使用。清单 1-6 显示了一个类型为ExternalName
的简单服务。
apiVersion: v1
kind: Service
metadata:
name: db1
spec:
type: ExternalName
externalName: mysql01.database.test.com
Listing 1-6database-external-name.yaml Showing ExternalName Configuration
当内部 pod 寻找服务db1
时,它们接收到一个mysql01.database.text.com
的 CNAME 记录。不涉及转发;在 DNS 级别只发生一次重定向。
ExternalName
还允许开发者向服务添加自定义 IP 地址,客户端可以通过该地址访问服务。IP 分配是集群管理器的唯一职责;它不是来自库伯内特斯。清单 1-7 展示了一个服务的外部 IP 分配的例子。
apiVersion: v1
kind: Service
metadata:
name: externalIpAssignedService
spec:
selector:
app: externalIpService
ports:
- name: http
protocol: TCP
port: 80
targetPort: 9000
externalIPs:
- 70.34.1.23
Listing 1-7External IP Assigned to a Service
如前所述,Kubernetes 是一个自修复的平台。让我们试着在集群上玩一玩,看看 Kubernetes 服务在其中的作用。
Kubernetes 是自愈的
在任何应用中,很难保证单个节点 100%的正常运行时间或可用性。Kubernetes 提供了一种创建服务副本的方法,并确保副本的数量保持不变。让我们修改我们的部署并增加副本的数量。
> kubectl scale --replicas=2 deployment webapp-deployment
> kubectl get deployments
图 1-13 显示了展开的结果
图 1-13
增加副本后的部署状态
> kubectl get pods
图 1-14 显示了运行吊舱。
图 1-14
增加副本后的部署状态
如果有人试图杀死任何一个 pod,复制控制器会尝试恢复状态并产生一个新的 pod。让我们试着终止其中一个 pod 来查看应用的状态。图 1-15 显示了一个 pod 的删除。
图 1-15
从集群中强制删除一个 pod
图 1-16 显示了一个新的 pod 如何自动衍生以匹配复制号。
图 1-16
删除的 pod 终止时,会产生新的 pod
通过这一点,Kubernetes 试图保持服务在任何时候都可用。
添加微服务
现在你已经看到了如何在 Kubernetes 上部署和运行一个微服务,也看到了微服务如何相互交互的理论。让我们创建一个新的微服务,它使用来自 webapp 的响应并将其呈现给 UI。我们姑且称这个 app 为istio-frontend
。我们已经创建了一个 Docker 文件。
应用设置
istio-frontend
是一个 Java 应用,它向webapp
服务发出请求,并用接收到的数据填充它的 web 页面。如果没有收到数据或者web-app
服务不可用,它填充ERROR RECEIVED
作为响应。我们已经创建了一个标签为frontend-app:1.0
的 Docker 文件。让我们遵循与前面的应用相同的方法,为应用创建一个部署和服务。清单 1-8 和清单 1-9 显示了部署和服务文件。
apiVersion: v1
kind: Service
metadata:
name: frontendservice
spec:
selector:
app: frontend
ports:
- protocol: TCP
port: 80
targetPort: 8080
Listing 1-9frontend-service.yaml Configuration
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend-deployment
labels:
app: frontend
spec:
replicas: 1
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: frontend
image: frontend:1.0
imagePullPolicy: Never
ports:
- containerPort: 8080
Listing 1-8frontend-deployment.yaml Configuration
图 1-17 显示了可用的新服务。让我们尝试代理新服务,让应用运行起来。
图 1-17
新的前端服务可用
图 1-18 显示了输出页面。
图 1-18
代理端口到前端 pod 的输出
将webapp
服务的副本计数减少到 0 给出了如图 1-19 和图 1-20 所示的状态。
图 1-20
如果无法与后端服务通信,前端会显示错误
图 1-19
webapp 的副本减少到 0 个
发布和部署
在大型组织中,任何投入生产的应用都需要定期开发和维护。随着像敏捷这样的新方法的出现,发布频率已经增加到一天发布多个版本,发布回滚也是如此。关闭应用、重新部署和重新启动的传统过程会导致停机。在 99.99%可用性的世界里,停机时间的范围意味着在七天的时间里不超过一分钟,所以一周一次的发布违反了敏捷方法。
为了最大限度地减少停机时间,使用了多种部署技术,例如蓝绿色、淡黄色和滚动部署。我们将在后面的章节中讨论这些技术。默认情况下,Kubernetes 遵循滚动部署。换句话说,它创建了两个完全相同的环境,一旦新环境启动,流量将被路由到新环境,而旧环境将被终止。
让我们将我们的 webapp 升级到 2.0,看看 Kubernetes 上的部署是如何运行的。清单 1-10 显示了文件中的变化。我们将简单地在欢迎消息中添加时间。
from flask import Flask
import datetime
app = Flask(__name__)
@app.route("/")
def main():
currentDT = datetime.datetime.now()
return "Welcome user! current time is " + str(currentDT)
if __name__ == "__main__":
app.run(host='0.0.0.0')
Listing 1-10Updated Web Requests Handler: app.py
按照前面所述的相同过程创建一个新容器。清单 1-11 显示了修改后的部署文件和升级后的容器细节。
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp-deployment
labels:
app: webapp
spec:
replicas: 1
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
containers:
- name: webapp
image: web-app:2.0
imagePullPolicy: Never
ports:
- containerPort: 5000
Listing 1-11Updated webapp-deployment-v2.yaml for webapp 2.0
让我们使用以下代码在集群上部署应用:
> kubectl apply -f webapp-deployment-v2.yaml
一个新的圆荚体产生了,一旦新的圆荚体准备好了,先前的圆荚体就终止了。图 1-21 显示了新部署的应用的输出。
图 1-21
前端显示来自后端服务的新响应
在后台发生的是一个新的环境,有一台机器和 2.0 版本,而webapp
服务仍然指向旧的环境。一旦新产生的 pods 返回了running
状态,webapp
服务将流量指向新的环境,早期的 pods 被终止。
这里有一个问题:当一个新的 pod 产生了,但是里面的应用还在部署,还没有启动,会发生什么呢?此时,pod 返回一个运行状态,但是应用仍然关闭,同时服务开始将流量定向到新环境。这增加了服务的停机时间,直到应用启动并运行。为了解决这个问题,Kubernetes 使用了一个就绪探测器。
就绪探测
用新部署更新部署会导致停机,因为旧部署会被新部署取代。如果由于某种原因,新部署配置错误或有一些错误,停机时间将继续,直到检测到错误。当使用就绪探测时,服务不会将流量转发到新的 pod,直到探测成功。它还确保在新的部署单元准备好之前旧的单元不会被终止。这确保了出现错误的部署根本不会收到任何流量。
为了整合就绪性调查,我们需要向我们的 webapp 添加一个健康链接。清单 1-12 显示了app.py
代码的变化。添加了一个/health
链接,该链接将在应用启动并运行后可用。代码中添加了 60 秒的延迟,这将有助于演示 Kubernates 的这种行为。
from flask import Flask
import datetime
import time
time.sleep(60)
app = Flask(__name__)
@app.route("/")
def main():
currentDT = datetime.datetime.now()
return "Welcome user! current time in v3 is " + str(currentDT)
@app.route("/health")
def health():
return "OK"
if __name__ == "__main__":
app.run(host='0.0.0.0')
Listing 1-12Addition of Health Link to app.py
创建一个带有标签web-app:3.0
的新容器,并将其添加到部署文件中,如清单 1-13 所示。
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp-deployment
labels:
app: webapp
spec:
replicas: 1
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
containers:
- name: webapp
image: web-app:3.0
imagePullPolicy: Never
ports:
- containerPort: 5000
readinessProbe:
httpGet:
path: /health
port: 5000
initialDelaySeconds: 40
Listing 1-13Updated webapp-deployment-v3.yaml for web-app 3.0
就绪探测器初始化的初始延迟为 40 秒。如果已经知道应用部署需要一些时间,可以在initialDelaySeconds
中说明,以避免对应用进行不必要的检查。在最初的延迟之后,Kubelet 会定期检查/health
链路,当链路启动时,pod 会进入就绪状态以接受流量。图 1-22 显示了不同时间的展开状态。
图 1-22
带就绪状态检查的部署
我们来看看后台发生了什么。
-
已检查可用的部署。A
frontend-deployment
和 awebapp-deployment
正在工作,每个都有一个可用的 pod 处于就绪状态。 -
应用了新的版本 3 配置。
-
准备舱的数量保持不变。
-
在获取 pod 的详细信息时,我们可以看到两个
webapp-deployment
pod。旧的已经准备好了,最新的是运行但是仍然没有准备好接受流量。 -
在 40 秒时,Kubernetes 没有触发对准备就绪探测器的请求;因此,pod 仍处于就绪-待定状态。默认情况下,运行状况检查每 10 秒进行一次。
-
部署 60 秒后,新的吊舱升级到就绪状态,旧的吊舱转移到终止状态。
这确保了在新部署准备就绪之前,先前的部署不会被丢弃,并且流量会被路由到较旧的部署。这在升级应用或部署新应用时很有帮助。但是在部署完成并且旧的部署单元终止之后,这就没有用了。如果在此之后,部署单元由于已知/未知的原因而失败,则就绪性探测失败,并且流量不会被发送到单元。一方面,这确保了应用不会停机,但是可用于服务流量的 pod 数量会减少。一种极端情况是,部署中的所有单元都出现了同样的问题;你的完整申请可能会被否决。
没有理想的方法来处理这样的问题,但是 Kubernetes 提供了一个常见的解决方案,如果应用变得不负责任,就重启应用。活跃度探测与就绪性探测类似,对应用进行检查,如果应用停止响应,它会重新启动 pod。
让我们在应用中做一个小小的改动,在 60 秒内终止应用,并观察活跃度探测器的行为。清单 1-14 显示了变化。
from flask import Flask
import datetime
import time
import threading
import os
time.sleep(60)
app = Flask(__name__)
@app.route("/")
def main():
currentDT = datetime.datetime.now()
return "Welcome user! current time is " + str(currentDT)
@app.route("/health")
def health():
return "OK"
def exit_after():
time.sleep(60)
os._exit(1)
exit_thread = threading.Thread(target=exit_after)
exit_thread.start()
if __name__ == "__main__":
app.run(host='0.0.0.0')
Listing 1-14Autostopping
the Application After Some Time in app.py
创建一个标签为web-app:4.0
的新容器,并将其添加到部署文件中,如清单 1-15 所示。
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp-deployment
labels:
app: webapp
spec:
replicas: 1
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
containers:
- name: webapp
image: web-app:4.0
imagePullPolicy: Never
ports:
- containerPort: 5000
readinessProbe:
httpGet:
path: /health
port: 5000
initialDelaySeconds: 40
livenessProbe:
httpGet:
path: /health
port: 5000
initialDelaySeconds: 120
Listing 1-15Updated webapp-deployment-v4.yaml for web-app 4.0
活性探测器初始化有 120 秒的延迟。因为我们已经知道应用的启动时间需要 60 秒,所以在应用启动之前重启它是没有用的。遵循与redinessProbe
相同的过程来检查应用的健康状况。让我们看看图 1-23 中的动作变化。
图 1-23
准备就绪和活动状态检查部署
假设我们的应用在部署后的某个时候失败了,Kubernetes 尝试这样恢复它:
-
当应用停止运行时,就绪探测会失败。
-
Kubernetes 停止了那个 pod 上的通信,并将自己限制在其余的复制品上。在我们的例子中,由于我们只有一个副本,应用必然会停机。
-
活性探测器停止工作,因为它位于同一健康链路上。
-
Kubernetes 尝试重启 pod 并恢复应用状态。
-
重启后,应用启动,就绪探测成功。
-
这个舱的通信恢复了。
摘要
在这一章中,我们回顾了 Kubernetes 的简史。现在,您已经了解了它的基本组件,并学习了其中使用的术语。我们用 Minikube 和 Docker 容器运行时在本地设置 Kubernetes。我们还创建了一个示例应用,并展示了集群中的应用部署以及 Kubernetes 应用如何自修复。
在下一章中,我们将介绍微服务架构、其面临的挑战,以及如何使用服务网格解决这些挑战。
二、服务网格简介
在前一章中,我们快速浏览了 Kubernetes,容器编排引擎。有效利用这样的基础设施需要采用基于微服务的应用架构。但是这种类型的体系结构带来了一系列新的开发和操作挑战。在本章中,我们将讨论服务网格如何帮助减轻这些挑战。
微服务架构
整体架构是传统上使用的开发模型。在整体架构中,完整的软件是一个统一的、自包含的应用。应用具有离散的子系统/功能,这些子系统/功能与应用的其余部分紧密耦合。对应用的一部分进行更改需要发布完整的软件。见图 2-1 。
图 2-1
整体建筑
这种系统的一个例子是负责以下功能的交易应用:
-
使用桌面客户端的交易捕捉系统。这些客户端也被称为应用视图。可以有使交易者能够使用基于浏览器的用户界面的网络视图。该系统负责运行一些验证,然后在系统中预订交易。这些验证使用规则引擎来表达和执行。
-
一个移动应用和一个基于浏览器的交易记事本。记事本显示当前已登记的交易。此外,应用可以有一个移动视图,用于查看可用的报告。
-
浏览器中显示的交易风险分析报告。风险子系统显示交易记账后近乎实时的风险敞口。
-
一种转账系统,用于将资金从系统中的一个账户转移到另一个账户。
-
一种资产分配系统,用于在当前可用的不同投资工具之间分配资金。
-
用于定义应用使用哪些金融工具的工具和市场黄金来源(称为参考数据)。这有助于直接选择具有别名的工具/市场,而不是每次都提供完整的细节。
除了这一组主要功能之外,还有其他功能,如风险警报、图书验证等。所有这些功能都作为一个软件应用进行编译和部署。
monolith 应用从小型开始。因为完整的应用具有很高的内聚性,所以在早期阶段,一切都可以快速开发。完整的应用更容易部署和测试,因为它是一个单一的单元。
但是随着应用的有机增长,维护和操作变得更加困难。随着时间的推移,这样的应用通常由多个开发团队维护。这些团队负责应用的每个子系统。这些子系统高度耦合,经过一段时间后,开发团队会相互依赖以提供新的功能。但是由于只有一个单位,每个团队都面临以下一系列问题:
-
快速特性推出:团队是相互依赖的,所以他们很少能独立发布特性。通常这样的应用发布是一个大的协同工作,一个季度只能做几次。
-
弃用的栈:由于技术被广泛使用,一个团队很少能独立地改变/更新一个子系统。它通常需要完全重写多个子系统,这对业务来说是有风险的。
-
陡峭的学习曲线:由于整个系统是紧密耦合的,没有一个开发人员可以在不了解其他子系统的情况下工作。这导致新开发人员的学习曲线很陡。通常这样的应用有一个大的代码库,而开发者只对特定的子集感兴趣。这导致整个开发周期缓慢。
-
缩放问题:monolith 应用只能垂直缩放。如果一个特定的子系统上有大量请求的负载,就不可能部署更多的子系统单元或者只扩展特定的子系统。对于 monolith 应用来说,这是一个要么全有要么全无的场景,而且这通常是一件非常昂贵的事情。
通常,这些挑战要求组织考虑其他模式。微服务架构是整体架构的替代方案。这通常需要将应用分解成各种独立的、松散耦合的单元。目标是拥有独立的业务服务,具有定义良好的接口和操作。这些服务中的每一个都有自己独立的上下文。这些服务可以与其他服务交互来执行所需的任务。回头看看我们的贸易处理应用,可以为风险计算、贸易转移和黄金数据源构建单独的服务。此外,还可以有报告服务,这些服务使用来自每个基本服务的数据来提供正确的报告。可以使用另一组服务将报告呈现给 Web 和移动设备。因此,完整的整体可以分解成不同的部件,如图 2-2 所示。
图 2-2
微服务架构
采用这样的架构不仅需要软件设计的改变,还需要组织交互的改变。团队从这样的应用设计中获得了以下好处。
灵活
敏捷性是组织采用微服务架构的最大驱动因素之一。该架构提供的松散耦合允许加速开发。该模型指示小型独立开发团队在其定义的边界内工作。每个团队都可以在各自的环境中完全自由地工作。这加快了开发速度。
该架构还提高了弹性和故障隔离。在出现故障的情况下,故障可以被定位到一个特定的服务。因此,系统的其余部分可以正常运行,从而延长了正常运行时间。特定的服务可以独立地固定和部署,从而获得更好的服务质量。
创新ˌ革新
微服务架构促进小型独立开发团队在其服务边界内完全自主地工作。这些团队负责保持他们的系统始终正常运行。这导致了组织内的服务所有权文化。
开发团队通常很清楚他们的服务缺点。服务所有权导致关于解决服务问题的自主决策。他们可以修复问题,并根据他们的服务里程碑部署改进的服务。他们完全有权为此选择适当的工具和框架。这最终导致组织的技术栈和创新得到改进。
可量测性
在整体系统中,随着负载的增加,并不是所有的子系统都成比例地增加流量。通常情况下,系统的某些部分会获得更多流量,从而影响服务性能。通常它是控制整个系统性能的服务的子集。因此,整个系统的负载增长在其所有子系统中并不是线性的。但是,当一个整体系统处于负载状态时,可以通过添加更多的硬件来扩展系统。这通常会导致额外硬件的利用不足,因为我们需要为完整的软件添加更多的硬件,而不仅仅是为负载下的子系统。
微服务提供的分离使组织能够了解每个微服务所服务的流量。开发人员可以根据需求做出独立于服务的决策,并提高效率。他们可以采用适当的编程语言和框架,用尽可能好的配置进行微调,并部署到生产中。此外,在加载时,是按需扩展服务的底层硬件,而不是扩展整个生态系统。
可维护性
如前所述,使用 monolith 软件有一个陡峭的学习曲线。通常有一些部分是当前团队中没有人理解的,团队在处理这些部分时会感到不舒服。这通常被称为技术债务。解决整体架构中的技术债务是困难的,因为人们可能害怕破坏其中一个工作特性。曾经有过不需要的死代码通过解决特定整体架构中的技术债务而复活的例子。
微服务架构遵循分而治之的原则,有助于缓解这一问题。好处可以与面向对象的应用设计联系起来,在面向对象的应用设计中,系统被分解成对象。每个对象都有一个定义好的契约,可以改善整个系统的维护。开发人员可以对每个被重构的对象进行单元测试,以验证其正确性。类似地,围绕业务上下文创建的微服务也有定义好的契约。开发人员可以在验证服务契约的同时解决服务的技术债务。
挑战
微服务解决了整体架构的一些现有问题,但也带来了新的挑战。意识到这些挑战是很重要的。微服务将单个系统分解成分布式系统。分布式架构需要仔细建模。必须注意各种故障点,微服务也不例外。在 20 世纪 90 年代末,詹姆斯·高斯林编制了一个错误假设的列表,这些假设会使分布式系统效率低下、不安全、更难维护。让我们在微服务架构的背景下讨论它们。
网络是可靠的
根据墨菲定律,“任何可能出错的事情都会出错。”
在分布式体系结构中,有许多移动的部分,其中一个服务随时都有可能失败。故障可能是由软件问题、硬件故障(如网络交换机)或 DNS 问题引起的。这将导致消费者服务的交付失败,并可能导致系统的陈旧状态。作为软件设计师,我们需要通过在系统中放置足够的冗余来准备处理这样的场景。
具体来说,对于云原生微服务,Kubernetes 为这一挑战提供了开箱即用的解决方案。我们通常使用复制因子将服务部署到 Kubernetes 集群。Kubernetes 确保以指定的冗余部署服务,并且在出现故障的情况下,所有服务调用都被路由到部署在集群中的辅助服务。与此同时,Kubernetes 试图引入一个新的实例,并为上述服务维护复制因子。让我们看一个示例 Kubernetes 配置。
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: spring-gs
spec:
replicas: 1
template:
metadata:
labels:
app: spring-gs
version: v0.1.0
spec:
containers:
- name: spring-gs
image: k8s-sample /spring-gs:0.1.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
---
在之前的配置中,我们已经指定了replicas : 1
。这将确保 Kubernetes 部署至少两个服务实例。这种机制是用于从应用崩溃中恢复的服务器端解决方案。在客户端,我们需要添加一个重试机制来处理网络故障。在客户端完全丢弃请求之前,请求必须发送几次。这样,应用将能够处理瞬时故障。这只是一个例子;作为微服务架构采纳者,我们需要在构建服务时添加所需的弹性模式。
延迟为零
如今,每种语言都为 web 服务/远程调用生成存根。生成的客户端屏蔽了调用函数调用所需的样板逻辑。但与此同时,开发人员有时会认为客户端调用与本地服务调用是同等的。这不是真的;网络电话比本地电话慢得多。如果处理不当,它会阻塞应用很长时间。服务中断是很少发生的极端情况。更常见的情况是,由于负载,服务响应缓慢。测试服务响应延迟非常困难。在一个单元测试案例中,大多数外部依赖都被嘲笑;因此,为此构建一个测试需要一定程度的开发人员成熟度。在没有足够负载复制的验收环境中,这是非常困难的。
Michael Nygard 提出的断路器模式旨在处理这些场景。该模式就是将远程函数调用封装在一个跟踪失败的对象中。如果有更多的失败,那么服务不再调用外部调用;相反,它会立即返回一个错误代码。断路器在以下三种状态下跟踪连接(见图 2-3 ):
图 2-3
断路器状态
-
关闭:这是外部服务调用工作正常,没有任何故障报告的状态。断路器记录故障。这使得断路器对服务性能波动更有弹性。但是当故障数量超过阈值时,断路器跳闸,进入断开状态。
-
打开:在这种状态下,断路器返回错误,不调用外部调用。
-
半开:经过配置的时间间隔后,断路器进入半开状态,验证外部调用是否仍然失败。如果在这种半开状态下呼叫失败,断路器将再次跳闸。如果成功,断路器复位到正常的闭合状态。
有效的断路器策略对于发布现有服务的新版本也是必不可少的。一次性升级整个系统是徒劳的。往往会导致大量的开销。相反,对现有服务进行定期滚动更新要有效得多,但这只有在相关服务以有效的方式处理故障时才有可能。采用微服务架构的团队需要接受停机,并且不能将其视为极端情况。如果得不到解决,这些故障会在整个生态系统中产生连锁反应。这将导致大量不必要的服务高峰。
缓存应用数据是我们处理故障的另一种方式。这适用于我们可以操作陈旧数据的情况。应用可以缓存它们收到的最后一个响应。下一次调用可以获取新数据,也可以提供旧的缓存数据。
带宽是无限的
在微服务架构中,我们在生产中部署的服务数量呈指数级增长。通常,每当我们处理一个新的用例时,我们会问我们是否需要一个新的服务,或者功能是否可以构建在现有的服务中。在绿地案例中,我们更经常得出的结论是将其构建为一项新服务。但是这对于现有的部署服务来说是有成本的。假设有一个 SSO 服务;如果每个新服务都调用 SSO 验证,SSO 团队就会开始遇到问题,如果有一个流氓服务的话。为了克服这个问题,应用需要有一个配额分配和消耗跟踪的机制。
爆炸式增长也给服务器端负载均衡器带来了问题。负载均衡器中的大量调用通常会使它们的流量饱和,从而降低它们的性能。面对这种情况,客户端负载均衡通常是一个好方法。
网络是安全的
在像微服务这样的分布式架构中,安全的责任在每个团队身上。首先,有许多技术选择,因此团队需要跟踪每个技术中的 bug 修复/问题。这些技术都必须正确配置,以避免任何安全问题。第二,需要控制军种间的通信。服务级授权是过滤掉虚假或未知服务连接的必要条件。此外,执行纯文本交换的应用会暴露敏感数据。因此,需要使用安全协议进行通信。
拓扑不会改变
敏捷性是采用微服务架构的原因之一。敏捷性意味着能够快速构建、发布和部署。这意味着依赖服务之间的低耦合。这说起来容易做起来难。假设由于负载,一个现有服务的新实例被部署到生产中。其他服务必须连接到这个新的动态添加的实例。这反过来意味着服务不再有硬编码/固定的依赖关系地址位置。需要一种有效的服务解析机制来帮助确定服务的当前位置。
添加服务发现的功能还可以提高整个系统的弹性。在生产中,会有意想不到的失败。系统停机,拓扑结构发生变化。服务发现使所有正在运行的服务能够发现这些变化,而不是痛苦地对它们进行配置更改。
有一个管理员
在使用微服务等分布式系统时,操作是另一个需要关注的领域。传统上,只有一个系统可以由几个人监控和管理。但是一旦微服务出现爆炸式增长,这种模式就失效了。少数人几乎不可能对生态系统中运行的所有微服务拥有完整的运营知识。微服务旨在让团队自治。因此,每个团队负责维护和管理他们自己的服务。这要求为每个团队成员提供生产系统访问权限。因此,为了在这种方法中获得成功,需要有效的基于角色的访问,这可以联合到开发团队中。
运输成本为零
在微服务中,我们会对依赖服务进行大量调用。值得注意的是,数据交换会产生成本。数据必须在一端序列化,在另一端反序列化。因此,成本取决于消耗的 CPU 资源和传输的数据量。通信协议对我们的服务性能有影响。像 SOAP/XML 交换这样的传统协议效率非常低。JSON 被认为是比 SOAP 更好的选择。但是像协议缓冲区这样的二进制协议已经胜过了 JSON。考虑到信息交换发生在应用之间,最好为完整的生态系统选择一个二进制协议。
网络是同构的
如果一个网络在所有系统上运行相同的一组硬件,并且所有应用都按照标准协议进行通信,则该网络被归类为同构网络。在所有系统上运行相同的硬件几乎是不可能的,但是使用 Kubernetes,我们可以限制资源,以便每次部署应用容器时,它都获得相同数量的资源。
apiVersion: v1
kind: Pod
metadata:
name: frontend
spec:
containers:
- name: gs
image: istio-ks/spring-gs
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
在前面的代码中,spring-gs
应用容器请求指定数量的 CPU 和内存资源。它还定义了 CPU 和内存消耗的上限。
同样,不可能为整个生态系统运行单一的通信协议。这种想法对于独立自主团队的微服务哲学来说是徒劳的。
James 指出,构建一个分布式系统而不明确处理这些问题是失败的。企业级系统在所有这些方面总是会有问题。因此,缓解这些挑战对于成功采用微服务架构至关重要。除了早期的通用分布式系统挑战之外,微服务还有自己的一系列问题。这些问题是由于敏捷和自治团队的哲学。
基础设施
微服务架构比整体架构复杂得多。有许多移动部件,每个部件都依赖于一个或多个微服务。由于每个团队都可以选择他们的微服务背后的技术,因此支持所有这些基础架构要求是一项挑战。
基础架构团队首先需要找到以经济高效的方式按需配置基础架构的方法。在微服务采用之旅中,随着我们部署更多服务,硬件需求也会增加。微服务规定的粒度使我们能够在加载时扩展单个服务。要做到这一点,基础设施应该在需要时可用。基础设施不再像 monolith 版本那样成为瓶颈。
配置硬件不仅仅是让它可用。这也是关于获得正确的库和框架集。技术的广泛选择使得在不中断现有服务的情况下支持新需求变得非常困难。开发和基础设施团队之间需要更紧密的协作。
监控和调试
监控微服务与监控整体架构完全不同。用于监控整体架构的技术不适用于微服务。一个功能有许多活动部件。因此,它需要更加自动化。获取有效的指标也是一个重要的方面。这影响了服务如何扩大或缩小。随着服务规模的扩大或缩小,监控应该能够发现它们并采取相应的措施。
传统的日志处理机制不能很好地处理微服务。首先,整个系统完成了更多的日志记录。需要有效的日志归档,因为系统会很快耗尽空间。其次,我们必须能够快速查找和关联多个服务之间的日志。在生产中断的时候,我们确实希望处于一个很难将两个相互依赖的服务联系起来的位置。
至此,我们已经讨论了主要的运营挑战。我们将寻找解决问题的方法。还有其他挑战,如服务测试方法、知识分布等。这些挑战超出了本书的范围。如果我们仔细观察,就会发现所有的运营挑战都与联合应用体系结构和传统应用管理方法之间的不匹配有关。实现像 Kubernetes 这样的容器编排器有助于解决一些与基础设施相关的挑战。团队可以使用 Kubernetes 快速提供资源。它可以帮助服务按需扩展。但它不能解决剩下的问题。需要更多的工具来有效地应对这些挑战。
语言库
在 21 世纪初,像网飞和 Twitter 这样的公司开始在他们的解决方案中采用微服务架构。他们面临所有之前讨论过的挑战。这些公司都开始构建工具和基础设施库来解决这些挑战。这些库由这些公司使用的技术组成;例如,网飞服务大多基于 Java,因此网飞创建了以下 Java 库来解决这些问题:
-
Eureka :这个工具旨在服务发现。它由两个组件组成:服务注册中心和服务客户机。bootstrap 中的服务使用服务客户端将它们注册到 Eureka 服务器。应用还使用服务客户机来发现它们的依赖关系的实例。
-
这个工具是一个 Java 库,提供服务交互之间的容错。
-
Ribbon :这个工具是一个支持客户端负载均衡的 Java 库。
-
这个工具是一个应用网关,支持基于服务发现的动态路由。
这些解决方案是在 2012 年左右开源的,并由开发人员进行了调整。随着时间的推移,Spring Boot 被释放,这些项目与它合并为春云网飞。
现在让我们用这些库构建一个简单的微服务示例。
实践示例
这本书遵循实践的方法,你将通过一个例子来学习。我们选择了一个简单的例子来理解前面讨论的问题。关于这些库的深入细节超出了本书的范围。
我们将构建一个简单的问候服务。这项服务有一个简单的选项。我们可以使用一个userName
来调用服务,它将用一个问候来响应指定的用户。因此,作为契约,用户可以调用下面的 HTTP GET 调用:http://localhost:8080/greeting/Rahul
。该服务将使用以下响应进行响应:
Hello ! Rahul
为了让解决方案工作,我们必须做一些基本的设置。在您的计算机上安装以下软件:
-
Java :我们需要 Java 8 或以上版本。请从甲骨文官方网站
http://www.oracle.com/technetwork/java/javase/downloads/index.html
下载 Java 的最新更新。在撰写本文时,最新的 Java 版本是 12.01。您可以通过运行以下命令来检查您的 Java 版本:$ java --version openjdk 11.0.3 2019-04-16 OpenJDK Runtime Environment (build 11.0.3+7-Ubuntu-1ubuntu218.04.1) OpenJDK 64-Bit Server VM (build 11.0.3+7-Ubuntu-1ubuntu218.04.1, mixed mode, sharing)
-
IDE :本章的代码是用 IntelliJ 构建的。但是您可以使用自己选择的任何 Java IDE。
-
Maven : Maven 是 JVM 生态系统中流行的构建工具。它用于依赖性管理和运行自动化任务。您不需要在本地机器上安装 Maven。从
https://maven.apache.org/
下载最新版本的 Apache Maven。要了解更多关于 Maven 的信息,可以参考 Maven 文档。您可以使用以下命令检查 Maven 版本:$ mvn -version Apache Maven 3.6.0 (97c98ec64a1fdfee7767ce5ffb20918da4f719f3; 2018-10-25T00:11:47+05:30) Maven home: /home/home/Tools/apache-maven-3.6.0 Java version: 1.8.0_201, vendor: Oracle Corporation, runtime: /usr/lib/jvm/java-8-oracle/jre Default locale: en_IN, platform encoding: UTF-8 OS name: "linux", version: "4.15.0-50-generic", arch: "amd64", family: "unix"
现在,我们已经具备了所有的先决条件,让我们使用下面的命令创建一个 Maven 项目:
$ mvn archetype:generate greeting-rest-service
现在更新POM.xml
来添加 Spring Boot 依赖项。
<project xmlns:="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>eureka-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
在 POM 文件中,我们完成了以下操作:
-
添加
spring-boot-starter-parent:20104.RELEASE
为我们的家长。这确保我们获得所需 Spring Boot 依赖项的正确版本。 -
添加了
spring-boot-starter-web
作为开始,以确保我们拥有创建基于 REST 的服务所需的依赖关系。 -
添加了
spring-boot-maven-plugin
,它可以让我们通过使用mvn spring-boot:run
从命令行运行我们的项目作为可执行文件。
现在,让我们以下面的方式为我们的功能添加一个 REST 控制器:
@RestController
class ApplicationRestController {
@RequestMapping("/greeting/{user}")
public String greeting(@PathVariable String user) {
return "Hello! " + user;
}
}
在前面的代码中,我们完成了以下工作:
-
创建了一个
ApplicationRestController
并用RestController
对其进行注释,以提供 REST API 端点。 -
添加了一个问候方法来处理一个
"/greeting/{user}"
位置,并向它发回一个String
响应。
要运行这个应用,我们需要添加以下主类:
@SpringBootApplication
public class ApplicationMain {
public static void main(String[] args) {
SpringApplication.run(ApplicationMain .class, args);
}
}
在前面的代码中,我们完成了以下工作:
-
用
SpringBootApplication
注释对ApplicationMain
进行了注释。它将用所需的配置引导 Spring。 -
main 方法使用带注释的 main 类调用
SpringApplication
。
现在让我们在 IDE (Eclipse/IntelliJ)中运行它。在浏览器中通过查找http://localhot:8080/greeting/Rahul
进行验证。见图 2-4 。
图 2-4
问候服务输出
现在,我们有了一个简单的 REST 服务,可以用来体验网飞的各种图书馆。
启用断路器
现在,让我们构建一个服务,它可以是我们的 web 服务的客户端之一。
@Service
class GreetingService {
private final RestTemplate restTemplate;
public GreetingService(RestTemplate rest) {
this.restTemplate = rest;
}
public String greet(String username) {
URI uri = URI.create("http://localhost:8080/greeting/"+username);
return this.restTemplate.getForObject(uri, String.class);
}
}
在前面的代码中,我们完成了以下工作:
-
添加了一个正在给我们的
greeting{User}
位置打休息电话的GreetingService
。 -
greet()
方法抽象了 REST 调用,使其看起来像是对其他组件的本地调用。
可以在测试中调用该方法,如下所示:
@Test
public void testGreetingService() {
String response = greetingService.greet("user");
then(response).contains("Hi! User");
}
在前面的测试用例中,我们调用了greetingService
并验证了响应。但是,如果问候语服务不可用或面临性能问题,此测试将失败,并显示以下错误:
I/O error on GET request for "http://localhost:8080/greeting/user": Connection refused (Connection refused); nested exception is java.net.ConnectException: Connection refused (Connection refused)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:744)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:710)
at org.springframework.web.client.RestTemplate.getForObject(RestTemplate.java:329)
at hello.GreetingService.greet(RestTemplateClientTest.java:87)
现在让我们配置 Hystrix 断路器来处理这些常见的生产问题。在我们继续之前,我们需要添加spring-cloud-starter-netflix-hystrix
依赖项。为此,按如下方式更新POM.xml
文件:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
.....
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
...
</dependencies>
在前面的代码中,我们完成了以下工作:
-
添加了配置正确版本的
spring-cloud
依赖项的spring-cloud-dependencies
BOM。 -
添加了
spring-cloud-starter-netflix-hystrix
依赖项。
现在让我们为GreetingService
配置 Hystrix 断路器。
@HystrixCommand(fallbackMethod = "fallbackGreeting", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000")
})
public String greet(String username) {
URI uri = URI.create("http://localhost:8080/greeting/"+username);
return this.restTemplate.getForObject(uri, String.class);
}
public String fallbackGreeting(String username) {
return "Hi! there";
}
在前面的代码中,我们以下列方式配置了 Hystrix 断路器:
-
我们通过使用
HystrixCommand
注释启用了断路器。 -
fallbackGreeting
方法用于为故障提供回退方法。使用HystrixCommand
的fallbackMethod
属性进行配置。 -
我们通过使用
execution.isolation.thread.timeoutInMilliseconds
属性配置了一个超时。
现在,当我们运行测试用例时,我们不再看到异常。我们从fallbackGreeting
方法而不是原始的 greet 方法获得响应。
启用服务发现
现在让我们使用网飞尤里卡配置服务发现。第一步,我们需要运行尤里卡服务器。这是通过添加spring-cloud-starter-netflix-eureka-server
依赖项来实现的。
<dependencies>
....
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
....
</dependencies>
现在,可以通过以下方式从 Spring Boot 应用启动 Eureka 服务器:
-
EnableEurekaServer
注释启动了 Eureka 服务。 -
服务器可以配置以下附加应用属性:
@EnableEurekaServer
@SpringBootApplication
public class EurekaServiceApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServiceApplication.class, args);
}
}
server.port=8761
eureka.client.register-with-eureka=false
让我们运行应用并查找http://localhost:8671/
。见图 2-5 。
图 2-5
用于服务发现的 Eureka 服务器
现在我们需要配置之前创建的 REST 服务,将这个 Eureka 服务器用于服务注册,然后将服务发现用于我们的greetingService
。要启用服务注册,添加spring-cloud-starter-netflix-eureka-client
依赖项。
<dependencies>
....
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
...
</dependencies>
这个依赖关系将使用配置的spring.application.name
属性名来配置服务注册中心。启动服务并查找 Eureka 服务器。它列出了一个注册的服务。见图 2-6 。
图 2-6
尤里卡服务器注册的服务
现在,让我们在greeting-rest-service
中使用service-discovery
。我们有很多方法可以配置它。Spring Boot 提供了一个本地的EurekaClient
来与 Eureka 服务器交互。或者,如果我们使用restTemplate
,同样的事情可以通过使用网飞功能区启用客户端负载均衡来实现。
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
// Rest removed for Brevity
public String greet(String username) {
URI uri = URI.create("http://A-BOOTIFUL-CLIENT/greeting/"+username);
return this.restTemplate.getForObject(uri, String.class);
}
在前面的代码中,我们完成了以下工作:
-
用
LoadBalancer
注释对RestTemplate
配置进行了注释。如果spring-cloud-starter-netflix-ribbon
在我们的类路径中,这将启用功能区。 -
使用服务名而不是主机和端口地址来连接服务。首先在 Eureka 服务器中查找服务名,以确定注册服务的地址。
在上一节中,我们了解了网飞 OSS 如何利用微服务架构解决各种问题。类似地,谷歌和 Twitter 等其他公司也围绕 Stubby 和 Finagle 等框架构建了栈。
但是在使用基础设施库时会出现一些问题。首先,应用代码与其基础设施相结合。这使得服务依赖于基础设施组件。此外,由于每个调用都在应用中进行了微调,因此团队必须与运营团队合作来调整和微调超时。
潜在地,基础设施库使得应用团队很难更新他们的框架。此外,这些框架是面向语言的。网飞栈基于 Java,而 Twitter 基于 Scala。这些栈中的每一个都提供一组不同的功能。如果我们正在寻找一个在选择的栈中不可用的特性,我们不能改变它。因此,简而言之,如果我们真的想要选择技术的灵活性,那么我们就不能被这些框架所束缚。最后,围绕这些框架构建服务需要开发人员有一个学习曲线。参见图 2-7 。
图 2-7
特定于语言的库
服务网格
之前,我们讨论了采用微服务架构时面临的挑战。如果我们深入研究这些挑战,我们会发现它们与微服务业务逻辑无关,而是与服务之间的交互方式有关。在像 Kubernetes 这样的云原生生态系统中,这些挑战可以通过部署服务网格来解决。
服务网格被定义为以集成方式解决微服务网络挑战的分布式系统。完整的系统提供了与 Kubernetes 互补的功能。它建立在控制 Kubernetes 集群中所有服务交互的专用层的基础上。该层通常由与服务一起部署的轻量级网络代理组成,而服务并不知道它。参见图 2-8 。
图 2-8
服务网格
部署服务网格的目的是拥有一个能够主动监控所有服务流量的集成平台。这样,服务网格可以解决与容错、服务安全和应用监控相关的问题。正如上一节所讨论的,有一些独立的库可以解决这些问题。但是服务网格提供了一种新的非侵入性的方式,在云原生环境中高效地做事。与特定于语言的框架相比,服务网格解决了服务之外的挑战。服务不知道它们正在与服务网格一起工作。
服务网格是协议不可知的,并且在第 5 层上运行。这样,他们可以部署在一个多点环境中。这种抽象允许开发人员专注于他们的应用业务逻辑,并允许系统工程师有效地操作基础设施。这对两个人来说都是双赢。总的来说,服务网格解决了对这些挑战的担忧,并增加了以下好处。
交通控制
在整体应用中,交通流量是从北到南的方向。但是在微服务应用中,服务相互通信以提供完整的功能。因此,有更多的东西向交通。服务需要发现新部署的服务并将调用路由到它们。
在 Kubernetes 集群中,可以使用 DNS 解析部署的服务。Kubernetes 还充当负载均衡器,在所有可用的服务节点之间平均分配所有流量。当我们想要更新我们的服务时,这是非常有用的。我们可以通过一次更新一个实例来更新所有实例。但是 Kubernetes 不允许细粒度的流量控制机制,在这种机制下,流量可以使用各种其他方法进行路由。参见图 2-9 。
图 2-9
服务网格流量路由
另一方面,服务网格允许我们微调服务的每个已部署实例的流量。服务器网格能够查找第 7 层请求报头,然后将请求路由到特定的服务实例。这个黑暗启动/罐头厂部署的过程可以用来为一部分用户发布服务。这个过程允许我们在向整个用户群发布新版本之前获得对它的信心。这创建了从一个版本到另一个版本的平滑发布过程,如果新版本产生了新的错误或问题,可以自动回滚。
安全
服务级安全性是 Kubernetes 集群中拥有服务网格的另一个主要好处。部署在 Kubernetes 集群中的应用在继续之前执行基于 SSL 的握手。SSL 协议验证服务身份,并相应地对其进行授权。安全问题远远超出了 Kubernetes 的范围,Kubernetes 的目标是获得最大的服务正常运行时间。参见图 2-10 。
图 2-10
TLS 安全性
我们还可以应用一个细粒度的策略来查找应用用户属性。因为服务网格是建立在 L4 通信之上的,所以它可以应用所有的安全策略,而不需要理解服务协议。服务网格还允许我们配置速率限制,以便限制恶意服务用户。
分析学
服务网格允许我们系统地跟踪和关联各种服务之间的交互。它以请求时间表的形式呈现这些信息。这种能力被称为追踪。跟踪使我们能够调试请求如何从一个服务流向另一个服务。在 monolith 应用中,我们曾经有一个请求日志,它可以指向作为服务的请求。但是在每个服务调用下一个服务的服务网格中,很难调试请求生命周期。跟踪使我们能够重建请求流,并确定应用中的性能问题。见图 2-11 。
图 2-11
分布式跟踪
服务网格中的微服务通过转发跟踪上下文头来实现这一点。这样,分布式跟踪有助于可视化服务间的依赖关系。服务网格还捕获请求量和失败率方面的指标。
另一个重要方面是服务日志处理。在传统的应用中,通常有一个记录所有内容的日志文件。在微服务架构中,我们有许多服务日志记录,我们需要查找所有服务以了解适用的行为。服务网格提供集中式日志记录和基于日志构建的图形仪表板。其目的是在微服务中提供操作可见性。
总之,服务网格允许我们将基础设施从应用代码中分离出来。它还简化了底层网络拓扑,因为网络只提供物理连接。所有防火墙、负载均衡器和子网都可以删除,因为它们不想控制任何服务交互。相反,所有这些都可以使用服务网格来配置。
边车模式
服务网格通常是按照 Sidecar 模式实现的。如前所述,一个单独的进程,也称为 sidecar,与应用代码一起部署。这种样式被命名为边车,因为它类似于摩托车上的边车。sidecar 负责提供诸如联网、监控、跟踪、记录等功能。sidecar 依赖于父应用。它与父应用具有相同的生命周期。这样,我们可以跨不同的技术栈扩展应用,包括不提供可扩展性的遗留应用。
在 Kubernetes 集群中,sidecar 与集群中运行的每个服务一起运行。sidecar 代理进出该服务的所有流量。sidecar 与其他 sidecar 通信,并由 Kubernetes 框架管理。参见图 2-12 。
图 2-12
边车模式
值得注意的是,该模式适用于应用横切关注点,而不考虑任何性能开销。如果性能是一个考虑因素,那么该模式的网络开销将使它不适合。因为 sidecar 与父应用具有相同的生命周期,所以它与父应用一起扩展。该模式不允许孤立地向上扩展边车。在服务网格中,边车代理执行以下任务:
-
服务发现:代理确定可用的上游和下游服务实例的列表。
-
健康检查:代理检查服务发现返回的上游服务实例是否健康,是否准备好接受网络流量。这些检查可以包括对
/health
端点的查找。它也可以基于服务失败率(例如,使用三个连续的5xx
值作为不健康状态的指示)。 -
路由:给定来自服务网格的服务对
/foo
的 REST 请求,代理确定它应该将该请求路由到哪个集群。 -
负载均衡:路由时一旦选择了上游服务集群,请求应该发送到哪个上游服务实例?代理还负责应用断路器(可能超时)并重试。
-
认证和授权:它通过使用 mTLS 或任何其他机制来验证服务交互。
-
可观察性:对于每个请求,都会生成详细的统计数据、日志记录和分布式跟踪数据,以便操作员能够了解分布式流量,并在出现问题时进行调试。
所有上述项目都是服务网状边车的责任。换句话说,sidecar 负责有条件地翻译、转发和观察流入和流出服务实例的每个网络数据包。sidecar 也称为服务网格数据平面。
特使,边车供应商
网络应该对应用透明。当网络和应用出现问题时,应该很容易确定问题的根源。
—特使设计目标
现在我们了解了 sidecar 的作用,让我们使用 Envoy 创建一个。Envoy 是用 C++编写的高性能 L7 代理和通信总线。它已经在大型的、现代的、面向服务的架构中经受了考验。特使架构具有以下优势:
-
用本地语言编写的高度优化的进程外服务代理
-
可插入 L3/L4,对收到的 TCP 消息执行各种任务
-
可在 HTTP L7 上插入,以执行诸如请求路由、速率限制等任务
-
多用途的应用可观察性,使 it 能够捕获和报告所有组件的统计数据
-
支持各种应用,如 MongoDB、DynamoDB、MySQL、Thrift 等等
自从发布以来,Envoy 在社区中广受欢迎,并且已经成为应用 sidecars 的事实上的标准。特使架构的核心由以下组件组成:
-
端口监听器:监听器允许 Envoy 监听指定地址的网络流量。Envoy 仅支持基于 TCP 的侦听器。作为一种实践,建议运行配置了多个侦听器的单个 Envoy 实例。
-
过滤器:过滤器使 Envoy 能够执行各种操作,如路由、翻译协议、生成统计数据等。,对收到的消息。每个端口监听器配置自己的一组过滤器。所有这些过滤器组合成一个过滤器链,每个 TCP 消息都会调用这个过滤器链。Envoy 有一大套现成的过滤器。这些过滤器可以大致分类如下:
-
监听器过滤器:监听器过滤器作为连接请求中握手的一部分被调用。这些负责诸如 TLS 检查或服务远程目的地等事情。这些过滤器在初始阶段访问原始数据并操纵 L4 连接的元数据。
-
网络过滤器:网络过滤器在连接后为每个 TCP 消息调用。它们执行各种任务,如应用授权、速率限制、TLS 认证等。它们不局限于一般事物。MySQL 和 MongoDB 等特定于应用的协议有过滤器,它们被调用来收集统计数据、执行基于角色的访问等。
-
HTTP 过滤器 : Envoy 附带了大量的 HTTP 过滤器。过滤器可以做各种事情,比如 gzip 压缩、grpc 到 JSON 的转换等等。这些过滤器可以处理特使代理收到的 HTTP 请求。这些筛选器是由 HTTP 网络连接管理器网络筛选器创建的。
-
-
集群:集群定义为 Envoy 连接的一组逻辑相似的主机。特使集群可以定义为静态配置,也可以使用内置的服务发现动态生成。
图 2-13 总结了每个服务的特使组件。
图 2-13
特使过滤器链
值得注意的是,这些组件是在配置文件中静态定义的。或者,可以用动态服务配置来配置 Envoy。配置由以下组件生成:
-
端点发现服务****【EDS】:Envoy 可以使用此服务在集群中添加/删除服务。该服务提供了 DNS 的替代方案,可用于解决 DNS 瓶颈。
-
集群发现服务****【CDS】:该服务使 Envoy 能够动态发现路由中使用的应用集群。发现集群后,Envoy 会在配置中添加、更新和删除集群。
-
路由发现服务(RDS) :该服务使 Envoy 能够动态构建到 HTTP 连接管理器过滤器的过滤器链。
-
侦听器发现服务(LDS) :该服务使 Envoy 能够动态地构建到 HTTP 连接管理器过滤器的完整侦听器链。
-
秘密发现服务(SDS) :该服务使 Envoy 能够发现 TLS 证书、私钥等应用秘密。该服务还负责提供客户的公共证书。
现在我们已经简单了解了 Envoy 的工作原理,让我们试着为我们的 Spring Boot 问候服务配置它。
配置特使
在我们可以使用 Envoy 之前,我们需要将它安装在我们的盒子上。在撰写本文时,1.10.0 是最新版本。Envoy 是一款开源应用;它需要针对特定的系统进行编译和构建。可以使用 https://www.envoyproxy.io/docs/envoy/latest/install/building
中的指令进行构建。
或者,我们可以使用特使项目发布的 Docker 映像。为此,我们需要确保安装了 Docker,如下所示:
$ docker --version
Docker version 18.09.6, build 481bc77
在 https://docs.docker.com/install
下载 Docker 的最新版本。
特使的正式发布版本也作为 Docker 映像发布。这些映像可在以下存储库中找到:
envoyproxy/envoy :在 Ubuntu Xenial 基础之上包含一个 envoy 二进制文件
envoyproxy/envoy-alpine :在 glibc alpine 基础上包含一个 envoy 二进制文件
envoy proxy/Envoy-alpine-debug:在 glibc alpine 基础之上包含一个带有调试符号的 Envoy 二进制文件
一旦我们安装了 Docker,让我们下载 Envoy 代理 alpine 映像。
$ docker run -it envoyproxy/envoy-alpine envoy –version
envoy version: e95ef6bc43daeda16451ad4ef20979d8e07a5299/1.10.0/Clean/RELEASE/BoringSSL
Envoy 有一个命令行界面,可以用来配置它。让我们看看一些常用的选项。
-
-版本:描述特使发布的版本。
-
-c :提供用于特使的配置文件。该配置可提供 YAML 或 YSON。
-
- mode :可以参与启动代理服务器或者验证代理配置。这是通过调用验证模式来实现的。默认情况下,特使在服务器模式下运行。
-
-l,- log-level :用于设置特使日志级别。还有其他选项可用于配置日志路径和日志格式。
-
- service-node :定义特使运行的本地服务节点。
-
- service-cluster :定义特使运行的本地集群节点。
Envoy CLI 有大量选项。大部分是可选值。我们只讨论了那些经常使用的。您应该阅读官方文档来详细了解它们。
现在让我们配置 Envoy 来代理我们的问候服务。为此,让我们首先运行我们的问候服务,并通过查找http://localhost:8080/greeting/Rahul
来验证它。见图 2-14 。
图 2-14
Spring Boot 问候服务
现在,让我们使用以下配置来配置 Envoy:
static_resources:
listeners:
- address:
socket_address:
address: 0.0.0.0
port_value: 80
filter_chains:
- filters:
- name: envoy.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager
codec_type: auto
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: service
domains:
- "*"
routes:
- match:
prefix: "/greeting"
route:
cluster: greeting_service
http_filters:
- name: envoy.router
typed_config: {}
clusters:
- name: local_service
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
load_assignment:
cluster_name: greeting_service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: 172.17.0.1
port_value: 8080
admin:
access_log_path: "/dev/null"
address:
socket_address:
address: 0.0.0.0
port_value: 8081
此配置包括许多部分,如下所述:
-
listeners
部分指定 Envoy 正在监听我们容器的端口 80。 -
监听器与一个
filter_chain
相关联。过滤器链有一个第 7 层 HTTP 过滤器。 -
过滤器有一个路由配置,它匹配所有去往
/greeting
位置的域名。该配置将所有这样的请求路由到一个greeting_service
。 -
greeting_service
位置由簇定义。它可以用来定义一个服务的多个实例以及它们之间可能的负载均衡机制。在前面的配置中,我们在端口 8080 上运行服务。机器的 IP 地址指定了 Docker 网关的地址。我们可以使用以下命令确定 Docker 网关地址:$ docker network inspect bridge [ { "Name": "bridge", "Id": "93196df71406f690bf83ba65d7556a4ba9fae676b828e578c53832f8b59608ef", "Created": "2019-05-30T07:42:54.43279813+05:30", "Scope": "local", "Driver": "bridge", "EnableIPv6": false, "IPAM": { "Driver": "default", "Options": null, "Config": [ { "Subnet": "172.17.0.0/16", "Gateway": "172.17.0.1" } ] }, // Removed for Brevity } ]
-
最后,Envoy 有一个
admin
服务,可以用来获取统计数据、配置代理等。在上面的配置中,管理服务运行在容器的 8081 端口上。
现在,通过使用以下 Docker 命令,使用以前的配置运行 Envoy:
$ docker run -v /home/rahul/Projects/envoy-conf:/envoy-conf -p 80:80 -p 8081:8081 -it envoyproxy/envoy-alpine envoy -c /envoy-conf/service-envoy.yaml
[2019-05-30 06:03:17.152][1][info][main] [source/server/server.cc:205] initializing epoch 0 (hot restart version=10.200.16384.127.options=capacity=16384, num_slots=8209 hash=228984379728933363 size=2654312)
[2019-05-30 06:03:17.152][1][info][main] [source/server/server.cc:207] statically linked extensions:
[2019-05-30 06:03:17.152][1][info][main] [source/server/server.cc:209] access_loggers: envoy.file_access_log,envoy.http_grpc_access_log
[2019-05-30 06:03:17.152][1][info][main] [source/server/server.cc:212] filters.http: envoy.buffer,envoy.cors,envoy.ext_authz,envoy.fault,envoy.filters.http.grpc_http1_reverse_bridge,envoy.filters.http.header_to_metadata,envoy.filters.http.jwt_authn,envoy.filters.http.rbac,envoy.filters.http.tap,envoy.grpc_http1_bridge,envoy.grpc_json_transcoder,envoy.grpc_web,envoy.gzip,envoy.health_check,envoy.http_dynamo_filter,envoy.ip_tagging,envoy.lua,envoy.rate_limit,envoy.router,envoy.squash
[2019-05-30 06:03:17.152][1][info][main] [source/server/server.cc:215] filters.listener: envoy.listener.original_dst,envoy.listener.original_src,envoy.listener.proxy_protocol,envoy.listener.tls_inspector
[2019-05-30 06:03:17.152][1][info][main] [source/server/server.cc:218] filters.network: envoy.client_ssl_auth,envoy.echo,envoy.ext_authz,envoy.filters.network.dubbo_proxy,envoy.filters.network.mysql_proxy,envoy.filters.network.rbac,envoy.filters.network.sni_cluster,envoy.filters.network.thrift_proxy,envoy.filters.network.zookeeper_proxy,envoy.http_connection_manager,envoy.mongo_proxy,envoy.ratelimit,envoy.redis_proxy,envoy.tcp_proxy
[2019-05-30 06:03:17.152][1][info][main] [source/server/server.cc:220] stat_sinks: envoy.dog_statsd,envoy.metrics_service,envoy.stat_sinks.hystrix,envoy.statsd
[2019-05-30 06:03:17.152][1][info][main] [source/server/server.cc:222] tracers: envoy.dynamic.ot,envoy.lightstep,envoy.tracers.datadog,envoy.zipkin
[2019-05-30 06:03:17.152][1][info][main] [source/server/server.cc:225] transport_sockets.downstream: envoy.transport_sockets.alts,envoy.transport_sockets.tap,raw_buffer,tls
[2019-05-30 06:03:17.152][1][info][main] [source/server/server.cc:228] transport_sockets.upstream: envoy.transport_sockets.alts,envoy.transport_sockets.tap,raw_buffer,tls
[2019-05-30 06:03:17.152][1][info][main] [source/server/server.cc:234] buffer implementation: old (libevent)
[2019-05-30 06:03:17.160][1][info][main] [source/server/server.cc:281] admin address: 0.0.0.0:8081
在前面的 Docker 命令中,我们执行了以下操作:
-
-v /home/rahul/Projects/envoy-conf:/envoy-conf
:-v
选项将主机中的/home/rahul/Projects/envoy-conf
位置挂载到容器中的/envoy-conf
位置。 -
-p 80:80 -p 8081:8081
:-p
选项将容器中的端口 80 和 8081 绑定到主机。 -
envoy -c /envoy-conf/service-envoy.yaml
:这个特使命令运行配置文件中指定的配置。
该命令在端口 80 上运行特使代理,并将其绑定到本地主机端口 80。
验证服务
让我们通过查找http://localhost/greeting/rahul
来验证服务。见图 2-15 。
图 2-15
通过特使代理的问候服务
特使现在是我们服务的代理人。它正在监控对我们的问候服务的所有请求。如果我们关闭问候服务并查找http://localhost/greeting/rahul
,我们会得到一个 503 错误而不是 404。见图 2-16 。
图 2-16
特使服务 503
现在让我们在端口 8081 上查找特使管理 UI,如http://localhost:8081/
所示。该界面使我们不仅可以查看代理的状态,还可以修改它。见图 2-17 。
图 2-17
特使管理用户界面
我们可以在/stats
位置下找到服务统计。我们还可以找到报告的断路器统计数据。这些统计数据是 Prometheus 格式的,可以注入 Prometheus 服务器进行详细监控。
cluster.local_service.bind_errors: 0
cluster.local_service.circuit_breakers.default.cx_open: 0
cluster.local_service.circuit_breakers.default.cx_pool_open: 0
cluster.local_service.circuit_breakers.default.rq_open: 0
cluster.local_service.circuit_breakers.default.rq_pending_open: 0
cluster.local_service.circuit_breakers.default.rq_retry_open: 0
cluster.local_service.circuit_breakers.high.cx_open: 0
cluster.local_service.circuit_breakers.high.cx_pool_open: 0
cluster.local_service.circuit_breakers.high.rq_open: 0
cluster.local_service.circuit_breakers.high.rq_pending_open: 0
cluster.local_service.circuit_breakers.high.rq_retry_open: 0
在前面的配置中,我们基于 Envoy 构建了一个简单的前端代理。我们可以将服务添加到配置中,以便进行扩展和 canary 部署。Envoy 是一个通用的框架,可用于各种工具,如 Ambassador、Istio 等。这里的目的是让你对嫉妒有一个简单的了解。全面的特使知识超出了本书的范围。
摘要
在本章中,我们解释了对服务网格的需求。本章首先介绍了微服务架构及其挑战。因为微服务架构是一个分布式系统,它屈服于“分布式计算的谬误”像网飞和 Twitter 这样的微服务架构的先驱企业已经建立了特定语言的框架来应对微服务的挑战。这些面向语言的框架并没有为手头的问题提供一个清晰的解决方案。接下来,我们看了带有服务网格的 Kubernetes 如何以一种语言中立的方式帮助处理这些挑战。我们还研究了服务网格提供的各种好处。在我们的旅程中,我们使用 Sidecar 模式查看了服务网状架构。本章最后简要介绍了特使,事实上的 sidecar 代理框架。在下一章,我们将看看 Istio 服务网格,并为各种用例进行配置。
三、安装 Istio
在前一章中,我们介绍了微服务架构及其挑战。您了解了编排引擎如何解决这些挑战,但是为了使解决方案更加优雅和可重用,您需要一个服务网格。在本章中,我们将向您展示如何设置 Istio 环境并了解 Istio 的基础知识。
Istio 服务网格
正如上一章所讨论的,当使用微服务架构时,当您想要提供响应时,通过网络调用服务会有点不太可靠。为了简化流程,客户应该能够动态地发现服务,并保证服务的可用性。服务必须以这样的方式被发现,即调用服务同意被发现服务的 API 版本契约。除此之外,调用服务应该处理网络调用期间的任何错误,重试任何失败的调用,并且在必要时超时。这些步骤是在微服务架构中创建流畅的网络交互所必需的。此外,还需要一个应用来记录呼叫和工具交易,并限制对不同服务的访问。所有这些都在应用内部带来了额外的冗余工作,并且与应用逻辑没有太大关系。
当你仔细观察时,所有这些问题和挑战都与网络通信直接或间接相关。Istio 服务网格为服务到服务的通信提供了一个基础设施层,抽象出了网络复杂性和挑战。以下是 Istio 提供的核心功能:
-
弹性 : Istio 无需在应用中编写断路器代码。它还负责服务的超时和重试,而应用并不知道。
-
安全性 : Istio 通过支持包括密钥管理在内的基于 TLS 的加密来负责访问控制。
-
遥测:由于 Istio 是网络层上的一个抽象,它可以跟踪网络调用,因此可以跟踪从单个源发起的多个服务的调用,它还可以收集调用的度量。
-
服务发现:在生产环境中运行的应用的主要需求之一是高度可用。这要求人们随着负载的增加而扩大服务规模,当不需要节省成本时则缩小服务规模。服务发现跟踪服务的可用节点,并准备好接受新任务。如果某个节点不可用,服务发现会将其从可用节点列表中删除,并停止向该节点发送新任务/请求。
-
路由选择:Istio 提供了灵活性,因此可以很好地控制服务的可用节点之间的流量。以下列表强调了 Istio 提供的基本支持:
-
负载均衡 : Istio 允许基于不同算法的负载均衡,例如循环、随机、加权和最小请求。
-
健康检查 : Istio 不仅关注节点可用性,还关注服务是否启动、运行并仍在响应,然后才将其包含在可用节点中。
-
自动部署:根据使用的部署类型,Istio 以加权模式将流量驱动到新节点。
-
图 3-1 让你一瞥使用 Istio 的服务和它的节点之间发生了什么。
图 3-1
Istio 特征
这些特性是应用必须具备的,将这些功能从应用代码中去掉会使代码更加整洁,并删除不必要的模块和跨多种语言的冗余。让我们看看 Istio 是如何通过浏览其架构来实现这一功能的。
Istio 建筑
如上所述,Istio 特性也可以在应用内部实现。Istio 使用 Sidecar 模式,将这些特性提取出来作为小组件,并包装每个服务,以便观察、验证和处理任何入站和出站请求。所有流量都被定向到代理,代理根据规则或策略来决定如何、何时或是否将流量部署到服务。使用这些规则,它还支持故障注入、电路中断和金丝雀部署等技术,而服务无需担心所有这些问题。
图 3-2 显示了 Istio 架构的不同组件。
图 3-2
Istio 组件
Istio 在逻辑上分为两大部分,数据平面和控制面板。
数据平面
数据平面负责转换、转发和监控流入和流出实例的每个网络数据包。顾名思义,它负责在服务网格中收集元数据。它拥有健康检查、路由、服务发现、负载均衡、安全性和遥测等关键功能。正如上一章所讨论的,sidecar 使用 Envoy 代理来处理所有这些关键特性。从本质上讲,数据平面可以被视为跨服务网格部署的边车代理。图 3-3 显示了数据平面的概况。
图 3-3
带特使边车的数据平面服务
如果数据平面负责所有基本项目,那么控制面板做什么?
制导机
数据平面是一组相互交互的独立节点,控制面板利用它们创建一个分布式系统。当发起网络请求时,代理不知道要连接到哪个其他代理。控制面板是网格中提供此信息的平面。当发现新服务时,控制面板填充现有的服务列表,然后代理使用该列表来判断新服务的存在并引导流量。断路、负载均衡、超时、安全信息等基本配置存储在控制面板中。当新部署发生时,数据平面具有新节点准备好接受请求的信息,但是是进行蓝/绿部署还是逐渐转移流量由控制面板定义。
控制面板向数据平面提供策略和配置,而不接触网格中的任何网络分组。图 3-4 显示了系统中网络数据包和元数据的流向。这些配置是从控制面板引用的,而所有操作都发生在数据平面。
图 3-4
控制面板引导请求并从数据平面接收元数据
在任何架构中,数据层总是存在的。如前一章所述,数据层中陈述的大多数特性在 Nginx、HAproxy 和 Envoy 等流行项目中都可用。但是这些都需要手动设置配置,或者通过自己编写的脚本,或者使用多种其他工具。Istio 将所有这些结合在一起,提供了一个单一的平台,消除了样板配置,并在解决方案中提供了持久性。Istio 使用四个主要组件来简化这些繁琐的任务。
搅拌器
Mixer 是一个独立于平台的组件。它提供了一种为服务收集遥测数据的机制,并强制执行授权策略。它从 Istio 系统中抽象出基础设施后端提供的基本支持功能,如遥测捕获、配额实施、计费系统等。服务通常与基础设施后端紧密绑定,以获取这些细节,这导致遵循特定的协议,并增加了限制和依赖性。
考虑一个服务将日志写入文件系统的例子。由于容器是易变的,服务日志可能会随着时间的推移而丢失。为了解决这个问题,人们开始将日志发送到云服务。几个月后,有人想引入一个新的日志捕获器,它能够搜索日志。传统上,服务将被修改为向这两个日志服务发送日志,但是理想情况下,服务应该只关心它的任务,而不关心日志。Istio 负责收集这些数据,Mixer 为 Istio 提供了统一的抽象,以便与基础设施后端进行交互。
图 3-5 显示了混频器遵循的拓扑结构。所有请求都被发送进行策略检查,然后,请求遥测数据被报告给 Mixer。使用高速缓存和缓冲来优化该过程。
图 3-5
请求已发送,需要用报告的遥测数据进行策略检查
基础设施后端可能依赖于基础设施提供商。要使 Mixer 成为一个模块化和可扩展的组件,将它绑定到一个特定的协议并要求基础设施提供者遵循它是不正确的。相反,Mixer 提供了称为适配器的通用插件。
适配器
适配器允许 Mixer 与基础设施后端进行交互,并保持 Istio 的其余部分从提供者中抽象出来。要使用的适配器通过混合器配置驱动,以在运行时面向任何基础设施后端。下面是一些与不同后端交互的流行适配器:
-
遥测后端:这有助于处理从吊舱收集的遥测数据。
- StatsD :将度量数据传递给 StatsD 监控后端。
-
授权后端:这有助于授权 Istio 网格内外的任何请求。
- List :对 IP 地址或正则表达式模式执行简单的白名单和黑名单检查。
-
配额后端:这有助于跟踪不同的端点配额。
- Redis 配额:支持固定或滚动窗口算法的限速配额。顾名思义,它使用 Redis 存储数据。
-
日志后端:这个适配器帮助处理和保存来自服务的日志。
-
CloudWatch :这允许 Mixer 向 Amazon CloudWatch 交付指标,并向 Amazon CloudWatchLogs 发送日志。
-
Fluentd :这将日志传送到 Fluentd 守护进程。
-
图 3-6 描述了 Mixer 与基础设施后端的交互。
图 3-6
Istio 通过 Mixer 适配器与后端服务交互
由于 Mixer 与不同的基础设施后端交互,它如何决定向哪个后端发送数据或从哪个后端请求数据?Mixer 本质上依赖于它可用的属性来进行这个调用。
属性
属性是定义请求属性的最小数据块。属性包括请求路径、请求指向的 IP 地址、响应代码、响应大小、请求大小等等。Mixer 处理这些属性,并根据配置触发对不同基础设施后端的调用。如图 3-7 所示,数据流从数据平面开始,通过 Mixer 到达基础设施后端。
图 3-7
数据平面将属性发送到属性处理器或混合器
混合器接收属性并让适配器调用基础设施后端,但是如上所述,存在定义活动适配器的配置,如何将输入属性映射到适配器属性,以及哪些实例属性应该给予哪些适配器。这属于 Mixer 的配置模型。
配置模型
配置模型基于适配器和模板。模板定义了如何将属性输入适配器。结合,他们做三种类型的配置。
-
处理程序:负责定义适配器的配置。在 StatsD 的情况下,请求计数可以是一个提供给适配器的属性。这需要在配置中定义。
-
实例:这定义了实例属性应该如何映射到适配器输入。对于请求计数,实例配置度量可以将该值定义为 1(即,对每个请求计数一次)。
-
规则:现在我们知道了从实例中读取的属性,以及如何将它们映射到适配器属性。规则定义了何时运行此流程。让我们假设我们只想推送一个服务的请求计数;然后,该检查需要放在配置的匹配规范中,如
destination.service.name == <Service Name>
所示。
这为运营商提供了粒度控制,让他们知道使用哪个基础设施后端,并在需要时添加和删除它,而无需对服务进行任何更改,这让开发人员的工作变得更加轻松。
飞行员
飞行员驾驶交通。它找出新的路径,管理交通,并处理死角。换句话说,它执行路由,提供服务发现,并促进超时、重试、断路器等等。Pilot 将特定于平台的服务发现方式从 Istio 中分离出来,从而允许 Istio 在多个环境中运行,如 Kubernetes、Nomad 等。Istio 在所有 pod 的边车中使用 Envoy 代理来处理流量和配置。Pilot 将流量相关的配置转换为特使配置,并在运行时将其推送到边车。图 3-8 显示了 Pilot 的架构及其工作原理。
图 3-8
试点建筑
为了对每个服务进行流量控制,飞行员在网格中为每个服务维护一个模型。副本和服务发现的任何更新都在模型中以服务方式被跟踪。这有助于遵循一致的协议,在跨多个环境的模型中保存数据。这也意味着环境适配器必须对通过其资源获得的数据进行操作,以将其转换为试验服务模型。
让我们考虑一个部署在 Kubernetes 上的网格示例。当 Kubernetes 创建一个新的 pod 时,它会通知它的适配器,适配器会在特定于服务的模型中存储有关服务的新副本的信息。根据网络规则和配置,这创建了特定于特使的配置,并且特使 API 通知侧柜新的服务发现。这里重要的是,由于环境适配器负责服务发现,服务可能位于多个环境中,这意味着 Istio 可以跨多个环境部署网格。图 3-9 显示了元数据从环境到 Istio 的流程。
图 3-9
Istio 中的服务发现
在服务模型中,Pilot 存储副本的数量,并配置 Envoy 以支持不同类型的负载均衡,如循环、加权、随机等,这些都不是环境提供商现成可用的。
这些服务能够相互调用,确保可用性和响应性。但是,所有的服务都应该能够调用其他服务吗?这些服务应该通过未加密的连接进行通信吗?所有这些问题都由 Citadel 处理。
城堡
我们看到了引入微服务是如何改善应用的开发时间和性能的,但它也带来了安全问题,因为网络连接成为了应用的一部分。这种连接必须防范常见的安全问题,如中间人攻击;因此,需要 TLS 支持。Citadel 提供了在 Istio 网格中加密请求的特性。它还为网格中的服务提供基于角色的访问控制。请注意,Citadel 只启用加密(换句话说,提供证书来启用服务之间的安全连接),但这个配置是由 Pilot 推送给 Envoy 的。
活版盘
厨房可以被认为是一个管理平面。它的核心职责是从用户和底层环境中抽象出配置输入的 Istio mesh。Galley 存储用户配置,对其进行验证,然后将其发送给 Pilot 进行进一步操作。
既然对 Istio 架构有了基本的了解,那我们就来看看如何在不同的环境下设置 Istio。
建立 Istio
如前所述,Istio 在 Kubernetes、Nomad 等多种环境中都得到了支持。我们将把我们的设置限制在 Kubernetes。有两种方法可以在机器上安装 Istio 一个使用 Helm 图,另一个是快速演示安装。让我们过一遍这些。
使用 Helm 安装
Helm 是一个运行在 Kubernetes 上的包管理器。它允许我们通过掌 Helm 图来定义应用结构。这种安装应该用于生产环境。它为定制数据平面和控制面板组件提供了灵活性。Helm 帮助我们生成配置文件,然后 Kubernetes 控制器可以使用该文件进行安装。
下载 Istio 版本
从 GitHub 下载 Istio 版本并设置 Istio 路径。
-
从 GitHub 中拉出 Istio 并安装。版本 1.2.2 是撰写本文时的最新版本。
curl -L https://git.io/getLatestIstio | ISTIO_VERSION=1.2.2 sh -
-
将 Istio 路径添加到环境变量。当前的 Istio 文件夹是
istio-1.2.2
。export PATH=$PWD/bin:$PATH
图 3-10 显示在 Mac 机上安装 Istio 的输出。
图 3-10
安装 Istio 版本
安装 Helm
在不同的平台上设置 Helm 是不同的,但是在所有情况下都很简单。
-
在 macOS 上安装:在 Mac 上,可以用自制软件安装 Helm。
brew install kubernetes-helm
-
在 Ubuntu 上安装:在 Ubuntu 上,使用 Snap 安装 Helm。
sudo snap install helm --classic
-
将 Helm 初始化为客户端,以获取远程存储库。
helm init --client-only
一旦安装了 Helm,我们需要添加istio-release
库到 Helm。这将包括 Istio 提供的图表。
-
将 Istio 存储库添加到 Helm。
helm repo add istio.io https://storage.googleapis.com/istio-release/releases/1.2.2/charts/
图 3-11 显示了安装过程。
图 3-11
安装 Helm
安装 Istio
让我们使用 Helm 安装 Istio。我们将使用在第一章中创建的 Minikube Kubernetes 集群。请确保 Minikube 已启动并正在运行。图 3-12 显示了如何检查状态并在出现故障时启动 Minikube。
图 3-12
宇宙魔方启动
-
我们将创建一个名为
istio-system
的名称空间,在这个名称空间下将部署 Istio 服务。kubectl create namespace istio-system
-
Istio 附带了 23 个自定义资源定义(CRD ),可以在配置 Istio 时使用。让我们使用
kubectl
来安装它们。确保你在istio
文件夹中,因为install
文件夹应该在当前位置。helm template install/kubernetes/helm/istio-init --name istio-init --namespace istio-system | kubectl apply -f -
-
验证所有 CRD 是否安装正确。
kubectl get crds | grep 'istio.io\|certmanager.k8s.io' | wc -l
-
在
install
文件夹中有多个配置文件。查看install/kubernetes/helm/istio
。我们将使用演示配置文件,它允许我们试验大多数 Istio 特性。helm template install/kubernetes/helm/istio --name istio --namespace istio-system \ --values install/kubernetes/helm/istio/values-istio-demo.yaml | kubectl apply -f -
图 3-13 显示了安装输出。
图 3-13
使用 Helm 安装 Istio
不带头盔的演示装置
这种安装更快更容易,并允许使用大多数 istio 功能。这可以在不安装任何其他第三方软件的情况下完成。确保您在istio
文件夹中。
-
所有 Istio 自定义资源定义都存在于 Istio
init
文件夹中。for i in install/kubernetes/helm/istio-init/files/crd*yaml; do kubectl apply -f $i; done
-
我们允许 Istio 使用相互 TLS 和非 TLS 模式。
istio
文件夹内的Istio-demo.yaml
允许此设置。kubectl apply -f install/kubernetes/istio-demo.yaml
参见图 3-14 了解此次安装和预期结果。
图 3-14
使用 demo.yaml 设置不带 Helm 的 Istio
GKE 装置
在 GKE 上安装 Istio 与其他两种安装方法类似,只是多了一个工具安装。假设 Kubernetes 已为 GKE 项目启用。请遵循以下步骤:
-
在本地机器上安装
gcloud
。该工具有助于管理 GKE 上的资源。安装步骤在https://cloud.google.com/sdk/docs/#deb
的 Google cloud 文档中。 -
可以使用
gcloud
安装 Kubectl。kubectl get svc -n istio-system
-
设置启用 Kubernetes 和安装 Istio 的项目 ID 和区域。
gcloud config set project [PROJECT_ID] gcloud config set compute/zone [COMPUTE_ENGINE_ZONE]
-
创建新的集群。
gcloud container clusters create istio-installation --machine-type=n1-standard-2 --num-nodes=2
-
现在可以使用 Kubernetes 集群来安装 Istio。使用这两种方法中的任何一种来设置 Istio。
验证安装
安装后,让我们验证安装是否正确完成,这意味着所有服务都在运行,并且它们的 pod 是活动的。
-
在 Istio 名称空间中查找所有正在运行的服务。
kubectl get svc -n istio-system
图 3-15 显示了正在运行的服务。
图 3-15
在 Istio 中运行服务
-
确保 Istio 系统中的所有单元都已启动并运行。
kubectl get pods -n istio-system
图 3-16 显示了预期的输出。
图 3-16
Istio 的跑步 POD
现在我们有了一个 Istio 环境,可以部署应用了。让我们来看看服务。
服务研究所
大多数服务都很简单,并且可以与本章前面讨论的组件相关联。Citadel、Galley、Pilot、Policy 和 Telemetry 是其中的几个。演示安装提供以下附加服务:
-
Grafana :这将从不同服务收集的数据呈现在一个仪表板中,用于分析和监控。它是监视集群中发生的事情的一个非常好的工具。
-
Kiali :它跟踪服务网格中的服务、它们之间的连接方式、数据流以及它们各自的性能。这是一个很好的工具,可以用来检查微服务何时关闭或影响网格的整体性能。
-
Jaeger :它监控分布式系统中的事务并排除故障。它有助于优化性能和延迟。
-
Prometheus :这是一个流行的基于开源指标的系统监控和警报工具包。它有一个强大的数据模型和查询语言,允许分析应用和基础设施。
-
跟踪和 Zipkin :这些工具在分布式系统中跟踪请求。
现在我们已经有了一个可以部署应用的 Istio 环境,但是在我们继续创建部署之前,让我们先来看几个重要的 Istio 命令和 CRD。
使用 Istio
在安装过程中,我们在 Kubernetes 集群上安装了许多自定义资源定义,我们将在后面的章节中使用这些定义来查看 Istio 的运行情况。让我们去拜访其中的几个,了解一下它们是什么。
-
Virtualservices :这定义了当一个服务调用另一个主机时使用的一组流量规则。这些规则定义了在对呼叫应用规则之前要匹配的标准。
-
DestinationRules :这在路由完成时起作用。它涵盖了负载均衡、连接池大小等基本配置。
-
ServiceEntries :这将额外的条目添加到 Istio 服务注册表中,以便自动发现的服务可以访问手动定义的条目。它配置服务的基本细节,如地址、协议、端口等。当存在服务网格外部的服务时,这是有帮助的。
-
网关:这可以看作是一个负载均衡器,位于服务网格的入口处,监听特定端口的外部连接,然后在网格内部分配流量。
-
使用这个工具,用户可以在 Pilot 已经生成的过滤器的基础上定义特定于 Envoy 代理的过滤器。换句话说,它可以在 Istio 无法自动更正错误的情况下修改网格流量;因此,需要小心使用。
-
Policies :这个工具执行一些规则,比如对服务的流量进行速率限制、报头重写、黑名单和白名单服务访问。
在安装步骤中还定义了许多其他 CRD,但这些是最常用和最常用的。我们将在本书后面的章节中用到它们。
使用 Istio CLI
我们现在已经使用 Kubectl 来部署服务,并设置了 Istio 来完成与服务网格相关的所有任务。Istio 自带 CLI,可以灵活配置 Istio 设置。调试应用的第一种方法是查看日志,但是进一步深入,istioctl
允许我们调试和诊断网格中的每个部署。让我们来看一些有助于应用调试和设置的istioctl
命令。
奥滕
这是一个命令行参数,用于与 Istio 身份验证策略进行交互。例如,让我们检查一个 Istio pods 上的tls-authentication
设置。我们将在第十章中讨论更多关于认证的内容。
istioctl authn tls-check <pod-name>
注销
这是一个命令行参数,用于从注册到的服务中取消注册现有 IP 地址。当有人想强行从服务中删除 pod 时,这是必需的。
istioctl deregister <service-name> <ip-to-be-removed>
注册
这是将 pod 注册到服务的命令行参数。
istioctl register <service-name> <ip-to-be-added> <port>
实验的
这允许使用istioctl
并生成可以修改或废弃的实验命令。它允许在四个领域进行实验。
实验授权
这允许与网格中的身份验证和授权策略进行交互。
istioctl experimental auth check <pod-name>
实验性转换-入口
这将尽最大努力将 Kubernetes 入口转换为VirtualService
配置。结果是 Istio 配置的开始。少数情况下会生成转换可能失败的警告,这可能需要手动干预。考虑清单 3-1 中的入口示例。我们会尽量把它转换成VirtualService
。
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: gateway
spec:
rules:
- http:
paths:
- path: /
backend:
serviceName: frontendservice
servicePort: 80
Listing 3-1Sample Ingress Config ingress-smaple.yaml
图 3-17 显示了将入口转换为 Istio VirtualService
产生的输出。
图 3-17
将入口配置转换为 Istio 虚拟服务
实验仪表板 grafana
使用istioctl
可以轻松查看 Grafana 仪表盘。
istioctl experimental dashboard grafana
这将为 Grafana 服务设置一个代理,并使其可以通过随机端口在 web 浏览器中访问。图 3-18 和图 3-19 显示了命令和仪表板。
图 3-19
Grafana 仪表板在随机端口上可见,此处为 53869
图 3-18
请求显示 Grafana 仪表板
在我们当前的设置中,类似的仪表盘也适用于 Envoy、Jaegar、Kiali、Promethus 和 Zipkin。
实验度量
这将打印 Kubernetes 中指定服务的指标。这取决于 Prometheus。当请求服务指标时,该命令向 Prometheus 发出一系列关于指标的请求,并打印它们。
istioctl experimental metrics <service-name>
kube 注入
该工具通过将 Envoy sidecar 注入任何现有的 Kubernetes 资源,将 Kubernetes 配置转换为 Istio 配置。对于不受支持的资源,配置保持不变。让我们从第一章中选择一个部署,并尝试将 Kubernetes 配置转换为 Istio 配置(参见清单 3-2 )。
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp-deployment
labels:
app: webapp
spec:
replicas: 1
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
containers:
- name: webapp
image: web-app:4.0
imagePullPolicy: Never
ports:
- containerPort: 5000
readinessProbe:
httpGet:
path: /health
port: 5000
initialDelaySeconds: 40
livenessProbe:
httpGet:
path: /health
port: 5000
initialDelaySeconds: 120
Listing 3-2Sample Ingress Config ingress-smaple.yaml
清单 3-3 显示了转换后的配置。
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: webapp
name: webapp-deployment
spec:
replicas: 1
selector:
matchLabels:
app: webapp
strategy: {}
template:
metadata:
annotations:
sidecar.istio.io/status: '{"version":"761ebc5a63976754715f22fcf548f05270fb4b8db07324894aebdb31fa81d960","initContainers":["istio-init"],"containers":["istio-proxy"],"volumes":["istio-envoy","istio-certs"],"imagePullSecrets":null}'
creationTimestamp: null
labels:
app: webapp
spec:
containers:
- image: web-app:4.0
imagePullPolicy: Never
livenessProbe:
httpGet:
path: /health
port: 5000
initialDelaySeconds: 120
name: webapp
ports:
- containerPort: 5000
readinessProbe:
httpGet:
path: /health
port: 5000
initialDelaySeconds: 40
resources: {}
- args:
- proxy
- sidecar
- --domain
- $(POD_NAMESPACE).svc.cluster.local
- --configPath
- /etc/istio/proxy
- --binaryPath
- /usr/local/bin/envoy
- --serviceCluster
- webapp.$(POD_NAMESPACE)
- --drainDuration
- 45s
- --parentShutdownDuration
- 1m0s
- --discoveryAddress
- istio-pilot.istio-system:15010
- --zipkinAddress
- zipkin.istio-system:9411
- --dnsRefreshRate
- 300s
- --connectTimeout
- 10s
- --proxyAdminPort
- "15000"
- --concurrency
- "2"
- --controlPlaneAuthPolicy
- NONE
- --statusPort
- "15020"
- --applicationPorts
- "5000"
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: INSTANCE_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: ISTIO_META_POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: ISTIO_META_CONFIG_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: ISTIO_META_INTERCEPTION_MODE
value: REDIRECT
- name: ISTIO_META_INCLUDE_INBOUND_PORTS
value: "5000"
- name: ISTIO_METAJSON_LABELS
value: |
{"app":"webapp"}
image: docker.io/istio/proxyv2:1.2.2
imagePullPolicy: IfNotPresent
name: istio-proxy
ports:
- containerPort: 15090
name: http-envoy-prom
protocol: TCP
readinessProbe:
failureThreshold: 30
httpGet:
path: /healthz/ready
port: 15020
initialDelaySeconds: 1
periodSeconds: 2
resources:
limits:
cpu: "2"
memory: 1Gi
requests:
cpu: 10m
memory: 40Mi
securityContext:
readOnlyRootFilesystem: true
runAsUser: 1337
volumeMounts:
- mountPath: /etc/istio/proxy
name: istio-envoy
- mountPath: /etc/certs/
name: istio-certs
readOnly: true
initContainers:
- args:
- -p
- "15001"
- -u
- "1337"
- -m
- REDIRECT
- -i
- '*'
- -x
- ""
- -b
- "5000"
- -d
- "15020"
image: docker.io/istio/proxy_init:1.2.2
imagePullPolicy: IfNotPresent
name: istio-init
resources:
limits:
cpu: 100m
memory: 50Mi
requests:
cpu: 10m
memory: 10Mi
securityContext:
capabilities:
add:
- NET_ADMIN
runAsNonRoot: false
runAsUser: 0
volumes:
- emptyDir:
medium: Memory
name: istio-envoy
- name: istio-certs
secret:
optional: true
secretName: istio.default
status: {}
---
Listing 3-3Sidecar Injected into Kubenetes Deployment
代理配置引导程序|群集|端点|侦听器|路由
此工具检索关于引导、集群、端点、侦听器或 pod 中特使实例的路由特定配置的信息。
生效
这将在 Istio 配置应用于网格之前对其进行验证。让我们验证清单 3-3 中的输出。图 3-20 显示了从文件中生成的警告,因为我们还没有向部署添加版本。
图 3-20
验证生成的 Istio 配置
这些命令有助于在服务网格中创建、修改、注入和使用 Istio 配置。
摘要
在本章中,我们学习了 Istio 架构,您了解了控制层和数据层的去耦如何帮助组织配置和数据流。我们浏览了负责配置的控制面板组件 Mixer、Pilot、Citadel 和 Galley,并展示了它们如何组织数据流,以及如何将多个 pod 转换为分布式系统。我们还介绍了如何在本地机器和 GKE 上通过 Helm 图设置 Istio,而无需任何第三方软件。我们简要地浏览了 Istio 的几个重要的 CRD。我们还简要介绍了 Istio CLI 工具,它将在后面的章节中广泛使用。在下一章,我们将深入研究 Istio CRD,并提供一些如何创建 Istio 网格的例子。
四、 Istio 虚拟服务
在前一章中,我们详细讨论了 Istio 架构。我们使用控制面板来配置数据平面。这两个组件之间的解耦允许它们独立运行,从而提高了故障处理能力。该架构使集中式操作团队能够使用通用规则来配置基础架构。Istio 针对不同的目的制定了不同类型的规则。在本章中,我们将展示如何使用 Istio 的流量管理规则。具体来说,我们将采用现有的 Kubernetes 服务,并通过它路由流量。
请求路由
在本章中,我们将从第一章开始逐步开发我们的应用,所以让我们回顾一下到目前为止已经完成的工作。在第一章中,我们开发了一个多语言应用,并将其部署到 Kubernetes 集群中。该应用有一个基于 Java 的前端和一个基于 Python 的后端。这两个应用都部署在同一个 Kubernetes 名称空间中。webapp 是使用以下配置部署的:
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp-deployment
labels:
app: webapp
spec:
replicas: 1
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
containers:
- name: webapp
image: web-app:4.0
imagePullPolicy: Never
ports:
- containerPort: 5000
-------
apiVersion: v1
kind: Service
metadata:
name: webservice
spec:
selector:
app: webapp
ports:
- protocol: TCP
port: 80
targetPort: 5000
这个配置在 Kubernetes 集群中创建了一个 pod 和一个服务。类似的配置用于部署前端 Java 应用。这两个应用可以通过使用 DNS 名称相互引用,因为它们属于同一个名称空间。类似地,不同名称空间中的服务可以通过使用完全限定的服务名来相互引用。
到目前为止,我们总是用新版本更新我们的 Kubernetes 部署。因此,我们的应用只有一个版本。但是这是一个边缘用例,不符合常规的 Kubernetes 部署。实际上,一个 Kubernetes 集群会运行同一个应用的多个版本。这将有助于我们满足不同的用例,如应用部署、A/B 测试等等。但是一旦我们有了部署的应用的许多版本,我们就会遇到各种各样的请求处理问题。
在扩展我们的示例时,我们需要向应用响应添加一个版本标识符。这可以通过在响应头中添加版本信息或在响应中添加版本前缀来实现。让我们修改我们的 Python 应用,将其版本作为欢迎消息的前缀(因为这很容易识别)。
app = Flask(__name__)
@app.route("/")
def main():
currentDT = datetime.datetime.now()
return "[{}]Welcome user! current time is {} ".format(os.environ['VERSION'],str(currentDT))
## removed for Brevity
我们现在需要通过 Dockerfile 设置VERSION
环境变量。
FROM python:3.7-alpine
COPY ./requirement.txt /app/requirement.txt
WORKDIR /app
RUN pip install -r requirement.txt
COPY . /app
ENTRYPOINT [ "python" ]
ARG ver=NA
ENV VERSION=$ver
CMD [ "app.py" ]
为了验证前面的行为,我们需要将这个应用的几个版本部署到我们的集群中。因此,首先使用以下命令行为不同版本构建一些 Docker 映像:
$docker build . -t web-app:6.0 --build-arg ver=6.0
$docker build . -t web-app:6.1 --build-arg ver=6.1
$docker build . -t web-app:6.2 --build-arg ver=6.2
现在将所有以前的版本部署到 Kubernetes 集群。这是通过使用前面讨论过的webapp-deployment
命令和不同的 Docker 映像来完成的。而且,webapp Kubernetes 服务配置了选择器app: webapp
。这将选择所有匹配这些属性的 pod,并将请求路由到其中一个。如果我们请求 webapp 服务,我们会得到来自该服务所有版本的响应。
图 4-1 显示了在 Kubernetes 上运行的 webapp 的多个版本。我们来做一个http://10.152.183.146/
的查找,它是前端服务。
图 4-1
Kubernetes 部署服务
图 4-2 ,图 4-3 ,图 4-4 显示了响应。
图 4-4
来自 6.0 的响应
图 4-3
对 6.2 的回应
图 4-2
对 6.1 的回应
在本章的其余部分,我们将为所需的版本配置 Istio 请求路由规则。Istio 可以相当好地处理 TCP 和 HTTP 服务。Istio 还支持请求路由的 L4 和 L7 属性查找。
不可分割的做法
在我们能够配置 Istio 请求路由之前,我们需要确保我们的 Kubernetes 集群遵循下面列出的实践。如果有不满足这些要求的服务,那么对这些服务的调用将不受 Istio 控制。此类请求将由 Kubernetes 组件解决。
命名服务端口
Istio 路由需要在 Kubernetes 服务中以<protocol>[<-suffix>]
格式定义的端口名称。以下是 Istio 支持的协议:
-
http
-
http2
-
https
-
grpc
-
mysql
-
mongo
-
redis
-
tcp
-
tls
-
udp
在我们的例子中,我们必须将 web 服务的端口命名为http-webapp
或http
。不同服务的端口可以有相同的名称,但是同一服务的不同端口不能有相同的名称。值得注意的是,这是 Istio 遵循的一个命名约定,并没有给 Kubernetes 规范增加任何额外的协议值。因此,让我们用以下配置更新我们的 web 服务:
apiVersion: v1
kind: Service
metadata:
name: webservice
spec:
selector:
app: webapp
ports:
- name: http-webservice
protocol: TCP
port: 80
targetPort: 5000
在前面的配置中,我们添加了一个值为http-webservice
的name
属性。使用以下命令应用以前的配置:
$kubectl apply -f ../config/webservice.yaml
带有版本标签的窗格
Istio 将根据应用版本进行路由。要选择某个版本的节点,必须对它们进行相应的标记。因此,我们所有的部署和 pod 都必须应用应用和版本标签。Istio 还在度量和遥测数据收集中使用这些标签。现在让我们标记我们的 web 服务。
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp-deployment-6.2
labels:
app: webapp
version: v6.2
spec:
replicas: 1
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
version: v6.2
spec:
containers:
# REMOVED FOR BREVITY
在前面的配置中,我们已经将version: v6.2
添加到部署和模板中。Kubernetes 标签只支持字符串值;因此,我们的应用版本被定义为 v6.2。最后,使用以下命令应用之前的配置:
$kubectl apply -f ../config/webapp-deployent.yaml
声明的 Pod 端口
只有在部署模板中声明了 pod 公开的端口时,才能应用 Istio 路由。可以将端口声明为部署模板中containerPort
字段的列表。根据 Kubernetes 文档,containerPort
字段用于提供信息。容器可以运行监听 0.0.0.0 和端口的服务。集群中的所有容器都可以访问该端口。如果 Istio 路由适用于不属于部署模板的端口,则它将被绕过。
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp-deployment-6.2
# Removed FOR BREVITY
containers:
- name: webapp
image: web-app:6.0
imagePullPolicy: Never
ports:
- containerPort: 5000
readinessProbe:
httpGet:
path: /health
port: 5000
initialDelaySeconds: 40
在前面的配置中,我们将 5000 声明为容器公开的端口。使用以下命令应用以前的配置:
$kubectl apply -f ../config/webapp-deployent.yaml
到目前为止,我们已经了解了 Istio 路由的先决条件。在配置它之前,让我们先了解一下它是如何工作的。使用VirtualService
和DestinationRule
组件在服务网格中配置请求路由。图 4-5 描述了各种相关组件之间的相互作用。
图 4-5
目的地分辨率
涉及以下交互,如图 4-5 所示:
-
服务 X 尝试使用完全限定的域名连接到服务 Y。
-
虚拟服务查找服务 Y FQDN 以确定它是否需要被处理。
-
如果是,那么匹配
DestinationRule
以确定最终的 Kubernetes 服务。 -
最后,呼叫被转发到所需的服务 y。
目的地规则
DestinationRule
将请求目标位置解析为 Kubernetes 集群中的网络地址。在上一节中,您了解到 Istio 规定版本号是 pod 标签的一部分。然后,这些标签可以在DestinationRule
中进行匹配,从而为请求处理定义基于版本的服务子集。现在让我们为 web 服务配置一些目的地规则。
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: webapp-destination
spec:
host: webservice
subsets:
- name: v1
labels:
version: v6.2
前面定义的规则配置了一个简单的目标规则 v1。host: webservice
用于选择配置了 web 服务 Kubernetes 服务的 pod。然后,它从这些节点集中选择匹配version: v6.2
标签的节点来定义子集 v1。我们可以通过以下方式创建规则:
$kubectl create -f ../config/webapp-destinationrules.yaml
之后,使用以下命令验证创建的目的地规则,如图 4-6 所示:
图 4-6
目的地规则
$istioctl get destinationrules:
在前面的配置中,子集部分可用于定义多个命名子集。每个子集都可以配置不同的VirtualService
组件。
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: webapp-destination
spec:
host: webservice
subsets:
- name: v1
labels:
version: v6.2
- name: v0
labels:
version: v6.0
使用以下命令更新规则,然后使用istioctl
进行验证(参见图 4-7 ):
图 4-7
多个目的地规则
$kubectl apply -f ../config/webapp-destinationrules.yaml
DestinationRule
组件应该由服务所有者来配置。服务所有者不仅可以创建不同的子集,还可以为每个子集定义connectionPool
、负载均衡和异常值检测属性。这些设置决定了使用者服务如何连接到各个节点。
连接池
不用说,共享连接有好处。在我们启用了 TLS 握手的服务网格中,每个新连接的成本相对较高。传统上,我们在消费者应用中添加了各种连接池驱动程序。但是 Istio 为连接池提供了开箱即用的支持。connectionPool
配置决定了消费者服务如何连接到提供者服务。这些设置必须由服务所有者进行微调。连接池设置适用于消费者服务的每个主机。
Istio 连接池支持keepAlive
TCP 方法。因此,我们不仅可以使用池,还可以重用未使用的连接。这些设置有一组单独的属性来配置 HTTP 和 TCP 连接池。这些属性使我们能够微调 HTTP 连接重用。以下是最重要的属性。我们不会涵盖所有的属性;请参考 Istio 文档了解更多信息。
-
maxConnections
:该设置定义了服务连接数的上限。默认值设置为 1024。此设置适用于 TCP 和 HTTPv1.0 服务。 -
connectionTimeout
:该设置定义了 TCP 连接超时。 -
Http2MaxRequets
:该设置适用于 HTTPv2.0,在 HTTP 2.0 中我们建立一个连接,并在多个请求中重复使用。这些设置定义了可以通过连接执行的请求数量的上限。 -
Http1MaxPendingRequests
:该设置定义了通过连接挂起的 HTTP 请求数量的上限。这也适用于 HTTPv2.0/GRPC 服务。
我们可以为每个定义的子集配置connectionPool
属性。
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: webapp-destination
spec:
host: webservice
subsets:
- name: v0
labels:
version: v6.0
trafficPolicy:
connectionPool:
tcp:
maxConnections: 100
connectTimeout: 30ms
tcpKeepalive:
time: 7200s
interval: 75s
在前面的代码中,我们已经为 6.0 版本的服务配置了connectionPool
。这些设置配置池化资源的最大数量以及连接和保持活动超时。
注意
值得注意的是,连接池是由特使代理监控的。如果违反了配置的限制,Envoy 将启动以下断路器:
-
upstream_cx_overflow
:当集群中的一个服务超过最大连接数时,抛出这个断路器。这通常适用于 TCP 和 HTTP/1 服务。由于 HTTP/2 重用相同的连接,因此该限制不适用于它。 -
upstream_rq_pending_overflow
:当集群中的服务发出的 HTTP 请求超过配置的限制时,抛出此断路器。这通常适用于 HTTP/2。 -
upstream_rq_retry_overflow
:当集群中的服务发出的 HTTP 请求超过配置的限制时,抛出此断路器。
负载均衡
负载均衡是在选定目的地的不同主机之间分配请求的过程。有各种机制可以实现这一点。可以通过loadBalancer
设置进行配置。Istio 支持以下类型的负载均衡器:
-
循环:随机选择一个主机。如果没有为选定的 pod 启用运行状况检查,这样会更好。
-
最小连接:该方法执行 O(1)查找来确定两个健康的主机。它会选择连接数量最少的一个。
-
随机:随机选择一个主机。如果没有为选定的 pod 启用运行状况检查,它的性能会更好。
-
一致散列:该方法基于请求头或 cookies 配置散列。
因为我们正在运行 web 服务的单个实例,所以我们不会为它配置负载均衡。
离群点检测
异常值检测是确定负载均衡集群中不健康主机的过程。该过程之后是从负载均衡集中删除主机。在 Istio 中,Envoy 断路器用于跟踪由目的主机引起的错误。这些错误可能是由服务或相应的边车引起的。在这两种情况下,主机都将被标记为不健康。
Istio 只记录服务抛出的连续错误。默认值设置为五个连续错误。对于基于 TCP 的服务,连接超时被视为错误。而基于 HTTP 的服务,5xx HTTP 响应也被记录为错误。在记录这些错误时,缺省情况下,Istio 从负载均衡集中驱逐一个服务 30 秒。经过该时间间隔后,主机将回到负载均衡集中,并以 10 秒的间隔重新评估(默认情况下)。可以通过配置各种可用的属性来改变这些计时。
总之,我们已经在DestinationRule
中配置了子集。子集通过匹配配置的选择器来选择节点。Istio 然后对它们应用connectionPool
、负载均衡和异常值检测设置。这些设置可以在DestinationRule
级别进行配置。然后,这些设置将应用于在它下面创建的每个子集。但是如果在子集级别有任何配置,那么它将覆盖DestinationRule
级别的配置。
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: webapp-destination
spec:
host: webservice l
trafficPolicy:
connectionPool:
tcp:
maxConnections: 100
connectTimeout: 30ms
subsets:
- name: v1
labels:
version: v6.2
- name: v0
labels:
version: v6.0
在前面的代码中,我们已经为在webapp-destination DestinationRule
组件中定义的所有子集(v0 和 v1)配置了connectionPool
。这些设置配置池化资源的最大数量和连接超时。如前所述,DestinationRule
组件只有在virtualService
向其发送请求时才有效。在下一节中,我们将介绍如何定义一个virtualService
并使用它的不同配置。
虚拟服务
Istio VirtualService
组件的行为类似于 Kubernetes 中的Service
组件。基本上,VirtualService
是一种抽象,它将请求映射到服务网格中定义的服务。该服务可以是 Kubernetes 服务,也可以是 Istio 定义的服务。使用DestinationRule
执行VirtualService
组件的目的地解析。一个VirtualService
组件可以执行目的地解析来处理以下用例:
-
服务的单一版本
-
基于 HTTP 头的查找以选择服务
-
一组选定服务版本之间的加权比较
VirtualService
抽象将请求路由与应用部署分离开来。因此,在一个集群中,我们可以部署同一服务的许多版本,并以精细控制的方式在它们之间分配负载。
促进
在前面的例子中,我们定义了 v0 和 v1 子集。以下配置将所有请求发送到 v1 子集:
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: webservice-vs
spec:
hosts:
- webservice
http:
- route:
- destination:
host: webservice
subset: v1
前面的配置是VirtualService
最简单的形式之一。它将所有来自虚拟服务的请求只路由到目标服务的特定版本。见图 4-8 。
图 4-8
单个服务
使用以下属性配置VirtualService
行为:
-
hosts
属性定义了请求必须匹配的主机名或主机和端口的列表。主机地址可以是使用 DNS 解析成 FQDN 的服务名(在我们的例子中是 Kubernetes DNS)。它也可以是主机 IP 和端口。 -
请求匹配后,再转发给目的主机的
subset
v1。
我们可以使用下面的代码创建如图 4-8 所示的虚拟服务:
$kubectl create -f ../config/webapp-simple-vs.yaml
之后,通过使用以下命令验证创建的虚拟服务(参见图 4-9 ):
图 4-9
v1 虚拟服务
$istioctl get virtualservices
前面的配置将所有请求发送到 web 服务的 v6.2。让我们通过多次加载前端 web 服务(http://10.152.183.146/
)来验证这一点。我们可以看到我们所有的响应都来自于 webapp 的 6.2 版本(见图 4-10 )。
图 4-10
请求路由到 6.2 版本
重写
在前面的示例中,我们创建了请求转发,但是虚拟服务也能够执行请求重写。此行为使用以下属性进行配置:
-
match
属性定义了哪些请求将执行重写。匹配可以基于 URI、HTTP 报头、查询参数、HTTP 方法、方案等。为了执行重写,我们必须指定 URI 以及其他选择器(如果需要的话)。 -
rewrite
属性定义了请求需要发送到的新 URI 补丁。根据匹配的类型,重写将只替换匹配的 URI 部分。这意味着如果我们匹配 URI 前缀,那么重写只会改变前缀。如果完整的 URI 匹配,那么重写将改变完整的 URI。 -
subset
属性定义了被重写的请求被转发到的目的主机。
以下配置匹配/hello
请求,并将其发送到我们的 web 服务版本 6.2 的/
路径:
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: webservice-rewrite-vs
spec:
hosts:
- webservice
http:
- match:
- uri:
prefix: /hello
rewrite:
uri: /
route:
- destination:
host: webservice
subset: v1
我们可以通过以下方式创建虚拟服务:
$kubectl create -f ../config/webapp-rewrite-vs.yaml
现在有两个虚拟服务在处理webservice
主机。这将会产生问题,所以让我们首先使用以下命令删除之前创建的虚拟服务:
$kubectl delete -f ../config/webapp-simple-vs.yaml
但是我们如何验证我们的改变呢?我们每次都可以更换我们的前端容器。或者,我们可以使用 Kubernetes 提供的exec
命令。exec
命令允许我们在一个容器中执行命令。因此,我们可以执行wget
命令来验证请求路由。
$kubectl exec pod/frontend-deployment-c9c975b4-p8z2t -- wget -O - http://webservice/hello
Defaulting container name to frontend.
Use 'kubectl describe pod/frontend-deployment-c9c975b4-p8z2t -n default' to see all of the containers in this pod.
[6.0]Welcome user! current time is 2019-07-21 07:15:59.395367 Connecting to webservice (10.152.183.230:80)
- 100% |********************************| 62 0:00:00 ETA
HTTP 属性查找
Istio 能够执行 HTTP 属性查找。如前一节所述,match
属性支持 URIs、HTTP 头、查询参数、HTTP 方法、方案等。我们可以匹配前面提到的任何属性,并将请求转发给匹配的主机。
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: webservice-httplookup-vs
spec:
hosts:
- webservice
http:
- match:
- headers:
x-upgrade:
exact: "TRUE"
route:
- destination:
host: webservice
subset: v1
- route :
- destination:
host: webservice
subset: v0
在前面的配置中,我们已经配置了match
属性来寻找一个x-upgrade
头。如果报头可用,那么它被转发到服务的较新版本。我们还添加了一个默认路由,所有不匹配的请求都会转发到该路由。
注意
Istio 配置为match
属性的所有配置部分获取一个字符串。字符串喜欢true
、TRUE
、25 等。被转换为适当的数据类型,因此不能直接传递。这些值可以通过用双引号括起来转换成字符串,就像我们在前面的配置中所做的那样。
让我们应用配置。
$kubectl create -f code/config/webservice-httplookup-vs.yaml
之后,通过使用以下内容验证创建的虚拟服务(参见图 4-11 ):
图 4-11
基于 HTTP 的虚拟服务
$istioctl get virtualservices
让我们确保没有任何之前创建的虚拟服务。
让我们首先在不设置标题的情况下发出一个请求。
$kubectl exec pod/frontend-deployment-c9c975b4-p8z2t -- wget -O - http://webservice/
Defaulting container name to frontend.
Use 'kubectl describe pod/ frontend-deployment-c9c975b4-p8z2 -n default' to see all of the containers in this pod.
[6.0]Welcome user! current time is 2019-07-21 11:19:35.349452 Connecting to webservice (10.152.183.230:80)
现在在wget
命令行中传递适当的头。
$kubectl exec frontend-deployment-c9c975b4-p8z2t -- wget -O - --header='x-upgrade: TRUE' http://webservice/
Defaulting container name to frontend.
Use 'kubectl describe pod/frontend-deployment-c9c975b4-p8z2t -n default' to see all of the containers in this pod.
[6.2]Welcome user! current time is 2019-07-21 11:19:28.565401 Connecting to webservice (10.152.183.230:80)
加权分布
Istio 能够按照配置的比例在不同版本的服务中分发请求。该比率由目的地的weight
属性决定。见图 4-12 。
图 4-12
重量分布服务
以下配置在 v0 和 v1 子集之间分配流量。对于每四个请求,我们希望将其中三个发送到旧版本,一个发送到新版本。
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: webservice-wtdist-vs
spec:
hosts:
- webservice
http:
- route:
- destination:
host: webservice
subset: v1
weight: 25
- destination:
host: webservice
subset: v0
weight: 75
使用以下内容应用配置:
$kubectl create -f code/config/webservice-wtdist-vs.yaml
之后,使用以下命令验证创建的虚拟服务:
$istioctl get virtualservices
让我们确保没有任何之前创建的虚拟服务。参见图 4-13 。
图 4-13
加权分布式虚拟服务
现在做几次wget
请求。我们可以看到,每四个请求中就有一个被路由到 v6.2,而剩下的请求由 v6.0 提供服务。
$kubectl exec pod/frontend-deployment-c9c975b4-p8z2t -it -- sh -il
Defaulting container name to frontend.
Use 'kubectl describe pod/frontend-deployment-c9c975b4-p8z2t -n default' to see all of the containers in this pod.
frontend-deployment-c9c975b4-p8z2t:/# wget -qO - http://webservice
[6.0]Welcome user! current time is 2019-07-22 17:52:24.164478 frontend-deployment-c9c975b4-p8z2t:/#
frontend-deployment-c9c975b4-p8z2t:/# wget -qO - http://webservice
[6.0]Welcome user! current time is 2019-07-22 17:52:28.977615 frontend-deployment-c9c975b4-p8z2t:/#
frontend-deployment-c9c975b4-p8z2t:/# wget -qO - http://webservice
[6.0]Welcome user! current time is 2019-07-22 17:52:33.068721 frontend-deployment-c9c975b4-p8z2t:/#
frontend-deployment-c9c975b4-p8z2t:/# wget -qO - http://webservice
[6.2]Welcome user! current time is 2019-07-22 17:52:41.291074 frontend-deployment-c9c975b4-p8z2t:/#
frontend-deployment-c9c975b4-p8z2t:/#
在这个例子中,我们有一个交互式会话,不像前面的例子,我们在容器中执行命令。交互式会话允许我们一个接一个地执行多个命令。
注意
到目前为止,我们已经将目的地配置为 Kubernetes 服务和基于版本的子集。但是我们也可以在不同的 Kubernetes 服务之间分配请求,而不需要子集定义。在我们的例子中,host
属性可以引用 Kubernetes 服务prod.webservice
和test.webservice
。
金丝雀释放
canary 发布是向一部分用户发布软件的过程。该流程允许开发人员在向整个用户群推广新版本之前,先向一部分用户验证新版本。如果在新版本中发现问题,可以回滚到较小的一组服务器。这有助于减轻影响并提高服务正常运行时间。Kubernetes 还通过管理应用的实例/复制计数来支持 canary 测试。但是这个管理 pod 实例的过程很快变得复杂并且难以支持。另一方面,Istio 对选择请求有丰富的支持,因此可以很容易地完成这项工作。
金丝雀发布是对第一章中讨论的蓝绿色部署的补充。作为一般过程,采取以下步骤:
-
使用蓝绿色部署流程,我们在少量容器上部署新版本。
-
当服务被标记为健康时,我们不是路由所有请求,而是从一定百分比的请求开始。
-
我们继续测试新版本,直到我们对结果满意为止。
-
最后,这一变化被部署在为所有用户服务的整个车队上。
在前面的小节中,我们讨论了简单的请求匹配。match
方法验证一个简单的属性并请求转发。但是对于 canary release 这样相当高级的用例来说,这还不够好。匹配需要处理用 AND/OR 等操作连接在一起的多个子句。Istio match
属性以下列方式支持这两种操作:
-
AND 操作通过在单个
match
属性下嵌套多个条件来执行。 -
OR 运算是通过在单个
match
属性下设置不同的条件来执行的。
注意
在 YAML 语法中,列表的值是通过在每个值前加一个连字符(-)来创建的。这意味着带有前缀连字符的条件是不同的值。
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: webservice-and-or-vs
spec:
hosts:
- webservice
http:
- match:
- headers:
x-upgrade:
exact: "TRUE"
- queryParams:
ver:
exact: v1
method:
exact: GET
route:
- destination:
host: webservice
subset: v1
- route :
- destination:
host: webservice
subset: v0
之前的配置会尝试匹配以下两个条件中的任何一个(或操作):
-
HTTP 头具有正确的值
x-upgrade
。 -
queryString
有ver=v1
和HTTP 方法是 GET。
部署并验证之前的配置。
$kubectl exec frontend-deployment-c9c975b4-p8z2t -- wget -O - --header='x-upgrade: TRUE' http://webservice/
Defaulting container name to frontend.
Use 'kubectl describe pod/frontend-deployment-c9c975b4-p8z2t -n default' to see all of the containers in this pod.
[6.2]Welcome user! current time is 2019-07-21 18:09:25.296747 Connecting to webservice (10.152.183.230:80)
$kubectl exec frontend-deployment-c9c975b4-p8z2t -- wget -O – http://webservice/?ver=v1
Defaulting container name to frontend.
Use 'kubectl describe pod/frontend-deployment-c9c975b4-p8z2t -n default' to see all of the containers in this pod.
[6.2]Welcome user! current time is 2019-07-21 18:10:03.728678 Connecting to webservice (10.152.183.230:80)
因为我们正在制定新的多重规则,所以了解 Istio 如何评估它们是很重要的。Istio 根据规则的声明顺序评估所有匹配规则。首先评估第一个声明的匹配条件。如果条件失败,Istio 将评估下一个条件。在以下配置中,我们在第一个位置添加了默认路由:
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: webservice-httplookup-vs
spec:
hosts:
- webservice
http:
- route :
- destination:
host: webservice
subset: v0
- match:
- headers:
x-upgrade:
exact: "TRUE"
route:
- destination:
host: webservice
subset: v1
让我们部署并验证之前的配置。
$ kubectl exec frontend-deployment-c9c975b4-p8z2t -- wget -O - --header='x-upgrade: TRUE' http://webservice/
Defaulting container name to frontend.
Use 'kubectl describe pod/frontend-deployment-c9c975b4-p8z2t -n default' to see all of the containers in this pod.
[6.0]Welcome user! current time is 2019-07-21 18:19:55.581391 Connecting to webservice (10.152.183.230:80)
这意味着服务所有者必须始终确保首先声明最具体的规则。所有匹配规则都应该从最具体的到一般的进行声明。
生产就绪的 Istio 配置将具有匹配条件、规则优先级和请求的加权分布。因此,对于我们的示例 web 服务,以下配置包括所有这些方面:
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: webservice-canary-vs
spec:
hosts:
- webservice
http:
- match:
- headers:
host:
exact: "user1.com"
route:
- destination:
host: webservice
subset: v1
weight: 10
- destination:
host: webservice
subset: v0
weight: 90
- route :
- destination:
host: webservice
subset: v0
按照之前的配置,除了来自user1.com
的请求之外,所有请求都由 v0 服务提供服务。此外,来自user1.com
的 10%的请求由 v1 web 服务提供服务;所有剩余的都被路由到 v0 版本。
摘要
在本章中,我们使用了VirtualService
和DestinationRule
来执行请求路由。我们通过将多个版本的 webapp 部署到 Kubernetes 集群来开始讨论。接下来,我们确保我们遵循了 Istio 为端口和 pod 规定的命名约定。之后,我们使用目的地规则定义了版本子集。当通过虚拟服务连接时,评估定义的目的地规则。因此,我们为单一服务路由、HTTP 属性路由和加权路由构建了不同的虚拟服务配置。最后,我们看了金丝雀部署。我们发现了该流程如何帮助减少应用停机时间并提高应用稳定性。我们使用多个匹配条件、目的地优先级和加权请求分布构建了 canary 部署示例。本章的重点是服务请求路由。在下一章中,我们将研究如何配置Ingress
、Egress
和ServiceEntry
组件,以便与 Kubernetes 集群之外的世界进行交互。
五、Istio 网关
在前一章中,我们讨论了 Istio 请求路由。我们展示了如何配置虚拟服务和部署规则,以便与应用的不同版本进行交互。到目前为止,我们所有的服务都部署在 Kubernetes 集群中。但是在现实场景中,我们需要与 Kubernetes 集群之外的组件进行交互。有许多适用的用例。例如,在集群内部运行的应用可能会与部署在集群外部的数据库进行交互。类似地,可以在集群中部署用户应用。这些应用需要通过互联网访问。在本章中,我们将展示如何配置 Istio 网关和服务入口,这将使我们能够实现所讨论的行为。
进入
术语入口被定义为入口外观。它是为所有外部发起的请求提供服务访问的位置。使用 Istio 网关配置入口。它是一个边缘组件,用于在集群外部公开服务。它可以用来公开 HTTP 以及 TCP 服务。网关提供 TLS 终止和请求转发等功能。
在大多数生产集群中,网关与 Kubernetes 负载均衡器服务一起配置。在这种情况下,Kubernetes 服务会创建一个基于云的 L4 负载均衡器。负载均衡器有一个公共 IP 地址,Kubernetes 集群外部的世界可以访问该地址。当负载均衡器收到请求时,它会将请求委派给匹配的 Istio 网关。然后,网关使用 Istio 流量路由,并将请求分派给适当的服务版本。Istio 还将必要的遥测技术和安全性应用于网关。见图 5-1 。
Istio 网关可以与 Kubernetes 入口资源相比较,但与入口资源不同,该网关没有配置任何流量路由规则。网关将所有入站流量委托给虚拟服务,并应用相关的路由配置。总之,网关仅在 L4、L5 和 L6 工作。
图 5-1
Istio 入口
现在让我们扩展第四章中的 web 服务例子。之前,在第一章中,我们开发了一个多语言应用,并将其部署到 Kubernetes 集群中。该应用有一个基于 Java 的前端和一个基于 Python 的后端。这两个应用都部署在同一个 Kubernetes 名称空间中。在第四章中,我们扩展了 web 服务应用。我们在 Kubernetes 集群上部署了两个版本的 web 服务。最后,我们使用以下虚拟服务和相关的distinationRules
将请求路由到两个版本:
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: webservice-wtdist-vs
spec:
hosts:
- webservice
http:
- route:
- destination:
host: webservice
subset: v1
weight: 25
- destination:
host: webservice
subset: v0
weight: 75
前面的配置将四个请求中的一个发送到 v1 版本,其余三个发送到 v0 版本。但这仅适用于 Istio 网格内部的服务。如果我们想从外部世界发送请求,我们需要创建下面的ingress
网关:
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: webapp-gateway
spec:
selector:
istio: ingressgateway
servers:
- port:
number: 80
name: http
protocol: HTTP
hosts:
- "*.greetings.com"
之前的网关配置了一个负载均衡器,以允许外部 HTTP 流量*.greetings.com
进入网状网络。接下来,我们必须配置相关的虚拟服务来处理来自已配置网关的请求。
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: webservice-wtdist-vs
spec:
hosts:
- webservice
- webservice.greetings.com
gateways :
- webapp-gateway
http:
##REST REMOVED FOR BREVITY
在前面的配置中,我们修改了虚拟服务webservice-wtdist-vs
来处理webapp-gateway
。这是通过将网关名称添加到gateways
字段来完成的。此外,虚拟服务必须与其配置的主机网关相匹配。它可以是网关支持的通配符的精确匹配或子集。因此,我们将webservice.greetings.com
添加到了主机列表中。
现在我们需要测试配置。为此,我们需要负载均衡器的地址。执行以下命令确定地址(参见图 5-2 ):
图 5-2
入口网关地址
$kubectl get svc istio-ingressgateway -n istio-system
EXTERNAL-IP
值显示负载均衡器的 IP 地址。这适用于基于云的环境,比如 AWS、Azure 等等。在我们的例子中,因为我们已经将应用部署到 Minikube,所以EXTERNAL-IP
列显示为<PENDING>
。在这种情况下,我们可以跳过负载均衡器,使用istio-ingressgateway
的nodePort
地址。在我们的例子中,节点 IP 地址是 Minikube 服务器的地址,端口是 31380。我们可以使用以下方法确定 IP 地址:
$ minikube ip
192.168.1.27
因为我们已经向外界公开了服务,所以我们可以通过在集群外部的主机上执行curl
命令来验证它。由于我们不拥有greetings.com
,我们将无法处理它的任何子域。但是这不是必需的。网关检查Host
报头字段。我们可以使用适当的curl
选项来设置标题字段。
$curl -v -HHost:webservice.greetings.com http://192.168.1.27:31380/
* Trying 192.168.1.27...
* TCP_NODELAY set
* Connected to 192.168.1.27 port 31380 (#0)
> GET / HTTP/1.1
> Host:webservice.greetings.com
> User-Agent: curl/7.58.0
> Accept: */*
>
< HTTP/1.1 200 OK
< content-type: text/html; charset=utf-8
< content-length: 62
< server: istio-envoy
< date: Sun, 04 Aug 2019 08:04:22 GMT
< x-envoy-upstream-service-time: 8
<
* Connection #0 to host 192.168.1.27 left intact
[6.0]Welcome user! current time is 2019-08-04 08:04:22.383137
在前面的输出中,我们可以看到网关匹配头字段并适当地路由请求。我们可以尝试执行几次curl
。我们可以看到,每四个请求中就有一个是由新版本的 web 服务提供的。
[6.0]Welcome user! current time is 2019-08-04 14:18:13.330905
[6.0]Welcome user! current time is 2019-08-04 14:18:13.359514
[6.0]Welcome user! current time is 2019-08-04 14:18:13.381638
[6.2]Welcome user! current time is 2019-08-04 14:18:13.402238
在前一章中,我们为网格中的边车代理配置了一个虚拟服务。但是一旦使用网关配置了虚拟服务,虚拟服务就会从每个 Istio 服务代理中删除。我们可以通过使用第四章中的测试来验证这一点,如下所示:
$kubectl exec pod/frontend-deployment-c9c975b4-p8z2t -it -- sh -il
Defaulting container name to frontend.
Use 'kubectl describe pod/frontend-deployment-c9c975b4-p8z2t -n default' to see all of the containers in this pod.
frontend-deployment-c9c975b4-p8z2t:/# wget -qO - http://webservice/
[6.2]Welcome user! current time is 2019-08-04 16:55:03.230895
frontend-deployment-c9c975b4-p8z2t:/# wget -qO - http://webservice/
[6.0]Welcome user! current time is 2019-08-04 16:55:07.876481
frontend-deployment-c9c975b4-p8z2t:/# wget -qO - http://webservice/
[6.0]Welcome user! current time is 2019-08-04 16:55:12.130989
frontend-deployment-c9c975b4-p8z2t:/# wget -qO - http://webservice/
[6.2]Welcome user! current time is 2019-08-04 16:55:14.911224
在前面的输出中,我们从前端 pod 执行了wget
命令。我们可以看到,在 web 服务的两个版本中,请求是以循环方式处理的。如果我们想要将相同的路由应用到网格中的所有服务代理,我们需要将关键字mesh
添加到gateways
字段中。
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: webservice-wtdist-vs
spec:
hosts:
- webservice
- webservice.example.com
gateways :
- webapp-gateway
- mesh
http:
# REMOVED for BREVITY
我们可以使用之前执行的命令来验证之前的配置。我们的请求应该按照配置的权重来处理。
mesh
是省略gateways
属性时的默认行为。
安全套接字层
Istio 网关为 SSL 交换提供了完整的支持。我们可以在网关中设置 SSL 证书交换。或者,网关可以充当传递介质。这样,SSL 终止可以由运行在 Kubernetes 集群中的 HAProxy 或 Nginx 来处理。
在启用 SSL 之前,我们需要一个证书和一个私钥。在本节中,我们将使用自签名证书。可以使用openssl
生成自签名证书。我们将介绍几个基本步骤,但是认证生成超出了本书的范围。如果您已经有证书,可以继续下一部分。
我们将使用下面的openssl
命令生成一个自签名证书。接下来会出现提示,询问更多的细节。
$openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365
Generating a RSA private key
.................................................++++
...........................................................++++
writing new private key to 'key.pem'
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]: US
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:packt
Organizational Unit Name (eg, section) []:istio-book
Common Name (e.g. server FQDN or YOUR name) []:*.greetings.com
Email Address []:email@greetings .com
让我们确保通用名称有一个*.greetings.com
通配符,这将允许我们为不同的服务使用证书。还有一点需要注意的是,Kubernetes 将无法读取生成的密钥,因为它受密码保护。我们可以使用以下命令删除密码:
$ openssl rsa -in key.pem -out key2.pem
Enter pass phrase for key.pem:
writing RSA key
现在我们有了所需的cert.pem
和key2.pem
文件。我们可以使用它们以不同的方式配置 SSL。
配置 istio-Ingres 网关-证书
Kubernetes 为秘密管理提供了很好的支持。我们可以在集群中创建一个命名的秘密。然后可以用.spec.volumes[].secret.secretName
属性配置一个 pod。Kubernetes 将在 pod 的指定文件位置挂载命名的机密。
apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
containers:
- name: mypod
image: redis
volumeMounts:
- name: foo
mountPath: "/etc/foo"
readOnly: true
volumes:
- name: foo
secret:
secretName: mysecret
在我们的例子中,我们不打算配置istio-ingressgateway
。或者,Istio 网关已经配置了名为secret
的istio-ingressgateway-certs
。因此,我们所需要的就是用这个名字创建一个 Kubernetes 秘密。
$ kubectl create -n istio-system secret tls istio-ingressgateway-certs --key key2.pem --cert cert.pem
secret "istio-ingressgateway-certs" created
我们可以使用下面的方法来验证这个秘密(见图 5-3 ):
图 5-3
秘密研究所
$kubectl describe secret istio-ingressgateway-certs -n istio-system
已经为/etc/istio/ingressgateway-certs
文件路径配置了istio-ingressgateway-certs
机密。这意味着 Kubernetes 在前面指定的路径中挂载密钥和证书文件。现在,我们可以按以下方式配置证书:
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: webapp-gateway
spec:
selector:
istio: ingressgateway
servers:
- port:
number: 443
name: https
protocol: HTTPS
tls:
mode: SIMPLE
serverCertificate: /etc/istio/ingressgateway-certs/tls.crt
privateKey: /etc/istio/ingressgateway-certs/tls.key
hosts:
- "*.greetings.com"
在前面的配置中,我们在端口 443 上配置了 HTTPS 协议。我们还提供了带有密钥的证书。密钥和证书分别被命名为tls.key
和tls.crt
。我们已经将tls.mode
启用为SIMPLE
。这是标准的 SSL 配置,其中网关不验证客户端的身份。这是所有需要的;我们现在可以使用curl
来验证网关。
$for i in 1 2 3 4; do curl -HHost:webservice.greetings.com --resolve webservice.greetings.com:31390:127.0.0.1 -k https://webservice.greetings.com:31390/; echo "; done
[6.0]Welcome user! current time is 2019-08-10 18:16:05.814646
[6.0]Welcome user! current time is 2019-08-10 18:16:05.843160
[6.0]Welcome user! current time is 2019-08-10 18:16:05.872700
[6.2]Welcome user! current time is 2019-08-10 18:16:05.901381
在前面的curl
命令中,除了-HHost
头,我们还使用了几个选项。
-
--resolve webservice.greetings.com:31390:127.0.0.1
:这将把webservice.greetings.com:31390:
设置为localhost
,因为我们正在使用NodePort
。 -
-k
:由于我们已经添加了自签名证书,除非我们启用不安全访问,否则curl
将会失败。
配置 istio-Ingres 网关-ca-证书
到目前为止,我们已经配置了服务器端 TLS。这对最终用户应用来说很好。但是通常需要相互 TLS 认证。可以使用客户端 SSL 证书为相互 TLS 配置 Istio 网关。在这里,证书链也可以加载到名为 Kubernetes secret
的istio-ingressgateway-ca-certs
中。
$ kubectl create -n istio-system secret tls istio-ingressgateway-ca-certs --cert cert.pem
secret "istio-ingressgateway-ca-certs" created
已经为/etc/istio/ingressgateway-ca-certs
文件路径配置了istio-ingressgateway-ca-certs
机密。现在,我们可以按以下方式配置客户端的证书链:
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: webapp-gateway
# REMOVED FOR BREVITY
tls:
mode: MUTUAL
serverCertificate: /etc/istio/ingressgateway-certs/tls.crt
privateKey: /etc/istio/ingressgateway-certs/tls.key
caCertificates: /etc/istio/ingressgateway-ca-certs/ca-chain.cert.pem
hosts:
- "*.greetings.com"
在之前的配置中,我们启用了MUTUAL
TLS 认证。客户端证书链被命名为ca-chain.cert.pem
。我们现在可以使用curl
来验证网关。我们将分别通过使用cacerts
和key
选项来传递客户端证书和密钥。
$for i in 1 2 3 4; do curl -HHost:webservice.greetings.com --resolve webservice.greetings.com:31390:127.0.0.1 --cacerts client.certs.pem --key client.key.pem https://webservice.greetings.com:31390/; echo "; done
[6.0]Welcome user! current time is 2019-08-10 19:16:05.814646
[6.0]Welcome user! current time is 2019-08-10 19:16:05.843160
[6.0]Welcome user! current time is 2019-08-10 19:16:05.872700
[6.2]Welcome user! current time is 2019-08-10 19:16:05.901381
到目前为止,我们已经在网关中配置了 TLS 终端。但是网关也有一个PASSTHROUGH
模式,在该模式下它不执行任何终止。终止的责任被委托给虚拟服务。我们将让您尝试用 Nginx 或 HAProxy 配置 TLS 终端。
外部服务访问
到目前为止,我们已经向外部世界公开了我们的服务。但是,如何使用运行在集群之外的服务呢?在 Istio 网格内部运行的服务可以访问集群外部的服务。默认的 Istio 配置不对外部服务访问施加任何限制。这看起来像是一个简单的默认设置,但它可能不是我们想要的。企业通常需要监控和控制组织外部的流量。以前,我们看到所有流量都流经 sidecar 代理。因此,根据网格的配置方式,可以完成以下任务:
-
允许/拒绝所有外部访问
-
允许访问有限的服务
-
控制权限以允许访问
默认情况下,Istio 配置为ALLOW_ANY
模式。对于网格未知的所有服务,该配置将绕过代理。在这种模式下,请求不会被发送到边车。相反,它们由应用 pod 网络直接处理。见图 5-4 。
图 5-4
边车旁路
我们可以使用以下命令来验证 Istio 模式:
$kubectl get configmap istio -n istio-system -o yaml |grep -o "mode: .*"
mode: ALLOW_ANY\n\nlocalityLbSetting:\n {}\n \n\n# The namespace to treat
我们现在可以尝试从我们的一个网状节点访问维基百科。我们可以执行wget
命令来确定行为。
$ kubectl exec pod/frontend-deployment-c9c975b4-p8z2t -it -- sh -il
frontend-deployment-c9c975b4-p8z2t:/# wget -qSO - http://en.wikipedia.org/ >/dev/null
HTTP/1.1 301 Moved Permanently
Date: Sun, 11 Aug 2019 14:36:07 GMT
Content-Type: text/html; charset=utf-8
我们可以看到,我们能够使用 301 响应代码到达en.wikipedia.org
。现在我们把模式改成REGISTRY_ONLY
。Istio 配置存储在名为configmap
的istio
中。让我们更新它并检查其行为。
$ kubectl get configmap istio -n istio-system -o yaml | sed 's/mode: ALLOW_ANY/mode: REGISTRY_ONLY/g' | kubectl replace -n istio-system -f -
configmap/istio replaced
$ kubectl exec pod/frontend-deployment-c9c975b4-p8z2t -it -- sh -il
Defaulting container name to frontend.
frontend-deployment-c9c975b4-p8z2t:/# wget -qSO - http://en.wikipedia.org/
HTTP/1.1 502 Bad Gateway
wget: server returned error: HTTP/1.1 502 Bad Gateway
我们可以看到该位置已无法访问。它返回 502 响应,表示需要查看代理配置。我们需要控制对有限地点的访问。我们可以使用以下组件对此进行配置。
辅助平巷
ServiceEntry
是一种可以在 Istio 服务注册中心配置网格外部服务的方式。配置外部运行的业务组件通常很有帮助。配置服务条目后,Istio 将监控服务的所有流量。见图 5-5 。
图 5-5
辅助平巷
让我们继续之前的示例,并启用对en.wikipedia.org
的访问。我们可以使用以下配置来配置其服务入口端点:
---
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
name: wikipedia
spec:
hosts:
- en.wikipedia.org
ports:
- number: 443
name: https
protocol: HTTPS
resolution: DNS
location: MESH_EXTERNAL
先前的配置具有以下属性:
-
spec.hosts
:该属性定义了使用该服务条目配置的主机列表。 -
spec.ports
:该属性定义配置的端口。 -
spec.resolution
:该属性定义了需要如何进行地址查找。如果在配置中定义了主机 IP 地址,它可以是基于 DNS 的,也可以是静态的。 -
spec.location
:该属性定义了服务所在的位置。服务地点可以定义为INTERNAL
或EXTERNAL
。在EXTERNAL
服务的情况下,Istio 禁用相互 TLS 行为。
现在将配置应用到 Kubernetes 集群。我们应该被允许访问维基百科。
$ kubectl create -f config/service-entry.yaml
serviceentry.networking.istio.io/wikipedia configured
virtualservice.networking.istio.io/wikipedia configured
$ kubectl exec pod/frontend-deployment-c9c975b4-p8z2t -it -- sh -il
Defaulting container name to frontend.
frontend-deployment-c9c975b4-p8z2t:/# wget -qSO - https://en.wikipedia.org/
HTTP/1.1 302 Found
Date: Sun, 11 Aug 2019 16:44:19 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 0
Connection: close
........
frontend-deployment-c9c975b4-p8z2t:/# wget -qSO - http://en.wikipedia.org/
HTTP/1.1 502 Bad Gateway
wget: server returned error: HTTP/1.1 502 Bad Gateway
我们可以看到,只允许访问 HTTPS 服务。对 HTTP 服务的访问失败,出现错误。我们可以有以下配置,这将允许访问两种协议:
---
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
name: wikipedia
spec:
hosts:
- en.wikipedia.org
ports:
- number: 80
name: http
protocol: HTTP
- number: 443
name: https
protocol: HTTPS
resolution: DNS
location: MESH_EXTERNAL
出口
在上一节中,我们限制了对一组有限的外部服务的访问。但是网格中运行的所有服务仍然可以连接到可用的外部服务。有时,企业要求必须评估所有外部流量,以确保其符合授权规则。必须对每个请求进行检查,以限制只有经过授权的人才能访问。此外,网状网络外的所有流量都必须从受监控的单一位置通过。Istio 为此定义了一个出口网关。这是一个可以拦截离开服务网格的流量的组件。参见图 5-6 。
图 5-6
出口
在前面的例子中,我们配置了对en.wikipedia.org
的直接访问。现在,在以下配置中,我们已经定义了出口主机并截获了其中的所有请求:
---
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: wikipedia-egressgateway
spec:
selector:
istio: egressgateway
servers:
- port:
number: 80
name: http
protocol: HTTP
hosts:
- en.wikipedia.org
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: wiki-egress-gateway
spec:
hosts:
- en.wikipedia.org
gateways:
- wikipedia-egressgateway
- mesh
http:
- match:
- gateways:
- mesh
port: 80
route:
- destination:
host: istio-egressgateway.istio-system.svc.cluster.local
port:
number: 80
weight: 100
- match:
- gateways:
- wikipedia-egressgateway
port: 80
route:
- destination:
host: en.wikipedia.org
port:
number: 80
weight: 100
---
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
name: wikipedia
spec:
hosts:
- en.wikipedia.org
ports:
- number: 80
name: http
protocol: HTTP
resolution: DNS
location: MESH_EXTERNAL
之前的配置充分利用了 Istio 路由功能。我们在其中定义了以下行为:
-
对
en.wikipedia.org
的服务条目进行配置,以便网格内部的服务可以访问它。 -
我们定义了
wikipedia-egressgateway
,它可以处理匹配指定主机和端口的请求。 -
我们定义了一个
wiki-egress-gateway
虚拟服务。服务是两个组件之间的粘合剂。它针对以下内容进行了配置:-
虚拟服务处理对
en.wikipedia.org
的所有请求。它适用于网关和所有边车代理。 -
源自边车的请求被路由至
istio-egressgateway
。网关部署在istio-system
名称空间中。 -
网关为
wikipedia-egressgateway
匹配这些传入的请求。然后,虚拟服务将网关发出的请求路由到服务入口主机。
-
这样,所有发往en.wikipedia.org
的流量都会被istio-egress
网关捕获。让我们应用配置并使用wget
命令进行测试,如下所示:
$ kubectl create -f config/egress-gateway.yaml
gateway.networking.istio.io/wikipedia-egressgateway created
virtualservice.networking.istio.io/wiki-egress-gateway created
serviceentry.networking.istio.io/wikipedia created
$kubectl exec pod/frontend-deployment-c9c975b4-p8z2t -it -- sh -il
frontend-deployment-c9c975b4-p8z2t:/# wget -qSO - http://en.wikipedia.org/
HTTP/1.1 301 Moved Permanently
date: Sun, 11 Aug 2019 17:53:52 GMT
server: envoy
......
我们可以查看egressgateway
中的日志。它应该已经捕获了请求。
$microk8s.kubectl logs -l istio=egressgateway -c istio-proxy -n istio-system
......
[2019-08-11T17:53:52.214Z] "GET / HTTP/2" 301 - "-" "-" 0 0 395 355 "10.1.1.8" "Wget" "e2766b89-6b38-9744-9b02-fe9a32c6deea" "en.wikipedia.org" "103.102.166.224:80" outbound|80||en.wikipedia.org - 10.1.1.7:80 10.1.1.8:49880 -
摘要
在这一章中,我们向外部世界展示了我们的 Kubernetes 集群。一开始,我们定义了入口网关,以允许外部客户端连接到集群中运行的服务。我们还在边缘网关上配置了 SSL 终端。接下来,我们试图控制对运行在集群之外的服务的访问。我们将默认的ALLOW_ANY
策略修改为RESTRICTED_ONLY
。接下来,我们使用服务条目配置对 Istio 服务的访问。服务条目有助于监控外部连接。Istio 提供了出口网关,以便对服务条目定义的服务进行日志记录和访问控制。最后,我们使用出口网关来控制服务入口访问。
六、服务弹性
在前一章中,我们介绍了如何配置Ingress
、Egress
和ServiceEntry
组件来与 Kubernetes 集群之外的世界进行交互。我们还讨论了如何加密微服务之间的请求。因此,现在我们可以在 Kubernetes 上部署一个安全的应用,能够控制与外部网络的交互。在理想的情况下,这种设置应该足以运行一个生产应用,但正如 Peter Deutsch 和 Sun Microsystems 的人员正确指出的那样,开发人员往往会忘记分布式计算的谬误。
在开发应用时,人们必须注意分布式系统中的谬误,并解决这些谬误。
-
假设网络是可靠的。这种假设导致对网络错误很少甚至没有错误处理的发展。其结果是网络问题、应用停滞和响应时间长。网络恢复后,暂停的应用可能无法恢复正常功能,可能需要重新启动。
-
当使用网络作为交流的渠道时,所有的反应都是自发的;换句话说,操作中不会引入延迟。
-
可用于通信的带宽没有上限。在现实世界中,如果超过带宽阈值,服务将无法通信。
图 6-1 显示了分布式系统中的挑战。
图 6-1
分布式系统中的挑战
此外,如果其中一个服务节点关闭或没有响应,即使其他服务节点工作正常,也会出现故障。这会导致一些请求失败,并影响最终用户。要解决这些情况,必须执行以下操作:
-
让应用处理服务中的网络故障,使其能够通过网络恢复进行恢复。
-
如果延迟增加,应用应该能够适应,并且最终不会影响最终客户。
-
在带宽阻塞或其他节点故障的情况下,服务应该重试,或者有一个类似于网络中断场景的处理程序。
早期,开发人员使用流行的框架,如 CORBA、EJB、RMI 等,使网络调用看起来像本地方法调用,但这使系统容易受到级联故障的影响,其中一个服务故障传播到所有调用服务。Istio 通过 sidecars 提供弹性实现,帮助开发人员关注业务逻辑。
应用设置
让我们继续第四章和第五章的例子。见图 6-2 。
图 6-2
Istio 示例应用
让我们快速浏览一下描述istio-frontent
部署和服务、webapp 部署和服务的配置。见清单 6-1 ,清单 6-2 ,清单 6-3 ,清单 6-4 。
apiVersion: v1
kind: Service
metadata:
name: frontendservice
spec:
selector:
app: frontend
ports:
- protocol: TCP
port: 80
targetPort: 8080
Listing 6-4frontend-service.yaml to Expose Front-End Deployment
apiVersion: v1
kind: Service
metadata:
name: webservice
spec:
selector:
app: webapp
ports:
- name: http-webservice
protocol: TCP
port: 80
targetPort: 5000
Listing 6-3webapp-service.yaml to Expose Web App Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend-deployment
labels:
app: frontend
spec:
replicas: 1
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: frontend
image: frontend-app:1.0
imagePullPolicy: Never
ports:
- containerPort: 8080
Listing 6-2frontend-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp-deployment-7.0
labels:
app: webapp
version: v7.0
spec:
replicas: 2
selector:
matchLabels:
app: webapp
version: v7.0
template:
metadata:
labels:
app: webapp
version: v7.0
spec:
containers:
- name: webapp
image: web-app:7.0
imagePullPolicy: Never
ports:
- containerPort: 5000
Listing 6-1WebApp-deployment-v7.yaml
现在一个目的地规则作为中间层与webapp-service
交互。它有允许跨单元分配流量的策略。参见清单 6-5 。
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: webapp-destination
spec:
host: webservice
subsets:
- name: v0
labels:
version: v7.0
Listing 6-5Destination-rule.yaml
现在定义一个网关,使前端服务可以在 Minikube 中的 Kubernetes 集群之外访问。如果没有这一点,只能通过私有 IP 或域名在私有网络内部访问 pod 和服务。参见清单 6-6 。
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: webapp-gateway
spec:
selector:
istio: ingressgateway # use istio default controller
servers:
- port:
number: 80
name: http
protocol: HTTP
hosts:
- "*"
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: frontendservice
spec:
hosts:
- "*"
gateways:
- webapp-gateway
http:
- match:
- uri:
prefix: /
route:
- destination:
host: frontendservice
Listing 6-6gateway.yaml
部署服务后,当我们使用 Minikube IP 访问网关时的输出如图 6-3 所示。
图 6-3
应用输出
我们现在有一个正在运行的应用;让我们看看根据前面解释的谬误,什么都可能出错。
-
webapp 服务的一个节点关闭。前端服务调用 webapp 服务,并看到
UNKNOWN ERROR
的响应。 -
webapp 节点的响应时间比预期的长。这可能是因为网络速度慢或节点本身有问题。前端服务等待未收到响应的时间。
-
一个 webapp 节点现在过载了比预期更多的请求,并且新的请求不断到来,这使得问题更加复杂。
我们如何在一个环境中解决这些问题?
-
如果前端服务收到一个错误,指出该服务已关闭,请重试请求,并检查是否有其他节点可以处理相同的请求。
-
前端服务应该在请求中加入超时,避免用户长时间等待。之后,它可以根据产品流程重试请求或向用户显示错误,而不会浪费用户的时间。
-
如果一个节点或服务过载并反复返回错误,应该给它一些时间来冷却或恢复。为了实现这一点,进一步的传入请求应该直接返回一个错误,而不是实际发送给服务。这个过程被称为断路,类似于我们在家庭中防止电器永久损坏的方法。
让我们看看这些场景,并弄清楚 Istio 如何帮助解决这些挑战。
负载均衡
负载均衡是一个常见的概念,意思是在几个节点之间分配负载,以提高吞吐量和效率。负载均衡器是所有请求进入的节点,它将负载转发/代理到分布式节点。虽然这看起来是一个好方法,但是它创建了一个单点故障,而这正是我们最初试图避免的。这也造成了瓶颈,因为所有的请求都是通过这个入口点路由的。
Istio 带有客户端负载均衡的概念。请求服务可以根据负载均衡标准决定将请求发送到哪里。这意味着没有单点故障和更高的吞吐量。Istio 支持以下负载均衡技术:
-
循环调度:请求一个接一个地均匀分布在所有节点上。
-
Random :随机挑选一个节点为请求服务。它最终变得类似于循环赛,但没有任何顺序。
-
加权:可以给实例增加权重,请求可以根据百分比转发。
-
最少请求:这似乎是一种有效的技术,但取决于用例。它将请求转发给到那时为止接收到最少请求的节点。
Istio 使用平台的服务发现特性来获取新节点的详细信息,并将其存在分发到其余节点。其余节点在其负载均衡中包括新服务。让我们使用 webapp 4.0 来看看负载均衡器的运行情况。配置参见清单 6-7 。
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp-deployment-4
labels:
app: webservice
version: v4
spec:
replicas: 1
selector:
matchLabels:
app: webservice
version: v4
template:
metadata:
labels:
app: webservice
version: v4
spec:
containers:
- name: webapp
image: web-app:4.0
imagePullPolicy: Never
ports:
- containerPort: 5000
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: webservice
spec:
host: webservice
subsets:
- name: v0
labels:
version: v7.0
- name: v1
labels:
version: v4
trafficPolicy:
loadBalancer:
simple: ROUND_ROBIN
---
apiVersion: v1
kind: Service
metadata:
name: webservice
spec:
selector:
app: webservice
ports:
- name: http-webservice
protocol: TCP
port: 80
targetPort: 5000
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: webservice
spec:
hosts:
- "*"
gateways:
- webapp-gateway
http:
- route:
- destination:
host: webservice
match:
- uri:
prefix: /
Listing 6-7Deployment Configuration and Load Balancing Config
图 6-4 显示了服务上请求分布的输出。
图 6-4
循环负载均衡
因为循环平衡已经完成,所以在大多数情况下,交替请求被发送到每个版本。
重试请求
当服务调用由于延迟或服务中的临时故障而失败时,最终用户会看到一个错误,对此我们假设用户可能会重试请求。在微服务架构中,这种重试在网络调用的每一层都会成倍增加。随着用户重试请求,每一层的请求失败的组合会增加。在图 6-2 中,假设网关和前端服务之间的调用失败,前端服务和 webapp 服务之间还有第二次失败的机会。依靠用户不放弃直到没有失败是不现实的期望。解决方案是在每次网络调用时建立自动重试。
在微服务架构中,可以在每个网络调用中加入重试和超时,这将增加开发工作负载和编码时间,并且与业务逻辑无关,也可以让网络层来处理故障。
让我们在我们的 webapp 服务中做一个小小的改变,随机返回 503 错误代码,这表明服务已经关闭。如果服务过载,无法接受新的请求,并且对一些现有请求失败,就会发生这种情况。有关变更,请参考清单 6-8 。
from flask import Flask
import datetime
import time
import os
import random
app = Flask(__name__)
@app.route("/")
def main():
currentDT = datetime.datetime.now()
status = 200
if random.random() > 0.5:
status = 503
return "[{}]Welcome user! current time is {} ".format(os.environ['VERSION'],str(currentDT)), status
@app.route("/health")
def health():
return "OK"
if __name__ == "__main__":
app.run(host='0.0.0.0')
Listing 6-8Change in Web App Service to Return Error in About 50 Percent of Cases
现在让我们在网关上发送一些流量,看看结果。我们将使用siege
对应用进行连续请求。见图 6-5 。
图 6-5
如果服务节点返回错误,则请求输出
服务的停机时间或故障会传播给用户。只需再次尝试失败的请求,就可以提高可用性。让我们通过 Istio VirtualService
组件来实现这一点,而不是修改代码。VirtualService
允许我们在失败的情况下重试请求。默认情况下,如果请求失败,Envoy 会重试一次。让我们按照清单 6-9 添加这个配置。
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: webservice
spec:
hosts:
- webservice
http:
- route :
- destination:
host: webservice
retries:
attempts: 1
Listing 6-9Changing webapp-virtualservice to Allow One Retry
只需使用istioctl
应用配置,让我们再次发送相同的流量,并查看发送给用户的失败请求。
如图 6-6 所示,可用性从 40%提高到 80%。我们可以进一步增加重试次数,从而减少失败次数,但这是以响应时间为代价的。每一个失败的请求都需要时间,这个时间会被添加到调用服务的总响应时间中。见图 6-7 。
图 6-7
调用服务的响应时间随着每次重试而增加
图 6-6
通过重试,最终用户可以获得更好的可用性
虽然重试似乎是提高可用性的好方法,但显然在某些情况下应该避免重试。
-
重试应该是等幂的。如果响应会根据请求计数发生变化,则应避免重试请求。
-
如果已知一个请求要花很多时间,换句话说,是一个昂贵的请求,那么应该避免重试。这也可能导致下一个请求失败,并可能导致额外资源的消耗。
我们对代码进行了修改,以测试应用在一个微服务中承受错误的持久性。有一种更简单的方法,通过故障注入。我们可以配置虚拟服务,故意插入一个错误来测试持久性。让我们将 webapp 代码恢复到以前的版本,并使用VirtualService
在服务中注入一个错误。清单 6-10 显示中止注入迫使 50%的请求失败。
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: webservice
spec:
hosts:
- webservice
http:
- fault:
abort:
httpStatus: 503
percent: 50
route :
- destination:
host: webservice
retries:
attempts: 0
Listing 6-10Modified VirtualService Component to Forcefully Inject Fault
使用siege
故意使重试次数为零,以证明失败的请求。图 6-8 显示了失败请求的输出。
图 6-8
使用 Istio 故障注入的围攻响应
这里的失败次数超过了 50 %,因为对 webapp 的请求也是从代码内部失败的。将重试次数更改为 1 会提高应用的可用性。
虽然重试可以解决当服务的一些 pod 不响应或关闭时的问题,但它会尝试使用可用的 pod 来服务请求,但是如果 pod 过载并将请求放入队列中,会发生什么情况呢?结果可能是响应在很长时间后才提供,或者调用服务在很长时间后收到错误。在这两种情况下,最终用户都会受到响应延迟的影响。在用户放弃之前,超时成为一个重要因素。
超时请求
超时是使系统可用的一个重要因素。在对服务的网络调用过程中,如果调用花费了大量时间,则很难确定服务是关闭了,还是速度太慢或过载了。在这种情况下,调用服务不能无所事事地等待请求完成,因为最终用户会受到这种延迟的影响。另一种方法是快速失败,而不是让用户等待。Istio 提供了一个特性,如果响应时间超过一个阈值,请求就会超时。
让我们在 webapp 服务中注入一个错误,将 50%的请求的响应时间增加到 5 秒以上。清单 6-11 显示了修改后的VirtualService
配置。
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: webservice
spec:
hosts:
- webservice
http:
- fault:
delay:
fexedDelay: 5s
percent: 50
route :
- destination:
host: webservice
retries:
attempts: 0
Listing 6-11Modified Virtual Service to Forcefully Inject a Delay for Some Requests
请求的平均响应时间增加到了 2.5 秒,这使得 50%的用户需要等待 5 秒钟才能得到响应。图 6-9 显示了使用siege
时的性能。如果服务一直在等待响应,这是一种常见的情况。
图 6-9
延迟喷射围攻响应
一种方法是,如果在一秒钟内没有收到响应,就让请求超时。由于我们在 webapp 服务中注入了一个故障,我们将在前端服务中添加一个超时。修改后的VirtualService
配置看起来像清单 6-12 。
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: frontendservice
spec:
hosts:
- "*"
gateways:
- webapp-gateway
http:
- match:
- uri:
prefix: /
route:
- destination:
host: frontendservice
timeout: 1s
retries:
attempts: 0
Listing 6-12Modified Front-End Virtual Service to Set Timeout to 1 Second
应用的siege
输出显示了一些错误,但是响应时间的上限是一秒。见图 6-10 。
图 6-10
超时一秒的围攻响应
我们已经成功地满足了一个标准,即用户不必等待响应。让我们尝试一下用户收到 OK 响应的标准。为此,只需再次重试失败的请求。请注意,这意味着错误响应时间现在将增加到 1.5 秒,或每次尝试 0.5 秒。配置的变化如清单 6-13 所示。
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: frontendservice
spec:
hosts:
- "*"
gateways:
- webapp-gateway
http:
- match:
- uri:
prefix: /
route:
- destination:
host: frontendservice
port:
number: 80
retries:
attempts: 1
perTryTimeout: 0.5s
Listing 6-13Modified Web App Virtual Service to Set Timeout to 1 Second and Add Retries to 1
图 6-11 显示了所有请求成功的siege
结果,但在少数情况下,这是以三次命中为代价的。从图中很容易看出,任何响应时间接近 1.5 秒的请求都被点击了三次。
图 6-11
围攻响应,重试三次,每次尝试超时 0.5 秒
随着点击量的不断增加,我们保证用户得到响应但在供给端;换句话说,提供响应的服务可能必须处理比预期多得多的请求。有些情况下,服务会因请求而过载,并且可能会因为连续的请求而开始失败。即使超时使最终用户免于面对问题,即使有超时,服务也继续处理请求,导致资源的进一步消耗。在这种情况下,服务总是会因请求而过载,如果应用整天都在使用,可能永远无法恢复。
为了解决这个问题,服务需要一段冷却时间来完成所有挂起的请求(即使超时),并开始服务新的请求。这是使用断路器完成的。
断路器
断路器在电器中很常见。它确保任何一个设备都不会透支电流。过电流会导致电路发热,并可能导致火灾和整体故障。为了避免这种情况,断路器切断过电流设备的电源。
在微服务架构中,最常见的问题是服务故障的级联。如果服务由于某种原因没有响应,重复向服务发送请求会增加延迟,并给服务带来不必要的负载。断路器允许过载服务在开始保留新请求之前获得一些冷却时间。图 6-12 显示了断路器动作前后的请求行为。
图 6-12
断路器动作
当服务 C 中连续故障的数量超过阈值时,断路器跳闸,并且在一段时间内,对服务 C 的所有呼叫立即失败。一段时间后,允许几个请求通过电路,以测试服务 C 是否已恢复。如果请求成功,服务 C 被恢复,或者服务在另一段时间内保持电路断开状态。图 6-13 显示了断路器断开后服务的恢复。
图 6-13
服务恢复后释放断路器
软件中的断路器可以使用流行的客户端断路器库来实现。如果 Java 是开发语言,客户端断路器的一个例子是网飞库 Hysterix,但同样,这需要开发人员负责应用内的断路器,这与应用逻辑没有太大关系。同时,对于多语言应用,它需要以多种语言实现。Istio 抽象出断路器,并使用特使配置来处理该过程。Envoy 在网络层实施断路,而不是在应用代码层。
Istio 在连接池级别和负载均衡器级别实现了断路器。
连接池断路器
在每次调用时创建到每个服务的新连接可能是一个开销很大的过程。它需要创建一个套接字,协商安全参数,然后通过网络进行通信并安全地关闭连接。保持连接池可以减少每次创建新连接的昂贵过程,而不是对每个请求都这样做。Envoy 为 Istio 提供了开箱即用的解决方案;换句话说,它在诸如 HTTP/1.1 和 HTTP/2 之类的有线协议之上支持一个抽象连接池。
HTTP/1.1 连接池
它创建的连接数量达到配置中规定的阈值。当可用时,请求被绑定到连接。连接的可用性可以基于现有连接变得空闲或新连接的产生,因为连接的数量仍然低于配置的阈值。如果一个连接断开,将建立一个新的连接来替换它。
HTTP/2 连接池
它创建了到上游主机的单一连接,并要求所有请求都通过它进行多路传输。如果主机重置连接或连接达到其最大流限制,池将创建一个新连接并释放以前的连接。
Istio 在特使层抽象了上述池,并优化了连接。现在让我们看一个例子,看看断路器的作用。
我们将创建 webapp 服务的新版本,并在网格中创建它的新部署。为了区别于旧版本,我们给代码增加了 0.5 秒的延迟。清单 6-14 向服务引入了新方法,这将向服务添加一个错误。
from flask import Flask
import datetime
import time
import os
import random
app = Flask(__name__)
global status
status = 200
@app.route("/")
def main():
currentDT = datetime.datetime.now()
if (status == 200):
time.sleep(0.5)
return "[{}]Welcome user! current time is {} ".format(os.environ['VERSION'],str(currentDT)), status
@app.route("/health")
def health():
return "OK"
@app.route("/addfault")
def addfault():
global status
if (status == 200):
status = 503
else:
status = 200
return "OK"
if __name__ == "__main__":
app.run(host='0.0.0.0')
Listing 6-14Addition of a Fault to the Webapp Application
清单 6-15 展示了新的部署。
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: frontendservice
spec:
hosts:
- "*"
gateways:
- webapp-gateway
http:
- match:
- uri:
prefix: /
route:
- destination:
host: frontendservice
port:
number: 80
retries:
attempts: 1
perTryTimeout: 0.5s
Listing 6-15Webapp-deployment-v7.1.yaml
让我们更改目的地规则以适应 7.0 版和 7.1 版(参见清单 6-16 )。
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: webservice
spec:
host: webservice
subsets:
- name: v0
labels:
version: v7.0
- name: v1
labels:
version: v7.1
Listing 6-16Destination rule modified to add v7.1
有了这些变化,让我们使用siege
来检查应用的性能。参见图 6-14 。
图 6-14
7.0 版和 7.1 版 webapp 的应用性能
我们很容易区分出 7.1 版是在 0.5+秒内响应的版本。在实验中,所有的调用似乎都是成功的,但在实际环境中,0.5 秒的延迟可能会不断堆积,如果并发用户增加,会使调用服务(前端服务)保持在队列中。这将增加服务的响应时间,导致我们在前面的步骤中配置的超时,但同时仍然让服务处理请求。因此,假设在一定数量的响应失败后,服务将失败,我们可以配置一个断路器来防止服务受到它无法处理的进一步请求的轰炸。让我们创建一个规则来演示请求的失败,并查看断路器的运行情况。
清单 6-17 显示了一个目的地规则,将连接数和每个连接的最大请求数限制在 7.1 版。
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: webservice-circuitbreaker
spec:
host: webservice
subsets:
- name: v1
labels:
version: v7.1
trafficPolicy:
connectionPool:
tcp:
maxConnections: 1
http:
http1MaxPendingRequests: 1
maxRequestsPerConnection: 1
Listing 6-17Destination Rule for v7.1 Restricting Number of Connections
有了这种配置,我们将用并发请求轰炸服务。这种限制的结果如图 6-15 所示。
图 6-15
用四个并发请求轰炸服务
与之前的结果相比,我们的大多数请求都在 0.1 秒内完成。从用户的角度来看,错误的数量会增加,但是服务会有一段冷却时间来稳定在阈值以下。在我们的例子中,因为我们已经将最大连接数限制设置为 1,所以我们很容易在示例中演示这一点。
负载均衡器断路器
到目前为止,我们已经看到了在低性能的情况下,服务是如何避免流量轰炸的,但是这增加了用户得到的错误的数量。只要这是一个暂时的故障,这可能是好的,但是如果故障持续很长时间,最终用户将继续收到这些错误。在这种情况下,解决方法应该是从集群中删除服务节点,直到它恢复。Istio 会尝试检测表现突出的节点或异常值,并将其从负载均衡器中移除。
让我们重新配置我们的目的地规则,添加对异常值的检查,并在需要时从负载均衡器中删除它。清单 6-18 显示了修改后的配置。
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: webservice-circuitbreaker
spec:
host: webservice
subsets:
- name: v1
labels:
version: v7.1
trafficPolicy:
connectionPool:
tcp:
maxConnections: 1
http:
http1MaxPendingRequests: 1
maxRequestsPerConnection: 1
outlierDetection:
baseEjectionTime: 10s
consecutiveErrors: 1
interval: 1s
maxEjectionPercent: 100
Listing 6-18Destination Rule for v7.1 Adding Configuration to Remove Outliers
在这种配置中,如果出现一个以上的连续错误,就会弹出故障节点。它以间隔的频率不断检查节点是否回来。如果需要的话,我们还允许删除服务的所有副本。参见图 6-16 了解服务的故障添加和完整结果。现在离群点已经被剔除,每个请求的最大响应时间不到 0.15 秒。
图 6-16
从负载均衡器中移除异常值,所有请求都将转到 webapp 服务的 7.0 版
断路器本身足以挽救灾难,但不能解决分布式系统的全部问题。
跳回
系统的整体架构不仅要服务于最终用户的请求,还要保持应用正常运行以满足未来的请求。将所有这些 Istio 特性结合起来,您将获得一个稳定的系统,如下所示:
-
最终用户请求服务进行响应。如果响应需要很长时间,请求就会超时。
-
一旦请求超时,不是让最终用户重试请求,而是在每个网络跳重试请求。这一次,请求应该发送到不同的 pod,假设之前的那个 pod 可能出现了临时故障。
-
通过负载均衡将进一步的请求分发到服务的不同单元,确保没有一个单元过载。
-
因为使用了客户端负载均衡器,所以负载均衡器不会过载。如果客户端宕机,其副本可以在此期间接管,直到产生替代客户端。
-
如果其中一个节点没有响应,使用断路器给它一段冷却时间,同时调用服务可以尝试向另一个节点发出请求。
-
如果冷却期不足,则从服务池中退出该节点,以避免在服务恢复之前的任何未来请求。
摘要
在本章中,我们讨论了 Istio 弹性。我们看到了重试和超时是如何对最终用户隐藏应用中的错误和延迟的。负载均衡很重要,因为对同一实例的重试可能会再次失败。客户端负载均衡具有无单点故障的概念,可防止对指向单个节点的请求进行节流以实现平衡。断路器和连接池试图使应用服务保持健康状态,避免它们过载和网络连接开销。在下一章中,我们将使用 Grafana 和 Prometheus 等工具来研究应用度量和监控。
七、应用指标
在前一章中,我们介绍了 Istio 提供的弹性,通过将代码中的复杂性抽象到 Envoy 代理来提高服务的可用性和持久性。在生产环境中,总有失败的时候。这可以通过从系统收集的数据来确定,然后可以相应地准备这些数据。度量收集是任何系统的重要部分,但是在分布式系统中,很难简单地读取数据并快速理解数据。开源工具有助于数据的收集和可视化。我们将在这一章中讨论其中的几个。
应用监控
对于应用的不同用例,监控是主观的。对于一个静态网站,仅仅检查网站是否在运行就足够了。这可以通过 Pingdom 等流行的服务提供商来实现,因为网站是公开的。而对于使用微服务架构运行的 web 应用,包括数据库在内的许多服务可能不是公共的。对于这样的场景,外部检查可能会说 web 应用没有运行,但是不能确定故障点。在这种情况下,对哪个服务失败进行内部检查可能会更有帮助。这就是为什么我们需要在运行应用的私有网络中使用监控工具。我们在本书中一直使用的 K8s 提供了失败服务的自动恢复。图 7-1 显示了不同类型应用中的监控机制。
图 7-1
静态网站的监控和分布式系统的监控
识别和修复故障组件是好的,但是这个过程会造成一些停机时间。如今,所有应用都以 99.9%的可用性为目标,这可以通过采取预防措施而不是事后补救系统故障来实现。在任何应用进入生产环境之前,我们都要处理负载能力,并确定系统可能出现故障的阈值。尽管如此,仍然存在系统可能由于未知情况而失败的情况。记住这一点,开发人员获取服务的基本指标,如请求数量、响应时间、响应状态等。,以及运行服务的节点的基本指标,如 CPU 利用率和内存消耗。负载能力有助于确定与请求和响应相关的阈值,当节点过载或内存不足时,会有一些标准。一旦我们有了这些指标和阈值细节,我们就可以通过收集和分析这些指标来监控应用。基于分析,我们可以确定应用何时会失败。
当应用即将失败时,给开发人员或 DevOps 或各自的利益相关者的消息可能有助于他们做好准备,或者在应用崩溃之前拯救系统。这就是警报发挥作用的地方。根据警报的严重性和应用的使用情况,警报可以使用不同的渠道,从简单的电子邮件到电话。
图 7-2 展示了一对参数的阈值以及超过阈值时的动作。
图 7-2
达到阈值后向风险承担者发送警报
如图 7-2 所示,整个监控过程可分为三大步骤。
-
应用度量收集
-
度量分析
-
需要时提醒利益相关方
让我们看看 Istio 如何在三步流程中提供帮助。
Istio Mixer
在前几章中,我们看到了 Mixer 如何从 Istio 数据平面获取遥测数据。收集的数据是关于服务如何相互交互以及实例的 CPU 和内存消耗。图 7-3 显示了从 Mixer 到后端服务的数据流程;在这种情况下,我们使用 Prometheus。
图 7-3
指标从数据平面流向 Prometheus
指标从数据平面流向 Mixer,Mixer 提取属性并通过配置模型传递它们,配置模型确定需要传递什么以及以什么形式传递给适配器。适配器将数据传递给后端服务,在本例中是 Prometheus。在我们深入探讨这个问题之前,让我们先来看看 Prometheus 是如何工作的。
Prometheus
Prometheus 是一款开源监控和警报工具。它基本上将所有数据存储为一个时间序列,根据指标名称和称为标签的键值对进行分组。Prometheus 公司提供以下产品:
-
不同指标的时间序列数据
-
分析数据的查询语言
-
警报系统根据第二步中完成的分析发送通知
Prometheus 通过 HTTP 上的拉模型收集指标。换句话说,需要向 Prometheus 配置提供端点来收集指标数据。它在本地存储所有数据,并对数据运行规则,以得出新的时间序列数据或生成警报。图 7-4 显示了所有利益相关者之间的数据流。
图 7-4
Prometheus 度量流
让我们从在 K8s 集群上设置 Prometheus 开始。
装置
Prometheus 预配置了 Istio。Mixer 有一个内置的 Prometheus 适配器,公开为生成的指标值提供服务的端点。Prometheus 服务器作为附件出现在安装的 Istio 集群中。它也可以安装使用 Helm 图表在一个单一的步骤。
helm install --name prometheus stable/prometheus
这可以进一步配置,以提供不同的存储、进入规则等,但我们不在这里讨论这些。有关更多配置信息,请参考 Prometheus 文档。
Istio 的 Prometheus 附加组件预配置为收集混合器端点以收集公开的指标。可用的端点如下:
-
istio-telemetry.istio-system:42422/metrics
:返回所有混合器生成的指标。 -
istio-telemetry.istio-system:15014/metrics
:这将返回所有特定于混音器的指标。这将从 Mixer 返回指标。 -
返回特使生成的原始数据。Prometheus 被配置为寻找
envoy-prom
端点暴露的吊舱。附加组件配置在收集期间过滤掉大量特使度量,以试图限制附加组件进程的数据规模。 -
istio-pilot.istio-system:15014/metrics
:返回导频生成的指标。 -
istio-galley.istio-system:15014/metrics
这将返回厨房生成的指标。 -
istio-policy.istio-system:15014/metrics
:这将返回所有与策略相关的度量。 -
istio-citadel.istio-system:15014/metrics
:返回所有 Citadel 生成的指标。
该插件将时间序列数据保存在其文件系统中。这对我们很有用。对于生产环境,单独设置 Prometheus 或为其定义不同的数据存储可能更好。
如图 7-3 所示,指标通过配置模型到达 Prometheus,配置模型具有通过规则连接的实例和处理程序。Istio Prometheus 配置提供的预定义实例如下:
-
accesslog
:捕获请求源和目的地详细信息的日志条目 -
attributes
:捕获源和目标 pod、工作负载和命名空间的详细信息 -
requestcount
:捕获从源到目的地的请求数量 -
requestduration
:捕捉网格中所有调用的响应时间 -
requestsize
:捕获从源发送到目的地的有效载荷的请求大小 -
responsesize
:捕获从源发送到目的地的有效载荷的响应大小 -
tcpaccesslog
:捕获 TCP 请求的度量 -
tcpbytereceived
:捕获 TCP 请求中目的地接收的字节 -
tcpbytesent
:捕获 TCP 请求中源发送的字节 -
tcpconnectionsclosed
:捕获 TCP 连接关闭的次数 -
tcpconnectionsopened
:捕获 TCP 连接打开的次数
每个实例数据通过其处理程序被推入 Prometheus。让我们访问网格中的 Prometheus 节点,并在进一步移动之前查看仪表板。
Prometheus 仪表板
Prometheus 节点驻留在istio-system
名称空间中,如前所述,它将时间序列数据保存在本地文件系统中。通过端口转发请求来访问 pod 很简单,如图 7-5 和图 7-6 所示。您可以配置一个网关和访问它的规则,但是我们在这里不描述这些步骤。
图 7-5
端口转发到 Istio 网格中的 Prometheus 实例
图 7-6 显示了如何访问localhost:9090
上的仪表板。
图 7-6
Prometheus 仪表盘在 Istio 网格内运行
仪表板允许我们查询从网格中收集的不同指标。让我们看一个请求计数指标的简单示例。为此,让我们将一些请求放入我们的服务中,如图 7-7 所示。
图 7-7
对前端服务的围攻请求
这些请求的指标由 Mixer 收集,并传递给 Prometheus 后端服务。收集的指标现在可以在仪表板中看到,如图 7-8 所示。
图 7-8
Prometheus 公司记录的所有请求指标
我们只发出了四个请求,但是控制台中提供了许多请求指标。这是因为 Prometheus 已经跟踪了所有请求,包括发送给istio-system
的记录遥测数据的请求。让我们尝试使用 Prometheus 查询语言(也称为 PromQL)将请求限制到我们的名称空间。图 7-9 显示了过滤结果。
图 7-9
将指标限制到默认名称空间
这可能看起来不可读,但是图表部分给出了对网格请求数量正在增加的合理估计。我们正在用更多的请求轰炸服务,以使更改在图上可见,如图 7-10 所示。
图 7-10
网格请求数量的图形视图
可以使用如下查询来过滤对单个服务的请求:
istio_requests_total{destination_service_namespace="default", destination_service_name="webservice"}
这些都是预定义的指标,对于大多数监控情况应该足够了,但是 Istio 还允许添加定制的指标。
自定义指标
Istio Mixer 收集所有属性并将它们注入到 Prometheus 中,但是有些情况下需要重新计算指标才有意义。Mixer 允许添加配置来做到这一点。如本章前面所述,用户可以配置实例和处理程序,并添加规则以向 Prometheus 添加指标。
让我们创建一个场景,将对 webapp 服务的所有请求加倍。实例配置可以在清单 7-1 中找到。
apiVersion: config.istio.io/v1alpha2
kind: instance
metadata:
name: requestdouble
namespace: istio-system
spec:
compiledTemplate: metric
params:
value: "2"
dimensions:
source: source.workload.name | "unknown"
destination: destination.workload.name | "unknown"
Listing 7-1Requestdouble-instance.yaml Configuration
这种配置只是从网格中获取指标,并报告每个指标value
次,在本例中为 2 次。清单 7-2 中的处理程序引入了两个特定于该实例的新维度。
apiVersion: config.istio.io/v1alpha2
kind: handler
metadata:
name: doublehandler
namespace: istio-system
spec:
compiledAdapter: prometheus
params:
metrics:
- name: doublerequest_count # Prometheus metric name
instance_name: requestdouble.instance.istio-system
kind: COUNTER
label_names:
- source
- destination
Listing 7-2Requestdouble-handler.yaml Configuration
前面的配置只是将指标从声明的instance_name
推送到 Prometheus。它还容纳了两个新的维度,并将它们作为标签在 Prometheus 中传播。清单 7-3 中显示的规则定义了处理程序与实例的连接。
apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
name: requestdouble-prometheus
namespace: istio-system
spec:
actions:
- handler: doublehandler
instances: [ requestdouble ]
Listing 7-3Requestdouble-rule.yaml Configuration
这在 Prometheus 中记录了两次所有的请求。Prometheus 中的公制输出如图 7-11 所示。
图 7-11
所有请求在 istio_doublerequest_count 度量中计数两次
让我们调整规则,只为 web 服务连接一个实例和处理程序。清单 7-4 显示了match
的添加。
apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
name: requestdouble-prometheus
namespace: istio-system
spec:
match: match(destination.service.name, "webservice")
actions:
- handler: doublehandler
instances: [ requestdouble ]
Listing 7-4Requestdouble-rule.yaml Configuration
现在,只有一个服务的请求计数增加,如图 7-12 所示。
图 7-12
仅 web 服务的请求数增加了两倍
现在我们已经收集了我们这边的指标。尽管我们能够看到指标,但同时可视化不同的指标变得很困难。Prometheus 支持 Grafana 允许可视化部分。让我们来看看 Grafana 的行动。
格拉凡娜
Grafana 是一个可视化、分析和监控指标的开源平台。它支持从多个来源导入数据,并允许在单一平台上进行分析。在我们的例子中,我们将局限于从 Prometheus 号上获取的数据。
Grafana 可以被视为一个 UI 可视化工具,它使用来自 Prometheus 的数据,并在需要时发送警报。图 7-13 显示了从 Istio 流向 Grafana 的指标。让我们从设置 Grafana 使用我们的 Prometheus 服务器开始。
图 7-13
数据从 Istio 流向 Grafana
装置
与 Prometheus 类似,Grafana 已经预配置了 Istio 设置。我们在第三章中看到 Grafana 附带了一个演示设置,我们还查看了 Grafana 仪表盘。在这里,我们将通过设置新的仪表板和警报来使用 Grafana。
Istio Grafana 预先配置为从网格中运行的 Prometheus 服务获取数据。通过更改配置,可以将 Grafana 的数据存储配置为从 SQLite 切换到 MySQL、Redis 或 Postgres。
Grafana 可以使用定制配置的 Helm 图表进行安装。让我们建立一个单独的 Grafana 配置与 Helm 图表。
helm install --name grafana --tiller-namespace kube-system stable/grafana
可以在这里进行定制配置,提供单独的数据库连接、凭证、警报管理等。一旦 Grafana 建立起来,我们就可以在 Grafana 中建立一个数据源。
让我们尝试访问网格中可用的默认 Grafana 仪表板,如图 7-14 所示。
图 7-14
在网格中访问 Grafana 仪表板
Grafana 可供我们使用,随时可用。让我们看看仪表板。
Grafana 控制板
Grafana 有一套预配置的仪表盘,可以开箱即用。图 7-15 显示了直接访问 Istio 预配置仪表板以查看 Istio 网格的性能。
图 7-15
预配置的 Istio 网格仪表板
如上所述,该 Grafana 配置预先配置为从预先配置的 Prometheus 数据源读取指标,如图 7-16 所示。还可以在 Grafana 中添加额外的数据源来创建新的仪表板。
图 7-16
Grafana 预配置的 Prometheus 数据源
让我们创建一个新的仪表板来监控最近配置的 RequestDouble 指标。我们将创建一个显示对webapp-deployment-8
的请求率的视图,如图 7-17 所示。
图 7-17
带有图表的新仪表板,用于监控请求率
保存仪表板将创建该特定目的地的请求率视图。让我们假设,如果这个服务超过了三个请求的阈值,它可能会受到影响。为了准备好基础设施和团队,我们将在达到阈值之前设置外出警报。
格拉凡娜警报
Grafana 提供了一个简单的机制,当指标超过一个特定的阈值时,提醒涉众。让我们在对webapp-deployment-v8
的请求速率超过阈值 2.5 时设置一个警报。在我们开始之前,让我们设置警报通道。Grafana 允许一组公平的渠道来发送通知。它们包括以下内容:
-
HipChat
-
OpsGenie
-
扇子
-
三码网关
-
Prometheus Alertmanager
-
不和,电子邮件
-
维克托罗普斯
-
谷歌视频聊天
-
Kafka 休息代理
-
线条
-
容易做的事情
-
web 手册
-
丁丁
-
寻呼机杜蒂
-
松弛的
-
微软团队
-
电报
让我们建立一个 webhook 作为例子。我们将向该链接推送一条提醒:
https://jsonblob.com/api/jsonBlob/0d0ef717-d0a0-11e9-8538-43dbd386b327
参见图 7-18 了解如何在 Grafana 上添加网钩。
图 7-18
添加新的 webhook 频道
发送的任何通知都可以通过之前共享的链接看到。现在我们来设置一个警报。编辑我们在上一步中创建的面板,如图 7-19 所示。
图 7-19
编辑请求双倍费率面板
访问 Alert 选项卡,设置当请求阈值超过 2.5 个请求时发出警报,如图 7-20 所示。
图 7-20
基于带有自定义消息的条件创建警报
让我们用siege
向前端服务发出多个请求。
siege -c40 -r10 "http://192.168.99.160:31380"
几秒钟内,Grafana 开始显示警报,如图 7-21 所示。
图 7-21
请求显示超出阈值的双仪表板
webhook 接收带有细节的数据,如清单 7-5 所示。
{
"evalMatches": [{
"value": 107.19741952834556,
"metric": "{destination=\"webapp-deployment-8\", instance=\"172.17.0.6:42422\", job=\"istio-mesh\", source=\"frontend-deployment\"}",
"tags": {
"destination": "webapp-deployment-8",
"instance": "172.17.0.6:42422",
"job": "istio-mesh",
"source": "frontend-deployment"
}
}],
"message": "Requests threshold of web-deployment-8 reaching threshold, action required",
"ruleId": 1,
"ruleName": "RequestDouble Rate alert",
"ruleUrl": "http://localhost:3000/d/hpc70ncWk/requestdouble-dashboard?fullscreen\u0026edit\u0026tab=alert\u0026panelId=2\u0026orgId=1",
"state": "alerting",
"title": "[Alerting] RequestDouble Rate alert"
}
Listing 7-5Response Received on the Webhook
摘要
在本章中,我们介绍了使用 Prometheus 进行监控以及如何设置自定义指标。我们向您展示了 PromQL 如何帮助数据过滤,但它仍然是一次显示一个指标。我们将 Grafana 与 Prometheus 进行了整合,并创建了一个新的显示多个指标的仪表板。我们致力于在 Grafana 中配置警报,并集成了一个发送这些警报的通道。在下一章中,我们将致力于从分布式服务中收集日志,并跟踪调用以分析系统中的任何挑战。
八、日志和跟踪
在前一章中,我们使用了 Istio 提供的一些可观察性特性。我们能够捕获应用指标,但是指标只是可观察性的一个方面。可观察性是指在所有可能的维度上收集数据。在应用停机期间,查看可观察性的各个方面有助于开发人员理解应用行为,以便执行事件分析。有许多工具可以帮助实现这一目标。但是,了解添加一个行为所需的成本是很重要的。运营团队应该能够配置他们选择的工具,而不需要开发人员。在本章中,我们将看到如何无缝地捕获额外的行为,如请求跟踪和应用日志。我们还将使用 Istio 即插即用模型,该模型为捕获额外的应用行为提供了统一的机制。
分布式跟踪
在微服务应用中,一个请求通常部分由部署在集群中的多个应用提供服务。分布式跟踪是跨不同应用跟踪请求流的过程。跟踪通常通过显示某个时刻的请求、响应和延迟来描述应用的行为。运营团队通常使用跟踪来确定哪些服务导致了应用的性能问题。分布式跟踪有很多解决方案,比如 Zipkin、Jagger、Skywalking 等等。Istio 服务网格可以与它们一起工作。跟踪通常由特使代理生成。然后,这些跟踪被发送到跟踪器后端。
分布式跟踪依赖于一组额外的 HTTP 头。这些是广为人知的 b3 请求头。这些头构建了一个请求上下文,用于标识父请求,然后从一个系统传播到另一个系统。特使代理可以为每个传出请求生成这些头。但是对于每个传入的请求,头部必须传播到子请求。如果没有正确完成,那么 Envoy 将生成新的头,因此这些跨度将不会相互关联。
总之,以下一组头必须从传入请求传播到所有传出的子请求:
-
x-request-id
-
x-b3-traceid
-
x-b3-spanid
-
x-b3-parentspanid
-
x-b3-sampled
-
x-b3-flags
这里有一些特定于语言的 OpenTracing 库,可以帮助实现所需的头传播。OpenTracing 的细节超出了本书的范围。要了解更多信息,请参考前面提到的某个库。
在我们继续之前,我们需要将一个 tracer 应用部署到我们的 Kubernetes 集群。在这一章中,我们将使用 Jagger,一个在优步开发的开源分布式追踪应用。Jagger 基于 Dapper 和 OpenZipkin 提出的概念。出于我们的目的,我们将使用 Jagger 操作符( https://github.com/jaegertracing/jaeger-operator
)部署 Jagger。Kubernetes 有一个操作扩展,可以用来在 Kubernetes 集群上打包和部署应用。第一步,我们需要通过执行以下命令来安装 Jagger 操作符:
$ git clone https://github.com/jaegertracing/jaeger-operator.git
$ kubectl create namespace observability
namespace/observability created
$ kubectl create -f jaeger-operator/deploy/crds/jaegertracing_v1_jaeger_crd.yaml
serviceaccount/jaeger-operator created
$ kubectl create -f jaeger-operator/deploy/service_account.yaml
serviceaccount/jaeger-operator created
$ kubectl create -f jaeger-operator/deploy/role.yaml
clusterrole.rbac.authorization.k8s.io/jaeger-operator created
$ kubectl create -f jaeger-operator/deploy/role_binding.yaml
clusterrolebinding.rbac.authorization.k8s.io/jaeger-operator created
$ kubectl create -f jaeger-operator/deploy/operator.yaml
deployment.apps/jaeger-operator created
这些执行的命令在observability
名称空间中部署操作符。Kubernetes 操作符的细节超出了本书的范围。请参考 Kubernetes 文档以了解更多信息。
我们可以验证操作符,如下所示:
$ kubectl get all -n observability
NAME READY STATUS RESTARTS AGE
pod/jaeger-operator-5574c4fb9-4vn5q 1/1 Running 0 2m4s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/jaeger-operator 1/1 1 1 2m4s
NAME DESIRED CURRENT READY AGE
replicaset.apps/jaeger-operator-5574c4fb9 1 1 1 2m4s
Jagger 操作符现在可用于我们的 Kubernetes 集群。它用于部署 Jagger 实例。我们将部署尽可能简单的配置,即默认的配置有内存存储的 AllInOne Jagger 包。“一体化”映像在单个 pod 中部署了代理、收集器、查询、ingester 和 Jaeger UI。这可以通过使用以下配置来实现:
---
apiVersion: jaegertracing.io/v1
kind: Jaeger
metadata:
name: simplest
使用以下命令将此配置应用到我们的 Kubernetes 集群:
$kubectl apply -f jagger.yaml
现在让我们检查 Jagger 安装是否工作正常。我们可以首先检查我们的集群中已部署的服务,如下所示:
$kubectl get all
NAME READY STATUS RESTARTS AGE
pod/frontend-deployment-c9c975b4-p8z2t 2/2 Running 38 35d
pod/simplest-56c7bd47bf-z7cnx 0/1 ContainerCreating 0 16s
pod/webapp-deployment-6.2-654c5fd8f9-mrc22 2/2 Running 140 43d
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/simplest-agent ClusterIP None <none> 5775/TCP,5778/TCP,6831/TCP,6832/TCP 16s
service/simplest-collector ClusterIP 10.152.183.169 <none> 9411/TCP,14250/TCP,14267/TCP,14268/TCP 16s
service/simplest-collector-headless ClusterIP None <none> 9411/TCP,14250/TCP,14267/TCP,14268/TCP 17s
service/simplest-query ClusterIP 10.152.183.25 <none> 16686/TCP 16s
我们可以看到部署的所有组件都在运行。现在让我们通过查找最简单的查询服务的节点端口地址来打开 UI。见图 8-1 。
图 8-1
Jagger UI
我们只使用内存模式部署了 Jagger。这对于测试来说已经足够好了。Jagger 提供了在生产环境中部署它的配置选项(使用持久性存储)。参考 Jagger 文档了解更多信息。
一旦 Jagger 可用,Istio 服务网格就需要引用它。有几种方法可以实现这一点。
-
如果我们正在安装服务网格,我们可以在变量
global.tracer.zipkin.address=jagger-FQDN:16686
中提供 Jagger 地址。 -
在现有的安装中,我们需要编辑配置并指定
trace_zipkin_url
变量。让我们使用以下命令来编辑我们的配置:
$ kubectl -n istio-system edit deployment istio-telemetry
我们现在已经有了正确的基础设施。接下来,我们需要指示边车开始生成轨迹。特使代理可被配置成对所有接收到的请求的子集进行采样。这可以通过以下方式之一实现:
-
作为 Istio 安装的一部分,设置
pilot.traceSampling
变量。 -
使用以下命令将
PILOT_TRACE_SAMPLING
变量设置为现有安装:
$ kubectl -n istio-system edit deploy istio-pilot
在此之后,Envoy 将生成请求跨度,并将它们发送到 Jagger 服务器。我们可以通过执行前端 Java 应用的请求来验证这一点。
$ for i in {1..500}; do curl http://10.152.183.230/;echo "; done
现在让我们查找 Jagger UI 并搜索以前执行的请求。所有请求对于前端和 webapp 应用都有一个跨度。参见图 8-2 。
图 8-2
贾格尔痕迹
如图 8-2 所示,Jagger 提供了所有已执行请求的直方图。它显示了处理请求所花费的时间以及调用的应用。我们可以单击各个跟踪,详细查看各个应用的延迟和时间线。
应用日志
Istio 不为管理 Kubernetes 集群中运行的应用生成的日志提供任何支持。这在大型部署中是一个巨大的挑战,因为为我们的每个应用生成的日志都保留在运行该应用的容器中。让我们回头看看我们在第三章开发的例子。我们用 Java 创建了一个前端应用,用 Python 创建了一个 web 服务后端。我们在集群中部署了两个后端实例和一个前端应用实例。我们可以对我们的前端执行请求,这将调用后端。为了调试行为,我们需要查看应用日志。这是通过使用以下命令对每个容器执行日志查找来实现的:
$ kubectl logs pod/frontend-deployment-c9c975b4-p8z2t -c frontend
2019-08-26 13:52:42.032 INFO 1 --- [ main] istio.IstioFrontendApplication : Starting IstioFrontendApplication v0.0.1-SNAPSHOT on frontend-deployment-c9c975b4-p8z2t with PID 1 (/app.war started by root in /)
2019-08-26 13:52:42.039 INFO 1 --- [ main] istio.IstioFrontendApplication : No active profile set, falling back to default profiles: default
2019-08-26 13:53:01.243 INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2019-08-26 13:53:01.471 INFO 1 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2019-08-26 13:53:01.471 INFO 1 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.19]
这是非常麻烦和容易出错的,因为我们需要查找每个容器。此外,随着容器重启,我们会丢失应用日志中的信息。日志记录包含最详细的系统状态,团队必须能够参考日志来跟踪、验证和诊断应用的状态。因此,我们可以说应用日志的处理不够好,我们需要一个更好的解决方案来做到这一点。
除了应用日志之外,应用还可以创建访问日志。传统上,我们在前端代理中看到过这种情况,Apache HTTP 服务器正在创建access.log
文件。日志记录包含我们的应用收到的请求以及对它的响应。这是非常有用的信息。现在,如果我们看一下 Istio 服务网格,所有请求都是通过 Envoy sidecar 发出的。sidecar 因此跟踪它接收到的请求-响应。我们可以配置 Envoy 代理来打印这些日志或创建一个日志文件。但这对我们没有帮助,因为容器重启会丢失所有这些信息。
Kubernetes 描述了一种集群级日志记录方法,该方法利用了 ELK、Splunk、Stackdriver 等日志记录后端应用。该方法利用了服务网格使用的 sidecar 模式。应用日志的完整解决方案如下所示:
图 8-3
立方结构记录日志
-
部署在集群中的应用需要将日志写入文件。日志文件在由
volume-mount
创建的位置创建。 -
我们运行安装了导出卷的第二个容器。容器将运行一个能够执行日志解析的
fluentd
进程。 -
sidecar 容器然后读取日志并将它们发送到适当的后端。参见图 8-3 。
看一下前面的解决方案,我们的前端应用的配置如下所示:
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend-deployment
############### OMITTED FOR BREVITY
containers:
- name: frontend
image: frontend-app:1.0
imagePullPolicy: Never
env:
- name: LOG_PATH
value: /var/log
volumeMounts:
- name: varlog
mountPath: /var/log
############### OMITTED FOR BREVITY
- name: log-agent
image: k8s.gcr.io/fluentd-gcp:1.30
env:
- name: FLUENTD_ARGS
value: -c /etc/fluentd-config/fluentd.conf
volumeMounts:
- name: varlog
mountPath: /var/log
- name: config-volume
mountPath: /etc/fluentd-config
############### OMITTED FOR BREVITY
在前面的代码中,我们完成了以下工作:
-
我们已经将
/var/log
卷添加到我们的前端 Spring Boot 容器中。路径已经导出到LOG_PATH
变量。该变量指示 Spring Boot 在/var/log
路径创建spring.log
文件。 -
接下来,我们的 pod 中有一个日志代理容器。容器用配置文件
/etc/fluentd-config/fluentd.conf
运行fluentd
进程。
下面的fluentd.conf
文件读取我们的应用生成的日志,并将它们发送到 ELK 聚合器:
<source>
type tail
format /^\[[^ ]* (?<time>[^\]]*)\] \[(?<level>[^\]]*)\] (?<message>.*)$/
time_format %b %d %H:%M:%S %Y
path /var/log/spring.log
pos_file /var/log/agent/spring.log.pos
tag hostname.system
</source>
<match *.**>
type forward
<server>...</server>
<!--removed for Brevity -->
</match>
在应用之前的配置之前,我们需要部署fluentd
聚合器。这可以通过使用 Kubernetes 操作符来完成。配置fluentd
的细节超出了本书的范围。详见 https://github.com/vmware/kube-fluentd-operator
。
最后,我们需要设置fluentd
聚合器。fluentd
聚合器需要向 ELK 栈发送数据。它需要使用以下示例配置运行:
<match **>
type elasticsearch
log_level info
host elasticsearch
port 9200
logstash_format true
buffer_chunk_limit 2M
buffer_queue_limit 8
flush_interval 5s
num_threads 2
</match>
如果前面的配置有效,我们将在 ELK 栈中看到我们的应用日志。现在下一步是将 Istio 生成的访问日志发送到 ELK 实例。在下一节中,我们将看看 Mixer 扩展,它可以用来发送所需的日志。
搅拌器
Istio 使用可扩展的混合器子系统捕获所有遥测数据。该子系统非常灵活,允许对不同的监控和警报系统采用即插即用的方法。这种抽象使运营团队能够改变他们的应用监控方法,而不需要任何开发更改。Istio Mixer 部署了许多适配器。这些适配器中的每一个都向诸如 Prometheus、StatsD 等监控系统提交所需的数据。Envoy sidecar 为每个请求调用 Mixer,从而通过适配器捕获所有数据。由于 Envoy 为它收到的每个请求调用 Mixer,因此将 Mixer 组件嵌入到 sidecar 中听起来可能是合乎逻辑的。但是具有独立混合器组件的方法具有以下优点:
-
混音器是一个独立制造的组件;因此,它更符合 Istio 设计原则。另一方面,Envoy 是 Lyft 的代理服务。这种固有的差异使得 Mixer 更容易扩展到完整的方法。
-
该方法使系统更具容错性。Mixer 有许多外部依赖性,因此更容易出现网络故障。另一方面,特使不能容忍失败。即使混音器依赖项不可用,它也必须保持运行。
-
拥有独立的混合器和特使组件的方法使整个生态系统更加安全。Mixer 与各种外部系统集成。因此,它可能有许多安全漏洞。但是这些问题在调音台级别上受到限制。每个 Envoy 实例都可以配置为具有非常窄的交互范围,从而限制潜在攻击的影响。
-
Istio 为每个实例部署了一个边车;因此,边车必须尽可能轻。将所有第三方改编从边车中分离出来使得边车更加灵活。
混频器使 Istio 系统更加灵活,但也增加了系统的复杂性。混合器目前支持以下三种用例:
-
前提条件检查
-
配额管理,如 API 限制
-
遥测报告,如日志和请求
Istio 提供了多种适配器,可以使用 Mixer 进行配置。我们可以尝试扩展上一节中的日志示例。我们能够在 ELK 实例中发送我们的应用日志。我们现在需要发送访问日志。现在让我们试着用混音器来实现这一点。在我们继续之前,调用以下命令来获取可用适配器的列表:
$ kubectl get crd -listio=mixer-adapter
NAME CREATED AT
adapters.config.istio.io 2019-07-14T07:46:10Z
bypasses.config.istio.io 2019-07-14T07:45:59Z
circonuses.config.istio.io 2019-07-14T07:45:59Z
deniers.config.istio.io 2019-07-14T07:46:00Z
fluentds.config.istio.io 2019-07-14T07:46:00Z
Kubernetes envs.config.istio.io 2019-07-14T07:46:00Z
listcheckers.config.istio.io 2019-07-14T07:46:00Z
memquotas.config.istio.io 2019-07-14T07:46:01Z
noops.config.istio.io 2019-07-14T07:46:01Z
opas.config.istio.io 2019-07-14T07:46:02Z
prometheuses.config.istio.io 2019-07-14T07:46:02Z
rbacs.config.istio.io 2019-07-14T07:46:03Z
redisquotas.config.istio.io 2019-07-14T07:46:03Z
servicecontrols.config.istio.io 2019-07-14T07:46:04Z
signalfxs.config.istio.io 2019-07-14T07:46:04Z
solarwindses.config.istio.io 2019-07-14T07:46:04Z
stackdrivers.config.istio.io 2019-07-14T07:46:05Z
statsds.config.istio.io 2019-07-14T07:46:05Z
stdios.config.istio.io 2019-07-14T07:46:05Z
Istio 1.2 附带了丰富的适配器,如 Zipkin、StatsD、Stackdriver、CloudWatch 等。可以在 https://istio.io/docs/reference/config/policy-and-telemetry/adapters/
访问适配器的完整列表。
既然我们知道有各种适配器可用,我们将尝试为我们的应用配置它们。每个可用的适配器都可以使用图 8-4 所示的组件进行配置。
图 8-4
适配器组件
处理者
处理程序描述了需要如何调用适配器。它提供了必要的选项,可用于配置相关适配器的行为。可用处理程序的列表取决于服务网格中部署的适配器。此外,我们需要参考适配器文档来了解可用的配置选项。作为一个例子,让我们看看fluentds.config.istio.io
适配器。该适配器用于将访问日志发送到fluentd
聚合器 demon。
---
apiVersion: config.istio.io/v1alpha2
kind: handler
metadata:
name: fluentdhandler
namespace: istio-system
spec:
compiledAdapter: fluentd
params:
address: "fluentd-aggregator-host:port"
注意,我们没有描述日志格式,我们为应用日志描述了日志格式。
情况
一个实例定义了我们需要为一个请求捕获什么数据。数据以一组属性的形式表示。属性表示为名称和类型。类型定义属性保存的数据种类。因此,我们可以说一个属性描述了一个请求的单个属性。例如,属性可用于指定 HTTP 响应代码或每个 HTTP 头。对于每个请求,Envoy sidecar 都将相关的属性发送到混合器子系统。特使边车通过使用可用的环境/请求/响应值来生成这些属性。
Istio 有一组在所有请求中都可用的公共属性。这些属性的列表可在 https://istio.io/docs/reference/config/policy-and-telemetry/attribute-vocabulary/
获得。
适配器不能理解任何类型的数据。适配器理解的数据在四个模板中编译。每个模板都有一组可以被适配器捕获的属性。每个适配器都有一个可用于发送数据的模板列表。因此,实例可以被定义为属性的映射,由侧柜发送到相关适配器的模板中。以下命令显示可用模板的列表:
$ kubectl get crd -listio=mixer-instance
NAME CREATED AT
apikeys.config.istio.io 2019-07-14T07:46:06Z
authorizations.config.istio.io 2019-07-14T07:46:06Z
checknothings.config.istio.io 2019-07-14T07:46:06Z
edges.config.istio.io 2019-07-14T07:46:07Z
instances.config.istio.io 2019-07-14T07:46:11Z
Kubernetes es.config.istio.io 2019-07-14T07:46:06Z
listentries.config.istio.io 2019-07-14T07:46:07Z
logentries.config.istio.io 2019-07-14T07:46:07Z
metrics.config.istio.io 2019-07-14T07:46:08Z
quotas.config.istio.io 2019-07-14T07:46:08Z
reportnothings.config.istio.io 2019-07-14T07:46:08Z
servicecontrolreports.config.istio.io 2019-07-14T07:46:08Z
tracespans.config.istio.io 2019-07-14T07:46:09Z
我们可以使用以下命令来确定具有先前模板的现有实例:
$ kubectl get logentries.config.istio.io --all-namespaces
NAMESPACE NAME AGE
istio-system accesslog 41d
istio-system tcpaccesslog 41d
We can look at the accesslog definition to know which details are captured by it:
$ kubectl get logentries.config.istio.io accesslog -n istio-system -o yaml
apiVersion: config.istio.io/v1alpha2
kind: logentry
metadata:
// REMOVED FOR BREVITY
spec:
monitored_resource_type: '"global"'
severity: '"Info"'
timestamp: request.time
variables:
apiClaims: request.auth.raw_claims | ""
apiKey: request.api_key | request.headers["x-api-key"] | ""
.......
destinationApp: destination.labels["app"] | ""
destinationIp: destination.ip | ip("0.0.0.0")
destinationName: destination.name | ""
latency: response.duration | "0ms"
method: request.method | ""
protocol: request.scheme | context.protocol | "http"
receivedBytes: request.total_size | 0
// REMOVED for brevity
前面的日志条目捕获了完整的请求-响应属性集。该模板还为缺失的属性分配默认值。前一项是预编译的实例。但是如果它不可用,我们可以使用以下 YAML 配置添加一个实例:
apiVersion: config.istio.io/v1alpha2
kind: instance
metadata:
name: myaccesslog
namespace: istio-system
spec:
compiledTemplate: logentry
params:
severity: '"info"'
timestamp: request.time
variables:
source: source.labels["app"] | source.workload.name | "unknown"
user: source.user | "unknown"
destination: destination.labels["app"] | destination.workload.name | "unknown"
responseCode: response.code | 0
responseSize: response.size | 0
latency: response.duration | "0ms"
monitored_resource_type: '"UNSPECIFIED"'
规则
规则将部署的处理程序与部署的实例结合起来。它在调用相关实例之前匹配指定条件的请求。规则必须指定处理程序和实例的完全限定名。如果它们都部署在同一个名称空间中,那么规则可以使用短名称。以下规则将accesslog
和myaccesslog
日志发送到前面创建的fluentd
处理程序:
---
apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
name: fluentdrule
namespace: istio-system
spec:
match: "true"
actions:
- handler: fluentdhandler
instances: [ myaccesslog, accesslog ]
现在,我们可以使用以下命令部署所有这些组件:
$ kubectl apply -f fluentd-adapter.yaml
接下来,让我们使用curl
命令来访问我们的服务。这将生成日志并将它们发送到 ELK 实例。让我们通过在 Kibana 中进行查找来验证这一点。
前面的例子被用作一个简单的垫脚石。Istio 也可以扩展到其他用例。
-
使用 StatsD 构建统计数据并将其发送到 Icinga/Nargios
-
验证类似配额的 API 限制
摘要
在这一章中,我们看了 Istio 提供的可观察性特性。我们从捕获请求跟踪开始,以确定应用性能。Istio 允许我们使用自己选择的跟踪解决方案。接下来,我们希望捕获应用级日志。我们意识到 Istio 没有提供应用日志的解决方案。然而,我们可以扩展我们的 Kubernetes 集群,为我们部署的应用提供一个日志解决方案。该解决方案不需要任何额外的开发工作。为此,我们部署了一个 ELK 实例,并使用 Sidecar 模式路由应用日志。下一个目标是扩展日志解决方案,以包含由 Istio 生成的日志。在旅途中,我们使用混合器组件来实现边车日志摄取。总之,我们使用了 Istio 的可扩展性特性,该特性可用于将其与第三方系统连接。
九、策略和规则
在微服务架构中,应用安全性是一个挑战。开发人员用各种语言构建业务流程微服务。所有这些应用都必须通过适当的身份验证和授权来保护。大多数企业都有一种进行身份验证和授权的方式。在本章中,我们将讨论 Istio 中提供的安全特性。之前我们看到每个 Istio 特性都是由 Envoy 代理驱动的。安全也不例外。Istio 使用 Envoy 代理提供安全特性。因此,它从业务服务中卸载了身份验证和授权逻辑。
证明
认证是为收到的请求建立用户身份的过程。但是在分布式体系结构中,单个用户请求跨越不同应用的多个子请求。验证用户身份的原始请求更容易,但是每个子请求还必须建立用户的身份。这通常是通过使用基于令牌的身份验证来实现的,比如 SSO、OAuth 等等。此外,分布式体系结构容易出现网络漏洞。在这样的体系结构中,应用通过一个它们都无法控制的网络进行通信。网络可能有能够伪造、复制、重放、更改、破坏和延迟响应的对手。为了有效地工作,应用必须能够信任通信交换。Istio 使用各种身份验证机制支持前面的所有需求。
传输认证
在微服务中,架构应用容易受到网络攻击。应用可以通过为它们的通信实现 TLS 协议来防范这些攻击。该协议旨在提供两个或多个通信应用之间的隐私和数据完整性。但是对于每个服务来说,实现 TLS 通信是昂贵且复杂的。Istio 服务网格通过使用特使代理支持 TLS 交换来减轻这一成本。
TLS 协议基于公钥基础设施(PKI)的概念。PKI 声明有一个定义身份的私钥。每个私钥也有一个公钥。公钥在颁发给应用的证书中指定。应用通过网络进行通信,使用它们的私钥加密请求,使用其他服务的公钥解密响应。为了成功工作,公钥/证书需要由可信方(证书颁发机构)签名。Istio 通过使用两个组件来实现 PKI 逻辑:Citadel 和 Node-Agent。Citadel 扮演证书权威的角色。它负责颁发证书。颁发证书的过程如下:
-
Istio 节点代理生成一个私钥和一个证书签名请求(CSR)。
-
Istio 节点代理将 CSR 及其密钥发送到 Citadel 进行签名。
-
Citadel 验证与 CSR 相关联的凭证,并对 CSR 进行签名以生成证书。
-
节点代理将从 Citadel 收到的证书和私钥发送给特使代理。
对于密钥和证书轮换,以周期性的间隔重复前面的过程。现在,每个边车都有一个证书-密钥对,因此它们可以使用以下步骤执行 TLS 通信:
-
客户端特使开始与服务器端特使代理的相互 TLS 握手。
-
在握手期间,客户端特使代理进行安全命名检查,以验证服务器证书中提供的服务帐户是否被授权运行目标服务。
-
客户端特使代理和服务器端特使代理建立相互的 TLS 连接,并且 Istio 将流量从客户端特使代理转发到服务器端特使代理。
授权后,服务器端特使代理通过本地 TCP 连接将流量转发到服务器服务。
如图 9-1 所示,该流程要求所有交互都要进行 TLS 通信。为整个应用资产实现这个过程是一个挑战。在集群内部,Istio 提供了一个经济高效的解决方案。在服务网格中启用mtls
模式通常被视为最佳实践。但是如果服务与部署在服务网格之外的应用进行通信,那么实现握手就会成为一个主要的障碍。因此,Istio 提供了一种许可模式,允许服务同时接受纯文本流量和双向 TLS 流量。这极大地简化了服务网格的启动过程。
图 9-1
加州城堡
Istio mutual TLS 身份验证是通过创建策略来配置的。该策略强制应用支持的交换类型。可以在不同的级别创建策略。创建后,该策略适用于指定级别下部署的所有服务。如果有多个级别的策略,则 Istio 会应用最具体的策略。
-
Mesh :这是一个影响整个服务网格的全局策略。
-
名称空间:该策略影响在特定名称空间中运行的服务。
-
服务:这个策略只影响一个特定的服务。
Istio 中已经部署了网格策略。该策略显示了与 Istio 安装捆绑在一起的配置。
$ kubectl get meshpolicies.authentication.istio.io -o yaml
apiVersion: v1
items:
- apiVersion: authentication.istio.io/v1alpha1
kind: MeshPolicy
metadata:
## REMOVED for BREVITY
generation: 1
labels:
app: security
chart: security
heritage: Tiller
release: istio
name: default
spec:
peers:
- mtls:
mode: PERMISSIVE
kind: List
metadata:
resourceVersion: ""
selfLink: ""
我们可以看到mtls
被设置为PERMISSIVE
模式。因此,我们已经能够从网格外部执行curl
命令。我们现在可以为我们的 webapp 配置STRICT
模式。在下面的代码中,我们将 web 服务配置为只接受基于mtls
的请求:
apiVersion: "authentication.istio.io/v1alpha1"
kind: "Policy"
metadata:
name: "strict-policy"
spec:
targets:
- name: webservice
peers:
- mtls:
mode: null
让我们从网格外部部署策略并执行我们的curl
命令。我们需要 web 服务的 IP。(你可以用kubectl
来确定这一点。)
$curl http://10.152.183.230/
curl: (56) Recv failure: Connection reset by peer
在前面的curl
命令中,我们正在尝试一个纯文本请求,该请求被特使代理丢弃。完成前面的步骤后,我们会注意到 pod 开始出现故障。这是因为来自 Kubernetes 服务器的活性探测请求开始失败,并且 pod 被标记为失败。见图 9-2 。
图 9-2
由于 mtls 导致 pod 故障
作为第一步,我们必须修复来自 Kubernetes 服务器的请求。这可以通过检查应用端口以外的端口来实现。这将绕过特使代理。或者,我们可以为检查配置ProbeRewrite
。这将把检查请求发送到Pilot-Agent
,?? 将把它们发送到应用容器。在我们完成这个之前,我们需要使用下面的命令启用ProbeRewrite
:
$ kubectl get cm istio-sidecar-injector -n istio-system -o yaml | sed -e "s/ rewriteAppHTTPProbe: false/ rewriteAppHTTPProbe: true/" | kubectl apply -f -
configmap/istio-sidecar-injector configured
之后,我们需要为我们的部署配置rewriteAppHTTPProbers
注释。
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp-deployment-6.0
## REMOVED for BREVITY
template:
metadata:
labels:
app: webapp
version: v6.0
annotations:
sidecar.istio.io/rewriteAppHTTPProbers: "true"
spec:
containers:
- name: webapp
## REMOVED for BREVITY
现在 Pods 应该不会再出故障了。pod 运行良好,但是我们不能从网格外部运行curl
命令。网格中运行的服务已经建立了密钥-证书对,所以我们可以从我们的前端 pod 尝试一个curl
命令。
frontend-deployment-78d98b75f4-rwkbm:/# curl http://webservice/
curl: (56) Recv failure: Connection reset by peer
请求仍然失败,并显示相同的错误消息,因为没有发生mtls
交换。让我们了解一下引擎盖下发生了什么。以前,我们让应用服务器执行mtls
。现在,我们需要指导客户执行mtls
握手。这是通过配置目标规则来完成的。
apiVersion: "networking.istio.io/v1alpha3"
kind: "DestinationRule"
metadata:
name: "wb-rule"
namespace: "default"
spec:
host: webservice
trafficPolicy:
tls:
mode: ISTIO_MUTUAL
在第四章中,我们使用虚拟服务的目的地规则来定义子集。这里,在前面的配置中,我们已经指示 Envoy sidecar 为 web 服务目的地执行一次mtls
握手。现在再次执行请求。我们可以看到它像预期的那样工作。
在前面的示例中,我们已经在服务级别启用了策略;或者,我们可以在网格或名称空间级别启用策略。这将使它适用于在配置的作用域下运行的所有服务。应用这样的策略需要特定的服务规则来覆盖它。
用户认证
Istio 提供基于 OAuth 令牌的身份验证。每个请求都伴随着一个 OAuth 令牌。在响应请求之前,特使代理用配置的 OpenID 提供者验证令牌。然后,令牌以 JSON Web 令牌(JWT)格式发送。Istio 认证按照以下步骤执行:
-
向授权服务器发出初始请求,以交换凭证并生成令牌。生成的 JWT 与一组特定的用户角色和权限相关联。
-
每个后续请求必须指定令牌,允许用户访问该令牌允许的授权路由、服务和资源。
特使代理验证令牌。它还在每个子请求上复制令牌。见图 9-3 。
图 9-3
基于 JWT 的认证
JWT 是一个开放标准(RFC 7519 ),它定义了一种紧凑且独立的方式,以 JSON 对象的形式在各方之间安全地传输信息。该信息经过数字签名,可以被验证和信任。加密的令牌可用于指定与其相关联的用户角色和权限。因此,它最常用于指定用户授权。
在我们继续之前,我们需要一个 OpenID 提供者。Istio 允许我们与许多提供商合作,如 Auth0、Google Auth 等等。在这一章中,我们将使用 KeyCloak ( http://KeyCloak.org
)。我们在一个工作站上部署了一个 KeyCloak 实例。见图 9-4 。
图 9-4
凯克洛克
现在,我们需要在 KeyCloak 中添加用户。这些用户将可以访问我们的 Kubernetes 应用。为此,我们需要首先在 KeyCloak 中选择/添加一个领域。在 KeyCloak 中,一个领域可以拥有由 ID-secret 对指定的客户端。客户端可以等同于生态系统中的不同应用。反过来,这些应用都有用户。这是通过为每个客户端创建不同的用户来映射的。每个创建的用户可以有不同的属性/权限。前面的描述是对 KeyCloak 安全提供者的 50,000 英尺的看法。KeyCloak 的细节超出了本书的范围。请参考 KeyCloak 文档以了解更多信息。
以下是将用户添加到 KeyCloak 的步骤。如果我们已经在 OpenID 提供程序中设置了用户,我们可以跳过这一部分。
-
使用管理员帐户登录到 KeyCloak 管理控制台。
-
主下拉菜单显示现有领域;单击添加领域并创建一个 K8s-dev 领域。
-
现在选择 K9s-dev 领域并单击 Users 打开用户列表页面。
-
打开“添加用户”页面。输入用户名,然后单击保存。
-
在“用户”页面上,单击“凭据”选项卡为新用户设置临时密码。键入新密码并确认。
在我们当前的例子中,我们创建了一个 K8s-dev 领域。该领域包含 web 服务和前端应用的客户端 ID。这两个客户端都有一个用户映射到它。此时,我们还没有为这些用户添加任何额外的权限。参见图 9-5 。
图 9-5
键盘锁配置
在执行了前面的配置之后,我们将获得 OpenID 端点的详细信息。这些端点用于执行用户身份验证和令牌验证。以下是我们将使用的几个重要端点:
issuer "http://172.18.0.1:8181/auth/realms/k8s-dev"
authorization_endpoint "http://172.18.0.1:8181/auth/realms/k8s-dev/protocol/OpenID -connect/auth"
token_endpoint "http://172.18.0.1:8181/auth/realms/k8s-dev/protocol/OpenID -connect/token"
jwks_uri "http://172.18.0.1:8181/auth/realms/k8s-dev/protocol/OpenID -connect/certs"
现在,我们将使用之前提供的端点来配置 Istio 用户身份验证。我们将创建一个策略,如前一部分所示。
apiVersion: "authentication.istio.io/v1alpha1"
kind: "Policy"
metadata:
name: "user-auth"
spec:
targets:
- name: webservice
origins:
- jwt:
issuer: http://172.18.0.1:8181/auth/realms/k8s-dev
jwksUri: http://172.18.0.1:8181/auth/realms/k8s-dev/protocol/OpenID -connect/certs
trigger_rules:
- excluded_paths:
- exact: /health
principalBinding: USE_ORIGIN
在之前的配置中,我们完成了以下工作:
-
我们将 JWT 设置配置为指向我们的 k8s-dev 领域。
-
现在,在我们的代理中可能有两个安全主体。一个来自
mtls
配置,另一个来自用户身份令牌。在这种情况下,我们从用户令牌配置绑定主体。 -
我们已经从身份验证中排除了
/heath
URL,因为它被 Kubernetes 用于活性检查。如果我们阻塞这条路径,那么 pod 将开始失效,就像我们启用mtls
时看到的那样。
可以为特定路径启用或禁用 JWT 令牌身份验证。此外,我们可以添加多个 JWT 块来处理不同的路径。如果对请求路径禁用了所有 jwt,则身份验证也会通过,就像没有定义任何 jwt 一样。现在让我们通过执行这些curl
命令来测试我们的配置:
$curl -v http://10.152.183.230/
< HTTP/1.1 401 Unauthorized
< content-length: 29
< content-type: text/plain
< date: Thu, 05 Sep 2019 08:50:03 GMT
< server: istio-envoy
< x-envoy-decorator-operation: webservice.default.svc.cluster.local:80/*
<
Origin authentication failed.
该服务返回 401 错误代码。为了让它工作,我们需要在请求中添加一个 JWT。让我们首先通过发送 OAuth 请求来生成一个。我们将使用 Postman 生成它,但是您也可以使用任何其他合适的方法。目的是获得一个可以在授权头中传递的 JWT 值。
我们可以按以下方式使用 Postman:
图 9-6
JWT 令牌
-
在 Postman 中选择 Authorization 选项卡,并将类型设置为 OAuth 2.0。
-
单击获取新的访问令牌。这将打开一个新的表单,我们需要在其中填写来自 OpenID 配置的值。
-
定位正确的值后,单击请求令牌。参见图 9-6 。
然后它会询问登录凭证。成功登录后,它会发回一个令牌。我们需要复制该值,并在身份验证头中发送它。
$curl --header "Authorization: Bearer $TOKEN" -v http://10.152.183.230/
> GET / HTTP/1.1
> Host: 10.152.183.230
> User-Agent: curl/7.58.0
> Accept: */*
> Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJpejZyRi1RQUw4STVlNFRDcVdaSE9SLWpLN1A2UjVEUnR2d2ZsZk5MSnZVIn0.eyJqdGkiOiI4NzYyOGQ4Ni04MTg3LTQ1ZGEtOWRiMi1iZGIyNThkYzk5MGMiLCJleHAiOjE1Njc2MTkyMDcsIm5iZiI6MCwiaWF0IjoxNTY3NjE4OTA3LCJpc3MiOiJodHRwOi8vMTcyLjE4LjAuMTo4MTgxL2F1dGgvcmVhbG1zL2s4cy1kZXYiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiNmY3MTNlMDMtOWYyNC00MmMyLTgzMDktZWI2ZGY0NmZiNzU1IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoid2Vic2VydmljZS11c2VyIiwiYXV0aF90aW1lIjoxNTY3NjE4MDQyLCJzZXNzaW9uX3N0YXRlIjoiYzNhOTk1NWMtYTA5YS00NGFlLWE3NzEtMzM3OTE0OTRjZTg1IiwiYWNyIjoiMCIsImFsbG93ZWQtb3JpZ2lucyI6WyIqIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsid2Vic2VydmljZS11c2VyIjp7InJvbGVzIjpbInVzZXIiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6InJhaHVsIiwiZW1haWwiOiJyYWh1bEBrOHMuY29tIn0.aHrwRFT2jG0FFBEhNA-bbaY-NxGIGGDBqn9XxqvHUJLIagnjhkTZGioH44kog_A_LT9IeGj2bMeOBeb0NQn4K1a-c66EpQa4bwt9kcsFfcSKb1Z1dtOhp8tg7jjST93220dq9h9SqHdrMbhJ_eL0rdOKs5VE8DiOOONaP1OkQj4B5Ya58VMuIEAeajgOsSivRRKZlseXp-kr2rPlS2fbPmGFPCfxZl_OEygGaiKWPyQ79DvI_ecEDKxUmg4iLtp86ieVWcu6H_X6ETHmdk9QInWTXI4ORHygd9loY0BoDFtVG9K3STPv9Cn6eDwn6jHCuyyEJ9V0k-2OXqqopF-ggA
>
< HTTP/1.1 200 OK
< content-type: text/html; charset=utf-8
< content-length: 62
< server: istio-envoy
< date: Wed, 04 Sep 2019 17:44:50 GMT
< x-envoy-upstream-service-time: 3
< x-envoy-decorator-operation: webservice.default.svc.cluster.local:80/*
<
* Connection #0 to host 10.152.183.230 left intact
[6.0]Welcome user! current time is 2019-09-05 17:44:50.140684
批准
在上一节中,我们完成了身份验证。这意味着我们已经确定了用户的身份。但是不允许所有用户访问应用的所有部分。控制对仅允许部分的访问的过程被称为授权。基于角色的访问控制(RBAC)通常用于限制用户使用适用于他们的功能。不得允许用户在其领域之外执行操作。Istio 在特使代理中运行 RBAC 引擎。代理从 Pilot 获取适用的 RBAC 策略。它将请求中的 JWT 与配置的授权策略进行比较。因此,它要么允许,要么拒绝请求。
默认情况下,Istio 禁用基于角色的访问控制。作为第一步,我们需要让 RBAC 支持 Istio。这可以通过应用以下配置来实现:
apiVersion: "rbac.istio.io/v1alpha1"
kind: ClusterRbacConfig
metadata:
name: default
spec:
mode: 'ON_WITH_INCLUSION'
inclusion:
namespaces: ["default"]
前面的配置为“默认”命名空间启用了 RBAC 控制。值得注意的是,ClusterRbacConfig
是一个单独的集群范围的对象,名为default
。该模式还有其他值可用于微调 RBAC:
-
关 : Istio 授权被禁用。
-
ON:mesh 中的所有服务都启用 Istio 授权。
-
ON_WITH_INCLUSION : Istio 授权仅对 INCLUSION 字段中指定的服务和名称空间启用。
-
ON_WITH_EXCLUSION :除了在 EXCLUSION 字段中指定的服务和名称空间之外,为网格中的所有服务启用 Istio 授权。
我们可以尝试使用curl
命令来访问服务,但是它会以 403 响应失败。
$ curl --header "Authorization: Bearer $TOKEN" -v http://10.152.183.230/
< HTTP/1.1 403 Forbidden
< content-length: 19
< content-type: text/plain
RBAC: access denied
启用 RBAC 后,我们需要定义角色/权限。权限可以在服务级别定义。它们还可以针对路径、HTTP 方法和请求头进行微调。这些权限是使用ServiceRole
配置定义的。
---
apiVersion: "rbac.istio.io/v1alpha1"
kind: ServiceRole
metadata:
name: http-viewer
spec:
rules:
- services: ["webservice"]
methods: ["GET"]
之前的配置定义了一个用于访问webservice
的http-viewer
角色。需要将定义的角色分配给用户。可以为用户或通过其令牌的属性识别的用户进行分配。或者,也可以保持匿名访问。在应用中,我们可能希望允许 GET 请求,以便用户可以查看数据。但是 POST 请求需要基于角色的授权。因此,让我们为执行更新再定义一个角色。
---
apiVersion: "rbac.istio.io/v1alpha1"
kind: ServiceRole
metadata:
name: http-update-webservice
spec:
rules:
- services: ["webservice"]
methods: ["POST"]
我们已经为 POST 方法添加了更新角色,但是可以使用 URL 路径对其进行限制。现在我们需要将http-viewer
权限分配给每个人,并且只将http-update-webservice
分配给经过身份验证的用户。这是通过配置ServiceRoleBinding
完成的。
---
apiVersion: "rbac.istio.io/v1alpha1"
kind: ServiceRoleBinding
metadata:
name: bind-http-viewer
spec:
subjects:
- user: "*"
roleRef:
kind: ServiceRole
name: "http-viewer"
前面的绑定将http-viewer
角色分配给所有用户。或者,我们可以验证用户主体,并为其分配相应的角色。这是通过以下配置实现的:
---
apiVersion: "rbac.istio.io/v1alpha1"
kind: ServiceRoleBinding
metadata:
name: bind-http-update
spec:
subjects:
- properties:
request.auth.claims[scope]: "webservice"
roleRef:
kind: ServiceRole
name: "http-update-webservice"
前面的绑定将http-update-webservice
角色分配给具有jwt
和webservice
范围的请求。request.auth.claims
用于读取 JWT 的不同部分。应用先前的ServiceRole
和ServiceRoleBinding
配置。现在我们可以试试curl
命令。它应该像预期的那样工作。
规则
在上一节中,我们实施了身份验证和授权策略。但是策略也可以用于实施应用规则。这对运营团队非常有用,他们可以创建规则来管理资源利用或控制应用黑名单/白名单,等等。值得注意的是,这些需求是基于运行时行为的,因此非常多样化。使用规则引擎而不是开发定制代码来实现这些不断变化的需求是一个好主意。Istio 支持使用混合器组件进行规则验证。以前,我们配置了 Mixer 组件来与 Jagger 等第三方扩展一起工作。混合器由三部分组成。
-
处理器:定义适配器配置
-
实例:定义请求需要捕获的属性
-
规则:将一个处理程序与能够发送所需数据的实例相关联
特使代理向 Istio Pilot 发送请求。Pilot 调用 Mixer,Mixer 捕获实例配置中定义的数据,并将其发送给处理程序。以前,处理程序在外部系统中捕获数据。或者,处理程序可以对接收到的请求执行布尔检查。特使代理可以基于检查响应来允许或拒绝请求。像大多数其他特性一样,Istio 提供了一个disablePolicyChecks
标志来切换规则检查。让我们首先使用以下命令来启用它:
$ kubectl get cm istio -n istio-system -o yaml | sed -e "s/ disablePolicyChecks: true/ disablePolicyChecks: false/" | kubectl apply -f -
configmap/istio configured
在以下示例中,我们将配置一个白名单规则。基本上,我们希望只允许从我们的前端服务访问 webapp。不应允许任何其他来源。为此,我们需要用一个listentry
模板配置一个listchecker
处理程序。
---
apiVersion: config.istio.io/v1alpha2
kind: handler
metadata:
name: whitelist
spec:
compiledAdapter: listchecker
params:
overrides: ["frontend"]
blacklist: true
---
apiVersion: config.istio.io/v1alpha2
kind: instance
metadata:
name: appsource
spec:
compiledTemplate: listentry
params:
value: source.labels["app"]
---
apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
name: checksrc
spec:
match: destination.labels["app"] == "webapp"
actions:
- handler: whitelist
instances: [ appsource ]
我们现在可以执行我们的curl
命令,该命令将失败,并出现以下错误:
$curl -v http://10.152.183.230/
> GET / HTTP/1.1
> Host: 10.152.183.146
> User-Agent: curl/7.58.0
> Accept: */*
>
* Empty reply from server
* Connection #0 to host 10.152.183.
curl: (52) Empty reply from server
但是如果我们尝试对我们的前端服务做一个curl
,我们将得到预期的响应。在前面的代码中,我们实现了白名单,通过更改处理程序的blacklist
属性,可以将它切换到黑名单。到目前为止,我们已经使用了应用白名单。Istio 捆绑了几个处理程序,可用于执行各种检查,如配额管理、简单拒绝等。
摘要
在本章中,我们使用了 Istio 的安全特性。我们研究了 Istio 提供的两种身份验证。所有服务通信都使用相互 TLS 模式来实现传输身份验证。这使得服务器和客户端必须拥有私钥和证书对。Istio 实现了 PKI 逻辑,简化了服务网格中的mtls
握手。Istio 支持一种PERMISSIVE
模式来提供纯文本交互。这简化了与部署在服务网格之外的服务的交互。Istio 使用 JWT 格式的基于 OAuth 的令牌提供用户身份验证。接下来,我们讨论了使用 Istio RBAC 的授权。RBAC 可用于为服务、路径、HTTP 方法和请求属性构建细粒度的权限。最后,我们讨论了 Istio 规则引擎,它可用于执行检查,如黑名单、请求配额等。
十、故障排除
Istio 服务网格中的故障排除问题相当复杂。有许多组件协同工作来交付所需的行为,每个系统都有自己的细微差别。把所有的情况都考虑进去是不可能的。因此,在事故期间,故障排除过程可能感觉像大海捞针。在这一章中,我们将介绍一些可以用来解决服务网格问题的工具。本章中讨论的命令适用于裸机 Kubernetes 安装。知道我们在寻找什么样的配置是很重要的。在基于云的集群中,可能需要稍微不同的命令。
配置映射
Istio 有许多由相应特征标志驱动的特征。我们已经在策略检查、请求跟踪等等中看到了这些标志。配置标志是确定如何配置功能的第一个位置。它可以回答一系列广泛的问题,例如:
-
特使代理使用什么配置?
-
网关是如何工作的?
-
是否启用了 Istio 的分布式跟踪,配置了哪个跟踪提供程序?
这些标志是使用 Kubernetes 配置图定义的 Istio 配置的一部分。具体来说,Istio 由两个配置图驱动。
-
istio
:这定义了 Istio pilot、Mixer、Tracing、Prometheus、Grafana 等的配置。每个组件的配置选项都以组件名称为前缀。 -
istio-sidecar-injector
:这定义了 Istio 边车的配置,包括飞行员的位置、Kubernetes api-server 等。
我们可以使用以下命令获得这些配置图:
$kubectl -n istio-system get cm istio -o jsonpath="{@.data.mesh}"
disablePolicyChecks: false
enableTracing: true
accessLogFile: "/dev/stdout"
#REMOVED for BREVITY
$kubectl -n istio-system get cm istio-sidecar-injector -o jsonpath="{@.data.config}"
policy: enabled
alwaysInjectSelector:
[]
template: |-
rewriteAppHTTPProbe: {{ valueOrDefault .Values.sidecarInjectorWebhook.rewriteAppHTTPProbe false }}
{{- if or (not .Values.istio_cni.enabled) .Values.global.proxy.enableCoreDump }}
initContainers:
#REMOVED for BREVITY
接下来我们可以确定边车是如何注入的。Istio 支持自动注入,只要它是为名称空间配置的。我们可以使用以下命令列出自动istion-sidecar
的所有名称空间:
$kubectl get namespace -L istio-injection
NAME STATUS AGE ISTIO-INJECTION
default Active 221d enabled
istio-system Active 60d disabled
kube-node-lease Active 75d
kube-public Active 221d
kube-system Active 221d
代理
Istio 使用 Envoy 代理应用大多数功能。Istio 代理与 Istio pilot 连接以获取最新配置。因此,必须知道特使代理是否与试验中可用的最新策略同步。istioctl proxy-status
是获取所有代理状态的有用命令。
$ istioctl proxy-status
PROXY CDS LDS EDS RDS PILOT VERSION
istio-ingress-6458b8c98f-7ks48.istio-system SYNCED SYNCED SYNCED NOT SENT istio-pilot-75bdf98789-n2kqh 1.1.2
istio-ingressgateway-7d6874b48f-qxhn5.istio-system SYNCED SYNCED SYNCED SYNCED istio-pilot-75bdf98789-n2kqh 1.1.2
productpage-v1-6c886ff494-hm7zk.default SYNCED SYNCED SYNCED STALE istio-pilot-75bdf98789-n2kqh 1.1.2
ratings-v1-5d9ff497bb-gslng.default SYNCED SYNCED SYNCED SYNCED istio-pilot-75bdf98789-n2kqh 1.1.2
webservice-v1-55d4c455db-zjj2m.default SYNCED SYNCED SYNCED SYNCED istio-pilot-75bdf98789-n2kqh 1.1.2
webservice-v2-686bbb668-99j76.default SYNCED SYNCED SYNCED SYNCED istio-pilot-75bdf98789-tfdvh 1.1.2
webservice-v3-7b9b5fdfd6-4r52s.default SYNCED SYNCED SYNCED SYNCED istio-pilot-75bdf98789-n2kqh 1
所有正在运行的代理将处于以下状态之一:
-
SYNCED
:表示边车随着所有的变化而更新。 -
这意味着有变化,但是边车没有挑选这些变化。
-
这意味着没有变化。
如果列表中缺少代理,则它没有连接到 Istio pilot。我们可以使用以下命令找到代理配置:
$ istioctl proxy-config bootstrap -n istio-egressgateway-9b7866bf5-8p5rt.istio-system
{
"bootstrap": {
"node": {
"id": "router~172.30.86.14~ istio-egressgateway-9b7866bf5-8p5rt -system~istio-system.svc.cluster.local",
"cluster": "istio-ingressgateway",
"metadata": {
"POD_NAME": " istio-egressgateway-9b7866bf5-8p5rt ",
"istio": "sidecar"
},
"buildVersion": "0/1.8.0 //RELEASE"
},
## REMOVED for BREVITY
}
最后,我们可以调查代理的日志,了解它的行为。可以使用logs
命令访问代理日志。
$kubectl logs pod/frontend-deployment-78d98b75f4-rwkbm istio-proxy
[2019-09-15T20:17:00.310Z] "GET / HTTP/2" 204 - 154 0 226 100 "10.0.35.28"
"" "cc21d9b0-cf5c-432b-8c7e-98aeb7988cd2" "" "tcp://10.0.2.1:8080"
[2019-09-15T20:17:01.102Z] "GET / HTTP/2" 204 - 154 0 226 100 "10.0.35.28"
"" "cc21d9b0-tfdvh-432b-n2kqh-75bdf98789" "" "tcp://10.0.2.1:8080"
路线
流量路由是 Istio 最重要的特性之一。你在第 4 和第五章中学习了交通路由。有些情况下,虚拟服务将不起作用,相关的目标规则也可能失败。我们可以通过确定 Istio sidecar 监听的端口来开始排除流量路由故障。
$ istioctl proxy-config listeners istio-ingressgateway-75ddf64567-jtl68.istio-system
ADDRESS PORT TYPE
0.0.0.0 15090 HTTP
前面的命令总结了代理正在侦听的端口。要获得如何为特定端口上的流量设置代理的详细信息,我们需要更多的详细信息。这可以使用以下命令来完成:
$ istioctl proxy-config listeners istio-egressgateway-9b7866bf5-8p5rt.istio-system -o json --address 0.0.0.0 --port 15090
[
. . . . .
{
"address": {
"socketAddress": {
"address": "0.0.0.0",
"portValue": 15090
}
},
. . . . .
]
前面的命令显示了用于指定端口的路由名称。我们可以使用以下命令找出哪些主机被解析为路由:
$ istioctl proxy-config routes frontend-v1-6549877cc8-67cc8 --name 8080 -o json
[
{
"name": "8080",
"virtualHosts": [
{
"name": "webservice.default.svc.cluster.local:8080",
"domains": [
"webservice.default.svc.cluster.local",
"webservice.default.svc.cluster.local:8080",
"webservice",
"webservice:8080",
"webservice.default.svc.cluster",
"webservice.default.svc.cluster:8080",
### REMOVED for BREVITY
],
"routes": [
{
"match": {
"prefix": "/"
},
"route": {
"cluster": "outbound|8080||webservice.default.svc.cluster.local",
"timeout": "0.000s"
},
...
我们可以看到不同的 web 服务域和 IP 地址被解析为一个出站地址。解析的出站地址被配置为群集位置,这可以使用以下命令来确定:
$ istioctl proxy-config cluster frontend-v1-6549877cc8-67cc8 --fqdn webservice.default.svc.cluster.local -o json
[
{
"name": "outbound|8080||webservice.default.svc.cluster.local",
"type": "EDS",
"edsClusterConfig": {
"edsConfig": {
"ads": {}
},
"serviceName": "outbound|8080||webservice.default.svc.cluster.local"
},
"connectTimeout": "1.000s",
"circuitBreakers": {
"thresholds": [
{}
]
}
}
]
最后,我们可以验证群集位置的位置端点。
$ istioctl proxy-config endpoints frontend-v1-6549877cc8-67cc8 --cluster "outbound|8080||webservice.default.svc.cluster.local"
ENDPOINT STATUS OUTLIER CHECK CLUSTER
172.17.0.17:8080 HEALTHY OK outbound|8080||webservice.default.svc.cluster.local
172.17.0.18:8080 HEALTHY OK outbound|8080||webservice.default.svc.cluster.local
172.17.0.5:8080 HEALTHY OK outbound|8080||webservice.default.svc.cluster.local
目标规则还用于配置客户端的相互 TLS 身份验证。但有时目的地规则不起作用,握手会失败,并出现 503 错误代码。在这种情况下,我们必须检查目的地规则是否违反了现有的配置。
$ istioctl authn tls-check istio-ingressgateway-75ddf64567-jtl68.istio-system
HOST:PORT STATUS SERVER CLIENT AUTHN POLICY DESTINATION RULE
grafana.istio-system.svc.cluster.local:3000 OK HTTP HTTP grafana-ports-mtls-disabled/istio-system -
istio-citadel.istio-system.svc.cluster.local:8060 OK HTTP/mTLS HTTP default/ -
istio-citadel.istio-system.svc.cluster.local:15014 OK HTTP/mTLS HTTP default/ -
摘要
在本章中,我们讨论了解决 Istio 问题的命令。我们展示了如何使用 Kubernetes cm
命令找出 Istio 配置细节。此后,我们查看了代理日志并检查了部署在 Istio 中的目的地规则。Istio 是一个复杂的分布式应用,很难理解每一个细微差别。在事故期间,您可以使用本章介绍的命令来调试配置,以便进行根本原因分析。