سرویس رجیستری در معماری میکروسرویس‌

Registry service in microservice architecture

در دنیای نرم‌افزار های سازمانی مدرن، معماری میکروسرویس (Microservice Architecture) به یک استاندارد تبدیل شده است. در این رویکرد، یک نرم‌افزار بزرگ به مجموعه‌ای از سرویس‌های کوچک‌ تر، مستقل و متمرکز بر یک وظیفه خاص تقسیم می‌شود. هر سرویس می‌تواند به‌صورت جداگانه توسعه یابد، مستقر شود و مقیاس‌بندی گردد.

با این حال، این استقلال مزایا و چالش‌ های جدیدی نیز به همراه دارد. در یک سیستم سنتی (Monolithic)، تمامی اجزا در یک پروسه قرار میپیرد و ارتباط بین آن‌ ها از طریق فراخوانی‌ های درون‌ حافظه‌ای (In-memory calls) انجام می‌شود. اما در میکروسرویس‌ ها، ارتباطات باید از طریق شبکه (معمولاً HTTP/REST یا gRPC) صورت گیرد.

مشکل اصلی اینجاست: چگونه یک سرویس A می‌تواند آدرس دقیق (IP و پورت) یک سرویس B را پیدا کند، در حالی که سرویس B ممکن است لحظه‌ای قبل از کار افتاده باشد یا برای تحمل بار بیشتر، چندین نسخه از آن (مثلاً 5 نسخه) به صورت همزمان در حال اجرا باشند؟

پاسخ این معمای ارتباطی، سرویس رجیستری (Service Registry) است.

تعریف ساده

سرویس رجیستری را می‌توان به عنوان دفتر تلفن هوشمند یا لیست دایر کتوری زنده کل سیستم میکروسرویس در نظر گرفت.

هر سرویس (مثلاً سرویس مدیریت سفارشات، سرویس سبد خرید، سرویس احراز هویت) به محض اینکه آماده‌ی دریافت درخواست‌ها می‌شود، یک "کارت ویزیت" شامل نام خود، آدرس محلی (IP و پورت) و وضعیت سلامتش را به این دفتر تلفن مرکزی ارسال کرده و ثبت می‌کند. این فرآیند به عنوان ثبت سرویس (Service Registration) شناخته می‌شود.

هنگامی که سرویسی نیاز به برقراری ارتباط با سرویس دیگری دارد (مثلاً سرویس پرداخت نیاز به تأیید هویت از سرویس احراز هویت دارد)، به جای اینکه آدرس سرویس احراز هویت را مستقیماً در کد خود داشته باشد، از دفتر تلفن (رجیستری) می‌پرسد: "کدام نسخه‌های فعال از سرویس احراز هویت در حال حاضر در دسترس هستند؟" رجیستری لیست بروز شده آدرس‌ ها را به سرویس درخواست‌ کننده می‌دهد.

اهمیت سرویس رجیستری

اهمیت اصلی رجیستری در مواجهه با مفاهیم کلیدی معماری مدرن نهفته است:

  1. پویایی (Dynamism): در محیط‌ های ابری (Cloud) و کانتینری (مانند Docker و Kubernetes)، ماشین‌ های مجازی و کانتینرها مرتباً ایجاد، حذف یا جابه‌ جا می‌شوند. این تغییرات باعث می‌شوند آدرس‌های IP و پورت‌ها دائم در حال تغییر باشند. رجیستری تضمین می‌کند که این تغییرات فوراً منعکس شده و آدرس‌های قدیمی حذف شوند.
  2. مقیاس‌ پذیری (Scalability): اگر تقاضا برای یک سرویس افزایش یابد، می‌توان چندین نمونه (Instance) از آن سرویس را به صورت موازی اجرا کرد. رجیستری این آدرس‌ های جدید را فوراً ثبت می‌کند و درخواست‌ های ورودی می‌توانند بین آن‌ها توزیع شوند.
  3. کاهش وابستگی‌ها سخت‌ کد شده (Decoupling): بدون رجیستری، هر سرویسی باید آدرس سرویس‌های دیگر را در فایل پیکربندی خود (Config File) داشته باشد. هر بار که یک سرویس جدید مستقر شود یا آدرس آن تغییر کند، باید پیکربندی تمامی سرویس‌های وابسته نیز به‌روزرسانی شود. این امر مدیریت سیستم را به یک کابوس تبدیل می‌کند. رجیستری این وابستگی را از بین می‌برد.

