LiveCommerce
Jonli efir paytidagi cheklangan flash sale platformasi: 100 dona uchun 1 000 xarid urinishi, ortiqcha sotuv 0
Bu loyiha nimani isbotlaydi
- To'g'rilik: 100 dona uchun 1 000 urinish, 3 run — 0 ortiqcha sotuv, to'lov va sotilgan dona nomuvofiqligi 0, HTTP xato 0; har run oxirida 5 ta invariant tekshiriladi
- Nosozlikka chidamlilik: Kafka yoki to'lov bazasi haqiqatan to'xtatilganda ham 0 ta event yo'qolmaydi, 0 ta ikki marta pul yechilmaydi (chaos testlari)
- O'lchab tezlashtirish: sotuv tugashi 1 daq 27 s → ~23 s, buyurtma yaratish p95 5.72 s → 2.68 s; connection pool gipotezasi o'lchov bilan rad etildi
- Production'dagi biznes xavfini dalil bilan yopish: bitta hisob to'lanmagan buyurtmalar bilan butun zaxirani band qila olardi; partial unique index bilan yopildi (15 → 0 o'rniga 15 → 5)
- Chegara himoyasi: login, signup va buyurtma uchun rate limit; mijoz IP'si ishonchli proxy zanjiridan olinadi, soxta X-Forwarded-For 429'dan qutqarmaydi (production'da tekshirildi)
- Operatsiya: SHA-tagli deploy, health'ga bog'langan rollback, production'da ikki yo'nalishli mashq, restartdan keyin ham token yaroqli qoladigan JWT kaliti
- Rol
- Yakka: arxitektura, 7 ta servis, frontend, testlar, yuk testi, deploy va operatsiya (127 commit)
- Muddat
- 2026.09 – 2026.10
- Texnologiyalar
- Kotlin
- Spring Boot
- Spring Cloud Gateway
- Spring Data JPA
- PostgreSQL
- Apache Kafka
- WebSocket (STOMP)
- Flyway
- Toss Payments
- OpenTelemetry
- Jaeger
- Next.js
- TypeScript
- Tailwind CSS
- JUnit 5
- k6
- Docker
- Caddy
Asosiy raqamlar
| Ko'rsatkich | Qiymat | Turi | Dalil |
|---|---|---|---|
| Ortiqcha sotuv (100 dona, 1 000 urinish, 3 run) | 0 | O'lchangan | Dalilni ko'rish |
| Sotilgan = pul yechilgan | 100 / 100 | O'lchangan | Dalilni ko'rish |
| Buyurtma yaratish p95 | 2,68 s | O'lchangan | Dalilni ko'rish |
| To'lov yuborish p95 | 0,63 s | O'lchangan | Dalilni ko'rish |
| Sotuv tugash vaqti | 23 s | O'lchangan | Dalilni ko'rish |
| Zaxira yangilanishi yetib borishi (500 tomoshabin) | 99,2 % | O'lchangan | Dalilni ko'rish |
| Tomoshabinlar soni xabarlari kamayishi | 98 % | O'lchangan | Dalilni ko'rish |
| Chaos testlari: yo'qolgan event / ikki marta to'lov | 0 | O'lchangan | Dalilni ko'rish |
Hal qilinadigan muammo
Jonli efir paytidagi cheklangan sotuvda buyurtmalar bir necha soniyaga to'planadi, zaxira, to'lov va yetkazish esa turli servislarda. Flash sale'da "sotib bo'lindi" odatiy holat, shuning uchun avval pul olinsa har biri refund va PG komissiyasiga aylanadi; bitta servis to'xtasa event yo'qolishi yoki ikki marta pul yechilishi mumkin.
Arxitektura
api-gateway (Spring Cloud Gateway) JWT'ni tekshiradi, identity sarlavhalarini qayta yozadi, rate limit va WebSocket proxy'ni bajaradi. order-service'dagi Saga orkestratori avval zaxirani 10 daqiqaga band qiladi, keyin to'lov oladi (AWAITING_STOCK → AWAITING_PAYMENT → PAYMENT_CONFIRMING → CONFIRMING_STOCK → COMPLETED, to'lov oynasi 5 daqiqa). inventory (band qilish, tasdiqlash, qaytarish, muddati o'tganlarni tozalash), payment (Toss tasdiqlash va bekor qilish, Idempotency-Key), live (STOMP chat, real-time zaxira, tomoshabinlar soni), auth (RS256 JWT, JWKS, refresh token rotatsiyasi) va delivery (yetkazish, HMAC kuryer webhook'lari) har biri o'z PostgreSQL bazasiga ega va bir-birining ma'lumotini faqat API yoki Kafka event orqali o'qiydi. Event'lar har bir servisning transactional outbox'idan traceparent bilan chiqadi, shuning uchun Kafka orqali o'tsa ham butun saga bitta Jaeger trace.
Ma'lumotlar modeli
Asosiy cheklovlar va indekslar
- database per service: PostgreSQL 5개, 서비스 간 FK 없음, order_id / member_id UUID와 Kafka 이벤트로만 연결 (Flyway 마이그레이션 21개)
- orders.uq_orders_one_open_per_member_product: 열린 상태 partial UNIQUE (V5)
- orders.version (@Version), 인덱스 (status, updated_at), partial payment_deadline WHERE status='AWAITING_PAYMENT'
- product_stock.quantity_available CHECK (>= 0): 코드가 틀려도 DB가 초과 판매를 막음, unit_price는 서버 측
- inventory_reservations.order_id UNIQUE: 중복 이벤트가 재고를 두 번 줄이지 않음, partial index expires_at WHERE status='HELD'
- payments.order_id UNIQUE: 주문 하나에 결제 하나
- tracking_events.event_id UNIQUE: 택배 웹훅 재전송도 한 번만 적용
- refresh_tokens.token_hash UNIQUE, family_id 인덱스: 재사용된 토큰은 가족 전체를 폐기
- outbox_events.trace_parent (W3C), partial index created_at WHERE status='PENDING'
Asosiy loyihaviy qarorlar
Avval zaxira, keyin to'lov (hold-then-pay)
- Vaziyat
- Flash sale'da "sotib bo'lindi" odatiy holat; avval pul olinsa har biri refund va PG komissiyasiga aylanadi.
- Qaror
- Zaxira 10 daqiqaga band qilinadi, 5 daqiqalik to'lov oynasi ochiladi, to'lov tasdiqlangach hold tasdiqlanadi.
- Evaziga
- Zaxira 10 daqiqa band turadi, shuning uchun bir xaridorga bitta ochiq buyurtma qoidasi kerak bo'ldi.
Choreography emas, orkestratsiya
- Vaziyat
- Holat mashinasi, tiklash va admin'dan qayta urinish bitta joyda bo'lishi kerak.
- Qaror
- order-service saga'ni boshqaradi, qotib qolgan qadamlarni tiklash scheduler'i har 5 s da qayta yuritadi.
- Evaziga
- order-service markaziy bo'g'in bo'ladi.
Transactional outbox, broker tasdiqlagandan keyin PUBLISHED
- Vaziyat
- Kafka to'xtasa ham event yo'qolmasin.
- Qaror
- Holat o'zgarishi va event bitta tranzaksiyada yoziladi; relay parallel yuboradi va faqat ketma-ket tasdiqlangan prefiksni PUBLISHED qiladi.
- Evaziga
- Dublikat bo'lishi mumkin — iste'molchilar idempotent qilingan.
Toss chaqiruvi tranzaksiyadan tashqarida, natija outbox bilan bitta tranzaksiyada
- Vaziyat
- Tashqi API sekinlashsa DB ulanishi ushlanib qolmasin.
- Qaror
- TransactionTemplate bilan Toss tasdig'i tashqarida chaqiriladi, natija va outbox event bitta tranzaksiyada yoziladi.
- Evaziga
- Natija yozilmay qolsa — tiklash scheduler'i Idempotency-Key bilan qayta urinadi.
Lock'siz skalyar o'qish bilan "sotib bo'lindi"ni oldindan rad etish
- Vaziyat
- Sotuv tugagach ham hamma FOR UPDATE navbatida kutardi (p95 5.8 s).
- Qaror
- Qulfdan oldin bitta son o'qiladi va 0 bo'lsa rad etiladi. Entity o'qilmaydi — aks holda Hibernate keyingi FOR UPDATE'da eski keshlangan nusxani qaytarib, ortiqcha sotuvga yo'l ochardi.
- Evaziga
- Zaxira qaytarilayotgan lahzada xaridor "sotib bo'lindi" javobini olishi mumkin; ortiqcha sotuv baribir imkonsiz.
Bitta ochiq buyurtma — partial unique index
- Vaziyat
- Bir hisob butun zaxirani to'lamasdan band qilardi; parallel so'rovlarda ham bazada kafolat kerak edi.
- Qaror
- Ochiq statuslar uchun (member_id, product_id) partial unique index (V5); buzilsa 409 OPEN_ORDER_EXISTS va mavjud orderId qaytadi.
- Evaziga
- Xaridor yangi buyurtmadan oldin eskisini to'lashi yoki bekor qilishi kerak; UI uni mavjud buyurtmaga qaytaradi.
Rate limit gateway xotirasida, buyurtmalar foydalanuvchi bo'yicha
- Vaziyat
- Redis'siz oddiy yechim; Koreyada ko'p xaridor bitta CGNAT IP'dan chiqadi.
- Qaror
- Login 10/daq, signup 5/soat IP bo'yicha; buyurtma 20/daq foydalanuvchi bo'yicha; IP XFF zanjiridagi eng o'ngdagi public manzildan olinadi.
- Evaziga
- Gateway bir nusxa bo'lsagina to'g'ri; login cheklovi ko'p IP'dan bitta hisobga hujumni to'xtatmaydi.
JWT kaliti .env'da, kid — RFC 7638 thumbprint
- Vaziyat
- Restart va deploy'dan keyin token yaroqli qolsin, gateway JWKS keshi bilan mos kelsin.
- Qaror
- JWT_SIGNING_KEY serverdagi .env'dan (chmod 600) yuklanadi, bir xil kalit doim bir xil kid beradi; kalit berilmasa compose start bo'lmaydi.
- Evaziga
- Kalit almashtirilganda eski tokenlar qisqa vaqt rad etiladi.
Oldin va keyin
- Nima o'zgartirildi
- Topilgan 4 sabab tuzatildi: sotuv tugagach ham FOR UPDATE navbati → qulfdan oldin skalyar o'qish; har topic'da 1 partition va 1 consumer (zaxira band qilish p95 17.8 s) → 6 partition va 6 thread (kalit orderId); outbox relay 500 ms da bittalab tasdiq kutib yuborishi → 100 ms, parallel batch; 500 tomoshabinga har kirishda son yuborish (O(n²)) → faqat o'zgarganda, soniyasiga ko'pi bilan bir marta. To'lov yuborish p95 0.71 → 0.63 s, zaxira band qilinguncha p95 17.77 → 4.80 s, yakunlanguncha p95 33.55 → 11.04 s, tomoshabinlar soni xabarlari 454 583 → 8 151. "Oldin" kechikishlari kesilgan: k6 sekin buyurtmalarni 15–20 s da tashlab ketgan, shuning uchun haqiqiy farq kattaroq.
Nosozlik tarixi
- Belgi
- 2026-10-09 production'da lc-hoard-check.sh bitta test akkaunt bilan ketma-ket to'lanmagan buyurtmalar yaratdi: 2×201, zaxira 15 → 0. Ya'ni bitta foydalanuvchi 10 daqiqalik hold davomida jonli efirdagi barcha mahsulotni boshqalardan yopib qo'ya olardi.
- Sabab
- Saga har bir POST /api/orders uchun alohida hold ochardi, "bitta xaridor — bitta ochiq buyurtma" qoidasi hech qayerda yo'q edi.
- Yechim
- V5 migratsiya — ochiq statuslar (AWAITING_STOCK, AWAITING_PAYMENT, PAYMENT_CONFIRMING, CONFIRMING_STOCK) uchun (member_id, product_id) partial unique index. Controller oldindan tekshiradi, poygada esa DataIntegrityViolationException index nomi bo'yicha ajratilib, 409 OPEN_ORDER_EXISTS va mavjud orderId qaytadi; frontend shu buyurtmani davom ettiradi.
- Takrorlanmasligi uchun
- Lokal Postgres'da ikki parallel tranzaksiyadan ikkinchisi 1.5 s kutib rad etildi; production'da qayta tekshiruv: 1×201 + 409, zaxira 5 da qoldi. 10 ta parallel so'rovdan aynan bittasi 201 ekanini tekshiruvchi integration test qo'shildi; k6 flash-sale.js endi har bir VU'ga o'z foydalanuvchisini beradi (USERS ≥ VUS tekshiriladi).
Test va sifat
- Testlar soni
- 46
- Qamrov
- —
- CI
- —
- Test vositalari
- JUnit 5 (통합 24, 카오스 3, gateway 단위 14, SigningKeys 5), 실행 중인 전체 스택에 실제 HTTP, k6 3종
Deploy va ekspluatatsiya
- Deploy muhiti
- iwinv VPS'da docker-compose.prod.yml: 16 konteyner (7 Spring servis, Next.js, 5 PostgreSQL, Kafka KRaft, Jaeger, Caddy edge). Hech bir konteyner port publish qilmaydi; faqat edge tashqi proxy tarmog'iga ulangan, /api/* va /ws/* ni gateway'ga, qolganini frontend'ga yuboradi. JVM'lar USER app (root emas), har servisga xotira chegarasi (Spring 512 MB, PostgreSQL 256 MB, Kafka 768 MB). PAYMENTS_FAKE_ENABLED=false. deploy.sh: conflict-marker tekshiruvi → git pull --ff-only → SHA tegli 8 ta image build → up -d → har servisning /actuator/health va frontend edge ichidan 60×3 s tekshiriladi → muvaffaqiyatda APP_TAG va .deploy-history. 2026-10-10 o'lchovi: deploy 2 daq 38 s (build 116 s), uzilish ~27 s.
- Backup
- Server umumiy cron backup'i (har kuni 03:00, 7 kun) log'ida livecommerce'ning 5 ta PostgreSQL bazasi dump qilingani ko'rinadi; tiklash testi hali qilinmagan.
- Rollback
- rollback.sh <sha>: 8 ta image borligini tekshiradi, keyin build'siz up -d --no-deps --force-recreate. Migratsiyalar Flyway bilan faqat oldinga (V5 faqat index qo'shadi, eski kod ishlaydi). Production mashqida rollback 48 s (uzilish ~36 s), qayta oldinga 39 s (~26 s).
- Monitoring
- lc-diagnose.sh bitta buyruq bilan: konteyner holati, restart/OOM, xotira, 24 soatlik ERROR soni, public endpoint kechikishi, statuslar bo'yicha buyurtmalar, 15 daqiqadan ortiq osilganlar, outbox PENDING, zaxira, DB hajmlari, Kafka lag. 2026-10-09: 9 kun uzluksiz, 0 restart, 0 OOM, 0 ERROR, Kafka lag 0, PENDING 0. Jaeger (OpenTelemetry, sampling 20%) bilan butun saga trace'i.
Skrinshotlar
Cheklovlar va xulosalar
Hozirgi cheklovlar
- Yuk raqamlari bitta noutbukda o'lchangan (±25%), production throughput emas; keyingi qadam — alohida yuk mashinasi va production'ga o'xshash muhit
- V5 dan keyin flash-sale.js qayta o'lchanmagan, oldin/keyin jadvali eski skript bilan; keyingi qadam — yangi skript bilan 3 run medianasi
- live-service holati (tomoshabinlar, sessiya) xotirada, bitta instance; restartda tomoshabinlar qayta ulanadi; keyingi qadam — Redis pub/sub yoki broker relay
- Rate limiter xotirada: gateway ko'paytirilsa limit instance soniga ko'payadi, restartda nolga tushadi; login limiti faqat IP bo'yicha, ko'p IP'dan bitta akkauntga hujumni to'xtatmaydi; keyingi qadam — Redis limiter va akkaunt bo'yicha limit
- JWT kaliti env'da, KMS emas, rotatsiya yo'q; keyingi qadam — JWKS'da ikki kalit bilan rotatsiya va secret manager
- Deploy paytida ~26–36 s uzilish (bitta instance, rolling/blue-green yo'q), bu vaqtda gateway 503 emas, 500 qaytaradi — mijoz qayta urinish kerakligini bilmaydi
- outbox_events va refresh_tokens tozalanmaydi, vaqt o'tishi bilan o'sadi; keyingi qadam — retention job
- Bo'lishilgan 8 GB VPS, swap 813 MB — xotira bosimida kechikish oshishi mumkin
- CI (GitHub Actions billing), alerting, backup tiklash mashqi va frontend avtomatik testlari yo'q
- To'lovlar Toss test kalitlari bilan, video simulyatsiya
Asosiy kod
order-service/…/saga/OrderSagaOrchestrator.ktSaga holat mashinasi: hold → to'lov → tasdiq, kompensatsiya, OPEN_ORDER_EXISTS
order-service/…/saga/SagaRecoveryScheduler.kt20 s dan ortiq qotgan qadamlar va to'lov muddati o'tgan buyurtmalarni har 5 s tiklaydi
order-service/…/outbox/OutboxRelay.ktParallel yuborish, faqat tasdiqlangan ketma-ket prefiks PUBLISHED, birinchi xatoda to'xtash
inventory-service/…/service/InventoryService.ktreserve / confirm / release / expire; qulfsiz rad etish, keyin FOR UPDATE
payment-service/…/gateway/TossPaymentGateway.ktToss confirm/cancel: timeout, Idempotency-Key, rad javobini xaritalash
api-gateway/…/filter/IdentityRelayFilter.ktMijoz yuborgan identity header'larini o'chirib, JWT'dan qayta yozadi
api-gateway/…/ratelimit/RateLimitFilter.kt, ClientAddress.ktRate limit va XFF zanjiridan ishonchli IP
auth-service/…/config/SigningKeys.ktKalitni env'dan yuklash, thumbprint kid
delivery-service/…/webhook/WebhookSignature.ktHMAC imzo, timestamp yangiligi, event_id bo'yicha bir martalik qo'llash
order-service/…/V5__one_open_order_per_member_product.sqlBitta ochiq buyurtma — partial unique index
load-test/k6/flash-sale.js200 VU flash sale va 5 invariant
Hozirgi holat va keyingi qadamlar
100 dona uchun 1 000 ta xarid urinishi (200 VU) 3 marta yugurildi: har safar ortiqcha sotuv 0, to'lov va sotilgan dona o'rtasida nomuvofiqlik 0, HTTP xato 0; har run oxirida 5 ta invariant admin API orqali tekshirildi. O'lchov bilan to'siqlar topilib, sotuv tugash vaqti 1 daq 27 s dan ~23 s ga tushdi, connection pool gipotezasi esa o'lchov bilan rad etildi. Production'da bitta hisob 2 ta to'lanmagan buyurtma bilan butun zaxirani (15 → 0) band qila olishi partial unique index bilan yopildi (15 → 5); auth restartidan keyin bir xil token turli servislarda turlicha javob olgan JWT kaliti muammosi (401/200) tuzatildi (0eeaf2c); login va buyurtmaga cheklov qo'shildi (085ded0). Shuningdek broker yo'qligida outbox tranzaksiyasi daqiqalab qulf ushlashi (881afda), partitsiya ko'paytirilgandan keyin consumer'lar yangi partitsiyalarni ko'rmagani, oldingi run'dagi hold muddati tugab sotilgan 101 ko'ringan k6 invariant xatosi va boshqa loyiha konteyneri 8080 portni egallagani sabab 14 ta integration test yiqilgani ham sababi topilib hal qilindi.