博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
基于 Hystrix 高并发服务限流第 1 篇 —— 必须了解的相关概念
阅读量:2094 次
发布时间:2019-04-29

本文共 8385 字,大约阅读时间需要 27 分钟。

查看之前的博客可以点击顶部的【分类专栏】

 

Are you ready?

 

什么是高并发、高可用、高可靠、分布式、微服务架构?

高并发(HC,High Concurrency):

高并发是指在同一个时间点,有很多用户同时的访问同一 API 接口或者 Url 地址。它经常会发生在有大活跃的用户量,用户高聚集的业务场景中(比如秒杀、抢票)。

 

高可用(HA,High Availability):

在高并发的情况下,网站仍然可以不间断的提供服务。

 

高可靠(HR,High Reliability)

在信息技术领域,高可靠性指的是运行时间能够满足预计时间的一个系统或组件。可靠性可以用“100%可操作性”或者“从未失败”这两种标准来表示。一个被广泛应用但却难以达到的标准是著名的“5个9标准”,就是说工作的可靠性要达到99.999%。

 

分布式:

根据业务需求进行拆分成N个子系统,多个子系统相互协作才能完成业务流程,子系统之间通讯使用RPC远程通讯技术。

 

微服务架构:

微服务是指开发一个单个、小型的但有业务的服务,每个服务都有自己的处理和轻通讯机制,可以部署在单个服务器上,让专业的人做专业的事情。

 

解决高并发的设计原则是什么?

系统设计不仅需要考虑实现业务功能,还要保证系统高并发、高可用、高可靠等。同时还应考虑系统容量规划(流量、容量等)、SLA制定(吞吐量、响应时间、可用性、降级方案等)、监控报警(机器负载、响应时间、可用率等)、应急预案(容灾、降级、限流、隔离、切流量、可回滚版本等)。

 

解决高并发有什么技术可以考虑?

集群、负载均衡、降级、限流、熔断、缓存、异步并发、连接池、线程池、扩容、消息队列、分布式任务、主从复制、读写分离、防幂等、请求令牌、接口验签、数据备份、前端图形验证(防机器攻击)等。

比如:

1、通过负载均衡和反向代理实现分流(Nginx 负载均衡,恶意 IP 使用 Nginx Deny 策略或者 iptables 拒绝)

2、通过限流保护服务免受雪崩之灾。

3、通过降级实现部分可用、有损服务。

4、通过隔离实现故障隔离。

5、通过合理设置的超时与重试机制避免请求堆积造成雪崩。

6、通过回滚机制快速修复错误版本。

 

OK,本系列博客只着重讲【降级、限流、熔断】。

 

什么是服务雪崩?

在分布式的微服务架构中,系统往往被拆分成很多个服务单元,每个服务单元部署在不同的机器或者不同的进程中,通过远程调用的方式进行通讯。这样一来,就有可能因为网络故障等原因导致服务调用异常或延迟。如果此时调用方的请求不断增加,最后因为服务的请求形成积压、阻塞,结果导致服务瘫痪,宕机,俗称“服务雪崩”。(就像堆木积,不断的往上堆积,造成底部的压力增大,容易导致崩塌。)

 

Hystrix 如何防止服务雪崩?

1、请求缓存

如果某一个接口返回的数据都相同,在高并发中,可以把这个接口返回的数据进行缓存,以便能快速的响应。

 

2、请求合并

在微服务架构中,我们将一个项目拆分成多个独立的子模块,这些独立的模块通过远程调用来互相通讯,在高并发情况下,通信次数的增加会导致总的通信时间增加。同时,线程池的资源是有限的,高并发环境会导致大量的线程处于等待状态,进而导致响应延迟。为了解决这个问题,我们使用 Hystrix 的请求合并来解决。(就像过安检一样,凑够一撮人,就放行)。请求合并主要是解决请求多次在网络传输的问题:减少网络的多次传输,不再讲解此方法,用得少,还麻烦。

 

3、服务隔离

没有线程池隔离的项目所有接口默认都运行在一个同一个线程池中,当某个接口的请求压力过大,达到服务的线程池(默认是 tomcat)的最大限制,其它接口的调用一直无法获取到线程资源导致请求大量堆积,发生服务雪崩效应。为防止服务雪崩,可以使用服务隔离机制(线程池方式或信号量),让每个服务或者每个接口都有自己独立的线程池,解决雪崩效应。

 

