本文是个人在学习书籍《Go语言高并发与微服务实战》第6章内容过程中,而动手做的小实验,涉及consul和go-kid相关知识,仅供参考。

安装consul

到consul官网,找到适合你系统的包并下载,解压后拷贝consul到你的PATH路径中即可。
https://www.consul.io/downloads

在Unix系统中 ~/bin/usr/local/bin 是通常的安装目录。
在Windows系统中可以安装到 %PATH% 的路径中。

consul的简单使用

单机启动

consul agent -dev

启动后可打开浏览器查看:http://127.0.0.1:8500/

集群启动

主机器1:consul agent -server -bootstrap-expect 1 -data-dir /tmp/consul -node=hdp2 -bind=10.0.0.52
从机器2:consul agent -data-dir /tmp/consul -node=hdp3 -bind=10.0.0.53
加入集群:consul join 10.0.0.52

查询服务

集群成员:consul members
DNS API: dig @127.0.0.1 -p 8600 web.service.consul
HTTP API: curl http://localhost:8500/v1/catalog/service/web

编写go-kit服务

层级划分

go-kit的架构分为三层结构:Transport层,Endpoint层,Service层。

  • Transport层主要负责与传输协议HTTP,GRPC,THRIFT等相关的逻辑
  • Endpoint层主要负责 request/response 格式的转换,以及公用拦截器相关的逻辑;
  • Service层则专注于业务逻辑。

服务端实现

大致逻辑如下:

  • 服务发现的客户端连接consul
  • 声明service层对象
  • 通过service层对象,创建各个endpoint层对象
  • transport层创建http路由处理,跟endpoint层映射关系
  • 向consul进行服务注册
  • 开启http端口的监听服务
  • 程序退出时向consul取消注册

main.go

package main

import (
	"context"
	"flag"
	"fmt"
	"github.com/longjoy/micro-go-book/ch6-discovery/config"
	"github.com/longjoy/micro-go-book/ch6-discovery/discover"
	"github.com/longjoy/micro-go-book/ch6-discovery/endpoint"
	"github.com/longjoy/micro-go-book/ch6-discovery/service"
	"github.com/longjoy/micro-go-book/ch6-discovery/transport"
	uuid "github.com/satori/go.uuid"
	"net/http"
	"os"
	"os/signal"
	"strconv"
	"syscall"
)

func main() {

	// 从命令行中读取相关参数,没有时使用默认值
	var (
		// 服务地址和服务名
		servicePort = flag.Int("service.port", 10086, "service port")
		serviceHost = flag.String("service.host", "127.0.0.1", "service host")
		serviceName = flag.String("service.name", "SayHello", "service name")
		// consul 地址
		consulPort = flag.Int("consul.port", 8500, "consul port")
		consulHost = flag.String("consul.host", "127.0.0.1", "consul host")
	)

	flag.Parse()

	ctx := context.Background()
	errChan := make(chan error)

	// 声明服务发现客户端
	var discoveryClient discover.DiscoveryClient

	discoveryClient, err := discover.NewKitDiscoverClient(*consulHost, *consulPort)
	// 获取服务发现客户端失败,直接关闭服务
	if err != nil{
		config.Logger.Println("Get Consul Client failed")
		os.Exit(-1)
	}

	// 声明并初始化 Service
	var svc = service.NewDiscoveryServiceImpl(discoveryClient)

	// 创建打招呼的Endpoint
	sayHelloEndpoint := endpoint.MakeSayHelloEndpoint(svc)
	// 创建服务发现的Endpoint
	discoveryEndpoint := endpoint.MakeDiscoveryEndpoint(svc)
	//创建健康检查的Endpoint
	healthEndpoint := endpoint.MakeHealthCheckEndpoint(svc)

	endpts := endpoint.DiscoveryEndpoints{
		SayHelloEndpoint:		sayHelloEndpoint,
		DiscoveryEndpoint:		discoveryEndpoint,
		HealthCheckEndpoint:	healthEndpoint,
	}

	//创建http.Handler
	r := transport.MakeHttpHandler(ctx, endpts, config.KitLogger)
	// 定义服务实例ID
	instanceId := *serviceName + "-" + uuid.NewV4().String()
	// 启动 http server
	go func() {
		config.Logger.Println("Http Server start at port:" + strconv.Itoa(*servicePort))
		//启动前执行注册
		if !discoveryClient.Register(*serviceName, instanceId, "/health", *serviceHost,  *servicePort, nil, config.Logger){
			config.Logger.Printf("string-service for service %s failed.", serviceName)
			// 注册失败,服务启动失败
			os.Exit(-1)
		}
		handler := r
		errChan <- http.ListenAndServe(":"  + strconv.Itoa(*servicePort), handler)
	}()

	go func() {
		// 监控系统信号,等待 ctrl + c 系统信号通知服务关闭
		c := make(chan os.Signal, 1)
		signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
		errChan <- fmt.Errorf("%s", <-c)
	}()

	error := <-errChan
	//服务退出取消注册
	discoveryClient.DeRegister(instanceId, config.Logger)
	config.Logger.Println(error)
}

完整代码详见附录的配套代码 ch6-discovery ,这里就不完整展示了。

运行效果

# go run main.go
2021/07/16 16:45:46 Http Server start at port:10086
2021/07/16 16:45:46 Register Service Success!

consul后台会看到SayHello服务已注册,使用查询指令也能正常查到结果。

# dig @127.0.0.1 -p 8600 SayHello.service.consul SRV

; <<>> DiG 9.10.6 <<>> @127.0.0.1 -p 8600 SayHello.service.consul SRV
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 881
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 3
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;SayHello.service.consul.	IN	SRV

;; ANSWER SECTION:
SayHello.service.consul. 0	IN	SRV	1 1 10086 7f000001.addr.dc1.consul.

;; ADDITIONAL SECTION:
7f000001.addr.dc1.consul. 0	IN	A	127.0.0.1
yisonlideMacBook-Pro.local.node.dc1.consul. 0 IN TXT	"consul-network-segment="

;; Query time: 1 msec
;; SERVER: 127.0.0.1#8600(127.0.0.1)
;; WHEN: Fri Jul 16 16:51:51 CST 2021
;; MSG SIZE  rcvd: 183

# curl http://localhost:8500/v1/catalog/service/SayHello 
[
    {
        "ID": "7e80199a-e09b-018f-c0c9-f35d36445717",
        "Node": "yisonlideMacBook-Pro.local",
        "Address": "127.0.0.1",
        "Datacenter": "dc1",
        "TaggedAddresses": {
            "lan": "127.0.0.1",
            "lan_ipv4": "127.0.0.1",
            "wan": "127.0.0.1",
            "wan_ipv4": "127.0.0.1"
        },
        "NodeMeta": {
            "consul-network-segment": ""
        },
        "ServiceKind": "",
        "ServiceID": "SayHello-5f93a8c1-f3c5-44cb-a996-96ef6de69358",
        "ServiceName": "SayHello",
        "ServiceTags": [],
        "ServiceAddress": "127.0.0.1",
        "ServiceTaggedAddresses": {
            "lan_ipv4": {
                "Address": "127.0.0.1",
                "Port": 10086
            },
            "wan_ipv4": {
                "Address": "127.0.0.1",
                "Port": 10086
            }
        },
        "ServiceWeights": {
            "Passing": 1,
            "Warning": 1
        },
        "ServiceMeta": {},
        "ServicePort": 10086,
        "ServiceEnableTagOverride": false,
        "ServiceProxy": {
            "MeshGateway": {},
            "Expose": {}
        },
        "ServiceConnect": {},
        "CreateIndex": 22,
        "ModifyIndex": 22
    }
]

检查服务功能

# curl http://127.0.0.1:10086/say-hello
{"message":"Hello World!"}

# curl http://127.0.0.1:10086/discovery
{"error":"invalid request parameter"}

# curl http://127.0.0.1:10086/discovery?serviceName=SayHello
{"instances":[{"ID":"SayHello-5f93a8c1-f3c5-44cb-a996-96ef6de69358","Service":"SayHello","Tags":[],"Meta":null,"Port":10086,"Address":"127.0.0.1","TaggedAddresses":{"lan_ipv4":{"Address":"127.0.0.1","Port":10086},"wan_ipv4":{"Address":"127.0.0.1","Port":10086}},"Weights":{"Passing":1,"Warning":1},"EnableTagOverride":false,"CreateIndex":22,"ModifyIndex":22,"Proxy":{"MeshGateway":{},"Expose":{}},"Connect":{}}],"error":""}

总结

服务注册与发现,原理看起来没有太复杂,也是使用 服务端+客户端 进行通信,使用一致性算法Raft或ZAB保证服务;
如果单单只是看书、只是看原理,也许能理解,但或许不太深刻;只有动手实践后,你才明白其中的意义。
虽然只试验了其中一种组件,但这时再回过头来看各个服务注册发现的组件对比,或者做架构选型时,就清晰多了。

附录

关于我

name: yison.li
blog: http://yyeer.com
github: https://github.com/yisonli