Ingeniería de software moderna: Parte 1: Diseño de sistemas

Al crecer a finales de los 80 y principios de los 90, mi exposición a las computadoras se limitó bastante a las consolas (creo que la Atari 800 y la Commodore 64 porque solo había visto juegos ejecutándose en ellas) o los primeros sistemas X86. No fue hasta que estuve en la universidad en el año 2000 que tuve una estación de trabajo Sun Microsystems SPARC, UNIX y Slackware Linux que podía instalar en mi máquina Intel 486 en casa.

En aquel entonces, el desarrollo de software significaba principalmente software que se ejecutaba en su máquina o, si tenía la oportunidad, en una computadora de tiempo compartido con significativamente más potencia de procesamiento de la que podía... hacer asuntos relacionados con el negocio. En la universidad, recuerdo haber oído hablar de un programa utilizado por científicos informáticos que requería un procesador multinúcleo para generar horarios de cursos para miles de estudiantes; tomó semanas generar e imprimir los horarios de los cursos. Hasta el día de hoy, todavía no estoy seguro de qué tomó más tiempo: ejecutar el programa e imprimir en papel.

Hoy en día, la mayor parte del software que se desarrolla se ejecuta en la nube, en un dispositivo que necesita acceder a la nube, o impulsa otro software que también se ejecuta en la nube. Los sistemas de software que funcionan en espacios reducidos, como los sistemas de software integrados, son muy raros si no hay plataformas informáticas más potentes disponibles en otros lugares. Los sistemas de contabilidad ahora comprimen grandes cantidades de datos que se alojan en granjas de servidores en almacenes de datos dentro o fuera de la empresa. Las relaciones con los clientes para los sistemas de ventas ahora son administradas por terceros, y sus complementos son desarrollados por terceros o desarrolladores internos.

Pero, ¿cómo se construyen hoy estos sistemas de software para atender a cientos o millones de usuarios y al mismo tiempo mantener el rendimiento y la capacidad de respuesta que se espera del software que utilizamos hoy?

Como ingeniero de software durante 20 años, he visto muchos sistemas desarrollados en todos los niveles de la pila. Desde controladores de interrupciones de la era DOS hasta animaciones basadas en JavaScript e incluso generación de informes sin código. ¡Hace unas semanas, incluso hice que ChatGPT-4 generara el código Python que quería basándose en algunas descripciones que le di! ¡Pero esa es otra historia! Pero esa es otra historia.

En este artículo, escribo sobre el diseño de sistemas, cómo se ha convertido en una parte clave de la práctica moderna de la ingeniería de software y cómo será una de las áreas clave donde los ingenieros de software humanos aún pueden aportar valor en el corto y mediano plazo.

La importancia del diseño del sistema.

Hace mucho tiempo, yo era ingeniero de software para una empresa que tenía problemas para manejar la carga de éxito que habían logrado. A esta empresa la llamo Friendster. Cuando me uní a la empresa, el proyecto en el que estaba trabajando llegaba muy tarde y tenía muchos errores relacionados con la gestión de la memoria. Su servicio principal (sí, era un microservicio antes de que lo llamaramos así en 2007) está escrito en C++, pero tiene pérdidas de memoria, tarda demasiado en procesar las solicitudes y está diseñado para almacenar en caché y servir datos en su propia memoria. Tenía que ser sin estado, pero terminó siendo con estado.

Unas semanas después de iniciado el proyecto, le rogué a los altos directivos de ingeniería que abandonaran esta iteración del servicio y en su lugar escribieran algo compatible desde cero; sería un reemplazo directo para la implementación existente. Tenemos una fecha límite porque el servicio solo puede manejar el crecimiento durante unos meses más antes de que ya no pueda manejar el tamaño del caché en rehidratación.

Reiniciar el servicio lleva más tiempo del que puede aguantar antes de perder memoria. Fue un momento de "apostar mi carrera", pero casi no tuve posibilidades. Tenemos que hacerlo funcionar.

Comienza el diseño del sistema. Lo primero que hacemos es establecer los requisitos que debe cumplir el sistema, cuál es el contrato entre el servicio dependiente (código front-end PHP) y este servicio principal, y un plan sobre cómo cumpliremos tres requisitos no técnicos clave. : Rendimiento, eficiencia y resiliencia.

