Usando python3 com asyncio pra passar pelos problemas do mysql

Categoria: Python Publicado: Terça, 08 Dezembro 2020 Escrito por Helio Loureiro Imprimir

Foto do aerporto GRU no dia em que emigrei do Brasil.

Um dos trabalhos que faço como voluntário é manter alguns serviços "alternativos" na empresa.  Todos baseados em software livre.

Dos que são mantidos temos um mediawiki, um encurtador yourls e um etherpad-lite.   E esse último foi o que precisei mexer pra transferir pra um servidor novo.

Muitas pessoas gostam do etherpad-lite e o usam, mas devo dizer que por trás é um lixo.  Serviço porco.  Ele usa uma só tabela no MySQL/MariaDB com dois campos:

mysql> show tables;
+-----------------+
| Tables_in_paddb |
+-----------------+
| store           |
+-----------------+
1 row in set (0.00 sec)

mysql> desc store;
+-------+--------------+------+-----+---------+-------+
| Field | Type         | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| key   | varchar(100) | NO   | PRI |         |       |
| value | longtext     | YES  |     | NULL    |       |
+-------+--------------+------+-----+---------+-------+
2 rows in set (0.01 sec)

Sério.  2 campos.  E só.  Um é uma chave toscamente preparada pra ser chave primária e o resto... é valor.  Então o uso do DB só cresce, sem chances de uma manutenção decente.

Enquanto o uso do etherpad-lite é um dor nas costelas, o assunto é mais da migração dos dados.  Então continuando o assunto, o nosso DB chegou ao incrível valor de 13 GB.   Daí como faz a migração?  O básico é tirar um dump do DB antigo com mysqldump e carregar usando o comand mysql mesmo.

Algo como isso:

# mysql --host=remote-server.mysql.internal.com --port=1234 --user=sqluser --password=sqlpassword mydb < etherpad-migration-backup.sql

 que pra todos efeitos funciona.  O único problema foi que depois de passar 15 horas carregando o arquivo...

ERROR 2013 (HY000) at line 19057418: Lost connection to MySQL server during query

Dizem que não tem dor maior que a dor do parto.  Tem sim e chama-se carregar um dump de 13 GB por 15 horas e falhar.  Assim.

E o que restou fazer.  Bom... eu sabia a linha onde estava o arquivo, mas já tinham sido 15 horas num arquivo serial, que faz linha por linha.   Então decidi quebrar o dump em vários arquivos menores.  Dei um rápido "wc -l" no dump e vi que tinham exatamente 28993313 linhas.  Então era possível quebrar em 28 arquivos de 1 milhão de linhas cada.  E foi o que fiz.

Assim eu sabia que podia continuar do arquivo 20 em diante.  E depois resolvia como fazer com o que faltava.

# split -l 1000000 -d etherpad-migration-backup.sql etherpad-migration-backup.sql.
# ls -1 pad-migration-backup.sql.??
etherpad-migration-backup.sql.00
etherpad-migration-backup.sql.01
etherpad-migration-backup.sql.02
etherpad-migration-backup.sql.03
etherpad-migration-backup.sql.04
etherpad-migration-backup.sql.05
etherpad-migration-backup.sql.06
etherpad-migration-backup.sql.07
etherpad-migration-backup.sql.08
etherpad-migration-backup.sql.09
etherpad-migration-backup.sql.10
etherpad-migration-backup.sql.11
etherpad-migration-backup.sql.12
etherpad-migration-backup.sql.13
etherpad-migration-backup.sql.14
etherpad-migration-backup.sql.15
etherpad-migration-backup.sql.16
etherpad-migration-backup.sql.17
etherpad-migration-backup.sql.18
etherpad-migration-backup.sql.19
etherpad-migration-backup.sql.20
etherpad-migration-backup.sql.21
etherpad-migration-backup.sql.22
etherpad-migration-backup.sql.23
etherpad-migration-backup.sql.24
etherpad-migration-backup.sql.25
etherpad-migration-backup.sql.26
etherpad-migration-backup.sql.27
etherpad-migration-backup.sql.28
etherpad-migration-backup.sql.29

Com isso eu tive vários arquivos que eu podia subir em paralelo.  E foi o que fiz.  O resultado?  Não só um mas vários erros depois de algumas horas carregando.   Eu queria chorar.  No chuveiro.  Em posição fetal.  Só isso.

O maldito do comando mysql não te permite dar um replay descartando o que já existisse no DB, o que seria uma mão na roda nessas situações.  Então fiz isso com python.  Mas achei que seria lento demais manter serializado.  Então era um bom momento pra testar o asyncio, que usei pouquíssimo até hoje.  E valeu muito a pena.  Esse é o script final:

#! /usr/bin/python3

import sys
import pymysql.cursors
import asyncio

connection = pymysql.connect(host="remote-server.mysql.internal.com",
        port=1234,
        user="sqluser",
        password="sqlpassword",
        db="mydb",
        charset='utf8mb4',
        cursorclass=pymysql.cursors.DictCursor)

cursor = connection.cursor()
sema = asyncio.Semaphore(value=10)

async def commit_line(line):
    await sema.acquire()
    print(line)
    try:
        cursor.execute(line)
        connection.commit()
    except:
        print("Line (", line[:10],") already inserted")
        pass
    sema.release()


with open(sys.argv[1]) as sqlfile:
    loop = asyncio.get_event_loop()
    for line in sqlfile.readlines():
        loop.run_until_complete( commit_line(line) )
    loop.close()

Não está dos mais polidos, e com senha dentro, mas era uma coisa rápida pra resolver meu problema.  E resolveu.

Eu criei uma fila de 10 processos em paralelo pra rodar com: sema = asyncio.Semaphore(value=10)

o controle de acesso ao processo pra rodar é feito com sema.acquire() e sema.release().  Muito fácil.  Nem precisei criar um objeto Queue.

Dentro do loop do commit_line() eu sabugue um "enfia essa linha lá ou então continua".  Simples assim.  E funcionou.

Eu já tinha deixado o tmux aberto com várias janelas, uma pra cada arquivo, então foi só rodar o mesmo em cada uma que falhou.

Levou mais umas 2 ou 3 horas mas carregou tudo.

Foi lindo, não foi?

Acessos: 1172
Mastodon Mastodon
We use cookies

We use cookies on our website. Some of them are essential for the operation of the site, while others help us to improve this site and the user experience (tracking cookies). You can decide for yourself whether you want to allow cookies or not. Please note that if you reject them, you may not be able to use all the functionalities of the site.