从 StarRocks Stream Load 400 报错,彻底搞懂 HTTP 100-continue

一次看似简单的 Nginx 配置缺失,背后牵出的是 HTTP 协议中一个被严重低估的机制。本文从一个真实生产报错出发,层层深入 Expect: 100-continue 的协议原理、工程实现与架构权衡。


一、事故现场:一个令人困惑的 400 错误

1.1 报错信息

某天,数据团队反馈 Flink 写入 StarRocks 的任务大面积失败,Nginx 日志中出现大量如下错误:

bash 复制代码
$ curl -T data.csv http://starrocks-fe:8030/api/mydb/mytable/_stream_load

# 响应:
{
    "Status": "Fail",
    "Message": "There is no 100-continue header"
}

1.2 第一反应:这什么头?

大多数工程师的第一反应是:

"我传个文件,为什么要一个没听说过的 header?"

事实上,Expect: 100-continue 是 HTTP/1.1 协议(RFC 7231)中定义的一个流控协商机制 ,在数据导入、大文件上传等场景中被广泛使用。StarRocks 的 Stream Load、阿里云 MaxCompute Tunnel、AWS S3 Multipart Upload 等系统都将其作为协议强制要求

1.3 根因定位

排查链路:

scss 复制代码
Flink Connector → Nginx (反向代理) → StarRocks FE

问题出在 Nginx 默认配置会丢弃 Expect 。当请求经过 Nginx 代理时,客户端发出的 Expect: 100-continue 未被透传到后端 StarRocks,导致 FE 认为客户端没有遵循协议,直接返回 400。

修复只需两行:

nginx 复制代码
location /api/ {
    proxy_http_version 1.1;
    proxy_set_header Expect $http_expect;   # ← 关键:透传 Expect 头
    proxy_pass http://starrocks_backend;
}

⚠️ proxy_http_version 1.1 是前提条件。HTTP/1.0 不支持 Expect 语义。


二、什么是 100-continue?

2.1 一句话定义

100-continue 是客户端在发送请求体(Body)之前,先向服务端"请示"是否允许发送的协商机制。

2.2 没有它的世界

arduino 复制代码
Client                                    Server
  │                                          │
  │─── Headers + 500MB Body 一股脑发过去 ───→│
  │                                          │  ← 收到 500MB 后才发现:
  │                                          │     权限不够 / 表不存在 / 限流
  │←────── 403 / 404 / 429 ─────────────────│
  │                                          │
  │   💀 500MB 白传了,带宽、时间、IO 全浪费   │

2.3 有了它的世界

arduino 复制代码
Client                                    Server
  │                                          │
  │─── Headers + Expect: 100-continue ────→ │
  │                                          │  ← 先校验:权限✓ 表存在✓ 限流✓
  │←────── 100 Continue ────────────────────│  ← "可以发,我准备好了"
  │                                          │
  │─── 500MB Body ──────────────────────────→│
  │                                          │
  │←────── 200 OK ──────────────────────────│

如果校验不通过:

arduino 复制代码
Client                                    Server
  │                                          │
  │─── Headers + Expect: 100-continue ────→ │
  │                                          │  ← 校验失败:Quota 超限
  │←────── 403 QuotaExceeded ───────────────│  ← 直接拒绝,Body 0 字节传输
  │                                          │
  │   ✅ 0 字节浪费,立即重试或降级            │

2.4 协议规范(RFC 7231 §5.1.1)

markdown 复制代码
Expect = "100-continue"

客户端:
  - 发送请求头后,必须等待服务端响应
  - 收到 100 Continue → 发送 Body
  - 收到 4xx/5xx → 不发送 Body,处理错误
  - 超时未响应 → 可自行决定是否发送 Body

服务端:
  - 收到 Expect: 100-continue → 必须回复 100 或 4xx
  - 不能忽略该头部
  - 不能先收 Body 再拒绝(违反协议)

三、工作原理:时序与状态机

3.1 完整时序图

