# SQLHealthCheck 巡检服务 - API 调用指南

SQLHealthCheck 是慢 SQL 相关巡检项的统一服务。当前包含：

- `tablefullscan_slow_query`：保持原触发 URL、参数、S3 文件名和结果结构兼容
- `slowlog_top_digest`：按 slow log 一级字段聚合 digest，输出不同维度的 Top Digest 列表
- `sql_tikv_high_duration`：识别可能拖慢 TiKV coprocessor request 的 SQL digest
- `sql_tikv_high_cpu`：识别可能打高 TiKV CPU 的 SQL digest

## 服务地址

| 巡检项 | 外部地址 | 内部地址（集群内调用） |
|------|---------|----------------------|
| TableFullScan | `/clinic-internal/healthcheck/sql/tablefullscan-slowquery` | `http://sql-health-check:8080/clinic-internal/healthcheck/sql/tablefullscan-slowquery` |
| Slow Log Top Digest | `/clinic-internal/healthcheck/sql/slowlog-top-digest` | `http://sql-health-check:8080/clinic-internal/healthcheck/sql/slowlog-top-digest` |
| SQL TiKV High Duration | `/clinic-internal/healthcheck/sql/tikv-high-duration` | `http://sql-health-check:8080/clinic-internal/healthcheck/sql/tikv-high-duration` |
| SQL TiKV High CPU | `/clinic-internal/healthcheck/sql/tikv-high-cpu` | `http://sql-health-check:8080/clinic-internal/healthcheck/sql/tikv-high-cpu` |

> TableFullScan 的触发 URL、请求参数、报告文件名和结果结构保持兼容；服务、部署和镜像统一使用 `sql-health-check` 命名。

服务内部查询 slow logs 时，默认会直接访问集群内的 Clinic Data Proxy：

`http://clinic-data-service:8087/data-proxy/query`

TiKV hotspot 巡检还会通过同一个 Data Proxy 服务访问 Prometheus-compatible Metrics API：

`http://clinic-data-service:8087/data-proxy/metrics/{clusterId}/api/v1/query[_range]`

只有在集群外本地调试时，才建议通过环境变量切到外部地址 `https://clinic.pingcap.com/data-proxy/query` 并配置 API key。

## 请求方式

`POST`，需要 Bearer Token 认证。

## 请求示例

```bash
curl -s -X POST https://dev-clinic.pingcap.com/clinic-internal/healthcheck/sql/tablefullscan-slowquery \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer <your-token>" \
  -d '{
    "clusterId": "10754652599988043412",
    "reportId": "33333",
    "timeRange": {
      "start": "2026-03-02T00:00:00Z",
      "end": "2026-03-02T23:59:59Z"
    },
    "options": {
      "region": "us-east-1",
      "dataProxyPageSize": 2000,
      "dataProxyTimeoutSeconds": 180
    },
    "params": {
      "scan_rows_threshold": 10000,
      "topN": 100,
      "sample_top_size": 5
    }
  }'
```

## 请求参数

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| clusterId | string | 是 | 集群 ID |
| reportId | string | 否 | 报告 ID，不传则自动生成 UUID |
| timeRange | object | 是 | 查询时间范围 |
| timeRange.start | string | 是 | 开始时间，ISO 8601 格式 |
| timeRange.end | string | 是 | 结束时间，ISO 8601 格式 |
| dataSource | string/object | 否 | 保留兼容字段；不再直接读取 S3。若传 object，可用 `endpoint`、`timeoutSeconds`、`pageSize` 覆盖 data proxy 配置 |
| options.region | string | 否 | 集群 region，用于生成 Clinic 跳转链接，默认 `us-east-1` |
| options.dataProxyPageSize | int | 否 | 每次 data proxy 查询拉取的记录页大小，默认 `2000` |
| options.dataProxyTimeoutSeconds | int | 否 | 单次 data proxy 查询超时时间，默认 `180` 秒 |
| options.metricsDataProxyBaseURL | string | 否 | TiKV hotspot 巡检使用的 metrics data proxy base URL，默认集群内 `clinic-data-service:8087` |
| options.metricsDataProxyTimeoutSeconds | int | 否 | 单次 metrics data proxy 查询超时时间，默认 `120` 秒 |
| version | string | 否 | TiDB version，用于生成 Grafana 链接 |
| dashboardVersion | string | 否 | Dashboard version tag，用于生成 Grafana 链接 |
| params.scan_rows_threshold | int | 否 | 全表扫描行数阈值，默认 `10000` |
| params.topN | int | 否 | 返回结果条目数，按 `total_scanned_rows` 倒序取前 N 条；最小 `1`，默认 `100`，不设最大值 |
| params.sample_top_size | int | 否 | 每条结果中返回的样本数，默认 `10` |
| params.ignore_dml | bool | 否 | 是否忽略 DML 语句，默认 `false` |
| params.ignore_tiflash | bool | 否 | 是否忽略 TiFlash 查询，默认 `false` |
| clusterMeta | object | 否 | 集群元信息，用于生成 Clinic 跳转链接 |
| clusterMeta.clusterName | string | 否 | 集群名称 |
| clusterMeta.orgName | string | 否 | 组织名称 |
| clusterMeta.orgId | string | 否 | 组织 ID |
| clusterMeta.projectId | string | 否 | 项目 ID |
| clusterMeta.vendor | string | 否 | 云厂商，默认 `aws` |

