go 进阶 go-zero相关: 四. 服务注册原理

一. 基础

  1. go-zero是一个集成了Web和RPC协议的服务框架,RPC也就是指go-zero的zRPC部分, zRPC底层依赖gRPC,内置了服务注册、负载均衡、拦截器等模块,其中还包括自适应降载,自适应熔断,限流等微服务治理方案,是一个简单易用的可直接用于生产的企业级RPC框架,我们在这里先了解一个服务治理这部分,怎么实现服务注册, 服务发现,服务更新的
  2. zRPC支持直连和基于etcd服务发现两种方式,
  3. zRPC中可以分成一下几个模块
  1. client: zRPC客户端,负责发起请求
  2. server: zRPC服务端,负责处理请求
  3. resolver: 服务注册模块,实现了gRPC的resolver.Builder接口并注册到gRPC
  4. discov: 服务发现模块,基于etcd实现服务发现功能
  5. balancer: 负载均衡模块,实现了p2c负载均衡算法,并注册到gRPC
  6. interceptor: 拦截器,对请求和响应进行拦截处理
  1. 在我们编写zrpc服务时,通常一个zrpc服务端,对应一个zrpc客户端, 在配置文件中设置etcd地址,将服务注册到etcd上,配置文件如下, 当服务端启动时会连接etcd将当前服务的服务名,服务地址保存到etcd, 当客户端启动时也会注册,同时会获取需要的服务端调用地址列表保存到本地
Name: hello.rpc           // 服务名
ListenOn: 127.0.0.1:9090  // 服务监听地址
Etcd:
  Hosts:
    - 127.0.0.1:2379      // etcd服务地址
  Key: hello.rpc          // 服务注册key
  1. 当客户端发起调用时会基于服务发现模块获取目标服务地址列表,负载均衡选择指定地址进行调用,并且基于拦截器统计请求结果,实现熔断限流等功能
  2. 接下来我们看一下服务注册,发现的源码,了解一下具体的执行流程

二. resolver 服务注册底层原理

  1. 参考博客
  2. 首先go-zero中rpc服务发现支持三种模式,当前只讨论服务发现模式
//直连模式
DirectScheme = "direct"
//服务发现模式
DiscovScheme = "discov"
//k8s模式
KubernetesScheme = "k8s"
  1. 编写rpc服务端时,需要配置etcd地址,设置当前服务名,服务启动时将当前服务调用地址保存到etcd,在上方已经做了配置文件的示例, 接下来看一下启动zrpc服务端代码
  1. 首先会读取配置文件,获取到当前服务的配置信息与etcd连接配置,
  2. 创建服务运行需要的Context对象,
  3. 通过proto生成的RegisterXXXServer(),将当前rpc服务实例注册到rpc服务器,
  4. 执行zrpc.MustNewServer()创建RpcServer,
  5. 调用RpcServer下的Start()启动服务
import (
	"flag"
	"fmt"
	"go_cloud_demo/rpc/internal/config"
	"go_cloud_demo/rpc/internal/server"
	"go_cloud_demo/rpc/internal/svc"
	"go_cloud_demo/rpc/types/user"
	"github.com/zeromicro/go-zero/core/conf"
	"github.com/zeromicro/go-zero/core/service"
	"github.com/zeromicro/go-zero/zrpc"
	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"
)

// 命令行参数读取配置文件所在路径
var configFile = flag.String("f", "rpc/etc/user.yaml", "the config file")

func main() {
	flag.Parse()
	//1.读取配置文件解析到Config结构体上
	var c config.Config
	conf.MustLoad(*configFile, &c)
	//2.创房服务运行上下文
	ctx := svc.NewServiceContext(c)

	//3.将服务注册到rpc服务器,并且监听指定端口启动服务
	//参数一"c.RpcServerConf":保存了当前rpc服务配置信息
	//参数二"func(grpcServer *grpc.Server)"一个函数,当执行该函数时
	//会调用通过proto生成的RegisterXXXServer(),将当前rpc服务实例注册到rpc服务器
	s := zrpc.MustNewServer(c.RpcServerConf,
		func(grpcServer *grpc.Server) {
			user.RegisterUserServer(grpcServer, server.NewUserServer(ctx))

			if c.Mode == service.DevMode || c.Mode == service.TestMode {
				reflection.Register(grpcServer)
			}
		})
	defer s.Stop()

	fmt.Printf("Starting rpc server at %s...\n", c.ListenOn)
	//4.启动
	s.Start()
}
  1. 了解服务注册中的可以分为两个步骤,分别是
  1. 执行MustNewServer()创建rpcServer时内部创建registerEtcd函数,并将该函数封装到keepAliveServer结构体中
  2. 执行Start()启动服务时,内部获取到keepAliveServer中保存的registerEtcd函数并执行,实现服务注册

1. 创建registerEtcd函数,并将该函数封装到keepAliveServer结构体中

  1. 我们看一下zrpc.MustNewServer()创建rpcServer源码,在"zrpc/server.go"下, 在MustNewServer()中调用了NewServer()
func MustNewServer(c RpcServerConf, register internal.RegisterFn) *RpcServer {
	//调用NewServer()
	server, err := NewServer(c, register)
	if err != nil {
		log.Fatal(err)
	}
	return server
}

// NewServer returns a RpcServer.
func NewServer(c RpcServerConf, register internal.RegisterFn) (*RpcServer, error) {
	var err error
	if err = c.Validate(); err != nil {
		return nil, err
	}

	var server internal.Server
	metrics := stat.NewMetrics(c.ListenOn)
	serverOptions := []internal.ServerOption{
		internal.WithMetrics(metrics),
		internal.WithRpcHealth(c.Health),
	}
	//判断是否配置了etcd连接地址与当前服务名key,如果有
	if c.HasEtcd() {
		//执行NewRpcPubServer()
		server, err = internal.NewRpcPubServer(c.Etcd, c.ListenOn, serverOptions...)
		if err != nil {
			return nil, err
		}
	} else {
		server = internal.NewRpcServer(c.ListenOn, serverOptions...)
	}

	server.SetName(c.Name)
	//设置拦截器
	if err = setupInterceptors(server, c, metrics); err != nil {
		return nil, err
	}

	rpcServer := &RpcServer{
		server:   server,
		register: register,
	}
	if err = c.SetUp(); err != nil {
		return nil, err
	}

	return rpcServer, nil
}
  1. 在NewServer()中,首先执行c.HasEtcd()判断是否配置了etcd地址,是否配置了当前服务key服务名,如果配置了,执行NewRpcPubServer()函数,该函数中,创建了服务注册核心函数registerEtcd,并将registerEtcd封装到了keepAliveServer结构体中,执行NewRpcServer()对keepAliveServer中的Server进行赋值
func NewRpcPubServer(etcd discov.EtcdConf, listenOn string, opts ...ServerOption) (Server, error) {
	//创建一个registerEtcd 函数,
	registerEtcd := func() error {
		pubListenOn := figureOutListenOn(listenOn)
		var pubOpts []discov.PubOption
		if etcd.HasAccount() {
			pubOpts = append(pubOpts, discov.WithPubEtcdAccount(etcd.User, etcd.Pass))
		}
		if etcd.HasTLS() {
			pubOpts = append(pubOpts, discov.WithPubEtcdTLS(etcd.CertFile, etcd.CertKeyFile,
				etcd.CACertFile, etcd.InsecureSkipVerify))
		}
		//创建pubClient 
		pubClient := discov.NewPublisher(etcd.Hosts, etcd.Key, pubListenOn, pubOpts...)
		//执行pubClient.KeepAlive()方法,实现服务注册
		return pubClient.KeepAlive()
	}
	//封装到keepAliveServer结构体中
	server := keepAliveServer{
		registerEtcd: registerEtcd,
		Server:       NewRpcServer(listenOn, opts...),
	}

	return server, nil
}
  1. 继续看一下keepAliveServer的结构
  1. keepAliveServer继承了一个Server结构体,通过这个Server属性保存了服务端公共的属性方法等
  2. 并且持有一个用来实现服务注册的函数类型属性registerEtcd
  3. 查看NewRpcServer()对keepAliveServer中的Server进行赋值的函数,内部会创建一个rpcServer结构体变量
  4. rpcServer中有一个middlewares属性,用来存储服务的中间件,还有一个baseRpcServer属性用于封装一些rpc服务端的公共属性和方法,会调用newBaseRpcServer()函数初始化这个属性
  5. 查看baseRpcServer比如存在用来存储流式拦截器的streamInterceptors属性,存储一元拦截器的器的unaryInterceptors属性等等
type keepAliveServer struct {
	registerEtcd func() error
	Server
}

func NewRpcServer(addr string, middlewares ServerMiddlewaresConf, opts ...ServerOption) Server {
	var options rpcServerOptions
	for _, opt := range opts {
		opt(&options)
	}
	if options.metrics == nil {
		options.metrics = stat.NewMetrics(addr)
	}

	return &rpcServer{
		baseRpcServer: newBaseRpcServer(addr, &options),
		middlewares:   middlewares,
		healthManager: health.NewHealthManager(fmt.Sprintf("%s-%s", probeNamePrefix, addr)),
	}
}

func newBaseRpcServer(address string, rpcServerOpts *rpcServerOptions) *baseRpcServer {
	var h *health.Server
	if rpcServerOpts.health {
		h = health.NewServer()
	}
	return &baseRpcServer{
		address: address,
		health:  h,
		metrics: rpcServerOpts.metrics,
		options: []grpc.ServerOption{grpc.KeepaliveParams(keepalive.ServerParameters{
			MaxConnectionIdle: defaultConnectionIdleDuration,
		})},
	}
}

type baseRpcServer struct {
	//服务监听的地址,例如":8080"
	address            string
	//用于提供grpc健康检查服务
	health             *health.Server
	//用于收集和上报服务的统计信息,例如请求数、响应时间、错误数等
	metrics            *stat.Metrics
	//用于存储创建grpc.Server对象时所需的配置数据
	options            []grpc.ServerOption
	//用于存储添加的流拦截器
	streamInterceptors []grpc.StreamServerInterceptor
	//于存储添加的一元拦截器
	unaryInterceptors  []grpc.UnaryServerInterceptor
}
  1. 再看一下服务端启动时添加的拦截器,在NewServer()内部的NewRpcPubServer()执行完毕keepAliveServer创建成功,并且内部持有一个用实现服务注册的函数,代码继续向下执行,调用了setupInterceptors()函数,该函数内部会根据配置选择注册拦截器
//setupInterceptors()作用:根据配置信息为rpc服务添加一些拦截器
//入参: svr是一个rpc服务对象
//		c是一个RpcServerConf结构体,包含了rpc服务的配置信息
//		metrics是一个stat.Metrics对象,用于收集和报告统计指标
func setupInterceptors(svr internal.Server, c RpcServerConf, metrics *stat.Metrics) error {
	//1.如果配置中指定了CpuThreshold参数,表示要开启自适应限流功能
	if c.CpuThreshold > 0 {
		// 创建一个自适应限流器对象,设置CPU阈值为配置中的值
		shedder := load.NewAdaptiveShedder(load.WithCpuThreshold(c.CpuThreshold))
		// 为rpc服务添加一个一元拦截器,用于执行限流逻辑,并记录统计指标
		svr.AddUnaryInterceptors(serverinterceptors.UnarySheddingInterceptor(shedder, metrics))
	}

	//2.如果配置中指定了Timeout参数,表示要开启超时控制功能
	if c.Timeout > 0 {
		// 为rpc服务添加一个一元拦截器,用于执行超时控制逻辑,超时时间为配置中的值
		svr.AddUnaryInterceptors(serverinterceptors.UnaryTimeoutInterceptor(
			time.Duration(c.Timeout) * time.Millisecond))
	}

	//3.如果配置中指定了Auth参数,表示要开启鉴权功能
	if c.Auth {
		// 调用setupAuthInterceptors函数,为rpc服务添加鉴权相关的拦截器
		if err := setupAuthInterceptors(svr, c); err != nil {
			return err
		}
	}
	return nil
}
  1. 继续查看用来添加拦截器的AddUnaryInterceptors()方法,最终会将拦截器保存到baseRpcServer的streamInterceptors属性,或unaryInterceptors属性中(下方源码在网上搜的,不知道是不是我的版本不对还是操作有问题在go-zero源码包中没找到…不能确认是否正确)
