Appearance
client-go基础
ops-checker 已经能做 HTTP 巡检,但 Kubernetes 里的服务状态还会体现在资源对象上。接口返回 200 时,Deployment 可能已经少了副本;Pod 可能频繁重启;Node 可能 NotReady;Service 背后可能没有可用 Endpoint。
Kubernetes 资源都通过 API Server 读写。Go 程序要读取 Pod、Deployment、Node 这些对象,需要一个 Kubernetes Go 客户端。client-go 是 Kubernetes 官方客户端库,负责把 Go 里的调用变成对 API Server 的请求。
一、检查范围
HTTP 检查回答“入口能不能访问”。Kubernetes 检查回答“集群对象当前是不是健康”。
常见差异:
| 现象 | HTTP 巡检看到的结果 | Kubernetes 对象里能看到的信息 |
|---|---|---|
| 副本不足 | 负载均衡还转到幸存 Pod,接口仍然 200 | Deployment 的 availableReplicas(就绪副本数)少于期望值 |
| Pod 频繁重启 | 请求刚好打到正常 Pod,接口仍然 200 | Pod 的 restartCount(重启次数)持续增加 |
| 节点异常 | 入口层还有缓存或其他副本 | Node 的 condition 出现 Ready=False(节点未就绪) |
| Service 无后端 | 入口可能直接 503 | EndpointSlice(Service 的后端地址列表)里没有 ready endpoint |
这些字段都可以通过 kubectl get 或 kubectl describe 看到,对应的 Go 客户端查询方式见第七节。
ops-checker 接入 Kubernetes 后,原来的 HTTP 检查仍然保留,再增加 Kubernetes 检查类型。一个工具同时记录入口状态和集群对象状态。
二、连接链路
执行 kubectl get pod 时,kubectl 会读取 kubeconfig,找到当前集群地址、认证信息和 namespace,然后向 API Server 发请求。
Go 程序也是同样链路:
这几个对象各自处理不同问题:
| 对象 | 解决的问题 |
|---|---|
kubeconfig | 配置文件,记录连哪个集群、用哪个用户、默认 namespace |
RESTConfig | Go 程序运行时使用的连接配置 |
ClientSet | 访问 Kubernetes 内置资源的客户端集合 |
Informer | 长时间监听资源变化并维护本地缓存 |
ops-checker 作为命令行巡检工具,使用 ClientSet 做一次性查询。Informer 属于长期运行的控制器场景。
三、配置变更
原来的 configs/targets.json 只有 HTTP 目标:
json
[
{
"type": "http",
"name": "api",
"url": "https://example.com/health",
"timeout_seconds": 3
}
]增加 Kubernetes Pod 检查后,目标仍然放在同一个配置文件里。
修改文件:configs/targets.json
json
[
{
"type": "http",
"name": "api",
"url": "https://example.com/health",
"timeout_seconds": 3
},
{
"type": "kubernetes_pods",
"name": "nginx-pods",
"namespace": "default",
"label_selector": "app=nginx",
"min_ready": 2
}
]type 表示检查类型。HTTP 目标用 url,Kubernetes Pod 检查用 namespace、label_selector 和 min_ready。
修改文件:internal/config/target.go
给 Target 增加类型和 Kubernetes 字段:
go
type Target struct {
Type string `json:"type"`
Name string `json:"name"`
URL string `json:"url"`
TimeoutSeconds int `json:"timeout_seconds"`
Namespace string `json:"namespace"`
LabelSelector string `json:"label_selector"`
MinReady int `json:"min_ready"`
}字段不是每种检查都会用到。type=http 时,Kubernetes 字段为空;type=kubernetes_pods 时,url 为空。检查类型继续增加后,再拆成更严格的配置结构。
四、依赖包
项目开始接入 Kubernetes 后,需要新增 client-go 相关依赖。
修改后的目录会多出一个 internal/kube/:
text
ops-checker/
├── cmd/
│ └── ops-checker/
│ └── main.go
├── configs/
│ └── targets.json
└── internal/
├── checker/
│ ├── http.go
│ ├── kubernetes.go
│ ├── result.go
│ └── runner.go
├── config/
│ └── target.go
├── kube/
│ └── client.go
└── output/
├── json.go
└── text.go添加依赖:
bash
go get k8s.io/client-go/kubernetes
go get k8s.io/client-go/tools/clientcmd
go get k8s.io/apimachinery/pkg/apis/meta/v1client-go 版本通常跟 Kubernetes 主版本对齐,例如 Kubernetes 1.30 对应 client-go v0.30.x。版本差太多时,代码可能仍然能编译,但某些新字段、新资源类型或认证插件行为会不一致。
五、RESTConfig
kubeconfig 是 YAML 文件,常见位置:
| 场景 | 路径 |
|---|---|
| Linux 普通用户 | ~/.kube/config |
| kubeadm 管理节点 | /etc/kubernetes/admin.conf |
| 多配置合并 | KUBECONFIG 环境变量指定 |
kubectl 用来验证当前配置:
bash
kubectl config current-context
kubectl get nskubectl get ns 能验证三件事:API Server 地址能连通,认证信息能通过,当前身份至少有读取 namespace 的权限。这个命令失败时,Go 程序里创建 ClientSet 也很难成功。
新增文件:internal/kube/client.go
go
package kube
import (
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)
func BuildConfig(kubeconfigPath string) (*rest.Config, error) {
if kubeconfigPath == "" {
// 本机运行时默认读取 ~/.kube/config,行为接近 kubectl。
return clientcmd.BuildConfigFromFlags("", clientcmd.RecommendedHomeFile)
}
return clientcmd.BuildConfigFromFlags("", kubeconfigPath)
}
func NewClientSet(kubeconfigPath string) (*kubernetes.Clientset, error) {
config, err := BuildConfig(kubeconfigPath)
if err != nil {
return nil, err
}
return kubernetes.NewForConfig(config)
}RESTConfig 还不是客户端。它只是“怎么连接 API Server”的运行时配置,里面包含地址、证书、token、QPS、Burst、UserAgent 等信息。NewForConfig 才会基于这份配置创建 ClientSet。
Pod 内运行的程序通常不用本机 kubeconfig,而是使用 ServiceAccount。Kubernetes 会自动把 ServiceAccount 的 token 挂载到 /var/run/secrets/kubernetes.io/serviceaccount/token,把 CA 证书挂载到同目录下的 ca.crt,把当前 namespace 写入同目录的 namespace 文件。rest.InClusterConfig() 会读取这些挂载路径,自动构造连接配置:
go
import "k8s.io/client-go/rest"
config, err := rest.InClusterConfig()
if err != nil {
// 不在 Pod 内运行时(比如本地开发),会因为没有挂载路径而报错
panic(err.Error())
}
clientset, err := kubernetes.NewForConfig(config)本机开发时用 BuildConfigFromFlags(读 kubeconfig),部署到集群内时用 InClusterConfig()。两种方式可以按优先级封装:先尝试集群内配置,失败时回退到 kubeconfig。
六、ClientSet
ClientSet 是访问 Kubernetes 内置资源的类型化客户端。Pod 属于 core group 的 v1,所以通过 CoreV1() 访问。
修改文件:cmd/ops-checker/main.go
先给入口加 kubeconfig 参数:
go
kubeconfigPath := flag.String("kubeconfig", "", "kubeconfig path")再创建 ClientSet:
go
clientset, err := kube.NewClientSet(*kubeconfigPath)
if err != nil {
fmt.Fprintln(os.Stderr, "create kubernetes client:", err)
os.Exit(2)
}外部运行时显式传 kubeconfig:
bash
go run ./cmd/ops-checker \
-config configs/targets.json \
-kubeconfig ~/.kube/configClientSet 创建失败属于工具自身错误,用退出码 2。常见原因是 kubeconfig 路径错误、认证插件不可用、证书不匹配、API Server 地址不可达。
七、Pod 检查
查询 Pod 对应的 kubectl 命令大概是:
bash
kubectl get pod -n default -l app=nginxclient-go 里同一件事写成:
go
pods, err := clientset.CoreV1().Pods(target.Namespace).List(ctx, metav1.ListOptions{
LabelSelector: target.LabelSelector,
})target.Namespace 对应 -n default,LabelSelector 对应 -l app=nginx。ListOptions 里还能放字段选择器、分页参数和 resourceVersion;当前 Pod 检查只用标签选择器。
新增文件:internal/checker/kubernetes.go
go
package checker
import (
"context"
"fmt"
"example.com/ops-checker/internal/config"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)
func CheckKubernetesPods(ctx context.Context, clientset *kubernetes.Clientset, target config.Target) Result {
pods, err := clientset.CoreV1().Pods(target.Namespace).List(ctx, metav1.ListOptions{
LabelSelector: target.LabelSelector, // 和 kubectl -l 的含义一致
})
if err != nil {
return Result{Name: target.Name, OK: false, Message: err.Error()}
}
ready := 0
for _, pod := range pods.Items {
if isPodReady(pod) {
ready++
}
}
minReady := target.MinReady
if minReady <= 0 {
minReady = 1 // 不写 min_ready 时,至少要求有 1 个 Ready Pod
}
ok := ready >= minReady
return Result{
Name: target.Name,
OK: ok,
Message: fmt.Sprintf("ready=%d total=%d min_ready=%d", ready, len(pods.Items), minReady),
}
}同一个文件里再加 Pod Ready 判断:
go
func isPodReady(pod corev1.Pod) bool {
if pod.Status.Phase != corev1.PodRunning {
return false
}
for _, condition := range pod.Status.Conditions {
if condition.Type == corev1.PodReady && condition.Status == corev1.ConditionTrue {
return true
}
}
return false
}代码没有把 Running 直接当作健康。Pod 进入 Running 只说明容器已经启动,Ready condition 才说明它已经通过探针并能接流量。
八、检查分发
HTTP 和 Kubernetes 检查按 target.Type 分发。原来的 worker 只调用 CheckHTTP,现在改成统一入口。
修改文件:internal/checker/runner.go
新增函数:RunOne
go
func RunOne(ctx context.Context, clientset *kubernetes.Clientset, target config.Target) Result {
switch target.Type {
case "http":
return CheckHTTP(ctx, target)
case "kubernetes_pods":
return CheckKubernetesPods(ctx, clientset, target)
default:
return Result{Name: target.Name, OK: false, Message: "unknown target type: " + target.Type}
}
}继续修改文件:internal/checker/runner.go
worker 里把原来的 CheckHTTP 替换成 RunOne:
go
for target := range jobs {
results <- RunOne(ctx, clientset, target)
}函数签名也要带上 ClientSet:
go
func Run(ctx context.Context, clientset *kubernetes.Clientset, targets []config.Target, concurrency int) []Result这样 HTTP 检查、Kubernetes 检查仍然走同一个结果模型、同一套输出和同一套退出码。新增检查类型时,主要是增加一个检查函数和一个 case。
九、RBAC
client-go 最终仍然受 Kubernetes RBAC 控制。程序是否能 list Pod,取决于 kubeconfig 里的用户或 Pod 里的 ServiceAccount 有没有权限。
本机 kubeconfig 验证:
bash
kubectl auth can-i list pods -n default
kubectl auth can-i list pods -APod 内 ServiceAccount 验证:
bash
kubectl auth can-i list pods \
--as=system:serviceaccount:ops-system:ops-checker \
-n default常见错误:
| 错误 | 读到时的含义 | 排查入口 |
|---|---|---|
connection refused | API Server 地址不通或端口错 | kubeconfig、网络、代理 |
x509: certificate signed by unknown authority | CA 证书不匹配 | kubeconfig 里的 cluster CA |
Unauthorized | 认证信息无效或过期 | token、证书、exec 登录插件 |
forbidden | 认证通过,但 RBAC 不允许 | kubectl auth can-i |
context deadline exceeded | 请求超时 | 网络、API Server 负载、DNS |
forbidden 和 Unauthorized 不一样。Unauthorized 是身份没通过;forbidden 是身份通过了,但权限不够。
十、Informer
ops-checker 当前是一次性巡检命令:启动、读取配置、查询 API Server、输出结果、退出。这种模式用 ClientSet 的 List 就够了。
Informer 属于长期运行的程序。它会先 List 当前对象,再 Watch 后续变化,把对象放到本地缓存里:
只 Watch 不 List,会漏掉程序启动前已经存在的对象;只 List 不 Watch,又需要反复全量查询。Informer 把这两个动作合在一起,并处理断线重连和本地缓存。
ops-checker 处在命令行巡检形态时,ClientSet 的一次性查询已经够用。常驻进程或控制器形态再进入 Informer。
十一、kubectl 对照
client-go 代码里的问题,常用 kubectl 复现同一件事:
| Go 程序动作 | kubectl 对照 |
|---|---|
| 读取 kubeconfig | kubectl config view --minify |
| 测试连接 | kubectl get ns |
| 查询 Pod | kubectl get pod -n default -l app=nginx |
| 看 Pod Ready | kubectl get pod -n default -o wide |
| 看完整字段 | kubectl get pod <name> -n default -o yaml |
| 查权限 | kubectl auth can-i list pods -n default |
kubectl 使用同一份 kubeconfig、同一个 namespace、同一个 label selector 能查到对象,再回到程序里看 RESTConfig、ClientSet 调用和错误处理,能缩小问题范围。
十二、目录结构
接入 client-go 后,目录比上一版多了 internal/kube/client.go 和 internal/checker/kubernetes.go:
text
ops-checker/
├── cmd/
│ └── ops-checker/
│ └── main.go
├── configs/
│ └── targets.json
└── internal/
├── checker/
│ ├── http.go
│ ├── kubernetes.go
│ ├── result.go
│ └── runner.go
├── config/
│ └── target.go
├── kube/
│ └── client.go
└── output/
├── json.go
└── text.gointernal/kube/client.go 负责把 kubeconfig 变成 ClientSet;internal/checker/kubernetes.go 负责具体检查逻辑。连接集群和判断对象状态分开后,增加 Deployment、Node、EndpointSlice 检查时,main.go 仍然只串流程。