IlmHamroh
JavaScript Full-stack/31-qism. Prodakshn backend va AI integratsiya10/34-dars21 daqiqa
Mundarija (26)

CDC va ma'lumot sinxronlash: PostgreSQL o'zgarishlarini WAL'dan o'qish

Qisqacha: CDC (Change Data Capture, o'zgarishlarni ushlash) — bazadagi har INSERT, UPDATE, DELETE ni oqim sifatida tashqariga uzatish. Postgres buni WAL'dan qiladi: logical decoding WAL yozuvlarini "qaysi jadvalda qaysi qator qanday o'zgardi" ko'rinishiga keltiradi, replikatsiya sloti esa iste'molchi qayergacha o'qiganini eslab turadi. Kanon o'lchovida commit'dan Node iste'molchigacha mediana 5.1 ms bo'ldi. Lekin iste'molchi to'xtasa, slot WAL'ni ushlab turadi: 42 kB dan 77 MB gacha o'sdi. CDC "nima o'zgardi"ni beradi, "nima uchun"ni emas — shuning uchun loyihada 29-qismdagi outbox qoladi.

Bu darsda

  • CDC qaysi muammoni hal qilishini va qayerda kerakligini tushuntirasiz.
  • O'z bazangizda logical decoding'ni test_decoding bilan ishga tushirasiz.
  • REPLICA IDENTITY va rollback qilingan tranzaksiyalarning oqimdagi ko'rinishini bilasiz.
  • Kanon Node iste'molchisini (pgoutput) va uning o'lchovini o'qiysiz.
  • Slot WAL to'plashi xavfini va monitoringini bilasiz.
  • CDC va transactional outbox'ni solishtirib, to'g'ri tanlov qilasiz.

Oldin bilishingiz kerak: Replikatsiya va read replica'lar, Transactional outbox naqshi, Meilisearch va Typesense, LISTEN/NOTIFY.

Replikatsiya darsida fizik replikatsiya butun klasterni bayt-bayt ko'chirishini ko'rdik, mantiqiy replikatsiya esa faqat nomini aytib o'tdik. Bu darsda aynan mantiqiy qismni ochamiz — va o'zgarishlarni boshqa Postgres'ga emas, istalgan tizimga uzatamiz.

1. Nega bu kerak?

Ma'lumot ko'pincha bitta joyda yashamaydi:

Eng oddiy yo'l — ikki joyga yozish (dual write): ilova avval Postgres'ga, keyin Meilisearch'ga yozadi. Muammo: ikkinchi yozuv yiqilsa yoki jarayon o'rtada o'lsa, ikki tizim farq qilib qoladi. Bundan tashqari, kimdir bazani to'g'ridan o'zgartirsa (migratsiya, admin SQL, trigger), ilova umuman bilmaydi.

CDC boshqacha yondashadi: haqiqat manbai — bazaning o'zi. Har o'zgarish allaqachon WAL'ga yozilgan. Uni o'qib, tashqariga uzatish kifoya. O'xshatish: bank hisobvarag'i ko'chirmasi. Har operatsiya ko'chirmaga tushadi. Buxgalter sizdan "nima qildingiz?" deb so'ramaydi — ko'chirmani o'qiydi.

flowchart LR
  A["Ilova"] -->|"INSERT / UPDATE"| P[("Postgres")]
  P --> W["WAL"]
  W --> S["Slot + logical decoding"]
  S --> I["Iste'molchi"]
  I --> Q["Qidiruv indeksi"]
  I --> K["Kesh"]
  I --> AN["Analitika"]

Ilova faqat Postgres'ga yozadi. Qolgan hamma narsa WAL'dan kelib chiqadi: iste'molchi o'zgarishlarni o'qiydi va har tizimga tarqatadi.

2. Logical decoding

2.1 Tushunchalar

  • Logical decoding — WAL'dagi jismoniy yozuvlarni (sahifa, bayt) mantiqiy o'zgarishlarga aylantirish: "vazifalar jadvaliga shunday qator qo'shildi".
  • Chiqish plagini (output plugin) — o'zgarishlarni qaysi formatda berish. Postgres bilan ikkitasi keladi: test_decoding (odam o'qiydigan matn, sinov uchun) va pgoutput (ikkilik protokol, mantiqiy replikatsiya va kutubxonalar ishlatadi).
  • Replikatsiya sloti — iste'molchining "xatcho'pi": qayergacha o'qilgani. Replikatsiya darsida fizik slotni ko'rdik, bu — mantiqiy slot.
  • wal_level = logical — WAL'ga mantiqiy dekodlash uchun yetarli ma'lumot yozilsin. Sukut qiymat — replica, uni o'zgartirish server qayta ishga tushishini talab qiladi.

Biz o'z test klasterimizda wal_level = logical qo'ydik (biz ishga tushirdik, PostgreSQL 17.6):

sql
SHOW wal_level;
text
 wal_level
-----------
 logical
(1 row)

Bu blok nocheck: sukut bo'yicha o'rnatmada replica bo'ladi. O'zingiz sinamoqchi bo'lsangiz — postgresql.conf da wal_level = logical qilib, serverni qayta ishga tushiring (lokal test bazasida).

2.2 Slot yaratish va o'zgarishlarni o'qish

Kichik jadval va test_decoding slotini yaratamiz:

sql
CREATE TABLE vazifalar (
  id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  matn text NOT NULL,
  bajarildi boolean NOT NULL DEFAULT false
);
SELECT slot_name
  FROM pg_create_logical_replication_slot(
         'cdc_dars', 'test_decoding');
SELECT slot_name, plugin, slot_type, active
  FROM pg_replication_slots;
text
CREATE TABLE
 slot_name
-----------
 cdc_dars
(1 row)

 slot_name |    plugin     | slot_type | active
-----------+---------------+-----------+--------
 cdc_dars  | test_decoding | logical   | f
(1 row)

Slot yaratilgan paytdan boshlab har o'zgarish uning uchun saqlanadi. Endi ilova odatdagidek ishlaydi:

sql
INSERT INTO vazifalar (matn)
  VALUES ('Non olish'), ('Dars tayyorlash');
UPDATE vazifalar SET bajarildi = true WHERE id = 1;
DELETE FROM vazifalar WHERE id = 2;

SELECT data FROM pg_logical_slot_peek_changes(
  'cdc_dars', NULL, NULL, 'include-xids', '0');
text
INSERT 0 2
UPDATE 1
DELETE 1
                                                data
----------------------------------------------------------------------------------------------------
 BEGIN
 table public.vazifalar: INSERT: id[bigint]:1 matn[text]:'Non olish' bajarildi[boolean]:false
 table public.vazifalar: INSERT: id[bigint]:2 matn[text]:'Dars tayyorlash' bajarildi[boolean]:false
 COMMIT
 BEGIN
 table public.vazifalar: UPDATE: id[bigint]:1 matn[text]:'Non olish' bajarildi[boolean]:true
 COMMIT
 BEGIN
 table public.vazifalar: DELETE: id[bigint]:2
 COMMIT
(10 rows)

