Semua contoh sejauh ini membuka satu koneksi di awal program dan menutupnya di akhir — pola yang tepat untuk skrip sekali jalan. Server web hidup di dunia yang berbeda: ratusan request datang serentak, dan bila setiap request membuka koneksi baru, server MySQL harus melakukan handshake lengkap ratusan kali per detik hanya untuk menyapa. Dalam chapter ini kita mengukur biaya pembukaan koneksi secara nyata, memperkenalkan connection pooling — sekelompok koneksi hangat yang dipakai bergantian — lalu mengujinya dengan mysql2, menulis versi DIY-nya di Python, dan menakar batas paling penting dari sisi server: max_connections.
Berapa Mahal Sebuah Koneksi Baru? #
Membuka koneksi ke MySQL bukan sekadar memanggil connect(). Di balik satu panggilan itu terjadi rangkaian yang cukup panjang: resolusi alamat dan TCP three-way handshake untuk membangun socket, lalu authentication handshake — server mengirim nilai acak (nonce), klien merespons dengan jawaban challenge berbasis password, server memverifikasi dan mengirim paket OK — dan setelah itu server mengalokasikan thread khusus beserta buffer per-koneksi untuk klien tersebut. Hanya setelah semua itu barulah query pertama bisa dikirim.
Di server latihan course ini (koneksi localhost, tanpa TLS), satu pembukaan koneksi terukur sekitar 2 milidetik, sementara satu query sederhana di koneksi yang sudah terbuka hanya sekitar 0,05 milidetik — koneksi baru berbiaya kira-kira empat puluh kali satu query. Angka ini melonjak drastis di jaringan nyata antar-mesin dengan TLS aktif, dan yang lebih penting: server juga membayar di sisi memorinya — thread dan buffer per koneksi menggerus memori untuk setiap koneksi hidup, sekaligus memakan satu slot dari max_connections.
Ide Pooling #
Connection pool menjawab masalah itu dengan cara yang sederhana: buat beberapa koneksi di muka, simpan di satu wadah, dan pinjamkan secara bergantian. Request mengambil koneksi dari pool, memakainya untuk satu atau beberapa query, lalu mengembalikannya — tanpa perlu membuka koneksi baru. Saat permintaan melebihi jumlah koneksi yang tersedia, permintaan berikutnya menunggu (antre) sampai ada koneksi yang dikembalikan.
Dua kata kunci mengatur hidup pool: acquire (mengambil) dan release (mengembalikan). Koneksi yang lupa direlease akan menggantung sebagai “dipinjam” selamanya — pool perlahan kosong dan semua request akhirnya menunggu; kebocoran ini adalah bug pooling paling umum, persis sepupu kebocoran koneksi per-request di atas.
Menyiapkan Data Latihan #
Pool kita akan meminjamkan koneksi untuk membaca tabel produk di database belajar_app. Skemanya dibangun dulu lewat klien supaya chapter ini mandiri:
DROP DATABASE IF EXISTS belajar_app;
CREATE DATABASE belajar_app;
USE belajar_app;
CREATE TABLE produk (
id INT PRIMARY KEY,
nama VARCHAR(100) NOT NULL,
harga INT NOT NULL
);
INSERT INTO produk (id, nama, harga) VALUES
(1, 'Kopi Toraja', 52000),
(2, 'Teh Melati', 28000),
(3, 'Gula Aren', 32000);Pool mysql2: pool.js #
mysql2 menyediakan pooling bawaan lewat createPool. Berikut pool.js — tersimpan di bundle chapter (bagian Berkas Sumber), memakai dua gaya sekaligus: pool.query() yang mengambil dan mengembalikan koneksi secara otomatis, dan getConnection() untuk meminjam koneksi secara eksplisit:
// pool.js — pola connection pooling dengan mysql2.
// Jalankan dari folder chapter: node pool.js
// Port default 3306; timpa lewat variabel lingkungan DB_PORT.
const mysql = require('mysql2/promise');
async function main() {
const pool = mysql.createPool({
host: '127.0.0.1',
port: Number(process.env.DB_PORT || 3306),
user: 'root',
password: '',
database: 'belajar_app',
connectionLimit: 3,
enableKeepAlive: true,
idleTimeout: 60000,
});
const [rows] = await pool.query('SELECT id, nama FROM produk ORDER BY id');
console.log(`pool.query mengembalikan ${rows.length} baris:`);
rows.forEach((r) => console.log(` ${r.id} | ${r.nama}`));
const koneksi = await pool.getConnection();
const [statistik] = await koneksi.query(
'SELECT COUNT(*) AS jumlah, MIN(harga) AS termurah, MAX(harga) AS termahal FROM produk'
);
console.log(
`statistik lewat getConnection: ${statistik[0].jumlah} produk, ` +
`termurah ${statistik[0].termurah}, termahal ${statistik[0].termahal}`
);
koneksi.release();
const [murah] = await pool.query(
'SELECT id, nama FROM produk WHERE harga < ? ORDER BY id',
[40000]
);
console.log(`query berikutnya lewat pool: ${murah.length} baris di bawah 40000`);
await pool.end();
console.log('Pool ditutup.');
}
main().catch((err) => {
console.error('Terjadi galat:', err.code);
process.exit(1);
});Menjalankannya dari terminal — dengan pola DB_PORT yang sama seperti chapter sebelumnya (di komputer Anda cukup node pool.js):
-- @test-bash
DB_PORT=3306 node content/mysql/integrasi-aplikasi/database-connection-pooling/pool.jsOutput yang Dihasilkan:
pool.query mengembalikan 3 baris:
1 | Kopi Toraja
2 | Teh Melati
3 | Gula Aren
statistik lewat getConnection: 3 produk, termurah 28000, termahal 52000
query berikutnya lewat pool: 2 baris di bawah 40000
Pool ditutup.Mari kita bedah bagian-bagiannya:
mysql.createPool({...}): membuat objek pool — perhatikan tidak adaawaitdi sini; pool tidak membuka koneksi apa pun di muka. Koneksi dibuat malas saat pertama kali dibutuhkan, sampai batasconnectionLimit(di sini 3).connectionLimit: 3: jumlah maksimum koneksi yang boleh dibuka pool.pool.query()berikutnya akan otomatis meminjam koneksi idle yang sudah terbuka — query pertama dan ketiga di file ini memakai koneksi yang sama.enableKeepAlivedanidleTimeout: perawatan koneksi idle — TCP keepalive menjaga socket tetap hidup, danidleTimeout(milidetik) menutup koneksi yang menganggur terlalu lama. Keduanya kita bahas lagi di bagian health check.pool.query(...): pintu masuk paling praktis — driver mengambil satu koneksi dari pool, menjalankan query, lalu mengembalikan koneksi itu secara otomatis. Kita tidak pernah menyentuh lifecycle koneksi.pool.getConnection(): pinjaman eksplisit, untuk saat kita butuh beberapa statement di satu koneksi yang sama (misalnya satu transaksi). Konsekuensinya: koneksi harus dikembalikan manual dengankoneksi.release()— di file ini dipanggil segera setelah statistik tercetak.pool.end(): menutup semua koneksi saat program berakhir; tanpa ini, proses Node.js bisa terus hidup karena koneksi pool masih terbuka.
Pool DIY dengan PyMySQL (Ilustratif) #
PyMySQL tidak menyertakan pool bawaan — dan justru itu peluang untuk melihat isi kandangnya. Pool minimal berupa antrean koneksi plus dua operasi pinjam-kembali:
import queue
import threading
import pymysql
class PoolSederhana:
"""Pool kecil: antrean koneksi siap pakai, aman dipakai banyak thread."""
def __init__(self, ukuran=5, **pengaturan):
self._antrean = queue.LifoQueue()
self._ukuran_maks = ukuran
self._kunci = threading.Lock()
self._pengaturan = pengaturan
self._dibuat = 0
def ambil(self):
try:
return self._antrean.get_nowait()
except queue.Empty:
with self._kunci:
if self._dibuat < self._ukuran_maks:
self._dibuat += 1
return pymysql.connect(**self._pengaturan)
return self._antrean.get()
def kembalikan(self, koneksi):
self._antrean.put(koneksi)
def tutup_semua(self):
while True:
try:
self._antrean.get_nowait().close()
except queue.Empty:
returnKelas ini cukup untuk memahami mekanismenya: ambil() mengutamakan koneksi bekas pakai (LifoQueue mengembalikan koneksi yang paling baru dikembalikan — masih hangat), membuat koneksi baru hanya bila jumlah maksimum belum tercapai, dan mengantre bila antrean penuh. Namun untuk produksi, versi ini masih kekurangan banyak hal: tidak ada pemeriksaan kesehatan sebelum meminjamkan (koneksi.ping(reconnect=True)), tidak ada batas waktu antrean, tidak ada penghitungan umur koneksi, dan tidak ada penanganan saat beberapa thread menutup pool bersamaan. Di proyek Python sungguhan, gunakan pustaka DBUtils (kelas PooledDB) atau pool bawaan framework — engine SQLAlchemy, misalnya, sudah mengelola pooling lengkap dengan pool_pre_ping di dalamnya.
Pool di Tiap Stack #
| Stack | Mekanisme pool |
|---|---|
Node.js (mysql2) | createPool dengan connectionLimit, otomatis pada pool.query() |
Go (database/sql) | bawaan — objek sql.DB adalah pool; atur lewat SetMaxOpenConns, SetMaxIdleConns, SetConnMaxIdleTime |
| Python | pustaka DBUtils, atau pool pada engine SQLAlchemy |
| PHP | persistent connection prefiks p: pada mysqli/PDO per proses PHP-FPM, atau pool lapis terpisah (proxy SQL) |
| Java | pooler dewasa seperti HikariCP (default Spring Boot) |
Perhatikan pola yang berulang: ekosistem yang matang selalu berakhir di mekanisme yang sama — sedikit koneksi, dipakai bergantian, dengan batas atas yang eksplisit.
Ukuran Pool dan max_connections #
Setiap koneksi pool menempati satu slot di sisi server, dan slot itu terbatas. Kita periksa batasnya di server latihan:
SHOW VARIABLES LIKE 'max_connections';Output yang Dihasilkan:
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| max_connections | 151 |
+-----------------+-------+
1 row in set (0.00 sec)Nilai 151 adalah default MySQL untuk server kecil — cukup untuk latihan, tetapi aritmetikanya wajib dipikirkan di produksi: bila satu aplikasi memakai pool 20 koneksi, lima instance aplikasi berarti 100 koneksi, dan tersisa cuma 51 slot untuk koneksi DBA, monitoring, backup, dan aplikasi lain. Melampaui batas itu, koneksi baru ditolak dengan error 1040 Too many connections — kegagalan yang tidak bisa diselesaikan dari sisi aplikasi selain memperkecil pool atau menaikkan max_connections (yang harus dibayar dengan memori lebih besar per koneksi).
Aturan praktisnya sederhana: jumlah total koneksi dari semua aplikasi ditambah headroom administratif harus berada di bawah max_connections, dan pool yang lebih besar tidak otomatis lebih cepat — koneksi yang benar-benar dipakai paralel itulah yang berharga; sisanya hanya duduk memakan slot.
Health Check Koneksi Idle #
Pool yang tua punya musuh senyap: server menutup koneksi yang terlalu lama diam. Variabel wait_timeout (default 8 jam) menjatuhkan koneksi idle dari sisi server; ketika pool meminjamkan koneksi mati itu, aplikasi mendapat galat MySQL server has gone away (kode 2006) di tengah request yang tidak bersalah. Tiga penangkalnya: TCP keepalive seperti enableKeepAlive mysql2, pemeriksaan sebelum meminjam (koneksi.ping(reconnect=True) di PyMySQL, pool_pre_ping di SQLAlchemy), dan idleTimeout pool yang lebih pendek dari wait_timeout supaya pool sendiri yang membersihkan koneksi tuanya. Pola ini akan muncul lagi di chapter Menangani Database Errors sebagai salah satu baris tabel galat.
Membersihkan Latihan #
pool.js hanya membaca, jadi data masih utuh setelah program berjalan. Kita jatuhkan database latihan lewat terminal:
-- @test-bash
mysql -h 127.0.0.1 -P 3306 -u root -e "DROP DATABASE IF EXISTS belajar_app;"Referensi #
- Connection Management — MySQL 8.4 Reference Manual
- SHOW VARIABLES — MySQL 8.4 Reference Manual
- Server System Variables — MySQL 8.4 Reference Manual
Pool menyelesaikan masalah jumlah koneksi; bab berikutnya menghadapi musuh yang berbeda di jalur yang sama: string SQL yang dirakit dari input pengguna. Pola prepared statement di sisi aplikasi — placeholder tiap driver, eksekusi berulang, dan perbedaan sisi server versus sisi klien — adalah topik chapter Prepared Statements dalam Aplikasi.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
pool.js
Unduh berkas// pool.js — pola connection pooling dengan mysql2.
// Jalankan dari folder chapter: node pool.js
// Port default 3306; timpa lewat variabel lingkungan DB_PORT.
const mysql = require('mysql2/promise');
async function main() {
const pool = mysql.createPool({
host: '127.0.0.1',
port: Number(process.env.DB_PORT || 3306),
user: 'root',
password: '',
database: 'belajar_app',
connectionLimit: 3,
enableKeepAlive: true,
idleTimeout: 60000,
});
const [rows] = await pool.query('SELECT id, nama FROM produk ORDER BY id');
console.log(`pool.query mengembalikan ${rows.length} baris:`);
rows.forEach((r) => console.log(` ${r.id} | ${r.nama}`));
const koneksi = await pool.getConnection();
const [statistik] = await koneksi.query(
'SELECT COUNT(*) AS jumlah, MIN(harga) AS termurah, MAX(harga) AS termahal FROM produk'
);
console.log(
`statistik lewat getConnection: ${statistik[0].jumlah} produk, ` +
`termurah ${statistik[0].termurah}, termahal ${statistik[0].termahal}`
);
koneksi.release();
const [murah] = await pool.query(
'SELECT id, nama FROM produk WHERE harga < ? ORDER BY id',
[40000]
);
console.log(`query berikutnya lewat pool: ${murah.length} baris di bawah 40000`);
await pool.end();
console.log('Pool ditutup.');
}
main().catch((err) => {
console.error('Terjadi galat:', err.code);
process.exit(1);
});