Menggunakan UDP dengan Load Balancer Jaringan passthrough eksternal

Dokumen ini membahas cara bekerja dengan Load Balancer Jaringan passthrough eksternal menggunakan Protokol Datagram Pengguna (UDP). Dokumen ini ditujukan untuk developer aplikasi, operator aplikasi, dan administrator jaringan.

Tentang UDP

UDP biasanya digunakan dalam aplikasi. Protokol yang dijelaskan dalam RFC-768, menerapkan layanan paket datagram stateless dan tidak dapat diandalkan. Misalnya, protokol QUIC Google meningkatkan pengalaman pengguna dengan menggunakan UDP untuk mempercepat aplikasi berbasis streaming.

Bagian stateless dari UDP berarti bahwa lapisan transpor tidak mempertahankan status. Oleh karena itu, setiap paket dalam "koneksi" UDP bersifat independen. Faktanya, tidak ada koneksi nyata dalam UDP. Sebagai gantinya, peserta biasanya menggunakan 2 tuple (ip:port) atau 4 tuple (src-ip:src-port, dest-ip:dest-port) untuk mengenali satu sama lain.

Seperti aplikasi berbasis TCP, aplikasi berbasis UDP juga dapat memperoleh manfaat dari load balancer, itulah sebabnya Load Balancer Jaringan passthrough eksternal digunakan dalam skenario UDP.

Load Balancer Jaringan passthrough eksternal

Load Balancer Jaringan passthrough eksternal adalah load balancer passthrough. Load Balancer tersebut memproses paket yang masuk dan mengirimkannya ke server backend bersama paket tersebut. Selanjutnya, server backend akan mengirim paket yang ditampilkan langsung ke klien. Teknik ini disebut Direct Server Return (DSR). Di setiap virtual machine (VM) Linux yang berjalan di Compute Engine yang merupakan backend dari Load Balancer Jaringan passthrough eksternal Google Cloud, entri dalam tabel perutean lokal merutekan traffic yang ditujukan untuk alamat IP load balancer ke pengontrol antarmuka jaringan (NIC). Contoh berikut menunjukkan teknik ini:

root@backend-server:~# ip ro ls table local
local 10.128.0.2 dev eth0 proto kernel scope host src 10.128.0.2
broadcast 10.128.0.2 dev eth0 proto kernel scope link src 10.128.0.2
local 198.51.100.2 dev eth0 proto 66 scope host
broadcast 127.0.0.0 dev lo proto kernel scope link src 127.0.0.1
local 127.0.0.0/8 dev lo proto kernel scope host src 127.0.0.1
local 127.0.0.1 dev lo proto kernel scope host src 127.0.0.1
broadcast 127.255.255.255 dev lo proto kernel scope link src 127.0.0.1

Pada contoh sebelumnya, 198.51.100.2 adalah alamat IP load balancer. Agen google-network-daemon.service bertanggung jawab untuk menambahkan entri ini. Namun, seperti yang ditampilkan dalam contoh berikut, VM sebenarnya tidak memiliki antarmuka yang memiliki alamat IP load balancer:

root@backend-server:~# ip ad ls
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1460 qdisc mq state UP group default qlen 1000
    link/ether 42:01:0a:80:00:02 brd ff:ff:ff:ff:ff:ff
    inet 10.128.0.2/32 brd 10.128.0.2 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::4001:aff:fe80:2/64 scope link
       valid_lft forever preferred_lft forever

Load Balancer Jaringan passthrough eksternal mengirimkan paket masuk, dengan alamat tujuan yang tidak disentuh, ke server backend. Entri tabel perutean lokal merutekan paket ke proses aplikasi yang benar, dan paket respons dari aplikasi dikirim langsung ke klien.

Diagram berikut menunjukkan cara kerja Load Balancer Jaringan passthrough eksternal. Paket yang masuk akan diproses oleh load balancer yang disebut Maglev, yang mendistribusikan paket ke server backend. Paket keluar kemudian dikirim langsung ke klien melalui DSR.

Maglev mendistribusikan paket yang masuk ke server backend, yang mendistribusikan paket tersebut melalui DSR.

Masalah terkait paket yang ditampilkan UDP

Saat Anda menggunakan DSR, ada sedikit perbedaan antara cara kernel Linux memperlakukan koneksi TCP dan UDP. Karena TCP adalah protokol stateful, kernel memiliki semua informasi yang diperlukan tentang koneksi TCP, termasuk alamat klien, port klien, alamat server, dan port server. Informasi ini dicatat dalam struktur data soket yang merepresentasikan koneksi. Dengan demikian, setiap paket yang ditampilkan dari koneksi TCP memiliki alamat sumber yang ditetapkan dengan benar ke alamat server. Untuk load balancer, alamat tersebut adalah alamat IP load balancer.

Namun, ingatlah bahwa UDP bersifat stateless, sehingga objek soket yang dibuat dalam proses aplikasi untuk koneksi UDP tidak memiliki informasi koneksi. Kernel tidak memiliki informasi tentang alamat sumber paket keluar, dan tidak mengetahui hubungan dengan paket yang diterima sebelumnya. Untuk alamat sumber paket, kernel hanya dapat mengisi alamat antarmuka yang menjadi tujuan paket UDP yang ditampilkan. Atau, jika aplikasi sebelumnya mengaitkan soket ke alamat tertentu, kernel akan menggunakan alamat tersebut sebagai alamat sumber.

Kode berikut menunjukkan program {i>echo<i} sederhana:

#!/usr/bin/python3
import socket,struct
def loop_on_socket(s):
  while True:
    d, addr = s.recvfrom(1500)
    print(d, addr)
    s.sendto("ECHO: ".encode('utf8')+d, addr)

if __name__ == "__main__":
   HOST, PORT = "0.0.0.0", 60002
   sock = socket.socket(type=socket.SocketKind.SOCK_DGRAM)
   sock.bind((HOST, PORT))
   loop_on_socket(sock)

Berikut adalah output tcpdump selama percakapan UDP:

14:50:04.758029 IP 203.0.113.2.40695 > 198.51.100.2.60002: UDP, length 3
14:50:04.758396 IP 10.128.0.2.60002 > 203.0.113.2.40695: UDP, length 2T

198.51.100.2 adalah alamat IP load balancer, dan 203.0.113.2 adalah alamat IP klien.

Setelah paket keluar dari VM, perangkat NAT lain–gateway Compute Engine–di jaringan Google Cloud menerjemahkan alamat sumber ke alamat eksternal. Gateway tidak mengetahui alamat eksternal mana yang harus digunakan, sehingga hanya alamat eksternal VM (bukan alamat load balancer) yang dapat digunakan.

Dari sisi klien, jika Anda memeriksa output dari tcpdump, paket dari server akan terlihat seperti berikut:

23:05:37.072787 IP 203.0.113.2.40695 > 198.51.100.2.60002: UDP, length 5
23:05:37.344148 IP 198.51.100.3.60002 > 203.0.113.2.40695: UDP, length 4

198.51.100.3 adalah alamat IP eksternal VM.

Dari sudut pandang klien, paket UDP tidak berasal dari alamat yang menjadi tujuan pengiriman klien. Hal ini menyebabkan masalah: kernel menghapus paket ini, dan jika klien berada di belakang perangkat NAT, perangkat NAT juga akan terinstal. Akibatnya, aplikasi klien tidak mendapat respons dari server. Diagram berikut menunjukkan proses ini saat klien menolak paket yang ditampilkan karena alamat tidak cocok.

Klien menolak paket yang dikembalikan.

Menyelesaikan masalah UDP

Untuk mengatasi masalah tanpa respons, Anda harus menulis ulang alamat sumber paket keluar ke alamat IP load balancer di server yang menghosting aplikasi. Berikut adalah beberapa opsi yang dapat Anda gunakan untuk menyelesaikan penulisan ulang header ini. Solusi pertama menggunakan pendekatan berbasis Linux dengan iptables; solusi lainnya mengambil pendekatan berbasis aplikasi.

Diagram berikut menunjukkan ide inti dari opsi ini: menulis ulang alamat IP sumber dari paket yang ditampilkan agar cocok dengan alamat IP load balancer.

Tulis ulang alamat IP sumber dari paket yang ditampilkan agar cocok dengan alamat IP
load balancer.

Menggunakan kebijakan NAT di server backend

Solusi kebijakan NAT adalah menggunakan perintah iptables Linux untuk menulis ulang alamat tujuan dari alamat IP load balancer ke alamat IP VM. Pada contoh berikut, Anda menambahkan aturan DNAT iptables untuk mengubah alamat tujuan paket masuk:

iptables -t nat -A POSTROUTING -j RETURN -d 10.128.0.2 -p udp --dport 60002
iptables -t nat -A PREROUTING -j DNAT --to-destination 10.128.0.2 -d 198.51.100.2 --dport 60002 -p udp

Perintah ini menambahkan dua aturan ke tabel NAT sistem iptables. Aturan pertama mengabaikan semua paket masuk yang menargetkan alamat eth0 lokal. Oleh karena itu, traffic yang tidak berasal dari load balancer tidak akan terpengaruh. Aturan kedua mengubah alamat IP tujuan paket masuk ke alamat IP internal VM. Aturan DNAT bersifat stateful, yang berarti bahwa kernel melacak koneksi dan menulis ulang alamat sumber paket yang ditampilkan secara otomatis.

Kelebihan Kekurangan
Kernel menerjemahkan alamat, tanpa perlu mengubah aplikasi. CPU ekstra digunakan untuk melakukan NAT. Dan karena DNAT bersifat stateful, konsumsi memori mungkin juga tinggi.
Mendukung beberapa load balancer.