Har tranzaksiya — BEGIN va COMMIT orasida. Uch buyruq — uch tranzaksiya (har buyruq o'z avtomatik tranzaksiyasida). Ikki qatorli INSERT — bitta tranzaksiyada ikki o'zgarish. 'include-xids', '0' — tranzaksiya raqamlarini chiqarmaslik (ular har yugurishda boshqacha).

Diqqat qiling: UPDATE qatori yangi qiymatlarni to'liq beradi, DELETE esa faqat id ni. Eski qiymatlar qayerda?

2.3 REPLICA IDENTITY

Postgres sukut bo'yicha UPDATE va DELETE uchun WAL'ga faqat birlamchi kalit (primary key) eski qiymatini yozadi — qatorni topish uchun shu yetarli. Bu sozlama — REPLICA IDENTITY. FULL qilinsa, eski qatorning hamma ustunlari yoziladi:

sql
ALTER TABLE vazifalar REPLICA IDENTITY FULL;
UPDATE vazifalar SET matn = 'Non va sut olish' WHERE id = 1;

SELECT data FROM pg_logical_slot_peek_changes(
  'cdc_dars', NULL, NULL, 'include-xids', '0')
 OFFSET 10;
text
ALTER TABLE
UPDATE 1
                                                                                        data
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 BEGIN
 COMMIT
 BEGIN
 table public.vazifalar: UPDATE: old-key: id[bigint]:1 matn[text]:'Non olish' bajarildi[boolean]:true new-tuple: id[bigint]:1 matn[text]:'Non va sut olish' bajarildi[boolean]:true
 COMMIT
(5 rows)

Endi old-key — eski qator to'liq, new-tuple — yangisi. Bu kerak bo'ladi, masalan, qidiruv indeksida "eski matn bo'yicha yozuvni o'chirib, yangisini qo'shish" uchun yoki audit uchun. Narxi — har UPDATE/DELETE WAL'ga ikki baravar ko'p yozadi. OFFSET 10 — oldingi bo'limdagi o'nta qatorni o'tkazib yuborish: peek slotni siljitmaydi, har safar boshidan ko'rsatadi. Boshidagi bo'sh BEGIN / COMMIT — ALTER TABLE tranzaksiyasi: logical decoding DDL buyruqlarini uzatmaydi, faqat bo'sh o'ram qoladi. Ya'ni jadval tuzilishi o'zgarganini iste'molchi oqimdan bilmaydi — bu ham CDC'ning cheklovi.

2.4 Bekor qilingan tranzaksiya

sql
BEGIN;
INSERT INTO vazifalar (matn) VALUES ('Bu ko''rinmaydi');
ROLLBACK;

SELECT count(*) AS yangi_qatorlar
  FROM pg_logical_slot_peek_changes(
    'cdc_dars', NULL, NULL, 'include-xids', '0')
 WHERE data LIKE '%ko''rinmaydi%';
text
BEGIN
INSERT 0 1
ROLLBACK
 yangi_qatorlar
----------------
              0
(1 row)

Nol. Logical decoding faqat tasdiqlangan (commit qilingan) tranzaksiyalarni beradi. Ilova "yozdim, keyin xato bo'lib qaytardim" desa, iste'molchi hech narsa ko'rmaydi. Ikki joyga yozishning eng katta muammosi — "birinchisi yozildi, ikkinchisi yo'q" — shu bilan yopiladi.

2.5 O'qish va tasdiqlash

peek — faqat ko'rish. get — o'qish va slotni oldinga siljitish:

sql
SELECT count(*) AS oqildi FROM pg_logical_slot_get_changes(
  'cdc_dars', NULL, NULL, 'include-xids', '0');
SELECT count(*) AS qoldi FROM pg_logical_slot_peek_changes(
  'cdc_dars', NULL, NULL, 'include-xids', '0');
text
 oqildi
--------
     15
(1 row)

 qoldi
-------
     0
(1 row)

get dan keyin slot "bu o'zgarishlar yetkazildi" deb biladi va ularga kerakli WAL'ni bo'shatadi. Haqiqiy iste'molchi get ni SQL bilan emas, replikatsiya protokoli orqali qiladi va har qabul qilgan joyini tasdiqlaydi (acknowledge). Tasdiqlamay o'lsa — qayta ulanganda o'sha joydan qayta oladi. Demak bir o'zgarish ikki marta kelishi mumkin: yetkazish kafolati — kamida bir marta (at-least-once). Transactional outbox darsidagi kabi, iste'molchi idempotent bo'lishi shart.

2.6 Publication

pgoutput plagini qaysi jadvallarni uzatishni publication'dan oladi — Replikatsiya darsidagi mantiqiy replikatsiya tushunchasi:

sql
CREATE PUBLICATION vazifalar_cdc FOR TABLE vazifalar;
SELECT pubname, schemaname, tablename FROM pg_publication_tables;
text
CREATE PUBLICATION
    pubname    | schemaname | tablename
---------------+------------+-----------
 vazifalar_cdc | public     | vazifalar
(1 row)

Publication — "shu jadvallar o'zgarishlari tashqariga chiqadi" degan e'lon. Uni boshqa Postgres SUBSCRIPTION bilan o'qisa — mantiqiy replikatsiya. Node yoki Debezium o'qisa — CDC. Mexanizm bitta.

Tajribadan keyin slotni o'chiramiz — aks holda u WAL'ni abadiy ushlab turadi (keyingi bo'lim):

sql
SELECT pg_drop_replication_slot('cdc_dars');
DROP PUBLICATION vazifalar_cdc;
text
 pg_drop_replication_slot
--------------------------

(1 row)

DROP PUBLICATION

Tekshirib ko'ring: Iste'molchi DELETE hodisasini oldi, lekin qatorning matn ustuni kerak edi (qidiruv indeksidan o'chirish uchun). Jadval REPLICA IDENTITY DEFAULT da. Ikki yechim ayting.

Javob

Birinchisi — REPLICA IDENTITY FULL: WAL'ga eski qatorning hamma ustunlari yoziladi (WAL hajmi oshadi). Ikkinchisi — iste'molchi matn ga emas, id ga tayansin: qidiruv indeksida yozuv id bo'yicha saqlansa, o'chirish uchun id yetarli. Ko'pincha ikkinchisi to'g'ri: tashqi tizimdagi yozuv birlamchi kalit bilan bog'lanadi.

3. Kanon tajribasi: Node iste'molchi

3.1 Tanlov

Kanon pgoutput plaginini tanladi — u Postgres ichida, qo'shimcha o'rnatishsiz. Node tomonda kanon pg-logical-replication 2.6.0 kutubxonasini oldi. Eng yangisi 3.0.0 edi, lekin kanon 7 kundan eski versiyani tanladi. Yangi chiqqan paketni bir hafta kutish — buzilgan yoki zararli reliz ehtimolini kamaytiradigan ehtiyot odati. wal2json plagini image'da yo'q, Debezium esa Kafka Connect talab qiladi — ikkalasi ham sinalmagan.

Skript yadrosi (qatorlar bo'lindi, o'lchov qismi qisqartirildi):

js
import pg from "pg";
import {
  LogicalReplicationService, PgoutputPlugin,
} from "pg-logical-replication";

const URL = "postgres://postgres@127.0.0.1:5496";
// ... cdc_tajriba bazasi, vazifalar jadvali, keyin:
await db.query(
  "CREATE PUBLICATION vazifalar_cdc FOR TABLE vazifalar");
await db.query(
  "SELECT pg_create_logical_replication_slot(" +
    "'cdc_slot', 'pgoutput')");

const kelgan = new Map();
const turlar = {};
const xizmat = new LogicalReplicationService(
  { connectionString: `${URL}/cdc_tajriba` },
  { acknowledge: { auto: true, timeoutSeconds: 10 } },
);
xizmat.on("data", (_lsn, x) => {
  turlar[x.tag] = (turlar[x.tag] ?? 0) + 1;
  if (x.tag === "insert") {
    kelgan.set(Number(x.new.id), performance.now());
  }
});
const plagin = new PgoutputPlugin({
  protoVersion: 2, publicationNames: ["vazifalar_cdc"],
});
xizmat.subscribe(plagin, "cdc_slot");

// 200 ta alohida INSERT: yuborishdan iste'molchi olguncha
for (let i = 0; i < 200; i++) {
  const t0 = performance.now();
  const { rows: [{ id }] } = await db.query(
    "INSERT INTO vazifalar (matn) VALUES ($1) RETURNING id",
    [`vazifa ${i}`]);
  for (let j = 0; j < 2000 && !kelgan.has(Number(id)); j++) {
    await new Promise((ok) => setTimeout(ok, 1));
  }
  kech.push((kelgan.get(Number(id)) ?? NaN) - t0);
}

xizmat.on("data", ...) — har o'zgarish uchun chaqiriladi: x.tag — xabar turi (begin, insert, commit …), x.new — yangi qator. acknowledge: { auto: true } — kutubxona qabul qilingan joyni avtomatik tasdiqlaydi. Asosiy sikl INSERT yuboradi va iste'molchi o'sha id ni ko'rguncha har 1 ms da tekshiradi.

3.2 Natija

Chiqish ikki qismdan iborat. Birinchi qatorda — kechikish (p50 — mediana, ya'ni o'lchovlarning yarmi shundan tez; p95 — 100 tadan 95 tasi shundan tez). Ikkinchi qismda — iste'molchi to'xtaganda slot qancha WAL ushlab qolgani:

text
1) commit → iste'molchi (200 INSERT, ketma-ket): p50 5.1 ms, p95 8.5 ms, maks 17.8 ms
   xabar turlari: {"begin":201,"relation":1,"insert":200,"commit":201,"update":10} (begin/commit/relation — har tranzaksiya o'rami; UPDATE 10 qator → 10 ta update)
2) iste'molchi TO'XTAGAN (slot active=false): slot ushlab turgan WAL 42 kB → 200 000 INSERT + UPDATE dan keyin 77 MB (iste'molchi qaytmasa disk to'ladi → max_slot_wal_keep_size)

Birinchi qator: o'zgarish iste'molchiga odatda 5 ms ichida yetdi. Ikkinchi qator — xabarlar soni. 200 INSERT va bitta UPDATE ... WHERE id <= 10 — jami 201 tranzaksiya. Shuning uchun begin va commit xabarlari 201 tadan keldi. relation xabari jadval tuzilishini tasvirlaydi. U birinchi o'zgarishdan oldin bir marta keladi. UPDATE bitta buyruq, lekin 10 ta qatorni o'zgartirdi — 10 ta update xabari: CDC buyruqni emas, har qatorni uzatadi.

INSERT yuborishdan iste'molchi qabul qilguncha (200 o'lchov)
  • p50 (mediana)5,1 ms
  • p958,5 ms
  • eng uzoq17,8 ms

Manba: IlmHamroh kanon o'lchovi, 2026-11-03: PostgreSQL 18.6 (wal_level=logical, Docker), pgoutput, pg-logical-replication 2.6.0, Node 24.21.0; bitta mashina, bitta yugurish

3.3 Natijani halol o'qish

Bu "commit → iste'molchi" emas. Skript vaqtni INSERT yuborishdan oldin oladi (t0). Demak 5.1 ms ichiga INSERT'ning o'zi (tarmoq, bajarish, commit, javob) ham kiradi. Skript chiqishidagi sarlavha buni "commit → iste'molchi" deb ataydi — aslida "yozishdan → iste'molchigacha".

O'lchov qadami — 1 ms dan katta bo'lishi mumkin. Sikl har tekshiruvdan keyin setTimeout(ok, 1) bilan kutadi. Windows'da taymer aniqligi ko'pincha 1 ms dan qo'pol — haqiqiy kechikish o'lchangandan biroz kichikroq bo'lishi mumkin. Bu ta'sirni kanon o'lchamagan.

Bitta mashina, bitta yugurish. Iste'molchi va baza bir kompyuterda, tarmoqsiz. Haqiqiy tizimda iste'molchi boshqa serverda va yuk ostida — kechikish kattaroq, ayniqsa katta tranzaksiyalarda: iste'molchi tranzaksiyani faqat commit'dan keyin oladi.

4. Slot va WAL to'planishi

Kanon tajribaning ikkinchi qismida iste'molchini to'xtatdi va bazaga 200 000 qator yozib, hammasini yangiladi. Slot WAL'ni ushlab turdi: 42 kB → 77 MB. Iste'molchi qaytib kelmasa, bu o'sishda davom etadi — server diski to'lguncha. Bu — CDC'ning eng xavfli tomoni. Replikatsiya darsidagi fizik slot ogohlantirishi shu yerda raqam bilan tasdiqlandi.

Himoya choralari:

  • max_slot_wal_keep_size — slot ushlab tura oladigan WAL chegarasi. Oshsa, slot "yo'qolgan" deb belgilanadi va iste'molchi boshidan (to'liq nusxadan) boshlashi kerak bo'ladi. Disk to'lishidan yaxshiroq.
  • Monitoring — har slotning orqada qolgan hajmi:
sql
SELECT slot_name, active,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(),
                                      restart_lsn)) AS ushlab_turibdi
  FROM pg_replication_slots;

Kanon aynan shu ifoda bilan 42 kB va 77 MB ni o'lchagan. Bu qiymat o'sishda davom etsa va active = f bo'lsa — ogohlantirish (Monitoring va favqulodda vaziyatlar).

  • Keraksiz slotni o'chirish — tajriba yoki eski iste'molchidan qolgan slot. Kanon tajribadan keyin pg_drop_replication_slot('cdc_slot') bilan o'chirdi.

max_slot_wal_keep_size nimani o'zgartirishini kichik modelda ko'ramiz. Raqamlar o'ylab topilgan: iste'molchi to'xtadi, baza soatiga 300 MB WAL yozadi, diskda 20 GB bo'sh joy bor. Chiqishda har holat qanday tugashiga qarang:

js
function slotniKuzat(soatlar, walSoatiga, chegara, disk) {
  let ushlangan = 0;
  for (let soat = 1; soat <= soatlar; soat++) {
    ushlangan += walSoatiga;
    if (chegara !== null && ushlangan > chegara) {
      return `${soat}-soat: slot yo'qoldi, disk xavfsiz`;
    }
    if (ushlangan >= disk) {
      return `${soat}-soat: DISK TO'LDI — baza to'xtaydi`;
    }
  }
  return `${soatlar} soat: ${ushlangan} MB ushlab turibdi`;
}

