目录
问题
首先,我们思考这样一个问题:
访问k8s集群中的pod, 客户端需要知道pod地址,需要感知pod的状态。那如何获取各个pod的地址?若某一node上的pod故障,客户端如何感知?
什么是service
是发现后端pod服务;
是为一组具有相同功能的容器应用提供一个统一的入口地址;
是将请求进行负载分发到后端的各个容器应用上的控制器。
对service的访问来源
访问service的请求来源有两种:k8s集群内部的程序(Pod)和 k8s集群外部的程序。
创建service
创建一个servcie有两种方式
命令式
1 |
kubectl expose deployment goweb --name=gowebsvc --port=80 --target-port=8000 |
声明式
创建yaml文件 svc-goweb.yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 |
apiVersion: v1 kind: Service metadata: name: gowebsvc spec: selector: app: goweb ports: - name: default protocol: TCP port: 80 targetPort: 8000 type: ClusterIP |
创建服务
1 |
kubectl apply -f svc-goweb.yaml |
我们来看下配置文件中几个重点字段:
- selector指定了app: goweb标签。说明该svc代理所有包含有”app: goweb”的pod
- port字段指定了该svc暴露80端口
- targetPort指定改svc代理对应pod的8000端口
- type定义了svc的类型为ClusterIP,这也是svc的默认类型
通过apply创建服务后,来查看一下服务状态
1 2 3 |
$ 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中负载均衡。
service类型
采用微服务架构时,作为服务所有者,除了实现业务逻辑以外,还需要考虑如何把服务发布到k8s集群或者集群外部,使这些服务能够被k8s集群内的应用、其他k8s集群的应用以及外部应用使用。因此k8s提供了灵活的服务发布方式,用户可以通过ServiceType来指定如何来发布服务,类型有以下几种:
ClusterIP(集群内部使用)
默认方式,分配一个稳定的IP地址,即VIP,只能在集群内部访问。
1 2 3 4 5 6 7 8 9 10 11 12 |
apiVersion: v1 kind: Service metadata: name: service-python spec: ports: - port: 3000 protocol: TCP targetPort: 443 selector: run: pod-python type: ClusterIP |
类型为ClusterIP的service,这个service有一个Cluster-IP,其实就一个VIP。具体实现原理依靠kubeproxy组件,通过iptables或是ipvs实现。
这种类型的service 只能在集群内访问。
NodePort(对外暴露应用)
在每个节点启用一个端口来暴露服务,可以在集群外部访问,通过NodeIP:NodePort访问
端口范围:30000~32767
1 2 3 4 5 6 7 8 9 10 11 12 13 |
apiVersion: v1 kind: Service metadata: name: service-python spec: ports: - port: 3000 protocol: TCP targetPort: 443 nodePort: 30080 selector: run: pod-python type: NodePort |
此时我们可以通过http://4.4.4.1:30080或http://4.4.4.2:30080
对pod-python访问。该端口有一定的范围,比如默认Kubernetes 控制平面将在--service-node-port-range
标志指定的范围内分配端口(默认值:30000-32767)。
LoadBalancer(对外暴露应用,适用于公有云)
与NodePort类似,在每个节点启用一个端口来暴露服务。除此之外,K8s请求底层云平台的负载均衡器,把每个[Node IP]:[NodePort]作为后端添加进去。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
apiVersion: v1 kind: Service metadata: name: service-python spec: ports: - port: 3000 protocol: TCP targetPort: 443 nodePort: 30080 selector: run: pod-python type: LoadBalancer |
可以看到external-ip。我们就可以通过该ip来访问了。
当然各家公有云支持诸多的其他设置。大多是公有云负载均衡器的设置参数,都可以通过svc的注解来设置,例如下面的aws:
1 2 3 4 5 6 7 8 9 10 11 |
metadata: name: my-service annotations: service.beta.kubernetes.io/aws-load-balancer-access-log-enabled: "true" # Specifies whether access logs are enabled for the load balancer service.beta.kubernetes.io/aws-load-balancer-access-log-emit-interval: "60" # The interval for publishing the access logs. You can specify an interval of either 5 or 60 (minutes). service.beta.kubernetes.io/aws-load-balancer-access-log-s3-bucket-name: "my-bucket" # The name of the Amazon S3 bucket where the access logs are stored service.beta.kubernetes.io/aws-load-balancer-access-log-s3-bucket-prefix: "my-bucket-prefix/prod" # The logical hierarchy you created for your Amazon S3 bucket, for example `my-bucket-prefix/prod` |
ExternalName:
类型为 ExternalName 的service将服务映射到 DNS 名称,而不是典型的选择器,例如my-service
或者cassandra
。 您可以使用spec.externalName
参数指定这些服务。
创建 ExternalName 类型的服务的 yaml 如下:
1 2 3 4 5 6 7 8 9 10 11 |
kind: Service apiVersion: v1 metadata: name: service-python spec: ports: - port: 3000 protocol: TCP targetPort: 443 type: ExternalName externalName: remote.server.url.com |
说明:您需要 CoreDNS 1.7 或更高版本才能使用
ExternalName
类型。
当查找主机 service-python.default.svc.cluster.local
时,集群DNS服务返回CNAME记录,其值为my.database.example.com
。 访问service-python
的方式与其他服务的方式相同,但主要区别在于重定向发生在 DNS 级别,而不是通过代理或转发。
将生产工作负载迁移到Kubernetes集群并不容易。大多数我们不可以停止所有服务并在Kubernetes集群上启动它们。有时,尝试迁移轻量且不会破坏你服务的服务是很好的。在此过程中,一个可能不错的解决方案是使用现有的有状态服务(例如DB),并首先从无状态容器开始。
从Pod中访问外部服务的最简单正确的方法是创建ExternalName
service。例如,如果您决定保留AWS RDS,但您还希望能够将MySQL容器用于测试环境。让我们看一下这个例子:
1 2 3 4 5 6 7 8 |
kind: Service apiVersion: v1 metadata: name: test-service namespace: default spec: type: ExternalName externalName: test.database.example.com |
你已将Web应用程序配置为使用URL测试服务访问数据库,但是在生产集群上,数据库位于AWS RDS上,并且具有以下URL test.database.example.com
。创建ExternalName service 并且你的Web Pod尝试访问test-service上的数据库之后,Kubernetes DNS服务器将返回值为test.database.example.com
的CNAME记录。问题解决了。
ExternalName service 也可以用于从其他名称空间访问服务。例如:
1 2 3 4 5 6 7 8 9 10 |
kind: Service apiVersion: v1 metadata: name: test-service-1 namespace: namespace-a spec: type: ExternalName externalName: test-service-2.namespace-b.svc.cluster.local ports: - port: 80 |
在这里,我可以使用名称空间a中定义的test-service-1
访问命名空间b中的服务test-service-2
。
这个意义在哪里?
ExternalName service 也是一种service,那么ingress controller 会支持,那么就可以实现跨namespace的ingress。
几种特殊的service
除了上面这些通常的service配置,还有几种特殊情况:
Multi-Port Services
service可以配置不止一个端口,比如官方文档的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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
Headless services是指一个服务没有配置了clusterIP=None的服务。这种情况下,kube-proxy不会为这个服务做负载均衡的工作,而是交予DNS完成。具体又分为2种情况:
- 有配置selector: 这时候,endpoint控制器会为服务生成对应pod的endpoint对象。service对应的DNS返回的是endpoint对应后端的集合。
- 没有配置selector:这时候,endpoint控制器不会自动为服务生成对应pod的endpoint对象。若服务有配置了externalname,则生成一套cnmae记录,指向externalname。如果没有配置,就需要手动创建一个同名的endpoint对象。dns服务会创建一条A记录指向endpoint对应后端。
External IPs
如果有个非node本地的IP地址,可以通过比如外部负载均衡的vip等方式被路由到任意一台node节点,那就可以通过配置service的externalIPs字段,通过这个IP地址访问到服务。集群以这个IP为目的IP的请求时,会把请求转发到对应服务。参考官方文档的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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服务才能真正满足生产环境的需求。
service selector
service通过selector和pod建立关联。
k8s会根据service关联到pod的podIP信息组合成一个endpoint。
若service定义中没有selector字段,service被创建时,endpoint controller不会自动创建endpoint。
service负载分发策略
service 负载分发策略有两种:
RoundRobin
:轮询模式,即轮询将请求转发到后端的各个pod上(默认模式);
SessionAffinity
:基于客户端IP地址进行会话保持的模式,第一次客户端访问后端某个pod,之后的请求都转发到这个pod上。
k8s服务发现方式
虽然Service解决了Pod的服务发现问题,但不提前知道Service的IP,怎么发现service服务呢?
k8s提供了两种方式进行服务发现:
环境变量: 当创建一个Pod的时候,kubelet会在该Pod中注入集群内所有Service的相关环境变量。需要注意的是,要想一个Pod中注入某个Service的环境变量,则必须Service要先比该Pod创建。这一点,几乎使得这种方式进行服务发现不可用。
DNS:可以通过cluster add-on的方式轻松的创建KubeDNS来对集群内的Service进行服务发现————这也是k8s官方强烈推荐的方式。为了让Pod中的容器可以使用kube-dns
来解析域名,k8s会修改容器的/etc/resolv.conf
配置。
k8s服务发现原理
endpoint
endpoint是k8s集群中的一个资源对象,存储在etcd中,用来记录一个service对应的所有pod的访问地址。
service配置selector,endpoint controller才会自动创建对应的endpoint对象;否则,不会生成endpoint对象。
例如,k8s集群中创建一个名为k8s-classic-1113-d3的service,就会生成一个同名的endpoint对象,如下图所示。其中ENDPOINTS就是service关联的pod的ip地址和端口。
endpoint controller
endpoint controller是k8s集群控制器的其中一个组件,其功能如下:
- 负责生成和维护所有endpoint对象的控制器
- 负责监听service和对应pod的变化
- 监听到service被删除,则删除和该service同名的endpoint对象
- 监听到新的service被创建,则根据新建service信息获取相关pod列表,然后创建对应endpoint对象
- 监听到service被更新,则根据更新后的service信息获取相关pod列表,然后更新对应endpoint对象
- 监听到pod事件,则更新对应的service的endpoint对象,将podIp记录到endpoint中
Service代理模式
Service是由kube-proxy实现的
kube-proxy支持三种代理模式: 用户空间,iptables和IPVS;它们各自的操作略有不同。
Userspace
作为一个例子,考虑前面提到的图片处理应用程序。 当创建 backend Service
时,Kubernetes master 会给它指派一个虚拟 IP 地址,比如 10.0.0.1。 假设 Service
的端口是 1234,该 Service
会被集群中所有的 kube-proxy
实例观察到。 当代理看到一个新的 Service
, 它会打开一个新的端口,建立一个从该 VIP 重定向到新端口的 iptables,并开始接收请求连接。
当一个客户端连接到一个 VIP,iptables 规则开始起作用,它会重定向该数据包到 Service代理
的端口。 Service代理
选择一个 backend,并将客户端的流量代理到 backend 上。
这意味着 Service
的所有者能够选择任何他们想使用的端口,而不存在冲突的风险。 客户端可以简单地连接到一个 IP 和端口,而不需要知道实际访问了哪些 Pod
。
iptables
再次考虑前面提到的图片处理应用程序。 当创建 backend Service
时,Kubernetes 控制面板会给它指派一个虚拟 IP 地址,比如 10.0.0.1。 假设 Service
的端口是 1234,该 Service
会被集群中所有的 kube-proxy
实例观察到。 当代理看到一个新的 Service
, 它会配置一系列的 iptables 规则,从 VIP 重定向到 per-Service
规则。 该 per-Service
规则连接到 per-Endpoint
规则,该 per-Endpoint
规则会重定向(目标 NAT)到 backend。
当一个客户端连接到一个 VIP,iptables 规则开始起作用。一个 backend 会被选择(或者根据会话亲和性,或者随机),数据包被重定向到这个 backend。 不像 userspace 代理,数据包从来不拷贝到用户空间,kube-proxy 不是必须为该 VIP 工作而运行,并且客户端 IP 是不可更改的。 当流量打到 Node 的端口上,或通过负载均衡器,会执行相同的基本流程,但是在那些案例中客户端 IP 是可以更改的。
IPVS(推荐)
在大规模集群(例如10,000个服务)中,iptables 操作会显着降低速度。 IPVS 专为负载平衡而设计,并基于内核内哈希表。 因此,您可以通过基于 IPVS 的 kube-proxy 在大量服务中实现性能一致性。 同时,基于 IPVS 的 kube-proxy 具有更复杂的负载平衡算法(最小连接,局部性,加权,持久性)。
参考
详解k8s 4种类型Service
浅谈 kubernetes service 那些事
Kubernetes Service详解