11 paź 2025·6 min czytania

Pule workerów w Go vs goroutine na zadanie dla zadań w tle

Pule workerów w Go kontra uruchamianie goroutine dla każdego zadania — dowiedz się, jak każdy model wpływa na przepustowość, użycie pamięci i kontrolę obciążenia (backpressure) przy przetwarzaniu w tle i długotrwałych workflowach.

Pule workerów w Go vs goroutine na zadanie dla zadań w tle

Jaki problem rozwiązujemy?

Większość usług w Go robi więcej niż tylko odpowiada na żądania HTTP. Uruchamia także pracę w tle: wysyła maile, zmienia rozmiary obrazów, generuje faktury, synchronizuje dane, przetwarza zdarzenia lub odbudowuje indeks wyszukiwania. Niektóre zadania są szybkie i niezależne. Inne tworzą długie workflowy, gdzie każdy krok zależy od poprzedniego (np. pobranie opłaty, czekanie na potwierdzenie, powiadomienie klienta i aktualizacja raportów).

Gdy ludzie porównują „Go worker pools vs goroutine-per-task”, zazwyczaj próbują rozwiązać jeden problem produkcyjny: jak uruchomić dużo pracy w tle bez spowalniania, zwiększania kosztów lub niestabilności serwisu.

Odczuwasz to w kilku miejscach:

  • Opóźnienia: praca w tle zabiera CPU, pamięć, połączenia do bazy i pasmo sieciowe z żądań użytkownika.
  • Koszty: niekontrolowana współbieżność pcha cię do większych maszyn, więcej zasobów DB albo wyższych rachunków za kolejki i API.
  • Stabilność: skoki (importy, masowe wysyłki, burze retryów) mogą wywołać timeouty, crash z powodu OOM lub kaskadowe awarie.

Prawdziwy kompromis to prostota kontra kontrola. Uruchomienie goroutine na zadanie jest proste do napisania i często wystarcza, gdy wolumen jest niski lub naturalnie ograniczony. Pula workerów dodaje strukturę: stała współbieżność, wyraźne limity i naturalne miejsce na timeouty, retry i metryki. Kosztem jest dodatkowy kod i decyzja, co robić, gdy system jest zajęty (czy zadania czekają, są odrzucane, czy zapisywane gdzie indziej?).

Tu chodzi o codzienne przetwarzanie w tle: przepustowość, pamięć i backpressure (jak zapobiec przeładowaniu). Nie omawiamy tu wszystkich technologii kolejek, rozproszonych silników workflow ani dokładnie semantics "exactly-once".

Jeśli budujesz kompletne aplikacje z logiką w tle na platformie takiej jak AppMaster (appmaster.io), te same pytania pojawiają się szybko. Twoje procesy biznesowe i integracje nadal potrzebują limitów wokół baz danych, zewnętrznych API i dostawców email/SMS, żeby jeden zajęty workflow nie spowolnił wszystkiego innego.

Dwa popularne wzorce prostymi słowami

Goroutine na zadanie

To najprostsze podejście: gdy pojawia się zadanie, uruchamiasz goroutine, żeby je obsłużyć. „Kolejka” to często to, co wywołuje pracę, np. odbiornik kanału albo bezpośrednie wywołanie z handlera HTTP.

Typowy kształt: odbierz zadanie, potem go handle(job). Czasami wciąż używa się kanału, ale jako punkt przekazania, a nie ogranicznik.

Dobrze działa, gdy zadania głównie czekają na I/O (wywołania HTTP, zapytania DB, uploady), wolumen jest umiarkowany, a skoki są małe lub przewidywalne.

Wadą jest to, że współbieżność może rosnąć bez wyraźnego ograniczenia. To może skokowo zwiększyć pamięć, otworzyć zbyt wiele połączeń lub przeciążyć usługę zewnętrzną.

Pula workerów

Pula workerów uruchamia stałą liczbę goroutine workerów i podaje im zadania z kolejki, zwykle buforowanego kanału w pamięci. Każdy worker w pętli: bierze zadanie, przetwarza, powtarza.