// soatiga 300 MB WAL, bo'sh disk 20 000 MB
console.log("chegarasiz:", slotniKuzat(100, 300, null, 20000));
console.log("chegara 5 GB:", slotniKuzat(100, 300, 5000, 20000));
console.log("iste'molchi 8 soatda qaytdi:",
  slotniKuzat(8, 300, 5000, 20000));

Konsolda:

text
chegarasiz: 67-soat: DISK TO'LDI — baza to'xtaydi
chegara 5 GB: 17-soat: slot yo'qoldi, disk xavfsiz
iste'molchi 8 soatda qaytdi: 8 soat: 2400 MB ushlab turibdi

Nima bo'ldi? Chegarasiz holatda dam olish kunlari to'xtagan iste'molchi uch kundan kamroq vaqtda butun bazani to'xtatadi — ilova ham ishlamay qoladi. Chegara bilan esa faqat CDC zarar ko'radi: slot yo'qoladi, iste'molchini boshlang'ich nusxadan qayta qurish kerak, lekin baza ishlayveradi. Iste'molchi chegaraga yetmasdan qaytsa — hech narsa yo'qolmaydi, u ushlangan WAL'ni o'qib, yetib oladi. Chegara qiymatini shunday tanlang: "iste'molchini tuzatishga ulguramiz", lekin disk to'lmaydi.

Tekshirib ko'ring: Monitoring ikki slotni ko'rsatdi: biri active = t, 3 MB; ikkinchisi active = f, 9 GB va o'sib boryapti. Qaysi biri xavotirli va birinchi qadam nima?

Javob

