پست

بررسی عمیق ZooKeeper برای مصاحبه‌های طراحی سیستم

مبانی ZooKeeper شامل znode، ensemble و watch، کاربرد آن در مدیریت پیکربندی، service discovery، انتخاب leader و قفل توزیع‌شده، نحوه کار داخلی با ZAB، محدودیت‌ها و جایگزین‌های مدرن

بررسی عمیق ZooKeeper برای مصاحبه‌های طراحی سیستم

ZooKeeper

در این فصل با نحوه استفاده از ZooKeeper برای حل تعداد زیادی از مسائل طراحی سیستم آشنا می‌شویم.

هماهنگ کردن سیستم‌های توزیع‌شده سخت است. با اینکه قدرت پردازش و تکنیک‌های مقیاس‌دهی چشمگیر پیشرفت کرده‌اند، مسئله اساسی همچنان هست: چطور ده‌ها یا صدها سرور را هماهنگ کنید تا یکپارچه با هم کار کنند؟ وقتی این ماشین‌ها باید leader انتخاب کنند، پیکربندی یکدست نگه دارند و خرابی را بلادرنگ تشخیص دهند، دقیقاً با مسائلی روبه‌رو می‌شوید که ZooKeeper برای حل آن‌ها طراحی شده است.

ZooKeeper که در سال ۲۰۰۸ منتشر شد، قدیمی شده و جایگزین‌های متعددی پیدا کرده است. با این حال، به‌خصوص در اکوسیستم Apache، همچنان نقش مرکزی دارد.

با وجود قدمتش، فهمیدن ZooKeeper مفاهیم ضروری سیستم‌های توزیع‌شده را یاد می‌دهد که حتی اگر هرگز مستقیم از آن استفاده نکنید هم کاربرد دارند. با یاد گرفتن این‌که ZooKeeper هماهنگی را با چند مؤلفه ساده (فضای نام سلسله‌مراتبی، گره‌های داده و watch) انجام می‌دهد، دید خوبی از حل مسائل جهانی مثل اجماع (consensus)، انتخاب leader و مدیریت پیکربندی می‌گیرید.

ببینیم ZooKeeper چطور کار می‌کند، کی از آن استفاده کنیم و در چشم‌انداز امروز سیستم‌های توزیع‌شده چطور تکامل یافته است.

یک مثال انگیزشی

برای فهمیدن اینکه چرا هماهنگی سخت است، با یک مثال شروع کنیم. فرض کنید اپلیکیشن چت می‌سازید.

اول، اپلیکیشن چت شما روی یک سرور اجرا می‌شود. زندگی ساده است. وقتی آلیس به باب پیام می‌فرستد، هر دو کاربر به همان یک سرور وصل‌اند. سرور دقیقاً می‌داند پیام را کجا تحویل دهد. همه چیز در حافظه است، تأخیر کم و هماهنگی لازم نیست.

اما اپلیکیشن شما محبوب می‌شود. یک سرور دیگر نمی‌تواند بار را تحمل کند، پس سرور دوم را اضافه می‌کنید. حالا مشکل دارید: وقتی آلیس (وصل به سرور ۱) به باب (وصل به سرور ۲) پیام می‌فرستد، سرور ۱ باید بداند باب کجاست تا پیام را تحویل دهد. سرور ۱ چطور بفهمد باب به کدام سرور وصل است؟

ممکن است فکر کنید «فقط از پایگاه داده استفاده می‌کنم!». جدولی که کاربران را به سرورهایشان نگاشت می‌کند. وقتی کاربر وصل می‌شود، پایگاه داده را به‌روز کن. وقتی پیامی باید مسیریابی شود، پایگاه داده را چک کن. ساده است، درست؟

اما این مشکل‌های جدید می‌سازد. پایگاه داده به single point of failure تبدیل می‌شود. اگر از کار بیفتد، کل سیستم چت شما می‌خوابد. تأخیر هم اضافه می‌کند، چون حالا هر پیام قبل از مسیریابی به جستجوی پایگاه داده نیاز دارد. وقتی سیستم شما به میلیون‌ها کاربر برسد، این پایگاه داده زیر کوئری‌ها غرق می‌شود و گلوگاه می‌سازد.

ممکن است با کش بهینه‌اش کنید، اما حالا مشکل سازگاری کش دارید. اگر کاربر از سرور ۲ قطع شود و به سرور ۳ وصل شود، اما کش سرور ۱ هنوز فکر کند او روی سرور ۲ است؟ پیام‌ها گم می‌شوند.

پس فکر می‌کنید «سرورها را مستقیم از تغییرات باخبر می‌کنم!». وقتی کاربر به سرور ۳ وصل می‌شود، آن را برای همه سرورهای دیگر broadcast می‌کند. اما با رسیدن سیستم به ده‌ها یا صدها سرور، این broadcastها ترافیک شبکه زیادی می‌سازند. هر سرور باید با هر سرور دیگر ارتباط بگیرد. این n² اتصال است که خوب مقیاس نمی‌گیرد.

بعد مشکل خرابی سرور می‌آید. اگر سرور ۲ از کار بیفتد چه؟ نمی‌تواند به کسی بگوید که خراب شده است. کاربران وصل به آن قطع شده‌اند، اما سرورهای دیگر این را نمی‌دانند. به‌دنبال فرستادن پیام به سرور مرده می‌روند.

پس heartbeat پیاده می‌کنید: سرورها به‌صورت دوره‌ای چک می‌کنند سرورهای دیگر زنده‌اند یا نه. اما این مسئله جدیدی می‌سازد: «پارتیشن شبکه» معروف. اگر سرور ۲ زنده باشد اما مشکل شبکه‌ای بین آن و سرور ۱ وجود داشته باشد چه؟ سرور ۱ فکر می‌کند سرور ۲ مرده، اما این‌طور نیست. بعضی سرورها ممکن است سرور ۲ را زنده ببینند و بعضی دیگر مرده. حالا دید ناسازگاری از وضعیت سیستم دارید.

با اینکه اپلیکیشن چت می‌تواند از الگوهای Pub/Sub برای جلوگیری از بعضی مسائل هماهنگی استفاده کند، ما روی سناریوهایی تمرکز می‌کنیم که هماهنگی مستقیم برای نشان دادن قابلیت‌های ZooKeeper ضروری است. بسیاری از سیستم‌های واقعی به نوعی از مسائل هماهنگی که اینجا درباره‌شان صحبت می‌کنیم نیاز دارند.

هر کدام از این مسائل، یعنی service discovery، اشتراک پیکربندی، تشخیص شکست، انتخاب leader و اجماع توزیع‌شده، چالش‌های بنیادین سیستم‌های توزیع‌شده‌اند. درست حل کردنشان فوق‌العاده سخت است و الگوریتم‌هایی می‌خواهد که بتوانند با خرابی جزئی و تأخیر شبکه کنار بیایند و هم‌زمان سازگاری را حفظ کنند. این دقیقاً مجموعه مسائلی است که ZooKeeper برای حل آن‌ها ساخته شده است.

