Simas Razinskas

Nulinio prastovos laiko „blue-green“ diegimai su sveikatos patikra grįstu atstatymu

Būseną turinčių programų diegimas viename VM daug kartų per dieną su sveikatos patikra grįstais „blue-green“ leidimais, migracijų patikromis ir atstatymu prieš klaidai pasiekiant naudotojus.

Paskelbta 2026-06-30Peržiūrėta 2026-07-01

Trumpai

Problema
Būseną turinčiai programai reikėjo dažnų leidimų be prastovų, sugadintų konteinerių sraute ir blogų migracijų, sunaikinančių atstatymo kelią.
Apribojimas
Tikslas buvo vienas savistatos VM su viena tikra duomenų baze, be Kubernetes ir be valdomo apkrovos balansavimo, kuris sugertų klaidas.
Sukurta sistema
Vieną kartą kuriamas artefaktas, blue-green Compose servisai, Traefik maršrutizavimas, Docker sveikatos vartai, migracijų versijų patikros ir momentinės kopijos prieš migracijas.
Rezultatas
Paaukštinamas tik jau išbandytas atvaizdo tagas, srautas perjungiamas tik praėjus sveikatos patikrai, o nesėkmingas leidimas atstatomas prieš naudotojams jį pamatant.
Kur tai pritaikoma
Aktualu komandoms, kurioms reikia gamybinio diegimo patikimumo, bet kurios nenori iš karto šokti į per daug sudėtingą orkestravimo platformą.
Technologijos
Docker Compose · Traefik · Postgres · shell orchestration

Kodėl tai svarbu

Jei jūsų komanda planuoja sistemą su tais pačiais gedimo scenarijais — būtent nuo tokio mąstymo prasideda pirmas pokalbis.

„Blue-green“ diegimo schema: CI sukuria vieną kartą, parengties aplinka išbando, gamyboje veikia „blue“ ir „green“ konteineriai už sveikatos patikra grįsto tarpinio serverio, kuris arba perjungia srautą sėkmės atveju, arba automatiškai atstato nesėkmės atveju; migracijos rašomos „expand/contract“ principu su momentine duomenų bazės kopija prieš migraciją.

Apžvalga

Pirmiau aprašytas apribojimas — visa užduotis: viena savistatos VM, viena tikra Postgres duomenų bazė ir leidimų ritmas — dešimtys diegimų per dieną, be priimtinos prastovos ir be priimtino duomenų praradimo. Po šiuo sprendimu nėra orkestratoriaus — nei Kubernetes, nei valdomo apkrovos balansavimo įrenginio, nei antrojo regiono, į kurį būtų galima persijungti. Viskas toliau sukurta iš atvirkštinio tarpinio serverio, dviejų veikiančių to paties atvaizdo kopijų ir diegimo scenarijaus, kuris atsisako rinktis lengvesnį kelią. Programa turi būseną, ir bet kurią valandą ją naudoja tikri vartotojai, todėl kiekvienas trumpasis kelias, mainantis šiek tiek saugumo į šiek tiek patogumo, turi realų gedimo scenarijų už nugaros.

Architektūra: sukurti vieną kartą, paaukštinti nepakeistą

Didžioji dalis diegimo rizikos kyla iš perstatymo. Jei parengties ir gamybos aplinkos kiekviena vykdo savo docker build, jos iš tikrųjų nevykdo tos pačios programinės įrangos — pasikeitęs bazinis atvaizdas, podėlio nepataikymas, šiek tiek kitaip išspręsta priklausomybė, ir artefaktas, praėjęs visus testus, nebėra tas, kuris aptarnauja srautą. Sprendimas — visiškai pašalinti perstatymą iš gamybos kelio: CI sukuria lygiai vieną atvaizdą kiekvienam leidimui, pažymi jį „commit“ maišos (SHA) žyma, o ne latest, ir įkelia į registrą. Ta žyma — vienintelis dalykas, kuris kada nors pasikeičia tarp parengties ir gamybos.

Parengties aplinka vykdo tiksliai tą patį atvaizdą su parengties duomenų baze, įskaitant migracijas — ne fiktyvia duomenų baze, ne schemos poaibiu, o ta pačia migracija, kurią netrukus gaus gamybos duomenų bazė. Paaukštinimas tada — tiesiog gamybos diegimo scenarijaus nukreipimas į žymą, kuri jau praėjo patikrą; niekas neperstatoma, niekas iš naujo nesprendžiama, ir atotrūkis tarp „išbandyta“ ir „veikia“ susiaurėja iki nulio.

Kodėl ne tiesiog perkrauti konteinerį vietoje

Naivi alternatyva — docker compose up su nauju atvaizdu, vietoje — tai vienas konteineris, kurį reikia sustabdyti, kol jo pakaitalas galės užimti tą patį prievadą. Kelias sekundes tarp to programa neveikia. Dar blogiau — jei naujo atvaizdo migracija vykdoma konteinerio paleidimo metu ir yra klaidinga, ji jau įvykdyta tuo metu, kai kas nors tai pastebi; nebelieka jokios „prieš“ būsenos, į kurią grįžti, neatkuriant iš atsarginės kopijos. Dvi spalvos egzistuoja būtent tam, kad tarp „nauja versija egzistuoja“ ir „nauja versija aptarnauja srautą“ atsirastų sveikatos patikra.

  • Gamyboje veikiantis atvaizdas baitas į baitą identiškas tam, kuris praėjo parengties patikrą — tai ne „tas pats kodas, tik perstatytas“.
  • Nepavykęs parengties paleidimas niekada nepasiekia gamybos, nes gamyba neturi iš ko statyti pagal blogą „commit“ — tik žymą, kurią paaukštinti.
  • Atstatymas atvaizdo lygmeniu — tiesiog ankstesnės žymos paaukštinimas iš naujo, o ji jau turi žinomą gerą istoriją.

Schema: sukūrimo vieną kartą, tik paaukštinimo konvejeris — CI sukuria ir pažymi vieną atvaizdą, parengties aplinka jį išbando su tiesioginėmis migracijomis, o gamyba paaukština tą pačią žymą nepakeistą.

Sveikatos vartų projektavimas

Gamyboje veikia dvi to paties atvaizdo spalvos už atvirkštinio tarpinio serverio — vadinkime jas mėlyna ir žalia. Bet kuriuo metu viena gyva, kita laisva. Leidimas paleidžia laisvąją spalvą, nepaliesdamas gyvosios, ir tik laisvajai spalvai įrodžius save, tarpinis serveris perkelia srautą ant jos. Įrodymo mechanizmas — Docker integruota konteinerio sveikatos patikra: komanda, kurią variklis vykdo intervalu, su savo laiko limitu, pakartojimų skaičiumi ir paleidimo periodo malonės langu, kad lėtai pasileidžianti programa nebūtų pažymėta nesveika, dar nespėjus pakilti.

services:
  app-green:
    image: registry.internal/app:1.42.0
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/healthz"]
      interval: 5s
      timeout: 3s
      retries: 6
      start_period: 20s

Compose sveikatos patikra laisvajai spalvai

Kiekvienas laukas suderintas su realiu gedimo scenarijumi. interval pakankamai trumpas, kad regresija, atsiradusi iškart po paleidimo, būtų greitai pastebėta, bet ne toks trumpas, kad pati patikra sukurtų pastebimą apkrovą. start_period egzistuoja todėl, kad šaltai pasileidžiantis procesas — šildantis podėlius, atveriantis ryšių telkinį — pirmas kelias sekundes nepraeis griežtos patikros, nors realiai nėra sugedęs; be šio malonės lango vartai iš principo atmestų kiekvieną leidimą. retries amortizuoja vienkartinį trikdį — lėtą šiukšlių rinkimo pauzę, akimirksnio tinklo gedimą — neeskaluodamas jo iki atstatymo, bet vis tiek pagaudamas nuolat nepavykstančią patikrą.

Kas laikoma sveika

Patikra, tikrinanti tik „ar procesas priima ryšius“, praeina per lengvai — procesas gali veikti ir vis tiek aptarnauti šiukšles. Šiame sprendime vykdoma patikra patvirtina tris atskirus dalykus, ir bet kuriam iš jų nepavykus, spalva lieka pažymėta nesveika ir neįtraukiama į maršrutizavimo telkinį:

  • HTTP serveris priima ryšius
  • pavyksta atverti ryšį su duomenų baze
  • duomenų bazėje pritaikytos migracijos versija atitinka tai, ko tikisi šis atvaizdas
docker compose up -d app-green

