ბიბლიოთეკა
00/07 · ~38 წთ
GUIDEDECK · PART 2 · სისტემებისთვის, რომლებმაც უნდა შეთანხმდნენ

მოწინავე
სისტემური დიზაინი —
თანმიმდევრულობა ავარიების პირობებში.

38-წუთიანი ღრმა სესია, რომელიც იქიდან იწყება, სადაც შესავალი დეკი დასრულდა. ის გულისხმობს, რომ მასშტაბირება, ქეშირება, რეპლიკაცია, რიგები და CAP უკვე იცით სისტემური დიზაინიდან მასშტაბისთვის — და მიდის რთულ ნაწილზე: როგორ შეთანხმდნენ ბევრი მანქანა, ბევრ რეგიონში, მაშინ როცა ქსელიც და მანქანებიც განუწყვეტლივ ავარდება.

~38 წთსაშუალო → მოწინავეეყრდნობა 1 ნაწილს
გადაახვიეთ
01 · თანმიმდევრულობა 5 წთ

"თანმიმდევრული" ერთი რამ არაა —
ეს სპექტრია, რომელსაც ირჩევთ.

ნაწილი 1 CAP-ით დასრულდა: როცა ქსელი იყოფა, ირჩევთ C-ს ან A-ს. ეს დეკი ერთი დონით ქვემოთ იწყება. თანმიმდევრულობა გადამრთველი არაა — ეს გარანტიების კიბეა, სადაც ყოველი შემდეგი წინაზე სუსტი და იაფია. თითოეული მონაცემისთვის სწორი საფეხურის არჩევა — სწორედ ესაა მთელი ხელოვნება. თუ დანაწილებასა და რეპლიკაციის ჩამორჩენას გახსენება სჭირდება, ეს შესავალი დეკია.

თანმიმდევრულობის მოდელი არის კონტრაქტი საცავსა და თქვენს კოდს შორის იმაზე, თუ რომელ წაკითხვას რომელი ჩაწერის დანახვა შეუძლია. ძლიერი მოდელები მეტ მოულოდნელ დალაგებას კრძალავს — და მეტი კოორდინაცია ჯდება, ანუ მეტი შეყოვნება და ნაკლები ხელმისაწვდომობა. ხელოვნება ისაა, რომ ძლიერი თანმიმდევრულობა მხოლოდ იქ დახარჯოთ, სადაც არასწორი პასუხი მართლაც მიუღებელია.
ლინეარიზებადი
ერთი წესრიგი, რეალურ დროში
თანმიმდევრული
წესრიგი, და არა საათი
კაუზალური
მიზეზი შედეგამდე
საკუთარი ჩანაწერი
სესიის გარანტიები
საბოლოო
ოდესმე დაემთხვევა
ძვირი
იაფი

კიბეზე ქვემოთ ყოველი მოდელი ნაკლებ დალაგებას კრძალავს — და ნაკლები კოორდინაცია სჭირდება, ამიტომ უფრო სწრაფი და ხელმისაწვდომია.

საფეხურები, ზემოდან ქვემოთ

  • ლინეარიზებადი (Linearizable, ძლიერი) — ყოველი ოპერაცია ისე ჩანს, თითქოს მყისიერად მოხდა ერთ წერტილში გამოძახებასა და დაბრუნებას შორის, ერთ რეალურ დროში გლობალურ წესრიგში. როგორც კი ჩაწერა დაბრუნდა, ყოველი შემდგომი წაკითხვა მას ხედავს. სწორედ ამას გულისხმობენ, როცა ამბობენ "ძლიერად თანმიმდევრული".
  • თანმიმდევრობითი (Sequential) — არსებობს რაღაც ერთი წესრიგი, რომელზეც ყველა კვანძი თანხმდება და რომელიც თითოეული კლიენტის საკუთარ რიგს იცავს — მაგრამ მას რეალურ დროს დამთხვევა არ ევალება.
  • მიზეზობრივი (Causal) — თუ A გამოიწვია B (წაიკითხეთ A, მერე ჩაწერეთ B), ყველა A-ს B-მდე ხედავს. ნამდვილად პარალელური ჩაწერები სხვადასხვა რიგით შეიძლება დაინახონ. ეს ყველაზე ძლიერი მოდელია, რომელიც ქსელის გახლეჩისასაც ხელმისაწვდომია.
  • საბოლოო (Eventual) — შეწყვიტეთ ჩაწერა და რეპლიკები დაემთხვევა. შუალედში დალაგების არანაირი დაპირება.

ლინეარიზებადობა, ზუსტად

// W(x=1) returns at t1. Any read that STARTS after t1 // must observe x=1 — no exceptions, on any replica. writer: write(x, 1) ────► ok@t1 readerA: read(x) ─► 1 // ✓ started after t1 readerB: read(x) ─► 0|1 // overlaps t1 → either ok // the cost: a quorum round-trip on the write path, // so a single global write can't dodge cross-node latency.
writer reader write x=1 t1 read → 1 ✓

წაკითხვა t1-ის შემდეგ იწყება, ამიტომ ლინეარიზებადობა აიძულებს, დააბრუნოს ახალი მნიშვნელობა 1.

სესიის გარანტიები — იაფი მოგება

გლობალური წესრიგი იშვიათად გჭირდებათ; გჭირდებათ, რომ თქვენივე ხედვა იყოს აზრიანი. ეს სესიის დონის დაპირებები eventual საცავის თავზე ზის და სწორედ იმ სიმპტომებს კურნავს, რომლებსაც მომხმარებლები ამჩნევენ — ჩვეულებრივ სესიის ერთ რეპლიკაზე მიწებებით ან ვერსიის ტოკენის თვალყურის დევნებით.

სესია

თქვენივე ჩანაწერების კითხვა

პროფილის განახლების შემდეგ თქვენ ახალ მნიშვნელობას ხედავთ შემდეგ წაკითხვაზე — მაშინაც კი, თუ სხვა მომხმარებლები ცოტა ხანს ვერ ხედავენ. ყველაზე ხშირად გამოტოვებული გარანტია, რომლის უკანაც დგას ბაგის რეპორტი "შევინახე და გაქრა".

სესია

მონოტონური წაკითხვები

როგორც კი მნიშვნელობა დაინახეთ, აღარასდროს ნახავთ უფრო ძველს — გვერდის განახლებამ უფრო ჩამორჩენილ რეპლიკაზე დროში მოგზაურობა არ უნდა გამოიწვიოს.

სესია

თანმიმდევრული პრეფიქსი

ჩაწერებს ნამდვილი წესრიგის პრეფიქსად ხედავთ — არასდროს პასუხს იმ შეტყობინებამდე, რომელსაც ის პასუხობს. სწორედ ეს დაპირება ინარჩუნებს ჩატების წაკითხვადობას.

პრაქტიკული წესი: ნაგულისხმევად აირჩიეთ eventual + სესიის გარანტიები, ლინეარიზებადობა კი მხოლოდ იმ რამდენიმე ინვარიანტზე დახარჯეთ, რომელიც არასდროს უნდა მოიხაროს — ბალანსები, მარაგის რაოდენობა, უნიკალური მომხმარებლის სახელები, ლიდერის ვინაობა.

02 · კონსენსუსი 6 წთ

როგორ შეათანხმო N მანქანა
ერთ მნიშვნელობაზე — დამტკიცებულად რთულია.

ძლიერი თანმიმდევრულობა, ერთი ლიდერი, დაფიქსირებული ლოგი: სიღრმეში ყველა მათგანი კონსენსუსამდე დაიყვანება — როცა ბევრი კვანძი ერთ მნიშვნელობაზე თანხმდება, მიუხედავად ავარიებისა და დანაკარგიანი ქსელისა. ეს არის მზიდი ალგორითმი ყველა იმ საკოორდინაციო სისტემისა, რომელზეც დამოკიდებული ხართ.

კონსენსუსი — ყველა გამართული კვანძი ერთსა და იმავე მნიშვნელობას წყვეტს და მხოლოდ იმას, რაც ნამდვილად შემოთავაზდა. FLP-ის შედეგი (Fischer–Lynch–Paterson) ამტკიცებს, რომ სრულად ასინქრონულ ქსელში ვერცერთი დეტერმინისტული ალგორითმი ვერ გარანტიას გაუწევს, რომ კონსენსუსი ყოველთვის დასრულდება, თუ თუნდაც ერთი კვანძი შეიძლება ავარდეს. ამიტომ რეალური ალგორითმები უსაფრთხოებას უპირობოდ ინარჩუნებენ და წინსვლისთვის ტაიმაუტებს ეყრდნობიან — გარანტირებულ სიცოცხლისუნარიანობას თითქმის-დარწმუნებულში ცვლიან.

Raft — კონსენსუსი, შექმნილი იმისთვის, რომ გასაგები იყოს

Raft ამოცანას ორ ამბად ყოფს: აირჩიე ერთი ლიდერი, შემდეგ კი ამ ლიდერს მიაცემინე მხოლოდ-დამატებადი ლოგის რეპლიკაცია. ყოველი გადაწყვეტილება უმრავლესობის ქვორუმზე დგას — 2f+1 კვანძით f ავარიას გადაიტანთ, რადგან ნებისმიერი ორი უმრავლესობა ერთმანეთს გადაფარავს.

ლიდერის არჩევა — ვადები და ხმები

  • დრო ვადებად (მონოტონური რიცხვები) იყოფა. ყოველ ვადას სულ მცირე ერთი ლიდერი ჰყავს.
  • მიმდევარი, რომელიც შემთხვევითი ტაიმაუტის განმავლობაში არაფერს გაიგონებს, კანდიდატი ხდება, ვადას ზრდის და ხმებს ითხოვს.
  • აიღე უმრავლესობა → ლიდერი ხარ; შემდეგ ჰართბითებს აგზავნის. შემთხვევითი ტაიმაუტი ხმების გახლეჩას იშვიათს და თვითგანკურნებადს ხდის.

5-დან 3 ხმა უმრავლესობაა — ლიდერი ერთი კვანძის ჩავარდნისასაც ძალაში რჩება.

ლოგის რეპლიკაცია — ფიქსაცია ქვორუმზე

// leader-only write path; followers just append
function onClientWrite(cmd: Command): void {
  log.append({ term, cmd })          // 1 · append locally
  sendAppendEntries(followers, log)  // 2 · replicate (RPC)
  if (acks >= majority()) {          // 3 · quorum = N/2 + 1
    commitIndex = log.lastIndex      //     durable once a majority has it
    applyToStateMachine(cmd)         // 4 · apply IN LOG ORDER
  }
}
  • ჩანაწერი დაფიქსირებულია მხოლოდ მას შემდეგ, რაც ის უმრავლესობას დისკზე უწერია — ანუ ის ნებისმიერ უმცირესობის ჩავარდნას გადაიტანს.
  • ლოგების დამთხვევა: თუ ორი ლოგი ერთ ინდექსზე/ვადაზე ემთხვევა, ისინი ყველაფერზე ემთხვევა, რაც მანამდეა — ლიდერი მიმდევრებს დამთხვევას აიძულებს.
  • მოძველებული ლოგის მქონე კანდიდატს მოგება არასდროს შეუძლია, ამიტომ ახალი ლიდერი ყოველთვის ფლობს ყველა დაფიქსირებულ ჩანაწერს (Leader Completeness).

