kubernetes资源对象应用类(二)

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

kubernetes资源对象应用类二

Service 的 ClusterIP 地址

  既然每个 Pod 都会被分配一个单独的 IP 地址而且每个 Pod 都提供了一个独立的 EndpointPod IP + containerPort以被客户端访问那么现在多个 Pod 副本组成了一个集群来提供服务客户端如何访问他们呢传统的做法是部署一个负载均衡器软件或硬件为这组 Pod 开启一个对外的服务端口如 8000 端口并且将这些 Pod 的 Endpoint 列表加入 8000 端口的转发列表中客户端就可以通过负载均衡器的对外 IP 地址 + 8000 端口来访问此服务了。Kubernetes 也是类似的做法Kubernetes 内部在每个Node上都运行了一套全局的虚拟负载均衡器自动注入并自动实时更新集群中所有 Service 的路由表通过 iptables 或者 IPVS 机制把对 Service 的请求转发到其后端对应的某个 Pod 实例上并在内部实现服务的负载均衡与会话保持机制。不仅如此Kubernetes 还采用了一种很巧妙又影响深远的设计—— ClusterIP 地址。我们知道 Pod 的Endpoint 地址会随着 Pod 的销毁和重新创建而发生改变因为新的 Pod 的 IP 地址与之前旧的 Pod 的不同。 Service 一旦被创建Kubernetes 就会自动为它分配一个全局唯一的虚拟 IP 地址—— ClusterIP 地址而且在 Service 的整个生命周期内其 ClusterIP 地址不会发生改变这样一来每个服务就变成了具备唯一 IP 地址的通信节点远程服务之间的通信问题就变成了基础的 TCP 网络通信问题。

  任何分布式系统都会涉及“服务发现”这个基础问题大部分分布式系统都通过提供特定的 API 来实现服务发现功能但这样做会导致平台的侵入性较强。也增加了开发、测试的难度。Kubernetes 则采用了直观朴素的思路轻松解决了这个棘手的问题只要用 Service 的 Name 与 ClusterIP 地址做一个 DNS 域名映射即可。比如我们定义一个 MySQL Service Service 的名称是 mydbserverService 的端口是 3306则在代码中直接通过 mydbserver:3306 即可访问此服务不再需要任何 API 来获取服务的 IP 地址和端口信息。

  之所以说 CLusterIP 地址是一种虚拟 IP 地址原因有以下几点。

  • CLusterIP 地址仅仅作用于 Kubernetes Service 这个对象并由 Kubernetes 管理和分配 IP 地址来源于 ClusterIP 地址池与 Node 和 Master 所在的物理网络完全无关。
  • 因为没有一个“实体网络对象”来响应所以 ClusterIP 地址无法被 Ping 通。ClusterIP 地址只能与 Service Port 组成一个具体的服务访问端点单独的 ClusterIP 不具备 TCP/IP 通信的基础。
  • CLusterIP 属于 Kubernetes 集群这个封闭的空间集群外的节点要访问这个通信端口则需要做一些额外的工作。

下面是名为 tomcat-service.yaml 的 Service 定义文件内容如下

apiVersion: v1
kind: Service
metadata:
  name: tomcat-service
spec:
  ports:
  - port: 8080
  selector:
   tier: frontend

以上代码定义了一个名为 tomcat-service 的 Service它的服务端口为 8080拥有 tier=frontend 标签的所有 Pod 实例都属于它运行下面的命令进行创建

kubectl create -f tomcat-service.yaml

  我们之前在 tomcat-service.yaml 里定义的 Tomcat 的 Pod 刚好拥有这个标签所以刚才创建的 tomcat-service 已经对应了一个 Pod 实例运行下面的命令可以查看 tomcat-service 的 Endpoint 列表其中 172.17.1.3 是 Pod 的 IP 地址8080 端口是 Container 暴露的端口

kubectl get endpoints
NAMEENDPOINTSAGE
kubernetes192.168.18.131:644315d
tomcat-service172.17.1.3:80801m

  你可能有疑问“说好的 Service 的 ClusterIP 地址呢怎么没有看到” 运行下面的命令即可看到 tomcat-service 被分配的 ClusterIP 地址及更多的信息

kubectl get svc tomcat-service -o yaml
apiVersion: v1
kind: Service
spec:
  clusterIP: 10.245.85.70
  ports:
  - port: 8080
    protocol: TCP
    targetPort: 8080
  selector:
    tier: frontend
  sessionAffinity: None
  type: ClusterIP
