跳转到内容

反向代理一个应用

反向代理是大多数人最初想要的东西:一个公开地址、后面一到多个应用实例、应用本身 不用改。本页从单个上游开始,一直搭到带健康检查、超时和备用的上游池,并展示应用 在另一侧看到什么。

  • 已经安装并运行 Pingclair(安装),实验期间先停掉服务: sudo pc service stop
  • 一个在本地端口监听的应用。示例用 127.0.0.1:3000
  • 代理自己的端口,示例用 :8080
{
admin 127.0.0.1:2019
}
http://:8080 {
reverse_proxy 127.0.0.1:3000
}
终端窗口
sudo cp Pingclairfile /etc/Pingclair/Pingclairfile
sudo pingclair validate /etc/Pingclair/Pingclairfile
sudo kill -USR1 "$(systemctl show -p MainPID --value pingclair)"
curl -i http://localhost:8080/

响应来自应用,它的响应头原样透传。admin 是为了让 pingclair reload 能联系 运行中的服务器;SIGUSR1 不需要它(重载意味着什么)。

to 列出实例,再选择分流方式:

http://:8080 {
reverse_proxy {
to 127.0.0.1:3000
to 127.0.0.1:3001
lb_policy round_robin
}
}

两台都活着时,六次请求会交替;这是用「哪个端口回答了」的应用测出来的:

3000 3001 3000 3001 3000 3001
lb_policy 行为
round_robin 按顺序每个上游一次。默认值。
random 随机选一个上游。
least_conn 当前连接数最少的上游。
ip_hash 同一客户端地址总是落到同一台。
first 第一个可用的上游。
header <名>, cookie <名>, query <名> 按该字段哈希,让会话固定在一台。
weighted_round_robin <权重> … 在同一行给出每台上游的权重。

权重也可以逐台写,当理由各不相同时更好读:

http://:8080 {
reverse_proxy {
to 127.0.0.1:3000 {
weight 3
}
to 127.0.0.1:3001
}
}

⚠️ lb_policy weighted_round_robin 3 1 数的是已经写出来的上游,所以 to 行必须 写在前面。写反了,validate 会用 2 weights were given for 0 upstreams 拒绝该文件。

标了 backup 的上游只在其他上游全部不可用时才使用:

http://:8080 {
reverse_proxy {
to 127.0.0.1:3000
to 127.0.0.1:3001 {
backup
}
}
}

两台都在时,所有请求都去 3000。停掉那个进程,下一个请求由 3001 应答。

没有检查时,上游要等到某个请求失败之后才会被摘掉;有检查则会提前摘掉:

http://:8080 {
reverse_proxy {
to 127.0.0.1:3000
to 127.0.0.1:3001
health_check {
path /health
interval 2s
timeout 1s
status 200
consecutive_failure 2
consecutive_success 1
}
}
}

应用需要一个便宜就能应答的端点(这里是 /health)。每次状态变化都会写进日志, 这也是查清某台为什么被摘掉的办法:

INFO pingclair_proxy::health_check: 🩺 Active upstream health changed backend=Inet(127.0.0.1:3001) healthy=false
INFO pingclair_proxy::health_check: 🩺 Active upstream health changed backend=Inet(127.0.0.1:3001) healthy=true

在这套配置上实测:杀掉第二台,全部流量转到第一台;它回来后,在 consecutive_success 次成功探测之后重新加入轮换。真实 Caddyfile 常用的扁平写法 (health_urihealth_intervalhealth_timeouthealth_statushealth_failshealth_passes)设置的是同一个检查。

超时写在 reverse_proxy 里面的 transport http 块中,而不是直接写在它下面:

http://:8080 {
reverse_proxy {
to 127.0.0.1:3099
to 127.0.0.1:3000
transport http {
connect_timeout 1s
first_byte_timeout 1s
read_timeout 30s
write_timeout 30s
}
}
}

实测:127.0.0.1:3099 什么都不接受时,connect_timeout 1s 花掉一秒,请求会重试 到第二个上游并拿到 200。只接受连接、正文等三秒的应用会触发 first_byte_timeout 1s,客户端收到 504

dial_timeout 不是 reverse_proxy 的选项;写在那里,validate 会用 Unknown directive 'reverse_proxy: dial_timeout' 拒绝。transport http 里的名字 是 connect_timeout

上游可以是名字而不是地址,这正是换了 IP 重新起来的容器需要的:

{
dns_refresh 5s
}
http://:8080 {
reverse_proxy {
to api.internal:3000
}
}

按这个间隔重新解析域名,变化会写进日志:

INFO pingclair_proxy::dns: 🔄 Upstream DNS scheduler enabled interval_secs=5 pools=1
INFO pingclair_proxy::dns: 🔄 Upstream DNS refresh changed=1 adopted=0 kept_stale=0 unresolved=0

/etc/hosts 当作唯一事实来源实测:把 api.internal 指向 127.0.0.1 时由第一台 应答;改成 127.0.0.2 后,在一个间隔内改由第二台应答,不需要重启,也没有请求 失败。解析失败时,轮换里会保留上一个地址。

应用收到原始的 Host,以及按惯例放在请求头里的客户端地址:

{
"host": "127.0.0.1:8080",
"x_forwarded_for": "127.0.0.1",
"x_forwarded_proto": "http",
"x_real_ip": "127.0.0.1"
}

如果前面还有一层代理,除非它被列进 trusted_proxies,这些头里的地址就是那一层 的地址;Cloudflare Tunnel 指南 讲的正是这种 情况。

  • 代理返回 502 没有任何上游应答。确认应用在监听 (sudo ss -ltnp | grep :3000),地址也没写错。
  • 停顿后 504 有超时触发:后端慢是 first_byte_timeout,正文慢是 read_timeout,对方从不接受连接是 connect_timeout
  • Unknown directive 'reverse_proxy: …' 该选项属于嵌套块 —— 超时在 transport http 下,检查在 health_check 下 —— validate 会指明它拒绝的 确切写法。
  • 配置改动没有生效。 重载应用的是策略,不是新的监听器;当重载新增或移动了 监听器时,unit 的 status line 会指出变化的地址,sudo pc service restart 才是应用它的命令。见 以服务方式运行
  • 所有请求都落到同一台。 那是唯一健康的上游。健康检查日志会说明其他几台何时 因何被摘掉(ConnectRefusedfailure_statuses 等)。