পাঠ ৪৭ · ৫১-এর মধ্যে · মডিউল ১১
Home / Courses / System Design / রাইড-শেয়ারিং সিস্টেম

কেস স্টাডি: রাইড-শেয়ারিং সিস্টেম ডিজাইন করা

Case study: designing a ride-sharing system
১১ মিনিট পড়া উচ্চ · Advanced Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • রাইড-শেয়ারিং সিস্টেমের ফাংশনাল ও নন-ফাংশনাল রিকোয়ারমেন্ট এবং কেন লোকেশন-আপডেট থ্রুপুট সবচেয়ে বড় চ্যালেঞ্জ
  • L02-এর পদ্ধতিতে ড্রাইভার লোকেশন পিং ও রাইড ম্যাচিং QPS আলাদাভাবে অনুমান করা
  • লোড ব্যালেন্সিং (L11), স্টেটলেস সার্ভিস (L12) ও শার্ডেড স্টোরেজ (L17) দিয়ে একটি হাই-লেভেল আর্কিটেকচার
  • Python দিয়ে গ্রিড-বাকেটিং বাস্তবায়ন করে দেখানো কীভাবে "নিকটতম ড্রাইভার" সার্চ পুরো ফ্লিট স্ক্যান না করেই সম্ভব

১ · রিকোয়ারমেন্ট

একটি রাইড-শেয়ারিং সিস্টেমের ফাংশনাল রিকোয়ারমেন্ট মোটামুটি সরল — রাইডার রাইড রিকোয়েস্ট করবে, সিস্টেম একজন নিকটতম উপলব্ধ ড্রাইভারের সাথে ম্যাচ করবে, ট্রিপ চলাকালীন রিয়েল-টাইম অবস্থান দেখাবে, এবং ট্রিপ শেষে ভাড়া হিসাব করবে।

ফাংশনাল
রাইড রিকোয়েস্ট, নিকটতম ড্রাইভার ম্যাচিং, লাইভ ট্রিপ ট্র্যাকিং, ফেয়ার ক্যালকুলেশন।
নন-ফাংশনাল
লো-লেটেন্সি ম্যাচিং (সেকেন্ডের মধ্যে ড্রাইভার অফার), উচ্চ-ঘনত্বের কনকারেন্ট লোকেশন আপডেট, নির্ভুল জিওস্প্যাশিয়াল কোয়েরিGeospatial Queryভৌগোলিক অবস্থান (lat/long) ভিত্তিক কোয়েরি — যেমন "এই বিন্দুর কাছাকাছি কী কী আছে"।।

আসল চ্যালেঞ্জ হলো নন-ফাংশনাল দিক — বিশেষত নিকটতম উপলব্ধ ড্রাইভার খোঁজা যখন শহরে হাজার হাজার ড্রাইভার সচল অবস্থায় প্রতি কয়েক সেকেন্ডে নিজেদের অবস্থান আপডেট করছে।

২ · ক্যাপাসিটি অনুমান

L02-এর পদ্ধতিতে দুটি আলাদা সংখ্যা হিসাব করা দরকার — রাইড-রিকোয়েস্ট QPS এবং ড্রাইভার-লোকেশন-পিং QPS। ধরি একটি শহরে ৫০ লক্ষ (৫০,০০,০০০) DAU রাইডার আছে, গড়ে প্রতিজন দিনে ২টি রাইড রিকোয়েস্ট করে।

Python
daily_riders = 5_000_000
rides_per_rider_per_day = 2
seconds_per_day = 24 * 60 * 60

total_ride_requests = daily_riders * rides_per_rider_per_day
avg_ride_qps = total_ride_requests / seconds_per_day

active_drivers = 500_000        # পিক আওয়ারে সচল ড্রাইভার সংখ্যা
ping_interval_seconds = 4        # প্রতি ৪ সেকেন্ডে একটি লোকেশন পিং

location_ping_qps = active_drivers / ping_interval_seconds

print(f"গড় রাইড-রিকোয়েস্ট QPS: {avg_ride_qps:,.1f}")
print(f"ড্রাইভার লোকেশন-পিং QPS: {location_ping_qps:,.1f}")
print(f"পিং QPS, রাইড QPS-এর তুলনায় কত গুণ বেশি: {location_ping_qps / avg_ride_qps:,.0f}x")

    
কোডটি চালিয়ে দেখুন — লোকেশন-পিং QPS (~১,২৫,০০০) রাইড-রিকোয়েস্ট QPS (~১১৫.৭)-এর চেয়ে বহুগুণ বেশি। এই কারণেই রাইড-শেয়ারিং সিস্টেমের ইঞ্জিনিয়ারিং সময়ের বড় অংশ ব্যয় হয় লোকেশন-আপডেট পাইপলাইন (একটি হাই-থ্রুপুট স্ট্রিম, L24) অপ্টিমাইজ করায়, শুধু ম্যাচিং লজিকে নয়।

