1. Proy1.1: Servidor concurrente
El proyecto 1, pretende convertir un servidor web en concurrente y distribuido, a partir de un código heredado serial (legacy code). Involucra tanto concurrencia de tareas como paralelismo de datos. Se realiza en equipos de máximo cuatro personas.
1.1. Repositorio y archivos
La solución se entrega en un repositorio privado que cada equipo de estudiantes debe crear para ambos proyectos. Agregue a su docente al repositorio. Todos los miembros del equipo deben demostrar un nivel similar de participación en el desarrollo de la solución. Cada participante debe asegurarse de hacer commits frecuentes, que impliquen un aporte, con mensajes breves pero descriptivos. Debe haber una adecuada distribución de la carga de trabajo, y no desequilibrios como que "un miembro del equipo sólo documentó el código".
El código de la rama principal representará el servidor web en producción. No se deberían hacer commits directamente a ella. En su lugar, por cada requerimiento identificado, creen una rama (branch). Se recomienda el uso de muchas ramas cortas. Una vez que el requerimiento esté implementado, se recomienda asignar otro u otros miembros revisen el código y unan (merge) la rama a la principal a través de un pull request (también llamado merge request). De esta manera se estará emulando el flujo de trabajo habitual en la industria, que además tiene beneficios importantes al revisar código de otras personas.
La rama principal siempre debe compilar y correr. Un merge o commit en esta rama nunca "romper el build", por ejemplo con un error de compilación. Para las revisiones de avances se considerarán los commits en la rama principal (master) y uno de ellos rotulado (git tag
) por el equipo con avance11
(o en inglés milestone11
) será usado como referencia. En caso de que surjan, este historial de control de versiones se usará como evidencia para la resolución de reclamos y conflictos.
Dentro del repositorio creen una carpeta exclusiva para cada proyecto, llamada carpeta del proyecto. Cada proyecto debe tener su documento de análisis (readme
). El repositorio debe tener también su propio readme
ubicado en la raíz del repositorio. El readme
del repositorio debe indicar el propósito de del repositorio (sobre proyectos del curso), tener enlaces hacia las carpetas de los dos proyectos que facilite su navegabilidad junto con un párrafo que resuma el propósito del proyecto, y listar los nombres de los miembros del equipo.
El código heredado es parte del ejemplo patrón productor-consumidor. Sigue la estructura de archivos y directorios característica de un proyecto de Unix, descrita en el enunciado de la tarea01, y debe ser mantenida por los miembros del equipo. No se recomienda agregar el código de simulación de red (carpeta simulation/
) a su proyecto01 en el repositorio.
Las clases, métodos, y tipos de datos, deben estar documentados con Doxygen
. La salida de Doxygen
debe estar en una carpeta doc/
y ésta no se debe agregar a control de versiones. Documentar el código no trivial en los cuerpos de las subrutinas. Su solución debe ser modular, dividiendo el código en subrutinas, clases, y archivos. El código debe tener un estilo consistente y no generar diagnósticos del linter (cpplint
).
1.2. Análisis
El objetivo del proyecto es desarrollar un servidor web concurrente y distribuido que permita a sus visitantes interaccionar con dos aplicaciones web. Una aplicación provee la factorización prima de números. La otra provee sumas de Goldbach. En este primer avance el servidor web debe ser concurrente, y la aplicación web debe ser orientada a objetos.
En este proyecto el equipo trabaja con una base de código existente, lo cual es el escenario más probable que enfrente la persona profesional al integrarse a la fuerza laboral y para ello reciba una inducción de parte de otras personas profesionales. Una inducción a este código está disponible en el formato de video en la sección Servidor web concurrente (proyecto 1) del material del curso.
Los miembros del equipo elaboran el documento de análisis, tal como se estipula en la carta al estudiante, con el fin de emular la práctica en la industria donde las personas profesionales en informática recaban y documentan los requerimientos de los clientes y no estos últimos. Los miembros del equipo deben recabar los requerimientos a partir de los insumos provistos (este documento y las grabaciones de inducción) y anotarlos en un archivo readme
en la carpeta para el proyecto 01 en el repositorio de control de versiones.
Al leer el readme.ext
debe quedar claro a una persona lectora ajena al proyecto, qué problema éste resuelve. Además es sumamente importante un manual de uso, que incluye cómo compilar el servidor web y cómo correrlo. Explique los argumentos en línea de comandos que recibe, ejemplos de ejecuciones, y cómo detenerlo tanto con Ctrl+C como el comando kill
. No es necesario duplicar los nombres de los integrantes del equipo si son los mismos del readme
del repositorio.
Se recomienda que un miembro del equipo haga su análisis en el readme
y lo suba al repositorio, luego otro integrante enriquece el documento conforme realiza su análisis y así hasta el último miembro del equipo. También pueden trabajar simultáneamente todos los miembros del equipo en un documento compartido que luego suben a control de versiones. Recuerde: no escriba para cumplir una asignación de un curso, ni para el docente, no es este el fin. Escriba para perfeccionar sus habilidades de comunicación, que le permitirán junto con sus habilidades técnicas subir en la escala salarial. Una vez completado el documento de análisis se recomienda probarlo pidiéndole a personas ajenas al proyecto leerlo y pedirles que les expliquen de vuelta lo que comprendieron.
Una vez implementada la aplicación de factorización o de sumas de Goldbach, agreguen al inicio del readme.ext
una captura de pantalla de una consulta hecha a través de un navegador. Agregar una imagen al inicio es una práctica común en los repositorios de control de versiones que provee un efecto más llamativo y profesional del proyecto. Guarde la o las imágenes en una subcarpeta img/
y no en la raíz del proyecto.
1.3. Diseño
Esta primera entrega consiste en modificar el 'código heredado' serial para que el servidor web se comporte de manera concurrente, es decir, que sea capaz de aceptar y atender múltiples conexiones de manera concurrente. El servidor web debe recibir solicitudes de usuarios las cuales consisten de listas de números para los cuales una aplicación realizará su factorización prima o sumas de Goldbach.
Su solución debe tener un buen diseño concurrente expresado mediante un diagrama de flujo de datos, es decir, una cadena de producción que resalte la concurrencia y la aplicación del patrón productor-consumidor. Puede tomar el siguiente diseño inicial como punto de partida (clic en la imagen para abrirla). En este diseño las elipses representan hilos de ejecución, los rectángulos nodos en colas, los hilos dentro de un rectángulo indican un equipo, y los rectángulos punteados indican objetos.
El diseño de la Figura 1 es inicial, incompleto. Por el contrario, su diseño debe ser concreto, e incluir al menos los siguientes detalles:
-
Mostrar el recorrido completo de datos concretos (sockets, solicitudes, y números) por el flujo. Los datos inician con dos conexiones (dos sockets). Puede suponer que el socket 1 es la conexión del cliente 1 y el socket 2 es la conexión con el cliente 2.
-
En al menos uno de esos sockets se piden al menos dos solicitudes HTTP. Puede representar una solicitud con el texto
F n1 … nN
oG n1 … nN
, dondeF
indica que es una solicitud de factorización yG
que es una solicitud de sumas de Goldbach. Es seguida por una cantidad arbitraria de números enteros separados por espacios en blanco. Con tres solicitudes HTTP es suficiente para el diagrama. -
Al menos una solicitud HTTP debe solicitar al menos dos números a factorizar y al menos una solicitud HTTP debe solicitar al menos dos números para sumas de Goldbach. Al menos uno de los números a factorizar debe tener al menos dos potencias primas. Al menos uno de los números de Goldbach debe ser par y otro impar. Al menos uno de los números genera al menos dos sumas de Goldbach. Estos datos se deben representar en las aplicaciones para el avance 1.1, y luego en nodos de colas para los avances 1.2 y 1.3.
-
El diagrama inicial presenta sólo la aplicación de factorización y su modelo. Deben agregarse los objetos para la aplicación web de Goldbach y su modelo. Deben además redirigirse los datos correspondientes a cada aplicación.
-
Use elementos gráficos consistentemente. Provea colores, formas, patrones que agreguen significado al diseño. Provea una Simbología o Leyenda al diagrama, explicando el significado de cada elemento gráfico. Por ejemplo, es importante distinguir las flechas que indican flujo de datos del patrón productor-consumidor (entre un hilo y otro), de flechas que sólo son invocaciones de métodos dentro del mismo hilo de ejecución.
-
Las relaciones que no utilizan simbología estándar UML, debe indicarse su nombre. Por ejemplo, una relación de una cola hacia un hilo probablemente llevará el nombre de consume(), mientras que una relación hacia el objeto modelo será el nombre del método con que ese objeto encuentra la factorización prima o las sumas de Goldbach.
-
En la cadena de producción intervienen instancias (objetos) de clases. Las clases nuevas que ustedes agregan al proyecto o modifican del código heredado, se deben representar en UML. Ubique estas clases en los alrededores del diagrama de flujo de datos para facilitar la lectura del diseño y conéctelas con líneas punteadas a alguna de las instancias del flujo de datos. En las instancias (objetos) dentro del flujo de datos no es necesario indicar los nombres de los atributos, basta con indicar sólo los valores de los campos en el mismo orden que están en la clase UML.
-
Los métodos más relevantes para la concurrencia en la cadena de producción corresponden a los métodos de correr los hilos, detenerlos, producir y consumir. El código a ejecutar en estos métodos pueden diseñarse en pseudocódigo como se ha visto en las lecciones del curso. El diseño debe centrarse en la lógica concurrente y no los detalles algorítmicos seriales, tales las factorizaciones o encontrar sumas de Goldbach, establecimiento de conexiones de red entre clientes y servidor, entre otros.
Los diseños deben guardarse en la carpeta design/
dentro de su proyecto 1. Las imágenes deben exportarse a formato SVG (preferible) o PNG. Dentro de esta carpeta agregue el documento de diseño design/readme.ext
, cuya extensión depende del formato (Markdown, AsciiDoc, LaTex), donde se explique de forma resumida su diseño e incruste las imágenes (diagramas) y listados de código (algoritmos).
Al leer el documento de diseño, una persona desarrolladora tendrá una comprensión de alto nivel de la solución ("una visión del bosque"). Es muy útil para la fase de implementación o para futuros miembros de un equipo con el fin de poder comprender el diseño de la solución. Recuerde: no diseñe para cumplir con una asignación de un curso, ni para su docente (esta persona no lo necesita, pues no es un cliente real). Ese no es el fin. Su propósito es desarrollar sus habilidades para resolver problemas con artefactos baratos y fáciles de modificar (diseño) antes de implementar, y recibir realimentación que le ayude en el futuro cuando sí esté construyendo soluciones para sus clientes reales.
En el documento de análisis para el proyecto, agregue una sección de diseño y un enlace el documento de diseño. Esto permitirá la navegabilidad desde la raíz del repositorio al proyecto 01, y de éste hacia su diseño.
Probablemente notará que en el diseño de la Figura 1 no hay una cadena de producción concurrente completa. Es una observación correcta. Al final de este avance el equipo tendrá la oportunidad de mejorar este diseño, y en el avance siguiente (proy1.2
) la oportunidad de implementar esta mejora haciendo concurrente la aplicación web (la parte derecha de la figura).
1.4. Servidor web concurrente
El 'código heredado' implementa un servidor web que se comporta de manera serial. Como es sabido, los servidores (o servicios) de software son de naturaleza concurrente (ej.: servidores de bases de datos, de correo, enrutadores de red, y hasta su sistema operativo). El reto del equipo es convertir a este servidor web serial en uno concurrente para que naturalmente pueda atender varias conexiones de clientes al mismo tiempo. Nótese que esta mejora sólo altera el código del servidor (parte izquierda de la Figura 1) y no de las aplicaciones web (parte derecha de la Figura 1). Estas se ven beneficiadas por la concurrencia del servidor aunque las aplicaciones sigan siendo seriales.
Al iniciar, el servidor web recibirá tres argumentos de línea de comandos, todos opcionales, y el equipo podría agregar otros:
-
El puerto en el que el servidor web esperará por conexiones de clientes.
-
La cantidad máxima de conexiones que el servidor puede atender concurrentemente.
-
La capacidad máxima de la cola de conexiones.
El segundo argumento se puede llamar max_connections
e indica el número máximo de conexiones de clientes que el servidor web debe permitir de manera concurrente. Si este número no se indica, se puede suponer tantas conexiones como CPUs hay disponibles en la máquina. Una vez que el servidor inicia, se debe crear este número de hilos que se encargarán de atender conexiones. Estos hilos serán instancias de una clase cuyo nombre se sugiere sea HttpConnectionHandler
y se almacenará en la carpeta src/http/
(ver Figura 1).
Para poner los HttpConnectionHandler
en funcionamiento, se deben agregar a la cadena de producción. Esta cadena no existe, puesto que el servidor es serial. Se debe modificar la clase HttpServer
ubicada en la carpeta http/
del código base dado para implementar un patrón productor consumidor. El productor será el hilo principal que produce conexiones aceptadas con los clientes, llamadas sockets que corresponden a objetos de la clase Socket
y se deben poner copias de estos objetos en la cola. Los consumidores de estas conexiones serán los HttpConnectionHandler
.
La cola de conexiones (sockets) entre el servidor web y los HttpConnectionHandler
debe ser un buffer acotado, para reducir la probabilidad de que el servidor acepte muchas conexiones que no podrá atender por saturación. El tercer argumento de línea de comandos indica la capacidad de esta cola, que debe ser un número positivo (1 o más). Si no se provee un tercar argumento, suponga el número máximo en que se puede inicializar un semáforo POSIX.
El código en la carpeta prodcons/
servirá de base para crear nuevas clases de productores y consumidores. Puede examinar como ejemplo el programa simulation/
que también se describió durante las lecciones del curso. El código brindado tiene además documentación y anotaciones de tipo TODO:
que proporcionan una guía sobre lo que se debe implementar.
En el código dado, el servidor serial acepta una conexión, de ella extrae una solicitud HTTP, la responde, cierra la conexión, y continúa con la próxima conexión, y así indefinidamente. En la versión concurrente, este diseño debe iniciar una pequeña cadena de producción como la siguiente:
-
El objeto
HttpServer
en el hilo principal sólo debe aceptar solicitudes y ponerlas en cola, nada más, no atenderlas. -
Los hilos/objetos
HttpConnectionHandler
consumen las conexiones de red con los clientes. -
Los hilos/objetos
HttpConnectionHandler
extraen todas las solicitudes del cliente (no sólo la primera). Por cada solicitud ensambla los objetosHttpRequest
yHttpResponse
. -
El
HttpConnectionHandler
es un objeto de la capa del servidor, no de aplicación. Por cada solicitud que extraiga, no debe atenderla directamente, sino que delega en las aplicaciones web. Es decir, elHttpConnectionHandler
recorrerá todas las aplicaciones web en busca de aquella encargada de atender la solicitud (si la hay). Para este avance la aplicación web es sólo un objeto, no un hilo de ejecución. Por lo tanto, la aplicación web es un objeto que recibirá la solicitud (los objetosHttpRequest
yHttpResponse
) y hará su trabajo del dominio (ej.: calculará la factorización prima o las sumas de Goldbach de cada número solicitado) en el mismo hilo de ejecución delHttpConnectionHandler
. -
Después de obtener el resultado del dominio (la descomposición prima o las sumas de Goldbach de los números de una solicitud), la aplicación web responderá directamente al cliente usando el socket, pero sin cerrarlo (a menos de que el cliente use el protocolo
HTTP/1.0
). -
Una vez que la aplicación web responde al cliente, el
HttpConnectionHandler
retorna al ciclo de solicitudes para atender más peticiones de factorización que el cliente pueda tener (paso 3). Cuando el cliente no tenga más solicitudes, cerrará la conexión (socket) y elHttpConnectionHandler
saldrá del ciclo de atender solicitudes y regresará a la cola para consumir otra conexión, si la hay (paso 2).
El ciclo indicado en los pasos anteriores se detendrá cuando el HttpConnectionHandler
extraiga una condición de parada de la cola, representada por un Socket
inválido creado con el constructor por defecto. Para ello, el servidor web tendrá que colocar en cola tantas condiciones de parada como HttpConnectionHandler
se hayan creado, cuando reciba la señal para detenerse, como un Ctrl+C, explicado más adelante.
1.5. Aplicación web orientada a objetos
En este primer avance no se requiere que la aplicación web (la que resuelve las factorizaciones primas o sumas de Goldbach) sea concurrente. La aplicación web debe estar implementada en el paradigma de programación orientada a objetos y siguiendo el patrón modelo-vista-controlador (MVC), en coherencia con el resto de la base de código provisto. De acuerdo a este patrón, el código del servidor web (controlador), la aplicación web (vista), y el cálculo del dominio del problema (modelo), deben estar en objetos separados. Se recomienda una separación en al menos las dos siguientes clases de C++:
-
La aplicación web. Tiene principalmente responsabilidades de una vista ("lo que ve el usuario"). Se le puede dar un nombre como
FactWebApp
oGoldbachWebApp
. Se encarga de recibir las solicitudes HTTP que emite el cliente, extraer los números del cuerpo de la solicitud, pasarlos al modelo (explicada en el siguiente punto), esperar el resultado que retorna el modelo, ensamblar y despachar las respuestas HTTP que serán las que verán los usuarios. Nótese que ni el servidor web ni la aplicación web deben realizar cálculo alguno relativo al dominio, pues de esto se encarga el objeto modelo. -
El modelo. Tiene estrictamente responsabilidades de un modelo, por lo tanto, no debe saber nada de web ni redes de computadoras. Se le puede dar un nombre como
PrimeFactModel
,PrimeFactSolver
,PrimeFactCalculator
,FactorizationCalculator
,GoldbachCalculator
, etc. Se encarga de aplicar la lógica del dominio a números representados en aritmética de precisión fija (o si se quiere, precisión arbitraria). Esta clase trabaja con estructuras de datos en memoria. Por ejemplo, recibe colecciones de números y retorna colecciones de números factorizados o sumas de Goldbach. No debe producir resultados en formato HTML ni texto formateado. Recuerde que por ser un modelo, esta clase debería poderse reutilizar en otras aplicaciones (ej.: de dispositivos móviles) sin cambio alguno.
Nota: Puede ocurrir que entre la aplicación de factorización prima y la aplicación de sumas de Goldbach haya código común. No genere redundancia de código. Utilice mecanismos de reutilización de código, como es debido.
Su solución debe realizar los cálculos correctamente. Es decir, la aplicación web responde con una página que tiene las factorizaciones primas o sumas de Goldbach. Para efectos de este proyecto, el servidor web produce un único mensaje de respuesta por cada solicitud HTTP del cliente, la cual contiene todos los resultados de los números solicitados. Esto implica que si el cálculo de las factorizaciones o sumas toma tiempo al servidor, el cliente deberá esperar. Si el cálculo tarda más que la duración máxima de inactividad de una conexión de red (timeout), cuando la aplicación haya concluido, no podrá responder al cliente porque éste se habrá desconectado ya. Para efectos de esta evaluación, no es necesario corregir este error.
La aplicación web debe permitir ingresar listas de números separados por comas, positivos o negativos, tanto en el URI (barra de direcciones del navegador), como el formulario web. La aplicación valida entradas. Números mal formados, fuera de rango, o entradas mal intencionadas son reportadas con mensajes de error en la página web resultado, en lugar de caerse o producir resultados con números incorrectos.
1.6. Finalización del servidor
Al finalizar el servidor web (con Ctrl+C o el comando kill
), éste debe reaccionar a la señal y hacer la limpieza debida. Por ejemplo, detenerse de aceptar más conexiones de clientes, avisar a los hilos secundarios para que terminen su ejecución, esperar que terminen el trabajo pendiente, y finalmente liberar recursos como estructuras de datos en memoria dinámica, archivos, u otros, sin provocar fugas de memoria ni otros problemas. Para lograr este efecto se recomienda realizar lo siguiente.
-
Convertir la clase
HttpServer
en un objeto singleton. Este patrón de software se aplica a aquellas clases que sólo pueden tener una única instancia (objeto) dentro de un proceso. Sus constructor es privado, no permite copias, y un método permite obtener acceso a esa única instancia. La claseLog
en el código heredado ya implementa este patrón. -
Registrar un procedimiento libre o método estático (preferible), técnicamente conocido como signal handler, que se invoque cuando el proceso reciba la señal
SIGINT
(Ctrl+C) oSIGTERM
(comandokill PID
). Véase la función de bibliotecasignal()
de la biblioteca estándar de C. -
Cuando el signal handler se invoque, consigue la instancia del servidor web (singleton) y le solicita que se detenga, invocando su método
stop()
. Sólo se debe hacer esta invocación y nada más, porque el sistema operativo puede llamar al signal handler en cualquiera de los hilos del proceso, y no necesariamente en el hilo principal. El propósito de llamar astop()
es provocar que el hilo principal se detenga de esperar por nuevas conexiones de clientes y comience el proceso de finalización del servidor. -
El método
HttpServer::stop()
, que podría ser invocado en cualquier hilo y no necesariamente en el hilo principal, invoca el método heredadostopListening()
, el cual cierra de golpe el socket de aceptar conexiones. Esto provoca que elHttpServer
corriendo en el hilo principal falle al tratar de conseguir el próximo cliente, lance una excepción, retorne de la invocaciónthis→acceptAllConnections()
e ingrese a la seccióncatch
en el métodostart()
. A partir de este punto en adelante del métodostart()
invoque métodos para detener la cadena de producción (enviar condiciones de parada), liberar estructuras de datos, y al servidor, sabiendo que este código se ejecutará desde el hilo principal.
Para poder probar que su servidor finaliza adecuadamente y libera los recursos, puede correr el ejecutable instrumentalizado (ej.: asan
, tsan
, …) en forma interactiva y presionar Ctrl+C. Si corre el ejecutable en un ambiente simulado (ej.: memcheck
, helgrind
), abra otra terminal, obtenga el identificador de proceso (PID, Process ID) con el comando ps -eu
(Valgrind lo reporta en la forma ===PID===
) y detenga el proceso con el comando kill PID
donde PID
se reemplaza por el número de proceso que obtuvo con ps
o Valgrind. Al detener su proceso, no se deben obtener reportes de errores ni fugas de memoria.
1.7. Pruebas de concurrencia
Una vez que se haya implementado, es necesario probar que el servidor web sea concurrente. Una prueba sencilla es crear dos conexiones, por ejemplo, con dos ventanas de un navegador. Solicitar primero en una ventana uno o varios números lentos de procesar, que tarden varios segundos. Mientras esa solicitud aún no haya terminado, en la segunda ventana del navegador solicitar uno o varios números pequeños. Si el servidor responde la segunda solicitud antes de que la primera finalice, es un indicio de que es concurrente. Si por el contrario, la segunda solicitud es atendida hasta después que la primera finalice, indica que el servidor web está serializando las conexiones.
Para determinar más sistemáticamente que el servidor web realmente atiende varias conexiones de forma concurrente, se usará una herramienta de pruebas de estrés (stress testing), como httperf. Idealmente se correrán las pruebas en el laboratorio de computadoras del curso. Se ejecuta el servidor web en una máquina. Primero obtenga la dirección IP local de esa máquina con el comando ip addr
. Se recomienda también tener abierto un administrador de procesos para monitorear el consumo de CPU. Luego corra el servidor web. Por ejemplo, el siguiente comando corre el servidor en el puerto 8080, para atender un máximo de 30 conexiones concurrentes y un máximo de 70 conexiones en espera:
$ ip addr | grep -E '\b\d+(\.\d+){3}\b'
$ bin/webserv 8080 30 70
En al menos dos computadoras del laboratorio, inicie en paralelo la prueba de estrés, una de la aplicación de factorización y otra de la aplicación de Goldbach. El siguiente comando trata de establecer un total de 200 conexiones (--num-conns
) con el servidor que está escuchando en el puerto 8080 (--port
) en la máquina con dirección IP 10.137.1.117 (--server
), a una tasa de 50 conexiones cada segundo (--rate
). Es decir, en el primer segundo tratará de establecer las primeras 50 conexiones, en el siguiente segundo enviará las solicitudes de conexión 51 a 100, y así, hasta que en el cuarto segundo solicite las 200 conexiones.
$ httperf --server 10.137.1.117 --port 8080 --num-conns 200 --rate 50 --num-call 3 --uri /fact?number=123 --timeout 1
Sin embargo, el servidor atenderá las primeras 30 solicitudes de conexión de forma concurrente, las siguientes 70 que vayan apareciendo las aceptará pero las pondrá en cola. Las restantes 100 solicitudes de conexión quedarán pendientes hasta que se libere espacio en la cola, o transcurra el segundo indicado en el argumento --timeout
, lo que ocurra primero.
Una vez que el servidor web acepte cualquiera de las 200 conexiones, httperf
enviará de inmediato 3 solicitudes HTTP (--num-call
) por esa conexión. Cada solicitud pedirá el recurso identificado con el URI http://10.137.1.117:8080/fact?number=123
, el cual usted deberá ajustar a lo que su aplicación web espera. Es recomendado obtener este URI de una ejecución con el navegador. Si el servidor web no le llegara a responder una de estas solicitudes en 1 segundo (--timeout
), httperf
lo tomará como una no-respuesta y lo reportará en las estadísticas. En el el sitio del proyecto httperf se explica con detalle cómo interpretar su salida.
Para la versión serial (el código dado) del servidor web, se tendrán respuestas positivas sólo si se realiza una única solicitud HTTP por conexión, es decir --num-call 1
, ya que esta versión atiende una solicitud y cierra la conexión con el cliente, ignorando las demás solicitudes que éste realice. Para este avance, una vez que se haya implementado concurrencia en el servidor web, se debería tener respuesta de todas las solicitudes que el cliente haga en la misma conexión con el argumento --num-call
mayor a 1.
El Makefile
provisto facilita la ejecución de pruebas de estrés con la regla make stress
. El siguiente ejemplo replica la prueba de estrés presentada anteriormente:
$ make stress HOST=10.137.1.117 NUMS=123 TIMEOUT=1
Haga varias pruebas de estrés en el laboratorio de computadoras del curso. Ajuste los parámetros de httperf
de tal manera que su servidor pueda apenas responder el 100% de las solicitudes en el tiempo establecido. Guarde esta consulta en las variables del Makefile
provisto, pues servirán para pruebas durante la revisión de éste y futuros avances.
Agregue a su documento de análisis una subsección de "Pruebas de concurrencia" en la sección de "Manual de usuario". Escriba los comandos para correr httperf
, describa sus argumentos, y una explicación corta de la salida esperada.
1.8. Diseño del siguiente avance
Para el segundo avance, la aplicación web debe ser concurrente con el fin de que aproveche de forma más balanceada los recursos de paralelismo del hardware en el que corre. Como se indica en el respectivo enunciado (proy1.2), el equipo debe crear un diseño que logre satisfacer esta necesidad. En la revisión de este primer avance, el equipo puede –y se recomienda– traer preparado su diseño del avance siguiente, para obtener realimentación antes de iniciar la programación del mismo.
1.9. Evaluación
La revisión de avance será mediante una sesión interactiva en el laboratorio del curso. Los miembros del equipo primero realizan una presentación del producto y corren las pruebas de validez y de rendimiento. Luego se revisan algunas regiones de código elegidas por sus docentes. Para esta etapa se debe mostrar en pantalla tanto el diseño como el código al mismo tiempo. Todos los miembros del equipo deben participar durante la sesión. Se dará crédito a la evidencia presentada en cada aspecto de este enunciado y se distribuye en los siguientes rubros.
-
[5%] Buen uso y aporte equilibrado del repositorio (commits, ignores, tags) y directorios.
-
[10%] Análisis:
README.md
, problema, integrantes, manual de usuario, captura, navegabilidad. -
[10%] Diseño concurrente: documento de diseño, flujo de datos, clases en UML, pseudocódigo.
-
[25%] Servidor web concurrente mediante el patrón productor consumidor.
-
[15%] Aplicaciones web orientadas a objetos y sus modelos.
-
[15%] Finalización correcta del servidor. Buen uso de la memoria y de la concurrencia (Sanitizers y Valgrind).
-
[10%] Pruebas de concurrencia (
httpef
). -
[5%] Modularización en subrutinas y archivos (
.hpp
,.cpp
). Apego a una convención de estilos (cpplint
). -
[5%] Documentación de interfaces (
doxygen
) e implementaciones (algoritmos).
Recuerde que en todos los rubros se evalúan las buenas prácticas de programación.
2. Proy1.2: Aplicaciones concurrentes
En el avance 1.1 el servidor web pasó de ser serial a concurrente, mientras que las aplicaciones eran seriales. En este avance, sus aplicaciones web serán también concurrentes para hacer un mejor uso del hardware en que corre el servidor.
2.1. Repositorio y archivos
Nota: Para optar por la mejora en la calificación del avance 01 tras la revisión, el equipo debe crear los issues y aplicar las correcciones siguiendo los mismos lineamientos para este aspecto indicados en las tareas individuales. Al final de las correcciones marcar el último commit con la etiqueta avance11fix
(o en inglés milestone11fix
).
Los cambios en el código para este segundo avance se deben hacer in place. Es decir, se realizan sobre la misma carpeta del avance 01 del repositorio de control de versiones. Los avances no son productos diferentes, sino, ir mejorando y madurando un producto, como ocurriría con software para un cliente real.
Los lineamientos sobre el trabajo en el repositorio y entrega de archivos estipulados en el avance 1, se aplican para este avance. Esto incluye igual participación de los miembros del equipo, políticas de commits y branches, estructura de archivos y carpetas, documentación con Doxygen, modularización, y estilo de código (cpplint
).
2.2. Análisis
En el avance 1 se construyó un servidor web concurrente que podía atender una cantidad arbitraria de clientes de forma simultánea, los cuales solicitan cálculos sobre el dominio (ej.: factorización prima o sumas de Goldbach de números enteros), que son calculadas por una aplicación web serial. Sin embargo, este diseño no es óptimo. Por ejemplo, si en una máquina con 8 núcleos de procesador se levanta un servidor web capaz de atender 1000 conexiones concurrentes, podrían ocurrir escenarios como los siguientes:
-
Si en efecto se conectan 1000 usuarios que solicitan una cantidad considerable de trabajo cada uno, el servidor tendrá 1000 hilos (ó "1000 calculadoras") compitiendo por los 8 núcleos, lo que crea una gran competencia y cambio de contexto entre ellos, además de mayor consumo de memoria principal.
-
Si se conectan varios usuarios, uno de ellos hace una solicitud que requiere una cantidad considerable de trabajo, digamos media hora, y los demás hacen solicitudes livianas. Los hilos de ejecución que atienden las solicitudes livianas terminarán rápido, mientras que el hilo que se encargue de la solicitud pesada se quedará trabajando por tiempo prolongado en un núcleo de CPU, mientras las restantes siete CPUs estarán desaprovechadas. Peor aún, cuando el hilo sobrecargado finalice, es muy probable que su cliente se haya desconectado por superar el límite de espera de red. Por más que el cliente reintente, el servidor será incapaz de responderle a tiempo aunque disponga de los recursos para lograrlo.
En este avance se creará una solución que haga mejor uso de los recursos de la máquina, al hacer concurrentes las aplicaciones web que realizan la lógica del dominio. Se deberá crear tantos hilos para los cálculos del dominio (calculadoras) como CPUs hay disponibles en el sistema, indiferentemente de la cantidad de aplicaciones.
2.3. Diseño
En esta segunda entrega, modifique sus aplicaciones web para que no hagan un consumo agresivo o desequilibrado de los recursos de la máquina. Esta modificación consiste en hacer las aplicaciones web concurrentes, para que puedan controlar la cantidad de hilos que realmente consumen CPU. Estos hilos que realizan los cálculos del dominio, son de paralelismo de datos. La cantidad de estos hilos debería coincidir con la cantidad de núcleos de procesador disponibles en el sistema. El equipo de HttpConnectionHandler
no se consideran hilos de paralelismo de datos, si no de concurrencia de tareas, porque la mayoría del tiempo pasan bloqueados esperando solicitudes de sus clientes.
El equipo de desarrollo debe actualizar su diseño para que cumpla el propósito anterior como se detalla a continuación. El diagrama de flujo de datos hecho en el avance anterior debe actualizarse para reflejar una optimizada cadena de producción completa que aplique el patrón productor-consumidor. El diseño de la Figura 1 es incompleto, pero servirá de base para ejemplificar algunos de los cambios que el equipo de desarrollo debe considerar.
El diseño del avance anterior creaba un hilo HttpConnectionHandler
que realizaba varios asuntos, entre ellos el cálculo del dominio (factorización o sumas de Goldbach) por cada conexión cliente. En este avance, su diseño debe crear un equipo de tantos hilos como CPUs hay disponibles en el sistema para estos cálculos del dominio. Se les va a llamar calculadoras por sencillez en este documento. El rol de este equipo de hilos debe estar claramente definido en su diagrama de acuerdo al patrón concurrente visto en el curso (productores, consumidores, ensambladores, o repartidores).
El diseño debe contemplar cómo se comunican los hilos calculadores con otros hilos de la cadena de producción. Antes de las calculadoras se encuentran los hilos HttpConnectionHandler
. Sin embargo la comunicación entre ellos no es trivial dado que los HttpConnectionHandler
forman parte del servidor web, los hilos calculadores son parte de la aplicación web, y el servidor web debe trabajar con una cantidad arbitraria de aplicaciones web. Por esto se sugiere la siguiente cadena de producción.
-
Al igual que el avance 1, el servidor sólo acepta solicitudes y las pone en cola, nada más.
-
Los
HttpConnectionHandler
consumen las conexiones. De una conexión, extraen todas las solicitudes HTTP del cliente (no sólo la primera). Sin embargo, en este avance 2, losHttpConnectionHandler
no atienden estas solicitudes HTTP, sino que las ponen en otra cola. La capacidad de esta cola puede controlarse con un argumento de la línea de comandos. -
Un nuevo hilo consume las solicitudes HTTP de la cola anterior. Por cada solicitud determina a cuál aplicación web va dirigida y la pone en la cola correspondiente. El equipo de desarrollo debe determinar el rol del patrón productor-consumidor que mejor se ajusta a esta responsabilidad. Además debe modificar el proceso de registro de las aplicaciones web ante el servidor web para que las aplicaciones puedan proveer sus criterios que decidan cuando una solicitud va dirigida a ellas.
-
Modificar las aplicaciones web para que sean parte de la cadena de producción. Es decir, las aplicaciones web (clase
HttpApp
) serían hilos que toman un rol del patrón productor-consumidor. Las aplicaciones consumen las solicitudes HTTP que son dirigidas hacia ellas. -
Cada vez que una aplicación web consuma una solicitud HTTP, extrae los números en el cuerpo de la solicitud, pero no calcula el resultado, sino que coloca estos números en cola para enviarlos al equipo de hilos calculadores. Esto crea una separación entre la vista concurrente (la aplicación web) y el modelo concurrente (los factorizadores o sumas de Goldbach). Aunque parece sencillo, el equipo de desarrollo debe diseñar con cuidado y detalle este paso, dado que implica decisiones de descomposición, mapeo, y trazabilidad que tienen un impacto en el rendimiento y deben reflejarse en el diseño de los nodos de varias colas de la cadena de producción. Más adelante se habla sobre este aspecto.
-
Los hilos calculadores también tienen un rol del patrón productor-consumidor. Cada vez que un hilo calculador extrae trabajo de la cola, invocará un método de la clase modelo correspondiente que realiza el cálculo (la factorización prima o sumas de Goldbach). El hilo calculador no necesita saber cuál de estos cálculos está realizando, sólo es necesario que ese hilo ejecute un método que puede ser virtual puro (u otro diseño reutilizable). El resultado del cálculo es puesto en otra cola.
-
Un hilo "empaquetador" recibe los resultados de los cálculos y revisa si la solicitud a la que pertenece está completa. Es decir, si todos los números que el cliente indicó en la solicitud HTTP han sido calculados. Si la solicitud está completa, sólo la pone en cola.
-
Un hilo "despachador" recibe las solicitudes que están completas, ensambla el mensaje de respuesta HTTP y lo envía a su respectivo solicitante (navegador).
El diseño debe reflejar el recorrido de los datos de ejemplo concretos escogidos en el avance 1, en los nodos de las colas a lo largo de la cadena de producción. Recuerde indicar el diseño en UML de las clases propias, en especial para los nodos de cada cola. La elección de los campos de cada nodo deben ser el resultado de una eficiente descomposición y mapeo, y una correcta trazabilidad.
La capacidad de la cola entre los HttpConnectionHandler
y las calculadoras puede ser controlada al iniciar el servidor web. Permita proveer un cuarto argumento opcional de línea de comandos que indique la capacidad de esta cola. Si este argumento no se provee, suponga el máximo valor entero con el que se puede inicializar un semáforo. El resto de colas que continúan la cadena de producción no estarán acotadas.
El diseño debe balancear la carga de trabajo lo más equitativamente posible entre las CPUs disponibles en el sistema. Por tanto, deberán tomar decisiones de descomposición y mapeo que afectan a la naturaleza de las solicitudes. Es decir, las solicitudes serán descompuestas en unidades más finas de trabajo. Esto provoca que los datos que hayan en una cola tengan diferente granularidad que los datos de otras colas. Por lo tanto, no habrá una asociación 1 a 1 entre ellos, por lo que el equipo deberá decidir algún mecanismo para mantener la trazabilidad.
La trazabilidad consiste en agregar datos a los nodos de las colas que permitan ensamblar luego las respuestas que serán enviadas a los clientes. La cantidad de datos a agregar debe ser la mínima y el mecanismo escogido debe ser correcto y eficiente. Recuerde que se debe reflejar la trazabilidad a través de los ejemplos a lo largo de la cadena de producción.
Al igual que el avance 1, el diseño concurrente consta del flujo de datos, diagramas de clase UML, y pseudocódigo. El pseudocódigo sirve para detallar las acciones que se realizan en la creación, producción, y consumo a lo largo de la cadena de producción. El texto del documento de diseño debe actualizarse junto con los diagramas como el pseudocódigo anterior.
2.4. Implementación
Se debe aplicar el diseño de la aplicación web concurrente siguiendo los patrones sugeridos (productor-consumidor, modelo-vista-controlador, y singleton) al código solución en C++. La implementación de la solución debe apegarse al diseño elaborado por el equipo en la sección anterior. Para facilitar la comprensión del código durante la revisión, se solicitará mantener el diseño visible mientras se recorre la cadena de producción en el código fuente.
Al igual que el avance anterior, la aplicación debe permite ingresar listas de números separados por comas, positivos o negativos, tanto en el URI (barra de direcciones del navegador), como el formulario web. Además valida entradas. Números mal formados, fuera de rango, o entradas mal intencionadas son reportados con mensajes de error (en la página web resultado), en lugar de caerse o producir resultados con números incorrectos.
Al finalizar el servidor web, interactivamente con Ctrl+C o el comando kill
, éste debe reaccionar a la señal y hacer la limpieza debida de la aplicación concurrente. Esto implica al menos, detenerse de aceptar más conexiones de clientes, avisar a los hilos secundarios para que terminen su ejecución, tanto los que atienden conexiones como los hilos calculadores, esperar que terminen el trabajo pendiente, y finalmente liberar recursos como estructuras de datos en memoria dinámica, archivos, u otros.
2.5. Pruebas
El código solución debe demostrar calidad en el uso de memoria y concurrencia a través de pruebas como Sanitizers y Valgrind. Para probar que la solución realiza un adecuado proceso de descomposición y mapeo, se pueden hacer varias consultas en paralelo. Por ejemplo, una consulta con varios números que requieren mucho procesamiento con números de diversa dificultad debería consumir todos los CPUs disponibles y responderse en un tiempo menor que si fuese atendida de forma serial como ocurría en el avance 1. Dos consultas simultáneas, primero una con números pesados más que la cantidad de CPUs disponibles y segundo otra con números rápidos de procesar, provocaría que la segunda no se atienda de inmediato.
Dado que se tiene un mejor diseño concurrente, se puede esperar una mayor tasa de respuesta ante una prueba de estrés. El equipo puede confirmarlo al incrementar los valores en los parámetros de httperf
como la cantidad de solicitudes por conexión (--num-call
) o la tasa de solicitudes por segundo (--rate
). El equipo debe hacer estos incrementos en los parámetros hasta obtener solicitudes no respondidas tanto en el código del avance 1 como el avance 2. Para volver al avance 1 se puede usar la etiqueta, por ejemplo, git checkout avance11fix
y realizar las mediciones. Para volver al último commit se puede con git checkout master
. Los resultados (argumentos de httperf
) como sus salidas, actualizarlas en la sección de "Pruebas de concurrencia" al documento de análisis del proyecto.
2.6. Diseño del siguiente avance
Para el tercer avance del proyecto 1, la aplicación web debe escalar la concurrencia y hacer los cálculos del dominio de forma distribuida, tratando de descomponer y mapear el trabajo lo más equitativamente posible entre procesos ubicados en máquinas conectadas por una red de computadoras. El equipo puede –y se recomienda– traer preparado su diseño del avance siguiente, para obtener realimentación antes de iniciar la programación del mismo.
2.7. Evaluación
La revisión de avance será mediante una sesión interactiva en el laboratorio del curso. Los miembros del equipo primero realizan una presentación del producto y corren las pruebas de validez y de rendimiento. Luego se revisan algunas regiones de código elegidas por sus docentes. Para esta etapa se debe mostrar en pantalla tanto el diseño como el código al mismo tiempo. Todos los miembros del equipo deben participar durante la sesión. Se dará crédito a la evidencia presentada en cada aspecto de este enunciado y se distribuye en los siguientes rubros.
-
[5%] Repositorio: Buen uso y aporte equilibrado del repositorio (commits, ignores, tags) y directorios.
-
[20%] Análisis: actualizar readme. Diseño concurrente: documento de diseño, flujo de datos, clases en UML, pseudocódigo.
-
[25%] Servidor: Implementación de la cadena de producción. Agrega las aplicaciones como hilos a la cadena de producción.
-
[25%] Aplicaciones: Implementa tantos hilos de paralelismo de datos como CPUs hay disponibles en el sistema. Descompone en unidades finas. Logra trazabilidad de los mensajes de las aplicaciones.
-
[10%] Finalización correcta del servidor y de las aplicaciones. Buen uso de la memoria y de la concurrencia (Sanitizers/Valgrind).
-
[5%] Pruebas de concurrencia (httperf) con ASan y TSan.
-
[5%] Estilo de código: modularización en subrutinas y archivos (.hpp, .cpp), apego a una convención (cpplint).
-
[5%] Documentación: de interfaces (doxygen) e implementaciones (algoritmos).
Recuerde que en todos los rubros se evalúan las buenas prácticas de programación.
3. Proy1.3: Aplicaciones distribuidas
Al igual que los avances previos, se aplican las mismas políticas para optar por las mejoras en el avance 2, de manejo del repositorio de control de versiones, participación de los miembros del equipo, documentación, estilo de código, etc.
En el avance 1 se construyó un servidor web concurrente que podía atender una cantidad arbitraria de clientes de forma simultánea, los cuales solicitaban obtener cálculos por una aplicación web serial, lo cual no es óptimo. En el avance 2 se re-diseñó la solución para hacer un uso más balanceado de las CPUs paralelizando la aplicación web. De esta forma los HttpConnectionHandlers
eran hilos livianos de concurrencia de tareas que esperaban solicitudes pero no las atendían, sino que las trasladaban a la aplicación web, la cual contaba con hilos de paralelismo de datos (calculadoras) que se encargaban del cálculo que consume procesamiento. Además se usó una unidad de descomposición fina para mejorar el balance de carga entre los hilos de paralelismo de datos. Esta solución logra hacer un uso eficiente de los recursos de una máquina, pero desaprovecha otras que podrían estar disponibles en una organización o un clúster.
En el avance 3 el objetivo es distribuir la aplicación web para que sea escalable, logre aprovechar varias máquinas que se tengan disponibles en una organización, y reduzca sustancialmente los tiempos de respuesta. De esta forma, si el servidor web recibe una carga sustancial de trabajo, puede responder en menor tiempo y disminuir la probabilidad de que los clientes se impacienten y se desconecten dejando trabajo residual en el servidor.
3.1. Distribución de la carga de trabajo
En este avance se deben distribuir los modelos de la aplicación web, es decir, "las calculadoras". Una parte de la aplicación web corre junto con el servidor web en el mismo proceso, mientras que las calculadoras corren en procesos separados, sean en la misma o distintas máquinas. Es como cortar la cadena de producción como se ve en la línea gruesa punteada roja del siguiente diagrama.
Los procesos que realizan cálculos, al lado derecho de la división en la Figura 3 rotulados con ProcessN
, deben iniciarse primero. Para poderse comunicar con ellos, pueden correr como servidores TCP (recomendado, véase la clase TcpServer
). Si el equipo lo prefiere, en lugar de servidores TCP, pueden ser servidores web (técnicamente se conoce como servicio web), lo que potencialmente podría reutilizar código (agregar una clase HttpClient
), pero debe tenerse cuidado de no hacer un diseño muy complejo.
El proceso con el servidor web, al lado izquierdo de la división en la Figura 3 rotulado con Process0
, lee de un archivo de configuración la ubicación de los "procesos calculadores". Estas ubicaciones son indicadas como direcciones IP y puertos. El nombre del archivo podría ser provisto como argumento de línea de comandos. Si el nombre del archivo se omite, la aplicación debe crear las calculadoras en la misma máquina, puede ser como hilos de la misma forma que se hizo en el avance 2.
Cada vez que la aplicación web recibe las solicitudes HTTP de un HttpConnectionHandler
, las descompone, y distribuye el trabajo entre las calculadoras mediante un mapeo eficiente que balancee la carga. El equipo debe idear este mapeo, y existen esquemas sencillos de implementar.
Una vez que los procesos calculadores hayan encontrado las soluciones, ponen en cola estos resultados para que sean enviados de regreso al servidor web. Aquí ocurre el mecanismo inverso: el proceso con el servidor web también debe poder recibir datos, es decir, tener un hilo servidor TCP (o un servicio web) a la espera de números procesados. En este escenario, los procesos con calculadoras tienen hilos que son clientes TCP (o su correspondientes clientes web si se usan servicios web).
El equipo debe mantener la trazabilidad de las solicitudes a lo largo de la cadena de producción distribuida, con el fin de que el proceso con el servidor web pueda reconstruir los mensajes de respuesta HTTP. Esto se debe reflejar en los diagramas de diseño en los nodos de las colas y en las clases UML.
Al tener distribución, se debería poder dar respuesta a una carga de trabajo mayor. El equipo debe confirmar este indicio al incrementar los valores de estrés de httperf
hasta el punto en que la solución no logre responder la totalidad de las solicitudes. Estos resultados se deben divulgar en el documento de análisis. Para realizar las pruebas se debe usar una red de computadoras, como el laboratorio o clúster asignado al curso.
3.2. Pruebas de estrés
Es importante que el equipo realice pruebas distribuidas en el laboratorio de computadoras días antes de la revisión. Para realizar una prueba de estrés, prepare las siguientes máquinas:
-
Una computadora que tomará el rol de servidor frontal o máster.
-
Dos computadoras que serán clientes, y por lo tanto, correrán
httperf
con los argumentos encontrados en avances previos. -
Tres computadoras o más que correrán procesos esclavos, uno compilado con ASan, otro con TSan, y los demás en modo release.
En cada computadora tener visible la línea de comandos y las gráficas del administrador de procesos, que permitan ver la actividad en las computadoras cuando se ejecuten las pruebas de estrés. Idealmente la máquina frontal debería mostrar ligero consumo de CPU, y las máquinas esclavas un alto consumo de CPU.
3.3. Evaluación
Se dará crédito por los siguientes aspectos a evaluar.
-
[5%] Buen uso y aporte equilibrado del repositorio (commits, ignores, tags) y directorios.
-
[15%] Actualizar el diseño para incluir distribución de la carga de trabajo.
-
[30%] Implementar el paso de mensajes hacia y desde los procesos calculadores con las clases de la capa de red (
TcpServer
,TcpClient
,HttpServer
) o la capa de aplicación (servicios web). -
[20%] Balancear la carga entre las calculadoras (descomposición y mapeo) y mantener la trazabilidad de las solicitudes.
-
[15%] Detener los procesos calculadores cuando se detiene el servidor web (con Ctrl+C o el comando
kill
). Buen uso de la memoria y de la concurrencia (Sanitizers y Valgrind). -
[5%] Pruebas de concurrencia (
httperf
) y su documentación (readme
). -
[5%] Modularización en subrutinas y archivos (
.hpp
,.cpp
). Apego a una convención de estilos (cpplint
). -
[5%] Documentación de interfaces (
doxygen
) e implementaciones (algoritmos).