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ā.
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.
gadi, piegādājot mobilās lietotnes reāliem lietotājiem
mobilās platformas produkcijā: iOS, Android, Windows
mākoņi, ekspluatēti bez piesaistes piegādātājam: GCP, Azure, AWS
galvenās valodas, apgūtas dziļumā: Rust, Go, Python, TypeScript
Steks, izvēlēts pēc slāņa, nevis pēc modes.
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
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
} 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
Vienpadsmit gadi piegādes reālām ierīcēm, nevis simulatoriem.
iOS es strādāju Swift un Objective-C ar SwiftUI jaunajām virsmām, Core Data noturībai, Core Animation kustībai un SiriKit balss nodomiem. Android tas ir Kotlin un Java pret Android SDK, ar versiju fragmentācijas disciplīnu, kas nāk no daudzu OS versiju atbalstīšanas laukā.
Vairāku platformu gadījumā es ķeros pie React Native (augsts līmenis) vai Flutter (augsts līmenis), kad koplietota koda bāze atmaksājas, un esmu piegādājis ar Xamarin darba līmenī. Aiz lietotnēm atrodas reāli backend risinājumi — Node/Express, Spring Boot, Django/Flask, Ruby on Rails vai Go ar Gin un Gorm — eksponējot REST un GraphQL.
- iOS — Swift, Objective-C, SwiftUI, Core Data, Core Animation, SiriKit
- Android — Kotlin, Java, Android SDK daudzās OS versijās
- Vairāku platformu — React Native, Flutter (augsts līmenis), Xamarin (darba līmenis)
- Dati — SQLite, Redis, ORM (Sequelize, Mongoose); REST un GraphQL API
Pārnesams pēc dizaina, nav apprecējies ar vienu piegādātāju.
Es saglabāju slodzes pārnesamas, lai mākonis paliktu prece, nevis atkarība. GCP es izmantoju Vertex AI, Cloud Run, Cloud Functions, GKE, Pub/Sub, BigQuery un Firestore. Azure — AKS, Functions, Cognitive Services un Container Apps. AWS — Lambda, ECS/EKS, S3, RDS un SQS/SNS aiz CloudFront.
Edge es darbinu Cloudflare Workers un Vercel Edge; ātri mainīgiem produktiem es paļaujos uz Supabase vai Firebase kā backend. Viss tiek piegādāts caur konteineriem — Docker un Kubernetes — ar CI/CD uz GitHub Actions vai GitLab CI un infrastruktūru, kas definēta kā kods, lai vide būtu reproducējama, nevis būvēta ar roku.
- GCP — Vertex AI, Cloud Run, Cloud Functions, GKE, Pub/Sub, BigQuery, Firestore
- Azure — AKS, Functions, Cognitive Services, Container Apps
- AWS — Lambda, ECS/EKS, S3, RDS, SQS/SNS, CloudFront
- Edge un BaaS — Cloudflare Workers, Vercel Edge, Supabase, Firebase
Sadalītas sistēmas, kas paliek pareizas, kamēr lietas kustas.
Backend darbs ir tur, kur atmaksājas sistēmu domāšana. Es projektēju ap ziņojumu rindām un notikumvirzītu arhitektūru, lai komponenti paliktu atsaistīti, un ap reāllaika sistēmām, kur WebSocket un soketa līmeņa komunikācija nes stāvokli starp klientu un serveri ar zemu latentumu.
Pakalpojums-uz-pakalpojumu protokoli, idempotence un pretspiediens ir pirmās šķiras rūpes, nevis vēlākas pārdomas. Mērķis ir topoloģija, kas eleganti degradējas — vienam pakalpojumam atsakot, sistēmai jāpalēninās, nevis jāapstājas.
- Ziņojumu rindas un notikumvirzīta arhitektūra
- Reāllaika sistēmas pa WebSocket un soketa līmeņa komunikāciju
- Pakalpojums-uz-pakalpojumu protokoli, idempotence, pretspiediens
- Sadalīta koordinācija ar elegantu degradāciju
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.
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.
Azure
AKS, Functions, Cognitive Services, Container Apps — izmantots tur, kur organizācija jau dzīvo Microsoft vidē.
AWS
Lambda, ECS/EKS, S3, RDS, SQS/SNS aiz CloudFront — primitīvu plašums tradicionālajiem produkcijas stekiem.
Edge
Cloudflare Workers un Vercel Edge — loģika, kas darbojas tuvu lietotājam, latentuma un globālā tvēruma dēļ.
BaaS
Supabase un Firebase (Auth, Firestore, Realtime DB) — kad produktam vajadzīgs backend dienās, nevis nedēļās.
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.
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
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.
Atsaistīts ar ziņojumu, nevis ar cerību.
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
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
- 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.
- 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.
- 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.
- 04 Staging Izvietot parakstīto attēlu staging vidē, kas atspoguļo produkciju, palaist smoke testus un īsu soak.
- 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ā.
Stāvoklis, kas nests pa soketu, ar apstrādātiem kļūmes režīmiem.
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
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
} 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;
},
}; 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.
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.
BigQuery
Analītisks mērogs. Kolonnu glabāšana un sadalīšana jautājumiem, kas skenē miljardus rindu, bet darbojas reti.
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.
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.
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.
Migrācijas
Shēma kā versionētas, atgriežamas migrācijas repozitorijā, lai datubāze būtu reproducējama, nevis arheoloģiska.
Kodols, uz kura darbojas konteiners, uzskatīts par produkta daļu.
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
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.–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.
- 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.
- 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.
- 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.