Mười một phần, mười một demo Go trong Docker, và vài trăm con số. Phần này không đo thêm gì mới; nó sắp xếp lại những gì đã đo thành hai thứ dùng được ngay: một khung chọn cho câu hỏi "dùng polling, SSE hay WebSocket", và một checklist vận hành trong đó mỗi dòng gắn với một con số và một sự cố đã thấy tận mắt trong sê-ri. Nếu chỉ đọc một phần trong mười hai, đọc phần này; nếu muốn biết con số đến từ đâu, mỗi dòng có số phần để quay lại.
Khung chọn: bốn câu hỏi theo thứ tự

Hình 1: Bốn câu hỏi theo thứ tự, mỗi nhánh trả lời bằng số đã đo, và sáu thứ bắt buộc dù chọn nhánh nào.
Câu 1: dữ liệu đổi bao lâu một lần, và người dùng cần thấy sau bao lâu? Nếu câu trả lời là vài phút và "không cần tức thì", polling thắng vì nó không cần gì cả: không kết nối dài, không heartbeat, không reconnect, đi qua mọi proxy và cache. Giá đã đo ở phần 1: poll mỗi giây cho độ trễ p50 486 ms và 8,4 KB mỗi phút cho một client không có gì mới để nhận; poll mỗi 5 giây cho 2,3 giây. Nếu câu trả lời là "dưới một giây", bạn cần một kết nối dài, và cả SSE lẫn WebSocket cho 0,3 ms với 1,2 đến 1,3 KB mỗi phút, bằng nhau trong sai số.
Câu 2: chiều lên có dày không, có binary không? Đây là câu phân biệt SSE với WebSocket. Có một trong hai thì WebSocket: hai chiều trên một kết nối, framing 2 đến 6 byte (phần 3). Không có cả hai thì SSE đủ, và bạn bớt được toàn bộ code reconnect vì EventSource tự làm với Last-Event-ID (phần 10). Chiều lên thưa qua POST tốn 0,112 ms thay vì 0,064 ms của WebSocket echo, không đáng kể với dashboard hay thông báo, đáng kể với chat hay game. Điều kiện đi kèm SSE: HTTP/2 ở edge để thoát giới hạn 6 kết nối của trình duyệt, và tắt đệm trên mọi proxy.
Câu 3: mất một thông điệp thì sao? WebSocket và SSE đều không trả lời câu này; giao thức ứng dụng của bạn trả lời. Nếu thông điệp sau thay thế thông điệp trước (giá, vị trí, số người online), không cần số thứ tự, chỉ cần một snapshot đầy đủ khi nối lại. Nếu không được mất và mọi người xem cùng một stream, số thứ tự cộng ring buffer với gap marker khi tràn: phần 8 đo 100 KB cho mọi client, mất 0 sau 3 giây đứt, ring quá nhỏ thì báo gap thay vì im lặng. Nếu không được mất và dữ liệu riêng từng người, session cộng ack cộng dedup: 83 KB cho mỗi client đang đứt 3 giây, trùng 10 sự kiện phải lọc, và session bắt buộc có thời hạn sống.
Câu 4: bao nhiêu kết nối, trên bao nhiêu máy? Dưới 10.000 kết nối trên một máy, một tiến trình Go là đủ và rẻ: 27 KB RSS mỗi kết nối idle với bộ đệm mặc định, 280 MB cho 10.000 (phần 6). Nhiều máy thì cần điểm gặp, thường là Redis pub/sub: thêm 0,4 ms ở p50 và 12 ms ở p99, mọi sự kiện phải đi một đường duy nhất kể cả về chính node phát, nếu không client của node đó nhận trùng 50%, và presence phải dựa trên TTL chứ không dựa trên sự kiện leave (phần 9).
Sáu thứ không được bỏ qua dù chọn nhánh nào. Write deadline, vì không có nó một client không đọc dừng toàn bộ hub mãi mãi (phần 5, phần 11). Cô lập goroutine ghi, vì deadline chỉ giới hạn thiệt hại ở deadline nhân số client chậm chung goroutine (phần 7). Ping/pong, vì TCP không báo khi đầu kia biến mất và keepalive mặc định của Go cần 150 giây (phần 4). Số thứ tự trong payload, vì không có nó mất là im lặng. Không cấp phát trong đường fan-out, vì GC assist chứ không phải pause làm p99 tăng 2,6 lần. Và cấu hình proxy: WebSocket qua proxy thiếu Upgrade chết ngay với 400 (phần 2), SSE qua proxy đệm bắt tay thành công rồi im 6,6 giây.
Checklist vận hành: mỗi dòng một con số đã đo

