前言
Flink任务常见的既支持Standalone独立部署,也支持运行在Yarn、K8s等集群中。Oceanus作为酷家乐一个实时流平台,向业务方提供数据的实时同步功能,在平台初期建设时,采用的是将Flink任务提交到Yarn集群上运行,在建成后的很长一段时间内为公司进行着重要的数据同步工作。在2023年进行了改造,将Oceanus平台的Flink任务从Yarn向K8s进行迁移,最终摆脱了对Yarn的依赖、实现了对Yarn的完全剥离,所有在运行任务安全迁移到K8s上运行。
背景
2023年之前,Flink任务主要运行在Yarn上。主要原因是,在初期Yarn和Flink结合的比较好,Yarn的调度性能表现较好,可以支撑上万节点的调度,而K8s在早期不能支持这么大的节点。另外Yarn可以有效的整合Hadoop生态,方便使用HDFS和Hive。
2023年至今,Flink切换到K8s。主要因为K8s是一个统一的云生态,有丰富的应用,是云生态基础架构发展的大趋势;可以做统一的资源管理、统一的应用管理以及在离线混部等架构规划,为在线业务提供了更好的发布、管理机制,并保证其稳定运行;具有很好的生态优势,能很方便的和各种运维工具集成,如 prometheus 监控,主流的日志采集工具等;在资源弹性方面提供了很好的扩缩容机制,很大程度上提高了资源利用率;最后,K8s有很好的隔离性,能够提供更稳定的生产保障。
K8s相比于Yarn,在扩展性、CSI、CNI、CRI、CRD、调度等许多其他可以扩展的接口上有很大的提升。从生态系统上来讲,K8s依托CNCF社区,专注于云计算领域,而Yarn依托Apache社区,主要是传统大数据领域,因此将Flink任务向K8s迁移是顺应云原生发展的必然动作。
在改造之前先了解Oceanus的任务是怎么跑起来的,这里先简单介绍下Oceanus底层:
核心包含如下这些概念
- Yarn集群,管理整个资源的集群,分两种角色RM和NM,其中RM可以理解为master,负责管理整个集群,NM理解为worker,负责具体的任务
- 队列,整个Yarn集群采用queue的形式进行资源隔离,项目空间与队列一一对应
- Cu:整个Yarn任务内最小的资源单位,一个Cu的大小为1/4 core + 2Gb内存
- Yarn application:一个Yarn的任务,一个Yarn的任务由分布在多个nm上的container组成
- Flink集群 :一个yarn application精确对应一个Flink集群,一个Flink集群由一个job manager(jm)和若干个task manager(tm)组成,每个jm和每个tm均是一个container
- oceanus任务: 一个Flink集群中真正运行的Flink任务,即宽表任务,一个Flink集群内可能会运行多个Flink任务,是否运行多个Flink任务由是否由组提交决定
对应图:
单个任务提交流程:
-
在Yarn的RM上创建单个Flink集群,命令 :
-
得到一个创建好的yarn application,也就是一个Flink集群
-
刚创建好的Flink集群内是空的:
-
提交任务到Flink集群内部运行
-
后端通过一个web.py写的web服务程序与底层进行交互,实现在Yarn上Flink集群的创建和Flink任务的提交、销毁,主要提供以下几个接口:
接口Url 接口用途 执行逻辑 返回结果 POST /jobs/oceanusdev 提交一个Oceanus dev任务指定集群 在这台服务器上执行flink run命令,提交指定的flink任务到指定的flink集群使用jar包是oceanus dev包 flink run命令执行结果 POST /jobs/oceanus 提交一个Oceanus Prod任务到指定集群 在这台服务器上执行flink run命令,提交指定的flink任务到指定的flink集群使用jar包是oceanus prod包 flink run命令执行结果 POST /flinkserver 创建一个新的flink集群 在这台服务器上执行yarn -session命令,创建一个指定的flink集群 yarn session命令的执行结果 DELETE /flinkserver 销毁一个指定的flink集群 在这台服务器上执行yarn-kill命令,销毁指定的flink集群 yarn kill命令的执行结果
改造
想要将Oceanus平台上的Flink任务由Yarn向K8s迁移,要解决几个问题。
- 设计
- 平台对K8s进行支持
- 如何将生产任务无痛迁移到K8s上运行
- 生产环境的高可用性保证
下面分别对解决以上四个问题所做工作进行介绍。
一、设计
目前常用的在K8s中执行Flink任务的方式有四种
- Session 模式
- Per-job 模式
- Native Session 模式
- Native Per-job 模式
这四种部署模式的优缺点可以简单总结为以下表格
| 隔离性 | 作业启动等待时间 | 资源利用率 | 资源按需创建 | 发布版本 | |
|---|---|---|---|---|---|
| Session 模式 | 弱多个作业共享一个集群 | 较短立即启动 | 较低集群长期存在 | 否 | Flink 1.2 |
| Per-job 模式 | 强单个作业独享一个集群 | 最长需要等待Flink集群创建完成 | 一般任务结束后释放资源 | 否 | Flink 1.6 |
| Native Session 模式 | 弱多个作业共享一个集群 | 一般需要等待TaskManager创建完成 | 较好TaskManager按需申请 | 是 | Flink 1.10(Beta) |
| Native Per-job 模式 | 强单个作业独享一个集群 | 一般需要等待Flink集群创建完成Job Graph分析由JM完成,加速任务启动 | 最好Flink资源按需申请 | 是 | Flink 1.11 |
由于Native特性即Flink直接与Kubernetes进行通信并按需申请资源,所以采用Native模式。Oceanus本身支持以任务组的方式进行提交,而Native Per-job模式的隔离性太强即单个作业独享一个集群,这样会使Oceanus不再支持任务组,因此采用Native Per-job 模式。
在资源隔离方面,采用和Yarn集群类似的方式,按项目空间进行资源隔离,项目空间和namespace一一对应
二、平台需要对K8s进行支持
1、api准备
Oceanus通过一个web.py程序进行后端与底层的交互,在平台建设时使用的python写的这个web服务,为了便于维护和后端代码统一,在准备K8s api的时候对web程序进行改写,使用java实现。
对应在Yarn环境上的操作,K8s环境也需要有一套api进行支持,实现在K8s上Flink集群的创建和Flink任务的提交、销毁,主要提供以下几个接口:
| 接口Url | 接口用途 | 执行逻辑 | 返回结果 |
|---|---|---|---|
| POST /flinkserver/k8s | 创建一个新的Flink集群 | 在K8s client上执行"./bin/kubernetes-session.sh"命令启动脚本,创建一个指定的Flink集群 | ./bin/kubernetes-session.sh命令的执行结果 |
| DELETE /flinkserver/k8s/namespace/{namespace}/cluster/{clusterId} | 销毁一个指定的Flink集群 | 在K8s client上执行"kubectl delete deployment xxx -n xxx"命令,销毁指定的deployment | kubectl delete deployment命令的执行结果 |
| POST /jobs/oceanus/k8s | 提交一个Oceanus Prod任务到指定集群 | 在K8s client上执行"flink run"命令,提交指定的Flink任务到指定的Flink集群使用jar包是oceanus.jar | flink run命令执行结果 |
| POST /jobs/oceanusdev/k8s | 提交一个Oceanus dev任务指定集群 | 在K8s client上执行"flink run"命令,提交指定的Flink任务到指定的Flink集群使用jar包是oceanus-dev.jar包 | flink run命令执行结果 |
| Post /flinkserver/k8s/clusterid | 获取Flink集群在K8s上真正的nodeIp | 在K8s client上执行"kubectl get pods -o wide"命令,获取jm pod的详细信息 | kubectl get pods -o wide命令执行结果 |
2、集群资源准备
(1)K8s机器准备
对于不同环境的Oceanus任务,所提交的K8s集群服务器需要进行隔离,dev环境任务提交到下沙集群,这部分机器复用原Yarn集群dev任务所在机器,prod环境任务提交到腾讯云K8s,由运维进行维护。
(2)客户端准备
a. 部署kubectl
- 下载kubectl 或 从已部署了kubectl的机器上,将 /usr/bin/kubectl 文件拷贝到待安装的机器上
curl -LO https://storage.googleapis.com/kubernetes-release/release/v1.18.6/bin/linux/amd64/kubectl -o /usr/bin/kubectl --proxy "http://xx.xx.xx.xx:xxxx" |
|---|
- kubectl文件增加执行权限
chmod +x /usr/bin/kubectl |
|---|
- 配置Kubernetes集群配置文件
config配置文件添加 $HOME/.kube目录,config文件向运维申请
$HOME/.kube/config |
|---|
- 测试kubectl是否安装成功
kubectl get pod -n ${namespace} |
|---|
b. 安装flink
以安装flink-1.16.1版本为例
- 上传flink文件
cd /opt``rz (上传flink文件)``ln -s flink-``1.16``.``1 flink |
|---|
- 准备flink-conf.yaml文件和pod-template.yml文件
配置文件按照生产任务所需进行编辑
-
上传Oceanus所需的lib
c. 部署Oceanus资源
-
部署任务提交脚本submit.py
- 通过堡垒机进入对应机器,切换对应用户进入脚本文件目录
- 通过堡垒机进入对应机器,切换对应用户进入脚本文件目录
-
- ps -ef | grep oceanusSubmit-2.0.0.jar,查看进程是否存活;如果进程不存在,需要部署进程(含端口号)
- ps -ef | grep oceanusSubmit-2.0.0.jar,查看进程是否存活;如果进程不存在,需要部署进程(含端口号)
-
- source /etc/profile 激活FLINK_HOME变量
- source /etc/profile 激活FLINK_HOME变量
-
- nohup java -jar oceanusSubmit-2.0.0.jar --server.port=12071 & 部署进程
- nohup java -jar oceanusSubmit-2.0.0.jar --server.port=12071 & 部署进程
-
- 修改脚本文件 若要修改oceanusSubmit-2.0.0.jar文件,改完后需要先杀掉原进程kill -9 34219,然后再重新部署进程
-
上传Oceanus底层jar包
(3)namespace准备
Oceanus以业务线为单位,每个业务线创建一个Namespace,并需要给对应的帐号创建权限和quota(request和limit)。
- 申请创建namespace并申请权限获取kubeconfig,按照业务线项目空间进行申请
- 修改Limit配额,并增加Request配额:kubectl edit resoucequota -n ${namespace} {namespace}
apiVersion: v1
kind: ResourceQuota
metadata:
name: oceanus-tx-bigdata-prod
namespace: oceanus-tx-bigdata-prod
spec:
hard:
limits.cpu: "200"
limits.memory: 200Gi
requests.cpu: "60"
- 在各个Namespace下创建oceanus ServiceAccount并授权,执行以下命令:
kubectl get ns | grep oceanus | awk '{print $1}' | xargs -n 1 sh -c ' kubectl create sa oceanus -n $1' argv0
kubectl get ns | grep oceanus | awk '{print $1}' | xargs -n 1 sh -c ' kubectl create rolebinding oceanus-role-binding-flink --clusterrole=edit --serviceaccount=$1:oceanus -n $1' argv0
- 给Captain申请的帐号授权,执行以下命令:
kubectl get ns | grep oceanus | awk '{print $1}' | xargs -n 1 sh -c ' kubectl create rolebinding captain-oceanus-role-binding --clusterrole=edit --serviceaccount=auto-kube-sa:captain-auto-oceanus -n $1' argv0
三、生产环境的高可用性保障
为了保证任务迁移后的高可用性,对平台的故障恢复能力进行优化。
原高可用逻辑中,通过一个定时检测任务状态的定时任务,每隔2分钟向Yarn/K8s集群同步job_commit表中正在运行的任务 在Yarn/K8s集群上最新的Flink job运行状态,如果任务的状态为失败或者在集群中找不到相应Flink任务,并且判断任务是高可用任务,则将这些任务放到高可用重启队列中进行自动重提交,但是高可用重启队列为单线程从队列中拿任务进行提交,当遇到集群故障等导致大批任务失败需要重启的情况出现时,队列中会长时间排队拥堵,导致任务恢复缓慢,并且队列没有优先级,无法使程序自动化保证p0p1任务的优先启动。
优化后,对高可用重启任务,定时任务会每2分钟将数据库中的集群(Yarn/K8s)信息读出,并向对应集群同步Flink job状态,然后获取数据库中的在运行任务提交记录,遍历这些提交记录以对齐这些任务的Flink job状态;根据不同的job状态,将这些任务信息放进不同的List中,其中updatedJobs集合放入能查到的所有任务的信息,alarmedJobs集合放入Flink job状态为失败的任务信息,jobsOfNotFoundFromOpsCluster集合放入由于yarnApplication/k8sCluster被kill而无法获取Flink job状态的任务信息,finishedJobs集合放入Flink job装态为结束的任务信息(表示全量同步任务结束);接下来需要根据Flink job状态更新数据库中的任务状态,jobsOfNotFoundFromOpsCluster集合中的任务状态置为failed,updatedJobs集合中的任务状态根据Flink job状态进行更新,finishedJobs集合中的任务需要通知用户任务完成,alarmedJobs集合中的任务需要发送任务失败报警,并且如果是高可用任务(prod任务、手动设置高可用的任务),将高可用任务放入任务高可用重启队列和任务高可用重启检查队列,多线程重启线程不间断地从高可用重启队列中take任务进行重启,从高可用重启检查队列中检测在规定重启时间内任务是否重启成功。对手动降级重启任务,将任务放入降级重启队列中,多线程不间断地从降级重启队列中take任务进行重启。采用的优先级队列对高P任务进行排队前置,优先保障高P任务重启。
经过重新设计,在批量任务需要恢复时,大大缩短了批量故障恢复时间,优先保证了高P任务的恢复,降低线上影响。
四、如何将生产任务无痛迁移到K8s上运行
Oceanus在运行生产任务数量有1000+,全部运行在腾讯云EMR的Yarn集群上,其中还有一些对线上影响重大的p0p1任务,当迁移的准备工作全部就绪后,就需要将这些任务往K8s进行迁移。确保任务的无痛迁移也是一个重要工作,需要尽可能减少对线上产生的影响。
首先,综合考虑数据同步的流量低峰期以及业务线上访问流量低峰期,迁移时间必须选在夜间;
其次,由于任务数量众多,需要对任务进行多次分批的迁移,特别是p0p1任务在最后一批迁移,这样做的目的是为了防止迁移过程中的未知错误对大量任务产生影响,同时减轻K8s集群的压力;
最后,加强任务监控能力,通过多维度、多渠道对任务、集群进行监控告警:
- Flink自身指标,比如Flink延时指标、lag指标、checkpoint指标等;
- 性能指标,GC时间、CPU 使用率,如果指标异常,需要用户进行资源调整;
- 反压率,在流式计算中出现背压,说明作业处理有问题;
- 集群资源监控
- 接入Tetris,提供用户自定义告警
考虑到Oceanus已经拥有的手动批量降级重启能力,在手动降级重启时可以指定提交的任务运维集群,最终确定对迁移任务采用批量降级重启的方式在夜间流量低峰期实现从Yarn集群到K8s集群的迁移。
资源收益
通过将Oceanus上1000+的线上任务迁移K8s,后续在K8s上优化任务性能,降低整体的内存消耗从2.3T到1.7T, 同时节省大量资金成本 , 改造的Oceanus的高可用机制,提升任务高可用重启速度十倍以上。