পাঠ ২৬ · ৫১-এর মধ্যে · মডিউল ৭
Home / Courses / System Design / API গেটওয়ে ও সার্ভিস ডিসকভারি

API গেটওয়ে ও সার্ভিস ডিসকভারি

API gateway & service discovery
৮ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • API গেটওয়ে কী কাজ করে এবং কেন মাইক্রোসার্ভিসের সামনে এটি প্রায় অপরিহার্য
  • সার্ভিস রেজিস্ট্রি ও হেলথ চেক কীভাবে "কে বেঁচে আছে" ট্র্যাক করে
  • ক্লায়েন্ট-সাইড বনাম সার্ভার-সাইড সার্ভিস ডিসকভারির পার্থক্য
  • Python দিয়ে একটি সার্ভিস রেজিস্ট্রি ও রাউন্ড-রবিন ডিসকভারি বাস্তবায়ন করা

১ · সমস্যা — অনেকগুলো সার্ভিস, ক্লায়েন্ট কী করবে?

L25-এ যখন একটি অ্যাপ্লিকেশনকে ১০-২০টি মাইক্রোসার্ভিসে ভাঙা হয়, তখন ক্লায়েন্টের (মোবাইল অ্যাপ, ওয়েব ফ্রন্টএন্ড) সামনে দুটি নতুন প্রশ্ন আসে — (১) প্রতিটি সার্ভিসের জন্য কি ক্লায়েন্টকে আলাদা URL/ঠিকানা মনে রাখতে হবে? (২) একটি সার্ভিসের একাধিক ইনস্ট্যান্স (স্কেলিংয়ের জন্য) চললে ক্লায়েন্ট কোনটায় কল করবে, এবং কোনো ইনস্ট্যান্স ক্র্যাশ করলে সেটা কীভাবে জানবে?

২ · API গেটওয়ে

API গেটওয়েAPI Gatewayমাইক্রোসার্ভিসের সামনে বসা একক এন্ট্রি পয়েন্ট। হলো মাইক্রোসার্ভিসের সামনে বসা একটি একক এন্ট্রি পয়েন্ট। ক্লায়েন্ট শুধু গেটওয়ের ঠিকানা জানে — গেটওয়ে রিকোয়েস্ট বিশ্লেষণ করে সঠিক ব্যাকএন্ড সার্ভিসে ফরওয়ার্ড করে।

রাউটিং
URL পাথ/হেডার দেখে কোন সার্ভিসে রিকোয়েস্ট যাবে তা ঠিক করে (যেমন /orders/* → Order Service)।
অথেন্টিকেশন
প্রতিটি সার্ভিসে আলাদাভাবে অথ যাচাই না করে গেটওয়েতে একবার কেন্দ্রীয়ভাবে যাচাই করা যায় (L39-এ বিস্তারিত)।
রেট লিমিটিং
প্রতিটি সার্ভিসকে আলাদাভাবে রক্ষা না করে গেটওয়েতে কেন্দ্রীয়ভাবে ক্লায়েন্টদের সীমাবদ্ধ করা যায় (L35, L44-এ বিস্তারিত)।
রেসপন্স অ্যাগ্রিগেশন
একাধিক সার্ভিস থেকে ডেটা জোগাড় করে একটি একক রেসপন্সে মিলিয়ে ক্লায়েন্টকে দেওয়া — ক্লায়েন্টকে একাধিক কল করতে হয় না।

৩ · সার্ভিস ডিসকভারি ও সার্ভিস রেজিস্ট্রি

গেটওয়ে বা যেকোনো ক্লায়েন্টকে জানতে হয় একটি সার্ভিসের বর্তমানে কোন কোন ইনস্ট্যান্স বেঁচে আছে (ইনস্ট্যান্স স্কেল হওয়া, ক্র্যাশ করা, বা রিডিপ্লয় হওয়ার কারণে এই তালিকা ক্রমাগত বদলায়)। এই তথ্যের কেন্দ্রীয় উৎস হলো সার্ভিস রেজিস্ট্রিService Registryজীবিত সার্ভিস ইনস্ট্যান্সগুলোর একটি আপ-টু-ডেট তালিকা — নিয়মিত হেলথ চেকের মাধ্যমে মৃত ইনস্ট্যান্স বাদ দেওয়া হয়। — একটি ডেটাবেস যা প্রতিটি সার্ভিসের জীবিত ইনস্ট্যান্সের ঠিকানা রাখে, এবং নিয়মিত হেলথ চেকের মাধ্যমে মৃত ইনস্ট্যান্স সরিয়ে ফেলে (L11-এর লোড ব্যালেন্সার হেলথ চেকের মতোই ধারণা)।

ক্লায়েন্ট-সাইড ডিসকভারি
ক্লায়েন্ট নিজে রেজিস্ট্রি জিজ্ঞেস করে ইনস্ট্যান্স তালিকা পায়, নিজে একটি বেছে নেয় (নিজস্ব লোড-ব্যালেন্সিং লজিক থাকে)। সরল, কিন্তু প্রতিটি ক্লায়েন্টে ডিসকভারি লজিক দরকার।
সার্ভার-সাইড ডিসকভারি
ক্লায়েন্ট একটি স্থির ঠিকানায় (যেমন লোড ব্যালেন্সার/গেটওয়ে) কল করে; সেই রাউটার নিজে রেজিস্ট্রি জিজ্ঞেস করে সঠিক ইনস্ট্যান্সে ফরওয়ার্ড করে। ক্লায়েন্ট সরল থাকে, জটিলতা কেন্দ্রীভূত হয়।
ক্লায়েন্ট Client API গেটওয়ে API Gateway সার্ভিস রেজিস্ট্রি Service Registry সার্ভিস A instance 1 সার্ভিস A instance 2 হেলথ চেক — মৃত ইনস্ট্যান্স রেজিস্ট্রি থেকে সরায় Health checks remove dead instances
ক্লায়েন্ট শুধু গেটওয়ে জানে; গেটওয়ে রেজিস্ট্রি থেকে জীবিত ইনস্ট্যান্স জেনে সঠিক জায়গায় ফরওয়ার্ড করে।

৪ · Python-এ একটি সার্ভিস রেজিস্ট্রি ও রাউন্ড-রবিন ডিসকভারি

নিচে একটি ছোট সার্ভিস রেজিস্ট্রি বানানো হলো — register(), deregister(), discover() ফাংশনসহ, এবং তার উপর একটি ক্লায়েন্ট-সাইড রাউন্ড-রবিন ডিসকভারি ফাংশন যা পরপর কলে ভিন্ন ভিন্ন ইনস্ট্যান্স বেছে নেয়।

Python
registry = {}

def register(service_name, address):
    registry.setdefault(service_name, [])
    if address not in registry[service_name]:
        registry[service_name].append(address)
    print(f"register: {service_name} <- {address} | বর্তমান instances: {registry[service_name]}")

def deregister(service_name, address):
    if service_name in registry and address in registry[service_name]:
        registry[service_name].remove(address)
    print(f"deregister: {service_name} -x- {address} | বর্তমান instances: {registry[service_name]}")

def discover(service_name):
    return registry.get(service_name, [])

# round-robin এর জন্য প্রতিটি সার্ভিসের আলাদা কাউন্টার
_rr_counters = {}

def discover_round_robin(service_name):
    instances = discover(service_name)
    if not instances:
        return None
    idx = _rr_counters.get(service_name, 0)
    chosen = instances[idx % len(instances)]
    _rr_counters[service_name] = idx + 1
    return chosen

register("order-service", "10.0.0.1:8080")
register("order-service", "10.0.0.2:8080")
register("order-service", "10.0.0.3:8080")

print("\nক্লায়েন্ট-সাইড round-robin discovery -- ৬টি পরপর কল:")
for i in range(6):
    picked = discover_round_robin("order-service")
    print(f"  call {i+1}: routed -> {picked}")

deregister("order-service", "10.0.0.2:8080")
print(f"\nderegister-এর পর জীবিত instances: {discover('order-service')}")

    
লক্ষ্য করুন — ৩টি ইনস্ট্যান্সের উপর ৬টি কল রাউন্ড-রবিন ক্রমে সমানভাবে ভাগ হয়ে যায় (instance 1, 2, 3, 1, 2, 3)। deregister() কল করার পর discover() শুধু জীবিত ইনস্ট্যান্সই ফেরত দেয় — বাস্তব সিস্টেমে এই deregister ঘটে স্বয়ংক্রিয় হেলথ চেক ব্যর্থ হলে, ম্যানুয়াল কলে নয়।
মূল কথা · Key takeaway

API গেটওয়ে ও সার্ভিস ডিসকভারি একসাথে মাইক্রোসার্ভিস আর্কিটেকচারের (L25) "কে কোথায়" সমস্যাটি সমাধান করে — ক্লায়েন্ট একটি স্থির ঠিকানায় বিশ্বাস রাখে, আর নিচের স্তরে সার্ভিসের সংখ্যা ও অবস্থান যতই বদলাক না কেন (স্কেল আপ/ডাউন, ক্র্যাশ, রিডিপ্লয়), সেই পরিবর্তন রেজিস্ট্রি ও গেটওয়ে/ডিসকভারি স্তরে শোষিত হয়ে যায়।

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

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

প্র ০১ API গেটওয়ে নিজেই কি একটি single point of failure হয়ে উঠতে পারে? এই ঝুঁকি কীভাবে কমানো যায়?

হ্যাঁ — যদি একটিমাত্র গেটওয়ে ইনস্ট্যান্স থাকে এবং সেটি ডাউন হয়ে যায়, তাহলে পুরো সিস্টেমে কোনো ট্রাফিকই ঢুকতে পারবে না, যদিও ব্যাকএন্ড সার্ভিসগুলো সম্পূর্ণ সুস্থ থাকতে পারে। এই ঝুঁকি কমানো হয় ঠিক যেভাবে যেকোনো ক্রিটিক্যাল সার্ভিসের ঝুঁকি কমানো হয় — গেটওয়ের নিজেরও একাধিক ইনস্ট্যান্স চালিয়ে তার সামনে একটি লোড ব্যালেন্সার (L11) বসানো, স্বয়ংক্রিয় হেলথ চেক ও ফেইলওভার (L37) রাখা, যাতে গেটওয়ে স্তরেও কোনো একক ব্যর্থতার বিন্দু না থাকে।

প্র ০২ সার্ভার-সাইড ডিসকভারি ব্যবহার করলে ক্লায়েন্ট কোডে কী সুবিধা হয়, এবং এর বিনিময়ে কী খরচ দিতে হয়?

সুবিধা — ক্লায়েন্ট কোড অনেক সরল থাকে; তাকে রেজিস্ট্রি সম্পর্কে কিছুই জানতে হয় না, শুধু একটি স্থির ঠিকানায় কল করে। বিশেষ করে যখন অনেক ভিন্ন ভাষায় লেখা ক্লায়েন্ট (মোবাইল অ্যাপ, ওয়েব, অন্য সার্ভিস) থাকে, তখন প্রতিটিতে আলাদা ডিসকভারি লজিক লেখার বদলে একটি জায়গায় (রাউটার/গেটওয়েতে) এই লজিক রাখা অনেক কম কাজ। খরচ হলো — একটি অতিরিক্ত নেটওয়ার্ক হপ (ক্লায়েন্ট → রাউটার → প্রকৃত সার্ভিস) যোগ হয়, যা সামান্য লেটেন্সি বাড়ায়, এবং রাউটার/গেটওয়ে নিজেই একটি অতিরিক্ত অবকাঠামো-নির্ভরতা হয়ে দাঁড়ায় যা সঠিকভাবে স্কেল ও রিলায়েবল রাখতে হয়।

প্র ০৩ উপরের কোডে হেলথ চেক ছাড়া শুধু ম্যানুয়াল deregister() কল দেখানো হয়েছে। বাস্তবে যদি একটি ইনস্ট্যান্স হঠাৎ ক্র্যাশ করে (কেউ deregister না করেই), তাহলে কী সমস্যা হবে?

রেজিস্ট্রি সেই মৃত ইনস্ট্যান্সকে এখনও "জীবিত" ভাববে, এবং রাউন্ড-রবিন ডিসকভারি মাঝে মাঝে সেই মৃত ঠিকানায় রিকোয়েস্ট পাঠাবে — যা টাইমআউট বা কানেকশন এরর দেবে। এই কারণে বাস্তব সার্ভিস রেজিস্ট্রি (Consul, etcd, Eureka) শুধু ম্যানুয়াল deregister-এর উপর নির্ভর করে না — প্রতিটি ইনস্ট্যান্সকে নিয়মিত একটি "heartbeat" বা হেলথ-চেক এন্ডপয়েন্ট পিং করতে হয়; একটি নির্দিষ্ট সময় ধরে সাড়া না পেলে রেজিস্ট্রি নিজে থেকেই সেই ইনস্ট্যান্সকে তালিকা থেকে বাদ দেয় (L11-এর লোড ব্যালেন্সার হেলথ চেকের মতোই নীতি)।

অনুশীলন

  1. চিন্তা করুন: API গেটওয়ের ৪টি কাজ (রাউটিং, অথ, রেট লিমিটিং, রেসপন্স অ্যাগ্রিগেশন) এর মধ্যে কোনটি ছাড়া একটি মাইক্রোসার্ভিস সিস্টেম সবচেয়ে বেশি নিরাপত্তা ঝুঁকিতে পড়বে, এবং কেন?

    অথেন্টিকেশন — যদি গেটওয়েতে কেন্দ্রীয় অথ যাচাই না থাকে, তাহলে প্রতিটি ব্যাকএন্ড সার্ভিসকে নিজে নিজে অথ যাচাই করতে হবে, এবং যেকোনো একটি সার্ভিসে এই লজিক ভুলভাবে বাস্তবায়িত বা বাদ পড়লে সেই একটি সার্ভিসই পুরো সিস্টেমের একটি নিরাপত্তা ফাঁকফোকর হয়ে দাঁড়াবে। কেন্দ্রীয়ভাবে গেটওয়েতে অথ যাচাই করলে একটি জায়গায় ঠিকভাবে বাস্তবায়ন করলেই পুরো সিস্টেম সুরক্ষিত থাকে (L39-এ বিস্তারিত)।

  2. কোড পরিবর্তন করুন: উপরের কোড সেলে একটি "least connections" স্টাইলের discovery ফাংশন discover_least_loaded(service_name, active_connections) লিখুন যেখানে active_connections একটি dict (ঠিকানা → বর্তমান কানেকশন সংখ্যা) — এটি সবচেয়ে কম কানেকশনযুক্ত ইনস্ট্যান্স বেছে নেবে (round-robin-এর বদলে)। ৩টি ইনস্ট্যান্সের জন্য ভিন্ন ভিন্ন কানেকশন সংখ্যা ধরে টেস্ট করুন।

    সমাধানের কাঠামো: def discover_least_loaded(service_name, active_connections):
      instances = discover(service_name)
      return min(instances, key=lambda addr: active_connections.get(addr, 0))
    । যদি active_connections = {"10.0.0.1:8080": 5, "10.0.0.3:8080": 2} হয় (instance 2 ইতিমধ্যে deregister হয়ে গেছে), তাহলে এটি 10.0.0.3:8080 ফেরত দেবে কারণ তার কানেকশন সংখ্যা (২) সবচেয়ে কম — এটি ঠিক L11-এর "least connections" লোড-ব্যালেন্সিং অ্যালগরিদমের একটি রূপ, শুধু এখানে সার্ভিস ডিসকভারির সাথে মেশানো হলো।

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

আগের পাঠ
মনোলিথ বনাম মাইক্রোসার্ভিস