debug-plan-cache-misses

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Debug Chalk query plan cache misses

调试 Chalk 查询计划缓存未命中

Chalk caches a compiled query plan the first time it plans a query, keyed by a normalized
BatchQuery
. A cache miss forces the engine to replan on the hot path, which is slow. Misses are expected the first time a query shape is seen; they are a problem when a query that "looks identical" to a previous one keeps missing — that means some field in the request is varying between calls.
This skill: (1) finds misses in the logs, and (2) diffs the planned queries to identify exactly which field differs and forces the miss.
Chalk 会在首次规划查询时缓存编译后的查询计划,其缓存键是规范化后的
BatchQuery
。缓存未命中会迫使 engine 在热路径上重新规划,因此速度很慢。 首次遇到某种查询结构时,未命中属于预期现象;但如果一个与此前“看起来相同”的 查询持续未命中,就构成问题——这意味着请求中的某个字段在两次调用之间 发生了变化。
此 skill 会:(1) 从日志中查找未命中记录;(2) 对比规划后的查询,精确找出 导致未命中的差异字段。

What the cache key actually is

实际的缓存键

The plan cache key wraps the entire normalized
BatchQuery
. The only normalization applied is that
planner_options
are resolved to their effective values — so leaving an option unset and setting it explicitly to its default produce the same key and will not cause a spurious miss.
Everything else in the request is part of the key, including:
  • given_features
    — the exact set of input features the caller provides
  • target features (
    optional_target_features
    /
    required_target_features
    )
  • tags
    ,
    required_resolver_tags
  • max_staleness_overrides
    and staleness/recompute flags
  • query_name_and_version
  • and the remaining
    BatchQuery
    fields
So any difference in these forces a new plan. The single most common real-world cause is a varying
given_features
set
— the caller sometimes supplies more or fewer input features (e.g. raw
*_original
inputs, or derived inputs like
email_username
) than a previous call. A query that provides a different set of inputs genuinely needs a different plan (different resolvers must run), so it cannot reuse the cached one.
计划缓存键封装了整个规范化后的
BatchQuery
。唯一的规范化处理,是将
planner_options
解析为生效值——因此,将某个配置项留空与显式设置为其 默认值会产生相同的键,不会导致虚假的未命中。
请求中的其他所有内容都属于缓存键,包括:
  • given_features
    ——调用方提供的输入特征精确集合
  • 目标特征(
    optional_target_features
    /
    required_target_features
    )
  • tags
    、
    required_resolver_tags
  • max_staleness_overrides
    以及 staleness/recompute 标志
  • query_name_and_version
  • BatchQuery
    中剩余的字段
因此,上述任何差异都会强制生成新的计划。最常见的现实原因是
given_features
集合发生变化
——调用方有时会提供更多或更少的输入特征 (例如有时包含原始
*_original
输入,有时不包含;或者包含
email_username
这样的派生输入)。提供不同输入集合的查询确实需要不同的计划(必须运行不同的 resolver),因此无法复用缓存中的计划。

The two log lines that signal a miss

表示未命中的两条日志

The engine emits these on the planning path:
  1. Ad-hoc miss warning (
    batch_online_query_service.py
    ):
    Ad-hoc query request with query name/version '(<name>, <version>)' missed the plan cache. The query was: <BatchQuery ...>
  2. Plan computation — emitted on every miss (ad-hoc or named), and the most useful line for debugging (
    local_plan_factory.py
    ):
    Computed plan for [<target features>] given [<given features>] with planner options <NonNullPlannerOptions(...)>; query.query_name_and_version=(<name>, <version>)
There are also statsd counters if you want to trend rather than read lines:
chalk.engine.planner.python_plan_cache_miss
vs
chalk.engine.planner.python_plan_cache_hit
.
engine 会在规划路径上发出以下两类日志:
  1. Ad-hoc 未命中警告(
    batch_online_query_service.py
    ):
    Ad-hoc query request with query name/version '(<name>, <version>)' missed the plan cache. The query was: <BatchQuery ...>
  2. 计划计算——每次未命中时都会发出,无论是 ad-hoc 还是 named;这也是调试时 最有用的日志(
    local_plan_factory.py
    ):
    Computed plan for [<target features>] given [<given features>] with planner options <NonNullPlannerOptions(...)>; query.query_name_and_version=(<name>, <version>)
如果希望观察趋势而不是逐行阅读,还可以使用 statsd counters:
chalk.engine.planner.python_plan_cache_miss
与
chalk.engine.planner.python_plan_cache_hit
。

Finding misses with
chalk logs

使用
chalk logs
查找未命中

chalk logs
searches the environment's engine logs. Point it at the right environment/context first (
chalk config
/ the usual project or
--environment
selection), then query by log fields.
Search for the miss warnings over the last day:
sh
chalk logs --query 'message:"missed the plan cache"' \
  --start-time "24h ago" --end-time "now"
Search for plan computations for one query name (best for diffing):
sh
chalk logs --query 'message:"Computed plan for" message:sv5_idplus_features_parallel' \
  --start-time "24h ago" --end-time "now"
Trend miss volume over time in 10-minute buckets:
sh
chalk logs --aggregate --window-period 10m \
  --query 'message:"missed the plan cache"' --start-time "6h ago"
Tail misses live while reproducing:
sh
chalk logs --follow --query 'message:"missed the plan cache"'
chalk logs
会搜索 environment 的 engine 日志。首先将其指向正确的 environment/context(
chalk config
/ 常用的 project 或
--environment
选择方式), 然后按日志字段查询。
搜索过去一天的未命中警告:
sh
chalk logs --query 'message:"missed the plan cache"' \
  --start-time "24h ago" --end-time "now"
