Zpět na domů

Async-background: úkoly na pozadí v Ruby bez Redis a Postgres

Async-background — gem pro Ruby, který zajišťuje úkoly na pozadí bez externích závislostí. Řeší problémy s řízením zdrojů a spolehlivostí. Vhodný pro aplikace na Async a Falcon.

Jak spouštět úkoly na pozadí v Ruby bez Redis? Řešení async-background
Advertisement 728x90

# Async-background: Lehkováhový systém pozadíových úkolů pro Ruby bez externích závislostí

Vývojáři Ruby aplikací se často setkávají s potřebou spouštění pozadíových úkolů, ale hotová řešení jako Sidekiq vyžadují dodatečnou infrastrukturu. Nový gem async-background nabízí alternativu, která běží na Async bez Redis nebo Postgres, s detailní kontrolou zdrojů.

Proč stávající řešení nevyhovují

Obvykle se v Ruby pro pozadíové úlohy používá Sidekiq, ale vyžaduje Redis. Pro malé projekty s pár úkoly je to zbytečné: další kontejner v docker-compose, další bod selhání a náklady na infrastrukturu. Navíc Sidekiq neumožňuje flexibilně rozkládat zátěž mezi workery: těžký noční report může zpomalit zpracování uživatelských e-mailů.

Další varianty mají také své nedostatky:

Google AdInline article slot
  • whenever — generuje systémový crontab, bez fronty a integrace s aplikací.
  • rufus-scheduler — běží v samostatných vláknech, což v aplikacích Async způsobuje přepínání kontextu a problémy s duplikací při více procesech.
  • good_job a solid_queue — jsou vázané na ActiveRecord a Postgres. To vytváří rizika: pozadíové úlohy soutěží s transakční zátěží o zdroje databáze, což může vést ke zpomalení hlavní aplikace.

Tím vznikla potřeba řešení, které kombinuje cron, intervaly a dynamickou frontu bez externích závislostí a s možností kontroly využití zdrojů.

Klíčové principy návrhu

Autor gemu formuloval šest základních principů:

  • Jeden event loop. Plánovač běží ve stejné reaktivní smyčce jako aplikace, bez samostatných vláken.
  • Zero infrastructure. Fronta je uložena na disku (SQLite v WAL režimu nebo soubor v tmp/), bez Redis nebo Postgres.
  • Volitelné závislosti. Dodatečné funkce (např. metriky) se připojují přes volitelné gemy.
  • Multi-process safety bez koordinátora. Zajišťuje, že úkol se spustí pouze v jednom procesu při použití Falcon (forků).
  • Persistence jako pojistka, rychlost jako samostatný mechanismus. Spolehlivost zajišťuje uložení do SQLite, rychlost probuzení přes UNIX soket.
  • Řízené využití workerů. Vývojář může přiřadit úlohy konkrétním workerům přes proměnné prostředí a YAML konfiguraci.

Tyto principy umožnily vytvořit gem, který se snadno integruje do stávajících Async aplikací a nevyžaduje dodatečnou infrastrukturu.

Google AdInline article slot

Architektonické vlastnosti

Minimální halda místo tiků

Místo smyčky se sleep 1 (která se budí každou sekundu a kontroluje všechny úlohy) se používá binární minimální halda (MinHeap). To umožňuje:

  • Získat nejbližší úkol za O(1) pomocí peek.
  • Aktualizovat haldu po spuštění úkolu za O(log n) pomocí replace_top.
  • Spát přesně do okamžiku další události a šetřit CPU zdroje.

Implementace zabírá pouze 74 řádků a nevyžaduje externí závislosti.

Dvě časové škály

Pro intervalové úlohy (např. every: 60) se používají monotónní hodiny (CLOCK_MONOTONIC), aby se vyhnuli problémům při synchronizaci času (NTP). Pro cron úlohy (např. cron: "0 3 *") se používají zdí hodin (Time.now), protože plán je vázaný na kalendářní čas.

Google AdInline article slot

V kódu to vypadá takto:

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

Sharding přes CRC32

Při použití Falcon (který se forkne) je nutné zajistit, že se úkol spustí pouze v jednom procesu. K tomu slouží deterministický sharding: pro název úkolu se počítá Zlib.crc32(name) % total_workers + 1. Pokud výsledek odpovídá indexu aktuálního workeru, úkol se načte.

assigned = config['worker']&.to_i || ((Zlib.crc32(name) % total_workers) + 1)
next unless assigned == worker_index

To eliminuje potřebu distribuovaných zámků.

Skip-on-overlap

Pokud úkol trvá déle než interval (např. interval 60 sekund, úkol 90), nový úkol se nespustí. Místo toho se aktuální úkol přeskočí, zaloguje a přesune na další slot:

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

Fronta: SQLite + UNIX-soket

Dynamická fronta využívá dva mechanismy:

  • SQLite — zdroj pravdy. Úlohy se ukládají na disk, což zajišťuje spolehlivost.
  • UNIX-soket — pro okamžité upozornění. Po přidání úkolu do DB se pošle signál do soketu, aby worker úkol okamžitě zpracoval.

Kromě toho je polling každých 5 sekund jako pojistka pro případ ztráty signálu.

Obnovení po selháních

Pokud worker spadne během provádění úkolu, stav zůstane „v práci". Při restartu Queue::Store#recover najde takové záznamy a vrátí je do fronty.

Co je důležité

  • Žádná závislost na externích službách. Async-background používá pouze souborový systém, což zjednodušuje nasazení.
  • Kontrola zdrojů. Možnost izolovat workery pro různé typy úkolů (cron, fronta) zabraňuje soutěži o zdroje.
  • Spolehlivost. Úlohy se neztrácejí díky uložení do SQLite a mechanismu obnovení.
  • Flexibilita. Konfigurace přes YAML a proměnné prostředí umožňuje snadné nastavení distribuce úkolů.
  • Výkon. Minimální halda a UNIX soket zajišťují nízkou latenci a efektivní využití CPU.

— Editorial Team

Advertisement 728x90

Číst dál