Kubernetes и GlusterFS

Docker friends gang linux tux go gopherЕсть множество правильных способов развернуть Kubernetes поверх существующей инфраструктуры. Разворачивая поверх OpenStack и vSphere я столкнулся с множеством багов, и в первую очередь, с подсистемой хранения. В результате родилась мысль, что должен быть унифицированный слой инфраструктуры, который бы не зависел от того, на какой именно платформе производится запуск.

Ниже я опишу как производится установка в полу-ручном режиме с ссылками на документацию, чтобы не дублировать текст. По мотивам можно написать Ansible скрипты и использовать их, например, с Terraform, как это делается в Kubespray. С той лишь разницей, что в Kubespray очень уж все усложнили по сравнению с kubeadm.

В связке Kubernetes-GlusterFS есть только одна большая проблема — нужно вручную создавать разделы. Но для решения этой проблемы существует фреймворк Heketi.

Файлы на GitHub.

Исходные дынные

  • 2 узла хранилища у каждого два диска/раздела — системный и данных
    2 ядра/2Гб/16Гб+1Тб
  • 1 мастер Kubernetes
    2 ядра/4Гб/128Гб+256Тб
  • 2 ноды Kubernetes
    16 ядер/64Гб/128Гб+1Тб

Для простоты у всех машин по 1 сетевому интерфейсу и все объединены в плоскую сеть.

GlusterFS

Разработчики ориентируются на Red Hat семейство. Так что я ставил на CentOS 7. Ставится все очень просто по мануалу. Но, так как управлять разделами у нас будет Heketi — нам не нужно их форматировать.

Нам достаточно:

  1. убедиться, что на серверах статический IP
  2. стоит ssh-сервер на 22 порту
  3. сервера видят друг-друга по hostname (можно прописать в /etc/hosts)
  4. подключить репозиторий
    Gluster Ubuntu PPAs
    CentOS Wiki
  5. установить серверные пакеты на все узлы хранилища
    glusterfs gluster-cli glusterfs-libs glusterfs-server
  6. установить клиентские пакеты на ноды Kubernetes (они будут монтировать)
    glusterfs-client
  7. форматировать диски в xfs и что-либо монтировать не нужно! (это будет делать Heketi)
  8. но нужно объединить серверы в пул
    на первом gluster peer probe gluster2.example.com
    и на втором gluster peer probe gluster1.example.com
  9. если вы планируете отдать целиком диск под хранилище, лучше на весь диск сделать primary раздел fdisk’ом — это может предотвратить некоторые проблемы в будущем
  10. на всякий случай нужно убрать с диска следы прошлых файловых систем
    sudo wipefs --all --force /dev/sdb1

Пример установки на CentOS 7 (от root):
# yum install centos-release-gluster
# yum --enablerepo=centos-gluster*-test install glusterfs-server
# systemctl enable glusterd
# systemctl start glusterd
# firewall-cmd --zone=public --add-port=24007-24008/tcp --permanent
# firewall-cmd --zone=public --add-port=24009/tcp --permanent
# firewall-cmd --zone=public --add-service=nfs --add-service=samba --add-service=samba-client --permanent
# firewall-cmd --zone=public --add-port=111/tcp --add-port=139/tcp --add-port=445/tcp --add-port=965/tcp --add-port=2049/tcp \
> --add-port=38465-38469/tcp --add-port=631/tcp --add-port=111/udp --add-port=963/udp --add-port=49152-49251/tcp --permanent
# firewall-cmd --reload
# gluster peer probe <NODE-2-NAME>
# fdisk /dev/vdb

В fdisk: n(ew) -> Enter -> Enter ->Enter

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

Docker

Для работы Kubernetes важно. чтобы на всех узлах был установлен docker. Причем не любой, для Kubernetes 1.6 — это а 1.12. И именно эта версия есть в официальных репозиториях основных Linux дистрибутивов. В Ubuntu 16.04 это docker.ioKubernetes 1.8 уже протестирован с 1.13 и 17.03.2, но есть баг с настройкой iptables. Так же не обнаружено проблем запуска на Docker 17.09.0.