4、服务熔断

在高并发下,如果请求达到一定极限(可以自己设置阔值)如果流量超出了设置阈值,为了避免请求的堆积造成服务的瘫痪,可以直接拒绝服务,并且配合服务降级方式返回一个友好提示(托底数据),以保护当前服务以免宕机。

 

5、服务降级

在高并发情况下,当某个服务不可用,使用 fallback 方法直接返回一个友好的错误提示(托底数据),防止用户一直等待,提高用户的体验感。

 

6、服务限流

服务限流就是对接口访问进行限制,常用服务限流算法令牌桶、漏桶。计数器也可以进行粗暴限流实现。

 

开始实验

本篇博客代码地址:  提取码:p4yw

OK,我们先看本次案例代码:有3个微服务:一个注册中心(8080端口),一个商品服务(9091端口),一个订单服务(9090端口)。订单服务通过声明式调用 Feign 调用商品服务。

正常的请求如下(都是写固定的测试数据):

订单接口:

多个产品接口:

订单调用产品接口:

 

OK,我们来模拟以下高并发的场景。

在商品服务获取结合的接口,增加线程休眠 2 秒

我们知道,tomcat 默认的并发是200个,我们先把 product-server 的 tomcat 服务器的最大线程数调整为10

同时,还要调整 order-server 微服务的 Feign 的 Hystrix 的超时时间,否则 Hystrix 会一直生效,导致无法模拟超时的效果:注意,这是 SpringBoot2.0+ 的版本的配置方法

feign:  client:    config:      default:        connect-timeout: 8000        read-timeout: 8000

否则报错:

java.net.SocketTimeoutException: Read timed out

    at java.net.SocketInputStream.socketRead0(Native Method) ~[na:1.8.0_91]
    at java.net.SocketInputStream.socketRead(SocketInputStream.java:116) ~[na:1.8.0_91]
 

 

重启2台服务。

请求产品服务集合 getList 的接口,浏览器打开 F12 调试工具,看到响应时间250毫秒左右,其中2秒是我们 sleep 的。

然后我们使用 JMeter  模拟高并发场景。

不懂用 JMeter?查看博客:

我们模拟30个线程,每个线程发送50个请求,并发就是1500。查询产品接口。

然后我们对 product/getList 这个接口发送请求。注意订单微服务调用的是获取单个的接口。

 

点击开始,模拟高并发的请求 getList 接口,这时候,产品微服务的请求压力是很大的。那么我们看下订单微服务去请求产品微服务接口,有什么效果。

时间花费了4秒多。

 

Hystrix 的请求缓存

我们使用 Redis 做缓存,在 pom.xml 中增加 Redis 依赖。注意 SpringBoot 2.0 之后,默认使用 ,在 2.0 之前是使用 Jedis 客户端,Lettuce 和 Jedis 的都是连接Redis Server的客户端程序。Jedis在实现上是直连redis server,多线程环境下非线程安全(即多个线程对一个连接实例操作,是线程不安全的),除非使用连接池,为每个Jedis实例增加物理连接。Lettuce基于Netty的连接实例(StatefulRedisConnection),可以在多个线程间并发访问,且线程安全,满足多线程环境下的并发访问(即多个线程公用一个连接实例,线程安全),同时它是可伸缩的设计,一个连接实例不够的情况也可以按需增加连接实例。

在 order-server 增加 Redis 依赖

org.springframework.boot
spring-boot-starter-data-redis-reactive
org.apache.commons
commons-pool2

配置文件增加 Redis 配置

spring:  application:    name: order-server  # Redis 相关配置  redis:    host: 192.168.0.105    port: 6379    password: 123456    database: 0    timeout: 5000 #连接超时时间    lettuce:      pool:        max-active: 50   #最大连接数,默认是8        max-wait: 1000   #最大连接阻塞数        max-idle: 100    #最大空闲连接,默认是8        min-idle: 5      #最小空闲连接,默认是0      #在关闭客户端连接之前等待任务处理完成的最长时间,在这之后,无论任务是否执行完成,都会被执行器关闭,默认100ms      shutdown-timeout: 5000

我们知道,Redis 的模板对象需要重新配置,Redis 默认使用 JDK 序列化,JDK 序列化的好处就是在反序列化的时候,不需要声明类型。但是数据是 JSON 的五倍大小,非常消耗 Redis 的内存。因此我们需要把它的序列化重写,不使用 JDK 默认的序列化。

