Как заменить etcd — мозг кластера Kubernetes — на PostgreSQL или любую другую СУБД, которую вы хотите

etcd — это мозг каждого кластера Kubernetes, хранилище ключей и значений, отслеживающее все объекты в кластере. Он переплетен и тесно связан с Kubernetes, и может показаться, что это неотъемлемая часть кластера, или это так?

В этой статье мы рассмотрим, как мы можем заменить etcd базой данных PostgreSQL, а также почему и когда это может иметь смысл.

Почему?

Если вы используете свой собственный кластер Kubernetes, то вам известны трудности управления etcd. Помимо точки зрения пользователя, etcd мало используется за пределами Kubernetes и находится в состоянии упадка, потому что никто не хочет его поддерживать. Из-за этого исправление критических ошибок занимает много времени.

Кроме того, почему бы и нет? Почему мы не должны использовать другие серверные части хранилища с Kubernetes? Наличие большего количества опций — это хорошо, и у запуска Kubernetes с РСУБД действительно нет недостатков, будь то PostgreSQL, MySQL или что-то еще, что вам может быть удобно.

Кроме того, запуск Kubernetes с РСУБД — не такая уж новая идея, k3s, дистрибутив Kubernetes производственного уровня может работать с реляционной БД вместо etcd. Если это работает для k3s, почему это не работает для любого другого кластера?

Как?

Как упоминалось в начале, Kubernetes и etcd тесно связаны. Компоненты кластера (сервер API) ожидают etcd-like интерфейса, в который они могут писать и из которого они могут читать. Поэтому, чтобы использовать базу данных SQL в качестве хранилища, нам нужно предоставить etcd-to-SQL уровень перевода, который называется Kine. Kine — это компонент k3s, позволяющий использовать различные СУБД в качестве замены etcd. Он обеспечивает реализацию функций GRPC, на которые опирается Kubernetes. Что касается Kubernetes, то он разговаривает с сервером etcd.

Прежде чем мы приступим к запуску Kine, если мы хотим запустить Kubernetes с PostgreSQL, нам, очевидно, понадобится экземпляр PostgreSQL:

Примечание. Если вы хотите продолжить или быстро развернуть кластер на виртуальной машине с поддержкой PostgreSQL, вы можете проверить мой репозиторий (ветвь k8s-without-etcd).

apt -y install postgresql postgresql-contrib
systemctl start postgresql.service

В этом руководстве мы будем использовать PostgreSQL, работающий как системный сервис, мы также настроим SSL для базы данных:

# Generate self signed root CA cert
openssl req -addext "subjectAltName = DNS:localhost" -nodes \
  -x509 -newkey rsa:2048 -keyout ca.key -out ca.crt -subj "/CN=localhost"

# Generate server cert to be signed
openssl req -addext "subjectAltName = DNS:localhost" -nodes \
  -newkey rsa:2048 -keyout server.key -out server.csr -subj "/CN=localhost"

# Sign the server cert
openssl x509 -extfile <(printf "subjectAltName=DNS:localhost") -req \
  -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt

chmod og-rwx ca.key
chmod og-rwx server.key

cp {server.crt,server.key,ca.crt} /var/lib/postgresql/
chown postgres.postgres /var/lib/postgresql/server.key

sed -i -e "s|ssl_cert_file.*|ssl_cert_file = '/var/lib/postgresql/server.crt'|g" /etc/postgresql/14/main/postgresql.conf
sed -i -e "s|ssl_key_file.*|ssl_key_file = '/var/lib/postgresql/server.key'|g" /etc/postgresql/14/main/postgresql.conf
sed -i -e "s|#ssl_ca_file.*|ssl_ca_file = '/var/lib/postgresql/ca.crt'|g" /etc/postgresql/14/main/postgresql.conf

systemctl restart postgresql.service

Мы используем openssl для создания корневого сертификата ЦС, запроса на подпись, а также сертификата и ключа сервера. Затем мы ограничиваем права доступа к ключам, чтобы их мог прочитать только их владелец. Копируем сертификаты и ключ в /var/lib/postgresql/ и делаем пользователя postgres владельцем ключа сервера. Наконец, мы модифицируем postgresql.conf, чтобы указать PostgreSQL использовать наши новые SSL-сертификаты и ключ.

Когда база данных запущена, мы можем перейти к созданию кластера. Мы сделаем это, используя конфигурацию kubeadm и ниже:

# kubeadm-config.yaml
apiVersion: kubeadm.k8s.io/v1beta3
kind: InitConfiguration
localAPIEndpoint:
  advertiseAddress: 192.168.56.2
  bindPort: 6443
nodeRegistration:
  criSocket: "unix:///var/run/crio/crio.sock"