El diseño de sistemas implica comprender las restricciones bajo las cuales un sistema debe realizar su función, cuál es la función deseada y qué propiedades del sistema son importantes en relación con todas las demás propiedades. Una vez que tenga estas definiciones, puede comenzar a diseñar un sistema que cumpla con los requisitos y planificar sistemáticamente la entrega de la solución.

Componentes del diseño del sistema.

Cuando hablamos de diseño de sistemas, generalmente hay varios componentes que requieren:

  • Arquitectura: ¿cómo es la solución general? ¿Implica múltiples subsistemas? ¿Hay componentes individuales que forman un todo? ¿Cómo interactúan y cuáles son sus relaciones?
  • Topología: ¿la solución tiene capas? Si se trata de un sistema distribuido, ¿dónde están ubicados física o lógicamente los servicios componentes?
  • Diseño de bajo nivel: ¿Qué interfaces ha definido a través de las cuales interactúan las diferentes partes del sistema? ¿Tiene algoritmos específicos que aborden aspectos clave de la solución (rendimiento, eficiencia, rendimiento, resiliencia, etc.)?

Primero es necesario entender cosas como: ¿Es el sistema autónomo (es decir, no accederá a recursos externos) o está distribuido? ¿Tendrá una interfaz de usuario o no será interactivo (por ejemplo, generará un informe impreso o requerirá la entrada de un ser humano u otro sistema durante su funcionamiento)? ¿Necesita manejar mucho tráfico? ¿Lo utilizarán sólo diez personas a la vez o lo utilizarán 10 millones de usuarios a la vez?

Una vez que tenga respuestas a algunas de estas preguntas, será más fácil tomar decisiones a través de los principios de diseño del sistema.

Principios de diseño de sistemas.

En esta sociedad moderna, varios principios clave para el diseño de sistemas de software no emergen completamente hasta que el sistema necesita escalar: de un sistema de un solo usuario a uno que debería ser capaz de manejar miles o incluso millones de usuarios simultáneamente. Estas son algunas de las cosas que cubriremos en este artículo:

  • Escalabilidad
  • FiabilidadFiabilidad
  • MantenibilidadMantenibilidad
  • Disponibilidad Disponibilidad
  • SeguridadSeguridad

Escalabilidad

Un sistema es escalable cuando se puede implementar para manejar aumentos de carga a medida que los recursos crecen proporcionalmente. El factor de escalabilidad de un sistema se define como el aumento en la cantidad de recursos necesarios para atender el aumento en la carga del sistema. Nos encontraremos con dos situaciones típicas de expansión en sistemas software: expansión vertical y expansión horizontal.

La expansión vertical se refiere a proporcionar más espacio o recursos independientes para que un sistema de software maneje el aumento de la demanda. Consideremos el caso de los dispositivos de almacenamiento conectados a la red. Cuanto más almacenamiento proporcione a través de un dispositivo, más datos podrá almacenar. Si lo necesita para manejar más conexiones y operaciones de E/S (IOP) simultáneas, normalmente necesitará agregar más potencia informática e interfaces de red para manejar el aumento de carga.

El escalado horizontal se refiere a replicar un sistema o varias máquinas con copias de software para manejar el crecimiento de la demanda. Considere el caso de un servidor de contenido web estático escondido detrás de un equilibrador de carga. Agregar más servidores permite que más clientes se conecten y descarguen contenido de los servidores web, y cuando la carga disminuye, la cantidad de servidores web se puede reducir para satisfacer la demanda actual.

Algunos sistemas pueden manejar extensiones mixtas o diagonales. Por ejemplo, algunas arquitecturas de bases de datos distribuidas permiten la partición de nodos de computación y almacenamiento para que las cargas de trabajo con uso intensivo de computación puedan utilizar nodos con más recursos de computación. Por el contrario, las cargas de trabajo con muchos IOP se pueden ejecutar en nodos de almacenamiento y computación. Por ejemplo, una aplicación de procesamiento de flujo podría aislar cargas de trabajo que requieren más memoria y computación (por ejemplo, cargas de trabajo de análisis o abastecimiento de eventos) y escalar esas cargas de trabajo de manera apropiada e independientemente de las cargas de trabajo pesadas con IOP (por ejemplo, compresión y archivo).

