标签 性能优化 下的文章

Vercel 最近发布了 React 最佳实践库,将十余年来积累的 React 和 Next.js 优化经验整合到了一个指南中。

其中一共包含8 个类别、40 多条规则

这些原则并不是纸上谈兵,而是 Vercel 团队在 10 余年从无数生产代码库中总结出的经验之谈。它们已经被无数成功案例验证,能切实改善用户体验和业务指标。

以下将是对你的 React 和 Next.js 项目影响最大的 10 大实践。

1. 将独立的异步操作并行

请求瀑布流是 React 应用性能的头号杀手。

每次顺序执行 await 都会增加网络延迟,消除它们可以带来最大的性能提升。

❌ 错误:

async function Page() {
  const user = await fetchUser();
  const posts = await fetchPosts();
  return <Dashboard user={user} posts={posts} />;
}

✅ 正确:

async function Page() {
  const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]);
  return <Dashboard user={user} posts={posts} />;
}

当处理多个数据源时,这个简单的改变可以将页面加载时间减少数百毫秒。

策略1:并行异步操作

2. 避免桶文件导入

从桶文件导入会强制打包程序解析整个库,即使你只需要其中一个组件。

这就像把整个衣柜都搬走,只为了穿一件衣服。

❌ 错误:

import { Check, X, Menu } from "lucide-react";

✅ 正确:

import Check from "lucide-react/dist/esm/icons/check";
import X from "lucide-react/dist/esm/icons/x";
import Menu from "lucide-react/dist/esm/icons/menu";

更好的方式(使用 Next.js 配置):

// next.config.js
module.exports = {
  experimental: {
    optimizePackageImports: ["lucide-react", "@mui/material"],
  },
};

// 然后保持简洁的导入方式
import { Check, X, Menu } from "lucide-react";

直接导入可将启动速度提高 15-70%,构建难度降低 28%,冷启动速度提高 40%,HMR 速度显著提高。

策略2:避免桶文件导入

3. 使用延迟状态初始化

当初始化状态需要进行耗时的计算时,将初始化程序包装在一个函数中,确保它只运行一次。

❌ 错误:

function Component() {
  const [config, setConfig] = useState(JSON.parse(localStorage.getItem("config")));
  return <div>{config.theme}</div>;
}

✅ 正确:

function Component() {
  const [config, setConfig] = useState(() => JSON.parse(localStorage.getItem("config")));
  return <div>{config.theme}</div>;
}

组件每次渲染都会从 localStorage 解析 JSON 配置,但其实它只需要在初始化的时候读取一次,将其封装在回调函数中可以消除这种浪费。

策略3:延迟状态初始化

4. 最小化 RSC 边界的数据传递

React 服务端/客户端边界会将所有对象属性序列化为字符串并嵌入到 HTML 响应中,这会直接影响页面大小和加载时间。

❌ 错误:

async function Page() {
  const user = await fetchUser(); // 50 fields
  return <Profile user={user} />;
}

("use client");
function Profile({ user }) {
  return <div>{user.name}</div>; // uses 1 field
}

✅ 正确:

async function Page() {
  const user = await fetchUser();
  return <Profile name={user.name} />;
}

("use client");
function Profile({ name }) {
  return <div>{name}</div>;
}

只传递客户端组件实际需要的数据。

策略4:最小化RSC边界

5. 动态导入大型组件

仅在功能激活时加载大型库,减少初始包体积。

❌ 错误:

import { AnimationPlayer } from "./heavy-animation-lib";

function Component() {
  const [enabled, setEnabled] = useState(false);
  return enabled ? <AnimationPlayer /> : null;
}

✅ 正确:

function AnimationPlayer({ enabled, setEnabled }) {
  const [frames, setFrames] = useState(null);

  useEffect(() => {
    if (enabled && !frames && typeof window !== "undefined") {
      import("./animation-frames.js").then((mod) => setFrames(mod.frames)).catch(() => setEnabled(false));
    }
  }, [enabled, frames, setEnabled]);

  if (!frames) return <Skeleton />;
  return <Canvas frames={frames} />;
}

typeof window 可以防止将此模块打包用于 SSR,优化服务端包体积和构建速度。

策略5:动态导入组件

6. 延迟加载第三方脚本

分析和跟踪脚本不要阻塞用户交互。

❌ 错误:

export default function RootLayout({ children }) {
  useEffect(() => {
    initAnalytics();
  }, []);

  return (
    <html>
      <body>{children}</body>
    </html>
  );
}

✅ 正确:

import { Analytics } from "@vercel/analytics/react";

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <Analytics />
      </body>
    </html>
  );
}

在水合后加载分析脚本,优先处理交互内容。

策略6:延迟加载脚本

7. 使用 React.cache() 进行请求去重

防止服务端在同一渲染周期内重复请求。

❌ 错误:

async function Sidebar() {
  const user = await fetchUser();
  return <div>{user.name}</div>;
}

async function Header() {
  const user = await fetchUser(); // 重复请求
  return <nav>{user.email}</nav>;
}

✅ 正确:

import { cache } from "react";

const getUser = cache(async () => {
  return await fetchUser();
});

async function Sidebar() {
  const user = await getUser();
  return <div>{user.name}</div>;
}

async function Header() {
  const user = await getUser(); // 已缓存,无重复请求
  return <nav>{user.email}</nav>;
}

策略7-8:缓存去重

8. 实现跨请求数据的 LRU 缓存

React.cache() 仅在单个请求内有效,因此对于跨连续请求共享的数据,使用 LRU 缓存。

❌ 错误:

import { LRUCache } from "lru-cache";

const cache = new LRUCache({
  max: 1000,
  ttl: 5 * 60 * 1000, // 5 分钟
});

export async function getUser(id) {
  const cached = cache.get(id);
  if (cached) return cached;

  const user = await db.user.findUnique({ where: { id } });
  cache.set(id, user);
  return user;
}

这在 Vercel 的 Fluid Compute 中特别有效,多个并发请求共享同一个函数实例。

9. 通过组件组合实现并行化

React 服务端组件在树状结构中按顺序执行,因此需要使用组合对组件树进行重构以实现并行化数据获取:

❌ 错误:

async function Page() {
  const data = await fetchPageData();
  return (
    <>
      <Header />
      <Sidebar data={data} />
    </>
  );
}

✅ 正确:

async function Page() {
  return (
    <>
      <Header />
      <Sidebar />
    </>
  );
}

async function Sidebar() {
  const data = await fetchPageData();
  return <div>{data.content}</div>;
}

这样一来,页眉和侧边栏就可以并行获取数据了。

10. 使用 SWR 进行客户端请求去重

当客户端上的多个组件请求相同的数据时,SWR 会自动对请求进行去重。

❌ 错误:

function UserProfile() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch("/api/user")
      .then((r) => r.json())
      .then(setUser);
  }, []);

  return <div>{user?.name}</div>;
}

function UserAvatar() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch("/api/user")
      .then((r) => r.json())
      .then(setUser);
  }, []);

  return <img src={user?.avatar} />;
}

✅ 正确:

import useSWR from "swr";

const fetcher = (url) => fetch(url).then((r) => r.json());

function UserProfile() {
  const { data: user } = useSWR("/api/user", fetcher);
  return <div>{user?.name}</div>;
}

function UserAvatar() {
  const { data: user } = useSWR("/api/user", fetcher);
  return <img src={user?.avatar} />;
}

SWR 只发出一个请求,并将结果在两个组件之间共享。

11. 总结

这些最佳实践的美妙之处在于:它们不是复杂的架构变更。大多数都是简单的代码修改,却能产生显著的性能改进。

一个 600ms 的瀑布等待时间,会影响每一位用户,直到被修复。

一个桶文件导入造成的包膨胀,会减慢每一次构建和每一次页面加载

所以越早采用这些实践,就能避免积累越来越多的性能债务。

总结:立即行动

现在开始应用这些技巧,让你的 React 应用快如闪电吧!

我是冴羽,10 年笔耕不辍,专注前端领域,更新了 10+ 系列、300+ 篇原创技术文章,翻译过 Svelte、Solid.js、TypeScript 文档,著有小册《Next.js 开发指南》、《Svelte 开发指南》、《Astro 实战指南》。

欢迎围观我的“网页版朋友圈”,关注我的公众号:冴羽(或搜索 yayujs),每天分享前端知识、AI 干货。

部署到 Cloudflare Workers

  1. fork 本存储库Fork xixu-me/Xget

  2. 获取 Cloudflare 凭证

  3. 配置 GitHub Secrets

    • 进入你的 GitHub 存储库 → Settings → Secrets and variables → Actions
    • 添加以下 secrets:
      • CLOUDFLARE_API_TOKEN:你的 API 令牌
      • CLOUDFLARE_ACCOUNT_ID:你的 Account ID
  4. 触发部署

    • 推送代码到 main 分支会自动触发部署
    • 仅修改文档文件(.md)、LICENSE.gitignore 等不会触发部署
    • 也可以在 GitHub Actions 页面手动触发部署
  5. 绑定自定义域名(可选):在 Cloudflare Workers 控制台中绑定你的自定义域名

部署到 Cloudflare Pages

  1. fork 本存储库Fork xixu-me/Xget

  2. 获取 Cloudflare 凭证

  3. 配置 GitHub Secrets

    • 进入你的 GitHub 存储库 → Settings → Secrets and variables → Actions
    • 添加以下 secrets:
      • CLOUDFLARE_API_TOKEN:你的 API 令牌
      • CLOUDFLARE_ACCOUNT_ID:你的 Account ID
  4. 触发部署

    • 存储库会自动将 Workers 代码转换为 Pages 兼容格式并同步到 pages 分支
    • 推送代码到 main 分支会自动触发同步和部署工作流
    • 仅修改文档文件(.md)、LICENSE.gitignore 等不会触发部署
    • 也可以在 GitHub Actions 页面手动触发部署
  5. 绑定自定义域名(可选):在 Cloudflare Pages 控制台中绑定你的自定义域名

注意pages 分支是从 main 分支自动生成的。请勿手动编辑 pages 分支,因为它会被同步工作流覆盖。

部署到 EdgeOne Pages

  1. fork 本存储库Fork xixu-me/Xget

  2. 获取 EdgeOne Pages API Token

  3. 配置 GitHub Secrets

    • 进入你的 GitHub 存储库 → Settings → Secrets and variables → Actions
    • 添加以下 secret:
      • EDGEONE_API_TOKEN:你的 API Token
  4. 触发部署

    • 存储库会自动将 Workers 代码转换为 Pages 兼容格式并同步到 pages 分支
    • 推送代码到 main 分支会自动触发同步和部署工作流
    • 仅修改文档文件(.md)、LICENSE.gitignore 等不会触发部署
    • 也可以在 GitHub Actions 页面手动触发部署
  5. 绑定自定义域名(可选):在 EdgeOne Pages 控制台中绑定你的自定义域名

注意pages 分支是从 main 分支自动生成的。请勿手动编辑 pages 分支,因为它会被同步工作流覆盖。

部署到 Vercel

  1. fork 本存储库Fork xixu-me/Xget

  2. 获取 Vercel 凭证

    • 访问 Vercel Account Settings 创建并记录 Access Token
    • 访问 Team Settings 记录 Team ID
    • 新建项目后访问项目的 Settings 记录 Project ID
  3. 配置 GitHub Secrets

    • 进入你的 GitHub 存储库 → Settings → Secrets and variables → Actions
    • 添加以下 secrets:
      • VERCEL_TOKEN:你的 Access Token
      • VERCEL_ORG_ID:你的 Team ID
      • VERCEL_PROJECT_ID:你的 Project ID
  4. 触发部署

    • 存储库会自动将 Workers 代码转换为 Functions 兼容格式并同步到 functions 分支
    • 推送代码到 main 分支会自动触发同步和部署工作流
    • 仅修改文档文件(.md)、LICENSE.gitignore 等不会触发部署
    • 也可以在 GitHub Actions 页面手动触发部署
  5. 绑定自定义域名(可选):在 Vercel 控制台中绑定你的自定义域名

注意functions 分支是从 main 分支自动生成的。请勿手动编辑 functions 分支,因为它会被同步工作流覆盖。

部署到 Netlify

  1. fork 本存储库Fork xixu-me/Xget

  2. 获取 Netlify 凭证

    • 访问 Netlify User Settings 创建并记录 personal access token
    • 新建项目后访问 Project configuration 记录 Project ID
  3. 配置 GitHub Secrets

    • 进入你的 GitHub 存储库 → Settings → Secrets and variables → Actions
    • 添加以下 secrets:
      • NETLIFY_AUTH_TOKEN:你的 personal access token
      • NETLIFY_SITE_ID:你的 Project ID
  4. 触发部署

    • 存储库会自动将 Workers 代码转换为 Functions 兼容格式并同步到 functions 分支
    • 推送代码到 main 分支会自动触发同步和部署工作流
    • 仅修改文档文件(.md)、LICENSE.gitignore 等不会触发部署
    • 也可以在 GitHub Actions 页面手动触发部署
  5. 绑定自定义域名(可选):在 Netlify 控制台中绑定你的自定义域名

注意functions 分支是从 main 分支自动生成的。请勿手动编辑 functions 分支,因为它会被同步工作流覆盖。

部署到 Deno Deploy

  1. fork 本存储库Fork xixu-me/Xget

  2. 切换默认分支

    • 进入你的 GitHub 存储库 → Settings → General → Default branch
    • 将默认分支从 main 切换到 functions
  3. 部署到 Deno Deploy

  4. 绑定自定义域名(可选):在 Deno Deploy 控制台中绑定你的自定义域名

注意functions 分支是从 main 分支自动生成的。请勿手动编辑 functions 分支,因为它会被同步工作流覆盖。

自托管部署

如果你希望在自己的服务器上运行 Xget,可以使用 Docker 或 Podman 部署:

使用预构建镜像

从 GitHub Container Registry 拉取并运行预构建的镜像:

使用 Docker:

# 拉取最新镜像
docker pull ghcr.io/xixu-me/xget:latest

# 运行容器
docker run -d \
  --name xget \
  -p 8080:8080 \
  ghcr.io/xixu-me/xget:latest

使用 Podman:

# 拉取最新镜像
podman pull ghcr.io/xixu-me/xget:latest

# 运行容器
podman run -d \
  --name xget \
  -p 8080:8080 \
  ghcr.io/xixu-me/xget:latest

本地构建

从源码构建容器镜像:

使用 Docker:

# 克隆存储库
git clone https://github.com/xixu-me/Xget.git
cd Xget

# 构建镜像
docker build -t xget:local .

# 运行容器
docker run -d \
  --name xget \
  -p 8080:8080 \
  xget:local 

使用 Podman:

# 克隆存储库
git clone https://github.com/xixu-me/Xget.git
cd Xget

# 构建镜像
podman build -t xget:local .

# 运行容器
podman run -d \
  --name xget \
  -p 8080:8080 \
  xget:local 

使用 Docker Compose / Podman Compose

创建 docker-compose.yml 文件:

version: '3.8' services: xget:  ghcr.io/xixu-me/xget:latest container_name: xget ports: - "8080:8080" restart: unless-stopped 

使用 Docker Compose:

docker compose up -d

使用 Podman Compose:

podman compose up -d

部署完成后,Xget 将在 8080 端口运行。

注意:自托管部署不包括全球边缘网络加速,性能取决于你的服务器配置和网络环境。


📌 转载信息
原作者: xixu-me
转载时间: 2026/1/25 23:15:14

作者:ba0tiao
编者按:
在AI浪潮席卷全球的今天,有人认为传统关系型数据库已走向黄昏,MySQL 的生命力正在被边缘化。但事实真的如此吗?AliSQL,作为 MySQL 的重要分支,自2010年诞生以来,始终默默支撑着阿里巴巴集团核心业务的高并发、高可用需求。它从未消失,只是沉寂太久。
2026年,AliSQL社区的一帮开发者们,开始为AliSQL注入创新的血液!这是他们的第一篇,系统阐述了MySQL深度融合DuckDB的重大技术实践。这不仅是对“MySQL 只擅长 TP”这一行业共识的突破性回应,更是一次兼具工程魄力与架构远见的创新——在保持 MySQL 协议、语法、运维体系完全兼容的前提下,以轻量、高效、零侵入的方式,为MySQL 注入了 OLAP 能力。
国内首场《2026 AliSQL Innovate 用户大会暨 AliSQL DuckDB 开源发布会》将于2月3日在杭州开启!
席位有限,快来报名吧https://page.aliyun.com/form/act1162737496/index.htm

MySQL的插件式存储引擎架构

MySQL的核心创新之一就是其插件式存储引擎架构(Pluggable Storage Engine Architecture),这种架构使得MySQL可以通过多种不同的存储引擎来扩展自己的能力,从而支持更多的业务场景。MySQL的插件式架构如下图所示:
图片
MySQL的插件式存储引擎架构可以划分为四个主要的部分:

  • 运行层(Runtime Layer):负责MySQL运行相关的任务,比如通讯、访问控制、系统配置、监控等信息。
  • Binlog层(Binlog Layer): 负责Binlog的生成、复制和应用。
  • SQL层(SQL Layer):复制SQL的解析、优化和SQL的执行。
  • 存储引擎层(Storage Engine Layer):负责数据的存储和访问。
    MySQL在SQL计算和数据存储之间设计了一套标准的数据访问控制接口(Plugable Engine Interface),SQL层通过这个标准的接口进行数据的更新、查询和管理,存储引擎得以作为独立组件实现“热插拔”式集成。
    目前MySQL中常用的存储引擎包括:
  • MyISAM:MySQL最早使用的引擎,因为不支持事务已经被InnoDB取代。但是一直到MySQL-5.7还是系统表的存储引擎。
  • InnoDB:MySQL的默认引擎。因期对事务的支持以及优秀的性能表现,逐步替代MyISAM成为MySQL最广泛使用的引擎。
  • CSV: CSV文件引擎,MySQL慢日志和General Log的存储引擎。
  • Memory:内存表存储引擎,也可作为SQL执行时内部临时表的存储引擎。
  • TempTable:MySQL-8.0引入的引擎,用于存储内部临时表。
    InnoDB作为引擎引入到MySQL,是MySQL插件式引擎架构的一个非常重要的里程碑。在互联网发展的初期,MyISAM因其简单高效的访问赢得了互联网业务的青睐,和Linux、Apach、PHP一起被称为LAMP架构。
    随着电商、社交互联网的兴起,MyIASAM的短板越来越明显。InnoDB因其对事务ACID的支持、在并发访问和性能上的优势,大大的拓展了MySQL的能力。在InnoDB的加持下,MySQL成为最流行的开源OLTP数据库。随着MySQL的广泛使用,我们看到有越来越多基于TP数据的分析型查询。InnoDB的架构是天然为OLTP设计,虽然在TP业务场景下能够有非常优秀的性能表现。但InnoDB在分析型业务场景下的查询效率非常的低。这大大的限制了MySQL的使用场景。时至今日,MySQL一直欠缺一个分析型查询引擎。DuckDB的出现让我们看到了一种可能性。

    DuckDB简介

    DuckDB 是一个开源的在线分析处理(OLAP)和数据分析工作负载而设计。因其轻量、高性能、零配置和易集成的特性,正在迅速成为数据科学、BI 工具和嵌入式分析场景中的热门选择。DuckDB主要有以下几个特点:

  • 卓越的查询性能:单机DuckDB的性能不但远高于InnoDB,甚至比ClickHouse和SelectDB的性能更好。
  • 优秀的压缩比:DuckDB采用列式存储,根据类型自动选择合适的压缩算法,具有非常高的压缩率。
  • 嵌入式设计:DuckDB是一个嵌入式的数据库系统,天然的适合被集成到MySQL中。
  • 插件化设计:DuckDB采用了插件式的设计,非常方便进行第三方的开发和功能扩展。
  • 友好的License:DuckDB的License允许任何形式的使用DuckDB的源代码,包括商业行为。
    基于以上的几个原因,我们认为DuckDB非常适合成为MySQL的AP存储引擎。因此我们将DuckDB集成到了AliSQL中。
    图片
    DuckDB引擎的定位是实现轻量级的单机分析能力,目前基于DuckDB引擎的RDS MySQL DuckDB只读实例已经上线,欢迎试用。未来我们还会上线主备高可用的RDS MySQL DuckDB主实例,用户可以通过DTS等工具将异构数据汇聚到RDS MySQL DuckDB实例,实现数据的分析查询。RDS MySQL DuckDB只读实例的架构
    图片
    DuckDB分析只读实例,采用读写分离的架构。分析型业务和主库业务分离,互不影响。和普通只读实例一样,通过Binlog复制机制从主库复制数据。DuckDB分析只读节点有以下优势:
  • 高性能分析查询:基于DuckDB的查询能力,分析型查询性能相比InnoDB提升高达200倍(详见性能部分)。
  • 存储成本低:基于DuckDB的高压缩率,DuckDB只读实例的存储空间通常只有主库存储空间的20%。
  • 100% 兼容MySQL语法,免去学习成本。DuckDB作为引擎集成到MySQL中,因此用户查询仍然使用MySQL语法,没有任何学习成本。
  • 无额外管理成本:DuckDB只读实例仍然是RDS MySQL实例,相比普通只读实例仅仅增加了一些MySQL参数。因此DuckDB和普通RDS MySQL实例一样管理、运维、监控。监控信息、慢日志、审计日志、RDS API等无任何差异。
  • 一键创建DuckDB只读实例,数据自动从InnoDB转成DuckDB,无额外操作。DuckDB 引擎的实现
    图片
    DuckDB只读实例使用上可以分为查询链路和Binlog复制链路。查询链路接受用户的查询请求,执行数据查询。Binlog复制链路连接到主实例进行Binlog复制。下面会分别从这两方面介绍其技术原理。

    查询链路

    图片
    查询执行流程如上图所示。InnoDB仅用来保存元数据和系统信息,如账号、配置等。所有的用户数据都存在DuckDB引擎中,InnoDB仅用来保存元数据和系统信息,如账号、配置等。
    用户通过MySQL客户端连接到实例。查询到达后,MySQL首先进行解析和必要的处理。然后将SQL发送到DuckDB引擎执行。DuckDB执行完成后,将结果返回到Server层,server层将结果集转换成MySQL的结果集返回给客户。
    查询链路最重要的工作就是兼容性的工作。DuckDB和MySQL的数据类型基本上是兼容的,但在语法和函数的支持上都和MySQL有比较大的差异,为此我们扩展了DuckDB的语法解析器,使其兼容MySQL特有的语法;重写了大量的DuckDB函数并新增了大量的MySQL函数,让常见的MySQL函数都可以准确运行。自动化兼容性测试平台大约17万SQL测试,显示兼容率达到99%。

    Binlog复制链路

    图片

    幂等回放

    由于DuckDB不支持两阶段提交,因此无法利用两阶段提交来保证Binlog GTID和数据之间的一致性,也无法保证DDL操作中InnoDB的元数据和DuckDB的一致性。因此我们对事务提交的过程和Binlog的回放过程进行了改造,从而保证实例异常宕机重启后的数据一致性。

    DML回放优化

    由于DuckDB本身的实现上,有利于大事务的执行。频繁小事务的执行效率非常低,会导致严重的复制延迟。因此我们对Binlog回放做了优化,采用攒批(Batch)的方式进行事务重放。优化后可以达到30万行/s的回放能力。在Sysbench压力测试中,能够做到没有复制延迟,比InnoDB的回放性能还高。
    图片

    并行Copy DDL

    MySQL中的一少部分DDL比如修改列顺序等,DuckDB不支持。为了保证复制的正常进行,我们实现了Copy DDL机制。DuckDB原生支持的DDL,采用Inplace/Instant的方式执行。当碰到DuckDB不支持的DDL时,会采用Copy DDL的方式创建一个新表替换原表。
    图片

Copy DDL采用多线程并行执行,执行时间缩短7倍。
图片

DuckDB只读实例的性能

测试环境ECS 实例 32Cpu、128G内存、ESSD PL1云盘 500GB
测试类型TPC-H SF100
图片

结语

通过将DuckDB深度集成到AliSQL中,我们成功打造了兼具高性能与高兼容性的MySQL分析型实例。这一创新不仅弥补了MySQL长期以来在OLAP场景下的能力短板,也开创了一种全新的“HTAP轻量化”实现路径——无需复杂的分布式架构,即可实现强大的实时分析能力。
DuckDB引擎的引入,使得用户可以在不改变现有应用架构的前提下,轻松获得高达200倍的分析查询性能提升。更重要的是,用户可以使用MySQL协议、沿用熟悉的SQL语法、无需学习新工具、无需改造应用程序。一键创建、自动同步、无缝切换,真正做到了“分析能力即服务”。

未来已来,创新不止。我们将持续拓展 AliSQL DuckDB 引擎的能力边界,赋能更高效、更智能的数据处理新体验。
2026年2月3日(星期二)13:30–16:30,2026 AliSQL Innovate 用户大会 暨 AliSQL DuckDB 开发者线下活动 将在杭州盛大启幕!
以“Innovate”之名,我们重启 MySQL 生态的无限可能——重启 · 再创 · 向新而生
这是一场属于开发者的技术盛宴,一次思想碰撞与技术共创的深度交流。诚邀广大开发者、技术爱好者与行业伙伴齐聚杭州,共同见证 AliSQL 的进化之路,携手探索数据库的未来方向。
席位有限,立即扫码报名,锁定你的专属席位!我们在杭州,等你共赴创新之约!
图片

GIL的移除对于Python而言,绝非单纯的性能解锁动作,而是从底层运行逻辑到上层实践体系的全方位重构,其核心挑战在于长期被全局锁掩盖的调度失衡、内存竞争与语义模糊问题被彻底暴露,原有并发体系的底层支撑逻辑随之失效,重构的核心起点便是打破全局锁带来的粗粒度管控惯性。在CPU密集型的大规模数据处理与计算场景中,此前依赖GIL实现的字节码串行化执行,虽以牺牲多核性能为代价规避了线程间的直接冲突,却也让Python在多核硬件环境中始终处于算力利用率不足的状态,而移除GIL后,若直接沿用旧有的线程调度逻辑,会引发线程间的无序资源抢占,带来频繁的上下文切换与缓存失效问题,反而造成性能的反向回落。真正的重构核心,在于建立调度颗粒度与硬件底层特性深度亲和的全新逻辑,通过对任务进行全维度的特性画像,精准感知计算强度、数据依赖关系与资源占用规律,进而动态调整线程与CPU核心的绑定策略,让高频数据交互的任务组共享核心缓存池,减少核间通信的额外开销,让完全独立的计算任务分散至不同NUMA节点的核心中,实现算力的最大化利用。这一过程中需要彻底摒弃“以锁控安全”的传统认知,转而探索基于任务生命周期与特性的调度协议,让并发执行从被动的锁限制走向主动的资源适配,让每一个线程的执行都能与硬件资源形成最优匹配,这也是无GIL时代Python并发模型重构的核心价值与底层逻辑。

内存管理机制的重构是GIL移除后Python并发体系落地的根本支撑,其核心在于彻底摆脱对全局锁的依赖,建立起与多线程并行执行相适配的、线程安全且高效的对象生命周期管理体系,让内存操作的效率与安全形成动态平衡。此前Python的核心引用计数机制,因GIL的存在实现了天然的线程安全,无需考虑跨线程的计数竞争问题,而在无GIL的多线程环境中,若直接为引用计数引入原子操作,会在高频对象访问场景中产生大量的总线争用,造成显著的性能损耗,这也是内存管理重构需要解决的核心矛盾。在实际的技术探索与实践中可以发现,Python在各类业务场景中的对象访问均呈现出明显的线程归属特性,即超过九成的局部变量、临时计算结果等对象,仅会在单个线程内完成创建、使用与销毁的全生命周期,仅有少量核心结果对象会发生跨线程的传递与共享。基于这一实际的访问规律,偏向引用计数的设计思路成为重构的核心方向,即为每个对象建立本地计数与共享计数的双维度统计体系,单线程内的访问仅操作无同步开销的本地计数,只有当对象发生跨线程传递时,才会启动原子操作更新共享计数,实现线程间的状态同步。在大规模数据预处理的实际场景中,通过为数据集打上轻量的访问属性标记,让单线程主导的分块数据处理任务沿用轻量的本地计数模式,保障执行效率,而跨线程汇总的结果集则自动切换至共享计数模式,确保线程安全,这种差异化的内存管理策略,让内存操作能够精准适配实际的访问规律,而非强行套用统一的同步机制,真正实现了效率与安全的双重保障。

并发语义的重新定义是衔接Python底层并发机制与上层开发实践的关键纽带,GIL的长期存在让Python处于“伪并发”的语义框架之下,开发者无需关注底层线程的真实执行状态与资源竞争问题,而移除GIL后,必须建立起与真并发相匹配的语义体系,让语义定义与硬件执行逻辑、内存管理机制形成闭环,同时降低开发者的并发编程心智负担。这种语义重构并非简单的API新增或调整,而是从底层逻辑出发,让并发语义成为硬件执行、内存管理的上层具象化表达,实现不同层级的语义一致性,让开发者能够基于明确的语义规则设计安全高效的并发代码。新的并发语义体系构建的核心,在于明确不同类型对象的安全边界,并设计基于对象类型的自动同步协议,通过为对象增加轻量的安全标识,划分出线程私有、跨线程共享、全局共享三个层级,底层运行时会根据对象的标识自动选择适配的同步策略,开发者无需手动添加显式锁,即可实现对象的安全访问。在多线程数据聚合的实际场景中,通过语义层面的“状态可见性声明”,让开发者能够根据业务需求,选择数据更新的“即时可见”或“最终一致”模式,底层则通过语义协议实现对应的同步逻辑,让线程间的数据传递无需依赖手动的锁操作,即可确保数据更新的即时性与完整性。例如在分布式日志聚合的场景中,每个线程的本地日志对象被标记为线程私有,无需同步开销,而全局的日志聚合对象被标记为跨线程共享,底层语义协议会自动为其添加轻量的同步机制,确保多线程写入时的状态一致。这种语义重构的核心价值,在于让并发语义成为底层机制的上层抽象,既保留了底层优化的灵活性,又让开发者能够摆脱繁琐的底层同步细节,聚焦于业务逻辑的实现,真正降低了并发编程的技术门槛。

生态工具链的适配重构是GIL移除后Python新并发模型落地普及的关键支撑,第三方库与运行时环境的协同优化程度,直接决定了新并发模型的实际实用性与生态兼容性,而重构的核心原则是分层适配,而非要求所有库进行全盘重写,最大限度保护现有生态的技术投资。此前绝大多数Python第三方库均基于GIL环境设计,内部未考虑线程安全问题,核心逻辑的实现未做任何同步处理,若直接迁移至无GIL的运行环境,会导致对象状态异常、数据访问错误等问题,但全盘重写所有第三方库显然不具备实际可行性,因此分层适配的策略成为工具链重构的核心方向。针对Python的底层基础库,如数据结构库、网络通信库、核心算法库,需要进行核心交互逻辑的重构,采用与新内存管理机制、并发语义体系兼容的接口设计,通过暴露对象的访问权限标识与状态元数据,让基础库能够感知当前的并发执行环境,实现与底层机制的深度协同。针对上层的应用库,如科学计算库、图像处理库,则通过构建轻量的适配层,封装底层的同步逻辑,提供与原有版本一致的调用接口,开发者无需修改业务逻辑,即可实现新旧并发模式的兼容运行。在科学计算的实际场景中,数值计算库通过重构数据传递接口,让数组对象的跨线程访问能够自动触发底层的同步机制,而开发者的计算代码无需任何修改;在图像处理场景中,图形处理库通过适配层拆分串行依赖步骤与并行可执行步骤,让耗时的像素运算能够利用多核并行执行,而流程控制部分保持单线程执行,这种分层适配策略,既让现有生态库能够快速适配无GIL环境,又能充分发挥新并发模型的多核性能优势,实现生态的平稳过渡。

开发范式的深度转变是Python并发模型重构的最终落脚点,GIL的移除让开发者必须从传统的“规避并发冲突”的防御性编程思维,转向“主动设计并发效率”的建设性思维,这种范式转变并非要求所有开发者成为底层并发机制专家,而是建立基于任务特性的并发设计直觉,让并发设计成为业务优化的自然延伸。传统的防御性思维下,开发者为了避免锁竞争与数据异常,往往会盲目选择多进程替代多线程,却忽略了进程间通信的高额开销,反而导致整体性能下降,而在无GIL的新环境中,建设性思维的核心是对任务进行全维度的特性分析,根据任务的无状态/有状态、CPU密集/I/O密集、数据耦合度高低,选择适配的并发策略,而非简单的线程或进程数量叠加。在大规模文本处理的实际场景中,将无状态的文本分词、关键词提取任务拆分为粒度适中的独立单元,通过任务队列分配至多个线程实现并行执行,而存在强状态依赖的结果整合、主题聚类任务则采用串行化处理,这种基于任务特性的拆分策略,比单纯增加线程数量更能提升整体执行效率。同时,开发者需要建立起全新的性能评估体系,摒弃以“是否避免锁竞争”为核心的评估标准,转而关注CPU核心利用率、缓存命中率、线程上下文切换次数等底层指标,通过观察运行时的调度日志与内存访问统计,持续优化任务拆分的粒度与调度策略。在实际开发中,通过对任务进行多次的粒度调整与性能测试可以发现,任务粒度过细会导致调度开销过高,粒度过粗则会导致并行度不足,只有根据硬件的核心数量、缓存大小调整至合适的粒度,才能实现资源利用率的最大化,这种基于实际硬件与任务特性的并发设计思路,正是建设性编程思维的核心体现。

GIL移除带来的Python并发模型重构,本质上是一次全层级的分层进化,从底层的调度机制与内存管理,到中层的并发语义与生态工具链,再到上层的开发范式,每个层级都在建立新的协同关系,而非简单的技术替代,这种重构并非一蹴而就的工程,而是一个基于社区实践持续迭代优化的过程。各层级的重构并非孤立进行,而是形成了相互支撑、相互适配的闭环,底层的偏向引用计数与细粒度调度机制,为中层的并发语义提供了底层支撑,而并发语义则成为上层开发范式的具象化规则,生态工具链的适配重构则让底层机制与上层语义能够落地到实际的业务场景中,各层级的协同进化,让新的并发体系形成了从底层到上层的完整支撑。

在HarmonyOS应用开发中,性能问题直接决定用户体验——滑动卡顿、启动缓慢、内存泄漏等问题,往往成为应用上线的“拦路虎”。DevEco Profiler作为官方性能分析利器,提供了实时监控、深度录制、多场景专项分析能力,能精准定位从底层资源到上层UI的各类性能瓶颈。

本文将以“理论+实操+专项”三维视角,拆解基于DevEco Profiler的性能优化闭环流程,重点覆盖Frame(卡顿丢帧)与ArkUI(组件/状态)两大高频场景,提供可直接落地的分析方法与避坑指南,助力开发者高效解决性能难题。

一、核心认知:性能优化的闭环逻辑与指标基准

性能优化并非“头痛医头”,而是一套“识别-定界-定位-优化-验证”的闭环流程。在动手分析前,需先明确性能指标基准与工具分工,避免无方向调优。

1.1 关键性能指标基准

以用户可感知体验为核心,结合HarmonyOS应用特性,核心指标参考如下(可根据业务场景微调):

  • 流畅度:页面滑动、动画播放帧率稳定在60fps以上,无掉帧、卡顿;60fps对应Vsync周期16.6ms,单帧耗时需控制在该阈值内。
  • 启动速度:冷启动耗时≤2秒,热启动耗时≤500ms;启动阶段需重点监控初始化链路耗时。
  • 资源占用:无高负载操作时,CPU占用率≤30%;内存无持续上涨(排除泄漏);GPU使用率适配场景,无无效渲染。
  • 稳定性:无因性能过载导致的崩溃、闪退,正常使用无异常发烫。

1.2 DevEco Profiler核心工具分工

工具能力与优化流程深度绑定,核心分工如下,避免重复操作或无效录制:

工具模块

核心作用

适用阶段

Realtime Monitor(实时监控)

快速识别资源异常,定界问题类型与场景

识别-定界、验证阶段

场景化模板(Frame/ArkUI/Launch等)

深度录制数据,精准定位问题根因(代码级)

定位阶段

离线符号解析、源码跳转

还原Native函数栈,定位具体代码行

定位阶段(底层问题)

二、性能优化全流程实操(闭环落地)

本流程适用于所有性能问题场景,核心是“先快速定界,再精准定位”,避免盲目深度录制浪费资源。

步骤1:实时监控定界——快速锁定异常场景

核心目标:10分钟内排查是否存在性能问题、明确问题类型与触发场景,不深入底层细节。

实操步骤(零基础可照做):

  1. 环境准备:USB连接真机(不支持模拟器),开启开发者模式与USB调试;确保macOS 12+,DevEco Studio版本匹配(建议5.1.0+)。
  2. 启动工具与选目标:通过菜单栏(View→Tool Windows→Profiler)、底部工具栏“Profiler”或搜索启动工具,在左侧会话区依次选择“设备—应用—进程”。
  3. 复现场景并监控:会话列表默认加载Realtime Monitor,操作应用复现核心场景(冷启动、列表滑动、动画播放等),观察数据区泳道的CPU、内存、帧率、GPU数据。
  4. 标记异常并定界:用快捷键M标记异常时间点,记录核心信息——如“列表滑动时帧率降至40fps(卡顿)”“内存多次操作后只增不减(泄漏)”,明确问题类型与场景。

干货技巧:实时监控仅用于“筛问题”,无需长时间录制;重点关注帧率、CPU占用两大指标,可快速锁定80%的表层性能问题。

步骤2:深度录制定位——精准找到代码根因

核心目标:针对定界的问题,用场景化模板录制精细化数据,从宏观指标拆解至具体代码行,找到根本原因。

实操核心步骤:

  1. 选对场景化模板(关键!):模板选错会导致数据无效,匹配关系如下:

问题类型

推荐模板

核心分析维度

页面滑动/动画卡顿

Frame/ArkUI

帧率丢帧、组件绘制、状态更新

应用启动慢

Launch

启动各阶段耗时、热点函数

ArkTS层内存泄漏

Snapshot

对象持有关系、内存分配节点

Native层问题

Allocation/CPU

Native内存分配、CPU热点函数

  1. 深度录制场景:选中模板后点击“Create Session”,点击录制按钮(▶),完整复现异常场景(如滑动卡顿需滑动3次以上),结束录制后等待数据解析。
  2. Top-Down逐层分析(高效方法):从宏观到微观拆解数据,以卡顿问题为例:

  • 顶层:Frame泳道查看丢帧时间点与类型(App侧/Render侧);
  • 中层:CPU/Callstack泳道查看耗时函数;
  • 底层:双击函数栈帧跳转至源码,定位耗时代码行。

干货技巧:用Alt+框选聚焦异常时段,可快速过滤无关数据;涉及Native层问题需导入离线符号表(工具控制栏按钮),还原函数名才能定位代码。

步骤3:代码优化+验证——形成闭环

核心原则:围绕“降负载”优化,分为永久降负载(彻底解决)与临时降负载(缓解体验),避免过度优化。

高频优化场景与方案:

  • 卡顿优化:简化UI层级(减少嵌套)、耗时计算移至子线程、避免滑动时执行复杂渲染。
  • 冗余刷新:拆分大型Object为小对象、避免子组件重复绑定同一状态变量。
  • 内存泄漏:释放无用对象引用、避免全局变量滥用、正确使用@Prop/@Link装饰器。

验证步骤:优化后重新用Realtime Monitor复现场景,对比指标——如卡顿场景帧率恢复至60fps、启动耗时缩短50%,即说明优化有效;未达标则重复“定位-优化”流程。

三、专项分析:Frame卡顿丢帧深度拆解

Frame模板是分析卡顿的核心工具,可覆盖GPU渲染、帧链路、异常操作等多维度,精准定位掉帧根源。

3.1 核心泳道解读(必懂)

展开Frame泳道后,重点关注以下子泳道,覆盖帧渲染全链路:

  • RS Frame/App Frame:分别对应Render Service侧与App侧帧数据,绿色为正常帧,红色为卡顿帧(耗时超16.6ms)。
  • Lost Frames/Hitch Time:直观展示丢帧数与卡顿时长,点选可查看具体时段数据。
  • Anomaly:检测图片解码超时(超8.3ms告警)、序列化/反序列化超时(默认8ms阈值),仅支持非上架应用。
  • User Events:查看用户操作(如点击)的处理耗时,定位交互卡顿原因。

3.2 实操分析流程(卡顿场景)

  1. 框选卡顿时段,查看RS Frame/App Frame泳道,判断卡顿来自App侧还是Render侧;
  2. 若为App侧卡顿:切换至ArkTS Callstack泳道,定位耗时最长的组件绘制或状态更新函数;
  3. 若为Render侧卡顿:查看GPU使用率,排查是否因硬件合成渲染过载;
  4. 通过“Statistics”区域统计卡顿率、次数,验证优化后的数据改善情况。

3.3 快捷键高效操作(提升50%效率)

  • 时间轴:W/S放大/缩小,A/D左右移动(需激活泳道区);
  • 标记:M添加单点标记,Shift+M添加时间段标记;
  • 标记切换:Ctrl+,/Ctrl+. 前后切换单点标记,Ctrl+[/Ctrl+] 切换时间段标记。

四、专项分析:ArkUI组件与状态卡顿定位

ArkUI层卡顿多源于组件布局、状态管理不当,通过ArkUI模板的专属泳道,可精准定位这类上层问题。

4.1 典型问题场景(高频踩坑点)

  1. 布局嵌套过多:组件层级超过5层,导致绘制链路冗长;
  2. 冗余刷新:更新大型Object部分属性,触发全对象刷新;
  3. 状态绑定异常:子组件重复绑定同一状态变量,更新时多次刷新;
  4. 装饰器误用:@Prop传递大型对象,引发不必要的深度拷贝。

4.2 核心泳道实操

4.2.1 ArkUI Component泳道(组件绘制分析)

  1. 框选时段后,“Summary”列表展示组件绘制统计(次数、总耗时、最大耗时),快速锁定绘制耗时最长的组件;
  2. 点选泳道条块,“More”区域展示组件树,直观查看布局嵌套层级,优化冗余组件。

4.2.2 ArkUI State泳道(状态更新分析)

  1. 录制状态更新场景(如点击按钮更新数据),“Summary”区域展示状态变量的变化次数、所属组件;
  2. 选中状态变量变化记录,开启“Delivery Chain”开关,图形化查看状态影响的组件链路,定位冗余刷新组件;
  3. 关联ArkUI Component泳道,验证状态更新是否触发组件过度刷新。

注意事项

因隐私政策,已上架应用不支持录制ArkUI Component/State泳道,需在开发测试阶段完成全量性能验证。

五、实战避坑与优化建议(干货总结)

结合大量项目实践,整理以下高频避坑点与优化技巧,帮你少走弯路:

  • 录制时务必完整复现场景:如卡顿需重复触发3次以上,避免数据碎片化导致定位失败;
  • 优先优化“耗时占比最高”的函数:这类函数往往是性能瓶颈的核心,优化后收益最明显;
  • 版本适配:页面布局查看、Component Animation等能力需DevEco Studio 5.1.0+,提前升级避免功能缺失;
  • 避免过度优化:如为简化布局牺牲功能扩展性,需平衡性能与代码可维护性;
  • 数据备份:解析完成后导出会话数据,便于团队共享分析或后续回溯问题。

六、总结

DevEco Profiler的核心价值的是“让性能问题可量化、可定位”,其优化流程的本质是“用数据驱动决策”——而非凭经验猜测。通过“实时监控定界→深度录制定位→优化验证闭环”的标准化流程,结合Frame与ArkUI专项分析,可高效解决HarmonyOS应用的各类性能问题。

建议在开发阶段就融入性能测试,每完成一个核心功能就用Realtime Monitor排查,避免上线前集中“救火”。

[Flutter 独立开发] 挑战千元机极限:纯客户端计算 K 线+指标,三星 A53 实测 70 FPS

大家好,我是《交易学徒》的独立开发者,祝大家周末愉快!

做过金融类 App 的朋友都知道,移动端的 K 线图( Candlestick Chart ) 渲染一直是性能优化的“深水区”。

为了降低服务器成本和网络延迟,我做了一个“违背祖宗”的决定:完全依赖客户端算力。 所有的技术指标( MA, BOLL, MACD 等)计算,全部在移动端本地实时完成,不依赖后端返回计算结果。

这意味着,一台三星 A53 ( Exynos 1280 处理器,典型的千元机性能)不仅要负责 UI 绘制,还要在主线程实时遍历数组计算指标。

在这种“地狱模式”下,优化成果如何?

📉 性能实测:A53 跑分数据

测试设备:三星 Galaxy A53 (Exynos 1280)
测试场景

  • GOLD 平均每秒 2.5 次报价
  • 加载 500 根 K 线数据
  • 同时开启 MA (移动平均线) + BOLL (布林带) + MACD 三组指标 + 图表网格
  • 所有指标数据均为 本地实时计算
  • 进行高频拖拽、缩放操作

实测结果

1. 三星 A53 (低端机代表)

A53 帧率测试图

70 FPS !
在这种重负载下,UI 线程依然保持极高的流畅度,超过 60Hz 的及格线。对于一款千元机来说,这个渲染性能我已经非常满意了。

2. 三星 S25+ (旗舰机代表)

S25+ 帧率图

旗舰机毫无压力,贵的还是好哇。


🏛️ 技术挑战:为何 Dart 能抗住?

很多人对 Flutter 的印象还停留在“套壳性能差”。但实际上,通过合理的架构,Dart 的性能完全够用。我的优化核心思路是:UI 渲染与数据计算分离,用空间换时间。

1. 极致的分层渲染 (Layered Rendering)

我利用 Stack 将视图拆解为三个独立的渲染层级:

  • **Layer A (底层)**:静态 K 线与网格。这是最“重”的层(包含数千个顶点),只有在缩放或平移时才重绘。
  • **Layer B (中间层)**:技术指标 (MA, BOLL)。与 K 线同步,但逻辑分离。
  • Layer C (交互层)这是优化的关键。包含当前的 Bid/Ask 价格线、十字光标。这一层极其轻量,且更新频率最高(每秒数次)。

通过 Stack + RepaintBoundary,实现了:价格跳动时,底层的几百根 K 线完全不需要参与重绘,GPU 只需要绘制那几条横线。

2. 动态 LOD (Level of Detail) 策略

在手机屏幕上展示 500 根 K 线时,GPU 光栅化压力巨大。
我在代码中加入了 LOD 策略:

  • Zoom In:渲染完整的 Candlestick(蜡烛图)。
  • Zoom Out:当可视区域数据点过多时,自动切换为 LineChart(收盘价连线)。

这极大地降低了 GPU 的顶点绘制数量,解决了缩放时的“卡顿感”。


📱 软件界面预览

目前的 UI 风格偏向现代扁平,针对移动端操作做了很多适配。

软件界面截图


🔗 下载与体验

软件目前已上架 Google Play ,名为“交易学徒”。如果你对高性能 Flutter 开发或者交易感兴趣,欢迎下载体验。

🎁 V 友专属福利

感谢大家看完这么枯燥的技术分析。
人肉送 VIP:在评论区留下你的 用户 ID(在“我的”页面可以看到),我会手动为你开通 1 个月的 VIP 会员

欢迎大家对 UI 、交互或者技术实现提出建议,每一条我都会认真看!

Android生态的硬件碎片化与Python解释型语言的执行特质,构成了性能优化的底层矛盾——这并非简单的代码精简或资源压缩所能破解,而是要深入两者运行逻辑的核心,实现从指令执行到资源调度的全链路协同。多数开发者在Android平台部署Python应用时,极易陷入“表层调优”的误区,过度纠结于脚本执行速度的零散提升,却忽视了ART虚拟机的字节码转换损耗、Python解释器与系统资源调度的节奏错位、跨层数据交互的隐性开销、硬件架构适配的精准度不足等深层问题。真正的性能突破,始于对Android运行时环境的本质认知:从不同CPU架构(ARMv8、x86等)的指令集差异到内存层级(高速缓存、物理内存、虚拟内存)的数据流转规律,从进程调度的优先级动态调整规则到原生能力调用的底层效率,每一个环节都暗藏着未被挖掘的优化空间。实践反复证明,只有让Python的动态执行逻辑与Android的静态资源管理体系形成“同频共振”,通过重构执行路径、优化资源分配策略、打通跨层交互壁垒、适配硬件特性,才能实现从“勉强运行”到“高速响应、低耗运行”的质变,这种底层逻辑的深度融合与动态协同,正是Android Python性能优化的核心要义,也是区分普通开发者与优化高手的关键所在。

Python解释器在Android平台的运行效率瓶颈,根源在于解释器内核与Android硬件架构、系统调度机制的适配断层,这种断层并非单一因素导致,而是多重逻辑冲突的叠加。不同品牌、不同价位的Android设备,其CPU架构存在显著差异,ARMv8架构的指令集精简高效,而x86架构则侧重兼容性,默认Python解释器的指令解析模块多为通用设计,未针对特定架构进行优化,导致在ARMv8设备上出现指令执行冗余,在x86设备上则因指令转换产生额外开销。同时,Android设备的内存层级缓存策略各不相同,部分中低端设备的高速缓存容量有限,而Python解释器的内存访问逻辑未考虑缓存命中率,频繁出现缓存失效,导致内存访问效率低下。更关键的是,Android的进程调度机制会根据应用的生命周期状态(前台、后台、休眠)动态分配CPU资源,而Python解释器的默认线程管理逻辑是独立于系统调度的,往往在应用进入后台后仍维持高资源占用,引发系统资源竞争,或在前台高负载运行时因CPU资源分配不足导致卡顿。应对这一困境,核心思路是对Python解释器进行“架构化定制”而非“通用化改造”:针对目标设备的CPU指令集,裁剪解释器内核中冗余的指令解析模块,保留与该架构高度兼容的核心执行逻辑,甚至对关键指令的解析流程进行重写,让指令执行更贴合硬件特性;同时优化解释器的线程调度模型,通过调用Android系统API感知应用的生命周期状态,在前台交互场景下自动提升线程优先级以保障响应速度,在后台运行时则降低线程调度频率、释放非必要资源,主动适配系统调度规则。在长期的实践探索中发现,经过架构化定制的解释器,在ARMv8架构的中高端Android设备上,指令执行效率提升近五成,内存占用降低三成,而在x86架构的平板设备上,兼容性未受影响的前提下,运行速度提升约三成,这一优化路径的关键在于“针对性适配”,要求开发者深入理解不同硬件架构的指令特性、Android的进程管理机制与线程调度规则,而非依赖通用化的解释器版本。