Clinic taskmanager 会在触发巡检时传入 cluster metadata、TiDB version 和 dashboard version；本地调试请求缺失这些字段时，链接会按默认值降级生成，不会额外反查外部接口。

TiKV hotspot 巡检的 `params` 支持 JSON object 或 JSON string。面向用户建议只展示下面这些核心参数；后端仍兼容内部调参字段，但 UI 和报告页不应默认展开。

| 字段 | 默认值 | 说明 |
|------|--------|------|
| params.window_min_duration_seconds | `180` | 异常窗口最小持续时间；调低可巡检更短尖刺 |
| params.critical_window_duration_seconds | `600` | 异常持续时间达到该值时定级为 Critical |
| params.topsql_cpu_percent_threshold | `10.0` | digest 在 TopSQL total CPU 中的最低占比，小于该值忽略 |
| params.topsql_dominance_ratio_threshold | `2.0` | 主导 SQL 判断阈值，当前候选 CPU 达到下一名 N 倍时截断后续候选 |
| params.duration_threshold_ms | `1000` | `sql_tikv_high_duration` 的 TiKV coprocessor p99 duration 阈值 |
| params.critical_duration_threshold_ms | `4000` | `sql_tikv_high_duration` 的 Critical duration 阈值 |
| params.absolute_cpu_threshold_percent | `75.0` | `sql_tikv_high_cpu` 的绝对高 CPU 阈值 |
| params.relative_cpu_delta_threshold_percent | `10.0` | `sql_tikv_high_cpu` 的相对 P50 离群阈值 |
| params.critical_cpu_threshold_percent | `85.0` | `sql_tikv_high_cpu` 的 Critical CPU 阈值 |

完整参数与判定逻辑见 `health_check/sql_health_check/tikv_hotspot_check.md`。

## 响应

服务采用**异步模式**：接收请求后立即返回 `202`，后台执行巡检，结果写入 S3。

服务端默认采用**有界任务队列**而不是“每请求起一个线程”：

- 默认每个进程同时执行 `2` 个巡检任务：`SQL_HEALTHCHECK_MAX_CONCURRENT_INSPECTIONS=2`
- 默认最多排队 `32` 个待执行任务：`SQL_HEALTHCHECK_MAX_PENDING_INSPECTIONS=32`
- 兼容旧环境变量：`TABLEFULLSCAN_MAX_CONCURRENT_INSPECTIONS`、`TABLEFULLSCAN_MAX_PENDING_INSPECTIONS`
- Gunicorn 默认 worker 数已收紧为 `1`，可通过 `GUNICORN_WORKERS` 覆盖

### 响应示例（HTTP 202）

```json
{
  "name": "tablefullscan_slow_query",
  "status": "processing",
  "clusterId": "10754652599988043412",
  "reportId": "33333",
  "s3_url": "s3://delicated-healthcheck/healthcheck-report/10754652599988043412_33333_tablefullscan_slow_query.json"
}
```

| 字段 | 说明 |
|------|------|
| name | 巡检项名称 |
| status | `processing` 表示后台处理中 |
| clusterId | 集群 ID |
| reportId | 报告 ID |
| s3_url | 结果文件的 S3 地址，巡检完成后可读取 |

### S3 结果文件格式

巡检完成后，结果 JSON 写入 `s3_url` 指定的路径，结构如下：

