پست

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

مبانی Kafka شامل broker، topic، partition، producer و consumer، نحوه کار آن، مقیاس‌پذیری، مدیریت hot partition، دوام و تحمل خطا، retry و بهینه‌سازی کارایی برای مصاحبه طراحی سیستم

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

Kafka

در این فصل یاد می‌گیریم چطور می‌توان از Kafka برای حل تعداد زیادی از مسائل طراحی سیستم استفاده کرد.

به احتمال زیاد نام Kafka را شنیده‌اید. محبوب است. طبق وب‌سایت خودش، ۸۰٪ از Fortune 100 از آن استفاده می‌کنند. اگر برای کمک به بزرگ‌ترین شرکت‌های دنیا به اندازه کافی خوب است، احتمالاً برای مصاحبه طراحی سیستم بعدی شما هم به اندازه کافی خوب است.

Apache Kafka یک پلتفرم متن‌باز و توزیع‌شده برای event streaming است که هم به‌عنوان Message Queue و هم به‌عنوان سیستم پردازش استریم قابل استفاده است. Kafka در ارائه کارایی بالا، مقیاس‌پذیری و دوام عالی است. طوری مهندسی شده که حجم عظیمی از داده را بلادرنگ مدیریت کند، تضمین کند هیچ پیامی هیچ‌وقت گم نمی‌شود و هر قطعه داده با بیشترین سرعت ممکن پردازش می‌شود.

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

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

جام جهانی است (مسابقه مورد علاقه شخصی من). ما وب‌سایتی راه می‌اندازیم که آمار بلادرنگ مسابقه‌ها را ارائه می‌دهد. هر بار که گلی زده می‌شود، بازیکنی کارت می‌گیرد یا تعویضی انجام می‌شود، می‌خواهیم وب‌سایتمان را با آخرین اطلاعات به‌روز کنیم.

رویدادها هنگام رخ دادن در یک صف قرار می‌گیرند. به سرور یا فرایندی که این رویدادها را در صف می‌گذارد producer (تولیدکننده) می‌گوییم. در پایین‌دست، سروری داریم که رویدادها را از صف می‌خواند و وب‌سایت را به‌روز می‌کند. به این consumer (مصرف‌کننده) می‌گوییم.

حالا تصور کنید جام جهانی از ۴۸ تیم برتر به تورنمنت فرضی ۱۰۰۰ تیمی گسترش پیدا کرده و همه بازی‌ها هم‌زمان برگزار می‌شوند. تعداد رویدادها چشمگیر زیاد شده و سرور تکی ما که میزبان صف است برای ادامه دادن دست‌وپا می‌زند. به همین ترتیب، consumer ما حس می‌کند دهانش را جلوی شلنگ آتش‌نشانی گرفته‌اند و زیر بار در حال سقوط است.

باید سیستم را با اضافه کردن سرورهای بیشتر برای توزیع صف مقیاس بدهیم. اما چطور مطمئن شویم رویدادها هنوز به‌ترتیب پردازش می‌شوند؟

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

راه‌حل منطقی این است که آیتم‌های صف را بر اساس بازی‌ای که به آن مربوط‌اند توزیع کنیم. به این ترتیب همه رویدادهای یک بازی به‌ترتیب پردازش می‌شوند، چون در یک صف هستند. این یکی از ایده‌های اساسی Kafka است: پیام‌هایی که از طریق Kafka فرستاده و دریافت می‌شوند به یک استراتژی توزیع نیاز دارند که کاربر مشخص می‌کند.

اما consumer ما هنوز غرق است. اضافه کردن consumer بیشتر به‌اندازه کافی آسان است، اما چطور مطمئن شویم هر رویداد فقط یک بار پردازش می‌شود؟ می‌توانیم consumerها را در گروهی به نام consumer group در Kafka دسته‌بندی کنیم. با consumer group تضمین می‌شود هر رویداد فقط توسط یک consumer در گروه پردازش شود.

در نهایت تصمیم گرفته‌ایم جام جهانی فرضی خود را به ورزش‌های بیشتری مثل بسکتبال گسترش دهیم. اما نمی‌خواهیم وب‌سایت فوتبال ما رویدادهای بسکتبال را پوشش دهد و نمی‌خواهیم وب‌سایت بسکتبال ما رویدادهای فوتبال را پوشش دهد. پس مفهوم topic را معرفی می‌کنیم. هر رویداد با یک topic مرتبط است و consumerها می‌توانند در topicهای مشخصی مشترک شوند. بنابراین consumerهایی که وب‌سایت فوتبال را به‌روز می‌کنند فقط در topic فوتبال مشترک می‌شوند و consumerهایی که وب‌سایت بسکتبال را به‌روز می‌کنند فقط در رویدادهای بسکتبال.

