阅读视图

发现新文章,点击刷新页面。

字节跳动观测数据埋点标准化实践

来源|字节跳动基础架构-可观测团队

背景

随着字节跳动业务规模不断扩大,对存量和新增业务的服务质量承诺变得越发关键。稳定性治理方面:怎样支持保障服务线上的高可用性,或者在出现故障/事故时,如何高效且迅速地止损、定位分析影响面已成为一个重要议题。

稳定性建设所涉及的话题十分广泛,涵盖流程梳理与标准化、数据标准化、SLO 定义、故障自愈、事故复盘和演练等方面,字节跳动基础架构可观测团队提供的稳定性平台建设思路是“事前预防、事中处理、事后复盘、事后补救/反哺事前”这四个阶段。

其中, 观测数据标准化以及一系列配套的数据链路,如:数据埋点、数据消费、数据存储、数据查询、离线数仓等,都是后续稳定性建设的重要数据基石。

并且,由此引申出排障/止损效率的问题,由于字节的服务/基础设施是分层建设的,包括端侧客户体验层、网络接入层、应用服务层、基础设施层、IDC\资源层等,不同层面的统计/描述口径是否一致、能否对应,以达到在跨层间能上卷下钻和平层内过滤聚合的“车同轨书同文”效果,这对于大幅提升整体排查效率, 让 SRE/GOC 同学能够自助完成端到端的问题排查就显得尤为重要。

img_v3_02ik_a98c1a06-a522-493e-a443-78169a1b9f3g.png

拥有统一的观测数据标准, 能够在很大程度上提升团队间的排障效率,从人工分析的方式提升至更大程度的自助/自动化排障的阶段。

埋点标准化的重要性

提高研发效率 & 降低研发协同成本

  • 面向排障方面:跨层间的上下文过滤便捷,术语统一。
  • 进行历史数仓分析(容量优化)时,整体数据处理逻辑的适配成本会大幅降低。
  • 用户的学习曲线陡峭,理解心智负担沉重。

为 AIOps 提供强有力的数据支撑

观测数据属于 AIOps 的五大基石(数据、知识、算法、代码联动、人机协同)之一。在清华裴丹老师的《AIOps 落地的 15 条原则》里,也都提及了数据的重要性。

拥有数据标准化和统一的访问体验,为后续稳定性的终极目标 MTTR 1-5-10(1 分钟发现,5 分钟响应以及 10 分钟快恢复)提供了数据层面的保障。包括同层数据的聚合 / 过滤,以及跨层数据的下钻和上卷,都会有统一的使用方式。

名词解释

名词 解释
Metrics 2.0 字节跳动内部使用广泛的时序数据处理引擎,提供了时序数据收集、存储和聚合查询的功能。2.0 版本提供引入多值概念,打平prometheus 4类指标类型语义、支持秒级打点& 存储周期定制化等多租户特性、 端到端高性能优化的分布式时序集群版本。
BytedTrace BytedTrace是字节跳动使用的一套集成了 Tracing/Logging/Metrics 三种能力的可观测性解决方案,提供了从采集、传输、存储、检索到前端产品化交互的整套能力。它定义了统一的数据模型(Trace 、Span 、Event、Metrics 等),提供了各语言配套 SDK,并与公司各主流框架组件实现默认集成。
观测埋点 TagKV Metrics TagKV 是一种用于标记和管理度量数据的键值对(Key-Value Pair)格式。通常用于监控系统、分布式追踪系统和日志管理系统等领域,TagKV 提供了一种灵活且高效的方法来分类和筛选数据。
Measurement 可观测对象的某个指标,如服务的上游调用延时,物理机的 CPU 使用率。Measurement 是带有可观测对象的 context的,是语义化的,同时能识别在不同条件下应该使用哪个版本的指标以及对应的 TagKV。而且可以根据观测对象的元数据条件,同时关联多个时序数据源,便于按需时序数据源切换。
SLO Service Level Objectives,服务级目标是指服务提供方对所提供服务的某些性能或质量指标所设定的目标值。这些指标通常用于衡量服务的可用性、性能和其他关键属性,旨在确保服务达到预期的质量水平。
TCE Toutiao Cloud Engine,为字节跳动内部提供的高度可用、弹性扩展的容器服务。
PSM Product Subsys Module,是字节跳动内部服务的唯一标识。
GOC Global Operations Center,基于字节跳动各类研发,运维体系下的高可用产品能力,结合稳定性保障策略及运营机制,提供字节跳动全线基础产品的可靠性服务与设施稳定性保障,达成字节跳动全线业务各类场景下的端到端高可用性。

字节埋点标准化挑战与拆解思路

挑战: 历史上可观测性埋点质量偏低

首先,我们对埋点标准化进行定义,包括但不仅限于如下的标准定义,包括覆盖完整、定义统一、计量准确、面向引擎友好等四大方面。

img_v3_02ik_56f19e79-13a2-4bdf-aa70-2d6d49e02b8g.png

简而言之,在 2020 年以前,从覆盖完整定义统一计量准确面向引擎友好等维度来看,字节整体的观测数据埋点存在一定的差距。

具体如下:

  • 负载均衡 埋点

    • 计量准确:中等水平

      • 存在较严重的打点丢失问题
    • 面向引擎友好:较低水平

      • 指标打点对于配置预计算不友好
      • 指标名膨胀也比较严重
  • 微服务 埋点

    • 覆盖完整:中等水平

      • 20 年前 Tracing 方案还在 V1 版本
    • 计量准确:中等水平

      • 遇到高基数的指标会被封禁
    • 面向引擎友好:较低水平

      • 指标打点对于配置预计算不友好
      • 指标名膨胀也比较严重
      • 加权计算也不好实现
  • 语言 运行时 埋点

    • 定义统一:较低水平

      • Golang & C++ 框架 不同的版本定义的指标格式都不太一样
    • 面向引擎友好:较低水平

      • 指标打点对于配置预计算不友好
  • 容器指标 埋点

    • 覆盖完整:较低水平

      • 没有日志采集覆盖
    • 计量准确:中等水平

      • 遇到高基数的指标会被封禁
    • 面向引擎友好:较低水平

      • 指标打点对于配置预计算不友好
  • 基础架构 存储 & 数据库 埋点

    • 覆盖完整:较低水平

      • 存储、数据库、MQ 客户端没有黄金指标打点
      • 没有日志采集覆盖
    • 计量准确:较低水平

      • 不同存储、数据库、MQ 产品打点格式 都不一
    • 面向引擎友好:较低水平

      • 指标打点对于配置预计算不友好

思路: 分层&向后兼容推进埋点标准化

总结来说,之前的字节服务端观测数据质量大致存在三类问题。

  • 同层数据/跨层数据不一致。
  • 观测的多模态数据类型(指标、日志、链路)的数据定义不统一。
  • 观测数据格式对引擎不够友好,例如所有数据都在 default 租户的一个大仓里,再比如很多观测指标的定义对于预计算不友好。

针对上述问题,我们采取了如下的多个思路逐一进行解决。

实施思路

一方面,在埋点侧就尽可能统一埋点 TagKV 定义,而且平台级 TagKV 都通过环境变量或者请求上下文自动注入对应的 Tag Value, 以防止由业务手工注入带来的人工错误。

另一方面,对于指标、链路和日志侵入式 SDK,我们通过字节内部的远程过程调用框架以及存储、数据库、消息中间件的客户端 SDK 搭载嵌入中间件,对于业务来说,能相对透明地升级到最新特性的版本。另外, 对于远远低于 SDK 基线版本的服务, 我们也通过字节软件供应链安全治理平台通过编译卡点的不同程度[warning 提示/发布卡点]推动业务升级。

在 负载均衡、应用、中间件、存储计算组件等各个纵向方面, 我们也主动与对应的平台对接,推动指标、日志、链路的埋点注入。

最后,在指标埋点上也额外关注对于多租户的声明,以达到一定的分库分表功能,以及多值声明,以最大程度减少数据消费和存储成本。如下所示, 就是团队在各个不同观测对象的埋点方面所做的业务推进情况。

img_v3_02ik_82b1a459-030a-43f1-8db0-808d5cc209eg.jpg

难点: 识别和解决

类似观测数据标准化的工作历经多年,牵涉的团队众多,整个过程并非毫无波折。遇到问题时要解决问题并思考能否将其标准化或者平台化,同时也要考虑能否尽可能地复用其他团队的能力和工具来助力我们进一步推广。当时如何高效地推动业务升级是我们的主要目标。

[业务推进] 高效推动业务升级观测SDK

在 Metrics SDK 需要升级到基线版本的情况下,以前的做法是在字节软件供应链安全治理平台上配置版本拦截,提醒用户升级,但是整体升级效率比较低,我们也无法跟踪用户的升级进展。因此我们联合字节软件供应链安全治理平台团队实现 SDK 自动升级功能。

Metrics ****SDK 自动升级

Metrics ****SDK 自动升级功能可以自动实现在当前业务代码库的代码提交期间,如果检测到对应集成的metrics SDK 低于基线版本,则会向用户推送代码提交失败的通知,提醒用户需要主动升级到metrics SDK基线版本或以上的操作。

远程过程调用 框架 & 基础组件客户端 集成 ****BytedTrace ****SDK 集成

