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

Case study: ride-sharing — location update, geohash আর matching

Case Study: Ride-Sharing — Location Updates, Geohash & Matching

Taught in Bangla, this lesson designs a ride-sharing app as a write-heavy problem: estimating driver location updates and memory, the rider, driver and location APIs, storing latest positions, route history and trip state separately, geohash with neighbouring cells, quadtrees and hexagon grids, sharding by cell, ETA-based and batch matching, offer timeouts, compare-and-set and the trip state machine, with hands-on simulators.

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

  • একটি ride-sharing অ্যাপের requirement, পরিধি আর হিসাব — কেন এটি লেখা-ভারী সমস্যা
  • API, data model আর পুরো architecture — যাত্রীর অনুরোধ থেকে trip শুরু পর্যন্ত
  • চালকের অবস্থান গ্রহণ, GPS-এর ভুল, নেটওয়ার্ক কেটে যাওয়া আর পথের ইতিহাস রাখা
  • Geohash-এর ভেতরের কাজ, পাশের ঘর, quadtree ও hexagon grid, আর cell ধরে sharding
  • ETA-ভিত্তিক ও batch matching, offer-এর timeout, CAS আর trip-এর state machine; ভাঙন, নিরাপত্তা আর ইন্টারভিউর উত্তর

১ · সমস্যা আর পরিধি — কী বানাচ্ছি, কী বানাচ্ছি না

ধরা যাক আমরা “পথিক” নামের একটি কাল্পনিক ride-sharing অ্যাপ বানাচ্ছি। ঢাকার মতো একটি ঘনবসতির শহরে যাত্রী অ্যাপ খুলে গন্তব্য দেন, ভাড়ার অনুমান দেখেন, “অনুরোধ” চাপেন। কয়েক সেকেন্ডে কাছের একজন চালক — বাইক, গাড়ি বা CNG — গ্রহণ করেন, আর যাত্রী মানচিত্রে তাঁকে এগিয়ে আসতে দেখেন। আমরা পর্ব ৬ — একটি নকশা কীভাবে এগোয়-এর আট ধাপ ধরে এগোব; আগের ধারণাগুলোর শুধু প্রয়োগ আর মূল পর্বের লিংক থাকবে।

রেলস্টেশনের বাইরের CNG স্ট্যান্ডের লাইনম্যান জানেন কোন গাড়ি খালি, কে কোথায়। যাত্রী এলে কাছের গাড়িকে ডাকেন — রাস্তা ধরে কাছের, কারণ একমুখী রাস্তার ওপারের গাড়ি ঘুরে আসতে দশ মিনিট। পাঁচজন একসাথে এলে ভেবে জোড়া মেলান, আর একই গাড়িকে দুজনের দিকে পাঠান না। আমাদের সিস্টেম এই লাইনম্যানের কাজই করবে — লাখো গাড়ি আর পুরো শহর জুড়ে।

Functional requirement — যা করতেই হবে

  • যাত্রী: ভাড়ার অনুমান দেখা, গাড়ির ধরন বেছে অনুরোধ বা বাতিল, চালকের চলমান অবস্থান আর ETA দেখা।
  • চালক: অনলাইন হওয়া, কয়েক সেকেন্ড পরপর অবস্থান পাঠানো, offer গ্রহণ বা প্রত্যাখ্যান, trip শুরু ও শেষ।
  • সিস্টেম: কাছের চালক খুঁজে একজনকে offer (dispatch), trip-এর জীবনচক্র, পথ ধরে ভাড়া, পরিশোধ শুরু — পরিশোধের নকশা পর্ব ৬০ — Case study: ticket booking আর payment-এ।

Non-functional requirement — কতটা ভালোভাবে

  • সতেজতা আর গতি: matching-এর দেখা অবস্থান ধরা যাক ১০ সেকেন্ডের মধ্যে; প্রথম offer কয়েক সেকেন্ডে, খোঁজার p99 শ-খানেক ms-এর ঘরে (লক্ষ্য)।
  • সঠিকতা: একজন চালক একসাথে দুটি trip নয়, একটি trip দুজন চালক নয়, একবার চাপলে একটিই trip — কঠোর consistency; অবস্থানে eventual যথেষ্ট — পর্ব ৪০ — CAP, PACELC আর consistency model.
  • Availability: অনুরোধ আর চলমান trip সবসময়; বিশ্লেষণ পিছিয়ে থাকলেও চলে — পর্ব ৫ — Availability আর reliability.
  • গোপনীয়তা: অবস্থান থেকে কে কোথায় থাকেন, কোথায় কাজ করেন — সব বোঝা যায় (অংশ ৯)।
  • বাংলাদেশের বাস্তবতা (ধরা যাক): সবাই মোবাইলে; চলন্ত অবস্থায় ডেটা মাঝে মাঝে কাটে; ব্যাটারি আর ডেটা খরচ গুরুত্বপূর্ণ।

পরিধির বাইরে: পরিশোধ আর ledger (পর্ব ৬০), notification-এর বিস্তারিত (পর্ব ৫৭ — Case study: notification system), চালকের নিবন্ধন, rating, শেয়ার রাইড (অনুশীলন ২), ডেলিভারি আর মানচিত্র তৈরি — রাস্তার graph আর routing engine আছে বলে ধরছি।

ইন্টারভিউয়ার যে প্রশ্নগুলো আশা করেন

মাপ

কয়টি শহর, ব্যস্ত সময়ে কতজন চালক, দিনে কত trip?

অংশ ২-এর হিসাব

সতেজতা

অবস্থান কত সেকেন্ড পুরোনো হলে চলে? ঘনঘন পাঠালে লেখা, ব্যাটারি আর ডেটা খরচ বাড়ে।

৪ সেকেন্ড? ১০?

Matching-এর লক্ষ্য

এই যাত্রীর সবচেয়ে কাছের চালক, নাকি সবার মোট অপেক্ষা কম?

অংশ ৮

ভুলের দাম

Double assignment কতটা ক্ষতিকর, আর পুরোনো অবস্থান কতটা সহনীয়?

consistency কোথায়

২ · হিসাব — লেখার ঢেউ বড়, memory ছোট

পর্ব ৩ — Back-of-the-envelope হিসাব-এর নিয়মে আগে অনুমানগুলো লিখে নিই। সংখ্যাগুলো কোনো আসল কোম্পানির নয় — সবই “ধরা যাক”, আর নিচের প্রোগ্রামে বদলে দেখা যায়।

অনুমান (ধরা যাক)মানকেন এটি দরকার
ব্যস্ত সময়ে অনলাইন চালক, সব শহর মিলে১,০০,০০০ (সবচেয়ে বড় শহরে ৫০,০০০)লেখার হার আর memory
অবস্থান পাঠানোর ব্যবধানপ্রতি ৪ সেকেন্ডলেখার হার আর সতেজতা
দিনভর গড়ে অনলাইন চালক৪০,০০০দিনে মোট বার্তা
দিনে সম্পন্ন trip১০ লাখ; ব্যস্ত ঘণ্টা গড়ের ৩ গুণঅনুরোধের হার, ইতিহাস
গড় trip-এর দৈর্ঘ্য২০ মিনিটtrip-এর পথের বিন্দু
একটি বার্তা wire-এ; memory-তে একটি এন্ট্রি; জমা একটি বিন্দু১৫০ B; ২০০ B; ৫০ Bbandwidth, memory, storage
// Ride-sharing estimator. Every input is an ASSUMPTION (dhora jak) - change them and re-run.
const A = {
  peakDrivers: 100000,      // online drivers at the busiest hour, all cities
  bigCity: 50000,           // of them, in the largest city
  everySec: 4,              // a driver app sends GPS every 4 s
  wireBytes: 150,           // one location message on the wire (payload + framing + TLS)
  memBytes: 200,            // one "latest location" entry incl. index overhead
  avgDrivers: 40000,        // online drivers averaged over 24 h
  tripsPerDay: 1000000,     // completed trips per day
  peakFactor: 3,            // busiest hour vs daily average
  tripMin: 20,              // average trip length (minutes)
  pointBytes: 50,           // one stored trace point, compact binary
  eventsPerTrip: 8,         // requested, offered, accepted, arrived, started, ended, paid, rated
  copies: 3,                // replication factor for stored traces
};
const fmt = (x) => Math.round(x).toLocaleString("en-US");
const writes = A.peakDrivers / A.everySec, city = A.bigCity / A.everySec;
console.log("location writes/s at peak : " + fmt(writes) + "  (largest city " + fmt(city) + ")");
console.log("ingest bandwidth          : " + (writes * A.wireBytes * 8 / 1e6).toFixed(0) + " Mbit/s");
console.log("memory, latest locations  : " + (A.peakDrivers * A.memBytes / 1e6).toFixed(0) + " MB");
const reqAvg = A.tripsPerDay / 86400;
console.log("ride requests/s avg, peak : " + reqAvg.toFixed(1) + ", " + (reqAvg * A.peakFactor).toFixed(0));
console.log("write : request at peak   : " + fmt(writes / (reqAvg * A.peakFactor)) + " : 1");
const allPts = A.avgDrivers * 86400 / A.everySec;
const tripPts = A.tripsPerDay * A.tripMin * 60 / A.everySec;
console.log("location events/day, all  : " + fmt(allPts));
console.log("trace points/day, on trip : " + fmt(tripPts) + "  (" + fmt(A.tripMin * 60 / A.everySec) + " per trip)");
console.log("trip state events/day     : " + fmt(A.tripsPerDay * A.eventsPerTrip));
const day = tripPts * A.pointBytes, year = day * 365;
console.log("trip traces: " + (day / 1e9).toFixed(1) + " GB/day, " + (year / 1e12).toFixed(2) +
  " TB/year, x" + A.copies + " copies = " + (year * A.copies / 1e12).toFixed(1) + " TB");
console.log("all pings for 1 year (x" + A.copies + ")  : " + (allPts * A.pointBytes * 365 * A.copies / 1e12).toFixed(1) + " TB");

আউটপুট:

location writes/s at peak : 25,000  (largest city 12,500)
ingest bandwidth          : 30 Mbit/s
memory, latest locations  : 20 MB
ride requests/s avg, peak : 11.6, 35
write : request at peak   : 720 : 1
location events/day, all  : 864,000,000
trace points/day, on trip : 300,000,000  (300 per trip)
trip state events/day     : 8,000,000
trip traces: 15.0 GB/day, 5.47 TB/year, x3 copies = 16.4 TB
all pings for 1 year (x3)  : 47.3 TB

চারটি শিক্ষা। এক, লেখা-ভারী সমস্যা: ২৫,০০০ লেখা/সেকেন্ড বনাম ৩৫টি অনুরোধ — ৭২০ : ১; নকশার কেন্দ্র লেখার পথ (পর্ব ২ — Requirement আর trade-off). দুই, সব চালকের সর্বশেষ অবস্থান মাত্র ২০ MB — ভাগ করার কারণ আকার নয়, লেখার হার, availability আর শহর আলাদা রাখা। তিন, trip-এর পথ রাখলে বছরে কপিসহ প্রায় ১৬.৪ TB, সব বার্তা রাখলে প্রায় ৪৭.৩ TB — retention খরচ আর গোপনীয়তার সিদ্ধান্ত (অংশ ৬)। চার, ব্যস্ত সময়ে ধরা যাক ৪০% চালক trip-এ, তাই যাত্রীদের দিকেও ১,০০,০০০ × ০.৪ ÷ ৪ = ১০,০০০ বার্তা/সেকেন্ড।

৩ · API — যাত্রী, চালক আর অবস্থানের বার্তা

যাত্রীর কাজগুলো সাধারণ অনুরোধ-উত্তর, তাই REST (পর্ব ৮ — REST API নকশা)। চালকের অ্যাপ সারাক্ষণ অবস্থান পাঠায় আর যেকোনো মুহূর্তে offer পেতে পারে — তাই একটি স্থায়ী সংযোগ, যেমন WebSocket বা MQTT-ধাঁচের protocol (পর্ব ১০ — Real-time যোগাযোগ)। প্রতিটি অনুরোধ আগে API gateway-তে পরিচয় যাচাই আর rate limiter পার হয় (পর্ব ১১ — Reverse proxy, API gateway আর BFF).

POST /v1/fare-quotes
{ "pickup": {"lat": 23.7801, "lon": 90.4003},
  "dropoff": {"lat": 23.8103, "lon": 90.4125}, "vehicle": "car" }

200 OK
{ "quote_id": "q_8f21", "fare_min": 310, "fare_max": 360, "currency": "BDT",
  "surge": 1.0, "pickup_eta_min": 6, "expires_in_s": 120 }

POST /v1/rides
Idempotency-Key: 5b0e-77c1
{ "quote_id": "q_8f21", "payment": "cash" }

201 Created
{ "ride_id": "r_90412", "status": "MATCHING" }

