Kubernetes: Resource: Service

12th March 2021 at 4:57pm
Kubernetes: Resource

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 端口列表指向你想访问的外部服务即可。过程如下:

  1. 创建一个没有 selector 的 service:
    apiVersion: v1
    kind: Service
    metadata:
      name: external-service     
    spec:                       
      ports:
      - port: 80 
  2. 再创建一个同名 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 失败了,这样定位:

  1. 确定请求发起方是在集群内,而不是集群外
  2. 不要 ping service IP,没有用
  3. 如果定义了 readiness probe,看看它成功了吗
  4. kubectl get endpoints 看看该 service 的 pod 列表
  5. 如果你是用 service 的域名来访问的话,试试直接访问 service 的 ClusterIP 能否成功
  6. 确认下你访问的端口号是不是 service 的端口号(而不是 pod 的端口号)
  7. 直接访问 pod 的 IP 端口试试;如果不能访问,看看 pod 中的服务是不是监听在 localhost 上了(localhost 不能被 pod 外访问)