Kafka Streaming 처리를 할 때, 자주 문제가 되는 부분은 보통 다음과 같은 것들이 있습니다.
- 잘못된 입력의 데이터
- 갑자기 들어온 많은 양의 데이터
잘못된 입력의 데이터는 각자 잘 알아서 처리를 하면되지만? 갑자기 들어오는 많은 양의 데이터는 어떻게 해야할까요? 간단한 방법은 처리하는 서버 대수를 늘리는 것입니다. 그런데 평소에는 2대면 충분한데, 피크때는 10대 정도가 필요한 상황이면 어떻게 해야할까요? 방법은 간단합니다. 그냥 항상 10대를 돌리면 됩니다. 돈만 많다면…(그러나 저는 거지입니다. T.T)
두번째 방법은 Spark의 Auto Scaling 을 이용하는 방법입니다. spark.streaming.dynamicAllocation 설정을 이용해서 Scaling이 가능합니다. 아래와 같은 설정을 이용합니다.(이 방법은 DStream을 사용할때만 유용하고, Structured Streaming 하고는 상관없을 수 있습니다.)
spark.dynamicAllocation.enabled=false
spark.streaming.dynamicAllocation.enabled=true
spark.streaming.dynamicAllocation.minExecutors=2
spark.streaming.dynamicAllocation.maxExecutors=8
#default
spark.streaming.dynamicAllocation.scalingInterval=60
spark.streaming.dynamicAllocation.scaleUpRatio=0.9
spark.streaming.dynamicAllocation.scaleDownRatio=0.3
원래 spark의 Dynamic Resource Allocation(DRA)이 Streaming 과는 상성이 맞지 않았습니다. Spark의 DRA는 Idle Timedㅡㄹ 체크하는데 Spark의 Streaming은 Micro 배치라 계속 일정시간 마다 작업을 하게되어서 Executor를 놓지 않아서, 늘어는 나도 줄어들지 않게 됩니다.
그래서 Spark 2.x에 나온것이 spark.streaming.dynamicAllocation 이 생겼습니다. spark.streaming.dynamicAllocation 정책은 처음부터 리소스를 전부 가져가지 않고, 다음과 같이 ratio를 계산해서 이를 이용하게 됩니다.
ratio = processing time / processing interval
- ratio > spark.streaming.dynamicAllocation.scaleUpRatio => executor 1개 추가
- ratio < spark.streaming.dynamicAllocation.scaleDownRatio => executor 1개 제거
이 값이 minExecutors, maxExecutors 설정의 최대치 까지 적용이 됩니다. 다만 이처리는 ExecutorAllocationManager 에서 처리되는데, 하나의 배치 안에서 증가/추가되지 않고, onBatchCompleted 가 호출될때 마다 실행시간 정보가 반영되어서 이번 Micro 배치가 끝나고 다음번에 영향을 주게 됩니다. 즉 하나의 배치 타이밍만에서만 크게 생기면 효과를 보기 힘들지만, 특정 시간동안 이슈가 있다면 서서히 증가해서 서서히 줄어들게 됩니다.
spark streaming dynamicAllocation을 쓸려면 기존의 spark.dynamicAllocation.enabled=false로 설정해야 합니다.
이렇게 Auto Scaling을 설정하면 모든게 해결될 것 같지만, 그렇지 않습니다. 일단 대부분의 Auto Scaling은 min/max 설정이 있습니다. 이 얘기는 해당 수준보다 더 데이터가 들어오면 결국은 뭔가 데이터 처리에 문제가 장애가 발생할 수 있다는 얘기가 됩니다. 그리고 비용도 더 든다는 얘깁니다. 그리고 또 문제가 되는것은 위의 Auto Scaling 설정도 위의 설정이 반영되는 것은, 엄청 많은 데이터가 들어온다면, 최소한 한번 그 많은 양을 처리해야 합니다.
그래서 이제 도리어 생각을 살짝 바꿔보면, 그냥 아주 느리게 평소에 처리할 수 있는 양 까지만 처리하면 되지 않을까라는 생각을 할 수 있습니다. 그래서 일정 수준까지만 처리하겠다라는 개념이 BackPressure 입니다.
보통 Spark에서 Kafka Streaming 에서 배치 때 처리하는 양은 지난번 처리한 마지막 offset 에서 현재 마지막 offset 까지를 가져와서 처리를 하게 됩니다. 즉, 한번에 1000개씩 처리하던 배치가 1분만에 10000 개가 들어오면, 그 배치 기간에는 10000개를 처리하게 됩니다. 이런 경우가 발생하는 경우가 다음과 같은 두 가지 경우가 있습니다.
- offset 정책이 earliest 면서 offset 정보가 없어서 처음으로 시작될 때
- 장애나, 트래픽이 늘어나서, 마지막 처리한 offset 과 현재의 offset 정보 차이가 많은 경우.
BackPressure를 설정하면, 딱 그만큼만 계속 가져오게 됩니다. 즉 1분마다 처리되는 Streaming 인데, BackPressure가 1000으로 설정되면 1분마다 1000개씩만 처리하게 됩니다. 전체 처리는 늦어지지만, 처리하는 입장에서는 부하가 늘어나지 않습니다. Spark 에서 Kafka Streaming은 DStream 과 Structured Streaming에서 셋팅 방법이 좀 다릅니다.(자세한 옵션을 다루지는 않습니다.)
DStream에서의 설정
spark.streaming.backpressure.enabled=true
spark.streaming.backpressure.initialRate=100000
spark.streaming.receiver.maxRate=100000
spark.streaming.kafka.maxRatePerPartition=20000
Structured Streaming 에서의 설정
코드에서 추가해야 합니다.
option("maxOffsetsPerTrigger", "100000")
Structured Streaming에서 주의할 점
Structured Streaming 에서는 checkpoint를 spark에서 관리를 하면서 다음 Offsets을 미리 만들어두고 진행하게 됩니다. 이게 생기게 되면, 해당 Offset 을 가져오려고 시도를 하고 maxOffsetsPerTrigger가 이 다음에 설정되면 해당 배치에는 적용이 되지 않고 그 다음 배치부터 적용되게 됩니다.(이 때는 최종 commit 된 다음 번호의 offset을 지워주면 됩니다.)
다음 offset 을 계산하는 방법에서 RateLimit을 설정해두는데, 이게 항상 현재 Kafka의 마지막 Offset이 되게 됩니다. 그런데 maxOffsetsPerTrigger 가 설정되면, RateLimit 가 해당 값으로 설정되서, 다음 Offset 이 현재 처리된 Offset + rateLImit 로 설정이 됩니다. 이를 이용해서 일종의 BackPressure가 설정이 되게 되는 것입니다. 해당 내용은 이전 블로그를 참고하세요. [입 개발] Spark Structured Streaming 에서 Offset 은 어떻게 관리되는가(아주 간략한 버전)? | Charsyam’s Blog (wordpress.com)
그런데 DStream 방식과 Structured Streaming에서 방식이 달라지는데, 이와 관련해서 정리를 하다보니 [SPARK-24815] Structured Streaming should support dynamic allocation – ASF JIRA (apache.org) 현재 dynamic allocation이 Structured Streaming 하고는 뭔가 어울리지 않는 것 같습니다. 그런데 또 코드를 보면 설정이 상관이 없는건 아닌듯 한… 코드를 보면 BackPressure 설정도 maxOffsetPerTrigger 는 kafka에만 존재하는 값이라 external/kafka-0-10-sql 안에 존재하고 있습니다.
뭔가… 정리하다 보니 이상하게 끝나버리지만… 대충 이렇습니다.