Foundationপ্রথম নীতি থেকে
LEVEL 12লেসন ১৬/২২কঠিন৫০ মিনিট

Reverse Proxy — nginx, Envoy, আর Service Mesh-এর জন্ম

Reverse Proxies: nginx, Envoy, and the Birth of the Service Mesh

Reverse proxy হলো সেই সফটওয়্যার যা clients-এর চোখে 'একটা সার্ভার' হয়ে দাঁড়ায় অথচ পেছনে backend-এর একটা পুরো fleet লুকিয়ে রাখে — nginx এই কাজটা config-file আর reload দিয়ে করে, Envoy করে streaming xDS API দিয়ে যাতে reload ছাড়াই backend পরিবর্তন সাথে সাথে প্রতিফলিত হয়। সেই একই idea যখন cluster-এর প্রতিটা service instance-এর পাশে একটা করে ছোট sidecar Envoy বসিয়ে সবার জন্য এক করে দেওয়া হয়, তখন জন্ম হয় service mesh-এর।

এই লেসন শেষে আপনি পারবেন

  • Forward proxy আর reverse proxy-র মধ্যেকার সংজ্ঞাগত পার্থক্য — কে কাকে প্রতিনিধিত্ব করছে — নির্ভুলভাবে ব্যাখ্যা করতে পারবেন, আর কেন reverse proxy আসলে L7 load balancing বাস্তবায়নের একটা concrete সফটওয়্যার-স্তর
  • একটা বাস্তব nginx config-এ `upstream` block আর `location`-ভিত্তিক `proxy_pass` পড়ে, লাইন-বাই-লাইন ব্যাখ্যা করতে পারবেন কীভাবে path-based routing দুইটা ভিন্ন backend pool-এ ট্রাফিক পাঠায়
  • nginx-এর config-reload মডেল আর Envoy-র xDS-ভিত্তিক streaming মডেলের মধ্যে পার্থক্য বলতে পারবেন, আর কেন dynamic container/Kubernetes পরিবেশে reload-based মডেল বিশেষভাবে ব্যয়বহুল
  • Sidecar pattern-এর মেকানিজম বর্ণনা করতে পারবেন — প্রতিটা service instance-এর নিজস্ব local Envoy proxy কীভাবে একই Pod-এ বসে সব ইন-আউট ট্রাফিক আটকায় — আর কেন এটা retry/timeout/mTLS/observability-কে ভাষা-নিরপেক্ষ করে তোলে
  • একটা simulation চালিয়ে reload-based বনাম streaming config-propagation-এর staleness measure করে সংখ্যায় দেখাতে পারবেন কেন dynamic backend churn-এ streaming মডেল জেতে

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

virtualization-cloud/load-balancing-l4-l7 লেসন একটা প্রশ্নে থেমেছিল: L7 load balancing মানে HTTP-র ভেতরটা পড়ে (path, header, cookie) routing সিদ্ধান্ত নেওয়া — কিন্তু ঠিক কোন সফটওয়্যার এই কাজটা বাস্তবে করে? আজকের লেসনের উত্তর একটা নাম: reverse proxy। L7 load balancer একটা ভূমিকা (role); reverse proxy সেই ভূমিকা পালন করা একটা concrete, চালানো-যায়-এমন প্রোগ্রাম — nginx, HAProxy, Envoy, Traefik-এর মতো সফটওয়্যার, যেগুলো routing ছাড়াও আরও অনেক কিছু করে: TLS termination, response caching, header rewriting, compression।

কিন্তু “proxy” শব্দটা নিজেই দুই দিকে তাকাতে পারে — একটা clients-এর পাশে দাঁড়িয়ে, আরেকটা servers-এর পাশে। এই দুইটাকে গুলিয়ে ফেলা industry-র সবচেয়ে সাধারণ, বিভ্রান্তিকর ভুলগুলোর একটা — আজকের প্রথম কাজ এই দুইটাকে স্পষ্টভাবে আলাদা করা।

তারপর আমরা দেখব একই কাজ (reverse proxying) করার দুইটা প্রজন্মের সফটওয়্যার — nginx-এর ক্লাসিক, ফাইল-ভিত্তিক কনফিগারেশন মডেল, আর Envoy-র আধুনিক, streaming API-ভিত্তিক মডেল — আর কেন এই পার্থক্যটা ঠিক এই module-এর কেন্দ্রীয় থিম, container orchestration-এর dynamism-এর সাথে সরাসরি সংঘর্ষে আসে। শেষে দেখব কীভাবে সেই একই Envoy, cluster-জুড়ে ছড়িয়ে দিলে, জন্ম দেয় service mesh নামের একটা সম্পূর্ণ নতুন architecture-স্তরের।

মূল ধারণা

Forward proxy বনাম reverse proxy — কে কাকে লুকায়

একই শব্দ, দুইটা বিপরীতমুখী ভূমিকা। পার্থক্যটা একটা প্রশ্নে ধরা যায়: proxy-টা কার পক্ষে দাঁড়িয়ে আছে, ক্লায়েন্টের নাকি সার্ভারের?

Forward proxy — client-দের প্রতিনিধি। একটা corporate office-এ সব কর্মীর ব্রাউজার একটা কেন্দ্রীয় proxy দিয়ে ইন্টারনেটে বের হয়। বাইরের ওয়েবসাইট শুধু সেই proxy-র IP দেখে — ভেতরের individual কর্মীর মেশিন অদৃশ্য। Forward proxy client-দের সামনে দাঁড়িয়ে বাইরের জগতের কাছে তাদের প্রতিনিধিত্ব করে, তাদের লুকায় ও রক্ষা করে (content filtering, access logging, anonymization)। ক্লায়েন্ট জানে সে একটা proxy ব্যবহার করছে — proxy-র ঠিকানা ব্রাউজার-সেটিংসে explicitly বসানো থাকে।

