Rook用还是不用,这就是Kubernetes


【编者的话】本文作者介绍了Rook项目并分享了他们学到的一些生产环境的使用经验和见解。

就在今年的五月份,Rook官方宣告Rook 1.0.0主版本发布了,“可用于Kubernetes,已经生产就绪的云原生存储”。大约在一年前,这个解决方案首次出现在我们的视野里,但是距离我们实际使用它又过去了一段时间。最后,我们很高兴在这里分享我们学到的一些经验教训。
0_MrxXNO55N-rSYLEb.png

简而言之,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是我的强项!”(来自一位不知名的棋手)

0_DGLCeT9iunOYK6Oh.png

使用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(译者:吴佳兴)

0 个评论

要回复文章请先登录注册