Kluczowa różnica to kontrola. Liczba workerów jest twardym limitem współbieżności. Jeśli zadania przychodzą szybciej niż workerzy je kończą, zadania czekają w kolejce (albo są odrzucane, gdy kolejka jest pełna).

Pule workerów pasują, gdy praca jest ciężka dla CPU (przetwarzanie obrazów, generowanie raportów), gdy potrzebujesz przewidywalnego użycia zasobów lub gdy musisz chronić bazę danych czy API przed skokami.

Gdzie żyje kolejka

Oba wzorce mogą używać kanału w pamięci — to szybkie, ale znika przy restarcie. Dla zadań, których nie można stracić, albo długich workflowów, kolejka często przenosi się poza proces (tabela w DB, Redis lub broker wiadomości). W takim setupie nadal wybierasz między goroutine-per-task a pulą workerów, ale teraz działają jako konsumenci zewnętrznej kolejki.

Prosty przykład: system musi nagle wysłać 10 000 maili. Goroutine-per-task może próbować wysłać je wszystkie naraz. Pula może wysyłać 50 na raz i trzymać resztę w kontrolowanym porządku.

Przepustowość: co się zmienia, a co nie

Często oczekuje się dużej różnicy w przepustowości między pulami a goroutine-per-task. W większości przypadków surowa przepustowość jest ograniczona czymś innym, a nie sposobem uruchamiania goroutine.

Przepustowość zwykle osiąga sufit przy najwolniejszym współdzielonym zasobie: baza danych lub limity zewnętrznego API, dysk lub pasmo sieci, praca CPU-heavy (JSON/PDF/obrót obrazu), blokady i współdzielony stan lub zależności, które zwalniają pod obciążeniem.

Jeśli wspólny zasób jest wąskim gardłem, więcej goroutine nie przyspieszy pracy. Przede wszystkim stworzy więcej oczekiwania na tym samym punkcie zatoru.

Goroutine-per-task może wygrywać, gdy zadania są krótkie, głównie I/O-bound i nie konkurują o wspólne limity. Start goroutine jest tani, a Go dobrze planuje dużą ich liczbę. W pętli typu „pobierz, sparsuj, zapisz jeden wiersz” podejście to może utrzymać CPU zajęte i ukryć latencję sieciową.

Pule workerów wygrywają, gdy musisz ograniczyć kosztowne zasoby. Jeśli każde zadanie trzyma połączenie DB, otwiera pliki, alokuje duże bufory lub uderza w limit API, stała współbieżność utrzyma serwis stabilnym i pozwoli osiągnąć maksymalną bezpieczną przepustowość.

Opóźnienia (szczególnie p99) to miejsce, gdzie różnica często jest widoczna. Goroutine-per-task może wyglądać świetnie przy niskim obciążeniu, a potem gwałtownie spaść, gdy zadania się nagromadzą. Pula wprowadza opóźnienie kolejkowania (zadania czekają na wolnego workera), ale zachowanie jest bardziej stabilne, bo unikasz stada walczącego o ten sam limit.

Prosty model mentalny:

  • Jeśli praca jest tania i niezależna, większa współbieżność może zwiększyć przepustowość.
  • Jeśli praca blokowana jest przez wspólny limit, więcej współbieżności głównie zwiększa czas oczekiwania.
  • Jeśli zależy ci na p99, mierz czas w kolejce oddzielnie od czasu przetwarzania.

Użycie pamięci i zasobów

Duża część debaty o pulach vs goroutine-per-task dotyczy pamięci. CPU można zwykle skalować w górę lub na zewnątrz. Awarie pamięci są bardziej nagłe i mogą zabrać cały serwis.

Goroutine jest tani, ale nie darmowy. Każda zaczyna od małego stosu, który rośnie przy głębszych wywołaniach lub przy trzymaniu dużych zmiennych lokalnych. Jest też narzut planera i runtime. 10 000 goroutine może być w porządku. 100 000 może być niespodzianką, jeśli każda trzyma referencje do dużych danych.