status:
  loadBalancer: {}

  在 spec.ports 的定义中targetPort 属性用来确定提供该服务的容器所暴露Expose的端口号即具体的业务进程在容器内的 targetPort 上提供 TCP/IP 接入 port 属性则定义了 Service 端口。前面定义 Tomcat 服务时并没有指定 targetPort 所以 targetPort 默认与 port 相同。除了正常的 Service还有一种特殊的 Service——Headless Service只要在 Service 的定义中设置了 clusterIP:None就定义了一个 Headless Service 它与普通 Service 的关键区别在于它没有 ClusterIP 地址如果解析 Headless Service 的 DNS 域名则返回的是该 Service 对应的全部 Pod 的 Endpoint 列表这意味着客户端是直接与后端的 Pod 建立 TCP/IP连接进行通信的没有通过虚拟 ClusterIP 地址进行转发因此通信性能最高等同于“原生网络通信”。

  接下来看看 Service 的多端口问题。很多服务都存在多个端口通常一个端口提供业务服务另一个端口提供管理服务比如 Mycat、Codis等常见中间件。Kubernetes Service 支持多个 Endpoint在存在多个 Endpoint 的情况下要求每个 Endpoint 都定义一个名称进行区分。下面是 Tomcat 多端口的 Service 定义样例

apiVersion: v1
kind: Service
metadata:
  name: tomcat-service
spec:
  ports:
  - port: 8080
    name: service-port
  - port: 8005
    name: shutdown-port
  selector:
    tier: frontend

Service 的外网访问问题

  前面提到服务的 ClusterIP 地址在 Kubernetes 集群内才能被访问那么如何让集群外的应用访问我们的服务呢这也是一个相对复杂的问题。要弄明白这个问题的解决思路和解决办法我们需要先弄明白 Kubernetes 的三种 IP这三种 IP 分别如下。

  • Node IP Node 的 IP 地址。
  • Pod IPPod 的 IP 地址。
  • Service IPService 的 IP 地址。

  首先Node IP 是 Kubernetes 集群中每个节点的物理网卡的 IP 地址是一个真实存在的物理网络所有属于这个网络的服务器都能通过这个网络直接通信不管其中是否有部门节点不属于这个 Kubernetes 集群。这也表明 Kubernetes 集群之外的节点访问 Kubernetes 集群内的某个节点或者 TCP/IP 服务时都必须通过 Node IP 通信。

  其次Pod IP 是每个 Pod 的 IP 地址在使用 Docker 作为容器支持引擎的情况下它是 Docker Engine 根据 docker() 网桥的 IP 地址段进行分配的通常是一个虚拟二层网络。前面说过Kubernetes 要求位于不同 Node 上的 Pod 都能够彼此直接通信所以 Kubernetes 中一个 Pod 里的容器访问另外一个 Pod 里的容器时就是通过 Pod IP 所在的虚拟二层网络进行通信的而真是的 TCP/IP 流量是通过 Node IP 所在的物理网卡流出的。

  在 Kubernetes 集群内Service 的 ClusterIP 地址属于集群内的地址无法在集群外直接使用这个地址。为了解决这个问题Kubernetes 首先引入了 NodePort 这个概念NodePort 也是解决集群外的应用访问集群内服务的直接、有效的做法。

  以 tomcat-service 为例在 Service 的定义里做如下扩展即可

tomcat-service.yaml

apiVersion: v1
kind: Service
metadata:
  name: tomcat-service
spec:
  type: NodePort
  ports:
  - port: 8080
    nodePort: 31002
  selector:
   tier: frontend

  其中nodePort:31002 这个属性表明手动指定 tomcat-service 的 NodePort 为 31002 ,否则 Kubernetes 会自动为其分配一个可用的端口。接下来在浏览器里访问 http://:31002/就可以看到 Tomcat 的欢迎界面了。

  NodePort 的确功能强大且通用性强但也存在一个问题即每个 Service 都需要在 Node 上独占一个端口而端口又是有限的物力资源那能不能让多个 Service 共用一个对外端口呢这就是后来增加的 Ingress 资源对象所要解决的问题。在一定程度上我们可以把 Ingress 的实现机制理解为基于 Nginx 的支持虚拟主机的 HTTP 代理。下面是一个 Ingress 的实例

kind: Ingress
metadata:
  name: name-virtual-host-ingress
spec:
  rules:
  - host: foo.bar.com
    http:
      paths:
      - backend:
        serviceName: service1
        servicePort: 80
  - host: bar.foo.com
    http:
      paths:
      - backend:
        serviceName: service2
        servicePort: 80

有状态的应用集群

  我们知道Deployment 对象是用来实现无状态服务的多副本自动控制功能的那么有状态的服务比如 ZooKeeper 集群、MySQL 高可用集群3 节点集群、Kafka 集群等是怎么实现自动部署和管理的呢这个问题就复杂多了这些一开始是依赖 StatefulSet 解决的但后来发现对于一些复杂的有状态的集群应用来说StatefulSet 还是不够通用和强大所以后面又出现了 Kubernetes Operator。

  我们先说说 StatefulSet 。StatefulSet 之前曾用过 PetSet 这个名称很多人都知道在 IT 世界里有状态的应用被类比为宠物Pet无状态的应用则被类比为牛羊每个宠物在主人那里都是“唯一的存在”宠物生病了我们是要花很多钱去治疗的需要我们用心照料而无差别的牛羊则没有这个待遇。总结下来在有状态集群中一般有如下特殊共性。

  • 每个节点都有固定的身份 ID通过这个 ID集群中的成员可以相互发现并通信。
  • 集群的规模是比较固定的集群规模不能随意变动。
  • 集群中的每个节点都是有状态的通常会持久化数据到永久存储中每个节点在重启后都需要使用原有的持久化数据。
  • 集群中成员节点的启动顺序以及关闭顺序通常也是确定的。
  • 如果磁盘损坏则集群里的某个节点无法正常运行集群功能受损。

  如果通过 Deployment 控制 Pod 副本数量来实现以上有状态的集群我们就会发现上述很多特性大部分难以满足比如 Deployment 创建的 Pod 因为 Pod 的名称是随机产生的我们事先无法为每个 Pod 都确定唯一不变的 ID不同 Pod 的启动顺序也无法保证所以在集群中的某个成员节点宕机后不能在其他节点上随意启动一个新的 Pod 实例。另外为了能够在其他节点上恢复某个失败的节点这种集群中的 Pod 需要挂接某种共享存储为了解决有状态集群这种复杂的特殊应用集群Kubernetes 引入了专门的资源对象 StatefulSet。StatefulSet 从本质上来说可被看作 Deployment/RC 的一个特殊变种它有如下特性。

  • StatefulSet 里的每个 Pod 都有稳定、唯一的网络标识可以用来发现集群内的其他成员。假设 StatefulSet 的名称为 kafka那么第 1 个 Pod 叫 kafka-0 第 2 个叫kafka-1以此类推。
  • StatefulSet 控制的 Pod 副本的启停顺序是受控的操作第 n 个 Pod 时前 n - 1个 Pod 已经是运行且准备好的状态。
  • StatefulSet 里的 Pod 采用稳定的持久化存储卷通过 PV 或 PVC 来实现删除 Pod 时默认不会删除与 StatefulSet 相关的存储卷为了保证数据安全。

  StatefulSet 除了要与 PV 卷捆绑使用以存储 Pod 的状态数据还要与 Headless Service 配合使用即在每个 StatefulSet 定义中都要声明它属于哪个 Headless Service。StatefulSet 在 Headless Service 的基础上又为 StatefulSet 控制的每个 Pod 实例都创建了一个 DNS 域名这个域名格式如下

${podname}.${headless service name}

  比如一个 3 节点的 Kafka 的 StatefulSet 集群对应的 Headless Service 的名称为 kfafkaStatefulSet 的名称为 kafka则 StatefulSet 里的 3 个 Pod 的 DNS 名称分别为 kafak-0.kafka 、 kafka-1.kafka、kafka-2.kafka这些 DNS名称可以直接在集群的配置文件中固定下来。

  StatefulSet 的建模能力有限面对复杂的有状态集群时显得力不从心所以就有了后来的 Kubernetes Operator 框架和众多的 Operator 实现了。需要注意的是Kubernetes Operator 框架并不是面向普通用户的而是面向 Kubernetes 平台开发者的。平台开发者借助 Operator 框架提供的 API可以更方便地开发一个类似 StatefulSet 的控制器。在这个控制器里开发者通过编码方式实现对目标集群的自定义操控包括集群部署、故障发现及集群调整等方面都可以实现有针对性的操控从而实现更好的自动部署和智能运维功能。从发展趋势来看未来主流的有状态集群基本都会以 Operator 方式部署到 Kubernetes 集群中。

批处理应用

  除了无状态服务、有状态集群、常见的第三种应用还有批处理应用。批处理应用的特点是一个或多个进程处理一组数据图像、文件、视频等在这组数据都处理完成后批处理任务自动结束。为了支持这类应用Kubernetes 引入了新的资源对象——Job下面是一个计算圆周率的经典例子

apiVersion: batch/v1
kind: Job
metadata:
  name: pi
spec:
  template:
    spec:
      containers:
      - name: pi
        image: perl
        command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(100)"]
      restartPolicy: Never
  parallelism: 1
  completions: 5

  Jobs控制器提供了两个控制并发数的参数completions 和 parallelismcompletions表示需要运行任务数的总数parallelism 表示并发运行的个数例如设置 parallelism 为 1则会依次运行任务在前面的任务运行后再运行后面的任务。Job 所控制的 Pod 副本是短暂运行的可以将其视为一组容器其中的每个容器都仅运行一次。当 Job 控制的所有 Pod 副本都运行结束时对应的 Job 也就结束了。Job 在实现方式上与 Deployment 等副本控制器不同Job 生成的 Pod 副本是不能自动重启的对应 Pod 副本的 restartPolicy 都被设置为 Never因此当对应的 Pod 副本都执行完成时相应的 Job 也就完成了控制使命。后来Kubernetes 增加了 CronJob可以周期地执行某个任务。

应用的配置问题

  通过前面的学习我们初步理解了三种应用建模的资源对象总结如下。

  • 无状态服务的建模Deployment。
  • 有状态集群的建模StatefulSet。
  • 批处理应用的建模Job。

  在进行应用建模时应该如何解决应用需要在不同的环境中修改配置的问题呢这就涉及 ConfigMap 和 Secret 两个对象。

  ConfigMap 顾名思义就是保存配置项key=value的一个 Map如果你只是把它理解为编程语言中的一个 Map那就大错特错了。ConfigMap 是分布式系统中“配置中心”的独特实现之一。我们知道几乎所有应用都需要一个静态的配置文件来提供启动参数当这个应用是一个分布式应用有多个副本部署到不同的机器上时配置文件的分发就成为一个让人头疼的问题所以很多分布式系统都有一个配置中心的组件来解决这个问题。但配置中心通常会引入新的 API从而导致应用的耦合和侵入。Kubernetes 则采用了一种简单的方案来规避这个问题如图 1.13 所示具体做法如下。

  • 用户将配置文件的内容保存到 ConfigMap 中文件名可作为 keyvalue 就是整个文件的内容多个配置文件都可被放入同一个 ConfigMap。
  • 在建模用户应用时在 Pod 里将 ConfigMap 定义为特殊的 Volume 进行挂载。在 Pod 被调度到某个 Node 上时ConfigMap 里的配置文件会被自动还原到本地目录下然后映射到 Pod 里指定的配置目录下这样用户的程序就可以无感知地读取配置了。
  • 在 ConfigMap 的内容发生修改后Kubernetes 会自动重新获取 ConfigMap 的内容并在目标节点上更新对应的文件。

  接下来说说 Secret。Secret 也用于解决应用配置的问题不过它解决的是对敏感信息的配置问题比如数据库的用户名和密码、应用的数字证书、Token、SSH 密钥及其他需要保密的敏感配置。对于这类敏感信息我们可以创建一个 Secret 对象然后被 Pod 引用。Secret 中的数据要求以 BASE64 编码格式存放。注意BASE64 并不是加密的在 Kubernetes1.7 版本以后Secret 中的数据才可以以加密的形式进行保存更加安全。

应用的运维问题

  本节最后说说与应用的自动运维相关的几个重要对象。

  首先就是 HPAHorizontal Pod Autoscaler如果我们用 Deployment 来控制 Pod 的副本数量则可以通过手工运行 kubectl scale 命令来实现 Pod 扩容或缩容。如果仅仅到此为止则显然不符合谷歌对 Kubernetes 的定位目标——自动化、智能化。在谷歌看来分布式系统要能够根据当前负载的变化自动触发水平扩容或缩容因为这一过程可能是频繁发生、不可预料的所以采用手动控制的方式是不现实的因此就有了后来的 HPA 这个高级功能。我们可以将 HPA 理解为 Pod 横向自动扩容即自动控制 Pod 数量的增加或减少。通过追踪分析指定 Deployment 控制的所有目标 Pod 的负载变化情况来确定是否需要有针对性地调整目标 Pod 的副本数量这是 HPA 的实现原理。Kubernetes 内置了基于 Pod 的 CPU 利用率进行自动扩缩容的机制应用开发者也可以自定义度量指标如每秒请求数来实现自定义的 HPA 功能。下面是一个 HPA 定义的例子

apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
  name: php-apache
  namespace: default
spec:
  maxReplicas: 10
  minReplicas: 1
  scaleTargetRef:
    kind: Deployment
    name: php-apache
  targetCPUUtilizationPercentage: 50

  根据上面的定义我们可以知道这个 HPA 控制的目标对象是一个名为 php-apache 的 Deployment 里的 Pod 副本当这些 Pod 副本的 CPU利用率的值超过 90%时会触发自动动态扩容限定 Pod 的副本数量为 1~10。HPA 很强大也比较复杂我们在后续章节中会继续深入学习。

  接下来就是 VPAVertical Pod Autoscaler即垂直 Pod 自动扩缩容它根据容器资源使用率自动推测并设置 Pod 合理的 CPU 和内存的需求指标从而更加精确地调度 Pod实现整体上节省集群资源的目标因为无须人为操作因此也进一步提升了运维自动化的水平。VPA 目前属于比较新的特性也不能与 HPA 共同操控同一组目标 Pod它们未来应该会深入融合建议读者关注其发展状况。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: k8s