Но есть одна особенность, нужно внимательно подходить к настройке драйвера хранилища. По умолчанию docker использует overlayfs/overlayfs2 — более-менее универсальное решение. В современных debian-based дистрибутивах появился драйвер aufs — но он не лучше overlayfs. В продакшене рекомендуется использовать отдельный диск и devicemapper для хранения образов, контейнеров и volume’ов. Это наиболее эффективный и стабильный вариант. Именно по-этому у меня на нодах Kubernetes дополнительный диск. Настраивается довольно быстро по инструкции. Но если у вас нет отделного диска, то не стоит использовать devicemapper-loop — он работает хуже, чем overlay. Есть еще zfs и btrf — эти решения не для каждого, применять их можно только в том случае, если вы сами пришли к этой необходимости.

Пример установки на Debian 9 (от root):
# apt-get install -y --no-install-recommends \
# apt-transport-https \
    ca-certificates \
    curl \
    software-properties-common
# curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add -
# add-apt-repository \
"deb [arch=amd64] https://download.docker.com/linux/$(. /etc/os-release; echo "$ID") \
$(lsb_release -cs) \
stable"
# apt-get update && apt-get install -y docker-ce=$(apt-cache madison docker-ce | grep 17.09 | head -1 | awk '{print $3}')
# systemctl enable docker
# systemctl start docker
# docker info

По умолчанию будет использовться драйвер overlayfs2.

И glusterfs:

# apt-get -y install glusterfs-client

Kubernetes

Я пробовал несколько различных способов поставить k8s: magnum, kube-up, kubespray и другие. В итоге пришел к выводу, что удобнее и надежнее всего kubeadm.

Поставить так-же просто, как нарисовать сову:

  1. Ставим kubectl на свое рабочее место (хотя можно и пакет из репозитория как в п.2)
    # curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl
    # chmod +x ./kubectl
    # mv ./kubectl /usr/local/bin/kubectl
  2. Ставим kubeadm на все ноды
    # curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
    # cat <<EOF >/etc/apt/sources.list.d/kubernetes.list
    deb http://apt.kubernetes.io/ kubernetes-xenial main
    EOF
    # apt-get update
    # apt-get install -y kubelet kubeadm kubectl
  3. Разворачиваем кластер
    1. На мастере
      # kubeadm init --pod-network-cidr=10.244.0.0/16
      сохраняем себе весь вывод, он пригодится в будущем
    2. Особенно настройки для kubectl
      $ mkdir -p $HOME/.kube
      $ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
      $ sudo chown $(id -u):$(id -g) $HOME/.kube/config
    3. И команду, которую нужно выполнить на нодах для включения в класетр (выполнять после установки сетевога плагина)
      # kubeadm join --token <TOKEN> <MASTER_IP>:6443 --discovery-token-ca-cert-hash <TOKEN_HASH>
    4. Устанавливаем сетевой плагин
      $ kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/v0.9.1/Documentation/kube-flannel.yml
    5. На момент написания, был баг Dokcer 17.03 с Kubernetes 1.8.4. На всех узлах нужно включить маршрутизацию вручную
      # iptables -P FORWARD ACCEPT
      # sysctl -w net.ipv4.ip_forward=1
      # echo 'net.ipv4.ip_forward=1' >> /etc/sysctl.conf

Инструкция по ссылке проста и понятна. Нужно только не забыть, что на всех нодах должен быть установлен клиент glusterfs.

Начиная с версии Kubernetes 1.8, токен, по умолчанию, живет 24 часа. После этого, для подключения новых нод, нужно выпустить новый
# kubeadm token create

Нужно выбирать сетевой плагин. Сейчас наиболее стабильным и быстрым является flannel с vxlan бэк-эндом. С одной лишь оговоркой — только поверх физической сети. Vxlan поверх Vxlan (flannel в openstack) у меня выдавал 3Мбит/с по чети 1Гбит/сек.

Соответственно при инициализации мастера нужно не забыть указать соответствующий ключ
# kubeadm init --pod-network-cidr=10.244.0.0/16

Инструкция по установке сетевого плагина подразумевает, что файл деплоя будет браться с raw.githubusercontent.com. У меня были проблемы с доступом, так что я просто скачал kube-flannel.yml и kube-flannel-rbac.yml с репозитория на github. Кстати это так-же может быть полезно, если нужно подправить какие-либо параметры, например тот-же pod-network-cidr.

После инициализации мастера, создастся файл /etc/kubernetes/admin.conf. Его можно скопировать себе в $HOME/.kube/config чтобы работал kubectl.

Heketi

Это самая темная часть системы. Суть проста — слушает запросы по RESTful интерфейсу и если просят создать volume — подключается к нодам харнилища и создает нужные разделы. А после отвечает на REST-запрос путем к разделу в нотации GlusterFS.

