高可用是指,系统在面对各种挑战时,如硬件故障、软件缺陷、网络问题等,依然能够持续提供服务。高可用通常用“五个九”(99.999%的可用性)这样的术语来衡量,意味着系统一年中的停机时间只有几分钟。
分布式系统的高可用架构设计如图所示:
- 用户请求:所有用户的请求都首先到达负载均衡器。
- 负载均衡器:它是高可用架构的关键组成部分。它接收来自用户的请求,并将其均匀地分配给多个服务器,如服务器A和服务器B。这样做的目的是防止任何单个服务器过载并提高资源利用率。负载均衡器还负责进行健康检查,以确保所有流量仅被转发到健康的服务器。
- 服务器A和服务器B:代表应用服务器的集群,每个服务器都能独立处理请求。这些服务器是冗余的,如果一个服务器失败,则负载均衡器可以将流量重定向到其他健康的服务器。
- 数据库A和数据库B:代表数据层的冗余,确保数据的持久性和可用性。通过主从复制或其他同步机制,可以在一个数据库发生故障时快速切换到另一个数据库,以维护服务的持续性。
- 监控系统:负载均衡器也可以连接一个监控系统,用来监控整个架构的健康状态,包括服务器和数据库的性能指标。监控系统可以自动响应某些事件,如在检测到服务器故障时,自动从服务器池中剔除故障服务器,或者在资源使用率达到某个阈值时触发扩容。
图片
【实战1】在高并发场景下,使用“限流”防止系统崩溃
在设计高可用架构时,除需要考虑冗余和备份机制外,还需要实施一系列服务治理策略,来确保系统在面对异常流量、服务依赖故障等特殊情况时的稳定性和弹性。这些策略包括限流、降级、熔断和隔离等,它们有助于系统在压力下保持核心功能,避免全面崩溃。
是电商系统中常用的一种保护机制,用于控制流入系统的请求速率,防止系统因短时间内请求过多而过载。
限流的常用策略如下。
- 固定窗口计数器:在固定的时间窗口(如每分钟)内,只允许一定数量的请求通过。
- 滑动窗口计数器:在滑动的时间窗口内,计算请求的速率,并动态调整允许的请求数量。
- 令牌桶(Token Bucket):系统以固定速率生成令牌,请求需要消耗令牌才能通过,令牌的生成和消耗类似于漏桶模型。这种方式允许突发流量在短时间内高速传入,但长期速率限制在固定值。
- 优先级队列:对请求进行优先级排序,高优先级的请求先通过限流检查。在电商系统中,可能会对以下几方面实施限流。
- 用户级别的限流:通过用户ID进行识别,为每个用户都设置独立的限流规则,防止单个用户频繁请求,如每秒限制用户下单次数。例如,使用Redis等缓存数据库记录每个用户的请求时间戳,只有当时间间隔满足要求时,才允许新的请求通过。
- 接口级别的限流:对特定的API设置限流规则,如每个API在一定时间内只能处理一定数量的请求。可以使用Sentinel等工具,为每个接口都配置限流规则,并在网关或API控制器中实现限流逻辑。
- 全局级别的限流:对整个系统或服务集群设置总体的流量控制,防止整个系统因流量过大而崩溃。可以在负载均衡器或网关层面实现全局限流,如Nginx的限流模块。
1.接口级别的限流
以访问电商系统中的商品详情页为例,假设我们希望限制每个用户每秒对商品详情页的访问次数不超过5次,则可以通过使用令牌桶算法实现。Spring Cloud Gateway提供了集成令牌桶算法限流的能力,结合Redis使用,可以实现分布式的限流策略。Spring Cloud的代码如下。
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter;
@Configuration
public class GatewayConfig {
// 定义路由规则,指向商品详情服务
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder){
return builder.routes()
.route("product_detail_route", r -> r.path("/product/details/**")
.filters(f -> f.requestRateLimiter().configure(c -> c.setRateLimiter(redisRateLimiter())))
.uri("lb://PRODUCT-SERVICE"))
.build();
}
// 应用限流过滤器,使用Redis实现
@Bean
RedisRateLimiter redisRateLimiter() {
// 创建RedisRateLimiter实例
// 参数分别是允许用户每秒进行5次请求,突发流量大小为10个令牌
return new RedisRateLimiter(5, 10);
}
}
对代码的解析如下。
requestRateLimiter():应用限流过滤器,使用Redis实现。
RedisRateLimiter(5, 10):配置令牌桶参数,表示每秒生成5个令牌,允许的突发流量大小为10个令牌。
通过上述配置,当用户对商品详情页的访问频率超过每秒5次时,额外的请求将被限流策略拦截,从而保护了背后的商品详情服务不会因为过高的访问频率而过载。
提示:在实际部署时,需要考虑Redis的性能和高可用配置,确保限流服务本身不成为系统的瓶颈。
全局级别的限流
在电商系统中,全局限流是一个关键的策略,它能够帮助系统在面对大量用户请求时保持稳定和高效运行。全局限流策略通常涉及多个层面,包括客户端、接入层、应用层、数据层的优化。
图10-2是一个常见的电商系统架构,在该架构的基础上实施全局限流的具体步骤和策略如下。
图片
(1)接入层限流。
接入层限流是对进入系统的请求进行控制的第一道防线,旨在保护后端服务不被突发的高流量击溃。这通常在API网关(位置③)或负载均衡器层面实现南北流量的限流。通过定义流量阈值来限制短时间内处理的请求量,超过阈值的请求会被暂时阻塞或拒绝,以此来确保系统稳定运行并防止服务过载。
(2)读缓存的优化。
①客户端缓存。客户端缓存(位置①)主要针对变化不频繁的数据,如用户的配置信息、商品的静态详情等。通过在客户端(如Web浏览器、移动应用)存储这些数据,可以减少对后端服务的请求次数。
②CDN缓存。CDN(内容分发网络)(位置②)可以缓存静态资源如图片、CSS、JS文件等,使用户能就近访问这些资源。这不仅加速了用户的访问速度,还大大减轻了后端服务器的读取压力。
③Redis缓存。将热点数据(如频繁查询的商品库存信息、用户会话等)缓存到Redis(位置⑦),可以显著提高读取效率,减轻数据库的查询压力。
【实战2】在高并发场景下,使用“熔断”防止服务崩溃
在电商系统中,服务间的依赖关系复杂,一旦某个服务发生故障,很容易引发连锁反应,导致整个系统瘫痪,这就是所谓的“服务雪崩”效应。
以商品详情页服务为例,这个服务在电商系统中非常关键,它需要从多个依赖服务中获取数据,如商品描述服务、商品价格服务、商品评论服务等。如果其中一个服务发生故障,没有熔断保护,则导致用户无法正常访问商品详情页,严重时甚至可能影响整个系统,如图10-3所示。
图片
熔断策略是一种有效的机制,用于防止这种情况的发生。就像电路中的保险丝一样,当电流过大时,保险丝会熔断以保护电路不受损害。在高可用架构中,熔断器也能够在服务出现问题时“断开”,从而保护系统不受进一步损害。
在上述商品详情页服务中添加熔断器后,如图10-4所示。
图片
熔断器主要有如下三种状态。
- 关闭(CLOSED):在正常状态下,所有请求都可以直接调用服务。
- 打开(OPEN):当错误率超过阈值时,熔断器打开。此时,对该服务的调用会立即失败,不会执行实际的业务逻辑。这个状态会持续一段时间,被称为“熔断时间”。
- 半开(HALF-OPEN):熔断时间过后,熔断器进入半开状态。这时,允许有限的请求通过以测试服务是否恢复正常。如果这些请求都成功了,则熔断器回到闭合状态;如果仍有过多的请求失败,则熔断器再次打开。
以下是使用Spring Cloud Hystrix实现的熔断器示例。假设有一个获取商品评论的服务,当评论服务发生故障时,熔断器将自动断开,调用备用方法返回默认评论。
(1)添加Hystrix依赖到项目中(以Maven为例),代码如下。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
(2)在Spring Boot应用主类或配置类上添加@EnableCircuitBreaker注解来启用Hystrix,代码如下。
@SpringBootApplication
@EnableCircuitBreaker
public class ProductServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ProductServiceApplication.class, args);
}
}
(3)创建一个服务类,使用@HystrixCommand注解定义熔断策略,代码如下。
@Service
public class CommentService {
@HystrixCommand(fallbackMethod = "getDefaultComments")
public String getComments(String productId) {
// 模拟从评论服务获取评论的操作,可能会因为服务发生故障而抛出异常
if (new Random().nextInt(10) < 8) { // 假设有80%的概率服务会失败
throw new RuntimeException("评论服务异常");
}
return "正常获取评论数据";
}
// 定义熔断时的备用方法
public String getDefaultComments(String productId) {
return "评论服务当前不可用,请稍后再试";
}
}
在上述代码中,getComments()方法尝试获取商品的评论信息。这里我们通过随机数模拟服务的不稳定性。@HystrixCommand注解指定了当getComments()方法调用失败时,将自动调用getDefaultComments()方法作为备用逻辑,向用户返回一个友好的提示信息。
通过上面的示例,我们可以将这种策略应用到所有的服务调用中,以保证即使在某个服务发生故障时,也能向用户提供最基本的服务,从而提高系统的整体可用性和用户的体验。
此外,在Node.js中可以使用Hystrix.js库来实现熔断策略,代码如下。
const Hystrix = require('hystrix-js');
// 创建一个熔断器,设置错误率阈值为50%
const paymentHystrix = new Hystrix({
metrics: {
healthPercentThresholds: [50]
}
});
// 包装支付服务的调用
function makePaymentRequest() {
return paymentHystrix.wrap(((resolve, reject) => {
// 假设的支付服务调用
payService支付((err, result) => {
if (err) {
reject(err); // 支付失败,触发熔断器
} else {
resolve(result); // 支付成功
}
});
})).then(() => {
// 处理支付成功
}).catch(err => {
// 处理支付失败,可能触发熔断器
console.error('支付失败:', err);
});
}
在这段代码中,Hystrix对象包装了支付服务的调用。如果支付服务的错误率达到了50%,则将触发熔断器,之后的支付请求将会立即失败,从而保护系统不受进一步的损害。
【实战3】在高并发场景下,使用“降级”应对性能瓶颈
在高并发场景下,电商系统经常会遇到性能瓶颈,这可能导致用户体验下降,甚至服务中断。为了应对这种情况,开发团队通常会采用降级策略,有计划地降低系统的部分功能,以确保核心功能的稳定运行。
常用的降级策略如下。
- 服务降级:在后端服务响应慢时,返回缓存的数据或者简化的响应内容。
- 功能降级:暂时关闭或简化一些非核心功能,如商品推荐、促销活动等。
- 用户体验降级:在无法提供完整服务时,只提供基本的服务,如用文本描述代替图片等。
1.服务降级
对于电商系统的后端服务(如商品服务),服务降级的具体实现策略如下。
(1)使用缓存数据。
- 系统预先将热门商品的详情页信息缓存起来,如缓存在Redis或Memcached中。
- 当后端服务响应变慢或不可用时,系统自动切换为返回缓存数据。
- 可以定期更新缓存,以保证信息的相对新鲜度。
(2)简化响应内容。
- 对于一些非核心的信息(如用户评论、推荐商品等),可以在系统负载高时不加载这些内容。
- 提供一个简化版本的商品详情页,只包含最关键的购买信息,如价格、库存和基本描述。这样可以减少网络传输的数据量和前端渲染的复杂度。
- 在某些情况下,可以根据实时的系统负载动态决定是否触发降级处理。例如,如果检测到CPU或内存使用率超过预设阈值,则自动开始返回简化的响应内容。
下面是一段简单的Java代码,展示了如何根据后端服务的状态返回不同的响应。
public class ProductService {
private CacheManager cacheManager;
public ProductService(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
// 获取商品详情
public ProductDetail getProductDetail(String productId) {
try {
// 尝试从数据库中获取最新的商品详情页
ProductDetail productDetail = fetchProductDetailFromDB(productId);
return productDetail;
} catch (Exception e) {
// 如果数据库访问出错或超时,则使用缓存数据
ProductDetail cachedDetail = cacheManager.getCachedProductDetail(productId);
if (cachedDetail != null) {
return cachedDetail; // 返回缓存的商品详情页信息
}
// 如果没有缓存,则返回简化的商品信息
return new ProductDetail(productId, "暂无详细描述,请稍后再试。");
}
}
// 模拟从数据库中获取商品详情页
private ProductDetail fetchProductDetailFromDB(String productId) throws Exception {
// 模拟数据库操作可能的延迟
Thread.sleep(200); // 假设有200ms的延迟
return new ProductDetail(productId, "完整的商品描述。");
}
}
对代码的解析如下。
- ProductService类负责处理商品详情页的获取。
- 如果从数据库中获取数据失败(如抛出异常),则尝试从缓存中获取商品详情页。
- 如果缓存也不存在,则返回一个包含极简信息的ProductDetail对象,提示用户稍后再试。
通过这种方式,即使后端服务由于某些原因而性能下降,用户依然可以得到必要的商品信息,保证了用户体验的连续性和系统的高可用。
2.功能降级
假设电商系统中有一个商品推荐服务,它根据用户的浏览历史和购买历史来推荐商品。虽然这项服务可以增强用户体验,但它并不是完成交易的核心部分。在高流量事件中,如大型促销活动或购物季等,服务器可能会面临极大的压力。
这时就需要对商品推荐服务进行降级处理,处理步骤如下。
(1)定义服务状态控制。
在服务的Java实现中,定义一个静态变量isServiceEnabled,用来标识推荐服务当前是否可用。
(2)服务降级开关。
在服务的Java实现中,提供一个公共方法setServiceStatus(boolean status),允许动态地控制推荐服务的开启或关闭。
(3)服务方法调整。
在推荐方法中,首先检查isServiceEnabled的状态。如果服务被设置为不可用,则进行降级处理,推荐方法将直接返回一个空列表或预设的简单推荐列表,这样可以大幅减少对系统资源的占用。
上述降级处理的具体代码如下。
public class RecommendationService {
// 标志位,用于标识推荐服务是否可用
private static volatile boolean isServiceEnabled = true;
// 设置服务状态的方法,可以被外部调用来开启或关闭服务
public static void setServiceStatus(boolean status) {
isServiceEnabled = status;
}
// 推荐商品的方法
public List<Product> recommendProducts(User user) {
// 检查推荐服务是否被降级(即关闭)
if (!isServiceEnabled) {
// 在推荐服务被关闭时,返回一个空列表或者预设的简单推荐列表
return Collections.emptyList(); // 这里为了简化,直接返回空列表
}
// 正常情况下的推荐逻辑
// 假设有一个复杂的逻辑,根据用户的浏览历史、购买历史等信息生成推荐列表
// ……
return new ArrayList<>(); // 在实际业务中应有复杂逻辑返回推荐商品
}
}
在系统监控工具观察到服务器负载达到临界点时,系统管理员或自动化脚本可以调用setServiceStatus(false)方法来关闭推荐服务,减轻服务器的压力。当系统负载恢复正常后,再次调用setServiceStatus(true)方法来重新开启推荐服务。
3.用户体验降级
假设一个电商网站在特价促销期间遭遇流量激增,服务器负载急剧上升,导致图片服务器响应变慢。为了不影响核心购买流程,网站可以实施用户体验降级策略,即在图片加载困难时用文本描述或图标作为替代,确保页面加载速度和核心交易功能的正常运行。
用户体验降级策略如下。
- 用文本描述代替图片:在图片加载缓慢或失败时,使用文本描述来代替图片,让用户至少知道商品的基本信息。
- 简化页面布局:在前端页面渲染能力受限时,提供一个简化的页面布局,只包含必要的元素和信息。
- 延迟加载:对于非关键的页面元素,如广告和推荐商品,可以采用延迟加载的方式,优先保证核心内容的加载。
例如,在前端代码中实现降级逻辑,确保在后端服务不稳定或网络条件差时,用户仍然能够获取基本信息。前端的代码如下。
// 假设这是一个商品详情页的前端代码片段
function fetchProductDetails(productId) {
fetch(`/api/product/${productId}`)
.then(response => {
if (!response.ok) {
// 如果请求失败,则返回降级内容
return getDegradedProductDetails(productId);
}
return response.json();
})
.then(details => renderProductDetails(details))
.catch(error => {
// 如果请求失败,则返回降级内容
getDegradedProductDetails(productId).then(degradedDetails =>{
renderDegradedProductDetails(degradedDetails);
});
});
}
function getDegradedProductDetails(productId) {
// 这里可以从本地缓存中获取,或者是更简单的数据
return new Promise(resolve => {
resolve({
name: '商品名称',
description: '商品描述',
price: '商品价格'
});
});
}
function renderDegradedProductDetails(degradedDetails) {
// 使用简化的模板或布局渲染降级内容
const container = document.getElementById('product-details');
container.innerHTML = '
<h1>${degradedDetails.name}</h1>
<p>${degradedDetails.description}</p>
<p>价格:$${degradedDetails.price}</p>
';
}
在上述代码中,fetchProductDetails()方法尝试从后端获取完整的商品详情页。如果请求失败,则它会调用getDegradedProductDetails()方法来获取一个简化的商品详情页,并使用 renderDegradedProductDetails()方法来渲染。