跨层数据交互的隐性开销,是Android Python应用性能损耗的重要来源,这种开销往往被开发者忽视,却在实际运行中占据了大量的响应时间,尤其在高频交互场景下更为明显。Python脚本与Android原生组件(如Activity、Service、ContentProvider)的交互,传统方式需经过多轮数据类型转换与序列化/反序列化过程,Python的动态数据类型(如列表、字典)需先转换为中间格式,再序列化后传输至原生组件,原生组件接收后需反序列化再转换为自身支持的数据类型,这一系列操作不仅存在数据格式不兼容的风险,更会因转换逻辑复杂、数据冗余导致响应延迟。在处理大数据量场景时,如实时传感器数据流(加速度传感器、陀螺仪数据)、图像像素数据、音频采样数据,这种开销会被急剧放大,甚至出现数据传输中断、交互卡顿的现象。很多开发者会选择第三方桥接库简化交互流程,但多数桥接库为兼容多场景、多数据类型,设计了通用化的转换逻辑,反而增加了额外的性能损耗,无法满足高频、大数据量交互的需求。有效的优化策略是“定制化数据交互协议”:基于具体业务场景的数据流特性,定义轻量化的私有数据格式,仅保留必要字段,剔除冗余信息,减少数据传输体量;同时绕过中间件的多层转发,直接调用Android原生的跨进程通信接口(如Binder),实现Python脚本与原生组件的直接数据传输,甚至将Python输出的数据直接封装为Android原生支持的内存缓冲区格式,彻底避免序列化/反序列化过程。例如在处理实时传感器数据时,通过定制化协议将传感器数据封装为连续的二进制流,直接写入原生组件的内存缓冲区,可将数据传输延迟降低六成以上,且数据丢失率几乎为零;在图像数据交互场景中,采用原生支持的像素格式进行数据传输,避免格式转换的性能损耗,可让图像处理的整体响应速度提升近一倍。这一优化思路的本质是“场景化精简”,即根据数据的传输频率、体量、格式要求,设计最贴合的交互路径,而非依赖通用化的桥接方案,这需要开发者同时掌握Python的数据处理逻辑与Android的原生通信机制、数据格式规范。

内存管理的动态均衡,是解决Android Python应用资源占用过高、运行卡顿的核心抓手,其关键在于让Python的内存分配逻辑与Android的内存回收机制形成深度协同,而非各自独立运行。Python解释器的默认垃圾回收策略是基于自身的内存占用阈值触发,完全未考虑Android设备的内存层级结构与系统级的内存回收机制,导致频繁出现“Python内存未释放而Android系统触发低内存查杀预警”的矛盾——Python解释器认为内存占用未达阈值,未触发垃圾回收,而Android系统已因整体内存紧张开始清理后台应用,若Python应用此时处于后台,极易被系统查杀;更隐蔽的是,Python的对象引用机制与Android的内存泄漏检测逻辑不兼容,部分Python对象的隐性引用无法被Android的内存检测工具识别,长期运行后会产生隐性内存占用,导致应用可用内存逐渐减少,响应速度变慢。此外,Python脚本中频繁创建与销毁短期对象的行为,会导致内存波动剧烈,增加Android系统内存管理的负担,进一步影响性能。优化的核心路径是“双维度内存调控”:一方面修改Python解释器的垃圾回收触发条件,通过调用Android系统API获取当前设备的可用内存比例、系统内存紧张状态,将其与Python自身的内存占用阈值结合,在系统内存紧张时提前触发垃圾回收,释放冗余对象,主动适配系统内存管理策略;另一方面优化Python脚本的对象创建逻辑,采用对象池复用机制,对频繁创建的短期对象(如数据处理过程中的临时变量、循环中的迭代对象)进行复用,减少对象创建与销毁带来的内存波动,同时通过代码重构避免循环引用、全局变量过度使用等导致垃圾回收无法识别的隐性占用。实践表明,通过这种双维度调控,Python应用的内存波动幅度可降低七成,后台运行时的内存占用可压缩至原来的一半,应用被系统低内存查杀的概率降低八成以上,且长期运行后的响应速度衰减幅度控制在10%以内,这一过程需要开发者深入理解Python的垃圾回收原理(如引用计数、标记-清除算法)与Android的内存管理架构(如内存分级、低内存查杀机制),实现两者的动态适配而非独立调控。

原生能力的深度融合,是突破Python在Android平台性能上限的关键路径,核心在于“用原生优势弥补解释型语言短板”,构建Python与Android原生的协同执行体系,而非让Python单独承担所有任务。Python作为解释型语言,在CPU密集型任务(如复杂数学计算、图像视频处理、大数据解析)和IO密集型任务(如高并发网络请求、大文件读写)中,受限于解释执行的特性,性能往往远不及Android原生开发语言(Java、Kotlin)编译后的机器码执行效率。但多数开发者仅满足于通过桥接库简单调用原生API,却未充分利用原生组件的底层优化能力——如原生图形处理框架的硬件加速、网络框架的并发调度优化、文件系统的高效读写接口,导致“原生优势未充分发挥”,整体性能仍受限于Python的解释执行速度。真正的深度融合,是基于“优势互补”的模块化分工:将核心性能瓶颈模块交由Android原生实现,充分利用原生框架的硬件加速、系统级优化能力,而Python则专注于业务逻辑编排、动态扩展、数据灵活处理等其擅长的领域,通过轻量化的交互接口实现两者的协同执行。例如在图像识别场景中,将图像预处理(如像素裁剪、格式转换、降噪)等CPU密集型操作封装为Android原生组件,利用原生图形框架的硬件加速能力提升处理效率,Python脚本仅负责调用该组件、传入原始图像数据,并处理最终的识别结果,这种分工可将整体处理效率提升三倍以上;在网络请求场景中,利用Android原生的网络框架实现高并发请求调度、缓存管理、断点续传等功能,Python则专注于数据解析、业务逻辑判断,避免解释型语言在网络IO调度中的低效问题;在大数据解析场景中,将数据读取、格式转换等IO密集型操作交由原生组件处理,Python专注于数据过滤、统计分析,可显著提升解析速度。这一优化思路的本质是“模块化分工”,即根据不同模块的性能需求与语言特性,合理分配执行载体,打破“单一语言开发”的思维定式,让Python与Android原生各自发挥优势,实现1+1>2的性能提升,这需要开发者同时掌握Python的业务编排能力与Android的原生开发技术。

性能监控与自适应调优体系的搭建,是保障Android Python应用长期稳定高效运行的核心支撑,而非依赖“一次性优化”的静态方案——Android生态的复杂性决定了固定优化策略无法适配所有场景。Android设备的硬件差异巨大,高端旗舰机的CPU性能、内存容量是入门机型的数倍,固定的运行参数在高端机上可能浪费资源,在入门机型上则可能导致卡顿;系统版本迭代频繁,从Android 10到Android 14,运行时特性、权限机制、资源调度规则均有变化,旧版本的优化方案可能在新版本上失效;用户的使用场景更是多样,前台交互场景需要高响应速度,后台计算场景需要低资源占用,低电量场景则需兼顾性能与功耗,固定的优化策略无法满足多场景需求。很多开发者在完成初期优化后缺乏持续监控机制,无法及时发现新场景、新设备、新版本系统下的性能退化,导致应用体验不稳定。

本次更新主要是体验优化上,列一下更新内容:

代码高亮方案换为 ShiKi

新方案体验有问题希望可以及时反馈我做修正,旧方案比较老旧了所以用了新的。

介绍页内容变化

经验值与金币值的常量现在实时了,另外可以输入站内表情以及自动优化中英文空格。

评论列表性能优化

之前楼层数多了,尤其是楼中楼多的情况下,输入会出现卡顿,新版做了优化看体验是否更好一些。

浏览优化

现在滚动时在左侧上方会出现返回按钮,方便回到上一页。

输入优化

现在评论的编辑框增加了@当前楼层的所有人以及@所有人。目前@所有人只取当前评论列表的所有用户名,后续再看是否查询全部。

金币消耗变动

♥️与 👍 现在的消耗增加,让这两个表情的分量更加重一些,且获得会被系统扣除掉 10%。同时金币打赏的最低额度设置为 100 金币,最高 500 金币,各位现在不那么容易慷慨解囊了。

既然有用户慷慨的发现有些操作不消耗金币,我就勉强加入了:

  • 编辑评论、帖子、附言均需要消耗金币;
  • 新增附言需要消耗金币。

消息提醒优化

现在长连接通知收到后,会在浏览器的标题上显示未读计数。

还有不少优化

不列了,比较琐碎。sobbing

最新消息,Apache DolphinScheduler 3.4.0 已正式发布!

本次版本带来了多租户调度隔离、工作流并行性能优化、任务重试与告警机制增强,以及资源管理和日志处理改进。无论是复杂企业业务场景,还是高并发任务调度,3.4.0 都让系统更高效、更可靠、更易用。立即升级,体验全新调度能力!

升级与下载

下载页面(可选择镜像下载):
https://dolphinscheduler.apache.org/zh-cn/download/3.4.0

GitHub Release 页面
https://github.com/apache/dolphinscheduler/releases/tag/3.4.0
升级时建议参考官方文档中的集群升级指南,确保兼容性和配置一致性。

核心功能增强与重要更新

通用 OIDC 认证支持

3.4.0 引入了对 OpenID Connect(OIDC)的通用支持,旨在简化与企业身份认证系统的集成。通过 OIDC,用户可以使用统一的身份提供商(如 Keycloak、Okta 等)进行 SSO 登录,无需额外实现复杂自定义逻辑。这提升了安全性和用户体验,尤其是在多系统联邦登录与统一认证场景中,能够使 DolphinScheduler 更自然地融入企业级认证体系,减少重复配置和验证成本,从而提高登录配置的扩展性和一致性。


(参考图)

gRPC 任务插件支持

本版本新增了 gRPC 任务插件能力,使调度器能够通过原生 gRPC 协议直接与远程服务交互。用户可以将后端微服务暴露的 gRPC 接口作为任务执行目标,无需中间脚本封装。这种方式特别适合微服务生态或跨语言执行场景,通过明确参数契约和高性能通信协议提升任务整合效率,从而减少资源调度延迟、提高任务可靠性。

支持工作流串行策略

实现了 工作流串行执行策略(Workflow Serial Strategy) 的核心逻辑重构,通过引入一个全新的串行命令队列机制(t_ds_serial_command 表及相关 DAO/Mapper),配合一套串行执行协调器(WorkflowSerialCoordinator)及策略处理器,使 DolphinScheduler 能更智能地管理串行类型的工作流(如 SERIAL_WAITSERIAL_PRIORITYSERIAL_DISCARD)。

该设计改进了工作流触发流程的执行类型判断、状态管理、命令队列处理等关键路径,使串行调度逻辑更清晰、更可靠,有助于提升串行工作流场景下的调度稳定性与可控性。同时,3.4.0 重构了触发器与状态机相关代码,增强该能力的可维护性和扩展性。

移除 PyTorch 任务类型

3.4.0 对任务类型体系进行了精简,正式移除了内置的 PyTorch 任务类型。该调整主要基于实际使用情况和长期维护成本的考量,因为原有 PyTorch 任务实现使用率较低,且与调度器核心任务模型耦合度较高,增加了版本演进和兼容性维护的复杂度。通过移除该任务类型,DolphinScheduler 能保持核心架构的简洁与稳定。

我们鼓励用户通过更通用的 Shell、Python 或插件化方式运行 PyTorch 作业,从而提升系统整体的可维护性和扩展性。

稳定性与重要修复

Kubernetes Worker 部署增强

在 Kubernetes 原生部署场景下,3.4.0 使 Worker StatefulSet 的 Helm Chart 支持注入 Secrets 和 InitContainers。通过 Secrets 注入,可以安全传递证书或凭据;InitContainers 允许在主容器启动前完成必要的初始化逻辑,如准备文件系统或校验环境依赖。

这些增强有助于在容器化环境下实现更安全、更一致的部署策略和生命周期管理。

SQL 任务取消能力

针对 SQL 任务类型,本次版本提供了对任务执行取消的原生支持。当执行的 SQL 语句由于逻辑错误或长期运行导致资源占用时,用户可以通过调度器下发取消操作,使任务尽快中止,而不是简单失败或等待超时。这一能力改善了任务控制能力,避免长时间运行对集群资源的无效占用,有助于提升整体资源利用率和执行调度体验。

条件任务节点在前置失败情况下执行逻辑修复

在某些复杂工作流中,当条件任务节点的前置任务失败时,条件节点未按预期执行。3.4.0 修复了这一调度核心逻辑,确保条件节点能够正确响应前置失败状态。这样,工作流分支逻辑能够按照既定 DAG 定义可靠运行,从而避免因逻辑错误导致的流程中断或不一致执行。

ZooKeeper 节点清理问题修复

在使用 ZooKeeper 作为协调组件的高可用部署中,部分用户反馈 Master Server 在启动失败后未正确清理已注册的 failover 节点路径,可能导致后续状态异常。该版本修复了这个问题,使 Master 在异常启动路径中能够正确清理关联注册节点,保持注册中心状态一致,确保高可用场景下集群状态的健康和可靠性。

Worker Group 分配逻辑错误修复

此前版本中,项目与 Worker Group 关联/移除操作可能在 API 层出现逻辑不一致,导致调度器未能正确识别项目与 Worker Group 的关系。本次版本修正了相关逻辑,使 API 行为与用户预期一致,从而改善 Worker 管控、资源隔离和调度分配体验。

此外,3.4.0 版本还进行了很多功能优化和问题修复,包括文档与配置规范完善(时区、安全、负载均衡)、核心调度与注册中心稳定性增强(TraceId、Failover 清理、可重入锁)、性能与资源管理优化(任务组索引)、前端与插件体验改进(日志查询、DataX 校验、文件展示)、依赖与安全更新(PostgreSQL JDBC、Spring Boot CVE 修复)等,篇幅所限不再一一展开,详情可查询完整更新列表:https://github.com/apache/dolphinscheduler/releases/tag/3.4.0

Bug 修复亮点

标记任务为 Inactive 状态逻辑修复

某些生命周期事件中,当任务状态需要被标记为 Inactive 时,状态变更可能未正确触发,导致 UI 和执行引擎状态不一致。此版本修复了这一逻辑,使状态标记与生命周期事件更加一致。

Workflow Lineage 删除逻辑优化

在工作流血缘关系删除操作中,系统可能未能彻底清理相关引用,导致历史血缘链路残留。3.4.0 改进了删除逻辑,使 DolphinScheduler 在删除血缘链时能够更精确地清理对应关系,避免分析后续依赖时出现错误链路。

其他 Bug 修复包括前置任务失败导致条件节点不执行问题修复、项目级 Worker Group 绑定与移除逻辑修正、子工作流触发参数丢失问题修复等,详情请查询完整 Release Note:https://github.com/apache/dolphinscheduler/releases/tag/3.4.0

文档更新

  1. 发布并完善 Apache DolphinScheduler 3.3.2 版本发布说明文档。
  2. 修复文档 CI 构建错误,提升文档发布流程的稳定性。
  3. 补充 Prometheus 指标接口的认证机制及其在 Kubernetes 环境下的使用说明。
  4. 同步更新 JdbcRegistry 引入事务机制后的相关文档描述,保证文档与实际行为一致。

致谢

本次版本发布离不开社区各位贡献者的热情参与与支持。特别感谢 @ Gallardot 作为 3.4.0 的 Release Manager,从版控、构建、候选版验证到最终投票组织,确保发布流程高质量推进。

同时,感谢以下本次版本的所有贡献者(GitHub ID,排名不分先后):

Gallardot、njnu‑seafish、det101、Mrhs121、EinsteinInIct、sanfeng‑lhh、ruanwenjun、tusaryan、qiong‑zhou、SbloodyS、kvermeulen、npofsi、CauliflowerEater、ChaoquanTao、dill21yu、sdhzwc、zhan7236、KwongHing、jmmc‑tools、liunaijie

感谢所有通过提交 PR、Issue、文档贡献、社区讨论、测试验证等方式参与 Apache DolphinScheduler 项目的人。正是你们的努力推进了 DolphinScheduler 的持续演进与社区繁荣,欢迎更多人加入我们的队伍!

【Unity Shader Graph 使用与特效实现】专栏-直达

Billboard节点是UnityShaderGraph中一个功能强大的顶点变换工具,专门用于实现面向相机的渲染效果。在实时渲染中,Billboard技术被广泛应用于粒子系统、植被渲染、UI元素和特效制作等领域,能够确保特定物体始终面向摄像机,从而提供最佳的视觉效果。

Billboard技术概述

Billboard技术源于计算机图形学中的精灵渲染概念,其核心思想是通过动态调整物体的朝向,使其始终面对观察者。这种技术在游戏开发中具有重要价值:

  • 在粒子系统中用于渲染烟雾、火焰、魔法效果等动态元素
  • 在开放世界游戏中用于优化树木和植被的渲染性能
  • 在UI系统中确保界面元素始终以正确角度显示
  • 在特效制作中创建各种视觉欺骗效果

UnityShaderGraph中的Billboard节点封装了这一复杂技术,让开发者能够通过可视化方式轻松实现面向相机的渲染效果,无需编写复杂的着色器代码。

节点端口详解

Billboard节点包含多个输入和输出端口,每个端口都有特定的功能和用途。

输入端口

Position OS端口接收物体空间的顶点位置数据。这个端口是Billboard变换的基础,提供了需要进行旋转的原始顶点坐标信息。在实际应用中,这个端口通常直接连接到顶点着色器的位置输出,或者与其他位置变换节点相连。

Normal OS端口处理物体空间的法线向量。法线数据对于光照计算至关重要,Billboard节点会对法线进行相应的旋转,确保光照效果在物体旋转后仍然正确。如果忽略法线变换,可能会导致光照异常或材质表现不正确。

Tangent OS端口管理物体空间的切线向量。切线主要用于法线贴图和某些高级着色效果,Billboard节点会同步旋转切线数据,保持与顶点和法线的一致性。在需要复杂材质表现的场景中,正确的切线变换尤为重要。

输出端口

Position输出端口提供旋转后的物体空间顶点位置。这是Billboard节点的核心输出,包含了经过相机对齐变换后的顶点坐标。这个输出通常直接连接到主节点的顶点位置输入,完成最终的顶点变换。

Normal输出端口返回旋转后的物体空间法线向量。变换后的法线确保了光照计算与物体新朝向的一致性,对于保持材质视觉真实性至关重要。

Tangent输出端口提供旋转后的物体空间切线向量。这个输出确保了法线贴图和其他依赖切线空间的着色效果能够正确工作。

控件参数解析

Billboard Mode是Billboard节点最重要的控制参数,决定了物体的对齐方式和旋转行为。

All Axis模式

All Axis模式实现完全相机对齐,物体的所有坐标轴都会与相机坐标系对齐。在这种模式下,物体会完全面向相机,类似于始终正对观察者的广告牌。

这种模式的特点包括:

  • 物体完全面向相机,保持正面朝向观察者
  • 所有轴向都会根据相机方向进行旋转
  • 适用于需要完全正面展示的效果,如粒子特效、公告板文字等
  • 在VR和AR应用中特别有用,确保UI元素始终面向用户

All Axis模式的一个典型应用场景是粒子系统中的精灵渲染。当相机移动时,每个粒子都会自动调整方向,始终以最佳角度面向观察者,从而保证视觉效果的一致性。

Around Y Axis模式

Around Y Axis模式提供受限的对齐方式,物体仅围绕Y轴旋转,保持Y轴方向不变。这种模式在保持物体部分方向稳定的同时,实现基本的面向相机效果。

这种模式的特点包括:

  • 物体围绕世界空间或物体空间的Y轴旋转
  • X轴和Z轴与相机对齐,但Y轴保持原有方向
  • 适用于树木、路灯等需要保持垂直方向的物体
  • 在开放世界游戏中广泛用于植被渲染优化

Around Y Axis模式在大型场景的性能优化中特别有用。通过将3D树木替换为Billboard四边形,可以大幅减少渲染负载,同时通过限制Y轴旋转保持视觉上的自然感。

技术实现原理

理解Billboard节点的内部工作原理有助于更好地使用和调试相关效果。

顶点变换矩阵

Billboard节点的核心是基于视图矩阵的逆向变换。本质上,它计算相机的旋转矩阵,然后将这个旋转应用于输入的顶点数据。在All Axis模式下,节点会提取相机的完整旋转矩阵;而在Around Y Axis模式下,则会提取并修改旋转矩阵,将Y轴分量重置为单位矩阵的Y轴。

数学上,这个过程可以表示为:

旋转矩阵 = 提取相机旋转矩阵
如果模式为Around Y Axis:
    旋转矩阵[1] = [0, 1, 0] // 重置Y轴
变换后位置 = 旋转矩阵 × 原始位置

法线和切线变换

法线和切线的变换遵循与位置数据相同的旋转逻辑,但由于它们是方向向量而非位置点,变换时不考虑平移分量。正确的法线和切线变换确保了光照和材质效果在Billboard变换后仍然保持视觉一致性。

法线变换需要特别注意,由于法线是协变向量,其变换矩阵通常为顶点变换矩阵的逆转置矩阵。但在Billboard这种纯旋转的情况下,由于旋转矩阵是正交矩阵,逆转置矩阵等于原矩阵,因此可以直接使用相同的旋转矩阵。

实际应用案例

Billboard节点在游戏开发中有多种实际应用,以下是一些典型场景。

粒子系统效果

在粒子系统中,Billboard技术是创建各种视觉特效的基础。

火焰和烟雾效果可以通过Billboard四边形配合透明度渐变纹理实现。每个粒子都是一个面向相机的四边形,使用噪声纹理和颜色渐变创建动态的火焰和烟雾外观。通过All Axis模式确保无论相机如何移动,效果都能正确显示。

魔法和能量场效果利用Billboard节点创建环绕角色的魔法光环或能量屏障。结合扭曲效果和发光着色器,可以制作出视觉上吸引人的魔法特效。Billboard确保这些效果始终面向玩家,提供最佳的视觉体验。

环境装饰优化

在大型开放世界游戏中,Billboard技术是性能优化的重要手段。

树木和植被渲染使用Around Y Axis模式的Billboard技术,将复杂的3D树木模型替换为简单的四边形,大幅减少三角形数量。当玩家距离较远时,使用Billboard树木;当玩家靠近时,逐渐淡入完整的3D模型。这种LOD(层次细节)策略在保持视觉质量的同时显著提升性能。

远处山脉和云层可以通过Billboard技术创建。使用多层Billboard平面配合透明度混合,可以模拟出具有深度感的远景效果。这种方法比使用完整3D模型更加高效,特别适合移动平台或性能受限的场景。

UI和交互元素

在用户界面和交互设计中,Billboard技术确保重要信息始终可见。

世界空间UI元素使用Billboard技术创建始终面向玩家的对话框、任务提示或交互图标。这在3D游戏中特别有用,玩家可以从任何角度都能清晰看到UI内容。

AR和VR应用中的界面元素通过Billboard技术确保虚拟界面始终面向用户,提供自然的交互体验。无论是信息面板、控制菜单还是虚拟标签,Billboard都能保证最佳的可读性和可用性。

性能优化考虑

使用Billboard节点时需要考虑性能影响,特别是在大量使用的情况下。

渲染性能

Billboard技术通过减少几何复杂度来提升性能,但顶点着色器的计算负载会增加。在移动设备或低端硬件上,需要平衡视觉质量和性能消耗。

优化策略包括:

  • 控制Billboard物体的数量,避免在同一帧中渲染过多Billboard
  • 使用LOD系统,根据距离动态切换Billboard和完整模型
  • 合并多个Billboard物体,减少绘制调用
  • 在性能敏感的区域使用更简单的Billboard效果

内存和带宽

Billboard通常使用简单的四边形几何体,这有助于减少内存占用和顶点数据传输带宽。但在使用高质量纹理时,需要注意纹理内存的消耗。

优化建议:

  • 使用纹理图集将多个Billboard纹理合并为一张大图
  • 根据距离使用不同分辨率的纹理
  • 压缩纹理格式以减少内存占用
  • 合理管理纹理的加载和卸载,避免内存峰值

常见问题与解决方案

在使用Billboard节点时可能会遇到一些常见问题,以下是相应的解决方案。

光照异常

问题描述:Billboard物体上的光照显示不正确,高光或阴影位置异常。

解决方案:

  • 确保正确连接Normal OS端口,并提供准确的法线数据
  • 检查Billboard模式是否适合场景需求
  • 在复杂光照环境下,考虑使用自定义光照模型或简化光照计算
  • 验证法线贴图是否正确应用,确保切线数据正确变换

深度排序问题

问题描述:Billboard物体与其他物体的深度排序错误,出现穿透或遮挡异常。

解决方案:

  • 调整渲染队列顺序,确保Billboard物体在正确的渲染阶段绘制
  • 使用Alpha混合时,注意透明物体的渲染顺序问题
  • 在粒子系统中使用软粒子技术缓解深度冲突
  • 考虑使用自定义深度偏移解决特定的排序问题

运动模糊和抗锯齿

问题描述:快速移动的Billboard物体可能出现运动模糊异常或抗锯齿效果不佳。

解决方案:

  • 在运动剧烈的Billboard物体上禁用运动模糊,或使用自定义运动向量
  • 调整抗锯齿设置,确保Billboard边缘平滑
  • 对于特别敏感的视觉效果,考虑使用更高分辨率的纹理
  • 在后期处理中应用特定的抗锯齿技术,如TAA(时间性抗锯齿)

