一、项目架构图

 

 

 

 

二、现有问题 

  • register接口接收大量的SDK请求,但并未对请求的并发数进行控制,导致服务无法拥有足够的内存,从而频繁被系统 Kill。

 

 

三、解决方案

  • consul中启用健康检查,让节点内存、CPU资源紧张时能“休息一下” 
  • register里面根据节点内存剩余量做过载保护,并将过载错误码返回给SDK,SDK延时重试。注:SDK目前对所有错误码都重试,理论上只有过载和服务器内部错误需要重试。
  • 微服务在系统内存紧张时手动GC,释放内存,让节点迅速恢复健康。
  • 把register和其他服务混布,增加节点数,充分利用资源。
  •  register对其他服务的访问目前是串行的,这不够高效,只要没有依赖关系,就应改为并行
  • 根据请求处理时间延时做负载判断,使用令牌桶做自适应限流
  • 排查register依赖的服务,找出真正的瓶颈所在

 

四、具体实现

新请求限流:使用 time/rate 实现令牌桶逻辑,控制每秒内所接收的新请求数量

在途请求限流:记录当前正在处理的请求数量,当接收到SDK请求时,将 curConcurrency 加1,代表这个请求正在被处理(在途请求);在响应前将 curConcurrency 加1,代表这个请求已经完成。需配置在途请求的最大并发数,只有在当前的 curConcurrency < maxConcurrency 时,才对请求进行处理,否则将请求直接丢弃。

 

// 构造一个限流器对象        NewLimiter(r, b): r 每秒可向Token桶中产生多少 token,b Token桶的容量大小
limiter := NewLimiter(10, 1);        // 令牌桶大小为 1, 以每秒 10 个 token 的速率向桶中放置 Token


// Every 方法用来指定向 Token 桶中放置 token 的时间间隔
limit := Every(100 * time.Millisecond);        // 每 100ms 往桶中放一个 Token; 一秒钟产生 10 个token
limiter := NewLimiter(limit, 1);


// 消费方式:

// Wait/WaitN: 阻塞消费
// 调用 Wait 方法消费时, 若桶内的token数组长度 不足n(小于n), Wait方法将会阻塞, 直到token数量满足条件位置; 如果桶内token数组满足条件则直接返回
func (lim *Limiter) Wait(ctx context.Context) (err error)
func (lim *Limiter) WaitN(ctx context.Context, n int) (err error)    // 可设置 ctx(context) 参数的 Deadline、Timeout 来决定此次 Wait 的最长时间



// Allow/AllowN: 桶内token满足条件消费
// AllowN: 截止到某一时刻, 当前桶内token数量是否至少为n个, 满足返回true, 同时从桶内消费n个token; 反之返回false不消费token
func (lim *Limiter) Allow() bool
func (lim *Limiter) AllowN(now time.Time, n int) bool



// Reserve/ReserveN: 对象等待消费
// 调用 ReserveN 方法后, 无论 token 是否充足都会返回一个 Reservation* 对象, 再调用该对象的 Delay 方法, 可返回需要等待的时间, 若等待时间为0, 则无需等待; 
// 否则必须等待到之后时间后, 才能继续工作; 若期间不想再等待可调用 Cancel方法取消, 它将会把 token 归还
r := lim.Reserve()
f !r.OK() {
    // Not allowed to act! Did you remember to set lim.burst to be > 0 ?
    return
}
time.Sleep(r.Delay())
Act() // 执行相关逻辑



// 动态调整速率
//Limiter 支持可以调整速率和桶大小:
SetLimit(Limit)     // 改变放入 Token 的速率, 接收的参数: 如果为 1、2、3...等数字可直接传递, 如果是将数字保存在变量中时(基本类型 int64、float64...) 必须使用 rate.Limit(variable) 进行转换为 Limit 类型, 即使底层的数据类型是一致
SetBurst(int)         // 改变 Token 桶大小
time/rate 使用

 

为什么要实现在途请求的限流?

服务当中是以上一秒的总请求数、正常处理请求数、超时请求数作为,下一秒设置令牌桶大小的参考, 控制着当前这1秒应该产生多少token,令牌桶的大小决定了 当前这一秒允许多少个的新请求进入屋子等待服务的响应。将令牌桶想象成大队长,它专用来清点请求数量,放指定数量的请求进屋;虽然大队长能够控制这一秒应该进去多少请求,但它无法控制这群请求什么时候出来,会在屋子里面呆多久的时间。假设每个请求需要5s才能返回响应,每秒假设会由令牌桶大队长放进来100个请求,那么到了第5秒的时候,屋子里面总共会有500个请求,既包括最后1秒的新请求,又包括了前4秒的在途请求。这些请求越积越多,但是屋子只有那么小,最后装不下了,内存也就爆了(总请求数=新请求+在途请求)。而并发控制就是为了解决 令牌桶 无法控制屋子里的总请求数的缺陷。

 

令牌桶 博客参考链接:

 

consul 参考链接:

 

 posted on 2020-03-07 14:33  lin-gooo  阅读(570)  评论(0编辑  收藏  举报