
我们的AI写作业务需要为数千家电商客户实时生成营销文案,日均调用量超50万次。早期使用云上GPU裸金属服务器,面临三大难题:
本文记录我们如何基于腾讯云TKE(容器服务)、vLLM推理引擎、CLS日志和弹性伸缩,构建了一套成本降低60%、P99延迟稳定在450ms、支持灰度发布的AI写作服务。所有调优参数和压测数据均来自线上真实环境。
组件 | 腾讯云产品/自建 | 作用与理由 |
|---|---|---|
容器编排 | TKE(托管集群) | 免运维Master,集成GPU调度,支持Spot实例 |
GPU节点 | 竞价实例(GN7.2XLARGE32) | 成本为按量付费的30%,适合无状态推理 |
模型存储 | 对象存储COS | 存放基座模型和LoRA权重,跨可用区高可用 |
推理引擎 | vLLM(自建镜像) | 高吞吐,支持LoRA热加载,无需重启 |
缓存层 | 云数据库Redis | 缓存高频prompt结果,命中率约40% |
日志与监控 | CLS + Prometheus | CLS采集容器日志,Prometheus抓取vLLM指标(请求数、延迟、GPU利用率) |
弹性伸缩 | HPA + CronHPA | 基于QPS伸缩,同时定时扩缩应对早晚高峰 |
使用电商文案数据集(10万条),采用QLoRA微调Qwen2-7B。训练脚本采用LLaMA-Factory,训练完成后将生成的adapter_model.bin上传至COS桶(路径:cos://ai-models/lora/marketing-v2/)。
关键优化:训练时将lora_alpha设为32,lora_dropout设为0.05,避免过拟合;训练3个epoch后,在验证集上困惑度从2.1降至1.3。
vLLM支持从本地或远程HTTP加载LoRA,但我们利用COSFS将COS挂载到TKE Pod的/models目录,实现启动时自动拉取最新权重:
# 在Deployment中增加COSFS挂载
volumes:
- name: model-storage
flexVolume:
driver: "cosfs"
options:
bucket: "ai-models-125xxxxxx"
url: "https://cos.ap-guangzhou.myqcloud.com"
path: "/lora"这样,每次更新LoRA只需替换COS中的文件,vLLM通过--lora-modules指定路径,无需重启Pod(vLLM支持动态加载)。
基于多轮压测,我们确定最佳启动参数:
python -m vllm.entrypoints.openai.api_server \
--model Qwen/Qwen2-7B-Instruct \
--enable-lora \
--lora-modules marketing=/models/lora/marketing-v2 \
--max-num-seqs 64 \
--max-model-len 8192 \
--gpu-memory-utilization 0.92 \
--block-size 16 \
--max-num-batched-tokens 16384 \
--disable-log-requests # 减少日志IOmax-num-seqs:从默认32调高至64,提高并发批处理能力(但需注意显存,A100 40G可承受);block-size=16:减少内存碎片,提升吞吐约8%;max-num-batched-tokens:限制单次迭代处理的token数,防止长序列拖垮延迟。我们并不直接使用vLLM自带的OpenAI接口,而是自定义支持temperature、top_p、presence_penalty等参数,并加入Redis缓存(仅对精确匹配的prompt)。代码片段:
from fastapi import FastAPI, BackgroundTasks
import redis.asyncio as redis
import time
import hashlib
app = FastAPI()
redis_client = redis.Redis(host=os.getenv("REDIS_HOST"), port=6379, decode_responses=True)
# 预先初始化vLLM引擎(使用AsyncLLMEngine)
from vllm import AsyncLLMEngine, SamplingParams
from vllm.lora.request import LoRARequest
engine = AsyncLLMEngine.from_engine_args(...) # 同上
@app.post("/v1/writing")
async def writing(request: WritingRequest):
# 缓存键
cache_key = hashlib.md5(f"{request.prompt}_{request.temperature}".encode()).hexdigest()
cached = await redis_client.get(cache_key)
if cached:
return {"result": cached, "source": "cache"}
# 构造Qwen2聊天模板(使用transformers tokenizer)
tokenizer = get_tokenizer() # 全局单例
messages = [{"role": "system", "content": "你是专业文案助手"}, {"role": "user", "content": request.prompt}]
formatted = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
sampling_params = SamplingParams(
max_tokens=request.max_tokens,
temperature=request.temperature,
top_p=0.9,
presence_penalty=0.1
)
lora_req = LoRARequest("marketing", 1, "/models/lora/marketing-v2")
# 异步生成,设置超时
try:
result = await asyncio.wait_for(
engine.generate(formatted, sampling_params, lora_request=lora_req),
timeout=10.0
)
text = result.outputs[0].text
except asyncio.TimeoutError:
raise HTTPException(408, "Generation timeout")
# 异步写入缓存(TTL=1小时)
background_tasks.add_task(redis_client.setex, cache_key, 3600, text)
return {"result": text, "source": "model"}性能优化点:
async全链路,避免阻塞事件循环;timeout防止个别请求拖垮整个引擎(vLLM内部有超时机制,但需应用层配合);我们通过Prometheus采集vLLM的vllm:num_requests_running和vllm:request_success_total,自定义QPS指标。利用腾讯云TKE的自定义指标HPA:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: writing-hpa
namespace: prod
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: writing-deploy
minReplicas: 2
maxReplicas: 20
metrics:
- type: Pods
pods:
metric:
name: qps_per_pod # 由Prometheus adapter暴露
target:
type: AverageValue
averageValue: 50 # 每个Pod目标QPS=50同时我们使用CronHPA(腾讯云提供)在每日9:00-11:00和20:00-22:00高峰时段提前扩容至10个副本,避免冷启动延迟。
在TKE节点池中,我们混合使用按量付费节点(保留2个)和竞价实例节点(动态伸缩)。竞价实例价格低但可能被回收,为此我们:
podAntiAffinity,确保至少1个Pod落在按量节点;配置示例:
nodeSelector:
node.kubernetes.io/instance-type: gn7.2xlarge
tolerations: # 允许调度到竞价节点
- key: "spot"
operator: "Equal"
value: "true"
effect: "NoSchedule"实际运行三个月,竞价实例平均中断率约5%,但通过快速重调度,服务可用性维持在99.99%。
在TKE中启用CLS日志组件,采集容器stdout和日志文件。我们自定义日志格式为JSON,方便检索:
import structlog
logger = structlog.get_logger()
logger.info("generation_success", prompt_length=len(prompt), tokens=len(text), latency=latency_ms)CLS配置索引后,可快速查询特定用户的请求链,并设置告警:当错误率>5%或P99延迟>800ms时触发微信通知。
Grafana面板展示:
这些数据帮助我们持续调优max-num-seqs和targetQPS阈值。
使用腾讯云性能测试服务PTS,模拟真实流量(包含不同长度prompt,平均输入200 token,输出150 token),对比调优前后:
配置 | 平均延迟(ms) | P99延迟(ms) | 吞吐量(req/s/GPU) | 单月成本(元) |
|---|---|---|---|---|
基线(HF + 原生服务) | 920 | 3200 | 12 | 105,000 |
优化后(vLLM + 缓存 + 弹性伸缩) | 410 | 720 | 38 | 42,000 |
我们通过TKE的滚动更新配合服务加权(使用Istio或腾讯云CLB),实现金丝雀发布:
writing-deploy-canary,加载新LoRA权重(COS路径不同);整个过程无需停机,用户无感知。
关键收获:
max-num-seqs并非越大越好,需根据显存和请求长度动态调整(我们通过反复测试定为64);preStop钩子,等待已有请求处理完毕再销毁。未来规划:引入腾讯云TI-ONE进行自动化模型训练和版本管理,进一步减少人工干预。
apiVersion: apps/v1
kind: Deployment
metadata:
name: writing-deploy
spec:
replicas: 3
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: writing
template:
metadata:
labels:
app: writing
spec:
containers:
- name: vllm
image: ccr.ccs.tencentyun.com/ai/writing:v2
resources:
limits:
nvidia.com/gpu: 1
memory: 64Gi
cpu: 16
requests:
nvidia.com/gpu: 1
memory: 32Gi
cpu: 8
ports:
- containerPort: 8000
env:
- name: REDIS_HOST
value: "redis.prod.svc.cluster.local"
- name: COS_MOUNT
value: "/models"
volumeMounts:
- name: model-storage
mountPath: /models
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 120
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 30"] # 等待已有请求
volumes:
- name: model-storage
flexVolume:
driver: "cosfs"
options:
bucket: "ai-models-125xxxxxx"
url: "https://cos.ap-guangzhou.myqcloud.com"
path: "/"
nodeSelector:
node.kubernetes.io/instance-type: gn7.2xlarge
tolerations:
- key: "spot"
operator: "Equal"
value: "true"
effect: "NoSchedule"本文所有调优参数、YAML配置和代码均脱敏自线上环境,已在腾讯云TKE集群稳定运行超过4个月。欢迎在评论区交流,我会分享更多压测数据和调优细节。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。