lid's Wiki

Introduction

Haute disponibilité de stockage: NAS, SAN, RAID

RAID

/Note: le RAID n'est pas un backup!!!/

RAID signifie /Redundant Array of Independent Disks/ - on dit aussi parfois /Redundant Array of Inexpensive Disks/ :p

RAID0 (lol)

Alors, si vous avez des données dont vous n'avez rien à faire, que vous pouvez facilement retélécharger par exemple, et que vous avez besoin de performances, RAID-0 est tout indiqué (par exemple: jeux vidéos). Mais c'est à peu près tout 😄. RAID-0 "découpe" les données entre les disques, en lecture comme en écriture, en stripes dont la taille est prédéfinie lors de la création de l'agrégat - en général des stripes de 64Ko. Dès lors, on peut facilement écrire nos données sur 2 (ou plus) disques en même temps. idem pour la lecture. On peut considérer, grossièrement, qu'un agrégat RAID-0 de deux disques identiques permet de quasiment doubler les performances en écriture et en lecture. Remplacez les disques à plateau par des SSD et on peut obtenir des performances plus que sympathiques 😃

Par contre, si un disque de l'agrégat rencontre un problème, tout l'agrégat est perdu, et donc les données qui se trouvaient dessus 😃

RAID1

Ici, nos deux (ou plus) disques sont en miroir, c'est à dire que les deux (ou plus) disques contiennent exactement les mêmes données. On peut dès lors perdre tous les disques sauf un avant de perdre nos données, mais bien sûr les coûts sont plus élevés, vu qu'il faut, au minimum, doubler chaque disque pour chaque agrégat RAID-1.

RAID4 et RAID-DP

Ici, un disque (dans le cas de RAID-4) est dédié aux informations de parité. Deux disques sont réservés aux informations de parité dans le cas de RAID-DP. On rencontre souvent RAID-DP sur les baies Netapp: on a un bon ratio performance/résilience (on peut perdre deux disques par agrégat sans perdre de données dans le cas de RAID-DP, un seul dans le cas de RAID-4)

RAID5

Ici, on n'utilise pas un disque réservé pour les informations de parité, mais ces informations sont réparties sur chaque disque de l'agrégat, de manière séquentielle. Il nécessite, tout comme RAID-4, au minimum 3 disques. On obtient les meilleures performances sur un agrégat de 6/7 disques. Par exemple ici, une grappe RAID-5 sur une carte Perc H700, avec 6 disques SATA moisis:

lidstah@polaris:~$ sudo pveperf
CPU BOGOMIPS:      76612.16
REGEX/SECOND:      1511699
HD SIZE:           97.93 GB (/dev/mapper/pve-root)
BUFFERED READS:    467.61 MB/sec
AVERAGE SEEK TIME: 7.67 ms
FSYNCS/SECOND:     1697.46
DNS EXT:           32.21 ms
DNS INT:           47.76 ms

On voit qu'on obtient des performances plus que décentes niveau reads et fsync.

Par contre, on ne peut perdre qu'un seul disque.

RAID6

RAID6 est une évolution de RAID5 permettant d'utiliser /n/ informations redondantes de parité au lieu d'une seule. La grande majorité des implémentations de RAID6 se limitent à une valeur de /n = 2/ du fait de la complexité des fondements mathématiques à la base de ce système.

Ses avantages principaux sont de pouvoir résister à la perte de /n/ disques (en pratique, souvent deux disques), mais ses défauts principaux sont des temps d'écriture plus longs du fait des calculs plus complexes, et un temps de reconstruction du RAID en cas de perte de 2 disques très, très, trèèèès long 😃

Cependant il est de plus en plus utilisé du fait de l'augmentation des performances du matériel dédié, comme des CPUs, permettant de l'implémenter de façon logicielle (ajouté au kernel Linux 2.6, donc ça remonte).

RAID-Z (ZFS)

Similaire à RAID-5, le RAID-Z, utilisé par ZFS de Sun Microsystems, utilise un système de /copy-on-write/ afin de limiter les effets de /trous d'écriture/ (baisse des performances en écriture de nombreux petits fichiers sur un RAID-5). Néanmoins il est moins performant que RAID-5, sauf si on alloue de la RAM ou des flashpools SSD pour mettre en cache les opérations de lecture/écriture (L2ARC et ZIL).

RAIDs rigolos

RAID01

RAID10

RAID05

RAID15

RAID50

RAID51

RAID Matériel et RAID Logiciel

RAID Matériel

Le RAID Matériel implique l'utilisation de cartes dédiées (en général PCIe), utilisant souvent des firmwares propriétaires. La carte RAID s'occupe de la gestion des agrégats RAID et présente au système d'exploitation un seul /block device/. L'OS ne "voit" alors qu'un seul disque par agrégat RAID, sans nécessairement connaître l'état des disques utilisés - cependant des drivers existent permettant de vérifier l'état - notamment SMART - des disques utilisés par l'agrégat au niveau du système d'exploitation.

Ces cartes RAID disposent souvent de mémoire cache, permettant d'augmenter notablement les performances. De plus, elles disposent en général d'ASICs dédiés à la gestion des différents niveau de RAID, permettant de libérer le CPU lors, par exemple, de la reconstruction d'un RAID dont un disque a été remplacé.

RAID Logiciel

Ici, c'est l'OS qui gère directement les agrégats RAID, ce qui permet une meilleure souplesse d'utilisation, mais impacte - légèrement - les performances.

