Una retrospectiva de las gemas y modas del software

Quizás una de las cualidades más destacadas de un desarrollador es la capacidad de elegir la herramienta adecuada para el trabajo adecuado, sin subirse al carro ni reinventar la rueda. Esto puede requerir un poco de vigilancia tecnológica, pero aún más, un toque de pensamiento crítico.

He aquí un repaso de algunas tendencias exageradas y sutilezas subestimadas, en diferentes áreas del maravilloso mundo de las ciencias de la computación: bases de datos, asincronicidad, criptomonedas y formatos de datos . No volveré sobre el tema de los servicios web REST, sobre los que ya despotricé mucho.

Como de costumbre , sus comentarios son más que bienvenidos si algún error fáctico se desliza en este artículo (no del todo imparcial).

Bases de datos: NoSQL & amp; ZODB

Pocos momentos, en la historia de las ciencias de la computación, fueron tan irónicamente iluminados como la llegada de las bases de datos sin SQL, alrededor de 2009. Un maremoto golpeó las orillas del desarrollo de backend y la administración de sistemas: las bases de datos SQL eran demasiado rígidas, demasiado lentas , demasiado difícil de replicar. Así que los nuevos proyectos los abandonaron masivamente en favor de almacenes de valores clave como Redis, bases de datos orientadas a documentos como MongoDB / CouchDB o bases de datos orientadas a gráficos como Neo4j. Y debemos reconocer una cosa: estas nuevas bases de datos brillaron en los puntos de referencia; brillaron tanto…. como haría brillar cualquier base de datos SQL eliminando todas sus restricciones ACID y flexibilidad de lenguaje de consulta.

Pero el horizonte era sombrío para numerosos programadores. Aprendieron, por las malas, que la persistencia de los datos no era una preocupación menor y que necesitaban, por ejemplo, activar explícitamente “Write Concerns” en MongoDB, para asegurarse de que los datos no se perdieran antes de llegar al óxido del disco. Esa “consistencia eventual” era una bonita palabra para “inconsistencia temporal”, abriendo la puerta a errores desagradables, silenciosos y difíciles de reproducir en producción. Que las transacciones, y su bloqueo implícito, eran características preciosas, y que imitarlas a mano, con banderas incómodas metidas en los documentos, era casi fácil y sólido. Los esquemas de datos y la integridad referencial eran más que bienvenidos para evitar que las bases de datos se convirtieran en montones de objetos incoherentes. Que la falta de capacidades avanzadas de indexación (en múltiples claves, en campos de documentos profundos) en los almacenes de valores clave podría resultar bastante embarazoso.

Por lo tanto, la gente comenzó a reinventar las funciones de SQL sobre bases de datos NoSQL, imitando esquemas de datos, claves externas, agregación avanzada, en bibliotecas “ORM” específicas del lenguaje (mongoengine, mongoid, mongomapper…). En este contexto, este acrónimo “Object-Relational Mapper” debería haber sido, por sí solo, una pista de que algo se había vuelto loco.

Hubo algo surrealista en observar las bases de datos NoSQL, que se perfeccionaron para casos de uso específicos (datos muy replicados o heterogéneos, colecciones de tamaño limitado o TTL, sistemas pub / subs …), que se usaban solo para almacenar un montón de datos de la misma forma objetos en una sola instancia de servidor. Una base de datos SQL estándar habría hecho el trabajo por completo y habría ofrecido muchas más herramientas y complementos (diferentes motores de almacenamiento, scripts del kit de herramientas Percona, IDE como HeidiSql o Mysql Workbench, procesos de migración de esquemas DB integrados en marcos web …); incluso si eso significaba rellenar datos extra no estructurados en un campo de texto serializado (o, hoy en día, campos Json de PostgreSQL dedicados).

