18

分布式负载均衡算法之亲和性轮询原理

 4 years ago
source link: https://studygolang.com/articles/26477
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

无论是在早期的负载均衡器中,还是当前微服务基于客户端的负载均衡中,都有一个最基础的轮询算法,即将请求平均分布给多台机器,今天聊聊在此基础上, kube proxy是如何实现亲和性轮询的核心数据结构. 了解亲和性策略实现,失败重试等机制

1. 基础筑基

1.1 Service与Endpoints

EvaIRz7.png!web Service和Endpoint是kubernetes中的概念,其中Service代表一个服务,后面通常会对应一堆pod,因为pod的ip并不是固定的,用Servicel来提供后端一组pod的统一访问入口, 而Endpoints则是一组后端提供相同服务的IP和端口集合在这节内容中大家知道这些就可以来,

1.2 轮询算法

3qeI7rE.png!web 轮询算法可能是最简单的算法了,在go里面大多数实现都是通过一个slice存储当前可以访问的后端所有地址,而通过index来保存下一次请求分配的主机在slice中的索引

1.3 亲和性

ammmquE.png!web 亲和性实现上也相对简单,所谓亲和性其实就是当某个IP重复调用后端某个服务,则将其转发到之前转发的机器上即可

2. 核心数据结构实现

2.1 亲和性实现

zaiyQzq.png!web

2.1.1 亲和性之亲和性策略

亲和性策略设计上主要是分为三个部分实现:affinityPolicy:亲和性类型,即根据客户端的什么信息来做亲和性依据,现在是基于clientipaffinityMap:根据Policy中定义的亲和性的类型作为hash的key, 存储clientip的亲和性信息ttlSeconds: 存储亲和性的过期时间, 即当超过该时间则会重新进行RR轮询算法选择

type affinityPolicy struct {
    affinityType v1.ServiceAffinity // Type字段只是一个字符串不需要深究
    affinityMap  map[string]*affinityState // map client IP -> affinity info
    ttlSeconds   int
}复制代码

2.1.2 亲和性之亲和性状态

上面提到会通过affinityMap存储亲和性状态, 其实亲和性状态里面关键信息有两个endpoint(后端要访问的endpoint)和lastUsed(亲和性最后被访问的时间)

type affinityState struct {
    clientIP string
    //clientProtocol  api.Protocol //not yet used
    //sessionCookie   string       //not yet used
    endpoint string
    lastUsed time.Time 
}复制代码

2.2 Service数据结构之负载均衡状态

7Vjaeey.png!web balancerState存储当前Service的负载均衡状态数据,其中endpoints存储后端pod的ip:port集合, index则是实现RR轮询算法的节点索引, affinity存储对应的亲和性策略数据

type balancerState struct {
    endpoints []string // a list of "ip:port" style strings
    index     int      // current index into endpoints
    affinity  affinityPolicy
}复制代码

2.3 负载均衡轮询数据结构

UbiUvmF.png!web 核心数据结构主要通过services字段来保存服务对应的负载均衡状态,并通过读写锁来进行service map进行保护

type LoadBalancerRR struct {
    lock     sync.RWMutex
    services map[proxy.ServicePortName]*balancerState
}复制代码

2.4 负载均衡算法实现

我们只关注负载均衡进行轮询与亲和性分配的相关实现,对于感知service与endpoints部分代码,省略更新删除等逻辑, 下面章节是NextEndpoint实现

2.4.1 加锁与合法性效验

合法性效验主要是检测对应的服务是否存在,并且检查对应的endpoint是否存在

lb.lock.Lock()
    defer lb.lock.Unlock() // 加锁
    // 进行服务是否存在检测
    state, exists := lb.services[svcPort]
    if !exists || state == nil {
        return "", ErrMissingServiceEntry
    }
    // 检查服务是否有服务的endpoint
    if len(state.endpoints) == 0 {
        return "", ErrMissingEndpoints
    }
    klog.V(4).Infof("NextEndpoint for service %q, srcAddr=%v: endpoints: %+v", svcPort, srcAddr, state.endpoints)复制代码

2.4.2 亲和性类型支持检测

通过检测亲和性类型,确定当前是否支持亲和性,即通过检查对应的字段是否设置

sessionAffinityEnabled := isSessionAffinity(&state.affinity)

func isSessionAffinity(affinity *affinityPolicy) bool {
    // Should never be empty string, but checking for it to be safe.
    if affinity.affinityType == "" || affinity.affinityType == v1.ServiceAffinityNone {
        return false
    }
    return true
}复制代码

2.4.3 亲和性匹配与最后访问更新

亲和性匹配则会优先返回对应的endpoint,但是如果此时该endpoint已经访问失败了,则就需要重新选择节点,就需要重置亲和性

var ipaddr string
    if sessionAffinityEnabled {
        // Caution: don't shadow ipaddr
        var err error
        // 获取对应的srcIP当前是根据客户端的ip进行匹配
        ipaddr, _, err = net.SplitHostPort(srcAddr.String())
        if err != nil {
            return "", fmt.Errorf("malformed source address %q: %v", srcAddr.String(), err)
        }
        
        // 亲和性重置,默认情况下是false, 但是如果当前的endpoint访问出错,则需要重置
        // 因为已经连接出错了,肯定要重新选择一台机器,当前的亲和性就不能继续使用了
        if !sessionAffinityReset {
            // 如果发现亲和性存在,则返回对应的endpoint
            sessionAffinity, exists := state.affinity.affinityMap[ipaddr]
            if exists && int(time.Since(sessionAffinity.lastUsed).Seconds()) < state.affinity.ttlSeconds {
                // Affinity wins.
                endpoint := sessionAffinity.endpoint
                sessionAffinity.lastUsed = time.Now()
                klog.V(4).Infof("NextEndpoint for service %q from IP %s with sessionAffinity %#v: %s", svcPort, ipaddr, sessionAffinity, endpoint)
                return endpoint, nil
            }
        }
    }复制代码

2.4.4 根据clientIP构建亲和性状态

// 获取一个endpoint, 并更新索引
    endpoint := state.endpoints[state.index]
    state.index = (state.index + 1) % len(state.endpoints)

    if sessionAffinityEnabled {
        // 保存亲和性状态
        var affinity *affinityState
        affinity = state.affinity.affinityMap[ipaddr]
        if affinity == nil {
            affinity = new(affinityState) //&affinityState{ipaddr, "TCP", "", endpoint, time.Now()}
            state.affinity.affinityMap[ipaddr] = affinity
        }
        affinity.lastUsed = time.Now()
        affinity.endpoint = endpoint
        affinity.clientIP = ipaddr
        klog.V(4).Infof("Updated affinity key %s: %#v", ipaddr, state.affinity.affinityMap[ipaddr])
    }

    return endpoint, nil复制代码

好了,今天的分析就到这里,希望能帮组到大家,了解亲和性轮询算法的实现, 学习到核心的数据结构设计,以及在产生中应对故障的一些设计,就到这里,感谢大家分享关注,谢谢大家

微信号:baxiaoshi2020 A3YNnyZ.jpg!web

关注公告号阅读更多源码分析文章 fuAvQjn.jpg!web

更多文章关注 www.sreguide.com

本文由博客一文多发平台 OpenWrite 发布


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK