Barcha loyihalarIshlamoqda

LedgerCore

Pul o'z-o'zidan paydo bo'lmaydigan va yo'qolmaydigan, hech bir o'tkazma ikki marta bajarilmaydigan PostgreSQL double-entry ledger

Bu loyiha nimani isbotlaydi

  • Parallellikda to'g'rilik: tartiblangan qator qulflari bilan 12/12 yuk testida deadlock 0 va fault 0; reversal race topilib, test bilan tuzatildi
  • Invariantlar bazada: debet = kredit qoidasi COMMIT paytida trigger bilan tekshiriladi; yuk ostida ham, zaxiradan tiklangan bazada ham imbalance 0
  • O'lchovga asoslangan optimallashtirish: balans o'qish bitta statement'ga qisqartirildi, A/B'da p95 24.9 → 13.4 ms
  • Muammoni dalil bilan topish: yuk ostidagi 10 soniyalik to'xtash Postgres va API loglarini vaqt bo'yicha solishtirib topildi, sababi log yozish bo'lib chiqdi
  • Operatsiya: SHA-tagli deploy, ~1 soniyalik rollback, har kungi zaxira va uni tiklab tekshirish
Rol
Yakka: dizayn, backend, DB, testlar, yuk testi, deploy (53 commit)
Muddat
2026.08 – 2026.10
Texnologiyalar
  • NestJS
  • TypeScript
  • PostgreSQL
  • node-pg-migrate
  • zod
  • React
  • Vite
  • Vitest
  • k6
  • Docker
  • Caddy
  • GitHub Actions

Asosiy raqamlar

Ko'rsatkichQiymatTuriDalil
Yaroqli yuk testlari (deadlock 0, fault 0)12 / 12O'lchanganDalilni ko'rish
Oddiy o'tkazma throughput (50 VU)249 req/sO'lchanganDalilni ko'rish
Oddiy o'tkazma p95228 msO'lchanganDalilni ko'rish
Issiq hisob o'tkazma p95 (40 VU → 1 hisob, 409 va deadlock 0)896 msO'lchanganDalilni ko'rish
Aralash yuk throughput (30 o'quvchi + 10 yozuvchi)884 req/sO'lchanganDalilni ko'rish
Balans o'qish p95 (qat'iy tezlik A/B, keyin)13,4 msO'lchanganDalilni ko'rish
Backup'dan tiklash (14/14 jadval, imbalance 0)0,68 sO'lchanganDalilni ko'rish
Avtomatik testlar392O'lchanganDalilni ko'rish

Hal qilinadigan muammo

Hisoblar orasidagi pul harakatini yozadigan tizimning yagona talabi — hech qachon xato qilmaslik. So'rovlar bir vaqtda kelsa ham, mijoz qayta yuborsa ham, jarayon o'rtada to'xtasa ham pul paydo bo'lmasligi, yo'qolmasligi va bitta o'tkazma ikki marta bajarilmasligi kerak.

Arxitektura

API (NestJS 10 modulli monolit) foydalanuvchi va hisoblar, depozit va yechib olish, o'tkazma, reversal, balans, tarix va reconciliation'ni bajaradi. O'sha kod bazasidagi alohida worker jarayoni outbox hodisalarini FOR UPDATE SKIP LOCKED bilan olib, HMAC imzoli webhook sifatida yuboradi. Invariantlarni PostgreSQL 16 deferred constraint trigger'lari tekshiradi, shuning uchun ilova kodidagi xato ularni chetlab o'tolmaydi. Operatsion konsol (React + Vite) hisoblar, o'tkazmalar va outbox yetkazilishini ko'rsatadi.

Ma'lumotlar modeli

Asosiy cheklovlar va indekslar

  • ledger_entries: UPDATE / DELETE 금지 (트리거), append-only
  • covering index (account_id, id) INCLUDE (direction, amount): 잔액 합계를 index-only scan으로
  • accounts: 통화마다 SYSTEM 계좌 1개 (unique index)
  • transactions.reverses_id: 원 거래 하나에 reversal 하나 (unique index)
  • idempotency_keys: 원장 엔트리와 같은 트랜잭션에 저장
  • 모든 FK ON DELETE RESTRICT: 돈과 관련된 행은 cascade로 지울 수 없음
  • outbox_events: created_at 기준 월별 파티션 + DEFAULT 파티션

Asosiy loyihaviy qarorlar

ADR-0001 Pul — BIGINT minor unit, API'da string

Vaziyat
JSON raqamlari IEEE 754, katta summalar sezdirmasdan buzilishi mumkin.
Qaror
BIGINT minor unit'da saqlanadi, bigint asosidagi Money bilan o'raladi va har bir chegarada string sifatida yuboriladi.
Evaziga
Mijoz summani string sifatida o'qishi kerak; evaziga float xatosi yo'q.

