微服务注册与监听

etcd架构

ectd是etc distributed的缩写。在Linux中的etc目录存储了系统的配置文件,所以etcd代表了分布式的配置中心系统。然而,它能够实现的功能远不是同步配置文件这么简单。etcd可以作为分布式协调的组件帮助我们实现分布式系统。使用etcd的重要项目包括coreos与k8s。etcd使用了go语言编写,底层使用了Raft协议。

etcd全局架构与原理

etcd抽象出了raft-http模块,由于etcd通常以分布式集群的方式部署,因此该层主要负责处理和其他etcd节点之间的网络通信。etcd内部使用了http进行通信,由于消息类型很多,心跳探活的数据量较小,快照信息较大(可达GB级别),所以有两种处理消息的通道,分别是Pipeline消息和Stream消息通道。

Stream 消息通道是etcd中节点与节点之间进行维护的长连接,它可以处理频蒙的小消息,还能复用HTTP底层的连接,不必每次通信都建立新的连接。而Pipeline消息通道用于处理快照这样数据量大的信息,处理完毕后连接会关闭。

etcd-raft模块,它是etcd的核心。该层实现了PRaft协议,可以实现节点状态的转移、节点的选举、数据处理等重要功能,确保分布式系统的一致性与故障容错性。我们已经知道,Raft中的节点有3种状态,分别是领导(Leader)、候选人(Candidates)和跟随者(Follower),在此基础上,etcd为Raft节点新增了一个预候选人(PreCandidate)。

Raft协议:如果节点收不到来自Leader的心跳检测,就会变为Candidates并开始新的选举。如果当前节点位于不足半数的网络分区中,短期内就不会影响集群的使用,但是在当前节点

不断发起选举的过程中,当前选举周期的Term号会不断增长,当网络分区消失后,由于该节点的 Term号高于当前集群中Leader节点的Term号,Raft协议会迫使当前的Leader 切换状态并开始新一轮的选举。

这种选举是没有意义的。为了解决这样的问题,etcd在选举之前插入了一个新的阶段叫作 PreVote,当前节点会先尝试连接集群中的其他节点,只有成功连接半数以上的节点,才开始新一轮的选举。

在etcd-raft模块的基础上,etcd进一步封装了raft-node模块。该模块充当上层模块与下层Raft模块之间的桥梁;并负责调用Storage模块,将记录(Record)存储到WAL日志文件以实现持久化。WAL日志文件可存储以下几种类型的记录。

  • WAL文件的元数据,记录节点ID、集群 ID 信息。
  • Entry记录,即客户端发送给服务器处理的数据。
  • 集群的状态信息,包含集群的任期号、节点投票信息。
  • 数据校验信息,可以校验文件数据的完整性与正确性。
  • 快照信息,包含快照的相关信息,但不包含实际的快照数据,可以校验快照数据的完整性。

WAL 日志文件对于系统的稳定性至关重要,因为它在记录应用到状态机之前,确保了大多数节点已达成一致并实现了记录的持久化。这样,在节点崩溃并重启后,就能从WAL中恢复数据了。

WAL日志的数量与大小随着时间不断增加,可能超过可容纳的磁盘容量。同时,在节点宕机后,如果要恢复数据就必须从头到尾读取WAL 日志文件,耗时非常久。为了解决这一问题,etcd 会定期创建快照并保存到文件中,在恢复节点时会先加载快照数据,并从快照所在的位置后读取WAL文件,这就加快了节点的恢复速度。快照的数据也有单独的SNAP模块进行管理。

在raft-node模块之上是etcd-server模块,它的核心任务是执行Entry 对应的操作,在这个过程中提供限流操作与权限控制的能力,这些操作最终会使状态机到达最新的状态。etcd-server还会维护当前etcd集群的状态信息,并提供线性读的能力。

etcd-server模块为外部访问提供了一系列GRPC API。同时,利用GRPC-gateway作为反向代理,使得etcd-server模块具备向外部提供HTTPAPI的能力。

最后,etcd提供了客户端工具etcdctl和clientv3代码库,使用GRPC协议与etcd服务器交互。客户端支持负载均衡、节点间故障自动转移等机制,极大降低了业务使用etcd的难度,提升了开发的效率。