Quote-এর মেয়াদ: ভাড়া দুই মিনিট বৈধ, অনুরোধে শুধু quote_id — app ভাড়া বদলাতে পারে না, যাত্রী যা দেখেছেন তা-ই পান। Idempotency key: উত্তর হারালে app একই key-তে আবার পাঠায়; সার্ভার আগের ride_id-ই ফেরত দেয়।

→ {"t":"loc","seq":1842,"pts":[
     {"ts":1793170802,"lat":23.78112,"lon":90.40261,"acc":6,"spd":7.9,"hdg":84},
     {"ts":1793170806,"lat":23.78118,"lon":90.40292,"acc":7,"spd":8.1,"hdg":86}]}
← {"t":"offer","offer_id":"of_771","ride_id":"r_90412","pickup_eta_min":4,
   "expires_in_s":15}
→ {"t":"accept","offer_id":"of_771"}
← {"t":"assigned","ride_id":"r_90412"}          (or {"t":"offer_gone"} if too late)

অবস্থানের বার্তায় ক্রমিক sequence number, ফোনের সময় (ts), নির্ভুলতা (acc, মিটারে), গতি আর দিক; নেটওয়ার্ক খারাপ হলে কয়েকটি বিন্দু একসাথে। বাস্তবে সাধারণত Protobuf-এর মতো binary format-এ — পর্ব ১২ — Data format আর serialization. accept offer_id দিয়ে idempotent; দেরিতে এলে offer_gone. Trip চলাকালে যাত্রীর app একটি subscription খোলে, যাতে সার্ভার নিজেই অবস্থান পাঠায়।

৪ · Data model — কোন ডেটা কোথায়, কোন key-তে ভাগ

সবচেয়ে বড় সিদ্ধান্ত: ডেটাকে চরিত্র অনুযায়ী আলাদা করা। সর্বশেষ অবস্থান প্রতি ৪ সেকেন্ডে বদলায়, হারালেও পরের বার্তায় ভরে যায় — soft state. Trip-এর অবস্থা আর ভাড়া হারানো চলে না। এক database-এ দুটো চালালে হয় অবস্থান-লেখায় সেটি ডোবে, নয়তো trip-এর নিয়ম ঢিলা হয় (পর্ব ২০ — SQL বনাম NoSQL).

ডেটাকোথায়Key / ভাগকেন
চালকের সর্বশেষ অবস্থান ও অবস্থাMemory-র geo index (Redis-ধাঁচের store বা নিজস্ব সার্ভিস)geohash cell ধরে shard; চালকের key-তে TTL ~৩০ sপ্রতি সেকেন্ডে হাজারো overwrite; হারালে কয়েক সেকেন্ডে পূরণ
অবস্থানের ধারা (trace)Log-ভিত্তিক stream (Kafka-ধাঁচের)driver_id — একজনের বিন্দু ক্রমে থাকেঅনেক গ্রাহক: ইতিহাস, ভাড়া, ট্রাফিক, surge
পুরোনো traceObject storage-এ সংকুচিত ফাইলশহর/দিন ধরে ফোল্ডারসস্তা, বড়, কম পড়া হয় — পর্ব ২৬
Trip ও চালকের dispatch-অবস্থাRelational DB (PostgreSQL-ধাঁচের), version column-সহশহর (city) ধরে shardCAS, transaction, কঠোর নিয়ম
Trip-এর ঘটনা (requested, accepted…)DB-তে append-only table + streamtrip_idহিসাব-নিকাশ, বিবাদ মীমাংসা
যাত্রী বা চালকের trip-ইতিহাসআলাদা read modelrider_id বা driver_id“আমার trip” পাতা — পর্ব ৩৫
ভাড়ার quoteCache, TTL ২ মিনিটquote_idক্ষণস্থায়ী
চালক, গাড়ি, প্রোফাইলRelational DB + cachedriver_idকম বদলায়, বেশি পড়া হয়

Trip DB-র partition key কেন শহর? যাত্রী, চালক আর পথ প্রায় সবসময় একই শহরে, তাই চালকের অবস্থা আর trip একই shard-এ — “চালক রাজি, trip গৃহীত” এক local transaction-এ (অংশ ৮), shard-জোড়া transaction ছাড়াই (পর্ব ২৫ — Distributed transaction). দাম: খুব বড় শহর একাই বড় shard — তখন এলাকা বা trip_id ধরে আরও ভাগ (পর্ব ২৩ — Partitioning আর sharding). rider_id-এ ইতিহাস খুঁজতে scatter-gather-এর বদলে আলাদা read model — পর্ব ৩৫ — Event sourcing আর CQRS; index-এর বাছাই পর্ব ২১ — Index আর query performance-এর অনুশীলনে।

CREATE TABLE trips (
  trip_id     BIGINT PRIMARY KEY,          -- Snowflake-style id (পর্ব ২৩)
  city_id     INT    NOT NULL,             -- partition key
  rider_id    BIGINT NOT NULL,
  driver_id   BIGINT,
  state       TEXT   NOT NULL,             -- REQUESTED, MATCHING, ACCEPTED, ...
  version     INT    NOT NULL DEFAULT 1,   -- every change: WHERE version = old
  quote_id    TEXT   NOT NULL,
  client_key  TEXT   NOT NULL,             -- the Idempotency-Key
  UNIQUE (rider_id, client_key)
);
CREATE TABLE driver_dispatch (
  driver_id   BIGINT PRIMARY KEY,
  city_id     INT    NOT NULL,
  state       TEXT   NOT NULL,             -- offline, available, offered, en_route, on_trip
  trip_id     BIGINT,
  offer_until TIMESTAMPTZ,                 -- an offer is a lease: it expires
  version     INT    NOT NULL
);

৫ · High-level design — অনুরোধ থেকে trip শুরু

বাঁ দিকে লেখার পথ: অবস্থান যায় একটি connection gateway-এ (শুধু লাখো স্থায়ী সংযোগ ধরে রাখে), তারপর location service-এ — memory-র geo index আর trace stream. ডান দিকে অনুরোধের পথ: যাত্রীর app → API gateway → ride service → matching, যে location service-এ কাছের চালক খুঁজে gateway দিয়ে offer পাঠায়।

একটি ride-sharing অ্যাপের architecture

Left: the location write path. Right: the ride request path. Bottom: shared services and pointers to other lessons

Ride-sharing অ্যাপের architecture — অবস্থান লেখার পথ আর অনুরোধের পথ চালকের অ্যাপ Driver app যাত্রীর অ্যাপ Rider app GPS · প্রতি ~৪ s HTTPS Location gateway স্থায়ী সংযোগ API gateway auth · rate limit অবস্থান offer Location service in-memory geo index wh0r2wh0r3wh0r6 cell ধরে shard সর্বশেষ অবস্থান + অবস্থা Ride service quote · অনুরোধ Matching · dispatch ETA দিয়ে বাছাই একজনকে offer খোঁজ trip-এর বিন্দু accept Trace stream log · পর্ব ৩৩ Trip service state machine · CAS event Trace store ইতিহাস · ভাড়া · ETA Trip DB শহর ধরে shard সহায়ক সার্ভিস — Ride service আর Matching ডাকে Routing · ETA Pricing · surge Notification → পর্ব ৫৭ Payment → পর্ব ৬০
ছবি ৫৮.১ — বাঁয়ে লেখার পথ (প্রতি সেকেন্ডে হাজারো অবস্থান), ডানে অনুরোধের পথ (প্রতি সেকেন্ডে কয়েক ডজন অনুরোধ)। Matching location service-এ কাছের চালক খোঁজে, আর offer যায় চালকের সংযোগ ধরে রাখা gateway দিয়েই। Trip service প্রতিটি অবস্থা-বদল event হিসেবে stream-এ লেখে। ড্যাশ দেওয়া বাক্স দুটি অন্য পর্বের বিষয়।

লেখার পথ: সংযোগ খোলার সময় gateway একবার token যাচাই করে, তারপর প্রতিটি বার্তা চালকের cell-এর shard-এ পাঠায়; shard অবস্থান overwrite করে, cell বদলালে চালককে নতুন cell-এ সরায়, আর বিন্দুটি trace stream-এ append করে। প্রতিটি সংযোগ একটি নির্দিষ্ট gateway-তে, তাই কোন চালক কোথায়, তা একটি ছোট registry-তে থাকে, যাতে offer ঠিক জায়গায় যায় (পর্ব ১৫ — Stateless বনাম stateful; একই নকশা চ্যাটের জন্য পর্ব ৫৫ — Case study: chat আর messaging system-এ)।

অনুরোধের পথ: নিচের stepper-এ একটি অনুরোধের পুরো যাত্রা, নির্দিষ্ট সংখ্যাসহ। “পরে” চাপুন।

  1. যাত্রীPOST /v1/ridesr_90412
  2. Geo indexwh0r31 + ৮প্রার্থী খোঁজা
  3. Matchingoffer + 15 sএকজনকে
  4. চালকacceptD-23
  5. TripON_TRIPপথ রেকর্ড
Ride serviceidempotency key আগে দেখা
Idempotency-Key 5b0e-77c1 → নতুন
quote q_8f21 → বৈধ (আরও 74 s)
INSERT trips (r_90412, state = MATCHING, version 1)
→ যাত্রী: 201 Created · matching queue-তে কাজ
Location service৯টি cell, তারপর ছাঁকা
pickup → geohash-6 wh0r31, সাথে ৮টি পাশের cell
পাওয়া গেল ১৪ জন চালক (ধরন: bike)
বাদ: ৩ জন trip-এ, ১ জনের অবস্থান ২১ s পুরোনো, ১ জন ভুল দিকে
প্রার্থী ৯ জন → সবচেয়ে কাছের ৫ জনের ETA চাওয়া হলো
MatchingETA দিয়ে সাজানো, একজনকে offer
D-17  সোজা দূরত্ব 0.4 km   ETA 11 min (রেললাইনের ওপারে)
D-23  সোজা দূরত্ব 0.9 km   ETA 4 min
D-08  সোজা দূরত্ব 1.1 km   ETA 5 min
CAS: D-23 available v3 → offered(r_90412) v4 ✔
offer of_771 → D-23-এর gateway, মেয়াদ 15 s
Trip serviceএক transaction-এ দুই সারি
accept of_771 (6 s পরে, মেয়াদের ভেতরে)
trips  r_90412: MATCHING v1 → ACCEPTED v2, driver D-23
driver D-23  : offered v4 → en_route v5
→ যাত্রীকে push: চালক D-23, বাইক, ETA 4 min
Trip চলছেtrace stream-এ trip_id
ARRIVED → যাত্রী উঠলেন → ON_TRIP v4
D-23-এর প্রতিটি বিন্দু: trace stream (key D-23, trip r_90412)
যাত্রীর app: চালকের অবস্থান প্রতি ~4 s, সার্ভার নিজেই পাঠায়
  1. অনুরোধ এলো। Idempotency key নতুন, quote বৈধ। Trip DB-তে r_90412, অবস্থা MATCHING, version ১। যাত্রী সাথে সাথে উত্তর পান; কাজটি যায় শহরের matching queue-তে (পর্ব ৩১ — Message queue).
  2. প্রার্থী খোঁজা। Pickup-এর cell wh0r31 আর চারপাশের ৮টিতে ১৪ জন বাইক-চালক। বাদ পাঁচজন: তিনজন ব্যস্ত, একজনের অবস্থান ২১ সেকেন্ড পুরোনো, একজন উল্টো দিকে। বাকি ৯ জনের মধ্যে কাছের ৫ জনের ETA চাওয়া হলো — ETA দামি, তাই সবার নয়।
  3. ETA দিয়ে বাছাই, একজনকে offer. সোজা দূরত্বে কাছে D-17 (০.৪ km), কিন্তু রেললাইনের ওপারে — ১১ মিনিট। D-23 ০.৯ km দূরে, অথচ ৪ মিনিট — তিনিই প্রথম। তাঁর অবস্থা CAS দিয়ে available v3 থেকে offered v4. ১৫ সেকেন্ডের মেয়াদ একরকম lease; matcher মরে গেলেও চালক আটকে থাকবেন না।
  4. চালক গ্রহণ করলেন। ৬ সেকেন্ডে accept. দুটি সারি একই শহরের shard-এ, তাই এক transaction-এ: trip ACCEPTED v2, চালক en_route v5. প্রত্যাখ্যান বা মেয়াদ শেষ হলে পরের জন — D-08 — offer পেতেন।
  5. Trip শুরু। চালক পৌঁছে “শুরু” চাপলেন — ON_TRIP. D-23-এর প্রতিটি বিন্দু trip_id-সহ trace stream-এ যায়; এই পথ থেকেই ভাড়া। যাত্রীর app অবস্থান পায় সার্ভারের push-এ।

৬ · Deep dive ১ — প্রতি ৪ সেকেন্ডে লাখো অবস্থান

কত ঘনঘন পাঠাবেন — তিনটি দাম একসাথে

ব্যবধান কমালে অবস্থান তাজা, কিন্তু লেখা, ফোনের ব্যাটারি আর মোবাইল ডেটা — তিনটিই বাড়ে। ঘণ্টায় ৩০ km গতিতে (প্রায় ৮.৩ m/s) চলা একটি গাড়ির হিসাব দেখুন; বার্তার আকার অংশ ২-এর মতো ১৫০ B ধরা।

ব্যবধান১ লাখ চালকে লেখা/সেকেন্ডসর্বোচ্চ পুরোনো অবস্থান (৩০ km/h)একজন চালকের ডেটা/ঘণ্টা
১ সেকেন্ড১,০০,০০০প্রায় ৮ mপ্রায় ৫৪০ KB
৪ সেকেন্ড২৫,০০০প্রায় ৩৩ mপ্রায় ১৩৫ KB
১০ সেকেন্ড১০,০০০প্রায় ৮৩ mপ্রায় ৫৪ KB

৪ সেকেন্ডে চালক বড়জোর প্রায় ৩৩ মিটার সরেন — খোঁজার জন্য যথেষ্ট। অনেক নকশায় হার বদলায় (ধরা যাক): দাঁড়িয়ে থাকা চালক কম ঘনঘন, pickup-এর পথে বা trip-এ বেশি ঘনঘন।

সার্ভারে একটি বার্তার পথ

  • Overwrite, append নয়: memory-তে প্রতি চালকের একটিই এন্ট্রি — অবস্থান, সময়, অবস্থা, গাড়ির ধরন, cell; নতুন বার্তা আগেরটিকে মুছে বসে।
  • পুরোনো বার্তা উপেক্ষা: পুনঃসংযোগের পর জমা বার্তা দেরিতে আসতে পারে। Sequence number আগেরটির চেয়ে ছোট হলে latest-এ বসানো হয় না, শুধু trace-এ যায়। এটি last-write-wins, তবে “শেষ” ঠিক হয় ফোনের ক্রমিক নম্বরে, সার্ভারে পৌঁছানোর সময়ে নয় (পর্ব ৩৯ — সময় আর ক্রম)।
  • নীরব চালক নিজে থেকে মুছে যান: এন্ট্রিতে TTL প্রায় ৩০ সেকেন্ড; বার্তা না এলে চালক তালিকা থেকে সরে যান — অবস্থানের বার্তাই একরকম heartbeat (পর্ব ৪৪ — Membership আর failure detection).
  • Trace stream-এ append: key হিসেবে driver_id, তাই একজন চালকের সব বিন্দু একটি partition-এ ক্রম ধরে থাকে (পর্ব ৩৩ — Log-ভিত্তিক streaming). গ্রাহক: trace store, ভাড়া, রাস্তার গতি, surge-এর গণনা। Shard ধীর হলে gateway পুরোনো বার্তা ফেলে নতুনটি রাখে — এক ধরনের load shedding (পর্ব ১৮ — Overload সামলানো)।

GPS-এর ভুল আর map-matching

ফোনের GPS খোলা আকাশে সাধারণত কয়েক মিটারে নির্ভুল, কিন্তু উঁচু দালানের সারি বা উড়ালসড়কের নিচে সংকেত প্রতিফলিত হয়ে কয়েক দশ মিটার বা আরও সরে যেতে পারে। তাই:

  • ছাঁকা: acc খুব বড়, বা অসম্ভব গতি (৪ সেকেন্ডে ৫০০ m) হলে বিন্দুটি সন্দেহজনক — ভুয়া অবস্থানেরও লক্ষণ হতে পারে।
  • Map-matching: এলোমেলো বিন্দুগুলোকে আসল রাস্তার ওপর বসানো — কোন রাস্তা দিয়ে গাড়িটি সত্যিই গেছে। ভাড়া, ETA আর রাস্তার গতি এর ওপর দাঁড়ায়; দামি বলে চলে stream processor-এ (পর্ব ৩৬ — Batch আর stream processing).
  • ফাঁক পূরণ: সংকেত কিছুক্ষণ না থাকলে শেষ জানা গতি আর দিক থেকে অবস্থানের অনুমান — dead reckoning — শুধু যাত্রীর মানচিত্রে মসৃণ দেখানোর জন্য; ভাড়ার হিসাবে পরে আসল বিন্দু দিয়ে ঠিক করা হয়।

নেটওয়ার্ক কেটে গেলে

চলন্ত অবস্থায় ডেটা কয়েক সেকেন্ড থেকে কয়েক মিনিট কেটে যেতে পারে (Computer Networks কোর্সের মোবাইল নেটওয়ার্ক — cell আর handover). তিনটি নিয়ম: (১) অ্যাপ বিন্দু জমিয়ে রাখে, সংযোগ ফিরলে সময় আর sequence-সহ পাঠায়। (২) Trip-এর অবস্থা সার্ভারে; ping না এলে trip বাতিল হয় না। (৩) যাত্রী দেখেন “শেষ জানা অবস্থান · ৪০ সেকেন্ড আগে” — পুরো সেবা ভেঙে না পড়ে মান একটু কমে, পর্ব ৪৮ — Resilience pattern-এর graceful degradation. অ্যাপের এই আচরণ offline-first নকশার একটি ছোট রূপ।

কতদিন রাখবেন? সব ping রাখলে বছরে প্রায় ৪৭ TB, শুধু trip-এর পথ প্রায় ১৬ TB. নীতি (ধরা যাক): stream-এ কয়েক দিন; trip-এর পথ বিবাদ ও নিরাপত্তার প্রয়োজনমতো; trip-এর বাইরের অবস্থান অল্প সময়, তারপর এলাকাভিত্তিক গড়ে downsampling. খরচ আর গোপনীয়তা — দুটিরই সিদ্ধান্ত — retention policy, অংশ ৯।

৭ · Deep dive ২ — spatial index: geohash, quadtree আর hexagon

“এই বিন্দুর ১ km-এর মধ্যে খালি চালক কারা?” — সাধারণ B-tree এতে ভালো নয়: (lat, lon) index-এ lat-এর পরিসর দেয় পুরো শহর জুড়ে একটি সরু ফালি, lon প্রতিটি সারিতে আলাদা মেলাতে হয়। লাগে দুই মাত্রাকে এক মাত্রায় আনার কৌশল, বা মাত্রা ধরে ভাগ করা গাছ।

Geohash — পৃথিবীকে নামওয়ালা ঘরে ভাগ

পৃথিবীকে বারবার দুই ভাগ করুন — পালা করে দ্রাঘিমা (longitude) আর অক্ষাংশ (latitude) ধরে — আর প্রতিবার বিন্দুটি কোন অর্ধেকে পড়ল তার একটি bit লিখুন (১ = বড় অর্ধেক)। দুই মাত্রার bit একটির পর একটি বসানো — এই interleaving-ই Z-order curve. প্রতি ৫টি bit মিলে একটি অক্ষর, ৩২টি অক্ষরের বর্ণমালা থেকে — base32 (0–9 আর a থেকে z, কিন্তু a, i, l, o বাদ)। অক্ষর যত বেশি, ঘর তত ছোট; একই prefix মানে একই বড় ঘর। নিচে একটি কাল্পনিক বিন্দু (অক্ষাংশ ২৩.৭৮, দ্রাঘিমা ৯০.৪০):

বিন্দু: lat 23.78, lon 90.40 · প্রতি অক্ষরে ৫ bit
Geohash
এই অক্ষরের bit
অক্ষাংশের পরিসর
দ্রাঘিমার পরিসর
ঘরের আকার (এই অক্ষাংশে)
  1. প্রথম অক্ষর w. দ্রাঘিমা: মাঝখান ০, ৯০.৪০ বড় → ১। অক্ষাংশ: মাঝখান ০, বড় → ১। দ্রাঘিমা: মাঝখান ৯০, বড় → ১। অক্ষাংশ: মাঝখান ৪৫, ছোট → ০। দ্রাঘিমা: মাঝখান ১৩৫, ছোট → ০। 11100 = ২৮, বর্ণমালায় ০ থেকে গুনলে ২৮ নম্বর অক্ষর w — এশিয়ার একটি বিশাল অংশ।
  2. দ্বিতীয় অক্ষর h. এবার শুরু অক্ষাংশ দিয়ে — bit পালা করে চলে, তাই প্রথম অক্ষরে দ্রাঘিমার ৩টি, এখানে অক্ষাংশের ৩টি। 10000 = ১৬ → h.
  3. তৃতীয় অক্ষর 0. পাঁচটি bit-ই ০ — প্রতিটি ভাগে ছোট দিকে। ঘর প্রায় ১৪৩ × ১৫৭ km.
  4. চতুর্থ অক্ষর r. 10111 = ২৩ → r. প্রায় ৩৬ × ২০ km — একটি বড় শহর আর তার আশপাশ।
  5. পঞ্চম অক্ষর 3. প্রায় ৪.৫ × ৪.৯ km — কয়েকটি মহল্লা। খেয়াল করুন, প্রতি অক্ষরে ঘরের ক্ষেত্রফল ৩২ ভাগের এক ভাগ হয়।
  6. ষষ্ঠ অক্ষর 1 — wh0r31. প্রায় ১.১২ × ০.৬১ km — কাছের চালক খোঁজার মানানসই আকার; ঘরের সব চালক একটি key-তে: cell:wh0r31. ঘর বর্গ নয়: দুই মাত্রা ১৫টি করে bit পেলেও অক্ষাংশের পরিসর (১৮০°) দ্রাঘিমার (৩৬০°) অর্ধেক।
অক্ষর (precision)বিষুবরেখায় ঘর (চওড়া × লম্বা)অক্ষাংশ ২৩.৭৮-একাজে লাগে
৫প্রায় ৪.৮৯ × ৪.৮৯ kmপ্রায় ৪.৪৮ × ৪.৮৯ kmshard-এর এলাকা, surge-এর এলাকা
৬প্রায় ১.২২ × ০.৬১ kmপ্রায় ১.১২ × ০.৬১ kmঘন শহরে কাছের চালক খোঁজা
৭প্রায় ১৫৩ × ১৫৩ mপ্রায় ১৪০ × ১৫৩ mpickup point, খুব ঘন এলাকা
৮প্রায় ৩৮ × ১৯ m—প্রায় একটি ভবন

সীমানার ফাঁদ — কেন ৯টি ঘর

একই prefix মানে কাছাকাছি, কিন্তু উল্টোটি সত্য নয়: সীমানার দুই পাশের দুটি বিন্দু কয়েক মিটার দূরে থেকেও আলাদা prefix পায়। তাই খোঁজা হয় নিজের ঘর আর চারপাশের ৮টি — একটি ৩ × ৩ block. নিচের প্রোগ্রাম geohash বানায়, ঘরের সীমা ফিরিয়ে আনে, প্রতিবেশী খোঁজে, তারপর seed দেওয়া ২,০০০ জন চালকের মধ্যে কাছের জনকে দুইভাবে খোঁজে — ৯টি ঘরে, আর সবাইকে মেপে (brute force).

// Geohash: bits alternate lon, lat; every 5 bits -> one base32 char. Then a nearest-driver query.
const B32 = "0123456789bcdefghjkmnpqrstuvwxyz", KM = 111.32, rad = Math.PI / 180;
const LAT = 23.78, LON = 90.40;          // a made-up point in a dense city
function encode(lat, lon, p) {           // k = 1 halves longitude, k = 0 latitude; lon goes first
  const lo = [-90, -180], hi = [90, 180], v = [lat, lon]; let s = "", ch = 0, k = 1;
  for (let i = 0; i < 5 * p; i++, k ^= 1) {
    const mid = (lo[k] + hi[k]) / 2, bit = v[k] >= mid ? 1 : 0;
    if (bit) lo[k] = mid; else hi[k] = mid;
    ch = ch * 2 + bit; if (i % 5 === 4) { s += B32[ch]; ch = 0; }
  } return s; }
function cell(g) {                       // decode: the cell's [lat, lon] lower and upper bounds
  const lo = [-90, -180], hi = [90, 180]; let k = 1;
  for (const c of g) for (let b = 4; b >= 0; b--, k ^= 1) {
    const mid = (lo[k] + hi[k]) / 2; if ((B32.indexOf(c) >> b) & 1) lo[k] = mid; else hi[k] = mid; }
  return { lo, hi }; }
function block3x3(g) {                   // g and its 8 neighbours: re-encode the centres around it
  const { lo, hi } = cell(g), out = [];
  for (const i of [1, 0, -1]) for (const j of [-1, 0, 1]) out.push(encode((lo[0] + hi[0]) / 2 +
    i * (hi[0] - lo[0]), (lo[1] + hi[1]) / 2 + j * (hi[1] - lo[1]), g.length));
  return out; }
for (const p of [5, 6, 7]) { const g = encode(LAT, LON, p), { lo, hi } = cell(g);
  console.log("p=" + p + " " + g.padEnd(8) + ((hi[1] - lo[1]) * KM * Math.cos(LAT * rad)).toFixed(3) + " km wide x " + ((hi[0] - lo[0]) * KM).toFixed(3) + " km tall"); }
console.log("3x3 around " + encode(LAT, LON, 6) + ": " + block3x3(encode(LAT, LON, 6)).join(" "));
let seed = 58; const rand = () => { seed = (seed + 0x6D2B79F5) | 0; let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
  t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };   // mulberry32
