源码分析 kubernetes kube-proxy 的设计实现

源码分析 kubernetes kube-proxy 的设计实现

kubernetes kube-proxy 的版本是 v1.27.0

cmd 入口

kube-proxy 启动的过程就不细说了,简单说就是解析传递配置,构建 ProxyServer 服务,最后启动 ProxyServer.

cmd/kube-proxy/proxy.go: main
    cmd/kube-proxy/app/server.go: NewProxyCommand
        cmd/kube-proxy/app/server.go: ProxyServer.Run()

ProxyServer

在创建 NewProxyServer() 时,根据 proxyMode 实例化不同的 proxier 对象. 在 kube-proxy 里 proxier 有iptables 和 ipvs 两种实现, 以前在 userspace 实现的代理已经在老版本中剔除了.

代码位置: cmd/kube-proxy/app/server.go

// NewProxyServer returns a new ProxyServer.
func NewProxyServer(o *Options) (*ProxyServer, error) {
    return newProxyServer(o.config, o.master)
}

func newProxyServer(
    config *proxyconfigapi.KubeProxyConfiguration,
    master string) (*ProxyServer, error) {

    // 实例化 iptables 的 proxy
    if proxyMode == proxyconfigapi.ProxyModeIPTables {
        if dualStack {
        } else {
            proxier, err = iptables.NewProxier(
                iptInterface,
                utilsysctl.New(),
                execer,
                config.IPTables.SyncPeriod.Duration,
                config.IPTables.MinSyncPeriod.Duration,
                ...
            )
        }

    // 实例化 ipvs 的 proxy
    } else if proxyMode == proxyconfigapi.ProxyModeIPVS {
        // 实例化 kernel, ipset,ipvs 管理工具
        kernelHandler := ipvs.NewLinuxKernelHandler()
        ipsetInterface = utilipset.New(execer)
        ipvsInterface = utilipvs.New()

        if dualStack {
        } else {
            if err != nil {
                return nil, fmt.Errorf("unable to create proxier: %v", err)
            }

            proxier, err = ipvs.NewProxier(
                iptInterface,
                ipvsInterface,
                ipsetInterface,
                utilsysctl.New(),
                ...
            )
        }
        ...
    }

    return &ProxyServer{
        Client:                 client,
        EventClient:            eventClient,
        IptInterface:           iptInterface,
        IpvsInterface:          ipvsInterface,
        IpsetInterface:         ipsetInterface,
        ...
    }, nil
}

ProxyServer 在调用 Run() 启动时会实例化 informer 监听器,然后一直监听 apiserver 反馈的 service, endpoints, node 变动事件.

func (s *ProxyServer) Run() error {
    informerFactory := informers.NewSharedInformerFactoryWithOptions(s.Client, s.ConfigSyncPeriod,
        informers.WithTweakListOptions(func(options *metav1.ListOptions) {
            options.LabelSelector = labelSelector.String()
        }))

    // 监听 service resource 资源
    serviceConfig := config.NewServiceConfig(informerFactory.Core().V1().Services(), s.ConfigSyncPeriod)
    serviceConfig.RegisterEventHandler(s.Proxier)
    go serviceConfig.Run(wait.NeverStop)

    // 监听 endpoints resource 资源
    endpointSliceConfig := config.NewEndpointSliceConfig(informerFactory.Discovery().V1().EndpointSlices(), s.ConfigSyncPeriod)
    endpointSliceConfig.RegisterEventHandler(s.Proxier)
    go endpointSliceConfig.Run(wait.NeverStop)

    informerFactory.Start(wait.NeverStop)

    // 监听 node 资源更新
    currentNodeInformerFactory := informers.NewSharedInformerFactoryWithOptions(s.Client, s.ConfigSyncPeriod,
        informers.WithTweakListOptions(func(options *metav1.ListOptions) {
            options.FieldSelector = fields.OneTermEqualSelector("metadata.name", s.NodeRef.Name).String()
        }))
    nodeConfig := config.NewNodeConfig(currentNodeInformerFactory.Core().V1().Nodes(), s.ConfigSyncPeriod)
    nodeConfig.RegisterEventHandler(s.Proxier)
    go nodeConfig.Run(wait.NeverStop)

    currentNodeInformerFactory.Start(wait.NeverStop)

    s.birthCry()

    go s.Proxier.SyncLoop()

    return <-errCh
}

如何监听和处理事件 ?

