company_banner

Rook или не Rook — вот в чём вопрос



    В начале этого месяца, 3 мая, был анонсирован крупный релиз «системы управления для распределённых хранилищ данных в Kubernetes» — Rook 1.0.0. Более года назад мы уже публиковали общий обзор Rook. Тогда же нас просили рассказать об опыте его использования на практике — и вот, как раз к столь значимой вехе в истории проекта, мы рады поделиться накопленными впечатлениями.

    Если кратко, Rook представляет собой набор операторов для Kubernetes, которые полностью берут под контроль развертывание, управление, автоматическое восстановление таких решений для хранения данных, как Ceph, EdgeFS, Minio, Cassandra, CockroachDB.

    На данный момент самым развитым (и единственным в стабильной стадии) решением является rook-ceph-operator.

    Примечание: среди значимых изменений в релизе Rook 1.0.0, связанных с Ceph, можно отметить поддержку Сeph Nautilus и возможность использовать NFS для CephFS- или RGW-бакетов. Из прочих выделяется «созревание» поддержки EdgeFS до уровня беты.

    Итак, в этой статье мы:

    • ответим на вопрос, какие плюсы видим в использовании Rook для развертывания Ceph в кластере Kubernetes;
    • поделимся опытом и впечатлениями от использования Rook в production;
    • расскажем, почему мы говорим Rook’у «Да!», и о своих планах на него.

    Начнём с общих концепций и теории.

    «У меня преимущество в одну Ладью!» (неизвестный шахматист)




    Одним из главных преимуществ Rook является то, что взаимодействие с хранилищами данных ведется через механизмы Kubernetes. Это означает, что больше не нужно копировать команды для настройки Ceph с листочка в консоль.

    — Хочешь развернуть в кластере CephFS? Просто напиши YAML-файл!
    ­— Что? Хочешь развернуть ещё и object store с S3 API? Просто напиши второй YAML-файл!


    Rook создан по всем правилам типичного оператора. Взаимодействие с ним происходит при помощи CRD (Custom Resource Definitions), в которых мы описываем необходимые нам характеристики сущностей Ceph (поскольку это единственная стабильная реализация, по умолчанию в статье будет идти речь именно про Ceph, если явно не указано иное). Согласно заданным параметрам, оператор автоматически выполнит необходимые для настройки команды.

    Конкретику давайте рассмотрим на примере создания Object Store, а точнее — CephObjectStoreUser.

    apiVersion: ceph.rook.io/v1
    kind: CephObjectStore
    metadata:
      name: {{ .Values.s3.crdName }}
      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.crdName }}
      namespace: kube-rook
    spec:
      store: {{ .Values.s3.crdName }}
      displayName: {{ .Values.s3.username }}

    Указанные в листинге параметры достаточно стандартны и вряд ли нуждаются в комментариях, однако стоит обратить особое внимание на те, что выделены в переменные шаблонов.

    Общая схема работы сводится к тому, что через YAML-файл мы «заказываем» ресурсы, для чего оператор выполняет нужные команды и возвращает нам «не самый настоящий» секрет, с которым мы можем дальше работать (см. ниже). А из переменных, что указаны выше, будет составлена команда и имя секрета.

    Что же это за команда? При создании пользователя для объектного хранилища Rook-оператор внутри pod’а выполнит следующее:

    radosgw-admin user create --uid="rook-user" --display-name="{{ .Values.s3.username }}"

    Результатом выполнения этой команды станет JSON-структура:

    {
        "user_id": "rook-user",
        "display_name": "{{ .Values.s3.username }}",
        "keys": [
            {
               "user": "rook-user",
               "access_key": "NRWGT19TWMYOB1YDBV1Y",
               "secret_key": "gr1VEGIV7rxcP3xvXDFCo4UDwwl2YoNrmtRlIAty"
            }
        ],
        ...
    }

    Keys — то, что потребуется в будущем приложениям для доступа к объектному хранилищу через S3 API. Rook-оператор любезно выбирает их и складывает в свой namespace в виде секрета с именем rook-ceph-object-user-{{ $.Values.s3.crdName }}-{{ $.Values.s3.username }}.

    Чтобы использовать данные из этого секрета, достаточно добавить их в контейнер в качестве переменных окружения. Как пример приведу шаблон для Job, в котором мы автоматически создаем bucket’ы для каждого пользовательского окружения:

    {{- range $bucket := $.Values.s3.bucketNames }}
    apiVersion: batch/v1
    kind: Job
    metadata:
      name: create-{{ $bucket }}-bucket-job
      annotations:
        "helm.sh/hook": post-install
        "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", "while ! getent ahostsv4 rook-ceph-rgw-{{ $.Values.s3.crdName }}; do sleep 1; done" ]
          - name: config
            image: rook/ceph:v1.0.0
            command: ["/bin/sh", "-c"]
            args: ["s3cmd --configure --access_key=$(ACCESS-KEY) --secret_key=$(SECRET-KEY) -s --no-ssl --dump-config | tee /config/.s3cfg"]
            volumeMounts:
            - name: config
              mountPath: /config
            env:
            - name: ACCESS-KEY
              valueFrom:
                secretKeyRef:
                  name: rook-ceph-object-user-{{ $.Values.s3.crdName }}-{{ $.Values.s3.username }}
                  key: AccessKey
            - name: SECRET-KEY
              valueFrom:
                secretKeyRef:
                  name: rook-ceph-object-user-{{ $.Values.s3.crdName }}-{{ $.Values.s3.username }}
                  key: SecretKey
          containers:
          - name: create-bucket
            image: rook/ceph:v1.0.0
            command: 
            - "s3cmd"
            - "mb"
            - "--host=rook-ceph-rgw-{{ $.Values.s3.crdName }}"
            - "--host-bucket= "
            - "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 и Rados в радость


    Использование связки Ceph + RBD накладывает определенные ограничения на монтирование томов к pod’ам.

    В частности, в namespace обязательно должен лежать секрет для доступа к Ceph, чтобы stateful-приложения могли функционировать. Нормально, если у вас есть 2-3 окружения в своих пространствах имен: можно пойти и скопировать секрет вручную. Но что делать, если на каждую feature для разработчиков создается отдельное окружение со своим namespace?

    У себя мы решили данную проблему при помощи shell-operator, который автоматически копировал секреты в новые namespace (пример подобного хука описан в этой статье).

    #! /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 (пока в бета-стадии) и поэтому не требует секретов.

    Rook автоматически решает многие проблемы, что и подталкивает нас использовать его в новых проектах.

    Осада Rook


    Завершим практическую часть разворачиванием Rook и Ceph для возможности проведения собственных экспериментов. Для того, чтобы брать штурмом эту неприступную башню было легче, разработчики подготовили Helm-пакет. Давайте скачаем его:

    $ helm fetch rook-master/rook-ceph --untar --version 1.0.0

    В файле rook-ceph/values.yaml можно найти множество различных настроек. Самое важное — указать tolerations для агентов и поиска. Для чего можно использовать механизм taints/tolerations, мы подробно рассказывали в этой статье.

    Если вкратце, мы не хотим, чтобы pod’ы с клиентским приложением располагались на тех же узлах, где расположены диски для хранения данных. Причина проста: так работа агентов Rook не будет влиять на само приложение.

    Итак, открываем файл 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-чарт командой:

    $ 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: filestore
        resources:
          limits:
            memory: "1024Mi"
          requests:
            memory: "1024Mi"
        nodes:
        - name: host-1
          directories:
          - path: "/mnt/osd"
        - name: host-2
          directories:
          - path: "/mnt/osd"
        - name: host-3
          directories:
          - path: "/mnt/osd"

    Проверяем статус 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?


    Как видно, разработка Rook идёт полным ходом. Но всё ещё остаются проблемы, которые не позволяют нам полностью отказаться от ручной настройки Ceph:

    • Ни один драйвер Rook не умеет экспортировать метрики по использованию смонтированных блоков, что лишает нас мониторинга.
    • Flexvolume и CSI не умеют изменять размер томов (в отличие от того же RBD), поэтому Rook лишается полезного (а иногда и критически нужного!) инструмента.
    • Rook всё ещё не такой гибкий, как обычный Ceph. Если мы захотим настроить, чтобы пул для метаданных CephFS хранился на SSD, а сами данные — на HDD, потребуется прописывать отдельные группы устройств в CRUSH maps вручную.
    • Несмотря на то, что rook-ceph-operator считается стабильным, на данный момент существуют определенные проблемы при обновлении Ceph c версии 13 до 14.

    Выводы


    «Сейчас Ладья закрыта от внешнего мира пешками, но мы верим, что однажды она сыграет решающую роль в партии!» (цитата придумана специально для этой статьи)

    Проект Rook, несомненно, завоевал наши сердца — мы считаем, что [со всеми своими плюсами и минусами] он точно заслуживает и вашего внимания.

    У нас же дальнейшие планы сводятся к тому, что сделать rook-ceph модулем для addon-operator, что сделает его использование в наших многочисленных Kubernetes-кластерах ещё более простым и удобным.

    P.S.


    Читайте также в нашем блоге:

    • +24
    • 2,3k
    • 3
    Флант
    251,00
    Специалисты по DevOps и Kubernetes
    Поделиться публикацией

    Комментарии 3

      0
      Спасибо за статью, я к нему уже давно пристматриваюсь. Мне вот интересно если весь кластер сломается. Его (rook) можно будет потом востановить. В смысле данные то останутся, но на сколько это проблемно их опять в кластер получить?
      У меня раз кластер ломался, я как представлю, что при этом ещё и данные надо как-то востанавливать.
        +1
        Сам rook не сломается, а вот сломанный ceph востановить будет сложно — rook генерирует конфиги через init контейнеры и это сильно усложняет процесс.
        +1

        Мой опыт с k8s в районе persistent volumes показал, что оно не плохо, а катастрофически плохо (т.е. плохо во время катастроф). Без адекватного STONITH оно жить не сможет, а STONITH в k8s не завезли.


        А как сделать "плохо"? Ну, например, во время установки stp-соединения не закрыть канал. Раз 8 или 16.


        Вы думаете, что у вас в инсталляции нет STP? А если найду? STP находится где-то между SAS HBA и SAS Enclosure, а цифра 8 или 16 соответствует wide port в SAS.


        Что происходит после того, как все 16 каналов заняты? Теоретически, контроллер должен послать bus/host reset, но тут интрига: не проходит.


        С практической стороны это выглядит как TASK_UNINTERRUPTIBLE (D+) и делай что хочешь. В силу устройства контейнеров, пока все процессы не прибъёшь — pod живёт. А если процесс игнорирует ваши просьбы-9? Продолжает жить. Если pod продолжает жить, deploy не видит смысла спанить ещё один.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое