原文:
zh.annas-archive.org/md5/50389059E7B6623191724DBC60F2DDF3
译者:飞龙
第十一章:处理系统中的变更、依赖和秘密
在本章中,我们将描述与多个微服务交互的不同元素。
我们将研究如何制定服务描述其版本的策略,以便依赖的微服务可以发现它们,并确保它们已经部署了正确的依赖关系。这将允许我们在依赖服务中定义部署顺序,并且如果不是所有依赖关系都准备好,将停止服务的部署。
本章描述了如何定义集群范围的配置参数,以便它们可以在多个微服务之间共享,并在单个位置进行管理,使用 Kubernetes ConfigMap。我们还将学习如何处理那些属于秘密的配置参数,比如加密密钥,这些密钥不应该对团队中的大多数人可见。
本章将涵盖以下主题:
-
理解微服务之间的共享配置
-
处理 Kubernetes 秘密
-
定义影响多个服务的新功能
-
处理服务依赖关系
在本章结束时,您将了解如何为安全部署准备依赖服务,以及如何在微服务中包含不会在其预期部署之外可访问的秘密。
技术要求
代码可在 GitHub 上的以下 URL 找到:github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter11
。请注意,该代码是Chapter10
代码的扩展,其中包含本章描述的额外元素。结构相同,有一个名为microservices
的子目录,其中包含代码,另一个名为kubernetes
的子目录,其中包含 Kubernetes 配置文件。
要安装集群,您需要使用以下命令构建每个单独的微服务:
$ cd Chapter11/microservices/
$ cd rsyslog
$ docker-compose build
...
$ cd frontend
$ ./build-test.sh
...
$ cd thoughts_backend
$./build-test.sh
...
$ cd users_backend
$ ./build-test.sh
...
这将构建所需的服务。
请注意,我们使用build-test.sh
脚本。我们将在本章中解释它的工作原理。
然后,创建namespace
示例,并使用Chapter11/kubernetes
子目录中的配置启动 Kubernetes 集群:
$ cd Chapter11/kubernetes
$ kubectl create namespace example
$ kubectl apply --recursive -f .
...
这将在集群中部署微服务。
Chapter11
中包含的代码存在一些问题,在修复之前将无法正确部署。这是预期的行为。在本章中,我们将解释两个问题:无法配置秘密,以及无法满足前端的依赖关系,导致无法启动。
继续阅读本章以找到所描述的问题。解决方案将作为评估提出。
要能够访问不同的服务,您需要更新您的/etc/hosts
文件,包括以下行:
127.0.0.1 thoughts.example.local
127.0.0.1 users.example.local
127.0.0.1 frontend.example.local
有了这些,您就可以访问本章的服务了。
理解微服务之间的共享配置
某些配置可能适用于多个微服务。在我们的示例中,我们正在为数据库连接重复相同的值。我们可以使用 ConfigMap 并在不同的部署中共享它,而不是在每个部署文件中重复这些值。
我们已经看到如何在第十章 监控日志和指标 的设置指标部分中添加 ConfigMap 以包含文件。尽管它只用于单个服务。
ConfigMap 是一组键/值元素。它们可以作为环境变量或文件添加。在下一节中,我们将添加一个包含集群中所有共享变量的通用配置文件。
添加 ConfigMap 文件
configuration.yaml
文件包含系统的公共配置。它位于Chapter11/kubernetes
子目录中:
---
apiVersion: v1
kind: ConfigMap
metadata:
name: shared-config
namespace: example
data:
DATABASE_ENGINE: POSTGRES
POSTGRES_USER: postgres
POSTGRES_HOST: "127.0.0.1"
POSTGRES_PORT: "5432"
THOUGHTS_BACKEND_URL: http://thoughts-service
USER_BACKEND_URL: http://users-service
与数据库相关的变量,如DATABASE_ENGINE
、POSTGRES_USER
、POSTGRES_HOST
和POSTGRES_PORT
,在 Thoughts Backend 和 Users Backend 之间共享。
POSTGRES_PASSWORD
变量是一个密钥。我们将在本章的处理 Kubernetes 密钥部分中描述这一点。
THOUGHTS_BACKEND_URL
和USER_BACKEND_URL
变量在前端服务中使用。尽管它们在集群中是通用的。任何想要连接到 Thoughts Backend 的服务都应该使用与THOUGHTS_BACKEND_URL
中描述的相同 URL。
尽管它目前只在单个服务 Frontend 中使用,但它符合系统范围的变量的描述,并应包含在通用配置中。
拥有共享变量存储库的一个优点是将它们合并。
在创建多个服务并独立开发它们的同时,很常见的情况是最终以两种略有不同的方式使用相同的信息。独立开发的团队无法完美共享信息,这种不匹配会发生。
例如,一个服务可以将一个端点描述为URL=http://service/api
,另一个使用相同端点的服务将其描述为HOST=service PATH=/api
。每个服务的代码处理配置方式不同,尽管它们连接到相同的端点。这使得以统一方式更改端点更加困难,因为需要在两个或更多位置以两种方式进行更改。
共享位置是首先检测这些问题的好方法,因为如果每个服务保留自己独立的配置,这些问题通常会被忽略,然后调整服务以使用相同的变量,减少配置的复杂性。
在我们的示例中,ConfigMap 的名称是shared-config
,如元数据中所定义的,像任何其他 Kubernetes 对象一样,可以通过kubectl
命令进行管理。
使用 kubectl 命令
可以使用通常的一组kubectl
命令来检查 ConfigMap 信息。这使我们能够发现集群中定义的 ConfigMap 实例:
$ kubectl get configmap -n example shared-config
NAME DATA AGE
shared-config 6 46m
请注意,ConfigMap 包含的键或变量的数量是显示的;在这里,它是6
。要查看 ConfigMap 的内容,请使用describe
:
$ kubectl describe configmap -n example shared-config
Name: shared-config
Namespace: example
Labels: <none>
Annotations: kubectl.kubernetes.io/last-applied-configuration:
{"apiVersion":"v1","data":{"DATABASE_ENGINE":"POSTGRES","POSTGRES_HOST":"127.0.0.1","POSTGRES_PORT":"5432","POSTGRES_USER":"postgres","THO...
Data
====
POSTGRES_HOST:
----
127.0.0.1
POSTGRES_PORT:
----
5432
POSTGRES_USER:
----
postgres
THOUGHTS_BACKEND_URL:
----
http://thoughts-service
USER_BACKEND_URL:
----
http://users-service
DATABASE_ENGINE:
----
POSTGRES
如果需要更改 ConfigMap,可以使用kubectl edit
命令,或者更好的是更改configuration.yaml
文件,并使用以下命令重新应用它:
$ kubectl apply -f kubernetes/configuration.yaml
这将覆盖所有的值。
配置不会自动应用到 Kubernetes 集群。您需要重新部署受更改影响的 pod。最简单的方法是删除受影响的 pod,并允许部署重新创建它们。
另一方面,如果配置了 Flux,它将自动重新部署依赖的 pod。请记住,更改 ConfigMap(在所有 pod 中引用)将触发在该情况下所有 pod 的重新部署。
我们现在将看到如何将 ConfigMap 添加到部署中。
将 ConfigMap 添加到部署
一旦 ConfigMap 就位,它可以用于与不同部署共享其变量,保持一个中央位置来更改变量并避免重复。
让我们看看微服务(Thoughts Backend、Users Backend 和 Frontend)的每个部署如何使用shared-config
ConfigMap。
Thoughts Backend ConfigMap 配置
Thoughts Backend 部署定义如下:
spec:
containers:
- name: thoughts-backend-service
image: thoughts_server:v1.5
imagePullPolicy: Never
ports:
- containerPort: 8000
envFrom:
- configMapRef:
name: shared-config
env:
- name: POSTGRES_DB
value: thoughts
...
完整的shared-config
ConfigMap 将被注入到 pod 中。请注意,这包括以前在 pod 中不可用的THOUGHTS_BACKEND_URL
和USER_BACKEND_URL
环境变量。可以添加更多环境变量。在这里,我们保留了POSTGRES_DB
,而没有将其添加到 ConfigMap 中。
我们可以在 pod 中使用exec
来确认它。
请注意,为了能够连接到密钥,它应该被正确配置。请参阅处理 Kubernetes 密钥部分。
要在容器内部检查,请检索 pod 名称并在其中使用exec
,如下面的命令所示:
$ kubectl get pods -n example
NAME READY STATUS RESTARTS AGE
thoughts-backend-5c8484d74d-ql8hv 2/2 Running 0 17m
...
$ kubectl exec -it thoughts-backend-5c8484d74d-ql8hv -n example /bin/sh
Defaulting container name to thoughts-backend-service.
/opt/code $ env | grep POSTGRES
DATABASE_ENGINE=POSTGRESQL
POSTGRES_HOST=127.0.0.1
POSTGRES_USER=postgres
POSTGRES_PORT=5432
POSTGRES_DB=thoughts
/opt/code $ env | grep URL
THOUGHTS_BACKEND_URL=http://thoughts-service
USER_BACKEND_URL=http://users-service
env
命令返回所有环境变量,但 Kubernetes 会自动添加很多环境变量。
用户后端 ConfigMap 配置
用户后端配置与我们刚刚看到的前一种类型的配置类似:
spec:
containers:
- name: users-backend-service
image: users_server:v2.3
imagePullPolicy: Never
ports:
- containerPort: 8000
envFrom:
- configMapRef:
name: shared-config
env:
- name: POSTGRES_DB
value: thoughts
...
POSTGRES_DB
的值与 Thoughts 后端中的相同,但我们将其留在这里以展示如何添加更多环境变量。
前端 ConfigMap 配置
前端配置仅使用 ConfigMap,因为不需要额外的环境变量:
spec:
containers:
- name: frontend-service
image: thoughts_frontend:v3.7
imagePullPolicy: Never
ports:
- containerPort: 8000
envFrom:
- configMapRef:
name: shared-config
前端 pod 现在还将包括连接到数据库的信息,尽管它不需要。对于大多数配置参数来说,这是可以的。
如果需要,您还可以使用多个 ConfigMaps 来描述不同的配置组。不过,将它们放在一个大桶中处理会更简单。这将有助于捕获重复的参数,并确保所有微服务中都有所需的参数。
然而,一些配置参数必须更加小心处理,因为它们将是敏感的。例如,我们从shared-config
ConfigMap 中省略了POSTGRES_PASSWORD
变量。这允许我们登录到数据库,并且不应该存储在任何带有其他参数的文件中,以避免意外暴露。
为了处理这种信息,我们可以使用 Kubernetes 秘密。
处理 Kubernetes 秘密
秘密是一种特殊的配置。它们需要受到保护,以免被其他使用它们的微服务读取。它们通常是敏感数据,如私钥、加密密钥和密码。
记住,读取秘密是有效的操作。毕竟,它们需要被使用。秘密与其他配置参数的区别在于它们需要受到保护,因此只有授权的来源才能读取它们。
秘密应该由环境注入。这要求代码能够检索配置秘密并在当前环境中使用适当的秘密。它还避免了在代码中存储秘密。
记住永远不要在 Git 存储库中提交生产秘密。即使删除了 Git 树,秘密也是可检索的。这包括 GitOps 环境。
还要为不同的环境使用不同的秘密。生产秘密需要比测试环境中的秘密更加小心。
在我们的 Kubernetes 配置中,授权的来源是使用它们的微服务以及通过kubectl
访问的系统管理员。
让我们看看如何管理这些秘密。
在 Kubernetes 中存储秘密
Kubernetes 将秘密视为一种特殊类型的 ConfigMap 值。它们可以在系统中定义,然后以与 ConfigMap 相同的方式应用。与一般的 ConfigMap 的区别在于信息在内部受到保护。虽然它们可以通过kubectl
访问,但它们受到意外暴露的保护。
可以通过kubectl
命令在集群中创建秘密。它们不应该通过文件和 GitOps 或 Flux 创建,而应该手动创建。这样可以避免将秘密存储在 GitOps 存储库下。
需要秘密来操作的 pod 将在其部署文件中指示。这是安全的存储在 GitOps 源代码控制下,因为它不存储秘密,而只存储对秘密的引用。当 pod 被部署时,它将使用适当的引用并解码秘密。
登录到 pod 将授予您对秘密的访问权限。这是正常的,因为在 pod 内部,应用程序需要读取其值。授予对 pod 中执行命令的访问权限将授予他们对内部秘密的访问权限,因此请记住这一点。您可以阅读 Kubernetes 文档了解秘密的最佳实践,并根据您的要求进行调整(kubernetes.io/docs/concepts/configuration/secret/#best-practices
)。
既然我们知道如何处理它们,让我们看看如何创建这些秘密。
创建秘密
让我们在 Kubernetes 中创建这些秘密。我们将存储以下秘密:
-
PostgreSQL 密码
-
用于签署和验证请求的公钥和私钥
我们将它们存储在同一个 Kubernetes 秘密中,该秘密可以有多个密钥。以下命令显示了如何生成一对密钥:
$ openssl genrsa -out private_key.pem 2048
Generating RSA private key, 2048 bit long modulus
........+++
.................+++
e is 65537 (0x10001)
$ openssl rsa -in private_key.pem -outform PEM -pubout -out public_key.pub
writing RSA key
$ ls
private_key.pem public_key.pub
这些密钥是唯一的。我们将使用它们来替换前几章中存储的示例密钥。
在集群中存储秘密
将秘密存储在集群中,在thoughts-secrets
秘密下。请记住将其存储在example
命名空间中:
$ kubectl create secret generic thoughts-secrets --from-literal=postgres-password=somepassword --from-file=private_key.pem --from-file=public_key.pub -n example
您可以列出命名空间中的秘密:
$ kubectl get secrets -n example
NAME TYPE DATA AGE
thoughts-secrets Opaque 3 41s
您还可以描述更多信息的秘密:
$ kubectl describe secret thoughts-secrets -n example
Name: thoughts-secrets
Namespace: default
Labels: <none>
Annotations: <none>
Type: Opaque
Data
====
postgres-password: 12 bytes
private_key.pem: 1831 bytes
public_key.pub: 408 bytes
您可以获取秘密的内容,但数据以 Base64 编码检索。
Base64 是一种编码方案,允许您将二进制数据转换为文本,反之亦然。它被广泛使用。这使您可以存储任何二进制秘密,而不仅仅是文本。这也意味着在检索时秘密不会以明文显示,从而在意外显示在屏幕上等情况下增加了一层保护。
要获取秘密,请使用如下所示的常规kubectl get
命令。我们使用base64
命令对其进行解码:
$ kubectl get secret thoughts-secrets -o yaml -n example
apiVersion: v1
data:
postgres-password: c29tZXBhc3N3b3Jk
private_key.pem: ...
public_key.pub: ...
$ echo c29tZXBhc3N3b3Jk | base64 --decode
somepassword
同样,如果要编辑秘密以更新它,输入应该以 Base64 编码。
秘密部署配置
我们需要在部署配置中配置秘密的使用,以便在所需的 pod 中可用。例如,在用户后端的deployment.yaml
配置文件中,我们有以下代码:
spec:
containers:
- name: users-backend-service
...
env:
...
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: thoughts-secrets
key: postgres-password
volumeMounts:
- name: sign-keys
mountPath: "/opt/keys/"
volumes:
- name: sign-keys
secret:
secretName: thoughts-secrets
items:
- key: public_key.pub
path: public_key.pub
- key: private_key.pem
path: private_key.pem
我们创建了来自秘密的POSTGRES_PASSWORD
环境变量。我们还创建了一个名为sign-keys
的卷,其中包含两个密钥文件,public_key.pub
和private_key.pem
。它挂载在/opt/keys/
路径中。
类似地,Thoughts 后端的deployment.yaml
文件包括秘密,但只包括 PostgreSQL 密码和public_key.pub
。请注意,私钥没有添加,因为 Thoughts 后端不需要它,也不可用。
对于前端,只需要公钥。现在,让我们来建立如何检索这些秘密。
应用程序检索秘密
对于POSTGRES_PASSWORD
环境变量,我们不需要更改任何内容。它已经是一个环境变量,代码已经从中提取它。
但是对于存储为文件的秘密,我们需要从适当的位置检索它们。存储为文件的秘密是签署身份验证标头的关键。公共文件在所有微服务中都是必需的,而私钥仅在用户后端中使用。
现在,让我们来看一下用户后端的config.py
文件:
import os
PRIVATE_KEY = ...
PUBLIC_KEY = ...
PUBLIC_KEY_PATH = '/opt/keys/public_key.pub'
PRIVATE_KEY_PATH = '/opt/keys/private_key.pem'
if os.path.isfile(PUBLIC_KEY_PATH):
with open(PUBLIC_KEY_PATH) as fp:
PUBLIC_KEY = fp.read()
if os.path.isfile(PRIVATE_KEY_PATH):
with open(PRIVATE_KEY_PATH) as fp:
PRIVATE_KEY = fp.read()
当前密钥仍然作为默认值存在。当秘密文件没有挂载时,它们将用于单元测试。
再次强调,请不要使用这些密钥。这些仅用于运行测试,并且对于任何可以访问本书的人都是可用的。
如果/opt/keys/
路径中存在文件,它们将被读取,并且内容将被存储在适当的常量中。用户后端需要公钥和私钥。
在 Thoughts 后端的config.py
文件中,我们只检索公钥,如下所示:
import os
PUBLIC_KEY = ...
PUBLIC_KEY_PATH = '/opt/keys/public_key.pub'
if os.path.isfile(PUBLIC_KEY_PATH):
with open(PUBLIC_KEY_PATH) as fp:
PUBLIC_KEY = fp.read()
前端服务将公钥添加到settings.py
文件中:
TOKENS_PUBLIC_KEY = ...
PUBLIC_KEY_PATH = '/opt/keys/public_key.pub'
if os.path.isfile(PUBLIC_KEY_PATH):
with open(PUBLIC_KEY_PATH) as fp:
TOKENS_PUBLIC_KEY = fp.read()
此配置使秘密对应用程序可用,并为秘密值关闭了循环。现在,微服务集群使用来自秘密值的签名密钥,这是一种安全存储敏感数据的方式。
定义影响多个服务的新功能
我们谈到了单个微服务领域内的更改请求。但是,如果我们需要部署在两个或多个微服务中运行的功能,该怎么办呢?
这种类型的功能应该相对罕见,并且是与单体应用程序相比微服务中的开销的主要原因之一。在单体应用程序中,这种情况根本不可能发生,因为一切都包含在单体应用程序的墙内。
与此同时,在微服务架构中,这是一个复杂的更改。这至少涉及到每个相关微服务中的两个独立功能,这些功能位于两个不同的存储库中。很可能这些存储库将由两个不同的团队开发,或者至少负责每个功能的人将不同。
逐个更改
为了确保功能可以顺利部署,一次一个,它们需要保持向后兼容。这意味着您需要能够在服务 A 已部署但服务 B 尚未部署的中间阶段生存。微服务中的每个更改都需要尽可能小,以最小化风险,并且应逐个引入更改。
为什么不同时部署它们?因为同时发布两个微服务是危险的。首先,部署不是瞬时的,因此会有时刻,过时的服务将发送或接收系统尚未准备处理的调用。这将导致可能影响您的客户的错误。
但是存在一种情况,其中一个微服务不正确并且需要回滚。然后,系统将处于不一致状态。依赖的微服务也需要回滚。这本身就是有问题的,但是当在调试此问题期间,两个微服务都卡住并且在问题得到解决之前无法更新时,情况会变得更糟。
在健康的微服务环境中,部署会经常发生。因为另一个服务需要工作而不得不停止微服务的流水线是一个糟糕的处境,它只会增加压力和紧迫感。
记住我们谈到了部署和变更的速度。经常部署小的增量通常是确保每次部署都具有高质量的最佳方式。增量工作的持续流非常重要。
由于错误而中断此流程是不好的,但是如果无法部署影响了多个微服务的速度,影响会迅速扩大。
同时部署多个服务也可能导致死锁,其中两个服务都需要进行修复工作。这会使开发和解决问题的时间变得更加复杂。
需要进行分析以确定哪个微服务依赖于另一个而不是同时部署。大多数情况下,这是显而易见的。在我们的例子中,前端依赖于 Thoughts 后端,因此任何涉及它们两者的更改都需要从 Thoughts 后端开始,然后转移到前端。
实际上,用户后端是两者的依赖项,因此假设有一个影响它们三者的更改,您需要首先更改用户后端,然后是 Thoughts 后端,最后是前端。
请记住,有时部署可能需要跨多个服务进行多次移动。例如,让我们想象一下,我们对身份验证标头的签名机制进行了更改。然后,流程应该如下:
-
在用户后端实施新的身份验证系统,但通过配置更改继续使用旧系统生成令牌。到目前为止,集群仍在使用旧的身份验证流程。
-
更改 Thoughts 后端以允许与旧系统和新的身份验证系统一起工作。请注意,它尚未激活。
-
更改前端以使其与两种身份验证系统一起工作。但是,此时新系统尚未被使用。
-
在用户后端更改配置以生成新的身份验证令牌。现在是新系统开始使用的时候。在部署过程中,可能会生成一些旧系统令牌。
-
用户后端和前端将使用系统中的任何令牌,无论是新的还是旧的。旧令牌将随着时间的推移而消失,因为它们会过期。只有新令牌才会被创建。
-
作为可选阶段,可以从系统中删除旧的身份验证系统。三个系统可以在没有任何依赖关系的情况下删除它们,因为此时系统不再使用。
在整个过程的任何步骤中,服务都不会中断。每个单独的更改都是安全的。该过程正在慢慢使整个系统发展,但如果出现问题,每个单独的步骤都是可逆的,并且服务不会中断。
系统往往通过添加新功能来发展,清理阶段并不常见。通常,即使功能在任何地方都没有使用,系统也会长时间保留已弃用的功能。
我们将在《第十二章》跨团队协作和沟通中更详细地讨论清理工作。
此过程也可能需要进行配置更改。例如,更改用于签署身份验证标头的私钥将需要以下步骤:
-
使 Thoughts 后端和前端能够处理多个公钥。这是一个先决条件和一个新功能。
-
更改 Thoughts 后端中处理的密钥,使其同时具有旧公钥和新公钥。到目前为止,系统中没有使用新密钥签名的标头。
-
更改前端中处理的密钥,使其同时具有旧密钥和新密钥。但是,系统中仍没有使用新密钥签名的标头。
-
更改用户后端的配置以使用新的私钥。从现在开始,系统中有用新私钥签名的标头。其他微服务能够处理它们。
-
系统仍然接受用旧密钥签名的标头。等待一个安全期以确保所有旧标头都已过期。
-
删除用户后端的旧密钥配置。
步骤 2 至 6 可以每隔几个月重复使用新密钥。
这个过程被称为密钥轮换,被认为是一种良好的安全实践,因为它减少了密钥有效的时间,缩短了系统暴露于泄露密钥的时间窗口。为简单起见,我们没有在示例系统中实施它,但建议您这样做。尝试更改示例代码以实现此密钥轮换示例!
完整的系统功能可能涉及多个服务和团队。为了帮助协调系统的依赖关系,我们需要知道某个服务的特定依赖项何时部署并准备就绪。我们将在《第十二章》跨团队协作和沟通中讨论团队间的沟通,但我们可以通过使服务 API 明确描述已部署的服务版本来通过编程方式进行帮助,正如我们将在处理服务依赖关系部分中讨论的那样。
如果新版本出现问题,刚刚部署的版本可以通过回滚快速恢复。
回滚微服务
回滚是将微服务之一迅速退回到先前版本的过程。
当新版本出现灾难性错误时,可以触发此过程,以便快速解决问题。鉴于该版本已经兼容,可以在非常短的反应时间内放心地进行此操作。通过 GitOps 原则,可以执行revert
提交以恢复旧版本。
git revert
命令允许您创建一个撤消另一个提交的提交,以相反的方式应用相同的更改。
这是撤消特定更改的快速方法,并允许稍后撤消撤消并重新引入更改。您可以查看 Git 文档以获取更多详细信息(git-scm.com/docs/git-revert
)。
鉴于保持前进的战略性方法,回滚是一种临时措施,一旦实施,将停止微服务中的新部署。应尽快创建一个解决导致灾难性部署的错误的新版本,以保持正常的发布流程。
随着部署次数的增加,并且在适当的位置进行更好的检查,回滚将变得越来越少。
处理服务依赖关系
为了让服务检查它们的依赖项是否具有正确的版本,我们将使服务通过 RESTful 端点公开它们的版本。
我们将遵循 GitHub 上的 Thoughts Backend 示例,网址为:github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter11/microservices/thoughts_backend
。
在前端检查版本是否可用(github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter11/microservices/frontend
)。
该过程的第一步是为每个服务正确定义版本。
服务版本控制
为了清晰地了解软件的进展,我们需要命名要部署的不同版本。由于我们使用git
来跟踪更改,系统中的每个提交都有一个独立的提交 ID,但它没有遵循任何特定的模式。
为了赋予其意义并对其进行排序,我们需要开发一个版本模式。有多种制定版本模式的方法,包括按发布日期(Ubuntu 使用此方法)或按major.minor.patch
。
在所有地方使用相同的版本控制方案有助于在团队之间发展共同的语言和理解。它还有助于管理了解变化,无论是在发布时的变化还是变化的速度。与您的团队商定一个在您的组织中有意义的版本控制方案,并在所有服务中遵循它。
在此示例中,我们将使用vMajor.Minor
模式,并将用户后端的版本设置为v2.3
。
软件版本控制中最常见的模式是语义版本控制。这种版本控制模式对于软件包和面向客户的 API 非常有用,但对于内部微服务 API 则不太有用。让我们看看它的特点是什么。
语义版本控制
语义版本控制对不同版本号的每个更改赋予了含义。这使得很容易理解各个版本之间的变化范围,以及更新是否对依赖系统有风险。
语义版本控制使用三个数字定义每个版本:主要版本、次要版本和补丁版本,通常描述为major.minor.patch
。
增加这些数字中的任何一个都具有特定的含义,如下所示:
-
增加主要版本号会产生不兼容的变化。
-
增加次要版本号会添加新功能,但保持向后兼容。
-
增加补丁号修复错误,但不添加任何新功能。
例如,Python 按照以下模式工作:
-
Python 3 与 Python 2 包含了兼容性变化。
-
Python 3.7 版本与 Python 3.6 相比引入了新功能。
-
Python 3.7.4 版本相对于 Python 3.7.3 增加了安全性和错误修复。
这种版本控制方案在与外部合作伙伴的沟通中非常有用,并且非常适用于大型发布和标准软件包。但对于微服务中的小型增量变化,它并不是非常有用。
正如我们在前面的章节中讨论的那样,持续集成的关键是进行非常小的更改。它们不应该破坏向后兼容性,但随着时间的推移,旧功能将被删除。每个微服务都以受控的方式与其他服务协同工作。与外部包相比,没有必要具有如此强烈的功能标签。服务的消费者是集群中受严格控制的其他微服务。
一些项目由于操作方式的改变而放弃了语义版本。例如,Linux 内核停止使用语义版本来生成没有特定含义的新版本(lkml.iu.edu/hypermail/linux/kernel/1804.1/06654.html
),因为从一个版本到下一个版本的更改相对较小。
Python 也将版本 4.0 视为在 3.9 之后的版本,并且不像 Python 3 那样有重大变化(www.curiousefficiency.org/posts/2014/08/python-4000.html
)。
这就是为什么在内部不建议使用语义版本。保持类似的版本方案可能是有用的,但不要强制它进行兼容性更改,只需增加数字,而不对何时更改次要或主要版本做出具体要求。
然而,从外部来看,版本号可能仍然具有营销意义。对于外部可访问的端点,使用语义版本可能是有趣的。
一旦确定了服务的版本,我们就可以着手创建一个公开此信息的端点。
添加版本端点
要部署的版本可以从 Kubernetes 部署或 GitOps 配置中读取。但是存在一个问题。一些配置可能会误导或不唯一地指向单个镜像。例如,latest
标签可能在不同时间代表不同的容器,因为它会被覆盖。
此外,还存在访问 Kubernetes 配置或 GitOps 存储库的问题。对于开发人员来说,也许这些配置是可用的,但对于微服务来说不会(也不应该)。
为了让集群中的其他微服务发现服务的版本,最好的方法是在 RESTful API 中明确创建一个版本端点。服务版本的发现是被授予的,因为它使用与任何其他请求中将使用的相同接口。让我们看看如何实现它。
获取版本
为了提供版本,我们首先需要将其记录到服务中。
正如我们之前讨论过的,版本是存储为 Git 标签的。这将是我们版本的标准。我们还将添加提交的 Git SHA-1,以避免任何差异。
SHA-1 是一个唯一的标识符,用于标识每个提交。它是通过对 Git 树进行哈希处理而生成的,因此能够捕获任何更改——无论是内容还是树历史。我们将使用 40 个字符的完整 SHA-1,尽管有时它会被缩写为八个或更少。
提交的 SHA-1 可以通过以下命令获得:
$ git log --format=format:%H -n 1
这将打印出最后一次提交的信息,以及带有%H
描述符的 SHA。
要获取此提交所指的标签,我们将使用git-describe
命令:
$ git describe --tags
基本上,git-describe
会找到最接近当前提交的标签。如果此提交由标签标记,正如我们的部署应该做的那样,它将返回标签本身。如果没有,它将在标签后缀中添加有关提交的额外信息,直到达到当前提交。以下代码显示了如何使用git describe
,具体取决于代码的提交版本。请注意,与标签不相关的代码将返回最接近的标签和额外的数字:
$ # in master branch, 17 commits from the tag v2.3
$ git describe
v2.3-17-g2257f9c
$ # go to the tag
$ git checkout v2.3
$ git describe
v2.3
这将始终返回一个版本,并允许我们一目了然地检查当前提交的代码是否在git
中标记。
将部署到环境中的任何内容都应该被标记。本地开发是另一回事,因为它包括尚未准备好的代码。
我们可以以编程方式存储这两个值,从而使我们能够自动地进行操作,并将它们包含在 Docker 镜像中。
将版本存储在镜像中
我们希望在镜像内部有版本可用。由于镜像是不可变的,所以在构建过程中实现这一目标是我们的目标。我们需要克服的限制是 Dockerfile 过程不允许我们在主机上执行命令,只能在容器内部执行。我们需要在构建时向 Docker 镜像中注入这些值。
一个可能的替代方案是在容器内安装 Git,复制整个 Git 树,并获取值。通常不鼓励这样做,因为安装 Git 和完整的源代码树会给容器增加很多空间,这是不好的。在构建过程中,我们已经有了 Git 可用,所以我们只需要确保外部注入它,这在构建脚本中很容易做到。
通过ARG
参数传递值的最简单方法。作为构建过程的一部分,我们将把它们转换为环境变量,这样它们将像配置的任何其他部分一样容易获取。让我们来看看以下代码中的 Dockerfile:
# Prepare the version
ARG VERSION_SHA="BAD VERSION"
ARG VERSION_NAME="BAD VERSION"
ENV VERSION_SHA $VERSION_SHA
ENV VERSION_NAME $VERSION_NAME
我们接受一个ARG
参数,然后通过ENV
参数将其转换为环境变量。为了简单起见,两者都具有相同的名称。ARG
参数对于特殊情况有一个默认值。
使用build.sh
脚本构建后,这使得版本在构建后(在容器内部)可用,该脚本获取值并调用docker-compose
进行构建,使用版本作为参数,具体步骤如下:
# Obtain the SHA and VERSION
VERSION_SHA=`git log --format=format:%H -n 1`
VERSION_NAME=`git describe --tags`
# Build using docker-compose with arguments
docker-compose build --build-arg VERSION_NAME=${VERSION_NAME} --build-arg VERSION_SHA=${VERSION_SHA}
# Tag the resulting image with the version
docker tag thoughts_server:latest throughs_server:${VERSION_NAME}
在构建过程之后,版本作为标准环境变量在容器内部可用。
在本章的每个微服务中都包含了一个脚本(例如,github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/blob/master/Chapter11/microservices/thoughts_backend/build-test.sh
)。这个脚本模拟 SHA-1 和版本名称,以创建一个用于测试的合成版本。它为用户后端设置了v2.3
版本,为思想后端设置了v1.5
版本。这些将被用作我们代码中的示例。
检查 Kubernetes 部署是否包含这些版本(例如,github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/blob/master/Chapter11/microservices/thoughts_backend/docker-compose.yaml#L21
镜像是v1.5
版本)。
此外,VERSION_NAME
也可以作为 CI 管道的参数传递给脚本。为此,您需要替换脚本以接受外部参数,就像在build-ci.sh
脚本中看到的那样:
#!/bin/bash
if [ -z "$1" ]
then
# Error, not version name
echo "No VERSION_NAME supplied"
exit -1
fi
VERSION_SHA=`git log --format=format:%H -n 1`
VERSION_NAME=$1
docker-compose build --build-arg VERSION_NAME=${VERSION_NAME} --build-arg VERSION_SHA=${VERSION_SHA}
docker tag thoughts_server:latest throughs_server:${VERSION_NAME}
所有这些脚本的版本都包括使用VERSION_NAME
作为标签对镜像进行标记。
我们可以在 Python 代码中在容器内检索包含版本的环境变量,并在端点中返回它们,使版本通过外部 API 轻松访问。
实现版本端点
在admin_namespace.py
文件中,我们将使用以下代码创建一个新的Version
端点:
import os
@admin_namespace.route('/version/')
class Version(Resource):
@admin_namespace.doc('get_version')
def get(self):
'''
Return the version of the application
'''
data = {
'commit': os.environ['VERSION_SHA'],
'version': os.environ['VERSION_NAME'],
}
return data
现在,这段代码非常简单。它使用os.environ
来检索在构建过程中注入的环境变量作为配置参数,并返回一个包含提交 SHA-1 和标签(描述为版本)的字典。
可以使用docker-compose
在本地构建和运行服务。要测试对/admin/version
端点的访问并进行检查,请按照以下步骤进行:
$ cd Chapter11/microservices/thoughts_backend
$ ./build.sh
...
Successfully tagged thoughts_server:latest
$ docker-compose up -d server
Creating network "thoughts_backend_default" with the default driver
Creating thoughts_backend_db_1 ... done
Creating thoughts_backend_server_1 ... done
$ curl http://localhost:8000/admin/version/
{"commit": "2257f9c5a5a3d877f5f22e5416c27e486f507946", "version": "tag-17-g2257f9c"}
由于版本可用,我们可以更新自动生成的文档以显示正确的值,如app.py
中所示:
import os
...
VERSION = os.environ['VERSION_NAME']
...
def create_app(script=False):
...
api = Api(application, version=VERSION,
title='Thoughts Backend API',
description='A Simple CRUD API')
因此,版本将在自动生成的 Swagger 文档中正确显示。一旦微服务的版本通过 API 中的端点可访问,其他外部服务就可以访问它以发现版本并加以利用。
检查版本
通过 API 能够检查版本使我们能够以编程方式轻松访问版本。这可以用于多种目的,比如生成一个仪表板,显示不同环境中部署的不同版本。但我们将探讨引入服务依赖的可能性。
当微服务启动时,可以检查其所依赖的服务,并检查它们是否高于预期版本。如果不是,它将不会启动。这可以避免在依赖服务更新之前部署依赖服务时出现配置问题。这可能发生在部署协调不佳的复杂系统中。
在start_server.sh
中启动服务器时,要检查版本,我们将首先调用一个检查依赖项的小脚本。如果不可用,它将产生错误并停止。我们将检查前端是否具有 Thought 后端的可用版本,甚至更高版本。
我们将在我们的示例中调用的脚本称为check_dependencies_services.py
,并且在前端的start_server.sh
中调用它。
check_dependencies_services
脚本可以分为三个部分:所需依赖项列表;一个依赖项的检查;以及一个主要部分,其中检查每个依赖项。让我们来看看这三个部分。
所需版本
第一部分描述了每个依赖项和所需的最低版本。在我们的示例中,我们规定thoughts_backend
需要是版本v1.6
或更高:
import os
VERSIONS = {
'thoughts_backend':
(f'{os.environ["THOUGHTS_BACKEND_URL"]}/admin/version',
'v1.6'),
}
这里重用环境变量THOUGHTS_BACKEND_URL
,并使用特定版本路径完成 URL。
主要部分遍历了所有描述的依赖项以进行检查。
主要函数
主要函数遍历VERSIONS
字典,并对每个版本执行以下操作:
-
调用端点
-
解析结果并获取版本
-
调用
check_version
来查看是否正确
如果失败,它以-1
状态结束,因此脚本报告为失败。这些步骤通过以下代码执行:
import requests
def main():
for service, (url, min_version) in VERSIONS.items():
print(f'Checking minimum version for {service}')
resp = requests.get(url)
if resp.status_code != 200:
print(f'Error connecting to {url}: {resp}')
exit(-1)
result = resp.json()
version = result['version']
print(f'Minimum {min_version}, found {version}')
if not check_version(min_version, version):
msg = (f'Version {version} is '
'incorrect (min {min_version})')
print(msg)
exit(-1)
if __name__ == '__main__':
main()
主要函数还打印一些消息,以帮助理解不同的阶段。为了调用版本端点,它使用requests
包,并期望200
状态代码和可解析的 JSON 结果。
请注意,此代码会遍历VERSION
字典。到目前为止,我们只添加了一个依赖项,但用户后端是另一个依赖项,可以添加。这留作练习。
版本字段将在check_version
函数中进行检查,我们将在下一节中看到。
检查版本
check_version
函数检查当前返回的版本是否高于或等于最低版本。为了简化,我们将使用natsort
包对版本进行排序,然后检查最低版本。
您可以查看natsort
的完整文档(github.com/SethMMorton/natsort
)。它可以对许多自然字符串进行排序,并且可以在许多情况下使用。
基本上,natsort
支持常见的版本排序模式,其中包括我们之前描述的标准版本模式(v1.6
高于v1.5
)。以下代码使用该库对版本进行排序,并验证最低版本是否为较低版本:
from natsort import natsorted
def check_version(min_version, version):
versions = natsorted([min_version, version])
# Return the lower is the minimum version
return versions[0] == min_version
有了这个脚本,我们现在可以启动服务,并检查 Thoughts 后端是否具有正确的版本。如果您按照技术要求部分中描述的方式启动了服务,您会发现前端无法正确启动,并产生CrashLoopBackOff
状态,如下所示:
$ kubectl get pods -n example
NAME READY STATUS RESTARTS AGE
frontend-54fdfd565b-gcgtt 0/1 CrashLoopBackOff 1 12s
frontend-7489cccfcc-v2cz7 0/1 CrashLoopBackOff 3 72s
grafana-546f55d48c-wgwt5 1/1 Running 2 80s
prometheus-6dd4d5c74f-g9d47 1/1 Running 2 81s
syslog-76fcd6bdcc-zrx65 2/2 Running 4 80s
thoughts-backend-6dc47f5cd8-2xxdp 2/2 Running 0 80s
users-backend-7c64564765-dkfww 2/2 Running 0 81s
检查一个前端 pod 的日志,以查看原因,使用kubectl logs
命令,如下所示:
$ kubectl logs frontend-54fdfd565b-kzn99 -n example
Checking minimum version for thoughts_backend
Minimum v1.6, found v1.5
Version v1.5 is incorrect (min v1.6)
要解决问题,您需要构建一个具有更高版本的 Thoughts 后端版本,或者减少依赖要求。这将作为本章结束时的评估留下。
总结
在本章中,我们学习了如何处理同时与多个微服务一起工作的元素。
首先,我们讨论了在新功能需要更改多个微服务时要遵循的策略,包括如何以有序的方式部署小的增量,并且能够在出现灾难性问题时回滚。
然后,我们讨论了定义清晰的版本模式,并向 RESTful 接口添加了一个版本端点,以允许微服务自我发现版本。这种自我发现可以用来确保依赖于另一个微服务的微服务在没有依赖项的情况下不会被部署,这有助于协调发布。
本章中的前端 GitHub 代码(github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter11/microservices/frontend
)包含对 Thoughts 后端的依赖,这将阻止部署。请注意,原样的代码无法工作。修复留作练习。
我们还学习了如何使用 ConfigMap 来描述在 Kubernetes 集群中共享的配置信息。我们随后描述了如何使用 Kubernetes secrets 来处理敏感且需要额外注意的配置。
在下一章中,我们将看到协调不同团队与不同微服务高效工作的各种技术。
问题
-
在微服务架构系统和单体架构中发布更改的区别是什么?
-
在微服务架构中,为什么发布的更改应该很小?
-
语义版本化是如何工作的?
-
微服务架构系统中内部接口的语义版本化存在哪些问题?
-
添加版本端点的优势是什么?
-
我们如何修复本章代码中的依赖问题?
-
我们应该在共享的 ConfigMap 中存储哪些配置变量?
-
您能描述将所有配置变量放在单个共享的 ConfigMap 中的优缺点吗?
-
Kubernetes ConfigMap 和 Kubernetes secret 之间有什么区别?
-
我们如何更改 Kubernetes secret?
-
假设根据配置,我们决定将
public_key.pub
文件从秘密更改为 ConfigMap。我们需要实施哪些更改?
进一步阅读
要处理 AWS 上的秘密,您可以与一个名为 CredStash 的工具交互(github.com/fugue/credstash
)。您可以在书籍AWS SysOps Cookbook – Second Edition (www.packtpub.com/cloud-networking/aws-administration-cookbook-second-edition
)中了解更多信息。
第十二章:跨团队合作和沟通
正如我们之前讨论的,微服务的主要特点是能够并行开发。为了确保最大效率,我们需要成功协调我们的团队,以避免冲突。在本章中,我们将讨论确保不同团队成功合作所需了解的不同元素。
首先,我们将介绍如何在不同的微服务中获得一致的视野,不同的沟通结构如何塑造软件元素中的沟通,以及如何确保我们不会在软件中积累垃圾。然后,我们将讨论如何确保团队在发布和完善其流程和工具方面协调自己,使它们变得越来越可靠。
本章将涵盖以下主题:
-
保持一致的架构视野
-
分工和康威定律
-
平衡新功能和维护
-
设计更广泛的发布流程
在本章结束时,我们将知道如何构建和协调不同独立工作的团队,以便我们能够充分利用它们。
保持一致的架构视野
在基于微服务的系统中,每个团队能够独立完成大部分任务,独立于其他团队。设计服务,使其尽可能独立并且具有最小的依赖性,对于实现良好的开发速度至关重要。
因此,微服务的分离允许团队独立并行工作,而在单体系统中,大多数人都在关注发生的事情,甚至分散了特定开发人员关注领域之外的工作。他们会知道何时发布新版本,并看到新代码添加到他们正在工作的同一代码库中。然而,在微服务架构中并非如此。在这里,团队专注于他们的服务,不会被其他功能分散注意力。这带来了清晰和高效。
然而,仍然需要一个全局的系统视野。需要对系统的架构如何随时间变化而改变有一个长期的观点,以便能够适应。这种视野(在单片系统中)是隐含的。微服务需要更好地理解这些变化,以便能够有效地工作,因此一个能统一这种全局视野的领先架构师非常重要。
软件行业中的架构师角色并没有一致的定义。
在本书中,我们将其定义为处理 API 和服务整体结构的角色。他们的主要目标是在技术问题上协调团队,而不是直接处理代码。
明确指定一个负责系统全局架构的人有助于我们保持对系统如何发展的长期视野。
在小公司中,首席技术官可能会担任架构师的角色,尽管他们还忙于处理与管理流程和成本相关的元素。
领先架构师的主要责任是确保微服务划分在演变中仍然有意义,并且服务之间通信的 API 是一致的。他们还应该努力促进跨团队的标准生成,并在整个组织中分享知识。
架构师在涉及哪个功能与哪个微服务相关的任何问题以及可能涉及多个团队的其他冲突时,也应该是最终的决策者。这个角色在从单体架构过渡到微服务架构时非常有帮助,但在这个过程完成后,他们也可以确保组织能够适应新的挑战,并控制技术债务。微服务架构系统旨在创建独立的团队,但他们都会从一个外部人员创造的共享全局愿景中受益。
为了更好地协调,团队如何分工是非常重要的。让我们了解一下当我们将系统开发分成不同团队时会出现的一些挑战。
分工和康威定律
微服务架构系统适用于大型软件系统,尽管公司往往从单体应用程序开始。这对于任何有小团队的系统都是有意义的。随着系统的探索和转变,它会随着时间的推移而增长。
但当单体系统增长到一定规模时,它们变得难以处理和开发。由于历史原因,内部变得交织在一起,随着复杂性的增加,系统的可靠性可能会受到影响。在灵活性和冗余之间找到平衡可能很困难。
记住,当开发团队很大时,微服务是有用的。对于小团队来说,单体架构更容易开发和维护。只有当许多开发人员在同一个系统上工作时,分工和接受微服务架构的额外开销才是有意义的。
扩展开发团队可能会变得困难,因为那里会有太多的旧代码,学习如何在其中导航是困难且需要很长时间。开发人员(那些在团队中待了很长时间的人)知道哪些注意事项可以帮助,但他们成为了瓶颈。增加团队的规模并不能帮助,因为任何改变都可能变得复杂。因此,每个新的开发人员在能够成功地进行错误修复和新功能开发之前都需要接受大量的培训。
团队也有一个自然的规模限制。超过这个限制可能意味着必须将其分成更小的团队。
团队的规模是非常灵活的,但通常来说,7±2 个成员被认为是团队中理想人数的经验法则。
更大的团体往往会自行生成更小的团体,但这意味着管理的任务会变得繁重,有些团队可能没有明确的焦点。很难知道其他团队在做什么。
较小的团队在管理和团队间沟通方面往往会产生额外的开销。他们会因为成员更多而开发更快。
在一个大型单体系统中,多个独立团队往往会在没有明确的长期视野的情况下胡乱操作。通过设计一个健壮的内部结构可以缓解这种情况,但这需要巨大的前期规划和严格的监管来确保其得到遵守。
微服务架构是一种解决这些问题的设计,因为它在系统的各个部分之间建立了非常严格的边界。然而,这样做需要开发团队达到一定规模,以便他们可以像几个小团队一样独立工作。这是微服务架构系统的主要特点。构成它的每个微服务都是一个独立的服务,可以独立开发和发布。
这种工作方式允许团队并行工作,没有任何干扰。他们的行动范围是明确的,任何依赖关系都是明确设定的。因此,微服务之间的边界是强大的。
仅仅因为一个微服务可以独立发布并不意味着单个发布就足以发布一个完整的功能。正如我们已经看到的,有时,一个微服务中的一个功能需要在部署之前对另一个微服务进行处理。在这种情况下,需要处理多个微服务。
在规划如何划分团队时,要牢记的最重要的想法是团队结构如何在软件中得到体现。这是由康威定律描述的。
描述康威定律
康威定律是一个软件格言。换句话说,在任何生产软件的组织中,软件将复制组织的通信结构。例如,以一种非常简化的方式,一个组织被分为两个部门:采购和销售。这将产生两个软件模块:一个专注于购买,另一个专注于销售。它们将在需要时进行通信。
在这一部分,我们将讨论软件单元。这是一个通用术语,用来描述任何被视为单一凝聚元素的软件。它可以是一个模块、一个包,或者一个微服务。
在微服务架构中,这些软件单元主要是微服务,但在某些情况下,也可能是其他类型。我们将在将软件划分为不同类型的软件单元部分看到这方面的例子。
这可能并不奇怪。不同团队之间的沟通水平以及同一个团队内部的沟通水平是不同的,这是很自然的。然而,团队合作的影响是巨大的,其中一些如下:
-
团队间的 API 比团队内的 API 更昂贵,无论是在操作上还是在开发上,因为它们的通信更加复杂。将它们设计成通用和灵活的是有意义的,这样它们就可以被重复使用。
-
如果通信结构复制了人类组织,那么明确是有意义的。团队间的 API 应该比团队内的 API 更加可见、公开和有文档记录。
-
在设计系统时,将它们划分到分层团队结构的界线上是最不费力的路径。以其他方式对其进行工程设计将需要组织变革。
-
另一方面,改变组织结构是一个困难和痛苦的过程。任何经历过组织重组的人都知道这一点。这种变化将反映在软件中,因此要做好计划。
-
让两个团队共同处理同一个软件单元会产生问题,因为每个团队都会试图将其引向自己的目标。
软件单元的所有者应该是一个团队。这向每个人展示了谁对任何变化负责并有最终决定权,并帮助我们专注于我们的愿景并减少技术债务。
- 不同的物理位置会施加通信限制,比如时差,这将在我们跨地域开发软件时产生障碍。通常会根据地理位置划分团队,这就需要构建这些团队之间的通信(因此 API)结构。
请注意,DevOps 运动与康威定律有关。传统的工作分工方式是将正在开发的软件与其运行方式分开。这在康威定律所描述的两个团队之间产生了鸿沟,从而产生了与两个团队之间缺乏理解相关的问题。
对这个问题的反应是创建能够开发和运行自己软件的团队,并部署它。这就是所谓的 DevOps。它将运营问题转移到开发团队,旨在创建一个反馈循环来激励、理解和解决问题。
康威定律并不是一件需要克服的坏事。它反映了任何组织结构对软件结构的影响。
记住这一点可能有助于我们设计系统,使得组织和现有软件的通信流程合理。
DevOps 运动的一个关键组成部分是推进构建系统的技术,以简化生产环境的操作,使部署过程更简单。这使我们能够以新的方式组织团队,从而使多个团队能够控制发布。
现在,让我们谈谈软件如何被结构化为不同的部门。
将软件划分为不同类型的软件单元
虽然本书的主要目标是讨论微服务中软件的划分,但这并不是唯一可能的划分。其他划分可以包括微服务内的模块或共享包。
微服务的主要特点是在开发和部署方面是独立的,因此可以实现完全的并行化。其他划分可能会减少这一点并引入依赖关系。
确保你能够证明这些改变。
在我们在本书中介绍的示例系统中,我们引入了一个模块,用于验证请求是否由用户签名。用户后端生成一个签名头,思想后端和前端通过token_validation.py
模块独立验证它。
这个模块应该由拥有用户后端的同一个团队拥有,因为它是它的自然延伸。我们需要验证它是否生成与用户后端生成的相同的令牌。
避免重复并始终保持同步的最佳方法是生成一个 Python 软件包,可以安装在依赖的微服务上。然后,这些软件包可以像requirements.txt
文件中的任何其他外部依赖一样对待。
要在 Python 中打包一个库,我们可以使用几种工具,包括官方的Python Packaging User Guide(packaging.python.org/
)中的工具,以及较新的工具,如 Poetry(poetry.eustace.io
),这些工具对于新项目来说更容易使用。
如果我们希望将软件包公开,可以将其上传到 PyPI。或者,如果需要,我们可以将其上传到私有存储库,使用诸如 Gemfury 之类的工具,或者自己托管存储库。这将明确划分软件包及其维护者,以及使用它作为依赖项的团队之间的关系。
将软件单元划分成团队划分方面有着重要的影响。现在,让我们来看看如何组织团队。
设计工作结构
考虑到康威定律,划分软件应该反映组织的结构。当我们从单体架构迁移到微服务架构时,这一点非常重要。
记住,从单体架构迁移到微服务在我们操作方式上是一个重大的改变。这既是组织上的改变,也是技术上的改变。主要的风险在于人的因素,包括培训人员使用新技术以及让开发人员满意他们将要工作的新领域。
对组织结构进行根本性的改变可能非常困难,但将需要进行一些小的调整。当从单体架构迁移到微服务时,团队将需要重新调整。
请记住,大规模的重组可能会激怒人们并引发政治问题。人类不喜欢改变,任何决定都需要有意义。需要准备好解释和澄清这一举措。明确新结构的目标将有助于赋予其目的。
让我们来看一些团队划分的例子,以及它们的优缺点。
围绕技术构建团队
在某些情况下,与技术相关的不同技能可能是相关的。系统的某些部分可能涉及与其他任何东西完全不同的技术。
一个很好的例子是移动应用程序,因为它们在使用的语言方面是受限制的(Android 使用 Java,iOS 使用 Objective-C 或 Swift)。一个具有网站和移动应用程序的应用可能需要一个专门的团队来处理移动应用程序的代码。
一个更传统的例子是围绕数据库管理员(DBA)构建的数据库团队。他们将控制对数据库的访问并操作它们以保持良好状态。然而,这种结构正在消失,因为数据库操作现在更容易,通常由大多数开发人员处理,并且近年来数据库的基础设施管理已大大简化。
这可能使我们能够为特定领域创建特定团队。技术的障碍确保系统之间的通信是有结构的。
下图是我们将遇到的团队类型的示例。它们按技术和沟通方式分组。数据库团队将与创建 Web 服务后端的团队进行通信,他们将与 Web 和移动团队进行通信:
这种模式的主要缺点是新功能可能需要多个团队共同努力。对客户端代码的任何更改,以便我们可以在数据库中存储新值,都需要每个团队的工作投入。这些功能需要额外的协调,这可能会限制开发速度。
围绕领域构建团队
另一种结构是围绕不同的知识领域,通常与公司的业务领域相关。每个知识领域都有自己独立的系统,但它们彼此通信。一些部分可能具有外部可访问的接口,而其他部分可能没有。
这种结构通常在已经成功运作多年的成熟组织中找到。
例如,一个在线零售商可能分为三个领域:
-
销售:负责外部网站和营销。
-
库存:购买商品以便销售,并处理库存。
-
运输:将产品送到客户手中。跟踪信息显示在网站上。
在这种情况下,每个领域都有自己的数据库,以便存储相关数据和服务。它们通过定义的 API 相互通信,最频繁的变化发生在特定领域内。这允许在领域内快速发布和开发。
跨领域拥有新功能也是可能的。例如,对运输跟踪信息的更改可能需要我们匹配销售产生的更改。然而,这些变化应该发生得更少。
在这个例子中,每个团队将像下图所示相互通信:
这种结构的主要不便之处在于可能会创建孤立的团队和独立思维。每个系统都有自己的做事方式,因此它们可能会分歧到不共享相同的基本语言的程度。当需要跨领域功能时,可能会导致讨论和摩擦。
围绕客户构建团队
在一些组织中,主要目标是为客户创建定制工作。也许客户需要以定制的 B2B 方式与产品集成。在这种情况下,能够开发和运行定制代码至关重要。
结构侧重于客户。三个团队(称为红色、金色和蓝色)分配给客户,并为每个客户维护特殊服务,包括他们的自定义代码。每个客户团队处理几个客户。另一个团队处理产品的后端,其中包含系统的通用代码和基础设施。这个团队与客户分开工作,但在共享时从客户团队添加功能,以便将其包含在产品中。一般的改进也是共享的。
这在组织中形成了两种速度。客户团队专注于客户的短期需求,而产品团队专注于客户的长期需求。
在这里,产品团队将与客户团队交谈,但客户团队之间的交流不会太多。这在下图中有所体现:
这种结构对于高度定制的服务非常有效,以便它们可以包含为单个客户生成的代码,这可能会使它们失去对一般产品的关注。这里的主要问题是客户团队可能面临的高压力,因为他们面对苛刻的客户,这可能对开发人员造成负担。产品团队需要确保他们为产品做出有用的添加,并尽量减少他们的长期问题。
围绕混合结构团队
前面的三个例子是合成用例。现实生活更加复杂,可能需要混合使用所有这些例子,或者完全新的结构。
如果组织足够大,可能会有数十个不同的团队和软件单元。请记住,如果团队足够大,一个团队可以处理多个软件单元。但是,为了避免所有权和缺乏重点,两个团队不应拥有相同的软件单元。
分析组织中的沟通流程,以便了解在转向微服务时需要解决的痛点,并确保人员结构考虑了微服务和软件单元的设计。
团队的另一个重要元素是找到在添加新功能和维护现有代码之间花费的时间之间的适当平衡。
平衡新功能和维护
每个软件服务都需要维护,以保持良好状态,但不会增加明显的外部价值。维护任务对于良好的运行至关重要,可以分为两类:定期维护和管理技术债务。
技术债务是将使用大部分时间并需要进一步讨论的部分,但在此之前,让我们先看看定期维护。
定期维护
这种维护以与软件服务的性质固有的任务形式出现。通过运行依赖于其他组件的服务,例如底层操作系统或 Python 解释器,我们需要使它们保持最新并升级到新版本。
在使用容器和 Kubernetes 的情况下,有两个充当操作系统的系统需要考虑。一个是容器中的操作系统;在这里,我们使用了 Alpine。另一个是处理 Kubernetes 节点的操作系统,在这里 AWS EKS 会自动处理,但需要升级到 Kubernetes 版本。
保持依赖项最新的主要原因如下,按重要性排序:
-
新版本修复安全问题。
-
一般性能改进。
-
可以添加新功能以实现新功能。
如果我们计划进行这些任务,这些任务可以得到缓解。例如,使用标记为长期支持(LTS)的操作系统版本可以减少在更新系统时出现的问题。
操作系统的 LTS 版本是在长周期内接收支持和关键更新的版本。例如,常规的 Ubuntu 版本每 6 个月发布一次,并在 9 个月内接收更新(包括关键安全更新)。LTS 版本每 2 年发布一次,并获得 5 年的支持。
运行服务时,建议使用 LTS 版本,以最小化所需的维护工作。
所有这些软件包和依赖关系都需要更新,以确保操作系统正常工作。另一种选择是存在安全漏洞或者使用过时的系统。
更新依赖关系可能需要我们调整代码,这取决于它的部分是否已被弃用或移除。在某些情况下,这可能是昂贵的。在撰写本文时,Python 社区最著名的迁移是从 Python 2 升级到 Python 3,这是一个需要多年时间的任务。
大多数升级通常相当常规,需要很少的工作。尝试生成一个跟得上的升级计划,并制定明确的指导方针;例如,规则如当新的操作系统 LTS 版本发布时和所有系统应在接下来的 3 个月内迁移。这样可以产生可预测性,并给每个人一个明确的目标,可以跟进和执行。
持续集成工具可以帮助这个过程。例如,GitHub 自动检测文件中的依赖关系,如requirements.txt
,并在检测到漏洞时通知我们。甚至可以在更新模块时自动生成拉取请求。查看更多信息的文档:help.github.com/en/github/managing-security-vulnerabilities/configuring-automated-security-fixes
。
升级依赖关系可能是最常见的定期维护任务,但还有其他可能性:
-
清理或归档旧数据。这些操作通常可以自动化,节省大量时间并减少问题。
-
修复依赖于业务流程的操作,比如生成月度报告等等。这些应该在可能的情况下自动化,或者设计工具,使用户可以自动产生它们,而不是依赖技术人员进行定制操作。
-
修复由错误或其他错误产生的永久问题。错误有时会使系统处于糟糕的状态;例如,数据库中可能有损坏的条目。在修复错误时,我们可能需要通过解除进程或用户来解决这种情况。
这些过程可能很烦人,特别是如果它们是重复的,但通常是被理解的。
另一种维护形式,涉及技术债务,更加复杂,因为它更逐渐地引入,并且更难以清晰地检测到。妥善解决技术债务是最具挑战性的维护任务,但在我们做任何事情之前,我们需要理解它。
理解技术债务
技术债务是软件开发中使用的一个概念,用来描述当实施非最佳解决方案时将在未来增加的额外成本。换句话说,选择快速或简单的选择意味着以后的功能需要更长时间和更难开发。
作为一个隐喻,技术债务自上世纪 90 年代初就存在,但在此之前就已经描述了这个概念。
像任何隐喻一样,它是有用的,但也有局限性。特别是,非技术人员倾向于将其与财务债务联系起来,尽管它们有不同的含义。例如,大多数技术债务是在我们甚至没有注意到的情况下创建的。确保不要过分使用这个隐喻。
技术债务在一定程度上是不可避免的。在实施功能之前,没有无限的时间来研究所有可能性,也没有完美的信息。这也是任何复杂系统中熵增长的结果。
除了是不可避免的,技术债务也可以是一个有意识的选择。由于时间的限制,开发受到限制,因此对市场的不完美快速解决方案可能比错过截止日期更可取。
技术债务的另一个迹象是专注于某些知识。无论如何,技术债务会随着时间的推移不断积累,并给新功能带来摩擦。复杂性的增加也可能会导致可靠性问题,因为错误会变得越来越难以理解和修复。
简单是可靠系统的最好朋友。简单的代码易于理解和纠正,使错误要么显而易见,要么很容易检测到。微服务架构旨在通过创建独立的、更小的服务,并为它们分配明确的责任,并在它们之间创建明确的接口,来减少单体架构的固有复杂性。
技术债务可能会增长到需要一个大型架构的程度。我们已经看到,从单体架构转移到微服务架构可能是其中一个时刻。
这样的架构迁移是一项艰巨的工作,需要时间来完成。在单体架构中已经存在的功能,新微服务可能会与引入的新功能发生冲突。
这会产生一个移动目标效应,可能会带来很大的破坏性。确保你识别出这些冲突点,并尽量在迁移计划中将其最小化。例如,一些新功能可能可以推迟到新的微服务准备就绪的时候。
然而,我们不应该等到技术债务变得如此庞大,以至于只有激进的改变才足以解决它,我们需要能够更早地解决技术债务。
持续解决技术债务
减少技术债务需要是一个持续的过程,并且需要引入到日常运营中。专注于持续改进的敏捷技术试图引入这种思维方式。
检测技术债务通常来自开发团队内部,因为他们更接近代码。团队应该考虑哪些地方的操作可以更顺畅,并留出时间进行改进。
允许我们检测技术债务的一个很好的信息来源是指标,比如我们在第十章中设置的监控日志和指标。
忽视解决这些问题的风险是陷入软件腐烂,已经存在的功能变得越来越慢和不太可靠。随着时间的推移,它们将对客户和外部合作伙伴变得越来越明显。在那之前,工作在这种环境中将使开发人员的生活变得困难,并存在燃尽的风险。新开发的延迟也会很常见,因为代码本身就很难处理。
为了避免陷入这种情况,需要不断地分配时间来持续减少技术债务,交替进行新功能和其他工作。应该在维护和技术债务减少以及新功能之间找到平衡。
我们在本书中讨论的许多技术手段都有助于我们以持续的方式改进系统,从我们在第四章中描述的持续集成技术,到我们在第八章中描述的代码审查和批准,再到我们在第十章中设置的监控日志和指标。
分布可能高度依赖于系统的当前形状,但明确并强制执行这一点确实有帮助。例如,花费在技术债务减少上的特定时间百分比可能会有所帮助。
减少技术债务是昂贵且困难的,因此尽量减少引入技术债务是有意义的。
避免技术债务
处理技术债务的最佳方法是首先不引入技术债务。然而,这说起来容易做起来难。有多个因素可能影响导致技术债务的决策的质量。
最常见的原因如下:
-
缺乏战略性的高层计划来指导:这会产生不一致的结果,因为每次发现相同的问题时,都会以不同的方式解决。我们谈到了跨团队协调需要解决组织内的标准,并确保它们得到遵守。有人担任软件架构师,寻求在整个组织中创建一致的指导方针,应该会极大地改善这种情况。
-
没有足够的知识来选择正确的选项:这是相当常见的。有时,需要做决定的人由于沟通不畅或简单缺乏经验而没有所有相关信息。这个问题是缺乏处理当前问题经验的结构的典型问题。确保团队经过高度培训,并且正在创造一个更有经验的成员帮助和指导初级成员的文化,将减少这些情况。跟踪以前的决定并简化如何使用其他微服务的文档将帮助我们协调团队,使他们拥有所有相关的拼图部分。这有助于他们避免由于错误的假设而犯错。另一个重要因素是确保团队对他们使用的工具进行适当的培训,以便他们充分了解自己的能力。这对于外部工具来说应该是这样,比如熟练掌握 Python 或 SQL,以及任何需要培训材料、文档和指定联系点的内部工具。
-
没有花足够的时间调查不同的选项或进行规划:这个问题是由压力和迫切需要取得快速进展所造成的。这可能已经根深蒂固在组织文化中,当组织增长时,减缓决策过程可能是一项具有挑战性的任务,因为较小的组织往往需要更快的流程。记录决策过程并要求同行审查或批准可以帮助减缓这一过程,并确保工作是彻底的。在决定哪些决策需要更多审查和哪些不需要方面找到平衡是很重要的。例如,所有适合在一个微服务内的东西可以在团队内部进行审查,但需要多个微服务和团队的功能应该在外部进行审查和批准。在这种情况下,找到收集信息和做决定之间的适当平衡是重要的。记录决策和输入,以便了解得出这些决策的过程并完善你的流程。
避免这些问题的最佳方法是反思以前的错误并从中吸取教训。
设计更广泛的发布流程
虽然能够独立部署每个微服务确实是系统的关键要素,但这并不意味着不需要协调。
首先,仍然有一些功能需要在多个微服务中部署。我们已经看过如何可以在开发过程中处理这些细节,包括处理版本和明确检查依赖关系。那么现在呢?
在这些情况下,需要团队之间的协调,以确保依赖关系得到实施,并且不同的部署按适当的顺序执行。
虽然一些协调可以由主要架构师来协助,但架构角色应该专注于长期目标,而不是短期发布。允许团队自行协调的好工具是在会议上通知其他团队有关发布的情况。
在每周发布会议中的规划
当发布流程是新的,并且从单体系统迁移仍在进行时,向每个团队提供他们正在做什么的见解是一个好主意。每周的发布会应该由每个团队的代表参加,这样可以很好地传播关于其他团队正在进行的工作的知识。
发布会的目标应该是:
-
下一个 7 天的计划发布和大致时间;例如,我们计划在周三发布新版本的用户后端。
-
对于任何重要的新功能,尤其是其他团队可以使用的功能,你应该提前通知。例如,如果新版本改进了身份验证,请确保将你的团队重定向到新的 API,以便他们也可以获得这些改进。
-
说明任何阻碍因素。例如,我们无法发布这个版本,直到 Thoughts 后端发布带有功能 A 的版本。
-
如果有关键维护或可能影响发布的任何更改,请提出警告。例如,周四早上,我们需要进行数据库维护,所以请不要在 12 点之前发布任何东西。工作完成后,我们会发送电子邮件通知。
-
回顾上周发生的发布问题。我们稍后会更详细地讨论这个问题。
这类似于许多敏捷实践中常见的站立会议,比如 SCRUM,但专注于发布。为了能够做到这一点,我们需要提前指定发布时间。
考虑到微服务发布的异步性质,以及持续集成实践的实施和加速这一过程,将会有很多例行发布不会提前计划那么长时间。这是可以接受的,也意味着发布流程正在得到完善。
在涉及风险较高的发布时,尽量提前计划,并利用发布会有效地与其他团队沟通。这个会议是保持对话开放的工具。
随着持续集成实践的不断确立和发布速度的不断加快,每周的发布会将逐渐变得越来越不重要,甚至可能不再需要定期举行。这是对持续改进实践的反思的一部分,也是通过识别发布问题来实现的。
反思发布问题
并不是每次发布都会顺利进行。有些可能会因为工具或基础设施的问题而失败,或者可能是因为流程中存在易犯的错误。事实上,有些发布会出现问题。不幸的是,无法避免这些情况。
随着时间的推移,减少和最小化发布问题,每次发现问题时,都需要将其记录并在每周的发布会或等价的论坛上提出。
一些问题可能很小,只需要额外的一点工作,就可以成功发布;例如,一个错误的配置会导致新版本无法启动,直到修复,或者一个协调问题,导致一个服务在其依赖之前部署。
其他问题可能更大,甚至可能导致故障。在这种情况下,回滚将非常有用,这样我们就可以快速返回到已知状态并重新规划。
无论如何,它们都应该被适当地记录,即使只是简要地记录,然后共享,以便流程得以完善。分析出了什么问题是关键,以便不断改进发布,使其更快速、更简单。
对这些问题要坦诚。如果希望检测到每一个问题并快速评估解决方案,那么创建一个公开讨论和承认问题的文化是很重要的。
捕捉问题并不是,也永远不应该是,归咎于谁的责任。检测和纠正问题是组织的责任。
如果发生这种情况,环境不仅会变得不那么吸引人,而且团队会隐藏问题,以免受到指责。
未解决的问题往往会成倍增加,因此可靠性将大大降低。
能够顺利发布对于快速部署和提高速度至关重要。当处理这类问题时,通常只需要轻量级文档,因为它们通常是轻微的,最坏的情况下可能会延迟一两天的发布。
对于更大的问题,当外部服务中断时,最好有一个更正式的流程来确保问题得到适当解决。
我们可以改进的另一种方式是正确理解中断现场系统服务的问题的原因。这方面最有效的工具是事后总结会议。
进行事后总结会议
不仅限于发布,有时会发生中断服务并需要大量工作才能修复的重大事件。在这些紧急情况下,第一个目标是尽快恢复服务。
在服务恢复稳定后,为了从这次经历中吸取教训并避免再次发生,应该由参与事件的所有人参加事后总结会议。事后总结会议的目标是从紧急情况中学到的教训中创建一系列后续任务。
为了记录这一点,您需要创建一个模板。这将在事后总结会议期间填写。模板应该包括以下信息:
-
检测到了什么问题? 如果这不明显,包括如何检测到的;例如,网站宕机并返回 500 错误。这表明错误增加了。
-
它是什么时候开始和结束的? 事件的时间轴;例如,周四下午 3 点到 5 点。
-
谁参与了解决这次事件? 无论是检测问题还是解决问题。这有助于我们收集关于发生了什么的信息。
-
为什么会失败? 找到根本原因和导致这一结果的一系列事件;例如,网站宕机是因为应用程序无法连接到数据库。数据库无响应是因为硬盘已满。硬盘已满是因为本地日志填满了磁盘。
-
它是如何修复的? 采取了解决事件的步骤;例如,删除了一周前的日志。
-
从这次事件中应该采取哪些行动? 应该采取纠正或修复不同问题的行动。理想情况下,它们应该包括谁将执行这些行动;例如,不应该存储本地日志,而应该将其发送到集中日志。应该监视硬盘空间的使用情况,并在空间少于 80%时发出警报。
其中一些元素可以在紧急情况后立即填写,例如谁参与了。然而,最好是在事件发生后一到三天安排事后总结会议,以便每个人都有时间消化和处理这些数据。根本原因可能与我们最初的想法不同,花一些时间思考发生了什么有助于我们提出更好的解决方案。
正如我们在反思发布问题部分讨论的那样,在处理服务中断事件时,一定要鼓励开放和坦率的讨论。
事后总结会议并不是为了责怪任何人,而是为了改进服务并在团队合作时减少风险。
应该在会议中决定后续行动,并相应地进行优先排序。
尽管检测根本原因非常重要,但请注意应该采取针对其他原因的行动。即使根本原因只有一个,也有其他预防性行动可以最小化其再次发生时的影响。
事后总结会议产生的行动通常具有很高的优先级,并应尽快完成。
总结
在本章中,我们看了团队之间协调的不同方面,以便成功管理运行微服务架构的组织。
我们首先讨论了保持全局视野和各部分之间协调的好处。我们谈到了明确指定的领先架构师监督系统,并具有高层视图,使他们能够确保团队之间不会发生冲突。
我们描述了康威定律以及沟通结构最终塑造了软件结构,因此对软件所做的任何更改都应在组织中得到反映,反之亦然。然后,我们学习了如何划分责任领域,并提供了一些可能的划分示例,基于不同的组织。
接下来,我们介绍了技术债务如何减缓持续开发过程,以及引入持续解决技术债务的思维方式对于避免降低内部团队和客户体验的重要性。
最后,我们解决了发布可能引起的一些问题,无论是在团队之间进行充分协调方面,特别是在使用 GitOps 的早期阶段,还是在发布失败或服务中断时进行回顾分析。
问题
-
为什么领先的架构师对微服务架构系统很方便?
-
康威定律是什么?
-
为什么会引入技术债务?
-
为什么重要创造一种文化,可以持续努力减少技术债务?
-
为什么重要记录发布中的问题并与每个团队分享?
-
事后总结会议的主要目标是什么?
进一步阅读
要了解更多有关架构师角色的信息,请阅读《软件架构师手册》(www.packtpub.com/application-development/software-architects-handbook
),其中包括专门讨论软技能和架构演变的章节。您可以在《新工程游戏》(www.packtpub.com/data/the-new-engineering-game
)中了解更多有关康威定律和构建数字化业务的信息。
第十三章:评估
第一章
- 什么是单体?
单体应用是指以单个块创建的软件应用程序。该应用程序作为单个进程运行。它只能一起部署,尽管可以创建多个相同的副本。
- 单体可能会遇到什么问题?
随着发展,单体可能会遇到以下问题:
-
代码变得太大且难以阅读。
-
可扩展性问题。
-
需要协调部署。
-
资源的不良使用。
-
不可能在不同情况下使用冲突的技术(例如,相同库的不同版本,或两种编程语言)。
-
一个错误和部署可能会影响整个系统。
- 你能描述微服务架构吗?
微服务架构是一组松散耦合的专业化服务的集合,它们协同工作以提供全面的服务。
- 微服务最重要的特性是什么?
微服务最重要的特性是它们可以独立部署,以便可以独立开发。
- 从单体架构迁移到微服务时,我们需要克服的主要挑战是什么?
可能的挑战包括以下内容:
-
- 需要进行大的变更,需要我们改变服务的运行方式,包括团队的文化。这可能导致成本高昂的培训。
-
调试分布式系统更加复杂。
-
我们需要计划变更,以便不中断服务。
-
每个开发的微服务都会产生大量开销。
-
我们需要在允许每个团队决定如何工作和标准化以避免重复造轮之间找到平衡。
-
我们需要记录服务,以便与另一个团队进行交互。
- 我们如何进行这样的迁移?
我们需要分析系统,测量,相应地计划并执行计划。
- 描述我们如何使用负载均衡器从旧服务器迁移到新服务器,而不中断系统。
首先,我们必须配置负载均衡器,使其指向旧的 Web 服务器,这将使流量通过 Web 服务器。然后,我们必须更改 DNS 记录,使其指向负载均衡器。流量经过负载均衡器后,我们需要为新服务创建一个新条目,以便负载均衡器在两者之间分配流量。确认一切按预期工作后,我们需要从旧服务中删除条目。现在,所有流量将路由到新服务。
第二章
- RESTful 应用程序的特征是什么?
虽然 RESTful 应用程序被理解为将 URI 转换为对象表示并通过 HTTP 方法操纵它们的 Web 界面(通常使用 JSON 格式化请求),但 REST 架构的典型特征如下:
-
统一接口
-
客户端-服务器
-
无状态
-
可缓存
-
分层系统
-
按需代码(可选)
您可以在restfulapi.net/
了解有关 REST 架构的更多信息。
- 使用 Flask-RESTPlus 的优势是什么?
使用 Flask-RESTPlus 的一些优势包括:
-
自动生成 Swagger。
-
可以定义和解析输入并整理输出的框架。
-
它允许我们在命名空间中组织代码。
- Flask-RESTPlus 的一些替代方案是什么?
其他选择包括 Flask-RESTful(这类似于 Flask-RESTPlus,但它不支持 Swagger)和 Django REST 框架,它拥有丰富的生态系统,充满了第三方扩展。
- 命名用于测试以修复时间的 Python 软件包。
freezegun
。
- 描述认证流程。
认证系统(用户后端)生成编码的令牌。此令牌使用只有用户后端拥有的私钥进行编码。此令牌以 JWT 编码,并包含用户 ID 以及其他参数,例如告诉我们令牌有效的时间。此令牌包含在Authentication
标头中。
令牌从标头中获取,并使用相应的公钥进行解码,该公钥存储在 Thoughts 后端中。这使我们能够独立获取用户 ID,并确信它已被用户后端验证。
- 为什么我们选择 SQLAlchemy 作为数据库接口?
SQLAlchemy 在 Flask 中得到很好的支持,并允许我们定义已经存在的数据库。它高度可配置,并允许我们在低级别(即接近底层 SQL)和高级别上工作,从而消除了任何样板代码的需求。在我们的用例中,我们从遗留系统继承了一个数据库,因此需要与现有模式无缝工作。
第三章
- Dockerfile 中的 FROM 关键字是做什么的?
它从现有的镜像开始,向其添加更多的层。
- 如何使用预定义命令启动容器?
您将运行以下命令:
docker run image
- 为什么在 Dockerfile 中创建一个删除文件的步骤不会创建一个更小的镜像?
由于 Docker 使用的文件系统具有分层结构,Docker 文件中的每个步骤都会创建一个新的层。文件系统是所有操作协同工作的结果。最终镜像包括所有现有的层;添加一个层永远不会减小镜像的大小。删除的新步骤将不会出现在最终镜像中,但它将始终作为前一个层的一部分可用。
- 多阶段 Dockerfile 是如何工作的?
多阶段 Dockerfile 包含多个阶段,每个阶段都将以FROM
命令开始,该命令指定作为起点的镜像。数据可以在一个阶段生成,然后复制到另一个阶段。
多阶段构建在我们希望减小最终镜像大小时非常有用;只有生成的数据将被复制到最终阶段。
- 运行和执行命令之间有什么区别?
run
命令从镜像启动一个新的容器,而exec
命令连接到已经存在的运行中的容器。请注意,如果在执行时容器停止,会话将被关闭。
在exec
会话中停止容器可能会发生。保持容器运行的主要进程是run
命令。如果您终止命令或以其他方式停止它,容器将停止,会话将被关闭。
- 何时应该使用-it 标志?
当您需要保持终端打开时,例如交互式运行bash
命令。请记住这个助记符交互式终端。
- 除了使用 uWSGI 来提供 Web Python 应用程序之外,还有哪些替代方案?
任何支持 WSGI 网络协议的 Web 服务器都可以作为替代方案。最受欢迎的替代方案是 Gunicorn,旨在易于使用和配置,mod_wsgi
是流行的 Apache Web 服务器的扩展,支持 WSGI Python 模块,以及 CherryPy,它包括自己的 Web 框架。
- docker-compose 用于什么?
docker-compose
允许轻松编排,也就是说,我们可以协调多个相互连接的 Docker 容器,使它们协同工作。它还帮助我们配置 Docker 命令,因为我们可以使用docker-compose.yaml
文件来存储所有受影响容器的配置参数。
- 你能描述一下 Docker 标签是什么吗?
Docker 标签是一种在保持其根名称的同时标记图像的方法。它通常标记相同应用程序或软件的不同版本。默认情况下,latest
标签将应用于图像构建。
- 为什么我们需要将镜像推送到远程注册表?
我们将镜像推送到远程注册表,以便与其他系统和开发人员共享镜像。除非需要将镜像推送到另一个存储库,否则 Docker 会在本地构建镜像,以便其他 Docker 服务可以使用它们。
第四章
- 增加部署数量是否会降低部署的质量?
不会;已经证明增加部署数量与其质量增加有很强的相关性。一个能够快速部署的系统必须依赖于强大的自动化测试,这会增加系统的稳定性和整体质量。
- 什么是管道?
管道是用于执行构建的有序步骤或阶段的连续顺序。如果其中一个步骤失败,构建将停止。步骤的顺序应该旨在最大程度地早期检测问题。
- 我们如何知道我们的主分支是否可以部署?
如果我们自动运行我们的管道以在每次提交时生成构建,我们应该在提交时尽快检测主分支上的问题。构建应该让我们确信主分支的顶部提交可以部署。主分支的中断应该尽快修复。
- Travis CI 的主要配置来源是什么?
.travis.yml
文件,可以在存储库的根目录中找到。
- Travis CI 默认在何时发送通知邮件?
Travis CI 在构建中断时发送通知邮件,以及先前中断的分支成功通过时发送通知邮件。成功的构建发生在先前的提交成功但未报告的情况下。
- 我们如何避免将中断的分支合并到主分支?
我们可以通过在 GitHub 中进行配置来避免这种情况,这可以确保分支在合并到受保护的分支之前通过构建。为了确保功能分支没有偏离主分支,我们需要强制它与构建合并。为了实现这一点,它需要与主分支保持最新。
- 为什么我们应该避免将机密存储在 Git 存储库中?
由于 Git 的工作方式,任何引入的机密都可以通过查看提交历史来检索,即使它已被删除。由于提交历史可以在任何克隆的存储库中复制,这使得我们无法验证它是否正确 - 我们无法将提交历史重写到克隆的存储库中。除非正确加密,机密不应存储在 Git 存储库中。任何错误存储的机密都应该被删除。
第五章
- 什么是容器编排器?
容器编排器是一个系统,我们可以在其中部署多个容器,这些容器可以协同工作,并以有序的方式管理供应和部署。
- 在 Kubernetes 中,什么是节点?
节点是集群中的物理服务器或虚拟机。节点可以被添加或从集群中移除,Kubernetes 会相应地迁移或重新启动正在运行的容器。
- Pod 和容器之间有什么区别?
一个 Pod 可以包含多个共享相同 IP 的容器。要在 Kubernetes 中部署容器,我们需要将其与一个 Pod 关联起来。
- 作业和 Pod 之间有什么区别?
一个 Pod 预期会持续运行。一个作业或定时作业执行单个操作,然后所有 Pod 容器完成它们的执行。
- 我们何时应该添加 Ingress?
当我们需要能够从集群外部访问服务时,我们应该添加 Ingress。
- 命名空间是什么?
命名空间是一个虚拟集群。集群中的所有定义都需要具有唯一的名称。
- 我们如何在文件中定义 Kubernetes 元素?
我们需要以 YAML 格式指定它,并提供关于其 API 版本、元素类型、具有名称和命名空间的元数据部分,以及spec
部分中的元素定义。
- kubectl get 和 describe 命令有什么区别?
kubectl get
获取多个元素,如服务或 pod,并显示它们的基本信息。另一方面,describe
访问单个元素并呈现更多关于它的信息。
- CrashLoopBackOff 错误表示什么?
这个错误表明一个容器已经执行了定义的启动命令。这个错误只与 pod 有关,因为它们永远不应该停止执行。
第六章
- 我们正在部署的三个微服务是什么?
以下是我们正在部署的三个微服务:
-
用户后端用于控制身份验证和用户处理方式。
-
Thoughts Backend 用于存储思想并允许我们创建和搜索它们。
-
前端为我们提供了一个用户界面,以便我们可以与系统进行交互。它通过 RESTful 调用调用其他两个微服务。
- 这三个微服务中哪一个需要其他两个微服务可用?
前端调用其他两个微服务,因此它们需要对前端可用。
- 为什么我们需要在运行 docker-compose 时使用外部 IP 连接到微服务?
docker-compose
为每个微服务创建一个内部网络,因此它们需要使用外部 IP 进行通信,以便正确路由。虽然我们在主机计算机上暴露端口,但可以使用外部主机 IP。
- 每个应用程序所需的主要 Kubernetes 对象是什么?
对于每个微服务,我们提供一个部署(自动生成一个 pod)、一个服务和一个 Ingress。
- 有没有任何不需要的对象?
用户后端和 Thoughts 后端的 Ingress 并不是绝对必需的,因为它们可以通过节点端口访问,但这样做可以更轻松地访问它们。
- 如果我们扩展到多个 pod 或任何其他微服务,我们能检测到问题吗?
用户后端和 Thoughts 后端创建了一个包含两个容器的 pod,其中包括数据库。如果我们创建多个 pod,将创建多个数据库,并在它们之间交替可能会导致问题。
例如,如果我们在一个 pod 中创建一个新的想法,如果请求是在另一个 pod 中进行的,我们将无法搜索到它。
另一方面,前端可以轻松扩展。
- 我们为什么要使用
/etc/hosts
文件?
我们正在使用这个文件来定义一个host
,将其路由到我们的本地 Kubernetes 集群。这样我们就不必定义 FQDN 并配置 DNS 服务器。
第七章
- 为什么我们不应该管理自己的 Kubernetes 集群?
由于 Kubernetes 是一个抽象层,让云提供商负责维护、管理和安全最佳实践更加方便。将集群委托给现有的商业云提供商也非常便宜。
- 你能说出一些具有托管 Kubernetes 解决方案的商业云提供商吗?
亚马逊网络服务、谷歌云服务、微软 Azure、Digital Ocean 和 IBM Cloud 都是商业云提供商,提供了托管的 Kubernetes 解决方案。
- 你需要执行什么操作才能推送到 AWS Docker 注册表?
您需要登录到 Docker 守护程序。您可以使用以下代码获取登录命令:
$ aws ecr get-login --no-include-email
- 我们用什么工具来设置 EKS 集群?
eksctl
允许我们从命令行创建整个集群,并根据需要进行扩展或缩减。
- 我们在本章中做了哪些主要更改,以便我们可以使用之前章节中的 YAML 文件?
我们必须更改图像定义才能使用 AWS 注册表。我们包括了活跃性和就绪性探针,以及部署策略。
这些只添加到frontend
部署中。将其余部署添加留给你作为练习。
- 在这个集群中有没有不需要的 Kubernetes 元素?
Ingress 元素并不是严格要求的,因为 Thoughts Backend 和 Users Backend 无法从外部访问。前端服务能够创建一个面向外部的 ELB。
不要觉得我们的配置限制了你。你可以手动配置 ELB,这样你就可以以不同的方式访问集群,如果你愿意,你可以使用 Ingress 配置。
- 我们为什么需要控制与 SSL 证书关联的 DNS?
我们需要证明我们拥有 DNS,以便 SSL 证书可以验证只有 DNS 地址的合法所有者才能访问该 DNS 的证书。这是 HTTPS 的根本要素,并且表明您正在与特定 DNS 的所有者进行私人通信。
- 存活探针和就绪探针之间有什么区别?
如果就绪探针失败,Pod 将不接受请求,直到再次通过。如果存活探针失败,容器将被重新启动。
- 滚动更新在生产环境中为什么重要?
它们很重要,因为它们避免了服务中断。它们一次添加一个工作进程,同时删除旧的工作进程,确保任何时候可用的工作进程数量保持不变。
- 自动缩放 Pod 和节点之间有什么区别?
由于节点反映在物理实例中,对它们进行扩展会影响系统中的资源。与此同时,扩展 Pod 使用它们可用的资源,但不会修改它们。
换句话说,增加节点数量会增加需要在系统上运行的硬件。这是有成本的,因为我们需要从云提供商那里租用更多的硬件。增加 Pod 数量在硬件方面没有成本,这就是为什么应该有一些额外的开销来允许增加。
两种策略应该协调,以便我们可以迅速对负载增加做出反应,并同时减少正在使用的硬件数量,以便降低成本。
- 在本章中,我们部署了自己的数据库容器。在生产中,这是不需要的。但是,如果您连接到已经存在的数据库,您将如何做到这一点?
第一步是更改thoughts_backend/deployment.yaml
和users_backend/deployment.yaml
文件中的环境变量。主要连接的是POSTGRES_HOST
,但用户和密码可能也需要更改。
我们可以创建一个名为postgres-db
的内部 Kubernetes 服务,它指向外部地址,而不是直接连接到POSTGRES_HOST
作为 IP 或 DNS 地址。这可以帮助我们抽象外部数据库的地址。
这将一次性部署,以确保我们可以连接到外部数据库。
然后,我们可以删除部署中描述的数据库容器,即thoughts-backend-db
和users-backend-db
。这些容器的映像仅用于测试和开发。
第八章
- 使用脚本将新代码推送到服务器和使用 Puppet 等配置管理工具之间有什么区别?
当使用脚本将新代码推送到服务器时,每台服务器都需要单独推送代码。Puppet和其他配置管理工具有一个集中的服务器,接收新数据并适当分发。它们还监视服务器是否按预期运行,并可以执行补救任务。
配置管理工具用于大型集群,因为它们减少了需要在自定义脚本中处理的工作量。
- DevOps 的核心理念是什么?
DevOps 的核心理念是赋予团队控制权,使他们能够自行部署和管理基础设施。这需要一套自动化程序作为安全网络,以确保这些操作易于进行、安全且快速。
- 使用 GitOps 的优势是什么?
使用 GitOps 的主要优势如下:
-
Git 是大多数团队已经知道如何使用的常见工具。
-
它保留了基础设施定义的副本,这意味着我们可以将其用作备份,并从灾难性故障中恢复,或者轻松地基于先前的定义创建新的集群。
-
基础设施更改是有版本的,这意味着我们可以逐个进行小的离散更改,并在出现问题时撤消其中任何一个。
- Kubernetes 集群只能使用 GitOps 吗?
尽管 GitOps 与 Kubernetes 具有协同作用,因为 Kubernetes 可以通过 YAML 文件进行控制,但没有什么能阻止我们使用 Git 存储库来控制集群。
- Flux 部署位于哪里?
它位于自己的 Kubernetes 集群中,以便可以从 Git 中提取数据。
- 您需要在 GitHub 中配置什么,以便 Flux 可以访问它?
您需要将 SSH 密钥添加到 GitHub 存储库的部署密钥中。您可以通过调用fluxctl identity
来获取 SSH 密钥。
- 在生产环境中工作时,GitHub 提供的哪些功能确保我们对部署有控制?
在我们可以合并到主分支之前,需要进行审查和批准,这会触发部署。强制从特定用户那里获得批准的代码所有者的包含可以帮助我们控制敏感区域。
第九章
- 在微服务架构下运行的系统中,当收到新的业务功能时,需要进行哪些分析?
我们需要确定新业务功能影响哪个微服务或多个微服务。影响多个微服务的功能使其实施更加困难。
- 如果一个功能需要更改两个或更多微服务,我们如何决定首先更改哪个?
这应该以向后兼容的方式进行,以保持向后兼容性。在考虑向后兼容性的情况下添加新功能,因此可能性有限。一旦后端准备就绪,前端可以相应地进行更改,以便我们可以利用新功能。
- Kubernetes 如何帮助我们设置多个环境?
在 Kubernetes 中创建新的命名空间非常容易。由于系统的定义封装在 YAML 文件中,它们可以被复制和修改以创建一个重复的环境。这可以用作基线,然后进行演变。
- 代码审查是如何工作的?
在一个分支中的代码与主分支进行比较。另一个开发人员可以查看它们之间的差异并进行评论,要求澄清或更改。然后可以讨论这些评论,如果审阅者认为代码足够好,那么代码就可以被批准。合并可以被阻止,直到它获得一个或多个批准。
- 代码审查的主要瓶颈是什么?
主要瓶颈是没有审阅者提供反馈并批准代码。这就是为什么有足够的人可以担任审阅者角色非常重要。
- 根据 GitOps 原则,部署的审查与代码审查不同吗?
不;在 GitOps 下,部署被视为代码,因此它们可以像任何其他代码审查一样进行审查。
- 一旦功能准备合并到主分支中,为什么有一个清晰的部署路径是重要的?
重要的是要有一个清晰的部署路径,以便每个人都在同一页面上。它还提供了部署速度的明确期望。通过这样做,我们可以指定何时需要审查。
- 为什么数据库迁移与常规代码部署不同?
它们不同,因为无法轻松回滚。虽然代码部署可以回滚以便重新部署以前的镜像,但数据库迁移会对数据库或数据的模式进行更改,如果它们被回滚可能会导致数据丢失。通常,数据库迁移只能向前进行,任何出现的问题都需要通过新的部署进行更正。
这就是为什么我们必须特别小心数据库迁移,并确保它们不向后兼容的主要原因。
第十章
- 系统的可观察性是什么?
这是系统的容量。它让您知道其内部状态是什么。
- 默认情况下日志中有哪些不同的严重级别?
按严重性递增的不同严重级别是DEBUG
、INFO
、WARNING
、ERROR
和CRITICAL
。
- 指标用于什么?
指标允许您了解系统上发生的事件的聚合状态,并了解系统的一般状态。
- 为什么需要在日志中添加请求 ID?
您需要向日志中添加请求 ID,以便您可以将与同一请求对应的所有日志分组在一起。
- Prometheus 中有哪些类型的指标?
计数器,用于计算特定事件;仪表,用于跟踪可以上升或下降的值;以及直方图(或摘要),用于跟踪与值相关联的事件,例如事件发生的时间或请求返回的状态代码。
- 度量中的 75th 百分位数是什么,它与平均值有何不同?
对于直方图,75^(th)百分位数是平均值高于25%的事件的位置,而低于它的事件占75%。平均值是通过将所有值相加并将该值除以最初相加在一起的值的数量来找到的。通常,平均值将接近 50th 百分位数,尽管这取决于值的分布方式。
*90(th)*-*95(th)*百分位数对于确定延迟很有用,因为它提供了请求的上限时间,不包括异常值。平均值可能会被异常值所偏离,因此不能为绝大多数请求提供真实的数字。
- 四个黄金信号是什么?
四个黄金信号是收集系统健康描述的四个测量值。它们是请求的延迟、流量量、返回错误的百分比和资源的饱和度。
第十一章
- 在微服务架构系统和单体架构中发布更改有哪些不同?
在单体架构中发布更改只涉及一个存储库,因为单体只是一个代码库。在微服务架构中进行的一些更改将需要我们更改两个或更多微服务,以便我们可以分配它们。这需要更多的规划和关注,因为我们需要确保这是正确协调的。在正确架构的微服务系统中,这种多存储库更改应该相对罕见,因为它们会产生额外的开销。
- 为什么在微服务架构中发布更改应该小?
微服务的优势在于我们可以并行发布微服务,这比单体发布更快。然而,鉴于微服务中的发布可能会影响其他微服务,它们应该以迭代方式工作,减少更改的规模并增加部署速度。
小的更改风险较小,如果需要,可以更容易地回滚。
- 语义版本如何工作?
在语义版本中,版本有三个数字:Major版本号,Minor版本号和Patch版本号。它们之间都用点分隔:
-
补丁版本的增加只修复错误和安全问题。
-
次要版本的增加会增加更多功能,但不会引入向后不兼容的更改。
-
主要版本的增加会产生不向后兼容的更改。
- 微服务架构系统中内部接口的语义版本控制存在哪些问题?
由于微服务中的部署非常常见,向后兼容性非常重要,因此主要发布的含义变得淡化。此外,大多数微服务的消费者是内部的,因此版本之间的隐式通信变得不那么重要。
当发布变得常见时,语义版本控制失去了意义,因为目标是不断完善和改进产品,而不是标记大的发布。
- 添加版本端点的优势是什么?
任何使用微服务的消费者都可以以与进行任何其他请求相同的方式请求其版本:通过使用 RESTful 调用。
- 我们如何解决本章代码中的依赖问题?
本章的代码存在依赖问题。
- 我们应该在共享 ConfigMap 中存储哪些配置变量?
我们应该存储被多个微服务访问的配置变量。我们应该预先存储大部分配置变量,以便它们可以被重复使用。
- 描述将所有配置变量放入单个共享 ConfigMap 的优缺点。
一个共享的 ConfigMap 使配置变量非常明确。它鼓励每个人重复使用它们,并告诉其他人配置用于其他微服务的用途。
更改微服务的依赖将触发重新启动,因此更改作为一切依赖的 ConfigMap 将导致集群中的所有微服务重新启动,这是耗时的。
此外,单个 ConfigMap 文件可能会变得相当大,将其拆分为几个较小的文件可以帮助我们更有效地组织数据。
- Kubernetes ConfigMap 和 Kubernetes Secret 有什么区别?
Kubernetes Secrets 更好地防止意外访问。直接访问工具不会以明文显示 Secret。对 Secret 的访问也需要以更明确的方式进行配置。另一方面,ConfigMaps 可以进行批量配置,因此 pod 将能够访问 ConfigMap 中存储的所有值。
- 我们如何更改 Kubernetes Secret?
我们可以使用kubectl edit
更改 Secret,但它需要以 Base64 格式进行编码。
例如,要用以下代码替换postgres-password
秘密为someotherpassword
值:
$ echo someotherpassword | base64
c29tZW90aGVycGFzc3dvcmQK
$ kubectl edit secrets -n example thoughts-secrets
# Please edit the object below. Lines beginning with a '#' will be ignored,
# and an empty file will abort the edit. If an error occurs while saving this file will be
# reopened with the relevant failures.
#
apiVersion: v1
data:
postgres-password: c29tZW90aGVycGFzc3dvcmQK
...
secret/thoughts-secrets edited
一旦重新启动,我们的 pod 将能够使用新的 Secret。
- 假设基于我们的配置,我们决定将 public_key.pub 从 Secret 更改为 ConfigMap。我们需要做哪些更改?
我们需要更改 ConfigMap,以便它包含configuration.yaml
中的文件:
THOUGHTS_BACKEND_URL: http://thoughts-service
public_key.pub: |
-----BEGIN PUBLIC KEY-----
<public key>
-----END PUBLIC KEY-----
USER_BACKEND_URL: http://users-service
注意缩进以界定文件。 |
字符标记多行字符串。
然后,在deployment.yaml
文件中,我们需要将挂载的来源从 Secret 更改为 ConfigMap:
volumes:
- name: public-key
configMap:
name: shared-config
items:
- key: public_key.pub
path: public_key.pub
记得先将这些更改应用到 ConfigMap 中,以便在应用部署文件时它们是可用的。
请注意,此方法创建了一个名为public_key.pub
的环境变量,以及文件的内容,因为它作为shared-config
ConfigMap 的一部分应用。另一种方法是创建一个独立的 ConfigMap。
在所有 pod 重新启动后,可以删除 Secret。
第十二章
- 为什么对于微服务架构系统来说,有一个领先的架构师很方便?
在微服务架构中构建系统允许我们创建可以并行处理的独立服务。这些服务仍然需要相互通信和合作。
独立团队通常无法把握全局,倾向于专注于自己的项目。为了协调和整体系统的发展,独立团队需要一个高层次概览系统的首席架构师。
- 康威定律是什么?
康威定律是一句谚语,它说软件结构复制了编写它的组织的沟通结构。
这意味着,要改变软件的结构,组织需要改变,这是一项更加困难的任务。
为了成功设计和发展大型系统,组织需要考虑并相应地进行规划。
- 技术债务是如何引入的?
技术债务可以以许多方式产生。
通常,技术债务可以分为以下四类或它们的混合:
-
通过过快地发展而没有花时间分析其他选项
-
通过妥协缩短开发时间,同时知道这种妥协以后需要修复
-
通过对当前系统或工具的了解不够充分,或者缺乏培训或专业知识
-
通过对外部问题做出错误假设,从而为不一定需要修复的东西设计
- 为什么重要建立一种文化,以便我们可以持续减少技术债务?
重要的是建立一种文化,以避免软件腐烂,即由于向现有软件添加复杂性而导致性能和可靠性持续下降。除非解决技术债务成为一个持续的过程,否则日常发布的压力意味着我们将无法进行维护。
- 为什么重要记录发布中的问题并与团队其他成员分享?
这很重要,因为每个团队都可以从其他人的经验和解决方案中学习,并改进他们的流程。这也可以创造一种开放的文化,人们不会害怕对自己的错误负责。
- 事后总结会议的主要目标是什么?
事后总结会议的主要目标是创建解决事故原因的后续任务。为此,我们需要尽可能确信已成功检测到根本原因(这也是次要目标)。