فواید کلیدی برای کسب‌وکار و تیم توسعه

  • استقرار مستقل و سریع (Independent Deployment): تیم‌ها می‌توانند بدون هماهنگی مستقیم با تیم‌ های دیگر برای بروزرسانی آدرس‌ها، سرویس‌ های خود را مستقر کنند.
  • صرفه‌ جویی در زمان توسعه: توسعه‌ دهندگان دیگر نیازی به صرف وقت برای مدیریت آدرس‌ های شبکه در سطوح مختلف محیطی (توسعه، تست، عملیات) ندارند.
  • افزایش تحمل خطا (Fault Tolerance): اگر یک نمونه از سرویس از کار بیفتد، رجیستری آن را حذف می‌کند و ترافیک به طور خودکار به نمونه‌ های سالم هدایت می‌شود، که منجر به بهبود تجربه کاربری می‌شود.

نمونه واقعی

در شرکت‌ هایی با زیرساخت‌ های بسیار بزرگ مانند دیجی‌کالا، احتمالاً صدها سرویس مختلف (از احراز هویت و مدیریت کاتالوگ گرفته تا پرداخت و رهگیری مرسوله) در حال کار هستند. تصور کنید اگر قرار باشد تغییر در آدرس سرور پرداخت، نیازمند بازنویسی کد یا تغییر فایل‌ های پیکربندی در 50 سرویس دیگر باشد. این کار در محیطی که هزاران بار در روز استقرار (Deployment) انجام می‌شود، غیرممکن است. سرویس رجیستری این هماهنگی پیچیده را در پشت صحنه به صورت خودکار و شفاف انجام می‌دهد.

 

 بخش فنی برای برنامه‌نویس

برای درک عمیق‌تر، نحوه کار سرویس رجیستری در دو الگوی اصلی کشف سرویس (Service Discovery) بررسی می‌شود.

معماری کلی رجیستری

سیستم Service Registry از دو جزء اصلی تشکیل شده است که به صورت فعال با یکدیگر در تعامل هستند:

  1. Registry Server (سرور مرکزی): این سرور نقش پایگاه داده موقت (Key-Value Store) توزیع‌شده را ایفا می‌کند. وظیفه اصلی آن دریافت درخواست‌های Register (ثبت)، Deregister (حذف) و Lookup (جستجو) از سرویس‌های کلاینت است. این سرور باید به شدت مقاوم در برابر خطا و با دسترسی بالا (Highly Available) باشد.
  2. Registry Client (کلاینت سرویس): این کتابخانه یا ماژولی است که در هر سرویس میکروسرویس جاسازی می‌شود. این کلاینت مسئول برقراری ارتباط با Registry Server است.
    • ثبت (Registration): هنگام بالا آمدن سرویس، کلاینت داده‌های سرویس (نام، آدرس، پورت، متادیتای اضافی) را برای ثبت ارسال می‌کند.
    • پایش سلامت (Heartbeat/Health Check): برای اطمینان از زنده بودن سرویس، کلاینت به صورت دوره‌ای (مثلاً هر 30 ثانیه) یک سیگنال به رجیستری ارسال می‌کند که نشان می‌دهد سرویس هنوز در حال کار است.

الگو های کشف سرویس (Service Discovery Patterns)

نحوه استفاده سایر سرویس‌ها از اطلاعات رجیستری به دو روش اصلی دسته‌بندی می‌شود:

1. Client-Side Discovery (کشف از سمت کلاینت)

در این روش، سرویس مصرف‌کننده مستقیماً با رجیستری ارتباط برقرار می‌کند.

جریان عملیاتی:

  1. سرویس A (مصرف‌کننده) می‌خواهد با سرویس B ارتباط برقرار کند.
  2. سرویس A از Registry Server آدرس‌های در دسترس سرویس B را درخواست می‌کند.
  3. Registry Server لیست آدرس‌ها را برمی‌گرداند.
  4. سرویس A از یک الگوریتم توزیع بار داخلی (مانند Round Robin) استفاده کرده و یکی از آدرس‌های دریافتی را انتخاب می‌کند.
  5. سرویس A درخواست را مستقیماً به آن آدرس می‌فرستد.
  • مزایا: سبک بودن زیرساخت شبکه، کنترل کامل بر نحوه توزیع بار توسط خود سرویس.
  • معایب: نیاز به تعبیه منطق کشف سرویس در هر کلاینت (نیاز به یک کتابخانه مشترک در تمام زبان‌ها و فریم‌ ورک‌های استفاده شده).
  • نمونه پیاده‌ سازی: Netflix Eureka (که اغلب با Spring Cloud استفاده می‌شود).

2. Server-Side Discovery (کشف از سمت سرور)

در این روش، سرویس‌های کلاینت هرگز مستقیماً با رجیستری صحبت نمی‌کنند. در عوض، تمام ترافیک ابتدا به یک Load Balancer هوشمند (که اغلب به عنوان Reverse Proxy یا API Gateway عمل می‌کند) هدایت می‌شود.

جریان عملیاتی:

  1. سرویس A می‌خواهد با سرویس B ارتباط برقرار کند، اما فقط آدرس Load Balancer را می‌شناسد.
  2. سرویس A درخواست را به Load Balancer ارسال می‌کند (مثلاً /api/users).
  3. Load Balancer با رجیستری ارتباط برقرار کرده و می‌پرسد: "کدام نسخه‌های فعال سرویس B را سراغ دارم؟"
  4. Load Balancer آدرس سالم را انتخاب کرده و درخواست را به آن ارسال می‌کند.
  • مزایا: سرویس کلاینت ساده می‌شود و فقط نیاز به دانستن آدرس Load Balancer دارد. این روش برای سرویس‌هایی که از لحاظ زبان برنامه‌نویسی متفاوت هستند، انعطاف بیشتری ایجاد می‌کند.
  • معایب: اضافه شدن یک لایه پیچیدگی (Load Balancer) که باید خودش قوی و مقاوم باشد.
  • نمونه پیاده‌سازی: استفاده از Kubernetes Services (که به صورت داخلی از DNS و Kube-Proxy برای این کار استفاده می‌کند) یا AWS ALB/NLB.

گزینه‌های معروف برای Service Registry

انتخاب ابزار مناسب برای رجیستری به محیط عملیاتی و اکوسیستم مورد استفاده بستگی دارد:

 

 Eureka NetflixClient-Side Discovery بسیار سبک، اغلب با جاوا و Spring Cloud همراه است. تمرکز بر سادگی و سرعت.

Consul HashiCorpClient/Server-Side یک پلتفرم قوی‌ تر که علاوه بر رجیستری، قابلیت‌های توزیع کلید-مقدار و Mesh Service را نیز ارائه می‌دهد.

etcdCNCF ، ذخیره سازی کلید-مقدار بسیار پایدار، مقیاس‌پذیر و دقیق. معمولاً به عنوان هسته اصلی برای مدیریت پیکربندی در Kubernetes استفاده می‌شود.

ZooKeeperApache کمی قدیمی‌تر، اما بسیار قابل اعتماد برای مدیریت وضعیت‌ های هماهنگ‌ سازی پیچیده (معمولاً توسط Kafka استفاده می‌شود).

طراحی ساده با Node.js و NestJS (نمونه مفهومی)

فرض کنید یک سرویس به نام UserService داریم که باید خود را در یک رجیستری مرکزی (که از طریق HTTP قابل دسترسی است) ثبت کند.

// registry-client.service.ts (درون پروژه UserService)
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { map } from 'rxjs/operators';
import { lastValueFrom } from 'rxjs';

const REGISTRY_URL = 'http://registry-server:8761/eureka/apps/';
const SERVICE_NAME = 'USERSERVICE';

@Injectable()
export class RegistryClientService implements OnModuleInit, OnModuleDestroy {
  private instanceId: string;
  private heartbeatInterval: NodeJS.Timeout;

  constructor(private readonly http: HttpService) {
    // شناسه منحصر به فرد این نمونه از سرویس
    this.instanceId = `${SERVICE_NAME}:${process.env.HOST}:${process.env.PORT}:${Date.now()}`;
  }

  async onModuleInit() {
    await this.registerService();
    // شروع پایش سلامت (Heartbeat) هر 30 ثانیه
    this.heartbeatInterval = setInterval(this.renewLease.bind(this), 30000);
  }

  async onModuleDestroy() {
    clearInterval(this.heartbeatInterval);
    await this.deregisterService();
  }

  private async registerService() {
    const registrationPayload = {
      instance: {
        hostName: process.env.HOST || '127.0.0.1',
        app: SERVICE_NAME,
        vipAddress: SERVICE_NAME,
        instanceId: this.instanceId,
        port: {
          '@enabled': true,
          $: parseInt(process.env.PORT || '3000', 10),
        },
        // ... سایر متادیتاها
      },
    };

    await lastValueFrom(
        this.http.post(`${REGISTRY_URL}${SERVICE_NAME}`, registrationPayload).pipe(
            map(res => res.data)
        )
    );
    console.log(`Service ${SERVICE_NAME} registered successfully.`);
  }

