Implementa Memcached en GKE


Con este instructivo, aprenderás a implementar un clúster de servidores distribuidos de Memcached en Google Kubernetes Engine (GKE) con Kubernetes, Helm y Mcrouter. Memcached es un sistema de almacenamiento en caché de código abierto y multifuncional de uso muy extendido. Suele funcionar como un almacén temporal de datos que se usan con frecuencia a fin de acelerar las aplicaciones web y aligerar las cargas de bases de datos.

Características de Memcached

Memcached tiene dos objetivos de diseño principales:

  • Simplicidad: Memcached funciona como una gran tabla hash y ofrece una API simple con la que puedes almacenar y recuperar objetos de formas arbitrarias por medio de claves.
  • Rapidez: Memcached conserva datos almacenados en caché exclusivamente en la memoria RAM, lo que hace que el acceso a los datos sea extremadamente rápido.

Memcached es un sistema distribuido que permite a su tabla hash la capacidad de escalar horizontalmente a través de un grupo de servidores. Cada servidor de Memcached opera de forma completamente aislada de los otros servidores del grupo. Por lo tanto, el enrutamiento y el balanceo de cargas entre los servidores debe realizarse a nivel del cliente. Los clientes de Memcached aplican un esquema de hashing coherente para seleccionar los servidores meta de forma adecuada. Este esquema garantiza las siguientes condiciones:

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

En el diagrama siguiente, se muestra un alto nivel de interacción entre un cliente de Memcached y un grupo distribuido de servidores de Memcached.

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

Objetivos

  • Aprender acerca de algunas características de la arquitectura distribuida de Memcached
  • Implementar un servicio de Memcached en GKE con Kubernetes y Helm
  • Implementar Mcrouter, un proxy de código abierto de Memcached, para mejorar el rendimiento del sistema

Costos

En este documento, usarás los siguientes componentes facturables de Google Cloud:

  • Compute Engine

Para generar una estimación de costos en función del uso previsto, usa la calculadora de precios. Es posible que los usuarios nuevos de Google Cloud califiquen para obtener una prueba gratuita.

Antes de comenzar

  1. Accede a tu cuenta de Google Cloud. Si eres nuevo en Google Cloud, crea una cuenta para evaluar el rendimiento de nuestros productos en situaciones reales. Los clientes nuevos también obtienen $300 en créditos gratuitos para ejecutar, probar y, además, implementar cargas de trabajo.
  2. En la página del selector de proyectos de la consola de Google Cloud, selecciona o crea un proyecto de Google Cloud.

    Ir al selector de proyectos

  3. Asegúrate de que la facturación esté habilitada para tu proyecto de Google Cloud.

  4. Habilita las API de Compute Engine and GKE.

    Habilita las API

  5. En la página del selector de proyectos de la consola de Google Cloud, selecciona o crea un proyecto de Google Cloud.

    Ir al selector de proyectos

  6. Asegúrate de que la facturación esté habilitada para tu proyecto de Google Cloud.

  7. Habilita las API de Compute Engine and GKE.

    Habilita las API

  8. Inicia una instancia de Cloud Shell
    Abre Cloud Shell

Implemente un servicio de Memcached

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

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

    gcloud container clusters create demo-cluster --num-nodes 3 --zone us-central1-f
    
  2. Descarga el archivo binario 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. Agrega el directorio binario helm a la variable de entorno PATH:

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

    Este comando permite que el archivo binario helm se pueda descubrir desde cualquier directorio durante la sesión de Cloud Shell actual. Para que esta configuración persista en varias sesiones, agrega el comando al archivo ~/.bashrc del usuario de Cloud Shell.

  5. Instala una nueva actualización del gráfico de Helm para 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 para Memcached usa un controlador StatefulSet. Uno de los beneficios de usar este controlador es que los nombres de los pods están ordenados y son predecibles. En este caso, los nombres son mycache-memcached-{0..2}. Este orden facilita que los clientes de Memcached hagan referencia a los servidores.

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

    kubectl get pods
    

    El resultado de la consola de Google Cloud se ve así:

    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

Descubra extremos de servicio de Memcached