观测团队多年来持续推动公司的远程过程调用 框架以及基础组件客户端 集成 BytedTrace SDK **** ****借助字节软件供应链安全治理平台进行递进式卡点推广,依靠代码血缘平台来推动框架、组件的基础库版本实现升级。在存有流量的微服务上,BytedTrace SDK的覆盖比例按照 TCE pod 接入情况来计算,当前已达到 95%。

从服务的优先级角度而言,公司当前96% 的 P0 服务中已接入 Bytedtrace SDK 。

[业务推进] 提升基础组件观测埋点质量

TCE 调度 / 运行时 打点格式设计思路

前文提到,提升业务层、应用层、容器层等多层间指标的跨层关联和下钻能力是指标标准化的一个重要目标,而实现跨层关联的关键动作在于保证同一含义的指标 TagKV 在各层上的定义保持统一,为实现这一点,我们对各个层次上的核心组件进行了统一的设计,具体如下:

层次 核心组件/着手点 埋点标准化设计思路
业务层 Metrics 2.0 SDK - 内置统一的平台级TagKV,提供横向跨语言、跨服务的TagKV统一
应用层 运行时 指标、远程过程调用 指标 - 横向上,提供统一的、跨语言的指标名定义
  • 纵向上,对齐Metrics 2.0 SDK 平台级TagKV规范 | | 容器层 | 与调度合作,对容器指标采集agent(TCE调度)进行标准化改造 | - 对齐Metrics 2.0 SDK 平台级TagKV规范 |
  1. 首先,我们在 Metrics 2.0 SDK 内置定义了一套平台级 TagKV,这样所有使用 Metrics 2.0 SDK 的业务打点都会携带标准的预定义的 TagKV。这些共同TagKV包括: _cluster、_psm、_pod_name、_ipv4 等。

  2. 在应用层,挑选了对业务排障、应用观测常用且通用的两类指标(运行时、远程过程调用)进行标准化,目标是在横向上,提供跨语言、统一的指标名、TagKV语义定义等;在纵向上,对齐 Metrics 2.0 SDK 平台级 TagKV 规范,以便于跨层关联。以 运行时 指标为例,其定义规范如下:

    1. 不同语言的指标采用统一命名约定:runtime. {runtime} . {metric}[ . {field}]
    2. 不同语言类似含义指标采用统一命名风格:如 go、java 中统计堆对象申请量的指标都命名为memory.allocated_bytes
    3. 必须包含 Metrics 2.0 SDK TagKV 规范的平台级 TagKV,如 _psm、_pod_name 等
  3. 在容器层,与调度团队共同推动其 TCE 容器指标采集 agent(TCE调度) 的指标标准化改造,指标 TagKV 对齐Metrics 2.0 SDK TagKV 规范。

通过将这些核心组件进行标准化改造,为跨层的指标关联和下钻提供了能力基础。同时,在每个核心组件的指标定义上,我们还通过以下两个方式进一步提升埋点的性能和成本收益,第一点是对各个组件使用独立租户,实现资源的隔离,保障写入稳定性和查询性能;

指标 租户名 集群类型
运行时 apm.runtime 独立集群
远程过程调用 框架 apm.rpc 独立集群
TCE 容器指标 computation.tce 独立集群

第二点是在语义明确的前提下,尽量使用多值格式定义指标,降低存储成本。以 TCE调度 指标为例,将原来 mem 相关的四个指标合并为一个多值指标的四个字段,存储成本大致可以被认为降低至四分之一。

原指标 改造后多值指标名 改造后多值字段
tce.host.mem_total inf.tce.host.mem total
tce.host.mem_free free
tce.host.mem_available available
tce.host.mem_used used

[配套工具] 帮助平滑迁移观测数据

[工具1] 语义 化指标替换

我们提供语义化指标替换,称为Measurement,其能力就是对原始 Metrics 打点的语义化封装;同时能识别在不同条件下应该使用哪个版本的指标以及对应的 TagKV。这两个关键能力能够促使在做数据迁移时,观测大盘和报警基本达到比较平滑的状态。

原始 Metrics 打点:直接写入时序数据库(可以是 metrics \ influxdb \ prometheus)的数据。

语义 封装:用标准的语义化来包装原始的 metrics 打点数据。 比如 go 服务的 gc 数量的 metrics 打点是 go.{{.psm}}.numGcs,其中{{.psm}}为具体的 psm, 我们会定制一个语义化指标名叫 "runtime.go.gc_num"来表达 go 服务的 gc 数量,包括用统一的 TagKV 来封装对应的原始 TagKV。 不管是 open api 还是前端调用, 都用指标 "runtime.go.gc_num" 对measurement 服务进行调用。

不同条件下的查询 路由:需要这个能力是因为在字节内部原始 Metrics 的打点会不断的升级, 比如 golang 运行时 历史上会有 v1 、v2 、v3 多个版本,我们需要能够在给定的输入信息条件下去查询到对应的指标版本。这个判断条件实现的逻辑一般为可用输入的 psm 名字构成 Metrics go v1 的指标名,再根据指标名的数据是否存在来判断是 runtime v1、runtime v2 或者 runtime v3 的版本,指标判断也以此类推。或者可以通过 psm 的 scm 编译信息确定该 psm 编译的 golang 运行时 版本是 v1、v2 或者 v3。 通过对应条件的判断来做到对应数据的查询路由。

img_v3_02ik_5c2f9415-8b2f-4dd5-a031-dff8ce63af6g.png

在有了 Measurement 能力后,我们抽象出了 Measurement 服务,该服务作为观测大盘和报警的一个数据源。在尽量不需要用户介入的情况下完成数据打点的迁移和替换。

当前借助 Measurement 能力,针对公司的 远程过程调用、HTTP 等框架,容器引擎、FaaS、机器学习推理等平台,还有负载均衡、缓存、数据库、消息队列等基础组件,以及golang 运行时 等,均进行了统一的标准化语义封装,这些语义化封装在观测平台上均有所展现。

[工具2] Metrics 前缀分流

怎样帮助业务顺利地迁移到新租户,同时确保新老指标的查询方式均可使用,是我们在推动业务租户迁移时所面临的较大挑战。

针对上述问题,观测团队起初推进引导用户主动迁移至新租户,旨在实现租户隔离,提供更优的稳定性保障,进行精细化容量治理以降低成本。然而,后来发现主动迁移的速度太慢,赶不上打点量的自然增长。于是,推出了让用户无感知的被动租户迁移方案。大致思路是依据某些特定的指标前缀,主要涵盖一级 / 二级前缀,通过特定配置把这些指标分别路由到不同的新租户,并且在新租户上支持查询翻译,即便用户不修改查询租户,继续用 Default 租户查询仍能正常获取数据。该方案具有以下优势:

  1. 业务在读写两侧无需进行代码变更,就能将流量迁移到新租户集群。
  2. 最大程度减少不同租户间因集群变更和读写流量变化对线上稳定性产生的相互影响,提供更出色的稳定性保障。
  3. 精准对接业务线租户,便于后续进行打点流量治理、容量规划以及资源充值等操作。

具体的实现由 Metrics 组件中各模块的相互配合完成,包括写入、控制面、查询、数仓等方面,大致的实现流程如下:

前缀分流租户的整个过程存在众多细节,为减少过程中的过多人为操作,防止出现某些环节被遗忘的情况,观测团队设计了分流流程工单以及白屏化运维平台,尽可能让整个操作流程实现自动化,提高分流租户的效率。此外,前缀分流迁移新租户的整个过程对于业务来说成本为零,同时对于 观测团队而言不像依赖业务方主动迁移那样周期漫长,其周期短、生效时间快,能够收敛团队人力的持续投入。

总的来说,观测团队提供了一种让用户无感知、实现无缝迁移新租户的方案,用户的核心观测大盘和报警也无需修改,最大程度降低了埋点标准化对用户的打扰。

埋点标准化字节的实践与效果

观测数据质量前后对比

