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?