apache kafka - kafka - Cómo exponer un servicio sin cabeza para un StatefulSet externamente en Kubernetes
scale statefulset (5)
Usando kubernetes-kafka como punto de partida con minikube.
Esto utiliza un StatefulSet y un servicio sin cabeza para el descubrimiento del servicio dentro del clúster.
El objetivo es exponer a los Corredores Kafka individuales de forma externa que se tratan internamente como:
kafka-0.broker.kafka.svc.cluster.local:9092
kafka-1.broker.kafka.svc.cluster.local:9092
kafka-2.broker.kafka.svc.cluster.local:9092
La restricción es que este servicio externo pueda dirigirse a los corredores específicamente.
¿Cuál es la forma correcta (o posible) de hacer esto? ¿Es posible exponer un servicio externo por kafka-x.broker.kafka.svc.cluster.local:9092
?
Cambie el servicio de un ClusterIP sin cabeza a un NodePort que reenviaría la solicitud a cualquiera de los nodos en un puerto establecido (30092 en mi ejemplo) al puerto 9042 en Kafkas. Golpearías una de las cápsulas al azar, pero supongo que eso está bien.
20dns.yml se convierte (algo como esto):
# A no longer headless service to create DNS records
---
apiVersion: v1
kind: Service
metadata:
name: broker
namespace: kafka
spec:
type: NodePort
ports:
- port: 9092
- nodePort: 30092
# [podname].broker.kafka.svc.cluster.local
selector:
app: kafka
Descargo de responsabilidad: Es posible que necesite dos servicios. Uno sin cabeza para los nombres dns internos y un NodePort para el acceso externo. No he intentado esto yo mismo.
De la documentación kubernetes kafka :
Acceso exterior con hostport
Una alternativa es utilizar el puerto de host para el acceso externo. Al usar esto, solo un corredor kafka puede ejecutar en cada host, lo cual es una buena idea de todos modos.
Para cambiar a hostport, la dirección de publicidad de kafka debe cambiarse a ExternalIP o ExternalDNS name del nodo que ejecuta el agente. en kafka / 10broker-config.yml cambia a
OUTSIDE_HOST=$(kubectl get node "$NODE_NAME" -o jsonpath=''{.status.addresses[?(@.type=="ExternalIP")].address}'') OUTSIDE_PORT=${OutsidePort}
y en kafka / 50kafka.yml agregue el hostport:
- name: outside containerPort: 9094 hostPort: 9094
Las soluciones hasta ahora no me satisfacían lo suficiente, así que voy a publicar una respuesta mía. Mis metas:
- Los pods aún deben gestionarse dinámicamente a través de un StatefulSet tanto como sea posible.
- Cree un servicio externo por Pod (es decir, Kafka Broker) para clientes productores / consumidores y evite el equilibrio de carga.
- Cree un servicio interno sin cabeza para que cada Broker pueda comunicarse entre sí.
Comenzando con kubernetes-kafka , lo único que falta es exponer el servicio externamente y dos desafíos al hacerlo.
- Generando etiquetas únicas por cada pod de Broker para que podamos crear un servicio externo para cada uno de los pods de Broker.
- Pidiéndoles a los Brokers que se comuniquen entre ellos usando el Servicio interno mientras configuran Kafka para decirle al productor / consumidores que se comuniquen a través del Servicio externo.
Etiquetas por pod y servicios externos:
Para generar etiquetas por pod, este problema fue realmente útil. Usándolo como una guía, agregamos la siguiente línea a la 10broker-config.yml init.sh
con:
kubectl label pods ${HOSTNAME} kafka-set-component=${HOSTNAME}
Mantenemos el servicio headless existente, pero también generamos un Servicio externo por pod utilizando la etiqueta (los agregué a 20dns.yml ):
apiVersion: v1
kind: Service
metadata:
name: broker-0
namespace: kafka
spec:
type: NodePort
ports:
- port: 9093
nodePort: 30093
selector:
kafka-set-component: kafka-0
Configurar Kafka con oyentes internos / externos
Encontré este problema increíblemente útil para tratar de entender cómo configurar Kafka.
Esto nuevamente requiere actualizar las propiedades init.sh
y server.properties
en 10broker-config.yml con lo siguiente:
Agregue lo siguiente a server.properties
para actualizar los protocolos de seguridad (actualmente utilizando PLAINTEXT
):
listener.security.protocol.map=INTERNAL_PLAINTEXT:PLAINTEXT,EXTERNAL_PLAINTEXT:PLAINTEXT
inter.broker.listener.name=INTERNAL_PLAINTEXT
Determine dinámicamente la IP externa y el puerto externo para cada Pod en el init.sh
:
EXTERNAL_LISTENER_IP=<your external addressable cluster ip>
EXTERNAL_LISTENER_PORT=$((30093 + ${HOSTNAME##*-}))
A continuación, configure los listeners
y las IP de advertised.listeners
para EXTERNAL_LISTENER
e INTERNAL_LISTENER
(también en la propiedad init.sh
):
sed -i "s/#listeners=PLAINTEXT:////:9092/listeners=INTERNAL_PLAINTEXT:////0.0.0.0:9092,EXTERNAL_PLAINTEXT:////0.0.0.0:9093/" /etc/kafka/server.properties
sed -i "s/#advertised.listeners=PLAINTEXT:////your.host.name:9092/advertised.listeners=INTERNAL_PLAINTEXT:////$HOSTNAME.broker.kafka.svc.cluster.local:9092,EXTERNAL_PLAINTEXT:////$EXTERNAL_LISTENER_IP:$EXTERNAL_LISTENER_PORT/" /etc/kafka/server.properties
Obviamente, esta no es una solución completa para la producción (por ejemplo, la seguridad para los corredores expuestos externamente) y todavía estoy mejorando mi comprensión de cómo permitir que los productores / consumidores internos también se comuniquen con los corredores.
Sin embargo, hasta ahora este es el mejor enfoque para mi comprensión de Kubernetes y Kafka.
Me gustaría decir que había leído esta pregunta y respuesta 3 veces antes de tratar de comprender qué eran los servicios sin cabeza y cuál era su objetivo. (y nunca entendí completamente Headless Services, o de qué se trataba esta pregunta y respuesta).
Y en la 4ª lectura (revisándola después de seguir educándome) finalmente hizo clic / finalmente entendí.
Entonces, el propósito de esta respuesta es replantear la pregunta / problema / de Nadir y responder como si se lo explicara a un estudiante de primaria. Para que otros que se topan con esto obtengan el significado de la asombrosa solución de Nadir en la primera lectura.
Conocimiento de fondo útil:
Existe un servicio de tipo: ExternalName.
El servicio ExternalName simplemente apunta a una dirección DNS.
Hay 2 sabores del servicio de ExternalName:- Sin una IP de clúster:
Un buen caso de uso sería permitir que un clúster de prueba y un clúster de producción compartan la mayor cantidad de código posible. (y por simple conveniencia en algunos casos) Los Pods tanto en las pruebas como en la producción apuntarían al mismo Nombre de la Dirección DNS del servicio, que sería el código reutilizable predecible. La diferencia sería que el entorno de prueba tendría un servicio que apunta a un servicio SQL que existe dentro del clúster. El clúster de producción usaría un Servicio de nombre externo, que redirigiría / apuntaría a la dirección DNS de una solución de SQL administrado de proveedores en la nube. - Con un IP de clúster:
Esta es la versión de un servicio de ExternalName que es clave para la solución.
- Sin una IP de clúster:
Un conjunto con estado tiene 3 partes a su identidad:
- Un ordinal
- Almacenamiento persistente
- Una IP persistente (pero esto no es predecible) <- Explica las necesidades para el Servicio sin cabeza
Hay 3 cosas importantes que recordar acerca de Kube-Proxy:
- Se asegura de que todo tenga una IP única.
- Es responsable de implementar las IP de Virtual Static Cluster (las IP de Virtual Static Cluster se consideran virtuales porque solo existen en cada nodo de iptables en la implementación de iptables de Kube-Proxy, o en una Kernel Hash Table en la versión de IP-vs next-gen de Kube-Proxy) y también es responsable del efecto de equilibrio de carga lógico que se produce con los servicios normales.
- KubeProxy es responsable de asignar el tráfico que entra en NodePorts a un servicio normal correspondiente. <- Esto es muy importante para el requisito de que los Servicios de Estado deben estar expuestos externamente, se supone que NodePorts siempre debe participar cuando se trata de exponer servicios externamente.
Hay 4 cosas importantes para recordar sobre un servicio sin cabeza:
- Crea una dirección DNS predecible.
- No actúa como un clúster interno de Load Balancer. Habla directamente con el pod identificado por la Dirección DNS predecible. (que es muy deseable para cargas de trabajo con estado)
- No tiene un IP de clúster estático.
- Como efecto secundario de las cualidades 2 y 3, está fuera del Reino de Kube-Proxy (que es responsable de dirigir el tráfico que ingresa en Puertos de nodo a Servicios). Parafrasearé esto algunas veces para que el problema se profundice: NodePorts puede Normalmente no reenvía el tráfico a Headless Services. El tráfico externo que ingresa al clúster generalmente no se puede reenviar a Headless Services. No es intuitivo cómo exponer externamente un servicio sin cabeza.
Por lo tanto, la pregunta original: ¿Cómo se puede exponer externamente un Servicio sin cabeza (que apunta a un miembro individual de un conjunto con estado)?
Antes de agregar el Servicio sin cabeza al Conjunto de estado, los pods kafka-0, kafka-1 y kafka-2 tendrían direcciones IP estáticas, pero no se podía predecir la dirección IP estática.
Un servicio sin cabeza proporciona un conjunto de estados de tiempo predecibles Direcciones de DNS del clúster interno (y el conjunto de estados no necesitaba un IP de clúster estático, porque ya tenía uno como parte de su identidad como un conjunto de estado)
kafka-0.broker.kafka.svc.cluster.local: 9092
kafka-1.broker.kafka.svc.cluster.local: 9092
kafka-2.broker.kafka.svc.cluster.local: 9092
Solución Parte 1:
Las direcciones DNS de clúster interno predecibles resuelven el acceso del clúster interno a los conjuntos de estado. (Esto permite que los pods en el conjunto con estado hablen entre sí, y permite que otros pods dentro del clúster tengan acceso a ellos).
Solución Parte 2:
Se necesita una segunda solución para resolver el acceso externo a los conjuntos / recursos con estado fuera del clúster, pudiendo acceder a los conjuntos con estado dentro del clúster.
Para cada pod en el Conjunto de Estado, se crea un Servicio de tipo ExternalName con una Dirección de ClusterIP Estática Virtual que es administrada por Kube-Proxy. Cada uno señala / redirige el tráfico a una dirección DNS del clúster interno estático predecible identificada en la Solución 1, y debido a que este servicio ExternalName tiene un IP del clúster estático virtual administrado a través de Kube-Proxy, puede haber una asignación de NodePorts a él.
Type=NodePort
esto en 1.7 cambiando el servicio sin cabeza a Type=NodePort
y configurando externalTrafficPolicy=Local
. Esto evita el equilibrio de carga interno de un Servicio y el tráfico destinado a un nodo específico en ese puerto de nodo solo funcionará si un pod de Kafka está en ese nodo.
apiVersion: v1
kind: Service
metadata:
name: broker
spec:
externalTrafficPolicy: Local
ports:
- nodePort: 30000
port: 30000
protocol: TCP
targetPort: 9092
selector:
app: broker
type: NodePort
Por ejemplo, tenemos dos nodos nodeA y nodeB, nodeB ejecuta un pod kafka. nodeA: 30000 no se conectará pero nodeB: 30000 se conectará al pod kafka que se ejecuta en nodeB.
https://kubernetes.io/docs/tutorials/services/source-ip/#source-ip-for-services-with-typenodeport
Tenga en cuenta que esto también estaba disponible en 1.5 y 1.6 como una anotación beta. Se puede encontrar más información aquí sobre disponibilidad de funciones: https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/#preserving-the-client-source-ip
Tenga en cuenta también que aunque esto vincula un pod kafka a una identidad de red externa específica, no garantiza que su volumen de almacenamiento esté vinculado a esa identidad de red. Si está utilizando VolumeClaimTemplates en un StatefulSet, entonces sus volúmenes están vinculados al pod, mientras que kafka espera que el volumen esté vinculado a la identidad de la red.
Por ejemplo, si el pod kafka-0 se reinicia y kafka-0 aparece en nodeC en lugar de nodeA, el pvc de kafka-0 (si usa VolumeClaimTemplates) tiene datos que son para nodeA y el broker que se ejecuta en kafka-0 comienza rechazando solicitudes pensando que es nodeA no nodeC.
Para solucionar este problema, esperamos volúmenes locales persistentes, pero en este momento tenemos un solo PVC para nuestro kafka StatefulSet y los datos se almacenan en $NODENAME
en ese PVC para vincular los datos del volumen a un nodo en particular.
https://github.com/kubernetes/features/issues/121 https://kubernetes.io/docs/concepts/storage/volumes/#local