Khi hai tiến trình chạy trên cùng một máy cần nói chuyện — ứng dụng với cơ sở dữ liệu, nginx với backend, một dịch vụ với Redis — bạn có hai lựa chọn phổ biến: một Unix domain socket (kết nối qua một tệp như /tmp/app.sock), hay một socket TCP qua loopback (127.0.0.1:cổng). Cả hai đều không ra khỏi máy, nhưng chúng đi hai con đường khác nhau trong nhân. Câu hỏi thực tế: chọn cái nào nhanh hơn, và nhanh hơn bao nhiêu? Tôi đo trực tiếp trong container — và cả hai kỳ vọng tôi mang vào đều bị phép đo bác bỏ.

Socket Unix và TCP

Hai cách nói chuyện nội máy

Một Unix domain socket là cơ chế liên lạc giữa các tiến trình (IPC) thuần túy trong nhân: hai đầu trao dữ liệu cho nhau qua bộ đệm trong nhân, không có địa chỉ IP, không có header TCP hay IP, không checksum, không máy trạng thái kết nối phức tạp. Nó về bản chất là một đường ống hai chiều có tên.

Một socket TCP qua loopback thì vẫn là TCP đầy đủ: dữ liệu được đóng thành segment TCP, gắn header, đi qua toàn bộ ngăn xếp TCP/IP của nhân — chỉ khác là ở cuối, thay vì ra card mạng thật, nó vòng lại qua giao diện loopback lo. Nghĩa là nó gánh toàn bộ logic của TCP (đánh số thứ tự, cửa sổ, ACK, kiểm soát tắc nghẽn) dù không có một mét cáp nào.

Trực giác nói: Unix socket bỏ qua cả cái ngăn xếp đó, nên phải nhanh hơn hẳn. Tôi vào đo để xác nhận, và mang theo con số kỳ vọng "nhanh hơn cả chục lần".

Đo: Unix nhanh hơn, nhưng chỉ khoảng rưỡi

Tôi dựng một echo server và cho client ping-pong 3000 vòng với tin nhắn 64 byte, lấy trung vị độ trễ mỗi vòng (micro giây):

Cách nối Độ trễ (trung vị)
Unix socket 18,8 µs
TCP loopback + NODELAY 30,2 µs
TCP loopback (Nagle bật, ghi 1 lần) 29,3 µs
TCP loopback (Nagle bật, ghi 2 lần) 29,4 µs

Unix socket nhanh hơn TCP loopback, nhưng chỉ khoảng 1,6 lần (18,8 so với ~30 µs) — không phải chục lần. Và về thông lượng, khi chuyển 200 MB:

Unix socket : 5403 MB/s
TCP loopback: 4162 MB/s   (Unix nhanh ~1,3 lần)

Cũng chỉ nhanh hơn ~1,3 lần. Cả hai khác biệt đều thật và đo được, nhưng khiêm tốn hơn nhiều so với con số tôi tưởng tượng.

Một lần tôi đo hớ: hai kỳ vọng, hai lần trượt

Tôi bước vào bài này với hai niềm tin, và phép đo bác cả hai.

Kỳ vọng một: "Unix bỏ qua ngăn xếp mạng nên phải nhanh hơn cả chục lần". Đo ra chỉ ~1,5 lần. Lý do nằm ở chữ loopback. TCP qua loopback vốn đã được nhân rút gọn gần hết những chi phí mà người ta hay gán cho "TCP": không có card mạng, không có truyền vật lý, không có độ trễ lan truyền, và trên loopback nhân thường bỏ qua cả checksum vì dữ liệu không rời khỏi bộ nhớ. Nên "TCP loopback" không hề gánh phần lớn cái giá của TCP thật ngoài mạng. Phần mà Unix socket tiết kiệm thêm — dựng header, chạy máy trạng thái — là có thật nhưng nhỏ, chỉ vài micro giây. Tôi đã tưởng tượng Unix đang tránh một ngăn xếp đắt đỏ, trong khi thực ra loopback đã tự tránh gần hết rồi.

Kỳ vọng hai: "Nagle sẽ làm TCP loopback treo, khuếch đại khác biệt". Tôi còn chủ động giăng bẫy: thêm một phép đo trong đó client ghi tin nhắn bằng hai lần send() (header rồi thân) với Nagle bật, hòng tái hiện cú treo 40 mili giây kinh điển của Nagle cộng delayed-ACK. Kết quả: 29,4 µs — y hệt như ghi một lần. Không có cú treo nào.