اصطلاحات و معماری پایه

مثال عالی بود، اما بیایید با رسمی کردن اصطلاحات و مفاهیم کلیدی‌ای که بالا معرفی شد، Kafka را کمی دقیق‌تر تعریف کنیم.

یک cluster در Kafka از چند broker تشکیل شده است. این‌ها فقط سرورهای تکی هستند (فیزیکی یا مجازی). هر broker مسئول ذخیره داده و خدمات‌دهی به کلاینت‌هاست. هرچه broker بیشتری داشته باشید، می‌توانید داده بیشتری ذخیره کنید و به کلاینت‌های بیشتری خدمت بدهید.

هر broker تعدادی partition دارد. هر partition دنباله‌ای مرتب و تغییرناپذیر از پیام‌هاست که مداوم به آن اضافه می‌شود. مثل یک فایل log به آن فکر کنید. partitionها راهی هستند که Kafka مقیاس می‌گیرد، چون اجازه می‌دهند پیام‌ها موازی مصرف شوند.

Topic فقط یک گروه‌بندی منطقی از partitionهاست. topicها نحوه انتشار و اشتراک داده در Kafka هستند. وقتی پیامی منتشر می‌کنید، آن را در یک topic منتشر می‌کنید و وقتی پیامی مصرف می‌کنید، آن را از یک topic مصرف می‌کنید. topicها همیشه چندتولیدکننده‌ای (multi-producer) هستند. یعنی یک topic می‌تواند صفر، یک یا تعداد زیادی producer داشته باشد که روی آن داده می‌نویسند.

پس تفاوت topic و partition چیست؟ topic گروه‌بندی منطقی پیام‌هاست. partition گروه‌بندی فیزیکی پیام‌هاست. یک topic می‌تواند چند partition داشته باشد و هر partition می‌تواند روی broker متفاوتی باشد. topicها فقط راهی برای سازمان‌دهی داده‌هایتان هستند، در حالی که partitionها راهی برای مقیاس دادن به داده‌هایتان.

در پایان producerها و consumerها را داریم. producerها کسانی هستند که روی topicها داده می‌نویسند و consumerها کسانی هستند که از topicها داده می‌خوانند. با این‌که Kafka یک API ساده هم برای producerها و هم برای consumerها ارائه می‌دهد، ساختن و پردازش پیام‌ها با شما، توسعه‌دهنده، است. Kafka به این‌که داده چیست اهمیتی نمی‌دهد. فقط آن را ذخیره و ارائه می‌کند.

نکته مهم این است که می‌توانید از Kafka هم به‌عنوان Message Queue و هم به‌عنوان استریم استفاده کنید. صادقانه، تمایز اینجا جزئی است. تنها تفاوت معنادار در نحوه تعامل consumerها با داده است. در Message Queue، consumerها پیام‌ها را از صف می‌خوانند و بعد تأیید (ack) می‌کنند که پیام را پردازش کرده‌اند. در استریم، consumerها پیام‌های استریم را می‌خوانند و بعد پردازششان می‌کنند، اما تأیید نمی‌کنند که پیام را پردازش کرده‌اند. این کار پردازش پیچیده‌تر داده را ممکن می‌کند.

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

وقتی رویدادی رخ می‌دهد، producer پیامی را که به آن record هم می‌گویند قالب‌بندی می‌کند و به topic در Kafka می‌فرستد. پیام یک field الزامی دارد، یعنی value، و سه field اختیاری: key، timestamp و header. از key برای تعیین این‌که پیام به کدام partition برود استفاده می‌شود و از timestamp برای ترتیب‌دهی پیام‌ها در partition. headerها مثل هدرهای HTTP جفت‌های کلید-مقدارند که می‌توان از آن‌ها برای ذخیره metadata پیام استفاده کرد.

با این‌که الزامی نیست، key برای تعیین این‌که پیام به کدام partition برود به کار می‌رود. اگر key ندهید، Kafka پیام را تصادفی به یک partition اختصاص می‌دهد. پس هنگام طراحی سیستم بزرگ و توزیع‌شده‌ای مثل آنچه احتمالاً در مصاحبه از شما می‌خواهند، باید از key استفاده کنید تا مطمئن شوید پیام‌ها به‌ترتیب پردازش می‌شوند و انتخاب این key مهم است. بعداً بیشتر درباره‌اش می‌گوییم.

به‌عنوان یک مثال سریع، این‌طور با ابزار خط فرمان Kafka به نام kafka-console-producer پیامی روی topicی به نام my_topic می‌فرستیم:

1
2
3
kafka-console-producer --broker-list localhost:9092 --topic my_topic --property "parse.key=true" --property "key.separator=:"
> key1: Hello, Kafka with key!
> key2: Another message with a different key

پرچم‌های --property "parse.key=true" و --property "key.separator=:" برای تعیین این‌که جفت‌های کلید-مقدار با دونقطه از هم جدا می‌شوند به کار می‌روند.

همین را با kafkajs، کلاینت محبوب Node.js برای Kafka، می‌بینیم:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Initialize the Kafka client
const kafka = new Kafka({
  clientId: 'my-app',
  brokers: ['localhost:9092']
})

// Initialize the producer
const producer = kafka.producer()

const run = async () => {
  // Connecting the producer
  await producer.connect()

  // Sending messages to the topic 'my_topic' with keys
  await producer.send({
    topic: 'my_topic',
    messages: [
      { key: 'key1', value: 'Hello, Kafka with key!' },
      { key: 'key2', value: 'Another message with a different key' }
    ],
  })
}

وقتی پیامی در topicی از Kafka منتشر می‌شود، Kafka اول partition مناسب پیام را تعیین می‌کند. این انتخاب partition بسیار مهم است، چون روی توزیع داده در cluster اثر می‌گذارد. این فرایند دو مرحله دارد.

  1. تعیین partition: Kafka از یک الگوریتم پارتیشن‌بندی استفاده می‌کند که key پیام را hash می‌کند تا پیام را به یک partition مشخص اختصاص دهد. اگر پیام key نداشته باشد، Kafka می‌تواند پیام‌ها را به‌صورت round-robin بین partitionها پخش کند یا از منطق پارتیشن‌بندی دیگری که در پیکربندی producer تعریف شده پیروی کند. این تضمین می‌کند پیام‌هایی که key یکسان دارند همیشه به همان partition می‌روند و ترتیب در سطح partition حفظ می‌شود.
  2. تخصیص broker: بعد از تعیین partition، Kafka مشخص می‌کند کدام broker آن partition مشخص را نگه می‌دارد. نگاشت partitionها به brokerهای مشخص را metadata مربوط به cluster در Kafka مدیریت می‌کند که توسط controller در Kafka (نقشی داخل cluster از brokerها) نگه داشته می‌شود. producer از این metadata برای فرستادن مستقیم پیام به brokerای استفاده می‌کند که partition هدف را میزبانی می‌کند.

هر partition در Kafka اساساً مثل یک فایل log فقط‌اضافه‌شونده (append-only) عمل می‌کند. پیام‌ها به‌ترتیب به انتهای این log اضافه می‌شوند. به همین دلیل Kafka را معمولاً «distributed commit log» توصیف می‌کنند. این طراحی append-only قلب معماری Kafka است و چند مزیت مهم می‌آورد.

  • تغییرناپذیری: پس از نوشته شدن، پیام‌های partition را نمی‌توان تغییر داد یا حذف کرد. این تغییرناپذیری برای کارایی و قابلیت اطمینان Kafka حیاتی است. replication را ساده می‌کند، فرایندهای بازیابی را سریع‌تر می‌کند و از مشکلات سازگاری رایج در سیستم‌هایی که داده‌شان قابل تغییر است دوری می‌کند.
  • کارایی: با محدود کردن عملیات به اضافه کردن داده به انتهای log، Kafka زمان seek دیسک را که در بسیاری از سیستم‌های ذخیره‌سازی گلوگاه بزرگی است حداقل می‌کند.
  • مقیاس‌پذیری: سادگی مکانیزم log فقط‌اضافه‌شونده مقیاس افقی را آسان می‌کند. می‌توان partitionهای بیشتری اضافه کرد و بین cluster ای از brokerها توزیع کرد تا بارهای فزاینده را مدیریت کند. هر partition را هم می‌توان روی چند broker تکثیر (replicate) کرد تا تحمل خطا بیشتر شود.

به هر پیام در partition در Kafka یک offset یکتا اختصاص داده می‌شود که شناسه‌ای متوالی است و موقعیت پیام را در partition نشان می‌دهد. consumerها از این offset برای ردیابی پیشرفت‌شان در خواندن پیام‌های topic استفاده می‌کنند. وقتی consumerها پیام‌ها را می‌خوانند، offset فعلی خود را نگه می‌دارند و به‌صورت دوره‌ای این offset را به Kafka commit می‌کنند. به این ترتیب در صورت شکست یا راه‌اندازی مجدد، می‌توانند خواندن را از جایی که متوقف کرده بودند ادامه دهند.

