Helm 部署 Kafka:9093 SSL + 9092 PLAINTEXT 双端口(域名)集群外部访问
# Helm 部署 Kafka:9093 SSL + 9092 PLAINTEXT 双端口外部访问 > 适用于 Bitnami Kafka 3.9.x (KRaft 模式),EKS + AWS NLB 内网场景。 --- ## 架构概览 ``` 外部客户端 (EC2) │ ├── kafka-datacenter-qa-N.icbc.com:9092 ──► NLB (internal) ──► Pod:9092 CLIENT (PLAINTEXT) └── kafka-datacenter-qa-N.icbc.com:9093 ──► NLB (internal) ──► Pod:9093 EXTERNAL (SSL) │ 集群内 broker 互联 (9095) ◄──┘ ``` | 监听器名称 | 端口 | 协议 | 用途 | |-----------|------|------|------| | CLIENT | 9092 | PLAINTEXT | 外部客户端非加密访问 | | EXTERNAL | 9093 | SSL | 外部客户端 TLS 加密访问 | | INTERNAL | 9095 | PLAINTEXT | Broker 间副本同步(集群内) | | CONTROLLER| 9094 | PLAINTEXT | KRaft Controller 选举 | --- ## 前置条件 ### 1. StorageClass ```bash kubectl get storageclass kafka-gp3 ``` 需要存在支持 `WaitForFirstConsumer` 的 gp3 StorageClass。 ### 2. TLS 证书 Secret ```bash # 证书文件:tls.crt(含证书链)、tls.key、ca.crt kubectl create secret generic kafka-datacenter-qa-tls \ --from-file=tls.crt=./kafka.crt \ --from-file=tls.key=./kafka.key \ --from-file=ca.crt=./ca.crt \ -n kafka-datacenter-qa ``` ### 3. DNS 解析 每个 Pod 对应一个域名,需在 Route53(或 DNS 服务商)创建指向 NLB 的记录: ``` kafka-datacenter-qa-0.icbc.com → NLB DNS kafka-datacenter-qa-1.icbc.com → NLB DNS kafka-datacenter-qa-2.icbc.com → NLB DNS ``` NLB 的 DNS 在 `externalAccess.controller.service.loadBalancerNames` 中预先指定, AWS Load Balancer Controller 会自动创建对应名称的 NLB。 --- ## values.yaml 核心配置说明 ### 监听器定义 ```yaml listeners: client: protocol: PLAINTEXT # 9092,不加密 interbroker: protocol: PLAINTEXT containerPort: 9095 # broker 内部副本同步 controller: protocol: PLAINTEXT containerPort: 9094 # KRaft controller external: protocol: SSL # 9093,TLS 加密 containerPort: 9093 tls: type: PEM existingSecret: kafka-datacenter-qa-tls # 提前创建好的证书 Secret ``` ### NLB 外部访问 ```yaml externalAccess: enabled: true autoDiscovery: enabled: false # 手动指定域名,不用 RBAC 自发现 controller: service: type: LoadBalancer ports: external: 9093 # SSL 主端口 extraPorts: - name: client-plaintext port: 9092 targetPort: 9092 protocol: TCP # 在同一个 NLB 上额外暴露 9092 loadBalancerNames: # 每个 Pod 独占一个 NLB(按 ordinal 顺序对应) - kafka-datacenter-qa-0.licbc.com - kafka-datacenter-qa-1.licbc.com - kafka-datacenter-qa-2.licbc.com annotations: service.beta.kubernetes.io/aws-load-balancer-type: "external" service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: "ip" service.beta.kubernetes.io/aws-load-balancer-scheme: "internal" # 内网 NLB ``` ### advertised.listeners 补丁(核心难点) **问题根源**:Bitnami `setup.sh` 在初始化时会将 CLIENT listener 的 advertised 地址写为 集群内 headless FQDN(`kafka-cluster-controller-N.kafka-cluster-controller-headless...`), 外部客户端无法解析这个地址,导致 bootstrap 成功但元数据返回后连接失败。 **解法**:在自定义 `command` 中,调用 `setup.sh` 之后直接覆写 `server.properties` 中的 `advertised.listeners` 行。 **关键坑**: 1. `/opt/bitnami/kafka/config/` 目录**只读**(无法创建新文件),但目录内已有文件可写。 - ❌ `mv /tmp/xxx server.properties`:`/tmp` 是 tmpfs,跨设备 mv 失败 - ❌ `> server.properties.new`:目录只读,无法创建新文件 - ✅ 把内容读入 shell 变量,再用 `>` 直接覆写已有文件(无需创建新文件) 2. `export KAFKA_CFG_ADVERTISED_LISTENERS` 必须在 `setup.sh` 之前设置,但 `setup.sh` 会在内部重新构造该变量,最终值仍然不对。因此必须在 `setup.sh` 之后再次覆写文件。 ```yaml controller: extraEnvVars: - name: MY_POD_NAME valueFrom: fieldRef: fieldPath: metadata.name command: - /bin/bash - -c - | ORDINAL="${MY_POD_NAME##*-}" DOMAIN="kafka-datacenter-qa-${ORDINAL}.icbc.com" INTERNAL_FQDN="kafka-cluster-controller-${ORDINAL}.kafka-cluster-controller-headless.kafka-datacenter-qa.svc.cluster.local" CONF_FILE="/opt/bitnami/kafka/config/server.properties" CORRECT_LISTENERS="CLIENT://${DOMAIN}:9092,INTERNAL://${INTERNAL_FQDN}:9095,EXTERNAL://${DOMAIN}:9093" export KAFKA_CFG_ADVERTISED_LISTENERS="${CORRECT_LISTENERS}" /opt/bitnami/scripts/kafka/setup.sh # 读入变量后直接覆写已有文件,目录只读但文件本身可写 FILTERED=$(grep -v '^advertised\.listeners=' "${CONF_FILE}") printf '%s\n' "${FILTERED}" > "${CONF_FILE}" echo "advertised.listeners=${CORRECT_LISTENERS}" >> "${CONF_FILE}" exec /opt/bitnami/kafka/bin/kafka-server-start.sh ${CONF_FILE} ``` --- ## 完整 values.yaml ```yaml global: storageClass: "kafka-gp3" security: allowInsecureImages: true image: registry: <YOUR_ECR_REGISTRY> repository: sretools tag: kafka-3.9.0 pullPolicy: Always controller: replicaCount: 3 automountServiceAccountToken: true nodeSelector: topology.kubernetes.io/zone: ap-northeast-1a resources: requests: cpu: "200m" memory: "6Gi" limits: cpu: "2" memory: "8Gi" persistence: enabled: true size: 300Gi storageClass: "kafka-gp3" heapOpts: "-Xmx4g -Xms4g -XX:+UseG1GC -XX:MaxGCPauseMillis=50 -XX:InitiatingHeapOccupancyPercent=30 -XX:G1HeapRegionSize=16m -XX:SoftMaxHeapSize=2560m -XX:MaxDirectMemorySize=512m -XX:+G1UseAdaptiveIHOP -XX:+ExplicitGCInvokesConcurrent -XX:+ParallelRefProcEnabled -Xlog:gc*:stdout:time,tags" containerPorts: client: 9092 controller: 9094 extraEnvVars: - name: MY_POD_NAME valueFrom: fieldRef: fieldPath: metadata.name command: - /bin/bash - -c - | ORDINAL="${MY_POD_NAME##*-}" DOMAIN="kafka-datacenter-qa-${ORDINAL}.icbc.com" INTERNAL_FQDN="kafka-cluster-controller-${ORDINAL}.kafka-cluster-controller-headless.kafka-datacenter-qa.svc.cluster.local" CONF_FILE="/opt/bitnami/kafka/config/server.properties" CORRECT_LISTENERS="CLIENT://${DOMAIN}:9092,INTERNAL://${INTERNAL_FQDN}:9095,EXTERNAL://${DOMAIN}:9093" export KAFKA_CFG_ADVERTISED_LISTENERS="${CORRECT_LISTENERS}" /opt/bitnami/scripts/kafka/setup.sh FILTERED=$(grep -v '^advertised\.listeners=' "${CONF_FILE}") printf '%s\n' "${FILTERED}" > "${CONF_FILE}" echo "advertised.listeners=${CORRECT_LISTENERS}" >> "${CONF_FILE}" exec /opt/bitnami/kafka/bin/kafka-server-start.sh ${CONF_FILE} broker: replicaCount: 0 automountServiceAccountToken: true listeners: client: protocol: PLAINTEXT interbroker: protocol: PLAINTEXT containerPort: 9095 controller: protocol: PLAINTEXT containerPort: 9094 external: protocol: SSL containerPort: 9093 tls: type: PEM existingSecret: kafka-datacenter-qa-tls externalAccess: enabled: true autoDiscovery: enabled: false controller: service: type: LoadBalancer ports: external: 9093 extraPorts: - name: client-plaintext port: 9092 targetPort: 9092 protocol: TCP loadBalancerNames: - kafka-datacenter-qa-0.licbc.com - kafka-datacenter-qa-1.licbc.com - kafka-datacenter-qa-2.licbc.com annotations: service.beta.kubernetes.io/aws-load-balancer-type: "external" service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: "ip" service.beta.kubernetes.io/aws-load-balancer-scheme: "internal" serviceAccount: create: true service: type: ClusterIP ports: client: 9092 controller: 9094 interbroker: 9095 extraConfig: | num.network.threads=2 num.io.threads=8 socket.send.buffer.bytes=-1 socket.receive.buffer.bytes=-1 socket.request.max.bytes=157286400 queued.max.requests=1000 unclean.leader.election.enable=false replica.lag.time.max.ms=45000 replica.fetch.max.bytes=16777216 num.partitions=3 default.replication.factor=2 min.insync.replicas=2 offsets.topic.replication.factor=3 transaction.state.log.replication.factor=3 transaction.state.log.min.isr=2 num.recovery.threads.per.data.dir=1 log.retention.check.interval.ms=1800000 auto.leader.rebalance.enable=false group.initial.rebalance.delay.ms=5000 compression.type=producer log.retention.hours=72 log.segment.bytes=268435456 log.cleanup.policy=delete auto.create.topics.enable=false delete.topic.enable=true controlled.shutdown.enable=true ssl.client.auth=none sasl: client: users: [] rbac: create: true metrics: jmx: enabled: true kafkaJmxPort: 9999 kafka: enabled: true serviceMonitor: enabled: true namespace: "kafka-datacenter-qa" labels: release: kube-prom-stack ``` --- ## 部署流程 ```bash # 1. 创建 namespace kubectl create namespace kafka-datacenter-qa # 2. 创建 TLS Secret kubectl create secret generic kafka-datacenter-qa-tls \ --from-file=tls.crt=./kafka.crt \ --from-file=tls.key=./kafka.key \ --from-file=ca.crt=./ca.crt \ -n kafka-datacenter-qa # 3. 部署 helm upgrade --install kafka-cluster \ oci://registry-1.docker.io/bitnamicharts/kafka \ --version 31.x.x \ -n kafka-datacenter-qa \ -f values.yaml # 4. 等待 StatefulSet 就绪 kubectl rollout status statefulset -n kafka-datacenter-qa kafka-cluster-controller # 5. 验证 advertised.listeners 是否正确 kubectl logs -n kafka-datacenter-qa kafka-cluster-controller-0 | grep '\[final\]' kubectl logs -n kafka-datacenter-qa kafka-cluster-controller-1 | grep '\[final\]' kubectl logs -n kafka-datacenter-qa kafka-cluster-controller-2 | grep '\[final\]' ``` 预期输出: ``` [final] advertised.listeners=CLIENT://kafka-datacenter-qa-0.licbc.com:9092,INTERNAL://...headless...:9095,EXTERNAL://kafka-datacenter-qa-0.licbc.com:9093 ``` --- ## 验证连接 ### PLAINTEXT 9092 ```bash kafka-topics.sh \ --bootstrap-server kafka-datacenter-qa-0.icbc.com:9092 \ --list ``` ### SSL 9093 ```bash kafka-topics.sh \ --bootstrap-server kafka-datacenter-qa-0.licbc.com:9093 \ --command-config client-ssl.properties \ --list ``` `client-ssl.properties` 示例: ```properties security.protocol=SSL ssl.truststore.location=/path/to/kafka.truststore.jks ssl.truststore.password=changeit ``` ### kcat 快速验证 ```bash # PLAINTEXT kcat -b kafka-datacenter-qa-0.icbc.com:9092 -L # SSL kcat -b kafka-datacenter-qa-0.icbc.com:9093 \ -X security.protocol=SSL \ -X ssl.ca.location=/path/to/ca.crt \ -L ``` --- ## 故障排查 ### 症状:bootstrap 成功但 9092 连接失败(UnknownHostException) ``` Error connecting to node kafka-cluster-controller-N.kafka-cluster-controller-headless...svc.cluster.local:9092 ``` **原因**:`advertised.listeners` 中 CLIENT 地址仍为集群内 headless FQDN,外部客户端无法解析。 **排查**: ```bash kubectl logs -n kafka-datacenter-qa kafka-cluster-controller-2 | grep '\[final\]' kubectl exec -n kafka-datacenter-qa kafka-cluster-controller-2 -- \ grep 'advertised.listeners' /opt/bitnami/kafka/config/server.properties ``` **修复**:确认 `command` 中的文件覆写步骤执行成功(无 `Read-only file system` 报错)。 ### 症状:command 中无法创建临时文件 ``` /bin/bash: /opt/bitnami/kafka/config/server.properties.new: Read-only file system ``` **原因**:`/opt/bitnami/kafka/config/` 目录本身只读,无法创建新文件,但已有文件可写。 **错误方案**: - `mv /tmp/xxx ${CONF_FILE}` — `/tmp` 是 tmpfs,跨设备 mv 报 `Invalid cross-device link` - `> ${CONF_FILE}.new` — 创建新文件报 `Read-only file system` **正确方案**:先读入变量,再用 `>` 覆写已有文件: ```bash FILTERED=$(grep -v '^advertised\.listeners=' "${CONF_FILE}") printf '%s\n' "${FILTERED}" > "${CONF_FILE}" echo "advertised.listeners=${CORRECT_LISTENERS}" >> "${CONF_FILE}" ``` ### 症状:setup.sh 调用后 KAFKA_CFG_ADVERTISED_LISTENERS 被覆盖 `export KAFKA_CFG_ADVERTISED_LISTENERS` 在 `setup.sh` 之前设置无效,因为 `setup.sh` 会根据 chart 配置重新生成该值。必须在 `setup.sh` 完成后再修改 `server.properties`。 --- ## 注意事项 - `ssl.client.auth=none`:9093 只做传输加密,不要求客户端提供证书(单向 TLS)。 如需双向认证,改为 `ssl.client.auth=required`。 - NLB 按 Pod ordinal 顺序一一对应,`loadBalancerNames` 列表顺序不能打乱。 - `autoDiscovery.enabled: false` 时无需给 ServiceAccount 赋予 `list services` 权限。 - KRaft 模式下 `broker.replicaCount: 0`,controller 节点同时承担 broker 角色。
http://www.cnblogs.com/Jame-mei
浙公网安备 33010602011771号