Paxos — იგივე გარანტია, ორ ფაზად

Paxos პირველი გაჩნდა და იმავე უსაფრთხოებას ამტკიცებს, ოღონდ ლიდერისა და ლოგის ნაცვლად წამომყენებლების, მიმღებებისა და შემსწავლელების ტერმინებში. ერთი მნიშვნელობა ორ რაუნდში ირჩევა:

  • ფაზა 1 — prepare/promise: წამომყენებელი ირჩევს უნიკალურ, მზარდ რიცხვს n და უმრავლესობას სთხოვს, დაპირდნენ, რომ ყველაფერს დაბალს უგულებელყოფენ. მიმღებები პასუხობენ იმ მნიშვნელობით, რომელიც უკვე მიიღეს.
  • ფაზა 2 — accept/accepted: წამომყენებელი აგზავნის n-ს მნიშვნელობასთან ერთად (თუ ასეთი არსებობს, უკვე მიღებულთაგან ყველაზე მაღალს). თუ უმრავლესობა მას მიიღებს, მნიშვნელობა სამუდამოდ არჩეულია.
  • Multi-Paxos ფაზა 1-ს გამოტოვებს სტაბილური ლიდერის არჩევით — და ამ მომენტში ის არსებითად Raft-ია, რის გამოც გასაგებობისთვის შობილი Raft ახალ სისტემებში ჩვეულებრივ იმარჯვებს.

ჰგავს  წინადადების მიღებას კრებაზე: ჯერ დარწმუნდი, რომ სიტყვა შენია (უმრავლესობა მოგისმენს), მერე კი ხმას მიეცი — და უმრავლესობის "კი" მას სავალდებულოს ხდის.

ინსტრუმენტების ლანდშაფტი — კონსენსუსი & კოორდინაცია

Raft-ს თითქმის არასდროს ახორციელებთ თავად. ის იჯარით აიღება: პატარა, ძლიერად თანმიმდევრული საცავი, რომელიც ყველა დანარჩენისთვის ლიდერის იჯარებს, კონფიგურაციას, ბლოკირებებსა და სერვისების რეესტრებს ინახავს.

raft · kv

etcd

Raft-ზე დაშენებული გასაღები-მნიშვნელობის საცავი; Kubernetes-ის ტვინი.

  • დადებითი: მარტივი gRPC API, ლინეარიზებადი წაკითხვები, იჯარები და თვალყურის დევნება — და დამტკიცებული K8s-ის მასშტაბზე.
  • უარყოფითი: მხოლოდ პატარა მონაცემები (მეხსიერებაში ეტევა); ზოგადი მონაცემთა ბაზა არაა.
zab · znodes

ZooKeeper

ვეტერანი: znodes-ის იერარქია ZAB პროტოკოლზე.

  • დადებითი: ათ წელზე მეტია ბრძოლაში გამოცდილი; მდიდარი რეცეპტები (ბლოკირებები, ბარიერები, ლიდერის არჩევა).
  • უარყოფითი: უფრო მძიმე JVM ოპერაციები, ძველი API — და მისი მთავარი მომხმარებელი, Kafka, ახლა ჩაშენებულ KRaft-ზე გადავიდა (4.0).
raft · აღმოჩენა

Consul

Raft KV პლუს სერვისების აღმოჩენა, ჯანმრთელობის შემოწმებები და მრავალ-დატაცენტრი.

  • დადებითი: აღმოჩენა, DNS და სერვის-მეში ერთად, პირველხარისხოვანი მრავალ-DC მხარდაჭერით.
  • უარყოფითი: გასაშვებად უფრო დიდი ზედაპირი; ზედმეტია, თუ მხოლოდ ბლოკირება გჭირდებათ.

როგორ ავირჩიო: Kubernetes-ზე etcd უკვე გაშვებული გაქვთ — გამოიყენეთ ის. Consul აირჩიეთ მაშინ, როცა სერვისების აღმოჩენა და მრავალ-DC გჭირდებათ, და არა მხოლოდ კოორდინაცია. ZooKeeper-ს ძირითადად მაშინ მიმართეთ, როცა ძველ JVM სისტემებთან ინტეგრაცია გჭირდებათ, რომლებიც მას კვლავ ელოდებიან.

03 · განაწილებული ტრანზაქციები 6 წთ

ერთი კომიტი ბევრ სერვისზე
წინააღმდეგობაა — შეწყვიტეთ ცდა.

ერთი ACID ტრანზაქცია სერვისებს შორის ნიშნავს, რომ ერთ ნელ სერვისს ყველას ბლოკირებების დაჭერა შეუძლია. თანამედროვე პასუხი უფრო დიდი ტრანზაქცია არაა — ეს არის თითო სერვისის ლოკალური ტრანზაქციები, ერთმანეთზე შეკერილი კომპენსაციით, საიმედო მოვლენებითა და იდემპოტენტურობით.

ორფაზიანი კომიტი (2PC) — კოორდინატორი ყველა მონაწილეს PREPARE-ს სთხოვს, შემდეგ კი COMMIT-ს მხოლოდ მაშინ, თუ ყველამ კი თქვა. ის სწორია, მაგრამ მბლოკავი: prepare-სა და commit-ს შორის ყოველი მონაწილე ბლოკირებებს იჭერს, და თუ კოორდინატორი სწორედ იქ მოკვდება, ისინი გაჭედილნი რჩებიან — გაურკვევლობაში და მიუწვდომლად, სანამ ის არ დაბრუნდება.

2PC ატომურია, მაგრამ მბლოკავი — კოორდინატორის ავარია PREPARE-ის შემდეგ ყველა მონაწილეს აყინავს.

საგები — ატომურობა ხელმისაწვდომობაზე გაცვალეთ

საგა არის ლოკალური ტრანზაქციების თანმიმდევრობა, სადაც თითოეულს მაკომპენსირებელი ქმედება აქვს, რომელიც მას აზრობრივად აბრუნებს უკან. თუ ნაბიჯი ჩავარდა, დასრულებული ნაბიჯების კომპენსაციებს უკუღმა უშვებთ. არც გლობალური ბლოკირებები, არც ბლოკირება — მაგრამ არც იზოლაცია, ამიტომ უნდა დაგეგმოთ ისე, რომ შუალედური მდგომარეობები ხილვადი იქნება.

// each step pairs a forward action with its undo
const saga = [
  { do: reserveStock, undo: releaseStock },
  { do: chargeCard,   undo: refundCard },
  { do: bookCourier,  undo: cancelCourier },
]
// fail at step i → run undo for i-1 … 0, newest first
ორკესტრაცია — ცენტრალური ტვინი
// one coordinator drives the steps & compensations class OrderSaga { async run(o) { await this.step(reserveStock, releaseStock) await this.step(chargeCard, refundCard) } // + clear flow − a new coupling point }
ქორეოგრაფია — მხოლოდ მოვლენები
// services react to each other's events; no central brain on("order.placed", () => reserveStock()) on("stock.reserved",() => chargeCard()) on("charge.failed", () => releaseStock()) // + loosely coupled − flow is implicit, harder to trace

Outbox პატერნი — მოკალით ორმაგი ჩაწერა

საგები და მოვლენები გულისხმობს, რომ საიმედოდ შეგიძლიათ "განაახლოთ DB და გამოაქვეყნოთ შეტყობინება". ორივეს ატომურად ორ სისტემაში ვერ გააკეთებთ — მათ შორის ავარია და ისინი ერთმანეთს დაშორდება. ტრანზაქციული outbox მოვლენას იმავე მონაცემთა ბაზის ტრანზაქციაში წერს, რომელშიც მდგომარეობის ცვლილებაა, შემდეგ კი რელეი მას აგზავნის.

// ONE local transaction writes BOTH rows — atomic
await db.tx(async (t) => {
  await t.insert("orders", order)        // business state
  await t.insert("outbox", {            // event, same tx
    topic: "order.placed", payload: order,
  })
})
// a relay tails the outbox (poll or WAL/CDC) → broker → marks sent

მოვლენა მდგომარეობასთან ერთად ატომურად ფიქსირდება; რელეი მას სულ მცირე ერთხელ მიაწვდის — არასდროს დაკარგული ან ფანტომური მოვლენა.

იდემპოტენტურობის გასაღებები მასშტაბზე

სულ მცირე ერთხელ მიწოდება და კლიენტის ხელახალი მცდელობები ნიშნავს, რომ ყოველი ჩაწერა შეიძლება ორჯერ მოვიდეს. იდემპოტენტურობის გასაღები — კლიენტის მიერ მოწოდებული უნიკალური id თითო ლოგიკურ ოპერაციაზე — სერვერს საშუალებას აძლევს, გამეორება ამოიცნოს და იგივე შედეგი დააბრუნოს, ბარათის ხელახლა დახარჯვის ნაცვლად.

// client sends  Idempotency-Key: <uuid>  with the request
async function charge(key: string, amount: number) {
  const ok = await store.setNX(key, { status: "pending" }) // claim it
  if (!ok) return await store.wait(key)   // replay → saved result
  const result = await gateway.charge(amount)
  await store.set(key, { status: "done", result }, { ttl: "24h" })
  return result
}
04 · გეო-განაწილება და რეპლიკაცია 6 წთ

რეგიონებს შორის მტერი დატვირთვა არაა —
ეს სინათლის სიჩქარეა.

კონტინენტებს შორის შემოვლა ~100–200 ms-ია, რამდენიც არ უნდა დახარჯოთ — ეს ფიზიკაა, და არა ბიუჯეტი. გადადით მრავალ რეგიონზე და ყოველი სინქრონული გლობალური ჩაწერა ამ ბაჟს გადაიხდის. ამიტომ ირჩევთ: ან ჩაწერები ერთ რეგიონში ჩამწკრივოთ, ან რეგიონებმა დამოუკიდებლად წერონ და კონფლიქტები შეათანხმონ.

ულიდერო რეპლიკაცია (Dynamo-ს სტილში) — ნებისმიერი რეპლიკა ნებისმიერ ჩაწერას იღებს, ხოლო წაკითხვები/ჩაწერები ქვორუმებს იყენებენ, რომ ერთმანეთს გადაფარონ. N რეპლიკის დროს, თუ ჩაწერა W-ს ეხება და წაკითხვა R-ს, სადაც R + W > N, ყოველი წაკითხვა უახლეს ჩაწერას კვეთს — რეგულირებადი თანმიმდევრულობა ერთი ლიდერის გარეშე, რომელსაც გადართვა დასჭირდებოდა.
R1
R2
R3
ჩაწერა
w+r
წაკითხვა
N = 3 · W = 2 · R = 2
R2 ორივე ნაკრებშია → წაკითხვა ხედავს ჩაწერას ✓
R + W > N გადაფარვას აიძულებს