css 复制代码
┌────────┐                              ┌────────┐
│ Client │                              │ Server │
└───┬────┘                              └───┬────┘
    │                                        │
    │  ① TCP 三次握手 (建立连接)              │
    │◄──────────────────────────────────────►│
    │                                        │
    │  ② HTTP 请求头                         │
    │  PUT /api/db/table/_stream_load        │
    │  Content-Length: 524288000             │
    │  Expect: 100-continue                  │
    │───────────────────────────────────────►│
    │                                        │
    │         ③ 服务端前置校验               │
    │         • 鉴权 Token 有效?            │
    │         • 目标表/分区存在?            │
    │         • Quota 是否超限?             │
    │         • 磁盘水位是否安全?           │
    │                                        │
    │  ④a 校验通过                           │
    │  HTTP/1.1 100 Continue                 │
    │◄───────────────────────────────────────│
    │                                        │
    │  ⑤ 发送 Body (500MB)                  │
    │═══════════════════════════════════════►│
    │                                        │
    │  ⑥ 最终响应                            │
    │  HTTP/1.1 200 OK                       │
    │◄───────────────────────────────────────│
    │                                        │
    │  ──── 或者 ────                        │
    │                                        │
    │  ④b 校验失败                           │
    │  HTTP/1.1 403 Forbidden                │
    │  Body: {"error": "QuotaExceeded"}      │
    │◄───────────────────────────────────────│
    │                                        │
    │  (Body 从未被发送,0 字节浪费)          │
    │                                        │

3.2 关键设计约束

规则 说明
客户端必须等待 发出 Expect 后不能立即发 Body(除非超时)
服务端必须响应 不能静默忽略,必须回 100 或 4xx
100 是中间响应 不是最终响应,之后还会有 200/4xx/5xx
连接不关闭 整个过程复用同一个 TCP 连接
代理必须透传 Nginx/Tengine 需显式配置转发 Expect 头

四、Nginx 代理中的坑

4.1 为什么 Nginx 默认会"吃掉" Expect 头?

Nginx 作为反向代理,默认行为:

nginx 复制代码
# 默认行为(等效):
proxy_http_version 1.0;          # ← HTTP/1.0 不支持 Expect
proxy_set_header Expect "";      # ← 主动清空 Expect 头

这是历史遗留设计------早期 HTTP/1.0 代理不理解 Expect,为避免后端困惑直接丢弃。

4.2 正确配置

nginx 复制代码
upstream starrocks {
    server fe1:8030;
    server fe2:8030;
}

server {
    listen 8080;

    location /api/ {
        proxy_http_version 1.1;                    # 必须 1.1
        proxy_set_header Expect $http_expect;      # 透传 Expect
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;

        # Stream Load 通常 Body 很大,需要调整以下参数
        client_max_body_size 10g;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
        proxy_buffering off;                       # 大文件建议关闭缓冲

        proxy_pass http://starrocks;
    }
}

4.3 验证方法

bash 复制代码
# 方法1:curl -v 观察握手过程
curl -v -T data.csv http://nginx:8080/api/db/table/_stream_load

# 期望看到:
# > Expect: 100-continue
# < HTTP/1.1 100 Continue     ← 如果看到这行,说明透传成功
# < HTTP/1.1 200 OK

# 方法2:抓包确认
tcpdump -i eth0 -A 'tcp port 8030' | grep -i "100-continue"

五、应用场景

5.1 场景总览

系统 是否强制 典型 Body 大小 说明
StarRocks Stream Load ✅ 强制 64MB ~ 数GB 协议契约
阿里云 MaxCompute Tunnel ✅ 强制 64MB ~ 256MB SDK 硬编码
AWS S3 PutObject (>2MB) SDK 自动添加 5MB ~ 5GB 非强制但推荐
HDFS WebHDFS ❌ 不要求 任意 大文件有益
通用 REST API ❌ 不要求 通常 < 10MB 视情况决定

5.2 阿里实际案例:MaxCompute Tunnel Upload

业务背景:DataWorks 数据集成将业务库(MySQL/Oracle)、日志、OSS 对象批量导入 MaxCompute 表。

arduino 复制代码
┌──────────────┐     ┌─────────────────┐     ┌──────────────────┐
│  DataWorks   │     │   Tengine       │     │  MaxCompute      │
│  Sync Engine │────→│   (Nginx 分支)   │────→│  Tunnel Server   │
│              │     │                 │     │                  │
│ HttpClient   │     │ proxy_http_     │     │ 强制校验         │
│ 自动加       │     │ version 1.1     │     │ Expect 头        │
│ Expect:      │     │ proxy_set_      │     │                  │
│ 100-continue │     │ header Expect   │     │ 校验通过→回100   │
│              │     │ $http_expect    │     │ 校验失败→回4xx   │
└──────────────┘     └─────────────────┘     └──────────────────┘

规模数据

  • 单次 Block:64MB ~ 256MB
  • 单任务并发:32 ~ 128 通道
  • 单 Region 并发连接:数万
  • 前置校验拒绝率(高峰期):2% ~ 8%

量化收益

