编者按:云原生是网易杭州研究院(网易杭研)奉行的核心技术方向之一,开源容器平台 Kubernetes 作为云原生产业技术标准、云原生生态基石,在设计上不可避免有其复杂性,Kubernetes 系列文章基于网易杭研资深工程师总结,多角度多层次介绍 Kubernetes 的原理及运用,如何解决生产中的实际需求及规避风险,希望与读者深入交流共同进步。
本文由作者授权发布,未经许可,请勿转载。
作者:李岚清,网易杭州研究院云计算技术中心资深工程师
为什么引入 service
众所周知,pod 的生命周期是不稳定的,可能会朝生夕死,这也就意味着 pod 的 ip 是不固定的。
比如我们使用三副本的 deployment 部署了 nginx 服务,每个 pod 都会被分配一个 ip,由于 pod 的生命周期不稳定,pod 可能会被删除重建,而重建的话 pod 的 ip 地址就会改变。也有一种场景,我们可能会对 nginx deployment 进行扩缩容,从 3 副本扩容为 5 副本或者缩容为 2 副本。当我们需要访问上述的 nginx 服务时,客户端对于 nginx 服务的 ip 地址就很难配置和管理。
因此,kubernetes 社区就抽象出了 service
这个资源对象或者说逻辑概念。
什么是 service
service 是 kubernetes 中最核心的资源对象之一,kubernetes 中的每个 service 其实就是我们经常提到的“微服务”。
service 定义了一个服务的入口地址,它通过 label selector 关联后端的 pod。service 会被自动分配一个 ClusterIP,service 的生命周期是稳定的,它的 ClusterIP 也不会发生改变,用户通过访问 service 的 ClusterIP 来访问后端的 pod。所以,不管后端 pod 如何扩缩容、如何删除重建,客户端都不需要关心。
(1)创建一个三副本的 nginx deployment:
nginx.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: nginx
imagePullPolicy: Always
name: nginx
# kubectl create -f nginx.yaml
deployment.extensions/nginx created
# kubectl get pods -o wide
nginx-5c7588df-5dmmp 1/1 Running 0 57s 10.120.49.230 pubt2-k8s-for-iaas4.dg.163.org <none> <none>
nginx-5c7588df-gb2d8 1/1 Running 0 57s 10.120.49.152 pubt2-k8s-for-iaas4.dg.163.org <none> <none>
nginx-5c7588df-gdngk 1/1 Running 0 57s 10.120.49.23 pubt2-k8s-for-iaas4.dg.163.org <none> <none>
(2)创建 service,通过 label selector 关联 nginx pod:
svc.yaml
apiVersion: v1
kind: Service
metadata:
name: nginx
spec:
type: ClusterIP
selector:
app: nginx
ports:
- port: 80
protocol: TCP
targetPort: 80
# kubectl create -f svc.yaml
service/nginx created
# kubectl get svc nginx -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
nginx ClusterIP 10.178.4.2 <none> 80/TCP 23s app=nginx
(3)在 k8s 节点上访问 service 地址
# curl 10.178.4.2:80
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
实现原理
service 中有几个关键字段:
-
spec.selector
: 通过该字段关联属于该 service 的 pod -
spec.clusterIP
: k8s 自动分配的虚拟 ip 地址 -
spec.ports
: 定义了监听端口和目的端口。用户可以通过访问clusterip: 监听端口
来访问后端的 pod
当用户创建一个 service 时,kube-controller-manager 会自动创建一个跟 service 同名的 endpoints 资源:
# kubectl get endpoints nginx
NAME ENDPOINTS AGE
nginx 10.120.49.152:80,10.120.49.23:80,10.120.49.230:80 12m
endpoints 资源中,保存了该 service 关联的 pod 列表,这个列表是 kube-controller-manager 自动维护的,当发生 pod 的增删时,这个列表会被自动刷新。
比如,我们删除了其中的一个 pod:
# kubectl delete pods nginx-5c7588df-5dmmp
pod "nginx-5c7588df-5dmmp" deleted
# kubectl get pods
nginx-5c7588df-ctcml 1/1 Running 0 6s
nginx-5c7588df-gb2d8 1/1 Running 0 18m
nginx-5c7588df-gdngk 1/1 Running 0 18m
可以看到 kube-controller-manager 立马补充了一个新的 pod。然后我们再看一下 endpoints 资源,后端 pod 列表也被自动更新了:
# kubectl get endpoints nginx
NAME ENDPOINTS AGE
nginx 10.120.49.152:80,10.120.49.23:80,10.120.49.73:80 16m
那么,当用户去访问 clusterip:port
时,流量是如何负载均衡到后端 pod 的呢?
k8s 在每个 node 上运行了一个 kube-proxy
组件,kube-proxy
会 watch service 和 endpoints 资源,通过配置 iptables 规则(现在也支持 ipvs,不过不在本文章讨论范围之内)来实现 service 的负载均衡。
可以在任一个 k8s node 上看一下上述 nginx service 的 iptables 规则:
# iptables -t nat -L PREROUTING
Chain PREROUTING (policy ACCEPT)
target prot opt source destination
KUBE-SERVICES all -- anywhere anywhere /* kubernetes service portals */
# iptables -t nat -L KUBE-SERVICES
Chain KUBE-SERVICES (2 references)
target prot opt source destination
KUBE-SVC-4N57TFCL4MD7ZTDA tcp -- anywhere 10.178.4.2 /* default/nginx: cluster IP */ tcp dpt:http
# iptables -t nat -L KUBE-SVC-4N57TFCL4MD7ZTDA
Chain KUBE-SVC-4N57TFCL4MD7ZTDA (1 references)
target prot opt source destination
KUBE-SEP-AHN4ALGUQHWJZNII all -- anywhere anywhere statistic mode random probability 0.33332999982
KUBE-SEP-BDD6UBFFJ4G2PJDO all -- anywhere anywhere statistic mode random probability 0.50000000000
KUBE-SEP-UR2OSKI3P5GEGC2Q all -- anywhere anywhere
# iptables -t nat -L KUBE-SEP-AHN4ALGUQHWJZNII
Chain KUBE-SEP-AHN4ALGUQHWJZNII (1 references)
target prot opt source destination
KUBE-MARK-MASQ all -- 10.120.49.152 anywhere
DNAT tcp -- anywhere anywhere tcp to:10.120.49.152:80
当用户访问 clusterip:port
时,iptables 会通过 iptables DNAT 均衡的负载均衡到后端 pod。
service ClusterIP
service 的 ClusterIP 是一个虚拟 ip,它没有附着在任何的网络设备上,仅仅存在于 iptables 规则中,通过 dnat 实现访问 clusterIP 时的负载均衡。
当用户创建 service 时,k8s 会自动从 service 网段中分配一个空闲 ip 设置到 .spec.clusterIP
字段。当然,k8s 也支持用户在创建 svc 时自己指定 clusterIP。
service 的网段是通过 kube-apiserver 的命令行参数 --service-cluster-ip-range
配置的,不允许变更。
service 网段不能跟机房网络、docker 网段、容器网段冲突,否则可能会导致网络不通。
service 的 clusterIP 是 k8s 集群内的虚拟 ip,不同的 k8s 集群可以使用相同的 service 网段,在 k8s 集群外是访问不通 service 的 clusterIP 的。
service 域名
kubernetes 是有自己的域名解析服务的。比如我们可以通过访问域名 nginx.default.svc.cluster.local
来访问上述的 nginx 服务:
$ curl nginx.default.svc.cluster.local
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
域名格式为:${ServiceName}.${Namespace}.svc.${ClusterDomain}
. 其中 ${ClusterDomain}的默认值是 cluster.local
,可以通过 kubelet 的命令行参数----cluster-domain
进行配置。
headless service
当不需要 service ip 的时候,可以在创建 service 的时候指定spec.clusterIP: None
,这种 service 即是 headless service。由于没有分配 service ip,kube-proxy 也不会处理这种 service。
DNS 对这种 service 的解析:
- 当 service 里定义 selector 的时候:Endpoints controller 会创建相应的 endpoints。DNS 里的 A 记录会将 svc 地址解析为这些 pods 的地址
- 当 service 里没有定义 selector:Endpoints controller 不会创建 endpoints。DNS 会这样处理:
- 首先 CNAME 到 service 里定义的 ExternalName
- 没有定义 ExternalName 的话,会搜寻所有的和这个 service 共享 name 的 Endpoints,然后将 A 记录解析到这些 Endpoints 的地址
service 的不同类型
service 支持多种不同的类型,包括 ClusterIP
、NodePort
、LoadBalancer
,通过字段spec.type
进行配置。
ClusterIP service
默认类型。对于 ClusterIP service,k8s 会自动分配一个只在集群内可达的虚拟的 ClusterIP,在 k8s 集群外无法访问。
NodePort service
k8s 除了会给 NodePort service 自动分配一个 ClusterIP,还会自动分配一个 nodeport 端口。集群外的客户端可以访问任一 node 的 ip 加 nodeport,即可负载均衡到后端 pod。
nodeport 的端口范围可以通过 kube-apiserver 的命令行参数 --service-node-port-range
配置,默认值是30000-32767
,当前我们的配置是30000-34999
。
但是客户端访问哪个 node ip 也是需要考虑的一个问题,需要考虑高可用。而且 NodePort 会导致访问后端服务时多了一跳,并且可能会做 snat 看不到源 ip。
另外需要注意的是,service-node-port-range
不能够跟几个端口范围冲突:
- Linux 的
net.ipv4.ip_local_port_range
,可以配置为35000-60999
- ingress nginx 中的四层负载均衡,端口必须小于 30000
- 其他普通业务的端口也需要小于 30000
LoadBalancer service
LoadBalancer service 需要对接云服务提供商的 NLB 服务。当用户创建一个 LoadBalancer 类型的 sevice 时,cloud-controller-manager
会调用 NLB 的 API 自动创建 LB 实例,并且将 service 后端的 pod 挂到 LB 实例后端。
apiVersion: v1
kind: Service
metadata:
name: nginx
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
type: LoadBalancer
$ kubectl get svc nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx LoadBalancer 10.178.8.216 10.194.73.147 80:32514/TCP 3s
service 中会话保持
用户可以通过配置 spec.serviceAffinity=ClientIP
来实现基于客户端 ip 的会话保持功能。该字段默认为 None。
还可以通过适当设置 service.spec.sessionAffinityConfig.clientIP.timeoutSeconds
来设置最大会话停留时间。(默认值为 10800 秒,即 3 小时)
apiVersion: v1
kind: Service
metadata:
name: nginx
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
type: ClusterIP
sessionAffinity: ClientIP
kubernetes
service
当我们部署好一个 k8s 集群之后,发现系统自动帮忙在 default
namespace 下创建了一个 name 为kubernetes
的 service:
# kubectl get svc kubernetes -o yaml
apiVersion: v1
kind: Service
metadata:
labels:
component: apiserver
provider: kubernetes
name: kubernetes
namespace: default
spec:
clusterIP: 10.178.4.1
ports:
- name: https
port: 443
protocol: TCP
targetPort: 6443
sessionAffinity: None
type: ClusterIP
status:
loadBalancer: {}
可以看到 kubernetes
svc 的 ip 是--service-cluster-ip-range
的第一个 ip,并且该 service 没有设置spec.selector
。理论上来说,对于没有设置 selector 的 svc,kube-controller-manager 不会自动创建同名的 endpoints 资源出来。
但是我们看到是有同名的 endpoints 存在的,并且多个 apiserver 的地址也被保存在 endpoints 资源中:
# kubectl get ep kubernetes
NAME ENDPOINTS AGE
kubernetes 10.120.0.2:6443,10.120.0.3:6443 137d
具体是如何实现的,感兴趣的可以看下源码k8s.io/kubernetes/pkg/master/reconcilers
Frequently Asked Questions
问题一 为什么 service clusterip 无法 ping 通
因为 service clusterip 是一个 k8s 集群内部的虚拟 ip,没有附着在任何网络设备上,仅仅存在于 iptables nat 规则中,用来实现负载均衡。
问题二 为什么 service 的网段不能跟 docker 网段、容器网段、机房网段冲突
假如 service 网段跟上述网段冲突,很容易导致容器或者在 k8s node 上访问上述网段时发生网络不通的情况。
问题三 为什么在 k8s 集群外无法访问 service clusterip
service clusterip 是 k8s 集群内可达的虚拟 ip,集群外不可达。不同的 k8s 集群可以使用相同的 service 网段。
或者说,集群外的机器上没有本 k8s 集群的 kube-proxy 组件,没有创建对应的 iptables 规则,因此集群外访问不通 service clusterip。
问题四 能否扩容 service 网段
原则上这个网段是不允许更改的,但是假如因为前期规划的问题分配的网段过小,实际可以通过比较 hack 的运维手段扩容 service 网段。
问题五 service 是否支持七层的负载均衡
service 仅支持四层的负载均衡,七层的负载均衡需要使用 ingress
参考文档
- https://kubernetes.io/docs/concepts/services-networking/service/
作者简介
李岚清,网易杭州研究院云计算技术中心容器编排团队资深系统开发工程师,具有多年 Kubernetes 开发、运维经验,主导实现了容器网络管理、容器混部等生产级核心系统研发,推动网易集团内部电商、音乐、传媒、教育等多个业务的容器化。