GTE中文嵌入模型部署案例:Kubernetes集群中水平扩缩容的嵌入服务编排实践

1. 引言:为什么需要可伸缩的文本嵌入服务

想象一下,你正在搭建一个智能客服系统,或者一个文档搜索引擎。用户每输入一个问题,系统都需要在后台将这个问题转换成计算机能理解的“数字密码”,然后去海量的知识库中寻找最匹配的答案。这个将文字转换成“数字密码”的过程,就是文本嵌入。

GTE中文文本嵌入模型,就是专门为中文场景打造的、效果出色的“密码生成器”。它能将一段中文文本,转换成一个1024维的向量(你可以理解为一串有1024个数字的密码)。这个密码非常神奇:意思相近的句子,它们的密码也会很相似。这样,我们通过计算密码之间的“距离”,就能判断两段文字在含义上是否接近。

现在问题来了。如果你的应用只有几十个用户,一台服务器跑这个模型绰绰有余。但当用户量暴涨到几千、几万,或者你需要同时处理大批量的文档时,一台服务器就会成为瓶颈,响应变慢,甚至直接崩溃。

这就是我们今天要解决的问题:如何让GTE嵌入服务像橡皮筋一样,能伸能缩,自动应对流量高峰和低谷? 答案就是使用Kubernetes(K8s)进行容器化编排,并实现服务的水平自动扩缩容。接下来,我将带你一步步实践这个方案。

2. 核心概念快速理解

在深入部署之前,我们先花几分钟搞清楚几个关键概念。不用担心,我会用最直白的方式解释。

2.1 文本嵌入:给文字装上“条形码”

你可以把文本嵌入理解成给每段文字生成一个独一无二的“条形码”。

  • 传统方法:像早期的商品条形码,只能区分不同商品,但看不出商品之间的关系(比如牛奶和面包都是食品)。
  • GTE这类现代模型:生成的则是“智能条形码”。不仅独一无二,还能体现含义。两段意思相近的文字,它们的“智能条形码”在扫描器下会显示非常接近,系统就知道它们说的是同一类事。

2.2 Kubernetes:数据中心的“自动驾驶系统”

Kubernetes(简称K8s)是一个管理大量容器的平台。你可以把它想象成一个全自动的、超级智能的仓库管理系统。

  • 容器:就像一个个标准化、封装好的货箱,里面装着你的应用(比如GTE服务)和它需要的所有环境。
  • K8s的作用:它负责把这些货箱(容器)调度到合适的货架(服务器节点)上运行。当某个货箱里的商品(服务)快卖完了(请求太多),它能自动复制出更多一模一样的货箱来应对。当需求减少时,它又能自动回收多余的货箱,节省资源。

2.3 水平扩缩容:让服务“分身有术”

这是本次实践的核心目标。

  • 水平扩容:当访问量变大时,不是去升级某一台服务器的CPU和内存(这称为垂直扩容,成本高且有限),而是直接启动多个完全相同的GTE服务实例,让它们一起分担压力。就像超市结账,人多时就多开几个收银台。
  • 自动缩容:当访问量下降时,自动关闭一些多余的服务实例,节省计算资源(也就是省钱)。
  • K8s的HPA:Horizontal Pod Autoscaler(水平Pod自动扩缩器)就是K8s里负责这个“自动开/关收银台”功能的组件。

理解了这些,我们就可以开始动手了。

3. 第一步:将GTE服务打包成容器

要让K8s管理我们的服务,首先得把它装进“标准货箱”——也就是Docker容器。

我们基于原始的app.py创建一个Dockerfile。这个文件告诉Docker如何构建我们的镜像。

# 使用一个包含Python和常用深度学习库的基础镜像
FROM pytorch/pytorch:2.0.1-cuda11.7-cudnn8-runtime

# 设置工作目录
WORKDIR /app

# 复制模型文件和应用代码
# 假设你的模型文件在本地目录 `nlp_gte_sentence-embedding_chinese-large` 中
COPY nlp_gte_sentence-embedding_chinese-large/ /app/

# 安装依赖
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install fastapi uvicorn

# 可选:将FastAPI服务整合进app.py,或创建一个新的main.py
# 这里我们假设你创建了一个新的入口文件 main.py (内容见下方)
COPY main.py /app/

# 暴露服务端口(与app.py中一致)
EXPOSE 7860

# 启动命令
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]

为了让服务更适合云原生部署,我们通常会用FastAPI重构一个API入口,因为它更轻量,对异步支持更好。创建一个main.py

from fastapi import FastAPI
from pydantic import BaseModel
from typing import List
import sys
import os