const index = new Map(), drivers = [];   // in-memory geo index: geohash-6 cell -> drivers in it
for (let i = 0; i < 2000; i++) { const d = { id: "d" + i, lat: 23.68 + 0.2 * rand(), lon: 90.30 + 0.2 * rand() };
  drivers.push(d); const g = encode(d.lat, d.lon, 6); index.set(g, (index.get(g) || []).concat(d)); }
const dist = (a, b) => KM * Math.hypot(a.lat - b.lat, (a.lon - b.lon) * Math.cos(a.lat * rad));
const me = { lat: LAT, lon: LON }; let ops = 0, best = null;
for (const g of block3x3(encode(LAT, LON, 6))) for (const d of index.get(g) || []) { ops++; if (!best || dist(me, d) < dist(me, best)) best = d; }
const brute = drivers.reduce((m, d) => dist(me, d) < dist(me, m) ? d : m);
console.log("3x3 cells: " + best.id + " at " + (dist(me, best) * 1000).toFixed(0) + " m after " + ops + " checks");
console.log("brute    : " + brute.id + " at " + (dist(me, brute) * 1000).toFixed(0) + " m after " + drivers.length + " checks");

আউটপুট:

p=5 wh0r3   4.477 km wide x 4.892 km tall
p=6 wh0r31  1.119 km wide x 0.611 km tall
p=7 wh0r315 0.140 km wide x 0.153 km tall
3x3 around wh0r31: wh0r2f wh0r34 wh0r36 wh0r2c wh0r31 wh0r33 wh0r2b wh0r30 wh0r32
3x3 cells: d314 at 15 m after 32 checks
brute    : d314 at 15 m after 2000 checks

প্রথম তিন লাইন টেবিলের সংখ্যাই (দূরত্ব সরল হিসাবে, প্রতি ডিগ্রি ১১১.৩২ km; ছোট দূরত্বে great-circle distance-এর খুব কাছে)। চতুর্থ লাইনে সীমানার ফাঁদ: wh0r31-এর পশ্চিমের তিনটি প্রতিবেশী wh0r2f, wh0r2c, wh0r2b — পঞ্চম অক্ষরই আলাদা, অথচ ঘরগুলো গা ঘেঁষে। Prefix wh0r3 দিয়ে খুঁজলে এদের চালক বাদ পড়তেন। শেষ দুই লাইন: ৯টি ঘরে ৩২টি দূরত্ব মেপে যে চালক (d314, ১৫ m), ২,০০০টি মেপেও তিনিই — কাজ প্রায় ৬২ ভাগের এক ভাগ।

কেন উত্তরটি নিশ্চিতভাবে ঠিক? যাত্রী মাঝের ঘরে, তাই ৩ × ৩ block-এর বাইরের যেকোনো বিন্দু তাঁর থেকে অন্তত এক ঘরের ছোট দিকের সমান দূরে — এখানে ০.৬১১ km. ৯টি ঘরে পাওয়া সবচেয়ে কাছের চালক এর চেয়ে কাছে হলে (১৫ m) বাইরে তাঁর চেয়ে কাছে কেউ থাকতে পারেন না। না পেলে, বা পাওয়া জন ০.৬১১ km-এর বেশি দূরে হলে, পরের বলয় (৫ × ৫) দেখতে হয়। ফাঁকা এলাকায় precision ৫, ঘন এলাকায় ৬ বা ৭।

Quadtree, hexagon আর অন্যান্য

কৌশলধারণাসুবিধাঅসুবিধা
Geohashনির্দিষ্ট গ্রিড, Z-order-এ নামএকটি string key; যেকোনো key-value store বা B-tree-তে চলে; shard key সহজসীমানার ফাঁদ; ঘরের আকার নির্দিষ্ট, ঘনত্ব মানে না; মেরুর দিকে ঘর সরু
Quadtreeঘরে বেশি বিন্দু হলে চার ভাগঘনত্ব মানে — শহরে ছোট ঘর, গ্রামে বড়চালক সরলে গাছ বদলায়; বিতরণ করা কঠিন
Hexagon grid (H3)পৃথিবী ষড়ভুজে ভাগ, কয়েক স্তরে৬টি প্রতিবেশীর কেন্দ্র সমান দূরে; এলাকাভিত্তিক গণনায় (surge, চাহিদা) সুবিধাস্তরগুলো হুবহু একটির ভেতরে আরেকটি নয়; কিছু পঞ্চভুজ থাকে
R-tree (যেমন PostGIS-এর GiST)বিন্দু ঘিরে আয়তক্ষেত্রের গাছdatabase-এর ভেতরে জটিল আকার, এলাকা, পথপ্রতি সেকেন্ডে হাজারো অবস্থান-বদলে ধীর
k-d treeমাত্রা পালা করে ভাগ করা binary treememory-তে স্থির বিন্দুর nearest-neighbour খোঁজাঘনঘন বদলানো বিন্দুতে বারবার গড়তে হয়

লেখার সময়ের documentation অনুযায়ী Redis-এর GEO কমান্ড অবস্থানকে 52-bit geohash-এ বদলে sorted set-এর score হিসেবে রাখে, আর ৬.২ সংস্করণ থেকে GEOSEARCH দিয়ে ব্যাসার্ধে খোঁজা যায় (পুরোনো GEORADIUS deprecated). আচরণ সংস্করণভেদে আলাদা।

GEOADD drivers:city7:bike 90.40261 23.78112 D-23 90.3975 23.7832 D-08
GEOSEARCH drivers:city7:bike FROMLONLAT 90.4003 23.7801 BYRADIUS 1.5 km ASC COUNT 20 WITHDIST
1) 1) "D-23"   2) "0.2611"
2) 1) "D-08"   2) "0.4473"

আরও গভীরে: Z-order-এর ধারণা G. M. Morton-এর ১৯৬৬ সালের IBM প্রতিবেদন থেকে; quadtree প্রকাশ করেন Finkel ও Bentley (১৯৭৪), k-d tree Bentley (১৯৭৫), R-tree Guttman (১৯৮৪)। Geohash Gustavo Niemeyer ২০০৮ সালে public domain-এ প্রকাশ করেন। Google-এর open-source S2 library পৃথিবীকে একটি ঘনকের ছয় মুখে ফেলে Hilbert curve দিয়ে ঘর সাজায় — Z-order-এর চেয়ে কম লাফ। H3 Uber ২০১৮ সালে open-source করে; এর documentation অনুযায়ী ১৬টি স্তর, প্রতিটি স্তরে ঘর আগের প্রায় সাত ভাগের এক ভাগ (স্তর ৮-এ গড়ে প্রায় ০.৭ km², স্তর ৯-এ প্রায় ০.১ km²)।

Geo index ভাগ করা — আর গরম ঘর

আকার ২০ MB, তবু ভাগ লাগে লেখার হার আর availability-র জন্য। Shard key কী হবে (পর্ব ২৩)?

  • driver_id ধরে ভাগ: লেখা সমান ছড়ায়, চালক সরলেও shard বদলায় না। কিন্তু “কাছের চালক” প্রশ্নে সব shard জিজ্ঞেস করতে হয় — scatter-gather, প্রতিটি অনুরোধে।
  • এলাকা (geohash-৪ বা ৫ prefix) ধরে ভাগ: কাছের চালকেরা একই shard-এ, তাই খোঁজা সাধারণত এক shard-এ, সীমানায় ২–৪টিতে। দাম: এলাকা পার হলে চালককে shard বদলাতে হয় (মোছা হারালেও TTL সাফ করে), আর কিছু এলাকা খুব ব্যস্ত। এলাকা থেকে shard-এ বসাতে consistent hashing (পর্ব ১৬) বা একটি এলাকা-মানচিত্র। পড়ার প্রশ্নটি ভৌগোলিক বলে এটিই সাধারণ পছন্দ।

গরম ঘর: খেলা শেষে স্টেডিয়ামে বা ঈদের আগে বড় স্টেশনে একটি-দুটি cell-এ হাজারো অনুরোধ — একটি hot spot (পর্ব ৩০ — Distributed cache-এর hot key-র মতো)। প্রতিকার: (১) গরম এলাকাকে ছোট cell-এ ভেঙে আলাদা shard-এ; (২) একটি জানালার সব অনুরোধের জন্য একবার খোঁজা (অংশ ৮); (৩) pickup এলাকা ঘিরে geofence আর চালকের সারি — ফেরিঘাটের সিরিয়ালের মতো; (৪) সীমা ছাড়ালে নতুন অনুরোধকে “কয়েক মিনিট অপেক্ষা”।

৮ · Deep dive ৩ — matching, dispatch আর trip-এর অবস্থা

দূরত্ব নয়, পৌঁছানোর সময়

ঢাকার একমুখী রাস্তায় মানচিত্রে ৪০০ m দূরের গাড়ি রেললাইনের ওপারে থাকলে ঘুরে আসতে দশ মিনিট। তাই matching দুই ধাপে: (১) geo index থেকে সোজা দূরত্বে কয়েক ডজন প্রার্থী — সস্তা; (২) প্রথম কয়েকজনের routing engine থেকে ETA — দামি, রাস্তার graph-এ সবচেয়ে ছোট পথ (DSA কোর্সের Dijkstra — সবচেয়ে ছোট পথ)। রাস্তার গতি আসে trace stream থেকে, কয়েক মিনিটের tumbling window-এ গড় করে।

একজন একজন করে, নাকি একসাথে — batch matching

সহজ নিয়ম: অনুরোধ আসামাত্র সবচেয়ে কাছের খালি চালক (greedy). কিন্তু প্রথম যাত্রীর নেওয়া চালক হয়তো পরের যাত্রীর একমাত্র ভালো বিকল্প ছিলেন। একটি রাস্তায় — R1 ২ km চিহ্নে (আগে এলেন), R2 ০ km-এ; চালক D1 ১ km-এ, D2 ৪ km-এ:

নিয়মR1 পানR2 পানমোটসবচেয়ে খারাপ
Greedy (আগে এলে আগে, সবচেয়ে কাছের)D1 — ১ kmD2 — ৪ km৫ km৪ km
Batch (দুজনকে একসাথে)D2 — ২ kmD1 — ১ km৩ km২ km

একসাথে দেখলে মোট ৫ থেকে ৩ km — ৪ মিনিট/km ধরলে গড় অপেক্ষা ১০ থেকে ৬ মিনিট। এটিই batch matching: ১–২ সেকেন্ডের একটি জানালায় আসা সব অনুরোধ আর খালি চালক মিলিয়ে এমন জোড়া খোঁজা, যাতে মোট খরচ সবচেয়ে কম — গণিতে assignment problem, যার ক্লাসিক সমাধান Hungarian অ্যালগরিদম (DSA কোর্সের Network flow, min cut আর bipartite matching পর্বে)। নিচের simulator ৬ × ৬ km এলাকায় ৪০০টি জানালা চালায়, প্রতিটিতে ৮টি অনুরোধ আর ১১ জন চালক; ছোট বলে সঠিক সমাধান bitmask DP দিয়েই।

// Greedy-immediate vs batched matching. 400 rounds; each round = a 2 s window in one area:
// 8 requests arrive, 11 free drivers. Distance = |dx| + |dy| km (a grid of roads), 6 x 6 km area.
let seed = 58; const rand = () => { seed = (seed + 0x6D2B79F5) | 0; let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
  t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };   // mulberry32
const R = 8, D = 11, MIN_PER_KM = 4;     // assume ~15 km/h in dense traffic = 4 min per km
const pt = () => ({ x: 6 * rand(), y: 6 * rand() }), km = (a, b) => Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
const greedy = [], batched = [];
for (let round = 0; round < 400; round++) {
  const req = Array.from({ length: R }, pt), drv = Array.from({ length: D }, pt);
  const c = req.map((r) => drv.map((d) => km(r, d)));
  const used = new Array(D).fill(false);  // (1) greedy: in arrival order, take the nearest free driver
  for (let i = 0; i < R; i++) {
    let j = -1; for (let k = 0; k < D; k++) if (!used[k] && (j < 0 || c[i][k] < c[i][j])) j = k;
    used[j] = true; greedy.push(c[i][j]);
  }
  // (2) batched: minimum total distance over the whole window (bitmask DP = exact assignment)
  const N = 1 << D, best = new Float64Array(N).fill(Infinity), from = new Int32Array(N); best[0] = 0;
  let end = -1;
  for (let m = 0; m < N; m++) {
    if (best[m] === Infinity) continue;
    let i = 0; for (let x = m; x; x &= x - 1) i++;        // i = requests already placed
    if (i === R) { if (end < 0 || best[m] < best[end]) end = m; continue; }
    for (let k = 0; k < D; k++) if (!(m >> k & 1) && best[m] + c[i][k] < best[m | 1 << k]) {
      best[m | 1 << k] = best[m] + c[i][k]; from[m | 1 << k] = k; }
  }
  for (let m = end, i = R - 1; m; i--) { const k = from[m]; batched.push(c[i][k]); m ^= 1 << k; }
}
function show(name, a) {
  const s = [...a].sort((x, y) => x - y), avg = a.reduce((x, y) => x + y, 0) / a.length;
  const p90 = s[Math.ceil(0.9 * s.length) - 1];   // nearest-rank p90
  console.log(name.padEnd(9) + "avg " + avg.toFixed(2) + " km (" + (avg * MIN_PER_KM).toFixed(1) + " min)   p90 " +
    p90.toFixed(2) + " km (" + (p90 * MIN_PER_KM).toFixed(1) + " min)   max " + s[s.length - 1].toFixed(2) + " km");
}
show("greedy", greedy); show("batched", batched);
console.log("pickups: " + greedy.length + " each; extra wait for batching: about 1 s on average (half the window)");