Hình 2: Ba nhóm checklist: giới hạn hệ thống, timeout và nhịp, chỉ số phải theo dõi, mỗi dòng gắn với con số và phần đã đo.
| mục | con số đã đo | phần |
|---|---|---|
ulimit -n |
1024 dừng ở 1.018 kết nối | 6 |
| cổng ephemeral | 28.232 mỗi đích, TIME_WAIT 60 s | 6 |
| kernel nuốt trước khi chặn | 2,5 MB, 411 sự kiện vào kết nối chết | 5, 8 |
| RAM mỗi kết nối idle | 27 KB, 20 KB với buffer 1 KB | 6 |
| phát hiện kết nối chết | ping 4,9 s, keepalive 11 s, Go mặc định 150 s | 4 |
| write deadline | 1 s × số client chậm chung goroutine | 7, 11 |
| hàng đợi mỗi client | 256 × 1 KB = 0,3 s ở 1.000 msg/s | 5 |
| ring buffer / session | 100 KB chung / 83 KB mỗi client mỗi 3 s | 8 |
| presence | TTL 3 s đúng lại; sự kiện leave thì sai mãi | 9 |
| GC | STW 2,3 ms nhưng p99 115 ms | 11 |
| p99.9 nền 10.000 kết nối | 59 ms | 11 |
Giới hạn hệ thống phải đặt trước khi lên tải, vì chúng gãy trước code. ulimit -n mặc định 1024 dừng server ở 1.018 kết nối và làm nó mù cả /proc của chính nó. Dải cổng ephemeral 28.232 mỗi địa chỉ đích cộng TIME_WAIT 60 giây làm load tester hoặc proxy cạn cổng trước khi server mệt: đóng 10.000 rồi mở 20.000 ngay cho 4.775 kết nối hỏng. Kernel nuốt 2,5 MB vào send buffer trước khi Write chặn, nên "ghi thành công" không bao giờ là bằng chứng "đã nhận". SSE cần HTTP/2 ở edge vì trình duyệt chỉ cho 6 kết nối HTTP/1.1 mỗi host. Và bộ nhớ tính được trước: 27 KB mỗi kết nối, 20 KB nếu hạ bộ đệm gorilla xuống 1 KB, thêm 5 KB nếu mỗi kết nối có timer ping riêng.
Timeout và nhịp là tham số nghiệp vụ, không phải kỹ thuật. Write deadline 1 giây giới hạn thiệt hại của client chậm ở 1 giây nhân số client chậm chung goroutine; 5 client chậm một shard là 5 giây cho 1.000 client. Hạ xuống 100 ms thì ít kẹt hơn nhưng đóng nhầm client chỉ hơi chậm; chọn theo p99 của mạng thật. Ping 5 giây với pongWait 8 giây phát hiện kết nối chết sau 4,9 giây; mạng di động có khoảng im 5 đến 10 giây nên production thường dùng 20 đến 30 giây. ReadDeadline đứng một mình giết client khoẻ sau đúng thời hạn, luôn phải đi kèm ping. Hàng đợi 256 thông điệp 1 KB là 0,3 giây dung sai ở 1.000 thông điệp mỗi giây; hàng đợi vô hạn là thêm 1 MB mỗi giây cho mỗi client chậm. Ring buffer tính bằng thời gian đứt tối đa nhân tốc độ; session phải có thời hạn sống vì 10.000 client đứt cùng lúc là 830 MB. Presence bằng khoá alive với TTL 3 giây tự sửa sau 3 giây; bằng sự kiện leave thì sai mãi khi node chết.
Chỉ số phải theo dõi, và hình dạng của từng sự cố. p50 không đổi trong cả chín kịch bản sự cố của phần 11, luôn quanh 20 ms; chỉ p99.9 và "client cuối mỗi vòng" kể chuyện, và hai số đó phân biệt được vài client lẻ tệ với một vòng tệ cho nghìn client. Khoảng nghỉ của hub: đứng hẳn là client chậm không deadline, nhảy 1 giây đều đặn là deadline đang cứu bạn. Hàng đợi tăng tuyến tính là hàng đợi vô hạn; drops lớn hơn 0 là có client chậm. 2 goroutine và 1 fd mỗi kết nối là bình thường, không phải rò rỉ. Với GC, nhìn phần CPU của GC chứ không nhìn pause: 2,3 ms dừng thế giới đi cùng p99 115 ms. TIME_WAIT trong sockstat phía client và load balancer. Và mất, trùng, sai thứ tự đếm được ở client nếu payload có số thứ tự: khoảng trừ số duy nhất là mất, thấy hai lần là trùng, so hash thứ tự giữa client là biết hai node có cùng lịch sử không.
Đánh đổi cần cân nhắc
Mọi con số ở đây là Docker cùng máy, không TLS, Go 1.23 với gorilla. Số của bạn sẽ khác: TLS thêm vài chục KB mỗi kết nối và CPU bắt tay, mạng thật thêm RTT vào mọi độ trễ, thư viện khác có mặc định khác. Thứ không đổi là cơ chế và thứ tự độ lớn: kernel vẫn nuốt vài MB, keepalive mặc định vẫn hàng phút, một client chậm vẫn kéo theo những ai chung goroutine ghi, GC assist vẫn đánh vào đường cấp phát nóng.
Khung chọn không có câu trả lời mặc định, và đó là chủ ý. WebSocket không "tốt hơn" SSE, SSE không "hiện đại hơn" polling. Polling đúng cho dữ liệu đổi mỗi vài phút; SSE đúng cho luồng đi xuống cần ít code; WebSocket đúng cho hai chiều dày hoặc binary. Chọn sai theo hướng "mạnh hơn" là trả bằng heartbeat, reconnect, cô lập, cấu hình proxy, tất cả những thứ 11 phần vừa đo, cho một bài toán không cần chúng.
Những gì sê-ri chưa đo. TLS và permessage-deflate; HTTP/3 và WebTransport; Redis Streams thay cho pub/sub khi cần replay giữa các node; mạng thật với mất gói một chiều; và trình duyệt thật thay cho client Go. Mỗi cái là một bài đo riêng, và cách đo thì giống hệt: dựng, cắt, đếm.
Ba ý mang về
- Bốn câu hỏi theo thứ tự, mỗi câu một con số: độ tươi cần thiết, chiều lên và binary, hậu quả khi mất, số kết nối và số máy. Trả lời xong là biết polling, SSE hay WebSocket, và biết phải trả gì.
- Sáu thứ bắt buộc dù chọn gì: write deadline, cô lập goroutine ghi, ping/pong, số thứ tự, không cấp phát trong đường nóng, cấu hình proxy. Mỗi thứ thiếu đã làm gãy một demo thật trong sê-ri này.
- Theo dõi p99.9 và hình dạng của nó, không phải p50: p50 đứng yên ở 20 ms qua chín sự cố; đuôi, khoảng nghỉ của hub, hàng đợi và phần CPU của GC mới là nơi sự cố hiện ra trước khi người dùng gọi.
Nguồn
- Toàn bộ 11 phần của sê-ri, từ phần 1 tới phần 11, mỗi phần kèm công cụ đo và số liệu gốc
- RFC 6455 — The WebSocket Protocol: https://www.rfc-editor.org/rfc/rfc6455
- WHATWG HTML — Server-sent events: https://html.spec.whatwg.org/multipage/server-sent-events.html
- Go — A Guide to the Go Garbage Collector: https://go.dev/doc/gc-guide
- gorilla/websocket — tài liệu gói: https://pkg.go.dev/github.com/gorilla/websocket
- Redis — Pub/Sub: https://redis.io/docs/latest/develop/interact/pubsub/