搜索某个 query name 的计划计算日志(最适合用于 diff):
sh
chalk logs --query 'message:"Computed plan for" message:sv5_idplus_features_parallel' \
  --start-time "24h ago" --end-time "now"
按 10 分钟桶查看未命中数量的趋势:
sh
chalk logs --aggregate --window-period 10m \
  --query 'message:"missed the plan cache"' --start-time "6h ago"
复现问题时实时跟踪未命中:
sh
chalk logs --follow --query 'message:"missed the plan cache"'

Query syntax

查询语法

field:value
pairs, space-separated (implicit AND). Quote any value that contains a space or a
.
— e.g.
message:"missed the plan cache"
,
query_name:"my.query"
. Useful fields:
  • message
    — substring match on the log message (use this for the two lines above)
  • component
    — Chalk component, e.g.
    component:engine
  • query_name
    — filter to a specific named query
  • operation_id
    — internal query id;
    correlation_id
    — caller-supplied query id
  • trace_id
    ,
    pod_name
    ,
    resource_group
    ,
    deployment
    ,
    app
  • all_filter
    — match across multiple fields at once
Time flags:
--start-time
/
--end-time
accept
'1h ago'
,
'now'
, or ISO-8601 (
'2024-01-01T00:00:00Z'
). Add
--tui
for an interactive viewer.
使用以空格分隔的
field:value
对(隐式 AND)。如果值包含空格或
.
,请加引号—— 例如
message:"missed the plan cache"
、
query_name:"my.query"
。常用字段:
  • message
    ——对日志消息进行子字符串匹配(用于查找上述两种日志)
  • component
    ——Chalk component,例如
    component:engine
  • query_name
    ——筛选特定的 named query
  • operation_id
    ——内部 query id;
    correlation_id
    ——调用方提供的 query id
  • trace_id
    、
    pod_name
    、
    resource_group
    、
    deployment
    、
    app
  • all_filter
    ——一次匹配多个字段
时间参数
--start-time
/
--end-time
接受
'1h ago'
、
'now'
或 ISO-8601 (
'2024-01-01T00:00:00Z'
)。添加
--tui
可打开交互式查看器。

Debugging workflow

调试工作流

  1. Confirm misses are happening and how often:
    sh
    chalk logs --query 'message:"missed the plan cache"' --start-time "24h ago"
    If this is empty, plan caching is working — look elsewhere for the latency.
  2. Pull the
    Computed plan for
    lines for the affected query name:
    sh
    chalk logs --query 'message:"Computed plan for" message:<query_name>' \
      --start-time "24h ago"
  3. Take the two most recent computations and diff them field by field: compare the
    given [...]
    lists first (most common culprit), then the target-feature lists, then the
    planner options
    block. Whatever differs is the cause of the miss. Because planner options are normalized before keying, a difference there is a real difference — not a set-vs-default artifact.
  4. Attribute the difference to the caller. A varying
    given
    set almost always means an upstream service is sending an inconsistent input schema (sometimes including raw/derived inputs, sometimes not). The fix is to make the caller send a stable
    given_features
    set on every request so the key is stable and subsequent identical queries hit the cache.
  1. 确认未命中是否正在发生以及发生频率:
    sh
    chalk logs --query 'message:"missed the plan cache"' --start-time "24h ago"
    如果结果为空,说明计划缓存工作正常——应转而排查其他延迟原因。
  2. 拉取受影响 query name 对应的
    Computed plan for
    日志:
    sh
    chalk logs --query 'message:"Computed plan for" message:<query_name>' \
      --start-time "24h ago"
  3. 取出最近两次计算结果,逐字段进行 diff:先比较
    given [...]
    列表 (最常见的原因),然后比较目标特征列表,最后比较
    planner options
    块。 存在差异的字段就是导致未命中的原因。由于 planner options 在生成缓存键前 会进行规范化,因此其中的差异是真实差异,而不是“未设置与默认值不同”造成的 假象。
  4. 将差异归因于调用方。不断变化的
    given
    集合几乎总是意味着上游服务发送了 不一致的 input schema(有时包含 raw/derived inputs,有时不包含)。修复方法是 让调用方在每次请求中发送稳定的
    given_features
    集合,从而保持缓存键稳定, 让后续相同的查询命中缓存。

Notes

注意事项

  • Two byte-identical requests should hit the cache on the second call. If they don't, that's a distinct issue from a field mismatch (e.g. cache not being populated/shared) — call it out separately rather than blaming the request.
  • Fixing a miss only helps if all callers converge on one request shape. If some callers still send a different
    given
    set, those keep missing.
  • This skill reads logs and reasons about the request; it does not mutate any environment. Any remediation (changing what a caller sends) happens in the caller's code, not here.
  • 两个逐字节完全相同的请求,第二次调用应当命中缓存。如果未能命中,则属于另一个 问题(例如缓存未填充或未共享)——应单独指出,而不要归咎于请求字段不一致。
  • 只有当所有调用方都统一到一种请求结构时,修复未命中才会有效。如果某些 调用方仍发送不同的
    given
    集合,它们仍会持续未命中。
  • 此 skill 只读取日志并分析请求;不会修改任何 environment。任何修复措施 (例如更改调用方发送的内容)都应在调用方代码中完成,而不是在此处完成。