সরাসরি পর্বে যান
পর্ব ৫৩ · ৬৪-এর মধ্যে · মডিউল ৮

Distributed system-এর OS-দিক — RPC, clock আর distributed file system

Distributed Systems from the OS Side — RPC Semantics, Physical and Logical Clocks, Mutual Exclusion, Election and Distributed File Systems

Taught in Bangla, this lesson looks at distributed systems from the OS side: network OS, distributed OS and middleware, RPC and its failure semantics, idempotency and retries, physical clock synchronization with Cristian's algorithm, NTP and Berkeley, logical clocks, distributed mutual exclusion, leader election, distributed file systems such as NFS, AFS and GFS/HDFS, and failure detection and consensus.

এই পর্বে যা শিখবেন

  • কেন একাধিক কম্পিউটার জোড়া হয়, আর partial failure, ঘড়ির অভাব ও “distributed computing-এর আটটি ভ্রান্তি” কেন কাজটি কঠিন করে
  • Network OS, distributed OS আর middleware-এর পার্থক্য; Amoeba, Sprite, Plan 9-এর শিক্ষা
  • RPC-র ভেতর — stub, marshalling, binding, ব্যর্থতার প্রতিটি ঘটনা, semantics আর idempotency; C-তে retry আর duplicate বাদ দেওয়া
  • Clock drift-এর হিসাব, Cristian, NTP আর Berkeley-র numerical; Lamport আর vector clock — নিয়ম, প্রমাণ, হাতে-কলমে উদাহরণ
  • Distributed mutual exclusion, bully ও ring election, NFS/AFS/GFS, consistency semantics, heartbeat আর consensus-এর ভূমিকা

১ · কেন একাধিক কম্পিউটার, আর কেন তা কঠিন

আপনি bKash বা ব্যাংকের অ্যাপে টাকা পাঠালেন। স্ক্রিনে ঘুরছে “অপেক্ষা করুন…”, তারপর লেখা এলো “Request timed out”. টাকা কি গেছে? আপনি জানেন না। অ্যাপও নিশ্চিত জানে না। আবার চাপলে কি দুবার কাটবে? এই একটি প্রশ্নের ভেতরেই পুরো distributed system-এর কঠিনতা লুকিয়ে আছে।

এ পর্যন্ত কোর্সে আমরা একটি কম্পিউটারের OS দেখেছি। সেখানে প্রসেসগুলো একই RAM, একই ঘড়ি আর একই kernel ভাগ করে। একটি distributed system হলো নেটওয়ার্কে যুক্ত অনেকগুলো স্বাধীন কম্পিউটার, যারা শুধু বার্তা পাঠিয়ে একসাথে কাজ করে। কেন এমন করা হয়?

সম্পদ ভাগ

একটি প্রিন্টার, একটি ফাইল সার্ভার বা একটি ডেটাবেস — অফিসের সবাই ব্যবহার করেন, প্রত্যেকের টেবিলে আলাদা কপি লাগে না।

NFS, Windows ফাইল শেয়ার

গতি

একটি বড় কাজ টুকরো করে একশো যন্ত্রে একসাথে চালানো — ঠিক যেন একটি গার্মেন্টসের কাটিং, সেলাই আর প্যাকিং লাইন একসাথে চলা।

MapReduce, Spark

নির্ভরযোগ্যতা

একটি যন্ত্র নষ্ট হলে আরেকটি কাজ ধরে নেয়। লোডশেডিংয়ে একটি ডেটা সেন্টার বন্ধ হলে অন্য শহরেরটি চলে।

replication, failover

মাপে বাড়ানো (scale)

গ্রাহক দশগুণ হলে একটি বিশাল যন্ত্র কেনার বদলে আরও সাধারণ যন্ত্র যোগ করা। একটি যন্ত্রের সীমা আছে, যন্ত্রের সংখ্যার সীমা অনেক দূরে।

ক্লাউড, ক্লাস্টার

দাম কী? এক কম্পিউটারে যা বিনামূল্যে পাওয়া যেত, এখানে সেগুলো আর নেই:

  • Shared memory নেই। পর্ব ১২-এর shared memory বা পর্ব ২৪-এর mutex একটি RAM-এর ওপর দাঁড়ানো। এখানে আছে শুধু message passing — আর বার্তা দেরিতে আসতে পারে, হারাতে পারে, দুবার আসতে পারে।
  • সবার জন্য এক ঘড়ি নেই। প্রতিটি যন্ত্রের নিজের ঘড়ি, আর সেগুলো আলাদা গতিতে চলে। “কোনটি আগে ঘটেছে” প্রশ্নের উত্তর দিতে আলাদা কৌশল লাগে (অংশ ৫ ও ৬)।
  • ব্যর্থতা আংশিক। এক কম্পিউটার crash করলে সব একসাথে থামে। এখানে একটি যন্ত্র মরে, বাকিরা চলে। আর বাইরে থেকে “মৃত”, “খুব ধীর” আর “নেটওয়ার্ক কাটা” — তিনটি দেখতে হুবহু এক: উত্তর আসছে না। একেই বলে partial failure.

ঢাকা আর চট্টগ্রামের দুটি শাখা অফিস শুধু চিঠি দিয়ে কথা বলে। চিঠি কখনো দেরিতে যায়, কখনো হারায়। দুই অফিসের দেয়ালঘড়ি কয়েক মিনিট আলাদা। আর চট্টগ্রাম থেকে চিঠির উত্তর না এলে ঢাকা জানে না — ওদিকে অফিস বন্ধ, পিয়ন পথে আটকে, নাকি উত্তর লেখা হয়ে পথে হারিয়েছে। Distributed system-এর প্রতিটি algorithm এই অফিস দুটির সমস্যারই সমাধান।

Distributed computing-এর আটটি ভ্রান্তি

১৯৯০-এর দশকে Sun Microsystems-এ L Peter Deutsch ও সহকর্মীরা কয়েকটি ভুল অনুমানের তালিকা করেন, যা নতুন distributed প্রোগ্রামাররা প্রায় সবাই করেন; প্রচলিত বর্ণনা অনুযায়ী অষ্টমটি পরে James Gosling যোগ করেন (তালিকার ইতিহাসের খুঁটিনাটি সূত্রভেদে একটু আলাদা)। একে বলা হয় fallacies of distributed computing:

ভুল অনুমানবাস্তবে কী ঘটে
১. নেটওয়ার্ক নির্ভরযোগ্যপ্যাকেট হারায়, সুইচ রিস্টার্ট হয়, তার কাটা পড়ে
২. Latency শূন্যএকই ডেটা সেন্টারে প্রায় এক মিলিসেকেন্ডের কাছাকাছি বা কম, মহাদেশ পার হলে শত মিলিসেকেন্ড পর্যন্ত — RAM-এর ন্যানোসেকেন্ডের তুলনায় লক্ষ গুণ বেশি
৩. Bandwidth অসীমবড় ফাইল কপি করলে অন্যদের ট্রাফিক আটকায়
৪. নেটওয়ার্ক নিরাপদমাঝপথে কেউ পড়তে বা বদলাতে পারে — TLS লাগে
৫. Topology বদলায় নাসার্ভার যোগ হয়, সরে যায়, IP বদলায়
৬. একজনই administratorআপনার সার্ভার, ক্লাউড কোম্পানির নেটওয়ার্ক, অন্য দলের firewall
৭. পরিবহনের খরচ শূন্যmarshalling-এ CPU লাগে, ক্লাউডে ডেটা বের করতে টাকা লাগে
৮. নেটওয়ার্ক একরকম (homogeneous)ভিন্ন OS, ভিন্ন CPU byte order, ভিন্ন version

২ · Network OS, distributed OS আর middleware

অনেক যন্ত্রকে একসাথে চালানোর দায়িত্ব কে নেবে — OS নিজে, নাকি OS-এর ওপরের একটি স্তর? ইতিহাসে তিনটি উত্তর এসেছে:

ধরনমূল ধারণাব্যবহারকারী কী দেখেনউদাহরণ
Network OSপ্রতিটি যন্ত্রে নিজের পূর্ণ OS; নেটওয়ার্কে ফাইল ভাগ, দূরে loginঅনেকগুলো আলাদা কম্পিউটার — কোনটি কোথায়, জানতে হয়Linux + SSH + NFS, Windows ফাইল শেয়ার
Distributed OSসব যন্ত্র মিলে একটি kernel-এর মতো আচরণ; প্রসেস যেকোনো যন্ত্রে চলে বা সরেএকটি বিশাল কম্পিউটার (single system image)Amoeba, Sprite, Plan 9 (গবেষণা)
Middlewareসাধারণ OS-এর ওপর একটি সফটওয়্যার স্তর, যা যোগাযোগ, নাম আর সমন্বয় দেয়প্রোগ্রামার একটি সাধারণ interface দেখেনgRPC, Kafka, Kubernetes, Hadoop

১৯৮০-এর দশক ছিল distributed OS গবেষণার সোনালি সময়। আমস্টারডামের Vrije Universiteit-এ Andrew Tanenbaum-এর দল বানায় Amoeba — একটি microkernel-ভিত্তিক সিস্টেম, যেখানে প্রসেসর-পুলের যেকোনো যন্ত্রে প্রোগ্রাম চলত; Guido van Rossum বলেছেন, Python-এর শুরু হয়েছিল Amoeba-র জন্য একটি স্ক্রিপ্টিং ভাষা হিসেবে। UC Berkeley-তে John Ousterhout-এর Sprite চলন্ত প্রসেসকে অন্য যন্ত্রে সরাতে পারত (process migration) আর বড় ফাইল cache রাখত; এখান থেকেই এসেছে log-structured file system-এর ধারণা। Bell Labs-এ Rob Pike, Ken Thompson ও সহকর্মীদের Plan 9 (১৯৯০-এর দশকের শুরুতে বাইরে আসে) Unix-এর “সবকিছু ফাইল” নীতিকে নেটওয়ার্কে নিয়ে যায়: দূরের CPU, স্ক্রিন, এমনকি নেটওয়ার্ক স্ট্যাকও একটি file system, যা 9P protocol দিয়ে mount হয়, আর প্রতিটি প্রসেসের নিজের namespace থাকে। UTF-8 encoding-ও প্রথম ব্যবহার হয় Plan 9-এ।

এগুলোর কোনোটিই ব্যাপক বাজার পায়নি — সাধারণ Unix/Linux আর Windows-এর ওপর middleware-ই জিতেছে (ভাবনার প্রশ্ন ৫-এ কারণ)। কিন্তু ধারণাগুলো বেঁচে আছে: Linux-এর per-process mount namespace (পর্ব ৫১-এর container-এর ভিত্তি) Plan 9-এর namespace থেকে অনুপ্রাণিত, আর 9P আজও Linux kernel-এ (v9fs) আছে; WSL-এ Windows থেকে Linux-এর ফাইল খোলার পথেও 9P ব্যবহার হয় বলে Microsoft জানিয়েছে। আজকের Kubernetes-কে অনেকে “ডেটা সেন্টারের OS” বলেন — কাজের ধরনে এটি middleware, কিন্তু scheduling, resource limit আর service discovery-র মতো OS-এর দায়িত্বই পালন করে।

৩ · RPC — দূরের ফাংশনকে কাছের মতো ডাকা

পর্ব ১২-এ (IPC — socket) দেখেছেন, দুই প্রসেস socket দিয়ে বাইট পাঠাতে পারে, আর একটি TCP socket একই কোড দিয়ে অন্য মেশিনেও পৌঁছায়। কিন্তু প্রতিটি অনুরোধের জন্য নিজে বাইট সাজানো, পাঠানো, উত্তর খোলা — এটি ক্লান্তিকর আর ভুলপ্রবণ। ১৯৮৪ সালে Andrew Birrell আর Bruce Nelson-এর বিখ্যাত গবেষণাপত্র এর একটি পরিচ্ছন্ন রূপ দেয়: remote procedure call. প্রোগ্রামার লেখেন balance = deposit(acct, 500); — যেন সাধারণ ফাংশন। ভেতরে কী হয়, ধাপে ধাপে দেখুন:

  1. Client প্রোগ্রামdeposit(1207, 500)সাধারণ কল
  2. Client stubmarshalবাইটে সাজানো
  3. দুই OS + নেটওয়ার্কsend / recvsocket, TCP
  4. Server stubunmarshalবাইট থেকে argument
  5. Server ফাংশনdeposit()আসল কাজ
বার্তা (big-endian, XDR-এর মতো)argument এখন শুধু বাইট
xid  = 00 00 00 29      ; request id 41
proc = DEPOSIT (v1)
acct = 00 00 04 b7      ; 1207
amt  = 00 00 01 f4      ; 500
Client-এ ফেরতstub উত্তর খুলে return করে
reply xid = 41, status = OK, balance = 1500
; client প্রোগ্রাম দেখে: balance = 1500
  1. সাধারণ ফাংশন কল। Client প্রোগ্রাম deposit(1207, 500) ডাকল। সে জানে না ফাংশনটি অন্য মেশিনে আছে — এটাই RPC-র “স্বচ্ছতা”। আসলে সে ডাকছে একটি স্থানীয় নকল ফাংশন, যার নাম client stub.
  2. Stub argument সাজায়। Stub প্রতিটি argument একটি নির্দিষ্ট ফর্ম্যাটে বাইটে লেখে — এর নাম marshalling. দুই মেশিনের byte order আলাদা হতে পারে (x86 little-endian), তাই নিয়ম নির্দিষ্ট: Sun-এর ONC RPC-র XDR সবসময় big-endian. সাথে যায় একটি request id (xid), যা পরে duplicate চেনায় কাজে লাগবে। Pointer পাঠানো যায় না — অন্য মেশিনে সেই ঠিকানার কোনো মানে নেই; তাই যে ডেটায় pointer দেখায়, সেটিই কপি করে পাঠাতে হয়।
  3. OS পাঠায়। Stub একটি system call (send/write) দিয়ে বার্তা kernel-এ দেয়; kernel-এর TCP/IP stack সেটি নেটওয়ার্কে পাঠায়, অন্য প্রান্তের kernel গ্রহণ করে socket-এ রাখে। এখান থেকেই দেরি আর হারানোর সব ঝুঁকি শুরু।
  4. Server stub খোলে। Server-এর stub (কোনো কোনো বইয়ে “skeleton”) বাইট থেকে আবার int বানায় (unmarshalling), কোন ফাংশন ডাকতে হবে তা দেখে।
  5. আসল কাজ। Server-এর আসল deposit() চলে, balance ১০০০ থেকে ১৫০০ হয়। খেয়াল করুন — কাজটি এখানেই হয়ে গেছে, উত্তর client-এ পৌঁছাক বা না পৌঁছাক।
  6. উত্তরের ফেরার পথ। একই পথে উল্টো দিকে: server stub ফল marshal করে, নেটওয়ার্ক বহন করে, client stub খুলে return 1500 করে। পুরো যাত্রায় হয়তো এক মিলিসেকেন্ড, যেখানে একটি স্থানীয় ফাংশন কলে লাগে কয়েক ন্যানোসেকেন্ড।

Stub হাতে লিখতে হয় না। প্রোগ্রামার একটি interface definition language (IDL)-এ ফাংশনের নাম আর argument-এর ধরন লেখেন, আর একটি টুল দুই দিকের stub তৈরি করে — ONC RPC-তে rpcgen, gRPC-তে Protocol Buffers-এর .proto ফাইল থেকে protoc.

Binding — server কোথায়?

Client-কে জানতে হবে কোন মেশিনের কোন port-এ server শুনছে। এই খোঁজার নাম binding. ONC RPC-তে প্রতিটি মেশিনে একটি rpcbind (পুরোনো নাম portmapper) port 111-এ বসে থাকে; server চালু হয়ে সেখানে নাম লেখায়, client আগে তাকে জিজ্ঞেস করে। Microsoft RPC-তে (DCE RPC থেকে আসা) একই কাজ করে endpoint mapper (port 135). আধুনিক microservice-এ binding সাধারণত DNS বা Kubernetes-এর service নামের মাধ্যমে হয়।

৪ · RPC যখন ব্যর্থ হয় — semantics, idempotency আর retry

স্থানীয় ফাংশন হয় ফেরে, নয় পুরো প্রোগ্রাম crash করে। RPC-তে মাঝামাঝি অনেক অবস্থা আছে। Client শুধু দেখে “নির্দিষ্ট সময়ে (timeout) উত্তর আসেনি” — কিন্তু কারণ পাঁচরকম হতে পারে:

কী ঘটেছেকাজটি কি হয়েছে?আবার পাঠালে
অনুরোধ পথে হারিয়েছেনানিরাপদ, দরকারি
Server কাজের আগেই crashনানিরাপদ (server ফিরলে)
Server কাজের পরে, উত্তরের আগে crashহ্যাঁদুবার হতে পারে
উত্তর পথে হারিয়েছেহ্যাঁদুবার হতে পারে
Server শুধু ধীরহয়তো এখনো চলছেদুবার হতে পারে

আরেকটি ঘটনা উল্টো দিকের: client নিজে crash করলে server-এ তার পাঠানো কাজ চলতেই থাকে, কেউ ফলের অপেক্ষায় নেই — একে বলে orphan. এই অনিশ্চয়তার মুখে RPC system তিনটি প্রতিশ্রুতির একটি দেয়:

At-least-once

উত্তর না পাওয়া পর্যন্ত বারবার পাঠানো। ফল পেলে কাজটি অন্তত একবার হয়েছে — কিন্তু দুবারও হতে পারে। শুধু idempotent কাজের জন্য নিরাপদ।

Sun RPC over UDP, পুরোনো NFS

At-most-once

Server প্রতিটি request id মনে রাখে; duplicate এলে আবার চালায় না, জমা রাখা উত্তর ফেরত দেয়। ব্যর্থ হলে client জানে: শূন্য বা একবার।

duplicate request cache

Exactly-once

ঠিক একবার — সাধারণ অর্থে অসম্ভব, কারণ server crash-এ তার স্মৃতিও হারাতে পারে। বাস্তবে retry + duplicate বাদ + টেকসই (disk-এ) id রেকর্ড = “কার্যত একবার”।

idempotency key
মূল ধারণা

একটি কাজ idempotent যদি একবার আর একাধিকবার করার ফল একই হয়। balance ১৫০০ সেট করা, ফাইলের ৪০৯৬ offset থেকে ৪ KB পড়া, ফাইলটি থাকলে মুছে ফেলা — idempotent. balance-এ ৫০০ যোগ করা বা ফাইলের শেষে একটি লাইন জোড়া — নয়। দুটি পথ: কাজকে idempotent করে নকশা করা, অথবা প্রতিটি অনুরোধে একটি অনন্য idempotency key দিয়ে server-এ duplicate বাদ দেওয়া। ডেটাবেসে এই key কীভাবে UNIQUE কলামে রাখা হয়, তা দেখুন DBMS · transaction-এর নকশা — retry আর idempotency.

নিচের C প্রোগ্রামে parent হলো client, child হলো ব্যাংকের server, আর মাঝে একটি socketpair নেটওয়ার্কের ভূমিকায়। Server প্রথম উত্তরটি ইচ্ছা করে “হারিয়ে” ফেলে। Client ১০০ ms অপেক্ষা করে (poll), উত্তর না পেলে একই request id দিয়ে আবার পাঠায়। একবার চলে সরল (naive) server, একবার duplicate-চেনা server:

#include <stdio.h>
#include <poll.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/wait.h>
#define SZ(x) ((ssize_t)sizeof(x))
typedef struct { int id, val; } Msg;   /* request: id + deposit amount; reply: id + balance */
static void server(int fd, int dedup) {
    int balance = 1000, dropped = 0; Msg q, last = {-1, 0};
    while (read(fd, &q, sizeof q) == SZ(q)) {
        if (!(dedup && q.id == last.id)) {           /* new request (or naive): execute */
            balance += q.val; last.id = q.id; last.val = balance;
            if (!dropped) { dropped = 1; continue; }  /* first reply is "lost" */
        }                                            /* a duplicate gets the saved reply */
        if (write(fd, &last, sizeof last) != SZ(last)) break;
    }
}
static void client(int fd, const char *mode) {
    Msg q = {1, 500}, r; struct pollfd p = {fd, POLLIN, 0};   /* id 1: deposit 500 */
    for (int attempt = 1; attempt <= 3; attempt++) {
        if (write(fd, &q, sizeof q) != SZ(q)) return;
        if (poll(&p, 1, 100) > 0 && read(fd, &r, sizeof r) == SZ(r)) {
            printf("%s attempt %d: reply id=%d balance=%d\n", mode, attempt, r.id, r.val); return;
        }
        printf("%s attempt %d: no reply in 100 ms, resend id=%d\n", mode, attempt, q.id);
    }
}
int main(void) {
    for (int dedup = 0; dedup <= 1; dedup++) {
        int sv[2]; if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == -1) return 1;
        fflush(stdout);                              /* empty the stdio buffer before fork */
        if (fork() == 0) { close(sv[0]); server(sv[1], dedup); _exit(0); }
        close(sv[1]); client(sv[0], dedup ? "dedup:" : "naive:"); close(sv[0]); wait(NULL);
    }
}

আউটপুট (প্রতিবার একই, কারণ শুধু parent ছাপে):

naive: attempt 1: no reply in 100 ms, resend id=1
naive: attempt 2: reply id=1 balance=2000
dedup: attempt 1: no reply in 100 ms, resend id=1
dedup: attempt 2: reply id=1 balance=1500

দুই ক্ষেত্রেই client-এর চোখে ঘটনা এক: প্রথম চেষ্টায় উত্তর নেই, দ্বিতীয় চেষ্টায় আছে। কিন্তু naive server দুবার ৫০০ যোগ করেছে — ১০০০ থেকে ২০০০. এটাই at-least-once-এর বিপদ: কাজ idempotent না হলে retry ক্ষতি করে। dedup server request id ১ চিনে আবার চালায়নি, জমা রাখা উত্তর (১৫০০) পাঠিয়েছে — retry + duplicate বাদ = কার্যত exactly-once. বাস্তবে এই “শেষ উত্তর” disk-এ রাখতে হয়, নইলে server restart হলেই স্মৃতি মুছে যাবে। আরও খেয়াল করুন: fork-এর আগে fflush দেওয়া হয়েছে, কারণ pipe-এ যাওয়া stdout-এর না-ছাপা buffer child-এ কপি হয়। এখানে child _exit দিয়ে বের হয় বলে সেই কপি ছাপা হয় না, কিন্তু child exit বা printf ব্যবহার করলে একই লাইন দুবার ছাপা হতে পারত (পর্ব ১০)।

Timeout আর retry-এর নিয়ম

  • প্রতিটি কলে সময়সীমা। Timeout ছাড়া একটি মৃত server-এর অপেক্ষায় থ্রেড চিরকাল আটকে থাকে। gRPC প্রতিটি কলে deadline দিতে বলে, আর সেটি পরের সার্ভিসেও বয়ে নেয়।
  • সময় মাপুন monotonic ঘড়িতে। Timeout হিসাব CLOCK_REALTIME-এ করলে NTP ঘড়ি পিছিয়ে দিলে timeout লম্বা বা ছোট হয়ে যায়। Linux-এ CLOCK_MONOTONIC (Windows-এ QueryPerformanceCounter) কখনো পেছায় না — অংশ ৫-এ কারণ।
  • Retry-তে exponential backoff আর jitter. সবাই একসাথে সাথে সাথে retry করলে সদ্য-জাগা server আবার ডুবে যায়। অপেক্ষা ১০০, ২০০, ৪০০ ms… আর একটু এলোমেলো।
  • শুধু idempotent বা key-যুক্ত কাজে retry. বাকিগুলোতে retry-র আগে জিজ্ঞেস করা: “id ৪১-এর কী অবস্থা?”

৫ · Physical clock — drift, Cristian, NTP আর Berkeley

প্রতিটি কম্পিউটারে একটি কোয়ার্টজ স্ফটিক নির্দিষ্ট হারে কাঁপে, আর হার্ডওয়্যার timer সেই কাঁপন গুনে OS-কে সময় দেয় (পর্ব ৪)। কিন্তু দুটি স্ফটিক হুবহু এক হারে কাঁপে না, তাপমাত্রায় হার বদলায়। ফলে ঘড়ি ধীরে ধীরে সরে যায় — একে বলে clock drift. কোনো মুহূর্তে দুটি ঘড়ির দেখানো সময়ের পার্থক্য হলো clock skew. মানদণ্ড হলো UTC, যা পারমাণবিক ঘড়ি থেকে আসে।

Numerical — কত ঘন ঘন মেলাতে হবে

ধরুন নির্মাতা বলেছেন সর্বোচ্চ drift rate ρ = ১০−৬ (প্রতি সেকেন্ডে সর্বোচ্চ ১ µs ভুল; সাধারণ কোয়ার্টজ এর কাছাকাছি থেকে কয়েক গুণ খারাপ পর্যন্ত হয়)।

  • এক দিনে: ৮৬,৪০০ s × ১০−৬ = ০.০৮৬৪ s ≈ ৮৬.৪ ms.
  • দুটি ঘড়ি: সবচেয়ে খারাপ অবস্থায় একটি দ্রুত, অন্যটি ধীর চলে — Δt সময়ে পার্থক্য বাড়ে ২ρΔt পর্যন্ত। পার্থক্য δ-এর মধ্যে রাখতে ২ρΔt ≤ δ, অর্থাৎ Δt ≤ δ/(২ρ).
  • δ = ১ ms হলে: ১০−৩/(২ × ১০−৬) = ৫০০ s — প্রায় সোয়া আট মিনিট পরপর মেলাতে হবে। শুধু UTC থেকে δ-এর মধ্যে রাখতে চাইলে δ/ρ = ১,০০০ s.

Cristian-এর algorithm — RTT-এর অর্ধেক যোগ

১৯৮৯ সালে Flaviu Cristian সহজ একটি পথ দেন: একটি time server-কে সময় জিজ্ঞেস করা। সমস্যা হলো, উত্তর আসতে আসতে সময় আরও এগিয়ে গেছে। সমাধান — যাওয়া-আসার সময় (round-trip time, RTT) মেপে তার অর্ধেক যোগ করা। ধাপে ধাপে:

1 T0 = client clock; send "time?"2 server reads Ts, sends it back3 T1 = client clock on arrival4 estimate = Ts + (T1 - T0) / 25 adjust; error <= (T1-T0)/2 - min
মান (সময় 10:00:00-এর পরের অংশ)
T0 (client)
Ts (server)
T1 (client)
RTT
অনুমান
সংশোধন
  1. প্রশ্ন পাঠানো। Client নিজের ঘড়িতে T0 = 10:00:00.000 লিখে রেখে server-কে “এখন কটা বাজে?” পাঠাল।
  2. Server-এর উত্তর। বার্তা পেয়ে server নিজের (UTC-র সাথে মেলানো) ঘড়ি পড়ল: Ts = 10:00:00.100, আর সেটি উত্তরে পাঠাল। কিন্তু উত্তরটি পৌঁছাতে আরও সময় লাগবে।
  3. উত্তর এলো। Client-এর ঘড়িতে তখন T1 = 10:00:00.026. যাওয়া-আসা মিলিয়ে RTT = T1 − T0 = ২৬ ms — এটি client নিজের ঘড়িতেই মাপে, তাই ঘড়ির ভুল এখানে প্রায় কিছু করে না।
  4. অনুমান। ধরে নেওয়া হয় যাওয়া আর আসায় সমান সময় লেগেছে, তাই উত্তর আসতে লেগেছে RTT/২ = ১৩ ms. এখনকার আসল সময় ≈ Ts + ১৩ ms = 10:00:00.113. Client পড়ছে .026, অর্থাৎ সে ৮৭ ms পিছিয়ে।
  5. সংশোধন আর ভুলের সীমা। Client ঘড়ি ৮৭ ms এগিয়ে দিল। Ts যেকোনো মুহূর্তে পড়া হতে পারত — T0 থেকে T1-এর মাঝে — তাই ভুল সর্বোচ্চ RTT/২ = ±১৩ ms. এক দিকের যাত্রায় অন্তত min = ৪ ms লাগে জানা থাকলে সীমা ছোট হয়: ১৩ − ৪ = ±৯ ms. RTT ছোট হলে নির্ভুলতা বেশি, তাই কয়েকবার জিজ্ঞেস করে সবচেয়ে ছোট RTT-এর উত্তরটি নেওয়া হয়।

ঘড়ি কখনো পেছনে নেবেন না। Client যদি ৮৭ ms এগিয়ে থাকত, সরাসরি পিছিয়ে দিলে একই সময় দুবার আসত — make ভাববে নতুন ফাইল পুরোনো, লগে ঘটনার ক্রম উল্টে যাবে। তাই OS ঘড়িকে slew করে: কিছুক্ষণ একটু ধীরে চালায়। ntpd-এর মতো ডেমন সাধারণত সর্বোচ্চ ৫০০ ppm হারে slew করে — ৮৭ ms শোধ করতে ০.০৮৭/০.০০০৫ = ১৭৪ s লাগে। বড় পার্থক্যে (ntpd-এর ডিফল্ট সীমা ১২৮ ms বলে নথিতে আছে) একবারে লাফ দেয়। এজন্যই timeout মাপতে monotonic clock ব্যবহার করা উচিত — যা কখনো পেছায় না, শুধু slew-এর হারে বদলায়।

NTP — চারটি timestamp, দুটি সূত্র

David Mills (University of Delaware)-এর Network Time Protocol ১৯৮৫ সাল থেকে ইন্টারনেটের ঘড়ি মেলাচ্ছে। Server-গুলো স্তরে সাজানো (stratum ০ = পারমাণবিক বা GPS ঘড়ি, stratum ১ = তার সাথে সরাসরি যুক্ত server…)। NTP যাওয়া আর আসার সময় সমান ধরে না, চারটি timestamp নেয়: t₁ client পাঠাল, t₂ server পেল, t₃ server পাঠাল, t₄ client পেল (t₁, t₄ client-এর ঘড়িতে; t₂, t₃ server-এর)।

offset θ = ((t₂ − t₁) + (t₃ − t₄)) / ২  ·  delay δ = (t₄ − t₁) − (t₃ − t₂)

Numerical: client আসলে server থেকে ৫০ ms পিছিয়ে; যেতে ১০ ms, server-এ ২ ms কাজ, ফিরতে ১৪ ms. চারটি timestamp: t₁ = ১০০০, t₂ = ১০৬০, t₃ = ১০৬২, t₄ = ১০২৬ (ms). তাহলে θ = ((১০৬০ − ১০০০) + (১০৬২ − ১০২৬))/২ = (৬০ + ৩৬)/২ = ৪৮ ms, আর δ = (১০২৬ − ১০০০) − (১০৬২ − ১০৬০) = ২৬ − ২ = ২৪ ms. আসল offset ৫০, অনুমান ৪৮ — ২ ms ভুল এসেছে যাওয়া-আসার অসমতা থেকে: (১৪ − ১০)/২ = ২. অসমতা মাপা অসম্ভব, তাই NTP-র ভুলের সীমা δ/২. ইন্টারনেটে সাধারণত কয়েক থেকে কয়েক দশ মিলিসেকেন্ড, ভালো LAN-এ এক মিলিসেকেন্ডের নিচে নির্ভুলতা পাওয়া যায়; আরও সূক্ষ্ম কাজে হার্ডওয়্যার timestamp-যুক্ত PTP (IEEE 1588) ব্যবহার হয়।

$ timedatectl                 # Linux (systemd): NTP চালু কি না, ঘড়ি মেলানো কি না
$ chronyc tracking            # chrony থাকলে: offset, frequency (ppm) ইত্যাদি
PS> w32tm /query /status      # Windows-এর Windows Time সার্ভিস

OS ভেদে ব্যবস্থা আলাদা: অনেক Linux ডিস্ট্রো chrony বা systemd-timesyncd চালায়, Windows-এ W32Time সার্ভিস, macOS-এ নিজস্ব timed ডেমন।

Berkeley algorithm — কারও ঘড়ি ঠিক না হলে গড়

যেখানে কোনো UTC উৎস নেই, সেখানে উদ্দেশ্য সবার ঘড়ি একে অপরের সাথে মেলানো। Gusella আর Zatti-র (১৯৮০-এর দশকের শেষে, Berkeley Unix-এর জন্য) পদ্ধতিতে একজন master সবাইকে জিজ্ঞেস করে (RTT সংশোধনসহ), গড় বের করে, আর প্রত্যেককে কতটুকু সরাতে হবে তা পাঠায় — সময় নয়, পার্থক্য পাঠানো হয়, যাতে পথের দেরি ফলকে বিকৃত না করে।

উদাহরণ: master-এর সাপেক্ষে A +১২ s, B −৯ s, C +৪৫ s. সীমা ২০ s ধরলে C স্পষ্টতই গোলমেলে, তাই গড় থেকে বাদ: (০ + ১২ − ৯)/৩ = +১ s. নির্দেশ: master +১, A −১১, B +১০, আর C −৪৪ (C-ও সংশোধন পায়, শুধু গড়ে ভোট দেয় না)। যাদের পেছাতে হবে (A, C), তারা slew করে।

৬ · Logical clock — সময় নয়, ক্রম

অনেক কাজে আসল সময় দরকারই নেই — দরকার শুধু ক্রম: কোন আপডেট আগে, কোনটি পরে। ১৯৭৮ সালে Leslie Lamport-এর “Time, Clocks, and the Ordering of Events in a Distributed System” প্রবন্ধ দেখায়, বার্তা দিয়েই একটি সুসংগত ক্রম তৈরি করা যায়। প্রথমে সম্পর্কটি — happened-before (→):

  1. একই প্রসেসে a আগে, b পরে ঘটলে a → b.
  2. a যদি একটি বার্তা পাঠানো আর b সেই বার্তা পাওয়া হয়, তাহলে a → b.
  3. a → b আর b → c হলে a → c (transitive).

a → b-ও নয়, b → a-ও নয় — এমন দুটি ঘটনা concurrent (a ∥ b): কেউ কারও খবর জানত না। খেয়াল করুন, এই “→” পর্ব ২৩-এর memory model-এর happens-before-এর পূর্বসূরি — সেখানেও একই চিন্তা, থ্রেড আর মেমরির জন্য।

Lamport-এর নিয়ম

প্রতিটি প্রসেস একটি পূর্ণসংখ্যা counter C রাখে, শুরু ০:

  • নিয়ম ১: প্রতিটি ঘটনার (local বা send) আগে C = C + ১; ঘটনাটির timestamp L = C.
  • নিয়ম ২: বার্তা পাঠানোর সময় বার্তার সাথে C পাঠানো।
  • নিয়ম ৩: timestamp t-সহ বার্তা পেলে C = max(C, t) + ১.

Space–time diagram — তিন প্রসেস, তিন বার্তা

বাঁ থেকে ডানে সময়; তীর মানে বার্তা; প্রতিটি বিন্দুর পাশে ঘটনার নাম আর Lamport timestamp

P1P2P3 a 1b 2c 3d 7 e 3f 4g 5 h 1i 2j 3k 5l 6 m1m2m3
ছবি ৫৩.১ — নিচের stepper আর simulator এই একই ১২টি ঘটনা ব্যবহার করে।
প্রতিটি প্রসেসের ঘটনা আর timestamp (ধাপের শেষে)
P1
P2
P3
পথে বার্তা
  1. দুটি local ঘটনা। P1-এ a, P3-এ h. দুজনেরই counter ০ থেকে ১. দুটি ঘটনার timestamp সমান (১), আর তারা concurrent — কেউ কারও খবর জানে না।
  2. প্রথম বার্তা। P1-এর b হলো m1 পাঠানো: C = ২, বার্তা বহন করে ২. ওদিকে P3-এ i, local, C = ২.
  3. নিয়ম ৩ প্রথমবার। P2-এর প্রথম ঘটনা e = m1 পাওয়া। P2-এর C ছিল ০, বার্তায় ২: max(০, ২) + ১ = ৩. এভাবে P2 “জানে” যে b তার আগে ঘটেছে। P3-এ j = ৩.
  4. আরেকটি পাঠানো। P2-এর f পাঠায় m2, timestamp ৪. P1-এ c, local: ২ + ১ = ৩. খেয়াল করুন, c = ৩ আর f = ৪ — সংখ্যায় c ছোট, কিন্তু c আর f-এর মধ্যে কোনো বার্তার শিকল নেই।
  5. max-এর কাজ। P3-এর k = m2 পাওয়া। P3-এর নিজের C ৩, বার্তায় ৪: max(৩, ৪) + ১ = ৫. নিজের ঘড়ি পিছিয়ে ছিল, বার্তা তাকে এগিয়ে দিল।
  6. শেষ বার্তা পাঠানো। P2-এ g = ৫ (local). P3-এর l পাঠায় m3, timestamp ৬.
  7. P1 এগিয়ে যায়। P1-এর d = m3 পাওয়া: নিজের C ৩, বার্তায় ৬, তাই max(৩, ৬) + ১ = ৭. a → b → e → f → k → l → d — পুরো শিকল জুড়ে timestamp বেড়েছে।
  8. পূর্ণ ক্রম (total order). Timestamp সমান হলে প্রসেস নম্বর দিয়ে ঝগড়া মেটানো হয় — (৩, P1) < (৩, P2) < (৩, P3). ফল: a h b i c e j f g k l d. সব প্রসেস একই নিয়মে একই ক্রমে পৌঁছায় — Lamport-এর distributed mutual exclusion আর replicated state machine এর ওপর দাঁড়ানো।