---
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
kubernetesVersion: v1.25.0
networking:
  podSubnet: 10.244.0.0/16
etcd:
  external:
    endpoints:
    - http://127.0.0.1:2379
apiServer:
  timeoutForControlPlane: 1m0s

Вы можете настроить это под свои нужды. Единственная важная часть здесь — это массив etcd.external.endpoints, который сообщает Kubernetes, где находится etcd (или etcd-compatible-interface) — в нашем случае это место, где Kine будет слушать.

Чтобы построить кластер с использованием этой конфигурации, запустите:

kubeadm init --config=/.../kubeadm-config.yaml --upload-certs \
  --ignore-preflight-errors ExternalEtcdVersion 2>&1 || true

kubectl taint nodes --all node-role.kubernetes.io/control-plane-

В дополнение к передаче файла конфигурации мы также указываем, что хотим игнорировать ошибки, связанные с etcd, во время запуска, и, учитывая, что этот кластер игровой площадки имеет только один узел, мы также очищаем главный узел, чтобы мы могли запускать на нем рабочие нагрузки. .

Теперь самое интересное — настройка Kine. Есть несколько способов развернуть его — как базовый процесс, как сервис systemd, как под в пространстве имен kube-system или мой предпочтительный вариант — как вспомогательный сервер API.

Когда мы развернули кластер с kubeadm, он уже сгенерировал для нас статические манифесты Pod, в том числе один для сервера API ( kube-apiserver.yaml), поэтому нам нужно будет пропатчить его, чтобы включить контейнер с запущенным Kine:

