#MigraciónDeServicios Recientemente migré todos mis servicios a Cloudflare y abro este hilo para documentar los problemas que encontré y las lecciones aprendidas (reflexiones sueltas). Las imágenes muestran una comparación aproximada del antes y el después de la migración.
En primer lugar, está la elección del lenguaje. El servicio anterior se desarrolló en Java. Como aplicación independiente, esta no fue una buena elección. Para empezar, las aplicaciones Java consumen más recursos y tienen mayores requisitos de servidor, lo que se traduce en mayores costos. Actualmente, utilizo una instancia ECS de Alibaba Cloud de 2 núcleos y 4 GB, que cuesta aproximadamente 280 yuanes al mes.
En segundo lugar, en cuanto a eficiencia de desarrollo, Java queda totalmente rezagado frente a lenguajes como PHP, Node.js y Python, y la velocidad de despliegue es crucial para productos independientes. En tercer lugar, su soporte para entornos sin servidor es inferior al de JavaScript; por ejemplo, no admite CloudFormation. En cuarto lugar, Java no admite cursores, lo que imposibilita volver a IntelliJ IDEA.
En el quinto ecosistema, existen muchos paquetes interesantes disponibles para usar JavaScript, mientras que Java se utiliza principalmente para aplicaciones empresariales, y los productos independientes generalmente no tienen ninguna posibilidad de triunfar en esta área. Con tantas desventajas, ¿por qué elegir Java desde el principio? En realidad, inicialmente planeaba usar PHP o Node.js, e incluso dediqué mucho tiempo a aprenderlos, pero o me rendí a la mitad o mi entusiasmo desapareció por completo.
Pasé casi dos años aprendiendo repetidamente estas tecnologías óptimas, aprendiendo, olvidando y volviendo a aprender. Un día, de repente me di cuenta de que habían pasado dos años y toda mi energía se había desperdiciado en cosas superficiales; no había creado ningún producto e incluso había olvidado qué quería crear inicialmente. Entonces tomé una decisión: usar lo que ya sabía y dejar de preocuparme. Utilicé mis conocimientos de Java para el backend y jQuery, HTML y CSS para el frontend. Lo más importante era que funcionara.
Ahora que el negocio se ha estabilizado (aunque los ingresos son inestables 🤡), me tomaré un tiempo para migrar el servicio a Node.js y familiarizarme con el ecosistema de JavaScript. En primer lugar, usaré TypeScript; facilita la detección de problemas. Habiendo usado Java anteriormente, TypeScript me resulta cómodo. El siguiente paso es el framework del backend. Cuando trabajaba con Java, siempre usaba Spring Boot. Tras migrar a TypeScript, mi primera idea fue usar NestJS, que es muy similar a Java. Sin embargo, finalmente la descarté.
Las razones principales son: Primero, NestJS es muy potente y cuenta con numerosas funcionalidades, al igual que Spring Boot, lo que lo hace pesado y con una curva de aprendizaje pronunciada. Segundo, me llevó mucho tiempo comprender que los workers de Cloud Foundry y Node.js utilizan entornos de ejecución diferentes, y que muchos paquetes de Node.js son incompatibles. Dado que el objetivo es migrar a Cloud Foundry, este punto quedó descartado. Luego descubrí un nuevo framework llamado HonoJS y, tras investigarlo, finalmente lo elegí. Hubo dos razones:
En primer lugar, es muy ligero y sencillo, con una mínima carga cognitiva. Es suficiente para mi negocio. En segundo lugar, puede ejecutarse fácilmente en diferentes entornos de ejecución, como Node.js nativo, Bundle, CF Worker, AWS, Vercel, etc., minimizando así la dependencia de la plataforma. Luego está el framework ORM; yo usé MyBatis Plus en Java. Tras la migración, me gustaría encontrar algo similar.
No parece haber muchas opciones de frameworks ORM para TypeScript. Investigué principalmente Prisma y Dizzle. Personalmente, me pareció que Prisma era más potente, mientras que Dizzle era más sencillo, así que elegí Dizzle por su menor curva de aprendizaje. Esto se debe a que actualmente no necesito ninguna funcionalidad particularmente avanzada. En cuanto a la elección de herramientas de uso común, antes usaba Hutool para Java, que era muy completo y extenso. He estado buscando algo similar en el ecosistema de JavaScript, pero lamentablemente, aún no lo he encontrado.
Los distintos paquetes solo se pueden importar para funciones específicas. El conjunto de herramientas básico utiliza Radash, que es más ligero y está más actualizado que Lodash. DayJS se usa para el procesamiento del tiempo, y ky para la biblioteca de solicitudes, también por su ligereza.
La razón para priorizar el diseño ligero es doble. Primero, los workers de Cloud Foundry tienen limitaciones en el tamaño de los paquetes publicados; los paquetes excesivamente grandes pueden fallar al desplegarse. Segundo, los paquetes más pesados tienen dependencias más profundas de Node.js, lo que podría hacerlos inutilizables durante la ejecución del worker de Cloud Foundry. Importar paquetes a un worker es como abrir una caja ciega; no se sabe si funcionará antes de iniciarlo. Este es un punto crucial a tener en cuenta al usar workers.
Una de las principales razones para migrar a Cloudflare fue su generoso plan gratuito. 100 000 solicitudes diarias, 100 000 escrituras en la base de datos y 5 millones de lecturas diarias: más que suficiente para mi negocio; me encantaría superarlo. Sin embargo, finalmente opté por el plan de pago de 5 $/mes. Una razón clave fue que la versión gratuita solo permitía 10 ms de tiempo de CPU por solicitud, lo que podía provocar que una API compleja fallara incluso antes de terminar de ejecutarse.
Además, incluso los planes de pago tienen limitaciones: una sola solicitud a la API puede tardar un máximo de 15 segundos, y las tareas programadas o las colas de mensajes tienen un límite de 15 minutos. El tiempo de ejecución de la migración a las API de worker debe evaluarse cuidadosamente. Ni siquiera consideres nada que consuma demasiado tiempo. Yo mismo me topé con este problema.
En JavaScript, los métodos asíncronos se modifican con la palabra clave `async`. Agregar `await` antes de la ejecución del método hará que espere a que finalice antes de ejecutar el código posterior. Sin `await`, el código se ejecutará de forma asíncrona sin afectar la ejecución del código posterior. Para llamadas a código como el registro de eventos o las notificaciones push que no afectan la lógica posterior, generalmente no uso `await`; no quiero que bloquee la lógica posterior, sino que se ejecute de forma asíncrona.
Sin embargo, tras el despliegue en el entorno de trabajo, descubrí que estas llamadas sin el modificador `await` probablemente no se ejecutaban. Esto se debe a que no bloqueaban el proceso principal; el código subsiguiente continuaba su ejecución hasta que se completaban la solicitud y la respuesta, momento en el que el proceso finalizaba, descartando todas las tareas pendientes, estuvieran o no en ejecución. Lo peor es que estas operaciones asíncronas funcionan correctamente en el entorno de desarrollo local, pero se descartan en el entorno de producción.
¿Cómo solucionar esto? La forma más sencilla es añadir una instrucción `await` a todas las llamadas a métodos asíncronos. Sin embargo, si una interfaz tiene demasiadas instrucciones `await`, su respuesta se volverá muy lenta. Mi función de devolución de llamada de Paddle se topó con este problema: Paddle detectó que las llamadas a mi interfaz de devolución de llamada expiraban constantemente y seguían reintentando. Cuando expiraba el tiempo de espera, cerraba la conexión y la lógica posterior dejaba de ejecutarse.
En este caso, puede usar el método `waitUntil` del worker. Esto garantiza que el métdevelopers.cloudflare.com/workers/runtim…á ejecutando después de que finalice la solicitud, siempre que no se exceda el límite de uso de CPU. (Es demasiado tarde, continuaré escribiendo mañana.) https://t.co/N2y3KguqWF
Realicé la migración dos veces. Tras la primera migración, encontré numerosos errores que parecían imposibles de resolver rápidamente, así que volví a migrar y poco a poco fui solucionando el problema. Los mensajes de error en línea son los siguientes:
Error: No se puede realizar E/S en nombre de otra solicitud. Los objetos de E/S (como flujos, cuerpos de solicitud/respuesta, etc.) creados en el contexto de un controlador de solicitudes no se pueden acceder desde el controlador de otra solicitud.
Esta es una limitación de Cloudflare Workers que nos permite mejorar el rendimiento general. (Tipo de E/S: ReadableStreamSource) La traducción es la siguiente:
Error: No se puede realizar la E/S en nombre de diferentes solicitudes. Los objetos de E/S (como flujos, cuerpos de solicitud/respuesta, etc.) creados en el contexto de un controlador de solicitudes no son accesibles desde otros controladores de solicitudes. Esta es una limitación de Cloudflare Workers que nos permite mejorar el rendimiento general. (Tipo de E/S: ReadableStreamSource)
Esta excepción me dejó algo confundido. ¿Cómo podía acceder al contenido de otra solicitud dentro de una sola solicitud? No lo entiendo; ¿acaso no son las solicitudes independientes? Busqué en línea, pero no encontré información útil. Luego, pasé la información de la excepción al cursor, dejándolo que analizara todo el código fuente para investigar qué parte del código causaba el error. El cursor proporcionó dos ubicaciones; la primera era donde registré el punto de entrada.
Inicialmente, para evitar modificar el objeto de solicitud, utilicé `c.req.raw.clone().json()` para recuperar los parámetros de la solicitud, con la intención de usar el objeto clonado. Sin embargo, esta operación de clonación podría haber afectado a otras solicitudes, por lo que posteriormente cambié a `c.req.json()` para solucionar el problema. En segundo lugar, el objeto de contexto se utiliza en muchos lugares de hono, como bases de datos, cachés y respuestas, y el objeto de contexto predeterminado se obtiene en el punto de entrada de la solicitud.
En otras palabras, la solicitud debe pasarse desde el punto de entrada. Esto implica que cada método necesite un parámetro de contexto, lo cual resulta engorroso. En Java, usaría ThreadLocal para solucionar este problema. Así que me pregunté si existía algo similar en JavaScript, y lo encontré: globalThis. Asigno el contexto a la propiedad de globalThis.honoContext en el punto de entrada de la solicitud.
Más tarde, cuando quise usar el objeto de contexto, simplemente llamé a `globalThis.honoContext` para obtenerlo. Esto logró el efecto dhono.dev/docs/middlewar…mentablemente, esto podría haber activado las restricciones de compartición de workers. Afortunadamente, Hono lanzó recientemente un método para obtener el contexto globalmente, lo que resolvió el problema después de que lo reemplacé con dicho método. https://t.co/Dh0zidQ1fc
En primer lugar, este problema es muy complejo porque no he podido reproducirlo, ni en mi entorno de desarrollo local ni en el de producción. Funcionaba perfectamente en local, sin errores, y todas las funciones operaban correctamente. Sin embargo, tampoco era reproducible en producción. Incluso ejecutando un entorno concurrente multihilo, el problema persistía. Solo pude corregir las dos partes sospechosas y desplegarlo en producción para ver si volvía a aparecer. Afortunadamente, no volvió a aparecer, así que estoy bastante seguro de que este es el problema.
Otro problema que suele pasarse por alto es la zona horaria. El proceso se ejecuta en UTC-0. En la versión anterior del servicio, usaba GMT+8, que está 8 horas adelantada con respecto a China. Sin embargo, dado que la base de datos almacena marcas de tiempo, esto tiene poco impacto en los usuarios. Pero sí puede afectar a las estadísticas de SQL y a las tareas programadas. Por ejemplo, tenía una tarea programada que comenzaba a las 7:00 (hora de China).
La consulta buscaba usuarios nuevos del día anterior, específicamente en la tabla de usuarios para los datos añadidos entre las 00:00 y las 23:59:59 de ayer. Tras la migración, detecté inconsistencias entre los servicios antiguo y nuevo. La investigación reveló un problema de zona horaria. Ejecutar el script a las 7:00 era incorrecto porque las zonas horarias tenían una diferencia de 8 horas; el proceso seguía en la zona horaria del día anterior, consultando esencialmente el día anterior. Por lo tanto, cambié la hora de la tarea a las 8:30.
Además, hay una diferencia horaria de 8 horas. Si desea analizarlo utilizando la hora local, las horas de inicio y finalización también deben retrasarse 8 horas. Otro punto a tener en cuenta es que las marcas de tiempo de Java se expresan en milisegundos por defecto y tienen una longitud de 13 dígitos, mientras que las de JavaScript se expresan en segundos por defecto y tienen una longitud de 10 dígitos. Esta conversión debe ser consistente; utilice solo 13 dígitos o solo 10, de lo contrario podría ver fechas de 1970 o de dentro de 50 000 años.
A continuación, hablemos de la base de datos. Anteriormente, el servicio anterior utilizaba Docker para ejecutar MySQL. Investigamos varias opciones de migración: D1 de Cloudflare, PostgreSQL de Supabase y Turso. Sus cuotas gratuitas eran muy generosas, suficientes para mi proyecto. D1 y Turso se basan en SQLite, mientras que Supabase se basa en PostgreSQL.
Finalmente, me decidí por D1, principalmente por dos factores: primero, la facilidad de uso. D1 es un producto de computación en la nube (CF), lo que facilita su integración con los nodos de trabajo. Segundo, la sobrecarga de red. Tanto los nodos de trabajo como D1 se encuentran en la red de CF e incluso pueden configurarse en la misma región, lo que reduce la sobrecarga de red para el acceso a la base de datos y, por consiguiente, mejora la velocidad. Subabase y Turso, en cambio, sí generan esta sobrecarga.
Tras la migración, algunos usuarios informaron no poder acceder a sus cuentas. Probé mi propia cuenta e inicié sesión sin problemas. El análisis de los registros no mostró ningún problema; la dirección de correo electrónico y la contraseña del usuario eran correctas, pero el sistema seguía mostrando el mensaje «Nombre de usuario o contraseña incorrectos». Completamente desconcertado, introduje la dirección de correo electrónico del usuario en una consulta SQL en la base de datos D1 y, sorprendentemente, no se encontró. Inicialmente, sospeché que se trataba de un problema con caracteres invisibles.
Cuando por fin localicé el problema, me quedé completamente atónito. Se trataba de la migración de MySQL a D1 (SQLite). Estaba mentalmente preparado para las diferencias, ya que SQLite admite muchos menos tipos que MySQL, lo que imposibilita las correspondencias uno a uno. Además, en SQLite, incluso si se definen los tipos, se pueden almacenar datos sin especificarlos sin que se produzca un error, y existen otras diferencias de sintaxis. Sin embargo, pasé por alto el problema de la distinción entre mayúsculas y minúsculas.
Cuando creo una base de datos MySQL, suelo configurarla para que no distinga entre mayúsculas y minúsculas. Es una práctica habitual; casi todas las bases de datos que he usado a lo largo de los años no distinguen entre mayúsculas y minúsculas. Ya es algo automático; ni siquiera se me había ocurrido que SQLite pudiera distinguir entre mayúsculas y minúsculas. Volviendo a la pregunta del usuario, este introdujo Abc@gmail.com durante el registro y luego volvió a usar abc@gmail.com al iniciar sesión.
Esto funciona correctamente en el antiguo servidor MySQL porque no distingue entre mayúsculas y minúsculas, y se considera el mismo usuario. Pero en D1, claramente se trata de dos usuarios diferentes. Entonces pensé en cómo solucionarlo. Primero, no encontré una configuración como la de MySQL para establecer una configuración global que no distinga entre mayúsculas y minúsculas en D1. Esto requiere usar la palabra clave `COLLATE NOCASE` al crear la tabla para especificar que un campo determinado ignore las mayúsculas y minúsculas. Así que pensé que debería modificar la tabla para agregar esta palabra clave.
Luego descubrí que la instrucción ALTER TABLE de SQLite solo permite modificar nombres de tablas y agregar columnas, pero no puede modificar directamente la definición de las columnas existentes (lo cual también representa un inconveniente). Modificar la tabla es imposible, así que tuve que cambiar la lógica del código. En tres pasos: Primero, extraje todas las operaciones SQL que involucraban correo electrónico a un método común y usé `lower(email)` para forzar la conversión a minúsculas durante la consulta.
Segundo, en el punto de entrada de la solicitud, si el parámetro incluye "email", llama a `toLowerCase()`. Tercero, fusiona y elimina las cuentas duplicadas causadas por el uso inconsistente de mayúsculas y minúsculas. Escribir este código fue un verdadero suplicio; fue como cargar con una montaña de excremento.
El problema de distinción entre mayúsculas y minúsculas se ha resuelto temporalmente. Tras analizar los registros durante unos días, observé que el error «Error: D1_ERROR: Se perdió la conexión de red» aparece ocasionalmente, con una probabilidad de aproximadamente unas pocas veces por cada mil. No estoy seguro de si el problema reside en mi código o en D1. Busqué en línea y descubrí que muchos usuarios del foro de CF y de la comunidad de Discord han reportado problemas similares, pero la mayoría no ha encontrado una solución.
La única solución que me sirvió un poco fue volver a intentarlo, lo cual también indicó que esto suele ocurrir 😕. Una de las principales razones por las que finalmente elegí el D1 fue para reducir la carga de la red. Si incluso con esto, siguen existiendo problemas de pérdida de conexión, tengo dudas sobre la estabilidad del D1. Este problema aún no está completamente resuelto; les mantendré informados de cualquier novedad.
Siguiendo con el tema del correo electrónico empresarial, antes usaba el correo empresarial de Alibaba Cloud, que en general funcionaba bien, pero la versión gratuita tenía ciertas limitaciones. No permitía vincular varios dominios y el límite de envío era poco claro. Quería cambiar a un servicio que admitiera varios dominios. Tras investigar un poco, descubrí que usar Gmail con Cloudflare había provocado pérdida de datos, aunque la dirección de Gmail seguía siendo visible.
iCloud es una opción, ya que admite múltiples dominios, pero su estabilidad es notoriamente deficiente. Además, al responder correos electrónicos de usuarios, a menudo no pueden recibirlos porque su cuenta de iCloud está llena, lo que resulta arriesgado. Lark es gratuito actualmente, pero su futuro es incierto. Buscaba algo más fiable. Finalmente, me decidí por Zoho porque es económico: $1 por usuario al mes, y normalmente solo se necesita un usuario.
También admite dominios ilimitados. Sus funciones son bastante completas. Si no utilizas algunas de las funciones avanzadas, la versión gratuita es suficiente. Es importante tener en cuenta que los planes de precios de Zoho varían según el país. La versión china cuesta 5 RMB por persona al mes, lo que a primera vista parece económico, pero requiere un mínimo de 5 usuarios. Recomendamos optar por la versión internacional.
En mi aplicación Java, utilizo directamente el protocolo SMTP para enviar correos electrónicos. Sin embargo, el envío de correos mediante SMTP en el servidor falla porque el protocolo no es compatible. Necesito usar la API HTTP (que no está disponible en la versión gratuita).
Antes usaba DataGrip para visualizar bases de datos, pero no es compatible con Drizzle 1 (al menos no chromewebstore.google.com/detail/drizzle…). La interfaz web de Drizzle 1 también es muy básica. Actualmente, uso principalmente dos plugins: uno es el plugin Drizzle Studio de Drizzle, que es muy práctico: https://t.co/wMKRcC1LaC
Otra opción es TablePlus. Si bien Dizzle Studio es práctico, es un complemento para navegador con funcionalidad limitada, careciendo de características como texto predictivo SQL y guardado del historial. Es adecuado para uso temporal. TablePlus ofrece funciones más completas, y además tengo una versión para SetApp que se puede usar gratis; comprarla por separado es demasiado caro, así que no la recomiendo.
En cuanto a la recopilación y el análisis de registros, anteriormente usábamos el servicio de registros de Alibaba Cloud, similar a la suite ELK, para Java. developers.cloudflare.com/workers/observ…s de Cloud Computing (CF) no conservan los registros; solo se pueden visualizar en tiempo real. Si se desea transferir los registros a otros servicios, se puede usar la función tail worker. Sin embargo, la versión de pago del worker permite usar el servicio de BaseLime de forma gratuita. Este último fue adquirido por Cloud Computing y se integra con un solo clic. https://t.co/YuNLJQo6tP
En cuanto a la atención al cliente, a diferencia de otros proveedores de la nube, Cloud Computing (CF) solo permite facturas, información de cuenta y tickets registrados por defecto, no incidencias técnicas. Además, el soporte en línea requiere una actualización al plan Business, que cuesta 250 $ al mes. Este plan utiliza principalmente permisos de producto relacionados con la CDN, no permisos de usuario. Es improbable que abran una cuenta independiente específicamente para la atención al cliente. Esto es comprensible, dada la gran base de usuarios y su enfoque en atender a los usuarios principales.
¿Y si te encuentras con problemas técnicos? Hay dos opciones: una es publicar una solicitud de ayuda en el foro de desarrolladores de CF. Sin embargo, no se puede garantizar una respuesta inmediata. Además, si no es un problema muy común, es muy probable que nadie pueda ayudarte. He visto publicaciones con problemas que no han recibido ni una sola respuesta incluso después de meses, y las respuestas suelen ser cosas como: «Oye, amigo, yo también tuve el mismo problema, ¿lo solucionaste?».
En segundo lugar, puedes unirte a la comunidad de Discord de CF para dar tu opinión, lo cual suele ser más oportuno. Sin embargo, debido a la diferencia horaria, es posible que otros usuarios aún estén durmiendo cuando te conectes. Además, el comunicado oficial recalca que el personal aquí presente «no son técnicos de soporte, sino desarrolladores y expertos técnicos que responden preguntas de forma voluntaria en su tiempo libre». Por lo tanto, al preguntar aquí, es mejor controlar tus expectativas y emociones.






