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

5次阅读

共计 4846 个字符,预计需要花费 13 分钟才能阅读完成。

摘要

字节的 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 官网理解详情!欢送关注字节跳动数据平台同名公众号

正文完
 0