আউটপুট:

greedy   avg 1.72 km (6.9 min)   p90 3.22 km (12.9 min)   max 8.71 km
batched  avg 1.55 km (6.2 min)   p90 2.87 km (11.5 min)   max 5.95 km
pickups: 3200 each; extra wait for batching: about 1 s on average (half the window)

একই ৩,২০০টি অনুরোধে batch-এ গড় pickup ১.৭২ থেকে ১.৫৫ km (প্রায় ১০% কম, গড়ে প্রায় ০.৭ মিনিট), p90 ৩.২২ থেকে ২.৮৭ km, আর সবচেয়ে খারাপ ৮.৭১ থেকে ৫.৯৫ km — লোভী নিয়মে শেষের যাত্রী দূরের চালক পান। দাম: গড়ে প্রায় এক সেকেন্ড অপেক্ষা। জানালা বড় হলে জোড়া ভালো, কিন্তু চালকেরা সরে যান — তাই সাধারণত এক থেকে কয়েক সেকেন্ড (ধরা যাক)। এখানে জানালাগুলো আলাদা, দূরত্ব সরল গ্রিডে; আসলে খরচ হবে ETA.

Offer — একজনকে, মেয়াদসহ

কয়েকজনকে একসাথে একই offer দিলে দ্রুত, কিন্তু বাকিরা বিরক্ত হন আর একই চালক কয়েকটি offer-এ জড়ান। তাই সাধারণত একবারে একজন, ১০–১৫ সেকেন্ডের মেয়াদ (ধরা যাক)। গ্রহণের সম্ভাবনা ০.৬ ধরলে পরপর তিনজনই না নেওয়ার সম্ভাবনা ০.৪³ = ০.০৬৪ — প্রায় ৬.৪% অনুরোধে তিন চেষ্টাতেও চালক নেই, আর প্রতিটি ব্যর্থতায় অপেক্ষা বাড়ে। মেয়াদের টাইমার থাকে টেকসই scheduler-এ, matcher-এর memory-তে নয় (পর্ব ৩৭ — Background job, scheduler আর workflow engine).

একই চালক, দুই dispatcher — double assignment

পাশাপাশি দুটি অনুরোধে দুটি dispatcher একই চালককে বাছতে পারে; দুজনই পড়ে “available”, দুজনই লেখে “আমার” — একটি ক্লাসিক race condition. নিচের simulator দুটি dispatcher-এর পড়া একটি ১০০ ms জানালার যেকোনো সময়ে রাখে; পড়া আর লেখার মাঝে ২০ ms (হিসাব আর একটি network hop).

// Two dispatchers both pick the same nearest driver. Each READS the driver's state, spends
// GAP ms (scoring, a network hop), then WRITES "offered to my trip". 10,000 trials per mode.
let seed = 58; const rand = () => { seed = (seed + 0x6D2B79F5) | 0; let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
  t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };   // mulberry32
const WINDOW = 100, GAP = 20, TRIALS = 10000;   // both reads land somewhere in the same 100 ms
function trial(useCas) {
  const driver = { state: "available", version: 7, offeredTo: [] };
  const ev = [];
  for (const who of ["A", "B"]) { const t = WINDOW * rand(); ev.push([t, "read", who], [t + GAP, "write", who]); }
  ev.sort((x, y) => x[0] - y[0]);
  const seen = {}; let retries = 0;
  for (const [, op, who] of ev) {
    if (op === "read") { seen[who] = { state: driver.state, version: driver.version }; continue; }
    if (seen[who].state !== "available") continue;   // saw it busy: go to the next driver
    if (!useCas) { driver.state = "offered"; driver.offeredTo.push(who); }   // naive: "it is mine"
    else if (driver.version === seen[who].version) {   // CAS: write only if nobody changed it
      driver.state = "offered"; driver.version++; driver.offeredTo.push(who);
    } else retries++;                    // lost the race: pick the next-best driver instead
  }
  return { double: driver.offeredTo.length > 1, retries };
}
for (const useCas of [false, true]) {
  let doubles = 0, retries = 0; seed = 58;   // same 10,000 timings for both modes
  for (let i = 0; i < TRIALS; i++) { const r = trial(useCas); doubles += r.double; retries += r.retries; }
  console.log((useCas ? "CAS on version" : "naive read-then-write").padEnd(22) + "double offers: " +
    String(doubles).padStart(5) + "   losers who retried: " + retries);
}
const p = 1 - (1 - GAP / WINDOW) ** 2;  // P(|tA - tB| < GAP) for two uniform times
console.log("theory for naive: 1 - (1 - " + GAP + "/" + WINDOW + ")^2 = " + p.toFixed(2) + " -> about " + Math.round(p * TRIALS));

আউটপুট:

naive read-then-write double offers:  3603   losers who retried: 0
CAS on version        double offers:     0   losers who retried: 3603
theory for naive: 1 - (1 - 20/100)^2 = 0.36 -> about 3600

সাধারণ পথে ১০,০০০-এর ৩,৬০৩টিতে double offer. সূত্র: দুটি পড়া ২০ ms-এর কম ব্যবধানে হলে দুজনই অন্যের লেখার আগে পড়ে; দুটি সমবণ্টিত সময়ের ব্যবধান ২০-এর কম হওয়ার সম্ভাবনা ১ − (১ − ২০/১০০)² = ০.৩৬. CAS-এ একই সময়-বিন্যাসে double offer শূন্য: ৩,৬০৩ বারই পরের লেখকের শর্ত (“version এখনো ৭”) মেলেনি, সে পরের চালকের দিকে গেছে।

চালক D-17-এর সারি (driver_dispatch) আর দুই dispatcher
D-17
Dispatcher A (T-501)
Dispatcher B (T-502)
Double offer?
  1. A পড়ল। T-501-এর dispatcher A দেখল D-17 available, version ৭।
  2. B-ও পড়ল — বিপদের মুহূর্ত। কয়েক ms পরে T-502-এর B-ও দেখল available, v7. দুজনের হাতে একই ছবি; শর্ত ছাড়া লিখলে দুজনই লিখত।
  3. A-র CAS সফল। A লেখে শর্তে: “version এখনো ৭ হলে offered → T-501, version ৮।” যাচাই আর লেখা একটিই অবিভাজ্য কাজ। Offer গেল, মেয়াদ ১৫ সেকেন্ড।
  4. B-র CAS ব্যর্থ। এখন version ৮, তাই B-র লেখায় 0 rows — B জানে সে হেরেছে। Lock নেই, অপেক্ষা নেই — optimistic concurrency, পর্ব ২৪ — Transaction আর isolation level.
  5. B পরের জনকে নিল। D-23-এর সারিতে CAS (v3 → v4) সফল — কয়েক ms দেরি, ভুল চালক নয়।
  6. D-17 সাড়া দিলেন না। মেয়াদ শেষে scheduler শর্তসহ লিখল “এখনো offered → T-501, v8 হলে available, v9” — তাই দেরির টাইমার কোনো নতুন বৈধ offer মুছবে না। T-501 আবার matching-এ।

একই নিয়ম SQLite-এ চালিয়ে দেখুন: কয়টি সারি বদলাল, সেটিই dispatcher-এর উত্তর — ১ মানে জিত, ০ মানে হার। শেষে trip আর চালক একই transaction-এ বদলায়, কারণ দুটি সারি একই শহরের shard-এ (অংশ ৪)। SQLite একটি মেশিনের database; এটি শুধু শর্তসাপেক্ষ লেখার যুক্তি দেখায়, বিতরণ করা সিস্টেমের ব্যর্থতা নয়।

CREATE TABLE drivers (id INTEGER PRIMARY KEY, state TEXT NOT NULL, trip_id INTEGER, version INTEGER NOT NULL);
CREATE TABLE trips (id INTEGER PRIMARY KEY, state TEXT NOT NULL, driver_id INTEGER, version INTEGER NOT NULL);
INSERT INTO drivers VALUES (17, 'available', NULL, 7), (23, 'available', NULL, 3);
INSERT INTO trips VALUES (501, 'MATCHING', NULL, 1), (502, 'MATCHING', NULL, 1);
-- A and B both read driver 17: available, version 7. A writes first:
UPDATE drivers SET state = 'offered', trip_id = 501, version = version + 1
 WHERE id = 17 AND state = 'available' AND version = 7;
-- B writes with the same old read:
UPDATE drivers SET state = 'offered', trip_id = 502, version = version + 1
 WHERE id = 17 AND state = 'available' AND version = 7;
-- 0 rows for B, so B offers driver 23 instead:
UPDATE drivers SET state = 'offered', trip_id = 502, version = version + 1
 WHERE id = 23 AND state = 'available' AND version = 3;
-- Driver 17 accepts: the trip and the driver change together
BEGIN;
UPDATE trips SET state = 'ACCEPTED', driver_id = 17, version = version + 1
 WHERE id = 501 AND state = 'MATCHING' AND version = 1;
UPDATE drivers SET state = 'en_route', version = version + 1
 WHERE id = 17 AND state = 'offered' AND trip_id = 501;
COMMIT;
-- An illegal jump (MATCHING to COMPLETED) matches no row:
UPDATE trips SET state = 'COMPLETED' WHERE id = 502 AND state = 'ON_TRIP';
SELECT id, state, trip_id, version FROM drivers ORDER BY id;
SELECT id, state, driver_id, version FROM trips ORDER BY id;

A-র UPDATE-এ ১টি সারি বদলেছে, একই পুরোনো শর্তে B-র UPDATE-এ ০টি সারি বদলেছে — B হেরেছে, তাই পরের UPDATE-এ চালক ২৩ পেল (১টি সারি বদলেছে)। Transaction-এর দুটি UPDATE-ই ১টি করে সারি বদলায়; শেষে অবৈধ লাফ MATCHING থেকে COMPLETED-এ ০টি সারি বদলেছে — শর্ত state = 'ON_TRIP' মেলেনি। শেষ অবস্থা: চালক 17 | en_route | 501 | 9 আর 23 | offered | 502 | 4; trip 501 | ACCEPTED | 17 | 2 আর 502 | MATCHING | NULL | 1 — কোনো চালক দুটি trip-এ নেই। PostgreSQL-এর READ COMMITTED-এ একই শর্তসাপেক্ষ UPDATE সমান্তরালেও নিরাপদ, কারণ দ্বিতীয় UPDATE প্রথমটির row lock-এর অপেক্ষা করে নতুন সংস্করণে শর্ত আবার যাচাই করে (পর্ব ২৪)।

অন্য পথ: প্রতিটি চালকের সব offer driver_id ধরে একটি নির্দিষ্ট matcher দিয়ে পাঠালে দুজন লেখকই থাকে না — single-writer principle. Distributed lock-ও সম্ভব, তবে fencing token ছাড়া বিপদ — পর্ব ৪৩ — Coordination — distributed lock, lease আর fencing token.

Trip-এর state machine

এখনকার অবস্থাঘটনাপরের অবস্থা
MATCHINGচালক রাজিACCEPTED
MATCHINGকয়েক চেষ্টাতেও চালক নেই / যাত্রী বাতিলNO_DRIVER / CANCELLED
ACCEPTEDচালক pickup-এ পৌঁছালেনARRIVED
ACCEPTED বা ARRIVEDযাত্রী বা চালক বাতিলCANCELLED (নিয়মমতো বাতিল-ফি)
ACCEPTED (চালক বাতিল করলেন)নতুন চালক খোঁজাMATCHING
ARRIVEDযাত্রী উঠলেনON_TRIP
ON_TRIPগন্তব্যে শেষCOMPLETED → পরিশোধ (পর্ব ৬০)

প্রতিটি বদল একটি শর্তসাপেক্ষ UPDATE (“অবস্থা X আর version v হলে”), আর একই transaction-এ একটি event — outbox pattern (পর্ব ২৫); notification, ভাড়া, বিশ্লেষণ কোনো ঘটনা হারায় না, অবৈধ লাফ database নিজেই আটকায়। Consistency-র ভাগ: trip আর dispatch-অবস্থায় strong consistency, অবস্থানে eventual consistency — ডেটার চরিত্র অনুযায়ী।

ETA, ভাড়ার অনুমান আর surge-এর ধারণা

