通過Nginx實現gRPC服務的負載均衡 | gRPC雙向資料流的互動控制系列(3)

阿狸不歌發表於2018-10-22

前情提要

本系列的第一篇文章 通過一個例子介紹了go語言實現gRPC雙向資料流的互動控制,第二篇文章介紹瞭如何通過Websocket與gRPC互動。通過這兩篇文章,我們可以一窺gRPC雙向資料流的開發方式,但是在生產環境當中一臺伺服器(一個服務端程式)是不夠的,我們往往會面臨各種複雜情況:訪問量上來了一臺伺服器不夠怎麼辦?伺服器掛了怎麼辦?有實戰經驗的讀者肯定知道答案:上負載均衡(Load Balancing)啊!

gRPC服務如何做負載均衡?

gRPC官方部落格上有一篇文章《gRPC Load Balancing》(https://grpc.io/blog/loadbalancing),詳細介紹了幾種方案,並分析了幾種方案各自的優劣。並附了一張解決方案表: gRPC負載均衡解決方案表

在gRPC的Github上還有一篇文章叫《Load Balancing in gRPC》(https://github.com/grpc/grpc/blob/master/doc/load-balancing.md),如果英文看著費勁可以看一篇中文的《gRPC服務發現&負載均衡》(https://segmentfault.com/a/1190000008672912)。


測試Nginx對gRPC服務的支援

因為上面幾篇文章介紹的很詳細了,所以本文不再展開討論。我們可以注意到上表中被紅框圈起來的部分寫著“Nginx coming soon”,現在這個Nginx的解決方案已經來了——2018年3月17日,Nginx官方宣佈nginx 1.13.10支援gRPC (https://www.nginx.com/blog/nginx-1-13-10-grpc/)

第一步:下載nginx最新的stable版(本文發稿時是1.14.0,如果會用docker的也可以下載其alpine版本)。 第二步:配置nginx的config檔案如下

server {
# ⚠️ nginx的監聽埠按你的實際情況設定
listen  80     http2;
access_log /var/log/nginx/access.log main;
location / {
        # ⚠️ 把下面的 grpc://127.0.0.1:3000換成你自己的grpc伺服器地址
        grpc_pass grpc://127.0.0.1:3000;
    }
}

第三步:把go語言實現gRPC雙向資料流的互動控制 一文中的client.go 中的服務端地址改為nginx服務的地址(比如:127.0.0.1:80)

第四步: (1)執行server.go (2)執行nginx服務 (3)執行client.go

如果沒什麼意外,gRPC客戶端發出的訊息可以通過nginx後被gRPC服務端收到。 nginx日誌

我們可以通過nginx日誌觀察到相應的資訊。

一個小坑

上述連線雖然已經實現,但是如果我們的客戶端有連續一分鐘沒有輸入資訊,會出現接收資訊出錯的情況。 連線被nginx斷開

這種情形在沒有使用nginx的時候不會出現,由於以前使用nginx給websocket做反向代理時也出現過類似情況,故而推斷是nginx對超過一段時間的連線進行了斷開。

新增心跳

解決上述問題可以採取的一個方法是增加心跳(如果您發現了什麼別的好辦法可以解決這個問題,比如在nginx裡配置一些引數,請留言告訴我?

client.go

新增一段隔40秒傳送心跳的程式碼

package main
import (
    "bufio"
    "context"
    "flag"
    "io"
    "log"
    "os"
    "time"
    "google.golang.org/grpc"
    proto "chat" // 根據proto檔案自動生成的程式碼
)
var 伺服器地址 string
func init() {
    flag.StringVar(&伺服器地址, "server", "127.0.0.1:80", "伺服器地址")
}
func main() {
    // 建立連線
    conn, err := grpc.Dial(伺服器地址, grpc.WithInsecure())
    if err != nil {
        log.Printf("連線失敗: [%v]\n", err)
        return
    }
    defer conn.Close()
    client := proto.NewChatClient(conn)
    // 宣告 context
    ctx := context.Background()
    // 建立雙向資料流
    stream, err := client.BidStream(ctx)
    if err != nil {
        log.Printf("建立資料流失敗: [%v]\n", err)
        return
    }
    // 啟動一個 goroutine 接收命令列輸入的指令
    go func() {
        log.Println("請輸入訊息...")
        輸入 := bufio.NewReader(os.Stdin)
        for {
            // 獲取 命令列輸入的字串, 以回車 \n 作為結束標誌
            命令列輸入的字串, _ := 輸入.ReadString('\n')
            // 向服務端傳送 指令
            if err := stream.Send(&proto.Request{Input: 命令列輸入的字串}); err != nil {
                return
            }
        }
    }()
    //⚠️ 新新增的部分: 啟動一個 goroutine 每隔40秒傳送心跳包
    go func() {
        for {
            // 每隔 40 秒傳送一次
            time.Sleep(40 * time.Second)
            log.Println("傳送心跳包")
            // 心跳字元用"\n"
            if err := stream.Send(&proto.Request{Input: "\n"}); err != nil {
                return
            }
        }
    }()
    for {
        // 接收從 服務端返回的資料流
        響應, err := stream.Recv()
        if err == io.EOF {
            log.Println("⚠️ 收到服務端的結束訊號")
            break
        }
        if err != nil {
            // TODO: 處理接收錯誤
            log.Println("接收資料出錯:", err)
            break
        }
        log.Printf("[客戶端收到]: %s", 響應.Output)
    }
}

server.go

新增一段檢測心跳的程式碼

package main
import (
    "flag"
    "io"
    "log"
    "net"
    "strconv"
    "google.golang.org/grpc"
    proto "chat" // 根據proto檔案自動生成的程式碼
)
// Streamer 服務端
type Streamer struct{}
// BidStream 實現了 ChatServer 介面中定義的 BidStream 方法
func (s *Streamer) BidStream(stream proto.Chat_BidStreamServer) error {
    ctx := stream.Context()
    for {
        select {
        case <-ctx.Done():
            log.Println("收到客戶端通過context發出的終止訊號")
            return ctx.Err()
        default:
            // 接收從客戶端發來的訊息
            輸入, err := stream.Recv()
            if err == io.EOF {
                log.Println("客戶端傳送的資料流結束")
                return nil
            }
            if err != nil {
                log.Println("接收資料出錯:", err)
                return err
            }
            // 如果接收正常,則根據接收到的 字串 執行相應的指令
            switch 輸入.Input {
            case "結束對話\n", "結束對話":
                log.Println("收到'結束對話'指令")
                if err := stream.Send(&proto.Response{Output: "收到結束指令"}); err != nil {
                    return err
                }
                // 收到結束指令時,通過 return nil 終止雙向資料流
                return nil
            case "返回資料流\n", "返回資料流":
                log.Println("收到'返回資料流'指令")
                // 收到 收到'返回資料流'指令, 連續返回 10 條資料
                for i := 0; i < 10; i++ {
                    if err := stream.Send(&proto.Response{Output: "資料流 #" + strconv.Itoa(i)}); err != nil     {
                        return err
                    }
                }
            // ⚠️ 攔截心跳字元"\n"
            case "\n":
                log.Println("收到心跳包")
                // 只接收心跳不回發資料也可以
            default:
                // 預設情況下, 返回 '服務端返回: ' + 輸入資訊
                log.Printf("[收到訊息]: %s", 輸入.Input)
                if err := stream.Send(&proto.Response{Output: "服務端返回: " + 輸入.Input}); err != nil {
                    return err
                }
            }
        }
    }
}
var 服務埠 string
func init() {
    flag.StringVar(&服務埠, "port", "3000", "服務埠")
}
func main() {
    log.Println("啟動服務端...")
    server := grpc.NewServer()
    // 註冊 ChatServer
    proto.RegisterChatServer(server, &Streamer{})
    address, err := net.Listen("tcp", ":"+服務埠)
    if err != nil {
        panic(err)
    }
    if err := server.Serve(address); err != nil {
        panic(err)
    }
}

新增完成後再度測試,連線不會再被nginx打斷。


Nginx實現服務端負載均衡的配置檔案

心跳的坑趟過去之後,剩下的其實就簡單了,我們修改nginx的配置檔案:

upstream backend {
    # ⚠️ 把下面的服務端地址和埠改成你自己的
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;
} 
server {
    listen  80     http2;
    access_log /var/log/nginx/access.log main;
    location / {
        grpc_pass grpc://backend;
    }
}

按如下順序啟動 (1)執行多個 server.go ,按照nginx配置檔案輸入埠引數(如 server.go -port 3001)

(2)執行nginx服務

(3)執行多個**client.go, (也可以執行websocket的那個程式,記得把心跳程式碼加上**,多開幾個瀏覽器視窗)

我們可以觀察到開啟的多個server都在進行gRPC資料流服務,至此大功告成?!


總結

gRPC服務端的負載均衡有很多種方案,也各有優劣,但是用Nginx似乎是最簡單的一種。總之,我們還得根據具體的業務場景來選擇具體的實現方案。


gRPC雙向資料流系列

(之一): gRPC雙向資料流的互動控制

(之二): 通過Websocket與gRPC互動

相關文章