Kubernetes调度与服务暴露:从"定时任务"到"服务发现"的完全指南
引言:让集群"记住"该做什么,并让外部"找到"该访问哪里
从Linux cron到K8s CronJob:定时任务的"进化论"
想象一下你需要在每天凌晨2点备份数据库,或者在每小时第15分钟清理日志。在Linux服务器上,我们用cron来设置这些周期性的任务。在Kubernetes集群中,同样需要这样的"定时器"------这就是CronJob。
专业视角:CronJob是Kubernetes的工作负载API的一部分,用于创建和管理基于时间的、周期性的Job(即一次性任务)。它解决了容器化环境中自动化运维的定时需求,例如定期备份、报表生成、数据清理等。
从Pod直接访问到Service:服务暴露的"智慧门牌"
当你运行一组Pod提供Web服务时,这些Pod的IP地址会随着重启、扩缩容而变化。客户端如何稳定地访问它们?就像一栋大楼里有很多房间,每个房间的号码可能会变,但大楼的前台(Service)有一个固定的总机号码,你只要拨打总机,前台就会帮你转接到正确的房间。
专业视角:Service是Kubernetes中一种抽象,它定义了一组Pod的逻辑集合以及访问它们的策略。Service提供了一个固定的虚拟IP和DNS名称,作为前端,后端Pod的变化对客户端完全透明。
第一部分:CronJob------Kubernetes的"定时器"
CronJob是什么?为什么需要它?
类比 :Linux中有cron程序定时执行任务(比如每天凌晨运行备份脚本),Kubernetes的CronJob提供了完全类似的功能,只不过它调度的是Job(一次性Pod),而不是Shell命令。
核心特性:
- 基于标准的Cron时间表语法
- 自动创建和管理Job对象
- 支持并发控制(允许、禁止、替换)
- 可配置成功/失败历史保留数量
命名限制:CronJob的名称必须是一个合法的DNS子域值,且不超过52个字符。因为CronJob控制器会自动在名称后追加11个字符(用于生成Job名称),而Job名称总长度不能超过63个字符。
快速上手:创建第一个CronJob
使用kubectl命令创建
bash
# 最简单的例子:每2分钟输出一行文字
[root@master30 ~]# kubectl create cronjob mycronjob \
--image=busybox \
--schedule='*/2 * * * *' \
-- echo hello k8s job!
等待几分钟,你会看到自动生成的Pod和Job:
bash
[root@master30 ~]# kubectl get all
NAME READY STATUS RESTARTS AGE
pod/mycronjob-28301160-gvm2v 0/1 Completed 0 5m18s
pod/mycronjob-28301162-25jll 0/1 Completed 0 3m18s
pod/mycronjob-28301164-h7mf7 0/1 Completed 0 78s
NAME SCHEDULE SUSPEND ACTIVE LAST SCHEDULE AGE
cronjob.batch/mycronjob */2 * * * * False 0 78s 36m
NAME COMPLETIONS DURATION AGE
job.batch/mycronjob-28301160 1/1 18s 5m18s
job.batch/mycronjob-28301162 1/1 19s 3m18s
job.batch/mycronjob-28301164 1/1 19s 78s
等效的YAML配置文件
yaml
apiVersion: batch/v1beta1 # 注意:v1版本中为batch/v1
kind: CronJob
metadata:
name: mycronjob
spec:
schedule: "*/2 * * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: hello
image: busybox
command: ["echo", "hello k8s job!"]
restartPolicy: Never
专业解释:
apiVersion:CronJob的API版本,在Kubernetes 1.21+为batch/v1,本文示例使用旧版batch/v1beta1。spec.schedule:Cron时间表达式,定义了触发频率。spec.jobTemplate:Job模板,定义了每次触发时要创建的任务规格。restartPolicy: Never:Job的Pod完成运行后不重启。
CronJob核心参数详解
1. 时间表语法(标准Cron)
text
# ┌───────────── 分钟 (0 - 59)
# │ ┌───────────── 小时 (0 - 23)
# │ │ ┌───────────── 月的某天 (1 - 31)
# │ │ │ ┌───────────── 月份 (1 - 12)
# │ │ │ │ ┌───────────── 周的某天 (0 - 6)(周日到周六)
# │ │ │ │ │
# * * * * *
常用示例:
| 表达式 | 含义 |
|---|---|
*/5 * * * * |
每5分钟执行一次 |
0 2 * * * |
每天凌晨2点执行 |
0 9 * * 1 |
每周一上午9点执行 |
0 0 1 * * |
每月1号午夜执行 |
0 0 1 1 * |
每年1月1日午夜执行 |
2. 任务模板(jobTemplate)
与标准的Job定义完全一致,包含Pod模板、并行策略、完成计数等。你可以为模板化的Job指定通用的元数据(标签、注解)。
3. 启动截止期限(startingDeadlineSeconds)
概念:如果任务由于某种原因错过了预期的调度时间,该参数定义了可以延迟启动的最大秒数。超过这个时间,本次调度将被跳过(视为失败),而未来的调度不受影响。
典型场景:你设置了一个每天凌晨2点的备份任务,允许最多延迟30分钟。如果凌晨2点时控制器因故没有创建Job,那么只要在2:30之前创建,它仍会执行;超过2:30,本次备份被跳过,等待第二天。
配置方式:
yaml
spec:
startingDeadlineSeconds: 200 # 最多延迟200秒
如果不设置,则没有最后期限,控制器会尽可能补执行错过的任务(但可能引发大量积压)。
4. 并发策略(concurrencyPolicy)
定义了当上一个任务尚未完成,新的调度时间又到来时的处理方式。
| 值 | 行为 | 适用场景 |
|---|---|---|
Allow(默认) |
允许并发执行,新旧任务同时运行 | 任务可并发且资源充足 |
Forbid |
禁止并发,新任务跳过,等待下一次调度 | 任务互斥(如操作同一文件) |
Replace |
终止当前正在运行的任务,用新任务替换 | 只关心最新状态的任务 |
注意:并发规则仅适用于同一个CronJob创建的任务。不同CronJob的任务总是允许并发。
5. 历史限制(successfulJobsHistoryLimit / failedJobsHistoryLimit)
successfulJobsHistoryLimit:保留的成功完成Job的数量(默认3)failedJobsHistoryLimit:保留的失败Job的数量(默认1)
设置为0表示不保留对应类型的已完成Job。这些限制有助于防止大量历史Job堆积占用API资源。
综合案例
案例1:定期清理主机目录内容
需求 :每分钟清空worker31节点上的/var/data目录。
思路:
- 使用
hostPath卷将主机目录挂载到Pod中 - 通过
nodeName将Pod调度到指定节点 - Pod执行
rm -fr /var/data/*命令 - 用CronJob每分钟触发一次
yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: clean
spec:
schedule: "* * * * *"
jobTemplate:
spec:
template:
spec:
nodeName: worker31.whisky.cloud # 固定到某个节点
containers:
- name: clean
image: busybox
command:
- sh
- -c
- sleep 3 && rm -fr /var/data/*
volumeMounts:
- name: data
mountPath: /var/data
volumes:
- name: data
hostPath:
path: /var/data
restartPolicy: OnFailure
专业拓展 :生产环境不建议直接使用nodeName,而应使用nodeSelector或亲和性。此外,可以结合securityContext设置权限提升。
案例2:通过CronJob操作Kubernetes集群
需求 :每分钟删除namespace controller中标签为app=hello的所有Pod。
思路:
- 使用
bitnami/kubectl镜像(内含kubectl客户端) - 将当前集群的kubeconfig通过ConfigMap挂载到Pod中
- Pod执行
kubectl delete pods -l app=hello -n controller
yaml
# 1. 创建ConfigMap保存kubeconfig
[root@master ~]# kubectl create cm kubeconfig --from-file=config=/root/.kube/config
# 2. 创建CronJob
apiVersion: batch/v1
kind: CronJob
metadata:
name: clean-pods
spec:
schedule: "* * * * *"
jobTemplate:
spec:
template:
spec:
restartPolicy: Never
containers:
- name: kubectl
image: bitnami/kubectl:latest
imagePullPolicy: IfNotPresent
args:
- delete
- pods
- -l
- app=hello
- -n
- controller
volumeMounts:
- name: kubeconfig
mountPath: "/.kube"
volumes:
- name: kubeconfig
configMap:
name: kubeconfig
安全提醒:生产环境中应使用RBAC和服务账户代替直接挂载管理员kubeconfig。可以创建一个具有特定权限的ServiceAccount,并授予其删除Pod的权限,然后将该ServiceAccount关联到CronJob的Pod。
第二部分:Service------稳定访问Pod的"智能代理"
为什么需要Service?
Pod的生命周期是短暂的,它们可以被创建、销毁、重新调度。每次重启或重新调度,Pod的IP地址都可能改变。如果客户端直接访问Pod IP,那么当Pod发生变化时,客户端就需要不断更新地址------这显然不可行。
类比:Pod就像餐馆后厨的厨师,Service就是前台的接待员。顾客(客户端)只和接待员说话(统一的电话号码),接待员再把请求转给某个空闲的厨师。不管厨师怎么换班,顾客永远只需拨打同一个号码。
Service的核心功能:
- 固定访问入口:提供稳定的虚拟IP(ClusterIP)和DNS名称。
- 负载均衡:将流入的请求分发到后端多个Pod。
- 服务发现:后端Pod的变化(扩缩容、更新)对客户端完全透明。
快速体验:从Pod直接暴露到使用Service
场景1:Pod直接使用hostPort(不推荐)
yaml
# pod-web.yml
apiVersion: v1
kind: Pod
metadata:
labels:
run: web
name: web
spec:
containers:
- image: httpd
name: web
ports:
- containerPort: 80
hostPort: 8080 # 将容器端口映射到节点端口
问题:
- 客户端必须知道Pod所在节点的IP和端口(
http://worker31:8080) - 当Pod被删除或迁移时,客户端无法自动更新
- 无法在同一节点上运行多个相同端口的Pod(端口冲突)
场景2:Deployment + hostPort(更糟)
bash
kubectl create deployment web --image=httpd --replicas=4
# 如果为每个Pod都设置hostPort:8080,只有2个Pod能启动(另外2个Pending)
# 因为每个节点只能有一个Pod占用8080端口
场景3:引入Service(正确做法)
bash
# 创建Deployment
kubectl create deployment web --image=httpd --replicas=3
# 创建Service(ClusterIP类型)
kubectl expose deployment web --port=8080 --target-port=80
现在,你可以通过Service的固定IP(如10.103.19.150:8080)访问,后端Pod的变化完全不可见。
Service基本管理
创建Service的方式
方式1:kubectl expose命令
bash
kubectl expose deployment web --port=8080 --target-port=80
方式2:kubectl create service命令
bash
kubectl create service clusterip web --tcp=8080:80
方式3:YAML文件
yaml
apiVersion: v1
kind: Service
metadata:
name: web
labels:
app: web
spec:
ports:
- port: 8080 # Service监听的端口
protocol: TCP
targetPort: 80 # 转发到Pod的端口
selector:
app: web # 选择具有此标签的Pod
type: ClusterIP
验证Service的负载均衡
bash
# 获取Pod名称
kubectl get pods -l app=web -o name
# 修改每个Pod的首页内容,显示Pod名称
for pod in $(kubectl get pods -l app=web -o name | cut -d/ -f2); do
kubectl exec $pod -- sh -c "echo $pod > /usr/local/apache2/htdocs/index.html"
done
# 多次访问Service,观察流量分发
for i in {1..30}; do curl -s 10.103.19.150:8080; done | sort | uniq -c
# 输出示例:三个Pod的访问次数大致相等
Service如何匹配Pod?
Service通过标签选择器(selector) 来动态确定后端Pod集合。只要Pod的标签与Service的selector匹配(无论该Pod是由Deployment创建、手动创建,还是其他控制器创建),Service就会自动将其纳入负载均衡池。
bash
# 手动创建一个具有相同标签的Pod
kubectl run web-extra --image=httpd --labels="app=web"
# 现在Service的后端Pod数量变为4个
Service发现:集群内如何访问Service
Kubernetes为集群内的Pod提供了三种发现Service的方式:
1. 通过ClusterIP直接访问
每个Service被分配一个虚拟IP(ClusterIP),集群内的任意Pod都可以直接访问该IP。
bash
# 在其他Pod中
curl http://10.103.19.150:8080
2. 通过环境变量访问
当Pod启动时,kubelet会为每个活跃的Service设置一系列环境变量。格式为:
{SVCNAME}_SERVICE_HOST{SVCNAME}_SERVICE_PORT
示例:
bash
# 启动一个busybox Pod并查看环境变量
kubectl run test --rm -it --image=busybox -- sh
/ # env | grep WEB
WEB_SERVICE_HOST=10.103.19.150
WEB_SERVICE_PORT=8080
限制 :环境变量只有在Pod创建之后创建的Service才不会被注入。因此,推荐使用DNS发现。
3. 通过DNS名称访问(推荐)
Kubernetes集群内置了CoreDNS,为每个Service创建DNS记录。记录格式为:
<service-name>.<namespace>.svc.cluster.local
同一命名空间内可简写为<service-name>。
bash
# 在任意Pod中
wget -qO- http://web.service:8080 # 跨命名空间
wget -qO- http://web:8080 # 同命名空间
示例 :部署WordPress+MySQL,WordPress使用DNS名称mysql连接数据库,而无需关心MySQL Service的实际IP。
Service类型详解
Kubernetes提供四种Service类型,以适应不同的访问场景。
| 类型 | 访问范围 | 典型用途 |
|---|---|---|
| ClusterIP | 仅集群内部 | 内部微服务通信 |
| NodePort | 集群外部通过节点IP+静态端口 | 开发测试、简单暴露 |
| LoadBalancer | 外部通过云负载均衡器IP | 生产环境外部访问 |
| ExternalName | 映射到外部域名 | 访问外部服务(如数据库) |
1. ClusterIP(默认)
- 分配一个内部虚拟IP,仅在集群内可访问。
- 是其他Service类型的基础。
2. NodePort
- 在每个节点上开放一个固定端口(默认30000-32767)。
- 外部客户端可以通过
<任意节点IP>:<NodePort>访问Service。 - 本质上是在ClusterIP基础上增加了节点端口映射。
yaml
apiVersion: v1
kind: Service
metadata:
name: web-nodeport
spec:
type: NodePort
ports:
- port: 8080
targetPort: 80
nodePort: 30080 # 可选,不指定则自动分配
selector:
app: web
bash
# 访问任一节点
curl http://10.1.8.30:30080
curl http://10.1.8.31:30080
iptables规则示意:
text
-A KUBE-NODEPORTS -p tcp --dport 30080 -j KUBE-SVC-XXXX
3. LoadBalancer
- 在NodePort的基础上,向外部负载均衡器申请一个公网/私网IP。
- 云提供商(AWS、GCP、Azure)或本地LB方案(如MetalLB)实现。
- 外部客户端通过负载均衡器的IP直接访问。
使用MetalLB搭建本地LoadBalancer:
bash
# 部署MetalLB
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.8/config/manifests/metallb-native.yaml
# 配置IP地址池
cat <<EOF | kubectl apply -f -
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: first-pool
namespace: metallb-system
spec:
addresses:
- 10.1.8.40-10.1.8.80
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: example
namespace: metallb-system
EOF
# 创建LoadBalancer类型的Service
kubectl expose deployment web --type=LoadBalancer --port=80 --target-port=80
kubectl get svc web
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# web LoadBalancer 10.109.101.67 10.1.8.40 80:32101/TCP 5s
流量路径 :
客户端 → MetalLB虚拟IP(10.1.8.40) → 节点kube-proxy → Service负载均衡 → Pod
4. ExternalName
- 将Service映射到外部的DNS名称,不创建任何代理。
- 集群内部Pod可以通过Service名称访问外部服务。
yaml
apiVersion: v1
kind: Service
metadata:
name: external-db
spec:
type: ExternalName
externalName: database.example.com
当Pod解析external-db时,CoreDNS返回database.example.com的CNAME记录。
5. Headless Service
- 设置
clusterIP: None,不分配虚拟IP。 - 用于需要直接访问每个Pod的场景(如StatefulSet)。
- 核心DNS会返回Pod IP列表(A记录),而不是Service IP。
yaml
apiVersion: v1
kind: Service
metadata:
name: headless-svc
spec:
clusterIP: None
selector:
app: myapp
ports:
- port: 80
Service高级特性
会话保持(Session Affinity)
问题:某些应用需要将同一个客户端的请求始终转发到同一个后端Pod(如购物车、WebSocket)。
解决方案 :设置sessionAffinity: ClientIP。
bash
# 修改现有Service
kubectl patch svc web -p '{"spec":{"sessionAffinity":"ClientIP"}}'
# 可选:设置超时时间(默认10800秒=3小时)
kubectl patch svc web -p '{"spec":{"sessionAffinityConfig":{"clientIP":{"timeoutSeconds":3600}}}}'
原理:kube-proxy根据客户端IP的哈希值选择后端Pod,相同IP的请求始终映射到同一个Pod。
注意:会话保持与后端Pod扩缩容不兼容------如果目标Pod被删除,该客户端的会话将中断。
金丝雀发布(Canary Deployment)结合Service
利用Service的标签选择器,可以实现新旧版本按比例流量切换。
场景:将10%的流量导入新版本(canary),其余90%仍在旧版本(stable)。
实现步骤:
- 创建旧版本Deployment(stable)和新版本Deployment(canary),它们有共同的标签(如
app: web, tier: frontend)。 - 创建Service,selector为
app: web, tier: frontend,这样Service会把流量同时分发给两个版本。 - 通过调整两个Deployment的副本数来控制流量比例。如果Service使用默认的轮询负载均衡,副本数比例≈流量比例。
yaml
# 旧版本 Deployment (stable)
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-stable
spec:
replicas: 9
selector:
matchLabels:
app: web
tier: frontend
track: stable
template:
metadata:
labels:
app: web
tier: frontend
track: stable
spec:
containers:
- image: nginx:1.28
name: nginx
---
# 新版本 Deployment (canary)
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-canary
spec:
replicas: 1
selector:
matchLabels:
app: web
tier: frontend
track: canary
template:
metadata:
labels:
app: web
tier: frontend
track: canary
spec:
containers:
- image: nginx:1.29
name: nginx
---
# Service
apiVersion: v1
kind: Service
metadata:
name: web
spec:
selector:
app: web
tier: frontend
ports:
- port: 80
bash
# 测试流量比例
for i in {1..100}; do curl -s http://10.98.235.36; done | sort | uniq -c
# 输出大致为:stable版本约90次,canary约10次
# 逐步扩大canary副本数,减少stable副本数
kubectl scale deployment web-stable --replicas 5
kubectl scale deployment web-canary --replicas 5
# 再次测试,流量接近1:1
最终,当canary版本验证无误后,可以删除stable Deployment,只保留canary,并修改其标签为track: stable。
知识点速查手册
1. CronJob 速查表
| 字段 | 作用 | 示例值 |
|---|---|---|
schedule |
Cron时间表达式 | "*/5 * * * *" |
jobTemplate |
Job模板 | 嵌套Job定义 |
startingDeadlineSeconds |
错过调度的最大延迟秒数 | 200 |
concurrencyPolicy |
并发策略 | Allow/Forbid/Replace |
successfulJobsHistoryLimit |
保留的成功Job数 | 3 |
failedJobsHistoryLimit |
保留的失败Job数 | 1 |
常用Cron表达式:
text
*/1 * * * * # 每分钟
0 */2 * * * # 每2小时
0 0 * * 0 # 每周日0点
0 0 1 */3 * # 每季度(每3个月的第1天)
2. Service 速查表
| 类型 | ClusterIP | 外部访问 | 使用场景 |
|---|---|---|---|
| ClusterIP | 自动分配 | 否 | 内部服务 |
| NodePort | 自动分配 | 是(节点IP:NodePort) | 测试、简单暴露 |
| LoadBalancer | 自动分配 | 是(LB IP) | 生产外部访问 |
| ExternalName | 无 | 否(DNS别名) | 访问外部服务 |
| Headless | None | 否(直接Pod IP) | StatefulSet |
Service YAML核心字段:
yaml
spec:
selector: # 标签选择器,指定后端Pod
app: web
ports:
- port: 8080 # Service监听的端口
targetPort: 80 # Pod容器端口
nodePort: 30080 # NodePort类型专用
type: ClusterIP # 服务类型
sessionAffinity: ClientIP # 会话保持
clusterIP: None # Headless Service
3. 服务发现方式比较
| 方式 | 优点 | 缺点 | 推荐度 |
|---|---|---|---|
| 环境变量 | 简单,无需额外组件 | 顺序依赖,仅注入已存在的Service | 不推荐 |
| ClusterIP | 稳定,负载均衡 | 需要知道IP,IP会变(除非固定) | 一般 |
| DNS(CoreDNS) | 名称稳定,自动更新,跨命名空间 | 需要DNS组件(已默认部署) | 强烈推荐 |
4. 常见故障排查
| 症状 | 可能原因 | 排查命令 |
|---|---|---|
| CronJob不创建Job | schedule错误或concurrencyPolicy=Forbid且有未完成Job | kubectl describe cronjob <name> |
| Pod一直Pending | NodePort端口冲突或资源不足 | kubectl describe pod <name> |
| Service无法访问 | selector不匹配后端Pod | kubectl get endpoints <svc> |
| 外部无法访问NodePort | 防火墙未开放端口或节点IP不可达 | curl <nodeIP>:<nodePort> |
| DNS解析失败 | CoreDNS Pod未运行 | `kubectl get pods -n kube-system |
5. 最佳实践清单
CronJob
- 为CronJob设置
startingDeadlineSeconds避免任务积压 - 根据任务性质选择合适的并发策略
- 设置合理的历史限制,防止API资源浪费
- 对于重要任务,配置Pod级别的资源限制
- 使用ServiceAccount而非管理员kubeconfig操作集群
Service
- 使用DNS名称而非ClusterIP进行服务发现
- 为重要Service设置标签(便于监控和选择)
- 使用
targetPort命名端口(便于修改容器端口) - 生产环境使用LoadBalancer或Ingress暴露服务
- 为有状态服务配置
sessionAffinity,但要注意Pod生命周期
文档版本 :v1.0
最后更新 :2026年4月
适用平台 :Kubernetes 1.20+
作者:基于课堂笔记扩展完善
重要提醒:
- CronJob的时区默认使用kube-controller-manager的时区(通常是UTC),如需自定义时区,可使用
CRON_TZ变量(需特定版本支持)。 - Service的负载均衡基于kube-proxy模式(iptables/IPVS),默认是随机或轮询;大规模集群推荐使用IPVS模式。
- 金丝雀发布是持续交付的重要策略,结合Service可以实现更精细的流量控制(如基于Header的路由需要Ingress)。
下一步学习:
- 学习Ingress Controller实现7层路由和TLS终止
- 研究Helm管理CronJob和Service的模板化部署
- 了解Service Mesh(如Istio)提供更高级的流量管理能力
本回答由 AI 生成,内容仅供参考,请仔细甄别。