Kubernetes入门

127 阅读12分钟

以下80%内容均来自张磊老师的《深入剖析Kubernetes》

kubernetes常常简称“k8s”,仅保留了头尾2个字母(k和s),中间的8个字母都去掉了,用“8”代替。

Kubernetes是开源的容器集群管理器,意图成为能够在容器领域自治化部署以及扩展应用程序的平台。

历史

Kubernetes这个词是“舵手”的希腊语,该项目是Google在2014年启动的。

Kubernetes 项目的基础特性,并不是几个工程师突然“拍脑袋”想出来的东西,而是 Google 公司在容器化基础设施领域多年来实践经验的沉淀与升华。

Kubernetes 项目早期的 GitHub Issue 和 Feature ,大多来自于 Borg 和 Omega 系统的内部特性。

跟很多基础设施领域先有工程实践、后有方法论的发展路线不同,Kubernetes 项目的理论基础则要比工程实践走得靠前得多,这当然要归功于 Google 公司在 2015 年 4 月发布的 Borg 论文了。

在 Google 公司已经公开发表的基础设施体系论文中,Borg 项目当仁不让地位居整个基础设施技术栈的最底层。

image.png

Docker的使用

以mysql为例:

  1. 访问 MySQL 镜像库地址:hub.docker.com/_/mysql?tab…

  1. 拉镜像
docker pull mysql:latest
  1. 运行容器
$ docker run -itd --name mysql-test -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql
  1. 启动成功
docker  ps
  1. 进入容器
docker exec -it mysql bash

容器的本质是进程。

容器的“单进程模型”,并不是指容器里只能运行“一个”进程,而是指容器没有管理多个进程的能力。这是因为容器里 PID=1 的进程就是应用本身,其他的进程都是这个 PID=1 进程的子进程。可是,用户编写的应用,并没有拥有进程管理的功能。比如,你的应用是一个 Java Web 程序(PID=1),然后你执行 docker exec 在后台启动了一个 Nginx 进程(PID=3)。可是,当这个 Nginx 进程异常退出的时候,你该怎么知道呢?这个进程退出后的垃圾收集工作,又应该由谁去做呢?

容器技术

  • 容器从一个开发者手里的小工具,一跃成为了云计算领域的绝对主角;而能够定义容器组织和管理规范的“容器编排”技术,则当仁不让地坐上了容器技术领域的“头把交椅”。
  • 容器其实是一种沙盒技术。顾名思义,沙盒就是能够像一个集装箱一样,把你的应用“装”起来的技术。这样,应用与应用之间,就因为有了边界而不至于相互干扰。
  • 容器技术的核心功能,就是通过约束和修改进程的动态表现,从而为其创造出一个“边界”。
  • 对于 Docker 等大多数 Linux 容器来说,Cgroups 技术是用来制造约束的主要手段,而 Namespace 技术则是用来修改进程视图的主要方法。

Namespace

PID Namespace

我们都知道docker容器其实只是一种特殊的进程而已,因此只会有一个主进程,也就是1号进程。PID Namespace 的作用是用来隔离进程,利用 PID Namespace 可以实现每个容器的主进程为 1 号进程,而容器内的进程在主机上却拥有不同的PID,也就是它真实的PID。

Namespace 的使用方式也非常有意思:它其实只是 Linux 创建新进程的一个可选参数。而当我们用 clone() 系统调用创建一个新进程时,就可以在参数中指定 CLONE_NEWPID 参数,比如:

int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL);

这时,新创建的这个进程将会“看到”一个全新的进程空间,在这个进程空间里,它的 PID 是 1。在宿主机真实的进程空间里,这个进程的 PID 还是真实的数值,比如 100。

而除了我们刚刚用到的 PID Namespace,Linux 操作系统还提供了 Mount、UTS、IPC、Network 和 User 这些 Namespace,用来对各种不同的进程上下文进行“障眼法”操作。

比如,Mount Namespace,用于让被隔离进程只看到当前 Namespace 里的挂载点信息;Network Namespace,用于让被隔离进程看到当前 Namespace 里的网络设备和配置。

所以,Docker 容器这个听起来玄而又玄的概念,实际上是在创建容器进程时,指定了这个进程所需要启用的一组 Namespace 参数。这样,容器就只能“看”到当前 Namespace 所限定的资源、文件、设备、状态,或者配置。而对于宿主机以及其他不相关的程序,它就完全看不到了。

所以,在使用 Docker 的时候,并没有一个真正的“Docker 容器”运行在宿主机里面。Docker 项目帮助用户启动的,还是原来的应用进程,只不过在创建这些进程时,Docker 为它们加上了各种各样的 Namespace 参数。

Cgroups

Linux Cgroups 就是 Linux 内核中用来为进程设置资源限制的一个重要功能。

Linux Cgroups 的全称是 Linux Control Group。它最主要的作用,就是限制一个进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等等。

比如,你可以通过它的相关命令,限制某个进程只能使用整个宿主机cpu的10%

在 Linux 中,Cgroups 给用户暴露出来的操作接口是文件系统,即它以文件和目录的方式组织在操作系统的 /sys/fs/cgroup 路径下。

可以看到,在 /sys/fs/cgroup 下面有很多诸如 cpuset、cpu、 memory 这样的子目录,也叫子系统。

比如,我们现在进入 /sys/fs/cgroup/cpu 目录下:

root@ubuntu:/sys/fs/cgroup/cpu$ mkdir container
root@ubuntu:/sys/fs/cgroup/cpu$ ls container/

cgroup.clone_children cpu.cfs_period_us cpu.rt_period_us  cpu.shares notify_on_release
cgroup.procs      cpu.cfs_quota_us  cpu.rt_runtime_us cpu.stat  tasks

这个目录就称为一个“控制组”。你会发现,操作系统会在你新创建的 container 目录下,自动生成该子系统对应的资源限制文件。现在,我们在后台执行一个死循环脚本,然后我们查到它的进程id(PID)是 226。这时候通过top指令可以看到cpu被打满了。

$ top
%Cpu0 :100.0 us, 0.0 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st

接下来,我们可以通过修改这些文件的内容来设置限制。比如,向 container 组里的 cfs_quota 文件写入 20 ms(默认是100ms):

$ echo 20000 > /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us

它意味着在每 100 ms 的时间里,被该控制组限制的进程只能使用 20 ms 的 CPU 时间,也就是说这个进程只能使用到 20% 的 CPU 带宽。

接下来,我们把被限制的进程的 PID 写入 container 组里的 tasks 文件,上面的设置就会对该进程生效了:

$ echo 226 > /sys/fs/cgroup/cpu/container/tasks 

这时再通过top指令,就可以看到计算机的 CPU 使用率立刻降到了 20%(%Cpu0 : 20.3 us)。

chroot

每当创建一个新容器时,我希望容器进程看到的文件系统就是一个独立的隔离环境,而不是继承自宿主机的文件系统。怎么才能做到这一点呢?我们可以在容器进程启动之前重新挂载它的整个根目录“/”。而由于 Mount Namespace 的存在,这个挂载对宿主机不可见,所以容器进程就可以在里面随便折腾了。

在 Linux 操作系统里,有一个名为 chroot 的命令可以帮助你在 shell 中方便地完成这个工作。顾名思义,它的作用就是帮你“change root file system”,即改变进程的根目录到你指定的位置

例如:执行 chroot 命令,告诉操作系统,我们将使用 $HOME/test 目录作为 /bin/bash 进程的根目录:

$ chroot $HOME/test /bin/bash

对 Docker 项目来说,它最核心的原理实际上就是为待创建的用户进程:

  1. 启用 Linux Namespace 配置;
  1. 设置指定的 Cgroups 参数;
  1. 切换进程的根目录(Change Root)。

这样,一个完整的容器就诞生了。

容器与虚拟机的区别

使用虚拟化技术作为应用沙盒,就必须要由 Hypervisor 来负责创建虚拟机,这个虚拟机是真实存在的,并且它里面必须运行一个完整的 Guest OS 才能执行用户的应用进程。这就不可避免地带来了额外的资源消耗和占用。

而相比之下,容器化后的用户应用,却依然还是一个宿主机上的普通进程,这就意味着这些因为虚拟化而带来的性能损耗都是不存在的;而另一方面,使用 Namespace 作为隔离手段的容器并不需要单独的 Guest OS,这就使得容器额外的资源占用几乎可以忽略不计。

Kubernetes的使用

架构与组件

Kubernetes 项目的架构,都由 Master 和 Node 两种节点组成,而这两种角色分别对应着控制节点和计算节点。

Master 节点: 由三个紧密协作的独立组件组合而成,它们分别是负责 API 服务的 kube-apiserver、负责调度的 kube-scheduler,以及负责容器编排的 kube-controller-manager。

Etcd : 整个集群的持久化数据,则由 kube-apiserver 处理后保存在 Etcd 中。

Node(计算)节点:

  • kubelet

    1.     kubelet 会在集群中每个节点(node)上运行。 它保证容器(containers)都运行在 Pod 中。
    2.     kubelet 接收一组通过各类机制提供给它的 PodSpecs, 确保这些 PodSpecs 中描述的容器处于运行状态且健康。 kubelet 不会管理不是由 Kubernetes 创建的容器。
    3.     在 Kubernetes 项目中,kubelet 主要负责同容器运行时(比如 Docker 项目)打交道。而这个交互所依赖的,是一个称作 CRI(Container Runtime Interface)的远程调用接口,这个接口定义了容器运行时的各项核心操作
    4.     kubelet 的另一个重要功能,则是调用网络插件和存储插件为容器配置网络和持久化存储。这两个插件与 kubelet 进行交互的接口,分别是 CNI(Container Networking Interface)和 CSI(Container Storage Interface)。
  • kube -proxy

    1.     kube-proxy是集群中每个节点(node)所上运行的网络代理, 实现 Kubernetes 服务(Service) 概念的一部分。
    2.     kube-proxy 维护节点上的一些网络规则, 这些网络规则会允许从集群内部或外部的网络会话与 Pod 进行网络通信。
    3.     如果操作系统提供了可用的数据包过滤层,则 kube-proxy 会通过它来实现网络规则。 否则,kube-proxy 仅做流量转发。
  • 容器运行时(Container Runtime)

    1.     容器运行环境是负责运行容器的软件。
    2.     Kubernetes 支持许多容器运行环境,例如 containerdCRI-O 以及 Kubernetes CRI (容器运行环境接口) 的其他任何实现。

Kubernetes对象

Pod

Pod 是 是 Kubernetes 项目中的最小编排单位,在常规环境下,我们的多个应用往往会被直接部署在同一台机器上,通过 Localhost 通信,通过本地磁盘目录交换文件。而在 Kubernetes 项目中,这些容器则会被划分为一个“Pod”,Pod 里的容器共享同一个 Network Namespace、同一组数据卷,从而达到高效率交换信息的目的。

所以,Pod,其实是一组共享了某些资源的容器。

Pod 就是 Kubernetes 世界里的“应用”;而一个应用,可以由多个容器组成。

那么如何创建Pod对象呢,什么才是 Kubernetes 项目能“认识”的方式,答案是编写配置文件,这些配置文件通常以yaml格式的文件存在。

apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  shareProcessNamespace: true
  containers:
  - name: nginx
    image: nginx
  - name: shell
    image: busybox
    stdin: true
    tty: true

Deployment

拿我们公司系统现在的后端系统为例,每一个后端服务都对应一个k8s中的一个Deployment对象

Deployment 扮演的正是 Pod 的控制器的角色。

像这样使用一种 API 对象(Deployment)管理另一种 API 对象(Pod)的方法,在 Kubernetes 中,叫作“控制器”模式(controller pattern)

---
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations: {}
  labels:
    app: data-collect
    pangu.dev/app-id: '377'
    pangu.dev/git-commit-id: 3ed1621b1454effce740d197dcdd7a4b356fe926
    pangu.dev/is-commit-id: 'true'
    pangu.dev/task-id: '23115'
  name: data-collect
  namespace: datacenter-testing
  resourceVersion: '101643452'
spec:
  progressDeadlineSeconds: 600
  replicas: 2
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      app: data-collect
  strategy:
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25%
    type: RollingUpdate
  template:
    metadata:
      annotations:
        kubectl.kubernetes.io/restartedAt: '2022-06-06T11:16:27+08:00'
        redeploy-timestamp: '1625626633484'
      creationTimestamp: null
      labels:
        app: data-collect
    spec:
      containers:
          image: >-
            registry-vpc.cn-shenzhen.cr.aliyuncs.com/datacenter/data-collect:20221020-49268-3ed1621
          imagePullPolicy: IfNotPresent
          lifecycle:
            preStop:
              exec:
                command:
                  - ./nacos-logout.sh
          resources:
            limits:
              cpu: '2'
              memory: 1Gi
            requests:
              cpu: 50m
              memory: 1Gi
          terminationMessagePath: /dev/termination-log
          terminationMessagePolicy: File
      dnsPolicy: ClusterFirst
      imagePullSecrets:
        - name: yatsen-docker
      restartPolicy: Always
      schedulerName: default-scheduler
      securityContext: {}
      terminationGracePeriodSeconds: 30

像这样的一个 YAML 文件,对应到 Kubernetes 中,就是一个 API Object(API 对象)。当你为这个对象的各个字段填好值并提交给 Kubernetes 之后,Kubernetes 就会负责创建出这些对象所定义的容器或者其他类型的 API 资源。

可以看到,这个 YAML 文件中的 Kind 字段,指定了这个 API 对象的类型(Type),是一个 Deployment。

所谓 Deployment,是一个定义多副本应用(即多个副本 Pod)的对象,此外,Deployment 还负责在 Pod 定义发生变化时,对每个副本进行滚动更新(Rolling Update)。

在上面这个 YAML 文件中,我给它定义的 Pod 副本个数 (spec.replicas) 是:2。

在了解了上述 Kubernetes 配置文件的基本知识之后,我们现在就可以把这个 YAML 文件“运行”起来。你可以使用 kubectl apply 指令完成这个操作:

$ kubectl apply  -f nginx-deployment.yaml

其他命令:

$ kubectl get pods
NAME                                READY     STATUS              RESTARTS   AGE
nginx-deployment-5c678cfb6d-v5dlh   0/1       ContainerCreating   0          4s
nginx-deployment-67594d6bf6-9gdvr   1/1       Running             0          10m
nginx-deployment-67594d6bf6-v6j7w   1/1       Running             0          10m
$ kubectl get pods
NAME                                READY     STATUS    RESTARTS   AGE
nginx-deployment-5c678cfb6d-lg9lw   1/1       Running   0          8s
nginx-deployment-5c678cfb6d-v5dlh   1/1       Running   0          19s

一般推荐使用 kubectl apply 命令,来统一进行 Kubernetes 对象的创建和更新操作。也就是说,作为用户,你不必关心当前的操作是创建,还是更新,你执行的命令始终是 kubectl apply,而 Kubernetes 则会根据 YAML 文件的内容变化,自动进行具体的处理。这样的操作方法,是 Kubernetes“声明式 API”所推荐的使用方法。

$ kubectl apply -f nginx-deployment.yaml
# 修改nginx-deployment.yaml的内容
$ kubectl apply -f nginx-deployment.yaml

这个流程的好处是,它有助于帮助开发和运维人员,围绕着可以版本化管理的 YAML 文件,而不是“行踪不定”的命令行进行协作,从而大大降低开发人员和运维人员之间的沟通成本。

可以看到,Kubernetes 推荐的使用方式,是用一个 YAML 文件来描述你所要部署的 API 对象。然后,统一使用 kubectl apply 命令完成对这个对象的创建和更新操作。

“声明式”带来最大的好处,其实正是“自动化”。作为一个 Kubernetes 用户,你只需要在 YAML 里描述清楚这个应用长什么样子,那么剩下的所有事情,就都可以放心地交给 Kubernetes 自动完成了:它会通过控制器模式确保这个系统里的应用状态,最终并且始终跟你在 YAML 文件里的描述完全一致。

 而 Kubernetes 里“最小”的 API 对象是 Pod。Pod 可以等价为一个应用,所以,Pod 可以由多个紧密协作的容器组成。在 Kubernetes 中,我们经常会看到它通过一种 API 对象来管理另一种 API 对象,比如 Deployment 和 Pod 之间的关系;而由于 Pod 是“最小”的对象,所以它往往都是被其他对象控制的。这种组合方式,正是 Kubernetes 进行容器编排的重要模式。

前面介绍 Kubernetes 架构的时候,提到过一个叫作 kube-controller-manager 的组件。实际上,这个组件,就是一系列控制器的集合。Deployment,正是这些控制器中的一种。

以 Deployment 为例,简单描述一下它对控制器模型的实现: Deployment 控制器从 Etcd 中获取到所有携带了“app: nginx”标签的 Pod,然后统计它们的数量,这就是实际状态;

Deployment 对象的 Replicas 字段的值就是期望状态;

Deployment 控制器将两个状态做比较,然后根据比较结果,确定是创建 Pod,还是删除已有的 Pod。

maxSurge 指定的是,在一次“滚动”中,Deployment 控制器还可以创建多少个新 Pod;而 maxUnavailable 指的是,在一次“滚动”中,Deployment 控制器可以删除多少个旧 Pod。

Deployment 控制器实际操纵的,是ReplicaSet 对象,而不是 Pod 对象。

通过这张图,我们就很清楚地看到,一个定义了 replicas=3 的 Deployment,与它的 ReplicaSet,以及 Pod 的关系,实际上是一种“层层控制”的关系。

而在此基础上,Deployment 同样通过“控制器模式”,来操作 ReplicaSet 的个数和属性,进而实现“水平扩展 / 收缩”和“滚动更新”这两个编排动作。

比方说,当我们应用构建完成后,运维平台就会修改镜像的标签,更新deployment的信息,执行apply操作。

新 ReplicaSet 管理的 Pod 副本数,从 0 个变成 1 个,再变成 2 个,最后变成 3 个。而旧的 ReplicaSet 管理的 Pod 副本数则从 3 个变成 2 个,再变成 1 个,最后变成 0 个。这样,就完成了这一组 Pod 的版本升级过程。

像这样,将一个集群中正在运行的多个 Pod 版本,交替地逐一升级的过程,就是“滚动更新”。

StatefulSet

Deployment中的pod,它们互相之间没有顺序,也无所谓运行在哪台宿主机上。需要的时候,Deployment 就可以通过 Pod 模板创建新的 Pod;不需要的时候,Deployment 就可以“杀掉”任意一个 Pod。属于无状态的。

StatefulSet 实现了对有状态应用的支持。就是通过某种方式记录这些状态,然后在 Pod 被重新创建时,能够为新 Pod 恢复这些状态。

拓扑状态。这些应用实例,必须按照某些顺序启动,比如应用的主节点 A 要先于从节点 B 启动。

存储状态。这种情况意味着,应用的多个实例分别绑定了不同的存储数据。对于这些应用实例来说,Pod A 第一次读取到的数据,和隔了十分钟之后再次读取到的数据,应该是同一份,哪怕在此期间 Pod A 被重新创建过。这种情况最典型的例子,就是一个数据库应用的多个存储实例。

Job

Kubernetes jobs主要是针对短时和批量的工作负载。它是为了结束而运行的,而不是像deployment、replicasets、replication controllers和DaemonSets等其他对象那样持续运行。

Service

一般来说,在k8s中一个服务有多个实例,是通过pod去部署的,多个实例则有多个pod,每一个pod都会有自己的容器组ip,在集群内,直接通过这个容器ip和端口是可以访问服务的,但是每次服务重启,也就是pod销毁重建的时候,ip也会改变。k8s并没有一个注册中心让我们注册实例的ip和端口到服务下面,并且其他服务通过这个注册中心去发现服务。在k8s里是通过service去实现的。

Kubernetes 项目的做法是给 Pod 绑定一个 Service 服务,而 Service 服务声明的 IP 地址等信息是“终生不变”的。这个 Service 服务的主要作用,就是作为 Pod 的代理入口(Portal),从而代替 Pod 对外暴露一个固定的网络地址。

这是一个service定义的例子, 这个 Service 将被指派一个 IP 地址(通常称为 “Cluster IP”),上述的selector代表这个service代理的pod选择器,也就是说具有这个app标签的选择器的一组pod,可以通过service这个统一的访问入口去做服务发现。

比如说我们现在有一组pod : bdp-service

image.png

C:\Users\huimin.li>kubectl get pods -l app=bdp-service -n datacenter-uat
NAME                          READY   STATUS    RESTARTS   AGE
bdp-service-cfc7f8999-2mfwq   1/1     Running   0          35d

查看它的service

C:\Users\huimin.li>kubectl describe service bdp-service -n datacenter-uat
Name:              bdp-service-svc
Namespace:         datacenter-uat
Labels:            app=bdp-service
Annotations:       kubectl.kubernetes.io/last-applied-configuration:
                     {"apiVersion":"v1","kind":"Service""metadata":{"annotations":{},"labels":{"app":"bdp-service"},"name":"bdp-service-svc","namespace":"data...
Selector:          app=bdp-service
Type:              ClusterIP
IP:                172.21.**.***
Port:              bdp-service  3908/TCP
TargetPort:        3908/TCP
Endpoints:         10.0.**.**:3908
Session Affinity:  None

service信息包括:name,namespace,selector,labels等基础信息

type是ClusterIP,IP这个值 即是在集群内部,其他所有的pod都可以通过这个虚拟的ip来访问service,service就会将这个请求负载均衡到后端的所有pod上去。

Port是访问service的端口

TargetPort是代理的pod的端口

Endpoints是当前service代理的所有pod的ip地址,当某个pod被销毁重建的时候,这里的ip地址也会随之变化(被 selector 选中的 Pod,就称为 Service 的 Endpoints)

集群内其他pod访问我们创建的service有三种方式:

  1. 通过clusterIp+port直接去访问
  2. 同一个namespace直接访问服务名,不同的 namespace 里面,我们可以通过 service 名字加“.”kube-dns可以解决Service的发现问题,k8s将Service的名称当做域名注册到kube-dns中,通过Service的名称就可以访问其提供的服务。

image.png 3. 通过环境变量访问,在同一个 namespace 里的 pod 启动时,kubelet 会为每个活跃的 Service 添加一组环境变量,会把 service 的一些 IP 地址、端口,以及一些简单的配置,通过环境变量的方式放到 K8s 的 pod 里面。因此我们可以通过环境变量去直接访问。

所谓 Service 的访问入口,其实就是每台宿主机上由 kube-proxy 生成的 iptables 规则,以及 kube-dns 生成的 DNS 记录。而一旦离开了这个集群,这些信息对用户来说,也就自然没有作用了。

上面三种方式都是在k8s集群内部互相访问的情况,对我们应用的某些部分,可能希望将 Service 暴露在一个外部 IP 地址上。 Kubernetes 支持两种实现方式:NodePort 和 LoadBalancer。

我们看下我们原本的service 的yaml文件

image.png

把type改成NodePort 或者 LoadBalancer

然后重新创建service,再看看信息   

image.png

发现有了EXTERNAL-IP,这个ip是一个公网的ip,因此我们就不只可以在集群上访问我们的服务了。

Kubernetes 上的 Flink session 集群

来自flink 官方文档

Flink session 集群 是以一种长期运行的 Kubernetes Deployment 形式执行的。你可以在一个 session 集群 上运行多个 Flink 作业。当然,只有 session 集群部署好以后才可以在上面提交 Flink 作业。

在 Kubernetes 上部署一个基本的 Flink session 集群 时,一般包括下面三个组件:

  1. 运行 JobManagerDeployment
  2. 运行 TaskManagersDeployment
  3. 暴露 JobManager 上 REST 和 UI 端口的 Service

使用通用集群资源定义中提供的文件内容来创建以下文件,并使用 kubectl 命令来创建相应的组件:

# 为集群创建 deployment
$ kubectl create -f jobmanager-session-deployment.yaml
$ kubectl create -f taskmanager-session-deployment.yaml

# Configuration 和 service 的定义
$ kubectl create -f flink-configuration-configmap.yaml
$ kubectl create -f jobmanager-service.yaml

接下来,我们设置端口转发以访问 Flink UI 页面并提交作业:

  1. 运行 kubectl port-forward ${flink-jobmanager-pod} 8081:8081
  2. 将 jobmanager 的 web ui 端口映射到本地 8081。在浏览器中导航到 http://localhost:8081 页面。
  3. 此外,也可以使用如下命令向集群提交作业:
$ ./bin/flink run -m localhost:8081 ./examples/streaming/TopSpeedWindowing.jar

可以使用以下命令停止运行 flink 集群:

$ kubectl delete -f jobmanager-service.yaml
$ kubectl delete -f flink-configuration-configmap.yaml
$ kubectl delete -f taskmanager-session-deployment.yaml
$ kubectl delete -f jobmanager-session-deployment.yaml

  yaml文件参考

jobmanager-session-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: flink-jobmanager
spec:
  replicas: 1
  selector:
    matchLabels:
      app: flink
      component: jobmanager
  template:
    metadata:
      labels:
        app: flink
        component: jobmanager
    spec:
      containers:
      - name: jobmanager
        image: apache/flink:1.15.2-scala_2.12
        args: ["jobmanager"]
        ports:
        - containerPort: 6123
          name: rpc
        - containerPort: 6124
          name: blob-server
        - containerPort: 8081
          name: webui
        livenessProbe:
          tcpSocket:
            port: 6123
          initialDelaySeconds: 30
          periodSeconds: 60
        volumeMounts:
        - name: flink-config-volume
          mountPath: /opt/flink/conf
        securityContext:
          runAsUser: 9999  # 参考官方 flink 镜像中的 _flink_ 用户,如有必要可以修改       volumes:
      - name: flink-config-volume
        configMap:
          name: flink-config
          items:
          - key: flink-conf.yaml
            path: flink-conf.yaml
          - key: log4j-console.properties
            path: log4j-console.properties

taskmanager-session-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: flink-taskmanager
spec:
  replicas: 2
  selector:
    matchLabels:
      app: flink
      component: taskmanager
  template:
    metadata:
      labels:
        app: flink
        component: taskmanager
    spec:
      containers:
      - name: taskmanager
        image: apache/flink:1.15.2-scala_2.12
        args: ["taskmanager"]
        ports:
        - containerPort: 6122
          name: rpc
        - containerPort: 6125
          name: query-state
        livenessProbe:
          tcpSocket:
            port: 6122
          initialDelaySeconds: 30
          periodSeconds: 60
        volumeMounts:
        - name: flink-config-volume
          mountPath: /opt/flink/conf/
        securityContext:
          runAsUser: 9999  # 参考官方 flink 镜像中的 _flink_ 用户,如有必要可以修改       volumes:
      - name: flink-config-volume
        configMap:
          name: flink-config
          items:
          - key: flink-conf.yaml
            path: flink-conf.yaml
          - key: log4j-console.properties
            path: log4j-console.properties

jobmanager-service.yaml

apiVersion: v1
kind: Service
metadata:
  name: flink-jobmanager
spec:
  type: ClusterIP
  ports:
  - name: rpc
    port: 6123
  - name: blob-server
    port: 6124
  - name: webui
    port: 8081
  selector:
    app: flink
    component: jobmanager

flink-configuration-configmap.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: flink-config
  labels:
    app: flink
data:
  flink-conf.yaml: |+
    jobmanager.rpc.address: flink-jobmanager
    taskmanager.numberOfTaskSlots: 2
    blob.server.port: 6124
    jobmanager.rpc.port: 6123
    taskmanager.rpc.port: 6122
    queryable-state.proxy.ports: 6125
    jobmanager.memory.process.size: 1600m
    taskmanager.memory.process.size: 1728m
    parallelism.default: 2    
  log4j-console.properties: |+
    # 如下配置会同时影响用户代码和 Flink 的日志行为
    rootLogger.level = INFO
    rootLogger.appenderRef.console.ref = ConsoleAppender
    rootLogger.appenderRef.rolling.ref = RollingFileAppender
    # 如果你只想改变 Flink 的日志行为则可以取消如下的注释部分
    #logger.flink.name = org.apache.flink
    #logger.flink.level = INFO
    # 下面几行将公共 libraries 或 connectors 的日志级别保持在 INFO 级别。
    # root logger 的配置不会覆盖此处配置。
    # 你必须手动修改这里的日志级别。
    logger.akka.name = akka
    logger.akka.level = INFO
    logger.kafka.name= org.apache.kafka
    logger.kafka.level = INFO
    logger.hadoop.name = org.apache.hadoop
    logger.hadoop.level = INFO
    logger.zookeeper.name = org.apache.zookeeper
    logger.zookeeper.level = INFO
    # 将所有 info 级别的日志输出到 console
    appender.console.name = ConsoleAppender
    appender.console.type = CONSOLE
    appender.console.layout.type = PatternLayout
    appender.console.layout.pattern = %d{yyyy-MM-dd HH:mm:ss,SSS} %-5p %-60c %x - %m%n
    # 将所有 info 级别的日志输出到指定的 rolling file
    appender.rolling.name = RollingFileAppender
    appender.rolling.type = RollingFile
    appender.rolling.append = false
    appender.rolling.fileName = ${sys:log.file}
    appender.rolling.filePattern = ${sys:log.file}.%i
    appender.rolling.layout.type = PatternLayout
    appender.rolling.layout.pattern = %d{yyyy-MM-dd HH:mm:ss,SSS} %-5p %-60c %x - %m%n
    appender.rolling.policies.type = Policies
    appender.rolling.policies.size.type = SizeBasedTriggeringPolicy
    appender.rolling.policies.size.size=100MB
    appender.rolling.strategy.type = DefaultRolloverStrategy
    appender.rolling.strategy.max = 10
    # 关闭 Netty channel handler 中不相关的(错误)警告
    logger.netty.name = org.jboss.netty.channel.DefaultChannelPipeline
    logger.netty.level = OFF