一、项目架构图
二、现有问题
- 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 桶大小
为什么要实现在途请求的限流?
服务当中是以上一秒的总请求数、正常处理请求数、超时请求数作为,下一秒设置令牌桶大小的参考, 控制着当前这1秒应该产生多少token,令牌桶的大小决定了 当前这一秒允许多少个的新请求进入屋子等待服务的响应。将令牌桶想象成大队长,它专用来清点请求数量,放指定数量的请求进屋;虽然大队长能够控制这一秒应该进去多少请求,但它无法控制这群请求什么时候出来,会在屋子里面呆多久的时间。假设每个请求需要5s才能返回响应,每秒假设会由令牌桶大队长放进来100个请求,那么到了第5秒的时候,屋子里面总共会有500个请求,既包括最后1秒的新请求,又包括了前4秒的在途请求。这些请求越积越多,但是屋子只有那么小,最后装不下了,内存也就爆了(总请求数=新请求+在途请求)。而并发控制就是为了解决 令牌桶 无法控制屋子里的总请求数的缺陷。
令牌桶 博客参考链接:
-
Golang 限流器 time/rate 使用介绍
-
Golang 限流器 time/rate 实现剖析
-
限流 | Go 实现速率限制
-
golang.org/x/time/rate 使用说明
- time/rate 官方文档:https://godoc.org/golang.org/x/time/rate
- time/rate 包注释版代码:https://github.com/chenyahui/AnnotatedCode/blob/master/go/x/time/rate/rate.go
-
go get golang.org/x 包失败: https://shockerli.net/post/go-get-golang-org-x-solution/
consul 参考链接:
- consul 官网地址: https://learn.hashicorp.com/consul/getting-started/install.html
- consul Github 地址:https://github.com/hashicorp/consul
- consul 官方文档: https://book-consul-guide.vnzmi.com/04_run_agent.html