kube-proxy 开启了对 services 和 endpoints 的事件监听,但篇幅原因这里就只聊 Services 的事件监听.

初始化阶段时会实例化 ServiceConfig 对象, 并向 serviceConfig 注册 proxier 对象. ServiceConfig 实现了对 informer 的监听,并向 informer 注册封装的回调接口. 当有 service 的增删改事件时, 调用 proxier 的 OnServiceAdd, OnServiceUpdate, OnServiceDelete 方法.

type ServiceConfig struct {
    listerSynced  cache.InformerSynced
    eventHandlers []ServiceHandler
}

// NewServiceConfig creates a new ServiceConfig.
func NewServiceConfig(serviceInformer coreinformers.ServiceInformer, resyncPeriod time.Duration) *ServiceConfig {
    result := &ServiceConfig{
        listerSynced: serviceInformer.Informer().HasSynced,
    }

    serviceInformer.Informer().AddEventHandlerWithResyncPeriod(
        cache.ResourceEventHandlerFuncs{
            AddFunc:    result.handleAddService,
            UpdateFunc: result.handleUpdateService,
            DeleteFunc: result.handleDeleteService,
        },
        resyncPeriod,
    )

    return result
}

// 注册事件处理,其实就是 proxier, 它实现了 ServiceHandler 接口.
func (c *ServiceConfig) RegisterEventHandler(handler ServiceHandler) {
    c.eventHandlers = append(c.eventHandlers, handler)
}

func (c *ServiceConfig) Run(stopCh <-chan struct{}) {
    // 第一次启动时, 同步下数据到缓存
    if !cache.WaitForNamedCacheSync("service config", stopCh, c.listerSynced) {
        return
    }

    // proxier 的 OnServiceSynced 实现是调用 `proxier.syncProxyRules()`
    for i := range c.eventHandlers {
        c.eventHandlers[i].OnServiceSynced()
    }
}

// 处理 service 新增事件
func (c *ServiceConfig) handleAddService(obj interface{}) {
    service, ok := obj.(*v1.Service)
    for i := range c.eventHandlers {
        c.eventHandlers[i].OnServiceAdd(service)
    }
}

// 处理 service 更新事件
func (c *ServiceConfig) handleUpdateService(oldObj, newObj interface{}) {
    oldService, ok := oldObj.(*v1.Service)
    for i := range c.eventHandlers {
        c.eventHandlers[i].OnServiceUpdate(oldService, service)
    }
}

// 处理 service 删除事件
func (c *ServiceConfig) handleDeleteService(obj interface{}) {
    service, ok := obj.(*v1.Service)
    for i := range c.eventHandlers {
        c.eventHandlers[i].OnServiceDelete(service)
    }
}

从 ipvs 的 proxier 里, 分析下 OnServiceAdd, OnServiceUpdate, OnServiceDelete 这几个方法的实现. 其实就是对 proxier 的 serviceChanges 做存储变动对象操作. 需要注意的是,真正处理变动的在 proxier.syncProxyRules() 方法里.

// OnServiceAdd is called whenever creation of new service object is observed.
func (proxier *Proxier) OnServiceAdd(service *v1.Service) {
    proxier.OnServiceUpdate(nil, service)
}

// OnServiceUpdate is called whenever modification of an existing service object is observed.
func (proxier *Proxier) OnServiceUpdate(oldService, service *v1.Service) {
    if proxier.serviceChanges.Update(oldService, service) && proxier.isInitialized() {
        proxier.Sync()
    }
}

// OnServiceDelete is called whenever deletion of an existing service object is observed.
func (proxier *Proxier) OnServiceDelete(service *v1.Service) {
    proxier.OnServiceUpdate(service, nil)
}

真正干活 Proxier 的实现

在阅读了 kube-proxy 源码后可以发现,Proxier 才是真正干活的类,kube-proxy 里对于 ipvs,iptables, ipset 等工具的操作都在 proxier 里.

在 kube-proxy 中 proxier 有 iptables 和 ipvs 两种实现, 现在已经少有用 iptables 的了,所以就只分析 ipvs proxier 实现.

在实例化 Proxier 对象时会注册一个定时任务,它会周期性调用 syncProxyRules 方法.