Con el tiempo, las propias bases de datos NoSQL mejoraron mucho, entre otras cosas, al tomar prestadas características del mundo SQL. Pero reinventar SQL no es una tarea fácil. Las bases de datos relacionales se ocupan del análisis del lenguaje de consulta, conjuntos de caracteres y agrupaciones, agregación y conversión de datos, transacciones y niveles de aislamiento, vistas y cachés de consultas, activadores, procedimientos integrados, SIG, permisos detallados, replicación y agrupación … funciones complejas y sensibles, controladas por cientos de configuraciones distribuidas en múltiples niveles (por base de datos, por tabla, por conexión). Entonces, a pesar de su gran progreso (transacciones de múltiples documentos, mejor agregación de datos, funciones de JavaScript almacenadas, almacenamiento conectable, control de acceso basado en roles en MongoDB), las bases de datos NoSQL todavía tienen problemas para desafiar las principales bases de datos SQL, puramente en cuanto a características.

Afortunadamente, la mayoría de los proyectos solo necesitan un pequeño subconjunto de estas características de la base de datos SQL: algunas validaciones de esquemas, algunos índices adecuados y el negocio puede comenzar; así que para los equipos que carecen de experiencia en SQL, la relativa simplicidad de muchas bases de datos NoSQL podría ser, para ser honesto, un factor relevante.

La ola parece haberse desvanecido a estas alturas y los proyectos parecen más inclinados a combinar diferentes bases de datos de acuerdo con las necesidades reales. Por lo tanto, separan las cuentas de usuario, las colas de trabajos y los cachés similares, los datos de registro y de estadísticas … cada uno en el almacenamiento más relevante.

Todas estas bases de datos NoSQL citadas, y sus innumerables alternativas, están brillando en sus casos de uso previstos. Pero me gustaría mencionar una joya del ecosistema Python muy poco conocida y muy poco utilizada. ¿Ya ha querido conservar sus datos de una manera realmente muy fácil? Entonces te reenvío a la ZODB. Lo abres como un diccionario, introduces los datos que quieras en él, confirmas la transacción y listo.

Ejemplo de instancia de ZODB local simple:

Los gráficos de datos se manejan con elegancia (sin error de recursividad), los objetos se cargan de manera perezosa en el acceso, se proporcionan tipos especiales de “árbol de depósitos” para explorar grandes cantidades de datos mientras se mantiene baja la memoria, y existen varios backends de almacenamiento, incluido el almacenamiento que aprovecha el poder de las bases de datos SQL. Perfecto, ¿no?

Muy bien, estoy mintiendo, hay algunas trampas. No hay un sistema de indexación incorporado (uno debe usar Zcatalog o los me gusta en su lugar); Se recomienda encarecidamente utilizar tipos “persistentes” dedicados para detectar y persistir automáticamente las mutaciones de los objetos; las herramientas generales son bastante limitadas en comparación con las bases de datos convencionales; y el modelo de concurrencia basado en el “bloqueo optimista” podría obligarlo, bajo una carga pesada, a reintentar una operación varias veces hasta que logre aplicarse. La gran cantidad de integración con el lenguaje Python tiene un inconveniente adicional: si introduce cambios importantes en su modelo de datos, es posible que su base de datos ya no se cargue, por lo que debe manejar las migraciones de esquemas con cuidado.

Pero el contexto lo es todo: ZODB no está diseñado para la persistencia de datos interoperables a largo plazo, sino para el almacenamiento sin esfuerzo de objetos de Python (posiblemente muy heterogéneos); Puede hacer que los scripts de larga ejecución puedan reanudarse después de una interrupción, puede almacenar datos de jugadores de sesiones de juegos en línea … si realmente desea almacenar artículos de blog o cuentas personales en ZODB, es mejor que se limite a los tipos de Python nativos e implemente su propios controles de cordura. Pero pase lo que pase, no use un estante stdlib muy limitado, si puede tener un ZODB ingenioso en la mano para almacenar sus datos de trabajo en progreso.

Asincronicidad: Asyncio, Trio y Green Threads

