ردیابی ۳۲ میلیثانیه تأخیر اضافه در ClickHouse
خلاصهٔ کاملتر
تیم ClickHouse تو این پست تعریف میکنه که یکی از مشتریهاشون حدود ۳۲ میلیثانیه تأخیر اضافه روی کوئریهای ساده SELECT میدید. فاصله شبکه کلاینت و سرور حدود ۳۲ میلیثانیه بود. ولی هر وقت اتصال ۲ ثانیه یا بیشتر بیکار میموند، کوئری بعدی حدود ۶۴ میلیثانیه طول میکشید، یعنی دو برابر RTT (زمان رفتوبرگشت یه بسته).
اول باید بدونیم Native protocol کلیکهاوس چطوری کار میکنه. کلاینت Query رو میفرسته، سرور ردیفهای جواب رو تو بستههای Data میفرسته و بعدش چند تا بسته متادیتا مثل ProfileEvents، Progress و Logs میده. آخر سر هم EndOfStream میاد. کلاینت تا این بسته آخر نرسه، کوئری رو تمومشده حساب نمیکنه.
طرف دیگه ماجرا TCP هست. congestion window یا cwnd (یعنی سقف دادهای که سرور میتونه بدون گرفتن ACK بفرسته) کمکم بزرگ میشه. ولی لینوکس بعد از یه مدت بیکاری که بهش RTO میگن، اونو برمیگردونه به مقدار اولیه یعنی initcwnd. این مقدار معمولاً ۱۰ هست و برای این مشتری ۸ بود. از اون طرف ClickHouse گزینه TCP_NODELAY رو روشن میکنه، یعنی الگوریتم Nagle (که نوشتنهای کوچیک رو جمع میکنه و یهجا میفرسته) خاموشه و هر write جدا یه بسته TCP جدا میشه.
ترکیب این دوتا مشکل رو ساخت. سرور بعد از بیکاری فقط ۸ بسته میتونست بفرسته. بعدش باید منتظر ACK کلاینت میموند و همین یه رفتوبرگشت کامل اضافه خرج داشت. تیم مشکل رو با network namespace لینوکس، ابزار tc برای شبیهسازی تأخیر ۳۲ میلیثانیه و یه برنامه Go دقیق بازسازی کرد. تو این تست، tail (فاصله اولین ردیف تا EndOfStream) هر بار حدود ۳۲ میلیثانیه بود.
چند تا راهحل بود: خاموش کردن ProfileEvents، تنظیم tcp_slow_start_after_idle=0، بزرگتر کردن initcwnd، استفاده از TCP_CORK یا دستهبندی بستهها تو خود برنامه. مشتری روی سه تای اول کنترلی نداشت و TCP_CORK هم مخصوص لینوکسه و کنترلو کامل میده دست کرنل. پس تیم بستههای متادیتای آخر کوئری رو تو یه write جمع کرد. فقط Progress جدا موند، چون باید سریع به کلاینت برسه تا پیشرفت واقعی کوئری رو نشون بده.
نتیجه: تعداد بستههای TCP برای همون کوئری از ۵۰ به ۲۵ رسید و tail از حدود ۳۲ میلیثانیه به حدود ۱ میلیثانیه افتاد. نویسنده جمعبندی میکنه که تأخیر بالا همیشه تقصیر دیتابیس کند نیست. گاهی مشکل تو برخورد flushهای برنامه با رفتار TCP کرنل قایم شده.
نکات کلیدی:
- بعد از بیکاری اتصال، لینوکس cwnd رو به initcwnd برمیگردونه (اینجا ۸ بسته)
- با TCP_NODELAY هر write کوچیک یه بسته TCP جدا میشه
- مشکل روی نسخههای قبل از 26.9 بازسازی شد
- بعد از پچ، بستهها از ۵۰ به ۲۵ و tail از ۳۲ به حدود ۱ میلیثانیه رسید
- بسته Progress عمداً دستهبندی نمیشه چون حساس به زمانه




