<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
    <channel>
        <title>نوشته های بهرام انیژ</title>
        <link>https://virgool.io/feed/@bahramanizh</link>
        <description>مهندس نرم افزار</description>
        <language>fa</language>
        <pubDate>2026-06-16 10:34:12</pubDate>
        <image>
            <url>https://files.virgool.io/upload/users/2731/avatar/V09Nxa.jpg?height=120&amp;width=120</url>
            <title>بهرام انیژ</title>
            <link>https://virgool.io/@bahramanizh</link>
        </image>

                    <item>
                <title>سفرهای علمی: Rebalance ناخواسته در Kafka</title>
                <link>https://virgool.io/@bahramanizh/%D8%B3%D9%81%D8%B1%D9%87%D8%A7%DB%8C-%D8%B9%D9%84%D9%85%DB%8C-%D8%B1%DB%8C-%D8%A8%D8%A7%D9%84%D8%A7%D9%86%D8%B3-%D9%86%D8%A7%D8%AE%D9%88%D8%A7%D8%B3%D8%AA%D9%87-%D8%AF%D8%B1-kafka-cwbl6nexvq9f</link>
                <description>ماجرا از اونجا شروع شد که یکی از سرویس‌هامون که مسئول پردازش رویدادهای یک تاپیک Kafka بود، دچار Lag شدید شد. با بررسی وضعیت Consumer Group مربوطه، متوجه شدیم که مصرف‌کننده‌ها در وضعیت &quot;PREPARING_REBALANCE&quot; گیر کرده‌ان و در یک حلقه بی‌انتها از ری‌بالانس گیر افتادن. همین موضوع باعث می‌شد که lag لحظه‌به‌لحظه بدتر بشه و سرویس عملاً از کار بیفته.لگ در پردازش رویداد‌ها، شرایطی بود که قبلاً هم باهاش روبرو شده بودیم، اما این بار علاوه بر کندی در پردازش، مصرف‌کننده‌ها (Consumers) در یک حلقه بی‌پایان از Rebalance گیر کرده بودند و در وضعیت &quot;PREPARING_REBALANCE&quot; باقی می‌موندند. این مشکل باعث تشدید لگ و بدتر شدن وضعیت سرویس شده بود.خب حالا برای اینکه بتونیم بریم سراغ مشکل و بیاین باهم دیگه گام به گام پیش بریم.و فرض میگیریم که شما با مفاهیم اولیه کافکا مثل Brokers , Topic , Partition , Consumer group آشنا هستید.اول بریم ببینیم این Rebalance کردن در Kafka چه معنی داره؟ و چه زمانی اتفاق میوفته؟ و چه ساید افکتی هایی داره؟Rebalance یک فرایند داخلی در کافکاست که طی آن پارتیشن‌های یک تاپیک، بین کانسیومرهای فعال در یک گروه، مجدداً توزیع می‌شوند. این فرایند برای حفظ توازن و اطمینان از پردازش مستمر پیام‌ها ضروری است و معمولاً در شرایط زیر رخ می‌دهد و ما برای پیدا کردن علت مشکل این دلایل رو بررسی کردیم:تغییر تعداد کانسیومرها: شاید یک کانسیومر به گروه اضافه یا ازش کم شده باشه. ولی چون ما هیچ Scale Up یا Scale Down نداشتیم، این مورد نمیتونه باشه.از کار افتادن اپلیکیشن: ممکنه اپلیکیشن کرش کرده باشه یا داون شده باشه. با بررسی وضعیت پادها، مطمئن شدیم که اپلیکیشن در حال اجراست و داره کار میکنه.تغییر تعداد پارتیشن‌های تاپیک: این هم می‌تونه باعث ری‌بالانس بشه، ولی تو اون بازه زمانی هیچ تغییری توی پارتیشن‌ها نداشتیم.خب وقتی دلایل بالا رد شدن برای پیدا کردن سرنخ بهتری سراغ لاگ های اپلیکیشن رفتیم که با با این خط مهم مواجه شدیم:consumer poll timeout has expired.و این شد سرنخ اصلی ما برای حل معمامون. اما این خطا به چه معناست و چرا باعث Rebalance میشه؟ برای پاسخ به این سوال، لازمه که یک قدمی به عقب بریم و ببینیم کافکا چطور وضعیت سلامت کانسیومرها رو بررسی می‌کنه.کافکا برای اینکه بفهمه کدوم کانسیومرها هنوز فعالن و درست کار می‌کنن، دو مکانیزم اصلی داره که هر کدوم توی یک ترد جدا اجرا می‌شن:1. Heartbeat Thread 2. Poll Thread (Proccesing Thread)- خب اولی برای این طراحی شده که بفهمه کانسیومر واقعاً زنده است یا نه.کانسیومرها توی بازه‌های زمانی ثابت، یک سیگنال heartbeat برای Consumer Coordinator می‌فرستن که این بازه زمانی هم با پارامتر زیر قابل تنظیمه:heartbeat.interval.msحالا اگر Coordinator برای یک مدت طولانی این سیگنال‌ها رو دریافت نکنه (مثلاً اپلیکیشن Shut down شده باشه یا ارتباط شبکه قطع شده باشه)، بعد از گذشت یک زمان مشخص، کانسیومر رو “dead” حساب می‌کنه. این مدت زمان هم با پارامتر زیر تعیین میشه:session.timeout.msوقتی این زمان تموم بشه، Group Coordinator فرایند Rebalance رو شروع می‌کنه و پارتیشن‌های مربوط به کانسیومر از کار افتاده رو به کانسیومرهای دیگه اساین می‌کنه.این دقیقاً همون سناریویی بود که اول کار، احتمال دادیم دلیل مشکل ما باشه.پس ترد Heartbeat در اصل یک جور سیستم مانیتورینگ داخلیه که اگر کانسیومر داون بشه یا مشکل ارتباطی پیدا کنه، سریع متوجه بشیم.- خب حالا میرسیم به ترد بعدی که اتفاقا به مشکل ما مربوط میشه یعنی poll threadتوی کافکا کانسیومر ها برای اینکه مسیج ها را از بروکر دریافت کنند از روش long polling استفاده میکنند.کانسیومر ها به صورت دوره ای عملیات poll انجام میدن ؛ یعنی به زبان ساده، میرن یک سرشماری از بروکرها می‌کنن تا ببینن پیام جدیدی رسیده یا نه و بعد اون پیام‌ها رو پردازش میکنن. این کار با صدا زدن متد ()poll انجام میشه. حالا نکته اینجاست که اگه فاصله زمانی بین دو بار صدا زدن این متد بیشتر از یک حد مشخص بشه، کافکا فرض می‌کنه کانسیومرمون از گروه خارج شده و در نتیجه همون داستان قبلی اتفاق میفته؛ disconnect و در نهایت rebalance.برای این ترد دو تا کانفیگ مهم داریم:max.poll.interval.ms: این مربوط به همون حداکثر زمانی میشه که کانسیومر فرصت داره تا poll بعدیش رو انجام بده که به صورت پیش فرض هم روی ۵ دقیقه تنظیم شده.یعنی مثلا اگه الان poll انجام شده کانسیومر تا حداکثر ۵ دقیقه بعد فرصت داره poll رو بازم صدا بزنه وگرنه disconnect میشه.max.poll.records: این یکی که میتونه خیلی مهم باشه; تعداد رکوردهایی که توی هر بار صدا زدن متد poll دریافت میکنیم رو کنترل میکنه, یعنی در واقع حداکثر رکوردی که توی هر poll میتونیم پردازش کنیم. مقدار پیش فرض این پارامتر هم ۵۰۰ رکورد هست.حالا که می‌دونیم این دو تا پارامتر چه کاری انجام میدن، باید مقادیرشون رو جوری تنظیم کنیم که با سرعت پردازش اپلیکیشنمون و حجم پیام‌ها (Throughput) هماهنگ باشه.پس Poll Thread همون جاییه که اگر پردازش داده‌ها کند بشه یا گیر کنه، سریع میشه متوجه شد.حالا با توجه به همه نکاتی که تا اینجا فهمیدیم، می‌تونیم برگردیم به مشکل خودمون.ما در آخر دیدیم که به ارور poll timeout has expired خورده بودیم و حالا با توجه به دانسته های الانمون یعنی کانسیومر نتونسته()pollبعدی رو به موقع اجرا کنه و بیشتر از ۵ دقیقه درگیر پردازش پیام‌هایpoll قبلی بوده.در نتیجه، مشکل اصلی مربوط به کندی پردازش پیام‌هاست. مثلا در سناریوی ما، یک سرویس خارجی خیلی دیر جواب میداد و همین باعث میشد پردازش طولانی بشه و کانسیومر نتونه به موقع poll() بعدی رو صدا بزنه.برای حل مشکل، اول باید خود سرویس رو بررسی کنیم و ببینیم آیا جایی از پردازش رو میشه بهترش کرد. مثلا می‌تونیم از resiliency patterns استفاده کنیم تا مطمئن بشیم که سرویس دیگه‌ای باعث گیر کردن پردازش پیام‌ها نمی‌شه، یا مثلا بعضی کارها رو به صورت async اجرا کنیم.بعد از ریفکتور کد، می‌تونیم سراغ تیون کردن کانفیگ‌های poll بریم. این کانفیگ‌ها بسته به اپلیکیشن، نرخ پیام‌ها، میانگین زمان پردازش هر پیام، تعداد کانسیومرها و پارتیشن‌های تاپیک می‌تونن متفاوت باشن.مثلا اگه پردازش هر پیام طولانیه، باید یا زمان بین pollها رو بیشتر کنیم یا تعداد رکوردهایی که هر بار poll می‌کنیم رو کمتر کنیم. فقط یه نکته هست: اگه تعداد رکوردها خیلی کم باشه، لود شبکه برای گرفتن پیام‌ها بیشتر می‌شه. در واقع این کانفیگ کمک می‌کنه که پیام‌ها رو به صورت دسته‌ای بگیریم و network overhead کمتر بشه.منابع:https://kafka.apache.org/documentation/#consumerconfigs_max.poll.interval.mshttps://medium.com/trendyol-tech/rebalance-and-partition-assignment-strategies-for-kafka-consumers-f50573e49609</description>
                <category>بهرام انیژ</category>
                <author>بهرام انیژ</author>
                <pubDate>Wed, 24 Dec 2025 16:41:12 +0330</pubDate>
            </item>
                    <item>
                <title>Server-assisted, client-side caching in Redis</title>
                <link>https://virgool.io/@bahramanizh/client-side-caching-p7rqiwgiidbl</link>
                <description>تو مقاله‌ی قبلی سعی کردیم داده‌هامون رو به‌صورت in-memory داخل اپلیکیشن نگه داریم. هدفمون چی بود؟ این‌که ابزارهای third-party مثل Redis و Memcached رو حذف کنیم تا هم ریسپانس‌تایم بهتر بشه و هم بار شبکه پایین بیاد. ولی خب، اونجا یکسری مشکلات داشتیم که مهمترینش inconsistency بود. حالا میخوایم ببینیم شرکت Paylocity این چالش رو چطوری حل کرده و باعث شده که ویژگی tracking در Redis معرفی بشه.پیشنهاد می‌کنم قبل از ادامه، حتماً مقاله‌ی قبلی رو یه نگاهی بندازید.داستان از این قراره که Paylocity هم دقیقاً همین نیاز ما رو داشت: ریسپانس تایم بهتر با نگه‌داری دیتا سمت اپلیکیشن. اونا هم مثل ما اومدن این ایده رو پیاده کنن، ولی به همون چالش‌هایی که گفتیم، برخوردن. در نهایت تیمشون تصمیم گرفت که کلاً بی‌خیال این روش بشه و بره سراغ Redis. اینجوری مشکلات قبلی حل شد، ولی یه چالش جدید پیش اومد: هر درخواست به Redis یه رفت‌وبرگشت TCP داشت که باعث چند میلی ثانیه تأخیر میشد. برای بیشتر تیم‌هاشون این تاخیر چیز مهمی نبود، ولی یه تیم که درخواست‌های سنگین داشت (مثلاً فیلترهای پیچیده توی هر صفحه)، دنبال سرعت بیشتر بود.اینجا بود که به فکر یه راه‌حل ترکیبی افتادن: ترکیب  in-memory cache سمت اپلیکیشن با Redis. حالا چالش اصلی این بود که چطور داده‌ها رو بین کش کلاینت‌ها و Redis هماهنگ نگه دارن. راه‌حلشون برای این موضوع، استفاده از  redis pub/sub  بود.ایده‌شون این بود که هر وقت داده‌ای توی Redis تغییر می‌کنه، یه پیام از طریق pub/sub به همه‌ی سرورها یا اینستنس‌ها ارسال بشه تا کش لوکال خودشون رو به‌روز کنن. اما این روش یه مشکل مهم داشت: اگه قرار بود کل کلید رو برای همه‌ی کلاینت‌ها بفرستن، بار شبکه به شدت بالا می‌رفت.برای حل این مشکل، از مدل hash slot توی Redis Cluster الهام گرفتن. hash slot فضا رو به چند تا بخش (slot) تقسیم می‌کنه و بعد با یه تابع هش مثل CRC16، هر کلید رو توی یکی از این بخش‌ها قرار میده. این مکانیزم توی Redis Cluster باعث می‌شه داده‌ها بین نودهای مختلف پخش بشن و عملکرد بهتری داشته باشیم.تیم Paylocity هم به جای اینکه خود کلید رو توی پیام بفرسته، فقط آیدی hash slot مربوطه رو به کلاینت‌ها ارسال می‌کرد. این کار باعث شد حجم هر پیام فقط ۲ بایت بشه! هر وقت یه کلید تغییر می‌کرد، حذف می‌شد یا منقضی می‌شد، یه پیام &quot;invalid data&quot; برای کلاینت‌ها فرستاده می‌شد، همراه با یه timestamp که نشون می‌داد آخرین آپدیت اون slot کی بوده.توی هر اپلیکیشن، یه آرایه بود که زمان آخرین بروزرسانی هر slot رو نگه می‌داشت و نشون می‌داد آخرین بار کی یه گروه از داده‌ها نامعتبر شده. حالا وقتی که میخواستن از cache استفاده کنند، زمان ذخیره‌سازی داده رو با زمان نامعتبر شدن slot مقایسه می‌کردن. اگه داده‌ها قبل از نامعتبر شدن cache ذخیره شده بودن، اون‌ها رو قدیمی در نظر می‌گرفتن. توی این حالت، داده‌ی قدیمی رو دور می‌ریختن و از Redis نسخه‌ی جدید رو می‌گرفتند.(lazy eviction)این روش باعث شد که تأخیر کم بشه و داده‌ها همیشه به‌روز بمونن، بدون اینکه پیچیدگی زیادی به سیستم اضافه بشه.جرقه‌ی تکامل: الهام‌گیری سالواتورهسالواتوره سانفیلیپو، خالق Redis، توی Redis Conf 2018 ارائه‌ی Ben Malec از شرکت Paylocity رو دید و به این فکر افتاد که کشینگ سمت کلاینت می‌تونه آینده‌ی Redis باشه. سال ۲۰۱۹، وقتی توی خیابونای نیویورک قدم می‌زد، به خودش گفت: چرا این ایده رو توی خود Redis پیاده نکنم؟ شرکت‌های بزرگی هستند که دیتاهاشون رو توی اپلیکیشن نگه میدارند تا سرعت بالا بره و بار سرور کم بشه، ولی Redis هنوز راه ساده‌ای برای این کار نداشت.سالواتوره از دو تا ایده‌ی بن الهام گرفت: گروه بندی کلیدها با hash slot و استفاده از pub/sub برای اطلاع‌رسانی تغییرات. بن با ۱۶ هزار slot کار کرده بود، ولی سالواتوره این رو به ۱۶ میلیون گروه (با ۲۴ بیت) ارتقا داد تا تغییرات دقیق‌تر بشن و هر پیام فقط چند کلید رو هدف بگیره. یعنی مثلا وقتی که شما یک سرور با 100 میلیون کلید داری، نباید پیام‌هایی که به خاطر تغییر یا حذف داده‌ها میاد، بیشتر از چند تا کلید رو در کش سمت مشتری تحت تاثیر قرار بدن.همچنین به جای اینکه اپلیکیشن پیام بفرسته، تصمیم گرفت سرور خودش این کارو بکنه و به کلاینت‌ها بگه چی تغییر کرده. این شد پایه‌ی ویژگی Tracking توی Redis 6.سالواتوره برگشت خونه، یه سند نوشت و شروع کرد اون رو پیاده‌ کنه. برای اینکه پیام‌ها خودکار برن، پروتکل جدید RESP3 رو آورد که می‌تونست پیام‌ها رو مستقیم به کلاینت‌ها بفرسته—یه چیزی که توی پروتکل قدیمی‌تر نبود. هدفش این بود که Redis بار رو از دوش اپلیکیشن‌ها برداره و کشینگ رو برای همه ساده‌تر و بهینه‌تر کنه.بلوغ ایده: Tracking در Redis 6تیم توسعه‌ی Redis اومد این ویژگی رو بهتر کرد و توی دو حالت ارائه داد:- حالت پیش‌فرض (CLIENT TRACKING ON)اینجا Redis مثل یه ناظر دقیق عمل می‌کنه. هر وقت یه کلاینت با دستور GET به یه کلید دسترسی پیدا کنه، سرور این تعامل رو ثبت می‌کنه و توی جدولی به نام Invalidation Table ذخیره می‌کنه. این جدول مثل یه دفترچه یادداشته که مشخص می‌کنه کدوم کلاینت‌ها روی چه کلیدهایی کار کردن.حالا اگه یه کلید تغییر کنه—چه با دستور SET از یه کلاینت دیگه، چه با منقضی شدن TTL، یا حتی به خاطر محدودیت maxmemory—سرور این جدول رو چک می‌کنه و فقط به اون کلاینت‌هایی که احتمالاً اون کلید رو توی حافظه لوکالشون دارن، پیام می‌فرسته. یعنی دیگه خبری از ارسال پیام‌های اضافه و غیرضروری نیست؛ همه چیز هدفمند و بهینه انجام می‌شه.مزیت بزرگ این روش اینه که بار شبکه رو کم می‌کنه و جلوی ارسال پیام‌های غیرضروری رو می‌گیره. تصور کنید یه برنامه با هزاران کلاینت داریم؛ اگه قرار بود هر تغییر به همه ارسال بشه، ترافیک شبکه به‌شدت بالا می‌رفت. ولی با این روش، فقط کلاینت‌های مرتبط مطلع می‌شن، و این یعنی بهینه‌سازی قابل‌توجه در مصرف پهنای باند.البته این دقت هزینه‌ای هم داره: سرور باید بخشی از حافظه رو برای نگهداری این جدول مصرف کنه. برای اینکه این موضوع مدیریت بشه، Redis اندازه‌ی جدول رو محدود می‌کنه. اگه جدول پر بشه، کلیدهای قدیمی‌تر رو با ارسال پیام‌های بی‌اعتباری جعلی حذف می‌کنه تا جا برای ورودی‌های جدید باز بشه.این تعادل بین کارایی و مصرف منابع باعث می‌شه که حالت پیش‌فرض، گزینه‌ی مناسبی برای سناریوهایی باشه که به دقت بالا نیاز دارن.- حالت انتشار: مقیاس‌پذیری بدون حافظه (CLIENT TRACKING ON BCAST)در مقابل، حالت انتشار (Broadcasting) رویکرد کاملاً متفاوتی داره و تغییرات رو به همه‌ی کلاینت‌های علاقه‌مند می‌رسونه.توی این روش، سرور دیگه نیازی به ذخیره‌ی اطلاعات کلیدهای دسترسی‌شده توسط کلاینت‌ها نداره، پس هیچ حافظه‌ی اضافی مصرف نمی‌کنه—که این یه مزیت بزرگ برای سیستم‌های بزرگ با منابع محدود حساب می‌شه.به جای اینکه سرور برای هر کلاینت جداگانه ردیابی انجام بده، کلاینت‌ها خودشون subscribe میکنن برای پیشوندهای خاصی مثل user: یا object:. وقتی کلیدی با این پیشوندها تغییر کنه، سرور یه پیام برای همه‌ی کلاینت‌های subscribe شده برای اون پیشوند می‌فرسته، بدون اینکه بررسی کنه واقعاً اون کلید رو ذخیره کردن یا نه.این سادگی، مقیاس‌پذیری رو به بالاترین حد می‌رسونه، ولی در عوض پیام‌های بیشتری ارسال میشه. مثلاً اگه کلیدی مثل user:1234 تغییر کنه، همه کلاینت‌هایی که به پیشوند user: متصل هستن، پیام می‌گیرن، حتی اگه فقط یکی از اون‌ها اون کلید رو ذخیره کرده باشن.این روش برای برنامه‌هایی که می‌خوان بدون فشار آوردن به سرور، کش سمت کلاینت رو پیاده کنن، خیلی مناسبه. ولی از طرف دیگه، نیاز به مدیریت دقیقتر سمت کلاینت داره تا بتونه پیام‌های غیرضروری رو فیلتر کنه.گزینه NOLOOP: کنترل در دستان کلاینتیکی از نوآوری‌های جذاب در Tracking، گزینه NOLOOP است که هم در حالت پیش‌فرض و هم در حالت انتشار کار می‌کنه.  فرض کنید یک کلاینت، کلیدی رو با دستور SET تغییر می‌ده و می‌خواد اون رو توی حافظه محلی خودش ذخیره کنه. بدون NOLOOP، سرور بلافاصله یه پیام Invalidate براش می‌فرسته و کلاینت مجبور میشه داده‌ای که خودش به‌روز کرده رو حذف کنه. خب این یه پارادوکس اذیت‌کننده‌س!حالا اگه NOLOOP فعال باشه (مثلاً با دستور CLIENT TRACKING ON NOLOOP)، سرور دیگه این پیام رو برای کلاینتی که تغییر رو ایجاد کرده، نمی‌فرسته.ادغام با RESP2 و RESP3: دو دنیای متفاوتاین فیچر Tracking با هر دو پروتکل RESP2 و RESP3 سازگاره، ولی تجربه استفاده از هر کدومشون خیلی فرق داره:پروتکل RESP2: این پروتکل قدیمی، مالتی‌پلکسینگ رو پشتیبانی نمی‌کنه، به همین دلیل Redis از دو کانکشن استفاده می‌کنه: یکی برای داده‌ها (مثل GET) و یکی برای پیام‌های invalidation. کانکشن دوم از Pub/Sub استفاده می‌کنه و به کانال __redis__:invalidate وصل میشه. پیام‌ها فقط به کلاینت مشخص شده با REDIRECT می‌رسن. ولی این مدل پیچیدگی‌هایی مثل مدیریت دو سوکت و احتمال بروز race conditions رو به همراه داره. برای کلاینت‌های قدیمی که هنوز به RESP3 مهاجرت نکردن، این روش کار می‌کنه، ولی می‌تونه دست‌وپاگیر باشه.پروتکل RESP3: پروتکل جدید Redis 6 با پشتیبانی از مالتی‌پلکسینگ و push messages، همه چیز رو خیلی ساده‌تر می‌کنه. شما می‌تونید از یک کانکشن برای داده‌ها و پیام‌های invalidation استفاده کنید، دیگه نیازی به Pub/Sub یا کانکشن اضافی نیست. وقتی کلیدی invalid می‌شه، پیام به‌صورت push توی همون کانکشن میرسه و ترتیب پیام‌ها هم تضمین‌شده‌ست که این به جلوگیری از مشکلات race conditions کمک می‌کنه. البته اگه بخواید، هنوز می‌تونید از دستور REDIRECT برای استفاده از دو کانکشن جداگانه استفاده کنید، ولی این دیگه اختیاریه.</description>
                <category>بهرام انیژ</category>
                <author>بهرام انیژ</author>
                <pubDate>Sat, 22 Mar 2025 23:29:35 +0330</pubDate>
            </item>
                    <item>
                <title>تجربه استفاده از In-Memory Cache به جای Distributed Cache</title>
                <link>https://virgool.io/@bahramanizh/%D8%AA%D8%AC%D8%B1%D8%A8%D9%87-%D8%A7%D8%B3%D8%AA%D9%81%D8%A7%D8%AF%D9%87-%D8%A7%D8%B2-in-memory-cache-%D8%A8%D9%87-%D8%AC%D8%A7%DB%8C-distributed-cache-ns7xam2ux5jm</link>
                <description>در بعضی از پروژه‌ها، وقتی بحث کش کردن (Caching) پیش میاد، اولین چیزی که به ذهن می رسد، استفاده از یک کش توزیع‌شده مثل  memcached یا redis هست. ولی در این پروژه، تصمیم گرفتم بدون استفاده از  هیچ سیستم کش خارجی دیگری، cache رو مستقیماً داخل خود اپلیکیشن و به‌صورت In-Memory پیاده‌سازی کنم. دلیلشم این بود که هم تاخیر شبکه (Latency) کمتر بشه، هم وابستگی به ابزارهای خارجی نداشته باشیم.پیاده‌سازی اولیه با In-Memory Cacheبا توجه به اینکه برخی از داده‌های پروژه نرخ تغییر پایینی داشتند، تصمیم گرفتم این داده‌ها رو به‌صورت In-Memory در هر Instance ذخیره کنم. این روش باعث بهبود عملکرد خواندن داده‌ ها شد و بعد از آن دیگر نیازی به دریافت داده از Central cache و  ایجاد ریکوست اضافه روی Network نبود.چالش‌های In-Memory Cache در محیط توزیع‌شده۱. مصرف حافظهبرای هندل کردن load، ما اپلیکیشن را scale کرده بودیم و در نتیجه داده ها به صورت مجزا روی Instance های مختلف نگهداری میشد. یعنی اگه حجم داده‌های کش‌شده مثلاً ۲ مگابایت بود و ۴ Instance داشتیم، در مجموع ۸ مگابایت حافظه برای این داده‌ها مصرف می‌شد. در حالی که اگر از یک کش مرکزی استفاده می‌کردیم، فقط ۲ مگابایت حافظه مصرف می‌شد.۲. ناسازگاری داده‌ها هنگام به‌روزرسانییکی از مشکلات اصلی این بود که وقتی داده‌ها آپدیت می‌شدند، این تغییرات باید روی همه‌ی Instanceها اعمال می‌شد. اما از آنجایی که هر Instance کش مخصوص به خودش رو داشت، وقتی داده‌ها به‌روزرسانی می‌شدند، فقط کش همون Instanceای که درخواست بهش رسیده بود آپدیت می‌شد و بقیه‌ی Instanceها هنوز داده‌های قدیمی رو نگه می‌داشتن. این قضیه باعث می‌شد که داده‌ها بین Instanceها ناسازگار بشن (inconsistency).۳. پاک شدن cache بعد از هربار ریست اپلیکیشن‌وقتی اپلیکیشن‌ها به هر دلیلی ری‌استارت می‌شدن، کش In-Memory پاک می‌شد و تا وقتی که کش دوباره ساخته بشه، ممکن بود تجربه کاربر رو خراب کنه و فشار زیادی هم به دیتابیس وارد کنه. برای اینکه این مشکل پیش نیاد، نیاز بود از روش‌هایی مثل cache warming استفاده کنیم تا کش از قبل آماده باشه.روش‌های هماهنگ‌سازی داده‌ها بین instance ها۱. استفاده از Message Broker برای هماهنگ‌سازی Cacheیکی از روش‌هایی که می‌شد امتحان کرد این بود که وقتی داده‌ها آپدیت می‌شدن، یه پیام توی Message Broker (مثلاً Kafka یا RabbitMQ یا همچنان Redis به صورت pub/sub ) فرستاده بشه. بعد بقیه instanceها این پیام رو دریافت کنن و cache خودشون رو آپدیت کنن.این روش چند تا مزیت داشت:- کاهش بار روی Network: دیگه لازم نبود برای گرفتن داده‌ها به یک تردپارتی روی شبکه ریکوست بزنیم.- بهینه برای بارهای read-heavy: از اونجایی که تعداد درخواست‌های خواندن خیلی بیشتر از درخواست‌های نوشتن بود، این روش عملکرد بهتری داشت.اما این روش معایب خودش رو هم داشت:- وابستگی به یک ابزار جدید: برای هماهنگ‌سازی کش‌ بین اینستنس‌ها، باید RabbitMQ یا Kafka رو راه‌اندازی و نگهداری می‌کردیم که این برخلاف هدف اولیه‌مون (کاهش وابستگی به ابزارهای خارجی) بود.- پیچیدگی پیاده‌سازی مکانیزم فالبک (Fallback): نیاز بود که مکانیزم فالبک پیاده‌سازی کنیم تا مطمئن بشیم پیام‌های آپدیت ِداده، همیشه به درستی منتشر می‌شن و هیچ چیزی از دست نمیره.۲. هماهنگ‌سازی با فراخوانی مستقیم بین اینستنس‌هایکی از روش‌های دیگه این بود که وقتی داده‌ها به‌روزرسانی می‌شد، بقیه‌ی اینستنس‌ها رو از طریق API فراخوانی کنیم تا کش خودشون رو به‌روزرسانی کنن. ولی این روش دو مشکل مهم داشت:- افزایش پیچیدگی مدیریت cache: هماهنگ کردن و نگهداری این سیستم خیلی سخت بود.- افزایش latency و سربار شبکه: درخواست‌های بین اینستنس‌ها باعث می‌شد تا latency بیشتر بشه و کارایی سیستم پایین بیاد.۳. استفاده از job ها برای به‌روزرسانی Cacheیک روش دیگه این بود که یک job به صورت دوره‌ای (مثلا هر ۵ دقیقه یکبار) روی هر اینستنس اجرا بشه و  داده‌های جدید یا آپدیت شده رو از دیتابیس بخونه و کش خودش رو به‌روزرسانی کنه. مزایا:- عدم نیاز به ابزار جانبی- مدیریت ساده‌تر به‌روزرسانی cache: مدیریت کش راحت‌تر می‌شد چون دیگه نیازی به ارسال پیام بین اینستنس‌ها نبود.اما این روش هم مشکلات خودش رو داشت:- تاخیر در به‌روزرسانی داده‌ها: چون به‌روزرسانی کش به صورت دوره‌ای انجام می‌شد، ممکن بود مثلاً حداکثر ۵ دقیقه اختلاف داده بین اینستنس‌ها وجود داشته باشه.- افزایش سربار روی دیتابیس: هر اینستنس مجبور بود به صورت دوره‌ای روی دیتابیس کوئری بزنه که این می‌تونست بار زیادی روی دیتابیس ایجاد کنه.در صورتی که Eventual Consistency را قبول کنیم و برامون Availability مهمتر باشه، روش Job دوره‌ای می‌تونه تا حد بسیار خوبی قابل قبول باشه، اما اگر بخواهیم Strong Consistency داشته باشیم، نیاز به روش‌های بهتری داریم .نتیجه‌گیری و بازگشت به Distributed Cacheبا در نظر گرفتن تمام این چالش‌ها، در نهایت تصمیم گرفتم که از یک کش مرکزی مثل Redis استفاده کنم. این تصمیم به چند دلیل گرفته شد:- مدیریت ساده‌تر cache: با استفاده از کش توزیع‌شده دیگه نیازی به هماهنگ‌سازی دستی بین اینستنس‌ها نبود.- حفظ یکپارچگی داده‌ها: همه اینستنس‌ها به یک منبع داده یکسان دسترسی داشتن.- بهینه‌سازی مصرف حافظه: به جای اینکه داده‌ها جداگانه تو هر اینستنس ذخیره بشن، داده‌ها فقط یه بار تو Redis ذخیره می‌شدن.استفاده از In-Memory Cache تجربه جالبی بود، اما در نهایت Distributed Cache انتخاب بهتری برای این سناریو محسوب می‌شود. با این حال، در برخی موارد خاص، In-Memory Cache همچنان می‌تواند مزایای خود را داشته باشد، به‌ویژه زمانی که تنها یک Instance در حال اجرا باشد، یا نرخ تغییر داده‌ها بسیار پایین باشد، یا زمانی که Latency برای سیستم حیاتی باشد.همچنین، استفاده ترکیبی از این دو روش نیز می‌تواند مفید باشد. به عنوان مثال، می‌توان از In-Memory Cache به عنوان L1 Cache و از Redis به عنوان L2 Cache استفاده کرد. در این روش، داده‌هایی که بیشترین درخواست را دارند، ابتدا در حافظه هر Instance ذخیره می‌شوند (L1 Cache) و در صورت نیاز به داده‌های جدید، از Redis (L2 Cache) بازیابی می‌شوند. این رویکرد می‌تواند تعادل مناسبی بین سرعت دسترسی بالا و هماهنگی داده‌ها برقرار کند.نظر شما چیه؟ تجربه مشابهی تو استفاده از In-Memory Cache داشتید؟</description>
                <category>بهرام انیژ</category>
                <author>بهرام انیژ</author>
                <pubDate>Sun, 02 Mar 2025 17:01:35 +0330</pubDate>
            </item>
                    <item>
                <title>تفاوت معماری Microservices و SOA - مقدمه</title>
                <link>https://virgool.io/@bahramanizh/%D8%AA%D9%81%D8%A7%D9%88%D8%AA-%D9%85%D8%B9%D9%85%D8%A7%D8%B1%DB%8C-microservices-%D9%88-soa-%D9%85%D9%82%D8%AF%D9%85%D9%87-siqshibbkfry</link>
                <description>Pieter Jansz. saenredam (Dutch 1597  -1665) The Interior of St.Bavo Haarlem 1628 Oil on panel (source: The j. Paul Getty Museum, Los Angeles)این پست براساس کتابی به اسم  microservices-vs-service-oriented-architecture نوشته شده است.یادگیری تفاوت های اصلی بین این دو معماری، می تواند منجر به انتخاب درست با توجه به شرایط خاص پروژه شما شود.دنیای معماری های مبتنی بر سرویسهر دو معماری microservice و  soa مبتنی بر سرویس هستند. به این معنی که این الگوهای معماری تاکید زیادی روی سرویس به عنوان جزء اصلی معماری برای پیاده سازی کارهای تجاری و غیرتجاری دارند. اگرچه microservice و soa تفاوت های زیادی باهم دارند اما ویژگی های مشترک بسیاری هم دارند. یک چیز مشترکی که معماری های مبتنی بر سرویس دارند، معماری توزیع شده اونهاست. به این معنی که سرویس ها می توانند از طریق rest ، soap ، AMQP,JMS,MSMQ,RMI با هم ارتباط برقرار کنند. معماری های توزیع شده مزایای قابل توجهی نسبت به معماری های یکپارچه ( monolithic) و مبتنی بر لایه ها دارند، از جمله مقیاس پذیری بهتر، جداسازی بهتر و کنترل بهتر روی توسعه ، تست و استقرار.اجزا در یک معماری توزیع شده تمایل بیشتری به خودمختاری دارند که کنترل بهتر روی تغییرات و نگهداری ساده تر را فراهم میکند و این به نوبه خود منجر به برنامه های قوی تر و واکنش پذیر می شود.معماری توزیع شده کمک میکنه به وابستگی کمتر و  modular بودن برنامه.در زمینه معماری های مبتنی بر سرویس modularity روشی است برای  encapsulating   بخشهایی از برنامه که منجر به داشتن یک سرویس خود مختار می شود که می توانند با کمترین وابستگی یا بدون وابستگی به سایر اجزا یا سرویس های داخل برنامه به صورت جداگانه ، توسعه ، تست و استقرار شود.معماری های ماژولار همچنین از مفهوم بازنویسی برای نگهداری حمایت میکند و اجازه میدهد بازنویسی ( refactoring) یا جایگزینی در قسمت های کوچکتر و در طول زمان رشد پروژه باشد (بر خلاف بازنویسی و جایگزینی یک اپلیکیشن با روش BIG-BANG)در مورد مزایای معماری توزیع شده اعتراضی نیست اما برای پیاده سازی آن یک سری هزینه و چالش پیچیده وجود دارد. انتخاب پروتکل مناسب برای ارتباط سرویس ها ، حفظ و نگهداری قرارداد سرویس ها ، تامین امنیت ارتباط سرویس ها ، مدیریت تراکنش های توزیع شده و نحوه برخورد با سرویس غیرپاسخگو و غیرقابل دسترس، تنها بخشی از مسائل پیچیده ای هستند که در هنگام پیاده سازی معماری مبتنی بر سرویس با ان روبرو می شویم.در ادامه هر کدام از این چالش ها را شرح می دهیم.قراردادهای سرویس (service contracts)قرارداد سرویس یک توافق بین سرویس و سرویس مشتری (client) است که تعیین میکند فرمت ورودی و خروجی دیتا به چه صورت باشد. (json, xml, java object,....)ایجاد و نگهداری این قرارداد یک کار دشوار است که نباید به آسانی پذیرفته و یا به صورت یک کاری که بعداً در موردش فکر میکنیم تلقی شود. به همین ترتیب این موضوع، مستلزم توجه ویژه ای در زمینه معماری مبتنی بر سرویس است.در معماری مبتنی بر سرویس شما می توانید از دو نوع مدل قرارداد سرویس استفاده کنید: قراردادهای مبتنی بر سرویس  (خدمات دهنده) و قراردادهای مبتنی بر مشتری (مصرف کننده یا سرویس مشتری یا کلاینت).  تفاوت واقعی بین این مدل قراردادها، درجه همکاری است. در قرارداد مبتنی بر سرویس (خدمات دهنده) ، سرویس تنها مالک قرارداد و به طور کلی آزاد است هر تغییری را در قرارداد بدون نیاز به فهمیدن نظر مصرف کننده  (سرویس مشتری یا کلاینت) ایجاد کند. این مدل، مصرف کنندگان را مجبور میکند تغییرات جدید قرارداد را اعمال کنند. چه یک مصرف کننده به آن قابلیت نیاز داشته باشد و یا نداشته باشد.از سوی دیگر، قراردادهای مبتنی بر مصرف، مبتنی بر ارتباط نزدیک بین سرویس و مصرف کنندگان خدمات است. با استفاده از این مدل همکاری قوی بین صاحب سرویس و مصرف کنندگان سرویس وجود دارد به طوری که نیازهای مصرف کنندگان سرویس با توجه به قراردادهایی که به آنها متصل می شوند، مورد توجه قرار می گیرند. این مدل به طور کلی نیاز به این دارد که سرویس بداند چه کسانی مصرف کننده اون هستند و هر کدام از این مصرف کننده ها چگونه از سرویس استفاده می کنند. مشتریان سرویس آزاد هستند که پیشنهادات تغییرات در قرارداد خدماتی را ارائه دهند، که سرویس دهنده می توانند بسته به اینکه چه تاثیری روی بقیه مصرف کنندگان می گذارد، پیشنهاد را پذیرفته یا رد کنند. در یک سناریوی کامل، مصرف کننده سرویس، تست های خود را به صاحب سرویس ارائه می دهند تا در صورتی که یک مصرف کننده تغییری را پیشنهاد کند، تست ها می توانند انجام شوند تا ببینند آیا این تغییر مصرف کننده خدمات دیگری را مختل می کند. ابزارهای متن باز مانند pact و pacto می توانند به حفظ و تست قراردادهای مبتنی بر مصرف کمک کنند.ادامه دارد...</description>
                <category>بهرام انیژ</category>
                <author>بهرام انیژ</author>
                <pubDate>Sat, 04 Jan 2020 00:39:56 +0330</pubDate>
            </item>
            </channel>
</rss>