func (rs *RpcServer) AddUnaryInterceptors(interceptors ...grpc.UnaryServerInterceptor) {
	//调用内部的Server对象的AddUnaryInterceptors方法
	rs.server.AddUnaryInterceptors(interceptors...) 
}

// server.go
func (s *Server) AddUnaryInterceptors(interceptors ...grpc.UnaryServerInterceptor) {
	//将拦截器添加到unaryInterceptors切片中
	s.unaryInterceptors = append(s.unaryInterceptors, interceptors...) 
}

2. 执行registerEtcd函数实现服务注册

  1. 接下来我们看一下启动rpc服务的源码,RpcServer下的Start()源码
type RpcServer struct {
	server   internal.Server
	register internal.RegisterFn
}

func (rs *RpcServer) Start() {
	//默认会调用keepAliveServer下的Start()
	if err := rs.server.Start(rs.register); err != nil {
		logx.Error(err)
		panic(err)
	}
}
  1. 在RpcServer下的Start()中会调用keepAliveServer的Start()方法,该方法内会执行registerEtcd()注册服务地址到etcd
func (s keepAliveServer) Start(fn RegisterFn) error {
	//1.先执行registerEtcd()注册服务地址
	if err := s.registerEtcd(); err != nil {
		return err
	}
	//2.启动服务
	return s.Server.Start(fn)
}

3. registerEtcd 服务注册详细流程

  1. 再次查看NewRpcPubServer()中封装的registerEtcd()函数的源码
	//创建一个registerEtcd 函数,
	registerEtcd := func() error {
		pubListenOn := figureOutListenOn(listenOn)
		var pubOpts []discov.PubOption
		if etcd.HasAccount() {
			pubOpts = append(pubOpts, discov.WithPubEtcdAccount(etcd.User, etcd.Pass))
		}
		if etcd.HasTLS() {
			pubOpts = append(pubOpts, discov.WithPubEtcdTLS(etcd.CertFile, etcd.CertKeyFile,
				etcd.CACertFile, etcd.InsecureSkipVerify))
		}
		//创建pubClient 
		pubClient := discov.NewPublisher(etcd.Hosts, etcd.Key, pubListenOn, pubOpts...)
		//执行pubClient.KeepAlive()方法,实现服务发布以及连接
		return pubClient.KeepAlive()
	}
  1. 在registerEtcd 函数中会创建一个Publisher 结构体,执行pubClient的KeepAlive()方法
	Publisher struct {
		endpoints  []string //edct的地址
		key        string //该服务的在etcd中的key前缀,从配置文件中取
		//在etcd中的key(如果设置了id 则是key和id的组合,如果没有设置id,则是key和lease的组合)
		fullKey    string 
		id         int64 //如果设置了则用于生成fullKey
		value      string //etcd的值,是RPC服务的地址
		lease      clientv3.LeaseID //etcd的lease id
		quit       *syncx.DoneChan //用于退出操作
		//用于服务注册暂停.如果暂停则会注销etcd的lease,暂停后有两个后续操作 重启或者关闭
		pauseChan  chan lang.PlaceholderType 
		resumeChan chan lang.PlaceholderType //用于服务暂停后重启
	}

	// KeepAlive keeps key:value alive.
	func (p *Publisher) KeepAlive() error {
		//1.获取etcd连接
		cli, err := internal.GetRegistry().GetConn(p.endpoints)
		if err != nil {
			return err
		}
		//2.服务注册
		p.lease, err = p.register(cli)
		if err != nil {
			return err
		}

		proc.AddWrapUpListener(func() {
			p.Stop()
		})
		//4.开启续约监听
		return p.keepAliveAsync(cli)
	}
  1. 在go-zero rpc中服务的注册,续约等逻辑都是通过Publisher实现的,在Publisher上绑定了以下方法
NewPublisher()//Publish的构造方法
KeepAlive()//服务发布以及连接
Pause()//服务暂停
Resume()//服务重启
Stop()//服务停止
keepAliveAsync()//开启续约监听
  1. 最核心的方法就是KeepAlive()与keepAliveAsync(),其中KeepAlive()内部封装了doRegister()和keepAliveAsync(),而doRegister()内部调用了register()实现了服务注册(不同版本实现不同)
func (p *Publisher) KeepAlive() error {
	cli, err := p.doRegister()
	if err != nil {
		return err
	}

	proc.AddWrapUpListener(func() {
		p.Stop()
	})

	return p.keepAliveAsync(cli)
}

Publisher.register() 服务注册

  1. 查看Publisher.register()服务注册源码
func (p *Publisher) register(client internal.EtcdClient) (clientv3.LeaseID, error) {
	//1.创建etcd lease 过期时间 TimeToLive为10秒
	resp, err := client.Grant(client.Ctx(), TimeToLive)
	if err != nil {
		return clientv3.NoLease, err
	}

	lease := resp.ID
	//2.创建fullKey,如果id有设置过则以key和id生成fullKey;如果id没有设置则以key和lease id生成
	if p.id > 0 {
		p.fullKey = makeEtcdKey(p.key, p.id)
	} else {
		p.fullKey = makeEtcdKey(p.key, int64(lease))
	}
	//3.将value存储到etcd中,绑定到刚创建的lease中
	//通过Put方法进行注册
	_, err = client.Put(client.Ctx(), p.fullKey, p.value, clientv3.WithLease(lease))

	return lease, err
}
  1. 其中key为在配置文件中配置的Key,id为租约id,value为服务的地址
  2. 当服务注册成功后执行以下命令就可以获取到指定服务的地址
etcdctl get 配置文件中定义的服务key --prefix

Publisher.keepAliveAsync()续约监听(不同版本实现不同并且有些没找到源码,慎重参考)

  1. 查看Publisher.keepAliveAsync()异步地保持续约监听源码(不同版本实现不同)
// 异步地保持续约
func (p *Publisher) keepAliveAsync(cli internal.EtcdClient) error {
	//1.调用etcd客户端的KeepAlive方法,创建一个用来续约的无缓存channel,
	//该channel中保存了etcd keeplive状态,通过这个channel实现续约
	ch, err := cli.KeepAlive(cli.Ctx(), p.lease)
	if err != nil {
		return err
	}

	//2.通过threading.GoSafe()安全的启动一个goroutine来处理续约响应和事件
	threading.GoSafe(func() {
		for {
			//监听保存了etcd keeplive状态的channel,获取连接状态
			select {
			case _, ok := <-ch:
				if !ok {
					//如果通道返回false说明续约关闭,调用revoke()撤销租约,然后调用doKeepAlive()实现重试
					p.revoke(cli)
					if err := p.doKeepAlive(); err != nil {
						logx.Errorf("etcd publisher KeepAlive: %s", err.Error())
					}
					return
				}
			case <-p.pauseChan:
				//监听Publisher的pauseChan,获取是否有暂停操作	
				//如果收到暂停信号,执行revoke()撤销租约
				logx.Infof("paused etcd renew, key: %s, value: %s", p.key, p.value)
				p.revoke(cli)
				//有暂停操作时注销后,通过resumeChan和quit监听后续是重启还是停止
				select {
				case <-p.resumeChan:
					// 如果resumeChan收到恢复信号,则执行doKeepAlive()重试续约
					if err := p.doKeepAlive(); err != nil {
						logx.Errorf("etcd publisher KeepAlive: %s", err.Error())
					}
					return
				case <-p.quit.Done():
					// 如果收到退出信号,返回
					return
				}
			case <-p.quit.Done():
				//通过p.quit.Done()监听是否有停止操作,如果需要停止就直接注销当前租约	
				p.revoke(cli)
				return
			}
		}
	})
	return nil
}
  1. doKeepAlive()源码(不同版本实现不同)
// 尝试与etcd保持续约
func (p *Publisher) doKeepAlive() error {
	// 创建一个一秒间隔的定时器
	ticker := time.NewTicker(time.Second)
	defer ticker.Stop()

	for range ticker.C {
		select {
		case <-p.quit.Done():
			// 如果收到退出信号,返回nil
			return nil
		default:
			// 尝试注册服务并获取租约ID
			cli, err := p.doRegister()
			if err != nil {
				// 如果注册失败,记录错误并跳出循环
				logx.Errorf("etcd publisher doRegister: %s", err.Error())
				break
			}

			// try to keep alive the lease with etcd
			// 尝试与etcd保持续约
			if err := p.keepAliveAsync(cli); err != nil {
				// 如果续约失败,记录错误并跳出循环
				logx.Errorf("etcd publisher keepAliveAsync: %s", err.Error())
				break
			}

			// 如果续约成功,返回nil
			return nil
		}
	}
	return nil
}
  1. 搜了一下网上的博客,说doKeepAlive()内部虽然开启了一个间隔一秒的定时器,但他是用来尝试注册服务和获取租约ID,而不是用来续约的,实际续约是在keepAliveAsync()中第一步执行的EtcdClient上绑定的KeepAlive()方法中,但是我又没找到相关源码,此处慎重参考,有的博客中提到:

在etcd上绑定的KeepAlive()方法中设置了一个定时器,定时时间为lease / 3大概3.3秒,也就是说每隔租约有效时间的三分之一会续约一次

三. 总结

  1. 在使用go-zero时,支持direct直连, discov服务发现, k8s三种模式,
  2. 在使用服务发现模式时,整个服务架构分注册中心,服务提供方,服务消费方三个角色
  1. go-zero默认使用etcd作为注册中心
  2. 创建服务时要在yaml文件中配置Name当前服务名,Host当前服务ip,Port当前服务端口号,配置Etcd.Hosts注册中心地址, Etcd.Key当前服务需要调用的服务提供方名称
  3. 服务启动时执行MustLoad()将配置文件读取解析到一个Config结构体变量上
  4. 编写生产rpc服务的proto文件,基于该文件生产rpc服务,生成的文件中有一个RegisterXXXServer()函数,执行该函数将当前rpc服务实例注册到rpc服务器
  5. 执行zrpc.MustNewServer()创建RpcServer,需要传递这个RegisterXXXServer()函数
  6. 调用RpcServer下的Start()启动服务
  1. 查看zrpc下的MustNewServer()函数,内部最终会调用到一个NewServer()函数,该函数中:
  1. 执行c.HasEtcd()判断是否配置了etcd注册中心地址,如果配置了,执行NewRpcPubServer()函数
  2. 在NewRpcPubServer()中会创建一个名为registerEtcd的function函数,并将这个function封装到keepAliveServer结构体中
  3. 自此rpcServer创建成功,并封装了keepAliveServer结构体变量,内部持有一个注册服务的registerEtcd()函数
  1. 拿到RpcServer后,我们继续查看启动服务执行的Start()方法,内部会获取到这个keepAliveServer,执行keepAliveServer上的Start(),通过这个方法,在该方法内部执行前面封装的registerEtcd()进行服务注册
  2. 查看registerEtcd重点关注
  1. 执行NewPublisher()创建Publisher结构体变量,也就是服务发现相关的核心
  2. 执行Publisher上的KeepAlive()方法实现服务发布以及连接功能
  1. Publisher是服务注册发现的核心,内部重点包含了:
  1. endpoints 属性,内部保存了注册中心地址
  2. key属性:当前服务在注册中心的前缀
  3. 用于服务注册暂停的pauseChan和服务暂停后重启的resumeChan
  4. 表示当前配置项的租约信息的lease属性
  1. 并且Publisher上绑定了
  1. NewPublisher()//Publish的构造方法
  2. doRegister()—>register()服务注册
  3. Pause()//服务暂停
  4. Resume()//服务重启
  5. Stop()//服务停止
  6. keepAliveAsync()//
    7.等方法(很多此处只摘要了一些重点关注的), 最核心的方法就是KeepAlive()与keepAliveAsync(),KeepAlive()中包装了doRegister()与keepAliveAsync()
  1. 查看Publisher下的register()方法
  1. 首先创建etcd lease租约信息,租约中的TimeToLive过期时间默认为10秒
  2. 创建fullKey,如果id有设置过则以key和id生成fullKey;如果id没有设置则以key和lease id生成
  3. 将value设置到刚创建的lease中,调用EtcdClient上的Put方法进行注册,value就是服务的地址
  1. 查Publisher下的keepAliveAsync()续约监听,首先通过threding.GoSafe()安全的开启了一个协程,协程内执行,开启for循环,通过select -case
  1. 监听保存了etcd keeplive状态的channel,不是true,则先注销当前租约然后再重新KeepLive()进行服务注册
  2. 监听Publisher的pauseChan,获取是否有暂停操作,有暂停操作,执行revoke()注销当前租约,并且在有暂停操作注销后,会监听resumeChan,如果返回true则重启调用KeepAlive()重新注册,会监听quit.Done()取消
  3. 监听Publisher的quit.Done()是否有取消信号,如果有调用revoke()注销
  1. 当服务注册完毕后最终会以"/服务前缀默认是rpcx/是当前/当前服务ip:端口号"格式将当前服务信息存储到注册中心中
  2. Publisher内部的lease租约信息中包含:
  1. leaseID: 租约ID,这个值在创建租约时由etcd分配。
  2. timeToLive: 租约持续时间,也就是服务注册的有效期,通常为10秒。如果超过了这个有效期,那么etcd就会将该服务注册信息删除。
  3. keepAliveResponseChan: 续租响应状态通道,用于保存etcd返回的续租结果。当续租成功时,keepAliveResponseChan会接收到一个true值,否则接收到false值。
  4. 服务注册时会创建当前服务的lease租约数据,只要租约未到期,会一直存在etcd中,服务消费方可以随时通过etcd查询服务注册信息并发起调用请求。在租约到期前,Publisher会通过keepAlive方法不断地向etcd发送续约请求,以延长租约的有效期。这样可以保证服务注册信息一直存在于etcd中,避免因为过期而被删除
  1. 在续约监听中为什么要用threding.GoSafe():
  1. 在 Go 的默认情况下,如果一个协程中的函数出现 panic,那么这个协程和整个进程都会被直接终止
  2. 为了防止因为某个协程中的 panic 导致整个进程崩溃的情况发生,threding.GoSafe() 函数利用了 recover 函数的特性,在协程内部进行了 panic 和 recover 的处理,保证了程序的健壮性
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
根据提供的引用内容,"container exited with a non-zero exit code 239. Error file: prelaunch.err" 是一个错误信息,表示容器以非零退出代码239退出,并且错误文件为prelaunch.err。这通常意味着在容器运行过程中发生了错误。 要解决这个问题,可以尝试以下几个步骤: 1. 检查错误文件:prelaunch.err是一个错误文件,其中可能包含有关容器退出的更多详细信息。你可以查看该文件以了解更多信息,例如具体的错误消息或异常堆栈跟踪。 2. 检查日志文件:除了prelaunch.err之外,还应该检查其他日志文件,例如应用程序的日志文件或容器管理器的日志文件。这些日志文件可能包含有关容器退出的更多信息,帮助你确定问题的根本原因。 3. 检查资源限制:容器退出的原因可能是由于资源限制导致的。你可以检查容器的资源配置,例如内存限制、CPU限制等,确保它们足够满足应用程序的需求。 4. 检查应用程序配置:容器退出的原因可能与应用程序的配置有关。你可以检查应用程序的配置文件,确保它们正确设置,并且没有任何错误或冲突。 5. 检查依赖项:容器退出的原因可能是由于缺少或错误的依赖项导致的。你可以检查应用程序的依赖项,确保它们正确安装并且与应用程序兼容。 6. 检查网络连接:容器退出的原因可能与网络连接有关。你可以检查网络连接是否正常,并确保容器可以访问所需的资源和服务。 请注意,以上步骤仅为一般性建议,具体解决方法可能因环境和具体情况而异。如果以上步骤无法解决问题,建议查阅相关文档或寻求专业支持以获取更详细的帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值