如何合理的设计高可用的系统
从一次线上故障说起
先讲个真实发生的事儿。
某天晚上8点,某视频网站的运营同学搞了个"限时免费看大片"活动,本来预估有个几万人参与就不错了。结果活动一开始,流量直接飙了10倍,用户拼命刷页面,评论服务先扛不住了,响应变慢。
问题来了,视频详情页要调用评论服务获取热门评论,评论服务慢了,详情页的请求线程就一直阻塞等着。等着等着,详情服务的线程池也满了,然后推荐服务调详情服务也开始超时...
不到5分钟,整个首页都打不开了。值班同学手忙脚乱,最后只能紧急扩容+重启服务,折腾了半个多小时才恢复。事后复盘,这就是典型的"雪崩效应"。
这种场景在分布式系统里太常见了,今天咱们就来好好聊聊怎么设计一个高可用系统,以及怎么防止雪崩。
高可用到底是个啥?
简单理解
高可用说白了就是:系统在绝大部分时间都能正常提供服务,即使出了问题也能快速恢复。
咱们平时说的"几个9"就是用来衡量可用性的:
graph LR
subgraph 可用性等级
A[99%<br/>一年宕机87.6小时]
B[99.9%<br/>一年宕机8.76小时]
C[99.99%<br/>一年宕机52分钟]
D[99.999%<br/>一年宕机5分钟]
end
A --> B --> C --> D
style A fill:#E57373,stroke:#C62828,stroke-width:2px,rx:10,ry:10
style B fill:#FF9800,stroke:#E65100,stroke-width:2px,rx:10,ry:10
style C fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,rx:10,ry:10
style D fill:#2196F3,stroke:#1565C0,stroke-width:2px,rx:10,ry:10很多公司对核心业务的要求是4个9(99.99%),也就是一年最多只能宕机52分钟。这个要求其实挺高的,稍微不注意就超标了。
系统为啥会挂?
根据这些年踩过的坑,总结下来无非就这几类:
第一类:流量问题
- 突发流量,比如秒杀活动、热点事件
- 爬虫或者恶意攻击导致的异常流量
第二类:代码问题
- 内存泄漏,跑着跑着OOM了
- 死循环或者死锁,CPU打满
- 慢SQL把数据库连接池耗尽
第三类:依赖问题
- 下游服务挂了或者变慢
- 第三方接口超时
- 中间件故障,比如Redis、MQ挂了
第四类:基础设施问题
- 服务器硬件故障
- 网络抖动或者断网
- 机房级别故障
雪崩是怎么发生的?
微服务架构下,服务之间的调用关系错综复杂,一个请求可能要经过好几个服务才能完成。这种情况下,任何一个环节出问题,都可能引发连锁反应。
用时间线还原一次雪崩
假设有个外卖App,用户下单时需要经过这些服务:
graph LR
User[用户] --> Gateway[网关]
Gateway --> OrderSvc[订单服务]
OrderSvc --> ShopSvc[商家服务]
OrderSvc --> CouponSvc[优惠券服务]
OrderSvc --> PaySvc[支付服务]
style User fill:#9C27B0,stroke:#6A1B9A,stroke-width:2px,rx:10,ry:10
style Gateway fill:#2196F3,stroke:#1565C0,stroke-width:2px,rx:10,ry:10
style OrderSvc fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,rx:10,ry:10
style ShopSvc fill:#FF9800,stroke:#E65100,stroke-width:2px,rx:10,ry:10
style CouponSvc fill:#E91E63,stroke:#AD1457,stroke-width:2px,rx:10,ry:10
style PaySvc fill:#00BCD4,stroke:#00838F,stroke-width:2px,rx:10,ry:10现在来看看雪崩是怎么一步步发生的:
T+0秒:午餐高峰,优惠券服务数据库CPU飙到95%
优惠券服务要查询用户可用券列表,结果一个复杂查询把数据库拖慢了,原本50ms能返回的接口,现在要5秒。
T+10秒:订单服务线程堆积
订单服务调用优惠券服务时,设置的超时时间是3秒。但优惠券服务5秒才返回,订单服务的请求线程就一直阻塞着。新的请求不断进来,线程池很快就满了。
graph TB
subgraph T+10秒
A[订单服务<br/>线程池200个]
B[已用180个<br/>都在等优惠券服务]
C[新请求进来<br/>排队等待]
end
A --> B --> C
style A fill:#FF9800,stroke:#E65100,stroke-width:2px,rx:10,ry:10
style B fill:#E57373,stroke:#C62828,stroke-width:2px,rx:10,ry:10
style C fill:#E57373,stroke:#C62828,stroke-width:2px,rx:10,ry:10T+30秒:网关层开始报警
订单服务处理不过来,网关那边积压了大量请求。用户端看到的就是转圈圈,过一会儿提示"网络异常请重试"。
T+60秒:用户疯狂重试,流量翻倍
用户发现下单失败,第一反应就是重试。一个用户重试3次,流量直接翻了3倍。本来已经过载的系统,这下彻底扛不住了。
T+120秒:多个服务陆续崩溃
商家服务也要调用订单服务查询订单状态,订单服务没响应,商家服务的线程也开始堆积。连锁反应下,半个系统都瘫痪了。
graph TB
subgraph 雪崩蔓延过程
Coupon[优惠券服务<br/>慢响应]
Order[订单服务<br/>线程池耗尽]
Shop[商家服务<br/>受影响]
Gateway[网关<br/>请求堆积]
User[用户<br/>体验崩溃]
end
Coupon -->|拖垮| Order
Order -->|影响| Shop
Order -->|请求超时| Gateway
Gateway -->|错误响应| User
User -->|重试| Gateway
style Coupon fill:#E57373,stroke:#C62828,stroke-width:3px,rx:10,ry:10
style Order fill:#E57373,stroke:#C62828,stroke-width:3px,rx:10,ry:10
style Shop fill:#FF9800,stroke:#E65100,stroke-width:2px,rx:10,ry:10
style Gateway fill:#FF9800,stroke:#E65100,stroke-width:2px,rx:10,ry:10
style User fill:#9C27B0,stroke:#6A1B9A,stroke-width:2px,rx:10,ry:10这就是雪崩,一个点的故障,通过服务调用链快速扩散,最终导致整个系统不可用。
防护手段全景图
好,问题分析清楚了,接下来聊聊怎么防。主要就这几板斧:
graph TB
subgraph 高可用防护体系
direction TB
A[限流<br/>控制流量入口]
B[熔断<br/>隔离故障服务]
C[降级<br/>保核心弃边缘]
D[超时控制<br/>防止无限等待]
E[重试策略<br/>容忍短暂故障]
F[横向扩容<br/>提升处理能力]
end
style A fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,rx:10,ry:10
style B fill:#FF9800,stroke:#E65100,stroke-width:2px,rx:10,ry:10
style C fill:#2196F3,stroke:#1565C0,stroke-width:2px,rx:10,ry:10
style D fill:#9C27B0,stroke:#6A1B9A,stroke-width:2px,rx:10,ry:10
style E fill:#00BCD4,stroke:#00838F,stroke-width:2px,rx:10,ry:10
style F fill:#E91E63,stroke:#AD1457,stroke-width:2px,rx:10,ry:10限流:控制流量入口
限流就像景区限制入园人数一样,系统能处理多少请求是有上限的,超过这个上限,宁可拒绝也不要把系统搞崩。
常用的限流算法:
| 算法 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 固定窗口 | 每个时间窗口内限制请求数 | 实现简单 | 窗口边界可能突发双倍流量 | 粗粒度限流 |
| 滑动窗口 | 平滑的时间窗口 | 解决边界问题 | 实现稍复杂 | 一般场景 |
| 漏桶算法 | 固定速率处理请求 | 流量平滑 | 无法应对突发流量 | 需要严格控速 |
| 令牌桶算法 | 按速率生成令牌,请求需获取令牌 | 允许一定突发 | 实现相对复杂 | 大部分场景 |
实战:用令牌桶限制直播间弹幕发送
直播间的弹幕如果不限制,一个热门直播可能每秒几万条弹幕,服务器肯定扛不住。
/**
* 直播弹幕限流器
* 使用令牌桶算法,控制每个用户和每个直播间的弹幕发送频率
*/
public class LiveBarrageLimiter {
// 每个用户每秒最多发5条弹幕
private final long userRatePerSecond = 5;
// 直播间每秒最多处理2000条弹幕
private final long roomRatePerSecond = 2000;
// 用户级别限流器缓存
private final LoadingCache<Long, TokenBucket> userLimiters;
// 直播间级别限流器缓存
private final LoadingCache<Long, TokenBucket> roomLimiters;
public LiveBarrageLimiter() {
this.userLimiters = CacheBuilder.newBuilder()
.expireAfterAccess(5, TimeUnit.MINUTES)
.build(new CacheLoader<Long, TokenBucket>() {
@Override
public TokenBucket load(Long userId) {
return new TokenBucket(userRatePerSecond * 2, userRatePerSecond);
}
});
this.roomLimiters = CacheBuilder.newBuilder()
.expireAfterAccess(10, TimeUnit.MINUTES)
.build(new CacheLoader<Long, TokenBucket>() {
@Override
public TokenBucket load(Long roomId) {
return new TokenBucket(roomRatePerSecond * 2, roomRatePerSecond);
}
});
}
/**
* 判断弹幕是否可以发送
*/
public boolean canSendBarrage(Long userId, Long roomId) {
try {
// 先检查用户级别限流
TokenBucket userBucket = userLimiters.get(userId);
if (!userBucket.tryAcquire()) {
log.info("用户{}发送弹幕过于频繁", userId);
return false;
}
// 再检查直播间级别限流
TokenBucket roomBucket = roomLimiters.get(roomId);
if (!roomBucket.tryAcquire()) {
log.info("直播间{}弹幕过于火爆,请稍后再发", roomId);
// 用户的令牌要还回去
userBucket.release();
return false;
}
return true;
} catch (Exception e) {
log.error("限流器异常", e);
// 限流器异常时,默认放行(根据业务需求决定)
return true;
}
}
}
/**
* 简易令牌桶实现
*/
public class TokenBucket {
private final long capacity; // 桶容量
private final long refillPerSecond; // 每秒填充数量
private final AtomicLong tokens; // 当前令牌数
private volatile long lastRefillTime; // 上次填充时间
public TokenBucket(long capacity, long refillPerSecond) {
this.capacity = capacity;
this.refillPerSecond = refillPerSecond;
this.tokens = new AtomicLong(capacity);
this.lastRefillTime = System.currentTimeMillis();
}
public synchronized boolean tryAcquire() {
refill();
if (tokens.get() > 0) {
tokens.decrementAndGet();
return true;
}
return false;
}
public void release() {
long current = tokens.get();
if (current < capacity) {
tokens.incrementAndGet();
}
}
private void refill() {
long now = System.currentTimeMillis();
long elapsed = now - lastRefillTime;
if (elapsed > 100) { // 每100ms填充一次
long tokensToAdd = elapsed * refillPerSecond / 1000;
if (tokensToAdd > 0) {
long newTokens = Math.min(capacity, tokens.get() + tokensToAdd);
tokens.set(newTokens);
lastRefillTime = now;
}
}
}
}熔断:隔离故障服务
熔断的思路很简单:与其让一个有问题的服务拖垮整个系统,不如直接切断对它的调用。
就像家里的保险丝,电流过大时自动断开,保护整个电路不被烧毁。
熔断器的三种状态:
stateDiagram-v2
[*] --> 关闭
关闭 --> 开启 : 错误率超过阈值
开启 --> 半开 : 等待冷却时间
半开 --> 关闭 : 探测成功
半开 --> 开启 : 探测失败
note right of 关闭
正常状态
记录成功/失败次数
end note
note right of 开启
熔断状态
请求直接失败
不调用下游
end note
note right of 半开
试探状态
放行少量请求
检测服务是否恢复
end note实战:音乐App的歌词服务熔断
用户听歌时需要展示歌词,歌词服务挂了不能影响听歌。
@Service
public class MusicPlayerService {
private final LyricsServiceClient lyricsClient;
private final CircuitBreaker lyricsCircuitBreaker;
public MusicPlayerService(LyricsServiceClient lyricsClient) {
this.lyricsClient = lyricsClient;
// 配置熔断器
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50) // 失败率超过50%触发熔断
.slowCallRateThreshold(80) // 慢调用超过80%也触发
.slowCallDurationThreshold(Duration.ofMillis(500)) // 超过500ms算慢调用
.waitDurationInOpenState(Duration.ofSeconds(30)) // 熔断后等待30秒
.permittedNumberOfCallsInHalfOpenState(5) // 半开状态放5个请求试探
.slidingWindowSize(20) // 统计窗口20个请求
.build();
this.lyricsCircuitBreaker = CircuitBreaker.of("lyrics-service", config);
}
/**
* 获取歌曲信息(包含歌词)
*/
public SongDetailVO getSongDetail(Long songId) {
// 1. 获取基础歌曲信息(核心功能)
Song song = songRepository.findById(songId)
.orElseThrow(() -> new SongNotFoundException(songId));
SongDetailVO detail = new SongDetailVO();
detail.setSongId(song.getId());
detail.setName(song.getName());
detail.setArtist(song.getArtist());
detail.setPlayUrl(song.getPlayUrl());
// 2. 获取歌词(非核心功能,带熔断保护)
String lyrics = fetchLyricsWithCircuitBreaker(songId);
detail.setLyrics(lyrics);
return detail;
}
private String fetchLyricsWithCircuitBreaker(Long songId) {
try {
return lyricsCircuitBreaker.executeSupplier(() -> {
// 调用歌词服务
return lyricsClient.getLyrics(songId);
});
} catch (CallNotPermittedException e) {
// 熔断器开启,直接返回降级内容
log.warn("歌词服务熔断中,songId={}", songId);
return "歌词加载中...";
} catch (Exception e) {
// 其他异常,也返回降级内容
log.error("获取歌词失败,songId={}", songId, e);
return "暂无歌词";
}
}
}降级:保核心弃边缘
降级的核心思想是:系统资源有限时,优先保障核心功能,非核心功能可以暂时关闭或简化。
就像电力紧张时,先保住医院、学校的供电,娱乐场所可以暂时停电。
降级的常见场景:
| 场景 | 降级策略 | 示例 |
|---|---|---|
| 大促期间 | 关闭非核心功能 | 关闭个性化推荐,只展示热门商品 |
| 服务异常 | 返回兜底数据 | 评分服务挂了,显示默认4.5分 |
| 超时严重 | 简化数据查询 | 只返回基本信息,不查询详细数据 |
| 资源不足 | 降低数据精度 | 库存显示"有货"而不是精确数量 |
实战:电商大促时的智能降级
@Service
public class ProductDetailService {
@Autowired
private ProductRepository productRepository;
@Autowired
private ReviewService reviewService;
@Autowired
private RecommendService recommendService;
@Autowired
private DegradeConfigService degradeConfig;
/**
* 获取商品详情(带智能降级)
*/
public ProductDetailVO getProductDetail(Long productId) {
// 核心数据:商品基础信息(必须返回)
Product product = productRepository.findById(productId)
.orElseThrow(() -> new ProductNotFoundException(productId));
ProductDetailVO detail = convertToVO(product);
// 降级级别1:大促期间关闭评价详情
if (degradeConfig.isEnabled("review-detail")) {
detail.setReviews(Collections.emptyList());
detail.setReviewSummary("大促期间暂不显示评价详情");
} else {
detail.setReviews(fetchReviewsSafe(productId));
}
// 降级级别2:关闭个性化推荐,使用静态热门推荐
if (degradeConfig.isEnabled("personalized-recommend")) {
detail.setRecommendProducts(getStaticHotProducts());
} else {
detail.setRecommendProducts(fetchRecommendSafe(productId));
}
// 降级级别3:库存显示简化
if (degradeConfig.isEnabled("inventory-simplify")) {
// 只显示有货/无货,不显示具体数量
int stock = product.getStock();
detail.setStockText(stock > 0 ? "有货" : "暂时缺货");
detail.setStockQuantity(null); // 不返回具体数量
} else {
detail.setStockQuantity(product.getStock());
}
return detail;
}
/**
* 安全获取评价(带降级)
*/
private List<ReviewVO> fetchReviewsSafe(Long productId) {
try {
return reviewService.getTopReviews(productId, 10);
} catch (Exception e) {
log.warn("获取评价失败,productId={},使用降级策略", productId);
// 降级:返回空列表
return Collections.emptyList();
}
}
/**
* 安全获取推荐(带降级)
*/
private List<ProductVO> fetchRecommendSafe(Long productId) {
try {
return recommendService.getRelatedProducts(productId, 6);
} catch (Exception e) {
log.warn("获取推荐失败,productId={},使用热门商品", productId);
// 降级:返回静态热门商品
return getStaticHotProducts();
}
}
/**
* 获取静态热门商品(缓存的,不会失败)
*/
private List<ProductVO> getStaticHotProducts() {
// 从本地缓存获取热门商品列表
return hotProductCache.get("daily-hot", () -> {
// 缓存不存在时返回硬编码的兜底数据
return Arrays.asList(
new ProductVO(1001L, "iPhone 15", "热卖中"),
new ProductVO(1002L, "iPad Pro", "好评如潮"),
new ProductVO(1003L, "MacBook Air", "轻薄首选")
);
});
}
}超时控制和重试策略
很多雪崩事故的根因就是:没设超时或者超时设置不合理。
一个请求等5秒没响应还在傻傻等着,线程资源就这样被白白占用。
超时设置的基本原则:
- 必须设置超时:任何外部调用都要设超时
- 超时要合理:太长起不到保护作用,太短会误杀正常请求
- 分层设置:网关超时 > 服务超时 > 数据库超时
重试策略的注意事项:
graph TB
subgraph 重试策略
A[可重试的场景<br/>网络抖动、超时、5xx错误]
B[不可重试的场景<br/>参数错误、业务异常、幂等性问题]
C[重试次数<br/>建议2-3次]
D[重试间隔<br/>指数退避,避免重试风暴]
end
style A fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,rx:10,ry:10
style B fill:#E57373,stroke:#C62828,stroke-width:2px,rx:10,ry:10
style C fill:#2196F3,stroke:#1565C0,stroke-width:2px,rx:10,ry:10
style D fill:#FF9800,stroke:#E65100,stroke-width:2px,rx:10,ry:10实战:外卖骑手位置上报的重试策略
@Service
public class RiderLocationService {
private final LocationStorageClient storageClient;
private final RetryTemplate retryTemplate;
public RiderLocationService(LocationStorageClient storageClient) {
this.storageClient = storageClient;
// 配置重试策略
this.retryTemplate = RetryTemplate.builder()
.maxAttempts(3) // 最多重试3次
.exponentialBackoff(100, 2, 1000) // 指数退避:100ms, 200ms, 400ms
.retryOn(IOException.class) // 只对IO异常重试
.retryOn(TimeoutException.class) // 超时也重试
.build();
}
/**
* 上报骑手位置(带重试)
*/
public void reportLocation(Long riderId, double lat, double lng) {
RiderLocation location = new RiderLocation();
location.setRiderId(riderId);
location.setLatitude(lat);
location.setLongitude(lng);
location.setReportTime(System.currentTimeMillis());
try {
retryTemplate.execute(context -> {
int attempt = context.getRetryCount() + 1;
if (attempt > 1) {
log.info("位置上报重试第{}次,riderId={}", attempt, riderId);
}
// 设置超时时间500ms
storageClient.saveLocation(location, Duration.ofMillis(500));
return null;
});
} catch (Exception e) {
// 重试都失败了,记录到本地,后续补偿
log.error("位置上报失败,riderId={},暂存本地", riderId, e);
localBuffer.add(location);
}
}
}横向扩容:提升处理能力
前面讲的都是"防守"策略,横向扩容则是"进攻"策略——直接增加处理能力。
graph LR
subgraph 扩容前
A1[服务实例1<br/>处理1000QPS]
end
subgraph 扩容后
B1[服务实例1<br/>处理1000QPS]
B2[服务实例2<br/>处理1000QPS]
B3[服务实例3<br/>处理1000QPS]
end
A1 -->|发现过载| LB[负载均衡]
LB --> B1
LB --> B2
LB --> B3
style A1 fill:#E57373,stroke:#C62828,stroke-width:2px,rx:10,ry:10
style B1 fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,rx:10,ry:10
style B2 fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,rx:10,ry:10
style B3 fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,rx:10,ry:10
style LB fill:#2196F3,stroke:#1565C0,stroke-width:2px,rx:10,ry:10扩容的几种方式:
| 方式 | 说明 | 适用场景 |
|---|---|---|
| 手动扩容 | 运维人员手动增加实例 | 可预见的活动,如双11 |
| 定时扩容 | 提前配置扩容计划 | 有规律的流量高峰,如午餐时间 |
| 自动扩容 | 根据指标自动增减实例 | 流量波动大的场景 |
Kubernetes自动扩容配置示例:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: order-service-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: order-service
minReplicas: 3
maxReplicas: 20
metrics:
# CPU使用率超过70%触发扩容
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
# 也可以根据自定义指标扩容
- type: Pods
pods:
metric:
name: http_requests_per_second
target:
type: AverageValue
averageValue: "1000"从源头把关:代码质量
说了这么多防护手段,其实最重要的还是从源头把控——写好代码,做好测试。
很多故障不是流量太大导致的,而是代码本身有问题。
常见的代码坑
1. 内存泄漏
// 错误示例:忘记关闭资源
public void processFile(String path) {
FileInputStream fis = new FileInputStream(path);
// 处理文件...
// 忘记close,每次调用都泄漏一点资源
}
// 正确示例:使用try-with-resources
public void processFile(String path) {
try (FileInputStream fis = new FileInputStream(path)) {
// 处理文件...
} // 自动关闭
}2. 连接池耗尽
// 错误示例:慢查询不设超时
public List<Order> queryOrders(Long userId) {
// 这个查询可能很慢,一直占用连接
return jdbcTemplate.query(
"SELECT * FROM orders WHERE user_id = ? ORDER BY create_time DESC",
new Object[]{userId},
orderRowMapper
);
}
// 正确示例:限制查询时间和结果数量
public List<Order> queryOrders(Long userId) {
// 限制只查最近100条,避免全表扫描
return jdbcTemplate.query(
"SELECT * FROM orders WHERE user_id = ? ORDER BY create_time DESC LIMIT 100",
new Object[]{userId},
orderRowMapper
);
}3. 死循环和递归爆栈
// 错误示例:递归没有终止条件
public int calculate(int n) {
return calculate(n - 1) + calculate(n - 2);
// n可能是负数,永远不会停
}
// 正确示例:有明确的终止条件
public int calculate(int n) {
if (n <= 0) return 0;
if (n == 1 || n == 2) return 1;
return calculate(n - 1) + calculate(n - 2);
}提升代码质量的工具
| 工具 | 作用 | 使用建议 |
|---|---|---|
| SonarQube | 代码静态分析 | 集成到CI流程,每次提交都检查 |
| Arthas | 线上问题诊断 | 排查线上问题的神器 |
| JProfiler | 性能分析 | 找出内存泄漏和性能瓶颈 |
| IDEA自带检查 | 代码质量提示 | 开发时就能发现问题 |
集群和冗余设计
单点故障是高可用的大敌。只要是关键服务,都应该做集群部署。
无状态服务集群
对于无状态服务(大部分业务服务都是),集群化很简单,直接多部署几个实例就行:
graph TB
LB[负载均衡]
subgraph 服务集群
S1[实例1<br/>192.168.1.10]
S2[实例2<br/>192.168.1.11]
S3[实例3<br/>192.168.1.12]
end
LB --> S1
LB --> S2
LB --> S3
style LB fill:#2196F3,stroke:#1565C0,stroke-width:2px,rx:10,ry:10
style S1 fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,rx:10,ry:10
style S2 fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,rx:10,ry:10
style S3 fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,rx:10,ry:10有状态服务的高可用
数据库、缓存这些有状态的服务,需要更复杂的高可用方案:
MySQL主从复制:
graph LR
App[应用] -->|写| Master[主库]
App -->|读| Slave1[从库1]
App -->|读| Slave2[从库2]
Master -->|同步| Slave1
Master -->|同步| Slave2
style App fill:#9C27B0,stroke:#6A1B9A,stroke-width:2px,rx:10,ry:10
style Master fill:#E57373,stroke:#C62828,stroke-width:2px,rx:10,ry:10
style Slave1 fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,rx:10,ry:10
style Slave2 fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,rx:10,ry:10Redis集群模式:
graph TB
subgraph Redis Cluster
M1[Master1<br/>槽0-5460] --- S1[Slave1]
M2[Master2<br/>槽5461-10922] --- S2[Slave2]
M3[Master3<br/>槽10923-16383] --- S3[Slave3]
end
style M1 fill:#E57373,stroke:#C62828,stroke-width:2px,rx:10,ry:10
style M2 fill:#E57373,stroke:#C62828,stroke-width:2px,rx:10,ry:10
style M3 fill:#E57373,stroke:#C62828,stroke-width:2px,rx:10,ry:10
style S1 fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,rx:10,ry:10
style S2 fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,rx:10,ry:10
style S3 fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,rx:10,ry:10监控和告警
再好的设计也需要监控来兜底。出问题时能第一时间发现,比什么都重要。
监控的几个层次
| 层次 | 监控内容 | 常用工具 |
|---|---|---|
| 基础设施 | CPU、内存、磁盘、网络 | Prometheus + Node Exporter |
| 中间件 | MySQL、Redis、MQ状态 | 各中间件自带监控 |
| 应用层 | QPS、响应时间、错误率 | Prometheus + Micrometer |
| 业务层 | 订单量、支付成功率等 | 自定义埋点 |
告警设置建议
分级告警:
| 级别 | 触发条件 | 通知方式 |
|---|---|---|
| P0-紧急 | 核心服务不可用 | 电话 + 短信 + 钉钉 |
| P1-重要 | 错误率>5%或响应时间>2s | 短信 + 钉钉 |
| P2-一般 | 错误率>1%或响应时间>1s | 钉钉 |
| P3-提醒 | 资源使用率>80% | 邮件 |
总结
设计一个高可用系统,说难也难,说简单也简单。核心就是这几点:
- 认识到系统会出问题:不要幻想系统永远不出问题,而是假设它一定会出问题,提前做好准备。
- 分层防护:限流、熔断、降级、超时控制,这些手段要组合使用,形成多层防护网。
- 消灭单点:关键服务做集群,数据做备份,任何单点故障都不应该让整个系统挂掉。
- 重视代码质量:很多故障的根因是代码问题,CodeReview和代码扫描不能省。
- 完善监控告警:出问题第一时间能发现,比什么都重要。
- 定期演练:纸上谈兵没用,要定期做故障演练,验证各种防护手段真的有效。
最后说一句,高可用不是一蹴而就的,是随着系统演进不断完善的。先把基本的做好,再逐步加强,比一开始就追求完美更现实。
更新: 2025-12-30 15:53:19
原文: https://www.yuque.com/u22210564/zoxfmt/zmpgtzmpk4773y8i