Menggunakan nftables untuk merusak kolom header IP secara stateless

Dalam solusi nftables, Anda menggunakan perintah nftables untuk merusak alamat sumber di header IP paket keluar. Kerusakan ini bersifat stateless, sehingga menggunakan lebih sedikit resource daripada menggunakan DNAT. Untuk menggunakan nftables, Anda memerlukan versi kernel Linux yang lebih tinggi dari 4.10.

Anda dapat menggunakan perintah berikut:

nft add table raw
nft add chain raw postrouting {type filter hook postrouting priority 300)
nft add rule raw postrouting ip saddr 10.128.0.2 udp sport 60002 ip saddr set 198.51.100.2
Kelebihan Kekurangan
Kernel menerjemahkan alamat, tanpa perlu mengubah aplikasi. Tidak mendukung beberapa load balancer.
Proses penerjemahan alamat bersifat stateless, sehingga konsumsi resource jauh lebih rendah. CPU tambahan digunakan untuk melakukan NAT.
nftables hanya tersedia untuk versi kernel Linux yang lebih baru. Beberapa distro, seperti Centos 7.x, tidak dapat menggunakan nftables.

Mengizinkan aplikasi secara eksplisit mengikat ke alamat IP load balancer

Dalam solusi binding ini, Anda perlu mengubah aplikasi agar terikat secara eksplisit ke alamat IP load balancer. Untuk soket UDP, operasi bind memungkinkan kernel mengetahui alamat mana yang akan digunakan sebagai alamat sumber saat mengirim paket UDP yang menggunakan soket tersebut.

Contoh berikut menunjukkan cara mengikat ke alamat tertentu di Python:

#!/usr/bin/python3
import socket
def loop_on_socket(s):
  while True:
    d, addr = s.recvfrom(1500)
    print(d, addr)
    s.sendto("ECHO: ".encode('utf8')+d, addr)

if __name__ == "__main__":
   # Instead of setting HOST to "0.0.0.0",
   # we set HOST to the Load Balancer IP
   HOST, PORT = "198.51.100.2", 60002
   sock = socket.socket(type=socket.SocketKind.SOCK_DGRAM)
   sock.bind((HOST, PORT))
   loop_on_socket(sock)

# 198.51.100.2 is the load balancer's IP address
# You can also use the DNS name of the load balancer's IP address

Kode sebelumnya adalah server UDP; kode ini menggemakan kembali byte yang diterima, dengan "ECHO: " sebelumnya. Perhatikan baris 12 dan 13, tempat server terikat ke alamat 198.51.100.2, yang merupakan alamat IP load balancer.

Kelebihan Kekurangan
Dapat dilakukan dengan perubahan kode sederhana pada aplikasi. Tidak mendukung beberapa load balancer.

Gunakan recvmsg/sendmsg, bukan recvfrom/sendto untuk menentukan alamat

Dalam solusi ini, Anda menggunakan panggilan recvmsg/sendmsg, bukan panggilan recvfrom/sendto. Dibandingkan dengan panggilan recvfrom/sendto, panggilan recvmsg/sendmsg dapat menangani pesan kontrol tambahan beserta data payload. Pesan kontrol tambahan ini mencakup alamat sumber atau tujuan paket. Solusi ini memungkinkan Anda mengambil alamat tujuan dari paket masuk, dan karena alamat tersebut adalah alamat load balancer sebenarnya, Anda dapat menggunakannya sebagai alamat sumber saat mengirim balasan.

Contoh program berikut menunjukkan solusi tersebut:

#!/usr/bin/python3
import socket,struct
def loop_on_socket(s):
  while True:
    d, ctl, flg, addr = s.recvmsg(1500, 1024)
    # ctl contains the destination address information
    s.sendmsg(["ECHO: ".encode("utf8"),d], ctl, 0, addr)

if __name__ == "__main__":
   HOST, PORT = "0.0.0.0", 60002
   s = socket.socket(type=socket.SocketKind.SOCK_DGRAM)
   s.setsockopt(0,   # level is 0 (IPPROTO_IP)
                8,   # optname is 8 (IP_PKTINFO)
                1)

   s.bind((HOST, PORT))
   loop_on_socket(s)

Program ini menunjukkan cara menggunakan panggilan recvmsg/sendmsg. Untuk mengambil informasi alamat dari paket, Anda harus menggunakan panggilan setsockopt untuk menetapkan opsi IP_PKTINFO.

Kelebihan Kekurangan
Berfungsi meskipun ada beberapa load balancer–misalnya, saat ada load balancer internal dan eksternal yang dikonfigurasi ke backend yang sama. Mengharuskan Anda untuk membuat perubahan kompleks pada aplikasi. Dalam beberapa kasus, hal ini mungkin tidak dapat dilakukan.

Langkah selanjutnya