  private async renewLease() {
    // فرآیند تمدید اجاره نامه (Heartbeat)
    await lastValueFrom(
        this.http.put(`${REGISTRY_URL}${SERVICE_NAME}/${this.instanceId}`).pipe(
            map(res => res.data)
        )
    );
    // console.log('Lease renewed.');
  }

  private async deregisterService() {
    await lastValueFrom(
        this.http.delete(`${REGISTRY_URL}${SERVICE_NAME}/${this.instanceId}`).pipe(
            map(res => res.data)
        )
    );
    console.log(`Service ${SERVICE_NAME} deregistered.`);
  }
}

پایش سلامت سرویس‌ها (Health Checks)

مکانیسم اصلی اطمینان از کارکرد صحیح رجیستری، پایش سلامت (Health Checking) است. دو نوع اصلی بررسی سلامت وجود دارد:

  1. Client-Side Health Check (Self-Reporting): همانند نمونه بالا، کلاینت خودش دوره‌ای به رجیستری اعلام می‌کند که زنده است (Heartbeat/Lease Renewal). این ساده‌ترین روش است.
  2. Server-Side Health Check (Registry Query): رجیستری به صورت فعال و دوره‌ای یک درخواست HTTP (مثلاً به مسیر /health) به سرویس‌ ها ارسال می‌کند. اگر سرویس در بازه زمانی مشخصی پاسخ ندهد، رجیستری آن را مرده تلقی کرده و از لیست فعال حذف می‌کند. این روش نسبت به روش اول مطمئن‌ تر است زیرا وابستگی کمتری به صحت عملکرد خود سرویس کلاینت دارد.

ارتباط سرویس رجیستری با Load Balancer

همانطور که اشاره شد، رجیستری نقش مرکزی برای Load Balancer ها دارد. اگر از روش Server-Side Discovery استفاده کنیم، Load Balancer (یا API Gateway) باید قابلیت اتصال به رجیستری را داشته باشد.

فرض کنید یک Load Balancer پیشرفته داریم. هنگامی که درخواستی برای سرویس OrderService دریافت می‌کند، آن Load Balancer می‌داند که باید از رجیستری آدرس‌ها را استعلام کند. اگر تعداد 5 نسخه از OrderService فعال باشند، Load Balancer می‌تواند از فرمول‌های پیشرفته‌تری برای توزیع بار استفاده کند، مانند:

[ \text{انتخاب نمونه} = f(\text{پینگ راندوم، سشن کاربر، آخرین زمان استفاده}) ]

این سطح از انعطاف در زمان اجرا، بدون رجیستری غیرممکن است.

نکات پایانی و جمع‌بندی فنی

  1. مقاومت در برابر نقص (Resiliency): از آنجایی که رجیستری یک نقطه شکست بالقوه (Single Point of Failure - SPOF) است، پیاده‌سازی آن باید توزیع‌ شده و بدون حالت (Stateless) باشد. معمولاً رجیستری‌ها به صورت خوشه‌ای (Clustered) با استفاده از الگوریتم‌هایی مانند Raft (که etcd و Consul از آن استفاده می‌کنند) اجرا می‌شوند تا در صورت از کار افتادن یک گره، گره‌های دیگر کار را ادامه دهند.
  2. Kubernetes و Service Discovery: در محیط مدرن مبتنی بر Kubernetes، نیاز به نصب ابزارهای ثالث مانند Eureka کاهش یافته است. Kubernetes به صورت بومی یک Service Registry مبتنی بر DNS Service و Kube-Proxy ارائه می‌دهد. هر سرویس در K8s یک نام DNS پایدار دریافت می‌کند و Kube-Proxy ترافیک را به پادهای (Pods) فعال هدایت می‌کند.

 جمع‌بندی نهایی

سرویس رجیستری ابزاری حیاتی و بنیادین در هر سیستم مبتنی بر میکروسرویس است که به دنبال پویایی، مقیاس‌ پذیری و انعطاف‌ پذیری است. این مفهوم به سرویس‌ ها اجازه می‌دهد تا در هر لحظه از وضعیت شبکه آگاه باشند، بدون اینکه نیاز به حفظ اطلاعات پیچیده و متغیر شبکه در پیکربندی‌ های ثابت داشته باشند. رجیستری، ستون فقرات کشف سرویس (Service Discovery) است و تضمین می‌کند که زیر ساخت می‌تواند بدون توقف به توسعه و تغییر ادامه دهد.