Większy ukryty koszt to często nie sama goroutine, lecz to, co ona utrzymuje. Jeśli zadania przychodzą szybciej niż się kończą, podejście goroutine-per-task tworzy nieograniczony backlog. „Kolejka” może być implicite (goroutine czekające na locki lub I/O) albo explicite (buforowany kanał, slice, batch w pamięci). W każdym wypadku pamięć rośnie wraz z backlogiem.

Pule workerów pomagają, bo wymuszają limit. Przy stałych workerach i ograniczonej kolejce masz realny limit pamięci i czytelny tryb awarii: gdy kolejka jest pełna, blokujesz, odrzucasz obciążenie lub odsyłasz je dalej.

Szybkie przybliżenie:

  • Peak goroutine = workerzy + zadania w locie + „oczekujące” zadania, które utworzyłeś
  • Pamięć na zadanie = payload (bajty) + metadane + wszystko, do czego trzyma referencje (requesty, zdeserializowane JSONy, wiersze DB)
  • Szczyt pamięci backlogu ~= oczekujące zadania * pamięć na zadanie

Przykład: jeśli każde zadanie trzyma 200 KB payloadu (lub referuje graf obiektów 200 KB) i pozwolisz na nagromadzenie 5 000 zadań, to ~1 GB tylko na payloady. Nawet gdyby goroutine były magicznie darmowe, backlog nie jest.

Backpressure: jak nie doprowadzić systemu do stopienia się

Zamień dane w automatyzację
Projektuj dane w modelach z PostgreSQL jako źródłem i trzymaj logikę zadań blisko nich.
Utwórz backend

Backpressure to prosta idea: gdy praca przychodzi szybciej niż możesz ją skończyć, system odpycha w kontrolowany sposób zamiast cicho się piętrzyć. Bez tego nie tylko robi się wolniej — pojawiają się timeouty, wzrost pamięci i błędy trudne do odtworzenia.

Brak backpressure zauważysz przy skoku (importy, maile, eksporty), gdy:

  • pamięć rośnie i nie spada,
  • czas oczekiwania w kolejce rośnie, podczas gdy CPU pozostaje zajęte,
  • opóźnienia dla niepowiązanych żądań rosną,
  • retry się kumulują,
  • pojawiają się błędy typu „too many open files” lub wyczerpanie puli połączeń.

Praktyczne narzędzie to ograniczony kanał: kontrolujesz, ile zadań może czekać. Producent blokuje się, gdy kanał jest pełny — to spowalnia tworzenie zadań u źródła.

Blokowanie nie zawsze jest właściwe. Dla opcjonalnej pracy wybierz jasną politykę, żeby przeciążenie było przewidywalne:

  • Odrzuć zadania niskiej wartości (np. duplikaty powiadomień)
  • Paczkuj wiele małych zadań w jedno zapisywanie lub jedno wywołanie API
  • Opóźnij pracę z jitterem, żeby uniknąć fal retryów
  • Odsuń do trwałej kolejki i szybko odpowiedz klientowi
  • Odrzuć obciążenie zwracając jasny błąd przy przeciążeniu

Rate limiting i timeouts to też narzędzia backpressure. Rate limit ogranicza, jak szybko uderzasz w zależność (dostawca maili, baza, zewnętrzne API). Timeouts ograniczają, jak długo worker może być zawieszony. Razem powstrzymują wolną zależność przed przerodzeniem się w pełen outage.

Przykład: generowanie wyciągów na koniec miesiąca. Jeśli 10 000 żądań przychodzi naraz, nieograniczone goroutine mogą uruchomić 10 000 renderów PDF i uploadów. Z ograniczoną kolejką i stałymi workerami renderujesz i retryujesz w bezpiecznym tempie.

Jak krok po kroku zbudować pulę workerów

Uczyń backpressure widocznym
Dodaj narzędzia administratorskie i kontrolki zadań obok API, żeby ops mogli zobaczyć, co się dzieje.
Zbuduj aplikację