৩ · হাই-লেভেল ডিজাইন

রাইডার ও ড্রাইভার অ্যাপ থেকে রিকোয়েস্ট একটি লোড ব্যালেন্সারের (L11) মধ্য দিয়ে স্টেটলেস অ্যাপ সার্ভারে (L12) যায়। ড্রাইভার লোকেশন পিং একটি ইন-মেমরি জিওস্প্যাশিয়াল ইনডেক্স সার্ভিসকে ক্রমাগত আপডেট করে — এই ইনডেক্সই "নিকটতম ড্রাইভার" প্রশ্নের উত্তর দেয়। ম্যাচ হওয়ার পর ট্রিপ ডেটা একটি শার্ডেড ডেটাবেসে (L17, প্রায়ই trip_id বা city_id দিয়ে শার্ডেড) সংরক্ষিত হয়।

রাইডার অ্যাপ Rider App ড্রাইভার অ্যাপ Driver App লোড ব্যালেন্সার Load Balancer (L11) স্টেটলেস অ্যাপ সার্ভার App Servers (L12) জিওস্প্যাশিয়াল ইনডেক্স Grid Index (ম্যাচিং) শার্ডেড ট্রিপ DB Sharded DB (L17)
রাইড-শেয়ারিং সিস্টেমের হাই-লেভেল আর্কিটেকচার — জিওস্প্যাশিয়াল ইনডেক্স ও শার্ডেড ট্রিপ ডেটাবেস আলাদা কনসার্ন।

৪ · ডিপ ডাইভ — গ্রিড-বাকেটিং দিয়ে নিকটতম ড্রাইভার খোঁজা

প্রতিটি রাইড রিকোয়েস্টে সব ড্রাইভারের সাথে দূরত্ব হিসাব করা (naive O(n) স্ক্যান) হাজার হাজার ড্রাইভারের জন্য অকার্যকর। বাস্তব সিস্টেম geohashingGeohashinglat/long-কে একটি স্ট্রিং-এ এনকোড করার পদ্ধতি, যেখানে কাছাকাছি অবস্থানের স্ট্রিং প্রিফিক্স একই থাকে — দ্রুত নৈকট্য-ভিত্তিক লুকআপ সম্ভব করে। বা কোয়াডট্রি ব্যবহার করে সার্চ স্পেস আগেই ছোট করে রাখে। আমরা একটি সরলীকৃত সংস্করণ — গ্রিড-বাকেটিং — বাস্তবায়ন করব: lat/long-কে নিকটতম ০.০১ ডিগ্রি (~১ কিমি) গ্রিড সেলে রাউন্ড করে একটি বাকেট কী বানানো হয়, এবং প্রতিটি ড্রাইভার তার বর্তমান বাকেটে থাকে। রাইডারের কাছাকাছি ড্রাইভার খুঁজতে শুধু রাইডারের নিজের বাকেট এবং তার ৮টি প্রতিবেশী বাকেট (মোট ৩×৩ গ্রিড) চেক করলেই যথেষ্ট।

Python
import random

def bucket_key(lat, lon, precision=0.01):
    # lat/long-কে নিকটতম ০.০১ ডিগ্রি গ্রিড সেলে রাউন্ড করা
    return (round(round(lat / precision) * precision, 2),
            round(round(lon / precision) * precision, 2))

# ৫০০ জন ড্রাইভার ঢাকার একটি এলাকায় ছড়িয়ে আছে (সিমুলেটেড, ফিক্সড সিড)
rng = random.Random(42)
drivers = {}
for driver_id in range(1, 501):
    lat = round(23.70 + rng.random() * 0.30, 4)   # ~২৩.৭০ - ২৪.০০
    lon = round(90.30 + rng.random() * 0.30, 4)   # ~৯০.৩০ - ৯০.৬০
    drivers[driver_id] = (lat, lon)

# গ্রিড বাকেট তৈরি: bucket_key -> [driver_id, ...]
grid = {}
for driver_id, (lat, lon) in drivers.items():
    key = bucket_key(lat, lon)
    grid.setdefault(key, []).append(driver_id)