Ikkinchisi. active = f — unga hech kim ulanmagan, demak WAL faqat to'planadi. Birinchi qadam — bu slot kimniki ekanini aniqlash: ishlayotgan iste'molchimi (to'xtab qolgan), yoki eski tajribadan qolgan "yetim" slotmi. Iste'molchi kerak bo'lsa — uni tiklash. Kerak bo'lmasa — pg_drop_replication_slot bilan o'chirish. Birinchi slot normal: kichik va faol.

5. Iste'molchini to'g'ri qurish

Kanon skripti — o'lchov uchun. Prodakshn iste'molchisida yana uch narsa o'ylanadi.

Tasdiqlash qachon. Kanon acknowledge: { auto: true } ishlatdi: kutubxona qabul qilingan joyni o'zi tasdiqlaydi. O'lchov uchun bu qulay. Lekin tasdiq o'zgarish qo'llanishidan oldin ketsa-yu, iste'molchi shu paytda yiqilsa — o'zgarish yo'qoladi: slot uni "yetkazildi" deb biladi. Ishonchli tartib: avval qidiruv indeksiga yozish, keyin tasdiqlash. Kutubxonada qo'lda tasdiqlash rejimi bor: README'ga ko'ra acknowledge: { auto: false } qo'yiladi va o'zgarish muvaffaqiyatli qo'llangandan keyin xizmat.acknowledge(lsn) chaqiriladi (2026-11-03 holati; kanon bu rejimni sinamagan). Shunda yetkazish "kamida bir marta" bo'ladi, takrorni esa 3-mashqdagi LSN tekshiruvi ushlaydi.

Tartib. Logical decoding tranzaksiyalarni commit tartibida beradi. Iste'molchi ularni parallel qo'llasa (masalan, har biriga alohida fetch), tartib buziladi: eski UPDATE yangi DELETE dan keyin yetib kelishi mumkin. Oddiy qoida — bitta slot, bitta ketma-ket iste'molchi. Tezlik kerak bo'lsa — o'zgarishlarni paketlab (masalan, 100 tadan) bitta so'rov bilan qo'llash.

Katta tranzaksiyalar. Iste'molchi tranzaksiyani faqat commit'dan keyin oladi. 200 000 qatorli bitta UPDATE — iste'molchiga birdaniga 200 000 xabar. Kanon tajribasining ikkinchi qismi aynan shunday yuk yaratdi. Iste'molchi xotirasi va qabul qiluvchi tizim (qidiruv indeksi) bunga tayyor bo'lishi kerak. Ommaviy migratsiyalarni paketlarga bo'lib bajarish CDC uchun ham foydali.

6. CDC va outbox

29-qismda xabar uzatish uchun transactional outbox qurdik: ilova o'zgarish bilan bir tranzaksiyada outbox jadvaliga hodisa yozadi, relay uni o'qib tarqatadi (Transactional outbox naqshi). CDC ham "bazadan tashqariga ishonchli uzatish". Farqi nimada?

Outbox CDC
Nima uzatiladi domen hodisasi: vazifa.bajarildi qator o'zgarishi: UPDATE vazifalar
"Nima uchun" bor (ilova yozadi) yo'q (faqat ustunlar)
Ilova kodi har amalda yozish kerak o'zgarmaydi
Bazani to'g'ridan o'zgartirish ko'rinmaydi ko'rinadi
Infratuzilma jadval + relay wal_level=logical, slot, iste'molchi

Misol farq: foydalanuvchi vazifani bajardi va 0.5 soniyadan keyin qaytardi. Outbox ikki domen hodisasini beradi: vazifa.bajarildi va qaytarildi. CDC ikki UPDATE ni beradi: bajarildi: t, keyin f. Webhook obunachisi uchun birinchisi tushunarli. Qidiruv indeksi uchun esa ikkinchisi yetarli: unga faqat oxirgi holat kerak.

Kanon qarori: loyihada outbox qoladi — webhook va bildirishnomalar domen hodisalarini kutadi. CDC — qidiruv indeksi va analitika sinxronlash uchun to'g'ri vosita, lekin loyihada hozircha bunday tizim yo'q. Ikkalasi birga ham ishlatiladi: outbox jadvalining o'zini CDC bilan o'qish — Debezium'ning "Outbox Event Router" naqshi (Debezium hujjatida alohida transformatsiya sifatida bor, 2026-11-03 holati; kursda sinalmagan).

Tekshirib ko'ring: Admin to'g'ridan psql orqali UPDATE vazifalar SET matn = ... WHERE id = 5 bajardi. Outbox orqali ishlaydigan webhook obunachisi buni biladimi? CDC iste'molchisi-chi?

Javob

Outbox — yo'q: hodisani ilova kodi yozadi, psql orqali o'zgarishda outbox jadvaliga hech narsa tushmaydi. CDC — ha: o'zgarish WAL'ga yozildi, logical decoding uni boshqa har qanday UPDATE kabi beradi. Bu CDC'ning kuchli tomoni ("hech narsa o'tib ketmaydi") va zaif tomoni ("nima uchun o'zgargani noma'lum") — ikkalasi bir vaqtda.

7. Debezium, wal2json va boshqalar

Quyidagilar rasmiy hujjat bo'yicha (hujjatlar 2026-11-03 da ochib ko'rildi; vositalarning o'zi kursda sinalmagan):

  • Debezium — eng mashhur CDC platformasi. Kafka Connect ichida ishlaydi: Postgres (pgoutput orqali), MySQL, MongoDB va boshqalardan o'zgarishlarni o'qib, Kafka mavzulariga yozadi. Boshida jadvalning to'liq nusxasini (snapshot) ham oladi.
  • wal2json — o'zgarishlarni JSON ko'rinishida beradigan chiqish plagini. Uni server kengaytmasi sifatida o'rnatish kerak — managed xizmatlarda har doim ham bor emas.
  • Managed xizmatlar — ko'p bulut Postgres'lari mantiqiy replikatsiyani alohida sozlama bilan yoqadi.

Boshlang'ich nusxa haqida: slot faqat yaratilgan paytdan keyingi o'zgarishlarni beradi. Qidiruv indeksini noldan qurish uchun avval mavjud qatorlarni ko'chirish, keyin slotdan davom ettirish kerak — va bu ikki bosqich orasida o'zgarish yo'qolmasligi lozim. Debezium buni o'zi qiladi; o'z iste'molchingizda bu alohida ish.

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
logical decoding requires "wal_level" >= "logical" sukut replica wal_level = logical + qayta ishga tushirish
Disk to'lmoqda, sabab noma'lum o'chgan iste'molchining sloti WAL'ni ushlab turibdi slot monitoringi, max_slot_wal_keep_size
DELETE hodisasida faqat id REPLICA IDENTITY DEFAULT FULL yoki id ga tayanish
Bir o'zgarish ikki marta qayta ishlangan qayta ulanishda tasdiqlanmagan joydan idempotent iste'molchi (LSN yoki versiya bo'yicha)
Indeksda eski yozuvlar yo'q slot faqat yangi o'zgarishlarni beradi boshlang'ich snapshot

Birinchi qatordagi xabarni o'zimiz ko'rdik: test klasterimizni vaqtincha wal_level = replica bilan qayta ishga tushirib, slot yaratishga urindik (biz ishga tushirdik, PostgreSQL 17.6):

text
ERROR:  logical decoding requires "wal_level" >= "logical"

Keyin sozlamani logical ga qaytardik. wal_level = replica bo'lsa, CREATE PUBLICATION ham ogohlantiradi: WARNING: "wal_level" is insufficient to publish logical changes — publication yaratiladi, lekin slot yaratib bo'lmagani uchun uni hech kim o'qiy olmaydi.

9. Mashqlar

Mashqlar kurs/mashqlar/31/10-cdc/ da. 2- va 3-mashq yechimlari Node 24.21.0 da tekshirilgan (biz ishga tushirdik).

1-mashq (oson): bo'shliqlarni to'ldiring

  • WAL'ni "qaysi qator qanday o'zgardi" ko'rinishiga keltirish — logical
  • Iste'molchi qayergacha o'qiganini eslab turadigan obyekt — replikatsiya
  • Eski qatorning hamma ustunini WAL'ga yozish — REPLICA IDENTITY
  • Kanonda to'xtagan iste'molchi sloti ushlab qolgan WAL — 42 kB dan MB gacha
  • Rollback qilingan tranzaksiyaning oqimdagi o'zgarishlari soni —
Yechim
  • decoding — test_decoding yoki pgoutput plagini bilan.
  • sloti — mantiqiy slot; o'chirilmasa WAL to'planadi.
  • FULL — sukut — faqat birlamchi kalit.
  • 77 — 200 000 INSERT + UPDATE dan keyin.
  • 0 — faqat commit qilinganlar uzatiladi.

2-mashq (o'rta): test_decoding qatorini tahlil qilish

test_decoding qatorini obyektga aylantiring: qatorniOch("table public.vazifalar: UPDATE: id[bigint]:1 matn[text]:'Non olish' bajarildi[boolean]:true") → { jadval: "public.vazifalar", amal: "UPDATE", ustunlar: { id: "1", matn: "Non olish", bajarildi: "true" } }. Qiymatlar satr bo'lib qolsin; qo'shtirnoqli matn ichidagi bo'shliqlar saqlansin. BEGIN/COMMIT qatorlari uchun null qaytaring. Ishora: matchAll va guruhli RegExp (RegExp): ustun — nom[tur]:qiymat, qiymat — '...' yoki bo'shliqsiz so'z.

Yechim
js
const BOSH = /^table ([\w.]+): (INSERT|UPDATE|DELETE): (.*)$/;
const USTUN = /(\w+)\[[^\]]+\]:('(?:[^']|'')*'|\S+)/g;

function qatorniOch(qator) {
  const m = qator.match(BOSH);
  if (!m) return null;
  const ustunlar = {};
  for (const [, nom, qiymat] of m[3].matchAll(USTUN)) {
    ustunlar[nom] = qiymat.startsWith("'")
      ? qiymat.slice(1, -1).replaceAll("''", "'")
      : qiymat;
  }
  return { jadval: m[1], amal: m[2], ustunlar };
}

console.log(qatorniOch("BEGIN"));
console.log(qatorniOch("table public.vazifalar: UPDATE: " +
  "id[bigint]:1 matn[text]:'Non olish' bajarildi[boolean]:true"));
console.log(qatorniOch("table public.vazifalar: INSERT: " +
  "id[bigint]:3 matn[text]:'O''qish kitobi' bajarildi[boolean]:false")
  .ustunlar.matn);

Konsolda:

text
null
{
  jadval: 'public.vazifalar',
  amal: 'UPDATE',
  ustunlar: { id: '1', matn: 'Non olish', bajarildi: 'true' }
}
O'qish kitobi

Uchinchi misol — qiyin holat: test_decoding matn ichidagi apostrofni SQL qoidasi bilan ikkilantiradi (''). replaceAll("''", "'") uni qaytaradi. Real kodda test_decoding emas, pgoutput va kutubxona ishlatiladi — kutubxona ikkilik protokolni o'zi ochadi. Bu mashq esa format qanday tuzilganini his qilish uchun.

3-mashq (qiyin): idempotent iste'molchi

Qidiruv indeksi — Map (id → matn). Iste'molchi o'zgarishlarni { lsn, amal, id, matn } ko'rinishida oladi (lsn — o'suvchi son). Qayta ulanishdan keyin ba'zi o'zgarishlar ikkinchi marta keladi. qolla(indeks, holat, ozgarishlar) funksiyasini yozing: holat.oxirgiLsn dan kichik yoki teng LSN'li o'zgarishni o'tkazib yuborsin, qolganlarini qo'llasin (INSERT/UPDATE — set, DELETE — delete) va oxirgiLsn ni yangilasin. Qo'llangan va o'tkazilganlar sonini qaytarsin. Ishora: Map (Map).