# 将原app.py的路径加入系统路径,以便导入
sys.path.insert(0, '/app')
# 这里需要根据你实际的app.py结构调整导入方式
# 假设app.py中有一个初始化好的模型对象 `model` 和计算函数 `compute_similarity`, `get_embedding`
try:
    from app import model, compute_similarity, get_embedding
except ImportError:
    # 如果导入失败,这里写一个模拟函数,实际使用时请替换
    print("Warning: Using mock functions. Please implement real imports.")
    model = None
    def compute_similarity(source, candidates):
        return [0.8 for _ in candidates]
    def get_embedding(text):
        return [0.1] * 1024

app = FastAPI(title="GTE Chinese Embedding Service")

class SimilarityRequest(BaseModel):
    source_text: str
    candidate_texts: List[str]

class EmbeddingRequest(BaseModel):
    text: str

@app.get("/health")
def health_check():
    return {"status": "healthy"}

@app.post("/v1/similarity")
def calculate_similarity(request: SimilarityRequest):
    """计算源文本与一系列候选文本的相似度"""
    scores = compute_similarity(request.source_text, request.candidate_texts)
    return {"scores": scores}

@app.post("/v1/embedding")
def get_text_embedding(request: EmbeddingRequest):
    """获取单个文本的向量表示"""
    vector = get_embedding(request.text)
    return {"embedding": vector, "dimension": len(vector)}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=7860)

构建并测试镜像

# 在包含Dockerfile和代码的目录下执行
docker build -t gte-embedding:1.0 .

# 测试运行
docker run -p 7860:7860 --name gte-test gte-embedding:1.0

用curl测试一下API是否正常:

curl -X POST http://localhost:7860/v1/embedding \
  -H "Content-Type: application/json" \
  -d '{"text": "今天天气真好"}'

如果看到返回了一个1024维的向量,恭喜你,容器化第一步成功了!接下来,我们要把这个镜像推送到一个K8s能拉取到的镜像仓库(如Docker Hub、阿里云容器镜像服务等)。

docker tag gte-embedding:1.0 your-registry.com/your-username/gte-embedding:1.0
docker push your-registry.com/your-username/gte-embedding:1.0

4. 第二步:在Kubernetes中部署基础服务

现在,我们有了标准的“货箱”(容器镜像),可以把它交给K8s这个“仓库管理系统”了。我们需要编写一个K8s的部署配置文件。

创建一个文件叫 gte-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: gte-embedding-deployment
  labels:
    app: gte-embedding
spec:
  replicas: 2  # 初始启动2个实例(Pod)
  selector:
    matchLabels:
      app: gte-embedding
  template:
    metadata:
      labels:
        app: gte-embedding
    spec:
      containers:
      - name: gte-container
        image: your-registry.com/your-username/gte-embedding:1.0  # 替换为你的真实镜像地址
        ports:
        - containerPort: 7860
        resources:
          requests:  # 每个容器最少需要的资源
            memory: "2Gi"
            cpu: "1000m"  # 1个CPU核心
          limits:    # 每个容器最多能用的资源
            memory: "4Gi"
            cpu: "2000m"  # 2个CPU核心
        livenessProbe:  # 存活探针,检查容器是否健康
          httpGet:
            path: /health
            port: 7860
          initialDelaySeconds: 30  # 容器启动后30秒开始检查
          periodSeconds: 10        # 每10秒检查一次
        readinessProbe: # 就绪探针,检查容器是否准备好接收流量
          httpGet:
            path: /health
            port: 7860
          initialDelaySeconds: 5
          periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: gte-embedding-service
spec:
  selector:
    app: gte-embedding
  ports:
  - port: 80          # 服务对集群内暴露的端口
    targetPort: 7860  # 转发到容器的端口
  type: ClusterIP     # 服务类型,仅在集群内部可访问

关键配置解释

  • replicas: 2:告诉K8s一开始就运行2个完全一样的GTE服务实例(Pod),实现负载均衡和高可用。
  • resources:这是至关重要的配置。它定义了每个容器需要多少CPU和内存。后面的自动扩缩容(HPA)就是根据这里的requests值来计算资源使用率的。我们预估GTE模型加载后需要约2GB内存,推理时需要一定CPU。
  • livenessProbe & readinessProbe:健康检查。K8s会定期调用/health接口。如果连续失败,livenessProbe会重启容器;readinessProbe失败则会将该实例从流量入口中暂时移除,直到恢复健康。这保证了服务的自愈能力。

应用这个配置到你的K8s集群

kubectl apply -f gte-deployment.yaml

检查部署状态

kubectl get deployments
kubectl get pods -l app=gte-embedding
kubectl get svc gte-embedding-service

你应该能看到名为gte-embedding-deployment的部署创建成功,并且有两个Pod在运行,还有一个对应的Service。

5. 第三步:实现自动水平扩缩容(HPA)

这是让服务具备“弹性”的关键。我们将创建一个HPA资源,让它监控Pod的CPU使用率,并自动调整Pod的数量。

创建一个文件 gte-hpa.yaml

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: gte-embedding-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: gte-embedding-deployment  # 指向我们刚才创建的Deployment
  minReplicas: 1  # 最少保持1个实例
  maxReplicas: 10 # 最多可扩展到10个实例
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70  # 目标:所有Pod的平均CPU使用率维持在70%
  behavior:  # 扩缩容行为配置,防止抖动
    scaleDown:
      stabilizationWindowSeconds: 300  # 缩容冷却期300秒
      policies:
      - type: Percent
        value: 50  # 一次缩容最多减少当前副本数的50%
        periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 60   # 扩容冷却期60秒
      policies:
      - type: Percent
        value: 100 # 一次扩容最多增加当前副本数的100%(即翻倍)
        periodSeconds: 60
      - type: Pods
        value: 4   # 或者一次最多增加4个Pod
        periodSeconds: 60
      selectPolicy: Max  # 取两个策略中扩容幅度最大的一个

配置解读

  • target: averageUtilization: 70:HPA会持续计算所有运行中Pod的CPU使用率的平均值。如果这个平均值超过70%,它就会触发扩容,增加Pod数量来分担负载。如果低于70%,一段时间后就会触发缩容。
  • behavior:这部分配置是为了避免“抖动”。例如,一个短暂的流量脉冲导致CPU飙升,HPA立即扩容,但流量很快回落,又立即缩容。stabilizationWindowSeconds(稳定窗口)设置了冷却时间,在窗口期内,HPA会观察指标是否持续维持在阈值之外,再决定是否行动。policies则控制了每次扩缩容的幅度,避免变化过于剧烈。

应用HPA配置

kubectl apply -f gte-hpa.yaml

查看HPA状态

kubectl get hpa gte-embedding-hpa -w

-w参数会持续观察状态。一开始,TARGETS列可能会显示<unknown>/70%,需要等待一段时间(通常1-2分钟)让指标收集器(如Metrics Server)收集到数据。

6. 第四步:压力测试与效果验证

部署好了,我们得验证一下自动扩缩容是否真的有效。我们可以使用一个简单的压力测试工具,比如 heywrk

首先,我们需要让集群外的测试工具能访问到服务。有几种方式:

  1. 临时端口转发(最简单,用于测试):

    kubectl port-forward svc/gte-embedding-service 8080:80
    

    这样,本地8080端口就映射到了集群内的服务。

  2. 修改Service类型为NodePort或LoadBalancer(生产环境常用)。

我们用端口转发的方式,然后写一个Python脚本进行压力测试:

# stress_test.py
import concurrent.futures
import requests
import time
import sys

SERVICE_URL = "http://localhost:8080"  # 如果用了port-forward
# SERVICE_URL = "http://<你的服务真实IP>:<端口>"  # 生产环境

def send_embedding_request(text):
    """发送一个嵌入请求"""
    try:
        start = time.time()
        resp = requests.post(
            f"{SERVICE_URL}/v1/embedding",
            json={"text": text},
            timeout=10
        )
        latency = time.time() - start
        if resp.status_code == 200:
            return {"success": True, "latency": latency}
        else:
            return {"success": False, "latency": latency, "code": resp.status_code}
    except Exception as e:
        return {"success": False, "error": str(e)}

def run_test(concurrent_users=10, total_requests=200):
    texts = ["这是一个测试句子" + str(i) for i in range(100)]  # 准备100个不同的句子
    print(f"开始压力测试: {concurrent_users}并发,总计{total_requests}请求")

    results = []
    with concurrent.futures.ThreadPoolExecutor(max_workers=concurrent_users) as executor:
        future_to_req = {
            executor.submit(send_embedding_request, texts[i % len(texts)]): i 
            for i in range(total_requests)
        }
        for future in concurrent.futures.as_completed(future_to_req):
            results.append(future.result())

    success_count = sum(1 for r in results if r.get('success'))
    avg_latency = sum(r.get('latency', 0) for r in results if r.get('success')) / max(success_count, 1)
    print(f"测试完成。成功率: {success_count}/{total_requests} ({success_count/total_requests*100:.1f}%)")
    print(f"平均延迟: {avg_latency:.3f}秒")
    return success_count / total_requests

