今天在更新一个以 StatefulSet 形式部署的服务时出现了一个诡异的问题:一个从 YAML 文件中被删掉的 env
环境变量,在应用 YAML 后的 StatefulSet 对象中仍然存在,导致 Pod 中的应用程序没有正常运行。
而出现问题的 StatefulSet 资源,唯一的不寻常之处是 —— 上次更新(apply -f
)后进行了回滚(rollout undo
)……
复盘
首先,我们有一个 StatefulSet,它运行得很正常:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: my-wonderhoy-app
labels:
app: my-wonderhoy-app
spec:
serviceName: my-wonderhoy-app
replicas: 3
selector:
matchLabels:
app: my-wonderhoy-app
template:
metadata:
labels:
app: my-wonderhoy-app
spec:
containers:
- name: nginx
image: nginx:latest
env:
- name: TO_BE_DELETED
value: needs to be deleted!
- name: ANOTHER_ENV
value: who cares?
我们想要部署一个新版,进行了一些代码改动,发了一个新的 Docker 映像,并且不再需要其中的某一个 env
:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: my-wonderhoy-app
labels:
app: my-wonderhoy-app
spec:
serviceName: my-wonderhoy-app
replicas: 3
selector:
matchLabels:
app: my-wonderhoy-app
template:
metadata:
labels:
app: my-wonderhoy-app
spec:
containers:
- name: nginx
# 总之这个 image 不工作
image: nginx:a-broken-version
env:
# 这个被删掉了!
# - name: TO_BE_DELETED
# value: needs to be deleted!
- name: ANOTHER_ENV
value: who cares?
然后,我们发现这个新版的 Docker 映像不工作,于是回滚到上一个版本:
# 列出历史版本,找到上一个工作的版本
kubectl rollout history statefulset.apps/my-wonderhoy-app
REVISION CHANGE-CAUSE
1 kubectl apply -f my-wonderhoy-app-v1.yaml
2 kubectl apply -f my-wonderhoy-app-v2.yaml
# 回滚到所需的版本
kubectl rollout undo statefulset.apps/my-wonderhoy-app --to-revision=1
statefulset.apps/my-wonderhoy-app rolled back
回滚后,我们的服务回到了正常工作的状态,它使用上一个版本的 Docker 映像,并且存在着被上一个版本所需要的 TO_BE_DELETED
变量。
而当我们修复了 Docker 映像的问题,再次部署新版时,我们发现 TO_BE_DELETED
这个变量并没有被删除:
kubectl apply -f my-wonderhoy-app-v3.yaml
statefulset.apps/my-wonderhoy-app configured
kubectl get statefulset.apps/my-wonderhoy-app -o jsonpath='{.spec.template.spec.containers[0].env}'
[map[name:TO_BE_DELETED value:needs to be deleted!] map[name:ANOTHER_ENV value:who cares?]]
原因
这个问题是由于 kubectl apply
的合并策略导致的。kubectl apply
并不是简单地用新的 YAML 文件来替换掉集群中现有资源的配置,而是会遵循一个策略来进行合并。对于 env
的成员,Kubernetes 认为它是一种 Key-Value 结构,会对每个 Key 计算合并后的 Value。为了进行 Kubernetes 所认为的「正确」的合并,它对每个资源维护了一个 last-applied-configuration
字段,记录了上次使用 kubectl apply
时为它应用的 YAML 配置,而在本次 kubectl apply
中,它的合并策略是,对于一个 Key:
- 如果该 Key 存在于新的 YAML 配置中,那么直接使用新的 Value;
- 如果该 Key 不存在于新的 YAML 配置中:
- 如果该 Key 存在于上次应用的 YAML 配置中,Kubernetes 会认为这个 Key 应当被删除;
- 如果该 Key 不存在于上次应用的 YAML 配置中,Kubernetes 会认为这个 Key 是被通过 YAML 文件之外的方式被添加的,不应当被删除。
这个策略在大多数情况下是没有问题的,我们(在非调试用途下)一般不会通过 YAML 文件之外的方式来修改资源,所以 last-applied-configuration
一般会运行中的配置一致,使得 Kubernetes 能够正确地判断出哪些 Key 需要被删除。但这次巧合的是:
- 首先,我们应用了一次(有 Bug 的)新 YAML 文件,这时
last-applied-configuration
中已经没有要删除的TO_BE_DELETED
这个 Key 了; - 然后,我们发现 Bug 之后回滚到了上一个版本,但
last-applied-configuration
没有被回滚,它之中仍然没有TO_BE_DELETED
这个 Key;- 此时,工作中的该资源
statefulset.apps/my-wonderhoy-app
的配置就与last-applied-configuration
不一致了;
- 此时,工作中的该资源
- 最后,重新应用(修复了 Bug 的)新 YAML 文件后,Kubernetes 的合并策略会认为
TO_BE_DELETED
是被通过 YAML 文件之外的方式添加的,不应当被删除。
解决
搞明白整个问题的流程之后,它就不可怕了。要解决它,只需要手动删掉这个没有被自动删掉的 env
即可:
kubectl edit statefulset.apps/my-wonderhoy-app # 编辑 YAML 文件,删除掉不需要的 env
statefulset.apps/my-wonderhoy-app edited
这个问题在一直使用 kubectl apply
的环境中不会出现,所以我们会误认为 kubectl apply
是一个简单的替换操作,但在手动修改了资源之后,就必须要考虑这个合并策略的问题了。