经过 2020-2022 年推进 BytedTrace SDK 覆盖率、2023 年推动云基础组件和应用层指标租户迁移之后, 从埋点标准化的 4 个维度看,都有不同程度的质量提升。

  • 负载均衡

    • 计量准确:较高水平 [2020年为中等水平]

      • 通过 2.0 SDK 三个特性, 基本消除丢点的问题:

        • 打点本地聚合
        • 面向字节流的 codec 编码
        • Agentless 投递
    • 面向引擎友好:较高水平 [2020年为较低水平]

      • 实现面向预计算友好的效果
    • 成本收益:

      • Metrics 2. 0 打点商品成本相对 1.0 下降 94%
      • Metrics 2. 0 很好地解决了打点封禁问题,特别是在一些配置量巨大的核心集群,解决了其超过 90%打点无法查询的情况
      • Metrics2. 0 TLB 机器成本初步统计主容器和 adaptor 打平,同时相对 1.0 节约了 ms2 的 15000 核资源
  • 微服务

    • 覆盖完整:较高水平 [2020年为中等水平]

      • 80%以上 PSM 覆盖到 BytedTrace SDK 集成
    • 计量准确:中等偏上水平 [2020年为中等水平]

      • 高基数的指标封禁问题 由于迁移到了新租户 可以做封禁阈值定制化
      • [计划中] 升级 bytedTrace 内的 metrics 2.0 SDK 降低丢点的风险
    • 面向引擎友好:较高水平 [2020年为较低水平]

      • 实现面向预计算友好的效果
    • 成本收益:

      • 以计算关键组件 Consumer 为例,新租户只需要老租户 20%的资源,就可以完成相同数据的写入计算;其他写入计算类组件也类似
      • 以存储关键组件 tsdc 为例,新租户只需要老租户 55%的资源,就可以完成数据的写入、存储
  • 语言 运行时

    • 定义统一:较高水平 [2020年为较低水平]

      • 统一了不同语言和框架的 运行时 打点格式
  • 容器指标

    • 覆盖完整:中等水平 [2020年为较低水平]

      • TCE调度 接入日志租户
    • 计量准确:较高水平 [2020年为中等水平]

      • 引入多值 降低指标名数量

      • 高基数的指标封禁问题 由于迁移到了新租户 可以做封禁阈值定制化

      • 通过 2.0 SDK 三个特性, 基本消除丢点的问题

        • 打点本地聚合
        • 面向字节流的 codec 编码
        • Agentless 投递
    • 面向引擎友好:较高水平 [2020年为较低水平]

      • 实现面向预计算友好的效果
  • 基础架构 存储 & 数据库

    • 计量准确:较高水平 [2020年为中等水平]

      • 引入多值 降低指标名数量

      • 高基数的指标封禁问题 由于迁移到了新租户 可以做封禁阈值定制化

      • 通过 2.0 SDK 三个特性, 基本消除丢点的问题

        • 打点本地聚合
        • 面向字节流的 codec 编码
    • 面向引擎友好:中等水平 [2020年为较低水平]

      • 打点格式调整的 支持预计算配置
    • 成本收益:

      • 以 mysql 迁移为例

        • Mysql 租户 成本节省 45.7%
        • Mysql 租户 带宽节省了 80%

截止到今年年初, Metrics 在中国国内区域已经接入 60+ 租户,占总流量的 70% 左右。

赋能效果总结

加速微服务端到端根因定位

通过指标标准化 & 多模观测数据 [指标, 日志,链路]标签术语的标准化, 我们实现面向微服务的上卷 & 下钻关联分析。

也使得使得跨层问题根因分析有了可能性:

目前端到端根因定位覆盖了60%以上的报警场景,日均触发根因定位 50余万 次,用户对定位结果的正反馈率超过80%。

简化服务性能离线数仓构建

在实现了在线观测数据的标准化,并将其导入统一的存储介质之后,构建字节整体关于服务性能、容量、吞吐量的数仓大盘就更加便捷。比如 展现某服务的单核 QPS 分时热力图 如下:

目前基于微服务应用性能数仓已覆盖公司超97%的微服务量化,有效支持字节跳动各业务线服务性能、服务应用健康度度量,由此带动一系列精准的成本优化。

观测底座自身收益

  • 从稳定性角度看,由于引入metrics多租户概念,所以我们能够通过逻辑租户映射到物理资源,从而降低故障半径,减少不同租户间流量的相互干扰。
  • 从成本角度看,我们能够依据每个租户的副本数、存储时长 TTL、打点的最小精度以及多值定义,最大程度地降低写入流量和存储容量的成本。metrics 多租户迁移前后对比,成本节省幅度在 20% ~ 80% 不等。

总结

历经上述观测埋点套件 BytedTrace SDK推广、Metrics 指标标准化迁移和推广、部分业务接入日志多租户,字节后端观测数据的质量在覆盖完整度定义统一计量准确面向引擎友好四个方面上取得了显著的质量提升。这也为后续的全景全栈高效排障奠定了坚实的基础,帮助更多业务团队在业务稳定性方向持续建设。


依托字节跳动内部可观测团队大规模技术实践,通过内外合力,在火山引擎上推出了应用性能监控全链路版(APMPlus)、托管 Prometheus(VMP)、云监控等可观测产品,致力于为用户提供全面、智能、高效、易用且安全的全栈可观测解决方案。

目前 APMPlus Server 端监控已正式 GA 并支持最新的大模型链路追踪相关能力,欢迎咨询了解。

🔗 相关链接

APMPlus www.volcengine.com/product/apm…

VMP www.volcengine.com/product/pro…

云监控 www.volcengine.com/product/clo…

详解veImageX助力卓特视觉智能、高效生成设计素材

前言

设计素材行业为设计师和创意工作者提供丰富的视觉和创意资源。数字媒体和互联网的迅猛发展,促使这一行业市场规模不断扩大,用户对设计素材的个性化和定制化需求与日俱增。卓特视觉,作为Adobe Stock中国区官方合作伙伴,自2014年成立以来,始终致力于推动中国创意产业的繁荣发展。在AI的技术浪潮中,卓特视觉选择与火山引擎veImageX(一站式图片解决方案)携手合作,旨在通过AIGC加成,更加智能和高效的生成设计素材,进一步拓宽创意表达的边界。

卓特视觉(Droit Vision),Adobe Stock中国区官方合作伙伴,全面整合全球范围内的高质量图片、矢量插画、高清视频及音效音乐等素材资源,专注于为新媒体、设计、广告、各类垂直行业及个人用户,提供一站式的视觉素材和解决方案,助力创意人士和企业提升其视觉作品的品质和影响力。

至今,卓特视觉在线销售高清正版图片总数超5.6亿和超3,600万条高清视频。自2014年成立以来,卓特视觉成功为众多知名企业提供了安全、高效、优质的视觉创意解决方案,赢得了广泛的企业级客户信任。

场景概述

在设计素材行业,传统的商业模式通常由创作者提供内容并上传至平台,平台负责销售和分发,同时负责版权等问题,用户通过付费获取平台的高质量素材资源,平台则根据销售情况与创作者分成。而在AI的技术推动下,平台会提供一系列的AIGC工具,帮助用户实现图片生成、放大、扩展、风格转换等效果,同时收取使用这些功能的费用。

图片来自卓特视觉官网

方案介绍

火山引擎veImageX基于字节跳动的图像领域最佳应用实践,提供端到端的一站式图片解决方案。

整体架构

一套方案解决上传、存储、图像处理、分发、解码、QoS&QoE监控的全链路方案,覆盖从内容生产端到图像消费端。

veImageX的服务端具备强大的实时处理能力,不仅包含了裁剪、缩放、格式转换等基础图像处理功能,还提供了画质评估、画质增强、智能裁剪、超分、盲水印等丰富的AI组件能力。

卓特视觉接入了veImageX的哪些能力

一、画质评估

画质评估组件支持模仿人类对于图像的视觉感受,从而对图像的各方面进行评分。评分指标有大众美学评分、噪声强度评分、纹理丰富度评分和色调均衡程度评分等。veImageX通过抖音集团内部的大量线上业务实验发现,图片画质优劣对点击率、停留时长等消费类指标有正相关影响,间接影响用户收益指标。卓特视觉通过画质评估组件,对线上的海量素材文件进行了广泛的评估,在网站尽量展示评分较高的图片,并在用户查询图片时,优先推荐同类型中评分高的图片。这一系列举措不仅提升了网站整体的图片质量及用户的满意度,还促进了业务增长,并获得了良好的用户口碑。

二、智能裁剪

智能裁剪是 veImageX 提供的全新图片裁剪附加能力,支持对输入图片进行指定尺寸变换,能够自动判断主体区域的位置,并支持自动化适配不同尺寸图片内容的裁剪。卓特视觉的用户分布在各行各业,用途包含宣传页、海报、杂志、电商平台、户外广告等,对图片的尺寸和表现侧重点都有个性化的要求,卓特视觉通过智能裁剪能力批量对原图进行裁剪,自动化适配用户对于不同尺寸的要求,同时确保在任何尺寸下,图片主体都能处于最佳位置。快速高效满足客户需求的同时,也拓宽了产品的适用边界。

三、存储

卓特视觉目前拥有超过5.6亿的正版素材,并且数量仍在持续高速增长,占用的存储空间日益庞大,成本也与日俱增,veImageX提供存储服务,同时支持根据上传时间变更存储类型的智能降冷策略,有效节省存储的成本。此外, 为了进一步帮助企业降低存储成本,veImageX通过自研BVC算法,提供全球领先的极限图片压缩比,对比JPEG压缩率提升8-10倍,在不降低图片质量的前提下,在保持图片清晰度基本不变的情况下,单张图片体积节约超过70%,可以实现显著的成本节约。

四、分发

veImageX作为端到端的图片解决方案,除了强大的AI图像处理能力,还提供存储和分发能力,在分发阶段,veImageX利用自建 CDN 节点进行灵活的智能调度,为国内外用户提供极致的观看体验。卓特视觉通过使用veImageX的高效分发方案,确保了全球用户访问的快速和稳定。

设计素材行业其他需求的能力

一、智能生图能力

用户在平台可能会遇到不符合设计标准的素材,不仅影响了创作效率,同时也会影响平台的口碑,因此,引入AIGC智能生图能力显得尤为重要,当现有素材无法满足需求时,可以通过AIGC快速生成。veImageX结合豆包的AI生图方案,最新上线了智能生图能力,封装了文生图、图生图一站式解决方案。支持将豆包生成的图片进行后处理,包含存储、压缩、二次处理、超分辨率、盲水印、裁剪、适配、分发等。典型功能如下图展示:

  • 文生图场景

  • 图生图场景

此外,veImageX智能生图能力还支持桥接第三方模型文生图、图生图服务,直接对接veImageX进行上传、编码、存储与管理,并支持完善的后处理服务。大大扩展了方案的灵活性。

二、智能审核

设计素材平台如果遇到涉黄、涉暴的素材上传,不仅涉嫌法律风险,而且对平台的品牌可信度将会是极大的折损,而面对每天数以十万计的素材,人工审核显然无法满足。veImageX 提供了图片智能审核功能,支持分类型智能检测图片中涉黄、涉暴恐、违法违规等十几种禁用行为,并返回最终识别结果。识别并预警用户上传的不合规图片,协助平台快速定位处理。

三、盲水印

在设计素材行业,素材的版权归属一贯容易产生争议。在版权意识和版权法逐渐完善的今天,稍有不慎可能就会产生法律纠纷。veImageX兼顾版权追踪和图片美观,支持对图片添加盲水印,同时支持对图像提取盲水印信息,方便追踪溯源。盲水印是一种肉眼不可见的水印方式,可以在保持原图图片美观的同时,又可以保护资源版权。对原图进行解码后,可以得到盲水印信息证明图像的版权归属,避免未经授权的复制和拷贝而造成的版权问题。

四、超分辨率

设计素材平台的用户在制作海报、广告牌等场景时,往往需要对原始素材进行放大,同时需要保持放大后图像的清晰度,即所谓的“无损放大”。veImageX支持将图像做2-8倍智能放大,并保持处理后图像的清晰度,使图像更加清晰、锐利、干净,给用户带来良好的视觉体验。

五、智能背景移除

用户在使用平台提供的设计素材时,如果发现图片中的主体部分符合需求,但是为了配合使用场景、符合品牌调性等原因,需要对原始图片中的背景进行移除。veImageX的智能背景组件,支持保留图像的主体并抠除其复杂的背景,从而生成保留主体的透明底图片。veImageX提供了多种图像处理模型,支持精细化图像主体轮廓处理,可大幅度提升图像处理效率,降低人工成本。

结语

在AI的技术浪潮中,传统的设计素材行业正在向AI时代迈进,以满足客户日益个性化、精细化、创意化的诉求。火山引擎veImageX凭借夯实的技术底座和强大的AI能力,与卓特视觉携手合作,共同迈入设计素材行业AI新纪元,助力我国视觉版权服务市场的蓬勃发展。

了解更多:www.volcengine.com/product/ima…

半空:LLM 辅助的 Go2Rust 项目迁移

试想一下:将一个 Golang 项目(大象)改写为(装进) Rust(冰箱) 总共需要几步?

“Gopher in 冰箱” by DALLE3

背景

当 Rust 语言为我们展示出在「性能」、「安全」、「协作」等方面诱人的特性之后,却因为其陡峭的学习/上手曲线拒人千里之外。是否存在一种科技,能够帮助我们的同学在语言学习项目迁移上完美并行,最终真正将 Rust 项目迁移这个看似美好的荆棘之果转变为触手可得的「低垂果实」呢?

为了将美好的愿望转变为实际,我们结合 LLMs 做了一些尝试,利用 LLMs 在编程语言上体现出的「涌现」能力,设计了一套基于 LLMs 的应用开发基座(ABCoder),在这个基座之上进一步演进出了我们本篇的主角:「半空」。

ABCoder 是字节内部一个编程向 LLMs 应用开发基座,包含自研的 LLMs 原生解析器、工具(Tools)以及工作流(Workflows),对编程项目本身进行深度解析、理解和压缩,并将其制作为源码知识库(Source code as Knowledge),之后利用这类知识库实现对 LLMs 推理过程中所需上下文进行补齐,从而构建出高质量、低幻觉、稳定的编程类 LLMs 应用。有关 ABCoder 更多的介绍可以参考这里

半空

TL;DR 传送门

按照 ABCoder 的设想,让 LLMs 理解编程项目的入口就是结合对项目的解析、理解、压缩后的知识关联和构建,这对于一个轻量化的应用来说可能足够(ABCoder 当前已经能够实现将一个标准 Hertz 项目“转述”为一个 Volo-HTTP 项目),但对应到实际场景中的业务项目来说(增加大量业务属性且复杂度更高),要想真正让 LLMs 完整理解整个项目,并且在有需要的时候让 LLMs 完整的将整个项目“转述”为另外一个语言的项目时我们就需要对我们的解析、理解、压缩、应用流程进行更加细粒度的设计和优化了。

「半空」主要讨论的就是对于复杂项目的理解提升辅助 LLMs 渐进式多轮迭代构建出一个复杂项目的可行性。核心需要解决的是因为项目规模提升所带来的复杂度以及上下文规模提升和 ABCoder 所制作的对应知识库知识密度跟不上的矛盾。

内核简述

罗马不是一日建成的,参考软件工程标准的项目迭代方式,迭代一个越庞大的项目,引入的标准作业流程和所花费的迭代周期和人力就越多。ABCoder 要想深刻的解析并理解一个大型项目,一口永远吃不成一个胖子。

好消息是构建一个复杂项目的过程是有迹可循的的,ABCoder 需要做的其实就是逆着项目构建的路径,反向解析出项目构建过程中涉及到的不同粒度的知识库。

之后将这些知识库输入 LLMs 驱动的 Workflows,通过构建渐进式的多轮迭代流,将原来的项目以任意编程语言又输出出来,基于对知识库的持续构建,甚至实现为其他语言的项目:语言翻译

意译 or 直译?

相较于给 LLMs 一段代码,让他直接翻译为另外一个语言(直译),「半空」所做的类比下来更像是:帮助 LLMs 理解代码,之后经过抽象和设计结合我们希望它采纳的知识,重写出另外一个语言实现的版本(意译)。

理解和设计

按照 ABCoder 的通用处理流,一个任意庞大的项目我们几乎都可以通过解析、级联压缩的方式构建函数、方法、结构体、变量的语义化映射。但仅仅通过这些散落的信息 LLMs 是没有办法高效的建立一个对这个项目系统深刻的理解。因此我们在做 LLMs 辅助的项目文档梳理应用的时候,就已经开始下意识的做一些单元聚合工作了:通过将某个包(文件/模块)中的函数、方法、结构体、变量语义化含义进一步抽象,得到关于这个包(文件/模块)的语言和框架无关的高层次语义化抽象,按照这个思路,我们可以自底向上抽象,到最终项目维度。

举个直观的例子,对于 Hertz 的项目,任意一个 Hertz 项目在项目维度都能够抽象为形如:这个项目是一个基于 HTTP 应用框架的应用,它或许注册了/a/b/c 路由 (Route)的 GET 方法(Method),关联了某个对应的逻辑(Handler)

仔细分析这个抽象,尝试对其中蕴含的细节进行总结:

  1. 一个基于 Hertz 的 Golang 项目,在经过某个维度的抽象之后,丢掉了大量细节,留下了一些在当前维度的关键信息。在上述例子中,我们得到的抽象已经不关心这个项目具体采用的语言实现和具体涉及到的应用框架了,仅仅需要关注的是 HTTP 框架应用以及 HTTP 应用必备的信息:注册了某个路由,处理了某个业务逻辑。

  2. 通过这层抽象,我们可以将任意一个复杂项目映射出了一个最简单的迭代入口:启动一个 HTTP 应用框架,并注册处理某个 URL 的某个逻辑函数。

  3. 对整个复杂项目的理解过程被我们巧妙的转换为对一个项目自底向上的逐层抽象的过程,如果我们能将这个抽象过程做的足够清晰和准确,对于一个完成抽象的项目来说,我们反过来也得到了一个支持我们至顶向下层层细化的项目构建流。

  4. 理论上通过增加、减少、优化各层级抽象,我们就能不断提升对这个项目深度理解的效果。

多轮的抽象和迭代的本质是项目在不同维度上多语言实现和 ABCoder 抽象语义的不断对齐:

配合语言对应的知识库建设,按照标准抽象块(已归一化逻辑)进行知识检索,分层分模块持续迭代,填充核心逻辑,辅助业务完成项目构建。

实施和测试

当我们通过上述解析和抽象,得到了关于一个项目完整的理解知识,之后就可以至顶向下辅助 LLMs 逐层实现项目的渐进式迭代了。同样,接着上一小结里提到例子来说,我们在这层抽象上做的事情就是:

  1. 根据「HTTP 应用框架」匹配目标语言对应的知识,比如检索出 Volo-HTTP 库的知识(如果我们的目标是将这个应用实现为一个 Rust 项目),之后结合 Volo-HTTP 提供的框架初始化逻辑,拉起一个 Volo-HTTP 的项目
  2. 之后按照本层抽象剩下的描述信息,完成**「/a/b/c** 路由 **和对应处理函数」**的注册
  3. 由于本层抽象并不具备这个处理函数的详细描述信息,因此仅仅需要生成一个空实现的桩函数即可
  4. 之后我们所做的所有变成,二次确认完成了具体实现和对应语义化抽象的对齐

以上即是对一轮迭代核心流程的描述,完成本轮迭代之后即可开启下一层抽象的对齐。之后按照这个流程持续的迭代这个项目。

因为抽象本身会丢掉本层部分细节,而丢掉的这部分细节其实还是保留在抽象前的层级中的,对应迭代路径来说,上一层丢掉的细节一定会在下一层迭代中被补充回来。因此,通过多轮的迭代构建出来的项目,理论上也并不会丢失具体的实现细节。

每一层迭代后都会有一次人工介入时机 —— 即可以及时人工介入修改代码并反馈到后续的翻译轮次中,这也是「半空」的核心能力之一 —— 在这个切面上能够按需的扩展任意的软件测试解决方案,包括时下流行的:LLMs 辅助 UT 生成等技术。等到所有的修改和测试通过之后,即可开启下一层的迭代或者选择直接退出手动接管剩余的翻译工作。

交付内容

作为用户最为关心的部分,「半空」究竟在项目 Go2Rust 转换(存量 Golang 项目改写为 Rust)上帮助我们做到哪些事情呢?其实非常简单,好比将大象装进冰箱,「半空」辅助下的 Go2Rust 自动化迁移也是三个核心步骤:

  1. 打开冰箱门:基于 ABCoder 对存量 Go 项目完成系统解析,产出函数粒度的项目理解原料

  2. 把大象放进去:基于项目理解原料产出将该项目改写为 ****Rust 对应的项目设计文档

  3. 关上冰箱门:基于设计文档中指引的迭代顺序,全自动可控地,产出各层迭代代码

实际上,结合简介中的描述,聪明的小伙伴也许已经发现:「半空」作为一套通用框架,应用面其实并不仅仅局限在 Go2Rust 上,对于任意语言之间的相互转换逻辑上都是完全一致的,区别在于对语言特异性处理和特定语言的知识库构建。「半空」一期重点针对 Go2Rust 场景完成内场的适配和持续打磨,后续如果有对更多语言栈(Python2Go/Java2Go/...)的切换诉求也非常欢迎勾搭~

项目实战举例

一个使用「半空」做 Go2Rust 项目转换的示例

项目介绍

Easy_note 是 CloudWeGo 社区对外提供的一个基于 Hertz 和 KiteX 的实现复杂、功能覆盖度高的业务实战示例项目;其使用 Hertz 提供了若干 API 接口,并在接口实现中通过 KiteX client 发起对下游 KiteX Server RPC 接口的调用。

本次使用「半空」翻译的是其 API 模块,其主要功能列表如下:

  • 用户管理

    • 用户注册 (HTTP 接口 -> RPC 调用)
    • 用户登录 (HTTP 接口 -> RPC 调用)
  • 笔记管理

    • 创建笔记 (HTTP 接口 -> RPC 调用)

    • 查询笔记 (HTTP 接口 -> RPC 调用)

    • 更新笔记 (HTTP 接口 -> RPC 调用)

    • 删除笔记 (HTTP 接口 -> RPC 调用)

涉及到的 Hertz/KiteX 框架相关的核心能力如下:

  • 初始化 Hertz Server
  • 注册 Hertz 路由和 handler
  • 实现 Hertz 自定义中间件(JWT、服务发现)
  • 实现 Hertz 的 handler 逻辑
  • 使用 KiteX Client 调用下游接口

流程说明

从输入原始项目产出 ABCoder 理解知识原料开始,「半空」会结合函数粒度知识原料,自底向上完成整个项目的逐层抽象和理解,之后至顶向下完成重构设计的制定,同时确定项目渐进式构建顺序:从粗粒度 知识映射细粒度 知识映射到最后逐个 Package 的实现,最终完成 Golang 项目到 Rust 项目的渐进式构建(意译)。这个过程中项目构建进度完全由用户掌控,结合人工修改反馈辅助协同,推动项目完成 Go2Rust 迁移落地。

上图提到的 Golang AST / Rust AST 是 ABCoder 在分析仓库代码,将函数、方法、结构体、变量等定义以树形关联出来的数据结构体集合,是一个能够与项目一比一映射的 LLMs 原生 抽象语法树

设计阶段:Package 翻译顺序

根据 ABCoder 解析后的项目原料,「半空」自动化根据 Package 的依赖关系完成了使用 Rust 重构这个项目所需的设计文档的编写,自顶向下得到如下迭代顺序:

  1. "github.com/cloudwego/b…":项目的二进制入口和基础框架搭建
  2. "github.com/cloudwego/b…":HTTP 通用 handler 的实现
  3. "github.com/cloudwego/b…":HTTP 通用 router 的注册
  4. "github.com/cloudwego/b…":HTTP 业务 router 的注册
  5. "github.com/cloudwego/b…":HTTP 业务 handler 的实现
  6. "github.com/cloudwego/b…":请求下游的 RPC 封装
  7. "github.com/cloudwego/b…":通用/业务中间件具体实现

实施阶段:根据设计文档顺序逐步展开

  1. " easy_note/cmd/api "

对应 MR: github.com/cloudwego/b…

main package,主要实现了 HTTP server 的初始化、路由注册调用等能力

Golang 原始实现 「半空」意译效果
main() main()
customizedRegister() customized_register()
常量定义[本轮不实现,只mock] 常量定义[mock实现]
  • 结果评估

    • 目录:

      • 所有 main package 的内容,都生成到 Rust 项目的 /src/bin/main.rs下;后续支持细粒度的文件模块映射
    • 内容:

      • 翻译的函数内容逻辑,基本正确;会将函数的具体过程用顺序表示出来,便于进行修改
    • 错误:

      • Opentelemetry 相关的使用报错;原因:目前还没有注入相关知识;不影响正常逻辑,先注释掉
    • Mock:

      • Main package 会依赖其他包的内容,因此会将其他 package 下的内容进行 mock,确保可以正确编译,但是 mock 的内容不一定完全准确,会在后续迭代完成最终实现;具体 mock 内容可参考上面的示例
  • 修改记录

    • 对 main/init 中涉及 Opentelemetry 的代码注释掉
  • 优化方式

    • 通过补充内场 Opentelemetry 相关缺失知识可以进一步提升完备率和可编译度
  • 数据统计

    • 生成节点完备率=无需改造的节点/生成节点总数

      可编译度=1-修改的代码行数/生成的代码总行数

    • 生成节点完备率: 50%

    • 生成代码可编译度:73%

  1. " easy_note/cmd/api/hertz_handler "

对应 MR: github.com/cloudwego/b…

hertz_handler package 主要实现了一个 ping handler,用于处理 ping-pong 请求

Golang 原始实现 Rust 意译效果
Ping() ping()
  • 结果评估

    • 目录:

      • 所有 Golang cmd/api/hertz_handler包的内容,都生成到 Rust 项目的 /src/cmd/api/hertz_handler/mod.rs
    • 内容:

      • 翻译的函数内容逻辑完全正确
    • 错误:

      • Cargo.toml 里没有加入 "serde_json" 依赖,导致报错
    • Mock:

      • 没有尝试参考 hertz 去 mock 状态码和 utils.H,而是自行利用 volo-http 框架的能力完成响应返回
  • 修改记录

    • 增加 "serde_json"
  • 优化方式

    • 在 cargo.toml 知识里增加通用、常用的依赖
  • 数据统计

    • 生成节点完备率=无需改造的节点/生成节点总数

      可编译度=1-修改的代码行数/生成的代码总行数

    • 生成节点完备率:100%

    • 生成代码可编译度:95%

  1. " easy_note/cmd/api/hertz_router "

对应 MR: github.com/cloudwego/b…

hertz_router 包主要实现 Hertz 路由的总体注册逻辑,调用 idl 生成的路由

Golang 原始实现 「半空」意译效果
GeneratedRegister() generated_register()
Register()[本轮不实现,只mock] register()[mock]
  • 结果评估

    • 目录:

      • 所有 Golang cmd/api/hertz_``router包的内容,都生成到 Rust 项目的 /src/cmd/api/hertz_router/mod.rs
    • 内容:

      • 翻译的函数内容逻辑完全正确
    • 错误:

      • 没有正确地将下层依赖 pub 出来,而是直接使用了依赖路径
    • Mock:

      • IDL 生成的路由注册部分,将其 mock 出来
  • 修改记录

    • 将 "hertz_router/demoapi" mod pub 出来
  • 优化方式

    • 在生成代码后对新增内容做一次解析和关联
  • 数据统计

    • 生成节点完备率=无需改造的节点/生成节点总数

      可编译度=1-修改的代码行数/生成的代码总行数

    • 生成节点完备率: 100%

    • 生成代码可编译度:88%

  1. " easy_note/cmd/api/hertz_router/demoapi "

对应 MR: github.com/cloudwego/b…

hertz_router/demoapi package 主要实现了具体了路由注册(idl 映射)以及 Hertz 中间件的定义

Golang 原始实现 「半空」意译效果
Register() register()[路由注册有问题,需要 check & 修改]
rootMw() root_mw()[包含了中间件里的 mock 实现]
mw 定义 mw 定义
CreateUser[本轮不实现,只mock] create_user[mock]
  • 结果评估

    • 目录:

      • 所有 Golang cmd/api/hertz_``router/demoapi包的内容,都生成到 Rust 项目的 /src/cmd/api/hertz_router/demoapi/mod.rs
    • 内容:

      • register(): 路由注册的逻辑对应上了,但是实现不对;生成的路由没有和原始的路由一比一映射成功,是根据函数的描述自行生成的路由:需要用户手动将路由修改正确,参照生成的用法很快就可以实现

      • root_mw():

        • 能够以注释的形式描述出来 root_mw 里所需要做的内容,但是没有正确实现。因为 volo 里没有这样把多个中间件组成一个切片的操作:需要用户自行补充实现
        • 没能实现 recovery、RequestId、Gzip 的中间件逻辑;主要原因是无法推测出这些功能在 rust 里的实现方式:需要用户自行补充实现
      • 其余的中间件均正常

    • 错误:

      • register() 的路由注册逻辑不对,优化思路如下:

        • 这部分是 IDL 映射的内容,本身就被拆的比较细;后续会做框架之间的 IDL 映射
        • 增强函数的细节描述
    • Mock:

      • Mock 实现了所有的handler内容,这部分没什么问题
  • 修改记录

    • 对路由逻辑进行重新梳理和注册
    • 对 recovery/request_id/jwt 中间件的逻辑进行实现(ps. 示例还未实现,暂时注释掉)
    • 删除/添加一些依赖信息
  • 优化方式

    • 增强细节逻辑的总结&实现能力
  • 数据统计

    • 生成节点完备率=无需改造的节点/生成节点总数

      可编译度=1-修改的代码行数/生成的代码总行数

    • 生成节点完备率:62%

    • 生成代码可编译度:76%

  1. " easy_note/cmd/api/hertz_handler/demoapi "

对应 MR: github.com/cloudwego/b…

  • Hertz_handler/demoapi package 主要实现了具体的 HTTP 接口实现,下面使用 "create_note" 作为展示
Golang 原始实现 「半空」意译效果
CreateNote() create_note()
SendResponse() send_response()
rpc CreateNote[本轮不实现,只mock] rpc create_note[mock]
ErrNo[本轮不实现,只 mock] ErrNo[mock]

handler 这轮翻译完,出现的代码报错较多,主要原因如下:

  1. 是代码量本身比较大,同样错误报错多次

  2. handler 里涉及了一些业务逻辑以及业务在 golang 里的特定的用法,LLMs 不能很好转换

以下都以"create_note" 接口为例,进行结果评估

  • 结果评估

    • 目录:

      • 所有 Golang cmd/api/hertz_``handler/demoapi包的内容,都生成到 Rust 项目的 /src/cmd/api/hertz_handlers/demoapi/mod.rs
    • 内容:

      • create_note(): 能把原 create_note 的逻辑按顺序进行实现,包括 获取参数、发起调用、返回响应等
      • send_response(): 基本能实现出原接口的含义,但是错误较多,图里展示的是手动修改过的
    • 错误:

      • create_note(): 逻辑是正确的,主要有以下错误

        • mock 的结构体,没有带 #[derive(Debug, Deserialize)]需要用户补
        • send_response() 的调用无法对齐,一直报错
        • 获取请求上下文的时候,可能会有误传参
      • send_response(): 整体逻辑是对的,但是不会用 volo-http 的写响应方式

    • Mock:

      • 直接 mock 的内容基本都正确不需要修改
      • 没有去对二级依赖进行mock,导致会有些编译错误;例如,当前接口依赖了 "rpc/create_note",其又依赖了 "NoteDate" 类型,这个没有进行实现
  • 修改记录

    • send_response 的逻辑重新实现
    • 修改 handler 的调用逻辑,以及一些 ctx 上下文传参的问题
    • 增加/删除一些依赖信息
  • 优化方式

    • 补充 volo-http 的请求/响应相关操作示例,指导 LLMs 生成更准确的 SDK 使用姿势
  • 数据统计

    • 生成节点完备率=无需改造的节点/生成节点总数

      可编译度=1-修改的代码行数/生成的代码总行数

    • 生成节点完备率:14%

    • 生成代码可编译度:88%

至此,我们就完成了 "github.com/cloudwego/biz-demo/easy_note/cmd/api" 这个 moudle 的全部翻译,用户在 check 完整个项目后,即可以编译 & 运行项目。

总结

整体意译效果说明

  • 函数翻译完备性

完备性说明:完全无需人工介入的函数统计为完备函数

package 生成函数的个数 完备函数的个数 完备率
easy_note/cmd/api 4 2 50%
easy_note/cmd/api/hertz_handler 1 1 100%
easy_note/cmd/api/hertz_router 1 1 100%
easy_note/cmd/api/hertz_router/demoapi 13 8 62%
easy_note/cmd/api/hertz_handler/demoapi 7 1 14%
  • 代码可编译度

可编译说明:相对于整体生成代码行数,人工介入修改的代码行数占比,需要修改的代码越少,可编译度越高

package 生成函数的行数 人工修改的代码行数 可编译度
easy_note/cmd/api 106 28 73%
easy_note/cmd/api/hertz_handler 19 1 95%
easy_note/cmd/api/hertz_router 9 1 88%
easy_note/cmd/api/hertz_router/demoapi 173 38 76%
easy_note/cmd/api/hertz_handler/demoapi 254 30 88%

整体上,通过知识库的持续建设和关键知识的补齐,「半空」在完备性和可编译度上也会随之持续提升。

语言学习和项目迁移

在这个过程中,结合「半空」为我们生成的 Rust 项目设计文档,从整体项目的角度出发,逐步对每个包进行深入理解、翻译与确认。这一过程条理清晰、循序渐进地将一个 Golang 项目从零构建为一个 Rust 项目。同时,我们一同参与项目构建的每一个迭代,「半空」每一个迭代生成的代码完全遵循内场和业内 Rust 项目编写的最佳实践,这不仅帮助我们深刻理解整个项目,同时也为学习一门新语言提供了极大的支持。通过这种逐步渐进迁移的方式,我们能够不断深入学习并掌握 Rust 语言及项目本身,最终成功完成项目的转型。

ByteHouse技术详解:基于OLAP构建高性能GIS地理空间能力

在数字化时代,地理空间分析(Geospatial Analytics)成为辅助企业市场策略洞察的重要手段。无论是精准广告投放,还是电商物流的效率优化,都离不开对地理空间数据的查询、分析和可视化处理,以便助力企业更好决策。

以一家连锁咖啡店为例:

该店想要在新城市开设分店,并希望确保新店铺的位置能够最大化利润。

首先,商家通过收集新城市的地理数据,包括人口分布、交通流量等,建立了一个详细的地理信息数据库。然后,商家利用空间数据分析工具,对这些数据进行了深入分析。

通过人口分布数据,商家发现新城市的一些区域人口密集,潜在顾客群体较大。同时,交通流量数据显示,某些区域的交通流量较大,意味着这些区域的顾客流动性较高,有利于店铺的曝光和吸引顾客。

此外,商家还分析了同行情况竞争对手的位置,以避免在已有众多同类型店铺的区域开设分店。空间数据分析帮助商家识别了那些既有足够潜在顾客,又相对较少竞争者的区域。

基于这些分析结果,商家最终确定了新店铺的位置。开设分店后,由于选址精准,店铺迅速吸引了大量顾客,销售额和利润均实现了预期目标。

以上案例离不开对地理空间数据库的支持。一些传统的地理信息系统数据库具备丰富的地理空间对象结构、成熟的空间索引能力,在导航、旅游、智能城市等典型应用场景中被广泛使用。

但随着实时分析报表等OLAP市场的扩大,地理空间分析也作为新的增值特性被业界几大OLAP主流产品所推广。OLAP+GIS能力在满足用户地理空间数据分析的基础上,还能在数据体量大、实效性要求高的情况下,满足业务高性能查询的需求。

作为火山引擎推出的一款OLAP引擎,ByteHouse近期发布了高性能地理空间分析GIS能力,为位置洞察、人群圈选等场景提供高性能地理数据分析服务。本篇内容将从技术实现角度,详细介绍ByteHouse如何集成GIS能力,并通过benchmark测试,展示ByteHouse与市场同类产品相比(ClickHouse、StarRocks、PostGIS、DuckDB)的性能情况。

应用场景和价值

位置洞察: 例如,在给定中心点的情况下,展示半径X公里内的圆内其他商家的同一商品的客流分布、经营情况等,有助于帮助商家客户洞察竞争对手情况,为定价策略和市场定位提供数据支持。

作战地图: 给定特定多边形,观察多边形内部商家的供给和客流量,为即时零售业务的配送优化提供决策依据。例如:生活服务的即时零售业务需要观察实时的配给。

经过我们对行业上相关业务场景的需求分析,商家或者销售代理等客户需要的是一种“对某个地理空间(多边形/圆)内的对象进行多种业务维度的分析和决策能力”。从整个执行链路来看,链路不仅含GIS的二维空间数据筛选,还有经典OLAP的聚合和关联分析等逻辑,因此可以总结出一层GIS+OLAP链路的抽象。从性能优化角度来看,OLAP优化器有必要去结合GIS的特性来进行适配,提升端到端的总体性能。

详细介绍

在关键性能层面,ByteHouse GIS在列式小批组织的数据结构上引入RTree等二维空间索引能力,并在CPU硬件层面实现了二维空间函数的性能优化,整体提升了端到端性能。在功能层面,兼容OGC标准,支持导入标准GIS文件格式,目前已支持超过50个主流的空间函数。更值得一提的是,我们还在探索在我们自研的优化器上结合GIS特性适配,如在高效的多表关联上适配GIS等,以及GPU硬件层面优化二维空间函数。

二维空间索引

回顾业务场景:给定一个查询窗口(通常是一个多边形或圆),返回包含在该查询窗口中的物体。

如果要提升查询性能,读取的数据量通常是比较关键的,那取决于:

1)数据的排序方式 2)数据读取的粒度 3)索引

社区ClickHouse数据组织

ByteHouse 是火山引擎基于开源 ClickHouse 进行了深度优化和改造的版本。 ClickHouse 社区版直接按照Order By latitude, longtitude里面的纬度进行排序,再按照经度排序。

因为经度上相距很远的数据可能被放到一个mark,而查询是一个多边形和圆,查询的模式和数据的组织不匹配从而造成严重的读放大问题,导致数据局部性较弱。

微信截图_20241226100633.png

ByteHouse空间索引:Google S2 + R tree

ByteHouse GIS 通过使用Google S2 [3]库将所有的经纬度点从二维转换转换成一维,并排序。排序后的经纬度点效果如下图:

图片来源:[3]

由于ByteHouse的数据是按照列式存储,相比于传统的行级别索引,我们会对S2排序后的经纬度数据,先按照小块粒度切分,再利用RTree来索引每个小块数据。这样,基于小块粒度的RTree索引的存储开销更小,加载和查询效率更高。给定一个查询的多边形或圆,RTree能快速索引到匹配的数据块。由于每个数据块内的经纬度数据是按照二维层面聚集,这样使得相邻的点在二维空间上更加紧密,数据局部性更好。

ByteHouse GIS索引结构

针对某个具体场景中给出的一个圈选范围,需要返回范围内的所有POI (Point of Interest)点。下面两幅图分别展示了传统经纬度排序方式(Order By latitude, longitude)和ByteHouse GIS索引排序方式(Order By point)的圈选效果。其中,图中黑色的框代表了所有数据块,红色部分代表了圈选命中的数据块。

从结果中看出,传统经纬度排序命中的范围会横跨很广的纬度,造成读取许多无用的数据。而按照ByteHouse GIS索引搜索出的数据块只集中在北京地域,正好满足圈选所需的最小数据块集合。

传统经纬度排序方式的搜索效果

ByteHouse GIS排序方式的搜索效果

兼容OGC标准

数据类型

按照OGC标准,新增7种几何类型,包括Point、LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, GeometryCollection。

存储层面上,传统GIS数据库(例如,PostGIS)将几何数据序列化为Blob类型,读取时需要额外花费反序列化的开销。而ByteHouse GIS则按照数值数组和列式方式存储,减少存储量、序列化和反序列化开销。

空间函数

功能上,ByteHouse GIS目前已支持超过50个通用的空间函数,下面表格列举了几大函数分类。另外,我们针对个别高频使用的空间函数进行了基于列式数据存储格式的性能优化。

微信截图_20241226100839.png

存量数据迁移

同时,ByteHouse GIS也支持常见数据格式的导入与导出,包括WKT、WKB、GeoJson、ShapeFile、Parquet、CSV和Arrow等文件格式。

Benchmark 测试

标准NYC Taxi数据集

为了说明性能效果,我们基于两个关键的 GIS 函数,使用 NYC Taxi 数据集,选取纽约的 3 个地理区域,将ByteHouse、ClickHouse、StarRocks、PostGIS、DuckDB进行了性能对比(以上对比的版本参照发文日期的最新版本)。

在本次测试中,我们选取了两个关键的 GIS 函数:ST_DistanceSphereST_Within;并使用 NYC Taxi 数据集(Size:21GB;条数:169,001,162),数据集将纽约拆分成多个地理区域(比如 Brooklyn,Manhattan),本实验选取其中 3 个不同大小的地理区域(按照过滤度区分:zone 1、zone 2、zone 3)进行了性能对比。

  1. ST_Within 函数性能对比:在 ST_Within 函数的测试中,从查询延迟来看,OLAP引擎的整体查询延时低于1s,由于二维空间索引和向量化的数据处理方式,ByteHouse查询延时最低;当前版本的DuckDB由于没有空间索引,同时采用了BLOB的存储方式,数据扫描和反序列化开销比较大,查询性能不好;采用行存的PostGIS在大范围搜索的情况下(zone3),虽然有索引加持,依然会有较重的读放大,查询延时超过6s。从每秒吞吐量来看,ByteHouse通过索引降低了数据读取和反序列化开销,展现出明显优势,其次为PostGIS,在小范围搜索(zone1和zone2)情况下表现优秀。

ST_Within函数性能对比

ST_Within每秒处理空间查询数

  1. ST_DistanceSphere 函数性能对比:在 ST_DistanceSphere 函数的测试中,在处理相同数据集和查询时,ByteHouse具备二维空间索引过滤和向量化计算的优势,性能控制在0.1s以内。ClickHouse和StarRocks同样具备较好的0.1s-1s内的较好性能表现。

ST_DistanceSphere 函数性能对比

基于标准数据集的测试结果来看,对比传统的PostGIS:

  • ByteHouse GIS将OLAP和GIS结合了起来。在OLAP层面,ByteHouse对比PostGIS已经有计算优势。
  • 在GIS层面,空间数据对象按照列的方式存储,而非序列化成字节数组,在存储上能够做到更加紧凑并节省空间,在计算上能够充分发挥向量化的优势。
  • 特别是在空间函数层面,可以利用硬件的并行化能力提速。

对比社区ClickHouse:

  • ByteHouse GIS兼容OGC标准,场景上能够水平替换之前PostGIS的场景。
  • 另外,空间索引能力可以大大减少ClickHouse的读放大的现象。
  • 还有,ByteHouse自研的优化器同样具备适配GIS特性的能力。

业务数据集

在电商场景中,ByteHouse GIS能力不仅满足平台商家运营快速分析商家经营状态、管理商家的需求,还将数据读取量减少超过50%,进一步降低了磁盘IO以及计算带来的CPU开销。

总结

本文具体拆解了ByteHouse GIS能力的技术实现方案,并将ByteHouse、ClickHouse、StarRocks、PostGIS、DuckDB五款数据库产品的性能进行分析和比较。

结论总结如下:ByteHouse在ST_DistanceSphere 函数及ST_Within 函数的查询延迟低于其他产品,查询吞吐量更高,具备比较明显的性能优势。

需要注意的是,性能测试结果取决于多个因素,在实际应用中,需要综合考虑各种因素,如数据规模、可扩展性、易用性、稳定性、安全性以及是否需要与其他系统集成等其他因素进行综合选择,并对数据库进行合理的配置和优化,以获得最佳的性能表现。

对于专注于地理空间数据分析的项目,PostGIS能提供了全面的地理空间功能支持,是一个比较好的选择。然而,如果地理空间数据只是大数据分析的一部分,且如果性能是首要考虑因素,那么ByteHouse、ClickHouse、StarRocks、DuckDB是合适的选择,其中ByteHouse GIS 功能不仅提供了高性能的地理空间分析能力,还具有易于使用、实时分析和云原生等特点,这使得企业可以更灵活、更高效地利用地理空间数据。

参考

  1. PostGIS: postgis.net/
  2. OGC: www.ogc.org/standard/sf…
  3. Google S2: s2geometry.io/
  4. Geos: libgeos.org/
  5. clickhouse.com/docs/en/sql…
  6. Cuda: developer.nvidia.com/cuda-toolki…
  7. github.com/rapidsai/cu…
  8. github.com/arctern-io/…
  9. halfrost.com/go_spatial_…

ROG:高性能 Go 实现

本文根据字节跳动服务框架团队研发工程师在 CloudWeGo 技术沙龙暨三周年庆典中演讲内容《ROG——高性能 Go 实现》整理。

作者|不愿意透露姓名的小刘市民

ROG 之缘起

ROG 的诞生是因为我们一部分业务使用 Rust 重写之后,获得了非常好的收益,比如 AVG、CPU、MEM、P99,这些数据表现非常好,大约节省了接近 50%的 CPU,内存大大降低。

这个性能数据让人眼红,因此团队考虑既然 Rust 有这么好的性能,我们有没有办法提升一下用户在 Go 上的性能?

图片

在和一些用户的对接中我们发现,让用户把 Go 业务通过 Rust 重写,难度其实非常大。很多用户会抱怨 Rust 的一些问题让他们很痛苦,比如,Rust 生命周期太复杂,泛型系统太复杂,报错看不懂,编程速度慢等等。因为这一系列问题,所以让用户把原来的 Go 项目通过 Rust 重写,对于用户来说是很难推动的事情。

于是,我们就有了一个大胆的想法,如果我们可以像使用 Rust 那样的编译技术去生成性能更好的可执行文件,同时使用 Rust 重写 Go 的 Runtime 和 GC 这两个核心组件,再通过几乎零开销的 FFI(Foreign Function Interface) 方式来支持 Rust 和 Go 之间的互调,是不是可以让用户 Go 的源码也能达到接近 Rust 的性能。这就是我们的初衷,因此有了 ROG 这个项目。

ROG 进展

我们目前测试了一些简单的场景,比如快排和二分、Simple Lisp。这些都是通过 time 命令来计算两个二进制文件执行所需要的时间。目前在快排、二分上,Go 的执行需要 5.97s,ROG 的执行需要 4.12s,在 Simple Lisp 这个项目上,Go 需要 8.17s,ROG 执行只需要 7.09s。

图片

从以上几个基本数字来看,在一些简单的场景下,ROG 会比 Go 性能好很多。但这只是一些非常简单的 case。如果面对一些非常复杂的 case 呢?比如在复杂的微服务场景下,ROG 会有怎样的性能领先?ROG 在上个季度刚好能够支撑 Kitex Benchmark 跑起来,目前我们完成了一次压测。

图片

我们使用 Kitex 官方的 Benchmark 工具完成了简单的 RPC 调用测试(github.com/cloudwego/k… 100,测试包大小 1024kb 的体积。在这个测试中,Go 的 QPS 可以达到 27W,ROG 28W。虽然 ROG 的 QPS 只比 Go 领先了一点,但是 P99 上有很大的提升。我们在测试过程中发现了 ROG 还有很多可以挖掘能力,只是还需要进一步优化。

架构设计

通过刚才几个性能场景测试,我们发现 ROG 相比 Go 在不同的场景下,多多少少有一些领先。但是为什么 ROG 相比于 Go 会有这样的领先呢?早期我们其实也经历过 ROG 测试结果比 Go 还差 50%的状态。所以想先给大家介绍一下我们的设计架构。

图片

从图中可以看出,首先会有一个 ROG 的前端来处理用户的 Go 源码,在前端经历 Parser 解析后生成 AST(Abstract Syntax Tree),做符号解析,每个函数,每个类型的符号。然后进行类型检查,分析出函数的签名以及每个变量的类型。这是一套非常常见的前端处理流程。

在经历这个过程之后,会产生一个中间语言叫做 MIR(Rust's Mid-level Intermediate Representation),之后会基于 MIR 去做一些前端时的优化,比如编译时计算、常量传播计算、逃逸分析(能够分析出哪些变量应该被逃逸到堆上去)、Inliner、SROA,以及对于特定 Go 函数的优化。

在这些优化算法处理之后,会生成一份 LLVM IR(Intermediate Representation),之后把它交给 ROG 后端。ROG 后端是我们自己魔改的一个 LLVM 版本。在 LLVM codegen 阶段我们给每个函数插入了一些对应的 Stack Check 以及对应的 STW(Stop The World) Checkpoint 指令,同时生成相应的 GC Barrier。

优化好之后就生成一份比较高质量的二进制代码了。这是对于 Go 语言的处理,而对于 Go 的 Runtime & GC 这部分,我们基本上完全是重写的。通过 Rust 重写之后,我们把这些代码通过自己维护的一个 Rust 版本去构建、打包好,调成对应的 LLVM 文件,最后和用户的 Go 代码连接起来,形成一个最终的二进制文件。这就是我们的编译流程。

收益来源

这个编译架构为什么相比 Go 或多或少有些性能优化呢?有哪些领先点?其实领先点主要来源于三个部分。

图片

第一部分,编译优化。因为 ROG 利用了 LLVM 积累多年的编译优化算法,能够生成一些性能更好的代码,而 Go 的编译优化会为编译速度做出一定牺牲。

第二部分,ROG 提供了跨语言 LTO(Link Time Optimization) 以及 FFI,通过几乎零开销的方式调用 Rust 提供的方法,因此在一些需要更高性能的场景,用户可以使用 Rust 开发,由 ROG 进行编译并进行调用。而 Go 对于 FFI 会使用 CGO,并且 CGO 会存在一些 overhead。

第三部分,Runtime&GC。ROG 完全使用 Rust 重写,再通过上面提供的 FFI 来保证调用的性能,而 Go 的 Runtime & GC 则是完全使用 Go 原生实现的。单纯从语言的表达能力上限来说,Go 远不如 Rust,所以如果我们通过 Rust 来重写 Runtime & GC 这两部分组件,理论上会比 Go 拥有更好的性能。

面临的挑战

介绍完性能来源之后,可能很多人会有疑问,貌似我们的主要性能受益都是来自于 LLVM。LLVM 本身优化已经做得很好了,我们做的是不是就是非常简单地把一个 Go 源码翻译到 LLVM 就行了呢?

其实整个事情并没有那么简单,在这一年里,我们踩过非常多的坑。以下举几个简单的例子。

Go Runtime

如果大家之前了解过 TinyGo,就会发现 TinyGo 的思路和 ROG 非常接近——TinyGo 也是把 Go 的源码给翻译到 LLVM。我们可以回想下在使用 TinyGo 的时候遇到过什么问题。

首先,TinyGo 需要用户手动通过 runtime.Gosched 这个函数来进行协作调度,所以它对用户代码是有影响的。如果用户没有在关键的地方去插入这个函数调度,会对它的调度产生影响。另外,TinyGo 本身也不支持多线程,并且缺少相应的 channel timer reflect 等 lib 的支持。

而 ROG 把这些问题都解决了,ROG 会在编译阶段插入代码,完成协作式调度,并且 ROG 设计的本身也是为了高性能,所以自然会对多线程进行支持,并且 ROG 对于 channel timer reflect 全部都重写。对于我们来说,解决 TinyGo 的不足的过程也相当艰难,毕竟重写整个 Runtime & GC 等是一个非常大的工作量。

图片

Safety FFI

假设如果我们要在 Go 提供 FFI,当用户写出这样的代码会发生什么事情?

图片

左边这张图是用户写的一份 Go 代码,里面有函数。rog_test(a *int32) 这个函数可能就是 FFI 提供的一个外部函数。如果用户直接去调用这个外部函数,而 rog test 本身是由 Rust 实现的,如右图,当我们写出这样的代码的时候,会发生什么事情?

因为 rust_tup 被 Rust Allocator 管理,Go GC 无法扫描到这个变量,所以这个变量 a 也无法被 GC 扫描到,而 “a” 这个变量是被 Go 的 Allocator 管理的,所以如果  a 无法被 GC 扫描到,那么 a 就会被 free 掉。但是这个时候,`` rust_tup 仍然会持有变量 a 的指针,在 Go 那边相当于是一个对外内存引用了 Go 的一个对象,但是因为 Go 扫不到这个对象,所以这个对象就被 free 掉了,但是对外内存仍然引用这个指针。

当我们提供 FFI 的时候,很有可能会面临这样的情况。这种情况该怎么处理?在 ROG 这边,我们就会通过一个模改的 Rust 编译器,提供一个 Managed Chekcer 去限制用户写出这样的代码,在编译器阶段保证用户不会写出这样的代码,保证 FFI 的安全性。这是 ROG 解决这个问题的思路。

Roadmap & 未来规划

CGO

目前 ROG 虽然能跑过 Kitex Benchmark,并在内部一些服务上做了测试,但它仍有很多功能需要改进,比如 CGO。CGO 是 Go 语言用来提供 FFI 的一种方案,但 ROG 的 FFI 是通过一种非常简单粗暴的方式提供的。目前 ROG 的 FFI 需要用户手工去标记 ROG,写上 rog:linkname 标记。这样我们在链接时才能链接上对应的符号。而 CGO 可以让用户简单的直接在 Go 文件的一个注释里写上 C 代码 import C,通过 import c 这个 package 来进行调用。

从 FFI 来说,CGO 会比现在的 ROG 方便很多,而且已有很多的开源库,以及字节内部一些服务,他们也在使用 CGO。我们在未来会支持 CGO,兼容 CGO 的表达方式,提供 ROG 需要的 FFI,生成 ROG 需要的 FFI 代码进行调用。

宏/编译器生成代码

Rust 宏在我看来是一个非常强的功能,因为 Rust 宏可以简单地在每个 Rust 进行标记,申明这个结构可以提供 Serialize(序列化)和 Deserialize(反序列化)这两种方法。这样就可以在编译时为它生成序列化和反序列化的代码,直接进行调用,而不需要像 Go 原始的 JSON,它有反射开销。而这种反射开销在需要高性能的序列化场景会有很大的性能开销。为了解决 Go 的反射开销,sonic 做出了 JIT 方案,而 JIT 对开发 sonic 的开发者来说,负担是非常大的。

图片

那么如果我们可以把 Rust 宏的理念引入到 ROG 中,会有什么样的体验?

首先,更好的开发体验。以 Kitex 举例,我们可以直接在编译时,通过宏为每个 IDL 生成 clint 的代码,这样就不需要用户去手动调用一些 main 去生成。

其次,更高效的序列化。像 JSON 这种序列化,我们可以通过类似 Rust 宏的方式在编译时生成好序列化和反序列化所需要的代码,直接调用,这样就可以省掉反射的开销。

图片

关于宏带来的案例,我们还在继续探索中,之后我们会基于宏做一些更好更方便的尝试。这也是我们对于宏的规划。但是不得不提到的是,宏的出现会对 Go 本身有一定的影响,因此可能只会通过注释的方式去提供,保证对 Go 语法的兼容性;并且只会在 JSON 等序列化这些地方进行一些替换,保证用户的开发体验不会受到影响。

开源

ROG 未来肯定会进行开源,并且贡献到社区。目前我们的想法是 2024 年先在公司内部完成一些业务的试用,能够稳定地上生态环境,并且能够取得一定的收益。在这些都稳定并且处理好 Go 本身大部分特性问题之后,才会将其开源。因此如果顺利,最早可能会需要等到 2025 年的第二季度才会去准备开源工作。欢迎大家保持关注~

-END-

项目地址

GitHub:github.com/cloudwego

官网:www.cloudwego.io

三周年演讲 PPT 下载链接:github.com/cloudwego/c…

❌