文章目录
前言
在云原生时代,微服务无疑是应用的主流形态。为了更好地支持微服务以及服务网格这样的应用架构,Kubernetes 又专门定义了一个新的对象:Service
,它是集群内部的负载均衡机制
,用来解决服务发现
的关键问题。在 Kubernetes Service 文档中,Service
被定义为将运行在一组 Pods 上的应用程序公开为网络服务的抽象方法。
提示:以下是本篇文章正文内容,下面案例可供参考
一、为什么要有 Service
有了 Deployment 和 DaemonSet,我们在集群里发布应用程序的工作轻松了很多。借助 Kubernetes 强大的自动化运维能力,我们可以把应用的更新上线频率由以前的月、周级别提升到天、小时级别,让服务质量更上一层楼。
在 Kubernetes 集群里 Pod 的生命周期是比较短暂
的,虽然 Deployment 和 DaemonSet 可以维持 Pod 总体数量的稳定,但在运行过程中,难免会有 Pod 销毁又重建,这就会导致 Pod 集合处于动态的变化之中。
这导致了一个问题: 如果一组 Pod(称为后端
)为集群内的其他 Pod(称为前端
)提供功能, 那么前端如何找出并跟踪要连接的 IP 地址,以便前端可以使用提供工作负载的后端部分?
业内早就有解决方案来针对这样不稳定
的后端服务,那就是负载均衡
,典型的应用有LVS、Nginx
等等。它们在前端与后端之间加入了一个中间层
,屏蔽后端的变化,为前端提供一个稳定的服务。
因此 Kubernetes 定义了一个新的 API 对象:Service
。
二、Service 的工作原理
Service
的工作原理和 LVS、Nginx 差不多,Kubernetes 会给它分配一个静态 IP 地址
,然后它再去自动管理、维护
后面动态变化的 Pod 集合
,当客户端访问 Service
,它就根据某种策略,把流量转发给后面的某个 Pod
。
这张图展示的是 Service 的 iptables
代理模式。每个节点上的kube-proxy
组件自动维护 iptables
规则,客户不再关心 Pod 的具体地址,只要访问 Service 的固定 IP 地址
,Service
就会根据 iptables
规则转发请求给它管理的多个 Pod,这是典型的负载均衡架构
。此外,使用 iptables
处理流量具有较低的系统开销,因为流量由 Linux netfilter
处理, 而无需在用户空间和内核空间之间切换。 这种方法也可能更可靠。
Service
并不是只能使用 iptables
来实现负载均衡,它还有另外两种实现技术:性能更差的 userspace
和性能更好的 ipvs
,但这些都属于底层细节,我们不需要刻意关注。
三、使用 YAML 描述 Service
我们还是可以用命令 kubectl api-resources
查看它的基本信息,可以知道它的简称是 svc
,apiVersion
是 v1
。注意,这说明它与 Pod 一样,属于 Kubernetes 的核心对象,不关联业务应用,与 Job、Deployment 是不同的。
这里 Kubernetes 又表现出了行为上的不一致。虽然它可以自动创建 YAML 样板,但不是用命令 kubectl create,而是另外一个命令 kubectl expose
,也许 Kubernetes 认为 expose
能够更好地表达 Service 暴露
服务地址的意思吧。
使用 kubectl expose
指令时还需要用参数 --port
和 --target-port
分别指定映射端口和容器端口,而 Service
自己的 IP 地址和后端 Pod 的 IP 地址可以自动生成,用法上和 Docker 的命令行参数 -p
很类似,只是略微麻烦一点。
例如,我们对 Deployment 笔记中的 ngx-dep 对象生成 Service ,命令就应该这样写:
$ export out="--dry-run=client -o yaml"
$ kubectl expose deploy ngx-dep --port=80 --target-port=80 $out
下面是剔除一些字段后的 YAML:
apiVersion: v1
kind: Service
metadata:
name: ngx-svc
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: ngx-dep
可以发现,Service
的定义非常简单,spec
中只有两个关键字 selector
和 ports
。
selector
和 Deployment/DaemonSet 里的作用是一样的,用来过滤出要代理的那些 Pod。因为我们指定要代理Deployment
,所以Kubernetes
就为我们自动填上了ngx-dep
的标签,会选择这个Deployment
对象部署的所有 Pod。ports
就很好理解了,里面的三个字段分别表示外部端口、内部端口和使用的协议,在这里就是内外部都使用 80 端口,协议是 TCP
四、在 Kubernetes 中使用 Service
在 YAML 创建 Service 对象之前,我们要先对 Deployment 笔记中的 ngx-dep
做一点改造:方便观察 Service 的效果。
首先,创建一个 ConfigMap
,定义一个 Nginx 的配置片段,它会响应服务器地址、主机名、请求的 URI
等信息:
# ngx-conf.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: ngx-conf
data:
default.conf: |
server {
listen 80;
location / {
default_type text/plain;
return 200
'srv : $server_addr:$server_port\nhost: $hostname\nuri : $request_method $host $request_uri\ndate: $time_iso8601\n';
}
}
$ kubectl apply -f ngx-conf.yaml
然后在 Deployment
中的 template.volumes
里定义存储卷,再用 volumeMounts
将配置文件加载到容器中:
# ngx-dep.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: ngx-dep
spec:
replicas: 3
selector:
matchLabels:
app: ngx-dep
template:
metadata:
labels:
app: ngx-dep
spec:
volumes:
- name: ngx-conf-vol
configMap:
name: ngx-conf
containers:
- image: nginx:alpine
name: nginx
ports:
- containerPort: 80
volumeMounts:
- mountPath: /etc/nginx/conf.d
name: ngx-conf-vol
$ kubectl apply -f ngx-conf.yaml
部署这个 Deployment 之后,我们就可以创建 Service 对象了:
# ngx-svc.yaml
apiVersion: v1
kind: Service
metadata:
labels:
app: ngx-dep
name: ngx-dep
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: ngx-dep
$ kubectl apply -f ngx-svc.yaml
创建之后,我们就可以查看该 Service 对象的状态:
$ kubelet get svc -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 2d <none>
ngx-svc ClusterIP 10.96.21.182 <none> 80/TCP 10s app=ngx-dep
Kubernetes 为 Service 对象自动分配了一个 IP 地址 10.96.21.182
,这个地址段是独立于 Pod 地址段的(10.10.xx.xx)。而且 Service
对象的 IP 地址还有一个特点,它是一个虚地址
,不存在实体,只能用来转发流量。
想要看 Service
代理了哪些后端的 Pod,可以使用 describe
子命令:
$ kubectl describe svc ngx-svc
Name: ngx-svc
Namespace: default
Labels: <none>
Annotations: <none>
Selector: app=ngx-dep
Type: ClusterIP
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.96.21.182
IPs: 10.96.21.182
Port: <unset> 80/TCP
TargetPort: 80/TCP
Endpoints: 10.10.1.34:80,10.10.1.35:80,10.10.1.36:80
Session Affinity: None
Events: <none>
显示 Service 对象管理了 3 个 endpoint:10.10.1.34:80, 10.10.1.35:80, 10.10.1.36:80
。
下面查看一下 Pod
详情:
$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
ngx-dep-545884c69c-9q6gd 1/1 Running 0 39m 10.10.1.34 k8s-worker01 <none> <none>
ngx-dep-545884c69c-rbfpn 1/1 Running 0 39m 10.10.1.35 k8s-worker01 <none> <none>
ngx-dep-545884c69c-vzdf2 1/1 Running 0 39m 10.10.1.36 k8s-worker01 <none> <none>
redis-ds-8zmdb 1/1 Running 1 (140m ago) 23h 10.10.0.39 k8s-master01 <none> <none>
redis-ds-tp9sd 1/1 Running 1 (140m ago) 23h 10.10.1.30 k8s-worker01 <none> <none
接下来测试负载均衡的效果,因为 Service、 Pod 的 IP 地址都是 Kubernetes 集群的内部网段,所以我们需要用kubectl exec
进入到 Pod 内部(或者 ssh 登录集群节点),再用 curl
等工具来访问 Service:
$ kubectl exec -it ngx-dep-545884c69c-9q6gd -- sh
/ # curl 10.96.21.182
srv : 10.10.1.34:80
host: ngx-dep-545884c69c-9q6gd
uri : GET 10.96.21.182 /
date: 2023-03-20T03:04:55+00:00
/ # curl 10.96.21.182
srv : 10.10.1.36:80
host: ngx-dep-545884c69c-vzdf2
uri : GET 10.96.21.182 /
date: 2023-03-20T03:04:59+00:00
/ # curl 10.96.21.182
srv : 10.10.1.36:80
host: ngx-dep-545884c69c-vzdf2
uri : GET 10.96.21.182 /
date: 2023-03-20T03:05:02+00:00
在 Pod
里,用 curl
访问 Service 的 IP
地址,就会看到它把数据转发给后端的 Pod,输出信息会显示具体是哪个 Pod 响应了请求,就表明 Service
确实完成了对 Pod 的负载均衡任务。
再试着删除一个 Pod,看看 Service 是否会更新后端 Pod 的信息,实现自动化的服务发现:
$ kubectl delete pod ngx-dep-545884c69c-9q6gd
pod "ngx-dep-545884c69c-9q6gd" deleted
$ kubectl describe svc ngx-svc
Name: ngx-svc
Namespace: default
Labels: <none>
Annotations: <none>
Selector: app=ngx-dep
Type: ClusterIP
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.96.21.182
IPs: 10.96.21.182
Port: <unset> 80/TCP
TargetPort: 80/TCP
Endpoints: 10.10.1.35:80,10.10.1.36:80,10.10.1.37:80
Session Affinity: None
Events: <none>
可以看到一个 IP 地址为 10.10.1.37
的 Pod 被添加进来了。
由于 Pod 被 Deployment 对象管理,删除后会自动重建,而 Service 又会通过controller-manager
实时监控 Pod
的变化情况,所以就会立即更新它代理的 IP 地址。
五、以域名方式使用 Service
Service 还有一些高级特性值得了解。
首先是 DNS
域名。Service 对象的 IP 地址是静态的,保持稳定,这在微服务里确实很重要,不过数字形式的 IP 地址用起来还是不太方便。这个时候 Kubernetes
的 DNS 插件
就派上了用处,它可以为 Service
创建易写易记的域名
,让 Service
更容易使用。
使用 DNS 域名之前,我们要先了解一个新的概念:名字空间 namespace
,它被用来在集群里实现对 API 对象的隔离和分组。
namespace
的简写是 ns
,使用命令 kubectl get ns
来查看当前集群里都有哪些名字空间,也就是说 API 对象有哪些分组:
$ kubectl get ns
NAME STATUS AGE
default Active 2d
kube-flannel Active 2d
kube-node-lease Active 2d
kube-public Active 2d
kube-system Active 2d
Kubernetes 有一个默认的名字空间 default
,如果不显式指定
,API 对象都会在这个 default
名字空间里。而其他的名字空间都有各自的用途,比如 kube-system
就包含了 apiserver、etcd
等核心组件的 Pod
。
因为 DNS
是一种层次结构,为了避免太多的域名导致冲突,Kubernetes
就把名字空间
作为域名
的一部分,减少了重名的可能性。
Service 对象的域名完全形式是:
Object.namespace.svc.cluster.local
但很多时候也可以省略后面的部分,直接写object.namesapce
甚至 object 就足够了,默认会使用对象
所在的名字空间(比如这里就是 default)。
现在我们来试验一下 DNS 域名的用法,还是先 kubectl exec
进入 Pod
,然后用 curl
访问 ngx-svc
、ngx-svc.default
等域名:
$ kubectl exec -it ngx-dep-545884c69c-rbfpn -- sh
/ # curl ngx-svc
srv : 10.10.1.37:80
host: ngx-dep-545884c69c-mmrnm
uri : GET ngx-svc /
date: 2023-03-20T04:05:02+00:00
/ # curl ngx-svc.default
srv : 10.10.1.35:80
host: ngx-dep-545884c69c-rbfpn
uri : GET ngx-svc.default /
date: 2023-03-20T04:05:02+00:00
可以看到,现在我们就不再关心 Service
对象的 IP 地址,只需要知道它的名字,就可以用 DNS
的方式去访问后端服务。
顺便说一下,Kubernetes 也为每个 Pod
分配了域名,形式是 IP Address.namespace.pod.cluster.local
,但需要把 IP 地址里的 . 改成 - 。比如地址 10.10.1.87
,它对应的域名就是 10-10-1-87.default.pod
。
这里是 Kubernetes 文档 - Service 与 Pod 的 DNS。
六、让 Service 对外暴露服务
由于 Service
是一种负载均衡技术
,所以它不仅能够管理 Kubernetes
集群内部的服务,还能够担当向集群外部暴露
服务的重任。
Service
对象有一个关键字段 type
,表示 Service
是哪种类型的负载均衡。
ClusterIP
- 对集群内部 Pod 的负载均衡,Service 的静态 IP 地址只能在集群内访问ExternalName
- 通过返回 CNAME 和对应值,可以将服务映射到 externalName 字段的内容(例如 foo.bar.example.com)。 无需创建任何类型代理查看文档LoadBalancer
- 一般依赖云服务提供商 查看文档NodePort
- 通过每个节点上的 IP 和静态端口(NodePort)暴露服务
在实验环境里我们使用NodePort
:
如果在使用命令kubectl expose
的时候加上参数 --type=NodePort
,或者在 YAML 里添加字段 type:NodePort
,那么 Service
除了会对后端的 Pod 做负载均衡之外,还会在集群里的每个节点上创建一个独立的端口
,用这个端口对外提供服务,这也正是 NodePort
这个名字的由来。
下面我们给 Service 的 YAML 文件加上 type
字段:
apiVersion: v1
kind: Service
metadata:
name: ngx-svc
spec:
type: NodePort
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: ngx-dep
$ kubectl apply -f ngx-svc.yaml
service/ngx-svc configured
$ kubectl get svc -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 2d5h <none>
ngx-svc NodePort 10.96.21.182 <none> 80:30952/TCP 5h25m app=ngx-dep
就会看到 TYPE
变成了 NodePort
,而在 PORT
列里的端口信息也不一样,除了集群内部使用的 80
端口,还多出了一个 30952
端口,这就是 Kubernetes 在节点上为 Service
创建的专用映射端口。
因为这个端口号属于节点,外部能够直接访问,所以现在我们就可以不用登录集群节点或者进入 Pod 内部,直接在集群外使用任意一个节点的 IP 地址,就能够访问 Service
和它代理的后端服务了。
比如我使用宿主机 IP: 10.0.0.12 访问集群内的 Worker1 节点 IP: 10.0.0.12:30952 就可以得到 Nginx Pod 的响应数据:
$ curl 10.0.0.12:30952
srv : 10.10.1.36:80
host: ngx-dep-545884c69c-vzdf2
uri : GET 10.0.0.12 /
date: 2023-03-20T08:20:21+00:00
NodePort
类型的 Service
有下面几个缺点:
- 端口数量很有限。Kubernetes 为了避免端口冲突,默认只在“30000~32767”这个范围内随机分配,只有 2000 多个,而且都不是标准端口号,这对于具有大量业务应用的系统来说根本不够用
- 它会在每个节点上都开端口,然后使用 kube-proxy 路由到真正的后端 Service,这对于有很多计算节点的大集群来说就带来了一些网络通信成本,不是特别经济
- 它要求向外界暴露节点的 IP 地址,这在很多时候是不可行的,为了安全还需要在集群外再搭一个反向代理,增加了方案的复杂度
总结
- Pod 的生命周期很短暂,会不停地创建销毁,所以就需要用 Service 来实现负载均衡,它由 Kubernetes 分配固定的 IP 地址,能够屏蔽后端的 Pod 变化。
- Service 对象使用与 Deployment、DaemonSet 相同的“selector”字段,选择要代理的后端 Pod,是松耦合关系。
- 基于 DNS 插件,我们能够以域名的方式访问 Service,比静态 IP 地址更方便。
- 名字空间是 Kubernetes 用来隔离对象的一种方式,实现了逻辑上的对象分组,Service 的域名里就包含了名字空间限定。
- Service 的默认类型是“ClusterIP”,只能在集群内部访问,如果改成“NodePort”,就会在节点上开启一个随机端口号,让外界也能够访问内部的服务。