k6 v0.54.0 技术解析:实验性 CSV 模块、云本地执行模式与 ECMAScript 新特性
【免费下载链接】k6A modern load testing tool, using Go and JavaScript项目地址: https://gitcode.com/GitHub_Trending/k6/k6
本文围绕 Grafana k6 v0.54.0 版本的核心变更展开,重点剖析新增的k6/experimental/csv实验性 CSV 数据处理模块、k6 cloud run --local-execution本地执行云测试模式、基于 Sobek 引擎升级带来的 ECMAScript 新特性,以及 gRPC、浏览器、WebSocket 模块的若干改进。读完本文,你将掌握 CSV 模块两套 API(csv.parse与csv.Parser)的完整用法与底层实现原理,理解云命令体系的重构方向,并能快速评估这些 Breaking Changes 对自己脚本与扩展的影响。
版本概览
k6 v0.54.0 是一次功能密度很高的发布,主要包含以下内容:
- 新增实验性 CSV 模块,提供 Go 底层实现的高效 CSV 解析与流式读取能力;
- 新增
k6 cloud下的本地执行(--local-execution)与脚本上传(k6 cloud upload)命令; - Sobek(k6 使用的 Go 版 ECMAScript 实现)升级,带来数字分隔符、
BigInt、RegExp dotAll、ES2023 数组方法等新特性; - 更新了 logo 与终端启动横幅的品牌形象;
- 同时包含 4 项 Breaking Changes 与大量 UX 改进、Bug 修复和内部维护工作。
Breaking Changes:升级前必须关注的四项变更
math/big.Int到bigint的类型映射调整
本次发布(对应 PR #3913)改变了 Go 类型math/big.Int在 k6 JavaScript 运行时中的映射方式,现在它与新增的 JavaScriptBigInt语义保持一致,统一映射为bigint类型。这对依赖旧映射行为(例如将大整数当作普通 number 使用)的自定义扩展或脚本属于破坏性变更,发布说明中明确提示扩展作者与用户需要检查相关用法。
lib.Min与lib.Max从 Go API 中移除
PR #3922 移除了lib.Min和lib.Max两个 Go 函数。这是纯 Go 层面的 API 调整,主要影响依赖这两个函数的自定义 k6 扩展,普通 JS 脚本不受影响。如果你维护扩展,需要将相关调用替换为 Go 标准库或自定义实现。
k6/experimental/timers模块正式移除
k6/experimental/timers自 v0.53.0 起已被标记废弃(见 v0.53.0 发布说明),在 v0.54.0 中正式移除。原因是定时器 API(setTimeout、setInterval、setImmediate、clearTimeout、clearInterval、clearImmediate)已经全局可用,不再需要任何 import。从源码看,稳定的k6/timers模块本身也只是把全局对象上的定时器函数重新暴露出来——timers.go 直接读取Runtime().GlobalObject()上的setTimeout、setInterval等成员。因此迁移路径很简单:删除import ... from 'k6/experimental/timers'语句,直接使用全局函数即可。
k6/experimental/websockets的binaryType默认值改为"blob"
PR #3944 将k6/experimental/websockets中binaryType的默认值从"arraybuffer"调整为"blob",使其符合 WebSocket 规范。这意味着:未显式设置binaryType的脚本,接收二进制消息时得到的将是Blob对象而非ArrayBuffer。如果你的脚本依赖旧默认值,需要在创建 WebSocket 时显式声明ws.binaryType = "arraybuffer"。
新特性:实验性 CSV 模块k6/experimental/csv
为什么需要它
此前 k6 用户解析 CSV 只能依赖纯 JavaScript 方案(如 papaparse)。这类方案存在两个痛点:解析完全跑在 JS 运行时中,大文件速度慢;且数据只能放在单个 VU 的 JS 堆里,多 VU 间无法共享。v0.54.0 新增的实验性 CSV 模块(对应 PR #3743)用 Go 实现底层解析,直接对接k6/experimental/fs的文件句柄,并提供两种互补的解析策略:
| API | 模式 | 内存占用 | 速度 | 适用场景 |
|---|---|---|---|---|
csv.parse() | 全量解析为 SharedArray | 较高(整个文件常驻共享内存) | 快(绕过大部分 JS 运行时) | 追求性能、数据可整体装载 |
csv.Parser(next()) | 逐行流式读取 | 低(每次只保留一行) | 相对慢(每行有读取开销) | 内存敏感、需要精细控制解析过程 |
两种方式都通过fs.open获得文件句柄,因此天然支持跨 VU 共享数据。
csv.parse():一次性全量解析为 SharedArray
csv.parse(file, options)接收一个由k6/experimental/fs的open()返回的文件对象,在init 阶段一次性消费整个文件,返回解析后的SharedArray(来自k6/data模块的数据结构)。仓库中的完整示例见 examples/experimental/csv-parse.js:
import { open } from 'k6/experimental/fs' import csv from 'k6/experimental/csv' import { scenario } from 'k6/execution' export const options = { iterations: 10, } // 在 init 阶段打开并一次性解析整个文件 const file = await open('data.csv'); const csvRecords = await csv.parse(file, { delimiter: ',' }) export default async function() { // csvRecords 是一个 SharedArray,每个元素是一条记录(字段数组) // 借助 scenario.iterationInTest 可以按迭代顺序取记录 console.log(csvRecords[scenario.iterationInTest]) }配套的示例数据文件为 examples/experimental/data.csv。注意示例中使用了顶层 await(await open(...)、await csv.parse(...)),这是模块文档推荐的 init 阶段写法。
代价与权衡:csv.parse在测试的初始化阶段就把整个文件读入内存,因此大文件会拉长启动时间并抬高内存占用。它适合"性能优先于内存"的场景。从源码实现看,module.go 中的Parse方法会校验参数、构造内部Reader,然后通过data模块的NewSharedArrayFrom在后台 goroutine 中构建共享数组,最终以 Promise 形式返回。
csv.Parser:逐行流式解析
new csv.Parser(file, options)创建一个流式解析器,通过parser.next()逐行读取。next()返回一个迭代器风格的对象:done表示是否还有更多行,value是该行各字段组成的数组。仓库示例见 examples/experimental/csv-parser.js:
import { open } from 'k6/experimental/fs' import csv from 'k6/experimental/csv' export const options = { iterations: 10, } const file = await open('data.csv'); const parser = new csv.Parser(file); export default async function() { const { done, value } = await parser.next(); if (done) { throw new Error("No more rows to read"); } // value 是字符串数组,每个元素是 CSV 记录中的一个字段 console.log(done, value); }源码中 module.go 的NewParser构造函数做了几处关键约束:
- 必须在 init 上下文调用:
csv.Parser构造函数会检查mi.vu.State(),在测试执行阶段(VU 运行时)调用会直接抛错; - 第一个参数必须是
fs.File实例,否则抛出类型错误; next()内部在独立 goroutine 中调用底层Reader.Read(),遇到io.EOF时返回{done: true},从而形成类似 JS 迭代器的语义。
解析选项详解
CSV 模块的解析行为可通过 options 对象定制。从源码 reader.go 与 module.go 可以整理出完整的选项表:
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
delimiter | string | , | 字段分隔符,必须是单个字符,否则报错 |
skipFirstLine | boolean | false | 是否跳过首行(如表头) |
fromLine | number | 未设置 | 从第 N 行开始读取(0 起) |
toLine | number | 未设置 | 读到第 N 行停止(含该行) |
asObjects | boolean | false | 将每行映射为对象(键为表头列名,值为对应字段值),此时首行被当作表头 |
选项之间存在若干互斥/校验规则(validateOptions):
asObjects与skipFirstLine不能同时开启;asObjects与fromLine > 0不能同时使用;fromLine、toLine必须大于等于 0,且fromLine必须小于toLine;- 开启
asObjects时若文件没有表头,读取会报错;某行字段数与表头列数不一致时同样报错。
底层实现原理
csv.Parser与csv.parse共享同一套 Go 实现。核心是一个包装encoding/csv.Reader的 Reader:
NewReaderFrom在构造阶段完成"跳过首行""跳到 fromLine"等预处理,使Read()调用路径尽量简单;Read()逐行调用底层csv.Reader.Read(),并用原子计数器currentLine跟踪行号以支持toLine截断;- 当
asObjects开启时,Read()会把字段数组映射为map[string]string(列名 → 值)。
共享内存的关键在 data 模块。csv.parse之所以能返回 SharedArray,是因为它复用了k6/data模块的 NewSharedArrayFrom ——该函数接收实现了RecordReader接口(即Read() (any, error))的对象,在后台把每条记录 JSON 序列化后装入共享数组。注释中明确说明这样做的动机是绕过整套 JS 运行时机制,避免在 JS 层填充共享数组带来的巨大性能开销。同时 module.go 会用"文件路径 + 解析选项"计算一个 SHA-256 哈希作为共享数组名(csv.parse.<hash>),保证同一文件同一配置的解析结果在多个 VU 间只实例化一次。
此外,CSV 模块的入口 module.go 导出两个命名成员:parse(函数)和Parser(构造函数);文件句柄则来自k6/experimental/fs模块,其File结构体通过公开的 ReadSeekStater 接口向其他模块暴露io.Reader/io.Seeker能力,且该接口字段在 JS 侧被隐藏(js:"-"),避免泄漏实现细节——这正是 CSV 模块能在 Go 层直接读取文件的原因。
k6 cloud命令体系演进
v0.53.0 开始把云相关功能收拢到k6 cloud子命令下,v0.54.0 继续深化这一方向,新增了本地执行与文件上传两个能力。
k6 cloud run --local-execution:云测试本地执行
新引入的--local-execution标志允许你在本地机器上执行测试,同时把指标发送到 Grafana Cloud k6:
k6 cloud run --local-execution script.js关键行为(依据 cloud_run.go 的实现):
- 默认情况下,
--local-execution仍会把测试归档(archive)上传到 Grafana Cloud k6;若想跳过上传,加--no-archive-upload; - 目前该模式的功能与
k6 run -o cloud基本等价,后者已被标记为废弃(但短期内不会移除);未来--local-execution会获得k6 run -o cloud不具备的新能力。
本地执行模式专属的其他标志(均只在--local-execution下有效,混用会报InvalidConfig错误):
| 标志 | 说明 |
|---|---|
--linger | 测试结束后保持 API server 存活 |
--no-usage-report | 不发送匿名使用统计 |
--no-archive-upload | 不把测试归档上传到云服务 |
--no-cloud-secrets | 不自动配置云 secret source |
--no-cloud-logs | 不把日志推送到云端 |
同时,--local-execution与--exit-on-running、--show-logs互斥(见 preRun)。
从实现看,k6 cloud run在本地执行模式下会复用cmdRun(即k6 run的执行链路):通过loadAndConfigureLocalTest加载测试,用createCloudTest在云端创建测试运行,然后以本地 controller 驱动执行;配置层面强制把输出设为cloud(getCloudRunLocalExecutionConfig 中out := []string{"cloud"})。这解释了"本地跑、云端看指标"的语义。
k6 cloud upload:上传测试文件到云端
新增的k6 cloud upload命令取代了k6 cloud --upload-only标志(后者标记为废弃)。旧实现中uploadOnly的行为是"上传归档后停止,不启动测试运行"——从 cloud.go 可以看到,上传完成后它会打印测试 URL 与 "Archived" 状态并直接返回。新命令把这一流程独立成一级子命令,属于云服务文件处理链路的一次整理。
配套的 UX 改进
k6 cloud login增加了 token 校验(PR #3930),登录后能立即获得反馈;- 云测试运行中阈值失败时的进程退出码得到修正(PR #3876、#3923),CI 场景下可以依赖退出码判断云测试是否达标;
- 修复了
cloud命令不出现在k6帮助文本中的问题(PR #3901)。
gRPC 模块:三项来自社区贡献的更新
discardResponseMessage:丢弃服务端响应消息
新增的调用参数discardResponseMessage(PR #3877、#3820)允许丢弃从服务端接收到的消息:
const resp = client.invoke('main.RouteGuide/GetFeature', req, {discardResponseMessage: true});它的价值在于:当你不关心响应内容(比如只压测请求吞吐)时,丢弃消息可以减少内存占用和 GC 压力,从而降低测试机负载、让测试结果更稳定。从源码看,参数解析位于 params.go,invoke会把它透传到底层 gRPC 调用(client.go),相关行为在 client_test.go 中有InvokeDiscardResponseMessage、AsyncInvokeDiscardResponseMessage等测试用例覆盖。感谢贡献者 @lzakharov。
stream 回调新增meta参数
gRPC 流式调用的回调(如'data'事件)新增第二个参数meta(PR #3801),用于携带原始事件发生的时间戳(例如消息被接收的时刻):
let stream = new grpc.Stream(client, "main.FeatureExplorer/ListFeatures") stream.on('data', function (data, meta) { // 打印消息接收时刻的时间戳 call(meta.ts); });源码层面,stream.go 的queueMessage在收到消息时记录now := time.Now(),构造一个元数据对象并写入ts字段(timestampMetadata = "ts",Unix 秒),随后连同消息一起传给监听器。这使得回调里可以直接拿到接收时间,而无需在 JS 侧自行计时。感谢贡献者 @cchamplin。
gRPC reflection 允许缺失 file descriptors
PR #3871 放宽了 gRPC reflection 的使用限制:当服务端返回的 file descriptors 不完整时不再报错,提升了与真实世界 gRPC 服务的兼容性。感谢贡献者 @Lordnibbler。
Sobek 升级:新的 ECMAScript 特性
v0.54.0 更新了 Sobek(k6 的 Go 语言 ECMAScript 引擎实现,对应 PR #3899、#3925、#3913),以下新语法与 API 现在可直接在 k6 脚本中使用:
数字字面量分隔符——提高大数字可读性:
const billion = 1_000_000_000BigInt——表示超出 number 原始类型精度范围的整数:
const huge = BigInt(9007199254740991);注意:v0.54.0 之前 Go 的math/big.Int映射到其他类型,因此本次bigint类型映射调整对依赖旧行为的扩展是破坏性的(见上文 Breaking Changes)。
RegExp dotAll(s标志)——让.可以匹配换行符:
const str1 = "bar\nexample foo example"; const regex1 = /bar.example/s; console.log(regex1.dotAll); // trueES2023 数组方法:Array.prototype.with、toSpliced、toReversed、toSorted。这些是不可变方法,返回新数组而不修改原数组,例如:
const arr = [3, 1, 2]; const sorted = arr.toSorted(); // [1, 2, 3],arr 不变 const reversed = arr.toReversed(); // [2, 1, 3] const spliced = arr.toSpliced(1, 1); // 从索引 1 删 1 个元素 const withVal = arr.with(0, 9); // [9, 1, 2]感谢 @shiroyk 同时贡献了数组方法与 BigInt 支持。顺带一提,自 v0.53.0 起 k6 已原生支持 ESM(见 v0.53.0 发布说明),这些语言特性在原生 ESM 语境下工作得更加一致。
浏览器模块:新增setChecked方法
此前设置复选框/单选框的状态需要分别调用check()与uncheck()。v0.54.0 新增了setChecked方法(对应 xk6-browser PR #1403),用一个布尔参数即可完成两种状态切换:
await page.setChecked('#checkbox', true); // 勾选复选框 await page.setChecked('#checkbox', false); // 取消勾选该方法同时覆盖了Page、Frame、ElementHandle、Locator四类对象。从仓库源码可以确认其落地情况:映射层在 page_mapping.go、frame_mapping.go、element_handle_mapping.go、locator_mapping.go 中均有对应绑定,核心动作则实现在 element_handle.go 的setChecked方法中,走的是与check/uncheck相同的指针动作(pointer action)链路。
WebSocket 模块向规范靠拢
send支持 ArrayBufferViews
PR #3944 让k6/experimental/websockets的send方法可以直接接收Uint8Array等 ArrayBufferViews,不再需要手动取出.buffer传入。这提升了与基于 WebSocket API 的 JS 库的互操作性(感谢 @pixeldrew 反馈)。
readyState真正成为 number
由于底层 JS 引擎(goja/Sobek)内部机制的原因,readyState在 JS 视角下并不是一个严格意义上的 number,导致一些库的互操作出现异常。PR #3972 修复了这一问题,现在readyState是标准的数值类型(感谢 @dougw-bc 反馈)。结合前文提到的binaryType默认值变更,这一系列改动都是k6/experimental/websockets走向规范兼容、进而转正(stable)的铺垫。
使用数据采集机制重做
PR #3917 与 #3951 重构了 k6 内部的使用数据(usage)采集机制。旧的采集系统只能收集表层信息(VU 数量、k6 版本、加载了哪些内部模块),且结构僵硬、难以扩展。重做后新增了以下上报项:
- 云测试运行标识:使用 Grafana Cloud 时上报当前测试运行 ID,便于把云端运行与本地运行区分开,未来可针对使用实验性模块的云用户发出警告;
- 解析文件数与
.ts文件数:帮助团队判断用户项目规模以及 TypeScript 支持的采用情况; require的使用:原生 ESM 支持落地后,require/CommonJS 增加了维护复杂度,团队需要评估未来移除require的可行性(考虑到其他运行时仍在支持,这大概率是多年后的事);global的使用:用于判断是否可以移除extendedcompatibility-mode 与base模式之间的差异(相关讨论见 v0.53.0 发布说明中关于global别名globalThis的说明)。
同时,usage 报告发送的端点也做了更新(PR #3945)。这些数据仅用于产品决策,可通过--no-usage-report关闭(该标志在本地执行模式下同样可用)。
其他 UX 改进与 Bug 修复
值得留意的改进包括:
SetupTimeout选项增加参数校验(PR #3898);Rate指标不再使用容易混淆的✓/✗符号,改为{x} out of {y}形式(PR #3765);- 修复
options为nil(如从模块导出但未真正导出)时的 panic(PR #3947); - 修复事件系统丢事件导致无限挂起的问题(PR #3968);
- 浏览器模块修复了 iframe attach panic、
page.reload处理 null 响应、click空指针、goroutine 无限等待、browser.close中止 CDP close 请求等一系列问题。
维护与内部改进
工程层面,v0.54.0 将go.mod切换到 Go 1.21 并引入 toolchain 机制(PR #3915),CI 与 Dockerfile 同步升级到 Go 1.23 + Alpine 3.20(PR #3938、#3939);golangci-lint 升级到 1.60.1 并新增fatcontext、canonicalheader两个 linter(PR #3900、#3902);新增testutils.MakeMemMapFs测试辅助工具,方便在测试中模拟文件系统(PR #3933);内部删除了NetTrail类型以简化迭代与数据传输指标的内部实现(PR #3908)。
路线图与迁移提示
v0.52.0 中浏览器模块已从实验性转正为稳定的k6/browser,并全面采用 Async API。仍在k6/experimental/browser上的脚本需要迁移到新模块(详见 v0.53.0 发布说明中的迁移指引与 v0.52.0 相关说明)。结合 v0.53.0 的预告,k6/experimental/tracing计划在 v0.55.0 移除、StatsD 输出也将在 v0.55.0 移除,建议相关用户尽早规划迁移到 jslib 或对应扩展。
小结
v0.54.0 在三个方向上都有实质性进展:数据层上,实验性 CSV 模块用 Go 原生解析 + SharedArray 共享内存解决了大数据集压测的解析瓶颈;云服务层上,--local-execution与k6 cloud upload让"本地执行 + 云端观测"的混合工作流成为一等公民;语言层上,Sobek 升级持续拉近 k6 与标准 ECMAScript 的距离。与此同时,四项 Breaking Changes(bigint类型映射、lib.Min/lib.Max移除、timers 模块移除、WebSocketbinaryType默认值)提醒升级用户:在动手升级前,检查自定义扩展与脚本中涉及大整数、定时器 import 和 WebSocket 二进制消息的代码,可以最大程度降低迁移成本。
【免费下载链接】k6A modern load testing tool, using Go and JavaScript项目地址: https://gitcode.com/GitHub_Trending/k6/k6
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考