რადგან R + W > N, წაკითხვის ქვორუმს ყოველთვის აქვს საერთო კვანძი ჩაწერის ქვორუმთან — ანუ ახალი მნიშვნელობა ყოველთვის მისაწვდომია.

როგორ შევინარჩუნოთ რეპლიკები პატიოსნად

  • დაუდევარი ქვორუმი + მინიშნებით გადაცემა — თუ საკუთარი რეპლიკა მიუწვდომელია, დამნაცვლებელს მიწერეთ, რომელიც მინიშნებას ინახავს და მოგვიანებით გადასცემს. ხელმისაწვდომობა სიმკაცრეზე წინაა.
  • წაკითხვისას შეკეთება — წაკითხვა, რომელიც ერთმანეთს დაუთანხმებელ რეპლიკებს ხედავს, ადგილზევე აბრუნებს უახლეს მნიშვნელობას.
  • ანტი-ენტროპია — ფონური სინქრონიზაცია რეპლიკებს მერკლის ხეებით ადარებს, რომ განსხვავება იაფად იპოვოს და განკურნოს.
  • დაარეგულირეთ თითო გამოძახებაზე: W=N უსაფრთხო ჩაწერისთვის, R=1 სწრაფი წაკითხვისთვის, ან ორივე დააბალანსეთ.

როცა ორი რეგიონი ერთსა და იმავე გასაღებში წერს

ლიდერის გარეშე პარალელური ჩაწერები აუცილებლად დაუპირისპირდება ერთმანეთს. გჭირდებათ დეტერმინისტული, რიგისგან დამოუკიდებელი გზა მათი შერწყმისთვის — თორემ მონაცემებს უხმაუროდ დაკარგავთ.

ბოლო ჩაწერა იგებს

შეინახეთ ის ჩაწერა, რომელსაც უმაღლესი დროის ნიშნული აქვს. სასაცილოდ მარტივია — და მეორე პარალელურ ჩაწერას ყრის. ქეშებისა და ონლაინ სტატუსისთვის კარგია; საშიშია ყველაფრისთვის, რასაც ვერ დაკარგავთ.

ვექტორული საათები

თითო კვანძის ვერსიის ვექტორი ყოველ ჩაწერას ნიშნავს, ისე რომ საცავს შეუძლია პარალელურობის აღმოჩენა (არცერთი არაა მეორეზე ადრე მომხდარი) და კონფლიქტების ამოტანა — შემდეგ კი აპლიკაციის ლოგიკა ან შერწყმა წყვეტს.

CRDT-ები

მონაცემთა ტიპები, რომელთა შერწყმის დამთხვევაც მათემატიკურად გარანტირებულია — არც ცენტრალური არბიტრი, არც დაკარგული განახლებები. ყველაზე ძლიერი პასუხი, როცა ის თქვენს მონაცემებს ერგება.

CRDT — Conflict-free Replicated Data Type — სტრუქტურა, რომლის შერწყმაც კომუტაციური, ასოციაციური და იდემპოტენტურია, ამიტომ რეპლიკები, რომლებიც იმავე განახლებებს ნებისმიერი რიგით, თუნდაც ორჯერ, გამოიყენებენ, ერთსა და იმავე მდგომარეობამდე მიდიან. მდგომარეობაზე დაფუძნებული CRDT-ები მთელ მდგომარეობებს აგზავნიან და ერწყმიან (გაერთიანება ნახევრადმესერზე); ოპერაციაზე დაფუძნებულები კომუტაციურ ოპერაციებს აგზავნიან. მთვლელებს, სიმრავლეებს (OR-Set), რეგისტრებს და კოლაბორაციულ ტექსტსაც კი (RGA) აქვს CRDT ფორმა.
// G-Counter: each node owns a tally; merge = element-wise max function merge(a, b) { const out = {} for (const node of keysOf(a, b)) out[node] = Math.max(a[node] ?? 0, b[node] ?? 0) return out // commutative · associative · idempotent } const value = (c) => sum(valuesOf(c)) // total = sum of tallies
რეპლიკა A
{a:3, b:1}
რეპლიკა B
{a:1, b:4}
{a:3, b:4} = 7
მაქს. კვანძზე

თითო კვანძზე მაქსიმუმის აღება არაფერს კარგავს და ერთსა და იმავე შედეგამდე მიდის, როგორი რიგითაც არ უნდა გადაიკვეთოს შერწყმები — სწორედ ესაა CRDT-ის გარანტია.

ინსტრუმენტების ლანდშაფტი — გლობალურად განაწილებული მონაცემთა ბაზები

truetime · SQL

Google Spanner

გლობალურად გარეგნულად თანმიმდევრული SQL, დალაგებული TrueTime-ით (GPS + ატომური საათები).

  • დადებითი: ლინეარიზებადი ტრანზაქციები რეგიონებს შორის, ნამდვილი SQL-ითა და ჰორიზონტალური მასშტაბირებით.
  • უარყოფითი: მართული GCP პროდუქტი — ვენდორზე მიბმული და შესაბამისად ფასიანი.
HLC · postgres-wire

