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ự

Ảnh chụp nền tối khung chọn realtime bốn câu hỏi không có câu trả lời mặc định tổng kết 11 phần đo thật Go 1.23 gorilla websocket Redis 7 mọi con số trong khung đều là số đã đo trong sê-ri, bốn câu hỏi theo thứ tự gồm 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 vài phút không cần tức thì polling phần 1 1 s poll bằng 486 ms p50 8,4 KB mỗi phút 5 s poll bằng 2,3 s, dưới giây chủ yếu đi xuống SSE 0,3 ms 1,2 KB mỗi phút reconnect miễn phí phần 1 10, dưới giây hai chiều dày WebSocket 0,3 ms 1,3 KB mỗi phút 2 tới 6 B framing phần 1 3, 2 chiều lên dày không có binary không có một trong hai WebSocket không SSE đủ và bớt được code reconnect phần 10 POST cộng SSE 0,112 vs WS 0,064 ms, 3 mất một thông điệp thì sao thay thế được giá vị trí không seq snapshot khi nối lại, không được mất stream chung seq cộng ring buffer GAP khi tràn phần 8 100 KB cho mọi client mất 0, không được mất riêng từng người session cộng ack cộng dedup phần 8 83 KB mỗi client mỗi 3 s đứt trùng 10, 4 bao nhiêu kết nối trên bao nhiêu máy dưới 10000 một máy một tiến trình 27 KB mỗi kết nối 280 MB phần 6, nhiều máy Redis pub sub một đường đi duy nhất presence có TTL phần 9 cộng 0,4 ms p50 naive trùng 50 phần trăm, những thứ không được bỏ qua dù chọn gì gồm write deadline không có một client chậm dừng tất cả mãi mãi phần 5 11 hub đứng ở vòng 22, cô lập goroutine ghi deadline chỉ giới hạn thiệt hại bằng deadline nhân số client chậm chung goroutine phần 7 11, heartbeat ping pong TCP không báo keepalive mặc định Go 150 s ping 5 s pongWait 8 s bằng 4,9 s phần 4, số thứ tự trong payload không có mất là im lặng phần 8 615 event biến mất không lỗi, không cấp phát đường nóng GC assist không phải STW làm p99 nhân 2,6 phần 11, proxy Upgrade cộng tắt đệm WS bắt tay 400 ngay phần 2 SSE bắt tay OK rồi im 6,6 s phần 10

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

Ảnh chụp nền tối checklist vận hành mỗi dòng một con số đã đo 11 phần mỗi phần một demo thật trong Docker số ở đây để đối chiếu thứ tự độ lớn không phải SLO của bạn, giới hạn hệ thống phải đặt trước khi lên tải gồm ulimit -n mặc định 1024 dừng ở 1018 kết nối server mù cả proc của chính nó phần 6, cổng ephemeral 28232 mỗi IP đích TIME_WAIT 60 s đóng 10k rồi mở 20k 4775 hỏng phần 6, tcp_wmem sndbuf kernel nuốt 2,5 MB trước khi Write chặn ghi thành công khác đã nhận phần 5 8, HTTP 2 cho SSE trình duyệt 6 kết nối mỗi host h2c 300 stream bằng 2 TCP Go 250 stream mỗi kết nối phần 10, RAM theo kết nối 27 KB mặc định 20 KB với buffer 1 KB cộng 5 KB nếu timer ping riêng phần 6, timeout và nhịp gồm write deadline 1 s hub kẹt 1 s nhân client chậm mỗi shard 5 s với 5 100 ms ít kẹt dễ đóng nhầm phần 7 11, pongWait ping 8 s 5 s phát hiện 4,9 s TCP keepalive chỉnh 11 s mặc định Go 150 s phần 4, ReadDeadline đơn độc giết client khoẻ sau đúng 10 s phải đi kèm ping phần 4, hàng đợi mỗi client 256 nhân 1 KB bằng 0,3 s dung sai ở 1000 msg mỗi giây vô hạn bằng cộng 1 MB mỗi giây mỗi client chậm phần 5, ring session TTL ring bằng đứt tối đa nhân rate session phải hết hạn 10k client đứt bằng 830 MB phần 8, presence TTL alive EX 3 s đúng lại sau 3 s join leave qua pub sub user ma mãi mãi phần 9, chỉ số phải theo dõi và hình dạng của sự cố gồm p99.9 cộng client cuối mỗi vòng p50 không đổi trong cả 9 kịch bản sự cố 20 ms đuôi mới kể chuyện phần 11, hub gap vòng phát xong đứng client chậm không deadline 1 s đều đặn deadline đang cứu bạn phần 5 11, drops removed queued queued tăng tuyến tính bằng hàng đợi vô hạn drops lớn hơn 0 bằng client nào đó chậm phần 5, goroutines fds RSS 2 goroutine mỗi kết nối là bình thường 10002 fd ở 10k kết nối là bình thường phần 6, GC assist không chỉ pause STW 2,3 ms đi cùng p99 115 ms nhìn gc CPU fraction không nhìn pause phần 11, TIME_WAIT phía client LB sockstat tw load tester cạn cổng trước khi server mệt phần 6, mất trùng theo seq đếm ở client span trừ duy nhất bằng mất seen lớn hơn 1 bằng trùng so hash thứ tự phần 8 9, nói thẳng mọi số đo trong sê-ri là Docker cùng máy không TLS Go 1.23 cộng gorilla số của bạn sẽ khác cơ chế và thứ tự độ lớn thì không checklist này là của 11 bài đã đo không có mục nào ở đây mà chưa từng làm gãy một demo thật

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ề

  1. 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ì.
  2. 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.
  3. 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