29 $/mėn. „užduočių planuotojų“ absurdas ir Kubernetes monstrai

Kas keletą mėnesių Silicio slėnio rizikos kapitalo inkubatoriuose gimsta naujas „revoliucinis“ produktas: debesų užduočių planuotojas (angl. serverless cron orchestrator), siūlomas už „ypatingą“ 29 ar 49 dolerių mėnesinį mokestį. Šviežiems programuotojams, kurie paniškai bijo atsidaryti SSH terminalą ir parašyti crontab -e, parduodamas gražus pastelinės spalvos valdymo skydelis, trys animuotos diagramos ir „Slack“ pranešimų integracija. Tačiau po visa šia brangia rinkodaros migla slepiasi ta pati elementari, vos šešiasdešimties C kodo eilučių programa, kurią Brianas Kernighanas ir Kenas Thompsonas „Bell Labs“ laboratorijoje sukūrė dar Džimio Karterio prezidentavimo metais.

Dar didesnį absurdą stebime modernioje įmonių infrastruktūroje. Vardan madingos „debesų brandos“ inžinieriai sukuria milžinišką „Kubernetes“ klasterį, sukonfigūruoja 80 eilučių YAML CronJob manifestą, parsiunčia 450 megabaitų „Alpine Linux“ ir „Node.js“ konteinerį, rezervuoja 2 gigabaitus operatyviosios atminties (RAM) ir laukia trisdešimt sekundžių, kol konteineris pakils. Kam? Tam, kad kartą per parą įvykdytų dviejų eilučių SQL užklausą, ištrinančią dešimt pasenusių sesijų įrašų iš duomenų bazės. Mes paėmėme elegantiškiausią, patikimiausią ir mažiausiai resursų reikalaujančią abstrakciją kompiuterijos istorijoje ir palaidojome ją po sunkiais nereikalingų abstrakcijų sluoksniais. Tuo tarpu kuklus, pamirštas Linux virtualusis serveris kampe su viena Vixie cron eilute 0 3 * * * tyliai ir nepriekaištingai atlieka tas pačias užduotis jau keturiolika metų be menkiausio lūžio.

Laiko dievo „Chronos“ istorija: nuo Unix v7 (1979 m.) iki Paul Vixie

Automatizuoto užduočių planavimo šaknys siekia pačią daugiavartotojiškų operacinių sistemų aušrą. Kai 1979 metais „Bell Labs“ išleido septintąją „Unix Version 7“ versiją, joje debiutavo foninis demonas cron. Jo vardas kilo iš senovės graikų laiko personifikacijos – dievo Chrono (Χρόνος). Ši pirminė versija pasižymėjo asketišku griežtumu: tai buvo vienas nuolat veikiantis procesas, kuris kas minutę pabusdavo, nuskaitydavo centrinį tekstinį failą /usr/lib/crontab, patikrindavo laiką ir paleisdavo atitinkamas programas supervartotojo (root) teisėmis. Jei paprastam programuotojui reikėjo suplanuoti atsarginių kopijų kūrimą, tekdavo eiti pas barzdotą sistemos administratorių ir maldauti, kad šis rankiniu būdu papildytų centrinį sisteminį failą.

Tikroji revoliucija įvyko 1987 metais, kai talentingas inžinierius Paulas Vixie „Usenet“ konferencijoje comp.sources.unix paskelbė Vixie Cron v1. Jis iš esmės perrašė visą architektūrą. Vixie sukūrė atskirus, kiekvienam sistemos vartotojui izoliuotus tvarkaraščių failus kataloge /var/spool/cron/crontabs, įdiegė saugų crontab įrankį su griežta setuid prieigos kontrole ir sugalvojo sintaksę, kurią šiandien laikome savaime suprantama: kableliais atskirtus sąrašus (1,15,30), skaitinius rėžius (9-17), patogius žingsnius (*/15) bei raidines santrumpas (JAN-DEC, SUN-SAT). Kiekvienas šiuolaikinis „Linux“, „macOS“, „FreeBSD“ ar „OpenBSD“ serveris iki šiol naudoja tiesioginį Paulo Vixie kodo palikimą.

5 stulpelių anatomija ir lemtingi „Mėnesio diena vs Savaitės diena“ spąstai

Klasikinė Unix crontab eilutė susideda iš penkių laiko stulpelių ir vykdomosios komandos: Minutė (0–59), Valanda (0–23), Mėnesio diena (1–31), Mėnuo (1–12) ir Savaitės diena (0–7, kur 0 ir 7 yra sekmadienis). Nors ši lentelė atrodo elementari, joje slypi vieni pavojingiausių inžinerinių spąstų visoje Unix ekosistemoje, dėl kurių ne vienam programuotojui teko raudonuoti prieš buhalteriją.

Pagal įprastą loginę intuiciją, programuotojai tikisi, kad visi sąlygų filtrai jungiami loginiu AND (ir). Jei nurodote 0 minutę, 3 valandą, 15 mėnesio dieną ir penktadienį, logiška manyti, kad užduotis bus vykdoma tik tada, kai 15 mėnesio diena sutampa su penktadieniu. Tačiau Vixie cron veikia visiškai kitaip. Oficiali POSIX specifikacija griežtai numato: jei abu laukai – Mėnesio diena (3 stulpelis) ir Savaitės diena (5 stulpelis) – yra apriboti (nė vienas iš jų nėra žvaigždutė *), demonas juos vertina pagal loginį OR (loginį ARBA), o ne sankirtą!