此外,etcd框架中还有一些辅助功能,例如权限管理、限流管理、重试、GRPC拦截器等。

etcd架构的优点

  1. 高内聚
  2. 低耦合:各个模块之间边界清晰,用接口进行交流与组合的设计给了程序极大的扩展性。
  3. 优雅的数据同步:在etcd中,极少使用互斥锁。更多的时候,它是借助协程与通道相互配合来传递信息,即完成了通信又优雅地解决了并发安全问题。
  4. 更高的读取性能:etcd实现了ReadIndex机制,Follower从Leader读取当前最新的Commit Index,同时Leader需要确保自己没有被未知的新Leader取代。它会发出新一轮的心跳,并等待集群中大多数节点确认,一旦收到确认信息,Leader就知道在心跳信息发出的那一刻,不可能存在更新的Leader了。也就是说,在那一刻,ReadIndex是集群中所有节点见过的最大的Commit Index。Follower会在自己的状态机上将日志至少执行到该Commit Index之后,然后查询当前状态机生成的结果,并将结果返回客户端。
  5. 可靠的Watch机制与高性能的并发处理:etcdv3版本将所有键值对的历史版本都存储了起来,这就让Watch机制的可靠性更高了,它实现了MVCC机制页提高了系统处理并发请求的数量。

GRPC与Protocol Buffers

在微服务的远程通信中,通常选择GRPC协议或遵循RESTful风格的HTTP,GRPC具有以下优势。

  • 使用http/2来传输序列化后的二进制信息,传输速度更快。
  • 可以为不同的语言生成对应的client库,外部访问非常便利。
  • 使用Protocal Buffers定义API的行为,提供了强大的序列化与反序列化能力。
  • 支持双向的流式传输。

GRPC默认使用Protocol Buffers来定义接口,其特点如下:

  • 提供了与语言、框架无关的序列化与反序列化能力。
  • 序列化生成的字节数组比JSON更小,同时序列化与反序列化的速度也比JSON更快。
  • 有良好的向后与向前兼容性。

Protocol Buffers将接口语言定义在以.proto为后缀的文件中,proto编译器结合特定语言的运行库生成特定的SDK文件,这个SDK文件有助于我们在Client端访问,也有助于生成GRPC Server。

下面,通过一个案例来学习如何使用Protocol Buffers。

第一步,编写一个简单的文件hello.proto。

clike 复制代码
syntax = "proto3";
option go_package="proto/greeter";

service Greeter{
  rpc Hello(Request) returns (Response){}
}

message Request{
  string name = 1;
}

message Response{
  string greeting=2;
}
  • syntax = "proto3";标识协议的版本,每个版本的语言可能有所不同,目前最新的使用最多的版本是proto3。
  • option go_package定义生成的package名。
  • service Greeter定义了一个服务Greeter,它的远程方法为Hello,Hello参数为结构体Request,返回值为结构体Response。

根据proto文件生成对应的协议文件需要首先完成前置工作:下载proto的编译器protoc,protoc指定版本的安装方式可以查看这篇博客:https://blog.csdn.net/qq_61635026/article/details/130780919

此外,还需要安装protoc的go语言插件。

clike 复制代码
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

第二步,运行protoc命令进行编译。编译完成后,将生成hello.pb.go和hello_grpc.go两个协议文件。编译命令如下:

clike 复制代码
protoc -I . --go_out=. --go-grpc_out=. hello.proto

在hello_grpc.pb.go文件中,我们可以看到protoc自动生成了GreeterServer接口,其中包含了Hello方法。

第三步,在main函数中生成结构体Greeter,实现GreeterServer接口,然后调用协议文件中的pb.RegisterGreeterServer将Greeter注册到GRPC server中,代码如下:

clike 复制代码
package main

import (
	"context"
	pb "crawler/proto/greeter/proto/greeter"
	"google.golang.org/grpc"
	"log"
	"net"
)

type Greeter struct {
	pb.UnimplementedGreeterServer
}

func (g *Greeter) Hello(ctx context.Context, req *pb.Request) (rsp *pb.Response, err error) {
	rsp.Greeting = "Hello " + req.Name
	return rsp, nil
}

func main() {
	listener, err := net.Listen("tcp", ":9990")
	s := grpc.NewServer()
	pb.RegisterGreeterServer(s, &Greeter{})
	if err = s.Serve(listener); err != nil {
		log.Fatalf("failed to server: %v", err)
	}
}

至此,我们就生成了一个GRPC服务,该服务提供了Hello方法。

go-micro与go-gateway

上述代码是使用原生的生成GRPC服务器的方法。接下来我们使用一个目前微服务领域比较流行的框架go-micro来实现GRPC服务器。

相比原生的方式,go-micro拥有更丰富的生态和功能,更方便的工具和API。例如,在go-micro中,服务注册可以方便地切换到etcd、zookeeper、gossip等注册中心,方便开发者实现服务注册功能。server端同时支持grpc、http等协议。

要在go-micro中实现grpc服务器,同样需要利用前面地proto文件生成的协议文件。不过go-micro在此基础上进行了扩展,需要下载protoc-gen-micro插件来生成micro适用的协议文件。这个插件的版本需要和go-micro的版本相同。目前,最新的go-micro版本为v4。

clike 复制代码
go install github.com/asim/go-micro/cmd/protoc-gen-micro/v4@latest

接着输入如下命名,生成一个新的文件hello.pb.micro.go

clike 复制代码
protoc -I . --micro_out=. --go_out=. --go-grpc_out=. hello.proto

在hello.pb.micro.go中,micro生成了一个接口GreeterHandler,所以我们需要在代码中实现这个新的接口。

clike 复制代码
package main

import (
	"context"
	pb "crawler/proto/greeter/proto/greeter"
	"go-micro.dev/v4"
	"log"
)

type Greeter1 struct {
}

func (g *Greeter1) Hello(ctx context.Context, req *pb.Request, rsp *pb.Response) (err error) {
	rsp.Greeting = "Hello " + req.Name
	return nil
}

func main() {
	service := micro.NewService(
		micro.Name("helloworld"),
	)

	service.Init()
	pb.RegisterGreeterHandler(service.Server(), new(Greeter1))
	if err := service.Run(); err != nil {
		log.Fatal(err)
	}
}

GRPC的调试比http要繁琐,有些外部服务可能不支持适用GRPC,为了解决这些问题,可以让服务同时具备GRPC和HTTP的能力。

要实现这一目标,需要借助一个第三方库------grpc-gateway。grpc-gateway的功能就是生成一个http的代理服务,这个http代理服务会将http请求转换为GRPC协议,并转发到GRPC服务器中。这样,服务便能同时提供http接口和grpc接口。

  1. 修改.proto文件,引入依赖"google/api/annotations.proto",并且加入了自定义的option选项,grpc-gateway插件会识别这个自定义选项,并为我们生成http代理服务。

    go 复制代码
    syntax = "proto3";
    option go_package="proto/greeter";
    import "google/api/annotations.proto";
    
    service Greeter{
      rpc Hello(Request) returns (Response){
        option(google.api.http)={
          post :"/greeter/hello"
          body:"*"
        };
      }
    }
    
    message Request{
      string name = 1;
    }
    
    message Response{
      string greeting=2;
    }
  2. 安装grpc-gateway插件。

    go 复制代码
    go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest
    go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest

    同时,提前下载依赖文件google/api/annotations.proto。这里,我们手动下载依赖文件并放入GOPATH中。

    go 复制代码
    git clone https://github.com/googleapis/googleapis
    mv googleapis/google $(go env GOPATH)/src/google

    最后,使用下面的指令将proto文件生成协议文件。注意,这里同时加入了go-micro插件和grpc-gateway插件,两个插件之间可能存在命名冲突,所以指定了grpc-gateway的选项register_func_suffix为Gw,它能够让生成的函数名包含该Gw前缀,解决命名冲突的问题。

    go 复制代码
    protoc -I $env:GOPATH/src -I $env:GOPATH/src/google/api -I . --micro_out=. --go_out=. --go-grpc_out=. --grpc-gateway_out=logtostderr=true,register_func_suffix=Gw:. hello.proto

    这样就生成了4个文件,分别是hello.pb.go、hello.pb.gw.go、hello.pb.micro.go、hello_grpc.pb.go。其中,hello.pb.gw.go为grpc-gateway插件生成的文件。

go 复制代码
package main

import (
	"context"
	"crawler/log"
	pb "crawler/proto/greeter/proto/greeter"
	"fmt"
	gs "github.com/go-micro/plugins/v4/server/grpc"
	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	"go-micro.dev/v4"
	"go-micro.dev/v4/server"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"google.golang.org/grpc"
	"net/http"
)

type Greeter1 struct {
}

func (g *Greeter1) Hello(ctx context.Context, req *pb.Request, rsp *pb.Response) (err error) {
	rsp.Greeting = "Hello " + req.Name
	return nil
}

func main() {

	plugin := log.NewStdoutPlugin(zapcore.DebugLevel)
	logger := log.NewLogger(plugin)
	logger.Info("log init end")

	// set zap global logger
	zap.ReplaceGlobals(logger)
	go HandleHTTP2()
	service := micro.NewService(
		micro.Name("go.micro.server.worker"),
		micro.Server(gs.NewServer()),
		micro.Address(":9090"),
		micro.WrapHandler(logWrapper(logger)),
	)

	service.Init()
	pb.RegisterGreeterHandler(service.Server(), new(Greeter1))
	if err := service.Run(); err != nil {
		logger.Error("========")
	}

}

func logWrapper(log *zap.Logger) server.HandlerWrapper {
	return func(handlerFunc server.HandlerFunc) server.HandlerFunc {
		return func(ctx context.Context, req server.Request, rsp interface{}) error {
			log.Info("recieve request", zap.String("method", req.Method()),
				zap.String("Service", req.Service()),
				zap.Reflect("request param:", req.Body()))
			err := handlerFunc(ctx, req, rsp)
			return err
		}
	}
}

func HandleHTTP2() {
	ctx := context.Background()
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	mux := runtime.NewServeMux()
	opts := []grpc.DialOption{grpc.WithInsecure()}

	err := pb.RegisterGreeterGwFromEndpoint(ctx, mux, "localhost:9090", opts)
	if err != nil {
		fmt.Println(err)
	}

	http.ListenAndServe(":8080", mux)
}

接下来,使用http服务服务:

注册中心与etcd

部署etcd服务器,笔者使用docker来部署etcd服务器,docker-compose如下:

go 复制代码
version: "3.8"
services:
  etcd:
    image: bitnami/etcd:3.4.24
    volumes:
      - /tmp/etcd:/etcd-data
    ports:
      - '2379:2379'
      - '2380:2380'
    expose:
      - 2379
      - 2380
    networks:
      counter-net:
    environment:
      - ETCDCTL_API=3
      # ⚠️ 注意:如果你希望你的 etcd 以单节点运行,需要调整这些参数。
      # Bitnami 镜像会使用这些环境变量来构建启动命令。
      - ETCD_NAME=etcd
      - ETCD_DATA_DIR=/etcd-data
      - ETCD_INITIAL_ADVERTISE_PEER_URLS=http://0.0.0.0:2380
      - ETCD_LISTEN_PEER_URLS=http://0.0.0.0:2380
      - ETCD_ADVERTISE_CLIENT_URLS=http://0.0.0.0:2379
      - ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379
      - ETCD_INITIAL_CLUSTER=etcd=http://0.0.0.0:2380
      - ETCD_INITIAL_CLUSTER_STATE=new
      - ETCD_INITIAL_CLUSTER_TOKEN=tkn
      - ALLOW_NONE_AUTHENTICATION=yes
    # healthcheck:
    #   # ⚠️ 最好使用 /opt/bitnami/scripts/etcd/healthcheck.sh 或 /opt/bitnami/etcd/bin/etcdctl
    #   # 这里先保留您的配置,但请注意如果失败可能需要调整路径
    #   test: ["CMD", "/usr/local/bin/etcdctl" ,"get", "--prefix", "/"] 
    #   interval: 5s
    #   timeout: 5s
    #   retries: 55

networks:
  counter-net:
    driver: bridge
go 复制代码
package main

import (
	"context"
	"crawler/log"
	pb "crawler/proto/greeter/proto/greeter"
	"fmt"
	etcdReg "github.com/go-micro/plugins/v4/registry/etcd"
	gs "github.com/go-micro/plugins/v4/server/grpc"
	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	"go-micro.dev/v4"
	"go-micro.dev/v4/registry"
	"go-micro.dev/v4/server"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"google.golang.org/grpc"
	"net/http"
)

type Greeter1 struct {
}

func (g *Greeter1) Hello(ctx context.Context, req *pb.Request, rsp *pb.Response) (err error) {
	rsp.Greeting = "Hello " + req.Name
	return nil
}

func main() {

	plugin := log.NewStdoutPlugin(zapcore.DebugLevel)
	logger := log.NewLogger(plugin)
	logger.Info("log init end")
	//注册etcd服务器
	reg := etcdReg.NewRegistry(registry.Addrs("8.134.171.198:2379"))

	// set zap global logger
	zap.ReplaceGlobals(logger)
	go HandleHTTP2()
	service := micro.NewService(
		micro.Name("go.micro.server"),
		micro.Server(gs.NewServer()),
		micro.Address(":9090"),
		micro.Registry(reg),
		micro.WrapHandler(logWrapper(logger)),
	)

	service.Init()
	pb.RegisterGreeterHandler(service.Server(), new(Greeter1))
	if err := service.Run(); err != nil {
		logger.Error("========")
	}

}

func logWrapper(log *zap.Logger) server.HandlerWrapper {
	return func(handlerFunc server.HandlerFunc) server.HandlerFunc {
		return func(ctx context.Context, req server.Request, rsp interface{}) error {
			log.Info("recieve request", zap.String("method", req.Method()),
				zap.String("Service", req.Service()),
				zap.Reflect("request param:", req.Body()))
			err := handlerFunc(ctx, req, rsp)
			return err
		}
	}
}

func HandleHTTP2() {
	ctx := context.Background()
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	mux := runtime.NewServeMux()
	opts := []grpc.DialOption{grpc.WithInsecure()}

	err := pb.RegisterGreeterGwFromEndpoint(ctx, mux, "localhost:9090", opts)
	if err != nil {
		fmt.Println(err)
	}

	http.ListenAndServe(":8080", mux)
}

然后我们先执行上述rpc服务,然后创建一个rpc客户端,访问rpc服务暴露出来的Hello方法。

go 复制代码
package main

import (
	"context"
	pb "crawler/proto/greeter/proto/greeter"
	"fmt"
	"github.com/go-micro/plugins/v4/client/grpc"
	"github.com/go-micro/plugins/v4/registry/etcd"
	"go-micro.dev/v4"
	"go-micro.dev/v4/registry"
)

func main() {
	reg := etcd.NewRegistry(registry.Addrs("8.134.171.198:2379"))

	service := micro.NewService(
		micro.Registry(reg),
		micro.Client(grpc.NewClient()))

	service.Init()

	cl := pb.NewGreeterService("go.micro.server", service.Client())

	rsp, _ := cl.Hello(context.Background(), &pb.Request{
		Name: "John",
	})

	fmt.Sprintf("rsp is %v,", rsp.String())

}

执行结果如下:

相关推荐
ascarl201037 分钟前
Kubernetes 环境 NFS 卡死问题排查与解决纪要
云原生·容器·kubernetes
未来魔导41 分钟前
go语言中json操作总结(下)
数据分析·go·json
jinxinyuuuus1 小时前
局域网文件传输:WebRTC与“去中心化应用”的架构思想
架构·去中心化·webrtc
阿里云云原生1 小时前
快速构建企业 AI 开放平台,HiMarket 重磅升级
云原生
狗哥哥1 小时前
从零到一:打造企业级 Vue 3 高性能表格组件的设计哲学与实践
前端·vue.js·架构
小马哥编程1 小时前
【软考架构】滑动窗口限流算法的原理是什么?
java·开发语言·架构
西格电力科技2 小时前
面向工业用户的绿电直连架构适配技术:高可靠与高弹性的双重设计
大数据·服务器·人工智能·架构·能源
北邮刘老师3 小时前
【智能体互联协议解析】ACPs/AIP为什么还在用“落后”的“中心化”架构?
网络·人工智能·架构·大模型·智能体·智能体互联网
神奇小汤圆3 小时前
上下文协议(MCP)Java SDK 指南
架构
pingzhuyan3 小时前
微服务: springboot整合kafka实现消息的简单收发(上)
spring boot·微服务·kafka