Comment GH-OST a sauvé nos mises en production

Julien Roy @ Bankin'

Bankin'

Fonctionnellement

  • Aide à la gestion d’argent

  • Agregation données bancaire

  • Interface de virements bancaire

  • B2C : Application iOS / Android ( bankin.com )

  • B2B : API SaaS ( bridgeapi.io )

Techniquement

  • Backend Java

  • Bases de données MySQL ( 3 To )

  • Hébergement AWS RDS Aurora

Problématique

  • Mise en production sans downtime

  • Migration de schéma de base de données

  • MySQL lock toute la table durant le processus

  • Peux prendre des heures pour certaines tables

Présentation

gh ost logo light 160

Triggerless online schema migration for MySQL

Fonctionnement

gh ost general flow

Utilisation

> gh-ost
  --host localhost \
  --database testdb \
  --user root \
  --password root \
  --table transactions \
  --alter 'ADD COLUMN is_deleted TINYINT' \

Démonstration

Automatisation

  • Intégré au pipeline de déploiement

  • Orchestré par FlywayDB

  • Utilisation de JDBC pour les CREATE TABLE

  • Utilisation de gh-ost pour les ALTER TABLE

Code

class GhostMigrationExecutor : MigrationExecutor {

    override fun execute(context: Context) {
        val jdbcTemplate = JdbcTemplate(context.connection)
        for (statement in sqlScript.sqlStatements) {
            if (isAlterTable(statement.sql)) {
                executeWithGhost(context, statement)
            } else {
                executeSql(jdbcTemplate, statement)
            }
        }
    }
    ...

Code

private fun executeSql(jdbc: Jdbc, statement: Statement) {
    val results = jdbc.executeStatement(statement.sql)
    ...
}

Code

private fun executeWithGhost(table: String, alter: String) {
    val cmds: MutableList<String> = ArrayList()
    cmds.add("gh-ost")
    cmds.add("--user=$user")
    cmds.add("--password=$password")
    cmds.add("--host=${connexion.host}")
    ...
    cmds.add("--table=$table")
    cmds.add("--alter=$alter")
    cmds.add("--chunk-size=10000")
    cmds.add("--cut-over=atomic")
    cmds.add("--execute")

    ProcessBuilder().inheritIO().command(cmds).start()

    ...

    try {
        val p = processBuilder.start()
        if (p.waitFor() != 0) {
            throw FlywayException("Migration failed !")
        }
    } catch (e: IOException) {
        throw FlywayException("Migration failed !", e)
    } catch (e: InterruptedException) {
        throw FlywayException("Migration failed !", e)
    }
}

Tips, bonnes pratiques

  • Tester sur un replicat

  • Tester en dry-run en Prod

  • Verifier les synchronisation entre les tables

  • Utiliser une session background ( screen , tmux )

Limitations

  • Bin log activé ( mode RBR )

  • Foreign key non supporté

  • Triggers non supporté

  • Tables doivent partager une clé unique

Alternatives

  • Percona : pt-online-schema-change

  • Facebook : OnlineSchemaChange

  • SoundCloud : lhm

  • MySQL : Online DDL ( bcp de limitations , pas de pause , throttle, or rollback )

  • AWS Aurora : FastDDL ( Seulement mode lab, non recommandé en Production )

Questions

Références