服务注册和发现
什么是服务发现和注册
- 服务注册:将提供某个服务的模块信息(通常是这个服务的IP和端口)注册到1个公共的租价你上去(比如:zookeeper、consul、etcd)
- 服务发现:新注册的这个服务模块能够及时的被其他的调用者发现,不管是服务新增和服务删减都能实现自动发现
服务注册与发现应用在哪里
我们知道随着互联网的迅速发展,客户端的请求量正在迅速的增加。那么服务器的压力就会越来越大。那么我们有什么方案去解决这个问题呢。
Web1.0
在web1.0的时代,所有的client调用的都是一台服务器。所有的服务也都在一个项目中(如下图)。在互联网起步阶段是可以满足当时的访问量的。但是随着客户端不停的增加单台服务器已经无法满足这么大的访问量了,那我们如何处理呢?
Web2.0
通过鸡蛋和篮子理论,当所有鸡蛋都放到一个篮子里,但是鸡蛋确越来越多的情况下,我们只能增加篮子了。把鸡蛋放到不同的篮子里这样就可以解决这个问题。
这样我们就进入了web2.0的时代。也就是我们尝尝说道的服务器集群,所有的客户端会通过一个负载均衡把所有的请求通过不同的算法分发到集群中的机器里。这样就可以负载更多客户端的请求了。
微服务时代
随着时代的进步,我们的业务从单一的网站浏览变成了各种各样的APP。用户的增加也就是我们的功能变得多样化。那么在Web2.0中使用的单体服务集群部署的方式就会让项目变的十分的复杂。给我们的开发和维护造成了很大的困扰。那么再次根据鸡蛋篮子理论,我们的解决方案是把不同的鸡蛋放到不同的篮子里。简单的说就是把一个单体的服务根绝业务的维度拆分成不同的服务,这些服务里的资源需要做到相互隔离。
微服务对于服务发现注册的需求
我们可以想一下,一个电商服务我们可以分成用户、商品、订单、库存等微服务。如果只是单单5个微服务我们可能觉得关系并不是很复杂可以使用配置文件或者其他的方式去解决服务和服务之间的调用配置。那么随着业务越来越复杂,用户微服务又会被拆分成用户、登记、会员等等独立的微服务。这个时候微服务的数量将会变得十分的庞大。那么这么多的微服务指尖的错综合更复杂的相互调用呢?这个时候服务注册和发现就可以解决这些问题。
可以想象一下,用户服务我只需要向服务中心注册一个名字为“User”的服务,然后把自己的ip和端口上传到注册中心里。那么调用方就可以简单根据这个服务的名称来找到User服务,调用方并不关心有多少台机器在这个服务中这些都会是注册中心来解决的问题。那这么做有什么好处呢:
- 可以随时增加微服务中服务器的数量
- 可以通过统一的注册中心做心跳检测,来保证服务的可用性
- 不必维护复杂的服务名称和对应的IP端口等等
- 可以做统一的注册发现系统,做统一的预警或者报警机制,出现问题可以被及时的发现
这就是为什么各个微服务框架中都会有服务发现注册这个模块了。
在Kratos如何使用服务发现注册
Kratos本文就不做过多的解释了,是一个B站开源的微服务框架。在这个框架中有一个接口:
type Registrar interface {
// 注册实例
Register(ctx context.Context, service *ServiceInstance) error
// 反注册实例
Deregister(ctx context.Context, service *ServiceInstance) error
}
type Discovery interface {
// 根据 serviceName 直接拉取实例列表
GetService(ctx context.Context, serviceName string) ([]*ServiceInstance, error)
// 根据 serviceName 阻塞式订阅一个服务的实例列表信息
Watch(ctx context.Context, serviceName string) (Watcher, error)
}
只要我们的对象实现了这两个接口就可以在框架中使用服务注册与发现了。
当然在Kratos中已经默认实现了大多数主流的服务发现组件比如consul、etcd、kubernetes、zookeeper等等。
那我们要如何使用呢?
服务注册
在服务注册中,我们只需要创建一个Registrar对象,讲这个对象注入到Kratos中就可以实现服务的发现。当然这个服务需要有一个不重复的名称。
下面的代码中使用的时consul为例的代码。
import (
consul "github.com/go-kratos/kratos/contrib/registry/consul/v2"
"github.com/hashicorp/consul/api"
)
// new consul client
client, err := api.NewClient(api.DefaultConfig())
if err != nil {
panic(err)
}
// new reg with consul client
reg := consul.New(client)
app := kratos.New(
// service-name
kratos.Name(Name),
kratos.Version(Version),
kratos.Metadata(map[string]string{}),
kratos.Logger(logger),
kratos.Server(
hs,
gs,
),
// with registrar
kratos.Registrar(reg),
)
核心代码
下面是Kratos中consul作为服务发现的核心代码,我们看到第一个方法是对服务的一些处理和判断,注意这个服务里可以包含http协议的也包好grpc协议的。最终组成一个对象,调用ServiceRegister方法,方法中可以看到是真正的向consul发送一个put请求,注册了一个服务。
这个put请求是consul对外公开的注册服务的接口。其实Kratos是对这个接口封装了一层,通过配置读取自动的做服务注册。
// Register register service instance to consul
func (c *Client) Register(_ context.Context, svc *registry.ServiceInstance, enableHealthCheck bool) error {
addresses := make(map[string]api.ServiceAddress)
checkAddresses := make([]string, 0, len(svc.Endpoints))
for _, endpoint := range svc.Endpoints {
raw, err := url.Parse(endpoint)
if err != nil {
return err
}
addr := raw.Hostname()
port, _ := strconv.ParseUint(raw.Port(), 10, 16)
checkAddresses = append(checkAddresses, net.JoinHostPort(addr, strconv.FormatUint(port, 10)))
addresses[raw.Scheme] = api.ServiceAddress{Address: endpoint, Port: int(port)}
}
asr := &api.AgentServiceRegistration{
ID: svc.ID,
Name: svc.Name,
Meta: svc.Metadata,
Tags: []string{fmt.Sprintf("version=%s", svc.Version)},
TaggedAddresses: addresses,
}
if len(checkAddresses) > 0 {
host, portRaw, _ := net.SplitHostPort(checkAddresses[0])
port, _ := strconv.ParseInt(portRaw, 10, 32)
asr.Address = host
asr.Port = int(port)
}
if enableHealthCheck {
for _, address := range checkAddresses {
asr.Checks = append(asr.Checks, &api.AgentServiceCheck{
TCP: address,
Interval: fmt.Sprintf("%ds", c.healthcheckInterval),
DeregisterCriticalServiceAfter: fmt.Sprintf("%ds", c.deregisterCriticalServiceAfter),
Timeout: "5s",
})
}
}
if c.heartbeat {
asr.Checks = append(asr.Checks, &api.AgentServiceCheck{
CheckID: "service:" + svc.ID,
TTL: fmt.Sprintf("%ds", c.healthcheckInterval*2),
DeregisterCriticalServiceAfter: fmt.Sprintf("%ds", c.deregisterCriticalServiceAfter),
})
}
err := c.cli.Agent().ServiceRegister(asr)
if err != nil {
return err
}
if c.heartbeat {
go func() {
time.Sleep(time.Second)
err = c.cli.Agent().UpdateTTL("service:"+svc.ID, "pass", "pass")
if err != nil {
log.Errorf("[Consul]update ttl heartbeat to consul failed!err:=%v", err)
}
ticker := time.NewTicker(time.Second * time.Duration(c.healthcheckInterval))
defer ticker.Stop()
for {
select {
case <-ticker.C:
err = c.cli.Agent().UpdateTTL("service:"+svc.ID, "pass", "pass")
if err != nil {
log.Errorf("[Consul]update ttl heartbeat to consul failed!err:=%v", err)
}
case <-c.ctx.Done():
return
}
}
}()
}
return nil
}
func (a *Agent) serviceRegister(service *AgentServiceRegistration, opts ServiceRegisterOpts) error {
r := a.c.newRequest("PUT", "/v1/agent/service/register")
r.obj = service
r.ctx = opts.ctx
if opts.ReplaceExistingChecks {
r.params.Set("replace-existing-checks", "true")
}
_, resp, err := a.c.doRequest(r)
if err != nil {
return err
}
defer closeResponseBody(resp)
if err := requireOK(resp); err != nil {
return err
}
return nil
}
服务发现
参考文章:
深入了解服务注册与发现