CockroachDB

Spanner-ის იდეა ატომური საათების გარეშე — ჰიბრიდული ლოგიკური საათები და serializable იზოლაცია, Postgres-თან თავსებადი.

  • დადებითი: თვითჰოსტი ან ღრუბელი, Postgres-ის wire პროტოკოლი, ნაგულისხმევად serializable.
  • უარყოფითი: რეგიონებს შორის ჩაწერები კვლავ იხდიან ქვორუმის შეყოვნებას; ბირთვი ახლა შემზღუდველი ლიცენზიის ქვეშაა.
ულიდერო · AP

Cassandra / Dynamo

ულიდერო, რეგულირებადი თანმიმდევრულობის ფართო-სვეტოვანი / KV საცავები, აგებული ჩაწერის მოცულობისთვის.

  • დადებითი: უზარმაზარი ჩაწერის გამტარუნარიანობა, მუდამ ჩართული, დიზაინითვე მრავალრეგიონული.
  • უარყოფითი: ნაგულისხმევად eventual, არანაირი join — ცხრილებს ზუსტი შეკითხვის გზების გარშემო აწყობთ.

როგორ ავირჩიო: გჭირდებათ გლობალური ძლიერი თანმიმდევრულობა SQL-ით და GCP-ზე ხართ → Spanner. გინდათ იგივე, ოღონდ პორტატული და Postgres-ის ფორმის → CockroachDB. გჭირდებათ ჩაწერის უკიდურესი მასშტაბი და რეგულირებად/eventual თანმიმდევრულობას იტანთ → Cassandra ან Dynamo-ს სტილის საცავი.

05 · სიხშირის ლიმიტი და უკუწნევა 5 წთ

დაიცავით სისტემა გამომძახებლებისგან —
და საკუთარი თავისგან.

უსაზღვრო მოთხოვნა ყოველთვის იპოვის თქვენს ყველაზე სუსტ კომპონენტს. სიხშირის ლიმიტი ზღუდავს იმას, რისი გამოგზავნაც გამომძახებლებს შეუძლიათ; უკუწნევა გაჯერებულ კომპონენტს აძლევს საშუალებას, ზემოთ მდგარებს შენელება სთხოვოს — იმის ნაცვლად, რომ ჩუმად აგროვოს რიგი, სანამ არ ჩავარდება.

Token bucket — B ტევადობის ვედრო წამში r ტოკენით ივსება; ყოველი მოთხოვნა ერთ ტოკენს ხარჯავს, ცარიელი ვედრო კი უარყოფას ნიშნავს. ის უშვებს მოკლე ნახტომებს B-მდე და გრძელვადიან საშუალოს r-ზე ინარჩუნებს — ნაგულისხმევი ლიმიტერი, რადგან იაფია, სამართლიანი და ნახტომებს იტანს.
-- atomic token bucket on Redis (Lua = one round trip) local tokens = tonumber(redis.call("GET", k) or cap) tokens = math.min(cap, tokens + elapsed * rate) -- refill if tokens < 1 then return 0 end -- reject redis.call("SET", k, tokens - 1, "EX", ttl) -- consume return 1 -- allow
cap = B refill r/s allow ✓ empty → 429

ტოკენები r ტემპით წვეთავს; ნახტომი ვედროს ცლის, შემდეგ კი გამომძახებლები შევსების ტემპამდე ნელდებიან.

ფანჯარა

ფიქსირებული ფანჯარა

დათვალეთ მოთხოვნები საათის ყოველ წუთზე. ტრივიალურია, მაგრამ საზღვარზე გადამჯდარ 2× ნახტომს უშვებს — ორი სრული ფანჯარა ზედიზედ.

ფანჯარა · ლოგი

მოცურავე ფანჯრის ლოგი

შეინახეთ ყოველი მოთხოვნის დროის ნიშნული (მაგალითად, Redis-ის sorted set-ში) და ბოლო 60 წამი ზუსტად დათვალეთ. ზუსტია, მაგრამ მეხსიერება ტრაფიკთან ერთად იზრდება.

ფანჯარა · მთვლელი

მოცურავე ფანჯრის მთვლელი

წინა ფანჯრის რაოდენობა აწონეთ იმით, რამდენად შორს ხართ მიმდინარეში. საზღვრის ნახტომს O(1) მეხსიერებით აგლუვებს — ჩვეულებრივი არჩევანი პროდაქშენში.

ინსტრუმენტების ლანდშაფტი — განაწილებული ლიმიტერი Redis-ზე

ერთი კვანძის მეხსიერებაში მყოფი მთვლელი მთელ ფლოტზე არ მუშაობს — გჭირდებათ საერთო, ატომური საცავი. Redis ნაგულისხმევია: ქვემოთ მოცემული ყოველი მიდგომა ერთ ატომურ ოპერაციად სრულდება, ამიტომ პარალელური მოთხოვნები ლიმიტს ვერ გაუსწრებენ.

INCR + EX

ატომური მთვლელი

  • დადებითი: ორი ბრძანება, ტრივიალური — INCR თითო ფანჯრის გასაღებზე, TTL-ით.
  • უარყოფითი: ეს ფიქსირებული ფანჯარაა, ამიტომ საზღვრის ნახტომის ხარვეზს იმემკვიდრეობს.
ZSET

sorted set-ის ფანჯარა

  • დადებითი: ზუსტი მოცურავე ფანჯარა — ძველი დროის ნიშნულები მოაჭერით, დანარჩენი დათვალეთ.
  • უარყოფითი: მეხსიერება და CPU თითო გასაღებზე მოთხოვნების მოცულობასთან ერთად იზრდება.
