golang grpc consul 服務註冊、發現和負載均衡。

開發環境

  • grpc-go: v1.24
  • consul:1.6
  • 設備: docker-compose、 mac

搭建一個單節點的consul環境(無acl)

共兩個節點,server和client。

version: '2'
services:
  consul_server: 
    image: consul:1.6
    restart: always
    ports: 
      - "8301:8301"
    command: agent -server -bind=0.0.0.0 -client=0.0.0.0 -bootstrap-expect=1 -node=consul1 -datacenter=dc1
  consul_client:
    image: consul:1.6
    restart: always
    ports: 
      - "8400:8400"
      - "8500:8500"
      - "8600:8600"
    depends_on: 
      - consul_server
    command: agent -bind=0.0.0.0 -client=0.0.0.0 -retry-join=consul_server -ui -node=client1 -datacenter=dc1  -join consul_server

其中consul_server爲單節點,並自舉爲master節點。consul_client爲當前主機使用的client。

服務註冊

grpc中預留了服務註冊和服務解析服務負載均衡的接口,均可以自行實現。
其中服務註冊最易實現,也比較好理解。分以下幾個步驟

  • 1、自行實現一個註冊服務用的結構體記錄服務信息
  • 2、創建一個consul/api 的客戶端,連接到先前安裝配置好的單節點consul集羣。
  • 3、實現check功能,因爲是grpc的服務直接選擇了grpc類型。
  • 4、實現check函數,並註冊進consul
regis.go 服務實際腳本,實現了 健康檢查
package regis

import (
	"fmt"
	"google.golang.org/grpc"
	"google.golang.org/grpc/health/grpc_health_v1"
	"google.golang.org/grpc/reflection"
	"library/microservice"
	"net"
)

type Server struct {
	server   *grpc.Server
}

var svr = Server{}

func init() {
	svr.server = grpc.NewServer()
}
func GetServer() *grpc.Server {
	return svr.server
}
func Start(){

	defer func() {
		fmt.Println("啓動錯誤")
	}()
	//使用consul註冊服務
	register:= microservice.NewConsulRegister()
	register.Port = 8079
	register.Name = "user"
	register.Tag = []string{"user"}
	if err := register.Register(); err !=nil{
		panic(err)
	}
	grpc_health_v1.RegisterHealthServer(svr.server, &microservice.HealthImpl{Status:grpc_health_v1.HealthCheckResponse_SERVING})

	lis, err := net.Listen("tcp", fmt.Sprintf(":%d",register.Port))
	if err != nil {
		panic(err)
	}
	fmt.Println("user server ready listen")
	reflection.Register(svr.server)
	if err := svr.server.Serve(lis); err != nil {
		panic(err)
		//log.Fatalf("failed to serve: %v", err)
	}
}
簡單的健康檢查功能
package microservice

import (
	"context"
	"fmt"
	"google.golang.org/grpc/health/grpc_health_v1"
)

type HealthImpl struct {
	Status   grpc_health_v1.HealthCheckResponse_ServingStatus
	Reason string
}

func (h *HealthImpl) Watch(*grpc_health_v1.HealthCheckRequest, grpc_health_v1.Health_WatchServer) error {
	return nil
}

func (h *HealthImpl) OffLine(reason string) {
	h.Status = grpc_health_v1.HealthCheckResponse_NOT_SERVING
	h.Reason = reason
	fmt.Println(reason)
}
func (h *HealthImpl) OnLine(reason string) {
	h.Status = grpc_health_v1.HealthCheckResponse_SERVING
	h.Reason = reason
	fmt.Println(reason)
}

func (h *HealthImpl) Check(ctx context.Context, req *grpc_health_v1.HealthCheckRequest) (*grpc_health_v1.HealthCheckResponse, error) {
	return &grpc_health_v1.HealthCheckResponse{
		Status:h.Status,
	}, nil
}

consul_register.go 註冊用 ,Address爲自己的consul api地址
package microservice

import (
	"fmt"
	"github.com/hashicorp/consul/api"
	"net"
	"time"
)

//consul
// NewConsulRegister create a new consul register
func NewConsulRegister() *ConsulRegister {
	return &ConsulRegister{
		Address:                        192.168.1.118:8500, //consul address
		Name:                           "unknown",
		Tag:                            []string{},
		Port:                           3000,
		DeregisterCriticalServiceAfter: time.Duration(1) * time.Minute,
		Interval:                       time.Duration(10) * time.Second,
	}
}