Sous Windows, le Gestionnaire de disques permet de facilement créer des agrégats RAID (0,1,5,6 iirc).

Sous GNU/Linux, on utilisera l'utilitaire /mdadm/. Par exemple pour créer un RAID-1 entre nos disques sdc et sdd (de taille identique):

# dans les deux cas:
# type de table de partitions GPT
# taille identique de partition
# type de partition: "fd", RAID Linux Autodetect
$ sudo cfdisk /dev/sdc
$ sudo cfdisk /dev/sdd

Puis on crée notre agrégat RAID-1 avec la commande mdadm:

$ sudo mdadm --create /dev/md0 --level=1 --raid-devices=2 /dev/sdc1 /dev/sdd1
# puis on "daemonise" notre volume RAID pour qu'il soit chargé à chaque démarrage:
$ sudo mdadm --daemonise /dev/md0
# en cas de fail:
$ sudo mdadm --monitor --daemonise /dev/md0

On peut maintenant vérifier les détails de notre aggrégat RAID-1:

$ sudo fdisk -l
$ sudo mdadm --detail /dev/md0

Et enfin, le formater et le monter:

$ sudo mkfs.ext4 /dev/md0
$ sudo mount /dev/md0 /mnt
$ df -h
$ sudo umount /mnt

Pour finir, on peut le déclarer dans fstab. Admettons qu'on veuille le monter dans le répertoire raid1 du répertoire home:

$ sudo mkdir /home/raid1
$ sudo vim /etc/fstab
/dev/md0  /home/raid1  ext4 defaults 0 1
:wq

Si un des disques vient à flancher, on va d'abord le retirer de l'agrégat avant d'ajouter un nouveau disque:

# on le marque "faulty"
$ sudo mdadm --manage /dev/md0 --set-faulty /dev/sdc1
# on le retire de la grappe
$ sudo mdadm --manage /dev/md0 --remove /dev/sdc1
# une fois le disque remplacé par un nouveau disque (partitionné, etc)
# on remet le nouveau disque en prod
# on va partir du principe que la partition est /dev/sdc1:
$ sudo mdadm --manage /dev/md0 --add /dev/sdc1

On peut aussi vouloir stopper complètement une grappe et la supprimer:

$ sudo mdadm --detail /dev/md0
$ sudo mdadm --stop /dev/md0
$ sudo mdadm --zero-superblock /dev/sdc1
$ sudo mdadm --zero-superblock /dev/sdd1
# Puis on modifie la conf' de mdadm si nécessaire

NAS

Définition

Un NAS (/Network Attached Storage/) fournit des services similaires à ceux d'un serveur de fichiers. Il expose des partages de fichiers NFS (/Network File System/), SMB/CIFS (euuurk), et parfois du FTP (euurk) ou WebDAV. Il se différencie d'un SAN dans le sens où il gère aussi le système de fichiers (ext4, WAFL chez Netapp) exposé via les protocoles sus-cités, là où un SAN expose des /block devices/ via des /LUNs/ (atchoum, on y revient plus bas) exposés en général sur un réseau dédié (mais pas obligatoirement).

Quelques fabricants de NAS bien connus:

On peut tout à fait s'en servir pour héberger des images disques (vmdk, qcow2, raw, etc) auxquelles les VMs d'un cluster d'hypervision accèdent, notamment si le NAS dispose de plusieurs interfaces réseau rapides (10Gbps et +, mais c'est utilisable à partir d'1Gbps). petit exemple de migration live avec proxmox

Ils sont aussi très utilisés comme stockage de sauvegarde: ils implémentent en effet pour la plupart plusieurs niveaux de RAID.

NFS /Network File System/

NFS a été créé par Sun MicroSystems en 1984. Il utilise le port 2049, en UDP (oui oui) ou en TCP. Il a été formalisé dans la RFC1094 pour sa v1.

Les versions du protocole en cours d'utilisation sont principalement la v3 (2003, RFC 3530) et la v4 (2010, RFC 5661), qui sont incompatibles entre elles, pour plusieurs raisons:

SMB/CIFS (lol)

Beaucoup utilisé en environnement Microsoft (comme c'est étrange). /Server Message Block/, anciennement CIFS /Common Internet File System/, descend de LAN Manager, créé par IBM (comme quoi, le jour où Microsoft créera quelque chose de lui-même n'est pas encore arrivé 🧌) en 1985 pour OS/2. Il en est actuellement à sa version 3 (Windows 8 et Server 2012).

Historiquement, SMB se basait sur la couche session, NetBIOS, connu pour générer du broadcast et donc pour gaspiller inutilement de la bande passante. On le remplace désormais par un serveur WINS.

