Skip to content
Seni Architecture Sistem yang 'Sempurna': Kenapa Simple Itu Susah

Seni Architecture Sistem yang 'Sempurna': Kenapa Simple Itu Susah

Seni Architecture Sistem yang "Sempurna": Kenapa Simple Itu Susah

Setiap sistem akan sampai pada satu tahap di mana ia nampak hampir sempurna. Pasukan sudah ada diagram yang cantik, semua komponen nampak tersusun, dan semua orang dalam architecture review mengangguk setuju.

Kemudian production datang.

Dua tahun kemudian, diagram itu biasanya sudah tidak lagi menggambarkan sistem sebenar.

Sepanjang bekerja dengan sistem yang semakin besar, saya lebih banyak belajar daripada architecture yang gagal berbanding architecture yang berjaya. Daripada incident, panggilan 2 a.m. dan postmortem yang panjang, satu perkara sentiasa muncul: simplicity bukan bermaksud kurang usaha. Ia hasil daripada banyak perkara yang sengaja dibuang.

Masalahnya, membuang sesuatu jauh lebih susah daripada menambah sesuatu.

Godaan complexity

Masuk ke mana-mana architecture review dan bandingkan dua diagram: satu mempunyai sepuluh kotak microservice, satu lagi hanya mempunyai satu kotak bertulis app.

Selalunya diagram dengan sepuluh kotak nampak lebih "serius".

Itulah perangkapnya.

Complexity mudah disalah anggap sebagai rigor. Lebih banyak kotak bermaksud lebih banyak abstraction, lebih banyak layer dan lebih banyak istilah teknikal untuk diterangkan. Diagram nampak lebih canggih, tetapi itu tidak semestinya bermaksud sistem lebih baik.

Dijkstra sudah menyedari perkara ini sejak 1984:

"Simplicity is a great virtue but it requires hard work to achieve it and education to appreciate it. And to make matters worse: complexity sells better."

InfoQ turut membincangkan perkara yang sama: architecture yang simple biasanya lebih mudah dikomunikasikan, dibina, di-deploy, dikendalikan dan dikembangkan.

Namun begitu, kita masih cenderung menambah kotak.

Sebabnya mudah: simple tidak sama dengan mudah.

Reka bentuk yang nampak simple selalunya terhasil selepas banyak keputusan, eksperimen dan perkara yang sengaja tidak dimasukkan.

Bab 1 — Ilusi kesempurnaan

Architecture yang sempurna sebenarnya tidak wujud. Apa yang wujud ialah sekumpulan trade-off yang sesuai dengan masalah, pasukan dan keadaan pada masa tertentu.

Masalahnya, keputusan itu jarang kekal selama-lamanya.

Dua corak kesilapan biasanya muncul sebelum sistem menjadi terlalu kompleks.

1. The Golden Hammer.

Anda ada Kubernetes, jadi setiap masalah nampak seperti masalah yang memerlukan Kubernetes.

Anda ada Kafka, jadi setiap aliran data nampak seperti sesuatu yang patut menjadi stream.

Akhirnya, tool bertukar menjadi kepercayaan. Bila itu berlaku, dependency paling susah untuk dibuang bukan lagi library atau service, tetapi andaian bahawa tool tersebut memang diperlukan.

Masalahnya bukan Kubernetes atau Kafka. Masalahnya ialah menggunakan sesuatu kerana kita sudah memilikinya, bukan kerana masalah itu benar-benar memerlukannya.

2. Premature abstraction.

Kita membuat abstraction sebelum kita benar-benar memahami corak yang hendak diabstrakkan.

Hasilnya biasanya ialah terlalu banyak indirection. Satu POST /orders yang sepatutnya mudah dijejak boleh berakhir dengan sebelas fail, empat interface dan beberapa layer sebelum request itu sampai kepada database.

Abstraction memang berguna. Tetapi abstraction yang dibuat terlalu awal selalunya berdasarkan andaian, bukan pengalaman.

Fred Brooks menunjukkan betapa pentingnya abstraction dalam evolusi perisian, tetapi abstraction hanya membantu apabila ia berada di tempat yang betul.

Lakaran pertama sesuatu abstraction biasanya masih merupakan tekaan.

Cukai abstraksi