Lua სკრიპტი

Token bucket

  • დადებითი: ნახტომები + O(1) მეხსიერება, ყველაფერი ატომურად, ერთ შემოვლაში.
  • უარყოფითი: შესანარჩუნებელი სკრიპტი და ცენტრალურ Redis-ზე გადასვლა ყოველ მოთხოვნაზე.

როგორ ავირჩიო: ნაგულისხმევად აიღეთ Lua-ს token bucket — ნახტომებიანი, O(1), ატომური. დაეშვით უბრალო INCR-ზე, როცა უხეში ჭერიც საკმარისია, ხოლო sorted-set ფანჯარა მხოლოდ მაშინ გამოიყენეთ, როცა მართლა ზუსტი რაოდენობები გჭირდებათ. ძალიან მაღალ RPS-ზე ლიმიტი ლოკალურად დააწესეთ, თითო კვანძის მცირე ბიუჯეტით, და პერიოდულად დაასინქრონეთ Redis-თან — ცოტა სიზუსტეს შეყოვნებაზე გაცვლით.

უკუწნევა — გაჯერება ზემოთ გაუშვით

  • შეზღუდული რიგები. უსაზღვრო ბუფერი გადატვირთვას უბრალოდ მალავს OOM-მდე. შეზღუდული, რომელიც სავსეობისას უარყოფს, თავად არის სიგნალი.
  • ლიმიტი edge-ზე გაიტანეთ. უარყავით ადრე, გეითვეიზე, და არა მაშინ, როცა სამუშაო სიღრმეში უკვე ნახევრადაა შესრულებული.
  • დატვირთვის ჩამოშორება > ჩამონგრევა. დაბალპრიორიტეტული ტრაფიკის ჩამოგდება დანარჩენს ჯანმრთელად ინარჩუნებს — თანდათანობითი დეგრადაცია სრულ გათიშვას სჯობს.
producer bounded worker full → slow down shed excess

სავსე შეზღუდული რიგი უკუწნევის სიგნალია: შეანელეთ პროდიუსერი ან ჩამოიშორეთ ის, რაც არ ეტევა.

06 · საიმედოობის პატერნები 5 წთ

ერთმა ნელმა დამოკიდებულებამ
მთელი სისტემა არ უნდა ჩააგდოს.

განაწილებულ სისტემაში ჩავარდნები კასკადურად ვრცელდება: ნელი ქვედა სერვისი თრედებს იკავებს, ეს თრედები გამომძახებელს აჩერებენ, გაჩერება კი სტეკზე მაღლა ადის, სანამ ყველაფერი "ჩავარდნილი" არ გახდება. ეს ოთხი პატერნი აფეთქების რადიუსს ზღუდავს.

Circuit breaker — შეფუთეთ სარისკო გამოძახება ისე, რომ საკმარისი ჩავარდნების შემდეგ ის "გაითიშოს" და ლოდინის ნაცვლად სწრაფად ჩავარდეს. სამი მდგომარეობა: closed (გამოძახებები მიდის, ჩავარდნები ითვლება), open (მყისიერი უარყოფა გაგრილების პერიოდში), half-open (ერთი შესამოწმებელი გამოძახება გაატარეთ — გამოვიდა, იხურება; ჩავარდა, ისევ იხსნება). ის ავადმყოფ დამოკიდებულებას თქვენი თრედების დაცლის საშუალებას არ აძლევს.
// closed → open → half-open → closed if (state === "open") { if (now < openedAt + cooldown) throw CircuitOpen // fail fast state = "half-open" // allow one probe } try { const r = await call(); onSuccess(); return r // close on success } catch (e) { if (++fails >= threshold) trip("open"); throw e }
closed open half- open fails ≥ N cooldown probe ✓

N ჩავარდნის შემდეგ იხსნება; გაგრილების შემდეგ ერთი half-open შემოწმება წყვეტს, დაიხუროს თუ არა ისევ.

იზოლაცია

ტიხრები (bulkheads)

მიეცით ყოველ დამოკიდებულებას საკუთარი თრედების/კავშირების პული, რომ ერთმა დატბორილმა დამოკიდებულებამ დანარჩენები არ დააშიმშილოს — როგორც წყალგაუმტარი განყოფილებები გემის კორპუსში. ნელი სერვისი საკუთარ ტიხარში იხრჩობა და არა მთელ გემში.

უსაფრთხო გამეორება

გამეორებები + jitter

გაიმეორეთ მხოლოდ იდემპოტენტური გამოძახებები, ექსპონენციალური backoff-ითა და jitter-ით, რომ ათასმა კლიენტმა ერთდროულად არ გაიმეოროს და აღდგენად სერვისს ხელახლა არ დაეცეს ხროვად. საერთო მცდელობები შეზღუდეთ გამეორებების ბიუჯეტით.

დეგრადაცია

დატვირთვის ჩამოშორება

გაჯერებისას ჩამოაგდეთ ყველაზე ნაკლებად მნიშვნელოვანი სამუშაო — დაშვების კონტროლი კარებთანვე. მომსახურებული გადახდა და ჩამოგდებული ანალიტიკის პინგი ორივეს ჩავარდნას სჯობს. დააწყვილეთ ყოველ გამოძახებაზე ტაიმაუტთან/ვადასთან.

