07 — Programmatūras inženierija

No silīcijam tuvā C līdz edge TypeScript

Es izvēlos valodu pēc kļūmes režīma, no kura cenšos izvairīties.

Rust tur, kur konkurencei jābūt pierādāmi pareizai, Go tur, kur tīkla pakalpojumiem jāpaliek vienkāršiem, C tuvu aparatūrai, Python un TypeScript tur, kur modeļi un cilvēki satiekas ar sistēmu — izvēlēti apzināti, ekspluatēti dziļumā.

Apliecinājums

Godīgs inženiera mērs nav steks CV — tas ir tas, kas izdzīvojis saskari ar reāliem lietotājiem. Mans ir vienpadsmit gadi tā.

Vairāk nekā vienpadsmit gadus es esmu piegādājis mobilās lietotnes reāliem lietotājiem uz iOS, Android un Windows. Šis skaitlis ir apliecinājums, kuram es uzticos visvairāk, jo piegādāt ierīcei kāda kabatā ir nesaudzīgi: nav staging vides avārijai uz svešinieka tālruņa, nav rollback funkcijai, kas mulsina cilvēku, kurš to lieto. Desmitgade tā māca specifisku disciplīnu — versiju fragmentāciju, bezsaistes uzvedību, akumulatora un atmiņas budžetus un lēno gaumes uzkrāšanos par to, ko atstāt ārpusē.

Aiz katras no šīm lietotnēm bija backend un mākoņa infrastruktūra zem tā. Tātad prakse nav tikai stikla priekšpuse; tā skrien no skāriena notikuma uz leju, caur API, rindu, datu krātuvi un konteineru, līdz kodolam, uz kura konteiners darbojas. Esmu pavadījis pietiekami daudz laika katrā slānī, lai zinātu, kur ir šuves, un lai projektētu tā, ka šuves neplīst zem slodzes.

Tas, ko esmu pievienojis kopš tā laika, ir dziļums sistēmu valodās un vairāku mākoņu piegādē: Rust un Go kodoliem, kuriem jābūt pareiziem un ātriem, Docker un Kubernetes piegādei, un apzināts atteikums piesaistīt jebko no tā vienam piegādātājam. Pārējā šī lapa ir tā konkrētā forma — valodas, platformas, mākoņi un mazie rīki, ko būvēju sev pa ceļam.

0+

gadi, piegādājot mobilās lietotnes reāliem lietotājiem

0

mobilās platformas produkcijā: iOS, Android, Windows

0

mākoņi, ekspluatēti bez piesaistes piegādātājam: GCP, Azure, AWS

0

galvenās valodas, apgūtas dziļumā: Rust, Go, Python, TypeScript

Valodu steks

Steks, izvēlēts pēc slāņa, nevis pēc modes.

virsma — lietotāji, modeļi, dati TypeScript / JS Node, pārlūks, edge. Python MI, dati, zinātniskie aprēķini, automatizācija. Go Konkurentiski tīkla pakalpojumi. Rust Atmiņā droši, konkurentiski kodoli. C / C++ Tuvu aparatūrai. aparatūra — silīcijs, reģistri, programmaparatūra

Sistēmu valodu slāņu diagramma

Katra valoda nopelna savu vietu slānī, kur svarīgas tās garantijas.

Es neķeros pie vienas iecienītas valodas, locot katru problēmu uz to. Diagramma lasāma no silīcija uz augšu: C un C++ tur, kur kods skar aparatūru, Rust atmiņā drošajiem konkurentiskajiem kodoliem, Go konkurentiskajam tīkla slānim, tad Python un TypeScript virsmā, kur dzīvo MI, dati un lietotāji.

Joslu platums ir aptuvena izjūta par lietojuma plašumu manā darbā — nevis etalons, tikai tas, kur mēdz atrasties koda apjoms.

  • Zemākie slāņi pērk determinismu un drošību
  • Augšējie slāņi pērk ātrumu un tvērumu
  • Robeža ir apzināta izvēle, pieņemta katram projektam
