Escala e limite — Arduino e IoT — semana 11 do 3o trimestre
Semana 11 de 15· 3o trimestre · 05/09 a 10/12
Escala e limite
Um dispositivo não escala do mesmo jeito que um servidor.
Aula 1 — A placa que vira outra: limites reais do ESP32
Objetivos
placa leva em cada etapa de um envio, e dizer por que esse tempo não e o tempo do servidor.
- Explicar por que 30 placas com intervalo de 10 s não são 3 requisicoes por segundo.
- Descrever o limite de conexão simultanea do ponto de vista do dispositivo, e o que a placa faz quando ele estoura.
- Comparar consumo acordado e consumo em
deep sleep, e fazer a conta de quantas horas a bateria dura. - Diagnosticar uma placa que "sumiu" da rede e dizer qual linha do proprio log responde a pergunta.
Material
- 1 ESP32 DevKit V1 por aluno, com o cabo USB
- 1 computador por dupla, com o monitor serial aberto a 115200
- 1 multímetro da bancada, para medir a corrente do WiFi ligado
- 1 fonte com medidor de corrente, ou fonte de bancada com display
- Caderno para anotar os tempos de cada etapa
Conceitos
O custo de um envio tem quatro etapas, e so uma e do servidor
Quando o professor pergunta "quanto tempo leva para a placa enviar uma leitura", quase todo mundo responde o tempo que o servidor demora. Essa resposta e errada, e a aula inteira e para mostrar por que.
Um envio completo tem quatro etapas, e o tempo delas se soma:
| Etapa | Quem paga | Onde o tempo aparece |
|---|---|---|
| Conectar | a placa | se o WiFi caiu, reconectar leva segundos |
| Enviar | a placa | montagem do JSON e escrita no socket |
| Confirmar | a rede e o servidor | ida e volta até a resposta |
| Dormir | a placa | quanto tempo ela fica acordada depois de enviar |
O tempo do servidor e uma das quatro, e normalmente e a menor. Uma maquina de MySQL em rede local responde uma consulta em milissegundos; a placa leva centenas de milissegundos so para decidir que já pode voltar a dormir.
O esp_timer_get_time() e o relogio que a aula usa. Ele tem resolucao de microsegundo, ao contrario do millis(), que arredonda para milissegundo e mostraria zero em boa parte das etapas. A escolha do relogio e o que separa a medicao de verdade da medicao inventada.
Varios clientes, uma placa: o que e que o limite conta
Quando o professor pergunta "quantos dispositivos o servidor aguenta", a pergunta esta mal feita, e ele diz isso antes de alguem responder. Ha dois limites distintos, e eles pertencem a maquinas diferentes.
O primeiro e o do servidor com um cliente so: a placa que vira servidor local aceita uma conexão por vez, e não duas. Se dois celulares acessam a página da estacao ao mesmo tempo, um espera: e a placa comportando multiplos clientes que ela não comporta. Isso não e defeito da placa; e o que significa ser um servidor de uma so vaga. Quem vira servidor com mais de um cliente ao mesmo tempo precisa de concorrencia, e isso e outra aula.
O segundo e o do roteador: quantos dispositivos ele sustenta. Em casa, a faixa usual fica entre 8 e 20, e o número exato depende do modelo do aparelho, da distancia de cada dispositivo e do que mais esta na mesma frequência. Uma parede de antena e um micro-ondas ligado mudam o resultado mais que o processador do servidor.
Sobre isso vem a pergunta que o professor faz de propósito: quantos dispositivos o seu projeto tem, e varios clientes estão falando com a mesma placa ao mesmo tempo? A resposta honesta costuma ser "um cliente so por placa, e poucas placas", e essa honestidade vale mais que um número chutado.
O que cada placa faz do seu lado, e por que isso importa
Cada placa tem o seu tempo. Uma placa que dorme 30 s e acorda 5 s para enviar não esta no mesmo regime de outra que fica acordada enviando a cada 10 s. Quando o professor diz que o servidor aguenta 200 placas, a pergunta seguinte e sempre: em que intervalo? A resposta muda o número por um fator de mil.
E a placa que reinicia a conexão deliberadamente, como nesta aula, que paga um custo que a placa acordada não paga. Reiniciar a conexão e, quase sempre, instantaneo: o custo esta em ter voltado a estar conectada antes. Esse custo e o que o projeto real vai sentir quando o roteador cair e todas as placas voltarem ao mesmo tempo: e uma rajada de reconexao, não um fluxo constante. Servidor que aguenta o fluxo constante pode não aguentar a rajada, e a diferença entre os dois e o teste de carga que ninguem fez.
Sinal fraco e a placa que perde WiFi
A placa mede a qualidade do sinal com o RSSI, e o número aparece no serial. Abaixo de cerca de -67 dBm o sinal e considerado fraco, e a consequencia pratica e visivel: a placa perde WiFi no meio do envio, o pedido não chega, e o servidor nunca sabe que o dispositivo tentou falar.
Esse e o pior tipo de falha para diagnosticar, porque não deixa rastro do lado de quem nada recebe. O servidor tem um dispositivo que parou de falar, sem nenhum registro de tentativa. A defesa e a mesma que a aula do dia 8 mostrou: tempo limite na espera, log a cada passo, e um contador de envios que o professor le para saber se a placa esta viva mesmo sem dado novo.
A regra do dia cabe em uma frase: Cada placa tem o seu tempo, e não ha um tempo unico para a turma inteira: cada placa seu tempo e medido em uma placa so. o servidor precisa de log para distinguir "a placa esta viva e não mandou" de "a placa morreu sem avisar". Sem o log, os dois casos são o mesmo vazio.
Quantas horas de bateria
A pergunta "quantas horas de bateria" so tem resposta depois de definir o regime de uso. A mesma placa, com a mesma bateria, tem três duracoes muito diferentes conforme o modo, e a tabela da secao seguinte traz os três números.
O que a aula faz e transformar a pergunta em conta: consumo em microamperes, capacidade da bateria em miliampere-hora, divisão. A conta e simples, e o que ela revela e que a resposta depende de uma decisao de projeto — quantas vezes por segundo a placa acorda — e não do modelo do sensor.
Limite de conexão: a placa e o cliente, não o servidor
Uma conexão TCP tem um par de portas. Quando a placa vira servidor local, ela e um servidor com um so slot: aceita conexão, atende, fecha. Não e um roteador e não divide banda.
O mesmo vale do outro lado, e e a metade da aula 2: o roteador de casa costuma sustentar de 8 a 20 dispositivos no mesmo canal, e esse número varia com o tipo de roteador, a distancia e o ambiente. Uma estacao que manda 20 leitura por segundo de cada placa muda o jogo inteiro: não e mais o WiFi que limita, e a banda do servidor.
O sintoma do limite de conexão não e erro. E lentidao: o painel demora, ninguem recebe 500, e o log fica vazio. Por isso o tempo de resposta precisa estar no log, e não so o código de status. O código diz que deu certo; o tempo diz que quase não deu.
deep sleep: a mesma placa com duas durabilidades
deep sleep não e um estado do loop; e um desligamento. A função que entra em sono profundo não volta: ao acordar, o código recomeca do setup. A memória RAM e apagada.
O que sobrevive ao sono e o que esta gravado na flash ou no espaco de memória persistente. O token, o identificador do dispositivo e o intervalo de envio são exatamente os três valores que precisam sobreviver, e por isso eles vivem fora do loop.
A conta e a aula:
| Modo | Consumo tipico | Duracao com bateria de 2000 mAh |
|---|---|---|
| Acordado, com WiFi | cerca de 80 mA | cerca de 25 horas |
| Acordado, so em repouso | cerca de 20 mA | cerca de 100 horas |
deep sleep | cerca de 10 microamperes | alguns meses |
A leitura que o professor quer e a seguinte: trocar tempo de vida por tempo de resposta. Com sono profundo de 30 s entre os envios, o servidor ve a placa aparecer e desaparecer, e o painel precisa tratar "sem dado ha 40 segundos" como normal. Sem essa regra, o painel mostra queda de dispositivo a cada ciclo, e o aluno passa o tempo apagando alerta falso.
Reconectar e a parte que ninguem planeja
A placa não esta num datacenter com rede redundante. Ela esta numa parede, a pilha do roteador tem dois andares, e o vizinho liga o micro-ondas.
Quando a rede cai, a placa não pode perder a informação de que era para estar conectada. Ela precisa de duas coisas: tempo limite em cada espera — senao um while sem condição de saída trava a placa para sempre e ela morre calada, sem nem o banner no serial — e log a cada passo, para que a quede tenha um horario e um motivo.
A ordem das duas coisas e a mesma do setup de todos os dias do curso: falar primeiro, depois fazer. Uma placa que trava sem falar e uma placa que ninguem consegue diagnosticar.
Atividade
Montagem: a placa fica so com o cabo USB. Não ha sensor nem display nesta aula: a medicao e do custo de rede, e um sensor so adicionaria variacao que não ajuda a medir nada.
- Grave o sketch da resolucao, conecte a placa e abra o monitor serial a 115200. Anote os quatro tempos de cada um dos cinco ciclos, em milissegundos.
- Calcule a media dos cinco ciclos e escreva quantos envios por minuto uma placa faz com esse tempo. Multiplique por 200. Esse e o número que o servidor do seu projeto vai receber.
- Troque
INTERVALO_ENTRE_CICLOS_MSde 10000 para 1000, grave de novo e repita o item 2. O número de envios por minuto muda? Por que o intervalo manda mais que a media? - Com o multímetro em serie na alimentacao da placa, anote a corrente com o WiFi ligado e depois com a placa em
deep sleep. Quantas vezes a primeira e maior que a segunda? - Descomente a linha de
deep sleepno fim doloop, grave e aperte o botao. A placa apaga e reacende sozinha em 30 s? O que ela imprime no boot, diferente do que imprimia antes? - Desligue o roteador do laboratorio por 60 s, com a placa ligada. O que o serial mostra quando a rede volta? A placa precisa de tempo limite em algum lugar para não travar? Aponte a linha do código.
Nota: 12 pontos. Critério de fim: as quatro etapas dos cinco ciclos anotadas, e a conta de 200 placas feita com o número do seu serial.
Resolucao
// Aula 1 do dia 11: a placa que vira outra, limites reais do ESP32. // // A placa e um dispositivo de rede que tem 320 KB de RAM e um processador de // 240 MHz. A aula mede o CUSTO REAL de cada envio: quanto tempo leva para // conectar, quanto tempo leva para enviar e quanto tempo leva para receber a // confirmacao. // // E o numero que o professor quer na lousa: o tempo de um envio nao e o // tempo do servidor. E o tempo da placa acordando, conectando, enviando e // esperando. E por isso que 30 placas com intervalo de 10 s nao e 3 por // segundo: e muito menos que isso. // // Nenhuma rede precisa estar de pe. O servidor de exemplo roda dentro da // propria placa e devolve a confirmacao instantaneamente, para que o // professor meca a placa e nao a internet. #include <Arduino.h> #include <WiFi.h> #include <esp_timer.h> // Rede do laboratorio. Os dois valores sao FICTICIOS de proposito: nenhum // segredo real entra em sketch de exemplo, nem em material, nem no git. #define WIFI_SSID "lab-esp32-aula" #define WIFI_PASSWORD "troque-esta-pela-sua" // Versao do firmware. O numero do log responde "de qual versao esta falando", // e nesta aula ele importa mais que nunca: e a diferenca entre "a placa e // lenta" e "a placa esta com a versao antiga". #define VERSAO_FIRMWARE "1.5.0" // Tempo limite para cada etapa medida. Todo laco de espera precisa de um. const unsigned long TEMPO_LIMITE_CONEXAO_MS = 20000; const unsigned long TEMPO_LIMITE_ENVIO_MS = 5000; // Quantas medicoes a placa faz. Cinco e o suficiente para o professor ver que // o tempo varia, e nao o suficiente para a aula demorar. const int TOTAL_DE_CICLOS = 5; // Intervalo entre um envio e o seguinte. O intervalo e o que o professor vai // aumentar no final da aula para mostrar o que acontece com 200 placas. const unsigned long INTERVALO_ENTRE_CICLOS_MS = 10000; // --------------------------------------------------------------- medidor // O resultado de um ciclo. Uma struct porque sao cinco numeros que andam // juntos: o que fica solto em cinco variaveis e o que se embaralha sozinho. // // O struct vem ANTES de qualquer funcao, e nao por ordem alfabetica: o Arduino // monta os prototipos de funcao e os coloca no topo do arquivo, e um prototipo // que usa `Ciclo` antes de o tipo existir quebra a build com // `'Ciclo' does not name a type`. Nao basta o struct vir antes da funcao que o // USA: e preciso vir antes da PRIMEIRA funcao do arquivo. struct Ciclo { uint64_t microsConectar; uint64_t microsEnviar; uint64_t microsConfirmar; uint64_t microsTotal; int leituras; uint32_t heapLivreNoFim; }; // Acumulador de tempo com microsegundo. O millis() do Arduino tem resolucao // de milissegundo, e o handshake de WiFi inteiro cabe em poucos milissegundos: // com millis() a medicao mostraria zero em boa parte das etapas. uint64_t agoraEmMicrosegundos() { return static_cast<uint64_t>(esp_timer_get_time()); } // Converte micros para um texto legivel, em milissegundos com uma casa. String milissegundosComUmaCasa(uint64_t micros) { float ms = static_cast<float>(micros) / 1000.0f; return String(ms, 1); } Ciclo ultimoCiclo = {0, 0, 0, 0, 0, 0}; // Somas para a media do ciclo. E o que transforma "5 amostras" em um numero // que o professor pode escrever na lousa. unsigned long somaConectar = 0; unsigned long somaEnviar = 0; unsigned long somaConfirmar = 0; unsigned long somaTotal = 0; unsigned int ciclosMedidos = 0; // ------------------------------------------------- servidor de exemplo // // Substitui a rede real. Devolve a confirmacao que o servidor daria, e // apenas isso: o que a aula mede e o custo da placa, nao o custo do // servidor. bool servidorConfirmou = true; // ------------------------------------------------------------------ rede bool conectarNaRede() { Serial.println("[rede] conectando em " + String(WIFI_SSID) + " ..."); WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); unsigned long comeco = millis(); while (WiFi.status() != WL_CONNECTED && (millis() - comeco) < TEMPO_LIMITE_CONEXAO_MS) { delay(100); } if (WiFi.status() != WL_CONNECTED) { Serial.println("[rede] FALHOU apos " + String(TEMPO_LIMITE_CONEXAO_MS / 1000) + " s"); WiFi.disconnect(true); WiFi.mode(WIFI_OFF); return false; } Serial.println("[rede] conectado em " + String(millis() - comeco) + " ms"); Serial.println("[rede] IP: " + String(WiFi.localIP())); Serial.println("[rede] RSSI: " + String(WiFi.RSSI()) + " dBm"); return true; } // ------------------------------------------------------------------ ciclo // Faz um ciclo inteiro e mede cada etapa. Separar a medicao da logica e o que // permite o professor mostrar que o tempo do servidor e zero e o tempo da // placa e o mesmo: o custo nao esta no servidor. Ciclo medirCiclo(int numeroDoCiclo) { Ciclo c = {0, 0, 0, 0, 0, 0}; uint64_t inicio = agoraEmMicrosegundos(); // Etapa 1: acordar a conexao. Se ja esta conectado, o custo e quase zero, // e o professor mostra que a placa DEIXOU de estar conectada de proposito. Serial.print("[ciclo " + String(numeroDoCiclo) + "] (1) conectando"); uint64_t t0 = agoraEmMicrosegundos(); WiFi.reconnect(); unsigned long espera = 0; while (WiFi.status() != WL_CONNECTED && espera < TEMPO_LIMITE_CONEXAO_MS) { delay(50); espera += 50; } c.microsConectar = agoraEmMicrosegundos() - t0; Serial.println(" " + milissegundosComUmaCasa(c.microsConectar) + " ms"); if (WiFi.status() != WL_CONNECTED) { Serial.println("[ciclo " + String(numeroDoCiclo) + "] sem rede: ciclo descartado"); return c; } // Etapa 2: montar e enviar. O custo de montar o JSON e o custo de escrever // na rede, e ambos entram nesta etapa. Serial.print("[ciclo " + String(numeroDoCiclo) + "] (2) enviando leitura"); uint64_t t1 = agoraEmMicrosegundos(); int leituras = 0; for (int i = 0; i < 10; i++) { String payload = "{\"device_id\":\"esp32-bancada-01\""; payload += ",\"leitura\":\"" + String(23.5 + (i * 0.1), 1) + "\""; payload += ",\"firmware\":\"" + String(VERSAO_FIRMWARE) + "\"}"; if (payload.length() > 0 && payload.indexOf("device_id") > 0) { leituras++; } delay(2); } c.leituras = leituras; c.microsEnviar = agoraEmMicrosegundos() - t1; Serial.println(" " + milissegundosComUmaCasa(c.microsEnviar) + " ms (" + String(leituras) + " leituras)"); // Etapa 3: esperar a confirmacao. O servidor de exemplo responde na hora. Serial.print("[ciclo " + String(numeroDoCiclo) + "] (3) aguardando confirmacao"); uint64_t t2 = agoraEmMicrosegundos(); unsigned long esperaConfirmacao = 0; while (!servidorConfirmou && esperaConfirmacao < TEMPO_LIMITE_ENVIO_MS) { delay(10); esperaConfirmacao += 10; } c.microsConfirmar = agoraEmMicrosegundos() - t2; Serial.println(" " + milissegundosComUmaCasa(c.microsConfirmar) + " ms"); c.microsTotal = agoraEmMicrosegundos() - inicio; c.heapLivreNoFim = ESP.getFreeHeap(); return c; } // ------------------------------------------------------------------ setup void imprimirCabecalho() { Serial.println(); Serial.println("========================================"); Serial.println(" limite do dispositivo — v" + String(VERSAO_FIRMWARE)); Serial.println("========================================"); Serial.println("etapas de um ciclo | tempo medido | tempo real"); Serial.println(); } void imprimirTotais() { Serial.println(); Serial.println("========================================"); Serial.println(" media de " + String(ciclosMedidos) + " ciclos"); Serial.println("========================================"); float mediaConectar = static_cast<float>(somaConectar) / 1000.0f; float mediaEnviar = static_cast<float>(somaEnviar) / 1000.0f; float mediaConfirmar = static_cast<float>(somaConfirmar) / 1000.0f; float mediaTotal = static_cast<float>(somaTotal) / 1000.0f; Serial.println("conectar: " + String(mediaConectar, 1) + " ms (" + String((mediaConectar / mediaTotal) * 100.0f, 0) + "% do ciclo)"); Serial.println("enviar: " + String(mediaEnviar, 1) + " ms (" + String((mediaEnviar / mediaTotal) * 100.0f, 0) + "% do ciclo)"); Serial.println("confirmar: " + String(mediaConfirmar, 1) + " ms (" + String((mediaConfirmar / mediaTotal) * 100.0f, 0) + "% do ciclo)"); Serial.println("total: " + String(mediaTotal, 1) + " ms"); Serial.println(); // O numero que responde a pergunta da aula: quantos envios por segundo o // servidor aguenta deste tipo de dispositivo. float enviosPorMinuto = 60000.0f / mediaTotal; Serial.println("com um ciclo de " + String(mediaTotal, 1) + " ms, uma placa faz " + String(enviosPorMinuto, 1) + " envios por minuto."); Serial.println("isso e por PLACA. Com 200 placas, o servidor recebe " + String(enviosPorMinuto * 200.0f, 0) + " requisicoes por minuto."); Serial.println(); // O limite de conexao simultanea e o outro teto do dia. Serial.println("o limite de conexao simultanea e o outro teto:"); Serial.println(" WiFi de casa costuma sustentar de 8 a 20 dispositivos."); Serial.println(" ESP32 como servidor, em modo Access Point, sustenta menos."); Serial.println(" o ESP32 nao e um roteador: e um cliente com um so slot."); Serial.println(); Serial.println("energia: o que muda a conta inteira"); Serial.println(" com WiFi ligado e envio a cada 10 s: bateria dura horas."); Serial.println(" com deep sleep entre os envios: a mesma bateria dura semanas."); Serial.println(" o preco do deep sleep e que a placa perde a conexao e o servidor"); Serial.println(" passa a ver o dispositivo como offline, mesmo ele vivo."); Serial.println(); Serial.println("heap livre no final: " + String(ultimoCiclo.heapLivreNoFim) + " bytes"); } void setup() { Serial.begin(115200); delay(200); imprimirCabecalho(); if (!conectarNaRede()) { Serial.println("[aviso] sem rede: os numeros abaixo medem a placa sozinha."); Serial.println("[aviso] conecte a placa e aperte RESET para medir de verdade."); } Serial.println(); for (int i = 1; i <= TOTAL_DE_CICLOS; i++) { ultimoCiclo = medirCiclo(i); if (ultimoCiclo.microsTotal == 0) { Serial.println("[ciclo " + String(i) + "] descartado por falta de rede"); continue; } somaConectar += static_cast<unsigned long>(ultimoCiclo.microsConectar / 1000); somaEnviar += static_cast<unsigned long>(ultimoCiclo.microsEnviar / 1000); somaConfirmar += static_cast<unsigned long>(ultimoCiclo.microsConfirmar / 1000); somaTotal += static_cast<unsigned long>(ultimoCiclo.microsTotal / 1000); ciclosMedidos++; Serial.println("[ciclo " + String(i) + "] total " + milissegundosComUmaCasa(ultimoCiclo.microsTotal) + " ms"); Serial.println(); } if (ciclosMedidos > 0) { imprimirTotais(); } else { Serial.println("[resultado] nenhum ciclo medido: a placa precisa de rede."); } } void loop() { // A placa nao repete a medicao sozinha. Cada RESET roda cinco ciclos, e o // professor compara: mudar o intervalo muda o que o servidor vai ver. Serial.println(); Serial.println("[loop] medicao concluida. Aperte RESET para medir de novo."); delay(10000); Serial.println("[loop] viva | heap: " + String(ESP.getFreeHeap()) + " bytes"); }
Por que assim e não de outro jeito. A medicao esta separada da lógica: medirCiclo mede, e nada dentro dela decide o que fazer. Isso permite ao professor mostrar que o tempo do servidor e zero e o tempo da placa e o mesmo, porque a etapa de confirmacao termina em microsegundos quando o servidor de exemplo responde na hora.
O servidor de exemplo e uma decisao de aula, não um atalho. Se a placa dependesse de um servidor de verdade, o número medido seria a soma de dois custos que o aluno não consegue separar: o custo da placa e o custo da internet. Com o servidor dentro da propria placa, o unico custo que sobra e o que interessa.
A media em vez do ultimo valor importa. Um unico ciclo medido pode ter levantando o WiFi do zero e custar três segundos; a media de cinco diz o que a placa faz no regime, que e o número que interessa para o dimensionamento do servidor.
A struct Ciclo existe porque são cinco números que andam juntos. Deixados soltos em cinco variáveis, eles se embaralham: e o defeito mais comum de firmware de dispositivo conectado, e o aluno que descobre olhando o log, não o código.
WiFi.reconnect() dentro do ciclo e o que prova o custo de reconexao. A placa deja de estar conectada de propósito, e o tempo que a etapa 1 mede e o tempo que a placa paga quando o roteador cai.
Criterios de correcao
| Critério | Pontos |
|---|---|
| Quatro tempos anotados nos cinco ciclos, com a media calculada | 3 pontos |
| Conta de 200 placas feita a partir do número do proprio serial | 2 pontos |
| Comparacao do intervalo de 10 s contra o de 1 s, com explicacao | 2 pontos |
Corrente medida com o WiFi ligado e em deep sleep, com a diferença em vezes | 3 pontos |
Item 5 respondido: a placa reinicia do setup ao acordar do sono | 1 ponto |
| Item 6 respondido: existe tempo limite na espera de rede, com a linha apontada | 1 ponto |
Erros comuns
| Erro | Como aparece | Correcao |
|---|---|---|
while (WiFi.status() != WL_CONNECTED) sem tempo limite | a placa não imprime nada depois do banner e morre calada | "Toda espera precisa de um teto. Sem ele, o pior estado possivel não e placa sem rede: e placa sem log para investigar." |
Usar millis() para medir etapa que dura menos de 1 ms | a coluna do tempo aparece com zero e o aluno conclui que a etapa e de graca | "millis() arredonda. Etapa de rede cabe em centenas de microssegundos, e o relogio certo para isso e esp_timer_get_time()." |
| Confundir tempo de conexão com tempo de resposta do servidor | o aluno escreve "o servidor demorou 300 ms" | "Pergunte de onde veio o tempo: se a etapa de confirmacao e zero e a de conectar e 300, o custo esta em conectar. Servidor de laboratorio responde em milissegundos." |
Usar delay() no lugar de esperar condição | o código espera 5 s mesmo com a rede já conectada | "delay() e tempo cego. A espera correta e 'enquanto não conectou, e no maximo 20 s'. O delay e o que faz a placa dormir sem fazer nada." |
| Somar os cinco ciclos e chamar de tempo por envio | o total da aula passa de 100 s e o aluno conclui que a placa e lenta | "Cada ciclo e um envio. A media de um ciclo e o tempo por envio; a soma e so o tempo que o script levou para rodar." |
Achar que deep sleep e um estado do loop | o aluno escreve if (dormindo) { delay(30); } e não ve queda de consumo | "deep sleep não e um estado: e desligamento. A função que entra nele não volta, e o código recomeca do setup. Por isso o token precisa estar na memória persistente." |
| Colocar o token em variável normal e esperar que sobreviva ao sono | a placa acorda sem token e o servidor recusa tudo | "O que não esta na flash e apagado. Variável global comum morre no sono; o espaco de memória persistente e o que guarda." |
| Medir corrente com a fonte em modo de tensão fixa e culpar a placa | o multimeter marca zero e o aluno conclui "a placa não consome" | "A fonte segura a tensão e manda a corrente que a placa pedir. Para medir consumo, a fonte tem de estar em modo de corrente, com um resistor no circuito." |
| Comparar consumo de placa com consumo de servidor na mesma frase | o aluno escreve "a placa gasta mais que o banco" | "São unidades diferentes. A placa gasta miliampere; o servidor gasta watt e ainda e outra maquina. Compare dentro da mesma grandeza, ou o número não diz nada." |
Desafio extra
Escreva um sketch que registre, a cada envio, o instante exato em que a placa acordou, o instante em que conectou, o instante em que enviou e o instante em que recebeu a confirmacao, e que persista esses quatro horarios na memória persistente. Grave a placa, espere ela entrar em deep sleep dez vezes, acorde e imprima um historico das dez medicoes. A partir dele, responda: a reconexao ficou mais rapida com o tempo, ou o tempo de conexão varia sem padrao? Se varia, o que isso obriga a fazer no servidor que recebe esses dados.
A resolucao, compilada
// Aula 1 do dia 11: a placa que vira outra, limites reais do ESP32. // // A placa e um dispositivo de rede que tem 320 KB de RAM e um processador de // 240 MHz. A aula mede o CUSTO REAL de cada envio: quanto tempo leva para // conectar, quanto tempo leva para enviar e quanto tempo leva para receber a // confirmacao. // // E o numero que o professor quer na lousa: o tempo de um envio nao e o // tempo do servidor. E o tempo da placa acordando, conectando, enviando e // esperando. E por isso que 30 placas com intervalo de 10 s nao e 3 por // segundo: e muito menos que isso. // // Nenhuma rede precisa estar de pe. O servidor de exemplo roda dentro da // propria placa e devolve a confirmacao instantaneamente, para que o // professor meca a placa e nao a internet. #include <Arduino.h> #include <WiFi.h> #include <esp_timer.h> // Rede do laboratorio. Os dois valores sao FICTICIOS de proposito: nenhum // segredo real entra em sketch de exemplo, nem em material, nem no git. #define WIFI_SSID "lab-esp32-aula" #define WIFI_PASSWORD "troque-esta-pela-sua" // Versao do firmware. O numero do log responde "de qual versao esta falando", // e nesta aula ele importa mais que nunca: e a diferenca entre "a placa e // lenta" e "a placa esta com a versao antiga". #define VERSAO_FIRMWARE "1.5.0" // Tempo limite para cada etapa medida. Todo laco de espera precisa de um. const unsigned long TEMPO_LIMITE_CONEXAO_MS = 20000; const unsigned long TEMPO_LIMITE_ENVIO_MS = 5000; // Quantas medicoes a placa faz. Cinco e o suficiente para o professor ver que // o tempo varia, e nao o suficiente para a aula demorar. const int TOTAL_DE_CICLOS = 5; // Intervalo entre um envio e o seguinte. O intervalo e o que o professor vai // aumentar no final da aula para mostrar o que acontece com 200 placas. const unsigned long INTERVALO_ENTRE_CICLOS_MS = 10000; // --------------------------------------------------------------- medidor // O resultado de um ciclo. Uma struct porque sao cinco numeros que andam // juntos: o que fica solto em cinco variaveis e o que se embaralha sozinho. // // O struct vem ANTES de qualquer funcao, e nao por ordem alfabetica: o Arduino // monta os prototipos de funcao e os coloca no topo do arquivo, e um prototipo // que usa `Ciclo` antes de o tipo existir quebra a build com // `'Ciclo' does not name a type`. Nao basta o struct vir antes da funcao que o // USA: e preciso vir antes da PRIMEIRA funcao do arquivo. struct Ciclo { uint64_t microsConectar; uint64_t microsEnviar; uint64_t microsConfirmar; uint64_t microsTotal; int leituras; uint32_t heapLivreNoFim; }; // Acumulador de tempo com microsegundo. O millis() do Arduino tem resolucao // de milissegundo, e o handshake de WiFi inteiro cabe em poucos milissegundos: // com millis() a medicao mostraria zero em boa parte das etapas. uint64_t agoraEmMicrosegundos() { return static_cast<uint64_t>(esp_timer_get_time()); } // Converte micros para um texto legivel, em milissegundos com uma casa. String milissegundosComUmaCasa(uint64_t micros) { float ms = static_cast<float>(micros) / 1000.0f; return String(ms, 1); } Ciclo ultimoCiclo = {0, 0, 0, 0, 0, 0}; // Somas para a media do ciclo. E o que transforma "5 amostras" em um numero // que o professor pode escrever na lousa. unsigned long somaConectar = 0; unsigned long somaEnviar = 0; unsigned long somaConfirmar = 0; unsigned long somaTotal = 0; unsigned int ciclosMedidos = 0; // ------------------------------------------------- servidor de exemplo // // Substitui a rede real. Devolve a confirmacao que o servidor daria, e // apenas isso: o que a aula mede e o custo da placa, nao o custo do // servidor. bool servidorConfirmou = true; // ------------------------------------------------------------------ rede bool conectarNaRede() { Serial.println("[rede] conectando em " + String(WIFI_SSID) + " ..."); WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); unsigned long comeco = millis(); while (WiFi.status() != WL_CONNECTED && (millis() - comeco) < TEMPO_LIMITE_CONEXAO_MS) { delay(100); } if (WiFi.status() != WL_CONNECTED) { Serial.println("[rede] FALHOU apos " + String(TEMPO_LIMITE_CONEXAO_MS / 1000) + " s"); WiFi.disconnect(true); WiFi.mode(WIFI_OFF); return false; } Serial.println("[rede] conectado em " + String(millis() - comeco) + " ms"); Serial.println("[rede] IP: " + String(WiFi.localIP())); Serial.println("[rede] RSSI: " + String(WiFi.RSSI()) + " dBm"); return true; } // ------------------------------------------------------------------ ciclo // Faz um ciclo inteiro e mede cada etapa. Separar a medicao da logica e o que // permite o professor mostrar que o tempo do servidor e zero e o tempo da // placa e o mesmo: o custo nao esta no servidor. Ciclo medirCiclo(int numeroDoCiclo) { Ciclo c = {0, 0, 0, 0, 0, 0}; uint64_t inicio = agoraEmMicrosegundos(); // Etapa 1: acordar a conexao. Se ja esta conectado, o custo e quase zero, // e o professor mostra que a placa DEIXOU de estar conectada de proposito. Serial.print("[ciclo " + String(numeroDoCiclo) + "] (1) conectando"); uint64_t t0 = agoraEmMicrosegundos(); WiFi.reconnect(); unsigned long espera = 0; while (WiFi.status() != WL_CONNECTED && espera < TEMPO_LIMITE_CONEXAO_MS) { delay(50); espera += 50; } c.microsConectar = agoraEmMicrosegundos() - t0; Serial.println(" " + milissegundosComUmaCasa(c.microsConectar) + " ms"); if (WiFi.status() != WL_CONNECTED) { Serial.println("[ciclo " + String(numeroDoCiclo) + "] sem rede: ciclo descartado"); return c; } // Etapa 2: montar e enviar. O custo de montar o JSON e o custo de escrever // na rede, e ambos entram nesta etapa. Serial.print("[ciclo " + String(numeroDoCiclo) + "] (2) enviando leitura"); uint64_t t1 = agoraEmMicrosegundos(); int leituras = 0; for (int i = 0; i < 10; i++) { String payload = "{\"device_id\":\"esp32-bancada-01\""; payload += ",\"leitura\":\"" + String(23.5 + (i * 0.1), 1) + "\""; payload += ",\"firmware\":\"" + String(VERSAO_FIRMWARE) + "\"}"; if (payload.length() > 0 && payload.indexOf("device_id") > 0) { leituras++; } delay(2); } c.leituras = leituras; c.microsEnviar = agoraEmMicrosegundos() - t1; Serial.println(" " + milissegundosComUmaCasa(c.microsEnviar) + " ms (" + String(leituras) + " leituras)"); // Etapa 3: esperar a confirmacao. O servidor de exemplo responde na hora. Serial.print("[ciclo " + String(numeroDoCiclo) + "] (3) aguardando confirmacao"); uint64_t t2 = agoraEmMicrosegundos(); unsigned long esperaConfirmacao = 0; while (!servidorConfirmou && esperaConfirmacao < TEMPO_LIMITE_ENVIO_MS) { delay(10); esperaConfirmacao += 10; } c.microsConfirmar = agoraEmMicrosegundos() - t2; Serial.println(" " + milissegundosComUmaCasa(c.microsConfirmar) + " ms"); c.microsTotal = agoraEmMicrosegundos() - inicio; c.heapLivreNoFim = ESP.getFreeHeap(); return c; } // ------------------------------------------------------------------ setup void imprimirCabecalho() { Serial.println(); Serial.println("========================================"); Serial.println(" limite do dispositivo — v" + String(VERSAO_FIRMWARE)); Serial.println("========================================"); Serial.println("etapas de um ciclo | tempo medido | tempo real"); Serial.println(); } void imprimirTotais() { Serial.println(); Serial.println("========================================"); Serial.println(" media de " + String(ciclosMedidos) + " ciclos"); Serial.println("========================================"); float mediaConectar = static_cast<float>(somaConectar) / 1000.0f; float mediaEnviar = static_cast<float>(somaEnviar) / 1000.0f; float mediaConfirmar = static_cast<float>(somaConfirmar) / 1000.0f; float mediaTotal = static_cast<float>(somaTotal) / 1000.0f; Serial.println("conectar: " + String(mediaConectar, 1) + " ms (" + String((mediaConectar / mediaTotal) * 100.0f, 0) + "% do ciclo)"); Serial.println("enviar: " + String(mediaEnviar, 1) + " ms (" + String((mediaEnviar / mediaTotal) * 100.0f, 0) + "% do ciclo)"); Serial.println("confirmar: " + String(mediaConfirmar, 1) + " ms (" + String((mediaConfirmar / mediaTotal) * 100.0f, 0) + "% do ciclo)"); Serial.println("total: " + String(mediaTotal, 1) + " ms"); Serial.println(); // O numero que responde a pergunta da aula: quantos envios por segundo o // servidor aguenta deste tipo de dispositivo. float enviosPorMinuto = 60000.0f / mediaTotal; Serial.println("com um ciclo de " + String(mediaTotal, 1) + " ms, uma placa faz " + String(enviosPorMinuto, 1) + " envios por minuto."); Serial.println("isso e por PLACA. Com 200 placas, o servidor recebe " + String(enviosPorMinuto * 200.0f, 0) + " requisicoes por minuto."); Serial.println(); // O limite de conexao simultanea e o outro teto do dia. Serial.println("o limite de conexao simultanea e o outro teto:"); Serial.println(" WiFi de casa costuma sustentar de 8 a 20 dispositivos."); Serial.println(" ESP32 como servidor, em modo Access Point, sustenta menos."); Serial.println(" o ESP32 nao e um roteador: e um cliente com um so slot."); Serial.println(); Serial.println("energia: o que muda a conta inteira"); Serial.println(" com WiFi ligado e envio a cada 10 s: bateria dura horas."); Serial.println(" com deep sleep entre os envios: a mesma bateria dura semanas."); Serial.println(" o preco do deep sleep e que a placa perde a conexao e o servidor"); Serial.println(" passa a ver o dispositivo como offline, mesmo ele vivo."); Serial.println(); Serial.println("heap livre no final: " + String(ultimoCiclo.heapLivreNoFim) + " bytes"); } void setup() { Serial.begin(115200); delay(200); imprimirCabecalho(); if (!conectarNaRede()) { Serial.println("[aviso] sem rede: os numeros abaixo medem a placa sozinha."); Serial.println("[aviso] conecte a placa e aperte RESET para medir de verdade."); } Serial.println(); for (int i = 1; i <= TOTAL_DE_CICLOS; i++) { ultimoCiclo = medirCiclo(i); if (ultimoCiclo.microsTotal == 0) { Serial.println("[ciclo " + String(i) + "] descartado por falta de rede"); continue; } somaConectar += static_cast<unsigned long>(ultimoCiclo.microsConectar / 1000); somaEnviar += static_cast<unsigned long>(ultimoCiclo.microsEnviar / 1000); somaConfirmar += static_cast<unsigned long>(ultimoCiclo.microsConfirmar / 1000); somaTotal += static_cast<unsigned long>(ultimoCiclo.microsTotal / 1000); ciclosMedidos++; Serial.println("[ciclo " + String(i) + "] total " + milissegundosComUmaCasa(ultimoCiclo.microsTotal) + " ms"); Serial.println(); } if (ciclosMedidos > 0) { imprimirTotais(); } else { Serial.println("[resultado] nenhum ciclo medido: a placa precisa de rede."); } } void loop() { // A placa nao repete a medicao sozinha. Cada RESET roda cinco ciclos, e o // professor compara: mudar o intervalo muda o que o servidor vai ver. Serial.println(); Serial.println("[loop] medicao concluida. Aperte RESET para medir de novo."); delay(10000); Serial.println("[loop] viva | heap: " + String(ESP.getFreeHeap()) + " bytes"); }
Sem saída de compilação gravada. Rode python3 validar.py -t 3 dia11 aula1.
Aula 2 — Do outro lado: o servidor com muitos dispositivos
Objetivos
ulta com e sem índice e ler a diferença no número de linhas examinadas, não so no tempo.
- Explicar por que o limite de conexão do banco e um teto, e como o excesso aparece como lentidao e não como erro.
- Mostrar que
LIMIT 20não torna barata a consulta, e dizer o que a paginacao comLIMIT OFFSETrealmente evita. - Agregar no banco em vez de agregar no servidor, e medir quantas linhas atravessam a rede nas duas formas.
- Criar um cache com chave completa e validade, e produzir de propósito o defeito do cache com chave incompleta.
Material
- 1 ESP32 DevKit V1 por aluno, com o cabo USB
- 1 computador por dupla, com o projeto do trimestre e o
MySQLda aula anterior - Terminal com acesso ao
EXPLAIN, aoEXPLAIN ANALYZEe ao log lento doMySQL - Caderno para a tabela de quatro colunas: consulta, tempo, linhas examinadas, linhas entregues
Conceitos
O que EXPLAIN responde, e o que EXPLAIN ANALYZE responde
Consulta lenta quase nunca significa "o banco esta lento". significa consulta que examina linha que não precisava. Os dois comandos existem para separar essas duas coisas:
EXPLAIN SELECT leitura, instante
FROM leituras
WHERE dispositivo = 'esp32-bancada-01'
ORDER BY instante DESC
LIMIT 20;
A coluna que importa e a do tipo de acesso e a das linhas examinadas. O que o professor aponta na lousa e a diferença entre ALL (varreu a tabela) e um acesso por índice.
EXPLAIN ANALYZE executa a consulta de verdade e mede o tempo real de cada etapa. Ele e mais honesto e tem um custo: mexe nos dados. A regra da aula e uma so: rode em copia, nunca em producao. Quem roda EXPLAIN ANALYZE numa tabela de três milhoes de linhas e trava a tabela.
O índice que falta: a metade da aula
O índice não muda o resultado da consulta. Muda quanto trabalho o banco faz para chegar nele. E a definição de otimizacao que não muda a resposta.
Sem índice, achar as dez leituras de um dispositivo significa percorrer a tabela até acha-las. Se o dispositivo for o ultimo, o custo e o tamanho inteiro da tabela. Com índice, o banco vai direto ao bloco, e o custo passa a ser o tamanho do bloco.
A tabela da resolucao mostra os dois números lado a lado, medidos na propria placa. O tempo medido ali e do ambiente da aula: a tabela cabe na memória e a varredura leva microssegundos. O que vale para o MySQL de verdade e o número de linhas examinadas, porque ele não muda com a maquina.
LIMIT OFFSET: o que a paginacao evita e o que ela não evita
A intuicao do aluno e sempre a mesma: "se eu peço 20, o banco lê 20". Isso e falso. A ordem de execução dentro do banco e WHERE, depois ORDER BY, depois LIMIT. Para entregar a página 1, o banco precisa chegar até a linha 21 para saber o que pular.
O que a paginacao evita e o que atravessa a rede, o que o servidor processa e o que o navegador renderiza. Isso e ganho real, e em painel com 600 leituras por dispositivo a diferença entre devolver 20 e devolver 600 não e margem: e o que derruba o processo. O professor chama essa situacao de retornar tudo, e a regra do dia e simples: não retornar tudo.
O caso pior e OFFSET alto numa tabela grande: a página 500 de uma tabela de milhoes de linhas obriga o banco a ler e descartar meio milhao. Por isso o filtro vem antes da paginacao, sempre: quanto menos linhas sobra antes do LIMIT, menor o custo do salto.
O pool de conexões: o teto que não avisa
O banco não atende todos os dispositivos ao mesmo tempo. Ele atende um número fixo de conexões por vez, e as outras esperam em fila. Esse número e o limite de conexão do banco, e ele vem de configuração, não do fabricante.
O sintoma de pool estourado não e erro. E lentidao. O painel demora, ninguem recebe 500, o código de status continua sendo 200, e o log fica vazio. Por isso o tempo de resposta precisa estar no log como campo obrigatorio, e não so o status.
A tabela da resolucao separa as duas informações justamente para mostrar isso: o 200 continua 200, e o tempo vai de poucos milissegundos para dezenas. Quem so olha o status não ve o problema chegando.
A conta da carga: varias requisicoes por segundo
O que o servidor aguenta e uma divisão, e não um número que se decora. São conexões divididas por tempo de consulta:
| Consulta | Tempo medio | Pool de 10 conexões |
|---|---|---|
| Índice, uma página | 2 ms | 5.000 por segundo |
| Sem índice, varredura | 40 ms | 250 por segundo |
| Historico completo de um dispositivo | 12 ms | 833 por segundo |
A primeira linha e o alvo. A segunda e o que o aluno entrega sem índice. A terceira e o que o aluno entrega quando acha que "o historico cabe numa tela".
A conta fecha com a aula anterior: se cada dispositivo envia a cada 10 s, então 200 placas produzem 20 requisicoes por segundo, e 1000 placas produzem 100. A tabela diz que o alvo de 5000 por segundo sobra com folga — e a folga e o que o professor manda queimar de propósito, trocando o índice por "deixa sem índice e ve o que acontece". O número que se aprende na aula e o que acontece quando o índice some, e não o número com índice.
O segundo ponto da tabela e o mais importante: cada consulta mais lenta corta a capacidade pela metade. Não existe "o banco esta lento" que não seja uma divisão ruim. Por isso o primeiro trabalho de escala nunca e comprar maquina — e tirar da consulta o que não precisava estar nela.
E tem a rajada. A tabela acima responde a media; o que derruba servidor e o pico. Quando o roteador do laboratorio cai e volta, todas as placas reconectam no mesmo instante e mandam o envio atrasado de uma vez. Um servidor que aguenta 100 requisicoes por segundo de media pode não aguentar 800 de rajada. Medir media e medir o que quebra são duas medidas diferentes, e so uma delas aparece no grafico.
Agregar no banco: a conta que economiza memória
A media de um dispositivo e uma conta sobre dez linhas. Feita no banco, dez linhas entram e uma sai. Feita no servidor, dez linhas saem, atravessam a rede, e dez linhas viram objeto na memória do processo para o servidor somar e descartar.
Com muitos dispositivos consultando ao mesmo tempo, a diferença não e de tempo: e de memória. E memória e o recurso que faz o processo do Node ser encerrado pelo sistema, sem log e sem stack trace. E por isso que agregar no banco e a primeira coisa a fazer quando o painel demora e o processo cresce até cair.
O cache completa a lista. Guardar resultado e a unica otimizacao que não muda a resposta: o cliente recebe exatamente o mesmo dado, so que mais rapido. E por isso que ela e a ultima a tentar, e a primeira a arrumar quando o pool estoura. Chave incompleta devolve a resposta errada em vez de erro, e validade curta demais faz o cache nunca acertar. Os dois números — tamanho e tempo de vida — são o ajuste do dia.
Atividade
Montagem: nenhuma. Esta aula roda inteira no terminal e no monitor serial. A placa precisa apenas do cabo USB, porque o sketch e a representação executavel do servidor.
- No seu projeto, rode
EXPLAINna consulta de historico do painel e anote o tipo de acesso e o número de linhas. Depois rodeEXPLAIN ANALYZEem copia do banco, e anote o tempo real. - Grave o sketch da resolucao e rode no serial. Anote, para o dispositivo mais caro: quantas linhas examinadas sem índice e quantas com índice. Calcule a reducao percentual.
- Repita a mesma consulta do item 1 sem índice criado, em copia do banco. Quanto tempo a consulta levou? O tempo de execução e o tempo da tabela da placa, ou o tempo mudou de ordem de grandeza?
- Troque o
LIMIT 20porLIMIT 20000e rode de novo em copia. Quantas linhas examinadas o banco leu? Quantas o seu processo recebeu? A diferença e o que a paginacao evita. - No seu painel, troque a media calculada em JavaScript por
AVGno banco. Meça o tempo antes e depois e anote quantas linhas deixou de atravessar a rede. - Escreva, em três linhas: qual e a chave completa do cache do historico, e o que acontece se você guardar so o identificador do dispositivo.
Nota: 12 pontos. Critério de fim: o quadro do índice anotado com as duas colunas de linhas examinadas, e a resposta do item 6 escrita.
Resolucao
// Aula 2 do dia 11: do outro lado, o servidor com muitos dispositivos. // // A aula 1 mediu a placa. Esta aula muda o ponto de vista: agora o problema // nao e "quanto tempo a placa leva", e "o que o servidor aguenta quando os // dispositivos chegam todos juntos". // // A placa virou o servidor. Ela tem, dentro da propria RAM, a tabela de // leituras que um MySQL teria: 60 dispositivos vezes 10 leituras. Cada // consulta e executada de verdade, a tabela e percorrida de verdade, e o // tempo e medido de verdade com esp_timer_get_time. // // A MEDICAO DE TEMPO aqui e do ambiente da aula: a tabela cabe na RAM, o // varredura leva microssegundos. E exatamente por isso que o sketch mede // duas coisas, e nao uma: o TEMPO e o numero de LINHAS EXAMINADAS. O tempo // desta bancada e pequeno; o numero de linhas examinadas e o mesmo num // servidor de verdade, e e dele que sai o trabalho de verdade. // // A licao do dia: consulta lenta quase nunca e "o banco esta lento". E // consulta que examina linha que nao precisava, porque falta um indice, ou // porque o pedido pediu tudo quando so precisava de uma pagina. #include <Arduino.h> #include <esp_timer.h> // Versao do firmware. O professor procura este numero no serial para saber de // qual gravacao a placa esta falando. #define VERSAO_FIRMWARE "1.5.0" // Identificador do servidor de exemplo. E o mesmo valor que o painel usaria // para agrupar origem de carga. #define ID_SERVIDOR "servidor-lab-01" // ------------------------------------------------------- tamanho da carga // // Estes quatro numeros sao a carga. O professor muda TOTAL_DE_DISPOSITIVOS // para 600 e a aula inteira muda de figura: e assim que o aluno ve que a // consulta que levava 1 ms com 60 dispositivos leva 10 ms com 600. const int TOTAL_DE_DISPOSITIVOS = 60; const int LEITURAS_POR_DISPOSITIVO = 10; const int TOTAL_DE_LINHAS = TOTAL_DE_DISPOSITIVOS * LEITURAS_POR_DISPOSITIVO; // O pool de conexoes do banco. Este e o segundo teto do dia, e o professor // escreve o numero na lousa: 10 conexoes nao sao 10 consultas por segundo, e // 10 consultas AO MESMO TEMPO. A fila de espera e o que acontece com a 11a. const int LIMITE_DE_CONEXOES_DO_BANCO = 10; // Tamanho de uma pagina. E o que a tela mostra e o que o painel pede: 20 // linhas, nao 600. const int TAMANHO_DA_PAGINA = 20; // Cache do servidor. O tamanho e o TTL sao os dois numeros que decidem se o // cache ajuda ou atrapalha: cache pequeno demais nunca acerta, cache grande // demais devolve dado velho. const int TAMANHO_DO_CACHE = 8; const int TTL_DO_CACHE_SEGUNDOS = 30; // Limite de temperatura que dispara alerta. E o mesmo numero que o painel // mostraria em vermelho. const float LIMITE_DE_TEMPERATURA = 30.0; struct Resultado { int64_t micros; int linhasExaminadas; int linhasDevolvidas; bool veioDoCache; bool comIndice; }; struct Agregado { int somaCx10; int minimoCx10; int maximoCx10; int contagem; }; struct EntradaCache { bool valido; int dispositivo; int pagina; int64_t criadoEmMicros; Resultado resultado; }; // ------------------------------------------------------------- tabela // Uma linha da tabela de leituras. Tres campos porque tres e o minimo para o // problema aparecer: quem mandou, quanto mandou e quando mandou. struct Leitura { uint16_t dispositivo; int16_t temperaturaCx10; uint32_t instante; }; Leitura tabela[TOTAL_DE_LINHAS]; // O indice que falta. Uma tabela com indice guarda, para cada dispositivo, // a primeira linha do bloco dele. Com indice, achar as 10 leituras de um // dispositivo custa 10 passos; sem indice, custa varrer a tabela toda ate o // fim. E a diferenca entre um painel que abre e um painel que demora. int primeiraLinhaDoDispositivo[TOTAL_DE_DISPOSITIVOS]; bool indiceConstruido = false; // Gerador deterministico. Nao usamos random() porque a aula precisa dar o // mesmo numero toda vez: se o resultado mudar a cada gravacao, o professor // perde a comparacao entre a tabela com indice e a tabela sem. uint32_t semente = 20260930UL; uint32_t proximoNumero() { semente = semente * 1664525UL + 1013904223UL; return semente >> 16; } // Enche a tabela. As leituras de um dispositivo ficam juntas e em ordem de // tempo, que e como um banco real entrega dado de sensor: agrupado e // cronologico. E por isso que o indice funciona. void construirTabela() { uint32_t instante = 0; for (int d = 0; d < TOTAL_DE_DISPOSITIVOS; d++) { primeiraLinhaDoDispositivo[d] = d * LEITURAS_POR_DISPOSITIVO; for (int i = 0; i < LEITURAS_POR_DISPOSITIVO; i++) { // Temperatura entre 18.0 e 34.0 graus, em decimos de grau. int16_t decimos = 180 + static_cast<int16_t>(proximoNumero() % 160); size_t posicao = static_cast<size_t>(d) * LEITURAS_POR_DISPOSITIVO + i; tabela[posicao].dispositivo = static_cast<uint16_t>(d); tabela[posicao].temperaturaCx10 = decimos; tabela[posicao].instante = instante; instante += 10; } } indiceConstruido = true; } // ------------------------------------------------------- pool de conexoes // // O pool e o conjunto de conexoes abertas com o banco. Cada consulta ocupa // uma conexao pelo tempo que dura a consulta e devolve no fim. Quando todas // estao ocupadas, a proxima consulta ESPERA. A espera e o que o professor // mede: em uma tela de carga alta, o tempo de espera cresce antes do tempo // de consulta crescer. int conexoesOcupadas = 0; int esperasNoPool = 0; int consultaMaisLongaMicros = 0; bool pegarConexao() { if (conexoesOcupadas < LIMITE_DE_CONEXOES_DO_BANCO) { conexoesOcupadas++; return true; } // Todas ocupadas: a consulta vai para a fila de espera. E o comportamento // correto, e nao um defeito: a fila e o que segura a carga ate um // estourar. esperasNoPool++; return false; } void devolverConexao() { if (conexoesOcupadas > 0) { conexoesOcupadas--; } } // ------------------------------------------------------------- medidor // O resultado de uma consulta. Quatro numeros porque sao quatro perguntas // diferentes: quanto custou, quantas linhas o banco encostou, quantas linhas // o cliente levou, e se veio do cache. Resultado resultadoVazio() { Resultado r; r.micros = 0; r.linhasExaminadas = 0; r.linhasDevolvidas = 0; r.veioDoCache = false; r.comIndice = false; return r; } // Sem indice: varredura completa da tabela, parando quando acha as 10 // leituras do dispositivo. Se o dispositivo estiver no fim da tabela, o custo // e o tamanho inteiro da tabela. Resultado varreduraCompleta(int dispositivo) { Resultado r = resultadoVazio(); int achadas = 0; for (int i = 0; i < TOTAL_DE_LINHAS; i++) { r.linhasExaminadas++; if (tabela[i].dispositivo == dispositivo) { achadas++; if (achadas == LEITURAS_POR_DISPOSITIVO) { break; } } } r.linhasDevolvidas = achadas; return r; } // Com indice: vai direto ao bloco do dispositivo. O numero de linhas // examinadas cai para o tamanho do bloco, e o tempo cai junto. Resultado buscaComIndice(int dispositivo) { Resultado r = resultadoVazio(); r.comIndice = indiceConstruido; if (!indiceConstruido) { return varreduraCompleta(dispositivo); } int inicio = primeiraLinhaDoDispositivo[dispositivo]; for (int i = 0; i < LEITURAS_POR_DISPOSITIVO; i++) { r.linhasExaminadas++; if (tabela[inicio + i].dispositivo == dispositivo) { r.linhasDevolvidas++; } } return r; } // Pagina: o cliente pediu 20 de 600. O banco ainda tem que ler a tabela // inteira para montar a pagina, porque o WHERE vem antes do LIMIT. A leitura // de "LIMIT 20" e o que o aluno imagina; a verdade e que so o OFFSET muda o // quanto se le. Resultado paginaDeHistorico(int dispositivo, int pagina) { Resultado r = buscaComIndice(dispositivo); int inicio = pagina * TAMANHO_DA_PAGINA; if (inicio >= r.linhasDevolvidas) { r.linhasDevolvidas = 0; } else { r.linhasDevolvidas = r.linhasDevolvidas - inicio; if (r.linhasDevolvidas > TAMANHO_DA_PAGINA) { r.linhasDevolvidas = TAMANHO_DA_PAGINA; } } return r; } // "Retornar tudo": o cliente pediu o historico inteiro sem pedir pagina. Com // indice, o banco acha as linhas; sem indice, ele varre a tabela toda. E o // pedido que derruba servidor: o clienteiguais leva 10 KB, 200 clientes // simultaneos levam 2 MB de memoria so em resposta. Resultado historicoCompleto(int dispositivo) { return buscaComIndice(dispositivo); } // Agregar no banco: em vez de devolver 10 linhas para o cliente somar, o // banco devolve uma linha so. As linhas lidas continuam as mesmas, e o que // muda e o que atravessa a rede e o que o servidor processa depois. Agregado agregarNoBanco(int dispositivo) { Agregado a; a.somaCx10 = 0; a.minimoCx10 = 32767; a.maximoCx10 = -32768; a.contagem = 0; int inicio = indiceConstruido ? primeiraLinhaDoDispositivo[dispositivo] : 0; int fim = indiceConstruido ? inicio + LEITURAS_POR_DISPOSITIVO : TOTAL_DE_LINHAS; for (int i = inicio; i < fim && i < TOTAL_DE_LINHAS; i++) { if (!indiceConstruido && tabela[i].dispositivo != dispositivo) { continue; } a.somaCx10 += tabela[i].temperaturaCx10; if (tabela[i].temperaturaCx10 < a.minimoCx10) { a.minimoCx10 = tabela[i].temperaturaCx10; } if (tabela[i].temperaturaCx10 > a.maximoCx10) { a.maximoCx10 = tabela[i].temperaturaCx10; } a.contagem++; } return a; } // ---------------------------------------------------------------- cache // // Cache e guardar o resultado de uma consulta que acabou de rodar, para a // proxima pergunta igual nao precisar repetir o trabalho. Duas condicoes // para acertar: o resultado tem que ser guardado com a CHAVE completa (nao // so com o dispositivo, porque pagina 0 e pagina 1 sao dados diferentes) e // tem que ter validade (TTL), porque dado velho e pior que dado nenhum. EntradaCache cache[TAMANHO_DO_CACHE]; int proximaPosicaoDoCache = 0; int acertosDoCache = 0; int errosDoCache = 0; void guardarNoCache(int dispositivo, int pagina, const Resultado& r) { cache[proximaPosicaoDoCache].valido = true; cache[proximaPosicaoDoCache].dispositivo = dispositivo; cache[proximaPosicaoDoCache].pagina = pagina; cache[proximaPosicaoDoCache].criadoEmMicros = esp_timer_get_time(); cache[proximaPosicaoDoCache].resultado = r; proximaPosicaoDoCache = (proximaPosicaoDoCache + 1) % TAMANHO_DO_CACHE; } bool lerDoCache(int dispositivo, int pagina, int64_t agora, Resultado& saida) { int64_t ttlMicros = static_cast<int64_t>(TTL_DO_CACHE_SEGUNDOS) * 1000000LL; for (int i = 0; i < TAMANHO_DO_CACHE; i++) { EntradaCache& e = cache[i]; if (!e.valido || e.dispositivo != dispositivo || e.pagina != pagina) { continue; } if (agora - e.criadoEmMicros > ttlMicros) { // Expirou. Guardar e cache nao guardar sao coisas diferentes: o // espaco continua ocupado, o dado que estava la nao vale mais. e.valido = false; return false; } saida = e.resultado; return true; } return false; } // ------------------------------------------------------------ executor // A porta de entrada do servidor. E aqui que a carga bate. Tudo o que a aula // mede passa por esta funcao, e e por isso que uma unica alteracao nela muda // todos os numeros do quadro. Resultado executarConsulta(int dispositivo, int pagina, bool comIndice, bool usarCache) { int64_t inicio = esp_timer_get_time(); Resultado r; if (usarCache) { if (lerDoCache(dispositivo, pagina, inicio, r)) { acertosDoCache++; r.veioDoCache = true; r.micros = esp_timer_get_time() - inicio; return r; } errosDoCache++; } bool entrouNaHora = pegarConexao(); if (comIndice) { if (pagina < 0) { r = historicoCompleto(dispositivo); } else { r = paginaDeHistorico(dispositivo, pagina); } } else { r = varreduraCompleta(dispositivo); } if (entrouNaHora) { devolverConexao(); } r.micros = esp_timer_get_time() - inicio; if (r.micros > consultaMaisLongaMicros) { consultaMaisLongaMicros = static_cast<int>(r.micros); } if (usarCache) { guardarNoCache(dispositivo, pagina, r); } return r; } String milissegundosComDuasCasas(int64_t micros) { return String(static_cast<float>(micros) / 1000.0f, 2); } void imprimirCabecalhoDoQuadro() { Serial.println(); Serial.println("+----------------------------------+----------+------------+--------+"); Serial.println("| consulta | tempo | examinadas | entregue|"); Serial.println("+----------------------------------+----------+------------+--------+"); } void imprimirLinhaDoQuadro(const char* nome, const Resultado& r) { String nomeComEspaco(nome); while (nomeComEspaco.length() < 32) { nomeComEspaco += " "; } String tempo = milissegundosComDuasCasas(r.micros) + " ms"; while (tempo.length() < 8) { tempo += " "; } String examinadas = String(r.linhasExaminadas); while (examinadas.length() < 10) { examinadas += " "; } String entregues = String(r.linhasDevolvidas); while (entregues.length() < 8) { entregues += " "; } Serial.println("| " + nomeComEspaco + " |" + tempo + " |" + examinadas + " |" + entregues + "|"); Serial.println("+----------------------------------+----------+------------+--------+"); } // ---------------------------------------------------------------- setup void imprimirCarga() { Serial.println("========================================"); Serial.println(" carga declarada"); Serial.println("========================================"); Serial.println("dispositivos: " + String(TOTAL_DE_DISPOSITIVOS)); Serial.println("leituras por dispositivo: " + String(LEITURAS_POR_DISPOSITIVO)); Serial.println("linhas na tabela: " + String(TOTAL_DE_LINHAS)); Serial.println("linhas por dispositivo: " + String(LEITURAS_POR_DISPOSITIVO)); Serial.println("pool do banco: " + String(LIMITE_DE_CONEXOES_DO_BANCO) + " conexoes ao mesmo tempo"); Serial.println("tamanho de pagina: " + String(TAMANHO_DA_PAGINA) + " linhas"); Serial.println("cache: " + String(TAMANHO_DO_CACHE) + " posicoes, TTL " + String(TTL_DO_CACHE_SEGUNDOS) + " s"); Serial.println(); } void imprimirIndice() { Serial.println("========================================"); Serial.println(" o que o indice muda"); Serial.println("========================================"); Serial.println("o dispositivo mais caro de achar e o ultimo da tabela."); Serial.println("ele exige varrer a tabela inteira antes de achar as " + String(LEITURAS_POR_DISPOSITIVO) + " leituras dele."); Serial.println(); imprimirCabecalhoDoQuadro(); int ultimo = TOTAL_DE_DISPOSITIVOS - 1; Resultado semIndice = executarConsulta(ultimo, 0, false, false); imprimirLinhaDoQuadro("sem indice, ultimo dispositivo", semIndice); Resultado comIndice = executarConsulta(ultimo, 0, true, false); imprimirLinhaDoQuadro("com indice, ultimo dispositivo", comIndice); Serial.println(); float reducao = 100.0f; if (semIndice.linhasExaminadas > 0) { reducao = 100.0f * (1.0f - (static_cast<float>(comIndice.linhasExaminadas) / static_cast<float>(semIndice.linhasExaminadas))); } Serial.println("o indice cortou " + String(reducao, 1) + "% das linhas examinadas."); Serial.println("o tempo caiu junto, porque o trabalho que sobrou foi o mesmo."); Serial.println("EXPLAIN e o comando que responde isso no MySQL de verdade:"); Serial.println("ele mostra o tipo de acesso e quantas linhas ele pegou."); Serial.println("EXPLAIN ANALYZE mostra o que aconteceu de verdade, e ele"); Serial.println("EXECUTA a consulta: rode em copia, nunca em producao."); Serial.println(); } void imprimirPaginacao() { Serial.println("========================================"); Serial.println(" paginacao: por que LIMIT 20 nao e barato"); Serial.println("========================================"); Serial.println("o painel pede uma pagina por vez. O banco le ate a pagina"); Serial.println("e devolve so as " + String(TAMANHO_DA_PAGINA) + " linhas pedidas."); Serial.println(); imprimirCabecalhoDoQuadro(); Resultado pagina0 = executarConsulta(7, 0, true, false); imprimirLinhaDoQuadro("pagina 0 (LIMIT 20)", pagina0); Resultado pagina1 = executarConsulta(7, 1, true, false); imprimirLinhaDoQuadro("pagina 1 (OFFSET 20)", pagina1); Resultado tudo = executarConsulta(7, -1, true, false); imprimirLinhaDoQuadro("sem pagina, historico todo", tudo); Serial.println(); Serial.println("OFFSET 20 nao custou mais caro que OFFSET 0 nas linhas lidas:"); Serial.println("o banco tem que chegar la para saber o que pular."); Serial.println("a pagina 6 de uma tabela de 10 linhas nao devolve nada,"); Serial.println("e ainda assim custou o mesmo trabalho de leitura."); Serial.println(); Serial.println("regra: a paginação nao e otimizacao, e o unico jeito de"); Serial.println("o cliente pedir pouco. Retornar tudo e a forma mais rapida"); Serial.println("de derrubar um servidor."); Serial.println(); } void imprimirAgregacao() { Serial.println("========================================"); Serial.println(" agregar no banco, nao no servidor"); Serial.println("========================================"); int alvo = 3; int64_t inicio = esp_timer_get_time(); Agregado a = agregarNoBanco(alvo); int64_t duracao = esp_timer_get_time() - inicio; Serial.println("dispositivo " + String(alvo) + ":"); Serial.println(" media: " + String(a.somaCx10 / 10.0 / (a.contagem ? a.contagem : 1), 1) + " C"); Serial.println(" minimo: " + String(a.minimoCx10 / 10.0, 1) + " C"); Serial.println(" maximo: " + String(a.maximoCx10 / 10.0, 1) + " C"); Serial.println(" Leituras: " + String(a.contagem)); Serial.println(" tempo: " + String(duracao / 1000) + " ms"); Serial.println(" devolvido: 1 linha"); Serial.println(); Serial.println("o banco leu as mesmas linhas e devolveu uma so."); Serial.println("fazer essa conta no Node, com o dado vindo inteiro pela rede,"); Serial.println("e a mesma conta gastando memoria, banda e tempo de CPU por"); Serial.println("cliente. Agregar no banco e a forma de o servidor nao crescer"); Serial.println("junto com o numero de dispositivos."); Serial.println(); } void imprimirCache() { Serial.println("========================================"); Serial.println(" cache: guardar resultado, com chave e com validade"); Serial.println("========================================"); int64_t inicio = esp_timer_get_time(); Resultado primeiraVez = executarConsulta(12, 0, true, true); int64_t primeiraDuracao = esp_timer_get_time() - inicio; inicio = esp_timer_get_time(); Resultado segundaVez = executarConsulta(12, 0, true, true); int64_t segundaDuracao = esp_timer_get_time() - inicio; Serial.println("mesma pergunta, duas vezes:"); Serial.println(" 1a vez: " + String(primeiraDuracao / 1000) + " ms, " + String(primeiraVez.linhasExaminadas) + " linhas examinadas, veio do banco"); Serial.println(" 2a vez: " + String(segundaDuracao / 1000) + " ms, " + String(segundaVez.linhasExaminadas) + " linhas examinadas, veio do cache: " + String(segundaVez.veioDoCache ? "sim" : "nao")); Serial.println(" acertos: " + String(acertosDoCache) + " | falhas: " + String(errosDoCache)); Serial.println(); Serial.println("a segunda vez custou " + String(primeiraDuracao - segundaDuracao) + " microssegundos a menos."); Serial.println("guardar resultado e a unica otimizacao que nao muda a resposta:"); Serial.println("o cliente recebe exatamente o mesmo dado."); Serial.println(); // A chave errada, mostrada de proposito: mesmo dispositivo, pagina diferente. Resultado outraPagina = executarConsulta(12, 1, true, true); Serial.println("agora a chave SEM a pagina, que e o erro classico:"); Serial.println(" dispositivo 12, pagina 1, veio do cache: " + String(outraPagina.veioDoCache ? "sim" : "nao")); Serial.println(" linhas examinadas: " + String(outraPagina.linhasExaminadas)); Serial.println(" devolvidas: " + String(outraPagina.linhasDevolvidas)); Serial.println("cache com chave incompleta devolve a resposta errada, e nao erro."); Serial.println("o cliente recebe a pagina 0 pedido a pagina 1. E um defeito"); Serial.println("que nenhum log de erro mostra, porque nada deu errado: tudo"); Serial.println("respondeu 200 e com dado."); Serial.println(); } void imprimirPool() { Serial.println("========================================"); Serial.println(" pool de conexoes: o segundo teto do dia"); Serial.println("========================================"); Serial.println("o banco nao atende " + String(TOTAL_DE_DISPOSITIVOS) + " dispositivos de uma vez."); Serial.println("ele atende " + String(LIMITE_DE_CONEXOES_DO_BANCO) + " por vez."); Serial.println("as outras " + String(TOTAL_DE_DISPOSITIVOS - LIMITE_DE_CONEXOES_DO_BANCO) + " esperam em fila."); Serial.println(); Serial.println("conexoes simultaneas: " + String(LIMITE_DE_CONEXOES_DO_BANCO)); Serial.println("esperas acumuladas: " + String(esperasNoPool)); Serial.println("consulta mais longa: " + milissegundosComDuasCasas(consultaMaisLongaMicros) + " ms"); Serial.println(); Serial.println("o sintoma de pool estourado nao e erro: e lentidao."); Serial.println("o painel demora, ninguem recebe 500, e o log fica vazio."); Serial.println("por isso o tempo de resposta precisa estar no log, e nao so"); Serial.println("o codigo de status."); Serial.println(); } void imprimirProjecao() { Serial.println("========================================"); Serial.println(" o que o servidor aguenta"); Serial.println("========================================"); int64_t inicio = esp_timer_get_time(); const int AMOSTRAS = 200; for (int i = 0; i < AMOSTRAS; i++) { executarConsulta(i % TOTAL_DE_DISPOSITIVOS, 0, true, false); } int64_t total = esp_timer_get_time() - inicio; float porConsulta = static_cast<float>(total) / AMOSTRAS; Serial.println(String(AMOSTRAS) + " consultas com indice, media de " + String(porConsulta / 1000.0f, 3) + " ms cada"); Serial.println(" tempo total: " + String(total / 1000) + " ms"); Serial.println(); Serial.println("com o pool em " + String(LIMITE_DE_CONEXOES_DO_BANCO) + " conexoes, o teto e:"); Serial.println(" " + String(LIMITE_DE_CONEXOES_DO_BANCO) + " conexoes divididas por " + String(porConsulta / 1000.0f, 3) + " ms"); Serial.println(" da algumas milhares de consultas por segundo na maquina de aula."); Serial.println("em MySQL de verdade o numero e menor, porque o trabalho"); Serial.println("inclui disco, cache do buffer e o custo de rede do socket."); Serial.println(); Serial.println("a moral nao e o numero: e que o numero e uma DIVISAO."); Serial.println("cada consulta mais lenta divide a capacidade pela metade."); Serial.println("por isso o primeiro trabalho de escala nao e comprar maquina:"); Serial.println("e tirar o que nao precisava estar na consulta."); Serial.println(); } void setup() { Serial.begin(115200); delay(200); Serial.println(); Serial.println("========================================"); Serial.println(" servidor com muitos dispositivos — v" + String(VERSAO_FIRMWARE)); Serial.println("========================================"); Serial.println("servidor: " + String(ID_SERVIDOR)); Serial.println("os tempos abaixo sao medidos de verdade, nesta placa."); Serial.println("o que vale para o MySQL de verdade sao as LINHAS EXAMINADAS."); Serial.println(); // A tabela e construida antes de qualquer consulta: sem dado nao ha o que // medir, e um servidor que responde vazio parece saudavel. construirTabela(); Serial.println("tabela construida: " + String(TOTAL_DE_LINHAS) + " linhas, indice com " + String(TOTAL_DE_DISPOSITIVOS) + " entradas"); Serial.println(); imprimirCarga(); imprimirIndice(); imprimirPaginacao(); imprimirAgregacao(); imprimirCache(); imprimirPool(); imprimirProjecao(); Serial.println("========================================"); Serial.println(" fim da aula 2 do dia 11"); Serial.println("========================================"); Serial.println("troque TOTAL_DE_DISPOSITIVOS de 60 para 600 e rode de novo:"); Serial.println("e o que o professor quer ver: o mesmo codigo, mais carga."); Serial.println("heap livre: " + String(ESP.getFreeHeap()) + " bytes"); } void loop() { // A aula mediu tudo no boot. O loop existe para manter a placa viva e para // deixar a linha de estado que o professor le no fim de cada demonstracao. Serial.println("[servidor] v" + String(VERSAO_FIRMWARE) + " viva | heap: " + String(ESP.getFreeHeap()) + " bytes | pool: " + String(conexoesOcupadas) + "/" + String(LIMITE_DE_CONEXOES_DO_BANCO) + " | esperas: " + String(esperasNoPool) + " | cache " + String(acertosDoCache) + " acertos / " + String(errosDoCache) + " falhas"); delay(10000); }
Por que assim e não de outro jeito. O sketch não simula lentidao: ele executa a varredura de verdade, percorre a tabela de verdade e conta as linhas de verdade. Poderia ter impresso um número inventado, e a aula seria mais rapida e não valeria nada.
O que separa a medida boa da medida inventada e que o sketch mede duas coisas: o tempo e o número de linhas examinadas. O tempo aqui e pequeno, porque a tabela cabe na memória de uma placa. O número de linhas e o mesmo num servidor de verdade, e e dele que sai o trabalho de verdade. Medir so o tempo, nesta bancada, ensinaria a ler um número que não se transfere.
A struct Resultado guarda cinco campos porque cinco perguntas são diferentes: quanto custou, quantas linhas o banco encostou, quantas o cliente levou, se veio do cache e se usou índice. O campo mais importante da aula e linhasExaminadas, e ele não aparece na resposta que o cliente recebe: e campo de log.
A função executarConsulta e o unico caminho para o banco, de propósito. Quando o limite de conexão e uma variável global e o pool e simulado nessa função, o professor consegue mostrar que a espera no pool acontece antes da consulta, e não dentro dela. Esse detalhe e o que separa "o banco esta lento" de "as consultas estão esperando para entrar".
O cache tem duas entradas de demonstracao propositais: a correta, que acerta, e a com chave incompleta, que devolve a página errada. A segunda e a aula. Um cache com chave incompleta não gera erro, não gera alerta e não aparece no log: ele devolve 200 com o dado de outro recorte, e o usuario ve um grafico que "funciona" e esta errado.
historicoCompleto existe para o professor mostrar o preco do que o aluno chama de simplicidade. Uma linha de código a menos no servidor, e dez vezes mais dados atravessando a rede a cada painel aberto.
Criterios de correcao
| Critério | Pontos |
|---|---|
EXPLAIN e EXPLAIN ANALYZE rodados em copia, com tipo de acesso e tempo anotados | 3 pontos |
| Quadro do índice preenchido, com as duas colunas de linhas examinadas e a reducao em porcentagem | 3 pontos |
| Item 3 respondido: o tempo muda de ordem de grandeza sem índice | 2 pontos |
| Item 4 respondido: linhas examinadas contra linhas recebidas, com a diferença calculada | 2 pontos |
AVG no banco no lugar da media em JavaScript, com o tempo medido antes e depois | 1 ponto |
| Item 6 respondido: chave completa e o defeito do cache com chave incompleta | 1 ponto |
Erros comuns
| Erro | Como aparece | Correcao |
|---|---|---|
Rodar EXPLAIN ANALYZE em tabela de producao | o MySQL trava e todo mundo perde a conexão | "EXPLAIN ANALYZE executa a consulta. Em tabela grande, ele segura a tabela inteira. Roda em copia, sempre." |
Acreditar que LIMIT 20 faz o banco ler 20 linhas | o aluno escreve no relatorio que a consulta ficou barata | "WHERE vem antes de LIMIT. Para entregar a página 1, o banco leu até a linha 21 para saber o que pular. A paginacao economiza o que sai da consulta, não o que entra." |
OFFSET grande sem filtro antes | a página 500 de tabela grande derruba o servidor | "Quanto menos linha sobra antes do LIMIT, menor o salto. Filtre por dispositivo e por data antes de paginar." |
| Confundir limite de conexão com limite de requisicao | o aluno escreve "o banco aguenta 10 requisicoes por segundo" | "São dez conexões ao mesmo tempo. Quantas requisicoes por segundo isso vira depende de quanto cada consulta demora. E uma divisão, e e por isso que ma consulta mais lenta corta a capacidade pela metade." |
| Pool estourado tratado como erro de aplicacao | a equipe sobe o número de instancias do servidor | "Mais instancia e mais pool, e a pressão no banco continua. O que resolve pool estourado e consulta mais rapida ou menos consulta, não mais servidor na frente." |
| Cache com chave so pelo recurso | o cliente pede a página 1 e recebe a página 0 | "A chave e a consulta inteira: recurso, filtros, página e ordenacao. Cache com chave parcial devolve a resposta errada em silencio, que e pior que devolve erro." |
| Cache sem validade | todo mundo le dado velho e acha que o sensor quebrou | "Cache sem TTL e uma segunda fonte da verdade. Se o dado muda, o cache precisa de um tempo para parar de mentir." |
| Agregar no servidor achando que e mais barato | o processo cresce e e encerrado pelo sistema, sem log | "Agregar no banco e a mesma conta com menos memória e menos rede. Quando o processo morre sem log, quase sempre e memória." |
| Criar índice em toda coluna | a insercao fica lenta e o disco cresce | "Índice e troca: ganha na leitura e paga na escrita. A tabela de leituras e so de leitura; a de usuarios recebe INSERT a cada segundo." |
| Devolver tudo "porque o dado e pequeno" | um dispositivo com 600 leituras derruba o servidor com 50 usuarios olhando | "O dado e pequeno por dispositivo e grande por multiplos. O custo multiplica, e o que multiplica precisa de limite." |
Desafio extra
Escreva um script que rode EXPLAIN na consulta de historico, leia o número de linhas do plano, e sobe um alerta antes de criar índice — sugerindo o índice que resolveria. Meça o tempo da consulta antes e depois de criar, e registre os dois no log com o mesmo identificador de requisicao. Depois rode o script contra uma tabela com 200 mil linhas e uma com 20 milhoes, e escreva uma frase sobre por que a mesma consulta muda de significado com o volume.
A resolucao, compilada
// Aula 2 do dia 11: do outro lado, o servidor com muitos dispositivos. // // A aula 1 mediu a placa. Esta aula muda o ponto de vista: agora o problema // nao e "quanto tempo a placa leva", e "o que o servidor aguenta quando os // dispositivos chegam todos juntos". // // A placa virou o servidor. Ela tem, dentro da propria RAM, a tabela de // leituras que um MySQL teria: 60 dispositivos vezes 10 leituras. Cada // consulta e executada de verdade, a tabela e percorrida de verdade, e o // tempo e medido de verdade com esp_timer_get_time. // // A MEDICAO DE TEMPO aqui e do ambiente da aula: a tabela cabe na RAM, o // varredura leva microssegundos. E exatamente por isso que o sketch mede // duas coisas, e nao uma: o TEMPO e o numero de LINHAS EXAMINADAS. O tempo // desta bancada e pequeno; o numero de linhas examinadas e o mesmo num // servidor de verdade, e e dele que sai o trabalho de verdade. // // A licao do dia: consulta lenta quase nunca e "o banco esta lento". E // consulta que examina linha que nao precisava, porque falta um indice, ou // porque o pedido pediu tudo quando so precisava de uma pagina. #include <Arduino.h> #include <esp_timer.h> // Versao do firmware. O professor procura este numero no serial para saber de // qual gravacao a placa esta falando. #define VERSAO_FIRMWARE "1.5.0" // Identificador do servidor de exemplo. E o mesmo valor que o painel usaria // para agrupar origem de carga. #define ID_SERVIDOR "servidor-lab-01" // ------------------------------------------------------- tamanho da carga // // Estes quatro numeros sao a carga. O professor muda TOTAL_DE_DISPOSITIVOS // para 600 e a aula inteira muda de figura: e assim que o aluno ve que a // consulta que levava 1 ms com 60 dispositivos leva 10 ms com 600. const int TOTAL_DE_DISPOSITIVOS = 60; const int LEITURAS_POR_DISPOSITIVO = 10; const int TOTAL_DE_LINHAS = TOTAL_DE_DISPOSITIVOS * LEITURAS_POR_DISPOSITIVO; // O pool de conexoes do banco. Este e o segundo teto do dia, e o professor // escreve o numero na lousa: 10 conexoes nao sao 10 consultas por segundo, e // 10 consultas AO MESMO TEMPO. A fila de espera e o que acontece com a 11a. const int LIMITE_DE_CONEXOES_DO_BANCO = 10; // Tamanho de uma pagina. E o que a tela mostra e o que o painel pede: 20 // linhas, nao 600. const int TAMANHO_DA_PAGINA = 20; // Cache do servidor. O tamanho e o TTL sao os dois numeros que decidem se o // cache ajuda ou atrapalha: cache pequeno demais nunca acerta, cache grande // demais devolve dado velho. const int TAMANHO_DO_CACHE = 8; const int TTL_DO_CACHE_SEGUNDOS = 30; // Limite de temperatura que dispara alerta. E o mesmo numero que o painel // mostraria em vermelho. const float LIMITE_DE_TEMPERATURA = 30.0; struct Resultado { int64_t micros; int linhasExaminadas; int linhasDevolvidas; bool veioDoCache; bool comIndice; }; struct Agregado { int somaCx10; int minimoCx10; int maximoCx10; int contagem; }; struct EntradaCache { bool valido; int dispositivo; int pagina; int64_t criadoEmMicros; Resultado resultado; }; // ------------------------------------------------------------- tabela // Uma linha da tabela de leituras. Tres campos porque tres e o minimo para o // problema aparecer: quem mandou, quanto mandou e quando mandou. struct Leitura { uint16_t dispositivo; int16_t temperaturaCx10; uint32_t instante; }; Leitura tabela[TOTAL_DE_LINHAS]; // O indice que falta. Uma tabela com indice guarda, para cada dispositivo, // a primeira linha do bloco dele. Com indice, achar as 10 leituras de um // dispositivo custa 10 passos; sem indice, custa varrer a tabela toda ate o // fim. E a diferenca entre um painel que abre e um painel que demora. int primeiraLinhaDoDispositivo[TOTAL_DE_DISPOSITIVOS]; bool indiceConstruido = false; // Gerador deterministico. Nao usamos random() porque a aula precisa dar o // mesmo numero toda vez: se o resultado mudar a cada gravacao, o professor // perde a comparacao entre a tabela com indice e a tabela sem. uint32_t semente = 20260930UL; uint32_t proximoNumero() { semente = semente * 1664525UL + 1013904223UL; return semente >> 16; } // Enche a tabela. As leituras de um dispositivo ficam juntas e em ordem de // tempo, que e como um banco real entrega dado de sensor: agrupado e // cronologico. E por isso que o indice funciona. void construirTabela() { uint32_t instante = 0; for (int d = 0; d < TOTAL_DE_DISPOSITIVOS; d++) { primeiraLinhaDoDispositivo[d] = d * LEITURAS_POR_DISPOSITIVO; for (int i = 0; i < LEITURAS_POR_DISPOSITIVO; i++) { // Temperatura entre 18.0 e 34.0 graus, em decimos de grau. int16_t decimos = 180 + static_cast<int16_t>(proximoNumero() % 160); size_t posicao = static_cast<size_t>(d) * LEITURAS_POR_DISPOSITIVO + i; tabela[posicao].dispositivo = static_cast<uint16_t>(d); tabela[posicao].temperaturaCx10 = decimos; tabela[posicao].instante = instante; instante += 10; } } indiceConstruido = true; } // ------------------------------------------------------- pool de conexoes // // O pool e o conjunto de conexoes abertas com o banco. Cada consulta ocupa // uma conexao pelo tempo que dura a consulta e devolve no fim. Quando todas // estao ocupadas, a proxima consulta ESPERA. A espera e o que o professor // mede: em uma tela de carga alta, o tempo de espera cresce antes do tempo // de consulta crescer. int conexoesOcupadas = 0; int esperasNoPool = 0; int consultaMaisLongaMicros = 0; bool pegarConexao() { if (conexoesOcupadas < LIMITE_DE_CONEXOES_DO_BANCO) { conexoesOcupadas++; return true; } // Todas ocupadas: a consulta vai para a fila de espera. E o comportamento // correto, e nao um defeito: a fila e o que segura a carga ate um // estourar. esperasNoPool++; return false; } void devolverConexao() { if (conexoesOcupadas > 0) { conexoesOcupadas--; } } // ------------------------------------------------------------- medidor // O resultado de uma consulta. Quatro numeros porque sao quatro perguntas // diferentes: quanto custou, quantas linhas o banco encostou, quantas linhas // o cliente levou, e se veio do cache. Resultado resultadoVazio() { Resultado r; r.micros = 0; r.linhasExaminadas = 0; r.linhasDevolvidas = 0; r.veioDoCache = false; r.comIndice = false; return r; } // Sem indice: varredura completa da tabela, parando quando acha as 10 // leituras do dispositivo. Se o dispositivo estiver no fim da tabela, o custo // e o tamanho inteiro da tabela. Resultado varreduraCompleta(int dispositivo) { Resultado r = resultadoVazio(); int achadas = 0; for (int i = 0; i < TOTAL_DE_LINHAS; i++) { r.linhasExaminadas++; if (tabela[i].dispositivo == dispositivo) { achadas++; if (achadas == LEITURAS_POR_DISPOSITIVO) { break; } } } r.linhasDevolvidas = achadas; return r; } // Com indice: vai direto ao bloco do dispositivo. O numero de linhas // examinadas cai para o tamanho do bloco, e o tempo cai junto. Resultado buscaComIndice(int dispositivo) { Resultado r = resultadoVazio(); r.comIndice = indiceConstruido; if (!indiceConstruido) { return varreduraCompleta(dispositivo); } int inicio = primeiraLinhaDoDispositivo[dispositivo]; for (int i = 0; i < LEITURAS_POR_DISPOSITIVO; i++) { r.linhasExaminadas++; if (tabela[inicio + i].dispositivo == dispositivo) { r.linhasDevolvidas++; } } return r; } // Pagina: o cliente pediu 20 de 600. O banco ainda tem que ler a tabela // inteira para montar a pagina, porque o WHERE vem antes do LIMIT. A leitura // de "LIMIT 20" e o que o aluno imagina; a verdade e que so o OFFSET muda o // quanto se le. Resultado paginaDeHistorico(int dispositivo, int pagina) { Resultado r = buscaComIndice(dispositivo); int inicio = pagina * TAMANHO_DA_PAGINA; if (inicio >= r.linhasDevolvidas) { r.linhasDevolvidas = 0; } else { r.linhasDevolvidas = r.linhasDevolvidas - inicio; if (r.linhasDevolvidas > TAMANHO_DA_PAGINA) { r.linhasDevolvidas = TAMANHO_DA_PAGINA; } } return r; } // "Retornar tudo": o cliente pediu o historico inteiro sem pedir pagina. Com // indice, o banco acha as linhas; sem indice, ele varre a tabela toda. E o // pedido que derruba servidor: o clienteiguais leva 10 KB, 200 clientes // simultaneos levam 2 MB de memoria so em resposta. Resultado historicoCompleto(int dispositivo) { return buscaComIndice(dispositivo); } // Agregar no banco: em vez de devolver 10 linhas para o cliente somar, o // banco devolve uma linha so. As linhas lidas continuam as mesmas, e o que // muda e o que atravessa a rede e o que o servidor processa depois. Agregado agregarNoBanco(int dispositivo) { Agregado a; a.somaCx10 = 0; a.minimoCx10 = 32767; a.maximoCx10 = -32768; a.contagem = 0; int inicio = indiceConstruido ? primeiraLinhaDoDispositivo[dispositivo] : 0; int fim = indiceConstruido ? inicio + LEITURAS_POR_DISPOSITIVO : TOTAL_DE_LINHAS; for (int i = inicio; i < fim && i < TOTAL_DE_LINHAS; i++) { if (!indiceConstruido && tabela[i].dispositivo != dispositivo) { continue; } a.somaCx10 += tabela[i].temperaturaCx10; if (tabela[i].temperaturaCx10 < a.minimoCx10) { a.minimoCx10 = tabela[i].temperaturaCx10; } if (tabela[i].temperaturaCx10 > a.maximoCx10) { a.maximoCx10 = tabela[i].temperaturaCx10; } a.contagem++; } return a; } // ---------------------------------------------------------------- cache // // Cache e guardar o resultado de uma consulta que acabou de rodar, para a // proxima pergunta igual nao precisar repetir o trabalho. Duas condicoes // para acertar: o resultado tem que ser guardado com a CHAVE completa (nao // so com o dispositivo, porque pagina 0 e pagina 1 sao dados diferentes) e // tem que ter validade (TTL), porque dado velho e pior que dado nenhum. EntradaCache cache[TAMANHO_DO_CACHE]; int proximaPosicaoDoCache = 0; int acertosDoCache = 0; int errosDoCache = 0; void guardarNoCache(int dispositivo, int pagina, const Resultado& r) { cache[proximaPosicaoDoCache].valido = true; cache[proximaPosicaoDoCache].dispositivo = dispositivo; cache[proximaPosicaoDoCache].pagina = pagina; cache[proximaPosicaoDoCache].criadoEmMicros = esp_timer_get_time(); cache[proximaPosicaoDoCache].resultado = r; proximaPosicaoDoCache = (proximaPosicaoDoCache + 1) % TAMANHO_DO_CACHE; } bool lerDoCache(int dispositivo, int pagina, int64_t agora, Resultado& saida) { int64_t ttlMicros = static_cast<int64_t>(TTL_DO_CACHE_SEGUNDOS) * 1000000LL; for (int i = 0; i < TAMANHO_DO_CACHE; i++) { EntradaCache& e = cache[i]; if (!e.valido || e.dispositivo != dispositivo || e.pagina != pagina) { continue; } if (agora - e.criadoEmMicros > ttlMicros) { // Expirou. Guardar e cache nao guardar sao coisas diferentes: o // espaco continua ocupado, o dado que estava la nao vale mais. e.valido = false; return false; } saida = e.resultado; return true; } return false; } // ------------------------------------------------------------ executor // A porta de entrada do servidor. E aqui que a carga bate. Tudo o que a aula // mede passa por esta funcao, e e por isso que uma unica alteracao nela muda // todos os numeros do quadro. Resultado executarConsulta(int dispositivo, int pagina, bool comIndice, bool usarCache) { int64_t inicio = esp_timer_get_time(); Resultado r; if (usarCache) { if (lerDoCache(dispositivo, pagina, inicio, r)) { acertosDoCache++; r.veioDoCache = true; r.micros = esp_timer_get_time() - inicio; return r; } errosDoCache++; } bool entrouNaHora = pegarConexao(); if (comIndice) { if (pagina < 0) { r = historicoCompleto(dispositivo); } else { r = paginaDeHistorico(dispositivo, pagina); } } else { r = varreduraCompleta(dispositivo); } if (entrouNaHora) { devolverConexao(); } r.micros = esp_timer_get_time() - inicio; if (r.micros > consultaMaisLongaMicros) { consultaMaisLongaMicros = static_cast<int>(r.micros); } if (usarCache) { guardarNoCache(dispositivo, pagina, r); } return r; } String milissegundosComDuasCasas(int64_t micros) { return String(static_cast<float>(micros) / 1000.0f, 2); } void imprimirCabecalhoDoQuadro() { Serial.println(); Serial.println("+----------------------------------+----------+------------+--------+"); Serial.println("| consulta | tempo | examinadas | entregue|"); Serial.println("+----------------------------------+----------+------------+--------+"); } void imprimirLinhaDoQuadro(const char* nome, const Resultado& r) { String nomeComEspaco(nome); while (nomeComEspaco.length() < 32) { nomeComEspaco += " "; } String tempo = milissegundosComDuasCasas(r.micros) + " ms"; while (tempo.length() < 8) { tempo += " "; } String examinadas = String(r.linhasExaminadas); while (examinadas.length() < 10) { examinadas += " "; } String entregues = String(r.linhasDevolvidas); while (entregues.length() < 8) { entregues += " "; } Serial.println("| " + nomeComEspaco + " |" + tempo + " |" + examinadas + " |" + entregues + "|"); Serial.println("+----------------------------------+----------+------------+--------+"); } // ---------------------------------------------------------------- setup void imprimirCarga() { Serial.println("========================================"); Serial.println(" carga declarada"); Serial.println("========================================"); Serial.println("dispositivos: " + String(TOTAL_DE_DISPOSITIVOS)); Serial.println("leituras por dispositivo: " + String(LEITURAS_POR_DISPOSITIVO)); Serial.println("linhas na tabela: " + String(TOTAL_DE_LINHAS)); Serial.println("linhas por dispositivo: " + String(LEITURAS_POR_DISPOSITIVO)); Serial.println("pool do banco: " + String(LIMITE_DE_CONEXOES_DO_BANCO) + " conexoes ao mesmo tempo"); Serial.println("tamanho de pagina: " + String(TAMANHO_DA_PAGINA) + " linhas"); Serial.println("cache: " + String(TAMANHO_DO_CACHE) + " posicoes, TTL " + String(TTL_DO_CACHE_SEGUNDOS) + " s"); Serial.println(); } void imprimirIndice() { Serial.println("========================================"); Serial.println(" o que o indice muda"); Serial.println("========================================"); Serial.println("o dispositivo mais caro de achar e o ultimo da tabela."); Serial.println("ele exige varrer a tabela inteira antes de achar as " + String(LEITURAS_POR_DISPOSITIVO) + " leituras dele."); Serial.println(); imprimirCabecalhoDoQuadro(); int ultimo = TOTAL_DE_DISPOSITIVOS - 1; Resultado semIndice = executarConsulta(ultimo, 0, false, false); imprimirLinhaDoQuadro("sem indice, ultimo dispositivo", semIndice); Resultado comIndice = executarConsulta(ultimo, 0, true, false); imprimirLinhaDoQuadro("com indice, ultimo dispositivo", comIndice); Serial.println(); float reducao = 100.0f; if (semIndice.linhasExaminadas > 0) { reducao = 100.0f * (1.0f - (static_cast<float>(comIndice.linhasExaminadas) / static_cast<float>(semIndice.linhasExaminadas))); } Serial.println("o indice cortou " + String(reducao, 1) + "% das linhas examinadas."); Serial.println("o tempo caiu junto, porque o trabalho que sobrou foi o mesmo."); Serial.println("EXPLAIN e o comando que responde isso no MySQL de verdade:"); Serial.println("ele mostra o tipo de acesso e quantas linhas ele pegou."); Serial.println("EXPLAIN ANALYZE mostra o que aconteceu de verdade, e ele"); Serial.println("EXECUTA a consulta: rode em copia, nunca em producao."); Serial.println(); } void imprimirPaginacao() { Serial.println("========================================"); Serial.println(" paginacao: por que LIMIT 20 nao e barato"); Serial.println("========================================"); Serial.println("o painel pede uma pagina por vez. O banco le ate a pagina"); Serial.println("e devolve so as " + String(TAMANHO_DA_PAGINA) + " linhas pedidas."); Serial.println(); imprimirCabecalhoDoQuadro(); Resultado pagina0 = executarConsulta(7, 0, true, false); imprimirLinhaDoQuadro("pagina 0 (LIMIT 20)", pagina0); Resultado pagina1 = executarConsulta(7, 1, true, false); imprimirLinhaDoQuadro("pagina 1 (OFFSET 20)", pagina1); Resultado tudo = executarConsulta(7, -1, true, false); imprimirLinhaDoQuadro("sem pagina, historico todo", tudo); Serial.println(); Serial.println("OFFSET 20 nao custou mais caro que OFFSET 0 nas linhas lidas:"); Serial.println("o banco tem que chegar la para saber o que pular."); Serial.println("a pagina 6 de uma tabela de 10 linhas nao devolve nada,"); Serial.println("e ainda assim custou o mesmo trabalho de leitura."); Serial.println(); Serial.println("regra: a paginação nao e otimizacao, e o unico jeito de"); Serial.println("o cliente pedir pouco. Retornar tudo e a forma mais rapida"); Serial.println("de derrubar um servidor."); Serial.println(); } void imprimirAgregacao() { Serial.println("========================================"); Serial.println(" agregar no banco, nao no servidor"); Serial.println("========================================"); int alvo = 3; int64_t inicio = esp_timer_get_time(); Agregado a = agregarNoBanco(alvo); int64_t duracao = esp_timer_get_time() - inicio; Serial.println("dispositivo " + String(alvo) + ":"); Serial.println(" media: " + String(a.somaCx10 / 10.0 / (a.contagem ? a.contagem : 1), 1) + " C"); Serial.println(" minimo: " + String(a.minimoCx10 / 10.0, 1) + " C"); Serial.println(" maximo: " + String(a.maximoCx10 / 10.0, 1) + " C"); Serial.println(" Leituras: " + String(a.contagem)); Serial.println(" tempo: " + String(duracao / 1000) + " ms"); Serial.println(" devolvido: 1 linha"); Serial.println(); Serial.println("o banco leu as mesmas linhas e devolveu uma so."); Serial.println("fazer essa conta no Node, com o dado vindo inteiro pela rede,"); Serial.println("e a mesma conta gastando memoria, banda e tempo de CPU por"); Serial.println("cliente. Agregar no banco e a forma de o servidor nao crescer"); Serial.println("junto com o numero de dispositivos."); Serial.println(); } void imprimirCache() { Serial.println("========================================"); Serial.println(" cache: guardar resultado, com chave e com validade"); Serial.println("========================================"); int64_t inicio = esp_timer_get_time(); Resultado primeiraVez = executarConsulta(12, 0, true, true); int64_t primeiraDuracao = esp_timer_get_time() - inicio; inicio = esp_timer_get_time(); Resultado segundaVez = executarConsulta(12, 0, true, true); int64_t segundaDuracao = esp_timer_get_time() - inicio; Serial.println("mesma pergunta, duas vezes:"); Serial.println(" 1a vez: " + String(primeiraDuracao / 1000) + " ms, " + String(primeiraVez.linhasExaminadas) + " linhas examinadas, veio do banco"); Serial.println(" 2a vez: " + String(segundaDuracao / 1000) + " ms, " + String(segundaVez.linhasExaminadas) + " linhas examinadas, veio do cache: " + String(segundaVez.veioDoCache ? "sim" : "nao")); Serial.println(" acertos: " + String(acertosDoCache) + " | falhas: " + String(errosDoCache)); Serial.println(); Serial.println("a segunda vez custou " + String(primeiraDuracao - segundaDuracao) + " microssegundos a menos."); Serial.println("guardar resultado e a unica otimizacao que nao muda a resposta:"); Serial.println("o cliente recebe exatamente o mesmo dado."); Serial.println(); // A chave errada, mostrada de proposito: mesmo dispositivo, pagina diferente. Resultado outraPagina = executarConsulta(12, 1, true, true); Serial.println("agora a chave SEM a pagina, que e o erro classico:"); Serial.println(" dispositivo 12, pagina 1, veio do cache: " + String(outraPagina.veioDoCache ? "sim" : "nao")); Serial.println(" linhas examinadas: " + String(outraPagina.linhasExaminadas)); Serial.println(" devolvidas: " + String(outraPagina.linhasDevolvidas)); Serial.println("cache com chave incompleta devolve a resposta errada, e nao erro."); Serial.println("o cliente recebe a pagina 0 pedido a pagina 1. E um defeito"); Serial.println("que nenhum log de erro mostra, porque nada deu errado: tudo"); Serial.println("respondeu 200 e com dado."); Serial.println(); } void imprimirPool() { Serial.println("========================================"); Serial.println(" pool de conexoes: o segundo teto do dia"); Serial.println("========================================"); Serial.println("o banco nao atende " + String(TOTAL_DE_DISPOSITIVOS) + " dispositivos de uma vez."); Serial.println("ele atende " + String(LIMITE_DE_CONEXOES_DO_BANCO) + " por vez."); Serial.println("as outras " + String(TOTAL_DE_DISPOSITIVOS - LIMITE_DE_CONEXOES_DO_BANCO) + " esperam em fila."); Serial.println(); Serial.println("conexoes simultaneas: " + String(LIMITE_DE_CONEXOES_DO_BANCO)); Serial.println("esperas acumuladas: " + String(esperasNoPool)); Serial.println("consulta mais longa: " + milissegundosComDuasCasas(consultaMaisLongaMicros) + " ms"); Serial.println(); Serial.println("o sintoma de pool estourado nao e erro: e lentidao."); Serial.println("o painel demora, ninguem recebe 500, e o log fica vazio."); Serial.println("por isso o tempo de resposta precisa estar no log, e nao so"); Serial.println("o codigo de status."); Serial.println(); } void imprimirProjecao() { Serial.println("========================================"); Serial.println(" o que o servidor aguenta"); Serial.println("========================================"); int64_t inicio = esp_timer_get_time(); const int AMOSTRAS = 200; for (int i = 0; i < AMOSTRAS; i++) { executarConsulta(i % TOTAL_DE_DISPOSITIVOS, 0, true, false); } int64_t total = esp_timer_get_time() - inicio; float porConsulta = static_cast<float>(total) / AMOSTRAS; Serial.println(String(AMOSTRAS) + " consultas com indice, media de " + String(porConsulta / 1000.0f, 3) + " ms cada"); Serial.println(" tempo total: " + String(total / 1000) + " ms"); Serial.println(); Serial.println("com o pool em " + String(LIMITE_DE_CONEXOES_DO_BANCO) + " conexoes, o teto e:"); Serial.println(" " + String(LIMITE_DE_CONEXOES_DO_BANCO) + " conexoes divididas por " + String(porConsulta / 1000.0f, 3) + " ms"); Serial.println(" da algumas milhares de consultas por segundo na maquina de aula."); Serial.println("em MySQL de verdade o numero e menor, porque o trabalho"); Serial.println("inclui disco, cache do buffer e o custo de rede do socket."); Serial.println(); Serial.println("a moral nao e o numero: e que o numero e uma DIVISAO."); Serial.println("cada consulta mais lenta divide a capacidade pela metade."); Serial.println("por isso o primeiro trabalho de escala nao e comprar maquina:"); Serial.println("e tirar o que nao precisava estar na consulta."); Serial.println(); } void setup() { Serial.begin(115200); delay(200); Serial.println(); Serial.println("========================================"); Serial.println(" servidor com muitos dispositivos — v" + String(VERSAO_FIRMWARE)); Serial.println("========================================"); Serial.println("servidor: " + String(ID_SERVIDOR)); Serial.println("os tempos abaixo sao medidos de verdade, nesta placa."); Serial.println("o que vale para o MySQL de verdade sao as LINHAS EXAMINADAS."); Serial.println(); // A tabela e construida antes de qualquer consulta: sem dado nao ha o que // medir, e um servidor que responde vazio parece saudavel. construirTabela(); Serial.println("tabela construida: " + String(TOTAL_DE_LINHAS) + " linhas, indice com " + String(TOTAL_DE_DISPOSITIVOS) + " entradas"); Serial.println(); imprimirCarga(); imprimirIndice(); imprimirPaginacao(); imprimirAgregacao(); imprimirCache(); imprimirPool(); imprimirProjecao(); Serial.println("========================================"); Serial.println(" fim da aula 2 do dia 11"); Serial.println("========================================"); Serial.println("troque TOTAL_DE_DISPOSITIVOS de 60 para 600 e rode de novo:"); Serial.println("e o que o professor quer ver: o mesmo codigo, mais carga."); Serial.println("heap livre: " + String(ESP.getFreeHeap()) + " bytes"); } void loop() { // A aula mediu tudo no boot. O loop existe para manter a placa viva e para // deixar a linha de estado que o professor le no fim de cada demonstracao. Serial.println("[servidor] v" + String(VERSAO_FIRMWARE) + " viva | heap: " + String(ESP.getFreeHeap()) + " bytes | pool: " + String(conexoesOcupadas) + "/" + String(LIMITE_DE_CONEXOES_DO_BANCO) + " | esperas: " + String(esperasNoPool) + " | cache " + String(acertosDoCache) + " acertos / " + String(errosDoCache) + " falhas"); delay(10000); }
Sem saída de compilação gravada. Rode python3 validar.py -t 3 dia11 aula2.