// Lamport clocks: 3 processes, events listed in one possible real-time order.
// [process, event, kind, message]   kind: L = local, S = send, R = receive
const script = [
  ["P1","a","L"], ["P3","h","L"], ["P1","b","S","m1"], ["P3","i","L"],
  ["P2","e","R","m1"], ["P3","j","L"], ["P2","f","S","m2"], ["P1","c","L"],
  ["P3","k","R","m2"], ["P2","g","L"], ["P3","l","S","m3"], ["P1","d","R","m3"],
];
const C = { P1: 0, P2: 0, P3: 0 };   // one counter per process
const carried = {};                   // message -> timestamp it carries
const L = {};                         // event -> Lamport timestamp
for (const [p, ev, kind, m] of script) {
  let how;
  if (kind === "R") {                 // rule 3: max(own, message) + 1
    how = "recv " + m + ": max(" + C[p] + ", " + carried[m] + ") + 1";
    C[p] = Math.max(C[p], carried[m]) + 1;
  } else {                            // rules 1 and 2: tick before the event
    C[p] += 1;
    how = kind === "S" ? "send " + m + " carrying " + C[p] : "local";
    if (kind === "S") carried[m] = C[p];
  }
  L[ev] = C[p];
  console.log(p + " " + ev + " = " + C[p] + "   (" + how + ")");
}
// total order: by timestamp, ties broken by process number
const total = script.map(([p, ev]) => [L[ev], +p[1], ev])
  .sort((x, y) => x[0] - y[0] || x[1] - y[1])
  .map(([t, n, ev]) => ev + "(" + t + "." + n + ")");
console.log("Total order: " + total.join(" "));
console.log("c = " + L.c + " < f = " + L.f + ", yet c and f are not causally related");

আউটপুট:

P1 a = 1   (local)
P3 h = 1   (local)
P1 b = 2   (send m1 carrying 2)
P3 i = 2   (local)
P2 e = 3   (recv m1: max(0, 2) + 1)
P3 j = 3   (local)
P2 f = 4   (send m2 carrying 4)
P1 c = 3   (local)
P3 k = 5   (recv m2: max(3, 4) + 1)
P2 g = 5   (local)
P3 l = 6   (send m3 carrying 6)
P1 d = 7   (recv m3: max(3, 6) + 1)
Total order: a(1.1) h(1.3) b(2.1) i(2.3) c(3.1) e(3.2) j(3.3) f(4.2) g(5.2) k(5.3) l(6.3) d(7.1)
c = 3 < f = 4, yet c and f are not causally related

প্রতিটি লাইন stepper-এর একটি ঘটনা — তিনটি receive-এ max-এর হিসাব স্পষ্ট দেখা যাচ্ছে। শেষ লাইনটি মূল সতর্কতা: L(c) = ৩ < L(f) = ৪, অথচ c আর f-এর মধ্যে কোনো happened-before নেই। script-এর ক্রম বদলে (যেমন c-কে b-এর ঠিক পরে নিয়ে) দেখুন — বৈধ যেকোনো ক্রমে timestamp একই থাকে, কারণ এগুলো শুধু প্রতিটি প্রসেসের ঘটনার ক্রম আর বার্তার ওপর নির্ভর করে।

প্রমাণ — clock condition (a → b ⇒ L(a) < L(b)): happened-before-এর সংজ্ঞার তিন ধাপে induction. (১) একই প্রসেসে a-র পরে b: নিয়ম ১-এ প্রতিটি ঘটনায় C কমপক্ষে ১ বাড়ে, তাই L(a) < L(b). (২) a = send, b = তার receive: বার্তা বহন করে L(a), আর নিয়ম ৩-এ L(b) = max(C, L(a)) + ১ > L(a). (৩) a → c → b হলে L(a) < L(c) < L(b). ∎ উল্টোটি সত্য নয়: L(a) < L(b) থেকে a → b বলা যায় না (c আর f). শুধু এটুকু বলা যায় যে b → a অসম্ভব।

Vector clock — concurrency চেনা

উল্টো দিকটি জানতে — দুটি ঘটনা আসলে কারণ-সম্পর্কিত কি না — ১৯৮৮ সালে Colin Fidge আর Friedemann Mattern আলাদাভাবে vector clock দেন। n প্রসেস হলে প্রতিটি প্রসেস n সংখ্যার একটি vector V রাখে; V[j] মানে “প্রসেস j-এর কতগুলো ঘটনার খবর আমি জানি”:

  • প্রতিটি ঘটনার আগে নিজের ঘর বাড়ানো: V[i] = V[i] + ১.
  • পাঠানোর সময় পুরো vector পাঠানো।
  • পেলে প্রতিটি ঘরে max নেওয়া, তারপর নিজের ঘর + ১.
  • তুলনা: V(a) ≤ V(b) মানে প্রতিটি ঘরে ≤. a → b ⇔ V(a) ≤ V(b) এবং V(a) ≠ V(b); কোনো দিকেই ≤ না হলে a ∥ b.
// Vector clocks on the same 12 events; then compare pairs of events.
const script = [
  ["P1","a","L"], ["P3","h","L"], ["P1","b","S","m1"], ["P3","i","L"],
  ["P2","e","R","m1"], ["P3","j","L"], ["P2","f","S","m2"], ["P1","c","L"],
  ["P3","k","R","m2"], ["P2","g","L"], ["P3","l","S","m3"], ["P1","d","R","m3"],
];
const V = { P1: [0,0,0], P2: [0,0,0], P3: [0,0,0] };
const carried = {}, at = {};
for (const [p, ev, kind, m] of script) {
  const i = +p[1] - 1, v = V[p];
  if (kind === "R") carried[m].forEach((x, j) => { v[j] = Math.max(v[j], x); });
  v[i] += 1;                                   // tick own entry
  if (kind === "S") carried[m] = [...v];       // message carries a copy
  at[ev] = [...v];
  console.log(p + " " + ev + " = [" + v + "]");
}
const leq = (x, y) => x.every((n, j) => n <= y[j]);
function relation(a, b) {
  const x = at[a], y = at[b];
  if (leq(x, y) && x.join() !== y.join()) return a + " -> " + b;
  if (leq(y, x) && x.join() !== y.join()) return b + " -> " + a;
  return a + " || " + b + "  (concurrent)";
}
for (const [a, b] of [["b","k"], ["c","f"], ["e","j"], ["g","l"], ["h","d"], ["c","e"]])
  console.log("[" + at[a] + "] vs [" + at[b] + "]: " + relation(a, b));

আউটপুট:

P1 a = [1,0,0]
P3 h = [0,0,1]
P1 b = [2,0,0]
P3 i = [0,0,2]
P2 e = [2,1,0]
P3 j = [0,0,3]
P2 f = [2,2,0]
P1 c = [3,0,0]
P3 k = [2,2,4]
P2 g = [2,3,0]
P3 l = [2,2,5]
P1 d = [4,2,5]
[2,0,0] vs [2,2,4]: b -> k
[3,0,0] vs [2,2,0]: c || f  (concurrent)
[2,1,0] vs [0,0,3]: e || j  (concurrent)
[2,3,0] vs [2,2,5]: g || l  (concurrent)
[0,0,1] vs [4,2,5]: h -> d
[3,0,0] vs [2,1,0]: c || e  (concurrent)

d = [৪, ২, ৫] পড়া যায় এভাবে: “P1-এর ৪টি, P2-এর ২টি, P3-এর ৫টি ঘটনার খবর d জানে।” g = [২, ৩, ০] আর l = [২, ২, ৫] — g-এর দ্বিতীয় ঘর বড়, l-এর তৃতীয় ঘর বড়, তাই কেউ কারও আগে নয়: concurrent. Lamport-এ L(g) = ৫ < L(l) = ৬ দেখে ভুল করে “g আগে” মনে হতো। এই ক্ষমতারই দাম: প্রতিটি বার্তায় n সংখ্যা। Amazon-এর Dynamo-র মতো সিস্টেম একই key-তে দুটি concurrent লেখা চিনতে এই ধরনের version vector ব্যবহার করেছে।

৭ · Distributed mutual exclusion আর leader election

পর্ব ২৪-এর mutex একটি মেশিনের atomic instruction-এর ওপর দাঁড়ানো। কয়েকটি মেশিন যদি একটি shared ফাইল বা প্রিন্টার এক এক করে ব্যবহার করতে চায়, তাহলে mutual exclusion বার্তা দিয়েই করতে হবে। তিনটি ক্লাসিক পথ:

  • Centralised: একজন coordinator — ব্যাংকের টোকেন-কাউন্টারের মতো। চাইলে REQUEST, ফাঁকা থাকলে GRANT, শেষে RELEASE: প্রতি প্রবেশে ৩টি বার্তা। সহজ ও ন্যায্য (FIFO), কিন্তু coordinator একক বিপদ-বিন্দু, আর তার নীরবতা থেকে বোঝা যায় না “ব্যস্ত” না “মৃত”।
  • Ricart–Agrawala (১৯৮১): কোনো coordinator নেই। প্রবেশ করতে চাইলে সবাইকে (n − ১ জন) Lamport timestamp-সহ REQUEST পাঠানো, সবার OK পেলে প্রবেশ। REQUEST পেলে: নিজে আগ্রহী না হলে সাথে সাথে OK; নিজে ভেতরে থাকলে উত্তর জমা; নিজেও অপেক্ষায় থাকলে (timestamp, id) তুলনা — অন্যজনেরটি ছোট হলে OK, নিজেরটি ছোট হলে জমা। বের হয়ে জমা উত্তরগুলো পাঠানো। মোট ২(n − ১) বার্তা।
  • Token ring: প্রসেসগুলো একটি যৌক্তিক বৃত্তে; একটিমাত্র token ঘোরে, যার হাতে token সে-ই ঢুকতে পারে — রিলে দৌড়ের ব্যাটনের মতো। প্রতি প্রবেশে ১ থেকে অসীম বার্তা (কেউ না চাইলেও token ঘোরে), অপেক্ষা ০ থেকে n − ১ ধাপ। Token হারালে বা কেউ মরলে নতুন token বানাতে হয় — আর হারানো token আর ধীর token আলাদা করা কঠিন।
Algorithmবার্তা/প্রবেশn = ৫ হলেপ্রবেশের আগে দেরি (বার্তার পর্যায়)দুর্বলতা
Centralised৩৩২coordinator crash
Lamport (১৯৭৮)৩(n − ১)১২২যেকোনো একজনের crash
Ricart–Agrawala২(n − ১)৮২যেকোনো একজনের crash
Token ring১ … ∞১ … ∞০ … n − ১token হারানো