func NewProxier(
    syncPeriod time.Duration,
    minSyncPeriod time.Duration,
    masqueradeAll bool,
    ...
) (*Proxier, error) {
    ...
    proxier := &Proxier{
        svcPortMap:            make(proxy.ServicePortMap),
        endpointsMap:          make(proxy.EndpointsMap),
        ...
    }

    ...
    proxier.syncRunner = async.NewBoundedFrequencyRunner("sync-runner", proxier.syncProxyRules, minSyncPeriod, syncPeriod, burstSyncs)
    return proxier, nil
}

syncProxyRules 函数是 kube-proxy 实现的核心. 该主要逻辑每次都从头开始遍历 servcie 和 endpoints 所有对象,对比当前配置来进行增删改 ipvs virtualserver 和 realserver, 及绑定解绑 dummy ip地址,还有配置 ipset 和 iptables 的相关策略. 原理大概是这样,下面看细节实现.

代码位置: https://github.com/kubernetes/kubernetes/blob/master/pkg/proxy/ipvs/proxier.go

func (proxier *Proxier) syncProxyRules() {
    proxier.mu.Lock()
    defer proxier.mu.Unlock()

    // 把从 informer 拿到变更的 servcie 结构,更新到 svcPortMap 里, 每次 Update 还会把 changes 清空.
    serviceUpdateResult := proxier.svcPortMap.Update(proxier.serviceChanges)

    // 把从 informer 拿到变更的 endpoint 结构,更新到 endpointsMap 里, 每次 Update 还会把 changes 清空.
    endpointUpdateResult := proxier.endpointsMap.Update(proxier.endpointsChanges)

    // 创建名为 `kube-ipvs0` 的 dummy 类型网络设备
    _, err := proxier.netlinkHandle.EnsureDummyDevice(defaultDummyDevice)
    if err != nil {
        return
    }

    // 创建 ipset 规则,调用 ipset create 创建 ipset 时,还需指定 size 和 entry 的格式.
    for _, set := range proxier.ipsetList {
        if err := ensureIPSet(set); err != nil {
            return
        }
        set.resetEntries()
    }

    // 遍历当前的 services 对象集合
    for svcPortName, svcPort := range proxier.svcPortMap {
        // 遍历 endpoints 对象
        for _, e := range proxier.endpointsMap[svcPortName] {

            // 拿到 endpoints 对象
            ep, ok := e.(*proxy.BaseEndpointInfo)
            epIP := ep.IP()
            epPort, err := ep.Port()

            // 定义 endpoint 的 ipset entry 结构
            entry := &utilipset.Entry{
                IP:       epIP,
                Port:     epPort,
                Protocol: protocol,
                IP2:      epIP,
                SetType:  utilipset.HashIPPortIP,
            }

                // 在 kubeLoopBackIPSet 配置集合中加入 entry 配置.
            proxier.ipsetList[kubeLoopBackIPSet].activeEntries.Insert(entry.String())
        }

        // 定义 service 的 ipset entry 结构
        entry := &utilipset.Entry{
            IP:       svcInfo.ClusterIP().String(),
            Port:     svcInfo.Port(),
        }

        // 把 service ipset entry 加入到 kubeClusterIPSet 配置集合中
        proxier.ipsetList[kubeClusterIPSet].activeEntries.Insert(entry.String())
        serv := &utilipvs.VirtualServer{
            Address:   svcInfo.ClusterIP(),
            Port:      uint16(svcInfo.Port()),
        }

        // 创建或更新 lvs virtualServer 配置
        if err := proxier.syncService(svcPortNameString, serv, true, bindedAddresses); err == nil {
            // 创建或更新 lvs realserver 配置
            if err := proxier.syncEndpoint(svcPortName, internalNodeLocal, serv); err != nil {}
        }
    }

    // 篇幅不提, 解决 external ip.
    for _, externalIP := range svcInfo.ExternalIPStrings() {
        ...
    }
    // 篇幅不提,解决 load balancer
    for _, ingress := range svcInfo.LoadBalancerIPStrings() {
        ...
    }

    // 同步 ipset 配置.
    for _, set := range proxier.ipsetList {
        set.syncIPSetEntries()
    }

    // 同步 iptables 的配置.
    proxier.writeIptablesRules()
    proxier.iptablesData.Reset()
    proxier.iptablesData.Write(proxier.natChains.Bytes())
    proxier.iptablesData.Write(proxier.natRules.Bytes())
    err = proxier.iptables.RestoreAll(proxier.iptablesData.Bytes(), utiliptables.NoFlushTables, utiliptables.RestoreCounters)
    ...

    // 清理绑定的ip地址
    legacyBindAddrs := proxier.getLegacyBindAddr(activeBindAddrs, currentBindAddrs)

    // 清理需要删除 service, 逻辑里含有 ipvs vs 和 rs 的清理.
    proxier.cleanLegacyService(activeIPVSServices, currentIPVSServices, legacyBindAddrs)

    // 遍历不新鲜的 servcies 集合,通过 contrack tool 工具剔除在 contrack 里协议为 UDP 的旧连接.
    for _, svcIP := range staleServices.UnsortedList() {
        if err := conntrack.ClearEntriesForIP(proxier.exec, svcIP, v1.ProtocolUDP); err != nil {
            klog.ErrorS(err, "Failed to delete stale service IP connections", "IP", svcIP)
        }
    }
}

