#MigraçãoDeServiços Recentemente migrei todos os meus serviços para o Cloudflare e estou criando este tópico para registrar as dificuldades que encontrei e as lições que aprendi (pensamentos aleatórios). As imagens mostram uma comparação aproximada do antes e depois da migração.
Primeiro, há a escolha da linguagem. O serviço legado foi desenvolvido em Java. Como um aplicativo independente, essa não foi uma boa escolha. Em primeiro lugar, aplicativos Java consomem mais recursos e têm requisitos de servidor mais elevados, resultando em custos de servidor mais altos. Atualmente, estou usando uma instância ECS da Alibaba Cloud com 2 núcleos e 4 GB de RAM, que custa aproximadamente 280 yuans por mês.
Em segundo lugar, em termos de eficiência de desenvolvimento, o Java fica completamente atrás de linguagens como PHP, Node.js e Python, e a velocidade de implantação é crucial para produtos independentes. Em terceiro lugar, seu suporte para ambientes sem servidor é inferior ao do JavaScript; por exemplo, não suporta workers do CloudFormation. Em quarto lugar, o Java não pode usar cursores, o que impossibilita a migração de volta para o IntelliJ IDEA.
No quinto ecossistema, existem muitos pacotes interessantes disponíveis para usar JavaScript, enquanto o Java é usado principalmente para aplicações empresariais, e produtos independentes geralmente não têm chance de se destacar nessa área. Com tantas desvantagens, por que escolher Java desde o início? Na verdade, inicialmente planejei usar PHP ou Node.js, e até passei bastante tempo aprendendo-os, mas ou desisti no meio do caminho ou meu entusiasmo simplesmente desapareceu.
Passei cerca de dois anos aprendendo repetidamente essas tecnologias otimizadas, aprendendo e esquecendo, para depois aprender novamente. Um dia, de repente, percebi que dois anos haviam se passado e toda a minha energia tinha sido desperdiçada em coisas superficiais; eu não havia criado nenhum produto e até mesmo esquecido o que inicialmente queria criar. Então, tomei uma decisão: usar o que eu sabia e parar de me preocupar com isso. Usei minhas antigas habilidades em Java para o backend e jQuery, HTML e CSS para o frontend. O mais importante era fazer funcionar.
Agora que os negócios basicamente se estabilizaram (a receita está instável 🤡), vou dedicar um tempo para migrar o serviço para Node.js e me integrar ao ecossistema JavaScript. Em primeiro lugar, definitivamente usarei TypeScript; é mais fácil identificar problemas. Como já usei Java antes, TypeScript não me incomoda. Em seguida, vem o framework de backend. Quando trabalhava com Java, sempre usei Spring Boot. Depois de migrar para TypeScript, minha ideia inicial era usar NestJS, que é muito semelhante ao Java. No entanto, acabei descartando essa ideia.
Os principais motivos são: Primeiro, o NestJS é muito poderoso e rico em recursos, assim como o Spring Boot, o que o torna pesado e com uma curva de aprendizado íngreme. Segundo, levei muito tempo para entender que os workers do CloudFormation e o Node.js usam ambientes de execução diferentes, e muitos pacotes do Node.js são incompatíveis. Como o objetivo é migrar para o CloudFormation, esse ponto foi descartado. Então descobri um novo framework chamado honojs e, após pesquisá-lo, acabei optando por ele. Havia dois motivos:
Primeiro, é muito leve e simples, com carga mental mínima. É suficiente para o meu negócio. Segundo, pode ser facilmente executado em diferentes ambientes de execução, como Node.js nativo, Bundle, CF Worker, AWS, Vercel, etc., minimizando assim a dependência da plataforma. Depois, há o framework ORM; eu usava o MyBatis Plus em Java. Após a migração, gostaria de encontrar algo semelhante.
Não parecem existir muitas opções de frameworks ORM para TypeScript. Pesquisei principalmente o Prisma e o Dizzle. Pessoalmente, achei o Prisma mais robusto, enquanto o Dizzle era mais simples, então escolhi o Dizzle por ter uma curva de aprendizado mais suave. Isso porque, no momento, não preciso de nenhum recurso particularmente avançado. Em relação à escolha de ferramentas de uso comum, eu usava o Hutool para Java, que era abrangente e completo. Tenho procurado algo semelhante no ecossistema JavaScript, mas infelizmente ainda não encontrei.
Diferentes pacotes só podem ser importados para diferentes funções. O conjunto de ferramentas básico usa o Radash, que é mais leve e atualizado que o Lodash. O DayJS é usado para processamento de tempo e o ky é usado como biblioteca de requisições, também por ser muito leve.
A ênfase em um design leve se justifica por dois motivos. Primeiro, os workers do CloudFormation têm limitações quanto ao tamanho dos pacotes publicados; pacotes excessivamente grandes podem falhar na implantação. Segundo, pacotes mais pesados têm dependências mais profundas do Node.js, o que pode torná-los inutilizáveis durante a execução do worker do CloudFormation. Importar pacotes para um worker é como abrir uma caixa às cegas; você não tem ideia se vai funcionar antes de iniciar. Este é um ponto crucial a ser lembrado ao usar workers.
Um dos principais motivos para migrar para o Cloudflare foi o seu generoso plano gratuito. 100.000 requisições por dia, 100.000 gravações no banco de dados e 5 milhões de leituras por dia — mais do que suficiente para o meu negócio; eu ficaria muito feliz em ultrapassar esses limites. No entanto, acabei optando pelo plano pago de US$ 5/mês. Um dos principais motivos foi que a versão gratuita permitia apenas 10 ms de tempo de CPU por requisição, o que poderia causar a falha de uma API complexa antes mesmo de sua execução ser concluída.
Além disso, mesmo os planos pagos têm limitações: uma única requisição à API pode levar no máximo 15 segundos, e tarefas agendadas ou filas de mensagens têm um limite de 15 minutos. O tempo de execução da migração para APIs de worker deve ser cuidadosamente avaliado. Nem considere nada que seja muito demorado. Eu mesmo me deparei com essa armadilha.
Em JavaScript, métodos assíncronos são modificados com a palavra-chave `async`. Adicionar `await` antes da execução do método fará com que ele espere sua conclusão antes de executar o código subsequente. Sem ele, o código será executado de forma assíncrona, sem afetar a execução do código posterior. Para chamadas de código como logs ou notificações push que não afetam a lógica subsequente, geralmente não uso `await`; não quero que bloqueiem a lógica posterior, mas sim que sejam executadas de forma assíncrona.
No entanto, após a implantação no worker, descobri que essas chamadas sem o modificador `await` provavelmente não eram executadas. O motivo é que elas não bloqueavam o processo principal; o código subsequente continuava a ser executado até que a solicitação e a resposta fossem concluídas e, em seguida, o processo era encerrado, descartando todas as tarefas pendentes, estivessem elas ainda em execução ou não. O pior é que essas operações assíncronas funcionam perfeitamente no ambiente de desenvolvimento local, mas são descartadas no ambiente de produção.
Como resolver isso? A maneira mais simples é adicionar uma instrução `await` a todas as chamadas de métodos assíncronos. No entanto, se uma interface tiver muitas instruções `await`, você perceberá que a resposta da interface fica muito lenta. Meu callback do Paddle enfrentou esse problema; o Paddle detectou que as chamadas para minha interface de callback estavam constantemente expirando e tentando novamente. E quando a conexão expira, ele a fecha ativamente e a lógica subsequente para de ser executada.
Nesse caso, você pode usar o método waitUntil do worker. Ele garante que o método asdevelopers.cloudflare.com/workers/runtim…cutado após a conclusão da requisição, desde que o limite de uso da CPU não seja excedido. (É tarde demais, continuarei escrevendo amanhã.) https://t.co/N2y3KguqWF
Realizei a operação de migração duas vezes. Após a primeira migração, encontrei inúmeros erros, que pareciam impossíveis de resolver rapidamente, então migrei de volta e, em seguida, trabalhei na solução do problema aos poucos. As mensagens de erro online são as seguintes:
Erro: Não é possível executar E/S em nome de uma solicitação diferente. Objetos de E/S (como fluxos, corpos de solicitação/resposta e outros) criados no contexto de um manipulador de solicitação não podem ser acessados pelo manipulador de uma solicitação diferente.
Essa é uma limitação do Cloudflare Workers que nos permite melhorar o desempenho geral. (Tipo de E/S: ReadableStreamSource) A tradução é a seguinte:
Erro: Não foi possível realizar operações de E/S em nome de diferentes solicitações. Objetos de E/S (como fluxos, corpos de requisição/resposta, etc.) criados no contexto de um manipulador de requisição não podem ser acessados por outros manipuladores de requisição. Essa é uma limitação do Cloudflare Workers que nos permite melhorar o desempenho geral. (Tipo de E/S: ReadableStreamSource)
Essa exceção me deixou um pouco confuso. Como eu poderia acessar o conteúdo de outra requisição dentro de uma única requisição? Está além da minha compreensão; cada requisição não deveria ser independente? Pesquisei online, mas não encontrei nenhuma informação útil. Então, passei as informações da exceção para o cursor, permitindo que ele percorresse todo o código para investigar qual trecho estava causando o erro. O cursor forneceu dois locais; o primeiro foi onde registrei o ponto de entrada.
Inicialmente, para evitar afetar o objeto de requisição, usei `c.req.raw.clone().json()` para recuperar os parâmetros da requisição, com a intenção de usar o objeto clonado. No entanto, essa operação de clonagem pode ter afetado outras requisições, então posteriormente mudei para `c.req.json()` para resolver o problema. Em segundo lugar, o objeto de contexto é usado em muitos lugares no hono, como bancos de dados, caches e respostas, e o objeto de contexto padrão é obtido no ponto de entrada da solicitação.
Em outras palavras, a requisição precisa ser passada de cima para baixo a partir do ponto de entrada. Isso resulta na necessidade de um parâmetro de contexto para cada método, o que é trabalhoso. Em Java, eu usaria `ThreadLocal` para resolver esse problema. Então, me perguntei se havia algo semelhante em JavaScript, e de fato encontrei: `globalThis`. Eu atribuo o contexto à propriedade `globalThis.honoContext` no ponto de entrada da requisição.
Mais tarde, quando precisei usar o objeto de contexto, simplesmente chamei `globalThis.honoContext` para obtê-lo. Isso alcançou o efeithono.dev/docs/middlewar… infelizmente, isso poderia ter acionado restrições de compartilhamento de workers. Felizmente, o Hono lançou recentemente um método para obter o contexto globalmente, o que resolveu o problema depois que o substituí por esse método. https://t.co/Dh0zidQ1fc
Primeiramente, este problema é muito complexo, pois não consegui reproduzi-lo, seja no meu ambiente de desenvolvimento local ou no ambiente de produção. Localmente, funcionava perfeitamente, sem erros, e todas as funções operavam corretamente. No entanto, o problema também não era sempre reproduzível no ambiente de produção. Mesmo executando um ambiente multithread e concorrente, o problema não se repetiu. Só consegui corrigir as duas partes suspeitas e implantá-las em produção para verificar se o problema reapareceria. Felizmente, ele não reapareceu, então tenho quase certeza de que este é o problema.
Outro problema que muitas vezes passa despercebido é o fuso horário. O servidor está rodando em UTC-0. No serviço anterior, eu usava GMT+8, que está 8 horas à frente da China. No entanto, como o banco de dados armazena registros de data e hora, isso tem pouco impacto para os usuários. Mas pode afetar as estatísticas do SQL Server e as tarefas agendadas. Por exemplo, eu tinha uma tarefa agendada que começava às 7h (horário da China).
A consulta busca novos usuários do dia anterior, especificamente a tabela de usuários com dados adicionados entre 00:00 e 23:59:59 do dia anterior. Após a migração, encontrei inconsistências entre os serviços antigo e novo. A investigação revelou um problema de fuso horário. Executar o script às 7h da manhã estava incorreto, pois os fusos horários tinham uma diferença de 8 horas; o processo ainda estava no fuso horário do dia anterior, consultando essencialmente o dia anterior a ontem. Então, alterei o horário da tarefa para 8h30 da manhã.
Além disso, há uma diferença de fuso horário de 8 horas. Se você quiser analisar usando o horário local, os horários de início e término também precisam ser atrasados em 8 horas. Outro ponto a observar é que os timestamps em Java são em milissegundos por padrão e têm 13 dígitos, enquanto os timestamps em JavaScript são em segundos por padrão e têm 10 dígitos. Essa conversão deve ser consistente; use todos os 13 dígitos ou todos os 10 dígitos, caso contrário, você poderá ver datas de 1970 ou de 50.000 anos no futuro.
Em seguida, vamos falar sobre o banco de dados. Anteriormente, o serviço antigo utilizava Docker para executar o MySQL. Pesquisamos diversas opções de migração: D1 da Cloudflare, PostgreSQL da Supabase e Turso. As cotas gratuitas oferecidas por eles eram bastante generosas, suficientes para o meu projeto. D1 e Turso são baseados em SQLite, enquanto a Supabase é baseada em PostgreSQL.
Por fim, escolhi o D1, principalmente por dois fatores: primeiro, a facilidade de uso. O D1 é um produto de Computação em Nuvem (CF), o que facilita a integração com os servidores. Segundo, a sobrecarga de rede. Os servidores e o D1 estão ambos na rede CF e podem até ser configurados na mesma região, reduzindo a sobrecarga de rede para acesso ao banco de dados e, indiretamente, melhorando a velocidade. O Subabase e o Turso, por outro lado, incorrem nessas sobrecargas.
Após a migração, alguns usuários relataram não conseguir acessar suas contas. Testei minha própria conta e consegui acessar normalmente. A análise dos logs não mostrou nenhum problema; o endereço de e-mail e a senha do usuário estavam corretos, mas o sistema continuava exibindo a mensagem "nome de usuário ou senha incorretos". Completamente perplexo, inseri o endereço de e-mail do usuário em uma consulta SQL no banco de dados D1 e, surpreendentemente, ele não foi encontrado. Inicialmente, suspeitei que fosse um problema com caracteres invisíveis.
Quando finalmente localizei o problema, fiquei completamente atônito. Tratava-se da migração do MySQL para o D1 (SQLite). Eu estava mentalmente preparado para as diferenças, já que o SQLite suporta muito menos tipos de dados que o MySQL, tornando o mapeamento um-para-um impossível. Além disso, no SQLite, mesmo definindo tipos, é possível armazenar dados sem especificar o tipo sem gerar erros, e existem outras diferenças de sintaxe. No entanto, eu havia ignorado a questão da diferenciação entre maiúsculas e minúsculas.
Ao criar um banco de dados MySQL, geralmente o configuro para não diferenciar maiúsculas de minúsculas. Essa é uma prática padrão; quase todos os bancos de dados que usei ao longo dos anos não diferenciavam maiúsculas de minúsculas. Tornou-se um hábito automático; nunca sequer considerei que o SQLite pudesse diferenciar maiúsculas de minúsculas. Voltando à pergunta do usuário acima, ele digitou Abc@gmail.com durante o cadastro e, em seguida, usou abc@gmail.com novamente ao fazer login.
Isso funciona bem no servidor MySQL antigo porque não diferencia maiúsculas de minúsculas e é considerado o mesmo usuário. Mas no D1, são claramente dois usuários diferentes. Então pensei em como resolver isso. Primeiro, não consegui encontrar uma configuração como a do MySQL para ignorar maiúsculas e minúsculas globalmente no D1. É necessário usar a palavra-chave `COLLATE NOCASE` ao criar a tabela para especificar que um determinado campo deve ignorar maiúsculas e minúsculas. Então pensei que deveria modificar a tabela para adicionar essa palavra-chave.
Então descobri que a instrução ALTER TABLE do SQLite só permite modificar nomes de tabelas e adicionar colunas, mas não consegue modificar diretamente a definição de colunas existentes (o que também é uma desvantagem). Modificar a tabela é impossível, então tive que alterar a lógica do código. Em três etapas: Primeiro, extraí todas as operações SQL envolvendo e-mail para um método comum e usei `lower(email)` para forçar a conversão para minúsculas durante a consulta.
Em segundo lugar, no ponto de entrada da requisição, se o parâmetro incluir "email", chame `toLowerCase()`. Em terceiro lugar, mescle e remova contas duplicadas causadas por inconsistências na capitalização. Escrever esse código foi uma verdadeira tortura; parecia uma montanha de cocô.
O problema de diferenciação entre maiúsculas e minúsculas foi temporariamente resolvido. Após observar os logs por alguns dias, notei que o erro "Error: D1_ERROR: Network connection lost." ocorre ocasionalmente, com uma probabilidade de cerca de alguns por mil. Não tenho certeza se o problema está no meu código ou no próprio D1. Pesquisei online e descobri que muitas pessoas no fórum do CF e na comunidade do Discord relataram problemas semelhantes, mas a maioria não encontrou uma solução.
A única solução minimamente útil foi tentar novamente, o que também indicou que isso normalmente aconteceria 😕. Um dos principais motivos pelos quais escolhi o D1 foi reduzir a sobrecarga da rede. Se, mesmo assim, ainda houver problemas de perda de conexão, tenho dúvidas sobre a estabilidade do D1. Esse problema ainda não foi completamente resolvido; informarei vocês sobre qualquer novidade.
Continuando com o tema de e-mail corporativo, eu usava o serviço de e-mail corporativo da Alibaba Cloud, que geralmente funcionava bem, mas o ponto de entrada da versão gratuita parecia obscuro. Não era possível vincular vários domínios e o limite de envio era pouco claro. Eu queria migrar para um serviço que suportasse múltiplos domínios. Após algumas pesquisas, descobri que usar o Gmail com o Cloudflare resultava em perda de dados, embora o endereço do Gmail permanecesse visível.
O iCloud é uma opção, com suporte para vinculação a múltiplos domínios, mas sua estabilidade é notoriamente ruim. Além disso, ao responder e-mails de usuários, eles frequentemente não conseguem recebê-los porque suas contas do iCloud estão cheias, o que torna o uso arriscado. O Lark é gratuito atualmente, mas seu futuro é incerto. Eu queria algo mais confiável. Finalmente, escolhi o Zoho por ser barato — US$ 1 por usuário por mês, e geralmente apenas um usuário é necessário.
Ele também suporta domínios ilimitados. Os recursos são bastante abrangentes. Se você não usa alguns dos recursos avançados, a versão gratuita é suficiente. Um ponto a observar é que os planos de preços do Zoho variam de acordo com o país. A versão chinesa custa 5 RMB por pessoa por mês, o que parece barato à primeira vista, mas exige um mínimo de 5 usuários. Recomendamos escolher a versão internacional.
Na minha aplicação Java, utilizo diretamente o protocolo SMTP para enviar emails. No entanto, o envio de emails via SMTP no worker falha porque o protocolo não é suportado. Preciso utilizar a API HTTP (que não está disponível na versão gratuita).
Antes, eu usava o DataGrip para visualizar bancos de dados, mas ele não é compatível com o D1 (pelo mechromewebstore.google.com/detail/drizzle…u usava). A interface web do D1 também é extremamente básica. Atualmente, uso principalmente dois plugins: um deles é o Drizzle Studio, da Drizzle, que é muito prático: https://t.co/wMKRcC1LaC
Outra opção é o TablePlus. Embora o Dizzle Studio seja prático, trata-se de um plugin para navegador com funcionalidades limitadas, sem recursos como texto preditivo para SQL e salvamento de histórico. É adequado para uso temporário. O TablePlus possui recursos mais abrangentes, e eu também tenho uma versão setapp que pode ser usada gratuitamente; comprá-lo separadamente é muito caro, então não o recomendo.
Em relação à coleta e análise de logs, anteriormente utilizávamos o serviço de logs da Alibaba Cloud, similar ao conjunto ELK, para Java. Por padrão, os workdevelopers.cloudflare.com/workers/observ…(CF) não armazenam logs; você só pode visualizar logs em tempo real. Se desejar transferir logs para outros serviços de log, você pode usar o recurso de worker de cauda. No entanto, a versão paga do worker pode usar o serviço da BaseLime gratuitamente. Este último foi adquirido pelo Cloud Computing e pode ser integrado com um único clique. https://t.co/YuNLJQo6tP
Em relação ao suporte ao cliente, diferentemente de alguns outros provedores de nuvem, o Cloud Computing (CF) permite, por padrão, apenas o envio de faturas, informações da conta e abertura de chamados, não abordando problemas técnicos. Além disso, o suporte online exige a assinatura do plano Business, que custa US$ 250 por mês. Esse plano utiliza principalmente permissões de produto relacionadas à CDN, e não permissões de servidor. É improvável que eles criem uma conta separada especificamente para suporte ao cliente. Isso é compreensível, considerando a grande base de usuários e o foco no atendimento aos usuários principais.
E se você realmente se deparar com problemas técnicos? Existem duas opções: uma é pedir ajuda no fórum de desenvolvedores do CF. No entanto, é difícil garantir uma resposta rápida. Além disso, se não for um problema muito comum, é bem provável que ninguém consiga respondê-lo. Já vi posts sobre problemas que não receberam uma única resposta mesmo depois de meses, e as respostas geralmente são coisas como: "Ei, amigo, eu também tive esse problema, você conseguiu resolver?".
Em segundo lugar, você pode participar da comunidade do Discord do CF para dar feedback, que é relativamente mais oportuno. No entanto, considerando a diferença de fuso horário, outros membros podem ainda estar dormindo quando você estiver lá. Além disso, a declaração oficial enfatiza que a equipe ali "não é composta por pessoal de suporte técnico, mas sim por desenvolvedores e especialistas técnicos comuns que respondem a perguntas voluntariamente em seu tempo livre". Portanto, ao fazer perguntas por lá, é melhor controlar suas expectativas e emoções.






