首页 体育 教育 财经 社会 娱乐 军事 国内 科技 互联网 房产 国际 女人 汽车 游戏

使用 Go 语言徒手撸一个简单的负载均衡器

2019-12-18

在运用了专业的负载均衡器之后,我试着自己开发一个简略的负载均衡器。我挑选运用 Go 言语来完成,由于它对并发支撑得十分好,还供给了丰厚的规范库,只需求几行代码就能够开宣布高功能的应用程序。

负载均衡器在向后端服务分发流量负载时能够运用几种战略。

我计划完成最简略的战略,即轮询。

简略的轮询负载均衡器

轮询的原理十分简略,后端服务有相等的时机处理使命。

轮询处理恳求

如上图所示,轮询进程是循环不断的,但咱们不能直接运用这种办法。

假如其间的一个后端发作毛病该怎么办?咱们当然不期望把流量定向给它。咱们只能把流量路由给正常运转的服务。

咱们需求知道一切后端服务器的状况,比方一个服务是死了仍是活着,还要盯梢它们的 url。

咱们能够界说一个结构体来保存后端的信息。

type Backend struct { 
 URL *url.URL 
 Alive bool 
 mux sync.RWMutex 
 ReverseProxy *httputil.ReverseProxy 
} 

咱们还需求一种办法来盯梢一切后端,以及一个核算器变量。

type ServerPool struct { 
 backends []*Backend 
 current uint64 
} 

之前说过,负载均衡器的作用是将流量负载分发到后端的服务器上,并将成果回来给客户端。

依据 Go 言语文档的描绘:

ReverseProxy 是一种 HTTP 处理器,它接收入向恳求,将恳求发送给另一个服务器,然后将呼应发送回客户端。

这刚好是咱们想要的,所以咱们没有必要重复创造轮子。咱们能够直接运用 ReverseProxy 来中继初始恳求。

u, _ := url.Parse 
rp := httputil.NewSingleHostReverseProxy 
 
// 初始化服务器,并添加处理器 
http.HandlerFunc 

咱们运用 httputil.NewSingleHostReverseProxy 初始化一个反向署理,这个反向署理能够将恳求中继到指定的 url。在上面的比方中,一切的恳求都会被中继到 localhost:8080,成果被发送给初始客户端。

假如看一下 ServeHTTP 办法的签名,咱们会发现它回来的是一个 HTTP 处理器,所以咱们能够将它传给 http 的 HandlerFunc。

在咱们的比方中,能够运用 Backend 里的 URL 来初始化 ReverseProxy,这样反向署理就会把恳求路由给指定的 URL。

在挑选下一个服务器时,咱们需求越过现已死掉的服务器,但不管怎样,咱们都需求一个计数器。

由于有许多客户端衔接到负载均衡器,所以发作竟态条件是不可防止的。为了防止这种状况,咱们需求运用 mutex 给 ServerPool 加锁。但这样做对功能会有影响,更何况咱们并不是真想要给 ServerPool 加锁,咱们仅仅想要更新计数器。

最理想的解决方案是运用原子操作,Go 言语的 atomic 包为此供给了很好的支撑。

func  NextIndex int { 
 return int) % uint64)) 
} 

咱们经过原子操作递加 current 的值,并经过对 slice 的长度取模来取得当时索引值。所以,回来值总是介于 0 和 slice 的长度之间,究竟咱们想要的是索引值,而不是总的计数值。

咱们需求循环将恳求路由到后端的每一台服务器上,但要越过现已死掉的服务。

GetNext 办法总是回来一个介于 0 和 slice 长度之间的值,假如这个值对应的服务器不可用,咱们需求遍历一遍 slice。

遍历一遍 slice

如上图所示,咱们将从 next 方位开端遍历整个列表,但在挑选索引时,需求确保它处在 slice 的长度之内,这个能够经过取模运算来确保。

在找到可用的服务器后,咱们将它符号为当时可用服务器。

上述操作对应的代码如下。

// GetNextPeer 回来下一个可用的服务器 
func  GetNextPeer *Backend { 
 // 遍历后端列表,找到可用的服务器 
 next := s.NextIndex 
 l := len + next // 从 next 开端遍历 
 for i := next; i   l; i++ { 
 idx := i % len // 经过取模核算取得索引 
 // 假如找到一个可用的服务器,将它作为当时服务器。假如不是初始的那个,就把它保存下来 
 if s.backends[idx].IsAlive { 
 if i != next { 
 atomic.StoreUint64) // 符号当时可用服务器 
 } 
 return s.backends[idx] 
 } 
 } 
 return nil 
} 

咱们还需求考虑到一些状况,比方不同的 goroutine 会一起拜访 Backend 结构体里的一个变量。

咱们知道,读取这个变量的 goroutine 比修正这个变量的要多,所以咱们运用 RWMutex 来串行化对 Alive 的拜访操作。

// SetAlive 
func  SetAlive { 
 b.mux.Lock 
 b.Alive = alive 
 b.mux.Unlock 
} 
 
// 假如后端还活着,IsAlive 回来 true 
func  IsAlive  { 
 b.mux.RLock 
 alive = b.Alive 
 b.mux.RUnlock 
 return 
} 

在有了上述的这些东西之后,接下来就能够用下面这个简略的办法来对恳求进行负载均衡。只有当一切的后端服务都死掉它才会退出。

