Consejos y trucos de Kubernetes: acelerar el arranque de grandes bases de datos

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:

 #!/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 

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:

 #!/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('password') 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 -ppassword somedb /usr/bin/mysqladmin shutdown -uroot -ppassword cd /var/lib/mysql/ tar -czf /dump/mysql_bindump.tar.gz ./* touch /dump/healthz rm -rf /var/lib/mysql/* 

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:

  1. 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.
  2. ¿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.
  3. ¿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:

Source: https://habr.com/ru/post/es417509/


All Articles