Ricart–Agrawala-র সঠিকতা (সংক্ষেপে): ধরুন Pi আর Pj একসাথে ভেতরে। দুজনেই একে অপরের OK পেয়েছে। দুজনের অনুরোধ যদি একই সময়ে চলমান থাকে, তবে (Ti, i) আর (Tj, j) পূর্ণ ক্রমে আলাদা — ধরা যাক (Ti, i) ছোট। তখন Pj, Pi-এর অনুরোধ পেয়ে দেখবে নিজেরটি বড়, তাই OK দেবে; কিন্তু Pi, Pj-এর অনুরোধ পেয়ে দেখবে নিজেরটি ছোট, তাই উত্তর জমা রাখবে, যতক্ষণ না বের হয়। ফলে Pj ভেতরে ঢুকতেই পারত না — বৈপরীত্য। আর যদি Pj-এর অনুরোধ Pi বের হওয়ার পরে আসে, তাহলে দুজন একসাথে ভেতরে থাকেই না। Deadlock আর starvation নেই, কারণ সবচেয়ে ছোট timestamp-এর অনুরোধকে কেউ আটকায় না। ∎ বিদ্যমান DBMS-এর two-phase locking একই সমস্যার ডেটাবেস-রূপ, যেখানে একটি lock manager থাকে।

Leader election — নতুন coordinator কে?

Centralised mutex, Berkeley-র master, ডেটাবেসের primary — সবখানে একজন নেতা লাগে। নেতা মরলে বাকিদের একমত হয়ে নতুন নেতা বাছতে হয় — leader election. Hector Garcia-Molina-র (১৯৮২) bully algorithm-এ ধরা হয় প্রত্যেকের একটি অনন্য id আছে, বার্তা নির্ভরযোগ্য, আর timeout দিয়ে crash চেনা যায়। নিয়ম একটাই: সবচেয়ে বড় id-ধারী জীবিত প্রসেস নেতা।

প্রত্যেকের অবস্থা
P1
P2
P3
P4
P5
মোট বার্তা
  1. সমস্যা টের পাওয়া। P2, coordinator P5-কে একটি অনুরোধ পাঠিয়ে timeout পর্যন্ত উত্তর পায়নি। সে ধরে নেয় P5 মৃত (আসলে হয়তো ধীর — অংশ ৯)। এই অনুরোধটি নির্বাচনের বার্তা নয়, তাই গোনা হয়নি।
  2. বড়দের জিজ্ঞাসা। P2 নিজের চেয়ে বড় সবাইকে — P3, P4, P5 — ELECTION পাঠায়: ৩টি বার্তা। ছোট P1-কে জিজ্ঞেস করার দরকার নেই; সে তো নেতা হবে না।
  3. “আমি বড়, দায়িত্ব আমার”। জীবিত P3 আর P4 উত্তর দেয় OK (মোট ৫)। মৃত P5 চুপ। OK পেয়ে P2 সরে দাঁড়ায় — এখন অন্য কেউ দায়িত্ব নেবে। নামের “bully” এখান থেকেই: বড়রা ছোটদের সরিয়ে দেয়।
  4. বড়দের নিজস্ব নির্বাচন। P3 পাঠায় P4, P5-কে (২টি), P4 উত্তর দেয় OK P3-কে (১টি) — মোট ৮. P3-ও সরে দাঁড়ায়।
  5. সবচেয়ে বড় জীবিত। P4 পাঠায় শুধু P5-কে (মোট ৯)। Timeout পর্যন্ত কোনো OK নেই — P4-এর চেয়ে বড় কেউ বেঁচে নেই, তাই P4 জয়ী।
  6. ঘোষণা। P4 সব ছোটদের — P1, P2, P3 — COORDINATOR পাঠায় (মোট ১২: ELECTION ৬, OK ৩, COORDINATOR ৩)। P5 পরে ফিরলে সে নিজেই নির্বাচন চালিয়ে আবার নেতা হবে — বড়রা সবসময় জেতে।
// Bully election: P1..P5, coordinator P5 has crashed, P2 notices first.
const N = 5, alive = [false, true, true, true, true, false]; // index 1..5
const starter = 2;
const count = { ELECTION: 0, OK: 0, COORDINATOR: 0 };
function send(type, from, to) {
  count[type]++;
  console.log("  P" + from + " -> P" + to + "  " + type +
    (alive[to] ? "" : "   (no answer: P" + to + " is down)"));
}
const queue = [starter], started = new Set();
let round = 0;
while (queue.length) {
  const p = queue.shift();
  if (started.has(p)) continue;
  started.add(p);
  console.log("Round " + (++round) + ": P" + p + " holds an election");
  let gotOk = false;
  for (let q = p + 1; q <= N; q++) {          // ask every higher id
    send("ELECTION", p, q);
    if (!alive[q]) continue;
    send("OK", q, p);                           // "I am bigger, I take over"
    gotOk = true;
    if (!started.has(q)) queue.push(q);         // q runs its own election
  }
  if (gotOk) { console.log("  P" + p + " got OK, steps aside and waits"); continue; }
  console.log("  P" + p + " timed out waiting for OK -> P" + p + " wins");
  for (let q = 1; q < p; q++) if (alive[q]) send("COORDINATOR", p, q);
  break;
}
const total = count.ELECTION + count.OK + count.COORDINATOR;
console.log("Messages: ELECTION " + count.ELECTION + ", OK " + count.OK +
  ", COORDINATOR " + count.COORDINATOR + ", total " + total);

আউটপুট:

Round 1: P2 holds an election
  P2 -> P3  ELECTION
  P3 -> P2  OK
  P2 -> P4  ELECTION
  P4 -> P2  OK
  P2 -> P5  ELECTION   (no answer: P5 is down)
  P2 got OK, steps aside and waits
Round 2: P3 holds an election
  P3 -> P4  ELECTION
  P4 -> P3  OK
  P3 -> P5  ELECTION   (no answer: P5 is down)
  P3 got OK, steps aside and waits
Round 3: P4 holds an election
  P4 -> P5  ELECTION   (no answer: P5 is down)
  P4 timed out waiting for OK -> P4 wins
  P4 -> P1  COORDINATOR
  P4 -> P2  COORDINATOR
  P4 -> P3  COORDINATOR
Messages: ELECTION 6, OK 3, COORDINATOR 3, total 12

Stepper-এর হুবহু একই ১২টি বার্তা। alive আর starter বদলে দেখুন: N = ৬, P6 মৃত আর P5 প্রথম টের পেলে মাত্র ১ ELECTION + ৪ COORDINATOR = ৫টি বার্তা — এটিই সেরা অবস্থা (n − ১). সবচেয়ে ছোট id নির্বাচন শুরু করলে প্রায় সবাই সবাইকে পাঠায় — সবচেয়ে খারাপ অবস্থায় O(n²) বার্তা।

Ring algorithm (Chang ও Roberts-এর ধারায়): প্রসেসগুলো বৃত্তে সাজানো; যে টের পায় সে একটি ELECTION বার্তা পরের জীবিতকে পাঠায়, প্রত্যেকে নিজের id যোগ করে এগিয়ে দেয় (মৃতকে ডিঙিয়ে)। বার্তা শুরুর জনের কাছে ফিরলে তালিকার সবচেয়ে বড় id নেতা, আর আরেক পাক ঘুরে COORDINATOR জানানো হয়। উপরের উদাহরণে (P5 মৃত, P2 শুরু): P2 → P3 → P4 → P1 → P2, তালিকা [২, ৩, ৪, ১] — নেতা P4; তারপর COORDINATOR আরও ৪ ধাপ: মোট ৮টি বার্তা, প্রায় ২n.

৮ · Distributed file system — NFS, AFS আর GFS/HDFS

পর্ব ৪২-এ (Directory, link আর mounting — VFS) দেখেছেন, VFS-এর কারণে NFS-এর মতো দূরের file system-ও একটি সাধারণ ডিরেক্টরিতে mount করা যায়, আর open/read একই থাকে। এখানে প্রশ্ন ভেতরের: অনেক client একই ফাইল cache করলে কে কখন কার লেখা দেখবে, আর server crash করলে কী হবে।

NFS — stateless থেকে stateful

NFS (Sun, ১৯৮৪-৮৫) ONC RPC-র ওপর চলে। প্রতিটি ফাইল একটি অস্বচ্ছ file handle দিয়ে চেনা হয়, আর প্রতিটি অনুরোধ নিজেই সম্পূর্ণ: “এই handle-এর এই offset থেকে এত বাইট পড়া” — কোনো “খোলা ফাইল” server মনে রাখে না।

NFSv2/v3 — stateless server

Server crash করে ফিরলে client শুধু retry করে, কোনো recovery লাগে না — at-least-once RPC-র সাথে মানানসই, কারণ READ, WRITE-at-offset, GETATTR idempotent. কিন্তু REMOVE, RENAME, CREATE নয়: তাই server-রা একটি duplicate request cache রাখে। File locking আলাদা একটি protocol-এ (NLM), যা আসলে stateful — নকশার একটি দুর্বল জোড়।

v3: RFC 1813 (১৯৯৫)

NFSv4 — stateful

OPEN, CLOSE আর lock protocol-এর ভেতরে; server অবস্থা রাখে lease দিয়ে — client নির্দিষ্ট সময় পরপর নবায়ন না করলে তার lock বাতিল। Server restart-এর পর একটি grace period-এ client-রা নিজেদের অবস্থা ফিরিয়ে নেয়। একটি port (2049), কয়েকটি অনুরোধ এক RPC-তে (COMPOUND), আর delegation: একা ব্যবহারকারী client-কে ফাইলটি নিজে cache-এ সামলাতে দেওয়া।

v4.0 ২০০০/২০০৩, v4.1 ২০১০

Caching আর close-to-open consistency: NFS client ফাইলের ডেটা আর attribute (আকার, mtime) কিছুক্ষণ cache করে — Linux-এ attribute cache-এর মেয়াদ সাধারণত কয়েক সেকেন্ড থেকে এক মিনিটের মধ্যে (acregmin/acregmax mount option). প্রতিশ্রুতিটি দুর্বল কিন্তু কাজের: ফাইল close করার সময় client সব লেখা server-এ পাঠায়, আর open করার সময় server-এর কাছে attribute যাচাই করে। তাই A লিখে বন্ধ করার পরে B খুললে A-র লেখা দেখবে — কিন্তু দুজনে একসাথে খোলা রেখে লিখলে কে কখন কী দেখবে, তার নিশ্চয়তা নেই। এই খুঁটিনাটি mount option আর version ভেদে আলাদা, তাই গুরুত্বপূর্ণ কাজে NFS-এর ওপর একসাথে লেখার আগে নথি দেখে পরীক্ষা করা জরুরি।

AFS — পুরো ফাইল cache আর callback

Carnegie Mellon-এর Andrew প্রকল্পের (১৯৮০-এর দশকের মাঝামাঝি) AFS-এর লক্ষ্য ছিল হাজার হাজার ওয়ার্কস্টেশন। কৌশল দুটি। Whole-file caching: খোলার সময় পুরো ফাইল client-এর লোকাল ডিস্কে আনা, পড়া-লেখা সব লোকালি, বন্ধ করলে পুরোটা ফেরত — লাইব্রেরি থেকে পুরো বই বাসায় নেওয়ার মতো। আর callback: server প্রতিশ্রুতি দেয়, “ফাইলটি বদলালে আমি আপনাকে জানাব”; ফলে প্রতিবার খোলার সময় server-কে জিজ্ঞেস করতে হয় না। কেউ নতুন সংস্করণ ফেরত দিলে server বাকিদের callback ভাঙে। এতে server-এর ভার খুব কমে।

GFS আর HDFS — বড় ফাইল, বড় টুকরো, তিন কপি

২০০৩ সালের Google File System প্রবন্ধ ভিন্ন প্রয়োজন থেকে আসে: কয়েক GB-র ফাইল, বেশিরভাগ লেখা শেষে জোড়া (append), আর হাজারো সস্তা যন্ত্র, যার কিছু প্রতিদিনই নষ্ট হয়। নকশা: একটি master শুধু metadata রাখে (কোন ফাইলের কোন টুকরো কোথায়), ডেটা থাকে chunk server-এ, প্রতিটি chunk ৬৪ MB, ডিফল্ট ৩টি কপি আলাদা যন্ত্রে। Client master-কে শুধু ঠিকানা জিজ্ঞেস করে, ডেটা সরাসরি chunk server থেকে আনে। Hadoop-এর HDFS একই ধাঁচে বানানো (NameNode আর DataNode; নতুন version-এ ডিফল্ট block ১২৮ MB, replication ৩)। বড় chunk মানে metadata ছোট, master-এ কম অনুরোধ — কিন্তু ছোট ফাইলের জন্য অপচয়ী।

Consistency semantics — কে কখন কার লেখা দেখে

