MySQL և MariaDB
InnoDB, ինդեքսներ, transaction-ներ, օգտատերեր, դանդաղ հարցումներ, replication և backup-ներ գործնականում։
30 վայրկյանում
MySQL-ը ամենալայն տարածված open-source ռելյացիոն տվյալների բազաներից մեկն է և PHP ու Laravel hosting-ների մեծ մասի համար լռելյայն ընտրությունը։ Նրա InnoDB engine-ը տալիս է transaction-ներ, տողային մակարդակի locking և crash-ից վերականգնում։ MariaDB-ն համայնքային fork է, որը դեռ մեծապես համատեղելի է։ Երկուսից որևէ մեկը լավ աշխատեցնելը հանգում է լավ ինդեքսների, մի քանի հիմնական կարգավորումների, օգտատերերի արտոնությունների զգույշ բաշխման և այնպիսի backup-ների, որոնք իրականում վերականգնել եք։
Ինչու եմ օգտագործում
- MySQL և MariaDB օգտագործում եմ իմ Laravel նախագծերի մեծ մասում, որովհետև դրանք արագ են տիպիկ վեբ ծանրաբեռնվածության համար, աջակցվում են ամենուր և լավ հասկանալի են hosting panel-ների ու թիմերի համար։
- InnoDB-ի transaction-ների վրա եմ հենվում checkout-ի, wallet-ի և պահեստի գործողությունների համար, որտեղ մի քանի տող պետք է փոխվեն միասին կամ ընդհանրապես չփոխվեն։
- Տվյալների բազաները կապում եմ localhost-ին կամ private ցանցին, յուրաքանչյուր հավելվածի համար առանձին օգտատիրոջով, որովհետև ինտերնետին բաց տվյալների բազան ամենատարածված breach-երից մեկն է։
- Գնահատում եմ slow query log-ը և EXPLAIN-ը, որովհետև իմ շտկած performance խնդիրների մեծ մասը բացակայող ինդեքս էր, ոչ թե hardware-ի պակաս։
Ինչպես է աշխատում
01InnoDB. ինչպես է MySQL-ը պահում տվյալները
InnoDB-ն լռելյայն storage engine-ն է և միակը, որը պետք է օգտագործել հավելվածի տվյալների համար։ Այն յուրաքանչյուր աղյուսակ պահում է որպես clustered index. տողերը ֆիզիկապես դասավորված են ըստ primary key-ի B-tree-ի ներսում։ Secondary ինդեքսները պահում են ինդեքսավորված սյունակները գումարած primary key-ը և օգտագործում են այն ամբողջական տողը գտնելու համար։ Սրանից երկու հետևանք կա։ Կարճ, անընդհատ աճող primary key-ը (bigint) բոլոր ինդեքսները փոքր է պահում, իսկ insert-ները՝ էժան, մինչդեռ պատահական UUID-ները առաջացնում են page split-եր և ուռճացում։ Եվ secondary ինդեքսով որոնումն արժե երկու որոնում, եթե ինդեքսը չի ծածկում բոլոր անհրաժեշտ սյունակները։ InnoDB-ն նաև պահում է redo log՝ crash-ից վերականգնման համար, undo log՝ rollback-ների և consistent read-երի համար, և buffer pool, որը page-երը cache է անում հիշողության մեջ։
- innodb_buffer_pool_size. RAM-ի մոտ 50-ից 70%-ը՝ առանձնացված database սերվերի վրա։
- Յուրաքանչյուր աղյուսակի պետք է բացահայտ primary key։
- Ամենուր օգտագործեք utf8mb4. հին utf8-ը չի կարող պահել emoji-ներ և որոշ գրերի նշաններ։
02Ինդեքսներ և EXPLAIN
MySQL-ի ինդեքսները B-tree-եր են, և գործում է leftmost-prefix կանոնը. (status, created_at) ինդեքսն օգնում է WHERE status = 'paid' և WHERE status = 'paid' AND created_at > ... հարցումներին, բայց ոչ միայն created_at-ին։ Covering index-ը պարունակում է հարցմանը պետք եկող բոլոր սյունակները, ուստի MySQL-ը աղյուսակին ընդհանրապես չի դիպչում. EXPLAIN-ը ցույց է տալիս 'Using index'։ Սյունակների վրա ֆունկցիաները, ինչպես WHERE DATE(created_at) = ..., խանգարում են ինդեքսի օգտագործմանը. վերաշարադրեք դրանք որպես միջակայքեր։ Սկզբում wildcard-ը (LIKE '%term') չի կարող օգտագործել B-tree, ուստի օգտագործեք FULLTEXT կամ search engine։ Օգտագործեք EXPLAIN՝ plan-ը տեսնելու համար, և EXPLAIN ANALYZE (MySQL 8)՝ իրական ժամանակները տեսնելու համար։ Հետևեք type սյունակին. const-ը և ref-ը լավ են, ALL-ը նշանակում է full table scan։
ALTER TABLE orders ADD INDEX idx_status_created (status, created_at);
EXPLAIN ANALYZE
SELECT id, total_minor
FROM orders
WHERE status = 'paid'
AND created_at >= '2026-10-01'
AND created_at < '2026-11-01';
03Transaction-ներ և locking
Transaction-ը խմբավորում է հրամանները, որպեսզի դրանք բոլորը հաջողվեն կամ բոլորը ձախողվեն (ACID՝ atomic, consistent, isolated, durable)։ InnoDB-ի լռելյայն isolation level-ը REPEATABLE READ-ն է. transaction-ի ներսում տեսնում եք համահունչ snapshot։ Սովորական SELECT-ները երբեք չեն արգելափակում գրողներին՝ շնորհիվ MVCC-ի (multi-version concurrency control)։ Երբ պետք է անվտանգ կարդալ տողը և հետո թարմացնել այն, օրինակ՝ պահեստի մնացորդը կամ wallet-ի հաշվեկշիռը, lock արեք այն SELECT ... FOR UPDATE-ով կամ թարմացրեք ատոմար՝ մեկ հրամանով և պայմանով։ Transaction-ները կարճ պահեք. երկարները պահում են lock-եր, արգելափակում ուրիշներին և մեծացնում undo log-ը։ Deadlock-ներ լինում են, երբ երկու transaction տողերը lock են անում հակառակ հերթականությամբ. InnoDB-ն հայտնաբերում է դրանք և մեկին rollback անում, ուստի ձեր կոդը պետք է կրկնի։
START TRANSACTION;
SELECT stock FROM variants WHERE id = 42 FOR UPDATE;
UPDATE variants SET stock = stock - 2
WHERE id = 42 AND stock >= 2; -- 0 rows affected = out of stock
INSERT INTO order_items (order_id, variant_id, qty, unit_price_minor)
VALUES (1001, 42, 2, 2490);
COMMIT;
04Օգտատերեր և արտոնություններ
MySQL-ի հաշիվները user գումարած host են. 'app'@'localhost'-ը և 'app'@'%'-ը տարբեր հաշիվներ են։ Հետևեք least privilege սկզբունքին։ Հավելվածի օգտատերը ստանում է միայն տվյալների իրավունքներ (SELECT, INSERT, UPDATE, DELETE) իր սեփական տվյալների բազայի վրա։ Առանձին migration օգտատերը ստանում է սխեմայի իրավունքներ (CREATE, ALTER, INDEX, DROP), իսկ backup-ները՝ իրենց սեփական read-only հաշիվը։ Երբեք թույլ մի տվեք հավելվածներին միանալ որպես root։ MySQL 8.4-ից սկսած հին mysql_native_password plugin-ը լռելյայն անջատված է. caching_sha2_password-ը լռելյայն է և աշխատում է ընթացիկ driver-ների հետ։ Սերվերը bind-address-ով կապեք 127.0.0.1-ին կամ private ցանցի հասցեին, և 3306 port-ը փակ պահեք firewall-ում։ Եթե միացումներն անցնում են ցանցով, օգտագործեք TLS։
CREATE USER 'shop_app'@'localhost' IDENTIFIED BY 'use-a-long-random-secret';
GRANT SELECT, INSERT, UPDATE, DELETE ON shop.* TO 'shop_app'@'localhost';
CREATE USER 'shop_migrate'@'localhost' IDENTIFIED BY 'another-long-secret';
GRANT ALL PRIVILEGES ON shop.* TO 'shop_migrate'@'localhost';
SHOW GRANTS FOR 'shop_app'@'localhost';
05Slow query log
Slow query log-ը գրանցում է յուրաքանչյուր հրաման, որը long_query_time վայրկյանից ավելի է տևում։ Դա ամենաարագ միջոցն է գտնելու, թե իրականում ինչն է ցավեցնում production-ում, գուշակելու փոխարեն։ Միացրեք այն մոտ կես վայրկյանից մեկ վայրկյան շեմով, ըստ ցանկության staging պատճենի վրա գրանցեք նաև ինդեքս չօգտագործող հարցումները, և հետո ամփոփեք ֆայլը։ mysqldumpslow-ը գալիս է MySQL-ի հետ. Percona Toolkit-ի pt-query-digest-ը տալիս է ավելի լավ հաշվետվություն՝ խմբավորված ըստ query fingerprint-ի, որը ցույց է տալիս, թե որ հարցումներն են ամենաշատ ընդհանուր ժամանակը ծախսում։ Շտկեք առաջին մի քանիսը, սովորաբար ինդեքսով կամ վերաշարադրված հարցումով, հետո կրկնեք։ performance_schema-ն և sys schema-ն նույն պատկերը տալիս են live, օրինակ՝ sys.statements_with_runtimes_in_95th_percentile։
# /etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
slow_query_log = ON
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 0.5
# then summarise:
# sudo pt-query-digest /var/log/mysql/slow.log | less
06Replication-ի հիմունքներ
Replication-ը փոփոխությունները պատճենում է source սերվերից մեկ կամ մի քանի replica-ների։ Source-ը յուրաքանչյուր փոփոխություն գրում է իր binary log-ում (binlog). յուրաքանչյուր replica քաշում է binlog-ը և նորից կատարում այն։ Օգտագործեք row-based ֆորմատը (լռելյայն) և GTID-ներ (global transaction ids), որոնք failover-ը և replica-ների վերաուղղումը շատ ավելի պարզ են դարձնում։ Replication-ը լռելյայն ասինխրոն է, ուստի replica-ն կարող է հետ մնալ. նրանից կարդացածը կարող է մի փոքր հնացած լինել, ինչը կարևոր է «կարդա քո իսկ գրածը» հոսքերի համար, օրինակ՝ checkout-ից անմիջապես հետո պատվերը ցույց տալիս։ Replica-ները օգտակար են կարդալու մասշտաբավորման, հաշվետվությունների և որպես warm standby։ Replication-ը backup չէ. DROP TABLE-ը կամ սխալ UPDATE-ը ակնթարթորեն replicate է լինում։ MySQL 8.4-ից սկսած տերմիններն են source և replica, SHOW REPLICA STATUS-ի նման հրամաններով։
07Backup-ներ, և MySQL ընդդեմ MariaDB
Backup-ները երկու տեսակի են։ Logical backup-ները, որոնք արվում են mysqldump-ով կամ MySQL Shell-ի dump utility-ներով, export են անում SQL կամ տվյալների ֆայլեր. դրանք portable են տարբերակների միջև և հեշտ է մասնակի վերականգնել, բայց դանդաղ են մեծ տվյալների բազաների համար։ Օգտագործեք --single-transaction՝ առանց lock-ի համահունչ InnoDB snapshot ստանալու համար։ Physical backup-ները պատճենում են հենց տվյալների ֆայլերը՝ MySQL-ի համար Percona XtraBackup-ով, MariaDB-ի համար՝ mariadb-backup-ով. մեծ բազաների դեպքում դրանք շատ ավելի արագ են ստեղծվում և վերականգնվում, և աջակցում են incremental backup-ներ։ Պահեք binlog-երը՝ point-in-time recovery հնարավոր դարձնելու համար, պատճենները պահեք սերվերից դուրս և պարբերաբար ստուգեք վերականգնումը։ MariaDB-ն MySQL-ից fork է արվել 2009-ին։ Ամենօրյա SQL-ի համար դրանք դեռ մոտ են, բայց տարամիտվել են. JSON-ը, նույնականացումը, replication-ը և optimiser-ի հնարավորությունները տարբերվում են, ուստի դրանց վերաբերվեք որպես առանձին պրոդուկտների և ստուգեք նախքան անցում կատարելը։
# consistent logical backup, compressed, off to another location
mysqldump --single-transaction --routines --triggers --events \
-u backup_user -p shop | gzip > shop-$(date +%F).sql.gz
# restore into a test database to prove the backup works
gunzip < shop-2026-11-05.sql.gz | mysql -u root -p shop_restore_test
Օգտագործեք, երբ
- Տիպիկ վեբ և e-commerce ծանրաբեռնվածություն՝ բազմաթիվ կարճ read-երով և write-երով։
- Laravel և PHP նախագծեր, հատկապես shared կամ panel-ով կառավարվող hosting-ի վրա։
- Թիմեր, որոնք ուզում են հասուն, լավ փաստաթղթավորված տվյալների բազա՝ պարզ replication-ով։
- Read-heavy հավելվածներ, որոնք կարող են մասշտաբավորվել replica-ներով։
Մի՛ օգտագործեք, երբ
- Երբ պետք են առաջադեմ հնարավորություններ, ինչպես հարուստ JSONB ինդեքսավորում, row-level security, exclusion constraint-ներ կամ բազմաթիվ extension-ներ. այստեղ PostgreSQL-ն ավելի ուժեղ է։
- Ծանր analytics միլիարդավոր տողերի վրա, որտեղ ավելի լավ է համապատասխանում սյունակային տվյալների բազան, օրինակ՝ ClickHouse-ը։
Այլընտրանքներ
- PostgreSQL
Ավելի հարուստ SQL, JSONB, extension-ներ և row-level security. իմ ընտրությունը բարդ multi-tenant SaaS-ի համար։
- MariaDB
Համայնքային fork, տարածված Linux դիստրիբուտիվներում և hosting panel-ներում. հիմնականում համատեղելի, բայց տարամիտվող։
- Managed MySQL (RDS, Aurora, PlanetScale)
Backup-ները, failover-ը և թարմացումները ձեր փոխարեն են արվում՝ ավելի բարձր գնով։
Լավագույն պրակտիկաներ
- Յուրաքանչյուր աղյուսակում օգտագործեք InnoDB, utf8mb4 և բացահայտ կարճ primary key։
- Bind արեք localhost-ին կամ private ցանցին և հանրային 3306 port-ը փակեք։
- Յուրաքանչյուր հավելվածին տվեք իր least-privilege օգտատերը. երբեք մի միացեք որպես root։
- Միացրեք slow query log-ը և պարբերաբար վերանայեք այն։
- Transaction-ները կարճ պահեք և deadlock-ի դեպքում կրկնեք։
- Համադրեք պարբերական backup-ները binlog-երի հետ՝ point-in-time recovery-ի համար, և ստուգեք վերականգնումները։
- Մնացեք աջակցվող release line-ի վրա, օրինակ՝ MySQL 8.4 LTS։
Տարածված սխալներ
- Օգտագործել հին utf8 charset-ը, որն իրականում 3-բայթանոց ենթաբազմություն է։
- Ինդեքսավորված սյունակները փաթաթել ֆունկցիաների մեջ, ինչի պատճառով ինդեքսներն անտեսվում են։
- Replica-ին վերաբերվել որպես backup-ի։
- Ծանրաբեռնված սերվերի վրա mysqldump աշխատեցնել առանց --single-transaction-ի։
- Սերվերը թողնել հանրային interface-ի վրա լսելիս՝ թույլ գաղտնաբառերով։
Խորը վերլուծություն
08Հիշողություն և durability՝ buffer pool, redo log և flush կարգավորումներ
MySQL-ի tuning-ի մեծ մասը հանգում է InnoDB-ի մի քանի կարգավորման։ Buffer pool-ը cache է անում տվյալների և ինդեքսների էջերը․ նվիրված բազայի սերվերի վրա սովորաբար նրան տրվում է RAM-ի 50-ից 75 տոկոսը, և նպատակն այն է, որ «տաք» աշխատանքային տվյալները տեղավորվեն, և ընթերցումները հազվադեպ դիպչեն սկավառակին։ innodb_dedicated_server = ON կարգավորումը buffer pool-ի և redo log-ի չափերը ավտոմատ որոշում է մեքենայի հիշողությունից, ինչը լավ default է այն host-ի վրա, որտեղ աշխատում է միայն MySQL-ը։ Redo log-ը գրանցում է փոփոխությունները մինչև տվյալների էջերի գրվելը․ MySQL 8.0.30-ից սկսած դրա չափը մեկ կարգավորում է՝ innodb_redo_log_capacity, և չափազանց փոքր log-ը գրելու բեռի տակ ստիպում է անընդհատ flush անել։ Durability-ն փոխզիջում է, որը դուք ընտրում եք գիտակցաբար։ innodb_flush_log_at_trx_commit = 1 և sync_binlog = 1 արժեքները flush են անում յուրաքանչյուր commit-ի ժամանակ և դիմանում են հոսանքի անջատմանը՝ առանց commit արված transaction-ների կորստի․ 2 և 0 արժեքներն ավելի արագ են, բայց crash-ի դեպքում կարող են կորցնել մոտ մեկ վայրկյանի commit-ներ։ Փողի հետ կապված ամեն ինչի համար պահեք 1 և 1։
# /etc/mysql/mysql.conf.d/mysqld.cnf (dedicated DB host, 16 GB RAM)
[mysqld]
innodb_buffer_pool_size = 10G
innodb_redo_log_capacity = 4G
innodb_flush_log_at_trx_commit = 1 # full durability
sync_binlog = 1
max_connections = 300 # each connection also uses memory
binlog_expire_logs_seconds = 604800 # keep 7 days for point-in-time recovery
# check how often reads miss the buffer pool:
# SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';
09Online սխեմայի փոփոխություններ՝ INSTANT, INPLACE և metadata lock-եր
MySQL 8-ում ALTER TABLE-ն ունի երեք ալգորիթմ։ INSTANT-ը փոխում է միայն metadata-ն և ավարտվում է միլիվայրկյաններում՝ անկախ աղյուսակի չափից․ 8.0.29-ից սկսած այն կարող է ավելացնել կամ հեռացնել սյունակներ ցանկացած դիրքում, ինչպես նաև վերանվանել սյունակներ և փոխել default արժեքներ։ INPLACE-ը շատ գործողությունների համար վերակառուցում կամ փոփոխում է աղյուսակը՝ առանց ընթերցումներն ու գրառումները արգելափակելու, օրինակ՝ secondary ինդեքս ավելացնելիս LOCK=NONE-ով, բայց այն դեռ ժամանակ և I/O է պահանջում, իսկ replica-ներում փոփոխությունը վերարտադրվում է հետո՝ առաջացնելով lag։ COPY-ն վերակառուցում է ամբողջ աղյուսակը և արգելափակում գրառումները։ Ալգորիթմը նշեք բացահայտ․ այդ դեպքում MySQL-ը կմերժի, այլ ոչ թե լուռ կանցնի ավելի դանդաղ տարբերակի։ Թաքնված վտանգը metadata lock-ն է։ Ամեն ALTER սկզբում և վերջում կարճ ժամանակով բացառիկ metadata lock է պահանջում։ Եթե աղյուսակի վրա երկար transaction է բաց, ALTER-ը սպասում է, և այդ աղյուսակի ամեն նոր հարցում հերթ է կանգնում ALTER-ի հետևում․ կայքը կարծես սառած լինի։ Սահմանեք կարճ lock_wait_timeout և կրկնեք։ Հսկայական աղյուսակների կամ ռիսկային փոփոխությունների համար gh-ost-ը կամ pt-online-schema-change-ը ֆոնում պատճենում են աղյուսակը՝ սահմանափակված չափաբաժիններով։
SET SESSION lock_wait_timeout = 5; -- give up quickly instead of freezing traffic
ALTER TABLE orders ADD COLUMN gift_note VARCHAR(255) NULL, ALGORITHM=INSTANT;
ALTER TABLE orders ADD INDEX idx_orders_customer (customer_id, created_at),
ALGORITHM=INPLACE, LOCK=NONE;
-- who is holding or waiting for metadata locks?
SELECT * FROM sys.schema_table_lock_waits\G
10Lock-երի ներքին աշխատանքը և deadlock-ների վերլուծություն
InnoDB-ն lock է անում ինդեքսի գրառումները, ոչ թե վերացական տողերը, և սա բացատրում է անակնկալների մեծ մասը։ Locking read-ը կամ UPDATE-ը lock է անում իր սկանավորած բոլոր ինդեքսային գրառումները, ոչ միայն փոփոխվողները, ուստի UPDATE-ը, որի WHERE-ը չի կարող ինդեքս օգտագործել, կարող է lock անել աղյուսակի մեծ մասը։ REPEATABLE READ-ի դեպքում InnoDB-ն նաև gap և next-key lock-եր է վերցնում միջակայքերի վրա՝ phantom-ները կանխելու համար, ինչը նշանակում է, որ INSERT-ը կարող է սպասել մի միջակայքի, որը սկանավորել է մեկ այլ transaction։ Գործնական կանոններ․ համոզվեք, որ UPDATE և DELETE հրամանները ֆիլտրում են ինդեքսով, տողերին դիպչեք հետևողական հերթականությամբ, օրինակ՝ դասավորված ըստ primary key-ի, որպեսզի երկու transaction չկարողանան lock անել հակառակ հերթականությամբ, և transaction-ները պահեք կարճ։ Երբ deadlock է լինում, InnoDB-ն rollback է անում ավելի էժան transaction-ը 1213 սխալով, և հավելվածը պարզապես պետք է կրկնի ամբողջ transaction-ը։ Այն հասկանալու համար կարդացեք SHOW ENGINE INNODB STATUS-ի LATEST DETECTED DEADLOCK բաժինը կամ սահմանեք innodb_print_all_deadlocks = ON՝ ամեն deadlock log անելու համար։ Ընթացիկ սպասումները երևում են performance_schema.data_locks-ում և sys.innodb_lock_waits-ում։
-- who blocks whom right now
SELECT waiting_pid, waiting_query, blocking_pid, blocking_query, wait_age
FROM sys.innodb_lock_waits;
-- last deadlock, with both transactions and the locks they held
SHOW ENGINE INNODB STATUS\G
-- log every deadlock to the error log
SET PERSIST innodb_print_all_deadlocks = ON;
11High availability՝ զուգահեռ replication, semi-sync և cluster-ներ
Սովորական ասինխրոն replication-ը կարող է կորցնել վերջին transaction-ները, եթե source-ը ընկնի, իսկ replica-ն կարող է հետ մնալ ծանր գրելու բեռի տակ։ Մի քանի հնարավորություն լուծում է սա։ Multi-threaded replication-ը replica-ի վրա transaction-ները կիրառում է զուգահեռ․ replica_parallel_workers-ի default-ը 8.0.27-ից 4 է, իսկ replica_preserve_commit_order-ը անվտանգ է պահում commit-ների հերթականությունը։ Semi-synchronous replication-ը ստիպում է source-ին սպասել, մինչև առնվազն մեկ replica ստանա յուրաքանչյուր transaction, նախքան commit-ը հաստատելը՝ փոքր-ինչ latency փոխանակելով failover-ի ժամանակ հաստատված գրառումները չկորցնելու հետ։ Ավտոմատ failover-ի համար MySQL InnoDB Cluster-ը համատեղում է Group Replication-ը՝ երեք կամ ավելի հանգույցների consensus պրոտոկոլը, և MySQL Router-ը, որը կապերն ուղարկում է ընթացիկ primary-ին։ MariaDB-ի օգտատերերը հաճախ փոխարենը ընտրում են Galera Cluster-ը։ Ինչ էլ ընտրեք, մոնիտորինգ արեք lag-ը (SHOW REPLICA STATUS կամ ավելի լավ՝ heartbeat աղյուսակ), խուսափեք հսկայական միակ transaction-ներից, որոնք replica-ները պետք է վերարտադրեն հաջորդաբար, և «կարդա քո իսկ գրածը» հարցումներն ուղղեք primary-ին։
- Մեծ UPDATE կամ DELETE job-երը բաժանեք մի քանի հազար տողանոց չափաբաժինների, որպեսզի replica-ները հետ չմնան։
- Յուրաքանչյուր աղյուսակում primary key-ը պարտադիր է Group Replication-ի համար և լավ է row-based replication-ի համար։
- Փորձարկեք failover-ը․ cluster-ը, որի վրա ոչ ոք failover չի արել, չստուգված ենթադրություն է։
Իրական իրավիճակներ
Պարզ ALTER TABLE-ը սառեցրեց ամբողջ կայքը
Խնդիր
Deploy-ի ժամանակ migration-ը ինդեքս ավելացրեց orders աղյուսակին։ Վայրկյանների ընթացքում orders-ին դիպչող բոլոր էջերը կախվեցին, իսկ process list-ը լցվեց 'Waiting for table metadata lock' գրառումներով։
Ինչպես լուծել
Նախ վերականգնեք աշխատանքը․ գտեք արգելափակողին SELECT * FROM sys.schema_table_lock_waits-ով․ սովորաբար դա երկար աշխատող կամ անգործ բաց transaction է, օրինակ՝ մոռացված console session կամ հաշվետվության հարցում։ Kill արեք արգելափակողին կամ ALTER-ը, որպեսզի տրաֆիկը նորից հոսի։ Հետո կանխեք կրկնությունը։ Migration-ները գործարկեք SET SESSION lock_wait_timeout = 5-ով և ավտոմատ կրկնեք մի քանի անգամ, որպեսզի արգելափակված ALTER-ը հրաժարվի, այլ ոչ թե բոլորին հերթի կանգնեցնի իր հետևում։ Բացահայտ նշեք ALGORITHM=INSTANT կամ INPLACE, LOCK=NONE։ Alert դրեք մեկ րոպեից ավելի բաց transaction-ների վրա։ Մեծ աղյուսակների կամ պատճենում պահանջող փոփոխությունների համար օգտագործեք gh-ost կամ pt-online-schema-change, որոնք դանդաղեցնում են աշխատանքը ըստ բեռի և replica lag-ի։
Replica lag-ը ամեն գիշեր կտրուկ աճում է
Խնդիր
Read replica-ները գիշերային մաքրման job-ի ընթացքում 20-30 րոպե հետ են մնում, և հաճախորդները replica-ներից կարդացող էջերում տեսնում են պատվերների հնացած կարգավիճակներ։
Ինչպես լուծել
Նայեք job-ին․ միլիոնավոր տողերի դիպչող մեկ DELETE կամ UPDATE մեկ transaction է, որը replica-ները պետք է վերարտադրեն որպես մեկ ամբողջություն, և այն հնարավոր չէ զուգահեռացնել։ Վերագրեք այն չափաբաժիններով աշխատելու համար, օրինակ՝ DELETE ... WHERE created_at < ? ORDER BY id LIMIT 5000 ցիկլով՝ կարճ դադարով, կանգ առնելով, երբ replica lag-ը գերազանցի շեմը։ Համոզվեք, որ WHERE-ն ինդեքս է օգտագործում, որպեսզի ամեն չափաբաժին արագ լինի։ Ստուգեք, որ replica_parallel_workers-ը 1-ից մեծ է, և replica-ներն ունեն source-ի նույն hardware-ը։ Այն էջերի համար, որոնք տվյալները ցույց են տալիս փոփոխությունից անմիջապես հետո, օրինակ՝ պատվերի կարգավիճակը վճարումից հետո, կարդացեք primary-ից կամ գրելուց հետո մի քանի վայրկյան օգտագործեք sticky read-եր։
Բեռի տակ MySQL-ը սպանվում է out-of-memory killer-ի կողմից
Խնդիր
8 GB RAM ունեցող սերվերի վրա, որտեղ աշխատում են MySQL-ը և հավելվածը, MySQL-ը վերագործարկվում է տրաֆիկի գագաթնակետերին։ Kernel log-ը ցույց է տալիս, որ OOM killer-ն ընտրել է mysqld-ն։
Ինչպես լուծել
Ազնվորեն գումարեք հիշողությունը։ Buffer pool-ը սահմանված էր 6 GB, կարծես MySQL-ը մենակ լիներ, իսկ max_connections-ը 1,000 էր, մինչդեռ յուրաքանչյուր կապ կարող է հատկացնել sort, join և ժամանակավոր աղյուսակների buffer-ներ։ Իջեցրեք buffer pool-ը՝ տեղ թողնելով հավելվածի և օպերացիոն համակարգի համար, օրինակ՝ 3-ից 4 GB այս ընդհանուր host-ի վրա։ max_connections-ը սահմանափակեք հավելվածի իրական կարիքով և հավելվածում օգտագործեք connection pool, որպեսզի գագաթնակետերը հերթ կանգնեն, այլ ոչ թե հարյուրավոր session-ներ բացեն։ sort_buffer_size-ի նման session-ային buffer-ները թողեք default-ով։ Ըստ բաղադրիչների հիշողությանը հետևեք sys schema-ի memory view-երով։ Եթե բեռը շարունակում է աճել, MySQL-ը տեղափոխեք առանձին սերվեր, որտեղ innodb_dedicated_server-ը կարող է այն անվտանգ չափավորել։
Արագ ուղեցույց
- Սյունակը ավելացնել ակնթարթորեն
ALTER TABLE orders ADD COLUMN note VARCHAR(255) NULL, ALGORITHM=INSTANT;- Ինդեքս կառուցել առանց գրառումները արգելափակելու
ALTER TABLE orders ADD INDEX idx_c (customer_id), ALGORITHM=INPLACE, LOCK=NONE;- Metadata lock-ի դեպքում արագ ձախողվել
SET SESSION lock_wait_timeout = 5;- Տեսնել metadata lock-ի սպասումները
SELECT * FROM sys.schema_table_lock_waits;- Տեսնել row lock-ի սպասումները
SELECT * FROM sys.innodb_lock_waits;- Կարդալ վերջին deadlock-ը
SHOW ENGINE INNODB STATUS\G- Ստուգել replica-ի վիճակը և lag-ը
SHOW REPLICA STATUS\G- Գտնել երկար բաց transaction-ները
SELECT * FROM information_schema.innodb_trx ORDER BY trx_started LIMIT 10;- Ավտոմատ չափավորում նվիրված DB host-ի համար
innodb_dedicated_server = ON- Հսկայական աղյուսակի online փոփոխություն
gh-ost --database=shop --table=orders --alter="ADD INDEX idx_s (status)" --execute # plus connection flags
Իմ իրական նախագծերում
- Իմ սպասարկած production սերվերներից մի քանիսը host են անում Laravel կայքեր՝ MariaDB/MySQL-ով, Plesk-ով կառավարվող stack-երի վրա։
- Յուրաքանչյուր սերվերի համար իմ անվտանգության baseline-ը տվյալների բազաները պահում է localhost-ին կամ private ցանցին կապված, երբեք՝ հանրային։
- Ստուգել եմ այդ սերվերների panel backup-ները, և իմ ստանդարտը snapshot-ներն են գումարած ստուգված վերականգնումները, իսկ սերվերից դուրս backup-ները ընթացիկ համախմբման պլանի մաս են։
- Իմ ղեկավարած Laravel backend-ներում, ինչպես cashback wallet-ով ռեստորանային պատվերների պլատֆորմը և պահեստային մնացորդների շարժով կենդանիների ապրանքների խանութը, checkout-ը և հաշվեկշռի փոփոխությունները հենց այն հոսքերն են, որտեղ transaction-ներն ամենից շատ են կարևոր։
Հարցեր և պատասխաններ
Q1Ինչու՞ է primary key-ի ընտրությունն այդքան կարևոր InnoDB-ում։
Որովհետև InnoDB-ն աղյուսակը պահում է որպես clustered index՝ դասավորված ըստ primary key-ի, և յուրաքանչյուր secondary ինդեքս պահում է primary key-ի արժեքը որպես տողի ցուցիչ։ Կարճ, հաջորդական key-ը, օրինակ՝ bigint-ը, նշանակում է, որ նոր տողերը ավելանում են B-tree-ի վերջում, page-երը մնում են լեցուն, և յուրաքանչյուր secondary ինդեքս մնում է փոքր։ Որպես string պահված պատահական UUID-ը ստիպում է յուրաքանչյուր insert ընկնել պատահական page, առաջացնելով page split-եր, ֆրագմենտացիա և ավելի շատ I/O, ու ուռճացնում է բոլոր secondary ինդեքսները։ Եթե UUID-ներ են պետք, պահում եմ դրանք որպես BINARY(16), նախընտրում եմ ժամանակով դասավորված UUIDv7, կամ պահում եմ bigint key և առանձին unique public id։
Q2Ինչպե՞ս եք գտնում և շտկում դանդաղ հարցումը MySQL-ում։
Սկսում եմ տվյալներից, ոչ թե գուշակություններից. slow query log կամ performance_schema, ամփոփված pt-query-digest-ով, որպեսզի գտնեմ ամենաբարձր ընդհանուր ժամանակով հարցումները, ոչ թե պարզապես ամենադանդաղ մեկ գործարկումը։ Գլխավոր հարցման համար աշխատեցնում եմ EXPLAIN ANALYZE և ստուգում access type-ը, դիտարկված տողերն ընդդեմ վերադարձված տողերի, և արդյոք կա filesort կամ temporary table։ Տարածված շտկումներն են՝ composite ինդեքս ճիշտ սյունակային հերթականությամբ, covering index, սյունակի վրայի ֆունկցիայի վերաշարադրում միջակայքի, OFFSET pagination-ի փոխարինում keyset pagination-ով կամ ORM-ում N+1 pattern-ի վերացում։ Հետո նորից չափում եմ։
Q3Ո՞ր isolation level-ն է օգտագործում InnoDB-ն, և ի՞նչ է դա նշանակում գործնականում։
Լռելյայն՝ REPEATABLE READ։ Transaction-ի ներսում սովորական SELECT-ները կարդում են առաջին կարդալու պահին վերցված համահունչ snapshot-ից, ուստի կրկնվող read-երը վերադարձնում են նույն տվյալները, նույնիսկ եթե ուրիշները փոփոխություններ են commit անում, և MVCC-ի շնորհիվ կարդացողները երբեք չեն արգելափակում գրողներին։ Locking read-երի համար, ինչպես SELECT ... FOR UPDATE-ը, InnoDB-ն օգտագործում է next-key lock-եր, որոնք նաև gap-եր են lock անում phantom տողերը կանխելու համար, և դա կարող է ավելի շատ lock wait-եր ու deadlock-ներ առաջացնել, քան մարդիկ սպասում են։ Շատ թիմեր անցնում են READ COMMITTED-ի՝ ավելի քիչ locking-ի համար։ Ամեն դեպքում, read-then-write տրամաբանության համար, ինչպես պահեստի մնացորդը կամ հաշվեկշիռները, օգտագործում եմ բացահայտ lock-եր կամ ատոմար պայմանական update-ներ։
Q4mysqldump, թե՞ physical backup։
Փոքր և միջին տվյալների բազաների համար, մինչև մի քանի տասնյակ գիգաբայթ, mysqldump-ը --single-transaction-ով պարզ է, portable՝ տարբերակների միջև, և թույլ է տալիս վերականգնել մեկ աղյուսակ։ Նրա թույլ կողմը արագությունն է. dump-ը դանդաղ է, իսկ վերականգնումը՝ շատ ավելի դանդաղ, որովհետև յուրաքանչյուր ինդեքս վերակառուցվում է։ Ավելի մեծ բազաների համար օգտագործում եմ physical backup-ներ Percona XtraBackup-ով կամ mariadb-backup-ով, որոնք պատճենում են տվյալների ֆայլերը սերվերի աշխատանքի ընթացքում, արագ վերականգնվում են և աջակցում incremental-ներ։ Երկու դեպքում էլ պահում եմ binary log-երը point-in-time recovery-ի համար, պատճենները գաղտնագրում և պահում սերվերից դուրս, ու պարբերաբար վերականգնում test instance-ի մեջ, որովհետև չստուգված backup-ը ընդամենը հույս է։
Q5MySQL, թե՞ MariaDB. նույնն են դրանք։
Նրանք ունեն ընդհանուր պատմություն և ամենօրյա SQL-ի մեծ մասը, ուստի տիպիկ Laravel հավելվածը աշխատում է երկուսի վրա էլ։ Բայց 2009-ի fork-ից ի վեր դրանք տարամիտվել են։ MySQL-ը, որը զարգացնում է Oracle-ը, ունի native բինար JSON տիպ, optimiser-ի սեփական հնարավորություններ և InnoDB Cluster. MariaDB-ն JSON-ը պահում է որպես տեքստ՝ validation-ով, ունի իր սեփական հնարավորությունները, ինչպես system-versioned աղյուսակները, sequence-ները և տարբեր storage engine-ներ, ինչպես նաև իր replication-ն ու GTID ֆորմատը։ Տարբերակների համարներն այլևս չեն համընկնում, և դրանց միջև replication-ը կամ physical backup-ները չեն աջակցվում։ Յուրաքանչյուր նախագծի համար ընտրում եմ մեկը, նույնն օգտագործում development-ում և CI-ում, և զգույշ ստուգում նախքան մեկից մյուսին migrate անելը։
Q6Ինչպե՞ս եք պաշտպանում MySQL սերվերը։
Նախ՝ ցանցը. bind եմ անում localhost-ին կամ private interface-ին և 3306 port-ը փակ եմ պահում թե՛ սերվերի firewall-ում, թե՛ պրովայդերի firewall-ում, որպեսզի այն երբեք հասանելի չլինի ինտերնետից։ Հետո՝ հաշիվները. հեռացնում եմ anonymous օգտատերերին և հեռավոր root-ը, յուրաքանչյուր հավելվածին տալիս եմ իր օգտատերը՝ սահմանափակված իր տվյալների բազայով և տվյալների հրամաններով, migration-ների և backup-ների համար օգտագործում եմ առանձին օգտատերեր, և ուժեղ գեներացված գաղտնաբառեր՝ պահված որպես secret։ Երբ տրաֆիկն անցնում է ցանցով, օգտագործում եմ TLS։ Սերվերը պահում եմ աջակցվող տարբերակի վրա և patch արված, և գաղտնագրում եմ backup-ները։ Վերջապես՝ սահմանափակում եմ, թե ինչ կարող է անել հավելվածը, որպեսզի SQL injection-ը չկարողանա աղյուսակներ ջնջել։
Սենիոր մակարդակի հարցեր
S1Ինչպե՞ս կփոխեիք 500 միլիոն տողանոց MySQL աղյուսակի սխեման առանց downtime-ի։
Նախ ստուգում եմ՝ արդյոք փոփոխությունը կարող է INSTANT լինել․ սյունակներ ավելացնելը կամ հեռացնելը, վերանվանելը կամ default-ները փոխելը MySQL 8.0.29-ից սկսած սովորաբար կարող են, և այդ դեպքում աղյուսակի չափը նշանակություն չունի։ Եթե ոչ, LOCK=NONE-ով INPLACE փոփոխությունը նման չափի աղյուսակի վրա դեռ ծանր I/O և մեծ replica lag է առաջացնում, քանի որ replica-ները այն վերարտադրում են հետո։ Ուստի օգտագործում եմ gh-ost կամ pt-online-schema-change․ դրանք ստեղծում են shadow աղյուսակ նոր սխեմայով, պատճենում են տողերը փոքր չափաբաժիններով, կիրառում են ընթացիկ փոփոխությունները (gh-ost-ը կարդում է binlog-ը, pt-osc-ն օգտագործում է trigger-ներ), դանդաղեցնում են ըստ բեռի և replica lag-ի, և ավարտում արագ ատոմար rename-ով։ Երկուսն էլ cut-over-ի պահին դեռ կարճ metadata lock են պահանջում, ուստի սահմանում եմ lock_wait_timeout, cut-over-ը պլանավորում ցածր տրաֆիկի ժամին և համոզվում, որ երկար transaction-ներ բաց չեն։ Իսկ փոփոխությունն ինքնին հետևում է expand and contract մոտեցմանը, որպեսզի և՛ հին, և՛ նոր կոդը աշխատեն։
S2READ COMMITTED, թե՞ REPEATABLE READ ծանրաբեռնված OLTP հավելվածի համար։
REPEATABLE READ-ը default է և յուրաքանչյուր transaction-ին տալիս է կայուն snapshot, բայց locking read-երի, UPDATE-ների և DELETE-ների համար այն gap և next-key lock-եր է վերցնում սկանավորված միջակայքերի վրա։ Բարձր զուգահեռության դեպքում դա ավելի շատ lock-ի սպասումներ և deadlock-ներ է առաջացնում, հատկապես ծանրաբեռնված միջակայքերում insert-ների ժամանակ։ READ COMMITTED-ը գործողությունների մեծ մասի համար վերցնում է միայն record lock-եր և վաղ ազատում չհամապատասխանող տողերի lock-երը, ուստի ավելի քիչ մրցակցություն ունի․ գինը non-repeatable read-երն են, այսինքն՝ մեկ transaction-ում երկու նույնական SELECT կարող են տեսնել տարբեր commit արված տվյալներ, և այն պահանջում է row-based binary logging, որն արդեն default է։ Բարձր տրաֆիկով MySQL-ի շատ տեղադրումներ աշխատում են READ COMMITTED-ով, և այն համընկնում է PostgreSQL-ի default-ի հետ։ Իմ կարծիքով՝ ճշտությունը, միևնույն է, չպետք է կախված լինի isolation level-ից։ Կոդը, որը կարդում և հետո գրում է, պետք է օգտագործի բացահայտ lock-եր կամ ատոմար պայմանական update-ներ, և այդ դեպքում READ COMMITTED-ը սովորաբար ավելի լավ գործառնական ընտրություն է։
S3Ինչպե՞ս կնախագծեիք MySQL-ը high availability-ի համար համեստ բյուջեով։
Դիզայնը համապատասխանեցնում եմ downtime-ի և տվյալների կորստի իրական պահանջին։ Բազմաթիվ բիզնես հավելվածների համար մեկ primary և մեկ replica առանձին host-ի վրա, GTID-ի վրա հիմնված replication, semi-sync՝ հաստատված commit-ները չկորցնելու համար, և script-ավորված, փորձարկված ձեռքով failover-ը քիչ ծախսով տալիս են րոպեների վերականգնում։ Backup-երը binlog-երի հետ մնում են պաշտպանություն մարդկային սխալից, որը replication-ը պատճենում է ակնթարթորեն։ Երբ րոպեները չափազանց երկար են, երեք հանգույցով InnoDB Cluster-ը և MySQL Router-ը տալիս են ավտոմատ failover՝ ավելի շատ գործառնական գիտելիքի, յուրաքանչյուր աղյուսակում primary key-ի և հանգույցների միջև ցանցային latency-ի նկատմամբ զգայունության գնով։ Managed ծառայությունն այդ աշխատանքը տեղափոխում է պրովայդերին՝ ավելի բարձր ամսական գնով։ Փոխզիջումը միշտ փողն ու բարդությունն են վերականգնման ժամանակի դիմաց, ուստի նախ գրի եմ առնում վերականգնման նպատակային ժամանակը և թույլատրելի տվյալների կորուստը, հետո ընտրում ամենապարզ կարգավորումը, որը դրանք բավարարում է։
Բառարան
- Buffer pool
- InnoDB-ի հիմնական հիշողության cache-ը տվյալների և ինդեքսների էջերի համար․ MySQL-ի հիշողության ամենակարևոր կարգավորումը։
- Redo log
- Փոփոխությունների write-ahead log, որը InnoDB-ն վերարտադրում է crash-ից հետո, որպեսզի commit արված transaction-ները չկորչեն։
- Metadata lock
- Lock աղյուսակի սահմանման վրա․ DDL-ին այն պետք է բացառիկ, և այն սպասում է աղյուսակի վրա բաց transaction-ներին։
- ALGORITHM=INSTANT
- ALTER TABLE-ի ռեժիմ, որը փոխում է միայն metadata-ն և ավարտվում գրեթե անմիջապես՝ անկախ աղյուսակի չափից։
- Next-key lock
- Lock ինդեքսի գրառման և դրանից առաջ եղած բացվածքի (gap) վրա՝ phantom տողերը կանխելու համար։
- Semi-synchronous replication
- Source-ը սպասում է, մինչև replica-ն հաստատի transaction-ի ստացումը, նախքան commit-ը հաստատելը։
- Group Replication
- MySQL-ի consensus-ի վրա հիմնված replication մի քանի հանգույցների միջև, InnoDB Cluster-ի ավտոմատ failover-ի հիմքը։
- MySQL Router
- Թեթև proxy, որը հավելվածի կապերն ուղղում է cluster-ի ընթացիկ primary-ին կամ replica-ներին։
- gh-ost
- Online սխեմայի փոփոխության գործիք, որը binlog-ի միջոցով պատճենում է աղյուսակը սահմանափակված չափաբաժիններով՝ առանց trigger-ների։
Շարունակեք սովորել
Պրոդուկտի գաղափար ունե՞ք։Եկեք ստեղծենք այն։
Պատմեք ձեր գաղափարի, պրոդուկտի կամ թիմի մասին։ Սովորաբար պատասխանում եմ մեկ աշխատանքային օրվա ընթացքում։