Vì sao bẫy không sập? Vì một ping-pong chặt chỉ có đúng một tin bay giữa hai bên tại mỗi thời điểm: client gửi, rồi chờ trả lời trước khi gửi tiếp. Khi server echo lại, gói trả lời mang luôn ACK về — nên client không bao giờ rơi vào cảnh "gửi thêm dữ liệu trong khi đang chờ một ACK bị trì hoãn". Cú treo Nagle+delayed-ACK cần một kiểu tải rất riêng: ghi nhiều mẩu nhỏ dồn dập không chờ trả lời, chứ không phải mọi lần dùng TCP. Tôi đã nhớ đúng cơ chế Nagle nhưng gán sai điều kiện kích hoạt nó.

Bài học đo lường: hai trực giác nghe rất hợp lý — "bỏ ngăn xếp thì nhanh vọt" và "Nagle luôn rình treo" — đều đếm thừa khi gặp thực tế cụ thể. Loopback đã bỏ sẵn phần lớn ngăn xếp, nên cái Unix tiết kiệm thêm là nhỏ; và Nagle chỉ cắn đúng kiểu tải của nó, không phải mọi kết nối TCP. Đo trên chính tình huống mình quan tâm, đừng ngoại suy từ một câu chuyện chung.

Vì sao điều này quan trọng khi lập trình

Hệ quả đầu tiên là dùng Unix socket cho liên lạc nội máy khi có thể, vì nó rẻ hơn và còn an toàn hơn — nhưng đừng kỳ vọng phép màu tốc độ. ~1,5 lần độ trễ và ~1,3 lần thông lượng là lợi ích thật, đáng lấy khi hai bên chắc chắn ở cùng máy (ứng dụng với PostgreSQL/MySQL/Redis cục bộ đều hỗ trợ Unix socket). Nhưng nếu ai đó hứa "chuyển sang Unix socket sẽ nhanh gấp mười", hãy hoài nghi — với loopback, phần lớn chi phí bạn định tránh vốn đã không tồn tại. Lợi ích lớn nhất của Unix socket thường không phải tốc độ, mà là bảo mật và đơn giản: nó dùng quyền tệp của hệ thống để kiểm soát ai kết nối được, và không mở một cổng mạng nào ra ngoài để lỡ bị truy cập từ xa.

Hệ quả thứ hai là hiểu vì sao độ trễ nội máy đo bằng micro giây, còn mạng thật đo bằng mili giây. Cả hai con số ở đây (~19 và ~30 µs) đều nhỏ hơn một nghìn lần so với một RTT mạng LAN điển hình (~0,5 ms) và nhỏ hơn chục nghìn lần so với một RTT Internet (~50 ms, như các phần đầu sê-ri đã đo). Nghĩa là: nếu hai dịch vụ nói chuyện rất nhiều, đặt chúng cùng máy (Unix socket hay loopback đều được) thắng đậm so với tách ra hai máy — không phải vì chọn loại socket khéo, mà vì tránh được cả một chuyến đi mạng. Chọn Unix hay loopback là tinh chỉnh vài micro giây; đặt cùng máy hay khác máy là khác biệt cả nghìn lần.

Hệ quả thứ ba là một nếp đo lường lặp lại suốt sê-ri: một con số "nghe hợp lý" không thay được một phép đo, và một phép đo phải chạy đúng điều kiện bạn quan tâm. Con số mang theo: cùng máy, Unix socket nhanh hơn TCP loopback ~1,6 lần độ trễ (18,8 so 30,2 µs) và ~1,3 lần thông lượng — thật nhưng khiêm tốn, vì loopback đã lược bỏ phần lớn ngăn xếp; và Nagle không làm treo một ping-pong chặt vì kiểu tải đó không kích hoạt nó. Chọn socket theo bảo mật và ngữ cảnh trước, theo vài micro giây sau.

Thử ba mươi giây

Nếu bạn dùng PostgreSQL, MySQL hay Redis cục bộ, thử cả hai đường kết nối và cảm nhận. Với Postgres: psql -h /var/run/postgresql (đường dẫn Unix socket) so với psql -h 127.0.0.1 (TCP loopback) — cùng một cơ sở dữ liệu, hai con đường. Với redis-benchmark, cờ -s /tmp/redis.sock (Unix) so với -h 127.0.0.1 -p 6379 (TCP) sẽ in ra số lệnh mỗi giây cho từng loại; bạn sẽ thấy Unix nhỉnh hơn một chút — nhưng chỉ một chút, đúng như bài này đo. Và nếu chuỗi kết nối của bạn đang trỏ 127.0.0.1 tới một dịch vụ chắc chắn cùng máy, đổi sang Unix socket là một cải thiện nhỏ và miễn phí — vừa nhanh hơn tí, vừa bớt một cổng mạng hở ra ngoài.