وقتی پیامی در partition تعیین‌شده منتشر شد، Kafka با مکانیزم قوی replication دوام و دسترس‌پذیری آن را تضمین می‌کند. Kafka برای replication از مدل leader-follower استفاده می‌کند که این‌طور کار می‌کند.

  • تخصیص leader replica: هر partition یک leader replica مشخص دارد که روی یک broker قرار دارد. این leader replica مسئول رسیدگی به همه درخواست‌های خواندن و نوشتن برای آن partition است. تخصیص leader replica را controller مدیریت می‌کند و تضمین می‌کند leader replica هر partition به‌طور مؤثر در cluster توزیع شود تا بار متعادل شود.
  • replication در follower: در کنار leader replica، چند follower replica برای هر partition وجود دارد که روی brokerهای مختلف قرار دارند. این followerها درخواست مستقیم کلاینت را رسیدگی نمی‌کنند. در عوض به‌صورت غیرفعال داده را از leader replica تکثیر می‌کنند. با تکثیر پیام‌هایی که leader دریافت می‌کند، این followerها مثل پشتیبان عمل می‌کنند و اگر leader شکست بخورد آماده تحویل گرفتن کار هستند.
  • همگام‌سازی و سازگاری: followerها پیوسته با leader replica همگام می‌شوند تا مطمئن شوند آخرین مجموعه پیام‌ها به log partition اضافه شده است. این همگام‌سازی برای حفظ سازگاری در cluster حیاتی است. اگر leader replica شکست بخورد، یکی از followerهایی که کامل همگام شده سریع می‌تواند به‌عنوان leader جدید ارتقا پیدا کند و downtime و از دست رفتن داده را حداقل کند.
  • نقش controller در replication: controller در cluster Kafka این فرایند replication را مدیریت می‌کند. سلامت همه brokerها را زیر نظر دارد و پویایی leadership و replication را مدیریت می‌کند. وقتی brokerی شکست می‌خورد، controller نقش leader را به یکی از followerهای دوباره‌همگام‌شده اختصاص می‌دهد تا دسترس‌پذیری مداوم partition تضمین شود.

در پایان، consumerها با مدل مبتنی بر pull پیام‌ها را از topicهای Kafka می‌خوانند. برخلاف بعضی سیستم‌های پیام‌رسانی که داده را به consumerها push می‌کنند، consumerهای Kafka فعالانه از broker برای پیام جدید در فاصله‌هایی که خودشان کنترل می‌کنند poll می‌کنند. همان‌طور که در مستندات رسمی Apache Kafka توضیح داده شده، این رویکرد pull انتخاب عمدی طراحی بود که چند مزیت دارد: به consumerها اجازه می‌دهد نرخ مصرف خودشان را کنترل کنند، مدیریت خرابی را ساده‌تر می‌کند، از غرق شدن consumerهای کند جلوگیری می‌کند و batching کارآمد را ممکن می‌کند.

برای کامل کردن مثال قبلی، این‌طور پیام‌های topic به نام my_topic را با ابزار خط فرمان kafka-console-consumer مصرف می‌کنیم:

1
2
3
4
5
kafka-console-consumer --bootstrap-server localhost:9092 --topic my_topic --from-beginning

# Output
key1: Hello, Kafka with key!
key2: Another message with a different key

به‌طور مشابه با kafkajs می‌توانیم پیام‌های topic به نام my_topic را مصرف کنیم:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Initialize the Kafka client
const kafka = new Kafka({
  clientId: 'my-app',
  brokers: ['localhost:9092']
})

// Initialize the consumer
const consumer = kafka.consumer({ groupId: 'my-group' })

const run = async () => {
  // Connecting the consumer
  await consumer.connect()

  // Subscribing to the topic 'my_topic'
  await consumer.subscribe({ topic: 'my_topic' })

  // Consuming messages
  await consumer.run({
    eachMessage: async ({ topic, partition, message }) => {
      console.log({
        value: message.value.toString(),
        key: message.key.toString()
      })
    },
  })
}

وقتی همه این‌ها را به هم وصل کنیم، چیزی شبیه این می‌گیریم.

چه زمانی در مصاحبه از Kafka استفاده کنیم؟

Kafka را می‌توان هم به‌عنوان Message Queue و هم به‌عنوان استریم به کار برد.

تفاوت اصلی این دو در نحوه تعامل consumerها با داده است. در Message Queue، consumerها معمولاً وقتی آماده پردازش پیام هستند آن را از صف برمی‌دارند. در استریم، consumerها پیوسته پیام‌ها را هنگام رسیدن بلادرنگ مصرف و پردازش می‌کنند، مثل نوشیدن از رودخانه‌ای جاری.

وقتی Message Queue به سیستم اضافه کنید:

  • پردازشی دارید که می‌تواند ناهمزمان انجام شود. YouTube مثال خوبی است. وقتی کاربران ویدیو آپلود می‌کنند، می‌توانیم ویدیوی با کیفیت استاندارد را فوراً در دسترس قرار دهیم و بعد ویدیو را (از طریق لینک) در topicی از Kafka بگذاریم تا هر وقت سیستم وقت داشت، encode شود.
  • باید مطمئن شوید پیام‌ها به‌ترتیب پردازش می‌شوند. می‌توانیم برای صف انتظار مجازی (virtual waiting queue) در طراحی Ticketmaster از Kafka استفاده کنیم. هدف آن اطمینان از این است که کاربران به همان ترتیبی که وارد شده‌اند وارد صفحه رزرو شوند.
  • می‌خواهید producer و consumer را جدا کنید تا مستقل مقیاس بگیرند. معمولاً یعنی producer پیام‌ها را سریع‌تر از آنچه consumer بتواند مصرف کند تولید می‌کند. این در میکروسرویس‌ها الگوی رایجی است، جایی که می‌خواهید مطمئن شوید یک سرویس نمی‌تواند سرویس دیگر را از کار بیندازد.

استریم‌ها وقتی مفیدند که:

  • به پردازش پیوسته و فوری داده ورودی نیاز دارید و آن را به‌عنوان استریم بلادرنگ در نظر می‌گیرید. برای مثالی که در آن داده کلیک را بلادرنگ جمع می‌کنیم، به طراحی Ad Click Aggregator نگاه کنید.
  • پیام‌ها باید هم‌زمان توسط چند consumer پردازش شوند. در طراحی Facebook Live Comments می‌توانیم از Kafka به‌عنوان سیستم Pub/Sub برای فرستادن کامنت‌ها به چند consumer استفاده کنیم.

آنچه درباره Kafka برای مصاحبه طراحی سیستم باید بدانید

درباره Kafka چیزهای زیادی برای دانستن هست. اما روی بخش‌هایی تمرکز می‌کنیم که به احتمال زیاد به مصاحبه طراحی سیستم شما مرتبط‌اند.

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

مقیاس‌پذیری

با فهمیدن محدودیت‌های یک Kafka broker شروع کنیم. در مصاحبه مهم است throughput و تعداد پیام‌هایی را که ذخیره می‌کنید تخمین بزنید تا بفهمید اصلاً باید نگران مقیاس‌دهی باشیم یا نه.

اول، محدودیت سختی برای اندازه پیام در Kafka نیست، چون از طریق message.max.bytes قابل پیکربندی است. با این حال توصیه می‌شود پیام‌ها را زیر ۱ مگابایت نگه دارید تا با کم کردن فشار حافظه و استفاده بهتر از شبکه، کارایی بهینه حفظ شود.

ذخیره Blobهای بزرگ داده در Kafka ضدالگوی رایجی در مصاحبه‌های طراحی سیستم است. Kafka پایگاه داده نیست و برای ذخیره فایل‌های بزرگ ساخته نشده. برای ذخیره پیام‌های کوچکی است که سریع پردازش می‌شوند.

مثلاً هنگام طراحی YouTube باید ویدیوها را بعد از آپلود پردازش کنیم. ساده‌لوحانه ممکن است ویدیوها را در Kafka بگذارید تا worker مربوط به chunking یا transcoding آن‌ها را ناهمزمان از صف بردارد و پردازش کند. این ایده خوبی نیست. در عوض باید ویدیوها را در سیستم فایل توزیع‌شده‌ای مثل S3 ذخیره کنید و پیامی در Kafka با محل ویدیو در S3 بگذارید. به این ترتیب پیام Kafka کوچک است و مثل اشاره‌گری به ویدیوی کامل در S3 عمل می‌کند.

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

اگر به مقیاس‌دهی نیاز دارید، چند استراتژی دارید.

  • مقیاس افقی با broker بیشتر: ساده‌ترین راه مقیاس دادن Kafka اضافه کردن brokerهای بیشتر به cluster است. این کار به توزیع بار کمک می‌کند و تحمل خطای بیشتری می‌دهد. هر broker می‌تواند بخشی از ترافیک را مدیریت کند و ظرفیت کلی سیستم را بالا ببرد. هنگام اضافه کردن brokerها واقعاً مهم است مطمئن شوید topicهای شما partition کافی برای استفاده از brokerهای اضافه دارند. partitionهای بیشتر یعنی موازی‌سازی بیشتر و توزیع بار بهتر. اگر partition کم دارید، نمی‌توانید از brokerهای جدید اضافه‌شده سود ببرید.
  • استراتژی پارتیشن‌بندی: این باید تمرکز اصلی استراتژی مقیاس‌دهی شما در مصاحبه باشد و تصمیم اصلی شما هنگام کار با Kafka cluster (چون امروز بیشتر مقیاس‌دهی در سرویس‌های مدیریت‌شده به‌صورت پویا انجام می‌شود). باید تصمیم بگیرید داده‌هایتان را بین brokerها چطور تقسیم کنید. این کار با انتخاب key برای پیام‌ها انجام می‌شود. partition با hash کردن key با تابع hash (به‌طور پیش‌فرض murmur2) و گرفتن مدول با تعداد partitionها تعیین می‌شود: partition = hash(key) % num_partitions. اگر key بدی انتخاب کنید، ممکن است به hot partitionهایی برسید که پر از ترافیک‌اند. keyهای خوب آن‌هایی هستند که یکنواخت در فضای partition توزیع شده‌اند.

شایان ذکر است که خارج از مصاحبه، بسیاری از ملاحظات مقیاس‌دهی با سرویس‌های مدیریت‌شده Kafka مثل Confluent Cloud یا AWS MSK آسان می‌شوند. این سرویس‌ها بیشتر مقیاس‌دهی را برایتان انجام می‌دهند، اما همچنان باید مفاهیم زیربنایی را بفهمید.

وقتی با Kafka کار می‌کنید، معمولاً به‌جای کل cluster، به مقیاس دادن topicها فکر می‌کنید. چون topicهای مختلف ممکن است نیازهای متفاوتی داشته باشند. مثلاً ممکن است topicی با throughput بسیار بالا داشته باشید که باید بین brokerهای زیادی تقسیم شود، در حالی که topic دیگری throughput پایین دارد و یک broker تکی می‌تواند مدیریتش کند. برای مقیاس دادن یک topic می‌توانید تعداد partitionها را زیاد کنید که اجازه می‌دهد از brokerهای بیشتری استفاده کنید.

hot partitionها را چطور مدیریت کنیم؟

مصاحبه‌گرها دوست دارند این سوال را بپرسند. Ad Click Aggregator را در نظر بگیرید که در آن Kafka جریانی از رویدادهای کلیک را ذخیره می‌کند، وقتی کاربران روی تبلیغات کلیک می‌کنند. طبیعتاً با پارتیشن‌بندی بر اساس شناسه تبلیغ شروع می‌کنید. اما وقتی Nike تبلیغ جدید LeBron James را راه می‌اندازد، مطمئن باشید آن partition زیر ترافیک غرق می‌شود و یک hot partition دارید.

چند استراتژی برای مدیریت hot partition هست.

  • پارتیشن‌بندی تصادفی بدون key: اگر key ندهید، Kafka partition را به‌طور تصادفی به پیام اختصاص می‌دهد و توزیع یکنواخت را تضمین می‌کند. نکته منفی این است که توانایی تضمین ترتیب پیام‌ها را از دست می‌دهید. اگر این برای طراحی شما مهم نیست، این گزینه خوبی است.
  • salting تصادفی: هنگام تولید key پارتیشن‌بندی می‌توانیم عدد یا timestamp تصادفی به شناسه تبلیغ اضافه کنیم. این کار به توزیع یکنواخت بار بین چند partition کمک می‌کند، هرچند ممکن است بعداً منطق aggregation را سمت consumer پیچیده کند. اغلب به آن «salting» کلید می‌گویند.
  • استفاده از key ترکیبی: به‌جای استفاده از شناسه تبلیغ، ترکیبی از شناسه تبلیغ و ویژگی دیگری، مثل ناحیه جغرافیایی یا بخشی از شناسه کاربر، برای ساختن key ترکیبی (compound key) استفاده کنید. این رویکرد به توزیع یکنواخت‌تر ترافیک کمک می‌کند و به‌خصوص اگر بتوانید ویژگی‌هایی پیدا کنید که مستقل از شناسه تبلیغ تغییر می‌کنند مفید است.
  • backpressure: بسته به نیاز شما، راه‌حل آسان این است که سرعت producer را کم کنید. اگر از سرویس مدیریت‌شده Kafka استفاده می‌کنید، ممکن است مکانیزم‌های داخلی برای مدیریت این داشته باشد. اگر Kafka cluster خودتان را اجرا می‌کنید، می‌توانید با وادار کردن producer به چک کردن lag روی partition و کند شدن در صورت زیاد بودن آن، backpressure اعمال کنید.

تحمل خطا و دوام

اگر Kafka را انتخاب کرده‌اید، یک دلیل ممکن است تضمین‌های دوام قوی آن باشد. اما Kafka چطور تضمین می‌کند داده‌هایتان امن‌اند و هیچ پیامی گم نمی‌شود؟

Kafka دوام داده را با مکانیزم replication تضمین می‌کند. هر partition روی چند broker تکثیر می‌شود، یک broker به‌عنوان leader و بقیه follower. وقتی producer پیامی می‌فرستد، روی leader نوشته می‌شود و بعد برای followerها تکثیر می‌شود. این تضمین می‌کند حتی اگر یک broker شکست بخورد، داده در دسترس می‌ماند. تنظیم acknowledgment در producer (یعنی acks) در اینجا نقش مهمی دارد. تنظیم acks=all تضمین می‌کند پیام فقط وقتی تأیید می‌شود که replicaهای همگام (in-sync) آن را دریافت کرده باشند. این بیشترین دوام را تضمین می‌کند.

بسته به میزان دوام موردنیاز، می‌توانید replication factor را برای topicها پیکربندی کنید. replication factor تعداد کپی‌هایی است که برای هر partition نگه داشته می‌شود. replication factor برابر ۳ رایج است، یعنی هر partition یک leader و دو follower دارد. پس اگر یکی از brokerها شکست بخورد، داده روی دو broker دیگر هنوز موجود است و می‌توانیم یک follower را به‌عنوان leader جدید ارتقا بدهیم.

اما وقتی consumer از کار بیفتد چه می‌شود؟

معمولاً Kafka را همیشه در دسترس (always available) می‌دانند. اغلب می‌شنوید که مردم می‌گویند «Kafka همیشه در دسترس است، گاهی سازگار.» یعنی سوالی مثل «اگر Kafka از کار بیفتد چه می‌شود؟» خیلی واقع‌بینانه نیست و اگر مصاحبه‌گر آن را پرسید، حتی ممکن است بخواهید با ملایمت به او فشار بیاورید.

آنچه خیلی مرتبط‌تر و محتمل‌تر است این است که consumer از کار بیفتد. وقتی consumer شکست می‌خورد، مکانیزم‌های تحمل خطای Kafka به تضمین تداوم کمک می‌کنند.

  • مدیریت offset: یادتان باشد partitionها logهای append-only هستند که به هر پیام offset یکتایی اختصاص داده می‌شود. consumerها بعد از پردازش پیام، offsetشان را به Kafka commit می‌کنند. این روشی است که consumerها می‌گویند «این پیام را پردازش کرده‌ام.» وقتی consumer دوباره راه‌اندازی می‌شود، آخرین offset commit شده‌اش را از Kafka می‌خواند و پردازش را از همان‌جا ادامه می‌دهد. این تضمین می‌کند هیچ پیامی گم یا تکراری نشود.
  • rebalancing: وقتی consumer بخشی از consumer group است، اگر یکی از consumerها از کار بیفتد، Kafka partitionها را بین consumerهای باقی‌مانده توزیع می‌کند تا همه partitionها همچنان در حال پردازش باشند.

مصالحه‌ای که ممکن است در مصاحبه لازم باشد در نظر بگیرید این است که کی offset را commit کنید. مثلاً در طراحی web crawler، می‌خواهید مراقب باشید تا مطمئن نشده‌اید HTML خام در Blob Storage ذخیره شده، offset را commit نکنید. هرچه consumer باید کار بیشتری انجام دهد، در صورت شکست consumer احتمال بیشتری هست که مجبور شوید کار را دوباره انجام دهید. به همین دلیل کوچک نگه داشتن کار consumer تا جای ممکن استراتژی خوبی است. همان‌طور که در web crawler دیدیم، crawler را به دو مرحله تقسیم کردیم: دانلود HTML و بعد parse کردن آن.

مدیریت retry و خطا

با این‌که خود Kafka بیشتر قابلیت اطمینان را کنترل می‌کند (همان‌طور که بالا دیدیم)، ممکن است سیستم ما پیام‌ها را درست به Kafka ندهد یا از آن نگیرد. باید این سناریوها را با ظرافت مدیریت کنیم.

retry در producer

اول از همه، ممکن است در وهله اول نتوانیم پیامی را به Kafka برسانیم. خطاها ممکن است به‌خاطر مشکلات شبکه، در دسترس نبودن broker یا خرابی‌های گذرا رخ دهند. برای مدیریت این سناریوها، producerهای Kafka از retry خودکار پشتیبانی می‌کنند. این نگاه کوتاهی به نحوه پیکربندی آن‌هاست:

1
2
3
4
5
6
7
const producer = kafka.producer({
  retry: {
    retries: 5, // Retry up to 5 times
    initialRetryTime: 100, // Wait 100ms between retries
  },
  idempotent: true,
});

باید مطمئن شوید حالت idempotent producer را فعال کرده‌اید تا هنگام retry از پیام‌های تکراری جلوگیری شود. این فقط تضمین می‌کند پیام‌ها فقط یک بار نوشته شوند، حتی اگر به اشتباه فکر کنیم ارسال نشده‌اند.

retry در consumer

سمت consumer، ممکن است به هر دلیلی نتوانیم پیامی را پردازش کنیم. Kafka در واقع به‌صورت پیش‌فرض از retry برای consumerها پشتیبانی نمی‌کند (اما AWS SQS می‌کند!). پس باید منطق retry را خودمان پیاده کنیم. الگوی رایج این است که topic سفارشی بسازیم که بتوانیم پیام‌های شکست‌خورده را به آن منتقل کنیم و بعد consumer جداگانه‌ای داشته باشیم که این پیام‌ها را پردازش کند. به این ترتیب می‌توانیم پیام‌ها را هر چند بار که بخواهیم بدون اثر روی consumer اصلی دوباره امتحان کنیم. اگر پیامی بارها شکست بخورد، می‌توانیم آن را به Dead Letter Queue (DLQ) منتقل کنیم. DLQها فقط جایی برای ذخیره پیام‌های شکست‌خورده‌اند تا بعداً بتوانیم بررسی‌شان کنیم.

در تحلیل web crawler خواهید دید که ما در واقع به‌جای Kafka، SQS را انتخاب می‌کنیم تا از قابلیت‌های داخلی retry و Dead Letter Queue استفاده کنیم و لازم نباشد خودمان پیاده‌شان کنیم.

بهینه‌سازی کارایی

به‌خصوص هنگام استفاده از Kafka به‌عنوان event stream، باید مراقب کارایی باشیم تا بتوانیم پیام‌ها را در سریع‌ترین زمان ممکن پردازش کنیم.

اولین کاری که می‌توانیم بکنیم batch کردن پیام‌ها در producer قبل از فرستادن آن‌ها به Kafka است. این هم فقط یک پیکربندی ساده است.

1
2
3
4
5
6
const producer = kafka.producer({
  batch: {
    maxSize: 16384, // Maximum batch size in bytes
    maxTime: 100,   // Maximum time to wait before sending a batch
  }
});

راه رایج دیگر برای بهتر کردن throughput فشرده‌سازی پیام‌ها روی producer است. این کار را می‌توان با تنظیم گزینه compression در پیکربندی producer انجام داد. Kafka از چند الگوریتم فشرده‌سازی از جمله GZIP، Snappy و LZ4 پشتیبانی می‌کند. در اصل فقط پیام‌ها را کوچک‌تر می‌کنیم تا سریع‌تر ارسال شوند.

1
2
3
const producer = kafka.producer({
  compression: CompressionTypes.GZIP,
});

شاید بزرگ‌ترین اثری که می‌توانید روی کارایی بگذارید، به انتخاب key پارتیشن‌بندی بازگردد. هدف این است که با اطمینان از توزیع یکنواخت پیام‌ها بین partitionها، موازی‌سازی را حداکثر کنید. در مصاحبه، بحث درباره استراتژی پارتیشن‌بندی، همان‌طور که بالا گفتیم، باید تقریباً همیشه جایی باشد که از آن شروع می‌کنید.

سیاست‌های retention

topicهای Kafka یک سیاست retention دارند که تعیین می‌کند پیام‌ها چه مدت در log نگه داشته شوند. این با تنظیمات retention.ms و retention.bytes پیکربندی می‌شود. سیاست پیش‌فرض retention نگه داشتن پیام‌ها به مدت ۷ روز است.

در مصاحبه ممکن است از شما بخواهند سیستمی طراحی کنید که لازم است پیام‌ها را مدت طولانی‌تری ذخیره کند. در این مورد می‌توانید سیاست retention را طوری پیکربندی کنید که پیام‌ها مدت بیشتری نگه داشته شوند. فقط به اثر آن روی هزینه و کارایی ذخیره‌سازی توجه کنید.

جمع‌بندی

مرور سریعی کنیم.

Apache Kafka پلتفرم متن‌باز و توزیع‌شده event streaming است که برای کارایی بالا، مقیاس‌پذیری و دوام مهندسی شده. از producerها برای فرستادن پیام به topicها و از consumerها برای خواندن آن‌ها استفاده می‌کند و پیام‌ها در partitionهای مرتب و تغییرناپذیر روی چند broker (سرور) ذخیره می‌شوند. برای پردازش داده بلادرنگ و صف پیام ناهمزمان در طراحی سیستم بسیار مناسب است.

وقتی صحبت از مقیاس می‌شود، مطمئن شوید با بحث درباره استراتژی پارتیشن‌بندی و نحوه مدیریت hot partitionها شروع کرده‌اید. و یادتان باشد، Kafka همیشه در دسترس است، گاهی سازگار.