Pula workerów ogranicza współbieżność poprzez uruchomienie stałej liczby workerów i podawanie im zadań z kolejki.

1) Wybierz bezpieczny limit współbieżności

Zacznij od tego, na co twoje zadania najwięcej czasu poświęcają.

  • Dla pracy CPU-heavy trzymaj workerów blisko liczby rdzeni.
  • Dla pracy I/O-heavy możesz być wyżej, ale zatrzymaj się, gdy zależności zaczną timeoutować lub throttlingować.
  • Dla mieszanej pracy mierz i dostosowuj. Rozsądny początkowy zakres to często 2x–10x liczby rdzeni, potem tuning.
  • Szanuj wspólne limity. Jeśli pula DB ma 20 połączeń, 200 workerów tylko będzie walczyć o te 20.

2) Wybierz kolejkę i ustaw jej rozmiar

Buforowany kanał jest powszechny — jest wbudowany i łatwy do zrozumienia. Bufor to twój amortyzator na skoki.

Małe bufory szybciej ujawniają przeciążenie (producent blokuje się wcześniej). Duże bufory wygładzają nagłe skoki, ale mogą ukrywać problemy i zwiększać pamięć oraz opóźnienia. Dobierz rozmiar z zamiarem i zdecyduj, co się stanie, gdy się zapełni.

3) Uczyń każde zadanie anulowalnym

Przekazuj context.Context do każdego zadania i upewnij się, że kod zadania z niego korzysta (DB, HTTP). Dzięki temu możesz czysto zatrzymać pracę przy wdrożeniach, zamknięciach i timeoutach.

func StartPool(ctx context.Context, workers, queueSize int, handle func(context.Context, Job) error) chan\u003c- Job {
    jobs := make(chan Job, queueSize)
    for i := 0; i \u003c workers; i++ {
        go func() {
            for {
                select {
                case \u003c-ctx.Done():
                    return
                case j := \u003c-jobs:
                    _ = handle(ctx, j)
                }
            }
        }()
    }
    return jobs
}

4) Dodaj metryki, których faktycznie użyjesz

Jeśli śledzisz tylko kilka liczb, niech to będą:

  • Głębokość kolejki (jak bardzo jesteś w tyle)
  • Czas zajętości workerów (jak bardzo pula jest nasycona)
  • Czas trwania zadania (p50, p95, p99)
  • Wskaźnik błędów (i liczba retryów, jeśli retryujesz)

To wystarczy, żeby na podstawie danych dobierać liczbę workerów i rozmiar kolejki, zamiast zgadywać.

Typowe błędy i pułapki

Większość zespołów nie trafia w kłopoty przez wybór „złego” wzorca. Szkodzą im małe domyślne ustawienia, które przy skokach stają się przyczyną incydentów.

Gdy goroutine mnożą się

Klasyczna pułapka to uruchamianie jednej goroutine na zadanie podczas skoku. Kilkaset jest ok. Kilkaset tysięcy może zalać scheduler, heap, logi i sockety. Nawet jeśli każda goroutine jest mała, koszt sumaryczny narasta, a odzyskiwanie trwa, bo praca jest już w toku.

Inny błąd to traktowanie ogromnego buforowanego kanału jako „backpressure”. Duży bufor to po prostu ukryta kolejka. Może dać czas, ale też ukryć problem, aż uderzysz w ścianę pamięci. Jeśli potrzebujesz kolejki, nadaj jej celowy rozmiar i zdecyduj, co się stanie, gdy będzie pełna (blokuj, odrzucaj, retryuj później lub zapisuj na dysku).

Ukryte wąskie gardła

Wiele zadań w tle nie jest CPU-bound. Ogranicza je coś zewnętrznego. Jeśli ignorujesz te limity, szybki producent zalaże wolnego konsumenta.

