Go微服務框架go-kratos實戰04:kratos中服務註冊和服務發現的使用

九卷 發表於 2022-06-04
框架 微服務 Go

一、簡介

關於服務註冊和服務發現介紹,我前面的文章有介紹過 - 服務註冊和發現的文章

作為服務中心的軟體有很多,比如 etcd,consul,nacos,zookeeper 等都可以作為服務中心。

go-kratos 把這些服務中心的功能作為外掛,整合進了 kratos 中。

下面就用 etcd 作為服務中心來說說 kratos 裡服務註冊和服務發現功能的使用。

image-20220603213155270

二、服務註冊和服務發現

2.1 介面定義

從 go-kratos 服務註冊和發現文件中,我們知道它的介面定義非常簡單:

註冊和反註冊服務:

type Registrar interface {
    // 註冊例項
    Register(ctx context.Context, service *ServiceInstance) error
    // 反註冊例項
    Deregister(ctx context.Context, service *ServiceInstance) error
}

獲取服務:

type Discovery interface {
    // 根據 serviceName 直接拉取例項列表
    GetService(ctx context.Context, serviceName string) ([]*ServiceInstance, error)
    // 根據 serviceName 阻塞式訂閱一個服務的例項列表資訊
    Watch(ctx context.Context, serviceName string) (Watcher, error)
}

2.2 簡單使用

服務端註冊服務

使用 etcd 作為服務中心。

1.新建 etcd連線client, etcdregitry.New(client)

2.把 regitry傳入 kratos.Registrar(r)

3.傳入服務名稱 kratos.Name("helloworld")

看官方的示例程式碼,server/main.go

package main

import (
	"context"
	"fmt"
	"log"

	etcdregitry "github.com/go-kratos/kratos/contrib/registry/etcd/v2"
	"github.com/go-kratos/kratos/v2"
	"github.com/go-kratos/kratos/v2/middleware/recovery"
	"github.com/go-kratos/kratos/v2/transport/grpc"
	"github.com/go-kratos/kratos/v2/transport/http"

	pb "github.com/go-kratos/examples/helloworld/helloworld"
	etcdclient "go.etcd.io/etcd/client/v3"
)

type server struct {
	pb.UnimplementedGreeterServer
}

func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
	return &pb.HelloReply{Message: fmt.Sprintf("welcome %+v!", in.Name)}, nil
}

func main() {
	// 建立 etcd client 連線
	client, err := etcdclient.New(etcdclient.Config{
		Endpoints: []string{"127.0.0.1:2379"},
	})
	if err != nil {
		log.Fatal(err)
	}

	// 初始化 http server
	httpSrv := http.NewServer(
		http.Address(":8080"),
		http.Middleware(
			recovery.Recovery(),
		),
	)

	// 初始化 grpc server
	grpcSrv := grpc.NewServer(
		grpc.Address(":9000"),
		grpc.Middleware(
			recovery.Recovery(),
		),
	)

	// 在伺服器上註冊服務
	s := &server{}
	pb.RegisterGreeterServer(grpcSrv, s)
	pb.RegisterGreeterHTTPServer(httpSrv, s)

	// 建立一個 registry 物件,就是對 ectd client 操作的一個包裝
	r := etcdregitry.New(client)

	app := kratos.New(
		kratos.Name("helloworld"), // 服務名稱
		kratos.Server(
			httpSrv,
			grpcSrv,
		),
		kratos.Registrar(r), // 填入etcd連線(etcd作為服務中心)
	)
	if err := app.Run(); err != nil {
		log.Fatal(err)
	}
}

etcd作為服務中心的使用步驟圖解:
image-20220604032611364

客戶端獲取服務

客戶端的服務發現,主要也是 3 個步驟.

1.新建 etcd連線, 傳入到 etcdregitry.New(client)

2.將 registry 傳入 WithDiscovery(r)

3.獲取服務WithEndpoint("discovery:///helloworld")

步驟與服務沒有多大區別。

官方的示例程式碼,client/main.go

package main

import (
	"context"
	"log"
	"time"

	"github.com/go-kratos/examples/helloworld/helloworld"
	etcdregitry "github.com/go-kratos/kratos/contrib/registry/etcd/v2"
	"github.com/go-kratos/kratos/v2/transport/grpc"
	"github.com/go-kratos/kratos/v2/transport/http"
	etcdclient "go.etcd.io/etcd/client/v3"
	srcgrpc "google.golang.org/grpc"
)

func main() {
	client, err := etcdclient.New(etcdclient.Config{
		Endpoints: []string{"127.0.0.1:2379"},
	})
	if err != nil {
		log.Fatal(err)
	}

	r := etcdregitry.New(client) // 傳入 etcd client,也就是選擇 etcd 為服務中心

	connGRPC, err := grpc.DialInsecure(
		context.Background(),
		grpc.WithEndpoint("discovery:///helloworld"), // 服務發現
		grpc.WithDiscovery(r),                        // 傳入etcd registry
	)
	if err != nil {
		log.Fatal(err)
	}
	defer connGRPC.Close()

	connHTTP, err := http.NewClient(
		context.Background(),
		http.WithEndpoint("discovery:///helloworld"),
		http.WithDiscovery(r),
		http.WithBlock(),
	)
	if err != nil {
		log.Fatal(err)
	}
	defer connHTTP.Close()

	for {
		callHTTP(connHTTP)
		callGRPC(connGRPC)
		time.Sleep(time.Second)
	}
}

func callHTTP(conn *http.Client) {
	client := helloworld.NewGreeterHTTPClient(conn)
	reply, err := client.SayHello(context.Background(), &helloworld.HelloRequest{Name: "go-kratos"})
	if err != nil {
		log.Fatal(err)
	}
	log.Printf("[http] SayHello %+v\n", reply)
}

func callGRPC(conn *srcgrpc.ClientConn) {
	client := helloworld.NewGreeterClient(conn)
	reply, err := client.SayHello(context.Background(), &helloworld.HelloRequest{Name: "go-kratos"})
	if err != nil {
		log.Fatal(err)
	}
	log.Printf("[grpc] SayHello %+v\n", reply)
}

執行程式

1.執行etcd,沒有安裝etcd的請自行百度或gg安裝

2.執行服務端

$ cd ./etcd/server
$ go run ./main.go
INFO msg=[HTTP] server listening on: [::]:8080
INFO msg=[gRPC] server listening on: [::]:9000

3.執行客戶端

$ cd ./client
$ go run .\main.go
INFO msg=[resolver] update instances: [{"id":"8fc08b88-e37b-11ec-bb6f-88d7f62323b4","name":"helloworld","version":"","metadata":null,"endpoints":["http://192.168.56.1:8080","grpc://192.168.56.1:9000"]}]
2022/06/04 04:28:21 [http] SayHello message:"welcome go-kratos!"
2022/06/04 04:28:21 [grpc] SayHello message:"welcome go-kratos!"
INFO msg=[resolver] update instances: [{"id":"8fc08b88-e37b-11ec-bb6f-88d7f62323b4","name":"helloworld","version":"","metadata":null,"endpoints":["http://192.168.56.1:8080","grpc://192.168.56.1:9000"]}]
2022/06/04 04:28:22 [http] SayHello message:"welcome go-kratos!"
2022/06/04 04:28:22 [grpc] SayHello message:"welcome go-kratos!"
2022/06/04 04:28:23 [http] SayHello message:"welcome go-kratos!"
2022/06/04 04:28:23 [grpc] SayHello message:"welcome go-kratos!"
2022/06/04 04:28:24 [http] SayHello message:"welcome go-kratos!"
2022/06/04 04:28:24 [grpc] SayHello message:"welcome go-kratos!"

... ...

程式執行成功

看看 etcd 執行日誌:

2022-06-04 04:26:03.896230 W | wal: sync duration of 1.1565369s, expected less than 1s
2022-06-04 04:26:03.991356 N | embed: serving insecure client requests on 127.0.0.1:2379, this is strongly discouraged!
2022-06-04 04:27:18.187663 W | etcdserver: request "header:<ID:7587862969930594823 > put:<key:\"/microservices/helloworld/8fc08b88-e37b-11ec-bb6f-88d7f62323b4\" value_size:162 lease:7587862969930594821 >" with result "size:4" took too long (113.4545ms) to execute

2.3 簡析服務註冊程式

一圖解千言:

image-20220604043508393

  • etcdregitry.New(client)

    這裡是對 etcd client 的包裝處理,那麼選擇的服務中心就是 etcd。也可以使用consul,zookeeper 等,kratos 對它們都有封裝。

// 對 etcd client 的包裝在處理
r := etcdregitry.New(client)

// https://github.com/go-kratos/kratos/contrib/registry/etcd/registry.go#L56
// New creates etcd registry
func New(client *clientv3.Client, opts ...Option) (r *Registry) {
	op := &options{
		ctx:       context.Background(),
		namespace: "/microservices",
		ttl:       time.Second * 15,
		maxRetry:  5,
	}
	for _, o := range opts {
		o(op)
	}
	return &Registry{
		opts:   op,
		client: client,
		kv:     clientv3.NewKV(client),
	}
}
  • kratos.New()

    對應用程式初始化化,應用程式引數初始化 - 預設引數或接受傳入的引數。

// https://github.com/go-kratos/kratos/blob/v2.3.1/app.go#L39
func New(opts ...Option) *App {
    o := options{
        ctx:              context.Background(),
        sigs:             []os.Signal{syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGINT},
        registrarTimeout: 10 * time.Second,
        stopTimeout:      10 * time.Second,
    }
    ... ...
    return &App{
        ctx:    ctx,
        cancel: cancel,
        opts:   o,
    }
}
  • kratos.Name("helloworld")

    處理應用的服務引數。這個引數傳入到上面 func New(opts ...Option) *App

// https://github.com/go-kratos/kratos/options.go#L41
// Name with service name.
func Name(name string) Option {
     return func(o *options) { o.name = name }
}
  • kratos.Registrar(r)

    選擇哪個服務中心(etcd,consul,zookeeper,nacos 等等)作為 kratos 的服務中心。
    這個引數傳入到上面 func New(opts ...Option) *App

// https://github.com/go-kratos/kratos/blob/v2.3.1/options.go#L81
func Registrar(r registry.Registrar) Option {
	return func(o *options) { o.registrar = r }
}
  • registrar.Register()

真正把服務註冊到服務中心的是 app.Run() 這個方法裡的 a.opts.registrar.Register() 方法,Register() 方法把服務例項註冊到服務中心。

// https://github.com/go-kratos/kratos/app.go#L84
if err := a.opts.registrar.Register(rctx, instance); err != nil {
   return err
}

引數 instance 就是方法 buildInstance() 返回的服務例項 ServiceInstance,ServiceInstance struct 包含了一個服務例項所需的欄位。

// https://github.com/go-kratos/kratos/app.go#L154
func (a *App) buildInstance() (*registry.ServiceInstance, error)

// https://github.com/go-kratos/kratos/registry/registry.go#L33
type ServiceInstance struct {
    ID string `json:"id"`
    Name string `json:"name"`
    Version string `json:"version"`
    Metadata map[string]string `json:"metadata"`
    Endpoints []string `json:"endpoints"`
}

三、參考