Проблема только в том, что приложение, по-видимому, писали для какого-то конкретного случая и не особо потрудились документировать.

Подготовка узлов хранилища

Heketi должен иметь возможность создавать разделы на узлах хранения. Делать это он может двумя способами — используя Kubernetes exec — если узлы GlusterFS запущены в Kubernetes, и по ssh. В моем случае, когда у меня узлы GlusterFS — это отдельные машины, остается только вариант ssh.

А так как нужно работать с дисками, то нужны root права. Конечно можно было дать пользователя с sudo, но конкретно этот параметр разработчики не посчитали нужным вынести в переменные окружения. И авторизовываться придется по ключу, так как других вариантов нам тоже не предлагают.

  1. Заходим на один из узлов root’ом и генерируем ssh-ключ
    # ssh-keygen
  2. Разрешаем подключаться с этим ключом к этой-же машине
    # ssh-copy-id localhost
  3. Проделываем то-же самое с другими узлами
    # ssh-copy-id gluster2.example.com
  4. Сохраняем себе приватный ключ, чтобы отдать его Heketi
    # scp ~/.ssh/id_rsa master.example.com:

Помимо прочего, на узлах должен быть lvm и xfs.

Секрет

Теперь нужно положить id_rsa в секрет Kubernetes, чтобы позже его использовать в контейнере Heketi

$ kubectl create secret generic heketi-id-rsa --from-file=id_rsa

Deployment

