چرا این موضوع مهم است؟
Configuration HAProxy از global/defaults تا frontend و backend، مسئولیتها را جدا میکند. frontend تصمیم میگیرد connection چگونه پذیرفته و route شود و backend pool و policy ارسال request را تعریف میکند.
در یک سیستم واقعی، قبل از انتخاب ابزار باید مدل ذهنی درستی از لایهها داشته باشید. هر بار که یک مشکل را فقط با یک command یا تنظیم حفظی حل میکنیم، دانش به نسخه و محیط خاص گره میخورد. روش بهتر این است که ورودی، state، policy، مسیر داده و خروجی را مشخص کنیم. بعد ابزار را برای مشاهدهٔ یکی از این لایهها انتخاب کنیم. این رویکرد باعث میشود وقتی نسخه یا توزیع عوض شد، هنوز بدانیم دنبال چه evidenceای هستیم.
مدل معماری
در موضوع طراحی load balancing با HAProxy و تفکیک frontend/backend، componentها را روی یک مسیر ساده بکشید و مرز مسئولیت هرکدام را مشخص کنید. state پایدار، state runtime، dependency بیرونی و نقطهٔ اعمال policy را جدا کنید. وقتی diagram ساده دارید، خطا از یک «سیستم بزرگ و مبهم» به چند مرحلهٔ قابل آزمون تبدیل میشود.
روش عملی کار
Timeoutهای connect/client/server را صریح تنظیم کنید، health check را به endpoint معنادار وصل کنید و log format را قبل از incident قابل جستوجو کنید. config validation باید قبل از reload اجباری باشد.
برای Production، تغییر باید قابل اندازهگیری و قابل برگشت باشد. baseline قبل از تغییر شامل نسخه، config مؤثر، log سالم، metric اصلی و یک تست end-to-end است. rollout را با canary شروع کنید؛ بعد از هر مرحله هم تست مثبت و هم تست منفی داشته باشید. اگر success criterion از قبل نوشته نشده باشد، بعد از اجرا بهسادگی هر نتیجهای را میتوان «موفق» تفسیر کرد. rollback هم باید تمرین شود، نه اینکه فقط یک دستور روی کاغذ باشد.
از دید امنیت
Health check فقط TCP open اگر application خراب باشد false healthy میدهد. timeoutهای بینهایت، sticky session بدون نیاز و reload بدون validation نیز failure متداولاند.
از دید امنیت، کمکردن سطح دسترسی مهم است اما availability و قابلیت عیبیابی هم جزو امنیت عملیاتیاند. secretها، tokenها، capabilityها، network path و logهای حساس را جدا بررسی کنید. هر exception باید owner، دلیل و تاریخ بازبینی داشته باشد. کنترلهایی که هیچکس نمیتواند اثرشان را مشاهده یا علتشان را توضیح دهد، در زمان incident به مانع تبدیل میشوند.
آزمایشگاه پیشنهادی
دو backend ساده بسازید، یکی را degraded کنید و رفتار health check را ببینید. سپس config اشتباه تولید و با haproxy -c قبل از reload آن را متوقف کنید.
برای هر مرحله timestamp و خروجی را ذخیره کنید. یک failure کنترلشده ایجاد کنید تا مطمئن شوید monitoring واقعاً failure را میبیند. سپس سیستم را به baseline برگردانید و زمان recovery را ثبت کنید.
عیبیابی بر پایهٔ Evidence
Troubleshooting را با تغییر config شروع نکنید. اول محل دقیق failure را پیدا کنید: آیا ورودی وارد سیستم میشود؟ آیا state مورد انتظار ساخته شده؟ آیا policy اجازه میدهد؟ آیا خروجی از component خارج میشود؟ log، packet capture، metric و state API باید به یک timeline مشترک وصل شوند. نبودن event نیز evidence است؛ اگر request قبل از یک component دیده میشود و بعد از آن نه، دامنهٔ مسئله کوچک شده است.
Performance و ظرفیت
عملکرد را فقط با average نسنجید. p95/p99 latency، saturation، queue depth، retry و resource pressure تصویر بهتری میدهند. تغییری که در lab کوچک سریع است ممکن است زیر concurrency یا dataset واقعی bottleneck متفاوتی نشان دهد. capacity limitها را قبل از rollout بزرگ شناسایی کنید و alert را به thresholdهای قابل اقدام وصل کنید.
Production readiness
قبل از rollout نهایی، owner، change window، blast radius، success metric و rollback trigger باید مشخص باشند. اگر dependency خارجی دارید، رفتار هنگام timeout یا unavailable شدن آن را تست کنید. اگر config به چند host میرود، rollout مرحلهای و inventory دقیق لازم است. یک تغییر خوب باید observable، explainable و reversible باشد.
Runbook و انتقال دانش
در مستندسازی، فقط «چه دستورهایی اجرا شد» را ننویسید. سؤال، فرضیه، evidence مورد انتظار، نتیجه و دلیل تصمیم را ثبت کنید. اگر همکار دیگری بتواند با همان ورودی آزمایش را تکرار کند و به نتیجهٔ مشابه برسد، runbook ارزش عملیاتی دارد. اگر فقط نویسنده میداند چرا مرحلهای لازم است، دانش هنوز منتقل نشده است.
قبل از Production چه چیزی را اعتبارسنجی کنیم؟
نتیجهٔ Lab را مستقیم به Production تعمیم ندهید. یک change plan کوتاه بسازید که در آن دامنهٔ تغییر، owner، زمان اجرا، dependencyها، success metric و rollback trigger مشخص باشد. ابتدا روی canary یا درصد کوچکی از workload اجرا کنید و حداقل یک چرخهٔ واقعی restart، credential rotation یا deploy دوم را ببینید؛ بسیاری از خطاها در اولین اجرای سالم ظاهر نمیشوند. رفتار هنگام unavailable شدن dependency خارجی، پر شدن disk، timeout شبکه و ورودی نامعتبر را نیز بررسی کنید. اگر feature امنیتی است، test منفی باید نشان دهد مسیر غیرمجاز واقعاً بسته است؛ اگر feature performance است، benchmark باید با load و dataset نزدیک به واقعیت اجرا شود. در پایان، تصمیم rollout را با evidence ثبت کنید، نه با جملهٔ مبهم «بهنظر خوب است».
نگهداری بلندمدت و بازبینی دورهای
معماری بعد از deploy ثابت نمیماند. نسخهٔ سیستمعامل، dependency، policy سازمان، certificate، endpoint و حجم workload تغییر میکنند. برای همین باید بازبینی دورهای تعریف شود: نسخههای پشتیبانینشده را پیدا کنید، exceptionهای قدیمی را ببندید، alertهای بیارزش را حذف کنید و recovery را دوباره تمرین کنید. اگر configuration در Git نگهداری میشود، drift میان state واقعی و repository را اندازه بگیرید. اگر ابزار SaaS یا سرویس خارجی است، تغییر API، pricing، limit و مدل permission را دنبال کنید. مستندات باید تاریخ review داشته باشند تا تیم بداند کدام فرضها هنوز معتبرند. نگهداری خوب یعنی سیستم بعد از شش ماه هم قابل توضیح و قابل بازیابی باشد، نه اینکه فقط روز اول deploy موفق بوده باشد.
جمعبندی
هدف این مقاله دربارهٔ طراحی load balancing با HAProxy و تفکیک frontend/backend ارائهٔ یک دستور جادویی نیست؛ هدف ساختن مدل ذهنی است. وقتی معماری را بفهمید، baseline داشته باشید و تغییر را با evidence کوچک کنید، ابزارها قابل تعویض میشوند اما روش حل مسئله باقی میماند. منبع اصلی این مقاله از HAProxy انتخاب شده تا جزئیات فنی قابل پیگیری باشند.
چکلیست سریع
- مسئله را در یک جمله تعریف کن.
- component و مرز اعتماد را مشخص کن.
- baseline را قبل از تغییر ذخیره کن.
- یک test مثبت و یک test منفی بساز.
- تغییر را با canary شروع کن.
- evidence را timestampدار نگه دار.
- rollback را واقعاً تمرین کن.
- نتیجه و exceptionها را در runbook ثبت کن.
خواندن پیکربندی از ورودی تا مقصد
در HAProxy بخش frontend نقطه دریافت اتصال و تصمیم اولیه مسیریابی است و backend مجموعه مقصدها و قواعد ارسال را توصیف میکند. بخش defaults مقادیری را به بخشهای بعدی منتقل میکند و global به رفتار سطح فرایند مربوط است. برای بازبینی یک درخواست، از نشانی و پورتی شروع کنید که کاربر به آن وصل میشود، سپس شرط انتخاب backend و در پایان مقصد انتخابشده را دنبال کنید. صرف وجود نام یک backend در فایل ثابت نمیکند مسیر مورد نظر واقعاً به آن میرسد.
فرض کنید سایت عمومی و API پشت یک ورودی قرار دارند. یک شرط مبتنی بر میزبان میتواند آنها را جدا کند، اما باید رفتار میزبان ناشناخته و درخواست بدون مقدار مورد انتظار نیز مشخص باشد. backend پیشفرض را آگاهانه انتخاب کنید. اگر همه ورودیهای ناشناخته به سرویس حساس هدایت شوند، خطای تایپی در شرط تبدیل به دسترسی ناخواسته میشود. برای هر شرط، نمونه مطابق، نمونه نامطابق و نمونه مرزی بسازید و با همان نسخه پیکربندی آزمایش کنید.
تفاوت عبور TCP و پردازش HTTP
در حالت عبور TCP، پراکسی جریان بایت را جابهجا میکند و نباید انتظار همان تصمیمهای مبتنی بر مسیر HTTP را داشت. اگر TLS در HAProxy خاتمه یابد، مسئولیت گواهی، کلید خصوصی و انتقال امن یا کنترلشده به backend نیز به طراحی اضافه میشود. اگر TLS تا مقصد عبور کند، دید و گزینههای مسیریابی متفاوت خواهند بود. این انتخاب را قبل از نوشتن ACL مشخص کنید؛ بسیاری از تنظیمات ظاهراً درست در لایهای قرار میگیرند که داده مورد نیاز را نمیبیند.
نشانی اصلی کاربر نیز باید قراردادی روشن داشته باشد. دریافت یک header با نام متداول از اینترنت، آن را قابل اعتماد نمیکند. زنجیره پراکسیهای مورد اعتماد و محل بازنویسی یا افزودن اطلاعات باید معلوم باشد تا برنامه پاییندستی header جعلی را هویت واقعی کاربر فرض نکند. همین مسئله روی ثبت رخداد، محدودسازی نرخ و تصمیم امنیتی اثر میگذارد. یک درخواست آزمایشی با header ساختگی بفرستید و ببینید برنامه دقیقاً چه نشانیای را ثبت میکند.
Health check باید چه چیزی را ثابت کند؟
باز بودن پورت نشان میدهد listener پاسخ میدهد، اما لزوماً به معنی آماده بودن برنامه برای درخواست واقعی نیست. در مقابل، بررسی بسیار سنگین سلامت میتواند خودش به پایگاه داده فشار بیاورد. endpoint سلامت را با نقش آن طراحی کنید: آیا صرف زنده بودن فرایند کافی است یا باید توان خدمترسانی را بررسی کند؟ وابستگی نامطمئن نباید بیدلیل همه backendها را همزمان از مدار خارج کند. برای هر نتیجه check، رفتار مورد انتظار را به زبان برنامه بنویسید.
آستانه خروج و بازگشت باید نوسان گذرا را از خرابی پایدار جدا کند. اگر یک پاسخ کند فوراً مقصد را حذف کند و پاسخ بعدی فوراً آن را برگرداند، ترافیک بین گرهها نوسان میکند. هنگام آزمایش، خرابی کوتاه و خرابی طولانی را جدا ایجاد کنید. ثبت کنید چه زمانی مقصد از انتخاب تازه خارج میشود و اتصالهای موجود چه سرنوشتی دارند. خروج از چرخش و قطع همه اتصالها یک عملیات واحد نیستند.
Timeout، retry و درخواستهای تکراری
Timeout اتصال به مقصد، زمان انتظار پاسخ و عمر اتصال کلاینت معانی متفاوت دارند. انتخاب عدد بزرگ برای همه آنها خطا را حل نمیکند؛ فقط منابع بیشتری را مدت طولانی نگه میدارد. ابتدا بودجه زمانی کل درخواست را مشخص کنید و آن را میان لایهها تقسیم کنید. اگر برنامه پس از timeout پراکسی همچنان عملیات را انجام دهد، کاربر ممکن است دوباره تلاش کند و اثر تجاری تکرار شود. برای عملیات نویسنده، قرارداد idempotency برنامه از تنظیم پراکسی جداست.
Retry را نیز به نوع شکست و نوع درخواست مرتبط کنید. ناموفق بودن دریافت پاسخ اثبات نمیکند مقصد هیچ کاری انجام نداده است. در آزمون یک عملیات آزمایشی، شناسه یکتا را تا backend دنبال کنید و تعداد اجرای واقعی را بشمارید. این کار تفاوت بهبود دسترسپذیری و تکثیر ناخواسته عملیات را روشن میکند.
Reload با شاهد قابل بررسی
پیش از reload، اعتبار نحوی پیکربندی را با همان باینری نصبشده بررسی کنید و از فایلهای گواهی و مسیرهای مورد اشاره مطمئن شوید. سپس یک اتصال کوتاه و یک اتصال طولانی آزمایشی را هنگام تغییر مشاهده کنید. عبارت reload موفق فقط شروع بررسی است؛ باید معلوم شود فرایند جدید پیکربندی تازه را گرفته و فرایند قبلی چگونه اتصالهای باقیمانده را تمام میکند. اگر منابع یا اتصالها باقی میمانند، قبل از rollout بعدی علت را روشن کنید تا نسلهای قدیمی روی هم انباشته نشوند.