- 时序数据库
- 后端
- 数据存储
- 可观测性
- 云原生
【免费下载链接】mimir
Grafana Mimir provides horizontally scalable, highly available, multi-tenant, long-term storage for Prometheus.
导读
kuberesolver 是一个通过 Kubernetes API 为 gRPC 提供名称解析(Name Resolver)的轻量级库,它内置了约 250 行代码的精简 Kubernetes 客户端,用于发现 Service 的 EndpointSlice,因此不会显著增大二进制体积。在 Grafana Mimir 的 Kubernetes 部署场景中,kuberesolver 与 dskit 的 httpgrpc 客户端配合,让各组件能够以kubernetes://协议动态解析集群内 Service 的真实端点地址,并结合客户端侧负载均衡实现零停机滚动部署。读完本文,你将掌握 kuberesolver 的注册方式、目标 URL 的完整语法、与 DNS 直连的本质区别、RBAC 权限要求、TLS 注意事项,以及它在 Mimir 代码库中的真实落地路径。
kuberesolver 是什么:为 gRPC 而生的 Kubernetes 名称解析器
kuberesolver(vendor/github.com/sercand/kuberesolver/v6/README.md)是一个 gRPC 名称解析器(grpc name resolver)。它的核心定位是:用 Kubernetes API 替代 DNS 来解析服务地址。
它自带一个非常小的 Kubernetes 客户端(约 250 行代码),只做一件事——查询并监听 Service 的 EndpointSlice 信息。由于不依赖client-go这类重型依赖,它"won't bloat your binaries",非常适合对二进制体积敏感的组件。
在 gRPC 的命名体系(grpc naming docs)中,一个 URI 由scheme://authority/endpoint组成。kuberesolver 向 gRPC 注册自己的 Builder,声明它支持kubernetes这个 scheme。此后,只要 gRPC Dial 的目标 URL 以kubernetes://开头,gRPC 就会把解析工作交给 kuberesolver。
快速上手:注册并拨号
注册解析器
使用前,需要在调用grpc.Dial之前把 kuberesolver 注册到 gRPC:
// Import the module import "github.com/sercand/kuberesolver/v6" // Register kuberesolver to grpc before calling grpc.Dial kuberesolver.RegisterInCluster() // it is same as resolver.Register(kuberesolver.NewBuilder(nil /*custom kubernetes client*/ , "kubernetes"))从源码看,RegisterInCluster()实际就是RegisterInClusterWithSchema(kubernetesSchema)的简写(builder.go),而kubernetesSchema常量为字符串"kubernetes"。NewBuilder接收两个参数:
client K8sClient:自定义 Kubernetes 客户端接口;传nil时,Build内部会自动调用NewInClusterK8sClient()创建集群内客户端(builder.go);schema string:该解析器负责的 scheme 名称,默认"kubernetes"。
拨号建立连接
注册完成后,gRPC 遇到kubernetesschema 就会使用 kuberesolver 解析地址:
// if schema is 'kubernetes' then grpc will use kuberesolver to resolve addresses cc, err := grpc.Dial("kubernetes:///service.namespace:portname", opts...)使用自定义 Schema
如果需要使用其他 scheme(例如grpc://),可在启动时改用RegisterInClusterWithSchema(schema)代替RegisterInCluster()。
支持的目标 URL 语法
kuberesolver 接受以下形式的 URL(详见 README):
kubernetes:///service-name:8080 kubernetes:///service-name:portname kubernetes:///service-name.namespace:8080 kubernetes:///service-name.namespace.svc.cluster_name kubernetes:///service-name.namespace.svc.cluster_name:8080 kubernetes://namespace/service-name:8080 kubernetes://service-name:8080/ kubernetes://service-name.namespace:8080/ kubernetes://service-name.namespace.svc.cluster_name kubernetes://service-name.namespace.svc.cluster_name:8080其中service-name是 Kubernetes Service 名称,namespace是命名空间,portname是 Service 端口的命名端口(named port),cluster_name是集群域名。
重要提示:cluster_name不会参与端点解析,它只是为了让完整限定域名(fully qualified service name)写法合法,例如test.default.svc.cluster.local。
底层是如何解析这些 URL 的
parseResolverTarget 按三种形态拆分目标:
kubernetes:///service.namespace:port:URL.Host 为空,从 Endpoint 路径中拆出 service、port、namespace;kubernetes://namespace/service:port:URL.Host 是 namespace,从 Endpoint 中拆出 service 与 port;kubernetes://service.namespace:port:直接从 URL.Host 拆分。
拆分逻辑splitServicePortNamespace先按最后一个冒号切出端口,再按.最多切三段取前两段作为 service 与 namespace,因此能正确处理service-name.<namespace>.svc.<cluster-domain-name>这类完整域名,并忽略集群域名部分(builder.go)。
关于端口的三种处理方式也在该函数中确定:
- 未指定端口 →
useFirstPort = true,取 EndpointSlice 中第一个端口的实际数值; - 端口是纯数字(如
8080)→ 直接使用; - 端口是非数字字符串(如
grpc)→resolveByPortName = true,按命名端口匹配。
如果目标未指定 namespace,则通过读取/var/run/secrets/kubernetes.io/serviceaccount/namespace获取当前 Pod 所在命名空间,读取失败时回退为default(kubernetes.go)。
客户端侧负载均衡:配合 Balancer 实现零停机
kuberesolver 只负责"解析出全部端点地址",真正的流量分发需要配合客户端侧负载均衡策略。拨号时需要传入grpc.WithBalancerName:
grpc.DialContext(ctx, "kubernetes:///service:grpc", grpc.WithBalancerName("round_robin"), grpc.WithInsecure())这会让 gRPC 为每个可用 Service 端点建立子连接(subconnection),并按轮询(round robin)策略分发请求。
为什么这能实现零停机部署
- 每次端点集合变化(Pod 滚动、扩缩容、就绪状态变化)时,kuberesolver 通过
cc.UpdateState(resolver.State{...})向 gRPC 推送最新的地址列表(builder.go); - gRPC 会断开不再存在的子连接,同时为新地址建立连接;
- 因此滚动发布过程中,请求会平滑地从旧 Pod 迁移到新 Pod,实现零停机。
makeAddresses构造地址时只保留Conditions.Ready == true的端点(builder.go),确保只有处于就绪状态的 Pod 才会被纳入负载均衡池,同时把ServerName设置为service.namespace形式的地址,为后续 TLS 连接预留了 SNI 信息。
与直接拨号service.namespace:8080的本质区别
很多人会问:既然 Kubernetes 自带 DNS,为什么不直接grpc.Dial("service.namespace:8080")?README 给出了明确的解释:
- 直接拨号走 DNS,DNS 只返回 Service 的稳定 ClusterIP;
- gRPC 只能看到这一个 ClusterIP,不知道背后每个 Endpoint 的真实 Pod IP;
- 当某个 Pod 故障或 Service 端点变化时,gRPC无法感知并重连到正确的目标,造成请求失败或无法利用多副本能力。
而 kuberesolver 通过 Kubernetes API获取并持续监听 Service 的 EndpointSlice,把每个可用 Pod 的 IP:Port 都提供给 gRPC。配合客户端侧负载均衡器,就能实现:
- 请求均匀分布到所有就绪副本;
- 故障 Pod 被自动剔除,新副本自动加入;
- 滚动部署期间流量平滑切换,实现零停机。
源码级工作机制:Watch 与兜底轮询
kuberesolver 解析器的运行循环由 kResolver.watch 驱动,其工作方式如下:
- 启动即全量拉取:
watchEndpointSlice会先发起一个 watch 流,Kubernetes watch 语义保证流建立时会先推送当前已有的全部对象("watch endpoints lists existing endpoints at start"); - 增量监听:后续的
ADDED、MODIFIED、DELETED事件(见 models.go)会实时驱动kResolver.handle更新 gRPC 状态; - 定时兜底轮询:内置一个默认
30 分钟的定时器(defaultFreq = time.Minute * 30,builder.go),定时调用resolve()重新 GET 一次 EndpointSlice 列表,防止 watch 流静默失效导致状态陈旧; - 断线自动重连:外层
until循环(util.go)采用指数退避重试:首次间隔 1 秒,此后每次翻倍,达到 30 秒上限后重置回 1 秒,并捕获 panic 记录日志避免崩溃。
Watch 流解析细节
stream.go 中的streamWatcher将 Kubernetes watch 的 chunked JSON 流逐条解码为Event,仅在ADDED / MODIFIED / DELETED / ERROR四种类型中放行,其他类型视为非法事件;遇到io.EOF、context.Canceled等正常终止情况会静默退出。
自带的可观测性指标
kuberesolver 通过 Prometheuspromauto注册了三个 Gauge 指标(builder.go),标签为target(形如kubernetes://namespace/service:port):
| 指标名 | 含义 |
|---|---|
kuberesolver_endpoints_total | 目标 Service 的端点(Endpoint)数量 |
kuberesolver_addresses_total | 实际下发到 gRPC 的可用地址数量 |
kuberesolver_client_last_update | 解析器客户端最后一次成功更新的 Unix 时间戳 |
这三个指标在 Mimir 集群运维中可直接用于排查"地址解析是否跟上 Pod 变化"。
集群内客户端的构建方式
NewInClusterK8sClient(kubernetes.go)在 Pod 内通过环境变量与挂载文件构建客户端:
- 从环境变量
KUBERNETES_SERVICE_HOST与KUBERNETES_SERVICE_PORT得到 API Server 地址; - 从
/var/run/secrets/kubernetes.io/serviceaccount/token读取 ServiceAccount 令牌; - 从
/var/run/secrets/kubernetes.io/serviceaccount/ca.crt读取 CA 证书,构造 HTTPS 客户端; - 动态刷新令牌:用
fsnotify监听 token 文件,Kubernetes 通过符号链接轮换 ServiceAccount 令牌时会触发Remove/Write事件,客户端随即重新读取并更新 token(源码注释明确说明这是为了兼容 configmap 的符号链接机制)。
此外还提供了NewInsecureK8sClient(apiURL),适用于通过代理访问 Kubernetes API 等非集群内场景。
RBAC 权限要求
如果集群启用了 RBAC,运行 kuberesolver 的 ServiceAccount 需要具备对endpointslices资源的GET和WATCH权限。一个参考的最小 RBAC 配置:
apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: kuberesolver-endpointslices rules: - apiGroups: ["discovery.k8s.io"] resources: ["endpointslices"] verbs: ["get", "watch"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: kuberesolver-endpointslices subjects: - kind: ServiceAccount name: <your-service-account> namespace: <your-namespace> roleRef: kind: ClusterRole name: kuberesolver-endpointslices apiGroup: rbac.authorization.k8s.io从请求 URL 可以确认权限粒度:kuberesolver 实际请求的是GET /apis/discovery.k8s.io/v1/namespaces/{ns}/endpointslices?labelSelector=kubernetes.io/service-name={svc}与对应的/watch/...路径(kubernetes.go),通过kubernetes.io/service-name标签选择器精确过滤目标 Service 的 EndpointSlice。
使用 TLS
如果要通过 TLS 连接后端服务,需要准备一个名为service-name.namespace的证书。也就是说,证书的通用名(CN)必须与服务目标的service.namespace完全匹配,这样 kuberesolver 在构造resolver.Address时填充的ServerName才能与证书匹配,从而通过 TLS 主机名校验。
在 Grafana Mimir 中的实际落地
kuberesolver 并不是 Mimir 直接 import 的依赖(在 go.mod 中为 indirect 依赖),而是通过 Mimir 依赖的 dskit 库间接使用。真实的调用链如下:
- 注册:dskit 的 httpgrpc 客户端在
NewClient(address)中调用kuberesolver.RegisterInCluster()(vendor/github.com/grafana/dskit/httpgrpc/server/server.go),确保解析器在拨号前完成注册; - URL 归一化:
ParseURL负责兼容处理:直接透传带:///的 kuberesolver v2 URL;无 scheme 的地址按kubernetes处理(向后兼容);direct://原样返回;kubernetes://则把service.namespace:port规范化为kubernetes:///service.namespace:port形式(server.go); - 负载均衡:拨号时通过
grpc.WithDefaultServiceConfig传入{"loadBalancingPolicy":"round_robin"},即用 round_robin 策略配合 kuberesolver 解析出的多端点实现客户端侧负载均衡; - 使用场景:Mimir 的 pkg/mimir/modules.go 使用
httpgrpc_server.NewServer(...)提供 gRPC 化 HTTP 转发服务,配合 query-scheduler 的 worker 在组件间通信;pkg/alertmanager/multitenant.go、pkg/distributor/push.go 等文件也均引用了github.com/grafana/dskit/httpgrpc/server。这些组件以 Kubernetes Deployment/StatefulSet 方式部署时,即可通过kubernetes:///地址解析彼此的真实端点。
从 Mimir 的 CHANGELOG.md 也可以看到,项目持续跟进 kuberesolver 的版本升级(如从 v2.4.0 升级到 v4.0.0 以配合 gRPC 1.53.0 的 API 变化),当前 vendored 版本为 v6.0.1,说明该解析器在 Mimir 的长期演进中一直承担着集群内服务发现的关键职责。
小结与最佳实践
- 拨号前先调用
kuberesolver.RegisterInCluster()(或自定义 schema 版本)完成注册; - 目标地址优先使用
kubernetes:///service.namespace:portname规范形式;命名空间缺省时自动使用 Pod 所在命名空间; - 务必为 ServiceAccount 配置
endpointslices的get与watch权限; - 需要多副本负载均衡与零停机滚动时,配合
round_robin客户端负载均衡策略,而不要直接 DNS 拨号 Service ClusterIP; - 使用 TLS 时确保证书 CN 为
service-name.namespace; - 可通过
kuberesolver_endpoints_total、kuberesolver_addresses_total与kuberesolver_client_last_update三个指标观察解析器健康状态。
- 时序数据库
- 后端
- 数据存储
- 可观测性
- 云原生
【免费下载链接】mimir
Grafana Mimir provides horizontally scalable, highly available, multi-tenant, long-term storage for Prometheus.
相关推荐
WAN2.2 All In One:AI视频生成在消费级硬件上的技术突破
WAN2.2 All In One:AI视频生成在消费级硬件上的技术突破 技术挑战与解决方案演进 当前AI视频生成面临的核心技术瓶颈在于计算资源消耗与生成质量的
人工智能计算机视觉基础模型Kubernetes 集群中的 Traefik Ingress:服务发现与负载均衡实战指南
Kubernetes 集群中的 Traefik Ingress:服务发现与负载均衡实战指南 导读 本指南以 Traefik 作为 Ingress Control
云原生教程PyTextRank源码深度剖析:掌握四大TextRank算法的实现细节
PyTextRank源码深度剖析:掌握四大TextRank算法的实现细节 PyTextRank是一个基于Python的TextRank算法实现库,专注于短语提取
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考