4. 请求路由与负载均衡:基于模型类型的路由、基于负载的路由、一致性哈希
好,咱们进入第四章。这一章聊的是多模型流水线的「交通指挥」问题。
你想想看,一个生产环境里,可能同时跑着十几个模型。有做文本分类的,有做图像识别的,还有做语音合成的。每个模型的算力需求不一样,响应时间要求也不一样。如果请求来了随便分配,那系统很快就会乱成一锅粥。
我个人习惯把路由策略分成三类:按模型类型、按负载情况、按一致性哈希。咱们一个一个说。
4.1 基于模型类型的路由
这是最直观的做法。说白了就是:什么类型的请求,就发给什么类型的模型实例。
举个例子。你的集群里有三台机器:
- 机器A:部署了BERT文本分类模型
- 机器B:部署了ResNet图像识别模型
- 机器C:部署了TTS语音合成模型
请求来了,网关根据请求头里的 model_type 字段,直接转发到对应的机器。简单粗暴,效率也高。
核心配置示例(Nginx + Lua):
-- 基于模型类型的路由
local model_type = ngx.var.arg_model_type
if model_type == "text_classification" then
ngx.exec("@backend_text")
elseif model_type == "image_recognition" then
ngx.exec("@backend_image")
elseif model_type == "speech_synthesis" then
ngx.exec("@backend_speech")
else
ngx.exit(400)
end
我在项目中遇到过一个问题:某个文本分类模型特别火,请求量是其他模型的10倍。结果那台机器CPU打满,其他机器却闲着。嗯,这就是纯类型路由的短板——它不考虑负载。
我的建议:类型路由适合模型数量少、请求量均匀的场景。如果某个模型是热点,一定要配合负载均衡使用。
4.2 基于负载的路由
这个就聪明多了。它不看请求是什么类型,只看哪台机器现在最闲。
常见的负载指标有:
- CPU使用率
- 内存占用
- 当前活跃连接数
- 请求队列长度
- 平均响应时间
我一般推荐用最小连接数算法。为什么呢?因为它最直接反映当前压力。
负载均衡策略对比:
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 轮询 | 实现简单 | 不考虑机器性能差异 | 同配置机器 |
| 加权轮询 | 考虑机器性能 | 权重需要手动调整 | 异构集群 |
| 最小连接数 | 动态感知负载 | 需要维护连接状态 | 长连接场景 |
| 响应时间加权 | 自动适应性能变化 | 计算开销稍大 | 延迟敏感场景 |
我曾经踩过一个坑:用CPU使用率做负载指标。结果发现,模型在加载权重时CPU飙高,但实际推理还没开始。请求被路由到其他机器,导致那台机器一直闲着。后来我改用请求队列深度,问题就解决了。
注意:负载均衡器本身不能成为瓶颈。我见过有人用Python写了个负载均衡器,结果它自己先撑不住了。生产环境建议用Nginx、HAProxy或者Envoy。
4.3 一致性哈希
这个稍微有点绕。但如果你做过缓存系统,一定不陌生。
先说说为什么需要它。假设你有3个模型实例,每个实例都缓存了部分请求的结果。如果某个实例挂了,或者你扩缩容了,传统的哈希取模会导致大量缓存失效。所有请求都得重新计算,系统性能瞬间暴跌。
一致性哈希就是为了解决这个问题。它把服务器和请求都映射到一个环上,请求只路由到环上最近的服务器。当服务器增减时,只有少量请求需要重新路由。
一致性哈希的核心代码(Python实现):
import hashlib
class ConsistentHash:
def __init__(self, nodes=None, virtual_nodes=150):
self.virtual_nodes = virtual_nodes
self.ring = {}
self.sorted_keys = []
if nodes:
for node in nodes:
self.add_node(node)
def _hash(self, key):
return int(hashlib.md5(key.encode()).hexdigest(), 16)
def add_node(self, node):
for i in range(self.virtual_nodes):
virtual_key = f"{node}:{i}"
hash_val = self._hash(virtual_key)
self.ring[hash_val] = node
self.sorted_keys.append(hash_val)
self.sorted_keys.sort()
def remove_node(self, node):
for i in range(self.virtual_nodes):
virtual_key = f"{node}:{i}"
hash_val = self._hash(virtual_key)
del self.ring[hash_val]
self.sorted_keys.remove(hash_val)
def get_node(self, key):
if not self.ring:
return None
hash_val = self._hash(key)
for ring_key in self.sorted_keys:
if hash_val <= ring_key:
return self.ring[ring_key]
return self.ring[self.sorted_keys[0]]
这里有个关键点:虚拟节点。为什么需要它?
你想想看,如果只有3个物理节点,它们在环上的分布可能很不均匀。有的节点负责的区域大,有的负责的区域小。虚拟节点就是把每个物理节点复制成150个虚拟节点,均匀分布在环上。这样负载就均衡了。
我的经验:虚拟节点数量一般设为100-200。太少分布不均匀,太多增加内存开销。我曾经试过500个虚拟节点,结果环上全是节点,查找效率反而下降了。
4.4 三种策略如何选择
说实话,没有银弹。我一般这样选:
- 模型类型差异大(比如文本和图像混跑):先用类型路由做第一层分流,再用负载均衡做第二层
- 模型类型相同(比如都是BERT):直接用负载均衡,最小连接数优先
- 有缓存需求(比如模型推理结果可复用):用一致性哈希,保证相同请求落到相同实例
一个生产级的配置示例(Envoy):
static_resources:
listeners:
- name: model_gateway
address:
socket_address: { address: 0.0.0.0, port_value: 8080 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
route_config:
name: model_routes
virtual_hosts:
- name: model_services
domains: ["*"]
routes:
- match: { headers: [{"name": "model_type", "exact_match": "text"}] }
route: { cluster: text_cluster }
- match: { headers: [{"name": "model_type", "exact_match": "image"}] }
route: { cluster: image_cluster }
http_filters:
- name: envoy.filters.http.router
clusters:
- name: text_cluster
type: STRICT_DNS
lb_policy: LEAST_REQUEST
load_assignment:
cluster_name: text_cluster
endpoints:
- lb_endpoints:
- endpoint: { address: { socket_address: { address: text1, port_value: 5000 } } }
- endpoint: { address: { socket_address: { address: text2, port_value: 5000 } } }
- name: image_cluster
type: STRICT_DNS
lb_policy: RING_HASH
load_assignment:
cluster_name: image_cluster
endpoints:
- lb_endpoints:
- endpoint: { address: { socket_address: { address: image1, port_value: 5000 } } }
- endpoint: { address: { socket_address: { address: image2, port_value: 5000 } } }
你看,这个配置里:文本模型用了最小连接数,图像模型用了一致性哈希。这就是根据业务需求灵活组合。
最后说一句:路由策略不是配完就完事的。我建议每两周检查一次负载分布,看看有没有热点。如果有,及时调整权重或者增加实例。毕竟,生产环境的变化,永远比你想象的快。