# vim /etc/kubernetes/manifests/kube-apiserver.yaml
# ...
  containers:
  - name: kube-apiserver
  # ...  Existing API server container
  # -------------------------------------
  - image: rancher/kine:v0.10.1-amd64
    name: kine
    securityContext:  # Don't do this in real deployment...
      runAsUser: 0
      runAsGroup: 0
    command: [ "/bin/sh", "-c", "--" ]
    args: [ 'kine --endpoint="postgres://$(POSTGRES_USERNAME):$(POSTGRES_PASSWORD)@localhost:5432/postgres"
      --ca-file=/var/lib/postgresql/ca.crt 
      --cert-file=/var/lib/postgresql/server.crt 
      --key-file=/var/lib/postgresql/server.key' ]
    env:
      - name: POSTGRES_USERNAME  # This should be a secret
        value: "postgres"
      - name: POSTGRES_PASSWORD  # This should be a secret
        value: "somepass"
    volumeMounts:
      - mountPath: /var/lib/postgresql/
        name: kine-ssl
        readOnly: true
  volumes:
  # -------------------------------------
  # ... Existing volumes used by API Server container
  # -------------------------------------
  - hostPath:
      path: /var/lib/postgresql
      type: DirectoryOrCreate
    name: kine-ssl

Выше показан контейнер, который нам нужно добавить в статический под API-сервера, он использует образ Kine из Docker Hub по адресу rancher/kine:.... Он указывает точку входа, которая указывает на базу данных PostgreSQL, а также сертификаты SSL и ключ, которые мы сгенерировали ранее.

После применения этих изменений к серверу API мы увидим, что Kubelet успешно запустит как kube-apiserver, так и kine контейнер:

crictl ps
CONTAINER      IMAGE              CREATED         STATE    NAME            ATTEMPT  POD ID         POD
09172381b42f1  4d2edfd10d3e3f...  29 seconds ago  Running  kube-apiserver  0        3187286722eba  kube-apiserver-kubemaster
55ac5108ae677  ccdd8a15f4ca3e...  29 seconds ago  Running  kine            0        3187286722eba  kube-apiserver-kubemaster

И просто чтобы доказать, что мы работаем без etcd, мы можем проверить пространство имен kube-system и увидеть, что etcd pod'ов нет:

kubectl get pods -n kube-system
NAME                                 READY   STATUS    RESTARTS   AGE
kube-controller-manager-kubemaster   1/1     Running   0          5d19h
kube-scheduler-kubemaster            1/1     Running   0          5d19h
kube-apiserver-kubemaster            1/1     Running   0          5d19h
kube-proxy-mrfs5                     1/1     Running   0          5d19h
coredns-565d847f94-wrn82             1/1     Running   0          5d19h
coredns-565d847f94-7zvwr             1/1     Running   0          5d19h

Или в качестве дополнительного теста мы можем развернуть в кластере какие-то ресурсы, например, эти.

kubectl get pods -n default
NAME                                  READY   STATUS    RESTARTS   AGE
example-deployment-78d75878cc-b56kl   1/1     Running   0          22h
example-deployment-78d75878cc-4ftj5   1/1     Running   0          22h
example-deployment-78d75878cc-bvpx6   1/1     Running   0          22h

kubectl get all -n default
NAME                                      READY   STATUS    RESTARTS   AGE
pod/example-deployment-78d75878cc-b56kl   1/1     Running   0          22h
pod/example-deployment-78d75878cc-4ftj5   1/1     Running   0          22h
pod/example-deployment-78d75878cc-bvpx6   1/1     Running   0          22h

NAME                      TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
service/kubernetes        ClusterIP   10.96.0.1      <none>        443/TCP   6d17h
service/example-service   ClusterIP   10.98.111.70   <none>        80/TCP    22h

NAME                                 READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/example-deployment   3/3     3            3           22h

NAME                                            DESIRED   CURRENT   READY   AGE
replicaset.apps/example-deployment-78d75878cc   3         3         3       22h

И ура, мы видим, что все работает, что было бы невозможно без работающего резервного хранилища.

Изучение/ковыряние

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

psql -U postgres -p 5432 -h 127.0.0.1  # somepass

И тогда мы можем просмотреть схему и таблицы:

-- List tables:
\dt public.*
        List of relations
 Schema | Name | Type  |  Owner   
--------+------+-------+----------
 public | kine | table | postgres
(1 row)

-- Describe Kine table
\d public.kine;
                                        Table "public.kine"
     Column      |          Type          | Collation | Nullable |             Default              
-----------------+------------------------+-----------+----------+----------------------------------
 id              | integer                |           | not null | nextval('kine_id_seq'::regclass)
 name            | character varying(630) |           |          | 
 created         | integer                |           |          | 
 deleted         | integer                |           |          | 
 create_revision | integer                |           |          | 
 prev_revision   | integer                |           |          | 
 lease           | integer                |           |          | 
 value           | bytea                  |           |          | 
 old_value       | bytea                  |           |          | 
Indexes:
    "kine_pkey" PRIMARY KEY, btree (id)
    "kine_id_deleted_index" btree (id, deleted)
    "kine_name_id_index" btree (name, id)
    "kine_name_index" btree (name)
    "kine_name_prev_revision_uindex" UNIQUE, btree (name, prev_revision)
    "kine_prev_revision_index" btree (prev_revision)

Как вы можете видеть выше, есть единственная таблица с именем kine, которая содержит все данные. Kine использует базу данных как хранилище с журнальной структурой, поэтому при каждой записи с сервера API создается новая строка, в которой хранится созданный или обновленный объект Kubernetes.

Давайте посмотрим на данные:

-- Some 1000+ rows
select count(*) from public.kine;
 count 
-------
  1319
(1 row)

select name, encode(public.kine.value, 'escape') as value_plain 
from public.kine where name like '/registry/pods/default/example%' limit 1;

select name, encode(public.kine.value, 'escape') as value_plain
from public.kine where name like '/registry/configmaps/default/example%' limit 1;

Столбец name использует ту же структуру, что и etcd — он указывает путь к объекту в кластере — /registry/RESOURCE_TYPE/NAMESPACE/NAME. Столбец value содержит фактический манифест в виде массива байтов. Здесь я опускаю фактический результат запроса, потому что декодированные данные довольно уродливы из-за декодирования пробелов и наличия данных "управляемых полей", но если вы попробуете это сами, вы сможете расшифровать это.

Масштабирование/производительность

Мы выяснили, как запустить Kubernetes без etcd, но стоит ли? Какова производительность такого кластера и масштабируется ли он?

Как уже упоминалось, Kine и, следовательно, бэкенды RDBMS используются k3s, поэтому мы можем проверить документы по профилированию ресурсов k3s, чтобы сравнить, как работают базы данных SQL по сравнению с etcd.

k3s также имеет набор тестов с настраиваемой базой данных/серверной частью, так что вы можете запускать тесты и сравнивать, если хотите.

Для масштабирования и, в частности, PostgreSQL, вы также можете последовать совету в этой проблеме GitHub и хранить разные типы данных в разных таблицах, используя секционированные таблицы PostgreSQL.

Заключительные мысли

Я считаю, что нет никаких недостатков в использовании РСУБД вместо etcd для кластера Kubernetes, но избавление от etcd не решит всех ваших проблем. Каждый инструмент приносит свои собственные проблемы и проблемы, и то же самое относится к PostgreSQL или любой другой базе данных SQL.

Вам придется взвесить все за и против запуска вашего кластера с базой данных SQL для вашего конкретного случая использования, возможно, например, знакомый интерфейс SQL и ваш опыт в управлении СУБД перевешивают накладные расходы, хлопоты или любые возможные проблемы, которые могут возникнуть. приходят с заменой etcd.

…или, может быть, просто используйте k3s 😉.



Want to Connect?

This article was originally posted at martinheinz.dev.