乐趣区

关于性能优化:以一次Data-Catalog架构升级为例聊业务系统的性能优化

摘要

字节的 DataCatalog 零碎,在 2021 年进行过大规模重构,新版本的存储层基于 Apache Atlas 实现。迁徙过程中,咱们遇到了比拟多的性能问题。本文以 Data Catalog 系统升级过程为例,与大家探讨业务零碎性能优化方面的思考,也会介绍咱们对于 Apache Atlas 相干的性能优化。

背景

字节跳动 Data Catalog 产品晚期,是基于 LinkedIn Wherehows 进行二次革新,产品晚期只反对 Hive 一种数据源。后续为了反对业务倒退,做了很多修修补补的工作,零碎的可维护性和扩展性变得不可忍耐。比方为了反对数据血统能力,引入了字节外部的图数据库 veGraph,写入时,须要业务层解决 MySQL、ElasticSearch 和 veGraph 三种存储,模型也须要同时了解关系型和图两种。更多的背景能够参照之前的文章。

新版本保留了原有版本全量的产品能力,将存储层替换成了 Apache Atlas。然而,当咱们把存量数据导入到新零碎时,许多接口的读写性能都有重大降落,服务器资源的应用也被拉伸到夸大的境地,比方:

  • 写入一张超过 3000 列的 Hive 表元数据时,会继续将服务节点的 CPU 占用率晋升到 100%,十几分钟后触发超时
  • 一张几十列的埋点表,上下游很多,关上详情展现时须要等 1 分钟以上
    为此,咱们进行了一系列的性能调优,联合 Data Catlog 产品的特点,调整了 Apache Atlas 以及底层 Janusgraph 的实现或配置,并对优化性能的方法论做了一些总结。

业务系统优化的整体思路

在开始探讨更多细节之前,先概要介绍下咱们做业务类系统优化的思路。本文中的业务零碎,是绝对于引擎零碎的概念,特指解决某些业务场景,给用户间接裸露前端应用的 Web 类零碎。

优化之前,首先应明确优化指标。与引擎类零碎不同,业务类零碎不会谋求极致的性能体验,更多是以解决理论的业务场景和问题登程,做针对性的调优,须要分外留神防止过早优化与适度优化。

精确定位到瓶颈,能力事倍功半。一套业务零碎中,能够优化的点通常有很多,从业务流程梳理到底层组件的性能晋升,然而对瓶颈处优化,才是 ROI 最高的。

依据问题类型,挑性价比最高的解决方案。解决一个问题,通常会有很多种不同的计划,就像条条大路通罗马,但在理论工作中,咱们通常不会谋求最完满的计划,而是选用性价比最高的。

优化的成果得能疾速失去验证。性能调优具备肯定的不确定性,当咱们做了某种优化策略后,通常不能上线察看成果,须要一种更麻利的验证形式,能力确保及时发现策略的有效性,并及时做相应的调整。

业务系统优化的细节

优化指标的确定

在业务零碎中做优化时,比拟禁忌两件事件:

  • 过早优化:在一些性能、实现、依赖零碎、部署环境还没有稳固时,过早的投入优化代码或者设计,在后续零碎产生变更时,可能会造成精力节约。
  • 适度优化:与引擎类零碎不同,业务零碎通常不须要跑分或者与其余零碎产出性能比照报表,理论工作中更多的是贴合业务场景做优化。比方用户间接拜访前端界面的零碎,通常不须要将响应工夫优化到 ms 以下,几十毫秒和几百毫秒,曾经是满足要求的了。

    优化范畴抉择

    对于一个业务类 Web 服务来说,特地是重构阶段,优化范畴比拟容易圈定,次要是找出与之前零碎相比,显著变慢的那局部 API,比方能够通过以下形式收集须要优化的局部:

  • 通过前端的慢查问捕获工具或者后端的监控零碎,筛选出 P90 大于 2s 的 API
  • 页面测试过程中,研发和测试同学陆续反馈的 API
  • 数据导入过程中,研发发现的写入慢的 API 等

    优化指标确立

    针对不同的业务性能和场景,定义尽可能粗疏的优化指标,以 Data Catalog 零碎为例:

    定位性能瓶颈伎俩

    零碎简单到肯定水平时,一次简略的接口调用,都可能牵扯出底层宽泛的调用,在优化某个具体的 API 时,如何精确找出造成性能问题的瓶颈,是后续其余步骤的要害。上面的表格是咱们总结的罕用瓶颈排查伎俩。

    优化策略

    在找到某个接口的性能瓶颈后,下一步是着手解决。同一个问题,修复的伎俩可能有多种,理论工作中,咱们优先思考性价比高的,也就是实现简略且有明确成果。

    疾速验证

    优化的过程通常须要一直的尝试,所以疾速验证特地要害,间接影响优化的效率。

    Data Catalog 系统优化举例

    在咱们降级字节 Data Catalog 零碎的过程中,宽泛应用了上文中介绍的各种技巧。本章节,咱们筛选一些较典型的案例,具体介绍优化的过程。

调节 JanusGraph 配置

实际中,咱们发现以下两个参数对于 JanusGraph 的查问性能有比拟大的影响:

  • query.batch = ture
  • query.batch-property-prefetch=true
    其中,对于第二个配置项的细节,能够参照咱们之前公布的文章。这里重点讲一下第一个配置。

JanusGraph 做查问的行为,有两种形式:

针对字节外部的利用场景,元数据间的关系较多,且元数据结构简单,大部分查问都会触发较多的节点拜访,咱们将 query.batch 设置成 true 时,整体的成果更好。

调整 Gremlin 语句缩小计算和 IO

一个比拟典型的利用场景,是对通过关系拉取的其余节点,依据某种属性做 Count。在咱们的零碎中,有一个叫“BusinessDomain”的标签类型,产品上,须要获取与某个此类标签相关联的元数据类型,以及每种类型的数量,返回相似上面的构造体:

{
                "guid": "XXXXXX",
                "typeName": "BusinessDomain",
                "attributes": {
                    "nameCN": "直播",
                    "nameEN": null,
                    "creator": "XXXX",
                    "department": "XXXX",
                    "description": "直播业务标签"
                },
                "statistics": [
                    {
                        "typeName": "ClickhouseTable",
                        "count": 68
                    },
                    {
                        "typeName": "HiveTable",
                        "count": 601
                    }
                ]
            }

咱们的初始实现转化为 Gremlin 语句后,如下所示,耗时 2~3s:

g.V().has('__typeName', 'BusinessDomain')
    .has('__qualifiedName', eq('XXXX'))
    .out('r:DataStoreBusinessDomainRelationship')
    .groupCount().by('__typeName')
    .profile();

优化后的 Gremlin 如下,耗时~50ms:

g.V().has('__typeName', 'BusinessDomain')
    .has('__qualifiedName', eq('XXXX'))
    .out('r:DataStoreBusinessDomainRelationship')
    .values('__typeName').groupCount().by()
    .profile();

Atlas 中依据 Guid 拉取数据计算逻辑调整

对于详情展现等场景,会依据 Guid 拉取与实体相干的数据。咱们优化了局部 EntityGraphRetriever 中的实现,比方:

  • mapVertexToAtlasEntity 中,批改边遍历的读数据形式,调整为以点以及点上的属性过滤拉取,触发 multiPreFetch 优化。
  • 反对依据边类型拉取数据,在应用层依据不同的场景,指定不同的边类型汇合,做数据的裁剪。最典型的利用是,在详情展现页面,去掉对血缘关系的拉取。
  • 限度关系拉取的深度,在咱们的业务中,大部分关系只须要拉取一层,个别的须要一次性拉取两层,所以咱们接口实现上,反对传入拉取关系的深度,默认一层。
    配合其余的批改,对于被宽泛援用的埋点表,读取的耗时从~1min 降落为 1s 以内。

对大量节点顺次获取信息加并行处理

在血统相干接口中,有个场景是须要依据血缘关系,拉取某个元数据的上下游 N 层元数据,新拉取出的元数据,须要额定再查问一次,做属性的裁减。

咱们采纳减少并行的形式优化,简略来说:

  • 设置一个 Core 线程较少,但 Max 线程数较多的线程池:须要拉取全量上下游的状况是多数,大部分状况下几个 Core 线程就够用,对于多数状况,再启用额定的线程。
  • 在批量拉取某一层的元数据后,将每个新拉取的元数据顶点退出到一个线程中,在线程中独自做属性裁减
  • 期待所有的线程返回
    对于关系较多的元数据,优化成果能够从分钟级到秒级。

对于写入瓶颈的优化

字节的数仓中有局部大宽表,列数超过 3000。对于这类元数据,初始的版本简直没法胜利写入,耗时也常常超过 15 min,CPU 的利用率会飙升到 100%。

定位写入的瓶颈

咱们将线上的一台机器从 LoadBalance 中移除,并结构了一个领有超过 3000 个列的元数据写入申请,应用 Arthas 的 itemer 做 Profile,失去下图:

从上图可知,总体 70% 左右的工夫,破费在 createOrUpdate 中援用的 addProperty 函数。

耗时剖析

  • JanusGraph 在写入一个 property 的时候,会先找到跟这个 property 相干的组合索引,而后从中筛选出 Coordinality 为“Single”的索引
  • 在写入之前,会 check 这些为 Single 的索引是否曾经含有了以后要写入的 propertyValue
  • 组合索引在 JanusGraph 中的存储格局为:
  • Atlas 默认创立的“guid”属性被标记为 globalUnique,他所对应的组合索引是__guid。
  • 对于其余在类型定义文件中被申明为“Unique”的属性,比方咱们业务语义上全局惟一的“qualifiedName”,Atlas 会了解为“perTypeUnique”,对于这个 Property 自身,如果也须要建索引,会建出一个 coordinity 是 set 的齐全索引,为“propertyName+typeName”生成一个惟一的齐全索引
  • 在调用“addProperty”时,会首先依据属性的类型定义,查找“Unique”的索引。针对“globalUnique”的属性,比方“guid”,返回的是“__guid”;针对“perTypeUnique”的属性,比方“qualifiedName”,返回的是“propertyName+typeName”的组合索引。

  • 针对惟一索引,会尝试查看“Unique”属性是否曾经存在了。办法是拼接一个查问语句,而后到图里查问
  • 在咱们的设计中,写入表的场景,每一列都有被标记为惟一的“guid”和“qualifiedName”,“guid”会作为全局惟一来查问对应的齐全索引,“qualifiedName”会作为“perTypeUnique”的查问“propertyName+typeName”的组合齐全索引,且整个过程是程序的,因而当写入列很多、属性很多、关系很多时,总体上比拟耗时。

    优化思路

    对于“guid”,其实在创立时曾经依据“guid”的生成规定保障了全局唯一性,简直不可能有抵触,所以咱们能够思考去掉写入时对“guid”的唯一性查看,节俭了一半工夫。
    对于“qualifiedName”,依据业务的生成规定,也是“globalUnique”的,与“perTypeUnique”的性能差异简直是一倍:
    ](/img/bVc0cRh)

    优化实现成果

  • 去除 Atlas 中对于“guid”的唯一性的查看。
  • 增加“Global_Unqiue”配置项,在类型定义时应用,在初始化时对“__qualifiedName”建设全局惟一索引。
  • 配合其余优化伎俩,对于超多属性与关系的 Entity 写入,耗时能够升高为分钟级。

    总结

  • 业务类零碎的性能优化,通常会以解决某个具体的业务场景为指标,从接口动手,逐层解决
  • 性能优化根本遵循思路:发现问题 -> 定位问题 -> 解决问题 -> 验证成果 -> 总结晋升
  • 优先思考“巧”方法,“土”方法,比方加机器改参数,不为了谋求高大上而走弯路

欢送跳转火山引擎大数据研发治理套件 DataLeap 官网理解详情!欢送关注字节跳动数据平台同名公众号

退出移动版