云原生大数据是大数据平台新一代架构和运行形态。随着字节跳动内部业务的快速增长,传统大数据运维平台的劣势开始逐渐暴露,如组件繁多,安装运维复杂,与底层环境过度耦合;对业务方来说缺少开箱即用的日志、监控、告警功能等。在此背景下,我们进行了一系列云原生大数据运维管理实践。通过云原生的方式进行运维管理,最终达到弱化业务方对状态的感知,屏蔽环境的差异,统一不同环境下的使用体验。
作者|字节跳动资深研发工程师-罗来锋
业务现状与背景介绍
背景介绍
字节跳动过去几年在支撑自身业务的过程中积累了很多大数据领域的引擎工具,目前也在探索将这些引擎工具的能力进行标准化、产品化的输出。在此过程中主要有以下几个难点:
- 组件繁多:大数据领域完成一项工作需要很多组件配合。比如分布式大数据存储及各种任务执行引擎:Flink、Spark 及各种 ETL 的 OLAP 工具和调度 ETL 的任务调度工具,还有支撑工具引擎的运行日志监控系统和项目用户权限的辅助系统等;
- 部署复杂:这些系统的组件繁多,相互配合也非常复杂,导致部署变得困难。比如部署一套完整的生产环境,可能会涉及到多个依赖和配置管理。有强依赖,比如各种任务引擎对底层大数据存储的依赖;也有弱依赖,比如任务引擎对日志监控系统的依赖;甚至还有循环依赖,比如消息中间件可能需要采集日志,但日志采集本身又依赖消息中间件,另外它们的配置还会形成相互嵌套;
- 环境耦合:比如任务执行引擎可能需要嵌套大数据存储配置,日志采集可能需要感知每个组件的目录以及它的格式等。部署复杂就会造成环境的耦合,因为日常需要维护这些复杂的配置及依赖等,日积月累下就会与这套环境形成了一个深度耦合造成移植困难。
随着近几年云原生概念的兴起,我们也尝试将这些工具进行云原生改造来解决以上问题。
云原生场景特性
- 无服务状态感知: 用户可以使用功能而不需要关注背后的运行状态,也不需要关心背后的逻辑;
- 极致弹性伸缩:对用户隐藏运行状态后,在云原生场景下的伸缩更为极致,按需使用可以使成本降低显著;
- 快速故障转移:当故障发生时借助极致的弹性伸缩特性,可以快速下线故障节点,补充新的正常节点,从而实现快速故障转移,并且这个故障转移对用户来说也是无感无损的动作。
以上这三个特性会相互促进,形成一个良性的循环。
云原生演进方向
对于上述所说的云原生化改造,主要归纳总结了以下几个大的演进方向:
- 组件微服务化:通过将整体服务按职责划分成多个小的组件,在整体架构上更加高内聚低耦合,降低整个环境变更复杂度,更加方便大规模合作开发;
- 应用容器化:容器提供了可移植性,可以保证环境间的一致性;
- 基础设施不可变:通过将所有内容进行封装,从而实现底层基础设施的隔离,进而保证基础设施不可变,可以带来部署的一致性、可靠性和简单性,对环境的状态也更加可控;
- 声明式API:通过声明式 API,用户只需要声明自己想要达到的状态,后端服务尽力去满足,使用户无需感知具体过程,整体环境更加稳定,而且功能的变更与演进也会更简单,同时也简化了使用门槛。
架构演进
云原生大数据简介
云原生大数据主要是构建在容器上的,这里的容器可以是公有云的容器服务,也可以是私有云的容器底座,私有云的容器底座可以是开源的 K8s/基于 K8s 改造的底座,整个云原生大数据可以分为三大平台和一大支撑体系,三大平台分别是调度层、引擎层和平台层。在容器之上是资源调度层,负责统一管理调度整个集群的计算、存储和网络等资源。调度层上面的核心引擎层主要是是字节自研的统一大数据存储系统,兼容 HDFS 语义的同时支持对接标准的 S3 对象存储。存储层的上一层是 Flink、Spark 等各类字节自研或优化的计算引擎、消息中间件、日志搜索及实时分析引擎等工具。最上面即是平台服务层,负责将这些引擎能力封装整合成一个对外输出的产品。
本次介绍的运维管理平台支撑了上述的三大平台,提供日常组件运维的管理功能,为了更好地适应整个大数据云原生的改造,我们对运维管理模块也做了云原生的改进。
云原生上的运维实践
- 资源占用率低:运维管理模块不是面向用户的产品核心功能,所以它的存在感要足够低,资源占比要足够小,甚至在一些小型场景下要可以被忽略不计;
- 伸缩性强:在日常的运维管理中,因为日志监控跟集群的规模是呈正相关的,那么所有运维管理相关的功能要有跟环境进行快速水平伸缩的能力;
- 稳定性高:运维管理对稳定性的要求很高,即使在发生故障的情况下,也要能够快速恢复,并且也需要对其他组件提供容灾管理的能力;
- 可移植性强: 运维管理模块的一个大目标就是支持整个云原生大数据产品的快速移植,那么就要求它本身就不能有环境耦合的问题,并且所有的相关功能都需要支持插拔式设计,可以灵活的在不同环境提供一套完整的运维管理功能;
- 环境感知弱:向上层业务屏蔽由于环境差异带来的运维管理的差异性,保证上层业务可以用统一的方式使用不同环境上的运维管理功能。
所以为了满足以上要求总结了接下来需要关注的几个方向。在环境管理方面需要我们抽象出一套统一的环境模型去适应不同的部署;另外还要有一个灵活便捷的组件管理服务统一管理组件元数据的依赖、配置等信息;最后还需要拥有功能抽象的能力,比如对常见的日志、监控、告警等功能可以通过抽象统一对上层业务屏蔽环境差异性。
环境管理与组件服务
环境管理
可以将整个环境按照功能划分成三个逻辑区域,分别是控制面、系统面和数据面,需要注意的是这三块区域只是逻辑区域的划分,并不是物理环境上的隔离。比如在一些场景下控制面可以与系统面进行合并,甚至在一些小型场景下,三个面也可以合并在一个物理集群内。
-
控制面:用来提供弱业务承载,是全局唯一一个负责环境管控、成本核算及服务网关等支撑性的工作;
-
系统面:在每个逻辑单元内是唯一的,但是整个系统中可以有多个逻辑单元,比如在同城多活/异地多活的场景下,每一个逻辑单元都可能对应着一个机房/一个区域,多个逻辑单元间的关系依靠控制面协调;
-
数据面:用于提供引擎运行所需的计算、存储、网络等资源,并且在系统面的统一协调下多个数据面可以形成一个逻辑上的联邦集群。
组件服务
通过对环境的区域划分对组件进行层级的划分,主要可以分成系统级、集群级、租户级和项目级。系统级负责大部分的业务管控逻辑;集群级则主要完成日志数据/监控数据 Agent 和内部自研的调度器及 Operator 等的采集工作;租户级主要用于支撑特定大用户独占的组件;最下层的项目级就是用户的作业实例、中间件实例及其他第三方工具等。通过这里的划分就把整个部署划分为了网格形式,使每个组件只需要关注自己所在的网格,很好的屏蔽了组件与环境信息的耦合。
组件****服务: Helm 定制化改进
K8s 对单个资源的支持十分友好,对特定领域的操作也十分丰富。但是简单的服务也需要多个资源的配合,比如 Deployment 承载业务逻辑就需要 ConfigMap 去保存它的配置,然后又为了方便地对外暴露服务需要通过 Service 统一访问入口,但是这里的资源协调在 K8s 中并没有提供很好的工具。在开源的解决方案中很多开源组件基本上都提供了迁移 K8s 的 Helm Chart,但为了更好地融入开源的生态体系,我们也基于 Helm 构建了自己的组件服务。
由于开源 Helm 命令行工具并不适用于云原生场景下组件间的 API 调用,所以我们对开源 Helm 进行了深度服务化定制,在常见的部署、卸载、升级、回滚等需求中通过 API 的方式进行对外暴露,并增加可视化界面,同时还支持了一些深度的仿真部署,让用户可以快速的进行部署、验证、调试等工作,也对层级配置做了精细的划分使组件在部署时可以进行合并覆盖,另外在组件部署时配置了对资源的动态修改,通过以上措施使上层的业务组件可以更加关注在自己的业务领域。
磁盘管理
原生的 K8s 对无状态的负载支持是十分友好的,但对有状态的负载支持就有点差强人意,主要体现在本地磁盘的使用上,我们分析总结了以下痛点:
- 环境耦合:在 K8s 使用本地磁盘时需要提前感知到本地磁盘的挂载点、类型、大小等,会造成一定的耦合现象;
- 利用率低:缺少全局统一进行存储调度和管理的组件,导致组件与组件间无法形成高效的混部,使得磁盘整体利用率偏低;
- 隔离性差:磁盘以整盘的方式使用会导致利用率低,但如果不以整盘的方式使用,又会使组件与组件间缺乏隔离性;
- 维护难度大:在业务运行期间由于组件对磁盘的动态需求往往需要做扩缩容调整,但是提前协调各个使用方的操作使时间跨度大、链路长、维护难度高。
统一调度
为此我们开发了一套统一的 CSI(容器存储接口)来用于管理,不仅能够统一采集集群的所有磁盘信息,也可以进行统一管理。在此基础上我们将整个磁盘的使用场景分成了三类,分别是共享容量卷、共享磁盘卷和独占磁盘卷。
共享容量卷即容量是共享的,这类场景对 IO 不敏感,也不需要很强的空间容量的限制,但对于灵活性要求更高,比如典型的大数据作业的临时数据存储、日志等;
共享磁盘卷对 IO 也不是很敏感,但对隔离性、持久化有一定的需求,需要在出现故障时能够找回,但是找不回的情况也不会产生灾难性的后果,其中最典型的场景就是缓存;
独占磁盘卷需要高度的 IO 隔离特性,典型的场景如消息中间件 Kafka、HDFS 等。
磁盘管理概览
在磁盘管理中将其分成两大块区域,第一块区域是 K8s 维护的,比如常用的 EmptyDir,这个部分推荐用来存储配置数据或者少量的临时性数据。
剩下的区域就是上文提到的通过 CSI 进行统一管理,主要细化成了三块区域,分别对应的前面提到的三个存储卷,共享容量卷基于简单的本地路径的方式进行支持;对于共享磁盘卷会先会把所有的磁盘组装组合成一个 Volume Group,当业务组件申请共享磁盘卷时可以创建一个逻辑卷使用,从而达到隔离的效果。
独占磁盘卷就是拥有整块磁盘,然后通过统一的 CSI 抽象成一系列的 Storage Class,上层的业务组件可以根据自己的需求申请对应的存储卷。如果是公有云云盘或者有中心化存储的场景下,仍然推荐通过这套 CSI 给业务提供各类的存储卷,以达到容量管控的同时也可以通过这个 CSI 将磁盘信息与组件解耦。
统一的日志监控告警
日志
日志也是产生可移植性困难较大的一个因素,为此我们也做了统一的日志采集的链路管理,以达到业务隔离、高效采集、公平分配、安全可靠。
对于日志采集目前支持两种方式,一种是侵入式采集,即提供各种 Collector,主要支持 Java 、Python 两种方式,由于这种方式具有侵入性,大部分组件习惯使用基于文件的采集,因此我们也通过 Filebeat 支持文件方式的采集。采集后汇聚到本集群的日志代理上进行流量管控,后续再汇聚到统一的中心化存储中用统一的 API 支撑日志搜索场景。也对定制化引擎提供了针对性的 API使用户可以根据具体场景使用对应的 API。
第二种是 Filebeat 采集,在容器场景下是一个基于文件的采集,和基于物理机的采集不同,主要区别在容器视角,日志存储路径与实际的物理存储路径也是不一样的。为解决这个问题首先通过定制的日志规则 CRD 声明自己的采集规则,然后再通过组件部署服务,随着整个组件的创建、更新,Filebeat 的 Discovery 机制可以动态地发现日志规则的 CRD 创建、变更或删除,再通过 Filebeat 热加载的机制生成、加载成自己的日志采集规则。
在 Filebeat 的部署形态中如果能够感知到集群的节点信息并拥有对应的权限,那么就可以部署成 DeamonSet 的方式,使整体资源占比更低,集群是共用一套 Filebeat 进行数据采集。由于在公共云的容器服务场景下是感知不到具体的节点信息的,也没有部署 DeamonSet 的权限,所以这里支持通过 Sidecar 的方式注入到具体的一个个 Pod 中负责日志采集,通过这种方式我们就统一了在容器里基于文件的日志采集。
日志数据链路
在云原生的场景下,日志采集远远不只是统一采集链路,而是要用尽可能低的资源消耗支撑日志的高效采集需求。因为云原生场景天然地面向多租户,那么租户与租户间,组件与组件间的流量差异会很大,不能因为单个租户不正常的流量对整个日志采集造成扰动。所以我们在每个集群内部的日志代理中,会针对租户做流量管控,当发现大流量异常的时采取限流或者熔断措施。同时也要保障多租户场景下的公平分配、日志采集的故障转移、云原生场景下 Pod 重建/主动升级等,这几个部分都是后续将主要投入的大方向。
告警
告警体系整体是基于开源的夜莺改造而成的,在开源夜莺的概览中包括存储指标数据的 Prometheus、存储告警业务数据的数据库及核心组件: WebApi 和 Server。 WebApi 用于承担用户的交互,比如规则的增删改查及执行指标查询等。Server 负责加载规则、生成告警事件、发送告警通知等。在开源夜莺中,Server 还承担着 Prometheus 的 PushGateway 职责,字节的产品有自己的用户体系和监控系统,所以对告警方面的定制主要集中在 WebApi 和 Server 上。
流程概览
用户首先通过 WebAPI 生成自己的告警规则,并持久化到数据库中, Server 再加载规则到自己的内存中,通过一致性哈希环决定处理哪些规则并转换成指标查询判断是否有告警事件产生。当有告警事件产生时会调用对应的控制模块发送告警通知,将告警事件回填到的数据库中,主要优化体现在以下方面:
- 首先,我们的产品体系中有统一的用户体系,和运维管理平台一样,并在此基础上增加了用户组、值班表功能,使它更加符合告警领域的使用习惯;
- 第二,开源夜莺加载日志规则是通过全量的方式,但是会有潜在的性能隐患,我们通过将全量加载改造成增量加载,进一步消除相关性能隐患;
- 第三,告警通知模块,与环境有强耦合关系。因为不同环境的告警通知的差异会很大,比如钉钉、企业微信、飞书,还有短信电话等等。即使是同样的短信告警,不同的环境可能有不同的短信提供商,有不同的对接接口等,所以我们提供了动态消息模版的能力。
动态消息模板
- 通过动态消息模板可以引用告警事件的一些信息,进而可以组装出带有丰富上下文的告警信息,使告警系统的灵活性更广,体验感也更好。
- 在通知方式上设计将其做成一个个插件,用户只需要针对不同的环境开发不同的发送插件就可以了,这种操作也可以使我们的核心流程保持一致。
通知模块
在通知模块中通过 Server 生成告警事件,再由前面的消息模板渲染得到一个真实的告警消息,然后将这个告警消息发送给通知模块,由通知模块结合通知方式及对象生成通知记录并放到队列中。为了更好地适应各种环境,这里的队列可以是真实的消息队列,也可以是通过数据库模拟的消息队列。最后由若干个 Worker 并发消费信息调用不同的发送插件发送消息;Worker 之外还有一些定时的线程轮询/巡检整体发送的状态对发送失败的消息进行重试,当重试次数达到一定量的时候生成运维上的告警。
开源夜莺系统还有一个比较大的特性是动态阈值,在整体的使用中就是事件发生、触发告警、然后人工反馈到训练分析形成循环监督学习的过程,并不断调整动态阈值的生成规则。
监控
在整体的监控架构概览中可以看到上层就是前面所说的数据面,数据面中的每个集群都会有一个Prometheus用来采集、汇聚本物理集群所有组件的监控数据,这里的 Prometheus 本质是一个 Agent,不会承担任何数据存储、查询等职责,最后由 Prometheus 把采集到的监控数据 Remote Write 到系统面的监控系统中。
监控系统用来保存所有的监控数据。为了方便对数据存储进行水平伸缩也做了一层抽象,背后的真正实现可以是公有云上现有的云监控服务、也可以是 S3 的对象存储服务,还可以是我们自研的大数据存储服务,在部分私有云的场景下,也可以对接用户自己自定义的存储服务。在本层的统一存储中通过加一个 Query 服务承担所有监控数据的查询服务,并形成了可视化的大盘展示及前端交互。
对于查询服务也做了一些针对性的优化。比如监控数据只有新增、没有更新,频繁对某个时间段的监控数据进行反复检索的场景引入了水平拆分的能力,通过将时间跨度大的查询拆分成多个小跨度的子查询并发执行,然后聚合回忆查询速度。另外由于监控数据本身不可变,我们引入了缓存,可以对部分查询出的数据做 一个缓存加上查询场景方便预测。通过以上两个优化相互促进提高了整体的查询效率。
经过整体优化后的监控系统核心优势是可以支持各种环境下的一键指标采集;在性能优化上支持了数据的预聚合、降采样等能力,丰富了整体的功能体系;并且对接了大数据存储,某种程度上可以具备存算分离的特性使得整体系统的水平伸缩能力得到了很大的增强;最后也把监控和其它的运维工具,比如日志、告警、链路追踪等功能做了深度整合,优化了产品的整体使用体验。