Skip to main content

29 Tekniikkaa

29Tekniikka logo
Laravelin queue toimii usein pitkään ilman, että siihen tarvitsee kiinnittää juuri mitään huomiota. Pyyntö käsitellään nopeasti, raskas työ siirtyy taustalle ja käyttäjä saa sivun heti takaisin.
Sitten jokin muuttuu.
Yksi job jää odottamaan. Seuraavatkin alkavat kasaantua. Lopulta huomataan, että esimerkiksi sähköposteja ei ole lähetetty tai ulkoiseen järjestelmään tarkoitettuja tietoja ei ole siirretty lainkaan.
Tässä vaiheessa vikaa ei kannata vielä nimetä Laravel-ongelmaksi. Queue on oikeastaan vain ketju useasta eri osasta. Job täytyy lähettää jonoon, jonkin prosessin pitää hakea se, työn pitää pystyä suorittamaan koodi loppuun asti ja mahdollisten uusintayritysten pitää olla hallittuja.
Jos yksi näistä kohdista pettää, käyttäjälle lopputulos näyttää samalta: job ei valmistunut.

Aloita yhdestä epäonnistuneesta jobista

Kun queue on täynnä virheitä, ensimmäinen reaktio on helposti katsoa koko järjestelmää kerralla.

Se ei yleensä auta.

Ota yksi epäonnistunut job ja selvitä mitä sille tapahtui. Mistä se lähetettiin? Mihin queueen se meni? Löysikö worker sen? Mikä virhe syntyi? Ehtikö job tehdä jotain ennen virhettä?

Tällä tavalla ongelma alkaa yleensä rajautua melko nopeasti.

Esimerkiksi failed_jobs-taulussa voi näkyä selvästi, että job kaatui tietokantavirheeseen. Silloin workerin uudelleenkäynnistäminen ei ratkaise varsinaista ongelmaa.

Toisessa tapauksessa job ei ole koskaan epäonnistunut. Se vain odottaa jonossa, koska worker ei käsittele sitä.

Nämä ovat kaksi aivan eri vikaa, vaikka käyttäjälle molemmat näkyvät samalla tavalla.

Jos jobit vain jäävät jonoon

➢ Tässä tilanteessa tarkistaisin ensin workerit.
➢ Laravel-sovellus voi toimia normaalisti samalla kun kaikki queue-workerit ovat pysähtyneet. Web-palvelin ei tarvitse queue-workereita näyttääkseen sivut käyttäjälle.
➢ Tämä aiheuttaa tuotannossa hieman hämäävän tilanteen: verkkosivusto toimii, mutta kaikki taustalla tapahtuva työ on pysähtynyt.
➢ Workerien pitääkin olla erikseen käynnissä ja valvottuina. Palvelimen uudelleenkäynnistyksen, deployn tai prosessin kaatumisen jälkeen niiden pitää myös palata takaisin.
➢ Jos tuotannossa käytetään Supervisorin kaltaista prosessienhallintaa, tarkistaisin ensin sen tilan. Jos workerit ajetaan jollain muulla tavalla, sama kysymys pätee:
  • Jos vastausta ei ole, queue-järjestelmässä on jo ylläpidollinen heikkous.
Kuka käynnistää workerin uudelleen, jos se kuolee?

Väärä queue on yllättävän helppo ongelma

Laravelissa job voidaan ohjata tiettyyn queueen.

dispatch(new GenerateReport)->onQueue('reports');

Jos worker kuuntelee vain oletusqueuea, raportti ei tietenkään valmistu.

Tässä ei välttämättä ole mitään varsinaista virhettä. Job on jonossa ja worker on käynnissä. Ne vain eivät puhu samasta jonosta.

Tämä tulee vastaan erityisesti projekteissa, joissa queue-rakennetta on muutettu ajan mittaan. Vanha worker-asetus voi jäädä tuotantoon, vaikka sovelluksen koodi käyttää jo uusia queue-nimiä.