Typowe pułapki:

  • Brak anulowania lub timeoutu, więc workerzy mogą blokować się na API lub zapytaniu DB na zawsze
  • Liczby workerów dobrane bez sprawdzenia realnych limitów jak połączenia DB, I/O dysku czy limity stron trzecich
  • Retry, które potęgują obciążenie (natychmiastowe retry dla 1 000 nieudanych zadań)
  • Jedna współdzielona blokada lub jedna transakcja, która wszystko serializuje — „więcej workerów” tylko dodaje narzut
  • Brak widoczności: brak metryk dla głębokości kolejki, wieku zadań, liczby retryów i wykorzystania workerów

Przykład: nocny eksport wyzwala 20 000 zadań „wyślij powiadomienie”. Jeśli każde uderza w bazę i dostawcę maili, łatwo przekroczyć pule połączeń i limity. Pula 50 workerów z timeoutami i małą kolejką ujawni limit. Jedna goroutine na zadanie z gigantycznym buforem sprawia wrażenie, że system jest ok, aż nagle przestaje być.

Przykład: skokowe eksporty i powiadomienia

Weryfikuj założenia o konkurencji
Przetestuj limity workerów i zachowanie zadań wcześnie z prawdziwym backendem i prawdziwymi endpointami.
Zbuduj prototyp

Wyobraź sobie, że zespół wsparcia potrzebuje danych do audytu. Jedna osoba klika „Export”, potem kilku kolegów robi to samo i nagle w minutę tworzy się 5 000 zadań eksportu. Każdy eksport czyta z DB, formatuje CSV, zapisuje plik i wysyła powiadomienie (email lub Telegram).

W podejściu goroutine-per-task system przez chwilę działa świetnie. Wszystkie 5 000 zadań startuje niemal natychmiast i wygląda, że kolejka się szybko opróżnia. Potem pojawiają się koszty: tysiące równoczesnych zapytań do DB walczą o połączenia, pamięć rośnie, bo zadania trzymają bufory, a timeouty stają się powszechne. Zadania, które mogłyby skończyć szybko, zostają ugrzęźnięte za retryami i wolnymi zapytaniami.

W podejściu z pulą workerów start jest wolniejszy, ale przebieg spokojniejszy. Przy 50 workerach tylko 50 eksportów robi ciężką pracę naraz. Użycie DB pozostaje w przewidywalnym zakresie, bufory są częściej reużywane, a opóźnienia bardziej stabilne. Całkowity czas zakończenia łatwiej oszacować: w przybliżeniu (zadania / workerzy) * średni czas zadania, plus narzut.

Różnica nie polega na tym, że pule są magicznie szybsze. Chodzi o to, że nie pozwalają systemowi szkodzić samemu sobie podczas skoków. Sterowany przebieg 50 naraz często kończy się szybciej niż 5 000 zadań, które między sobą walczą.

Gdzie stosować backpressure zależy od tego, co chcesz chronić:

  • Na warstwie API odrzucaj lub opóźniaj nowe żądania eksportu, gdy system jest zajęty.
  • W kolejce akceptuj żądania, ale enqueue'uj i spuszczaj je w bezpiecznym tempie.
  • W puli workerów ogranicz współbieżność dla kosztownych części (odczyty DB, generowanie plików, wysyłka powiadomień).
  • Dla każdego zasobu stosuj oddzielne limity (np. 40 workerów dla eksportów, ale tylko 10 dla powiadomień).
  • Dla zewnętrznych wywołań rate-limituj email/SMS/Telegram, żeby nie dostać blokady.

Szybka lista kontrolna przed wdrożeniem

Podłącz się do swojego stacku
Podłącz email, SMS, Telegram, Stripe i więcej bez przepisywania głównego serwisu.
Dodaj integracje

