阅读视图

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

抖音 renderD128 系统级疑难OOM分析与解决

  1. 背景

抖音长期存在renderD128内存占用过多导致的虚拟内存OOM,且多次出现renderD128内存激增导致OOM指标严重劣化甚至发版熔断。因受限于闭源的GPU驱动、现场有效信息极少、线上线下的Native内存检测工具均未能检测到相关内存分配等原因,多个团队都进行过分析,但一直未能定位到问题根因,问题反馈到厂商也一直没有结论。

以往发生renderD128内存激增时,解决办法往往都是通过二分法去定位导致问题MR进行回滚(MR代码写法并无问题,仅仅是正常调用系统Api),但是回滚业务代码会影响业务正常需求的合入,也无法从根本上解决该问题,而且每次都会消耗我们大量人力去分析排查,因此我们有必要投入更多时间和精力定位根因并彻底解决该问题。在历经数月的深入分析和排查后,我们最终定位了问题根因并彻底解决了该问题,也取得了显著的OOM收益,renderD128导致发版熔断的问题再也没有发生过。

接下来,将详细介绍下我们是如何一步步深入分析定位到问题根因,以及最终如何将这个问题给彻底解决的。

  1. 问题分析

  1. 问题特征

主要集中在华为Android10系统, 表现为renderD128内存占用过多。

机型特征: 联发科芯片、PowerVR GPU

   如:华为y6p/华为畅享e

OS version: Android 10(主要),少量Android 8.1.0/9.0/11.0/12.0

abi: armeabi-v7a, armeabi

崩溃原因: 虚拟内存耗尽,主要由于/dev/dri/renderD128类型的内存占用过多(1G左右)

  1. 问题复现

我们根据抖音过往导致renderD128内存激增的MR,找到了一种能稳定复现该问题的办法“新增View,并调用View.setAlpha会引发renderD128内存上涨”。

复现机型:华为畅享10e(Android 10)

测试方式:

  • 对照组:新增10个view,每个view设置背景色,不设置透明度,查看绘制前后内存变化
  • 实验组:新增10个view,每个view设置背景色,并设置alpha为0.5,查看绘制10个view前后renderD128类内存变化

测试结果:

  • 对照组: 新增View,renderD128内存无变化

  • 实验组: 新增View,renderD128内存出现显著上涨,且每增加1个View,renderD128内存增加大概25M

结论: 如果view被设置了透明度,绘制时会申请大量内存,且绘制完成不会释放

  1. 监控工具完善

我们在线上线下都开启了虚拟内存监控,但是均并未找到renderD128相关的内存监控信息(分配线程、堆栈等)

  1. 关键接口代理

以下是我们Hook相关接口开启虚拟内存监控的情况

接口 是否可以监控 备注
mmap/mmap64/mremap/__mmap2 监控不到
ioctl 仅监控到一个命令,但该命令并没有映射内存操作 1. 命令调用前后renderD128相关内存并无变化
  1. 这个命令相关的ioctl调用频繁 | | 上层接口 | 播放视频时没有监控到这些函数的调用(比较奇怪,讲道理应该是有调用的) | | | open | 并未监控到设备文件打开的时机和路径 | |

根据hook ioctl接口获取到的相关堆栈(虽然ioctl操作并没有影响内存,也可通过堆栈找到关键so库)

  • libsrv_um.so

  • gralloc.mt6765.so

  1. 调查内存映射方式

  1. 从内核源码中寻找线索

由于关键接口代理均无法监控到renderD128相关的内存申请,此时猜想:可能是在内核中分配的内存?

于是找到了华为畅享e的内核源代码,阅读其中DRM驱动的相关代码

找到了唯一一个ioctl调用对应命令(0xc0206440)的定义和参数数据结构。

根据参数的数据结构,很容易理解驱动应该是根据传入的bridge_id和bridge_func_id来决定做何操作的。(根据堆栈其实也能大致推测每个id对应的操作,但此处暂时不对其进行研究)

但除此之外,在内核代码中并没有找到“内存是在内核中分配的”证据,猜测应该还是用户空间申请的,比较有“嫌疑”的库是libdrm.so、libsrv_um.so和gralloc.mt6765.so

  1. 从驱动和关键so库中寻找线索
  • libdrm库

DRM

DRM是Linux内核层的显示驱动框架,它把显示功能封装成 open/close/ioctl 等标准接口,用户空间的程序调用这些接口,驱动设备,显示数据。libdrm库封装了DRM driver提供的这些接口。通过libdrm库,程序可以间接调用DRM Driver

但libdrm库中的drm_mmap是调用 mmap__mmap2(都是监控中的接口)

#if defined(ANDROID) && !defined(__LP64__)
extern void *__mmap2(void *, size_t, int, int, int, size_t);

static inline void *drm_mmap(void *addr, size_t length, int prot, int flags,
                             int fd, loff_t offset)
{
   /* offset must be aligned to 4096 (not necessarily the page size) */
   if (offset & 4095) {
      errno = EINVAL;
      return MAP_FAILED;
   }

   return __mmap2(addr, length, prot, flags, fd, (size_t) (offset >> 12));
}
#else
/* assume large file support exists */
#  define drm_mmap(addr, length, prot, flags, fd, offset) \
              mmap(addr, length, prot, flags, fd, offset)
  • mesa3D

mesa3D

mesa3D中是通过调用libdrm库中的接口,间接调用DRM Driver的

gitlab.freedesktop.org/mesa/mesa

在mesa的源代码中找到了类似libsrv_um.so中PRVSRVBridgeCall的函数 pvr_srv_bridge_call

static int pvr_srv_bridge_call(int fd,
                               uint8_t bridge_id,
                               uint32_t function_id,
                               void *input,
                               uint32_t input_buffer_size,
                               void *output,
                               uint32_t output_buffer_size)
{
   struct drm_srvkm_cmd cmd = {
      .bridge_id = bridge_id,
      .bridge_func_id = function_id,
      .in_data_ptr = (uint64_t)(uintptr_t)input,
      .out_data_ptr = (uint64_t)(uintptr_t)output,
      .in_data_size = input_buffer_size,
      .out_data_size = output_buffer_size,
   };

   int ret = drmIoctl(fd, DRM_IOCTL_SRVKM_CMD, &cmd);
   if (unlikely(ret))
      return ret;

   VG(VALGRIND_MAKE_MEM_DEFINED(output, output_buffer_size));

   return 0U;
}

同时发现了BridgeCall的相关id定义

通过提交的commit了解到这部分代码是为powerVR rogue GPU增加的驱动

commit链接:gitlab.freedesktop.org/mesa/mesa/-…

存在renderD128内存问题的机型使用的GPU也是PowerVR GPU,那么内存申请关键逻辑应该确实就在libsrv_um.so和gralloc.mt6765.so中

Huawei Y6p - Full phone specifications

  • libsrv_um.so与gralloc.mt6765.so

暂时无法在飞书文档外展示此内容

暂时无法在飞书文档外展示此内容

奇怪的是,libsrv_um.so中只有munmap的符号,却没有mmap的符号(gralloc.mt6765.so同样没有)

这比较不符合常理,一般来说,mmap和munmap都是成对出现的,猜测有三种可能性:

  1. 在其他库中mmap

  2. 用其他方式实现mmap操作

    1. 使用dlsym拿到mmap等的符号,再调用 ❌

      1. 这种情况,使用inline hook是可以监控到的
    2. 调用ioctl实现mmap操作 ❌

      1. 并未监控到
    3. 直接使用系统调用 ✅

      1. 在libsrv_um.so中发现调用了syscall,系统调用号是0xC0(192),正是mmap的系统调用号!

      2. gralloc.mt6765.so同libsrv_um.so,也是通过系统调用进行mmap的!

结论:hook syscall 应该可以监控到renderD128相关内存的调用!

  1. 验证监控方案

监控方式:

  1. 使用bytehook劫持了libsrv_um.so和gralloc.mt6765.so中对syscall的调用
  2. 记录renderD128内存的变化

测试: 播放视频

测试结果:

  1. 系统调用mmap可以监控到renderD128内存的分配
  2. 在播放视频期间renderD128内存增长大小符合通过系统调用mmap分配的大小

堆栈:

内存变化:

结论: 底层驱动可能考虑到架构适配或者效率问题,直接使用系统调用而非通用接口调用。在之前的监控中并未考虑到这种情况,所以会导致监控不全。

  1. 相关内存分配

内存监控工具完善之后,从线上我们收集到如下的堆栈信息:

从堆栈上可以看到 libIMGegl.so有一个方法KEGLGetPoolBuffers,这个方法中会调用PVRSRVAcquireCPUMapping申请内存;

从“KEGLGetPoolBuffers”这个方法名可以推断:

  1. 有一个缓存池

  2. 可以调用KEGLGetPoolBuffers从缓存池中获取buffer

  3. 如果缓存池中有空闲buffer,会直接分配,无须从系统分配内存

  4. 如果缓存池中无空闲buffer,会调用PVRSRVAcquireCPUMapping从系统中申请内存

我们继续通过hook KEGLGetPoolBuffers 打印一些关键日志来确认猜想

  日志中前两次调用KEGLGetPoolBuffers没有申请内存,符合“存在空闲buffer直接分配”的猜想。

  后面的多次调用,每次都会连续调用5次 PVRSRVAcquireCPUMapping,分配5个大小不一的内存块(猜测应该是5类buffer),一共25M内存,和前面测试的结果刚好一致

  1. 相关内存释放

