Worker antrian di lingkungan produksi umumnya bergantung pada runtime tingkat tinggi seperti Python (Celery) atau Node.js (BullMQ). Meskipun ekosistem tersebut menawarkan kemudahan integrasi, overhead memori per proses (biasanya 30-100 MB per worker) dan jeda eksekusi akibat Garbage Collection (GC) membatasi throughput pada sistem dengan latensi rendah. Mengadopsi arsitektur single-binary menggunakan pure C memungkinkan eksekusi deterministik dengan alokasi memori statis di bawah 5 MB per worker serta eliminasi network round-trip melalui mekanisme Inter-Process Communication (IPC) lokal.
Arsitektur IPC: Shared Memory vs POSIX Message Queue
Untuk mendistribusikan job antar proses pada satu mesin tanpa overhead socket TCP, Linux menyediakan dua mekanisme IPC berkecepatan tinggi: POSIX Shared Memory (shm_open) dan POSIX Message Queue (mq_open).
POSIX Message Queue ideal untuk transfer pesan berukuran kecil (< 8 KB) dengan prioritas bawaan kernel. Namun, kernel tetap menyalin data dari user-space ke kernel-space, lalu kembali ke user-space worker. Untuk beban kerja tinggi dengan payload lebih besar, kombinasi Shared Memory Ring Buffer dan POSIX Mutex memberikan performa zero-copy optimal.
- Shared Memory (shm): Menyimpan struct antrian mentah dan payload pekerjaan langsung di RAM yang dipetakan (
mmap) ke virtual memory kedua proses. - Robust Mutex (pthread): Berada di dalam shared memory untuk mengoordinasikan pembacaan (dequeue) dan penulisan (enqueue) antar proses secara thread-safe dan process-safe.
Sinkronisasi Deterministik dan Mitigasi Worker Crash
Tantangan utama shared memory antar proses adalah penanganan anomali saat worker mengalami crash (misalnya akibat SIGSEGV atau SIGKILL) ketika sedang menahan mutex antrian. Jika menggunakan mutex standar, mutex akan terkunci permanen (deadlock) dan menghentikan seluruh pemrosesan antrian.
Solusinya adalah mengonfigurasi pthread mutex dengan atribut PTHREAD_MUTEX_ROBUST dan PTHREAD_PROCESS_SHARED. Saat proses pemegang lock mati mendadak, kernel Linux menandai mutex tersebut dan memicu return value EOWNERDEAD pada proses berikutnya yang memanggil pthread_mutex_lock().
Ketika worker lain mendeteksi EOWNERDEAD, worker tersebut bertanggung jawab memulihkan integritas state antrian dan memanggil pthread_mutex_consistent() sebelum melepas lock kembali.
Implementasi Worker: Non-blocking Queue dan Signal Cleanup
Berikut implementasi consumer non-blocking yang membaca job dari circular ring buffer di shared memory, dilengkapi mitigasi EOWNERDEAD dan graceful termination via signal handling:
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <pthread.h>
#define SHM_NAME "/job_queue_shm"
#define QUEUE_CAPACITY 1024
#define PAYLOAD_SIZE 256
typedef struct {
uint64_t job_id;
uint32_t payload_len;
char data[PAYLOAD_SIZE];
} job_t;
typedef struct {
pthread_mutex_t lock;
size_t head;
size_t tail;
size_t count;
job_t jobs[QUEUE_CAPACITY];
} shared_queue_t;
static volatile sig_atomic_t g_running = 1;
static void handle_signal(int sig) {
(void)sig;
g_running = 0;
}
static bool dequeue_job(shared_queue_t *queue, job_t *out_job) {
int rc = pthread_mutex_trylock(&queue->lock);
if (rc == EOWNERDEAD) {
/* Worker sebelumnya crash saat memegang lock. Pulihkan state antrian. */
pthread_mutex_consistent(&queue->lock);
} else if (rc != 0) {
return false;
}
bool has_job = false;
if (queue->count > 0) {
*out_job = queue->jobs[queue->head];
queue->head = (queue->head + 1) % QUEUE_CAPACITY;
queue->count--;
has_job = true;
}
pthread_mutex_unlock(&queue->lock);
return has_job;
}
int main(void) {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = handle_signal;
sigaction(SIGTERM, &sa, NULL);
sigaction(SIGINT, &sa, NULL);
int shm_fd = shm_open(SHM_NAME, O_RDWR, 0660);
if (shm_fd < 0) {
perror("shm_open");
return EXIT_FAILURE;
}
shared_queue_t *queue = mmap(NULL, sizeof(shared_queue_t),
PROT_READ | PROT_WRITE,
MAP_SHARED, shm_fd, 0);
close(shm_fd);
if (queue == MAP_FAILED) {
perror("mmap");
return EXIT_FAILURE;
}
job_t current_job;
while (g_running) {
if (dequeue_job(queue, ¤t_job)) {
/* Eksekusi payload tanpa blocking kernel IO */
write(STDOUT_FILENO, current_job.data, current_job.payload_len);
} else {
/* Hindari busy-wait murni saat antrian kosong */
usleep(1000);
}
}
munmap(queue, sizeof(shared_queue_t));
return EXIT_SUCCESS;
}Perbandingan Karakteristik: C vs High-Level Worker
Membangun antrian dengan arsitektur C murni mengubah profil konsumsi sumber daya sistem secara signifikan dibanding arsitektur berbasis interpreter atau JIT.
- Memory Footprint: Worker C mengonsumsi RSS (Resident Set Size) statis di kisaran 1-4 MB, sedangkan runtime Node.js atau Python membutuhkan 30-100 MB per worker process hanya untuk inisialisasi runtime dan module tree dasar.
- Overhead Alokasi: Penggunaan ring buffer tetap (statically-sized chunks) menghilangkan overhead alokasi dinamis (
malloc/free) di hot path pemrosesan antrian. - CPU Jitter: Tidak adanya background GC thread menghasilkan latensi eksekusi persentil 99 (p99) yang konsisten di sub-milidetik.
- IPC Throughput: Zero-copy memory access menghilangkan context-switch berulang ke kernel network stack jika dibandingkan komunikasi Redis via loopback interface (
127.0.0.1).
Keterbatasan dan Trade-offs
Arsitektur single-binary C worker bukan pengganti mutlak solusi enterprise queue terdistribusi. Batasan arsitektur ini meliputi:
- Skalabilitas Terbatas Satu Node: Shared memory murni hanya bekerja pada batasan IPC mesin lokal. Jika butuh distribusi beban multi-mesin, layer transport jaringan tambahan (seperti TCP atau UDP socket) tetap harus diimplementasikan.
- Strict Schema: Payload antrian sebaiknya berukuran tetap (fixed-size struct). Mendukung arbitrary payload sizes di shared memory memerlukan implementasi memory allocator kustom (arena allocator atau buddy allocator).
- Keamanan Memori: Bug pada pointer logic di satu worker dapat merusak memori antrian yang sedang diakses worker lain, sehingga penulisan kode harus diverifikasi ketat menggunakan tooling seperti AddressSanitizer (ASan) dan Valgrind.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!