def find_nearby_drivers(rider_lat, rider_lon):
    rb_lat, rb_lon = bucket_key(rider_lat, rider_lon)
    candidates = []
    for d_lat in (-0.01, 0.0, 0.01):
        for d_lon in (-0.01, 0.0, 0.01):
            neighbor_key = (round(rb_lat + d_lat, 2), round(rb_lon + d_lon, 2))
            candidates.extend(grid.get(neighbor_key, []))
    return candidates

rider_lat, rider_lon = 23.81, 90.41   # রাইডারের অবস্থান
nearby = find_nearby_drivers(rider_lat, rider_lon)

print(f"মোট সিমুলেটেড ড্রাইভার: {len(drivers)}")
print(f"রাইডারের গ্রিড বাকেট: {bucket_key(rider_lat, rider_lon)}")
print(f"৩×৩ নেইবার গ্রিডে পাওয়া প্রার্থী ড্রাইভার সংখ্যা: {len(nearby)}")
print(f"প্রার্থী ড্রাইভার আইডি: {nearby}")
reduction = 100 - round(len(nearby) / len(drivers) * 100, 1)
print(f"স্ক্যান কমেছে: {len(drivers)} জন থেকে মাত্র {len(nearby)} জনে (~{reduction}% কম চেক)")

    
মূল কথা

গ্রিড-বাকেটিং দূরত্ব হিসাব করে না — এটি শুধু সার্চ স্পেসকে ৫০০ জন থেকে মুষ্টিমেয় প্রার্থীতে নামিয়ে আনে। চূড়ান্ত র‍্যাঙ্কিং (প্রকৃত দূরত্ব বা ETA অনুযায়ী) এই ছোট প্রার্থী তালিকার উপর করা হয় — পুরো ফ্লিটের উপর নয়। একই নীতি geohashing ও কোয়াডট্রি-ভিত্তিক ইনডেক্সেও কাজ করে, শুধু বাকেটিং স্কিমটা ভিন্ন।

৫ · ট্রেড-অফ

গ্রিড-বাকেটিং সহজ ও দ্রুত, কিন্তু একটি নির্দিষ্ট গ্রিড-সাইজ বেছে নিতে হয় — খুব ছোট সেল হলে ঘনবসতিপূর্ণ এলাকায় (যেমন শহরের কেন্দ্র) খুব কম বাকেট কভার হয়ে যায় ও ফলাফল অসম্পূর্ণ হতে পারে; খুব বড় সেল হলে প্রতিটি বাকেটে অনেক ড্রাইভার জমা হয়ে সুবিধাটাই কমে যায়। geohashing ভ্যারিয়েবল-প্রিসিশন প্রিফিক্স দিয়ে এটি আংশিক সমাধান করে, আর কোয়াডট্রি ঘনত্ব অনুযায়ী সেল সাইজ ডাইনামিকভাবে ছোট-বড় করে — বাড়তি জটিলতার বিনিময়ে ভালো ভারসাম্য। উচ্চ-লেখা থ্রুপুটের (প্রতি ৪ সেকেন্ডে লক্ষাধিক পিং) কারণে জিওস্প্যাশিয়াল ইনডেক্সকে ইন-মেমরি ও শার্ডেড রাখতে হয় — একটি একক ডেটাবেসে সরাসরি এই আপডেট রেট সামলানো বাস্তবসম্মত নয়।

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

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ কেন ড্রাইভার লোকেশন-পিং QPS রাইড-রিকোয়েস্ট QPS-এর চেয়ে বহুগুণ বেশি হওয়া সত্ত্বেও, বেশিরভাগ ইঞ্জিনিয়ারিং আলোচনা "ম্যাচিং অ্যালগরিদম" নিয়ে বেশি হয়?

লোকেশন-পিং থ্রুপুট মূলত একটি ভালো-বোঝা "high-write-volume স্ট্রিম" সমস্যা (L24) — সঠিক ইনফ্রাস্ট্রাকচার (কিউ, ব্যাচিং, শার্ডিং) দিয়ে সমাধান প্রায় "স্ট্যান্ডার্ড"। কিন্তু "নিকটতম, সবচেয়ে দ্রুত পৌঁছাতে পারা, এখনো ব্যস্ত নয় এমন ড্রাইভার" খোঁজা একটি চলমান, ক্রমাগত পরিবর্তনশীল অপ্টিমাইজেশন সমস্যা — এখানেই বেশিরভাগ প্রোডাক্ট-ডিফারেনসিয়েশন ও ইঞ্জিনিয়ারিং জটিলতা লুকিয়ে থাকে, তাই আলোচনার কেন্দ্রবিন্দু হয়।