FiabilidadFiabilidad

Un sistema es confiable cuando puede tolerar fallas y recuperaciones parciales sin degradar gravemente la calidad del servicio. Parte de la confiabilidad de un sistema incluye la previsibilidad de sus operaciones, es decir, la latencia, el rendimiento y el cumplimiento de los límites operativos acordados.

Los métodos comunes para garantizar la confiabilidad del sistema incluyen los siguientes aspectos:

  • Configure la redundancia del sistema para admitir una conmutación por error transparente o mínimamente disruptiva.
  • Establezca tolerancia a fallas en caso de errores internos o fallas inducidas por entradas.
  • Defina claramente contratos y objetivos de latencia, rendimiento y disponibilidad.
  • Establezca suficiente capacidad adicional para dar cabida a aumentos repentinos y orgánicos de carga.
  • Medidas de garantía de la calidad del servicio para hacer cumplir los límites de tarifas y la segregación entre clientes y empresas.
  • Aplique una degradación gradual del servicio en caso de sobrecarga o falla catastrófica.

La clave que hay que recordar al crear sistemas confiables es manejar fallas potenciales de una manera bien definida que permita que los sistemas dependientes reaccionen. Esto significa que si hay entradas que podrían hacer que el sistema no esté disponible para todos, entonces no es un sistema confiable. Del mismo modo, si un sistema depende de otro sistema que puede no ser confiable, entonces debería tener políticas para abordar la falta de confiabilidad para garantizar la confiabilidad.

MantenibilidadMantenibilidad

Un sistema es mantenible cuando los cambios se realizan con un esfuerzo proporcional y se implementan con una mínima interrupción para el usuario. Esto requiere que al implementar el sistema, se asuma que los requisitos cambiarán y que el sistema sea lo suficientemente flexible para manejar cambios de dirección previsibles. También significa garantizar que el código sea legible para que el próximo grupo de mantenedores (quizás el mismo equipo pero mirándolo con nuevos ojos en el futuro) pueda mantener el software y permitirle evolucionar para satisfacer necesidades futuras.

Nadie quiere quedarse estancado manteniendo un software rígido, difícil de cambiar, mal organizado, mal documentado, mal diseñado, no probado y improvisado.

Garantizar una alta calidad del código es parte de la excelencia en ingeniería y refleja profesionalismo y excelente artesanía. Esto no solo es algo bueno, sino que también permite que equipos de ingeniería altamente funcionales y de alto rendimiento entreguen software que puede cambiar y escalar para continuar brindando valor.

Disponibilidad Disponibilidad

Si su servicio no está disponible, probablemente no exista.

El diseño del sistema debe abordar cómo un sistema debe permanecer disponible para seguir siendo relevante para los clientes y usuarios del sistema. esto significa:

  • Introducir redundancia para manejar fallas subyacentes del sistema.
  • Tenga planes de respaldo y recuperación y orientación operativa para permitir la recuperación del sistema ante fallas graves.
  • Elimine tantos puntos únicos de falla como sea posible del sistema.
  • Además de la escalabilidad horizontal, tenga replicación regional y configure una red de entrega de contenido (cuando corresponda) para que sus datos estén disponibles.
  • Supervise la disponibilidad de su sistema desde la perspectiva del cliente para comprender mejor cómo su sistema presta servicio a sus clientes.

Al principio de mi carrera, aprendí que un sistema inestable e inutilizable a veces puede ser la principal razón para perder la confianza del cliente. Una vez que pierde la confianza de sus clientes, es difícil recuperarla.

SeguridadSeguridad

