- PVSM.RU - https://www.pvsm.ru -
Этой статьёй мы открываем череду публикаций с практическими инструкциями о том, как облегчить жизнь себе (эксплуатации) и разработчикам в различных ситуациях, случающихся буквально ежедневно. Все они собраны из реального опыта решения задач от клиентов и со временем улучшались, но по-прежнему не претендуют на идеал — рассматривайте их скорее как идеи и заготовки.
Я начну с «трюка» по подготовке больших дампов баз данных вроде MySQL и PostgreSQL для их быстрого развёртывания для различных нужд — в первую очередь, на площадках для разработчиков. Контекст описанных ниже операций — наше типовое окружение, включающее в себя работающий кластер Kubernetes и применение GitLab (и dapp [1]) для CI/CD. Поехали!

Основная боль в Kubernetes при использовании feature branch — это большие базы данных, когда разработчики хотят протестировать/продемонстрировать свои изменения на полной (или почти полной) базе из production. Например:
Примечание: Обычно базы мы бэкапим MySQL с помощью innobackupex от Percona, что позволяет сохранить и все базы, и пользователей… — словом, всё, что может потребоваться. Именно такой пример и рассмотрен дальше в статье, хотя в общем случае абсолютно всё равно, как именно вы делаете бэкапы.
Итак, допустим, у нас есть бэкап базы данных. Что делать дальше?
Первым делом мы создадим в Kubernetes Deployment, который будет состоять из двух init-контейнеров (т.е. таких специальных контейнеров [2], что запускаются до подов с приложением и позволяют выполнять предварительную настройку) и одного пода.
Но где его размещать? У нас большая база (1 Тб) и мы хотим поднять десять её экземпляров — потребуется сервер с большим диском (10+ Тб). Закажем его отдельно для этой задачи и пометим узел с этим сервером специальным лейблом [3] dedicated: non-prod-db. Заодно воспользуемся одноимённым taint [4], который будет говорить Kubernetes, что на этот узел могут катиться только приложения, которые устойчивы (имеют tolerations) к нему, т.е., переводя на язык Kubernetes, dedicated Equal non-prod-db.
С помощью nodeSelector и tolerations выберём нужный узел (размещённый на сервере с большим диском):
nodeSelector:
dedicated: non-prod-db
tolerations:
- key: "dedicated"
operator: "Equal"
value: "non-prod-db"
effect: "NoExecute"
… и займёмся описанием содержимого этого узла.
Первый init-контейнер мы назовём get-bindump. В него монтируется emptyDir (в /var/lib/mysql), куда будет складываться полученный с бэкап-сервера дамп базы данных. Для этого в контейнере есть все необходимое: SSH-ключи, адреса бэкап-серверов. Данная стадия в нашем случае занимает около 2 часов.
Описание этого контейнера в Deployment выглядит следующим образом:
- name: get-bindump
image: db-dumps
imagePullPolicy: Always
command: [ "/bin/sh", "-c", "/get_bindump.sh" ]
resources:
limits:
memory: "5000Mi"
cpu: "1"
requests:
memory: "5000Mi"
cpu: "1"
volumeMounts:
- name: dump
mountPath: /dump
- name: mysqlbindir
mountPath: /var/lib/mysql
- name: id-rsa
mountPath: /root/.ssh
Используемый в контейнере скрипт get_bindump.sh:
#!/bin/bash
date
if [ -f /dump/version.txt ]; then
echo "Dump file already exists."
exit 0
fi
rm -rf /var/lib/mysql/*
borg extract --stdout user@your.server.net:somedb-mysql::${lastdump} stdin | xbstream -x -C /var/lib/mysql/
echo $lastdump > /dump/version.txt
После скачивания бэкапа запускается второй init-контейнер — prepare-bindump. Он выполняет innobackupex --apply-log (так как файлы уже доступны в /var/lib/mysql — благодаря emptyDir из get-bindump) и стартует сервер MySQL.
Именно в этом init-контейнере мы делаем все необходимые преобразования в БД, готовя её к выбранному применению: очищаем таблицы, для которых это допустимо, меняем доступы внутри базы и т.п. Затем выключаем сервер MySQL и просто архивируем весь /var/lib/mysql в tar.gz-файл. В итоге, дамп умещается в файл размером 100 Гб, что уже на порядок меньше, чем исходный 1 Тб. Данная стадия занимает около 5 часов.
Описание второго init-контейнера в Deployment:
- name: prepare-bindump
image: db-dumps
imagePullPolicy: Always
command: [ "/bin/sh", "-c", "/prepare_bindump.sh" ]
resources:
limits:
memory: "5000Mi"
cpu: "1"
requests:
memory: "5000Mi"
cpu: "1"
volumeMounts:
- name: dump
mountPath: /dump
- name: mysqlbindir
mountPath: /var/lib/mysql
- name: debian-cnf
mountPath: /etc/mysql/debian.cnf
subPath: debian.cnf
Используемый в нём скрипт prepare_bindump.sh выглядит примерно так:
#!/bin/bash
date
if [ -f /dump/healthz ]; then
echo "Dump file already exists."
exit 0
fi
innobackupex --apply-log /var/lib/mysql/
chown -R mysql:mysql /var/lib/mysql
chown -R mysql:mysql /var/log/mysql
echo "`date`: Starting mysql"
/usr/sbin/mysqld --character-set-server=utf8 --collation-server=utf8_general_ci --innodb-data-file-path=ibdata1:200M:autoextend --user=root --skip-grant-tables &
sleep 200
echo "`date`: Creating mysql root user"
echo "update mysql.user set Password=PASSWORD('toor') WHERE user='root';" | mysql -uroot -h 127.0.0.1
echo "delete from mysql.user where USER like '';" | mysql -uroot -h 127.0.0.1
echo "delete from mysql.user where user = 'root' and host NOT IN ('127.0.0.1', 'localhost');" | mysql -uroot -h 127.0.0.1
echo "FLUSH PRIVILEGES;" | mysql -uroot -h 127.0.0.1
echo "truncate somedb.somedb_table_one;" | mysql -uroot -h 127.0.0.1 -ptoor somedb
/usr/bin/mysqladmin shutdown -uroot -ptoor
cd /var/lib/mysql/
tar -czf /dump/mysql_bindump.tar.gz ./*
touch /dump/healthz
rm -rf /var/lib/mysql/*
Финальный аккорд — запуск основного пода, что происходит после выполнения init-контейнеров. В поде у нас стоит простой nginx, а через emtpyDir подложен сжатый и обрезанный дамп в 100 Гб. Функция данного nginx — отдавать этот дамп.
Конфигурация пода:
- name: nginx
image: nginx:alpine
resources:
requests:
memory: "1500Mi"
cpu: "400m"
lifecycle:
preStop:
exec:
command: ["/usr/sbin/nginx", "-s", "quit"]
livenessProbe:
httpGet:
path: /healthz
port: 80
scheme: HTTP
timeoutSeconds: 7
failureThreshold: 5
volumeMounts:
- name: dump
mountPath: /usr/share/nginx/html
- name: nginx-config
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf
readOnly: false
volumes:
- name: dump
emptyDir: {}
- name: mysqlbindir
emptyDir: {}
---
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: db-dumps
spec:
strategy:
rollingUpdate:
maxUnavailable: 0
revisionHistoryLimit: 2
template:
metadata:
labels:
app: db-dumps
spec:
imagePullSecrets:
- name: regsecret
nodeSelector:
dedicated: non-prod-db
tolerations:
- key: "dedicated"
operator: "Equal"
value: "non-prod-db"
effect: "NoExecute"
initContainers:
- name: get-bindump
image: db-dumps
imagePullPolicy: Always
command: [ "/bin/sh", "-c", "/get_bindump.sh" ]
resources:
limits:
memory: "5000Mi"
cpu: "1"
requests:
memory: "5000Mi"
cpu: "1"
volumeMounts:
- name: dump
mountPath: /dump
- name: mysqlbindir
mountPath: /var/lib/mysql
- name: id-rsa
mountPath: /root/.ssh
- name: prepare-bindump
image: db-dumps
imagePullPolicy: Always
command: [ "/bin/sh", "-c", "/prepare_bindump.sh" ]
resources:
limits:
memory: "5000Mi"
cpu: "1"
requests:
memory: "5000Mi"
cpu: "1"
volumeMounts:
- name: dump
mountPath: /dump
- name: mysqlbindir
mountPath: /var/lib/mysql
- name: log
mountPath: /var/log/mysql
- name: debian-cnf
mountPath: /etc/mysql/debian.cnf
subPath: debian.cnf
containers:
- name: nginx
image: nginx:alpine
resources:
requests:
memory: "1500Mi"
cpu: "400m"
lifecycle:
preStop:
exec:
command: ["/usr/sbin/nginx", "-s", "quit"]
livenessProbe:
httpGet:
path: /healthz
port: 80
scheme: HTTP
timeoutSeconds: 7
failureThreshold: 5
volumeMounts:
- name: dump
mountPath: /usr/share/nginx/html
- name: nginx-config
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf
readOnly: false
volumes:
- name: dump
emptyDir: {}
- name: mysqlbindir
emptyDir: {}
- name: log
emptyDir: {}
- name: id-rsa
secret:
defaultMode: 0600
secretName: somedb-id-rsa
- name: nginx-config
configMap:
name: somedb-nginx-config
- name: debian-cnf
configMap:
name: somedb-debian-cnf
---
apiVersion: v1
kind: Service
metadata:
name: somedb-db-dump
spec:
clusterIP: None
selector:
app: db-dumps
ports:
- name: http
port: 80
Дополнительные примечания:
/dump (и в скрипте есть проверка на существование /dump/version.txt)? Это сделано на тот случай, если будет перезапущен сервер, на котором работает под. Контейнеры будут стартовать заново и без этой проверки начнёт повторно скачиваться дамп. Если же мы один раз уже подготовили дамп, то при следующем старте (в случае ребута сервера) файл-флаг /dump/version.txt сообщит об этом.db-dumps? Мы его собираем dapp'ом и его Dappfile [5] выглядит так:
dimg: "db-dumps"
from: "ubuntu:16.04"
docker:
ENV:
TERM: xterm
ansible:
beforeInstall:
- name: "Install percona repositories"
apt:
deb: https://repo.percona.com/apt/percona-release_0.1-4.xenial_all.deb
- name: "Add repository for borgbackup"
apt_repository:
repo="ppa:costamagnagianfranco/borgbackup"
codename="xenial"
update_cache=yes
- name: "Add repository for mysql 5.6"
apt_repository:
repo: deb http://archive.ubuntu.com/ubuntu trusty universe
state: present
update_cache: yes
- name: "Install packages"
apt:
name: "{{`{{ item }}`}}"
state: present
with_items:
- openssh-client
- mysql-server-5.6
- mysql-client-5.6
- borgbackup
- percona-xtrabackup-24
setup:
- name: "Add get_bindump.sh"
copy:
content: |
{{ .Files.Get ".dappfiles/get_bindump.sh" | indent 8 }}
dest: /get_bindump.sh
mode: 0755
- name: "Add prepare_bindump.sh"
copy:
content: |
{{ .Files.Get ".dappfiles/prepare_bindump.sh" | indent 8 }}
dest: /prepare_bindump.sh
mode: 0755
При выкате базы данных MySQL в тестовом окружении разработчика у него есть кнопка в GitLab, которая запускает редеплой Deployment'а с MySQL со стратегией RollingUpdate.maxUnavailable: 0:

spec:
strategy:
rollingUpdate:
maxUnavailable: 0
Т.е. мы говорим Kubernetes, чтобы он обновлял Deployment (создавал новый под) и при этом следил за тем, чтобы как минимум один под был живой. Так как при создании нового пода у него есть init-контейнеры, пока они работают, новый под не переходит в статус Running, а значит — старый под продолжает работать. И только в момент, как сам под с MySQL запустился (и отработала readiness probe), трафик переключается на него, а старый под (со старой базой) удаляется.
Подробности об этой схеме можно почерпнуть из следующих материалов:
Выбранный подход позволяет нам дождаться, пока будет скачан, разархивирован и запущен новый дамп, а только после этого удалится старый под с MySQL. Таким образом, пока у нас готовится новый дамп, мы спокойно работаем со старой базой.
В init-контейнере данного Deployment используется команда следующего вида:
curl "$DUMP_URL" | tar -C /var/lib/mysql/ -xvz
Т.е. мы скачиваем сжатый дамп базы, который был подготовлен на шаге 1, разархивируем его в /var/lib/mysql, после чего стартует под Deployment'а, в котором запускается MySQL с уже подготовленными данными. Всё это занимает примерно 2 часа.
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: mysql
spec:
strategy:
rollingUpdate:
maxUnavailable: 0
template:
metadata:
labels:
service: mysql
spec:
imagePullSecrets:
- name: regsecret
nodeSelector:
dedicated: non-prod-db
tolerations:
- key: "dedicated"
operator: "Equal"
value: "non-prod-db"
effect: "NoExecute"
initContainers:
- name: getdump
image: mysql-with-getdump
command: ["/usr/local/bin/getdump.sh"]
resources:
limits:
memory: "6000Mi"
cpu: "1.5"
requests:
memory: "6000Mi"
cpu: "1.5"
volumeMounts:
- mountPath: /var/lib/mysql
name: datadir
- mountPath: /etc/mysql/debian.cnf
name: debian-cnf
subPath: debian.cnf
env:
- name: DUMP_URL
value: "http://somedb-db-dump.infra-db.svc.cluster.local/mysql_bindump.tar.gz"
containers:
- name: mysql
image: mysql:5.6
resources:
limits:
memory: "1024Mi"
cpu: "1"
requests:
memory: "1024Mi"
cpu: "1"
lifecycle:
preStop:
exec:
command: ["/etc/init.d/mysql", "stop"]
ports:
- containerPort: 3306
name: mysql
protocol: TCP
volumeMounts:
- mountPath: /var/lib/mysql
name: datadir
- mountPath: /etc/mysql/debian.cnf
name: debian-cnf
subPath: debian.cnf
env:
- name: MYSQL_ROOT_PASSWORD
value: "toor"
volumes:
- name: datadir
emptyDir: {}
- name: debian-cnf
configMap:
name: somedb-debian-cnf
---
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
clusterIP: None
selector:
service: mysql
ports:
- name: mysql
port: 3306
protocol: TCP
---
apiVersion: v1
kind: ConfigMap
metadata:
name: somedb-debian-cnf
data:
debian.cnf: |
[client]
host = localhost
user = debian-sys-maint
password = password
socket = /var/run/mysqld/mysqld.sock
[mysql_upgrade]
host = localhost
user = debian-sys-maint
password = password
socket = /var/run/mysqld/mysqld.sock
Получается, что у нас всегда есть Deployment, который выкатывается каждую ночь и делает следующее:
mysql (например, это может быть имя сервиса в namespace).Для рассмотренного нами примера создание дампа с реальной реплики занимает около 6 часов, подготовка «образа базы» — 7 часов, а обновление базы в окружении разработчика — 2 часа. Поскольку первые два действия выполняются «в фоне» и незримы для разработчиков, то по факту они могут разворачивать себе продовую версию базы (с размером в 1 Тб) за те же 2 часа.
Вопросы, критика и исправления к предложенной схеме и её компонентам — с радостью принимаются в комментариях!
P.S. Конечно, мы понимаем, что в случае VMware и некоторых других инструментов можно было бы обойтись созданием снапшота виртуалки и запуском новой вируталки из снапшота (что ещё быстрее), но этот вариант не включает в себя подготовку базы, с учётом которой получится примерно то же время… Не говоря уже о том, что не у всех есть возможность или желание использовать коммерческие продукты.
Читайте также в нашем блоге:
Автор: Wimbo
Источник [15]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/mysql/286484
Ссылки в тексте:
[1] dapp: https://github.com/flant/dapp
[2] специальных контейнеров: https://kubernetes.io/docs/concepts/workloads/pods/init-containers
[3] лейблом: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
[4] taint: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/
[5] Dappfile: https://habr.com/company/flant/blog/351838/
[6] Performing a Rolling Update: https://kubernetes.io/docs/tutorials/kubernetes-basics/update/update-intro/
[7] Rolling Updates with Kubernetes Deployments: https://tachingchen.com/blog/kubernetes-rolling-update-with-deployment/#rolling-update
[8] Kubernetes deployment strategies: https://container-solutions.com/kubernetes-deployment-strategies/
[9] Сборка и дeплой приложений в Kubernetes с помощью dapp и GitLab CI: https://habr.com/company/flant/blog/345580/
[10] Практика с dapp. Часть 1: Сборка простых приложений: https://habr.com/company/flant/blog/336212/
[11] Практика с dapp. Часть 2. Деплой Docker-образов в Kubernetes с помощью Helm: https://habr.com/company/flant/blog/336170/
[12] Оркестровка СУБД CockroachDB в Kubernetes: https://habr.com/company/flant/blog/328756/
[13] Наш опыт с Kubernetes в небольших проектах: https://habr.com/company/flant/blog/331188/
[14] Полезные утилиты при работе с Kubernetes: https://habr.com/company/flant/blog/330198/
[15] Источник: https://habr.com/post/417509/?utm_campaign=417509
Нажмите здесь для печати.