გულუბრყვილო გამეორება — სინქრონული ხროვა
// every client waits the SAME fixed delay for (let i = 0; i < 5; i++) { try { return await call() } catch { await sleep(1000) } // all retry together → re-flood }
Backoff სრული jitter-ით
for (let i = 0; i < 5; i++) { try { return await call() } catch { const cap = Math.min(MAX, BASE * 2 ** i) // exponential await sleep(Math.random() * cap) // full jitter } }
07 · გლობალური დიზაინი + შეჯამება 5 წთ

ავაწყოთ ერთად:
გლობალური საგადახდო რეესტრი.

მთელი მსოფლიოს მომხმარებლები ანგარიშებს შორის ფულს გადარიცხავენ. მკაცრი ინვარიანტი: არცერთი ბალანსი არასდროს ხდება უარყოფითი და ფული არც იქმნება, არც იკარგება — რეგიონებს, ხელახალ მცდელობებსა და გახლეჩებს შორისაც კი. ამ დეკის ყოველი ინსტრუმენტი თავის ადგილს იმსახურებს.

ნაბიჯი 1 · ინვარიანტი

ლინეარიზებადი იქ, სადაც საჭიროა

თავად რეესტრი ერთადერთი ადგილია, სადაც ძლიერი თანმიმდევრულობაა საჭირო. ბალანსები გლობალურად თანმიმდევრულ SQL საცავში მოათავსეთ (Spanner / CockroachDB), რომ დებეტი და კრედიტი ერთ serializable ტრანზაქციაში დაფიქსირდეს. დანარჩენი ყველაფერი უფრო სუსტიც შეიძლება იყოს.

ნაბიჯი 2 · სერვისებში

საგა + outbox, არასდროს 2PC

გადარიცხვა მოიცავს რეესტრს, თაღლითობის შემოწმებისა და შეტყობინებების სერვისებს. გაუშვით ის საგად, კომპენსაციებით, და ყოველი მოვლენა ტრანზაქციული outbox-ით გამოუშვით, რომ ავარიამ ნაბიჯი არასდროს დაკარგოს ან გააორმაგოს.

ნაბიჯი 3 · გამეორებები

იდემპოტენტურობა შესასვლელთან

კლიენტი ყოველ გადარიცხვაზე იდემპოტენტურობის გასაღებს აგზავნის. ტაიმაუტის შემდეგ ხელახალი მცდელობა შენახულ შედეგს აბრუნებს — ფული ზუსტად ერთხელ გადაინაცვლებს, რამდენჯერაც არ უნდა მოვიდეს მოთხოვნა.

ნაბიჯი 4 · დატვირთვა და ჩავარდნები

ლიმიტები, breaker-ები, უკუწნევა

token bucket ლიმიტერი გეითვეიზე ბოროტად გამოყენებას ზღუდავს; circuit breaker-ები და ტიხრები თაღლითობის სერვისს იზოლირებს; backoff jitter-ით და დატვირთვის ჩამოშორება პიკს კასკადში გადაზრდის საშუალებას არ აძლევს.

ერთი კომპრომისი, რომელიც უნდა დაასახელოთ: რეესტრი CP-ია — გახლეჩისას უმცირესობის რეგიონი ჩაწერებზე უარს ამბობს, ვიდრე ორმაგი ხარჯვის რისკს აიღებდეს. სამაგიეროდ ანგარიშის ფიდი და შეტყობინებები AP-ია და CRDT-ით შერწყმული რეპლიკებიდან მოდის. ერთი და იგივე პროდუქტი, თანმიმდევრულობის ორი საფეხური, თითო ინვარიანტზე არჩეული. სწორედ ეს არჩევანი — და არა რომელიმე ერთი ინსტრუმენტი — არის სენიორის უნარი.

ხუთი წესი, რომელიც თან უნდა წაიღოთ

1თანმიმდევრულობა რეგულატორია და თითო ინვარიანტზე ეწყობა. ძლიერი მხოლოდ იქ, სადაც არასწორი პასუხი მიუღებელია; დანარჩენ ადგილას — სესიის გარანტიები + eventual.
2შეთანხმება ქვორუმი ღირს. კონსენსუსი (Raft/Paxos) არის ის, როგორ თანხმდებიან მანქანები ჩავარდნის პირობებში — იქირავეთ (etcd), ნაცვლად იმისა, რომ ააგოთ.
3ნუ გაწელავთ ტრანზაქციას სერვისებზე. ბლოკირებადი 2PC-ის ნაცვლად გამოიყენეთ საგები + outbox + იდემპოტენტურობის გასაღებები.
4რეგიონებს შორის შერწყმა დაგეგმეთ. ქვორუმები თანმიმდევრულობას არეგულირებს; CRDT-ები ლიდერის გარეშე ერთდება; LWW მონაცემებს ჩუმად კარგავს.
5ჩავარდნა განზრახ შემოსაზღვრეთ. სიხშირის ლიმიტები, უკუწნევა, breaker-ები, ტიხრები და jitter-იანი გამეორებები კასკადს თანდათანობით დეგრადაციად აქცევს.
ცოდნის შემოწმება

დაგამახსოვრდათ?

ხუთი შეკითხვა თანმიმდევრულობაზე, კონსენსუსზე, განაწილებულ ტრანზაქციებზე, გეო-რეპლიკაციასა და საიმედოობაზე — მყისიერი პასუხი, ავტორიზაციის გარეშე.

შეაფასეთ ეს დასტა
იყავით პირველი

ნავიგაცია ← → ღილაკებით ან სქროლით · უკან ბიბლიოთეკაში