指标 无 100-continue 有 100-continue 改善
失败请求耗时 ~2.1s ~3ms 700x
无效带宽 (高峰) ~30GB/min ~0 100%
Tunnel P99 延迟 850ms 120ms 7x
失败恢复时间 分钟级 秒级 60x
arduino 复制代码
┌────────────┐     ┌────────┐     ┌──────────────┐
│   Flink    │     │ Nginx  │     │  StarRocks   │
│ Connector  │────→│ 代理   │────→│  FE / BE     │
│            │     │        │     │              │
│ HttpClient │     │ 需配置 │     │ 强制要求     │
│ 硬编码     │     │ 透传   │     │ Expect 头    │
│ Expect头   │     │ Expect │     │              │
└────────────┘     └────────┘     └──────────────┘

与阿里内部链路完全同构:Tengine ↔ Nginx,Tunnel ↔ StarRocks,DataWorks ↔ Flink。


六、使用对比:有 vs 无

6.1 行为对比

bash 复制代码
# ❌ 无 Expect 头 → StarRocks 直接拒绝
$ curl -T data.csv http://starrocks:8030/api/db/tbl/_stream_load
{"Status":"Fail","Message":"There is no 100-continue header"}

# ✅ 有 Expect 头 → 正常导入
$ curl -H "Expect: 100-continue" -T data.csv \
    http://starrocks:8030/api/db/tbl/_stream_load
{"Status":"Success","NumberLoadedRows":100000}

6.2 性能对比(模拟 100MB 文件,1% 失败率)

指标 无 100-continue 有 100-continue
成功请求耗时 800ms 801ms (+1ms)
失败请求耗时 801ms (白传) 2ms (快速拒绝)
期望耗时 800.01ms 793.01ms
失败时带宽浪费 100MB 0
128 并发下失败带宽 12.8GB/批 0

6.3 代码对比

Java (HttpClient)

java 复制代码
// ❌ 不带 Expect → StarRocks 400
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("http://sr:8030/api/db/tbl/_stream_load"))
    .PUT(HttpRequest.BodyPublishers.ofFile(path))
    .build();

// ✅ 带 Expect → 正常
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("http://sr:8030/api/db/tbl/_stream_load"))
    .header("Expect", "100-continue")   // ← 关键
    .PUT(HttpRequest.BodyPublishers.ofFile(path))
    .build();

curl

bash 复制代码
# -T 自动添加 Expect: 100-continue
curl -T file.csv http://sr:8030/api/db/tbl/_stream_load

# -d @file 超过 1024 字节才自动添加
curl -d @file.csv http://sr:8030/api/db/tbl/_stream_load

Go (net/http)

go 复制代码
// Go 标准库从不自动添加,必须手动设置
req, _ := http.NewRequest("PUT", url, body)
req.Header.Set("Expect", "100-continue")  // ← 必须手动

七、什么时候该用?什么时候不该用?

7.1 判断矩阵

场景 是否需要 原因
StarRocks Stream Load 强制 协议契约,与大小无关
MaxCompute Tunnel 强制 SDK 硬编码,与大小无关
通用 API,Body < 10KB ❌ 不需要 RTT 开销 > 传 Body 开销
通用 API,Body 10KB~1MB,失败率 < 1% ⚠️ 可选 收益有限
通用 API,Body > 1MB,失败率 > 1% ✅ 推荐 正收益
通用 API,Body > 100MB 强烈推荐 失败代价极高
内网微服务 RPC ❌ 通常不用 延迟敏感,失败率极低

7.2 核心判断公式

css 复制代码
使用 100-continue 的期望收益 = 失败率 × Body传输时间 - RTT

当 失败率 × Body传输时间 > RTT 时,使用有正收益。

7.3 一个常见误区

"我的程序保证 99.9% 成功率,100-continue 纯属浪费。"

这个推理有三个漏洞

假设 现实
"99.9% 可用 = 99.9% 接受 Body" ❌ 服务可用 ≠ 接受数据(限流/Schema变更/Quota)
"额外 RTT 很贵" ❌ 同连接上几十字节响应,内网 < 0.2ms
"程序能保证成功率" ❌ 无法保证外部系统行为不变

💡 类比 :100-continue 是数据导入的"安全带"。你 99.9% 的行程不会撞车,但不会因此拆掉安全带------因为它保护的不是平均情况,而是最坏情况下的灾难性后果


八、RTT 到底是什么?

在讨论中反复出现"额外 RTT",这里做一个精确定义:

8.1 RTT = Round-Trip Time(往返时延)

