编者的话】本文作者介绍了Rook项目并分享了他们学到的一些生产环境的使用经验和见解。
就在今年的五月份,Rook官方宣告Rook 1.0.0主版本发布了,“可用于Kubernetes,已经生产就绪的云原生存储”。大约在一年前,这个解决方案首次出现在我们的视野里,但是距离我们实际使用它又过去了一段时间。最后,我们很高兴在这里分享我们学到的一些经验教训。
简而言之,Rook就是一组Kubernetes的Operator,它可以完全控制多种数据存储解决方案(例如Ceph、EdgeFS、Minio、Cassandra)的部署,管理以及自动恢复。
到目前为止,rook-ceph-operator仍然是最先进(也是唯一一个稳定的)解决方案。
注意:Rook 1.0.0这次发布了一些和Ceph有关的显著特性,包括对Ceph Nautilus的支持,以及启动NFS守护进程用于导出CephFS卷或者RGW桶的CephNFS CRD。此外,新版本还加入了期待已久的对EdgeFS的beta级支持。
在本文中,我们将会:
- 解答在Kubernetes集群里使用Rook来部署Ceph有什么好处的问题;
- 分享我们在生产环境使用Rook的一些经验和见解;
- 解释我们为什么对Rook说"Yes"的原因并分享我们的未来规划。
但是现在,让我们先从一些通俗的概念和理论讲起吧。
“Rook是我的强项!”(来自一位不知名的棋手)
使用Rook的其中一个主要好处在于它是通过原生的Kubernetes机制和数据存储交互。这就意味着你不再需要通过命令行手动配置Ceph。
- 你想要在一个集群里部署CephFS吗?只需要创建一个YAML文件就行了!
- 什么?你还想要部署一个支持S3 API的对象存储?行,另外再建一个YAML文件就行!
Rook具备了一个典型的Kubernetes Operator的所有功能。与它的交互依赖于自定义资源定义(CRD)来描述我们需要的Ceph实例的一些属性(除非另有明确说明,否则在本文的其余部分中,我们将会隐去Ceph的字眼,因为它是Rook目前唯一稳定的存储解决方案)。通过使用给定的参数,一个Operator将会自动执行设置所需的命令。
我们不妨看一个创建对象存储的具体示例,以CephObjectStoreUser
为例:
apiVersion: ceph.rook.io/v1 kind: CephObjectStore metadata: name: {{ .Values.s3.storeName }} namespace: kube-rook spec: metadataPool: failureDomain: host replicated: size: 3 dataPool: failureDomain: host erasureCoded: dataChunks: 2 codingChunks: 1 gateway: type: s3 sslCertificateRef: port: 80 securePort: instances: 1 allNodes: false --- apiVersion: ceph.rook.io/v1 kind: CephObjectStoreUser metadata: name: {{ .Values.s3.username }} namespace: kube-rook spec: store: {{ .Values.s3.storeName }} displayName: {{ .Values.s3.username }}
上述清单里列出的参数都是一些常见配置,无需进一步说明。不过这里要重点关注一下参数值指定为模板变量的部分。
一般流程的结构如下:我们会通过一个YAML文件请求资源,然后一个operator会去执行所有必要的命令并返回一个“不那么真实”的secret,我们可以据此进行接下来的工作(参见下文)。然后,基于上述提供的变量,Rook会帮助用户生成要执行的命令以及secret的名称。
什么命令呢?当Rook Operator为对象存储创建一个用户时,它会在Pod里执行这条命令:
radosgw-admin user create --uid={{ .Values.s3.username }} --display-name={{ .Values.s3.username }} --rgw-realm={{ .Values.s3.storeName }} --rgw-zonegroup={{ .Values.s3.storeName }}
它会生成如下结构的JSON数据:
{ "user_id": "{{ .Values.s3.username }}", "display_name": "{{ .Values.s3.username }}", "keys": [ { "user": "{{ .Values.s3.username }}", "access_key": "NRWGT19TWMYOB1YDBV1Y", "secret_key": "gr1VEGIV7rxcP3xvXDFCo4UDwwl2YoNrmtRlIAty" } ], ... }
这里的keys
在将来用于通过S3 API为应用程序提供访问对象存储的权限。Rook Operator将它们照单全收,然后以rook-ceph-object-user-{{ .Values.s3.crdName }}-{{ .Values.s3.username }}
的格式作为secret存储到它的namespace里。
为了使用保存到secret里的数据,所有你要做的就是通过环境变量把它们传入到容器里。这里有一个Job模板的示例,它会自动给每个用户环境创建对应的bucket:
{{- range $bucket := $.Values.s3.bucketNames }} apiVersion: batch/v1 kind: Job metadata: name: create-{{ $bucket }}-bucket-job namespace: kube-rook annotations: "helm.sh/hook": post-install, post-upgrade "helm.sh/hook-weight": "2" spec: template: metadata: name: create-{{ $bucket }}-bucket-job spec: restartPolicy: Never initContainers: - name: waitdns image: alpine:3.6 command: ["/bin/sh", "-c"] args: - "while ! getent ahostsv4 rook-ceph-rgw-{{ $.Values.s3.storeName }}; do sleep 1; done" - name: config image: rook/toolbox:v0.7.1 command: ["/bin/sh", "-c"] args: - >- s3cmd --configure --access_key=$(ACCESS-KEY) --secret_key=$(SECRET-KEY) --no-ssl --dump-config --host=rook-ceph-rgw-{{ $.Values.s3.storeName }} --host-bucket=rook-ceph-rgw-{{ $.Values.s3.storeName }} | tee /config/.s3cfg volumeMounts: - name: config mountPath: /config env: - name: ACCESS-KEY valueFrom: secretKeyRef: name: rook-ceph-object-user-{{ $.Values.s3.storeName }}-{{ $.Values.s3.username }} key: AccessKey - name: SECRET-KEY valueFrom: secretKeyRef: name: rook-ceph-object-user-{{ $.Values.s3.storeName }}-{{ $.Values.s3.username }} key: SecretKey containers: - name: create-bucket image: rook/toolbox:v0.7.1 command: ["s3cmd", "mb", "s3://{{ $bucket }}"] ports: - name: s3-no-sll containerPort: 80 volumeMounts: - name: config mountPath: /root volumes: - name: config emptyDir: {} --- {{- end }}
这个Job的所有操作仅在Kubernetes内部执行。YAML文件里描述的数据结构已经被添加到了Git仓库里以便复用。对于DevOps工程师和整体的CI/CD流程而言,这个功能真是棒极了。目前,Rook团队已经计划使用Bucket置备库,这将会使你和S3 bucket的交互更加方便。
通过Rook处理RADOS
传统的Ceph + RBD组合在为Pod挂载卷时施加了特定的限制。
换句话说,namespace必须包含访问Ceph的secret这样有状态应用才可以正常工作。如果你有,比方说,namespace里有2-3个环境的话,这很容易做到:你可以手动复制secret。但是如果针对每个功能都创建一个具有自己的namespace的单独环境的话该怎么办呢?我们已经通过shell-operator解决了这个问题,它会自动将secret复制到新的namespace里(在这篇文章中你可以找到一个这样的hook示例)。
#!/bin/bash if [[ $1 == "--config" ]]; then cat <<EOF {"onKubernetesEvent":[ {"name": "OnNewNamespace", "kind": "namespace", "event": ["add"] } ]} EOF else NAMESPACE=$(kubectl get namespace -o json | jq '.items | max_by( .metadata.creationTimestamp ) | .metadata.name') kubectl -n ${CEPH_SECRET_NAMESPACE} get secret ${CEPH_SECRET_NAME} -o json | jq ".metadata.namespace=\"${NAMESPACE}\"" | kubectl apply -f - fi
但是,如果使用Rook的话就不存在这个问题了。卷的挂载流程是基于Flexvolume或者CSI(目前还是beta阶段)驱动,不需要任何secret。
Rook通过一些手段自动化地自行解决了这些问题,因此我们倾向于将它用于我们的新项目。
上手Rook
让我们通过安装Rook和Ceph来结束实战部分,这样你便可以实际上手体验。为了简化安装过程,开发人员提供了一个Helm包。让我们下载它:
$ helm fetch rook-master/rook-ceph --untar --version 1.0.0
你可以在rook-ceph/values.yaml
文件里找到许多不同的设置。这里最重要的是为discover和agent指定toleration。这里我们不打算过多涉及Kubernetes里的taints和tolerations的细节,你只需要知道我们不想让应用的Pod调度到带有数据存储盘的节点上即可。理由也是显而易见的:这样做的话,我们可以避免Rook的Agent影响应用程序本身。
如今是时候在你喜欢的文本编辑器里打开rook-ceph/values.yaml
然后将下列部分追加到文件的末尾:
discover: toleration: NoExecute tolerationKey: node-role/storage agent: toleration: NoExecute tolerationKey: node-role/storage mountSecurityMode: Any
我们还为每个保留用于数据存储的节点打上了相应的污点(taint):
$ kubectl taint node ${NODE_NAME} node-role/storage="":NoExecute
然后通过下列命令安装Helm chart:
$ helm install --namespace ${ROOK_NAMESPACE} ./rook-ceph
现在我们可以去创建集群,然后指定OSD的路径:
apiVersion: ceph.rook.io/v1 kind: CephCluster metadata: clusterName: "ceph" finalizers: - cephcluster.ceph.rook.io generation: 1 name: rook-ceph spec: cephVersion: image: ceph/ceph:v13 dashboard: enabled: true dataDirHostPath: /var/lib/rook/osd mon: allowMultiplePerNode: false count: 3 network: hostNetwork: true rbdMirroring: workers: 1 placement: all: tolerations: - key: node-role/storage operator: Exists storage: useAllNodes: false useAllDevices: false config: osdsPerDevice: "1" storeType: bluestore nodes: - name: ceph01 deviceFilter: ^sd[b-i] - name: ceph02 deviceFilter: ^sd[b-i] - name: ceph03 deviceFilter: ^sd[b-i]
Ceph的状态应当是HEALTH_OK
:
$ kubectl -n ${ROOK_NAMESPACE} exec $(kubectl -n ${ROOK_NAMESPACE} get pod -l app=rook-ceph-operator -o name -o jsonpath='{.items[0].metadata.name}') -- ceph -s
我们还要确保应用程序的Pod不会调度到那些为Ceph保留的节点上去:
$ kubectl -n ${APPLICATION_NAMESPACE} get pods -o custom-columns=NAME:.metadata.name,NODE:.spec.nodeName
现在你可以定制其他额外的组件了。相关的信息可以在文档里找到。为了能够让我们在集群里的后续操作变得更加顺滑,我们强烈推荐开启Dashboard并且部署toolbox。
人无完人:Rook的不足之处是什么?
如你所见,Rook的开发进展顺利。但是存在一些问题,使得一些Ceph的手动配置仍然无法避免。
- 目前没有Rook驱动可以导出展示已挂载的区块的使用情况的指标数据,因此我们无法监控它们的状态(Rook v1.0.3在Flexvolume卷上加入了这项功能)。
- Flexvolume和CSI(与RBD不同)无法调整卷的大小,因此Rook缺乏这项有用的(有时甚至非常关键)工具。(在写完这篇文章后不久,业内已经出现了一些实现方案但是也仅仅只是针对Flexvolume;不过也有讨论到CSI)
- Rook仍然不如Ceph灵活。举个例子,要将CephFS元数据存储到SSD上,然后将相关数据存储到HDD时,你必须得在CRUSH映射里手动定义每组设备。
- 尽管rook-ceph-operator被视为已经稳定了,但是从Ceph 13升级到14版本的过程中仍然会遇到一些问题。
小结
“今天Rook是通过小兵们来抵御外面的世界,但是终有一天它会在游戏里扮演一个至关重要的角色!”(这句话简直像是为本文量身定制的。)
我们无疑非常喜欢Rook项目,尽管有许多利弊,但是我们相信它绝对值得你的关注。
我们未来的计划归结为将rook-ceph变成我们addon-operator的一个模块。这样一来,我们可以很轻松方便地在我们维护的大量Kubernetes集群里使用它。
本文最初是由Flant的工程师Maksim Nabokikh用俄语撰写和发布的。我们工程师的更多技术资料即将推出-请不要忘记关注我们的博客!
原文链接:To Rook, or not to Rook, that’s Kubernetes(译者:吴佳兴)