El diseño del sistema debe abordar la seguridad como un vínculo clave, especialmente en la era de los sistemas conectados a Internet, donde las amenazas y vulnerabilidades de seguridad pueden causar daños reales a nuestros clientes y usuarios del sistema. El objetivo de crear software seguro no es ser perfecto, sino comprender los riesgos involucrados en las vulnerabilidades y los ataques. Contar con un modelo de amenazas a la seguridad implementado y un enfoque sistemático para comprender dónde residen los riesgos y qué tipos de amenazas justifican priorizar y diseñar mitigaciones es donde comienzan las prácticas de diseño e ingeniería de seguridad.

Hoy en día, la seguridad ya no es opcional a medida que nuestros sistemas de software pasan a formar parte de servicios de misión crítica en más partes de la sociedad moderna. Tomar en serio la seguridad desde el principio en los sistemas que diseñamos nos acerca a poder confiar mejor en el software que creamos e implementamos para satisfacer las necesidades de nuestros usuarios. Ganarse la confianza de sus clientes ya es bastante difícil; sólo hace falta una infracción para perder una buena parte de ella.

patrones de diseño moderno

En vista de los aspectos anteriores, han surgido algunos patrones de sistemas distribuidos modernos para resolver algunos problemas en estos aspectos de diferentes maneras. Exploremos algunos de los patrones de diseño más populares que hemos visto hoy con respecto a los cinco aspectos del diseño de sistemas.

MicroserviciosMicroservicios

Con el auge de los sistemas distribuidos, que se centran en generar confiabilidad y escala a través de la redundancia, eficiencia y rendimiento a través del escalamiento horizontal, y resiliencia al desacoplar partes del sistema en servicios que se ejecutan de forma independiente, el término "microservicios" gana popularidad al lograr lo siguiente:

  • Conecte el desarrollo, implementación, operación y mantenimiento de servicios independientes con los equipos propietarios de esos servicios dentro de la operación comercial más amplia. Podemos hacer esto atendiendo a clientes externos directa o indirectamente a clientes internos a través de API.
  • Permita que los microservicios escale de forma independiente según la demanda.
  • Los servicios se proporcionan a través de un contrato bien definido, lo que permite a los implementadores evolucionar hacia un servicio independiente o un sistema de servicios.

Desde nuestra perspectiva, los microservicios tienen propiedades atractivas que los convierten en un buen patrón si se aplica al caso de uso:

  • Escalabilidad: los microservicios sin estado suelen estar diseñados para escalar horizontalmente y también pueden beneficiarse del escalamiento vertical. Cuando los microservicios se implementan en un entorno de orquestación en contenedores, como un clúster de Kubernetes, los microservicios pueden incluso ejecutarse en los mismos nodos, haciendo un mejor uso del hardware existente y escalando la capacidad disponible según la demanda. Un inconveniente es la complejidad de la implementación a medida que el tamaño y la criticidad de un microservicio crecen dentro de un gráfico de microservicio.
  • Confiabilidad: los microservicios sin estado generalmente se alojan detrás de balanceadores de carga y se distribuyen geográficamente para evitar fallas regionales que consumen toda la capacidad del sistema. Una desventaja de establecer la confiabilidad con microservicios sin estado es que el sistema de almacenamiento a menudo necesita ser tan confiable o incluso más confiable que la implementación/implementación del microservicio. Los microservicios con estado sufren lo peor de ambos enfoques: el costo de la confiabilidad a menudo se presenta en forma de aprovisionamiento excesivo para manejar posibles interrupciones.
  • Mantenibilidad: microservicios que implementan contratos estables y bien definidos proporcionados a través de una API, lo que permite a los clientes programar con esa API e implementaciones que pueden evolucionar de forma independiente. Sin embargo, coordinar los cambios de API implica migraciones de clientes potencialmente costosas y coordinación entre equipos, lo que introduce un período en el que un microservicio tiene múltiples versiones con soporte activo hasta que el cliente final migra desde la implementación anterior. Esta situación solo empeorará a medida que más clientes comiencen a interactuar con microservicios.
  • Disponibilidad: los microservicios a menudo dependen de entornos de implementación e infraestructura externa para cumplir con los requisitos de disponibilidad del cliente. La desventaja de esto es depender de la infraestructura específica en la que se implementan los microservicios para proporcionar una solución de alta disponibilidad. Los sistemas como la malla de servicios y los balanceadores de carga de software se convierten en partes críticas de la infraestructura y ya no están controlados por los implementadores. Esto puede ser algo bueno, pero también puede ser una fuente continua de mantenimiento, ya que estos sistemas también tienen ciclos de actualización y costos operativos.
  • Seguridad: la autenticación, la autorización, la gestión de identidades y la gestión de credenciales se pueden delegar al middleware o mediante mecanismos externos (como la identidad de la carga de trabajo en Kubernetes), y las implementaciones de microservicios pueden centrarse en integrar la lógica empresarial relevante. Al igual que con la disponibilidad, la desventaja es que estas partes externas de la solución se convierten en partes críticas de la infraestructura, lo que suma sus propios costos operativos a la implementación de microservicios.