En environnement non-Windows il est notablement moins performant que NFS (qu'on préfèrera donc comme /datastore/ pour nos hyperviseurs).

Une implémentation libre de SMB est Samba (qui ne fait pas que ça).

SAN

Définition

Un SAN /Storage Area Network/ est un réseau spécialisé mutualisant les ressources de stockage. À la différence d'un NAS, il propose un accès bas niveau aux disques. Son trafic réseau est dès lors très similaire aux principes utilisés par les disques internes de nos machines (SATA, SCSI, SAS).

Il permet de mutualiser, tout comme le NAS, les ressources de stockage. Mais à la différence d'un NAS, les baies de stockage n'apparaissent pas comme des volumes partagés sur le réseau: elles exposent des /block devices/, et donc chaque serveur voit les LUN /Logical Unit Number/ auxquels il a accès comme son propre disque dur.

On dispose de plusieurs protocoles pour le partage de ces disques: iSCSI, qui utilise le réseau (de préférence dédié) classique, et Fibre Channel, qui nécessite des cartes FC et des commutateurs FC dédiés, avec les prix qui vont avec, et les performances qui vont avec (jusqu'à 128Gbps, mais plus généralement 8/16Gbps).

iSCSI /Internet Small Computer System Interface/

C'est un protocole de stockage en réseau basé sur le protocole IP. Il transporte les commandes SCSI sur les réseaux IP (d'où son nom :p). On peut l'utiliser aussi bien sur un LAN que sur un WAN (et notamment Internet), et dès lors il permet d'être indépendant de l'emplacement physique du stockage. Il ne nécessite pas d'infrastructure dédiée, contrairement à Fibre Channel.

Il est composé de deux éléments:

Il existe des passerelles iSCSI <-> Fibre Channel.

iSCSI est un protocole de l'IETF (RFC3720, 3721, 3722).

Fibre Channel

Fibre Channel est un protocole dont la norme est ANSI X3T11 (atchoum). Il a à la base été créé pour les super-ordinateurs - son but étant de permettre une connexion très, très haut débit à une solution de stockage - mais il est devenu un des protocoles standard des SAN.

C'est un protocole série, qui peut fonctionner aussi bien sur des câbles Ethernet, coaxiaux ou de la fibre optique. On utilise des /Global Link Modules/ GLM pour l'interconnexion entre tout ce beau monde 😃

Il existe trois topologies différentes pour Fibre Channel:

On peut encapsuler ce protocole dans des trames Ethernet: FCoE, /Fibre Channel over Ethernet/.

Les débits sont assez impressionnants:

Génération Année Débit en Mo/s
FC 1Gb 1997 125
FC 2Gb 2001 250
FC 4Gb 2004 500
FC 8Gb 2005 1000
FC 10Gb 2008 1250
FC 16Gb 2011 2000
FC 32Gb 2016 4000
FC 128Gb 2016 16000

Fibre channel utilise aussi plusieurs types de ports:

Concepts de haute disponibilité des systèmes et services

Load-Balancing et Haute Disponibilité

Concepts

Imaginons une infrastructure Web classique. En phase de développement, 3 VMs étaient utilisées: un serveur Web+CGI, un serveur de base de données, et un serveur de stockage, soit autant de SPOFs.

Une première solution pour la production serait d'ajouter de la redondance:

Et pour la distribution des requêtes entre nos serveurs web et de BDD, on pourrait ajouter une machine de /load-balancing/ qui va répartir la charge entre nos différentes machines. Ce faisant, on ajoute cependant un SPOF supplémentaire: le load-balancer. Dès lors il va nous en falloir plusieurs, et il va aussi falloir un mécanisme pour pouvoir assigner et déplacer l'adresse IP (ou les adresses...) de notre plateforme d'un LB à l'autre. C'est ce qu'on appelle une IP flottante ou /IP Failover/.

Pour cela, on peut utiliser différents logiciels, par exemple /keepalived/

keepalived

Tout d'abord, il faut activer la possibilité de pouvoir se /bind/ sur une adresse IP qui ne soit pas locale dans notre fichier:

/etc/sysctl.d/10-nonlocalbind.conf

net.ipv4.ip_nonlocal_bind = 1

sudo systemctl restart systemd-sysctl.service

Sinon HAProxy nous jettera sur un restart 😃

Autres préparatifs:

sudo apt install keepalived

On peut maintenant attaquer la configuration de keepalived sur notre MASTER:

# vim /etc/keepalived/keepalived.conf
global_defs {
        # process identifier
        lvs_id haproxy_DH
        enable_script_security
        script_user root root
}


# on verif que haproxy tourne bien
vrrp_script check_haproxy {
        script "systemctl status haproxy"
        interval 2
        weight 2
}

# virtual interface

vrrp_instance VI_01 {
        state MASTER
        interface ens18
        virtual_router_id 51
        priority 101
        # ip fo partagee entre les serveurs
        virtual_ipaddress {
                192.168.1.70
        }
        track_script {
                check_haproxy
        }
}

/Note: en vrai on utilisera un autre user que root hein/

Et sur notre replica, les seules différences concernant le paramètre state (BACKUP) et la priorité plus faible:

global_defs {
        # process identifier
        lvs_id haproxy_DH
        enable_script_security
        script_user root root
}


# on verif que haproxy tourne bien
vrrp_script check_haproxy {
        script "systemctl status haproxy"
        interval 2
        weight 2
}

# virtual interface

vrrp_instance VI_01 {
        state BACKUP
        interface ens18
        virtual_router_id 51
        priority 100
        # ip fo partagee entre les serveurs
        virtual_ipaddress {
                192.168.1.70
        }
        track_script {
                check_haproxy
        }
}

Maintenant, on peut tester la fonctionnalité en tuant brutalement haproxy sur la VM master. Ou en tuant la VM master tout court 😄

Haproxy

Haproxy est un reverse-proxy/Load Balancer très performant. Il va nous permettre de distribuer les requêtes reçues sur notre IP FO, soit en HTTP vers nos serveurs Web, soit en TCP vers nos serveurs de base de données. On va l'installer:

sudo apt install haproxy hatop

Puis utiliser la configuration suivante (adaptez aux IPs de vos VMs):

global
	log /dev/log	local0
	log /dev/log	local1 notice
	chroot /var/lib/haproxy
	stats socket /run/haproxy/admin.sock mode 660 level admin
	stats timeout 30s
	user haproxy
	group haproxy
	daemon

	# Default SSL material locations
	ca-base /etc/ssl/certs
	crt-base /etc/ssl/private

	# Default ciphers to use on SSL-enabled listening sockets.
	#ssl-default-bind-ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS
	#ssl-default-bind-ciphers kEECDH+aRSA+AES256:kRSA+AES256:+AES256:!kEDH:!LOW:!EXP:!MD5:!aNULL:!eNULL
	#ssl-default-bind-ciphers kEECDH+aRSA+AES256:kRSA+AES256:!kEDH:!LOW:!EXP:!MD5:!aNULL:!eNULL
	ssl-default-bind-ciphers kEECDH+aRSA+AES256:!kEDH:!LOW:!EXP:!MD5:!aNULL:!eNULL
	ssl-default-bind-ciphersuites TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256
	tune.ssl.default-dh-param 2048
	ssl-default-bind-options ssl-min-ver TLSv1.2
	maxconn 2000

defaults
	log	global
	mode	http
	maxconn	2000
	option	httplog
	option	dontlognull
        timeout connect 5000
        timeout client  50000
        timeout server  50000
	errorfile 400 /etc/haproxy/errors/400.http
	errorfile 403 /etc/haproxy/errors/403.http
	errorfile 408 /etc/haproxy/errors/408.http
	errorfile 500 /etc/haproxy/errors/500.http
	errorfile 502 /etc/haproxy/errors/502.http
	errorfile 503 /etc/haproxy/errors/503.http
	errorfile 504 /etc/haproxy/errors/504.http

frontend B3-front
	bind 192.168.1.70:80
	default_backend B3-backend

backend B3-backend
	balance leastconn
	option httpclose
	option forwardfor
	compression algo gzip
	server web-1	192.168.1.73:80 check
	server web-2	192.168.1.74:80 check

listen SQLbackend
  balance leastconn
  bind 192.168.1.70:3306
  mode tcp
  option tcplog
  server sql1 192.168.1.75:3306 check
  server sql2 192.168.1.76:3306 check

Bases de données

ACID (Jim Gray)

Les propriétés ACID (/Atomicité, Cohérence, Isolation, Durabilité/) sont un ensemble de propriétés qui garantissent qu'une transaction est effectuée de manière fiable.

Ces propriétés et les technologies liées ont été définies et développées par Jim Gray à la fin des années 70 - note "Les mystères de l'Histoire de l'Informatique": Jim Gray a disparu en 2008 avec son voilier. On n'a jamais retrouvé ni l'un ni l'autre, et il a été déclaré mort /in absentia/ en 2012. À la base défini pour les systèmes transactionnels bancaires (TPF & cie), ce modèle s'applique particulièrement bien aux bases de données relationnelles.

Réplication active

Ici, chaque membre du cluster de réplication effectue les mêmes calculs que la machine maître.

Réplication passive

Ici, seul le primary exécute les calculs et ne propage que les modifications.

Réplication primary/replica (actif/passif), en cascade (postgreSQL), schéma primaire

Ici, une base de données maître gère les écritures et transmet les requêtes à sa(ses) base(s) de données replica. On peut ensuite répartir la charge des requêtes en lecture sur le primary et les replicas, mais seul le primary peut écrire des données. On peut y adjoindre un mécanisme de failover (postgreSQL notamment) permettant de /switcher/ un replica en primary si le primary ne répond plus.

On peut distinguer deux modes de fonctionnement:

Réplication Master/Master, multimaster (actif/actif)

Ici, les deux (ou plus) bases de données sont toutes primary. Dans le cas de MySQL/MariaDB, elles sont effectivement replicas les unes des autres (inception anyone? 😃).

Cependant cela peut poser des problèmes de concurrence dans certaines situations particulières, la prévention des conflits entre plusieurs transactions simultanées pouvant être particulièrement complexe.

On peut citer, pêle-mêle:

comme solutions de réplication multimaîtres.

Exemple: Setup MariaDB/Galera

MariaDB, fork de MySQL (suite au rachat de Sun par Oracle), est une des BDDs les plus utilisées. Elle propose, via un module nommé Galera, une solution de /clusterisation/ (désolé pour l'anglicisme/néologisme) de type master/master/master.../master. En situation réelle, on utilisera à minima 3 instances de Galera pour obtenir un bon quorum. Cependant, au vu de la situation actuelle, on utilisera 2 VMs pour "simuler" notre Cluster Galera.

On va commencer par installer mariadb sur les VMs de notre "cluster":

$ sudo apt update && sudo apt install mariadb-server mariadb-client

On va ensuite configurer Galera sur notre première VM (attention, il faudra repasser sur la config une fois le cluster en place). Dans cet exemple, mes deux VMs ont pour IP 192.168.1.43 et 192.168.1.27:

$ sudo vim /etc/mysql/conf.d/galera.cnf
[mysqld]
#####################
#mysql settings
#####################

log_bin=1
log_slave_updates=1
sync_binlog=1
binlog_format=ROW

default-storage-engine=innodb
innodb_autoinc_lock_mode=2
query_cache_size=1M
query_cache_type=OFF

innodb_flush_log_at_trx_commit=0
innodb_buffer_pool_size=256M

bind-address=0.0.0.0

#####################
# Galera settings
#####################
wsrep_provider=/usr/lib/libgalera_smm.so
wsrep_cluster_name="dynacluster"
# ici, on ne précise rien pour le moment. 
# On renseignera ensuite les IPs des membres
# du cluster. Mais pour l'init du cluster
# on laisse "gcomm://" vide
wsrep_cluster_address="gcomm://"
wsrep_sst_auth=sst_backup:supermotdepasse
wsrep_sst_method=rsync
wsrep_on=ON
wsrep_node_address="192.168.1.43"
wsrep_node_name="dyna-sql1.lidstah.info"
# on sauvegarde,quitte et on relance le service
$ sudo systemctl restart mysql

Sur notre client, on procède quasiment de même:

[mysqld]
#####################
#mysql settings
#####################

log_bin=1
log_slave_updates=1
sync_binlog=1
binlog_format=ROW

default-storage-engine=innodb
innodb_autoinc_lock_mode=2
query_cache_size=1M
query_cache_type=OFF

innodb_flush_log_at_trx_commit=0
innodb_buffer_pool_size=256M

bind-address=0.0.0.0

#####################
# Galera settings
#####################
wsrep_provider=/usr/lib/libgalera_smm.so
wsrep_cluster_name="dynacluster"
# cette fois ci on renseigne bien
# toutes les IPs des membres du cluster
# dès le début
wsrep_cluster_address="gcomm://192.168.1.43,192.168.1.27"
wsrep_sst_auth=sst_backup:supermotdepasse
wsrep_sst_method=rsync
wsrep_on=ON
wsrep_node_address="192.168.1.27"
wsrep_node_name="dyna-sql2.lidstah.info"
# on save/quit puis restart mysql
$ sudo systemctl restart mysql

Il ne nous reste plus qu'à terminer la configuration de notre premier serveur, en modifiant la ligne:

$ sudo vim /etc/mysql/conf.d/galera.cnf
wsrep_cluster_address="gcomm://"
# on remplace par:
wsrep_cluster_address="gcomm://192.168.1.43,192.168.1.27"
# save/quit/restart ;)
$ sudo systemctl restart mysql

On va maintenant tester que la réplication est fonctionnelle entre nos deux machines, par exemple en créant une DB sur la première machine, en vérifiant qu'elle est bien créée aussi sur la seconde, puis en la supprimant sur la seconde machine et en regardant si elle a bien été supprimée du premier:

sql1$ sudo mysql
CREATE DATABASE test;
sql2$ sudo mysql
SHOW DATABASES;
DROP DATABASE test;
sql1$ sudo mysql
SHOW DATABASES;

Génial, non? Maintenant, on placera en général 2 LBs (+keepalive 😉) en mode TCP "devant" notre cluster de BDD. Bonus, on obtient ainsi de la répartition de charge entre les machines de notre cluster de BDD 😃

Mon cluster est pété! comment faire?

Sur une des machines du cluster:

$ sudo galera_recovery
WSREP: Recovered position 849806a9-3ec1-11eb-bf1b-f71b6f48588f:4                                         
--wsrep_start_position=849806a9-3ec1-11eb-bf1b-f71b6f48588f:4 

Ici, la partie qui nous intéresse est 849806a9-3ec1-11eb-bf1b-f71b6f48588f:4

Ensuite, on édite le fichier suivant:

$ sudo vim /var/lib/mysql/grastate.dat
version: 2.1
uuid:    849806a9-3ec1-11eb-bf1b-f71b6f48588f
seqno:   -1
safe_to_bootstrap: 0

Et on remplace UUID par l'UID obtenue dans la commande galera_recovery - c'est l'UID avant les deux points et le numéro de séquence.

On remplace enfin la valeur de seqno par la valeur après les deux points (dans notre exemple, 4) et on modifie le paramètre safe_to_bootstrap à 1:

version: 2.1
uuid:    849806a9-3ec1-11eb-bf1b-f71b6f48588f
seqno:   4
safe_to_bootstrap: 1

On relance enfin mariadb, puis on fait de même sur les autres membres du cluster.

Tolérance de panne réseau

Comme on a vu précédemment avec le stockage et avec les bases de données, notre but est d'éliminer la plus grande partie des SPOF de notre infrastructure. Mais pour le moment il nous reste un gros SPOF: le réseau 😃

Pour cela, on va en général redonder notre équipement réseau (routeurs, switches) et utiliser des mécanismes d'agrégation de lien (/bonding/) pour conserver la connectivité et ce même en cas de défaillance d'un routeur, d'un switch, ou d'une interface réseau.

/bonding/ d'interfaces réseau sous GNU/Linux

Il est possible d'agréger plusieurs interfaces réseau afin qu'elles semblent se comporter comme une seule interface, avec sa propre adresse MAC. C'est ce qu'on appelle le /bonding/ d'interfaces. Suivant les situations, notamment le type de bond et le matériel utilisé il peut être nécessaire d'effectuer une configuration similaire côté commutateur (LACP notamment).

Pour déclarer un bond, on va d'abord choisir les interfaces réseau appartenant à ce bond et le déclarer dans la configuration (ici eno1 et ens1f0 vont être /bondées/ dans l'interface bond007 (joke inside©)):

sudo apt install ifenslave
vim /etc/network/interfaces

iface eno1 inet manual
  bond-master bond007

iface ens1f0 inet manual
  bond-master bond007

On déclare ensuite notre interface bond007:

auto bond007
iface bond007 inet static # manual si bridgé
  bond-mode 1 # active-backup
  bond-slaves eno1 ens1f0
  bond-miimon 100 # monitor (ms)
  bond-downdelay 250 # timeout (ms)
  bond-updelay 250 # up (ms)
  address ...
  gateway ...
  mtu ...

Les différents modes de bonding sous GNU/Linux sont:

Gestion du stockage

Réplication synchrone

Une réplication synchrone du stockage implique que les deux espaces de stockage de la réplication sont identiques au bloc près à tout moment.

Réplication asynchrone

La réplication asynchrone diffère de la réplication synchrone du stockage car elle permet un léger décalage entre la source et la cible.

Exemple: DRBD sous GNU/Linux (/Distributed Replicated Block Device/)

DRBD est un système de stockage distribué pour GNU/Linux fonctionnant en mode bloc. Il permet la réplication des /block devices/ entre plusieurs serveurs. Il utilise un module du kernel, et un ensemble d'utilitaires dans l'espace utilisateur permettant son administration.

Il effectue la réplication des données:

DRBD ne permet la réplication qu'entre deux noeuds jusqu'à la version 9.

Il peut être utilisé pour répliquer:

Je vous encourage chaudement à parcourir la documentation des commandes suivantes 😃 :

drbdadm (administration de DRBD)
drbdsetup (configuration du module DRBD)
drbdmeta (gestion des métadonnées)

Hyperviseurs et haute disponibilité

La plupart des clusters de virtualisation (Proxmox, VMWare, Nutanix, XCP-NG/Xen orchestra, Ganeti/Xen, etc) permettent de migrer à chaud des VMs répliquées en DRBD ou dont le disque virtuel est sur un stockage distant (NAS ou SAN par exemple), quasi instantanément (minus le temps de copie de la RAM de la VM d'un hyperviseur à un autre).

De même, ces solutions permettent de relancer automatiquement une machine virtuelle dont l'hyperviseur d'origine rencontrerait une défaillance. Il faut alors penser à implémenter un mécanisme de /fencing/ afin d'éviter une situation de ce type:

Sur du /stateless/ ce n'est pas embêtant outre mesure, sur du /stateful/ c'est très, très problématique 😃

Orchestrateurs de containers et haute disponibilité

Ici, les seuls SPOFs seraient les VMs control-planes de notre orchestrateur, les autres noeuds du cluster (VMs ou physiques) étant interchangeables et remplaçables à volonté tant qu'on dispose des ressources nécessaires. Dès lors il est important:

de notre solution d'orchestration.

Il faut ensuite définir les stratégies que l'orchestrateur aura à suivre en cas de défaillance d'un noeud du cluster, par exemple relancer les services (pods, déploiements, replica sets dans le cas de kubernetes) sur d'autres noeuds.

Mise en application : "lightweight" Kubernetes, K3S

Ici, nous allons mettre en place un mini-cluster K3S. K3S est une distribution de Kubernetes conçue pour s'exécuter sur des machines aux performances restreintes, raspberry Pi et clones, petites VMs, etc.

Nous allons d'abord créer deux machines virtuelles:

Idéalement, en production, on exécutera plusieurs control-planes (pour des raisons de haute-disponibilité, minimum 3 pour des raisons de quorum) et bien évidemment plusieurs workers. Si l'un de nos /workers/ rencontre une indisponibilité, ses charges de travail seront réparties entre les /workers/ restants - dans la limite de leur capacité.

Donc, notre cluster est capable de "s'auto-soigner". Cette solution permet d'assurer la haute-disponibilité de nos services, dont les différents éléments constitutifs seront en général déjà répliqués (par exemple plusieurs containers wordpress).

Fonctionnement d'un cluster Kubernetes

Séparation de nos charges de travail, isolation: Namespaces

On organisera nos charges de travail à l'intérieur d'espaces de noms (/namespaces/) qui, s'ils sont assez permissifs par défaut, permettent d'isoler les charges de travail entre elles avec des paramétrages assez fins, notamment réseau (/NetworkPolicies/). On peut aussi limiter l'utilisation CPU ou mémoire des applicatifs s'exécutant à l'intérieur d'un namespace. On peut tout à fait imaginer créer des namespaces du type:

C'est un concept très malléable qui permet d'organiser et isoler les différentes charges de travail que notre cluster kubernetes exécute 😃

Quelques commandes /kubectl/ pratiques:

  #lister les namespaces:
  kubectl get ns
  #créer un namespace
  kubectl create ns mon-namespace
  #Supprimer un Namespace
  #Et tout ce qu'il contient (FEAR)
  kubectl delete ns mon-namespace
  #Décrire un namespace
  kubectl describe ns mon-namespace

De plus, Kubernetes propose des mécanismes d'auto-dimensionnement des applicatifs, notamment l'/Horizontal Pods Autoscaler/ qui permet de créer de nouvelles instances d'un applicatif si celui-ci subit une charge importante, puis de détruire les instances non-utiles une fois la charge supplémentaire absorbée.

/NOTE: attention au tarif sur une solution Kubernetes "cloud" si vous le combinez avec du Vertical Autoscaling (qui va vous créer des VMs!)!!/

L'unité de base: le Pod

Un pod est très similaire à une /stack/ docker-compose. En effet, bien que la majorité des pods n'exécutent qu'un container, il est possible de créer des pods contenant plusieurs containers. Imaginons que la base de notre microservice API soit composée d'un petit serveur web et d'un cache Redis (par exemple pour stocker les résultats des requetes de notre API vers la base de données, afin de limiter la charge sur la BDD). On a plusieurs choix (plus ou moins pertinents):

Exemple: créer un pod avec l'image nginx dans l'espace de noms par défaut 'default', exécuter un shell dans le pod, et détruire le pod:

  kubectl run nginx --image=nginx
  kubectl exec -it nginx -- /bin/bash
  root@nginx:/# ls -al; exit
  kubectl delete pod nginx

C'est assez pratique pour tester rapidement qu'une image se lance ou pas, etc. Mais en général on préfèrera utiliser un déploiement (ou un daemonset, ou un statefulset) pour créer et gérer nos ressources.

Création/Suppression de nos pods: Déploiements, Daemonsets, StatefulSets et ReplicaSet

En effet, créer nos pods "à la main" ne serait pas pratique à l'échelle (si on doit en gérer plusieurs centaines par exemple), ni, non plus, d'un point de vue reproducibilité de notre infrastructure, sans compter les éventuelles erreurs humaines.

Au lieu de créer manuellement nos pods, nous allons définir des ressources de façon programmatique, dans des fichiers YAML, expliquant au cluster quel(s) pods nous voulons créer, dans quel(s) namespaces, avec quel(s) droits, quel(s) volumes de stockage, etc. Il en existe plusieurs suivant le besoin à couvrir:

De plus, nous pouvons définir des variables d'environnement dans la /spec/ de nos pods, qui seront passées à l'environnement du pod: par exemple une /envvar/ contenant l'adresse de notre base de données, une autre l'utilisateur, une autre son mot de passe (on utilisera généralement un secret Kubernetes pour passer ce genre de données sensibles à nos pods afin que ces informations ne risquent pas de se retrouver dans notre solution de versionnage (git via github par exemple...).

Comme nous décrivons nos ressources de façon programmatique, dans des fichiers YAML, il faut donc appliquer ces fichiers yaml via kubectl (ou des solutions de déploiement continu comme FluxCD, ArgoCD, etc):

  kubectl apply -f mon-deploiement.yml

De façon identique à la manipulation des pods vue précedemment, on peut manipuler ces ressources via kubectl (ici un déploiement):

  kubectl get deploy -n mon-namespace
  kubectl describe deploy -n mon-namespace mon-deploy
  kubectl delete deploy -n mon-namespace mon-deploy

Bien évidemment, je vous invite fortement à consulter la documentation en ligne de kubernetes et kubectl, très fournie (et parfois un peu absconce).

Réseau: Services, Load-Balancers et Ingresses

Si nos déploiements, damonsets, et statefulsets nous permettent de créer les différents pods composant notre service, ils ne nous permettent pas d'exposer ces services à d'autres pods du cluster (ClusterIP), ni à l'extérieur du cluster (c'est à dire à nos utilisateurs et utilisatrices, soit via une LoadBalancerIP, soit via un Ingress (qui sera exposé via un service de type LoadBalancer et une IP "publique")).

L'avantage d'un Ingress étant ici de n'utiliser qu'une seule IP publique pour distribuer entre plusieurs applicatifs. Cependant, il est plus complexe d'exposer d'autres services notamment en TCP pur ou en UDP (DNS) que directement avec un LoadBalancer. C'est la solution la plus simple, par contre, pour exposer des services depuis un cluster kubernetes /managé/ (GKS, Scaleway Kapsule, Azure Kubernetes, etc).

Stockage: Volumes Persistants (PV), Réclamation de PV (PersistentVolumeClaim) et montage dans nos pods

Il faut savoir que les systèmes de fichiers de nos pods sont immutables. Toute modification manuelle du contenu du système de fichiers des containers de nos pods ne survivra pas à un redéploiement (par exemple, une mise à jour) et ne se propagera pas entre les pods d'un meme service.

Afin de pouvoir faire persister et partager nos données entre nos pods et leurs futures versions, il est nécessaire de définir des Volumes Persistants (Persistent Volume) qui seront liés à nos pods via une réclamation de volume persistant (PersistentVolumeClaim), et assignés à nos pods via les directives /volumes/ et /volumeMounts/ dans nos déploiements, daemonsets et statefulsets.

Afin que les données soient accessibles depuis tous nos workers, on utilisera généralement du stockage distant, comme NFS, iSCSI, MiniIO, Netapp Trident, etc, ou des solutions de réplication des données entre nos noeuds comme LongHorn (iSCSI), Rook (Ceph), etc.

Dans notre cas nous utiliserons NFS avec provisionnement automatique des volumes persistants via l'utilitaire nfs-subdir-external-provisioner.

ConfigMaps et Secrets

On peut aussi vouloir passer un fichier de configuration (ex: /etc/haproxy/haproxy.cfg dans un pod HAProxy) à nos pods. Pour cela, on peut utiliser une ConfigMap, qui sera montée comme un fichier dans nos pods.

De plus, on peut aussi avoir besoin de "cacher" des informations sensibles (logins, mots de passes, certificats HTTPS et notamment la clef privée) et ne pas vouloir les /commit/ dans notre solution de gestion de versions (git, fossil, mercurial...). Pour celà, Kubernetes propose des ressources de type Secrets, qu'on pourra assigner à des variables d'environnements ou monter dans des fichiers à l'intérieur de nos pods afin qu'ils soient lus par nos applicatifs. On sort un peu du sujet du cours et du TP mais gardez le bien à l'esprit

RBAC: Role Based Access Control et comptes de service (serviceAccount)

Pour le moment on utilise kubectl et kubernetes avec le role RBAC cluster-admin. Bien que ce soit suffisant pour nos besoins limités, gardez à l'esprit que ce role a les pleins pouvoirs sur le cluster. En mixant droits RBAC, serviceAccounts et Tokens d'accès, on peut définir des droits très fins limitant ce qu'un utilisateur peut faire dans le cluster.

Par exemple, on peut imaginer un role RBAC donnant tous les pouvoirs dans un namespace donné - mais juste ce namespace, lié à un serviceAccount /michel-dev/. Michel pourra accéder et modifier les ressources de ce namespace en se connectant avec le Token lié à son serviceAccount.

Là aussi on sort un peu du "scope" de ce TP, mais je vous recommande plus que chaudement la lecture de la documentation de Kubernetes relative à RBAC, surtout si vous voulez utiliser Kubernetes en production 😃

####* Conclusion
Maintenant qu'on a abordé la théorie, passons à la pratique. Nous utiliserons k3s du fait de sa légèreté et simplicité d'utilisation qui nous permettra de nous concentrer sur nos recettes et nos applicatifs, mais il existe beaucoup d'autres distributions de kubernetes comme:

Installation du control-plane

Sur notre VM debian, il suffit simplement d'un (horrible curl | shell, mais bon...):

  curl -fsL https://get.k3s.io | sh -

Pour installer notre control plane.

On va noter/copier/etc le contenu du fichier:

  /var/lib/rancher/k3s/server/node-token

qui va nous permettre de joindre notre ou nos workers à notre cluster fraichement créé 😃

Installation des workers

Là aussi, ça va etre extremement simple! Si notre control-plane a pour IP 192.168.10.13, et que notre fichier node-token contenait abcdefgh12345678:

  curl -fsL https://get.k3s.io | \
  K3S_URL=https://192.168.10.13:6443 \
  K3S_TOKEN=abcdefgh12345678 \
  sh -

et voilà!

On va maintenant vérifier que nos nodes sont bien joints au cluster avec la commande:

  kubectl get nodes

qui devrait nous lister nos différents nodes (normalement juste un control-plane et un worker)

Installation de nfs-subdir-external-provisionner

On va simplement copier le fichier suivant dans un fichier nfs.yaml sur notre master, dans le chemin:

  /var/lib/rancher/k3s/server/manifests/nfs.yaml

Ce fichier doit contenir (adaptez la conf aux données de votre serveur NFS si vous avez dévié des choix proposés durant le TP:

apiVersion: helm.cattle.io/v1
kind: HelmChart
metadata:
  name: nfs
  namespace: default
spec:
  chart: nfs-subdir-external-provisioner
  repo: https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner
  targetNamespace: default
  set:
    nfs.server: 192.168.10.17
    nfs.path: /srv/nfs/k3s
    storageClass.name: nfs

Notre premier service: wordpress (lol)

Ici, nous allons créer notre premier service, wordpress, qui utilisera le cluster galera comme base de données:

apiVersion: v1
kind: Namespace
metadata:
  name: web
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: wp-pvc
  namespace: web
spec:
  storageClassName: "nfs"
  accessModes:
  - ReadWriteMany
  resources:
    requests:
      storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: wp
  name: wp
  namespace: web
spec:
  replicas: 1
  selector:
    matchLabels:
      app: wp
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: wp
    spec:
      volumes:
        - name: wp-data
          persistentVolumeClaim:
            claimName: wp-pvc
      containers:
      - name: wp
        image: wordpress
        env:
        - name: WORDPRESS_DB_HOST
          value: "192.168.10.10"
        - name: WORDPRESS_DB_USER
          value: "wp"
        - name: WORDPRESS_DB_PASSWORD
          value: "epsiepsi"
        - name: WORDPRESS_DB_NAME
          value: "wordpress"
        securityContext:
          allowPrivilegeEscalation: false
        resources:
          requests:
            memory: "256Mi"
            cpu: "100m"
        ports:
        - containerPort: 80
          protocol: TCP
        volumeMounts:
          - name: wp-data
            mountPath: /var/www/html
      restartPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
  name: wp-svc
  namespace: web
spec:
  type: ClusterIP
  ports:
  - port: 80
    targetPort: 80
    protocol: TCP
    name: web
  selector:
    app: wp
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: traefik
    traefik.ingress.kubernetes.io/frontend-entry-points: http
    #HTTPS setup with automatic redirect
    #traefik.ingress.kubernetes.io/frontend-entry-points: http,https
    #traefik.ingress.kubernetes.io/redirect-entry-point: https
    #traefik.ingress.kubernetes.io/redirect-permanent: "true"
  name: web-ingress
  namespace: web
spec:
  rules:
  - host: wordpress.lidstah.info
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: wp-svc
            port:
              number: 80