mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-13 20:08:05 +08:00
💋 Sync 2026-11-02 01:02:56
This commit is contained in:
1 parent
d97dc35e3d
commit
2f941f0ffb
48 files changed
+4891
-1002
No files matched your search
+835
@@ -0,0 +1,835 @@
|
||||
#!/bin/sh
|
||||
|
||||
CONF="cf-optimizer"
|
||||
RUN_DIR="/tmp/run/cf-optimizer"
|
||||
TMP_DIR="/tmp/cf-optimizer"
|
||||
HOSTS_FILE="/tmp/hosts/cf-optimizer"
|
||||
LOG_FILE="/var/log/cf-optimizer.log"
|
||||
DEFAULT_IPS_FILE="/usr/share/cf-optimizer/default_ips.txt"
|
||||
COLO_MAP_FILE="/usr/share/cf-optimizer/colo_map.json"
|
||||
PID_FILE="$RUN_DIR/cf-optimizer.pid"
|
||||
COLO_CACHE_FILE="$RUN_DIR/colo_cache.txt"
|
||||
STATUS_JSON="$RUN_DIR/status.json"
|
||||
CURRENT_IP_FILE="$RUN_DIR/current_ip.txt"
|
||||
PRIMARY_IP_FILE="$RUN_DIR/primary_ip.txt"
|
||||
STANDBY_IP_FILE="$RUN_DIR/standby_ip.txt"
|
||||
BASELINE_TCP_FILE="$RUN_DIR/baseline_tcp.txt"
|
||||
FAIL_STREAK_FILE="$RUN_DIR/fail_streak.txt"
|
||||
|
||||
log() {
|
||||
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $1"
|
||||
echo "$msg"
|
||||
mkdir -p "$(dirname "$LOG_FILE")"
|
||||
echo "$msg" >> "$LOG_FILE"
|
||||
# 限制日志大小不超过 1000 行
|
||||
if [ -f "$LOG_FILE" ] && [ $(wc -l < "$LOG_FILE") -gt 1500 ]; then
|
||||
tail -n 800 "$LOG_FILE" > "$LOG_FILE.tmp" && mv -f "$LOG_FILE.tmp" "$LOG_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
get_colo_name() {
|
||||
local code="$1"
|
||||
if [ -f "$COLO_MAP_FILE" ] && [ -n "$code" ]; then
|
||||
local name
|
||||
name=$(grep -E "\"$code\"[[:space:]]*:" "$COLO_MAP_FILE" 2>/dev/null | head -n 1 | sed -E 's/.*:[[:space:]]*"([^"]+)".*/\1/')
|
||||
if [ -n "$name" ]; then
|
||||
echo "$name"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
echo "${code:-未知}"
|
||||
}
|
||||
|
||||
update_colo_cache() {
|
||||
local ip="$1"
|
||||
local colo="$2"
|
||||
[ -z "$ip" ] || [ -z "$colo" ] && return
|
||||
mkdir -p "$RUN_DIR"
|
||||
touch "$COLO_CACHE_FILE"
|
||||
grep -v "^$ip[[:space:]]" "$COLO_CACHE_FILE" > "$COLO_CACHE_FILE.tmp" 2>/dev/null
|
||||
echo "$ip $colo" >> "$COLO_CACHE_FILE.tmp"
|
||||
mv -f "$COLO_CACHE_FILE.tmp" "$COLO_CACHE_FILE"
|
||||
}
|
||||
|
||||
get_cached_colo() {
|
||||
local ip="$1"
|
||||
[ ! -f "$COLO_CACHE_FILE" ] && return
|
||||
grep "^$ip[[:space:]]" "$COLO_CACHE_FILE" 2>/dev/null | tail -n 1 | awk '{print $2}'
|
||||
}
|
||||
|
||||
clean_hosts() {
|
||||
mkdir -p "$RUN_DIR"
|
||||
if [ -f "$HOSTS_FILE" ]; then
|
||||
rm -f "$HOSTS_FILE"
|
||||
killall -HUP dnsmasq 2>/dev/null
|
||||
log "已清空优选 hosts 映射并重载 dnsmasq"
|
||||
fi
|
||||
rm -f "$CURRENT_IP_FILE" "$PRIMARY_IP_FILE" "$STANDBY_IP_FILE" "$BASELINE_TCP_FILE" "$FAIL_STREAK_FILE"
|
||||
rm -rf "$RUN_DIR/task.lock"
|
||||
cat <<EOF > "$STATUS_JSON"
|
||||
{
|
||||
"running": false,
|
||||
"last_update": "",
|
||||
"best_ip": "",
|
||||
"best_colo": "",
|
||||
"best_tcp_lat": 0,
|
||||
"best_tls_lat": 0,
|
||||
"standby_ip": "",
|
||||
"standby_colo": "",
|
||||
"standby_tcp_lat": 0,
|
||||
"watchdog_status": "未运行",
|
||||
"domains": [],
|
||||
"rankings": []
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
sync_hosts() {
|
||||
. /lib/functions.sh
|
||||
config_load "$CONF"
|
||||
local enabled
|
||||
config_get_bool enabled settings enabled 0
|
||||
if [ "$enabled" != "1" ]; then
|
||||
clean_hosts
|
||||
return 0
|
||||
fi
|
||||
|
||||
local domains=""
|
||||
append_sync_domain() {
|
||||
local d="$1"
|
||||
[ -n "$d" ] && domains="$domains $d"
|
||||
}
|
||||
config_list_foreach settings domains append_sync_domain
|
||||
|
||||
if [ -z "$domains" ]; then
|
||||
clean_hosts
|
||||
return 0
|
||||
fi
|
||||
|
||||
local current_ip=""
|
||||
[ -f "$CURRENT_IP_FILE" ] && current_ip=$(cat "$CURRENT_IP_FILE" 2>/dev/null)
|
||||
|
||||
if [ -z "$current_ip" ]; then
|
||||
local cand_tmp="$TMP_DIR/cand_init.txt"
|
||||
mkdir -p "$TMP_DIR"
|
||||
load_candidate_ips "$cand_tmp"
|
||||
current_ip=$(head -n 1 "$cand_tmp" 2>/dev/null)
|
||||
rm -f "$cand_tmp"
|
||||
fi
|
||||
|
||||
[ -z "$current_ip" ] && return 0
|
||||
|
||||
local hosts_tmp="$HOSTS_FILE.tmp"
|
||||
mkdir -p "$(dirname "$HOSTS_FILE")"
|
||||
> "$hosts_tmp"
|
||||
echo "# Auto-generated by cf-optimizer sync on $(date '+%Y-%m-%d %H:%M:%S')" >> "$hosts_tmp"
|
||||
for d in $domains; do
|
||||
echo "$current_ip $d" >> "$hosts_tmp"
|
||||
done
|
||||
mv -f "$hosts_tmp" "$HOSTS_FILE"
|
||||
killall -HUP dnsmasq 2>/dev/null
|
||||
log "已即时同步域名配置至 hosts 并重载 dnsmasq (定向至: $current_ip)"
|
||||
}
|
||||
|
||||
load_candidate_ips() {
|
||||
local out_file="$1"
|
||||
> "$out_file"
|
||||
|
||||
local ip_source
|
||||
config_get ip_source settings ip_source "builtin"
|
||||
|
||||
if [ "$ip_source" = "builtin" ] || [ "$ip_source" = "mixed" ]; then
|
||||
if [ -f "$DEFAULT_IPS_FILE" ]; then
|
||||
grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' "$DEFAULT_IPS_FILE" >> "$out_file"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$ip_source" = "custom" ] || [ "$ip_source" = "mixed" ]; then
|
||||
append_custom_ip() {
|
||||
local item="$1"
|
||||
[ -z "$item" ] && return
|
||||
# 若为 CIDR 简单网段 (如 /24),取前 3 个 IP
|
||||
case "$item" in
|
||||
*/*)
|
||||
local base_prefix=$(echo "$item" | cut -d'/' -f1 | cut -d'.' -f1-3)
|
||||
for host in 1 2 3; do
|
||||
echo "$base_prefix.$host" >> "$out_file"
|
||||
done
|
||||
;;
|
||||
*)
|
||||
echo "$item" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' >> "$out_file"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
config_list_foreach settings custom_ips append_custom_ip
|
||||
fi
|
||||
|
||||
# 兜底:若文件为空,填入稳定官方公共 IP
|
||||
if [ ! -s "$out_file" ]; then
|
||||
echo "104.16.0.1" >> "$out_file"
|
||||
echo "104.17.0.1" >> "$out_file"
|
||||
echo "104.18.0.1" >> "$out_file"
|
||||
echo "172.64.0.1" >> "$out_file"
|
||||
fi
|
||||
|
||||
# 排序去重
|
||||
sort -u "$out_file" > "$out_file.tmp" && mv -f "$out_file.tmp" "$out_file"
|
||||
}
|
||||
|
||||
# 粗筛单节点探测
|
||||
probe_fast() {
|
||||
local ip="$1"
|
||||
local res_dir="$2"
|
||||
# 仅测 TCP 握手
|
||||
local connect_time
|
||||
connect_time=$(curl -s -k -o /dev/null --connect-timeout 1 -m 2 -w "%{time_connect}\n" \
|
||||
--resolve "speed.cloudflare.com:443:$ip" "https://speed.cloudflare.com/__down?bytes=0" 2>/dev/null)
|
||||
|
||||
if [ $? -eq 0 ] && [ -n "$connect_time" ] && [ "$connect_time" != "0.000000" ]; then
|
||||
local ms=$(awk "BEGIN {printf \"%08.2f\", $connect_time * 1000}")
|
||||
echo "$ms $ip" > "$res_dir/$ip"
|
||||
fi
|
||||
}
|
||||
|
||||
run_optimization() {
|
||||
. /lib/functions.sh
|
||||
config_load "$CONF"
|
||||
|
||||
local enabled
|
||||
config_get_bool enabled settings enabled 0
|
||||
if [ "$enabled" != "1" ]; then
|
||||
log "服务未启用,退出任务"
|
||||
clean_hosts
|
||||
return 0
|
||||
fi
|
||||
|
||||
local domains=""
|
||||
append_domain() {
|
||||
local d="$1"
|
||||
[ -n "$d" ] && domains="$domains $d"
|
||||
}
|
||||
config_list_foreach settings domains append_domain
|
||||
|
||||
if [ -z "$domains" ]; then
|
||||
log "未配置目标加速域名,跳过优选"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local concurrency sample_count hysteresis_ms
|
||||
config_get concurrency settings concurrency 10
|
||||
config_get sample_count settings sample_count 4
|
||||
config_get hysteresis_ms settings hysteresis_ms 30
|
||||
|
||||
local colo_filter_mode
|
||||
config_get colo_filter_mode settings colo_filter_mode "none"
|
||||
|
||||
local target_colos=""
|
||||
append_target_colo() {
|
||||
local item="$1"
|
||||
[ -z "$item" ] && return
|
||||
for c in $item; do
|
||||
local clean_c=$(echo "$c" | tr 'a-z' 'A-Z' | tr -d ' \t\r\n')
|
||||
if [ -n "$clean_c" ]; then
|
||||
case " $target_colos " in
|
||||
*" $clean_c "*) ;;
|
||||
*) target_colos="$target_colos $clean_c" ;;
|
||||
esac
|
||||
fi
|
||||
done
|
||||
}
|
||||
if [ "$colo_filter_mode" != "none" ]; then
|
||||
local multi_colos=""
|
||||
config_get multi_colos settings colo_list ""
|
||||
[ -n "$multi_colos" ] && append_target_colo "$multi_colos"
|
||||
config_list_foreach settings colo_list append_target_colo
|
||||
config_list_foreach settings custom_colos append_target_colo
|
||||
fi
|
||||
|
||||
check_colo_allowed() {
|
||||
local colo="$1"
|
||||
[ -z "$colo" ] && return 0
|
||||
[ "$colo_filter_mode" = "none" ] && return 0
|
||||
|
||||
local match=0
|
||||
for c in $target_colos; do
|
||||
if [ "$c" = "$colo" ]; then
|
||||
match=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$colo_filter_mode" = "exclude" ]; then
|
||||
[ "$match" -eq 1 ] && return 1 || return 0
|
||||
elif [ "$colo_filter_mode" = "include" ]; then
|
||||
[ "$match" -eq 1 ] && return 0 || return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
log "开始执行优选任务,加速域名数: $(echo $domains | wc -w),机房策略: $colo_filter_mode${target_colos:+ ($target_colos)}"
|
||||
|
||||
local lock_dir="$RUN_DIR/task.lock"
|
||||
if ! mkdir "$lock_dir" 2>/dev/null; then
|
||||
local lock_pid=""
|
||||
[ -f "$lock_dir/pid" ] && lock_pid=$(cat "$lock_dir/pid" 2>/dev/null)
|
||||
if [ -n "$lock_pid" ] && ! kill -0 "$lock_pid" 2>/dev/null; then
|
||||
log "检测到残留任务锁 (PID: $lock_pid 已退出),自动释放死锁"
|
||||
rm -rf "$lock_dir"
|
||||
mkdir "$lock_dir" 2>/dev/null || return 0
|
||||
else
|
||||
log "已有优选任务正在运行中,跳过本次执行"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
echo $$ > "$lock_dir/pid"
|
||||
|
||||
mkdir -p "$TMP_DIR/fast" "$TMP_DIR/deep" "$RUN_DIR"
|
||||
rm -rf "$TMP_DIR/fast"/* "$TMP_DIR/deep"/*
|
||||
|
||||
local cand_file="$TMP_DIR/candidates.txt"
|
||||
load_candidate_ips "$cand_file"
|
||||
local total_cands=$(wc -l < "$cand_file")
|
||||
log "阶段一:开始并发初筛,候选节点总数: $total_cands,并发度: $concurrency"
|
||||
|
||||
# 并发初筛(采用 POSIX 批次等待机制,确保在任何非交互式 Shell 环境下严格限流)
|
||||
local running=0
|
||||
while read -r ip; do
|
||||
[ -z "$ip" ] && continue
|
||||
probe_fast "$ip" "$TMP_DIR/fast" &
|
||||
running=$((running + 1))
|
||||
if [ "$running" -ge "$concurrency" ]; then
|
||||
wait
|
||||
running=0
|
||||
fi
|
||||
done < "$cand_file"
|
||||
wait
|
||||
|
||||
# 汇总初筛候选,结合机房缓存预先剔除黑名单机房
|
||||
local fast_sorted_file="$TMP_DIR/fast_sorted.txt"
|
||||
cat "$TMP_DIR/fast"/* 2>/dev/null | sort -n | head -n 30 > "$fast_sorted_file"
|
||||
|
||||
local top_selected_file="$TMP_DIR/top_selected_ips.txt"
|
||||
> "$top_selected_file"
|
||||
local picked_count=0
|
||||
while read -r _lat ip; do
|
||||
[ -z "$ip" ] && continue
|
||||
local cached_colo=$(get_cached_colo "$ip")
|
||||
if [ -n "$cached_colo" ] && ! check_colo_allowed "$cached_colo"; then
|
||||
log "初筛预过滤: 节点 $ip ($cached_colo) 命中机房排除策略,自动略过"
|
||||
continue
|
||||
fi
|
||||
echo "$ip" >> "$top_selected_file"
|
||||
picked_count=$((picked_count + 1))
|
||||
[ "$picked_count" -ge 10 ] && break
|
||||
done < "$fast_sorted_file"
|
||||
|
||||
local top_count=$(wc -l < "$top_selected_file")
|
||||
if [ "$top_count" -eq 0 ]; then
|
||||
log "阶段一初筛无可用候选节点(或全部被机房区域策略过滤),请检查配置"
|
||||
rm -rf "$TMP_DIR"
|
||||
rm -rf "$lock_dir"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local probe_domain=$(echo "$domains" | awk '{print $1}')
|
||||
[ -z "$probe_domain" ] && probe_domain="speed.cloudflare.com"
|
||||
|
||||
log "阶段一初筛完成,选出 $top_count 个候选节点进入精测采样,探活域名: $probe_domain(每节点轮询交叉采样 $sample_count 轮)"
|
||||
|
||||
# 阶段二:轮询交叉时序采样(跨越完整测试时段,精准捕获阶段性丢包与网络毛刺)
|
||||
mkdir -p "$TMP_DIR/samples"
|
||||
|
||||
for seq in $(seq 1 "$sample_count"); do
|
||||
while read -r ip; do
|
||||
[ -z "$ip" ] && continue
|
||||
[ -f "$TMP_DIR/blocked_${ip}" ] && continue
|
||||
[ -f "$TMP_DIR/colo_excluded_${ip}" ] && continue
|
||||
|
||||
local raw_header="$TMP_DIR/header_${ip}_${seq}.txt"
|
||||
local metrics
|
||||
metrics=$(curl -s -k -I -o /dev/null --connect-timeout 2 -m 3 \
|
||||
-A "Mozilla/5.0 (compatible; Cloudflare-Optimizer/1.0)" \
|
||||
--resolve "$probe_domain:443:$ip" "https://$probe_domain/" \
|
||||
-D "$raw_header" \
|
||||
-w "%{time_connect} %{time_appconnect} %{http_code}\n" 2>/dev/null)
|
||||
|
||||
local tcp_sec=$(echo "$metrics" | awk '{print $1}')
|
||||
local tls_sec=$(echo "$metrics" | awk '{print $2}')
|
||||
local http_code=$(echo "$metrics" | awk '{print $3}')
|
||||
|
||||
if [ "$http_code" = "403" ]; then
|
||||
touch "$TMP_DIR/blocked_${ip}"
|
||||
rm -f "$raw_header"
|
||||
continue
|
||||
fi
|
||||
|
||||
local last_colo=""
|
||||
if [ -f "$raw_header" ]; then
|
||||
local cf_ray=$(grep -i '^cf-ray:' "$raw_header" 2>/dev/null | tr -d '\r\n')
|
||||
if [ -n "$cf_ray" ]; then
|
||||
last_colo=$(echo "$cf_ray" | sed -E 's/.*-([A-Za-z0-9]+).*/\1/' | tr 'a-z' 'A-Z')
|
||||
fi
|
||||
rm -f "$raw_header"
|
||||
fi
|
||||
|
||||
if [ -n "$last_colo" ]; then
|
||||
update_colo_cache "$ip" "$last_colo"
|
||||
if ! check_colo_allowed "$last_colo"; then
|
||||
log "节点 $ip 机房 ($last_colo) 不符合区域过滤策略 ($colo_filter_mode),已终止采样并排除"
|
||||
touch "$TMP_DIR/colo_excluded_${ip}"
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$http_code" ] && [ "$http_code" != "000" ] && [ -n "$tcp_sec" ] && [ "$tcp_sec" != "0.000000" ]; then
|
||||
local tcp_ms=$(awk "BEGIN {printf \"%.2f\", $tcp_sec * 1000}")
|
||||
local tls_ms=$(awk "BEGIN {printf \"%.2f\", $tls_sec * 1000}")
|
||||
echo "1|$tcp_ms|$tls_ms|$last_colo" >> "$TMP_DIR/samples/$ip"
|
||||
else
|
||||
echo "0|0|0|$last_colo" >> "$TMP_DIR/samples/$ip"
|
||||
fi
|
||||
done < "$top_selected_file"
|
||||
done
|
||||
|
||||
# 汇总统计各节点采样指标
|
||||
while read -r ip; do
|
||||
[ -z "$ip" ] && continue
|
||||
if [ -f "$TMP_DIR/blocked_${ip}" ]; then
|
||||
log "节点 $ip 触发权限阻断 (HTTP 403/企业限制),已自动排除"
|
||||
continue
|
||||
fi
|
||||
if [ -f "$TMP_DIR/colo_excluded_${ip}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
local sample_file="$TMP_DIR/samples/$ip"
|
||||
[ ! -f "$sample_file" ] && continue
|
||||
|
||||
local success_count=0
|
||||
local total_tcp=0
|
||||
local total_tls=0
|
||||
local min_tcp=99999
|
||||
local max_tcp=0
|
||||
local last_colo=""
|
||||
|
||||
while IFS='|' read -r ok tcp_ms tls_ms colo; do
|
||||
[ -n "$colo" ] && last_colo="$colo"
|
||||
if [ "$ok" = "1" ]; then
|
||||
success_count=$((success_count + 1))
|
||||
total_tcp=$(awk "BEGIN {printf \"%.2f\", $total_tcp + $tcp_ms}")
|
||||
total_tls=$(awk "BEGIN {printf \"%.2f\", $total_tls + $tls_ms}")
|
||||
min_tcp=$(awk "BEGIN {if ($tcp_ms < $min_tcp) print $tcp_ms; else print $min_tcp}")
|
||||
max_tcp=$(awk "BEGIN {if ($tcp_ms > $max_tcp) print $tcp_ms; else print $max_tcp}")
|
||||
fi
|
||||
done < "$sample_file"
|
||||
|
||||
if [ -z "$last_colo" ]; then
|
||||
last_colo=$(get_cached_colo "$ip")
|
||||
fi
|
||||
|
||||
if ! check_colo_allowed "$last_colo"; then
|
||||
log "节点 $ip 机房 ($last_colo) 最终区域校验未通过,舍弃"
|
||||
continue
|
||||
fi
|
||||
|
||||
local loss_rate=$(awk "BEGIN {printf \"%.1f\", (1 - $success_count / $sample_count) * 100}")
|
||||
if [ "$success_count" -gt 0 ]; then
|
||||
local avg_tcp=$(awk "BEGIN {printf \"%.2f\", $total_tcp / $success_count}")
|
||||
local avg_tls=$(awk "BEGIN {printf \"%.2f\", $total_tls / $success_count}")
|
||||
local jitter=$(awk "BEGIN {printf \"%.2f\", $max_tcp - $min_tcp}")
|
||||
# 综合评分:平均时延 + 抖动*0.5 + 丢包率*10(采用固定宽度 0 填充)
|
||||
local score=$(awk "BEGIN {printf \"%08.2f\", $avg_tcp + $jitter * 0.5 + $loss_rate * 10}")
|
||||
|
||||
local colo_disp=$(get_colo_name "$last_colo")
|
||||
echo "$score|$ip|$avg_tcp|$min_tcp|$max_tcp|$avg_tls|$loss_rate%|$colo_disp|$last_colo" >> "$TMP_DIR/deep_results.txt"
|
||||
fi
|
||||
done < "$top_selected_file"
|
||||
|
||||
if [ ! -s "$TMP_DIR/deep_results.txt" ]; then
|
||||
log "精测采样后无可用健康节点"
|
||||
rm -rf "$TMP_DIR"
|
||||
rm -rf "$lock_dir"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 排序:按 score 从低到高数值排序
|
||||
sort -n "$TMP_DIR/deep_results.txt" > "$TMP_DIR/deep_sorted.txt"
|
||||
|
||||
local best_line=$(head -n 1 "$TMP_DIR/deep_sorted.txt")
|
||||
local best_score=$(echo "$best_line" | cut -d'|' -f1)
|
||||
local best_ip=$(echo "$best_line" | cut -d'|' -f2)
|
||||
local best_avg_tcp=$(echo "$best_line" | cut -d'|' -f3)
|
||||
local best_min_tcp=$(echo "$best_line" | cut -d'|' -f4)
|
||||
local best_max_tcp=$(echo "$best_line" | cut -d'|' -f5)
|
||||
local best_avg_tls=$(echo "$best_line" | cut -d'|' -f6)
|
||||
local best_loss=$(echo "$best_line" | cut -d'|' -f7)
|
||||
local best_colo=$(echo "$best_line" | cut -d'|' -f8)
|
||||
|
||||
log "测速完成,最优候选: $best_ip ($best_colo), 平均延迟: ${best_avg_tcp}ms, 丢包率: $best_loss"
|
||||
|
||||
# 迟滞逻辑判断
|
||||
local current_ip=""
|
||||
[ -f "$CURRENT_IP_FILE" ] && current_ip=$(cat "$CURRENT_IP_FILE")
|
||||
|
||||
local final_ip="$best_ip"
|
||||
local final_colo="$best_colo"
|
||||
local final_tcp="$best_avg_tcp"
|
||||
local final_tls="$best_avg_tls"
|
||||
|
||||
if [ -n "$current_ip" ] && [ "$current_ip" != "$best_ip" ]; then
|
||||
local cur_info=$(grep "|$current_ip|" "$TMP_DIR/deep_sorted.txt" 2>/dev/null)
|
||||
local cur_colo_allowed=1
|
||||
local cur_cached_colo=$(get_cached_colo "$current_ip")
|
||||
if [ -n "$cur_cached_colo" ] && ! check_colo_allowed "$cur_cached_colo"; then
|
||||
cur_colo_allowed=0
|
||||
fi
|
||||
|
||||
if [ -n "$cur_info" ] && [ "$cur_colo_allowed" -eq 1 ]; then
|
||||
local cur_tcp=$(echo "$cur_info" | cut -d'|' -f3)
|
||||
local cur_loss=$(echo "$cur_info" | cut -d'|' -f7)
|
||||
local cur_colo=$(echo "$cur_info" | cut -d'|' -f8)
|
||||
|
||||
if [ "$cur_loss" = "0.0%" ]; then
|
||||
# 测算相比当前在用 IP 的绝对时延差 (ms)
|
||||
local diff_ms=$(awk "BEGIN {if ($cur_tcp > $best_avg_tcp) printf \"%.2f\", $cur_tcp - $best_avg_tcp; else print 0}")
|
||||
local is_better=$(awk "BEGIN {print ($diff_ms >= $hysteresis_ms) ? 1 : 0}")
|
||||
|
||||
if [ "$is_better" -eq 0 ]; then
|
||||
log "新优选 IP 优化幅度 (${diff_ms}ms) 未达迟滞阈值 (${hysteresis_ms}ms),保持当前在用 IP: $current_ip"
|
||||
final_ip="$current_ip"
|
||||
final_colo="$cur_colo"
|
||||
final_tcp="$cur_tcp"
|
||||
final_tls=$(echo "$cur_info" | cut -d'|' -f6)
|
||||
else
|
||||
log "新优选 IP 性能显著提升 (时延改善 ${diff_ms}ms >= ${hysteresis_ms}ms),触发切换"
|
||||
fi
|
||||
else
|
||||
log "当前在用 IP 发生丢包 ($cur_loss),强制切换至新最优 IP: $best_ip"
|
||||
fi
|
||||
else
|
||||
if [ "$cur_colo_allowed" -eq 0 ]; then
|
||||
log "当前在用 IP ($current_ip / $cur_cached_colo) 已被机房过滤策略排除,强制切换至新最优 IP: $best_ip"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# 原子写入 /tmp/hosts/cf-optimizer
|
||||
local hosts_tmp="$HOSTS_FILE.tmp"
|
||||
mkdir -p "$(dirname "$HOSTS_FILE")"
|
||||
> "$hosts_tmp"
|
||||
echo "# Auto-generated by cf-optimizer on $(date '+%Y-%m-%d %H:%M:%S')" >> "$hosts_tmp"
|
||||
for d in $domains; do
|
||||
echo "$final_ip $d" >> "$hosts_tmp"
|
||||
done
|
||||
mv -f "$hosts_tmp" "$HOSTS_FILE"
|
||||
echo "$final_ip" > "$CURRENT_IP_FILE"
|
||||
echo "$final_ip" > "$PRIMARY_IP_FILE"
|
||||
echo "0" > "$FAIL_STREAK_FILE"
|
||||
|
||||
# 选拔次优备选节点(排名紧随其后且 IP 不同的健康节点)
|
||||
local standby_ip=""
|
||||
local standby_colo="无备选"
|
||||
local standby_tcp="-"
|
||||
while IFS='|' read -r _sc _ip _atip _minip _maxip _attls _lrate _cdisp _craw; do
|
||||
if [ -n "$_ip" ] && [ "$_ip" != "$final_ip" ]; then
|
||||
standby_ip="$_ip"
|
||||
standby_colo="$_cdisp"
|
||||
standby_tcp="$_atip"
|
||||
break
|
||||
fi
|
||||
done < "$TMP_DIR/deep_sorted.txt"
|
||||
echo "$standby_ip|$standby_colo|$standby_tcp" > "$STANDBY_IP_FILE"
|
||||
echo "$final_tcp" > "$BASELINE_TCP_FILE"
|
||||
|
||||
# 发送 HUP 信号通知 dnsmasq 毫秒级重载
|
||||
killall -HUP dnsmasq 2>/dev/null
|
||||
log "已更新 $HOSTS_FILE 并热重载 dnsmasq,在用主选: $final_ip ($final_colo),次优备选: ${standby_ip:-无} ($standby_colo)"
|
||||
|
||||
# 组装状态 JSON 给 LuCI 前端消费
|
||||
local now_str=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
local domain_json="["
|
||||
local first_d=1
|
||||
for d in $domains; do
|
||||
[ "$first_d" -eq 0 ] && domain_json="$domain_json,"
|
||||
domain_json="$domain_json{\"domain\":\"$d\",\"ip\":\"$final_ip\",\"colo\":\"$final_colo\",\"tcp_lat\":\"$final_tcp ms\",\"tls_lat\":\"$final_tls ms\",\"status\":\"正常\"}"
|
||||
first_d=0
|
||||
done
|
||||
domain_json="$domain_json]"
|
||||
|
||||
local rankings_json="["
|
||||
local first_r=1
|
||||
local rank_idx=1
|
||||
while IFS='|' read -r sc ip atip minip maxip attls lrate cdisp craw; do
|
||||
[ -z "$ip" ] && continue
|
||||
[ "$first_r" -eq 0 ] && rankings_json="$rankings_json,"
|
||||
rankings_json="$rankings_json{\"rank\":$rank_idx,\"ip\":\"$ip\",\"colo\":\"$cdisp\",\"avg_lat\":\"$atip ms\",\"lat_range\":\"$minip / $maxip ms\",\"loss_rate\":\"$lrate\"}"
|
||||
first_r=0
|
||||
rank_idx=$((rank_idx + 1))
|
||||
[ "$rank_idx" -gt 5 ] && break
|
||||
done < "$TMP_DIR/deep_sorted.txt"
|
||||
rankings_json="$rankings_json]"
|
||||
|
||||
cat <<EOF > "$STATUS_JSON.tmp"
|
||||
{
|
||||
"running": true,
|
||||
"last_update": "$now_str",
|
||||
"best_ip": "$final_ip",
|
||||
"best_colo": "$final_colo",
|
||||
"best_tcp_lat": "$final_tcp ms",
|
||||
"best_tls_lat": "$final_tls ms",
|
||||
"standby_ip": "$standby_ip",
|
||||
"standby_colo": "$standby_colo",
|
||||
"standby_tcp_lat": "$standby_tcp ms",
|
||||
"watchdog_status": "正常监控中",
|
||||
"domains": $domain_json,
|
||||
"rankings": $rankings_json
|
||||
}
|
||||
EOF
|
||||
mv -f "$STATUS_JSON.tmp" "$STATUS_JSON"
|
||||
rm -rf "$TMP_DIR"
|
||||
rm -rf "$lock_dir"
|
||||
return 0
|
||||
}
|
||||
|
||||
apply_fast_failover() {
|
||||
local target_ip="$1"
|
||||
local target_colo="$2"
|
||||
local target_tcp="${3:--}"
|
||||
|
||||
. /lib/functions.sh
|
||||
config_load "$CONF"
|
||||
local domains=""
|
||||
append_failover_domain() {
|
||||
local d="$1"
|
||||
[ -n "$d" ] && domains="$domains $d"
|
||||
}
|
||||
config_list_foreach settings domains append_failover_domain
|
||||
|
||||
local hosts_tmp="$HOSTS_FILE.tmp"
|
||||
mkdir -p "$(dirname "$HOSTS_FILE")"
|
||||
> "$hosts_tmp"
|
||||
echo "# Auto-generated by cf-optimizer failover on $(date '+%Y-%m-%d %H:%M:%S')" >> "$hosts_tmp"
|
||||
for d in $domains; do
|
||||
echo "$target_ip $d" >> "$hosts_tmp"
|
||||
done
|
||||
mv -f "$hosts_tmp" "$HOSTS_FILE"
|
||||
echo "$target_ip" > "$CURRENT_IP_FILE"
|
||||
echo "" > "$STANDBY_IP_FILE"
|
||||
if [ -n "$target_tcp" ] && [ "$target_tcp" != "-" ]; then
|
||||
echo "$target_tcp" > "$BASELINE_TCP_FILE"
|
||||
else
|
||||
rm -f "$BASELINE_TCP_FILE"
|
||||
fi
|
||||
|
||||
killall -HUP dnsmasq 2>/dev/null
|
||||
log "已将 hosts 紧急倒换至次优节点: $target_ip ($target_colo) 并重载 dnsmasq"
|
||||
|
||||
if [ -f "$STATUS_JSON" ]; then
|
||||
local now_str=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
local domain_json="["
|
||||
local first_d=1
|
||||
for d in $domains; do
|
||||
[ "$first_d" -eq 0 ] && domain_json="$domain_json,"
|
||||
domain_json="$domain_json{\"domain\":\"$d\",\"ip\":\"$target_ip\",\"colo\":\"$target_colo\",\"tcp_lat\":\"$target_tcp ms\",\"tls_lat\":\"-\",\"status\":\"已倒换\"}"
|
||||
first_d=0
|
||||
done
|
||||
domain_json="$domain_json]"
|
||||
|
||||
local tmp_status="$STATUS_JSON.tmp"
|
||||
sed -E "s/\"best_ip\":[[:space:]]*\"[^\"]*\"/\"best_ip\": \"$target_ip\"/" "$STATUS_JSON" | \
|
||||
sed -E "s/\"best_colo\":[[:space:]]*\"[^\"]*\"/\"best_colo\": \"$target_colo\"/" | \
|
||||
sed -E "s/\"standby_ip\":[[:space:]]*\"[^\"]*\"/\"standby_ip\": \"\"/" | \
|
||||
sed -E "s/\"standby_colo\":[[:space:]]*\"[^\"]*\"/\"standby_colo\": \"已倒换转正\"/" | \
|
||||
sed -E "s/\"last_update\":[[:space:]]*\"[^\"]*\"/\"last_update\": \"$now_str (已触发倒换)\"/" | \
|
||||
sed -E "s/\"watchdog_status\":[[:space:]]*\"[^\"]*\"/\"watchdog_status\": \"已触发故障倒换\"/" | \
|
||||
sed -E "s/\"domains\":[[:space:]]*\[[^]]*\]/\"domains\": $domain_json/" > "$tmp_status" 2>/dev/null
|
||||
[ -s "$tmp_status" ] && mv -f "$tmp_status" "$STATUS_JSON"
|
||||
fi
|
||||
}
|
||||
|
||||
check_health_and_failover() {
|
||||
[ ! -f "$CURRENT_IP_FILE" ] && return 0
|
||||
local current_ip=$(cat "$CURRENT_IP_FILE" 2>/dev/null)
|
||||
[ -z "$current_ip" ] && return 0
|
||||
|
||||
. /lib/functions.sh
|
||||
config_load "$CONF"
|
||||
|
||||
local failover_enabled fail_threshold latency_multiplier
|
||||
config_get_bool failover_enabled settings failover_enabled 1
|
||||
[ "$failover_enabled" != "1" ] && return 0
|
||||
config_get fail_threshold settings fail_threshold 2
|
||||
config_get latency_multiplier settings latency_multiplier "2.0"
|
||||
|
||||
local domains=""
|
||||
append_check_domain() {
|
||||
local d="$1"
|
||||
[ -n "$d" ] && domains="$domains $d"
|
||||
}
|
||||
config_list_foreach settings domains append_check_domain
|
||||
local probe_domain=$(echo "$domains" | awk '{print $1}')
|
||||
[ -z "$probe_domain" ] && probe_domain="speed.cloudflare.com"
|
||||
|
||||
local metrics
|
||||
metrics=$(curl -s -k -I -o /dev/null --connect-timeout 2 -m 3 \
|
||||
-A "Mozilla/5.0 (compatible; Cloudflare-Optimizer/1.0)" \
|
||||
--resolve "$probe_domain:443:$current_ip" "https://$probe_domain/" \
|
||||
-w "%{time_connect} %{http_code}\n" 2>/dev/null)
|
||||
|
||||
local tcp_sec=$(echo "$metrics" | awk '{print $1}')
|
||||
local http_code=$(echo "$metrics" | awk '{print $2}')
|
||||
local cur_tcp_ms=0
|
||||
if [ -n "$tcp_sec" ] && [ "$tcp_sec" != "0.000000" ]; then
|
||||
cur_tcp_ms=$(awk "BEGIN {printf \"%.2f\", $tcp_sec * 1000}")
|
||||
fi
|
||||
|
||||
local baseline_tcp=0
|
||||
[ -f "$BASELINE_TCP_FILE" ] && baseline_tcp=$(cat "$BASELINE_TCP_FILE" 2>/dev/null)
|
||||
baseline_tcp=${baseline_tcp:-0}
|
||||
|
||||
local streak=0
|
||||
[ -f "$FAIL_STREAK_FILE" ] && streak=$(cat "$FAIL_STREAK_FILE" 2>/dev/null)
|
||||
streak=${streak:-0}
|
||||
|
||||
local is_abnormal=0
|
||||
local abnormal_reason=""
|
||||
|
||||
if [ -z "$http_code" ] || [ "$http_code" = "000" ] || [ "$http_code" = "403" ]; then
|
||||
is_abnormal=1
|
||||
abnormal_reason="HTTP ${http_code:-000} 阻断或超时"
|
||||
elif [ "$latency_multiplier" != "0" ] && [ "$latency_multiplier" != "0.0" ]; then
|
||||
local is_degraded
|
||||
is_degraded=$(awk "BEGIN {
|
||||
if ($baseline_tcp > 0 && $cur_tcp_ms >= ($baseline_tcp * $latency_multiplier) && ($cur_tcp_ms - $baseline_tcp) >= 50)
|
||||
print 1;
|
||||
else
|
||||
print 0;
|
||||
}")
|
||||
if [ "$is_degraded" -eq 1 ]; then
|
||||
is_abnormal=1
|
||||
abnormal_reason="延迟严重劣化 (${cur_tcp_ms}ms >= 基线 ${baseline_tcp}ms x ${latency_multiplier})"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$is_abnormal" -eq 0 ]; then
|
||||
if [ "$streak" -gt 0 ]; then
|
||||
log "主用优选节点 $current_ip 探活恢复正常 (HTTP $http_code, TCP ${cur_tcp_ms}ms)"
|
||||
echo "0" > "$FAIL_STREAK_FILE"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
streak=$((streak + 1))
|
||||
echo "$streak" > "$FAIL_STREAK_FILE"
|
||||
log "在用优选节点 $current_ip 探活异常 [$abnormal_reason],连续异常: $streak/$fail_threshold"
|
||||
|
||||
if [ "$streak" -ge "$fail_threshold" ]; then
|
||||
echo "0" > "$FAIL_STREAK_FILE"
|
||||
local standby_info=""
|
||||
[ -f "$STANDBY_IP_FILE" ] && standby_info=$(cat "$STANDBY_IP_FILE" 2>/dev/null)
|
||||
local standby_ip=$(echo "$standby_info" | cut -d'|' -f1)
|
||||
local standby_colo=$(echo "$standby_info" | cut -d'|' -f2)
|
||||
|
||||
if [ -n "$standby_ip" ] && [ "$standby_ip" != "$current_ip" ]; then
|
||||
log "正在核验次优备选节点健康度: $standby_ip ($standby_colo)"
|
||||
local s_metrics
|
||||
s_metrics=$(curl -s -k -I -o /dev/null --connect-timeout 2 -m 3 \
|
||||
-A "Mozilla/5.0 (compatible; Cloudflare-Optimizer/1.0)" \
|
||||
--resolve "$probe_domain:443:$standby_ip" "https://$probe_domain/" \
|
||||
-w "%{time_connect} %{http_code}\n" 2>/dev/null)
|
||||
local s_tcp_sec=$(echo "$s_metrics" | awk '{print $1}')
|
||||
local s_code=$(echo "$s_metrics" | awk '{print $2}')
|
||||
local s_tcp_ms="-"
|
||||
[ -n "$s_tcp_sec" ] && [ "$s_tcp_sec" != "0.000000" ] && s_tcp_ms=$(awk "BEGIN {printf \"%.2f\", $s_tcp_sec * 1000}")
|
||||
|
||||
if [ -n "$s_code" ] && [ "$s_code" != "000" ] && [ "$s_code" != "403" ]; then
|
||||
log "主用节点异常,次优备选节点验证健康 (HTTP $s_code, TCP ${s_tcp_ms}ms),秒级倒换至次优节点"
|
||||
apply_fast_failover "$standby_ip" "$standby_colo" "$s_tcp_ms"
|
||||
return 0
|
||||
else
|
||||
log "次优备选节点同样不可用 (HTTP ${s_code:-000}),提前拉起全网节点紧急重测"
|
||||
fi
|
||||
else
|
||||
log "未配置次优备选节点,提前拉起全网节点紧急重测"
|
||||
fi
|
||||
|
||||
run_optimization
|
||||
fi
|
||||
}
|
||||
|
||||
daemon_loop() {
|
||||
echo $$ > "$PID_FILE"
|
||||
trap 'rm -f "$PID_FILE"; exit 0' INT TERM EXIT
|
||||
|
||||
log "守护进程启动 [PID: $$]"
|
||||
|
||||
local last_full_run=0
|
||||
|
||||
while true; do
|
||||
. /lib/functions.sh
|
||||
config_load "$CONF"
|
||||
|
||||
local enabled
|
||||
config_get_bool enabled settings enabled 0
|
||||
if [ "$enabled" != "1" ]; then
|
||||
log "配置已禁用,清理并退出守护进程"
|
||||
clean_hosts
|
||||
break
|
||||
fi
|
||||
|
||||
local interval failover_enabled health_interval
|
||||
config_get interval settings interval 120
|
||||
[ "$interval" -lt 10 ] && interval=10
|
||||
local full_interval_sec=$((interval * 60))
|
||||
|
||||
config_get_bool failover_enabled settings failover_enabled 1
|
||||
config_get health_interval settings health_interval 60
|
||||
[ "$health_interval" -lt 15 ] && health_interval=15
|
||||
|
||||
local now=$(date +%s)
|
||||
if [ "$last_full_run" -eq 0 ] || [ $((now - last_full_run)) -ge "$full_interval_sec" ]; then
|
||||
run_optimization
|
||||
last_full_run=$(date +%s)
|
||||
elif [ "$failover_enabled" = "1" ]; then
|
||||
check_health_and_failover
|
||||
fi
|
||||
|
||||
local sleep_sec="$health_interval"
|
||||
if [ "$failover_enabled" != "1" ]; then
|
||||
sleep_sec=30
|
||||
fi
|
||||
|
||||
local elapsed=0
|
||||
while [ "$elapsed" -lt "$sleep_sec" ]; do
|
||||
sleep 5 &
|
||||
wait $!
|
||||
elapsed=$((elapsed + 5))
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
run)
|
||||
run_optimization
|
||||
;;
|
||||
sync-hosts)
|
||||
sync_hosts
|
||||
;;
|
||||
daemon)
|
||||
daemon_loop
|
||||
;;
|
||||
clean)
|
||||
clean_hosts
|
||||
;;
|
||||
status)
|
||||
if [ -f "$STATUS_JSON" ]; then
|
||||
cat "$STATUS_JSON"
|
||||
else
|
||||
echo '{"running":false,"domains":[],"rankings":[]}'
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "用法: $0 {run|sync-hosts|daemon|clean|status}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Reference in new issue
Block a user