如何在 Kubernetes 中对外公开 StatefulSet 的无头服务

Posted

技术标签:

【中文标题】如何在 Kubernetes 中对外公开 StatefulSet 的无头服务【英文标题】:How to expose a headless service for a StatefulSet externally in Kubernetes 【发布时间】:2018-03-09 10:27:07 【问题描述】:

使用kubernetes-kafka 作为 minikube 的起点。

这使用 StatefulSet 和 headless service 在集群内进行服务发现。

目标是在外部公开单个 Kafka 代理,内部地址为:

kafka-0.broker.kafka.svc.cluster.local:9092
kafka-1.broker.kafka.svc.cluster.local:9092 
kafka-2.broker.kafka.svc.cluster.local:9092

限制是这个外部服务能够专门处理代理。

解决此问题的正确(或一种可能)方法是什么?是否可以根据kafka-x.broker.kafka.svc.cluster.local:9092 公开外部服务?

【问题讨论】:

嗨,你认为这个解决方案可以在两者之间使用 nginx 更好地实现,满足你特别寻找的相同要求。你试过用 nginx 吗?你能告诉我你对此的看法吗? 【参考方案1】:

将服务从无头 ClusterIP 更改为 NodePort,它将请求转发到设置端口(在我的示例中为 30092)上的任何 节点 到 Kafka 上的端口 9042。你会随机击中其中一个吊舱,但我想这很好。

20dns.yml 变成(类似这样):

# 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

免责声明:您可能需要两项服务。一个用于内部 dns 名称的无头端口,一个用于外部访问的 NodePort。我自己没试过。

【讨论】:

就可以了。但是有一个重要的限制(我忘了澄清)试图访问代理的外部服务能够专门解决代理。从阅读来看,它在某些方面似乎与 Kubernetes 是对立的,但想知道是否有可能。即使这意味着为每个代理后端创建单独的服务。【参考方案2】:

我们在 1.7 中通过将无头服务更改为 Type=NodePort 并设置 externalTrafficPolicy=Local 解决了这个问题。这绕过了 Service 的内部负载平衡,并且只有在 Kafka pod 位于该节点上时,才能在该节点端口上发往特定节点的流量。

apiVersion: v1
kind: Service
metadata:
  name: broker
spec:
  externalTrafficPolicy: Local
  ports:
  - nodePort: 30000
    port: 30000
    protocol: TCP
    targetPort: 9092
  selector:
    app: broker
  type: NodePort

例如,我们有两个节点 nodeA 和 nodeB,nodeB 正在运行一个 kafka pod。 nodeA:30000 不会连接,但 nodeB:30000 会连接到 nodeB 上运行的 kafka pod。

https://kubernetes.io/docs/tutorials/services/source-ip/#source-ip-for-services-with-typenodeport

请注意,这在 1.5 和 1.6 中也可以作为 beta 注释使用,更多功能可在此处找到:https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/#preserving-the-client-source-ip

另请注意,虽然这会将 kafka pod 绑定到特定的外部网络身份,但并不能保证您的存储卷将绑定到该网络身份。如果您在 StatefulSet 中使用 VolumeClaimTemplates,那么您的卷与 pod 相关联,而 kafka 期望卷与网络身份相关联。

例如,如果 kafka-0 pod 重新启动并且 kafka-0 出现在 nodeC 而不是 nodeA 上,则 kafka-0 的 pvc(如果使用 VolumeClaimTemplates)具有用于 nodeA 的数据,并且在 kafka-0 上运行的代理启动拒绝认为它是 nodeA 而不是 nodeC 的请求。

为了解决这个问题,我们期待本地持久卷,但现在我们的 kafka StatefulSet 有一个 PVC,数据存储在该 PVC 上的$NODENAME 下,以将卷数据绑定到特定节点。

https://github.com/kubernetes/features/issues/121 https://kubernetes.io/docs/concepts/storage/volumes/#local

【讨论】:

这是否要求外部客户端知道给定代理在哪个节点上运行?那么一个 Pod,并且只有 Stateful 的 Pod 集可以在该节点上运行? 两个问题都是。我们在节点的一个子集上运行 Kafka,并使用节点标签匹配。我们还使用 pod 亲和性和反亲和性来确保只有一个 Kafka pod 登陆给定节点。 它不起作用...我已经尝试过了,还将app: broker 更改为app: kafka。在 GKE 上创建了防火墙规则,我得到了 failed: Operation timed out。有什么想法吗? 如何设置“advertised.listeners”,使其指向外部节点 IP?【参考方案3】:

到目前为止的解决方案对我来说还不够满意,所以我将发布我自己的答案。我的目标:

    仍应尽可能通过 StatefulSet 动态管理 Pod。 为生产者/消费者客户端为每个 Pod(即 Kafka 代理)创建一个外部服务并避免负载平衡。 创建内部无头服务,以便每个 Broker 可以相互通信。

从Yolean/kubernetes-kafka 开始,唯一缺少的就是将服务暴露在外部,这样做有两个挑战。

    为每个 Broker pod 生成唯一标签,以便我们可以为每个 Broker pod 创建一个外部服务。 告诉 Brokers 使用内部 Service 相互通信,同时配置 Kafka 告诉生产者/消费者通过外部 Service 进行通信。

每个 pod 标签和外部服务:

要为每个 pod 生成标签,this issue 真的很有帮助。使用它作为指导,我们将以下行添加到 10broker-config.yml init.sh 属性:

kubectl label pods $HOSTNAME kafka-set-component=$HOSTNAME

我们保留现有的无头服务,但我们还使用标签为每个 pod 生成一个外部服务(我将它们添加到 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

为 Kafka 配置内部/外部侦听器

我发现 this issue 在尝试了解如何配置 Kafka 时非常有用。

这再次需要使用以下内容更新10broker-config.yml 中的init.shserver.properties 属性:

将以下内容添加到server.properties 以更新安全协议(当前使用PLAINTEXT):

listener.security.protocol.map=INTERNAL_PLAINTEXT:PLAINTEXT,EXTERNAL_PLAINTEXT:PLAINTEXT
inter.broker.listener.name=INTERNAL_PLAINTEXT

动态确定init.sh中每个Pod的外部IP和外部端口:

EXTERNAL_LISTENER_IP=<your external addressable cluster ip>
EXTERNAL_LISTENER_PORT=$((30093 + $HOSTNAME##*-))

然后为EXTERNAL_LISTENERINTERNAL_LISTENER 配置listenersadvertised.listeners IP(也在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

显然,这不是生产的完整解决方案(例如解决外部暴露代理的安全问题),我仍在完善我对如何让内部生产者/消费者也与代理进行通信的理解。

但是,到目前为止,这是我了解 Kubernetes 和 Kafka 的最佳方法。

【讨论】:

替代方案可以是使用initContainer,将外部IP写入本地存储,然后kafka容器从中读取。 github.com/kow3ns/kubernetes-kafka/issues/3 如果我们扩大 pod,您是否找到了一种动态创建外部服务的方法。我试图在我们增加 pod 后实现自动化,然后应该自动创建外部服务【参考方案4】:

注意:我在最初发布一年后完全重写了这篇文章: 1. 鉴于 Kubernetes 的更新,我写的一些内容不再相关,我认为应该删除它以避免混淆人们。 2. 我现在对 Kubernetes 和 Kafka 都了解得更多了,应该可以做一个更好的解释。

Kafka on Kubernetes 的背景语境理解: 假设一个集群 IP 和有状态集类型的服务用于在 Kubernetes 集群上部署一个 5 pod Kafka 集群,因为有状态集用于创建 pod,它们每个都会自动获取以下 5 个内部集群 dns 名称,然后clusterIP 类型的 kafka 服务提供了另一个内部集群 dns 名称。

M$*  kafka-0.my-kafka-headless-service.my-namespace.svc.cluster.local 
M$   kafka-1.my-kafka-headless-service.my-namespace.svc.cluster.local 
M *  kafka-2.my-kafka-headless-service.my-namespace.svc.cluster.local 
M *  kafka-3.my-kafka-headless-service.my-namespace.svc.cluster.local 
M$   kafka-4.my-kafka-headless-service.my-namespace.svc.cluster.local
     kafka-service.my-namespace.svc.cluster.local

^ 假设您有 2 个 Kafka 主题:$ 和 * 每个 Kafka 主题在 5 个 pod Kafka 集群中复制 3 次 (上面的 ASCII 图显示了哪些 pod 保存了 $ 和 * 主题的副本,M 表示元数据)

4 点有用的背景知识: 1. .svc.cluster.local 是内部集群 DNS FQDN,但 Pod 会自动填充知识以自动完成,因此您可以在通过内部集群 DNS 交谈时省略它。 2. kafka-x.my-kafka-headless-service.my-namespace 内部集群DNS名称解析为单个pod。 3. kafka-service.my-namespace 集群 IP 类型的 kubernetes 服务就像一个内部集群第 4 层负载均衡器,将在 5 个 kafka pod 之间循环流量。 4. 要实现的一个关键 Kafka 特定概念是,当 Kafka 客户端与 Kafka 集群通信时,它分两个阶段进行。假设 Kafka 客户端想要从 Kafka 集群中读取 $ 主题。 阶段 1:客户端读取 kafka 集群元数据,这在所有 5 个 kafka pod 之间同步,因此客户端与哪个 pod 交谈并不重要,因此使用 kafka-service.my-namespace 进行初始通信可能很有用(LB 只转发到一个随机健康的 kafka pod) 第 2 阶段:元数据告诉 Kafka 客户端哪些 Kafka 代理/节点/服务器/pod 有感兴趣的主题,在这种情况下,$ 存在于 0、1 和 4。因此对于第 2 阶段,客户端只会直接与 Kafka 对话拥有所需数据的经纪人。

如何在外部公开无头服务/Statefulset 的 Pod 和 Kafka 特定的细微差别: 假设我在 Kubernetes 集群上启动了一个 3 pod HashiCorp Consul Cluster,我对其进行了配置,以便启用网页,并且我想从 LAN 中查看网页/从外部公开它。豆荚是无头的这一事实并没有什么特别之处。您可以使用 NodePort 或 LoadBalancer 类型的 Service 来公开它们,就像您通常对任何 pod 一样,NP 或 LB 将在 3 个 consul pod 之间循环 LB 传入流量。

因为 Kafka 通信分两个阶段进行,这引入了一些细微差别,当您的 Kafka 集群包含超过 1 个 Kafka pod . 1. Kafka 客户端希望在第 2 阶段通信期间直接与 Kafka Broker 对话。因此,您可能需要 6 个 NodePort/LB 类型的服务,而不是 1 个 NodePort 类型的服务。 1 将轮询第 1 阶段的 LB 流量,5 将 1:1 映射到各个 pod 以进行第 2 阶段通信。 (如果您对 5 个 Kafka pod 运行 kubectl get pods --show-labels,您会看到有状态集的每个 pod 都有一个唯一的标签 statefulset.kubernetes.io/pod-name=kafka-0 ,并且允许您手动创建 1 个 NP/LB 服务,该服务映射到有状态集的 1 个 pod。)(请注意,仅此还不够) 2. 当您在 Kubernetes 上安装 Kafka 集群时,其默认配置通常只支持 Kubernetes 集群内的 Kafka 客户端。请记住,来自 Kafka 客户端阶段 1 的元数据与 Kafka 集群通信,以及 kafka 集群可能已配置为它的“广告侦听器”由内部集群 DNS 名称组成。因此,当 LAN 客户端通过 NP/LB 与外部暴露的 Kafka 集群通信时,它在阶段 1 成功,但在阶段 2 失败,因为阶段 1 返回的元数据提供了内部集群 DNS 名称作为阶段期间直接与 pod 通信的方式2 通信,集群外的客户端无法解析,因此仅适用于集群内的 Kafka 客户端。因此,配置您的 kafka 集群非常重要,以便第 1 阶段元数据返回的“advertised.listeners”可由集群外部和集群内部的客户端解析。

明确 Kafka Nuance 引起的问题出在哪里: 对于 Kafka Client -> Broker 之间的通信阶段 2,您需要将“advertised.listeners”配置为可外部解析。 使用标准 Kubernetes 逻辑很难实现这一点,因为您需要的是 kafka-0 ... kafka-4 每个都有一个独特的配置/每个都有一个可以从外部访问的独特的“advertised.listeners”。但默认情况下,有状态集意味着具有或多或少相同的千篇一律的配置。

Kafka Nuances 引起的问题的解决方案: Bitnami Kafka Helm Chart 有一些自定义逻辑,允许 statefulset 中的每个 pod 具有唯一的“advertised.listerners”配置。 Bitnami 提供加固容器,根据 Quay.io 2.5.0 只有一个 High CVE,以非 root 运行,有合理的文档,可以对外暴露*,https://quay.io/repository/bitnami/kafka?tab=tags

我参与的最后一个项目是使用 Bitnami,因为安全性是首要任务,而且我们只有 kubernetes 集群内部的 kafka 客户端,我最终不得不弄清楚如何在开发环境中将其从外部公开,所以有人可以运行某种测试,我记得能够让它工作,我还记得这不是超级简单,如果我要在 Kubernetes 项目上做另一个 Kafka,我建议研究 Strimzi Kafka Operator ,因为它在外部暴露 Kafka 的选项方面更加灵活,并且它有一个很棒的 5 部分深入研究写了不同的选项,用于使用 Strimzi(通过 NP、LB 或 Ingress)外部暴露在 Kubernetes 上运行的 Kafka 集群(I虽然不确定 Strimzi 的安全性是什么样的,所以我建议在尝试 PoC 之前使用 AnchorCLI 之类的东西对 Strimzi 图像进行左移 CVE 扫描) https://strimzi.io/blog/2019/04/17/accessing-kafka-part-1/

【讨论】:

kubectl 端口转发呢?可以用吗? 我认为 kubectl 端口转发根本不起作用,我强烈建议阅读这篇深入研究的文章以更好地理解问题。 developers.redhat.com/blog/2019/06/06/… 现在我知道 kafka 和 kubernetes 更好,而且 kafka 是一个边缘案例,我将大量编辑我的答案以更清楚。 @wwjdm 我重写了我的整个帖子,由于第二个细微差别,我认为 kubectl 端口转发不会起作用。【参考方案5】:

来自kubernetes kafka documentation:

通过主机端口进行外部访问

另一种方法是使用主机端口进行外部访问。什么时候 使用这个只有一个 kafka 代理可以在每个主机上运行,​​这是一个很好的 还是有想法的。

为了切换到主机端口,需要 kafka 广告地址 切换到运行节点的 ExternalIP 或 ExternalDNS 名称 经纪人。在 kafka/10broker-config.yml 切换到

OUTSIDE_HOST=$(kubectl get node "$NODE_NAME" -o jsonpath='.status.addresses[?(@.type=="ExternalIP")].address')
OUTSIDE_PORT=$OutsidePort

并在 kafka/50kafka.yml 添加主机端口:

    - name: outside
      containerPort: 9094
      hostPort: 9094

【讨论】:

【参考方案6】:

我通过为每个代理创建单独的 statefulset 并为每个代理创建单独的 NodePort 类型的服务来解决这个问题。内部通信可以发生在每个单独的服务名称上。外部通信可以发生在 NodePort 地址上。

【讨论】:

以上是关于如何在 Kubernetes 中对外公开 StatefulSet 的无头服务的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Kubernetes 中使用负载均衡器服务公开多个端口

使用 nginx 反向代理在 Kubernetes 中公开服务

如何在kubernetes上构建kafka集群后公开kafka以进行外部访问?

从Mesos转向Kubernetes,美国最大点评网站Yelp,开源Clusterman集群系统

从 Kubernetes 部署中公开 SCDF 服务

如何使用 Terraform 公开具有公共 IP 地址的 Azure Kubernetes 集群