透過Nginx實現gRPC服務的負載均衡 | gRPC雙向資料流的互動

daqianmen發表於2021-09-09

第一步:下載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;
    }
}

第三步:把 一文中的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 mainimport (    "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 服務埠 stringfunc 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資料流服務,至此大功告成!



作者:阿狸不歌
連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/1727/viewspace-2819642/,如需轉載,請註明出處,否則將追究法律責任。

相關文章