Bali, Indonesia
Insights

Menambal Kebocoran Socket: Mengapa TCP Keepalive Gagal Menyelamatkan Pool Database di Balik Envoy

October 14, 2026
0:00
Pernahkah Anda terbangun jam tiga pagi karena alert 'Connection pool exhausted' dari service backend, sementara dashboard PostgreSQL atau MySQL di hilir menunjukkan beban CPU di bawah sepuluh persen dan mayoritas koneksi berstatus idle? Pola insiden seperti ini sering kali muncul justru setelah tim infrastruktur melakukan modernisasi dengan menyelipkan Envoy sebagai service mesh sidecar atau L4 TCP proxy di depan database. Naluri pertama engineer biasanya langsung memeriksa sysctl Linux, menyalakan 'tcp_keepalive_time = 60', lalu mengira masalah selesai. Nyatanya, seminggu kemudian insiden serupa berulang. Masalah mendasarnya bukan pada kernel Linux yang lalai, melainkan ilusi bahwa mekanisme transport layer (L4) bisa menyelesaikan masalah stateful connection pool di layer aplikasi. TCP keepalive dirancang pada era komputasi jaringan yang sangat berbeda, di mana fungsi utamanya hanyalah mendeteksi apakah kabel jaringan fisik putus atau host di ujung sana mengalami hard crash. Paket probe keepalive adalah segmen TCP kosong (atau berisi satu byte payload sampah) dengan sequence number 'SEG.SEQ = SND.NXT - 1'. Dari kacamata stack TCP lawan, probe ini hanya memicu ACK balasan secara otomatis di dalam kernel space tanpa pernah menyentuh, membangunkan, apalagi mereset idle timer aplikasi database di user space.
Ketika Envoy ditaruh di tengah aliran trafik antara client pool (misalnya HikariCP atau pgx) dan database engine, arsitektur TCP kita tidak lagi berupa satu koneksi 'end-to-end'. Ada dua koneksi independen yang terbentuk: downstream (Client ke Envoy) dan upstream (Envoy ke Database). Masing-masing segmen memiliki socket buffer, sequence tracking, dan timer timeout sendiri.
TEXT
+----------------+         +---------------+         +-----------------+
| Client Pool    |         | Envoy Proxy   |         | PostgreSQL / DB |
| (App Server)   |         | (L4 TCP/Mesh) |         | Engine          |
+----------------+         +---------------+         +-----------------+
        |                          |                          |
        |====== Downstream TCP ===>|====== Upstream TCP =====>|
        |                          |                          |
        |  (Client Pool Idle: 10m) |  (Envoy Idle Timeout: 5m)|
        |                          |                          |
        |                          x [Envoy idle_timeout!]    |
        |                          |--- FIN/RST ------------->|
        |<-- FIN ------------------|                          |
        | (Masuk CLOSE_WAIT)       |                          |
        v                          v                          v