Pavyzdžiui, jei įrašysite 0 3 15 * 5, tikėdamiesi, kad sąskaitos bus išsiųstos tik penktadienį, 15-ą mėnesio dieną, demonas suveiks kiekvieną mėnesio penktadienį BEI papildomai kiekvieno mėnesio 15-ą dieną! Vietoj vieno ar dviejų paleidimų per metus, komanda bus įvykdyta penkis ar šešis kartus per mėnesį. Jei jums iš tiesų reikalinga griežta sankirta (pavyzdžiui, paleisti užduotį tik penktadienį, 13-ą dieną), vieną iš stulpelių privalote palikti žvaigždute, o antrąjį filtrą perkelti į pačią komandą: 0 3 13 * * [ $(date +\%u) -eq 5 ] && /usr/local/bin/generuoti_saskaitas.sh.

Vasaros/žiemos laiko (DST) košmarai ir >/dev/null 2>&1 higiena

Serverio operacinės sistemos laiko nustatymas pagal vietinę laiko zoną (pvz., „Europe/Vilnius“) gamybinėje aplinkoje prilygsta rusiškai ruletei. Du kartus per metus, sukiojant vasaros ir žiemos laiką (DST), cron demonas patiria tikrą laiko anomaliją. Pavasarį, kai laikrodžio rodyklė peršoka iš 01:59:59 tiesiai į 03:00:00, visos užduotys, suplanuotos tarp 02:00 ir 02:59, yra apskritai praleidžiamos, nes tokia valanda serverio procesoriaus cikle paprasčiausiai neegzistavo. Rudenį, kai laikas persukamas atgal iš 02:59:59 į 02:00:00, ta pati antros valandos užduotis bus įvykdyta du kartus dviejose skirtingose fizinėse valandose su identišku laiko antspaudu. Todėl pirmoji griežta taisyklė: visi gamybiniai serveriai ir duomenų bazės privalo veikti išimtinai suderintuoju pasauliniu laiku (UTC).

Antroji kritinė higienos taisyklė – išvesties srautų valdymas. Pagal nutylėjimą, jei suplanuota komanda išspausdina bent vieną simbolį į standartinį išvesties (stdout) arba klaidų (stderr) srautą, cron demonas bando išsiųsti šį tekstą vietiniam vartotojui per sisteminį pašto siuntėją sendmail. Šiuolaikiniuose serveriuose, kur vietinis SMTP paštas nėra sukonfigūruotas, šie laiškai kaupiasi faile /var/spool/mail/root. Jei skriptas leidžiamas kas minutę ir registruoja smulkias klaidas, per pusmetį susikaupia šimtai tūkstančių pranešimų, sugeneruojančių dešimtis gigabaitų šiukšlių. Tai išnaudoja visus failų sistemos inodus (angl. inode exhaustion) ir visiškai paralyžiuoja serverį, sustabdydamas duomenų bazes su klaida „No space left on device“. Štai kodėl patyrę administratoriai prirašo >/dev/null 2>&1 (nukreipia abu srautus į juodąją skylę) arba nukreipia įrašus į sisteminį žurnalą per logger -t cron_darbas.

Paslėptos Unix supergalios: L, W, # ir @reboot makrokomandos

Nors klasikinis 5 stulpelių Vixie cron formatas patenkina 90% kasdienio automatizavimo poreikių, šiuolaikinės įmonių sistemos (tokios kaip „Quartz Scheduler“, „AWS EventBridge“, „Spring Boot“ bei išplėstinis Vixie cron) palaiko specialiuosius modifikatorius, išsprendžiančius sudėtingiausius kalendorinius galvosūkius:

  • Raidė L (angl. Last): Paskutinės mėnesio dienos problema yra tikras košmaras programuotojams dėl kintamo vasario dienų skaičiaus (28 arba 29 d. keliamaisiais metais) bei 30/31 d. kaitos. Išraiška 0 23 L * * elegantiškai išsprendžia šį klausimą be jokių pagalbinių skriptų – užduotis suveiks tiksliai paskutinę bet kurio mėnesio dieną. Taip pat galima nurodyti 5L (paskutinis mėnesio penktadienis).
  • Raidė W ir derinys LW (angl. Last Weekday): Buhalterijos ir finansų automatizavimo šventasis gralis. Simbolis W randa artimiausią darbo dieną (pirmadienį–penktadienį). Pavyzdžiui, 15W reiškia: jei 15-a diena yra šeštadienis, vykdyti penktadienį (14 d.); jei sekmadienis – pirmadienį (16 d.). Tuo tarpu derinys 0 17 LW * * garantuoja, kad atlyginimų pavedimai ar mėnesinės ataskaitos bus sugeneruotos tiksliai paskutinę mėnesio darbo dieną.
  • Grotelės # (N-toji savaitės diena): Leidžia apibrėžti n-tąjį savaitės dienos pasikartojimą mėnesyje. Pavyzdžiui, JAV Padėkos diena švenčiama ketvirtąjį lapkričio ketvirtadienį – tai išreiškiama 0 9 * 11 4#4. Kodas 1#1 reiškia pirmąjį mėnesio pirmadienį.
  • Vixie Cron @ makrokomandos: Vietoj laiko stulpelių galima naudoti intuityvius trumpinius: @hourly, @daily, @weekly, @monthly, @yearly. Ypatingo inžinierių dėmesio nusipelno @reboot. Tai viena geriausių ir lengviausių alternatyvų sudėtingiems „systemd“ ar „supervisord“ servisams – komanda @reboot /usr/bin/python3 /var/www/botas.py & bus automatiškai įvykdyta vieną kartą vos tik perkrovus ar paleidus serverį.