NiFi 从 3 台扩到 25 台:三轮扩容后我才明白,证书必须全量重签、安装包绝对不能拷

NiFi 从 3 台扩到 25 台:三轮扩容后我才明白,证书必须全量重签、安装包绝对不能拷

这是「NiFi 生产集群」系列的第 2 篇。原来只想写"3 台扩到 7 台"那一次,后来发现真正值钱的是三轮扩下来之后才看清的两条铁律。

上一篇:NiFi 1.17 + Kerberos 从零搭建 本篇:3 → 7 → 13 → 25,三轮扩容 下一篇:NiFi 的权限模型不是 RBAC

1. 现在的样子

主集群不是一次扩到位的,是三轮叠出来的:

轮次 时间 新增 累计 触发原因
初始 2022 年 3 3 试点
第一轮 2022 年 +4 7 FTP 接入文件量上来,单节点 CPU 顶不住
第二轮 2022--2023 年 +6 13 接入业务方增多,队列积压
第三轮 2023 年 +12 25 全量接入,一次性补齐算力

另外还有一套独立的 5 节点小集群,跑测试和边缘接入,独立部署,证书隔离、授权文件也隔离。

25 个节点意味着什么?意味着:

  • 证书一次要签 25 套、分发 25 份、校验 25 份;
  • authorizers.xml 里要有 25 条 Node Identity,而且 25 台的这份文件必须逐字一致;
  • 任何"手工改一台"的操作,在 25 台规模下都等于埋雷;
  • 扩容窗口内一旦证书不一致,不是"某台连不上",而是集群裂成两半。

先把结论放这儿:每一轮扩容,都必须把全部节点的证书重新生成一遍;安装包绝对不能从老集群打包拷过去。 这两条是用真金白银换来的,下面逐条讲。

2. 三个"看起来很合理"的错误做法

2.1 做法 A:把老节点的整个 nifi 目录打包拷到新节点

ruby 复制代码
# 千万别这么干
tar czf nifi.tar.gz /opt/nifi
scp nifi.tar.gz nifi-node-04:/opt/

现象 :新节点起来后,集群页面节点数没变多,反而原来某台老节点变成 DISCONNECTED;再起一台,又挤掉一台。日志里能看到节点反复注册、被拒绝、再注册。25 台的时候这个现象更恶心------你根本分不清是哪台被顶了,只看得到节点数在 25 上下跳。

根因:NiFi 的安装目录里不只有程序,还有"这个节点是谁"的所有证据:

目录/文件 里面有什么 拷过去会怎样
conf/flow.json.gz 整张数据流,所有组件的 UUID 两个节点声称拥有同一批组件,flow 版本同步打架
conf/nifi.properties nifi.cluster.node.address、端口 新节点用老节点的地址和端口去注册,集群里出现两个"同一个节点"
state/ 本地状态、myid ZK 身份冲突
conf/keystore.jks、truststore.jks 节点证书 两个节点用同一个 DN,集群认为它们是同一个节点
flowfile_repository/、content_repository/ 队列里的数据 数据被两个节点同时读写,直接损坏

NiFi 判断"这台机器是不是集群里已有的那个节点",靠的是节点身份 + 集群协议端口 + 证书 DN。整包拷贝等于复制了一个"身份完全相同的节点",ZooKeeper 里的节点表只认一个位置,后注册的把先注册的顶掉,表现出来就是"后启动的主机把前面的主机挤下去"。

正确做法 :新节点永远是同版本安装包全新解压 ,只从老节点拷"配置模板"(nifi.properties、authorizers.xml、login-identity-providers.xml、state-management.xml、zookeeper.properties、bootstrap.conf)和"流程模板"(UI 导出的 template XML),绝不拷 flow.json.gz、state/、各类 repository、证书。

实测复盘:第一次扩容,我就是这么省事的

第一轮扩容(3 → 7)那个窗口,我图省事,没下安装包,直接在 nifi-node-01 上打了一个整包:

bash 复制代码
tar czf /tmp/nifi.tar.gz /opt/nifi
scp /tmp/nifi.tar.gz nifi-node-04:/opt/
ssh nifi-node-04 "cd /opt && tar xzf nifi.tar.gz && /opt/nifi/bin/nifi.sh start"

新节点起得挺顺,nifi-app.log 里一路走到 Successfully joined cluster。然后刷新集群页面:节点数还是 3,原来那台 nifi-node-02 变成了 DISCONNECTED。

现象:"新加入的把原有的挤掉了",看起来就像新节点冒用了老节点的身份。第一反应是"网络抖了一下",重启 node-02,它回来了,node-04 掉;再起一台 node-05,又挤掉一台。就这么来回顶了半个多小时,集群一直在 3 台和 4 台之间横跳。

当时的怀疑很朴素:tar 包里带着缓存,也带着某个"唯一标识"。我们把包里的东西翻了一遍,三样都在------conf/flow.json.gz(整张 flow + 所有组件 UUID,跟老节点一模一样)、state/(本地状态和 myid,等于把 ZK 身份也搬了一份)、conf/keystore.jks(证书 DN 和老节点完全一样,集群只能认为是同一个节点)。

根因(当时没查到底) :机制上我只确认到"整包带着 flow 和证书"这一层,具体是哪一处触发了顶替------是 flow 里的组件 UUID、是 state/ 里的心跳记录、还是证书 DN------没有逐项拆开验证。拆开一项项试的成本,比重新装一台高得多,那时候窗口就剩两个小时,我先选了能通的路。

处置 :从官网下原始安装包、全新解压、只从老节点拷配置模板和 UI 导出的 template,再启动------一次就加进去了,node-04 和 node-02 同时 ACTIVE,之前怎么重启都复现不出来的顶替现象,再没出现过。

后来第二轮 +6 台、第三轮 +12 台,全部"新包 + 只拷配置",一次加完,一台没顶。所以"换新包就好"这个结论,是被后面两轮重复验证过的。

结论一句话:安装包不能复用,这不是"干净不干净"的问题,是"身份会不会重复"的问题。

2.2 做法 B:只给新节点签证书

现象 :新节点启动看起来正常,但一直 join 不进集群,日志里是 TLS 握手失败或者 Unable to communicate with node。

根因 :集群节点之间是双向 TLS 。老节点的 truststore.jks 里只有旧 CA 和旧证书,新节点拿的是新 CA 签的证书,老节点根本不认。反过来,新节点也不认老节点。

正确做法 :全集群重签。3 台时是 3 台一起重签,25 台时就是 25 台一起重签。只要集群里还有一台用旧 truststore,新节点就进不来;反过来,只要有一台用新证书,它和老节点之间也通不了。

2.3 做法 C:只在新节点上加 Node Identity

现象 :证书换了、能握手了,但节点还是 CONNECTING,UI 里能看到节点在列表里却一直连不上。

根因 :authorizers.xml 里的 Node Identity 是"集群内互相授权"的白名单。老节点的白名单里没有新节点,新节点发起的集群请求会被 403。这个文件每个节点都要有全量条目,而且是逐字一致的全量。

25 台时这一条最容易出错:手工往 25 个文件里各加 12 行,只要有一台少一行、多一个空格,那台就永远连不上,而且日志不会直接告诉你"第几行错了"。

2.4 扩容前,先把流程全停

上面三条都是"技术做法"层面的错。还有一个更基础的错,是我在第一轮扩容准备阶段踩的:流程还在跑,我就开始做扩容准备了。

当时的想法很自然:这套流程白天要跑,窗口又只有几个小时,那就在业务跑着的时候先把证书签好、配置改好、安装包解压好,等窗口一到直接停服务换文件,能省一半时间。

现象:做到一半发现做不下去。

  1. flow.json.gz 的 mtime 一直在变,我根本没法确定"刚备份的这一份是几点几分的 flow";
  2. UI 导出 template 时,导出来的组和画布上对不上------导出那一刻,上游正在往组里加组件;
  3. 新节点起来后继承到的 flow 是半新半旧的:一部分组在、一部分组不在,还有几个组件的版本比 primary 旧。这种状态比"完全没起来"更难收拾:节点进了集群,但画布上的组件和 primary 对不齐。

根因 :NiFi 的 flow 是以 primary 为准向下同步 的。流程运行时,组件状态、组件增删、参数上下文都在持续变化,flow 版本一直在往前走。新节点在同步过程中加入,继承到的是一个移动的目标------它拉到的是"某一瞬间"的 flow,等它同步完,primary 那边的 flow 又变了。

这时候你以为在扩容,实际在做两件互相打架的事:一边让 flow 保持一致,一边往一致性还没建立的新节点上灌流量。

处置 :没有硬扛。我们把老集群整体回退------flow 恢复到扩容前那一份、证书回到旧的那一套、全部节点重启,业务停了一个窗口。等下一个窗口,从"停流程"开始重新做,一次过。

正确的顺序只有这一个,步骤不能调换:

  1. 停全部流程:所有 Process Group 里的 Processor 全部 Stop,Controller Service 全部 Disable(有 Enable 依赖的,最后再关);
  2. 等队列排空 / 记录关键队列水位:能排空的排空,排不空的把每个关键队列的 count、bytes、oldest FlowFile 时间记下来,扩容后要用它对齐;
  3. 导出 template 备份:每个组导一份,这是唯一跨集群可用的业务资产;
  4. 全集群停服务:从最后一个节点往前停;
  5. 全量重签证书:25 台一起签,输出目录带日期;
  6. 分发 + 校验 :分发完立刻跑指纹和 md5 校验,不一致的当场处理;
  7. 起 ZooKeeper 三台:01/02/03,等这几个 ZK 进程稳定;
  8. 分批起节点:每批 5~6 台,这批全 ACTIVE 再起下一批;
  9. 验证 25 台 ACTIVE :看集群页面 + grep 各节点日志里没有反复 Disconnect;
  10. 再逐一启流程:按依赖顺序,先 Controller Service,再 Processor,一组一组开,开一组看一眼队列。

为什么要"停"而不是"边跑边扩",说透一点:

变量 流程跑着的时候 流程全停之后
flow 版本 持续变化,备份没有明确的时间点 冻结,备份就是唯一版本
template 导出 可能与画布不一致 与画布逐组件一致
新节点继承 继承一个移动的目标,容易出现半新半旧 继承一个静止的 flow
队列 有新数据进,水位一直在动 排空或水位可记录,可对比
失败回退 回退到哪个版本说不清 回退到明确的那一份备份

一句话经验:扩容是一次停机变更,不是在线加机器。

3. 25 台的正确扩容顺序

sql 复制代码
停全部流程 + 等队列排空
备份 flow 模板 + conf + 旧证书
25 台一起重签 + 分发校验
新节点全新解压安装包
改主机名三处 + 25 条 Node Identity + 统一 sensitive key
停机窗口内全停
先起 ZK 3 台, 再分 5 批起其余
验证 25 节点全 ACTIVE

3.1 备份

bash 复制代码
# 1) 每个流程组导出 template(UI: 选中组 → 右上角 → Create template → 下载)
#    这是最干净的"业务资产"备份,跨集群可导入
​
# 2) 配置与证书整目录备份
tar czf /backup/nifi-conf-$(date +%F).tar.gz -C /opt/nifi conf

模板备份比 flow.json.gz 更重要:跨集群迁移只认 template,不认 flow.json.gz。后者带着原集群的节点信息和组件 UUID,导进新集群会打架。

