Kubernetes Service 从入门到实践:服务发现与基本管理
Kubernetes Service 从入门到实践:服务发现与基本管理
适合读者:已经能用 Deployment 跑起 Pod,但还在为“怎么访问 Pod”发愁的 K8s 初学者。
在 Kubernetes 里跑应用,第一步往往是创建 Deployment、拉镜像、等 Pod 变成 Running。但紧接着就会遇到一个灵魂拷问:我的应用到底该怎么被别人访问?
直接用 Pod IP?不行。Pod 是会“死”的,也是会“换”的——Deployment 滚动更新、Pod 崩溃重启、节点故障重新调度,都会让 Pod 的 IP 发生变化。更麻烦的是,集群外部的客户端甚至不知道 Pod 跑在哪台节点上。
这篇文章我们就从两个“翻车现场”开始,理解 Kubernetes 为什么需要 Service,然后完整走一遍 Service 的创建、验证与服务发现。
一、先看两个“反面教材”
示例 1:用 hostPort 暴露 Pod
先创建一个最简单的 Pod:
kubectl run web --image=hub.shaka.cn/library/httpd --image-pull-policy=IfNotPresent -o yaml --dry-run=client > pod-web.yml
kubectl apply -f pod-web.yml
kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE
web 1/1 Running 0 10s 10.224.51.149 worker31.shaka.cn
Pod 有了自己的 IP,但集群外部的节点访问这个 IP 时,根本不通:
curl http://10.224.193.65
解决办法是在 Pod 上加上 hostPort,把 Pod 的 80 端口映射到所在节点的 8080 端口:
apiVersion: v1
kind: Pod
metadata:
labels:
run: web
name: web
spec:
containers:
- image: hub.shaka.cn/library/httpd
name: web
ports:
- containerPort: 80
hostPort: 8080
这次外部就能访问了:
curl http://worker31.shaka.cn:8080
<html><body><h1>It works!</h1></body></html>
但代价很明显:
- 外部客户端必须事先知道 Pod 在哪个主机上;
- 如果主机上再起一个同样使用 8080 的 Pod,端口直接冲突。
示例 2:多副本 Deployment + hostPort
创建一个 4 副本的 Deployment,同样加上 hostPort: 8080:
kubectl create deployment web --image=hub.shaka.cn/library/httpd --replicas=4 --dry-run=client -o yaml > deploy-web.yml
kubectl apply -f deploy-web.yml
kubectl get pod
NAME READY STATUS RESTARTS AGE
web-79fc679949-2tr7x 0/1 Pending 0 7s
web-79fc679949-msbc5 1/1 Running 0 7s
web-79fc679949-t6twf 0/1 Pending 0 7s
web-79fc679949-vqfr8 1/1 Running 0 7s
两个 Pod 一直 Pending。kubectl describe 会告诉我们原因:
Warning FailedScheduling 49s default-scheduler 0/3 nodes are available:
1 node(s) had taint {node-role.kubernetes.io/master: }, that the pod didn't tolerate,
2 node(s) didn't have free ports for the requested pod ports.
问题总结:
- 集群外部客户端需要知道 Pod 运行在哪个主机,才能访问到 Pod;
- Pod 由控制器管理、成批创建时,同一个主机上无法创建多个使用相同 hostPort 的 Pod。
hostPort 只适合调试和个别特殊场景,绝不适合作为常规的服务暴露方式。
二、Service 是什么
Deployment 可以动态创建和销毁 Pod。在任何时刻,你都不知道有多少个 Pod 在工作、它们健不健康,甚至不知道它们的具体 IP——Pod 是临时资源,不应该指望某个 Pod 既可靠又耐用。
这就带来一个问题:如果一组 Pod(后端)要为另一组 Pod(前端)提供功能,前端如何发现并跟踪要连接的 IP?
答案是 Service。
Service 是 Kubernetes 中的一种资源,它把运行在一个或一组 Pod 上的网络应用,暴露为一个稳定的网络服务:
- Service 有自己的 IP 和端口,并且这个 IP 不变;
- Service 通过**标签选择器(selector)**动态关联后端 Pod;
- Service 自动为后端 Pod 做负载均衡;
- 无论后端 Pod 如何增删、重启、迁移,客户端始终只访问 Service,不受任何影响。
Service 的一个关键设计目标是:让应用无需修改,就能被服务发现。不管你的代码是为云原生设计的,还是被容器化的老应用,都可以通过 Service 让一组 Pod 在网络上被访问。
三、Service 基本管理
1. 准备后端 Pod
创建一个 3 副本的 Deployment:
kubectl create deployment web --image=hub.shaka.cn/library/httpd:2.4.58 --replicas=3
kubectl get pods --show-labels
NAME READY STATUS RESTARTS AGE LABELS
web-5646dd6f6c-5hs6f 1/1 Running 0 8m43s app=web,pod-template-hash=5646dd6f6c
web-5646dd6f6c-6fjqs 1/1 Running 0 8m43s app=web,pod-template-hash=5646dd6f6c
web-5646dd6f6c-tvw78 1/1 Running 0 8m43s app=web,pod-template-hash=5646dd6f6c
注意 Pod 上都带有 app=web 标签,后面 Service 就是靠它来“认人”的。
2. 用一条命令创建 Service
kubectl create service clusterip web --tcp=8080:80
kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web ClusterIP 10.103.19.150 <none> 8080/TCP 6m58s
这里 --tcp=8080:80 的含义是:访问 ClusterIP 的 8080 端口,会转发到后端 Pod 的 80 端口。
kubectl create service 自动生成的 Service 默认标签是 app=<svc-name>,所以它刚好和 Deployment 创建的 Pod 标签 app=web 匹配上。
3. 查看 Service 详情
kubectl describe service web
Name: web
Namespace: shaka
Labels: app=web
Selector: app=web
Type: ClusterIP
IP: 10.103.19.150
Port: 8080-80 8080/TCP
TargetPort: 80/TCP
Endpoints: 10.224.193.67:80,10.224.193.68:80,10.224.41.131:80
Session Affinity: None
关键信息是 Endpoints:Service 已经自动把三个 Pod 的 IP 都纳入后端了。
curl 10.103.19.150:8080
<html><body><h1>It works!</h1></body></html>
4. 验证 Service 的负载均衡
给每个 Pod 写入不同的主页,再反复访问 Service:
for pod in $(kubectl get pods -o name | awk -F/ '{print $2}'); do
kubectl exec -it $pod -- bash -c "echo $pod > htdocs/index.html"
done
for i in {1..60}; do curl -s 10.103.19.150:8080; done | sort | uniq -c
19 web-5646dd6f6c-5hs6f
19 web-5646dd6f6c-6fjqs
22 web-5646dd6f6c-tvw78
流量被大致平均地分发到了三个 Pod 上——Service 天然具备负载均衡能力。
5. 验证动态发现
Service 的后端是动态的,不是创建时“拍板定死”的:
- 手动创建一个带相同标签
app=web的 Pod,它也会立刻被 Service 纳入后端; - 执行
kubectl rollout restart deployment web,Pod 全部重建后,Service 依然能自动发现新 Pod,客户端访问不受影响。
Service 匹配的只是标签,它并不关心后端是谁创建的。甚至 Deployment 的 selector 和 Service 的 selector 可以不一样:
# Deployment 的 selector 匹配 app1=web1
# Service 通过 expose 指定匹配 app2=web2
kubectl expose deployment web --port=8080 --target-port=80 --selector=app2=web2
只要 Pod 上同时带有这两个标签,Service 和 Deployment 就各管各的,互不干扰。
6. 一个小细节:ping 不通 ClusterIP
ping 10.103.19.150 # 不通
ClusterIP 是 kube-proxy 通过 iptables/IPVS 规则“虚拟”出来的,只对配置过的端口(如 80、8080)生效。Service 只做了 HTTP 端口映射,没有为 ICMP 配置规则,所以 ping 不通 Service IP,但可以 ping 通 Pod IP。
7. 用 YAML 创建 Service
命令行适合快速创建,正式环境一般还是把 YAML 固化到文件里。先获取模板:
kubectl create service clusterip web --tcp=8080:80 -o yaml --dry-run=client > svc-web.yml
apiVersion: v1
kind: Service
metadata:
labels:
app: web
name: web
spec:
ports:
- name: 8080-80
port: 8080
protocol: TCP
targetPort: 80
selector:
app: web
type: ClusterIP
核心字段就三个:
port:Service 对外监听的端口;targetPort:后端 Pod 的端口;selector:选择后端 Pod 的标签。
四、Service 的三种发现方式
“发现 Service”是指集群内部的应用如何访问 Service。官方推荐 DNS,但我们从最简单的 IP 方式开始,逐一感受演进过程。
实验环境用经典的 mysql + wordpress 组合:WordPress 需要连接 MySQL,正好模拟“前端找后端”。
方式一:通过 IP 访问
先创建 MySQL Pod 并暴露成 Service:
kubectl run mysql --image=hub.shaka.cn/library/mysql \
--image-pull-policy=IfNotPresent \
--env=MYSQL_ROOT_PASSWORD=redhat \
--env=MYSQL_USER=tom \
--env=MYSQL_PASSWORD=redhat \
--env=MYSQL_DATABASE=blog \
--dry-run=client -o yaml > pod-mysql.yaml
kubectl apply -f pod-mysql.yaml
kubectl expose pod mysql --port=3306 --target-port=3306
kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
mysql ClusterIP 10.111.69.45 <none> 3306/TCP 25m
然后直接使用 Service IP 连接数据库:
mysql -u tom -predhat -h 10.111.69.45 --ssl-mode=DISABLED -e 'show databases;'
能连上,但问题是:IP 是 Kubernetes 分配的,应用代码里写死 IP 并不优雅,而且换环境(测试→生产)就得改配置。
方式二:通过环境变量访问
Kubernetes 会在创建 Pod 时,把同 Namespace 中已存在的 Service 信息注入为环境变量。看一个 busybox 里的效果:
kubectl run test --rm -it --image=hub.shaka.cn/library/busybox --image-pull-policy=IfNotPresent sh
/ # env | grep MYSQL
MYSQL_PORT_3306_TCP_ADDR=10.111.69.45
MYSQL_PORT_3306_TCP_PORT=3306
MYSQL_SERVICE_HOST=10.111.69.45
MYSQL_SERVICE_PORT=3306
MYSQL_PORT=tcp://10.111.69.45:3306
所以创建 WordPress 时,可以直接引用环境变量,而不是写死 IP:
kubectl run wordpress \
--image=hub.shaka.cn/library/wordpress \
--image-pull-policy=IfNotPresent \
--env=WORDPRESS_DB_USER=tom \
--env=WORDPRESS_DB_PASSWORD=redhat \
--env=WORDPRESS_DB_NAME=blog \
--env=WORDPRESS_DB_HOST='$(MYSQL_SERVICE_HOST)'
使用环境变量有三个注意事项:
- Service 必须先创建,后创建 Pod,环境变量才会注入。顺序反了,Pod 里就没有这个变量;
- 环境变量只对同一个 Namespace 中的 Pod 生效;
- 名字是固定的
{SERVICE_NAME}_SERVICE_HOST/{SERVICE_NAME}_SERVICE_PORT,大小写有讲究。
方式三:通过 DNS 名称访问(推荐)
kubeadm 部署的集群默认装有 CoreDNS:
kubectl get deployments.apps --namespace=kube-system
NAME READY UP-TO-DATE AVAILABLE AGE
calico-kube-controllers 1/1 1 1 2d23h
coredns 2/2 2 2 3d
CoreDNS 是一个 DNS 服务器,每当有新的 Service 创建,它就会自动添加对应的 DNS 记录。Pod 内可以通过名称访问 Service:
<SERVICE_NAME>.<NAMESPACE_NAME>
比如 WordPress 和 MySQL 在同一个 Namespace,直接写 mysql 就能访问:
kubectl run wordpress \
--image=hub.shaka.cn/library/wordpress \
--image-pull-policy=IfNotPresent \
--env=WORDPRESS_DB_USER=tom \
--env=WORDPRESS_DB_PASSWORD=redhat \
--env=WORDPRESS_DB_NAME=blog \
--env=WORDPRESS_DB_HOST=mysql
用 busybox 实测一下 DNS 解析效果:
kubectl run busybox --rm -it --image=hub.shaka.cn/library/busybox /bin/sh
/ # cat /etc/resolv.conf
nameserver 10.96.0.10
search service.svc.cluster.local svc.cluster.local cluster.local
/ # wget wordpress:80
Connecting to wordpress:80 (10.106.106.246:80)
index.html 100% |************************| 11607 0:00:00 ETA
10.96.0.10 就是 kube-system 里的 kube-dns Service:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kube-dns ClusterIP 10.96.0.10 <none> 53/UDP,53/TCP,9153/TCP 3d2h
完整的 DNS 名称其实是 wordpress.service.svc.cluster.local。因为 resolv.conf 里配置了 search 域,所以同 Namespace 下可以省略 Namespace 直接写 wordpress;跨 Namespace 时则要写成 svc名.命名空间名。
三种方式对比:
| 方式 | 易用性 | 抗变化能力 | 适用场景 |
|---|---|---|---|
| Service IP | 一般 | 换集群就要改 IP | 临时测试、调试 |
| 环境变量 | 较好 | 依赖创建顺序 | 老应用快速迁移 |
| DNS 名称 | 最好 | 完全透明 | 生产环境首选 |
五、小结
这一篇我们解决了两个问题:
- 为什么需要 Service:Pod 是临时的,IP 会漂移;hostPort 有端口冲突和主机依赖问题。Service 用稳定的 IP + 标签选择器,把“一组随时变化的 Pod”封装成了“一个固定不变的入口”。
- 怎么管理和发现 Service:创建、验证、动态发现,以及 IP / 环境变量 / DNS 三种发现方式。
Service 有稳定入口之后,下一个问题自然就是:外部用户怎么访问集群? 这就轮到 Service 的四种类型出场了——ClusterIP、NodePort、LoadBalancer、ExternalName,还有特殊的 Headless Service。我们下一篇文章见。
更多推荐



所有评论(0)