Kafka UI 与 AKHQ 部署、只读接入和消息排障工具手册
能打开页面,为什么仍然看不到 Topic
开发团队给共享 Kafka 加了一个 Web 控制台。页面能登录,集群卡片却不断转圈;换成 localhost:9092 后容器日志报连接失败;临时改成管理员账号,Topic 终于出现,但任何登录者也同时获得了删 Topic、改 retention、重置消费位点和查看业务消息的能力。
这不是一个前端页面故障,而是四条边界被混在了一起:
浏览器只连接控制台的 HTTP 端口。控制台作为 Kafka 客户端连接 bootstrap server,再根据元数据访问 broker 的 advertised listener。控制台自己的登录和角色,决定按钮是否可见、接口是否允许调用。
Kafka ACL、Schema Registry 凭据和网络策略,决定后台请求最终能否成功。
控制台不是 Kafka 的权限替代品。真正可信的只读链路必须同时满足“页面不提供写操作”和“Kafka 服务账号本身没有写权限”。只完成其中一层,最多减少误点,不能阻止绕过 UI 调接口或直接使用泄漏的凭据。
先把连接地址放到正确的网络里
bootstrap.servers 只是发现入口。控制台连接任一 bootstrap broker 后会取得集群元数据,然后改连元数据中公布的 broker 地址。因此,容器里的控制台不能把宿主机视角的 127.0.0.1:9092 当成 Kafka;127.0.0.1 在容器中指向控制台自己。
假设 Kafka 与控制台都在 Compose 网络 dev-messaging 中,Kafka 应向容器客户端公布诸如 kafka:19092 的内部 listener;宿主机 CLI 可以继续使用 127.0.0.1:9092 的外部 listener。开始部署前先从控制台将要运行的位置验证 DNS 和 TCP:
docker run --rm --network dev-messaging busybox:1.37 \
sh -c "nslookup kafka && nc -vz kafka 19092"正向证据应包含 kafka 的容器网络地址,以及 19092 连接成功。若 TCP 成功而页面日志随后尝试连接 localhost:9092,问题不在控制台的 bootstrap 配置,而在 Kafka 的 advertised.listeners。先用 Kafka CLI 查看 broker 配置和元数据,再修改 listener;反复更换控制台镜像不会修复错误的元数据地址。
共享集群还要先准备一个专用服务身份。它至少需要读取集群元数据、列出目标 Topic、描述 consumer group,并在确需查看消息时拥有目标 Topic 的读取权限。不要复用生产者、消费者或集群管理员凭据,因为控制台的故障半径会随该身份的权限一起扩大。
Kafbat UI:从单集群只读实例开始
常被简称为 Kafka UI 的开源项目已经由 Kafbat 团队继续维护。部署时应使用 Kafbat 仓库和 ghcr.io/kafbat/kafka-ui 镜像,而不是沿用无人确认的旧镜像坐标。项目的 Release 页面 给出可锁定的发行版本;下面以 v1.5.0 演示,升级时先在测试集群验证配置结构与权限行为,不使用漂移的 latest 作为团队基线。
用容器跑起本机只读实例
先创建独立目录,配置文件只保存非敏感连接结构:
tools/kafbat-ui/
├── compose.yaml
├── config.yml
└── .env.exampleconfig.yml:
kafka:
clusters:
- name: dev-local
bootstrap-servers: kafka:19092
read-only: true
polling-throttle-rate: 2
properties:
security.protocol: PLAINTEXT这些字段改变的不是页面文案,而是后台客户端行为:
bootstrap-servers 必须使用控制台容器可达的 listener。read-only: true 禁用 Kafbat UI 的写操作,但不构成 Kafka 侧授权。polling-throttle-rate 可降低界面高频轮询给共享集群带来的管理请求压力。
properties 接受 Kafka 客户端属性;认证、加密和超时应在这里显式配置。
Compose 文件把 HTTP 端口仅绑定到本机,并以只读方式挂载配置:
services:
kafbat-ui:
image: ghcr.io/kafbat/kafka-ui:v1.5.0
container_name: kafbat-ui
restart: unless-stopped
ports:
- "127.0.0.1:18080:8080"
environment:
SPRING_CONFIG_ADDITIONAL_LOCATION: /etc/kafkaui/config.yml
DYNAMIC_CONFIG_ENABLED: "false"
SWAGGER_UI_ENABLED: "false"
volumes:
- ./config.yml:/etc/kafkaui/config.yml:ro
networks:
- dev-messaging
healthcheck:
test: ["CMD", "wget", "-q", "-O", "-", "http://127.0.0.1:8080/actuator/health"]
interval: 10s
timeout: 3s
retries: 12
networks:
dev-messaging:
external: true启动并保留第一轮证据:
docker compose up -d
docker compose ps
docker compose logs --tail=120 kafbat-ui
curl -fsS http://127.0.0.1:18080/actuator/health健康端点返回 UP 只证明 Web 进程活着。随后打开 http://127.0.0.1:18080,确认 dev-local 集群能列出 broker、Topic 和 consumer group,才证明 Kafka 链路可用。Kafbat 的 配置文件说明 同时给出了 YAML、环境变量、SSL、客户端属性、只读开关和数据遮罩字段;团队应把生效配置纳入代码评审,而不是在页面里临时改完就失去记录。
接入 SASL/SSL 与 Schema Registry
当 Kafka 使用 SCRAM 和 TLS 时,将信任库以只读文件挂入容器,并把密码从环境或 Secret 管理器注入。配置骨架如下:
kafka:
clusters:
- name: shared-dev
bootstrap-servers: broker-a.dev.example:9093,broker-b.dev.example:9093
read-only: true
ssl:
truststore-location: /run/kafka-secrets/client.truststore.jks
truststore-password: ${KAFKA_TRUSTSTORE_PASSWORD}
verify-ssl: true
properties:
security.protocol: SASL_SSL
sasl.mechanism: SCRAM-SHA-512
sasl.jaas.config: >-
org.apache.kafka.common.security.scram.ScramLoginModule required
username="${KAFKA_UI_USERNAME}"
password="${KAFKA_UI_PASSWORD}";
schemaRegistry: https://schema.dev.example
schema-registry-auth:
username: ${SCHEMA_REGISTRY_USERNAME}
password: ${SCHEMA_REGISTRY_PASSWORD}属性名会随 Kafbat UI 发行线演进,升级时应对照 完整配置结构 检查,而不是照搬旧博客。尤其要区分三类失败:
UnknownHostException 或连接超时通常指向 DNS、路由、listener 或防火墙。SSLHandshakeException 通常指向信任链、主机名校验、证书有效性或协议不一致。SaslAuthenticationException 指向机制、用户名、密码或 JAAS 字符串,而不是网络。
Schema Registry 是另一套 HTTP 身份和 TLS 边界。Kafka 认证成功并不意味着 Schema Registry 可用;Registry 认证失败时,Topic 仍可能可见,但 Avro、JSON Schema 或 Protobuf 消息无法正确反序列化。不要为了“看见内容”关闭 TLS 校验或把管理员凭据塞进 Git。
页面只读还要经过 Kafka ACL 反证
Kafbat UI 支持 read-only,也支持更细的认证、RBAC 和数据遮罩。共享实例应先接入 UI 认证,再按 RBAC 资源和动作 将 Topic、consumer、schema、ACL 和应用配置分开授权。
本地 LOGIN_FORM 只支持一个用户,并且与 RBAC 不兼容,适合临时单人实例,不适合拿来伪装团队权限体系。团队实例使用 OIDC/LDAP,让身份提供方的 group 或 role 进入 Kafbat,再绑定只读角色。以下骨架省略了具体提供方地址,值由 Secret 注入:
auth:
type: OAUTH2
oauth2:
client:
team_oidc:
clientId: ${KAFBAT_OIDC_CLIENT_ID}
clientSecret: ${KAFBAT_OIDC_CLIENT_SECRET}
scope: ["openid", "profile", "email", "groups"]
provider: team_oidc
issuer-uri: https://id.example/realms/engineering
user-name-attribute: preferred_username
custom-params:
type: oauth
roles-field: groups
rbac:
roles:
- name: kafka-observers
clusters: ["shared-dev"]
subjects:
- provider: oauth_team_oidc
type: group
value: kafka-observers
permissions:
- resource: clusterconfig
actions: ["view"]
- resource: topic
value: "project-a\\..*"
actions: ["view"]
- resource: consumer
value: "project-a\\..*"
actions: ["view"]Kafbat 的资源名和 action 会随发行线扩展,部署前用目标版本的 RBAC 文档确认,并对每个被拒绝的写动作保留后台响应。OIDC 登录成功只证明用户身份,不能证明角色映射正确。
最小只读验证分两步。先做正向操作:
登录只读账号。查看目标 Topic 的分区、副本和配置。查看指定 consumer group 的当前 offset 与 lag。
在脱敏测试 Topic 中按时间窗口读取一条消息。
页面应返回元数据和测试消息,日志不应出现授权失败。随后执行反向操作:尝试创建 permission_probe_should_fail Topic、修改 retention,或重置测试消费组 offset。页面应隐藏或拒绝这些动作;即便直接调用后台接口,Kafka 也应返回 TopicAuthorizationException、GroupAuthorizationException 或等价拒绝证据。
如果反向操作成功,不能把“操作员不会点错”当成控制措施。立即撤销服务账号的写 ACL,检查 Kafbat RBAC 和 read-only 生效配置,再重新执行反例。只有拒绝证据同时来自 UI 层和 Kafka 层,才算形成只读闭环。
AKHQ:配置文件驱动的另一条完整链路
AKHQ 是独立产品,不是 Kafbat UI 的皮肤或兼容模式。它使用自己的 akhq.connections、安全组和角色模型,镜像、升级节奏、数据遮罩与故障表现也不同。项目 Release 页面 可用于锁定发行版本;下面使用 0.27.1。该修复发行包含鉴权响应、上下文路径、版本显示和 consumer group 依赖修复,升级时仍要重新验证登录跳转、API 拒绝、consumer group 展示和权限映射;其分组权限模型不能直接套用早于 0.25.0 的旧配置。
创建可审查的 AKHQ 配置
目录结构:
tools/akhq/
├── compose.yaml
├── application.yml
└── .env.exampleapplication.yml 先建立本机只读连接:
micronaut:
security:
enabled: false
token:
jwt:
signatures:
secret:
generator:
secret: ${AKHQ_JWT_SECRET}
akhq:
connections:
dev-local:
properties:
bootstrap.servers: "kafka:19092"
security.protocol: PLAINTEXT
security:
default-group: reader
pagination:
page-size: 25
topic-data:
size: 20
poll-timeout: 1000
kafka-max-message-length: 1048576
ui-options:
topic:
default-view: HIDE_INTERNAL
groups-default-view: HIDE_EMPTYdefault-group: reader 将匿名访问降为读取,但 JWT 签名密钥不能省略。AKHQ 的 Groups 文档 明确指出:配置组却不设置签名密钥时,API 可能不会执行组角色限制,限制只停留在 UI。共享环境还必须把 micronaut.security.enabled 设为 true 并接入 Basic、LDAP 或 OIDC;本机匿名只读实例只能绑定 127.0.0.1。
几个容量字段要按用途调整:
page-size 限制 Topic 列表分页,不宜为了“一页看全”无限增大。topic-data.size 控制单页消息条数,消息体大时会直接放大堆内存和浏览器负担。poll-timeout 是消息读取等待时间,不是 Kafka 请求的总超时。
kafka-max-message-length 限制传给 UI 的记录长度,避免大消息把控制台拖垮。HIDE_EMPTY 在消费组很多的集群中可减少无效展示与查询压力。
Compose 文件:
services:
akhq:
image: tchiotludo/akhq:0.27.1
container_name: akhq
restart: unless-stopped
ports:
- "127.0.0.1:18081:8080"
environment:
AKHQ_JWT_SECRET: ${AKHQ_JWT_SECRET:?set AKHQ_JWT_SECRET}
volumes:
- ./application.yml:/app/application.yml:ro
networks:
- dev-messaging
networks:
dev-messaging:
external: trueAKHQ 的 容器配置说明 支持 AKHQ_CONFIGURATION、MICRONAUT_APPLICATION_JSON 和 MICRONAUT_CONFIG_FILES,但团队配置较长时,挂载单一 application.yml 更容易评审。只挂载具体文件,不要把宿主机目录覆盖到 /app;后者会遮住镜像内程序并导致 exec: ./akhq: not found。
启动和验证:
cp .env.example .env
# 生成随机值后写入本机 .env;不要提交 .env
docker compose up -d
docker compose ps
docker compose logs --tail=120 akhq
curl -I http://127.0.0.1:18081打开页面后重复 Topic、consumer group、offset 和测试消息的正向查看,再尝试创建 Topic、删除消息、修改 Topic 配置和更新 offset。reader 组应拒绝所有写动作。若按钮隐藏但直接请求 API 成功,优先检查 JWT 签名密钥是否真的注入、生效配置是否被另一份 Micronaut 文件覆盖。
共享实例可以先用 Basic Auth 验证角色链,再迁移到 OIDC/LDAP。密码必须保存为 SHA-256 或 bcrypt 哈希,不能把明文密码写进配置:
micronaut:
security:
enabled: true
akhq:
security:
basic-auth:
- username: project-a-reader
password: ${AKHQ_READER_PASSWORD_HASH}
passwordHash: BCRYPT
groups:
- project-a-observers浏览器用户身份与 akhq.connections 中的 Kafka 服务身份是两套凭据。前者决定谁能进入 AKHQ,后者决定 AKHQ 后台能对 Kafka 做什么;两者都必须最小化。
AKHQ 的 SASL/SSL 和 Registry 连接
AKHQ 将标准 Kafka consumer 属性放在连接的 properties 下。SCRAM over TLS 示例:
akhq:
connections:
shared-dev:
properties:
bootstrap.servers: "broker-a.dev.example:9093,broker-b.dev.example:9093"
security.protocol: SASL_SSL
sasl.mechanism: SCRAM-SHA-512
sasl.jaas.config: >-
org.apache.kafka.common.security.scram.ScramLoginModule required
username="${KAFKA_UI_USERNAME}"
password="${KAFKA_UI_PASSWORD}";
ssl.truststore.location: /run/kafka-secrets/client.truststore.jks
ssl.truststore.password: ${KAFKA_TRUSTSTORE_PASSWORD}
schema-registry:
url: "https://schema.dev.example"
type: confluent
basic-auth-username: ${SCHEMA_REGISTRY_USERNAME}
basic-auth-password: ${SCHEMA_REGISTRY_PASSWORD}
properties:
schema.registry.ssl.truststore.location: /run/kafka-secrets/client.truststore.jks
schema.registry.ssl.truststore.password: ${KAFKA_TRUSTSTORE_PASSWORD}AKHQ cluster configuration 说明了 bootstrap.servers、SASL/SSL、Schema Registry、Kafka Connect 与 ksqlDB 的连接结构。证书和信任库应以只读 Secret 挂载,密码交给运行环境注入;不要把 ${...} 替换成仓库中的真实值。
AKHQ 的 Registry 配置支持特定类型和认证方式,但“Registry 可连接”仍不代表任何消息都能正确展示。检查顺序应是:Topic 是否有 schema、subject 命名策略是否一致、Registry 身份是否能读对应 subject、序列化格式是否受支持、消息是否超过 UI 限制。无法反序列化时保留原始字节和客户端证据,不要在控制台里猜测业务对象。
用角色、资源模式和数据遮罩缩小读取面
共享实例不应给所有人全局 reader。AKHQ 允许角色绑定资源动作,再用 patterns 和 clusters 限制资源名称与集群:
akhq:
security:
roles:
project-observer:
- resources: ["TOPIC", "TOPIC_DATA", "CONSUMER_GROUP"]
actions: ["READ"]
- resources: ["TOPIC"]
actions: ["READ_CONFIG"]
groups:
project-a-observers:
- role: project-observer
patterns: ["project-a\\..*"]
clusters: ["shared-dev"]认证系统再把用户或群组映射到 project-a-observers。升级前特别检查 0.25.0 引入的分组权限破坏性变更;旧 ACL 配置不会因为 YAML 仍能解析就自动拥有相同语义。
能够读 Topic 消息就可能看到手机号、令牌、订单地址或内部标识。AKHQ 支持正则和 JSON 字段遮罩,数据遮罩配置 还提供“默认全部遮罩、按字段放行”的模式。敏感环境优先使用默认遮罩;代价是 Registry 不可用或消息无结构时可能无法安全展示原文,这应被视为安全保护生效,而不是改成明文模式的理由。
把 Topic、Group、Offset 和消息查看变成诊断链
控制台最有价值的地方不是“看起来更直观”,而是把故障现象按因果顺序缩小。
消费延迟持续增长
先确认生产速率是否增长,再查看目标 group 每个分区的 current offset、log end offset 和 lag。若只有一个分区 lag 上升,进一步检查分区热点、消费者分配和单条慢消息;若所有分区同步上升,检查消费者实例数、处理耗时和下游依赖。不要看到总 lag 就立即重置 offset,重置只改变读取位置,不会消除处理瓶颈。
应保留以下证据:
cluster=shared-dev
topic=project-a.orders
group=project-a.order-indexer
partition=3
current_offset=<观察值>
log_end_offset=<观察值>
lag=<观察值>
consumer_instance=<观察值>这些值要与 kafka-consumer-groups.sh --describe 的 CLI 结果交叉验证。控制台与 CLI 不一致时,先核对是否连接了同一集群、同一 group,以及页面缓存和刷新时间,不要直接认定 Kafka 丢数据。
页面看不到刚发送的消息
消息浏览器通常要创建临时消费者并从指定 offset 或时间位置读取。依次检查:
选择的分区和起始位置是否包含目标消息。服务身份是否拥有 Topic READ,而不只是 DESCRIBE。消息是否因 retention 或 compact 已不可见。
key/value 是否需要 Registry 和正确的反序列化器。单条消息是否超过控制台读取上限。页面过滤表达式是否排除了目标记录。
使用专门构造的脱敏测试消息做正向实验,例如 key 为 ui-probe-001、value 只包含虚构字段。能按 key 找到该消息,才能证明读取链路;不要用生产用户数据作为探针。
Offset 重置为什么是高风险操作
重置到更早位置会重放消息,可能重复扣款、重复发券或重复写入下游;重置到更晚位置会跳过尚未处理的消息。即便业务实现了幂等,仍要先记录原 offset、目标 offset、分区、操作人和回退命令。
团队应把 offset 修改权限从日常观察角色移除。确需操作时采用双人复核,在暂停消费者或确认消费语义后执行,并用 CLI 保存前后证据。控制台只是操作入口,不负责判断业务能否承受重放或跳过。
反向实验:让错误稳定暴露
错误 listener
把控制台的 bootstrap 指向可连接的 Kafka 入口,但让 Kafka 元数据公布一个控制台容器不可解析的主机名。预期表现是首次 bootstrap 可能成功,随后日志反复出现目标 broker 的 DNS 或连接错误。恢复正确 advertised listener 后重启控制台客户端,再确认 broker 与 Topic 列表出现。
错误 SASL 机制
将只读测试身份的 sasl.mechanism 从集群实际使用的机制改成另一个机制。预期日志出现认证失败,而不是 Topic 为空。恢复机制和凭据后,先用 Kafka CLI 在同一网络验证,再验证控制台;这样能区分 Kafka 身份问题和 UI 配置问题。
Registry 权限不足
保留 Kafka 读取权限,撤销测试 Registry 身份对目标 subject 的读取权限。预期 Topic 元数据仍可查看,但结构化消息解码失败或只能显示原始内容。恢复最小 subject 读取权限后再次打开同一条测试消息,证明修复点在 Registry,而不是 broker。
UI 只读而 Kafka 身份可写
保留 Kafbat read-only 或 AKHQ reader,却给后台身份创建 Topic 的 Kafka ACL。页面可能隐藏按钮,但泄漏凭据仍可通过 CLI 写入。这一反例证明 UI 角色不是安全边界。撤销 Kafka 写 ACL 后,CLI 创建测试 Topic 必须收到授权拒绝。
选 Kafbat UI 还是 AKHQ
两者都能观察多集群、Topic、消息和 consumer group,但决策不应只看截图。
| 决策信号 | Kafbat UI | AKHQ |
|---|---|---|
| 配置模型 | kafka.clusters,支持 cluster read-only、RBAC、遮罩 | akhq.connections,角色、组、资源模式和集群模式组合 |
| 只读入口 | cluster 级 read-only 加 RBAC | reader 或自定义角色加 group |
| 配置持久化 | 静态 YAML或动态配置;共享环境应明确唯一事实源 | 配置文件、环境变量或 Helm values,适合 Git 审查 |
| Schema Registry | 支持 Registry 接入和多种消息格式 | Registry、Connect、ksqlDB 连接集中在 connection 下 |
| 安全重点 | UI 认证、RBAC、Kafka ACL、数据遮罩同时生效 | 必须关注 JWT 签名密钥与分组权限版本迁移 |
| 升级重点 | Kafbat 发行线、旧 Provectus 配置迁移、动态配置结构 | AKHQ 发行线、0.25.0 后 group ACL 结构变化 |
先在测试集群用真实的 SASL/SSL、Registry 和权限模型完成正反实验,再决定。若团队只需要短期本机查看,单副本、仅绑定回环地址即可;若作为共享入口,则身份集成、审计、脱敏、配置版本化和服务 owner 比“功能更多”更重要。
共享控制台的高可用、容量和成本
控制台故障不应影响 Kafka 生产消费,因此不要把业务可用性依赖在 UI 上。开发环境通常单副本足够;团队共享实例可部署两个副本,但要确认动态配置是否共享、会话是否需要粘性,以及多个副本的轮询会把多少额外请求打到 broker、Registry 和 Connect。
容量评估至少记录:集群数、broker 数、Topic/分区数、consumer group 数、页面并发用户、消息浏览频率、最大消息大小和刷新周期。常见失控路径是:所有人打开自动刷新页面,控制台持续枚举大量 group;有人浏览超大 Topic 的消息并触发反序列化;多个副本对每个集群重复采集元数据。出现控制台堆内存增长或 Kafka 管理请求突增时,先降低分页、消息条数和轮询频率,限制接入集群,再考虑扩容 UI。
成本不只有容器 CPU 和内存。还包括跨可用区流量、Registry 请求、日志保留、OIDC/LDAP 接入、证书轮换、值班排障和升级验证。把生产集群接给每个开发者的个人控制台,通常比维护一个受控共享入口更贵,也更难审计。
团队接入和长期治理
将控制台作为工具产品维护,而不是随手启动的容器:
每个集群使用独立服务身份,生产与非生产完全分离。观察、消息查看、Topic 管理、Schema 管理、offset 操作拆成不同角色。默认禁止查看消息体;按 Topic 前缀授权并配置字段遮罩。
镜像使用版本或 digest 锁定,升级先跑 listener、认证、只读、Registry 和反向写入测试。配置进入评审,真实密码、JAAS、信任库和 OIDC secret 只由 Secret 系统注入。HTTP 入口放在内网、VPN 或受控网关后,开启 TLS、身份认证、访问日志和超时。
为控制台设置 owner、资源预算、升级窗口、故障降级和退出方案。
控制台配置变更的回滚比镜像回滚更重要。升级前保存镜像 digest、配置版本和服务身份 ACL;若新版本无法连接或权限行为变化,先恢复旧镜像与旧配置,再验证 Kafka ACL 没有因临时排障被放宽。
清理本地实例
两个控制台默认不需要保存 Kafka 数据。停止并删除容器:
docker compose down --remove-orphans若曾启用 Kafbat 动态配置或额外挂载 AKHQ 数据目录,删除前先检查其中是否只有本机工具状态。清理顺序应是先停 HTTP 入口,再撤销测试服务身份和 Registry 凭据,最后删除本地配置中的 Secret 引用。共享环境下不要用 down -v 猜测 volume 归属,也不要删除 Kafka、Registry 或其他团队正在使用的网络和数据卷。
完成一次接入后,读者应能用下面的不变量复查结果:页面能读目标元数据;测试消息只在获批 Topic 中可见且敏感字段被遮罩;创建 Topic、修改配置和更新 offset 都被拒绝;控制台凭据脱离 UI 后仍无法写 Kafka;关闭控制台不会影响任何生产者或消费者。