Zanim uruchomisz zadania w tle w produkcji, sprawdź limity, widoczność i obsługę awarii. Większość incydentów nie wynika ze „złego kodu”, lecz z braku zabezpieczeń przy skokach lub niestabilnej zależności.

  • Ustaw twarde maksima współbieżności per zależność. Nie wybieraj jednej globalnej liczby i nie zakładaj, że pasuje do wszystkiego. Ogranicz zapisy do DB, wyjścia HTTP i pracę CPU osobno.
  • Zrób kolejkę ograniczoną i obserwowalną. Nałóż realny limit na oczekujące zadania i wystaw kilka metryk: głębokość kolejki, wiek najstarszego zadania i szybkość przetwarzania.
  • Dodaj retry z jitterem i ścieżkę dead-letter. Retryuj selektywnie, rozłóż retry w czasie, a po N niepowodzeniach przenieś zadanie do dead-letter queue lub tabeli „failed” z wystarczającymi danymi do przeglądu i ponownego uruchomienia.
  • Zweryfikuj zachowanie przy zamykaniu: drenaż, anulowanie, bezpieczne wznawianie. Zdecyduj, co się dzieje przy wdrożeniu lub awarii. Uczyń zadania idempotentnymi, żeby bezpiecznie je powtarzać, i zapisuj postęp długich workflowów.
  • Chroń system timeoutami i obwodami (circuit breakers). Każde wywołanie zewnętrzne powinno mieć timeout. Jeśli zależność jest niedostępna, fail fast (albo zatrzymaj przyjmowanie), zamiast kumulować pracę.

Praktyczne następne kroki

Wybierz wzorzec, który pasuje do tego, jak system wygląda w normalny dzień, nie w idealny. Jeśli praca przychodzi w skokach (uploady, eksporty, masowe maile), stała pula workerów z ograniczoną kolejką jest zwykle bezpieczniejszym domyślem. Jeśli ruch jest stały, a każde zadanie mały, goroutine-per-task może być ok, pod warunkiem, że gdzieś i tak narzucisz limity.

Wygrywa wybór, który czyni awarie nudnymi. Pule ujawniają limity. Goroutine-per-task sprawia, że łatwo zapomnieć o limitach, aż do pierwszego prawdziwego skoku.

Zacznij prosto, potem dodaj limity i widoczność

Zacznij od prostego rozwiązania, ale wcześnie dodaj dwa elementy: limit współbieżności i sposób na obserwowanie kolejkowania i błędów.

Praktyczny plan rollout:

  • Określ kształt obciążenia: skokowy, stały czy mieszany (i co oznacza „szczyt”).
  • Nałóż twardy limit na pracę w locie (rozmiar puli, semafor lub ograniczony kanał).
  • Zdecyduj, co się dzieje, gdy limit zostanie osiągnięty: blokować, odrzucać czy zwracać jasny błąd.
  • Dodaj podstawowe metryki: głębokość kolejki, czas w kolejce, czas przetwarzania, retry i dead-lettery.
  • Przetestuj obciążenie skokiem 5x twojego oczekiwanego szczytu i obserwuj pamięć oraz opóźnienia.

Gdy pula nie wystarcza

Jeśli workflowy trwają minuty lub dni, prosta pula może nie wystarczyć, bo praca to nie tylko „zrób to raz”. Potrzebujesz stanu, retryów i możliwości wznawiania. To zwykle oznacza zapisywanie postępu, idempotentne kroki i backoff. Może też oznaczać podział dużego zadania na mniejsze kroki, żeby po awarii łatwo wznowić.

Jeśli chcesz szybciej dostarczyć kompletny backend z workflowami, AppMaster (appmaster.io) może być praktyczną opcją: modelujesz dane i logikę biznesową wizualnie, a platforma generuje kod Go dla backendu, dzięki czemu zachowujesz dyscyplinę wokół limitów konkurencji, kolejkowania i backpressure bez ręcznego składania wszystkiego.

FAQ

Kiedy powinienem użyć puli workerów zamiast uruchamiania goroutine dla każdego zadania?

Domyślnie wybierz pulę workerów, gdy zadania pojawiają się w skokach lub odwołują się do wspólnych limitów jak połączenia DB, CPU czy limity zewnętrznych API. Użyj podejścia goroutine-per-task, gdy wolumen jest niewielki, zadania są krótkie i nadal masz jakiś jasny limit gdzie indziej (na przykład semafor lub rate limiter).

Jaki jest prawdziwy kompromis między goroutine-per-task a pulą workerów?