Los microservicios son una excelente manera de descomponer una aplicación grande, donde puede identificar particiones lógicas que necesitan sus propios dominios para escalamiento y confiabilidad. Sin embargo, cuando se comienza desde cero, diseñar microservicios desde cero no es lo ideal, ya que es posible dividir el servicio en partes demasiado pequeñas. El costo de la comunicación entre microservicios (normalmente solicitudes HTTP o gRPC) es significativo y solo se debe incurrir en él cuando sea necesario. Una buena forma de determinar si la funcionalidad es apropiada para un servicio es seguir prácticas como el diseño basado en dominio o la descomposición funcional.

Sin servidor

Al igual que en las soluciones basadas en microservicios, el uso de una implementación sin servidor delega aún más partes funcionales críticas de las solicitudes de servicio a la infraestructura subyacente. Si en los microservicios el servicio lo proporciona un proceso persistente, las soluciones sin servidor normalmente solo implementan un único punto de entrada para manejar las solicitudes al punto final (generalmente un URI a través de HTTP o gRPC). En una implementación sin servidor, no se configura ningún servidor real; en cambio, el entorno de implementación genera recursos según sea necesario para manejar las solicitudes entrantes. A veces estos recursos permanecen durante un período de tiempo para amortizar el costo de iniciarlos, pero eso es sólo un detalle de implementación.

Veamos varios aspectos del diseño de sistemas para ver cómo se comparan las soluciones sin servidor:

  • Escalabilidad: las soluciones sin servidor son tan escalables horizontalmente como los microservicios, si no más, porque están diseñadas para escalar según demanda. La desventaja de este enfoque es que requiere más control y delega completamente la funcionalidad de escalado a la infraestructura sin servidor subyacente.
  • Confiabilidad: la confiabilidad sin servidor depende de la capacidad de escalar horizontalmente y enrutar el tráfico de red. Esto tiene los mismos inconvenientes que las soluciones de microservicios.
  • Mantenibilidad: una implementación sin servidor es más fácil de mantener que los microservicios porque se centra en la lógica empresarial de manejar solicitudes y minimiza las plantillas. Este es el mismo problema de evolución de API que tienen los microservicios.
  • Disponibilidad: las implementaciones sin servidor solo están disponibles según el entorno en el que se implementan. Esto tiene el mismo problema, donde la infraestructura subyacente se vuelve más crítica que la solución misma.
  • Seguridad: la implementación sin servidor depende completamente de la configuración de seguridad de la infraestructura subyacente. Esto tiene el mismo problema, donde la infraestructura subyacente se vuelve más crítica que la solución misma.

Las soluciones sin servidor, o funciones como servicio, son una forma muy atractiva de crear prototipos e incluso implementarlas en producción centrándose en la lógica y el valor empresarial y dejando que la infraestructura subyacente maneje la escalabilidad, confiabilidad y disponibilidad de la solución a nivel de servicio. Este es un punto de partida típico para poner en funcionamiento una solución con una carga operativa mínima y, como ocurre con la mayoría de los prototipos, es una excelente manera de probar nuestra hipótesis. También es una experiencia típica que una vez que estas soluciones alcanzan sus límites de escala, los costos asociados con su ejecución se vuelven bastante altos. Todo esto se convierte en implementaciones de microservicios más optimizadas y ajustadas a la escala requerida.

Impulsado por eventosImpulsado por eventos

Sin embargo, algunas áreas problemáticas no requieren procesamiento de transacciones en línea, y los microservicios y las implementaciones sin servidor no cumplen con los requisitos. Considere situaciones en las que las transacciones se puedan procesar en segundo plano o cuando haya recursos disponibles. Otro caso es la actividad de procesamiento en segundo plano, cuyo resultado no es necesariamente interactivo.

Los sistemas controlados por eventos siguen el patrón de tener un origen de eventos y un receptor de eventos, desde donde provienen y se envían los eventos (mensajes). El procesamiento lo realizan los suscriptores y editores, respectivamente, en estas fuentes y receptores. Un ejemplo de un sistema basado en eventos es un chatbot que puede participar en muchas conversaciones (fuentes y receptores de eventos) y procesar mensajes a medida que llegan.

Los sistemas distribuidos controlados por eventos pueden tener varios controladores de mensajes simultáneos esperando en el mismo origen, lo que podría publicar demasiados receptores como orígenes para otros controladores de mensajes. Este patrón de encadenamiento de procesadores a través de sumideros y fuentes se denomina canalización de eventos. Normalmente, los sumideros y fuentes tienen una implementación única que proporciona una interfaz de cola de mensajes y se extiende según la demanda de mensajes que pasan por el sistema. Muchos sistemas de gestión de colas distribuidas también pueden beneficiarse eficazmente del escalado diagonal, como Apache Kafka, RabbitMQ, etc.

Examinemos los sistemas distribuidos controlados por eventos a través de nuestros cinco aspectos:

  • Escalabilidad: tanto las implementaciones de intermediarios de mensajes/eventos como los controladores de mensajes son escalables de forma independiente. Algunas desventajas surgen cuando se procesan demasiados mensajes/eventos y la demanda del agente de eventos crece mucho más allá de la capacidad disponible del sistema.
  • Confiabilidad: una buena implementación de intermediario de mensajes proporciona un alto nivel de confiabilidad. Es una buena idea no crear su propia implementación de intermediario de mensajes. La desventaja es la dependencia de una solución que satisfaga las necesidades de confiabilidad (por ejemplo, procesar transacciones financieras es muy diferente de procesar el enrutamiento de mensajes instantáneos para una sala de chat).
  • Mantenibilidad: si utiliza un formato de intercambio de mensajes flexible como búferes de protocolo, tiene sentido evolucionar los redactores y lectores de mensajes utilizando el mismo lenguaje de descripción de datos. Esto todavía requiere coordinación, pero no es tan engorroso como la evolución de contratos API en sistemas de procesamiento de transacciones en tiempo real (como en microservicios e implementaciones sin servidor).
  • Disponibilidad: dado que los mensajes generalmente se almacenan en medios persistentes, los sistemas controlados por eventos generalmente son más fáciles de lograr disponibilidad, especialmente porque generalmente son aplicaciones no interactivas. Los costos de disponibilidad pueden provenir de mensajes obsoletos y retrasos ilimitados en el procesamiento de colas.
  • Seguridad: los sistemas controlados por eventos deben gestionar la disponibilidad de los datos independientemente de las identidades y credenciales. Garantizar que sólo ciertos servicios o procesadores de mensajes tengan acceso a colas o registros de mensajes específicos se convierte en un trabajo de tiempo completo a medida que se extraen datos cada vez más diversos a través del sistema.

Conclusión

La ingeniería de software moderna requiere diseñar sistemas que sean escalables, confiables, mantenibles, disponibles y seguros. El diseño de sistemas distribuidos requiere requisitos muy estrictos porque la complejidad real de los sistemas modernos crece con la demanda de la sociedad de mejores servicios de software. Revisamos tres patrones de diseño modernos para sistemas distribuidos y examinamos cinco aspectos de un sistema bien diseñado.

Como ingenieros de software, tenemos la responsabilidad de diseñar sistemas que resuelvan problemas clave en los sistemas distribuidos modernos.
En el próximo artículo de esta serie, escribiré sobre las pruebas y su papel en la ingeniería de software moderna.

Supongo que te gusta

Origin blog.csdn.net/jeansboy/article/details/131703187
Recomendado
Clasificación