本项目为基于 Go/Grpc/kubernetes/Istio
开发微服务的最佳实践提供参考。
并基于 Jenkins/Gitlab/Harbor
实现了CICD
。
并使用 grpc-gateway
作为网关代理。
本最佳实践分为三个部分:
pingservice
的微服务pongservice
的微服务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
在src
目录下创建名为pongservice
的微服务,目录结构如下:
pongservice ├── cmd │ └── server ├── config ├── proto │ └── v1 └── service 6 directories, 0 files
在 pongservice/proto 目录下执行以下命令:
buf mod init
此命令创建一个名为 buf.yaml
的文件,位于 ponservice/proto
目录中。buf.yaml
的代码如下:
version: v1 breaking: use: - FILE lint: use: - DEFAULT
在 version
和 breaking
之间添加如下依赖代码:
deps: - buf.build/googleapis/googleapis
请查看添加此依赖代码的原因:https://github.com/grpc-ecosystem/grpc-gateway#3-external-configuration
添加依赖代码后完整的 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/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/proto/v1
目录中使用以下代码创建一个名为 pongservice.yaml
的原型文件:
type: google.api.Service config_version: 3 http: rules: - selector: proto.v1.PongService.Pong get: /pong.v1.pong
在 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
在 pongservice
目录中创建 go.mod
:
go mod init github.com/janrs-io/Jgrpc/src/pongservice && go mod tidy
在 pongservice/config
目录下,创建 config.yaml
文件并添加以下代码:
# grpc config grpc: host: "" port: ":50051" name: "pong-grpc" # http config http: host: "" port: ":9001" name: "pong-http"
在 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
。
在 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 }
在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
文件。
最后一步,在 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) }
在 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