Ha habido un desafío inmemorial entre los modelos de programación sincrónicos y asincrónicos, en todos los programas vinculados a IO. Los kernels han proporcionado modos asincrónicos para operaciones de disco, con más o menos éxito (IO superpuesto sin bloqueo en Windows, API io_submit () limitada en Linux…). El código de red ha agudizado aún más el problema, con la necesidad de una gran cantidad de conexiones a largo plazo, cada una de las cuales realiza solo operaciones menores de la CPU. Algunos lenguajes, como Erland, se enfrentaron a esto siendo asíncronos desde el principio y permitiendo que diferentes tareas se comunicaran mediante el paso de mensajes (también conocido como Modelo de actor).

En otros lenguajes, surgieron varios patrones de diseño para abordar el problema:

La devolución de llamada era anteriormente la solución principal en los marcos convencionales, por ejemplo, en jQuery o Twisted: el desarrollador proporcionaría callables como argumentos o como métodos de instancia, y estos serían llamados al completar / cancelar IO, en un patrón llamado Inversión de control . Funciona, seguro, pero da flujos de programa bastante difíciles de predecir y depurar, de ahí el término “sopa de devolución de llamada” que se usa a menudo en este contexto.

Durante los últimos años, la sintaxis async / await se ha puesto de moda, especialmente en el mundo de Python. Pero hay un problema: como Inversión de control, es una forma completamente nueva de programación, casi un lenguaje nuevo. La gran cantidad de paquetes disponibles actualmente, hechos de módulos, clases y métodos, simplemente NO funciona con async / await. Cualquier IO, cualquier operación costosa, escondida en lo más profundo de una subdependencia, podría arruinarle el día. Por lo tanto, actualmente estamos viendo miles de excelentes módulos que se están reimplementando felizmente, con un mundo completamente nuevo de errores y funciones faltantes.

¿Vale la pena todo? Los desarrolladores de Python se han lanzado masivamente al tren del paquete asyncio, que se ha convertido en parte de stdlib. Pero esta tecnología tiene problemas aterradores, como la dificultad de la contrapresión del socket, el manejo frágil de excepciones y ctrl-C, la cancelación insegura de tareas (con fugas) y la curva de aprendizaje empinada de una API llena de errores y conceptos redundantes. Otros marcos, como Trio / Curio, parecían mucho más cuidadosos en estos temas. Si tenemos que recodificar toneladas de bibliotecas existentes, ¿por qué basar las nuevas versiones en un motor que algunos desarrolladores tienen, no sin argumentos, llamado un basurero de mal diseño? Pero el efecto de red es enorme en tales casos, y los frameworks alternativos basados ​​en async / await tendrán dificultades para desafiar el estándar.

¿Y qué hay del tercer patrón citado anteriormente, hilos ligeros? Mucho antes de esta tendencia asincrónica / en espera, los desarrolladores de Python pensaron: ya tenemos un código comercial sincrónico perfectamente fino, así que cambiemos la forma en que se ejecuta, no la forma en que está escrito. Así aparecieron hilos ligeros, o “greenlets”. Funcionan como un montón de pequeñas tareas programadas sobre unos pocos subprocesos nativos, tareas que se controlan entre sí solo cuando se bloquean en IO o lo hacen explícitamente; y con un rendimiento mucho mayor que los subprocesos nativos, en términos de uso de memoria y retraso de conmutación.

Al final, este sistema puede impulsar rápidamente cualquier base de código existente para que admita miles de tareas simultáneas a largo plazo. Y este no es un experimento loco aislado: los hilos ligeros de Python se han utilizado originalmente en el juego Eve Online (a través de Stackless Python), y desde entonces se han portado con éxito a CPython (Gevent, Eventlet…) y PyPy. Y de hecho, han existido durante mucho tiempo en muchos lenguajes de programación, con diferentes nombres (procesos verdes, hilos verdes, fibras …).

¿Los inconvenientes de este sistema?

Como vemos, estos inconvenientes son similares a los de async / await, excepto que casi no tienes que tocar el código síncrono original. Un “excepto” que puede significar meses o años de trabajo evitados; su CTO y CEO deberían estar muy satisfechos con esto.