既然有内部分配,必然有其对应的内存释放,我们hook 泄漏线程RenderThread线程的munmap调用,抓到下面的堆栈,libsrv_um.so中相对偏移0xf060处(对应下面栈回溯#04栈帧,0xf061最后一位是1代表是thumb指令)的方法是DevmemReleaseCpuVirtAddr,但DevmemReleaseCpuVirtAddr这个方法并没有导出,glUnmapBuffer其实是调用了PVRSRVReleaseCPUMapping方法,在PVRSRVReleaseCPUMapping调用了DevmemReleaseCpuVirtAddr,进而最终调用到munmap方法释放内存的。

之所以在堆栈中没有PVRSRVReleaseCPUMapping这层栈帧,是因为PVRSRVReleaseCPUMapping跳转到DevmemReleaseCpuVirtAddr使用的是指令b(而非bl指令)

(glUnmapBuffer --> PVRSRVReleaseCPUMapping --> DevmemReleaseCpuVirtAddr --> ... --> munmap )

#01 pc 00009f41  /data/app/com.example.crash.test-bqPIslSQVErr7gyFpcHl_w==/lib/arm/libnpth_vm_monitor.soproxy_munmap)
#02 pc 0001474b  /vendor/lib/libsrv_um.so
#03 pc 000115d9  /vendor/lib/libsrv_um.so
#04 pc 0000f061  /vendor/lib/libsrv_um.soDevmemReleaseCpuVirtAddr+44#05 pc 00015db1  /vendor/lib/egl/libGLESv2_mtk.so (glUnmapBuffer+536)
#06 pc 003b865d  /system/lib/libhwui.so!libhwui.so (offset 0x244000) (GrGLBuffer::onUnmap()+54)
#07 pc 001a0eb3  /system/lib/libhwui.so (GrResourceProvider::createPatternedIndexBuffer(unsigned short const*, int, int, int, GrUniqueKey const*)+174)
#08 pc 001666b9  /system/lib/libhwui.so (GrResourceProvider::createQuadIndexBuffer()+24)
#09 pc 00153df1  /system/lib/libhwui.so (GrResourceProvider::refQuadIndexBuffer()+44)
#10 pc 001535c9  /system/lib/libhwui.so (GrAtlasTextOp::onPrepareDraws(GrMeshDrawOp::Target*)+328)

PVRSRVAcquireCPUMapping和PVRSRVReleaseCPUMapping是libsrv_um.so中进行内存分配和释放的一对方法

同理,KEGLGetPoolBuffers和KEGLReleasePoolBuffers是libIMGegl.so中分配和释放缓存buffer的一对方法

但在测试过程中,并没有看到在为buffer分配内存之后有调用PVRSRVReleaseCPUMapping释放内存,在绘制结束前,会调用KEGLReleasePoolBuffers释放buffer(但并未释放内存),查看KEGLReleasePoolBuffers的汇编发现方法内部只是对buffer标记可用,并不存在内存释放。

(左图KEGLGetPoolBuffers申请buffer,会申请内存;右图KEGLReleasePoolBuffers释放buffer,但不释放内存)

看来这个缓存池可能是统一释放内存的,由于libIMGegl.so中大部分方法都没有符号,从这层比较难推进,不妨再从上层场景分析一下,跟绘制相关的缓存池会什么时候释放呢?首先想到的可能是Activity销毁的时候,经过测试发现并没有……

但是在一次测试中发现 在Activity销毁之后,过了一段时间(1min左右)再启动一个新的Activity时突然释放了一堆renderD128相关的内存,抓到的是下面的堆栈。RenderThreaad中会执行销毁CanvasContext的任务,每次销毁CanvasContext时都会释放在一定时间范围内(30s)未使用的一些资源。销毁CanvasContext的时机是Activity Destroy时。(这里其实有些疑问,应该还有释放时机没有被发现)

    #01 pc 0000edc1  /data/app/com.example.crash.test-o-BAwGot5UWCmlHJALMy2g==/lib/arm/libnpth_vm_monitor.so
    #02 pc 0001d29b  /vendor/lib/libIMGegl.so
    #03 pc 0001af31  /vendor/lib/libIMGegl.so
    #04 pc 000187c1  /vendor/lib/libIMGegl.so
    #05 pc 0001948b  /vendor/lib/libIMGegl.so
    #06 pc 00018753  /vendor/lib/libIMGegl.so
    #07 pc 0000b179  /vendor/lib/libIMGegl.so
    #08 pc 0000f473  /vendor/lib/libIMGegl.so (IMGeglDestroySurface+462)
    #09 pc 000171bd  /system/lib/libEGL.so (android::eglDestroySurfaceImpl(void*, void*)+48)
    #10 pc 0025d40b  /system/lib/libhwui.so!libhwui.so (offset 0x245000) (android::uirenderer::renderthread::EglManager::destroySurface(void*)+30)
    #11 pc 0025d2f7  /system/lib/libhwui.so!libhwui.so (offset 0x245000) (android::uirenderer::skiapipeline::SkiaOpenGLPipeline::setSurface(ANativeWindow*, android::uirenderer::renderthread::SwapBehavior, android::uirenderer::renderthrea 
    #12 pc 00244c03  /system/lib/libhwui.so!libhwui.so (offset 0x243000) (android::uirenderer::renderthread::CanvasContext::setSurface(android::sp<android::Surface>&&)+110)
    #13 pc 00244af5  /system/lib/libhwui.so!libhwui.so (offset 0x243000) (android::uirenderer::renderthread::CanvasContext::destroy()+48)
    #15 pc 0023015f  /system/lib/libhwui.so!libhwui.so (offset 0x208000) (std::__1::packaged_task<void ()>::operator()()+50)
    #16 pc 0020da97  /system/lib/libhwui.so!libhwui.so (offset 0x208000) (android::uirenderer::WorkQueue::process()+158)
    #17 pc 0020d8f5  /system/lib/libhwui.so!libhwui.so (offset 0x208000) (android::uirenderer::renderthread::RenderThread::threadLoop()+72)
    #18 pc 0000d91b  /system/lib/libutils.so (android::Thread::_threadLoop(void*)+182)
    #19 pc 0009b543  /apex/com.android.runtime/lib/bionic/libc.so!libc.so (offset 0x8d000) (__pthread_start(void*)+20)

  1. 总结

renderD128类内存导致的OOM问题,并非由于内存泄漏,而是大量内存长期不释放导致。在大型APP中,Activity存活的时间可能会很长,如果缓存池只能等到Activity销毁时才能释放,大量内存长期无法释放,就极易发生OOM。

  1. 优化方案

  1. 手动释放内存

  1. 方案一:释放空闲buffer

从相关内存的分配和释放章节的分析来看,get & release buffer的操作有点不对称,我们期望:

  1. 分配缓存:有可用buffer直接使用;无可用buffer则申请新的;

  2. 释放缓存:标记buffer空闲,空闲buffer达到某一阈值后则释放。

而现状是空闲buffer达到某一阈值后并不会释放,是否可以尝试手动释放呢?

首先需要了解缓存池的结构

由于相关so代码闭源,我们通过反汇编推导出缓存池的结构,大致如下图所示,pb_global是缓存池的管理结构体,其中的buffers_list中分别保存了5类buffer的list,内存组织方式如下示意

KEGLReleasePoolBuffers中会标记每一个buffer->flag为0(空闲)

暂时无法在飞书文档外展示此内容

手动释放内存的方式

在KEGLReleasePoolBuffers标记buffer为空闲之后,检查当前空闲buffer个数是否超过阈值(或者检查当前render D128相关内存是否超过阈值),如果超过阈值则释放一批buffer,并将buffer从链表中取下。

(相关代码如下👇)

static void release_freed_buffer(pb_ctx_t* ctx) {
    /** 一些检查和判空操作会省略 **/
    ...
    /** 阈值检查 **/
    if (!limit_check(ctx)) return;

    // 拿到buffer_list
    pb_buffer_list_t* buffers_list = ctx->pb_global->buffers_list;

    pb_buffer_info_t *buffer_info, *prev_info;
    for (int i = 0; i < 5; i++) {
        buffer_info = buffer_info->buffers[i];
        if (buffer_info == NULL) continue;

        /** 第一个buffer不释放,简化逻辑 **/
        while(buffer_info) {
            prev_info = buffer_info;
            buffer_info = buffer_info->next;

            if (buffer_info && buffer_info->flag == 0) {
                int ret = pvrsrvReleaseCPUMapping((void**)buffer_info->sparse_buffer->cpu_mapping_info->info);

                LOGE("%s, release cpu mapping ret: %d", __FUNCTION__, ret);
                if (ret == 0) {
                    buffer_info->flag = 1;
                    buffer_info->sparse_buffer->mmap_ptr = NULL;
                    prev_info->next = buffer_info->next;
                    buffers_list->buffer_size[i]--;
                    free(buffer_info);
                    buffer_info = prev_info;
                }
            }
        }
    }
}

方案效果

测试环境和方式与前面“问题复现”章节一致

内存释放时机 绘制结束后renderD128相关内存大小 结果比较
每次释放缓存 33M 左右 与不设置透明度的对照组结果接近
renderD128内存> 100M 86M 左右 100M以下,符合预期
renderD128内存> 300M 295M 左右 跟实验组一致,因为并没有超过300M的阈值。符合预期
buffer总数 > 5 33M 左右 与不设置透明度的对照组结果接近,绘制结束时会释放完所有空闲buffer
buffer总数 > 10
buffer总数 > 20 295M 左右 跟实验组一致,因为并没有超过20个buffer的阈值(10个view大概会用到10~15个buffer)。符合预期
空闲buffer > 5 138M 左右 空闲buffer个数不太可控,无法精确控制内存水位
空闲buffer > 10 33M 左右

方案结论:

这个方案虽然也可缓解问题,但是存在以下问题:

  1. 性能影响(理论,未测)

    1. 增加了内存申请和释放的概率,会有一定的性能影响
    2. 每次进行阈值判定,都需要统计当前buffer/内存的值,频繁调用接口时,也会影响性能
  2. 稳定性

    1. 硬编码缓存池相关的数据结构,如果有些机型数据结构不一致的话,就可能会崩溃

这个方案应该不是最优解,先做备用方案,再探索一下

  1. 方案二:上层及时释放资源

从前面“相关内存释放”章节的分析可知,缓存池的内存并不是不会释放,而是释放时机很晚,那么能否早点释放呢?

查看CanvasContext的释放路径,仅发现了一个可操作点(尝试了一些方式都会崩溃,会释放掉正在使用的资源),CacheManager::trimStaleResources方法中可以把释放30s内未使用的资源,改成释放1s(或10s)内未使用的资源

修改指令:MOVW R2, #30000 ==> MOVW R2,#1000

(相关代码如下👇)

#define ORIGIN_TIME_LIMIT_INST      0x5230f247 // 30s
#define NEW_TIME_LIMIT_INST      0x32e8f240 // 1s 提前构造好的指令编码
#define FUNC_SYM "_ZN7android10uirenderer12renderthread12CacheManager18trimStaleResourcesEv"

static void change_destroy_wait_time() {
    /** 一些检查和判空操作会省略 **/
#ifdef __arm__
    void* handle = dlopen("libhwui.so");
    // 从trimStaleResources方法的起始地址开始搜索内存
    void* sym_ptr = dlsym(handle, FUNC_SYM);

    sym_ptr = (void*)((uint32_t)sym_ptr & 0xfffffffc);

    uint32_t* inst_start = (uint32_t*)sym_ptr;
    uint32_t* search_limit = inst_start + 12;

    while(inst_start < search_limit) {
        /* 找到并修改对应指令 */
        if (*inst_start == ORIGIN_TIME_LIMIT_INST) {
            if(mprotect((void*)((uint32_t)inst_start & (unsigned int) PAGE_MASK), PAGE_SIZE, PROT_READ | PROT_WRITE | PROT_EXEC)) {
                return;
            }

            *inst_start = NEW_TIME_LIMIT_INST;
            flash_page_cache(inst_start);

            if(mprotect((void*)((uint32_t)inst_start & (unsigned int) PAGE_MASK), PAGE_SIZE, PROT_READ|PROT_WRITE|PROT_EXEC)) {
                return;
            }
            break;
        }

        inst_start++;
    }
#endif
}

方案结论: 该方案还是依赖于Activity销毁,只是销毁后能更快释放资源,所以缓解内存方面起到的作用很有限

  1. 控制缓存池增长

在尝试前面两个方案之后,这个问题逐渐让人崩溃,似乎已经没有什么好的解决办法了,已经准备就此放弃。

  1. 新的突破点

山重水复疑无路,柳岸花明又一村。在后续的一次压测中,我们发现了一个新的突破点“每次调用一次renderD128 内存会上涨25M,但是并不是无限上涨,上涨到1.3G左右就不再增长了”,且另外翻看线上相关OOM问题,renderD128内存占用最多的也在1.3G上下,由此我们大胆猜测renderD128 内存缓存池大小应该是有上限的,这个上限大概在1.3G上下,那么我们可以尝试从调小缓存池的阈值入手。

再次尝试:

我们再次尝试复现该问题,并hook相关内存分配 ;从日志可以看到,在内存增长到1.3G后

  1. 下一次调用KEGLGetPoolBuffers获取buffer时,返回值是0(代表分配失败)
  2. 再下一次调用KEGLGetPoolBuffers,返回值是1(代表分配成功),但没有申请内存

再增加多一点信息,发现当KEGLGetPoolBuffers获取buffer失败后,会有KEGLReleasePoolBuffers调用,释放了大量buffer,之后再重新调用KEGLGetPoolBuffers

KEGLGetPoolBuffers end, a1: 0xd1b95068, a2: 0x1, ret: 0x1 ==> before: 1265852416, after: 1292066816, alloc: 26214400
KEGLGetPoolBuffers end, a1: 0xd1b95068, a2: 0x1, ret: 0x1 ==> before: 1292066816, after: 1318281216, alloc: 26214400
KEGLGetPoolBuffers end, a1: 0xd1b95068, a2: 0x1, ret: 0x0 ==> before: 1318281216, after: 1318281216, alloc: 0
KEGLReleasePoolBuffers end, ret: 1 ==> before: 1318281216, after: 1318281216, release: 0
KEGLReleasePoolBuffers end, ret: 1 ==> before: 1318281216, after: 1318281216, release: 0
KEGLReleasePoolBuffers end, ret: 1 ==> before: 1318281216, after: 1318281216, release: 0
...
KEGLReleasePoolBuffers end, ret: 1 ==> before: 1318281216, after: 1318281216, release: 0
KEGLReleasePoolBuffers end, ret: 1 ==> before: 1318281216, after: 1318281216, release: 0
KEGLReleasePoolBuffers end, ret: 1 ==> before: 1318281216, after: 1318281216, release: 0
KEGLGetPoolBuffers end, a1: 0xd1b95068, a2: 0x1, ret: 0x1 ==> before: 1318281216, after: 1318281216, alloc: 0
KEGLGetPoolBuffers end, a1: 0xd1b95068, a2: 0x1, ret: 0x1 ==> before: 1318281216, after: 1318281216, alloc: 0
KEGLGetPoolBuffers end, a1: 0xd1b95068, a2: 0x1, ret: 0x1 ==> before: 1318281216, after: 1318281216, alloc: 0

从堆栈看应该是提前flush了,所以就可以释放之前的buffer

#01 pc 0000ebf5  /data/app/com.example.crash.test-1hHKnp6FBSv-HjrVtXQo1Q==/lib/arm/libnpth_vm_monitor.so (proxy_KEGLReleasePoolBuffers)
#02 pc 00047c2d  /vendor/lib/egl/libGLESv2_mtk.so
#03 pc 00046a7b  /vendor/lib/egl/libGLESv2_mtk.so (ResetSurface)
#04 pc 00028bf7  /vendor/lib/egl/libGLESv2_mtk.so
#05 pc 000d2165  /vendor/lib/egl/libGLESv2_mtk.so (RM_FlushHWQueue)
#06 pc 00028c73  /vendor/lib/egl/libGLESv2_mtk.so 
#07 pc 000453fd  /vendor/lib/egl/libGLESv2_mtk.so (PrepareToDraw)
#08 pc 0001d977  /vendor/lib/egl/libGLESv2_mtk.so (glDrawArrays+738)
#09 pc 00009edd  /system/lib/libGameGraphicsOpt.so (hw_glDrawArraysHookV2+18)
#10 pc 001d1769  /system/lib/libhwui.so (GrGLGpu::sendMeshToGpu(GrPrimitiveType, GrBuffer const*, int, int)+74)
#11 pc 001d15f3  /system/lib/libhwui.so (GrMesh::sendToGpu(GrMesh::SendToGpuImpl*) const+38)
#12 pc 001d13e5  /system/lib/libhwui.so (GrGLGpu::draw(GrRenderTarget*, GrSurfaceOrigin, GrPrimitiveProcessor const&, GrPipeline const

2. #### 方案三:KEGLGetPoolBuffers中限制buffer分配

根据上面的分析,发现可以尝试:

  • Hook KEGLGetPoolBuffers函数,判断内存增长到一定阈值后,在KEGLGetPoolBuffers函数中就直接返回0,触发其内部的空闲buffer释放
  • 空闲buffer释放之后,才允许分配buffer(如下流程)

暂时无法在飞书文档外展示此内容

方案结论: 该方案需要每次分配内存前读取maps获取renderD128占用内存大小,对性能不是很友好

  1. 方案四:修改缓存池阈值

从上面的分析,我们知道KEGLGetPoolBuffers函数返回0时分配失败,会开始释放buffer。我们继续反汇编KEGLGetPoolBuffers函数,根据KEGLGetPoolBuffers的返回值为0 可以回溯到汇编中进行阈值判断的逻辑

v8:buffers_list

v7:buffer类型(0~4)

v8+4*v7+24:v7这个buffer类型 的buffer数量(右图中的buffer_size[i]

v49:buffer_info

v49 + 28: buffer_limit 缓存池中每种类型的buffer 的阈值(右图中的buffer_limits)

简单来说,这里将buffer_limits与buffer_size[i]进行比较,如果buffer_size[i]大于等于阈值,就会返回0,分配失败

接下来的操作就很简单了,只需对buffer_limits进行修改就行,在测试设备上buffer_limits值是50(50*25M 大约是1.25G),我们将buffer_limits改小一点就可以将renderD128内存值控制在一个更小的阈值范围内,以此降低renderD128内存占用。

(相关代码如下👇)

int opt_mtk_buffer(int api_level, int new_buffer_size) {
    ...(无关代码省略)
    if (check_buffer_size(new_buffer_size)) {
        prefered_buffer_size = new_buffer_size;
    }

    KEGLGetPoolBuffers_stub = bytehook_hook_single(
            "libGLESv2_mtk.so",
            NULL,
            "KEGLGetPoolBuffers",
            (void*)proxy_KEGLGetPoolBuffers,
            (bytehook_hooked_t)bytehook_hooked_mtk,
            NULL);
    ...(无关代码省略)

    return 0;
}

static void* proxy_KEGLGetPoolBuffers(void** a1, void* a2, int a3, int a4) {
    //修改buffer_limits
    modify_buffer_size((pb_ctx_t*)a1);
    void* ret = BYTEHOOK_CALL_PREV(proxy_KEGLGetPoolBuffers, KEGLGetPoolBuffers_t, a1, a2, a3, a4);
    BYTEHOOK_POP_STACK();
    return ret;
}

static void modify_buffer_size(pb_ctx_t* ctx) {
    if (__predict_false(ctx == NULL || ctx->node == NULL || ctx->node->buffer_inner == NULL)) {
        return;
    }

    if (ctx->node->buffer_inner->num == ORIGIN_BUFFER_SIZE) {
        ctx->node->buffer_inner->num = prefered_buffer_size;
    }
}

Demo验证:

缓存值阈值 内存峰值
50 1.3G
20 530M
10 269M

方案结论: 该方案修改少,性能影响小,且稳定性可控

  1. 最终方案

通过的上面的分析,由于方案四“修改缓存池阈值”修改少,性能影响小,且稳定性可控 最终我们决定选用该方案。

  1. 修复效果

开启修复实验后相关机型OOM崩溃率显著下降近50% ,观察数周之后各项业务指标也均为正向,符合预期。全量上线后大盘renderD128相关OOM也大幅下降,另外renderD128导致发版熔断的情况从此再也没有发生过。

  1. 总结

在分析内存问题时,不论是系统申请的内存还是业务申请的内存,都需要明确申请逻辑和释放逻辑,才能确定是否发生泄漏还是长期不释放,再从内存申请和释放逻辑中寻找可优化点。

相关资料

  1. 华为畅享e内核源代码链接:consumer.huawei.com/en/opensour…
  2. mesa源代码链接:gitlab.freedesktop.org/mesa/mesa

MySQL 遇到 AI:字节跳动开源 MySQL 虚拟索引 VIDEX

虚拟索引技术(virtual index,也称为 hypothetical index)在数据库系统的查询优化、索引推荐等场景中扮演着关键角色。简单来说,虚拟索引可以理解为数据库的'沙盘推演'系统——无需真实构建索引,仅基于统计信息即可精准模拟不同索引方案对查询计划的优化效果。由于虚拟索引的创建/删除代价极低,使用者可以大量创建和删除索引、反复推演,确定最有效的索引方案。 AI 时代,基于 机器学习模型 NDV 、Cardinality 估计算法层出不穷,但是在 MySQL 落地往往遇到很大挑战:无法 在 MySQL 生成查询计划时 注入机器学习模型的 预测值 优化器 便难以给出更合适的索引推荐结果。

业界许多数据库已经以官方或第三方的方式提供了虚拟索引功能,例如 PostgresOracleIBM DB2。大量数据库领域的研究都围绕虚拟索引技术展开。遗憾的是,MySQL 长期缺乏这一能力,导致其在复杂场景下的优化效果始终受限。

经过长期的生产验证,字节跳动正式开源了 MySQL 虚拟索引项目 VIDEXVirtual Index ),让 MySQL 也有了自己的虚拟索引机制 🎉

VIDEX 开源地址:github.com/bytedance/v…

VIDEX 已经部署在了字节跳动大规模生产系统中,每天为数千用户、数十万慢 SQL 提供优化服务。VIDEX 的实用价值、工业级部署设计等特点,引来 Daniel Black(MariaDB Foundation 首席创新官)和 Federico Razzoli (Founder of Vettabase) 等业界知名专家的点赞与认可。

VIDEX 提供开箱即用的虚拟索引能力,可无缝集成至现有 MySQL 生态;对于数据库研究者,VIDEX 模块化设计允许新算法(如 NDV 估计、Cardinality 估计等等)在 MySQL 上快速验证,推动前沿技术落地。

具体来说,VIDEX 的贡献如下:

  1. 弥补 MySQL 虚拟 索引 空白:尽管业界已经有多种数据库支持了虚拟索引功能( PostgresOracleIBM DB2),也有一些论文和博客提到了 MySQL 的虚拟索引技术 [1,2],但据我们所知,VIDEX 是首个开源的、可拓展、支持多形态部署的 MySQL 虚拟索引解决方案。

  2. 精度 地拟合 MySQL:我们已经在 TPC-HTPC-H-SkewJOB 等复杂分析基准测试上对 VIDEX 进行了测试。给定准确的独立值估计(ndv) 和基数估计(Cardinality) 信息,VIDEX 可以 100% 模拟 MySQL InnoDB 的查询计划

  3. 基于分离架构的多形态部署:VIDEX 实现了数据库实例-VIDEX 优化器插件-VIDEX 算法服务的模块分离。既支持作为插件无缝集成到现有 MySQL 实例,也可作为独立服务构建虚拟库环境,实现生产环境零干扰的索引验证;额外地,将 VIDEX 优化器插件和 VIDEX 算法服务也做了分离,便于 AI 算法服务的集成和热更新。

  4. 可拓展的实验平台:准确地模拟 MySQL 查询代价依赖于对独立值(ndv)和基数(Cardinality)的准确估计——这正是 ****AI + 数据库研究中最火热的方向之一 [3]。VIDEX 给出了标准化、清晰易懂的接口设计,屏蔽了复杂的系统细节。研究者可以自由地用各种语言来重写 VIDEX 的算法模型,甚至只需要改动一个 JSON 文件,就能将自己的新算法应用于 MySQL 查询优化器!

多形态部署:从实验平台到生产环境

由于 VIDEX 将真实数据库实例、虚拟数据库实例、算法服务器三个部分解耦了,因此可以灵活应用于各种适用场景,从个人研究到生产环境部署:

VIDEX-Optimizer 的两种形态:

  1. 作为插件安装到真实数据库:将 VIDEX 作为插件安装到真实数据库实例,这样只需要一台 MySQL 实例,即可体验基于虚拟索引的各种 what-if 分析。适合于个人实验和分析。
  2. 以独立 实例 启动:独立启动 VIDEX 示例,同步统计信息,然后开始分析。此模式可以完全避免影响在线运行实例的稳定性,在工业环境中很实用。

VIDEX 算法服务器的两种形态:

  1. 与 VIDEX-Optimizer 配套启动:最经典的方式,无须额外设置,VIDEX-Optimizer 会自动寻找本地启动的 VIDEX 算法服务器。
  2. 独立启动算法服务器:只要设置一下 SQL 环境变量(SET @``VIDEX_STATISTIC_SERVER``='ip:port'),VIDEX-Optimizer 会将算法请求转发到指定的算法服务器上。对于研究者来说,可以自由实现算法、启动自定义的算法服务;对于云原生场景,可以将大量 MySQL 实例的算法请求发往中心式的算法服务,便于运维和快速更新。

VIDEX 任务的两种形态:

  1. 非任务模式:默认情况下,用户不需要关注 “task_id” —— 只需要指定目标库、指定虚拟库,同步数据即可;

  2. 任务模式:在大规模分析任务中(例如大规模索引推荐任务),各种用户往往会对同一个生产库的不同表、或不同实例的同名表发起多次分析。这种情况下,用户可以指定任务 id(SET @VIDEX_OPTIONS={'task_id': 'abc'}),让多个任务彼此互不影响。

算法试验场:把算法模型接入 MySQL 优化器

MySQL 采用了分离式的架构,上层的查询优化器会向下层存储引擎请求各种信息,包括元数据信息(table_rows、data_length 等等)、独立值(ndv)、基数(cardinality)、索引内存加载率等等。其中基数估计和独立值估计是 AI for DB 研究领域的热点方向。现已有大量 data-driven 或者 query-driven 的算法被提出,但这些算法往往只能以 PostgreSQL 作为试验场。

VIDEX 让用户不必与 MySQL 查询优化器做交互、也屏蔽了 MySQL 对库表元数据信息(table_rows、deta_length)的请求。由此,用户可以专注于一些重点的算法问题,例如 NDV 估计和 Cardinality 估计。

方法 1:在 VIDEX-Statistic-Server 中添加一种新方法

考虑到许多研究者习惯于用 Python 研究各种 AI 与 DB 结合的算法,因此,我们用 Python 实现了 VIDEX-Statistic。

用户可以继承并修改 VidexModelInnoDBVidexModelInnoDB 为用户屏蔽了系统变量、索引元数据格式等复杂细节,并提供了一个基于独立、均匀分布假设的 ndv 和 cardinality 算法。这样用户可以聚焦于 cardinality 和 ndv 这两个研究热点:

class VidexModelBase(ABC):
    """
    Abstract cost model class. VIDEX-Statistic-Server receives requests from VIDEX-Optimizer for Cardinality
    and NDV estimates, parses them into structured data for ease use of developers.

    Implement these methods to inject Cardinality and NDV algorithms into MySQL.
    """

    @abstractmethod
    def cardinality(self, idx_range_cond: IndexRangeCond) -> int:
        """
        Estimates the cardinality (number of rows matching a criteria) for a given index range condition.

        Parameters:
            idx_range_cond (IndexRangeCond): Condition object representing the index range.

        Returns:
            int: Estimated number of rows that match the condition.

        Example:
            where c1 = 3 and c2 < 3 and c2 > 1, ranges = [RangeCond(c1 = 3), RangeCond(c2 < 3 and c2 > 1)]
        """
        pass

    @abstractmethod
    def ndv(self, index_name: str, table_name: str, column_list: List[str]) -> int:
        """
        Estimates the number of distinct values (NDV) for specified fields within an index.

        Parameters:
            index_name (str): Name of the index.
            table_name (str): Table Name
            column_list (List[str]): List of columns(aka. fields) for which NDV is to be estimated.

        Returns:
            int: Estimated number of distinct values.

        Example:
            index_name = 'idx_videx_c1c2', table_name= 't1', field_list = ['c1', 'c2']
        """
        raise NotImplementedError()

假设用户用 VidexModelExample 重载了 VidexModelInnoDB ,可以指定模然后启动 VIDEX-Statistic-Server(详见代码启动脚本)。

startup_videx_server(VidexModelClass=VidexModelExample)

方法 2: 全新实现 VIDEX-Statistic-Server

用户可以用任何编程语言实现 HTTP 响应、并在任意位置启动 VIDEX-Statistic。

使用时,只需要指定环境变量(SET @VIDEX_STATISTIC_SERVER='ip:port'),VIDEX-Optimizer 就会将所有请求转发到指定服务上。

两步玩转 VIDEX,在 TPC-H 上看看效果

步骤 1: Docker 启动 VIDEX

最简单的情况下,用户可以用 Docker 启动一个安装好 VIDEX-Optimizer 和 VIDEX-Statistic 的容器。用户也可以参考文档说明,尝试其他启动方式。

为简化部署,我们提供了预编译的 Docker 镜像,包含:

  • VIDEX-Optimizer: 基于 Percona-MySQL 8.0.34-26,并集成了 VIDEX 插件
  • VIDEX-Statistic: ndv 和 cardinality 算法服务

如果您尚未安装 Docker:

docker run -d -p 13308:13308 -p 5001:5001 --name videx kangrongme/videx:0.0.2

步骤 2: VIDEX 数据准备

VIDEX 需要 Python 3.9 环境,执行元数据采集等任务。我们推荐使用 Anaconda/Miniconda 创建独立的 Python 环境来安装,详见 README 文档的 Quick Start 章节

git clone git@github.com:bytedance/videx.git videx_statistic
cd videx_statistic
python3.9 -m pip install -e . --use-pep517

指定原库和 VIDEX 库地址,用脚本一键式同步数据(以 tpch 为例):

python src/sub_platforms/sql_opt/videx/scripts/videx_build_env.py \
 --target 127.0.0.1:13308:tpch_tiny:videx:password \
 --videx 127.0.0.1:13308:videx_tpch_tiny:videx:password

效果展示:以 TPC-H 为例

本示例使用 TPC-H 数据集演示 VIDEX 的完整使用流程。

假设用户已经准备好了 TPCH 数据。篇幅限制,我们将更详细的步骤说明放到了 README 文档的 Example 章节

为了展示 VIDEX 的有效性,我们对比了 TPC-H Q21 的 EXPLAIN 细节,这是一个包含四表连接的复杂查询,涉及 WHERE聚合ORDER BYGROUP BYEXISTSSELF-JOIN 等多种部分。初始情况下,MySQL 可以选择的索引有 11 个,分布在 4 个表上。

EXPLAIN FORMAT = JSON
SELECT s_name, count(*) AS numwait
FROM supplier,
     lineitem l1,
     orders,
     nation
WHERE s_suppkey = l1.l_suppkey
  AND o_orderkey = l1.l_orderkey
  AND o_orderstatus = 'F'
  AND l1.l_receiptdate > l1.l_commitdate
  AND EXISTS (SELECT *
              FROM lineitem l2
              WHERE l2.l_orderkey = l1.l_orderkey
                AND l2.l_suppkey <> l1.l_suppkey)
  AND NOT EXISTS (SELECT *
                  FROM lineitem l3
                  WHERE l3.l_orderkey = l1.l_orderkey
                    AND l3.l_suppkey <> l1.l_suppkey
                    AND l3.l_receiptdate > l3.l_commitdate)
  AND s_nationkey = n_nationkey
  AND n_name = 'IRAQ'
GROUP BY s_name
ORDER BY numwait DESC, s_name;

让我们来对比 VIDEX 和 InnoDB 的估计效果。我们使用 EXPLAIN FORMAT=JSON,这是一种更加严格的格式。

我们不仅比较表连接顺序和索引选择,还包括查询计划的每一个细节(例如每一步的行数和代价)。

如下图所示,VIDEX(左图)能生成一个与 InnoDB(右图)几乎 100% 相同的查询计划。

VIDEX 的一个重要作用是模拟索引代价。我们额外新增一个索引。VIDEX 增加索引的代价是 O(1) ,因为他并不需要在真实数据上创建索引:

-- 为 innodb 库创建索引
ALTER TABLE tpch_tiny.orders ADD INDEX idx_o_orderstatus (o_orderstatus);
-- 为 videx 创建索引
ALTER TABLE videx_tpch_tiny.orders ADD INDEX idx_o_orderstatus (o_orderstatus);

再次执行 EXPLAIN,我们看到 MySQL-InnoDB 和 VIDEX 的查询计划发产生了相同的变化,两个查询计划均采纳了新索引,并且查询计划的细节也非常接近。

VIDEX 的行数估计 (7404) 与 MySQL-InnoDB (7362) 相差约为 0.56%,这个误差来自于基数估计算法的误差。

深入解析 VIDEX 架构

如图展现了 VIDEX 的架构。总体来说,VIDEX 包含两个模块:

  • VIDEX-Optimizer-Plugin(简称 VIDEX-Optimizer) :VIDEX 的“前端”。可以作为插件安装到现有数据库,或者以一个独立的新实例启动。这一部分实现了 MySQL 查询优化器接口,并将其中一部分复杂的请求转发到 VIDEX-Statistic-Server。我们全面梳理了 MySQL handler 的超过 90 个接口函数,并实现与索引(Index)相关的接口。

  • VIDEX-Statistic-Server(简称 VIDEX-Statistic):VIDEX 的“后端”。基于收集到的统计信息(表行数、表大小、直方图等等)和集成的算法或模型,计算独立值(NDV) 和基数(Cardinality),并将结果返回给 VIDEX-Optimizer。

当用户指定了要分析(what-if analysis)的真实数据库之后,VIDEX 会在 VIDEX-Optimizer 上创建一个虚拟数据库。虚拟数据库与真实数据库的关系表结构完全一致,只是将 Engine 从 InnoDB 更换为 VIDEX。为了准确模拟目标数据库的查询代价,VIDEX 会调用脚本,从真实数据库采集必要的统计信息。上述过程都可以用我们提供好的脚本一键式完成。

用户也可以自定义地提供一份元数据文件、让脚本直接导入。元数据文件是 json 格式,包含了库表结构信息、统计信息(table_rows、单列 ndv 等等)、直方图信息,非常容易理解。

VIDEX-Statistic-Server 是 VIDEX 的算法服务器。我们已经提供了基于独立均匀假设的 ndv 和 cardinality 算法。研究者可以自由地使用 Python、或者其他语言来实现算法,我们已经封装好了清晰明了的接口。

上述环节完成后,你就可以在虚拟数据库上自由的创建和删除索引,然后使用 EXPLAIN 来获取“贴近真实”的查询计划了 🚀

作者团队

我们来自字节跳动的 ByteBrain 团队,我们致力于用 AI 技术,为各种基础架构与系统(数据库、云原生、大数据)优化降本、提质增效。

如果您有任何疑问,请随时通过电子邮件联系我们:

参考资料

  1. Meta: Yadav, Ritwik, Satyanarayana R. Valluri, and Mohamed Zaït. "AIM: A practical approach to automated index management for SQL databases." 2023 IEEE 39th International Conference on Data Engineering (ICDE) . IEEE, 2023.
  2. Meituan: Slow Query Optimized Ddvice Driven by Cost Model: tech.meituan.com/2022/04/21/…
  3. Kossmann, J., Halfpap, S., Jankrift, M., & Schlosser, R. (2020). Magic mirror in my hand, which is the best in the land? an experimental evaluation of index selection algorithms. Proceedings of the VLDB Endowment, 13(12), 2382-2395.

字节跳动开源 Godel-Rescheduler:适用于云原生系统的全局最优重调度框架

背景

在云原生调度中,一次调度往往无法解决所有问题,需要配合重调度来优化资源分配和任务摆放。传统的重调度框架主要集中在识别异常节点或任务,并通过迁移或删除来解决。然而,这些框架往往只能解决局部问题,无法提供全局最优的调度方案,且容易出现多重调度策略冲突,影响整体稳定性。

为了解决这些问题,字节跳动研发了 Godel-Rescheduler,一个基于全局最优调度策略的重调度框架。它不仅能识别集群中的异常节点和任务,还能智能推荐任务到最合适的位置,并通过图算法生成详细的迁移步骤,确保集群的整体稳定性,真正实现全局最优调度。

项目简介

Godel-Rescheduler 由两个核心模块组成:Policy Manager 和 Movement Manager。其中,Policy Manager 负责输出重调度决策,而 Movement Manager 则负责拆解并执行这些决策。整个框架的目标是通过重调度,使集群朝向全局最优状态发展。

> 架构概览

> 组件介绍

Policy Manager 作为算法与策略控制中心,Policy Manager 负责配置重调度策略、迁移条件检测和执行相应的算法。它输出全局或局部最优的调度结果,并将决策传递给 Movement Manager。

  • Policy Controller 负责整体调度流程的控制,利用各子模块提供的功能,输出最优的调度决策。

  • Policy Configurator 负责读取并解析配置文件,定义重调度策略的触发条件、参数和作用范围。支持四种触发方式:周期执行、Signal 信号、HTTP 请求和 Cronjob。每个策略都可以根据需要配置不同的触发方式。

  • Detector 用于检测集群、机器和实例的状态,评估是否需要进行局部迁移或全局重调度。不同的策略可以通过定制化 Detector 实现不同的检测逻辑,如热点检测、负载均衡或碎片整理等。

  • Algorithm Provider 根据 Detector 提供的输入,算法插件为每个需要重调度的实例找到最适合的目标节点。为确保调度决策的有效性,算法会进行目标节点的校验,并与其他策略的决策进行冲突检查。

  • Movement Checker 校验一个迁移是否会对集群稳定性造成负面影响,特别是对目标节点的资源消耗、负载情况等进行验证。

  • Validator 在框架层面对每个移动决策进行最终校验,确保未经过校验的错误决策不会被执行。

Movement Manager 负责决策的执行和排序,并将新的调度结果上报,同时清除过期的推荐结果。

  • Movement Generator 算法基于有向图强连通分量分解,依据实例在节点间的转移关系和 PDB(PodDisruptionBudget)限制,生成优化的迁移批次。目标是尽可能减少迁移次数,同时保证集群稳定性。

  • Task Killer 负责按顺序执行每个移动决策,确保每个决策在保证稳定性的前提下逐步实施。

  • Movement Recycler 确保在新策略生成前,及时清除旧的调度决策,避免过期决策影响新的调度计划。

> 字节跳动的实际落地

目前,字节跳动已经成功将 Godel-Rescheduler 应用到多个内部项目中,支持多种重调度策略的协同工作。例如:

  • 合并部署重调度:优化上下游应用实例在相同节点上的调度。

  • 负载均衡重调度:在负载、内存带宽、网络带宽等方面进行优化。

  • 碎片整理重调度:有效减少 CPU、GPU 等资源的碎片率等。

在实际应用中,Godel-Rescheduler 已帮助字节跳动的数万卡 GPU 集群将碎片率控制在 5% 以下,同时在大规模混合部署集群中,热点节点比例控制在 0.1% 以下,取得了显著的效果。

未来发展

未来,Godel-Rescheduler 将持续扩展和优化:

  • 更多重调度策略:引入更多实时数据,以丰富调度策略的多样性。

  • 稳定性建设:在优化调度效果的同时,持续降低重调度对集群稳定性的影响。

  • 扩展性优化:进一步简化策略接入方式,提升插件化能力。

  • 通用指标构建:制定通用的重调度评价指标,以全面评估调度效果。

  • 优化可解释性:增强重调度算法的可解释性,帮助用户更好地理解调度决策的依据。

相关链接

Godel ReScheduler 项目地址:

github.com/kubewharf/g…

Multi-SWE-bench:首个多语言代码修复基准开源

字节跳动豆包大模型团队正式开源首个多语言类 SWE 数据集——Multi-SWE-bench,可用于评估和提升大模型 “自动修 Bug” 能力。

在 SWE-bench 基础上,Multi-SWE-bench 首次覆盖 Python 之外的 7 种主流编程语言,是真正面向 “全栈工程” 的评测基准。其数据均来自 GitHub issue,历时近一年构建,以尽可能准确测评和提高大模型高阶编程智能水平。

本文将介绍 Multi-SWE-bench 的研究背景、数据集构建及后续计划,希望与业界共同促使代码生成技术走向成熟。

从 ChatGPT 到 4o、o1、o3、Claude-3.5/3.7,再到 Doubao-1.5-pro、DeepSeek-R1,大模型正在以惊人速度革新编码世界。

如今,AI 早已不限于写函数、查 API,让 AI 自动解决 GitHub 上提交的真实问题(Bug),也已成为衡量模型智能高低的标杆之一。

但问题也随之浮现:现有主流评测数据集,如 SWE-bench,全部是 Python 项目。这导致部分大模型在 Python 榜单上分数较高,但并不擅长其他语言。

为解决泛化能力不足的问题,字节跳动豆包大模型团队正式开源 Multi-SWE-bench。

该数据集是业内首个面向多语言代码问题修复的大模型评测基准,覆盖 Java、TypeScript、C、C++、Go、Rust 和 JavaScript 等编程语言。

作为一个标准化、可复现、覆盖多语言的 “自动编程” 开源评测基准,Multi-SWE-bench 旨在推动自动编程技术从仅能解决单一语言(如 Python)和低复杂度的任务,朝着支持多语言、具备真实问题解决能力的通用型智能体迈进。

伴随强化学习崛起,团队还同步开源了 Multi-SWE-RL,为 RL 在真实代码环境中的训练提供了标准化、可复用的数据基础设施。

目前 Multi-SWE-bench 论文、代码和数据集已全部公开。

团队认为,此次开源不过迈出了千里之行的小小一步,单靠一个团队远远无法满足技术发展所需,在此欢迎更多研究者共同参与开源基准和数据基础设施建设。

Multi-SWE-bench: A Multilingual Benchmark for Issue Resolving

论文链接:arxiv.org/abs/2504.02…

榜单链接:multi-swe-bench.github.io

代码链接:github.com/multi-swe-b…

数据链接:huggingface.co/datasets/By…

 1. 主流代码基准局限:覆盖语言单一、复杂任务有限 

代码生成任务对大语言模型的逻辑推理、上下文理解等核心能力提出了综合要求。相应地,SWE-bench 为代表的代码修复基准,近年来已成为衡量模型智能水平的重要指标。

SWE-bench 是当前最具代表性的代码修复评测基准,强调任务真实、难度高。它基于 GitHub issue,要求模型自动定位并修复 Bug,兼具跨文件修改、复杂语义推理与上下文理解等挑战。相比传统代码生成任务(例如 HumanEval、MBPP、LiveCodeBench),SWE-bench 更贴近现实开发场景,是衡量大模型高阶 “编程智能” 的关键标尺。

然而,随着行业快速发展与模型能力不断提升,该基准难以全面覆盖现实开发中的多语言环境与复杂任务,制约大模型代码智能进一步演进。

具体看,其局限主要体现在以下两方面:

(1)语言维度单一:当前主流评测几乎全部集中在 Python,缺乏其他语言覆盖,难以评估模型的跨语言泛化能力。

(2)任务难度不足:现有基准多为短 patch、单文件修复,未覆盖超多文件、多步骤、超长上下文等复杂开发场景。同时,SWE-bench 中的任务未做难度分级,难以系统衡量模型在不同能力层次上的表现。

在此背景下,行业迫切需要一个覆盖主流编程语言、具备高质量标注实例与难度分级的 “多语言 Bug 修复评测集”。

 2. 涵盖 7 种语言、1,632 个真实修复任务的 Multi-SWE-bench 

Multi-SWE-bench 旨在补全现有同类基准语言覆盖方面的不足,系统性评估大模型在复杂开发环境下的 “多语言泛化能力”,推动多语言软件开发 Agent 的评估与研究,其主要特性如下:

  • 首次覆盖 7 种主流编程语言(包括 Java、Go、Rust、C、C++、TypeScript、JavaScript),构建多语言开发环境下的代码修复任务,系统评估模型的跨语言适应与泛化能力;

  • 引入任务难度分级机制,将问题划分为简单(Easy)、中等(Medium)和困难(Hard)三类,涵盖从一行修改到多文件、多步骤、多语义依赖的开发挑战;

  • 1,632 个实例全部来源于真实开源仓库,并经过统一的测试标准和专业开发者的审核筛选,确保每个样本具备清晰的问题描述、正确的修复补丁以及可复现的运行测试环境。

面向不同模型代码能力评测分数

团队基于 Multi-SWE-bench 进行实验观察到,尽管当前 LLM 面向 Python 修复率表现不错,但面向其他语言平均修复率普遍不足 10%。

部分主流模型在 Python 上表现更为优异,面向其他语言则分数不佳。同时,随着任务难度增加,模型修复率呈现逐级下降趋势。

这也说明,多语言代码修复仍是大模型智能能力的一道分水岭,更是推动 AI 向通用编程智能体演进的核心方向。

 3. 历时近一年系统性构建,引入严格人工验证 

在构建 Multi-SWE-bench 过程中,团队设计并执行了一套系统性的数据构建流程,共分五个阶段,涵盖从项目筛选、数据采集到数据验证的全过程,最大程度保障数据的真实性、全面性与可用性。

Multi-SWE-bench 构建流程

第一步:开源仓库筛选

团队基于 GitHub 公开仓库,从多个维度筛选高质量的项目仓库,确保覆盖 7 大主流语言(Java、TypeScript、JavaScript、Go、Rust、C 和 C++)。选择标准包括: 

(1)超过 500 GitHub Stars,具备一定的社区活跃度;

(2)至少半年被持续维护; 

(3)拥有 CI/CD 支持,可通过 GitHub Actions 等工具自动化构建与测试;

(4)构建过程可复现,确保后续环境搭建无阻。

第二步:Pull Request(PR)爬取

完成仓库初筛后,团队通过自动化爬虫从项目中收集所有 PR,并应用以下过滤规则进行筛选:

(1)PR 必须关联至少一个 GitHub issue;

(2)包含测试文件的修改,确保修复行为可验证;

(3)已被合并至主分支,代码质量与维护者被充分认可。

其中,每条 PR 记录都会提取关键信息,包含:原始 issue 描述、修复补丁、测试补丁、commit 信息等。

第三步:构建可执行的 Docker 环境

为了保证数据集中的每个任务都具备完整的可运行性,团队基于每个 PR,构建了对应的 Docker 容器,复刻其运行环境。 

依托 CI/CD 配置、README 等元信息,提取依赖项并自动生成 Dockerfile。对于构建失败的情况,团队还会手动排查错误并尽可能修复,确保环境的完整性和可复现性。

第四步:PR 过滤和数据集制作

每个 PR 会在构建好的环境中依次运行三种状态的测试流程: 

(1)原始状态(未打任何 patch) ;

(2)仅应用测试补丁(test.patch) ;

(3)同时应用测试与修复补丁(test.patch + fix.patch) ;

团队通过分析三阶段测试日志,识别是否存在有效的修复行为(如 FAILED→PASSED),并排除存在回归风险、测试行为异常等不符合规范的样本。经过这一阶段后,团队最终保留了 2,456 条候选数据。

第五步:严格的人工验证机制

为进一步提升数据可靠性,团队引入了人工双重标注流程。共计 68 位专业标注者参与审核,所有标注人员具备对应语言开发经验与高度相关的背景。 

每条样本由两位独立标注员标注,并进行交叉复核,最终所有标注结果还需通过内部 QA 团队的抽检,确保一致性与准确性。

经过这一阶段,我们最终保留了 1,632 条高质量的实例,并公开所有标注问卷与评分数据,确保数据透明度。

通过系统化的数据构建流程,团队希望为未来自动编程智能体的评估与训练打下坚实基础,驱动相关研究走向规模化与工程化。

 4. Multi-SWE-RL 开源 & 社区招募 

随着 GPT-4o、o1、o3 等新一代模型的爆火,强化学习方法在自动编程中的潜力正受到广泛关注。 基于 RL 将对代码智能体起到重要推动作用的判断,豆包大模型团队进一步构建了 Multi-SWE-RL,为 RL 在代码环境中的训练,提供统一、标准的数据基础。这让模型不仅有了学习的 “教材”,还拥有学习的 “环境”。

作为首批贡献者,团队初步贡献了 4,723 个实例,每个实例均配备可复现的 Docker 环境,并且支持一键启动、自动评估、快速接入 RL 训练框架。同时,团队完整开源数据构建流程与工具链。

目前,团队同步启动开源社区计划,鼓励开发者共同参与数据集扩展、RL 数据贡献与新方法评测。Multi-SWE-RL 项目提供了详尽的贡献教程、激励机制,以及实时更新的任务看板,确保社区协作高效透明。所有新增数据与评测结果将定期纳入后续公开版本,并署名所有有效贡献者或作者。

豆包大模型团队期待与更多开发者、研究者共同推进 RL for Code 生态建设,为构建通用软件智能体奠定基础。

数据集链接:huggingface.co/datasets/By…

 5. 写在最后 

豆包大模型团队希望,Multi-SWE-bench 能作为大模型在多种主流编程语言与真实代码环境中的系统性评测基准,推动自动编程能力向更实用、更工程化的方向发展。

相比于以往聚焦 Python 的单语言任务,Multi-SWE-bench 更贴近现实中的多语言开发场景,也更能反映当前模型在 “自动化软件工程” 方向上的实际能力边界。

在未来,团队将持续拓展 Multi-SWE 系列的覆盖范围——包括新增语言、扩展更多软工任务,并通过社区共建机制,鼓励更多研究者和开发者参与 基准构建与 RL 训练数据贡献。

DeepSeek + Function Call:基于 Eino 的“计划——执行”多智能体范式实战

DeepSeek-R1(以下简称 DeepSeek)以其优秀的复杂问题推理能力和规划能力脱颖而出,然而其原生函数调用(Function Call)功能的缺失,无法让大模型去选择不同的工具和程序,以获取对应的信息,使其难以完成以下关键动作:

  • 实时数据获取(天气 / 票务 / 交通)

  • 外部服务交互(地图 API / 支付接口)

  • 复杂任务拆解执行(多步骤自动化)

这就导致它的应用场景受到限制,大多只能用于简单的对话式问答。有没有一个解决办法,能实现让 DeepSeek 做 Function Call?

答案是肯定的,我们提出 " 计划——执行” 多智能体的协同范式

由 DeepSeek 负责 “指挥”,由擅长 Function Call 的其他大模型去听指挥进行函数调用。这需要利用“计划——执行” 多智能体范式,由 “计划” 智能体负责推理和生成计划,由 “执行” 智能体负责执行计划:

“计划——执行” 多智能体范式的三大优势:

  1. 专业的 “智能体” 干专业的事情:比如 DeepSeek 负责推理和计划,豆包大模型负责 Function Call。

  2. “智能体” 层面的单一职责原则:每个智能体的职责是明确的,解耦的,方便 Prompt 调优和评测。

  3. 在提供解决问题整体方案的同时,保持灵活性:符合人类解决问题的通用模式。

要实现 “计划 —— 执行” 多智能体,我们必须要解决几个问题:多模型、多工具集成,复杂流程编排,上下文管理以及中间步骤追踪。Eino(文档 cloudwego.io/zh/docs/ein… 项目页 github.com/cloudwego/e… 框架通过提供开箱即用的模型组件实现和工具执行器、面向抽象接口的灵活流程编排能力、完备的全局状态管理以及回调机制,确保了上述问题的有效解决。

接下来,文章将直观的解释 “计划 —— 执行” 多智能体范式,介绍如何借助 Eino 框架来实现基于 DeepSeek 的‘计划 —— 执行’多智能体,最后通过一个有趣且靠谱的主题乐园行程规划助手的实战案例,带大家从 0 到 1 搭建一个完整的应用。

“计划——执行” 多智能体

基本的 ReAct 单智能体,是由一个 Agent 既负责计划拆解,也负责 Function Call:

可能存在的问题有三个:

  1. 对 LLM 的要求高:既要擅长推理规划,也要擅长做 Function Call。

  2. LLM 的 prompt 复杂:既要能正确规划,又要正确的做 Function Call,还要能输出正确的结果。

  3. 没有计划:每次 Function Call 之后,LLM 需要重新推理,没有整体的可靠计划。

解决的思路,首先是把单个的 LLM 节点拆分成两个,一个负责 “计划”,一个负责 “执行”:

这样就解决了上面的问题 3,Planner 会给出完整计划,Executor 依据这个完整计划来依次执行。部分解决了问题 1、2,Planner 只需要擅长推理规划,Executor 则需要擅长做 Function Call 和总结,各自的 prompt 都是原先的一个子集。但同时带来一个新的问题:

  1. 缺少纠错能力:最开始的计划,在执行后,是否真的符合预期、能够解决问题?

继续优化多智能体结构,在 Executor 后面增加一个 LLM 节点,负责 “反思和调整计划”:

这样就彻底解决了上面列出的问题,Executor 只需要按计划执行 Function Call,Reviser 负责反思和总结。

这就是 “计划——执行” 多智能体:通过将任务解决过程拆解为负责计划的 Planner 和 Reviser,以及负责执行的 Executor,实现了智能体的单一职责以及任务的有效计划与反思,同时也能够充分发挥 DeepSeek 这种推理模型的长项、规避其短板(Function Call)。

基于 Eino 框架实现 “计划——

执****行” 多智能体

实现一个 “计划——执行” 多智能体,需要:

  • 能够快速简单的集成 DeepSeek、豆包等各种大模型。

  • 能够快速简单的集成和执行各种 Tool。

  • 能够快速实现流程编排,把多个智能体以及工具按设计的流程串联起来,并能随时快速调整。

  • 能够及时的输出各智能体的执行过程,包括 DeepSeek 的推理过程。

  • 能够有效的管理和传递上下文。

Eino 是字节跳动开源的基于 Golang 的大模型应用开发框架,已在豆包、抖音、扣子等多个业务线广泛使用。我们选择 Eino 作为框架来进行全码开发,因为:

  • Eino 可以用几行代码完成对各种大模型的调用,包括 DeepSeek。

  • Eino 可以用几行代码快速把一个本地 Function 封装成 Tool,且有开箱即用的 Tool 执行器。

  • Eino 的流程编排能力可靠且灵活:分支判断,循环,运行时参数配置等。

  • Eino 的数据流处理能力为大模型应用场景而设计,可配合完整的回调机制实时输出中间结果。

  • Eino 可以通过在图编排时配置和读写全局状态来实现有效的上下文管理和传递。

Eino 的详细信息参见:文档 cloudwego.io/zh/docs/ein…

GitHub 项目页 github.com/cloudwego/e…

实战:主题乐园行程规划助手

我们通过实现一个主题乐园行程规划助手,来探索如何用 Eino 实现基于 DeepSeek 的 “计划——执行” 多智能体。这个多智能体的功能是根据用户的游园需求,规划出具体、符合要求、可操作的行程安排。完整代码仓库地址:github.com/cloudwego/e…

定义多智能体

首先定义多智能体以及需要的配置:

package plan_execute
import (
    "github.com/cloudwego/eino/components/model"
    "github.com/cloudwego/eino/compose"
    "github.com/cloudwego/eino/schema"
)
// Config “计划——执行”多智能体的配置.
type Config struct {
    PlannerModel        model.ChatModel // planner 智能体使用的大模型
    PlannerSystemPrompt string          // planner 智能体的 system prompt
    ExecutorModel        model.ChatModel         // executor 智能体使用的大模型
    ToolsConfig          compose.ToolsNodeConfig // executor 智能体使用的工具执行器配置
    ExecutorSystemPrompt string                  // executor 智能体的 system prompt
    ReviserModel        model.ChatModel // reviser 智能体使用的大模型
    ReviserSystemPrompt string          // reviser 智能体的 system prompt
    MaxStep int // 多智能体的最大执行步骤数,避免无限循环
}
// PlanExecuteMultiAgent “计划——执行”多智能体.
type PlanExecuteMultiAgent struct {
    // 图编排后的可执行体,输入是 Message 数组,输出是单条 Message
    runnable compose.Runnable[[]*schema.Message, *schema.Message]
}

多智能体编排逻辑

Eino 的流程编排有 “节点(Node)”、“边(Edge)” 和“分支 (Branch)” 组成,数据流转时要求严格的类型对齐。完整的数据流转图如下:

上图中,Planner,Executor,Reviser 都是输入为 []*Message,输出为 * Message 的 ChatModel 节点,Branch1 判断 Executor 是否完成了本轮次所有的 Function Call,Branch2 判断 Reviser 是否输出了最终答案,各个 ToList 节点负责连接两个 ChatModel,将输出的 *Message 转化为 []*Message,从而满足类型校验要求。

我们实现一个 NewMultiAgent 方法来实现上述编排逻辑:

// NewMultiAgent 根据配置编排一个“计划——执行”多智能体.
func NewMultiAgent(ctx context.Context, config *Config) (*PlanExecuteMultiAgent, error) {
    var (
       toolInfos      []*schema.ToolInfo
       toolsNode      *compose.ToolsNode
       err            error
       plannerPrompt  = config.PlannerSystemPrompt
       executorPrompt = config.ExecutorSystemPrompt
       reviserPrompt  = config.ReviserSystemPrompt
       maxStep        = config.MaxStep
    )
    if len(plannerPrompt) == 0 {
       plannerPrompt = defaultPlannerPrompt
    }
    if len(executorPrompt) == 0 {
       executorPrompt = defaultExecutorPrompt
    }
    if len(reviserPrompt) == 0 {
       reviserPrompt = defaultReviserPrompt
    }
    if maxStep == 0 {
       maxStep = defaultMaxStep
    }
    if toolInfos, err = genToolInfos(ctx, config.ToolsConfig); err != nil {
       return nil, err
    }
    // 为 Executor 配置工具
    if err = config.ExecutorModel.BindTools(toolInfos); err != nil {
       return nil, err
    }
    // 初始化 Tool 执行器节点,传入可执行的工具
    if toolsNode, err = compose.NewToolNode(ctx, &config.ToolsConfig); err != nil {
       return nil, err
    }
    // 创建一个待编排的 graph,规定整体的输入输出类型
    graph := compose.NewGraph[[]*schema.Message, *schema.Message]()
    // 定义 Executor 后的分支判断用的条件函数。该函数的输出是运行时选中的 NodeKey
    executorPostBranchCondition := func(_ context.Context, msg *schema.Message) (endNode string, err error) {
        if len(msg.ToolCalls) == 0 {
           return nodeKeyExecutorToList, nil
        }
        return nodeKeyTools, nil
    }
    // 定义 Reviser 后的分支判断用的条件函数。
    reviserPostBranchCondition := func(_ context.Context, sr *schema.StreamReader[*schema.Message]) (endNode string, err error) {
       defer sr.Close()
       var content string
       for {
          msg, err := sr.Recv()
          if err != nil {
             if err == io.EOF {
                return nodeKeyReviserToList, nil
             }
             return "", err
          }
          content += msg.Content
          if strings.Contains(content, "最终答案") {
             return compose.END, nil
          }
          if len(content) > 20 {
             return nodeKeyReviserToList, nil
          }
       }
    }
    // 添加 Planner 节点
    _ = graph.AddChatModelNode(nodeKeyPlanner, config.PlannerModel, compose.WithNodeName(nodeKeyPlanner))
    // 添加 Executor 节点
    _ = graph.AddChatModelNode(nodeKeyExecutor, config.ExecutorModel, compose.WithNodeName(nodeKeyExecutor))
    // 添加 Reviser 节点
    _ = graph.AddChatModelNode(nodeKeyReviser, config.ReviserModel, compose.WithNodeName(nodeKeyReviser))
    // 添加 Tool 执行器节点
    _ = graph.AddToolsNode(nodeKeyTools, toolsNode)
    // 添加三个 ToList 转换节点
    _ = graph.AddLambdaNode(nodeKeyPlannerToList, compose.ToList[*schema.Message]())
    _ = graph.AddLambdaNode(nodeKeyExecutorToList, compose.ToList[*schema.Message]())
    _ = graph.AddLambdaNode(nodeKeyReviserToList, compose.ToList[*schema.Message]())
    // 添加节点之间的边和分支
    _ = graph.AddEdge(compose.START, nodeKeyPlanner)
    _ = graph.AddEdge(nodeKeyPlanner, nodeKeyPlannerToList)
    _ = graph.AddEdge(nodeKeyPlannerToList, nodeKeyExecutor)
    _ = graph.AddBranch(nodeKeyExecutor, compose.NewStreamGraphBranch(executorPostBranchCondition, map[string]bool{
       nodeKeyTools:          true,
       nodeKeyExecutorToList: true,
    }))
    _ = graph.AddEdge(nodeKeyTools, nodeKeyExecutor)
    _ = graph.AddEdge(nodeKeyExecutorToList, nodeKeyReviser)
    _ = graph.AddBranch(nodeKeyReviser, compose.NewStreamGraphBranch(reviserPostBranchCondition, map[string]bool{
       nodeKeyReviserToList: true,
       compose.END:          true,
    }))
    _ = graph.AddEdge(nodeKeyReviserToList, nodeKeyExecutor)
    // 编译 graph,将节点、边、分支转化为面向运行时的结构。由于 graph 中存在环,使用 AnyPredecessor 模式,同时设置运行时最大步数。
    runnable, err := graph.Compile(ctx, compose.WithNodeTriggerMode(compose.AnyPredecessor), compose.WithMaxRunSteps(maxStep))
    if err != nil {
       return nil, err
    }
    return &PlanExecuteMultiAgent{
       runnable: runnable,
    }, nil
}

Tool 实现

我们的主题乐园行程规划助手,需要用到下列工具:

  • query_theme_park_opening_hour: 查询乐园 A 的整体营业时间

  • query_park_ticket_price: 查询乐园 A 的门票价格

  • list_locations: 列出乐园 A 中的所有区域,每个游乐设施都归属于一个区域

  • query_location_adjacency_info: 查询乐园 A 中的一个区域到其他相邻区域的步行时间,以分钟为单位

  • query_attraction_queue_time: 查询游乐设施的排队时间,以分钟为单位

  • query_attraction_info: 查询游乐设施的具体信息

  • query_performance_info: 查询演出的具体信息

  • query_restaurant_info: 查询餐厅的具体信息

  • validate_performance_time_table: 校验安排的表演场次是否符合事实

  • arrange_performances: 根据选中的表演名称,自动根据表演的时间表排程

  • validate_plan_items: 根据一个一日日程安排提案,校验各个计划项内部及之间是否自洽

首先定义核心的领域模型:

type ActivityType string
const (
    ActivityTypeAttraction  ActivityType = "attraction"
    ActivityTypePerformance ActivityType = "performance"
    ActivityTypeRestaurant  ActivityType = "restaurant"
    ActivityTypeOther       ActivityType = "other"
)
// Activity 主题乐园中的一个项目,可以是游乐设施、表演或餐厅.
type Activity struct {
    Name               string       `json:"name"`
    Desc               string       `json:"desc"`
    Type               ActivityType `json:"type"`
    Location           string       `json:"location" jsonschema:"description:项目所属的区域"`
    MinHeight          int          `json:"min_height,omitempty" jsonschema:"description:参加游乐设施需要的最小身高,单位是厘米。如果为空,则没有身高要求"`
    Duration           int          `json:"duration,omitempty" jsonschema:"description:一个项目参加一次需要的时间,注意不包括排队的时间。如果为空,则缺少具体的时间信息"`
    TimeTable          []string     `json:"time_table,omitempty" jsonschema:"description:一个演出的时间表。如果为空,则使用 OpenTime 和 CloseTime 来表示这个项目的运营时间范围"`
    OpenTime           string       `json:"open_time,omitempty" jsonschema:"description:一个项目开始运营的时间"`
    CloseTime          string       `json:"close_time,omitempty" jsonschema:"description:一个项目结束运营的时间"`
    RequireBooking     bool         `json:"require_booking,omitempty" jsonschema:"description:一个餐厅是否需要提前预约"`
    HasPriorityAccess  bool         `json:"has_priority_access,omitempty" jsonschema:"description:一个项目是否有高速票服务"`
    PriorityAccessCost int          `json:"priority_access_cost,omitempty" jsonschema:"description:一个项目如果有高速票服务,则一个人的高速票需要花多少钱"`
    QueueTime          int          `json:"queue_time,omitempty" jsonschema:"description:一个项目常规需要的排队时间,单位是分钟。如果为空,则这个项目一般不需要排队"`
}

注意大多数字段中都有 jsonschema:"description:xxx"go struct tag。Eino 框架可抽取这个信息以及其他的 tag 给到大模型。

实现工具列表中需要的本地 function,如:

// GetAttractionInfo 获取游乐设施信息.
func GetAttractionInfo(_ context.Context, in *ListAttractionRequest) (out *ListAttractionResponse, err error) {
    if len(in.Name) > 0 && in.Name != "all" {
       for _, a := range attractions {
          if a.Name == in.Name {
             return &ListAttractionResponse{
                Attractions: []Activity{
                   a,
                },
             }, nil
          }
       }
    }
    if len(in.Location) > 0 {
       locationAttractions := make([]Activity, 0)
       for _, a := range attractions {
          if a.Location == in.Location {
             locationAttractions = append(locationAttractions, a)
             return &ListAttractionResponse{
                Attractions: locationAttractions,
             }, nil
          }
       }
    }
    return &ListAttractionResponse{
       Attractions: attractions,
    }, nil
}

完整的领域模型及服务定义参见代码链接 (github.com/cloudwego/e…

数据来源:可以是主题乐园提供的 API,也可以是外置的数据库,在我们的场景中,直接在项目中维护结构化的信息(完整代码链接 github.com/cloudwego/e…

将本地 function 封装成 Tool:

func GetTools(ctx context.Context) (tools []tool.BaseTool, err error) {    queryTimeTool, err := utils.InferTool("query_theme_park_opening_hour", "查询乐园 A 的整体营业时间", GetParkHour)    if err != nil {       return nil, err    }    tools = append(tools, queryTimeTool)        // 以下省略多个 Tool        return}

完整的 Tool 封装代码参见代码链接 (github.com/cloudwego/e…

上下文管理

针对每个智能体的一次执行,它的上下文应当包括:

  • 用户输入的任务。

  • 之前执行的智能体(包括自身)的输出。

  • 之前执行的智能体的 Function Call,以及对应的结果。

  • 自身的 System Prompt。

为了保存多智能体的上下文,我们为 graph 增加全局状态,并在各智能体执行前以及 Tool 执行前,向这个全局状态中读写上下文:

// state 以多智能体一次运行为 scope 的全局状态,用于记录上下文
type state struct {
    messages            []*schema.Message
}
func NewMultiAgent(ctx context.Context, config *Config) (*PlanExecuteMultiAgent, error) {
    // ... 省略 N 行 ... 
    // 创建一个待编排的 graph,规定整体的输入输出类型,配置全局状态的初始化方法
    graph := compose.NewGraph[[]*schema.Message, *schema.Message](compose.WithGenLocalState(func(ctx context.Context) *state {
       return &state{}
    }))
    // 在大模型执行之前,向全局状态中保存上下文,并组装本次的上下文
modelPreHandle := func(systemPrompt string, isDeepSeek bool) compose.StatePreHandler[[]*schema.Message, *state] {
    return func(ctx context.Context, input []*schema.Message, state *state) ([]*schema.Message, error) {
       for _, msg := range input {
          state.messages = append(state.messages, msg)
       }
       if isDeepSeek {
          return append([]*schema.Message{schema.SystemMessage(systemPrompt)}, convertMessagesForDeepSeek(state.messages)...), nil
       }
       return append([]*schema.Message{schema.SystemMessage(systemPrompt)}, state.messages...), nil
    }
}
    // ... 省略 N 行 ...
    // 添加 Planner 节点,同时添加 StatePreHandler 读写上下文
    _ = graph.AddChatModelNode(nodeKeyPlanner, config.PlannerModel, compose.WithStatePreHandler(modelPreHandle(plannerPrompt, true)), compose.WithNodeName(nodeKeyPlanner))
    // 添加 Executor 节点,同时添加 StatePreHandler 读写上下文
    _ = graph.AddChatModelNode(nodeKeyExecutor, config.ExecutorModel, compose.WithStatePreHandler(modelPreHandle(executorPrompt, false)), compose.WithNodeName(nodeKeyExecutor))
    // 添加 Reviser 节点,同时添加 StatePreHandler 读写上下文
    _ = graph.AddChatModelNode(nodeKeyReviser, config.ReviserModel, compose.WithStatePreHandler(modelPreHandle(reviserPrompt, true)), compose.WithNodeName(nodeKeyReviser))
    // 添加 Tool 执行器节点,同时添加 StatePreHandler 读写上下文
    _ = graph.AddToolsNode(nodeKeyTools, toolsNode, compose.WithStatePreHandler(func(ctx context.Context, in *schema.Message, state *state) (*schema.Message, error) {
        state.messages = append(state.messages, in)
        return in, nil
    }))
    // ... 省略 N 行 ...
}

完整编排代码见链接 github.com/cloudwego/e…

main 函数:多智能体执行

多智能体执行逻辑需要实现下列功能:

  • 实例化 DeepSeek 和豆包的模型,并放到多智能体的配置中。

  • 获取 Tool 列表。

  • 依据配置编排和初始化多智能体。

  • 将多智能体的各中间步骤及时输出。

在 main 函数中:利用 Eino 框架提供的组件实现,实例化需要的大模型,获取 Tool,初始化多智能体:

func main() {
    ctx := context.Background()
    deepSeekModel, err := deepseek.NewChatModel(ctx, &deepseek.ChatModelConfig{
       Model:   os.Getenv("DEEPSEEK_MODEL_ID"),
       APIKey:  os.Getenv("DEEPSEEK_API_KEY"),
       BaseURL: os.Getenv("DEEPSEEK_BASE_URL"),
    })
    if err != nil {
       log.Fatalf("new DeepSeek model failed: %v", err)
    }
    arkModel, err := ark.NewChatModel(ctx, &ark.ChatModelConfig{
       APIKey: os.Getenv("ARK_API_KEY"),
       Model:  os.Getenv("ARK_MODEL_ID"),
    })
    if err != nil {
       log.Fatalf("new Ark model failed: %v", err)
    }
    toolsConfig, err := tools.GetTools(ctx)
    if err != nil {
       log.Fatalf("get tools config failed: %v", err)
    }
    // 创建多智能体的配置,system prompt 都用默认值
    config := &Config{
       // planner 在调试时大部分场景不需要真的去生成,可以用 mock 输出替代
       PlannerModel: &debug.ChatModelDebugDecorator{
          Model: deepSeekModel,
       },
       ExecutorModel: arkModel,
       ToolsConfig:   compose.ToolsNodeConfig{Tools: toolsConfig},
       ReviserModel: &debug.ChatModelDebugDecorator{
          Model: deepSeekModel,
       },
    }
    planExecuteAgent, err := NewMultiAgent(ctx, config)
    if err != nil {
       log.Fatalf("new plan execute multi agent failed: %v", err)
    }
    printer := newIntermediateOutputPrinter() // 创建一个中间结果打印器
    printer.printStream()                     // 开始异步输出到 console
    handler := printer.toCallbackHandler()    // 转化为 Eino 框架的 callback handler
    // 以流式方式调用多智能体,实际的 OutputStream 不再需要关注,因为所有输出都由 intermediateOutputPrinter 处理了
    _, err = planExecuteAgent.Stream(ctx, []*schema.Message{schema.UserMessage("我们一家三口去乐园玩,孩子身高 120 cm,园内预算 2000 元,最爱的是各种表演,游乐设施比较偏爱刺激项目,希望能在一天内尽可能多体验不同的活动,帮忙规划一个行程。我们会在园区开门之后立刻入园,在园区关闭之后再离开。")},
       agent.WithComposeOptions(compose.WithCallbacks(handler)), // 将中间结果打印的 callback handler 注入进来
       // 给 planner 指定 mock 输出
       //agent.WithComposeOptions(compose.WithChatModelOption(debug.WithDebugOutput(schema.AssistantMessage(debug.PlannerOutput, nil))).DesignateNode(nodeKeyPlanner)),
       // 给 reviser 指定 mock 输出
       //agent.WithComposeOptions(compose.WithChatModelOption(debug.WithDebugOutput(schema.AssistantMessage("最终答案", nil))).DesignateNode(nodeKeyReviser)),
    )
    if err != nil {
       log.Fatalf("stream error: %v", err)
    }
    printer.wait() // 等待所有输出都处理完再结束
}

完整 main 函数代码实现链接 github.com/cloudwego/e…

流式输出中间过程

在上面的 main 函数中可以看到,我们通过 printer 这个 “中间结果打印器”,把各智能体的流式输出异步打印了出来。这利用了 Eino 框架的 callback 机制,在 ChatModel 输出时和 Tool 起止时触发执行切面逻辑。

调试与优化

在上面的 main 函数中,通过 ChatModelDebugDecorator 把 Planner 智能体封装起来,这是因为调试过程中,我们经常需要固定 Planner 的输出,单独调试后续流程。在 Eino 框架中,能够很方便的实现类似的装饰者,因为所有的组件如 ChatModel 等都是 interface,从编排角度看,原始的 ChatModel 实现和对应的装饰者可以无缝替换。完整调试代码实现见链接 github.com/cloudwego/e…

如果你更习惯可视化调试,可以安装 Eino Dev IDE 插件 www.cloudwego.io/zh/docs/ein… Golang 和 VSCode),把多智能体的编排拓扑在 IDE 中直观的展示出来,还可以指定中间的某个智能体开始调试。

在实战过程中,对 system prompt 的优化占据了相当多的时间,有关优化的最佳实践还在总结之中,后续会在 Eino 项目中发布,可以关注我们的 Github 项目 github.com/cloudwego/e… 获取最新的进展。

实际效果

以下为 Reviser 智能体的最终输出:

最终答案:

总费用:门票 1100 元 + 高速票 540 元 + 餐饮 420 元 = **2060 元 **(超支 60 元,可通过午餐降级为蓝莓熊餐盒节省 60 元)

关键优化点:

  1. 高速票精准投放:仅在 “抱抱熊飞天赛车” 午间时段使用(性价比最高),其他项目利用排队低峰期

  2. 表演全覆盖:新增 “复仇者小分队培训行动”,保留所有核心表演场次

  3. 动线优化:区域集中化(未来世界→玩具的故事→幻想世界→奇幻园林→入口大街)

  4. 预算控制:通过调整午餐为快餐(蓝莓熊餐盒人均 50 元)可使总费用降至 2000 元以内

  5. 刺激项目最大化:包含 3 个过山车类项目(抱抱熊 / 矿山车 / 飞吧地平线)+ 2 个黑暗骑乘(家勒比海贼 / 派斯音速)

执行效果:在保证所有热门表演的前提下,实现 5 个高刺激项目体验,时间利用率达 92%,区域移动时间占比仅 8%

完整执行过程见链接 github.com/cloudwego/e…

相关链接

Eino 框架 Github 仓库地址:

项目官网:www.cloudwego.io

项目文档:cloudwego.io/zh/docs/ein…

让AI代码从能用变好用! Trae+火山引擎数智平台, 打造"会进化"的智能应用

在AI编程工具高速发展的今天,Cursor、Trae等工具凭借自然语言生成代码、跨语言支持等能力,极大提升了开发效率。

工具生成的代码注重功能实现。打造一款受欢迎的产品,有了能实现功能的代码并不够,还需要追踪后续数据(Track)并验证效果,才能使开发者在优化用户体验和商业决策时消除盲区。

如何让AI生成的代码真正融入业务场景,实现从“能用”到“好用”的跨越?火山引擎数智平台的DataTester(A/B测试平台)与DataFinder(增长分析工具)的深度集成,为这一难题提供了科学答案。

图片

AI 生成代码的痛点:功能完善≠效果最优

当前主流AI编程工具(如Cursor、Trae)虽能快速生成应用框架,但存在两大短板:

  1. 产品分析 数据缺失:生成的App缺乏埋点设计,无法追踪用户点击、转化路径等关键行为,导致优化无据可依。
  2. 实验验证能力不足:功能上线后难以通过A/B测试验证不同版本的效果差异,只能依赖主观判断或事后分析,试错成本高。

以电商场景为例,AI生成的促销页面可能因按钮位置、文案差异影响转化率,但若无埋点与实验能力,开发者无法量化哪种设计更优,最终导致资源浪费。


火山引擎 DataTester  +  DataFinder  :补齐 AI 工具的最后一环

在Trae中结合火山引擎数智平台(VeDI)的产品,将能获得比使用单一的AI编程工具更好的使用体验;通过数据产品的辅助,AI编程结果可以更好地进化迭代。

火山引擎数智平台(VeDI)的两大核心产品——DataTesterDataFinder,通过“数据采集+智能实验”的组合,为AI生成的代码注入全链路优化能力:

  1. 行为 数据 追踪:从“功能实现”到“数据驱动”
  • DataFinder提供轻量级SDK,支持一键集成到Trae生成的代码中,自动采集用户点击、停留时长、转化漏斗等行为数据,并生成可视化报告。
  • 例如,开发者通过Trae生成的购物车页面,可借助DataFinder分析用户从加购到支付的流失节点,定位体验瓶颈。
  1. A/B实验验证:科学决策取代经验主义
  • DataTester提供三类实验能力,适配多场景优化需求:
    • 策略迭代实验:测试不同UI设计、算法策略的效果差异,例如推荐算法模型A/B测试。
    • 功能发布实验:结合Feature Flag功能,实现代码功能的无感下发与灰度发布,降低线上风险。
    • 增长营销实验:针对AI生成的广告素材、落地页,快速验证点击率与转化率,优化投放ROI。
  • 例如,Trae生成的应用可通过DataTester对比不同的用户注册界面,结合DataFinder采集转化数据,选出转化率最优方案。
  1. 全链路闭环:从生成到优化,  AI 全程参与
  • Trae 生成代码 → DataFinder 埋点 追踪 → DataTester 实验验证 → AI 模型反馈调优,形成完整闭环。
  • 火山引擎DataTester支持与大模型联动,例如通过实验数据反哺Prompt优化,让AI生成的代码更贴合业务目标。

案例实践:  AI 工具+  火山引擎  ,释放业务增长潜能

场景1:社交App弹窗优化

  • 问题:AI生成的弹窗样式单一,用户关闭率高。
  • 方案
    • 使用DataTester创建多个弹窗设计版本(如按钮位置、文案语气)。
    • 通过DataFinder分析各版本的点击率与留存率。
    • 实验结果显示“底部按钮+趣味文案”组合的转化率提升32%,全量上线。

场景2:  电商推荐 算法迭代

  • 问题:AI生成的推荐模型效果不稳定。
  • 方案
    • DataTester并行运行新旧算法版本,划分流量对比GMV指标。
    • 结合DataFinder的用户路径分析,定位高价值群体的偏好差异。
    • 实验数据反馈至Trae的AI模型,优化后续代码生成逻辑。

未来展望:  AI 开发者的“科学工具箱”

随着火山引擎DataTester与DataFinder的深度整合,AI编程工具正从“代码生成器”进化为“业务增长引擎”。开发者可专注于创新设计,而数据埋点、实验验证等繁琐环节交由平台自动化处理。这一模式不仅适用于互联网行业,在金融、零售、汽车等领域的数字化场景中同样潜力巨大。

立即行动

  • 访问Trae官网(trae.com.cn)体验AI代码生成。
  • 扫描二维码,获取DataTester与DataFinder的定制化解决方案,让每一行代码都精准命中业务目标。

通过“AI生成+数据智能”的双轮驱动,开发者将真正实现从功能开发价值创造的跨越,开启效率与效果并重的新时代。

图片

AIBrix 深度解读:字节跳动大模型推理的云原生实践

AIBrix 项目目前已经开源,本文为 AIBrix 技术解析。详见:
🔗 vLLM 博客:blog.vllm.ai/2025/02/21/…
🔗 代码仓库:github.com/vllm-projec…
🔗 技术详解博客:aibrix.github.io/posts/2025-…

01

前言

随着 LLaMA、DeepSeek、Qwen 等开源大模型的快速崛起,企业在模型部署的灵活性、成本与自主可控性方面迎来了新的机遇。然而,仅靠对模型本身的优化尚不足以将这些模型部署成高效且可扩展的生产级 API。大模型推理往往引入诸多独特的系统挑战,如 GPU 弹性伸缩指标的非线性问题,长尾模型和精调模型流量过低的问题,多机推理时的角色编排以及 GPU 卡型的异构管理等,都对易用性和成本控制提出了更高要求。因此,我们需要从推理引擎到底层基础设施进行全栈系统设计,才能真正让大模型在生产环境中长期稳定且高效地运行。

AIBrix 作为首个基于 Kubernetes 的企业级推理系统项目,正好填补了业界在 “系统层” 上的空白。它通过优化资源调度、自适应扩缩容、缓存感知路由以及异构计算管理等多项能力,为企业级大模型的大规模部署提供高效、低成本、可扩展的解决方案。AIBrix 与 vLLM 等推理引擎深度协同,持续优化推理效率,并融合多项前沿研究成果,推动大模型推理走向更加高效、可落地的生产化阶段。

02

AIBrix 的项目背景与设计理念

在规划 AIBrix 项目的过程中,我们始终站在基础架构的角度,思考如何在大规模场景下为推理引擎提供更好支持。结合字节跳动内部的业务实践,我们发现,大模型往往会带来一系列与传统微服务截然不同的系统挑战,包括:

  • 模型权重的下载 / 加载:如何快速分发和加载体积庞大的模型文件,降低冷启动延迟。

  • GPU 弹性伸缩指标的非线性:大模型对 GPU 的利用率并非线性关系,传统的指标收集与弹性策略常常滞后或不精准。

  • 长尾模型或精调模型流量低:针对这些流量低但又需要及时响应的模型,如何做到有效的资源利用和成本控制。

  • 多机推理的角色编排:在分布式推理场景下,如何更高效地在多个节点之间分配和调度任务。

  • GPU 卡型异构:不同型号、不同性能的 GPU 共同部署时,如何协同工作并优化利用率。

  • 单服务跨 Region:多数据中心与跨区域部署的需求增大了同步管理模型与推理任务的难度,同时对容灾与可用性提出了更高要求。

传统微服务框架(如 KNative)或服务网格(如 Istio)在鉴权、流量管控、版本升级等通用能力上已经相当成熟,但对于大模型服务而言仍然显得过于臃肿,且缺少针对性的优化。此外,市面上大多数项目往往将推理引擎视作一个 “黑盒”,无法进行深度协同优化。

设计理念

为应对上述挑战,AIBrix 的核心理念在于通过 “引擎层” 与“系统层”的紧密协同,搭建一个轻量化、云原生的方案。具体而言,我们将部分通用的引擎功能卸载到系统层面进行管理,并对模型推理常用能力进行封装,向外提供统一的引擎接口层。这种模式能够在大规模场景下同时兼顾性能、成本和易用性,帮助企业级大模型部署实现更高的弹性和可控性。

系统架构

AIBrix 包含控制平面组件与数据平面组件,并完全基于 Kubernetes 进行开发,采用完整的云原生设计来确保系统的可扩展性、可靠性以及资源效率。AIBrix 充分利用了 Kubernetes 的现有功能,包括自定义资源 (CRD)、控制器机制以及动态服务发现等,为大规模 LLM 推理服务提供了稳健的基础设施。

控制平面组件主要负责管理模型元数据注册、自动扩缩容、模型适配器注册,并执行各种策略。数据平面组件则提供可配置的请求派发、调度与推理服务能力,实现灵活且高性能的模型推理执行。下图为 AIBrix 的系统架构

AIBrix 项目已发布了 v0.1.0 和 v0.2.0 两个版本。在 v0.1.0 阶段,我们主要针对 Serverless 场景进行了一系列优化,着重解决冷启动、弹性伸缩和高密度部署的问题。而在 v0.2.0 阶段,我们则聚焦于分布式与解耦化,通过多机推理、KV-Cache 管理以及异构计算管理等特性,让大模型的规模化部署更加高效可控。

03

AIBrix v0.1.0:Serverless 与高密度部署

Serverless 与弹性伸缩

AIBrix v0.1.0 的主要思路是将大模型在生产环境中面临的核心难题,与 Serverless 领域的几项关键技术(冷启动、快速伸缩与高密度部署)相结合。我们并不追求让大模型像 FaaS 一样彻底 “无服务器化”,因为这在现实中尚难达到理想效果,也并非企业级生产环境的最佳形态;更可行的路线是借鉴并改进 Serverless 的相关思路,对大模型的部署环节进行有针对性的优化。

线上观察:Autoscaling 与指标挑战

在实际应用中,Autoscaling 最大的难点是:流量波峰和推理实例利用率之间通常存在显著的时间滞后(常见在 2~5 分钟),导致高并发场景下容易出现短时过载,从而拉升长尾延迟。此外,传统的 GPU 性能指标(如 DCGM 暴露的 DCGM_FI_DEV_GPU_UTIL 或 DCGM_FI_PROF_SM_ACTIVE)严重依赖引擎自身实现,也很难体现 GPU 空间利用率,导致扩缩容决策往往不够精确。

多种伸缩方案探索

为此,我们尝试过将引擎 KV_CACHE 利用率 与队列中待处理请求的输入 / 输出指标结合起来,做出更精细的扩缩容判断。然而在实际业务中,保障 SLO(而非 GPU 利用率)通常是更高优先级的目标,这使得传统基于资源利用率的 Autoscaling 策略效果有限。为了应对这一挑战,我们又探索了 基于 Profiling 并以 SLO 驱动的扩缩容方案,通过对历史与实时流量分布进行分析,动态确定扩缩容时机,减少过载并降低尾部延迟。

目前,AIBrix 在此方向上仍在持续迭代研究,包括尝试更具前瞻性的 LLM 专用指标,以及 Proactive 主动式弹性策略,让系统在应对突发流量时更加游刃有余。

在架构设计中,v0.1.0 主要引入了 Gateway API Plugin (Envoy) + Runtime 这两个组件,以适配大模型通常面对的两类路由方式:应用层路由(app router) 和 代理层路由(proxy router)。在大模型社区,如 vLLM 正不断丰富自身 API(含 token、transcription、score 等),保持与引擎原生接口一致是一项不小的挑战。为此,我们采用了高性能标准化的 envoy gateway 配合 extension server 来实现定制化,来进行高性能且可定制化的流量管理:

  • 只在必要处做 request head/body 的修改,尽量避免重复实现类似 OpenAI 的 API;

  • 同时支持对请求进行缓存感知的调度,包括 kv cache least used、least of prompt、prefix-cache aware 等策略,以进一步缩短长尾 TTFT(Time to First Token) 等性能指标。

冷启动与模型加载优化

在冷启动问题上,我们重点考察了不同机型在 网络带宽、本地盘 I/O、RDMA 等方面的性能差异。虽然云原生社区已有如 Fluid 等项目可在 “1 -> N” 场景下发挥缓存加速作用,但在 “0 -> 1” 阶段,磁盘 I/O 并不总能比网络更快,有时通过 远程流式加载 直接将权重加载进 GPU memory 反而效率更高。

为此,AIBrix 在 v0.1.0 中实现了 GPU 流式加载 方案,支持在 Tensor 层面更细粒度地控制下载速度和顺序,为开发者提供灵活的组合策略。需要注意的是,若机型配有本地 NVMe 磁盘,则本地加载可能仍优于远程;而在分布式文件系统场景下,单机自我读取也能减轻对共享文件系统的集中访问压力。AIBrix 将这些能力进一步封装,开发者可基于自有机型和带宽状况,自行选择最佳加载方式。

高密度模型部署

对于精调模型(如 LoRA),实现高密度部署是释放其竞争力的关键。我们在 vLLM 项目中做了大量改动来支持 LoRA 的动态部署与度量,血缘关系追踪、LoRA metrics 单独计量等关键特征,方便与 AIBrix 控制面深度集成。但这其中依然存在若干未解决的挑战,我们正在逐步完善并计划在后续版本中支持更多功能:

  • 单容器混合部署:目前基本模型(Base Model)和精调模型(LoRA)常被打包在同一容器,虽然能减少部署节点,但也打破了容器隔离以及不可变性的原则,某些场景会因过载触发部署失败。

  • Adaptive LoRA batch、dynamic merge 等高级功能还在持续研发当中,旨在进一步提高同一 GPU 上运行多个模型或微调版本的效率。

  • 定制化内存分配器(memory allocator):在固定 GPU 资源中快速换入换出不同基础模型,利用引擎原生的 CUDA 虚拟内存(visual memory)管理能力,使多模型部署具备更好的鲁棒性与伸缩性。

04

AIBrix v0.2.0:分布式与解耦系统

分布式编排和多机推理

AIBrix v0.2.0 的核心工作在于构建分布式与解耦(Distributed and Disaggregated)系统,其中分布式部分主要关注多机推理的编排。我们在对 DeepSeek-R1 671B 模型、16 卡满配场景下进行验证后,已经实现了较为稳定的分布式推理方案。具体来说,AIBrix 采用 Ray 来编排分布式推理任务,原因包括:

  • vLLM 自带分布式 runtime:默认支持 Ray 与多进程架构,为分布式推理奠定良好基础。

  • KubeRay 场景经验积累:AIBrix 项目的核心成员曾主导 KubeRay 的开源工作,对如何在 Kubernetes 与 Ray 之间实现高效整合有着丰富的实践。目前,KubeRay 是行业通用的 Ray on Kubernetes 编排方案,被广泛应用于包括字节跳动在内的多家企业生产环境。

  • 云原生的多角色编排:在一个 CRD 中灵活编排不同容器或角色(如 TP/PP 等)并非易事,而多机调度策略也可能因具体业务场景(例如 P&D、Splitwise 论文提出的 Router/CLS、Mixed Pool 或 vLLM xPyD 等)而改变。通过 “混合编排(Hybrid Orchestration)” 理念,让 Ray 负责应用内部的角色管理,Kubernetes 则专注于升级、伸缩等通用工作,双方分工明确且更具灵活性。

   在实际实现中,我们将一个多容器推理实例视作一个 Ray 应用,用 RayCluster 来进行描述,再由   RayClusterFleet 负责升级与扩缩容等通用任务。除此之外,我们还在 vLLM 中加入了额外的弹性功能,允许集群节点在资源不足时先行等待,触发 Pod 调度与自动扩缩容后,再承接推理负载;这一改进在生产环境中显著提升了容错与鲁棒性。

KV Cache 组件管理

在 Prefix/Session Cache、P&D Disaggregation、跨机请求迁移等场景中,KV Cache 组件扮演至关重要的角色。如果仅放在推理引擎内部,诸如跨机分享 KV Cache 等操作就会非常复杂。为此,AIBrix 通过分布式 KV 缓存来应对这些挑战,不仅实现了跨引擎的 KV 复用,同时也在网络与内存效率方面进行了优化。我们的方案采用了一种可防扫描(scan-resistant)的淘汰策略,有选择地保留热点 KV 张量,从而最大程度地减少不必要的数据传输;此外,通过异步方式维护元数据更新进而降低系统开销,并在缓存与引擎的协同部署(colocation)中利用共享内存进行更快速的数据传输。

在实际部署场景中,我们发现:

  • 内存层次优化:在 prefix cache 等场景中,  如果低端 GPU 型号模型加载已经占用大部分 HBM 显存,留给 KV Cache 的空间十分有限;此时可借助空闲的 CPU DRAM 做 “二级” 缓存,能实现一定程度上的容量扩展。需要注意的是,从绝对性能角度,这种方案不可避免地会带来从 CPU DRAM 到 GPU HBM 间数据交换的额外开销,但在容量与性能间取得平衡对于某些业务仍然十分必要。

  • 灵活的淘汰策略:AIBrix 还在基于 vLLM v1 的全新架构做进一步调整,向上游社区贡献更多 KV Cache 淘汰策略的实现,敬请期待后续更新。

异构计算与成本优化

在异构资源环境中,并非所有用户都能在同一集群内获取一致的 GPU 规格,常常需要混合不同型号的 GPU 来支持同一业务。而异构卡的性能差异也会影响控制面的调度与数据面的路由。

AIBrix 针对这种需求,通过 **Profiling + ILP (整数线性规划)**的组合,找到了成本最优的机型分配和部署方案。对于异构路由策略层面的能力,目前相关功能和特性也正在开发中。

故障诊断与模拟工具

故障诊断与模拟工具链接:aibrix.github.io/posts/2025-…

AI Accelerator 故障诊断与模拟工具是 AIBrix 的系统组件,基于火山引擎容器服务 (VKE) 的经验开发,针对的是 GPU 故障和性能下降在大规模 AI 部署中构成重大挑战 -- 静默错误、过热、内存泄漏和间歇性等故障可导致模型性能下降、延迟增加,甚至系统崩溃;而在异构 AI accelerator 环境中,不同 GPU 型号在不同工作负载下表现不一致,故障诊断和自动化运维更加棘手。

  • 故障检测:目前针对不同厂商的卡型能够完成自动化故障检测, 帮助用户在影响负载之前识别性能问题。

  • 故障模拟:该工具可以模拟 GPU 的性能下降或硬件故障,方便开发者测试和构建高容错能力的 AI 系统。一旦故障发生,系统能平滑恢复,降低对整体服务的影响。

  • 硬件支持:目前已支持 NVIDIA GPU 等主流 AI 芯片,后续也将持续扩展兼容更多类型的加速器。

04

AIBrix On VKE

火山引擎容器服务已实现了 AIBrix 的组件化接入,在一系列 GenAI 场景下的基准测试中,弹性伸缩性能与 token 吞吐量提升超 10%,LoRA 应用成本最高降低 4.7 倍,模型加载提速可超 50%。收益详情如下:

在上述核心特性中,弹性伸缩是连接云上应用与云服务的桥梁。接下来,我们将着重聚焦 LLM 弹性伸缩,深入探究其在 GenAI 场景中发挥的作用以及与 VKE 结合所带来的价值。

Autocsaling On VKE

资源准备与镜像预置

VKE 通过节点池统一管理实例资源,使用节点池创建 8 台 A10 单卡实例,作为实验环境。

节点池支持包年包月、按量付费、弹性预约、Spot 等多种实例交付方式,满足不同场景下的成本与可用性需求

容器镜像方面,通过预加载的方式在实例上提前拉取 deepseek-coder-7b 模型镜像,加快 Pod 拉起速度。

端到端可观测性

VKE 集成了对网络请求流入流出、各类资源状态与利用率、Kubernetes 资源对象以及应用自身运行指标的端到端观测,并且支持应用的自定义指标透出,借助这些能力,可以全面观测 LLM 应用的运行状态。对于弹性伸缩场景,观测指标一方面用于工作负载伸缩,一方面用于观察 AIBrix 的弹性伸缩效果。

实验与结论

AIBrix 集成了多种 Pod 伸缩方法,在本例中,使用 Kubernetes 原生的水平 Pod 自动扩缩器(HPA)与 AIBrix 实现的 Kubernetes Pod 自动扩缩器(KPA,可参考 KPA)进行对比。

LLM 应用负载,使用 vllm 运行 deepseek-coder-7b,弹性伸缩指标使用 vllm:gpu_cache_usage_perc,访问请求从 ShareGPT 中随机抽取,并以指定的并发数将这些请求分发给该服务。对于 HPA,AIBrix 会创建一个 Kubernetes 原生的 HPA 实例,以扩展指标的方式进行伸缩。对于 KPA,AIBrix 实现了其完整的流程,包括指标收集、对目标部署状态的定期监控以及伸缩操作。

实验数据如下所示。AIBrix 支持直接从 Pod 中拉取关键指标,因此伸缩响应速度获得显著提升,大模型应用首次伸缩响应耗时 12 秒, 相比 HPA 的 67 秒耗时加速 82%。AIBrix 的完整扩容周期为 120 秒,而 HPA 为 320 秒,加速 62.5%,并且震动频次降低 33%。

05

写在最后

AIBrix 的目标是将大模型推理的 “系统侧” 能力与 “引擎侧” 创新完美结合,提供从资源调度、网络流量控制到分布式推理的端到端解决方案。通过与 vLLM 开源社区的深度协作,我们希望不断迭代并完善在云原生环境下的大模型部署架构,让企业能够更加轻量、弹性地构建面向生产的 LLM 推理服务。

在 AIBrix 开发过程中,我们的很多创新想法都受到了学术研究的启发,比如 Preble、Melange、QLM 和 MoonCake 等,在这里我们真诚地感谢这些成果背后的研究人员。我们也非常感谢 vLLM 社区的支持,使 AIBrix 成为了 vLLM 的控制面,进一步增强了我们构建可扩展和高效 AI 基础设施的使命感。

AIBrix 由字节跳动开源,现在正在开源社区的支持下成为一个完全开源的项目——目前项目已经吸引了来自密歇根大学、伊利诺伊大学厄巴纳 - 香槟分校、华盛顿大学、Google、DaoCloud 等学术界和工业界的开源伙伴。未来,我们也希望 AIBrix 能通过开放、协作的方法塑造 AI-Infra 的未来,持续将顶尖学术研究和行业内的生产级实践结合起来。也欢迎更多开发者和企业加入我们,为开放、可扩展的 AI 基础设施的未来做出贡献:github.com/vllm-projec…

vArmor:云原生容器安全的多场景应用实践

English Version: vArmor: Multi-scenario Application Practices for Cloud-Native Container Security

项目地址:github.com/bytedance/v…

项目官网:varmor.org

简介

vArmor 是字节跳动开源的云原生容器沙箱系统,它借助 Linux 的 AppArmor LSMBPF LSMSeccomp 技术进行容器加固。用户可以通过 vArmor 的 CRD API 在 Kubernetes 集群中管理安全策略,对指定工作负载的容器进行加固。vArmor 旨在降低利用现有技术加固容器的门槛和成本,从而平衡安全风险与防护成本。

本文将介绍我们推出 vArmor 项目的目的,然后从技术角度出发介绍其在不同场景的应用。本文将向您展示如何凭借vArmor 的技术特性来解决特定问题,从而实现技术与业务目标,助力企业构建云原生环境下的安全防线。

为什么推出 vArmor

容器运行时组件和 Kubernetes 早已增加了对 LSM、Seccomp 的支持,其中 Seccomp 在 Kubernetes v1.19 GA,AppArmor LSM 在 Kubernetes v1.30 GA。用户可以自行编写和管理 AppArmor、SELinux、Seccomp Profiles,并在工作负载中配置安全策略对其进行加固。几乎所有的容器运行时组件都附带了默认的 AppArmor 和 Seccomp 安全策略,但默认的 Seccomp 策略需要显式设置才会为容器开启,而默认的 AppArmor 策略需要操作系统支持才会为容器自动开启。

充分利用 Linux 系统的安全机制可以有效加固容器。例如通过 LSM、Seccomp 等技术对容器进程进行强制访问控制,可以减少内核攻击面、增加容器逃逸或横向移动攻击的难度与成本。它们的基本原理如下图所示。

然而,编写和管理安全策略则面临诸多挑战:

  • 容器运行时组件的默认安全策略存在局限性,无法防御某些漏洞、错误配置风险,也不能限制攻击者在容器内的渗透行为。

  • 构建 AppArmor、Seccomp、SELinux Profile 需要专业知识。

  • 为复杂且快速迭代的容器化应用制定健壮的安全策略(尤其是 Deny-by-Default 模式的策略)难度较大。

  • AppArmor 或 SELinux LSM 依赖操作系统发行版,存在一定局限性。

  • 在 Kubernetes 环境中,自动化管理和应用不同的安全策略比较复杂。

为了解决这些问题,vArmor 应运而生。它提供了多种策略模式、内置规则和配置选项,vArmor 会根据策略对象的定义,管理安全策略(AppArmor Profile、BPF Profile、Seccomp Profile)对不同工作负载的容器进行加固。vArmor 还基于 BPF 和 Audit 技术实现了行为建模功能,可以对不同应用进行行为采集并生成行为模型,从而辅助构建安全策略。

例如,用户可以按需配置策略对象,实现违规拦截、拦截并告警、只告警不拦截三种效果,并使用内置规则和自定义规则动态更新策略对象,从而满足不同应用场景的需要。下面我们将用几个实际应用场景来展示 vArmor 如何助力企业提升云原生环境中的容器安全防护能力。

vArmor 的应用场景

多租户隔离

多租户应用的风险

现代 SaaS 应用程序大多采用多租户模式,严重的漏洞及相应的利用链,极有可能致使恶意用户得以访问其他租户的数据。随着大语言模型时代来临,云服务的使用量还会进一步增长。因此,构建此类服务的人员更需关注多租户隔离风险并采取防范举措,以降低跨租户攻击的风险。

下图是 Wiz 在 PEACH 框架中描绘的一个典型跨租户攻击序列 [1]:

大量案例表明,跨租户漏洞、漏洞利用链的根因主要包括:

  • 用户接口复杂度较高,接口中的无害 bugs、features 加剧风险

  • 多租户共享组件实现不当。

  • 多租户独占组件安全边界实现不当。

针对这些问题,可采取以下缓解措施:

  • 减少用户接口复杂度

  • 将共享组件转变成租户独占组件

  • 提升租户独占组件的隔离性

如何选择加固方案

Wiz 在 PEACH 框架中指出,针对多租户应用,应根据安全建模结果,综合合规、数据敏感度、成本等因素选择租户隔离技术方案。企业可以通过选择不同类型的安全边界和防御技术,将不可控风险转化为可控成本。

租户隔离用于弥补由于接口的复杂性而带来的多租户隔离安全风险。而接口复杂度则与漏洞出现概率正相关,下表描述了接口复杂度的简单评估方法 [1]。

Interface type Typical input (example) Typical process Complexity level
Arbitrary code execution environment Arbitrary Execution High
Database client SQL query Database operation High
Arbitrary file scanner Arbitrary Parsing Medium
Binary data parsing Protobuf Parsing Medium
Web crawler JavaScript Rendering Medium
Port scanner Metadata Parsing Low
Reverse proxy Arbitrary Proxy Low
Queue message upload Arbitrary Proxy Low
Data entry form String Parsing Low
Bucket file upload Arbitrary Storage Low

因此,对于复杂接口(如支持租户执行任意代码的组件),建议选择高隔离等级的安全边界来保障租户数据安全,例如基于轻量级虚拟机技术的容器。对于不复杂的租户场景和接口,如文件解析、数据解析、网页渲染、文件上传等,可考虑使用 vArmor 等技术方案进行加固。

还需要做什么

由于 runc + vArmor 的隔离等级不及硬件虚拟化容器(如 Kata Container 等轻量级虚拟机容器),因而无法防御所有容器逃逸漏洞。因此,在使用 vArmor 加固多租户应用时,需假设高级攻击者可能会利用漏洞逃逸到宿主机。我们建议您配合以下安全实践,来增加攻击者逃逸后进一步攻击的难度和成本,并及时发现攻击行为。

  • 租户负载应满足 Pod Security Standard 的 Baseline 或 Restricted 标准 [2],并使用 NetworkPolicy 等技术实施网络微隔离。

  • 制定合理的调度策略,避免不同租户负载调度到同一个节点。

  • 不同租户使用独占命名空间,以最小权限原则授予租户负载有限的 Kubernetes RBAC 和 IAM 权限,避免授予敏感权限。敏感 RBAC 权限列表可参考 Palo Alto Networks 发布的白皮书 [3]。

  • 制定合理的调度策略,将具有敏感 Kubernetes RBAC 和 IAM 权限的系统组件负载调度到专用节点池,确保租户负载所在节点不存在可被滥用的服务账号和用户账号。

  • 系统组件的敏感接口应开启身份认证和鉴权,避免未授权漏洞。

  • 引入入侵检测系统,在主机、Kubernetes 层面进行入侵检测和防御,及时发现并响应入侵行为。

核心业务加固

加固的收益

虽然业内已经推出了一些基于硬件虚拟化技术和用户态内核的强隔离方案(例如 Kata、gVisor 等),但由于其技术门槛和成本较高,使得 runc 容器仍将是大部分业务场景的主流。用户在享受 runc 容器带来的性能与便捷时,也面临着诸如容器隔离性较弱的安全问题。例如近年来 Linux 内核、runc 组件、容器运行时组件的漏洞频发,每隔一段时间就会有新的漏洞可被用于容器逃逸等攻击;许多企业在容器化应用设计、开发、部署时,也易因错误设计和配置引入逃逸风险。

Verizon 发布的研究报告 [4] 表明,企业在补丁可用后平均需 55 天才能解决 50% 的关键漏洞,而影响基础设施的漏洞修复时间可能更长。当某个高危漏洞被全量修复后,可能又有新的漏洞出现并等待修复。在漏洞修复期间,企业往往缺乏除了入侵检测以外的防御措施。

使用 vArmor 的理由

vArmor 的以下特性,使其成为加固核心业务的选择:

  • 云原生:遵循 Kubernetes Operator 设计模式,贴近云原生应用开发和运维习惯,从业务视角加固容器化应用,因此易于理解和上手。

  • 灵活性:策略支持多种运行模式(例如 AlwaysAllow、RuntimeDefault、EnhanceProtect 模式),可动态切换且无需重启工作负载。支持拦截、拦截并告警、仅告警不拦截三种特性,有助于策略调试和安全监控。

  • 开箱即用:基于字节跳动在容器安全领域的攻防实践,提供了一系列内置规则,用户可按需在策略对象中选择使用。vArmor 会根据策略对象的配置,生成和管理 Allow-by-Default 模式的 AppArmor、BPF、Seccomp Profile,降低了对专业知识的要求。

  • 易用性:提供了行为建模功能、策略顾问工具,从而辅助策略制定,进一步降低了使用门槛。

常见用法

vArmor 丰富的特性为安全策略的制定和运营提供了多样的选择,以下是一些常见的使用方式:

  • 仅告警不拦截模式(观察模式) :将沙箱策略配置为仅告警不拦截模式,通过采集告警日志来分析安全策略对目标应用的影响。

    •     spec:
            policy:
              enforcer: BPF
              mode: EnhanceProtect
              enhanceProtect:
                # AuditViolations determines whether to audit the actions that violate the mandatory access control rules. Any detected violation will be logged to /var/log/varmor/violations.log file in the host.
                # It's disabled by default.
                auditViolations: true
                # AllowViolations determines whether to allow the actions that are against the mandatory access control rules.
                # It's disabled by default.
                allowViolations: true
      
  • 拦截并告警模式:沙箱策略制定完成后,可调整为拦截并告警模式运行,并持续采集告警日志。从而实现对目标工作负载的强制访问控制,并及时发现违规行为。

    •     spec:
            policy:
              enforcer: BPF
              mode: EnhanceProtect
              enhanceProtect:
                # AuditViolations determines whether to audit the actions that violate the mandatory access control rules. Any detected violation will be logged to /var/log/varmor/violations.log file in the host.
                # It's disabled by default.
                auditViolations: true
      
  • 高危漏洞应对:当出现高危漏洞时,您可以基于漏洞类型或利用向量分析对应的缓解方案,并通过更新策略对象(添加内置规则、自定义规则),在漏洞修复前进行防御。

spec:
  policy:
    enforcer: BPF
    mode: EnhanceProtect
    enhanceProtect:
      # The custom AppArmor rules:
      appArmorRawRules:
      - rules: |
          audit deny /etc/hosts r,
          audit deny /etc/shadow r,
      - rules: "audit deny /etc/hostname r,"
        targets:
        - "/bin/bash"
      # The custom BPF LSM rules:
      bpfRawRules:
        processes:
        - pattern: "**ping"
          permissions:
          - exec
        network:
          egresses:
          - ip: fdbd:dc01:ff:307:9329:268d:3a27:2ca7
          - ipBlock: 192.168.1.1/24
            port: 80
          sockets:
          - protocols:
            - "udp"
      # The custom Seccomp rules:
      syscallRawRules:
      - names:
        - fchmodat
        action: SCMP_ACT_ERRNO
        args:
        - index: 2
          value: 0x40     # S_IXUSR
          valueTwo: 0x40
          op: SCMP_CMP_MASKED_EQ
        - index: 2
          value: 0x8      # S_IXGRP
          valueTwo: 0x8
          op: SCMP_CMP_MASKED_EQ
        - index: 2
          value: 1        # S_IXOTH
          valueTwo: 1
          op: SCMP_CMP_MASKED_EQ
  • 策略影响排查:当用户怀疑沙箱策略影响目标应用正常执行时,可将策略模式动态切换为 AlwaysAllow、RuntimeDefault 模式排查(注:已启动容器的 Seccomp Profile 不支持动态更新)。
kubectl patch vcpol $POLICY_NAME --type='json' -p='[{"op": "replace", "path": "/spec/policy/mode", "value":"AlwaysAllow"}]'
  • 行为建模模式:使用实验功能 —— 行为建模模式,对目标应用进行建模。建模完成后使用策略顾问来生成沙箱策略模版,辅助沙箱策略的制定。
spec:
  policy:
    enforcer: AppArmorSeccomp
    mode: BehaviorModeling
    modelingOptions:
      # The duration in minutes to modeling
      duration: 30

特权容器加固

特权容器的定义

特权容器通常指包含 .securityContext.privileged=true 设置的容器,此类容器被授予全部 capabilities,可访问宿主机所有设备和内核接口。本文将所有拥有打破隔离性配置的容器称为 “特权容器” ,包括但不限于 privileged container、sensitive capabilities、sensitive mounts、shared namespaces、sensitive RBAC permissions。

许多企业因历史遗留问题、系统设计需求、安全意识不足等原因,在生产环境的业务负载和系统组件中引入了 “特权容器”。然而,这些容器的风险配置容易被攻击者利用,从而导致容器逃逸、横向移动等攻击。例如在 Wiz 披露的 BrokenSesame [5] 漏洞利用链中,容器间共享 PID ns、管理容器具有特权等风险设计和错误配置,就可被攻击者利用进行横向移动和权限提升攻击。

降低特权容器的风险

我们建议企业优先以最小权限原则评估并移除导致 “特权容器” 的风险配置。并在无法移除时,使用强隔离级别的安全边界来加固容器。

vArmor 可以作为补充,在彻底消除 “特权容器” 的安全风险前提供一定的加固能力。用户可利用 vArmor 提供的内置规则自定义规则来限制潜在攻击者的行为,阻断已知的攻击手法,提升攻击成本和入侵检测几率。vArmor 内置了 “容器加固”、“攻击防护” 和 “漏洞缓解” 三类规则,并且还在不断更新。在 “容器加固” 类规则中,vArmor 专门为 “特权容器” 安全风险内置了一系列规则,可用于阻断一些已知的攻击手法。

例如,在拥有 CAP_SYS_ADMIN capability 的容器中,通过改写宿主机的 core_pattern 来逃逸容器是常见的攻击手法。如下所示,攻击者可以通过挂载新的 procfs、重新挂载 procfs、移动 procfs 挂载点等方式获取宿主机 core_pattern 文件的写权限。

# mount a new procfs
mkdir /tmp/proc
mount -t proc tmpproc /tmp/proc
echo "xxx" > /tmp/proc/sys/kernel/core_pattern


# bind mount a procfs
mount --bind /proc/sys /tmp/proc
mount -o remount,rw /tmp/proc /tmp/proc
echo "xxx" > /tmp/proc/sys/kernel/core_pattern

使用 vArmor 的内置规则disallow-mount-procfs可阻断此利用向量。

policy:
  enforcer: BPF
  mode: EnhanceProtect
  enhanceProtect:
    hardeningRules:
    - disallow-mount-procfs
    # Privileged is used to identify whether the policy is for the privileged container.
    # Default is false.
    privileged: true

辅助特权容器降权

企业生产环境中往往存在许多“特权容器”,虽然大量研究报告和案例都阐明过使用“特权容器”的危害,但企业可能仍然难以对已有的“特权容器”进行降权,也无法按照最小权限原则授予新增容器必要的 capabilities。

vArmor 提供了实验功能 —— 行为建模模式。用户可以创建此模式的安全策略,并在指定时间范围内收集和处理目标工作负载的行为。建模结束后,vArmor 会生成一个 ArmorProfileModel 对象,用来保存目标工作负载的行为模型。当行为数据较多时,行为数据会被缓存在数据卷中,用户可以通过对应接口将其导出。


spec:
  policy:
    enforcer: AppArmorSeccomp
    # Switching the mode from BehaviorModeling to others is prohibited, and vice versa.
    # You need recraete the policy to switch the mode from BehaviorModeling to DefenseInDepth.
    mode: BehaviorModeling
    modelingOptions:
      # The duration in minutes to modeling
      duration: 30

行为数据包括目标应用所需的 capability、执行的进程、读写的文件、调用的 syscall 等,用户可以利用这些信息来辅助降权。请参考使用说明进一步了解如何使用 vArmor 的行为建模功能。注:当前仅 AppArmor 和 Seccomp enforcer 支持行为建模功能。

兼容性说明

vArmor 依托 Linux 系统的安全机制(AppArmor LSM、BPF LSM、Seccomp)来实现容器加固,其中

  • AppArmor enforcer 需系统启用 AppArmor LSM
  • BPF enforcer 需 Linux 5.10+ 内核版本支持

目前 vArmor 兼容 Kubernetes v1.19+ 版本,支持包括 AWS EKS、Azure AKS、Google GKE、火山引擎 VKE、阿里云 ACK 等主流云厂商的托管 Kubernetes 服务。其轻量化设计具备四大优势:

  1. 原生级性能损耗:依托内核安全子系统,不显著增加上下文切换和数据拷贝开销

  2. 无需额外硬件:纯软件实现

  3. 无环境绑定:不依赖特定操作系统发行版

  4. 零侵入性:保持集群及容器运行时组件的默认配置

总结

vArmor 针对当前容器安全领域在安全策略编写与管理方面的难题,提供了有效的解决方案。在多租户隔离场景下,尽管无法达到硬件虚拟化容器的隔离级别,但通过配合一系列安全实践,可降低跨租户攻击风险;在核心业务加固方面,vArmor 凭借云原生、灵活、开箱即用和易用等特性,为企业在享受 runc 容器性能与便捷的同时,提供了有效的安全防护手段;对于特权容器,vArmor 既能通过内置和自定义规则加固,阻断常见攻击手法,又能利用行为建模功能辅助降权。

vArmor 以其丰富的特性和灵活的应用方式,为容器安全提供了全面且实用的保障,助力企业在云原生环境中平衡安全与业务发展的需求。真诚欢迎社区开发者和企业加入社区,与我们一起参与项目共建。

引用

  1. PEACH: A Tenant Isolation Framework for Cloud Applications
  2. Kubernetes Privilege Escalation: Excessive Permissions in Popular Platforms
  3. Pod Security Standards
  4. 2024 Data Breach Investigations Report
  5. #BrokenSesame: Accidental ‘write’ permissions to private registry allowed potential RCE to Alibaba Cloud Database Services

285 学科全覆盖!豆包大模型团队开源基准测试集 SuperGPQA

近日,豆包大模型团队开源 SuperGPQA,一个领域全面且具备高区分度的知识推理基准测试。

该数据集构建了覆盖 285 个研究生级学科、包含 26529 道专业问题的评估体系,不仅涵盖主流学科,更将轻工业、农业、服务科学等长尾学科纳入其中,展现出全面学科的覆盖广度,填补了长尾知识评估领域的空白。

如今,SuperGPQA 已被用于揭示开源模型与闭源方案之间的显著性能差距,为 AI 发展提供了关键评估工具和跨学科分析框架。

随着大语言模型在通用学科中的表现逐渐接近人类水平,研究焦点也随之转向其在真实世界专业领域的应用。然而涉及人类研究领域的长尾学科时,由于有效评估的缺乏,LLM 的能力边界依然模糊不清。

为了全面衡量 LLM 的泛化能力与推理上限,字节跳动豆包大模型团队联合 M-A-P 开源社区推出基准测试 SuperGPQA,这一基准不仅覆盖了二百余个研究生级学科,还确保 42.33% 的题目需要数学计算或形式推理,构建了兼具广泛学科覆盖与复杂问题设计的评估新范式。

实验结果显示,DeepSeek-R1 在 SuperGPQA 上的准确率为 61.82%,在不同知识领域中,当前大语言模型性能仍有很大提升空间,这也进一步凸显 SuperGPQA 在评估模型真实能力方面的重要性和必要性。

⽬前论⽂成果和数据代码仓库均已对外公开,欢迎开源使用!

SuperGPQA: Scaling LLM Evaluation across 285 Graduate Disciplines

论文链接: arxiv.org/pdf/2502.14…

数据链接: huggingface.co/datasets/m-…

代码链接: github.com/SuperGPQA/S…

1. 现有评测基准学科占比失衡,长尾学科覆盖不足 5%

现有大语言模型评估体系主要面临两大核心困境:学科覆盖的严重失衡与评测基准的挑战性失效。

以 MMLU 和 GPQA 为代表的传统基准尽管在数学、物理等主流学科中建立了标准化测试框架,但其覆盖的学科数量通常不足 50 个,仅占人类知识体系的冰山一角。据统计,现有基准对轻工业、农业、服务科学等长尾学科的覆盖率甚至不足 5%。

多基准多维度对比雷达图

不同基准下最新模型的性能对比

更为严峻的是,现有评测体系失去区分度,无法有效衡量模型在真实复杂场景中的推理上限。比如,主流模型如 GPT-4o、DeepSeek-R1 在传统基准上准确率已突破 90%。

这主要源于传统基准构建范式的单一化数据来源与粗放化质量筛选。比如,不加辨别地依赖教科书例题或在线题库(例如 GPQA 中 42% 的问题来自维基百科),导致题目缺乏专业深度,且易被模型通过记忆机制 “破解”。实验发现,GPT-4o 对在线练习网站答案的重复率高达 67.3%,暗示其性能提升可能源于题目数据泄露而非真实推理能力。

此外,众包标注的专业水平参差和主观性问题评估难度进一步加剧了基准的不可靠性——早期尝试中,仅 37% 的众包标注问题通过专家审核,导致超过 60% 的标注资源浪费。

这使得我们无法准确评估模型的泛化能力和推理能力,严重阻碍了模型性能的进一步提升。

2. 首次全覆盖 285 个学科,探索 LLMs 真实能力边界

为突破以上限制,豆包大模型团队和 M-A-P 历时半年推出 SuperGPQA,一项全面的基准测试,实现 285 个研究生级学科全覆盖,旨在探索最先进的大语言模型潜力边界。

  • 全面学科覆盖 SuperGPQA 覆盖 13 个门类、72 个一级学科和 285 个二级学科,共 26,529 个问题,远超现有的 GPQA(448 题)和 MMLU-Pro(12,032 题),平均每题将会提供 9.67 个选项,挑战性显著高于传统的 4 选项格式。同时,它突破传统评测集仅侧重 STEM 学科的局限,兼顾科学、工程、医学等 STEM 学科与哲学、文学、历史等非 STEM 学科问题,且具有较高区分度。

  • 多样的难度分布: 问题难度在各学科间均衡分布,尤其在工程和科学领域,难题比例较高。42.33% 的问题需要数学计算或严谨推理,确保模型在高难度任务中的表现。

  • 丰富的语义结构: 通过 t-SNE 可视化,评测集 SuperGPQA 展示了跨学科的聚类模式,工程和科学类问题在语义上高度相似,人文学科则保持独特的知识中心,体现了领域特定的语言特色。

  • 一致的题目设计: 平均问题长度为 58.42 字,选项长度一致,增强了迷惑性和挑战性,确保评测的公平性与可靠性。

3. 专家 - LLM 协同,提高题库质量

SuperGPQA 的核心架构由三个关键阶段组成:源筛选、转录和质量检验。该过程涉及 80 多名专家标注员、交互式专家 - LLM 协作系统,为未来类似规模的研究项目提供了方法指导。

SuperGPQA 数据收集处理流程

  • 源筛选

为确保题目的高标准质量,团队摒弃了众包注释员收集资源的方式,转而由专家注释员负责从可信来源(如教科书和权威练习网站)筛选和收集原始问题,并要求提供来源截图。这一策略避免了早期大量无效问题的产生,提升了质量检查的效率和准确性。

  • 转录

在转录阶段,专家注释员对收集的原始问题进行语言规范化和格式转换,确保所有问题具备统一的学术语言和标准的多项选择题格式。团队发现,即使是最先进的语言模型(LLMs)在生成干扰项时也存在漏洞,因此需要专家统一重写,以提高干扰项的准确性和有效性,确保题目的挑战性和区分度。

  • 质量检验

团队在质量检验阶段采用三层检查机制,以保证数据集的整体质量:

1)基于规则的初步过滤: 识别并过滤格式明显不合规范的题目。

2)基于 LLM 的质量检查: 利用多个先进的 LLMs,如 GPT-4、Gemini-flash 等,进行有效性、负面和极端询问检测、多模态排除、领域相关性评估和区分度标记。通过多模型协作,不仅提升效率,还降低数据泄漏风险。

3)专家复审: 由专家注释员对可疑题目进行二次审核,确保最终题库的高可靠性和高区分度。

4. 最优推理模型仍有进步空间

发布评测基准的同时,研究团队也基于 SuperGPQA 对全球 6 个推理模型、28 个聊天模型和 17 个基础模型进行了评测,涵盖闭源、开源和完全开源三类模型。

其中,推理模型和聊天模型采用零样本评估,基础模型采用五样本评估(方法与 MMLU-Pro 类似),并将温度参数设置为 0,推理模型最大生成 token 数为 32K,其他模型为 4K。

我们的实验结果表明,在不同的知识领域中,当前最先进的大语言模型性能仍有很大提升空间,如当前最优模型 DeepSeek-R1 在 SuperGPQA 上的准确率仅为 61.82%。具体评测结果如下图所示:

LLMs 在不同划分层级的表现

LLMs 在不同学科的表现

  • 指令微调显著提升性****能

DeepSeek-V3 和 Qwen2.5-72B-Instruct 的得分(47.40 和 40.75),远超其基础版本得分(32.14 和 34.33),验证了指令微调的有效性。

  • 大模型表现更均衡

DeepSeek-R1 在简单(63.59)、中等(63.63)和困难(56.87)题目上均表现优异。相比之下,Qwen2.5-14B-Instruct 在同类别题目上的表现差距较大(44.82、37.90、19.97)。

  • 推理模型训练范式仍有待优化

DeepSeek-R1 与 DeepSeek-R1-Zero 性能差距不大,尤其在科学与工程领域,后者稍占优势,表明最佳训练方法尚未确定。

  • 预训练语料库的持续优化

LLM 系列如 Qwen-max、GPT-4o 模型系列在 SuperGPQA 上的表现随着时间显著提升,显示开发者高度重视长期知识的融入。

  • 开源模型面临挑战

尽管透明 LLM 如 MAP-Neo-7B 和 OLMo-2-1124-13B 表现尚可,但与业界的非透明开源和闭源模型相比,尤其在困难题上仍显不足。

  • 不同能力的模型表现差异

其中,Doubao-1.5-pro 以 55.09% 的准确率在 Chat Models 中位列第一,我们发现,通用大语言模型(如 Doubao 系列)在常见专业问题的知识回忆方面表现不错,但在长尾领域的推理方面存在困难。

o3-mini 系列在简单和中等难度题目的分数低于 Doubao-1.5-pro ,但在困难问题上却明显超过它,说明推理模型在难题上表现突出,却在广度知识覆盖方面存在不足。

5. 历时半年,探索模型真实能力边界

SuperGPQA 评测集搭建历时半年,近百位学界学者及硕博同学、业界工程师参与标注。通过 LLM - 专家协作的构建流程、285 学科全面覆盖和多样难度分布设计,SuperGPQA 填补了长尾领域专业评估的空白,有望成为衡量 LLM 泛化能力与推理上限的关键工具。

其实验结果不仅揭示了当前模型能力与通用人工智能之间仍存在巨大差距,也为 AGI 发展提供了跨学科分析框架。未来我们也将进一步扩展数据集范围、改进人类与模型协作标注模式,以应对快速演进的人工智能技术挑战。

AI 与星辰大海:2025,从新手到开挂勇士的奇幻旅程

作者:Data-TnS-Engineering-FE 团队

前言

曾几何时,代码敲击声回荡在深夜的办公室,你是否也曾幻想过有一个全能助手替你分担工作?如今,这个美好的愿景不再是空中楼阁。

想象一下,当你正为产品设计苦思冥想时,突然耳边传来 AI 的灵感火花;

开发过程中,AI 像是个比你还了解自己的最佳拍档,为你提供独到的建议;

当繁琐的测试工作如排山倒海而来,它早已帮你先行解决那些隐秘的 Bug;

交付环节则如同一个老练的质检专家,在每一个细节上都帮你擦亮眼睛;

而在运维阶段,它更是你的“夜间守卫者”,早早预警潜在问题;

有人说,AI 的到来让开发者的身份发生了质的飞跃,从“头发稀疏的代码独行侠”变成了“开挂勇士”。在 AI 的协助下,谁不想在产品设计上满怀创意、在代码编写上行云流水、在测试中无懈可击,在运维中高枕无忧呢?

在这个充满奇思妙想的科技时代,AI 既是你的忠实伙伴,又是你的全能助手,更是一位风趣的导师。它不知疲倦地助你不断攀登职业高峰,让每一步开发都像是一场精彩绝伦的探险。是时候跳出你的舒适区,体验 AI 如何点亮开发者们的星辰大海。

准备好了吗?接下来,我们将揭开这段 AI 助力开发的奇妙旅程,从产品设计到运维管理,为你重新定义“效率”、“质量”和“体验”。 每一刻都充满了独特的惊喜和乐趣,相信这将是一段你不想错过的神奇之旅。让我们一起踏上这段充满无限可能的旅程,重新发现开发的独特魅力与无穷乐趣。

在 AI 的魔法世界里,开发者不再是孤军奋战的英雄”

助力业务发展

随着 LLM 能力的迭代和更新,越来越多过去 LLM 无法很好解决的问题重新进入了大家的视野。在人审领域下人们一般都会围绕着 LLM 是否可以完全替代审核员对内容进行审核进行讨论与探索。在进一步进行详细的 LLM 赋能人工审核流程相关的例子前我们需要先了解一下“人类审核员”面对的一些挑战和要求:

  • 能处理复杂的内容(e.g.可能既可以是 A,也可以是 B,还可以是 C)并给出最合适的答案
  • 质量 & 稳定性:对于相同类型的内容要给出相同的结果
  • 效率:在不损伤质量的前提下需要达成一些数量上的要求

先说结论目前 LLM 的能力还无法完全替代审核员,更多的是在各方面提供辅助从而提升审核员的审核质量和效率。所以在产品结合的思路上,我们主要关注在如何利用 LLM 简化或加速审核员对于单一任务的操作并完成了一些功能的落地。

前置思考

说到 LLM 最出名的当属于 ChatGPT 了,如 GPT-4o

当我们思考 LLM 能为审核员带来什么样的辅助时我们首先想到的就是如何利用这些现有的天花板模型。这些模型一般都有良好的指令执行能力,并且在处理一些通用文字相关的问题时一般具有非常好的表现。

例如我们可以提供一段文字内容给模型,并告诉模型我们希望它帮助我们从中提取出不符合某些规则的内容如:潜在的语言攻击,歧视内容,潜在色情内容等等。

当有了模型识别出来的内容后可以通过一些特殊的形式展示这些信息如高亮等。 这可以帮助审核员在审核过程中快速捕捉到内容中存在的潜在风险并加速其对当前内容的审核。听起来是不是很简单?实际上将一个如此简单的辅助能力从离线验证到最终上线需要考虑的远远不止这些:

  • 成本:模型是以 token 来进行计算的。一段文字会先被转换成 token 然后再传给模型,同时模型输出的也是一堆 token 然后会再被转换成我们看得懂的语言。所以如何无损高效的压缩传给模型的内容是一个非常重要的课题。(如删除重复的内容等)
  • 合规:因为模型在迭代过程中需要大量的训练数据,厂商都会收集模型在实际使用过程中的数据以补充其训练数据集。如果将一些敏感或隐私数据不经过处理的直接传给模型可能会带来合规风险。(如公司的保密数据,或者用户的不公开信息等)我们可不希望当其他人问模型你的银行卡密码时模型能准确无误的回答上来hhh。
  • 时延:模型能力强大是有取舍的。可以姑且先理解为(在计算资源不变的前提下)模型的规模越大->模型的能力越强 -> 每次回答你问题的速度就越慢。同时给模型输入越多时返回的时长也会相应的延长。
  • 其他 n+ 问题:服务可用性,模型拒绝回答,模型选型等等...

但是由于模型能力的局限性,我们只能对标准文字内容审核提供符合标准的辅助能力。那对于像视频或者音频中出现的文字,或者话语或者一些复杂的问题怎么办?

例如我们想对于一个歌曲中的歌词进行风险识别。

这个时候我们就要引入分步的解决思路,歌曲中歌词的风险识别可以被拆分成如下工作流

暂时无法在飞书文档外展示此内容

举例来说,在预处理环节我们可能会需要对 ASR 转换的文字内容进行整理如加入标点符号,分句断句等。模型对于长文字内容的处理能力会随着内容变长而下降。我们需要根据业务的诉求和实际情况进行灵活调整。抛去 ASR 环节不说,后边三步可以有两种实现思路:

  1. 在一个 Prompt 中通过分步的方式指导模型进行处理

    1. 会更好的保留整体的上下文,但是可能会碰到如内容太长超出 context window size 的情况并且随着内容的变长模型的完成时间也会变慢。
  1. 将每一步拆分开然后通过串行调用的方式完成多次模型调用。

    1. 可能会丢失一些上下文内容,但是因为进行拆分后每一部分的长度都是相近的所以在模型响应时长和 context window 上限的问题上则有比较好的表现。

如果我们想让模型帮我们翻译火星文呢(没办法,业务上就是有这个诉求 hhh)

对于一些有高定制化诉求的场景普遍厂商也会开放对现有模型就行二次训练的能力。比如对于将内容从 A 语言翻译到B 语言并且对语言风格/用词有明确需求时,可以考虑对基本模型做一次 SFT(Supervised Fine Tuning) 。也就是对自身特殊诉求收集数据集并使用这个数据集对模型进行二次训练以达到更好的表现。

如果你说识别什么的还是太复杂了,有没有更简单的应用场景?

在实际产品开发过程中,我们经常性的需要对圈定/给定的一组数据进行频繁的离线验证以确保目前的能力表现是符合我们预期的。又或者在产品迭代的过程中我们时常需要一个指标/分数来量化当前的能力/体验。举例来说,在进行多目标语言混合翻译的能力开发过程中,我们在 prompt enginnering 过程中需要时刻关注模型的表现并确保每一次修改都不会对能力造成较大的退步。Multidimensional Quality Metrics (MQM) 是一个多维度指标翻译质量分析框架。我们可以通过自然语言的方式向模型输入我们的对于(译文与原文对比)不同维度上的要求如:

  • 翻译准确性

    • 是否丢失一些信息
    • 是否凭空捏造了一些信息
    • 翻译错误/未准确翻译
    • 未翻译
    • ...
  • 流畅度

    • 语法是否正确
    • 标点是否正确
    • 拼写
    • ...
  • 风格

    • 是否用了一些抽象的词

    • 是否是正式文风

    • ...

得益于 LLM 优秀的自然语言处理能力和跨语言能力,LLM 可以基于我们输入的多个维度来对译文进行评估并最终给出评估结果。需要注意的是为了保证模型输出结果的准确性我们一般不会直接要求模型输出如:0-10 分的打分。我们会尽量让模型在一个给定的状态下进行枚举。如:严重程度(严重, 普通, 可忽略不计)并通过代码对枚举进行映射后计算出最终的得分。这样可以最大程度上避免 LLM 输出不稳定和幻觉等问题。

实际上 MQM 这种多维度评估体系也可以应用在翻译之外的领域,如润色、故事生成等。甚至可以被用在图片打分。他们背后的原理都是类似的,都是通过发挥 LLM 出色的自然语言理解能力 + 通过自然语言描述框架来实现一些复杂的打分工作。

功能落地

上边我们只是利用了 LLM 的自然语言处理能力。如果我们想让模型帮我们回答一些不是通识性知识点的问题时该怎么办呢?

暂时无法在飞书文档外展示此内容

模型也可以像人一样,碰到不会的东西时可以先去查询然后再基于查询结果进行判断和回答

比如说我们想为审核员提供一个问答机器人回答一些审核领域相关的问题,或者基于以往的审核结果进行回答。

又或者我们想基于某一个数据库中的结果对审核员的问题进行回答

但是对于不同类型的内容,知识库在生产过程中采用的分割策略,结构等会大大影响最终问答质量的表现。如:

  • Chunking: 如当对于大段内容进行拆分时要拆分的多细,拆分后的信息是否应该保留整体上下文方便后续参考时使用等。
  • Embedding Model: 需要针对需要支持的语言来选择向量化模型,不同的模型在召回准确度上也有不小的区别。

当我们检索到的相关知识后可以通过将这些信息连同原始问题一并作为输入给模型并让模型基于相关知识点尝试回答。

当我们积累了一系列能力后,如果快速的向其他方向上推广和扩展?

随着越来越多基于 LLM 能力的需求落地,我们发现其实所有的 LLM 能力都可以被总结为:一个带顺序的分步流程。其中模型调用只是工作流中的一个步骤/节点。如果我们能快速的复用这个分步流程就可以快速的对取得成功的能力进行推广。

(分步流程可视化示意图)

随着我们对现有的一些优秀 LLM 编排能力库/平台的深入了解,我们发现现有的方案都无法很好的对我们的业务场景提供100%的支持。甚至大多数都无法通过合规这一环节。更不用说可能业务有自己的模型、数据库、基础能力等等。我们需要在业务下实现一套定制的流程引擎

流程引擎本质上可以被拆解为一下两种图的类型:

  • DAG 有向无环图。一般用作承载经典 Workflow 场景。需要注意的是这种图不能包含循环所以一般被用作实现单一 Agent 能力。
  • FSM 有限状态机。用作实现如 Multi-Agent 场景或有环的场景。是 DAG 的一个补充。但是需要注意的是因为状态机同时之后有一个激活状态所以并发分支等能力无法通过 FSM 实现。

当我们实现了上述两种流程引擎后可以进行组合实现更复杂的能力如:FSM 中嵌套 DAG,DAG 中嵌套 FSM 等等。

当我们有了上述流程引擎和对应的 DSL 之后。我们就可以在业务间快速复用能力(只要复制一下 DSL 或基于现有的 DSL 做二次开发就好了)。

可能你会问,为什么不根据需求直接把这些逻辑写在代码里呢?实际上在日常开发过程中我们发现大部分功能都是通过对有限能力的组合来实现的。如果不做流程引擎的建设会带来很大效率上的降低以及多余的开发量。

其他实践

Hornbill 是内部用于多个平台的 Oncall 工单管理工具,拥有三种升级策略。我们处理工单的团队包括用户运营团队和研发工程师,并在创建工单时自动拉入相关人员进行协作。由于审核员需要快速处理大量审核任务,Hornbill 通过 24 小时的用户运营团队快速解决问题,技术问题会被升级至研发解决。

Hornbill SDK 可整合到平台中,帮助用户在提交工单前通过 FAQ 寻找解决方案,从而减少不必要的工单。每周约有 几百个由用户运营团队处理的工单,因此减少工单数量可以让团队将时间用于更高价值的任务。

为了提高工单解决的效率并减少工单数量,我们引入了 AI 功能。

首先,相似工单检测能够有效识别并链接具有相同问题的工单,通过创建问题描述的嵌入向量,并使用向量相似性计算如余弦相似度,系统在用户提交新工单时可提示已有相似工单,推荐用户加入现有工单而非新建,或在工单提交后提醒用户操作团队进行链接,从而减少重复工单处理的工作量。

其次,AI 摘要生成功能则通过自动生成问题的总结,在群聊中梳理出问题描述、原因和结论,提供给不同班次的用户操作团队以保持问题处理的一致性。这一功能通过将所有的群聊内容传递给大型语言模型生成总结,从而避免因班次交接导致的上下文丢失,提高工单解决的连续性和效率。这些先进的 AI 功能减少了用户操作团队在票务处理上的重复性工作,让他们能够将更多时间投入到更具价值的任务中,不仅提升了团队的工作效率,也提高了用户问题解决的速度和质量,为用户提供了更优质的体验。

另外,Hornbill SDK FAQ 搜索功能旨在解决用户自行反馈且可通过非技术知识解决问题的工单。 通过将常见问题生成 FAQ,用户可以搜索相关知识库,并根据输入查询定制 FAQ 内容,使其更易理解。方法是为常见的可自解决问题创建 FAQ,并对 FAQ 问题进行向量嵌入,将其存储在向量数据库中。当用户输入问题时,我们利用余弦相似度比对向量嵌入,以找到匹配的 FAQ,并通过大型语言模型总结 FAQ 内容,展示给用户。这一功能减少了不必要的工单,提升了用户的自助解决效率。

总结

当我们完成了对流程引擎的落地,同时在流程中有技巧性的使用 LLM。基本上领域下的产品/业务诉求都可以基于这一套框架来实现。LLM 除了可以为我们做分类,识别等功能以外也可以帮助我们做离线验证/数据评估等工作。 与可视化编排界面配合可以大幅降低使用门槛。对于一个想要使用 LLM 能力来实现业务诉求的业务方,不论是 PM 同学还是运营同学都可以进行尝试可以大大解决研发的人力缺口。

提升开发体验

AI 编程在今年有了比较大的发展,因为出现了 Cursor、Windsurf、v0、bolt.new 这些,在不同场景下,成为了能指数级提升生产力的工具。这不仅仅得益于越来越强的模型能力,也得益于许多在应用/交互上的创新与探索。

工具形态

传统工具的替代品

搜索、文档工具、设计稿代码生成工具等

多智能体自然语言编程

代表工具:gpt-pilot、gpt-engineer、MetaGPT

这类工具直接通过自然语言与一个多智能体系统进行交互,多智能体系统会在内部划分多个角色/任务,如:程序员/开发/调试、架构师/设计、产品经理/拆解、项目经理/任务管理等。

但这类工具要将整个工程从新建到功能推进完全交给智能体维护,用户只能通过指令和自然语言对话对工程进行控制,并且受限于上下文窗口,也无法构建复杂的平台系统。使得这些工具都没有办法用于程序员日常的生产和实际的项目里。只能作为非专业人士的玩具,或者 AI 研究的尝试。

blog.pythagora.ai/2023/09/04/…

github.com/geekan/Meta…

代码辅助工具

代表工具:Github Copilot、Continue.dev、MarsCode

以 continue.dev 为例,这类工具的核心功能大概就是上述几类:以代码块/文件为上下文 Chat、Tab 自动补全、选中代码块进行自然语言编辑、在聊天框中对代码块进行应用。

这些功能已经在开发中被普遍使用了,开源的模型、插件对这些功能的支持也非常成熟了,不同公司内部也有类似的解决方案。

好用,但还不够强大,伴随着这些功能的组合和深度优化,期待更具生产力的工具逐步被开发出来并具备商业价值。

🆕 复合型编程IDE/插件

代表工具:Cursor、Windsurf、Cline

暂时无法在飞书文档外展示此内容

Windsurf 的 Cascade 工具,与 Cursor 的 Composer 类似

暂时无法在飞书文档外展示此内容

在代码辅助工具的基础上进一步增强代码整合能力,包括:

  • 能够有效汇总上下文
  • 能够将代码块转化为文件变更,以支持同时编辑多个文件
  • 能够管理AI批量编辑后文件的暂存状态,并灵活跳转到不同历史编辑版本

这些能力大大提高了使用体验和生产效率。

Cursor 在刚出来的时候也收获了大量非程序员以及博主们的力捧,但我们在日常使用中依然不多。主要还是因为无法直接用于我们日常工作的工程,但随着一些使用方法的探索,以及模型能力/上下文检索能力的进一步提升,越来越多的编程人员开始在日常工作中使用,以提升生产效率。

而下面的最佳实践也提供一种日常使用的工作流,该工作流基于文档,构建长期 AI 可维护的大型项目。

🆕 快速原型构建机

代表工具:v0、Bolt.New

其实和上面的 IDE 差不多,但更多的结合了 WebIDE 和 WebContainer 的优势。

常规的使用场景是:对于完全没有技术背景的角色来说,他们不知道如何为自己的需求创建一个工程并完成前期的原型验证工作,而这类 WebIDE 工具则提供了一个完美的平台让他们来得到一个可供开箱即用的工程原型。

在此之后,则可以将这个工程下载到本地,使用 Cursor 继续进行工程的后续维护和开发。

最佳实践

虽然新的工具将功能可用性提升了,但存在短板,需要结合一些使用的条件和方法:

  • 需要应用一整套闭环的工作流。其重点在各种文档的记录上(项目记忆力),让人和文档交互,完善设计,才能最终让 LLM 能更好的结合上下文生产出可靠的代码。
  • 不是所有场景都可以。逻辑性代码完全没问题,但无法还原视觉设计的细节。因此,在工作流中,通过 AI 完成逻辑代码的部分后,样式工作还得需要前端工程师来编写。
  • 技术栈和工程结构有限制。需要选择用比较老,且主流的技术栈和版本,社区训练物料比较多。
  • 合规和数据安全问题

工作流

暂时无法在飞书文档外展示此内容

这个工作流以文档为核心,适时的对项目和变更进行总结,以便在新增需求的时候,工具可以获得足够且精简的上下文,来精确生成新需求所需要的设计和代码。这些文档包括:

  1. 整个项目的描述,包括:项目简介、技术栈、文档结构、项目架构等全局信息
  2. Feature 文档,以及每个 feature 的技术实现细节
  3. 模块文档,在每个模块下,对于该模块代码的总结和索引

本质上讲,现阶段,如果我们将 AI Coding 工具拟人化,它有很多缺点:记忆力差,没有从代码仓库中持续积累特定于此仓库的经验,相当于每次找了一个新人来开发

所以,一个好的工具、开发者,应该是能够合理组织和提供一个指令足够的上下文的;一个完全适配的项目应该是,简单化模块化的原子能力 + 清晰的模块声明。类似于微前端、微服务这种组织形式可能更有利于文档的组织

未来展望

工作模式

这有一篇来自于红杉资本的文章中文版)描述了他们对生成式AI发展的展望:

虽然我对这个发展路径以及时间节点存疑,但也同样让我在想,结合AI,未来的编程是怎么样的呢?什么样的目的、什么样的实现方式、什么样的产品形态呢?往大了想,这些都太难回答了

但在一些小点上,站在程序员的角度,还是有一些想象的:

  • 近几年,就可以看见程序员的能力模型要求会有一些变化,更有 AI 辅助经验的,在生产力上会比纯手写更有优势

    • 就像在应用层,高级语言编程替代汇编语言(wiki: 编程语言世代
  • 语言本身的学习变得简单,当 JS 开发工程师想使用其他语言时,变得没什么门槛

  • 没有合规模型工具的公司,在生产力上会落后,人也一样

  • 会出现便宜的 AI 辅助编程解决方案,例如:

    • 针对特定技术栈的小参数 LLM ,本地 32G 内存的笔记本,就可以足够提升生产力了

更远的未来,如果产品形态和生产方式发生质的改变:

  • 那人可能可以专注于新的领域的扩展,而不用纠结于现有生产工具的熟练度
  • 模型越来越强大,实现这件事,真的可能会变成**「念咒语」**

工具链

现在的技术栈、工程化等方式还是基于人来建设的,但当 AI 编程占领主导之后,会有什么样的工具链来驱动呢?什么样的工具链对于 AI 编程更友好。

例如上面提到的最佳实践的工作流来说,整体围绕文档驱动,在生成文档的时候,我们也需要将项目代码转换成上下文提供给大语言模型,而 github.com/yamadashy/r… 就是一种可以将仓库代码打包成 LLM 上下文的工具。

这块还处于特别早期,因为具体的工作流还没有固化下来,但可以预见,会有越来越多的工具链产生。

推动测试进程

为什么做单元测试

LLM 生成单元测试代码,在 22 年底就已经取得了非常惊艳的效果:用例工整,分支覆盖详尽,mock 数据齐全。生成的用例如果可用,提交到仓库后会成为代码资产的一部分。如果用例有问题,这部分代码也不会直接影响到生产环境。所以,AI 单元测试是大语言模型第一次尝试落地工业生产环境的完美试验场景。

另外,单元测试本身就是研发环节中非常重要的一部分。全面的单元测试可以辅助发现很多变更引起的风险和问题。业界知名的开源软件必定包含大量自动化运行的单元测试,这在多人协作开发过程中至关重要。

我们在 23 年也尝试过使用 AI 生成单测。但是当时代码报错多、人工修复成本大,初步尝试的结果不尽如人意,于是暂且搁置了。24 年中,在业务痛点的驱使下,我们重启了 AI 单测的调研。这一次我们找到了新的角度,解决了报错多和人工修复成本大的问题,让大家看到了 AI 单测落地的可能性。

AI 单元测试效果如何

衡量标准

在讨论效果如何之前,首先要讨论如何衡量效果。怎样算效果好,怎样算不好?

在 23 年的调研中,我们没有具体的评判标准,只通过看到的结果得出一些主观判断。在 24 年的实践中,我们尝试以客观的评判标准为主,主观的感受为辅。

由于评判标准是为了服务于我们的目标,所以我们先花了点时间思考我们到底希望 AI 在单测这件事上做什么?我们希望通过引入 LLM,对编写单测提效,同时通过大量单测代码的引入,提高 bug 召回率,提升代码质量,减少线上bug。

基于这个目标,最终我们梳理出这样的指标体系:

  • 核心指标:bug 召回率

  • 准入指标:单测可执行率,单测覆盖率

  • 过程指标

    • 千行代码用例数、测试用例独立性
    • 单测执行时长
    • 项目渗透率、单测可维护性、研发接受度
    • 单测生成速度

实际效果

所以实际效果如何呢?

横向观测下,我们对比生成 case 数、case 可执行率以及单测覆盖率。大部分模型都基本满足了准入要求,小部分模型可能由于发布时间较早或提示词适配度低等工程原因,没有取得可用的表现。

在解决了一些难点问题后,我们在试点仓库做了推进。对于千行以下源码的小批量生成,单测可用率保持在 100%,覆盖率保持在 80% 左右。随着源码复杂度增加,可用率和覆盖率均略有下降,但整体表现已经进入了值得期待的状态。

除了客观数据,研发接受度(主观感受)也很重要。通过阅读我们看到,纯逻辑类函数 AI 可以做到考虑各种情形、针对特定情形 mock 数据并给出断言;React 组件 AI 可以做到考虑多种情况,并且试图在渲染的 dom 结构中寻找关键元素做断言,配合人工矫正生成部分快照可以低成本、高覆盖率完成单测编写;同时AI在边界条件测试上比人类更加严谨,也会通过函数名、注释等信息发现人类考虑不周之处。

从效率和 bug 召回的角度,我们都看到了希望,因此,我们开始在部门内推进 AI 单元测试。

为何会取得不错的效果

首先,我们标准化了基础设施,解决了一些基建问题。如果一类 case 有 3 种写法,那么我们选择其中一种我们最希望的写法让 AI 固定下来,同时帮 AI 打通任何调用 API 上的难题。

其次,人类程序员要懂单元测试。比如 Arrange-Act-Assert 单测组织方式,如何 mock 数据,如何模拟交互,如何合理断言等等。如果人本身不擅长做这件事,也就没办法更好地评判 AI 做的好还是不好。这和使用 LLM 学习其他领域、进行创意启发等场景是不同的,人类必须是相比 AI 更专业的角色。

最后,靠业界优秀论文和公司内团队支撑。从 23 年到 24 年,在 AI 单元测试领域出现了不少靠谱的论文,比如 Meta 的 arxiv.org/pdf/2402.09… 团队也是在调研之后,和公司内 Codeverse 团队一起,在我们的业务上落地了 AI 单元测试能力。

AIGC 落地的最后一公里

AI 单测真正的落地,还需要业务仓库研发同学的协助,不只是基础配置和存量单测代码的合入,还有对后续日常研发流程的改变。

接入AI单测能力,我们提供了3个模块:基础依赖包、流水线配置、ut-helper 本地工具。由于模型在纯函数上表现更好,在组件上略有欠缺,所以我们的推进策略是优先覆盖 P0 仓库的所有工具函数和通用组件,对于业务属性较强的代码暂不推进。 通过收集流水线上报的单测执行数据,我们建立了数据看板,展示仓库接入率、全量覆盖率、增量覆盖率和单测执行失败明细。

对于研发日常的影响,大家问的最多的问题是:生成了 case 之后还需要人类 review 吗?AI 有帮助人类纠错的能力吗?我们希望是不需要人工介入且能帮助发现潜在风险,而且也看到了一些希望。随着存量代码的覆盖完成,增量代码的覆盖更是对人类和 AI 的协作的考验。 AI 是否真的能在研发需求迭代过程中帮助研发规避潜在风险,部门的 bug 估分比是否能切实下降,这都是我们拭目以待的事情。

总结

在今年的实践中,我们对于 AI 落地这件事又有了更多的认识。它不是人类驱使一个远远强大于自己的怪物,落地工业生产场景仍然是靠人类往前迈一步,指挥 AI 在指定范围内做事。另外,AI 在真实落地中的挑战远不止模型训练,AI 和 AI、AI 和基建之间,有很多工程化的工作,它们可以在很大程度上改变最终的效果,有时会比实验室的大模型 pk 榜有趣得多。

保障交付质量

交付质量(Delivery Quality)是指在软件开发过程中,最终交付给客户或用户的软件产品所具备的质量水平。它涵盖了多个方面,包括功能性、可靠性、性能、安全性、易用性、可维护性等。交付质量的高低直接影响到用户满意度、产品市场竞争力以及企业的声誉。

交付质量对于软件开发非常关键,交付质量的劣化会带来用户满意度下降、维护成本增加、品牌声誉下降等问题,甚至会缩短产品的生存周期。交付质量的保障覆盖软件开发过程的所有环节,包括需求设计、开发、测试、发布上线、运维阶段,本文将从各个软件开发环节展开聊一下质量保障的手段和能力。

质量保障传统手段

完整的质量保障策略,需要各个阶段的努力,常见的质量保障框架包括基础的规范、工具、质量防控手段和质量度量:

暂时无法在飞书文档外展示此内容

保障交付质量是一个比较大的话题,交付质量问题可能出现在软件的开发生命周期任意一个环节,需求设计环节中的需求逻辑问题、开发阶段的代码质量问题和测试阶段的测试漏放问题最终都会导致交付质量的下降,常见的质量保障手段和理论基础:

  • 持续集成/持续交付(CI/CD) :持续集成(Continuous Integration, CI)和持续交付(Continuous Delivery, CD)是一种自动化软件交付流程的方法,通过频繁地集成代码、自动化测试和部署,确保软件始终处于可发布状态。

  • 测试驱动开发(TDD) :测试驱动开发(Test-Driven Development, TDD)是一种开发方法,要求开发者在编写功能代码之前先编写测试用例。通过这种方式,确保代码在开发过程中始终符合预期,提高代码质量和可维护性。

  • 行为驱动开发(BDD) :行为驱动开发(Behavior-Driven Development, BDD)是一种协作开发方法,通过自然语言描述系统行为,确保开发团队、测试团队和业务团队对需求有共同的理解。BDD 强调从用户的角度出发,编写可执行的测试用例。

  • DevOps:DevOps 是一种文化和实践,旨在通过开发(Development)和运维(Operations)团队之间的紧密协作,实现快速、可靠的软件交付。DevOps 强调自动化、监控和反馈,确保软件在整个生命周期中保持高质量。

  • 质量保证(QA)自动化:质量保证自动化工具和框架可以帮助开发团队自动化测试流程,确保软件在不同环境和配置下的稳定性和可靠性。

大模型如何赋能

LLM 的越加成熟为交付质量的提升带来了更多的可能,在整个软件开发生命周期过程中,当前阶段下 LLM 想要替代某一个角色的所有工作还不太可能,但是 LLM 已经可以在各个阶段为各个不同角色带来正向的作用,以下是一些相关的实践参考:

代码生成与重构

LLM 可以根据上下文生成高质量的代码片段,或者重构现有代码以提高其可读性和性能。代码生成能力在 LLM 上的应用和探索已经有较久的时间,随着模型能力的增强和各种工具的诞生和强化,代码生成和重构能力已经达到一个基本可用的状态,同时也有更多的专门为代码而生的代码模型逐步问世,例如 Claude 3.5 Sonnet、CodeGemma、Code Llama、Codex。

相关实践:

  1. MarsCode:智能编程助手,提供以智能代码补全为代表的核心能力,支持主流编程语言及 IDE,能在编码过程中提供单行或整个函数的建议;同时提供代码解释、单测生成、问题修复、AI Chat等辅助功能,提升编码效率与质量;

代码审查自动化

LLM 可以用于自动化代码审查,帮助开发者在代码提交前发现潜在的问题。例如,LLM 可以检测代码中的潜在错误、不规范的编码风格、安全漏洞等。

测试用例生成

LLM 可以根据代码逻辑生成测试用例,帮助开发者覆盖更多的代码路径,提高测试的全面性和有效性。例如,LLM 可以生成边界条件测试、异常处理测试等。

自动化部署与监控

LLM 可以辅助自动化部署流程,确保代码在不同环境中的正确性和一致性。此外,LLM 还可以帮助监控系统状态,及时发现和处理潜在问题。

相关实践:

  • 基于日志系统的自动化归因和排障能力,能实现在大范围故障中实现智能归因,找到根因,在实践中已经取得较好的效果;

团队协作与沟通

LLM 可以促进团队成员之间的协作与沟通,例如通过自动生成会议纪要、任务分配建议等,帮助团队更高效地协同工作。

相关实践:

  1. 飞书智能伙伴:飞书智能伙伴在群聊、会议、邮件等多个办公场景提供智能化能力,极大的提高了工作效率和沟通协作的质量;

展望大模型可探索方向

LLM 能力在交付质量保障中展现了较为强大的能力,在代码生成、代码审查、测试用例等方向已经存在一定的成熟度,但仍存在较大的潜力:

  1. 准确性提升:通过不断优化模型、训练策略、评估指标完善等策略,不断提升模型能力的准确性,降低心智负担
  2. 业务定制化:与现有的完善保障体系进行工具和流程的集合,融入业务特性,建设个性化 Agent,降低 LLM 使用成本,提升 LLM 能力覆盖度
  3. 全流程自动化:探索 LLM 自动化保障体系,管控整个需求开发周期,能够在各环节产物进行自动化质量保障,例如需求文档质量保障、代码质量问题回捞、用例质量保障、发布过程保障、线上运维监控等功能串联,形成全套的自动化方案

当前的发展现状来看,LLM 的能力还是在融入当前成熟的质量保障框架能力中,提升软件研发生命周期的的效率和质量,长期展望来看,LLM 会逐步接管质量保障的各个环节,实现高度自动化

运维管理优化

为什么需要更智能的运维

研发同学的一天,可能大部分时间不是用于开发,而是在处理各种各样的信息,比如告警、oncall、指标异常等等。

从这个角度讲,日常的运维管理比单纯的开发占据了研发的更多时间。从时间分配的角度上来说,对运维提效,可能比对开发提效相比带来的体感提升更大。

传统运维从触达渠道上可以分为两种方式:

  1. 系统日志上报,总结出各种各样的指标,当指标出现异常时,发送告警给研发
  2. 某系统 fatal 故障,发送卡片让研发检查自己负责的服务是否有问题

在这种模式下,主要的痛点在于:

  1. 收到这些运维管理的信息后,研发需要查询大量的上下文,然后定位问题
  2. 对于没有达到告警阈值的一些 case,缺乏触达能力

在这个基础上,我们需要更智能的定位分析能力和数据处理总结能力

AI 落地的思路

  • 如何处理海量的数据

用 AI 来分析海量数据,一个痛点就是受限于现在AI的语境,无法直接把所有数据都扔进去分析。

但是如果用知识/向量库的方法,用 RAG 的方法去分析,又无法让 AI 站在所有数据的角度去分析。

所以其中的一个思路是把分析的步骤拆解,一次给出局部的数据,然后通过对局部数据的分析,一步步缩小数据范围,在这个范围内给出更大体量的数据,然后让其做出更具体的分析。

  • 更多的上下文

在AI对于数据分析的基础上,查找出对应数据的上下文,然后让其总结分析。

比起单纯的数据类告警,这种分析帮助研发节省下在不同平台里查询上下文的时间。举个例子,当我们收到监控平台报警,可能会需要去埋点上报、流量监控等多个平台再去找一些次级数据,验证一些初步的判断。

在这一步,如果可以自动化的取到数据,然后给出初步的分析,就可以提高处理的效率。

  • 趋势性的数据分析

把尽可能多的数据提供给 AI,相当于 AI 帮我们观察了各种 dashboard,比如很多需要人去分析得出的尖刺,就可以让 AI 去查找。相比写死代码去分析,更加的智能弹性和节省开发人效。

数据分析的实践

目前 PAI 的数据分析,即遵循了这样的步骤:

  1. 观察整体PAI数据,分析出有尖刺的时间点
  2. 在有尖刺的时间点内,给出PAI的全部次级数据,深入分析
  3. 基于次级数据查找监控平台的对应错误,提供上下文以供分析
  4. 总结所有分析,并结构化数据发出推送卡片

暂时无法在飞书文档外展示此内容

出现演练事故,PAI的报警给出了准确的时间段,并在这个基础上给出了具体的usecase和场景,甚至具体的API。LLM能较大提升分析的效率,仅目前的分析看时间抓取的准确率达到100%,后续增加多数据源的输入(事故通报,上线记录,更多slardar错误抓取等)能增强分析的深度。

总结

一个很深入的感受是,随着 AI 能力的进化,对 AI 的使用反而应该更加的精细。

使用他要像对待一位新加入的同事,如果要让他负责你日常中的运维管理工作,你需要注意:

  1. 整理好并清晰的告诉他做这件事的步骤
  2. 准确的提供数据和保持语义化
  3. 尽量充分的上下文

结语

回顾过去的一年,AI 技术的飞速进步已深刻改变了团队的工作方式,也让我们逐渐认识到,AI 早已不再是遥不可及的梦想,而是我们日常工作中的得力助手。

团队在过去一年中对 AI 进行了大量的探索和研究。AI 单测的引入如及时雨,不仅提升了测试覆盖率和代码质量,还显著减少了人工修复的成本。AI 在内容审核领域极大地提升了审核员的工作效率和质量。在开发效率方面,AI 提供了全方位的智能代码补全、自动化代码审查、代码生成与重构、代码审查自动化等能力,涵盖了开发的方方面面,这极大的提升了我们开发效率。尽管目前 AI 相关工具还有很多不足之处,但 AI 发展的速度已让我们不得不正视起来。未来,当模型的准确性进一步提高,AI 有望在开发和生产的各个环节中提供更加全面和高效的解决方案。

技术的进步和应用场景的拓展,预示着 AI 将在我们日常开发中扮演愈发重要的角色。通过与 AI 的协作,我们相信技术生产力将达到新高度,为用户带来更好的体验。在这场技术革命中,我们迎风破浪,勇敢前行。以更开放的心态拥抱变革,用创新推动技术进步。每次新场景的落地和应用,都是团队智慧与汗水的结晶。

未来已来,我们早已做好准备,你呢?

仅需3步,稳定快速!火山引擎边缘大模型网关全面支持DeepSeek系列模型

DeepSeek 作为大模型新锐,凭借其在算法、架构及系统等核心领域的创新突破,迅速获得业界瞩目。在巨大的热度下,面对海量请求,越来越多用户遇到了请求失败、调用超时、结果无法返回等稳定性问题。

火山引擎边缘大模型网关通过一个 API 接入多家模型服务,利用全球边缘节点就近调用,提升响应速度;支持故障自动切换、重试和超时控制,确保服务可靠性;兼容 OpenAI 接口标准,可快速集成 DeepSeek 等模型,降低接入成本。

目前,火山引擎边缘大模型网关已全面支持 DeepSeek 系列模型,可通过两种方式进行模型使用:

  • 一是通过平台预置模型, 边缘大模型网关新增由火山方舟提供的 DeepSeek R1、DeepSeek V3、DeepSeek-R1-Distill-Qwen-7B/32B,您可直接使用并对其创建网关访问密钥,无需与三方模型提供商交互;

  • 二是通过自有三方模型, 边缘大模型网关新增由 DeepSeek 开放平台提供的 DeepSeek R1、DeepSeek V3 以及火山方舟提供的 DeepSeek R1、DeepSeek V3、DeepSeek-R1-Distill-Qwen-7B/32B,您可以将您在第三方模型平台的密钥纳管至边缘大模型网关,以实现通过边缘大模型网关签发的网关访问密钥进行对应模型的访问与调用。

01 3 步快速调用 DeepSeek

火山引擎边缘大模型网关支持通过一个 API 接口访问多家大模型提供商的模型与智能体,在端侧基于遍布全球的边缘计算节点就近调用。利用边缘云基础架构优势,提高模型访问速度,为终端用户提供更快速、可靠的 AI 服务体验。

在接入大模型的同时,通过配置调用顺序、自动重试、请求超时等能力,能够实现模型调用失败自动请求备用模型、单次请求失败自动重试、单次调用响应时间配置。通过产品化的配置,您可以迅速创建出与 OpenAI 的 API 和 SDK 完全兼容的网关访问密钥(API),并通过选配 DeepSeek 模型进行调用,节省大量适配成本,快速完成业务接入。

Step1 选择 DeepSeek 调用方式

调用平台预置 DeepSeek

边缘大模型网关的平台预置模型中上新了由火山方舟提供的 DeepSeek 模型,您可通过登录产品控制台查看支持模型,并通过点击创建网关访问密钥进行勾选。使用平台预置的模型 DeepSeek,您无需与模型提供商进行交互,可以直接通过边缘大模型网关进行模型配置与调用。

调用自有三方 DeepSeek

如果希望使用在火山方舟平台或 DeepSeek 开放平台购买的 DeepSeek 模型调用额度,您需要通过在边缘大模型网关平台创建对应模型提供商的调用渠道,在创建调用渠道时,需要提供您在第三方模型平台的密钥,同时勾选大模型以明确当前调用渠道可进行调用的模型配置。

完成调用渠道配置后,您可通过创建网关访问密钥勾选对应的 DeepSeek 模型,实现大模型的快速调用。

Step2 创建网关访问密钥

完成前序的 DeepSeek 模型选择后,您可在网关访问密钥创建的第二步进行模型调用配置,以更好地保障在终端业务调用时的稳定性。

  • 通过设置调用顺序,您可以手动调整上一步选择的模型调用顺序,可以根据不同厂商的容灾策略以及不同尺寸模型的降级进行设置,在前一个模型调用失败后,大模型网关将依次调用后续模型,直到成功调用一个模型。如果所有模型都调用失败,则返回错误响应。

  • 通过重试次数,您可以设置对一个模型进行调用的最大重试次数。当一个模型调用失败后,大模型网关将重新尝试调用此模型,直到重试次数耗尽。

  • 通过启用缓存,大模型网关会就近调用结果返回在边缘节点,从而加快重复查询、缩短响应时间并降低成本。

  • 通过设置**缓存的保留时长,**一旦超过指定时长,缓存将被清除。

  • 通过请求超时定义,您可以设置单次模型调用的超时时长,模型请求发出后,若在超时时长内未收到响应,则判定该请求失败。

Step3 服务调用与观测

当您根据上述流程完成网关访问密钥创建,您可以在网关访问密钥列表中查看已完成创建的信息。在详情页面,可以看到基本信息、用量统计、请求方式等详细信息。

通过详情页调用示例,您可以获得由边缘大模型网关提供的请求示例代码,包含 Curl 和 Python。当您从网关访问密钥绑定的模型中选择一个模型后,代码中的model参数值将自动替换成模型对应的值。如果网关访问密钥绑定了多个同一类型的模型,那么当选择一个模型后,可以通过单击右侧的图标查看模型故障转移的预览效果。当前模型调用失败时,大模型网关将依次调用后续的模型。在调用时,您需要将详情页 - 请求方式中的密钥替换示例代码中的$VEI_API_KEY

边缘大模型网关可根据您通过网关向模型发出的请求以及模型的响应来统计您的用量。不同模型提供商对模型用量的计量方式有所不同,根据模型调用计量方式,您的调用详情可以在用量统计中进行查看。

同时,通过云监控 - 大模型网关模块,您可以查询以网关访问密钥为维度的总用量(已消耗的 tokens 总量)与用量速率(每秒消耗的 tokens 额度)。

02 200 万 tokens 免费额度,体验边缘大模型网关

当前,火山引擎边缘大模型网关已适配 15+ 种主流大模型厂商及多个智能体提供商,点击www.volcengine.com/docs/6893/1… 了解并咨询 DeepSeek 模型~ 了解更多边缘大模型网关产品详情。

文档详情:

www.volcengine.com/docs/6893/1…

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

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

背景

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

稳定性建设所涉及的话题十分广泛,涵盖流程梳理与标准化、数据标准化、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_…
❌