Yechim
js
function qolla(indeks, holat, ozgarishlar) {
  let qollandi = 0, otkazildi = 0;
  for (const o of ozgarishlar) {
    if (o.lsn <= holat.oxirgiLsn) { otkazildi++; continue; }
    if (o.amal === "DELETE") indeks.delete(o.id);
    else indeks.set(o.id, o.matn);
    holat.oxirgiLsn = o.lsn;
    qollandi++;
  }
  return { qollandi, otkazildi };
}

const indeks = new Map();
const holat = { oxirgiLsn: 0 };
const birinchi = [
  { lsn: 10, amal: "INSERT", id: 1, matn: "Non olish" },
  { lsn: 20, amal: "INSERT", id: 2, matn: "Dars tayyorlash" },
];
console.log(qolla(indeks, holat, birinchi));
// iste'molchi qayta ulandi: 20 tasdiqlanmagan edi — qayta keldi
const ikkinchi = [
  { lsn: 20, amal: "INSERT", id: 2, matn: "Dars tayyorlash" },
  { lsn: 30, amal: "UPDATE", id: 1, matn: "Non va sut olish" },
  { lsn: 40, amal: "DELETE", id: 2 },
];
console.log(qolla(indeks, holat, ikkinchi));
console.log([...indeks], holat);