package com.study.config;import org.apache.commons.pool2.impl.GenericObjectPoolConfig;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.connection.RedisPassword;import org.springframework.data.redis.connection.RedisStandaloneConfiguration;import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;import org.springframework.data.redis.serializer.StringRedisSerializer;import java.time.Duration;/** * @author biandan * @description * @signature 让天下没有难写的代码 * @create 2021-06-20 上午 12:43 */@Configurationpublic class RedisConfig {    @Value("${spring.redis.database}")    private Integer database;    @Value("${spring.redis.host}")    private String host;    @Value("${spring.redis.port}")    private Integer port;    @Value("${spring.redis.password}")    private String password;    @Value("${spring.redis.timeout}")    private Long timeout;    @Value("${spring.redis.lettuce.shutdown-timeout}")    private Long shutDownTimeout;    //最大连接数    @Value("${spring.redis.lettuce.pool.max-active}")    private Integer maxActive;    //最大连接阻塞数    @Value("${spring.redis.lettuce.pool.max-wait}")    private Long maxWait;    //最大空闲连接    @Value("${spring.redis.lettuce.pool.max-idle}")    private Integer maxIdle;    //最小空闲连接    @Value("${spring.redis.lettuce.pool.min-idle}")    private Integer minIdle;    // Redis连接工厂    @Bean    public LettuceConnectionFactory lettuceConnectionFactory() {        //Redis 基本连接        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();        redisStandaloneConfiguration.setDatabase(database);        redisStandaloneConfiguration.setHostName(host);        redisStandaloneConfiguration.setPort(port);        redisStandaloneConfiguration.setPassword(RedisPassword.of(password));        //lettuce 连接池配置        GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();        genericObjectPoolConfig.setMaxIdle(maxIdle);        genericObjectPoolConfig.setMinIdle(minIdle);        genericObjectPoolConfig.setMaxTotal(maxActive);        genericObjectPoolConfig.setMaxWaitMillis(maxWait);        genericObjectPoolConfig.setTimeBetweenEvictionRunsMillis(100);        LettuceClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder()                .commandTimeout(Duration.ofMillis(timeout))                .shutdownTimeout(Duration.ofMillis(shutDownTimeout))                .poolConfig(genericObjectPoolConfig)                .build();        LettuceConnectionFactory factory = new LettuceConnectionFactory(redisStandaloneConfiguration, clientConfig);        return factory;    }    @Bean    public RedisTemplate
redisTemplate() { RedisTemplate
template = new RedisTemplate<>(); //为String类型的key设置序列化器 template.setKeySerializer(new StringRedisSerializer()); //为String类型value设置通用的序列化器 template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); //为Hash类型key设置序列化器 template.setHashKeySerializer(new StringRedisSerializer()); //为Hash类型value设置通用的序列化器 template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); template.setConnectionFactory(lettuceConnectionFactory()); return template; }}

然后在 order-server 启动类开启缓存

@EnableCaching //开启缓存注解

然后在获取 product-server 的产品接口上,增加以下代码:

@Cacheable(cacheNames = "order:product",key = "#id")

如图:

注意:

使用 @Cacheable 注解的方法,不能跟调用其方法在相同的类里,否则缓存无效。比如本案例中调用 getProductById 是在 OrderServiceImpl 类里。

 

然后重启 order-server 服务,还要启动我们的 Redis 环境。如果你们没有 Redis 环境,直接往下看就行了。

启动后,直接访问订单微服务,让数据先缓存到 Redis。请求:

因为要访问产品服务,还要存入 Redis,因此时间久一点。在产品服务里有打印:

然后查看 Redis:

 

然后,我们继续刷新访问:

第二次访问,直接从 Redis 读取缓存,速度非常快。

 

我们开启 JMeter 模拟高并发,然后再次访问订单-产品的接口:

说明产品微服务处于高并发下,并没有影响订单微服务获取相同 API 接口的数据,因为已经缓存到 Redis 了,直接从 Redis 缓存里读取数据,不需要跨服务调用数据。这就是请求缓存的案例。

 

OK,这篇博客就把相关概念讲解到这,以及讲解请求缓存这个案例,Hystrix 还有很多重要的特性解决高并发的问题。欲知后事如何,请听下回分解。

 

本篇博客代码地址:  提取码:p4yw

 

 

转载地址:http://wkuhf.baihongyu.com/

你可能感兴趣的文章