微服务 — gRPC+Consul项目实践
一、项目功能(项目运行需要启动consul服务端)
- 基于golang工程,开发一个中心-边缘类系统,实现例如数据采集、任务下发等场景的功能需要
- 集成gRPC,实现中心服务和边缘服务的远程交互
- 引入Consul,集成服务发现,改造中心边缘通过服务发现进行远程链接
二、技术简介
2-1、Go 协程技术- goroutine
-
goroutine的概念类似于线程,但 goroutine是由Go的运行时(runtime)调度和管理的。Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU。
-
Go语言中使用goroutine非常简单,只需要在调用函数的时候在前面加上go关键字,就可以为一个函数创建一个goroutine。
- 一个goroutine必定对应一个函数,可以创建多个goroutine去执行相同的函数。
-
-
启动goroutine的方式非常简单,只需要在调用的函数(普通函数和匿名函数)前面加上一个go关键字,例如:
func hello() { fmt.Println("Hello Goroutine!") } func main() { go hello() fmt.Println("main goroutine done!") }
-
在Go语言中,可以使用sync.WaitGroup来管理goroutine,实现goroutine的同步和退出等,例如:
var wg sync.WaitGroup func hello(i int) { defer wg.Done() // goroutine结束就登记-1 fmt.Println("Hello Goroutine!", i) } func main() { for i := 0; i < 10; i++ { wg.Add(1) // 启动一个goroutine就登记+1 go hello(i) } wg.Wait() // 等待所有登记的goroutine都结束 }
2-2、gRPC
-
gRPC 基于如下思想:定义一个服务, 指定其可以被远程调用的方法及其参数和返回类型。gRPC 默认使用 protocol buffers 作为接口定义语言,来描述服务接口和有效载荷消息结构。
-
gRPC 是一个高性能、开源和通用的 RPC 框架,面向移动和 HTTP/2 设计。
-
gRPC支持多语言,目前提供 C、Java 和 Go 语言版本。
-
gRPC 基于 HTTP/2 标准设计,带来诸如双向流、流控、头部压缩、单 TCP 连接上的多复用请求等特点。
-
-
gRPC 允许你定义四类服务方法:
- 单项 RPC,即客户端发送一个请求给服务端,从服务端获取一个应答,就像一次普通的函数调用。
- 服务端流式 RPC,即客户端发送一个请求给服务端,可获取一个数据流用来读取一系列消息。客户端从返回的数据流里一直读取直到没有更多消息为止。
- 客户端流式 RPC,即客户端用提供的一个数据流写入并发送一系列消息给服务端。一旦客户端完成消息写入,就等待服务端读取这些消息并返回应答。
- 双向流式 RPC,即两边都可以分别通过一个读写数据流来发送一系列消息。这两个数据流操作是相互独立的,所以客户端和服务端能按其希望的任意顺序读写。
-
gRPC 提供 protocol buffer 编译插件,能够从一个服务定义的 .proto 文件生成客户端和服务端代码。通常 gRPC 用户可以在服务端实现这些API,并从客户端调用它们。
- 在服务侧,服务端实现服务接口,运行一个 gRPC 服务器来处理客户端调用。gRPC 底层架构会解码传入的请求,执行服务方法,编码服务应答。
- 在客户侧,客户端有一个存根实现了服务端同样的方法。客户端可以在本地存根调用这些方法,用合适的 protocol buffer 消息类型封装这些参数— gRPC 来负责发送请求给服务端并返回服务端 protocol buffer 响应。
2-3、Consul
-
Consul是一个服务网格解决方案,提供了一个功能齐全的控制平面,具有服务发现、配置和分段功能。这些功能中的每一项都可以根据需要单独使用,也可以一起使用来构建一个完整的服务网格。Consul需要一个数据平面,并支持代理和原生集成模型。
-
Consul的主要功能有:
- 服务发现 : Consul的客户端可以注册一个服务,比如api或mysql,其他客户端可以使用Consul来发现特定服务的提供者。使用DNS或HTTP,应用程序可以很容易地找到他们所依赖的服务。
- 健康检查 : Consul客户端可以提供任何数量的健康检查,要么与给定的服务相关联,要么与本地节点相关联。这些信息可以运维人员用来监控集群的健康状况,并被服务发现组件来路由流量(比如: 仅路由到健康节点)
- KV存储 : 应用程序可以利用Consul的层级K/V存储来实现任何目的,包括动态配置、功能标记、协调、领导者选举等。Consul提供了HTTP API,使其非常简单以用。
- 安全服务通信: Consul可以为服务生成和分发TLS证书,以建立相互的TLS连接。可以使用Intention来定义哪些服务被允许进行通信。服务隔离可以通过可以实时更改Intention策略轻松管理,而不是使用复杂的网络拓扑结构和静态防火墙规则。
- 多数据中心: Consul支持开箱即用的多数据中心。这意味着Consul的用户不必担心建立额外的抽象层来发展到多个区域。
三、项目结构
├─.idea
├─client
│ └─client.go //边缘组件
├─common
│ ├─lib //工具包
│ ├─message // 协议文件-proto及其生成的代码
│ │ ├─transceiver
│ │ │ ├─transceiver.pb.gp
│ │ │ └─transceiver_grpc.pb.go
│ │ └─transceiver.proto
│ └─repo // 公共仓库-工具服务
│ └─consul // consul 服务集成
│ ├─uconsul.go
│ └─uconsul_test.go
└─server
└─server.go // 服务端组件
四、项目实现
-
transceiver.proto【产物生成详见:Grpc Golang项目实践从小白到入门_玉言心的博客-优快云博客】
syntax = "proto3"; option go_package="/transceiver"; package client; service Transceiver { rpc registerClient(TransceiverMsg) returns (TransceiverID); rpc getClient(TransceiverID) returns (TransceiverMsg); rpc taskHandel(TransceiverID) returns (TaskMsg); rpc taskCall(TaskMsg) returns (response); } message response { string code = 1; } message TaskMsg { machine monitor = 1; repeated targetMsg target = 2; // 目标数组 } message targetMsg { machine target = 1; taskInfo task = 2; } message taskInfo { controlMsg control = 1; tasks tasks = 2; } message tasks { string cmd = 1; string result = 2; } message controlMsg { string params = 1; string task_name = 2; string time = 3; string taskType = 4; string protocol = 5; } message machine { string name = 1; string prov = 2; string city = 3; string status = 4; string ip = 5; } message TransceiverMsg { string id = 1; // id float uuid = 2; // uid string name = 3; // 客户端名称:hostname string version = 4; // 版本号 string time = 5; // 上报时间,用于心跳检测 string status = 6; // 是否空闲等 string description = 7; // 描述 } message TransceiverID { string value = 1; } message TransceiverName { string value = 1; }
-
uconsul.go
package consul import ( "errors" "fmt" capi "github.com/hashicorp/consul/api" "log" "net" ) type CSApi struct { opt *capi.AgentServiceRegistration // log logger2.Logger cli *capi.Client } var instance *CSApi func CSInstance(opt *capi.AgentServiceRegistration) *CSApi { if instance == nil { instance = &CSApi{ opt: opt, // log: logger2.NewSugar("consul api", false), cli: nil, } //初始化consul配置 consulConfig := capi.DefaultConfig() //获取consul客户端 consulClient, err := capi.NewClient(consulConfig) if err != nil { fmt.Println("consul api NewClient err :", err) // retry once return CSInstance(opt) } instance.cli = consulClient } return instance } func (cs *CSApi) Register() error { if cs.opt == nil { log.Fatalf("Warn : info register config is nil") return errors.New("register config is nil") } //注册到consul err := cs.cli.Agent().ServiceRegister(cs.opt) if err != nil { log.Fatalf("register Error info : %v", err) return err } log.Println("register success ", "server id : ", cs.opt.ID) return nil } func (cs *CSApi) Deregister(serverID string) { err := cs.cli.Agent().ServiceDeregister(serverID) if err != nil { log.Fatalf("register server error : %v", err) } log.Println("server deregister success, server id :", serverID) } func (cs *CSApi) HealthCheck() error { if cs.cli.Agent().DisableServiceMaintenance(cs.opt.ID) != nil { err := cs.Register() if err != nil { return err } } log.Println("", "server_health_check", true) return nil }
-
client.go
package main import ( "context" "fmt" "github.com/hashicorp/consul/api" "google.golang.org/grpc/credentials/insecure" "log" "s-grpc/common/lib" "s-grpc/common/message/transceiver" "strconv" "sync" "time" "google.golang.org/grpc" ) const ( serverName = "Detect Server" addr = "localhost:50051" ) type Client struct { lock sync.RWMutex clientID string stopChan chan int cli transceiver.TransceiverClient } var ins *Client func Instance() *Client { if ins == nil { ins = &Client{ clientID: "", lock: sync.RWMutex{}, stopChan: make(chan int), } } return ins } func (c *Client) Start(clientName string) { consulConfig := api.DefaultConfig() consulClient, err := api.NewClient(consulConfig) if err != nil { fmt.Println("api.NewClient:", err) } services, _, err := consulClient.Health().Service(serverName, "Detect", true, nil) //获取链接 address := services[0].Service.Address + ":" + strconv.Itoa(services[0].Service.Port) // Set up a connection to the server. conn, err := grpc.Dial(address, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatalf("did not connect: %v", err) } defer conn.Close() c.cli = transceiver.NewTransceiverClient(conn) ctx := context.WithValue(context.Background(), "grpc", "agent-test") //指定测试人员 /*ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) defer cancel()*/ c.RegisterServer(ctx, clientName) lib.Loop(time.Second*time.Duration(5), ctx, c.GetTask, c.stopChan) } func (c *Client) RegisterServer(ctx context.Context, clientName string) { // Contact the server and print out its response. // name := "proto agent" description := "test grpc conn." uid := float32(1001.00) ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() r, err := c.cli.RegisterClient(ctx, &transceiver.TransceiverMsg{ Id: strconv.Itoa(int(uid)), Uuid: uid, Name: clientName, Version: "1.0.0", Time: "2022-11-25 16:25:00", Status: "0", Description: description, }) if err != nil { log.Fatalf("Client: Could not register Client: %v", err) } log.Printf("Client: ID %s register successfully", r.Value) c.clientID = r.Value } func (c *Client) GetTask(ctx context.Context) { cli, err := c.cli.GetClient(ctx, &transceiver.TransceiverID{Value: c.clientID}) if err != nil { log.Printf("Client: Could not get client: %v", err) } log.Printf("Client: %v", cli.String()) c.RunTask(ctx) } func (c *Client) RunTask(ctx context.Context) { task, err := c.cli.TaskHandel(ctx, &transceiver.TransceiverID{Value: c.clientID}) if err != nil { log.Fatalf("Client: Could not get client: %v", err) } log.Println("Client: receive task, len : ", len(task.Target)) for i := range task.Target { log.Println("Client: run task : ", task.Target[i].Task.Control.TaskName) task.Target[i].Task.Tasks.Result = "0.1::0.13::0.25::0.45" } call, err := c.cli.TaskCall(ctx, task) if err != nil { return } log.Printf("Client: run task end: { %v }", call) } func main() { // Get client instance cli := Instance() cli.Start("client-01") }
-
server.go
package main import ( "context" "fmt" "github.com/hashicorp/consul/api" "log" "math/rand" "net" "s-grpc/common/lib" "s-grpc/common/message/transceiver" "s-grpc/common/repo/consul" "strconv" "strings" "time" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) const ( port = "50051" address = "detect.center_api.com" ) // server is used to implement ecommerce/product_info. type server struct { clientMap map[string]*transceiver.TransceiverMsg // 连接池维护 tasks map[string]*transceiver.TaskMsg // 任务存储 } func (s *server) TaskCall(ctx context.Context, msg *transceiver.TaskMsg) (*transceiver.Response, error) { log.Println("server: receive task response : ", msg.Monitor.Name) if msg == nil || len(msg.Target) == 0 { log.Println("server: receive task result len : ", 0) } log.Println("server: -------- task result : ", msg.Target) return &transceiver.Response{Code: "200"}, nil } func (s *server) TaskHandel(ctx context.Context, id *transceiver.TransceiverID) (*transceiver.TaskMsg, error) { var tasks = s.tasks if len(tasks) == 0 { log.Printf("server: task is null.") } if _, ok := tasks[id.Value]; !ok { return &transceiver.TaskMsg{}, status.Errorf(codes.NotFound, "Client does not exist.") } return tasks[id.Value], nil } // RegisterClient implements ecommerce.AddProduct func (s *server) RegisterClient(ctx context.Context, in *transceiver.TransceiverMsg) (*transceiver.TransceiverID, error) { in.Id = lib.Md5(in.Name) if s.clientMap == nil { s.clientMap = make(map[string]*transceiver.TransceiverMsg) } s.clientMap[in.Id] = in if _, ok := s.tasks[in.Id]; !ok { s.tasks[in.Id] = &transceiver.TaskMsg{ Monitor: s.getMachine(in.Id), Target: nil, } } log.Printf("server: client [%v-%v] - registered.", in.Name, in.Id) return &transceiver.TransceiverID{Value: in.Id}, status.New(codes.OK, "").Err() } // GetClient implements ecommerce.GetProduct func (s *server) GetClient(ctx context.Context, in *transceiver.TransceiverID) (*transceiver.TransceiverMsg, error) { product, exists := s.clientMap[in.Value] if exists && product != nil { log.Printf("Server: client [%v-%v] - Retrieved.", product.Name, product.Id) return product, status.New(codes.OK, "").Err() } return nil, status.Errorf(codes.NotFound, "Client does not exist.", in.Value) } func (s *server) DivTask(ctx context.Context) { s.tasks = map[string]*transceiver.TaskMsg{} log.Printf("server: task div run. cleint len %d", len(s.clientMap)) for uid, _ := range s.clientMap { if _, ok := s.tasks[uid]; !ok { s.tasks[uid] = &transceiver.TaskMsg{ Monitor: s.getMachine(uid), Target: nil, } } targetMsg := &transceiver.TargetMsg{ Target: s.getMachine("target00" + uid), Task: s.getTask(uid), } s.tasks[uid].Target = append(s.tasks[uid].Target, targetMsg) } } func (s server) getTask(taskID string) *transceiver.TaskInfo { timeStr := time.Now().Format("2022-10-10 12:00:00") uis := fmt.Sprintf("%06v", rand.New(rand.NewSource(time.Now().UnixNano())).Int31n(1000000)) ctl := &transceiver.ControlMsg{ Params: "{}", TaskName: fmt.Sprintf("task-%s@%s ", taskID, uis), Time: timeStr, TaskType: "test", Protocol: "ping", } task := &transceiver.Tasks{ Cmd: "ping www.baidu.com", Result: "0::0::0::0", } return &transceiver.TaskInfo{ Control: ctl, Tasks: task, } } func (s *server) getMachine(uid string) *transceiver.Machine { return &transceiver.Machine{ Name: "test-" + uid, Prov: "guizhou", City: "guiyang", Status: "2201", Ip: "192.168.137.100", } } func main() { // 创建consul服务发现 serverName := "Detect Server" p, _ := strconv.Atoi(port) ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() csConfig := api.AgentServiceRegistration{ ID: "001", Tags: strings.Split(serverName, " "), Name: serverName, Address: address, Port: p, //注册check服务。 Check: &api.AgentServiceCheck{ CheckID: "grpc detect", //HTTP: fmt.Sprintf("https://%s:%d%s", address, p, "/check"), TCP: fmt.Sprintf("%s:%d", address, p), //设置超时 5s。 Timeout: "5s", //设置间隔 5s。 Interval: "5s", }, } consulInstance := consul.CSInstance(&csConfig) err := consulInstance.Register() if err != nil { return } // 服务注册检测,注册中心重启,需要重新注册 go lib.Loop(time.Second*time.Duration(30), ctx, func(ctx context.Context) { err = consulInstance.HealthCheck() if err != nil { log.Fatalf("err: %v", err) } }, nil) // 创建服务端 ser := &server{ clientMap: make(map[string]*transceiver.TransceiverMsg), tasks: make(map[string]*transceiver.TaskMsg), // taskQueue: queue.NewQueue(50), } // 任务分配 go lib.Loop(time.Second*time.Duration(10), ctx, ser.DivTask, nil) s := grpc.NewServer() // 注册服务端到grpc transceiver.RegisterTransceiverServer(s, ser) // 开启服务监听 lis, err := net.Listen("tcp", ":"+port) if err != nil { log.Fatalf("failed to listen: %v", err) } if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }
-
运行效果
-
服务端
-
客户端
-
注册中心
-