Anatomi Runtuhnya Koneksi: SYN Queue vs Accept Queue
accept4()).
Pada sebagian besar distribusi Linux untuk edge gateway, nilai default net.core.somaxconn dan parameter backlog broker sering kali masih berada di angka konservatif (misalnya 128 atau 512). Begitu laju koneksi datang melampaui kemampuan event loop broker untuk memanggil accept(), Accept queue akan penuh seketika. Secara default, Linux tidak merespons dengan paket TCP RST ketika antrean ini meluap; kernel cukup membuang (drop) paket ACK ketiga yang masuk. Mikrokontroler mengira koneksi sudah terbangun dan mengirim paket MQTT CONNECT, namun gateway tidak mengakuinya. Perangkat IoT kemudian mengalami timeout, menutup soket secara sepihak, dan memulai ulang siklus SYN baru. Siklus ganas ini melipatgandakan beban jabat tangan secara eksponensial di tingkat antarmuka jaringan.
BASH
# Verifikasi drop pada TCP accept queue di gateway
netstat -s | grep -i "listen drops"
# Tingkatkan batas antrean soket di tingkat kernel (/etc/sysctl.d/99-edge.conf)
net.core.somaxconn = 8192
net.ipv4.tcp_max_syn_backlog = 8192
net.ipv4.tcp_abort_on_overflow = 0tcp_abort_on_overflow = 0 memastikan kernel tetap bersikap defensif dengan membiarkan klien mencoba retransmisi ACK, alih-alih melempar RST yang memicu reset logika instan pada firmware mikrokontroler yang belum tentu menangani error RST dengan anggun.
File Descriptor Churn dan Jebakan TIME_WAIT
TIME_WAIT selama durasi 2MSL (Maximum Segment Lifetime, default Linux 60 detik).
Ketika ribuan perangkat mencoba menyambung ulang setiap 2 detik, soket dalam status TIME_WAIT menumpuk jauh lebih cepat daripada laju daur ulangnya oleh kernel. Kondisi ini memicu error klasik EMFILE (Too many open files) pada proses broker. Lebih parah lagi, jika gateway juga bertindak sebagai jembatan (MQTT bridge) yang meneruskan data lokal ke cloud melalui koneksi balik yang fluktuatif, kehabisan ephemeral port lokal (net.ipv4.ip_local_port_range) akan melumpuhkan komunikasi keluar gateway seutuhnya.
BASH
# Optimasi pemanfaatan kembali soket TIME_WAIT untuk koneksi keluar
net.ipv4.tcp_tw_reuse = 1
net.ipv4.ip_local_port_range = 10240 65535
# Pastikan limit file descriptor systemd service broker diperbesar
# /etc/systemd/system/mosquitto.service.d/limits.conf
[Service]
LimitNOFILE=65536Epoll Starvation: Saat Keep-Alive Ikut Tercekik
epoll. Dalam satu putaran event loop, broker memanggil epoll_wait(), memproses aktivitas deskriptor yang terdaftar, lalu kembali menunggu event baru. Ketika connection storm melanda, listen socket broker akan terus-menerus memancarkan event EPOLLIN yang menandakan tersedianya koneksi baru untuk diambil.
Di sinilah anomali event loop starvation terjadi. Jika kode internal broker dirancang untuk menguras seluruh koneksi baru secara serakah (greedy accept) sebelum melayani antrean soket klien yang sudah aktif, CPU core gateway akan sepenuhnya terpakai untuk menangani alokasi buffer koneksi baru dan jabat tangan TLS/SSL yang memakan banyak siklus komputasi. Klien yang sebelumnya sudah terhubung stabil tidak mendapatkan giliran untuk memproses paket MQTT PINGREQ (keep-alive) tepat waktu. Broker menduga klien lama tersebut telah hang atau mati karena durasi keep-alive terlewati, lalu memutus soket mereka secara paksa. Klien yang baru saja diputus ini seketika berbalik memicu SYN baru, mengunci gateway dalam siklus degradasi kaskade (cascading failure) yang tidak bisa pulih dengan sendirinya tanpa intervensi pemutusan daya fisik.
Broker modern mengatasinya dengan menerapkan batasan kuota koneksi per tick: hanya memproses sejumlah N pemanggilan accept() per putaran epoll_wait(), memastikan trafik I/O klien yang sudah hidup tetap mendapatkan porsi komputasi yang adil.
Menjinakkan Thundering Herd dari Sisi Edge Firmware
C
// Logika rekoneksi dengan Decorrelated Jitter pada ESP32 / FreeRTOS
uint32_t calculate_backoff(uint32_t prev_sleep, uint32_t min_sleep, uint32_t max_sleep) {
uint32_t upper_bound = prev_sleep * 3;
if (upper_bound > max_sleep) upper_bound = max_sleep;
uint32_t sleep_ms = min_sleep + (esp_random() % (upper_bound - min_sleep + 1));
return sleep_ms;
}