Cloudera Manager 에서 3노드 NiFi 가 계속 빨간색으로 표시되고 Web UI 접속이 안 되는 상황이었다. 로그에는 java.lang.OutOfMemoryError 가 반복됐고, 힙은 -Xms32g -Xmx32g 로 잡혀 있었다. 힙을 48 GiB 로 올려도 다시 OOM 이 났으며, 원인은 두 가지가 겹쳐 있었다. 하나는 NiFi 를 9시간 내렸다 올린 뒤 Kafka 백로그가 ConsumeKafka 로 한꺼번에 유입된 것이고, 다른 하나는 당일 변경한 DB 수집 쿼리가 실행되는 프로세스 그룹이 결과 전체를 힙에 올린 것이다.
로그에서 읽어야 할 단서는 다음과 같았다.
checkpointed ... (Stop-the-world time = 21658 milliseconds) → 힙이 거의 가득 차 Full GC 를 반복하는 상태. ZooKeeper 세션 SUSPENDED, Kafka DisconnectException, 클러스터 heartbeat 지연은 모두 GC pause 의 2차 증상이다.10000 FlowFiles were swapped in ... QueueSize[FlowFiles=52906, ContentSize=39,484,951 Bytes] → 5만 건이 쌓였는데 content 는 39 MB 뿐. 건당 750 byte 인데 힙이 터진다는 것은 backpressure 가 무력화됐다는 뜻이다(기본 Object Threshold 10,000).UpdateAttribute, AttributesToJSON, PutKudu 에서 커밋 실패 반복 → 힙을 잡아먹는 대표 원인은 FlowFile attribute 에 큰 값을 넣는 것(attribute 는 content 와 달리 전부 힙에 상주)과 큐 적체다.Java 프로세스는 OOM 이 나도 자동으로 죽지 않고 GC 만 반복하며 살아 있으므로, 그대로 두면 클러스터 전체를 흔든다. 재시작해야 한다. kill 해도 flowfile/content repository 는 디스크에 있어 큐 데이터는 유실되지 않는다.
핵심은 "유입을 끊고 → 하류가 소화하게 하고 → 유입을 천천히 다시 여는 것" 이다.
jmap -histo:live 상위에 String, char[], StandardFlowFileRecord, HashMap$Node 가 GB 단위면 attribute 문제다.jstat -gcutil <nifi pid> 1000 5
jmap -histo:live <nifi pid> | head -30 > /tmp/nifi01_histo.txt
bootstrap.conf 에 힙 덤프 옵션을 추가한다(덤프가 힙 크기 이상이므로 디스크 여유 확인).java.arg.20=-XX:+HeapDumpOnOutOfMemoryError
java.arg.21=-XX:HeapDumpPath=/nifi/dump
nifi.sh stop 이 30초 안에 안 내려가면 kill -9 한다.kafka-consumer-groups.sh --describe --group <그룹> 의 LAG 이 0 에 가까워지는 것으로 확인한다.LDAP 인증인 경우 계정으로 토큰을 받아 Bearer 로 넣는다(Kerberos 면 curl --negotiate -u : -X POST .../access/kerberos).
read -s PW
TOKEN=$(curl -k -X POST https://nifi01.example.com:8443/nifi-api/access/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode "username=${NIFI_USER}" --data-urlencode "password=${PW}")
# 루트 그룹 전체 정지
curl -k -X PUT https://nifi01.example.com:8443/nifi-api/flow/process-groups/root \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"id":"root","state":"STOPPED"}'
# 특정 하위 PG 만 정지 — id 조회 후 같은 엔드포인트에 넣는다 (하위 PG 까지 재귀 적용)
curl -k -H "Authorization: Bearer $TOKEN" \
https://nifi01.example.com:8443/nifi-api/process-groups/root/process-groups \
| python3 -c 'import sys,json; [print(p["id"], p["component"]["name"]) for p in json.load(sys.stdin)["processGroups"]]'
PGID=<id>
curl -k -X PUT https://nifi01.example.com:8443/nifi-api/flow/process-groups/$PGID \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d "{\"id\":\"$PGID\",\"state\":\"STOPPED\"}"
# 실제로 멈췄는지 · 클러스터 반영 여부
curl -k -H "Authorization: Bearer $TOKEN" https://nifi01.example.com:8443/nifi-api/flow/process-groups/root/status \
| python3 -m json.tool | grep -E '"running|"stopped|"activeThreadCount|"queuedCount'
curl -k -H "Authorization: Bearer $TOKEN" https://nifi01.example.com:8443/nifi-api/controller/cluster \
| python3 -m json.tool | grep -E '"address|"status'
응답이 {"id":"root","state":"STOPPED"} 로 돌아와도 "접수" 일 뿐이므로 runningCount 가 0 인지 확인한다. This node is disconnected from the cluster 가 나오면 CONNECTED 인 노드로 요청을 보내야 하고, 전부 DISCONNECTED 면 3대 모두 내리고 동시에 올리는 편이 빠르다.
nifi.flowcontroller.autoResumeState=false 는 클러스터에서 무력화된다. 노드가 join 하는 순간 코디네이터가 가진 flow 상태(RUNNING)로 동기화되기 때문이다. 살아 있는 노드가 하나라도 있으면 의미가 없으며, 3대 모두 이 값을 설정하고 3대 전부 내린 뒤 동시에 올려야 stopped 상태로 뜬다.Xms=Xmx 동일 설정은 권장이다.nifi.queue.swap.threshold (기본 20000) 보다 커넥션의 backpressure 임계값을 크게 잡으면 힙에 올라가는 양이 늘어난다.Fetch Size 가 0(기본)이면 상당수 드라이버가 ResultSet 전체를 클라이언트 메모리에 올리고, Max Rows Per Flow File 이 0 이면 결과가 FlowFile 하나로 만들어지며 Avro 변환 시 큰 버퍼가 생긴다. 쿼리 변경 후 결과가 폭증하면 그대로 힙이 터진다. Fetch Size 1000~10000, Max Rows Per Flow File 10000, QueryDatabaseTable 은 Max Fragments 로 상한을 걸고 해당 PG 커넥션에 Data Size Threshold 를 건다. 켜기 전에 DB 툴에서 SELECT COUNT(*) 로 변경 전후 건수를 비교한다.queued 를 확인하고 비정상적으로 큰 것은 큐를 비운다.