Konsolda:

text
{ qollandi: 2, otkazildi: 0 }
{ qollandi: 2, otkazildi: 1 }
[ [ 1, 'Non va sut olish' ] ] { oxirgiLsn: 40 }

LSN 20 ikkinchi marta keldi va o'tkazib yuborildi. Bu misolda takroriy INSERT zararsiz bo'lardi (o'sha qiymat qayta yoziladi). Lekin tartib buzilsa — masalan, eski UPDATE yangi DELETE dan keyin qayta kelsa — o'chirilgan yozuv "tirilib" qolardi. LSN tekshiruvi bundan himoya qiladi. Muhim: oxirgiLsn qidiruv indeksi bilan birga saqlanishi kerak — biri yozilib, ikkinchisi yo'qolsa, himoya buziladi.

10. Loyiha qadami: kanon tajribasi

Bu darsda vazifalar-api o'zgarmaydi va commit yo'q. Loyihada xabar uzatish uchun 29-qismdagi outbox ishlaydi va u domen hodisalarini beradi — webhook va bildirishnomalarga aynan shu kerak. CDC'ni ishlatadigan tashqi tizim (alohida qidiruv indeksi yoki analitik baza) hozircha yo'q. CDC infratuzilma ham talab qiladi: wal_level = logical, slot monitoringi, iste'molchi jarayoni — bu 33-qismdagi deploy bilan birga ko'riladi.

Kanon tajribasi: alohida baza (wal_level = logical), publication va pgoutput sloti, Node iste'molchi (pg-logical-replication 2.6.0), 200 ta INSERT kechikishi, xabar turlari, to'xtagan iste'molchi bilan WAL o'sishi. Oxirida slot va baza o'chirildi. wal2json va Debezium — sinalmagan.

O'zingiz takrorlaydigan qism: lokal test bazangizda wal_level = logical qilib, darsdagi test_decoding bloklarini bajaring. Keyin bitta tranzaksiyada uchta INSERT qiling va peek natijasida nechta BEGIN chiqishini oldindan ayting. Oxirida slotni albatta o'chiring.

11. Real ishda

  • Qayerda uchraydi: qidiruv indekslarini sinxronlash (Elasticsearch, Meilisearch), ma'lumotni analitik omborga oqizish, keshni bekor qilish, mikroservislar orasida ma'lumot almashish, eski tizimdan yangisiga bosqichma-bosqich ko'chish.
  • Intervyu savollari: "Dual write nega xavfli?", "CDC va outbox farqi?", "Replikatsiya sloti qanday xavf tug'diradi?", "At-least-once yetkazishda iste'molchini qanday idempotent qilasiz?"
  • Qachon kerak emas: qidiruv va hisobot bitta Postgres ichida yetsa, CDC — ortiqcha infratuzilma: slot, iste'molchi jarayoni, monitoring. Avval Postgres'da full-text search va oddiy indekslarni sinab ko'ring; tashqi tizim haqiqatan kerak bo'lgandagina CDC'ga o'ting.
  • Vositalar: Debezium + Kafka — katta kompaniyalarda standart; kichik loyihalarda — o'z Node iste'molchisi yoki tayyor bulut xizmati.
  • Kod review'da: "bu slotni kim kuzatadi?", "iste'molchi qayta ulanganda takror o'zgarishni qanday ushlaydi?", "boshlang'ich nusxa qayerdan olinadi?"

Xulosa

  • CDC — bazadagi har tasdiqlangan o'zgarishni WAL'dan o'qib, tashqi tizimlarga uzatish; dual write muammosini yopadi.
  • Logical decoding + chiqish plagini (test_decoding, pgoutput) + slot; wal_level = logical kerak.
  • REPLICA IDENTITY FULL — eski qiymatlar ham; rollback qilinganlar oqimga tushmaydi.
  • Kanon: yozishdan iste'molchigacha mediana 5.1 ms; to'xtagan iste'molchi sloti WAL'ni 42 kB → 77 MB ga o'stirdi.
  • Yetkazish — kamida bir marta: iste'molchi LSN bo'yicha idempotent bo'lsin.
  • Outbox — domen hodisalari ("nima uchun"), CDC — qator o'zgarishlari ("nima"); loyihada outbox qoladi.

Keyingi dars: Shaxsiy ma'lumotlar qonunchiligi: O'zbekiston va GDPR — foydalanuvchi ma'lumotlarini saqlash va o'chirish bo'yicha qonun talablari va ular loyihaga qanday ta'sir qiladi.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
CDC va ma'lumot sinxronlash: PostgreSQL o'zgarishlarini WAL'dan o'qish — IlmHamroh