高级应用技巧

掌握了Billboard节点的基本用法后,可以探索一些高级应用技巧。

自定义Billboard效果

通过组合Billboard节点与其他ShaderGraph节点,可以创建独特的视觉效果。

倾斜Billboard效果通过修改旋转矩阵,使Billboard物体以特定角度倾斜,而不是完全面向相机。这种效果可以用于创建更有动态感的粒子特效或风格化的视觉元素。

动态朝向Billboard根据游戏逻辑或玩家输入动态调整Billboard的朝向,而不是始终面向主相机。这种技术可以用于创建始终面向特定目标的效果,如追踪导弹的尾焰或指向任务目标的导航标记。

与其他系统的集成

Billboard节点可以与Unity的其他系统集成,创建更复杂的效果。

与VFX Graph集成,在视觉特效图中使用Billboard技术创建高性能的粒子效果。VFX Graph提供了更强大的粒子系统功能,结合Billboard可以实现电影级的视觉效果。

与Shader Graph高级特性结合,如曲面细分、几何着色器或光线追踪,创建更复杂的Billboard效果。这些高级技术可以增强Billboard的视觉质量,提供更逼真或更风格化的外观。


【Unity Shader Graph 使用与特效实现】专栏-直达
(欢迎

点赞留言

探讨,更多人加入进来能更加完善这个探索的过程,🙏)

当Python的科学计算与JavaScript的前端交互禀赋在浏览器环境中实现无界交融,一种颠覆传统开发逻辑的协同范式正悄然重塑Web开发的底层逻辑。这种无需后端中转、摆脱环境依赖的直接互操作,绝非简单的语法移植或功能拼接,而是基于运行时深度耦合的能力重构。在长期的探索中逐渐发现,浏览器内跨语言协作的核心价值,在于打破两种语言固有的生态壁垒,让数据流转与功能调用脱离接口协议的束缚,形成原生级的协同闭环。无论是需要前端承载复杂数据建模的可视化应用,还是依赖密集计算的交互式工具,这种互操作模式都能将Python在数据分析、机器学习领域的生态优势,与JavaScript在DOM操作、用户交互上的灵活性无缝衔接,构建出更轻量化、高效率的开发路径。这种变革背后,是对Web开发本质的重新认知——前端不再仅仅是界面呈现的载体,而是能够整合多语言能力的综合计算平台,让前端开发者无需切换开发环境即可调用全量Python工具链,同时为数据科学家提供了将模型与可视化成果直接嵌入网页的便捷途径,实现了技术能力的双向赋能与价值放大。在实际体验中,这种协同模式带来的不仅是开发效率的提升,更是思维方式的转变,让跨语言协作从“按需适配”升级为“原生共生”,为Web应用的功能边界与体验深度开辟了全新可能。

浏览器内JS与Python互操作的底层实现,其核心逻辑在于通过字节码转译技术构建共享执行空间,彻底摆脱了传统跨进程通信的性能瓶颈与复杂度。这种基于WebAssembly的沙箱化运行环境,能够让Python解释器在浏览器中原生启动,同时建立与JavaScript引擎的直接通信链路,实现两种语言的内存级交互。双向调用的实现并非依赖标准化的接口定义,而是通过构建动态适配层,完成类型系统的隐式转换与函数签名的智能映射,让不同语言的函数能够像原生函数一样被直接调用。在实践探索中发现,这种通信机制支持同步与异步两种调用模式,同步调用适用于轻量级计算场景,能够确保数据实时反馈,满足界面交互的即时性需求;异步调用则通过事件循环的协同调度,将Python的密集计算任务分流至后台,避免阻塞JavaScript的前端渲染进程,保障界面的流畅性。更具价值的是,借助Web Worker的并行处理能力,可以将Python的计算任务分配至独立的线程中,实现两种语言的并行执行,既充分发挥了Python在数据处理、模型计算上的效率优势,又保留了JavaScript对前端界面的精准控制。这种底层架构的创新,让跨语言调用的延迟降至微秒级,为复杂场景的应用提供了坚实的技术支撑。在实际测试中,即便是处理大规模数据集的转换与分析,也能实现无感知的实时响应,这种原生级的协同体验,是传统跨语言方案难以企及的。

环境适配是浏览器内JS与Python互操作落地的关键环节,其核心挑战在于解决Python生态与浏览器运行环境的兼容性鸿沟。Python的众多第三方库在设计之初并未考虑浏览器场景的限制,大量依赖系统级API与C扩展模块,直接迁移至浏览器环境必然面临功能失效的问题。实践中采用的惰性加载适配策略,并非简单的库移植,而是基于依赖分析的按需加载机制——通过静态分析工具识别Python代码的依赖链条,仅在实际功能被调用时,动态引入所需模块及其适配版本,既大幅减少了初始加载的资源体积与耗时,又有效降低了内存占用。对于包含C扩展的复杂库,通过编译层面的深度改造,将C代码转换为浏览器可识别的WebAssembly字节码,同时保留原有API的调用方式与参数规范,确保开发者无需修改代码即可直接使用。针对两种语言的数据类型差异,构建了智能转换机制,能够自动识别数值、序列、映射等不同类型的数据,在传递过程中完成格式适配与精度保留,避免手动转换带来的繁琐操作与数据丢失风险。此外,在适配过程中充分考虑了不同浏览器对WebAssembly的支持差异,通过特征检测与降级处理,确保在主流浏览器中都能获得一致的运行体验。这种环境适配的思路,既尊重了两种语言的原生特性与生态完整性,又通过灵活的适配层设计,实现了生态资源的最大化利用,为互操作模式的广泛应用奠定了基础。

能力封装的核心目标在于构建无感知的跨语言调用层,让开发者能够摆脱底层实现细节的束缚,以原生函数调用的体验实现JS与Python的相互调用。这种封装并非简单的函数包裹,而是基于接口标准化与功能模块化的设计理念,将Python的核心能力拆解为高内聚、低耦合的功能单元,同时为Python提供访问浏览器API的统一入口,实现双向能力的无缝渗透。在设计过程中,重点强化了函数调用的语法一致性,无论是从JavaScript调用Python的数据分析函数,还是从Python调用JavaScript的DOM操作方法,都采用统一的调用语法与参数传递规则,降低了跨语言开发的认知成本。针对异步场景,通过回调机制与Promise异步模式的深度融合,解决了跨语言调用中的异步协同问题,确保数据处理与界面响应的有序进行,同时提供了完善的异常捕获机制,让跨语言调用过程中的错误能够被精准定位与处理。此外,封装层还具备良好的可扩展性,支持开发者根据具体需求自定义类型转换规则与函数适配逻辑,实现个性化的协同方案。在实际使用中,这种封装策略不仅大幅降低了开发门槛,更实现了两种语言能力的有机整合,让开发者能够根据场景需求灵活组合使用两种语言的优势功能——比如用Python处理复杂的数值计算与数据建模,用JavaScript实现流畅的交互反馈与可视化呈现,构建出功能更强大、架构更简洁的应用,真正实现了“1+1>2”的协同效应。

性能优化是浏览器内JS与Python互操作走向实用的关键,其核心在于突破数据传输与计算调度的双重瓶颈,实现跨语言协同的高效稳定运行。在数据传输方面,摒弃了传统的JSON序列化与反序列化方式,采用基于内存视图的直接数据访问模式,让两种语言能够共享同一块内存区域,数据在传递过程中无需进行格式转换与拷贝,大幅降低了传输延迟与性能损耗。对于大规模数据集的处理,通过分块传输与流式处理相结合的方式,将数据分解为可并行处理的单元,既减少了单次传输的资源压力,又通过并行计算提高了整体处理效率。在计算调度上,构建了动态负载均衡机制,通过实时监控浏览器的CPU、内存占用情况,智能分配JavaScript与Python的计算任务,当前端界面需要响应用户操作时,自动降低Python计算任务的资源占用,确保界面流畅;当处于后台计算场景时,则充分利用空闲资源提升Python的计算效率。针对Python在浏览器中运行的特性,对垃圾回收机制进行了优化调整,通过动态调整回收时机与回收策略,避免长时间运行导致的内存泄漏问题,同时减少垃圾回收过程对前端交互的影响。此外,还通过代码层面的优化,比如Python函数的惰性执行、重复计算的缓存机制等,进一步提升运行效率。在实际测试中,经过多维度优化后,跨语言调用的性能损耗已降低至可忽略的范围,即便是处理百万级数据的分析任务,也能保持流畅的用户体验,为复杂场景的落地提供了性能保障。

生态融合与场景落地是浏览器内JS与Python互操作的最终价值体现,这种协同模式正在重构多个前端应用场景的开发逻辑,催生全新的应用形态。在数据可视化领域,Python的数据分析库能够直接处理前端获取的原始数据,完成数据清洗、建模、统计分析等复杂操作,生成的结果无需转换即可通过JavaScript的可视化工具渲染为交互式图表,实现从数据处理到界面呈现的全流程浏览器内完成,既减少了数据传输的延迟,又提升了可视化的实时性与交互性。在在线教育场景中,借助这种互操作模式,可构建轻量化的在线编程环境,学习者能够在网页中直接编写运行Python代码,通过JavaScript实现实时的代码校验、结果反馈与错误提示,同时结合前端交互设计,打造沉浸式的编程学习体验,让编程教育突破环境限制,更具便捷性与普及性。在科研工具开发中,可将Python的专业计算模型与JavaScript的交互界面相结合,打造无需安装、跨平台的科研辅助工具,科研人员能够通过前端界面输入参数、调整模型,实时获取计算结果与可视化分析,大幅降低科研工具的使用门槛。

文件I/O的效能瓶颈始终潜藏于数据从内存到存储介质的流转链路中,传统同步读写模式下的固定缓冲策略,早已无法匹配现代应用中多变的读写场景与海量数据处理诉求。异步缓冲优化算法的核心突破,绝非简单扩容缓冲空间或调整读写触发时机,而是构建了一套基于数据行为预判的动态资源调度体系,让缓冲策略与I/O请求特征、存储介质特性形成毫秒级实时联动。这种重构彻底打破了“缓冲即静态缓存”的固有认知,将异步机制的非阻塞优势与缓冲的预载、合并、分流能力深度绑定——在数据未被显式请求时,通过历史行为建模提前预判加载;在请求密集爆发时,智能合并同类操作减少设备交互;在系统空闲时段,通过分批落盘优化存储写入效率,实现了从“被动响应请求”到“主动适配需求”的效能跃迁。无论是大规模日志采集场景中每秒数万条记录的写入压力,高清视频流式处理时的低延迟读取需求,还是分布式数据备份中的跨节点数据传输,这种优化算法都能通过精准的行为感知,让文件I/O的延迟与吞吐量达到动态平衡。在长期的实践观察中发现,这种算法的价值不仅在于逻辑层面的革新,更在于对数据流转本质的重新解构——它不再将缓冲视为孤立的中间层,而是作为串联请求与存储的智能枢纽,为高并发、大数据量场景下的I/O处理提供了全新的解题思路,其带来的效能提升往往能突破硬件本身的物理限制,实现软件层面的效能重构。

异步缓冲优化算法的底层逻辑,核心在于构建“请求解析-缓冲调度-存储适配”的三角联动机制,而非孤立优化单个环节的性能表现。异步机制的真正价值并非单纯的非阻塞执行,而是通过对请求队列的智能排序与优先级调度,为缓冲策略争取宝贵的预判与调整时间窗口。缓冲层在此架构中不再是静态的中间存储区域,而是具备行为感知能力的动态枢纽,能够实时捕捉I/O请求的频率、数据块大小、访问连续性、重复度等多维特征,进而动态调整数据预载的范围、缓冲分区的划分规则以及数据落盘的时机与批次。在实际调试中发现,当算法检测到连续的顺序读取请求时,会自动扩大预载范围,按照存储介质的物理扇区大小,提前将后续1-3个数据块载入缓冲,这种预载策略能将磁盘寻道次数降低60%以上;而当识别到离散的小文件写入请求时,则会启动“零散数据聚合”机制,设置动态调整的聚合阈值,将短时间内来自不同进程的小写入请求暂时存储于缓冲的独立分区,待数据量达到阈值或触发超时机制后,批量写入存储介质,这种方式能有效减少存储设备的写入次数,降低机械硬盘的磁头损耗与SSD的写入放大效应。这种联动机制的实现,依赖于对I/O行为的精细化建模——通过统计学习方法捕捉请求模式的隐性规律,比如工作日高峰时段的请求密度、特定应用的读写偏好等,让缓冲策略能够自适应不同应用场景与存储设备的特性。它既避免了固定缓冲导致的资源浪费,又解决了异步调度中数据一致性与延迟控制的核心矛盾,在实际应用中,这种底层逻辑的优化能让文件I/O的整体效能提升30%-50%,实现了执行效率的根本性跃迁。

不同文件I/O场景的请求特征存在显著差异,异步缓冲优化算法的落地关键在于场景锚定与策略动态贴合,而非用一套固定方案适配所有情况。在高清视频流式处理场景中,I/O请求呈现大尺寸、连续性强、低延迟需求突出的特点,算法会针对性采用“大区块预载+增量缓冲”策略——将视频数据按帧组划分为固定大小的区块,通常以8MB或16MB为单位,在播放器解码当前区块时,提前载入后续1-2个区块的核心数据,同时根据解码进度动态补充剩余部分,既满足实时播放对低延迟的要求,又避免过量预载占用过多内存资源。实际测试中,这种策略能将视频加载的卡顿率降低70%以上,尤其在网络带宽波动或存储性能不稳定的环境中,表现更为突出。日志采集场景则以高频、小尺寸、离散写入为典型特征,算法会启用“请求聚合+延迟落盘”机制,设置基于系统负载动态调整的聚合阈值,当系统负载较低时,阈值可适当降低以保证数据实时性;当负载较高时,阈值自动提升以减少I/O交互。同时,通过缓冲分区隔离不同日志源的数据,防止多进程写入时的数据干扰,这种方式能将日志写入的吞吐量提升40%,且有效降低存储介质的写入压力。在分布式数据备份场景中,I/O请求伴随网络传输延迟与存储节点负载波动,算法会引入“缓冲水位动态调整”机制——实时监测网络带宽、节点响应速度与存储队列长度,动态调整缓冲的高低水位线。当网络拥堵时,提高水位线暂存更多数据,避免数据丢失或传输超时;当节点空闲时,降低水位线加速落盘,确保备份任务高效推进。这种场景化的适配思路,要求算法具备极强的灵活性,能够根据场景的核心痛点动态切换策略,在实际落地中,正是这种精准的场景适配让算法能够在不同领域都发挥出最优效能,避免了“一刀切”方案带来的适配短板。

缓冲的动态调整是异步优化算法的核心创新点,其关键在于摒弃传统的固定阈值模式,构建基于实时负载与请求特征的自适应调节体系。传统缓冲策略中,阈值设定往往依赖经验值,容易导致轻负载时缓冲利用率不足,重负载时缓冲溢出或数据积压,进而引发效能波动。新算法通过引入“缓冲生命周期管理”概念,将缓冲空间划分为预载区、活跃区、待落盘区三个动态分区,每个分区的大小根据实时I/O压力与系统资源状况动态伸缩,实现资源的最优分配。预载区的大小由请求连续性预测模型决定,模型通过分析近期请求的连续度、访问频率等数据,预判后续可能的访问范围,当预测到高连续性请求时自动扩容,离散请求时则收缩,确保预载的针对性;活跃区用于缓存当前高频访问的数据块,通过热度衰减机制淘汰长期未被访问的内容——设定基于访问次数与时间的双重权重,比如近5分钟内访问3次以上的数据视为热数据,超过30分钟未访问则自动标记为冷数据并释放空间,避免无效占用内存;待落盘区则根据存储介质的写入性能动态调整数据批量落盘的阈值,针对机械硬盘的高寻道延迟,适当提高阈值以减少写入次数;针对SSD的高速写入特性,降低阈值以保证数据实时性。同时,算法会实时监测系统内存占用、磁盘I/O队列长度等核心指标,当内存使用率超过80%时,优先释放非核心数据的缓冲空间;当磁盘I/O队列长度低于阈值时,主动清理待落盘区数据,确保缓冲资源在系统整体负载中处于最优分配状态。这种动态调整机制,让缓冲层具备了自我优化的能力,能够在复杂多变的运行环境中始终保持高效运转,避免了传统策略中“要么浪费资源,要么效能不足”的两难困境。

异步缓冲优化算法的性能调优,核心在于在延迟、吞吐量、资源占用三者之间寻求动态平衡,而非追求单一维度的极致提升。延迟控制的关键在于数据预载的精准度,算法通过分析历史I/O请求数据,构建请求序列预测模型——基于马尔可夫链或时序分析方法,捕捉请求的前后关联规律,提前预判后续可能被访问的数据块,将磁盘I/O操作提前至系统空闲时段完成,从而隐藏存储延迟。在实际调优中发现,预测模型的准确率每提升10%,I/O延迟可降低15%左右,因此模型的持续迭代优化成为延迟控制的核心。吞吐量优化则依赖于请求合并与并行调度的协同——将多个目标地址相同或相邻的I/O请求合并为单次操作,减少磁盘寻道与指令开销;同时,利用异步机制的并行处理能力,将不同分区的缓冲数据分配至独立的处理线程,实现数据预载、缓冲处理、磁盘写入的并行执行,这种并行调度能让吞吐量提升25%-40%,尤其在多进程并发读写场景中效果显著。资源占用的控制则通过缓冲池化管理实现,算法会根据系统整体资源状况,动态调整缓冲池的总容量,避免因缓冲过度占用内存导致系统卡顿;同时,采用“冷热数据分离”策略,将高频访问的热数据保留在高速缓冲中,低频访问的冷数据及时释放,确保缓冲资源的高效利用。在实际调优过程中,需要根据应用的核心诉求灵活调整三者的权重:实时性要求高的场景(如视频直播、实时监控数据写入)优先保障低延迟,适当牺牲部分吞吐量;数据传输密集型场景(如大数据批量处理、备份任务)则侧重提升吞吐量,在资源占用可控的前提下放宽延迟限制。这种多维度的精细化调控,让算法能够适配不同应用的性能需求,实现整体效能的最优解,而非单一指标的片面提升。

异步缓冲优化算法的落地价值不仅在于提升单一文件I/O的性能,更在于为复杂系统的底层效能重构提供了可复用的核心逻辑,其探索方向正朝着更智能、更贴合业务本质的方向延伸。在实际应用中,该算法已在多个非电商金融场景中展现出显著价值:在气象数据采集系统中,通过优化海量传感器数据的写入逻辑,将数据处理延迟降低40%以上,确保气象预测的实时性与准确性;在影视后期制作平台中,通过大文件分片缓冲与预载策略,实现了4K高清素材的流畅读写与实时编辑,让剪辑师无需等待数据加载,工作效率提升35%;在企业级备份系统中,通过请求聚合与动态落盘机制,将备份效率提升30%,同时减少了存储设备的写入损耗,延长硬件使用寿命达20%。这些落地案例充分证明,算法的价值并非停留在理论层面,而是能够切实解决实际场景中的效能痛点。未来的探索将聚焦于更深度的智能感知能力——比如结合存储设备的硬件特性(如机械硬盘的寻道时间、SSD的擦写寿命)进行自适应优化,根据不同硬件的性能曲线调整缓冲策略;基于业务逻辑的请求优先级动态排序,让核心业务的I/O请求获得更高的调度权重,确保关键操作的响应速度。

引言

随着大模型和多模态 AI 的快速发展,向量已成为文本、图像、音视频等多元数据的通用语义表示。在这种背景下,检索增强生成(RAG)技术成为连接私有知识与大模型的核心桥梁,而高效的向量检索则是其关键支柱。

与将向量检索视为独立外挂服务的方案不同,Apache Doris 4.0 选择将向量检索能力深度集成于其 MPP 分析型数据库内核。实现向量检索与 SQL 计算、实时分析和事务保障的无缝融合。

本文旨在深入剖析 Doris 向量检索的系统级设计与工程实践,展示其如何在性能、易用性与规模扩展之间取得的平衡。

1. ANN 索引核心设计

Apache Doris 的向量索引基于 ANN(近似最近邻)算法实现,并非独立的外挂组件,而是深度集成于存储、执行与 SQL 引擎中的原生能力。在 4.x 版本中,其核心 ANN 索引能力主要包括以下几方面:

  1. 多索引类型与距离度量支持:支持主流的 ANN 索引类型(HNSW、IVF)及常见距离度量(L2 距离、内积)。用户可根据业务在构建速度、内存占用与召回率上的要求灵活权衡。
  2. 原生 SQL 集成:向量检索以原生 SQL 算子形式提供,支持直接定义向量列、通过 ORDER BY distance LIMIT K 进行相似度搜索,并能与过滤、聚合、JOIN 等算子自由组合,天然支持混合检索与分析
  3. 构建与查询解耦:采用异步索引构建机制,数据导入后即可查询,索引在后台构建并加载,避免导入阻塞,保障查询高峰期的稳定低延迟写入。
  4. 向量压缩优化:在导入与构建阶段支持标量量化(SQ)、乘积量化(PQ)等压缩技术,显著降低存储与内存开销,提升高维大规模向量场景的资源效率。
  5. 分布式并行执行:依托于分布式架构,Doris 向量索引天然支持数据分片与索引分布式存储;查询可在各 BE 节点并行执行;Top-K 结果在上层进行合并与裁剪。随着节点数量增加,系统能够在数据规模与吞吐能力上实现近线性扩展。

2. Benchmark & Analysis

Apache Doris 的目标并非追求单一指标的极限表现,而是在真实生产负载下,实现性能的均衡性、系统稳定性与架构可扩展性。本次测试将围绕这一目标展开,所用工具为 ZillizTech 开源的向量搜索 BenchMark:https://github.com/zilliztech/VectorDBBench

  • 云服务商:阿里云
  • CPU:Intel Xeon Platinum 8369B @ 2.70GHz (16 核)
  • 内存:64GB

2.1 导入与构建性能

测试结果表明,在 Performance768D1M 数据集上,Apache Doris 在保证同等索引质量的前提下,导入性能显著优于对比系统。尤为重要的是,其导入速度的提升并未以牺牲图结构质量为代价。Doris 在 QPS 达到 895 的同时,仍保持了 97% 以上的召回率,在性能三角的三个维度上取得了出色的平衡

2.1 导入与构建性能.PNG

2.2 查询性能

即便单独考量查询性能,Apache Doris 同样处于业界第一梯队。

在 Performance768D10M 数据规模上,当召回率要求高于 95% 时,Apache Doris 的 QPS 表现优于 OpenSearch 与 Qdrant此结果为默认配置下的开箱性能,未针对 Segment 文件数量等进行专项调优

2.2 查询性能.png

这里比较的是开箱性能测试,即不做 segment 文件数量的优化时的性能对比。

Milvus 的 flat 版本以及 Cloud 版本会有更好的性能表现,但是其出品的 VectorDBBench 只提供了 SQ8 量化后的成绩。

3. 核心设计与性能优化

Apache Doris 采用 FE(协调节点)与 BE(计算节点)构成的分布式架构。BE 作为核心执行单元,承担查询计划执行与数据导入任务,负责几乎所有高负载计算与大规模数据吞吐,是系统高性能的基石。尤其在向量场景下,数据写入、索引构建与向量距离计算都属于典型的 CPU 与内存密集型工作。为充分发挥其性能、保障系统稳定运行,我们对 ANN 索引的写入、构建与查询路径进行了系统优化

3. 核心设计与性能优化.png

3.1 写入与构建路径优化

优化主要分为两类:功能优化性能优化

  • 在功能层面,依托 Doris 成熟的分布式集群管理与存储管理能力,引入 LightSchemaChange 实现轻量级的索引管理机制,这是目前专用向量数据库普遍不具备的能力。
  • 在性能层面,重点聚焦于索引构建流程的优化,以显著提升索引构建速度和整体吞吐能力。

3.1.1 异步索引构建机制

Apache Doris 针对 ANN 索引构建开销大的问题,提供了异步构建机制。用户可在数据导入后,选择业务低峰期触发索引构建;在查询高峰时,仅需将已建好的索引加载至内存即可快速检索,从而将密集的 CPU 消耗转移至成本更低的时段。

在 FE 侧,CREATE INDEXBUILD INDEX 通过 SchemaChangeHandler 编排:

  1. 为每个分区创建影子索引与影子 Tablet(IndexState.SHADOW),并建立 origin→shadow 的 Tablet 映射与影子副本(副本初始态为 ALTER)。
  2. 生成新的 schema version/hash,保障新旧版本隔离。
  3. 通过 FE→BE 的 AgentTask(Thrift)分发构建任务到各 BE,BE 在 Tablet 层面完成索引数据构建。
  4. 构建成功后,FE 原子性地将影子索引切换为正式索引,更新元数据并清理旧工件。

该流程在保证线上业务可读写的同时,实现了索引构建的在线隔离与数据一致性。

3.1.2 导入性能优化

为在保障索引质量的前提下提升写入吞吐与稳定性,Doris 采用了 多层级分片、双层并行、SIMD 向量化计算 的组合方式进行优化。

A. 多层级分片

Apache Doris 将逻辑表在内核层拆分为多个 Tablet。每次数据导入会生成一个 Rowset,每个 Rowset 又包含若干 Segment,而 ANN 索引正是在 Segment 粒度上构建与使用的。这一设计将“全表数据量”与“索引超参数”解耦,用户只需根据单批次导入的数据规模来设定参数,无需因数据总量增加而反复重建索引

3.1.2 导入性能优化.png

以单 BE 单分桶的典型场景为例,我们从实际经验中总结出如下参数可供参考:

3.1.2 导入性能优化-1.png

得益于 Apache Doris 的分片架构下,索引参数可稳定在合理的规模区间,不受全表数据总量增长的影响。换言之,索引超参数的设置只需基于单个 Tablet 单次导入的数据行数。即便集群规模扩大,也仅需根据机器与分桶数量相应调整批次大小(batch size)即可。

以 HNSW 索引为例,在单 BE 集群中,针对每批导入 25 万、50 万、100 万行的典型规模,分别选择 max_degree≈100/120/150ef_construction≈200/240/300hnsw_ef_search≈50~200,即可在延迟可控的同时平衡召回与构建成本。

经验上,召回率随 hnsw_ef_search 提高而改善,但查询延迟也会线性增加。max_degreeef_construction 过小会导致图结构稀疏、查询不稳定;过大则会显著增加构建时间与内存占用。因此,建议结合业务对召回和延迟的要求,通过离线压测确定最佳参数组合

B. 双层并行构建

集群层由多台 BE 并行处理导入批次;单机内再对同一批数据进行多线程距离计算和图结构更新。配合“内存攒批”(在内存中适度合并小批次),可避免过细分批导致的图结构稀疏与召回下滑,在固定超参数下获得更稳定的索引质量与构建速度。

以 768 维、1,000 万条向量为例:分 10 批构建的召回率约可达 99%,若切成 100 批则可能降至约 95%。适度的内存攒批既不显著抬高内存峰值,又能提升图连通性和近邻覆盖,从而减少查询阶段的回表与重复计算

C. SIMD 加速

3.1.2 导入性能优化-2.png

向量距离计算是典型的 CPU 密集型计算。Doris 在 BE 侧采用 C++ 实现距离计算,引入 SIMD(单指令多数据)并行计算。可以更少的指令、更少的访存,更快完成把同样的距离,从而显著提升向量索引构建和重排阶段的吞吐能力。具体来讲:

  • 并行计算多个维度:利用 SSE / AVX / AVX-512 等指令集,同时加载和计算 8~16 个浮点数,而非逐维循环。
  • 减少内存访问:在计算前对向量数据进行批处理和转置,使数据在内存中连续排列,优化 CPU Cache 访问模式。
  • 合并计算步骤:使用 FMA(乘加融合)指令,把“乘法 + 加法”合并为一步,并通过水平求和快速聚合向量数据。
  • 高效处理边界情况:对维度不对齐的尾部数据,使用掩码指令统一处理,避免额外分支和判断。

3.1.3 向量压缩技术

以 HNSW 为代表的高性能索引数据结构通常将向量与图结构常驻内存。在 RAG 场景中,文本/图片/音频等模态向量维度约为 1,000,若每维使用 FLOAT32 存储,一百万行占用 4 GB,千万行则约 40 GB。考虑到索引结构的额外占用(约 1.3 倍),一千万行整体接近 52 GB。以 16C64GB 机器为例,单机索引上限约为千万级,需预留空间以避免 OOM,并保障查询和构建的并行开销。

为了显著降低内存占用、扩展单机承载能力,向量压缩技术成为关键。Apache Doris 在此提供了两种主流的实现方案:标量量化与乘积量化

A. 标量量化(Scalar Quantization,SQ)

标量量化通过用低精度类型替换高精度类型来压缩存储空间,Doris 支持 INT8INT4 的标量量化,并在导入和构建阶段完成编码。

如若将 FLOAT32(4 字节)替换为 INT8(1 字节)可节省约 75% 存储,进一步压缩为 INT4 则节省约 87.5%。如果压缩后数据的分布形态保持一致,召回率在可控延迟内接近未压缩效果。

3.1.3 向量压缩技术.png

上图展示了在 128 维和 268 维向量上的测试结果。相比 FLAT(不编码,用完整 Float32 表示每个浮点数),SQ8 可实现接近 2.5 倍的压缩,而 SQ4 可实现接近 3.3 倍的压缩

值得说明的是,引入 SQ 不可避免的会带来额外的压缩计算开销(索引构建阶段),且标量量化更适用于各维度近似均匀分布的数据。如遇分布呈高斯或更复杂形态时,标量量化误差增大,则可采用乘积量化方式。

B. 乘积量化(Product Quantization, PQ)

RAG 等场景中,由 Transformer 编码器生成的向量,存在明显的语义结构、分布不均匀。乘积量化通过子空间划分 + 子空间学习型量化,能够更好地适配

PQ 将高维向量分割为多个子向量,并为每个子空间独立训练一个码本(例如通过 k-means 聚类学习质心)。这使得数据密集区域能用更精细的码本保持细节,从而在整体上用更短的码长维持原始的距离关系。查询时通过查表与累加来估算距离,大幅减少了计算与内存访问开销。

我们在 128 维与 268 维上对比 SQ 与 PQ,参数统一设定为 pq_m = dim/2pq_nbits = 8

3.1.3 向量压缩技术-1.png

从空间占用看,PQ(m=68/128, nbits=8)的内存占比与 SQ4 大致相当,可实现约 3× 压缩

3.1.3 向量压缩技术-2.png

除构建更快外,PQ 还可依赖查表加速解码,体现在更优的查询速度上。

3.1.3 向量压缩技术-3.png

关于 PQ 的超参数,实际使用时建议结合数据分布进行针对性适配与调优。根据经验,将 pq_m 设为原始维度的一半,pq_nbits 设为 8,在多数场景下即可取得良好的效果,可作为初始调优的参考起点。

综合来看,对于用户来说, SQ 和 PQ 该如何选择呢

  • 从使用上来说,SQ 的优点是使用方式简单,只需要指定数据类型即可,而 PQ 的使用门槛更高,需要对其原理有较为深刻的理解才能在生产环境中发挥其优势。
  • 从性能及开销上来说,SQ 在解码阶段存在额外计算开销,且随维度增加开销更高;PQ 则能在压缩的同时保持接近原始向量的查询性能。
  • 从场景上来说,SQ 更适用于各维度近似均匀分布的数据。如遇分布呈高斯或更复杂形态时,标量量化误差增大,则可采用乘积量化方式。

3.2 查询执行路径优化

搜索场景对延迟极为敏感。在千万级数据量与高并发查询的场景下,通常需要将 P99 延迟控制在 200 ms 以内。这对 Doris 的优化器、执行引擎以及索引实现都提出了更高要求。Apache Doris 为此做了大量优化,这一章节对其中涉及到的部分能力做介绍。

3.2.1 虚拟列机制

Apache Doris 的向量索引采用外挂方式。外挂索引便于管理与异步构建,但也带来性能挑战:如何避免重复计算与多余 IO

ANN 索引在返回行号时,会同步计算出向量距离。执行引擎在 Scan 算子阶段可直接利用该结果进行筛选和排序,无需在读取数据后重新计算。这一过程通过 “虚拟列” 机制自动实现,最终以 Ann Index Only Scan 的形式运行,完全消除了因距离计算而产生的数据读取 I/O

未应用 Index Only Scan:

3.2.1 虚拟列机制.png

应用 Index Only Scan 后:

3.2.1 虚拟列机制-1.png

例如 SELECT l2_distance_approximate(embedding, [...]) AS dist FROM tbl ORDER BY dist LIMIT 100;,执行过程将不再触发数据文件 IO。

该优化不仅适用于 TopK 检索,也支持 Range Search、复合检索(Range + TopK)以及与倒排索引结合的混合检索场景,实现了全路径的 Index Only Search

虚拟列机制并不局限于向量距离计算。对于正则抽取、复杂标量函数等 CPU 密集型表达式,若在同一查询中被多次引用,该机制也能复用中间结果,避免重复计算。以 ClickBench 数据集为例,以下查询统计从 Google 获得最多点击的 20 个网站

set experimental_enable_virtual_slot_for_cse=true;

SELECT counterid,
       COUNT(*)               AS hit_count,
       COUNT(DISTINCT userid) AS unique_users
FROM   hits
WHERE  ( UPPER(regexp_extract(referer, '^https?://([^/]+)', 1)) = 'GOOGLE.COM'
         OR UPPER(regexp_extract(referer, '^https?://([^/]+)', 1)) = 'GOOGLE.RU'
         OR UPPER(regexp_extract(referer, '^https?://([^/]+)', 1)) LIKE '%GOOGLE%' )
       AND ( LENGTH(regexp_extract(referer, '^https?://([^/]+)', 1)) > 3
              OR regexp_extract(referer, '^https?://([^/]+)', 1) != ''
              OR regexp_extract(referer, '^https?://([^/]+)', 1) IS NOT NULL )
       AND eventdate = '2013-07-15'
GROUP  BY counterid
HAVING hit_count > 100
ORDER  BY hit_count DESC
LIMIT  20;

核心表达式 regexp_extract(referer, '^https?://([^/]+)', 1) 为 CPU 密集型且被多处复用。启用虚拟列优化(set experimental_enable_virtual_slot_for_cse=true;)后,端到端性能提升约 3 倍

3.2.2 前过滤与谓词下推

在 ANN TopN 检索中,过滤谓词的应用时机是关键的设计权衡:

  • 前过滤:在 TopN 之前应用谓词,能阻止无效行进入候选;但需在候选集维护过程中实时剔除不符合条件的行。
  • 后过滤:先按相似度取出 TopN,再执行过滤,可能导致最终结果不足 N 条。虽然可通过扩大 N 来补偿,但会额外增加扫描与计算开销。

Apache Doris 在 Scan 算子内通过 row bitmap 实现自然的前过滤语义。每个谓词执行后即时更新 row bitmap。当 TopN 下推到 Scan 时,向索引传递一个基于 row bitmap 的 IDSelector,仅保留满足条件的行作为候选,从源头上避免无效候选进入 TopN。

为进一步提升效率,Doris 还会在扫描前借助分区、分桶、ZoneMap 等轻量元数据进行快速预过滤,并结合倒排索引进行精确的行号定位,多层次缩小候选集,能够显著提升查询性能与资源效率。

3.2.3 全局执行优化

在传统执行路径中,Doris 会对每条 SQL 执行完整优化流程(语法解析、语义分析、RBO、CBO)。这在通用 OLAP 场景必不可少,但在搜索等简单且高度重复的查询模式中会产生明显的额外开销。为此,Doris 进行了全局执行优化,充分发挥索引、过滤等性能。

A. Prepare Statement

Doris 4.0 扩展了 Prepare Statement,使其不仅支持点查,也适用于包含向量检索在内的所有 SQL 类型。Prepare Statement 的原理是将 SQL 编译与执行分离,模板化检索复用计划缓存,Execute 阶段跳过优化器。查询计划按“标准化 SQL + schema 版本”构建指纹进行缓存,执行阶段校验 schema version,变化则自动失效并重建。对频繁且结构相同仅参数不同的检索,Prepare 能显著降低 FE 侧 CPU 占用与排队等待。

B. Scan 并行度优化

为提升 ANN TopN 检索性能,Doris 重构了 Scan 并行策略。原策略基于行数划分任务,在高维向量场景下,单个 Segment 的实际行数常远低于划分阈值,导致多个 Segment 被分配至同一任务中串行扫描,制约性能。

为此,Doris 改为严格按 Segment 创建 Scan Task,显著提升了索引检索阶段的并行度。由于 ANN TopN 搜索本身过滤率极高(仅返回 TopN 行),后续回表阶段即使串行执行,对整体吞吐与延迟的影响也微乎其微。

以 SIFT 1M 数据集为例,开启 optimize_index_scan_parallelism=true 后,TopN 查询耗时从 230ms 降至 50ms,效果显著

此外,4.0 引入动态并行度调整:每轮调度前根据 Scan 线程池压力动态决定可提交的任务数;压力大则减并行、资源空闲则增并行,以在串行与高并发场景间兼顾资源利用率与调度开销。

C. TopN 全局延迟物化

典型的 ANN TopN 查询可分为两个关键阶段:局部检索与全局归并。在局部检索阶段,Scan 算子通过索引获取每个数据分片(Segment)中的局部 TopN 近似距离;随后在全局归并阶段,由专门的排序节点对所有分片的局部结果进行合并,筛选出最终的全局 TopN。

传统执行流程存在一个显著效率问题:若查询需要返回多列或包含大字段(如长文本),在第一阶段就会读取这些列的全部数据。这不仅会引发大量磁盘 I/O,而且绝大多数被读取的行会在第二阶段的排序竞争中被淘汰,造成计算与 I/O 资源的浪费

为此,Doris 引入了 “全局 TopN 延迟物化” 优化。该机制将非排序所需列的读取推迟到最终结果确定之后,大幅减少了不必要的 I/O

优化执行流程示例:

SELECT id, l2_distance_approximate(embedding, [...]) AS dist FROM tbl ORDER BY dist LIMIT 100; 为例:

  1. 局部轻量扫描:每个 Segment 利用 Ann Index Only Scan 结合虚拟列技术,仅计算出局部 Top 100 的距离值(dist)及其对应的行标识(rowid),不读取其他列。
  2. 全局排序筛选:系统汇总所有 M 个 Segment 的中间结果(共 100 × M 条候选),对其进行全局排序,从而确定最终的 100 个目标 rowid
  3. 按需延迟物化:最终的 Materialize 算子根据上一步得到的 rowid,精准地到对应的存储位置读取所需列(例如 id)的数据。

通过将完整数据的“物化”步骤推迟到最后,该优化确保了查询前期仅处理轻量的距离与行标识信息,彻底避免了在排序前读取非必要列所带来的 I/O 开销,从而显著提升了整体查询效率。

4. 实战:使用 Apache Doris 搭建企业知识库

企业级知识库是 RAG 的典型落地场景。因此,我们基于 LangChain + Apache Doris 搭建了一个以 Doris 官网文档为语料的最小可用知识库,用于验证 Doris 向量检索的端到端能力。完整示例代码见 GitHub

(1)环境准备

  • LLM:用于对话与答案生成,这里使用 DeepSeek。先在官网注册并创建 API Key,妥善保存,后续用于调用 DeepSeek API。
  • 嵌入模型:用于生成检索向量,这里使用 Ollama + bge-m3:latest。bge-m3 是开源的通用检索向量模型,兼顾中英文检索效果,默认输出 1024 维向量,适合知识库检索场景。

(2)建库与建表(方式一:SQL)

CREATE DATABASE doris_rag_test_db;

USE doris_rag_test_db;

CREATE TABLE doris_rag_demo (
  id int NULL,
  content text NULL,
  embedding array<float> NOT NULL,
  INDEX idx_embedding (embedding) USING ANN PROPERTIES("dim" = "1024", "ef_construction" = "40", "index_type" = "hnsw", "max_degree" = "32", "metric_type" = "inner_product")
) ENGINE=OLAP
DUPLICATE KEY(id)
DISTRIBUTED BY HASH(id) BUCKETS 1
PROPERTIES (
"replication_allocation" = "tag.location.default: 1",
"storage_medium" = "hdd",
"storage_format" = "V2",
"inverted_index_storage_format" = "V3",
"light_schema_change" = "true"
);
说明:若计划使用 SDK 一键建表与导入(见 ⑤),本节可省略。

(3)演示语料

示例使用 Apache Doris 官网文档作为语料来源:https://github.com/apache/doris-website

(4)离线文档处理

  • 切块(chunking):采用重叠分割,将长文档切分为段落片段。
from langchain_text_splitters import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=400, chunk_overlap=100, length_function=len
)
chunks = text_splitter.split_text(text)
  • 生成向量(embedding):对每个片段生成嵌入向量。
from typing import List, Dict
from langchain_community.embeddings import OllamaEmbeddings

embeddings = OllamaEmbeddings(model='bge-m3:latest', base_url='http://localhost:11434')

docs: List[Dict] = []
cur_id = 1
for chunk in chunks:
    docs.append({"id": cur_id, "content": chunk})
    cur_id += 1

contents = [d["content"] for d in docs]
vectors = embeddings.embed_documents(contents)

(5)导入 Doris(方式二:SDK 一键建表与导入)

import pandas as pd
df = pd.DataFrame(
        [
            {
                "id": d["id"],
                "content": d["content"],
                "embedding": vec,
            }
            for d, vec in zip(docs, vectors)
        ])

from doris_vector_search import DorisVectorClient, AuthOptions, IndexOptions

auth = AuthOptions(
    host='localhost',
    query_port=9030,
    http_port=8030,
    user='root',
    password='',
)

client = DorisVectorClient('doris_rag_test_db', auth_options=auth)

index_options = IndexOptions(index_type="hnsw", metric_type="inner_product")
table = client.create_table(
            'doris_rag_demo',
            df,
            index_options=index_options,
        )

说明:若已通过 ② 使用 SQL 创建好表并定义索引,可仅使用 SDK 的导入接口(如 insert/load 等,视 SDK 能力而定)将数据写入既有表。

(6)在线查询过程

向量检索

query = 'Doris 支持哪些存储模型?'
query_vec = embeddings.embed_query(query)
df = (
    table.search(query_vec)
    .limit(5)
    .select(["id", "content"])
    .to_pandas()
)

答案生成

ctx = "\n".join(f"{r['content']}" for _, r in df.iterrows())
prompt =  "以下是检索到的 Doris 文档片段:\n\n{}\n\n请根据上述内容回答:{}".format(ctx, query)

from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
            model='deepseek-v3-1-terminus',
            api_key='xxxx',
            base_url='https://xxx',
            temperature=float(1.0))
resp = llm.invoke(prompt)

返回的内容是:

'根据提供的文档内容,Apache Doris 支持以下三种存储模型:\n\n1.  明细模型(Duplicate Key Model):适用于存储事实表的明细数据。\n2.  主键模型(Unique Key Model):保证主键的唯一性,相同主键的数据会被覆盖,从而实现行级别的数据更新。\n3.  聚合模型(Aggregate Key Model):相同键(Key)的数值列(Value)会被自动合并,通过提前聚合来大幅提升查询性能。\n\n此外,文档在“灵活建模”部分还提到,Apache Doris 支持如宽表模型、预聚合模型、星型/雪花模型等建模方式,这些可以看作是建立在上述三种核心存储模型之上的数据组织方法。'

5. 总结

本文从 AI 时代的数据形态演进出发,系统性地介绍了 Apache Doris 在 4.x 版本中引入的向量检索能力,并对其底层实现进行了深入剖析。从 ANN 索引的能力边界,到 FE / BE 架构下的写入、构建与查询路径,再到 SIMD、压缩编码与执行引擎层面的工程优化,Doris 的向量搜索并非简单接入一个索引库,而是围绕 性能三角(召回率 / 查询延迟 / 构建吞吐) 精心设计的系统级方案。未来,我们还会进一步强化,使其成为 AI 时代数据系统智能检索的基石。

💥 事故现场
LZ 所在的量化小厂,早期基础设施全是 Python (Asyncio) 一把梭。 跑美股( US )的时候相安无事,毕竟 Tick 流是均匀的。 上周策略组说要加 A 股 (CN) 和 外汇 (FX) 做宏观对冲,我就按老套路接了数据源。

结果上线第一天 9:30 就炸了。 监控报警 CPU 100%,接着就是 TCP Recv-Q 堆积,最后直接断连。 策略端收到行情的时候,黄花菜都凉了(延迟 > 500ms )。

🔍 排查过程 (Post-Mortem)
被 Leader 骂完后,挂了 py-spy 看火焰图,发现两个大坑:

Snapshot 脉冲:A 股跟美股不一样,它是 3 秒一次的全市场快照。几千只股票的数据在同一毫秒涌进来,瞬间流量是平时的几十倍。

GIL + GC 混合双打:

json.loads 是 CPU 密集型,把 GIL 锁死了,网络线程根本抢不到 CPU 读数据。

短时间生成大量 dict 对象,触发 Python 频繁 GC ,Stop-the-world 。

🛠️ 架构重构 (Python -> Go)
为了保住饭碗,连夜决定把 Feed Handler 层剥离出来用 Go 重写。 目标很明确:扛住 A 股脉冲,把数据洗干净,再喂给 Python 策略。

架构逻辑:WebSocket (Unified API) -> Go Channel (Buffer) -> Worker Pool (Sonic Decode) -> Shm/ZMQ

为什么用 Go ?

Goroutine:几 KB 开销,随开随用。

Channel:天然的队列,做 Buffer 抗脉冲神器。

Sonic:字节开源的 JSON 库,带 SIMD 加速,比标准库快 2-3 倍(这个是关键)。

💻 Show me the code
为了解决 协议异构( A 股 CTP 、美股 FIX 、外汇 MT4 ),我接了个聚合源( TickDB ),把全市场数据洗成了统一的 JSON 。这样 Go 这边只用维护一个 Struct 。

以下是脱敏后的核心代码,复制可跑(需 go get 依赖)。
package main

import (
"fmt"
"log"
"runtime"
"time"

"github.com/bytedance/sonic" // 字节的库,解析速度吊打 encoding/json
"github.com/gorilla/websocket"
)

// 防爬虫/防风控,URL 拆一下
const (
Host = "api.tickdb.ai"
Path = "/v1/realtime"
// Key 是薅的试用版,大家拿去压测没问题
Key = "?api_key=YOUR_V2EX_KEY"
)

// 内存对齐优化:把同类型字段放一起
type MarketTick struct {
Cmd string `json:"cmd"`
Data struct {
Symbol string `json:"symbol"`
LastPrice string `json:"last_price"` // 价格统一 string ,下游处理精度
Volume string `json:"volume_24h"`
Timestamp int64 `json:"timestamp"` // 8 byte
Market string `json:"market"` // CN/US/HK/FX
} `json:"data"`
}

func main() {
// 1. 跑满多核,别浪费 AWS 的 CPU
runtime.GOMAXPROCS(runtime.NumCPU())

url := "wss://" + Host + Path + Key
conn, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
log.Fatal("Dial err:", err)
}
defer conn.Close()

// 2. 订阅指令
// 重点测试:A 股(脉冲) + 贵金属(高频) + 美股/港股
subMsg := `{
"cmd": "subscribe",
"data": {
"channel": "ticker",
"symbols": [
"600519.SH", "000001.SZ", // A 股:茅台、平安 (9:30 压力源)
"XAUUSD", "USDJPY", // 外汇:黄金、日元 (高频源)
"NVDA.US", "AAPL.US", // 美股:英伟达
"00700.HK", "09988.HK", // 港股:腾讯
"BTCUSDT" // Crypto:拿来跑 7x24h 稳定性的
]
}
}`
if err := conn.WriteMessage(websocket.TextMessage, []byte(subMsg)); err != nil {
log.Fatal("Sub err:", err)
}
fmt.Println(">>> Go Engine Started...")

// 3. Ring Buffer
// 关键点:8192 的缓冲,专门为了吃下 A 股的瞬间脉冲
dataChan := make(chan []byte, 8192)

// 4. Worker Pool
// 经验值:CPU 核数 * 2
workerNum := runtime.NumCPU() * 2
for i := 0; i < workerNum; i++ {
go worker(i, dataChan)
}

// 5. Producer Loop (IO Bound)
// 只管读,读到就扔 Channel ,绝对不阻塞
for {
_, msg, err := conn.ReadMessage()
if err != nil {
log.Println("Read err:", err)
break
}
dataChan <- msg
}
}

// Consumer (CPU Bound)
func worker(id int, ch <-chan []byte) {
var tick MarketTick
for msg := range ch {
// 用 Sonic 解析,性能起飞
if err := sonic.Unmarshal(msg, &tick); err != nil {
continue
}

if tick.Cmd == "ticker" {
// 简单的监控:全链路延迟
latency := time.Now().UnixMilli() - tick.Data.Timestamp

// 抽样打印
if id == 0 {
fmt.Printf("[%s] %-8s | Price: %s | Lat: %d ms\n",
tick.Data.Market, tick.Data.Symbol, tick.Data.LastPrice, latency)
}
}
}
}

📊 Benchmark (实测数据)
环境:AWS c5.xlarge (4C 8G),订阅 500 个活跃 Symbol 。 复现了 9:30 A 股开盘 + 非农数据公布 的混合场景。
指标,Python (Asyncio),Go (Sonic + Channel),评价
P99 Latency,480ms+,< 4ms,简直是降维打击
Max Jitter,1.2s (GC Stop),15ms,终于不丢包了
CPU Usage,98% (单核打满),18% (多核均衡),机器都不怎么转
Mem,800MB,60MB,省下来的内存可以多跑个回测

📝 几点心得
术业有专攻:Python 做策略逻辑开发是无敌的,但这种 I/O + CPU 混合密集型的接入层,还是交给 Go/Rust 吧,别头铁。

别造轮子:之前想自己写 CTP 和 FIX 的解析器,写了一周只想跑路。后来切到 TickDB 这种 Unified API ,把脏活外包出去,瞬间清爽了。

Sonic 是神器:如果你的 Go 程序瓶颈在 JSON ,无脑换 bytedance/sonic ,立竿见影。

代码大家随便拿去改,希望能帮到同样被 Python 延迟折磨的兄弟。 (Key 是试用版的,别拿去跑大资金实盘哈,被限流了别找我)

