LabHub
배우기 러닝패스 코스

Apache Flink — 스트림을 엔진으로 돌린다 · 실행 계획 읽기 · 이론

실행 계획 읽기 — SQL 한 줄이 태스크 몇 개가 되는가

LabHub 에서 이어서 보기

한 줄 요약

EXPLAIN 은 SQL 이 어떤 연산자들로 바뀌는지 보여 주고, 그 사이의 Exchange 가 데이터를 네트워크로 섞는 자리다. 섞지 않고 이어지는(FORWARD) 연산자들은 체이닝으로 한 태스크에 묶인다. 그래서 계획의 Exchange 를 세면 잡이 정점 몇 개로 뜰지 미리 알 수 있고, 두 단계 집계나 distinct 분할 같은 튜닝이 실제로 먹었는지도 계획으로 확인한다.

개념 지도: Exchange · 체이닝 · 계획 · EXPLAIN 의 세 구역.

왜 이게 필요했나

"잡이 느리다" 는 신고를 받으면 대개 병렬도부터 올린다. 그런데 병목이 한 키에 몰린 집계라면 병렬도를 올려도 그 키를 맡은 서브태스크 하나가 여전히 전부를 받는다. 필터가 원천까지 내려가지 못해 모든 행을 읽고 있을 수도 있고, 체이닝이 끊겨 태스크 사이 넘김이 늘었을 수도 있다. 이 원인들은 처리량 그래프에서는 똑같이 보이고, 계획에서는 전부 다르게 보인다.

튜닝 옵션도 마찬가지다. table.exec.mini-batch.enabled 를 켰다고 두 단계 집계가 생기는 것이 아니라, 옵티마이저가 조건을 만족한다고 판단할 때만 계획이 바뀐다. 설정 파일을 믿지 말고 계획을 보라는 것이 이 모듈의 요점이다.

어떻게 동작하나

![왼쪽은 EXPLAIN 실행 계획을 아래(원천)에서 위(싱크)로 그린 것이다. TableSourceScan, Calc 다음에 Exchange hash[page] 가 있고 그 위에 GroupAggregate, Calc, Writer 가 있다. 가운데는 체이닝을 켠 잡으로, Exchange 를 경계로 정점이 둘 — Source→Calc 와 GroupAggregate→Calc→Writer — 로 묶인다. 오른쪽은 체이닝을 끈 잡으로 연산자 다섯 개가 각자 정점이 된다. 아래 띠는 mini-batch 를 켜면 Exchange 아래에 LocalGroupAggregate 가 생기고 위의 집계가 GlobalGroupAggregate 로 바뀐다는 것을 보여 준다](/course-media/flk-plan-chaining-v1.svg)

EXPLAIN 의 세 구역. EXPLAIN PLAN FOR <질의> 는 세 구역을 낸다(공식 문서의 EXPLAIN Statements).

| 구역 | 무엇 |
| --- | --- |
| == Abstract Syntax Tree == | SQL 을 그대로 옮긴 논리 계획. LogicalFilter·LogicalAggregate |
| == Optimized Physical Plan == | 규칙을 적용한 물리 계획. 필터가 원천으로 내려가고 집계가 GroupAggregate 가 된다 |
| == Optimized Execution Plan == | 실제로 만들어질 연산자 트리 |

트리는 위가 싱크 쪽, 아래가 원천 쪽이다. 이 실습의 질의를 스트리밍으로 EXPLAIN 하면 GroupAggregate ← Exchange(distribution=[hash[page]]) ← Calc ← TableSourceScan 이 나오고, 원천 줄에 filter=[>(amount, 0)] 가 붙는다 — WHERE 가 원천까지 내려갔다는 뜻이다. Exchange 는 같은 키의 행을 같은 서브태스크로 보내려고 해시로 섞는 자리이고, 집계는 반드시 그 위에 선다.

