우리 포스트 테이블에는 약 3천만 개 미만의 포스트가 있습니다. 이로 인해 다른 포럼 소프트웨어에서 가져온 후 마크다운 수정을 수행하는 리베이크(rebake) 작업이 매우 비효율적인 상태입니다. 이를 최적화하는 방법을 찾고 있습니다.
특히 다음 호출은 현재 테스트 시스템에서 완료하는 데 수 시간이 걸립니다:
Post.update_all('baked_version = NULL')
이 플래그는 정확히 무엇을 하는 것인가요? 해당 필드는 정수 타입이지만, 리베이크 작업을 수행하기 전에 모든 포스트에 대해 초기화하는 것이 필수적인가요? 말씀드린 것처럼 포스트 테이블의 크기 때문에 이 단일 호출만 실행하는 데 수 시간이 소요됩니다. 저는 리베이크 코드를 약간 수정하여 리베이크가 호출되기 직전에 각 포스트마다 이 호출을 수행하도록 변경했습니다. 더 나은 방법이 있을까요?
또한, 성능에 가장 큰 영향을 미치는 요인은 코드에서 OFFSET의 사용으로 보입니다. 다음 줄이 심각한 문제를 일으키고 있습니다:
Post.order(id: :desc).offset(i).limit(batch).each do |post|
수백만 개의 포스트에 이르면 OFFSET은 현재 오프셋까지의 전체 테이블을 처리해야 하므로 문제가 됩니다. 엔진은 결과 집합을 반환하기 전에 오프셋 범위 이하의 모든 데이터를 폐기합니다. 이로 인해 수백만 개의 포스트에 불과한 시점에서도 호출에 수 분씩 소요됩니다. 이 과정이 발생하는 횟수는 배치 크기(기본값은 1000으로 하드코딩됨)에 따라 결정됩니다.
하드웨어 구성에 따라 지연 시간이 달라질 수 있다는 점은 인지하고 있지만, 더 많은 하드웨어를 투입하는 것이 해결책이 되지 않도록 개선할 수 있다고 생각합니다.
대신 저는 이 코드를 처리 중인 ID 범위에 대해 BETWEEN 연산자를 기반으로 작동하도록 수정했습니다. 제 이해로는 리베이크의 로직 자체는 전혀 변하지 않고, 단순히 SQL 쿼리를 훨씬 더 빠르게 만드는 것이라고 생각합니다.
실제로 제 경우, 이 변경으로 3개의 오더(1000배)의 성능 향상을 확인했습니다:
(0..(total).abs).step(batch) do |i|
Post.order(id: :desc).where('id BETWEEN ? AND ?', i, (i + batch - 1)).each do |post|
i는 현재 배치 인덱스이고 BETWEEN은 양 끝을 포함하므로, 각 배치의 마지막 포스트를 제외하여 중복 처리되지 않도록 했습니다. 모든 포스트를 가져오게 되어야 하지만, 어딘가에 약간의 로직 오류가 있을 수도 있습니다…
원본 버전과 새로운 버전의 최종 쿼리 처리 시간 차이는 다음과 같습니다:
원본:
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------
Limit (cost=12541237.13..12544635.84 rows=1000 width=783) (actual time=289375.555..289456.445 rows=1000 loops=1)
-> Index Scan Backward using posts_pkey on posts (cost=0.56..100746846.76 rows=29642680 width=783) (actual time=0.038..288798.236 rows=3691000 loops=1)
Filter: (deleted_at IS NULL)
Planning Time: 0.175 ms
JIT:
Functions: 6
Options: Inlining true, Optimization true, Expressions true, Deforming true
Timing: Generation 80.019 ms, Inlining 47.933 ms, Optimization 261.731 ms, Emission 106.263 ms, Total 495.947 ms
Execution Time: 289538.294 ms
(9 rows)
실제 시간(actual time) 섹션의 인덱스 스캔(Index Scan)의 행 수를 주목해 보세요: 3,691,000
이것은 거의 5분 동안 실행되는 쿼리입니다.
BETWEEN을 사용한 새로운 버전은 다음과 같습니다:
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------
Index Scan Backward using posts_pkey on posts (cost=0.56..4137.79 rows=1100 width=783) (actual time=1.956..273.467 rows=1001 loops=1)
Index Cond: ((id >= 3690000) AND (id <= 3691000))
Filter: (deleted_at IS NULL)
Planning Time: 26.421 ms
Execution Time: 274.035 ms
(5 rows)
관심 있는 1,000행만 검사되고 반환됩니다. 289,000ms 대신 274ms에 실행됩니다.
이것은 훌륭한 개선이지만, 프로세스가 멀티스레드로 작동할 수 있다면 더 나을 것입니다. 그것은 제 전문 분야를 약간 벗어날 수 있지만, ID의 MOD를 사용하여 포스트 테이블을 여러 프로세스에 걸쳐 분할하고 동시에 여러 리베이크 프로세스를 실행하는 방법을 고안할 수 있을 것 같습니다.
피드백을 환영합니다. 더 개선할 수 있을까요?
