When ovn-controller returns DNS responses with multiple IP addresses, clients typically use the first IP in the list. Without randomization, all clients direct traffic to the same backend, leading to uneven load distribution.
This patch implements Fisher-Yates shuffle on IPv4 and IPv6 address arrays before building DNS answers, ensuring each query returns IPs in a randomized order for better traffic distribution across backends. Signed-off-by: Mehrdad Moradi <[email protected]> --- controller/pinctrl.c | 15 ++++++++---- lib/ovn-util.c | 25 ++++++++++++++++++++ lib/ovn-util.h | 2 ++ tests/ovn.at | 56 +++++++++++++++++++++++++++++++++++++++----- 4 files changed, 88 insertions(+), 10 deletions(-) diff --git a/controller/pinctrl.c b/controller/pinctrl.c index ab8a0a37c..388955b44 100644 --- a/controller/pinctrl.c +++ b/controller/pinctrl.c @@ -3530,11 +3530,15 @@ pinctrl_handle_dns_lookup( goto exit; } + /* Shuffle indices for round-robin load balancing. */ + size_t *ipv4_order = shuffle_indices(ip_addrs.n_ipv4_addrs); + size_t *ipv6_order = shuffle_indices(ip_addrs.n_ipv6_addrs); + if (query_type == DNS_QUERY_TYPE_A || query_type == DNS_QUERY_TYPE_ANY) { for (size_t i = 0; i < ip_addrs.n_ipv4_addrs; i++) { - dns_build_a_answer(&dns_answer, in_queryname, idx, - ip_addrs.ipv4_addrs[i].addr); + ovs_be32 addr = ip_addrs.ipv4_addrs[ipv4_order[i]].addr; + dns_build_a_answer(&dns_answer, in_queryname, idx, addr); ancount++; } } @@ -3542,12 +3546,15 @@ pinctrl_handle_dns_lookup( if (query_type == DNS_QUERY_TYPE_AAAA || query_type == DNS_QUERY_TYPE_ANY) { for (size_t i = 0; i < ip_addrs.n_ipv6_addrs; i++) { - dns_build_aaaa_answer(&dns_answer, in_queryname, idx, - &ip_addrs.ipv6_addrs[i].addr); + struct in6_addr *addr = &ip_addrs.ipv6_addrs[ipv6_order[i]].addr; + dns_build_aaaa_answer(&dns_answer, in_queryname, idx, addr); ancount++; } } + free(ipv4_order); + free(ipv6_order); + /* DNS is configured with a record for this domain with * an IPv4/IPV6 only, so instead of ignoring this A/AAAA query, * we can reply with RCODE = 5 (server refuses) and that diff --git a/lib/ovn-util.c b/lib/ovn-util.c index 71098d478..48f9fd15f 100644 --- a/lib/ovn-util.c +++ b/lib/ovn-util.c @@ -21,6 +21,7 @@ #include "daemon.h" #include "include/ovn/actions.h" +#include "lib/random.h" #include "openvswitch/ofp-parse.h" #include "openvswitch/rconn.h" #include "openvswitch/vlog.h" @@ -1737,3 +1738,27 @@ is_partial_uuid_match(const struct uuid *uuid, const char *match) s2 = strip_leading_zero(s2); return !strncmp(s1, s2, strlen(s2)); } + +/* Return an array of shuffled indices [0, n-1] using Fisher-Yates algorithm. + * Caller must free the returned array. Returns NULL if n == 0. */ +size_t * +shuffle_indices(size_t n) +{ + if (!n) { + return NULL; + } + + size_t *indices = xmalloc(n * sizeof *indices); + for (size_t i = 0; i < n; i++) { + indices[i] = i; + } + + for (size_t i = n - 1; i > 0; i--) { + size_t j = random_range(i + 1); + size_t tmp = indices[i]; + indices[i] = indices[j]; + indices[j] = tmp; + } + + return indices; +} diff --git a/lib/ovn-util.h b/lib/ovn-util.h index 74931ed29..7c29b6afb 100644 --- a/lib/ovn-util.h +++ b/lib/ovn-util.h @@ -796,4 +796,6 @@ NEIGH_REDISTRIBUTE_MODES enum neigh_redistribute_mode parse_neigh_dynamic_redistribute(const struct smap *options); +size_t *shuffle_indices(size_t n); + #endif /* OVN_UTIL_H */ diff --git a/tests/ovn.at b/tests/ovn.at index 4d15d4a62..d5e51c60b 100644 --- a/tests/ovn.at +++ b/tests/ovn.at @@ -11636,6 +11636,7 @@ set_dns_params() { local ttl=00000e10 an_count=0001 type=0001 + expected_dns_answer_alt="" case $hname in vm1) # vm1.ovn.org @@ -11652,10 +11653,13 @@ set_dns_params() { vm2) # vm2.ovn.org query_name=03766d32036f766e036f726700 + # IPv4 addresses may be returned in any order due to DNS round-robin. # IPv4 address - 10.0.0.6 - expected_dns_answer=${query_name}00010001${ttl}00040a000006 + local ip1=${query_name}00010001${ttl}00040a000006 # IPv4 address - 20.0.0.4 - expected_dns_answer=${expected_dns_answer}${query_name}00010001${ttl}000414000004 + local ip2=${query_name}00010001${ttl}000414000004 + expected_dns_answer=${ip1}${ip2} + expected_dns_answer_alt=${ip2}${ip1} an_count=0002 ;; vm3) @@ -11712,6 +11716,12 @@ set_dns_params() { local dns_resp_header=010281200001${an_count}00000000 dns_req_data=${dns_req_header}${query_name}${type}0001 dns_resp_data=${dns_resp_header}${query_name}${type}0001${expected_dns_answer} + # Alternative response for cases with multiple IPs (DNS round-robin). + if test -n "$expected_dns_answer_alt"; then + dns_resp_data_alt=${dns_resp_header}${query_name}${type}0001${expected_dns_answer_alt} + else + dns_resp_data_alt="" + fi } # This shell function sends a DNS request packet @@ -11789,17 +11799,51 @@ src_ip=`ip_to_hex 10 0 0 4` dst_ip=`ip_to_hex 10 0 0 1` dns_reply=1 test_dns 1 f00000000001 f000000000f0 $src_ip $dst_ip $dns_reply $dns_req_data $dns_resp_data +# Generate alternative expected response for DNS round-robin (without sending packet). +if test -n "$dns_resp_data_alt"; then + # Build expected reply packet with alternative IP ordering. + ip_len_val=`expr 28 + ${#dns_resp_data_alt} / 2` + udp_len_val=`expr $ip_len_val - 20` + ip_len_hex=$(printf "%x" $ip_len_val) + udp_len_hex=$(printf "%x" $udp_len_val) + reply_alt=f00000000001f000000000f00800450000${ip_len_hex}0000000080110000 + reply_alt=${reply_alt}${dst_ip}${src_ip}0035923400${udp_len_hex}0000${dns_resp_data_alt} + echo $reply_alt > 1.expected.alt +fi # NXT_RESUMEs should be 1. OVS_WAIT_UNTIL([test 1 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`]) -OVN_CHECK_PACKETS([hv1/vif1-tx.pcap], [1.expected], ["cut -c -48"]) -# Skipping the IPv4 checksum. -OVN_CHECK_PACKETS([hv1/vif1-tx.pcap], [1.expected], ["cut -c 53-"]) +# Check packets - for vm2, IPs may be in either order due to DNS round-robin. +if test -f 1.expected.alt; then + $PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/vif1-tx.pcap > 1.received + cut -c 53- 1.expected > 1.expected.tail + cut -c 53- 1.expected.alt > 1.expected.alt.tail + cut -c 53- 1.received > 1.received.tail + # Accept either ordering of IPs. + if diff -q 1.expected.tail 1.received.tail > /dev/null 2>&1 || \ + diff -q 1.expected.alt.tail 1.received.tail > /dev/null 2>&1; then + : + else + echo "Expected (order 1):" && cat 1.expected.tail + echo "Expected (order 2):" && cat 1.expected.alt.tail + echo "Received:" && cat 1.received.tail + AT_FAIL_IF([true]) + fi + # Check header (first 48 chars). + cut -c -48 1.expected > 1.expected.head + cut -c -48 1.received > 1.received.head + AT_CHECK([diff 1.expected.head 1.received.head]) +else + OVN_CHECK_PACKETS([hv1/vif1-tx.pcap], [1.expected], ["cut -c -48"]) + # Skipping the IPv4 checksum. + OVN_CHECK_PACKETS([hv1/vif1-tx.pcap], [1.expected], ["cut -c 53-"]) +fi reset_pcap_file hv1-vif1 hv1/vif1 reset_pcap_file hv1-vif2 hv1/vif2 -rm -f 1.expected +rm -f 1.expected 1.expected.alt 1.received +rm -f 1.expected.head 1.expected.tail 1.expected.alt.tail 1.received.head 1.received.tail rm -f 2.expected -- 2.52.0 _______________________________________________ dev mailing list [email protected] https://mail.openvswitch.org/mailman/listinfo/ovs-dev
