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配置,另一个来自用户身份令牌。在这种情况下,我们从用户令牌配置绑定主体。 -
我们已经从身份验证中排除了
/heathURL,因为它被 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 规则引擎,它可用于执行检查,如黑名单、请求配额等。