wcore / dnslb
DNS-based load balancer and autoscaler (Route 53 latency routing or Cloudflare round robin) for Linode and AWS EC2
Requires
- php: >=7.4
- ext-curl: *
- ext-json: *
- ext-pdo_sqlite: *
- ext-simplexml: *
Requires (Dev)
- phpunit/phpunit: ^9.6
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
基于 DNS 的负载均衡与自动伸缩控制器,支持 AWS Route 53(按延迟就近)和 Cloudflare(轮询,可开启代理)。纯 PHP(7.4 ~ 8.x)、零 composer 运行时依赖,以 composer 包的形式在多个项目中复用。
- Route 53:用户按延迟就近解析到最近区域的节点(latency 路由 + 每个区域一组多值回答记录)
- Cloudflare:所有区域的节点在同一个域名下轮询,适合不需要就近访问、但需要健康检查和故障恢复的服务;开启代理(橙云)时摘除节点几秒内生效
- 自建并行健康检查,故障节点自动从 DNS 摘除,不使用(也不支付)DNS 服务商的健康检查
- 按 Prometheus 兼容指标(Grafana Cloud 等)自动扩缩容,节点在 Linode / AWS EC2 上通过 cloud-init 自动部署
- 缩容先从 DNS 摘除,排空一段时间后再删除机器,避免 DNS 缓存导致用户访问到已删除的节点
- 自动修复故障节点,SES 邮件告警,心跳推送用于监控控制器本身
- 支持通配符域名(如
*.example.com)、IPv6、从现有记录无中断切换
设计细节见 docs/design.md。
工作原理
Route 53(dns.provider 为 route53,默认):
app.example.com(或 *.example.com) Latency Alias 记录,每个区域一条(A / AAAA)
├─ ap-northeast-1 → tokyo.pool.app.example.com 多值回答记录,每个健康节点一条
└─ eu-central-1 → frankfurt.pool.app.example.com
Cloudflare(dns.provider 为 cloudflare):
api.example.com(或 *.example.com) 每个健康节点一条 A / AAAA 记录(所有池),备注 "dnslb node <节点 ID>"
开启代理时,用户连接最近的 Cloudflare 边缘,边缘在这些记录中随机选择节点回源,失败时自动重试其他节点。
控制器运行在一台常驻的小机器上(EC2 或 Linode 均可),通过 PHP 调用本包。由调度器(例如 Laravel scheduler)每分钟调用两个方法:
| 方法 | 职责 |
|---|---|
health() |
每 10 秒并行探测所有节点的健康检查地址,连续失败摘除、连续成功加回,并同步 DNS |
tick() |
推进节点生命周期 → 自动修复 → 维持 min / max → 按指标扩缩容 → 同步 DNS → 发送通知 → 心跳 |
节点生命周期:建机(PROVISIONING)→ 启动(BOOTING)→ 预热(WARMING,健康检查通过才上线)→ 在线(IN_SERVICE)→ 排空(DRAINING)→ 删除(TERMINATED)。
环境要求
- PHP 7.4 及以上,扩展:curl、json、pdo_sqlite、simplexml
- 控制器机器:能访问各节点的健康检查端口;开启 IPv6(
dns.ipv6)时必须有 IPv6 出口 - 节点镜像支持 cloud-init(Linode 需选择支持 Metadata 服务的机房和镜像)
- DNS:AWS Route 53 托管区域,或 Cloudflare 上的区域(域名的 NS 已切换到 Cloudflare)
- AWS 账号:使用 Route 53、EC2 或 SES 时需要
- 可选:Linode API token、Grafana Cloud(或其他 Prometheus 兼容服务)
安装
composer config repositories.dnslb vcs https://github.com/jasonbigl/wcore-dnslb.git composer require wcore/dnslb:dev-main
通过 Wcore\Dnslb\Dnslb 在 PHP 代码中使用(见「使用」)。
上手步骤
1. 准备凭证
- AWS:创建一个 IAM 用户,使用 docs/iam-policy.json 中的最小权限策略(把
ZONE_ID、ACCOUNT_ID、SERVICE、SES_REGION替换成实际值)。- EC2 只能创建带
dnslb:service=<服务名>标签的实例,也只能删除带这个标签的实例。 - 配置了
iam_instance_profile时,还需要对该角色的iam:PassRole权限。 route53:TestDNSAnswer只有tools/verify-route53.php用到。
- EC2 只能创建带
- Cloudflare(使用 Cloudflare DNS 时):创建 API 令牌,只授予该区域的
Zone - DNS - Edit和Zone - Zone - Read。 - Linode:创建 Personal Access Token,权限:Linodes 读写、Images 只读;使用
firewall_id时还需要 Firewalls 读写。 - Grafana Cloud:一个只读 token(控制器查询指标)和一个只写 token(节点上报指标、控制器推送心跳)。
- SES:验证发件域名或地址,并申请移出沙箱(否则只能发给已验证的地址)。
2. 编写配置
复制 examples/config.php(Route 53)或 examples/config-cloudflare.php(Cloudflare),每个服务(域名)一个配置文件,例如 /etc/dnslb/web.php。示例中每一项都有注释。要点:
pools:launch按顺序列出建机方式,前一种被明确拒绝(容量不足、配额等)自动用下一种;结果未知(超时、限流、5xx)时先在同一方式上确认,2 分钟后仍查不到实例才换下一种,避免在两处各建一台。使用 Route 53 时每个池对应一个 AWS 区域(latency_region,Linode 机房映射到地理上最近的 AWS 区域);使用 Cloudflare 时不需要latency_region,池只用于分区域建机和扩缩容。scaling.metrics:PromQL 查询必须按节点返回,并带node_id标签,例如... by (node_id)。user_data.template:cloud-init 模板,参考 examples/cloud-init.yaml。内置占位符{{node_id}}、{{service}}、{{pool}}、{{provider}}、{{region}}、{{latency_region}},其余在user_data.vars中定义。节点需要把指标带上node_id标签上报。- 通配符域名:
dns.name写成'*.example.com',并必须配置health_check.host为一个通配符覆盖的具体域名(用于 SNI 和 Host 头)。 - 凭证建议通过
getenv()读取,不要提交到代码仓库。
3. 检查与演练
$lb = Dnslb::fromConfigFile('/etc/dnslb/web.php'); foreach ($lb->check()->items as $item) { // 只读:凭证、DNS、建机参数、指标查询、SES、探测机出口 echo "[{$item->status}] {$item->name}:{$item->message}\n"; } $lb->tick(true); // 演练:日志中输出将要创建的机器和 DNS 变更,不做任何修改
4. 定时运行
每分钟调用一次 tick() 和 health(),Laravel 的写法见下文「Laravel 示例」。health() 每次运行约 55 秒,两者要并行调度,不能串行。两个方法各自有进程锁,重复启动不会并发执行。多个服务就各自一份配置、各自调度。
5. 从现有记录切换(如果域名已有记录)
对外域名上已有不由 dnslb 管理的 A / AAAA / CNAME 记录时,dnslb 不会修改这个名称(Route 53 只发布池记录,Cloudflare 不发布任何记录),并在日志中提示。待 status() 显示各池已有在线节点后:
foreach ($lb->takeover(true)->changes as $change) { // 预览将要删除和创建的记录 echo $change->describe(), "\n"; } $lb->takeover(); // 替换现有记录,解析不中断
- Route 53:删除旧记录和创建 latency 记录在同一个原子批次中完成。
- 未开启 IPv6(
dns.ipv6 = false)时,旧的 AAAA 记录也会被删除:保留的话双栈客户端会优先走 IPv6,继续访问旧服务器。之后只有纯 IPv6 的客户端无法访问(Cloudflare 代理模式下边缘本身提供 IPv6,不受影响)。预览时请确认要删除的记录。 - Cloudflare:先添加节点记录,等待几秒传播后再删除旧的 A / AAAA 记录,期间新旧记录一起轮询。旧记录是 CNAME 时(不能与 A 记录并存)只能在同一批次中先删后增,各边缘节点可能有几秒不一致。
- 接管后记录
dns_takeover事件。之后名称下又出现不由 dnslb 管理的记录时(它们会与节点一起分到流量),发出一次dns_foreign_records告警,删除后收到恢复通知。
6. 监控控制器本身
控制器挂掉时,健康检查和邮件通知也会停止,所以需要外部监控心跳。tick 和 health 每次正常运行结束都会发送心跳(heartbeat 配置);tick 有步骤失败(例如 DNS 同步失败、状态库无法写入)时不发送。tick 和 health 要分别监控,否则其中一个停止时另一个的心跳会让监控保持正常:
- Grafana Cloud:按示例配置推送 Influx 行协议
dnslb_heartbeat,service=web,cmd=tick up=1,然后建一条告警规则。指标名一般为dnslb_heartbeat_up(measurement_field),可以先在 Explore 中确认。规则示例:absent_over_time(dnslb_heartbeat_up{service="web", cmd="tick"}[5m]) absent_over_time(dnslb_heartbeat_up{service="web", cmd="health"}[5m]) - healthchecks.io / Uptime Kuma 等:
heartbeat.url填它们的 ping 地址(默认 GET)。地址能用{{cmd}}区分时为tick和health各建一个检查项,例如 healthchecks.io 的 slug 地址https://hc-ping.com/<ping key>/web-{{cmd}};只能配一个地址时(如 Uptime Kuma 的推送令牌),health单独停止不会被发现。
使用
use Wcore\Dnslb\Dnslb; $lb = Dnslb::fromConfigFile('/etc/dnslb/web.php', ['logger' => $psrLogger]); // logger 可选 // 或直接用配置数组:Dnslb::fromConfigArray($configArray, $baseDir),$baseDir 是配置中相对路径的基准目录
| 方法 | 返回 | 说明 |
|---|---|---|
tick(bool $dryRun = false) |
bool |
每分钟调度:生命周期、自动修复、扩缩容、DNS、通知、心跳。上一个 tick 未结束时返回 false |
health(int $duration = 55, bool $once = false, bool $dryRun = false) |
int |
每分钟调度:每 health_check.interval 秒一轮健康检查,返回完成的轮数 |
status() |
array |
各池、节点状态和最近事件(只读本地状态库),结构见 Operations\StatusReport |
check() |
CheckReport |
校验配置和外部服务(只读)。items 为检查项(status 为 ok / warn / fail、name、message),passed() 表示没有失败项 |
takeover(bool $dryRun = false) |
SyncResult |
无中断地接管对外域名上的现有记录。changes 为变更列表(describe() 可读描述),changeIds 为 Route 53 变更 ID(Cloudflare 为空) |
scale(string $pool, int $delta, bool $dryRun = false) |
ScaleResult |
手动扩缩容。requested / allowed(受 min / max 限制后的台数,缩容后剩下的健康节点也不少于 min)、created / failed / drained 为节点列表 |
drain(string $nodeId, bool $dryRun = false) |
Node |
立即从 DNS 摘除并排空指定节点 |
replace(string $nodeId, bool $dryRun = false) |
Node |
先建替补(返回替补节点),上线后排空指定节点 |
rollout(?string $pool = null, int $batch = 1, bool $dryRun = false) |
RolloutResult |
发布新版本:用新机器逐台替换现有节点,由之后的 tick 逐步完成(见下方「发布新版本」)。nodes 为池 => 待替换节点,warnings 为需要注意的情况 |
cancelRollout(?string $pool = null) |
— | 停止发布;已开始的替换继续完成,其余旧节点保持不变 |
pause(?string $pool = null) / resume(?string $pool = null) |
— | 暂停 / 恢复自动增删机器(维护时用;健康检查和 DNS 摘除照常)。resume 同时解除连续启动失败造成的自动暂停;$pool 为 null 时作用于所有池 |
rebuildState(bool $dryRun = false) |
RebuildResult |
状态库丢失后,根据云上实例和 DNS 记录重建,并记录 state_rebuilt 事件。restored 为恢复的节点,warnings 为需要确认的情况 |
cleanupOrphans(bool $dryRun = true) |
OrphanCleanupResult |
删除孤儿实例:云上带本服务标签、但连续 15 分钟没有节点引用的实例(tick 每 10 分钟检查并告警,status() 的 orphans 中列出)。默认只预览,orphans 为孤儿列表;传 false 删除,terminated / failed 为结果。状态库从备份恢复后先 rebuildState(),不要直接删除 |
app() |
Application |
底层组件(高级用法),例如 $lb->app()->nodes()->live() |
- 节点对象(
Wcore\Dnslb\State\Node)的常用属性:nodeId、pool、state、provider、providerRegion、instanceId、ipv4、ipv6、lastError。 - 参数错误抛出
InvalidArgumentException(例如池或节点不存在);操作无法执行抛出Operations\OperationException(例如节点不在线、替补建机失败);外部服务异常原样抛出。 - 修改节点的方法持有 tick 锁,不会与
tick()或其他手动操作同时执行(最多等 60 秒)。 $dryRun = true:在状态库的内存副本上执行,云厂商写操作和 DNS 变更只记录日志,不修改任何东西。logger可以是任意 PSR-3 风格的 logger,消息带[dnslb:<操作>]前缀;不传时按配置的log段输出到标准输出或文件。- 每次调用都创建新的内部实例,
Dnslb对象可以在常驻进程中长期持有。 - 只能在一台机器上运行(锁和状态库都在本机),多台服务器部署时要固定在其中一台。
发布新版本
节点不在原地更新:新机器开机时自动部署"当前版本"(由 cloud-init / 你的部署脚本实现),dnslb 负责用新机器替换旧机器。
- 发布流程先更新"当前版本"(例如镜像 tag),确认新开的机器能拿到新版本;
- 调用
$lb->rollout()(或rollout('dallas')只发布一个池,rollout(null, 2)每个池同时替换 2 台)。
调用时池中已有的节点(包括尚未上线的)都标记为旧版本,之后每次 tick:
- 每个池同时最多替换
batch台:先建替补,替补通过健康检查上线后才排空旧节点,在线节点数不会减少,替补不占max名额; - 发布期间因扩容、自动修复新建的节点都是新版本,不会被替换;缩容优先排空旧节点;替换不会让扩缩容等待;
- 手动
pause()或健康检查冻结时等待; - 新版本有问题、新机器通不过健康检查时:连续
autoheal.max_warmup_failures台未能上线后,该池自动暂停,发布停止并发送 CRITICAL 邮件,剩余旧节点继续服务。修复后先resume()再重新rollout(); - 全部旧节点下线后记录
rollout_completed事件。进度见status()中池的rollout(total、remaining、replacing),节点的outdated表示等待替换。
耗时约为"每个池的节点数 ÷ batch × 单台从建机到上线的时间",旧机器在排空(drain.max)后才删除。发布进行中再次调用 rollout() 会重新开始,已替换上的节点也会再替换一次。健康检查是唯一的上线门槛,健康检查路径应能反映新版本是否可用。
Laravel 示例
// AppServiceProvider::register() $this->app->singleton(Dnslb::class, function () { return Dnslb::fromConfigFile(config('services.dnslb.config'), ['logger' => Log::channel('dnslb')]); });
// app/Console/Commands/DnslbTick.php(health 同理,调用 $lb->health()) class DnslbTick extends Command { protected $signature = 'dnslb:tick {--dry-run}'; public function handle(Dnslb $lb): int { return $lb->tick((bool) $this->option('dry-run')) ? 0 : 1; } }
// 调度:health 每次运行约 55 秒,必须 runInBackground(),否则会阻塞同一分钟的其他任务。 // 两个命令自带进程锁,不需要 withoutOverlapping()。 $schedule->command('dnslb:tick')->everyMinute()->runInBackground(); $schedule->command('dnslb:health')->everyMinute()->runInBackground();
// 手动扩缩容,例如 php artisan dnslb:scale dallas 1 class DnslbScale extends Command { protected $signature = 'dnslb:scale {pool} {delta} {--dry-run}'; public function handle(Dnslb $lb): int { $result = $lb->scale($this->argument('pool'), (int) $this->argument('delta'), (bool) $this->option('dry-run')); if ($result->allowed !== $result->requested) { $this->warn("受 min / max 限制,只执行 {$result->allowed} 台"); } foreach ($result->created as $node) { $this->info("已创建 {$node->nodeId}({$node->provider}/{$node->providerRegion})"); } foreach ($result->failed as $node) { $this->error("建机失败:{$node->lastError}"); } foreach ($result->drained as $node) { $this->info("排空 {$node->nodeId}"); } return $result->failed === [] ? 0 : 1; } }
// 发布新版本,例如在发布流程更新版本号之后执行 php artisan dnslb:rollout class DnslbRollout extends Command { protected $signature = 'dnslb:rollout {pool?} {--batch=1} {--dry-run}'; public function handle(Dnslb $lb): int { $result = $lb->rollout($this->argument('pool'), (int) $this->option('batch'), (bool) $this->option('dry-run')); foreach ($result->nodes as $pool => $nodes) { $this->info("池 {$pool}:" . count($nodes) . ' 台节点待替换'); } foreach ($result->warnings as $warning) { $this->warn($warning); } return 0; } }
// 状态 foreach ($lb->status()['pools'] as $pool) { $this->line("池 {$pool['name']}" . ($pool['latency_region'] ? "({$pool['latency_region']})" : '')); $this->table(['节点', '状态', 'IPv4', 'v4', 'v6'], array_map(fn ($n) => [ $n['node_id'], $n['state'], $n['ipv4'], $n['v4_healthy'] ? 'up' : 'down', $n['v6_healthy'] ? 'up' : 'down', ], $pool['nodes'])); }
// takeover:先预览,确认后再提交 foreach ($lb->takeover(true)->changes as $change) { $this->line($change->describe()); } if ($this->confirm('确认切换 DNS?')) { $result = $lb->takeover(); $this->info('已提交:' . implode(', ', $result->changeIds)); }
// 孤儿实例(收到 orphan_instance 告警后):先预览,确认后再删除 $preview = $lb->cleanupOrphans(); foreach ($preview->orphans as $orphan) { $this->line("{$orphan->provider}/{$orphan->region} {$orphan->instanceId}(节点 ID {$orphan->nodeId})"); } if ($preview->orphans !== [] && $this->confirm('确认删除这些实例?')) { $result = $lb->cleanupOrphans(false); foreach ($result->failed as $instanceId => $error) { $this->error("{$instanceId} 删除失败:{$error}"); } }
配置参考
| 配置 | 默认值 | 说明 |
|---|---|---|
dns.provider |
route53 | route53:按延迟就近;cloudflare:所有池的节点轮询(需要 cloudflare.api_token,池不配置 latency_region) |
dns.ttl |
60 | 节点记录 TTL。Cloudflare 代理记录固定为自动,仅 DNS 时最小 60(企业版 30) |
dns.ipv6 |
false | 全局 IPv6 开关;开启后每个节点都必须有 IPv6,探测机也必须有 IPv6 出口。Cloudflare 代理模式下客户端始终可用 IPv6 访问边缘,节点不需要 IPv6 |
dns.pool_routing |
multivalue | 仅 Route 53。multivalue:每次返回最多 8 个 IP,客户端可自行重试;weighted:按 launch[].weight 加权,每次返回 1 个 |
dns.pool_domain |
{pool}.pool.<name>(通配符时 {pool}.dnslb-pool.<base>) |
仅 Route 53。池记录名,必须专用于 dnslb |
dns.proxied |
true | 仅 Cloudflare。true:代理(橙云);false:仅 DNS(灰云) |
health_check.interval |
10 | 每轮间隔(秒) |
health_check.fall / rise |
3 / 2 | 连续失败多少次摘除 / 连续成功多少次加回 |
health_check.global_freeze_ratio |
0.5 | 同一轮超过该比例的在线节点失败时冻结,不摘除任何节点。只有 1 台在线节点时不判断 |
health_check.pool_failover_after |
180 | 仅 Route 53:整池故障持续多久后撤下该区域,流量转到其他区域。Cloudflare 下整池故障时直接撤下,由其他池承接 |
warmup.timeout |
900 | 从建机到上线的最长时间 |
drain.min / drain.max |
300 / 1800 | 排空时间,从确认节点已不在 DNS 中时算起:配置了 drain.idle 时,过了 min 且流量低于阈值即可删除;否则等满 max |
autoheal.unhealthy_for |
300 | 在线节点持续不健康多久后替换 |
autoheal.max_unhealthy_ratio |
0.5 | 所有在线节点中不健康比例超过此值视为系统性故障,只告警不替换。只有 1 台在线节点时不判断 |
autoheal.max_replacements_per_hour |
5 | 每小时自动替换上限 |
autoheal.max_warmup_failures / pause_for |
3 / 3600 | 连续多少台新节点未能上线就暂停该池建机,以及暂停多久 |
scaling.step |
1 | 每次扩 / 缩台数 |
scaling.sustain |
300 | 指标持续超过阈值多久才动作 |
scaling.cooldown_out / cooldown_in |
600 / 900 | 扩容冷却 / 缩容冷却(距上次任何扩缩容) |
scaling.min_coverage |
0.5 | 有指标数据的在线节点比例低于此值时不做决策 |
notify.throttle |
3600 | 同一告警最小发送间隔。恢复通知不受限制:已发出的告警恢复时一定会收到恢复通知,之后再次故障立即告警 |
池可以用 pools.<池>.scaling 覆盖全局 scaling 的任意项;把某个指标设为 null 可在该池禁用它。
注意事项
- DNS 负载均衡生效慢:新节点只能接到新解析的流量,已缓存 IP 的客户端在 TTL 内(部分运营商 DNS 更久)仍访问旧节点。扩容要留出冷却时间,缩容依靠排空期覆盖缓存。
- 单探测点:健康检查只从控制器一个位置发起。金丝雀地址检测探测机自身网络,全局冻结防止探测机线路问题导致大面积误摘;代价是同一轮超过半数节点真实故障时,故障节点会留在 DNS 中,需要人工介入(见下方「后续迭代」)。
- 单节点服务:整个服务只有 1 台在线节点时,比例保护没有意义(任何故障都是 100%),因此不冻结、也不判定为系统性故障。节点故障时记录保留(没有其他节点可切),持续
autoheal.unhealthy_for后建替补,替补上线后再排空旧节点。探测机到节点的线路故障也会触发替换:替补通常同样通不过检查,旧节点保留在 DNS 中;连续max_warmup_failures台失败后该池暂停建机并告警。2 台及以上时照常保护(两台同时故障仍会冻结)。 - IMDSv2:EC2 实例强制使用 IMDSv2,cloud-init 脚本读取实例元数据时需要先申请令牌。
- 按指标不会缩容到 0 台:缩容前会预测剩余节点的负载,剩 0 台时预测不成立。
- 容量到顶:已达到 max 而指标仍持续超过扩容阈值时,发出
scale_at_max告警(每个池每小时最多一次),需要人来决定调大 max 还是换用更大的机型。 - 节点上报指标必须带
node_id标签。查询返回同一节点多条序列,或序列缺少node_id标签时,该指标视为不可用,不做决策。 - 改名或替换全部池:建议先加新池,新节点上线后再删旧池。一次换掉全部池也不会断解析:没有可以发布的节点时,对外名称保留现有记录(发出
dns_empty_held告警),旧节点继续服务到新节点上线后再切换。反过来,删掉全部池并不会让域名下线,记录和旧机器会一直保留,下线整个服务需要手动删除记录(删除后收到dns_empty_released,排空开始计时,之后删除机器)。
使用 Cloudflare 时:
- 不区分区域:用户被随机分配到任一池的节点。开启代理时,Cloudflare 边缘随机选择节点回源,与用户所在区域无关。
- 代理模式下 DNS 缓存不再是问题:客户端解析到的是 Cloudflare 的地址,摘除节点几秒内在边缘生效,
drain可以设得短一些(例如 60 ~ 300 秒)。仅 DNS 模式与 Route 53 一样受客户端缓存影响。 - 节点侧配置(代理模式):防火墙放行 Cloudflare 的 IP 段(以及控制器的健康检查),真实客户端 IP 从
CF-Connecting-IP头读取;SSL/TLS 加密模式建议用 Full (strict),节点证书需覆盖对外域名。代理只支持 HTTP/HTTPS 的标准端口(443、8443、80、8080 等)。 - 记录数量:免费版 2024-09 之后添加的区域最多 200 条记录,每个节点占 1 ~ 2 条(A / AAAA)。
- IPv6 地址按规范形式比较(例如
2001:DB8:0::2与2001:db8::2视为同一地址)。
开发
composer install vendor/bin/phpunit # PHP 7.4 上测试(需要 pdo_sqlite) docker run --rm -v "$PWD":/app -w /app php:7.4-cli vendor/bin/phpunit
tools/verify-route53.php 和 tools/verify-cloudflare.php 在真实环境中用临时名称验证记录结构与同步逻辑(会自动清理,只操作没有现有记录的测试名称)。凭证读取环境变量 AWS_KEY、AWS_SECRET、CF_TOKEN,未设置时读取仓库根目录的 .env(已在 .gitignore 中):
php tools/verify-route53.php --zone <ZoneId> --domain '*.dnslb-verify.example.com' php tools/verify-cloudflare.php --zone <区域 ID> --domain '*.dnslb-verify.example.com'
Cloudflare 的验证使用通配符名称时,还会通过 DoH(Cloudflare,无法访问时改用 Google)解析新的子域名,确认代理记录解析到 Cloudflare 边缘、仅 DNS 记录解析到节点地址。
后续迭代
- 目标跟踪扩缩容模式:第一版只支持固定步长(每次扩/缩
step台)。后续增加目标跟踪模式,按ceil(当前台数 × 当前指标值 / 目标值)一次算出所需台数,并受每个池的 min/max 约束。 - 健康检查冻结优化:目前同一轮超过
global_freeze_ratio的在线节点失败就冻结,不摘除任何节点,直到比例回落;计算时也包含已被摘除的节点,故障逐个累积超过阈值时同样会冻结,之后新故障的节点会留在 DNS 中。可改进为:只统计本轮新出现的失败(原本健康的节点),并增加freeze_max(例如 300 秒),金丝雀可达且冻结超时后视为真实故障、恢复正常摘除。 - 多探测点投票:第一版健康检查只有控制器一个探测点,可能因探测机到某区域的网络问题误判。后续支持多台探测机各自探测、汇总结果投票决定节点健康状态。