Connection Pool — nima uchun kerak, sozlash

~15 daq · MySQL · Xavfsizlik va Boshqaruv

Muammodan boshlaylik

Node.js API. Har bir so'rovda:

app.get('/mahsulotlar', async (req, res) => {
  const conn = await mysql.createConnection(config);   // ulanish ochish
  const [rows] = await conn.query('SELECT id, nom FROM mahsulotlar LIMIT 20');
  await conn.end();                                     // yopish
  res.json(rows);
});

Sekundiga 500 so'rov kelganda:

  • Har bir ulanish ochish — TCP handshake, autentifikatsiya, TLS: 5-20 ms. So'rovning o'zi esa 1 ms
  • Bir lahzada yuzlab ulanish ochilib-yopiladi
  • MySQL chegarasi:
ERROR 1040 (HY000): Too many connections

Sayt to'xtadi — bazada hech qanday og'ir so'rov yo'q, faqat ulanishlar ko'p.

Connection pool — ulanishlarni har safar yaratmasdan, qayta ishlatish.


Pool qanday ishlaydi

┌──────────────┐        ┌─────────────────────────┐        ┌───────────┐
│  So'rov 1    │──┐     │  POOL                   │        │           │
│  So'rov 2    │──┼───→ │  [ulanish 1] ← band     │ ←────→ │   MySQL   │
│  So'rov 3    │──┤     │  [ulanish 2] ← band     │        │           │
│  ...         │  │     │  [ulanish 3] ← bo'sh    │        │           │
│  So'rov 500  │──┘     │  ...  (jami 10 ta)      │        └───────────┘
└──────────────┘        │  navbat: so'rov 11-500  │
                        └─────────────────────────┘
  1. Ilova ishga tushganda pool bir nechta ulanishni oldindan ochadi
  2. So'rov keldi — pooldan bo'sh ulanishni oladi
  3. Ish tugadi — ulanishni yopmaydi, pool ga qaytaradi
  4. Hammasi band bo'lsa — so'rov navbatda kutadi

Hayotiy o'xshatma: taksi bekati. Har bir yo'lovchi uchun yangi mashina sotib olish (va yo'l oxirida tashlab ketish) o'rniga — bekatda 10 ta taksi navbatda turadi. Yo'lovchi oladi, borib keladi, mashina bekatga qaytadi.


Node.js — mysql2

import mysql from 'mysql2/promise';

const pool = mysql.createPool({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: 'dokon',
  connectionLimit: 10,        // maksimal ulanishlar
  maxIdle: 10,                // bo'sh turgan maksimal ulanishlar
  idleTimeout: 60000,         // bo'sh ulanish 60 soniyadan keyin yopiladi
  queueLimit: 0,              // navbat chegarasi (0 — cheksiz)
  enableKeepAlive: true,
});

app.get('/mahsulotlar', async (req, res) => {
  const [rows] = await pool.query('SELECT id, nom FROM mahsulotlar LIMIT 20');
  res.json(rows);
});

pool.query() — o'zi ulanish oladi, so'rovni bajaradi va qaytaradi.

Tranzaksiyalar — bitta ulanishda

Tranzaksiya bitta ulanish ichida bo'lishi shart. pool.query() har safar boshqa ulanish olishi mumkin:

// ❌ BEGIN bir ulanishda, INSERT boshqasida, COMMIT uchinchisida
await pool.query('START TRANSACTION');
await pool.query('INSERT ...');
await pool.query('COMMIT');

// ✅
const conn = await pool.getConnection();
try {
  await conn.beginTransaction();
  await conn.query('INSERT ...');
  await conn.query('UPDATE ...');
  await conn.commit();
} catch (e) {
  await conn.rollback();
  throw e;
} finally {
  conn.release();      // ⚠️ albatta pool ga qaytaring!
}

[!WARNING] conn.release() ni unutish — ulanish oqishi (connection leak). Har bir xatoli so'rov bitta ulanishni abadiy band qiladi. 10 ta xatodan keyin pool bo'shaydi va barcha so'rovlar navbatda abadiy kutadi — sayt "qotadi", bazada esa hech qanday yuklama yo'q. finally bloki — majburiy.


PHP — boshqacha model

Klassik PHP (PHP-FPM) da har bir HTTP so'rov — alohida jarayon, va u tugaganda hamma narsa yo'qoladi. Ilova ichida pool yo'q.

Variantlar:

  • Persistent connections — PDO::ATTR_PERSISTENT => true: har bir PHP-FPM worker o'z ulanishini so'rovlar orasida saqlaydi. 50 ta worker — 50 ta doimiy ulanish
  • Tashqi pooler — ProxySQL: PHP unga ulanadi, u esa MySQL ga kamroq doimiy ulanishlar saqlaydi
  • Uzoq yashovchi PHP — Swoole, RoadRunner, FrankenPHP, Laravel Octane: Node.js kabi doimiy jarayon, ichida haqiqiy pool

Kichik va o'rta loyihalarda oddiy PHP-FPM yetarli — har bir so'rovda ulanish ochish PHP + MySQL bitta serverda bo'lsa juda tez.


Pool hajmini tanlash

Intuitiv fikr: "katta pool — tezroq". Noto'g'ri.

MySQL serverining CPU yadrolari va disklari cheklangan. 8 yadroli serverga 500 ta ulanish bir vaqtda so'rov yuborsa — ular navbat uchun kurashadi, kontekst almashinuvi va qulflar ko'payadi, umumiy tezlik pasayadi.

Boshlang'ich formula (PostgreSQL jamiyatidan, MySQL uchun ham yaxshi yo'nalish):

pool hajmi ≈ (CPU yadrolari × 2) + disklar soni

8 yadroli baza serveri uchun — taxminan 15-20 ta faol ulanish. Bu barcha ilova serverlari uchun jami.

4 ta ilova serveri × connectionLimit 10 = 40 ta ulanish   ✅
20 ta ilova serveri × connectionLimit 50 = 1000 ta ulanish ❌

Ilova serverlari ko'payganda — har birining pool hajmini kamaytiring yoki tashqi pooler (ProxySQL) qo'ying.

Keyin — o'lchang va moslashtiring: navbat uzunligi, so'rov kutish vaqti, baza CPU yuklamasi.


MySQL tomonida

SHOW VARIABLES LIKE 'max_connections';        -- 151 (standart)
SHOW STATUS LIKE 'Threads_connected';         -- hozir ochiq ulanishlar
SHOW STATUS LIKE 'Max_used_connections';      -- eng ko'p bir vaqtda
SHOW STATUS LIKE 'Aborted_connects';          -- muvaffaqiyatsiz ulanishlar

max_connections ni oshirish (my.cnf):

[mysqld]
max_connections = 500
wait_timeout = 600              # bo'sh ulanish 10 daqiqadan keyin uziladi

Har bir ulanish MySQL da xotira oladi (bir necha MB). 5000 ulanish — gigabaytlar. max_connections ni oshirishdan oldin — nima uchun shuncha ulanish kerakligini tushuning (ko'pincha — leak yoki noto'g'ri pool hajmi).

wait_timeout tuzog'i

MySQL bo'sh turgan ulanishni wait_timeout dan keyin o'zi uzadi. Pool esa bundan bexabar va o'lik ulanishni beradi:

Error: Connection lost: The server closed the connection.

Yechim: pool ning idleTimeout ini MySQL ning wait_timeout idan kichik qilish, yoki enableKeepAlive.


ProxySQL — tashqi pooler

Katta tizimlarda ilova va MySQL orasiga ProxySQL qo'yiladi:

[ilova 1] ─┐
[ilova 2] ─┼─→ [ProxySQL] ─→ [source]
[ilova N] ─┘       │     └──→ [replica 1, 2]
  • Yuzlab ilova ulanishlarini o'nlab haqiqiy MySQL ulanishlariga to'playdi (multiplexing)
  • SELECT larni replica larga, yozishlarni source ga avtomatik yo'naltiradi (53-dars)
  • Failover da yo'naltirishni o'zgartiradi — ilova bilmaydi ham
  • So'rovlarni keshlash, cheklash, qayta yozish

Nima uchun shunday?

Nima uchun MySQL ulanishi bunchalik "qimmat" — oddiy so'rov-ku?

Har bir ulanish uchun MySQL alohida thread yaratadi, uning uchun xotira buferlari ajratadi, foydalanuvchini tekshiradi (parol xeshlash — ataylab sekin), TLS bo'lsa — shifrlash kalitlarini almashadi. Bularning hammasi so'rovning o'zidan ko'p vaqt oladi. Pool — bu "ishga tayyorlanish" xarajatini bir marta to'lab, keyin minglab so'rovga bo'lib yuborish.


Xulosa

Eslab qoling:

  • Har so'rovda ulanish ochish — sekin va Too many connections ga olib keladi
  • Pool — ulanishlarni qayta ishlatish; band bo'lsa — navbat
  • Node.js: mysql2.createPool({ connectionLimit }), pool.query()
  • Tranzaksiya — getConnection() + finally { conn.release() }; release ni unutish — leak
  • PHP-FPM: persistent connections yoki ProxySQL; Octane/Swoole — haqiqiy pool
  • Katta pool ≠ tez; boshlanish: ~(yadrolar × 2), barcha ilovalar uchun jami
  • max_connections, wait_timeout va pool idleTimeout ni moslang
  • Katta tizimlarda — ProxySQL

Keyingi dars

55-dars: Node.js — mysql2 moduli, CRUD, parametrli so'rovlar