微服务如何限制接口调用次数

这种限制接口调用次数的方式,我们通常称之为限流,那么为什么要做限流呢,一般有两种原因:

 

1. 首先是防止服务提供方被大量的请求击垮

我们开发一个项目,最理想的状况是有多少请求,都可以正常地响应,但是在现在的互联网环境,我们很难评估用户的增长,很难评估访问量有多少,甚至有些时候会遇到恶意攻击;那么相比于项目被流量击垮,【限制流量,只满足部分访问的正常响应】要好一些。

简单说就是:满足所有请求 > 满足部分请求 > 项目被击垮,所有请求无法响应。

 

2. 计费

现在很多平台对外开发的接口,并不全是免费的,比如普通会员每天只能调用 1000 次接口,高级会员每天可以调用 10 万次接口,或者按照调用量计费。

 

那么如何限制服务接口的调用次数呢?

使用限流算法

通常我们可以通过限流算法达到限制接口调用次数,比如计数器法、滑动窗口法、漏桶算法、令牌桶算法,这里我们就用令牌桶算法举例。

令牌桶算法,我们可以看做有一个桶,桶里面有 N 个令牌,并且系统会以一个恒定的速度往桶里投放令牌,每次处理之前先要获取令牌,如果获取不到的话,就拒绝服务;在这里我们使用 Google 出品的 Guava 工具库,里面提供了一个开箱即用的令牌桶 RateLimiter。

 

如图,我们编写了一个简单的接口,省略了业务逻辑,只返回一个字符串;我们设置 RateLimiter.create(2),表示每秒不超过 2 个任务被提交。

 

让我们用接口工具模拟一下并发调用:

 

他强任他强,我自巍然不动。因为我们使用了限流算法,每秒只处理 2 个请求,所以从日志中我们可以看到这样的效果:每秒只有两条日志。

 

分布式架构下的限流

因为使用开源的组件,限流的实现看起来非常简单,但是这里也有一个比较大的问题,就是实例中是一个应用包,但在实际的项目中,我们通常会是用集群部署的方式,将我们的应用部署在多台机器上,那么这时候该如何限流呢?

每台服务器上的应用自己控制自己的响应数量?比如每天只能调 100 次,那部署 10 台的话,总量就变成了 1000 次了;

反推?因为每天总量只能调 100 次,部署 10 台,那就是每台每天只能调 10 次?这是个很差的办法,先不说流量一定可以平均分配到每台机器上,如果有一台机器挂掉了,是不是今天只能支持调用 90 次了?

 

通常的解决方案,可以把令牌桶中的令牌,不要放在本地,而是放在一个公共的地方,比如 Redis 中,每次请求过来,就计算是否超过限制的总量,如果未超过,则正常处理,如果已超过,则返回错误信息。

 

具体做法是,用 Redis 中的 key-100 作为令牌桶,其中 100 表示一分钟可以调用 100 次,每次处理前对 value 进行减 1,返回的值大于 0 表示可以处理;每分钟将 value 设置回 100;或计数累加,开始是 0 ,不断累加,最后超过单位时间的总量限制;

不过这个方法要有一个定时任务,去设置令牌的数量,另外这种方法是不能应对突发流量的,比如前 59 秒一次请求也没有,第 60 秒来了 100 次,第 61 秒进入了一个新的周期,又来了 100 次请求 ,这样实际上是在两秒内处理了 200 次请求。

 

另外一种方案是使用 Redis 中的有序队列 Sorted Set ,存储近 100 次的调用时间,每次有新请求的时候,对比队列中第一个元素的时间和当前时间,如果相差超过 1 分钟,表示还没有超过流量限制,进行处理,并将第一个元素压出队列,将新的请求时间压入队列。

 

 

 

 

为什么需要限流

按照服务的调用方,可以分为以下几种类型服务

1.与用户打交道的服务

比如web服务、对外API,这种类型的服务有以下几种可能导致机器被拖垮:

  • 用户增长过快
  • 热点事件,如抢购、秒杀
  • 爬虫请求,爬取数据等
  • 恶意请求

这些情况都是无法预知的,不知道什么时候会有10倍甚至20倍的流量打进来,如果真碰上这种情况,扩容是根本来不及的(弹性扩容都是虚谈,一秒钟你给我扩一下试试)

2.对内的RPC服务

一个服务A的接口可能被BCDE多个服务进行调用,在B服务发生突发流量时,直接把A服务给调用挂了,导致A服务对CDE也无法提供服务。 这种情况时有发生,解决方案有两种: 每个调用方采用线程池进行资源隔离和使用限流手段对每个调用方进行限流。

常用限流算法

常见的限流算法有:计数器、令牌桶、漏桶。

1、计数器算法,采用计数器实现限流有点简单粗暴,一般我们会限制一秒钟的能够通过的请求数,比如限流qps为100,算法的实现思路就是从第一个请求进来开始计时,在接下去的1s内,每来一个请求,就把计数加1,如果累加的数字达到了100,那么后续的请求就会被全部拒绝。等到1s结束后,把计数恢复成0,重新开始计数。

具体的实现可以是这样的:对于每次服务调用,可以通过 AtomicLong#incrementAndGet()方法来给计数器加1并返回最新值,通过这个最新值和阈值进行比较。

这种实现方式,相信大家都知道有一个弊端:如果我在单位时间1s内的前10ms,已经通过了100个请求,那后面的990ms,只能眼巴巴的把请求拒绝,我们把这种现象称为“突刺现象”。

2、漏桶算法,为了消除"突刺现象",可以采用漏桶算法实现限流,漏桶算法这个名字就很形象,算法内部有一个容器,类似生活用到的漏斗,当请求进来时,相当于水倒入漏斗,然后从下端小口慢慢匀速的流出。不管上面流量多大,下面流出的速度始终保持不变。

