System DesignDatabasesScalingBackend

ChatGPT এর ১০০ কোটি user, data থাকে কোথায়?

OpenAI এর storage system Habitat প্রতি সেকেন্ডে ৭ কোটি request সামলায়, ৫০০ PB এর বেশি data নিয়ে। শুরুটা ছিল একটা সাধারণ Python library। সেখান থেকে শেখার মতো পাঁচটা engineering সিদ্ধান্ত।

· ৯ মিনিটের পড়া

শুরুর আগে একটা গল্প

আমরা অনেকেই ভাবি database মানে একটা Postgres, সাথে কিছু query, ব্যস। ছোট project এ কথাটা ঠিকও। কিন্তু ভাবো ChatGPT এর কথা। প্রায় ১০০ কোটি মানুষ, প্রত্যেকের chat history, settings, file, memory। প্রতিটা message পাঠানোর সাথে সাথে পেছনে কয়েকশো database call।

OpenAI এর engineering blog এ ওদের storage system এর গল্প পড়ে মনে হলো, scale এ গেলে database এর গল্পটা পুরো আলাদা হয়ে যায়। সেই গল্পটাই এখানে, সাথে প্রতিটা সিদ্ধান্তের পেছনের "কেন"।

বিষয়সংখ্যা
System এর নামHabitat
প্রতি সেকেন্ডে request৭ কোটি (70 million)
Data৫০০ PB এর বেশি
শুরু হয়েছিলএকটা সাধারণ Python library হিসেবে

শুরু: সবার code এ একটা library

প্রথম দিকে Habitat কোনো আলাদা system ছিল না। এটা ছিল একটা Python library। প্রতিটা team এই library নিজের service এর code এ import করে database এর সাথে কথা বলতো। Connection, retry, কোন data কোন server এ, সব logic ছিল library এর ভেতরে।

ছোট scale এ এটা চমৎকার কাজ করে:

  • আলাদা কোনো server চালাতে হয় না, রক্ষণাবেক্ষণ নেই।
  • মাঝখানে কোনো network hop নেই, তাই দ্রুত।
  • শুরু করা সহজ: pip install, তারপর ব্যবহার।

সমস্যা দেখা দেয় যেদিন library তে কিছু বদলাতে হয়।

যে outage আটকাতে গিয়ে outage হলো

একবার একটা বড় সমস্যা আটকাতে library তে নতুন logic যোগ করা হলো। এখন সেই নতুন version ডজনখানেক service এ পৌঁছাতে হবে। প্রতিটা team কে বলতে হবে version update করো, test করো, deploy করো। এতে কয়েক দিন লেগে গেল।

তারপর যা হলো, সেটাই আসল শিক্ষা: একটা team পুরো আলাদা একটা কারণে নিজের service rollback করলো। সেই rollback এর সাথে library ও ফিরে গেল পুরোনো, buggy version এ। আর যে outage আটকানোর জন্য এত খাটুনি, ঠিক সেই outage টাই হয়ে গেল।

library in every serviceservice Av1 · bugv2 · fixedservice Bv1 · bugv2 · fixedservice Cv1 · bugv2 · fixed↶ rolled backservice Dv1 · bugv2 · fixeddays to roll out · one rollback brings the bug backone storage serviceHabitatv2 · fixeddeploy once · every product gets it
বামে: fix প্রতিটা service এ আলাদা করে পৌঁছায়, ধীরে, আর একজনের rollback পুরোনো bug ফিরিয়ে আনে। ডানে: fix এক জায়গায়, সবাই একসাথে পায়।

সমাধান: library থেকে service

তাই ওরা library টাকে একটা আলাদা service বানালো। এখন product গুলো আর নিজের ভেতরে storage এর logic রাখে না, তারা শুধু Habitat service কে request পাঠায়। কোনো bug fix বা নতুন নিয়ম আনতে হলে এক জায়গায় deploy করলেই সব product সাথে সাথে পায়। কোনো team rollback করলেও storage এর logic আর পিছিয়ে যায় না।

LibraryService
Fix পৌঁছাতেপ্রতিটা team কে deploy করতে হয়, দিন লাগেএকবার deploy, মিনিট লাগে
Versionপ্রতিটা service এ আলাদা হতে পারেসবার জন্য একটাই
Rollback এর প্রভাবপুরোনো bug ফিরে আসতে পারেStorage এর logic স্বাধীন
খরচকোনো network hop নেইপ্রতি call এ একটা network hop, চালানোর জন্য আলাদা server

জেনে-বুঝে Python: "strategic tech debt"

এই পুরো গল্পে আমার কাছে সবচেয়ে মজার লেগেছে এই সিদ্ধান্তটা। ওরা জানতো Python এ service বানালে সেটা ধীর হবে। তবুও Python এই বানালো।

কেন? কারণ তখনকার priority ছিল team গুলোকে unblock করা, performance না। পুরোনো library ছিল Python এ। একই ভাষায় service বানালে পুরোনো code বেশিরভাগ ব্যবহার করা যায়, team সবাই ভাষাটা চেনে, আর সবচেয়ে দ্রুত আসল সমস্যাটা মেটে। ধীরগতির দাম পরে দেওয়া যাবে।

ওরা নিজেরাই এর নাম দিয়েছে strategic tech debt।

কেউ নিখুঁত system দিয়ে শুরু করে না। আসল দক্ষতা হলো জানা কোথায় compromise করছো, আর কেন। আর এই ধার ওরা পরে শোধও করেছে, Rust rewrite এর অংশে সেই গল্প।

Average না, সবচেয়ে ধীর call টা

ChatGPT তে তুমি একটা message পাঠালে পেছনে একটা না, শত শত database call হয়। তোমার history, settings, memory, file, permission, সব আলাদা আলাদা জায়গা থেকে আসে। আর তোমার উত্তর তৈরি হতে পারে কেবল তখনই, যখন সবগুলো call ফিরে আসে।

তার মানে তোমার অপেক্ষার সময় ঠিক করে দেয় সবচেয়ে ধীর call টা, গড়টা না।

1 message→ many DB calls38 ms52 ms41 ms60 ms47 ms230 ms44 ms55 msresponse can leave only nowp50 ≈ 48 msyou feel the slowest: 230 ms
একটা user request অনেকগুলো database call এ ভাগ হয়। বেশিরভাগ দ্রুত ফেরে, কিন্তু উত্তর পাঠাতে অপেক্ষা করতে হয় সবচেয়ে ধীরটার জন্য।

p50 আর p99 মানে কী

ধরো ১০০টা call এর সময় ছোট থেকে বড় সাজালে:

  • p50 (median): মাঝখানেরটা। অর্ধেক call এর চেয়ে দ্রুত।
  • p99: ৯৯তমটা। মানে ১০০টার মধ্যে মাত্র ১টা এর চেয়ে ধীর।

শুনে মনে হয় "১% ধীর call, তাতে কী?" এবার একটু অংক করি। ধরো প্রতিটা call এর ধীর হওয়ার সম্ভাবনা ১%। একটা user request যদি অনেকগুলো call করে, তাহলে অন্তত একটা ধীর call পড়ার সম্ভাবনা:

একটা request এ callঅন্তত একটা ধীর call এর সম্ভাবনা
১টা১%
১০টাপ্রায় ১০%
১০০টাপ্রায় ৬৩%
৩০০টাপ্রায় ৯৫%

অর্থাৎ যে system এ একটা request শত শত call করে, সেখানে "বিরল" ধীর call টাই প্রায় প্রত্যেক user অনুভব করে। এজন্যই বড় system এ p50 দেখে খুশি হওয়ার কিছু নেই। p99 (আর আরো বড় scale এ p99.9) টাই আসল মাপকাঠি।

ইচ্ছা করে SQL বাদ

Habitat এ SQL নেই। আছে শুধু একটা সহজ NoSQL API: মোটামুটি "এই key এর data দাও", "এই key তে data রাখো" ধরনের কাজ।

শুনে অবাক লাগতে পারে। SQL তো কত শক্তিশালী! Join, filter, aggregate, সব এক লাইনে। সমস্যাটা ঠিক সেখানেই। SQL এ অনেক ভারী query লেখা খুব সহজ। একটা ভুল join বা index ছাড়া একটা filter হাজার কোটি row ঘাঁটতে পারে। আর shared database এ একটা খারাপ query CPU, memory আর disk দখল করে নিলে বাকি সবার query আটকে যায়। একজনের ভুলে পুরো product নেমে যায়।

shared SQL databaseSELECT * … JOIN … JOINgetgetgeteveryone else waitsDBCPU 100%Habitat: simple get / putgetputgetputDBCPU steadyevery request: one key,a known, small cost
বামে: একটা ভারী SQL query database আটকে দেয়, বাকিরা লাইনে দাঁড়িয়ে। ডানে: সহজ get/put, প্রতিটার খরচ ছোট আর আগে থেকে জানা, তাই সব সমানে চলে।

ওরা ইচ্ছা করেই ক্ষমতা কমিয়েছে, যাতে system predictable থাকে। সহজ API তে প্রতিটা request এর খরচ মোটামুটি জানা: একটা key, একটা জায়গা, একটা উত্তর। কেউ ভুল করেও পুরো system নামাতে পারে না।

ধার শোধ: ২ জন engineer, Codex আর Rust

Python দিয়ে যে ধার নিয়েছিল, এবার সেটা শোধের পালা। Python এ লেখা service টা ওরা পুরোটা Rust এ নতুন করে লিখেছে। মাত্র ২ জন engineer, সাথে OpenAI এর নিজের coding agent Codex। ফলাফল:

CPUPythonRust6× lessMemoryPythonRust15× lesssame API, same behaviour · rewritten by 2 engineers with Codex
একই কাজ, একই API। Rust version এ CPU লাগে প্রায় ৬ ভাগের ১ ভাগ, memory প্রায় ১৫ ভাগের ১ ভাগ।
  • ৬ গুণ কম CPU
  • ১৫ গুণ কম memory

এখানে দুটো জিনিস লক্ষ্য করার মতো:

  1. API বদলায়নি। যেহেতু Habitat আগেই একটা আলাদা service আর তার API সহজ, তাই ভেতরের ভাষা পুরো বদলে ফেললেও product গুলোর কিছু জানার দরকার পড়েনি। Library থেকে service বানানোর সেই পুরোনো সিদ্ধান্তই এই rewrite কে সম্ভব করেছে।
  2. সঠিক সময়ে শোধ। প্রথম দিনেই Rust এ লিখতে গেলে team গুলো মাসের পর মাস আটকে থাকতো। আগে সমস্যা মিটিয়েছে, system কী করে সেটা পুরোপুরি বুঝেছে, তারপর গতি বাড়িয়েছে।

তোমার কাজে কী নেবে

তুমি হয়তো আজ প্রতি সেকেন্ডে ৭ কোটি request সামলাও না। কিন্তু এই সিদ্ধান্তগুলো ছোট system এও খাটে:

শিক্ষাছোট project এ কীভাবে
Shared logic অনেক জায়গায় ছড়ালে service বানাওএকই auth বা payment code পাঁচটা app এ copy না করে একটা ছোট service বা API দাও
জেনে-বুঝে tech debt নাও"এখন সহজ পথে, পরে optimize" ঠিক আছে, কিন্তু সেটা লিখে রাখো আর শোধের সময় ঠিক করো
Average না, p99 দেখোMonitoring এ p95/p99 যোগ করো, সবচেয়ে ধীর endpoint গুলো খোঁজো
ক্ষমতা কমিয়ে securityPublic API তে যেকোনো filter বা যেকোনো query চালাতে দিও না; সীমা, pagination আর timeout রাখো
আগে কাজ করাও, তারপর দ্রুত করোপরিষ্কার API রাখলে পরে ভেতরটা বদলানো সহজ

লেখাটা কাজে লাগলো?

Vote একবারই দেওয়া যায়, পরে বদলানো যায় না।