ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Nomad部署ClickHouse实战:HCL配置、Flink管道与优雅停止故障排查

Nomad部署ClickHouse实战:HCL配置、Flink管道与优雅停止故障排查 最近把一套用户行为分析用的 ClickHouse 从手工脚本挪到了 Nomad 上顺带把给 ClickHouse 供数的实时任务也一起收编进 Job 体系。这个项目在内部就叫“Nomad 组件部署 clickhouse-job”听起来很绕拆开其实就三件事ClickHouse 服务本身要容器化托管、Kafka 到 ClickHouse 的实时管道要能被 Nomad 调度、日常清理和运维操作要能自动化。这篇文章会把这套东西从选型到落地的全过程讲一遍包括 HCL 怎么写、内存和存储怎么规划、Flink 任务怎么优雅停止尤其是报错can not stop with a savepoint job这个坑我会把定位思路和处理方案完整写出来。正在从手工运维切换到 Nomad、或者打算用轻量调度器托管有状态数据服务的同学这份经验可以直接参考。1. 项目整体设计与技术选型思路1.1 为什么在 ClickHouse 场景选 Nomad 而不是 Kubernetes先说选型。团队里不缺会用 K8s 的人我们也不是不会用但评估完这个项目的真实体量之后我还是坚持上了 Nomad。原因不复杂我们需要的是一个能把进程管起来、能自动恢复、能滚动发布、能和 Consul 打通服务发现的调度器而不是一个完整的容器管理全家桶。Nomad 最吸引我的地方是单个二进制文件就能把 Server 和 Client 都跑起来没有 etcd、没有 apiserver、没有网络插件这一堆前置依赖两三个节点的小集群从装机到跑第一个 job 半小时内就能搞定。ClickHouse 在这套体系里是一个有状态的分析数据库它不像微服务那样需要频繁伸缩也不需要 K8s 里那套复杂的 network policy、ingress、HPA 能力反而对“数据目录持久化”“固定端口”“内存硬限制”这些朴素需求更敏感而这恰恰是 Nomad 能清晰表达的东西。Kubernetes 当然有它的优势比如生态完善、社区资料多、招聘时容易找人手但对应的代价是集群本身的运维成本和故障面也被放大了。我们之前出现过一次 K8s 节点 NotReady 导致大量 Pod 被驱逐的事故复盘时发现其实业务根本不需要那么动态的调度。Nomad 的调度模型更直接它把 CPU、内存、端口、磁盘当作资源维度把 job 定义成 service 或 batch一个大括号写清楚跑什么、跑几个、放哪类节点、挂了怎么处理。对于 ClickHouse 这种组件简单反而意味着可预测。我在这套方案里把 ClickHouse 服务、Flink 供数任务、定时清理任务统一成三个独立 job让它们各自拥有生命周期这在后面排障时帮了大忙。1.2 拆解“clickhouse-job”的三层组成标题里的“clickhouse-job”容易让人以为只要部署一个服务就完事真正做起来会发现是一个组合长驻服务、流式任务、定时任务三种形态差别不小必须分开对待。第一层是 ClickHouse server 本体它需要 7x24 小时在线属于 service 类型 job。这一层决定稳定性配置重点在持久化、内存上限、健康检查和滚动升级策略。第二层是实时数据管道我们的场景是 Flink 消费 Kafka 里的行为事件清洗后批量写入 ClickHouse。这个任务也是长驻的但从故障域和迭代节奏看它和 ClickHouse server 完全是两码事Flink 作业可能一天改两版ClickHouse 一个月升一次级混在一个 job 里会让更新互相绑架。第三层是每天凌晨清理过期分区的定时任务用 batch 类型加 periodic 调度正合适跑完即焚不需要保持实例。有人可能会想既然都是 Nomad job为什么不把所有 task 塞到一个 job 文件里统一管理我第一版确实这么干过结果升级 ClickHouse 的时候把管道任务也波及了。拆开之后每个 job 独立执行nomad job run、独立回滚、独立看日志出问题时用nomad job status一眼就能定位是哪个 allocation 出了问题。所以我的建议很明确按生命周期和故障域拆 job不要按机器归属拆。1.3 完整数据链路与资源规划这条链路从业务侧看很简单业务应用产生事件 → 写入 Kafka → Flink 作业消费并清洗 → 批量写入 ClickHouse → 报表查询直接读 ClickHouse 宽表。落地到 Nomad 集群里资源规划却有几个容易被忽略的点。ClickHouse 是典型的 IO 和内存敏感型应用生产环境我不想让它和一堆无状态容器混跑在宿主机核心业务上。我给 ClickHouse 单独规划了节点组数据盘用 SSD挂载路径统一放在/srv/nomad/volumes下。内存这块ClickHouse 官方镜像默认会自动探测宿主机内存并吃掉大半这在容器里非常危险必须显式限制。我的做法是在 Nomad 的resources块里预留内存同时在容器配置里设置镜像支持的max_server_memory_usage让两层约束对齐避免调度器以为内存够用、实际容器却 OOM。网络规划上ClickHouse 的 8123 HTTP 端口和 9000 native 端口我用了静态端口因为下游 JDBC 地址、监控探活、客户端配置全都写死了迁移成本最低。如果追求多实例共存静态端口会有冲突风险那就得改成动态端口并强制所有客户端走 Consul 服务发现。这方面的取舍后面章节会给出具体配置。2. 核心细节解析与实操要点2.1 Nomad Job HCL 结构与关键配置项先理清 Nomad job 文件的层级关系job是整个调度单位group是一组必须调度到同一台机器上的 task 集合task才是真正跑起来的容器或进程。这个层级很像 K8s 里的 Deployment 和 Pod 的关系但表达上更直接。我的习惯是除非有 sidecar 需求否则一个 group 只放一个 task保持“一容器一职责”排障时不用猜日志属于谁。HCL 里几个必看的配置块我用 ClickHouse 的 job 来说明。type字段决定调度语义service类型面向长驻服务调度器会尽力维持实例存活batch类型面向一次性任务跑完就退出并回收。ClickHouse server 和 Flink 管道必须用service定时清理任务用batch这点不能搞错否则任务异常退出时 reschedule 策略会表现得很不一样。update块控制滚动发布。我给 ClickHouse 配了max_parallel 1保证一次最多只有一个新实例在替换避免升级过程中出现两个实例同时挂载同一份数据目录。healthy_deadline 5m表示新实例必须在 5 分钟内通过健康检查否则任务会被判定失败并触发auto_revert自动回滚。这里有个细节ClickHouse 冷启动恢复数据可能要几分钟健康检查的窗口期不能给得太短否则会出现“新实例明明在恢复数据却被当成启动失败”的假阳性。restart和reschedule也有区别task 级别的restart管的是容器崩溃后的本地重启group 级别的reschedule管的是整个 allocation 失败后是否换一台机器重新调度。数据库服务我通常会把restart的 attempts 调低避免内存 OOM 后无限重启把日志刷爆。2.2 ClickHouse 部署的关键参数与存储设计ClickHouse 的配置项很多但部署层面真正决定稳定性的就那么几个踩过一遍的人都懂。内存是最先要处理的。前面提过镜像默认会探测宿主机内存然后按照很大比例设置内存池。解决方法是显式写覆盖配置我习惯在config.d/override.xml里固定max_server_memory_usage比如单机测试环境直接给 3GB避免它把容器所在宿主机的总内存当成了自己的预算。同时 Nomad 的 docker driver 会把resources.memory映射成容器的内存限制这样配置文件和容器限制两边一致问题就不容易发生。存储设计上ClickHouse 的数据目录/var/lib/clickhouse和日志目录/var/log/clickhouse-server必须放在宿主机挂载盘。我用 Nomad 的 host volume 机制在客户端配置里定义一个指向/srv/nomad/volumes/clickhouse的卷job 文件里通过volume_mount挂进容器。这样无论 allocation 怎么重启、容器怎么重建数据都还在宿主机上。还有一个权限坑容器内 clickhouse 用户是 uid 999挂载的目录如果用 root 创建容器会直接报 permission denied。我折腾过一次之后把创建目录和chown -R 999:999写进了初始化脚本以后再也不手动了。端口和访问控制方面8123 是 HTTP 接口9000 是 native 协议接口9009 用于集群副本通信。我在 Nomad 的network块里把它们声明成三个静态端口并通过 env 注入到容器。注意不要直接在network里写static 0那是动态端口的语义和静态端口写法完全两回事。2.3 Flink 供数任务以容器形态托管进 Nomad把这层拆开讲是因为 Flink 任务的管理方式和 ClickHouse server 差别很大。实时管道我们用的是 Flink DataStream消费 Kafka 后写入 ClickHouse。在 Nomad 里跑 Flink 有两种主流姿势一种是把 JobManager 和 TaskManager 当作两个常驻服务分别托管适合跑多个作业、需要复用集群的场景另一种是 standalone application 模式把 Flink job 打进镜像容器启动后直接运行作业整个生命周期由 Nomad 管理。这次我选了后者因为管道任务迭代频繁重新 build 一个镜像然后滚动更新比维护一个常驻 Flink session 集群要轻得多。实现细节上Flink 官方镜像提供了standalone-job.sh入口它会启动 Dispatcher 和 JobManager并自动提交指定的用户 jar。我在 Dockerfile 里把 pipeline jar 放在/opt/flink/usrlib/下CMD 参数里跟上入口类和各连接参数。容器运行时保持前台进程不退Nomad 的健康检查通过后就认为任务部署成功。这种方式在更新时非常清爽nomad job run触发滚动新容器起来、新代码生效旧容器按策略停掉。Flink 作业的状态恢复要提前想清楚。流式计算最怕的是容器重启后状态丢了从头消费 Kafka。我在作业代码里开了 checkpoint并将 checkpoint 配置成外部化存储路径指向共享存储。这样容器无论因为升级还是故障被 Nomad 拉起都能从最近一次成功的 checkpoint 恢复进度。这个机制和后面要讲的can not stop with a savepoint job也有关联先埋个伏笔。2.4 配置管理与密钥注入实操中还有一个容易被忽略的环节配置和密码怎么管。Nomad 本身不提供声明式的配置中心但它有template块可以在任务启动前把模板渲染到local目录再挂载进容器或直接读。这个方法用来管 ClickHouse 的override.xml非常好用因为配置文件本身跟着 job 走镜像保持干净升级镜像时不用重新 bake 配置。密钥方面测试环境我图省事直接在 env 里写了明文密码生产环境建议接 HashiCorp Vault 或者至少用 Consul KV 保存通过template的vault或consul函数渲染出来。Nomad 对 Vault 集成是原生支持的job 文件里声明需要的 Vault policy任务启动时就会自动获取临时 Token用完即失效比在镜像里写死密码安全得多。这里给一个最小例子在这个项目里我一开始图快把 ClickHouse 密码写死在 job 文件里结果全仓库都能看到后来改成 Consul KV 渲染一分钟改完心里踏实多了。如果你团队已经有 Vault直接走 Vault 集成别犹豫。3. 实操过程与核心环节实现3.1 环境准备数据目录、Host Volume 与客户端配置开始写 job 之前先把 Nomad 环境准备好。假设集群已经搭好Server 和 Client 都在运行下面几步是在 ClickHouse 将要运行的节点上做的。第一步创建数据目录并授权。ClickHouse 官方镜像里 clickhouse 用户的 uid 是 999宿主机上新建的目录默认是 root 所有必须改成 999 可写sudo mkdir -p /srv/nomad/volumes/clickhouse/data sudo mkdir -p /srv/nomad/volumes/clickhouse/logs sudo chown -R 999:999 /srv/nomad/volumes/clickhouse第二步在 Nomad 客户端配置文件里声明 host volume以 Linux 下/etc/nomad.d/client.hcl为例client { enabled true host_volume clickhouse-data { path /srv/nomad/volumes/clickhouse/data read_only false } host_volume clickhouse-logs { path /srv/nomad/volumes/clickhouse/logs read_only false } }改完重启 nomad client 服务让新配置生效。这时候用nomad node status查看节点属性如果能看到 host volume 已经注册说明环境就绪了。这一步很容易因为 client 配置没重启而被忽略job 跑起来会一直 pending光看 job 状态很难发现问题。3.2 部署 clickhouse-server 的完整配置与验证下面是实际在用的 ClickHouse job 文件已脱敏。它包含了持久化、内存限制、健康检查、滚动升级策略和自定义配置渲染直接可以照着改job clickhouse-server { datacenters [dc1] type service group clickhouse { count 1 network { port http { static 8123 } port native { static 9000 } port intra { static 9009 } } volume ck-data { type host source clickhouse-data read_only false } volume ck-logs { type host source clickhouse-logs read_only false } task server { driver docker config { image clickhouse/clickhouse-server:24.8.5.75 ports [http, native, intra] mount { type bind source local/config.d target /etc/clickhouse-server/config.d readonly true } } volume_mount { volume ck-data destination /var/lib/clickhouse } volume_mount { volume ck-logs destination /var/log/clickhouse-server } template { data -EOF clickhouse max_server_memory_usage3221225472/max_server_memory_usage listen_host0.0.0.0/listen_host max_connections4096/max_connections mark_cache_size536870912/mark_cache_size /clickhouse EOF destination local/config.d/override.xml change_mode restart } env { CLICKHOUSE_DB analytics CLICKHOUSE_USER default CLICKHOUSE_PASSWORD change-me-in-prod } resources { cpu 2048 memory 4096 } service { name clickhouse port http provider consul tags [clickhouse, analytics] check { type http path /ping interval 10s timeout 2s } } } } update { max_parallel 1 min_healthy_time 30s healthy_deadline 5m auto_revert true } }有几个点特意说明一下。template块里的max_server_memory_usage单位是字节3GB 对应 3221225472不要写错成 GB 数字。配置渲染到local/config.d/override.xml后挂载到容器change_mode restart表示配置变化时自动重启容器。健康检查用的/ping是 ClickHouse HTTP 服务自带接口返回 200 说明进程存活比只做 TCP 端口检查更能反映真实可用性。上线操作nomad job run clickhouse-server.hcl查看调度结果nomad job status clickhouse-server nomad alloc status alloc-id看到 running 后进容器或直接在外面验证clickhouse-client --host 127.0.0.1 --port 9000 --query SELECT version(), uptime()如果客户端不在本机记得用 8123 端口的 HTTP 接口或走 Consul 解析clickhouse.service.consul。到这里 ClickHouse 本身已经跑起来了。3.3 部署 Flink 管道任务并验证数据链路管道任务这层我先把 Flink 作业打成镜像。Dockerfile 保持简单重点是把 jar 放到 Flink 的 usrlib 目录下让standalone-job.sh能扫描到FROM flink:1.17.2 COPY ClickhousePipeline.jar /opt/flink/usrlib/ClickhousePipeline.jar ENTRYPOINT [/opt/flink/bin/standalone-job.sh] CMD [com.example.ClickhousePipeline, --kafka.servers, kafka.service.consul:9092, --clickhouse.url, jdbc:clickhouse://clickhouse.service.consul:8123/analytics]在 Nomad 里定义对应的 service 类型 jobjob clickhouse-pipeline { datacenters [dc1] type service group pipeline { count 1 task flink { driver docker config { image registry.internal/clickhouse-pipeline:1.4.2 } env { CHECKPOINT_INTERVAL_MS 60000 CHECKPOINT_EXTERNAL_RETENTION RETAIN_ON_CANCELLATION } resources { cpu 1024 memory 2048 } restart { attempts 5 delay 30s } } update { max_parallel 1 min_healthy_time 60s healthy_deadline 10m auto_revert true } } }这个 job 没有对外映射端口数据管道是外连 Kafka 和 ClickHouse 的。启动后观察日志nomad job status clickhouse-pipeline nomad alloc logs -f alloc-id日志里能看到 Flink 提交作业的过程以及 checkpoint 是否开始正常生成。等上几分钟到 ClickHouse 里查一下SELECT count() FROM analytics.events; SELECT toStartOfHour(ts) AS hour, count() FROM analytics.events GROUP BY hour ORDER BY hour DESC LIMIT 10;有数据增长就说明链路通了。这里我特别重视 checkpoint 的状态后面升级停止和故障恢复都依赖它。如果 checkpoint 一直失败先别继续往下走把 Kafka 消费组、ClickHouse sink 写入性能先调通否则后面所有“优雅”操作都是空中楼阁。3.4 用 batch 加 periodic 实现定时清理任务分析数据会在 ClickHouse 里越积越多过期分区必须定时清理。这一层用 Nomad 的 batch 类型 job 加 periodic 调度来跑一行 cron 表达式搞定job clickhouse-cleanup { datacenters [dc1] type batch periodic { cron 0 3 * * * prohibit_overlap true } group cleanup { task cleanup { driver docker config { image clickhouse/clickhouse-client:24.8 command /usr/bin/clickhouse-client args [ --host, clickhouse.service.consul, --query, ALTER TABLE analytics.events DROP PARTITION WHERE day now() - INTERVAL 30 DAY, ] } resources { cpu 256 memory 256 } } } }注意这里用的是DROP PARTITION而不是DELETE。大数据量下DELETE会生成 mutation 后台任务磁盘 IO 消耗大执行得很慢按天分区并直接 drop 旧分区才是 ClickHouse 推荐的做法秒级完成且不产生重写。分区表需要你在建表时就用PARTITION BY toYYYYMMDD(day)这类表达式否则这条命令没有意义。prohibit_overlap true防止上一次清理还没跑完下一次又触发避免两个清理任务同时操作同一张表产生锁竞争。定时任务单独一个 job 的好处在于如果某天业务需要手动清理直接nomad job run -detach clickhouse-cleanup手动触发一次就行不影响主服务。4. 常见问题与排查技巧实录4.1 报错“can not stop with a savepoint job”的完整排查项目里最值得一写的是这个报错。场景是这样的某次要升级 Flink 管道任务版本按流程应该先优雅停止作业并保留中间状态。我执行了/opt/flink/bin/flink stop -p /data/flink/savepoints jobId结果命令直接失败提示can not stop with a savepoint job作业还挂在 RUNNING。第一反应是查作业状态flink list显示 RUNNINGWeb UI 里 checkpoint 也在正常做完全看不出异常。于是去翻 JobManager 日志看到了更具体的报错信息停止操作触发的 savepoint 在超时时间内没有完成作业状态无法收敛。我把这类问题的排查顺序整理成一个表照着做基本能定位检查项具体操作解决办法作业是否启用 checkpointflink list -v或在 Web UI 看 Checkpoint 页代码里显式env.enableCheckpointing(60000)否则 stop 没有状态可保存source 是否可停止看 source 有没有实现可停止接口KafkaSource 一般可以改用flink cancel -s或换成可停止的 sourcesavepoint 存储路径是否可写检查 JobManager 所在节点对于state.savepoints.dir是否有写权限改路径权限避免用不稳定的 NFS停止过程是否超时看 JobManager 日志里有没有 stop/savepoint 超时字样手动先 cancel再从最近 checkpoint 恢复这次问题的根子其实在最后一个检查项。我们的 ClickHouse sink 是自定义写入算子数据先在内存缓冲批量攒着快照回调里没有强制 flush 当前批次导致触发 savepoint 时数据写不完超时后被 Flink 判定为无法停止。简单说不是 Nomad 的问题也不是 ClickHouse 的问题是流处理作业自己的生命周期没有处理干净。最终解决用的是两阶段方案。先用flink cancel jobId把作业取消这一步不会尝试生成 savepoint所以不会卡住然后从最近一次成功的 checkpoint 路径手动恢复任务/opt/flink/bin/flink run -s /data/flink/checkpoints/xxx/chk-123 /opt/jobs/pipeline.jarcheckpoint 原本就开着恢复后的进度最多回退几十秒Kafka 消费 offset 也能衔接上端到端不重不丢这个结果当时实测就很稳。之后我在新版本算子代码里修复了快照 flush 逻辑再次验证flink stop -p就能正常生成 savepoint 了。这里也想给个忠告stop和cancel -s是有本质区别的。stop要求作业的 source 能主动停止并且会触发一次严格的 savepoint链路里任何一步卡住都会报错cancel -s是强制取消的同时尝试保存状态对作业内部状态的配合度要求低一些。遇到报错别慌先确认你的任务是哪一类再决定走哪条路径。ClickHouse 这类强状态 sink 的作业一定要先把快照和缓冲 flush 的关系处理好优雅停止才谈得上。4.2 ClickHouse 容器资源与存储问题速查除了那个大头报错日常里这几个问题也很典型。OOM 是 ClickHouse 容器最常见的故障。现象是容器反复重启但看nomad alloc status还显示 running因为重启太快。排查时先看 ClickHouse 日志里有没有Memory limit相关报错再看容器的 cgroup 限制是否生效。Nomad 的resources.memory会映射给 docker driver 作为内存限制但 ClickHouse 内部的内存池设置要单独配置两者必须对齐。我之前遇到过配置里写了 4096MB 内存限制但max_server_memory_usage没显式设置结果 ClickHouse 自动探测后把内存池开到了接近宿主机全部内存uv 直接爆了。数据目录权限的问题也很常见。挂载了 host volume 但没做chown -R 999:999容器启动时会报cannot create directory /var/lib/clickhouse: Permission denied。这个问题在第一次跑 job 时最坑因为 job 文件看起来完全正常调度也成功就是容器反复启动失败不看日志根本找不到原因。我后来把目录初始化做成了一个独立脚本在创建 host volume 之后立刻执行再也没犯过。磁盘写满之后 ClickHouse 会把表变成只读这个状态比想象中更隐蔽。看起来服务存活、端口正常、健康检查也过但查询开始报READONLY或者写入全部失败。监控里一定要同时盯磁盘容量和 inode 使用率只盯内存是不够的。配合 3.4 里的分区清理任务基本上能把这类问题防在发生之前。还有一个小坑滚动升级时如果把count改成了 2并且两个实例挂载了同一个 host volume会出现两个 ClickHouse 进程抢同一个数据目录轻则锁冲突重则数据目录损坏。有状态服务不要轻易用多副本加共享卷的方式来搞高可用正确姿势是一次只运行一个实例通过备份恢复或真正的主从复制来保证可用性。4.3 Nomad 调度与运维的其他高频问题最后补充几个 Nomad 层面的高频问题不算疑难杂症但很影响体验。job 长时间 pending多半不是资源不够而是datacenters写错或节点没有满足约束。用nomad job status -verbose看 Placement Metrics它会直接告诉你没被调度上的原因比如 “1 node exhausted by memory” 或者 “no nodes with datacenter match”比瞎猜快得多。排障时记得还要看节点上的 client 配置是否和期望一致host volume 注册失败也会导致 job 一直 pending 但日志什么都不报。nomad job stop和nomad job restart是不同的操作。stop是彻底停掉这个 job 的所有 allocation不再自动拉起restart是重新调度一个 allocation。很多人刚上手会把它们混用想重启某个容器结果执行了stop紧接着发现服务没了还得重新nomad job run。日常更新代码直接重新nomad job run触发滚动就行不是必须先 stop。分配节点时constraint块里可以用attribute限定只跑在有某块 SSD 的机器上constraint { attribute ${node.class} value clickhouse }这种方式比把 job 绑定到某个固定节点更灵活节点故障时可以自动迁移。但记得给有状态服务配套好数据盘迁移方案否则换了机器数据没了反而比不迁移更糟。5. 最后再说几句实操体会整段实操做下来我对“Nomad 组件部署 clickhouse-job”这件事最大的体会是Nomad 本身不难真正考验人的是怎么把有状态服务和无状态作业的边界划清楚。ClickHouse 的数据就是命根子持久化、目录权限、内存上限这些事必须在 job 定义里显式写清楚不能指望镜像默认行为恰好满足生产需求Flink 这类流任务则要先想好停止和恢复的方式checkpoint、savepoint 这套机制不是摆设真出问题的时候能救命。还有一个小技巧分享给大家给每个 job 的 metadata 里写上 owner 和用途一行代码的事meta { owner data-platform purpose clickhouse-analytics }等集群里 job 多起来之后nomad job status列表里扫一眼就知道哪个是谁的省掉很多来回沟通的成本。这套方案跑了大半年ClickHouse 的部署时间从以前的手工配系统服务加脚本压缩到现在一条nomad job run加几行配置稳定性反而更好了。如果你也在折腾数据组件的 Nomad 化部署欢迎多交流边踩坑边沉淀的经验拿出来分享比自己闷头解决要有价值得多。
返回列表