一、项目架构图
二、现有问题
- 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 桶大小