第一章:Go cgo调用崩溃无堆栈?启用CGO_CFLAGS=”-g” + GODEBUG=cgocheck=2 精准回溯C层异常
当 Go 程序通过 cgo 调用 C 代码发生崩溃(如 SIGSEGV、SIGABRT)时,runtime/debug.PrintStack() 或默认 panic 输出往往仅显示 Go 栈帧,C 函数调用链完全丢失,导致定位困难。根本原因在于:默认编译的 C 代码未包含调试信息,且 cgo 运行时检查被弱化,掩盖了内存越界、指针误用等底层错误。
启用 C 代码调试符号
在构建前设置环境变量,强制 GCC 生成 DWARF 调试信息:
export CGO_CFLAGS="-g -O0" # -O0 避免优化干扰行号映射
go build -o myapp .
✅
-g是关键:使gdb/dlv能解析 C 函数名、源码行与变量;
⚠️ 避免-O2等优化标志,否则栈帧可能被内联或消除,破坏回溯完整性。
开启严格 cgo 运行时检查
设置 GODEBUG=cgocheck=2 可捕获常见 C 交互违规:
GODEBUG=cgocheck=2 ./myapp
该模式会检查:
- Go 指针非法传递给 C(如逃逸到 C 的栈上 Go 变量)
- C 返回的指针被 Go 误认为可寻址内存
- C 函数中调用 Go 导出函数时的 goroutine 绑定违规
一旦触发,将 panic 并打印清晰错误(如 cgo result has Go pointer),而非静默崩溃。
调试流程整合示例
| 步骤 | 操作 | 效果 |
|---|---|---|
| 1. 构建 | CGO_CFLAGS="-g -O0" GODEBUG=cgocheck=2 go build |
同时注入调试符号与运行时防护 |
| 2. 运行 | ./myapp |
崩溃时获得含 C 函数名的完整栈(runtime.cgocall → my_c_func → malloc) |
| 3. 深度分析 | dlv exec ./myapp -- --arg → bt |
交互式查看混合 Go/C 栈帧、寄存器及局部变量 |
启用二者后,原本“无堆栈”的崩溃将呈现类似以下结构:
panic: runtime error: invalid memory address or nil pointer dereference
...
runtime.cgocall(0x4b5a80, 0xc000047f20)
my_c_func(0x0) at example.c:12 ← 关键 C 行号与函数名出现!
这为修复 free() 后使用、数组越界等 C 层缺陷提供了直接依据。
第二章:cgo异常追踪的核心机制与底层原理
2.1 cgo调用栈断裂的根本原因:运行时切换与符号剥离分析
cgo 调用栈断裂并非偶然,而是 Go 运行时与 C 运行时(如 libc)在执行上下文切换和调试信息管理上的根本性差异所致。
运行时切换导致的栈帧不连续
当 C.xxx() 被调用时,Go goroutine 暂停调度,控制权移交至 C 的原生线程栈;此时 Go 的栈跟踪器(runtime.Callers)无法跨越 mcall/gogo 边界解析 C 帧。
符号剥离加剧诊断困难
Go 编译默认启用 -ldflags="-s -w"(剥离符号表与 DWARF),而 C 编译器(如 gcc)生成的 .debug_frame 或 .eh_frame 在链接阶段常被静默丢弃:
# 查看二进制中是否残留调试节
readelf -S myapp | grep -E "\.(debug|eh)_frame"
# 输出为空 → 栈回溯无帧指针元数据支撑
关键差异对比
| 维度 | Go 运行时栈 | C 运行时栈 |
|---|---|---|
| 栈管理 | 分段栈 + 协程调度 | 连续栈 + OS 线程 |
| 帧指针约定 | 隐式(SP/PC 推导) | 显式(%rbp 链式) |
| DWARF 支持 | 有限(仅 Go 函数) | 完整(需未 strip) |
// 示例:cgo 调用链中栈信息丢失点
/*
#cgo LDFLAGS: -ldl
#include <dlfcn.h>
void crash_me() { *(int*)0 = 1; }
*/
import "C"
func trigger() {
C.crash_me() // panic: runtime error: invalid memory address...
// 此处 traceback 将缺失 C.crash_me 及其调用者帧
}
上述调用触发 panic 后,runtime.Stack() 输出中 C.crash_me 不可见——因 Go 的 pcvalue 查找逻辑不识别 C 的 .eh_frame 编码格式,且 runtime.gentraceback 未实现对 libgcc unwind API 的桥接。
2.2 CGO_CFLAGS=”-g” 如何恢复C代码调试信息及实测对比验证
CGO 默认剥离 C 编译的调试符号,导致 gdb 或 dlv 无法单步进入 C 函数。通过设置环境变量 CGO_CFLAGS="-g" 可强制 GCC 生成 DWARF 调试信息。
调试开关对比效果
| 场景 | CGO_CFLAGS 值 | objdump -g main.o 输出 |
gdb 可见 C 函数栈帧 |
|---|---|---|---|
| 默认 | (空) | 无 .debug_* 段 |
❌ |
| 启用 | -g |
包含 .debug_line, .debug_info |
✅ |
编译与验证命令
# 编译时注入调试标志
CGO_CFLAGS="-g" go build -o app_with_debug main.go
# 验证调试段存在
readelf -S app_with_debug | grep debug
-g参数指示 GCC 生成完整调试信息(含源码行号、变量作用域),不增加运行时开销,仅增大二进制体积约15–30%。
调试流程示意
graph TD
A[Go 源码调用 C 函数] --> B[CGO_CFLAGS=-g 编译 C 部分]
B --> C[链接生成含 DWARF 的可执行文件]
C --> D[gdb ./app_with_debug → break my_c_func]
2.3 GODEBUG=cgocheck=2 的内存越界检测逻辑与崩溃现场捕获实践
GODEBUG=cgocheck=2 启用最严格的 CGO 跨边界内存访问检查,不仅校验 Go 指针是否非法传递给 C(如指向栈或逃逸失败的变量),还动态追踪所有 C 函数调用中对 Go 分配内存的读写地址合法性。
检测触发条件
- Go 切片/字符串底层指针被传入 C 函数后,C 代码越界读写(如
memcpy(dst, src, n)中n > len(slice)) - C 函数修改已释放的 Go 内存(如
free()后再次write())
典型崩溃复现代码
// cgo_test.c
#include <string.h>
void unsafe_copy(char *dst, const char *src, int n) {
memcpy(dst, src, n); // 若 n > 实际分配长度,则触发 cgocheck=2 panic
}
// main.go
/*
#cgo LDFLAGS: -L. -ltest
#include "cgo_test.h"
*/
import "C"
import "unsafe"
func main() {
s := make([]byte, 5)
C.unsafe_copy(
(*C.char)(unsafe.Pointer(&s[0])),
(*C.char)(unsafe.Pointer(&s[0])),
C.int(10), // ⚠️ 越界:申请5字节却写10字节
)
}
逻辑分析:
cgocheck=2在每次C.调用前后插入运行时钩子,比对传入指针的基地址、长度与 Go runtime 记录的mspan元信息。参数n=10超出s对应 span 的sizeclass管辖范围,立即触发fatal error: cgo argument has Go pointer to unexported var并打印完整 goroutine stack。
检测能力对比表
| 检查级别 | 检测内容 | 是否捕获越界写 |
|---|---|---|
cgocheck=0 |
完全禁用 | ❌ |
cgocheck=1 |
仅检查指针是否来自 Go 堆 | ❌ |
cgocheck=2 |
校验指针+长度在 span 内有效性 | ✅ |
graph TD
A[Go 代码调用 C 函数] --> B{cgocheck=2 启用?}
B -->|是| C[获取参数指针 & 长度]
C --> D[查询 runtime.mspan]
D --> E{地址+长度 ∈ span?}
E -->|否| F[panic + dump goroutine]
E -->|是| G[执行原 C 函数]
2.4 Go runtime 与 libc 交互中的信号传递链路还原(SIGSEGV/SIGABRT)
Go runtime 并不直接依赖 libc 的信号处理机制,而是通过 sigaction 系统调用接管关键信号(如 SIGSEGV、SIGABRT),实现自己的信号分发器。
信号拦截入口点
// src/runtime/signal_unix.go 中的初始化逻辑
func sigtramp() // 汇编桩,作为内核回调入口
func signal_ignore(sig int32) // 忽略非关键信号
func signal_enable(sig int32) // 启用并注册 runtime handler
该函数在 runtime.sighandler 初始化阶段调用 rt_sigaction,将 SA_ONSTACK | SA_SIGINFO 标志置位,确保信号在独立的 g0 栈上安全执行。
关键信号路由表
| 信号 | Go runtime 行为 | 是否屏蔽 libc 默认 handler |
|---|---|---|
| SIGSEGV | 触发 sigsegv → crash → panic 或 traceback |
是 |
| SIGABRT | 转交 sighandler → abort → exit(2) |
是 |
信号传递链路
graph TD
A[Kernel delivers SIGSEGV] --> B[Go's sigtramp asm entry]
B --> C[runtime.sigtrampgo]
C --> D{Is Go pointer fault?}
D -->|Yes| E[recover via defer/panic]
D -->|No| F[forward to default handler via sigaction]
此链路确保 Go 程序在空指针解引用等场景下可捕获并转换为 panic,而非直接终止。
2.5 DWARF调试信息在混合栈(Go+C)中的解析限制与绕过策略
栈帧语义割裂问题
Go 运行时使用分段栈和 goroutine 调度器,C 函数则依赖传统帧指针(rbp)或 CFA(Call Frame Address)推导。DWARF .debug_frame 和 .eh_frame 通常仅覆盖 C 编译单元,Go 编译器(gc)默认不生成完整 .debug_frame 条目,导致 libdw/gdb 在跨语言调用点(如 C.malloc → runtime.mallocgc)丢失栈展开能力。
典型解析失败场景
// cgo_export.h
void call_go_func(void); // 实际由 Go 导出,但 DWARF 无对应 .debug_info
逻辑分析:
call_go_func符号存在,但其.debug_info中缺少DW_TAG_subprogram及DW_AT_frame_base属性;libdw尝试读取DW_OP_call_frame_cfa时返回DW_DLV_NO_ENTRY,导致栈回溯中断。
绕过策略对比
| 策略 | 原理 | 适用性 |
|---|---|---|
GODEBUG=asyncpreemptoff=1 + CGO_CFLAGS=-gdwarf-4 |
禁用异步抢占以稳定栈布局,强制 C 部分携带完整 DWARF | 仅限调试环境 |
runtime/debug.SetGCPercent(-1) + 手动插入 __builtin_unwind_init() |
配合 libunwind 显式注册 Go 栈帧钩子 | 需 patch Go runtime |
数据同步机制
// 在 CGO 调用前手动记录当前 goroutine 栈基址
func recordGoStack() {
var buf [4096]byte
n := runtime.Stack(buf[:], false)
// 将 buf[:n] 注入 perf_event 或自定义 ringbuffer
}
参数说明:
runtime.Stack的false参数禁用 full goroutine dump,仅获取当前 G 的 PC/SP;配合perf_event_open(PERF_TYPE_SOFTWARE, PERF_COUNT_SW_BPF_OUTPUT)可实现跨语言栈事件关联。
graph TD
A[CGO Call Entry] --> B{DWARF .debug_frame exists?}
B -->|No| C[Use libunwind + Go runtime hook]
B -->|Yes| D[Standard DWARF CFI unwind]
C --> E[Register runtime.casgstatus via _cgo_thread_start]
第三章:构建可调试的cgo工程环境
3.1 跨平台编译时保留调试符号的Makefile与Bazel配置范式
在跨平台构建中,调试符号(如 DWARF)需在目标平台可解析,同时避免污染发布产物。
Makefile 中的符号保留策略
# 启用调试信息,但分离至 .dwo 文件便于按需分发
CFLAGS += -g -gdwarf-4 -gstrict-dwarf
LDFLAGS += -Wl,--strip-debug # 链接时不剥离,由后续步骤控制
debug-symbols: $(TARGET)
$(OBJCOPY) --only-keep-debug $< $@.debug
$(OBJCOPY) --strip-debug $<
$(OBJCOPY) --add-gnu-debuglink=$@.debug $<
-g 生成完整调试信息;--only-keep-debug 提取独立调试文件;--add-gnu-debuglink 建立运行时符号链接,确保 gdb 自动加载。
Bazel 构建配置要点
| 属性 | 值 | 说明 |
|---|---|---|
--copt=-g |
全局启用 | 为所有 C/C++ 规则注入调试宏 |
--strip=never |
构建阶段 | 禁用自动 strip,保留 .debug_* 段 |
--compilation_mode=dbg |
推荐模式 | 启用优化禁用 + 符号全量保留 |
# BUILD.bazel 片段:显式控制符号输出
cc_binary(
name = "app",
srcs = ["main.cc"],
copts = ["-g", "-gdwarf-4"],
linkopts = ["-Wl,--build-id=sha1"], # 支持 debuginfod 查找
)
--build-id 为二进制生成唯一哈希,配合 debuginfod 实现跨平台符号自动定位。
3.2 使用 delve + gdb 双调试器协同定位C层panic的实操流程
当 Go 程序在 CGO 调用中触发 C 层段错误(如 SIGSEGV),delve 无法穿透至原生栈帧,需与 gdb 协同接管。
启动双调试会话
# 在 panic 前暂停并导出进程 PID(如 12345)
dlv exec ./app --headless --api-version=2 --accept-multiclient &
gdb -p 12345
--headless启用无界面服务;--accept-multiclient允许 gdb 后续 attach;gdb 直接接管同一进程内存与信号控制流。
切换调试上下文
| 调试器 | 主要职责 | 关键命令 |
|---|---|---|
| Delve | Go 运行时状态、goroutine 栈 | goroutines, bt |
| GDB | C 函数栈、寄存器、内存布局 | info registers, x/10i $pc |
捕获 C 层崩溃现场
graph TD
A[Delve 触发 panic 断点] --> B[暂停 Go runtime]
B --> C[GDB attach 并接管 SIGSEGV]
C --> D[执行 info proc mappings]
D --> E[定位 cgo 分配的 .so 内存段]
3.3 静态链接libc与动态链接下的堆栈可追溯性差异验证
堆栈回溯(stack unwinding)在调试与崩溃分析中高度依赖运行时符号信息与调用帧布局。静态链接 libc 时,libgcc 和 libunwind 的 unwind 表被直接嵌入二进制,而动态链接则依赖 .eh_frame 段与 ld-linux.so 协同解析。
关键差异点
- 静态链接:
.eh_frame固定、无 PLT 干扰,backtrace()可完整还原至main - 动态链接:
dlopen/PLT插入间接跳转,部分帧可能被优化或缺失符号
验证代码片段
#include <execinfo.h>
#include <stdio.h>
void trace() {
void *buf[10];
int nptrs = backtrace(buf, 10);
backtrace_symbols_fd(buf, nptrs, STDERR_FILENO);
}
backtrace()依赖_Unwind_Backtrace;静态链接下libgcc.a提供完整 unwind 逻辑,而动态链接需libgcc_s.so.1加载时机匹配,否则返回截断帧。
| 链接方式 | .eh_frame 可读性 |
backtrace_symbols() 准确率 |
符号剥离后是否仍可回溯 |
|---|---|---|---|
| 静态 | ✅ 完整嵌入 | 98%+ | ✅(含编译期生成的 .debug_frame) |
| 动态 | ⚠️ 依赖共享库加载 | ~72%(PLT 帧常显示 ??) |
❌(无符号则无法解析函数名) |
graph TD
A[调用 backtrace] --> B{链接方式}
B -->|静态| C[直接调用 libgcc.a 中 _Unwind_Backtrace]
B -->|动态| D[通过 GOT 调用 libgcc_s.so.1 中符号]
C --> E[帧指针链完整,无 PLT 扰动]
D --> F[PLT/GOT 可能破坏帧链,_Unwind_Find_FDE 失败]
第四章:典型cgo崩溃场景的精准诊断方法论
4.1 C函数中释放已free指针导致的Go协程静默崩溃复现与修复
复现场景
当 Go 通过 cgo 调用 C 函数 free() 两次同一指针时,C 运行时触发 double free or corruption,但 Go 运行时不捕获该信号,协程直接静默退出(无 panic、无日志)。
关键代码片段
// cgo_export.h
void unsafe_double_free(void* p) {
free(p); // 第一次释放:合法
free(p); // 第二次释放:UB → SIGABRT 或静默内存破坏
}
逻辑分析:
p为 Go 分配后传入的C.CString指针;第二次free()触发 glibc 堆校验失败。Go 的runtime/cgo默认屏蔽SIGABRT,导致协程无声终止,难以定位。
修复策略
- ✅ 使用
C.free()后立即将 Go 端指针置nil(避免重复传入) - ✅ 在 C 层加
if (p) { free(p); p = NULL; }防御性检查 - ❌ 禁止在 Go 中多次调用
C.free()同一地址
| 方案 | 可靠性 | 调试友好性 |
|---|---|---|
| C 层空指针检查 | 高(拦截 UB) | 中(需 C 日志) |
| Go 层引用计数管理 | 最高 | 高(panic 可捕获) |
4.2 Go字符串传入C时未正确处理\0终止符引发的缓冲区溢出追踪
Go 字符串是长度确定、不以 \0 结尾的字节序列,而 C 函数(如 strcpy, printf)依赖 \0 终止符判断字符串边界。直接通过 C.CString() 转换后若未严格管控生命周期或误用非 \0 安全函数,极易触发越界读写。
典型错误调用模式
// ❌ 危险:C.CString() 分配内存,但未确保目标C缓冲区足够且\0被保留
s := "hello\x00world" // 含嵌入\0
cstr := C.CString(s) // 实际只复制到首个\0 → "hello"
defer C.free(unsafe.Pointer(cstr))
C.vulnerable_print(cstr) // 若C函数内部越界扫描,将读取后续栈/堆内存
C.CString(s) 内部调用 strlen(s) 截断至首 \0,导致原始数据截断且语义丢失;若 C 侧期望完整字节流(如二进制协议),则必然溢出。
安全替代方案对比
| 方法 | 是否保留完整字节 | 是否需手动管理内存 | 是否适配C标准库 |
|---|---|---|---|
C.CString() |
❌(遇\0即停) |
✅ | ✅(仅限纯文本) |
C.CBytes([]byte) |
✅ | ✅ | ❌(需C端按长度参数使用) |
数据流向示意
graph TD
A[Go string] -->|含\0或二进制数据| B[C.CBytes]
B --> C[void* ptr + len]
C --> D[C函数显式传len参数]
D --> E[安全边界访问]
4.3 C回调函数中非法调用Go函数(noescape逃逸失败)的汇编级定位
当C代码通过//export导出函数并被C回调时,若在该回调中直接调用Go函数(尤其含栈分配对象),可能触发noescape失效——因CGO调用链未正确标记指针逃逸,导致GC误回收栈变量。
汇编证据链定位
查看go tool compile -S输出,关键特征:
- 缺失
MOVQ R12, (SP)类参数压栈指令 CALL runtime.gcWriteBarrier未出现在调用前- 函数帧大小为
0x0(应为非零以容纳逃逸对象)
典型错误模式
// export goHandler
void goHandler() {
GoCallback(); // ❌ 非法:C栈帧中无Go调度上下文
}
分析:
GoCallback若创建[]byte或struct{sync.Mutex},其内存由Go堆分配但无逃逸标记,汇编中LEAQ后直接CALL,跳过runtime.newobject逃逸检查路径。
| 现象 | 汇编线索 | GC风险 |
|---|---|---|
noescape失效 |
SUBQ $0x0, SP |
栈对象被提前回收 |
| 跨CGO边界调用 | CALL main.GoCallback(SB) |
触发stack growth panic |
graph TD
A[C回调入口] --> B{是否在goroutine上下文中?}
B -->|否| C[跳过g.stackguard0检查]
B -->|是| D[执行runtime.checkescape]
C --> E[对象逃逸失败→悬垂指针]
4.4 多线程C库(如OpenSSL)与Go调度器竞争导致的栈损坏分析
当Go程序通过cgo调用OpenSSL等多线程C库时,C线程可能在非GOMAXPROCS管理的OS线程上执行,绕过Go调度器的栈管理机制。
栈空间冲突根源
- Go goroutine 栈初始仅2KB,动态增长;
- OpenSSL内部线程默认使用系统默认栈(通常8MB),若与Go runtime共享线程,其大栈帧可能覆盖Go分配的相邻栈内存;
runtime·stackmap无法跟踪C代码栈布局,导致GC误判或栈分裂失败。
典型复现代码片段
// openssl_wrapper.c
#include <openssl/ssl.h>
void unsafe_ssl_init() {
SSL_library_init(); // 可能触发内部线程创建
pthread_create(&tid, NULL, ssl_worker, NULL); // 独立线程+大栈
}
此调用在
CGO_ENABLED=1下绕过runtime.LockOSThread()保护,使C线程脱离Go调度器监控,其栈扩张无边界检查,易覆盖相邻goroutine栈尾部元数据。
关键规避策略
| 方法 | 原理 | 风险 |
|---|---|---|
runtime.LockOSThread() + C.pthread_setstacksize() |
绑定并限制C线程栈尺寸 | 需手动管理线程生命周期 |
使用// #cgo LDFLAGS: -lssl -lcrypto -lpthread显式链接静态lib |
避免动态线程池初始化 | 编译依赖复杂 |
graph TD
A[Go main goroutine] -->|cgo call| B[OpenSSL C函数]
B --> C{是否创建新pthread?}
C -->|是| D[OS线程无栈监控]
C -->|否| E[复用M线程,受Go调度器约束]
D --> F[栈溢出→破坏相邻goroutine stack guard]
第五章:总结与展望
实战项目复盘:某金融风控平台的模型迭代路径
在2023年Q3上线的实时反欺诈系统中,团队将LightGBM模型替换为融合图神经网络(GNN)与时序注意力机制的Hybrid-FraudNet架构。部署后,对团伙欺诈识别的F1-score从0.82提升至0.91,误报率下降37%。关键突破在于引入动态子图采样策略——每笔交易触发后,系统在50ms内构建以目标用户为中心、半径为3跳的异构关系子图(含账户、设备、IP、商户四类节点),并通过PyTorch Geometric实现实时推理。下表对比了两代模型在生产环境连续30天的线上指标:
| 指标 | Legacy LightGBM | Hybrid-FraudNet | 提升幅度 |
|---|---|---|---|
| 平均响应延迟(ms) | 42 | 48 | +14.3% |
| 欺诈召回率 | 86.1% | 93.7% | +7.6pp |
| 日均误报量(万次) | 1,240 | 772 | -37.7% |
| GPU显存峰值(GB) | 3.2 | 5.8 | +81.3% |
工程化瓶颈与应对方案
模型升级暴露了特征服务层的硬性约束:原有Feast特征仓库不支持图结构特征的版本化存储与实时更新。团队采用双轨制改造:一方面基于Neo4j构建图特征快照服务,通过Cypher查询+Redis缓存实现毫秒级子图特征提取;另一方面开发轻量级特征算子DSL,将“近7天同设备登录账户数”等业务逻辑编译为可插拔的UDF模块。以下为特征算子DSL的核心编译流程(Mermaid流程图):
flowchart LR
A[DSL文本] --> B[词法分析]
B --> C[语法树生成]
C --> D[图遍历逻辑校验]
D --> E[编译为Cypher模板]
E --> F[注入参数并缓存]
F --> G[执行Neo4j查询]
G --> H[结果写入Redis]
开源工具链的深度定制
为解决模型监控盲区,团队基于Evidently开源框架二次开发,新增“关系漂移检测”模块。该模块不仅计算节点属性分布变化(如设备型号占比),更通过Graph Edit Distance算法量化子图拓扑结构偏移程度。在灰度发布阶段,当检测到新模型在“跨境支付+高风险IP”子图上的GED均值突增2.3倍时,自动触发熔断机制并回滚至旧版本。
下一代技术栈验证进展
当前已在测试环境完成三项关键技术验证:① 使用NVIDIA Triton推理服务器实现GNN模型的动态批处理,吞吐量达12,800 QPS;② 基于Apache Flink的实时图更新引擎,将子图重建延迟压缩至18ms;③ 采用ONNX Runtime Web在浏览器端运行轻量化欺诈评分模型,支撑前端实时风险提示。所有验证数据均来自真实脱敏日志回放,覆盖日均2.4亿笔交易流量。
跨团队协作机制创新
打破算法与工程壁垒,在研发流程中嵌入“图特征契约”制度:算法团队提交模型前,必须提供包含节点类型、边关系、时间窗口、采样策略四要素的YAML契约文件;平台团队据此自动生成特征服务接口与SLA测试用例。该机制使模型交付周期从平均14天缩短至5.2天,契约文件示例如下:
graph_contract:
version: "v2.1"
nodes:
- type: "account"
features: ["balance_level", "age_days"]
- type: "device"
features: ["os_version", "rooted_flag"]
edges:
- type: "login"
temporal_window: "P7D"
sampling_strategy: "top_k_by_frequency" 