Mazs gabaliņš no īstā

Kā kods patiešām izskatās.

Ierobežots strādnieku pūls Go — izvērst darbu pa goroutines, savākt rezultātus caur channel un ievērot atcelšanu. Šis raksts parādās gandrīz katrā tīkla pakalpojumā, ko rakstu.

// FanOut runs fn across at most n goroutines, streaming results
// back on a channel and stopping early if ctx is cancelled.
func FanOut[T, R any](ctx context.Context, in []T, n int, fn func(T) R) []R {
    jobs := make(chan int)
    out := make([]R, len(in))
    var wg sync.WaitGroup

    for w := 0; w < n; w++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for i := range jobs {
                select {
                case <-ctx.Done():
                    return
                default:
                    out[i] = fn(in[i])
                }
            }
        }()
    }

    for i := range in {
        select {
        case <-ctx.Done():
            close(jobs)
            wg.Wait()
            return out
        case jobs <- i:
        }
    }
    close(jobs)
    wg.Wait()
    return out
}
Četras jomas, dziļumā

Kurp patiešām aiziet inženierija.

Izvēlēties valodu pēc kļūmes režīma, no kura izvairos.

Rust ir tur, kur pareizība konkurences apstākļos nav apspriežama. Borrow checker nav nodoklis — tā ir kompilācijas laika pierādījums, ka noteikta datu sacīkšu un use-after-free kļūdu klase nevar rasties, kas ir tieši tas, ko vēlos veiktspējas kritiskajam kodolam, kam jādarbojas bez uzraudzības.

Go ir tur, kur gribu konkurentiskus tīkla pakalpojumus, par kuriem varu ātri spriest: goroutines un channels konkurences modelim, iebūvētais pprof profilētājs karstā ceļa atrašanai, un statisks binārs, kas tīri izvietojas konteinerā. C un C++ paliek rezervēti aparatūrai tuvajam darbam — programmaparatūra, iegultais SoC, kods, kas atrodas tieši uz reģistru kartes.

  • Rust — ownership, lifetimes, nulles izmaksu abstrakcijas, bezbailīga konkurence
  • Go — goroutines, channels, context atcelšana, pprof profilēšana
  • C / C++ — iegultā programmaparatūra, aparatūrai tuva vadība, deterministisks laiks
  • Bash un SQL produkcijā, uz PostgreSQL un BigQuery
Vairāku mākoņu topoloģija

Viena lietotne, trīs mākoņi, bez piesaistes.

Slodze, ko es saglabāju pārnesamu: konteineri un infrastruktūra kā kods centrā, izvietojama uz tā piegādātāja, kurā uzdevums jau dzīvo. Mākonis ir preces slānis, nevis atkarība.

Lietotnes kodols Docker · K8s · IaC GCP Cloud Run · GKE · Vertex AI Azure AKS · Functions · Apps AWS Lambda · EKS · S3 · RDS Edge CF Workers · Vercel Edge
01

GCP

Vertex AI, Cloud Run, Cloud Functions, GKE, Pub/Sub, BigQuery, Firestore — mana noklusējuma izvēle MI slodzēm un pārvaldītai konteineru piegādei.

02

Azure

AKS, Functions, Cognitive Services, Container Apps — izmantots tur, kur organizācija jau dzīvo Microsoft vidē.

03

AWS

Lambda, ECS/EKS, S3, RDS, SQS/SNS aiz CloudFront — primitīvu plašums tradicionālajiem produkcijas stekiem.

04

Edge

Cloudflare Workers un Vercel Edge — loģika, kas darbojas tuvu lietotājam, latentuma un globālā tvēruma dēļ.

05

BaaS

Supabase un Firebase (Auth, Firestore, Realtime DB) — kad produktam vajadzīgs backend dienās, nevis nedēļās.

06

Piegāde

Docker un Kubernetes, GitHub Actions un GitLab CI, infrastruktūra kā kods — reproducējamas vides no sākuma līdz beigām.