Semanticsপ্রতিশ্রুতিকোথায়
Unix semanticsএকটি write শেষ হলেই পরের প্রতিটি read, যেকোনো প্রসেসে, নতুন ডেটা দেখেএকক মেশিনের ext4, NTFS; distributed-এ খুব দামি
Session semanticsখোলা ফাইলের বদল শুধু নিজের কাছে দৃশ্যমান; close-এর পর যারা নতুন করে খোলে, তারা দেখেAFS (প্রায় এমন); NFS-এর close-to-open কাছাকাছি
Immutable filesফাইল একবার লেখা হলে আর বদলায় না; বদল মানে নতুন ফাইলঅনেক object store-এর object, HDFS-এর মূল নকশা (লেখা শেষে কেবল append)

৯ · Failure detection, consensus আর বাস্তবের OS

প্রায় প্রতিটি algorithm-এ “timeout হলে মৃত ধরে নেওয়া” লুকিয়ে আছে। বাস্তবে এটি করা হয় heartbeat দিয়ে: প্রতিটি যন্ত্র নির্দিষ্ট সময় পরপর “আমি আছি” পাঠায়; কয়েকটি পরপর না এলে failure detector তাকে সন্দেহ করে — বাড়ির দারোয়ান যেমন প্রতি ঘণ্টায় ঘণ্টা বাজান, না বাজলে কেউ খোঁজ নিতে যান। দুই দিকের ভুল আছে: timeout ছোট হলে ধীর-কিন্তু-জীবিত যন্ত্রকে মৃত ধরা (মিথ্যা সন্দেহ), বড় হলে সত্যিকারের মৃত্যু দেরিতে ধরা। একটি asynchronous নেটওয়ার্কে — যেখানে দেরির কোনো ঊর্ধ্বসীমা নেই — নিখুঁত failure detector অসম্ভব।

মিথ্যা সন্দেহের সবচেয়ে বড় বিপদ split-brain: পুরোনো নেতা আসলে বেঁচে ছিল (হয়তো garbage collection-এ কয়েক সেকেন্ড থেমে ছিল), এদিকে নতুন নেতাও নির্বাচিত — দুজনেই লিখছে। প্রতিকার: majority quorum ছাড়া কেউ নেতা নয়, আর প্রতিটি নেতৃত্বের একটি ক্রমবর্ধমান নম্বর (term বা fencing token) — স্টোরেজ পুরোনো নম্বরের লেখা ফিরিয়ে দেয়।

Consensus — এক অনুচ্ছেদে

Bully algorithm ধরে নেয় timeout নির্ভুল; সেটি না হলে দুজন নেতা হতে পারে। সাধারণ সমস্যাটির নাম consensus: কয়েকটি যন্ত্র, যার কিছু crash করতে পারে আর বার্তা দেরিতে আসতে পারে, একটি মানে একমত হবে, আর সিদ্ধান্ত কখনো উল্টাবে না। ১৯৮৫ সালের FLP ফল দেখায়, সম্পূর্ণ asynchronous নেটওয়ার্কে একটিমাত্র crash থাকলেও কোনো নির্ধারিত (deterministic) algorithm সবসময় শেষ হওয়ার নিশ্চয়তা দিতে পারে না; বাস্তব algorithm তাই নিরাপত্তা সবসময় রাখে, আর অগ্রগতি পায় যখন নেটওয়ার্ক কিছুটা শান্ত। Leslie Lamport-এর Paxos ক্লাসিক সমাধান; Diego Ongaro ও John Ousterhout-এর Raft (২০১৪) একই কাজ বোঝার সুবিধাকে লক্ষ্য রেখে করে — একজন নেতা, একটি replicated log, majority-তে পৌঁছালে commit; ২f + ১ যন্ত্রে f-টি crash সয়। Quorum, CAP আর replication-এর ডেটাবেস-দিক বিস্তারিত আছে DBMS · consistency model, quorum আর consensus-এ (CAP: অংশ ৭)।

বাস্তবে — Kubernetes আর etcd

Kubernetes-এর পুরো ক্লাস্টারের অবস্থা — কোন pod কোথায়, কোন config — রাখা হয় etcd-এ, যা Raft দিয়ে সাধারণত ৩ বা ৫টি যন্ত্রে replicate হয় (৩টিতে ১টি, ৫টিতে ২টি ব্যর্থতা সয়)। Controller-রা API server-এর Lease object (যা শেষ পর্যন্ত etcd-তেই থাকে) দিয়ে নেতা বাছাই করে, যাতে একই কাজ দুজন না করে; প্রতিটি node-এর kubelet নিয়মিত heartbeat (node lease) পাঠায়, দেরি হলে node “NotReady” হয়। অর্থাৎ এই পর্বের প্রায় প্রতিটি ধারণা — RPC (API server-এর সাথে gRPC/HTTP), lease, heartbeat, election, consensus — একটি সাধারণ ক্লাস্টারেই প্রতিদিন চলছে, প্রতিটি যন্ত্রের Linux kernel-এর ওপরে।

আরও গভীরে: Google-এর Spanner (২০১২) GPS আর পারমাণবিক ঘড়ি দিয়ে TrueTime নামে একটি API দেয়, যা একটি সময় নয়, একটি ব্যবধান [earliest, latest] ফেরত দেয়; commit-এর পরে সেই অনিশ্চয়তার সমান সময় অপেক্ষা করে (commit wait) physical time দিয়েই বিশ্বজুড়ে ক্রম রক্ষা করে। আরেকটি পথ hybrid logical clock (Kulkarni ও সহকর্মী, ২০১৪) — physical time-এর কাছাকাছি থেকে Lamport-এর নিশ্চয়তা দেয়; CockroachDB-র মতো ডেটাবেস এটি ব্যবহার করে। Linux-এ CLOCK_REALTIME, CLOCK_MONOTONIC, CLOCK_BOOTTIME আলাদা ঘড়ি, আর kernel NTP-র নির্দেশে adjtimex দিয়ে ঘড়ির হার সূক্ষ্মভাবে বদলায়। NFSv4.1-এর session সুবিধা request-এর জন্য slot table রেখে exactly-once semantics দেওয়ার চেষ্টা করে।

পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:

  • Lamport clock-এর নিয়ম আর তার সীমাবদ্ধতা? — ঘটনায় +১, পেলে max(নিজের, বার্তার) + ১; a → b ⇒ L(a) < L(b), কিন্তু উল্টোটি নয় — concurrency ধরতে vector clock লাগে।
  • Ricart–Agrawala-তে প্রতি প্রবেশে কত বার্তা? — ২(n − ১): n − ১ REQUEST, n − ১ OK; centralised-এ ৩.
  • ρ আর δ দিলে resync interval? — দুটি ঘড়ির জন্য δ/(২ρ); যেমন ρ = ১০−৬, δ = ১ ms ⇒ ৫০০ s.
  • NFSv3 stateless কেন, লাভ কী? — প্রতিটি অনুরোধ স্বয়ংসম্পূর্ণ, তাই server crash-এর পর recovery লাগে না; দাম — locking আলাদা protocol, non-idempotent কাজে duplicate cache লাগে।
  • At-least-once আর at-most-once-এর পার্থক্য? — প্রথমটিতে retry, কাজ দুবারও হতে পারে (idempotent কাজে নিরাপদ); দ্বিতীয়টিতে server duplicate চিনে আবার চালায় না।

১০ · AI-এর যুগে distributed systems

AI খুব আত্মবিশ্বাসের সাথে retry লুপ, leader election বা ঘড়ি-মেলানোর কোড লিখে দেয়। সমস্যা হলো, distributed বাগ প্রায় কখনো ল্যাপটপে ধরা পড়ে না — একটি মেশিনে নেটওয়ার্ক হারায় না, ঘড়ি সরে না, কেউ অর্ধেক মরে না। “আমার মেশিনে তো চলছে” এখানে কোনো প্রমাণই নয়।

AI-এর সাধারণ ভুলকেন বিপজ্জনককীভাবে যাচাই
পেমেন্ট-এর মতো কাজে key ছাড়া অন্ধ retryউত্তর হারালে দুবার টাকা কাটেউপরের C প্রোগ্রামের মতো উত্তর ফেলে দিয়ে পরীক্ষা
Timeout-এ time.time() বা Date.now()NTP ঘড়ি সরালে timeout ভুলmonotonic ঘড়ি ব্যবহার হচ্ছে কি না দেখা
দুই সার্ভারের timestamp তুলনা করে “কে আগে” ঠিক করাskew কয়েক ms থেকে সেকেন্ড — ক্রম উল্টে যায়logical/vector clock বা একটি sequencer
নিজে লেখা “সহজ” leader electionsplit-brain, দুই নেতাetcd/ZooKeeper-এর মতো পরীক্ষিত টুল; partition করে পরীক্ষা
Numerical-এ ভুল সূত্র (যেমন δ/ρ আর δ/২ρ গুলিয়ে ফেলা)পরীক্ষায় নম্বর কাটা, বাস্তবে ভুল কনফিগহাতে হিসাব, ছোট simulator চালানো

AI-কে distributed কোড লেখাতে বললে ব্যর্থতার দৃশ্যগুলো আপনিই আগে বলে দিন, আর পরীক্ষার উপায় চেয়ে নিন:

আমি একটি Linux সার্ভারে (Ubuntu 24.04) payment service লিখছি, যা অন্য একটি সার্ভিসকে HTTP/gRPC দিয়ে "debit" কল করে। আমাকে একটি retry নীতি লিখে দাও। শর্ত: ১. প্রতিটি অনুরোধে একটি idempotency key থাকবে; server কীভাবে duplicate চিনবে আর কোথায় (disk/DB) key রাখবে, তা বলো। ২. timeout মাপবে monotonic clock দিয়ে; exponential backoff আর jitter থাকবে, সর্বোচ্চ কতবার তা বলো। ৩. এই চারটি ঘটনায় কী হবে, প্রতিটির জন্য আলাদা করে ব্যাখ্যা করো: অনুরোধ হারানো, উত্তর হারানো, server কাজের পরে crash, server শুধু ধীর। ৪. প্রতিটি ঘটনা আমি কীভাবে নিজে পরীক্ষা করব (যেমন উত্তর ইচ্ছা করে ফেলে দিয়ে), সেটাও দাও। নিশ্চিত না হলে বলো, অনুমান করো না।

AI-এর distributed নকশা মানার আগে — চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১“Exactly-once delivery অসম্ভব” — তাহলে ব্যাংক, bKash বা Kafka কীভাবে দাবি করে যে টাকা বা বার্তা ঠিক একবারই যায়?

অসম্ভবতার মূল একটি পুরোনো চিন্তা-পরীক্ষা — দুই সেনাপতির সমস্যা (Two Generals). দুই পাহাড়ে দুই সেনাপতি, মাঝে শত্রুর উপত্যকা দিয়ে দূত যায়, দূত ধরা পড়তে পারে। “কাল ভোরে আক্রমণ” পাঠালে প্রথম জন জানেন না দূত পৌঁছেছে কি না; প্রাপ্তিস্বীকার পাঠালে দ্বিতীয় জন জানেন না সেটি পৌঁছেছে কি না — এভাবে অনন্ত। বার্তা হারাতে পারে এমন নেটওয়ার্কে কোনো সীমিত সংখ্যক বার্তায় দুই পক্ষ নিশ্চিত হতে পারে না। RPC-র ভাষায়: উত্তর না পেলে client জানে না কাজটি হয়েছে কি না।

তাহলে দাবিগুলো কী? এগুলো আসলে “কার্যত একবার” (effectively-once) — delivery একাধিকবার হতে পারে, কিন্তু প্রভাব একবার। পদ্ধতি তিনটি স্তরের জোড়া: (১) at-least-once পাঠানো — উত্তর না পেলে retry; (২) প্রতিটি অনুরোধে একটি অনন্য id বা idempotency key; (৩) প্রাপক সেই id টেকসইভাবে (ডেটাবেসে, কাজের সাথে একই transaction-এ) লিখে রাখে, আবার এলে পুরোনো ফল ফেরত দেয়। তৃতীয় শর্তটিই কঠিন অংশ: id যদি কাজের পরে আলাদাভাবে লেখা হয়, মাঝখানে crash হলে কাজ হয়েছে কিন্তু id নেই — পরের retry আবার কাজ করবে।

