Service 是kubernetes中一个很重要的,也是很有用的概念,我们可以通过service来将pod进行分组,并提供外网的访问endpoint。在这个过程中还有比如kube-proxy
提供了对service的访问。
Connecting Users to Pods
如果我们要让一个用户能够使用应用程序,用户需要能访问到pod,但是pod是一个短暂存在的东西,很可能突然挂了然后重启,这时候ip地址就会改变,所以pod的ip地址并不是静态的。比如说:
用户在这张图里面通过ip地址访问到了4个pod,突然其中有一个pod挂了,然后controller又起了一个pod:
这时候用户就访问不到了,因为用户不知道新的ip地址是多少。
kubernetes为了解决这个问题,提供了一个高层的抽象,叫做Service。Service从逻辑上把pod进行分组,并且设置访问的策略。一般我们是通过label和selector来达到分组的目的的。
Services
比如,我们用app作为key,db和frontend作为value来区分pod:
通过selector(app=frontend和app=db),我们就可以把这些pod分为两个逻辑组了。
这个时候,我们再给这两个逻辑组加上一个名称,比如frontend-svc
和db-svc
,就是service了:
Service对象模型
一个service对象模型大致如下:
kind: Service
apiVersion: v1
metadata:
name: frontend-svc
spec:
selector:
app: frontend
ports:
- protocol: TCP
port: 80
targetPort: 5000
在这个对象模型中,我们创建了一个叫做frontend-svc
的Service,这个service选择了所有的app=frontend
的pod。在默认情况下,每个service都会有一个cluster内部可以访问到的ip地址,也被称为ClusterIP
:
用户现在可以通过service的ip地址来访问到pod了,service会负责做负载均衡。
当转发请求的时候,我们可以选择pod上的目标端口,比如在我们的例子里面,frontend-svc通过80端口来接受用户的请求,然后转发到pod的5000端口。如果目标端口没有被显式声明,那么会默认转发到service接受请求的端口(和service端口一样)。
一个pod、ip地址和目标端口的元组代表了一个service的endpoint,比如在这个例子里面,frontend-svc有3个endpoints,分别是10.0.1.3:5000
, 10.0.1.4:5000
和10.0.1.5:5000
。
kube-proxy
所有的worker node都有一个后台任务,叫做kube-proxy
。这个kube-proxy会检测API Server上对于service和endpoint的新增或者移除。对于每个新的service,在每个node上,kube-proxy都会设置相应的iptables的规则来记录应该转发的地址。当一个service被删除的时候,kube-proxy会在所有的pod上移除这些iptables的规则。
服务发现
我们已经知道,Service是和kubernetes进行沟通的主要方式,那么我们就需要有一个办法来在运行的时候能够对已有的服务进行发现。Kubernetes提供了两种方法:
环境变量
每个pod在worker node上启动的时候,kubelet都会通过环境变量把所有目前可用的service的信息传进去。举个例子,我们有一个叫做redis-master
的service,这个service expose了6379的端口,并且ClusterIP是172.17.0.6,那么在一个新创建的pod上,我们可以看到以下环境变量:
REDIS_MASTER_SERVICE_HOST=172.17.0.6
REDIS_MASTER_SERVICE_PORT=6379
REDIS_MASTER_PORT=tcp://172.17.0.6:6379
REDIS_MASTER_PORT_6379_TCP=tcp://172.17.0.6:6379
REDIS_MASTER_PORT_6379_TCP_PROTO=tcp
REDIS_MASTER_PORT_6379_TCP_PORT=6379
REDIS_MASTER_PORT_6379_TCP_ADDR=172.17.0.6
如果使用这个解决方案,我们必须非常小心启动服务的顺序,因为pod不会获得自己启动之后的service的env。
DNS
kubernetes有一些dns的addon,这些addon会自动为所有service创建一个类似my-svc.my-namespace.svc.cluster.local
的dns解析,并且在同一个namespace里面的service可以直接用service name进行访问。这是最为推荐的方法。
Service类型
当我们定义一个service的时候,我们可以选择可访问的范围,比如:
- 是否只能在cluster内部访问
- 是否同时可以被cluster内部和外部访问
- 是否是映射到一个集群外的entity上
可访问的范围由service的类型决定,service的类型可以在创建service的时候声明。
ClusterIP 和 NodePort
ClusterIP是默认的service type,一个service通过ClusterIP来获取自己的Virtual IP,这个IP是用来和别的service通信的,只能在集群内部被访问。
NodePort的service type除了会创建一个ClusterIP之外,还会把所有worker node上的一个30000-32767之间的端口映射到这个service,比如假设32233
端口映射到了frontend-svc
,那么不管我们连接到哪个worker node,我们都会被转发到service分配的ClusterIP——172.17.0.4。
默认情况下,当expose到有一个nodeport的时候,kubernetes master会自动随机选择一个30000-32767之间的port,当然,我们自己也可以手动指定这个port。
NodePort的这个service type在我们想要让外网访问我们服务的时候非常有用,用户通过访问node上指定的port就可以访问到这个service。管理员可以在kubernetes集群外再搭一个反向代理就可以更方便地进行访问了。
LoadBalancer
对于LoadBalancer这个Servicetype:
- NodePort和ClusterIP会被自动创建,外部的load balancer会自动路由上去
- service会在一个静态的端口上被暴露
- 通过底层的cloud provider提供的load balancer来暴露到外网
LoadBalancer这个service type只有在底层的基础架构支持了自动创建load balancer的时候kubernetes才支持,比如Google Cloud Platform和aws。
ExternalIP
如果一个service可以路由到一个或者多个worker node上,那么它可以被映射到一个ExternalIP地址。通过这个ExternalIP进入到集群的流量会被路由到其中一个endpoint上。
需要注意的是,ExternalIP并不是由k8s自动管理的,是由管理员手动设置路由到其中的一个node上的。
ExternalName
ExternalName是一个特定的service type,这种service type没有任何的selector也没有任何声明的endpoint。当在集群中访问到这个service的时候,会返回一个外部服务的CNAME。
这个service一般是用来让一个外部的服务在集群内部可以访问到的,比如我们有一个外部服务叫做my-database.example.com
,那么我们可以通过设置ExternalName类型的Service,让内部的其它service通过my-database
之类的名字访问到这个服务。