پرش به محتوای اصلی
OpenAI

۲ بهمن ۱۴۰۴

مهندسی

مقیاس‌دهی PostgreSQL برای پشتیبانی از ۸۰۰ میلیون کاربر ChatGPT

توسط Bohan Zhang، عضو کادر فنی

در حال بارگذاری…

سال‌هاست که PostgreSQL یکی از حیاتی‌ترین سیستم‌های داده در پشت صحنه بوده که محصولات اصلی مانند ChatGPT و API OpenAI را قدرت می‌بخشد. با افزایش سریع پایگاه کاربران ما، تقاضا بر پایگاه‌های داده ما نیز به‌طور تصاعدی افزایش یافته است. در طول سال گذشته، بار کاری PostgreSQL ما بیش از ۱۰ برابر افزایش یافته است و همچنان به سرعت در حال افزایش است.

تلاش‌های ما برای ارتقای زیرساخت تولیدمان به‌منظور پشتیبانی از این رشد، بینش تازه‌ای را آشکار کرد: PostgreSQL می‌تواند به‌طور قابل‌اعتمادی مقیاس‌پذیر شود تا از بارهای کاری بسیار بزرگ‌ترِ خواندن‌محور نسبت به آنچه بسیاری پیش‌تر ممکن می‌دانستند، پشتیبانی کند. این سیستم (که در ابتدا توسط تیمی از دانشمندان در دانشگاه کالیفرنیا، برکلی ایجاد شد) به ما امکان داده است تا ترافیک عظیم جهانی را با یک Azure PostgreSQL flexible server instance(در یک پنجره جدید باز می‌شود) اصلی و نزدیک به ۵۰ رپلیکای خواندنی که در چندین منطقه در سراسر جهان پراکنده‌اند، پشتیبانی کنیم. این داستان چگونگی مقیاس‌پذیری PostgreSQL در OpenAI است تا از طریق بهینه‌سازی‌های دقیق و مهندسی مستحکم، از میلیون‌ها پرس‌وجو در ثانیه برای ۸۰۰ میلیون کاربر پشتیبانی کند؛ همچنین نکات کلیدی‌ای را که در طول مسیر آموختیم پوشش خواهیم داد.

ترک‌هایی در طراحی اولیه ما

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

ممکن است شگفت‌انگیز به نظر برسد که یک معماری تک‌اصلی بتواند نیازهای مقیاس OpenAI را برآورده کند؛ با این حال، عملی کردن این موضوع در عمل ساده نیست. ما چندین SEV ناشی از اضافه‌بار Postgres مشاهده کرده‌ایم و آن‌ها اغلب از یک الگوی مشابه پیروی می‌کنند: یک مشکل بالادستی باعث افزایش ناگهانی بار پایگاه داده می‌شود، مانند از دست‌رفتن گسترده کش به دلیل خرابی لایه کش، جهش در joinهای چندطرفه و پرهزینه که CPU را اشباع می‌کند، یا یک طوفان نوشتاری ناشی از راه‌اندازی یک ویژگی جدید. با افزایش استفاده از منابع، تأخیر پرس‌وجو افزایش می‌یابد و درخواست‌ها شروع به قطع شدن می‌کنند. تلاش‌های مجدد بار را بیشتر تشدید کرده و چرخه‌ای معیوب را ایجاد می‌کنند که می‌تواند به افت عملکرد کل خدمات ChatGPT و API منجر شود.

نمودار بارگذاری مقیاس

اگرچه PostgreSQL برای بارهای کاریِ خواندن‌محور ما به‌خوبی مقیاس‌پذیر است، اما همچنان در دوره‌های ترافیک بالای نوشتن با چالش‌هایی مواجه می‌شویم. این عمدتاً به دلیل پیاده‌سازی کنترل هم‌زمانی چندنسخه‌ای (MVCC) در PostgreSQL است که کارایی آن را برای بارهای کاری سنگین نوشتن کاهش می‌دهد. برای مثال، زمانی که یک پرس‌وجو یک تاپل یا حتی یک فیلد واحد را به‌روزرسانی می‌کند، کل ردیف کپی می‌شود تا یک نسخه جدید ایجاد گردد. در بارهای سنگین نوشتن، این امر منجر به افزایش قابل‌توجه نوشتن می‌شود. این همچنین تقویت خواندن را افزایش می‌دهد، زیرا پرس‌وجوها باید برای بازیابی جدیدترین نسخه، از میان چندین نسخهٔ تاپل (تاپل‌های مرده) جستجو کنند. MVCC چالش‌های اضافی‌ای مانند تورم جدول و شاخص، افزایش سربار نگهداری شاخص، و تنظیم پیچیده autovacuum را معرفی می‌کند. (شما می‌توانید یک بررسی عمیق درباره این مسائل را در وبلاگی که با پروفسور اندی پاولو در دانشگاه کارنگی ملون نوشته‌ام با عنوان بخشی از PostgreSQL که بیش از همه از آن متنفر هستیم(در یک پنجره جدید باز می‌شود) پیدا کنید؛ ذکرشده(در یک پنجره جدید باز می‌شود) در صفحه ویکی‌پدیای PostgreSQL.)

مقیاس‌بندی PostgreSQL به میلیون‌ها QPS

برای کاهش این محدودیت‌ها و کم کردن فشار نوشتن، ما مهاجرت کرده‌ایم و همچنان به مهاجرت ادامه می‌دهیم، قابل تقسیم (یعنی بارهای کاری که می‌توانند به‌صورت افقی پارتیشن‌بندی شوند، بارهای کاری با نوشتن زیاد را به سیستم‌های شاردشده‌ای مانند Azure Cosmos DB منتقل کنید و منطق برنامه را بهینه کنید تا نوشتن‌های غیرضروری به حداقل برسد. ما همچنین دیگر اجازه افزودن جدول‌های جدید به پیاده‌سازی فعلی PostgreSQL را نمی‌دهیم. بارهای کاری جدید به‌صورت پیش‌فرض به سیستم‌های شاردشده اختصاص داده می‌شوند.

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

در بخش‌های بعدی، به چالش‌هایی که با آن‌ها روبه‌رو شدیم و بهینه‌سازی‌های گسترده‌ای که برای رفع آن‌ها و جلوگیری از قطعی‌های آینده اجرا کردیم می‌پردازیم؛ در این مسیر PostgreSQL را تا مرزهایش پیش بردیم و آن را تا میلیون‌ها پرس‌وجو در ثانیه (QPS) مقیاس‌بندی کردیم.

کاهش بار روی سرور اصلی

چالش: با تنها یک نویسنده، یک پیکربندی تک‌اصلی نمی‌تواند نوشتارها را در مقیاس بزرگ انجام دهد. افزایش ناگهانی در نوشتن می‌تواند به‌سرعت سرور اصلی را بیش از حد بارگذاری کند و بر سرویس‌هایی مانند ChatGPT و API ما تأثیر بگذارد.

راه‌حل: تا حد امکان بار روی سرور اصلی را—هم در خواندن و هم در نوشتن—به حداقل می‌رسانیم تا مطمئن شویم ظرفیت کافی برای مدیریت افزایش‌های نوشتن را دارد. ترافیک خواندن تا حد امکان به رپلیکاها منتقل می‌شود. با این حال، برخی از پرس‌وجوهای خواندن باید روی پایگاه داده اصلی باقی بمانند، زیرا بخشی از تراکنش‌های نوشتن هستند. برای آن‌ها، تمرکز ما بر این است که مطمئن شویم کارآمد هستند و از پرس‌وجوهای کند اجتناب کنیم. برای ترافیک نوشتن، بارهای کاری قابل شارد شدن و سنگین از نظر نوشتن را به سیستم‌های شاردشده‌ای مانند Azure CosmosDB منتقل کرده‌ایم. بارهای کاری که شارد کردن آن‌ها دشوارتر است اما همچنان حجم بالایی از نوشتن تولید می‌کنند، زمان بیشتری برای مهاجرت نیاز دارند و این فرایند هنوز ادامه دارد. ما همچنین برنامه‌های کاربردی خود را به‌طور تهاجمی بهینه‌سازی کردیم تا بار نوشتن را کاهش دهیم؛ برای مثال، اشکالات برنامه را که باعث نوشتن‌های تکراری می‌شدند برطرف کرده‌ایم و در موارد مناسب، نوشتن تنبل را معرفی کرده‌ایم تا اوج‌های ترافیک را هموار کنیم. علاوه بر این، هنگام پر کردن مجدد فیلدهای جدول، برای جلوگیری از فشار بیش از حد نوشتن، محدودیت‌های نرخ سخت‌گیرانه‌ای اعمال می‌کنیم.

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

چالش: ما چندین پرس‌وجوی پرهزینه را در PostgreSQL شناسایی کردیم. در گذشته، افزایش‌های ناگهانی حجم در این پرسش‌ها مقدار زیادی از CPU را مصرف می‌کرد و باعث کند شدن هم ChatGPT و هم درخواست‌های API می‌شد.

راه‌حل: چند کوئری پرهزینه، مانند آن‌هایی که بسیاری از جدول‌ها را به هم پیوند می‌دهند، می‌توانند به‌طور چشم‌گیری عملکرد را کاهش دهند یا حتی کل سرویس را از کار بیندازند. ما باید به‌طور مستمر پرس‌وجوهای PostgreSQL را بهینه‌سازی کنیم تا مطمئن شویم که کارآمد هستند و از الگوهای ضد رایج پردازش تراکنش آنلاین (OLTP) اجتناب کنیم. برای مثال، ما یک‌بار یک پرس‌وجوی بسیار پرهزینه را شناسایی کردیم که ۱۲ جدول را به هم متصل می‌کرد و جهش‌های این پرس‌وجو مسئول SEVهای با شدت بالا در گذشته بودند. باید تا حد امکان از پیوندهای پیچیده چندجدولی پرهیز کنیم. اگر پیوندها ضروری باشند، یاد گرفتیم که به جای آن، شکستن پرس‌وجو را در نظر بگیریم و منطق پیچیدهٔ پیوند را به لایهٔ برنامه منتقل کنیم. بسیاری از این پرسش‌های مشکل‌ساز توسط چارچوب‌های نگاشت شیء-رابطه‌ای (ORMs) تولید می‌شوند، بنابراین مهم است که SQL تولیدشده توسط آنها را با دقت بررسی نمایید و اطمینان حاصل کنید که همان‌طور که انتظار می‌رود عمل می‌کند. همچنین معمول است که در PostgreSQL پرس‌وجوهای بیکار طولانی‌مدت را بیابید. پیکربندی زمان‌های انتظار مانند idle_in_transaction_session_timeout ضروری است تا از مسدود شدن autovacuum جلوگیری شود.

کاهش خطر خرابی تک‌نقطه‌ای

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

راه‌حل: اکثر درخواست‌های حیاتی تنها شامل پرس‌وجوهای خواندن می‌شوند. برای کاهش نقطهٔ تک‌خرابی در سرور اصلی، آن خواندن‌ها را از نویسنده به رپلیکاها منتقل کردیم تا اطمینان حاصل شود که آن درخواست‌ها حتی اگر سرور اصلی از کار بیفتد هم می‌توانند به ارائهٔ سرویس ادامه دهند. در حالی که عملیات نوشتن همچنان ناموفق خواهد بود، تأثیر کاهش می‌یابد؛ دیگر SEV0 نیست، زیرا خواندن‌ها همچنان در دسترس هستند.

برای کاهش خرابی‌های اولیه، ما سامانه اصلی را در حالت دسترس‌پذیری بالا (HA) با یک آماده‌به‌کار داغ اجرا می‌کنیم؛ یک نسخه همگام‌سازی‌شده پیوسته که همیشه آماده است تا ارائه ترافیک را بر عهده بگیرد. اگر سرور اصلی از کار بیفتد یا نیاز باشد برای نگهداری از دسترس خارج شود، می‌توانیم به‌سرعت سرور آماده‌به‌کار را ارتقا دهیم تا زمان قطعی به حداقل برسد. تیم Azure PostgreSQL کار قابل‌توجهی انجام داده است تا اطمینان حاصل کند این failoverها حتی تحت بار بسیار بالا نیز ایمن و قابل‌اعتماد باقی بمانند. برای مدیریت خرابی‌های رپلیکای خواندن، ما در هر منطقه چندین رپلیکا را با ظرفیت مازاد کافی مستقر می‌کنیم تا اطمینان حاصل شود که خرابی یک رپلیکای واحد به قطعی منطقه‌ای منجر نشود.

ایزوله‌سازی بار کاری

چالش: ما اغلب با موقعیت‌هایی مواجه می‌شویم که برخی درخواست‌ها مقدار نامتناسبی از منابع را روی نمونه‌های PostgreSQL مصرف می‌کنند. این می‌تواند منجر به کاهش عملکرد برای سایر بارهای کاری در حال اجرا روی همان نمونه‌ها شود. برای مثال، راه‌اندازی یک ویژگی جدید می‌تواند کوئری‌های ناکارآمدی را معرفی کند که به‌شدت CPU PostgreSQL را مصرف کرده و باعث کند شدن درخواست‌ها برای سایر ویژگی‌های حیاتی شود.

راهکار: ما تقریباً ۵۰ رپلیکای خواندنی را در چندین منطقه جغرافیایی اجرا می‌کنیم تا تأخیر را به حداقل برسانیم. با این حال، با معماری فعلی، سرور اصلی باید WAL را به هر نسخه استریم کند. اگرچه در حال حاضر با انواع بسیار بزرگِ نمونه و پهنای باند بالای شبکه به‌خوبی مقیاس‌پذیر است، نمی‌توانیم بدون اینکه در نهایت نمونه اصلی را بیش از حد بارگذاری کنیم، بی‌نهایت رپلیکا اضافه کنیم. برای رسیدگی به این موضوع، ما با تیم Azure PostgreSQL روی تکثیر آبشاری(در یک پنجره جدید باز می‌شود) همکاری می‌کنیم، جایی که رونوشت‌های میانی WAL را به رونوشت‌های پایین‌دستی منتقل می‌کنند. این رویکرد به ما امکان می‌دهد تا بدون تحت فشار قرار دادن پایگاه اصلی، به‌طور بالقوه به بیش از صد رپلیکا مقیاس‌پذیر شویم. با این حال، این امر همچنین پیچیدگی عملیاتی بیشتری را معرفی می‌کند، به‌ویژه در زمینه مدیریت جابجایی خودکار. این ویژگی هنوز در حال آزمایش است؛ اطمینان حاصل می‌کنیم که پیش از عرضه در محیط تولید، پایدار است و می‌تواند در صورت بروز مشکل، به‌طور ایمن به حالت پشتیبان سوئیچ کند.

نمودار تکثیر آبشاری postgreSQL

محدودیت سرعت

چالش: افزایش ناگهانی ترافیک در نقاط پایانی خاص، افزایش تعداد کوئری‌های پرهزینه، یا طوفان تلاش‌های مجدد می‌تواند به سرعت منابع حیاتی مانند CPU، I/O و اتصالات را به اتمام برساند و باعث افت گسترده خدمات شود.

راهکار: ما محدودسازی نرخ را در چندین لایه—اپلیکیشن، تجمیع‌کننده اتصال، پروکسی و کوئری—پیاده‌سازی کردیم تا از افزایش‌های ناگهانی ترافیک که می‌تواند نمونه‌های پایگاه داده را تحت فشار قرار دهد و خرابی‌های آبشاری ایجاد کند، جلوگیری کنیم. همچنین بسیار مهم است که از فواصل زمانی تلاش مجددِ بیش از حد کوتاه اجتناب کنید، زیرا می‌تواند طوفان‌های تلاش مجدد را ایجاد کند. ما همچنین لایه ORM را بهبود بخشیدیم تا از محدودیت نرخ پشتیبانی کند و در صورت لزوم، خلاصه‌های خاص کوئری را به‌طور کامل مسدود نماییم. این شکل هدفمند کاهش بار، بازیابی سریع از جهش‌های ناگهانی پرس‌وجوهای پرهزینه را ممکن می‌سازد.

مدیریت الگو

چالش: حتی یک تغییر کوچک در الگو، مانند تغییر نوع یک ستون، می‌تواند باعث بازنویسی کامل جدول(در یک پنجره جدید باز می‌شود) شود. بنابراین ما تغییرات الگو را با احتیاط اعمال می‌کنیم—آن‌ها را به عملیات سبک محدود می‌کنیم و از هر تغییری که کل جدول‌ها را بازنویسی کند، اجتناب می‌کنیم.

راه‌حل: تنها تغییرات سبک در الگو مجاز هستند، مانند اضافه یا حذف برخی ستون‌ها که باعث بازنویسی کامل جدول نمی‌شوند. ما برای تغییرات الگو یک مهلت زمانی سختگیرانه ۵ ثانیه‌ای اعمال می‌کنیم. ایجاد و حذف ایندکس‌ها به‌طور هم‌زمان مجاز است. تغییرات الگو به جدول‌های موجود محدود می‌شوند. اگر یک ویژگی جدید به جدول‌های اضافی نیاز داشته باشد، باید در سیستم‌های شاردشده جایگزین مانند Azure CosmosDB باشند، نه PostgreSQL. هنگام پر کردن مجدد یک فیلد جدول، برای جلوگیری از افزایش ناگهانی نوشتن، محدودیت‌های نرخ سخت‌گیرانه‌ای اعمال می‌کنیم. اگرچه این فرآیند گاهی می‌تواند بیش از یک هفته طول بکشد، اما پایداری را تضمین می‌کند و از هرگونه تأثیر بر تولید جلوگیری می‌کند.

نتایج و مسیر پیش رو

این تلاش نشان می‌دهد که با طراحی و بهینه‌سازی‌های مناسب، Azure PostgreSQL می‌تواند برای مدیریت بزرگ‌ترین بارهای کاری تولیدی مقیاس‌بندی شود. PostgreSQL میلیون‌ها QPS را برای بارهای کاریِ خواندن‌محور مدیریت می‌کند و محصولات حیاتی OpenAI مانند ChatGPT و پلتفرم API را تأمین می‌کند. ما نزدیک به ۵۰ رپلیکای خواندن اضافه کردیم، در حالی که تأخیر تکثیر را نزدیک به صفر نگه داشتیم، خواندن‌های با تأخیر کم را در سراسر مناطق جغرافیایی توزیع‌شده حفظ کردیم و ظرفیت مازاد کافی ایجاد کردیم تا از رشد آینده پشتیبانی کنیم.

این مقیاس‌پذیری در حالی که تأخیر را به حداقل می‌رساند و قابلیت اطمینان را بهبود می‌بخشد، همچنان کار می‌کند. ما به‌طور مداوم تأخیر سمت کلاینت p99 در حد دو رقمی میلی‌ثانیه پایین و دسترس‌پذیری پنج نه (99.999%) را در محیط تولید ارائه می‌دهیم. و در ۱۲ ماه گذشته، فقط یک رخداد SEV-0 PostgreSQL داشته‌ایم (این رخداد در جریان راه‌اندازی ویروسی(در یک پنجره جدید باز می‌شود) ChatGPT ImageGen رخ داد، زمانی که ترافیک نوشتن ناگهان بیش از ۱۰ برابر افزایش یافت، زیرا بیش از ۱۰۰ میلیون کاربر جدید ظرف یک هفته ثبت‌نام کردند.)

در حالی که از اینکه PostgreSQL تا اینجا ما را رسانده راضی هستیم، همچنان مرزهای آن را جابه‌جا می‌کنیم تا مطمئن شویم برای رشد آینده فضای کافی داریم. ما قبلاً بارهای کاری سنگین و نوشتن‌محور قابل شارد را به سیستم‌های شاردشده‌مان مانند CosmosDB منتقل کرده‌ایم. بارهای کاری باقی‌مانده که نوشتن سنگین دارند، برای شارد کردن چالش‌برانگیزتر هستند—ما به‌طور فعال در حال مهاجرت آن‌ها نیز هستیم تا نوشتن‌ها را بیشتر از PostgreSQL اصلی برداریم. ما همچنین با Azure همکاری می‌کنیم تا تکثیر آبشاری را فعال کنیم تا بتوانیم با اطمینان به تعداد بسیار بیشتری از رپلیکای خواندنی مقیاس بدهیم.

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

نویسنده

Bohan Zhang

قدردانی‌ها

تشکر ویژه از Jon Lee، Sicheng Liu، Chaomin Yu و Chenglong Hao که در این پست مشارکت داشتند، و از کل تیمی که به مقیاس‌دهی PostgreSQL کمک کردند. همچنین مایلیم از تیم Azure PostgreSQL بابت همکاری قوی‌شان تشکر نماییم.