ZooKeeper همه مسائلی را که بالا گفتیم حل می‌کند. منبع حقیقت (source of truth) پایدار و قابل‌اعتمادی فراهم می‌کند که همه سرورها می‌توانند به آن اعتماد کنند. وقتی سرورها آنلاین می‌شوند، خودشان را در ZooKeeper ثبت می‌کنند. وقتی کاربران به سرورها وصل می‌شوند، این نگاشت در ZooKeeper ذخیره می‌شود. ZooKeeper بعد سرورهای علاقه‌مند را از تغییرات باخبر می‌کند و خرابی‌ها را از طریق ephemeral nodeهایش خودکار مدیریت می‌کند. بدون این‌که خودتان این الگوریتم‌های توزیع‌شده پیچیده را بسازید، service discovery قابل‌اعتماد، مدیریت پیکربندی و انتخاب leader می‌گیرید.

ببینیم ZooKeeper چطور با این چالش‌ها کنار می‌آید.

مبانی ZooKeeper

ZooKeeper در هسته‌اش مجموعه‌ای ساده اما قدرتمند از مؤلفه‌های پایه می‌دهد که به حل مسائل پیچیده هماهنگی توزیع‌شده کمک می‌کند. برای فهمیدن طرز کارش باید سه مفهوم کلیدی را بررسی کنیم: مدل داده مبتنی بر ZNode، نقش‌های سرور در ensemble ای از ZooKeeper، و مکانیزم watch که اعلان بلادرنگ را ممکن می‌کند.

ZooKeeper را مثل یک سیستم فایل همگام‌شده از metadata ببینید. هر گرهای که متصل است باید دید یکسانی از این داده داشته باشد. همین دید یکدست بین همه سرورهای شرکت‌کننده است که ZooKeeper را برای کارهای هماهنگی بسیار قدرتمند می‌کند.

مدل داده، ZNode

ZooKeeper داده‌هایش را در فضای نام سلسله‌مراتبی سازمان می‌دهد که خیلی شبیه سیستم فایل یا درخت است. گره‌های این درخت ZNode نامیده می‌شوند. اما برخلاف پوشه‌های سنتی در سیستم فایل، ZNodeها می‌توانند داده (معمولاً مقدار کم، کمتر از ۱ مگابایت) ذخیره کنند و metadata مرتبط دارند.

هر ZNode با یک مسیر، شبیه مسیرهای سیستم فایل، شناسایی می‌شود. مثلاً /app1/config ممکن است پیکربندی اپلیکیشنی به نام «app1» را نشان دهد. نکته کلیدی این است که ZNodeها برای ذخیره داده هماهنگی طراحی شده‌اند، نه داده حجیم مثل تصویر یا سند بزرگ. یعنی داده ذخیره‌شده در ZNodeها معمولاً کوچک است، در حالی که تعداد ZNodeها معمولاً زیاد است (در حد هزاران).

ZNodeها سه نوع اصلی دارند که هرکدام هدف مشخصی در اپلیکیشن چت ما دارند.

ZNodeهای دائمی (Persistent): این گره‌ها تا وقتی صریحاً حذف نشوند وجود دارند. در اپلیکیشن چتمان از گره‌های دائمی برای داده‌های پیکربندی مثل حداکثر اندازه پیام یا پارامترهای محدودسازی نرخ استفاده می‌کنیم.

1
2
# Store the maximum message size in bytes
create /chat-app/config/max_message_size "1024"

ZNodeهای زودگذر (Ephemeral): وقتی نشستی (session) که آن‌ها را ساخته تمام شود (چه با قطع ارتباط کلاینت و چه با timeout) خودکار حذف می‌شوند. این برای ردیابی این‌که کدام سرورها زنده‌اند و کدام کاربران آنلاین‌اند عالی است.

1
2
3
4
5
6
7
# Server 2 registers itself when it starts up
create -e /chat-app/servers/server2 "192.168.1.102:8080"

# When Bob connects to Server 2, it creates:
create -e /chat-app/users/bob "server2"

# If Server 2 crashes, both nodes automatically disappear!

ZNodeهای متوالی (Sequential): این‌ها شمارنده‌ای را به‌صورت خودکار به نام خودشان اضافه می‌کنند که خودکار افزایش می‌یابد. در اپلیکیشن چتمان می‌توانیم از این برای ترتیب‌دهی پیام یا پیاده‌سازی قفل توزیع‌شده استفاده کنیم.

1
2
3
# Alice sends a message to the global chat
create -s /chat-app/channels/global/msg- "Alice: Hello everyone!"
# Creates /chat-app/channels/global/msg-0000000001

برای اپلیکیشن چت ما، می‌توانید ساختاری شبیه این تصور کنید:

1
2
3
4
5
6
7
8
9
10
11
/chat-app
  /servers           # Directory of available servers
    /server1         # Ephemeral node containing "192.168.1.101:8080" the location of this server
    /server2         # Ephemeral node containing "192.168.1.102:8080" the location of this server
    /server3         # Ephemeral node containing "192.168.1.103:8080" the location of this server
  /users             # Directory of online users
    /alice           # Ephemeral node containing "server1" the server that alice is connected to
    /bob             # Ephemeral node containing "server2" the server that bob is connected to
  /config            # Application configuration
    /max_users       # Persistent node containing "10000" the maximum number of users allowed
    /message_rate    # Persistent node containing "100/sec" the maximum number of messages per second allowed

این مسئله هماهنگی ما را زیبا حل می‌کند. وقتی آلیس (روی سرور ۱) می‌خواهد به باب پیام بفرستد، سرور ۱ فقط /chat-app/users/bob را جستجو می‌کند تا بفهمد باب روی سرور ۲ است و بعد پیام را مناسب مسیریابی می‌کند. اگر باب قطع شود و به سرور ۳ وصل شود، ZNode خودکار به‌روز می‌شود و مطمئن می‌شود پیام‌ها همیشه درست مسیریابی شوند.

در واقعیت، اپلیکیشن چت محبوب میلیون‌ها کاربر آنلاین خواهد داشت و نمی‌خواهیم برای هر کاربر یک ZNode داشته باشیم. در عوض می‌توانیم از Consistent Hashing استفاده کنیم، جایی که سرورها در ZooKeeper ثبت می‌شوند و کاربران با تابع hash بر اساس شناسه کاربرشان به سرورها نگاشت می‌شوند. این رویکرد لازم می‌کند ZooKeeper فقط سرورها را ردیابی کند، نه میلیون‌ها کاربر تکی. این کار آن را خیلی مقیاس‌پذیرتر می‌کند و هم‌زمان با ephemeral nodeهای سرور تشخیص سریع خرابی را فراهم می‌کند.

نقش سرور و ensemble

ZooKeeper برای اجرا روی یک سرور طراحی نشده است، چون single point of failure می‌سازد. در عوض روی گروهی از سرورها به نام ensemble اجرا می‌شود. استقرار production معمولاً ۳، ۵ یا ۷ سرور دارد (عدد فرد به تصمیم‌گیری اکثریتی کمک می‌کند).

در این ensemble سرورها نقش‌های مختلفی می‌گیرند.

  • Leader: یک سرور به‌عنوان leader انتخاب می‌شود و مسئول پردازش همه درخواست‌های به‌روزرسانی است. وقتی سرور ۲ اتصال کاربر جدیدی را در ZooKeeper ثبت می‌کند، این درخواست نوشتن به leader می‌رود.
  • Follower: بقیه سرورها از دستورهای leader پیروی می‌کنند و به رسیدگی به درخواست‌های خواندن کمک می‌کنند. وقتی سرور ۱ باید بفهمد باب کجا وصل است، می‌تواند این اطلاعات را از هر سرور ZooKeeper در ensemble بخواند.