El gráfico de Helm para Memcached usa Headless service. Este expone las direcciones IP para todos sus pods de modo que puedan detectarse individualmente.

  1. Verifica que el servicio implementado no tenga interfaz gráfica:

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

    Con la salida None, confirmas que el servicio no tiene clusterIP y que, por lo tanto, no tiene interfaz gráfica.

    El servicio crea un registro DNS para el nombre del host con la forma siguiente:

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

    En este instructivo, el nombre del servicio es mycache-memcached. Debido a que no se definió de forma explícita un espacio de nombres, se usa el espacio de nombres predeterminado y, por lo tanto, el nombre de host completo es mycache-memcached.default.svc.cluster.local. El nombre del host se resuelve como un conjunto de direcciones IP y dominios para los tres pods expuestos por el servicio. Si luego se agregan algunos pods al grupo o se quitan los más antiguos, kube-dns actualizará de forma automática el registro DNS.

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

  2. Recupera las direcciones IP de los extremos:

    kubectl get endpoints mycache-memcached
    

    La salida es similar a esta:

    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 diferente: 10.36.0.32, 10.36.0.33 y 10.36.1.25 respectivamente. Estas direcciones IP pueden variar para tus propias instancias de servidor. Cada pod queda a la escucha del puerto 11211, que es el puerto predeterminado de Memcached.

  3. Como alternativa al paso 2, puedes llevar a cabo una inspección de DNS mediante un lenguaje de programación como Python:

    1. Inicie una consola interactiva de Python dentro de su 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()
      

      La salida es similar a esta:

      ('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. Para probar la implementación, abre una sesión telnet con uno de los servidores de 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 el símbolo del sistema de telnet, ejecuta estos comandos mediante el protocolo ASCII de Memcached:

    set mykey 0 0 5
    hello
    get mykey
    quit

    La salida 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 detección del servicio

Ya puedes implementar la lógica básica de descubrimiento de servicios que se muestra en el diagrama siguiente.

Lógica de descubrimiento de servicios
Figura 2: Lógica de descubrimiento de servicios.

A un alto nivel, la lógica de descubrimiento de servicios consiste en los pasos siguientes:

  1. La aplicación consulta kube-dns para el registro DNS de mycache-memcached.default.svc.cluster.local.
  2. La aplicación recupera las direcciones IP asociadas con ese registro.
  3. La aplicación ejemplifica un nuevo cliente de Memcached y le proporciona la dirección IP recuperada.
  4. El balanceador de cargas integrado del cliente de Memcached se conecta con los servidores de Memcached en las direcciones IP proporcionadas.

Ahora vamos a implementar esta lógica de detección de servicio usando Python:

  1. Implementa un pod habilitado para Python nuevo en tu clúster e inicia una sesión de shell dentro del pod:

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

    pip install pymemcache
    
  3. Inicia una consola interactiva de Python con el comando de 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 representa un literal de bytes, que es el formato en el que Memcached almacena los datos.

  5. Cierra la consola de Python:

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

Habilita la agrupación de conexiones

A medida que tus necesidades de almacenamiento en caché crezcan, y el grupo escale a decenas, cientos o miles de servidores de Memcached, puedes toparte con algunas limitaciones. En particular, el gran número de conexiones abiertas desde clientes de Memcached puede representar una gran carga para los servidores, como se muestra en el diagrama siguiente.

Número alto de conexiones abiertas cuando todos los clientes de Memcached acceden a todos los servidores de Memcached directamente
Figura 3: Alto número de conexiones abiertas cuando todos los clientes de Memcached acceden a los servidores de Memcached directamente.

A fin de reducir el número de conexiones abiertas, debes ingresar un proxy para permitir la reducción de conexiones, como se muestra en el diagrama siguiente.

Proxy para permitir la reducción de conexiones.
Figura 4: Usa un proxy para reducir el número de conexiones abiertas.

Mcrouter, un potente proxy de código abierto de Memcached, habilita la reducción de conexiones. Integrar Mcrouter resulta sencillo porque usa el protocolo ASCII estándar de Memcached. Para un cliente de Memcached, Mcrouter se comporta como el servidor normal de Memcached. Para un servidor de Memcached, Mcrouter se comporta como un cliente normal de Memcached.

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

  1. Borra la versión del gráfico de Helm mycache instalada antes:

    helm delete mycache
    
  2. Implementa pods de Memcached y de Mcrouter nuevos mediante la instalación de una versión nueva del gráfico de Helm para Mcrouter.

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

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

  3. A fin de probar esta configuración, conéctate a uno de los pods del 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 de telnet, ejecuta estos comandos:

    set anotherkey 0 0 15
    Mcrouter is fun
    get anotherkey
    quit

    Los comandos configuran y reproducen el valor de tu clave.

Ya has implementado un proxy que habilita la agrupación de conexiones.

Reducir la latencia

Para aumentar la resiliencia, se suele usar un clúster con varios nodos. En este instructivo, se usa un clúster con tres nodos. Sin embargo, usar varios nodos también acarrea el riesgo de una creciente latencia a causa del mayor tráfico de red entre los nodos.

Colocar pods del proxy

Puedes reducir este riesgo si conectas los pods de aplicaciones cliente solo a un pod proxy de Memcached que se encuentra en el mismo nodo. En el diagrama siguiente, se muestra esta configuración.

Topología de las interacciones entre los pods
Figura 5: Topología de las interacciones entre los pods de las aplicaciones, los pods de Mcrouter y los pods de Memcached en un clúster de tres nodos.

Aplica esta configuración como se indica a continuación:

  1. Asegúrate de que cada nodo contenga un pod del proxy en ejecución. Un enfoque común consiste en implementar los pods del proxy con un controlador DaemonSet. A medida que se agregan los nodos al clúster, se les incorporan nuevos pods del proxy automáticamente. A medida que se quitan los nodos del clúster, los pods se recolectan como elementos no utilizados. En este instructivo, en el gráfico de Helm para Mcrouter que implementaste antes se usa un controlador DaemonSet de forma predeterminada. Por lo tanto, este paso ya está completo.
  2. Establece un valor hostPort en los parámetros de Kubernetes del contenedor del proxy para que el nodo quede a la escucha de ese puerto y redireccione el tráfico al proxy. En este instructivo, en el gráfico de Helm para Mcrouter se usa este parámetro de forma predeterminada para el puerto 5000. Por lo tanto, este paso ya está completo.
  3. Expón el nombre del nodo como una variable de entorno dentro de los pods de la aplicación mediante el uso de la entrada spec.env y la selección del valor spec.nodeName fieldRef. Consulta más información sobre este método en la documentación de Kubernetes.

    1. Implementa algunos pods de aplicación de muestra:

      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. Para verificar que el nombre del nodo esté expuesto, mira dentro de los pods de la aplicación de muestra:

    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 da como resultado el nombre del nodo con la forma siguiente:

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

Conecta los pods

A partir de ahora, los pods de la aplicación de muestra 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. A fin de iniciar una conexión para uno de los pods, abre 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 de telnet, ejecuta estos comandos:

    get anotherkey
    quit
    

    Salida:

    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 mediante la recuperación de la variable NODE_NAME del entorno y el uso de 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')

Limpia

Para evitar que se apliquen cargos a tu cuenta de Google Cloud por los recursos usados en este instructivo, borra el proyecto que contiene los recursos o conserva el proyecto y borra los recursos individuales.

  1. Ejecuta el siguiente comando para borrar el clúster de GKE:

    gcloud container clusters delete demo-cluster --zone us-central1-f
    
  2. De forma opcional, puedes borrar el archivo binario de Helm:

    cd ~
    rm -rf helm-v3.7.1
    rm helm-v3.7.1-linux-amd64.tar.gz
    

Pasos siguientes

  • Explora las muchas otras características que ofrece Mcrouter más allá de la simple agrupación de conexiones, como las réplicas de conmutación por error, la transmisión de borrado confiable, el precalentamiento de memorias caché frías y la emisión de varios clústeres.
  • Explora los archivos fuente del gráfico de Memcached y el gráfico de Mcrouter si quieres obtener más información acerca de las opciones de configuración respectivas de Kubernetes.
  • Lee acerca de las técnicas efectivas para usar Memcached en App Engine. Algunas de ellas también se aplican a otras plataformas, como GKE.