Mākonim jābūt precei, ko varu nomainīt, nevis atkarībai, kas pieder produktam.

Rīku būvētāja instinkts
berze izdarīta divreiz mazs rīks ~/.local/bin rīkkopa uzkrājas

Nepārtraukta personīga Linux rīkkopa

Katra darbplūsmas berze kļūst par mazu programmu.

Es darbinu Linux administratora līmenī — kodola regulēšana, sysctl nostiprināšana, systemd vienības, konteineru izpildvides un tīkls zem tā. Bet daļa, kas definē, kā es strādāju, ir mazāka: gadu gaitā esmu uzrakstījis desmitiem savu komandrindas rīku, vienu pēc otra, katrs dzimis no berzes, ar ko saskāros vairāk nekā vienreiz.

Uzdevums, kas izdarīts divreiz, ir kandidāts rīkam. Rezultāts ir darbstacija, kas iederas manās rokās, nevis noklusējumos — un, vēl svarīgāk, tā instinkts, kurš būvē rīku, nevis pacieš robu. Šis instinkts ir tas pats, kas galu galā tiek piegādāts produktos.

  • Kodola regulēšana, sysctl nostiprināšana, systemd, konteineru izpildvides, tīkls
  • Desmitiem pašrakstītu CLI rīku, katrs risina atkārtotu berzi
  • Rīku būvētāja, nevis tikai rīku lietotāja reflekss
Steks, īsumā

Ko es ienesu būvē.

Inženierijas steks

Galvenās sistēmu valodas
Rust, Go, C/C++
Augsta līmeņa valodas
Python, TypeScript/JS
Mobilā — natīvā
Swift, Obj-C, Kotlin, Java
Mobilā — vairāku platformu
React Native, Flutter
Backend risinājumi
Node, Spring, Django, Rails, Go
API stili
REST, GraphQL
Datu krātuves
PostgreSQL, BigQuery, SQLite, Redis
Konteineri
Docker, Kubernetes
CI/CD
GitHub Actions, GitLab CI
Linux
Administratora līmenis

Nekas no tā nav vēlmju saraksts. Katra rinda ir rīks, ar kuru esmu piegādājis — izvēlēts konkrētā projektā, jo tas bija pareizā atbilde tam slānim un tam kļūmes režīmam.

Caurviju līnija visam ir tas pats temperaments: būvēt sistēmu pirms faila, projektēt šuvēm, saglabāt piegādi reproducējamu un atteikt atkarības, kas vēlāk piederētu produktam.

Notikumvirzīta arhitektūra

Atsaistīts ar ziņojumu, nevis ar cerību.

ražotājs lietotne · ierīce ražotājs pakalpojums brokeris Pub/Sub · rinda patērētājs patērētājs patērētājs mirusī vēstule

Ražotāja / brokera / patērētāja topoloģija

Komponenti sarunājas caur brokeri, lai viena kļūme palēnina sistēmu, nevis to aptur.

Kad pakalpojumi izsauc viens otru tieši, viena lēna atkarība pārvēršas kaskādē. Es tā vietā projektēju ap notikumu brokeri: ražotāji publicē faktus par notikušo, brokeris tos noturīgi tur, un patērētāji apstrādā savā tempā. Ražotājs nekad nebloķējas, gaidot patērētāju, un patērētājs, kas atpaliek, izsmeļ savu uzkrājumu, nepazaudējot darbu.

Notikumi kļūst par notikušā patiesības avotu — mirušo vēstuļu rinda noķer to, ko nevar apstrādāt, atkārtojumi ir ierobežoti un idempotenti, un jauns patērētājs var abonēt to pašu straumi, nevienam nemainot ražotāju. Tā ir īpašība, ko es pērku: tādu daļu neatkarīga attīstība, kurām nekad nav jāzina viena par otru.

  • Ražotāji publicē, patērētāji abonē — bez tiešas saistīšanas
  • Noturīgs brokeris absorbē uzplūdumus; patērētāji apstrādā savā tempā
  • Mirušo vēstuļu rinda un ierobežoti, idempotenti atkārtojumi kļūmēm
