微服务中的服务发现
什么是服务发现
服务发现是微服务架构中的关键机制,用于确定各个微服务的地址。例如,在一个 API Server
服务中,我们可能需要调用 User
服务来处理用户注册、登录和信息查询,也可能需要 Product
服务来获取商品相关信息。那么,如何发现并访问这些服务呢?
传统服务发现方法
最简单的方式是使用数据库存储服务名称及其对应的地址。每当有新的服务实例启动时,我们将其地址注册到数据库中。客户端在访问该服务时,可以从数据库中查询到对应的地址并发起请求。
在 Kitex
框架中,我们通常使用 ETCD
作为服务注册与发现的数据库,下面通过一个示例演示如何实现。
在 ETCD 中注册服务
在服务启动前,我们需要将其注册到 ETCD
中,以便其他微服务能够发现并访问它:
func kitexInit() (opts []server.Option) {cfg := config.GetConfig()// RegistryAddress为etcd数据库地址r, _ := etcd.NewEtcdRegistry(cfg.Registry.RegistryAddress)// address为user服务的地址address := cfg.KitexConfig.Address + cfg.KitexConfig.Portaddr, _:= net.ResolveTCPAddr("tcp", address)// 保存server的配置信息。opts = append(opts,server.WithServiceAddr(addr),server.WithRegistry(r),server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: cfg.KitexConfig.Service,},),)return
}func main() {config.InitConfig() // 初始化配置文件db.InitDB() // 初始化dbopts := kitexInit() // 初始化服务配置信息svr := user.NewServer(new(UserServiceImpl), opts...) //创建newservererr := svr.Run() // 开启server服务,并实时注册到etcd数据库中if err != nil {log.Println(err.Error())}
}
这样,我们就成功开启了一个服务,并且成功将其注册到etcd
数据库中。使其他服务可以通过 ETCD
进行发现和访问。
通过客户端调用服务
在微服务架构中,我们需要使用客户端来调用 User
服务,并通过 ETCD
进行服务发现:
func initUserClient() userservice.Client {// 1. 创建 etcd 服务解析器(连接注册中心)r, err := etcd.NewEtcdResolver(registryAddr)if err != nil {log.Fatalln(r)}// 2. 配置客户端选项:声明使用 etcd 作为服务发现源opts := []client.Option{client.WithResolver(r),}// 生命etcd进行服务发现。userClient, err := userservice.NewClient("douyinec.user", opts...)if err != nil {log.Fatalln(err)}return userClient
}
处理多个服务实例的情况
每次调用userClient
客户端时,client
会向etcd
查询,微服务地址,并发送请求。如果 etcd
里存储了多个 user service
的地址(即多个实例部署了 user service
),那么 client
会从 etcd
获取所有可用的 user service
地址,并根据负载均衡策略选择一个进行调用。
Kitex
默认使用随机负载均衡,但可以通过 client.WithLoadBalancer()
设定不同的策略,比如轮询`(Round Robin)、最小连接数(Least Connection)等。
处理服务变更(崩溃或重启)
如果 User
服务实例不可用了,会发生什么?
User
服务启动后,会通过etcd.NewEtcdRegistry(...)
注册自身地址到ETCD
。- 在运行期间,
Kitex
定期向ETCD
发送心跳,用于维持服务的可用状态。 - 如果
User
服务崩溃或手动关闭:
- 它将停止发送心跳信号
ETCD
发现心跳超时(例如 10s 内未收到心跳)ETCD
自动将该User
服务实例从注册列表中移除
如果 User 服务重新启动到了新的地址,客户端还能找到它吗?
客户端每次请求时都会向 ETCD
查询最新的 User
服务地址,不会缓存旧的地址。
当 User
服务重新启动并注册到新的地址时,客户端会自动获取最新的服务位置。
这样,无论 User
服务的实例数量如何变化,客户端始终能够找到可用的服务实例,实现了动态的服务发现和负载均衡。
k8s中服务发现的方法
在 Kubernetes(k8s)
中,每个应用服务对应一个 Service
,k8s
通过service
进行服务发现,它充当了访问 Pod
的稳定入口。Service
通过 Endpoint
关联到具体的 Pod
,并按照一定的负载均衡策略将请求路由到后端的 Pod。
Service、Endpoint 与 Pod 的关系
在 Kubernetes 中,
- Pod 是运行应用程序的最小单位,每个 Pod 可能包含一个或多个容器。
- Service 提供了一个稳定的访问入口,它会自动发现符合标签选择器(selector)的 Pod,并将请求负载均衡地转发给它们。
- Endpoint 记录了 Service 关联的 Pod 的实际 IP 和端口。
整个流程如下:
- Service 负责管理和暴露一组 Pod。
- Endpoint 维护了当前 Service 关联的 Pod 列表。
- kube-proxy 监听 Service 变更,并基于 Endpoint 配置负载均衡规则。
- 客户端访问 Service 时,kube-proxy 负责将请求转发给 Endpoint 中的 Pod。
查看当前 Kubernetes 集群中的 Service
我们可以通过kubectl get service -o wide
查看当前存在的所有service。实例输出:
kubectl get service -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
kubernetes ClusterIP 10.43.0.1 <none> 443/TCP 297d <none>
manager NodePort 10.43.196.96 <none> 8090:32545/TCP 191d app.oam.dev/component=manager
localstorage NodePort 10.43.183.49 <none> 8189:30621/TCP 191d app.oam.dev/component=localstorage
scheduler NodePort 10.43.8.18 <none> 5525:31965/TCP 191d app.oam.dev/component=scheduler
redis NodePort 10.43.167.133 <none> 6379:32155/TCP 191d app.oam.dev/component=redis
其中 redis 服务的 Cluster-IP
为 10.43.167.133
,我们可以继续查询其 Endpoint。
查看 Service 关联的 Endpoints
使用以下命令查看 kubectl get endpoints redis -o wide
。实例输出:
kubectl get endpoints redis -o wide
NAME ENDPOINTS AGE
redis 10.42.0.18:6379 191d
从这里可以看到,redis 服务对应的 Pod 运行在 10.42.0.18:6379
。
查看 Service 详细信息
之后通过kubectl describe service redis
查看详情。实例输出:
IP Families: IPv4
IP: 10.43.167.133
IPs: 10.43.167.133
Port: redis 6379/TCP
TargetPort: 6379/TCP
NodePort: redis 32155/TCP
Endpoints: 10.42.0.18:6379
Session Affinity: None
External Traffic Policy: Local```,
这里,这里的 Endpoints
字段表明,该 Service
的流量被转发到了 10.42.0.18:6379
上运行的 Pod
。其中IP
字段标识Service
的 ClusterIP
。Port: redis 6379/TCP
定义Service
的端口映射。TargetPort
标识Service
关联的 Pod
实际监听的端口,流量会被转发到该端口。
Service 的服务发现机制
Kubernetes 主要通过两种方式实现服务发现:
- 环境变量
Kubernetes
在Pod
启动时,会为其关联的Service
自动创建一组环境变量(处于同一namespace
的Service
),例如REDIS_SERVICE_HOST=10.43.167.133
和REDIS_SERVICE_PORT=6379
,在同一命名空间中的pod
可以通过这些环境变量连接Service
。
- DNS 解析方式
Kubernetes
内置的CoreDNS
服务会为Service
自动创建DNS
记录。例如,redis
服务的DNS
记录是redis.default.svc.cluster.local
,集群内部的Pod
直接访问redis:6379
即可连接。其中default
是服务所在的命名空间
我们运行ping redis.default.svc.cluster.local
,发现返回ip
地址10.42.0.18
,即redis service
的地址。
redis.default.svc.cluster.local
被称为 FQDN(Fully Qualified Domain Name,全限定域名)。
全限定域名信息存储在 Kubernetes
内部的 CoreDNS
组件中。其中Kubernetes API Server
监听 Service
资源的创建或删除。CoreDNS
通过 kube-dns
插件 监听这些变化,并在内部的 DNS
服务器 里维护这些 Service
的解析记录。当 Pod
需要访问 Service
,会向 CoreDNS
查询 xxx.namespace.svc.cluster.local
,CoreDNS
解析出 ClusterIP
并返回给 Pod
,这样 Pod
就能访问 Service
了。
Service 的负载均衡原理
当多个 Pod
运行同一个 Service
时,Kubernetes
通过 kube-proxy
进行负载均衡,主要有以下几种模式:
-
iptables(默认)
:kube-proxy
使用iptables
规则将流量随机转发到Endpoints
。 -
IPVS(更高效)
:使用IP Virtual Server
进行流量分发,支持更多的负载均衡策略(如rr
轮询、wrr
权重轮询、lc
最小连接数等)。