从单Pod到Service:在K8s中部署高可用Nginx的完整流程
从单Pod到Service:在K8s中部署高可用Nginx的完整流程
如果你已经对Kubernetes的基础概念有所了解,比如知道Pod是什么,也用过kubectl进行过一些简单操作,那么接下来面临的一个很实际的问题就是:如何将一个简单的应用,比如Nginx,从一个随时可能挂掉的“裸奔”Pod,一步步打造成一个稳定、可扩展、能对外提供服务的高可用服务?这个过程不仅仅是运行一个容器那么简单,它涉及到Kubernetes核心的编排思想——如何管理应用的生命周期、如何实现服务发现、如何保障业务连续性。
很多教程会直接告诉你创建一个Deployment和一个Service,但知其然更要知其所以然。这篇文章,我将以一个真实的演进视角,带你从最基础的单个Pod部署开始,逐步引入Deployment控制器,最后通过Service实现服务的稳定暴露。我会穿插大量实际操作命令、配置示例,以及我在实践中踩过的坑和总结的技巧,目标是让你不仅能跟着做出来,更能理解每一步背后的设计意图和最佳实践。无论你是想巩固K8s实战技能,还是正在为生产环境部署应用做准备,这篇深度指南都将为你提供清晰的路径。
1. 起点:理解Pod的脆弱性与部署实践
在Kubernetes的世界里,Pod是最小的可部署单元。你可以把它想象成一个逻辑主机,里面运行着一个或多个紧密相关的容器。我们最初的尝试,往往就是从运行一个简单的Pod开始的。
1.1 直接运行Pod:快速但脆弱
使用kubectl run命令可以快速创建一个Pod。假设我们想在dev这个命名空间里运行一个Nginx:
kubectl create namespace dev
kubectl run nginx-pod --image=nginx:latest --port=80 -n dev
执行后,用kubectl get pods -n dev -o wide查看,你会看到Pod被调度到某个节点上,并分配了一个集群内部的IP地址(如10.244.1.5)。此时,你可以在集群内部通过这个IP直接访问Nginx的欢迎页面。
注意:
kubectl run命令在较新版本的K8s中,默认创建的是Deployment而不是Pod。为了直接创建Pod,可能需要使用--restart=Never参数,或者更推荐的方式是使用YAML文件声明式创建。
这种方式的问题立刻显现:
- 单点故障:这个Pod如果所在节点宕机,或者容器自身崩溃,服务就中断了。
- IP不固定:Pod重启后,其IP地址会改变。你无法依赖一个固定的IP来访问服务。
- 无法扩展:手动管理多个相同的Pod副本极其繁琐,无法应对流量增长。
这就像在服务器上直接docker run一个容器,没有利用到K8s任何编排能力。因此,直接使用裸Pod(bare Pod)在生产环境中是绝对不推荐的,它只适用于一些一次性任务(Job/CronJob)或学习测试场景。
1.2 使用YAML定义Pod:声明式配置的入门
更规范的做法是使用YAML文件来定义Pod。这引入了K8s的核心哲学:声明式API。你告诉系统“我想要什么状态”,而不是“执行什么操作”。
创建一个nginx-pod.yaml文件:
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
namespace: dev
labels:
app: nginx
version: v1
spec:
containers:
- name: nginx-container
image: nginx:latest
ports:
- containerPort: 80
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
然后应用它:
kubectl apply -f nginx-pod.yaml
这个YAML文件比命令行提供了更丰富、更可控的配置能力:
- 标签(Labels):
app: nginx和version: v1。这是K8s进行资源分组和选择的关键元数据,后续的Deployment和Service都会依赖它。 - 资源限制(Resources):定义了容器请求(
requests)和上限(limits)的CPU和内存。这是保障集群稳定性和应用性能的必备项,防止某个应用耗尽节点资源。 - 声明式管理:使用
apply而非create。apply是幂等的,你可以反复执行来确保集群状态与文件描述一致。如果后续修改了YAML文件,再次apply即可更新Pod配置。
尽管我们规范了Pod的定义,但单点脆弱性问题依然存在。我们需要一个“管家”来管理Pod的生命周期,这就是控制器(Controller)。
2. 进化:引入Deployment实现高可用与滚动更新
Deployment是管理无状态应用最常用的控制器。它为我们解决了Pod的哪些痛点?请看下表对比:
| 特性 | 裸 Pod | Deployment管理的Pod |
|---|---|---|
| 高可用性 | 无。Pod挂了需手动干预。 | 自动维持指定数量的副本(Replicas)。Pod失败会自动重建。 |
| 伸缩性 | 手动操作,极其麻烦。 | 一行命令或修改YAML即可轻松扩缩容(kubectl scale)。 |
| 滚动更新 | 无法实现。需要手动删除旧Pod,创建新Pod,导致服务中断。 | 支持无缝滚动更新,可控制更新节奏和回滚策略。 |
| 版本回滚 | 无。 | 轻松回滚到任意历史版本。 |
| 配置管理 | 相对松散。 | 与Pod模板(Pod Template)紧密绑定,配置变更可追溯。 |
2.1 创建你的第一个Deployment
让我们用YAML创建一个管理3个Nginx副本的Deployment。文件nginx-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
namespace: dev
spec:
replicas: 3 # 核心:指定副本数量
selector:
matchLabels:
app: nginx # 选择器,用于匹配Pod标签
template: # Pod模板
metadata:
labels:
app: nginx # Pod必须拥有此标签,才能被Deployment管理
spec:
containers:
- name: nginx
image: nginx:1.20-alpine # 使用一个更具体的标签,而非latest
ports:
- containerPort: 80
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "200m"
memory: "256Mi"
livenessProbe: # 存活探针,检查容器是否健康
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe: # 就绪探针,检查容器是否准备好接收流量
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 10
应用并观察:
kubectl apply -f nginx-deployment.yaml
kubectl get deployments.apps -n dev -w # -w 参数用于持续观察状态变化
kubectl get pods -n dev --show-labels
你会看到Deployment创建了一个ReplicaSet(可以通过kubectl get rs -n dev查看),并由ReplicaSet创建了3个名称带随机哈希的Pod。Deployment通过ReplicaSet来确保始终有3个健康的Pod在运行。
2.2 深入Deployment的核心操作
扩缩容:
# 扩容到5个副本
kubectl scale deployment nginx-deployment --replicas=5 -n dev
# 或者通过修改YAML文件中的replicas字段后再次apply
滚动更新镜像:
假设我们要将Nginx升级到1.21-alpine版本。
kubectl set image deployment/nginx-deployment nginx=nginx:1.21-alpine -n dev
此时,K8s会启动一个新的ReplicaSet,逐步创建新版本的Pod,并逐步终止旧版本的Pod。你可以通过以下命令观察更新过程:
kubectl rollout status deployment/nginx-deployment -n dev
kubectl describe deployment nginx-deployment -n dev
更新策略:在Deployment的YAML中,可以通过strategy字段控制更新行为。默认是RollingUpdate,它有两个关键参数:
maxUnavailable:在更新过程中,最多允许多少个Pod不可用(可以是数字或百分比,如25%)。maxSurge:在更新过程中,最多可以创建多少个超出期望副本数的Pod(如25%)。
版本回滚: 如果新版本有问题,可以快速回滚。
# 查看发布历史
kubectl rollout history deployment/nginx-deployment -n dev
# 回滚到上一个版本
kubectl rollout undo deployment/nginx-deployment -n dev
# 回滚到指定版本
kubectl rollout undo deployment/nginx-deployment --to-revision=2 -n dev
就绪探针(Readiness Probe)的重要性:在上面的YAML中,我们配置了就绪探针。它的作用是告诉Service和Ingress控制器,这个Pod是否已经准备好接收流量。如果没有就绪探针,K8s会在Pod容器启动后立即将其加入服务的负载均衡池,如果此时应用还在初始化(如加载配置、连接数据库),就会导致请求失败。配置了就绪探针后,只有探针检查成功,Pod才会被标记为就绪,从而接收流量。
现在,我们有了多个稳定运行的Pod副本。但是,客户端该如何访问它们呢?直接使用Pod IP显然不行,因为它们是动态的。我们需要一个稳定的访问入口。
3. 统一入口:使用Service实现服务发现与负载均衡
Service是K8s中定义了一组Pod访问策略的抽象。它为Pod提供了一个稳定的虚拟IP(ClusterIP)和DNS名称,并负责将请求负载均衡到后端的健康Pod上。
3.1 Service的工作原理与核心类型
Service通过selector(选择器)与一组Pod关联。它持续监控符合标签选择器的Pod,并更新自己的端点(Endpoints)列表。当有请求到达Service时,它会根据配置的负载均衡策略(如轮询)将请求转发到其中一个Pod。
Kubernetes Service主要有三种类型,适用于不同场景:
| 类型 | 说明 | 适用场景 |
|---|---|---|
| ClusterIP | 默认类型。为Service分配一个集群内部的虚拟IP,只能在集群内部访问。 | 微服务间的内部通信。这是最常用的类型。 |
| NodePort | 在ClusterIP基础上,在每个集群节点上开放一个静态端口(NodePort)。通过<NodeIP>:<NodePort>可以从集群外部访问服务。 | 开发测试,或需要从外部直接访问的简单服务。 |
| LoadBalancer | 在NodePort基础上,与云提供商的负载均衡器集成,自动创建外部负载均衡器并分配外部IP。 | 在公有云上运行,需要暴露服务到公网。 |
3.2 创建ClusterIP Service
为我们的nginx-deployment创建一个内部服务。文件nginx-service-clusterip.yaml:
apiVersion: v1
kind: Service
metadata:
name: nginx-service
namespace: dev
spec:
type: ClusterIP # 可省略,默认就是ClusterIP
selector:
app: nginx # 关键:选择标签为 app: nginx 的Pod
ports:
- port: 80 # Service对外暴露的端口
targetPort: 80 # Pod容器内监听的端口
protocol: TCP
创建并查看:
kubectl apply -f nginx-service-clusterip.yaml
kubectl get svc -n dev
你会看到nginx-service被分配了一个CLUSTER-IP(如10.96.123.456)。这个IP在Service的生命周期内是稳定的。
现在,在集群内部的任何Pod或节点上,你都可以通过这个ClusterIP或者Service的DNS名称(nginx-service.dev.svc.cluster.local)来访问Nginx服务,并且请求会被自动负载均衡到后端的3个Pod上。
# 在集群内另一个临时Pod中进行测试
kubectl run curl-test --image=radial/busyboxplus:curl -i --rm --restart=Never -n dev --command -- sh -c 'for i in $(seq 1 5); do curl -s http://nginx-service.dev; echo; done'
你应该能看到请求被轮流分发到不同的后端Pod(如果你像之前实验那样修改了每个Pod的首页内容,会看到不同的输出)。
3.3 创建NodePort Service对外暴露
对于需要从集群外部访问的服务,我们可以使用NodePort。修改或新建一个Service定义文件nginx-service-nodeport.yaml:
apiVersion: v1
kind: Service
metadata:
name: nginx-service-external
namespace: dev
spec:
type: NodePort
selector:
app: nginx
ports:
- port: 80
targetPort: 80
nodePort: 30080 # 可选,指定范围在30000-32767之间。不指定则由系统自动分配。
应用后查看:
kubectl apply -f nginx-service-nodeport.yaml
kubectl get svc nginx-service-external -n dev -o wide
输出中会显示PORT(S)为80:30080/TCP。这意味着,你可以通过访问集群中任意节点的IP地址的30080端口来访问Nginx服务,例如http://<任意节点IP>:30080。
提示:NodePort虽然方便,但在生产环境中通常不会直接使用,因为需要用户记住IP和端口。更常见的做法是使用
LoadBalancer类型(在云环境下)或配合Ingress控制器(提供基于HTTP/HTTPS主机名和路径的路由)来对外提供服务。
3.4 深入Service:会话保持与流量策略
默认情况下,Service的会话是非保持的(Session非亲和),即每次请求可能被转发到不同的Pod。对于一些需要会话状态的应用(如登录状态),这会有问题。可以通过设置sessionAffinity为ClientIP来实现基于客户端IP的会话保持。
apiVersion: v1
kind: Service
metadata:
name: nginx-service-session
namespace: dev
spec:
type: ClusterIP
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds: 10800 # 会话保持超时时间
selector:
app: nginx
ports:
- port: 80
targetPort: 80
此外,Service还支持定义外部流量策略(externalTrafficPolicy),这对于NodePort和LoadBalancer类型很重要:
Cluster(默认):流量可以转发到集群中任何节点上的Pod,可能产生跨节点跳转。Local:流量只转发到接收流量的那个节点上运行的Pod。这可以保留原始客户端IP,但要求Pod必须调度到所有节点上,否则某些节点的请求会失败。
4. 实战进阶:配置、存储与健康检查
一个生产可用的Nginx部署,远不止运行容器本身。我们还需要考虑配置管理、静态文件持久化、更完善的监控等。
4.1 使用ConfigMap管理Nginx配置
将配置与镜像分离是云原生的重要原则。我们可以用ConfigMap来存储Nginx的配置文件。
首先,创建一个自定义的Nginx配置文件default.conf:
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
# 添加一个健康检查端点
location /healthz {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
# 自定义错误页面示例
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
然后从文件创建ConfigMap:
kubectl create configmap nginx-config --from-file=default.conf -n dev
在Deployment的Pod模板中挂载这个ConfigMap:
# 在spec.template.spec下添加
spec:
containers:
- name: nginx
image: nginx:1.20-alpine
volumeMounts:
- name: nginx-config-volume
mountPath: /etc/nginx/conf.d # 挂载到容器内的配置目录
readOnly: true
volumes:
- name: nginx-config-volume
configMap:
name: nginx-config
这样,修改default.conf文件后,只需更新ConfigMap(kubectl create ... --dry-run=client -o yaml | kubectl apply -f -),并重启Pod(或使用支持动态重载的Sidecar),即可应用新配置,无需重新构建镜像。
4.2 使用PersistentVolumeClaim挂载静态文件
如果Nginx需要提供静态文件(如图片、CSS、JS),这些文件需要持久化存储,不能放在容器内部。这时需要用到PersistentVolume(PV)和PersistentVolumeClaim(PVC)。
假设我们有一个网络存储(如NFS),并已创建好对应的PV。我们创建一个PVC来申请存储:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nginx-html-pvc
namespace: dev
spec:
accessModes:
- ReadWriteMany # 根据存储类型选择,NFS支持多节点读写
resources:
requests:
storage: 1Gi
然后在Deployment中挂载这个PVC:
spec:
containers:
- name: nginx
image: nginx:1.20-alpine
volumeMounts:
- name: nginx-config-volume
mountPath: /etc/nginx/conf.d
- name: nginx-html-volume # 挂载静态文件
mountPath: /usr/share/nginx/html
volumes:
- name: nginx-config-volume
configMap:
name: nginx-config
- name: nginx-html-volume
persistentVolumeClaim:
claimName: nginx-html-pvc
4.3 完善探针与资源监控
在之前的Deployment YAML中,我们已经添加了livenessProbe和readinessProbe。这里再强调一下它们的区别和配置技巧:
- 存活探针(Liveness):判断容器是否“活着”。如果失败,kubelet会杀死容器并根据重启策略决定是否重启。适用于检测死锁等无法自我恢复的问题。
- 就绪探针(Readiness):判断容器是否“就绪”接收流量。如果失败,Service会将该Pod从负载均衡端点中移除。适用于检测应用启动慢、依赖服务未就绪等情况。
一个更健壮的配置示例:
livenessProbe:
httpGet:
path: /healthz # 指向一个轻量的健康检查端点
port: 80
httpHeaders:
- name: Custom-Header
value: Awesome
initialDelaySeconds: 15 # 给容器足够的启动时间
periodSeconds: 20
timeoutSeconds: 5
failureThreshold: 3 # 连续失败3次才判定为不健康
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 2
successThreshold: 1
failureThreshold: 2
结合Horizontal Pod Autoscaler(HPA),你还可以实现基于CPU/内存等指标的自动扩缩容,但这通常需要安装Metrics Server等组件来提供资源指标。
走到这里,你已经完成了一个从最脆弱的单Pod,到具备高可用、可滚动更新、带健康检查、有配置和存储管理的Deployment,再到通过Service提供稳定内部访问和外部暴露的完整部署流程。这不仅仅是几个YAML文件的堆砌,而是一套应对云上应用部署的标准方法论。在实际项目中,你可能会在此基础上集成CI/CD流水线、使用Helm进行包管理、配置更复杂的Ingress路由规则,但核心的骨架——Deployment和Service——始终是那个坚实可靠的基础。理解并熟练运用它们,是掌握Kubernetes应用部署的关键一步。
更多推荐
所有评论(0)