until [ "$(docker inspect -f '{{.State.Health.Status}}' app-green)" = "healthy" ]; do
  sleep 2
done

update_proxy_target green
docker compose stop app-blue

Perjungimo scenarijus — palaukti sveikos būsenos, tada perjungti

Diegimo scenarijaus užduotis sąmoningai mechaniška: paleisti laisvąją spalvą, apklausti jos sveikatos būseną ir perjungti tarpinio serverio taikinį tik tada, kai pats Docker praneša healthy. Jis niekada netikrina programos žurnalų ir nepriima sprendimo — sprendimą jau priėmė sveikatos patikra, o scenarijus tiesiog veikia pagal jį.

Atstatymo (rollback) mechanika

Detalė, dėl kurios tai patikima, yra tai, kuo atstatymas nėra: tai ne antras kodo kelias. Gyvoji spalva niekada nesustabdoma, neištuštinama ir neliečiama, kol laisvoji spalva dar neįrodė esanti sveika. Jei sveikatos patikra niekada nepraneša healthy, diegimo scenarijus tiesiog niekada nepasiekia eilutės, perjungiančios tarpinį serverį — gamyba toliau aptarnauja lygiai tą pačią versiją, kurią aptarnavo prieš diegimo pradžią, nes joje niekas nepasikeitė.

Tai pašalina ištisą klaidų kategoriją: atstatymo kelią, kuris vykdomas tik realaus incidento metu, parašytas vieną kartą, niekada neišbandytas ir subtiliai klaidingas tada, kai jo galiausiai prireikia. Čia nėra atskiro atstatymo scenarijaus, kuris galėtų pasenti stalčiuje. Pažanga į priekį — srauto priėmimas naujojoje spalvoje — yra tai, ką reikia užsitarnauti; likimas senojoje spalvoje tiesiog įvyksta pagal nutylėjimą, jei niekas to neužsitarnauja.

Schema: nepavykusios patikros laiko juosta — laisvoji spalva pasileidžia, sveikatos patikra niekada nepraneša esanti sveika, o tarpinio serverio taikinys niekada nepasikeičia; gyvoji spalva toliau aptarnauja be pertrūkio.

Tai nepadaro atstatymo neklystančio. Sveikatos patikros scenarijus su sava klaida gali pranešti healthy, kai neturėtų, arba unhealthy, kai programa iš tikrųjų veikia gerai — vartai tik tiek sąžiningi, kiek sąžininga jų vykdoma patikra. O regresija, pasireiškianti tik realioje gamybos apkrovoje, o ne sintetinėje patikroje, vis tiek gali tvarkingai perjungti srautą ir paaiškėti tik vėliau. Tai atskira, sunkesnė problema, kurios šis sprendimas pats savaime neišsprendžia; būtent todėl patikra laikui bėgant tampa vis tikslesnė, o ne lieka bendriniu „ping“.

Migracijų strategija: „expand“ ir „contract“

Schemos pakeitimai — ten, kur dviejų spalvų sprendimas tampa reiklus, nes per visą perjungimo trukmę — ir tiek laiko po to, kiek dar gali įvykti atstatymas — ir senas, ir naujas programos kodas turi veikti su ta pačia duomenų baze. Migracija, kurią toleruoja tik naujas kodas, yra migracija, sugadinanti senąją spalvą tą akimirką, kai ji pritaikoma, o tai panaikina visą prasmę laikyti senąją spalvą gyvą kaip atsarginį variantą.

Expand, migrate, contract

Čia naudojama disciplina — lygiagrečių pakeitimų (parallel-change) principas: kiekvienas laužiantis schemos pakeitimas skaidomas į tris atskirus žingsnius vietoj vieno. „Expand“ prideda naują formą neliesdamas senosios — naują stulpelį, leidžiantį NULL, naują lentelę — todėl ankstesnis kodas, nežinantis apie ją, toliau veikia nepaliestas. „Migrate“ perkelia duomenis ir programos logiką į naują formą, kol abi formos egzistuoja kartu, papildant esamas eilutes paketais, o ne vienu ilgai trunkančiu sakiniu, nes migracija, trunkanti milisekundes su tūkstančiu testinių eilučių, su gamybos lentele su milijonais eilučių gali trukti valandas. „Contract“, vykdomas tik tada, kai niekas nebepriklauso nuo senos formos, ją pašalina.