Setiap layer yang ditambah mempunyai kos yang tidak kelihatan dalam diagram.

Kos itu muncul apabila anda perlu debug sistem.

Bayangkan satu request yang melalui sistem berikut:

MONOLITH TRACE                MICROSERVICE TRACE
─────────────────             ─────────────────────────────
1. app/router → service       1.  client
2. service → db               2.    → api-gateway (auth, TLS)
                              3.      → service-a (discovery)
Then you debug:               4.        → service-b (HTTP + retry)
  a. logic bug                5.          → service-c (event bus)
  b. db query                 6.            → db (per-service)
                              7.        ← 404 (schema drift)
Then you debug:
  a. logic bug      b. db query      c. which hop failed?
  d. timeout? retry? e. ordering?     f. schema drift? g. idempotency?

Dalam monolith, mungkin anda perlu mencari bug pada application logic atau database query.

Dalam sistem teragih, anda juga perlu bertanya:

  • Request gagal di service mana?
  • Adakah network hop tertentu timeout?
  • Adakah retry berlaku?
  • Adakah request dihantar dua kali?
  • Adakah schema antara dua service sudah tidak sepadan?
  • Adakah event sampai?
  • Siapa sebenarnya yang memiliki data tersebut?

Semua ini ialah complexity sebenar.

Ia tidak semestinya buruk. Distributed system memang diperlukan untuk masalah tertentu. Tetapi complexity itu perlu dibayar oleh sesuatu.

Kalau pengguna tidak mendapat manfaat daripadanya, kita perlu bertanya sama ada kos tersebut berbaloi.

Bab 2 — Cukai tersembunyi over-engineering

Over-engineering bukan sekadar mempunyai terlalu banyak kod.

Kosnya biasanya muncul dalam beberapa bentuk yang lebih sukar dilihat.

1. Latency yang tidak akan anda invois

Setiap network hop mempunyai kos.

Setiap service tambahan boleh membawa TLS, serialization, retry dan kemungkinan timeout. Satu request yang sebelum ini hanya memanggil database mungkin kini perlu melalui beberapa service terlebih dahulu.

Katakan monolith anda melayan request dalam 40ms, tetapi selepas dipecahkan kepada beberapa microservice request yang sama mengambil 400ms.

Anda mungkin mendapat isolation dan scalability yang lebih baik.

Tetapi anda juga mungkin baru sahaja menjadikan produk itu sepuluh kali lebih perlahan untuk pengguna.

Cloud tidak mengubah hukum fizik. Network tetap mempunyai latency walaupun diagram anda nampak hebat.

2. Beban operasi sebagai langganan

Ini ialah kos yang mudah terlepas pandang kerana ia tidak datang dalam satu bil.

Ia datang sedikit demi sedikit.

Sistem dengan tiga service mungkin memerlukan:

  • tiga deployment pipeline
  • tiga format log
  • tiga tempat menyimpan secrets
  • tiga health check
  • tiga set alert
  • tiga jadual upgrade
  • lebih banyak monitoring
  • lebih banyak tempat untuk mencari punca bug

MIT Technology Review memetik Thoughtworks tentang masalah ini: sesuatu penyelesaian seperti microservices tidak semestinya menghapuskan complexity. Ia sering hanya memindahkan complexity dari satu tempat ke tempat lain.

Microservices boleh menyelesaikan masalah deployment dan scaling.

Sementara itu, ia boleh mencipta masalah operasi yang sebelum itu tidak wujud.

Itu bukan kegagalan microservices. Itu memang trade-off yang datang bersamanya.

3. Beban kognitif pada pasukan

Resource paling terhad dalam sesebuah pasukan bukan CPU atau storage.

Ia ialah perhatian manusia.

Steve McConnell pernah menggambarkan programming sebagai usaha untuk mengatasi saiz memori manusia yang sangat terhad.

Setiap modul, if, service dan network hop menambah sesuatu yang perlu difahami.

Bayangkan seorang on-call engineer perlu memahami tujuh service pada pukul 3 pagi hanya untuk mencari punca satu request yang gagal.

Masalahnya bukan kerana engineer itu tidak cukup pandai.

Sistem itu sendiri mungkin terlalu mahal untuk difahami.

Bila kotak itu menipu: distributed monolith

Ini antara keadaan yang paling menyakitkan.

Pasukan memecahkan sistem kepada beberapa service untuk mendapatkan agility, tetapi masih berkongsi database, masih bergantung pada synchronous calls dan masih memerlukan release yang diselaraskan.

Akhirnya mereka tidak mendapat kelebihan penuh microservices.

Mereka hanya mendapat monolith yang mempunyai latency network dan failure mode tambahan.

Itulah distributed monolith.

Petunjuk yang mudah:

Kalau service A tidak boleh di-deploy tanpa service B kerana kedua-duanya perlu release bersama, sempadan antara kedua-duanya mungkin belum benar-benar wujud.

Pada ketika itu, fallacies of distributed computing bukan lagi teori. Ia sudah menjadi sebahagian daripada masalah production anda.

Bab 3 — Rangka kerja untuk simplicity

Simplicity bukan sekadar "vibe".

Ia perlu menjadi sebahagian daripada cara kita membuat keputusan.

Empat prinsip berikut cukup untuk mengelakkan banyak complexity yang tidak perlu.

1. YAGNI sebagai disiplin, bukan slogan

"You Ain't Gonna Need It" biasanya digunakan untuk feature.

Tetapi prinsip yang sama sangat berguna pada peringkat architecture.

Sebelum menambah satu sistem, tanya:

Masalah apa yang pengguna hadapi hari ini sehingga kita memerlukan benda ini?

Kalau jawapannya ialah "mungkin kita perlukan nanti", jangan bina sekarang.

Kos infrastructure bukan sekadar masa untuk setup.

Sebaik sahaja ia wujud, ia menjadi sebahagian daripada sistem yang perlu diselenggara. Engineer baharu perlu memahaminya. Pipeline perlu menyokongnya. Monitoring perlu memantaunya.

Sesuatu yang dibina "untuk masa depan" boleh menjadi hutang teknikal sebelum masa depan itu pernah tiba.

2. The Rule of Three

Jangan buat abstraction hanya kerana sesuatu muncul sekali.

Kali pertama, buat secara konkrit.

Kali kedua, perhatikan persamaan tetapi jangan terlalu cepat membuat abstraction.

Kali ketiga, anda sudah mempunyai bukti tentang corak tersebut.

Barulah abstraction mula mempunyai asas yang lebih kukuh.

The Rule of Three mengubah abstraction daripada tekaan kepada keputusan berdasarkan pengalaman.

3. Bajet complexity

Anggap complexity sebagai bajet.

Setiap kotak baharu dalam architecture diagram perlu memberikan sesuatu sebagai balasan.

Contohnya:

Cache ini mengurangkan satu DB round-trip.

Bagus.

Tetapi cache itu juga menambah:

  • invalidation
  • stale data
  • memory usage
  • monitoring
  • satu lagi sumber data yang perlu difahami

Jadi soalan sebenar bukan "boleh kita tambah cache?"

Soalannya ialah:

Adakah manfaat cache lebih besar daripada complexity yang ditambahkannya?

Dokumentasikan keputusan tersebut dalam ADR (Architecture Decision Record).

Dengan cara itu, engineer pada masa depan boleh faham mengapa sesuatu tidak dibina, bukan sekadar melihat sistem yang nampak simple dan menganggap ia kebetulan.

4. Bolehkah pilihan yang membosankan menyelesaikan masalah?

Ini antara soalan yang paling berguna dalam architecture review.

Kalau satu SQL query tambahan boleh menyelesaikan masalah, adakah kita benar-benar memerlukan service baharu?

Kalau satu HTTP call sudah mencukupi, adakah kita memerlukan event bus?

Kalau satu monolith masih boleh di-scale, adakah kita benar-benar memerlukan microservices?

Pilihan yang membosankan mempunyai satu kelebihan besar: kita sudah tahu bagaimana ia gagal.

Anda mahu complexity yang eksotik hanya berada di tempat yang benar-benar memerlukannya.

Bukan kerana ia nampak menarik dalam diagram.

Keseluruhan idea ini boleh diringkaskan seperti berikut:

           SIMPLE SYSTEM                           OVER-ENGINEERED SYSTEM
   ┌───────────────────────────┐          ┌───────────────────────────────┐
   │   app  →  db              │          │  client → gateway → service-a │
   │   one codebase            │          │  service-a → service-b (bus)  │
   │   one deploy              │          │  service-b → service-c → db   │
   │   one way to fail         │          │  three schemas, two queues,   │
   │   boring, fast, debuggable│          │  one event bus nobody owns    │
   └───────────────────────────┘          └───────────────────────────────┘
   failure modes: 2                       failure modes: 7+
   cognitive load: low                    cognitive load: high
   changes: touch one box                 changes: touch four PRs + release
   latency: one hop                       latency: three hops + retries

Bab 4 — Monolith vs microservices vs modular monolith

Ketiga-tiga pendekatan ini bukan agama.

Ia hanyalah pilihan dengan trade-off dan failure mode yang berbeza.

MonolithModular monolithMicroservices
LatencyPaling rendahRendahTinggi (network hops)
DeployabilitySatu unitSatu unit, seam yang jelasBebas
Peningkatan pasukanSusahSederhanaTerbaik
Beban operasiPaling rendahRendahPaling tinggi
Risiko refactorTinggi (menyentuh banyak bahagian)Rendah (dalam modul)Rendah dalam service, tinggi merentas service
Distributed complexityTiadaTiadaTinggi
Sesuai bilaPasukan kecil, satu domainPasukan membesar, domain jelasOrganisasi besar, banyak pasukan, scaling kompleks

Pandangan yang semakin popular dalam software engineering ialah modular monolith sering menjadi pilihan yang baik sebagai titik permulaan.

Contohnya, Spring Modulith menunjukkan bagaimana struktur modular boleh diwujudkan dalam satu application tanpa terus memecahkannya kepada distributed services.

Kelebihannya mudah difahami.

Anda boleh mempunyai sempadan yang jelas antara module tanpa terus membayar kos distributed system.

Kemudian, jika satu module benar-benar memerlukan scaling atau deployment secara berasingan, anda sudah mempunyai seam yang lebih jelas untuk memisahkannya.

Sebab itu saya lebih suka soalan berikut berbanding "monolith atau microservices?":

Apa masalah yang kita cuba selesaikan dengan memecahkan sistem ini?

Kalau jawapannya tidak jelas, jangan pecahkan sistem lagi.

Kesimpulan — keanggunan ialah membuang perkara yang tidak perlu

Architecture yang "sempurna" bukanlah architecture yang mempunyai paling banyak kotak.

Ia ialah architecture di mana setiap kotak yang masih ada memang mempunyai sebab untuk berada di situ.

Complexity yang tinggal sepatutnya datang daripada masalah sebenar yang perlu diselesaikan, bukan daripada trend, ketakutan atau keinginan untuk menghasilkan diagram yang nampak canggih.

Antoine de Saint-Exupéry sering dikaitkan dengan idea bahawa kesempurnaan tercapai bukan apabila tiada lagi yang boleh ditambah, tetapi apabila tiada lagi yang boleh dibuang.

Prinsip yang sama sesuai untuk architecture.

Simple itu susah kerana menambah sesuatu biasanya lebih mudah daripada membuangnya.

Lebih mudah untuk berkata:

"Kita mungkin perlukan ini nanti."

daripada berkata:

"Kita belum ada masalah yang memerlukan ini."

Tetapi keputusan kedua itulah yang menjaga sistem daripada menjadi semakin berat.

Jadi pada architecture review anda yang seterusnya, cuba tanya satu soalan mudah:

"Kalau kita buang benda ini, apa yang sebenarnya akan rosak?"

Kalau tiada jawapan yang jelas, mungkin benda itu memang tidak perlu ada.

Untuk bacaan seterusnya, prinsip yang sama boleh digunakan pada alat agentic yang kini mula digunakan dalam scientific computing — keep the architecture simple, and keep the humans in the loop.

Kekalkan sistem simple selagi simple masih menyelesaikan masalah.

Dan apabila complexity benar-benar diperlukan, pastikan anda tahu kenapa.

Pertama kali diterbitkan di hejes.my. Kalau anda ada pendapat tentang monolith, microservices atau modular monolith, bahagian komen terbuka.

// penulis

Gaara

Chief Operator

Gaara is the human operator behind hejes.my. He runs the briefing pipeline, curates the AI drafts, and presses the publish button.

// sertai suapan

satu ilmu seminggu. tiada spam.