Ahora, a veces escuchará raras racionalizaciones de personas que abandonaron los hilos ligeros en favor de una reimplementación completa asíncrona / en espera. Algo en las líneas de “ Explicit es mejor que implícito, y todas estas esperas me muestran exactamente dónde mi código podría cambiar de contexto, mientras que los hilos verdes podrían cambiar discretamente si una función de terceros realiza cualquier tipo de IO o cambio explícito ”.

Pero la cosa es …

PRIMERO, ¿por qué necesita saber en qué puntos exactamente el programa cambiará a otra tarea? Durante todos los últimos años, con subprocesos nativos (preventivos), un cambio podría ocurrir en cualquier lugar, en cualquier momento, incluso justo en medio de un incremento simple. Pero aprendimos a lidiar con esta amenaza invisible correctamente, protegiendo las secciones críticas con bloqueos y otras primitivas de sincronización (bloqueos recursivos, evento, condición, semáforo …), manteniendo un orden adecuado al anidar bloqueos y utilizando estructuras de datos seguras para subprocesos (colas y similares) que manejan la concurrencia para nosotros. Los subprocesos verdes son un término medio entre los subprocesos preventivos (implícitos) y asincrónicos / esperados (explícitos), pero es mejor que todas estas tecnologías se adhieran a la buena forma antigua de proteger las operaciones concurrentes.

Los bloqueos pueden ser peligrosos cuando se usan incorrectamente (especialmente porque la mayoría de las implementaciones se bloquean, en lugar de detectar puntos muertos y reportarlos como excepciones), pero son baratos y robustos. ¿Cuál es el punto de intentar hacer una simultaneidad sin bloqueo, comprobando la posición de cada llamada que pueda activar el interruptor, cuando en cualquier momento podría tener que agregar una nueva operación (incluso una salida de registro simple) en medio de su bloqueo cuidadosamente elaborado -sin secuencia y, por lo tanto, arruinar su seguridad?

Este código ingenuo muestra cómo una llamada recientemente agregada a log_counter_value () rompe un código asincrónico que de otro modo sería seguro.

SEGUNDO, ¿realmente tienes que lidiar con la sincronización? Especialmente en el mundo web, donde se supone que las solicitudes HTTP no interactúan, queremos paralelización, no concurrencia. Se supone que los datos persistentes (y transacciones) deben ser manejados por bases de datos y cachés externos, no en el montón de memoria de proceso. Por lo tanto, las buenas prácticas habituales de seguridad de subprocesos (utilizando la inicialización segura de subprocesos del proceso a través de bloqueos, estructuras de solo lectura para datos globales y datos de lectura y escritura solo locales para los marcos de pila) son suficientes para hacer que todo el sistema sea “thread / greenlet / asynctask seguro ”. Si un día necesita implementar algoritmos altamente concurrentes dentro de un proceso, elegirá la mejor herramienta para eso, pero no es necesario construir fábricas de martillos si todo lo que tiene que hacer es clavar un clavo.

Dinero: Bitcoins & amp; Alternativas

Reflexionemos por un momento. ¿Cuáles son los mayores desafíos de nuestro siglo XXI? ¿Cambio climático? ¿Evasión de impuestos? ¿Legitimidad del poder estatal? Entonces, las mentes sinceras podrían pensar que la sobriedad energética, la trazabilidad financiera y las organizaciones (realmente) democráticas serían objetivos a perseguir.

Pero un grupo de hackers inteligentes decidió que el dinero actual era un problema importante y se le ocurrió Bitcoins: un sistema de “prueba de trabajo” devorador de energía, fácil anonimato de los poseedores de dinero y un gobierno confuso (por lo menos).

Con tal adecuación entre necesidades y demanda, no es de extrañar que Bitcoins se convirtieran en lo que se convirtieron: un producto de (casi) pura especulación, elogiado por ransomwares y mafias diversas, explotado en masa por fábricas de tarjetas gráficas, con un nivel especialmente alto apetito por ser robado (o perdido). Este dinero, y sus hermanos que pronto surgieron, tienen una historia ya llena de momentos desconcertantes, con divisiones accidentales de cadenas, bifurcaciones blandas bloqueadas por razones políticas, bifurcaciones duras decididas arbitrariamente por diversas personas (o forzadas por ciberataques) y batallas interminables. entre diferentes monedas, o diferentes versiones de la misma moneda (Bitcoin Core, Cash, Gold, SV…). Los algoritmos (criptografía, consenso, código de transacción …) fueron elogiados como la base de un sistema autónomo y a prueba de balas, pero algunos actores tuvieron que piratear a sus propios usuarios para protegerlos del robo, mientras que incluso los tan glorificados “contratos inteligentes” mostró un montón de debilidades de seguridad aterradoras y no tantos casos de uso como algunos esperaban.

Dejémoslo claro: la cadena de bloques, un libro de contabilidad público basado en árboles de Merkle, está lejos de ser una mala idea. Pero cuando las decisiones no se basan en las necesidades de la sociedad y el cuidado con los errores, sino en la ideología y la codicia, se puede predecir el resultado. Y la disminución de la exageración es proporcional a las esperanzas invertidas indebidamente.

¿Cuál es la “mejor” contraparte de Bitcoin, Ethereum y similares? Existen muchas criptomonedas alternativas, con formas más ligeras de autorización, con diferentes algoritmos criptográficos, con diferentes configuraciones de privacidad, con diferentes tasas de adopción también … Pero si me preguntas, lo que realmente necesitaríamos es “ un dinero fácilmente rastreable para el Estado finanzas y ONG ”; un libro de contabilidad público diseñado para que cualquier ciudadano pueda auditar fácilmente cómo se utiliza el dinero público, desde el momento en que se recauda mediante impuestos y donaciones, hasta el momento en que vuelve a los circuitos privados mediante el pago de bienes o salarios de empleados. ¿Alguien ya existe algo así? No pude encontrarlo …

También se pueden mencionar los dineros no criptográficos pero locales (p. ej. el “Gonette” en Lyon, Francia), que se mantienen a la par con los dineros nacionales, que tienen la ventaja de favorecer a las empresas locales y, por tanto, reducir los daños colaterales del comercio internacional.

Formatos de datos: texto y binario

Un transeúnte ingenioso una vez definió XML como “ la legibilidad de datos binarios con la eficiencia del texto “. De hecho, los analizadores XML tienden a ser lentos y saturan la memoria (cuando están en modo DOM), en comparación con los cargadores de datos binarios; y editar configuraciones XML y documentos a mano no es la mejor experiencia de usuario que uno podría tener.

Entendemos fácilmente por qué XML, como metalenguaje que permite crear nuevas etiquetas y propiedades para todo tipo de usos, debe ser tan detallado. Pero, ¿por qué tanto entusiasmo por los formatos basados ​​en texto, cuando el objetivo es transmitir información entre servidores utilizando tipos de datos bien definidos?

Analizar cargas útiles HTTP en una representación interna y luego analizar, por ejemplo, su cuerpo JSON, termina agregando una sobrecarga significativa a las solicitudes de servicios web. ¿Para qué ganar? Los formatos binarios como Bson harían que la serialización / deserialización sea mucho más eficaz; y se podrían usar formatos de texto semánticamente equivalentes para la depuración (convertidos automáticamente por las herramientas de desarrollo del navegador web, Wireshark, CURL y similares) y para crear manualmente cargas útiles de prueba.

sin tanto esfuerzo.

Conclusión

¿Cuál es la moraleja de todo esto? Siempre lo mismo, “ use la herramienta adecuada para el trabajo correcto y tenga cuidado con las modas irracionales “.Puede ser necesario leer mucho antes de tener la suficiente profundidad de visión, sobre un tema específico, para tomar decisiones informadas; pero esta inversión se amortiza rápidamente.

Adivinar qué tan bien será un marcoapoyado a largo plazo, o qué protocolo / formato ganará una guerra de estandarización, es un problema diferente, pero al menos podemos tener nuestras opiniones firmemente fundamentadas, cuando se trata de aspectos puramente técnicos, y esto es Gold.