分佈式負載均衡算法之親和性輪詢原理

無論是在早期的負載均衡器中,還是當前微服務基於客戶端的負載均衡中,都有一個最基礎的輪詢算法,即將請求平均分佈給多臺機器,今天聊聊在此基礎上, kube proxy是如何實現親和性輪詢的核心數據結構. 瞭解親和性策略實現,失敗重試等機制

1. 基礎築基

1.1 Service與Endpoints

分佈式負載均衡算法之親和性輪詢原理

Service和Endpoint是kubernetes中的概念,其中Service代表一個服務,後面通常會對應一堆pod,因為pod的ip並不是固定的,用Servicel來提供後端一組pod的統一訪問入口, 而Endpoints則是一組後端提供相同服務的IP和端口集合 在這節內容中大家知道這些就可以來,

1.2 輪詢算法

分佈式負載均衡算法之親和性輪詢原理

輪詢算法可能是最簡單的算法了,在go裡面大多數實現都是通過一個slice存儲當前可以訪問的後端所有地址,而通過index來保存下一次請求分配的主機在slice中的索引

1.3 親和性

分佈式負載均衡算法之親和性輪詢原理

親和性實現上也相對簡單,所謂親和性其實就是當某個IP重複調用後端某個服務,則將其轉發到之前轉發的機器上即可

2. 核心數據結構實現

2.1 親和性實現

分佈式負載均衡算法之親和性輪詢原理

2.1.1 親和性之親和性策略

親和性策略設計上主要是分為三個部分實現: affinityPolicy:親和性類型,即根據客戶端的什麼信息來做親和性依據,現在是基於clientip affinityMap:根據Policy中定義的親和性的類型作為hash的key, 存儲clientip的親和性信息 ttlSeconds: 存儲親和性的過期時間, 即當超過該時間則會重新進行RR輪詢算法選擇

<code>type affinityPolicy struct {
\taffinityType v1.ServiceAffinity // Type字段只是一個字符串不需要深究
\taffinityMap map[string]*affinityState // map client IP -> affinity info
\tttlSeconds int
}
/<code>

2.1.2 親和性之親和性狀態

上面提到會通過affinityMap存儲親和性狀態, 其實親和性狀態裡面關鍵信息有兩個endpoint(後端要訪問的endpoint)和lastUsed(親和性最後被訪問的時間)

<code>type affinityState struct {
\tclientIP string
\t//clientProtocol api.Protocol //not yet used
\t//sessionCookie string //not yet used
\tendpoint string
\tlastUsed time.Time
}
/<code>

2.2 Service數據結構之負載均衡狀態

分佈式負載均衡算法之親和性輪詢原理

balancerState存儲當前Service的負載均衡狀態數據,其中endpoints存儲後端pod的ip:port集合, index則是實現RR輪詢算法的節點索引, affinity存儲對應的親和性策略數據

<code>type balancerState struct {
\tendpoints []string // a list of "ip:port" style strings
\tindex int // current index into endpoints
\taffinity affinityPolicy
}
/<code>

2.3 負載均衡輪詢數據結構

分佈式負載均衡算法之親和性輪詢原理

核心數據結構主要通過services字段來保存服務對應的負載均衡狀態,並通過讀寫鎖來進行service map進行保護

<code>type LoadBalancerRR struct {
\tlock sync.RWMutex
\tservices map[proxy.ServicePortName]*balancerState
}
/<code>

2.4 負載均衡算法實現

我們只關注負載均衡進行輪詢與親和性分配的相關實現,對於感知service與endpoints部分代碼,省略更新刪除等邏輯, 下面章節是NextEndpoint實現

2.4.1 加鎖與合法性效驗

合法性效驗主要是檢測對應的服務是否存在,並且檢查對應的endpoint是否存在

