gRPC服務發現與負載平衡

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

1)簡介

gRPCServer Load Balancer的主要實現機制是外部Server Load Balancer,即通過外部負載平衡器來向用戶端提供更新後的伺服器列表。

gRPC用戶端也內建對少量幾種Server Load Balancer策略API的支援,其中包括grpclb策略(該策略實現了外部Server Load Balancer),但並不鼓勵使用者在gRPC中添加更多的策略。新的Server Load Balancer策略應該在外部負載平衡器中實現。

2)工作流程

Server Load Balancer策略在名稱解析和伺服器串連方面與gRPC用戶端相適應,以下是其工作方式:



1.首先,gRPC用戶端會發出對伺服器名稱解析的請求,該名稱會被解析為一個或多個IP地址,每一個地址會標明它是伺服器位址還是Server Load Balancer器地址,並且服務配置中會註明用戶端所選用的Server Load Balancer策略(例如,round_robin或grpclb)。

2.用戶端執行個體化Server Load Balancer策略。

註:如果位址解析器返回的任何一個地址是平衡器地址,用戶端將會使用grpclb策略,從而直接忽略在服務配置中註明的Server Load Balancer策略。如果沒有平衡器地址,用戶端將使用在服務配置中註明的Server Load Balancer策略。如果服務配置中沒有標明負載策略,那麼用戶端會預設為選擇第一個可用伺服器位址的策略。

3.Server Load Balancer策略會建立通向每一個伺服器位址的子通道。

l對於除了grpclb以外的其他策略,這意味著每一個解析後的地址都會有一個子通道。註:這些策略會忽略任何解析後的平衡器地址。

l對於grpclb策略,工作流程如下:

a)該策略建立一個通向平衡器地址的流。它會向平衡器請求用戶端最初請求伺服器名稱的伺服器位址。

註:目前grpclb策略會忽略所有的非平衡器解析地址。在未來,這一機制可能會改變,這些非平衡器解析地址會成為備選方案,以防沒有關聯到任何平衡器。

b)被Server Load Balancer器定向到用戶端的gRPC伺服器叢集會向Server Load Balancer器通報負載情況。

c)Server Load Balancer器會向gRPC用戶端的grpclb策略返回伺服器列表。然後grpclb策略會建立通向列表中伺服器的子通道。

4.對於每一次RPC發送,Server Load Balancer策略會決定RPC應該傳向哪個子通道。

對於grpclb策略,用戶端會以Server Load Balancer器返回的伺服器列表順序向伺服器發送請求,如果伺服器列表為空白,請求會阻塞直到回去非空伺服器列表。

3)其他負載平衡方式簡介

1、集中式LBProxy Model



在服務消費者和服務提供者之間有一個獨立的LB,通常是專門的硬體裝置如F5,或者基於軟體如LVS,HAproxy等實現。LB上有所有服務的地址映射表,通常由營運配置註冊,當服務消費方調用某個目標服務時,它向LB發起請求,由LB以某種策略,比如輪詢(Round-Robin)做負載平衡後將請求轉寄到目標服務。LB一般具備健全狀態檢查能力,能自動摘除不健康的服務執行個體。該方案主要問題:

1.單點問題,所有服務調用流量都經過LB,當服務數量和調用量大的時候,LB容易成為瓶頸,且一旦LB發生故障影響整個系統;

2.服務消費方、提供方之間增加了一級,有一定效能開銷。

2、進程內LBBalancing-awareClient)



針對第一個方案的不足,此方案將LB的功能整合到服務消費方進程裡,也被稱為軟負載或者用戶端負載方案。服務提供者啟動時,首先將服務地址註冊到服務註冊表,同時定期報心跳到服務註冊表以表明服務的存活狀態,相當於健全狀態檢查,服務消費方要訪問某個服務時,它通過內建的LB組件向服務註冊表查詢,同時緩衝並定期重新整理目標服務地址清單,然後以某種負載平衡策略選擇一個目標服務地址,最後向目標服務發起請求。LB和服務發現能力被分散到每一個服務消費者的進程內部,同時服務消費方和服務提供者之間是直接調用,沒有額外開銷,效能比較好。該方案主要問題:

1.開發成本,該方案將服務調用方整合到用戶端的進程裡頭,如果有多種不同的語言棧,就要配合開發多種不同的用戶端,有一定的研發和維護成本;

2.另外生產環境中,後續如果要對客戶庫進行升級,勢必要求服務調用方修改代碼並重新發布,升級較複雜。

4)gRPC結合ETCD負載平衡實現思路(僅供參考)

ETCD的介紹在文字ETCD實現技術總結中已經有了詳細的介紹,gRPC結合ETCD實現外部負載平衡主要分以下四步:

1.服務註冊

以服務名稱作為Key,一個或多個伺服器IP作為Value,並附帶租期Put入etcd叢集;

2.服務要求

gRPC用戶端向etcd服務端發送服務名;

3.服務響應

etcd通過Get介面擷取服務對應的伺服器叢集IP,通過伺服器負載報告資訊對伺服器叢集IP進行排序,將排序結果清單返回用戶端;

4.gRPC服務要求與響應

gRPC用戶端通過etcd服務端返回的IP列表串連gRPC服務端,實現gRPC服務。

5)基於進程內LB方案實現

根據gRPC官方提供的設計思路,基於進程內LB方案(即第2個案,阿里開源的服務架構 Dubbo 也是採用類似機制),結合分布式一致的組件(如Zookeeper、Consul、Etcd),可找到gRPC服務發現和負載平衡的可行解決方案。接下來以GO語言為例,簡單介紹下基於Etcd3的關鍵代碼實現:

1)命名解析實現:resolver.go

package etcdv3

import (

"errors"

"fmt"

"strings"

etcd3 "github.com/coreos/etcd/clientv3"

"google.golang.org/grpc/naming"

)

// resolver is the implementaion of grpc.naming.Resolver

type resolver struct {

serviceName string // service name to resolve

}

// NewResolver return resolver with service name

func NewResolver(serviceName string) *resolver {

return &resolver{serviceName: serviceName}

}

// Resolve to resolve the service from etcd, target is the dial address of etcd

// target example: "http://127.0.0.1:2379,http://127.0.0.1:12379,http://127.0.0.1:22379"

func (re *resolver) Resolve(target string) (naming.Watcher, error) {

if re.serviceName == "" {

return nil, errors.New("grpclb: no service name provided")

}

// generate etcd client

client, err := etcd3.New(etcd3.Config{

Endpoints: strings.Split(target, ","),

})

if err != nil {

return nil, fmt.Errorf("grpclb: creat etcd3 client failed: %s", err.Error())

}

// Return watcher

return &watcher{re: re, client: *client}, nil

}

2)服務發現實現:watcher.go

package etcdv3

import (

"errors"

"fmt"

"strings"

etcd3 "github.com/coreos/etcd/clientv3"

"google.golang.org/grpc/naming"

)

// resolver is the implementaion of grpc.naming.Resolver

type resolver struct {

serviceName string // service name to resolve

}

// NewResolver return resolver with service name

func NewResolver(serviceName string) *resolver {

return &resolver{serviceName: serviceName}

}

// Resolve to resolve the service from etcd, target is the dial address of etcd

// target example: "http://127.0.0.1:2379,http://127.0.0.1:12379,http://127.0.0.1:22379"

func (re *resolver) Resolve(target string) (naming.Watcher, error) {

if re.serviceName == "" {

return nil, errors.New("grpclb: no service name provided")

}

// generate etcd client

client, err := etcd3.New(etcd3.Config{

Endpoints: strings.Split(target, ","),

})

if err != nil {

return nil, fmt.Errorf("grpclb: creat etcd3 client failed: %s", err.Error())

}

// Return watcher

return &watcher{re: re, client: *client}, nil

}

3)服務註冊實現:register.go

package etcdv3

import (

"fmt"

"log"

"strings"

"time"

etcd3 "github.com/coreos/etcd/clientv3"

"golang.org/x/net/context"

"github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"

)

// Prefix should start and end with no slash

var Prefix = "etcd3_naming"

var client etcd3.Client

var serviceKey string

var stopSignal = make(chan bool, 1)

// Register

func Register(name string, host string, port int, target string, interval time.Duration, ttl int) error {

serviceValue := fmt.Sprintf("%s:%d", host, port)

serviceKey = fmt.Sprintf("/%s/%s/%s", Prefix, name, serviceValue)

// get endpoints for register dial address

var err error

client, err := etcd3.New(etcd3.Config{

Endpoints: strings.Split(target, ","),

})

if err != nil {

return fmt.Errorf("grpclb: create etcd3 client failed: %v", err)

}

go func() {

// invoke self-register with ticker

ticker := time.NewTicker(interval)

for {

// minimum lease TTL is ttl-second

resp, _ := client.Grant(context.TODO(), int64(ttl))

// should get first, if not exist, set it

_, err := client.Get(context.Background(), serviceKey)

if err != nil {

if err == rpctypes.ErrKeyNotFound {

if _, err := client.Put(context.TODO(), serviceKey, serviceValue, etcd3.WithLease(resp.ID)); err != nil {

log.Printf("grpclb: set service '%s' with ttl to etcd3 failed: %s", name, err.Error())

}

} else {

log.Printf("grpclb: service '%s' connect to etcd3 failed: %s", name, err.Error())

}

} else {

// refresh set to true for not notifying the watcher

if _, err := client.Put(context.Background(), serviceKey, serviceValue, etcd3.WithLease(resp.ID)); err != nil {

log.Printf("grpclb: refresh service '%s' with ttl to etcd3 failed: %s", name, err.Error())

}

}

select {

case <-stopSignal:

return

case <-ticker.C:

}

}

}()

return nil

}

// UnRegister delete registered service from etcd

func UnRegister() error {

stopSignal <- true

stopSignal = make(chan bool, 1) // just a hack to avoid multi UnRegister deadlock

var err error;

if _, err := client.Delete(context.Background(), serviceKey); err != nil {

log.Printf("grpclb: deregister '%s' failed: %s", serviceKey, err.Error())

} else {

log.Printf("grpclb: deregister '%s' ok.", serviceKey)

}

return err

}

4)介面描述檔案:helloworld.proto

syntax = "proto3";

option java_multiple_files = true;

option java_package = "com.midea.jr.test.grpc";

option java_outer_classname = "HelloWorldProto";

option objc_class_prefix = "HLW";

package helloworld;

// The greeting service definition.

service Greeter {

//  Sends a greeting

rpc SayHello (HelloRequest) returns (HelloReply) {

}

}

// The request message containing the user's name.

message HelloRequest {

string name = 1;

}

// The response message containing the greetings

message HelloReply {

string message = 1;

}

5)實現服務端介面:helloworldserver.go

package main

import (

"flag"

"fmt"

"log"

"net"

"os"

"os/signal"

"syscall"

"time"

"golang.org/x/net/context"

"google.golang.org/grpc"

grpclb "com.midea/jr/grpclb/naming/etcd/v3"

"com.midea/jr/grpclb/example/pb"

)

var (

serv = flag.String("service", "hello_service", "service name")

port = flag.Int("port", 50001, "listening port")

reg = flag.String("reg", "http://127.0.0.1:2379", "register etcd address")

)

func main() {

flag.Parse()

lis, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", *port))

if err != nil {

panic(err)

}

err = grpclb.Register(*serv, "127.0.0.1", *port, *reg, time.Second*10, 15)

if err != nil {

panic(err)

}

ch := make(chan os.Signal, 1)

signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT, syscall.SIGKILL, syscall.SIGHUP, syscall.SIGQUIT)

go func() {

s := <-ch

log.Printf("receive signal '%v'", s)

grpclb.UnRegister()

os.Exit(1)

}()

log.Printf("starting hello service at %d", *port)

s := grpc.NewServer()

pb.RegisterGreeterServer(s, &server{})

s.Serve(lis)

}

// server is used to implement helloworld.GreeterServer.

type server struct{}

// SayHello implements helloworld.GreeterServer

func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {

fmt.Printf("%v: Receive is %s\n", time.Now(), in.Name)

return &pb.HelloReply{Message: "Hello " + in.Name}, nil

}

6)實現用戶端介面:helloworldclient.go

package main

import (

"flag"

"fmt"

"time"

grpclb "com.midea/jr/grpclb/naming/etcd/v3"

"com.midea/jr/grpclb/example/pb"

"golang.org/x/net/context"

"google.golang.org/grpc"

"strconv"

)

var (

serv = flag.String("service", "hello_service", "service name")

reg = flag.String("reg", "http://127.0.0.1:2379", "register etcd address")

)

func main() {

flag.Parse()

r := grpclb.NewResolver(*serv)

b := grpc.RoundRobin(r)

ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)

conn, err := grpc.DialContext(ctx, *reg, grpc.WithInsecure(), grpc.WithBalancer(b))

if err != nil {

panic(err)

}

ticker := time.NewTicker(1 * time.Second)

for t := range ticker.C {

client := pb.NewGreeterClient(conn)

resp, err := client.SayHello(context.Background(), &pb.HelloRequest{Name: "world " + strconv.Itoa(t.Second())})

if err == nil {

fmt.Printf("%v: Reply is %s\n", t, resp.Message)

}

}

}

7)運行測試

1.運行3個服務端S1、S2、S3,1個用戶端C,觀察各服務端接收的請求數是否相等?



2.關閉1個服務端S1,觀察請求是否會轉移到另外2個服務端?




3.重新啟動S1服務端,觀察另外2個服務端請求是否會平均分配到S1?



4.關閉Etcd3伺服器,觀察用戶端與服務端通訊是否正常?

關閉通訊仍然正常,但新服務端不會註冊進來,服務端掉線了也無法摘除掉。

5.重新啟動Etcd3伺服器,服務端上下線可自動回復正常。

6.關閉所有服務端,用戶端請求將被阻塞。

參考:

http://www.grpc.io/docs/

https://github.com/grpc/grpc/blob/master/doc/load-balancing.md

相關關鍵詞:
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.