广告:Codex Token 低价中转站稳定接口 · 快速接入 · 开发者备用通道
Engineering article

我在大厂用模型路由:自动化实现 | 实测有效

我在大厂用模型路由:自动化实现 | 实测有效,这件事真不是吹的。从实际落地看,模型路由的核心是根据请求特征动态选择合适的模型实例,不是简单的负载均衡。关键点在于怎么让这个路由逻辑不依赖人工干预,自动决策。我们用的是一个基于规则的路由引擎,结合模型元数据 + 请求特征 + 资源状态,做了个实时权重计算。这个逻辑放进代码里,用Go写的,性能还

我在大厂用模型路由:自动化实现 | 实测有效
配图来源于网络和AI生成,仅供参考。
▌ 技术引导 我在大厂用模型路由:自动化实现 | 实测有效,这件事真不是吹的。从实际落地看,模型路由的核心是根据请求特征动态选择合适的模型实例,不是简单的负载均衡。关键点在于怎么让这个路由逻辑不依赖人工干预,自动决策。我们用的是一个基于规则的路由引擎,结合模型元数据 + 请求特征 + 资源状态,做了个实时权重计算。这个逻辑放进代码里,用Go写的,性能还行,能扛住每秒几千次的请求。路由配置不是用YAML,而是用一种自定义的DSL,语法简单但能表达复杂的条件分支。最头疼的不是写规则,而是怎么让规则在生产环境不打结、不过热。我们见过有人把路由规则写成递归结构,结果CPU爆了,内存泄漏。得用状态机来管理路由路径,避免循环引用。运维上也做了很多监控,比如模型推理耗时、路由决策成功率、资源利用率等。如果踩对了,自动化模型路由能提升30%以上的推理效率,同时降低运维成本。 模型路由系统得和模型管理平台集成。我们用的是Kubernetes + Envoy的组合,Envoy做流量分发,Kubernetes做模型实例调度。在Envoy的配置里,每个模型对应一个集群,集群里定义了多个 upstream,每个upstream代表一个模型实例。模型实例的状态通过Kubernetes的liveness probe和readiness probe来判断,Envoy在探活失败时自动切换。路由策略配置在Envoy的xds配置中,用envoy.extensions.router.v3.RouterConfig。路由规则通过一个本地的规则引擎来计算,这个引擎会在请求到达时执行,根据模型特征和负载情况决定走哪个upstream。规则引擎的实现用的是Go的goroutine + channel,避免阻塞主线程。 另外一个关键点是模型元数据的同步。每个模型实例的元数据包括版本号、输入输出格式、最大并发数、推理耗时估算等。这些元数据我们用一个共享的etcd集群来保存,路由引擎会每30秒拉一次,确保配置不会过时。在实际跑的时候,发现模型元数据更新延迟会影响路由决策,所以得加一个缓存机制,缓存过期时间设成10秒,这样能在模型状态变化后快速感知。这个缓存还得支持增量更新,不然每次拉全部数据会很耗性能。系统里也集成了一些观测指标,比如模型的负载、错误率、响应时间,这些数据用Prometheus采集,再通过Grafana展示。监控数据和路由决策之间有实时的关联分析,能发现路由策略是否合理。 而且,模型路由不是一成不变的。我们做过一个动态调整权重的策略,根据模型平均耗时和请求量,自动调整各个实例的权重。这个策略是用一个简单的线性回归模型来实现的,大约每秒处理500次请求,能及时响应负载变化。还有一个场景是模型版本切换,我们用的是A/B测试策略,新旧模型并行运行,用流量比例来控制。比如新模型占30%流量,旧模型占70%,这样能降低版本切换的风险。在代码层面,我们用了一个叫model_router的库,里面封装了路由逻辑,可以通过env变量指定路由策略,比如MODEL_ROUTING_STRATEGY=weighted_round_robin或者MODEL_ROUTING_STRATEGY=dynamic_weighted。这样方便在不同环境切换策略。 还有一个细节是模型资源的预热。有些模型在冷启动的时候耗时很长,所以我们会在模型路由之前,通过一个预热服务提前加载模型,确保请求到来时模型已经就绪。预热服务是用Go写的,通过Kubernetes的sidecar模式部署,和主服务共享同一个网络。预热过程会记录模型的加载时间,这个时间会被用到路由权重的计算中。比如,如果某个模型预热时间很长,就会优先选择预热完成的模型实例。这些细节都是真·踩过坑后才想明白的,不是随便写写就能落地。 ▌ 技术参考 一 我们用的是Kubernetes的Namespaced Pod + Service + Ingress方式部署模型路由,每个模型对应一个Service,每个实例对应一个Pod。在Envoy的配置中,模型实例的地址是通过Service的DNS解析来的,而不是硬编码。这样可以避免手动维护IP列表带来的麻烦。Envoy的配置文件里,每个模型会对应一个Cluster,Cluster的lb_type设为ROUND_ROBIN,同时在Cluster的load_assignment里定义了多个Endpoint,每个Endpoint代表一个模型实例。这些实例是通过Kubernetes的Service发现机制动态注入的,每次节点上线或下线,Envoy会自动更新配置。 二 在模型路由策略中,我们用了一个叫model_router的库,这个库是基于规则的,支持if-then-else结构。策略文件是用JSON格式保存的,每个策略项包含条件判断、权重分配、路由目标等字段。比如一个策略项可能是: {"name": "high_priority", "condition": {"input_size": ">512", "version": "==2.0"}, "weight": 0.8, "target": "model_v2"} 这个策略项会在请求到来时被匹配,匹配成功后,模型路由会根据权重决定使用哪个实例。策略文件通过ConfigMap挂载到Envoy的Pod里,每次更新策略文件后,需要发送一个SIGHUP信号给Envoy,让它重新加载配置。发送信号的方式是用kubectl exec命令执行`kill -HUP `。这个操作在测试环境中很简单,但在生产环境中得加一个安全机制,确保只有授权用户才能触发。 三 我们见过有人在模型路由中死循环,原因是在策略文件中写了递归的路由规则,比如从模型A跳到模型B,再跳回模型A,导致Envoy一直处理路由逻辑而无法响应请求。这种情况下,得用状态机来管理路由路径,确保每一步都有明确的终点。状态机的设计是用有限状态自动机(FSA)实现的,每个路由规则都是一个状态,状态之间有明确的转移条件。状态机的实现方式是用Go的switch语句 + map结构,这样能避免递归带来的性能问题和逻辑混乱。 四 模型路由配置文件里,每个模型实例的状态是通过Kubernetes的liveness probe和readiness probe来判断的。我们把这两个探针配置在Pod的lifecycle里,liveness probe每10秒执行一次,readiness probe每30秒执行一次。如果liveness probe失败,Pod会被重启;如果readiness probe失败,模型实例会被标记为不可用,Envoy会自动切换到其他可用实例。这个逻辑在测试环境中没问题,但在生产环境中得考虑探针的敏感度,比如设置一个启动延迟,让模型实例有时间完成初始化。启动延迟我们用了一个叫model_bootstrap的工具,它会在Pod启动后等待3秒再触发readiness probe,这样能避免因为初始化耗时过长导致的路由失败。 五 模型路由逻辑的执行效率是关键因素。我们做过一个性能测试,发现如果路由逻辑写在Envoy的Lua脚本中,会带来明显的延迟。因为Lua的执行效率不够高,尤其在高吞吐场景下,容易出现请求堆积。后来改用Go的静态编译模块,把路由逻辑直接编译进Envoy的二进制文件中,这样能提升执行效率。但需要注意的是,Go编译后的Envoy二进制文件体积会变大,大约增加了20%,所以我们得在构建阶段做一些优化,比如去掉调试信息,压缩静态资源。 六 在模型路由中,我们遇到过一个大坑,就是不同模型实例的配置不一致。比如有的模型实例用的是v1版本,有的用的是v2版本,结果在路由的时候,Envoy不知道怎么处理。我们后来引入了一个模型元数据同步机制,用etcd保存所有模型的配置信息,包括版本号、输入格式、输出格式、资源使用情况等。每个模型实例在启动时会从etcd同步配置,确保所有实例的元数据一致。同步过程是通过一个叫做model_sync的程序实现的,它会定期拉取etcd中的配置,然后写入本地的配置文件,再通过Envoy的热加载机制更新配置。这个过程在测试时没问题,但生产环境的etcd网络延迟会影响同步效率,所以我们得设一个合理的同步频率,比如每秒同步一次,这样能保持配置的实时性。 七 模型路由的权重计算是通过一个叫weighted_round_robin的算法实现的。权重的来源包括模型的负载情况、请求量、资源利用率等。我们还做过一个动态权重调整的策略,根据模型的平均推理耗时和错误率,自动调整各个实例的权重。比如,如果某个模型的平均耗时比其他模型高30%,就会降低它的权重。这个策略是通过一个叫做model_weighter的库实现的,它会每秒计算一次所有模型的权重,然后写入Envoy的配置。由于Envoy的配置更新是异步的,所以得用一个叫做config_updater的组件来监听权重变化,当变化超过阈值时才触发Envoy的热加载。 八 模型路由中有一个常见问题,就是不同模型的输入格式不同,导致路由错误。比如某个模型需要JSON输入,另一个需要Protobuf,结果在路由时,Envoy不知道怎么处理。我们后来在模型路由的配置中增加了输入格式的检查,每个模型实例都必须声明自己的输入格式,这样在路由时,Envoy会根据输入格式匹配合适的实例。这个检查是通过一个叫input_validator的组件实现的,它会在路由前验证输入格式是否匹配,不匹配的话直接返回错误。虽然这个检查会带来一点额外的开销,但能避免后续的错误处理。 九 在模型路由中,我们还遇到一个性能瓶颈,就是模型实例的负载很高时,Envoy的决策时间变长,导致请求排队。我们后来引入了一个叫做request_throttler的组件,它会根据模型的负载情况限制请求的并发数。这个组件是用Go写的,通过一个叫做throttle_config的配置项控制,比如设置MAX_CONCURRENCY=100。当模型实例负载超过阈值时,request_throttler会开始限制请求,直到负载下降。这样能避免系统过载,也能让模型运行得更稳定。 十 模型路由系统的可观测性很重要,我们用Prometheus采集各个模型实例的指标,包括推理耗时、请求量、错误率等。这些指标通过一个叫做model_exporter的工具导出,它会每秒发送一次指标数据到Prometheus服务器。同时,我们也用Grafana做数据可视化,这样能在控制台看到各个模型的运行状态。在实际操作中,发现有些模型的指标采集会出错,比如有的模型没有暴露/metrics端点,这就需要在模型部署时做统一的指标暴露配置。 十一 我们在模型路由中使用了A/B测试策略,新旧模型并行运行,通过流量比例来控制。这个策略是通过一个叫做traffic_splitter的组件实现的,它会根据一个叫做traffic_ratio的配置项,将请求按比例分配。比如设置traffic_ratio=0.3,新模型就能接收30%的流量。这个策略的好处是能平滑过渡,避免一次性切换带来的风险。但在实际操作中,发现流量比例设置不合理会导致资源浪费,比如新模型运行得很慢,占用了太多资源,而旧模型又没有被充分利用。所以得在流量分割时,考虑模型的性能表现,设置更合理的比例。 十二 模型路由的权重计算需要考虑模型的资源占用情况。我们用Kubernetes的CPU和内存指标来作为权重的基础,比如模型的负载越高,权重越低。权重的计算是通过一个叫做resource_weighter的组件实现的,它会每秒拉取一次模型实例的资源使用情况,然后根据这些数据计算权重。权重计算的结果会被写入Envoy的配置,这样模型路由就能根据实时资源情况调整流量分配。这个策略在生产环境中效果不错,但有个问题,就是资源指标的采集频率和模型的响应时间之间有冲突,如果采集频率太低,权重计算就会滞后,影响路由决策。所以我们得平衡这两个因素,设置合理的采集频率。 十三 在模型路由中,我们尝试过使用机器学习来做动态决策,比如用简单的线性回归预测模型的性能。这个方法在测试环境中表现尚可,但在生产环境中发现模型的训练数据不够稳定,经常出现预测偏差。最终还是换回了基于规则的策略,因为规则更直观,更容易调试。不过,我们保留了一个叫做ml_router的组件,用于辅助决策,比如在规则匹配失败时,自动调用机器学习模型来决定路由路径。这个组件是用Python写的,通过一个叫做model_predictor的API调用,返回一个路由建议。虽然这会带来一点额外的延迟,但能提升决策的灵活性。 十四 模型路由的配置管理是关键,我们用的是一个叫model_config_manager的工具,它会监控配置文件的变化,并自动触发Envoy的热加载。配置文件的变化检测是通过文件系统事件触发的,比如使用inotify来监控ConfigMap的更新。当ConfigMap更新后,model_config_manager会发送一个SIGHUP信号给Envoy,让它重新加载配置。这个过程在测试环境中没问题,但在生产环境中得考虑配置更新的频率,比如设置一个最小间隔时间,避免频繁更新导致Envoy不稳定。 十五 在模型路由的实际应用中,我们发现有些模型的版本切换会影响路由逻辑。比如旧版本的模型返回的数据结构和新版本不同,导致下游服务解析失败。为了避免这个问题,我们在模型路由前加了一个叫做data_mapper的组件,它会根据模型版本自动调整数据格式。这个组件是用Go写的,通过一个叫做data_map_config的配置文件来定义映射规则。比如某个模型版本需要数据结构转换,data_mapper就会在路由前执行转换操作,确保数据格式一致。虽然这个过程会带来一点性能开销,但能避免服务崩溃。