Kafka-র “exactly-once semantics” (২০১৭-এর দিকে এসেছে) এভাবেই কাজ করে বলে নথিতে আছে: producer-কে একটি id আর প্রতিটি বার্তায় sequence number দিয়ে broker duplicate বাদ দেয়, আর transaction দিয়ে “পড়া–প্রক্রিয়া–লেখা” একসাথে commit হয়। কিন্তু প্রতিশ্রুতিটি Kafka-র ভেতরেই সীমাবদ্ধ; বাইরের একটি email পাঠানো বা অন্য ব্যাংকের API ডাকা সেই transaction-এর অংশ নয় — সেখানে আবার নিজের idempotency লাগে। পেমেন্ট নেটওয়ার্কগুলোতেও reconciliation — দিনের শেষে দুই পক্ষের হিসাব মেলানো — একটি শেষ রক্ষাকবচ হিসেবে থাকে।

মূল উপলব্ধি: নেটওয়ার্ক একবার পৌঁছানোর নিশ্চয়তা দিতে পারে না, কিন্তু সিস্টেম একবার প্রভাব ফেলার নিশ্চয়তা দিতে পারে — retry, অনন্য id আর একই transaction-এ id সংরক্ষণ দিয়ে। কেউ “exactly-once” বললে জিজ্ঞেস করুন: কোন সীমানার ভেতরে, আর id কোথায় টেকসইভাবে রাখা হয়?

প্র ০২ঘড়ি সঠিক রাখা এত কঠিন কেন যে leap second বা ঘড়ির লাফ বড় সার্ভিস বন্ধ করে দিয়েছে? Physical time কি কখনো ক্রমের জন্য যথেষ্ট?

পৃথিবীর ঘূর্ণন পুরোপুরি নিয়মিত নয়, তাই UTC-কে জ্যোতির্বিদ্যার সময়ের কাছে রাখতে মাঝে মাঝে একটি leap second যোগ করা হয়েছে — ঘড়ি 23:59:59-এর পরে 23:59:60 দেখায়। Unix time এই অতিরিক্ত সেকেন্ড প্রকাশ করতে পারে না, তাই সাধারণত একই সেকেন্ড দুবার আসে — অর্থাৎ ঘড়ি কার্যত পেছনে যায়।

বাস্তব ঘটনা: ৩০ জুন ২০১২-র leap second-এর সময় Linux kernel-এর একটি বাগের কারণে অনেক সার্ভারে Java আর MySQL-এর মতো প্রোগ্রাম হঠাৎ পুরো CPU খেতে শুরু করে, আর কয়েকটি বড় ওয়েবসাইট সমস্যায় পড়ে বলে তখন ব্যাপকভাবে রিপোর্ট হয়। ১ জানুয়ারি ২০১৭-র leap second-এ Cloudflare জানায়, তাদের DNS সফটওয়্যারের একটি অংশ ধরে নিয়েছিল সময় কখনো পেছায় না; দুটি timestamp-এর বিয়োগফল ঋণাত্মক হয়ে কিছু DNS অনুরোধ ব্যর্থ হয়। এর পরে Go ভাষা (১.৯ থেকে) time.Now()-এর ভেতরে একটি monotonic মানও রাখতে শুরু করে, যাতে সময়ের পার্থক্য মাপা ঘড়ির লাফে প্রভাবিত না হয়। Google-এর মতো কোম্পানিগুলো “leap smear” ব্যবহার করে — এক সেকেন্ডকে কয়েক ঘণ্টা জুড়ে একটু একটু করে ছড়িয়ে দেওয়া, ঠিক slewing-এর মতো। আন্তর্জাতিক সংস্থাগুলো ২০৩৫ সালের মধ্যে leap second বন্ধ করার সিদ্ধান্ত নিয়েছে বলে জানা গেছে।

Physical time কি ক্রমের জন্য যথেষ্ট? সাধারণভাবে না: দুই সার্ভারের skew যদি ৫ ms হয়, ৫ ms-এর কম ব্যবধানে ঘটা দুটি ঘটনার ক্রম timestamp দেখে ঠিক করা যায় না। “Last write wins” নীতিতে এর ফল — নতুন লেখা পুরোনোর নিচে চাপা পড়ে, নীরবে। ব্যতিক্রম Spanner-এর মতো সিস্টেম, যারা অনিশ্চয়তা মেপে (TrueTime) সেই সময়টুকু অপেক্ষা করে — কিন্তু সেজন্য প্রতিটি ডেটা সেন্টারে GPS আর পারমাণবিক ঘড়ি লাগে।

মূল উপলব্ধি: Wall-clock মানুষের জন্য, ক্রম মেশিনের জন্য। সময়ের পার্থক্য মাপতে monotonic ঘড়ি, কারণ-সম্পর্কের ক্রমে logical/vector clock, আর physical time দিয়ে ক্রম চাইলে অনিশ্চয়তার সীমা জানতে হবে।

প্র ০৩NFS শুরুতে ইচ্ছা করে stateless ছিল। তাহলে NFSv4 কেন stateful হলো — আগের নকশা কি ভুল ছিল?

১৯৮০-এর দশকে NFS-এর নকশাকারদের মূল চিন্তা ছিল crash recovery. Server যদি কিছুই মনে না রাখে, তাহলে crash করে reboot হলে কোনো অবস্থা পুনর্গঠন করতে হয় না — client-রা শুধু নিজেদের অনুরোধ আবার পাঠায়, আর ব্যবহারকারী দেখেন কয়েক সেকেন্ড থেমে থাকা, তারপর সব স্বাভাবিক। প্রতিটি READ/WRITE-এ file handle আর offset থাকে, তাই অনুরোধ স্বয়ংসম্পূর্ণ; আর এগুলো idempotent হওয়ায় সস্তা UDP-ভিত্তিক at-least-once RPC যথেষ্ট ছিল। ছোট LAN-এর জন্য সেটি চমৎকার প্রকৌশল।

কিন্তু stateless-এর দাম ধীরে ধীরে চোখে পড়ে। প্রথমত, file locking স্বভাবতই অবস্থা — কে কোন অংশ ধরে আছে। তাই আলাদা NLM protocol আর status monitor যোগ করতে হয়, যার crash recovery জটিল আর ভঙ্গুর বলে পরিচিত। দ্বিতীয়ত, REMOVE বা RENAME-এর মতো non-idempotent কাজ retry হলে ভুল error (“ফাইল নেই”) আসতে পারে, তাই server-এ duplicate request cache যোগ করতে হয় — অর্থাৎ কিছু অবস্থা লুকিয়ে ঢোকে। তৃতীয়ত, server জানে না কে কোন ফাইল cache করছে, তাই client-দের বারবার attribute যাচাই করতে হয় — বেশি নেটওয়ার্ক কল আর দুর্বল consistency. চতুর্থত, অনেকগুলো আলাদা protocol আর port (mount, lock, portmapper) firewall আর WAN-এ ঝামেলার।

NFSv4 (IETF-এর অধীনে, প্রথম RFC ২০০০ সালে) এগুলো এক জায়গায় আনে: OPEN/CLOSE আর lock protocol-এর ভেতরে, একটি port, COMPOUND অনুরোধ, আর delegation — যা AFS-এর callback-এর মতো ধারণা। Crash recovery-র সমস্যা সমাধান হয় lease দিয়ে: অবস্থা থাকে, কিন্তু মেয়াদসহ; server restart-এর পরে grace period-এ client-রা দাবি নবায়ন করে, আর যে client মরে গেছে তার lease আপনা থেকেই শেষ হয়।

মূল উপলব্ধি: আগের নকশা ভুল ছিল না — ভিন্ন প্রয়োজনের জন্য সঠিক ছিল। “Stateless” recovery সহজ করে, কিন্তু locking, cache আর দক্ষতার জন্য অবস্থা লাগেই; lease হলো মাঝের পথ: “অবস্থা থাকুক, কিন্তু মেয়াদসহ”, যাতে মৃত পক্ষের অবস্থা চিরকাল আটকে না থাকে।

প্র ০৪একটি সার্ভার “মৃত” না শুধু “ধীর” — এটি নিশ্চিতভাবে জানা যায় না কেন, আর ভুল সিদ্ধান্তে কী বিপদ হয়?

বাইরে থেকে একটি যন্ত্র সম্পর্কে আমরা জানি শুধু তার বার্তা দিয়ে। Heartbeat না এলে সম্ভাব্য কারণ: যন্ত্রটি crash করেছে; নেটওয়ার্ক কাটা; সুইচের queue ভরা বলে প্যাকেট দেরিতে; অথবা যন্ত্রটি বেঁচে আছে কিন্তু থেমে আছে — garbage collection-এর দীর্ঘ বিরতি, VM-কে hypervisor কিছুক্ষণ CPU দেয়নি (পর্ব ৫০), ডিস্ক swap-এ আটকে (পর্ব ৩৭)। একটি asynchronous নেটওয়ার্কে দেরির কোনো ঊর্ধ্বসীমা নেই, তাই যত বড় timeout-ই রাখুন, “আরেকটু অপেক্ষা করলে হয়তো উত্তর আসত” — সবসময় সম্ভব। ১৯৯৬ সালে Chandra আর Toueg failure detector-কে এভাবেই সংজ্ঞায়িত করেন: নিখুঁত নয়, কিছু নির্দিষ্ট ভুলের সীমা মেনে চলা একটি “সন্দেহকারী”।

ভুল সিদ্ধান্তের বিপদ নির্ভর করে কী করা হচ্ছে তার ওপর। শুধু অনুরোধ অন্য সার্ভারে পাঠালে মিথ্যা সন্দেহে ক্ষতি সামান্য। কিন্তু নেতা বদলালে বিপদ বড়: পুরোনো নেতা হয়তো ১৫ সেকেন্ডের GC বিরতি থেকে জেগে ভাবে সে এখনো নেতা, আর লিখতে থাকে — নতুন নেতাও লিখছে। এটাই split-brain; ডেটাবেসে দুই primary হলে ডেটা বিভক্ত হয়ে যায়, যা পরে মেলানো কঠিন বা অসম্ভব।

প্রকৌশলীরা তাই কয়েকটি স্তর জোড়েন। (১) Majority quorum: নেতা হতে অর্ধেকের বেশির সমর্থন লাগে, তাই দুই ভাগের একটিতেই কেবল নেতা থাকে। (২) Lease: নেতা নিজেই জানে তার নেতৃত্বের মেয়াদ কত; মেয়াদ শেষ হলে নবায়ন না পেয়ে নিজে থেকে লেখা বন্ধ করে — তবে এটি ঘড়ির গতি মোটামুটি একই ধরে নেয়। (৩) Fencing token: প্রতিটি নতুন নেতৃত্বের নম্বর বাড়ে, আর স্টোরেজ পুরোনো নম্বরের লেখা প্রত্যাখ্যান করে — জেগে ওঠা পুরোনো নেতা ক্ষতি করতে পারে না। Cassandra-র মতো সিস্টেম “φ accrual” ধরনের detector ব্যবহার করে বলে জানা যায়, যা হ্যাঁ/না নয়, সন্দেহের একটি মাত্রা দেয়, আর প্রয়োগ অনুযায়ী সীমা ঠিক করা যায়।

মূল উপলব্ধি: Distributed system-এ “মৃত” একটি সিদ্ধান্ত, তথ্য নয়। ভালো নকশা ধরে নেয় সিদ্ধান্ত ভুল হতে পারে, আর এমনভাবে বানানো হয় যাতে ভুল হলেও দুই পক্ষ একসাথে ক্ষতি করতে না পারে।

প্র ০৫Amoeba, Sprite, Plan 9-এর মতো distributed OS বাজারে টেকেনি, অথচ Kubernetes-কে “ডেটা সেন্টারের OS” বলা হয়। পার্থক্যটা কোথায়?

Distributed OS-এর স্বপ্ন ছিল পূর্ণ স্বচ্ছতা: ব্যবহারকারী আর প্রোগ্রাম জানবেই না যে অনেক যন্ত্র আছে — ফাইল, প্রসেস, ডিভাইস সব এক namespace-এ, প্রসেস নিজে থেকে ফাঁকা যন্ত্রে সরে যাবে। গবেষণায় এগুলো চমৎকার কাজ করেছে, আর অনেক ধারণা (Sprite-এর log-structured file system, Plan 9-এর namespace আর UTF-8) পরে সবার ব্যবহারে এসেছে।