این طراحی توزیع‌شده مشکل single point of failure را که هنگام استفاده از پایگاه داده برای نگاشت کاربر به سرور داشتیم حل می‌کند. اگر یکی از سرورهای ZooKeeper از کار بیفتد، تا وقتی اکثریت (quorum) سرورها در دسترس باشند، ensemble به کار ادامه می‌دهد. مثلاً با ensemble ۳ سروری ما، ZooKeeper می‌تواند خرابی ۱ سرور را تحمل کند.

وقتی سرورهای چت ما به ZooKeeper وصل می‌شوند، به همه سرورهای ensemble وصل می‌شوند:

1
2
3
4
// Chat Server 1 connecting to ZooKeeper
ZooKeeper zk = new ZooKeeper("zk1:2181,zk2:2181,zk3:2181",
                             3000, /* session timeout */
                             watcher /* callback */);

این طراحی ensemble تضمین می‌کند داده هماهنگی حیاتی ما، یعنی این‌که کاربران به کدام سرورها وصل‌اند، حتی وقتی سرورهای تکی ZooKeeper از کار می‌افتند، بسیار در دسترس و بادوام می‌ماند.

Watch، فهمیدن اینکه چیزی تغییر کرده

یکی از قدرتمندترین ویژگی‌های ZooKeeper مکانیزم watch آن است که مسئله اعلان در اپلیکیشن چت ما را زیبا حل می‌کند. watchها به سرورها اجازه می‌دهند وقتی ZNode تغییر می‌کند باخبر شوند و نیاز به polling مداوم یا ارتباط پیچیده سرور به سرور را حذف می‌کنند.

بدون watch، هر بار که پیام جدیدی فرستاده می‌شود، سرور باید برای پیدا کردن محل گیرنده از ZooKeeper کوئری بگیرد. این تأخیر قابل‌توجهی می‌سازد و فشار زیادی روی cluster ZooKeeper می‌گذارد. در مقیاس بزرگ، با هزاران پیام در ثانیه، ZooKeeper خیلی زود به‌جای راه‌حل، گلوگاه می‌شود.

watchها در اپلیکیشن چت ما این‌طور کمک می‌کنند.

وقتی سرور ۱ راه می‌افتد، یک watch روی دایرکتوری /chat-app/users می‌گذارد:

1
2
// Server 1 watching for user changes
zk.getChildren("/chat-app/users", true, null);

وقتی باب از سرور ۲ قطع می‌شود و به سرور ۳ وصل می‌شود، سرور ۳ ZNode باب را به‌روز می‌کند:

1
2
// Server 3 updates Bob's location
zk.setData("/chat-app/users/bob", "server3".getBytes(), -1);

ZooKeeper سرور ۱ را از طریق callback مربوط به watcher خودش به‌طور خودکار از این تغییر باخبر می‌کند:

1
2
3
4
5
6
7
8
9
10
11
// Server 1's watcher callback
public void process(WatchedEvent event) {
    if (event.getType() == EventType.NodeDataChanged &&
        event.getPath().equals("/chat-app/users/bob")) {
        // Get updated server for Bob
        byte[] data = zk.getData("/chat-app/users/bob", true, null);
        String bobsServer = new String(data);
        // Update routing table: Bob is now on Server 3
        routingTable.put("bob", bobsServer);
    }
}

این مکانیزم watch، سیستم broadcast پیچیده‌ای را که تصور کرده بودیم حذف می‌کند. به‌جای این‌که هر سرور مجبور باشد با هر سرور دیگر ارتباط بگیرد (n² اتصال)، همه سرورها فقط به ZooKeeper وصل می‌شوند. وقتی کاربر بین سرورها جابه‌جا می‌شود، فقط سرورهایی که به آن کاربر اهمیت می‌دهند اعلان می‌گیرند.

و خرابی سرور چه؟ اگر سرور ۲ از کار بیفتد، نشست آن با ZooKeeper تمام می‌شود و همه ephemeral nodeهایش (از جمله گره‌های کاربران وصل به آن) خودکار حذف می‌شوند. سرورهای دیگری که این گره‌ها را watch می‌کنند اعلان می‌گیرند و می‌توانند اقدام لازم را انجام دهند، مثل علامت زدن آن کاربران به‌صورت آفلاین.

نکته مهم این است که watchها الگوی کلیدی‌ای را ممکن می‌کنند که در آن سرورها یک کش محلی از state ZooKeeper نگه می‌دارند. وقتی سرور می‌خواهد بداند باب کجاست، نیازی به کوئری از ZooKeeper ندارد و فقط می‌تواند کش محلی خودش را نگاه کند. اگر چیزی تغییر کند، سرور باخبر می‌شود و می‌تواند کش محلی خودش را به‌روز کند.

به همین دلیل ZooKeeper را «سرویس هماهنگی» توصیف می‌کنند، نه «پایگاه داده». ZooKeeper برای باخبر کردن سیستم‌ها از تغییرات طراحی شده است، نه برای مدیریت ترافیک خواندن با حجم بالا.

با ترکیب این سه مفهوم اساسی، یعنی ZNode برای ذخیره داده، ensemble قابل‌اعتماد از سرورها و watch برای اعلان تغییر، ZooKeeper پایه قدرتمندی می‌سازد که همه چالش‌های هماهنگی توزیع‌شده‌ای را که در اپلیکیشن چت با آن روبه‌رو شدیم زیبا حل می‌کند.

قابلیت‌های کلیدی

ما به اپلیکیشن چت نگاه کرده‌ایم، اما ZooKeeper به همین‌جا ختم نمی‌شود. می‌توان از آن برای چهار قابلیت اصلی استفاده کرد: مدیریت پیکربندی، service discovery، انتخاب leader و قفل توزیع‌شده. ببینیم برای هرکدام چطور کار می‌کند.

ZooKeeper برای مدیریت پیکربندی

یکی از رایج‌ترین کاربردهای ZooKeeper ذخیره و توزیع داده پیکربندی در سیستم توزیع‌شده است. این می‌تواند رشته‌های اتصال پایگاه داده، feature flag یا endpointهای سرویس را شامل شود.

در اپلیکیشن چتمان قبلاً دیدیم چطور می‌توانیم مقدارهای پیکربندی ذخیره کنیم:

1
2
/chat-app/config/max_message_size "1024"
/chat-app/config/message_rate "100/sec"

قدرت واقعی از توانایی ZooKeeper برای باخبر کردن همه سرویس‌های علاقه‌مند هنگام تغییر پیکربندی می‌آید. فرض کنید می‌خواهید قابلیت جدیدی را در کل پلتفرم چتتان فعال کنید. با ZooKeeper می‌توانید:

  1. یک ZNode را به‌روز کنید: /chat-app/config/enable_reactions را روی «true» بگذارید.
  2. همه سرورهای چت که این گره را watch می‌کنند اعلان می‌گیرند.
  3. سرورها بدون راه‌اندازی مجدد رفتارشان را به‌روز می‌کنند.

این مدیریت پیکربندی متمرکز قدرتمندی می‌سازد، چون انتشار، نسخه‌گذاری و به‌روزرسانی اتمیک بلادرنگ را ممکن می‌کند.

یک پلتفرم فروشگاه آنلاین ممکن است از ZooKeeper برای پیکربندی این‌طور استفاده کند:

