Pod 在提供服务时有一些问题:
- Pod 不是永久的,可能会被销毁或者调度走
- Client 可能不知道 pod 的 IP 端口
- 提供同个服务的 pod 可以有很多个
因此需要有个固定的方式来访问 pod 中的服务。k8s 的 service 资源提供了这样的能力。
基础
Service 类似 pod,rc 等,是 k8s 中的一种资源。指定一个 YAML 或者使用 kubectl expose 来创建它。
Service 在其整个生命周期,有固定的 IP 和 端口。Client 连接到 service 的 IP 端口即可访问其后的 pod。Service 会做负载均衡。
Service 的 YAML,通过 selector 来指定其转发到哪些 pod 上:
apiVersion: v1
kind: Service
metadata:
name: kubia
spec:
ports:
- port: 80 # 访问该 service 的 80 端口时,
targetPort: 8080 # 请求会被转发到 pod 的 8080 端口
selector:
app: kubia
可以设置同一个 client 的多次请求都走向同一个 pod:
apiVersion: v1
kind: Service
spec:
sessionAffinity: ClientIP
# ...
可以转发到 pod 中的多个端口:
apiVersion: v1
kind: Service
metadata:
name: kubia
spec:
ports: # 多个 port 结构
- name: http
port: 80
targetPort: 8080
- name: https
port: 443
targetPort: 8443
selector:
app: kubia
targetPort
可以使用 pod 定义中的端口名字,方便 pod 更换端口号:
kind: Pod
spec:
containers:
- name: kubia
ports:
- name: http # 8080 端口的名字为 http
containerPort: 8080
- name: https
containerPort: 8443
apiVersion: v1
kind: Service
spec:
ports:
- name: http
port: 80
targetPort: http # 使用端口名来指向 pod 定义中的 8080 端口
- name: https
port: 443
targetPort: https
集群中的其他 pod 如何得知某 service 的 IP 端口?
k8s 提供了两种方式:环境变量 和 DNS 服务:
环境变量比较鸡肋,pod 只能看到在它启动之前已经存在的 service;后面创建的 service 不行。
DNS 服务是合适的。比如你要查 default 命名空间下的 backend-database service 的 IP,你可以通过 DNS 查询这个 FQDN 的 A / AAAA 纪录来获得 IP:
backend-database.default.svc.cluster.local
你也可以省略掉 svc.cluster.local
,只使用 backend-database.default
。如果发起请求的 pod 也在同个命名空间(default),那只使用 backend-database
来查询也是可以的。
kube-dns 也提供了 SRV 类型的 DNS 查询,可以查到端口。但不是很常用。请求方可能要提前知道 service 的端口(比如通过约定使用常用的 80 / 443)。
内部实现上,k8s 会修改容器中的 /etc/resolv.conf
使其指向 kube-dns
pod 所提供的 DNS 服务。kube-dns
本身也是一个 service,可以通过 kubectl get service -n kube-system
观察。
Service Endpoints
Endpoints 是 k8s 的另一种资源。当创建 service 时,k8s 会解析并监视被 service 选中的 pod,将其 IP 和端口存入其新建的同名 endpoints 中,比如:
$ kubectl describe svc kubia
Name: kubia
Namespace: default
Labels: <none>
Selector: app=kubia
Type: ClusterIP
IP: 10.111.249.153
Port: <unset> 80/TCP
Endpoints: 10.108.1.4:8080,10.108.2.5:8080,10.108.2.6:8080 # kubia service 对应的 endpoints
Session Affinity: None
创建 kubia service 时会同时建立的 kubia endpoints:
$ kubectl get endpoints kubia
NAME ENDPOINTS AGE
kubia 10.108.1.4:8080,10.108.2.5:8080,10.108.2.6:8080 1h
每当有新的 pod 满足 service 的 selector,或者有已有 pod 不再满足时,k8s 会监视到这部分变化并更新对应的 endpoints 结构。每当访问 service IP 端口时,k8s 会从 endpoints 中选择一个 pod 的 IP 端口进行转发。
如何使 service 指向外部服务(而不是集群内 pod)?
第一种方法叫 headless service,即没有 selector 的 service。
如果你创建 service 时不指定 selector,k8s 不会帮你自动创建 endpoints。此时你再手工创建同名 endpoints,使其 IP 端口列表指向你想访问的外部服务即可。过程如下:
- 创建一个没有 selector 的 service:
apiVersion: v1 kind: Service metadata: name: external-service spec: ports: - port: 80
- 再创建一个同名 endpoints:
apiVersion: v1 kind: Endpoints metadata: name: external-service subsets: - addresses: # 外部服务的 IP 端口列表 - ip: 11.11.11.11 - ip: 22.22.22.22 ports: - port: 80
第二种方法是使用 ExternalName 的 service 类型:
apiVersion: v1
kind: Service
metadata:
name: external-service
spec:
type: ExternalName
externalName: someapi.somecompany.com
ports:
- port: 80
如何使集群外可以访问 service?
默认的 service 类型是 ClusterIP,生成的 IP 只能在集群内被访问。Service 还有其他类型:
- NodePort:可以用集群内所有的 node 上的某个端口来访问此 service。这样外部服务可以访问任一 node 的 node port 来访问 service。缺点明显,某一 node 可能不可用,此时外部访问就失败了。同时要留意 node 机器上的防火墙策略
- LoadBalancer:这种需要你的容器服务厂商支持,比如 GKE。应用此类型后,会在 NodePort 基础上又新建一个外网可访问的负载均衡 IP(比如在 Google Cloud 中会新建一个 load balancer,公网 IP 会收费)。这样外部服务可以通过此 IP 来访问
但 最合理的方式是使用 ingress controller,也需要你的容器服务支持。Ingress controller 类似 nginx,提供了 7 层的负载均衡,可以在 HTTP 层做路由等:
创建一个 ingress controller 类似这样:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: kubia
spec:
rules:
- host: kubia.example.com
http:
paths:
- path: /
backend:
serviceName: kubia-nodeport
servicePort: 80
对比 LoadBalaner 的好处是:
- LoadBalancer 模式中每个 service 都需要一个单独的公网 IP;但是 ingress controller 可以用一个公网 IP,将请求路由到多个 service
- 可以在 ingress controller 上 配置 TLS,使外部请求通过 HTTPS 进来;而 ingress controller 转发给 pod 的请求可以只走 HTTP
另外,ingress 不会转发请求给 service;它只是通过 service 来找到对应的 pod。
如何判断某 pod 是否可以接受请求?
见 Kubernetes: Resource: Pod 中的 readinessProbe 一节。
如何同时请求某个 service 下的多个 pod?
将 ClusterIP 设为 None,使得查询 service 的 DNS 请求返回的不再是 service 的 IP,而是底下所有 pod 的 IP:
apiVersion: v1
kind: Service
metadata:
name: kubia-headless
spec:
clusterIP: None # 配置这里
ports:
- port: 80
targetPort: 8080
selector:
app: kubia
这样你可以对所有 pod 同时发请求。
出错时的定位方法
如果你请求某个 service 失败了,这样定位:
- 确定请求发起方是在集群内,而不是集群外
- 不要 ping service IP,没有用
- 如果定义了 readiness probe,看看它成功了吗
- 用
kubectl get endpoints
看看该 service 的 pod 列表 - 如果你是用 service 的域名来访问的话,试试直接访问 service 的 ClusterIP 能否成功
- 确认下你访问的端口号是不是 service 的端口号(而不是 pod 的端口号)
- 直接访问 pod 的 IP 端口试试;如果不能访问,看看 pod 中的服务是不是监听在 localhost 上了(localhost 不能被 pod 外访问)