数据建模的深层困惑,往往不在于工具本身的用法,而在于对其职责边界的模糊认知——dataclasses与Pydantic的选择之争,本质是对“数据载体”与“数据治理”核心诉求的错位判断。在长期的开发实践中,我曾多次陷入“一刀切”的工具使用误区:早期为了追求代码简洁,用dataclasses处理所有数据场景,结果在外部接口接入时因缺乏数据校验,导致非法数据流入核心业务,引发连锁性的逻辑异常;后来又盲目迷信Pydantic的强约束能力,将其用于内部模块高频数据传递,却发现额外的校验逻辑让系统响应延迟提升了近三成,尤其在数据批量处理场景中,性能损耗更为明显。这些踩坑经历让我逐渐意识到,两者并非替代关系,而是基于数据流转场景的互补存在,其边界划分的核心在于“是否需要主动介入数据生命周期的治理行为”。真正的实践智慧,是在数据创建、流转、校验、序列化的全链路中,精准匹配工具的核心能力:dataclasses专注于数据结构的轻量描述,不附加任何多余逻辑,确保内部数据传递的高效;Pydantic聚焦于数据行为的严格治理,通过类型注解与约束规则,构建可靠的外部交互边界。比如在内部模块间的配置传递场景中,dataclasses仅需几行代码就能完成数据结构定义,无需关注校验与转换,让开发者聚焦于业务逻辑;而在接收第三方接口数据时,Pydantic能自动完成类型校验、格式清洗与默认值填充,将不符合规则的数据拦截在业务逻辑之外,避免潜在风险。这种分工明确的使用方式,既保留了架构的简洁性,又确保了数据在关键节点的可靠性,让数据建模真正服务于业务效率与系统稳定。

dataclasses的核心价值,在于以最低成本实现数据结构的规范化描述,其设计哲学是“无侵入式的结构定义”,不附加额外的数据处理逻辑,仅专注于数据的存储与基础访问。在长期的学习与实践中,我深刻体会到它作为Python标准库一员的独特优势:无需引入任何第三方依赖,就能自动生成初始化、比较、字符串表示等常用方法,极大减少了冗余代码的编写。这种轻量性使其在内部系统的数据载体场景中表现尤为突出,尤其是在模块间无复杂交互、数据格式相对固定的场景下,能以极简的方式完成数据封装。例如在一个日志处理系统中,日志的核心字段(时间戳、级别、内容、模块名)相对固定,且仅在系统内部流转,使用dataclasses定义日志模型,既能保证字段的清晰性,又能避免不必要的性能开销。与Pydantic相比,dataclasses不具备主动的数据校验能力,也不支持复杂的类型转换与序列化,但这种“不足”恰恰是其优势所在——它不会对数据施加任何额外约束,完全尊重数据的原生状态,让数据在内部流转时保持最高效率。我曾在一个数据批量处理任务中做过对比:用dataclasses定义的数据模型,每万条数据的处理时间约为0.3秒,而用Pydantic定义的相同结构模型,处理时间则达到1.2秒,性能差距高达4倍。这一结果充分说明,在对性能敏感、无严格约束需求的内部场景中,dataclasses的轻量性是无可替代的。但同时也必须清晰认识到其职责边界的上限:一旦数据需要跨场景流转,尤其是面对外部输入时,仅靠dataclasses无法保证数据的完整性与合法性。比如曾尝试用dataclasses接收用户提交的表单数据,结果因未做类型校验,导致字符串类型的数字被直接传入计算逻辑,引发类型错误;又因缺乏必填字段校验,导致关键数据缺失,影响业务流程正常推进。这些经历让我明确,dataclasses的核心阵地是内部数据封装与传递,一旦超出这个边界,就需要借助其他工具的治理能力。

Pydantic的核心竞争力,体现在对数据全生命周期的主动治理能力,其设计核心是“以类型注解为基础的契约式编程”,通过明确的数据约束构建可靠的交互边界。实践中,我无数次感受到它在外部数据处理场景中的强大威力:无论是API接口的请求参数校验、配置文件的解析,还是数据持久化前的格式转换,Pydantic都能以 declarative 的方式,将复杂的数据治理逻辑封装在模型定义中,让开发者无需编写大量校验代码。例如在一个设备监控系统中,需要接收来自不同设备的上报数据,这些数据格式不一、字段缺失情况频发,使用Pydantic定义数据模型后,仅需通过类型注解和字段约束,就能自动完成数据类型转换(如将字符串格式的数字转为整数)、必填字段校验(如设备ID不能为空)、范围限制(如温度值不能超出合理区间),同时还能填充默认值(如将未上报的信号强度设为0)。这种自动化的数据治理能力,不仅极大降低了开发成本,还显著提升了系统的稳定性,避免了因数据异常导致的业务故障。Pydantic的优势远不止于此,它还支持复杂类型嵌套(如字典、列表的多层嵌套结构)、多格式序列化(如JSON、字典、字符串的相互转换)、自定义校验逻辑(如根据业务规则校验数据合法性)等高级功能,这些能力使其能够应对各类复杂的外部数据场景。但这种强大的治理能力并非无代价,其底层的校验逻辑与封装机制会带来一定的性能开销,尤其是在高频数据处理场景中,这种开销会被放大。我曾在一个实时数据接收服务中,因使用Pydantic处理每秒数千条的数据流,导致服务响应延迟大幅增加,后来通过将数据模型拆分为“Pydantic适配层”与“dataclasses核心层”,仅在数据接入时使用Pydantic进行校验转换,内部流转则使用dataclasses,才解决了性能问题。此外,过度依赖Pydantic的高级功能还可能导致数据模型与业务逻辑的耦合,比如将业务规则直接写入Pydantic的自定义校验方法中,会让模型变得臃肿,难以维护。这些实践经验让我明白,Pydantic的核心价值在于构建系统的“数据边界”,而非替代所有数据载体场景,只有在需要严格约束与治理的场景中使用,才能发挥其最大价值。

划分两者职责边界的关键,在于建立“场景-能力”的匹配框架,而非机械地按功能模块分割。经过大量实践总结,我提炼出三个核心判断维度,帮助在不同场景中做出精准选择。第一个维度是数据流转范围:如果数据仅在内部模块间流转,且模块由同一团队维护,数据格式相对稳定,优先选择dataclasses,因为此时效率与简洁性更为重要,无需额外的校验逻辑;如果数据需要跨系统、跨团队交互,或从外部接口接收、向第三方输出,必须使用Pydantic,通过明确的约束规则构建交互契约,避免因数据格式差异引发的沟通成本与系统故障。第二个维度是约束强度需求:如果仅需对数据结构进行规范化描述,无严格的类型与值约束要求,dataclasses足以满足需求;如果需要强制数据类型、校验字段必填性、限制值的范围、进行数据清洗转换等,必须依赖Pydantic的治理能力。第三个维度是性能敏感度:如果是高频数据处理、低延迟要求的场景(如实时计算、批量数据处理),应优先使用dataclasses,避免Pydantic的校验逻辑带来性能损耗;如果是低频交互、对可靠性要求高于性能的场景(如配置解析、接口请求处理),则可以放心使用Pydantic。更高级的实践是两者的协同使用,构建“适配层+核心层”的架构模式:以dataclasses作为核心业务数据模型,确保内部流转的轻量高效;以Pydantic作为数据接入与输出的适配层,处理外部数据的校验、转换与序列化。例如在一个用户行为分析系统中,外部接口接收的用户行为数据(如点击、浏览、下单)首先通过Pydantic模型进行校验,确保字段完整、类型正确,然后转换为dataclasses模型进入核心处理流程(如数据统计、特征提取),核心流程中数据高频流转,dataclasses的轻量性保证了处理效率;当需要将分析结果输出到报表系统时,再通过Pydantic模型进行序列化,确保输出格式符合第三方要求。这种协同模式既兼顾了性能与可靠性,又实现了关注点分离,让核心业务逻辑与数据治理逻辑相互独立,便于维护与扩展。在实践中,我还会根据业务场景的变化动态调整工具选择,比如当某个内部模块需要对外提供接口时,会为其新增Pydantic适配层,而不改变核心的dataclasses模型,这种弹性调整能力,让系统能够快速响应业务需求的变化。

实践中常见的误区,是将两者的职责边界绝对化,要么过度依赖Pydantic导致所有数据模型都带有强约束,要么完全摒弃Pydantic而仅用dataclasses处理所有场景。这种非此即彼的选择,往往源于对工具本质的理解不足,最终会给系统带来潜在风险或性能问题。我曾接触过一个项目,开发者为了追求“统一规范”,所有数据模型都使用Pydantic定义,包括内部模块间传递的简单数据对象。在系统上线初期,业务量较小时未出现明显问题,但随着业务增长,数据处理量大幅提升,系统响应速度越来越慢,排查后发现,大量内部数据的无意义校验占用了近40%的CPU资源。后来通过将内部数据模型替换为dataclasses,仅保留外部交互场景的Pydantic模型,系统性能立刻提升了35%。另一个极端案例是,某个项目完全使用dataclasses处理所有数据场景,包括接收外部API数据,结果因缺乏数据校验,导致恶意提交的非法数据流入数据库,不仅污染了数据,还引发了业务逻辑异常,排查与清理数据花费了大量时间。这些案例充分说明,工具的选择必须基于场景,而非个人偏好。正确的做法是根据具体场景的核心诉求灵活取舍,甚至在同一业务流程中让两者协同发挥作用。此外,还需要关注工具的版本演进与生态适配:dataclasses作为Python标准库的一部分,兼容性与稳定性更强,无需担心依赖冲突,适合长期维护的核心模块;Pydantic则在功能迭代上更活跃,新的治理能力(如更灵活的校验规则、更丰富的序列化格式)不断涌现,适合需要应对复杂数据场景的业务模块。在实践中,我会定期跟踪两者的版本更新,将有用的新功能融入到现有架构中,比如Pydantic新增的“部分校验”功能,就非常适合处理增量数据更新场景,而dataclasses新增的字段默认值功能,则进一步简化了内部数据模型的定义。这种基于场景与生态的动态选择,才能让数据建模工具真正服务于业务需求,而非成为技术负债。

dataclasses与Pydantic的职责边界划分,本质是对“简洁性”与“可靠性”的平衡艺术,其核心逻辑在于让工具回归其设计初衷,在合适的场景发挥其核心优势。从最初的混淆使用到后来的精准分工,这一过程不仅是技术工具的熟练运用,更是对数据建模本质的深刻理解——数据模型不仅是数据的容器,更是业务逻辑与系统交互的隐性契约。dataclasses以轻量性守护核心业务的高效运转,它摒弃了所有非必要的附加逻辑,让数据以最纯粹的形式在系统内部流转,这种极简主义的设计哲学,与Python“优雅、明确、简单”的理念高度契合;Pydantic以强约束构建系统交互的可靠边界,它通过类型注解与约束规则,将“数据应是什么样”的契约显性化,让系统与外部的交互变得可预测、可信任,这种契约式编程的思想,为复杂系统的稳定性提供了坚实保障。两者的协同构成了数据建模的完整解决方案,既解决了内部数据传递的效率问题,又攻克了外部数据交互的可靠性难题。

PostgreSQL 在各行各业的关键应用中具有极高适用性。尽管 PostgreSQL 提供了良好的性能,但仍存在一些用户不太关注但对整体效率与速度至关重要的问题。多数人认为增加 CPU 核数、更快的存储、更大内存即可提升性能,但还有同样重要的因素需要关注——那就是延迟。

延迟意味着什么?

数据库执行查询操作的耗时,仅占应用程序接收查询结果总耗时的极小部分。下图可直观呈现该过程的内在逻辑:

1.png

客户端应用发送请求后,驱动程序通过网络向 PostgreSQL 发送消息(a),数据库执行查询(b),并将结果集返回给应用程序(c)。关键问题在于:相较于查询执行时间(b),网络传输时间(a 与 c)是否具有显著影响。通过实验可以加以验证。

首先,使用 pgbench 初始化一个简单的测试数据库。对于本次测试,小规模数据库已足够:

cybertec$ pgbench -i blog
dropping old tables...
NOTICE:  table "pgbench_accounts" does not exist, skipping
NOTICE:  table "pgbench_branches" does not exist, skipping
NOTICE:  table "pgbench_history" does not exist, skipping
NOTICE:  table "pgbench_tellers" does not exist, skipping
creating tables...
generating data (client-side)...
vacuuming...
creating primary keys...
done in 0.19 s (drop tables 0.00 s, create tables 0.02 s, client-side generate 0.13 s, vacuum 0.02 s, primary keys 0.02 s).

随后进行第一次基础测试:建立单个 UNIX Socket 连接,运行 20 秒(只读测试):

cybertec$ pgbench -c 1 -T 20 -S blog
pgbench (17.5)
starting vacuum...end.
transaction type: <builtin: select only>
scaling factor: 1
query mode: simple
number of clients: 1
number of threads: 1
maximum number of tries: 1
duration: 20 s
number of transactions actually processed: 1035095
number of failed transactions: 0 (0.000%)
latency average = 0.019 ms
initial connection time = 2.777 ms
tps = 51751.287839 (without initial connection time)

关键指标如下:

  • 平均延迟:0.019 毫秒
  • 每秒事务处理量(TPS):51751

该数据表现对于单连接场景而言已属良好水平。

下一步执行相同查询测试,但将连接方式从 UNIX 套接字更换为指向本地主机(localhost)的 TCP 连接(非远程连接):

cybertec$ pgbench -c 1 -T 20 -S blog -h localhost
pgbench (17.5)
starting vacuum...end.
transaction type: <builtin: select only>
scaling factor: 1
query mode: simple
number of clients: 1
number of threads: 1
maximum number of tries: 1
duration: 20 s
number of transactions actually processed: 583505
number of failed transactions: 0 (0.000%)
latency average = 0.034 ms
initial connection time = 3.290 ms
tps = 29173.916752 (without initial connection time)

结果出现明显变化,关键指标如下:

  • 平均延迟:0.034 毫秒
  • 每秒事务数(TPS):29173

吞吐量下降约 44%。下图对此进行了直观展示:

2.png

值得注意的是,延迟仅从 0.019 毫秒上升至 0.034 毫秒,变化幅度极小。但由于查询本身执行速度极快,即便如此微小的延迟也会带来显著影响。执行计划可以说明这一点:

blog=# explain analyze SELECT *
      FROM   pgbench_accounts
WHERE  aid = 434232;
                         QUERY PLAN
------------------------------------------------------------
 Index Scan using pgbench_accounts_pkey on pgbench_accounts
   (cost=0.29..8.31 rows=1 width=97)
   (actual time=0.015..0.016 rows=0 l                                                                                                                  oops=1)
   Index Cond: (aid = 434232)
 Planning Time: 0.227 ms
 Execution Time: 0.047 ms
(4 rows)

执行计划中的关键数值为 0.016,表示索引扫描在表中定位记录所需的时间。将该数值与额外引入的网络延迟进行对比,即可理解微小变化为何会造成巨大差异。

真实网络环境中的延迟

在实际场景中,应用程序与数据库通常部署在不同的机器上。测试前,先查看 traceroute 的输出结果:

different_box$ traceroute 10.1.139.53
traceroute to 10.1.139.53 (10.1.139.53), 30 hops max, 60 byte packets
 1  _gateway (10.0.0.1)  0.212 ms  0.355 ms  0.378 ms
 2  cybertec (10.1.139.53)  0.630 ms  0.619 ms *

可以看到,从运行 pgbench 的主机到数据库服务器的路径较短,仅通过内部网络完成通信。

再次运行相同测试,结果如下:

different_box$ pgbench -h 10.1.139.53 -S -c 1 -T 20 blog
pgbench (17.5)
starting vacuum...end.
transaction type: <builtin: select only>
scaling factor: 1
query mode: simple
number of clients: 1
number of threads: 1
maximum number of tries: 1
duration: 20 s
number of transactions actually processed: 47540
number of failed transactions: 0 (0.000%)
latency average = 0.420 ms
initial connection time = 9.727 ms
tps = 2378.123901 (without initial connection time)

关键指标为:

  • 平均延迟:0.420 毫秒
  • 每秒事务数(TPS):2378

即便延迟仅为 0.420 毫秒,吞吐量已从 5 万 TPS 降至 2378 TPS。虽然该测试仍为单连接,但原因十分清晰:网络传输所消耗的 0.4 毫秒,与索引读取所需的 0.016 毫秒相比,已是数量级上的差距。

下图展示了吞吐量变化情况:

3.png

可确定的是,若网络架构中增加更多网络层级,吞吐量数据将进一步显著下降。该问题在云计算环境中尤为突出,每一层负载均衡、每一次网络跳转、每一台路由设备、每一条防火墙规则,均会增加网络延迟,进而降低应用程序运行效率。对于执行耗时极短的查询操作而言,网络延迟产生的额外开销占比越高,查询操作本身的执行耗时占比则越低,其对整体性能的影响程度也随之下降。

并发机制:可行的解决方案?

上述实验展示了极端情况,适用于单一应用在应用与数据库间频繁交互的场景。而在负载较高的业务系统中,通常存在多用户并发访问的情况。若增加并发连接数,系统性能可呈现较为理想的表现:

cybertec$ pgbench -c 4 -j 4 -T 20 -S blog -h localhost
pgbench (17.5)
starting vacuum...end.
transaction type: <builtin: select only>
scaling factor: 1
query mode: simple
number of clients: 4
number of threads: 4
maximum number of tries: 1
duration: 20 s
number of transactions actually processed: 1639827
number of failed transactions: 0 (0.000%)
latency average = 0.049 ms
initial connection time = 5.637 ms
tps = 82007.653121 (without initial connection time)

提取关键数据如下:

  • 平均延迟:0.429 毫秒
  • 每秒事务数(TPS):82007

使用 4 个并发连接,TPS 达到 82,000,增加更多并发可进一步提升。在现代服务器上,每秒超过 100 万次操作完全可行。但前提是数据库与查询来源距离接近,网络延迟不构成瓶颈。

更快的 CPU 是否有帮助?

常见疑问:增加 CPU 核数或提升单核性能是否有意义?对比如下:

  • 索引查找:0.016 毫秒
  • 网络延迟:0.490 毫秒

即便 CPU 更快,优化的仅为 0.016 毫秒,占总耗时约 3%,剩余 97% 时间不受影响。本质上,这与吞吐量关系不大,而是延迟问题。对于极短查询,延迟累积可能导致严重性能下降,尤其在云环境下网络复杂度更高。

对于执行时间较长的查询,延迟影响较小;但对于超快小查询,网络延迟可能成为主要性能瓶颈。

总结

延迟在高频、短时查询场景中具有决定性影响。单连接环境下,微小的网络延迟即可导致吞吐量大幅下降;通过并发可以在一定程度上缓解这一问题,但网络距离和拓扑结构仍是关键约束因素。相比之下,单纯提升 CPU 性能对以网络延迟为主导的场景改善有限。在云环境与分布式架构中,延迟问题需要在系统设计阶段予以重点关注。

原文链接:

https://www.cybertec-postgresql.com/en/postgresql-performance...

作者:Hans-Jürgen Schönig


HOW 2026 议题招募中

2026 年 4 月 27-28 日,由 IvorySQL 社区联合 PGEU(欧洲 PG 社区)、PGAsia(亚洲 PG 社区)共同打造的 HOW 2026(IvorySQL & PostgreSQL 技术峰会) 将再度落地济南。届时,PostgreSQL 联合创始人 Bruce Momjian 等顶级大师将亲临现场。

自开启征集以来,HOW 2026 筹备组已感受到来自全球 PostgreSQL 爱好者的澎湃热情。为了确保大会议题的深度与广度,我们诚邀您在 2026 年 2 月 27 日截止日期前,提交您的技术见解。

投递链接:https://jsj.top/f/uebqBc

一、背景

得物经过10年发展,计算任务已超10万+,数据已经超200+PB,为了降低成本,计算引擎和存储资源需要从云平台迁移到得物自建平台,计算引擎从云平台Spark迁移到自建Apache Spark集群、存储从ODPS迁移到OSS。

在迁移时,最关键的一点是需要保证迁移前后数据的一致性,同时为了更加高效地完成迁移工作(目前计算任务已超10万+,手动比数已是不可能),因此比数平台便应运而生。

二、数据比对关键挑战与目标

关键挑战一:如何更快地完成全文数据比对

现状痛点:

在前期迁移过程中,迁移同学需要手动join两张表来识别不一致数据,然后逐条、逐字段进行人工比对验证。这种方式在任务量较少时尚可应付,但当任务规模达到成千上万级别时,就无法实现并发快速分析。

核心问题:

  • 效率瓶颈:每天需要完成数千任务的比对,累计待迁移任务达10万+,涉及表数十万张。
  • 扩展性不足:传统人工比对方式无法满足大规模并发处理需求。

关键挑战二:如何精准定位异常数据

现状痛点:

迁移同学在识别出不一致数据后,需要通过肉眼观察来定位具体问题,经常导致视觉疲劳和分析效率低下。

核心问题:

  • 分析困难:在比对不通过的情况下,比对人员需要人工分析失败原因。
  • 复杂度高:面对数据量庞大、加工逻辑复杂的场景,特别是在处理大JSON数据时,肉眼根本无法有效分辨差异。
  • 耗时严重:单次比对不通过场景的平均分析时间高达1.67小时/任务。

比数核心目标

基于以上挑战,数据比对系统需要实现以下核心目标:

  • 高并发处理能力:支持每天数千任务的快速比对,能够处理10万+待迁移任务和数十万张表的规模。
  • 自动化比对机制:实现全自动化的数据比对流程,减少人工干预,提升比对效率。
  • 智能差异定位:提供精准的差异定位能力,能够快速识别并高亮显示不一致的字段和数据。
  • 可视化分析界面:构建友好的可视化分析平台,支持大JSON数据的结构化展示和差异高亮。
  • 性能优化:将用户单次比对分析时间从小时级大幅缩短至分钟级别。
  • 可扩展架构:设计可水平扩展的系统架构,能够随着业务增长灵活扩容。

三、解决方案实现原理

快速完成全文数据比对方法

比数方法调研

待比对两表数据大小:300GB,计算资源:1000c


经过调研分析比数平台采用第二种和第三种相结合的方式进行比数。

先Union再分组数据一致性校验原理

假如我们有如下a和b两表张需要进行数据比对

表a:


表b:


表行数比较:

select count(1) from a ;
select count(1) from b ;

针对上面的查询结果,如果数量不一致则退出比对,待修复后重新比数;数量一致则继续字段值比较。

字段值比较:

第一步:union a 和 b

select 1 as _t1_count, 0 as _t2_count, `id`, `name`, `age`, `score`
from a
union all
select 0 as _t1_count, 1 as _t2_count, `id`, `name`, `age`, `score`
from b

第二步:sum(_t1_count),sum(_t2_count) 后分组

select sum(_t1_count) as sum_t1_count, sum(_t2_count) as sum_t2_count, `id`, `name`, `age`, `score`
from (
select 1 as _t1_count, 0 as _t2_count, `id`, `name`, `age`, `score`
from a
union all
select 0 as _t1_count, 1 as _t2_count, `id`, `name`, `age`, `score`
from b
) as union_table
group by `id`, `name`, `age`, `score`


第三步:把不一致数据写入新的表中(即上面表中sum_t1_count和sum_t2_count不相等的数据)

drop table if exists a_b_diff_20240908;
create table a_b_diff_20240908 as select * from (
select sum(_t1_count) as sum_t1_count, sum(_t2_count) as sum_t2_count, `id`, `name`, `age`, `score`
from (
select 1 as _t1_count, 0 as _t2_count, `id`, `name`, `age`, `score`
from a
union all
select 0 as _t1_count, 1 as _t2_count, `id`, `name`, `age`, `score`
from b
) as union_table
group by `id`, `name`, `age`, `score`
having sum(_t1_count) <> sum(_t2_count)
) as tmp

如果a_b_diff_20240908没有数据则两张表没有差异,比数通过,如有差异如下:

第四步:读取不一致记录表,根据主键(比如id)找出不一致字段并写到结果表中。

第五步:针对不一致字段的数据进行根因分析,如 json 、数组顺序问题、浮点数精度问题等,给出不一致具体原因。

哈希值聚合实现高效一致性校验

针对上面union后sum 再 group by 方式 在数据量大的时候还是非常耗资源和时间的,考虑到比数任务毕竟有70%都是一致的,所以我们可以先采用哈希值聚合比较两表的的值是否一致,使用这种高效的方法先把两表数据一致的任务过滤掉,剩下的再采用上面方法继续比较,因为还要找出是哪个字段哪里不一致。原理如下:

SELECT count (*),SUM(xxhash64(cloum1)^xxhash64(cloum2)^...) FROM tableA 
EXCEPT 
SELECT count(*),SUM(xxhash64(cloum1)^xxhash64(cloum2)^...) FROM tableB

如果有记录为空说明数据一致,不为空说明数据不一致需要采用上面提到union 分组的方法去找出具体字段哪里不一样。

通过哈希值聚合,单个任务比数时间从500s降低到160s,节省大约70%的时间。

找到两张表不一致数据后需要对两张的数据进行分析确定不一致的点在哪里?这里就需要知道表的主键,根据主键逐个比对两张表的其他字段,因此系统会先进行主键的自动探查,以及无主键的兜底处理。

精准定位异常数据实现方法

自动探查主键:实现原理如下

刚开始我们采用的前5个字段找主键的方式,如下:

针对表a的前5个字段 循环比对
select count(distinct id) from a 与 select count(1) from a 比较 ,如相等主键为id ,不相等继续往下执行
select count(distinct id,name) from a 与 select count(1) from a比较,如相等主键为id,name ,不相等继续往下执行
select count(distinct id,name,age) from a 与 select count(1) from a比较,如相等主键为id,name,age ,不相等继续往下执行,直到循环结束

采用上面的方法不一致任务中大约有49.6%任务自动探查主键失败:因此需重点提升主键识别能力。

针对以上主键探查成功率低的问题,后续进行了一些迭代,优化后的主键探查流程如下:

一、先采用sum(hash)高效计算方式进行探查:

1.先算出两张表每个字段的sum(hash)值  。

select sum(hash(id)),sum(hash(name)),sum(hash(age)),sum(hash(score)) from a 
union all 
select sum(hash(id)),sum(hash(name)),sum(hash(age)),sum(hash(score)) from b;

2.找出值相等的所有字段,本案例中为 id, name。

3.对id,name 可能是主键进一步确认,先进行行数校验,如 select count(distinct id,name) from a 的值等于select count(1) from a 则进一步校验,否则进入到第二种探查主键方式。

4.唯一性验证,如果值为0则表示探查主键成功,否则进入到第二种探查主键方式。

slect count(*) from ((select id,name from a ) expect (select id,name from b))

二、传统distinct方式探查:

针对表a的前N(所有字段数/2或者前N、后N等)个字段 循环比对:

1.select count(distinct id) from a与select count(1) from a比较 ,如相等主键为id ,不相等继续往下执行。

2.select count(distinct id,name) from a 与 select count(1) from a比较,如相等主键为id,name ,不相等继续往下执行。

3.select count(distinct id,name,age) from a 与 select count(1) from a比较,如相等主键为id,name,age ,不相等继续往下执行,直到循环结束。

三、全字段排序模拟:

如果上面两种方式还是没有找到主键则把不一致记录表进行全字段排序然后对第一条和第二条记录挨个字段进行分析,找出不一致内容,示例如下:

slect * from a_b_diff_20240908 order by id,name,age,score asc limit 10;


通过以上结果表可以得出两表的age字段不一致 ,score不一致(但按key排序后一致)。

如果以上自动化分析还是找不到不一致字段内容,可以人工确认表的主键后到平台手动指定主键字段,然后点击后续分析即可按指定主键去找字段不一致内容。

通过多次迭代优化找主键策略,找主键成功率从最初的50.4%提升到75%,加上全字段order by排序后最前两条数据进行分析,相当于可以把找主键的成功率提升到90%以上。

根因分析:实现原理如下

当数据不一致时,平台会根据主键找出两个表哪些字段数据不一致并进行分析,具体如下:

  • 精准定位: 明确指出哪条记录、哪个字段存在差异,并展示具体的源数据和目标数据值。
  • 智能根因分析: 内置了多种差异模式识别规则,能够自动分析并提示不一致的可能原因,例如:
  • 精度问题:如浮点数计算1.0000000001与1.0的差异。
  • JSON序列化差异:如{"a":1, "b":2}与{"b":2, "a":1},在语义一致的情况下,因键值对顺序不同而被标记为差异。同时系统会提示排序后一致。
  • 空值处理差异:如NULL值与空字符串""的差异判定。
  • 日期时区转换问题:时间戳在不同时区下表示不同。

  • 比对结果统计: 提供总数据量、一致数据量、不一致数据量及不一致率百分比,为项目决策提供清晰的量化依据。
  • 比数人员根据平台分析的差异原因,决定是否手动标记通过或进行任务修复。
  • 效果展示:

四、比数平台功能介绍

数据比对基本流程

任务生成:三种比对模式

  • 两表比对: 最直接的比对方式。用户只需指定源表与目标表,平台即可启动全量数据比对。它适用于临时比对的场景。
  • 任务节点比对: 一个任务可能输出多个表,逐一配置这些表的比对任务繁琐且易遗漏,任务节点比对模式完美解决了这一问题。用户只需提供任务节点ID,平台便会自动解析该节点对应的SQL代码,提取出所有输出表,并自动生成比对任务,极大地提升任务迁移比对效率。
  • SQL查询比对: 业务在进行SDK迁移只关心某些查询在迁移后数据是否一样,因此需要对用户提交的所有查询SQL进行比对,平台会分别在ODPS和Spark引擎上执行该查询,将结果集导出到两张临时表,再生成比对任务。

前置校验:提前发现问题

在启动耗时的全量比对之前,需要对任务进行前置校验,确保比对是在表结构一致、集群环境正常的情况下进行,否则一旦启动比数会占用大量计算资源,最后结果还是比数不通过,会影响比数平台整体的运行效率。因此比数平台一般会针对如下问题进行前置拦截。

  • 元数据一致性校验: 比对双方的字段名、字段类型、字段顺序、字段个数是否一致。
  • 函数缺失校验: 针对Spark引擎,校验SQL中使用的函数是否存在、是否能被正确识别,避免因函数不支持而导致的比对失败。
  • 语法问题校验: 分析SQL语句的语法结构,确保其在目标引擎中能够被顺利解析,避免使用了某些特定写法会导致数据出现不一致情况,提前发现语法层面问题,并对任务进行改写。

更多校验点如下:




通过增加以上前置校验拦截,比数任务数从每天3000+下降到1500+, 减少50% 的无效比数,其中UDF缺失最多,有效拦截任务1238,缺少函数87个(帮比数同学快速定位,一次性解决函数缺失问题,避免多次找引擎同学陆陆续续添加,节省双方时间成本)。

破解比数瓶颈:资源分配与任务调度优化

由于比数平台刚上线的时候只有计算迁移团队在使用,后面随着更多的团队开始使用,性能遇到了如下瓶颈:

1.资源不足问题: 不同业务(计算迁移、存储迁移、SDK迁移)的任务相互影响,基本比数任务与根因分析任务相互抢占资源。

2.任务编排不合理: 没有优先级导致大任务阻塞整体比数进程。

3.引擎参数设置不合理: 并行度不够、数据分块大小等高级参数。

针对以上问题比数平台进行了如下优化:

  • 按不同业务拆分成多个队列来运行,保证各个业务之间的比数任务可以同时进行,不会相互影响。
  • 根因分析使用单独的队列,与数据比对任务的队列分开,避免相互抢占资源发生“死锁”。
  • 相同业务内部按批次分时段、分优先级运行,保障重要任务优先进行比对。
  • 针对Spark引擎默认调优了公共参数、并支持用户自主设置其他高级参数。

通过以上优化达到到了如下效果:

  • 比数任务从每天22点完成提前至18点前,同时支持比数同学自主控制高优任务优先执行,方便比数同学及时处理不一致任务。
  • 通过优化资源队列使用方式,使系统找不到主键辅助用户自主找主键接口响应时间从58.5秒降到 26.2秒。

五、比数平台收益分享

平台持续安全运行500+天,每日可完成2000+任务比对,有效比数128万+次,0误判。

  • 助力计算迁移团队节省45+人日/月,完成数据分析、离线数仓空间任务的比对、交割。
  • 助力存储迁移团队完成20%+存储数据的迁移。
  • 助力引擎团队完成800+批次任务的回归验证,确保每一次引擎发布的安全及高效。
  • 助力SDK迁移团队完成80%+应用的迁移。

六、未来演进方向

接下来,平台计划在以下方面持续改进:

智能分析引擎: 针对Json复杂嵌套类型的字段接入大模型进行数据根因分析,找出不一致内容。

比对策略优化: 针对大表自动切分进行比对,降低比数过程出现因数据量大导致异常,进一步提升比对效率。

通用方案沉淀: 将典型的比对场景和解决方案能用化,应用到更多场景及团队中去。

七、结语

比数平台是得物在迁移过程中,为了应对海量任务、大数据量、字段内容复杂多样、异常数据难定位等挑战,确保业务迁移后数据准确而专门提供的解决方案,未来它不单纯是一个服务计算迁移、存储迁移、SDK迁移、Spark版本升级等需要的数据比对工具,而是演进为数据平台中不可或缺的基础设施。

往期回顾

1.得物App智能巡检技术的探索与实践

2.深度实践:得物算法域全景可观测性从 0 到 1 的演进之路 

3.前端平台大仓应用稳定性治理之路|得物技术

4.RocketMQ高性能揭秘:承载万亿级流量的架构奥秘|得物技术

5.PAG在得物社区S级活动的落地

文 /Galaxy平台

关注得物技术,每周更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

在这里插入图片描述

摘要

随着 HarmonyOS / OpenHarmony 在手机、平板、智慧屏、车机等多设备上的落地,应用的复杂度正在明显提升。页面不再只是简单展示,而是伴随着网络请求、数据计算、设备协同等大量逻辑。如果这些逻辑处理不当,很容易出现页面卡顿、点击无响应,甚至 Ability 被系统回收的问题。

线程阻塞,已经成为鸿蒙应用开发中最容易踩坑、也最影响体验的问题之一。本文将结合实际开发场景,用尽量口语化的方式,聊一聊在鸿蒙系统中如何系统性地避免线程阻塞,并给出可以直接运行的 Demo 代码。

引言

在早期的应用开发中,很多开发者习惯把逻辑直接写在点击事件里,或者在页面加载时同步读取数据。这种写法在简单页面中问题不大,但在 HarmonyOS 这种强调流畅体验和多设备协同的系统中,很容易暴露问题。

鸿蒙的 UI 是声明式的,系统对主线程(UI 线程)非常敏感。一旦主线程被占用,页面掉帧、动画卡住、操作延迟都会立刻出现。因此,理解哪些操作会阻塞线程,以及如何把这些操作合理地“挪走”,是每个鸿蒙开发者绕不开的一课。

下面我们从原理、工具、代码和真实场景几个角度,完整地拆解这个问题。

为什么线程阻塞在鸿蒙中这么致命

UI 线程到底在忙什么

在 HarmonyOS 中,UI 线程主要负责三件事:

  • ArkUI 页面渲染
  • 用户事件分发(点击、滑动等)
  • Ability 生命周期回调

简单理解就是:只要和“看得见、点得动”有关的事情,几乎都在 UI 线程上完成

一旦你在这里做了耗时操作,比如计算、IO、网络等待,页面就会立刻表现出“卡”的感觉。

常见的阻塞来源

在实际项目中,最容易导致阻塞的操作通常包括:

  • 同步网络请求
  • 文件读写
  • 数据库查询
  • 大量 for 循环计算
  • 人为 sleep 或死循环

这些操作本身不一定是错的,问题在于它们被放在了不该放的线程上

鸿蒙中避免线程阻塞的核心思路

一个总原则

可以把鸿蒙里的线程使用总结成一句话:

UI 线程只处理 UI,其他事情交给异步、线程池或 Worker。

围绕这个原则,系统也提供了多种工具,帮助开发者把任务“分流”。

异步编程是第一道防线

使用 async / await 处理耗时逻辑

在 ArkTS 中,官方推荐优先使用 Promise 和 async / await。它的好处是代码结构清晰,而且不会阻塞 UI 线程。

示例:页面加载网络数据

@Entry
@Component
struct AsyncDemo {
  @State message: string = '加载中...'

  build() {
    Column() {
      Text(this.message)
        .fontSize(20)
        .margin(20)

      Button('重新加载')
        .onClick(() => {
          this.loadData()
        })
    }
  }

  async loadData() {
    this.message = '请求中...'
    let response = await fetch('https://example.com/data')
    let result = await response.text()
    this.message = result
  }
}

代码说明

  • loadData 使用 async 声明,不会阻塞 UI
  • await 只是暂停当前函数执行,不会卡住页面
  • UI 更新完全由状态变化驱动

这是最基础、也是最常用的一种防阻塞方式。

TaskPool:处理计算和 IO 的利器

什么时候该用 TaskPool

当你遇到下面这些情况时,TaskPool 几乎是必选项:

  • 大量计算
  • 批量数据处理
  • 文件压缩、解析

可运行 Demo 示例

import taskpool from '@ohos.taskpool'

@Concurrent
function calculateSum(count: number): number {
  let sum = 0
  for (let i = 0; i < count; i++) {
    sum += i
  }
  return sum
}

@Entry
@Component
struct TaskPoolDemo {
  @State result: string = '等待计算'

  build() {
    Column() {
      Text(this.result)
        .fontSize(18)
        .margin(20)

      Button('开始计算')
        .onClick(() => {
          this.startTask()
        })
    }
  }

  startTask() {
    this.result = '计算中...'
    taskpool.execute(calculateSum, 1000000).then(res => {
      this.result = `结果是:${res}`
    })
  }
}

代码说明

  • @Concurrent 表示该函数可以并发执行
  • TaskPool 自动管理线程,不需要开发者手动创建线程
  • UI 线程只负责接收结果和更新状态

在真实项目中,使用 TaskPool 往往能立刻解决页面卡顿问题。

Worker:长期后台任务的选择

Worker 的使用场景

如果任务具有下面这些特点,就更适合使用 Worker:

  • 长时间运行
  • 需要持续处理数据
  • 与 UI 强隔离

比如日志分析、音视频处理、复杂解析等。

示例:使用 Worker 处理数据

主线程代码

let worker = new Worker('workers/data_worker.ts')

worker.postMessage({ action: 'start' })

worker.onmessage = (e) => {
  console.log('收到结果:', e.data)
}

Worker 线程代码

onmessage = function (e) {
  if (e.data.action === 'start') {
    let result = 0
    for (let i = 0; i < 500000; i++) {
      result += i
    }
    postMessage(result)
  }
}

代码说明

  • Worker 与 UI 线程完全独立
  • 即使计算时间较长,也不会影响页面交互
  • 通过消息机制进行通信

结合实际场景的应用示例

场景一:列表页面加载大量数据

问题:

  • 首次进入页面时一次性处理全部数据
  • 页面明显卡顿

解决思路:

  • 网络请求使用 async
  • 数据整理放入 TaskPool
async loadList() {
  let data = await fetchData()
  taskpool.execute(processData, data).then(list => {
    this.list = list
  })
}

场景二:文件导入与解析

问题:

  • 文件较大
  • 解析过程耗时

解决思路:

  • Worker 负责解析
  • UI 只显示进度
worker.postMessage({ filePath })

场景三:复杂计算驱动 UI 更新

问题:

  • 计算逻辑和 UI 耦合

解决思路:

  • 计算完全放到 TaskPool
  • UI 只订阅结果

QA 环节

Q:async / await 会不会阻塞线程?
A:不会,它只是让出执行权,不会卡住 UI 线程。

Q:TaskPool 和 Worker 怎么选?
A:短期、一次性的任务优先 TaskPool,长期或持续任务用 Worker。

Q:能不能在生命周期里做耗时操作?
A:不建议,生命周期函数应尽量轻量。

总结

线程阻塞并不是某一个 API 的问题,而是设计问题。在 HarmonyOS 中,系统已经为我们准备好了异步模型、TaskPool 和 Worker,只要遵循“UI 线程只做 UI”的原则,大多数卡顿问题都可以提前避免。

在真实项目中,提前做好任务拆分、线程规划,比后期排查卡顿要省心得多。这也是鸿蒙开发从“能跑”到“跑得顺”的一个重要分水岭。

在这里插入图片描述

摘要

随着 HarmonyOS / OpenHarmony 在手机、平板、智慧屏、车机等多设备上的落地,应用的复杂度正在明显提升。页面不再只是简单展示,而是伴随着网络请求、数据计算、设备协同等大量逻辑。如果这些逻辑处理不当,很容易出现页面卡顿、点击无响应,甚至 Ability 被系统回收的问题。

线程阻塞,已经成为鸿蒙应用开发中最容易踩坑、也最影响体验的问题之一。本文将结合实际开发场景,用尽量口语化的方式,聊一聊在鸿蒙系统中如何系统性地避免线程阻塞,并给出可以直接运行的 Demo 代码。

引言

在早期的应用开发中,很多开发者习惯把逻辑直接写在点击事件里,或者在页面加载时同步读取数据。这种写法在简单页面中问题不大,但在 HarmonyOS 这种强调流畅体验和多设备协同的系统中,很容易暴露问题。

鸿蒙的 UI 是声明式的,系统对主线程(UI 线程)非常敏感。一旦主线程被占用,页面掉帧、动画卡住、操作延迟都会立刻出现。因此,理解哪些操作会阻塞线程,以及如何把这些操作合理地“挪走”,是每个鸿蒙开发者绕不开的一课。

下面我们从原理、工具、代码和真实场景几个角度,完整地拆解这个问题。

为什么线程阻塞在鸿蒙中这么致命

UI 线程到底在忙什么

在 HarmonyOS 中,UI 线程主要负责三件事:

  • ArkUI 页面渲染
  • 用户事件分发(点击、滑动等)
  • Ability 生命周期回调

简单理解就是:只要和“看得见、点得动”有关的事情,几乎都在 UI 线程上完成

一旦你在这里做了耗时操作,比如计算、IO、网络等待,页面就会立刻表现出“卡”的感觉。

常见的阻塞来源

在实际项目中,最容易导致阻塞的操作通常包括:

  • 同步网络请求
  • 文件读写
  • 数据库查询
  • 大量 for 循环计算
  • 人为 sleep 或死循环

这些操作本身不一定是错的,问题在于它们被放在了不该放的线程上

鸿蒙中避免线程阻塞的核心思路

一个总原则

可以把鸿蒙里的线程使用总结成一句话:

UI 线程只处理 UI,其他事情交给异步、线程池或 Worker。

围绕这个原则,系统也提供了多种工具,帮助开发者把任务“分流”。

异步编程是第一道防线

使用 async / await 处理耗时逻辑

在 ArkTS 中,官方推荐优先使用 Promise 和 async / await。它的好处是代码结构清晰,而且不会阻塞 UI 线程。

示例:页面加载网络数据

@Entry
@Component
struct AsyncDemo {
  @State message: string = '加载中...'

  build() {
    Column() {
      Text(this.message)
        .fontSize(20)
        .margin(20)

      Button('重新加载')
        .onClick(() => {
          this.loadData()
        })
    }
  }

  async loadData() {
    this.message = '请求中...'
    let response = await fetch('https://example.com/data')
    let result = await response.text()
    this.message = result
  }
}

代码说明

  • loadData 使用 async 声明,不会阻塞 UI
  • await 只是暂停当前函数执行,不会卡住页面
  • UI 更新完全由状态变化驱动

这是最基础、也是最常用的一种防阻塞方式。

TaskPool:处理计算和 IO 的利器

什么时候该用 TaskPool

当你遇到下面这些情况时,TaskPool 几乎是必选项:

  • 大量计算
  • 批量数据处理
  • 文件压缩、解析

可运行 Demo 示例

import taskpool from '@ohos.taskpool'

@Concurrent
function calculateSum(count: number): number {
  let sum = 0
  for (let i = 0; i < count; i++) {
    sum += i
  }
  return sum
}

@Entry
@Component
struct TaskPoolDemo {
  @State result: string = '等待计算'

  build() {
    Column() {
      Text(this.result)
        .fontSize(18)
        .margin(20)

      Button('开始计算')
        .onClick(() => {
          this.startTask()
        })
    }
  }

  startTask() {
    this.result = '计算中...'
    taskpool.execute(calculateSum, 1000000).then(res => {
      this.result = `结果是:${res}`
    })
  }
}

代码说明

  • @Concurrent 表示该函数可以并发执行
  • TaskPool 自动管理线程,不需要开发者手动创建线程
  • UI 线程只负责接收结果和更新状态

在真实项目中,使用 TaskPool 往往能立刻解决页面卡顿问题。

Worker:长期后台任务的选择

Worker 的使用场景

如果任务具有下面这些特点,就更适合使用 Worker:

  • 长时间运行
  • 需要持续处理数据
  • 与 UI 强隔离

比如日志分析、音视频处理、复杂解析等。

示例:使用 Worker 处理数据

主线程代码

let worker = new Worker('workers/data_worker.ts')

worker.postMessage({ action: 'start' })

worker.onmessage = (e) => {
  console.log('收到结果:', e.data)
}

Worker 线程代码

onmessage = function (e) {
  if (e.data.action === 'start') {
    let result = 0
    for (let i = 0; i < 500000; i++) {
      result += i
    }
    postMessage(result)
  }
}

代码说明

  • Worker 与 UI 线程完全独立
  • 即使计算时间较长,也不会影响页面交互
  • 通过消息机制进行通信

结合实际场景的应用示例

场景一:列表页面加载大量数据

问题:

  • 首次进入页面时一次性处理全部数据
  • 页面明显卡顿

解决思路:

  • 网络请求使用 async
  • 数据整理放入 TaskPool
async loadList() {
  let data = await fetchData()
  taskpool.execute(processData, data).then(list => {
    this.list = list
  })
}

场景二:文件导入与解析

问题:

  • 文件较大
  • 解析过程耗时

解决思路:

  • Worker 负责解析
  • UI 只显示进度
worker.postMessage({ filePath })

场景三:复杂计算驱动 UI 更新

问题:

  • 计算逻辑和 UI 耦合

解决思路:

  • 计算完全放到 TaskPool
  • UI 只订阅结果

QA 环节

Q:async / await 会不会阻塞线程?
A:不会,它只是让出执行权,不会卡住 UI 线程。

Q:TaskPool 和 Worker 怎么选?
A:短期、一次性的任务优先 TaskPool,长期或持续任务用 Worker。

Q:能不能在生命周期里做耗时操作?
A:不建议,生命周期函数应尽量轻量。

总结

线程阻塞并不是某一个 API 的问题,而是设计问题。在 HarmonyOS 中,系统已经为我们准备好了异步模型、TaskPool 和 Worker,只要遵循“UI 线程只做 UI”的原则,大多数卡顿问题都可以提前避免。

在真实项目中,提前做好任务拆分、线程规划,比后期排查卡顿要省心得多。这也是鸿蒙开发从“能跑”到“跑得顺”的一个重要分水岭。

度小满引入 Apache Doris 替换原有 Greenplum,实现整体查询效率提升 82%,与此同时,集群缩减 2/3、年省数百万的巨大效益。本文将分享度小满如何基于 Doris 从 0 到 1 构建超大规模数据分析平台,并围绕平滑迁移、异地多活容灾等方面,分享实践经验。

本文整理自度小满 Doris 数据库负责人汤斯在 Doris Summit 2025 中的演讲,并以演讲者第一视角进行叙述。

度小满金融(原百度金融)作为一家覆盖现代财富管理、支付、金融科技等多板块的科技公司,数据的分析处理对其极为重要,已经深度融入业务生命周期的每个环节,是进行风险控制、商业决策、用户体验优化及运营提效的基石。

随着业务高速发展,度小满原有基于 Greenplum 搭建的 OLAP 平台,逐渐暴露出三大痛点:

  • 规模与稳定性瓶颈:存储已接近饱和,扩容至百余台已接近硬件规模的承载上限,如果继续扩容,将面临更严重的稳定性挑战。

  • 性能与体验不佳:Greenplum SQL 查询执行速度慢,且经常出现 “计算时间远小于排队时间” 的情况,严重影响业务分析效率。

  • 缺失技术支持:当前使用的 Greenplum 6 版本技术架构已显得陈旧,并且 2024 年 Greenplum 宣布将停止开源,后续的技术支持与迭代升级将无法保障。

为了应对这些痛点,度小满金融迫切寻找更为高效、稳定且具备现代化技术架构的数据处理解决方案,以支持其未来的业务发展。

Apache Doris:高吞吐、快查询

面对日益增长的业务体量与复杂多变的分析需求,选用一个高效、可靠的数据库系统,已成为支撑业务稳健发展与快速创新的关键。Apache Doris 以其出色的性能表现与高度灵活的架构,成为众多场景下的优选方案。为深入验证其在海量数据与复杂分析场景中的能力,我们展开了一系列性能测试,关键结果如下:

  • 查询性能:在 1TB TPC-DS 标准测试集中, Apache Doris的查询速度约是 Greenplum 6 的 20-30 倍

  • 导入性能:在基于 Flink 写入的 TPS 测试中,基于单分片导入,压测最大 TPS 为:5000W/s

  • JSON 数据处理:针对新推出的 Variant JSON 数据类型,测试显示:存储 2-3 万 Key 时,其空间占用仅为普通 JSON 的 1/10 甚至更低,查询效率则提升至 10 倍以上

综上可知,Apache Doris 在写入吞吐、响应速度及存储效率上表现卓越,有力证明了其应对大规模、实时化、半结构化数据分析挑战的坚实技术基础。

基于 Apache Doris 的大规模数据分析平台

在上述详实的选型调研之后,我们决定采用 Apache Doris 替代原有 Greenplum 集群,构建超大规模数据分析平台。

为验证 Apache Doris 在真实业务场景中的表现,我们先进行了小范围试点,部署了少量 Doris 集群,并先行接入几个关键业务方。试点期间,系统在性能、稳定性和易用性方面获得高度评价。基于这一积极反馈,我们稳步扩展 Doris 集群规模,最终在效率与成本上实现大幅提升:

  • 整体效率:端到端分析任务耗时从 274 秒降至 47 秒,效率提升 82%,任务超时查杀比例从 1.3%骤降至 0.11%,降幅达 91%,彻底解决高峰期排队问题实现 0 排队,使分析师的工作不再因拥堵而中断,体验和生产力均有极大提升。

  • 集群成本:在同等资源成本下, Doris 仅以 1/3 的集群数量即可提供与 Greenplum 同等的服务能力,存储性能提升 200%。截至目前,已完成 百余台原 Greenplum 服务器的清退工作,以更少的硬件资源支撑了更高的计算与存储需求,实现年度硬件成本节约数百万元

从 0-1 数据平台建设经验

我们基于 Apache Doris 成功替换了 Greenplum,完成了从 0-1 的数据平台重构,覆盖架构设计、数据流转与业务协同的系统性工程。以下将围绕快速平滑迁移、异地多活容灾与全链路生态集成三个核心环节,展开具体实践。

01 快速迁移

为保障业务连续性与数据安全,我们开发了自动化迁移工具 SqlGlot,将大规模数据从原有 GP 集群迁移至 Doris 集群。整个过程历经半年,累计迁移 PB 级规模数据,全程业务无感知。

  • 表结构迁移:在表结构迁移阶段,团队从 GP 系统中导出表结构及相关元数据,借助 SqlGlot 工具实现字段映射与语法适配,并在此基础上完成分区构建与分桶策略设计,确保每个分桶数据量控制在 1G~3G 的合理范围内。该流程最终成功转换超过 20,000 张表,并保障了所有表的分区与分桶结构符合业务与性能要求。

  • 表数据迁移:我们通过分布式导出将 GP 数据并行迁移至 Doris 机器,并基于 Doris 官方推荐的 Stream Load 进行并发控制,以文件流式加载的方式高效导入数据至 Doris 集群。整个过程累计完成 PB 级规模数据迁移,稳定支持了 5000+ 次数据同步任务。

  • SQL 迁移:为解决因业务规模庞大、场景复杂而导致的官方工具语法支持不全的问题,我们基于 SqlGlot 并结合正则匹配能力,将 PostgreSQL SQL 高效转换为 Doris SQL。整个迁移流程包括“转换成功 → 执行成功 → 数据一致” ,累计完成约 47 万个 SQL 的转换,实现 95% 的执行成功率 与 92% 的数据一致率

02 异地双机房灾备

为保障数据安全并实现集群高可用,我们基于 Apache Doris 构建了异地双机房灾备架构,确保数据与服务具备跨机房容灾与双活能力。核心设计如下:

我们将所有 Doris 集群节点均匀部署于 A 与 B 两个异地机房,通过设置 tag.location 属性明确节点所属机房。用户账号按机房绑定,访问请求通过轮询机制自动分配,实现负载均衡(例如首次请求路由至 A 机房,第二次则路由至 B 机房)。建表时通过配置 location 参数,确保每张表在双机房各保留 2 个副本,从而达成数据异地双活与故障自动切换。

关键配置示例

  1. 设置节点机房标签

alter system modify backend ”BE1:9050" set ("tag.location" = "group_a");alter system modify backend ”BE2:9050" set ("tag.location" = "group_b");
复制代码

  1. 建表时指定双机房副本分布

CREATE TABLE ubevent (ts DATETIME, uid INT, ...) DUPLICATE KEY(ts) DISTRIBUTED BY HASH(uid) BUCKETS 10PROPERTIES ("replication_allocation" = "tag.location.group_b: 2, tag.location.group_a: 2");
复制代码

03 生态整合

为构建高效、稳定、易用的数据平台,我们还围绕 Apache Doris 进行系统性生态整合:

  • 计算引擎无缝集成:通过 Doris 官方提供的 Spark Connector 与 Flink Connector,实现了与现有 Spark、Flink 计算引擎的高效对接,保障了数据流水线稳定运行。

  • 运维体系化与自动化:集成 Prometheus、Grafana 及 Doris Manager,构建了覆盖监控、告警、管理与调优的自动化运维体系,全面提升集群稳定性与运维效率。

优化经验

为进一步提升数据平台的效率及资源利用率,在实际落地过程中,围绕集群、负载、存储等多维度总结了以下优化经验:

01 集群隔离

当前我们有多个 Doris 集群,为合理承接不同业务方的接入需求,我们主要依据业务成本与稳定性要求两大维度进行评估与路由。通常而言,稳定性越高,对应成本也越高。

新建集群时,稳定性最优,但相应成本也最高。为在成本与稳定性之间取得平衡,我们大多场景是基于 Workload Group 资源硬隔离方案,对 CPU 与内存进行资源组级别的隔离,有效减少不同业务负载间的资源竞争。若业务对稳定性的要求超出共享集群所能提供的范围,则仍需要通过新建独立集群来满足。

02 存储压力

在 Apache Doris 的落地与运维过程中,我们曾面临因业务快速增长带来的高达 80%-90% 的磁盘存储压力。针对这一问题,进行了一系列优化:

  • 控制表生命周期:部分业务或因对动态分区相关语法不熟悉,未主动采用该策略。为此,集成动态分区的参数配置,简化了开发难度,并提供统一注册入口,业务开发人员仅需选择是否开启、保留天数即可。

  • 修改压缩格式:将默认压缩算法从 LZ4 切换为 ZSTD。实测表明,存储空间平均节省约 50%,虽带来约 20%~30% 的 CPU 与内存负载上升,但整体 ROI 仍然较高。

  • 存储指标监控告警:为预防因误操作或异常行为导致的存储激增,建立了针对“人员”与“表”双维度的监控体系。环比分析业务人员数据占用趋势及单表每日增长量,可自动识别异常(如单日增长飙升至日常 10 倍),并及时触发告警及通知。

  • Hive 与 Doris 打通:在基于 Kerberos 认证的 Hive 环境中,对 Doris Hive Catalog 功能进行了二次开发,实现跨系统的直接数据访问,无需依赖 Flink 等同步工具,简化了架构并提升了数据使用效率。

03 负载均衡

为确保系统在负载高峰期的稳定运行,特别是应对异常 SQL 与大查询带来的资源压力,应对措施如下:

  • 双机房负载均衡:基于已有的异地双机房架构,通过轮询机制实现业务流量在 A 与 B 机房之间的自动分发:首个 SQL 请求路由至 A,次个请求则导向 B,以此循环,确保双机房负载均衡,避免单点资源过载。

  • SQL 参数限制:通过 enable_query_memory_overcommit = falseexec_mem_limit = 256 * 1024 * 1024 * 1024 等参数将最大占用内存限制为 256G,避免集群被打满,后续计划降至 60G。

  • Workload 资源队列动态调整:基于任务类型划分资源队列,配置 CPU 的软隔离和内存的硬隔离,并支持错峰调度。比如:例行任务通常在夜间执行,为其创建专门资源队列,数据分析等公共任务大多在白天执行,将配置更大的资源队列,随着白天/夜间需求的变化动态调整资源。此外,依据各队列负载设定并行度与并发数,控制任务排队时长。

  • 异常 SQL 拦截:实时识别与拦截异常 SQL,避免其影响 BE 节点稳定性。初期使用 Doris 内置正则规则进行拦截,但规则复杂导致 CPU 开销上升。为此,我们将拦截逻辑外移至平台层执行,以避免正则匹配及超大 JOIN 导致的 CPU 负载过高。

04 集群稳定性

随着集群规模不断扩大,保障 FE、BE 节点稳定性成为运维工作的核心挑战,为此,我们构建了以下保障体系:

  • 分层触达+全维度覆盖:根据不同指标优先级设置通知电话、短信、飞书提醒,P0 监控准确率 ≥80%;

  • 自动异常处理:为 FE 和 BE 的宕机重启设置了自动化处理方案,在识别到服务卡住时,系统会自动重启进程。此外,对于磁盘掉线,将自动下线故障盘并触发副本补齐。

我们同时采用对战分析、火焰图和日志查看等方法进行详细记录,以便后续调优。此外,编写了 SOP 手册,涵盖不同场景的应对措施,并进行了异常处理演练。

结束语

截至目前,我们已搭建 3 个基于 Doris 2.1.10 版本的线上集群,其中最大规模的集群达万 core 级别、上百 TB 内存和 PB 级磁盘。目前仍在扩容中,计划在年底前新增百余台 CN 节点和数十台 Mix 节点。未来,我们将重点关注并探索以下能力:

  • 存算分离:重点关注 Doris 3.X 版本的存储分离架构,推动落地实践。

  • 湖仓一体:全面打通数据湖与数据仓库,目前已小规模试点 Paimon;此外,针对数据外置场景,计划通过异步物化视图提升查询性能。

  • 智能物化视图探索:引入语义建模与 AI 智能分析,降低研发与业务沟通门槛,并对智能推荐与模板化方案进行探索与实践。

前言

在做图片相关功能时,有一个需求几乎绕不开:
用户拖动参数,图片实时变化。

比如:

  • 调整模糊强度
  • 改变对比度、饱和度
  • 预览滤镜效果,再决定是否应用

在 UIKit 时代,我们可能会用 UIImageView + CoreImage + GCD 硬撸。
但到了 SwiftUI,很多人第一反应是:

SwiftUI + CoreImage + 实时预览,这事靠谱吗?

答案是:靠谱,但得用对方式。

这篇文章就从一个最小可用 Demo开始,一步一步把实时滤镜预览这件事讲清楚。

先说结论:实时预览的关键点是什么?

在 SwiftUI 里做 CoreImage 实时预览,核心其实只有三点:

  1. 图片渲染要尽量轻
  2. 滤镜计算不能阻塞主线程
  3. UI 状态变化要最小化

如果你一上来就把所有滤镜计算都丢进 body
那基本等于在和 SwiftUI 的刷新机制正面硬刚。

一个最基础的目标效果

我们先定一个目标:

  • 显示一张原图
  • 拖动 Slider
  • 实时调整高斯模糊强度
  • 图片随着 Slider 连续变化

这是绝大多数滤镜编辑页的基础形态。

Step 1:准备 CoreImage 的基础组件

先把 CoreImage 的几个核心对象准备好:

import SwiftUI
import CoreImage
import CoreImage.CIFilterBuiltins

let context = CIContext()
let filter = CIFilter.gaussianBlur()

这里有两个细节值得注意:

  • CIContext 应该尽量复用
  • 不要在 body 里反复 new CIContext

CIContext 本身是重量级对象,频繁创建会直接拖垮性能。

Step 2:一个最简单的 SwiftUI 结构

我们先搭一个最基础的页面结构:

struct ContentView: View {
    @State private var intensity: Double = 0.5
    let image = UIImage(named: "example")!

    var body: some View {
        VStack {
            Image(uiImage: image)
                .resizable()
                .scaledToFit()

            Slider(value: $intensity)
                .padding()
        }
    }
}

到这一步,UI 是没问题的,但还没有任何滤镜逻辑

Step 3:把 CoreImage 滤镜接进来

关键思路是:
不要直接操作 UIImage,而是用 CIImage 作为中间态。

我们先写一个专门负责“生成滤镜图片”的方法:

func applyProcessing() -> UIImage {
    let beginImage = CIImage(image: image)
    filter.inputImage = beginImage
    filter.radius = Float(intensity * 20)

    guard let outputImage = filter.outputImage else {
        return image
    }

    if let cgimg = context.createCGImage(outputImage, from: beginImage!.extent) {
        return UIImage(cgImage: cgimg)
    }

    return image
}

这段代码做了几件事:

  1. UIImage 转成 CIImage
  2. 设置滤镜参数
  3. 通过 CIContext 渲染成 CGImage
  4. 再转回 UIImage

Step 4:把实时预览“接”到 SwiftUI 状态上

接下来是最关键的一步:
让 SwiftUI 在 Slider 变化时刷新图片,但不炸性能。

先引入一个新的状态:

@State private var processedImage: UIImage?

然后改造 body

var body: some View {
    VStack {
        Image(uiImage: processedImage ?? image)
            .resizable()
            .scaledToFit()

        Slider(value: $intensity)
            .padding()
            .onChange(of: intensity) { _ in
                processedImage = applyProcessing()
            }
    }
}

此时你已经可以看到:

  • Slider 一动
  • 图片跟着变
  • 滤镜是实时的

但——
这还不是一个“能上线”的写法。

性能问题从哪开始暴露?

当你快速拖动 Slider 时,会发现:

  • UI 有轻微卡顿
  • 真机上比模拟器更明显
  • 图片越大,问题越严重

原因也很直接:

滤镜计算跑在主线程。

Slider 的 onChange 本身就在主线程,
CoreImage 渲染又是 CPU / GPU 混合操作,
自然会影响 UI 响应。

Step 5:把滤镜计算移出主线程

一个简单、有效的方式是:
Task + MainActor 控制线程切换。

改造 onChange

.onChange(of: intensity) { _ in
    Task.detached {
        let output = applyProcessing()
        await MainActor.run {
            processedImage = output
        }
    }
}

这样做之后:

  • 滤镜计算在后台执行
  • UI 只负责展示结果
  • 拖动 Slider 明显顺滑很多

这一步,是“能不能实时预览”的分水岭。

再往前一步:为什么 SwiftUI 特别适合做这件事?

如果你用 UIKit 做过类似功能,会发现:

  • 手动管理线程
  • 手动刷新 ImageView
  • 状态和 UI 同步很痛苦

而 SwiftUI 的优势在于:

  • 状态驱动 UI
  • 图片只是状态的一个映射
  • 滤镜逻辑和 UI 逻辑可以完全解耦

你只需要保证一件事:

状态更新是轻的,计算是异步的。

一点真实项目里的经验总结

在真实项目中,我一般会遵守这几个原则:

  1. Slider 变化频繁时,必要时做节流
  2. 滤镜链尽量复用,不要每次 new
  3. 大图先 downscale 再做预览
  4. 最终导出时再跑一次“高质量渲染”

实时预览追求的是“看起来对”
而不是“每一帧都是最终质量”

总结

SwiftUI 并不是不适合做图像处理,
而是不能用同步思维去写异步计算

一旦你把:

  • CoreImage 的计算
  • SwiftUI 的状态刷新
  • 主线程和后台线程的职责

这三件事理顺了,
实时滤镜预览这件事,其实比 UIKit 时代要轻松得多。

度小满引入 Apache Doris 替换原有 Greenplum,实现整体查询效率提升 82%,与此同时,集群缩减 2/3、年省数百万的巨大效益。本文将分享度小满如何基于 Doris 从 0 到 1 构建超大规模数据分析平台,并围绕平滑迁移、异地多活容灾等方面,分享实践经验。

本文整理自度小满 Doris 数据库负责人汤斯在 Doris Summit 2025 中的演讲,并以演讲者第一视角进行叙述。

度小满金融(原百度金融)作为一家覆盖现代财富管理、支付、金融科技等多板块的科技公司,数据的分析处理对其极为重要,已经深度融入业务生命周期的每个环节,是进行风险控制、商业决策、用户体验优化及运营提效的基石。

随着业务高速发展,度小满原有基于 Greenplum 搭建的 OLAP 平台,逐渐暴露出三大痛点:

  • 规模与稳定性瓶颈:存储已接近饱和,扩容至百余台已接近硬件规模的承载上限,如果继续扩容,将面临更严重的稳定性挑战。

  • 性能与体验不佳:Greenplum SQL 查询执行速度慢,且经常出现 “计算时间远小于排队时间” 的情况,严重影响业务分析效率。

  • 缺失技术支持:当前使用的 Greenplum 6 版本技术架构已显得陈旧,并且 2024 年 Greenplum 宣布将停止开源,后续的技术支持与迭代升级将无法保障。

为了应对这些痛点,度小满金融迫切寻找更为高效、稳定且具备现代化技术架构的数据处理解决方案,以支持其未来的业务发展。

Apache Doris:高吞吐、快查询

面对日益增长的业务体量与复杂多变的分析需求,选用一个高效、可靠的数据库系统,已成为支撑业务稳健发展与快速创新的关键。Apache Doris 以其出色的性能表现与高度灵活的架构,成为众多场景下的优选方案。为深入验证其在海量数据与复杂分析场景中的能力,我们展开了一系列性能测试,关键结果如下:

  • 查询性能:在 1TB TPC-DS 标准测试集中, Apache Doris的查询速度约是 Greenplum 6 的 20-30 倍

  • 导入性能:在基于 Flink 写入的 TPS 测试中,基于单分片导入,压测最大 TPS 为:5000W/s

  • JSON 数据处理:针对新推出的 Variant JSON 数据类型,测试显示:存储 2-3 万 Key 时,其空间占用仅为普通 JSON 的 1/10 甚至更低,查询效率则提升至 10 倍以上

综上可知,Apache Doris 在写入吞吐、响应速度及存储效率上表现卓越,有力证明了其应对大规模、实时化、半结构化数据分析挑战的坚实技术基础。

基于 Apache Doris 的大规模数据分析平台

在上述详实的选型调研之后,我们决定采用 Apache Doris 替代原有 Greenplum 集群,构建超大规模数据分析平台。

为验证 Apache Doris 在真实业务场景中的表现,我们先进行了小范围试点,部署了少量 Doris 集群,并先行接入几个关键业务方。试点期间,系统在性能、稳定性和易用性方面获得高度评价。基于这一积极反馈,我们稳步扩展 Doris 集群规模,最终在效率与成本上实现大幅提升:

  • 整体效率:端到端分析任务耗时从 274 秒降至 47 秒,效率提升 82%,任务超时查杀比例从 1.3%骤降至 0.11%,降幅达 91%,彻底解决高峰期排队问题实现 0 排队,使分析师的工作不再因拥堵而中断,体验和生产力均有极大提升。

  • 集群成本:在同等资源成本下, Doris 仅以 1/3 的集群数量即可提供与 Greenplum 同等的服务能力,存储性能提升 200%。截至目前,已完成 百余台原 Greenplum 服务器的清退工作,以更少的硬件资源支撑了更高的计算与存储需求,实现年度硬件成本节约数百万元

从 0-1 数据平台建设经验

我们基于 Apache Doris 成功替换了 Greenplum,完成了从 0-1 的数据平台重构,覆盖架构设计、数据流转与业务协同的系统性工程。以下将围绕快速平滑迁移、异地多活容灾与全链路生态集成三个核心环节,展开具体实践。

01 快速迁移

为保障业务连续性与数据安全,我们开发了自动化迁移工具 SqlGlot,将大规模数据从原有 GP 集群迁移至 Doris 集群。整个过程历经半年,累计迁移 PB 级规模数据,全程业务无感知。

  • 表结构迁移:在表结构迁移阶段,团队从 GP 系统中导出表结构及相关元数据,借助 SqlGlot 工具实现字段映射与语法适配,并在此基础上完成分区构建与分桶策略设计,确保每个分桶数据量控制在 1G~3G 的合理范围内。该流程最终成功转换超过 20,000 张表,并保障了所有表的分区与分桶结构符合业务与性能要求。

  • 表数据迁移:我们通过分布式导出将 GP 数据并行迁移至 Doris 机器,并基于 Doris 官方推荐的 Stream Load 进行并发控制,以文件流式加载的方式高效导入数据至 Doris 集群。整个过程累计完成 PB 级规模数据迁移,稳定支持了 5000+ 次数据同步任务。

  • SQL 迁移:为解决因业务规模庞大、场景复杂而导致的官方工具语法支持不全的问题,我们基于 SqlGlot 并结合正则匹配能力,将 PostgreSQL SQL 高效转换为 Doris SQL。整个迁移流程包括“转换成功 → 执行成功 → 数据一致” ,累计完成约 47 万个 SQL 的转换,实现 95% 的执行成功率 与 92% 的数据一致率

02 异地双机房灾备

为保障数据安全并实现集群高可用,我们基于 Apache Doris 构建了异地双机房灾备架构,确保数据与服务具备跨机房容灾与双活能力。核心设计如下:

我们将所有 Doris 集群节点均匀部署于 A 与 B 两个异地机房,通过设置 tag.location 属性明确节点所属机房。用户账号按机房绑定,访问请求通过轮询机制自动分配,实现负载均衡(例如首次请求路由至 A 机房,第二次则路由至 B 机房)。建表时通过配置 location 参数,确保每张表在双机房各保留 2 个副本,从而达成数据异地双活与故障自动切换。

关键配置示例

  1. 设置节点机房标签

alter system modify backend ”BE1:9050" set ("tag.location" = "group_a");alter system modify backend ”BE2:9050" set ("tag.location" = "group_b");
复制代码

  1. 建表时指定双机房副本分布

CREATE TABLE ubevent (ts DATETIME, uid INT, ...) DUPLICATE KEY(ts) DISTRIBUTED BY HASH(uid) BUCKETS 10PROPERTIES ("replication_allocation" = "tag.location.group_b: 2, tag.location.group_a: 2");
复制代码

03 生态整合

为构建高效、稳定、易用的数据平台,我们还围绕 Apache Doris 进行系统性生态整合:

  • 计算引擎无缝集成:通过 Doris 官方提供的 Spark Connector 与 Flink Connector,实现了与现有 Spark、Flink 计算引擎的高效对接,保障了数据流水线稳定运行。

  • 运维体系化与自动化:集成 Prometheus、Grafana 及 Doris Manager,构建了覆盖监控、告警、管理与调优的自动化运维体系,全面提升集群稳定性与运维效率。

优化经验

为进一步提升数据平台的效率及资源利用率,在实际落地过程中,围绕集群、负载、存储等多维度总结了以下优化经验:

01 集群隔离

当前我们有多个 Doris 集群,为合理承接不同业务方的接入需求,我们主要依据业务成本与稳定性要求两大维度进行评估与路由。通常而言,稳定性越高,对应成本也越高。

新建集群时,稳定性最优,但相应成本也最高。为在成本与稳定性之间取得平衡,我们大多场景是基于 Workload Group 资源硬隔离方案,对 CPU 与内存进行资源组级别的隔离,有效减少不同业务负载间的资源竞争。若业务对稳定性的要求超出共享集群所能提供的范围,则仍需要通过新建独立集群来满足。

02 存储压力

在 Apache Doris 的落地与运维过程中,我们曾面临因业务快速增长带来的高达 80%-90% 的磁盘存储压力。针对这一问题,进行了一系列优化:

  • 控制表生命周期:部分业务或因对动态分区相关语法不熟悉,未主动采用该策略。为此,集成动态分区的参数配置,简化了开发难度,并提供统一注册入口,业务开发人员仅需选择是否开启、保留天数即可。

  • 修改压缩格式:将默认压缩算法从 LZ4 切换为 ZSTD。实测表明,存储空间平均节省约 50%,虽带来约 20%~30% 的 CPU 与内存负载上升,但整体 ROI 仍然较高。

  • 存储指标监控告警:为预防因误操作或异常行为导致的存储激增,建立了针对“人员”与“表”双维度的监控体系。环比分析业务人员数据占用趋势及单表每日增长量,可自动识别异常(如单日增长飙升至日常 10 倍),并及时触发告警及通知。

  • Hive 与 Doris 打通:在基于 Kerberos 认证的 Hive 环境中,对 Doris Hive Catalog 功能进行了二次开发,实现跨系统的直接数据访问,无需依赖 Flink 等同步工具,简化了架构并提升了数据使用效率。

03 负载均衡

为确保系统在负载高峰期的稳定运行,特别是应对异常 SQL 与大查询带来的资源压力,应对措施如下:

  • 双机房负载均衡:基于已有的异地双机房架构,通过轮询机制实现业务流量在 A 与 B 机房之间的自动分发:首个 SQL 请求路由至 A,次个请求则导向 B,以此循环,确保双机房负载均衡,避免单点资源过载。

  • SQL 参数限制:通过 enable_query_memory_overcommit = falseexec_mem_limit = 256 * 1024 * 1024 * 1024 等参数将最大占用内存限制为 256G,避免集群被打满,后续计划降至 60G。

  • Workload 资源队列动态调整:基于任务类型划分资源队列,配置 CPU 的软隔离和内存的硬隔离,并支持错峰调度。比如:例行任务通常在夜间执行,为其创建专门资源队列,数据分析等公共任务大多在白天执行,将配置更大的资源队列,随着白天/夜间需求的变化动态调整资源。此外,依据各队列负载设定并行度与并发数,控制任务排队时长。

  • 异常 SQL 拦截:实时识别与拦截异常 SQL,避免其影响 BE 节点稳定性。初期使用 Doris 内置正则规则进行拦截,但规则复杂导致 CPU 开销上升。为此,我们将拦截逻辑外移至平台层执行,以避免正则匹配及超大 JOIN 导致的 CPU 负载过高。

04 集群稳定性

随着集群规模不断扩大,保障 FE、BE 节点稳定性成为运维工作的核心挑战,为此,我们构建了以下保障体系:

  • 分层触达+全维度覆盖:根据不同指标优先级设置通知电话、短信、飞书提醒,P0 监控准确率 ≥80%;

  • 自动异常处理:为 FE 和 BE 的宕机重启设置了自动化处理方案,在识别到服务卡住时,系统会自动重启进程。此外,对于磁盘掉线,将自动下线故障盘并触发副本补齐。

我们同时采用对战分析、火焰图和日志查看等方法进行详细记录,以便后续调优。此外,编写了 SOP 手册,涵盖不同场景的应对措施,并进行了异常处理演练。

结束语

截至目前,我们已搭建 3 个基于 Doris 2.1.10 版本的线上集群,其中最大规模的集群达万 core 级别、上百 TB 内存和 PB 级磁盘。目前仍在扩容中,计划在年底前新增百余台 CN 节点和数十台 Mix 节点。未来,我们将重点关注并探索以下能力:

  • 存算分离:重点关注 Doris 3.X 版本的存储分离架构,推动落地实践。

  • 湖仓一体:全面打通数据湖与数据仓库,目前已小规模试点 Paimon;此外,针对数据外置场景,计划通过异步物化视图提升查询性能。

  • 智能物化视图探索:引入语义建模与 AI 智能分析,降低研发与业务沟通门槛,并对智能推荐与模板化方案进行探索与实践。