Piegādes pīpelis

No commit līdz produkcijai, artefaktam virzoties uz priekšu.

CI/CD pīpelis, ko uzskatu par neapspriežamu infrastruktūru. Attēls tiek izbūvēts vienreiz un neizmainīts virzīts caur katriem vārtiem, tā ka tas, kas darbojas produkcijā, ir baits pa baitam tas pats, kas izturēja testus.

No commit līdz canary

  1. 01 Commit Push uz zaru. Pirms-commit āķi palaiž formatēšanu un lint lokāli, lai acīmredzamais tiktu noķerts, pirms CI vispār iedarbojas.
  2. 02 Būvēt un testēt Konteinera attēls izbūvēts vienreiz, vienības un integrācijas testi palaisti pret to, pārklājuma slieksnis ieviests. Tas pats artefakts virzās uz priekšu.
  3. 03 Skenēt Statiskā analīze, atkarību audits un attēla ievainojamību skenēšana. Neizdevusies skenēšana bloķē apvienošanu, nevis cilvēka recenzenta atmiņa.
  4. 04 Staging Izvietot parakstīto attēlu staging vidē, kas atspoguļo produkciju, palaist smoke testus un īsu soak.
  5. 05 Virzīt Izlaist produkcijā aiz canary vai blue-green slēdža, vērot metrikas un turēt iepriekšējo versiju vienas komandas attālumā.
rollback — iepriekšējā versija vienas komandas attālumā commit būve+ tests skenēt staging canaryprod izlaišana
Reāllaika pakalpojumi

Stāvoklis, kas nests pa soketu, ar apstrādātiem kļūmes režīmiem.

klients · ws klients · ws klients · ws vārtejaizbeigt · novirzīt apstrādātājs apstrādātājs koplietota stāvokļa krātuve

Soketa pakalpojumu tīkls

WebSocket vārteja priekšā, stāvokļa savienojumi aizmugurē, koplietota krātuve noturībai.

Reāllaika darbs dzīvo vai mirst pēc neglamūrīgajām daļām: kas notiek, kad klienta savienojums pārtrūkst ziņojuma vidū, kad patērētājs atpaliek no ražotāja, kad tas pats notikums pienāk divreiz. Es izbeidzu WebSocket un soketa savienojumus vārtejā, novirzu katru uz stāvokļa apstrādātāju un turu noturīgu stāvokli koplietotā krātuvē, lai atkārtoti pieslēdzošs klients var atsākt, nevis pārstartēt.

Pretspiediens ir skaidrs — ierobežoti buferi ar definētu atmešanas vai apvienošanas politiku, nekad neierobežota rinda, kas klusi ēd atmiņu, līdz process mirst. Klātbūtne tiek izsekota ar heartbeats un TTL atslēgām, un piegāde ir idempotenta, lai at-least-once transports nekļūtu par at-least-twice uzvedību lietotājam.

  • WebSocket / soketa vārteja izbeidz un novirza savienojumus
  • Stāvokļa apstrādātāji uz savienojumu; koplietota krātuve noturībai
  • Heartbeats, TTL klātbūtne, atsākšana pie atkārtota pieslēguma, idempotenta piegāde

Reāllaika primitīvi

Transports
WebSocket, neapstrādāti TCP soketi, gRPC straumes
Fan-out
Pub/Sub, Redis kanāli, SSE vienam virzienam
Pretspiediens
Ierobežoti buferi, atmešanas / apvienošanas politikas
Piegāde
Idempotences atslēgas, at-least-once + deduplikācija
Klātbūtne
Heartbeats, TTL atslēgas, atkārtota savienošana ar atsākšanu
Stāvoklis
Aktieris uz savienojumu, koplietota krātuve noturībai
Otrs gabaliņš no īstā

Vēl divi fragmenti no izmantotā steka.

Rust ownership un Go channels risina to pašu problēmu — koplietotu stāvokli konkurences apstākļos — no pretējiem galiem: viens pārvieto ownership, lai nebūtu ko dalīt, otrs dala, komunicējot.

// Ownership moves; the compiler proves no use-after-move.
fn consume(buf: Vec<u8>) -> usize {
    buf.len() // buf is owned here, freed at the end
}

fn main() {
    let data = vec![1u8, 2, 3];
    let n = consume(data);
    // using 'data' here would not compile: it was moved.
    println!('consumed {} bytes', n);

    // Borrow instead of move when the caller keeps the value.
    let kept = vec![4u8, 5];
    let total: u8 = kept.iter().sum();
    println!('{} still owns {} items', total, kept.len());
}
// Share by communicating: a generator feeds a stage over a channel.
func gen(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for _, n := range nums {
            out <- n
        }
    }()
    return out
}

func square(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for n := range in {
            out <- n * n
        }
    }()
    return out
}
Edge apstrādātājs

Loģika, kas darbojas tuvu lietotājam.

TypeScript edge apstrādātājs — tāds, ko izvietoju uz Cloudflare Workers vai Vercel Edge. Tas darbojas klātbūtnes punktā, kas vistuvāk pieprasījumam, tā ka keša pārbaude vai autentifikācijas vārti nekad nemaksā turp-atpakaļ braucienu uz centrālo reģionu.

// Runs at the edge: serve from cache, fall through to origin once.
export default {
  async fetch(req: Request, env: Env, ctx: ExecutionContext) {
    const cache = caches.default;
    const hit = await cache.match(req);
    if (hit) return hit;

    const res = await fetch(req);
    if (res.ok && req.method === 'GET') {
      const cached = new Response(res.body, res);
      cached.headers.set('cache-control', 'public, max-age=60');
      ctx.waitUntil(cache.put(req, cached.clone()));
      return cached;
    }
    return res;
  },
};
Datu krātuves slānis

Pareizā krātuve piekļuves rakstam.

Es neizvēlos datu krātuvi pēc ieraduma. Es to izvēlos pēc lasījumu un rakstījumu formas, kas tai jāabsorbē, un pieņemu, ka lielākā daļa reālo sistēmu izmanto vairāk nekā vienu.

Relāciju krātuve nopelna noklusējuma pozīciju, jo lielākā daļa datu ir relacionāli un transakcijas ir vērtas savas izmaksas. Bet karstais ceļš, ko apkalpo kešs, analītiskais jautājums, kas skenē miljardus rindu, un local-first noturība uz tālruņa ir trīs dažādas problēmas — un izlikšanās, ka viens dzinējs atbild uz visām trim, ir tas, kā sistēmas kļūst lēnas veidos, kurus vēlāk grūti atsaukt.

Disciplīna zem tām visām ir tā pati: shēma dzīvo kā versionētas, atgriežamas migrācijas repozitorijā, indeksi tiek projektēti pret faktisko vaicājuma plānu, nevis uzminēti, un karstā ceļa vaicājumi tiek lasīti un noregulēti ar roku pat tad, kad ORM uzrakstīja pirmo melnrakstu.

01

PostgreSQL

Noklusējuma relāciju krātuve — transakcijas, JSONB tur, kur tas pelna savu vietu, un indeksi, kas projektēti pret vaicājuma plānu, nevis uzminēti.

02

BigQuery

Analītisks mērogs. Kolonnu glabāšana un sadalīšana jautājumiem, kas skenē miljardus rindu, bet darbojas reti.

03

SQLite

Noturība ierīcē un iegulta mobilajām un edge ierīcēm — viens fails, nulle servera, pareizais rīks local-first lietotnēm.

04

Redis

Karstais ceļš — kešošana, ātruma ierobežošana, rindas, īslaicīga klātbūtne un slēdzenes, kur mikrosekundes un TTL ir svarīgi.

05

ORM

Sequelize, Mongoose, GORM tur, kur tie paātrina piegādi — bet ģenerētais SQL paliek lasāms, un karstie vaicājumi tiek rakstīti ar roku.

06

Migrācijas

Shēma kā versionētas, atgriežamas migrācijas repozitorijā, lai datubāze būtu reproducējama, nevis arheoloģiska.

Linux nostiprināšanas steks

Kodols, uz kura darbojas konteiners, uzskatīts par produkta daļu.

Atjauninājumi Bezuzraudzības drošības ielāpi, reproducējami attēli Audits auditd, journald saglabāšana, žurnālu nosūtīšana ārpus hosta Process systemd smilškaste, seccomp, namespaces, cgroups Tīkls nftables default-deny, fail2ban, segmentēti porti Identitāte Mazāko privilēģiju lietotāji, sudo politika, tikai SSH atslēgas Sāknēšana un kodols Parakstīta sāknēšana, minimāli moduļi, sysctl bāzlīnija ārējais — eksponētā virsma, novērota un reģistrēta

Dziļumā veidots aizsardzības steks

Drošība kā slāņi, no parakstītas sāknēšanas līdz auditam ārpus hosta, nevis viena ugunsmūra kārtula.

Es administrēju Linux līdz kodolam un uzskatu hostu par uzbrukuma virsmas daļu, nevis inertu vietu, kur darbināt konteineru. Steks lasāms no apakšas uz augšu: minimāla parakstīta sāknēšana un sysctl bāzlīnija, mazāko privilēģiju lietotāji ar SSH atslēgām un bez parolēm, default-deny ugunsmūris, procesu izolācija caur systemd smilškasti un cgroups, un audita žurnāli, kas nosūtīti ārpus hosta, lai kompromitēšana nevarētu klusi izdzēst pati savas pēdas.

Nekas no tā nav eksotisks — tā ir parastā disciplīna darbināt hostu tā, it kā tas tiktu uzbrukts, jo galu galā tā arī notiek. Tas pats instinkts, kas būvē mazu CLI atkārtotai berzei, vienreiz uzbūvē nostiprinātu bāzes attēlu un to atkārtoti izmanto visur, tā ka drošais ceļš ir arī vieglais ceļš.

  • Parakstīta sāknēšana, minimāli kodola moduļi, sysctl bāzlīnija
  • nftables default-deny, systemd smilškaste, seccomp, cgroups
  • auditd un žurnālu nosūtīšana ārpus hosta; bezuzraudzības drošības atjauninājumi
Loks

Kā steks uzkrājās, slāni pa slānim.

Neviens atsevišķs gads nemācīja to visu. Tas uzkrājās tā, kā stekam jāuzkrājas — no stikla, kuru lietotājs skar, uz leju, līdz kodols kļuva tikpat pazīstams kā skats.

  1. 1.–3. gads Natīvā mobilā, no sākuma līdz beigām Swift un Objective-C uz iOS, Kotlin un Java uz Android. Apguvu disciplīnu piegādāt ierīcēm laukā — fragmentācija, bezsaiste, akumulatora un atmiņas budžeti.
  2. 3.–6. gads Backend risinājumi aiz lietotnēm Node, Spring, Django un Rails, kas apkalpo REST un GraphQL, balstīti uz PostgreSQL un Redis. Lietotne pārstāja beigties pie API un sākās pie kodola.
  3. 6.–9. gads Vairāku platformu un mākonis React Native un Flutter tur, kur koplietota koda bāze atmaksājās; konteineri, Kubernetes un vairāku mākoņu piegāde, lai infrastruktūra kļūtu reproducējama.
  4. 9.–šodien Sistēmu un sadalītā dziļums Rust un Go kodoliem, kuriem jābūt pareiziem un ātriem, notikumvirzītas un reāllaika topoloģijas, un nepārtraukta personīga Linux rīkkopa zem tā visa.

Open to the right work

Ja jums vajadzīgs viens inženieris, kas spēj saturēt visu steku — no kodola līdz mākonim — tas ir darbs, ko es daru.

If you are holding a problem that doesn't fit inside one field, that is the conversation I want.

NextDefense Telecom