GitLab Postgres स्कीमा डिज़ाइन पर मेरे नोट्स
- GitLab के Postgres स्कीमा को देखकर, अपनी डिज़ाइन की तुलना करना और GitLab की schema definitions से best practices सीखना।
- GitLab एक ओपन-सोर्स DevOps प्लेटफ़ॉर्म है, GitHub का विकल्प और self-hosting के लिए उपलब्ध है।
सही Primary Key प्रकार का उपयोग
- जब डेटाबेस छोटा होता है तब यह बड़ा मुद्दा नहीं लगता, लेकिन स्केल बढ़ने पर primary key storage space, write speed और read speed को प्रभावित कर सकता है।
- GitLab के पास 573 tables हैं: इनमें से 380 tables में
bigserialprimary key type है, 170 मेंserial4, और बाकी 23 में composite primary key है।
Internal और External ID का उपयोग
- अपनी internal primary key को बाहर expose न करना अच्छा practice है।
- GitLab
issues,ci_pipelines,deployments,epicsजैसी tables में internal ID (id) और external ID (iid) दोनों का उपयोग करता है।
text data type और CHECK constraint का उपयोग
- GitLab schema में
character varying(n)औरtextदोनों हैं, लेकिनtextका उपयोग ज्यादा है। textमें length limit नहीं होती, इसलिए लंबाई की सीमाएँCHECKconstraints से define की जाती हैं।
Naming conventions
- सभी tables plural form में हैं और namespace देने के लिए module prefix use किया गया है।
- table और column नाम snake_case format का पालन करते हैं।
Timestamp में time zone का उपयोग
- GitLab दोनों उपयोग करता है:
timestamp with timezoneऔरtimestamp without timezone। - सिस्टम operations के लिए
timestamp without timezoneऔर user actions के लिएtimestamp with timezoneuse होते हैं।
Foreign key constraints
- GitLab अधिकांश tables में foreign key constraints use करता है, लेकिन
audit_events,abuse_reports,web_hooks_logs,spam_logsजैसी कुछ tables में नहीं।
बड़े tables की partitioning
- GitLab query performance सुधारने के लिए बड़े हो सकने वाले tables को partition करता है।
Trigrams और gin_trgm_ops के साथ LIKE search use cases को support करना
- GitLab efficient खोज के लिए GIN (Generalized Inverted Index) index का उपयोग करता है।
jsonb का उपयोग
- GitLab schema कई tables में
jsonbडेटा type का उपयोग करता है।
अन्य tips
- बदलने योग्य tables में
updated_atजैसी audit fields रखी जाती हैं, जबकि immutable log tables में नहीं। - Enums को
character varyingके बजायsmallintमें store करके space बचाया जाता है।
GN⁺ की राय:
- GitLab का स्कीमा डिज़ाइन DB design के लिए अच्छे insights देता है और बड़े पैमाने पर चलने वाले systems के लिए schema optimization पर महत्वपूर्ण lessons देता है।
- GitLab ओपन-सोर्स होने की वजह से, ये schema design निर्णय अन्य developers के लिए practical examples बनते हैं जिन्हें अपने projects में apply किया जा सकता है।
- GitLab schema से सीख मिलती है कि datatype चुनना, indexing strategy, partitioning और foreign key constraints का सही उपयोग जैसे decisions सीधे DB performance और maintainability को प्रभावित करते हैं।
1 टिप्पणियां
Hacker News की राय
मुझे समझ नहीं आता कि primary key को बाहर expose न करने की प्रथा क्यों ज़रूरी है। अगर request को वैसे भी authenticate होना है, तो ID guess करने से रोकने की क्या वैल्यू है, यह भी समझ नहीं आता
अगर सिर्फ guessed ID से authentication/authorization के बिना कोई उपयोगी काम किया जा सकता है, तो सिस्टम में कहीं और पहले से गंभीर गड़बड़ है, और schema में बेकार complexity जोड़ने के बजाय उसी हिस्से पर ध्यान देना चाहिए। competitors ग्राहक संख्या का अनुमान न लगा सकें, ऐसी competitive intelligence छिपाना जैसी बात कुछ वैल्यू रख सकती है, लेकिन लगता नहीं कि GitLab ने इसे बहुत बड़ा मुद्दा माना होगा। GitLab का
id + iidवाला फैसला internal ID guessing रोकने से ज़्यादा query performance की ज़रूरतों से प्रेरित रहा होगाअगर आपने users को UUID expose किया है, तो वही vulnerability होने पर भी attacker को UUID सही guess करना होगा, इसलिए यह बहुत कठिन हो जाता है और शायद secondary source की ज़रूरत पड़े। data leak हो भी जाए तो response करने का समय मिलता है और leaked volume का हिसाब भी लगाया जा सकता है। इसके उलट sequential ID हो तो समस्या तुरंत full-scale leak में बदल सकती है, और privacy regulator को mandatory report करना पड़ने वाला बड़ा incident बन सकती है। यह ऐसी defense in depth है जिसकी ज़रूरत नहीं पड़नी चाहिए, लेकिन असल में ऐसे भयानक software मौजूद हैं जो इसी तरीके से breach हुए हैं
अगर issues table की primary key
idexpose कर दी जाए, तो किसी project में issue बनाते समय वह 1 से शुरू नहीं होती, इसलिए GitLab में कुल कितने issues हैं इसका अनुमान आसानी से लगाया जा सकता हैबेशक, अगर guessed ID मात्र से authentication/authorization के बिना कुछ हो सके तो यह बड़ा issue है। लेकिन उस point पर अगर कोई दूसरी layer नहीं है, तो game पहले ही खत्म है। bugs पहले से सूचना देकर नहीं आते, खासकर subtle bugs तो और भी नहीं। जब ऐसा bug सामने आए और आपने IDs को कम से कम unguessable बना रखा हो, तो आप शुक्र मनाएंगे कि सिस्टम के सभी user accounts तक आसानी से access होने वाली स्थिति से बच गए
जाहिर है, पहले सही access control सुनिश्चित करने पर focus करना चाहिए, लेकिन unguessable IDs किसी भयानक आपदा और बाल-बाल बचे incident के बीच फर्क बना सकती हैं। अगर UUID सही fit न हो, तो auto-increment database ID इस्तेमाल करके उसे encrypt करने का तरीका भी संभव है, और उचित software layer हो तो encrypted IDs भी लगभग automatically काम करती हैं
12.8 करोड़ public repositories में से अधिकांश दूसरी repositories के forks हैं, और सिर्फ main repository में pull request बनाने के लिए मौजूद हैं, इसलिए जब तक गलती न हो, उनमें issues शायद नहीं होंगे
छोटे toy projects या जल्दी abandon हो गए projects में भी issues न होने या बहुत कम होने की संभावना है। सैकड़ों-हज़ारों issues वाले projects निश्चित रूप से होंगे, लेकिन 12.8 करोड़ repositories का overall average काफी कम होगा, इसलिए 2 अरब limit के नीचे रहने की संभावना है। फिर भी, उस table में 4-byte type, सटीक कहें तो 31 bits, इस्तेमाल करना github.com समेत कुछ organizations के लिए time bomb है—इससे सहमत हूं
https://play.clickhouse.com/play?user=play#U0VMRUNUIHVuaXEoc...
PRIMARY KEY(repo_id, issue_id)जैसी basic requirement ActiveRecord के अंदर बेवजह complicated हो जाती है, और single primary key column मांगने वाले ActiveRecord के हिसाब से unique key और अलग primary key साथ में इस्तेमाल करने वाला workaround चाहिए होता है। UUID primary key भी workaround जैसी लगती है, लेकिन(repo_id, issue_id)pair की unique constraint फिर भी चाहिए, जिससे database size और overhead बढ़ता है। व्यापक रूप से देखें तो Ruby on Rails की single model/controller/view layer पर आधारित monolithic MVC architecture भी application बड़े होने पर scalability और maintainability problems पैदा करती है, और मेरे हिसाब से MVC modular या component-based architecture में बेहतर fit बैठता हैself-hosting और cloud दोनों देने वाले products अक्सर per-customer databases पसंद करते थे। वजह यह कि hosting type चाहे जो हो, समान queries इस्तेमाल करने के लिए codebase का shared हिस्सा बहुत सरल हो जाता है। per-customer database हो तो ऐसे usage limits के पास पहुंचना लगभग नहीं होगा, और अगर पहुंचता है तो self-hosting ज़्यादा उपयुक्त होगा
intसेbigintमें migrate करना संभव है। थोड़ी तैयारी और custom code चाहिए, लेकिन इसे zero-downtime के साथ किया जा सकता हैहम बड़े migrations manage करने के लिए मोटे तौर पर इसी procedure को follow कर रहे हैं, और थोड़ा modify करके इस्तेमाल कर रहे हैं: http://zemanta.github.io/2021/08/25/column-migration-from-in...
foreign keys, indexes और constraints पूरी process को कठिन बनाते हैं, लेकिन असंभव नहीं। मेरे case में data migration में कुछ घंटे लगे, लेकिन उसे तेज़ होना ज़रूरी नहीं था। मेरी जानकारी में GitLab के पास post-upgrade tasks चलाने वाला tool है, ताकि version upgrade के दौरान कहीं भी यह काम कर सके
UUID कॉलम के storage size वाली बात बहुत मजबूत नहीं लगती। अगर table में 5 और columns हैं, तो 128-bit बनाम 64-bit का फर्क बहुत बड़ा नहीं है
ज्यादा अहम चिंता performance है। UUIDv4 व्यापक रूप से supported है, लेकिन पूरी तरह random होने के कारण index performance के लिए ideal नहीं है। UUIDv7[0] Snowflake[1] के करीब है, इसलिए उसमें temporal locality होती है, लेकिन implementations अभी उतनी व्यापक नहीं हैं। एक अलग approach यह भी है कि
bigserialइस्तेमाल करें और key को encrypt करें: https://github.com/abevoelker/gfc64लेकिन इस तरीके में 1) secret value rotate नहीं की जा सकती और 2) एक बार leak हो जाए तो कोई भी table size का Fermi estimate लगा सकता है। public ID और internal ID को अलग करना झंझट भरा है, और अगर public ID UUIDv4 है तो performance भी sacrifice होती है। मेरे हिसाब से UUIDv7 सबसे ज्यादा शर्तें पूरी करने वाला समाधान है
[0]: https://uuid7.com/
[1]: https://en.wikipedia.org/wiki/Snowflake_ID
idforeign key के रूप में इस्तेमाल होता है और उन foreign key columns के लिए जो index size चाहिए, वह भी मुद्दा हैuser ID जैसी value के बारे में सोचें, जिसे पूरी database में दर्जनों या सैकड़ों foreign keys reference कर सकती हैं
database performance के तीन चरण होते हैं। 1) index और data दोनों memory में fit हो जाते हैं। 2) index memory में fit हो जाता है, लेकिन data नहीं। 3) index और data दोनों memory में fit नहीं होते। नंबर 1 अच्छा है, लेकिन अगर ऐसा नहीं है तो किसी भी कीमत पर नंबर 2 को बचाए रखना चाहिए। index size दोगुना होना इसे और मुश्किल बना देता है
typedef void*जैसे basic pointer की तरह समझा जा सकता है। उसका size memory और disk usage, throughput bottlenecks, और joins व lookups के सबसे अंदरूनी loops में keys compare करने में लगने वाले CPU time तक, पूरी performance को प्रभावित करता हैजब x86-64 CPU नए आए थे, 64-bit pointers पर shift का performance impact इतना बड़ा था कि x32/ilp32 बनाया गया, और .NET में आज भी “prefer 32-bit” default रखने की वजह भी यही है। database primary key के रूप में 128-bit UUID इस्तेमाल करना भयानक गलती है
उदाहरण के लिए, हो सकता है आप कोई video public release से एक महीने पहले upload करें, लेकिन viewers को यह पता न चलने देना चाहें
URLs, emails आदि में दिखाते समय value को encrypt करना भी संभव है: https://wiki.postgresql.org/wiki/Pseudo_encrypt
इससे sequential index के कई फायदे बने रहते हैं और key बदलना भी संभव होता है। हालांकि key बदलने से bookmarks टूट जाते हैं, पुराने emails में भेजे गए links invalid हो जाते हैं, और असर व्यावहारिक रूप से हर नाम बदल देने जैसा ही रहता है
छोटी-सी nitpick है, लेकिन
textबनामvarcharsection पर बात करना चाहूँगालेखक पहले ऐसी performance difference साबित करने के लिए बहुत कुछ लिखता है जो मौजूद ही नहीं है, और फिर निष्कर्ष निकालता है कि “दोनों types के बीच बड़ा performance difference नहीं है।” यह विषय बहुत पहले settle हो चुका है, और बात “बड़ा नहीं” की नहीं, बल्कि “कोई difference नहीं” की है। PostgreSQL wiki[1] साफ कहता है कि कोई बहुत अच्छी वजह न हो तो
textइस्तेमाल करें, और docs[2] भी कहते हैं कि “कई उद्देश्यों के लिएcharacter varying,textके ऊपर domain की तरह behave करता है,” और हरे Tip box में कहते हैं कि “इन तीनों types के बीच कोई performance difference नहीं है।” इसलिए GitLab का ज्यादातरtextइस्तेमाल करना इस बात का संकेत लगता है कि उन्होंने docs पढ़े और किसी अधकचरे “portable” schema के बजाय PostgreSQL के हिसाब से schema design किया[1] https://wiki.postgresql.org/wiki/Don%27t_Do_This#Don.27t_use...
[2] https://www.postgresql.org/docs/current/datatype-character.h...
varchar(300)कोvarchar(200)में बदलने के लिए हर row को फिर से लिखना पड़ता है, लेकिनtextcolumn की constraint update करना मूल रूप से लगभग free है और सिर्फ यह check करने के लिए full table scan चाहिए कि मौजूदा values नई constraint satisfy करती हैं या नहीं। लेख में भी कहा गया है किtexttype औरCHECKconstraint इस्तेमाल करने से, length check होने परcharacter varyingयाvarchar(n)की तुलना में schema evolution आसान होता हैforeign keys महंगी होती हैं—यह बात अक्सर दोहराई जाती है, लेकिन benchmark कम ही मिलते हैं
गलत implementation के कई तरीके हैं, लेकिन integrity तो stack में कहीं न कहीं enforce हो ही रही होती है। इसे फिर से implement करने के बजाय database का फायदा उठाने के लिए knowledge और experimentation चाहिए, और आम तौर पर यह बड़े हादसों से बचाता है
सोच रहा हूँ कि क्या किसी ने GitLab और GitHub के performance difference को summarize किया है या इस पर ध्यान दिया है
दोनों Rails-based applications हैं, लेकिन overall page load time में GitLab, GitHub के मुकाबले बेहद खराब लगता है
Chrome और GitHub users को नुकसान पहुँचाना पड़े तब भी हर तरह की tricks अपनाएँगे। उदाहरण के लिए, एक बार company GitHub में merge diff खोलकर
Ctrl Fसे search किया, result न मिलने पर next पर जाता रहा और Git history manually खंगालता रहा; 100वें diff पर जाकर पता चला कि सबसे important file गहराई में छिपी हुई थी। शायद इसलिए कि किसी के लिए page load metrics meet करना और promotion पाना आसान थाGitLab की culture और performance को हल्के में लेने को समझने में मदद कर सकने वाली एक former developer की post यहाँ है: https://news.ycombinator.com/item?id=39303323
मैं GitLab इतना इस्तेमाल नहीं करता कि performance issues खुद महसूस कर सकूँ, लेकिन मुझे लगता है यह post मददगार हो सकती है
CI variables
CI_PIPELINE_IIDऔरCI_MERGE_REQUEST_IIDमें जो extra I है, उसका मतलब क्या है, यह हमेशा सोचता थाअंदाजा था कि यह database-related choice होगी, और यह लेख इसकी पुष्टि करता है
“1 quintillion, 1,000,000,000 billion के बराबर है” देखकर यह काफी अजीब लगता है कि हम आम तौर पर सिर्फ
int32औरint64के बीच ही चुनते हैं। लगभग 1 trillion cardinality support करने वाला 5-byte integer type होना चाहिएauto-increment ID न इस्तेमाल करना reasonable हो सकता है, लेकिन internal और external use के लिए 2 IDs रखने का फायदा मुझे ठीक से समझ नहीं आता
columns और indexes की संख्या बढ़ती है, हमेशा पहले lookup करना पड़ता है, और ऐसा कोई security scenario भी दिमाग में नहीं आता जहाँ internal key बदलें लेकिन external key न बदलें। क्या मैं कुछ miss कर रहा हूँ?
कहा जाता है कि PostgreSQL native UUID v4 type को
bigserialकी जगह इस्तेमाल करने से table size 25% बढ़ जाता है और insertion ratebigserialके 25% तक गिर जाता है; सोच रहा हूँ UUIDv4 इतना खराब क्यों हैUUID तो बस 128-bit number नहीं है? क्या generation cost बहुत expensive है, या असल में क्या हो रहा है?
इसलिए UUIDv4 column की indexing धीमी हो जाती है, और यही UUIDv6 और UUIDv7 के development की motivation बना
बड़ा issue insertion rate है। UUID के मामले में insertion rate available RAM की मात्रा से limited होता है। auto-increment integers में ऐसा नहीं है। integers time के साथ correlated होते हैं, लेकिन UUID4 random होता है, इसलिए scale बढ़ने पर performance characteristics fundamentally बदल जाते हैं। छोटी tables में insertion penalty लगभग negligible होती है, लेकिन जब B-tree index size memory limit तक पहुँचता है, तो PostgreSQL पूरे UUID B-tree को memory में नहीं रख पाता और disk page replacement पर निर्भर करता है। auto-increment integers में समय के लिहाज से पास-पास वाली rows वही index pages इस्तेमाल करती हैं, इसलिए समान load में disk hit करने की जरूरत नहीं पड़ती। इस scale पर पहुँचने पर फर्क कोई constant 25% slowdown नहीं, बल्कि 25x performance cliff होता है, और schema migration को छोड़ दें तो RAM ज्यादा खरीदने के अलावा कोई उपाय नहीं बचता
UUID बहुत सारी unordered writes पैदा करता है, जिससे page bloat होता है
batch insertion की locality भी बाद में lookup करते समय खराब हो जाती है, जिससे related records को randomly ढूँढना पड़ता है। आखिरकार cost insert time और बाद के select time, दोनों पर चुकानी पड़ती है