同步 ipset 配置 ? (syncIPSetEntries)

syncIPSetEntries() 用来同步刷新 ipset 配置,就是把 ip 地址通过 ipset 命令更新到对应的 ipset 集合里.

代码位置: pkg/proxy/ipvs/ipset.go

func (set *IPSet) syncIPSetEntries() {
    // 从本机获取 name 相关所有的 ipset entry
    appliedEntries, err := set.handle.ListEntries(set.Name)
    if err != nil {
        return
    }

    // 放到一个 string set 集合里,为后面做差集做准备
    currentIPSetEntries := sets.NewString()
    for _, appliedEntry := range appliedEntries {
        currentIPSetEntries.Insert(appliedEntry)
    }

    // 如果不相等的话, 进行同步操作.
    if !set.activeEntries.Equal(currentIPSetEntries) {
        // 删除遗留的 ipset 元素. 也就是删除本机有,但在激活集合中不存在的 ipset entry.
        for _, entry := range currentIPSetEntries.Difference(set.activeEntries).List() {
            if err := set.handle.DelEntry(entry, set.Name); err != nil {
                ...
            }
        }
        // 添加 ipset 元素. 添加本机没有但在激活集合中存在的 ipset entry.
        for _, entry := range set.activeEntries.Difference(currentIPSetEntries).List() {
            if err := set.handle.AddEntry(entry, &set.IPSet, true); err != nil {
            }
        }
    }
}

ipset 的最终是依赖 exec 去调用 ipset 二进制执行文件实现的. 增删改 ipset 集合时先是拼凑 ipset 命令的参数,然后在传递给 ipset 命令去执行.

代码位置: pkg/util/ipset/ipset.go

const IPSetCmd = "ipset"

type IPSet struct {
    Name string
    SetType Type
    HashSize int
    MaxElem int
    ...
}

func (runner *runner) AddEntry(entry string, set *IPSet, ignoreExistErr bool) error {
    args := []string{"add", set.Name, entry}
    if ignoreExistErr {
        args = append(args, "-exist")
    }
    if out, err := runner.exec.Command(IPSetCmd, args...).CombinedOutput(); err != nil {
        return fmt.Errorf("error adding entry %s, error: %v (%s)", entry, err, out)
    }
    return nil
}

func (runner *runner) DelEntry(entry string, set string) error {
    if out, err := runner.exec.Command(IPSetCmd, "del", set, entry).CombinedOutput(); err != nil {
        return fmt.Errorf("error deleting entry %s: from set: %s, error: %v (%s)", entry, set, err, out)
    }
    return nil
}

同步 ipvs service 配置 ? (syncService)

如果 ipvs vs 在当前节点配置过,且配置没有变动,另外 service ip 已绑定到 dummy 设备上,就一路绿灯无需真实去操作.

如果当前节点不存在该 ipvs VirtualServer, 则需要进行创建,如果已经存在,但配置不同,则进行更新 VirtualServer,另外还需要在本地 kube-ipvs0 设备上绑定 service clusterIP 地址.

k8s 的 ipvs 是通过 moby/ipvs 库实现增删改查操作, 原理是 moby/ipvs 里使用 netlink 实现用户态进程跟内核态进行通信.

