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.
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:
Crea un clúster de GKE con tres nodos:
gcloud container clusters create demo-cluster --num-nodes 3 --location us-central1-f
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
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}
Añade el directorio del archivo binario
helm
a la variable de entornoPATH
: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.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.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.
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 tieneclusterIP
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 esmycache-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.
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
y10.36.1.25
. Estas direcciones IP pueden ser diferentes en tus instancias de servidor. Cada pod escucha el puerto11211
, que es el puerto predeterminado de Memcached.Como alternativa al paso 2, realiza una inspección de DNS con un lenguaje de programación como Python:
Inicia una consola interactiva de Python en tu clúster:
kubectl run -it --rm python --image=python:3.10-alpine --restart=Never python
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'])
Prueba la implementación abriendo una sesión
telnet
con uno de los servidores Memcached en ejecución en el puerto11211
: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.
A grandes rasgos, la lógica de detección de servicios consta de los siguientes pasos:
- La aplicación consulta
kube-dns
para obtener el registro DNS demycache-memcached.default.svc.cluster.local
. - La aplicación recupera las direcciones IP asociadas a ese registro.
- La aplicación crea una instancia de un nuevo cliente de Memcached y le proporciona las direcciones IP obtenidas.
- 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:
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
Instala la biblioteca
pymemcache
:pip install pymemcache
Inicia una consola interactiva de Python ejecutando el comando
python
.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.Sal de la consola de Python:
exit()
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.
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.
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.
Elimina la versión del gráfico de Helm
mycache
que se haya instalado previamente:helm delete mycache
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.
Prueba esta configuración conectándote a uno de los pods proxy. Usa el comando
telnet
en el puerto5000
, 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.
Para llevar a cabo esta configuración, sigue estos pasos:
- 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.
- 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 puerto5000
. Por lo tanto, este paso también está completado. 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 valorspec.nodeName
fieldRef
. Consulta más información sobre este método en la documentación de Kubernetes.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
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.
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'
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')