```json
{
  "name": "tablefullscan_slow_query",
  "description": "Detects SQL queries with table full scan operations that scan large amounts of rows",
  "status": "success",
  "params": {
    "tablefullscan_anomaly_threshold": 10000,
    "topN": 100,
    "sample_top_size": 5,
    "ignore_dml": false,
    "ignore_tiflash": false
  },
  "summary": {
    "anomaly_queries(scan>=10000rows)": 29215,
    "digests_with_anomaly_table_full_scan": 331,
    "digests_with_table_full_scan": 364,
    "max_scanned_rows": 317807060,
    "queries_with_table_full_scan": 29905,
    "queries_without_plan": 5800
  },
  "results": [
    {
      "digest": "7fd49b5b16a22325...",
      "query": "SELECT ...",
      "anomaly_logs(scan>=10000rows)": 4870,
      "total_scanned_rows": 57515055447,
      "max_scanned_rows": 13507387,
      "p50_scanned_rows": 8765523,
      "p80_scanned_rows": 11185036,
      "p95_scanned_rows": 13507344,
      "p99_scanned_rows": 13507384,
      "p50_latency_s": 1.747,
      "p80_latency_s": 1.972,
      "p95_latency_s": 2.353,
      "p99_latency_s": 2.634,
      "samples": [
        {
          "time": 1772467200.123,
          "time_formatted": "2026-03-02T00:00:00.123Z",
          "conn_id": "123456",
          "db": "app",
          "act_rows": 13507387,
          "latency_s": 2.634,
          "full_slow_log": {
            "time": 1772467200.123,
            "digest": "7fd49b5b16a22325...",
            "query_time": 2.634,
            "query": "SELECT ...",
            "decoded_plan": "{...}"
          }
        }
      ]
    }
  ]
}
```

如果 data proxy 分页请求连续重试仍失败，结果会以 `status=failed` 上传到同一路径，并保留已成功处理的部分结果，同时额外包含：

```json
{
  "status": "failed",
  "error": "Clinic Data Proxy query failed status=500: ... trace_id=..."
}
```

### 错误响应（HTTP 400）

```json
{
  "error": "Missing required fields: ['clusterId']"
}
```

### 错误响应（HTTP 429）

当任务队列已满时，请求会被拒绝，避免同一 pod 内无限叠加巡检线程和内存占用：

```json
{
  "name": "tablefullscan_slow_query",
  "status": "rejected",
  "error": "inspection queue is full",
  "clusterId": "10754652599988043412",
  "reportId": "33333"
}
```

## S3 结果路径规则

```
s3://delicated-healthcheck/healthcheck-report/{clusterId}_{reportId}_{name}.json
```

## Slow Log Top Digest 巡检项

触发 URL：

`POST /clinic-internal/healthcheck/sql/slowlog-top-digest`

请求参数：

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| params.dimensions | array/object/string | 否 | Top 维度，默认全部：`count`、`total_latency`、`total_keys`、`process_keys`、`request_count`、`sum_ru` |
| params.topN | int | 否 | 每个维度返回 Top N，默认 `10`，最大 `50` |
| params.ignore_dml | bool | 否 | 是否过滤 `INSERT`、`UPDATE`、`DELETE` 慢日志，默认 `false` |
| params.min_count | int | 否 | 统一的 digest slow log count 过滤阈值，对所有 Top 维度生效；巡检范围内 slow log count 小于该值的 digest 会被忽略，默认 `0` |
| params.min_total_keys | int | 否 | 统一的 slow log 行级 `total_keys` 过滤阈值；单条 slow log 的 `total_keys` 小于该值时不参与后续 digest 聚合，默认 `0` |
| params.total_keys_min_slow_log_count | int | 否 | Legacy 参数；仅当未设置 `params.min_count` 时生效，仅对 `total_keys` 维度生效，忽略 slow log count 小于等于该值的 digest |
| params.process_keys_min_slow_log_count | int | 否 | Legacy 参数；仅当未设置 `params.min_count` 时生效，仅对 `process_keys` 维度生效，忽略 slow log count 小于等于该值的 digest |
| params.request_count_min_slow_log_count | int | 否 | Legacy 参数；仅当未设置 `params.min_count` 时生效，仅对 `request_count` 维度生效，忽略 slow log count 小于等于该值的 digest |
| params.count | int | 否 | Legacy 参数；仅当未设置 `params.min_count` 时生效，`sum_ru` 维度的最小 slow log count 过滤阈值，忽略 slow log count 小于等于该值的 digest |
| params.sum_ru_min_slow_log_count | int | 否 | Legacy 参数；同 `params.count`，仅对 `sum_ru` 维度生效 |

该巡检项默认过滤 `is_internal=true` 的 slow log，不提供用户开关。`params.min_total_keys` 在 digest 聚合前对单条 slow log 生效；`params.min_count` 在 digest 聚合后通过 `HAVING COUNT(*) >= min_count` 生效，用于过滤巡检范围内低频执行的 SQL Digest。Legacy 维度级 count 阈值仍会在未设置 `params.min_count` 时通过 `HAVING COUNT(*) > threshold` 生效。结果写入：

`s3://delicated-healthcheck/healthcheck-report/{clusterId}_{reportId}_slowlog_top_digest.json`

结果中的 `results` 是对象，用户勾选多少个维度就包含多少个列表：