<code>\tlb.lock.Lock()
\tdefer lb.lock.Unlock() // 加鎖
\t// 進行服務是否存在檢測
\tstate, exists := lb.services[svcPort]
\tif !exists || state == nil {
\t\treturn "", ErrMissingServiceEntry
\t}
\t// 檢查服務是否有服務的endpoint
\tif len(state.endpoints) == 0 {
\t\treturn "", ErrMissingEndpoints
\t}
\tklog.V(4).Infof("NextEndpoint for service %q, srcAddr=%v: endpoints: %+v", svcPort, srcAddr, state.endpoints)
/<code>

2.4.2 親和性類型支持檢測

通過檢測親和性類型,確定當前是否支持親和性,即通過檢查對應的字段是否設置

<code>sessionAffinityEnabled := isSessionAffinity(&state.affinity) 


func isSessionAffinity(affinity *affinityPolicy) bool {
\t// Should never be empty string, but checking for it to be safe.
\tif affinity.affinityType == "" || affinity.affinityType == v1.ServiceAffinityNone {
\t\treturn false
\t}
\treturn true
}
/<code>

2.4.3 親和性匹配與最後訪問更新

親和性匹配則會優先返回對應的endpoint,但是如果此時該endpoint已經訪問失敗了,則就需要重新選擇節點,就需要重置親和性

<code>\tvar ipaddr string
\tif sessionAffinityEnabled {
\t\t// Caution: don't shadow ipaddr
\t\tvar err error
// 獲取對應的srcIP當前是根據客戶端的ip進行匹配
\t\tipaddr, _, err = net.SplitHostPort(srcAddr.String())
\t\tif err != nil {
\t\t\treturn "", fmt.Errorf("malformed source address %q: %v", srcAddr.String(), err)
\t\t}

// 親和性重置,默認情況下是false, 但是如果當前的endpoint訪問出錯,則需要重置
// 因為已經連接出錯了,肯定要重新選擇一臺機器,當前的親和性就不能繼續使用了
\t\tif !sessionAffinityReset {
// 如果發現親和性存在,則返回對應的endpoint
\t\t\tsessionAffinity, exists := state.affinity.affinityMap[ipaddr]
\t\t\tif exists && int(time.Since(sessionAffinity.lastUsed).Seconds()) < state.affinity.ttlSeconds {
\t\t\t\t// Affinity wins.
\t\t\t\tendpoint := sessionAffinity.endpoint
\t\t\t\tsessionAffinity.lastUsed = time.Now()
\t\t\t\tklog.V(4).Infof("NextEndpoint for service %q from IP %s with sessionAffinity %#v: %s", svcPort, ipaddr, sessionAffinity, endpoint)
\t\t\t\treturn endpoint, nil

\t\t\t}
\t\t}
\t}
/<code>

2.4.4 根據clientIP構建親和性狀態

<code>\t// 獲取一個endpoint, 並更新索引
\tendpoint := state.endpoints[state.index]
\tstate.index = (state.index + 1) % len(state.endpoints)

\tif sessionAffinityEnabled {
\t\t// 保存親和性狀態
\t\tvar affinity *affinityState
\t\taffinity = state.affinity.affinityMap[ipaddr]
\t\tif affinity == nil {
\t\t\taffinity = new(affinityState) //&affinityState{ipaddr, "TCP", "", endpoint, time.Now()}
\t\t\tstate.affinity.affinityMap[ipaddr] = affinity
\t\t}
\t\taffinity.lastUsed = time.Now()
\t\taffinity.endpoint = endpoint
\t\taffinity.clientIP = ipaddr
\t\tklog.V(4).Infof("Updated affinity key %s: %#v", ipaddr, state.affinity.affinityMap[ipaddr])
\t}

\treturn endpoint, nil
/<code>

好了,今天的分析就到這裡,希望能幫助到大家,瞭解親和性輪詢算法的實現, 學習到核心的數據結構設計,以及在產生中應對故障的一些設計,就到這裡,感謝大家分享關注,謝謝大家

文章來源:https://my.oschina.net/u/4131034/blog/3163809

關注我瞭解更多程序員資訊技術,領取豐富架構資料


分享到:


相關文章: