Setiap request pelanggan di toko online — memuat halaman produk, memproses checkout, menyimpan ulasan — hampir selalu menyentuh database. Bila aplikasi membuka koneksi baru untuk setiap request, biayanya menumpuk: jaringan, autentikasi, dan memori server dibayar berulang untuk pekerjaan yang persis sama. Connection pooling menyelesaikan pemborosan tersebut dengan koneksi yang dibuka sedikit di awal lalu dipakai bergantian oleh seluruh request. Bab ini membongkar cara kerjanya dan membangun pool mini yang benar-benar berjalan.
Mengapa Membuka Koneksi Itu Mahal #
Satu koneksi PostgreSQL baru menempuh perjalanan yang tidak singkat: TCP three-way handshake ke server, negosiasi autentikasi (memeriksa kata sandi, menyiapkan enkripsi), lalu PostgreSQL membuat satu proses backend khusus untuk koneksi tersebut — proses yang menghabiskan memori beberapa megabyte dan tetap hidup selama koneksi terbuka. Untuk query singkat, seluruh upacara pembukaan ini bisa lebih lama daripada query-nya sendiri.
SQLite berada di ujung lain: tanpa jaringan dan tanpa proses terpisah. Meski demikian, membaca header berkas, menyiapkan struktur internal, dan membuka file descriptor tetap pekerjaan yang tidak perlu diulang bila bisa dihindari.
Cara Kerja Connection Pool #
Pool bekerja seperti tumpukan kartu member di kasir: aplikasi mengambil satu kartu saat mulai bekerja dan mengembalikannya setelah selesai; request berikutnya memakai kartu yang sama.
Siklus hidupnya terdiri dari empat peran: pinjam (request mengambil koneksi kosong), pakai (query berjalan seperti biasa), kembalikan (koneksi masuk kembali ke pool dalam status bersih — transaksi yang terlupa di-ROLLBACK dibatalkan lebih dulu), dan rawat (pool menumbuhkan koneksi saat permintaan naik, menutup koneksi idle terlalu lama, dan menguji koneksi dengan ping berkala).
Pool Mini dengan queue dan Threading #
Python menyediakan bahan baku pool di standard library: queue.Queue yang aman untuk thread. Kita membangun PoolKoneksi untuk SQLite, lalu menguji dengan enam thread pekerja yang merekam pesanan melalui pool berukuran dua koneksi; pencatat di dalam skrip menghitung berapa koneksi benar-benar dipakai pada waktu yang sama.
import sqlite3
import threading
import time
from contextlib import contextmanager
from pathlib import Path
from queue import Queue
DB = Path("toko.db")
if DB.exists():
DB.unlink()
setup = sqlite3.connect(DB)
setup.execute("CREATE TABLE pesanan_log (pekerja INTEGER, keterangan TEXT)")
setup.commit()
setup.close()
class PoolKoneksi:
def __init__(self, path, ukuran):
self.antrean = Queue(maxsize=ukuran)
for _ in range(ukuran):
self.antrean.put(
sqlite3.connect(path, check_same_thread=False))
@contextmanager
def pinjam(self, tunggu=5):
koneksi = self.antrean.get(timeout=tunggu)
try:
yield koneksi
finally:
self.antrean.put(koneksi)
def tutup_semua(self):
while not self.antrean.empty():
self.antrean.get().close()
kunci_pencatat = threading.Lock()
pencatat = {"sedang": 0, "maks": 0}
def pekerja(nomor, pool):
with pool.pinjam() as koneksi:
with kunci_pencatat:
pencatat["sedang"] += 1
pencatat["maks"] = max(pencatat["maks"], pencatat["sedang"])
koneksi.execute("INSERT INTO pesanan_log VALUES (?, ?)",
(nomor, f"pesanan dari pekerja {nomor}"))
koneksi.commit()
time.sleep(0.2) # simulasi query yang memakan waktu
with kunci_pencatat:
pencatat["sedang"] -= 1
pool = PoolKoneksi(DB, ukuran=2)
awal = time.perf_counter()
utas = [threading.Thread(target=pekerja, args=(n, pool)) for n in range(1, 7)]
for utas_satu in utas:
utas_satu.start()
time.sleep(0.05)
for utas_satu in utas:
utas_satu.join()
durasi = time.perf_counter() - awal
pool.tutup_semua()
cek = sqlite3.connect(DB)
print("maks koneksi dipakai serentak:", pencatat["maks"])
print("jumlah pesanan tercatat:",
cek.execute("SELECT COUNT(*) FROM pesanan_log").fetchone()[0])
print(f"durasi total: {durasi:.2f} detik")
cek.close()Output yang Dihasilkan:
maks koneksi dipakai serentak: 2
jumlah pesanan tercatat: 6
durasi total: 0.65 detikMari kita telaah baris-baris kode di atas:
- Baris 18-23 (
__init__): pool mengisiQueuedenganukurankoneksi SQLite yang dibuka sekaligus;Queuesendiri sudah menjamin keamanan thread. - Baris 25-31 (
pinjam):@contextmanagermengubah method menjadi blokwith.antrean.get(timeout=...)memblokir pemanggil sampai ada koneksi kosong, dan blokfinallymemastikan koneksi kembali ke antrean apa pun yang terjadi — termasuk saat query gagal. - Baris 42-52 (
pekerja): pencatat menaikkansedangsaat koneksi dipegang dan menurunkannya setelah selesai, di bawahkunci_pencatatagar dua thread tidak menimpa angka.time.sleep(0.2)mensimulasikan query berat agar tabrakan peminjaman pasti terjadi. - Baris 55-71: enam thread dimulai dengan jeda 0,05 detik agar urutan stabil,
join()menunggu semua selesai, lalu ringkasan dicetak dari koneksi pemeriksa di luar pool.
Angka 2 pada baris pertama output adalah bukti kerja pool: enam pekerja datang, tetapi tidak pernah lebih dari dua koneksi terpakai pada waktu yang sama. Semua pesanan tetap tersimpan, dan durasi total sekitar 0,6-0,7 detik — tiga gelombang peminjaman (6 pekerja / 2 koneksi) dikali 0,2 detik pekerjaan.
Pooling di PostgreSQL dengan psycopg_pool #
Untuk PostgreSQL, kita tidak perlu menulis pool sendiri. Pustaka psycopg_pool (pendamping psycopg 3) menyediakan ConnectionPool produksi dengan fitur lebih lengkap daripada pool mini kita — pemeliharaan koneksi di latar belakang, penggantian koneksi putus, dan statistik. Kupasan berikut tidak dieksekusi di lingkungan ini (tidak ada server PostgreSQL); sintaksnya diverifikasi dengan py_compile dan mengikuti dokumentasi resmi.
from psycopg_pool import ConnectionPool
pool = ConnectionPool(
conninfo="host=localhost dbname=toko_online user=budi "
"password=YOUR_DATABASE_PASSWORD",
min_size=4,
max_size=20,
)
with pool.connection() as koneksi:
hasil = koneksi.execute("SELECT COUNT(*) FROM pesanan")
print("jumlah pesanan:", hasil.fetchone()[0])
pool.close()ConnectionPool membuka min_size koneksi begitu pool dibuat, menumbuhkannya hingga max_size saat permintaan menanjak, dan pool.connection() meminjamkan koneksi sebagai context manager — pola yang sama dengan pinjam() pada pool mini kita. Untuk banyak aplikasi berbagi satu cluster PostgreSQL, pooler eksternal seperti PgBouncer dapat dipasang di depan database agar penghematan terjadi satu kali untuk semua aplikasi. Rincian lengkap ada di dokumentasi psycopg_pool
.
Menentukan Ukuran Pool #
Pool terlalu kecil membuat request mengantri; terlalu besar membebani database yang harus merawat setiap koneksi (ingat: satu proses backend per koneksi di PostgreSQL). Angka berikut hanya patokan awal — angka sebenarnya ditemukan lewat pengukuran:
| Parameter | Peran | Panduan nilai awal |
|---|---|---|
min_size | koneksi yang selalu siap | 2-5 per instance aplikasi |
max_size | batas atas pertumbuhan | 10-20; naikkan hanya jika bukti pengukuran mendukung |
timeout pinjam | lama request bersedia antre | 1-5 detik, lebih cepat gagal daripada menumpuk |
max idle | umur koneksi menganggur | 5-10 menit |
Batas keras di sisi database juga ikut bermain: PostgreSQL bawaan hanya mengizinkan 100 koneksi (max_connections), dibagi oleh seluruh instance aplikasi, job ETL, hingga DBA yang membuka sesi manual. Dokumentasi queue.Queue yang menjadi fondasi pool mini tersedia di Python docs
.
Koneksi kini efisien dipakai ulang. Pertanyaan berikutnya berpindah dari “bagaimana memakai database” ke “bagaimana melindunginya dari kehilangan data” — jawabannya adalah strategi backup dan pemulihan yang terukur, dibahas pada Chapter Backup dan Recovery.