WebSocket và realtime đo thật: từ bắt tay tới 10.000 kết nối
Sê-ri 12 phần cho lập trình viên backend làm tính năng thời gian thực: polling, long polling, SSE và WebSocket so sánh bằng số đo; bắt tay và frame đọc từng byte; heartbeat và phát hiện kết nối chết; backpressure với client chậm; 10.000 kết nối trên một tiến trình Go; fan-out, thứ tự và mất thông điệp khi reconnect; scale ra nhiều máy bằng Redis pub/sub; load test và tail latency. Mỗi phần là một demo Go thật trong Docker, đo độ trễ, bộ nhớ, số thông điệp mất hay trùng.
4/12 phần đã đăng
Lập trình
1
Polling, long polling, SSE và WebSocket đo thật trên cùng một luồng sự kiện: polling mỗi giây trễ 486 ms và tốn 8,4 KB mỗi phút cho 23 sự kiện, WebSocket và SSE trễ 0,3 ms với 1,3 KB, với 200 client polling tốn CPU gấp ba mà trễ gấp 300 lần
Một server Go phát sự kiện ngẫu nhiên trung bình mỗi 2 giây, một client đo độ trễ từ lúc sự kiện sinh ra tới lúc nhận và đếm byte TCP thật. Polling 1 giây: p50 486 ms, p99 900 ms, 60 request và 8.373 byte mỗi phút. Polling 5 giây: p50 2,3 giây. Long polling: 0,5 ms nhưng 39 request mỗi phút. SSE và WebSocket: 0,3 ms, 1 kết nối, 1.221 và 1.276 byte mỗi phút. Sau netem 50 ms, mọi cách đẩy cộng đúng 50 ms. Với 200 client, polling tốn 0,33 giây CPU server mỗi 30 giây và 12.000 request mỗi phút trong khi SSE tốn 0,11 giây với 200 kết nối mở và 5,4 MB heap.
22/09/2026
· 8 phút đọc
2
Bắt tay WebSocket đọc từng byte: 155 byte đi, 129 byte về, Sec-WebSocket-Accept tự tính bằng SHA-1 khớp đúng ví dụ của RFC, bắt tay mất 80 µs ngang một GET mở kết nối mới nhưng gấp 8 lần GET keep-alive, và bốn cách bắt tay hỏng kèm mã lỗi thật
Một client Go viết tay từng dòng header bắt tay WebSocket và in nguyên văn phản hồi: 155 byte đi, 129 byte về sau 333 µs, Accept là base64 của SHA-1 key nối GUID, khớp đúng ví dụ RFC 6455. Đo 2.000 lần: dial WebSocket mất 80 µs p50, GET mở kết nối mới 94 µs, GET keep-alive 10 µs. Sau bắt tay, một thông điệp 10 byte tốn 16 byte đi và 12 byte về so với 99 byte header tối thiểu của một GET. Bốn cách hỏng đo thật: thiếu Upgrade 400, version 12 cũng 400 thay vì 426, Origin lạ 403, proxy bỏ header Upgrade gây bad handshake trong khi ReverseProxy của Go cho 101.
22/09/2026
· 8 phút đọc
3
Khung WebSocket đọc từng byte: header 2 đến 14 byte so với 260 byte của HTTP, mask tốn 0,29 ms mỗi MB nhưng phân mảnh 4 KB mặc định của gorilla làm chậm gấp đôi, và một thông điệp 1 MB chặn thông điệp nhỏ phía sau 436 ms trên cùng kết nối
Client Go đếm byte trên net.Conn và đọc thô từng khung: payload 10 byte tốn 16 byte đi, 12 byte về, so với 151 và 126 byte của HTTP; 1 MB tốn thêm 14 byte, nhưng WriteBufferSize mặc định 4 KB của gorilla cắt thành 256 khung và trả thêm 2.048 byte. Mask không phải nút cổ chai: XOR 1 MB mất 0,29 ms, chiều có mask với buffer 2 MB đạt 2.213 MB/s; phân mảnh 4 KB kéo xuống 1.151 MB/s. Dưới 20 Mbit/s, một thông điệp 1 MB chặn tick phía sau 436 ms trên cùng kết nối; chia 256 thông điệp 4 KB thì tick đến sau 0,5 ms; kết nối riêng vẫn chờ 24 ms. Text và binary nhanh như nhau: gorilla không kiểm UTF-8.
22/09/2026
· 10 phút đọc
4
Heartbeat và kết nối chết đo thật: rút cáp bằng iptables, server Go nhận ra sau bao lâu, ping/pong 4,9 giây, TCP keepalive chỉnh tay 8,4 giây, keepalive mặc định của Go 150 giây, tắt keepalive thì không bao giờ, ReadDeadline không ping giết nhầm
Server Go trong dist-lab mở sáu cấu hình WebSocket, client trong go-lab im lặng, rồi rút cáp bằng iptables DROP hai chiều để không có FIN hay RST. Ping mỗi 5 giây với pongWait 8 giây nhận ra sau 4,9 giây; TCP keepalive idle 5 giây, 3 probe cách 2 giây nhận ra sau 8,4 giây; keepalive mặc định của net/http Go sau 150,2 giây, đúng 15 cộng 9 nhân 15; tắt keepalive thì socket còn ESTABLISHED hết 300 giây; server đẩy 20 byte mỗi giây vẫn ghi thành công 300 lần khi ss báo backoff 10. ReadDeadline không ping giết client khoẻ sau đúng 10 giây. Tiến trình client chết thì mọi cấu hình thấy trong 3 ms.
22/09/2026
· 9 phút đọc
Còn 8 phần nữa sẽ lần lượt được đăng.