Reverse proxy — server-দের প্রতিনিধি। ঠিক উল্টো দিকে দাঁড়িয়ে: এটা servers-এর সামনে দাঁড়ায়, backend infrastructure-কে clients-এর কাছ থেকে লুকায়। একজন ক্লায়েন্ট api.example.com-এ request পাঠালে সে বিশ্বাস করে সরাসরি “সার্ভারের” সাথে কথা বলছে — বাস্তবে সে কথা বলছে reverse proxy-র সাথে, যেটা তারপর request-টা পেছনের বহু সম্ভাব্য real backend-এর একটাতে ফরওয়ার্ড করে। ক্লায়েন্ট এই ফরওয়ার্ডিং সম্পর্কে কিছুই জানে না — কোনো client-সাইড সেটিংস লাগে না; পুরো ব্যাপারটা ক্লায়েন্টের কাছে transparent।

virtualization-cloud/load-balancing-l4-l7 লেসনের ভাষায় বললে: reverse proxy হলো সেই সফটওয়্যার-স্তর যেখানে L7 routing সিদ্ধান্ত (path/header/cookie দেখে কোন backend-এ পাঠানো হবে) বাস্তবে কার্যকর হয়। কিন্তু routing শুধু তার একটামাত্র দায়িত্ব — একটা প্রোডাকশন reverse proxy সাধারণত আরও করে:

  • TLS termination — ক্লায়েন্টের সাথে HTTPS handshake (networking/tls-handshake লেসনের মেকানিক্স) reverse proxy-তেই শেষ হয়; backend-এর সাথে সংযোগ প্রায়ই plain HTTP (একই datacenter-এর ভেতরে) বা আলাদা internal TLS দিয়ে।
  • Response caching — বারবার একই request এলে backend-কে না জাগিয়ে proxy নিজেই আগের response ফেরত দেয় (networking/http লেসনের Cache-Control/ETag দুনিয়াই এখানে proxy-স্তরে প্রয়োগ হয়)।
  • Header rewriting — X-Forwarded-For যোগ করা (আসল ক্লায়েন্ট IP backend-কে জানানো, কারণ backend-এর দৃষ্টিতে সব request proxy থেকেই আসছে), Host header বদলানো, নিরাপত্তা-header যোগ করা।
  • Compression — response gzip/brotli করে ব্যান্ডউইথ কমানো।

ভেতরে কী ঘটছে

nginx-এর কনফিগারেশন মডেল — upstream আর proxy_pass

nginx-এ একটা reverse proxy সেটআপ করার কেন্দ্রীয় দুইটা ধারণা: upstream block — নাম-করা একটা backend server-এর pool সংজ্ঞায়িত করে — আর location block-এর ভেতরের proxy_pass — কোন URL path সেই pool-এ পাঠানো হবে তা নির্ধারণ করে। নিচের config দুইটা সম্পূর্ণ ভিন্ন backend pool-এ path অনুযায়ী route করে:

# /etc/nginx/conf.d/api-gateway.conf

upstream users_service {
    # তিনটা backend instance — round-robin (nginx-এর ডিফল্ট) ভাগ করে নেয়
    server 10.0.1.11:8080;
    server 10.0.1.12:8080;
    server 10.0.1.13:8080;
}

upstream orders_service {
    server 10.0.2.21:9090;
    server 10.0.2.22:9090;
}

