Post-Mortem & Incident Runbook: Serverless WebSockets di AWS
1. Alarm Tengah Malam: Saat Layanan Produksi Mulai Tumbang
Pukul 02:14 WIB, pager di meja samping tempat tidurku menjerit melengking, membangunkan tidur nyenyak. Layar ponsel menampilkan notifikasi merah dari PagerDuty: metrik latensi WebSocket melonjak drastis dari rata-rata 45 milidetik menjadi lebih dari 8.000 milidetik, dibarengi dengan lonjakan error 504 Gateway Timeout di dashboard CloudWatch. Sebagai Senior Staff Engineer yang bertanggung jawab atas infrastruktur real-time, malam ini berubah menjadi investigasi forensik kilat. Pengguna kami, yang mayoritas adalah trader finansial dan pengguna aplikasi kolaborasi real-time, mulai mengeluh bahwa pesan chat dan pembaruan grafik harga terputus total. Koneksi klien yang biasanya stabil tiba-tiba berguguran seperti daun di musim gugur, dan tagihan AWS billing harian melonjak tajam tanpa kendali yang jelas.
Sambil membuka laptop di dalam kamar yang remang-remang, aku langsung masuk ke terminal dan menyalakan VPN korporat. Ada dua hal yang langsung terpikir di kepala: apakah lambatnya layanan ini disebabkan oleh lonjakan trafik organik yang tak terduga, atau ada kebocoran sumber daya di lapisan backend yang membuat AWS API Gateway WebSocket kewalahan? Untuk menjawabnya, kita tidak bisa menebak-nebak; kita harus turun langsung ke dalam mesin, menelusuri log, dan membedah metrik satu per satu.
2. Jejak Digital: Menelusuri Anomali Log & Lonjakan Metrik
Tanpa membuang waktu, aku langsung melompat ke AWS CloudWatch Insights untuk menarik log dari AWS API Gateway dan AWS Lambda yang bertindak sebagai otak di balik komunikasi WebSocket ini. Kueri log awal langsung memperlihatkan pola yang sangat aneh namun familier bagi arsitek serverless yang kurang hati-hati:
fields @timestamp, @message
| filter @message like /Task timed out/
| stats count(*) as timeout_count by @logStream
| sort timeout_count desc
| limit 20
Hasilnya mengerikan. Ribuan eksekusi AWS Lambda mengalami timeout setelah berjalan selama 30 detik penuh. Aku beralih ke konsol DynamoDB tempat kami menyimpan pemetaan connectionId klien. Grafiknya menunjukkan kapasitas Read/Write Capacity Units (RCU/WCU) tercekik habis karena lonjakan throttling.
Ternyata, setiap kali ada pesan masuk yang harus disiarkan (broadcast) ke ribuan klien aktif, fungsi Lambda kami melakukan pemindaian tabel (Table Scan) secara brutal ke DynamoDB untuk mencari siapa saja yang sedang online, alih-alih melakukan BatchGet atau query yang terindeks rapi. Lebih parah lagi, arsitektur yang kami bangun gagal mengantisipasi masalah Cold Start pada fungsi Lambda berukuran besar, ditambah tidak adanya penanganan ketika klien memutus koneksi secara paksa, meninggalkan tumpukan stale connections di dalam basis data.
3. Membongkar Biang Kerok: Akar Masalah yang Sebenarnya
Setelah menggali lebih dalam, akar masalah dari insiden dini hari ini mengerucut pada tiga dosa besar arsitektur serverless WebSocket:
- Desain Rute API Gateway yang Naif: Kami mengandalkan
$defaultroute tanpa memetakan aksi spesifik secara efisien, memaksa Lambda memproses logika routing yang berat di dalam kode. - Manajemen DynamoDB yang Buruk: Menyimpan
connectionIdtanpa strategi partisi yang tepat dan melakukan operasiScanalih-alihQueryberbasis indeks sekunder global (GSI). - Abaikan Cold Start & Keep-Alive: Fungsi Lambda ditulis dalam Node.js dengan ukuran deployment monolitik yang membengkak hingga 80MB karena modul pihak ketiga yang tidak perlu, memicu Cold Start hingga 3,2 detik setiap kali terjadi lonjakan trafik mendadak.
[!WARNING] Menggunakan operasi
Scanpada DynamoDB untuk melacak koneksi WebSocket aktif adalah tiket express menuju tagihan AWS bengkak dan pelambatan sistem secara total.
Untuk memperbaiki ini secara permanen, kita harus merombak fondasi kode dan infrastrukturnya. Mari kita lihat bagaimana cara membangun rute, penyimpanan, dan fungsi Lambda yang kebal terhadap lonjakan.
4. Tindakan Darurat: Menghidupkan Kembali Layanan dalam Hitungan Menit
Sambil merencanakan perbaikan jangka panjang, sistem produksi harus diselamatkan sekarang juga. Berikut adalah emergency runbook one-liner berbasis AWS CLI yang aku eksekusi di terminal untuk memangkas concurrency Lambda yang macet dan membersihkan koneksi mati secara paksa:
# Perintah darurat untuk membersihkan koneksi mati dan mereset throttle API Gateway
aws apigatewayv2 get-connections --api-id ${API_ID} --query "items[?connectedAt < `$(date -d '2 hours ago' -u +%Y-%m-%dT%H:%M:%SZ)`].connectionId" --output text | xargs -I {} aws apigatewayv2 delete-connection --api-id ${API_ID} --connection-id {}
Perintah tersebut langsung menyapu bersih ribuan koneksi zombie yang menggantung dan membebani memori Lambda. Sembari trafik mulai berangsur normal, mari kita tulis ulang arsitekturnya agar kejadian ini tidak terulang kembali.
Pertama, kita definisikan rute AWS API Gateway WebSocket yang bersih menggunakan Terraform:
resource "aws_apigatewayv2_api" "websocket_api" {
name = "production-websocket"
protocol_type = "WEBSOCKET"
route_selection_expression = "$request.body.action"
}
resource "aws_apigatewayv2_route" "connect_route" {
api_id = aws_apigatewayv2_api.websocket_api.id
route_key = "$connect"
target = "integrations/${aws_apigatewayv2_integration.lambda_connect.id}"
}
resource "aws_apigatewayv2_route" "disconnect_route" {
api_id = aws_apigatewayv2_api.websocket_api.id
route_key = "$disconnect"
target = "integrations/${aws_apigatewayv2_integration.lambda_disconnect.id}"
}
resource "aws_apigatewayv2_route" "sendmessage_route" {
api_id = aws_apigatewayv2_api.websocket_api.id
route_key = "sendMessage"
target = "integrations/${aws_apigatewayv2_integration.lambda_sendmessage.id}"
}
Selanjutnya, untuk menangani penyimpanan connectionId di DynamoDB tanpa membuat tagihan meledak, kita gunakan skema tabel yang dioptimalkan dengan TTL (Time-To-Live) agar koneksi yang terbengkalai otomatis terhapus:
// Handler untuk $connect - Menyimpan Connection ID ke DynamoDB
const { DynamoDBClient } = require("@aws-sdk/client-dynamodb");
const { DynamoDBDocumentClient, PutCommand } = require("@aws-sdk/lib-dynamodb");
const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);
exports.handler = async (event) => {
const connectionId = event.requestContext.connectionId;
const ttl = Math.floor(Date.now() / 1000) + 7200; // Expired dalam 2 jam jika tidak ada aktivitas
const command = new PutCommand({
TableName: process.env.CONNECTIONS_TABLE,
Item: {
connectionId: connectionId,
ttl: ttl,
connectedAt: new Date().toISOString()
},
});
try {
await docClient.send(command);
return { statusCode: 200, body: 'Connected.' };
} catch (err) {
console.error("Gagal menyimpan koneksi:", err);
return { statusCode: 500, body: 'Failed to connect: ' + JSON.stringify(err) };
}
};
Terakhir, untuk mengatasi Cold Start yang mematikan, kita optimalkan fungsi Lambda dengan menggunakan AWS Lambda Power Tuning, memperkecil bundle size menggunakan esbuild, dan menerapkan Provisioned Concurrency hanya pada jam-jam puncak trafik.
[!TIP] Gunakan
esbuilduntuk melakukan bundling kode Lambda Node.js Anda. Ukuran file yang lebih kecil di bawah 5MB terbukti memangkas waktu Cold Start hingga 70%.
5. Kebijakan Retrospektif: Menambal Celah agar Tak Pernah Terulang
Insiden pukul 2 pagi ini memberikan pelajaran mahal tentang bagaimana memperlakukan arsitektur serverless secara disiplin. Kami mengadakan sesi blameless post-mortem keesokan harinya dan menetapkan aturan baru yang wajib dipatuhi seluruh tim engineering:
- Dilarang Keras Melakukan Scan pada Database Real-time: Semua operasi pencarian koneksi wajib menggunakan partisi kunci yang spesifik atau memanfaatkan Global Secondary Index (GSI) dengan kapasitas On-Demand atau Auto-Scaling yang terkonfigurasi ketat.
- Penerapan TTL Wajib: Setiap entri koneksi di DynamoDB wajib memiliki atribut TTL untuk mencegah penumpukan data basi akibat klien yang terputus tanpa memicu event
$disconnectsecara sempurna. - Optimasi Ukuran Paket Lambda: Setiap fungsi Lambda tidak boleh melebihi ukuran 10MB setelah di-kompres, dan wajib melaluiuji beban (load testing) menggunakan k6 sebelum disetujui masuk ke lingkungan produksi.
6. Pasang Alarm: Otomasi Deteksi Dini untuk Tim On-Call
Agar kita bisa tidur dengan tenang tanpa takut dikejutkan oleh lonjakan latensi mendadak, kita pasang sistem peringatan dini menggunakan CloudWatch Alarms yang terhubung langsung ke webhook Slack dan PagerDuty tim on-call:
resource "aws_cloudwatch_metric_alarm" "websocket_latency_alarm" {
alarm_name = "prod-websocket-high-latency"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "IntegrationLatency"
namespace = "AWS/ApiGateway"
period = 60
statistic = "Average"
threshold = 1000
alarm_description = "Alarm jika latensi integrasi WebSocket API Gateway melebihi 1 detik"
alarm_actions = [aws_sns_topic.pagerduty_alerts.arn]
dimensions = {
ApiId = aws_apigatewayv2_api.websocket_api.id
}
}
resource "aws_cloudwatch_metric_alarm" "lambda_errors_alarm" {
alarm_name = "prod-websocket-lambda-errors"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "Errors"
namespace = "AWS/Lambda"
period = 60
statistic = "Sum"
threshold = 5
alarm_description = "Alarm jika terjadi error pada fungsi Lambda WebSocket"
alarm_actions = [aws_sns_topic.pagerduty_alerts.arn]
dimensions = {
FunctionName = aws_lambda_function.websocket_handler.function_name
}
}
Dengan fondasi arsitektur yang tahan banting, penanganan DynamoDB yang optimal, serta sistem alarm otomatis yang siaga 24/7, ekosistem Serverless WebSocket kami kini siap menangani jutaan koneksi bersamaan dengan biaya operasional yang sangat efisien dan senyum lebar di wajah tim keuangan.