Uruchamianie goroutine na zadanie jest szybkie do napisania i przy niskim ruchu może dawać świetną przepustowość, ale pod skokami może stworzyć nieograniczony backlog. Pula workerów wprowadza twardy limit współbieżności i daje miejsce do zastosowania timeoutów, retryów i metryk, co zwykle czyni zachowanie produkcyjne bardziej przewidywalnym.

Czy pula workerów zmniejszy przepustowość w porównaniu z goroutine-per-task?

Zazwyczaj niewielki. W większości systemów przepustowość ogranicza wspólny wąski gardło jak baza danych, zewnętrzne API, I/O dysku czy intensywne obliczenia. Więcej goroutine nie pokona tego limitu — głównie zwiększy czas oczekiwania i kontencję.

Jak te wzorce wpływają na opóźnienia (zwłaszcza p99)?

Goroutine-per-task często daje lepsze opóźnienia przy niskim obciążeniu, ale przy dużym obciążeniu może bardzo się pogorszyć, bo wszystko zaczyna konkurować naraz. Pula wprowadza opóźnienie kolejkowania, ale zazwyczaj pozwala utrzymać stabilniejsze p99, zapobiegając „thundering herd” na tych samych zależnościach.

Dlaczego goroutine-per-task może powodować skoki zużycia pamięci?

Sam goroutine zwykle nie jest największym kosztem — problemem jest backlog. Jeśli zadania się piętrzą i każde trzyma duże obiekty lub payloady, pamięć może szybko rosnąć. Pula workerów z ograniczoną kolejką daje przewidywalny sufit pamięci i określone zachowanie przy przeciążeniu.

Czym jest backpressure i jak dodać je w Go?

Backpressure oznacza spowolnienie lub zatrzymanie przyjmowania nowej pracy, gdy system jest już obciążony, zamiast pozwalać na ciche gromadzenie się zadań. Prosty sposób w Go to ograniczona (bounded) kanał: gdy jest pełny, producent blokuje się lub zwracasz błąd, co zapobiega niekontrolowanej konsumpcji pamięci i wyczerpaniu połączeń.

Jak wybrać odpowiednią liczbę workerów?

Zacznij od realnych ograniczeń. Dla pracy CPU-heavy ustaw liczbę workerów blisko liczby rdzeni. Dla pracy I/O-heavy możesz ustawić wyższy poziom, ale skończ, gdy baza danych, sieć lub zewnętrzne API zaczną throttlingować lub timeoutować. Upewnij się też, że szanujesz rozmiary puli połączeń DB.

Jak duża powinna być kolejka/bufor zadań?

Wybierz rozmiar, który absorbuje normalne skoki, ale nie ukrywa problemów przez minuty. Małe bufory szybciej ujawniają przeciążenie; duże bufory zwiększają zużycie pamięci i mogą powodować dłuższe czekanie, zanim wystąpi błąd. Zdecyduj z góry, co zrobić, gdy kolejka jest pełna: zablokować producenta, odrzucić zadanie, zapisać je gdzie indziej czy zwrócić jasny błąd.

Jak zapobiec utknięciu workerów na zawsze?

Używaj context.Context dla każdego zadania i upewnij się, że wywołania DB i HTTP go respektują. Ustaw timeouts na zewnętrzne wywołania i zdefiniuj zachowanie przy zamykaniu, żeby workerzy mogli się zatrzymać bez pozostawiania wiszących goroutine lub niedokończonej pracy.

Jakie metryki powinienem monitorować dla zadań w tle?

Monitoruj głębokość kolejki, czas oczekiwania w kolejce, czas trwania zadań (p50/p95/p99) oraz liczbę błędów i retryów. Te metryki pokażą, czy potrzebujesz więcej workerów, mniejszej kolejki, krótszych timeoutów czy ograniczeń dostępu do zależności.

Łatwy do uruchomienia
Stworzyć coś niesamowitego

Eksperymentuj z AppMaster z darmowym planem.
Kiedy będziesz gotowy, możesz wybrać odpowiednią subskrypcję.

Rozpocznij