乐于分享
好东西不私藏

Android 金融 App CDN 动态切换实战:基于 OkHttp 实现多线路容灾

Android 金融 App CDN 动态切换实战:基于 OkHttp 实现多线路容灾

摘要:分享一套金融 App 生产级 CDN 多线路容灾方案,基于 OkHttp 拦截器 + EventListener 实现,包含架构设计、代码实现、踩坑点与灰度上线流程。

图1 CDN 节点超时后的客户端自动切换示意

文章目录

    • 全局常量约定
    • 一、问题背景:CDN 挂掉时,客户端不能只能等恢复
    • 二、方案目标:业务层无感,网络层动态换 Host
    • 三、整体架构:Interceptor + LineService + EventListener
    • 四、业务线维度:不要把所有请求塞进同一个线路池
    • 五、DynamicHostInterceptor:运行时重写请求 Host
    • 六、为什么拦截器顺序很关键
    • 七、固定 Host 透传:不是所有请求都能动态切
    • 八、线路选择策略:远端配置、本地缓存、包内默认
    • 九、选路算法:从缓存到实时排序的完整链路
    • 十、测速触发:不要每个请求都测速
    • 十一、质量回收:用 EventListener 记录真实请求耗时
    • 十二、线路排序:未尝试、成功率、耗时
    • 十三、HTTP 和 WebSocket 要分开治理
    • 十四、容易踩坑的点
    • 十五、可观测性:没有日志就没有线路治理
    • 十六、上线策略:先观测,再切流
    • 十七、总结
    • 附录 A:核心常量与默认配置
      • A.1 网络私有 Header 定义
      • A.2 线路策略阈值常量
      • A.3 远程配置工具类

全局常量约定

动态线路能力属于网络层基础设施,为避免代码中出现大量魔法值,私有请求头、各类超时/冷却阈值均统一收口管理。

  • 客户端内部控制 Header 统一使用 X-Line- 前缀,请求发出前必须批量移除,禁止透传到服务端;
  • 所有策略阈值优先读取远程配置,远程配置不存在时使用本地默认兜底值,方便线上故障动态调参;
  • 完整常量代码、远程配置工具类详见 附录 A:核心常量与默认配置

一、问题背景:CDN 挂掉时,客户端不能只能等恢复

很多 App 的网络架构里,Retrofit 或 OkHttp 会配置一个固定 BaseUrl。正常情况下这很简单,但金融交易 App 的问题在于:用户分布广、访问频率高、核心链路实时性强,一旦某条 CDN 线路异常,影响会被迅速放大。

典型故障包括:

  • 某个 CDN 域名在部分地区解析失败。
  • 某个运营商链路质量突然劣化。
  • 单个 CDN 节点异常,导致请求耗时飙升。
  • DNS 缓存污染或解析到不可用节点。
  • 某条业务线访问正常,另一条业务线访问异常。

如果客户端只有一个固定域名,能做的事情很有限:要么等待 CDN 恢复,要么依赖服务端或运维层调整解析,要么发版切换域名。对于金融 App 来说,这些方案都不够快。

更稳妥的做法是:客户端内置多线路能力,在运行时根据线路质量动态选择最优 CDN Host,业务层只感知统一的 API 能力,不感知真实域名变化。

二、方案目标:业务层无感,网络层动态换 Host

这套方案的核心目标不是“把域名写成可配置”,而是把 CDN 线路选择做成客户端网络层的基础能力。

目标可以拆成几条:

  • 业务代码仍然使用稳定的逻辑 BaseUrl,不直接关心 CDN 域名。
  • 网络层在请求发出前,根据业务线选择当前最优 CDN Host。
  • CDN 故障或请求变慢时,客户端能触发测速并更新最优线路。
  • 特殊接口支持固定 Host 透传,避免误切换。
  • 远端配置失败时,能够降级到本地缓存或包内默认线路。
  • 全链路可观测,能知道请求最终走了哪条线路、耗时多少、为什么切换。

整体链路可以理解为:

业务请求  -> Retrofit 生成 Request  -> DynamicHostInterceptor 判断是否需要换 Host  -> LineService 根据业务线选择最优 CDN  -> OkHttp 发起真实请求  -> NetworkEventListener 回收耗时和错误原因  -> 更新线路质量缓存

三、整体架构:Interceptor + LineService + EventListener

一个可维护的动态 Host 架构,至少需要三部分协作。

1. DynamicHostInterceptor

负责在 OkHttp 请求发出前改写 URL Host。它不做测速,也不保存复杂状态,只根据当前请求的业务类型,向线路服务查询“当前最优 Host”,然后生成新的 URL。

2. LineService

负责线路池管理、远端配置、本地缓存、默认线路、最优线路选择、测速触发、线路质量更新。它是整个多线路能力的状态中心。

3. NetworkEventListener

负责监听 OkHttp 请求生命周期,记录 DNS、连接、TLS、请求发送、响应接收、请求结束等关键耗时,并将请求结果反馈给 LineService。

这三个组件的职责边界很重要:

  • Interceptor 只负责“请求前选 Host”。
  • EventListener 只负责“请求后回收质量数据”。
  • LineService 负责“线路如何选、如何存、如何降级”。

如果把这三类逻辑都塞到一个拦截器里,短期能跑,长期会变成网络层黑盒:切了哪条线路、为什么切、什么时候恢复、是否触发测速,都很难排查。

四、业务线维度:不要把所有请求塞进同一个线路池

金融 App 往往不只有一类 API。常见业务线包括:

  • 行情 / 现货类 API
  • 合约类 API
  • 账户 / 资产类 API
  • DEX 类 API
  • 静态资源 / 文件类 API
  • H5 / WebView 链路
  • WebSocket 链路

不同业务线的域名、CDN、负载均衡和可用性策略可能完全不同。比如行情接口访问慢,不代表资产接口也慢;HTTP 线路质量好,也不代表 WebSocket 握手一定快。

因此,线路池建议按维度拆分:

LinePool  ├─ spot_http  ├─ spot_ws  ├─ future_http  ├─ future_ws  ├─ dex_api  ├─ dex_resource  └─ h5_web

Host 选择时也必须带上业务线:

enum classBizLine{    SPOT,    FUTURE,    DEX,    FILE,    H5}

落到实现上,建议不要在 Interceptor 里直接维护多个 List<String>,而是把线路池封装成一个稳定的模型。拦截器只问“这个业务线当前最优 Host 是谁”,不关心配置来源和排序细节。

class LinePool(config: ApiLineConfig) {    private val pools by lazy {        mapOf(            BizLine.SPOT to Pool(config.spotHosts),            BizLine.FUTURE to Pool(config.futureHosts),            BizLine.DEX to Pool(config.dexHosts),            BizLine.FILE to Pool(config.fileHosts),            BizLine.H5 to Pool(config.h5Hosts)        )    }    fungetBest(bizLine: BizLine, protocol: ProtocolType): HostInfo {        return pools.getValue(bizLine).rank(protocol).first()    }    data class Pool(private val hosts: List<HostInfo>) {        funrank(protocol: ProtocolType): List<HostInfo> {            return rankLines(hosts.filter { it.protocol == protocol })        }    }}

业务线较多时,线路池可以按需懒加载,避免 App 启动阶段一次性解析所有线路配置。真正要注意的是懒加载后的线程安全和首次访问耗时,核心业务线可以在网络模块初始化后低优先级预热。

业务线拆清楚后,线路切换才不会“牵一发动全身”。某条 DEX 线路异常,不应该影响行情 API;某个 H5 域名访问慢,也不应该影响交易下单接口。

五、DynamicHostInterceptor:运行时重写请求 Host

拦截器的核心逻辑并不复杂:判断请求是否允许切换 Host,如果允许,则根据业务线查询最优线路,重建 URL 后继续请求。

示例代码如下:

class DynamicHostInterceptor(    private val bizLine: BizLine,    private val lineService: LineService) : Interceptor {    override funintercept(chain: Interceptor.Chain): Response {        val originRequest = chain.request()        if (originRequest.keepOriginalHost()) {            return chain.proceed(originRequest.cleanLineHeaders())        }        val fixedHost = originRequest.header(NetLineHeader.HEADER_FIXED_HOST)            ?.takeIf { it.isNotBlank() }        val targetHost = if (fixedHost != null) {            lineService.parseFixedHost(fixedHost)        } else {            lineService.getBestHost(                bizLine = bizLine,                protocol = ProtocolType.HTTP,                useCache = true            )        } ?: return chain.proceed(originRequest.cleanLineHeaders())        if (!targetHost.isValidForRequest(isDebug = BuildConfig.DEBUG)) {            return chain.proceed(originRequest.cleanLineHeaders())        }        val newUrl = originRequest.url.newBuilder()            .scheme(targetHost.scheme)            .host(targetHost.host)            .port(targetHost.port)            .build()        val newRequest = originRequest.newBuilder()            .url(newUrl)            .build()            .cleanLineHeaders()        return chain.proceed(newRequest)    }}/** 判断当前请求是否禁止动态切换 Host。 */fun Request.keepOriginalHost()Boolean {    return header(NetLineHeader.HEADER_KEEP_ORIGINAL_HOST)        ?.equals("true", ignoreCase = true) == true}/** 清理网络层私有 Header,禁止透传到服务端。 */fun Request.cleanLineHeaders(): Request {    val builder = newBuilder()    headers.names().forEach { name ->        if (NetLineHeader.isPrivateHeader(name)) {            builder.removeHeader(name)        }    }    return builder.build()}fun HostInfo.isValidForRequest(isDebug: Boolean)Boolean {    if (scheme != "http" && scheme != "https"return false    if (host.isBlank()) return false    if (port !in 1..65535return false    // 金融 App 生产环境建议强制 HTTPS,HTTP 只允许测试环境使用。    if (!isDebug && scheme != "https"return false    return true}

这里有几个实现细节:

  • keepOriginalHost()
     用于固定 Host 透传,注意要做空安全判断。
  • NetLineHeader.HEADER_FIXED_HOST
     用于特殊接口指定 Host。
  • lineService.getBestHost()
     只返回当前业务线的最优线路。
  • 重建 URL 前必须校验 targetHost 合法性:scheme 只允许 http / https,生产环境建议强制 https;校验失败时不要继续改写 Host。
  • 重建 URL 时只改 scheme / host / port,完整保留 path / query / method / body,这是动态 Host 方案的核心约束。
  • 控制类 Header 要在真正请求前批量移除,避免透传到服务端;后续新增 X-Line- 私有 Header 时,也不需要逐个补 removeHeader()

最后一条尤其重要。动态 Host 切换只能改变网络入口,不能改变业务请求语义。如果重建 URL 时误改了 path、query、请求方法或请求体,本质上就不是“线路切换”,而是篡改了业务请求。

六、为什么拦截器顺序很关键

动态 Host 拦截器最好放在 OkHttp 应用拦截器的靠前位置。原因是后续很多能力可能依赖最终 URL:

  • Header 组装
  • 签名计算
  • 日志上报
  • 监控埋点
  • 缓存 Key
  • Sentry / APM 网络事件

推荐顺序:

DynamicHostInterceptor  -> HeaderInterceptor  -> CacheInterceptor  -> MonitorInterceptor  -> LoggingInterceptor

如果先做签名,再切 Host,而签名规则又包含 host,就可能导致服务端验签失败。如果先记录监控,再切 Host,监控里看到的是逻辑域名,不是真正访问的 CDN 域名,排障时会误判。

因此需要明确一个原则:凡是依赖最终 URL 的逻辑,都应该放在 DynamicHostInterceptor 之后。

七、固定 Host 透传:不是所有请求都能动态切

动态 Host 不能对所有请求一刀切。部分接口必须保持原始 Host 或由业务指定固定 Host。

常见场景包括:

  • 线路配置接口本身。
  • 线路测速接口。
  • 登录、支付、风控、KYC 等特殊安全接口。
  • 三方 SDK 上传或回调接口。
  • 文件上传、日志上传等要求固定域名的接口。
  • 签名强绑定 Host 的接口。

可以通过注解或 Header 标记:

interface ConfigApi {    @Headers(NetLineHeader.HEADER_KEEP_ORIGINAL_HOST_TRUE)    @GET("/config/line")    suspend funlineConfig(): ApiResponse<LineConfig>    @GET("/config/app")    suspend funappConfig(        @Header(NetLineHeader.HEADER_FIXED_HOST) host: String? = null    ): ApiResponse<AppConfig>}

实现时要注意:这类 Header 是客户端内部控制字段,最终请求前应移除,避免服务端收到无意义字段。

八、线路选择策略:远端配置、本地缓存、包内默认

线路选择不要只依赖远端配置。因为远端配置本身也可能请求失败。

推荐三级降级:

远端线路配置  -> 本地缓存线路  -> 包内默认线路

首次安装时,客户端至少要有一份包内默认线路。启动后拉取远端线路配置并更新 MMKV 缓存。后续启动时优先使用 MMKV 缓存中的历史最优线路,不要每次都从第一条开始。

本地线路状态建议使用 MMKV 持久化,包括历史最优线路、最近耗时、成功次数、失败次数、最近错误类型等。线路状态属于高频读、低到中频写的数据,启动、请求前选路、请求后质量回收都会访问;不建议使用 SharedPreferences 承载较大的线路池或频繁更新的质量数据,性能和一致性都不如专门的轻量 KV 存储稳定。

LineService 可以提供类似接口:

interface LineService {    fungetBestHost(        bizLine: BizLine,        protocol: ProtocolType,        useCache: Boolean = true    ): HostInfo    funparseFixedHost(fixedHost: String): HostInfo?    funupdateRemoteConfig(config: LineConfig)    funupdateHostQuality(        bizLine: BizLine,        host: String,        costTime: Long,        dnsCostTime: Long,        tlsCostTime: Long,        success: Boolean,        error: Throwable?    )    fungetDetectUrl(bizLine: BizLine): HttpUrl    funupdateDetectResult(        bizLine: BizLine,        host: String,        costTime: Long,        success: Boolean    )}

其中 HostInfo 不建议只保存字符串,最好包含完整结构:

data class HostInfo(    // 静态配置字段:来自远端配置、MMKV 缓存或 assets 默认配置    val scheme: String,              // http / https,生产环境建议只允许 https    val host: String,                // CDN Host,不包含 path 和 query    val port: Int,                   // 端口,通常为 443    val protocol: ProtocolType,      // HTTP / WebSocket 等协议维度    val priority: Int,               // 配置优先级,表达默认线路顺序    val weight: Int,                 // 灰度或容量权重,避免所有流量压到单线路    // 运行质量字段:来自 EventListener、测速任务和本地质量回收    val score: Double,               // 聚合评分,由成功率、耗时、错误类型计算    val used: Boolean,               // 是否已经被真实请求或测速验证过    val successCount: Int,           // 近期成功次数,可按滑动窗口统计    val failCount: Int,              // 近期失败次数,可按错误类型加权    val avgCostTime: Long?,          // 平均耗时,建议使用近期窗口而不是全量均值    val lastCostTime: Long?,         // 最近一次请求或测速耗时    val lastSuccess: Boolean?,       // 最近一次是否成功    val lastError: String?           // 最近一次错误类型,日志中不要带完整敏感 URL)

这里的 priority 通常来自配置,用来表达产品或运维侧的默认顺序;weight 适合表达线路容量或灰度比例;score 则是客户端根据成功率、耗时、错误类型计算出来的动态评分。三者不要混成一个字段,否则线上排障时很难判断“这条线路是配置优先,还是质量优先”。

包内默认线路在生产环境中也不建议明文存储。原因很简单:CDN 域名如果直接写在 APK 里,攻击者反编译后可以拿到完整入口列表,再配合批量探测或攻击绕过部分防护策略。更稳妥的做法是将包内默认线路做 AES 加密或同等级别的静态保护,并且只在网络模块初始化时执行一次解密。解密后的完整 Host 列表禁止打印到日志、控制台或异常堆栈里,线上日志最多记录线路编号、业务线、是否命中默认线路等脱敏信息。

九、选路算法:从缓存到实时排序的完整链路

前面定义了 lineService.getBestHost(),但真正决定方案质量的是它的内部选路链路。一个可落地的实现至少要处理五件事:策略降级、缓存命中、实时排序、全超时兜底、缓存更新。

private val lineSwitchGuard = LineSwitchGuard()fungetBestHost(    bizLine: BizLine,    protocol: ProtocolType,    useCache: Boolean): HostInfo {    // 1. 策略降级开关:线上异常时直接回到默认顺序    if (isStrategyDowngrade()) {        return getFirstLine(bizLine, protocol)    }    // 2. 缓存命中:避免每次请求都重新排序    if (useCache) {        val cached = getCachedBest(bizLine, protocol)        if (cached != null && !cached.isTimeout()) {            return cached        }    }    // 3. 实时排序:从当前可用线路池中选择最优线路    val lines = getLinesWithFallback(bizLine, protocol)    val best = rankLines(lines).first()    // 4. 切换熔断:避免短时间内在多个 Host 之间反复横跳    val guardedBest = if (lineSwitchGuard.canSwitch(bizLine)) {        best    } else {        getCachedBest(bizLine, protocol) ?: best    }    // 5. 全超时兜底:当前线路池质量很差时回到包内默认线路    val target = if (guardedBest.isTimeout()) {        getDefaultLine(bizLine, protocol)    } else {        guardedBest    }    // 6. 更新 MMKV 缓存:下一次请求可以直接命中历史最优线路    updateCache(bizLine, protocol, target)    return target}private fun HostInfo.isTimeout()Boolean {    return (lastCostTime ?: Long.MAX_VALUE) >= LineConstants.getTimeoutThresholdMs()}class LineSwitchGuard {    private val lastSwitchTime = ConcurrentHashMap<String, Long>()    funcanSwitch(bizLine: BizLine)Boolean {        val key = bizLine.name        val now = SystemClock.elapsedRealtime()        val lastTime = lastSwitchTime[key] ?: 0L        if (now - lastTime < LineConstants.getSwitchIntervalMs()) {            return false        }        lastSwitchTime[key] = now        return true    }}

这里有一个容易忽略的点:MMKV 缓存不是为了偷懒,而是为了稳定。如果每个请求都重新计算最优线路,可能会因为一次偶发抖动频繁切 Host,反而破坏连接复用和问题定位。更合理的做法是:正常情况下使用历史最优线路,只有请求变慢、连续失败、App 启动或用户进入核心页面时,再触发线路质量更新。

排序算法可以直接对应前面的策略:

private funrankLines(lines: List<HostInfo>): List<HostInfo> {    return lines.sortedByDescending { host ->        val avgCost = host.avgCostTime ?: LineConstants.getTimeoutThresholdMs()        val errorPenalty = host.errorPenalty()        when {            // 1. 从未用过的线路:最高优先级,给新线路验证机会            !host.used -> LineConstants.RANK_UNUSED_LINE_SCORE + (100 - host.priority)            // 2. 历史成功率高:成功率优先,再看耗时            host.successRate() >= LineConstants.HIGH_SUCCESS_RATE -> {                LineConstants.RANK_HIGH_SUCCESS_BASE_SCORE +                    host.weight +                    (LineConstants.getTimeoutThresholdMs() - avgCost).coerceAtLeast(0) -                    errorPenalty            }            // 3. 有成功记录但不够稳定:降权,但不直接禁用            host.successRate() >= LineConstants.MIN_USABLE_SUCCESS_RATE -> {                LineConstants.RANK_USABLE_BASE_SCORE +                    (host.successRate() * LineConstants.RANK_SUCCESS_RATE_MULTIPLIER).toInt() -                    errorPenalty            }            // 4. 失败率高:排到最后,等待后续探测恢复            else -> {                (host.successRate() * LineConstants.RANK_SUCCESS_RATE_MULTIPLIER).toInt() -                    errorPenalty            }        }    }}private fun HostInfo.successRate()Float {    val total = successCount + failCount    if (total == 0return 0f    return successCount.toFloat() / total}private fun HostInfo.errorPenalty()Int {    return LineErrorType.from(lastError).weight * 100}enum class LineErrorType(val weight: Int) {    DNS_FAILURE(10),    TLS_HANDSHAKE_FAILED(8),    CONNECT_TIMEOUT(5),    HTTP_5XX(3),    HTTP_4XX(1),    LOCAL_NETWORK_ERROR(0),    UNKNOWN(2);    companion object {        funfrom(error: String?): LineErrorType {            return values().firstOrNull { it.name == error } ?: UNKNOWN        }    }}

这段排序有三个取向:

  • 未尝试线路优先级高于已尝试线路
    ,防止历史最优线路一直垄断流量,新线路永远没有验证机会。
  • 成功率优先于耗时
    ,一条稳定的 300ms 线路,通常比一条偶尔 80ms、经常超时的线路更适合作为默认入口。
  • 失败线路不会永久禁用
    ,只是被降权。后续测速或真实请求恢复后,它仍然可以重新回到前面。
  • 错误类型要区别对待
    ,DNS 失败、TLS 失败、连接超时更像线路问题;用户断网、HTTP 4xx 不应该过度惩罚 CDN 线路。

线路来源也要按层级兜底,而不是只取远端配置:

远端配置线路 -> rankLines() 排序 -> 取最优  ↓ 远端为空或全部超时本地 MMKV 缓存线路 -> rankLines() 排序 -> 取最优  ↓ 缓存为空或不可用assets 内置加密线路 -> 解密 -> rankLines() 排序 -> 取最优  ↓ assets 也异常代码内硬编码默认线路

对应到代码,可以把线路来源兜底封装在一个方法里,保证上层选路永远能拿到至少一条可用候选:

private fungetLinesWithFallback(    bizLine: BizLine,    protocol: ProtocolType): List<HostInfo> {    val remoteLines = remoteConfigStore.getLines(bizLine, protocol)    if (remoteLines.isNotEmpty()) return remoteLines    val cacheLines = localLineStore.getLines(bizLine, protocol)    if (cacheLines.isNotEmpty()) return cacheLines    val assetsLines = assetsLineProvider.decryptLines(bizLine, protocol)    if (assetsLines.isNotEmpty()) return assetsLines    return listOf(getHardcodedLine(bizLine, protocol))}

最后一层“代码内硬编码默认线路”只应该作为极端兜底存在,不要放完整线路池,也不要承载灰度策略。它的价值是保证 App 在配置链路、缓存、assets 都异常时,仍然有一个最低限度的网络入口。

十、测速触发:不要每个请求都测速

线路测速是必要的,但不能滥用。测速本身也是网络请求,如果触发太频繁,会增加服务端压力,甚至让故障期间的情况更糟。

推荐触发时机:

  • App 启动后,低优先级触发一次。
  • 用户主动刷新核心页面时触发。
  • 请求耗时超过阈值时触发,例如超过 LineConstants.SLOW_REQUEST_THRESHOLD_MS
  • 某条线路连续失败达到阈值时触发。
  • WebSocket 握手失败或连接耗时过长时触发。

关键约束:

  • 同一业务线同一时间只允许一个测速任务。
  • 同一业务线要有冷却时间,例如 30s,避免故障期间反复测速引发雪崩。
  • 测速要有超时时间,例如 3s。
  • 测速结果写入 MMKV 缓存。
  • 失败原因也要保存,不能只保存耗时。
  • 测速接口必须强制使用固定兜底 Host,不参与动态线路切换,避免“测速链路自己也被错误线路带偏”。
  • 测速请求要轻量,优先使用专用 /line/ping 一类接口或 HEAD 请求,不要占用交易、资产等业务接口资源。

简单的 single-flight + 冷却控制:

class LineDetector(    private val okHttpClient: OkHttpClient,    private val lineService: LineService) {    // 测速冷却时间:30 秒,避免故障场景下频繁测速引发雪崩。    private val coolDownMs = LineConstants.DETECT_COOL_DOWN_MS    private val states = ConcurrentHashMap<BizLine, DetectState>()    suspend fundetectIfNeeded(bizLine: BizLine) {        val state = states.getOrPut(bizLine) { DetectState() }        val currentTime = SystemClock.elapsedRealtime()        if (state.running.get()) return        if (currentTime - state.lastDetectTime < coolDownMs) return        if (!state.running.compareAndSet(falsetrue)) return        // 冷却时间从测速开始计算,失败或超时也要进入冷却,避免故障期间反复打测速接口。        state.lastDetectTime = currentTime        try {            // 测速接口需要标记固定兜底 Host,不走 DynamicHostInterceptor。            withTimeout(LineConstants.TIMEOUT_THRESHOLD_MS) {                doDetect(bizLine)            }        } finally {            state.running.set(false)        }    }    private suspend fundoDetect(bizLine: BizLine) {        val detectUrl = lineService.getDetectUrl(bizLine)        val request = Request.Builder()            .url(detectUrl)            .head()            .header(NetLineHeader.HEADER_KEEP_ORIGINAL_HOST, "true")            .build()        val startTime = SystemClock.elapsedRealtime()        withContext(Dispatchers.IO) {            okHttpClient.newCall(request).execute().use { response ->                lineService.updateDetectResult(                    bizLine = bizLine,                    host = detectUrl.host,                    costTime = SystemClock.elapsedRealtime() - startTime,                    success = response.isSuccessful                )            }        }    }    private class DetectState {        val running = AtomicBoolean(false)        @Volatile        var lastDetectTime = 0L    }}

十一、质量回收:用 EventListener 记录真实请求耗时

拦截器负责请求前改 Host,但它不适合统计完整网络生命周期。OkHttp 提供的 EventListener 更适合做质量回收。

可以记录:

  • callStart
  • dnsStart / dnsEnd
  • connectStart / connectEnd
  • secureConnectStart / secureConnectEnd
  • responseHeadersStart
  • callEnd / callFailed

这里有一个 OkHttp 官方实践上的关键点:不要把 EventListener 做成全局单例。OkHttp 中每个 Call 都应该对应一个独立的 EventListener 实例;如果多个并发请求共用同一个实例,startTimeurl、DNS 耗时、TLS 耗时等成员变量会互相覆盖,最终统计出来的数据就是错的。

正确方式是使用 EventListener.Factory 为每个请求创建独立实例:

class LineEventListenerFactory(    private val bizLine: BizLine,    private val lineService: LineService) : EventListener.Factory {    override funcreate(call: Call): EventListener {        return LineEventListener(bizLine, lineService)    }}val okHttpClient = OkHttpClient.Builder()    .eventListenerFactory(LineEventListenerFactory(bizLine, lineService))    .build()

请求结束后,将实际 Host、耗时、状态码、失败原因回写给 LineService:

class LineEventListener(    private val bizLine: BizLine,    private val lineService: LineService) : EventListener() {    private var startTime = 0L    private var dnsStartTime = 0L    private var tlsStartTime = 0L    private var dnsCostTime = 0L    private var tlsCostTime = 0L    private var url: HttpUrl? = null    override funcallStart(call: Call) {        // 使用 elapsedRealtime 统计间隔耗时,不受用户或系统校时影响。        startTime = SystemClock.elapsedRealtime()        url = call.request().url    }    override fundnsStart(call: Call, domainName: String) {        dnsStartTime = SystemClock.elapsedRealtime()    }    override fundnsEnd(call: Call, domainName: String, inetAddressList: List<InetAddress>) {        dnsCostTime = SystemClock.elapsedRealtime() - dnsStartTime    }    override funsecureConnectStart(call: Call) {        tlsStartTime = SystemClock.elapsedRealtime()    }    override funsecureConnectEnd(call: Call, handshake: Handshake?) {        tlsCostTime = SystemClock.elapsedRealtime() - tlsStartTime    }    override funcallEnd(call: Call) {        report(call, success = true, error = null)    }    override funcallFailed(call: Call, ioe: IOException) {        report(call, success = false, error = ioe)    }    private funreport(call: Call, success: Boolean, error: Throwable?) {        val cost = SystemClock.elapsedRealtime() - startTime        val host = call.request().url.host        lineService.updateHostQuality(            bizLine = bizLine,            host = host,            costTime = cost,            dnsCostTime = dnsCostTime,            tlsCostTime = tlsCostTime,            success = success,            error = error        )    }}

这一步很关键。没有质量回收,所谓“动态切换”只能依赖静态配置;有了真实请求反馈,客户端才能逐步形成自己的线路质量缓存。除总耗时外,DNS 耗时和 TLS 握手耗时也建议单独记录:DNS 耗时异常通常指向解析、运营商或域名污染问题;TLS 耗时或失败则更容易定位到证书、SNI、CDN 边缘节点配置问题。

十二、线路排序:未尝试、成功率、耗时

线路排序不要只看最近一次耗时。一个更稳定的排序策略可以是:

未尝试线路 > 近期成功线路 > 成功率高线路 > 平均耗时低线路 > 失败线路

为什么未尝试线路优先级要高?因为如果所有用户都一直使用历史最优线路,新的可用线路永远没有机会被验证;当历史线路故障时,客户端也缺少备用线路质量数据。

如果不希望 HostInfo 同时承载配置字段和运行状态,也可以把运行态拆成独立的 LineState。这里建议使用近期滑动窗口,而不是全量累计成功 / 失败次数,否则一条线路早期的历史故障可能长期污染排序结果。

data class LineState(    val host: String,    val used: Boolean,    val successCountWindow: Int,    val failCountWindow: Int,    val windowStartTime: Long,    val avgCostTime: Long?,    val lastCostTime: Long?,    val lastSuccess: Boolean?,    val lastError: String?,    val updatedAt: Long)fun LineState.successRateInWindow()Float {    val total = successCountWindow + failCountWindow    if (total == 0return 0f    return successCountWindow.toFloat() / total}

上面 rankLines() 只是基础版。实际项目里还应把错误类型纳入评分,例如 DNS 失败、TLS 失败、HTTP 5xx、连接超时的权重不应该一样。DNS 失败和 TLS 失败更接近线路不可用,应该比业务 5xx 降权更重;移动网络切换、用户断网这类本地网络问题,则不应该过度惩罚 CDN 线路。

排序时要避免过度敏感。单次失败不一定说明线路不可用,可能只是用户当时断网。可以结合滑动窗口、失败次数、错误类型来判断。窗口时长可以走远程配置,例如默认 5 分钟;窗口过期后重置近期成功 / 失败计数,让排序结果更接近当前网络质量。

十三、HTTP 和 WebSocket 要分开治理

HTTP 线路质量好,不代表 WebSocket 线路质量也好。

原因包括:

  • HTTP 请求是短连接或连接复用,WebSocket 是长连接。
  • WebSocket 更依赖握手耗时和持续稳定性。
  • 某些 CDN 对 WebSocket 支持策略不同。
  • HTTP 接口成功不代表推送链路稳定。

因此线路池至少要区分:

spot_httpspot_wsfuture_httpfuture_ws

WebSocket 质量回收也不同。它更关注:

  • 发起连接到连接成功的耗时。
  • 首次收到消息的耗时。
  • 连接持续时间。
  • 关闭码。
  • 错误原因。
  • 是否在后台、断网、切前台时断开。

HTTP 线路选择和 WebSocket 线路选择可以共用 LineService,但不要共用同一个质量字段。

十四、容易踩坑的点

1. TLS 证书和 SNI

HTTPS 请求最终使用哪个 host,TLS SNI 和证书校验就会围绕哪个 host 工作。动态切换 Host 时,目标 CDN 域名必须具备合法证书,否则会出现握手失败或证书校验失败。

不要随意在 URL host 和 HTTP Host Header 之间制造不一致,除非服务端和证书体系明确支持。一般情况下,优先修改 URL host,而不是手工伪造 Host Header。

备用线路上线前必须逐条做证书链校验,包括顶级域名、泛域名、SAN 域名是否覆盖当前 Host。一个直接的验证方式是:

openssl s_client -connect api-backup.example.com:443 -servername api-backup.example.com

如果备用 CDN 的证书没有配好,客户端切过去会直接 SSL 握手失败,这种故障比“不切换”更隐蔽,也更难从业务日志里定位。

更稳的做法是在线路配置更新后,对备用线路做低频证书预校验,并把校验结果写入线路状态。排序时可以优先过滤证书无效的 Host;如果全部线路都未校验通过,再回到原有兜底逻辑,避免因为预校验服务异常导致客户端无路可走。证书预校验要受远程开关和频率限制,不能在启动主线程或高峰期批量执行。

2. 签名逻辑和 Host 顺序

如果请求签名包含 host,必须确保签名发生在 Host 重写之后;或者签名协议明确使用固定逻辑域名,而不是实际 CDN 域名。否则切换 Host 后可能导致服务端验签失败。

3. 控制 Header 不要透传

KEEP_ORIGINAL_HOSTFIXED_HOSTBIZ_LINE 这类 Header 只给客户端网络层使用,请求发出前应移除,避免污染服务端日志或触发网关规则。

4. 连接池与并发限制

OkHttp 的连接池、maxRequestsPerHost 都和最终 host 有关。Host 动态切换后,并发分布会发生变化。高频接口需要关注连接复用、连接数和请求排队情况。

如果业务线差异很大,可以按业务线创建不同的 OkHttpClient 或连接池配置,例如交易、行情使用更高的连接配额,静态资源、日志上传使用独立客户端,避免非核心请求抢占核心链路连接资源。但不要为了“隔离”无节制创建大量客户端,否则连接池复用率会下降,内存和握手成本都会上升。

5. 线路配置接口不能依赖动态线路

获取线路配置的接口本身要能固定 Host 或使用默认线路。否则一旦动态线路失效,客户端可能连“更新线路配置”的能力都没有。

十五、可观测性:没有日志就没有线路治理

动态 Host 切换必须可观测,否则线上出了问题只能猜。

建议至少记录:

  • 业务线。
  • 原始 URL host。
  • 最终请求 host。
  • 是否命中固定 Host。
  • 是否命中 MMKV 缓存。
  • 是否发生降级。
  • 请求耗时。
  • DNS 耗时。
  • TLS 握手耗时。
  • HTTP 状态码。
  • 异常类型。
  • 当前选路原因。

示例结构:

data class LineTrace(    val bizLine: BizLine,    val originHost: String,    val targetHost: String,    val fixedHost: Boolean,    val fromCache: Boolean,    val downgrade: Boolean,    val costTime: Long,    val dnsCostTime: Long,    val tlsCostTime: Long,    val statusCode: Int?,    val error: String?)

这些日志要脱敏,也要受远程开关控制。正常情况下低采样上报,线上排障时临时提高采样。

如果需要和服务端日志串联,建议由拦截器生成并注入 Trace ID,而不是在 EventListener.callStart() 里修改请求。EventListener 只适合观察请求生命周期,不适合改变请求内容。

class TraceIdInterceptor(    private val bizLine: BizLine,    private val traceIdGenerator: TraceIdGenerator) : Interceptor {    override funintercept(chain: Interceptor.Chain): Response {        val traceId = traceIdGenerator.generate()        val request = chain.request().newBuilder()            .header("X-Trace-Id", traceId)            .build()        LineLogger.logLineEvent(            LineLog(                traceId = traceId,                bizLine = bizLine.name,                targetHost = request.url.host,                eventType = "REQUEST_START"            )        )        return chain.proceed(request)    }}

结构化日志可以用统一模型输出,便于日志平台按业务线、事件类型和错误类型检索:

data class LineLog(    val traceId: String,    val bizLine: String,    val targetHost: String,    val eventType: String,    val costTime: Long? = null,    val errorType: String? = null,    val timestamp: Long = System.currentTimeMillis())

十六、上线策略:先观测,再切流

动态 Host 是网络底层能力,上线要谨慎。

推荐节奏:

  1. 只记录不切换
    :先接入 EventListener,统计各线路耗时和失败率。
  2. 灰度切换低风险业务
    :例如配置、行情、资源类接口。
  3. 扩大到核心 API
    :确认签名、缓存、监控、证书没有问题后,再覆盖交易相关接口。
  4. 开启动态测速
    :将请求慢、连续失败等触发条件接入线路检测。
  5. 增加远程开关
    :出现异常时能一键关闭动态 Host,回到默认线路。

这里一定要保留策略降级开关。一旦发现 Host 切换策略有问题,可以关闭 DynamicHostInterceptor 和线路质量回收,直接使用默认域名,保证基础可用性。

十七、总结

CDN 线路动态切换的价值,不是让客户端“聪明地改域名”,而是让网络可用性从静态配置变成运行时能力。

一套成熟的方案至少包含:

  • OkHttp 拦截器运行时重写 Host。
  • 按业务线维护独立线路池。
  • 固定 Host 透传,保护特殊接口。
  • 远端配置、MMKV 缓存、包内默认三级降级。
  • EventListener 回收真实请求质量。
  • HTTP 与 WebSocket 分开治理。
  • 线路切换日志可观测、可降噪。
  • 灰度上线和策略降级开关。

对于金融交易 App 来说,CDN 故障不是小概率事件,而是必须提前设计的可用性场景。把这套能力沉淀在网络层后,业务模块不需要感知真实 CDN 变化,客户端也能在域名异常、区域链路劣化时更快恢复服务能力。

附录 A:核心常量与默认配置

A.1 网络私有 Header 定义

统一前缀管理,支持批量判断私有请求头:

object NetLineHeader {    private const val PRIVATE_HEADER_PREFIX = "X-Line-"    const val HEADER_KEEP_ORIGINAL_HOST = "X-Line-Keep-Original-Host"    const val HEADER_FIXED_HOST = "X-Line-Fixed-Host"    const val HEADER_KEEP_ORIGINAL_HOST_TRUE = "X-Line-Keep-Original-Host: true"    funisPrivateHeader(name: String)Boolean {        return name.startsWith(PRIVATE_HEADER_PREFIX, ignoreCase = true)    }}

A.2 线路策略阈值常量

所有阈值优先读取远程配置,默认值作为兜底:

object LineConstants {    private const val DEFAULT_TIMEOUT_THRESHOLD_MS = 3_000L    private const val DEFAULT_SLOW_REQUEST_THRESHOLD_MS = 2_000L    private const val DEFAULT_DETECT_COOL_DOWN_MS = 30 * 1000L    private const val DEFAULT_SWITCH_INTERVAL_MS = 10 * 1000L    private const val DEFAULT_WINDOW_DURATION_MS = 5 * 60 * 1000L    const val HIGH_SUCCESS_RATE = 0.9f    const val MIN_USABLE_SUCCESS_RATE = 0.5f    const val RANK_UNUSED_LINE_SCORE = 10_000    const val RANK_HIGH_SUCCESS_BASE_SCORE = 5_000    const val RANK_USABLE_BASE_SCORE = 2_000    const val RANK_SUCCESS_RATE_MULTIPLIER = 1_000    fungetTimeoutThresholdMs()Long {        return RemoteLineConfig.getLong("line_timeout_threshold", DEFAULT_TIMEOUT_THRESHOLD_MS)    }    fungetSlowRequestThresholdMs()Long {        return RemoteLineConfig.getLong("line_slow_request_threshold", DEFAULT_SLOW_REQUEST_THRESHOLD_MS)    }    fungetDetectCoolDownMs()Long {        return RemoteLineConfig.getLong("line_detect_cool_down", DEFAULT_DETECT_COOL_DOWN_MS)    }    fungetSwitchIntervalMs()Long {        return RemoteLineConfig.getLong("line_switch_interval", DEFAULT_SWITCH_INTERVAL_MS)    }    fungetWindowDurationMs()Long {        return RemoteLineConfig.getLong("line_window_duration", DEFAULT_WINDOW_DURATION_MS)    }}

A.3 远程配置工具类

封装远程配置 + 本地缓存读取逻辑:

object RemoteLineConfig {    fungetLong(key: String, defaultValue: Long)Long {        // 实际项目接入远程配置 + MMKV 缓存        return defaultValue    }}

本文为一线金融移动端工程实践总结,持续分享架构、性能、稳定性相关技术内容,欢迎交流~ Github: https://github.com/brycegao

基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-06-12 02:05:46 HTTP/1.1 GET : https://www.yeyulingfeng.com/a/736749.html
  2. 运行时间 : 0.119627s [ 吞吐率:8.36req/s ] 内存消耗:4,962.20kb 文件加载:145
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=ced4180b725d51b5d652fbae47fc15c1
  1. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/autoload_static.php ( 6.05 KB )
  7. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/ralouphie/getallheaders/src/getallheaders.php ( 1.60 KB )
  10. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  11. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  12. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  13. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  14. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  15. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  16. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  17. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  18. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  19. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/guzzlehttp/guzzle/src/functions_include.php ( 0.16 KB )
  21. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/guzzlehttp/guzzle/src/functions.php ( 5.54 KB )
  22. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  23. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  24. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  25. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/provider.php ( 0.19 KB )
  26. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  27. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  28. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  29. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/common.php ( 0.03 KB )
  30. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  32. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/alipay.php ( 3.59 KB )
  33. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  34. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/app.php ( 0.95 KB )
  35. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/cache.php ( 0.78 KB )
  36. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/console.php ( 0.23 KB )
  37. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/cookie.php ( 0.56 KB )
  38. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/database.php ( 2.48 KB )
  39. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/filesystem.php ( 0.61 KB )
  40. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/lang.php ( 0.91 KB )
  41. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/log.php ( 1.35 KB )
  42. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/middleware.php ( 0.19 KB )
  43. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/route.php ( 1.89 KB )
  44. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/session.php ( 0.57 KB )
  45. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/trace.php ( 0.34 KB )
  46. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/view.php ( 0.82 KB )
  47. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/event.php ( 0.25 KB )
  48. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  49. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/service.php ( 0.13 KB )
  50. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/AppService.php ( 0.26 KB )
  51. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  52. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  53. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  54. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  55. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  56. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/services.php ( 0.14 KB )
  57. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  58. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  59. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  60. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  61. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  62. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  63. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  64. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  65. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  66. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  67. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  68. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  69. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  70. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  71. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  72. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  73. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  74. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  75. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  76. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  77. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  78. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  79. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  80. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  81. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  82. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  83. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  84. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  85. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  86. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  87. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/Request.php ( 0.09 KB )
  88. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  89. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/middleware.php ( 0.25 KB )
  90. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  91. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  92. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  93. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  94. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  95. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  96. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  97. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  98. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  99. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  100. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  101. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  102. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  103. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/route/app.php ( 3.94 KB )
  104. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  105. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  106. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/controller/Index.php ( 9.87 KB )
  108. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/BaseController.php ( 2.05 KB )
  109. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  110. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  111. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  112. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  113. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  114. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  115. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  116. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  117. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  118. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  119. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  120. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  121. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  122. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  123. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  124. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  125. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  126. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  127. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  128. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  129. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  130. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  131. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  132. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  133. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  134. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  135. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/controller/Es.php ( 3.30 KB )
  136. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  137. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  138. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  139. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  140. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  141. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  142. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  143. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  144. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/runtime/temp/c935550e3e8a3a4c27dd94e439343fdf.php ( 31.50 KB )
  145. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.000529s ] mysql:host=127.0.0.1;port=3306;dbname=wenku;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.000769s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.000274s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000365s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.000501s ]
  6. SELECT * FROM `set` [ RunTime:0.000205s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.000599s ]
  8. SELECT * FROM `article` WHERE `id` = 736749 LIMIT 1 [ RunTime:0.000577s ]
  9. UPDATE `article` SET `lasttime` = 1781201146 WHERE `id` = 736749 [ RunTime:0.000751s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 64 LIMIT 1 [ RunTime:0.000256s ]
  11. SELECT * FROM `article` WHERE `id` < 736749 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.000452s ]
  12. SELECT * FROM `article` WHERE `id` > 736749 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.000505s ]
  13. SELECT * FROM `article` WHERE `id` < 736749 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.001796s ]
  14. SELECT * FROM `article` WHERE `id` < 736749 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.001359s ]
  15. SELECT * FROM `article` WHERE `id` < 736749 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.000930s ]
0.122408s