Bencana dimulai ketika Envoy memutuskan koneksi downstream karena melampaui batas 'idle_timeout'. Katakanlah Envoy dikonfigurasi dengan idle timeout default 5 menit, sementara connection pool aplikasi mengizinkan koneksi idle bertahan hingga 10 menit. Ketika koneksi downstream tidak aktif selama 5 menit, Envoy akan mengirimkan paket TCP FIN ke client. Kernel di host aplikasi menerima FIN tersebut dan otomatis membalas dengan ACK, lalu menempatkan socket lokal ke status 'CLOSE_WAIT'. Namun, connection pool di aplikasi tidak tahu apa-apa soal perubahan ini. Selama pool aplikasi belum mencoba membaca (read) atau menulis (write) ke file descriptor tersebut, socket itu akan tetap dianggap valid dan sehat di dalam pool. Ketika traffic lonjak dan aplikasi meminjam koneksi tersebut via 'getConnection()', aplikasi baru menyadari socket sudah mati saat query dilempar. Jika driver tidak menangani error ini dengan failover yang agresif, thread akan hang menunggu respons sampai socket timeout aplikasi habis, antrean request meledak, dan pool kolaps seketika.
Banyak engineer mencoba memperbaiki kondisi di atas dengan menyalakan tcp_keepalive di Envoy listener atau cluster configuration. Harapannya, paket probe keepalive akan menjaga koneksi tetap hidup selamanya. Ini adalah perangkap logika. Envoy mengukur keaktifan koneksi berdasarkan aliran data aktual (payload data transfer), bukan paket probe L4 TCP. Probe TCP keepalive ditangani sepenuhnya oleh stack TCP kernel host tanpa melibatkan Envoy event loop. Karena Envoy tidak melihat adanya bytes yang diproses di L7 (atau data L4 yang diteruskan), internal timer Envoy tetap berdetik menuju batas timeout. Dari sudut pandang Envoy, koneksi tersebut tetap menganggur. Akibatnya, Envoy akan tetap mengeksekusi penutupan koneksi secara sepihak, menabrak probe keepalive yang sibuk lalu lalang di kernel. Lebih parah lagi jika di antara Envoy dan database terdapat NAT gateway atau stateful firewall (misalnya AWS NAT Gateway atau cloud load balancer). Perangkat-perangkat perantara ini memiliki connection tracking table (conntrack) dengan batas idle time yang ketat. Jika conntrack table menghapus mapping sesi karena dianggap idle tanpa mengirimkan FIN/RST ke kedua belah pihak, socket upstream akan masuk ke kondisi half-open. Upstream database masih menganggap koneksi aktif, sementara Envoy atau client yang mencoba mengirim query berikutnya akan terjebak dalam lubang hitam retransmisi TCP hingga akhirnya menerima timeout error yang lambat.
Solusi permanen untuk menambal kebocoran socket ini tidak berada di tweaking sysctl /proc/sys/net/ipv4/tcp_keepalive_*, melainkan pada sinkronisasi hierarki umur koneksi di tiga titik: Client Pool, Proxy, dan Database Server. Prinsip dasarnya sederhana: Koneksi harus selalu dimatikan secara proaktif oleh pihak yang paling dekat dengan inisiator (Client Pool), sebelum perantara atau server hilir kehabisan kesabaran. Rumus aman untuk menentukan timeout adalah: Pool Max Lifetime < Proxy Idle Timeout < Database Server Idle Timeout
YAML
# Snippet konfigurasi Envoy cluster
clusters:
  - name: postgres_backend
    connect_timeout: 2s
    type: STRICT_DNS
    dns_lookup_family: V4_ONLY
    typed_extension_protocol_options:
      envoy.extensions.upstreams.net.generic.v3.GenericConnectionPoolProto:
        "@type": type.googleapis.com/envoy.extensions.upstreams.net.generic.v3.GenericConnectionPoolProto
    # Pastikan idle_timeout L4 lebih longgar dari client pool idle time
    common_network_config:
      idle_timeout: 900s
Jika Envoy disetel dengan idle_timeout: 900s (15 menit), maka connection pool di sisi aplikasi (misalnya HikariCP) harus disetel dengan maxLifetime maksimal 10 atau 12 menit, dan idleTimeout di kisaran 5 hingga 8 menit. Dengan begitu, client pool akan menutup koneksi secara elegan melalui protokol database native sebelum Envoy atau middlebox memutuskan sambungan secara sepihak.
Langkah terakhir yang membedakan setup amatir dan production-grade adalah memindahkan tanggung jawab health check dari L4 TCP ke layer protokol database. Daripada mengandalkan probe kernel yang buta, aktifkan keepalive native milik database driver. Di PostgreSQL misalnya, parameter client-side seperti keepalives_idle, keepalives_interval, dan keepalives_count memang menyentuh socket option TCP_KEEPIDLE, tapi driver modern menyediakan mekanisme validasi koneksi yang jauh lebih superior. HikariCP memiliki fitur keepaliveTime yang secara periodik mengirimkan query ping ringan atau mengeksekusi JDBC isValid() pada koneksi yang sedang menganggur di pool. Karena ping ini membawa payload data riil, event loop di Envoy akan mendeteksinya sebagai aktivitas transfer yang valid, mereset idle timer Envoy, dan sekaligus memverifikasi bahwa query engine di database memang masih sanggup merespons. Menjaga stabilitas infrastruktur modern yang dipenuhi layer proxy dan abstraction bukan tentang menumpuk lebih banyak timeout defensif, melainkan menyelaraskan siklus hidup koneksi secara sadar dari layer soket kernel hingga runtime aplikasi. Dengan mengonfigurasi TCP socket buffer secara proporsional dan mengaktifkan heartbeat aktif di level protokol database, ancaman silent socket leak dapat dieliminasi secara tuntas dari arsitektur produksi.