Skip to content

如何合理的设计高可用的系统

从一次线上故障说起

先讲个真实发生的事儿。

某天晚上8点,某视频网站的运营同学搞了个"限时免费看大片"活动,本来预估有个几万人参与就不错了。结果活动一开始,流量直接飙了10倍,用户拼命刷页面,评论服务先扛不住了,响应变慢。

问题来了,视频详情页要调用评论服务获取热门评论,评论服务慢了,详情页的请求线程就一直阻塞等着。等着等着,详情服务的线程池也满了,然后推荐服务调详情服务也开始超时...

不到5分钟,整个首页都打不开了。值班同学手忙脚乱,最后只能紧急扩容+重启服务,折腾了半个多小时才恢复。事后复盘,这就是典型的"雪崩效应"。

这种场景在分布式系统里太常见了,今天咱们就来好好聊聊怎么设计一个高可用系统,以及怎么防止雪崩。

高可用到底是个啥?

简单理解

高可用说白了就是:系统在绝大部分时间都能正常提供服务,即使出了问题也能快速恢复

咱们平时说的"几个9"就是用来衡量可用性的:

mermaid
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,用户下单时需要经过这些服务:

mermaid
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秒才返回,订单服务的请求线程就一直阻塞着。新的请求不断进来,线程池很快就满了。

mermaid
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:10

T+30秒:网关层开始报警

订单服务处理不过来,网关那边积压了大量请求。用户端看到的就是转圈圈,过一会儿提示"网络异常请重试"。

T+60秒:用户疯狂重试,流量翻倍

用户发现下单失败,第一反应就是重试。一个用户重试3次,流量直接翻了3倍。本来已经过载的系统,这下彻底扛不住了。

T+120秒:多个服务陆续崩溃

商家服务也要调用订单服务查询订单状态,订单服务没响应,商家服务的线程也开始堆积。连锁反应下,半个系统都瘫痪了。

mermaid
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

这就是雪崩,一个点的故障,通过服务调用链快速扩散,最终导致整个系统不可用。

防护手段全景图

好,问题分析清楚了,接下来聊聊怎么防。主要就这几板斧:

mermaid
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

限流:控制流量入口

限流就像景区限制入园人数一样,系统能处理多少请求是有上限的,超过这个上限,宁可拒绝也不要把系统搞崩。

常用的限流算法:

算法原理优点缺点适用场景
固定窗口每个时间窗口内限制请求数实现简单窗口边界可能突发双倍流量粗粒度限流
滑动窗口平滑的时间窗口解决边界问题实现稍复杂一般场景
漏桶算法固定速率处理请求流量平滑无法应对突发流量需要严格控速
令牌桶算法按速率生成令牌,请求需获取令牌允许一定突发实现相对复杂大部分场景

实战:用令牌桶限制直播间弹幕发送

直播间的弹幕如果不限制,一个热门直播可能每秒几万条弹幕,服务器肯定扛不住。

java
/**
 * 直播弹幕限流器
 * 使用令牌桶算法,控制每个用户和每个直播间的弹幕发送频率
 */
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;
            }
        }
    }
}

熔断:隔离故障服务

熔断的思路很简单:与其让一个有问题的服务拖垮整个系统,不如直接切断对它的调用

就像家里的保险丝,电流过大时自动断开,保护整个电路不被烧毁。

熔断器的三种状态:

mermaid
stateDiagram-v2
    [*] --> 关闭
    关闭 --> 开启 : 错误率超过阈值
    开启 --> 半开 : 等待冷却时间
    半开 --> 关闭 : 探测成功
    半开 --> 开启 : 探测失败
    
    note right of 关闭
        正常状态
        记录成功/失败次数
    end note
    
    note right of 开启
        熔断状态
        请求直接失败
        不调用下游
    end note
    
    note right of 半开
        试探状态
        放行少量请求
        检测服务是否恢复
    end note

实战:音乐App的歌词服务熔断

用户听歌时需要展示歌词,歌词服务挂了不能影响听歌。

java
@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分
超时严重简化数据查询只返回基本信息,不查询详细数据
资源不足降低数据精度库存显示"有货"而不是精确数量

实战:电商大促时的智能降级

java
@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秒没响应还在傻傻等着,线程资源就这样被白白占用。

超时设置的基本原则:

  1. 必须设置超时:任何外部调用都要设超时
  2. 超时要合理:太长起不到保护作用,太短会误杀正常请求
  3. 分层设置:网关超时 > 服务超时 > 数据库超时

重试策略的注意事项:

mermaid
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

实战:外卖骑手位置上报的重试策略

java
@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);
        }
    }
}

横向扩容:提升处理能力

前面讲的都是"防守"策略,横向扩容则是"进攻"策略——直接增加处理能力

mermaid
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自动扩容配置示例:

yaml
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. 内存泄漏

java
// 错误示例:忘记关闭资源
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. 连接池耗尽

java
// 错误示例:慢查询不设超时
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. 死循环和递归爆栈

java
// 错误示例:递归没有终止条件
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自带检查代码质量提示开发时就能发现问题

集群和冗余设计

单点故障是高可用的大敌。只要是关键服务,都应该做集群部署。

无状态服务集群

对于无状态服务(大部分业务服务都是),集群化很简单,直接多部署几个实例就行:

mermaid
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主从复制:

mermaid
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:10

Redis集群模式:

mermaid
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%邮件

总结

设计一个高可用系统,说难也难,说简单也简单。核心就是这几点:

  1. 认识到系统会出问题:不要幻想系统永远不出问题,而是假设它一定会出问题,提前做好准备。
  2. 分层防护:限流、熔断、降级、超时控制,这些手段要组合使用,形成多层防护网。
  3. 消灭单点:关键服务做集群,数据做备份,任何单点故障都不应该让整个系统挂掉。
  4. 重视代码质量:很多故障的根因是代码问题,CodeReview和代码扫描不能省。
  5. 完善监控告警:出问题第一时间能发现,比什么都重要。
  6. 定期演练:纸上谈兵没用,要定期做故障演练,验证各种防护手段真的有效。

最后说一句,高可用不是一蹴而就的,是随着系统演进不断完善的。先把基本的做好,再逐步加强,比一开始就追求完美更现实。

更新: 2025-12-30 15:53:19
原文: https://www.yuque.com/u22210564/zoxfmt/zmpgtzmpk4773y8i

Java 后端面试知识库