Con este artículo, abrimos una serie de publicaciones con instrucciones prácticas sobre cómo hacernos la vida más fácil (la operación) y los desarrolladores en diversas situaciones que suceden literalmente todos los días. Todos ellos se recopilan a partir de la experiencia real en la resolución de problemas de los clientes y han mejorado con el tiempo, pero aún no afirman ser ideales: considérelos más como ideas y espacios en blanco.
Comenzaré con un "truco" en la preparación de grandes volcados de bases de datos como MySQL y PostgreSQL para su rápida implementación para diversas necesidades, en primer lugar, en las plataformas para desarrolladores. El contexto de las operaciones que se describen a continuación es nuestro entorno típico, que incluye un clúster de Kubernetes en funcionamiento y el uso de GitLab (y
dapp ) para CI / CD. Vamos!

El principal problema en Kubernetes cuando se utiliza la rama de características son grandes bases de datos, cuando los desarrolladores quieren probar / demostrar sus cambios en una base de datos completa (o casi completa) desde la producción. Por ejemplo:
- Hay una aplicación con una base de datos en MySQL para 1 TB y 10 desarrolladores que desarrollan sus propias características.
- Los desarrolladores quieren bucles de prueba individuales y un par de bucles más específicos para pruebas y / o demostraciones.
- Además, es necesario restaurar el volcado nocturno de la base de producción en su circuito de prueba durante un tiempo razonable, para reproducir el problema con el cliente o el error.
- Finalmente, es posible aligerar el tamaño de la base de datos en al menos 150 GB, no tanto, pero aún así ahorrar espacio. Es decir Todavía tenemos que preparar de alguna manera el vertedero.
Nota : Por lo general, hacemos una copia de seguridad de las bases de datos MySQL utilizando el innobackupex de Percona, que nos permite guardar todas las bases de datos y usuarios ..., en resumen, todo lo que pueda ser necesario. Es un ejemplo que se considera más adelante en el artículo, aunque en el caso general no importa exactamente cómo hacer copias de seguridad.Entonces, digamos que tenemos una copia de seguridad de la base de datos. ¿Qué hacer a continuación?
Paso 1: preparar una nueva base de datos desde el volcado
En primer lugar, crearemos en Kubernetes
Deployment , que constará de dos contenedores init
(es decir, dichos contenedores especiales que se ejecutan antes del hogar de
la aplicación y le permiten realizar la preconfiguración) y un hogar.
¿Pero dónde colocarlo? Tenemos una gran base de datos (1 TB) y queremos generar diez de sus instancias; necesitamos un servidor con un disco grande (10+ TB). Lo pedimos por separado para esta tarea y marcamos el nodo con este servidor con una
etiqueta especial
dedicated: non-prod-db
. Al mismo tiempo, usaremos la
mancha homónima, que Kubernetes dirá que solo las aplicaciones que son resistentes (tienen
tolerancias ) pueden pasar a este nodo, es decir, traducir Kubernetes al lenguaje,
dedicated Equal non-prod-db
.
Usando
nodeSelector
y
tolerations
seleccione el nodo deseado (ubicado en un servidor con un disco grande):
nodeSelector: dedicated: non-prod-db tolerations: - key: "dedicated" operator: "Equal" value: "non-prod-db" effect: "NoExecute"
... y tome la descripción del contenido de este nodo.
Contenedores Init: get-bindump
El primer contenedor init lo llamaremos
get-bindump
.
emptyDir
(en
/var/lib/mysql
), donde se agregará el volcado de la base de datos recibido del servidor de respaldo. Para hacer esto, el contenedor tiene todo lo que necesita: claves SSH, direcciones del servidor de respaldo. Esta etapa en nuestro caso dura aproximadamente 2 horas.
La descripción de este contenedor en
Implementación es la siguiente:
- 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
El script
get_bindump.sh
utilizado en el contenedor:
Contenedores Init: prepare-bindump
Después de descargar la copia de seguridad, se inicia el segundo contenedor init:
prepare-bindump
. Ejecuta
innobackupex --apply-log
(ya que los archivos ya están disponibles en
/var/lib/mysql
- gracias a
emptyDir
de
get-bindump
) y se inicia el servidor MySQL.
Es en este contenedor de inicio que hacemos todas las conversiones necesarias a la base de datos, preparándola para la aplicación seleccionada: borramos las tablas para las que está permitido, cambiamos los accesos dentro de la base de datos, etc. Luego apagamos el servidor MySQL y simplemente archivamos todo
/var/lib/mysql
en un archivo tar.gz. Como resultado, el volcado cabe en un archivo de 100 GB, que ya es un orden de magnitud más pequeño que el original de 1 TB. Esta etapa dura aproximadamente 5 horas.
Descripción del segundo contenedor init en
Implementación :
- 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
El script
prepare_bindump.sh
usa en este se parece a esto:
Bajo
El acorde final es el lanzamiento del hogar principal, que ocurre después de que se ejecutan los contenedores init. En pod, tenemos un nginx simple, y a través de
emtpyDir
comprimido y recortado de 100 GB. La función de este nginx es dar este volcado.
Configuración de hogar:
- 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: {}
Así es como se ve la implementación completa con sus initContainers ... --- 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
Notas adicionales:
- En nuestro caso, preparamos un nuevo volcado todas las noches utilizando el trabajo programado en GitLab. Es decir todas las noches, esta implementación se implementa automáticamente, lo que genera un nuevo volcado y lo prepara para su distribución a todos los entornos de desarrollo de prueba.
- ¿Por qué también estamos lanzando volumen
/dump
en contenedores de inicio (y en el script hay una comprobación de la existencia de /dump/version.txt
)? Esto se hace en caso de que se reinicie el servidor con el que se ejecuta. Los contenedores comenzarán de nuevo y, sin esta comprobación, el volcado comenzará a descargarse nuevamente. Si ya hemos preparado un volcado una vez, en el siguiente inicio (en caso de reinicio del servidor), el /dump/version.txt
indicador /dump/version.txt
informará sobre esto. - ¿Cuál es la imagen
db-dumps
? Lo recopilamos con dapp y su Dappfile
ve así:
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
Paso 2: lanzamiento de la base de datos en un entorno de desarrollador
Al implementar la base de datos MySQL en el entorno de prueba del desarrollador, tiene un botón en GitLab que inicia la redistribución de la
implementación con MySQL con la estrategia
RollingUpdate.maxUnavailable: 0
:

¿Cómo se implementa esto?En GitLab, cuando hace clic en
recargar db , se
implementa la
implementación con la siguiente especificación:
spec: strategy: rollingUpdate: maxUnavailable: 0
Es decir le decimos a Kubernetes que actualice la
implementación (cree una nueva en) y nos aseguremos de que al menos una de ellas esté activa. Dado que al crear un nuevo hogar, tiene contenedores de inicio mientras están trabajando, el nuevo
no entra en estado de
Ejecución , lo que significa que el antiguo continúa funcionando. Y solo en el momento en que MySQL se inició (y la sonda de preparación funcionó), el tráfico cambia a él y se elimina el anterior (con la base de datos anterior).
Los detalles sobre este esquema se pueden encontrar en los siguientes materiales:
El enfoque elegido nos permite esperar hasta que se descargue, descomprima y ejecute un nuevo volcado, y solo después de eso, el antiguo se eliminará de MySQL. Por lo tanto, mientras estamos preparando un nuevo volcado, estamos trabajando en silencio con la antigua base.
El contenedor de inicio de esta
implementación utiliza el siguiente comando:
curl "$DUMP_URL" | tar -C /var/lib/mysql/ -xvz
Es decir descargamos el volcado de la base de datos comprimida que se preparó en el paso 1, lo descomprimimos en
/var/lib/mysql
y luego se inicia en
Implementación , en el que MySQL se inicia con los datos ya preparados. Todo esto lleva unas 2 horas.
Y la implementación es la siguiente ... 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: "password" 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
Resumen
Resulta que siempre tenemos
Implementación , que se implementa todas las noches y hace lo siguiente:
- Obtiene un volcado de base de datos nuevo
- de alguna manera lo prepara para su correcto funcionamiento en un entorno de prueba (por ejemplo, trankeytit algunas tablas, reemplaza datos de usuario reales, crea los usuarios necesarios, etc.);
- brinda a cada desarrollador la oportunidad de implementar una base de datos preparada en su espacio de nombres en Implementación presionando un botón en CI; gracias al Servicio disponible en ella, la base de datos estará disponible en
mysql
(por ejemplo, puede ser el nombre del servicio en el espacio de nombres).
Para el ejemplo que examinamos, crear un volcado a partir de una réplica real lleva aproximadamente 6 horas, preparar una "imagen base" lleva 7 horas y actualizar la base de datos en el entorno del desarrollador lleva 2 horas. Dado que las dos primeras acciones se realizan "en segundo plano" y son invisibles para los desarrolladores, de hecho, pueden implementar una versión de producción de la base de datos (con un tamaño de 1 TB)
durante las mismas 2 horas .
¡Preguntas, críticas y correcciones al esquema propuesto y sus componentes son bienvenidos en los comentarios!
PD Por supuesto, entendemos que en el caso de VMware y algunas otras herramientas, sería posible crear una instantánea de una máquina virtual y lanzar un nuevo virusalka desde una instantánea (que es aún más rápido), pero esta opción no incluye la preparación de la base, teniendo en cuenta que resultará casi igual tiempo ... Sin mencionar el hecho de que no todos tienen la oportunidad o el deseo de utilizar productos comerciales.
PPS
Otros del ciclo de consejos y trucos de K8s:
Lea también en nuestro blog: