Trucs et astuces Kubernetes: accélérer le bootstrap de grandes bases de données

Avec cet article, nous ouvrons une série de publications avec des instructions pratiques sur la façon de nous faciliter la vie (l'opération) et les développeurs dans diverses situations qui se produisent littéralement tous les jours. Tous sont collectés à partir d'une expérience réelle dans la résolution des problèmes des clients et se sont améliorés au fil du temps, mais ne prétendent toujours pas être idéaux - considérez-les davantage comme des idées et des blancs.

Je vais commencer par une «astuce» dans la préparation de grands vidages de base de données comme MySQL et PostgreSQL pour leur déploiement rapide pour divers besoins - tout d'abord, sur les plates-formes pour les développeurs. Le contexte des opérations décrites ci-dessous est notre environnement typique, qui comprend un cluster Kubernetes fonctionnel et l'utilisation de GitLab (et dapp ) pour CI / CD. C'est parti!



La principale difficulté de Kubernetes lors de l'utilisation de la branche de fonctionnalité est les grandes bases de données, lorsque les développeurs souhaitent tester / démontrer leurs modifications sur une base de données complète (ou presque complète) depuis la production. Par exemple:

  • Il existe une application avec une base de donnĂ©es en MySQL pour 1 To et 10 dĂ©veloppeurs qui dĂ©veloppent leurs propres fonctionnalitĂ©s.
  • Les dĂ©veloppeurs veulent des boucles de test individuelles et quelques boucles plus spĂ©cifiques pour les tests et / ou les dĂ©mos.
  • De plus, il est nĂ©cessaire de restaurer le vidage de nuit de la base de production dans son circuit de test pendant une durĂ©e raisonnable - pour reproduire le problème avec le client ou le bogue.
  • Enfin, il est possible d'allĂ©ger la taille de la base de donnĂ©es d'au moins 150 Go - pas tant que cela, mais tout en Ă©conomisant de l'espace. C'est-Ă -dire nous devons encore prĂ©parer en quelque sorte la dĂ©charge.

Remarque : Habituellement, nous sauvegardons les bases de données MySQL en utilisant innobackupex de Percona, ce qui nous permet de sauvegarder toutes les bases de données et les utilisateurs ... - en bref, tout ce qui peut être nécessaire. C'est un tel exemple qui est examiné plus loin dans l'article, bien que dans le cas général, la façon dont vous effectuez les sauvegardes n'a pas d'importance.

Disons que nous avons une sauvegarde de base de données. Que faire ensuite?

Étape 1: préparation d'une nouvelle base de données à partir du vidage


Tout d'abord, nous créerons dans Kubernetes Deployment , qui se composera de deux conteneurs d'initialisation (c'est-à-dire de tels conteneurs spéciaux qui s'exécutent avant les foyers d'application et vous permettent d'effectuer la préconfiguration) et d'un foyer.

Mais où le placer? Nous avons une grande base de données (1 To) et nous voulons augmenter dix de ses instances - nous avons besoin d'un serveur avec un grand disque (10+ To). Nous le commandons séparément pour cette tâche et marquons le nœud avec ce serveur avec une étiquette spéciale dedicated: non-prod-db . Dans le même temps, nous utiliserons la tache éponyme, que Kubernetes dira que seules les applications qui lui sont résistantes (ont des tolérances ) peuvent rouler vers ce nœud, c'est-à-dire traduire Kubernetes dans le langage, dedicated Equal non-prod-db .

À l'aide de nodeSelector et des tolerations sélectionnez le nœud souhaité (situé sur un serveur avec un grand disque):

  nodeSelector: dedicated: non-prod-db tolerations: - key: "dedicated" operator: "Equal" value: "non-prod-db" effect: "NoExecute" 

... et reprenez la description du contenu de ce nœud.

Conteneurs init: get-bindump


Le premier conteneur d'initialisation que nous appellerons get-bindump . Il monte emptyDir (dans /var/lib/mysql ), où le vidage de la base de données reçu du serveur de sauvegarde sera ajouté. Pour ce faire, le conteneur contient tout ce dont vous avez besoin: clés SSH, adresses de serveur de sauvegarde. Cette étape dans notre cas prend environ 2 heures.

La description de ce conteneur dans le déploiement est la suivante:

  - 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 

Le script get_bindump.sh utilisé dans le conteneur:

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

Conteneurs init: prepare-bindump


Après avoir téléchargé la sauvegarde, le deuxième conteneur d'initialisation est lancé - prepare-bindump . Il exécute innobackupex --apply-log (puisque les fichiers sont déjà disponibles dans /var/lib/mysql - grâce à emptyDir de get-bindump ) et le serveur MySQL démarre.

C'est dans ce conteneur init que nous effectuons toutes les conversions nécessaires vers la base de données, la préparant pour l'application sélectionnée: nous effaçons les tables pour lesquelles elle est autorisée, modifions les accès à l'intérieur de la base de données, etc. Ensuite, nous éteignons le serveur MySQL et archivons simplement l'intégralité de /var/lib/mysql dans un fichier tar.gz. En conséquence, le vidage tient dans un fichier de 100 Go, ce qui est déjà un ordre de grandeur plus petit que le 1 To d'origine. Cette étape dure environ 5 heures.

Description du deuxième conteneur d'initialisation dans le déploiement :

  - 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 

Le script prepare_bindump.sh utilisé ressemble à ceci:

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

Sous


L'accord final est le lancement du foyer principal, qui se produit après l'exécution des conteneurs d'initialisation. Dans pod, nous avons un simple nginx, et via emtpyDir compressé et recadré de 100 Go est emtpyDir . La fonction de ce nginx est de donner ce vidage.

Configuration du foyer:

  - 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: {} 

Voici à quoi ressemble tout le déploiement avec ses 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 

Notes supplémentaires:

  1. Dans notre cas, nous préparons un nouveau vidage chaque nuit en utilisant le travail planifié dans GitLab. C'est-à-dire chaque nuit, ce déploiement se déroule automatiquement, ce qui génère un nouveau vidage et le prépare pour la distribution dans tous les environnements de développeur de test.
  2. Pourquoi lançons-nous également volume /dump dans des conteneurs init (et dans le script il y a une vérification de l'existence de /dump/version.txt )? Cela se fait au cas où le serveur sous lequel il s'exécute est redémarré. Les conteneurs recommenceront et sans cette vérification, le vidage recommencera le téléchargement. Si nous avons déjà préparé un vidage une fois, puis au prochain démarrage (en cas de redémarrage du serveur), le /dump/version.txt drapeau /dump/version.txt en informera.
  3. Qu'est db-dumps ? Nous le collectons avec dapp et son Dappfile ressemble Ă  ceci:

     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 

Étape 2: lancement de la base de données dans un environnement de développeur


Lors du déploiement de la base de données MySQL dans l'environnement de test du développeur, il a un bouton dans GitLab qui lance le redéploiement du déploiement avec MySQL avec la stratégie RollingUpdate.maxUnavailable: 0 :



Comment est-ce mis en œuvre?
Dans GitLab, lorsque vous cliquez sur reload db , le déploiement avec la spécification suivante est déployé:

 spec: strategy: rollingUpdate: maxUnavailable: 0 

C'est-à-dire nous demandons à Kubernetes de mettre à jour le déploiement (en créer un nouveau sous) et de nous assurer qu'au moins un sous est actif. Étant donné que lors de la création d'un nouveau foyer, il contient des conteneurs d'initialisation pendant qu'ils fonctionnent, le nouveau ne passe pas en état de fonctionnement, ce qui signifie que l'ancien continue de fonctionner. Et seulement au moment où MySQL lui-même a démarré (et que la sonde de préparation a fonctionné), le trafic y bascule et l'ancienne (avec l'ancienne base de données) est supprimée.

Les détails sur ce programme peuvent être trouvés dans les documents suivants:


L'approche choisie nous permet d'attendre qu'un nouveau dump soit téléchargé, décompressé et lancé, et ce n'est qu'après que l'ancien sera supprimé de MySQL. Ainsi, pendant que nous préparons une nouvelle décharge, nous travaillons tranquillement avec l'ancienne base.

Le conteneur init de ce déploiement utilise la commande suivante:

 curl "$DUMP_URL" | tar -C /var/lib/mysql/ -xvz 

C'est-à-dire nous téléchargeons le vidage de la base de données compressée qui a été préparé à l'étape 1, décompressons-le dans /var/lib/mysql , puis démarre sous Déploiement , dans lequel MySQL est lancé avec les données déjà préparées. Tout cela prend environ 2 heures.

Et le déploiement est le suivant ...
 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 

Résumé


Il s'avère que nous avons toujours le déploiement , qui se déroule tous les soirs et fait ce qui suit:

  • Obtient un nouveau vidage de base de donnĂ©es
  • en quelque sorte, il le prĂ©pare Ă  un fonctionnement correct dans un environnement de test (par exemple, trankeytit certaines tables, remplace les donnĂ©es rĂ©elles des utilisateurs, rend les utilisateurs nĂ©cessaires, etc.);
  • donne Ă  chaque dĂ©veloppeur la possibilitĂ© de dĂ©ployer une telle base de donnĂ©es prĂ©parĂ©e dans son espace de noms dans Deployment en appuyant sur un bouton dans CI - grâce au service qui y est disponible, la base de donnĂ©es sera disponible sur mysql (par exemple, il peut s'agir du nom du service dans l'espace de noms).

Pour l'exemple que nous avons examiné, la création d'un vidage à partir d'une réplique réelle prend environ 6 heures, la préparation d'une «image de base» prend 7 heures et la mise à jour de la base de données dans l'environnement du développeur prend 2 heures. Étant donné que les deux premières actions sont effectuées «en arrière-plan» et sont invisibles pour les développeurs, en fait, ils peuvent déployer une version de production de la base de données (d'une taille de 1 To) pendant les mêmes 2 heures .

Les questions, critiques et corrections du schéma proposé et de ses composantes sont les bienvenues dans les commentaires!

PS Bien sûr, nous comprenons que dans le cas de VMware et de certains autres outils, il serait possible de créer un instantané d'une machine virtuelle et de lancer un nouveau virusalka à partir d'un instantané (ce qui est encore plus rapide), mais cette option n'inclut pas la préparation de la base, compte tenu du fait qu'elle se révélera à peu près la même temps ... Sans oublier le fait que tout le monde n'a pas la possibilité ou le désir d'utiliser des produits commerciaux.

PPS


Autre du cycle de trucs et astuces K8:


Lisez aussi dans notre blog:

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


All Articles