প্র ০২ গ্রিড-বাকেটিং স্কিমে যদি একজন ড্রাইভার ঠিক দুটি বাকেটের সীমানার কাছাকাছি থাকে, তাহলে কী সমস্যা হতে পারে — এবং ৩×৩ নেইবার-গ্রিড চেক করাটা কীভাবে সেটা প্রশমিত করে?

একজন ড্রাইভার বাকেট সীমানার ঠিক ওপারে থাকলে, রাইডার নিজের বাকেট শুধু চেক করলে তাকে মিস করবে — যদিও বাস্তবে সে খুব কাছেই। এই কারণেই কোডে শুধু রাইডারের নিজের বাকেট নয়, তার ৮টি প্রতিবেশী বাকেটও (মোট ৩×৩ গ্রিড) চেক করা হয় — তাই সীমানার ওপারের কাছাকাছি ড্রাইভাররাও প্রার্থী তালিকায় ধরা পড়ে। এটি সম্পূর্ণ নির্ভুল নয় (একেবারে কোণার ক্ষেত্রে এখনও মিস হতে পারে), কিন্তু একক-বাকেট চেকের চেয়ে অনেক বেশি নির্ভরযোগ্য।

প্র ০৩ এই সিস্টেমে শার্ডেড ট্রিপ ডেটাবেসের (L17) জন্য শার্ড-কী হিসেবে city_id ব্যবহার করা কেন trip_id ব্যবহারের চেয়ে প্রায়ই ভালো হতে পারে?

city_id দিয়ে শার্ড করলে একই শহরের সব ট্রিপ ডেটা একই শার্ডে থাকে — অপারেশনাল কোয়েরি (যেমন "ঢাকার আজকের সব সক্রিয় ট্রিপ") এক শার্ডেই সম্পন্ন হয়, ক্রস-শার্ড জয়েন লাগে না (L17-এর একটি মূল চ্যালেঞ্জ)। trip_id দিয়ে শার্ড করলে লোড ভালোভাবে ছড়ায় কিন্তু শহরভিত্তিক অপারেশনাল কোয়েরির জন্য প্রতিবার সব শার্ডে স্ক্যাটার-গ্যাদার লাগবে — অ্যাক্সেস প্যাটার্ন বুঝে শার্ড-কী বেছে নেওয়াই আসল দক্ষতা।

অনুশীলন

  1. পরিবর্তন করুন: কোড সেলে গ্রিড precision-কে ০.০১ থেকে ০.০৫-এ বদলান এবং আবার চালান। প্রার্থী ড্রাইভার সংখ্যা কী বদলায় এবং কেন?

    precision ০.০৫ (~৫ কিমি) করলে প্রতিটি গ্রিড সেল আগের চেয়ে অনেক বড় এলাকা কভার করে, তাই প্রতিটি বাকেটে (এবং ফলে ৩×৩ নেইবার গ্রিডে) আগের চেয়ে বেশি ড্রাইভার পড়বে — প্রার্থী সংখ্যা বাড়বে। এটি দেখায় গ্রিড সাইজ বাছাই সরাসরি সার্চ-স্পেসের আকার নিয়ন্ত্রণ করে — খুব বড় সেল মানে কম "ফিল্টারিং সুবিধা"।

  2. চিন্তা করুন: ৩×৩ নেইবার গ্রিডেও যদি কোনো প্রার্থী ড্রাইভার না পাওয়া যায় (রাইডার একটি জনবিরল এলাকায়), সিস্টেমের পরবর্তী যুক্তিসঙ্গত পদক্ষেপ কী হওয়া উচিত?

    সার্চ ব্যাসার্ধ ধাপে ধাপে বাড়ানো উচিত — ৩×৩ থেকে ৫×৫, তারপর প্রয়োজনে আরও — যতক্ষণ না অন্তত একজন প্রার্থী পাওয়া যায় বা একটি সর্বোচ্চ ব্যাসার্ধ সীমা (যার পর "কোনো ড্রাইভার উপলব্ধ নেই" দেখানো হয়) অতিক্রম হয়। এটি নিশ্চিত করে জনবহুল এলাকায় সার্চ ছোট থাকে (দ্রুত), আর জনবিরল এলাকায় প্রয়োজনমতো বড় হয় (সম্পূর্ণতা)।

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

পূর্ববর্তী পাঠ
কেস স্টাডি: নিউজফিড/টাইমলাইন ডিজাইন করা