From 593dc8cca1c8a3dcf2a10fc4aeef904362314471 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=93=E9=9B=B7=E4=B8=8D=E6=80=95?= Date: Sun, 3 Sep 2017 07:04:43 +0800 Subject: [PATCH] k8s-tutorials-stateful-application (#5112) * k8s-tutorials-stateful-application * change author and email to pass CLA --- .../tutorials/stateful-application/Dockerfile | 17 + .../tutorials/stateful-application/FETCH_HEAD | 0 .../basic-stateful-set.md | 1107 ++++++++++++++++ .../cassandra-service.yaml | 12 + .../cassandra-statefulset.yaml | 94 ++ .../stateful-application/cassandra.md | 851 ++++++++++++ cn/docs/tutorials/stateful-application/dev | 0 .../mysql-wordpress-persistent-volume.md | 328 +++++ .../local-volumes.yaml | 27 + .../mysql-deployment.yaml | 61 + .../wordpress-deployment.yaml | 63 + .../tutorials/stateful-application/web.yaml | 45 + .../tutorials/stateful-application/webp.yaml | 45 + .../stateful-application/zookeeper.md | 1174 +++++++++++++++++ .../stateful-application/zookeeper.yaml | 157 +++ 15 files changed, 3981 insertions(+) create mode 100644 cn/docs/tutorials/stateful-application/Dockerfile create mode 100644 cn/docs/tutorials/stateful-application/FETCH_HEAD create mode 100644 cn/docs/tutorials/stateful-application/basic-stateful-set.md create mode 100644 cn/docs/tutorials/stateful-application/cassandra-service.yaml create mode 100644 cn/docs/tutorials/stateful-application/cassandra-statefulset.yaml create mode 100644 cn/docs/tutorials/stateful-application/cassandra.md create mode 100644 cn/docs/tutorials/stateful-application/dev create mode 100644 cn/docs/tutorials/stateful-application/mysql-wordpress-persistent-volume.md create mode 100644 cn/docs/tutorials/stateful-application/mysql-wordpress-persistent-volume/local-volumes.yaml create mode 100644 cn/docs/tutorials/stateful-application/mysql-wordpress-persistent-volume/mysql-deployment.yaml create mode 100644 cn/docs/tutorials/stateful-application/mysql-wordpress-persistent-volume/wordpress-deployment.yaml create mode 100644 cn/docs/tutorials/stateful-application/web.yaml create mode 100644 cn/docs/tutorials/stateful-application/webp.yaml create mode 100644 cn/docs/tutorials/stateful-application/zookeeper.md create mode 100644 cn/docs/tutorials/stateful-application/zookeeper.yaml diff --git a/cn/docs/tutorials/stateful-application/Dockerfile b/cn/docs/tutorials/stateful-application/Dockerfile new file mode 100644 index 0000000000000..8016958d830c1 --- /dev/null +++ b/cn/docs/tutorials/stateful-application/Dockerfile @@ -0,0 +1,17 @@ +# This is an image with Percona XtraBackup, mysql-client and ncat installed. +FROM debian:jessie + +RUN \ + echo "deb http://repo.percona.com/apt jessie main" > /etc/apt/sources.list.d/percona.list \ + && echo "deb-src http://repo.percona.com/apt jessie main" >> /etc/apt/sources.list.d/percona.list \ + && apt-key adv --keyserver keys.gnupg.net --recv-keys 8507EFA5 + +RUN \ + apt-get update && apt-get install -y --no-install-recommends \ + percona-xtrabackup-24 \ + mysql-client \ + nmap \ + && rm -rf /var/lib/apt/lists/* + +CMD ["bash"] + diff --git a/cn/docs/tutorials/stateful-application/FETCH_HEAD b/cn/docs/tutorials/stateful-application/FETCH_HEAD new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/cn/docs/tutorials/stateful-application/basic-stateful-set.md b/cn/docs/tutorials/stateful-application/basic-stateful-set.md new file mode 100644 index 0000000000000..3c45196561118 --- /dev/null +++ b/cn/docs/tutorials/stateful-application/basic-stateful-set.md @@ -0,0 +1,1107 @@ +--- +assignees: +- enisoc +- erictune +- foxish +- janetkuo +- kow3ns +- smarterclayton +title: StatefulSet基本使用 +--- + +{% capture overview %} + +本教程介绍了如何使用 [StatefulSets](/docs/concepts/abstractions/controllers/statefulsets/) 来管理应用。演示了如何创建、删除、扩容/缩容和更新 StatefulSets 的 Pods。 +{% endcapture %} + +{% capture prerequisites %} + +在开始本教程之前,你应该熟悉以下 Kubernetes 的概念: + +* [Pods](/docs/user-guide/pods/single-container/) +* [Cluster DNS](/docs/concepts/services-networking/dns-pod-service/) +* [Headless Services](/docs/concepts/services-networking/service/#headless-services) +* [PersistentVolumes](/docs/concepts/storage/volumes/) +* [PersistentVolume Provisioning](http://releases.k8s.io/{{page.githubbranch}}/examples/persistent-volume-provisioning/) +* [StatefulSets](/docs/concepts/abstractions/controllers/statefulsets/) +* [kubectl CLI](/docs/user-guide/kubectl) + +本教程假设你的集群被配置为动态的提供 PersistentVolumes 。如果没有这样配置,在开始本教程之前,你需要手动准备5个1 GiB的存储卷。 +{% endcapture %} + +{% capture objectives %} + +StatefulSets 旨在与有状态的应用及分布式系统一起使用。然而在 Kubernetes 上管理有状态应用和分布式系统是一个宽泛而复杂的话题。为了演示 StatefulSet 的基本特性,并且不使前后的主题混淆,你将会使用 StatefulSet 部署一个简单的 web 应用。 + +在阅读本教程后,你将熟悉以下内容: + +* 如何创建 StatefulSet +* StatefulSet 怎样管理它的 Pods +* 如何删除 StatefulSet +* 如何对 StatefulSet 进行扩容/缩容 +* 如何更新一个 StatefulSet 的 Pods + {% endcapture %} + + +{% capture lessoncontent %} + +##创建 StatefulSet + + +作为开始,使用如下示例创建一个 StatefulSet。它和 [StatefulSets](/docs/concepts/abstractions/controllers/statefulsets/) 概念中的示例相似。它创建了一个 [Headless Service](/docs/user-guide/services/#headless-services) `nginx` 用来发布StatefulSet `web` 中的 Pod 的 IP 地址。 + +{% include code.html language="yaml" file="web.yaml" ghlink="/docs/tutorials/stateful-application/web.yaml" %} + + +下载上面的例子并保存为文件 `web.yaml`。 + + +你需要使用两个终端窗口。在第一个终端中,使用 [`kubectl get`](/docs/user-guide/kubectl/{{page.version}}/#get) 来查看 StatefulSet 的 Pods 的创建情况。 + +```shell +kubectl get pods -w -l app=nginx +``` + + +在另一个终端中,使用 [`kubectl create`](/docs/user-guide/kubectl/{{page.version}}/#create) 来创建定义在 `web.yaml` 中的 Headless Service 和 StatefulSet。 + +```shell +kubectl create -f web.yaml +service "nginx" created +statefulset "web" created +``` + + +上面的命令创建了两个 Pod,每个都运行了一个 [NGINX](https://www.nginx.com) web 服务器。获取 `nginx` Service 和 `web` StatefulSet 来验证是否成功的创建了它们。 + +```shell +kubectl get service nginx +NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE +nginx None 80/TCP 12s + +kubectl get statefulset web +NAME DESIRED CURRENT AGE +web 2 1 20s +``` + + +### 顺序创建 Pod + + +对于一个拥有 N 个副本的 StatefulSet,Pod 被部署时是按照 {0..N-1}的序号顺序创建的。在第一个终端中使用 `kubectl get` 检查输出。这个输出最终将看起来像下面的样子。 + +```shell +kubectl get pods -w -l app=nginx +NAME READY STATUS RESTARTS AGE +web-0 0/1 Pending 0 0s +web-0 0/1 Pending 0 0s +web-0 0/1 ContainerCreating 0 0s +web-0 1/1 Running 0 19s +web-1 0/1 Pending 0 0s +web-1 0/1 Pending 0 0s +web-1 0/1 ContainerCreating 0 0s +web-1 1/1 Running 0 18s +``` + + +请注意在 `web-0` Pod 处于 [Running和Ready](/docs/user-guide/pod-states) 状态后 `web-1` Pod 才会被启动。 + + +## StatefulSet 中的 Pod + + +StatefulSet 中的 Pod 拥有一个唯一的顺序索引和稳定的网络身份标识。 + + +### 检查 Pod 的顺序索引 + + +获取 StatefulSet 的 Pod。 + +```shell +kubectl get pods -l app=nginx +NAME READY STATUS RESTARTS AGE +web-0 1/1 Running 0 1m +web-1 1/1 Running 0 1m + +``` + + +如同 [StatefulSets](/docs/concepts/abstractions/controllers/statefulsets/) 概念中所提到的, StatefulSet 中的 Pod 拥有一个具有黏性的、独一无二的身份标志。这个标志基于 StatefulSet 控制器分配给每个 Pod 的唯一顺序索引。 Pod 的名称的形式为`-`。`web` StatefulSet 拥有两个副本,所以它创建了两个 Pod:`web-0`和`web-1`。 + + +### 使用稳定的网络身份标识 + + +每个 Pod 都拥有一个基于其顺序索引的稳定的主机名。使用[`kubectl exec`](/docs/user-guide/kubectl/{{page.version}}/#exec) 在每个 Pod 中执行`hostname` 。 + +```shell +for i in 0 1; do kubectl exec web-$i -- sh -c 'hostname'; done +web-0 +web-1 +``` + + +使用 [`kubectl run`](/docs/user-guide/kubectl/{{page.version}}/#run) 运行一个提供 `nslookup` 命令的容器,该命令来自于 `dnsutils` 包。通过对 Pod 的主机名执行 `nslookup`,你可以检查他们在集群内部的 DNS 地址。 + +```shell +kubectl run -i --tty --image busybox dns-test --restart=Never --rm /bin/sh +nslookup web-0.nginx +Server: 10.0.0.10 +Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local + +Name: web-0.nginx +Address 1: 10.244.1.6 + +nslookup web-1.nginx +Server: 10.0.0.10 +Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local + +Name: web-1.nginx +Address 1: 10.244.2.6 +``` + + +headless service 的 CNAME 指向 SRV 记录(记录每个 Running 和 Ready 状态的 Pod)。SRV 记录指向一个包含 Pod IP 地址的记录表项。 + + +在一个终端中查看 StatefulSet 的 Pod。 + +```shell +kubectl get pod -w -l app=nginx +``` + +在另一个终端中使用 [`kubectl delete`](/docs/user-guide/kubectl/{{page.version}}/#delete) 删除 StatefulSet 中所有的 Pod。 + +```shell +kubectl delete pod -l app=nginx +pod "web-0" deleted +pod "web-1" deleted +``` + + +等待 StatefulSet 重启它们,并且两个 Pod 都变成 Running 和 Ready 状态。 + +```shell +kubectl get pod -w -l app=nginx +NAME READY STATUS RESTARTS AGE +web-0 0/1 ContainerCreating 0 0s +NAME READY STATUS RESTARTS AGE +web-0 1/1 Running 0 2s +web-1 0/1 Pending 0 0s +web-1 0/1 Pending 0 0s +web-1 0/1 ContainerCreating 0 0s +web-1 1/1 Running 0 34s +``` + + +使用 `kubectl exec` 和 `kubectl run` 查看 Pod 的主机名和集群内部的 DNS 表项。 + +```shell +for i in 0 1; do kubectl exec web-$i -- sh -c 'hostname'; done +web-0 +web-1 + +kubectl run -i --tty --image busybox dns-test --restart=Never --rm /bin/sh +nslookup web-0.nginx +Server: 10.0.0.10 +Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local + +Name: web-0.nginx +Address 1: 10.244.1.7 + +nslookup web-1.nginx +Server: 10.0.0.10 +Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local + +Name: web-1.nginx +Address 1: 10.244.2.8 +``` + + +Pod 的序号、主机名、SRV 条目和记录名称没有改变,但和 Pod 相关联的 IP 地址可能发生了改变。在本教程中使用的集群中它们就改变了。这就是为什么不要在其他应用中使用 StatefulSet 中的 Pod 的 IP 地址进行连接,这点很重要。 + + +如果你需要查找并连接一个 StatefulSet 的活动成员,你应该查询 Headless Service 的 CNAME。和 CNAME 相关联的 SRV 记录只会包含 StatefulSet 中处于 Running 和 Ready 状态的 Pod。 + + +如果你的应用已经实现了用于测试 liveness 和 readiness 的连接逻辑,你可以使用 Pod 的 SRV 记录(`web-0.nginx.default.svc.cluster.local`, +`web-1.nginx.default.svc.cluster.local`)。因为他们是稳定的,并且当你的 Pod 的状态变为 Running 和 Ready 时,你的应用就能够发现它们的地址。 + + +### 写入稳定的存储 + + +获取 `web-0` 和 `web-1` 的 PersistentVolumeClaims。 + +```shell +kubectl get pvc -l app=nginx +NAME STATUS VOLUME CAPACITY ACCESSMODES AGE +www-web-0 Bound pvc-15c268c7-b507-11e6-932f-42010a800002 1Gi RWO 48s +www-web-1 Bound pvc-15c79307-b507-11e6-932f-42010a800002 1Gi RWO 48s +``` + +StatefulSet 控制器创建了两个 PersistentVolumeClaims,绑定到两个 [PersistentVolumes](/docs/concepts/storage/volumes/)。由于本教程使用的集群配置为动态提供 PersistentVolume,所有的 PersistentVolume 都是自动创建和绑定的。 + + +NGINX web 服务器默认会加载位于 `/usr/share/nginx/html/index.html` 的 index 文件。StatefulSets `spec` 中的 `volumeMounts` 字段保证了 `/usr/share/nginx/html` 文件夹由一个 PersistentVolume 支持。 + + +将Pod的主机名写入它们的 `index.html` 文件并验证 NGINX web 服务器使用该主机名提供服务。 + +```shell +for i in 0 1; do kubectl exec web-$i -- sh -c 'echo $(hostname) > /usr/share/nginx/html/index.html'; done + +for i in 0 1; do kubectl exec -it web-$i -- curl localhost; done +web-0 +web-1 +``` + + +请注意,如果你看见上面的 curl 命令返回了 403 Forbidden 的响应,你需要像这样修复使用 `volumeMounts` +(due to a [bug when using hostPath volumes](https://github.com/kubernetes/kubernetes/issues/2630))挂载的目录的权限: + +```shell +for i in 0 1; do kubectl exec web-$i -- chmod 755 /usr/share/nginx/html; done +``` + +在你重新尝试上面的 curl 命令之前。 + +在一个终端查看 StatefulSet 的 Pod。 + +```shell +kubectl get pod -w -l app=nginx +``` + + +在另一个终端删除 StatefulSet 所有的 Pod。 + +```shell +kubectl delete pod -l app=nginx +pod "web-0" deleted +pod "web-1" deleted +``` + +在第一个终端里检查 `kubectl get` 命令的输出,等待所有 Pod 变成 Running 和 Ready 状态。 + +```shell +kubectl get pod -w -l app=nginx +NAME READY STATUS RESTARTS AGE +web-0 0/1 ContainerCreating 0 0s +NAME READY STATUS RESTARTS AGE +web-0 1/1 Running 0 2s +web-1 0/1 Pending 0 0s +web-1 0/1 Pending 0 0s +web-1 0/1 ContainerCreating 0 0s +web-1 1/1 Running 0 34s +``` + + +验证所有 web 服务器在继续使用它们的主机名提供服务。 + +``` +for i in 0 1; do kubectl exec -it web-$i -- curl localhost; done +web-0 +web-1 +``` + + +虽然 `web-0` 和 `web-1` 被重新调度了,但它们仍然继续监听各自的主机名,因为和它们的 PersistentVolumeClaim 相关联的 PersistentVolume 被重新挂载到了各自的 `volumeMount` 上。不管 `web-0` 和 `web-1` 被调度到了哪个节点上,它们的 PersistentVolumes 将会被挂载到合适的挂载点上。 + + +## 扩容/缩容 StatefulSet + +扩容/缩容 StatefulSet 指增加或减少它的副本数。这通过更新 `replicas` 字段完成。你可以使用[`kubectl scale`](/docs/user-guide/kubectl/{{page.version}}/#scale) 或者[`kubectl patch`](/docs/user-guide/kubectl/{{page.version}}/#patch)来扩容/缩容一个 StatefulSet。 + + +### 扩容 + + +在一个终端窗口观察 StatefulSet 的 Pod。 + +```shell +kubectl get pods -w -l app=nginx +``` + + +在另一个终端窗口使用 `kubectl scale` 扩展副本数为5。 + +```shell +kubectl scale sts web --replicas=5 +statefulset "web" scaled +``` + + +在第一个 终端中检查 `kubectl get` 命令的输出,等待增加的3个 Pod 的状态变为 Running 和 Ready。 + +```shell +kubectl get pods -w -l app=nginx +NAME READY STATUS RESTARTS AGE +web-0 1/1 Running 0 2h +web-1 1/1 Running 0 2h +NAME READY STATUS RESTARTS AGE +web-2 0/1 Pending 0 0s +web-2 0/1 Pending 0 0s +web-2 0/1 ContainerCreating 0 0s +web-2 1/1 Running 0 19s +web-3 0/1 Pending 0 0s +web-3 0/1 Pending 0 0s +web-3 0/1 ContainerCreating 0 0s +web-3 1/1 Running 0 18s +web-4 0/1 Pending 0 0s +web-4 0/1 Pending 0 0s +web-4 0/1 ContainerCreating 0 0s +web-4 1/1 Running 0 19s +``` + + +StatefulSet 控制器扩展了副本的数量。如同[创建 StatefulSet](#顺序创建pod)所述,StatefulSet 按序号索引顺序的创建每个 Pod,并且会等待前一个 Pod 变为 Running 和 Ready 才会启动下一个Pod。 + + +### 缩容 + + +在一个终端观察 StatefulSet 的 Pod。 + +```shell +kubectl get pods -w -l app=nginx +``` + + +在另一个终端使用 `kubectl patch` 将 StatefulSet 缩容回三个副本。 + +```shell +kubectl patch sts web -p '{"spec":{"replicas":3}}' +"web" patched +``` + + +等待 `web-4` 和 `web-3` 状态变为 Terminating。 + +``` +kubectl get pods -w -l app=nginx +NAME READY STATUS RESTARTS AGE +web-0 1/1 Running 0 3h +web-1 1/1 Running 0 3h +web-2 1/1 Running 0 55s +web-3 1/1 Running 0 36s +web-4 0/1 ContainerCreating 0 18s +NAME READY STATUS RESTARTS AGE +web-4 1/1 Running 0 19s +web-4 1/1 Terminating 0 24s +web-4 1/1 Terminating 0 24s +web-3 1/1 Terminating 0 42s +web-3 1/1 Terminating 0 42s +``` + + +### 顺序终止 Pod + + +控制器会按照与 Pod 序号索引相反的顺序每次删除一个 Pod。在删除下一个 Pod 前会等待上一个被完全关闭。 + + +获取 StatefulSet 的 PersistentVolumeClaims。 + +```shell +kubectl get pvc -l app=nginx +NAME STATUS VOLUME CAPACITY ACCESSMODES AGE +www-web-0 Bound pvc-15c268c7-b507-11e6-932f-42010a800002 1Gi RWO 13h +www-web-1 Bound pvc-15c79307-b507-11e6-932f-42010a800002 1Gi RWO 13h +www-web-2 Bound pvc-e1125b27-b508-11e6-932f-42010a800002 1Gi RWO 13h +www-web-3 Bound pvc-e1176df6-b508-11e6-932f-42010a800002 1Gi RWO 13h +www-web-4 Bound pvc-e11bb5f8-b508-11e6-932f-42010a800002 1Gi RWO 13h + +``` + + +五个 PersistentVolumeClaims 和五个 PersistentVolumes 仍然存在。查看 Pod 的 [稳定存储](#stable-storage),我们发现当删除 StatefulSet 的 Pod 时,挂载到 StatefulSet 的 Pod 的 PersistentVolumes不会被删除。当这种删除行为是由 StatefulSe t缩容引起时也是一样的。 + + +## 更新 StatefulSet + + +Kubernetes 1.7 版本的 StatefulSet 控制器支持自动更新。更新策略由 StatefulSet API Object 的 `spec.updateStrategy` 字段决定。这个特性能够用来更新一个 StatefulSet 中的 Pod 的 container images, resource requests,以及 limits, labels 和 annotations。 + + +### On Delete 策略 + +`OnDelete` 更新策略实现了传统(1.7之前)行为,它也是默认的更新策略。当你选择这个更新策略并修改 StatefulSet 的 `.spec.template` 字段时, StatefulSet 控制器将不会自动的更新Pod。 + + +Patch `web` StatefulSet 的容器镜像。 + +```shell +kubectl patch statefulset web --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value":"gcr.io/google_containers/nginx-slim:0.7"}]' +"web" patched +``` + + +删除 `web-0` Pod。 + +```shell +kubectl delete pod web-0 +pod "web-0" deleted +``` + +<-- +Watch the `web-0` Pod, and wait for it to transition to Running and Ready. +--> +观察 `web-0` Pod, 等待它变成 Running 和 Ready。 + +```shell +kubectl get pod web-0 -w +NAME READY STATUS RESTARTS AGE +web-0 1/1 Running 0 54s +web-0 1/1 Terminating 0 1m +web-0 0/1 Terminating 0 1m +web-0 0/1 Terminating 0 1m +web-0 0/1 Terminating 0 1m +web-0 0/1 Pending 0 0s +web-0 0/1 Pending 0 0s +web-0 0/1 ContainerCreating 0 0s +web-0 1/1 Running 0 3s +``` + + +获取 `web` StatefulSet 的 Pod 来查看他们的容器镜像。 + +```shell{% raw %} +kubectl get pod -l app=nginx -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.containers[0].image}{"\n"}{end}' +web-0 gcr.io/google_containers/nginx-slim:0.7 +web-1 gcr.io/google_containers/nginx-slim:0.8 +web-2 gcr.io/google_containers/nginx-slim:0.8 +{% endraw %}``` + +`web-0` has had its image updated, but `web-0` and `web-1` still have the original +image. Complete the update by deleting the remaining Pods. + +​```shell +kubectl delete pod web-1 web-2 +pod "web-1" deleted +pod "web-2" deleted +``` + + +观察 StatefulSet 的 Pod,等待它们全部变成 Running 和 Ready。 + +``` +kubectl get pods -w -l app=nginx +NAME READY STATUS RESTARTS AGE +web-0 1/1 Running 0 8m +web-1 1/1 Running 0 4h +web-2 1/1 Running 0 23m +NAME READY STATUS RESTARTS AGE +web-1 1/1 Terminating 0 4h +web-1 1/1 Terminating 0 4h +web-1 0/1 Pending 0 0s +web-1 0/1 Pending 0 0s +web-1 0/1 ContainerCreating 0 0s +web-2 1/1 Terminating 0 23m +web-2 1/1 Terminating 0 23m +web-1 1/1 Running 0 4s +web-2 0/1 Pending 0 0s +web-2 0/1 Pending 0 0s +web-2 0/1 ContainerCreating 0 0s +web-2 1/1 Running 0 36s +``` + + +获取 Pod 来查看他们的容器镜像。 + +```shell{% raw %} +kubectl get pod -l app=nginx -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.containers[0].image}{"\n"}{end}' +web-0 gcr.io/google_containers/nginx-slim:0.7 +web-1 gcr.io/google_containers/nginx-slim:0.7 +web-2 gcr.io/google_containers/nginx-slim:0.7 +{% endraw %} +``` + + +现在,StatefulSet 中的 Pod 都已经运行了新的容器镜像。 + + +### Rolling Update 策略 + + +`RollingUpdate` 更新策略会更新一个 StatefulSet 中所有的 Pod,采用与序号索引相反的顺序并遵循 StatefulSet 的保证。 + + +Patch `web` StatefulSet 来执行 `RollingUpdate` 更新策略。 + +```shell +kubectl patch statefulset web -p '{"spec":{"updateStrategy":{"type":"RollingUpdate"}}} +statefulset "web" patched +``` + +在一个终端窗口中 patch `web` StatefulSet 来再次的改变容器镜像。 + +```shell +kubectl patch statefulset web --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value":"gcr.io/google_containers/nginx-slim:0.8"}]' +statefulset "web" patched +``` + + +在另一个终端监控 StatefulSet 中的 Pod。 + +```shell +kubectl get po -l app=nginx -w +NAME READY STATUS RESTARTS AGE +web-0 1/1 Running 0 7m +web-1 1/1 Running 0 7m +web-2 1/1 Running 0 8m +web-2 1/1 Terminating 0 8m +web-2 1/1 Terminating 0 8m +web-2 0/1 Terminating 0 8m +web-2 0/1 Terminating 0 8m +web-2 0/1 Terminating 0 8m +web-2 0/1 Terminating 0 8m +web-2 0/1 Pending 0 0s +web-2 0/1 Pending 0 0s +web-2 0/1 ContainerCreating 0 0s +web-2 1/1 Running 0 19s +web-1 1/1 Terminating 0 8m +web-1 0/1 Terminating 0 8m +web-1 0/1 Terminating 0 8m +web-1 0/1 Terminating 0 8m +web-1 0/1 Pending 0 0s +web-1 0/1 Pending 0 0s +web-1 0/1 ContainerCreating 0 0s +web-1 1/1 Running 0 6s +web-0 1/1 Terminating 0 7m +web-0 1/1 Terminating 0 7m +web-0 0/1 Terminating 0 7m +web-0 0/1 Terminating 0 7m +web-0 0/1 Terminating 0 7m +web-0 0/1 Terminating 0 7m +web-0 0/1 Pending 0 0s +web-0 0/1 Pending 0 0s +web-0 0/1 ContainerCreating 0 0s +web-0 1/1 Running 0 10s +``` + +StatefulSet 里的 Pod 采用和序号相反的顺序更新。在更新下一个 Pod 前,StatefulSet 控制器终止每个 Pod 并等待它们变成 Running 和 Ready。请注意,虽然在顺序后继者变成 Running 和 Ready 之前 StatefulSet 控制器不会更新下一个 Pod,但它仍然会重建任何在更新过程中发生故障的 Pod, 使用的是它们当前的版本。已经接收到更新请求的 Pod 将会被恢复为更新的版本,没有收到请求的 Pod 则会被恢复为之前的版本。像这样,控制器尝试继续使应用保持健康并在出现间歇性故障时保持更新的一致性。 + + +获取 Pod 来查看他们的容器镜像。 + +```shell{% raw %} +for p in 0 1 2; do kubectl get po web-$p --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'; echo; done +gcr.io/google_containers/nginx-slim:0.8 +gcr.io/google_containers/nginx-slim:0.8 +gcr.io/google_containers/nginx-slim:0.8 +{% endraw %} +``` + +StatefulSet 中的所有 Pod 现在都在运行之前的容器镜像。 + + +**小窍门**:你还可以使用 `kubectl rollout status sts/` 来查看 rolling update 的状态。 + + +#### 分段更新 + +你可以使用 `RollingUpdate` 更新策略的 `partition` 参数来分段更新一个 StatefulSet。分段的更新将会使 StatefulSet 中的其余所有 Pod 保持当前版本的同时仅允许改变 StatefulSet 的 `.spec.template`。 + + +Patch `web` StatefulSet 来对 `updateStrategy` 字段添加一个分区。 + +```shell +kubectl patch statefulset web -p '{"spec":{"updateStrategy":{"type":"RollingUpdate","rollingUpdate":{"partition":3}}}}' +statefulset "web" patched +``` + + +再次 Patch StatefulSet 来改变容器镜像。 + +```shell +kubectl patch statefulset web --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value":"gcr.io/google_containers/nginx-slim:0.7"}]' +statefulset "web" patched +``` + + +删除 StatefulSet 中的 Pod。 + +```shell +kubectl delete po web-2 +pod "web-2" deleted +``` + + +等待 Pod 变成 Running 和 Ready。 + +```shell +kubectl get po -lapp=nginx -w +NAME READY STATUS RESTARTS AGE +web-0 1/1 Running 0 4m +web-1 1/1 Running 0 4m +web-2 0/1 ContainerCreating 0 11s +web-2 1/1 Running 0 18s +``` + + +获取 Pod 的容器。 + +```shell{% raw %} +get po web-2 --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}' +gcr.io/google_containers/nginx-slim:0.8 +{% endraw %} +``` + + +请注意,虽然更新策略是 `RollingUpdate`,StatefulSet 控制器还是会使用原始的容器恢复 Pod。这是因为 Pod 的序号比 `updateStrategy` 指定的 `partition` 更小。 + + +#### 灰度扩容 + +你可以通过减少 [上文](#分段更新)指定的 `partition` 来进行灰度扩容,以此来测试你的程序的改动。 + + +Patch StatefulSet 来减少分区。 + +```shell +kubectl patch statefulset web -p '{"spec":{"updateStrategy":{"type":"RollingUpdate","rollingUpdate":{"partition":2}}}}' +statefulset "web" patched +``` + + +等待 `web-2` 变成 Running 和 Ready。 + +```shell +kubectl get po -lapp=nginx -w +NAME READY STATUS RESTARTS AGE +web-0 1/1 Running 0 4m +web-1 1/1 Running 0 4m +web-2 0/1 ContainerCreating 0 11s +web-2 1/1 Running 0 18s +``` + + +获取 Pod 的容器。 + +```shell{% raw %} +kubectl get po web-2 --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}' +gcr.io/google_containers/nginx-slim:0.7 +{% endraw %} +``` + + +当你改变 `partition` 时,StatefulSet 会自动的更新 `web-2` Pod,这是因为 Pod 的序号小于或等于 `partition`。 + + +删除 `web-1` Pod。 + +```shell +kubectl delete po web-1 +pod "web-1" deleted +``` + + +等待 `web-1` 变成 Running 和 Ready。 + +```shell +kubectl get po -lapp=nginx -w +NAME READY STATUS RESTARTS AGE +web-0 1/1 Running 0 6m +web-1 0/1 Terminating 0 6m +web-2 1/1 Running 0 2m +web-1 0/1 Terminating 0 6m +web-1 0/1 Terminating 0 6m +web-1 0/1 Terminating 0 6m +web-1 0/1 Pending 0 0s +web-1 0/1 Pending 0 0s +web-1 0/1 ContainerCreating 0 0s +web-1 1/1 Running 0 18s +``` + + +获取 `web-1` Pod 的容器。 + +```shell{% raw %} +get po web-1 --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}' +gcr.io/google_containers/nginx-slim:0.8 +{% endraw %} +``` + +`web-1` 被按照原来的配置恢复,因为 Pod 的序号小于分区。当指定了分区时,如果更新了 StatefulSet 的 `.spec.template`,则所有序号大于或等于分区的 Pod 都将被更新。如果一个序号小于分区的 Pod 被删除或者终止,它将被按照原来的配置恢复。 + + +#### 分阶段的扩容 + +你可以使用类似[灰度扩容](#灰度扩容)的方法执行一次分阶段的扩容(例如一次线性的、等比的或者指数形式的扩容)。要执行一次分阶段的扩容,你需要设置 `partition` 为希望控制器暂停更新的序号。 + + +分区当前为`2`。请将分区设置为`0`。 + +```shell +kubectl patch statefulset web -p '{"spec":{"updateStrategy":{"type":"RollingUpdate","rollingUpdate":{"partition":0}}}}' +statefulset "web" patched +``` + + +等待 StatefulSet 中的所有 Pod 变成 Running 和 Ready。 + +```shell +kubectl get po -lapp=nginx -w +NAME READY STATUS RESTARTS AGE +web-0 1/1 Running 0 3m +web-1 0/1 ContainerCreating 0 11s +web-2 1/1 Running 0 2m +web-1 1/1 Running 0 18s +web-0 1/1 Terminating 0 3m +web-0 1/1 Terminating 0 3m +web-0 0/1 Terminating 0 3m +web-0 0/1 Terminating 0 3m +web-0 0/1 Terminating 0 3m +web-0 0/1 Terminating 0 3m +web-0 0/1 Pending 0 0s +web-0 0/1 Pending 0 0s +web-0 0/1 ContainerCreating 0 0s +web-0 1/1 Running 0 3s +``` + + +获取 Pod 的容器。 + +```shell{% raw %} +for p in 0 1 2; do kubectl get po web-$p --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'; echo; done +gcr.io/google_containers/nginx-slim:0.7 +gcr.io/google_containers/nginx-slim:0.7 +gcr.io/google_containers/nginx-slim:0.7 +{% endraw %} +``` + + +将 `partition` 改变为 `0` 以允许StatefulSet控制器继续更新过程。 + + +## 删除 StatefulSet + + +StatefulSet 同时支持级联和非级联删除。使用非级联方式删除 StatefulSet 时,StatefulSet 的 Pod 不会被删除。使用级联删除时,StatefulSet 和它的 Pod 都会被删除。 + + +### 非级联删除 + + +在一个终端窗口查看 StatefulSet 中的 Pod。 + +``` +kubectl get pods -w -l app=nginx +``` + + +使用 [`kubectl delete`](/docs/user-guide/kubectl/{{page.version}}/#delete) 删 除StatefulSet。请确保提供了 `--cascade=false` 参数给命令。这个参数告诉 Kubernetes 只删除 StatefulSet 而不要删除它的任何 Pod。 + +```shell +kubectl delete statefulset web --cascade=false +statefulset "web" deleted +``` + + +获取 Pod 来检查他们的状态。 + +```shell +kubectl get pods -l app=nginx +NAME READY STATUS RESTARTS AGE +web-0 1/1 Running 0 6m +web-1 1/1 Running 0 7m +web-2 1/1 Running 0 5m +``` + + +虽然 `web` 已经被删除了,但所有 Pod 仍然处于 Running 和 Ready 状态。 +删除 `web-0`。 + +```shell +kubectl delete pod web-0 +pod "web-0" deleted +``` + + +获取 StatefulSet 的 Pod。 + +```shell +kubectl get pods -l app=nginx +NAME READY STATUS RESTARTS AGE +web-1 1/1 Running 0 10m +web-2 1/1 Running 0 7m +``` + + +由于 `web` StatefulSet 已经被删除,`web-0` 没有被重新启动。 + + +在一个终端监控 StatefulSet 的 Pod。 + +```shell +kubectl get pods -w -l app=nginx +``` + + +在另一个终端里重新创建 StatefulSet。请注意,除非你删除了 `nginx` Service (你不应该这样做),你将会看到一个错误,提示 Service 已经存在。 + +```shell +kubectl create -f web.yaml +statefulset "web" created +Error from server (AlreadyExists): error when creating "web.yaml": services "nginx" already exists +``` + + +请忽略这个错误。它仅表示 kubernetes 进行了一次创建 nginx Headless Service 的尝试,尽管那个 Service 已经存在。 + + +在第一个终端中运行并检查 `kubectl get` 命令的输出。 + +```shell +kubectl get pods -w -l app=nginx +NAME READY STATUS RESTARTS AGE +web-1 1/1 Running 0 16m +web-2 1/1 Running 0 2m +NAME READY STATUS RESTARTS AGE +web-0 0/1 Pending 0 0s +web-0 0/1 Pending 0 0s +web-0 0/1 ContainerCreating 0 0s +web-0 1/1 Running 0 18s +web-2 1/1 Terminating 0 3m +web-2 0/1 Terminating 0 3m +web-2 0/1 Terminating 0 3m +web-2 0/1 Terminating 0 3m +``` + + +当重新创建 `web` StatefulSet 时,`web-0` 被第一个重新启动。由于 `web-1` 已经处于 Running 和 Ready 状态,当 `web-0` 变成 Running 和 Ready 时,StatefulSet 会直接接收这个 Pod。由于你重新创建的 StatefulSet 的 `replicas` 等于2,一旦 `web-0` 被重新创建并且 `web-1` 被认为已经处于 Running 和 Ready 状态时,`web-2` 将会被终止。 + + +让我们再看看被 Pod 的 web 服务器加载的 `index.html` 的内容。 + +```shell +for i in 0 1; do kubectl exec -it web-$i -- curl localhost; done +web-0 +web-1 +``` + + +尽管你同时删除了 StatefulSet 和 `web-0` Pod,但它仍然使用最初写入 `index.html` 文件的主机名进行服务。这是因为 StatefulSet 永远不会删除和一个 Pod 相关联的 PersistentVolumes。当你重建这个 StatefulSet 并且重新启动了 `web-0` 时,它原本的 PersistentVolume 会被重新挂载。 + + +### 级联删除 + + +在一个终端窗口观察 StatefulSet 里的 Pod。 + +```shell +kubectl get pods -w -l app=nginx +``` + + +在另一个窗口中再次删除这个 StatefulSet。这次省略 `--cascade=false` 参数。 + +```shell +kubectl delete statefulset web +statefulset "web" deleted +``` + +在第一个终端检查 `kubectl get` 命令的输出,并等待所有的 Pod 变成 Terminating 状态。 + +```shell +kubectl get pods -w -l app=nginx +NAME READY STATUS RESTARTS AGE +web-0 1/1 Running 0 11m +web-1 1/1 Running 0 27m +NAME READY STATUS RESTARTS AGE +web-0 1/1 Terminating 0 12m +web-1 1/1 Terminating 0 29m +web-0 0/1 Terminating 0 12m +web-0 0/1 Terminating 0 12m +web-0 0/1 Terminating 0 12m +web-1 0/1 Terminating 0 29m +web-1 0/1 Terminating 0 29m +web-1 0/1 Terminating 0 29m + +``` + +如同你在[缩容](#ordered-pod-termination)一节看到的,Pod 按照和他们序号索引相反的顺序每次终止一个。在终止一个 Pod 前,StatefulSet 控制器会等待 Pod 后继者被完全终止。 + + +请注意,虽然级联删除会删除 StatefulSet 和它的 Pod,但它并不会删除和 StatefulSet 关联 的Headless Service。你必须手动删除 `nginx` Service。 + +```shell +kubectl delete service nginx +service "nginx" deleted +``` + + +再一次重新创建 StatefulSet 和 Headless Service。 + +```shell +kubectl create -f web.yaml +service "nginx" created +statefulset "web" created +``` + + +当 StatefulSet 所有的 Pod 变成 Running 和 Ready 时,获取它们的 `index.html` 文件的内容。 + +```shell +for i in 0 1; do kubectl exec -it web-$i -- curl localhost; done +web-0 +web-1 +``` + + +即使你已经删除了 StatefulSet 和它的全部 Pod,这些 Pod 将会被重新创建并挂载它们的 PersistentVolumes,并且 `web-0` 和 `web-1` 将仍然使用它们的主机名提供服务。 + + +最后删除 `web` StatefulSet 和 `nginx` service。 + +```shell +kubectl delete service nginx +service "nginx" deleted + +kubectl delete statefulset web +statefulset "web" deleted +``` + + +## Pod 管理策略 + + +对于某些分布式系统来说,StatefulSet 的顺序性保证是不必要和/或者不应该的。这些系统仅仅要求唯一性和身份标志。为了解决这个问题,在 Kubernetes 1.7 中我们针对 StatefulSet API Object 引入了 `.spec.podManagementPolicy`。 + + +### OrderedReady Pod 管理策略 + + +`OrderedReady` pod 管理策略是 StatefulSets 的默认选项。它告诉 StatefulSet 控制器遵循上文展示的顺序性保证。 + + +### Parallel Pod 管理策略 + + +`Parallel` pod 管理策略告诉 StatefulSet 控制器并行的终止所有 Pod,在启动或终止另一个 Pod 前,不必等待这些 Pod 变成 Running 和 Ready 或者完全终止状态。 + +{% include code.html language="yaml" file="webp.yaml" ghlink="/docs/tutorials/stateful-application/webp.yaml" %} + + +下载上面的例子并保存为 `webp.yaml`。 + + +这份清单和你在上文下载的完全一样,只是 `web` StatefulSet 的 `.spec.podManagementPolicy` 设置成了 `Parallel`。 + + +在一个终端窗口查看 StatefulSet 中的 Pod。 + +```shell +kubectl get po -lapp=nginx -w +``` + + +在另一个终端窗口创建清单中的 StatefulSet 和 Service。 + +```shell +kubectl create -f webp.yaml +service "nginx" created +statefulset "web" created +``` + + +查看你在第一个终端中运行的 `kubectl get` 命令的输出。 + +```shell +kubectl get po -lapp=nginx -w +NAME READY STATUS RESTARTS AGE +web-0 0/1 Pending 0 0s +web-0 0/1 Pending 0 0s +web-1 0/1 Pending 0 0s +web-1 0/1 Pending 0 0s +web-0 0/1 ContainerCreating 0 0s +web-1 0/1 ContainerCreating 0 0s +web-0 1/1 Running 0 10s +web-1 1/1 Running 0 10s +``` + + +StatefulSet 控制器同时启动了 `web-0` 和 `web-1`。 + +保持第二个终端打开,并在另一个终端窗口中扩容 StatefulSet。 + +```shell +kubectl scale statefulset/web --replicas=4 +statefulset "web" scaled +``` + + +在 `kubectl get` 命令运行的终端里检查它的输出。 + +```shell +web-3 0/1 Pending 0 0s +web-3 0/1 Pending 0 0s +web-3 0/1 Pending 0 7s +web-3 0/1 ContainerCreating 0 7s +web-2 1/1 Running 0 10s +web-3 1/1 Running 0 26s +``` + + +StatefulSet 控制器启动了两个新的 Pod,而且在启动第二个之前并没有等待第一个变成 Running 和 Ready 状态。 + +保持这个终端打开,并在另一个终端删除 `web` StatefulSet。 + +```shell +kubectl delete sts web +``` + + +在另一个终端里再次检查 `kubectl get` 命令的输出。 + +```shell +web-3 1/1 Terminating 0 9m +web-2 1/1 Terminating 0 9m +web-3 1/1 Terminating 0 9m +web-2 1/1 Terminating 0 9m +web-1 1/1 Terminating 0 44m +web-0 1/1 Terminating 0 44m +web-0 0/1 Terminating 0 44m +web-3 0/1 Terminating 0 9m +web-2 0/1 Terminating 0 9m +web-1 0/1 Terminating 0 44m +web-0 0/1 Terminating 0 44m +web-2 0/1 Terminating 0 9m +web-2 0/1 Terminating 0 9m +web-2 0/1 Terminating 0 9m +web-1 0/1 Terminating 0 44m +web-1 0/1 Terminating 0 44m +web-1 0/1 Terminating 0 44m +web-0 0/1 Terminating 0 44m +web-0 0/1 Terminating 0 44m +web-0 0/1 Terminating 0 44m +web-3 0/1 Terminating 0 9m +web-3 0/1 Terminating 0 9m +web-3 0/1 Terminating 0 9m +``` + + +StatefulSet 控制器将并发的删除所有 Pod,在删除一个 Pod 前不会等待它的顺序后继者终止。 + + +关闭 `kubectl get` 命令运行的终端并删除 `nginx` Service。 + +```shell +kubectl delete svc nginx +``` +{% endcapture %} + +{% capture cleanup %} + +你需要删除本教程中用到的 PersistentVolumes 的持久化存储媒体。基于你的环境、存储配置和提供方式,按照必须的步骤保证回收所有的存储。 +{% endcapture %} + +{% include templates/tutorial.md %} diff --git a/cn/docs/tutorials/stateful-application/cassandra-service.yaml b/cn/docs/tutorials/stateful-application/cassandra-service.yaml new file mode 100644 index 0000000000000..35b07733b5cac --- /dev/null +++ b/cn/docs/tutorials/stateful-application/cassandra-service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: cassandra + name: cassandra +spec: + clusterIP: None + ports: + - port: 9042 + selector: + app: cassandra diff --git a/cn/docs/tutorials/stateful-application/cassandra-statefulset.yaml b/cn/docs/tutorials/stateful-application/cassandra-statefulset.yaml new file mode 100644 index 0000000000000..f283acf657c2e --- /dev/null +++ b/cn/docs/tutorials/stateful-application/cassandra-statefulset.yaml @@ -0,0 +1,94 @@ +apiVersion: "apps/v1beta1" +kind: StatefulSet +metadata: + name: cassandra +spec: + serviceName: cassandra + replicas: 3 + template: + metadata: + labels: + app: cassandra + spec: + containers: + - name: cassandra + image: gcr.io/google-samples/cassandra:v12 + imagePullPolicy: Always + ports: + - containerPort: 7000 + name: intra-node + - containerPort: 7001 + name: tls-intra-node + - containerPort: 7199 + name: jmx + - containerPort: 9042 + name: cql + resources: + limits: + cpu: "500m" + memory: 1Gi + requests: + cpu: "500m" + memory: 1Gi + securityContext: + capabilities: + add: + - IPC_LOCK + lifecycle: + preStop: + exec: + command: ["/bin/sh", "-c", "PID=$(pidof java) && kill $PID && while ps -p $PID > /dev/null; do sleep 1; done"] + env: + - name: MAX_HEAP_SIZE + value: 512M + - name: HEAP_NEWSIZE + value: 100M + - name: CASSANDRA_SEEDS + value: "cassandra-0.cassandra.default.svc.cluster.local" + - name: CASSANDRA_CLUSTER_NAME + value: "K8Demo" + - name: CASSANDRA_DC + value: "DC1-K8Demo" + - name: CASSANDRA_RACK + value: "Rack1-K8Demo" + - name: CASSANDRA_AUTO_BOOTSTRAP + value: "false" + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + readinessProbe: + exec: + command: + - /bin/bash + - -c + - /ready-probe.sh + initialDelaySeconds: 15 + timeoutSeconds: 5 + # These volume mounts are persistent. They are like inline claims, + # but not exactly because the names need to match exactly one of + # the stateful pod volumes. + volumeMounts: + - name: cassandra-data + mountPath: /cassandra_data + # These are converted to volume claims by the controller + # and mounted at the paths mentioned above. + # do not use these in production until ssd GCEPersistentDisk or other ssd pd + volumeClaimTemplates: + - metadata: + name: cassandra-data + annotations: + volume.beta.kubernetes.io/storage-class: fast + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 1Gi +--- +kind: StorageClass +apiVersion: storage.k8s.io/v1beta1 +metadata: + name: fast +provisioner: k8s.io/minikube-hostpath +parameters: + type: pd-ssd diff --git a/cn/docs/tutorials/stateful-application/cassandra.md b/cn/docs/tutorials/stateful-application/cassandra.md new file mode 100644 index 0000000000000..2489860d18056 --- /dev/null +++ b/cn/docs/tutorials/stateful-application/cassandra.md @@ -0,0 +1,851 @@ +--- +title: "Example: Deploying Cassandra with Stateful Sets" +title: “示例:使用 Stateful Sets 部署 Cassandra” +--- + + +## 目录 + + - [准备工作](#prerequisites) + - [Cassandra docker 镜像](#cassandra-docker) + - [快速入门](#quickstart) + - [步骤1:创建 Cassandra Headless Service](#step-1-create-a-cassandra-headless-service) + - [步骤2:使用 StatefulSet 创建 Cassandra Ring环](#step-2-use-a-statefulset-to-create-cassandra-ring) + - [步骤3:验证并修改 Cassandra StatefulSet](#step-3-validate-and-modify-the-cassandra-statefulset) + - [步骤4:删除 Cassandra StatefulSet](#step-4-delete-cassandra-statefulset) + - [步骤5:使用 Replication Controller 创建 Cassandra 节点 pods](#step-5-use-a-replication-controller-to-create-cassandra-node-pods) + - [步骤6:Cassandra 集群扩容](#step-6-scale-up-the-cassandra-cluster) + - [步骤7:删除 Replication Controller](#step-7-delete-the-replication-controller) + - [步骤8:使用 DaemonSet 替换 Replication Controller](#step-8-use-a-daemonset-instead-of-a-replication-controller) + - [步骤9:资源清理](#step-9-resource-cleanup) + - [Seed Provider Source](#seed-provider-source) + + + +下文描述了在 Kubernetes 上部署一个_云原生_ [Cassandra](http://cassandra.apache.org/) 的过程。当我们说_云原生_时,指的是一个应用能够理解它运行在一个集群管理器内部,并且使用这个集群的管理基础设施来帮助实现这个应用。特别的,本例使用了一个自定义的 Cassandra `SeedProvider` 帮助 Cassandra 发现新加入集群 Cassandra 节点。 + + +本示例也使用了Kubernetes的一些核心组件: + +- [_Pods_](/docs/user-guide/pods) +- [ _Services_](/docs/user-guide/services) +- [_Replication Controllers_](/docs/user-guide/replication-controller) +- [_Stateful Sets_](/docs/concepts/workloads/controllers/statefulset/) +- [_Daemon Sets_](/docs/admin/daemons) + + + +## 准备工作 + + +本示例假设你已经安装运行了一个 Kubernetes集群(版本 >=1.2),并且还在某个路径下安装了 [`kubectl`](https://kubernetes.io/docs/tasks/tools/install-kubectl/) 命令行工具。请查看 [getting started guides](https://kubernetes.io/docs/getting-started-guides/) 获取关于你的平台的安装说明。 + + +本示例还需要一些代码和配置文件。为了避免手动输入,你可以 `git clone` Kubernetes 源到你本地。 + + +## Cassandra Docker镜像 + + +Pods 使用来自 Google 的 [container registry](https://cloud.google.com/container-registry/docs/) 的 [```gcr.io/google-samples/cassandra:v12```](https://github.com/kubernetes/examples/blob/master/cassandra/image/Dockerfile) 镜像。这个 docker 镜像基于 `debian:jessie` 并包含 OpenJDK 8。该镜像包含一个从 Apache Debian 源中安装的标准 Cassandra。你可以通过使用环境变量改变插入到 `cassandra.yaml` 文件中的参数值。 + +| ENV VAR | DEFAULT VALUE | +| ---------------------- | :------------: | +| CASSANDRA_CLUSTER_NAME | 'Test Cluster' | +| CASSANDRA_NUM_TOKENS | 32 | +| CASSANDRA_RPC_ADDRESS | 0.0.0.0 | + + +## 快速入门 + + +如果你希望直接跳到我们使用的命令,以下是全部步骤: + +```sh +# +# StatefulSet +# + +# clone the example repository +git clone https://github.com/kubernetes/examples +cd examples + +# create a service to track all cassandra statefulset nodes +kubectl create -f cassandra/cassandra-service.yaml + +# create a statefulset +kubectl create -f cassandra/cassandra-statefulset.yaml + +# validate the Cassandra cluster. Substitute the name of one of your pods. +kubectl exec -ti cassandra-0 -- nodetool status + +# cleanup +grace=$(kubectl get po cassandra-0 -o=jsonpath='{.spec.terminationGracePeriodSeconds}') \ + && kubectl delete statefulset,po -l app=cassandra \ + && echo "Sleeping $grace" \ + && sleep $grace \ + && kubectl delete pvc -l app=cassandra + +# +# Resource Controller Example +# + +# create a replication controller to replicate cassandra nodes +kubectl create -f cassandra/cassandra-controller.yaml + +# validate the Cassandra cluster. Substitute the name of one of your pods. +kubectl exec -ti cassandra-xxxxx -- nodetool status + +# scale up the Cassandra cluster +kubectl scale rc cassandra --replicas=4 + +# delete the replication controller +kubectl delete rc cassandra + +# +# Create a DaemonSet to place a cassandra node on each kubernetes node +# + +kubectl create -f cassandra/cassandra-daemonset.yaml --validate=false + +# resource cleanup +kubectl delete service -l app=cassandra +kubectl delete daemonset cassandra +``` + + +## 步骤1:创建 Cassandra Headless Service + + +Kubernetes _[Service](/docs/user-guide/services)_ 描述一组执行同样任务的 [_Pods_](/docs/user-guide/pods)。在Kubernetes中,一个应用的原子调度单位是一个 Pod:一个或多个_必须_调度到相同主机上的容器。 + + +这个 Service 用于在Kubernetes 集群内部进行 Cassandra 客户端和 Cassandra Pods之间的 DNS 查找。 + + +以下为这个 service 的描述: + + + +```yaml +apiVersion: v1 +kind: Service +metadata: + labels: + app: cassandra + name: cassandra +spec: + clusterIP: None + ports: + - port: 9042 + selector: + app: cassandra +``` + + +[下载示例](https://raw.githubusercontent.com/kubernetes/examples/master/cassandra-service.yaml) + + + +为 StatefulSet 创建 service + + +```console +$ kubectl create -f cassandra/cassandra-service.yaml +``` + + +以下命令显示了 service 是否被成功创建。 + +```console +$ kubectl get svc cassandra +``` + + +命令的响应应该像这样: + +```console +NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE +cassandra None 9042/TCP 45s +``` + + +如果返回错误则表示 service 创建失败。 + + +## 步骤2:使用 StatefulSet 创建 Cassandra Ring环 + + +StatefulSets(以前叫做 PetSets)特性在 Kubernetes 1.5 中升级为一个 Beta 组件。在集群环境中部署类似于 Cassandra 的有状态分布式应用是一项具有挑战性的工作。我们实现了StatefulSet,极大的简化了这个过程。本示例使用了 StatefulSet 的多个特性,但其本身超出了本文的范围。[请参考 Stateful Set 文档。](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/) + + +以下是StatefulSet 的清单文件,用于创建一个由三个 pods 组成的 Cassandra ring环。 + + +本示例使用了 GCE Storage Class,请根据你运行的云平台做适当的修改。 + + + +```yaml +apiVersion: "apps/v1beta1" +kind: StatefulSet +metadata: + name: cassandra +spec: + serviceName: cassandra + replicas: 3 + template: + metadata: + labels: + app: cassandra + spec: + containers: + - name: cassandra + image: gcr.io/google-samples/cassandra:v12 + imagePullPolicy: Always + ports: + - containerPort: 7000 + name: intra-node + - containerPort: 7001 + name: tls-intra-node + - containerPort: 7199 + name: jmx + - containerPort: 9042 + name: cql + resources: + limits: + cpu: "500m" + memory: 1Gi + requests: + cpu: "500m" + memory: 1Gi + securityContext: + capabilities: + add: + - IPC_LOCK + lifecycle: + preStop: + exec: + command: ["/bin/sh", "-c", "PID=$(pidof java) && kill $PID && while ps -p $PID > /dev/null; do sleep 1; done"] + env: + - name: MAX_HEAP_SIZE + value: 512M + - name: HEAP_NEWSIZE + value: 100M + - name: CASSANDRA_SEEDS + value: "cassandra-0.cassandra.default.svc.cluster.local" + - name: CASSANDRA_CLUSTER_NAME + value: "K8Demo" + - name: CASSANDRA_DC + value: "DC1-K8Demo" + - name: CASSANDRA_RACK + value: "Rack1-K8Demo" + - name: CASSANDRA_AUTO_BOOTSTRAP + value: "false" + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + readinessProbe: + exec: + command: + - /bin/bash + - -c + - /ready-probe.sh + initialDelaySeconds: 15 + timeoutSeconds: 5 + # These volume mounts are persistent. They are like inline claims, + # but not exactly because the names need to match exactly one of + # the stateful pod volumes. + volumeMounts: + - name: cassandra-data + mountPath: /cassandra_data + # These are converted to volume claims by the controller + # and mounted at the paths mentioned above. + # do not use these in production until ssd GCEPersistentDisk or other ssd pd + volumeClaimTemplates: + - metadata: + name: cassandra-data + annotations: + volume.beta.kubernetes.io/storage-class: fast + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 1Gi +--- +kind: StorageClass +apiVersion: storage.k8s.io/v1beta1 +metadata: + name: fast +provisioner: kubernetes.io/gce-pd +parameters: + type: pd-ssd +``` + + +[下载示例](https://raw.githubusercontent.com/kubernetes/examples/master/cassandra-statefulset.yaml) + + + +创建 Cassandra StatefulSet 如下: + +```console +$ kubectl create -f cassandra/cassandra-statefulset.yaml +``` + + +## 步骤3:验证和修改 Cassandra StatefulSet + + +这个 StatefulSet 的部署展示了 StatefulSets 提供的两个新特性: + +1. Pod 的名称已知 +2. Pod 以递增顺序部署 + + +首先,运行下面的 `kubectl` 命令,验证 StatefulSet 已经被成功部署。 + +```console +$ kubectl get statefulset cassandra +``` + + +这个命令的响应应该像这样: + +```console +NAME DESIRED CURRENT AGE +cassandra 3 3 13s +``` + + +接下来观察 Cassandra pods 以一个接一个的形式部署。StatefulSet 资源按照数字序号的模式部署 pods:1, 2, 3 等。如果在 pods 部署前执行下面的命令,你就能够看到这种顺序的创建过程。 + +```console +$ kubectl get pods -l="app=cassandra" +NAME READY STATUS RESTARTS AGE +cassandra-0 1/1 Running 0 1m +cassandra-1 0/1 ContainerCreating 0 8s +``` + + +上面的示例显示了三个 Cassandra StatefulSet pods 中的两个已经部署。一旦所有的 pods 都部署成功,相同的命令会显示一个完整的StatefulSet。 + +```console +$ kubectl get pods -l="app=cassandra" +NAME READY STATUS RESTARTS AGE +cassandra-0 1/1 Running 0 10m +cassandra-1 1/1 Running 0 9m +cassandra-2 1/1 Running 0 8m +``` + + +运行 Cassandra 工具 `nodetool` 将显示 ring环的状态。 + +```console +$ kubectl exec cassandra-0 -- nodetool status +Datacenter: DC1-K8Demo +====================== +Status=Up/Down +|/ State=Normal/Leaving/Joining/Moving +-- Address Load Tokens Owns (effective) Host ID Rack +UN 10.4.2.4 65.26 KiB 32 63.7% a9d27f81-6783-461d-8583-87de2589133e Rack1-K8Demo +UN 10.4.0.4 102.04 KiB 32 66.7% 5559a58c-8b03-47ad-bc32-c621708dc2e4 Rack1-K8Demo +UN 10.4.1.4 83.06 KiB 32 69.6% 9dce943c-581d-4c0e-9543-f519969cc805 Rack1-K8Demo +``` + + +你也可以运行 `cqlsh` 来显示集群的 keyspaces。 + +```console +$ kubectl exec cassandra-0 -- cqlsh -e 'desc keyspaces' + +system_traces system_schema system_auth system system_distributed +``` + + +你需要使用 `kubectl edit` 来增加或减小 Cassandra StatefulSet 的大小。你可以在 [文档](/docs/user-guide/kubectl/kubectl_edit) 中找到更多关于 edit 命令的信息。 + + +使用以下命令编辑 StatefulSet。 + +```console +$ kubectl edit statefulset cassandra +``` + + +这会在你的命令行中创建一个编辑器。你需要修改的行是 `replicas`。这个例子没有包含终端窗口的所有内容,下面示例中的最后一行就是你希望改变的 replicas 行。 + +```console +# 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: apps/v1beta1 +kind: StatefulSet +metadata: + creationTimestamp: 2016-08-13T18:40:58Z + generation: 1 + labels: + app: cassandra + name: cassandra + namespace: default + resourceVersion: "323" + selfLink: /apis/apps/v1beta1/namespaces/default/statefulsets/cassandra + uid: 7a219483-6185-11e6-a910-42010a8a0fc0 +spec: + replicas: 3 +``` + + +按下面的示例修改清单文件并保存。 + +```console +spec: + replicas: 4 +``` + + +这个 StatefulSet 现在将包含四个 pods。 + +```console +$ kubectl get statefulset cassandra +``` + + +这个command的响应应该像这样: + +```console +NAME DESIRED CURRENT AGE +cassandra 4 4 36m +``` + + +对于 Kubernetes 1.5 发布版,beta StatefulSet 资源没有像 Deployment, ReplicaSet, Replication Controller或者 Job一样,包含 `kubectl scale` 功能, + + +## 步骤4:删除 Cassandra StatefulSet + + +删除或者缩容 StatefulSet 时不会删除与之关联的 volumes。这样做是为了优先保证安全。你的数据比其它会被自动清除的 StatefulSet 关联资源更宝贵。删除 Persistent Volume Claims 可能会导致关联的 volumes 被删除,这种行为依赖 storage class 和 reclaim policy。永远不要期望能在 claim 删除后访问一个 volume。 + + +使用如下命令删除 StatefulSet。 + +```console +$ grace=$(kubectl get po cassandra-0 -o=jsonpath='{.spec.terminationGracePeriodSeconds}') \ + && kubectl delete statefulset -l app=cassandra \ + && echo "Sleeping $grace" \ + && sleep $grace \ + && kubectl delete pvc -l app=cassandra +``` + + +## 步骤5:使用 Replication Controller 创建 Cassandra 节点 pods + + +Kubernetes _[Replication Controller](/docs/user-guide/replication-controller)_ 负责复制一个完全相同的 pods 集合。像 Service 一样,它具有一个 selector query,用来识别它的集合成员。和 Service 不一样的是,它还具有一个期望的副本数,并且会通过创建或删除 Pods来保证 Pods 的数量满足它期望的状态。 + + +和我们刚才定义的 Service 一起,Replication Controller 能够让我们轻松的构建一个复制的、可扩展的 Cassandra 集群。 + + +让我们创建一个具有两个初始副本的 replication controller。 + + + +```yaml +apiVersion: v1 +kind: ReplicationController +metadata: + name: cassandra + # The labels will be applied automatically + # from the labels in the pod template, if not set + # labels: + # app: cassandra +spec: + replicas: 2 + # The selector will be applied automatically + # from the labels in the pod template, if not set. + # selector: + # app: cassandra + template: + metadata: + labels: + app: cassandra + spec: + containers: + - command: + - /run.sh + resources: + limits: + cpu: 0.5 + env: + - name: MAX_HEAP_SIZE + value: 512M + - name: HEAP_NEWSIZE + value: 100M + - name: CASSANDRA_SEED_PROVIDER + value: "io.k8s.cassandra.KubernetesSeedProvider" + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + image: gcr.io/google-samples/cassandra:v12 + name: cassandra + ports: + - containerPort: 7000 + name: intra-node + - containerPort: 7001 + name: tls-intra-node + - containerPort: 7199 + name: jmx + - containerPort: 9042 + name: cql + volumeMounts: + - mountPath: /cassandra_data + name: data + volumes: + - name: data + emptyDir: {} +``` + + +[下载示例](https://raw.githubusercontent.com/kubernetes/examples/master/cassandra-controller.yaml) + + + +在这个描述中需要注意几件事情。 + + +`selector` 属性包含了控制器的 selector query。它能够被显式指定,或者在没有设置时,像此处一样从 pod 模板中的 labels 中自动应用。 + + +Pod 模板的标签 `app:cassandra` 匹配步骤1中的 Service selector。这就是 Service 如何选择 replication controller 创建的 pods 的原理。 + + +`replicas` 属性指明了期望的副本数量,在本例中最开始为 2。我们很快将要扩容更多数量。 + + +创建 Replication Controller: + +```console + +$ kubectl create -f cassandra/cassandra-controller.yaml + +``` + + +你可以列出新建的 controller: + +```console + +$ kubectl get rc -o wide +NAME DESIRED CURRENT AGE CONTAINER(S) IMAGE(S) SELECTOR +cassandra 2 2 11s cassandra gcr.io/google-samples/cassandra:v12 app=cassandra + +``` + + +现在,如果你列出集群中的 pods,并且使用 `app=cassandra` 标签过滤,你应该能够看到两个 Cassandra pods。(`wide` 参数使你能够看到 pods 被调度到了哪个 Kubernetes 节点上) + +```console + +$ kubectl get pods -l="app=cassandra" -o wide +NAME READY STATUS RESTARTS AGE NODE +cassandra-21qyy 1/1 Running 0 1m kubernetes-minion-b286 +cassandra-q6sz7 1/1 Running 0 1m kubernetes-minion-9ye5 + +``` + + +因为这些 pods 拥有 `app=cassandra` 标签,它们被映射给了我们在步骤1中创建的 service。 + + +你可以使用下面的 service endpoint 查询命令来检查 Pods 是否对 Service 可用。 + +```console + +$ kubectl get endpoints cassandra -o yaml +apiVersion: v1 +kind: Endpoints +metadata: + creationTimestamp: 2015-06-21T22:34:12Z + labels: + app: cassandra + name: cassandra + namespace: default + resourceVersion: "944373" + selfLink: /api/v1/namespaces/default/endpoints/cassandra + uid: a3d6c25f-1865-11e5-a34e-42010af01bcc +subsets: +- addresses: + - ip: 10.244.3.15 + targetRef: + kind: Pod + name: cassandra + namespace: default + resourceVersion: "944372" + uid: 9ef9895d-1865-11e5-a34e-42010af01bcc + ports: + - port: 9042 + protocol: TCP + +``` + + +为了显示 `SeedProvider` 逻辑是按设想在运行,你可以使用 `nodetool` 命令来检查 Cassandra 集群的状态。为此,请使用 `kubectl exec` 命令,这样你就能在一个 Cassandra pod 上运行 `nodetool`。同样的,请替换 `cassandra-xxxxx` 为任意一个 pods的真实名字。 + +```console + +$ kubectl exec -ti cassandra-xxxxx -- nodetool status +Datacenter: datacenter1 +======================= +Status=Up/Down +|/ State=Normal/Leaving/Joining/Moving +-- Address Load Tokens Owns (effective) Host ID Rack +UN 10.244.0.5 74.09 KB 256 100.0% 86feda0f-f070-4a5b-bda1-2eeb0ad08b77 rack1 +UN 10.244.3.3 51.28 KB 256 100.0% dafe3154-1d67-42e1-ac1d-78e7e80dce2b rack1 + +``` + + +## 步骤6:Cassandra集群扩容 + + +现在,让我们把 Cassandra 集群扩展到4个 pods。我们通过告诉 Replication Controller 现在我们需要4个副本来完成。 + +```sh + +$ kubectl scale rc cassandra --replicas=4 + +``` + + +你可以看到列出了新的 pods: + +```console + +$ kubectl get pods -l="app=cassandra" -o wide +NAME READY STATUS RESTARTS AGE NODE +cassandra-21qyy 1/1 Running 0 6m kubernetes-minion-b286 +cassandra-81m2l 1/1 Running 0 47s kubernetes-minion-b286 +cassandra-8qoyp 1/1 Running 0 47s kubernetes-minion-9ye5 +cassandra-q6sz7 1/1 Running 0 6m kubernetes-minion-9ye5 + +``` + + +一会儿你就能再次检查 Cassandra 集群的状态,你可以看到新的 pods 已经被自定义的 `SeedProvider` 检测到: + +```console + +$ kubectl exec -ti cassandra-xxxxx -- nodetool status +Datacenter: datacenter1 +======================= +Status=Up/Down +|/ State=Normal/Leaving/Joining/Moving +-- Address Load Tokens Owns (effective) Host ID Rack +UN 10.244.0.6 51.67 KB 256 48.9% d07b23a5-56a1-4b0b-952d-68ab95869163 rack1 +UN 10.244.1.5 84.71 KB 256 50.7% e060df1f-faa2-470c-923d-ca049b0f3f38 rack1 +UN 10.244.1.6 84.71 KB 256 47.0% 83ca1580-4f3c-4ec5-9b38-75036b7a297f rack1 +UN 10.244.0.5 68.2 KB 256 53.4% 72ca27e2-c72c-402a-9313-1e4b61c2f839 rack1 + +``` + + +## 步骤7:删除 Replication Controller + + +在你开始步骤5之前, __删除__你在上面创建的 __replication controller__。 + +```sh + +$ kubectl delete rc cassandra + +``` + + +## 步骤8:使用 DaemonSet 替换 Replication Controller + + +在 Kubernetes中,[_Daemon Set_](/docs/admin/daemons) 能够将 pods 一对一的分布到 Kubernetes 节点上。和 _ReplicationController_ 相同的是它也有一个用于识别它的集合成员的 selector query。但和 _ReplicationController_ 不同的是,它拥有一个节点 selector,用于限制基于模板的 pods 可以调度的节点。并且 pod 的复制不是基于一个设置的数量,而是为每一个节点分配一个 pod。 + + +示范用例:当部署到云平台时,预期情况是实例是短暂的并且随时可能终止。Cassandra 被搭建成为在各个节点间复制数据以便于实现数据冗余。这样的话,即使一个实例终止了,存储在它上面的数据却没有,并且集群会通过重新复制数据到其它运行节点来作为响应。 + + +`DaemonSet` 设计为在 Kubernetes 集群中的每个节点上放置一个 pod。那样就会给我们带来数据冗余度。让我们创建一个 DaemonSet 来启动我们的存储集群: + + + +```yaml +apiVersion: extensions/v1beta1 +kind: DaemonSet +metadata: + labels: + name: cassandra + name: cassandra +spec: + template: + metadata: + labels: + app: cassandra + spec: + # Filter to specific nodes: + # nodeSelector: + # app: cassandra + containers: + - command: + - /run.sh + env: + - name: MAX_HEAP_SIZE + value: 512M + - name: HEAP_NEWSIZE + value: 100M + - name: CASSANDRA_SEED_PROVIDER + value: "io.k8s.cassandra.KubernetesSeedProvider" + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + image: gcr.io/google-samples/cassandra:v12 + name: cassandra + ports: + - containerPort: 7000 + name: intra-node + - containerPort: 7001 + name: tls-intra-node + - containerPort: 7199 + name: jmx + - containerPort: 9042 + name: cql + # If you need it it is going away in C* 4.0 + #- containerPort: 9160 + # name: thrift + resources: + requests: + cpu: 0.5 + volumeMounts: + - mountPath: /cassandra_data + name: data + volumes: + - name: data + emptyDir: {} +``` + + +[下载示例](https://raw.githubusercontent.com/kubernetes/examples/master/cassandra-daemonset.yaml) + + + +这个 DaemonSet 绝大部分的定义和上面的 ReplicationController 完全相同;它只是简单的给 daemon set 一个创建新的 Cassandra pods 的方法,并且以集群中所有的 Cassandra 节点为目标。 + + +不同之处在于 `nodeSelector` 属性,它允许 DaemonSet 以全部节点的一个子集为目标(你可以向其他资源一样标记节点),并且没有 `replicas` 属性,因为它使用1对1的 node-pod 关系。 + + +创建这个 DaemonSet: + +```console + +$ kubectl create -f cassandra/cassandra-daemonset.yaml + +``` + + +你可能需要禁用配置文件检查,像这样: + +```console + +$ kubectl create -f cassandra/cassandra-daemonset.yaml --validate=false + +``` + + +你可以看到 DaemonSet 已经在运行: + +```console + +$ kubectl get daemonset +NAME DESIRED CURRENT NODE-SELECTOR +cassandra 3 3 + +``` + + +现在,如果你列出集群中的 pods,并且使用 `app=cassandra` 标签过滤,你应该能够看到你的网络中的每一个节点上都有一个(且只有一个)新的 cassandra pod。 + +```console + +$ kubectl get pods -l="app=cassandra" -o wide +NAME READY STATUS RESTARTS AGE NODE +cassandra-ico4r 1/1 Running 0 4s kubernetes-minion-rpo1 +cassandra-kitfh 1/1 Running 0 1s kubernetes-minion-9ye5 +cassandra-tzw89 1/1 Running 0 2s kubernetes-minion-b286 + +``` + + +为了证明这是按设想的在工作,你可以再次使用 `nodetool` 命令来检查集群的状态。为此,请使用 `kubectl exec` 命令在任何一个新建的 cassandra pods 上运行 `nodetool`。 + +```console + +$ kubectl exec -ti cassandra-xxxxx -- nodetool status +Datacenter: datacenter1 +======================= +Status=Up/Down +|/ State=Normal/Leaving/Joining/Moving +-- Address Load Tokens Owns (effective) Host ID Rack +UN 10.244.0.5 74.09 KB 256 100.0% 86feda0f-f070-4a5b-bda1-2eeb0ad08b77 rack1 +UN 10.244.4.2 32.45 KB 256 100.0% 0b1be71a-6ffb-4895-ac3e-b9791299c141 rack1 +UN 10.244.3.3 51.28 KB 256 100.0% dafe3154-1d67-42e1-ac1d-78e7e80dce2b rack1 + +``` + + +**注意**:这个示例让你在创建 DaemonSet 前删除了 cassandra的Replication Controller。这是因为为了保持示例的简单,RC 和 DaemonSet 使用了相同的 `app=cassandra` 标签(如此它们的 pods 映射到了我们创建的 service,这样 SeedProvider 就能识别它们)。 + + +如果我们没有预先删除 RC,这两个资源在需要运行多少 pods 上将会发生冲突。如果希望的话,我们可以使用额外的标签和 selectors 来支持同时运行它们。 + + +## 步骤9:资源清理 + + +当你准备删除你的资源时,按以下执行: + +```console + +$ kubectl delete service -l app=cassandra +$ kubectl delete daemonset cassandra + +``` + + +### 自定义 Seed Provider + + +我们使用了一个自定义的 [`SeedProvider`](https://svn.apache.org/repos/asf/cassandra/trunk/src/java/org/apache/cassandra/locator/SeedProvider.java) 来在 Kubernetes 之上运行 Cassandra。仅当你通过 replication control 或者 daemonset 部署 Cassandra 时才需要使用自定义的 seed provider。在 Cassandra 中,`SeedProvider` 引导 Cassandra 使用 gossip 协议来查找其它 Cassandra 节点。Seed 地址是被视为连接端点的主机。Cassandra 实例使用 seed 列表来查找彼此并学习 ring环拓扑。[`KubernetesSeedProvider`](https://github.com/kubernetes/kubernetes/blob/master/examples/storage/cassandra/java/src/main/java/io/k8s/cassandra/KubernetesSeedProvider.java) 通过 Kubernetes API 发现 Cassandra seeds IP 地址,那些 Cassandra 实例在 Cassandra Service 中定义。 + + +请查阅自定义 seed provider 的 [README](https://git.k8s.io/examples/cassandra/java/README.md) 文档,获取 `KubernetesSeedProvider` 进阶配置。对于本示例来说,你应该不需要自定义 Seed Provider 的配置。 + + +查看本示例的 [image](https://github.com/kubernetes/examples/tree/master/cassandra/image) 目录,了解如何构建容器的 docker 镜像及其内容。 + + +你可能还注意到我们设置了一些 Cassandra 参数(`MAX_HEAP_SIZE`和`HEAP_NEWSIZE`),并且增加了关于 [namespace](/docs/user-guide/namespaces) 的信息。我们还告诉 Kubernetes 容器暴露了 `CQL` 和 `Thrift` API 端口。最后,我们告诉集群管理器我们需要 0.1 cpu(0.1 核)。 + + + +[!Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/cassandra/README.md?pixel)]() + diff --git a/cn/docs/tutorials/stateful-application/dev b/cn/docs/tutorials/stateful-application/dev new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/cn/docs/tutorials/stateful-application/mysql-wordpress-persistent-volume.md b/cn/docs/tutorials/stateful-application/mysql-wordpress-persistent-volume.md new file mode 100644 index 0000000000000..635a246e06d46 --- /dev/null +++ b/cn/docs/tutorials/stateful-application/mysql-wordpress-persistent-volume.md @@ -0,0 +1,328 @@ +--- +title: "基于 Persistent Volumes 搭建 WordPress 和 MySQL 应用" +assignees: +- ahmetb +- jeffmendoza +--- + + +本示例描述了如何在 Kubeernetes 上持久化安装 [WordPress](https://wordpress.org/) 和 +[MySQL](https://www.mysql.com/) 。在这个安装里我们将使用官方的 [mysql](https://registry.hub.docker.com/_/mysql/) 和 +[wordpress](https://registry.hub.docker.com/_/wordpress/) 镜像(WordPress 镜像包含一个 Apache 服务)。 + + +展示的 Kubernetes 概念: + +* [Persistent Volumes](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) 定义持久化磁盘(磁盘生命周期不和 Pods 绑定)。 +* [Services](https://kubernetes.io/docs/concepts/services-networking/service/) 使得 Pods 能够找到其它 Pods。 +* [External Load Balancers](https://kubernetes.io/docs/concepts/services-networking/service/#type-loadbalancer) 对外暴露 Services。 +* [Deployments](http://kubernetes.io/docs/user-guide/deployments/) 确保 Pods 持续运行。 +* [Secrets](http://kubernetes.io/docs/user-guide/secrets/) 保存敏感密码信息。 + + +## 快速入门 + + +在一个名为 `password.txt` 的文件中放置你期望的 MySQL 密码,结尾不要有空行。如果你的编辑器添加了一个空行,开始的 `tr` 命令将会删除它。 + + +**请注意:**如果你的集群强制启用 **_selinux_** 特性并且你将使用 [Host Path](#host-path) 作为存储,请遵照这个[额外步骤](#selinux)。 + +```shell +tr --delete '\n' .strippedpassword.txt && mv .strippedpassword.txt password.txt +kubectl create -f https://raw.githubusercontent.com/kubernetes/examples/master/mysql-wordpress-pd/local-volumes.yaml +kubectl create secret generic mysql-pass --from-file=password.txt +kubectl create -f https://raw.githubusercontent.com/kubernetes/examples/master/mysql-wordpress-pd/mysql-deployment.yaml +kubectl create -f https://raw.githubusercontent.com/kubernetes/examples/master/mysql-wordpress-pd/wordpress-deployment.yaml +``` + + +## 目录 + + + + +[在 Kubernetes 上持久化安装 MySQL 和 WordPress](#persistent-installation-of-mysql-and-wordpress-on-kubernetes) + - [快速入门](#quickstart) + - [目录](#table-of-contents) + - [集群要求](#cluster-requirements) + - [决定在哪里存储你的数据](#decide-where-you-will-store-your-data) + - [Host Path](#host-path) + - [SELinux](#selinux) + - [GCE Persistent Disk](#gce-persistent-disk) + - [创建 MySQL 密码 secret](#create-the-mysql-password-secret) + - [部署 MySQL](#deploy-mysql) + - [部署 WordPress](#deploy-wordpress) + - [访问你的新 WordPress 博客](#visit-your-new-wordpress-blog) + - [删除并重启你的博客](#take-down-and-restart-your-blog) + - [接下来的步骤](#next-steps) + + + + +## 集群要求 + + +Kubernetes本质是模块化的,可以在各种环境中运行。但并不是所有集群都相同。此处是本示例的一些要求: +* 需要 1.2 版本以上的 Kubernetes,以使用更新的特性,例如 PV Claims 和 Deployments。运行 `kubectl version` 来查看你的集群版本。 +* [Cluster DNS](https://github.com/kubernetes/dns) 将被用于服务发现。 +* 一个 [external load balancer](https://kubernetes.io/docs/concepts/services-networking/service/#type-loadbalancer) 将被用于接入 WordPress。 +* 使用了 [Persistent Volume Claims](https://kubernetes.io/docs/concepts/storage/persistent-volumes/#persistentvolumeclaims)。你必须创建集群中需要的 Persistent Volumes。本示例将展示两种类型的 volume 的创建方法,但是任何类型的 volume 都是足够使用的。 + + +查阅 [Getting Started Guide](http://kubernetes.io/docs/getting-started-guides/),搭建一个集群并安装 [kubectl](http://kubernetes.io/docs/user-guide/prereqs/) 命令行工具。 + + +## 决定在哪里存储你的数据 + + +MySQL 和 WordPress 各自使用一个 [Persistent Volume](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) 来存储自己的数据。我们将使用一个 Persistent Volume Claim 来取得一个可用的持久化存储。本示例覆盖了 HostPath 和 +GCEPersistentDisk 卷类型。你可以从两者中选择一个,或者查看 [Persistent Volumes的类型](https://kubernetes.io/docs/concepts/storage/persistent-volumes/#types-of-persistent-volumes)。 + + +### Host Path + + +Host paths 是映射到主机上目录的卷。**这种类型应该只用于测试目的或者单节点集群**。如果 pod 在一个新的节点上重建,数据将不会在节点之间移动。如果 pod 被删除并在一个新的节点上重建,数据将会丢失。 + + +##### SELinux + + +在支持 selinux 的系统上,保持它为 enabled/enforcing 是最佳选择。然而,docker 容器使用 "_svirt_sandbox_file_t_" 标签类型挂载 host path,这和默认的 /tmp ("_tmp_t_") 标签类型不兼容。在 mysql 容器试图对 _/var/lib/mysql_ 执行 `chown` 时将导致权限错误。 +因此,要在一个启用 selinx 的系统上使用 host path,你应该预先创建 host path 路径(/tmp/data/)并将它的 selinux 标签类型改变为 "_svirt_sandbox_file_t_",就像下面一样: + +```shell +## on every node: +mkdir -p /tmp/data +chmod a+rwt /tmp/data # match /tmp permissions +chcon -Rt svirt_sandbox_file_t /tmp/data +``` + + +继续进行 host path 配置,在 Kubernetes 中使用 [local-volumes.yaml](https://git.k8s.io/examples/mysql-wordpress-pd/local-volumes.yaml) 创建 persistent volume 对象: + +```shell +export KUBE_REPO=https://raw.githubusercontent.com/kubernetes/examples/master +kubectl create -f $KUBE_REPO/mysql-wordpress-pd/local-volumes.yaml +``` + + + +### GCE Persistent Disk + + +如果在 [Google Compute Engine](http://kubernetes.io/docs/getting-started-guides/gce/) 上运行集群,你可以使用这个存储选项。 + + +创建两个永久磁盘。你需要在和 Kubernetes 集群相同的 [GCE zone](https://cloud.google.com/compute/docs/zones) 中创建这些磁盘。默认的安装脚本将在 `us-central1-b` zone 中创建集群,就像你在 [config-default.sh](https://git.k8s.io/kubernetes/cluster/gce/config-default.sh) 文件中看到的。替换下面的 `` 为合适的 zone。`wordpress-1` 和 `wordpress-2` 的名字必须和 [gce-volumes.yaml](https://git.k8s.io/examples/mysql-wordpress-pd/gce-volumes.yaml) 指定的 `pdName` 字段匹配。 + +```shell +gcloud compute disks create --size=20GB --zone= wordpress-1 +gcloud compute disks create --size=20GB --zone= wordpress-2 +``` + + +在 Kubernetes 为这些磁盘创建 persistent volume 对象: + +```shell +export KUBE_REPO=https://raw.githubusercontent.com/kubernetes/examples/master +kubectl create -f $KUBE_REPO/mysql-wordpress-pd/gce-volumes.yaml +``` + + +## 创建 MySQL 密码 Secret + + +使用一个 [Secret](http://kubernetes.io/docs/user-guide/secrets/) 对象存储 MySQL 密码。首先,创建一个名为 `password.txt` 的文件(和 wordpress 示例文件在相同的文件夹),并且将你的密码保存于其中。请确保密码文件的结尾没有空行。如果你的编辑器添加了一个,开始的 `tr` 命令将会删除这个空行。然后,创建这个 Secret 对象。 + +```shell +tr --delete '\n' .strippedpassword.txt && mv .strippedpassword.txt password.txt +kubectl create secret generic mysql-pass --from-file=password.txt +``` + + +MySQL 和 WordPress pod 配置引用了这个 secret,所以这些 pods 就可以访问它。MySQL pod 会设置数据库密码,并且 WordPress 将使用这个密码来访问数据库。 + + +## 部署 MySQL + +<-- +Now that the persistent disks and secrets are defined, the Kubernetes +pods can be launched. Start MySQL using +[mysql-deployment.yaml](https://git.k8s.io/examples/mysql-wordpress-pd/mysql-deployment.yaml). +--> +现在我们已经定义了永久磁盘和 secrets,可以启动 Kubernetes pods 了。使用 [mysql-deployment.yaml](https://git.k8s.io/examples/mysql-wordpress-pd/mysql-deployment.yaml) 启动 MySQL。 + +```shell +kubectl create -f $KUBE_REPO/mysql-wordpress-pd/mysql-deployment.yaml +``` + + +查看 [mysql-deployment.yaml](https://git.k8s.io/examples/mysql-wordpress-pd/mysql-deployment.yaml),注意到我们定义了一个挂载到 `/var/lib/mysql` 的卷,然后创建了一个请求 20G 卷的 Persistent Volume Claim。这个要求可以被任何符合这个要求的卷满足,在我们的例子中,可以是上面创建的卷中的一个。 + + +再看一下 `env` 一节,我们引用上面创建的 `mysql-pass` secret 来指定密码。Secrets 可以有多组键值对。我们的只有一个键 `password.txt`,它是我们用来创建 secret 的文件名。[MySQL镜像](https://hub.docker.com/_/mysql/) 使用 `MYSQL_ROOT_PASSWORD` 环境变量设置数据库密码。 + + +在很短的时间内,新建的 pod 将达到 `Running` 状态。列出所有的 pods,查看新建的 pod 的状态。 + +```shell +kubectl get pods +``` + +``` +NAME READY STATUS RESTARTS AGE +wordpress-mysql-cqcf4-9q8lo 1/1 Running 0 1m +``` + + +Kubernetes 记录每个 pod 的 stderr 和 stdout。使用 `kubectl log` 查看一个 pod 的日志。从 `get pods` 复制 pod 名字,然后: + +```shell +kubectl logs +``` + +``` +... +2016-02-19 16:58:05 1 [Note] InnoDB: 128 rollback segment(s) are active. +2016-02-19 16:58:05 1 [Note] InnoDB: Waiting for purge to start +2016-02-19 16:58:05 1 [Note] InnoDB: 5.6.29 started; log sequence number 1626007 +2016-02-19 16:58:05 1 [Note] Server hostname (bind-address): '*'; port: 3306 +2016-02-19 16:58:05 1 [Note] IPv6 is available. +2016-02-19 16:58:05 1 [Note] - '::' resolves to '::'; +2016-02-19 16:58:05 1 [Note] Server socket created on IP: '::'. +2016-02-19 16:58:05 1 [Warning] 'proxies_priv' entry '@ root@wordpress-mysql-cqcf4-9q8lo' ignored in --skip-name-resolve mode. +2016-02-19 16:58:05 1 [Note] Event Scheduler: Loaded 0 events +2016-02-19 16:58:05 1 [Note] mysqld: ready for connections. +Version: '5.6.29' socket: '/var/run/mysqld/mysqld.sock' port: 3306 MySQL Community Server (GPL) +``` + + +我们还需要在 [mysql-deployment.yaml](https://git.k8s.io/examples/mysql-wordpress-pd/mysql-deployment.yaml) 中创建一个 service 以允许其它 pods 访问这个 mysql 示例。`wordpress-mysql` 名称被解析为这个 pod 的 IP。 + + +到此为止,我们创建了一个 Deployment,一个 Pod,一个 PVC,一个 Service,一个 Endpoint,两个 PV 和一个 Secret,显示如下: + +```shell +kubectl get deployment,pod,svc,endpoints,pvc -l app=wordpress -o wide && \ + kubectl get secret mysql-pass && \ + kubectl get pv +``` + +```shell +NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE +deploy/wordpress-mysql 1 1 1 1 3m +NAME READY STATUS RESTARTS AGE IP NODE +po/wordpress-mysql-3040864217-40soc 1/1 Running 0 3m 172.17.0.2 127.0.0.1 +NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR +svc/wordpress-mysql None 3306/TCP 3m app=wordpress,tier=mysql +NAME ENDPOINTS AGE +ep/wordpress-mysql 172.17.0.2:3306 3m +NAME STATUS VOLUME CAPACITY ACCESSMODES AGE +pvc/mysql-pv-claim Bound local-pv-2 20Gi RWO 3m +NAME TYPE DATA AGE +mysql-pass Opaque 1 3m +NAME CAPACITY ACCESSMODES STATUS CLAIM REASON AGE +local-pv-1 20Gi RWO Available 3m +local-pv-2 20Gi RWO Bound default/mysql-pv-claim 3m +``` + + +## 部署 WordPress + + +接下来使用 [wordpress-deployment.yaml](https://git.k8s.io/examples/mysql-wordpress-pd/wordpress-deployment.yaml) 部署 WordPress: + +```shell +kubectl create -f $KUBE_REPO/mysql-wordpress-pd/wordpress-deployment.yaml +``` + + +我们在这里使用了许多相同的特性,比如对 persistent storage 的 volume claim 和 password 的 secret。 + + +[WordPress 镜像](https://hub.docker.com/_/wordpress/) 通过环境变量 `WORDPRESS_DB_HOST` 接收数据库的主机名。我们将这个环境变量值设置为我们创建的 MySQL +service 的名字:`wordpress-mysql`。 + + +WordPress service 具有 `type: LoadBalancer` 的设置。这将 wordpress service 置于一个外部 IP 之下。 + + +找到你的 WordPress service 的外部 IP 地址。**为这个 service 分配一个外部 IP 地址可能会耗时一分钟左右,这取决于你的集群环境。** + +```shell +kubectl get services wordpress +``` + +``` +NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE +wordpress 10.0.0.5 1.2.3.4 80/TCP 19h +``` + + +## 访问你的新 WordPress 博客 + + +现在,我们可以访问这个运行的 WordPress 应用。请使用你上面获取的 service 的外部 IP 地址。 + +``` +http:// +``` + + +你应该可以看到熟悉的 WordPress 初始页面。 + +![WordPress init page](WordPress.png "WordPress init page") + + +> 警告:不要在这个页面上留下你的 WordPress 设置。如果被其他用户发现,他们可能在你的实例上创建一个网站并用它来为可能有害的内容提供服务。你应该继续创建用户名密码之后的安装过程,删除你的实例,或者建立一个防火墙来限制接入。 + + +## 删除并重启你的博客 + + +建立你的 WordPress 博客并简单使用一下。然后删除它的 pods 并再次启动它们。由于使用了永久磁盘,你的博客的状态将被保留。 + + +所有的资源都被标记为 `app=wordpress`,你可以使用 label selector 轻松的删除它们: + +```shell +kubectl delete deployment,service -l app=wordpress +kubectl delete secret mysql-pass +``` + + +稍后使用原来的命令重建资源,这将会选择包含原来的完整数据的磁盘。由于我们没有删除 PV Claims,在删除我们的 pods 后,集群中没有任何一个 pod 能够 claim 它们。保留 PV Claims 也保证了重建 Pods 不会导致 PD 切换 Pods。 + + +如果你已经准备好了释放你的永久磁盘及其上的数据,请运行: + +```shell +kubectl delete pvc -l app=wordpress +``` + +And then delete the volume objects themselves: + +```shell +kubectl delete pv local-pv-1 local-pv-2 +``` + + +或者 + +```shell +kubectl delete pv wordpress-pv-1 wordpress-pv-2 +``` + + +## 接下来的步骤 + +* [Introspection and Debugging](http://kubernetes.io/docs/user-guide/introspection-and-debugging/) +* [Jobs](http://kubernetes.io/docs/user-guide/jobs/) may be useful to run SQL queries. +* [Exec](http://kubernetes.io/docs/user-guide/getting-into-containers/) +* [Port Forwarding](http://kubernetes.io/docs/user-guide/connecting-to-applications-port-forward/) + + +[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/examples/mysql-wordpress-pd/README.md?pixel)]() + diff --git a/cn/docs/tutorials/stateful-application/mysql-wordpress-persistent-volume/local-volumes.yaml b/cn/docs/tutorials/stateful-application/mysql-wordpress-persistent-volume/local-volumes.yaml new file mode 100644 index 0000000000000..89641133361b1 --- /dev/null +++ b/cn/docs/tutorials/stateful-application/mysql-wordpress-persistent-volume/local-volumes.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: local-pv-1 + labels: + type: local +spec: + capacity: + storage: 20Gi + accessModes: + - ReadWriteOnce + hostPath: + path: /tmp/data/pv-1 +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: local-pv-2 + labels: + type: local +spec: + capacity: + storage: 20Gi + accessModes: + - ReadWriteOnce + hostPath: + path: /tmp/data/pv-2 diff --git a/cn/docs/tutorials/stateful-application/mysql-wordpress-persistent-volume/mysql-deployment.yaml b/cn/docs/tutorials/stateful-application/mysql-wordpress-persistent-volume/mysql-deployment.yaml new file mode 100644 index 0000000000000..2253600de6f1d --- /dev/null +++ b/cn/docs/tutorials/stateful-application/mysql-wordpress-persistent-volume/mysql-deployment.yaml @@ -0,0 +1,61 @@ +apiVersion: v1 +kind: Service +metadata: + name: wordpress-mysql + labels: + app: wordpress +spec: + ports: + - port: 3306 + selector: + app: wordpress + tier: mysql + clusterIP: None +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: mysql-pv-claim + labels: + app: wordpress +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 20Gi +--- +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: wordpress-mysql + labels: + app: wordpress +spec: + strategy: + type: Recreate + template: + metadata: + labels: + app: wordpress + tier: mysql + spec: + containers: + - image: mysql:5.6 + name: mysql + env: + - name: MYSQL_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: mysql-pass + key: password + ports: + - containerPort: 3306 + name: mysql + volumeMounts: + - name: mysql-persistent-storage + mountPath: /var/lib/mysql + volumes: + - name: mysql-persistent-storage + persistentVolumeClaim: + claimName: mysql-pv-claim diff --git a/cn/docs/tutorials/stateful-application/mysql-wordpress-persistent-volume/wordpress-deployment.yaml b/cn/docs/tutorials/stateful-application/mysql-wordpress-persistent-volume/wordpress-deployment.yaml new file mode 100644 index 0000000000000..e15edc5998eb0 --- /dev/null +++ b/cn/docs/tutorials/stateful-application/mysql-wordpress-persistent-volume/wordpress-deployment.yaml @@ -0,0 +1,63 @@ +apiVersion: v1 +kind: Service +metadata: + name: wordpress + labels: + app: wordpress +spec: + ports: + - port: 80 + selector: + app: wordpress + tier: frontend + type: LoadBalancer +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: wp-pv-claim + labels: + app: wordpress +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 20Gi +--- +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: wordpress + labels: + app: wordpress +spec: + strategy: + type: Recreate + template: + metadata: + labels: + app: wordpress + tier: frontend + spec: + containers: + - image: wordpress:4.8-apache + name: wordpress + env: + - name: WORDPRESS_DB_HOST + value: wordpress-mysql + - name: WORDPRESS_DB_PASSWORD + valueFrom: + secretKeyRef: + name: mysql-pass + key: password + ports: + - containerPort: 80 + name: wordpress + volumeMounts: + - name: wordpress-persistent-storage + mountPath: /var/www/html + volumes: + - name: wordpress-persistent-storage + persistentVolumeClaim: + claimName: wp-pv-claim diff --git a/cn/docs/tutorials/stateful-application/web.yaml b/cn/docs/tutorials/stateful-application/web.yaml new file mode 100644 index 0000000000000..f5f246c47f7e9 --- /dev/null +++ b/cn/docs/tutorials/stateful-application/web.yaml @@ -0,0 +1,45 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: nginx + labels: + app: nginx +spec: + ports: + - port: 80 + name: web + clusterIP: None + selector: + app: nginx +--- +apiVersion: apps/v1beta1 +kind: StatefulSet +metadata: + name: web +spec: + serviceName: "nginx" + replicas: 2 + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: gcr.io/google_containers/nginx-slim:0.8 + ports: + - containerPort: 80 + name: web + volumeMounts: + - name: www + mountPath: /usr/share/nginx/html + volumeClaimTemplates: + - metadata: + name: www + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 1Gi + diff --git a/cn/docs/tutorials/stateful-application/webp.yaml b/cn/docs/tutorials/stateful-application/webp.yaml new file mode 100644 index 0000000000000..0a56f234e0cfc --- /dev/null +++ b/cn/docs/tutorials/stateful-application/webp.yaml @@ -0,0 +1,45 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: nginx + labels: + app: nginx +spec: + ports: + - port: 80 + name: web + clusterIP: None + selector: + app: nginx +--- +apiVersion: apps/v1beta1 +kind: StatefulSet +metadata: + name: web +spec: + serviceName: "nginx" + podManagementPolicy: "Parallel" + replicas: 2 + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: gcr.io/google_containers/nginx-slim:0.8 + ports: + - containerPort: 80 + name: web + volumeMounts: + - name: www + mountPath: /usr/share/nginx/html + volumeClaimTemplates: + - metadata: + name: www + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 1Gi \ No newline at end of file diff --git a/cn/docs/tutorials/stateful-application/zookeeper.md b/cn/docs/tutorials/stateful-application/zookeeper.md new file mode 100644 index 0000000000000..864ee0f7f8378 --- /dev/null +++ b/cn/docs/tutorials/stateful-application/zookeeper.md @@ -0,0 +1,1174 @@ +--- +assignees: +- bprashanth +- enisoc +- erictune +- foxish +- janetkuo +- kow3ns +- smarterclayton + +title: 运行 ZooKeeper, 一个 CP 分布式系统 +--- + +{% capture overview %} + +本教程展示了在 Kubernetes 上使用 [PodDisruptionBudgets](/docs/admin/disruptions/#specifying-a-poddisruptionbudget) 和 [PodAntiAffinity](/docs/user-guide/node-selection/#inter-pod-affinity-and-anti-affinity-beta-feature) 特性运行 [Apache Zookeeper](https://zookeeper.apache.org)。 +{% endcapture %} + +{% capture prerequisites %} + +在开始本教程前,你应该熟悉以下 Kubernetes 概念。 + +* [Pods](/docs/user-guide/pods/single-container/) +* [Cluster DNS](/docs/concepts/services-networking/dns-pod-service/) +* [Headless Services](/docs/concepts/services-networking/service/#headless-services) +* [PersistentVolumes](/docs/concepts/storage/volumes/) +* [PersistentVolume Provisioning](http://releases.k8s.io/{{page.githubbranch}}/examples/persistent-volume-provisioning/) +* [ConfigMaps](/docs/tasks/configure-pod-container/configmap/) +* [StatefulSets](/docs/concepts/abstractions/controllers/statefulsets/) +* [PodDisruptionBudgets](/docs/admin/disruptions/#specifying-a-poddisruptionbudget) +* [PodAntiAffinity](/docs/user-guide/node-selection/#inter-pod-affinity-and-anti-affinity-beta-feature) +* [kubectl CLI](/docs/user-guide/kubectl) + + + +你需要一个至少包含四个节点的集群,每个节点至少 2 CPUs 和 4 GiB 内存。在本教程中你将会 cordon 和 drain 集群的节点。**这意味着集群节点上所有的 Pods 将会被终止并移除。这些节点也会暂时变为不可调度。**在本教程中你应该使用一个独占的集群,或者保证你造成的干扰不会影响其它租户。 + + +本教程假设你的集群配置为动态的提供 PersistentVolumes。如果你的集群没有配置成这样,在开始本教程前,你需要手动准备三个 20 GiB 的卷。 +{% endcapture %} + +{% capture objectives %} + +在学习本教程后,你将熟悉下列内容。 + +* 如何使用 StatefulSet 部署一个 ZooKeeper ensemble。 +* 如何使用 ConfigMaps 一致性配置 ensemble。 +* 如何在 ensemble 中 分布 ZooKeeper 服务的部署。 +* 如何在计划维护中使用 PodDisruptionBudgets 确保服务可用性。 +{% endcapture %} + +{% capture lessoncontent %} + + +### ZooKeeper 基础 + + +[Apache ZooKeeper](https://zookeeper.apache.org/doc/current/) 是一个分布式的开源协调服务,用于分布式系统。ZooKeeper 允许你读取、写入数据和发现数据更新。数据按层次结构组织在文件系统中,并复制到 ensemble(一个 ZooKeeper 服务的集合) 中所有的 ZooKeeper 服务。对数据的所有操作都是原子的和顺序一致的。ZooKeeper 通过 [Zab](https://pdfs.semanticscholar.org/b02c/6b00bd5dbdbd951fddb00b906c82fa80f0b3.pdf) 一致性协议在 ensemble 的所有服务之间复制一个状态机来确保这个特性。 + + +ensemble 使用 Zab 协议选举一个 leader,在选举出 leader 前不能写入数据。一旦选举出了 leader,ensemble 使用 Zab 保证所有写入被复制到一个 quorum,然后这些写入操作才会被确认并对客户端可用。如果没有遵照加权 quorums,一个 quorum 表示包含当前 leader 的 ensemble 的多数成员。例如,如果 ensemble 有3个服务,一个包含 leader 的成员和另一个服务就组成了一个 quorum。如果 ensemble 不能达成一个 quorum,数据将不能被写入。 + + +ZooKeeper 在内存中保存它们的整个状态机,但是每个改变都被写入一个在存储介质上的持久 WAL(Write Ahead Log)。当一个服务故障时,它能够通过回放 WAL 恢复之前的状态。为了防止 WAL 无限制的增长,ZooKeeper 服务会定期的将内存状态快照保存到存储介质。这些快照能够直接加载到内存中,所有在这个快照之前的 WAL 条目都可以被安全的丢弃。 + + +## 创建一个 ZooKeeper Ensemble + + +下面的清单包含一个 [Headless Service](/docs/user-guide/services/#headless-services),一个 [ConfigMap](/docs/tasks/configure-pod-container/configmap/),一个 [PodDisruptionBudget](/docs/admin/disruptions/#specifying-a-poddisruptionbudget) 和 一个 [StatefulSet](/docs/concepts/abstractions/controllers/statefulsets/)。 + +{% include code.html language="yaml" file="zookeeper.yaml" ghlink="/docs/tutorials/stateful-application/zookeeper.yaml" %} + + +打开一个命令行终端,使用 [`kubectl create`](/docs/user-guide/kubectl/{{page.version}}/#create) 创建这个清单。 + +```shell +kubectl create -f https://k8s.io/docs/tutorials/stateful-application/zookeeper.yaml +``` + + +这个操作创建了 `zk-headless` Headless Service、`zk-config` ConfigMap、`zk-budget` PodDisruptionBudget 和 `zk` StatefulSet。 + +```shell +service "zk-headless" created +configmap "zk-config" created +poddisruptionbudget "zk-budget" created +statefulset "zk" created +``` + + +使用 [`kubectl get`](/docs/user-guide/kubectl/{{page.version}}/#get) 查看 StatefulSet 控制器创建的 Pods。 + +```shell +kubectl get pods -w -l app=zk +``` + + +一旦 `zk-2` Pod 变成 Running 和 Ready 状态,使用 `CRTL-C` 结束 kubectl。 + +```shell +NAME READY STATUS RESTARTS AGE +zk-0 0/1 Pending 0 0s +zk-0 0/1 Pending 0 0s +zk-0 0/1 ContainerCreating 0 0s +zk-0 0/1 Running 0 19s +zk-0 1/1 Running 0 40s +zk-1 0/1 Pending 0 0s +zk-1 0/1 Pending 0 0s +zk-1 0/1 ContainerCreating 0 0s +zk-1 0/1 Running 0 18s +zk-1 1/1 Running 0 40s +zk-2 0/1 Pending 0 0s +zk-2 0/1 Pending 0 0s +zk-2 0/1 ContainerCreating 0 0s +zk-2 0/1 Running 0 19s +zk-2 1/1 Running 0 40s +``` + + +StatefulSet 控制器创建了3个 Pods,每个 Pod 包含一个 [ZooKeeper 3.4.9](http://www-us.apache.org/dist/zookeeper/zookeeper-3.4.9/) 服务。 + + +### 促成 Leader 选举 + + +由于在匿名网络中没有用于选举 leader 的终止算法,Zab 要求显式的进行成员关系配置,以执行 leader 选举。Ensemble 中的每个服务都需要具有一个独一无二的标识符,所有的服务均需要知道标识符的全集,并且每个标志都需要和一个网络地址相关联。 + + +使用 [`kubectl exec`](/docs/user-guide/kubectl/{{page.version}}/#exec) 获取 `zk` StatefulSet 中 Pods 的主机名。 + +```shell +for i in 0 1 2; do kubectl exec zk-$i -- hostname; done +``` + + +StatefulSet 控制器基于每个 Pod 的序号索引为它们各自提供一个唯一的主机名。主机名采用 `-` 的形式。由于 `zk` StatefulSet 的 `replicas` 字段设置为3,这个 Set 的控制器将创建3个 Pods,主机名为:`zk-0`、`zk-1` 和 `zk-2`。 + +```shell +zk-0 +zk-1 +zk-2 +``` + + +ZooKeeper ensemble 中的服务使用自然数作为唯一标识符,每个服务的标识符都保存在服务的数据目录中一个名为 `myid` 的文件里。 + + +检查每个服务的 `myid` 文件的内容。 + +```shell +for i in 0 1 2; do echo "myid zk-$i";kubectl exec zk-$i -- cat /var/lib/zookeeper/data/myid; done +``` + + +由于标识符为自然数并且序号索引是非负整数,你可以在序号上加 1 来生成一个标识符。 + +```shell +myid zk-0 +1 +myid zk-1 +2 +myid zk-2 +3 +``` + + +获取 `zk` StatefulSet 中每个 Pod 的 FQDN (Fully Qualified Domain Name,正式域名)。 + +```shell +for i in 0 1 2; do kubectl exec zk-$i -- hostname -f; done +``` + + +`zk-headless` Service 为所有 Pods 创建了一个 domain:`zk-headless.default.svc.cluster.local`。 + +```shell +zk-0.zk-headless.default.svc.cluster.local +zk-1.zk-headless.default.svc.cluster.local +zk-2.zk-headless.default.svc.cluster.local +``` + + +[Kubernetes DNS](/docs/concepts/services-networking/dns-pod-service/) 中的 A 记录将 FQDNs 解析成为 Pods 的 IP 地址。如果 Pods 被调度,这个 A 记录将会使用 Pods 的新 IP 地址更新,但 A 记录的名称不会改变。 + + +ZooKeeper 在一个名为 `zoo.cfg` 的文件中保存它的应用配置。使用 `kubectl exec` 在 `zk-0` Pod 中查看 `zoo.cfg` 文件的内容。 + +``` +kubectl exec zk-0 -- cat /opt/zookeeper/conf/zoo.cfg +``` + + +文件底部为 `server.1`、`server.2` 和 `server.3`,其中的 `1`、`2`和`3`分别对应 ZooKeeper 服务的 `myid` 文件中的标识符。它们被设置为 `zk` StatefulSet 中的 Pods 的 FQDNs。 + +```shell +clientPort=2181 +dataDir=/var/lib/zookeeper/data +dataLogDir=/var/lib/zookeeper/log +tickTime=2000 +initLimit=10 +syncLimit=2000 +maxClientCnxns=60 +minSessionTimeout= 4000 +maxSessionTimeout= 40000 +autopurge.snapRetainCount=3 +autopurge.purgeInteval=0 +server.1=zk-0.zk-headless.default.svc.cluster.local:2888:3888 +server.2=zk-1.zk-headless.default.svc.cluster.local:2888:3888 +server.3=zk-2.zk-headless.default.svc.cluster.local:2888:3888 +``` + + +### 达成一致 + + + 一致性协议要求每个参与者的标识符唯一。在 Zab 协议里任何两个参与者都不应该声明相同的唯一标识符。对于让系统中的进程协商哪些进程已经提交了哪些数据而言,这是必须的。如果有两个 Pods 使用相同的序号启动,这两个 ZooKeeper 服务会将自己识别为相同的服务。 + + +当你创建 `zk` StatefulSet 时,StatefulSet 控制器按照 Pods 的序号索引顺序的创建每个 Pod。在创建下一个 Pod 前会等待每个 Pod 变成 Running 和 Ready 状态。 +```shell +kubectl get pods -w -l app=zk +NAME READY STATUS RESTARTS AGE +zk-0 0/1 Pending 0 0s +zk-0 0/1 Pending 0 0s +zk-0 0/1 ContainerCreating 0 0s +zk-0 0/1 Running 0 19s +zk-0 1/1 Running 0 40s +zk-1 0/1 Pending 0 0s +zk-1 0/1 Pending 0 0s +zk-1 0/1 ContainerCreating 0 0s +zk-1 0/1 Running 0 18s +zk-1 1/1 Running 0 40s +zk-2 0/1 Pending 0 0s +zk-2 0/1 Pending 0 0s +zk-2 0/1 ContainerCreating 0 0s +zk-2 0/1 Running 0 19s +zk-2 1/1 Running 0 40s +``` + + +每个 Pod 的 A 记录仅在 Pod 变成 Ready状态时被录入。因此,ZooKeeper 服务的 FQDNs 只会解析到一个 endpoint,而那个 endpoint 将会是一个唯一的 ZooKeeper 服务,这个服务声明了配置在它的 `myid` 文件中的标识符。 + +```shell +zk-0.zk-headless.default.svc.cluster.local +zk-1.zk-headless.default.svc.cluster.local +zk-2.zk-headless.default.svc.cluster.local +``` + + +这保证了 ZooKeepers 的 `zoo.cfg` 文件中的 `servers` 属性代表了一个正确配置的 ensemble。 + +```shell +server.1=zk-0.zk-headless.default.svc.cluster.local:2888:3888 +server.2=zk-1.zk-headless.default.svc.cluster.local:2888:3888 +server.3=zk-2.zk-headless.default.svc.cluster.local:2888:3888 +``` + + +当服务使用 Zab 协议尝试提交一个值的时候,它们会达成一致并成功提交这个值(如果 leader 选举成功并且至少有两个 Pods 处于 Running 和 Ready状态),或者将会失败(如果没有满足上述条件中的任意一条)。当一个服务承认另一个服务的代写时不会有状态产生。 + + +### Ensemble 健康检查 + + +最基本的健康检查是向一个 ZooKeeper 服务写入一些数据,然后从另一个服务读取这些数据。 + + +使用 `zkCli.sh` 脚本在 `zk-0` Pod 上写入 `world` 到路径 `/hello`。 + +```shell +kubectl exec zk-0 zkCli.sh create /hello world +``` + + +这将会把 `world` 写入 ensemble 的 `/hello` 路径。 + +```shell +WATCHER:: + +WatchedEvent state:SyncConnected type:None path:null +Created /hello +``` + + +从 `zk-1` Pod 获取数据。 + +```shell +kubectl exec zk-1 zkCli.sh get /hello +``` + + +你在 `zk-0` 创建的数据在 ensemble 中所有的服务上都是可用的。 + +```shell +WATCHER:: + +WatchedEvent state:SyncConnected type:None path:null +world +cZxid = 0x100000002 +ctime = Thu Dec 08 15:13:30 UTC 2016 +mZxid = 0x100000002 +mtime = Thu Dec 08 15:13:30 UTC 2016 +pZxid = 0x100000002 +cversion = 0 +dataVersion = 0 +aclVersion = 0 +ephemeralOwner = 0x0 +dataLength = 5 +numChildren = 0 +``` + + +### 准备持久存储 + + +如同在 [ZooKeeper 基础](#zookeeper-basics) 一节所提到的,ZooKeeper 提交所有的条目到一个持久 WAL,并周期性的将内存快照写入存储介质。对于使用一致性协议实现一个复制状态机的应用来说,使用 WALs 提供持久化是一种常用的技术,对于普通的存储应用也是如此。 + + +使用 [`kubectl delete`](/docs/user-guide/kubectl/{{page.version}}/#delete) 删除 `zk` StatefulSet。 + +```shell +kubectl delete statefulset zk +statefulset "zk" deleted +``` + + +观察 StatefulSet 中的 Pods 变为终止状态。 + +```shell +get pods -w -l app=zk +``` + + +当 `zk-0` 完全终止时,使用 `CRTL-C` 结束 kubectl。 + +```shell +zk-2 1/1 Terminating 0 9m +zk-0 1/1 Terminating 0 11m +zk-1 1/1 Terminating 0 10m +zk-2 0/1 Terminating 0 9m +zk-2 0/1 Terminating 0 9m +zk-2 0/1 Terminating 0 9m +zk-1 0/1 Terminating 0 10m +zk-1 0/1 Terminating 0 10m +zk-1 0/1 Terminating 0 10m +zk-0 0/1 Terminating 0 11m +zk-0 0/1 Terminating 0 11m +zk-0 0/1 Terminating 0 11m +``` + +重新应用 `zookeeper.yaml` 中的代码清单。 + +```shell +kubectl apply -f https://k8s.io/docs/tutorials/stateful-application/zookeeper.yaml +``` + + +`zk` StatefulSet 将会被创建。由于清单中的其他 API 对象已经存在,所以它们不会被修改。 + +```shell +statefulset "zk" created +Error from server (AlreadyExists): error when creating "zookeeper.yaml": services "zk-headless" already exists +Error from server (AlreadyExists): error when creating "zookeeper.yaml": configmaps "zk-config" already exists +Error from server (AlreadyExists): error when creating "zookeeper.yaml": poddisruptionbudgets.policy "zk-budget" already exists +``` + + +观察 StatefulSet 控制器重建 StatefulSet 的 Pods。 + +```shell +kubectl get pods -w -l app=zk +``` + + +一旦 `zk-2` Pod 处于 Running 和 Ready 状态,使用 `CRTL-C` 停止 kubectl命令。 + +```shell +NAME READY STATUS RESTARTS AGE +zk-0 0/1 Pending 0 0s +zk-0 0/1 Pending 0 0s +zk-0 0/1 ContainerCreating 0 0s +zk-0 0/1 Running 0 19s +zk-0 1/1 Running 0 40s +zk-1 0/1 Pending 0 0s +zk-1 0/1 Pending 0 0s +zk-1 0/1 ContainerCreating 0 0s +zk-1 0/1 Running 0 18s +zk-1 1/1 Running 0 40s +zk-2 0/1 Pending 0 0s +zk-2 0/1 Pending 0 0s +zk-2 0/1 ContainerCreating 0 0s +zk-2 0/1 Running 0 19s +zk-2 1/1 Running 0 40s +``` + + +从 `zk-2` Pod 中获取你在[健康检查](#sanity-testing-the-ensemble)中输入的值。 + +```shell +kubectl exec zk-2 zkCli.sh get /hello +``` + + +尽管 `zk` StatefulSet 中所有的 Pods 都已经被终止并重建过,ensemble 仍然使用原来的数值提供服务。 + +```shell +WATCHER:: + +WatchedEvent state:SyncConnected type:None path:null +world +cZxid = 0x100000002 +ctime = Thu Dec 08 15:13:30 UTC 2016 +mZxid = 0x100000002 +mtime = Thu Dec 08 15:13:30 UTC 2016 +pZxid = 0x100000002 +cversion = 0 +dataVersion = 0 +aclVersion = 0 +ephemeralOwner = 0x0 +dataLength = 5 +numChildren = 0 +``` + + +`zk` StatefulSet 的 `spec` 中的 `volumeClaimTemplates` 字段标识了将要为每个 Pod 准备的 PersistentVolume。 + +```yaml +volumeClaimTemplates: + - metadata: + name: datadir + annotations: + volume.alpha.kubernetes.io/storage-class: anything + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 20Gi +``` + + + +StatefulSet 控制器为 StatefulSet 中的每个 Pod 生成一个 PersistentVolumeClaim。 + + +获取 StatefulSet 的 PersistentVolumeClaims。 + +```shell +kubectl get pvc -l app=zk +``` + + +当 StatefulSet 重新创建它的 Pods时,Pods 的 PersistentVolumes 会被重新挂载。 + +```shell +NAME STATUS VOLUME CAPACITY ACCESSMODES AGE +datadir-zk-0 Bound pvc-bed742cd-bcb1-11e6-994f-42010a800002 20Gi RWO 1h +datadir-zk-1 Bound pvc-bedd27d2-bcb1-11e6-994f-42010a800002 20Gi RWO 1h +datadir-zk-2 Bound pvc-bee0817e-bcb1-11e6-994f-42010a800002 20Gi RWO 1h +``` + + +StatefulSet 的容器 `template` 中的 `volumeMounts` 一节使得 PersistentVolumes 被挂载到 ZooKeeper 服务的数据目录。 + +```shell +volumeMounts: + - name: datadir + mountPath: /var/lib/zookeeper +``` + +当 `zk` StatefulSet 中的一个 Pod 被(重新)调度时,它总是拥有相同的 PersistentVolume,挂载到 ZooKeeper 服务的数据目录。即使在 Pods 被重新调度时,所有对 ZooKeeper 服务的 WALs 的写入和它们的全部快照都仍然是持久的。 + + +## 确保一致性配置 + + +如同在 [促成 leader 选举](#facilitating-leader-election) 和 [达成一致](#achieving-consensus) 小节中提到的,ZooKeeper ensemble 中的服务需要一致性的配置来选举一个 leader 并形成一个 quorum。它们还需要 Zab 协议的一致性配置来保证这个协议在网络中正确的工作。你可以使用 ConfigMaps 达到目的。 + + +获取 `zk-config` 的 ConfigMap。 + +```shell + kubectl get cm zk-config -o yaml +apiVersion: v1 +data: + client.cnxns: "60" + ensemble: zk-0;zk-1;zk-2 + init: "10" + jvm.heap: 2G + purge.interval: "0" + snap.retain: "3" + sync: "5" + tick: "2000" +``` + + +`zk` StatefulSet 的 `template` 中的 `env` 字段读取 ConfigMap 到环境变量中。这些变量将被注入到容器的运行环境里。 + +```yaml +env: + - name : ZK_ENSEMBLE + valueFrom: + configMapKeyRef: + name: zk-config + key: ensemble + - name : ZK_HEAP_SIZE + valueFrom: + configMapKeyRef: + name: zk-config + key: jvm.heap + - name : ZK_TICK_TIME + valueFrom: + configMapKeyRef: + name: zk-config + key: tick + - name : ZK_INIT_LIMIT + valueFrom: + configMapKeyRef: + name: zk-config + key: init + - name : ZK_SYNC_LIMIT + valueFrom: + configMapKeyRef: + name: zk-config + key: tick + - name : ZK_MAX_CLIENT_CNXNS + valueFrom: + configMapKeyRef: + name: zk-config + key: client.cnxns + - name: ZK_SNAP_RETAIN_COUNT + valueFrom: + configMapKeyRef: + name: zk-config + key: snap.retain + - name: ZK_PURGE_INTERVAL + valueFrom: + configMapKeyRef: + name: zk-config + key: purge.interval +``` + + +在启动 ZooKeeper 服务进程前,容器的入口点调用了一个 bash 脚本:`zkGenConfig.sh`。这个 bash 脚本从提供的环境变量中生成了 ZooKeeper 的配置文件。 + +```yaml + command: + - sh + - -c + - zkGenConfig.sh && zkServer.sh start-foreground +``` + + +检查 `zk` StatefulSet 中所有 Pods 的环境变量。 + +```shell +for i in 0 1 2; do kubectl exec zk-$i env | grep ZK_*;echo""; done +``` + + +所有从 `zk-config` 取得的参数都包含完全相同的值。这将允许 `zkGenConfig.sh` 脚本为 ensemble 中所有的 ZooKeeper 服务创建一致性的配置。 + +```shell +ZK_ENSEMBLE=zk-0;zk-1;zk-2 +ZK_HEAP_SIZE=2G +ZK_TICK_TIME=2000 +ZK_INIT_LIMIT=10 +ZK_SYNC_LIMIT=2000 +ZK_MAX_CLIENT_CNXNS=60 +ZK_SNAP_RETAIN_COUNT=3 +ZK_PURGE_INTERVAL=0 +ZK_CLIENT_PORT=2181 +ZK_SERVER_PORT=2888 +ZK_ELECTION_PORT=3888 +ZK_USER=zookeeper +ZK_DATA_DIR=/var/lib/zookeeper/data +ZK_DATA_LOG_DIR=/var/lib/zookeeper/log +ZK_LOG_DIR=/var/log/zookeeper + +ZK_ENSEMBLE=zk-0;zk-1;zk-2 +ZK_HEAP_SIZE=2G +ZK_TICK_TIME=2000 +ZK_INIT_LIMIT=10 +ZK_SYNC_LIMIT=2000 +ZK_MAX_CLIENT_CNXNS=60 +ZK_SNAP_RETAIN_COUNT=3 +ZK_PURGE_INTERVAL=0 +ZK_CLIENT_PORT=2181 +ZK_SERVER_PORT=2888 +ZK_ELECTION_PORT=3888 +ZK_USER=zookeeper +ZK_DATA_DIR=/var/lib/zookeeper/data +ZK_DATA_LOG_DIR=/var/lib/zookeeper/log +ZK_LOG_DIR=/var/log/zookeeper + +ZK_ENSEMBLE=zk-0;zk-1;zk-2 +ZK_HEAP_SIZE=2G +ZK_TICK_TIME=2000 +ZK_INIT_LIMIT=10 +ZK_SYNC_LIMIT=2000 +ZK_MAX_CLIENT_CNXNS=60 +ZK_SNAP_RETAIN_COUNT=3 +ZK_PURGE_INTERVAL=0 +ZK_CLIENT_PORT=2181 +ZK_SERVER_PORT=2888 +ZK_ELECTION_PORT=3888 +ZK_USER=zookeeper +ZK_DATA_DIR=/var/lib/zookeeper/data +ZK_DATA_LOG_DIR=/var/lib/zookeeper/log +ZK_LOG_DIR=/var/log/zookeeper +``` + + +### 配置日志 + + +`zkGenConfig.sh` 脚本产生的一个文件控制了 ZooKeeper 的日志行为。ZooKeeper 使用了 [Log4j](http://logging.apache.org/log4j/2.x/) 并默认使用基于文件大小和时间的滚动文件追加器作为日志配置。 +从 `zk` StatefulSet 的一个 Pods 中获取日志配置。 + +```shell +kubectl exec zk-0 cat /usr/etc/zookeeper/log4j.properties +``` + + +下面的日志配置会使 ZooKeeper 进程将其所有的日志写入标志输出文件流中。 + +```shell +zookeeper.root.logger=CONSOLE +zookeeper.console.threshold=INFO +log4j.rootLogger=${zookeeper.root.logger} +log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender +log4j.appender.CONSOLE.Threshold=${zookeeper.console.threshold} +log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout +log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} [myid:%X{myid}] - %-5p [%t:%C{1}@%L] - %m%n +``` + + +这是在容器里安全记录日志的最简单的方法。由于应用的日志被写入标准输出,Kubernetes 将会为你处理日志轮转。Kubernetes 还实现了一个智能保存策略,保证写入标准输出和标准错误流的应用日志不会耗尽本地存储媒介。 + + +使用 [`kubectl logs`](/docs/user-guide/kubectl/{{page.version}}/#logs) 从一个 Pod 中取回最后几行日志。 + +```shell +kubectl logs zk-0 --tail 20 +``` + + +使用 `kubectl logs` 或者从 Kubernetes Dashboard 可以查看写入到标准输出和标准错误流中的应用日志。 + +```shell +2016-12-06 19:34:16,236 [myid:1] - INFO [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52740 +2016-12-06 19:34:16,237 [myid:1] - INFO [Thread-1136:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52740 (no session established for client) +2016-12-06 19:34:26,155 [myid:1] - INFO [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52749 +2016-12-06 19:34:26,155 [myid:1] - INFO [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52749 +2016-12-06 19:34:26,156 [myid:1] - INFO [Thread-1137:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52749 (no session established for client) +2016-12-06 19:34:26,222 [myid:1] - INFO [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52750 +2016-12-06 19:34:26,222 [myid:1] - INFO [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52750 +2016-12-06 19:34:26,226 [myid:1] - INFO [Thread-1138:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52750 (no session established for client) +2016-12-06 19:34:36,151 [myid:1] - INFO [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52760 +2016-12-06 19:34:36,152 [myid:1] - INFO [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52760 +2016-12-06 19:34:36,152 [myid:1] - INFO [Thread-1139:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52760 (no session established for client) +2016-12-06 19:34:36,230 [myid:1] - INFO [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52761 +2016-12-06 19:34:36,231 [myid:1] - INFO [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52761 +2016-12-06 19:34:36,231 [myid:1] - INFO [Thread-1140:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52761 (no session established for client) +2016-12-06 19:34:46,149 [myid:1] - INFO [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52767 +2016-12-06 19:34:46,149 [myid:1] - INFO [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52767 +2016-12-06 19:34:46,149 [myid:1] - INFO [Thread-1141:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52767 (no session established for client) +2016-12-06 19:34:46,230 [myid:1] - INFO [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52768 +2016-12-06 19:34:46,230 [myid:1] - INFO [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52768 +2016-12-06 19:34:46,230 [myid:1] - INFO [Thread-1142:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52768 (no session established for client) +``` + + + + +### 配置非特权用户 + + +在容器中允许应用以特权用户运行这条最佳实践是值得商讨的。如果你的组织要求应用以非特权用户运行,你可以使用 [SecurityContext](/docs/tasks/configure-pod-container/security-context/) 控制运行容器入口点的用户。 + + +`zk` StatefulSet 的 Pod 的 `template` 包含了一个 SecurityContext。 + +```yaml +securityContext: + runAsUser: 1000 + fsGroup: 1000 +``` + + +在 Pods 容器内部,UID 1000 对应用户 zookeeper,GID 1000对应用户组 zookeeper。 + + +从 `zk-0` Pod 获取 ZooKeeper 进程信息。 + +```shell +kubectl exec zk-0 -- ps -elf +``` + + +由于 `securityContext` 对象的 `runAsUser` 字段被设置为1000而不是 root,ZooKeeper进程将以 zookeeper 用户运行。 + +```shell +F S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD +4 S zookeep+ 1 0 0 80 0 - 1127 - 20:46 ? 00:00:00 sh -c zkGenConfig.sh && zkServer.sh start-foreground +0 S zookeep+ 27 1 0 80 0 - 1155556 - 20:46 ? 00:00:19 /usr/lib/jvm/java-8-openjdk-amd64/bin/java -Dzookeeper.log.dir=/var/log/zookeeper -Dzookeeper.root.logger=INFO,CONSOLE -cp /usr/bin/../build/classes:/usr/bin/../build/lib/*.jar:/usr/bin/../share/zookeeper/zookeeper-3.4.9.jar:/usr/bin/../share/zookeeper/slf4j-log4j12-1.6.1.jar:/usr/bin/../share/zookeeper/slf4j-api-1.6.1.jar:/usr/bin/../share/zookeeper/netty-3.10.5.Final.jar:/usr/bin/../share/zookeeper/log4j-1.2.16.jar:/usr/bin/../share/zookeeper/jline-0.9.94.jar:/usr/bin/../src/java/lib/*.jar:/usr/bin/../etc/zookeeper: -Xmx2G -Xms2G -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.local.only=false org.apache.zookeeper.server.quorum.QuorumPeerMain /usr/bin/../etc/zookeeper/zoo.cfg +``` + + +默认情况下,当 Pod 的 PersistentVolume 被挂载到 ZooKeeper 服务的数据目录时,它只能被 root 用户访问。这个配置将阻止 ZooKeeper 进程写入它的 WAL 及保存快照。 + + +在 `zk-0` Pod 上获取 ZooKeeper 数据目录的文件权限。 + +```shell +kubectl exec -ti zk-0 -- ls -ld /var/lib/zookeeper/data +``` + + +由于 `securityContext` 对象的 `fsGroup` 字段设置为1000,Pods 的 PersistentVolumes 的所有权属于 zookeeper 用户组,因而 ZooKeeper 进程能够成功的读写数据。 + +```shell +drwxr-sr-x 3 zookeeper zookeeper 4096 Dec 5 20:45 /var/lib/zookeeper/data +``` + + +## 管理 ZooKeeper 进程 + + +[ZooKeeper documentation](https://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_supervision) 文档指出“你将需要一个监管程序用于管理每个 ZooKeeper 服务进程(JVM)”。在分布式系统中,使用一个看门狗(监管程序)来重启故障进程是一种常用的模式。 + + +### 处理进程故障 + + +[Restart Policies](/docs/user-guide/pod-states/#restartpolicy) 控制 Kubernetes 如何处理一个 Pod 中容器入口点的进程故障。对于 StatefulSet 中的 Pods 来说,Always 是唯一合适的 RestartPolicy,这也是默认值。你应该**绝不**覆盖 stateful 应用的默认策略。 + + +检查 `zk-0` Pod 中运行的 ZooKeeper 服务的进程树。 + +```shell +kubectl exec zk-0 -- ps -ef +``` + + +作为容器入口点的命令的 PID 为 1,Zookeeper 进程是入口点的子进程,PID 为23。 + + +``` +UID PID PPID C STIME TTY TIME CMD +zookeep+ 1 0 0 15:03 ? 00:00:00 sh -c zkGenConfig.sh && zkServer.sh start-foreground +zookeep+ 27 1 0 15:03 ? 00:00:03 /usr/lib/jvm/java-8-openjdk-amd64/bin/java -Dzookeeper.log.dir=/var/log/zookeeper -Dzookeeper.root.logger=INFO,CONSOLE -cp /usr/bin/../build/classes:/usr/bin/../build/lib/*.jar:/usr/bin/../share/zookeeper/zookeeper-3.4.9.jar:/usr/bin/../share/zookeeper/slf4j-log4j12-1.6.1.jar:/usr/bin/../share/zookeeper/slf4j-api-1.6.1.jar:/usr/bin/../share/zookeeper/netty-3.10.5.Final.jar:/usr/bin/../share/zookeeper/log4j-1.2.16.jar:/usr/bin/../share/zookeeper/jline-0.9.94.jar:/usr/bin/../src/java/lib/*.jar:/usr/bin/../etc/zookeeper: -Xmx2G -Xms2G -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.local.only=false org.apache.zookeeper.server.quorum.QuorumPeerMain /usr/bin/../etc/zookeeper/zoo.cfg +``` + + +在一个终端观察 `zk` StatefulSet 中的 Pods。 + +```shell +kubectl get pod -w -l app=zk +``` + + +在另一个终端杀掉 Pod `zk-0` 中的 ZooKeeper 进程。 + +```shell + kubectl exec zk-0 -- pkill java +``` + + + +ZooKeeper 进程的终结导致了它父进程的终止。由于容器的 RestartPolicy 是 Always,父进程被重启。 + + +```shell +NAME READY STATUS RESTARTS AGE +zk-0 1/1 Running 0 21m +zk-1 1/1 Running 0 20m +zk-2 1/1 Running 0 19m +NAME READY STATUS RESTARTS AGE +zk-0 0/1 Error 0 29m +zk-0 0/1 Running 1 29m +zk-0 1/1 Running 1 29m +``` + + +如果你的应用使用一个脚本(例如 zkServer.sh)来启动一个实现了应用业务逻辑的进程,这个脚本必须和子进程一起结束。这保证了当实现应用业务逻辑的进程故障时,Kubernetes 会重启这个应用的容器。 + + + + + +你的应用配置为自动重启故障进程,但这对于保持一个分布式系统的健康来说是不够的。许多场景下,一个系统进程可以是活动状态但不响应请求,或者是不健康状态。你应该使用 liveness probes 来通知 Kubernetes 你的应用进程处于不健康状态,需要被重启。 + + + +`zk` StatefulSet 的 Pod 的 `template` 一节指定了一个 liveness probe。 + + +```yaml + livenessProbe: + exec: + command: + - "zkOk.sh" + initialDelaySeconds: 15 + timeoutSeconds: 5 +``` + + +这个探针调用一个简单的 bash 脚本,使用 ZooKeeper 的四字缩写 `ruok` 来测试服务的健康状态。 + + +```bash +ZK_CLIENT_PORT=${ZK_CLIENT_PORT:-2181} +OK=$(echo ruok | nc 127.0.0.1 $ZK_CLIENT_PORT) +if [ "$OK" == "imok" ]; then + exit 0 +else + exit 1 +fi +``` + + +在一个终端窗口观察 `zk` StatefulSet 中的 Pods。 + + +```shell +kubectl get pod -w -l app=zk +``` + + +在另一个窗口中,从 Pod `zk-0` 的文件系统中删除 `zkOk.sh` 脚本。 + + +```shell +kubectl exec zk-0 -- rm /opt/zookeeper/bin/zkOk.sh +``` + + +当 ZooKeeper 进程的 liveness probe 失败时,Kubernetes 将会为你自动重启这个进程,从而保证 ensemble 中不健康状态的进程都被重启。 + + +```shell +kubectl get pod -w -l app=zk +NAME READY STATUS RESTARTS AGE +zk-0 1/1 Running 0 1h +zk-1 1/1 Running 0 1h +zk-2 1/1 Running 0 1h +NAME READY STATUS RESTARTS AGE +zk-0 0/1 Running 0 1h +zk-0 0/1 Running 1 1h +zk-0 1/1 Running 1 1h +``` + + +### 可读性测试 + + + +可读性不同于存活性。如果一个进程是存活的,它是可调度和健康的。如果一个进程是就绪的,它应该能够处理输入。存活性是可读性的必要非充分条件。在许多场景下,特别是初始化和终止过程中,一个进程可以是存活但没有就绪。 + + +如果你指定了一个可读性探针,Kubernetes将保证在可读性检查通过之前,你的应用不会接收到网络流量。 + + +对于一个 ZooKeeper 服务来说,存活性实现了可读性。因此 `zookeeper.yaml` 清单中的可读性探针和存活性探针完全相同。 + + +```yaml + readinessProbe: + exec: + command: + - "zkOk.sh" + initialDelaySeconds: 15 + timeoutSeconds: 5 +``` + + + +虽然存活性探针和可读性探针是相同的,但同时指定它们两者仍然重要。这保证了 ZooKeeper ensemble 中唯一健康的服务能够接收网络流量。 + + + +## 容忍节点故障 + + +ZooKeeper 需要一个服务的 quorum 来成功的提交数据变动。对于一个 3 个服务的 ensemble,必须有两个是健康的写入才能成功。在基于 quorum 的系统里,成员被部署在故障域之间以保证可用性。为了防止由于某台机器断连引起服务中断,最佳实践是防止应用的多个示例在相同的机器上共存。 + + +默认情况下,Kubernetes 可以把 StatefulSet 的 Pods 部署在相同节点上。对于你创建的 3 个服务的 ensemble 来说,如果有两个服务并存于相同的节点上并且该节点发生故障时,你的 ZooKeeper 服务客户端将不能使用服务,至少一个 Pods 被重新调度后才能恢复。 + + +你应该总是提供额外的容量以允许关键系统进程在节点故障时能够被重新调度。如果你这样做了,服务故障就只会持续到 Kubernetes 调度器重新调度 ZooKeeper 服务之前。但是,如果希望你的服务在容忍节点故障时无停服时间,你应该设置 `podAntiAffinity`。 + + +获取 `zk` Stateful Set 中的 Pods 的节点。 + +```shell{% raw %} +for i in 0 1 2; do kubectl get pod zk-$i --template {{.spec.nodeName}}; echo ""; done +``` {% endraw %} + + +`zk` StatefulSe 中所有的 Pods 都被部署在不同的节点。 + +```shell +kubernetes-minion-group-cxpk +kubernetes-minion-group-a5aq +kubernetes-minion-group-2g2d +``` + + +这是因为 `zk` StatefulSet 中的 Pods 指定了 PodAntiAffinity。 + +```yaml + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: "app" + operator: In + values: + - zk-headless + topologyKey: "kubernetes.io/hostname" +``` + + +`requiredDuringSchedulingRequiredDuringExecution` 告诉 Kubernetes 调度器,在以 `topologyKey` 指定的域中,绝对不要把 `zk-headless` 的两个 Pods 调度到相同的节点。`topologyKey` +`kubernetes.io/hostname` 表示这个域是一个单独的节点。使用不同的 rules、labels 和 selectors,你能够通过这种技术把你的 ensemble 在物理、网络和电力故障域之间分布。 + + +## 存活管理 + + +**在本节中你将会 cordon 和 drain 节点。如果你是在一个共享的集群里使用本教程,请保证不会影响到其他租户** + + +上一小节展示了如何在节点之间分散 Pods 以在计划外的节点故障时存活。但是你也需要为计划内维护引起的临时节点故障做准备。 + + +获取你集群中的节点。 + +```shell +kubectl get nodes +``` + + +使用 [`kubectl cordon`](/docs/user-guide/kubectl/{{page.version}}/#cordon) cordon 你的集群中除4个节点以外的所有节点。 + +```shell{% raw %} +kubectl cordon < node name > +```{% endraw %} + + +获取 `zk-budget` PodDisruptionBudget。 + +```shell +kubectl get poddisruptionbudget zk-budget +``` + + +`min-available` 字段指示 Kubernetes 在任何时候,`zk` StatefulSet 至少有两个 Pods 必须是可用的。 + +```yaml +NAME MIN-AVAILABLE ALLOWED-DISRUPTIONS AGE +zk-budget 2 1 1h + +``` + + +在一个终端观察 `zk` StatefulSet 中的 Pods。 + +```shell +kubectl get pods -w -l app=zk +``` + + +在另一个终端获取 Pods 当前调度的节点。 + +```shell{% raw %} +for i in 0 1 2; do kubectl get pod zk-$i --template {{.spec.nodeName}}; echo ""; done +kubernetes-minion-group-pb41 +kubernetes-minion-group-ixsl +kubernetes-minion-group-i4c4 +{% endraw %} +``` + +使用 [`kubectl drain`](/docs/user-guide/kubectl/{{page.version}}/#drain) 来 cordon 和 drain `zk-0` Pod 调度的节点。 + +```shell {% raw %} +kubectl drain $(kubectl get pod zk-0 --template {{.spec.nodeName}}) --ignore-daemonsets --force --delete-local-data +node "kubernetes-minion-group-pb41" cordoned +WARNING: Deleting pods not managed by ReplicationController, ReplicaSet, Job, or DaemonSet: fluentd-cloud-logging-kubernetes-minion-group-pb41, kube-proxy-kubernetes-minion-group-pb41; Ignoring DaemonSet-managed pods: node-problem-detector-v0.1-o5elz +pod "zk-0" deleted +node "kubernetes-minion-group-pb41" drained +{% endraw %} +``` + + +由于你的集群中有4个节点, `kubectl drain` 执行成功,`zk-0 被调度到其它节点。 + +``` +NAME READY STATUS RESTARTS AGE +zk-0 1/1 Running 2 1h +zk-1 1/1 Running 0 1h +zk-2 1/1 Running 0 1h +NAME READY STATUS RESTARTS AGE +zk-0 1/1 Terminating 2 2h +zk-0 0/1 Terminating 2 2h +zk-0 0/1 Terminating 2 2h +zk-0 0/1 Terminating 2 2h +zk-0 0/1 Pending 0 0s +zk-0 0/1 Pending 0 0s +zk-0 0/1 ContainerCreating 0 0s +zk-0 0/1 Running 0 51s +zk-0 1/1 Running 0 1m +``` + + +在第一个终端持续观察 StatefulSet 的 Pods并 drain `zk-1` 调度的节点。 + +```shell{% raw %} +kubectl drain $(kubectl get pod zk-1 --template {{.spec.nodeName}}) --ignore-daemonsets --force --delete-local-data "kubernetes-minion-group-ixsl" cordoned +WARNING: Deleting pods not managed by ReplicationController, ReplicaSet, Job, or DaemonSet: fluentd-cloud-logging-kubernetes-minion-group-ixsl, kube-proxy-kubernetes-minion-group-ixsl; Ignoring DaemonSet-managed pods: node-problem-detector-v0.1-voc74 +pod "zk-1" deleted +node "kubernetes-minion-group-ixsl" drained +{% endraw %} +``` + + +`zk-1` Pod 不能被调度。由于 `zk` StatefulSet 包含了一个防止 Pods 共存的 PodAntiAffinity 规则,而且只有两个节点可用于调度,这个 Pod 将保持在 Pending 状态。 + +```shell +kubectl get pods -w -l app=zk +NAME READY STATUS RESTARTS AGE +zk-0 1/1 Running 2 1h +zk-1 1/1 Running 0 1h +zk-2 1/1 Running 0 1h +NAME READY STATUS RESTARTS AGE +zk-0 1/1 Terminating 2 2h +zk-0 0/1 Terminating 2 2h +zk-0 0/1 Terminating 2 2h +zk-0 0/1 Terminating 2 2h +zk-0 0/1 Pending 0 0s +zk-0 0/1 Pending 0 0s +zk-0 0/1 ContainerCreating 0 0s +zk-0 0/1 Running 0 51s +zk-0 1/1 Running 0 1m +zk-1 1/1 Terminating 0 2h +zk-1 0/1 Terminating 0 2h +zk-1 0/1 Terminating 0 2h +zk-1 0/1 Terminating 0 2h +zk-1 0/1 Pending 0 0s +zk-1 0/1 Pending 0 0s +``` + + +继续观察 stateful set 的 Pods 并 drain `zk-2` 调度的节点。 + +```shell{% raw %} +kubectl drain $(kubectl get pod zk-2 --template {{.spec.nodeName}}) --ignore-daemonsets --force --delete-local-data +node "kubernetes-minion-group-i4c4" cordoned +WARNING: Deleting pods not managed by ReplicationController, ReplicaSet, Job, or DaemonSet: fluentd-cloud-logging-kubernetes-minion-group-i4c4, kube-proxy-kubernetes-minion-group-i4c4; Ignoring DaemonSet-managed pods: node-problem-detector-v0.1-dyrog +WARNING: Ignoring DaemonSet-managed pods: node-problem-detector-v0.1-dyrog; Deleting pods not managed by ReplicationController, ReplicaSet, Job, or DaemonSet: fluentd-cloud-logging-kubernetes-minion-group-i4c4, kube-proxy-kubernetes-minion-group-i4c4 +There are pending pods when an error occurred: Cannot evict pod as it would violate the pod's disruption budget. +pod/zk-2 +{% endraw %} +``` + + +使用 `CRTL-C` 终止 kubectl。 + + +你不能 drain 第三个节点,因为删除 `zk-2` 将和 `zk-budget` 冲突。然而这个节点仍然保持 cordoned。 + + +使用 `zkCli.sh` 从 `zk-0` 取回你的健康检查中输入的数值。 + +```shell +kubectl exec zk-0 zkCli.sh get /hello +``` + + +由于遵守了 PodDisruptionBudget,服务仍然可用。 + +``` +WatchedEvent state:SyncConnected type:None path:null +world +cZxid = 0x200000002 +ctime = Wed Dec 07 00:08:59 UTC 2016 +mZxid = 0x200000002 +mtime = Wed Dec 07 00:08:59 UTC 2016 +pZxid = 0x200000002 +cversion = 0 +dataVersion = 0 +aclVersion = 0 +ephemeralOwner = 0x0 +dataLength = 5 +numChildren = 0 +``` + + +使用 [`kubectl uncordon`](/docs/user-guide/kubectl/{{page.version}}/#uncordon) 来取消对第一个节点的隔离。 + +```shell +kubectl uncordon kubernetes-minion-group-pb41 +node "kubernetes-minion-group-pb41" uncordoned +``` + + +`zk-1` 被重新调度到了这个节点。等待 `zk-1` 变为 Running 和 Ready 状态。 + +```shell +kubectl get pods -w -l app=zk +NAME READY STATUS RESTARTS AGE +zk-0 1/1 Running 2 1h +zk-1 1/1 Running 0 1h +zk-2 1/1 Running 0 1h +NAME READY STATUS RESTARTS AGE +zk-0 1/1 Terminating 2 2h +zk-0 0/1 Terminating 2 2h +zk-0 0/1 Terminating 2 2h +zk-0 0/1 Terminating 2 2h +zk-0 0/1 Pending 0 0s +zk-0 0/1 Pending 0 0s +zk-0 0/1 ContainerCreating 0 0s +zk-0 0/1 Running 0 51s +zk-0 1/1 Running 0 1m +zk-1 1/1 Terminating 0 2h +zk-1 0/1 Terminating 0 2h +zk-1 0/1 Terminating 0 2h +zk-1 0/1 Terminating 0 2h +zk-1 0/1 Pending 0 0s +zk-1 0/1 Pending 0 0s +zk-1 0/1 Pending 0 12m +zk-1 0/1 ContainerCreating 0 12m +zk-1 0/1 Running 0 13m +zk-1 1/1 Running 0 13m +``` + + +尝试 drain `zk-2` 调度的节点。 + +```shell{% raw %} +kubectl drain $(kubectl get pod zk-2 --template {{.spec.nodeName}}) --ignore-daemonsets --force --delete-local-data +node "kubernetes-minion-group-i4c4" already cordoned +WARNING: Deleting pods not managed by ReplicationController, ReplicaSet, Job, or DaemonSet: fluentd-cloud-logging-kubernetes-minion-group-i4c4, kube-proxy-kubernetes-minion-group-i4c4; Ignoring DaemonSet-managed pods: node-problem-detector-v0.1-dyrog +pod "heapster-v1.2.0-2604621511-wht1r" deleted +pod "zk-2" deleted +node "kubernetes-minion-group-i4c4" drained +{% endraw %} +``` + + +这次 `kubectl drain` 执行成功。 + + +Uncordon 第二个节点以允许 `zk-2` 被重新调度。 + +```shell +kubectl uncordon kubernetes-minion-group-ixsl +node "kubernetes-minion-group-ixsl" uncordoned +``` + + +你可以同时使用 `kubectl drain` 和 PodDisruptionBudgets 来保证你的服务在维护过程中仍然可用。如果使用 drain 来隔离节点并在此之前删除 pods 使节点进入离线维护状态,如果服务表达了 disruption budget,这个 budget 将被遵守。你应该总是为关键服务分配额外容量,这样它们的 Pods 就能够迅速的重新调度。 + +{% endcapture %} + +{% capture cleanup %} + +* 使用 `kubectl uncordon` 解除你集群中所有节点的隔离。 +* 你需要删除在本教程中使用的 PersistentVolumes 的持久存储媒介。请遵循必须的步骤,基于你的环境、存储配置和准备方法,保证回收所有的存储。 +{% endcapture %} +{% include templates/tutorial.md %} diff --git a/cn/docs/tutorials/stateful-application/zookeeper.yaml b/cn/docs/tutorials/stateful-application/zookeeper.yaml new file mode 100644 index 0000000000000..6fbce978256e5 --- /dev/null +++ b/cn/docs/tutorials/stateful-application/zookeeper.yaml @@ -0,0 +1,157 @@ +apiVersion: v1 +kind: Service +metadata: + name: zk-headless + labels: + app: zk-headless +spec: + ports: + - port: 2888 + name: server + - port: 3888 + name: leader-election + clusterIP: None + selector: + app: zk +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: zk-config +data: + ensemble: "zk-0;zk-1;zk-2" + jvm.heap: "2G" + tick: "2000" + init: "10" + sync: "5" + client.cnxns: "60" + snap.retain: "3" + purge.interval: "1" +--- +apiVersion: policy/v1beta1 +kind: PodDisruptionBudget +metadata: + name: zk-budget +spec: + selector: + matchLabels: + app: zk + minAvailable: 2 +--- +apiVersion: apps/v1beta1 +kind: StatefulSet +metadata: + name: zk +spec: + serviceName: zk-headless + replicas: 3 + template: + metadata: + labels: + app: zk + annotations: + pod.alpha.kubernetes.io/initialized: "true" + + spec: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: "app" + operator: In + values: + - zk-headless + topologyKey: "kubernetes.io/hostname" + containers: + - name: k8szk + imagePullPolicy: Always + image: gcr.io/google_samples/k8szk:v1 + resources: + requests: + memory: "4Gi" + cpu: "1" + ports: + - containerPort: 2181 + name: client + - containerPort: 2888 + name: server + - containerPort: 3888 + name: leader-election + env: + - name : ZK_ENSEMBLE + valueFrom: + configMapKeyRef: + name: zk-config + key: ensemble + - name : ZK_HEAP_SIZE + valueFrom: + configMapKeyRef: + name: zk-config + key: jvm.heap + - name : ZK_TICK_TIME + valueFrom: + configMapKeyRef: + name: zk-config + key: tick + - name : ZK_INIT_LIMIT + valueFrom: + configMapKeyRef: + name: zk-config + key: init + - name : ZK_SYNC_LIMIT + valueFrom: + configMapKeyRef: + name: zk-config + key: tick + - name : ZK_MAX_CLIENT_CNXNS + valueFrom: + configMapKeyRef: + name: zk-config + key: client.cnxns + - name: ZK_SNAP_RETAIN_COUNT + valueFrom: + configMapKeyRef: + name: zk-config + key: snap.retain + - name: ZK_PURGE_INTERVAL + valueFrom: + configMapKeyRef: + name: zk-config + key: purge.interval + - name: ZK_CLIENT_PORT + value: "2181" + - name: ZK_SERVER_PORT + value: "2888" + - name: ZK_ELECTION_PORT + value: "3888" + command: + - sh + - -c + - zkGenConfig.sh && zkServer.sh start-foreground + readinessProbe: + exec: + command: + - "zkOk.sh" + initialDelaySeconds: 15 + timeoutSeconds: 5 + livenessProbe: + exec: + command: + - "zkOk.sh" + initialDelaySeconds: 15 + timeoutSeconds: 5 + volumeMounts: + - name: datadir + mountPath: /var/lib/zookeeper + securityContext: + runAsUser: 1000 + fsGroup: 1000 + volumeClaimTemplates: + - metadata: + name: datadir + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 20Gi