// ConsulRegister consul service register
type ConsulRegister struct {
	Address                        string
	Name                           string
	Tag                            []string
	Port                           int
	DeregisterCriticalServiceAfter time.Duration
	Interval                       time.Duration
}

// Register register service
func (r *ConsulRegister) Register() error {
	config := api.DefaultConfig()
	config.Address = r.Address
	client, err := api.NewClient(config)
	if err != nil {
		return err
	}
	agent := client.Agent()

	IP := LocalIP()
	reg := &api.AgentServiceRegistration{
		ID:      fmt.Sprintf("%v-%v-%v", r.Name, IP, r.Port), // 服務節點的名稱
		Name:    r.Name,  										  // 服務名稱
		Tags:    r.Tag,                                          // tag,可以爲空
		Port:    r.Port,                                         // 服務端口
		Address: IP, 											// 服務 IP
		Check: &api.AgentServiceCheck{ // 健康檢查
			Interval: r.Interval.String(), // 健康檢查間隔
			GRPC:     fmt.Sprintf("%v:%v/%v", IP, r.Port, r.Name), // grpc 支持,執行健康檢查的地址,service 會傳到 Health.Check 函數中
			DeregisterCriticalServiceAfter: r.DeregisterCriticalServiceAfter.String(), // 註銷時間,相當於過期時間
		},
	}

	if err := agent.ServiceRegister(reg); err != nil {
		return err
	}

	return nil
}

func LocalIP() string {
	addrs, err := net.InterfaceAddrs()
	if err != nil {
		return ""
	}
	for _, address := range addrs {
		if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
			if ipnet.IP.To4() != nil {
				return ipnet.IP.String()
			}
		}
	}
	return ""
}

服務發現

服務發現同樣是自行實現的,結合了grpc自帶的round_robin負載均衡功能對發現的地址進行輪訓使用,負載均衡也可以自行實現,這裏使用了grpc自帶的。
roubrobin.go
流程如下:

  • 1、註冊自行實現的resolver 接口
  • 2、初始化resolver實例
  • 3、grpc.Dial 並配置。其中 grpc.WithBalancerName(roundrobin.Name), 雖然廢棄了單還可以使用。
  • 4、特別注意,雖然這樣就配好了,單其中有挺多坑會導致無法建立連接,註釋中有解釋。
  • 5、resolve原理:解析接口實現後通過定時調用consul api獲取相應服務的信息,通過cr.cc.UpdateState(resolver.State{Addresses:newAddrs})更新進入grpc.conn中。等待balance處理
  • 6、balance原理:當調用rpc函數時 invoke 函數會調用當前的負載均衡器:本文中就是round_robin, round_robin通過pick函數調用下一個可用的服務地址,並用新的地址替換舊的地址。連接成功後即可調用遠程函數。

調用鏈分析:

  • 過年了,剩下的等我空了補充~~~
consul_resolver.go
package microservice

import (
	"errors"
	"fmt"
	"github.com/hashicorp/consul/api"
	"google.golang.org/grpc/resolver"
	"net"
	"regexp"
	"strconv"
	"sync"
	"time"
)

const (
	defaultPort = "8500"
)

var (
	errMissingAddr = errors.New("consul resolver: missing address")

	errAddrMisMatch = errors.New("consul resolver: invalied uri")

	errEndsWithColon = errors.New("consul resolver: missing port after port-separator colon")

	regexConsul, _ = regexp.Compile("^([A-z0-9.]+)(:[0-9]{1,5})?/([A-z_]+)$")

	//單例模式
	builderInstance = &consulBuilder{}
)

func Init() {
	fmt.Printf("calling consul init\n")
	resolver.Register(CacheBuilder())
}

type consulBuilder struct {
}

type consulResolver struct {
	address              string
	wg                   sync.WaitGroup
	cc                   resolver.ClientConn
	name                 string
	disableServiceConfig bool
	Ch                   chan int
}

func NewBuilder() resolver.Builder {
	return &consulBuilder{}
}

func CacheBuilder() resolver.Builder {
	return builderInstance
}

func (cb *consulBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOption) (resolver.Resolver, error) {

	host, port, name, err := parseTarget(fmt.Sprintf("%s/%s", target.Authority, target.Endpoint))
	if err != nil {
		fmt.Println("parse err")
		return nil, err
	}
	fmt.Println(fmt.Sprintf("consul service ==> host:%s, port%s, name:%s",host, port, name))
	cr := &consulResolver{
		address:              fmt.Sprintf("%s%s", host, port),
		name:                 name,
		cc:                   cc,
		disableServiceConfig: opts.DisableServiceConfig,
		Ch:					  make(chan int, 0),
	}
	go cr.watcher()
	return cr, nil

}

func (cr *consulResolver) watcher() {
	fmt.Printf("calling [%s] consul watcher\n", cr.name)
	config := api.DefaultConfig()
	config.Address = cr.address
	client, err := api.NewClient(config)
	if err != nil {
		fmt.Printf("error create consul client: %v\n", err)
		return
	}
	t := time.NewTicker(2000 * time.Millisecond)
	defer func() {
		fmt.Println("defer done")
	}()
	for {
		select {
		case <-t.C:
			//fmt.Println("定時")
		case <-cr.Ch:
			//fmt.Println("ch call")
		}
		//api添加了 lastIndex   consul api中並不兼容附帶lastIndex的查詢
		services, _, err := client.Health().Service(cr.name, "", true, &api.QueryOptions{})
		if err != nil {
			fmt.Printf("error retrieving instances from Consul: %v", err)
		}

		newAddrs := make([]resolver.Address, 0)
		for _, service := range services {
			addr := net.JoinHostPort(service.Service.Address, strconv.Itoa(service.Service.Port))
			newAddrs = append(newAddrs, resolver.Address{
				Addr: addr,
				//type:不能是grpclb,grpclb在處理鏈接時會刪除最後一個鏈接地址,不用設置即可 詳見=> balancer_conn_wrappers => updateClientConnState
				ServerName:service.Service.Service,
			})
		}
		//cr.cc.NewAddress(newAddrs)
		//cr.cc.NewServiceConfig(cr.name)
		cr.cc.UpdateState(resolver.State{Addresses:newAddrs})
	}

}

func (cb *consulBuilder) Scheme() string {
	return "consul"
}

func (cr *consulResolver) ResolveNow(opt resolver.ResolveNowOption) {
	cr.Ch <- 1
}

func (cr *consulResolver) Close() {
}

func parseTarget(target string) (host, port, name string, err error) {

	if target == "" {
		return "", "", "", errMissingAddr
	}

	if !regexConsul.MatchString(target) {
		return "", "", "", errAddrMisMatch
	}

	groups := regexConsul.FindStringSubmatch(target)
	host = groups[1]
	port = groups[2]
	name = groups[3]
	if port == "" {
		port = defaultPort
	}
	return host, port, name, nil
}

grpc_client.go
package microservice

import (
	"fmt"
	"google.golang.org/grpc/balancer/roundrobin"

	//_ "github.com/mbobakov/grpc-consul-resolver" // It's important
	"google.golang.org/grpc"
	"library/util/constant"
)

func GetConsulHost() string {
	if constant.Env == constant.Dev {
		return "192.168.1.118:8500"
		//return "192.168.8.103:8079"
	}else{
		return "192.168.45.10:8500"
	}
}

type GrpcClient struct {
	Conn 		*grpc.ClientConn
	RpcTarget   string
	Name   		string
}

func (s *GrpcClient)RunGrpcClient(){
	conn, err := grpc.Dial(s.RpcTarget, grpc.WithInsecure())
	if err != nil {
		fmt.Println(err)
		return
	}
	s.Conn = conn
	fmt.Println("grpc client start success")
}
func (s *GrpcClient)RunConsulClient(){
	//初始化 resolver 實例
	Init()
	conn, err := grpc.Dial(
		fmt.Sprintf("%s://%s/%s", "consul", GetConsulHost(), s.Name),
		//不能block => blockkingPicker打開,在調用輪詢時picker_wrapper => picker時若block則不進行robin操作直接返回失敗
		//grpc.WithBlock(),
		grpc.WithInsecure(),
		//指定初始化round_robin => balancer (後續可以自行定製balancer和 register、resolver 同樣的方式)
		grpc.WithBalancerName(roundrobin.Name),
		//grpc.WithDefaultServiceConfig(`{"loadBalancingConfig": [{"round_robin":{}}]}`),
	)
	if err != nil {
		fmt.Println("dial err:", err)
		return
	}
	s.Conn = conn
	fmt.Println(fmt.Sprintf("gRpc consul client [%s] start success", s.Name))
}
使用(自己封裝的)
financeClient := &microservice.GrpcClient{Name: "finance"}
	financeClient.RunConsulClient()
	ssvr := settlement.NewSettlementClient(financeClient.Conn)
//ssvr就是rpc的實例,ssvr.SayHello(ctx ...) 類似的就可以調用函數了
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章