server {
    listen 443 ssl;
    server_name api.example.com;

    # TLS termination — ক্লায়েন্টের সাথে HTTPS এখানেই শেষ হয়
    ssl_certificate     /etc/nginx/certs/api.example.com.crt;
    ssl_certificate_key /etc/nginx/certs/api.example.com.key;

    location /users/ {
        proxy_pass http://users_service/;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    location /orders/ {
        proxy_pass http://orders_service/;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

লাইন-বাই-লাইন অর্থ:

  • upstream users_service { ... } — একটা নাম-করা pool সংজ্ঞা, তিনটা backend server entry। nginx ডিফল্টভাবে round-robin ভাগ করে দেয়; least_conn, ip_hash-এর মতো অন্য strategy-ও আছে (load-balancing-l4-l7 লেসনের algorithm-গুলোরই বাস্তবায়ন)।
  • server { listen 443 ssl; ... } — একটা virtual server block, নির্দিষ্ট hostname-এর জন্য। ssl_certificate/ssl_certificate_key এখানেই TLS termination ঘটায়।
  • location /users/ { ... } — path-prefix matcher। /users/ দিয়ে শুরু হওয়া যেকোনো request এই block-এ পড়ে।
  • proxy_pass http://users_service/; — এই directive-ই আসল কাজটা করে: matched request-টা users_service upstream-এর একটা backend-এ ফরওয়ার্ড করে দেয়। লক্ষ্য করুন upstream-এর নাম (users_service) একটা DNS hostname না — এটা nginx-এর নিজস্ব ভেতরের namespace-এ সংজ্ঞায়িত।
  • proxy_set_header X-Forwarded-For ... — backend-কে জানানো আসল ক্লায়েন্ট IP কী, কারণ backend-এর সংযোগ সবসময় nginx থেকে আসছে বলে মনে হবে নাহলে।

/orders/-এর জন্য একদম একই প্যাটার্ন, শুধু ভিন্ন upstream। এটাই path-based L7 routing-এর সবচেয়ে concrete রূপ — একটাই সার্ভার, একটাই পোর্ট, কিন্তু ভেতরে দুইটা সম্পূর্ণ আলাদা backend fleet-এ ট্রাফিক ভাগ হচ্ছে path দেখে।

উদাহরণ

Envoy — reload বাদ দিয়ে streaming আপডেটে যাওয়ার গল্প

nginx-এর reload-মডেল একটা স্ট্যাটিক backend fleet-এ (হাতে গোনা কয়েকটা VM, কালেভদ্রে বদলায়) পুরোপুরি ঠিকঠাক। কিন্তু এই module যে জগতের দিকে এগিয়েছে — Kubernetes Pod, যেগুলো ক্রমাগত তৈরি, ধ্বংস, reschedule হয়, প্রতিবার নতুন IP নিয়ে — সেখানে reload-মডেল একটা বাস্তব সমস্যা তৈরি করে: backend তালিকা প্রতি সেকেন্ডে বদলাতে পারে, কিন্তু প্রতিটা বদলে একটা ফাইল-রিরাইট আর reload ট্রিগার করা ব্যয়বহুল ও disruptive।

Envoy শুরু থেকেই এই সমস্যাকে কেন্দ্র করে ডিজাইন করা — এর সমাধানের নাম xDS API (x = Cluster/Endpoint/Route/Listener-এর জন্য বসানো প্লেসহোল্ডার অক্ষর — CDS, EDS, RDS, LDS)। এটা একটা gRPC streaming API: Envoy কোনো config ফাইল পড়ে না (বা শুধু bootstrap-এর জন্য ন্যূনতম একটা পড়ে) — বরং একটা আলাদা control plane প্রসেসের সাথে persistent gRPC স্ট্রিম খুলে রাখে, আর control plane যখনই backend তালিকা বদলায় (নতুন Pod up, পুরনোটা down), সেই মুহূর্তেই নতুন তথ্য push করে দেয় — Envoy-কে কোনো reload বা restart করতে হয় না।

একটা নতুন backend যোগ হওয়া — nginx বনাম Envoy
  1. ইভেন্ট: নতুন Pod Running হলো, নতুন IP পেলদুইটা মডেলেই শুরুর বিন্দু একই — orchestrator একটা নতুন backend তৈরি করেছে
  2. nginx path: কেউ config ফাইল এডিট করেএকটা external script/অ্যাডমিন `upstream` block-এ নতুন `server` লাইন যোগ করে
  3. nginx path: `nginx -s reload` কল হয়নতুন worker process পুরনো config replace করে — এই কল না হওয়া পর্যন্ত nginx নতুন backend সম্পর্কে অজ্ঞ থেকে যায়
  4. Envoy path: control plane নতুন endpoint জানেorchestrator থেকে সরাসরি বা watch করে, control plane (যেমন Istio-র istiod) নতুন Pod IP নথিভুক্ত করে
  5. Envoy path: EDS push, বিদ্যমান gRPC স্ট্রিমেকোনো reload নেই, কোনো নতুন connection নেই — Envoy-র চলমান cluster manager সরাসরি নতুন endpoint নিয়ে নেয়

দুইটা path-এর মধ্যে মূল পার্থক্য: nginx-এর ক্ষেত্রে একটা discrete, human/script-triggered ধাপ (file edit + reload signal) আবশ্যক; Envoy-র ক্ষেত্রে backend তালিকা সবসময় already streaming, নতুন কিছু ট্রিগার করার দরকার নেই। এই পার্থক্যটাই ঠিক container-orchestration প্রসঙ্গে গুরুত্বপূর্ণ — যেখানে হাজার হাজার Pod প্রতি মিনিটে বহুবার তৈরি/ধ্বংস হতে পারে, সেখানে “প্রতিটা বদলে একটা reload” স্কেল করে না, কিন্তু “প্রতিটা বদল একটা push message” স্কেল করে।

ছবিতে

Reload বনাম streaming — কনফিগারেশন কীভাবে প্রোপাগেট হয়

nginx: reload-ভিত্তিক প্রোপাগেশন            Envoy: xDS streaming প্রোপাগেশন
──────────────────────────────            ──────────────────────────

  backend change ঘটল (t=0)                   backend change ঘটল (t=0)
        │                                           │
        ▼                                           ▼
  [কনফিগ ফাইলে অপেক্ষা করছে]                  control plane (istiod-জাতীয়)
   কেউ script/cron চালানো                      সাথে সাথে জানে (watch/API)
   পর্যন্ত nginx এই backend                          │
   সম্পর্কে অজ্ঞ                                     ▼
        │                                     EDS push — persistent gRPC
        ▼                                     stream-এ, মিলিসেকেন্ডে
  ফাইল এডিট + `nginx -s reload`                      │
        │                                           ▼
        ▼                                     Envoy cluster manager
  নতুন worker, নতুন config লোড                 আপডেট — নতুন reload নেই
        │                                           │
        ▼                                           ▼
  এই মুহূর্ত থেকে nginx সঠিক ভিউ পায়            Envoy প্রায় সাথে সাথে সঠিক ভিউ পায়

  স্টেল উইন্ডো = reload trigger পর্যন্ত         স্টেল উইন্ডো ≈ এক gRPC round-trip
  বিলম্ব (batch/periodic হলে অনেক বড়)          (সাধারণত মিলিসেকেন্ড স্কেল)
তুলনাএকই কাজ — backend তালিকা আপডেট করা — দুইটা সম্পূর্ণ ভিন্ন প্রোপাগেশন পথ। বাম দিকে discrete, batch reload; ডানে continuous, per-event push।

পরের section-এর build এই diagram-এর “staleness window”-টাকেই একটা সংখ্যায় নামিয়ে আনবে — batch-reload মডেলে ব্যাচের ব্যবধান যত বড়, গড় staleness তত বড়; streaming মডেলে সেটা প্রায় শূন্যের কাছাকাছি থাকে।

নিজে চালিয়ে দেখুন

EXPERIMENT

config-propagation-sim.py — reload বনাম streaming staleness পরিমাপ

যেকোনো OS (Python 3, শুধু stdlib)· ১৫ মিনিট

এই স্ক্রিপ্ট সত্যিই চালানো হয়েছে। এটা একটা simulated timeline তৈরি করে — backend registration/deregistration ইভেন্ট এলোমেলো (কিন্তু deterministic seed দিয়ে reproducible) সময়ে ঘটছে — তারপর দুইটা মডেল দিয়ে সেই ইভেন্টগুলো “প্রয়োগ” করে দেখে প্রতিটা মডেলে proxy-র ভিউ কতক্ষণ পুরনো (stale) থাকে।

# config_propagation_sim.py -- reload-based vs streaming xDS-style
# config propagation, staleness measured in simulated seconds.
import random

random.seed(42)

SIM_DURATION_S = 300          # 5-minute simulated window
RELOAD_INTERVAL_S = 15        # nginx-style: config only re-read every 15s
EVENT_RATE_PER_MIN = 8        # backend churn: registrations+deregistrations/min

# ---- generate churn events across the timeline ----
n_events = int(SIM_DURATION_S / 60 * EVENT_RATE_PER_MIN)
event_times = sorted(random.uniform(0, SIM_DURATION_S) for _ in range(n_events))

# ---- model 1: reload-based (batch-apply on a fixed interval) ----
# A change that happens at time t is only VISIBLE to the proxy at the next
# reload boundary -- the ceiling of t to the nearest RELOAD_INTERVAL_S.
reload_staleness = []
for t in event_times:
    next_reload = (int(t // RELOAD_INTERVAL_S) + 1) * RELOAD_INTERVAL_S
    reload_staleness.append(next_reload - t)

# ---- model 2: streaming xDS-style (applied ~immediately, small fixed
# propagation delay for the gRPC push + control-plane processing) ----
STREAMING_PROPAGATION_DELAY_S = 0.05   # 50ms: control-plane push latency
streaming_staleness = [STREAMING_PROPAGATION_DELAY_S for _ in event_times]

avg_reload = sum(reload_staleness) / len(reload_staleness)
avg_streaming = sum(streaming_staleness) / len(streaming_staleness)
max_reload = max(reload_staleness)

print(f"simulated window          : {SIM_DURATION_S}s")
print(f"backend churn events      : {n_events} (rate: {EVENT_RATE_PER_MIN}/min)")
print(f"reload interval (nginx)   : {RELOAD_INTERVAL_S}s")
print()
print("=== reload-based (batch-apply) ===")
print(f"average staleness         : {avg_reload:.3f} s")
print(f"worst-case staleness       : {max_reload:.3f} s")
print()
print("=== streaming xDS-style (immediate push) ===")
print(f"average staleness         : {avg_streaming:.3f} s")
print()
improvement = avg_reload / avg_streaming
print(f"streaming is ~{improvement:,.0f}x lower average staleness than reload-based")

প্রকৃত চালিয়ে পাওয়া output (deterministic — seed(42) দিয়ে, হুবহু এটাই আসবে):

simulated window          : 300s
backend churn events      : 40 (rate: 8/min)
reload interval (nginx)   : 15s

=== reload-based (batch-apply) ===
average staleness         : 6.942 s
worst-case staleness       : 14.388 s

=== streaming xDS-style (immediate push) ===
average staleness         : 0.050 s

streaming is ~139x lower average staleness than reload-based

ফলাফলটা ঠিক তত্ত্ব-অনুযায়ী: reload-based মডেলে যেকোনো ইভেন্ট গড়ে reload-interval-এর অর্ধেক সময়ের কাছাকাছি স্টেল থাকে (একদম শুরুতে ঘটলে প্রায় পুরো ১৫ সেকেন্ড, ঠিক reload-এর আগ মুহূর্তে ঘটলে প্রায় শূন্য — গড়ে interval / 2-এর কাছাকাছি)। Streaming মডেলে staleness শুধু propagation-delay-তে (৫০ মিলিসেকেন্ড, বাস্তব gRPC push latency-র একটা যুক্তিসঙ্গত অনুমান) সীমাবদ্ধ, event rate যা-ই হোক না কেন। নিজে বাড়ান: RELOAD_INTERVAL_S বাড়িয়ে দেখুন (অনেক production nginx setup-এ reload প্রায়ই মিনিট-স্কেলে, সেকেন্ডে না) — অনুপাতটা কীভাবে বিস্ফোরিত হয়।

এটা কী প্রমাণ করে

ব্যাচ-ভিত্তিক periodic reload একটা প্রেডিক্টেবল, নন-জিরো গড় staleness তৈরি করে যা reload-interval-এর সাথে সরাসরি সমানুপাতিক; ইভেন্ট-ভিত্তিক streaming আপডেট সেই staleness প্রায় সম্পূর্ণ দূর করে দেয়, backend churn rate যা-ই হোক না কেন।

নিজে বানান

BUILD IT

xDS-lite — একটা মিনিমাল streaming-push cluster-manager simulate করুন

Python · ●●●○○
  1. একটা ClusterManager ক্লাস বানান যেটা backend endpoint-এর একটা সেট (set) ধরে রাখে
  2. দুইটা মেথড দিন: apply_reload(new_list) — পুরো তালিকা একসাথে replace করে (nginx-model), আর apply_stream_event(event) — একটা একক add/remove ইভেন্ট সাথে সাথে প্রয়োগ করে (Envoy xDS-model)
  3. উভয় মডেলে একই ইভেন্ট-সিকোয়েন্স চালিয়ে প্রতিটা মুহূর্তে "proxy-র বর্তমান ভিউ" আসল অবস্থার সাথে মেলে কিনা তুলনা করুন
  4. দেখান — reload-model-এ দুই reload-এর মাঝখানে ভুল ভিউ কতক্ষণ থাকে, stream-model-এ প্রায় কখনোই না

আগের experiment শুধু কতক্ষণ স্টেল থাকে তা মাপল — এই build সেটাকে এক ধাপ এগিয়ে দেখায় কী ভুল সেই স্টেল উইন্ডোতে proxy-র ভিউতে থাকে।

# xds_lite.py -- a minimal simulation of the two propagation models as
# stateful cluster managers, so we can inspect the WRONG view a reload-model
# proxy holds during its stale window (not just the duration).

class ClusterManager:
    def __init__(self, model):
        assert model in ("reload", "stream")
        self.model = model
        self.view = set()            # what the proxy currently believes
        self._pending_reload = set() # only used by reload model

    def apply_stream_event(self, kind, backend):
        """Envoy xDS-style: apply a single event to the LIVE view immediately."""
        if self.model != "stream":
            raise RuntimeError("only valid for stream model")
        if kind == "add":
            self.view.add(backend)
        else:
            self.view.discard(backend)

    def stage_for_reload(self, kind, backend):
        """nginx-style: event only updates a pending set, not the live view."""
        if self.model != "reload":
            raise RuntimeError("only valid for reload model")
        if kind == "add":
            self._pending_reload.add(backend)
        else:
            self._pending_reload.discard(backend)

    def reload(self):
        """The discrete moment a reload happens: live view jumps to match."""
        self.view = set(self._pending_reload)


# ---- drive both models through the same event sequence ----
events = [
    ("add", "10.0.1.11"),
    ("add", "10.0.1.12"),
    ("reload_boundary", None),   # nginx reloads here
    ("add", "10.0.1.13"),        # new pod scheduled
    ("remove", "10.0.1.11"),     # old pod evicted
    ("reload_boundary", None),   # nginx reloads again
]

stream_mgr = ClusterManager("stream")
reload_mgr = ClusterManager("reload")
reload_mgr._pending_reload = set()

print("step | actual truth (what SHOULD be routable) | stream-proxy view | reload-proxy view")
truth = set()
for kind, backend in events:
    if kind == "reload_boundary":
        reload_mgr.reload()
        print(f"{'RELOAD':6}|" + " " * 42 + f"|{'':20}| view -> {sorted(reload_mgr.view)}")
        continue
    if kind == "add":
        truth.add(backend)
    else:
        truth.discard(backend)
    stream_mgr.apply_stream_event(kind, backend)
    reload_mgr.stage_for_reload(kind, backend)
    print(f"{kind:6}| truth={sorted(truth)!s:30}| stream={sorted(stream_mgr.view)!s:8}| reload={sorted(reload_mgr.view)!s:8} <- STALE" if sorted(reload_mgr.view) != sorted(truth) else f"{kind:6}| truth={sorted(truth)!s:30}| stream={sorted(stream_mgr.view)!s:8}| reload={sorted(reload_mgr.view)!s:8}")

প্রকৃত চালিয়ে পাওয়া output:

step | actual truth (what SHOULD be routable) | stream-proxy view | reload-proxy view
add   | truth=['10.0.1.11']                 | stream=['10.0.1.11']| reload=[]       <- STALE
add   | truth=['10.0.1.11', '10.0.1.12']    | stream=['10.0.1.11', '10.0.1.12']| reload=[]       <- STALE
RELOAD|                                          |                    | view -> ['10.0.1.11', '10.0.1.12']
add   | truth=['10.0.1.11', '10.0.1.12', '10.0.1.13']| stream=['10.0.1.11', '10.0.1.12', '10.0.1.13']| reload=['10.0.1.11', '10.0.1.12'] <- STALE
remove| truth=['10.0.1.12', '10.0.1.13']    | stream=['10.0.1.12', '10.0.1.13']| reload=['10.0.1.11', '10.0.1.12'] <- STALE
RELOAD|                                          |                    | view -> ['10.0.1.12', '10.0.1.13']

লক্ষ্য করার মতো: stream-কলাম কখনো truth-এর থেকে ভিন্ন হয় না — প্রতিটা ইভেন্ট সাথে সাথে প্রয়োগ হচ্ছে। reload-কলাম দুইবার স্পষ্টভাবে ভুল — একবার নতুন backend (10.0.1.13) থাকা সত্ত্বেও proxy সেটা জানে না (ট্রাফিক হারানো সুযোগ), আরেকবার সরানো backend (10.0.1.11) এখনও proxy-র তালিকায়, মানে proxy তার দিকে ট্রাফিক পাঠাতেই পারে — একটা dead backend-এ request যাওয়ার ঝুঁকি, যা connection error/timeout হিসেবে ক্লায়েন্টের কাছে ফিরে আসতে পারে। এই দ্বিতীয় সমস্যাটাই বাস্তবে reload-based মডেলের সবচেয়ে বিরক্তিকর পরিণতি — শুধু “ধীর” না, সক্রিয়ভাবে ভুল।

বাস্তব সিস্টেমে

Sidecar pattern — প্রতিটা service-এর নিজস্ব ছোট Envoy

এতদূর reverse proxy-কে একটা কেন্দ্রীয়, একক পয়েন্ট হিসেবে দেখা হলো — cluster-এর প্রান্তে বসে বাইরের ট্রাফিক ভেতরে ঢোকাচ্ছে (একে প্রায়ই API gateway বা ingress বলা হয়)। কিন্তু আধুনিক microservices architecture-এ একটা গভীরতর সমস্যা আছে: service-থেকে-service ট্রাফিকও (যেমন users_service নিজে orders_service-কে কল করছে) একই সমস্যাগুলো ভোগে — retry logic, timeout, circuit-breaking, mutual TLS, request-level metrics। যদি প্রতিটা টিম তাদের নিজস্ব ভাষায় (Python, Go, Java, Node.js) এই লজিক আলাদাভাবে লেখে, ফলাফল হয় অসামঞ্জস্যপূর্ণ, বাগ-প্রবণ, রক্ষণাবেক্ষণ-দুঃস্বপ্ন।

সমাধান: virtualization-cloud/kvm-qemu আর namespace-লেসনগুলোর ভিত্তিতে গড়ে ওঠা multi-container Pod মেকানিজম ব্যবহার করে (Pod-এর সব container একই network namespace শেয়ার করে, তাই localhost-এ একে অপরকে দেখতে পায়) প্রতিটা service instance-এর পাশে, একই Pod-এ, একটা ছোট Envoy instance বসিয়ে দেওয়া — এটাই sidecar pattern। সেই service-এর ভেতরে-বাইরে যাওয়া সব ট্রাফিক — outgoing call, incoming request — সেই local sidecar দিয়ে রুট হয়, application কোড নিজে কখনো সরাসরি নেটওয়ার্ক টাচ করে না।

একটা service-to-service কল — sidecar দিয়ে
  1. Service A-র application কোড: `POST http://orders-service/orders`কোড শুধু localhost-এ কল করে — নিজের ভাষার সাধারণ HTTP client, retry/mTLS সম্পর্কে কিছু জানে না
  2. Service A-র sidecar Envoy (একই Pod, localhost)Request আটকায়: mTLS certificate যোগ করে, retry-policy প্রয়োগের জন্য প্রস্তুত থাকে, metrics রেকর্ড করে
  3. নেটওয়ার্কে বেরিয়ে যায় — mTLS-encryptedService B-র node/Pod-এ পৌঁছায়
  4. Service B-র sidecar Envoy (একই Pod হিসেবে, ওপাশে)mTLS ভেরিফাই করে, decrypt করে, লোকাল app-এ ফরওয়ার্ড করে, incoming metrics রেকর্ড করে
  5. Service B-র application কোডএকটা সাধারণ, plaintext, localhost HTTP request পায় — mTLS/retry-র কোনো সচেতনতাই লাগে না

এই কেন গুরুত্বপূর্ণ, নির্ভুলভাবে বললে: retry, timeout, circuit-breaking, mutual-TLS, আর observability (পরবর্তী observability লেসনের বিষয়ের একটা আভাস) — এই প্রতিটা এখন একটাই জায়গায়, একটাই ভাষায় (Envoy-র C++), একবার বাস্তবায়িত হয় — প্রতিটা application টিমকে নিজের ভাষায় আলাদাভাবে লিখতে হয় না। একটা Python service আর একটা Go service ঠিক একই mTLS নিয়ম, একই retry-budget পায়, কোনো কোড-শেয়ারিং ছাড়াই — কারণ নিয়মগুলো sidecar-এ থাকে, application-এ না।

যখন cluster-জুড়ে প্রতিটা service-এর নিজস্ব sidecar থাকে, সবগুলো sidecar একসাথে একটা জাল (mesh) তৈরি করে — এই সব ছোট Envoy-কে একসাথে বলা হয় data plane। কিন্তু কে এই সব sidecar-কে বলে দেয় কোন routing rule, কোন retry policy, কোন mTLS certificate ব্যবহার করতে? একটা কেন্দ্রীয় control plane — Istio (Envoy-র উপরে বানানো) এই ভূমিকার সবচেয়ে পরিচিত উদাহরণ। Istio-র control plane (istiod) ঠিক আগের section-এর xDS API ব্যবহার করে প্রতিটা sidecar-কে কেন্দ্রীয়ভাবে কনফিগার করে — একটা routing rule বদলালে, সেটা সব sidecar-এ streaming push হয়ে যায়, কোনো Pod restart ছাড়াই। Service mesh পুরো এই architecture-এর নাম: data plane (সব sidecar একসাথে) + control plane (কেন্দ্রীয় কনফিগারেশন-বিতরণকারী)।

যে ভুলগুলো সবাই করে

“Forward proxy আর reverse proxy আসলে একই জিনিস, শুধু নাম আলাদা”

সম্পূর্ণ ভুল — এরা বিপরীত ভূমিকা পালন করে। Forward proxy client-দের প্রতিনিধিত্ব করে বাইরের জগতের কাছে (client সচেতনভাবে proxy configure করে); reverse proxy server-দের প্রতিনিধিত্ব করে ক্লায়েন্টদের কাছে (client কিছুই জানে না, মনে করে সরাসরি সার্ভারের সাথে কথা বলছে)। কে কাকে লুকাচ্ছে তা সম্পূর্ণ উল্টো — একই নেটওয়ার্ক-অবস্থান (মাঝখানে বসা) থেকে দুইটা সম্পূর্ণ ভিন্ন নিরাপত্তা ও architecture-এর অর্থ বেরোয়।

“Envoy শুধু nginx-এর দ্রুততর একটা ভার্সন”

গতি এখানে মূল পার্থক্য না। মৌলিক পার্থক্যটা কনফিগারেশন-মডেলে — nginx ঐতিহ্যগতভাবে একটা static ফাইল পড়ে, backend পরিবর্তনে reload লাগে; Envoy শুরু থেকেই dynamic configuration-এর জন্য ডিজাইন করা, streaming xDS API দিয়ে backend পরিবর্তন reload ছাড়াই সরাসরি প্রয়োগ হয়। এই architectural পার্থক্যই Envoy-কে dynamic container/Kubernetes পরিবেশের জন্য বিশেষভাবে উপযোগী করে তোলে, শুধু raw performance-এর কারণে না।

“Sidecar pattern মানে প্রতিটা service নিজের নেটওয়ার্ক-লজিক নিজে লেখে, শুধু একটা আলাদা প্রসেসে”

উল্টো — sidecar pattern-এর পুরো লক্ষ্য হলো application টিমকে retry/timeout/mTLS/circuit-breaking লজিক লেখাই না লাগানো। প্রতিটা service একই sidecar সফটওয়্যার (Envoy) চালায়, একই কেন্দ্রীয় control plane থেকে কনফিগার হয় — লজিক একবারই, একটাই জায়গায় বাস্তবায়িত, ভাষা-নিরপেক্ষভাবে সব service-এ পুনঃব্যবহৃত হয়। Application কোড শুধু plain, local HTTP call করে — নেটওয়ার্ক-জটিলতা সম্পূর্ণ sidecar-এ delegated।

“Service mesh মানে শুধু বেশি network hop যোগ করা — বিশুদ্ধ overhead, কোনো উপকার নেই”

প্রতিটা কলে সত্যিই দুইটা অতিরিক্ত লোকাল hop যোগ হয় (client sidecar + server sidecar) — এটা সত্যি ও একটা বাস্তব latency-খরচ (সাধারণত লোকাল Pod-এর ভেতরে হওয়ায় সাব-মিলিসেকেন্ড স্কেলে)। কিন্তু বিনিময়ে যা পাওয়া যায় তা শুধু “সুবিধা” না — uniform mTLS (সব internal ট্রাফিক এনক্রিপ্টেড, ম্যানুয়াল সার্টিফিকেট ম্যানেজমেন্ট ছাড়াই), কেন্দ্রীয়ভাবে নিয়ন্ত্রণযোগ্য retry/circuit-breaking (একটা downstream service ধীর হলে পুরো cluster-এ cascading failure ঠেকানো), আর প্রতিটা service-hop-এর uniform observability — এই প্রতিটাই আলাদাভাবে প্রতিটা ভাষায় বাস্তবায়ন করলে বাস্তবে অনেক বেশি ব্যয়বহুল ও অসামঞ্জস্যপূর্ণ হতো।

বুঝেছেন কি না দেখুন

1এক বাক্যে — forward proxy কাকে প্রতিনিধিত্ব করে, আর reverse proxy কাকে?স্মরণ
Forward proxy client-দের প্রতিনিধিত্ব করে বাইরের জগতের কাছে (client-দের লুকায়/রক্ষা করে); reverse proxy server-দের প্রতিনিধিত্ব করে client-দের কাছে (backend infrastructure লুকায়, client মনে করে সরাসরি সার্ভারের সাথে কথা বলছে)।
2nginx config-এ upstream block আর proxy_pass directive-এর কাজ ঠিক কী, আলাদা করে বলুন।স্মরণ
upstream block একটা নাম-করা backend server-এর pool সংজ্ঞায়িত করে (একাধিক server entry, load-balancing strategy সহ)। proxy_pass (একটা location block-এর ভেতরে) সেই matched path-এর request-কে নির্দিষ্ট একটা upstream pool-এ ফরওয়ার্ড করার নির্দেশ দেয় — routing সিদ্ধান্ত ও backend pool সংজ্ঞা দুইটা আলাদা directive-এ ভাগ করা।
3কেন nginx-এর ঐতিহ্যগত reload-based মডেল একটা highly dynamic Kubernetes পরিবেশে বিশেষভাবে সমস্যাজনক, শুধু “কিছুটা ধীর” না?যুক্তি
Kubernetes Pod ক্রমাগত তৈরি/ধ্বংস হয়, প্রতিবার নতুন IP নিয়ে — মানে backend তালিকা প্রতি সেকেন্ডে বহুবার বদলাতে পারে। reload-based মডেলে প্রতিটা বদল একটা discrete, ব্যয়বহুল ধাপ (ফাইল এডিট + reload সিগন্যাল) দাবি করে — শুধু ধীর না, বরং দুইটা সক্রিয় ভুল তৈরি করে: নতুন backend চালু থাকা সত্ত্বেও ট্রাফিক না পাওয়া, আর সরানো backend-এ এখনও ট্রাফিক পাঠানো (dead backend-এ request, connection error)।
4আপনার team-এর একটা Python service আরেকটা Go service-কে কল করছে। দুইটার মধ্যে mutual TLS আর automatic retry-with-backoff দরকার। Sidecar pattern ছাড়া এটা বাস্তবায়ন করতে কী লাগত, আর sidecar pattern কীভাবে এটা বদলে দেয়?প্রয়োগ
Sidecar ছাড়া: Python আর Go দুই দলকে আলাদাভাবে নিজেদের ভাষায় mTLS certificate-হ্যান্ডলিং আর retry/backoff লজিক বাস্তবায়ন করতে হতো — দুইটা কোডবেস, দুইটা সম্ভাব্য বাগ-উৎস, ধারাবাহিকতার কোনো গ্যারান্টি নেই। Sidecar pattern-এ: উভয় service-এর পাশে একটা করে Envoy sidecar বসে, mTLS ও retry policy কেন্দ্রীয় control plane থেকে কনফিগার হয়ে দুই sidecar-এই একই আচরণ প্রয়োগ করে — application কোড শুধু plain, local HTTP কল করে, নেটওয়ার্ক-জটিলতা সম্পূর্ণ sidecar-এ delegated।
5একটা reload-based reverse proxy-তে reload interval ৩০ সেকেন্ড থেকে বাড়িয়ে ২ মিনিট করা হলো (অপারেশনাল খরচ কমাতে)। backend churn rate অপরিবর্তিত থাকলে, গড় staleness-এ কী প্রভাব পড়বে?প্রয়োগ
এই লেসনের simulation অনুযায়ী গড় staleness প্রায় interval / 2-এর সমানুপাতিক — তাই interval ৩০ সেকেন্ড থেকে ১২০ সেকেন্ডে (৪ গুণ) বাড়ালে গড় staleness-ও প্রায় ৪ গুণ বেড়ে যাবে (প্রায় ১৫ সেকেন্ড থেকে প্রায় ৬০ সেকেন্ড)। এই সময়ে backend তালিকার ভুল ভিউ (নতুন backend না পাওয়া, dead backend-এ ট্রাফিক পাঠানো) দ্বিগুণেরও বেশি দীর্ঘ সময় স্থায়ী হবে — অপারেশনাল খরচ বাঁচাতে গিয়ে সরাসরি নির্ভরযোগ্যতা বিসর্জন দেওয়া হচ্ছে।
6আপনি একটা নতুন ৫০-microservice প্ল্যাটফর্ম ডিজাইন করছেন, সব team-ই আলাদা ভাষা ব্যবহার করে (Python, Go, Java, Rust), আর requirement হলো: সব internal ট্রাফিক mTLS-encrypted, প্রতিটা service-hop-এ uniform retry/circuit-breaking, আর একটা কেন্দ্রীয় ড্যাশবোর্ডে সব service-এর latency দেখা যাবে। এই তিনটা requirement মেটাতে আপনি কী architecture বেছে নেবেন, আর কেন প্রতিটা team-কে আলাদাভাবে এসব লাইব্রেরি বসাতে বলা যথেষ্ট না?ডিজাইন
সঠিক উত্তর একটা service mesh — প্রতিটা service-এর পাশে Envoy sidecar (Istio-জাতীয় control plane দিয়ে কেন্দ্রীয়ভাবে কনফিগার করা)। প্রতিটা team-কে আলাদাভাবে mTLS/retry/metrics লাইব্রেরি বসাতে বলা যথেষ্ট না কারণ: (১) চারটা ভিন্ন ভাষায় চারটা ভিন্ন লাইব্রেরি বাস্তবায়ন করতে হবে, প্রতিটাতে আলাদা বাগ-সম্ভাবনা ও আচরণগত অসামঞ্জস্য; (২) একটা নীতি বদলাতে (যেমন retry-budget) চারটা কোডবেস আলাদাভাবে আপডেট, টেস্ট, ডিপ্লয় করতে হবে; (৩) কোনো team ভুলে লাইব্রেরি বাদ দিলে বা পুরনো ভার্সন রেখে দিলে পুরো নিরাপত্তা/নির্ভরযোগ্যতা মডেল ভেঙে পড়ে। Sidecar mesh-এ এই তিনটা সমস্যাই একবারে সমাধান হয় — নীতি একটাই জায়গায় (control plane), বাস্তবায়ন একটাই জায়গায় (Envoy), application কোড সম্পূর্ণ ভাষা-নিরপেক্ষভাবে অজ্ঞাত থাকে।

এরপর কী

আজকের লেসন এই module-এর একটা বড় অধ্যায় বন্ধ করল। virtualization-cloud/containers-vs-vms থেকে শুরু করে namespace/cgroup, pod-এর multi-container মেকানিজম, load balancing, আর আজকের reverse proxy/service mesh পর্যন্ত — আমরা এখন একটা সম্পূর্ণ ছবি বানিয়ে ফেলেছি কীভাবে একটা container-এর fleet নেটওয়ার্কড, ডিসকভারড, লোড-ব্যালান্সড, আর প্রক্সিড হয়।

এরপরের কয়েকটা লেসন compute অর্কেস্ট্রেশন থেকে জুম আউট করে অন্যান্য মূল cloud primitive-এর দিকে যাবে — শুরু হবে virtualization-cloud/object-storage-internals দিয়ে: cloud provider-রা কীভাবে প্রতিটা অ্যাপ্লিকেশনের অন্য অর্ধেক — ডেটা — বিশাল স্কেলে সংরক্ষণ করে, ফাইল হিসেবে না, বরং “object” হিসেবে।

আরও পড়ুন

  • NGINX Reverse Proxy — official documentation · `upstream` block, `proxy_pass`, header-forwarding directive-এর আনুষ্ঠানিক রেফারেন্স — এই লেসনের nginx config অংশের প্রাথমিক উৎস
  • Envoy Proxy — Architecture Overview · Envoy-র core architecture — listener, filter chain, cluster manager, hot restart ছাড়াই dynamic configuration-এর নকশা
  • Envoy Proxy — xDS API (Discovery Service) Protocol · gRPC streaming discovery protocol-এর স্পেসিফিকেশন — CDS/EDS/RDS/LDS কীভাবে control plane থেকে live আপডেট push করে reload ছাড়াই
  • Istio — Service Mesh Documentation · Envoy-ভিত্তিক sidecar-mesh-এর production বাস্তবায়ন — control plane কীভাবে সব sidecar-কে কেন্দ্রীয়ভাবে কনফিগার করে তার রেফারেন্স