sábado, 29 de enero de 2022

Concurrencia en Clojure - parte 7


Seguimos con concurrencia en Clojure

Ahora podemos modificar nuestro servicio web para usar sesiones. Primero, necesitamos una función que cree una nueva sesión:

(defn create-session []

  (let [snippets (repeatedly promise)

    translations (delay (map translate

                         (strings->sentences (map deref snippets))))]

      (new-session {:snippets snippets :translations translations})))


Seguimos usando una secuencia perezosa infinita de promesas para representar los fragmentos entrantes y un mapa sobre esa secuencia para representar las traducciones, pero ahora ambos están almacenados en una sesión.

A continuación, debemos modificar accept-snippet y get-translation para buscar :snippets o :translations dentro de una sesión:

(defn accept-snippet [session n text]

  (deliver (nth (:snippets session) n) text))

(defn get-translation [session n]

  @(nth @(:translations session) n))


Finalmente, definimos las rutas que vinculan estas funciones a las URI:

(defroutes app-routes
  (POST "/session/create" []
    (response (str (create-session))))

  (context "/session/:session-id" [session-id]
    (let [session (get-session (edn/read-string session-id))]
      (routes
        (PUT "/snippet/:n" [n :as {:keys [body]}]
          (accept-snippet session (edn/read-string n) (slurp body))
          (response "OK"))

        (GET "/translation/:n" [n]
          (response (get-translation session (edn/read-string n))))))))

Esto nos brinda un servicio web que hace un uso juicioso de los datos mutables pero aún se siente principalmente funcional.


jueves, 27 de enero de 2022

Concurrencia en Clojure - parte 6



Vamos a crear un servicio web que maneje múltiples transcripciones introduciendo el concepto de una sesión. Cada sesión tiene un identificador numérico único, que se genera de la siguiente manera:

(def last-session-id (atom 0))

(defn next-session-id []

(swap! last-session-id inc))

Esto usa un átomo, last-session-id , que se incrementa cada vez que queremos una nueva ID de sesión. Como resultado, cada vez que se llama a next-session-id, devuelve un número uno más alto que el anterior:

server.core=> (in-ns 'server.session)
#<Namespace server.session>
server.session=> (next-session-id)
1
server.session=> (next-session-id)
2
server.session=> (next-session-id)
3

Vamos a hacer un seguimiento de las sesiones activas con otro átomo llamado sesiones que contiene un mapa de ID de sesión y valores de sesión:

(def sessions (atom {}))

(defn new-session [initial]

  (let [session-id (next-session-id)]

    (swap! sessions assoc session-id initial)

     session-id))


(defn get-session [id]

   (@sessions id))

Creamos una nueva sesión pasando un valor inicial a new-session, que obtiene una nueva ID de sesión y la agrega a las sesiones llamando a swap!. Recuper una sesión con get-session es una simple cuestión de buscarlo por su ID.

Si no vamos a aumentar continuamente la cantidad de memoria que usamos, necesitaremos alguna forma de eliminar las sesiones cuando ya no estén en uso. Nosotros podría hacer esto explícitamente (quizás con una función de eliminación de sesión), pero dado que estamos escribiendo un servicio web en el que no necesariamente podemos confiar en que los clientes limpien correctamente, vamos a implementar la caducidad de la sesión (expiry) en su lugar. Esto requiere un pequeño cambio en el código anterior:

(def sessions (atom {}))

(defn now []

  (System/currentTimeMillis))


(defn new-session [initial]

  (let [session-id (next-session-id)

         session (assoc initial :last-referenced (atom (now)))]

    (swap! sessions assoc session-id session)

    session-id))


(defn get-session [id]

  (let [session (@sessions id)]

     (reset! (:last-referenced session) (now))

     session))


Hemos agregado una función llamada now que devuelve la hora actual. Cuando new-session crea una sesión, agrega una entrada :last-referenced a la sesión, otro átomo que contiene la hora actual. Esto se actualiza con reset! cada vez que get-session accede a la sesión.

Ahora que cada sesión tiene una entrada :last-referenced, podemos caducar sesiones verificando periódicamente si alguna no ha sido referenciada por más de un cierto período de tiempo:


(defn session-expiry-time []

  (- (now) (* 10 60 1000)))


(defn expired? [session]

  (< @(:last-referenced session) (session-expiry-time)))


(defn sweep-sessions []

  (swap! sessions #(remove-vals % expired?)))


(def session-sweeper

  (schedule {:min (range 0 60 5)} sweep-sessions))


Esto utiliza la biblioteca Schejulure para crear un barrido de sesiones, que se ejecuten una vez cada cinco minutos. Cada vez que se ejecuta, elimina cualquier sesión que haya expirado.

miércoles, 26 de enero de 2022

Concurrencia en Clojure - parte 5




Imagina que queremos tener un átomo que nunca tenga un valor negativo. Podemos garantizar esto por medio de una función de validación cuando creamos el átomo:

user=> (def non-negative (atom 0 :validator #(>= % 0)))

#'user/non-negative

user=> (reset! non-negative 42)

42

user=> (reset! non-negative -1)

IllegalStateException Invalid reference state

Un validador es una función que se llama cada vez que se intenta cambiar el valor del átomo. Si devuelve verdadero, el intento puede tener éxito, pero si devuelve falso, el intento se abandonará.

El validador se llama antes de que se haya cambiado el valor del átomo y, al igual que la función que se pasa a swap!. Por lo tanto, los validadores tampoco deben tener efectos secundarios.

Los átomos también pueden tener observadores asociados con ellos:

user=> (def a (atom 0))

#'user/a

user=> (add-watch a :print #(println "Changed from " %3 " to " %4))

#<Atom@542ab4b1: 0>

user=> (swap! a + 2)

Changed from 0 to 2

2

La clave se utiliza para identificar al observador (así, por ejemplo, si hay varios observadores, podemos eliminar uno específico proporcionando la clave correspondiente). La función watch se llama cada vez que cambia el valor del átomo. Se le dan cuatro argumentos: la clave que se le dio a add-watch, una referencia al átomo, el valor anterior y el nuevo valor.

En el código anterior, usamos la macro lectora #(...) nuevamente para definir una función anónima que imprime los valores antiguo (%3) y nuevo (%4) del átomo.

A diferencia de los validadores, las funciones de observación se llaman después de que el valor ha cambiado y solo se llamarán una vez, ¡sin importar la frecuencia con la que se intercambien! 

Por lo tanto, una función watch puede tener efectos secundarios. Que en el momento en que se llama a la función de observación, es posible que el valor del átomo ya haya cambiado nuevamente, por lo que las funciones de observación siempre deben usar los valores pasados como argumentos y nunca eliminar la referencia del átomo.


martes, 25 de enero de 2022

Concurrencia en Clojure - parte 4


Seguimos con concurrencia en Clojure

Las estructuras de datos persistentes son invaluables para la programación concurrente porque una vez que un subproceso tiene una referencia a una estructura de datos, no verá cambios realizados por ningún otro subproceso. Las estructuras de datos persistentes separan la identidad del estado. ¿Cuál es el nivel de combustible en su automóvil? En este momento, podría estar medio lleno. Algún tiempo después estará casi vacío, y unos minutos después de eso (después de que te detengas para llenar) estará lleno. La identidad "nivel de combustible en su automóvil" es una cosa, cuyo estado cambia constantemente. El “nivel de combustible en su automóvil” es en realidad una secuencia de diferentes valores: el 23/02/2012 a las 12:03 era 0,53; en 2012-02-23 14:30 era 0.12; y en 2012-02-23 14:31 era 1.00.

Una variable en un lenguaje imperativo completa (entrelaza, interconecta) la identidad y el estado: una única identidad solo puede tener un único valor, lo que hace que sea fácil perder de vista el hecho de que el estado es realmente una secuencia de valores a lo largo del tiempo. Las estructuras de datos persistentes separan la identidad del estado: si recuperamos el estado actual asociado con una identidad, ese estado es inmutable e muatble, sin importar lo que suceda con la identidad de la que recuperamos en el futuro.

Heráclito lo expresó así: No podías meterte dos veces en el mismo río; porque otras aguas están siempre fluyendo sobre ti. 

La mayoría de los lenguajes se aferran a la falacia de que el río es una sola entidad consistente; Clojure reconoce que está en constante cambio.

Debido a que Clojure es funcional, los átomos pueden no tener bloqueo; internamente, utilizan el método compareAndSet() en java.util.concurrent.AtomicReference. Eso significa que son muy rápidos y no se bloquean (por lo que no hay peligro de interbloqueo). Pero también significa ese intercambio necesita manejar el caso en el que el valor del átomo ha sido cambiado por otro hilo entre llamar a la función para generar un nuevo valor e intentar cambiar ese valor.

Si eso sucede, volverá a intentarlo. Descartará el valor devuelto por la función y lo llamará de nuevo con el nuevo valor del átomo. Ya vimos algo muy similar a esto cuando usamos ConcurrentHashMap. Esto significa que es esencial que la función pase a swap! no tiene efectos secundarios; si los tuviera, entonces esos efectos secundarios podrían ocurrir más de una vez. Afortunadamente, aquí es donde la naturaleza funcional de Clojure vale la pena: el código funcional naturalmente no tiene efectos secundarios.

lunes, 24 de enero de 2022

Concurrencia en Clojure - parte 3


Seguimos con concurrencia en Clojure

Las estructuras de datos persistentes, no tiene nada que ver con la persistencia en el disco o dentro de una base de datos. En cambio, se refiere a una estructura de datos que siempre conserva su versión anterior cuando se modifica, lo que permite que el código tenga una vista consistente de los datos ante modificaciones. 

Podemos ver esto fácilmente en el REPL:

user=> (def mapv1 {:name "paul" :age 45})

#'user/mapv1

user=> (def mapv2 (assoc mapv1 :sex :male))

#'user/mapv2

user=> mapv1

{:age 45, :name "paul"}

user=> mapv2

{:age 45, :name "paul", :sex :male}


Las estructuras de datos persistentes se comportan como si se hiciera una copia completa cada vez que se modifican. Si esa fuera la forma en que se implementaron realmente, sería muy ineficiente y, por lo tanto, de uso limitado 

Afortunadamente, la implementación es mucho más inteligente que eso y hace uso de la estructura compartida.

La estructura de datos persistentes más fácil de entender es la lista. Aquí hay una lista simple:

user=> (def listv1 (list 1 2 3))

#'user/listv1

user=> listv1

(1 2 3)


Y aquí hay un diagrama de cómo se ve en la memoria:



Ahora vamos a crear una versión modificada con cons, que devuelve una copia de la lista con un elemento agregado al frente:

user=> (def listv2 (cons 4 listv1))

#'user/listv2

user=> listv2

(4 1 2 3)

La nueva lista puede compartir toda la lista anterior, sin necesidad de copiar:

Finalmente, creemos otra versión modificada:

user=> (def listv3 (cons 5 (rest listv1)))

#'user/listv3

user=> listv3

(5 2 3)




En este caso, la nueva lista solo hace uso de una parte del original, pero aún no es necesaria la copia.

No siempre podemos evitar copiar. Las listas solo manejan bien las colas comunes: si queremos tener dos listas con colas diferentes, no tenemos más remedio que copiar.

Aquí hay un ejemplo:

user=> (def listv1 (list 1 2 3 4))
#'user/listv1
user=> (def listv2 (take 2 listv1))
#'user/listv2
user=> listv2
(1 2)

Todas las colecciones de Clojure son persistentes. Los vectores, mapas y conjuntos persistentes son más complejos de implementar que las listas, pero para nuestros propósitos todo lo que necesitamos saber es que comparten estructura y que proporcionan límites de rendimiento similares a sus equivalentes no persistentes en lenguajes como Ruby y Java.


Concurrencia en Clojure - parte 2


Seguimos con concurrencia en Clojure

Veamos un ejemplo que muestra cómo las estructuras de datos persistentes de Clojure significan que el estado mutable no puede escapar como lo hace en Java.

(def players (atom ()))

(defn list-players []

 (response (json/encode @players)))


(defn create-player [player-name]

  (swap! players conj player-name)

  (status (response "") 201))


(defroutes app-routes
  (GET "/players" [] (list-players))
  (PUT "/players/:player-name" [player-name] (create-player player-name)))

(defn -main [& args]
  (run-jetty (site app-routes) {:port 3000}))

Esto define un par de rutas: una solicitud GET a /players recuperará una lista de los jugadores actuales (en formato JSON) y una solicitud PUT a "/players/:player-name" agregará un jugador a esa lista. El servidor Embedded Jetty es multiproceso, por lo que nuestro código deberá ser seguro para subprocesos.

Veámoslo en acción. Podemos ejecutarlo desde la línea de comandos con curl:

$ curl localhost:3000/players

[]

$ curl -X put localhost:3000/players/john

$ curl localhost:3000/players

["john"]

$ curl -X put localhost:3000/players/paul

$ curl -X put localhost:3000/players/george

$ curl -X put localhost:3000/players/ringo

$ curl localhost:3000/players

["ringo","george","paul","john"]

Ahora veamos cómo funciona este código. El átomo de los jugadores :
  (def players (atom ()))
 se inicializa en la lista vacía (). Se agrega un nuevo jugador a la lista con conj :
  (swap! players conj player-name)
, y se devuelve una respuesta vacía con un estado HTTP 201 (creado). La lista de jugadores se devuelve mediante la codificación JSON del resultado de obtener el valor de los jugadores con @:
 (response (json/encode @players)))

Todo esto parece muy simple (y lo es), pero algo podría estar preocupándote algo al respecto. Tanto las funciones de listar jugadores como las de crear jugadores acceden a los jugadores. ¿Por qué este código no sufre el mismo problema que el código Java? ¿Qué sucede si un subproceso agrega una entrada a la lista de reproductores mientras otro lo itera y lo convierte a JSON? Este código es seguro para subprocesos porque las estructuras de datos de Clojure son persistentes.

Y que significa que sea persistente? y en el próximo post te cuento. 

martes, 18 de enero de 2022

La arquitectura del service discovery


Para comenzar nuestra discusión sobre la arquitectura de descubrimiento de servicios o service discovery, debemos comprender cuatro conceptos. Estos conceptos generales se comparten en todas las implementaciones de detección de servicios:

  • Registro de servicios: ¿Cómo se registra un servicio con el agente de detección de servicios?
  • Búsqueda del cliente de la dirección del servicio: ¿cuál es el medio por el cual un cliente del servicio busca información del servicio?
  • Información compartida: ¿Cómo se comparte la información del servicio entre los nodos?
  • Supervisión del estado: ¿cómo comunican los servicios su estado al agente de detección de servicios?

A medida que se inician las instancias de servicio, registrarán su ubicación física, ruta y puerto a los que se puede acceder con una o más instancias de detección de servicios. Si bien cada instancia de un servicio tendrá una dirección IP y un puerto únicos, cada instancia de servicio que surja se registrará con la misma ID de servicio. Un ID de servicio no es más que una clave que identifica de forma única a un grupo de las mismas instancias de servicio.

Por lo general, un servicio solo se registrará con una instancia de servicio de detección de servicios. La mayoría de las implementaciones de detección de servicios utilizan un modelo de propagación de datos de igual a igual en el que los datos de cada instancia de servicio se comunican a todos los demás nodos del clúster.

Dependiendo de la implementación del descubrimiento de servicios, el mecanismo de propagación podría usar una lista codificada de servicios para propagar o usar un protocolo de multidifusión. Como el protocolo "gossip" o "infection-style" para permitir que otros nodos "descubran" cambios en el clúster.

Finalmente, el servicio de detección de servicios empujará o sacará de su estado cada instancia de servicio. Cualquier servicio que no devuelva una buena verificación de estado se eliminará del conjunto de instancias de servicio disponibles.

Una vez que un servicio se ha registrado en un servicio de descubrimiento de servicios, está listo para ser utilizado por una aplicación o servicio que necesita usar sus capacidades. Existen diferentes modelos para que un cliente “descubra” un servicio. Un cliente puede confiar únicamente en el motor de descubrimiento de servicios para resolver las ubicaciones de los servicios cada vez que se llama a un servicio. Con este enfoque, el motor de detección de servicios se invocará cada vez que se realice una llamada a una instancia de microservicio registrada. Desafortunadamente, este enfoque es frágil porque el cliente del servicio depende completamente del motor de detección de servicios que se está ejecutando para encontrar e invocar un servicio.

Un enfoque más sólido es utilizar lo que se denomina balanceo de carga del lado del cliente. En este modelo, cuando un actor consumidor necesita invocar un servicio, se pondrá en contacto con el servicio de detección de servicios para todas las instancias de un servicio. El consumidor está solicitando y luego almacenar en caché los datos localmente en la máquina del consumidor del servicio.

Cada vez que un cliente desee llamar al servicio, el consumidor del servicio buscará la información de ubicación del servicio en la memoria caché. 

El almacenamiento en caché utilizará un algoritmo de balanceo de carga simple como el algoritmo de balanceo de carga "round-robin" para garantizar que las llamadas de servicio se distribuyan entre múltiples instancias de servicio.

Luego, el cliente se comunicará periódicamente con el servicio de descubrimiento de servicios y actualizará su caché de instancias de servicio. El caché del cliente finalmente es consistente, pero siempre existe el riesgo de que, entre el momento en que el cliente se pone en contacto con la instancia de detección de servicios para una actualización y se realicen las llamadas, las llamadas se dirijan a una instancia de servicio que no está en buen estado.

Si, durante el transcurso de la llamada a un servicio, la llamada de servicio falla, la memoria caché de detección de servicios local se invalida y el cliente de detección de servicios intentará actualizar sus entradas desde el agente de detección de servicios.

lunes, 17 de enero de 2022

Como tiene que ser el Service discovery en microservicios?


Ya vimos que no podemos utilizar un balanceador de carga tradicional para implementar el service discovery. Dado que la solución para un entorno de microservicios basado en la nube es usar un mecanismo de descubrimiento de servicios debe ser :

  • Altamente disponible: el descubrimiento de servicios debe ser compatible con un entorno de agrupación en clúster "caliente" donde las búsquedas de servicios se pueden compartir entre varios nodos en un clúster de descubrimiento de servicios. Si un nodo deja de estar disponible, otros nodos del clúster deberían poder reemplazarlo.
  • De igual a igual: cada nodo en el clúster de detección de servicios comparte el estado de una instancia de servicio.
  • Equilibrio de carga: el descubrimiento de servicios debe equilibrar dinámicamente la carga de solicitudes en todas las instancias de servicio para garantizar que las invocaciones de servicio se distribuyan en todas las instancias de servicio que gestiona. En muchos sentidos, el descubrimiento de servicios reemplaza a los balanceadores de carga más estáticos y administrados manualmente que se usaban en muchos de los primeras implementaciones de aplicaciones web.
  • Resiliente: el cliente del descubrimiento de servicios debe "almacenar en caché" la información del servicio localmente. El almacenamiento en caché local permite la degradación gradual del descubrimiento del servicio para que si el servicio de detección de servicios no está disponible, las aplicaciones aún pueden funcionar y ubicar los servicios en función de la información mantenida en su caché local.
  • Tolerante a fallas: el descubrimiento de servicios debe detectar cuándo una instancia de servicio no está en buen estado y eliminar la instancia de la lista de servicios disponibles que pueden tomar solicitudes de clientes. Debe detectar estas fallas con los servicios y tomar medidas sin intervención humana.

Podemos usar balanceadores de carga para implementar service discovery?


Hemos repasado los beneficios del descubrimiento de servicios, pero ¿cuál es el problema? Después de todo, ¿no podemos usar métodos probados y verdaderos como DNS (Servicio de nombres de dominio) o un balanceador de carga para ayudar a facilitar el descubrimiento de servicios? Analicemos por qué eso no funciona con una aplicación basada en microservicios, particularmente una que se ejecuta en la nube.

Siempre que tenga una aplicación que llama a recursos repartidos en varios servidores, necesita ubicar la ubicación física de esos recursos. En el mundo normal (no clud), esta resolución de ubicación de servicio a menudo se resolvía y se resuelve mediante una combinación de DNS y un balanceador de carga. 

Una aplicación si necesita invocar un servicio intenta invocar el servicio utilizando un nombre genérico junto con una ruta que representa de forma única el servicio que la aplicación intentaba invocar. El nombre DNS se resolvería en un balanceador de carga. 

El balanceador de carga, al recibir la solicitud del consumidor del servicio, ubica la entrada de la dirección física en una tabla de enrutamiento según la ruta a la que el usuario intentaba acceder. Esta entrada de la tabla de enrutamiento contiene una lista de uno o más servidores que alojan el servicio. Luego, el balanceador de carga elige uno de los servidores de la lista y reenvía la solicitud a ese servidor.

Cada instancia de un servicio se implementa en uno o más servidores de aplicaciones. La cantidad de estos servidores de aplicaciones a menudo era estática (por ejemplo, la cantidad de servidores de aplicaciones que alojaban un servicio no aumentaba ni disminuía) y persistente (por ejemplo, si fallaba un servidor que ejecutaba un servidor de aplicaciones, se restauraría al estado actual). mismo estado que tenía en el momento del bloqueo, y tendría la misma IP y configuración que tenía anteriormente).

Para lograr una forma de alta disponibilidad, un balanceador de carga secundario está inactivo y hace ping al balanceador de carga principal para ver si está activo. Si no está activo, el balanceador de carga secundario se activa, asumiendo la dirección IP del balanceador de carga principal y comenzando a atender las solicitudes.

Si bien este tipo de modelo funciona bien con aplicaciones que se ejecutan dentro de las cuatro paredes de un centro de datos corporativo y con una cantidad relativamente pequeña de servicios que se ejecutan en un grupo de servidores estáticos, no funciona bien para aplicaciones de microservicio basadas en la nube. Las razones para esto incluyen :

  • Punto único de falla: si bien el balanceador de carga puede tener una alta disponibilidad, es un punto único de falla para toda su infraestructura. Si el balanceador de carga se cae, todas las aplicaciones que dependen de él también se caen. Si bien puede hacer que un balanceador de carga esté altamente disponible, los balanceadores de carga tienden a ser cuellos de botella centralizados dentro de la infraestructura de la aplicación.
  • Escalabilidad horizontal limitada: al centralizar sus servicios en un solo grupo de balanceadores de carga, tiene una capacidad limitada para escalar horizontalmente su infraestructura de balanceo de carga en varios servidores. Muchos balanceadores de carga comerciales están limitados por dos cosas: su modelo de redundancia y los costos de licencia. La mayoría de los balanceadores de carga comerciales usan un modelo de intercambio en caliente para la redundancia, por lo que solo tiene un único servidor para manejar la carga, mientras que el balanceador de carga secundario está allí solo para la conmutación por error en el caso de una interrupción del balanceador de carga principal. Estás, en esencia, limitado por tu hardware. En segundo lugar, los balanceadores de carga comerciales también tienen modelos de licencia restrictivos orientados a una capacidad fija en lugar de un modelo más variable.
  • Gestionado estáticamente: la mayoría de los balanceadores de carga tradicionales no están diseñados para registrar y cancelar el registro de servicios rápidamente. Usan una base de datos centralizada para almacenar las rutas para las reglas y la única forma de agregar nuevas rutas es a menudo a través de la API (interfaz de programación de aplicaciones) propietaria del proveedor.
  • Complejo: debido a que un balanceador de carga actúa como un proxy para los servicios, las solicitudes de los consumidores de servicios deben tener sus solicitudes asignadas a los servicios físicos. Esta capa de traducción a menudo agregaba una capa de complejidad a su infraestructura de servicio porque las reglas de mapeo para el servicio deben definirse e implementarse a mano. En un escenario de balanceador de carga tradicional, este registro de nuevas instancias de servicio se realizaba a mano y no en el momento del inicio de una nueva instancia de servicio.

Estas cuatro razones no son una acusación general de los balanceadores de carga. Funcionan bien en un entorno corporativo donde el tamaño y la escala de la mayoría de las aplicaciones pueden manejarse a través de una infraestructura de red centralizada. Además, los balanceadores de carga aún tienen un papel que desempeñar en términos de centralizar la terminación SSL y administrar la seguridad del puerto del servicio. Un balanceador de carga puede bloquear el acceso al puerto de entrada (entrada) y salida (salida) a todos los servidores que se encuentran detrás de él. Este concepto de acceso mínimo a la red suele ser un componente crítico cuando se trata de cumplir con los requisitos de certificación estándar de la industria, como el cumplimiento de PCI (industria de tarjetas de pago).

Sin embargo, en la nube, donde tiene que lidiar con cantidades masivas de transacciones y redundancia, una pieza centralizada de infraestructura de red no funciona tan bien en última instancia porque no se escala de manera efectiva y no es rentable. 

sábado, 15 de enero de 2022

Deploying NGINX as an Api Gateway

 Me llego este mail con un libro gratuito y se los comparto: 

EBOOK

Image

Enterprises use NGINX load‑balancing features to complement existing hardware ADCs, or even to replace them, and while converting applications from monoliths to microservices. Among the many use cases for NGINX, the API gateway use case is becoming increasingly widespread, especially when consolidating load balancers and API gateways into a single solution.

We show you how to extend NGINX to manage API traffic in a comprehensive set of use cases. We start off with information about configuring the API gateway for single-service and microservices APIs, rewriting client requests, responding to errors, and authenticating users. Then you learn how to secure the gateway with rate limiting, access controls, limits on request methods and size, and other techniques. Finally, we explain how to publish gRPC services.

With NGINX for API management, you tap into the high performance, reliability, robust community support, and advanced functionality and professional support (for NGINX Plus customers) that NGINX is famous for.

In this eBook you will learn:

  • About using NGINX as a complement and replacement for existing API gateway and API management approaches
  • How to extend the configuration of an existing NGINX instance to also manage API traffic
  • About a range of safeguards for protecting and securing backend API services in production
  • How to deploy NGINX as an API gateway for gRPC services

viernes, 14 de enero de 2022

Que es service discovery?


En cualquier arquitectura distribuida, necesitamos encontrar la dirección física de donde se encuentra una máquina. Este concepto ha existido desde el comienzo de la computación distribuida y se conoce formalmente como descubrimiento de servicios. El descubrimiento de servicios puede ser algo tan simple como mantener un archivo de propiedades con las direcciones de todos los servicios remotos utilizados por una aplicación, o algo tan formalizado (y complicado) como un repositorio UDDI (Descripción Universal, Descubrimiento e Integración) El descubrimiento de servicios es crítico a aplicaciones de microservicio basadas en la nube por dos razones claves:

Ofrece al equipo de aplicaciones la capacidad de escalar horizontalmente rápidamente la cantidad de instancias de servicio que se ejecutan en un entorno. Los consumidores del servicio se abstraen de la ubicación física del servicio a través del descubrimiento del servicio. Debido a que los consumidores del servicio no conocen la ubicación física de las instancias de servicio reales, se pueden agregar o eliminar nuevas instancias de servicio del grupo de servicios disponibles. 

Esta capacidad de escalar rápidamente los servicios sin interrumpir a los consumidores del servicio es un concepto extremadamente poderoso, porque hace que un equipo de desarrollo acostumbrado a crear aplicaciones monolíticas deje de pensar en escalar solo en términos de agregar mayores , mejor hardware (escalado vertical) al enfoque más potente de escalado mediante la adición de más servidores (escalado horizontal). 

Un enfoque monolítico suele llevar a los equipos de desarrollo por el camino de sobrecomprar sus necesidades de capacidad. Los aumentos de capacidad vienen en grupos y picos y rara vez son un camino suave y constante. Los microservicios nos permiten escalar hacia arriba o hacia abajo nuevas instancias de servicio. El descubrimiento de servicios ayuda a abstraer que estas implementaciones ocurren lejos del consumidor del servicio.

El segundo beneficio del descubrimiento de servicios es que ayuda a aumentar la resistencia de las aplicaciones. Cuando una instancia de microservicio no está en buen estado o no está disponible, la mayoría de los motores de detección de servicios eliminarán esa instancia de su lista interna de servicios disponibles.

El daño causado por un servicio inactivo se minimizará porque el motor de descubrimiento de servicios enrutará los servicios alrededor del servicio no disponible.


jueves, 13 de enero de 2022

Consumir configuraciones de un servidor de configuración con Spring Cloud




Se acuerdan que en un post anterior hicimos un servidor de configuración con Spring Cloud. Ahora vamos a utilizarlo. 

Cuando el cliente se inicia por primera vez, pasará el perfil de Spring y el end point para comunicarse con el servicio de configuración de Spring Cloud. El valor del perfil Spring se asigna al entorno de las propiedades que se recuperan para el servicio Spring. El servicio Spring Cloud Config luego usará el repositorio de configuración de back-end configurado (sistema de archivos, Git, Consul, Eureka) para recuperar la información de configuración específica para el valor del perfil de Spring pasado en el URI. Spring Boot luego inyectará estos valores en las partes apropiadas de la aplicación.

Lo primero que debe hacer es agregar un par de entradas más al archivo Maven :

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-config-client</artifactId>

</dependency>

La dependencia, spring-cloud-config-client, contiene todas las clases necesarias para interactuar con el servidor de configuración de Spring Cloud.

Una vez definidas las dependencias de Maven, debe indicarle al servicio dónde ponerse en contacto con el servidor de configuración de Spring Cloud. En un servicio Spring Boot que usa Spring Cloud Config, la información de configuración se puede establecer en uno de dos archivos de configuración: bootstrap.yml y application.yml.

El archivo bootstrap.yml lee las propiedades de la aplicación antes de utilizar cualquier otra información de configuración. En general, el archivo bootstrap.yml contiene el nombre de la aplicación para el servicio, el perfil de la aplicación y el URI para conectarse a un servidor Spring Cloud Config.

Cualquier otra información de configuración que desee mantener local para el servicio (y no almacenada en Spring Cloud Config) se puede configurar localmente en los servicios en el archivo application.yml. Por lo general, la información que almacena en el archivo application.yml son datos de configuración que quizás desee tener disponibles para un servicio, incluso si el servicio Spring Cloud Config no está disponible. Los archivos bootstrap.yml y application.yml se almacenan en un directorio de proyectos src/main/resources.

Para que el cliente se comunique con su servicio Spring Cloud Config, debe agregar un archivo /src/main/resources/bootstrap.yml y configurar tres propiedades: spring.application.name, spring.profiles.active y spring.cloud.config.uri.

El archivo bootstrap.yml de los servicios de licencias se muestra en la siguiente lista.

spring.application.name es el nombre de su aplicación y debe asignarse directamente al nombre del directorio dentro de su servidor de configuración de Spring Cloud. Para el servicio de licencias, desea un directorio en el servidor de configuración de Spring Cloud llamado licenseservice.

La segunda propiedad, spring.profiles.active, se usa para decirle a Spring Boot con qué perfil debe ejecutarse la aplicación. Un perfil es un mecanismo para diferenciar los datos de configuración consumidos por la aplicación Spring Boot. Para el perfil del cliente, admitirá el entorno al que se asignará el servicio directamente en su entorno de configuración de la nube. Por ejemplo, al pasar dev como nuestro perfil, el servidor de configuración de Spring Cloud usará las propiedades dev. Si establece un perfil, el servicio de licencias utilizará el perfil predeterminado.
La tercera y última propiedad, spring.cloud.config.uri, es la ubicación donde el servicio de licencias debe buscar el servidor de configuración de Spring Cloud.

punto final. De forma predeterminada, el servicio de licencias buscará el servidor de configuración en http://localhost:8888. Más adelante en el capítulo, verá cómo anular las diferentes propiedades definidas en los archivos boostrap.yml y application.yml al iniciar la aplicación. Esto le permitirá decirle al microservicio de licencias en qué entorno se debe ejecutar.

Ahora, si abre el servicio de configuración de Spring Cloud, con la base de datos de Postgres correspondiente ejecutándose en su máquina local, puede iniciar el servicio de licencias utilizando su perfil predeterminado. Esto se hace cambiando al directorio de servicios de licencias y emitiendo los siguientes comandos:
 
 mvn spring-boot: ejecutar

Al ejecutar este comando sin establecer ninguna propiedad, el servidor de licencias intentará conectarse automáticamente al servidor de configuración de Spring Cloud utilizando el punto final (http://localhost:8888) y el perfil activo (predeterminado) definido en bootstrap.yml archivo del servicio de licencias.

Si desea anular estos valores predeterminados y apuntar a otro entorno, puede hacerlo compilando el proyecto de servicio de licencias en un JAR y luego ejecutar
el JAR con una anulación de propiedad del sistema -D. La siguiente llamada de línea de comando demuestra cómo iniciar el servicio de licencias con un perfil no predeterminado:

java -Dspring.cloud.config.uri=http://localhost:8888 \
-Dspring.profiles.active=dev \
-jar destino/servicio de licencias-0.0.1-SNAPSHOT.jar

Con la línea de comando anterior, está anulando los dos parámetros:
spring.cloud.config.uri y spring.profiles.active. Con la propiedad del sistema -Dspring.cloud.config.uri=http://localhost:8888, está apuntando a un servidor de configuración que se está ejecutando fuera de su caja local.

Con la propiedad del sistema –Dspring.profiles.active=dev, le indica al cliente que use el perfil de desarrollo (leído del servidor de configuración) 


domingo, 9 de enero de 2022

Construyendo nuestro servidor de configuración con Spring Cloud


El servidor de configuración de Spring Cloud es una aplicación basada en REST que está construida sobre Spring Boot. No viene como un servidor independiente. En su lugar, puede elegir embeberlo en una aplicación Spring Boot existente o iniciar un nuevo proyecto Spring Boot con el servidor embebido.

Vamos a hacer una nueva aplicación string boot como servidor de configuraciones. Yo voy a utilizar spring initializr pero podemos agregar estas dependencias en un nuevo proyecto o uno existente. 

<dependencies>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-config</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-config-server</artifactId>

</dependency>

</dependencies>

O con gradle sería así : 

implementation 'org.springframework.cloud:spring-cloud-starter-config'

implementation 'org.springframework.cloud:spring-cloud-config-server'


La primera dependencia es la dependencia spring-cloud-starter-config que utilizan todos los proyectos de Spring Cloud. La segunda dependencia es el proyecto de inicio spring-cloud-config-server. Esto contiene las bibliotecas centrales para spring-cloud-config-server.

Necesitamos configurar un archivo más para que el servidor de configuración central esté en funcionamiento. Este archivo es el archivo application.yml y lo debemos generar en el directorio src/main/resources. El archivo application.yml le dirá a su servicio de configuración de Spring Cloud qué puerto escuchar y dónde ubicar el back-end que entregará los datos de configuración. En mi caso el appication.yml es así : 

server:
port: 8888
spring:
profiles:
active: native
cloud:
config:
server:
native:
searchLocations: file:///{projectPath}/resources

Ojo que {projectPath} es donde esta el proyecto, la dirección por ejemplo el mio esta en /home/emanuel/misProyectos/application. 

En la configuración de Spring Cloud, todo funciona con una jerarquía. La configuración de la aplicación está representada por el nombre de la aplicación y luego un archivo de propiedades para cada entorno para el que se desea tener información de configuración. En cada uno de estos entornos, configurará dos propiedades de configuración. 

Vamos a suponer que tenemos 3 entornos uno de dev, otro de uat y otro de prod. Vamos a crear directorios para cada uno de los entornos en src/main/resources una carpeta por cada entorno. 

El mapeo de las carpetas y los archivos va a ser de la siguiente manera : 

/{application}/{profile}[/{label}]

/{application}-{profile}.yml

/{label}/{application}-{profile}.yml

/{application}-{profile}.properties

/{label}/{application}-{profile}.properties

Noten que el label es opcional. Por lo tanto si hacemos archivos application-dev.properties o application-uat.properties esto debería andar. 

Y por ultimo le decimos a spring que vamos a utilizar spring config con esta anotación : 


@SpringBootApplication
@EnableConfigServer
public class SettingsApplication {

Ahora levantamos el servidor y si necesitamos las configuraciones de uat deberíamos hacer : 

http://localhots:8888/application/uat

Tendremos este resultado : 

{"name":"demo","profiles":["uat"],"label":null,"version":null,"state":null,"propertySources":[{"name":"file:/home/emanuel/Projects/spring-cloud/settings/src/main/resources/demo-uat.yml","source":{"env":"dev"}},{"name":"file:/home/emanuel/Projects/spring-cloud/settings/src/main/resources/application.yml","source":{"server.port":8888,"spring.profiles.active":"native","spring.cloud.config.server.native.searchLocations":"file:///home/emanuel/Projects/spring-cloud/settings/src/main/resources"}}]}
Y listo, nos queda ver como podemos consumir estos settings. (pero eso es para otro post) 

Dejo link: https://cloud.spring.io/spring-cloud-config

sábado, 8 de enero de 2022

Concurrencia en Clojure


Clojure proporciona un estilo híbrido de programación funcional y estado mutable para resolver problemas de forma concurrente; el "Clojure Way" aprovecha las fortalezas de ambos para brindar un enfoque particularmente poderoso para la programación concurrente.

Si bien la programación funcional funciona increíblemente bien para algunos problemas, algunos tienen la modificación del estado como un elemento fundamental de la solución. Aunque puede ser posible crear una solución funcional a tales problemas, es más fácil pensar en ellos de una manera más tradicional. 

Un lenguaje funcional puro no proporciona soporte para datos mutables en absoluto. Clojure, por el contrario, es impuro: proporciona varios tipos diferentes de variables mutables conscientes de la concurrencia, cada una de las cuales es adecuada para diferentes casos de uso. Estos, en conjunto con las estructuras de datos persistentes de Clojure (vamos a cubriremos lo que significa persistente en este contexto más adelante) nos permiten evitar muchos de los problemas que tradicionalmente afligen a los programas concurrentes con estado mutable compartido.

La diferencia entre un lenguaje funcional impuro y un lenguaje imperativo es de énfasis. En un lenguaje imperativo, las variables son mutables por defecto y el código idiomático las modifica con frecuencia. En un lenguaje funcional impuro, las variables son inmutables por defecto y el código idiomático modifica aquellas que no lo son solo cuando es absolutamente necesario. Como veremos, las variables mutables de Clojure nos permiten manejar los efectos secundarios del mundo real sin dejar de ser seguros y consistentes.

Veremos cómo las variables mutables de Clojure funcionan en conjunto con las estructuras de datos persistentes para separar la identidad del estado. Esto permite que varios subprocesos accedan a variables mutables simultáneamente sin bloqueos (y el peligro asociado de interbloqueo) y sin ninguno de los problemas de estado mutable escapado u oculto. Comenzaremos por lo que podría decirse que es el más simple de los tipos de variables mutables de Clojure, el átomo.

Un átomo es una variable atómica  (los átomos de Clojure están construidos sobre java.util.concurrent.atomic). A continuación, se muestra un ejemplo de cómo crear y recuperar el valor de un átomo:

user=> (def my-atom (atom 42))

#'user/my-atom

user=> (deref my-atom)

42

user=> @my-atom

42

Un átomo se crea con átomo, que toma un valor inicial. Podemos encontrar el valor actual de un átomo con deref o @.
Si desea actualizar un átomo a un nuevo valor, se usa swap !:

user=> (swap! my-atom inc)
43
user=> @my-atom
43

Esto toma una función y le pasa el valor actual del átomo. El nuevo valor del átomo se convierte en el valor de retorno de la función. También podemos pasar argumentos adicionales a la función, como en este ejemplo:

user=> (swap! my-atom + 2)
45

El primer argumento pasado a la función será el valor actual del átomo, y luego cualquier argumento adicional dado para intercambiar. Entonces, en este caso, el nuevo valor se convierte en el resultado de (+ 43 2).

En raras ocasiones, es posible que desee establecer un átomo en un valor que no dependa de su valor actual, en cuyo caso puede usar reset !:

user=> (reset! my-atom 0)
0
user=> @my-atom
0

Los átomos pueden ser de cualquier tipo; muchas aplicaciones web utilizan un mapa atómico para almacenar datos de sesión, como en este ejemplo:

user=> (def session (atom {}))
#'user/session
user=> (swap! session assoc :username "paul")
{:username "paul"}
user=> (swap! session assoc :session-id 1234)
{:session-id 1234, :username "paul"}

Ya se que no vimos nada de concurrencia aun, pero paciencia, como me queda largo el post seguimos en otro... 

viernes, 7 de enero de 2022

Hagamos un proyecto con scalatra como en los viejos tiempos


 Hace mucho mucho que no hago un proyecto con scalatra y me dio añoranza, así que vamos a hacer un "hola mundo" para rememorar buenos tiempos. 

Primero tenemos que tener instalado sbt, yo lo instale con sdkman (que si no lo conocen dejo el post acá

sdk i sbt 

y listo, luego creemos nuestro proyecto : 

sbt new scalatra/scalatra.g8

ahí nos va a pedir unos datos, complete nomas ...

Y bueno, ya esta... Porque scalatra nos trae un ejemplo el cual esta en el paquete com.example.app

ackage com.example.app

import org.scalatra._

class MyScalatraServlet extends ScalatraServlet {

get("/") {
views.html.hello()
}

}

Que lo que hace es mostrar lo que esta en la vista es decir en src/main/twirl/views/hello.scala.html : 

@()
@layouts.html.default("Scalatra: a tiny, Sinatra-like web framework for Scala", "Welcome to Scalatra"){
<p>Hello, Twirl!</p>
}

Que es una mezcla de scala con Html. 

Ahora vamos a ver el ejemplo, en marcha, en el directorio del proyecto, escribimos sbt y luego 

> jetty:start

Y la aplicación se verá en http://localhost:8080

Y ya esta!! 

Dejo link: https://scalatra.org



miércoles, 5 de enero de 2022

Transforme su empresa con el Machine Learning

Me llego un mail de la gente de AWS, el cual tiene un link a un libro digital que trata sobre la transformación de las empresas con el Machine Learning y me gustaría compartirlo: 

 

Descubra en "Cómo conseguir resultados empresariales transformadores gracias al Machine Learning" cómo los clientes de AWS resuelven los desafíos empresariales con las soluciones de ML de AWS, entre los que se incluyen:
  • Mejorar la experiencia del cliente
  • Optimizar las operaciones empresariales y aumentar la productividad
  • Acelerar la innovación

Lea el libro electrónico »

 
 
Blog de AWS  ln brk   Facebook   Twitter   YouTube   Slideshare