```json
{
  "name": "slowlog_top_digest",
  "params": {
    "dimensions": ["count", "total_latency", "sum_ru"],
    "topN": 10,
    "ignore_dml": false,
    "min_count": 10,
    "min_total_keys": 1000,
    "dimension_count_thresholds": {},
    "exclude_internal": true
  },
  "results": {
    "top_by_count": [],
    "top_by_total_latency": [],
    "top_by_sum_ru": []
  }
}
```

## SQL TiKV Hotspot 巡检项

触发 URL：

- `POST /clinic-internal/healthcheck/sql/tikv-high-duration`
- `POST /clinic-internal/healthcheck/sql/tikv-high-cpu`

结果路径：

- `s3://delicated-healthcheck/healthcheck-report/{clusterId}_{reportId}_sql_tikv_high_duration.json`
- `s3://delicated-healthcheck/healthcheck-report/{clusterId}_{reportId}_sql_tikv_high_cpu.json`

结果同时提供 digest 聚合视角和异常窗口明细：

- `digest_summary`：按 SQL Digest 聚合，说明影响了多少异常窗口和 TiKV 节点。
- `results`：按 TiKV instance + anomaly window 组织，保留每个窗口的指标、TopSQL candidate、slow log enrichment 和链接。

```json
{
  "name": "sql_tikv_high_cpu",
  "status": "success",
  "summary": {
    "node_anomaly_windows": 1,
    "sql_candidate_windows": 1,
    "not_attributed_windows": 0,
    "unknown_windows": 0,
    "sql_candidates": 2,
    "distinct_sql_digests": 1,
    "data_quality": {
      "core_metrics_available": true,
      "topsql_available": true,
      "slow_log_available": true,
      "truncated": false
    }
  },
  "digest_summary": [
    {
      "digest": "abc",
      "affected_windows": 1,
      "affected_instances": 1,
      "instances": ["db-tikv-1"],
      "max_severity": "Critical",
      "total_topsql_cpu_seconds": 230.1,
      "avg_topsql_share_percent": 59.0,
      "max_topsql_share_percent": 59.0,
      "max_cpu_percent": 91.2,
      "slow_log_count": 12,
      "representative_window_id": "db-tikv-1_1772467200_1772467800"
    }
  ],
  "results": [
    {
      "decision": "sql_candidate",
      "node_severity": "Critical",
      "node_anomaly": {
        "type": "high_cpu",
        "instance": "db-tikv-1",
        "window_start": 1772467200,
        "window_end": 1772467800,
        "window_duration_seconds": 600,
        "anomaly_duration_seconds": 600,
        "max_cpu_percent": 91.2
      },
      "topsql_summary": {
        "topsql_total_cpu_seconds": 390.2,
        "topn_topsql_cpu_seconds": 345.3,
        "topn_topsql_share_percent": 88.5,
        "others_topsql_cpu_seconds": 44.9
      },
      "sql_candidates": [
        {
          "digest": "abc",
          "cpu_time_seconds": 230.1,
          "digest_topsql_cpu_share_percent": 59.0,
          "dominance_cutoff": true,
          "is_slow_query": true,
          "slow_log": {
            "count": 12,
            "sample_conn_id": "123456"
          }
        }
      ]
    }
  ]
}
```

## 备注

- 当前版本通过 Clinic Data Proxy 查询 slow logs，并将巡检项过滤、聚合、排序和 `topN` 下推到 SQL 引擎执行
- 当 `provider=azure` 时，Slow Log Top Digest、TableFullScan 和 TiKV hotspot slow-log enrichment 会生成 Synapse 兼容 SQL
- TableFullScan 遇到 `bytesBilledLimitExceeded` 时会跳过无效重试，改按时间切片查询并合并结果；若切片后仍失败，会上传 `status=failed` 且包含 `error` 的结果
- `samples` 只会针对最终 Top N digest 额外查询，并按每个 digest 的 `sample_top_size` 限制返回；每条 sample 的 `full_slow_log` 包含该慢日志行的完整字段
- data proxy 请求会带 `skipTableManagement=true` 以减少单次查询额外开销
- 单页查询失败时会自动重试 `2` 次；若仍失败，会将当前巡检结果标记为 `failed`，上传已完成部分的统计结果，并在 `error` 字段里写入失败信息
- 巡检统计使用 SQL 引擎侧近似分位数；Python 服务只组装聚合结果和有限样本，避免拉取全量慢日志
- 集群内默认直连 `clinic-data-service`，无需额外 API key；集群外访问可通过环境变量 `CLINIC_DATA_PROXY_ENDPOINT`、`CLINIC_DATA_PROXY_API_KEY` 或 `CLINIC_API_BASE_URL` 覆盖
- 巡检耗时取决于慢日志数据量与分页次数，时间跨度较大时会产生更多 data proxy 请求
- 请求返回后，需轮询 S3 等待结果文件生成
- S3 bucket: `delicated-healthcheck`，region: `us-west-2`
