最近在做年终技术总结时,翻看了这一年的故障记录和优化日志,发现自己在分布式系统优化这条路上真是踩坑无数。今天挑三个最让我印象深刻的问题分享出来:分布式锁的惊群效应、容器化后的依赖地狱、还有Serverless冷启动带来的首次请求超时。
一、分布式锁竞争优化:一个秒杀活动引发的雪崩
故事的开始
今年618的时候,我们搞了个限量商品秒杀活动。原本以为准备很充分了------Redis集群、分布式锁、限流器,该有的都有。结果活动一开始,监控面板就炸了:
makefile
10:00:00 活动开始
10:00:01 QPS: 50000
10:00:02 Redis CPU: 95%
10:00:03 获取锁超时: 12000次/秒
10:00:05 服务雪崩,全线告警
最骚的是,实际成功下单的只有1000多人,剩下的请求全在抢锁的路上超时了。
问题分析
先看看当时的分布式锁实现:
java
// 最初的实现
public boolean tryGetStock(String productId, int quantity) {
String lockKey = "lock:stock:" + productId;
String requestId = UUID.randomUUID().toString();
try {
// 尝试获取锁
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, 5, TimeUnit.SECONDS);
if (!result) {
return false; // 获取锁失败
}
// 扣减库存
Integer stock = (Integer) redisTemplate.opsForValue().get("stock:" + productId);
if (stock >= quantity) {
redisTemplate.opsForValue().decrement("stock:" + productId, quantity);
return true;
}
return false;
} finally {
// 释放锁
if (requestId.equals(redisTemplate.opsForValue().get(lockKey))) {
redisTemplate.delete(lockKey);
}
}
}
看起来没什么问题对吧?但在5万QPS的冲击下,问题暴露无遗:
时间点 | 等待获取锁的请求数 | Redis网络连接数 | 成功获取锁/秒 | CPU使用率 |
---|---|---|---|---|
10:00:01 | 8000 | 5000 | 200 | 45% |
10:00:02 | 25000 | 15000 | 200 | 78% |
10:00:03 | 45000 | 25000 | 180 | 95% |
10:00:04 | 连接池耗尽 | 30000 | 50 | 99% |
优化方案一:分段锁
第一个想法是把一个商品的库存分成多段:
java
public class SegmentedStockService {
private static final int SEGMENTS = 10; // 分10段
public boolean tryGetStock(String productId, int quantity) {
// 随机选择一个分段
int segment = ThreadLocalRandom.current().nextInt(SEGMENTS);
for (int i = 0; i < SEGMENTS; i++) {
int trySegment = (segment + i) % SEGMENTS;
String lockKey = String.format("lock:stock:%s:%d", productId, trySegment);
if (tryGetStockFromSegment(lockKey, productId, trySegment, quantity)) {
return true;
}
}
return false;
}
private boolean tryGetStockFromSegment(String lockKey, String productId,
int segment, int quantity) {
String requestId = UUID.randomUUID().toString();
// 使用Lua脚本保证原子性
String script =
"if redis.call('set', KEYS[1], ARGV[1], 'NX', 'EX', '3') then " +
" local stock = redis.call('get', KEYS[2]) " +
" if tonumber(stock) >= tonumber(ARGV[2]) then " +
" redis.call('decrby', KEYS[2], ARGV[2]) " +
" redis.call('del', KEYS[1]) " +
" return 1 " +
" else " +
" redis.call('del', KEYS[1]) " +
" return 0 " +
" end " +
"else " +
" return -1 " +
"end";
String stockKey = String.format("stock:%s:%d", productId, segment);
Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
Arrays.asList(lockKey, stockKey), requestId, String.valueOf(quantity));
return result == 1;
}
}
效果有改善,但还是不够理想。大量请求还是在空转。
优化方案二:令牌桶 + 分布式锁
最终方案是先用令牌桶过滤掉大部分请求,只让少量请求去竞争锁:
java
@Component
public class OptimizedStockService {
private final LoadingCache<String, RateLimiter> rateLimiters;
public OptimizedStockService() {
this.rateLimiters = CacheBuilder.newBuilder()
.maximumSize(1000)
.build(new CacheLoader<String, RateLimiter>() {
@Override
public RateLimiter load(String key) {
// 每个商品每秒最多处理1000个请求
return RateLimiter.create(1000);
}
});
}
public StockResult tryGetStock(String productId, int quantity) {
// 第一层:令牌桶限流
RateLimiter limiter = rateLimiters.getUnchecked(productId);
if (!limiter.tryAcquire()) {
return StockResult.rateLimited();
}
// 第二层:预扣库存(使用Redis的原子操作)
Long remainStock = redisTemplate.execute(new RedisCallback<Long>() {
@Override
public Long doInRedis(RedisConnection conn) throws DataAccessException {
byte[] key = ("stock:" + productId).getBytes();
return conn.decrBy(key, quantity);
}
});
if (remainStock < 0) {
// 库存不足,回滚
redisTemplate.opsForValue().increment("stock:" + productId, quantity);
return StockResult.outOfStock();
}
// 第三层:异步处理订单(放入消息队列)
sendToQueue(new StockDeductionMessage(productId, quantity, remainStock));
return StockResult.success();
}
}
优化后的效果:
优化阶段 | QPS承载能力 | 平均响应时间 | 成功率 | Redis CPU |
---|---|---|---|---|
原始方案 | 5000 | 2000ms | 15% | 95% |
分段锁 | 15000 | 500ms | 45% | 80% |
令牌桶+预扣 | 50000 | 50ms | 98% | 35% |
二、容器化依赖管理:一个简单的升级引发的连锁反应
依赖地狱的开端
去年年底,我们决定把所有服务都容器化。本以为就是写写Dockerfile的事,结果一个OpenSSL的小版本升级,让整个系统瘫痪了4个小时。
事情是这样的,我们有个基础镜像:
dockerfile
# base.Dockerfile
FROM alpine:3.14
RUN apk add --no-cache \
openssl=1.1.1k-r0 \
curl=7.77.0-r1 \
python3=3.9.5-r1
然后各个服务都基于这个镜像:
dockerfile
# service-a.Dockerfile
FROM company/base:latest
RUN pip3 install requests==2.26.0
COPY . /app
CMD ["python3", "/app/main.py"]
某天,运维同学因为安全漏洞,把基础镜像的OpenSSL升级到了1.1.1l。结果...
问题爆发
部署后的故障现象:
服务 | 故障现象 | 根因 | 影响范围 |
---|---|---|---|
API网关 | SSL握手失败 | OpenSSL与nginx版本不兼容 | 所有HTTPS请求 |
支付服务 | 加密算法异常 | Python cryptography库依赖特定OpenSSL | 支付功能全挂 |
数据服务 | 连接数据库超时 | MySQL客户端库版本冲突 | 数据查询失败 |
日志服务 | 内存泄漏 | 新版OpenSSL的内存分配变化 | 容器不断重启 |
解决方案:依赖版本矩阵
痛定思痛,我们建立了一套完整的依赖管理体系:
yaml
# dependencies-matrix.yaml
base_images:
alpine-3.14:
packages:
openssl:
versions: ["1.1.1k-r0", "1.1.1l-r0"]
compatible_with:
nginx: ["1.20.1", "1.20.2"]
python-cryptography: ["3.4.7", "3.4.8"]
python3:
versions: ["3.9.5-r1"]
pip_packages:
requests: ["2.26.0", "2.27.0"]
cryptography: ["3.4.7", "3.4.8"]
ubuntu-20.04:
packages:
openssl:
versions: ["1.1.1f-1ubuntu2.8"]
# ...
然后写了个依赖检查工具:
python
# dependency_checker.py
import yaml
import docker
import subprocess
class DependencyChecker:
def __init__(self, matrix_file):
with open(matrix_file) as f:
self.matrix = yaml.safe_load(f)
def check_dockerfile(self, dockerfile_path):
issues = []
with open(dockerfile_path) as f:
content = f.read()
# 解析FROM指令
base_image = self._parse_base_image(content)
# 解析安装的包
packages = self._parse_packages(content)
# 检查兼容性
for pkg, version in packages.items():
if not self._is_compatible(base_image, pkg, version):
issues.append(f"{pkg}:{version} 不兼容 {base_image}")
return issues
def suggest_compatible_versions(self, base_image, package):
"""建议兼容的版本"""
# 实现略...
pass
依赖编排最佳实践
最后我们总结了一套容器依赖管理的最佳实践:
yaml
# docker-compose.deps.yml
version: '3.8'
x-base-versions: &base-versions
ALPINE_VERSION: "3.14"
OPENSSL_VERSION: "1.1.1k-r0"
PYTHON_VERSION: "3.9.5-r1"
services:
base-builder:
build:
context: ./base
args:
<<: *base-versions
image: company/base:${VERSION:-latest}
service-a:
build:
context: ./service-a
args:
BASE_IMAGE: company/base:${VERSION:-latest}
depends_on:
- base-builder
配合CI/CD pipeline:
groovy
// Jenkinsfile
pipeline {
stages {
stage('Dependency Check') {
steps {
script {
sh 'python3 dependency_checker.py check ./*/Dockerfile'
}
}
}
stage('Build Matrix') {
matrix {
axes {
axis {
name 'BASE_OS'
values 'alpine-3.14', 'ubuntu-20.04'
}
axis {
name 'OPENSSL_VERSION'
values '1.1.1k', '1.1.1l'
}
}
stages {
stage('Build') {
steps {
sh 'docker build --build-arg BASE_OS=${BASE_OS} ...'
}
}
}
}
}
}
}
三、无服务器冷启动优化:15秒到500毫秒的优化之旅
遭遇冷启动
今年我们把一些边缘业务迁移到了Serverless,比如图片处理、日志分析这些。一开始觉得按需付费很香,直到用户投诉图片上传第一次总是超时...
测试数据让人震惊:
场景 | 冷启动时间 | 热启动时间 | 内存使用 | 包大小 |
---|---|---|---|---|
图片处理(Python+OpenCV) | 15.3s | 230ms | 512MB | 245MB |
日志分析(Java) | 8.7s | 180ms | 1024MB | 89MB |
API转发(Node.js) | 2.1s | 45ms | 256MB | 35MB |
数据清洗(Go) | 1.2s | 25ms | 128MB | 15MB |
Python+OpenCV的组合,冷启动15秒,这谁顶得住啊!
优化第一步:瘦身
先从部署包瘦身开始:
python
# 优化前:requirements.txt
opencv-python==4.5.3.56 # 90MB
numpy==1.21.0 # 35MB
pandas==1.3.0 # 40MB
scikit-image==0.18.2 # 80MB
# 总计:245MB
# 优化后:requirements-slim.txt
opencv-python-headless==4.5.3.56 # 45MB,无GUI版本
numpy==1.21.0 # 35MB
# 只安装必需的,总计:80MB
自定义构建脚本,只打包用到的模块:
bash
#!/bin/bash
# build-lambda-package.sh
# 创建虚拟环境
python3 -m venv venv
source venv/bin/activate
# 安装依赖
pip install -r requirements-slim.txt -t ./package
# 只复制用到的OpenCV模块
mkdir -p package/cv2_minimal
cp -r package/cv2/cv2.*.so package/cv2_minimal/
cp package/cv2/__init__.py package/cv2_minimal/
# 删除不需要的文件
find package -name "*.pyc" -delete
find package -name "test*" -type d -exec rm -rf {} +
find package -name "*.dist-info" -type d -exec rm -rf {} +
# 使用UPX压缩二进制文件
find package -name "*.so" -exec upx --best {} \;
# 打包
cd package && zip -r9 ../deployment.zip . && cd ..
zip -g deployment.zip handler.py
优化第二步:预热 + 层级缓存
利用Lambda Layers和容器复用:
python
# warm_keeper.py
import json
import boto3
import time
from concurrent.futures import ThreadPoolExecutor
class WarmKeeper:
def __init__(self, function_names, interval=4*60): # 4分钟
self.functions = function_names
self.interval = interval
self.lambda_client = boto3.client('lambda')
def warm_function(self, function_name):
"""预热单个函数"""
try:
response = self.lambda_client.invoke(
FunctionName=function_name,
InvocationType='RequestResponse',
Payload=json.dumps({'warmup': True})
)
return response['StatusCode'] == 200
except Exception as e:
print(f"预热失败 {function_name}: {e}")
return False
def run(self):
"""持续预热"""
with ThreadPoolExecutor(max_workers=10) as executor:
while True:
futures = []
for func in self.functions:
future = executor.submit(self.warm_function, func)
futures.append(future)
# 等待所有预热完成
results = [f.result() for f in futures]
success_rate = sum(results) / len(results)
print(f"预热成功率: {success_rate:.2%}")
time.sleep(self.interval)
# 在Lambda函数中处理预热请求
def lambda_handler(event, context):
# 检查是否是预热请求
if event.get('warmup'):
# 加载必要的模块,初始化连接等
import cv2
import numpy as np
# 做一些轻量级初始化
test_array = np.zeros((100, 100, 3), dtype=np.uint8)
cv2.cvtColor(test_array, cv2.COLOR_BGR2GRAY)
return {'statusCode': 200, 'body': 'warmed'}
# 正常业务逻辑
return handle_image_processing(event)
优化第三步:使用编译型语言
对于性能要求高的场景,我们用Go重写了部分服务:
go
// image_processor.go
package main
import (
"context"
"encoding/base64"
"github.com/aws/aws-lambda-go/lambda"
"github.com/disintegration/imaging"
)
type ImageEvent struct {
Image string `json:"image"` // base64
Width int `json:"width"`
Height int `json:"height"`
}
type ImageResponse struct {
ProcessedImage string `json:"processed_image"`
ProcessTime int64 `json:"process_time_ms"`
}
var (
// 全局变量,避免重复初始化
decoder *base64.Encoding
)
func init() {
// 在init中做初始化,减少冷启动时间
decoder = base64.StdEncoding
}
func HandleRequest(ctx context.Context, event ImageEvent) (ImageResponse, error) {
start := time.Now()
// 解码图片
imageData, err := decoder.DecodeString(event.Image)
if err != nil {
return ImageResponse{}, err
}
// 处理图片
img, err := imaging.Decode(bytes.NewReader(imageData))
if err != nil {
return ImageResponse{}, err
}
// 调整大小
resized := imaging.Resize(img, event.Width, event.Height, imaging.Lanczos)
// 编码结果
var buf bytes.Buffer
err = imaging.Encode(&buf, resized, imaging.JPEG)
if err != nil {
return ImageResponse{}, err
}
return ImageResponse{
ProcessedImage: decoder.EncodeToString(buf.Bytes()),
ProcessTime: time.Since(start).Milliseconds(),
}, nil
}
func main() {
lambda.Start(HandleRequest)
}
最终优化成果
经过这一系列优化,冷启动时间大幅下降:
优化阶段 | Python服务 | Go服务 | 部署包大小 | 月度成本 |
---|---|---|---|---|
初始版本 | 15.3s | - | 245MB | $127 |
瘦身后 | 8.2s | - | 80MB | $89 |
加预热 | 3.1s (平均) | - | 80MB | $95 |
Go重写 | - | 0.5s | 12MB | $42 |
更重要的是,用户体验得到了极大改善:
javascript
// 客户端也做了优化,增加重试和预加载
class ServerlessClient {
constructor() {
this.preloadQueue = [];
this.warmupInterval = null;
}
async callFunction(functionName, payload, options = {}) {
const maxRetries = options.maxRetries || 3;
const timeout = options.timeout || 30000;
for (let i = 0; i < maxRetries; i++) {
try {
const response = await this._invokeWithTimeout(
functionName,
payload,
timeout
);
if (response.statusCode === 200) {
return response;
}
} catch (error) {
if (i === maxRetries - 1) throw error;
// 冷启动导致的超时,延长等待时间
if (error.code === 'TimeoutError' && i === 0) {
timeout = timeout * 2;
}
await this._delay(Math.pow(2, i) * 1000);
}
}
}
startWarmup(functionNames, interval = 240000) {
this.warmupInterval = setInterval(() => {
functionNames.forEach(name => {
this.callFunction(name, { warmup: true }, { timeout: 5000 })
.catch(err => console.warn(`Warmup failed for ${name}`));
});
}, interval);
}
}
总结
这一年在分布式系统优化上的经历,让我深刻体会到:
-
分布式锁不是万能的。高并发场景下,要考虑用其他方案(如预扣库存)来减少锁竞争。
-
容器化不只是打包那么简单。依赖管理做不好,一个小更新可能引发大故障。
-
Serverless很美好,但冷启动是真实的痛。选择合适的语言和运行时,做好预热,才能发挥Serverless的优势。
-
监控和可观测性太重要了。每次优化都要有数据支撑,不能凭感觉。
-
没有银弹。每个方案都有适用场景,关键是根据实际情况选择。
最近在研究Service Mesh,感觉又是一个大坑。等踩得差不多了,再来分享。如果你也有类似的优化经验,欢迎交流!