3.2 二十五个节点一起重签

25 个 FQDN 手写容易漏,用脚本生成参数:

bash 复制代码
cd /opt/nifi-toolkit-1.17.0
​
HOSTS=$(for n in $(seq 1 25); do printf 'nifi-node-%02d.example.com,' "$n"; done)
HOSTS=${HOSTS%,}
​
bin/tls-toolkit.sh standalone \
  -C 'CN=nifi,OU=NIFI' \
  -n "$HOSTS" \
  --keyPassword '<KEYSTORE_PASS>' \
  --keyStorePassword '<KEYSTORE_PASS>' \
  --trustStorePassword '<KEYSTORE_PASS>' \
  -o 'certs-bak-<YYYYMMDD>'

要点:

  • -n 必须把 25 台全列上,少一台,那台就没有可用证书;
  • 输出目录带日期,不要覆盖上一套,回滚靠它;
  • 这一步会生成新的 CA,旧证书全部作废,25 台必须同时换;
  • 生成完先数一下目录数:ls -d certs-bak-<YYYYMMDD>/*/ | wc -l,应该是 25。

3.3 分发:循环 + 校验

bash 复制代码
cd /opt/nifi-toolkit-1.17.0/certs-bak-<YYYYMMDD>
​
for n in $(seq 1 25); do
  h=$(printf 'nifi-node-%02d' "$n")
  ssh "$h" "mkdir -p /backup/certs-$(date +%F) && cp -a /opt/nifi/conf/*.jks /opt/nifi/conf/*.pem /opt/nifi/conf/*.key /backup/certs-$(date +%F)/ 2>/dev/null"
  scp ${h}.example.com/{nifi-cert.pem,nifi-key.key,keystore.jks,truststore.jks} "$h":/opt/nifi/conf/
done

分发完必须做一致性校验------25 台靠人眼看是不可能的:

bash 复制代码
# 每台的 truststore 指纹必须完全一致(同一个 CA)
for n in $(seq 1 25); do
  h=$(printf 'nifi-node-%02d' "$n")
  ssh "$h" "keytool -list -keystore /opt/nifi/conf/truststore.jks -storepass '<KEYSTORE_PASS>' -v 2>/dev/null | grep -m1 'SHA256:'"
done | awk '{print $2}' | sort | uniq -c
# 期望:只输出 1 行,计数 25

同理,每台 keystore 里的 Owner 必须是自己的 FQDN:

bash 复制代码
for n in $(seq 1 25); do
  h=$(printf 'nifi-node-%02d' "$n")
  printf '%s ' "$h"
  ssh "$h" "keytool -list -v -keystore /opt/nifi/conf/keystore.jks -storepass '<KEYSTORE_PASS>' 2>/dev/null | grep -m1 Owner:"
done

这一步花 2 分钟,能省掉后面几小时的排查。

3.4 新节点安装:全新解压,只拷配置

bash 复制代码
# 新节点上
unzip nifi-1.17.0-bin.zip -d /opt
mv /opt/nifi-1.17.0 /opt/nifi
useradd nifi

# 从老节点拷"配置",一个文件一个文件拷,不拷目录
for f in nifi.properties authorizers.xml login-identity-providers.xml \
         state-management.xml zookeeper.properties bootstrap.conf; do
  scp nifi-node-01:/opt/nifi/conf/$f /opt/nifi/conf/
done

mkdir -p /home/nifi
scp nifi-node-01:/home/nifi/nifi.keytab /home/nifi/
chown -R nifi:nifi /home/nifi && chmod 600 /home/nifi/nifi.keytab

3.5 主机名三处,用脚本改

以 nifi-node-04 为例,25 台每一台都要改成自己的:

ini 复制代码
nifi.web.https.host=nifi-node-04.example.com
nifi.cluster.node.address=nifi-node-04.example.com
nifi.remote.input.host=nifi-node-04.example.com

手工改 25 台不现实,统一用 sed:

bash 复制代码
for n in $(seq 1 25); do
  h=$(printf 'nifi-node-%02d' "$n")
  ssh "$h" "sed -i \
    -e 's#^nifi.web.https.host=.*#nifi.web.https.host=${h}.example.com#' \
    -e 's#^nifi.cluster.node.address=.*#nifi.cluster.node.address=${h}.example.com#' \
    -e 's#^nifi.remote.input.host=.*#nifi.remote.input.host=${h}.example.com#' \
    /opt/nifi/conf/nifi.properties"
done

端口保持全集群一致:9443 / 11443 / 6342 / 10443。ZooKeeper 只在最初那 3 台上开:

ini 复制代码
# node-01/02/03
nifi.state.management.embedded.zookeeper.start=true
# node-04 ~ 25
nifi.state.management.embedded.zookeeper.start=false

nifi.zookeeper.connect.string 25 台写成同一串,指向 01/02/03 的 22181。

3.5.1 内嵌 ZooKeeper 到底怎么配

上面两行 true/false 只是开关,真正的配置在 conf/zookeeper.properties。这个文件 25 台都要有(NiFi 启动时会读它),但只有 01/02/03 会真的把内嵌 ZK 跑起来。

三台 ZK 的 zookeeper.properties 写法完全一样,server.N 写全三行:

ini 复制代码
# /opt/nifi/conf/zookeeper.properties (01/02/03 内容一致)
initLimit=10
syncLimit=5
dataDir=/opt/nifi/state/zookeeper
clientPort=22181
server.1=nifi-node-01.example.com:2888:3888;22181
server.2=nifi-node-02.example.com:2888:3888;22181
server.3=nifi-node-03.example.com:2888:3888;22181

server.N=host:A:B;C 这三段的含义,我第一年一直记混,列成表就清楚了:

段 例子 含义 谁来连
主机名 nifi-node-01.example.com 该 ZK 节点地址,25 台都要能解析 全部 ZK 节点 + NiFi
第 2 段 A 2888 数据同步端口(quorum) leader 与 follower 之间同步事务
第 3 段 B 3888 选主端口(election) 选主时 follower 之间通信
;C ;22181 客户端端口 NiFi 作为客户端连的端口,对应 nifi.zookeeper.connect.string

配套的 state/zookeeper/myid:只能是一个数字,1 / 2 / 3,并且必须和 server.N 的 N 对上。

bash 复制代码
# 只在 01/02/03 上执行,且各写各的
echo 1 > /opt/nifi/state/zookeeper/myid   # 仅 nifi-node-01
echo 2 > /opt/nifi/state/zookeeper/myid   # 仅 nifi-node-02
echo 3 > /opt/nifi/state/zookeeper/myid   # 仅 nifi-node-03
chown -R nifi:nifi /opt/nifi/state/zookeeper

04~25 这 22 台不跑 ZK ,正确状态是:embedded.zookeeper.start=false,并且不携带 state/zookeeper/ 这个目录 。从老节点拷配置时如果把 state/ 整个带过去,新节点就多出一个 myid,一旦哪天这个目录被 ZK 认了,就是两台机器同一个 ID,ZK 直接拒绝启动------这一条在第 9 节还会出现一次。

真实坑:端口撞了,bind 失败

有一轮扩容,nifi-node-02 的内嵌 ZK 就是起不来。logs/nifi-app.log 里是这么一行:

css 复制代码
ERROR [main] o.a.n.state.management.embedded.StandaloneServer
Exception while listening to address /10.20.30.12:3888, java.net.BindException: Cannot assign requested address

这行日志很容易看偏:Cannot assign requested address 读起来像"地址不存在",我第一反应是 /etc/hosts 写错了、DNS 解析不到。实际是 bind 失败 ------端口被别人占了,或者绑定的地址不属于本机。当时是同一台机器上一个采集 agent 占了这一段动态端口,3888 和 22181 都落在它的范围里。

处置也很典型:01/02/03 上把 3888 改成 23888、客户端端口把 22181 改成 22191(server.N 行末尾和 clientPort 一起改),ZK 正常起来了。

然后问题从"起不来"变成了"连不上":一部分节点 ACTIVE,一部分反复 CONNECTING。原因很简单------只改了这 3 台的 zookeeper.properties,忘了改 nifi.properties 里的 nifi.zookeeper.connect.string,于是 22 个客户端里有一半还在敲旧端口。

根因 :nifi.zookeeper.connect.string 是每个 NiFi 节点各自读自己的 nifi.properties,换 ZK 端口不是那三台的事,是全集群 25 台一起改的事;只改 ZK 侧,客户端就会去敲一个已经关掉的门。

处置 :全集群同步改这一个 key,然后逐台重启(分批,见 3.8)。改完必须校验------grep 出来只能有一种值:

bash 复制代码
for n in $(seq 1 25); do
  h=$(printf 'nifi-node-%02d' "$n")
  printf '%s ' "$h"
  ssh "$h" "grep '^nifi.zookeeper.connect.string' /opt/nifi/conf/nifi.properties"
done | awk -F= '{print $2}' | sort | uniq -c
# 期望:1 行,计数 25

从那以后,凡是动"全集群共用的那个字符串",我都按三步走:改之前先 grep 一遍现状、改完 uniq -c 校验、最后才重启。这条规则后来被搬进 Ansible(第 8 节),因为手工 for 循环做这三步,迟早会漏一台。

3.6 authorizers.xml:25 条 Node Identity,脚本生成

千万别手打。用一个占位块 + 脚本替换:

bash 复制代码
# 生成 25 条
{
  for n in $(seq 1 25); do
    printf '    <property name="Node Identity %d">CN=nifi-node-%02d.example.com, OU=NIFI</property>\n' "$n" "$n"
  done
} > /tmp/node-identities.xml

# 把 authorizer 里旧的 Node Identity 行整段替换掉(先备份)
for n in $(seq 1 25); do
  h=$(printf 'nifi-node-%02d' "$n")
  ssh "$h" "cp /opt/nifi/conf/authorizers.xml /opt/nifi/conf/authorizers.xml.bak && \
            sed -i '/Node Identity/d' /opt/nifi/conf/authorizers.xml"
  scp /tmp/node-identities.xml "$h":/tmp/
  ssh "$h" "sed -i '/<property name="Legacy Authorized Users File">/r /tmp/node-identities.xml' /opt/nifi/conf/authorizers.xml"
done

三件事必须同时满足:

  1. 25 台节点上的这 25 条完全一致 (包括 , 里的空格);
  2. DN 和证书 Owner 逐字一致;
  3. users.xml / authorizations.xml 里不要残留旧 DN 的用户记录。

校验:

bash 复制代码
# 每台的行数应当都是 25,md5 应当只有一种
for n in $(seq 1 25); do
  h=$(printf 'nifi-node-%02d' "$n")
  printf '%s ' "$h"
  ssh "$h" "grep -c 'Node Identity' /opt/nifi/conf/authorizers.xml; md5sum /opt/nifi/conf/authorizers.xml | cut -c1-8"
done

3.7 新节点的敏感属性密钥

nifi.sensitive.props.key 要和现有集群完全相同,否则新节点读到 flow 里的加密属性会解不开:

python 复制代码
/opt/nifi/bin/nifi.sh set-sensitive-properties-key '<SENSITIVE_KEY>'

3.8 停机窗口与分批启动

25 台不能"逐台滚动换证书" 。原因很简单:换证书期间,新 CA 和旧 CA 互不信任,先换的节点和后换的节点之间 TLS 直接断。你以为是滚动升级,实际是让集群分批裂开。

正确做法是一个窗口内全停、全换、分批起:

bash 复制代码
# 1) 全停(从最后一个节点开始往前停,减少 primary 切换抖动)
for n in $(seq 25 -1 1); do
  h=$(printf 'nifi-node-%02d' "$n")
  ssh "$h" "/opt/nifi/bin/nifi.sh stop"
done

# 2) 先起 3 台 ZK 节点,等 ACTIVE
for n in 01 02 03; do
  ssh nifi-node-$n "/opt/nifi/bin/nifi.sh start"
done
# 集群页面看到 3 个 ACTIVE 再继续

# 3) 其余 22 台,每批 5~6 台,等这批全 ACTIVE 再起下一批

为什么分批:冷启动时 primary 节点要重建整个集群的 flow 状态,25 台同时冲会让选主反复重试,nifi.cluster.flow.election.max.wait.time 默认 5 分钟,很容易出现"起了半小时还没 ACTIVE"的假故障。分批反而快。

扩容失败怎么退

窗口是有限的,失败是常态。所以扩容方案必须自带退路,而且退路要能在 10 分钟内跑起来。我们的退路是三条硬规矩:

  1. 证书输出目录必须带日期,旧目录一个都不删 。certs-bak-<YYYYMMDD>/ 是回滚的唯一依据,新目录只增不删。有一年清理磁盘时把旧证书目录当垃圾删了,那之后每次扩容都心慌------后来规定 /backup/certs-* 一律不参与清理。
  2. authorizers.xml 分发前先 cp 一份 .bak。这个文件是全集群逐字一致的,改坏了没有任何"局部修复"的办法,只能整份还原。
  3. 回退是全量回退 :4 个证书文件(keystore.jks、truststore.jks、nifi-cert.pem、nifi-key.key)+ 旧 authorizers.xml,一起还原到全部节点 ,再重启。不能只回滚一台------证书和 Node Identity 是全集群一致的,单台回滚等于制造新的不一致,集群当场就裂。

回退脚本长这样,事先写好、窗口前试跑一遍:

bash 复制代码
#!/bin/bash
# rollback-certs.sh <旧证书目录>  ------ 全量回退,不许分批
OLD=$1   # 例:certs-bak-<YYYYMMDD>
[ -d "$OLD" ] || { echo "旧证书目录不存在,中止"; exit 1; }

for n in $(seq 1 25); do
  h=$(printf 'nifi-node-%02d' "$n")
  ssh "$h" "/opt/nifi/bin/nifi.sh stop"
  scp "$OLD/${h}.example.com"/{nifi-cert.pem,nifi-key.key,keystore.jks,truststore.jks} "$h":/opt/nifi/conf/
  ssh "$h" "cp /opt/nifi/conf/authorizers.xml.bak /opt/nifi/conf/authorizers.xml 2>/dev/null; \
            chown nifi:nifi /opt/nifi/conf/*.jks /opt/nifi/conf/*.pem /opt/nifi/conf/*.key"
done
# 校验指纹只有一种值,再按 3.8 的顺序分批启动

真实经历 :有一轮扩容,证书分发完发现 truststore 指纹有两种值------有几台 scp 时目标文件正被进程占用,写了一半。那一次我们把整集群回退到旧证书、旧 flow,业务停了一个窗口,第二天重做才成功。事后复盘,唯一值得肯定的地方是回退执行得快:stop → scp → cp .bak → start,四步都在脚本里,没人需要现场回忆命令。

所以"回退方案要能在 10 分钟内执行",靠的是脚本 ,不是记忆。人一紧张就会漏 chown,就会忘 authorizers.xml.bak,就会想着"先回滚一台看看"。

3.9 新节点起来报 flow 冲突,第一反应是什么

扩容窗口里最常见的一种"节点加不进来",日志长这样。三种我按出现频率排:

css 复制代码
# 1) 最常见:本地 flow 和集群 flow 是两套,没法继承
ERROR [main] org.apache.nifi.web.server.JettyServer
UninheritableFlowException: Failed to connect node to cluster because local flow is different than cluster flow.

# 2) 伴随 1) 出现,说明本地 flow 已经写坏了一半,要先 disconnect
FlowSynchronizationException: Failed to synchronize with node <UUID>; local flow controller partially updated.
Administrator should disconnect node and review flow for corruption.

# 3) 版本差太多,proposed flow 无法被本地 flow controller 接受
Proposed flow is not inheritable by the flow controller ...

一句话处置:

bash 复制代码
/opt/nifi/bin/nifi.sh stop
sleep 10                      # 确认进程真的退出了:ps -ef | grep nifi 为空
ls /opt/nifi/conf/flow*       # 先看清有哪些,1.17 一般只有 flow.json.gz
rm -f /opt/nifi/conf/flow.json.gz /opt/nifi/conf/flow.xml.gz
/opt/nifi/bin/nifi.sh start   # 起来后从 primary 重新拉一份 flow

顺序不能反:必须先停服务,再删文件。 运行中的 NiFi 会周期性把内存里的 flow checkpoint 回磁盘(改配置时更是立即落盘),你先删、它后写,两分钟后 flow.json.gz 又长回来,你会以为是"删了没用"。

完整的机制、flow.json.gz 和 flow.json.gz.bak 的关系、什么时候该 disconnect 而不是删文件,放在第 5 篇讲,这里只给动作。顺便提醒一句:这是扩容后"节点加不进来"最常见的原因,别一上来就怀疑证书、重签一遍 。两者日志长得完全不一样------证书问题是 TLS 握手失败、certificate_unknown、unable to find valid certification path;flow 冲突是节点已经握手成功、进了集群协商阶段才报错。先看日志里有没有 SSL,能省掉一次无谓的重签。

4. 为什么"只补签新节点"不行

这是每次扩容都会争论一遍的问题,值得说透。

tls-toolkit.sh standalone 每次执行都会新生成一套 CA (根证书 nifi-cert.pem + CA 私钥)。新 CA 签出来的证书,老节点的 truststore.jks 里没有这个 CA,所以:

  • 新节点 → 老节点:老节点不认新证书,握手失败;
  • 老节点 → 新节点:新节点的 truststore 里只有新 CA,不认老证书,同样失败。

想让"只补签"成立,只有一条路:自己维护一套长期 CA,用内部 CA 给新节点签发证书,再把新证书导入全部节点的 truststore。这在有企业 CA 的环境里是标准做法,但那意味着:

  1. 证书生命周期、CRL、续期都得自己管;
  2. 每次扩容都要改 25 台 truststore 并重启;
  3. 一旦 CA 私钥丢失,整个集群无法恢复。

对"一年扩一两次"的场景,全量重签反而更省事:一条命令、一次分发、一次窗口,且证书有效期统一,下次过期时间可预期。我们三轮扩容都是全量重签,第三轮 12 台一次性加进来也没出问题------前提是严格按第 3 节的顺序。

5. 25 台才暴露的四件事

3 台、7 台的时候感觉不到,到了 25 台才变成问题:

5.1 3 台内置 ZK 成了瓶颈 25 个节点的集群状态、flow 版本、primary 选举都压在 3 台 ZK 上。ZK 的写是串行的,节点越多心跳越密集。建议:要么把内置 ZK 扩到 5 台(奇数),要么直接换成外部 ZK 集群。

5.2 一次加 12 台,启动批次比证书更容易翻车 12 台一起起,最先起来的会等后面的,election.max.wait.time 一到就开始重选。我们最后一轮的经验是:每批 5~6 台,等全 ACTIVE 再下一批,总耗时反而比一次性起短。

5.3 一致性只能靠脚本,不能靠人眼 25 台的 authorizers.xml、users.xml、authorizations.xml 必须逐字一致。人眼对 25 个文件无效,只能靠 md5sum + uniq -c。把校验做成脚本,每次变更后跑一遍,是这套环境能稳住的根本原因。

5.4 证书换了,truststore 没换

现象:这条在 3 台时几乎不会犯------三个文件,改完顺手就核对了;到 25 台,它变成了"看起来最不像证书问题"的故障。所有节点都能启动,UI 能登录,但集群页面上的节点一个接一个掉线,几分钟后只剩几台 ACTIVE。重启能好一阵,然后又掉。第一反应通常是网络、是 ZK、是负载------因为页面不会告诉你"证书不匹配"。

日志 :在掉线节点的 logs/nifi-app.log 里搜 SSL,能看到这一类:

css 复制代码
WARN [Clustering Tasks Thread-1] o.a.n.c.c.NodeProtocolSender
Remote input request failed for node <UUID>
javax.net.ssl.SSLHandshakeException: Received fatal alert: certificate_unknown

同一个文件里往往还有 PKIX path building failed / unable to find valid certification path to requested target------那是"本机不认对方的 CA",和上面那条是同一件事的两个方向。

根因 :只换了 keystore(节点自己的证书),truststore 还是旧 CA;或者反过来,某几台只更新了 truststore,keystore 还是旧的。两种情况都会让"一半信任另一半"。第二种最隐蔽:truststore.jks 换了以后,节点能验证别人,但自己出示的还是旧证书,对方 truststore 里没有旧 CA,于是握手失败------错误信息指向"对方不认我",很容易把排查方向带偏。

情况 表现 谁不认谁
keystore 新、truststore 旧 自己的新证书别人验不过 其他节点不认这台
truststore 新、keystore 旧 自己拿旧证书去握手 这台不认其他节点
25 台里有 1 台漏换 只有那台反复掉线 那台和其余 24 台互不信任

处置 :回到 3.3,全量重签 + 全量分发 + 全量校验,不要"就补那几台"。核心就一条命令------把 25 台的 truststore 指纹收上来,uniq -c 必须只有一行:

bash 复制代码
ansible -m shell -a "keytool -list -keystore /opt/nifi/conf/truststore.jks \
  -storepass '<KEYSTORE_PASS>' -v | grep -m1 'SHA256:'" nifi \
  | awk '{print $2}' | sort | uniq -c
# 期望:25  <同一个指纹>

keystore 的 Owner 同理(3.3 的第二个循环):25 个 FQDN 各不相同,才算换对了。

这两项都对得上------25 台 truststore 指纹只有一行、25 个 keystore Owner 各不相同------才是扩容收工的唯一证据。UI 上 25 个绿点不算,绿点会骗人,指纹不会。

6. 那套 5 节点小集群:别和主集群混

主集群之外还有一套 5 节点的小集群,跑测试和边缘接入。两条规矩:

  1. 证书各签各的 。小集群自己一套 CA、自己一套 truststore.jks。绝对不要把主集群的证书拷到小集群,也不要把两套证书混装在同一台机器上------一旦混了,Node Identity 和 DN 的对应关系就再也理不清。
  2. 授权文件各管各的 。小集群的 users.xml / authorizations.xml 独立维护。两边如果需要互传数据,走站点到站点(S2S),并在接收端的进程组上单独授 receive data via site-to-site,而不是共用一套授权。

一句话:一个集群一套 CA、一套 Node Identity、一套授权文件。集群之间只通过 S2S 端口说话。

7. 验证清单

扩容完不要只看 UI 上节点数变成 25 就收工:

检查项 方法 通过标准
节点数 Cluster 页面 25 个节点,状态全 ACTIVE
集群通信 各节点 logs/nifi-app.log 无 Unable to communicate with node、无反复 Disconnected
流程完整性 根画布 Process Groups 组数量、组件数量与备份一致
队列 各组件队列计数 无异常堆积、无 full
数据抽样 下游 HDFS 目录 新增文件数、文件大小与上游一致
站点到站点 任一组件的 S2S 输入/输出 能建立连接
证书 keytool -list 25 台都是新 DN、新有效期、同一 CA
授权 用普通用户登录 只能看到自己的流程组
truststore 指纹唯一 uniq -c 统计 25 台的 SHA256 只有一行,计数 25
/etc/hosts 25 台一致 `ansible -m shell -a 'md5sum /etc/hosts' nifi sort

回滚方案只有一条:把 certs-bak-<旧日期> 的 4 个文件 + 旧 authorizers.xml 一起还原到 全部节点 ,重启。不能只回滚一台------证书和 Node Identity 是全集群一致的,单台回滚等于制造新的不一致。具体脚本见 3.8 的"扩容失败怎么退"。

8. 用 Ansible 管 25 台,别再写 for 循环

前面所有命令,我最早都是用 for n in $(seq 1 25) + ssh 写的。3 台的时候它很好用,25 台的时候它是负担。

先是速度:串行 ssh,每台连一次、跑一条命令、断开,25 台跑完十几分钟。窗口只有几个小时,十几分钟就为了 grep 一行配置,很亏。

再是参数:端口不是 22 的机器要加 -p,跳板机要加 -o ProxyCommand,一条命令能攒出五六个参数,写一次错一次。最要命的是可靠性------for 循环失败不重试 :第 7 台 ssh 超时,脚本继续往下跑,你看到最后一行输出了,以为全成功了;实际上第 7 台根本没改。而且不幂等 ------同一个脚本跑两遍,mkdir 报错、sed 加两遍、scp 覆盖到一半。

换成 Ansible 之后,同样的事一条命令。差异是量级的:

维度 for + ssh Ansible
25 台耗时 12~18 分钟(串行) 40 秒~1 分钟(forks=20)
失败处理 无,脚本继续跑 单台失败不影响其他,--limit 可只重跑失败的
幂等 不幂等,跑两遍出问题 模块幂等,跑十遍一个结果
备份 自己写 cp,经常忘 copy 加 backup=yes,自动带时间戳
参数 端口、用户、跳板机一堆 -o inventory 里写一次
结果确认 满屏日志翻着找 -o 一台一行,异常一眼看出来

8.1 inventory:按"角色"分组,不按"机器"分组

ini 复制代码
# /etc/ansible/hosts
[nifi]
nifi-node-[01:25].example.com

[nifi_new]
nifi-node-[14:25].example.com

[zk]
nifi-node-01.example.com
nifi-node-02.example.com
nifi-node-03.example.com

[nifi:vars]
ansible_user=root
ansible_port=22
ansible_ssh_private_key_file=/root/.ssh/id_rsa

三个组的分法是有讲究的:[nifi] 是全集群,25 台一致的操作都打它;[nifi_new] 是本次新增,只在装包、改主机名、建目录时用,避免把老节点也重装一遍;[zk] 只有三台,重启 ZK、看 ZK 日志、改 zookeeper.properties 只打它。

ansible.cfg 里把并发调上去:

ini 复制代码
[defaults]
inventory = /etc/ansible/hosts
forks = 20
host_key_checking = False
timeout = 20
gathering = explicit

[ssh_connection]
pipelining = True

forks=20 而不是 25,是留余量:SSH 建连有抖动,卡一台的时候剩下的还能跑完。真急的时候命令行 -f 25 覆盖。

8.2 四个常用命令,覆盖 90% 的扩容操作

场景 命令
批量查(不改任何东西) ansible -m shell -a 'uptime; df -h /opt; grep -c "Node Identity" /opt/nifi/conf/authorizers.xml' nifi -o
分发配置(自动备份) ansible -b -m copy -a 'src=/tmp/authorizers.xml dest=/opt/nifi/conf/authorizers.xml owner=nifi group=nifi mode=0644 backup=yes' nifi
建目录(幂等) ansible -b -m file -a 'path=/backup/certs state=directory owner=nifi group=nifi mode=0750' nifi
一致性校验 `ansible -m shell -a 'md5sum /opt/nifi/conf/authorizers.xml' nifi

再补两个窗口里必用的:只对新节点操作就换 nifi_new 组(ansible -m shell -a 'hostname -f' nifi_new -o);分批重启用 -f 5 覆盖并发(ansible -m shell -a '/opt/nifi/bin/nifi.sh restart' nifi -f 5),一批全回来再起下一批。

-o 这个参数值得单独说:它把每台的结果压成一行,25 台 25 行,谁失败了一眼就能看到(行首是 FAILED 或 UNREACHABLE)。不加 -o,输出是几百行 JSON,窗口里根本没时间翻。

8.3 三条纪律

用 Ansible 不等于安全,它只是把"手抖"的概率降低。真正让 25 台保持一致的是下面三条,缺一条都会在某个凌晨以"节点掉线"的形式还回来。

  1. 分发前必带 backup=yes 。copy 不带这个参数,就是原地覆盖,覆盖错了没有回头路。带上以后,目标位置会自动留一份带时间戳的备份,出问题 cp 回来就行------这是 3.8 里"authorizers.xml 先 cp .bak"的自动化版本。
  2. 分发后必跑校验 。copy 返回 changed 只说明"文件传过去了",不说明"内容是你要的"。校验三件套:md5sum(内容一致)、keytool 指纹(证书一致)、grep -c 行数(条目数一致)。三条都过,才算分发完成。
  3. 属主统一 nifi:nifi 。用 root 通过 Ansible 拷过去的文件,属主默认是 root。NiFi 以 nifi 用户跑,读不了 authorizers.xml 就可能起不来;更恶心的是读得 了一部分、读不了另一部分,服务起得来,但某个授权判断静默失效,然后在某天以"节点掉线""登录后空白"的形式爆出来。所以 copy 和 file 模块里 owner=nifi group=nifi 是必填项,不是可选项。这一类问题的排查路径,第 5 篇里有完整的一条。

顺带一句:Ansible 只解决"批量",不解决"顺序"。分批启动、先 ZK 后节点、先停流程再动证书------这些顺序还是得人来定,-f 5 只是把"每批 5 台"变成一条命令,不能代替你看完这一批是不是真的 ACTIVE 了。

9. 那几轮真正踩的坑

  1. 新节点主机名和证书 CN 不一致 装机时 hostname 写的是短名,证书 CN 是 FQDN,DN 匹配不上。教训:装机第一件事就是 hostnamectl set-hostname <FQDN>,然后才签证书。
  2. authorizers.xml 里混入旧 DN 早期升级时手工加过一个 IP 形式的 Node Identity,扩容时没删,结果老节点仍然能连、新节点连不上。Node Identity 列表要"全量替换",不是"追加"。
  3. 新节点忘了 set-sensitive-properties-key 流程能起来,但某个 HDFS 处理器报凭据解不开,队列卡死。补执行后重启恢复。
  4. myid 重复 新节点拷配置时把 state/zookeeper/myid 也带过去了,两个节点都写 3,ZK 直接不认。新节点不跑 ZK,正确做法是 embedded.zookeeper.start=false 且不要这个目录。
  5. 25 台同时启动,选主超时 见 3.8,分批起就没事。
  6. 以为"只给新节点签证书"能省事 第一轮扩容时试过,最后全线回滚重签。省下的半小时,花了两天补回来。
  7. 扩容时流程没停 边跑边做扩容准备,flow.json.gz 一直在变,新节点继承到半新半旧的 flow,最后整集群回退、换窗口重做。见 2.4。这一条的代价最大,因为它不是"某台起不来",是"整个窗口白开"。
  8. ZK 端口撞了 3888 和采集 agent 的端口段重合,ZK 报 BindException: Cannot assign requested address;改端口后忘了同步 nifi.zookeeper.connect.string,一半节点连旧端口。见 3.5.1。教训:端口不是"ZK 那三台的事",是全集群 25 台的事。
  9. 用 systemctl 拉起的节点,加入集群后通信异常 早期为了"开机自启"写了一版 unit,User 和 LimitNOFILE 跟 nifi.sh start 拉起来的不一致,节点能加入、能显示 ACTIVE,但 S2S 和集群心跳时不时超时,表现是"节点随机掉线又自己回来"。这类"看起来是网络、实际是进程环境"的问题,定位方法在第 5 篇。

10. 小结

把三轮扩容浓缩成三句话:

  1. 每一轮扩容,全部节点的证书都要重签、都要换,因为集群是双向 TLS,少一台的 truststore 里没有新 CA,新节点就 join 不进来;
  2. 安装包绝对不能用老集群打包拷贝 ,flow.json.gz、state/、证书、repository 会把"节点身份"和"组件 UUID"一起复制,后启动的节点会把先启动的挤下线;新节点只拷配置和 template;
  3. 25 台的一致性靠脚本 :生成、分发、校验全自动化,md5sum 只能有一种值。

再补两句,是后面几年运维日报里反复出现的:

  1. 扩容是一次停机变更,不是在线加机器。流程还在跑就别开始------先停流程、排空队列、导出模板、全停服务,再动证书。边跑边扩,你继承到的是一个移动的目标。
  2. 退路要和方案一起写 。证书目录带日期且不删、authorizers.xml 留 .bak、回退脚本提前试跑一遍。回退必须全量,单台回退就是制造新的不一致。

下一篇讲权限:为什么 NiFi 的授权既不是 RBAC、也不是"建个用户给他个组"那么简单,users.xml 和 authorizations.xml 到底在存什么,以及怎么做到"一个业务一个流程组、互相看不见"。

相关推荐
做科研的周师兄21 分钟前
【R语言实战】MixSIAR 氢氧同位素水源解析全流程:从混合空间校验到贝叶斯 MCMC 收敛诊断(后附完整代码)
java·大数据·开发语言
isfox2 小时前
MapReduce 数据清洗:脏数据进来,干净数据出去,连 Reduce 都省了
大数据
Francek Chen4 小时前
【大数据处理与分析】数据仓库Hive:09 Hive 编程实践
大数据·数据仓库·hive·hadoop·sql
其实防守也摸鱼17 小时前
个人学习 Pwn 思路:从基础到漏洞利用的进阶路径
大数据·前端·人工智能·chrome·算法·安全·网络安全
美林数据Tempodata17 小时前
高职大数据技术专业升级的六个落地样本:从产业链需求库到实训室数据链路
大数据·人工智能·数字化转型·产教融合·ai教育
跨境卫士苏苏18 小时前
TikTokShop关联账户认定指引更新跨境代运营团队需要注意哪些管理责任
大数据·人工智能·市场营销·跨境电商·营销策略
迅易科技19 小时前
数据质量治理:从异常告警走向自动化根因诊断
大数据
组工部管理能手李哥19 小时前
一次政务知识库体验:AI回答之外,我更在意那行引用来源
大数据·人工智能·大模型·政务
GrowingIO技术社区20 小时前
SaaS与私有化部署怎么比较?责任、集成与成本
大数据·数据挖掘·产品