不管服务调用方多么不稳定,通过漏桶算法进行限流,每10毫秒处理一次请求。因为处理的速度是固定的,请求进来的速度是未知的,可能突然进来很多请求,没来得及处理的请求就先放在桶里,既然是个桶,肯定是有容量上限,如果桶满了,那么新进来的请求就丢弃。

在算法实现方面,可以准备一个队列,用来保存请求,另外通过一个线程池定期从队列中获取请求并执行,可以一次性获取多个并发执行。

这种算法,在使用过后也存在弊端:无法应对短时间的突发流量。

3、令牌桶算法,从某种意义上讲,令牌桶算法是对漏桶算法的一种改进,桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。

在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。

放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌,所以就存在这种情况,桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行,比如设置qps为100,那么限流器初始化完成一秒后,桶中就已经有100个令牌了,这时服务还没完全启动好,等启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。所以,只有桶中没有令牌时,请求才会进行等待,最后相当于以一定的速率执行。

实现思路:可以准备一个队列,用来保存令牌,另外通过一个线程池定期生成令牌放到队列中,每来一个请求,就从队列中获取一个令牌,并继续执行。

幸运的是,通过Google开源的guava包,我们可以很轻松的创建一个令牌桶算法的限流器。

集群限流

前面讨论的几种算法都属于单机限流的范畴,但是业务需求五花八门,简单的单机限流,根本无法满足他们。

比如为了限制某个资源被每个用户或者商户的访问次数,5s只能访问2次,或者一天只能调用1000次,这种需求,单机限流是无法实现的,这时就需要通过集群限流进行实现。

如何实现?为了控制访问次数,肯定需要一个计数器,而且这个计数器只能保存在第三方服务,比如redis+lua。

服务入口限流

这个就比较常见了,比如使用Nginx,在Nginx中通过配置实现请求频率的限制。

 

 

 

 

 

  • 2
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,这是一个比较大的问题,需要分多个步骤来进行演示。下面我将详细介绍如何实现微服务中台监控,并记录服务调用次数、服务调用来源、自定义注解,以及发送消息到MQ和消费消息插入到数据库。 1. 添加依赖 首先,需要在项目中添加Spring Boot的Actuator和Sleuth依赖,这两个依赖可以提供基本的微服务监控功能和跟踪功能。 ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId> </dependency> ``` 2. 自定义注解 为了记录服务调用来源,我们可以自定义一个注解`@Trace`,用于标识服务接口调用方。 ```java @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Trace { String value() default ""; } ``` 3. 拦截器 为了记录服务调用次数和服务调用来源,我们需要使用拦截器,拦截服务接口请求,然后进行记录。 ```java @Component public class TraceInterceptor implements HandlerInterceptor { private static final Logger LOGGER = LoggerFactory.getLogger(TraceInterceptor.class); private final Tracer tracer; private final CounterService counterService; public TraceInterceptor(Tracer tracer, CounterService counterService) { this.tracer = tracer; this.counterService = counterService; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Span currentSpan = this.tracer.currentSpan(); if (currentSpan != null) { String traceId = currentSpan.context().traceId(); String spanId = currentSpan.context().spanId(); LOGGER.info("TraceId: {}, SpanId: {}", traceId, spanId); } // 记录服务调用次数 String uri = request.getRequestURI(); this.counterService.increment(uri); // 记录服务调用来源 Method method = ((HandlerMethod) handler).getMethod(); Trace traceAnnotation = AnnotationUtils.findAnnotation(method, Trace.class); if (traceAnnotation != null) { String source = traceAnnotation.value(); LOGGER.info("Source: {}", source); } return true; } } ``` 4. MQ消息发送 为了将服务调用信息发送到MQ,我们需要使用Spring Boot提供的`RabbitTemplate`。 ```java @Service public class TraceService { private static final Logger LOGGER = LoggerFactory.getLogger(TraceService.class); private final RabbitTemplate rabbitTemplate; public TraceService(RabbitTemplate rabbitTemplate) { this.rabbitTemplate = rabbitTemplate; } public void sendTraceMessage(String message) { LOGGER.info("Sending trace message: {}", message); this.rabbitTemplate.convertAndSend("trace", message); } } ``` 5. MQ消息消费 为了将服务调用信息插入到数据库,我们需要使用Spring Boot提供的`@RabbitListener`注解,来消费MQ中的消息。 ```java @Component public class TraceMessageListener { private static final Logger LOGGER = LoggerFactory.getLogger(TraceMessageListener.class); private final JdbcTemplate jdbcTemplate; public TraceMessageListener(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @RabbitListener(queues = "trace") public void receiveTraceMessage(String message) { LOGGER.info("Receiving trace message: {}", message); String[] split = message.split(","); String uri = split[0]; String source = split.length > 1 ? split[1] : ""; String sql = "INSERT INTO trace (uri, source) VALUES (?, ?)"; this.jdbcTemplate.update(sql, uri, source); } } ``` 6. 配置文件 最后,我们需要在配置文件中进行配置,包括MQ的相关信息、拦截器、消息队列等。 ```yaml spring: application: name: demo rabbitmq: host: localhost port: 5672 username: guest password: guest sleuth: sampler: probability: 1.0 management: endpoints: web: exposure: include: "*" metrics: tags: application: ${spring.application.name} logging: level: com: example: demo: DEBUG server: port: 8080 trace: mq: queue: trace ``` 以上就是微服务中台监控的实现过程,包括记录服务调用次数、服务调用来源、自定义注解、发送消息到MQ和消费消息插入到数据库。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值