Quần Cam Blog

UDP trong Elixir và thủ thuật cache UDP header

#elixir #TIL

Vài tháng trước tui có giúp phát triển chức năng hỗ trợ Unix domain socket cho Fluxter, cho phép Fluxter gửi metrics qua UDP đến Telegraf dùng Unix domain socket.

Cách thông thường để gửi một UDP packet trong Erlang/Elixir là dùng gen_udp. Ở ví dụ bên dưới ta mở một UDP socket ở passive mode và dùng hàm :gen_udp.send/4 để gửi packet đến một UDP socket ở địa chỉ 127.0.0.1 và port 27027.

{:ok, sock} = :gen_udp.open(0, [active: false])
{ok, addr} = :inet.getaddr('127.0.0.1', :inet)
packet = [my_data]
:gen_udp.send(sock, addr, 27027, packet)

Tuy nhiên lúc đọc code của Fluxter, tui phát hiện ra cách gửi UDP packet khá lạ.

{:ok, sock} = :gen_udp.open(0, [active: false])

{:ok, {n1, n2, n3, n4}} = :inet.getaddr('127.0.0.1', :inet)
port = 27027
header =
  [
    1, # inet address family for Erlang >= 19,
    band(bsr(port, 8), 0xFF),
    band(port, 0xFF),
    band(n1, 0xFF),
    band(n2, 0xFF),
    band(n3, 0xFF),
    band(n4, 0xFF)
  ]
packet = [header, my_data]

send(sock, {self(), {:command, packet}})

Với đoạn code trên, ta tính trước header chứa thông tin đích đến của packet. Rồi mỗi lần cần gửi data, ta chèn header đã tính vào đầu packet rồi gửi bằng Erlang port_command.

Lợi ích khi làm như vậy là giúp tăng tốc. Theo benchmark của tác giả thì tốc độ tăng gần 20% sau khi cache header.

Fluxter là một metrics writer với packet được gửi với số lượng lớn. :gen_udp.send/4 luôn build lại header mỗi khi được gọi trong khi đích đến của ta là không đổi. Đó là một cost không cần thiết và bằng cách cache lại header và dùng port_command để gửi packet, ta giảm được cost này.

Bài viết này sẽ giúp tui tăng lương như thế nào?

Bài viết này được viết sau khi tác giả đã được tăng lương mua quần mới. Còn bạn được tăng lương không tui đâu quan tâm. #ahihi

Cơ mà sau khi nói xong lợi phải nói đến răng, lộn hại.

Tất nhiên việc tăng tốc không free, nó là một tradeoff (tạm dịch: sự đánh đổi). Cost chỉ chuyển hoá từ chỗ này sang chỗ khác, từ dạng này sang dạng khác.

Cost ở đây là việc phải maintain code mà đáng lẽ bạn đã không cần phải làm vậy nếu sử dụng abstraction. Với :gen_udp.send/4, bạn có thể yên tâm upgrade OTP version mà không phải lo gì cả (đã có người viết abstraction lo cho bạn). Còn khi tự build header, bạn sẽ phải tự kiểm tra, cập nhật CHANGELOG để tương thích app qua mỗi version OTP. Ví dụ OTP 19 và OTP 18 có cách build header khác nhau ở address family.

Tuy nhiên với cost là việc maintain thêm chừng 10 dòng code rất hiếm khi có breaking change để đổi lấy 20% giá trị performance, thương vụ này tui sẽ đầu tư. Còn shark Phú, không biết ông ấy nghĩ sao ta?


Bài viết cùng chủ đề

Poolboy và kĩ thuật pooling trong Erlang/Elixir

[Elixir RSS Reader] Phần 1 - HTTP client

Đây là phần 1 của loạt bài viết hướng dẫn học Elixir của mình qua việc viết một RSS reader. Ở phần này mình sẽ viết về GenServer.

Build a blog in Elixir with Nabo and Phoenix

Nabo is a simple, extendable and fast blog engine written in Elixir. This blog post shows how easy it is to integrate Nabo into your Phoenix application.