if __name__ == "__main__":
    # 先进行一轮低并发测试,观察基线
    print("=== 基线测试 (并发5) ===")
    run_test(5, 50)
    time.sleep(5)

    # 进行高并发测试,触发扩容
    print("\n=== 高负载测试 (并发30) ===")
    run_test(30, 300)

在测试过程中,打开另一个终端窗口,观察HPA和Pod的变化

# 观察HPA指标和副本数变化
kubectl get hpa gte-embedding-hpa -w

# 观察Pod数量的变化
kubectl get pods -l app=gte-embedding -w

你预期会看到的现象

  1. 压力测试开始时,现有Pod的CPU使用率会迅速上升。
  2. 当平均CPU使用率超过70%并持续一段时间后,HPA的REPLICAS列数字会增加(比如从2变成4、6)。
  3. kubectl get pods会显示新的Pod正在被创建(状态从PendingContainerCreating再到Running)。
  4. 新的Pod启动后,会通过readinessProbe检查,然后开始接收流量,整体CPU使用率会被拉低。
  5. 压力测试停止后,CPU使用率下降。经过300秒的缩容冷却期,HPA会开始逐步减少Pod数量,直到最小副本数1。

通过这个测试,你就能亲眼见证整个自动扩缩容流程的运作。

7. 生产环境进阶考量

上面的实践已经搭建了一个可用的弹性服务。但要用于真实生产环境,还需要考虑更多:

7.1 基于自定义指标的扩缩容

CPU使用率并非总是最合适的扩缩容指标。对于类似GTE的AI推理服务,每秒查询数(QPS)请求平均延迟(P95 Latency) 可能是更好的选择。

这需要:

  1. 部署PrometheusMetrics Server来收集应用自定义指标。
  2. 在应用中暴露指标端点(比如用prometheus_client库)。
  3. 使用K8s Custom Metrics APIPrometheus Adapter,让HPA能读取到你的自定义指标。
  4. 修改HPA配置,使用type: Podstype: Object的自定义指标。

7.2 资源优化与成本控制

  • 使用GPU资源:GTE模型在GPU上推理速度远快于CPU。你可以在Deployment的resources.limits中申请nvidia.com/gpu: 1。但GPU很贵,HPA扩缩GPU Pod的成本很高,需要精细设计,比如采用“CPU实例队列缓冲,GPU实例批量处理”的混合架构。
  • 使用节点亲和性/污点容忍:将GTE服务调度到带有GPU的特定节点上。
  • 设置合理的minReplicas:即使没有流量,也保持一个最小实例数,避免冷启动带来的首次请求延迟过高。你可以根据业务低谷期来设置。

7.3 高可用与灾难恢复

  • 多副本部署:我们已经做了,这是基础。
  • 使用Pod反亲和性:避免所有副本都调度到同一个物理节点上,防止节点宕机导致服务全挂。
    spec:
      template:
        spec:
          affinity:
            podAntiAffinity:
              preferredDuringSchedulingIgnoredDuringExecution:
              - weight: 100
                podAffinityTerm:
                  labelSelector:
                    matchExpressions:
                    - key: app
                      operator: In
                      values:
                      - gte-embedding
                  topologyKey: kubernetes.io/hostname
    
  • 完善的监控与告警:对服务的QPS、延迟、错误率、Pod数量、资源使用率设置监控看板和告警规则。

8. 总结

通过这次实践,我们完成了一个完整的闭环:将一个单机的GTE中文嵌入模型服务,改造为运行在Kubernetes上、能够根据负载自动水平扩缩容的弹性微服务。

回顾一下核心收获

  1. 容器化是基础:Docker将应用与环境打包,实现了环境一致性,为K8s调度铺平道路。
  2. Deployment定义服务形态:它声明了服务想要的状态(用哪个镜像、运行几个副本、需要多少资源),K8s会努力维持这个状态。
  3. HPA是实现弹性的引擎:通过监控CPU等指标,自动增减Pod副本数,让服务资源利用率保持高效,同时从容应对流量波动。
  4. 健康探针保障服务健壮性livenessProbereadinessProbe让服务具备了自检和自愈能力。
  5. 生产环境需要更多打磨:从基础的CPU扩缩容,到基于QPS/延迟的智能扩缩容,再到GPU资源管理、成本优化和高可用设计,每一步都值得深入探索。

这种基于Kubernetes的弹性架构,不仅适用于GTE嵌入模型,也适用于其他各类AI模型服务(如图像识别、语音合成、大语言模型推理等)。它解决了AI服务部署中常见的资源利用率不均、难以应对突发流量、运维复杂度高等痛点。

下次当你需要部署一个AI服务时,不妨从容器化和K8s编排开始思考,让你的服务从一开始就具备“云原生”的弹性基因。


获取更多AI镜像

想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