# Async-background: 외부 종속성 없는 Ruby용 경량 백그라운드 작업 시스템
Ruby 애플리케이션 개발자들은 종종 백그라운드 작업을 실행해야 하지만, Sidekiq 같은 기성 솔루션들은 추가 인프라가 필요합니다. 새로운 gem인 async-background은 Async 위에서 Redis나 Postgres 없이 실행되며, 세밀한 자원 제어를 제공하는 대안을 제시합니다.
기존 솔루션들의 단점
Ruby에서 백그라운드 작업에는 보통 Sidekiq을 사용하지만, Redis가 필요합니다. 몇 개 안 되는 작업만 있는 소규모 프로젝트에서는 과도합니다: docker-compose에 추가 컨테이너, 또 다른 장애 지점, 인프라 비용 발생. 게다가 Sidekiq은 워커 간 유연한 로드 분산을 지원하지 않습니다—무거운 야간 보고서가 사용자 이메일 처리를 지연시킬 수 있습니다.
다른 옵션들도 각자 단점이 있습니다:
whenever— 시스템 crontab을 생성하지만, 큐나 앱 통합이 없습니다.rufus-scheduler— 별도 스레드에서 실행되어 다중 프로세스 Async 앱에서 컨텍스트 스위칭과 중복 문제를 일으킵니다.good_job과solid_queue— ActiveRecord와 Postgres에 종속적입니다. 이로 인해 백그라운드 작업이 트랜잭션 로드와 데이터베이스 자원을 놓고 경쟁해 메인 앱 속도를 떨어뜨릴 위험이 있습니다.
따라서 외부 종속성 없이 cron 스케줄, 간격, 동적 큐를 결합하고 자원 사용을 제어할 수 있는 솔루션이 필요했습니다.
주요 설계 원칙
gem의 저자는 여섯 가지 핵심 원칙을 제시했습니다:
- 단일 이벤트 루프. 스케줄러는 별도 스레드 없이 앱과 동일한 리액티브 루프에서 실행됩니다.
- 제로 인프라. 큐는 디스크에 저장됩니다 (WAL 모드의 SQLite 또는
tmp/의 파일), Redis나 Postgres 불필요. - 선택적 종속성. 메트릭스 같은 추가 기능은 선택적 gem으로 제공.
- 코디네이터 없는 멀티 프로세스 안전성. Falcon (fork 사용 시)에서 작업이 하나의 프로세스에서만 실행되도록 보장.
- 지속성으로 안정성, 속도는 별도 메커니즘. SQLite 저장으로 신뢰성 확보, UNIX 소켓 wakeup으로 속도 향상.
- 관리되는 워커 활용. 환경 변수와 YAML 설정으로 특정 워커에 작업 할당 가능.
이 원칙들 덕분에 기존 Async 앱에 추가 인프라 없이 완벽하게 통합되는 gem을 만들 수 있었습니다.
아키텍처 특징
폴링 틱 대신 MinHeap
sleep 1 루프(매초 모든 작업 확인 위해 깨어남) 대신 이진 MinHeap을 사용합니다. 이를 통해:
peek로 다음 작업을 O(1) 시간에 가져옴.- 작업 실행 후
replace_top으로 힙 업데이트 O(log n) 시간. - 다음 이벤트까지 정확히 sleep하여 CPU 자원 절약.
구현은 74줄뿐이며 외부 종속성 없음.
두 가지 시간 척도
간격 기반 작업(예: every: 60)에는 시간 동기화(NTP) 문제를 피하기 위해 단조 증가 클럭(CLOCK_MONOTONIC)을 사용합니다. cron 작업(예: cron: "0 3 *")에는 달력 시간에 맞춰 벽시계 시간(Time.now)을 씁니다.
코드상으로는 다음과 같습니다:
next_run_at = if task_config[:interval]
now + jitter + task_config[:interval]
else
now_wall = Time.now
wall_wait = task_config[:cron].next_time(now_wall).to_f - now_wall.to_f
now + jitter + [wall_wait, MIN_SLEEP_TIME].max
end
CRC32를 통한 샤딩
Falcon(포크 사용)에서 작업이 하나의 프로세스에서만 실행되도록 결정론적 샤딩을 적용합니다: 작업 이름에 대해 Zlib.crc32(name) % total_workers + 1을 계산. 현재 워커 인덱스와 일치하면 작업 로드.
assigned = config['worker']&.to_i || ((Zlib.crc32(name) % total_workers) + 1)
next unless assigned == worker_index
이로 분산 락 불필요.
겹침 시 건너뛰기
작업 실행 시간이 간격보다 길면(예: 60초 간격에 90초 소요) 새 인스턴스를 건너뜁니다. 대신 현재 작업을 로그에 기록하고, 건너뛰며 다음 슬롯으로 재스케줄:
if entry.running
logger.warn('Async::Background') { "#{entry.name}: skipped, previous run still active" }
metrics.job_skipped(entry)
entry.reschedule(monotonic_now)
heap.replace_top(entry)
next
end
큐: SQLite + UNIX Socket
동적 큐는 두 메커니즘 사용:
- SQLite — 진실의 원천. 작업을 디스크에 지속해 신뢰성 확보.
- UNIX 소켓 — 즉시 알림. DB에 작업 추가 후 소켓으로 신호 보내 워커가 즉시 처리.
신호 손실 시 5초마다 폴링 백업.
장애 복구
워커가 작업 중 크래시하면 상태가 "running"으로 남습니다. 재시작 시 Queue::Store#recover가 해당 레코드를 찾아 큐로 반환.
주요 요점
- 외부 서비스 종속성 없음. Async-background은 파일시스템만 의지해 배포 간소화.
- 자원 제어. cron, 큐 등 작업 유형별 워커 격리로 자원 경쟁 방지.
- 신뢰성. SQLite 지속성과 복구 메커니즘으로 작업 손실 없음.
- 유연성. YAML과 환경 변수 설정으로 작업 분배 쉽게 조정.
- 성능. Min-heap과 UNIX 소켓으로 낮은 지연과 효율적 CPU 사용.
— Editorial Team
아직 댓글이 없습니다.