Migracao de banco — Arduino e IoT — semana 6 do 3o trimestre
Semana 6 de 15· 3o trimestre · 05/09 a 10/12
Migracao de banco
O banco que roda na aula da ultima semana precisa virar o banco de amanha.
Aula 1 — Migracao: mudar o schema sem perder dado
Objetivos
- Explicar por que mudar o schema sem controle é a mesma coisa que editar a tabela com o produção em pé, e nomear o risco.
- Escrever uma migração versionada, com número da migração e ordem de aplicação, e rodar as duas do projeto.
- Fazer um backfill: adicionar coluna, preencher coluna nova e remover coluna velha, sem perder uma linha.
- Escrever o rollback de uma migração e rodar para desfazer, medindo o que o rollback custa.
- Ler o estado da tabela de migrações e dizer o que a equipe do dia 6 precisa escrever antes de rodar em produção.
Material
- 1 ESP32 DevKit V1 por dupla, com o cabo USB
- 1 LED da placa no
GPIO2, com o resistor de 330 ohm da bancada - 1 computador com o projeto do 2o trimestre aberto, com um banco vazio de testes
- Folha de papel por dupla, com a lista das migrações em ordem
- 1 computador com o monitor serial aberto a 115200
- Projetor, para o terminal com a lista de migrações aplicadas e pendentes
Conceitos
Migração é o schema como código, e o schema é código
Uma migração — migration, em inglês — é uma mudança de versionamento de schema escrita como código, com número, nome e ordem. A definição curta esconde a consequência importante: quando o schema é código, ele entra no git, ele é revisado antes de rodar, e ele tem o mesmo histórico que o resto do sistema.
Sem migração, o schema é conhecimento oral. A pessoa que sabe quais colunas existem é a pessoa que estava no dia em que criou a tabela, e ela não está mais. A coluna nova que o professor acabou de adicionar "na mão" vai desaparecer na próxima vez que alguém rodar o script de criação em um banco novo, e o sintoma aparece semanas depois, em uma máquina diferente, com ninguém presente.
O número da migração é a parte que dá ordem, e ele tem duas propriedades que o professor exige:
- É crescente e único. Duas migrações com o mesmo número não podem coexistir, e é por isso que o número tem data e sequência.
- É imutável. Depois que a migração rodou em algum lugar, o número e o conteúdo não mudam mais. Corrigir uma migração já aplicada é o que cria banco com duas histórias diferentes.
O versionamento de schema tem uma propriedade que parece paradoxicala e é central: o schema do banco é a parte do sistema que nunca deveria ser editada depois de aplicada. Todo o resto do código evolui com o tempo; o schema evolui por acréscimo, e cada acréscimo é uma linha no histórico. É o mesmo princípio do log de append que o professor vai ver na fila do dia 7: nada é reescrito, tudo é acrescentado.
A tabela de migrações é o instrumento que torna isso verificável, e é ela que responde à pergunta de rodar migração: o que está pendente. Ela tem quatro colunas na forma mínima: número, nome, instante e quem aplicou. Consultá-la responde a três perguntas que a equipe faz toda semana: qual versão está no ar, o que falta aplicar, e o que foi aplicado que o código novo não conhece. A linha aplicada é escrita dentro da mesma transação que a mudança — e é por isso que o item 3 da atividade importa: migração aplicada sem registro é migração que o sistema não sabe que existe.
Adicionar, remover e renomear coluna, uma por uma
As três operações do dia têm custos e riscos muito diferentes, e a tabela abaixo é o resumo que a turma copia.
| Operação | Risco com dado existente | Custo |
|---|---|---|
| adicionar coluna | baixo, se com valor padrão | instantâneo em tabela pequena |
| renomear coluna | médio, se o código antigo ainda usa o nome | dois comandos, na ordem certa |
| remover coluna | alto, se alguém ainda lê | instantâneo, mas o dado foi |
| índice novo | médio: trava a tabela durante a criação | proporcional ao tamanho |
O detalhe que separa adicionar coluna de adicionar coluna com segurança é o valor padrão. Uma coluna nova sem valor padrão em tabela com linhas existentes viola a restrição na hora da criação, e a migração falha. Uma coluna nova com valor padrão é criada com o valor em todas as linhas, e é por isso que o professor insiste: coluna nova sempre nasce com DEFAULT.
O dado existente é a segunda metade do problema, e não perder dado é o requisito, e a metade que o DEFAULT não resolve. A coluna existe e está preenchida com o valor padrão; o dado bom está na coluna antiga. Aí entra o backfill — o preencher coluna nova — que é a etapa que leva tempo e é a que a aula 2 vai preparar para não derrubar a produção.
A ordem do adicionar coluna e do preencher coluna nova é o que a turma erra com mais frequência, e a razão é a intuição de "já que é a mesma informação, faço os dois de uma vez". Não dá: preencher a coluna nova exige que ela exista primeiro. Uma migração com os dois passos na ordem certa é a segunda da lista da atividade, e a ordem errada é a que a equipe descobre em produção, com a migração reprovada pela metade.
Renomear coluna é a operação que parece inofensiva e não é. O nome novo funciona para o código novo, e o código antigo continua usando o nome antigo até alguém recompilar. O resultado é um sistema em que metade do código funciona e a outra metade dá erro de coluna inexistente — e o erro só aparece na rota que o teste de ponta a ponta do dia 5 esqueceu de cobrir.
Índice novo entra na lista porque criar um índice é uma das operações que não é instantânea em tabela grande: criar um índice percorre a tabela inteira, e enquanto ele é criado a tabela fica travada para escrita. É a operação que a aula 2 vai medir e que separa uma migração de cinco segundos de uma migração de vinte minutos.
Backfill: a etapa que o tempo de downtime depende
O backfill é o preenchimento da coluna nova a partir da coluna antiga, linha por linha. Ele é a etapa que transforma "adicionei uma coluna" em "os dados estão na coluna certa", e é a etapa cujo tempo decide se a migração pode rodar com o sistema no ar.
A conta é simples e o professor pede que a turma faça com números do projeto: mil linhas preenchem em milissegundos; dez mil em dois segundos; um milhão em três minutos; cinco milhões em quinze. O número de linhas por segundo varia com o tamanho da linha, com o número de índices na tabela e com o que o banco está fazendo ao mesmo tempo — e o professor avisa que preencher enquanto o sistema grava em cima é a pior combinação, porque a leitura do UPDATE e a escrita da aplicação disputam a mesma linha.
Daí vem a pergunta que a aula 2 responde: em vez de um UPDATE que varre a tabela inteira, o backfill em lotes de mil linhas, com pausa entre os lotes. O custo é o mesmo no total, e o ganho é que a tabela nunca fica travada por muito tempo: cada lote dura milissegundos, e a pausa devolve a escrita ao sistema. O número que a turma anota é o tamanho do lote, e ele é decisão de projeto.
O que o backfill não faz, e é a armadilha da aula 2: ele não resolve o problema do código antigo que ainda escreve na coluna velha. Enquanto houver código gravando na antiga, o backfill pode ser repetido indefinidamente e nunca ficar completo. A solução é a ordem em duas etapas — coluna nova primeiro, código depois, coluna velha por último — e a aula 2 é sobre isso.
Rollback, e o que ele custa de verdade
O rollback é a operação que desfaz a migração. E é aqui que a turma descobre a parte desconfortável: nem toda migração é reversível, e a irreversibilidade não é defeito da migração, é consequência da operação.
| Operação | Rollback possível? | O que o rollback custa |
|---|---|---|
| adicionar coluna | sim, remover a coluna | perda do que foi escrito nela depois |
| renomear coluna | sim, renomear de volta | nada, se ninguém gravou |
| remover coluna | não | o dado foi |
| backfill | sim, esvaziar | não recupera o valor original |
| criar índice | sim, remover o índice | nada, o custo é recriar depois |
A linha da remoção de coluna é a que o professor sublinha duas vezes: não há rollback para dado removido. Não há porque o banco não guarda histórico de coluna. Por isso a ordem correta é remover por último, e por isso a migração de remoção é a que precisa de backup antes de migrar, que é o item da aula 2.
A migração reversível é a que a equipe deve tentar escrever sempre que puder, e o critério é objetivo antes de rodar: se eu desfizer isso, o que o sistema perde? Se a resposta é "nada", a migração é reversível. Se a resposta é uma lista, a migração precisa de backup, e o backup precisa estar escrito no procedimento com o nome do arquivo e onde ele está.
O que o rollback não é, e o professor alerta: ele não é plano de Contingência para o caso de o banco inteiro cair. Ele desfaz uma mudança de schema. Restaurar o banco é outro procedimento, com outro nome, e ele depende do backup. Misturar os dois é como llamar de reversão o botão de desligar do servidor.
A escrita da migração reversível segue um formato que o professor mostra, e que é o mesmo do código versionado: número, nome, o que faz, o que desfaz, e se é reversível. A última linha é a que ninguém escreve e que a equipe inteira acaba agradecendo seis meses depois.
Atividade
Montagem:
LEDda placa noGPIO2, com o resistor de 330 ohm e o jumper para oGND.- Cabo USB conectado, monitor serial em 115200.
- Banco de testes vazio, com a tabela de leitura do 2o trimestre já criada e com mil linhas.
- Folha de papel com a lista das migrações em ordem de aplicação.
- Escreva no papel a tabela de migrações do seu projeto: número, nome, o que faz e se é reversível. Comece pela migração que criou a tabela de leitura. Quantas linhas a sua lista tem hoje?
- Escreva a migração
002: adicionar colunaumidade_fontecomDEFAULT 'dht11'. Rode e consulte a tabela de migrações. A linha da migração foi gravada? Ela está na mesma transação da alteração? - Escreva a migração
003: o backfill, preenchendoumidade_fontecom o valor deorigempara as mil linhas. Meça o tempo e calcule quantas linhas por segundo. Rode a consulta de contagem antes e depois: o número de linhas preenchidas bate com o total? - Escreva a migração
004: remover coluna antiga. Antes de rodar, escreva a resposta da pergunta do rollback: se eu desfizer isso, o que o sistema perde? Rode e meça de novo o tempo. Essa migração é reversível? - Escreva a migração
005e rode duas vezes. O que a segunda execução respondeu? A tabela de migrações recusou? Se não recusou, o que aconteceu com oDEFAULTda coluna? - Escreva o rollback da migração
002e rode. A coluna foi removida? O que aconteceu com o dado que foi escrito nela depois? Escreva a resposta em uma frase e diga se a002era reversível de verdade. - Meça o tempo da migração
003com mil linhas e depois com a tabela cheia de dez mil. Calcule o tempo estimado para um milhão de linhas, e escreva o número ao lado da migração no papel. - No seu projeto, escreva a frase de uma migração que não é reversível e diga por quê. Essa frase precisa aparecer no arquivo da migração, no começo. Escreva qual é.
Nota: 12 pontos. Critério de fim: a tabela de migrações com as cinco migrações escritas e a consulta da tabela de migrações mostrada, e os dois tempos do backfill — mil e dez mil linhas — com a conta por segundo.
Resolucao
O sketch da aula está em codigo/t3/dia06/aula1.ino.
#include <Arduino.h> // =========================================================================== // dia 6, aula 1: migracao. A placa nao tem banco, e por isso ela reproduz o // QUE a migracao faz: uma lista ordenada de passos, cada um com um numero, // um nome, e a informacao de se ele desfaz. // // A tabela de migracoes aqui e um arranjo de memoria, e a mesma forma que a do // servidor. O que a placa mostra e o conceito: a ordem, o numero, e a linha // de registro que fica gravada JUNTO com a mudanca. // =========================================================================== const int PIN_LED = 2; const unsigned long INTERVALO_MS = 5000; // --------------------------------------------------------------------------- // A TABELA DE MIGRACOES. Quatro campos por linha, que e o minimo que responde // as tres perguntas da semana: qual versao esta no ar, o que falta, o que foi // aplicado. A coluna "reversivel" e a que ninguem escreve e todo mundo precisa. // --------------------------------------------------------------------------- struct Migracao { int numero; const char* nome; const char* faz; // o que ela muda const char* desfaz; // o que o rollback faria bool reversivel; bool aplicada; }; Migracao MIGRACOES[] = { {1, "criar_tb_leitura", "cria a tabela de leitura", "remove a tabela", false, true}, {2, "add_umidade_fonte", "adiciona coluna com DEFAULT", "remove a coluna", true, true}, {3, "backfill_umidade_fonte", "preenche a coluna nova a partir da antiga", "esvazia a coluna", true, false}, {4, "drop_origem", "remove a coluna antiga", "NAO EXISTE: o dado foi", false, false}, {5, "add_indice_periodo", "cria indice sobre o periodo", "remove o indice", true, false} }; const int TOTAL_MIGRACOES = 5; // O que a placa do dia 5 e o dia 8 mediram: mil linhas por segundo no // preenchimento. E o numero que multiplica pela contagem da tabela. const int LINHAS_POR_SEGUNDO = 1000; // --------------------------------------------------------------------------- // A CONSULTA DA TABELA DE MIGRACOES. E a funcao que responde "qual versao do // schema esta no ar" — a pergunta que a equipe faz toda semana e que, sem esta // tabela, so quem trabalhou no dia da criacao do banco responde. // --------------------------------------------------------------------------- int versaoAplicada() { int maior = 0; for (int i = 0; i < TOTAL_MIGRACOES; i++) { if (MIGRACOES[i].aplicada && MIGRACOES[i].numero > maior) { maior = MIGRACOES[i].numero; } } return maior; } int contarPendentes() { int n = 0; for (int i = 0; i < TOTAL_MIGRACOES; i++) { if (!MIGRACOES[i].aplicada) n++; } return n; } // --------------------------------------------------------------------------- // A OPERACAO DE ROLLBACK. Ela tem uma condicao que o professor pede para ler: // migracao que remove dado nao tem rollback. O `if` e a regra, e nao um detalhe. // --------------------------------------------------------------------------- bool podeDesfazer(const Migracao& m) { if (m.reversivel) return true; Serial.printf(" migração %d (%s) NÃO é reversível: %s\n", m.numero, m.nome, m.desfaz); return false; } // O desenho da ordem que a atividade mede: adicionar, preencher, remover. void mostrarOrdenacao() { Serial.println(" ordem que a sequência de migrações tem de ter:"); Serial.println(" 1. adicionar a coluna nova, COM DEFAULT"); Serial.println(" 2. preencher a coluna nova a partir da antiga (backfill)"); Serial.println(" 3. só então remover a coluna antiga"); Serial.println(" remover antes de preencher perde o dado, e não há como"); Serial.println(" recuperá-lo: o banco não guarda histórico de coluna."); Serial.println(); } void setup() { pinMode(PIN_LED, OUTPUT); Serial.begin(115200); delay(200); Serial.println(); Serial.println("=== dia 6, aula 1: migração e versionamento de schema ==="); Serial.println("O schema é código. Código muda com número e histórico."); Serial.println(); Serial.println("tabela de migrações:"); Serial.println(" numero nome aplicada reversivel"); for (int i = 0; i < TOTAL_MIGRACOES; i++) { Serial.printf(" %6d %-20s %-8s %s\n", MIGRACOES[i].numero, MIGRACOES[i].nome, MIGRACOES[i].aplicada ? "sim" : "PENDENTE", MIGRACOES[i].reversivel ? "sim" : "nao"); } Serial.println(); Serial.printf(" versão aplicada agora: %d\n", versaoAplicada()); Serial.printf(" migrações pendentes: %d\n", contarPendentes()); Serial.println(); Serial.println("o que cada uma faz e o que ela desfaz:"); for (int i = 0; i < TOTAL_MIGRACOES; i++) { Serial.printf(" %d %s\n faz: %s\n desfaz: %s\n", MIGRACOES[i].numero, MIGRACOES[i].nome, MIGRACOES[i].faz, MIGRACOES[i].desfaz); } Serial.println(); } void loop() { digitalWrite(PIN_LED, HIGH); Serial.println("--- o que acontece se rodar a 003 e a 004 fora de ordem ---"); Serial.println(" se a 004 rodar antes da 003, a coluna que o backfill"); Serial.println(" deveria preencher não existe mais. O UPDATE falha e a"); Serial.println(" migração para no meio, com a 003 e a 004 aplicadas."); Serial.println(" O banco fica num estado que nenhuma versão do código usa."); Serial.println(); mostrarOrdenacionamento(); Serial.println("--- o rollback, linha a linha ---"); for (int i = 0; i < TOTAL_MIGRACOES; i++) { Serial.printf(" %d %-20s ", MIGRACOES[i].numero, MIGRACOES[i].nome); if (podeDesfazer(MIGRACOES[i])) { Serial.print("pode desfazer: "); Serial.println(MIGRACOES[i].desfaz); } } Serial.println(); Serial.println("--- o custo do backfill, medido ---"); int mil = 1000, dezMil = 10000, umMilhao = 1000000; Serial.print(" 1.000 linhas: "); Serial.print(mil / LINHAS_POR_SEGUNDO); Serial.println(" s"); Serial.print(" 10.000 linhas: "); Serial.print(dezMil / LINHAS_POR_SEGUNDO); Serial.println(" s"); Serial.print(" 1.000.000 linhas: "); Serial.print(umMilhao / LINHAS_POR_SEGUNDO); Serial.println(" s"); Serial.println(); Serial.println(" é esse número que decide se a migração roda com o ar"); Serial.println(" no ar ou precisa de duas etapas, que é a aula 2."); Serial.println(); digitalWrite(PIN_LED, LOW); delay(INTERVALO_MS); }
Por que assim e não de outro jeito. A struct Migracao tem cinco campos, e o quinto — reversivel — é o que a equipe esquece. Os quatro primeiros respondem "o que aconteceu"; o quinto responde "o que eu faço se der errado", e ele é uma decisão que precisa ser escrita antes de a migração rodar, não depois do incidente. A mesma exigência vale para o campo desfaz: ele é a resposta escrita de rollback, e uma migração sem desfaz preenchido é uma migração em que ninguém pensou em como voltar atrás.
A versaoAplicada percorre a lista e devolve o maior número aplicado, e não a contagem. A diferença importa e o professor a explica: se o número 4 foi aplicado e o 3 não, a contagem de aplicadas dá dois e o maior número dá quatro. Quem usa a contagem como versão está mentindo sobre o estado do banco — e a próxima migração, que procura "maior número aplicado", vai rodar depois da 4 e não depois da 3. A versão é o número, não a quantidade.
O podeDesfazer é a função mais curta do arquivo e a que o professor lê com mais calma. Ela imprime a migração que não pode ser desfeita com a explicação do motivo, e não apenas false. A diferença é o que a equipe vai ler no incidente: "não é reversível: Não EXISTE: o dado foi" diz o que aconteceu, e um false seco obriga a pessoa a abrir o banco para descobrir.
O mostrarOrdenacao imprime a ordem das três etapas, e ela está no sketch como texto por um motivo que o professor admite: a ordem é conhecimento de requisito, e conhecimento de requisito no arquivo de migração é conhecimento que sobrevive à mudança de equipe. A sequência 002 adicionar, 003 preencher, 004 remover aparece na lista de migrações acima, e o nome de cada uma já diz a ordem — drop_origem é o número 4 porque é o quarto passo, não porque é o quarto que alguém pensou.
Os três números do custo do backfill usam a constante LINHAS_POR_SEGUNDO e não números escritos à mão, e isso é o que permite ao aluno mexer na constante e ver os três números mudarem. É a diferença entre uma tabela que demonstra e uma tabela que mede: com a constante, o item 7 da atividade — trocar por mil e depois por dez mil — vira uma edição de uma linha.
O LED acende e apaga a cada volta como sinal de vida. O intervalo de cinco segundos é escolha de aula: o professor precisa ler três blocos de texto na tela sem pausar, e cada bloco tem seis ou sete linhas.
Criterios de correcao
| Critério | Pontos |
|---|---|
| A tabela de migrações escrita, com número, nome, o que faz e reversibilidade | 2 pontos |
A migração 002 rodada, com a consulta da tabela de migrações colada | 2 pontos |
A migração 003 de backfill rodada, com a contagem antes e depois | 2 pontos |
A migração 004 rodada, com a resposta escrita sobre a perda no rollback | 2 pontos |
A migração 005 rodada duas vezes, com o que a segunda respondeu | 2 pontos |
O rollback da 002 rodado, com o efeito no dado descrito | 1 ponto |
| Os dois tempos de backfill, com a conta de linhas por segundo | 1 ponto |
Erros comuns
| Erro | Como aparece | Correção |
|---|---|---|
| Editar a tabela direto no banco | ALTER TABLE executado no terminal e nada no git | "O schema é código. A mudança que não está no git volta a desaparecer no próximo banco novo, e ninguém sabe que existiu." |
Coluna nova sem DEFAULT em tabela com linhas | A migração falha com violação de restrição | "Coluna nova nasce com valor padrão. Sem ele, a criação quebra em qualquer tabela que já tenha linha." |
| Remover a coluna antiga antes do backfill | O UPDATE do backfill falha com coluna inexistente | "A ordem é adicionar, preencher, remover. Remover antes perde o dado e o banco não guarda histórico para recuperar." |
| Migrar corrigindo a versão antiga | O arquivo da migração 002 foi editado depois de aplicada | "Migração aplicada é imutável. Você cria a 006. Editar a 002 produz bancos com histórias diferentes e o rollback perde o sentido." |
| Registro da migração fora da transação | A alteração foi aplicada e a linha não foi gravada | "A linha na tabela de migrações vai na mesma transação da mudança. Se ela falhar, a migração inteira falhou." |
| Versão calculada por contagem | SELECT COUNT(*) para saber a versão | "A versão é o maior número aplicado, não a quantidade. Com a 4 aplicada e a 3 pendente, a contagem dá dois e engana quem roda a próxima." |
| Dizer que toda migração tem rollback | A equipe promete reversão para a que remove coluna | "Remover coluna não tem rollback: o dado foi. A pergunta antes de rodar é 'se eu desfizer, o que se perde?', e a resposta tem que estar no arquivo." |
| Backfill sem medir o tempo | A migração entra no deploy sem saber quanto vai levar | "Meça com mil linhas e extrapole. O tempo do backfill é o que decide entre rodar com o ar no ar e fazer em duas etapas." |
Desafio extra
Escreva a migração do seu projeto que adiciona a coluna e faz o backfill, e meça o tempo do UPDATE com a tabela cheia do seu banco de testes. Depois escreva a versão em lotes — mil linhas por lote, com pausa de dois segundos entre um lote e o seguinte — e meça o tempo total e o tempo que cada lote segura a tabela. Escreva na frente os dois números e a frase que justifica a escolha. Em seguida, escreva o rollback de cada uma das duas versões e diga o que cada um perde. A pergunta que o professor espera de resposta: qual das duas versões vocêrodaria com o sistema no ar, e qual delas você rodaria com o sistema parado, e o que decide essa escolha além do tempo medido?
A resolucao, compilada
// Aula 1 do dia 6 do 3o trimestre de ESP32 e IoT. // Gerado a partir da secao ## Resolucao de conteudo/t3/dia06/aula1.md. // Se mudar o codigo, mude no .md e regere: o .ino e copia do .md. #include <Arduino.h> // =========================================================================== // dia 6, aula 1: migracao. A placa nao tem banco, e por isso ela reproduz o // QUE a migracao faz: uma lista ordenada de passos, cada um com um numero, // um nome, e a informacao de se ele desfaz. // // A tabela de migracoes aqui e um arranjo de memoria, e a mesma forma que a do // servidor. O que a placa mostra e o conceito: a ordem, o numero, e a linha // de registro que fica gravada JUNTO com a mudanca. // =========================================================================== const int PIN_LED = 2; const unsigned long INTERVALO_MS = 5000; // --------------------------------------------------------------------------- // A TABELA DE MIGRACOES. Quatro campos por linha, que e o minimo que responde // as tres perguntas da semana: qual versao esta no ar, o que falta, o que foi // aplicado. A coluna "reversivel" e a que ninguem escreve e todo mundo precisa. // --------------------------------------------------------------------------- struct Migracao { int numero; const char* nome; const char* faz; // o que ela muda const char* desfaz; // o que o rollback faria bool reversivel; bool aplicada; }; Migracao MIGRACOES[] = { {1, "criar_tb_leitura", "cria a tabela de leitura", "remove a tabela", false, true}, {2, "add_umidade_fonte", "adiciona coluna com DEFAULT", "remove a coluna", true, true}, {3, "backfill_umidade_fonte", "preenche a coluna nova a partir da antiga", "esvazia a coluna", true, false}, {4, "drop_origem", "remove a coluna antiga", "NAO EXISTE: o dado foi", false, false}, {5, "add_indice_periodo", "cria indice sobre o periodo", "remove o indice", true, false} }; const int TOTAL_MIGRACOES = 5; // O que a placa do dia 5 e o dia 8 mediram: mil linhas por segundo no // preenchimento. E o numero que multiplica pela contagem da tabela. const int LINHAS_POR_SEGUNDO = 1000; // --------------------------------------------------------------------------- // A CONSULTA DA TABELA DE MIGRACOES. E a funcao que responde "qual versao do // schema esta no ar" — a pergunta que a equipe faz toda semana e que, sem esta // tabela, so quem trabalhou no dia da criacao do banco responde. // --------------------------------------------------------------------------- int versaoAplicada() { int maior = 0; for (int i = 0; i < TOTAL_MIGRACOES; i++) { if (MIGRACOES[i].aplicada && MIGRACOES[i].numero > maior) { maior = MIGRACOES[i].numero; } } return maior; } int contarPendentes() { int n = 0; for (int i = 0; i < TOTAL_MIGRACOES; i++) { if (!MIGRACOES[i].aplicada) n++; } return n; } // --------------------------------------------------------------------------- // A OPERACAO DE ROLLBACK. Ela tem uma condicao que o professor pede para ler: // migracao que remove dado nao tem rollback. O `if` e a regra, e nao um detalhe. // --------------------------------------------------------------------------- bool podeDesfazer(const Migracao& m) { if (m.reversivel) return true; Serial.printf(" migração %d (%s) NÃO é reversível: %s\n", m.numero, m.nome, m.desfaz); return false; } // O desenho da ordem que a atividade mede: adicionar, preencher, remover. void mostrarOrdenacao() { Serial.println(" ordem que a sequência de migrações tem de ter:"); Serial.println(" 1. adicionar a coluna nova, COM DEFAULT"); Serial.println(" 2. preencher a coluna nova a partir da antiga (backfill)"); Serial.println(" 3. só então remover a coluna antiga"); Serial.println(" remover antes de preencher perde o dado, e não há como"); Serial.println(" recuperá-lo: o banco não guarda histórico de coluna."); Serial.println(); } void setup() { pinMode(PIN_LED, OUTPUT); Serial.begin(115200); delay(200); Serial.println(); Serial.println("=== dia 6, aula 1: migração e versionamento de schema ==="); Serial.println("O schema é código. Código muda com número e histórico."); Serial.println(); Serial.println("tabela de migrações:"); Serial.println(" numero nome aplicada reversivel"); for (int i = 0; i < TOTAL_MIGRACOES; i++) { Serial.printf(" %6d %-20s %-8s %s\n", MIGRACOES[i].numero, MIGRACOES[i].nome, MIGRACOES[i].aplicada ? "sim" : "PENDENTE", MIGRACOES[i].reversivel ? "sim" : "nao"); } Serial.println(); Serial.printf(" versão aplicada agora: %d\n", versaoAplicada()); Serial.printf(" migrações pendentes: %d\n", contarPendentes()); Serial.println(); Serial.println("o que cada uma faz e o que ela desfaz:"); for (int i = 0; i < TOTAL_MIGRACOES; i++) { Serial.printf(" %d %s\n faz: %s\n desfaz: %s\n", MIGRACOES[i].numero, MIGRACOES[i].nome, MIGRACOES[i].faz, MIGRACOES[i].desfaz); } Serial.println(); } void loop() { digitalWrite(PIN_LED, HIGH); Serial.println("--- o que acontece se rodar a 003 e a 004 fora de ordem ---"); Serial.println(" se a 004 rodar antes da 003, a coluna que o backfill"); Serial.println(" deveria preencher não existe mais. O UPDATE falha e a"); Serial.println(" migração para no meio, com a 003 e a 004 aplicadas."); Serial.println(" O banco fica num estado que nenhuma versão do código usa."); Serial.println(); mostrarOrdenacao(); Serial.println("--- o rollback, linha a linha ---"); for (int i = 0; i < TOTAL_MIGRACOES; i++) { Serial.printf(" %d %-20s ", MIGRACOES[i].numero, MIGRACOES[i].nome); if (podeDesfazer(MIGRACOES[i])) { Serial.print("pode desfazer: "); Serial.println(MIGRACOES[i].desfaz); } } Serial.println(); Serial.println("--- o custo do backfill, medido ---"); int mil = 1000, dezMil = 10000, umMilhao = 1000000; Serial.print(" 1.000 linhas: "); Serial.print(mil / LINHAS_POR_SEGUNDO); Serial.println(" s"); Serial.print(" 10.000 linhas: "); Serial.print(dezMil / LINHAS_POR_SEGUNDO); Serial.println(" s"); Serial.print(" 1.000.000 linhas: "); Serial.print(umMilhao / LINHAS_POR_SEGUNDO); Serial.println(" s"); Serial.println(); Serial.println(" é esse número que decide se a migração roda com o ar"); Serial.println(" no ar ou precisa de duas etapas, que é a aula 2."); Serial.println(); digitalWrite(PIN_LED, LOW); delay(INTERVALO_MS); }
Sem saída de compilação gravada. Rode python3 validar.py -t 3 dia06 aula1.
Aula 2 — Preparar a migração antes de ela quebrar a produção
Objetivos
- Reconhecer a migração que quebra, e dizer em qual das duas direções do código ela quebra primeiro.
- Aplicar a ordem de expandir e migrar, e explicar por que código antigo com coluna nova e código novo sem coluna são estados diferentes.
- Escrever a migração de
NOT NULLem duas etapas, e medir o downtime de cada uma. - Rodar a migração em cópia do banco de dados antes de rodar na de verdade, e comparar os dois resultados.
- Escrever o procedimento de backup antes de migrar com nome de arquivo, e o ler o schema que evita migração escrita contra banco errado.
Material
- 1 ESP32 DevKit V1 por dupla, com o cabo USB
- 1 LED da placa no
GPIO2, com o resistor de 330 ohm da bancada - 1 computador com o projeto do 2o trimestre aberto, com dois bancos: o de teste e uma cópia dele
- Folha de papel por dupla, com o desenho das duas etapas
- 1 computador com o monitor serial aberto a 115200
- Projetor, para o terminal com os dois schemas lado a lado
Conceitos
A migração que quebra tem uma direção
Uma migração que quebra é a que produz um schema que o código existente não sabe usar, ou que o banco recusa. A frase importante é que ela tem direção: existe um estado anterior e um estado novo, e o defeito aparece na transição entre os dois, em uma ordem específica.
O caso que abre a aula é o mais comum: transformar a coluna nova em NOT NULL. O comando é sintaticamente correto e a intenção é legítima — a coluna é obrigatória. O banco recusa, e a mensagem diz que existem linhas com nulo. O que o banco está fazendo é proteger a integridade do dado: a coluna nova tem valor padrão desde o backfill, mas o backfill rodou em mil linhas e a tabela tem duzentas mil.
Coluna NOT NULL em tabela cheia é o nome técnico do problema, e ele tem uma solução que não é "tirar o NOT NULL para sempre". A solução é a migração em duas etapas: primeiro, em lote, preencher os nulos que restaram; depois, aplicar o NOT NULL. São duas migrações, dois números, e a segunda só é segura porque a primeira terminou.
A diferença entre as duas etapas é o que o professor mede na lousa, e é o número que decide a estratégia. Preencher em lote de mil linhas custa milissegundos de escrita e não trava a tabela. Aplicar o NOT NULL custa uma varredura para verificar que não há nulos, e trava a tabela durante a verificação — que é proporcional ao tamanho. Em tabela de duzentas mil linhas, a verificação leva segundos; em tabela de cinco milhões, leva minutos. O downtime é o nome do que a equipe perde enquanto a tabela está travada, e ele é a métrica que decide se a migração entra no horário de ninguém.
O O default em coluna nova é o que torna a etapa um possível, e o professor volta nele com uma frase: coluna nova com DEFAULT nunca tem nulo, porque o banco preenche as linhas existentes com o valor padrão. Se há nulo, ou o DEFAULT não foi aplicado, ou o backfill não rodou, ou o DEFAULT foi removido depois. Nulo em coluna que deveria ter padrão é sintoma, e o sintoma tem três causas e a turma precisa saber quais são antes de consertar.
Os quatro estados da migração, e só um funciona
Expandir e migrar é o nome da ordem que existe porque, durante a troca de schema, o sistema passa por estados que antes não existiam. São quatro combinações de código e coluna, e o professor desenha as quatro na lousa antes de qualquer comando:
| Estado | Código antigo | Código novo | O que acontece |
|---|---|---|---|
| inicial | sem coluna | sem coluna | tudo normal |
| expandir | com coluna | sem coluna | o código antigo ignora a coluna extra |
| migrar | com coluna | com coluna | ambos escrevem, o backfill preenche |
| contrair | sem coluna | com coluna | quebra: o código novo não acha a coluna |
A linha "expandir" é a que responde à pergunta código antigo com coluna nova, e ela é segura por um motivo que vale a pena entender: uma coluna extra não atrapalha um INSERT que não a menciona. O código antigo continua gravando nas colunas que conhece, e a coluna nova fica esperando. O único efeito é que a coluna nova não recebe dado daquele caminho — e é exatamente por isso que o backfill vem depois.
A linha "contrair" é o estado de código novo sem coluna, e ela é a que quebra. O código novo faz INSERT com a coluna nova, a coluna não existe, e toda requisição falha. Zerar downtime é reduzir a janela de quebra, que é do tamanho do tempo entre publicar o código e aplicar a migração — que é exatamente o tempo que zerar downtime tenta reduzir.
A ordem correta, e ela é a sequência que o professor escreve na lousa como regra do dia:
- Expandir — adicionar a coluna nova, com
DEFAULT, e a colunaNOT NULLsó se a nova também puder nascer preenchida. - Migrar — backfill em lotes, e depois o código novo passa a escrever na coluna nova.
- Contrair — só depois que todo o código lê e escreve na coluna nova, remover a coluna antiga.
A A compatibilidade entre código e schema não é garantida por ninguém: ela é resultado da ordem. Um código novo assume que a coluna existe; um código antigo não sabe que ela existe. As duas suposições só são verdadeiras ao mesmo tempo no meio da sequência, e é por isso que cada etapa é uma migração separada e versionada.
Ler o schema, e a migração escrita contra o banco errado
O erro que a equipe do dia 6 paga mais caro não é a migração lenta. É a migração escrita contra o banco errado, e ele começa com uma pergunta que ninguém faz: qual é o schema deste banco agora?
Ler o schema é a operação que responde isso, e ela é a primeira coisa a fazer antes de escrever qualquer migração. Não é a mesma coisa que ler o script de criação, porque o script diz o que o banco *deveria* ter e o banco diz o que ele *tem*. Entre os dois há todas as mudanças que alguém aplicou direto no terminal, todas as migrações que rodaram em outra máquina e todas as divergências acumuladas desde o primeiro deploy.
A fonte da verdade para o schema é o banco, e a fonte da verdade para a história é a tabela de migrações. São duas leituras, e a segunda é a que responde "por que o banco está assim". A turma precisa das duas, e a aula entrega o hábito de fazer as duas antes de escrever.
Testar migração antes de rodar em produção tem quatro partes, e o professor as pede na ordem:
- Rodar migração em cópia — um banco clonado, nunca o de verdade.
- Rodar a migração na cópia e medir o tempo.
- Comparar o schema resultante com o que o código novo espera.
- Só então rodar na de verdade.
O quarto passo tem uma condição que o professor destaca: rodar na de verdade só depois que a cópia passou. E a cópia tem que ser de hoje, não da semana passada, porque o schema muda todo dia e a cópia desatualizada testa a migração contra um banco que não existe mais.
O que a cópia pega, e que a leitura do schema não pega: migração que depende de dados específicos, índice que não existe no banco de teste, coluna com tipo diferente do que o script declara. São três defeitos que só aparecem com dado dentro, e é por isso que a cópia precisa da quantidade de dado também — uma cópia com dez linhas não exercita o backfill de verdade.
Backup antes de migrar, com nome de arquivo
O backup antes de migrar é a rede debaixo de todas as outras redes, e o professor trata como procedimento, não como sugestão. A regra que ele escreve: antes de rodar em produção, existe um arquivo de backup, com nome, com caminho, com a data, e alguém sabe onde ele está.
Migração irreversível é a que justifica o backup, e ela se reconhece pela resposta da pergunta do dia anterior: "se eu desfizer isso, o que o sistema perde?". Perder coluna, perder tabela, transformar dado — são as três respostas que exigem backup. Adicionar coluna com DEFAULT não exige, e é por isso que a equipe pode correr com a etapa um.
O backup que serve não é o do arquivo compactado de três horas atrás. É o de antes da migração, com o instante mais próximo possível, e ele só é confiável se a equipe sabe restaurá-lo. E há um detalhe que o professor aponta e que ninguém testa: restaurar um backup em produção é uma operação que trava o serviço, e uma restauração que nunca foi testada é uma hipótese. O procedimento tem uma linha a mais que quase nenhum projeto escreve: quem restaura, e em quanto tempo.
O que a migração em duas etapas permite que a migração de uma etapa não permite é justamente rollback. Com a coluna nova adicionada e o código ainda antigo, o rollback é remover a coluna — e o dado que o código novo tinha escrito nela é perdido, o que é aceitável porque o código novo ainda não rodou. Com tudo de uma vez, o rollback exigiria desfazer a remoção, e o dado removido não volta. São as duas frases que o professor pede no fim da aula.
A última coisa que a aula entrega é a lista do que precisa estar escrito antes de o procedimento existir, e ela cabe em cinco itens que o professor enumera na lousa: backup feito e testado, cópia do banco de hoje, tempo medido, ordem dos passos, e quem faz. Sem os cinco, o procedimento é um deseo, e o incidente do dia seguinte é o que escreve a versão real dele.
Atividade
Montagem:
LEDda placa noGPIO2, com o resistor de 330 ohm e o jumper para oGND.- Cabo USB conectado, monitor serial em 115200.
- Dois bancos: o de teste e uma cópia dele feita agora, com duas mil linhas em ambos.
- Folha de papel com os quatro estados da migração e a ordem das etapas.
- Ler o schema dos dois bancos e colar as duas saídas aqui. Eles são iguais? Se não, qual é a diferença e como ela apareceu?
- Escreva no papel os quatro estados da migração em duas etapas, com a coluna e o código em cada um. Em qual deles o sistema quebra? Escreva a linha do seu código que quebra nesse estado.
- Tente rodar a migração de
NOT NULLna cópia, com a tabela cheia. O que o banco respondeu? Quantas linhas têm nulo? Copie a mensagem do banco. - Escreva a migração de preencher em lote — mil linhas por lote — e meça três números: o tempo de cada lote, o número de lotes e o tempo total. Anote o tempo que a tabela fica travada por lote.
- Agora rode a migração de
NOT NULLdepois do preenchimento. Mede o tempo de novo. Compare os dois tempos: o do preenchimento e o doNOT NULL. Qual dos dois é o downtime real da operação? - Escreva a rollback da etapa um e da etapa dois. O que cada uma perde? Marque qual delas é irreversível e escreva por quê.
- Rode a migração no banco de verdade, com o servidor Node parado, e meça o tempo. Depois rode com o servidor no ar, rodando, e meça de novo. Os dois números são iguais? O que muda quando há escrita concorrente durante o preenchimento?
- Escreva, no seu projeto, o procedimento de deploy de schema em cinco linhas: backup, cópia, migra a cópia, compara, migra a de verdade. Quem executa cada linha, e o que acontece se a linha 3 falhar?
Nota: 12 pontos. Critério de fim: as duas leituras de schema coladas e comparadas, os três tempos do item 4 com o tempo de travada por lote, e os dois tempos do item 7 — banco parado e banco com escrita concorrente.
Resolucao
O sketch da aula está em codigo/t3/dia06/aula2.ino.
#include <Arduino.h> // =========================================================================== // dia 6, aula 2: preparar a migracao antes de ela quebrar a producao. // // A placa NAO tem banco e NAO roda migracao. O que ela faz e o que importa: // mostrar os QUATRO estados que o sistema atravessa durante a troca de schema, // e medir o custo de cada etapa do preenchimento em lote. // // O numero de linhas por segundo e o mesmo do dia 6 aula 1, e e ele que // transforma "a tabela tem muito dado" em "a migracao leva este tempo". // =========================================================================== const int PIN_LED = 2; const unsigned long INTERVALO_MS = 5000; // --------------------------------------------------------------------------- // OS QUATRO ESTADOS. Coluna e codigo sao duas dimensoes, e a combinacao que // quebra e a ultima: codigo novo sem coluna. // // `funciona` e o que o professor pede para a turma prever antes de rodar. // --------------------------------------------------------------------------- struct Estado { const char* nome; bool colunaNovaExiste; bool codigoNovoEstaNoAr; bool funciona; const char* oQue Acontece; }; const Estado ESTADOS[] = { {"inicial", false, false, true, "tabela como o codigo espera"}, {"expandir", true, false, true, "codigo antigo ignora a coluna extra"}, {"migrar", true, true, true, "ambos gravam; o backfill preenche"}, {"contrair", false, true, false, "codigo novo pede coluna que sumiu"} }; const int TOTAL_ESTADOS = 4; // --------------------------------------------------------------------------- // O CUSTO DE CADA ETAPA. Estes sao os numeros que o professor mediu na copia // do banco, e a ordem deles e a ordem do downtime. // --------------------------------------------------------------------------- // O que o preenchimento em lote segura a tabela, por lote. const int TAMANHO_LOTE = 1000; const int LINHAS_POR_SEGUNDO = 1000; // O NOT NULL nao preenche nada: ele VARRE a tabela para verificar que nao ha // nulo. E por isso que ele e o downtime grande, e nao o preenchimento. const int LINHAS_POR_SEGUNDO_VERIFICACAO = 5000; int tempoDePreenchimento(int linhas) { return (linhas + LINHAS_POR_SEGUNDO - 1) / LINHAS_POR_SEGUNDO; } int tempoDeVerificacao(int linhas) { return (linhas + LINHAS_POR_SEGUNDO_VERIFICACAO - 1) / LINHAS_POR_SEGUNDO_VERIFICACAO; } void mostrarEstados() { Serial.println(" estado coluna codigo novo funciona o que acontece"); for (int i = 0; i < TOTAL_ESTADOS; i++) { Serial.printf(" %-11s %-7s %-12s %-9s %s\n", ESTADOS[i].nome, ESTADOS[i].colunaNovaExiste ? "sim" : "nao", ESTADOS[i].codigoNovoEstaNoAr ? "sim" : "nao", ESTADOS[i].funciona ? "sim" : "NAO", ESTADOS[i].oQue Acontece); } Serial.println(); Serial.println(" so o ultimo quebra: codigo novo sem a coluna."); Serial.println(" e por isso que a ordem e banco primeiro, codigo depois."); Serial.println(); } void mostrarDuasEtapas(int linhas) { Serial.println("--- a migracao que quebra, em duas etapas ---"); Serial.print(" linhas na tabela: "); Serial.println(linhas); Serial.println(); Serial.println(" ETAPA 1: preencher os nulos, em lote"); Serial.print(" lotes de "); Serial.print(TAMANHO_LOTE); Serial.println(" linhas"); Serial.print(" tempo total: "); Serial.print(tempoDePreenchimento(linhas)); Serial.println(" s"); Serial.print(" tempo por lote: "); Serial.print(tempoDePreenchimento(TAMANHO_LOTE)); Serial.println(" ms <- a tabela nao fica travada por mais que isso"); Serial.println(); Serial.println(" ETAPA 2: aplicar o NOT NULL"); Serial.println(" nao preenche nada: so varre para confirmar que nao ha nulo"); Serial.print(" tempo total: "); Serial.print(tempoDeVerificacao(linhas)); Serial.println(" s <- este e o downtime de verdade"); Serial.println(); Serial.println(" se as duas fossem uma so, o downtime seria a soma:"); Serial.print(" "); Serial.print(tempoDePreenchimento(linhas) + tempoDeVerificacao(linhas)); Serial.println(" s, e o risco do NOT NULL viria junto."); Serial.println(); } void setup() { pinMode(PIN_LED, OUTPUT); Serial.begin(115200); delay(200); Serial.println(); Serial.println("=== dia 6, aula 2: preparar a migração antes de quebrar algo ==="); Serial.println("Quatro estados, uma ordem, e o downtime medido de cada etapa."); Serial.println(); Serial.println("os quatro estados que o sistema atravessa:"); mostrarEstados(); } void loop() { digitalWrite(PIN_LED, HIGH); mostrarDuasEtapas(2000); Serial.println("--- e com a tabela de um milhão de linhas ---"); mostrarDuasEtapas(1000000); Serial.println("--- o que precisa estar escrito antes do procedimento ---"); Serial.println(" 1. backup feito, e o restore testado"); Serial.println(" 2. copia do banco de HOJE, com a mesma quantidade de dado"); Serial.println(" 3. tempo medido na copia"); Serial.println(" 4. ordem dos passos escrita"); Serial.println(" 5. quem executa cada passo"); Serial.println(); Serial.println(" sem os cinco, o procedimento e um desejo, e o incidente"); Serial.println(" do dia seguinte escreve a versao real dele."); Serial.println(); digitalWrite(PIN_LED, LOW); delay(INTERVALO_MS); }
Por que assim e não de outro jeito. A placa não tem banco e não roda migração, e o professor começa dizendo isso porque a pergunta vem rápida: "onde está o ALTER TABLE?". A resposta é que o sketch não executa nada — ele mostra os quatro estados e mede o downtime, que são as duas coisas que a equipe precisa saber antes de sentar na frente do banco de verdade.
Os quatro estados estão no arranjo porque a combinação código-coluna é uma grade, e uma grade precisa ser percorrida inteira para ser vista. O campo funciona é o que o professor pede para a turma preencher antes de rodar qualquer coisa: preencher essa coluna é escrever a previsão, e comparar com o que aconteceu depois é o teste de um plano de migração. Uma migração preparada é uma migração com previsão escrita.
O estado "contrair" tem funciona igual a nao, e é o único com nao na tabela. O professor deixa isso visível e pede a explicação à turma antes de dar: o código novo pede a coluna, a coluna não existe, toda requisição falha. Os outros três estados são aceitáveis porque são transitórios e o código em produção ainda é o antigo; esse é o estado que ninguém quer atravessar, e a ordem das etapas existe para passar de "migrar" para "contrair" só depois que o código antigo saiu do ar.
A separação entre tempoDePreenchimento e tempoDeVerificacao é a parte que mais importa na aula, e ela está em duas funções com duas constantes diferentes. O preenchimento escreve mil linhas por segundo e o NOT NULL varre cinco mil por segundo — e a razão é que as duas operações fazem coisas diferentes. Preencher é escrever; verificar é ler e conferir. O professor insiste nos dois números porque é a soma deles que aparece quando alguém faz as duas etapas em um comando só, e é essa soma que vira downtime de produção.
O TAMANHO_LOTE com mil linhas é a migração em duas etapas no eixo prático: cada lote trava a tabela por um milissegundo e devolve a escrita ao sistema, e o total continua sendo o mesmo. O professor mede o antes e o depois no banco de teste, e o que a turma descobre é que o tempo total praticamente não muda, enquanto a janela em que a tabela fica travada encolhe em três ordens de grandeza. Essa é a diferença entre uma migração que derruba o sistema e uma que ninguém percebe.
A mostrarDuasEtapas ser chamada duas vezes, com dois mil e com um milhão de linhas, é o que transforma a aula em medição. Com a constante no código e não o resultado colado à mão, o aluno muda o número, roda, e vê os dois tempos mudarem juntos. É a diferença entre uma tabela que demonstra e uma tabela que responde a pergunta "e se a tabela for maior?".
O LED acende e apaga a cada volta, e o intervalo de cinco segundos dá ao professor o tempo de ler a tabela de estados e as duas tabelas de tempos sem pausar. Como nas outras aulas, é escolha de sala e não do sistema.
Criterios de correcao
| Critério | Pontos |
|---|---|
| As duas leituras de schema coladas, com a comparação escrita | 2 pontos |
| Os quatro estados da migração no papel, com o que quebra e a linha do código | 2 pontos |
A mensagem do banco ao tentar o NOT NULL com nulo na tabela, colada | 1 ponto |
| Os três números do preenchimento em lote, com o tempo de travada por lote | 2 pontos |
Os dois tempos comparados — preenchimento e NOT NULL — e o downtime escolhido | 2 pontos |
| Os dois rollbacks escritos, com o que cada um perde e qual é irreversível | 2 pontos |
| Os dois tempos do item 7, banco parado e com escrita concorrente | 1 ponto |
Erros comuns
| Erro | Como aparece | Correção |
|---|---|---|
NOT NULL em coluna com nulo | O banco recusa e a equipe culpa o comando | "Não é erro de sintaxe: é integridade. Primeiro preenche os nulos em lote, depois aplica o NOT NULL. São duas migrações, dois números." |
| Fazer as duas etapas em um comando | UPDATE e ALTER na mesma transação, com downtime longo | "A soma dos tempos é o downtime. Separando, o preenchimento trava a tabela por milissegundos por lote e só a verificação trava de verdade." |
| Publicar o código antes da migração | O código novo sobe, pede a coluna, e todo pedido falha | "Ordem é banco, depois código. O estado 'código novo sem coluna' é o único que quebra, e a janela é do tamanho do deploy." |
| Fazer a migração no banco de verdade primeiro | O time descobre o problema com o sistema no ar | "Rode na cópia de hoje, com a mesma quantidade de dado. Dez linhas não exercitam o backfill, e a cópia da semana passada testa um banco que não existe mais." |
| Backup de três horas atrás | "Temos backup" e ninguém sabe restaurar | "O backup que serve é o de antes da migração, e o que vale precisa ter o restore testado. Backup nunca restaurado é hipótese, não rede." |
| Migrar sem ler o schema | A migração roda no banco de teste e falha no de verdade | "Leia o schema dos dois antes. O script diz o que o banco deveria ter; o banco diz o que tem. São coisas diferentes desde a primeira mudança aplicada na mão." |
Coluna nova sem DEFAULT | A criação falha em tabela com linhas | "Coluna nova nasce com valor padrão. Sem ele, não há como criar em tabela cheia, e o backfill vira obrigatório em vez de opcional." |
Desafio extra
Meça o tempo real da migração de verificação no seu banco de teste com mil linhas, com dez mil e com cem mil, e construa a curva do tempo pelo tamanho da tabela. Depois escreva a versão que cria o índice em segundo plano, se o seu banco oferecer a opção, meça de novo, e compare com o tempo da criação normal. Escreva na frente os dois gráficos em texto, com os números, e diga em que tamanho de tabela a diferença deixa de ser aceitável para um horário de produção. Por fim, escreva o procedimento de restaurar um backup com o tempo estimado de restauração, e diga o que a equipe precisa decidir antes de precisar dele.
>A resolucao, compilada
// Aula 2 do dia 6 do 3o trimestre de ESP32 e IoT. // Gerado a partir da secao ## Resolucao de conteudo/t3/dia06/aula2.md. // Se mudar o codigo, mude no .md e regere: o .ino e copia do .md. #include <Arduino.h> // =========================================================================== // dia 6, aula 2: preparar a migracao antes de ela quebrar a producao. // // A placa NAO tem banco e NAO roda migracao. O que ela faz e o que importa: // mostrar os QUATRO estados que o sistema atravessa durante a troca de schema, // e medir o custo de cada etapa do preenchimento em lote. // // O numero de linhas por segundo e o mesmo do dia 6 aula 1, e e ele que // transforma "a tabela tem muito dado" em "a migracao leva este tempo". // =========================================================================== const int PIN_LED = 2; const unsigned long INTERVALO_MS = 5000; // --------------------------------------------------------------------------- // OS QUATRO ESTADOS. Coluna e codigo sao duas dimensoes, e a combinacao que // quebra e a ultima: codigo novo sem coluna. // // `funciona` e o que o professor pede para a turma prever antes de rodar. // --------------------------------------------------------------------------- struct Estado { const char* nome; bool colunaNovaExiste; bool codigoNovoEstaNoAr; bool funciona; const char* oQueAcontece; }; const Estado ESTADOS[] = { {"inicial", false, false, true, "tabela como o codigo espera"}, {"expandir", true, false, true, "codigo antigo ignora a coluna extra"}, {"migrar", true, true, true, "ambos gravam; o backfill preenche"}, {"contrair", false, true, false, "codigo novo pede coluna que sumiu"} }; const int TOTAL_ESTADOS = 4; // --------------------------------------------------------------------------- // O CUSTO DE CADA ETAPA. Estes sao os numeros que o professor mediu na copia // do banco, e a ordem deles e a ordem do downtime. // --------------------------------------------------------------------------- // O que o preenchimento em lote segura a tabela, por lote. const int TAMANHO_LOTE = 1000; const int LINHAS_POR_SEGUNDO = 1000; // O NOT NULL nao preenche nada: ele VARRE a tabela para verificar que nao ha // nulo. E por isso que ele e o downtime grande, e nao o preenchimento. const int LINHAS_POR_SEGUNDO_VERIFICACAO = 5000; int tempoDePreenchimento(int linhas) { return (linhas + LINHAS_POR_SEGUNDO - 1) / LINHAS_POR_SEGUNDO; } int tempoDeVerificacao(int linhas) { return (linhas + LINHAS_POR_SEGUNDO_VERIFICACAO - 1) / LINHAS_POR_SEGUNDO_VERIFICACAO; } void mostrarEstados() { Serial.println(" estado coluna codigo novo funciona o que acontece"); for (int i = 0; i < TOTAL_ESTADOS; i++) { Serial.printf(" %-11s %-7s %-12s %-9s %s\n", ESTADOS[i].nome, ESTADOS[i].colunaNovaExiste ? "sim" : "nao", ESTADOS[i].codigoNovoEstaNoAr ? "sim" : "nao", ESTADOS[i].funciona ? "sim" : "NAO", ESTADOS[i].oQueAcontece); } Serial.println(); Serial.println(" so o ultimo quebra: codigo novo sem a coluna."); Serial.println(" e por isso que a ordem e banco primeiro, codigo depois."); Serial.println(); } void mostrarDuasEtapas(int linhas) { Serial.println("--- a migracao que quebra, em duas etapas ---"); Serial.print(" linhas na tabela: "); Serial.println(linhas); Serial.println(); Serial.println(" ETAPA 1: preencher os nulos, em lote"); Serial.print(" lotes de "); Serial.print(TAMANHO_LOTE); Serial.println(" linhas"); Serial.print(" tempo total: "); Serial.print(tempoDePreenchimento(linhas)); Serial.println(" s"); Serial.print(" tempo por lote: "); Serial.print(tempoDePreenchimento(TAMANHO_LOTE)); Serial.println(" ms <- a tabela nao fica travada por mais que isso"); Serial.println(); Serial.println(" ETAPA 2: aplicar o NOT NULL"); Serial.println(" nao preenche nada: so varre para confirmar que nao ha nulo"); Serial.print(" tempo total: "); Serial.print(tempoDeVerificacao(linhas)); Serial.println(" s <- este e o downtime de verdade"); Serial.println(); Serial.println(" se as duas fossem uma so, o downtime seria a soma:"); Serial.print(" "); Serial.print(tempoDePreenchimento(linhas) + tempoDeVerificacao(linhas)); Serial.println(" s, e o risco do NOT NULL viria junto."); Serial.println(); } void setup() { pinMode(PIN_LED, OUTPUT); Serial.begin(115200); delay(200); Serial.println(); Serial.println("=== dia 6, aula 2: preparar a migração antes de quebrar algo ==="); Serial.println("Quatro estados, uma ordem, e o downtime medido de cada etapa."); Serial.println(); Serial.println("os quatro estados que o sistema atravessa:"); mostrarEstados(); } void loop() { digitalWrite(PIN_LED, HIGH); mostrarDuasEtapas(2000); Serial.println("--- e com a tabela de um milhão de linhas ---"); mostrarDuasEtapas(1000000); Serial.println("--- o que precisa estar escrito antes do procedimento ---"); Serial.println(" 1. backup feito, e o restore testado"); Serial.println(" 2. copia do banco de HOJE, com a mesma quantidade de dado"); Serial.println(" 3. tempo medido na copia"); Serial.println(" 4. ordem dos passos escrita"); Serial.println(" 5. quem executa cada passo"); Serial.println(); Serial.println(" sem os cinco, o procedimento e um desejo, e o incidente"); Serial.println(" do dia seguinte escreve a versao real dele."); Serial.println(); digitalWrite(PIN_LED, LOW); delay(INTERVALO_MS); }
Sem saída de compilação gravada. Rode python3 validar.py -t 3 dia06 aula2.