Siksi tarkistaisin aina molemmat puolet: mihin job lähetetään ja mitä worker oikeasti kuuntelee.

Entä jos worker löytää jobin mutta se kaatuu?

Silloin tutkiminen siirtyy itse jobiin.

Esimerkiksi tällainen työ voi näyttää yksinkertaiselta:
public function handle()
{
$invoice = Invoice::findOrFail($this->invoiceId);

$response = Http::post($this->endpoint, [
'invoice' => $invoice->toArray(),
]);
}

Tuotannossa siinä on jo useita mahdollisia epäonnistumiskohtia.

Laskua ei ehkä enää löydy. Tietokantayhteydessä voi olla ongelma. Ulkoinen palvelu voi olla alhaalla. HTTP-pyyntö voi aikakatkaista.

Queue ei tässä tapauksessa ole ongelman varsinainen aiheuttaja.

Se vain suoritti koodin, joka epäonnistui.

Tämä ero kannattaa tehdä mahdollisimman aikaisin. Muuten queuea aletaan korjata tilanteessa, jossa vika on esimerkiksi tietokannassa tai ulkoisessa rajapinnassa.

Ulkoinen API tekee jobista helposti epäluotettavan

➠ Queueen siirretään usein juuri sellaisia töitä, joita ei haluta tehdä käyttäjän HTTP-pyynnön aikana.
➠ Integraatiot ovat hyvä esimerkki.
➠ Job käynnistyy, lähettää pyynnön ulkoiseen palveluun ja odottaa vastausta. Jos palvelu on hidas, worker odottaa. Jos yhteys katkeaa, job epäonnistuu. Jos API palauttaa virheen, seuraava kysymys on, pitäisikö työ yrittää uudelleen.
➠ Retry kuulostaa tässä hyvältä ratkaisulta.
➠ Aina se ei ole sitä.
➠ Jos ensimmäinen pyyntö ehti esimerkiksi luoda laskun ulkoiseen järjestelmään, mutta vastaus katosi matkalla, Laravel voi nähdä tilanteen epäonnistumisena. Uusi yritys voi silloin luoda saman laskun uudelleen.
➠ Tämän vuoksi integraatioihin liittyvien jobien kanssa pitää miettiä idempotenssia eikä vain retry-määrää.

Timeout ei ole vain asetuskysymys

Jos job kestää pitkään, timeoutia on helppo kasvattaa.

Se voi olla tarpeen, mutta ensin kannattaa selvittää, miksi työ kestää niin kauan.

Raportin muodostaminen voi oikeasti vaatia aikaa. Toisaalta job voi tehdä tuhansia tietokantakyselyitä yhden ison kyselyn sijaan.

Sama pätee tiedostojen käsittelyyn.

Jos yhden jobin pitäisi käsitellä esimerkiksi kokonainen suuri aineisto, ongelmaa ei välttämättä ratkaista muuttamalla timeoutia viidestä minuutista kahteenkymmeneen.

Työ voidaan joutua pilkkomaan pienempiin osiin.

Esimerkiksi yksi job voi käsitellä vain tietyn määrän rivejä ja lähettää seuraavan työn jatkamaan siitä. Tällöin yhden työn epäonnistuminen ei pysäytä koko aineiston käsittelyä.

Retry voi myös monistaa työn

Tämä on yksi kohta, joka jää queue-keskusteluissa helposti liian vähälle huomiolle.
Jobin uudelleen yrittäminen on turvallista vain, jos työn tekeminen uudelleen on turvallista.
Tietojen lukeminen ei yleensä aiheuta ongelmaa.
Mutta entä jos job:
  • veloittaa asiakkaalta
  • luo uuden tilauksen
  • lähettää sähköpostin
  • lisää tietueen ulkoiseen järjestelmään
  • lähettää webhookin?