ভাড়ার অনুমান সাধারণত: ভিত্তি + প্রতি km-এর হার × দূরত্ব + প্রতি মিনিটের হার × সময়, তারপর একটি গুণক; দূরত্ব-সময় routing-এর আনুমানিক পথ থেকে, trip শেষে map-matched পথ থেকে (upfront ভাড়ায় quote-এর মানই থাকে)। Surge বা dynamic pricing-এর ধারণা: একটি এলাকায় (geohash-৫ বা hexagon ঘর) কয়েক মিনিটে অনুরোধ : খালি চালকের অনুপাত বাড়লে গুণক একটু বাড়ে, যাতে কিছু যাত্রী অপেক্ষা করেন আর চালক সেখানে আসেন। ভালো নকশায় মসৃণ বদল, ঊর্ধ্বসীমা, স্পষ্ট দেখানো আর জরুরি অবস্থায় বন্ধের নিয়ম। কত গুণক ন্যায্য, তা ব্যবসা, আইন আর নৈতিকতার প্রশ্ন — কোথাও নিয়ন্ত্রক সীমা দিতে পারে; এখানে শুধু ধারণা।

৯ · ভাঙন, trade-off আর বিস্তার

কোনটি মরে গেলে কী হয়

যা ব্যর্থ হলোপ্রভাবপ্রতিকার
Geo index-এর একটি shardসেই এলাকায় কিছুক্ষণ “চালক নেই”Soft state: নতুন shard খালি শুরু করে, পরের ৪–৮ সেকেন্ডের বার্তাতেই প্রায় ভরে যায়; চাইলে একটি replica; ততক্ষণ পাশের shard থেকে বড় ব্যাসার্ধে খোঁজা
একটি gateway সার্ভারহাজারো চালকের সংযোগ কাটেঅ্যাপ jitter-সহ backoff দিয়ে অন্য সার্ভারে যুক্ত হয় — সবাই একসাথে নয়, নইলে thundering herd; জমা বিন্দু পরে আসে
Matcher offer-এর মাঝে মরে গেলTrip MATCHING-এ আটকেOffer-এর মেয়াদ (lease) শেষে চালক মুক্ত; queue-র কাজ ack না হওয়ায় আরেক matcher নেয় (পর্ব ৩৪ — Delivery semantics); CAS থাকায় দ্বিতীয়বার চালানো নিরাপদ
Trip DB-র leaderকয়েক সেকেন্ড নতুন trip ও বদল থামেFailover (পর্ব ২২ — Replication); চলমান trip চালকের অ্যাপে চলতে থাকে, বদল পরে সিঙ্ক
Routing/ETA সার্ভিসETA নেইসোজা দূরত্ব × শহরের গড় গতি দিয়ে মোটামুটি অনুমান — graceful degradation; circuit breaker (পর্ব ৪৮)
Payment providerপরিশোধ আটকেTrip COMPLETED থাকে, পরিশোধ “বাকি”, retry হয় — পর্ব ৬০
পুরো regionএকাধিক শহর বন্ধশহরভিত্তিক cell আরেক region-এ চালু; RPO-RTO ঠিক করা — পর্ব ৫১ — Failover আর disaster recovery

শহর = cell

প্রায় সব trip একটি শহরের ভেতরে। তাই প্রতিটি শহর একটি পূর্ণ, স্বাধীন কপি চালাতে পারে — নিজের gateway, geo index, matcher আর trip DB-র shard: cell-based architecture (পর্ব ৪৮)। এক শহরের সমস্যা বা ভুল deploy বাকিদের ছোঁয় না, আর নতুন সংস্করণ আগে একটি ছোট শহরে (পর্ব ৫৩ — Deployment আর release). শহর পেরোনো trip-এর মালিক যে শহরে শুরু; অ্যাকাউন্ট আর পরিশোধ cell-এর বাইরে।

১০ গুণ বড় হলে

  • লেখা ২,৫০,০০০/সেকেন্ড: আরও এলাকাভিত্তিক shard, gateway-তে বার্তা একসাথে পাঠানো; memory তখনো কয়েকশো MB.
  • সংযোগ ১০ লাখ: gateway-র memory আর file descriptor-এর সীমা — চ্যাটের মতোই (পর্ব ৫৫)।
  • ETA: routing প্রায়ই সবচেয়ে বেশি CPU খায় — প্রাক-হিসাব করা রাস্তার graph আর এলাকা-জোড়ার ETA cache.

নিরাপত্তা আর গোপনীয়তা (প্রতিরক্ষার দিক থেকে)

  • অবস্থান = PII: পথ থেকে বাসা আর কর্মস্থল বোঝা যায়। Data minimisation — সঠিক অবস্থান শুধু চলমান trip-এ, বিশ্লেষণে এলাকাভিত্তিক গড়, নাম ছাড়া।
  • প্রবেশাধিকার: কর্মীরা trace দেখেন শুধু নির্দিষ্ট কারণে, role অনুযায়ী, প্রতিটি দেখা audit log-এ। Transit-এ TLS, জমা ডেটায় encryption at rest — পর্ব ৫২ — নকশায় নিরাপত্তা; শুরু থেকে privacy by design.
  • যোগাযোগ আর অপব্যবহার: আসল ফোন নম্বরের বদলে মাঝের নম্বর; অস্বাভাবিক অবস্থান-ধারা পর্যালোচনায়; অনুরোধ আর বাতিলে rate limiting (পর্ব ৪৯)।

কী মাপবেন, কীসে alert

পর্ব ৫০ — Observability-এর SLI, প্রতি শহরে আলাদা: অবস্থান পৌঁছানোর দেরি (p99); পুরোনো এন্ট্রির অনুপাত; চালক মেলার সময় (p50, p90) আর হার; offer গ্রহণের হার; ETA-র ভুল; double assignment — সবসময় শূন্য, একটিও হলে alert.

খরচ কোথায়

Memory প্রায় নগণ্য। খরচ মূলত: লাখো সংযোগের gateway, routing-এর CPU, trace-এর storage (retention-এ নিয়ন্ত্রণযোগ্য), আর ব্যবহারকারীর মোবাইল ডেটা।

ইন্টারভিউর ৬০ সেকেন্ডের সারাংশ

“এটি লেখা-ভারী সমস্যা: ১ লাখ চালক প্রতি ৪ সেকেন্ডে, অর্থাৎ ২৫,০০০ লেখা/সেকেন্ড, অনুরোধ মাত্র কয়েক ডজন। তাই ডেটা তিন ভাগে: সর্বশেষ অবস্থান memory-র geo index-এ (soft state, মাত্র কয়েক দশ MB, geohash cell ধরে shard); পথের ইতিহাস driver_id-key দেওয়া log stream-এ, পরে সংকুচিত storage-এ; আর trip ও চালকের অবস্থা শহর ধরে ভাগ করা relational DB-তে, শর্তসাপেক্ষ লেখা আর state machine দিয়ে। কাছের চালক খুঁজি নিজের আর ৮টি প্রতিবেশী cell-এ, তারপর ETA দিয়ে সাজাই, কয়েক সেকেন্ডের জানালায় batch-এ বণ্টন করি, আর একবারে একজনকে মেয়াদসহ offer দিই — CAS-এর কারণে double assignment হয় না। প্রতিটি শহর একটি cell; অবস্থান-ডেটা PII হিসেবে সুরক্ষিত, সীমিত সময় রাখা।”

পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়, চাকরি বা system design ইন্টারভিউতে এই case-এ দুর্বল আর শক্ত উত্তরের পার্থক্য সাধারণত এখানে:

  • হিসাব: দুর্বল — “অনেক ব্যবহারকারী, তাই microservices.” শক্ত — লেখার হার (চালক ÷ ব্যবধান), memory-র আকার, আর লেখা : অনুরোধের অনুপাত বের করে বলা কোথায় চাপ।
  • অবস্থান কোথায়: দুর্বল — প্রতিটি ping-এ database-এ UPDATE. শক্ত — memory-তে সর্বশেষ, ইতিহাস stream-এ, আর কেন হারালেও ক্ষতি নেই।
  • খোঁজা: দুর্বল — “geohash prefix দিয়ে খুঁজব।” শক্ত — precision বাছাই, ৮টি প্রতিবেশী, সঠিকতার শর্ত আর বলয় বাড়ানো।
  • Matching: দুর্বল — সবচেয়ে কাছের চালক। শক্ত — ETA, batch, offer-এর মেয়াদ আর CAS দিয়ে double assignment ঠেকানো।
  • বিস্তার: শক্ত উত্তরে ব্যর্থতা (shard, gateway, matcher), শহরভিত্তিক cell আর অবস্থান-ডেটার গোপনীয়তা নিজে থেকেই আসে।

১০ · AI-এর যুগে — ride-sharing-এর নকশা AI দিয়ে, কিন্তু যাচাই করে

AI-কে শুধু “একটি ride-sharing অ্যাপের নকশা চাই” বললে আসে microservices, Kafka, Redis, Kubernetes-এর একটি সুন্দর ছবি — কিন্তু এই case-এর আসল প্রশ্নগুলো প্রায়ই বাদ বা ভুল:

AI-এর সাধারণ ভুলকীভাবে ধরবেন
প্রতিটি অবস্থান PostgreSQL-এ spatial index-সহ UPDATEলেখার হার নিজে হিসাব করুন (চালক ÷ ব্যবধান); প্রতিটি UPDATE-এ WAL আর index-এর কাজ ভাবুন; সর্বশেষ অবস্থান কেন soft state
হিসাবে একক গুলিয়ে ফেলা (মিনিট বনাম সেকেন্ড, গড় বনাম peak)অংশ ২-এর মতো একটি ছোট script-এ নিজে আবার হিসাব
“Geohash prefix দিয়ে খুঁজুন” — প্রতিবেশী ছাড়াসীমানার পাশের একটি বিন্দু নিয়ে অংশ ৭-এর মতো পরীক্ষা
সবচেয়ে কাছের চালক = সোজা দূরত্বে কাছেররেললাইন বা একমুখী রাস্তার একটি উদাহরণ দিয়ে জিজ্ঞেস করুন
Double assignment বা offer-এর মেয়াদের কথাই নেই“দুই matcher একই চালককে বাছলে কী হবে?” — failure-mode পর্যালোচনা
Redis GEO বা অন্য পণ্যের সীমা, দাম বানিয়ে বলাঅফিসিয়াল documentation দেখুন; একটি ছোট load test
ACID-এর consistency আর CAP-এর consistency এক করে ফেলাকোন ডেটায় কোন নিশ্চয়তা, আলাদা করে লিখুন (পর্ব ৪০)
ছোট শহরের ২,০০০ চালকের জন্য বিশাল stream-processing ব্যবস্থাহিসাব বলে একটি সার্ভারই যথেষ্ট কি না — over-engineering

AI-কে নকশা দিতে বলার সময় সংখ্যা, সীমা আর যাচাইয়ের দাবি একসাথে দিন:

তুমি একজন অভিজ্ঞ backend architect. একটি কাল্পনিক ride-sharing অ্যাপের নকশা দাও, এই অনুমানে: ব্যস্ত সময়ে ১ লাখ চালক অনলাইন, প্রতি ৪ সেকেন্ডে অবস্থান, দিনে ১০ লাখ trip, একটি ঘনবসতির শহরে মোবাইল ডেটা মাঝে মাঝে কাটে। ১. প্রথমে লেখা/সেকেন্ড, অনুরোধ/সেকেন্ড, memory আর বছরে storage হিসাব করো — প্রতিটি ধাপ সূত্রসহ দেখাও। ২. চালকের সর্বশেষ অবস্থান, পথের ইতিহাস আর trip-এর অবস্থা কোথায় রাখবে, আর প্রতিটির consistency কী — আলাদা করে বলো। ৩. কাছের চালক খোঁজার কৌশল দাও; geohash হলে precision আর প্রতিবেশী cell-এর যুক্তি দাও। ৪. দুই matcher একই চালককে বাছলে কী হবে, আর কীভাবে ঠেকাবে? ৫. পাঁচটি failure mode আর তাদের প্রতিকার দাও। কোনো পণ্যের সীমা বা দাম বললে "যাচাই করা দরকার" লিখে দাও; নিশ্চিত না হলে বানিয়ে বলো না।

AI-এর নকশা হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

ইন্টারভিউয়ার মূল নকশার পরে যে প্রশ্নগুলো দিয়ে গভীরে ঠেলেন। নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১Geohash, quadtree না hexagon grid — কোনটি বেছে নেবেন, আর কেন?