1
2
3
4
5
/ecommerce
  /config
    /pricing_algorithm "dynamic_v2"  # Switch pricing algorithm across all services
    /discount_threshold "50.00"      # Update discount threshold in real-time
    /maintenance_mode "false"        # Toggle maintenance mode off/on

هنگام استفاده از ZooKeeper برای پیکربندی، روی ذخیره مقدارهای پیکربندی پویا تمرکز کنید که ممکن است در زمان اجرا تغییر کنند. پیکربندی ایستا که فقط هنگام deploy تغییر می‌کند اغلب بهتر است در فایل‌ها یا متغیرهای محیطی نگه داشته شود.

بیشتر ارائه‌دهندگان ابر مدرن راه‌حل‌های مدیریت پیکربندی خودشان را دارند. مثلاً AWS دارد AWS Systems Manager Parameter Store و Azure دارد Azure App Configuration. اگر روی یک ارائه‌دهنده ابر کار می‌کنید، به‌جای ZooKeeper از راه‌حل‌های بومی آن‌ها استفاده کنید.

ZooKeeper برای service discovery

Service discovery فرایند شناسایی خودکار سرویس‌ها و endpointها در سیستم توزیع‌شده است. وقتی سرویس‌ها آنلاین می‌شوند خودشان را ثبت می‌کنند. وقتی آفلاین می‌شوند، ثبتشان را لغو می‌کنند (یا ephemeral nodeشان منقضی می‌شود).

این را در عمل دیدیم که سرورهای چتمان خودشان را ثبت کردند:

1
create -e /chat-app/servers/server2 "192.168.1.102:8080"

این الگو مقیاس‌دهی پویا، load balancing و health check را ممکن می‌کند.

معماری میکروسرویس برای یک پلتفرم پخش آنلاین را در نظر بگیرید:

1
2
3
4
5
6
7
8
9
10
/streaming
  /services
    /video-transcoder
      /instance1 "10.0.0.1:8080"
      /instance2 "10.0.0.2:8080"
    /recommendation-engine
      /instance1 "10.0.1.1:9000"
      /instance2 "10.0.1.2:9000"
    /payment-processor
      /instance1 "10.0.2.1:5000"

وقتی سرویس آپلود ویدیوی جدیدی باید encoderهای موجود را پیدا کند، به‌سادگی:

  1. فرزندهای /streaming/services/video-transcoder را می‌خواند.
  2. به یکی از نمونه‌های encoder موجود وصل می‌شود.
  3. watch می‌گذارد تا از هر تغییر در encoderهای موجود باخبر شود.

بسیاری از سیستم‌های مدرن حالا از ابزارهای اختصاصی service discovery مثل Consul یا etcd استفاده می‌کنند، یا به راه‌حل‌هایی که پلتفرم می‌دهد مثل Kubernetes Services یا AWS Service Discovery تکیه می‌کنند. با این حال الگوهایی که این ابزارها پیاده می‌کنند خیلی شبیه رویکرد ZooKeeper است و در بعضی موارد حتی داخلاً از ZooKeeper استفاده می‌کنند.

ZooKeeper برای انتخاب leader

در سیستم‌های توزیع‌شده اغلب لازم است یک گره به‌عنوان «leader» برای عملیات مشخص تعیین شود. مثلاً ممکن است بخواهید فقط یک سرور تراکنش‌های پرداخت را پردازش کند یا زمان‌بندی jobها را انجام دهد. داشتن leader اجازه می‌دهد این عملیات را هماهنگ کنید و مطمئن شوید در هر لحظه فقط یک سرور آن‌ها را انجام می‌دهد.

ZNodeهای متوالی ZooKeeper انتخاب leader را ساده می‌کنند:

  1. هر سرور یک ephemeral sequential node در مسیر مشخصی می‌سازد.
  2. سروری که کمترین شماره دنباله را دارد leader می‌شود.
  3. همه سرورهای دیگر گره بعدی با کمترین شماره دنباله را watch می‌کنند.
  4. اگر leader شکست بخورد، گره‌اش ناپدید می‌شود و سرور بعدی در ترتیب بالا می‌آید.

اگر بخواهیم سروری را برای مدیریت اعلان‌های سراسری انتخاب کنیم، در اپلیکیشن چت ما این‌طور به نظر می‌رسد:

1
2
3
4
5
6
7
8
# Server 1 creates:
create -s -e /chat-app/leader/node- "server1"  # Creates /chat-app/leader/node-0000000001

# Server 2 creates:
create -s -e /chat-app/leader/node- "server2"  # Creates /chat-app/leader/node-0000000002

# Server 3 creates:
create -s -e /chat-app/leader/node- "server3"  # Creates /chat-app/leader/node-0000000003

سرور ۱ حالا leader است، چون کمترین شماره دنباله را دارد. سرور ۲ گره node-0000000001 را watch می‌کند و سرور ۳ گره node-0000000002 را. اگر سرور ۱ از کار بیفتد، گره‌اش ناپدید می‌شود، سرور ۲ باخبر می‌شود و leader جدید می‌شود، و سرور ۳ حالا سرور ۲ را watch می‌کند.

این الگو failover خودکار را ممکن می‌کند و مطمئن می‌شود در هر لحظه فقط یک سرور عملیات حیاتی را انجام می‌دهد.

همین رویکرد در سیستم‌هایی مثل HBase به کار می‌رود، جایی که یک سرور باید تغییرات schema را هماهنگ کند، و در نسخه‌های قدیمی‌تر Kafka، جایی که یک broker کنترل‌کننده (controller) leadership پارتیشن‌ها را مدیریت می‌کند.

ZooKeeper برای قفل توزیع‌شده

قفل توزیع‌شده به چند فرایند روی ماشین‌های مختلف اجازه می‌دهد دسترسی به منابع مشترک را هماهنگ کنند. برای جلوگیری از race condition در سیستم‌های توزیع‌شده مهم‌اند.

ZooKeeper قفل توزیع‌شده را با ZNodeهای ephemeral sequential پیاده می‌کند:

  1. هر کلاینتی که می‌خواهد قفل را بگیرد، یک ephemeral sequential node زیر مسیر قفل می‌سازد.
  2. همه کلاینت‌ها گره‌ها را بر اساس شماره دنباله مرتب می‌کنند.
  3. کلاینتی که کمترین شماره دنباله را دارد قفل را در دست دارد.
  4. هر کلاینت گره بعدی با شماره دنباله پایین‌تر را watch می‌کند.
  5. وقتی کلاینت قفل را آزاد می‌کند (یا crash می‌کند)، ZNode آن حذف می‌شود و کلاینت بعدی باخبر می‌شود.

برای اپلیکیشن چت ما، اعمال محدودیت نرخ روی ارسال پیام را تصور کنید:

1
2
3
4
5
6
7
8
9
10
# Client 1 wants to send a message:
create -s -e /chat-app/locks/send_message- "client1"  # Creates /chat-app/locks/send_message-0000000001

# Client 2 also wants to send:
create -s -e /chat-app/locks/send_message- "client2"  # Creates /chat-app/locks/send_message-0000000002

# Client 1 sends its message, then deletes its node
delete /chat-app/locks/send_message-0000000001

# Client 2 is notified and now holds the lock

این الگوی قفل توزیع‌شده را می‌توان برای مواردی مثل تخصیص منابع، کنترل هم‌زمانی و زمان‌بندی cluster به کار برد.

با اینکه قفل‌های ZooKeeper خوب کار می‌کنند، برای قفل‌گذاری با فرکانس بالا (صدها بار در ثانیه) طراحی نشده‌اند. برای چنین کاربردهایی راه‌حل‌های تخصصی مثل قفل‌های مبتنی بر Redis یا تراکنش‌های پایگاه داده را در نظر بگیرید.

چه زمانی قفل ZooKeeper را به قفل Redis ترجیح دهیم؟

در تحلیل Ticketmaster و Uber، به‌خاطر کارایی و سادگی بهترشان از قفل توزیع‌شده Redis استفاده کردیم. وقتی برای عملیات حیاتی به تضمین سازگاری قوی‌تری نیاز دارید، به‌جای آن قفل ZooKeeper را انتخاب کنید، جایی که درستی بر کارایی غلبه می‌کند (مثل تراکنش‌های مالی). ZooKeeper برای قفل‌های طولانی‌مدت (ساعت‌ها) هم ترجیح داده می‌شود، چون تشخیص خودکار خرابی از طریق ephemeral nodeها خرابی سرور را قوی‌تر از قفل‌های Redis مدیریت می‌کند، که به مدیریت دقیق timeout و مکانیزم heartbeat نیاز دارند.

ZooKeeper چطور کار می‌کند

حالا که فهمیدیم ZooKeeper چه کارهایی می‌تواند بکند، ببینیم همه این‌ها را در پشت صحنه چطور انجام می‌دهد.

واقعیت این است که خیلی بعید است برای مصاحبه‌هایتان به دانستن این بخش نیاز داشته باشید. با این حال، بسیاری از مفاهیم اینجا برای سیستم‌های توزیع‌شده بسیار قابل‌استفاده‌اند و فهمیدنشان، حتی خارج از زمینه ZooKeeper، خوب است. با این حال، اگر خسته‌اید، نگران نباشید و می‌توانید این بخش را رد کنید.

اجماع با ZAB

همان‌طور که قبلاً گفتیم، خود ZooKeeper روی ensemble ای از سرورها ساخته شده است. به عبارت دیگر، چند سرور هستند که همگی نرم‌افزار ZooKeeper را اجرا می‌کنند و با هم هماهنگ می‌شوند. کنایه زیبایی اینجا هست: ZooKeeper به اپلیکیشن‌های دیگر کمک می‌کند مسائل هماهنگی‌شان را حل کنند، اما اول باید چالش‌های هماهنگی خودش را به‌عنوان سیستم توزیع‌شده حل می‌کرد.

در قلب ZooKeeper پروتکل ZooKeeper Atomic Broadcast (ZAB) است. ZAB چیزی است که همه سرورهای ZooKeeper را قادر می‌کند درباره state سیستم به توافق برسند، حتی وقتی سرورها از کار می‌افتند یا مشکل شبکه رخ می‌دهد.

ZAB در دو فاز اصلی کار می‌کند.

  • انتخاب leader: وقتی ensemble ZooKeeper راه می‌افتد یا leader فعلی شکست می‌خورد، سرورها با فرایند رأی‌گیری leader جدید را انتخاب می‌کنند. عامل اصلی در انتخاب leader داشتن به‌روزترین تاریخچه تراکنش است. اگر چند سرور تاریخچه تراکنش معادل داشته باشند، سروری که بالاترین شناسه را دارد ترجیح داده می‌شود. (به شکل گیج‌کننده‌ای، این برعکس الگوی انتخاب leader در سطح اپلیکیشن است که قبلاً دیدیم، که در آن گرهای که کمترین شماره ZNode متوالی را دارد leader می‌شود. نمونه کاملی از این‌که مکانیزم‌های داخلی ZooKeeper چطور می‌توانند با الگوهایی که روی آن ساخته می‌شوند فرق داشته باشند.)
  • پخش اتمیک (Atomic Broadcast): بعد از انتخاب leader، همه درخواست‌های نوشتن به leader فرستاده می‌شود. leader بعد این تغییرات را برای همه followerها پخش می‌کند. نوشتن فقط وقتی موفق حساب می‌شود که اکثریت (quorum) سرورها تغییر را ماندگار کرده باشند.

برای اپلیکیشن چت ما، یعنی وقتی سرور ۲ با ساختن یک ZNode کاربر جدیدی به نام باب را ثبت می‌کند، این اتفاق می‌افتد.

  1. سرور ۲ به leader در ZooKeeper: «یک ZNode در /chat-app/users/bob با مقدار server2 بساز.»
  2. leader در ZooKeeper به همه followerها: «بیایید این ZNode جدید را برای باب اضافه کنیم.»
  3. followerها به leader: «تغییر پذیرفته و ذخیره شد.» (اکثریت لازم است.)
  4. leader به سرور ۲: «ZNode باب با موفقیت ساخته شد.»

ZAB شبیه الگوریتم‌های اجماع Paxos و Raft است که ممکن است نامشان را شنیده باشید. با اینکه ZAB قبل از Raft است و تفاوت‌های پیاده‌سازی دارد، هدف سطح بالا یکی است: رسیدن به اجماع در سیستم توزیع‌شده.

این پروتکل دوفازی تضمین می‌کند حتی اگر بعضی سرورها از کار بیفتند یا پارتیشن شبکه رخ دهد، سرورهای باقی‌مانده دید یکدستی از داده را نگه می‌دارند. نیاز به quorum (اکثریت سرورها) یعنی با ensemble ۵ سروری، ZooKeeper می‌تواند خرابی ۲ سرور را تحمل کند و هنوز درست کار کند.

تضمین‌های سازگاری قوی

ZooKeeper چند تضمین مهم سازگاری ارائه می‌دهد که آن را برای هماهنگی توزیع‌شده قابل‌اعتماد می‌کند.

  • سازگاری ترتیبی (Sequential Consistency): به‌روزرسانی‌های یک کلاینت به ترتیبی که ارسال شده‌اند اعمال می‌شوند. اگر کلاینتی گره A و بعد گره B را به‌روز کند، همه سرورها به‌روزرسانی A را قبل از B می‌بینند.
  • اتمیک بودن (Atomicity): به‌روزرسانی‌ها یا کامل موفق می‌شوند یا کامل شکست می‌خورند. به‌روزرسانی جزئی وجود ندارد.
  • تصویر واحد از سیستم (Single System Image): کلاینت صرف‌نظر از این‌که به کدام سرور وصل است، همان دید را از سیستم خواهد دید (بعد از همگام‌سازی). کلاینت می‌تواند از هر سرور بخواند، اما همه نوشتن‌ها از leader رد می‌شوند.
  • دوام (Durability): وقتی به‌روزرسانی اعمال شد، تا وقتی کلاینتی آن را بازنویسی نکند باقی می‌ماند. حتی اگر سرورها از کار بیفتند و دوباره راه بیفتند، به‌روزرسانی‌ها از بین نمی‌روند.
  • به‌موقع بودن (Timeliness): سیستم تضمین می‌کند دید کلاینت‌ها از سیستم در بازه زمانی محدودی به‌روز می‌شود.

ZooKeeper این تضمین‌ها را از طریق پروتکل ZAB به دست می‌آورد. همه نوشتن‌ها از leader رد می‌شوند، ترتیب کلی از به‌روزرسانی‌ها می‌سازند و فقط وقتی موفق حساب می‌شوند که quorum ای از سرورها آن را در transaction log ماندگار کرده باشد. این پروتکل از فرایند commit دوفازی استفاده می‌کند که در آن leader به‌روزرسانی را پیشنهاد می‌دهد، منتظر تأیید می‌ماند و فقط بعد از آن تغییرات را commit می‌کند. این اتمیک بودن و دوام را حتی هنگام خرابی سرورها تضمین می‌کند.

برای حفظ تصویر واحد از سیستم، ZooKeeper از شماره نسخه برای هر ZNode و پروتکل‌های همگام‌سازی استفاده می‌کند که به followerها اجازه می‌دهد بعد از قطع ارتباط خودشان را به‌روز کنند. با این حال، خواندن از followerها می‌تواند داده کهنه برگرداند، چون در هر خواندن با leader مشورت نمی‌کنند. برای قوی‌ترین سازگاری، کلاینت‌ها می‌توانند قبل از خواندن از عملیات sync استفاده کنند تا مطمئن شوند به‌روزترین داده را می‌گیرند.

با اینکه ZooKeeper سازگاری قوی می‌دهد، برای سناریوهای خواندن و نوشتن با سرعت بالا طراحی نشده است. برای بارهای کاری خواندن‌محور و جایی که اندازه داده نسبتاً کوچک است بهینه شده است.

عملیات خواندن و نوشتن

معماری ZooKeeper مخصوصاً برای بارهای کاری خواندن‌محور بهینه شده است. این انتخاب طراحی روی نحوه مدیریت عملیات خواندن و نوشتن اثر می‌گذارد.

  • عملیات خواندن: هر سرور در ensemble می‌تواند درخواست‌های خواندن را مستقیم از کپی داده درون‌حافظه‌ای خودش جواب بدهد. این throughput بالا و تأخیر کم برای خواندن فراهم می‌کند. به همین دلیل ZooKeeper در بارهای کاری «خواندن‌محور» با نسبت خواندن به نوشتن حدود ۱۰ به ۱ عالی است.
  • عملیات نوشتن: همه درخواست‌های نوشتن باید از leader رد شوند، که با پروتکل ZAB به‌روزرسانی را در ensemble هماهنگ می‌کند. این رویکرد متمرکز ترتیب یکدست را تضمین می‌کند، اما نوشتن را از خواندن گران‌تر می‌کند.

برای اپلیکیشن چت ما، یعنی:

  • وقتی سرور ۱ باید پیدا کند باب به کجا وصل است (عملیات خواندن)، می‌تواند از هر سرور ZooKeeper کوئری بگیرد.
  • وقتی سرور ۳ باید محل باب را به‌روز کند (عملیات نوشتن)، این درخواست به leader می‌رود، که بعد آن را در ensemble همگام می‌کند.

چون خواندن‌ها محلی توسط هر سرور جواب داده می‌شوند، ممکن است کلاینتی داده کهنه بخواند، اگر به followerای وصل شود که هنوز با leader همگام نشده. برای اپلیکیشن‌هایی که قوی‌ترین سازگاری را می‌خواهند، ZooKeeper عملیات sync ارائه می‌دهد که مطمئن می‌شود سرور قبل از انجام خواندن به‌روز است.

نشست‌ها و مدیریت اتصال

ZooKeeper از مفهوم نشست (session) برای مدیریت اتصال کلاینت‌ها و نگه داشتن ephemeral nodeها استفاده می‌کند. نشست‌ها برای تشخیص خرابی کلاینت و پیاده‌سازی ویژگی‌هایی مثل ephemeral node حیاتی‌اند.

  1. برقراری نشست: وقتی کلاینت به ZooKeeper وصل می‌شود، نشستی با timeout قابل‌تنظیم (معمولاً ۱۰ تا ۳۰ ثانیه) می‌سازد.
  2. Heartbeat: کلاینت heartbeat دوره‌ای می‌فرستد تا نشستش را نگه دارد. اگر ZooKeeper در بازه timeout هیچ heartbeatی نگیرد، فرض می‌کند کلاینت شکست خورده است.
  3. بازیابی نشست: اگر کلاینت ارتباطش با سرور ZooKeeper را از دست بدهد، می‌تواند به سرور دیگری وصل شود و نشستش را بازیابی کند، به شرط این‌که قبل از timeout نشست این کار را بکند.
  4. انقضای نشست: اگر نشست منقضی شود، همه ephemeral nodeهایی که آن کلاینت ساخته خودکار حذف می‌شوند و همه watchهایی که آن کلاینت ثبت کرده برداشته می‌شوند.

برای اپلیکیشن چت ما، این مکانیزم نشست وقتی سرورها یا کاربران ناگهان قطع می‌شوند، خودکار پاک‌سازی می‌کند:

1
2
3
4
5
1. Server 2 establishes a session with ZooKeeper when it starts
2. It creates ephemeral nodes for itself and connected users
3. If Server 2 crashes, its session eventually times out
4. ZooKeeper automatically removes all ephemeral nodes owned by Server 2
5. Other servers watching these nodes are notified about users going offline

پیکربندی timeout مناسب نشست بسیار مهم است. اگر خیلی کوتاه باشد، مشکل‌های کوتاه و موقت شبکه ممکن است باعث انقضای غیرضروری نشست شود. اگر خیلی طولانی باشد، سیستم شما برای تشخیص خرابی واقعی کند می‌شود.

معماری ذخیره‌سازی

دوام چه؟ ZooKeeper چطور مطمئن می‌شود داده از بین نمی‌رود، به‌خصوص اگر سروری در ensemble crash کند؟

  • Transaction Log: هر تغییر state (تراکنش) اول در transaction log روی ذخیره‌ساز دائمی نوشته می‌شود. این write-ahead log تضمین می‌کند هیچ به‌روزرسانی تأییدشده‌ای هرگز گم نشود، حتی اگر سرور بلافاصله بعد از تأیید به‌روزرسانی crash کند.
  • Snapshot: ZooKeeper به‌صورت دوره‌ای از پایگاه داده درون‌حافظه‌ای خودش snapshot می‌گیرد تا بازیابی سریع‌تر شود. وقتی سرور دوباره راه‌اندازی می‌شود، آخرین snapshot را بار می‌کند و بعد transaction logها را دوباره پخش می‌کند تا state کامل بازیابی شود.

مستندات ZooKeeper تأکید می‌کند که transaction log از نظر کارایی حیاتی‌ترین بخش ZooKeeper است. ZooKeeper باید تراکنش‌ها را قبل از برگرداندن پاسخ با رسانه ذخیره‌سازی همگام (sync) کند، و داشتن دستگاه اختصاصی برای transaction log کلید کارایی خوب است.

swap شدن حافظه هم می‌تواند به‌شدت به کارایی ZooKeeper ضربه بزند، چون همه عملیات ترتیب‌دار هستند. اگر درخواستی به‌خاطر swap به دیسک برخورد کند، همه درخواست‌های صف‌شده عقب می‌افتند. اندازه مناسب heap برای جلوگیری از swap ضروری است.

مدیریت شکست‌ها

اگر سرور ZooKeeper در ensemble از کار بیفتد چه می‌شود؟

  • خرابی سرور: اگر followerای شکست بخورد، leader تا وقتی quorum ای از سرورها در دسترس باشد به کار ادامه می‌دهد. اگر leader شکست بخورد، انتخاب leader جدید خودکار انجام می‌شود و followerای که بالاترین شناسه را دارد ارتقا می‌یابد.
  • پارتیشن شبکه: اگر شبکه طوری پارتیشن شود که هیچ گروهی اکثریت سرورها را نداشته باشد، ZooKeeper تا حل شدن پارتیشن درخواست نوشتن را پردازش نمی‌کند. این جلوی مشکل «split brain» را می‌گیرد، جایی که بخش‌های مختلف سیستم درباره state فعلی اختلاف نظر دارند.
  • خرابی کلاینت: اگر کلاینتی crash کند، ephemeral nodeهایی که ساخته بود بعد از timeout نشست خودکار حذف می‌شوند. این برای اپلیکیشن چت ما مهم است. اگر سرور ۲ crash کند، همه کاربران وصل به آن خودکار آفلاین علامت می‌خورند، چون ZNodeشان ناپدید می‌شود.
  • مدیریت نشست کلاینت: ZooKeeper نشست‌های کلاینت را ردیابی می‌کند و برای کلاینت‌ها مکانیزم‌هایی فراهم می‌کند که بعد از قطع موقت، بدون از دست دادن state نشست یا watchهایشان، بازیابی و دوباره وصل شوند.

وقتی سرور crash می‌کند، در اپلیکیشن چت ما این اتفاق می‌افتد:

1
2
3
4
5
6
7
8
1. Server 2 crashes
2. Server 2's ZooKeeper session times out (typically after 10-30 seconds)
3. All ephemeral nodes created by Server 2 are automatically deleted, including:
   - /chat-app/servers/server2
   - All /chat-app/users/X where X was connected to Server 2
4. Other servers with watches on these nodes receive notifications
5. They update their routing tables to mark users as offline
6. When a new Server 3 comes back online, it creates a new session and registers itself

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

timeout نشست در ZooKeeper پارامتر پیکربندی بسیار مهمی است. اگر خیلی کم تنظیمش کنید، مشکل‌های موقت شبکه ممکن است خرابی غیرضروری ایجاد کنند. اگر خیلی بالا تنظیمش کنید، سیستم شما برای شناسایی و واکنش به خرابی واقعی زمان بیشتری می‌برد.

ZooKeeper در دنیای مدرن

با اینکه ZooKeeper همچنان نقش کلیدی در هماهنگی توزیع‌شده دارد، چشم‌انداز از زمان معرفی آن در سال ۲۰۰۸ چشمگیر تغییر کرده است. فهمیدن اینکه ZooKeeper با دنیای امروز سیستم‌های توزیع‌شده چطور جور درمی‌آید، به شما کمک می‌کند در مصاحبه انتخاب‌های طراحی بهتری بکنید و از ارائه راه‌حل‌های بالقوه قدیمی پرهیز کنید. ZooKeeper هنوز ابزار آزموده‌شده‌ای است، اما دیگر تنها گزینه نیست.

استفاده فعلی در سیستم‌های توزیع‌شده بزرگ

ZooKeeper هنوز در اکوسیستم Apache جایگاه خاصی دارد. بخش اصلی HBase، Hadoop، SolrCloud، Storm، NiFi و Pulsar است.

سیستم‌های مهم دیگری مثل ClickHouse برای هماهنگی replication، اجرای DDL توزیع‌شده و ذخیره metadata در راه‌اندازی‌های replicated به ZooKeeper نیاز دارند.

مهاجرت اخیر Kafka از ZooKeeper نشان‌دهنده تغییر مهمی در چشم‌انداز سیستم‌های توزیع‌شده است. Kafka بعد از سال‌ها تکیه بر ZooKeeper برای کارهای هماهنگی، حالت metadata مبتنی بر Raft به نام KRaft را معرفی کرد تا پیچیدگی عملیاتی را حذف کند، گلوگاه‌های مقیاس‌پذیری را از بین ببرد و نقاط شکست بالقوه را کم کند. این حرکت نشان‌دهنده گرایش گسترده‌تر به سمت سیستم‌های خودکفا با پروتکل‌های اجماع داخلی، به‌جای سرویس‌های هماهنگی خارجی است.

جایگزین‌هایی برای در نظر گرفتن

خب، اگر ZooKeeper کمتر از قبل برجسته است، چه جایگزین‌هایی هست؟

  • etcd: در محیط‌های cloud-native محبوب است و Kubernetes را تغذیه می‌کند. ذخیره‌گاه کلید-مقدار توزیع‌شده با سازگاری قوی ارائه می‌دهد، API مدرن HTTP/JSON و gRPC دارد و برای مجموعه داده‌های کوچک با حجم خواندن بالا بهینه شده است. برای مدیریت پیکربندی و service discovery ایده‌آل است.
  • Consul از HashiCorp فراتر از هماهنگی پایه، service discovery، health check و ذخیره‌گاه کلید-مقدار می‌دهد. ویژگی برجسته‌اش اتوماسیون زیرساخت شبکه است که load balancerها و فایروال‌ها را بر اساس تغییرات سرویس به‌صورت پویا پیکربندی می‌کند. رویکرد جامع‌تری نسبت به هماهنگی متمرکز ZooKeeper ارائه می‌دهد.
  • راه‌حل‌های ارائه‌دهندگان ابر مثل AWS Parameter Store، Azure App Configuration و Google Cloud Datastore سرویس‌های هماهنگی مدیریت‌شده‌ای ارائه می‌دهند که با پیشنهادهای ابری دیگر یکپارچه می‌شوند. این راه‌حل‌ها حداقل پیکربندی و نگهداری می‌خواهند و آن‌ها را جایگزین‌های عملی می‌کند که ذکرشان در مصاحبه‌های طراحی سیستم ارزش دارد.

با ظهور راه‌حل‌های ابری، بسیاری از ارائه‌دهندگان ابر سرویس‌های هماهنگی داخلی ارائه می‌دهند که نیاز به مدیریت مستقیم سیستم‌های اجماع مثل ZooKeeper را از بین می‌برد. مثلاً در اکوسیستم AWS، سرویس‌هایی مثل AWS ECS هماهنگی کانتینر را از طریق control plane متمرکز انجام می‌دهند، AWS CloudMap service discovery را ساده می‌کند و Amazon MSK عملکرد ZooKeeper را به‌صورت سرویس کاملاً مدیریت‌شده ارائه می‌دهد. این راه‌حل‌های یکپارچه به توسعه‌دهندگان اجازه می‌دهند به‌جای نگهداری زیرساخت هماهنگی پیچیده، روی ساخت اپلیکیشن تمرکز کنند.

محدودیت‌ها

به‌خصوص وقتی در مصاحبه عمیق طراحی زیرساخت هستید، دانستن محدودیت‌های ZooKeeper مهم است. این‌ها مهم‌ترین‌هایند.

  • مشکل hot spot: وقتی کلاینت‌های زیادی یک ZNode را watch می‌کنند (معمولاً در انتخاب leader یا قفل‌ها)، سرورها ممکن است زیر ترافیک اعلان غرق شوند. در مقیاس بزرگ، گره‌های محبوب گلوگاه می‌شوند. فکر کنید میلیون‌ها کاربر هم‌زمان در مثال چت ما آنلاین شوند.
  • محدودیت‌های کارایی: مدل سازگاری ZooKeeper نوشتن را گران می‌کند، چون باید از leader رد شود و تا quorum منتشر شود. مدل ذخیره‌سازی درون‌حافظه‌ای ظرفیت داده را محدود می‌کند. ZNodeها را زیر ۱ مگابایت نگه دارید و مطمئن شوید مجموعه داده‌تان در حافظه جا می‌شود.
  • پیچیدگی عملیاتی: ZooKeeper نیاز به پیکربندی دقیق پارامترهای Java، چیدمان دیسک و مانیتورینگ مداوم timeoutها و اتصال‌ها دارد. همان‌طور که یکی از نگهدارندگانش می‌گوید: «ZooKeeper برای استفاده ساده است اما برای اجرا پیچیده.»

اگر طراحی شما لازم دارد مقدار زیادی داده ذخیره کند، بارهای نوشتن بسیار بالا را مدیریت کند یا پیچیدگی عملیاتی را حداقل کند، شاید بخواهید گزینه‌های دیگر را در نظر بگیرید.

پس کی باید از ZooKeeper استفاده کنید؟

واقعیت این است که به‌ندرت باید اولین چیزی باشد که به سراغش می‌روید، اما جاهایی هست که ZooKeeper در مصاحبه معنا پیدا می‌کند.

مسیریابی هوشمند

در مثال اپلیکیشن چتمان بحث کردیم که ZooKeeper چطور به مسیریابی پیام به سرور مناسب کمک می‌کند. اما یک کاربرد پیچیده‌تر هم هست. حتی اگر ZooKeeper را برای تحویل پیام با سیستم Pub/Sub جایگزین کنیم، با یک مسئله بهینه‌سازی مهم روبه‌رو هستیم: محدود کردن تعداد کانال‌ها یا topicهایی که هر سرور در آن‌ها مشترک است. برای کارایی بهینه، می‌خواهیم کاربران یک اتاق چت روی یک سرور جمع شوند. به عبارت دیگر، همه اتصال‌های WebSocket برای یک اتاق چت مشخص باید (تا جای ممکن) توسط یک سرور مدیریت شوند.

این استراتژی همجواری (colocation) وقتی سیستم‌هایی مثل Facebook Live Comments یا YouTube Live Chat را طراحی می‌کنید اهمیت بیشتری پیدا می‌کند. وقتی میلیون‌ها کاربر در حال تماشای ویدیوی زنده هستند، وصل کردن همه بینندگان آن stream به یک سرور (یا گروه سرور) هزینه ارتباط بین سرورها را حداقل می‌کند. ZooKeeper به‌عنوان نقطه هماهنگی برای API Gateway شما عمل می‌کند و نگاشت این‌که کدام سرورها کدام اتاق‌های چت یا پخش‌های زنده را مدیریت می‌کنند نگه می‌دارد. وقتی کاربر جدیدی وصل می‌شود، gateway شما از ZooKeeper می‌پرسد بر اساس اتاق چت یا شناسه پخش زنده، سرور بهینه برای آن کاربر کدام است.

پیاده‌سازی عملی ممکن است این‌طور باشد: هر سرور خودش را با ظرفیت و فهرست اتاق‌هایی که مدیریت می‌کند در ZooKeeper ثبت می‌کند. API Gateway شما هنگام دریافت اتصال جدید، از ZooKeeper می‌خواهد بر اساس اتاق موردنظر کاربر سرور مناسب را پیدا کند و بعد کاربر را هدایت کند که به آنجا وصل شود. اگر همه سرورهایی که اتاق محبوبی را مدیریت می‌کنند به ظرفیت برسند، ZooKeeper کمک می‌کند سرور جدیدی برای گسترش آن اتاق به شکل هماهنگ انتخاب شود.

این موضوع پیشرفته است و احتمالاً فقط در مصاحبه‌های Staff به بالا یا مصاحبه‌های ارشد متمرکز بر سیستم‌های توزیع‌شده مرتبط است.

مسائل طراحی زیرساخت مشخص

ZooKeeper به‌خصوص در مصاحبه‌های عمیق طراحی زیرساخت مرتبط می‌شود، مثل «یک Message Queue توزیع‌شده طراحی کنید» یا «یک job scheduler توزیع‌شده طراحی کنید». برای این سوال‌ها، ZooKeeper مثل «مغز» اجماع سیستم شما عمل می‌کند، دقیقاً همان‌طور که Kafka در طول تاریخ از آن استفاده می‌کرد. در طراحی Message Queue توزیع‌شده، ZooKeeper توابع حیاتی هماهنگی را مدیریت می‌کند:

  • وقتی broker ای به cluster می‌پیوندد، خودش را در ZooKeeper ثبت می‌کند و به مؤلفه‌های دیگر اجازه می‌دهد آن را کشف کنند.
  • ساخت topic شامل نوشتن metadata در ZooKeeper است، از جمله تعداد partitionها و replication factor آن‌ها.
  • فرایند انتخاب leader که برای هر partition حیاتی است به ephemeral sequential nodeهای ZooKeeper تکیه می‌کند تا مطمئن شود فقط یک broker به‌عنوان leader برای هر partition عمل می‌کند.
  • consumer groupها عضویت و تخصیص partition خود را در ZooKeeper ردیابی می‌کنند و rebalance را هنگام پیوستن یا خروج consumerها ممکن می‌کنند.
  • و شاید مهم‌تر از همه، تشخیص خرابی broker از طریق ephemeral nodeهای ZooKeeper رخ می‌دهد. وقتی broker می‌میرد، نشستش منقضی می‌شود، گره‌هایش ناپدید می‌شوند و باعث تخصیص دوباره partitionهای آن می‌شود.

همان‌طور که گفتیم، این الگوی دقیق سال‌ها قبل از KRaft Kafka را تغذیه می‌کرد و رویکردهای مشابه در سیستم‌های توزیع‌شده دیگر مثل HBase، Hadoop و Solr به کار می‌روند. هنگام طراحی چنین سیستم‌هایی، ZooKeeper راه‌حل آزموده‌شده‌ای برای مسائل پیچیده هماهنگی که با آن روبه‌رو هستند ارائه می‌دهد.

قفل توزیع‌شده بادوام

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

جمع‌بندی

خلاصه کنیم.

ZooKeeper یک سرویس هماهنگی توزیع‌شده است که به مدیریت پیکربندی، نام‌گذاری و همگام‌سازی برای اپلیکیشن‌های توزیع‌شده کمک می‌کند. رابط ساده‌ای شبیه سیستم فایل می‌دهد اما قابلیت‌های پیچیده‌ای برای مدیریت پیکربندی، service discovery، انتخاب leader و قفل توزیع‌شده دارد. با اینکه جایگزین‌های جدیدتری مثل etcd، Consul و راه‌حل‌های ارائه‌دهندگان ابر وجود دارند، الگوهای ZooKeeper مخصوصاً برای طراحی سیستم‌های توزیع‌شده بنیادین هستند.

اما مراقب باشید. در مصاحبه طراحی سیستم بعدی‌تان سراغ ZooKeeper نروید، مگر این‌که سیستم زیرساخت عمیقی طراحی می‌کنید و لازم است درباره نحوه مدیریت هماهنگی دقیق بین چند سرور بحث کنید، یا به قابلیت‌های پیشرفته‌تری از آنچه load balancerهای مدرن و ابزارهای داخلی service discovery می‌دهند نیاز دارید.