Kubernetes能够方便的为容器应用提供了一个持续运行且方便扩展的环境,可是,应用最终是要被用户或其余应用访问、调用的。要访问应用pod,就会有如下两个问题:node
service主要就是用来解决这两个问题的。简单来讲,它是一个抽象的api对象,用来表示一组提供相同服务的pod及对这组pod的访问方式。web
service做为一个相似中介的角色,对内,它要能代理访问到不断变换的一组后端Pod;对外,它要能暴露本身给集群内部或外部的其余资源访问。咱们分别来看下具体是怎么实现的。segmentfault
以前的文章kubeadm部署最后的测试部分,建立了一组pod及服务来验证业务,继续以这个例子来讲明:后端
集群中已经有以下一组pod:api
NAME READY STATUS IP NODE APP goweb-55c487ccd7-5t2l2 1/1 Running 10.244.1.15 node-1 goweb goweb-55c487ccd7-cp6l8 1/1 Running 10.244.3.9 node-2 goweb goweb-55c487ccd7-gcs5x 1/1 Running 10.244.1.17 node-1 goweb goweb-55c487ccd7-pp6t6 1/1 Running 10.244.3.10 node-2 goweb
pod都带有app:goweb标签,对外暴露8000端口,访问/info路径会返回主机名。架构
建立一个servcie有两种方式app
$ kubectl expose deployment goweb --name=gowebsvc --port=80 --target-port=8000
# 定义服务配置文件 # svc-goweb.yaml apiVersion: v1 kind: Service metadata: name: gowebsvc spec: selector: app: goweb ports: - name: default protocol: TCP port: 80 targetPort: 8000 type: ClusterIP # 建立服务 $ kubectl apply -f svc-goweb.yaml
咱们来看下配置文件中几个重点字段:负载均衡
经过apply建立服务后,来查看一下服务状态less
$ kubectl get svc gowebsvc -o wide NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR gowebsvc ClusterIP 10.106.202.0 <none> 80/TCP 3d app=goweb
能够看到,Kubernetes自动为服务分配了一个CLUSTER-IP。经过这个访问这个IP的80端口,就能够访问到"app: goweb"这组pod的8000端口,而且能够在这组pod中负载均衡。dom
[root@master-1 ~]# curl http://10.106.202.0/info Hostname: goweb-55c487ccd7-gcs5x [root@master-1 ~]# curl http://10.106.202.0/info Hostname: goweb-55c487ccd7-cp6l8 [root@master-1 ~]# curl http://10.106.202.0/info Hostname: goweb-55c487ccd7-pp6t6
cluster-ip是一个虚拟的ip地址,并非某张网卡的真实地址。那具体的请求代理转发过程是怎么实现的呢? 答案是iptables。咱们来看下iptables中与cluster-ip相关的规则
[root@master-1 ~]# iptables-save | grep 10.106.202.0 -A KUBE-SERVICES ! -s 10.244.0.0/16 -d 10.106.202.0/32 -p tcp -m comment --comment "default/gowebsvc:default cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ -A KUBE-SERVICES -d 10.106.202.0/32 -p tcp -m comment --comment "default/gowebsvc:default cluster IP" -m tcp --dport 80 -j KUBE-SVC-SEG6BTF25PWEPDFT
能够看到,目的地址为CLUSTER-IP、目的端口为80的数据包,会被转发到KUBE-MARK-MASQ与KUBE-SVC-SEG6BTF25PWEPDFT链上。其中,KUBE-MARK-MASQ链的做用是给数据包打上特定的标记(待验证),重点来看下KUBE-SVC-SEG6BTF25PWEPDFT链:
-A KUBE-SVC-SEG6BTF25PWEPDFT -m statistic --mode random --probability 0.25000000000 -j KUBE-SEP-5ZXTVLEM4DKNW7T2 -A KUBE-SVC-SEG6BTF25PWEPDFT -m statistic --mode random --probability 0.33332999982 -j KUBE-SEP-EBFXI7VOCPDT2QU5 -A KUBE-SVC-SEG6BTF25PWEPDFT -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-C3PKSXKMO2M43SPF -A KUBE-SVC-SEG6BTF25PWEPDFT -j KUBE-SEP-2GQCCNJGO65Z5MFS
能够看到,KUBE-SVC-SEG6BTF25PWEPDFT链经过设置--probability,将请求等几率转发到4条链上,查看其中一条转发链:
[root@master-1 ~]# iptables-save | grep "A KUBE-SEP-5ZXTVLEM4DKNW7T2" -A KUBE-SEP-5ZXTVLEM4DKNW7T2 -s 10.244.1.15/32 -j KUBE-MARK-MASQ -A KUBE-SEP-5ZXTVLEM4DKNW7T2 -p tcp -m tcp -j DNAT --to-destination 10.244.1.15:8000
发现KUBE-SEP-5ZXTVLEM4DKNW7T2这条规则对请求的目的地址做了DNAT到10.244.1.15:8000,这正是goweb组中goweb-55c487ccd7-5t2l2这个pod的ip地址。这样,对svc的CLUSTER-IP的请求,就会经过iptables规则转发到相应的pod。
可是,还有个问题,svc是怎么跟踪pod的ip变化的?
注意到前面的nat规则,第一次转发的链名称是KUBE-SVC-xxx,第二次转发给具体pod的链名称是KUBE-SEP-xxx,这里的SEP实际指的是kubernetes另外一个对象endpoint,咱们能够经过vkubectl get ep命令来查看:
[root@master-1 ~]# kubectl get ep gowebsvc NAME ENDPOINTS gowebsvc 10.244.1.15:8000,10.244.1.17:8000,10.244.3.10:8000 + 1 more... 35d
在svc建立的时候,kube-proxy组件会自动建立同名的endpoint对象,动态地跟踪匹配selector的一组pod当前ip及端口,并生成相应的iptables KUBE-SVC-xxx规则。
上面说的请求代理转发的方式,是kubernetes目前版本的默认方式,实际上,service的代理方式一共有三种:
在这种模式下,kube-proxy为每一个服务都打开一个随机的端口,全部访问这个端口的请求都会被转发到服务对应endpoints指定的后端。最后,kube-proxy还会生成一条iptables规则,把访问cluster-ip的请求重定向到上面说的随机端口,最终转发到后端pod。整个过程以下图所示:
Userspace模式的代理转发主要依靠kube-proxy实现,工做在用户态。因此,转发效率不高。较为不推荐用该种模式。
iptables模式是目前版本的默认服务代理转发模式,上两小节作过详细说明的就是这种模式,来看下请求转发的示意图:
与userspace模式最大的不一样点在于,kube-proxy只动态地维护iptables,而转发彻底靠iptables实现。因为iptables工做在内核态,不用在用户态与内核态切换,因此相比userspace模式更高效也更可靠。可是每一个服务都会生成若干条iptables规则,大型集群iptables规则数会很是多,形成性能降低也不易排查问题。
在v1.9版本之后,服务新增了ipvs转发方式。kube-proxy一样只动态跟踪后端endpoints的状况,而后调用netlink接口来生成ipvs规则。经过ipvs来转发请求:
ipvs一样工做在内核态,并且底层转发是依靠hash表实现,因此性能比iptables还要好的多,同步新规则也比iptables快。同时,负载均衡的方式除了简单rr还有多种选择,因此很适合在大型集群使用。而缺点就是带来了额外的配置维护操做。
在集群内部对一个服务的访问,主要有2种方式,环境变量与DNS。
当一个pod建立时,集群中属于同个namespace下的全部service对象信息都会被做为环境变量添加到pod中。随便找一个pod查看一下:
$ kubectl exec goweb-55c487ccd7-5t2l2 'env' | grep GOWEBSVC GOWEBSVC_PORT_80_TCP_ADDR=10.106.202.0 GOWEBSVC_SERVICE_PORT=80 GOWEBSVC_SERVICE_PORT_DEFAULT=80 GOWEBSVC_PORT_80_TCP=tcp://10.106.202.0:80 GOWEBSVC_PORT_80_TCP_PROTO=tcp GOWEBSVC_PORT_80_TCP_PORT=80 GOWEBSVC_PORT=tcp://10.106.202.0:80 GOWEBSVC_SERVICE_HOST=10.106.202.0
能够看到,pod经过{SVCNAME}_SERVICE_HOST/PORT就能够方便的访问到某个服务。这种访问方式简单易用,能够用来快速测试服务。但最大的问题就是,服务必须先于pod建立,后建立的服务是不会添加到现有pod的环境变量中的。
DNS组件是k8s集群的可选组件,它会不停监控k8s API,在有新服务建立时,自动建立相应的DNS记录。。以gowebsvc为例,在服务建立时,会建立一条gowebsvc.default.svc.cluster.local的dns记录指向服务。并且dns记录做用域是整个集群,不局限在namespace。
虽然是可选组件,但DNS生产环境能够说是必备的组件了。这里先简单说明,后面打算专门开篇文章来详细介绍。
服务发现解决了集群内部访问pod问题,但不少时候,pod提供的服务也是要对集群外部来暴露访问的,最典型的就是web服务。k8s中的service有多种对外暴露的方式,能够在部署Service时经过ServiceType字段来指定。默认状况下,ServiceType配置是只能内部访问的ClusterIP方式,前面的例子都是这种模式,除此以外,还能够配置成下面三种方式:
该方式把服务暴露在每一个Node主机IP的特定端口上,同一个服务在全部Node上端口是相同的,并自动生成相应的路由转发到ClusterIP。这样,集群外部经过<NodeIP>:<NodePort>就能够访问到对应的服务。举个例子:
## 建立svc,经过Nodeport方式暴露服务 $ kubectl expose deployment goweb --name=gowebsvc-nodeport --port=80 --target-port=8000 --type=NodePort ## 查看svc,能够看到NodePort随机分配的端口为32538 $ kubectl get svc gowebsvc-nodeport NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE gowebsvc-nodeport NodePort 10.101.166.252 <none> 80:32538/TCP 86s ## 随便访问一个nodeip的32538端口,均可以访问到gowebsvc-nodeport服务对应的pod $ curl 172.16.201.108:32538/info Hostname: goweb-55c487ccd7-pp6t6 $ curl 172.16.201.109:32538/info Hostname: goweb-55c487ccd7-5t2l2
LoadBalance方式主要是给公有云服务使用的,经过配置LoadBalance,能够触发公有云建立负载均衡器,并把node节点做为负载的后端节点。每一个公有云的配置方式不一样,具体能够参考各公有云的相关文档。
当ServiceType被配置为这种方式时,该服务的目的就不是为了外部访问了,而是为了方便集群内部访问外部资源。举个例子,假如目前集群的pod要访问一组DB资源,而DB是部署在集群外部的物理机,尚未容器化,能够配置这么一个服务:
apiVersion: v1 kind: Service metadata: name: dbserver namespace: default spec: type: ExternalName externalName: database.abc.com
这样,集群内部的pod经过dbserver.default.svc.cluster.local这个域名访问这个服务时,请求会被cname到database.abc.com来。事后,假如db容器化了,不须要修改业务代码,直接修改service,加上相应selector就能够了。
除了上面这些一般的service配置,还有几种特殊状况:
service能够配置不止一个端口,好比官方文档的例子:
apiVersion: v1 kind: Service metadata: name: my-service spec: selector: app: MyApp ports: - name: http protocol: TCP port: 80 targetPort: 9376 - name: https protocol: TCP port: 443 targetPort: 9377
这个service保留了80与443端口,分别对应pod的9376与9377端口。这里须要注意的是,pod的每一个端口必定指定name字段(默认是default)。
Headless services是指一个服务没有配置了clusterIP=None的服务。这种状况下,kube-proxy不会为这个服务作负载均衡的工做,而是交予DNS完成。具体又分为2种状况:
若是有个非node本地的IP地址,能够经过好比外部负载均衡的vip等方式被路由到任意一台node节点,那就能够经过配置service的externalIPs字段,经过这个IP地址访问到服务。集群以这个IP为目的IP的请求时,会把请求转发到对应服务。参考官方文档的例子:
apiVersion: v1 kind: Service metadata: name: my-service spec: selector: app: MyApp ports: - name: http protocol: TCP port: 80 targetPort: 9376 externalIPs: - 80.11.12.10
这里的80.11.12.10就是一个不禁kubernetes维护的公网IP地址,经过80.11.12.10:80就能够访问到服务对应的pod。
简单总结下,service对象实际上解决的就是一个分布式系统的服务发现问题,把相同功能的pod作成一个服务集也能很好的对应微服务的架构。在目前的kubernetes版本中,service还只能实现4层的代理转发,而且要搭配好DNS服务才能真正知足生产环境的需求。