EXPLAIN 에는 붙일 수 있는 세부가 있다. ESTIMATED_COST 는 노드마다 추정 행 수와 누적 비용을, CHANGELOG_MODE 는 변경 로그 종류(2모듈)를, PLAN_ADVICE 는 위험 경고와 튜닝 조언을, JSON_EXECUTION_PLAN 은 연산자 그래프를 JSON 으로 붙인다. 이 파드에서 GROUP BY 에 PLAN_ADVICE 를 붙이면 "local-global two-phase 를 켜 보라" 는 [ADVICE] 가 나온다. 추정 비용은 통계가 없는 파일 원천에 대한 가정값이라 절대량으로 읽으면 안 된다.

체이닝과 정점. JSON 실행 계획의 노드는 연산자 하나하나이고, 노드 사이에 ship_strategy(FORWARD · HASH 등)가 적힌다. FORWARD 로 이어지고 병렬도가 같은 연산자들은 체인이 되어 한 스레드에서 돈다(1모듈). 그래서 선형 파이프라인이면 정점 수 = FORWARD 가 아닌 전달 수 + 1 이다. pipeline.operator-chaining.enabled = false 로 끄면 연산자마다 정점이 생긴다 — 디버깅할 때 연산자별 지표를 보려고 잠깐 끄는 스위치이지, 운영에서 켜 둘 것은 아니다.

두 단계 집계. mini-batch 는 입력을 잠깐 모아 키마다 상태 접근을 한 번으로 줄이는 장치다(Performance Tuning). mini-batch 가 켜지면 옵티마이저는 집계를 LocalGroupAggregate(섞기 전, 각 서브태스크에서 미리 합침)와 GlobalGroupAggregate(섞은 뒤 합침)로 나눌 수 있다. 문서는 이것을 MapReduce 의 Combine + Reduce 에 빗댄다. 한 키에 몰린 데이터가 있어도 Exchange 를 건너는 것은 미리 합친 누산기뿐이다. 이 파드에서는 mini-batch 세 설정만 주면(agg-phase-strategy 기본값 AUTO) 계획에 MiniBatchAssigner 와 Local/Global 이 나타났다.

distinct 분할. COUNT(DISTINCT user_id) 는 미리 합쳐도 잘 줄지 않는다 — 누산기가 결국 사용자 목록이기 때문이다. table.optimizer.distinct-agg.split.enabled 를 켜면 옵티마이저가 질의를 두 층으로 고쳐 쓴다. 첫 층은 MOD(HASH_CODE(user_id), 버킷 수) 를 키에 더해 섞고(PARTIAL), 둘째 층이 원래 키로 다시 섞어 합한다(FINAL). 버킷 수 기본값은 1024 다.

현장에서 만나는 모습

가장 자주 쓰는 장면은 "필터가 어디서 걸리나" 다. 원천이 필터 밀어 넣기를 지원하면 TableSourceScan 줄에 filter=[…] 가 붙는다. 붙지 않았다면 모든 행이 원천을 나와 Calc 에서야 버려진다.

두 번째는 핫 키. 대시보드에서 서브태스크 하나만 바쁘다면 계획의 Exchange(distribution=[hash[…]]) 키를 본다. 그 키의 분포가 치우쳐 있다면 병렬도가 아니라 두 단계 집계나 distinct 분할이 답이다. 켠 뒤에는 반드시 EXPLAIN 으로 Local/Global 또는 PARTIAL/FINAL 이 생겼는지 확인한다.

세 번째는 정점 수가 예상과 다를 때. 병렬도가 서로 다른 연산자 사이, 섞는 자리 사이에서는 체인이 끊긴다. /jobs/<jid> 의 정점 이름에 -> 로 묶인 연산자 목록이 나오므로, JSON 실행 계획의 전달 방식과 나란히 놓고 보면 어디서 끊겼는지 바로 보인다.

다음 실습에서 할 것

클릭 파일의 페이지별 집계를 EXPLAIN 해 세 구역을 받고, Exchange 의 위아래를 JSON 답안으로 옮긴다. 비용·조언을 붙인 EXPLAIN 과 JSON 실행 계획을 받은 뒤, 같은 INSERT 를 체이닝을 켜고 끈 두 번 돌려 정점 수를 JSON 의 전달 방식과 맞춘다. 마지막으로 mini-batch 두 단계 집계와 COUNT(DISTINCT) 분할의 계획을 받아 숫자를 보고서로 모은다.