服务注册与发现
什么是服务注册与发现
- 服务注册:服务进程在注册中心注册自己的元数据信息。通常包括主机和端口号,有时还有身份验证信息,协议,版本号,以及运行环境的信息。
- 服务发现:客户端服务进程向注册中心发起查询,来获取服务的信息。服务发现的一个重要作用就是提供给客户端一个可用的服务列表。
服务注册与发现解决了什么问题
项目的演进过程
- 一般来讲,一个项目的起初阶段,我们并不能判断出有多少用户量,并发量,每日大概有多少pv,uv,所以一开始不可能耗费大量的人力物力来搭建支持百万并发的平台,于是第一台服务器开始表演,集lnmp于一身,也就是就原始的单体架构。
- 随着用户量增加,服务器的cpu和内存飙升,咋办?把MySQL服务单独拉一台机器吧,后来,一些用户的更新操作导致一些用户无法浏览内容,怎么解决?于是就有了数据库的读写分离,主从架构。
- 突然有一天扫地阿姨不小心碰了电线,其中一台服务器掉电了,用户所有的请求都报错,随之而来的是一系列投诉电话。于是开始升级集群架构。将应用部署到多台机器上面,如果有一台机器出问题了,不影响其他机器继续提供服务。
- 业务越来越大,一个项目的代码都超过5个G了。代码之间的耦合很严重,一个地方改动可能引发好几个线上问题,开发成本,测试成本都很大。于是我们开始拆解业务,订单系统,库存系统,积分系统,评价系统。。。。一个大的服务根据业务相关性拆分为很多小的业务系统,这就是微服务拆分。
- 一个业务可能需要好几个微服务支撑,为了节省资源,最大化利用物理机,我们选择把微服务运行在容器之中,访问的时候,我们可以像我上面通过
localhost:9988
调用访问,但是一个微服务要动态的增加或者删减服务节点,重启之后ip也会发生变化,我们怎么合理地把请求均衡的分配给所有节点?ip变化之后怎么访问?这就需要用到服务动态注册与发现。
为什么redis不行?
我们刚才说过,需要有一个服务去存放我们的ip和端口,这个时候可能会有这样一个想法,我们根据key-value的存储方式,去存储我们服务的信息不行吗?
答案是否定的,因为作为一个服务发现服务,不仅要保存服务的访问方式(ip+port),而且还需要有服务监听的功能,隔一段时间去监听你的服务是否正常,即健康检查。
第二,可能有多个节点提供一个服务,特别是高并发的时候,我们可能会增加服务,那么我们来服务的实现负载均衡呢。
第三,如果几百个服务我们不可能一个一个去改服务的配置,而且本身作为微服务,每个服务之间应该都是独立的,一个服务的改动不应该影响到其他服务。
所以说一个服务发现的系统至少应该具备服务发现、健康检查、负载均衡和全局分布的键值存储的功能
服务发现框架
- 我们先来一张图看看服务发现是怎么提供服务的,我们根据服务名去服务发现拉取服务的ip和端口号。然后拿着ip和端口号就可以获取到服务。
通常我们用到的服务发现框架有三种,zookeeper,consul,etcd
- 我们来对比一下三者的区别,其中zookeeper是用java编写的,通过sdk去调用。consul和etcd都是用go编写的,但是consul功能更丰富一点,而且有web管理界面,也是go项目中最常用到的。
consul
-
consul是分布式的、高可用、横向扩展的。
-
service discovery:consul通过DNS或者HTTP接口使服务注册和服务发现变的很容易,一些外部服务,例如saas提供的也可以一样注册。
-
health checking:健康检测使consul可以快速的告警在集群中的操作。和服务发现的集成,可以防止服务转发到故障的服务上面。
-
key/value storage:一个用来存储动态配置的系统。提供简单的HTTP接口,可以在任何地方操作。
-
multi-datacenter:无需复杂的配置,即可支持任意数量的区域。
-
安装 相关安装地址
-
为了方便,我这里使用docker启动,相关命令如下 相关参考
docker run -d -p 8500:8500 -p 8300:8309 -p 8301:8301 -p8302:8302 -p 8600:8600/udp hashicorp/consul consul agent -dev -client=0.0.0.0
- 其中8500端口是提供给外部的web页面,即web ui,默认有一个consul服务
- 8300 server rpc 端口同一数据中心 consul server 之间通过该端口通信
- 8301 serf lan 端口,同一数据中心 consul client 通过该端口通信
- 8302 serf wan 端口:不同数据中心 consul server 通过该端口通信
- 8600 dns 端口,用于服务发现。
API接口
- 为了方便理解,我们先从最简单的api开始入手
- 相关参考相关参考
- consul给我们提供了api接口用于服务的注册,注销,健康检测等服务。
- Register Service
zhangguofu@bogon ~ $ curl -X PUT -H "Content-Type: application/json" -d '{"Name": "my-service", "Address": "localhost", "Port": 5000, "Tags": ["tag1", "tag2"]}' http://localhost:8500/v1/agent/service/register
- 查询服务列表
zhangguofu@bogon ~ $ curl http://localhost:8500/v1/catalog/services
{
"consul": [],
"my-service": [
"tag1",
"tag2"
]
}
- 查询服务的详细信息
zhangguofu@bogon ~ $ curl http://localhost:8500/v1/catalog/service/my-service
[
{
"ID": "06788a51-1192-fed7-11cd-e088eea65330",
"Node": "530e4f9e4899",
"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": "my-service",
# 服务名称
"ServiceName": "my-service",
# 服务标签
"ServiceTags": [
"tag1",
"tag2"
],
# ip地址
"ServiceAddress": "localhost",
"ServiceWeights": {
"Passing": 1,
"Warning": 1
},
"ServiceMeta": {},
# 服务的端口号
"ServicePort": 5000,
"ServiceSocketPath": "",
"ServiceEnableTagOverride": false,
"ServiceProxy": {
"Mode": "",
"MeshGateway": {},
"Expose": {}
},
"ServiceConnect": {},
"CreateIndex": 4318,
"ModifyIndex": 4318
}
]
- 删除
curl -X PUT http://localhost:8500/v1/agent/service/deregister/my-service
注意,在Consul中,如果要使用HTTP API进行服务注册,必须使用v1/agent API端点。这是因为v1/agent是Consul API中与代理进行交互的端点,可以让我们通过API与代理进行通信,从而注册、注销、查询服务等操作。使用v1/agent API端点可以帮助我们更好地管理服务实例,提高服务可用性和可靠性。
在go中使用consul
服务注册
- 我们先通过一个简单例子看一下
package main
import "fmt"
import consulapi "github.com/hashicorp/consul/api"
//定义服务注册的信息
type ServiceConfig struct {
ID string
Name string
Tags []string
Port int
Address string
}
//consul的服务地址
var consulIp="127.0.0.1:8500"
//注册
func RegisterSevice(s ServiceConfig) error{
config:=consulapi.DefaultConfig()
config.Address = consulIp
//获取到客户端
client, err := consulapi.NewClient(config)
if err != nil {
fmt.Printf("create consul client : %v\n", err.Error())
return err
}
registration := &consulapi.AgentServiceRegistration{
ID: s.ID,
Name: s.Name,
Port: s.Port,
Tags: s.Tags,
Address: s.Address,
}
if err := client.Agent().ServiceRegister(registration); err != nil {
fmt.Printf("register to consul error: %v\n", err.Error())
return err
}
return nil
}
func main() {
//先注册一下
service := ServiceConfig{
ID: "9527",
Name: "demo_service",
Tags: []string{"a", "b"},
Port: 10111,
Address: "192.168.0.125",
}
err := RegisterSevice(service)
if err != nil {
fmt.Printf("register to consul error: %v\n", err.Error())
}
}
- 运行完毕之后,在服务列表就出现了
- 但是这个服务是不存在的,我们通过下面的代码实现服务监控
//注册
func RegisterSevice(s ServiceConfig) error {
config := consulapi.DefaultConfig()
config.Address = consulIp
//获取到客户端
client, err := consulapi.NewClient(config)
if err != nil {
fmt.Printf("create consul client : %v\n", err.Error())
return err
}
registration := &consulapi.AgentServiceRegistration{
ID: s.ID,
Name: s.Name,
Port: s.Port,
Tags: s.Tags,
Address: s.Address,
}
//开启健康检测
check := &consulapi.AgentServiceCheck{}
//检测服务地址
check.TCP = fmt.Sprintf("%s:%d",service.Address,service.Port )
//设置过期时间
check.Timeout="5s"
//每5s执行一次
check.Interval="5s"
//检查失败超过20s将会被注销
check.DeregisterCriticalServiceAfter = "20s"
//将check属性添加上去
registration.Check=check
if err := client.Agent().ServiceRegister(registration); err != nil {
fmt.Printf("register to consul error: %v\n", err.Error())
return err
}
return nil
}
- 执行完毕之后,我们发现健康检查返回结果
- 那么我来注册一个可用的服务
func tcpService(){
network:="tcp"
address:=fmt.Sprintf("%s:%d",service.Address,service.Port )
//绑定和监听tpc和端口
listen, err := net.Listen(network, address)
if err != nil {
fmt.Println("listen err")
}
//关闭监听
defer listen.Close()
for{
//等待连接
_,err:=listen.Accept()
if err != nil {
fmt.Println("accept error")
}
}
}
- 我们也可以注册http服务
func httpService() {
url:=fmt.Sprintf("%s:%d",service.Address, service.Port)
http.HandleFunc("/", func(w http.ResponseWriter,r *http.Request){
fmt.Printf("run http service in %s",url)
})
//监听http请求
err := http.ListenAndServe(url, nil)
if err != nil {
fmt.Printf("http service error")
}
}
修改一下服务地址
- 批量注册服务
//注册
func RegisterSevice(s ServiceConfig) error {
config := consulapi.DefaultConfig()
config.Address = consulIp
//获取到客户端
client, err := consulapi.NewClient(config)
if err != nil {
fmt.Printf("create consul client : %v\n", err.Error())
return err
}
// 定义要注册的服务实例
services := []*consulapi.AgentServiceRegistration{
{
ID: "service-1",
Name: "test-service",
Address: "192.168.1.10",
Port: 8080,
Tags: []string{"v1"},
Check: &consulapi.AgentServiceCheck{
HTTP: "http://192.168.1.10:8080/health",
Interval: "10s",
},
},
{
ID: "service-2",
Name: "test-service",
Address: "192.168.1.11",
Port: 8080,
Tags: []string{"v2"},
Check: &consulapi.AgentServiceCheck{
HTTP: "http://192.168.1.11:8080/health",
Interval: "10s",
},
},
}
for _, service := range services {
err = client.Agent().ServiceRegister(service)
if err != nil {
fmt.Println("注册服务失败:", err)
return nil
}
fmt.Println("注册服务成功:", service.ID)
}
return nil
}
服务发现
- 我们通过向consul发送请求来获取服务列表
package main
import (
"encoding/json"
"fmt"
consulapi "github.com/hashicorp/consul/api"
)
//consul的服务地址
var consulIp = "127.0.0.1:8500"
var serviceName = "demo_service"
func main() {
//请求consul
config := consulapi.DefaultConfig()
config.Address = consulIp
client, err := consulapi.NewClient(config)
if err != nil {
fmt.Printf("consul client error: %v", err)
}
service, _, err := client.Health().Service(serviceName, "", false, nil)
if err != nil {
fmt.Printf("get service error: %v", err)
}
addr := ""
for _, v := range service {
j, err := json.Marshal(v)
if err != nil {
return
}
fmt.Printf("%s \n\n", j)
addr = fmt.Sprintf("http://%s:%d", v.Service.Address, v.Service.Port)
}
fmt.Printf("the service is %s",addr)
}