func (proxier *Proxier) syncService(svcName string, vs *utilipvs.VirtualServer, bindAddr bool, bindedAddresses sets.String) error {
    // 从当前节点里获取该 ipvs vs 配置
    appliedVirtualServer, _ := proxier.ipvs.GetVirtualServer(vs)
    // 如果当前节点不存在该 ipvs vs 配置,或者 有配置但配置发生了变更.
    if appliedVirtualServer == nil || !appliedVirtualServer.Equal(vs) {
        // 当前节点不存在该 ipvs vs, 则进行创建
        if appliedVirtualServer == nil {
            klog.V(3).InfoS("Adding new service", "serviceName", svcName, "virtualServer", vs)
            if err := proxier.ipvs.AddVirtualServer(vs); err != nil {
                return err
            }
        } else {
            // 如果配置发生变更,则进行 update 更新.
            klog.V(3).InfoS("IPVS service was changed", "serviceName", svcName)
            if err := proxier.ipvs.UpdateVirtualServer(vs); err != nil {
                return err
            }
        }
    }

    // 把 service ip 绑定到 dummy 设备上,也就是 kube-ipvs0 设备.
    if bindAddr {
        // 如果已经绑定了,那就无需再绑定.
        if bindedAddresses != nil && bindedAddresses.Has(vs.Address.String()) {
            return nil
        }

        // 绑定 ip 地址, 重复执行也不会返回 error,可理解为幂等操作.
        _, err := proxier.netlinkHandle.EnsureAddressBind(vs.Address.String(), defaultDummyDevice)
        if err != nil {
            return err
        }
    }

    return nil
}

如何删除 ipvs service ? (cleanLegacyService)

对比 active 和主机的 ipvs vs 的配置,如果主机存在某 service 配置,但 active 里没有,则删除并进行解绑 ip.

func (proxier *Proxier) cleanLegacyService(activeServices map[string]bool, currentServices map[string]*utilipvs.VirtualServer, legacyBindAddrs map[string]bool) {
    isIPv6 := netutils.IsIPv6(proxier.nodeIP)
    // 使用当前的 services 进行遍历
    for cs := range currentServices {
        svc := currentServices[cs]
        ...
        // 如果激活集合里存在,但当前集合不存在,说明需要删除该 service.
        if _, ok := activeServices[cs]; !ok {
            klog.V(4).InfoS("Delete service", "virtualServer", svc)
            // 调用 ipvs 删除 svc 相关的负载策略
            if err := proxier.ipvs.DeleteVirtualServer(svc); err != nil {
                klog.ErrorS(err, "Failed to delete service", "virtualServer", svc)
            }
            addr := svc.Address.String()
            if _, ok := legacyBindAddrs[addr]; ok {
                klog.V(4).InfoS("Unbinding address", "address", addr)
                // 解绑 cluster ip 地址
                proxier.netlinkHandle.UnbindAddress(addr, defaultDummyDevice)
                delete(legacyBindAddrs, addr)
            }
        }
    }
}

同步 ipvs endpoints 配置 (syncEndpoint)?

对比当前 virtualServer 的 realserver 集合,如果有新增变动,则调用 ipvs.AddRealServer, 有变动则使用 UpdateRealServer 更改.

func (proxier *Proxier) syncEndpoint(svcPortName proxy.ServicePortName, onlyNodeLocalEndpoints bool, vs *utilipvs.VirtualServer) error {
    // 获取当前主机的 ipvs vs 配置
    appliedVirtualServer, err := proxier.ipvs.GetVirtualServer(vs)
    if err != nil {
        klog.ErrorS(err, "Failed to get IPVS service")
        return err
    }

    curEndpoints := sets.NewString()
    curDests, err := proxier.ipvs.GetRealServers(appliedVirtualServer)
    if err != nil {
        return err
    }

    // 把 realserver 放到一个 set string 集合里
    for _, des := range curDests {
        curEndpoints.Insert(des.String())
    }

    endpoints := proxier.endpointsMap[svcPortName]
    newEndpoints := sets.NewString()
    for _, epInfo := range endpoints {
        newEndpoints.Insert(epInfo.String())
    }

    // 添加 realserver
    for _, ep := range newEndpoints.List() {
        ip, port, err := net.SplitHostPort(ep)
        portNum, err := strconv.Atoi(port)
        newDest := &utilipvs.RealServer{
            Address: netutils.ParseIPSloppy(ip),
            Port:    uint16(portNum),
            Weight:  1,
        }

        // 更新 realServer
        if curEndpoints.Has(ep) {
            if proxier.initialSync {
                for _, dest := range curDests {
                    // 如果新旧的权重不同, 则调用 ipvs 接口进行变更
                    if dest.Weight != newDest.Weight {
                        err = proxier.ipvs.UpdateRealServer(appliedVirtualServer, newDest)
                        if err != nil {
                            continue
                        }
                    }
                }
            }

            ...
        }
        // 为 vs 添加新的 realServer
        err = proxier.ipvs.AddRealServer(appliedVirtualServer, newDest)
        if err != nil {
            klog.ErrorS(err, "Failed to add destination", "newDest", newDest)
            continue
        }
    }
    ...
    return nil
}

在 conntrack 里剔除 UDP 旧连接 (ClearEntriesForIP) ?

kube-proxy 在删除 UDP 的 service 时候, 也需要清除这些 ip_conntrack 连接,否则后面的流量还是会被导入的废弃的 pod 上面. 为什么 TCP 不需要在 conntrack 里剔除,而 UDP 需要 ? 这是因为 TCP 本是有状态的连接,当把流量切到其他 pod 时,旧连接会因为对端在关闭时进行挥手 fin,或者不可达时,自然就会开辟新连接,新连接自然使用新的策略. UDP 由于是无状态的连接,所以不受影响, 虽然数据不可达,但照旧能用,所以需要剔除旧的 UDP conntrack 状态.

剔除连接的原理很简单,就是调用 conntrack-tools 来剔除连接,我们也可以使用 conntrack 工具来手动剔除.

比如 conntrack -D -s 114.114.114.114 -p UDP.

func ClearEntriesForIP(execer exec.Interface, ip string, protocol v1.Protocol) error {
    parameters := parametersWithFamily(utilnet.IsIPv6String(ip), "-D", "--orig-dst", ip, "-p", protoStr(protocol))
    err := Exec(execer, parameters...)
    if err != nil && !strings.Contains(err.Error(), NoConnectionToDelete) {
        return fmt.Errorf("error deleting connection tracking state for UDP service IP: %s, error: %v", ip, err)
    }
    return nil
}

func Exec(execer exec.Interface, parameters ...string) error {
    conntrackPath, err := execer.LookPath("conntrack")
    if err != nil {
        return fmt.Errorf("error looking for path of conntrack: %v", err)
    }

    output, err := execer.Command(conntrackPath, parameters...).CombinedOutput()
    return nil
}

ipvs 里 iptables 相关配置

分析问题

ipvs 里主要的 iptables 的实现都在 proxier.writeIptablesRules() 方法里,代码本不复杂,就是一些 iptables 的规则拼凑,所以不从源码来解析,换个角度直接通过生成的 iptabels 和 chains 流程走向来分析.

另外, kubernetes 在 ipvs 的 README.md 里有详细的 ipvs 和 iptables 的配置, 这里只说 --cluster-cidr=<cidr>, 暂时不深入 --masquerade-all=true 的配置.

代码位置: https://github.com/kubernetes/kubernetes/blob/master/pkg/proxy/ipvs/README.md

iptables chains 流程走向

iptables 配置如下:

# iptables -t nat -nL

Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
KUBE-SERVICES  all  --  0.0.0.0/0            0.0.0.0/0            /* kubernetes service portals */

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
KUBE-SERVICES  all  --  0.0.0.0/0            0.0.0.0/0            /* kubernetes service portals */

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination
KUBE-POSTROUTING  all  --  0.0.0.0/0            0.0.0.0/0            /* kubernetes postrouting rules */

Chain KUBE-MARK-MASQ (3 references)
target     prot opt source               destination
MARK       all  --  0.0.0.0/0            0.0.0.0/0            MARK or 0x4000

Chain KUBE-POSTROUTING (1 references)
target     prot opt source               destination
MASQUERADE  all  --  0.0.0.0/0            0.0.0.0/0            /* kubernetes service traffic requiring SNAT */ mark match 0x4000/0x4000
MASQUERADE  all  --  0.0.0.0/0            0.0.0.0/0            match-set KUBE-LOOP-BACK dst,dst,src

Chain KUBE-SERVICES (2 references)
target     prot opt source               destination
KUBE-MARK-MASQ  all  -- !10.244.16.0/24       0.0.0.0/0            match-set KUBE-CLUSTER-IP dst,dst
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0            match-set KUBE-CLUSTER-IP dst,dst

大家觉得文章对你有些作用! 如果想赏钱,可以用微信扫描下面的二维码,感谢!
另外再次标注博客原地址  xiaorui.cc