├── go.sum ├── go.mod ├── backend └── myapp.go ├── main.go ├── util.go ├── readme.md ├── .gitignore ├── slide.md ├── server.go └── server_pool.go /go.sum: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module loadbalancer 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /backend/myapp.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | func main() { 10 | 11 | // 1. 通过 flag 传参端口 12 | var serverPort string 13 | flag.StringVar(&serverPort, "port", "3000", "-port=6000 指定端口") 14 | flag.Parse() 15 | 16 | // 2. 返回内容,包含端口,用以辨别请求的是哪台服务器 17 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 18 | fmt.Fprintln(w, "Hi from Server: ", serverPort) 19 | }) 20 | 21 | // 3. 启动服务 22 | http.ListenAndServe(":"+serverPort, nil) 23 | } 24 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | ) 7 | 8 | // 配置信息 9 | var ( 10 | serverList = []string{ 11 | "http://127.0.0.1:6000", 12 | "http://127.0.0.1:6001", 13 | "http://127.0.0.1:6002", 14 | "http://127.0.0.1:6003", 15 | "http://127.0.0.1:6004", 16 | } 17 | port = "8000" 18 | ) 19 | 20 | func main() { 21 | 22 | // 1. 初始化连接池 23 | serverPool := NewServerPool(serverList) 24 | 25 | // 2. 转发请求 26 | http.HandleFunc("/", serverPool.ForwardRequest) 27 | 28 | // 3. 启动服务 29 | log.Printf("Load Balancer running at http://localhost:%v", port) 30 | if err := http.ListenAndServe(":"+port, nil); err != nil { 31 | log.Fatal(err) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "net/http" 4 | 5 | // ContextKey 类型作为 r.Context().Value 的 KEY 6 | type ContextKey string 7 | 8 | const ( 9 | AttemptsKey ContextKey = "attempts" 10 | RetriesKey ContextKey = "retries" 11 | ) 12 | 13 | // GetAttemptsFromContext 从 http.Request.Context 中读取 Attempts 14 | func GetAttemptsFromContext(r *http.Request) int { 15 | return getIntFromContext(r, AttemptsKey, 1) 16 | } 17 | 18 | // GetRetriesFromContext 从 http.Request.Context 中读取 Retries 19 | func GetRetriesFromContext(r *http.Request) int { 20 | return getIntFromContext(r, RetriesKey, 0) 21 | } 22 | 23 | func getIntFromContext(r *http.Request, key ContextKey, defaultValue int) int { 24 | if value, ok := r.Context().Value(key).(int); ok { 25 | return value 26 | } 27 | return defaultValue 28 | } 29 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## 说明 3 | 4 | 编写简单的负载均衡器,功能: 5 | 6 | - 轮询分发 7 | - 失败重试 8 | - 后端可用性检测 9 | 10 | 这个代码仓库包含两个项目,一个是后端服务项目,可以通过设置端口来启动多个服务器,模拟负载均衡器后端的多台服务器。第二个是 main.go 里的负载均衡器。 11 | 12 | ## 视频链接 13 | 14 | - [013. 负载均衡器第一部分:从零开始构建负载均衡器](https://learnku.com/courses/go-video/2022/building-load-balancers-from-scratch/11667) 15 | - [014. 负载均衡器第二部分:可用服务器监测](https://learnku.com/courses/go-video/2022/available-server-monitoring/11668) 16 | 17 | ## 运行代码 18 | 19 | ``` 20 | go run . 21 | ``` 22 | 23 | ## 后端服务 24 | 25 | 启动后端服务: 26 | 27 | ``` 28 | go run backend/myapp.go -port=6000 > /dev/null 2>&1 & 29 | go run backend/myapp.go -port=6001 > /dev/null 2>&1 & 30 | go run backend/myapp.go -port=6002 > /dev/null 2>&1 & 31 | go run backend/myapp.go -port=6003 > /dev/null 2>&1 & 32 | go run backend/myapp.go -port=6004 > /dev/null 2>&1 & 33 | ``` 34 | 35 | 停止所有后端服务: 36 | 37 | ``` 38 | kill -9 $(lsof -t -i:6000,6001,6002,6003,6004 -sTCP:LISTEN) 39 | ``` 40 | 41 | 停止单个后端: 42 | 43 | ``` 44 | kill -9 $(lsof -t -i:6004 -sTCP:LISTEN) 45 | ``` 46 | 47 | 批量发送 CURL 请求: 48 | 49 | ``` 50 | for n in {1..10}; do curl http://localhost:8000; done 51 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Golang # 2 | ###################### 3 | # `go test -c` 生成的二进制文件 4 | *.test 5 | # go coverage 工具 6 | *.out 7 | *.prof 8 | *.cgo1.go 9 | *.cgo2.c 10 | _cgo_defun.c 11 | _cgo_gotypes.go 12 | _cgo_export.* 13 | 14 | # 编译文件 # 15 | ################### 16 | *.com 17 | *.class 18 | *.dll 19 | *.exe 20 | *.o 21 | *.so 22 | 23 | # 压缩包 # 24 | ############ 25 | # Git 自带压缩,如果这些压缩包里有代码,建议解压后 commit 26 | *.7z 27 | *.dmg 28 | *.gz 29 | *.iso 30 | *.jar 31 | *.rar 32 | *.tar 33 | *.zip 34 | 35 | # 日志文件和数据库 # 36 | ###################### 37 | *.log 38 | *.sqlite 39 | *.db 40 | 41 | # 零时文件 # 42 | ###################### 43 | tmp/ 44 | .tmp/ 45 | 46 | # 系统生成文件 # 47 | ###################### 48 | .DS_Store 49 | .DS_Store? 50 | .AppleDouble 51 | .LSOverride 52 | ._* 53 | .Spotlight-V100 54 | .Trashes 55 | ehthumbs.db 56 | Thumbs.db 57 | .TemporaryItems 58 | .fseventsd 59 | .VolumeIcon.icns 60 | .com.apple.timemachine.donotpresent 61 | 62 | # IDE 和编辑器 # 63 | ###################### 64 | .idea/ 65 | /go_build_* 66 | out/ 67 | .vscode/ 68 | .vscode/settings.json 69 | *.sublime* 70 | __debug_bin 71 | .project 72 | 73 | # 前端工具链 # 74 | ###################### 75 | .sass-cache/* 76 | node_modules/ 77 | 78 | -------------------------------------------------------------------------------- /slide.md: -------------------------------------------------------------------------------- 1 | --- 2 | theme: gaia 3 | _class: lead 4 | paginate: true 5 | backgroundColor: #fff 6 | marp: true 7 | slide_tool: https://github.com/marp-team/marp-core/tree/main/themes 8 | --- 9 | 10 | # **什么是负载均衡** 11 | 12 | Nginx 常用的几种算法 13 | 14 | --- 15 | 16 | ![](https://cdn.learnku.com/uploads/images/202112/10/1/WNnsme2lgL.png!large) 17 | 18 | --- 19 | 20 | #### 1. 轮询(round-robin) 21 | 22 | 平均分配,如某台服务器不可用,能自动剔除。 23 | 24 | ``` 25 | http { 26 | upstream myapp1 { 27 | server 192.168.0.11; 28 | server 192.168.0.12; 29 | server 192.168.0.13; 30 | } 31 | server { 32 | listen 80; 33 | location / { 34 | proxy_pass http://myapp1; 35 | } 36 | } 37 | } 38 | ``` 39 | 40 | --- 41 | 42 | #### 2. 加权(weight) 43 | 44 | 指定轮询几率,weight 和访问比率成正比,用于后端服务器性能不均的情况。 45 | 46 | ``` 47 | upstream backserver { 48 | server 192.168.0.11 weight=2; 49 | server 192.168.0.12 weight=6; 50 | server 192.168.0.13 weight=2; 51 | } 52 | ``` 53 | 54 | --- 55 | 56 | #### 3. ip_hash 57 | 58 | 根据 IP 选择服务器,某个 IP 永远访问固定的某台服务器: 59 | 60 | ``` 61 | upstream myapp1 { 62 | ip_hash 63 | server 192.168.0.11; 64 | server 192.168.0.12; 65 | server 192.168.0.13; 66 | } 67 | ``` 68 | 69 | --- 70 | 71 | ## 我们的负载均衡器 72 | 73 | 功能: 74 | 75 | - 轮询模式 76 | - 可用性检测 77 | - 线程安全 78 | 79 | 80 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net/http" 7 | "net/http/httputil" 8 | "net/url" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | // Server 每一个 Server 对应一个后端服务 URL 14 | type Server struct { 15 | URL *url.URL 16 | ReverseProxy *httputil.ReverseProxy 17 | ServerPool *ServerPool 18 | 19 | // 是否可用的标示,false 为服务器不可用 20 | Alive bool 21 | 22 | // 读写锁,RWMutex 基于 Mutex 实现 23 | // RWMutex 是单写多读锁,该锁可以加多个读锁或者一个写锁 24 | // 读锁占用的情况下会阻止写,不会阻止读,多个 goroutine 可以同时获取读锁 25 | // 写锁会阻止其他 goroutine(无论读和写)进来,整个锁由该 goroutine 独占 26 | // 适用于读多写少的场景 27 | Mux sync.RWMutex 28 | } 29 | 30 | // NewServer 通过 URL 来初始化一个后端服务 31 | func NewServer(urlStr string, serverPool *ServerPool) *Server { 32 | 33 | // 1. 解析 URL 34 | url, _ := url.Parse(urlStr) 35 | 36 | // 2. 初始化后端服务 37 | server := &Server{ 38 | URL: url, 39 | Alive: true, 40 | ServerPool: serverPool, 41 | } 42 | 43 | // 3. 使用 httputil 包初始化后反向代理 44 | server.ReverseProxy = httputil.NewSingleHostReverseProxy(server.URL) 45 | 46 | server.ReverseProxy.ErrorHandler = server.ProxyErrorHandler 47 | 48 | return server 49 | } 50 | 51 | // SetAlive 标记可用性 52 | func (server *Server) SetAlive(alive bool) { 53 | server.Mux.Lock() 54 | server.Alive = alive 55 | server.Mux.Unlock() 56 | } 57 | 58 | // IsAlive 返回可用标示 59 | func (server *Server) IsAlive() (alive bool) { 60 | server.Mux.RLock() 61 | alive = server.Alive 62 | server.Mux.RUnlock() 63 | return 64 | } 65 | 66 | func (server *Server) ProxyErrorHandler(writer http.ResponseWriter, request *http.Request, e error) { 67 | 68 | // 1. 再尝试两次,杜绝暂时性不可用的情况 69 | log.Printf("Proxy Error:[%s], Error %s\n", server.URL, e.Error()) 70 | retries := GetRetriesFromContext(request) 71 | if retries < 3 { 72 | log.Printf("Retry [%s] for %d times\n", server.URL, retries) 73 | 74 | // 休息 10 毫秒(千分之一秒),给后端一点点恢复的时间 75 | time.Sleep(10 * time.Millisecond) 76 | ctx := context.WithValue(request.Context(), RetriesKey, retries+1) 77 | server.ReverseProxy.ServeHTTP(writer, request.WithContext(ctx)) 78 | 79 | return 80 | } 81 | 82 | // 2. 如果还出现错误,就设置为服务器不可用 83 | server.SetAlive(false) 84 | 85 | // 3. 尝试不同的后端服务 86 | ctx := context.WithValue(request.Context(), RetriesKey, 1) 87 | server.ServerPool.AttemptNextServer(writer, request.WithContext(ctx)) 88 | } 89 | 90 | // ReachableCheck 检测后端服务是否可用 91 | func (server *Server) ReachableCheck() bool { 92 | 93 | // 1. 设置过期时间并发送请求,2 秒足够了 94 | client := http.Client{ 95 | Timeout: 2 * time.Second, 96 | } 97 | // Head 方法只获取响应的 header,加快传输速度 98 | resp, err := client.Head(server.URL.String()) 99 | 100 | // 2. 出错了就设置为 false 101 | if err != nil || resp.StatusCode != http.StatusOK { 102 | server.SetAlive(false) 103 | return false 104 | } 105 | 106 | // 3. 请求成功 107 | server.SetAlive(true) 108 | return true 109 | } 110 | -------------------------------------------------------------------------------- /server_pool.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "sync/atomic" 9 | "time" 10 | ) 11 | 12 | // ServerPool 连接池 13 | type ServerPool struct { 14 | Backends []*Server 15 | Current uint64 16 | } 17 | 18 | func NewServerPool(servers []string) *ServerPool { 19 | 20 | // 1. 初始化 21 | serverPool := &ServerPool{ 22 | // 初始化 Backends 的读取 index,从 0 开始 23 | Current: 0, 24 | } 25 | 26 | // 2. 遍历创建所有 Server 实例 27 | for _, serverString := range servers { 28 | server := NewServer(serverString, serverPool) 29 | serverPool.Backends = append(serverPool.Backends, server) 30 | } 31 | 32 | // 3. 后端服务的健康检测 33 | go serverPool.StartHealthCheck() 34 | 35 | // 3. 返回 36 | return serverPool 37 | } 38 | 39 | // ForwardRequest 将请求迭代给连接池里的某个 40 | func (serverPool *ServerPool) ForwardRequest(writer http.ResponseWriter, request *http.Request) { 41 | 42 | attempts := GetAttemptsFromContext(request) 43 | if attempts > 3 { 44 | log.Printf("%s(%s) Max attempts reached, terminating\n", request.RemoteAddr, request.URL.Path) 45 | http.Error(writer, "Service not available", http.StatusServiceUnavailable) 46 | return 47 | } 48 | 49 | // 1. 获取下一个请求 50 | peer := serverPool.GetNextPeer() 51 | if peer != nil { 52 | peer.ReverseProxy.ServeHTTP(writer, request) 53 | log.Printf("Forward Request to %s, Path is %s\n", peer.URL, request.URL.Path) 54 | return 55 | } 56 | http.Error(writer, "No alive peer available", http.StatusServiceUnavailable) 57 | } 58 | 59 | // GetNextPeer 从连接池里取下一个连接,支持原子性 60 | func (serverPool *ServerPool) GetNextPeer() *Server { 61 | len := len(serverPool.Backends) 62 | nextIdx := int(atomic.AddUint64(&serverPool.Current, uint64(1)) % uint64(len)) 63 | 64 | // index 加 len 可以循环整个 Backends 数组 65 | loopCounter := nextIdx + len 66 | for i := nextIdx; i < loopCounter; i++ { 67 | 68 | // 处理 nextIdx = 4 , len = 5, i = 6 的情况 69 | usedIdx := i % len 70 | if serverPool.Backends[usedIdx].IsAlive() { 71 | // 只有 nextIdx 不可用时,才需要更新 serverPool.Current 的值 72 | if i != nextIdx { 73 | atomic.StoreUint64(&serverPool.Current, uint64(usedIdx)) 74 | } 75 | return serverPool.Backends[usedIdx] 76 | } 77 | } 78 | 79 | return nil 80 | } 81 | 82 | // AttemptNextServer 针对同一个请求尝试不同的后端服务,发生在服务不可用的情况 83 | func (serverPool *ServerPool) AttemptNextServer(writer http.ResponseWriter, request *http.Request) { 84 | 85 | attempts := GetAttemptsFromContext(request) 86 | fmt.Printf("\nAttempting %s(%s) , times: %d\n\n", request.RemoteAddr, request.URL.Path, attempts) 87 | ctx := context.WithValue(request.Context(), AttemptsKey, attempts+1) 88 | 89 | serverPool.ForwardRequest(writer, request.WithContext(ctx)) 90 | } 91 | 92 | // StartHealthCheck 遍历检测所有服务 93 | func (serverPool *ServerPool) StartHealthCheck() { 94 | 95 | // 每隔 5 秒钟检测所有后端服务的可用性 96 | for range time.Tick(time.Second * 5) { 97 | log.Println("Starting health check...") 98 | for _, backend := range serverPool.Backends { 99 | status := "up" 100 | 101 | // ReachableCheck 已经设置了请求超时为 2 秒 102 | alive := backend.ReachableCheck() 103 | if !alive { 104 | status = "down" 105 | } 106 | log.Printf("[%s] is [%s]\n", backend.URL, status) 107 | } 108 | log.Println("Health check completed") 109 | } 110 | } 111 | --------------------------------------------------------------------------------