Jos ensimmäinen suoritus ehti tehdä työn ja epäonnistui vasta lopussa, uusi yritys voi tehdä saman asian uudelleen.
Siksi tuotantokelpoisessa jobissa kannattaa miettiä myös epäonnistunutta keskeneräistä suoritusta.
Pelkkä tries = 3 ei vielä tee jobista luotettavaa.

Queue voi olla kunnossa, mutta tietokanta ei

➠ Pitkässä queue-jobissa tietokanta voi muodostua pullonkaulaksi ilman, että jobin koodi näyttää erityisen raskaalta.
➠ Esimerkiksi job käy tuhansia tietueita läpi ja tekee jokaiselle erillisen kyselyn.
➠ Pienellä testiaineistolla tätä ei välttämättä huomaa.
➠ Tuotannossa tietomäärä on toinen.
➠ Silloin jobin suoritus alkaa venyä, workerit pysyvät varattuina ja queue alkaa kasvaa.
➠ Lopulta näyttää siltä, että queue on hidas.
➠ Todellinen ongelma voi olla yksi huonosti toimiva kysely.
➠ Tässä tilanteessa katsoisin tietokannan kyselyaikoja ennen kuin lisäisin workerien määrää.

Lisää workereita ei aina ratkaise kasvavaa jonoa

Jos queue kasvaa, luonnollinen ajatus on käynnistää lisää workereita.

Joskus se toimii.

Joskus se pahentaa tilannetta.

Jos kaikki jobit käyttävät samaa tietokantaa, suurempi worker-määrä tarkoittaa myös enemmän samanaikaisia kyselyitä. Tietokanta voi olla jo valmiiksi rajalla.

Sama ongelma tulee vastaan ulkoisissa API-kutsuissa. Palvelu voi sallia vain tietyn määrän pyyntöjä minuutissa.

Silloin kymmenen uuden workerin käynnistäminen ei tee järjestelmästä kymmenen kertaa nopeampaa.

Queue-kapasiteettia pitää katsoa yhdessä niiden resurssien kanssa, joita jobit käyttävät.

Deploy voi jättää vanhan workerin käyntiin

Laravelin queue-worker on pitkäikäinen prosessi.

Se tarkoittaa, että worker on saattanut käynnistyä ennen kuin uusi sovellusversio julkaistiin.

Web-pyyntö käyttää jo uutta koodia, mutta vanha worker voi edelleen käyttää muistissa olevaa vanhaa versiota.

Tämä voi aiheuttaa tilanteen, joka näyttää aluksi täysin käsittämättömältä.

Uusi koodi toimii paikallisesti.

Deploy näyttää onnistuneen.

Silti queue antaa virheitä, joita uuden version ei pitäisi enää tuottaa.

Siksi workerit pitää ottaa huomioon deploy-prosessissa. Laravel tarjoaa tähän queue:restart-komennon, jolla käynnissä oleville workereille voidaan ilmoittaa hallitusta uudelleenkäynnistyksestä.

Tämän jälkeen prosessienhallinnan pitää käynnistää workerit uudelleen.

Jobin mukana kulkeva tieto voi olla vanhaa

➠ Queue tekee työn tarkoituksella myöhemmin.
➠ Tämä tarkoittaa, että jobin luomisen ja suorittamisen välillä voi tapahtua paljonkin.
➠ Asiakkaan tilaus voi muuttua. Käyttäjä voidaan poistaa. Tuotteen hinta voi päivittyä.
➠ Jos jobiin on tallennettu kokonainen Eloquent-malli tai suuri määrä alkuperäistä dataa, pitää tietää, mitä tietoa työn aikana oikeasti käytetään.
➠ Monessa tapauksessa jobiin riittää tunniste:
public function __construct(
public int $orderId
) {}
➠ Ja itse handle()-metodissa haetaan tuore tieto.
➠ Sekään ei ole yleispätevä sääntö. Jos jobin tarkoitus on käsitellä nimenomaan tiettyä historiallista tilaa, tietojen uudelleen hakeminen voi olla väärin.
➠ Oleellista on, että tämä päätös tehdään tarkoituksella.

Miksi job epäonnistui vasta tuotannossa?

Tämä on ehkä kiinnostavin kysymys koko aiheessa.

Kehitysympäristössä queue toimii.

Tuotannossa ei.

Syynä ei välttämättä ole ympäristön “huonompi” Laravel-asennus. Tuotannossa data on usein suurempaa ja työn suoritusmäärä aivan toinen.

Paikallisesti job käsittelee sata tietuetta.

Tuotannossa niitä on miljoona.

Paikallisesti API vastaa nopeasti.

Tuotannossa pyyntöjä tulee samaan aikaan paljon enemmän.

Paikallinen Redis on samassa koneessa. Tuotannossa yhteys kulkee verkon yli.

Siksi queue-jobin testaaminen vain yhdellä pienellä testiaineistolla kertoo melko vähän sen tuotantokäyttäytymisestä.

Mitä itse tarkistaisin ensimmäisenä?

Jos tuotannossa ilmoitetaan, että Laravel Queue ei toimi, en lähtisi heti muuttamaan workerien määrää tai retry-asetuksia.

Aloittaisin yhdestä epäonnistuneesta jobista.

Ensimmäisenä tarkistaisin, onko job todella tullut jonoon.

Sen jälkeen katsoisin, missä queue:ssa se on.

Jos job odottaa, tarkistaisin workerin.

Jos worker on käsitellyt työn, katsoisin exceptionin.

Jos virhe liittyy HTTP-pyyntöön, katsoisin myös ulkoisen palvelun lokit ja vastausajat.

Jos kyse on tietokannasta, mittaisin kyselyn eikä vain jobin kokonaiskestoa.

Jos job taas toimii käsin mutta epäonnistuu normaalissa ajossa, silloin ympäristö, worker-asetukset tai ajoitus voivat olla olennaisia.

Tällä tavalla tutkiminen pysyy konkreettisena.

Queue-ongelman korjaaminen ei aina tarkoita queue-asetuksen muuttamista

➠ Laravelin queue-järjestelmä on usein ensimmäinen asia, jota epäillään.
➠ Todellinen ongelma voi kuitenkin olla paljon alempana.
➠ Hidas SQL-kysely pitää workerin varattuna.
➠ Ulkoisen API:n timeout tekee työn suoritusajasta arvaamattoman.
➠ Liian suuri job kuormittaa muistia.
➠ Puuttuva ympäristömuuttuja kaataa työn vasta tuotannossa.
➠ Väärä queue-nimi jättää jobit odottamaan.
➠ Vanha worker käyttää vanhaa sovellusversiota.
➠ Näissä tilanteissa yhteinen nimittäjä on queue, mutta ratkaisu on joka kerta erilainen.

Lopuksi

Laravel Queue -jobin epäonnistuminen ei vielä kerro, missä vika on.

Se kertoo vain lopputuloksen.

Tuotannossa tärkeämpää on saada selville, mitä jobille tapahtui sen elinkaaren aikana: syntyikö se oikein, päätyikö oikeaan queueen, löysikö worker sen ja missä kohdassa suoritus katkesi.

Kun nämä asiat ovat näkyvissä, ongelmaa ei tarvitse arvailla.

Ja jos sama job epäonnistuu jatkuvasti, kannattaa katsoa vielä yksi askel pidemmälle. Onko itse job järkevässä muodossa? Onko työ liian suuri yhdelle suoritukselle? Voiko sen tehdä uudelleen turvallisesti? Riippuuko se palvelusta, jonka vastausaikaa ei oikeasti hallita?

Hyvä queue-ratkaisu ei ole sellainen, jossa yksikään job ei koskaan epäonnistu.

Parempi tavoite on järjestelmä, jossa epäonnistuminen on näkyvä, rajattavissa ja turvallisesti käsiteltävissä.