背景
随着访问量的逐渐增大,单体应用结构渐渐不满足需求,在微服务出现之后引用被拆分为一个个的服务,服务之间可以互相访问。初期服务之间的调用只要知道服务地址
和端口
即可,而服务会出现增减、故障、升级等变化导致端口和ip也变化,被调用者的变化也会导致调用者变化。这样很不方便。
注册中心
就诞生了,注册中心就像DNS服务器,注册中心是C/S
架构,服务调用者通过Client
调用服务名称,被调用者通过Client
上传服务名称和ip并发送心跳检测该服务的健康状态。注册中心为server
端保存服务名称和服务的ip地址及端口,这样不论被调用者服务如何变化,只要服务名称不变,调用者都不说影响。
常见的注册中心:
- etcd
- consul
- eureka
- nacos
- zookeeper
go实现nacos注册中心
Nacos的核心API中定义了两个接口NamingService和ConfigService。服务注册与发现围绕着NamingService展开,而配置管理则围绕着ConfigService展开。
Nacos的4个核心特性:服务发现和服务健康监测、动态配置服务、动态DNS服务、服务及其元数据管理。
作为注册中心的功能来说,Nacos提供的功能与其他主流框架很类似,基本都是围绕服务实例注册、实例健康检查、服务实例获取这三个核心来实现的。
nacos注册中心基本流程:
- 服务实例启动将自身注册到Nacos注册中心,随后维持与注册中心的心跳;
- 心跳维持策略为每5秒向Nacos Server发送一次心跳,并携带实例信息(服务名、实例IP、端口等);
- Nacos Server也会向Client主动发起健康检查,支持TCP/Http;
- 15秒内无心跳且健康检查失败则认为实例不健康,如果30秒内健康检查失败则剔除实例;
- 服务消费者通过注册中心获取实例,并发起调用;
其中服务发现支持两种场景:第一,服务消费者直接向注册中心发送获取某服务实例的请求,注册中心返回所有可用实例,但一般不推荐此种方式;第二、服务消费者向注册中心订阅某服务,并提交一个监听器,当注册中心中服务发生变化时,监听器会收到通知,消费者更新本地服务实例列表,以保证所有的服务均可用。
nacos数据模型
Nacos数据模型的Key由三元组唯一确定,Namespace默认是空串,公共命名空间(public),分组默认是DEFAULT_GROUP
Nacos基于namespace的设计是为了做多环境以及多租户数据(配置和服务)隔离的。如果用户有多套环境(开发、测试、生产等环境)。
一个服务模型如下:
服务注册
首先需要有一个rpc框架例如Zero,kit,grpc,kitex等,生成rpc服务的代码来模拟服务注册与获取。如下所示使用kitex框架生成rpc service,这里使用nacos官方go语言的sdk和rpc框架无关联,任选框架即可。
server端:
client直接连接:
import (
"github.com/cloudwego/kitex/client"
"order/rpc/orderclient"
)
func Client() orderclient.RPCClient {
rpcClient, err := orderclient.NewRPCClient("orderserver", client.WithHostPorts("192.168.5.118:10000"))
if err != nil {
panic(err)
}
return rpcClient
}
在上述的代码中添加nacos配置中心,首先区别存在一个运行的nacos服务,如下:
go连接nacos实现服务注册参考go-nacos-example
服务注册在启动类连接nacos注册中心,注册服务,如下:
package main
import (
...
"github.com/flairamos/go-component/nacos"
...
)
func main() {
opts := kitexInit()
svr := orderservice.NewServer(new(OrderServiceImpl), opts...)
param := vo.RegisterInstanceParam{
Ip: "127.0.0.1",
Port: 8848,
Enable: true,
Healthy: true,
Weight: 10,
Metadata: map[string]string{"version": "1.0"},
ClusterName: "test",
GroupName: "dev_food_platform",
Ephemeral: true,
ServiceName: "test_app",
}
config := nacos.DefaultClient("public", "mysql", "dev_food_platform", nil)
service := nacos.RegisterService(config, param)
if !service {
log.Println("nacos register failed")
return
}
log.Println("nacos register success")
err := svr.Run()
if err != nil {
klog.Error(err.Error())
}
}
代码中的nacos是小编封装的包,sdk源码参考nacos-go-sdk,如下:
// 官方源码
success, err := namingClient.RegisterInstance(vo.RegisterInstanceParam{
Ip: "10.0.0.11",
Port: 8848,
ServiceName: "demo.go",
Weight: 10,
Enable: true,
Healthy: true,
Ephemeral: true,
Metadata: map[string]string{"idc":"shanghai"},
ClusterName: "cluster-a", // 默认值DEFAULT
GroupName: "group-a", // 默认值DEFAULT_GROUP
})
启动服务:
nacso注册中心查看:
注册配置如下:
"github.com/nacos-group/nacos-sdk-go/vo"
vo.RegisterInstanceParam{
Ip: "127.0.0.1",
Port: 8848,
Enable: true,
Healthy: true,
Weight: 10,
Metadata: map[string]string{"version": "1.0"},
ClusterName: "test",
GroupName: "dev.food_platform",
Ephemeral: true,
ServiceName: "test_app",
}
在上述过程中服务已经注册到nacso注册中心了,完成了第一步。
服务健康检查
nacos服务健康检查是客户端进行的,通过配置来开启,无需手敲发送心跳,判断活跃,更新服务,剔除亚健康实例等代码。
Nacos中临时实例
会定时发送心跳维持活性,基本的健康检查流程基本如下:Nacos客户端会维护一个定时任务,每隔5秒发送一次心跳请求,以确保自己处于活跃状态。Nacos服务端在15秒内如果没收到客户端的心跳请求,会将该实例设置为不健康,在30秒内没收到心跳,会将这个临时实例摘除。
这些都不需要开发者手敲代码,在nacos中有一个github.com/nacos-group/nacos-sdk-go/vo.RegisterInstanceParam
配置参数,在服务注册的时候需要用到,其中Ephemeral
属性表示临时实例,将其设置为true
则开启服务心跳模式。
这个维护心跳的模式的线程是基于主函数的,主要主函数服务一直运行,发送心跳就会一直进行下去。
如果把他设置为false,如下:
不管服务有没有正常启动这个记录都在注册中心,只能通过代码注销实例:
success, err := namingClient.DeregisterInstance(vo.DeregisterInstanceParam{
Ip: "127.0.0.1",
Port: 8848,
ServiceName: "test_app",
Ephemeral: true,
Cluster: "test", // 默认值DEFAULT
GroupName: "dev_food_platform", // 默认值DEFAULT_GROUP
})
多服务实例注册
注意区分服务与实例的区别,一个服务包含若干实例,如user服务可能在不同主机注册那么该服务包含若干ip地址。
如果只有一个实例,该实例宕机系统就崩了,多实例通过服务名获取健康实例提高了系统稳定性。
为了系统的稳定性服务一般都是集群部署,因此在注册的时候可以看到,如下配置:
success, err := namingClient.RegisterInstance(vo.RegisterInstanceParam{
Ip: "10.0.0.11",
Port: 8848,
ServiceName: "demo.go",
Weight: 10,
Enable: true,
Healthy: true,
Ephemeral: true,
Metadata: map[string]string{"idc":"shanghai"},
ClusterName: "cluster-a", // 默认值DEFAULT
GroupName: "group-a", // 默认值DEFAULT_GROUP
})
在nacos注册时首先由配置信息确定集群Cluster
,再去更具服务实例的信息查找如namespace
,group
,servicename
等信息定位服务。注册阶段没有dataId,dataid在注册阶段就是servicename。
多个服务注册重复调用注册方法即可:
success, err := namingClient.RegisterInstance(vo.RegisterInstanceParam{
Ip: "10.0.0.11",
Port: 8848,
ServiceName: "demo.go",
Weight: 10,
Enable: true,
Healthy: true,
Ephemeral: true,
Metadata: map[string]string{"idc":"shanghai"},
ClusterName: "cluster-a", // 默认值DEFAULT
GroupName: "group-a", // 默认值DEFAULT_GROUP
})
注意事项:
- 没有使用集群(ClusterName),自定义字符串即可但同一名称的服务要保持一致。
- 注册时的配置ip与端口并没有验证,可以随意写,在不同机器上需要获取实际值。
- 不同主机注册同一服务服务名称(ServiceName)和组(GroupName)必须一致
开发的不同阶段以namespace
区分如dev
,prod
;不同的项目以GroupName
区分,如order,user;不同的实例以ServiceName
区分,如demo服务可能有,192.168.5.117:8000与192.168.5.118:8000两个实例。
同一服务不同实例的表现形式如下:
记录了服务的两个实例的信息。
获取服务
rpc服务也是基于c/s
架构的,在之前的章节中通过nacos client注册服务,该服务处于运行状态nacos server会维护该服务的活性。接下来就是服务的获取了。
服务获取也是基于nacos client的,而且是在rpc 的client。在很多的rpc框架如grpc等框架rpc的客户端与服务端都是生成式的,通过生成的方法创建服务端与客户端。这种方式也被称为直连方式
。如下:
import (
"github.com/cloudwego/kitex/client"
"order/rpc/orderclient"
)
// order/rpc/orderclient是框架生成的创建客户端对的目录
// 调用生成方法创建客户端实例
// 传入服务名与ip端口地址
func Client() orderclient.RPCClient {
rpcClient, err := orderclient.NewRPCClient("orderserver", client.WithHostPorts("192.168.5.118:10000"))
if err != nil {
panic(err)
}
return rpcClient
}
其实上述代码的服务名没起作用,传入空字符串也可以,关键是ip与端口。
这种直连的方式弊端也很明显,打那个传入的ip:port
宕机后,该客户端也无法使用,其所在服务也受影响。当然也可以将所有ip配置,形成ip列表,使用一些算法获取其中一个作为参数创建客户端实例等一些方法解决,但是了注册中心
是目前最好的方法。
引入nacos后,任然需要使用生成的方法,但是此时地址就不在是通过ip:port
获取了,而是nacos client通过服务名称
获取,nacos client通过namespace
,servicename
,cluster
,group
等信息获取一个服务或者一个服务实例列表,当然最好是获取一个健康的正在运行的实例,将其ip:port传递,即完成调用。
这样一来,服务调用者不用保存任何ip信息,提前约定服务名称即可,十分方便。
图片显示的是nacos-go-sdk提供的获取实例的方法。最好直接使用获取一个健康的实例的方法(免去开发者筛选),如下:
// SelectOneHealthyInstance将会按加权随机轮询的负载均衡策略返回一个健康的实例
// 实例必须满足的条件:health=true,enable=true and weight>0
instance, err := namingClient.SelectOneHealthyInstance(vo.SelectOneHealthInstanceParam{
ServiceName: "demo.go",
GroupName: "group-a", // 默认值DEFAULT_GROUP
Clusters: []string{"cluster-a"}, // 默认值DEFAULT
})
// "github.com/nacos-group/nacos-sdk-go/model"
// model.Instance
type Instance struct {
Valid bool `json:"valid"`
Marked bool `json:"marked"`
InstanceId string `json:"instanceId"`
Port uint64 `json:"port"`
Ip string `json:"ip"`
Weight float64 `json:"weight"`
Metadata map[string]string `json:"metadata"`
ClusterName string `json:"clusterName"`
ServiceName string `json:"serviceName"`
Enable bool `json:"enabled"`
Healthy bool `json:"healthy"`
Ephemeral bool `json:"ephemeral"`
}
获取的实例对象如上,将其ip与端口作为参数传递给rpc client即可。
import (
"fmt"
"github.com/cloudwego/kitex/client"
"github.com/flairamos/go-component/nacos"
"github.com/nacos-group/nacos-sdk-go/vo"
"order/rpc/orderclient"
)
// 直接连接
func Client() orderclient.RPCClient {
rpcClient, err := orderclient.NewRPCClient("orderserver", client.WithHostPorts("192.168.5.118:10000"))
if err != nil {
panic(err)
}
return rpcClient
}
// nacos注册中心连接
func ClientFormNacos() orderclient.RPCClient {
var param = vo.SelectOneHealthInstanceParam{
Clusters: []string{"test"},
ServiceName: "test_app1",
GroupName: "dev_food_platform",
}
config := nacos.DefaultClient("public", "mysql", "dev_food_platform", nil)
instance, err := nacos.SelectOneHealthyInstance(config, param)
if err != nil {
panic(err)
}
addr := fmt.Sprintf("%s:%d", instance.Ip, instance.Port)
rpcClient, err := orderclient.NewRPCClient("test_app1", client.WithHostPorts(addr))
return rpcClient
}