তিনটিই একই সমস্যার তিন উত্তর — দুই মাত্রার জায়গাকে এমনভাবে সাজানো, যাতে “কাছে” মানে “একসাথে রাখা”। ইতিহাস দেখলে ধারণাগুলো পুরোনো: G. M. Morton ১৯৬৬ সালে IBM-এর জন্য দুই স্থানাঙ্কের bit পালা করে বসিয়ে এক সারিতে আনার কৌশল দেন, যা আজ Z-order বা Morton code. Finkel ও Bentley ১৯৭৪ সালে quadtree প্রকাশ করেন। Gustavo Niemeyer ২০০৮ সালে Morton-এর ধারণার ওপর base32 নাম দিয়ে geohash প্রকাশ করেন, আর Uber ২০১৮ সালে ষড়ভুজের H3 library open-source করে।

  • Geohash: সবচেয়ে সরল। একটি string, তাই যেকোনো key-value store, B-tree বা shard key হিসেবে চলে; Redis-ধাঁচের GEO কমান্ডও ভেতরে এটিই ব্যবহার করে। দুর্বলতা: সীমানার ফাঁদ আর নির্দিষ্ট আকারের ঘর — ব্যস্ত মোড়ে একটি ঘরে হাজার চালক, গ্রামে একটি ঘরে কেউ নেই।
  • Quadtree: ঘনত্ব মানে — কোনো ঘরে বেশি বিন্দু হলে চার ভাগ হয়। স্থির ডেটায় (দোকান, স্কুল) চমৎকার; কিন্তু প্রতি ৪ সেকেন্ডে লাখো বিন্দু সরলে গাছ বারবার ভাঙে-জোড়ে, আর বহু মেশিনে ভাগ করা কঠিন।
  • Hexagon grid: ষড়ভুজের ছয় প্রতিবেশীর কেন্দ্র সমান দূরে, বর্গের মতো কোণের প্রতিবেশী নেই। তাই এলাকাভিত্তিক গণনায় — চাহিদা, জোগান, surge-এর মানচিত্র, ঘর থেকে ঘরে চলাচলের মডেল — হিসাব মসৃণ। দাম: স্তরগুলো হুবহু একটির ভেতরে আরেকটি নয়, আর পৃথিবী ঢাকতে কয়েকটি পঞ্চভুজ লাগে।

বাস্তবে অনেক নকশায় মিশ্রণ: কাছের চালক খোঁজা আর shard-এর জন্য geohash-এর মতো সরল গ্রিড বা S2-এর মতো cell, বিশ্লেষণ আর মূল্য নির্ধারণে hexagon. ইন্টারভিউতে যেকোনো একটি বেছে তার সীমাবদ্ধতা আর প্রতিকার বলতে পারাই মূল কথা।

মূল উপলব্ধি: spatial index বাছাই নির্ভর করে প্রশ্নের ধরন (কাছের কে, নাকি এলাকার গড়), ডেটা কত ঘনঘন বদলায়, আর কীভাবে বহু মেশিনে ভাগ হবে — তার ওপর।

প্র ০২চালকের সর্বশেষ অবস্থান database-এ না রেখে memory-তে কেন? সার্ভার restart হলে তো সব হারাবে।

হারাবে — এবং সেটাই ঠিক আছে। এখানে মূল প্রশ্ন হলো ডেটাটি কতক্ষণ মূল্যবান। চালকের অবস্থান ৪ সেকেন্ড পরেই পুরোনো; নতুন বার্তা এসে আগেরটিকে মুছে দেয়। এমন তথ্যকে বলে soft state: এর একমাত্র উৎস চালকের ফোন, আর ফোন প্রতি কয়েক সেকেন্ডে নতুন কপি পাঠায়। একটি shard restart হলে সেটি খালি শুরু হয়, আর পরের ৪–৮ সেকেন্ডের মধ্যে প্রায় সব চালক আবার বসে যান। মাঝের কয়েক সেকেন্ড সেই এলাকায় matching দুর্বল — পাশের shard থেকে বড় ব্যাসার্ধে খুঁজে বা অনুরোধ একটু পিছিয়ে সামলানো যায়।

উল্টো দিকে ভাবুন — প্রতিটি ping যদি একটি durable database-এ UPDATE হয়: প্রতি সেকেন্ডে ২৫,০০০টি লেখা, প্রতিটিতে write-ahead log, spatial index-এর হালনাগাদ, replication. পর্ব ১৯ — Storage engine-এ দেখেছেন, এমন ঘনঘন overwrite-এ disk আর index-এর কাজ অনেক বেশি, অথচ এই টেকসই কপি কখনো পড়াই হয় না, কারণ restart-এর পরে ৪ সেকেন্ড পুরোনো অবস্থানের দাম নেই।

তবে দুটি জিনিস অবশ্যই টেকসই: পথের ইতিহাস (ভাড়া, বিবাদ, নিরাপত্তা) — তাই প্রতিটি বিন্দু log stream-এ append হয়, যা replication-সহ disk-এ থাকে; আর চালকের dispatch-অবস্থা (কোন trip-এ আছেন) — যা trip DB-তে। Memory-র index হারালে চালক “অদৃশ্য” হন কয়েক সেকেন্ডের জন্য, কিন্তু কোনো trip বা টাকার হিসাব হারায় না। Redis-ধাঁচের store-এ persistence বা replica চালু করা যায়, তবে এখানে সেটি দ্রুত recovery-র সুবিধা, সঠিকতার শর্ত নয়।

মূল উপলব্ধি: প্রতিটি ডেটার জন্য জিজ্ঞেস করুন — “হারালে কি নিজে থেকে ফিরে আসে?” ফিরে এলে memory আর গতি; না এলে টেকসই storage আর কঠোর নিয়ম। একই সিস্টেমে দুই রকম ডেটাকে একই নিয়মে রাখাই সবচেয়ে সাধারণ নকশা-ভুল।

প্র ০৩একজন চালককে একসাথে দুই যাত্রীর কাছে পাঠানো ঠেকাতে distributed lock লাগবে কি?

সাধারণত না। সমস্যাটি একটি সারির — একজন চালকের অবস্থা — তাই সবচেয়ে সরল সমাধান সেই সারিতে শর্তসাপেক্ষ লেখা: “অবস্থা available আর version ৭ হলে offered, version ৮।” Database বা key-value store যাচাই আর লেখা একটি অবিভাজ্য কাজে করে; দুজন লেখকের মধ্যে ঠিক একজন জেতেন, অন্যজন জানেন তিনি হেরেছেন আর পরের প্রার্থীর দিকে যান। Lock ধরে রাখা নেই, কারও অপেক্ষা নেই।

Distributed lock-এ সমস্যা কোথায়? Lock সাধারণত একটি lease — মেয়াদসহ। Lock-ধারক matcher যদি garbage collection বা network-এর কারণে কয়েক সেকেন্ড থেমে যায়, মেয়াদ ফুরায়, আরেকজন lock পায়, আর প্রথমজন জেগে উঠে পুরোনো বিশ্বাসে লেখে। এটি আটকাতে লাগে fencing token — storage-কে যাচাই করতে হয় লেখকের token সবচেয়ে নতুন কি না (পর্ব ৪৩)। অর্থাৎ শেষ পর্যন্ত storage-এ শর্তসাপেক্ষ লেখাই লাগছে; lock শুধু একটি বাড়তি অংশ, যা নিজেও ব্যর্থ হতে পারে।

তৃতীয় পথ single writer: প্রতিটি চালকের সব offer একটি নির্দিষ্ট matcher-এর মাধ্যমে যাবে, driver_id ধরে ভাগ করে। তখন একই চালকের জন্য দুজন লেখকই নেই — কিন্তু সেই matcher বিকল হলে মালিকানা বদলের সময় আবার একই প্রশ্ন, তাই version-এর শর্ত সাধারণত রেখেই দেওয়া হয়।

আরও একটি সূক্ষ্মতা: offer-এর মেয়াদ শেষে চালককে মুক্ত করার লেখাটিও শর্তসাপেক্ষ হতে হবে (“এখনো এই offer-এ থাকলে”), নইলে দেরিতে চলা টাইমার একটি নতুন বৈধ offer মুছে দিতে পারে। আর চালকের রাজি হওয়ার সময় trip আর চালক — দুটি সারি একসাথে বদলাতে হয়; শহর ধরে ভাগ করায় দুটি একই shard-এ, তাই একটি local transaction যথেষ্ট।

মূল উপলব্ধি: distributed lock-এর আগে জিজ্ঞেস করুন — সংঘাতটি কি একটি সারিতে আনা যায়? গেলে সেই সারিতে compare-and-set সবচেয়ে সরল আর সবচেয়ে নির্ভরযোগ্য।

প্র ০৪Batch matching-এর জানালা কত বড় হবে? Hungarian অ্যালগরিদম কি real-time-এ চলে?

Assignment problem — n জন যাত্রী আর চালকের মধ্যে মোট খরচ সবচেয়ে কম রেখে জোড়া — সমাধানের ক্লাসিক পদ্ধতি Harold Kuhn ১৯৫৫ সালে প্রকাশ করেন; হাঙ্গেরির দুই গণিতবিদ Kőnig আর Egerváry-র আগের কাজের ওপর দাঁড়ানো বলে এর নাম Hungarian অ্যালগরিদম, আর James Munkres ১৯৫৭ সালে এর বিশ্লেষণ করেন। দক্ষ রূপে সময় লাগে প্রায় n³-এর অনুপাতে। একটি জানালায় এক এলাকায় কয়েক ডজন বা কয়েকশো অনুরোধ হলে এটি ms-এর ঘরে শেষ হয়; পুরো শহরের হাজারো অনুরোধ একসাথে নিলে ধীর। তাই বাস্তবে সমস্যাটি এলাকা ধরে ছোট টুকরো করা হয়, আর প্রতিটি যাত্রীর জন্য শুধু কাছের কয়েকজন চালককে খরচের তালিকায় রাখা হয় — বাকিদের খরচ “অসীম”।

জানালার আকার একটি trade-off. ছোট জানালা (১ সেকেন্ড) — অপেক্ষা কম, কিন্তু একসাথে কম অনুরোধ, তাই লাভও কম। বড় জানালা (১০ সেকেন্ড) — জোড়া আরও ভালো, কিন্তু যাত্রী বসে থাকেন, চালকেরা সরে যান, আর ETA-গুলো পুরোনো হয়। অংশ ৮-এর simulator-এ ২ সেকেন্ডের জানালায় গড় pickup প্রায় ১০% আর সবচেয়ে খারাপ ক্ষেত্র ৮.৭১ থেকে ৫.৯৫ km কমেছে, দাম গড়ে প্রায় এক সেকেন্ড। ব্যস্ত সময়ে লাভ বেশি (একসাথে অনেক অনুরোধ), শান্ত সময়ে প্রায় শূন্য — তাই জানালা চাহিদা অনুযায়ী বদলাতে পারে।

আরও দুটি প্রশ্ন ওঠে। লক্ষ্য কী? শুধু মোট দূরত্ব কমালে কোনো এক যাত্রী বারবার দূরের চালক পেতে পারেন; অপেক্ষার সময়কে খরচে যোগ করে ন্যায্যতা আনা যায়। ভবিষ্যৎ: এখন একটি চালককে কাছের যাত্রীকে দিলে পাঁচ মিনিট পরে কোন এলাকা খালি হবে — এই ধরনের পরিকল্পনা গবেষণার বিষয়, কিন্তু সহজ batch-ই প্রথম বড় লাভটা দেয়।

মূল উপলব্ধি: লোভী নিয়ম প্রতিটি সিদ্ধান্তে সেরা, সমষ্টিতে নয়; সামান্য অপেক্ষা কিনে সবার জন্য ভালো ফল পাওয়া — এটাই batch-এর যুক্তি।

প্র ০৫Trip চলার মাঝে চালকের ফোনের নেটওয়ার্ক কেটে গেলে trip-এর অবস্থা আর ভাড়ার কী হবে?

ধরে নিতে হবে এটি ঘটবেই — চলন্ত গাড়িতে এক টাওয়ার থেকে আরেক টাওয়ারে যাওয়ার সময়, ভবনের ভেতরে বা কিছু এলাকায় মোবাইল ডেটা কয়েক সেকেন্ড থেকে কয়েক মিনিট পর্যন্ত থাকতে নাও পারে। বাংলাদেশে এর মাত্রা এলাকা আর অপারেটরভেদে আলাদা; আমরা কোনো পরিসংখ্যান ধরছি না, শুধু নকশাকে এর জন্য তৈরি রাখছি।

  • অবস্থান হারায় না: চালকের অ্যাপ বিন্দুগুলো ফোনে জমায়, প্রতিটিতে ফোনের সময় আর sequence number. সংযোগ ফিরলে সব একসাথে যায়। সার্ভার সর্বশেষ অবস্থান হালনাগাদ করে শুধু নতুনতম sequence দিয়ে, আর পুরোনোগুলো trace-এ ঠিক ক্রমে বসায়। ফোনের ঘড়ি ভুল হতে পারে, তাই সার্ভার পৌঁছানোর সময়ও রাখে, আর ক্রমের জন্য ঘড়ির চেয়ে sequence-এ বেশি ভরসা করে (পর্ব ৩৯)।
  • Trip-এর অবস্থা সার্ভারে: ping না এলে trip বাতিল হয় না। চালক নেটওয়ার্ক ছাড়াই “শেষ” চাপলে অ্যাপ ঘটনাটি সময়সহ জমায় আর সংযোগ ফিরলে পাঠায়; সার্ভার state machine-এ বৈধ বদল হিসেবে নেয়। এই ঘটনা idempotent — দুবার পৌঁছালেও trip একবারই শেষ।
  • ভাড়া: পথে ফাঁক থাকলে map-matching ফাঁকের দুই প্রান্তের মাঝে সম্ভাব্য পথ routing দিয়ে পূরণ করে। Upfront ভাড়ায় সমস্যা কম — quote-এর মানই থাকে। দূরত্বভিত্তিক ভাড়ায় অস্বাভাবিক ফাঁক বা লাফ থাকলে ভাড়া চূড়ান্ত করার আগে পর্যালোচনা বা আনুমানিক পথের ভাড়া।
  • যাত্রীর দিক: যাত্রী দেখেন শেষ জানা অবস্থান আর কত সেকেন্ড আগের; দীর্ঘ নীরবতায় নিরাপত্তার জন্য support-এর কাছে সতর্কবার্তা। গুরুত্বপূর্ণ বার্তা — যেমন trip-এর OTP — push না পৌঁছালে SMS-এ যেতে পারে (পর্ব ৫৭)।

মূল উপলব্ধি: মোবাইল-প্রথম সিস্টেমে সংযোগ একটি বিলাসিতা, নিশ্চয়তা নয়। ঘটনাকে সময় ও ক্রমিক নম্বরসহ ফোনে জমিয়ে পরে idempotent ভাবে মেলানো — এটাই নির্ভরযোগ্যতার মূল কৌশল।

অনুশীলন

হিসাব, নকশা বদল, ত্রুটি খোঁজা আর আউটপুট অনুমান — চারটিই ইন্টারভিউর পরিচিত ধরন।

  1. হিসাব — আরেকটি শহর: একটি কাল্পনিক শহরে ব্যস্ত সময়ে ৩০,০০০ চালক অনলাইন, প্রতি ৩ সেকেন্ডে অবস্থান পাঠান। Memory-তে প্রতি চালক ২০০ B. দিনে ৪ লাখ trip, গড়ে ২৫ মিনিট; trip-এর প্রতিটি বিন্দু জমা হয় ৪০ B-এ, ৩টি কপি। বের করুন: (ক) লেখা/সেকেন্ড, (খ) memory, (গ) দিনে trip-এর বিন্দু, (ঘ) বছরে storage (কপিসহ)।

    (ক) ৩০,০০০ ÷ ৩ = ১০,০০০ লেখা/সেকেন্ড. (খ) ৩০,০০০ × ২০০ B = ৬০,০০,০০০ B ≈ ৬ MB — আবারও তুচ্ছ। (গ) একটি trip-এ ২৫ × ৬০ ÷ ৩ = ৫০০টি বিন্দু; দিনে ৪,০০,০০০ × ৫০০ = ২০ কোটি বিন্দু. (ঘ) দিনে ২০ কোটি × ৪০ B = ৮ GB; বছরে ৮ × ৩৬৫ = ২,৯২০ GB ≈ ২.৯২ TB; ৩টি কপিতে প্রায় ৮.৭৬ TB. অংশ ২-এর প্রোগ্রামে peakDrivers: 30000, everySec: 3, tripsPerDay: 400000, tripMin: 25, pointBytes: 40 বসিয়ে একই উত্তর মেলাতে পারেন। শিক্ষা: ব্যবধান ৪ থেকে ৩ সেকেন্ড করলেই লেখা আর trace দুটোই এক-তৃতীয়াংশ বাড়ে।

  2. Requirement বদল — শেয়ার রাইড: এখন একই গাড়িতে একই দিকের দুজন যাত্রী নেওয়া যাবে। নকশার কোন কোন অংশ বদলাতে হবে?
    • চালকের অবস্থা: শুধু available/on_trip নয় — খালি আসন আর বর্তমান পথ (কোন কোন stop বাকি)। CAS-এর শর্তে থাকবে “খালি আসন ≥ ১ আর version v”.
    • Matching: প্রশ্ন আর “কাছের খালি চালক” নয়, “কোন চলমান গাড়ির পথে এই যাত্রীকে ঢোকালে মোট বাড়তি সময় সবচেয়ে কম, আর আগের যাত্রীর দেরি একটি সীমার মধ্যে”। প্রার্থী খোঁজা হবে পথের কাছের গাড়িতে — শুধু বর্তমান অবস্থান নয়।
    • Data model: একটি trip-এ কয়েকজন যাত্রী, প্রত্যেকের আলাদা pickup-drop আর ভাড়া; গাড়ির একটি “route plan” সারি, version-সহ।
    • ভাড়া আর state machine: প্রত্যেক যাত্রীর আলাদা অবস্থা (উঠেছেন, নেমেছেন); ভাগাভাগির নিয়ম। এক যাত্রী বাতিল করলে পথ নতুন করে হিসাব।
    • হিসাব: প্রতিটি প্রার্থী গাড়িতে কয়েকটি ঢোকানোর জায়গা — ETA-র কাজ অনেক বাড়ে।
  3. ত্রুটি খুঁজুন: একজন প্রস্তাব দিলেন — সব চালক একটি PostgreSQL টেবিলে; প্রতিটি ping-এ UPDATE drivers SET lat, lon; অনুরোধ এলে SELECT … ORDER BY দূরত্ব LIMIT 1, কোনো spatial index ছাড়া; তারপর UPDATE drivers SET busy = true WHERE id = ?; আর সব চালকের WebSocket একটিমাত্র সার্ভারে। অন্তত চারটি সমস্যা আর প্রতিকার বলুন।
    • লেখার চাপ: হাজারো UPDATE/সেকেন্ড একটি database-এ, প্রতিটিতে WAL আর replication. প্রতিকার: সর্বশেষ অবস্থান memory-র geo index-এ, ইতিহাস stream-এ।
    • খোঁজা: index ছাড়া প্রতিটি অনুরোধে পুরো টেবিল পড়ে দূরত্ব হিসাব আর sort. প্রতিকার: geohash cell, ৩ × ৩ block, তারপর ETA.
    • Double assignment: WHERE id = ?-এ অবস্থার শর্ত নেই — দুই অনুরোধ একই চালককে busy করতে পারে, দুজনই “সফল”। প্রতিকার: WHERE id = ? AND state = 'available' AND version = ?, বদলানো সারির সংখ্যা দেখা।
    • Single point of failure: একটি WebSocket সার্ভার — বিকল হলে সব চালক অদৃশ্য, আর সংযোগের সংখ্যাও এক মেশিনের সীমায়। প্রতিকার: কয়েকটি gateway, পুনঃসংযোগে jitter, চালক → gateway registry.
    • আরও: offer-এর মেয়াদ নেই, সোজা দূরত্ব, stale চালক চেনার উপায় নেই, শহরভিত্তিক বিচ্ছিন্নতা নেই।
  4. আউটপুট অনুমান করুন: অংশ ৮-এর double-assignment simulator-এ GAP = 20 বদলে GAP = 50 করলে তিনটি লাইনে মোটামুটি কী আসবে? আগে সূত্রে ভাবুন, তারপর চালিয়ে মিলিয়ে নিন।

    সূত্রে সংঘাতের সম্ভাবনা ১ − (১ − ৫০/১০০)² = ১ − ০.২৫ = ০.৭৫, অর্থাৎ ১০,০০০-এর মধ্যে প্রায় ৭,৫০০. চালালে আসে:

    naive read-then-write double offers:  7475   losers who retried: 0
    CAS on version        double offers:     0   losers who retried: 7475
    theory for naive: 1 - (1 - 50/100)^2 = 0.75 -> about 7500

    CAS-এ double offer এখনো শূন্য — নিয়মটি সময়ের ওপর নির্ভর করে না। কিন্তু হারার সংখ্যা ৩,৬০৩ থেকে ৭,৪৭৫ — অর্থাৎ ধীর পথে সঠিকতা থাকে, দক্ষতা কমে: বেশি matcher দ্বিতীয় পছন্দে যায়। শিক্ষা: পড়া আর লেখার মাঝের সময় ছোট রাখুন, আর একই এলাকার অনুরোধ এক matcher-এর batch-এ আনুন।

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

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

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

  1. যাত্রী idempotency key-সহ অনুরোধ পাঠান, trip তৈরি হয় MATCHING অবস্থায়
  2. Pickup-এর geohash cell আর ৮টি প্রতিবেশীতে খালি চালক খোঁজা
  3. প্রথম কয়েকজনের ETA বের করে সাজানো
  4. বাছাই করা চালকের অবস্থা CAS দিয়ে offered, মেয়াদসহ offer
  5. চালক রাজি — trip ACCEPTED আর চালক en_route একই transaction-এ

ব্যস্ত সময়ে ৮০,০০০ চালক অনলাইন, প্রত্যেকে প্রতি ৪ সেকেন্ডে অবস্থান পাঠান। সার্ভারে প্রতি সেকেন্ডে কতটি অবস্থান-লেখা আসে?

  • ৩,২০,০০০
  • ২০,০০০
  • ১,৩৩৩
  • ২,০০০

Geohash দিয়ে কাছের চালক খোঁজার সময় শুধু যাত্রীর নিজের cell না দেখে চারপাশের ৮টি cell-ও কেন দেখা হয়?

  • নিজের cell-এর চালকদের সঠিক দূরত্ব জানতে
  • Geohash-এর precision বাড়াতে
  • সীমানার ওপারের খুব কাছের চালক যাতে বাদ না পড়েন
  • সব shard-এ চাপ সমান রাখতে

দুটি dispatcher একই চালককে “available, version 7” পড়ল। কোন লেখার নিয়মে ঠিক একজনই offer দিতে পারবে?

  • UPDATE … SET state='offered', version=8 WHERE id=17 AND state='available' AND version=7
  • UPDATE … SET state='offered' WHERE id=17
  • অ্যাপে আগে SELECT করে state দেখা, তারপর আলাদা UPDATE
  • লেখা ব্যর্থ হলে তিনবার retry করা

একটি ride-sharing অ্যাপে চালকের সর্বশেষ অবস্থানকে “soft state” বলা হয়। এর সবচেয়ে সঠিক অর্থ কোনটি?

  • ডেটাটি এনক্রিপ্ট না করেও রাখা যায়
  • ডেটাটি বছরের পর বছর রাখতে হয়
  • ডেটাটিতে strong consistency বাধ্যতামূলক
  • হারালে উৎস থেকে কয়েক সেকেন্ডের মধ্যে আবার পূরণ হয়ে যায়

একটি রাস্তায় যাত্রী R1 আছেন ২ km চিহ্নে (আগে এলেন), R2 আছেন ০ km-এ; চালক D1 ১ km-এ, D2 ৪ km-এ। দুজনকে একসাথে বণ্টন করলে (মোট দূরত্ব সবচেয়ে কম) মোট pickup দূরত্ব কত?

  • ৫ km
  • ৩ km
  • ২ km
  • ৬ km

একটি ride-sharing সিস্টেমে কোন ডেটার জন্য strong consistency সবচেয়ে জরুরি?

  • যাত্রীর মানচিত্রে দেখানো চালকের অবস্থান
  • একটি এলাকায় গত ৫ মিনিটের অনুরোধের সংখ্যা
  • Trip-এর অবস্থা আর কোন চালক কোন trip-এ
  • রাস্তার প্রতিটি অংশের গড় গতি

শব্দকোষ

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

মনে রাখুন

  1. Ride-sharing লেখা-ভারী: চালক ÷ ব্যবধান = লেখা/সেকেন্ড (১ লাখ ÷ ৪ = ২৫,০০০), অথচ সর্বশেষ অবস্থানের memory মাত্র কয়েক দশ MB.
  2. তিন রকম ডেটা, তিন ঘর: সর্বশেষ অবস্থান memory-র geo index-এ (soft state), পথের ইতিহাস log stream-এ, trip-এর অবস্থা শহর ধরে ভাগ করা database-এ।
  3. Geohash = দুই মাত্রার bit পালা করে, ৫ bit-এ একটি base32 অক্ষর; খুঁজুন নিজের আর ৮টি প্রতিবেশী cell-এ, আর ফল এক cell-এর ছোট দিকের চেয়ে দূরে হলে বলয় বাড়ান।
  4. Matching-এ ETA, সোজা দূরত্ব নয়; ছোট জানালায় batch বণ্টন সবার মোট অপেক্ষা কমায়; offer একবারে একজনকে, মেয়াদসহ।
  5. চালক আর trip-এর প্রতিটি বদল compare-and-set আর state machine-এ; প্রতিটি শহর একটি cell; অবস্থান-ডেটা PII — সীমিত প্রবেশ, সীমিত সময়।

পরের পর্বে: এখানে প্রতি সেকেন্ডে ছোট ছোট হাজারো বার্তা; পরের case study-তে উল্টো — বিশাল ভিডিও ফাইল আপলোড, নানা মানে transcoding, আর দুর্বল নেটওয়ার্কেও না থেমে চলা adaptive streaming.

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

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