qmgr (Queue Manager). Ketika sebuah Mail Transfer Agent dioperasikan pada infrastruktur Cloud VPS NVMe di penyedia lokal seperti Biznet Gio atau IDCloudHost—yang lazimnya memiliki throughput disk I/O tinggi serta latensi jaringan rendah ke pertukaran internet domestik (IIX/OIXP)—ekspektasi performa sering kali berbenturan dengan batasan throughput pengiriman email. Meskipun disk NVMe memiliki metrik IOPS acak yang sangat tinggi, single-instance Postfix sering kali mentok pada ratusan pesan per detik dan mengakibatkan penumpukan ribuan email di direktori spool incoming dan active.
Masalah tersebut bersumber dari arsitektur single-threaded event loop pada qmgr, serialisasi akses metadata VFS (Virtual Filesystem), serta perilaku kontrol kongesti TCP bawaan (Cubic) yang rentan mengalami throughput collapse saat mengirimkan email keluar (egress) melintasi rute peering heterogen. Optimasi throughput memerlukan pemecahan proses spooling melalui arsitektur multi-instance (postmulti), tuning I/O metadata filesystem, serta penerapan algoritma kontrol kongesti TCP BBR (Bottleneck Bandwidth and Round-trip propagation time) dengan fair queueing (fq).
Bottleneck qmgr dan Serialisasi I/O pada Direktori Spool
/var/spool/postfix) harus diurai secara mekanis.
- Penerimaan Pesan: Daemon
smtpdmenerima email dari jaringan, meneruskannya ke daemoncleanupmelalui internal UNIX domain socket. - Penulisan ke Disk: Daemon
cleanupmembuat file pesan di direktoriincomingdengan penamaan berbasis nomor inode atau format unik waktu-mikrodetik, lalu memicu system callfdatasync()untuk menjamin durabilitas pesan sebelumsmtpdmengembalikan kode status SMTP250 OKke klien. - Transisi ke Antrean Aktif:
cleanupmemberitahuqmgrvia FIFO pipe (public/qmgr). Daemonqmgrmemindai direktoriincomingdan memindahkan file pesan ke direktoriactivemelalui operasirename(). - Pengiriman:
qmgrmenjadwalkan file di direktoriactiveke worker delivery agent (smtp) sesuai ketersediaan concurrency slot per domain tujuan. - Penyelesaian: Setelah status pengiriman final tercapai, worker
smtpmenghapus file dari direktoriactivemelaluiunlink(), atau memindahkannya ke direktorideferredjika terjadi soft failure (kode status4xx).
TEXT
[smtpd] -> [cleanup] --fdatasync()--> [/var/spool/postfix/incoming]
| (FIFO notify)
v
[qmgr] --rename()--> [/var/spool/postfix/active]
|
+-------------+-------------+
| |
v v
[smtp worker 1] [smtp worker N]
| |
+--- unlink() / rename() ---+qmgr. Meskipun Postfix dapat menjalankan ratusan proses worker smtp secara paralel (default_destination_concurrency_limit), pemindaian, seleksi batch antrean, dan alokasi tugas ke worker tetap dieksekusi secara single-threaded oleh satu proses qmgr. Ketika volume antrean di active dan deferred melampaui ambang puluhan ribu pesan, overhead komputasi CPU untuk traversing struktur internal list, penegakan batasan domain rate limit, dan serialisasi pembacaan direktori via readdir() menjadi CPU-bound pada 1 inti CPU.
Selain itu, sistem penamaan file berbasis struktur direktori datar atau ber-hash parsial (hash_queue_names) menghasilkan ribuan operasi modifikasi metadata per detik (create, rename, unlink). Pada level kernel Linux, operasi-operasi ini memicu lock contention pada VFS inode mutex direktori induk. Meskipun media penyimpanan fisik berbasis NVMe mampu melayani ratusan ribu IOPS, kernel tetap harus menyelaraskan pembaruan log metadata journal (misalnya jurnal ext4 atau XFS log buffer). Hal ini membuat thread cleanup dan qmgr sering tertahan pada status uninterruptible sleep (D-state), yang terdeteksi melalui peningkatan nilai Pressure Stall Information (PSI) pada subsistem I/O.
Interaksi VFS, Inode Locking, dan Page Cache Linux
create() dan unlink() file email pada /var/spool/postfix memaksa modifikasi inode table, directory block, dan free block bitmap. Jika filesystem dipasang dengan opsi default relatime, pembaruan timestamp akses tetap membebani journal commit rate.
Dalam sistem antrean email dengan intensitas throughput tinggi, waktu hidup (lifetime) file di direktori spool sering kali hanya berkisar antara 50 milidetik hingga beberapa detik jika server tujuan langsung merespons. Menuliskan file yang berusia singkat ini secara berulang ke storage NVMe dan menunggu commit journal akan menghabiskan write endurance flash memory dan menyebabkan latensi I/O spike yang tidak perlu.
Siklus flusing dirty pages oleh kernel diatur oleh parameter vm sysctl. Jika proporsi dirty memory melampaui batas tertentu, thread aplikasi yang melakukan I/O akan dipaksa melakukan direct reclaim atau synchronous writeback, yang memblokir proses cleanup Postfix:
| Parameter Sysctl | Nilai Default Tipikal | Nilai Tuning High-Throughput NVMe | Mekanisme & Dampak Lapisan Kernel |
|---|---|---|---|
vm.dirty_background_ratio | 10% | 5% | Ambang batas kswapd/flusher thread mulai menulis dirty page ke disk di latar belakang tanpa memblokir thread pengguna. |
vm.dirty_ratio | 20% | 10% | Batas mutlak memori dirty. Jika terlampaui, proses yang memanggil write() akan dialihkan ke direct reclaim/writeout sinkron. |
vm.dirty_expire_centisecs | 3000 (30 detik) | 1000 (10 detik) | Usia maksimum data kotor di page cache sebelum wajib ditandai untuk writeback. Memperpendek durasi ini mencegah lonjakan antrean I/O masif. |
vm.dirty_writeback_centisecs | 500 (5 detik) | 200 (2 detik) | Interval bangun berkala kernel flusher thread untuk mengevaluasi dirty pages. |
fdatasync, pelacakan latensi I/O dapat dilakukan secara langsung menggunakan bpftrace:
BASH
# Melacak durasi eksekusi fdatasync pada thread cleanup Postfix
sudo bpftrace -e '
tracepoint:syscalls:sys_enter_fdatasync /comm == "cleanup"/ {
@start[tid] = nsecs;
}
tracepoint:syscalls:sys_exit_fdatasync /@start[tid]/ {
@latency_us = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}'Pemisahan Beban Melalui Postfix Multi-Instance Manager
qmgr tidak dapat diubah hanya dengan memodifikasi parameter main.cf, solusi arsitektural yang tepat adalah membagi beban spooling ke beberapa instance Postfix independen menggunakan framework postmulti.
Dalam konfigurasi multi-instance:
- Instance Null-Client / Inbound (
postfix-in): Bertugas khusus mendengarkan port 25/587, menjalankan filter spam/virus, memvalidasi SPF/DKIM/DMARC, dan menerima injeksi pesan lokal viasendmailbinary. Instance ini tidak melakukan external delivery langsung ke internet publik. - Loopback Dispatcher: Instance inbound mengalihkan pesan ke instance outbound melalui transport round-robin atau domain-hash hashing ke port internal loopback (misalnya
127.0.0.1:10026,127.0.0.1:10027, dst.). - Instance Outbound dedicated (
postfix-out-01,postfix-out-02, ...): Setiap instance memiliki direktori spool sendiri (/var/spool/postfix-out-01), file konfigurasi sendiri (/etc/postfix-out-01), dan yang paling penting: prosesqmgrsendiri. Masing-masing instance outbound dapat diikatkan ke alamat IP publik yang berbeda jika tersedia, atau berbagi IP dengan connection pooling terpisah.
TEXT
Internet / Ingestion
|
v
[Postfix Inbound Instance]
(spool: /var/spool/postfix)
|
+-----------------+-----------------+
| |
(127.0.0.1:10026) (127.0.0.1:10027)
v v
[Postfix Outbound 1] [Postfix Outbound 2]
(spool: /var/spool/postfix-out-01) (spool: /var/spool/postfix-out-02)
(independent qmgr) (independent qmgr)
| |
+-----------------+-----------------+
|
v
Egress (NVMe)qmgr dapat berjalan secara paralel pada core CPU yang berbeda tanpa saling memperebutkan lock internal, menggandakan atau melipatgandakan batas throughput penanganan antrean.
Langkah Konfigurasi postmulti
BASH
# Mengaktifkan manajemen multi-instance pada instalasi default
sudo postmulti -e init
# Membuat instance outbound pertama
sudo postmulti -I postfix-out-01 -G mta-outbound -e create
# Membuat instance outbound kedua
sudo postmulti -I postfix-out-02 -G mta-outbound -e create/etc/postfix-out-01 dan /var/spool/postfix-out-01. Konfigurasi main.cf dari instance default (/etc/postfix/main.cf) kemudian disesuaikan agar hanya berfungsi sebagai penampung awal yang meneruskan beban ke instance pengirim:
INI
# /etc/postfix/main.cf (Inbound / Null Router)
myhostname = mx1.domainanda.id
mydomain = domainanda.id
inet_interfaces = all
inet_protocols = ipv4
# Direktori spool default
queue_directory = /var/spool/postfix
# Forward seluruh email outbound ke instance downstream via random relay
default_transport = smtp_balanced
relayhost =
# Matikan pengiriman lokal langsung
local_transport = error:local mail delivery is disabled/etc/postfix/master.cf untuk mendistribusikan pesan ke kedua instance outbound melalui loopback port:
TEXT
# Menambahkan custom transport di /etc/postfix/master.cf (Inbound)
smtp_balanced_1 unix - - n - - smtp
-o smtp_bind_address=0.0.0.0
-o default_destination_concurrency_limit=50
smtp_balanced_2 unix - - n - - smtp
-o smtp_bind_address=0.0.0.0
-o default_destination_concurrency_limit=50/etc/postfix/transport:
TEXT
# Round-robin mapping ke port internal instance outbound
* relay:[127.0.0.1]:10026/etc/postfix-out-01/main.cf):
INI
# /etc/postfix-out-01/main.cf
master_service_disable =
authorized_submit_users = root
multi_instance_name = postfix-out-01
multi_instance_group = mta-outbound
multi_instance_enable = yes
# Network listening: Hanya mendengarkan pada internal port 10026
inet_interfaces = 127.0.0.1
master_service_disable =
# Isolasi Spool Directory
queue_directory = /var/spool/postfix-out-01
data_directory = /var/lib/postfix-out-01
# Tuning Queue Manager & Concurrency
qmgr_message_active_limit = 40000
qmgr_message_recipient_limit = 40000
default_destination_concurrency_limit = 20
default_destination_recipient_limit = 50
smtp_destination_concurrency_limit = 20
smtp_connect_timeout = 10s
smtp_helo_timeout = 10s
smtp_mail_timeout = 15s
# Hashing queue names untuk memecah directory traversal
hash_queue_names = incoming,active,deferred,bounce,defer,trace
hash_queue_depth = 2/etc/postfix-out-01/master.cf untuk memastikan port internal mendengarkan koneksi dari instance inbound:
TEXT
127.0.0.1:10026 inet n - n - - smtpd
-o smtpd_client_restrictions=permit_mynetworks,reject
-o smtpd_recipient_restrictions=permit_mynetworks,reject
-o mynetworks=127.0.0.0/8
-o smtpd_authorized_xforward_hosts=127.0.0.0/8/etc/postfix-out-02/main.cf dengan port 127.0.0.1:10027 dan path /var/spool/postfix-out-02. Setiap instance kini beroperasi secara independen dan memanfaatkan thread execution model terpisah di Linux.
Peran Kontrol Kongesti TCP BBR pada Egress Network SMTP
- Ketika satu paket segmen email mengalami drop, Cubic memangkas ukuran congestion window (
cwnd) hingga 30%–50% secara instan. - Throughput koneksi SMTP anjlok drastis.
- Waktu transfer payload email memanjang, menyebabkan worker delivery agent
smtpPostfix tertahan lebih lama untuk melayani satu koneksi (high connection hold time). - Batas konkurensi worker (
default_destination_concurrency_limit) tercapai dengan cepat, memicu antrean Postfix menumpuk kembali ke direktoriactive.
TEXT
Transmisi Paket SMTP via TCP Cubic:
RTT normal : [DATA] -> [ACK] (cwnd naik linear)
Packet Drop : [x] (Drop acak di rute internasional)
Reaksi Cubic: cwnd dipangkas 50% -> throughput jatuh -> delivery worker tertahan
Transmisi Paket SMTP via TCP BBR:
BBR Probe : Mengukur Max Bandwidth & Min RTT secara berkala
Packet Drop : Mengabaikan packet drop tunggal jika RTT tidak melonjak
Reaksi BBR : Transmisi dipacu stabil (Pacing Rate) -> delivery worker cepat rilissmtp Postfix menyelesaikan sesi pengiriman lebih cepat, dan slot konkurensi segera dibebaskan untuk memproses item berikutnya di antrean.
Verifikasi dan Implementasi TCP BBR pada Linux Kernel
fq atau fq_codel) pada lapisan packet scheduler (qdisc) Linux agar mekanisme packet pacing dapat berfungsi secara presisi.
Buka atau buat file /etc/sysctl.d/99-network-bbr.conf:
INI
# Mengatur default queuing discipline ke fq (wajib untuk pacing BBR)
net.core.default_qdisc = fq
# Mengaktifkan BBR v1/v2 sebagai algoritma kontrol kongesti default
net.ipv4.tcp_congestion_control = bbr
# Mengoptimalkan ukuran socket buffer untuk high BDP link
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
# Mengurangi latency socket out queue dengan notsent limit
net.ipv4.tcp_notsent_lowat = 16384
# Menutup koneksi FIN secara agresif untuk menghemat socket descriptor
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_tw_reuse = 1BASH
sudo sysctl --systemBASH
# Memeriksa kontrol kongesti aktif
sysctl net.ipv4.tcp_congestion_control
# Output yang diharapkan: net.ipv4.tcp_congestion_control = bbr
# Memeriksa status pacing BBR pada koneksi SMTP outbound aktif
ss -t -i 'dport = :25'ss akan menunjukkan metrik telemetri runtime socket:
TEXT
ESTAB 0 0 103.150.x.x:43210 142.250.x.x:25
bbr wscale:7,7 rto:220 rtt:18.42/0.12 ato:40 mss:1460 rcvspace:14600
pacing_rate 12.5Mbps delivery_rate 11.8Mbps app_limited
bw_rtts:10 mrtt:18.3 min_rtt:18.3pacing_rate dan delivery_rate mengonfirmasi bahwa kernel Linux secara langsung membatasi pelepasan burst segmen paket sesuai estimasi bandwidth BBR, menghindarkan switch ISP dari fenomena tail-drop.
Optimasi Filesystem Spool pada Media Penyimpanan NVMe
/var/spool/postfix di dalam partisi root (/). Pada Cloud VPS dengan storage NVMe, sangat disarankan untuk memisahkan spool direktori ke mount point independen atau minimal menyetel mount options yang memangkas latensi journal barrier.
Jika menggunakan filesystem ext4, opsi mount berikut wajib diterapkan pada partisi spool di /etc/fstab:
TEXT
# Konfigurasi /etc/fstab untuk partisi spool Postfix
UUID=xxxx-xxxx-xxxx-xxxx /var/spool/postfix ext4 noatime,nodiratime,data=writeback,barrier=0,nobh,commit=60 0 2| Parameter Mount Ext4 | Fungsi Teknis Lapisan Filesystem | Konsekuensi Keandalan / Trade-off |
|---|---|---|
noatime,nodiratime | Meniadakan penulisan inode access timestamp saat file dibaca atau direktori di-scan oleh qmgr. | Mengurangi 50% operasi write acak pada metadata. Tidak ada risiko korupsi data. |
data=writeback | Metadata tetap dicatat di jurnal, namun payload data file dapat ditulis ke disk setelah metadata diperbarui. | Data payload yang belum tersinkronisasi bisa hilang jika terjadi hard crash server, namun struktur filesystem tetap utuh. |
barrier=0 | Mematikan pengiriman instruksi write barrier (flush cache flush perintah SCSI/NVMe) pada setiap commit jurnal. | Meningkatkan throughput penulisan metadata drastis. Hanya aman digunakan jika storage virtual backed oleh sistem host dengan UPS/battery-backed write cache (BBWC). |
commit=60 | Memperpanjang interval sinkronisasi metadata jurnal dari 5 detik menjadi 60 detik. | Mengurangi I/O contention jurnal metadata secara masif, memaksimalkan penggunaan Linux page cache. |
XFS, gunakan opsi konfigurasi mount berikut:
TEXT
UUID=xxxx-xxxx-xxxx-xxxx /var/spool/postfix xfs noatime,nodiratime,logbufs=8,logbsize=256k,allocsize=64M 0 2logbsize=256k memperbesar buffer pencatatan log transaksi XFS di memori utama sebelum di-flush ke storage NVMe, meminimalisir penahanan lock pada operasi metadata VFS.
Parameter Tuning Postfix untuk Throughput Maksimal
main.cf dari instance outbound (postfix-out-01, postfix-out-02) untuk mendukung konkurensi tingkat tinggi:
INI
# ----------------------------------------------------------------------------
# PROSES & WORKER MANAGEMENT
# ----------------------------------------------------------------------------
# Memperbesar batas worker smtpd dan smtp secara global
default_process_limit = 250
# Mencegah worker terminasi terlalu cepat untuk mengurangi cost fork() kernel
max_idle = 60s
max_use = 1000
# ----------------------------------------------------------------------------
# QUEUE MANAGER INTERNAL SCHEDULING
# ----------------------------------------------------------------------------
# Jumlah pesan yang dipertahankan qmgr di active memory pool
qmgr_message_active_limit = 50000
# Mempercepat siklus pembacaan direktori antrean baru
qmgr_fudge_factor = 100
# Waktu penundaan pemindaian antrean incoming saat sepi
slow_flush_warning_time = 0
queue_run_delay = 180s
minimal_backoff_time = 180s
maximal_backoff_time = 1800s
# ----------------------------------------------------------------------------
# CONCURRENCY PER DESTINATION (DOMAIN RATE LIMITING)
# ----------------------------------------------------------------------------
# Batas koneksi paralel ke satu domain tujuan (misal gmail.com, yahoo.com)
default_destination_concurrency_limit = 25
smtp_destination_concurrency_limit = 25
# Nilai konkurensi minimum saat tujuan merespons lambat (slow start rate)
default_destination_concurrency_failed_cohort_limit = 5
default_destination_concurrency_positive_feedback = 1/5
default_destination_concurrency_negative_feedback = 1/8
# ----------------------------------------------------------------------------
# SMTP CLIENT CONNECTION & TIMEOUTS
# ----------------------------------------------------------------------------
# Mempersingkat timeout network agar thread tidak hung pada remote server zombie
smtp_connect_timeout = 8s
smtp_helo_timeout = 10s
smtp_mail_timeout = 10s
smtp_rcpt_timeout = 10s
smtp_data_init_timeout = 10s
smtp_data_xfer_timeout = 20s
smtp_data_done_timeout = 15s
smtp_quit_timeout = 5s
# Connection Cache Reuse (Mencegah handshake TLS berulang ke remote server yang sama)
smtp_connection_cache_on_demand = yes
smtp_connection_cache_reuse_limit = 10
smtp_connection_cache_time_limit = 120spostmulti:
BASH
# Memvalidasi syntax seluruh instance
sudo postmulti -p status
# Reload konfigurasi pada seluruh instance outbound saja
sudo postmulti -g mta-outbound -p reloadEvaluasi dan Hasil Benchmark Throughput
smtp-source.
Skenario benchmark membandingkan:
- Konfigurasi A (Baseline): Single-instance Postfix standar, filesystem ext4 default, kontrol kongesti TCP Cubic.
- Konfigurasi B (Optimized): Multi-instance Postfix (1 Inbound + 3 Outbound instances via
postmulti), filesystem ext4 dengan tuning metadata (barrier=0, commit=60, data=writeback), dan kontrol kongesti TCP BBR +fq.
| Metrik Pengujian | Konfigurasi A (Baseline Single-Instance) | Konfigurasi B (Multi-Instance + BBR + NVMe Tune) | Delta Performa |
|---|---|---|---|
| Egress Delivery Rate (Pesan/Detik) | 128 msgs/sec | 642 msgs/sec | +401.5% |
| Puncak IOPS Tulis Disk Spool | 4.820 IOPS | 1.150 IOPS (Aggregated) | -76.1% (Lebih efisien) |
Latensi Eksekusi fdatasync() (p99) | 18.4 ms | 1.2 ms | -93.4% |
Utilisasi CPU Core Tertinggi (qmgr) | 100% (Single Core Bottleneck) | 38% (Terdistribusi di 4 Core) | Load seimbang |
| Durasi Pengosongan Antrean 150k | 19 menit 32 detik | 3 menit 53 detik | -80.1% waktu tunggu |
| Socket Stalled Timeout Counts | 842 insiden | 14 insiden | -98.3% |
qmgr dan penghapusan stall transport TCP melalui kontrol kongesti BBR.