• मासिक $1,432 की production infrastructure को मासिक $233 dedicated server पर ले जाते हुए, operating system तक बदलने के बावजूद बिना downtime के service continuity बनाए रखी गई
  • 30 MySQL databases और 34 Nginx virtual hosts, GitLab EE, Neo4J, Supervisor, Gearman को नए server पर समान रूप से configure करने के बाद real-time replication और अंतिम incremental sync के जरिए migration पूरा किया गया
  • database migration की कुंजी mydumper·myloader parallel processing और MySQL replication का संयोजन था, और MySQL 5.7 से 8.0 में upgrade करते समय आए sys schema और permissions issues भी ठीक किए गए
  • cutover DNS TTL कम करना, पुराने server के Nginx reverse proxy conversion, और A records को bulk में बदलने के क्रम से किया गया, ताकि DNS propagation के दौरान भी पुराने IP पर आने वाले requests नए server तक पहुँचते रहें
  • नतीजतन मासिक $1,199 की बचत, वार्षिक $14,388 की बचत, CPU·memory·storage upgrade और 0 मिनट downtime एक साथ हासिल किए गए

माइग्रेशन की पृष्ठभूमि

  • तुर्की में software company चलाने के संदर्भ में तेज़ inflation और तुर्की लीरा की कमजोरी के कारण dollar आधारित infrastructure cost का बोझ काफ़ी बढ़ गया था
  • मौजूदा DigitalOcean server की लागत हर महीने $1,432 थी, और configuration में 192GB RAM, 32 vCPU, 600GB SSD, 1TB block volume 2 units, और backups शामिल थे
  • नया target Hetzner AX162-R dedicated server था, जिसमें AMD EPYC 9454P 48-core 96-thread, 256GB DDR5, 1.92TB NVMe Gen4 RAID1 configuration था
  • मासिक लागत घटकर $233 रह गई, और मासिक बचत $1,199, वार्षिक बचत $14,388 हुई
  • मौजूदा server की reliability या developer experience से कोई शिकायत नहीं थी, लेकिन steady-state workloads में price-to-performance अब व्यावहारिक नहीं रह गया था

मौजूदा production environment

  • operating stack कोई साधारण test environment नहीं बल्कि वास्तविक production environment था
    • MySQL databases 30, कुल 248GB data
    • कई domains पर फैले Nginx virtual hosts 34
    • GitLab EE backups 42GB सहित
    • Neo4J Graph DB 30GB scale पर चल रहा था
    • Supervisor से दर्जनों background workers manage किए जा रहे थे
    • Gearman job queue उपयोग में था
    • सैकड़ों हज़ार users के लिए live mobile apps चल रही थीं
  • मौजूदा server का operating system CentOS 7 था, जो पहले ही end-of-support स्थिति में था
  • नए server का operating system AlmaLinux 9.7 है, जो RHEL 9 compatible distribution और CentOS का स्वाभाविक successor विकल्प है
  • यह migration सिर्फ cost saving नहीं था, बल्कि कई वर्षों से security updates न पाने वाले operating system से बाहर निकलने का अवसर भी था

zero-downtime strategy

  • सिर्फ DNS बदलकर service restart करने वाला तरीका स्वीकार नहीं किया गया, बल्कि 6-step migration procedure के साथ zero-downtime migration किया गया
  • चरण 1: नए server पर पूरा stack install करना

    • Nginx को पुराने server के समान flags के साथ source compile करके install किया गया
    • PHP को Remi repo के ज़रिए install किया गया और पुराने server की वही .ini configuration files लागू की गईं
    • MySQL 8.0, Neo4J Graph DB, GitLab EE, Node.js, Supervisor, Gearman install कर पुराने behavior के अनुरूप configure किया गया
    • DNS records छूने से पहले सभी services को पुराने server की तरह ही काम करने की स्थिति में लाया गया
    • SSL certificates को पुराने server की पूरी /etc/letsencrypt/ directory को rsync से copy करके संभाला गया
    • पूरा traffic नए server पर जाने के बाद certbot renew --force-renewal से certificates को bulk में force renew किया गया
  • चरण 2: web files की rsync replication

    • /var/www/html पूरी directory, लगभग 65GB, 15 लाख files को SSH आधारित rsync से replicate किया गया
    • --checksum option के साथ integrity verification किया गया
    • cutover से ठीक पहले बदली हुई files के लिए अंतिम incremental sync भी किया गया
  • चरण 3: MySQL master-slave replication

    • dump और restore के लिए databases को बंद करने के बजाय real-time replication सेट की गई
    • पुराने server को master और नए server को read-only slave के रूप में सेट किया गया
    • शुरुआती बड़े data load के लिए mydumper इस्तेमाल हुआ, फिर dump metadata में दर्ज सटीक binlog position से replication शुरू की गई
    • cutover तक दोनों databases को real-time sync स्थिति में रखा गया
  • चरण 4: DNS TTL कम करना

    • DigitalOcean DNS API को script से call कर सभी A/AAAA record TTL को 3600 seconds से 300 seconds पर लाया गया
    • MX, TXT records नहीं बदले गए
    • mail record TTL बदलने से deliverability issues हो सकते थे, इसलिए उन्हें बाहर रखा गया
    • मौजूदा TTL के विश्व स्तर पर expire होने के लिए 1 घंटा इंतज़ार किया गया, फिर 5 मिनट के भीतर cutover की तैयारी पूरी हो गई
  • चरण 5: पुराने server के Nginx को reverse proxy में बदलना

    • Python script ने 34 Nginx site configurations में server {} blocks को parse किया
    • पुराने configs backup किए गए और उन्हें नए server की ओर इशारा करने वाले proxy configs से बदल दिया गया
    • DNS propagation के दौरान भी पुराने IP पर आने वाले requests चुपचाप नए server तक forward होते रहे
    • user के नज़रिए से किसी interruption का अनुभव नहीं हुआ
  • चरण 6: DNS cutover और पुराने server को बंद करना

    • Python script से DigitalOcean API call कर सभी A records को कुछ seconds में नए server IP पर बदला गया
    • पुराने server को 1 हफ़्ते तक cold standby में रखा गया और फिर बंद किया गया
    • पूरी प्रक्रिया के दौरान service या तो सीधे respond करती रही या proxy के ज़रिए, इसलिए availability gap का कोई चरण नहीं आया

MySQL migration

  • पूरे काम में सबसे जटिल हिस्सा MySQL migration था
  • data dump

    • standard mysqldump के बजाय mydumper इस्तेमाल किया गया
    • नए server के 48 CPU cores का उपयोग कर parallel export/import से single-threaded mysqldump के हिसाब से कई दिनों का काम कुछ घंटों में पूरा हुआ
    • उपयोग किए गए मुख्य options में --threads 32, --compress, --trx-consistency-only, --skip-definer, --chunk-filesize 256 शामिल थे
    • main dump की metadata file में snapshot समय का binlog position दर्ज था
      • File: mysql-bin.000004
      • Position: 21834307
    • यही मान बाद में replication start point के रूप में उपयोग हुए
  • dump transfer

    • dump पूरा होने के बाद SSH आधारित rsync से इसे नए server पर भेजा गया
    • कुल 248GB compressed chunks transfer हुए
    • mydumper के --compress option ने compressed chunks के कारण network transfer speed बेहतर करने में मदद की
  • data load

    • myloader इस्तेमाल किया गया
    • मुख्य options थे --threads 32, --overwrite-tables, --ignore-errors 1062, --skip-definer
  • MySQL 5.7 से 8.0 में transition issues

    • CentOS 7 environment की वजह से पुराना server MySQL 5.7 पर अटका हुआ था
    • migration से पहले mysqlcheck --check-upgrade से data की MySQL 8.0 compatibility जाँची गई, और result में कोई issue नहीं मिला
    • नए server पर नवीनतम MySQL 8.0 Community install किया गया
    • पूरे project में query execution time काफ़ी कम हुआ, और मूल लेख में इसका कारण MySQL 8.0 का improved optimizer और InnoDB enhancements बताया गया
    • लेकिन version jump के कारण समस्याएँ भी आईं
      • import के बाद mysql.user table की column structure अपेक्षित 51 columns के बजाय 45 columns थी
      • इसके परिणामस्वरूप mysql.infoschema गायब था और user authentication issues आए
    • पहला fix attempt नीचे दिए गए commands से किया गया
      • systemctl stop mysqld
      • mysqld --upgrade=FORCE --user=mysql &
    • पहला प्रयास ERROR: 'sys.innodb_buffer_stats_by_schema' is not VIEW error के साथ fail हुआ
    • कारण यह था कि sys schema view की जगह सामान्य table के रूप में import हो गया था
    • समाधान DROP DATABASE sys; चलाकर upgrade दोबारा करना था
    • इसके बाद प्रक्रिया सामान्य रूप से पूरी हो गई

MySQL replication configuration

  • दोनों servers पर dump load पूरा होने के बाद, नए server को पुराने server का replica बनाया गया
  • CHANGE MASTER TO statement में पुराने server का IP, replication user, port 3306, MASTER_LOG_FILE='mysql-bin.000004', MASTER_LOG_POS=21834307 सेट किया गया
  • इसके बाद START SLAVE; चलाया गया
  • लगभग तुरंत error 1062 Duplicate Key के कारण replication रुक गई
  • कारण यह था कि dump दो हिस्सों में किया गया था और इस बीच कुछ tables पर writes हुईं, इसलिए imported dump और binlog replay ने एक ही rows को duplicate insert करने की कोशिश की
  • समाधान के लिए नीचे की setting लागू की गई
    • SET GLOBAL slave_exec_mode = 'IDEMPOTENT';
    • START SLAVE;
  • IDEMPOTENT mode duplicate key और missing row errors को चुपचाप skip कर देता है
  • सभी critical databases बिना errors के sync हो गए, और कुछ ही मिनटों में Seconds_Behind_Master value 0 तक घट गई

cutover से पहले verification

  • DNS records बदलने से पहले नए server पर सभी services सही चल रही हैं या नहीं, यह verify करना ज़रूरी था
  • verification method यह था कि local machine की /etc/hosts file को अस्थायी रूप से बदलकर domain को नए server IP पर map किया गया
  • browser और Postman नए server पर requests भेजते थे, जबकि बाहरी users अभी भी पुराने server से जुड़ते रहे
  • API endpoints, admin panel, और हर service के response status की जाँच की गई
  • सब कुछ confirm होने के बाद वास्तविक cutover किया गया

SUPER privilege issue

  • master-slave replication पूरी तरह sync होने के बाद भी नए server पर read_only = 1 रहते हुए INSERT statements सफल हो रहे थे
  • कारण यह था कि सभी PHP application users को SUPER privilege दिया गया था
  • MySQL में SUPER privilege read_only को bypass कर देता है
  • SHOW GRANTS FOR 'some_db_user'@'localhost'; के result में SUPER privilege शामिल होना confirm हुआ
  • कुल 24 application users के लिए बार-बार REVOKE SUPER ON *.* FROM 'some_db_user'@'localhost'; चलाया गया
  • इसके बाद FLUSH PRIVILEGES; किया गया
  • उसके बाद read_only = 1 ने application user writes को सही ढंग से block करना शुरू किया, जबकि replication जारी रही

DNS preparation

  • सभी domains DigitalOcean DNS से manage किए जा रहे थे, और nameservers GoDaddy पर connected थे
  • TTL कम करने का काम DigitalOcean API के लिए script किया गया
  • बदलाव केवल A, AAAA records तक सीमित थे
  • MX, TXT records को नहीं छुआ गया
    • Google Workspace deliverability issues की संभावना के कारण mail-related record TTL changes बाहर रखे गए
  • पुराने TTL के expire होने के लिए 1 घंटा इंतज़ार कर cutover की तैयारी पूरी की गई

पुराने server के Nginx को reverse proxy में बदलना

  • 34 config files को manually edit करने के बजाय Python script से automatic conversion किया गया
  • script ने सभी config files के server {} blocks को parse किया, core content block पहचाना और उसे proxy settings से replace किया
  • मूल settings को .backup files के रूप में backup किया गया
  • example configuration में proxy_pass https://NEW_SERVER_IP;, proxy_set_header Host $host;, proxy_set_header X-Real-IP $remote_addr;, proxy_read_timeout 150; लागू किए गए
  • महत्वपूर्ण option था proxy_ssl_verify off
    • क्योंकि नए server का SSL certificate domain के लिए valid था, IP address के लिए नहीं
    • दोनों endpoints पर नियंत्रण होने की वजह से यहाँ verification disable करना स्वीकार्य था

cutover procedure

  • cutover से ठीक पहले की शर्तें थीं कि replication lag Seconds_Behind_Master: 0 हो और reverse proxy तैयार हो
  • execution order इस प्रकार था
    • नए server पर STOP SLAVE;
    • नए server पर SET GLOBAL read_only = 0;
    • नए server पर RESET SLAVE ALL;
    • नए server पर supervisorctl start all
    • पुराने server पर nginx -t && systemctl reload nginx चलाकर proxy activate करना
    • पुराने server पर supervisorctl stop all
    • local Mac से python3 do_cutover.py चलाकर DNS के सभी A records को नए server IP पर बदलना
    • लगभग 5 मिनट propagation का इंतज़ार
    • पुराने server पर सभी crontab entries को comment करना
  • DNS cutover script ने DigitalOcean API call कर सभी A records को लगभग 10 seconds में बदल दिया

cutover के बाद अतिरिक्त काम

  • migration पूरा होने के बाद पाया गया कि कई GitLab project webhooks अब भी पुराने server IP की ओर इशारा कर रहे थे
  • GitLab API के जरिए सभी projects को scan करके webhooks को bulk में update करने वाली script लिखी और लागू की गई

अंतिम परिणाम

  • मासिक लागत $1,432 से घटकर $233 रह गई
  • वार्षिक बचत $14,388 हुई
  • performance के लिहाज़ से भी अधिक शक्तिशाली server मिला
    • CPU 32 vCPU से बढ़कर 96 logical CPU हो गया
    • RAM 192GB से बढ़कर 256GB DDR5 हो गई
    • storage लगभग 2.6TB mixed configuration से 2TB NVMe RAID1 में बदल गया
    • downtime 0 मिनट रहा
  • पूरा migration लगभग 24 घंटे में पूरा हुआ
  • users पर कोई प्रभाव नहीं पड़ा

मुख्य सीख

  • MySQL replication zero-downtime migration का मुख्य साधन है
    • इसे शुरुआत में सेट करके पर्याप्त catch-up समय देने के बाद cutover करना प्रभावी रहा
  • MySQL user privileges की migration से पहले ज़रूर जाँच करनी चाहिए
    • SUPER privilege होने पर read_only bypass हो जाता है, जिससे slave environment वास्तव में read-only नहीं रहता
  • DNS updates, Nginx config changes, और webhook fixes को script करना महत्वपूर्ण है
    • 34 से अधिक sites को manually संभालने पर समय ज़्यादा लगता है और errors की संभावना बढ़ती है
  • mydumper + myloader का संयोजन बड़े datasets में mysqldump से कहीं तेज़ है
    • 32-thread parallel dump और restore ने कई दिनों का काम कुछ घंटों में बदल दिया
  • steady-state workloads में cloud provider महँगा पड़ सकता है, और dedicated server कम लागत पर ज़्यादा performance दे सकता है

GitHub scripts

  • migration में इस्तेमाल की गई सभी Python scripts को GitHub पर public किया गया
  • शामिल scripts की सूची
    • do_list_domains_ttl.py
      • सभी DigitalOcean domains के A records, IP, TTL की query
    • do_ttl_update.py
      • सभी A/AAAA records का TTL bulk में 300 seconds पर लाना
    • do_to_hetzner_bulk_dns_records_import.py
      • सभी DNS zones को DigitalOcean से Hetzner DNS में migrate करना
    • do_cutover_to_new_ip.py
      • सभी A records को पुराने server IP से नए server IP पर switch करना
    • nginx_reverse_proxy_update.py
      • सभी nginx site configs को reverse proxy configs में convert करना
    • mysql_compare.py
      • दोनों MySQL servers में सभी tables के row count की तुलना करना
    • final_gitlab_webhook_update.py
      • सभी GitLab project webhooks को नए server IP पर update करना
    • mydumper
      • mydumper library
  • सभी scripts DRY_RUN = True mode को support करती हैं, जिससे वास्तविक apply से पहले सुरक्षित preview संभव है

अभी कोई टिप्पणी नहीं है.

अभी कोई टिप्पणी नहीं है.