ADR-0002 Balans ustuni yo'q; balans yozuvlardan hisoblanadi

Vaziyat
Saqlangan balans tez o'qiladi, lekin yozuvlar bilan farq qilsa qaysi biri to'g'riligini aniqlab bo'lmaydi.
Qaror
Balans har doim ledger_entries yig'indisi; narxi covering index bilan cheklanadi.
Evaziga
O'qish narxi tarix bilan o'sadi; o'lchov talab qilganda snapshot qo'shiladi.

ADR-0003 ORM o'rniga raw SQL

Vaziyat
Ledger'da har bir so'rov va qulf ko'rinib turishi kerak; ORM yashiradigan narsa xavfli.
Qaror
pg driver va raw SQL, migratsiyalar node-pg-migrate bilan.
Evaziga
Tip xavfsizligini kompilyator emas, integratsion testlar ta'minlaydi.

ADR-0004 Modulli monolit

Vaziyat
Ledger va outbox'ni ajratish hamma narsa tayanadigan atomiklikni buzadi.
Qaror
accounts, ledger, idempotency, outbox modullari bitta jarayon va bitta bazada.
Evaziga
Modul boshqa modul jadvaliga to'g'ridan-to'g'ri murojaat qilmaydi.

ADR-0005 Invariantlar deferred constraint trigger'larda

Vaziyat
To'g'ri tranzaksiya qoidalari yozuvlar to'plamining xossasi, CHECK bilan ifodalab bo'lmaydi, ilova tekshiruvi esa chetlab o'tilishi mumkin.
Qaror
COMMIT paytidagi trigger'lar kamida 2 yozuv, debet = kredit, jami = tranzaksiya summasi va valyuta mosligini tekshiradi.
Evaziga
Xato statement'da emas, COMMIT'da chiqadi.

ADR-0006 READ COMMITTED + id tartibida FOR UPDATE

Vaziyat
Hisoblarni so'rov tartibida qulflash A→B va B→A o'tkazmalarida darhol deadlock beradi.
Qaror
Hisob qatorlari har doim id o'sish tartibida qulflanadi.
Evaziga
Har bir yangi yozish yo'li shu tartibga rioya qilishi shart.

ADR-0007 Faqat outbox oylik partition, ledger partition'siz

Vaziyat
transactions partition qilinsa, ikki marta qaytarishni to'sadigan unique himoya sezdirmasdan buziladi.
Qaror
created_at bo'yicha oylik partition + DEFAULT partition faqat outbox_events'ga qo'llandi.
Evaziga
Partition'larni oldindan yaratish operatsion vazifa bo'ladi.

Oldin va keyin

Oldinbalance p95 24.9 ms · 4 round tripsDalilni ko'rish
Keyinbalance p95 13.4 ms · 1 statementDalilni ko'rish
Nima o'zgartirildi
Balans endpoint'i BEGIN READ ONLY → hisobni tekshirish → SUM → COMMIT qilib, bazaga 4 marta borib-kelardi va ulanishni band qilardi (HTTP p95 141 ms, SQL'ning o'zi ~0.3 ms). READ COMMITTED'da har bir statement o'z snapshot'ini oladi, shuning uchun bu tranzaksiya hech qanday izchillik bermasdi. Pool 20 dan 40 ga oshirilganda faqat 7% tezlashdi, demak sabab pool emas edi; 200 000 yozuvli bazada EXPLAIN ANALYZE ikkala variantda ham 55 buffer va bir xil indeksni ko'rsatdi. accounts + CROSS JOIN LATERAL (SUM …) bitta statement'ga o'tkazildi (404 saqlandi) va bazaga bitta so'rov borishini tekshiradigan regressiya testi qo'shildi. Qat'iy tezlik 100 iter/s, 3 + 3 run; tarix va o'tkazma p95 farqi shovqin chegarasida.

Nosozlik tarixi

Belgi
To'liq bench'da 3 ta 500 va 3 ta 409 chiqdi, ledger esa muvozanatda qoldi.
Sabab
Postgres 04:00:52.7 da lock_timeout bergan, API esa javobni 10 soniya keyin yozgan, o'sha paytda 40 ms ichida bir guruh 500 chiqqan: Node jarayonining o'zi to'xtab qolgan. Sabab — har bir so'rovni info darajasida log qilish; Windows'da stdout pipe'ga yozish sinxron, PowerShell yo'naltirishi esa ulgurmagan.
Yechim
LOG_LEVEL=warn bilan xuddi shu stsenariyda fault 0, 409 0, throughput +37%. lock_timeout 3 s, statement_timeout 5 s va idle_in_transaction_session_timeout 10 s qulflarni bo'shatdi; 500 INTERNAL_ERROR qaytargan timeout'lar endi 503 DATABASE_TIMEOUT (retryable: true) qaytaradi (8c30dba).
Takrorlanmasligi uchun
Haqiqiy Postgres va pg-pool bilan timeout → 503 bo'yicha 3 ta test yozildi; production'da asinxron logger yoki sampling kerakligi cheklovlarga yozildi.