-- Expand: add the new column, nullable, no default backfill yet
ALTER TABLE accounts ADD COLUMN display_name text;

-- Migrate: backfill in batches, then have new code read/write
-- display_name while old code keeps reading/writing legacy_name
UPDATE accounts SET display_name = legacy_name
WHERE display_name IS NULL AND id BETWEEN 1 AND 10000;

-- Contract: a later, separate release, once no running
-- instance still reads legacy_name
ALTER TABLE accounts DROP COLUMN legacy_name;

Stulpelio pervadinimas nesugadinant veikiančios versijos

Atkreipkite dėmesį, kad „contract“ žingsnis nėra to paties leidimo dalis kaip „expand“ — jis išleidžiamas vėliau, kai „expand“ jau pakankamai ilgai veikia gamyboje, kad atstatymas į ankstesnę versiją nebebūtų realistiškas. Atlikus tai vienu žingsniu, atstatymo taikinys būtų ištrintas tos pačios migracijos, nuo kurios jis turėtų apsaugoti.

Momentinės kopijos prieš migraciją

„Expand/contract“ sumažina, kaip dažnai migracija yra destruktyvi, bet nesumažina šios rizikos iki nulio — „contract“ žingsnis destruktyvus pagal apibrėžimą, o klaida bet kurios migracijos SQL kode vis tiek įmanoma, kad ir kaip atsargiai ji suplanuota. Prieš bet kuriai migracijai pasileidžiant gamyboje, duomenų bazė momentinai nukopijuojama — taip kiekviena migracija, net pati atsargiausia, gauna žinomą gerą atstatymo tašką, nepriklausomą nuo to, ar pati migracija buvo grįžtama.

pg_dump -Fc --no-owner appdb > backups/pre-migrate-$(date +%Y%m%d-%H%M%S).dump

Momentinė kopija, paimta tiesiai prieš migraciją

Tai nepakeičia nuolatinių atsarginių kopijų ar atstatymo iki konkretaus laiko momento — tai pigus, greitai pasiekiamas saugiklis, skirtas būtent kitoms trisdešimčiai rizikos sekundžių, paimamas tą vienintelę akimirką, kai rizika tuoj šoktels.

Kompromisai ir apribojimai

Nė vienas iš šių dalykų nėra nemokamas, o sąžiningumas dėl to, ko šis sprendimas neišsprendžia, yra projekto dalis:

  • Dvi gyvos spalvos maždaug padvigubina pastovios būsenos resursų sunaudojimą perjungimo lango metu — proporcinga vienai VM šalutiniam projektui, bet realus išlaidų punktas didesniu mastu.
  • Tai vis dar viena mašina. Tai apsaugo nuo blogo leidimo išleidimo, o ne nuo pačios VM gedimo — čia nėra persijungimo tarp mašinų.
  • Sveikatos vartai įrodo tik tai, ką tikrina. Programa, grąžinanti sveiką būseną, bet tyliai rašanti sugadintus duomenis, vis tiek praeina.
  • „Expand/contract“ turi būti taikomas disciplina ir peržiūra, o ne įrankių priverstinai — niekas nesutrukdo paskubomis parašyti laužiančios migracijos vienu žingsniu.
  • „Contract“ žingsnis labiausiai linkęs būti praleistas spaudžiant terminams, todėl pamažu kaupiasi stulpeliai, kurių niekas nebeskaito.
  • Šalutiniai poveikiai už duomenų bazės ribų — išsiųstas laiškas, suveikęs „webhook“, nuskaityta įmoka — niekuo iš to neapsaugoti. Atstatomas tik diegimas; tai, ką jis jau padarė išoriniame pasaulyje, lieka.

Šaltiniai ir tolesnis skaitymas

Šie principai nėra originalūs — jie gerai įsitvirtinę, ir verta juos paskaityti originalo forma:

Susijusios atvejų analizės

Sistemos, kurias su šia sieja bendras apribojimas — pakartojimai, izoliacija, kaina, atkūrimas.

Technologijos: Docker Compose · Traefik · Postgres · shell orchestration← Visos atvejų analizės

Turite sistemą, kuri privalo veikti?

Rezervuokite pokalbį — susidėliosime, ką ji turi daryti, kur ji lūš ir kokią mažiausią versiją verta kurti pirmiausia.