基於Go/Grpc/kubernetes/Istio開發微服務的最佳實踐嘗試

2023-03-31 15:00:39

基於Go/Grpc/kubernetes/Istio開發微服務的最佳實踐嘗試 - 1/3

基於Go/Grpc/kubernetes/Istio開發微服務的最佳實踐嘗試 - 2/3

基於Go/Grpc/kubernetes/Istio開發微服務的最佳實踐嘗試 - 3/3

專案地址:https://github.com/janrs-io/Jgrpc


轉載請註明來源:https://janrs.com/br6f


Jgrpc

本專案為基於 Go/Grpc/kubernetes/Istio 開發微服務的最佳實踐提供參考。

並基於 Jenkins/Gitlab/Harbor 實現了CICD

並使用 grpc-gateway 作為閘道器代理。

本最佳實踐分為三個部分:

  1. 建立一個 pingservice 的微服務
  2. 建立一個 pongservice 的微服務
  3. 基於Jenkins/Gitlab/Harbor 建立 CICD 部署流程並部署到 k8s/istio

在這一部分中,我們將建立 pongservice 微服務。


前提

假設已經安裝了以下工具:

  • protoc-gen-grpc-gateway
  • protoc-gen-openapiv2
  • protoc-gen-go
  • protoc-gen-go-grpc
  • buf
  • wire

下面是安裝這些工具的教學地址:

protoc-gen-grpc-gateway & protoc-gen-openapiv2 & protoc-gen-go & protoc-gen-go-grpc
這四個工具的安裝教學請檢視:install protoc-gen* tools

wire
wire 工具的安裝教學請檢視:install wire tool

buf
buf 工具的安裝教學請檢視:install buf tool

專案結構

這部分最終的目錄結構如下:

Jgrpc
├── devops
├── istio-manifests
├── kubernetes-manifests
└── src
    └── pongservice
        ├── buf.gen.yaml
        ├── cmd
        │   ├── main.go
        │   └── server
        │       ├── grpc.go
        │       ├── http.go
        │       ├── run.go
        │       ├── wire.go
        │       └── wire_gen.go
        ├── config
        │   ├── config.go
        │   └── config.yaml
        ├── genproto
        │   └── v1
        │       ├── gw
        │       │   └── pongservice.pb.gw.go
        │       ├── pongservice.pb.go
        │       └── pongservice_grpc.pb.go
        ├── go.mod
        ├── go.sum
        ├── proto
        │   ├── buf.lock
        │   ├── buf.yaml
        │   └── v1
        │       ├── pongservice.proto
        │       └── pongservice.yaml
        └── service
            ├── client.go
            └── server.go

14 directories, 20 files

開始

建立專案的整體目錄結構如下:

Jgrpc
├── devops
├── istio-manifests
├── kubernetes-manifests
├── src

建立 pongservice 微服務

建立基本目錄

src目錄下建立名為pongservice的微服務,目錄結構如下:

pongservice
├── cmd
│   └── server
├── config
├── proto
│   └── v1
└── service

6 directories, 0 files

生成程式碼和檔案

生成 buf.yaml

在 pongservice/proto 目錄下執行以下命令:

buf mod init

此命令建立一個名為 buf.yaml 的檔案,位於 ponservice/proto 目錄中。
buf.yaml的程式碼如下:

version: v1
breaking:
  use:
    - FILE
lint:
  use:
    - DEFAULT

versionbreaking 之間新增如下依賴程式碼:

deps:
  - buf.build/googleapis/googleapis

請檢視新增此依賴程式碼的原因:https://github.com/grpc-ecosystem/grpc-gateway

新增依賴程式碼後完整的 buf.yaml 檔案如下:

version: v1
deps:
  - buf.build/googleapis/googleapis
breaking:
  use:
    - FILE
lint:
  use:
    - DEFAULT

然後在pongservice/proto目錄下執行如下命令:

buf mod update

執行命令後會生成一個buf.lock檔案,程式碼如下:

# Generated by buf. DO NOT EDIT.
version: v1
deps:
  - remote: buf.build
    owner: googleapis
    repository: googleapis
    commit: 463926e7ee924d46ad0a726e1cf4eacd

生成 pongservice.proto

pongservice/proto/v1 目錄中使用以下程式碼建立一個名為 pongservice.proto 的原型檔案:

syntax = "proto3";

package proto.v1;

option go_package = "github.com/janrs-io/Jgrpc/src/pongservice/genproto/v1";

service PongService {
  rpc Pong(PongRequest) returns(PongResponse){}
}

message PongRequest {
  string msg = 1 ;
}

message PongResponse {
  string msg = 1;
}

生成 pongservice.yaml

pongservice/proto/v1 目錄中使用以下程式碼建立一個名為 pongservice.yaml 的原型檔案:

type: google.api.Service
config_version: 3

http:
  rules:
    - selector: proto.v1.PongService.Pong
      get: /pong.v1.pong

生成 buf.gen.yaml

pongservice 目錄中使用以下程式碼建立一個名為 buf.gen.yaml 的 yaml 檔案:

version: v1
plugins:
  - plugin: go
    out: genproto/v1
    opt:
      - paths=source_relative
  - plugin: go-grpc
    out: genproto/v1
    opt:
      - paths=source_relative
  - plugin: grpc-gateway
    out: genproto/v1/gw
    opt:
      - paths=source_relative
      - grpc_api_configuration=proto/v1/pongservice.yaml
      - standalone=true

pongservice 目錄中,執行以下命令:

buf generate proto/v1

執行命令後,會在pongservice目錄下自動建立一個genproto目錄,該目錄下有以下檔案:

genproto
└── v1
    ├── gw
    │   └── ponservice.pb.gw.go
    ├── ponservice.pb.go
    └── ponservice_grpc.pb.go

2 directories, 3 files

生成 go.mod

pongservice 目錄中建立 go.mod

go mod init github.com/janrs-io/Jgrpc/src/pongservice && go mod tidy

生成 config.yaml

pongservice/config 目錄下,建立 config.yaml 檔案並新增以下程式碼:

# grpc config
grpc:
  host: ""
  port: ":50051"
  name: "pong-grpc"

# http config
http:
  host: ""
  port: ":9001"
  name: "pong-http"

生成 config.go

pongservice/config 目錄下,建立 config.go 檔案並新增以下程式碼:

package config

import (
	"net/http"

	"github.com/spf13/viper"
	"google.golang.org/grpc"
)

// Config Service config
type Config struct {
	Grpc Grpc `json:"grpc" yaml:"grpc"`
	Http Http `json:"http" yaml:"http"`
}

// NewConfig Initial service's config
func NewConfig(cfg string) *Config {

	if cfg == "" {
		panic("load config file failed.config file can not be empty.")
	}

	viper.SetConfigFile(cfg)

	// Read config file
	if err := viper.ReadInConfig(); err != nil {
		panic("read config failed.[ERROR]=>" + err.Error())
	}
	conf := &Config{}
	// Assign the overloaded configuration to the global
	if err := viper.Unmarshal(conf); err != nil {
		panic("assign config failed.[ERROR]=>" + err.Error())
	}

	return conf

}

// Grpc Grpc server config
type Grpc struct {
	Host   string `json:"host" yaml:"host"`
	Port   string `json:"port" yaml:"port"`
	Name   string `json:"name" yaml:"name"`
	Server *grpc.Server
}

// Http Http server config
type Http struct {
	Host   string `json:"host" yaml:"host"`
	Port   string `json:"port" yaml:"port"`
	Name   string `json:"name" yaml:"name"`
	Server *http.Server
}

然後在 pongservice 目錄中再次執行 go mod tidy

生成 client.go

pongservice/service 目錄下,建立 client.go 檔案並新增以下程式碼:

package service

import (
	"context"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"

	"github.com/janrs-io/Jgrpc/src/pongservice/config"
	v1 "github.com/janrs-io/Jgrpc/src/pongservice/genproto/v1"
)

// NewClient New service's client
func NewClient(conf *config.Config) (v1.PongServiceClient, error) {

	serverAddress := conf.Grpc.Host + conf.Grpc.Port
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	conn, err := grpc.DialContext(ctx, serverAddress, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		return nil, err
	}
	client := v1.NewPongServiceClient(conn)
	return client, nil

}

pongservice/service 目錄下,建立 server.go 檔案並新增以下程式碼:

package service

import (
	"context"

	"github.com/janrs-io/Jgrpc/src/pongservice/config"
	v1 "github.com/janrs-io/Jgrpc/src/pongservice/genproto/v1"
)

// Server Server struct
type Server struct {
	v1.UnimplementedPongServiceServer
	pongClient v1.PongServiceClient
	conf       *config.Config
}

// NewServer New service grpc server
func NewServer(conf *config.Config, pongClient v1.PongServiceClient) v1.PongServiceServer {
	return &Server{
		pongClient: pongClient,
		conf:       conf,
	}
}

func (s *Server) Pong(ctx context.Context, req *v1.PongRequest) (*v1.PongResponse, error) {
	return &v1.PongResponse{Msg: "response pong msg:" + req.Msg}, nil
}

生成 run server 檔案

pongservice/cmd/server目錄下,建立以下四個檔案:

  • grpc.go
  • http.go
  • run.go
  • wire.go

將以下程式碼新增到 grpc.go 檔案中:

package server

import (
	"fmt"
	"log"
	"net"

	"google.golang.org/grpc"

	"github.com/janrs-io/Jgrpc/src/pongservice/config"
	v1 "github.com/janrs-io/Jgrpc/src/pongservice/genproto/v1"
)

// RunGrpcServer Run grpc server
func RunGrpcServer(server v1.PongServiceServer, conf *config.Config) {

	grpcServer := grpc.NewServer()
	v1.RegisterPongServiceServer(grpcServer, server)

	fmt.Println("Listening grpc server on port" + conf.Grpc.Port)
	listen, err := net.Listen("tcp", conf.Grpc.Port)
	if err != nil {
		panic("listen grpc tcp failed.[ERROR]=>" + err.Error())
	}

	go func() {
		if err = grpcServer.Serve(listen); err != nil {
			log.Fatal("grpc serve failed", err)
		}
	}()

	conf.Grpc.Server = grpcServer

}

將以下程式碼新增到 http.go 檔案中:

package server

import (
	"context"
	"fmt"
	"net/http"

	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"

	"github.com/janrs-io/Jgrpc/src/pongservice/config"
	v1 "github.com/janrs-io/Jgrpc/src/pongservice/genproto/v1/gw"
)

// RunHttpServer Run http server
func RunHttpServer(conf *config.Config) {

	mux := runtime.NewServeMux()
	opts := []grpc.DialOption{
		grpc.WithTransportCredentials(insecure.NewCredentials()),
	}

	if err := v1.RegisterPongServiceHandlerFromEndpoint(
		context.Background(),
		mux,
		conf.Grpc.Port,
		opts,
	); err != nil {
		panic("register service handler failed.[ERROR]=>" + err.Error())
	}

	httpServer := &http.Server{
		Addr:    conf.Http.Port,
		Handler: mux,
	}
	fmt.Println("Listening http server on port" + conf.Http.Port)

	go func() {
		if err := httpServer.ListenAndServe(); err != nil {
			fmt.Println("listen http server failed.[ERROR]=>" + err.Error())
		}
	}()

	conf.Http.Server = httpServer

}

將以下程式碼新增到 run.go 檔案中:

package server

import (
	"context"
	"fmt"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/janrs-io/Jgrpc/src/pongservice/config"
	v1 "github.com/janrs-io/Jgrpc/src/pongservice/genproto/v1"
)

// Run Run service server
func Run(cfg string) {

	conf := config.NewConfig(cfg)
	// run grpc server
	RunGrpcServer(initServer(conf), conf)
	// run http server
	RunHttpServer(conf)
	// listen exit server event
	HandleExitServer(conf)

}

// SetServer Wire inject service's component
func initServer(conf *config.Config) v1.PongServiceServer {
	server, err := InitServer(conf)
	if err != nil {
		panic("run server failed.[ERROR]=>" + err.Error())
	}
	return server
}

// HandleExitServer Handle service exit event
func HandleExitServer(conf *config.Config) {

	ch := make(chan os.Signal, 1)
	signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
	<-ch
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	conf.Grpc.Server.GracefulStop()
	if err := conf.Http.Server.Shutdown(ctx); err != nil {
		panic("shutdown service failed.[ERROR]=>" + err.Error())
	}
	<-ctx.Done()
	close(ch)
	fmt.Println("Graceful shutdown http & grpc server.")

}

將以下程式碼新增到 wire.go 檔案中:

//go:build wireinject
// +build wireinject

package server

import (
	"github.com/google/wire"

	"github.com/janrs-io/Jgrpc/src/pongservice/config"
	v1 "github.com/janrs-io/Jgrpc/src/pongservice/genproto/v1"
	"github.com/janrs-io/Jgrpc/src/pongservice/service"
)

// InitServer Inject service's component
func InitServer(conf *config.Config) (v1.PongServiceServer, error) {

	wire.Build(
		service.NewClient,
		service.NewServer,
	)

	return &service.Server{}, nil

}

pongservice 目錄中再次執行 go mod tidy

go mod tidy

然後在 pongservice 目錄中執行以下 wire 命令:

wire ./...

執行 wire 命令後,將在 pongsevice/cmd/server 目錄中自動建立 wire_gen.go 檔案。

生成 main.go

最後一步,在 pongservice/cmd 目錄下建立 main.go 檔案.

package main

import (
	"flag"

	"github.com/janrs-io/Jgrpc/src/pongservice/cmd/server"
)

var cfg = flag.String("config", "config/config.yaml", "config file location")

// main main
func main() {
	server.Run(*cfg)
}

啟動 service

pongservice 目錄下執行以下命令啟動微服務:

注意
pongservice 目錄而不是 pongservice/cmd 目錄中執行命令

go run cmd/main.go

啟動服務後,會顯示如下資訊:

Listening grpc server on port:50051
Listening http server on port:9001

在瀏覽器中輸入以下地址即可存取該服務:

127.0.01:9001/pong.v1.pong?msg=best practice

如果成功,將返回以下資料:

{
    "msg": "response pong msg:best practice"
}

總結

現在,我們已經瞭解瞭如何建立可以開發基本功能微服務的專案結構。

在接下來的部分中,我們繼續建立一個名為 pingservice 的微服務,並存取我們在這部分中建立的 pongservice


轉載請註明來源:https://janrs.com/br6f