Test va sifat

Testlar soni
392
Qamrov
—
CI
GitHub Actions (커밋 메시지, 포맷, lint, typecheck, Postgres 서비스로 테스트, 빌드; billing 문제로 현재 중단)
Test vositalari
Vitest, supertest, 실제 PostgreSQL 16 (mock 없음), k6 + bench.ps1

Deploy va ekspluatatsiya

Deploy muhiti
iwinv VPS, Docker Compose. Faqat web (Caddy) tashqi proxy tarmog'ida turadi va /v1/* ni API'ga yo'naltiradi; Postgres va Redis portlari ochilmagan. deploy.sh: conflict belgilari tekshiruvi → git merge --ff-only → migrate, api va web image'lari SHA tegi bilan build (worker api image'ini ishlatadi) → migrate muvaffaqiyatli tugagach API va worker → /v1/health/ready 60 soniyagacha kutiladi → faqat tayyor versiya APP_TAG va .deploy-history'ga yoziladi, bo'lmasa aniq rollback buyrug'i chiqariladi.
Backup
Cron har kuni 03:00 da serverdagi barcha Postgres konteynerlarini pg_dump -Fc bilan zaxiralaydi, 7 kun saqlanadi. restore-test.sh eng so'nggi nusxani vaqtinchalik bazaga tiklab, jadvallar sonini solishtiradi; LedgerCore uchun VERIFY_SQL double-entry invariantini ham tekshiradi: 68K nusxa, 14/14 jadval, tiklash 0.68 s, 20 yozuv imbalance=0.
Rollback
rollback.sh build qilmasdan api, worker va web'ni oldingi (yoki berilgan) SHA'ga qaytaradi va health tekshiradi; Postgres va Redis'ga tegmaydi, migratsiyalar qaytarilmaydi. Production'da 2066318 → d10826f → 2066318 sinaldi, har safar ready (~1 s).
Monitoring
postgresql.prod.conf: lock_timeout 3 s, statement_timeout 5 s, idle_in_transaction_session_timeout 10 s, 500 ms dan sekin so'rovlar log qilinadi, pg_stat_statements yoqilgan, DB PG Insight'ga monitoring tarmog'i orqali ulangan. /v1/health/ready

Skrinshotlar

Cheklovlar va xulosalar

Hozirgi cheklovlar

  • Barcha raqamlar bitta noutbukdan (i7-8565U, 4C/8T, 15 W): k6, API va Postgres birga ishlagan; production quvvatini ko'rsatmaydi, faqat taqqoslashlar ishonchli
  • Oddiy o'tkazma p95 228 ms, skriptdagi 150 ms maqsadiga yetmadi
  • Balans narxi tarix bilan o'sadi (~240 yozuvda 0.42 ms, ~850 da 1.68 ms); keyingi qadam — yozuvlardan qayta quriladigan snapshot jadvali
  • assert_transaction_balanced har o'tkazmada 3 marta (har qator uchun) ishlaydi; baseline'da DB vaqtining ikkinchi eng katta qismi: 42 421 o'tkazmada 104 s
  • Har so'rovni sinxron log qilish yuk ostida Node'ni to'xtatishi mumkin; production'da asinxron logger yoki sampling kerak
  • Bitta Postgres primary: yozuvlar gorizontal kengaymaydi, replika va failover yo'q
  • Redis compose'da bor, lekin ishlatilmaydi; outbox navbati Postgres'da (SKIP LOCKED)
  • GitHub Actions billing sababli to'xtagan, testlar lokal va qo'lda yuritildi

Asosiy kod

Hozirgi holat va keyingi qadamlar

Yakuniy kodda 12/12 yuk testi yaroqli chiqdi: fault 0, deadlock 0, contention 0, har bir rundan keyin ledger muvozanatda. Balans o'qishni bitta statement'ga qisqartirib, qat'iy tezlikdagi A/B'da p95 24.9 ms dan 13.4 ms ga tushirildi. Raqamlarga ishonishdan oldin yana quyidagilar topilib tuzatildi: ikki parallel reversal'dan ikkinchisi 409 o'rniga 422 qaytargan race (ccee39b), .env.test gitignore'da bo'lgani uchun toza klonda testlar umuman ishlamagani (e0e6884), k6 skriptlaridagi __ITER va teardown xatolari (0f76291).