টেকেনি কেন — কয়েকটি কারণ একসাথে। প্রথম, সামঞ্জস্য: মানুষের হাতে ছিল বিপুল Unix আর Windows সফটওয়্যার; নতুন OS মানে সব নতুন করে লেখা বা port করা। ১৯৯০-এর দশকে Linux আর সস্তা PC এই অর্থনীতি আরও শক্ত করে। দ্বিতীয়, স্বচ্ছতার সীমা: ১৯৯৪ সালে Sun-এর Jim Waldo ও সহকর্মীদের “A Note on Distributed Computing” যুক্তি দেয়, দূরের আর কাছের কাজকে পুরোপুরি এক রকম দেখানো বিপজ্জনক — latency, partial failure আর আলাদা memory লুকানো যায় না, আর লুকালে প্রোগ্রাম সেগুলো সামলাতে শেখে না। তৃতীয়, ব্যর্থতার প্রসার: সব যন্ত্র এক kernel-এর মতো আচরণ করলে একটির সমস্যা সবার সমস্যা হতে পারে।

Kubernetes উল্টো পথ নিয়েছে। প্রতিটি যন্ত্রে সাধারণ Linux kernel থাকে, আর ক্লাস্টার-স্তরের কাজ middleware করে: কোথায় কোন container চলবে (scheduling), কত CPU/মেমরি পাবে (cgroup-এর মাধ্যমে, পর্ব ৫১), কে কাকে কোন নামে খুঁজে পাবে (service discovery), কেউ মরলে আবার চালু করা। এটি কোনো প্রোগ্রামকে এক কম্পিউটারের ভ্রম দেয় না — বরং প্রোগ্রামকে ধরে নিতে হয় যে তার pod যেকোনো সময় মরবে আর অন্য জায়গায় জন্মাবে। এর পূর্বসূরি Google-এর ভেতরের Borg, যার বর্ণনা ২০১৫ সালে প্রকাশিত হয়; Kubernetes প্রকাশ্যে আসে ২০১৪ সালে।

মূল উপলব্ধি: জিতেছে “স্বচ্ছতা” নয়, “স্পষ্টতা” — distributed হওয়াকে লুকানো নয়, বরং ব্যর্থতা আর দেরিকে প্রথম শ্রেণির ধারণা বানিয়ে প্রোগ্রামারের হাতে টুল দেওয়া। OS-এর দায়িত্বগুলো (scheduling, isolation, naming) একই আছে, শুধু স্তরটি উঠে গেছে kernel-এর ওপরে।

অনুশীলন

আগে খাতায় হাতে করুন, তারপর উত্তর বা simulator দিয়ে মেলান।

  1. Drift-এর numerical: দুটি সার্ভারের ঘড়ির সর্বোচ্চ drift rate ρ = ২ × ১০−৫. (ক) একটি ঘড়ি এক দিনে সর্বোচ্চ কত সরতে পারে? (খ) দুটির পার্থক্য ১০ ms-এর মধ্যে রাখতে কত পরপর মেলাতে হবে? (গ) Cristian-এ RTT = ৪০ ms আর server-এর পাঠানো সময় 08:30:00.200 হলে client কোন সময় বসাবে, ভুলের সীমা কত?

    (ক) ৮৬,৪০০ × ২ × ১০−৫ = ১.৭২৮ s — দিনে প্রায় পৌনে দুই সেকেন্ড। (খ) Δt ≤ δ/(২ρ) = ০.০১/(৪ × ১০−৫) = ২৫০ s, অর্থাৎ ৪ মিনিট ১০ সেকেন্ড পরপর। (গ) Ts + RTT/২ = 08:30:00.200 + ২০ ms = 08:30:00.220, ভুল সর্বোচ্চ ±২০ ms (এক দিকের ন্যূনতম দেরি জানা থাকলে আরও কম)। Client এগিয়ে থাকলে লাফ নয়, slew.

  2. হাতে trace — Lamport আর vector: দুই প্রসেস। P1: x = m পাঠানো (P2-কে), তারপর y (local), তারপর z = n পাওয়া। P2: u (local), তারপর v = m পাওয়া, তারপর w = n পাঠানো (P1-কে)। প্রতিটি ঘটনার Lamport timestamp আর vector clock বের করুন। y আর v-এর সম্পর্ক কী? u আর z-এর?

    Lamport: x = ১, y = ২, z = max(২, ৩) + ১ = ৪; u = ১, v = max(১, ১) + ১ = ২, w = ৩. Vector: x = [১, ০], y = [২, ০], u = [০, ১], v = [১, ২], w = [১, ৩], z = max([২, ০], [১, ৩]) + P1-এর ঘর = [৩, ৩].

    y = [২, ০] আর v = [১, ২]: প্রথম ঘরে y বড়, দ্বিতীয় ঘরে v বড় — concurrent, যদিও দুজনেরই Lamport timestamp ২. u = [০, ১] ≤ z = [৩, ৩] — u → z (u → v → w → z শিকল)। উপরের vector simulator-এর script বদলে মিলিয়ে নিতে পারেন।

  3. আউটপুট অনুমান: উপরের rpc_retry.c-তে server-এর লাইনটি বদলানো হলো, যাতে প্রথম দুটি উত্তর হারায়: if (dropped < 2) { dropped++; continue; }. এখন কী ছাপা হবে? naive আর dedup-এ শেষ balance কত? (সাবধান — লাইনটি কোডের কোন শাখায় আছে, দেখে নিন।)
    naive: attempt 1: no reply in 100 ms, resend id=1
    naive: attempt 2: no reply in 100 ms, resend id=1
    naive: attempt 3: reply id=1 balance=2500
    dedup: attempt 1: no reply in 100 ms, resend id=1
    dedup: attempt 2: reply id=1 balance=1500

    Naive: প্রতিটি চেষ্টা “নতুন অনুরোধ” শাখায় যায়, তাই তিনবারই ৫০০ যোগ হয় (১০০০ + ১৫০০ = ২৫০০), প্রথম দুটি উত্তর হারায়, তৃতীয়টি পৌঁছায়। Dedup: প্রথম চেষ্টায় কাজ হয়, উত্তর হারায় (dropped = ১)। দ্বিতীয় চেষ্টা duplicate — সে drop-এর লাইন পর্যন্ত যায়ই না, সরাসরি জমা উত্তর পায়। তাই dedup-এ দ্বিতীয় হারানো কখনো ঘটে না, ৫ লাইন ছাপা হয়। এই সূক্ষ্মতাই দেখায়, কেন অনুমানের পরে চালিয়ে মিলিয়ে নেওয়া জরুরি।

  4. Ricart–Agrawala হাতে চালানো: চার প্রসেস P1–P4. P1 timestamp ৮ নিয়ে আর P3 timestamp ৫ নিয়ে প্রায় একসাথে critical section চাইল; P2, P4 আগ্রহী নয়। কে আগে ঢুকবে? কে কার উত্তর জমা রাখবে? দুজনের প্রবেশ মিলিয়ে মোট কত বার্তা?

    P2 আর P4 দুজনকেই সাথে সাথে OK দেয়। P1, P3-এর অনুরোধ (৫) পেয়ে দেখে নিজের (৮) বড়, তাই OK দেয়। P3, P1-এর অনুরোধ (৮) পেয়ে দেখে নিজের (৫) ছোট, তাই উত্তর জমা রাখে। P3 তিনটি OK পেয়ে আগে ঢোকে; বের হয়ে জমা OK পাঠায়, তখন P1 তিনটি OK পেয়ে ঢোকে। বার্তা: প্রত্যেকের ২(n − ১) = ৬, মোট ১২ — উত্তর দেরিতে গেলেও সংখ্যা বদলায় না।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি RPC কলের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Client প্রোগ্রাম সাধারণ ফাংশনের মতো client stub ডাকে
  2. Client stub argument আর request id বাইটে সাজায় (marshalling)
  3. OS আর নেটওয়ার্ক বার্তাটি server মেশিনে পৌঁছায়
  4. Server stub বাইট থেকে argument খোলে (unmarshalling)
  5. Server-এর আসল ফাংশন চলে, ফল উত্তরে সাজানো হয়
  6. Client stub উত্তর খুলে client প্রোগ্রামে return করে

দুটি ঘটনা a আর b-এর Lamport timestamp L(a) = 3 আর L(b) = 5. নিশ্চিতভাবে কী বলা যায়?

  • a → b (a, b-এর আগে ঘটেছে)
  • b → a নয় (b, a-র আগে ঘটেনি)
  • a আর b অবশ্যই concurrent
  • a আর b একই প্রসেসের ঘটনা

তিন প্রসেসের সিস্টেমে ঘটনা x-এর vector clock [2, 1, 0] আর ঘটনা y-এর [1, 2, 0]. সম্পর্ক কী?

  • x → y
  • y → x
  • x আর y concurrent
  • x আর y একই ঘটনা

Ricart–Agrawala algorithm-এ ৬টি প্রসেস থাকলে একটি প্রসেসের একবার critical section-এ প্রবেশের জন্য মোট কতটি বার্তা লাগে?

  • 3
  • 10
  • 15
  • 12

Cristian-এর algorithm-এ client নিজের ঘড়িতে 12:00:00.000-এ অনুরোধ পাঠিয়ে 12:00:00.030-এ উত্তর পেল; উত্তরে server-এর সময় 12:00:00.500. Client কোন সময় বসাবে?

  • 12:00:00.500
  • 12:00:00.530
  • 12:00:00.470
  • 12:00:00.515

At-least-once semantics-এর RPC-তে retry হলে নিচের কোন কাজটি নিরাপদ নয়?

  • ফাইলের 4096 offset থেকে 4 KB পড়া
  • account-এর ঠিকানা “Rajshahi” সেট করা
  • account balance-এ 500 টাকা যোগ করা
  • ফাইলের 8192 offset-এ নির্দিষ্ট 4 KB ডেটা লেখা

Bully algorithm: প্রসেস P1–P6, coordinator P6 crash করেছে, আর P5 প্রথম টের পেয়ে নির্বাচন শুরু করল। নির্বাচন শেষ হতে মোট কতটি বার্তা (ELECTION, OK, COORDINATOR) লাগবে?

  • 5
  • 9
  • 20
  • 1

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Distributed মানে: shared memory নেই, এক ঘড়ি নেই, ব্যর্থতা আংশিক; “মৃত”, “ধীর” আর “নেটওয়ার্ক কাটা” বাইরে থেকে এক। আটটি ভ্রান্তি মনে রাখুন — নেটওয়ার্ক নির্ভরযোগ্য নয়, latency শূন্য নয়।
  2. RPC: stub + marshalling + binding. Timeout-এ কাজ হয়েছে কি না অজানা; at-least-once (retry, idempotent কাজে), at-most-once (duplicate cache), কার্যত exactly-once = retry + request id + টেকসই রেকর্ড।
  3. Physical clock: resync interval δ/(২ρ); Cristian = Ts + RTT/২, ভুল ±RTT/২; NTP θ = ((t₂ − t₁) + (t₃ − t₄))/২, δ = (t₄ − t₁) − (t₃ − t₂); Berkeley = গড় আর পার্থক্য পাঠানো; পেছাতে হলে slew, timeout-এ monotonic ঘড়ি।
  4. Logical clock: Lamport — +১, পেলে max + ১, a → b ⇒ L(a) < L(b); vector — a → b ⇔ V(a) ≤ V(b), V(a) ≠ V(b), নইলে concurrent.
  5. সমন্বয় ও ফাইল: mutex — centralised ৩, Ricart–Agrawala ২(n − ১), token ring ১…∞; bully — বড় id জেতে, সেরা n − ১, খারাপ O(n²); NFS v3 stateless, v4 lease-সহ stateful, close-to-open; AFS whole-file + callback; GFS/HDFS বড় chunk, ৩ কপি; consensus (Raft) — etcd, Kubernetes.

পরের পর্বে: ডেটা সেন্টারের বিশাল ক্লাস্টার থেকে এবার হাতের মুঠোয় — পর্ব ৫৪-এ মোবাইল, embedded আর real-time OS: Android, iOS আর RTOS.

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান