Ở phần trước ta thấy subnet mask quyết định một đích là "cùng mạng con" (trên-link) hay không. Nhưng khi đích ở trên-link, máy vẫn chưa gửi được ngay: nó biết địa chỉ IP 10.10.0.2, còn khung Ethernet trên dây lại cần địa chỉ MAC của máy kia. Bước dịch từ IP (tầng 3) sang MAC (tầng 2) đó chính là ARP. Bài này dựng hai máy ảo trong container rồi bắt đúng một vòng ARP, đo xem nó tốn bao nhiêu.
Một vòng ARP nhìn từ trên dây
Tôi tạo một cặp veth, đặt hai đầu vào cùng mạng con 10.10.0.0/24, rồi xóa sạch bảng hàng xóm và bắt gói ARP trong lúc gửi một ping:
ip link add veth0 type veth peer name veth1
ip addr add 10.10.0.1/24 dev veth0 # máy A
# veth1 ở netns khác, 10.10.0.2/24 # máy B
tcpdump -i veth0 arp -w a.pcap
ip neigh flush all && ping -c1 10.10.0.2
Bản bắt cho đúng hai gói, và đó là toàn bộ giao thức:
- Yêu cầu:
ai-là 10.10.0.2? báo 10.10.0.1. Địa chỉ đích của khung làff:ff:ff:ff:ff:ff— quảng bá, gửi tới mọi máy trong mạng con. Máy A chưa biết ai giữ10.10.0.2nên phải hỏi to cho cả phòng nghe. - Trả lời:
10.10.0.2 ở-tại 0a:49:88:d7:39:80. Lần này là gửi riêng (unicast) thẳng về máy A. Máy B nghe thấy tên mình được gọi, nó biết A là ai (từ chính gói hỏi), nên trả lời riêng chứ không cần quảng bá lại.
Cả hai khung đều dài 42 byte: 14 byte header Ethernet cộng 28 byte thân ARP. Đáng chú ý là trên veth nó không được đệm lên mức tối thiểu 60 byte của Ethernet vật lý — một chi tiết mà card mạng thật sẽ làm còn veth thì bỏ qua.
Một vòng "hỏi to, đáp riêng" đó là tất cả những gì ARP làm. Không có số thứ tự, không bắt tay, không mã hóa. Ai đang lắng nghe và giữ đúng IP đó thì trả lời.
Cái giá: trả một lần rồi cache
Vòng ARP không miễn phí, nhưng nó chỉ xảy ra một lần cho mỗi hàng xóm. Để đo, tôi so thời gian mở một kết nối TCP khi bảng ARP lạnh (vừa xóa, buộc phải phân giải) với khi nó ấm (đã có sẵn trong cache). Tôi lấy trung vị của 40 lần ấm và 8 lần lạnh, lặp ba vòng:
| connect ấm (cache) | connect lạnh (có ARP) | ARP thêm | |
|---|---|---|---|
| Lần 1 | 8,0 us | 189,5 us | 181,5 us |
| Lần 2 | 10,9 us | 221,9 us | 211,0 us |
| Lần 3 | 6,6 us | 197,2 us | 190,6 us |
Con số ổn định: một vòng ARP thêm khoảng 190 micro giây, tức gấp chừng 20 lần bản thân việc mở kết nối khi đã có cache. Nhưng cái giá đó chỉ trả một lần. Gói đầu tiên tới một hàng xóm mới phải chờ ARP xong; mọi gói sau đó đi thẳng nhờ địa chỉ MAC đã nằm trong cache. Đây là lý do gói đầu tiên của một kết nối tới một máy chưa từng nói chuyện luôn chậm hơn những gói tiếp theo một chút — và trên đường truyền xa, "một chút" đó có thể là cả một vòng khứ hồi mạng thật thay vì 190 micro giây trong phòng thí nghiệm.
Máy trạng thái của một hàng xóm
Cache ARP không chỉ là "có" hay "không". ip neigh cho thấy một máy trạng thái nhỏ. Tôi xóa rồi theo dõi:
ngay sau flush -> (trống)
gói đầu gửi đi -> INCOMPLETE (đã hỏi, đang chờ trả lời)
nhận được reply -> REACHABLE (kèm lladdr 96:e4:46:c6:f2:4e)
im một lúc -> STALE (còn dùng nhưng sẽ kiểm lại)
Trạng thái REACHABLE là lúc mọi thứ trơn tru: máy tin rằng địa chỉ MAC còn đúng và gửi thẳng. Sau một khoảng im lặng nó chuyển sang STALE — vẫn dùng địa chỉ cũ nhưng sẽ xác nhận lại ở lần sau. Cơ chế này là cách mạng chịu được việc một máy đổi card, đổi IP, hoặc biến mất.
Điều gì xảy ra khi hỏi một địa chỉ không có ai giữ? Tôi ping 10.10.0.99:
1 packets transmitted, 0 received, 100% packet loss
10.10.0.99 INCOMPLETE
Máy A gửi gói ARP ai-là 10.10.0.99 ra, không ai đáp. Hàng xóm kẹt ở INCOMPLETE, gói dữ liệu bị giữ lại chờ rồi rơi, và ứng dụng nhận "mất gói". Chú ý triệu chứng: không phải "kết nối bị từ chối", cũng không phải lỗi ngay — mà là một khoảng chờ rồi im lặng. Nếu bạn từng thấy một kết nối tới một IP trong cùng mạng con "treo" thay vì báo lỗi ngay, rất có thể ARP đang gõ cửa một địa chỉ trống.
Một lần tôi bắt hụt gói
Lần chạy đầu tiên, phần bắt ARP của tôi trả về rỗng — không một gói nào, dù ping rõ ràng đã chạy. Tôi tưởng bộ lọc arp sai, nhưng vấn đề nằm ở thứ tự thời gian.
Tôi đã xóa bảng hàng xóm, khởi động tcpdump ở nền, chờ 0,4 giây rồi mới ping. Trên một container vừa cài gói xong, tcpdump cần một khoảnh khắc để thật sự gắn vào giao diện và bắt đầu nghe. Vòng ARP chỉ có đúng hai gói và xảy ra trong vài micro giây; nếu tcpdump chưa sẵn sàng đúng lúc đó, nó lỡ mất cả cuộc trao đổi. Với luồng nhiều gói thì bạn không nhận ra vì vẫn bắt được phần sau, nhưng ARP một-lần thì mất là mất trắng.
Cách chữa là đảo thứ tự: cho tcpdump khởi động trước 0,6 giây, rồi mới xóa cache và ping. Sau đó nó bắt đủ hai gói mọi lần. Bài học đo lường: khi cần bắt một sự kiện chỉ xảy ra một lần và rất ngắn, hãy để công cụ bắt sẵn sàng hẳn rồi mới kích hoạt sự kiện — đừng tin vào một khoảng chờ ngắn ngay sau khi khởi động.
Vì sao điều này quan trọng
ARP là mắt xích thầm lặng giữa "tôi biết IP của bạn" và "tôi gửi được khung cho bạn". Nó giải thích vài điều thực tế: gói đầu tiên tới một máy mới luôn tốn thêm một vòng phân giải; cache ARP đầy lên theo số hàng xóm bạn nói chuyện; và một mạng con lớn (/16 chẳng hạn) đồng nghĩa miền quảng bá lớn — mỗi vòng ARP làm phiền nhiều máy hơn. Đó cũng là lý do người ta chia mạng thành các subnet nhỏ: không chỉ để đủ địa chỉ, mà để giữ tiếng ồn quảng bá trong tầm kiểm soát.
Có một điểm dễ quên: khi đích ngoài-link (khác mạng con), máy không ARP hỏi MAC của đích — nó ARP hỏi MAC của gateway, rồi gửi khung tới gateway để router lo phần còn lại. Nghĩa là gói ra Internet đầu tiên của bạn vẫn trả một vòng ARP, chỉ là cho router chứ không cho đích. Và có một biến thể tên là gratuitous ARP: một máy tự phát gói "10.10.0.2 ở-tại MAC-của-tôi" mà không ai hỏi, để báo cho cả mạng cập nhật cache — đây chính là cơ chế đằng sau việc chuyển địa chỉ IP nổi giữa hai máy chủ khi một máy chết, thứ mà một sê-ri trước đã đo ở tầng ứng dụng.
Thử ba mươi giây
Chạy ip neigh show trên máy của bạn, rồi để ý cột trạng thái. REACHABLE là hàng xóm vừa nói chuyện, STALE là đã lâu chưa xác nhận, FAILED hoặc INCOMPLETE là gõ cửa mà không ai mở. Bây giờ ip neigh flush all rồi ping một máy trong mạng LAN: gói đầu sẽ nhỉnh hơn các gói sau đúng một vòng ARP — cái giá bạn vừa đo, tận mắt.