arduino 复制代码
Client                          Server
  │                                │
  │─────── 请求 ──────────────────→│  ← t1 (单程传播)
  │                                │  ← 处理耗时
  │←────── 响应 ──────────────────│  ← t2 (单程传播)
  │                                │

RTT = t1 + 处理时间 + t2

8.2 在 100-continue 中,"额外 RTT" 特指

css 复制代码
正常:Headers+Body → 200 OK           (1 次 RTT)
Expect:Headers → [100 Continue] → Body → 200 OK  (2 次 RTT)
                    ↑
              这就是"额外 RTT"
              仅一个几十字节的中间响应
              ≠ 新建连接 / DNS / TLS

8.3 各环境 RTT 量级

环境 RTT 感知
同机器 loopback < 0.01ms
同机房 0.05 ~ 0.3ms
跨 AZ 0.5 ~ 2ms 极轻
同城公网 5 ~ 20ms
跨省 20 ~ 50ms 可感知
跨国 100 ~ 300ms 明显

在内网环境下,额外 RTT ≈ 传输 1~10KB 数据的时间,对于 MB 级以上的 Body 完全可忽略。


九、各语言/工具的默认行为速查

客户端 是否自动添加 阈值 备注
curl -T ✅ 始终 无阈值 只要用 -T 就加
curl -d @file ✅ 有条件 > 1024B POST 数据超 1KB
Java HttpURLConnection --- 需手动设置
Java HttpClient (11+) --- 需手动设置
Apache HttpClient ⚠️ 可配置 默认开启 ExpectContinueEnabled
Python requests ⚠️ 部分版本 > 1MB 行为不一致
Go net/http --- 必须手动
Flink StarRocks Connector ✅ 硬编码 无阈值 内部自动处理
MaxCompute SDK ✅ 硬编码 无阈值 协议强制

十、最佳实践清单

✅ DO

  • 对接 StarRocks Stream Load 时,所有请求 必须带 Expect: 100-continue
  • Nginx 代理层配置 proxy_http_version 1.1 + proxy_set_header Expect $http_expect
  • 大文件上传(> 1MB)到通用 HTTP 服务时,主动添加该头
  • 设置合理的 Expect 超时(通常 1~3s),超时后决定是否继续发送 Body
  • 监控 100 Continue 响应延迟,作为服务端前置校验耗时的指标

❌ DON'T

  • 不要按文件大小判断是否需要------协议强制场景与大小无关
  • 不要在 HTTP/1.0 下使用------协议不支持
  • 不要忘记 Nginx 的 client_max_body_size 限制
  • 不要在生产环境依赖"程序保证成功率"来跳过 Expect 机制
  • 不要把 100-continue 当作性能优化------它是安全机制

十一、总结

matlab 复制代码
┌─────────────────────────────────────────────────────────┐
│                                                         │
│   100-continue 的本质:                                  │
│                                                         │
│   不是性能优化,是数据导入协议的安全契约。                 │
│   不是"大文件才需要",是"协议要求就需要"。               │
│   不是"服务不可用才生效",是"任何拒绝都能快速生效"。     │
│                                                         │
│   在 StarRocks / MaxCompute 等系统中:                   │
│   它的存在不是为了优化那 0.1% 的失败,                   │
│   而是为了确保 100% 的请求都经过前置校验后才投入资源。    │
│                                                         │
└─────────────────────────────────────────────────────────┘

下次当你在 Nginx 日志中看到 There is no 100-continue header,你已经知道:这不是一个"奇怪的报错",而是数据导入协议在告诉你------请先敲门,再进屋。


相关推荐
程序员cxuan2 小时前
Anthropic:session 之间可以相互通信了
人工智能·后端·程序员
卷无止境3 小时前
FastAPI 前端托管全攻略:从静态文件到大型全栈项目架构
后端·python
webmote333 小时前
用 NVIDIA Nemotron 3 Super + .NET 构建有记忆的多轮对话
后端·算法
神奇小汤圆3 小时前
JUC三大常用工具类CountDownLatch、CyclicBarrier、Semaphore
后端
卷无止境3 小时前
当FastAPI项目开始"膨胀",代码该往哪儿放
后端·python
fliter3 小时前
程序员每天能省 1 小时的 50 个 macOS/终端/Git/浏览器技巧
后端
神奇小汤圆3 小时前
Redis为什么使用哈希槽而不用一致性哈希
后端
用户8356290780513 小时前
Python设置PowerPoint幻灯片背景的方法
后端·python
站大爷IP4 小时前
Python 的 is 把我坑惨了,原来 == 和 is 在小整数池外完全是两码事
后端