当前位置: 首页 > 科技观察

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

时间:2023-03-22 12:00:31 科技观察

以DataCatalog架构升级为例谈业务系统的性能优化在迁移过程中,我们遇到了很多性能问题。本文以DataCatalog系统的升级过程为例,与大家一起探讨我们对业务系统性能优化的思考,同时也介绍一下我们对ApacheAtlas相关的性能优化。背景字节跳动DataCatalog产品早期是基于LinkedInWherehows进行重构的。早期产品只支持Hive作为数据源。为了支撑业务发展,后续做了大量的修补工作,系统的可维护性和可扩展性变得不堪重负。例如,为了支持数据沿袭能力,引入了Byte内部的图数据库veGraph。写的时候业务层需要处理MySQL、ElasticSearch和veGraph三种存储,模型也需要同时理解关系型和图型。更多背景,请参考上一篇文章。新版本保留了原版本的所有产品能力,并将存储层替换为ApacheAtlas。然而,当我们将股票数据导入新系统时,很多接口的读写性能严重下降,服务器资源的使用也被拉伸到一个夸张的程度,例如:写一个Hive表元素超过3000列数据,会继续增加服务节点的CPU占用率到100%,十多分钟后触发超时。一张几十列的埋点表,上下游很多,打开详细显示需要一分多钟。为此,我们进行了一系列的性能调优,结合DataCatlog产品的特点,调整了ApacheAtlas和底层Janusgraph的实现或配置,并对性能优化的方法论做出了一些总结。业务系统优化的总体思路在讨论更多细节之前,我们先简单介绍一下我们的业务系统优化思路。本文中的业务系统是相对于引擎系统的概念而言的,特指解决某些业务场景,直接将前端使用暴露给用户的Web类系统。在优化之前,应该明确优化目标。与引擎系统不同,业务系统不追求极致的性能体验。他们更专注于解决实际的业务场景和问题,并进行有针对性的调优。需要特别注意避免过早优化和过度优化。只有准确定位瓶颈,才能事半功倍。在一个业务系统中,通常有很多可以优化的点,从业务流程的梳理到底层组件的性能提升,但优化瓶颈才是最高的ROI。根据问题的类型,选择最具成本效益的解决方案。解决一个问题,通常会有多种不同的方案,就像条条大路通罗马,但在实际工作中,我们通常不追求最完美的方案,而是选择性价比最高的方案。必须快速验证优化效果。性能调优具有一定的不确定性。我们实施了某种优化策略后,通常无法在线观察到效果。需要更敏捷的验证方法,以确保策略的有效性能够被及时发现,并及时做出相应的调整。.业务系统优化细节优化目标的确定优化业务系统时,有两件事是禁忌的:过早优化:当某些功能、实现、依赖系统和部署环境不稳定时,过早投入优化代码或设计,可能会导致发生后续系统更改时浪费精力。过度优化:与引擎系统不同,业务系统通常不需要跑分,也不需要与其他系统生成性能对比报告。在实际工作中,优化了更多的业务场景。比如用户直接访问前端界面的系统,通常不需要将响应时间优化到ms以下。几十毫秒、几百毫秒就已经满足要求了。优化范围选择对于一个业务web服务,尤其是在重构阶段,优化范围是比较容易划定的,主要是找出API中与之前的系统相比明显变慢的部分。例如可以收集以下方法优化部分:通过前端慢查询抓包工具或者后端监控系统,筛选出P90大于2s的API页面。在测试过程中,在研发和测试同学反馈的API数据导入过程中,研发发现的慢API等,建立了优化目标。针对不同的业务功能和场景,尽可能详细地定义优化目标。以DataCatalog系统为例:当系统复杂到需要定位性能瓶颈时,一个简单的接口调用可能会涉及到范围广泛的底层问题。调用,在优化具体的API时,如何准确找出导致性能问题的瓶颈,是后续其他步骤的关键。下表是我们总结的常见瓶颈排查方法的总结。优化策略找到某个接口的性能瓶颈后,接下来就是进行处理了。同样的问题,可能有很多种解决方法。在实际工作中,我们优先考虑性价比高的,即实施简单,效果明显。快速验证优化的过程通常需要不断尝试,因此快速验证尤为关键,直接影响优化效率。DataCatalog系统优化示例在升级ByteDataCatalog系统的过程中,我们大量使用了上述各种技术。本章我们选取一些典型案例,详细介绍优化过程。在调整JanusGraph配置的实践中,我们发现以下两个参数对JanusGraph的查询性能影响比较大:query.batch=turequery.batch-property-prefetch=true其中,对于第二个配置的细节项目,您可以参考我们以前发表的文章。这里我们着重介绍第一种配置。JanusGraph做查询行为有两种方式:对于字节内部的应用场景,元数据之间的关系很多,元数据的结构复杂。大多数查询会触发更多的节点访问。我们设置query.batch为true时,整体效果更好。调整Gremlin语句以减少计算量和IO的一个典型应用场景是根据某些属性统计通过关系拉取的其他节点。在我们的系统中,有一个名为“BusinessDomain”的标记类型。在产品上,需要获取该类型的某个标签关联的元数据类型,以及每个类型的数量,返回类似如下的结构:{"guid":"XXXXXX","typeName":"BusinessDomain","attributes":{"nameCN":"Live","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()。轮廓();Atlas中,拉取数据计算逻辑调整基于Guid详情展示等场景,实体相关的数据将根据Guid拉取我们在EntityGraphRetriever中优化了一些实现,例如:在mapVertexToAtlasEntity中,修改边遍历读取的方式数据调整为按点和点上的属性过滤拉取,触发multiPreFetch优化。支持根据边类型拉取数据,在应用层根据不同场景指定不同的边类型集合,进行数据裁剪。最典型的应用就是去掉详情展示页的血缘关系拉取。限制关系拉动的深度。在我们的业务中,大部分关系只需要拉一层,有的需要一次拉两层。因此,在我们的接口实现中,我们支持深度传入拉取关系。默认为一层。通过其他修改,对于被广泛引用的埋点表,读取时间从约1分钟减少到不到1秒。为大量节点顺序获取信息并并行处理。在血缘接口中,有一个场景需要根据血缘拉取某个元数据的上下游N层元数据。新拉取的??元数据需要重新查询,做属性扩展。我们通过增加并行度来优化。简单的说:设置一个Core线程比较少,Max线程比较多的线程池:难得拉全上下游。在大多数情况下,几个核心线程就足够了,而对于少数情况,启用额外的线程。批量拉取某一层的元数据后,将每一个新拉取的元数据顶点添加到一个线程中,在线程中分别做属性扩展,等待所有线程返回更多关系的元数据来优化效果。可以从分钟到秒。对于写入瓶颈的字节优化数据仓库,有一些3000多列的大而宽的表。对于这种元数据,初始版本很难写入成功,往往需要15分钟以上,CPU使用率会飙升至100%。为了定位写入的瓶颈,我们从LoadBalance中移除了一台在线机器,构造了一个3000多列的元数据写入请求。使用Arthasitemer作为Profile,我们得到了下图:从上图我们可以看到,大约70%的时间花在了createOrUpdate中引用的addProperty函数上。耗时分析JanusGraph在写入属性时,会先找到与该属性相关的组合索引,然后从中过滤掉Coordinality为“Single”的索引。写入前会检查这些单个索引是否已经包含JanusGraph中要写入的propertyValue组合索引当前存储格式为:Atlas默认创建的“guid”属性标记为globalUnique,其对应的组合索引为__guid.对于类型定义文件中声明为“Unique”的其他属性,比如我们业务语义中的全局唯一“qualifiedName”,Atlas会将其理解为“perTypeUnique”。对于Property本身,如果它也需要建立索引,那么会建立一个协调的是一个集合的完整索引,为“propertyName+typeName”生成一个唯一的完整索引。调用“addProperty”时,会先根据属性的类型定义查找“Unique”的索引。对于“globalUnique”的属性,如“guid”,返回“__guid”;对于“perTypeUnique”的属性,如“qualifiedName”,返回“propertyName+typeName”的复合索引。对于唯一索引,会尝试检查“唯一”属性是否已经存在。方法是拼接一条查询语句,然后在图中进行查询。在我们的设计中,写入表时,每一列都有唯一的“guid”和“qualifiedName”,“guid”会作为全局唯一查询对应的完整索引,“qualifiedName”会作为完整的“perTypeUnique”组合查询“propertyName+typeName”的索引,整个过程是顺序的,所以在写很多列,很多属性,很多关系的时候,一般会比较耗时。小时。优化思路对于“guid”,其实在创建时根据“guid”的生成规则已经保证了全局唯一性,几乎不可能有冲突,所以可以考虑去掉“guid”的唯一性检查guid"编写时,节省了一半的时间。对于“qualifiedName”,按照业务生成规则,也是“globalUnique”,“perTypeUnique”和“perTypeUnique”的性能差异几乎翻倍:优化后的实现效果去掉了Atlas中“guid”的唯一性检查。添加“Global_Unqiue”配置项,在类型定义时使用,在初始化时为“__qualifiedName”建立全局唯一索引。配合其他优化方法,可以将编写超多属性和关系的Entity的耗时降低到分钟级。总结业务系统的性能优化,通常是针对解决一个特定的业务场景,从接口入手,逐层解决性能优化基本遵循这样的思路:发现问题->定位问题->解决问题->验证结果->综上所述,优先考虑“聪明”的方法和“土”的方法,比如加机器改参数,不走弯路追求卓越