// lb 对入向恳求进行负载均衡 
func lb { 
 peer := serverPool.GetNextPeer 
 if peer != nil { 
 peer.ReverseProxy.ServeHTTP 
 return 
 } 
 http.Error 
} 

这个办法能够作为 HandlerFunc 传给 http 服务器。

server := http.Server{ 
 Addr: fmt.Sprintf, 
 Handler: http.HandlerFunc, 
} 

现在的 lb 办法存在一个严峻的问题,咱们并不知道后端服务是否处于正常的运转状况。为此,咱们需求测验发送恳求,查看一下它是否正常。

咱们能够经过两种办法来到达意图:

在发作过错时,ReverseProxy 会触发 ErrorHandler 回调函数,咱们能够运用它来查看毛病。

proxy.ErrorHandler = func { 
 log.Printf) 
 retries := GetRetryFromContext 
 if retries   3 { 
 select { 
 case  -time.After: 
 ctx := context.WithValue, Retry, retries+1) 
 proxy.ServeHTTP) 
 } 
 return 
 } 
 
 // 在三次重试之后,把这个后端符号为宕机 
 serverPool.MarkBackendStatus 
 
 // 同一个恳求在测验了几个不同的后端之后,添加计数 
 attempts := GetAttemptsFromContext 
 log.Printf Attempting retry %d
 , request.RemoteAddr, request.URL.Path, attempts) 
 ctx := context.WithValue, Attempts, attempts+1) 
 lb) 
} 

咱们运用强壮的闭包来完成过错处理器,它能够捕获外部变量过错。它会查看重试次数,假如小于 3,就把同一个恳求发送给同一个后端服务器。之所以要进行重试,是由于服务器可能会发作暂时过错,在经过时刻短的推迟之后,服务器又能够持续处理恳求。咱们运用了一个计时器,把重试时刻距离设定在 10 毫秒左右。

在重试失利之后,咱们就把这个后端符号为宕机。

接下来,咱们要找出新的可用后端。咱们运用 context 来保护重试次数。在添加重试次数后,咱们把它传回 lb,挑选一个新的后端来处理恳求。

但咱们不能不加以约束,所以咱们会在进一步处理恳求之前查看是否到达了最大的重试上限。

咱们从恳求里拿到重试次数,假如现已到达最大上限,就完结这个恳求。

// lb 对传入的恳求进行负载均衡 
func lb { 
 attempts := GetAttemptsFromContext 
 if attempts   3 { 
 log.Printf Max attempts reached, terminating
 , r.RemoteAddr, r.URL.Path) 
 http.Error 
 return 
 } 
 
 peer := serverPool.GetNextPeer 
 if peer != nil { 
 peer.ReverseProxy.ServeHTTP 
 return 
 } 
 http.Error 
} 

咱们能够运用 context 在 http 恳求中保存有用的信息,用它来盯梢重试次数。

首要,咱们需求为 context 指定键。咱们主张运用不抵触的整数值作为键,而不是字符串。Go 言语供给了 iota 关键字,能够用来完成递加的常量,每一个常量都包含了仅有值。这是一种完美的整型键解决方案。

const  

然后咱们就能够像操作 HashMap 那样获取这个值。默许回来值要视状况而定。

// GetAttemptsFromContext 回来测验次数 
func GetRetryFromContext int { 
 if retry, ok := r.Context.Value.; ok { 
 return retry 
 } 
 return 0 
} 

被动模式便是守时对后端履行 ping 操作,以此来查看它们的状况。

咱们经过树立 TCP 衔接来履行 ping 操作。假如后端及时呼应,咱们就以为它还活着。当然,假如你喜爱,也能够改成直接调用某个端点,比方 /status。牢记,在履行完操作后要封闭衔接,防止给服务器形成额定的担负,不然服务器会一向保护衔接,最终把资源耗尽。

// isAlive 经过树立 TCP 衔接查看后端是否还活着 
func isBackendAlive bool { 
 timeout := 2 * time.Second 
 conn, err := net.DialTimeout 
 if err != nil { 
 log.Println 
 return false 
 } 
 _ = conn.Close // 不需求保护衔接,把它封闭 
 return true 
} 

现在咱们能够遍历服务器,并符号它们的状况。

// HealthCheck 对后端履行 ping 操作,并更新状况 
func  HealthCheck { 
 for _, b := range s.backends { 
 status :=  up  
 alive := isBackendAlive 
 b.SetAlive 
 if !alive { 
 status =  down  
 } 
 log.Printf 
 } 
} 

咱们能够发动守时器来守时建议 ping 操作。

// healthCheck 回来一个 routine,每 2 分钟查看一次后端的状况 
func healthCheck { 
 t := time.NewTicker 
 for { 
 select { 
 case  -t.C: 
 log.Println 
 serverPool.HealthCheck 
 log.Println 
 } 
 } 
} 

在上面的比方中, -t.C 每 20 秒回来一个值,select 会检测到这个事情。在没有 default case 的状况下,select 会一向等候,直到有满意条件的 case 被履行。

最终,运用独自的 goroutine 来履行。

go healthCheck 

这篇文章提到了许多东西:

这个简略的负载均衡器还有许多能够改善的当地:

https://github.com/kasvith/simplelb/

热门文章

随机推荐

推荐文章