Desplegar Memcached en GKE

En este tutorial, aprenderás a desplegar un clúster de servidores Memcached distribuidos en Google Kubernetes Engine (GKE) con Kubernetes, Helm y Mcrouter. Memcached es un sistema de almacenamiento en caché de código abierto y polivalente muy popular. Normalmente, sirve como almacenamiento temporal de datos que se usan con frecuencia para acelerar las aplicaciones web y reducir la carga de las bases de datos.

Características de Memcached

Memcached tiene dos objetivos de diseño principales:

  • Sencillez: Memcached funciona como una gran tabla hash y ofrece una API sencilla para almacenar y recuperar objetos de forma arbitraria por clave.
  • Velocidad: Memcached almacena los datos de la caché exclusivamente en la memoria de acceso aleatorio (RAM), lo que hace que el acceso a los datos sea extremadamente rápido.

Memcached es un sistema distribuido que permite que la capacidad de su tabla hash se escale horizontalmente en un grupo de servidores. Cada servidor Memcached funciona de forma totalmente aislada de los demás servidores del grupo. Por lo tanto, el enrutamiento y el balanceo de carga entre los servidores deben realizarse a nivel de cliente. Los clientes de Memcached aplican un esquema de hash coherente para seleccionar los servidores de destino de forma adecuada. Este programa garantiza las siguientes condiciones:

  • Siempre se selecciona el mismo servidor para la misma clave.
  • El uso de memoria se reparte de forma equilibrada entre los servidores.
  • Se reubica un número mínimo de claves cuando se reduce o se amplía el grupo de servidores.

En el siguiente diagrama se muestra a grandes rasgos la interacción entre un cliente de Memcached y un conjunto distribuido de servidores de Memcached.

Interacción entre memcached y un grupo de servidores memcached
Figura 1: Interacción de alto nivel entre un cliente de Memcached y un grupo distribuido de servidores de Memcached.

Desplegar un servicio de Memcached

Una forma sencilla de desplegar un servicio de Memcached en GKE es usar un gráfico de Helm. Para continuar con la implementación, sigue estos pasos en Cloud Shell:

  1. Crea un clúster de GKE con tres nodos:

    gcloud container clusters create demo-cluster --num-nodes 3 --location us-central1-f
    
  2. Descarga el archivo binario de helm:

    HELM_VERSION=3.7.1
    cd ~
    wget https://get.helm.sh/helm-v${HELM_VERSION}-linux-amd64.tar.gz
    
  3. Descomprime el archivo en tu sistema local:

    mkdir helm-v${HELM_VERSION}
    tar zxfv helm-v${HELM_VERSION}-linux-amd64.tar.gz -C helm-v${HELM_VERSION}
    
  4. Añade el directorio del archivo binario helm a la variable de entorno PATH:

    export PATH="$(echo ~)/helm-v${HELM_VERSION}/linux-amd64:$PATH"
    

    Este comando hace que el archivo binario helm se pueda detectar desde cualquier directorio durante la sesión de Cloud Shell actual. Para que esta configuración se mantenga en varias sesiones, añade el comando al archivo ~/.bashrc del usuario de Cloud Shell.

  5. Instala una nueva versión del chart de Helm de Memcached con la arquitectura de alta disponibilidad:

    helm repo add bitnami https://charts.bitnami.com/bitnami
    helm install mycache bitnami/memcached --set architecture="high-availability" --set autoscaling.enabled="true"
    

    El gráfico de Helm de Memcached usa un controlador StatefulSet. Una de las ventajas de usar un controlador StatefulSet es que los nombres de los pods están ordenados y son predecibles. En este caso, los nombres son mycache-memcached-{0..2}. De esta forma, los clientes de Memcached pueden hacer referencia a los servidores más fácilmente.

  6. Para ver los pods en ejecución, ejecuta el siguiente comando:

    kubectl get pods
    

    La Google Cloud salida de la consola tiene este aspecto:

    NAME                  READY     STATUS    RESTARTS   AGE
    mycache-memcached-0   1/1       Running   0          45s
    mycache-memcached-1   1/1       Running   0          35s
    mycache-memcached-2   1/1       Running   0          25s

Descubrir los endpoints de servicio de Memcached

El gráfico de Helm de Memcached usa un servicio sin encabezado. Un servicio sin encabezado expone las direcciones IP de todos sus pods para que se puedan descubrir individualmente.

  1. Comprueba que el servicio implementado no tiene interfaz gráfica:

    kubectl get service mycache-memcached -o jsonpath="{.spec.clusterIP}"
    

    El resultado None confirma que el servicio no tiene clusterIP y, por lo tanto, no tiene interfaz.

    El servicio crea un registro DNS para un nombre de host con el siguiente formato:

    [SERVICE_NAME].[NAMESPACE].svc.cluster.local
    

    En este tutorial, el nombre del servicio es mycache-memcached. Como no se ha definido explícitamente ningún espacio de nombres, se utiliza el espacio de nombres predeterminado y, por lo tanto, el nombre de host completo es mycache-memcached.default.svc.cluster.local. Este nombre de host se resuelve en un conjunto de direcciones IP y dominios de los tres pods expuestos por el servicio. Si, en el futuro, se añaden pods al pool o se quitan algunos, kube-dns actualizará automáticamente el registro DNS.

    Es responsabilidad del cliente descubrir los endpoints del servicio Memcached, tal como se describe en los pasos siguientes.

  2. Obtén las direcciones IP de los endpoints:

    kubectl get endpoints mycache-memcached
    

    El resultado debería ser similar al siguiente:

    NAME                ENDPOINTS                                            AGE
    mycache-memcached   10.36.0.32:11211,10.36.0.33:11211,10.36.1.25:11211   3m
    

    Ten en cuenta que cada pod de Memcached tiene una dirección IP independiente:10.36.0.32, 10.36.0.33 y 10.36.1.25. Estas direcciones IP pueden ser diferentes en tus instancias de servidor. Cada pod escucha el puerto 11211, que es el puerto predeterminado de Memcached.

  3. Como alternativa al paso 2, realiza una inspección de DNS con un lenguaje de programación como Python:

    1. Inicia una consola interactiva de Python en tu clúster:

      kubectl run -it --rm python --image=python:3.10-alpine --restart=Never python
      
    2. En la consola de Python, ejecuta estos comandos:

      import socket
      print(socket.gethostbyname_ex('mycache-memcached.default.svc.cluster.local'))
      exit()
      

      El resultado debería ser similar al siguiente:

      ('mycache-memcached.default.svc.cluster.local', ['mycache-memcached.default.svc.cluster.local'], ['10.36.0.32', '10.36.0.33', '10.36.1.25'])
  4. Prueba la implementación abriendo una sesión telnet con uno de los servidores Memcached en ejecución en el puerto 11211:

    kubectl run -it --rm busybox --image=busybox:1.33 --restart=Never telnet mycache-memcached-0.mycache-memcached.default.svc.cluster.local 11211
    

    En la petición telnet, ejecuta estos comandos con el protocolo ASCII de Memcached:

    set mykey 0 0 5
    hello
    get mykey
    quit

    El resultado se muestra aquí en negrita:

    set mykey 0 0 5
    hello
    STORED
    get mykey
    VALUE mykey 0 5
    hello
    END
    quit

Implementar la lógica de descubrimiento de servicios

Ahora puedes implementar la lógica básica de detección de servicios que se muestra en el siguiente diagrama.

<img <="" alt="lógica de descubrimiento de servicios" img="" src="/static/architecture/images/memcached-fig-2.svg" />
Figura 2: lógica de descubrimiento de servicios.

A grandes rasgos, la lógica de detección de servicios consta de los siguientes pasos:

  1. La aplicación consulta kube-dns para obtener el registro DNS de mycache-memcached.default.svc.cluster.local.
  2. La aplicación recupera las direcciones IP asociadas a ese registro.
  3. La aplicación crea una instancia de un nuevo cliente de Memcached y le proporciona las direcciones IP obtenidas.
  4. El balanceador de carga integrado del cliente de Memcached se conecta a los servidores de Memcached en las direcciones IP indicadas.

Ahora, implementa esta lógica de descubrimiento de servicios con Python:

  1. Despliega un nuevo pod con Python en tu clúster e inicia una sesión de shell en el pod:

    kubectl run -it --rm python --image=python:3.10-alpine --restart=Never sh
    
  2. Instala la biblioteca pymemcache:

    pip install pymemcache
    
  3. Inicia una consola interactiva de Python ejecutando el comando python.

  4. En la consola de Python, ejecuta estos comandos:

    import socket
    from pymemcache.client.hash import HashClient
    _, _, ips = socket.gethostbyname_ex('mycache-memcached.default.svc.cluster.local')
    servers = [(ip, 11211) for ip in ips]
    client = HashClient(servers, use_pooling=True)
    client.set('mykey', 'hello')
    client.get('mykey')
    

    El resultado es el siguiente:

    b'hello'

    El prefijo b indica un literal de bytes, que es el formato en el que Memcached almacena los datos.

  5. Sal de la consola de Python:

    exit()
    
  6. Para salir de la sesión de shell del pod, pulsa Control+D.

Habilitar el grupo de conexiones

A medida que aumentan tus necesidades de almacenamiento en caché y el grupo se amplía a docenas, cientos o miles de servidores Memcached, es posible que te encuentres con algunas limitaciones. En concreto, el gran número de conexiones abiertas de clientes de Memcached puede suponer una carga pesada para los servidores, como se muestra en el siguiente diagrama.

<img <="" alt="Número elevado de conexiones abiertas cuando todos los clientes de Memcached acceden directamente a todos los servidores de Memcached" img="" src="/static/architecture/images/memcached-fig-3.svg" />
Figura 3: Número elevado de conexiones abiertas cuando todos los clientes de Memcached acceden directamente a todos los servidores de Memcached.

Para reducir el número de conexiones abiertas, debes introducir un proxy para habilitar la agrupación de conexiones, como se muestra en el siguiente diagrama.

<img <="" alt="Proxy to enable connection pooling." img="" src="/static/architecture/images/memcached-fig-4.svg" />
Figura 4: Uso de un proxy para reducir el número de conexiones abiertas.

Mcrouter (pronunciado "mick router"), un potente proxy de Memcached de código abierto, permite la agrupación de conexiones. La integración de Mcrouter es sencilla, ya que usa el protocolo ASCII estándar de Memcached. Para un cliente de Memcached, Mcrouter se comporta como un servidor de Memcached normal. Para un servidor Memcached, Mcrouter se comporta como un cliente Memcached normal.

Para implementar Mcrouter, ejecuta los siguientes comandos en Cloud Shell.

  1. Elimina la versión del gráfico de Helm mycache que se haya instalado previamente:

    helm delete mycache
    
  2. Implementa nuevos pods de Memcached y Mcrouter instalando una nueva versión del chart de Helm de Mcrouter:

    helm repo add stable https://charts.helm.sh/stable
    helm install mycache stable/mcrouter --set memcached.replicaCount=3
    

    Los pods proxy ya están listos para aceptar solicitudes de aplicaciones cliente.

  3. Prueba esta configuración conectándote a uno de los pods proxy. Usa el comando telnet en el puerto 5000, que es el puerto predeterminado de Mcrouter.

    MCROUTER_POD_IP=$(kubectl get pods -l app=mycache-mcrouter -o jsonpath="{.items[0].status.podIP}")
    
    kubectl run -it --rm busybox --image=busybox:1.33 --restart=Never telnet $MCROUTER_POD_IP 5000
    

    En el símbolo del sistema telnet, ejecuta estos comandos:

    set anotherkey 0 0 15
    Mcrouter is fun
    get anotherkey
    quit

    Los comandos definen y muestran el valor de tu clave.

Ahora has implementado un proxy que permite la agrupación de conexiones.

Reducir la latencia

Para aumentar la resiliencia, es habitual usar un clúster con varios nodos. En este tutorial se usa un clúster con tres nodos. Sin embargo, usar varios nodos también conlleva el riesgo de que aumente la latencia debido al mayor tráfico de red entre los nodos.

Colocación de pods proxy

Puedes reducir este riesgo conectando los pods de la aplicación cliente solo a un pod proxy de Memcached que esté en el mismo nodo. En el siguiente diagrama se muestra esta configuración.

<img <="" alt="topology for interactions between pods" img="" src="/static/architecture/images/memcached-fig-5.svg" />
Figura 5: Topología de las interacciones entre los pods de la aplicación, los pods de Mcrouter y los pods de Memcached en un clúster de tres nodos.

Para llevar a cabo esta configuración, sigue estos pasos:

  1. Asegúrate de que cada nodo contenga un pod proxy en ejecución. Una práctica habitual es desplegar los pods proxy con un controlador DaemonSet. A medida que se añaden nodos al clúster, se añaden automáticamente nuevos pods proxy. A medida que se eliminan nodos del clúster, esos pods se recogen como elementos no utilizados. En este tutorial, el gráfico de Helm de Mcrouter que desplegaste anteriormente usa un controlador DaemonSet de forma predeterminada. Por lo tanto, este paso ya está completado.
  2. Define un valor hostPort en los parámetros de Kubernetes del contenedor proxy para que el nodo escuche ese puerto y redirija el tráfico al proxy. En este tutorial, el gráfico de Helm de Mcrouter usa este parámetro de forma predeterminada para el puerto 5000. Por lo tanto, este paso también está completado.
  3. Expón el nombre del nodo como una variable de entorno dentro de los pods de la aplicación mediante la entrada spec.env y seleccionando el valor spec.nodeName fieldRef. Consulta más información sobre este método en la documentación de Kubernetes.

    1. Despliega pods de aplicaciones de ejemplo. El siguiente comando aplica un despliegue de Kubernetes. Un Deployment es un objeto de la API de Kubernetes que te permite ejecutar varias réplicas de pods distribuidas entre los nodos de un clúster:

      cat <<EOF | kubectl create -f -
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: sample-application
      spec:
        selector:
          matchLabels:
            app: sample-application
        replicas: 9
        template:
          metadata:
            labels:
              app: sample-application
          spec:
            containers:
              - name: busybox
                image: busybox:1.33
                command: [ "sh", "-c"]
                args:
                - while true; do sleep 10; done;
                env:
                  - name: NODE_NAME
                    valueFrom:
                      fieldRef:
                        fieldPath: spec.nodeName
      EOF
      
  4. Verifica que el nombre del nodo se expone buscando en uno de los pods de la aplicación de ejemplo:

    POD=$(kubectl get pods -l app=sample-application -o jsonpath="{.items[0].metadata.name}")
    
    kubectl exec -it $POD -- sh -c 'echo $NODE_NAME'
    

    Este comando muestra el nombre del nodo con el siguiente formato:

    gke-demo-cluster-default-pool-XXXXXXXX-XXXX

Conectar los pods

Los pods de la aplicación de ejemplo ya están listos para conectarse al pod de Mcrouter que se ejecuta en sus respectivos nodos mutuos en el puerto 5000, que es el puerto predeterminado de Mcrouter.

  1. Inicia una conexión para uno de los pods abriendo una sesión de telnet:

    POD=$(kubectl get pods -l app=sample-application -o jsonpath="{.items[0].metadata.name}")
    
    kubectl exec -it $POD -- sh -c 'telnet $NODE_NAME 5000'
    
  2. En el símbolo del sistema telnet, ejecuta estos comandos:

    get anotherkey
    quit
    

    Salida resultante:

    Mcrouter is fun

Por último, a modo de ejemplo, el siguiente código de Python es un programa de muestra que realiza esta conexión. Para ello, obtiene la variable NODE_NAME del entorno y usa la biblioteca pymemcache:

import os
from pymemcache.client.base import Client

NODE_NAME = os.environ['NODE_NAME']
client = Client((NODE_NAME, 5000))
client.set('some_key', 'some_value')
result = client.get('some_key')