Официальная инструкция, помимо описания установки gluster-daemonset, который нам не нужен, описывает деплой самого Heketi с использованием deploy-heketi-deployment.json в Kubernetes. Нам он не совсем подходит, так что я его немного переписал: изменил переменные окружения, настроил права сервис-акканута и привел в формат yaml.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: heketi-service-account
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: secret-manager
rules:
- apiGroups: [""]
  resources: ["secrets"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: heketi-can-change-secrets
subjects:
- kind: ServiceAccount
  name: heketi-service-account
roleRef:
  kind: Role
  name: secret-manager
  apiGroup: rbac.authorization.k8s.io
---
apiVersion: v1
kind: Secret
metadata:
  labels:
    heketi: db
    glusterfs: heketi-service
  name: heketi-db-backup
data:
  heketi.db: ""
type: Opaque
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  annotations:
    description: Defines how to deploy Heketi
  labels:
    glusterfs: heketi-deployment
  name: heketi
spec:
  replicas: 1
  template:
    metadata:
      labels:
        glusterfs: heketi-pod
        name: heketi
      name: heketi
    spec:
      containers:
      - env:
        - name: HEKETI_EXECUTOR
          value: ssh
        - name: HEKETI_SSH_KEYFILE
          value: /key/id_rsa
        - name: HEKETI_SSH_USER
          value: root
        - name: HEKETI_BACKUP_DB_TO_KUBE_SECRET
          value: "true"
        image: heketi/heketi:latest
        imagePullPolicy: Always
        livenessProbe:
          httpGet:
            path: /hello
            port: 8080
            scheme: HTTP
          initialDelaySeconds: 30
          timeoutSeconds: 3
        name: heketi
        ports:
        - containerPort: 8080
          protocol: TCP
        readinessProbe:
          httpGet:
            path: /hello
            port: 8080
            scheme: HTTP
          initialDelaySeconds: 3
          timeoutSeconds: 3
        volumeMounts:
        - mountPath: /backupdb
          name: heketi-db-secret
        - mountPath: /key
          name: id-rsa
        - mountPath: /var/lib/heketi
          name: db
      serviceAccount: heketi-service-account
      volumes:
      - name: db
      - name: heketi-db-secret
        secret:
          secretName: heketi-db-backup
      - name: id-rsa
        secret:
          secretName: heketi-id-rsa
---
apiVersion: v1
kind: Service
metadata:
  annotations:
    description: Exposes Heketi Service
  labels:
    deploy-heketi: support
    glusterfs: heketi-service
  name: heketi
spec:
  ports:
  - name: heketi
    port: 8080
    protocol: TCP
    targetPort: 8080
  selector:
    name: heketi

Этот файл деплоится как и все остальное через

$ kubectl apply -f heketi.yaml

CLI

Теперь нужно передать параметры GlusetrFS кластера. Это делается с помощью утилиты командной строки. Нужно просто скачать бинарник со страницы релизов — для меня это linux.arm64, распаковать архив, найти в нем исполняемый файл и положить его в /usr/local/bin/.

  1. Установить heketi-cli
    $ wget -O - https://github.com/heketi/heketi/releases/download/v5.0.0/heketi-client-v5.0.0.linux.amd64.tar.gz | tar -zxv
    $ sudo mv heketi-client/bin/heketi-cli /usr/local/bin/
  2. Узнать имя пода (не обязательно)
    kubectl get pod -lname=heketi -o name
  3. Прокинуть временно порт до него
    kubectl port-forward $(kubectl get pod -lname=heketi -o name | cut -d"/" -f2) 8182:8080 &
  4. Сконфигурировать адрес сервера
    export HEKETI_CLI_SERVER=http://localhost:8182

Теперь можно пользоваться утилитой командной строки. А можно добавить две строки в .bashrc

Есть даже простой автокомплит:

$ wget https://raw.githubusercontent.com/heketi/heketi/release/5/client/cli/go/heketi-cli.sh
$ source heketi-cli.sh

Топология

Теперь нужно объяснить Heketi какие у нас есть сервера и диски на них. Есть возможность на каждый сервис указывать два разных адреса: один для ssh, второй для GlusterFS.

{
    "clusters": [
        {
            "nodes": [
                {
                    "node": {
                        "hostnames": {
                            "manage": [
                                "<NODE 1 IP>"
                            ],
                            "storage": [
                                "<NODE 1 IP>"
                            ]
                        },
                        "zone": 1
                    },
                    "devices": [
                        "/dev/vdb1"
                    ]
                },
                {
                    "node": {
                        "hostnames": {
                            "manage": [
                                "<NODE 2 IP>"
                            ],
                            "storage": [
                                "<NODE 2 IP>"
                            ]
                        },
                        "zone": 2
                    },
                    "devices": [
                        "/dev/vdb1"
                    ]
                }
            ]
        }
    ]
}

Есть объяснение того, как правильно использовать зоны: разные зоны нужно использовать, когда есть разные источники питания, что-то еще. Но так как у меня всего 2 сервера, я просто сделал их в разных зонах.

Дальше файл с зоной просто загружается в Heketi

$ heketi-cli topology load --json=topology.json

И если все в порядке, Heketi зайдет на сервера, настроит lvm на разделах и напишет clusterid. Он пригодится дальше.

Проверяем

$ heketi-cli volume create --size=1 --replica 2

Если все хорошо, тестовый образ можно удалить. Если случилась ошибка, чаше всего это «Out of space», нужно смотреть логи pod’а:

$ kubectl logs $(kubectl get pod -lname=heketi -o name | cut -d"/" -f2)

Storage Class

Теперь осталось объяснить Kubernetes как использовать наше хранилище.

Нужно узнать IP сервиса Heketi

$ kubectl get svc heketi

Колонка CLUSTER-IP. Этот IP нужно подставить в файл storageclass.yaml, как и clusterid

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: gfs-mirror
provisioner: kubernetes.io/glusterfs
parameters:
  resturl: "http://10.96.97.101:8080"
  clusterid: "1530dd1c0d44182ab07390890b1950a0"
  volumetype: "replicate:2"

$ kubectl apply -f storageclass-mirror.yaml

В дополнение создадим еще одно хранилище, но без отказоустойчивости, зададим его для использования по умолчанию:

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: standard
  annotations:
    storageclass.kubernetes.io/is-default-class: true
provisioner: kubernetes.io/glusterfs
parameters:
  resturl: "http://10.96.97.101:8080"
  clusterid: "1530dd1c0d44182ab07390890b1950a0"
  volumetype: "none"

$ kubectl apply -f storageclass-distributed.yaml

Подробнее про настройку класса можно почитать на kubernetes.io.

Все, теперь можно использовать наше хранилище

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: testvolumeclaim
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  storageClassName: gfs-mirror
---
kind: Pod
apiVersion: v1
metadata:
  name: testpod
  labels:
    name: testpod
spec:
  containers:
    - name: test
      image: busybox
      command: ["sleep"]
      args: ["31536000"]
      volumeMounts:
      - mountPath: "/volume"
        name: vol
  volumes:
    - name: vol
      persistentVolumeClaim:
       claimName: testvolumeclaim

$ kubectl apply -f test-pv.yaml

Можно зайти внутрь

$ kubectl exec -ti testpod sh

И посмотреть скорость, например:

$ dd if=/dev/urandom of=/test1 bs=1M count=100
$ dd if=/test1 of=/dev/null bs=1M count=100
$ dd if=/dev/urandom of=/volume/test2 bs=1M count=100
$ dd if=/volume/test2 of=/dev/null bs=1M count=100

Уроборос

Ouroboros

А как же быть с хранением самой базы Heketi? Если ее потерять, то придется заново форматировать диски. Данные, конечно, вытащить будет не сложно, настраивать все нужно заново.

Бэкап базы (топология, кластер, ноды, диски) периодически сохраняется в kubernetes secret heketi-db-backup. А при запуске пода из секрета загружается. Еще можно, на всякий случай, сохранить себе копию базы:

$ kubectl cp $(kubectl get pod -lname=heketi -o name | cut -d"/" -f2):/var/lib/heketi/heketi.db ./heketi.db

Ну и самое лучшее место для хранения базы — GlusterFS.
Можно было бы использовать

heketi-cli setup-openshift-heketi-storage

но для этого требуется не менее 3 нод. Так что придется в полу-ручном режиме.

Создадим контейнер чтобы использовать его диск

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: heketivolumeclaim
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  storageClassName: gfs-mirror
---
kind: Pod
apiVersion: v1
metadata:
  name: fakepod
  labels:
    name: fakepod
spec:
  containers:
    - name: bb
      image: busybox
      volumeMounts:
      - mountPath: "/volume"
        name: vol
  volumes:
    - name: vol
      persistentVolumeClaim:
       claimName: heketivolumeclaim

$ kubectl apply -f create-pvc-for-heketi.yaml

Помимо claim здесь еще присутствует pod, он нужен, чтобы k8s и heketi отработали связывание pvc-pv-heketi пока heketi еще работает. При обновлении деплоя это может не получится.

Удаляем временный pod

$ kubectl delete pod fakepod

На всякий случай сохраняем БД

$ kubectl cp $(kubectl get pod -lname=heketi -o name | cut -d"/" -f2):/var/lib/heketi/heketi.db ./heketi.db

Обновляем deploy heketi

Разница только в добавлении двух строк описания volume db, теперь ссылаемся на созданный ранее pvc

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  annotations:
    description: Defines how to deploy Heketi
  labels:
    glusterfs: heketi-deployment
  name: heketi
spec:
  replicas: 1
  template:
    metadata:
      labels:
        glusterfs: heketi-pod
        name: heketi
      name: heketi
    spec:
      containers:
      - env:
        - name: HEKETI_EXECUTOR
          value: ssh
        - name: HEKETI_SSH_KEYFILE
          value: /key/id_rsa
        - name: HEKETI_SSH_USER
          value: root
        - name: HEKETI_BACKUP_DB_TO_KUBE_SECRET
          value: "true"
        image: heketi/heketi:latest
        imagePullPolicy: Always
        livenessProbe:
          httpGet:
            path: /hello
            port: 8080
            scheme: HTTP
          initialDelaySeconds: 30
          timeoutSeconds: 3
        name: heketi
        ports:
        - containerPort: 8080
          protocol: TCP
        readinessProbe:
          httpGet:
            path: /hello
            port: 8080
            scheme: HTTP
          initialDelaySeconds: 3
          timeoutSeconds: 3
        volumeMounts:
        - mountPath: /backupdb
          name: heketi-db-secret
        - mountPath: /key
          name: id-rsa
        - mountPath: /var/lib/heketi
          name: db
      serviceAccount: heketi-service-account
      volumes:
      - name: db
        persistentVolumeClaim:
         claimName: heketivolumeclaim
      - name: heketi-db-secret
        secret:
          secretName: heketi-db-backup
      - name: id-rsa
        secret:
          secretName: heketi-id-rsa


$ kubectl delete deployment heketi
$ kubectl apply -f heketi-deployment-wih-pvs.yaml

Теперь у нас есть полноценное сетевой хранилище.

P.S. Написано по мотивам Heketi v5.0.0-5-gb005e0f-release-5. В 4 версии еще нельзя было через переменную окружения включить бэкап базы в secret. А в 5-й версии, при использовании pvc и storageclass, размер volume на 1Гб больше, чем запрашивали.

Оставить комментарий


Примечание - Вы можете использовать эти HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

* Copy This Password *

* Type Or Paste Password Here *

57 698 Spam Comments Blocked so far by Spam Free Wordpress