Merge pull request #138927 from ytcisme/fix/ipvs-getalllocaladdresses-perf

proxy/ipvs: avoid per-interface RTM_GETADDR dump in GetAllLocalAddressesExcept
This commit is contained in:
Kubernetes Prow Robot 2026-05-25 11:45:16 +05:30 committed by GitHub
commit 465cb5fc8d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 151 additions and 16 deletions

View file

@ -171,23 +171,45 @@ func (h *netlinkHandle) isValidForSet(ip net.IP) bool {
// address can be assigned to many interfaces. This problem raised
// https://github.com/kubernetes/kubernetes/issues/114815
func (h *netlinkHandle) GetAllLocalAddressesExcept(dev string) (sets.Set[string], error) {
ifaces, err := net.Interfaces()
// We previously iterated over net.Interfaces() and called iface.Addrs()
// for each interface, but iface.Addrs() internally performs a full
// RTM_GETADDR netlink dump for the entire node and then filters in user
// space. With many interfaces and many addresses (for example tens of
// thousands of ClusterIPs bound to kube-ipvs0) the cost is
// O(N_interfaces * N_addresses) and dominates syncProxyRules latency.
//
// Instead, dump every address on the node in a single AF_UNSPEC
// RTM_GETADDR call and skip the ones whose LinkIndex matches dev. This
// makes the call O(N_addresses) and avoids the per-interface fan-out.
devLink, err := netlink.LinkByName(dev)
if err != nil {
return nil, err
klog.ErrorS(err, "Could not look up link", "dev", dev)
return nil, fmt.Errorf("could not look up link %q: %w", dev, err)
}
var addr []net.Addr
for _, iface := range ifaces {
if iface.Name == dev {
continue
}
ifadr, err := iface.Addrs()
if err != nil {
// This may happen if the interface was deleted. Ignore
// but log the error.
klog.ErrorS(err, "Reading addresses", "interface", iface.Name)
continue
}
addr = append(addr, ifadr...)
devIndex := devLink.Attrs().Index
addrs, err := netlink.AddrList(nil, unix.AF_UNSPEC)
if err != nil {
klog.ErrorS(err, "Failed to dump node addresses")
return nil, fmt.Errorf("could not list node addresses: %w", err)
}
return proxyutil.AddressSet(h.isValidForSet, addr), nil
return proxyutil.AddressSet(h.isValidForSet, filterAddrsExcept(addrs, devIndex)), nil
}
// filterAddrsExcept returns the addresses whose LinkIndex is not devIndex.
// It is extracted so it can be unit tested without requiring root privileges
// or a real network namespace.
func filterAddrsExcept(addrs []netlink.Addr, devIndex int) []net.Addr {
out := make([]net.Addr, 0, len(addrs))
for _, a := range addrs {
if a.LinkIndex == devIndex {
continue
}
if a.IPNet == nil {
continue
}
out = append(out, a.IPNet)
}
return out
}

View file

@ -0,0 +1,113 @@
//go:build linux
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package ipvs
import (
"net"
"reflect"
"sort"
"testing"
"github.com/vishvananda/netlink"
netutils "k8s.io/utils/net"
)
// addr builds a netlink.Addr suitable for use in filterAddrsExcept tests.
func addr(ip string, mask, linkIndex int) netlink.Addr {
parsed := netutils.ParseIPSloppy(ip)
bits := 32
if parsed.To4() == nil {
bits = 128
}
return netlink.Addr{
IPNet: &net.IPNet{IP: parsed, Mask: net.CIDRMask(mask, bits)},
LinkIndex: linkIndex,
}
}
func collectIPs(addrs []net.Addr) []string {
out := make([]string, 0, len(addrs))
for _, a := range addrs {
ipnet, ok := a.(*net.IPNet)
if !ok {
continue
}
out = append(out, ipnet.IP.String())
}
sort.Strings(out)
return out
}
func TestFilterAddrsExcept(t *testing.T) {
tests := []struct {
name string
addrs []netlink.Addr
devIndex int
expected []string
}{
{
name: "filters addresses on dev by LinkIndex",
addrs: []netlink.Addr{
addr("192.168.1.10", 24, 2),
addr("10.233.0.1", 32, 10),
addr("10.233.0.2", 32, 10),
addr("10.233.0.3", 32, 10),
addr("fd00::1", 128, 2),
},
devIndex: 10,
expected: []string{"192.168.1.10", "fd00::1"},
},
{
name: "keeps everything when no address belongs to dev",
addrs: []netlink.Addr{
addr("192.168.1.10", 24, 2),
addr("10.0.0.1", 24, 3),
},
devIndex: 10,
expected: []string{"10.0.0.1", "192.168.1.10"},
},
{
name: "drops nil IPNet defensively",
addrs: []netlink.Addr{
{IPNet: nil, LinkIndex: 2},
addr("192.168.1.10", 24, 2),
},
devIndex: 10,
expected: []string{"192.168.1.10"},
},
{
name: "empty input",
addrs: nil,
devIndex: 10,
expected: []string{},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := collectIPs(filterAddrsExcept(tc.addrs, tc.devIndex))
want := tc.expected
if want == nil {
want = []string{}
}
if !reflect.DeepEqual(got, want) {
t.Errorf("filterAddrsExcept(_, %d) = %v, want %v", tc.devIndex, got, want)
}
})
}
}