2
头图

在分布式系统中,高并发既是业务增长的标志,也是系统崩溃的导火索。

今天我们聚焦Go-zero这个框架,手把手带你掌握限流、熔断和降级:

限流:用令牌桶算法精准控制流量,防止单点过载

熔断:构建“断路器”机制,避免故障级联扩散

降级:优雅放弃非核心功能,守住业务生命线

本文不仅附有完整代码示例,还拆解了高频面试问题及回答技巧,助你从“会写代码”进阶为“懂系统设计”的技术人。

限流、熔断示例:

限流示例

Go-zero中可以使用ratelimiter中间件来实现API流量控制。示例代码如下:

首先创建一个limiter实例:

limiter := rate.NewLimiter(rate.Limit(qps), qps*3)

这里rate.Limit(qps)用来设置每秒允许的请求量,qps*3用来设置瞬间最大并发数。

然后在请求处理中使用该limiter

if!limiter.Allow() {
    return http.StatusTooManyRequests, nil
}

通过limiter.Allow()判断当前请求是否允许访问,如果超过了请求数,则返回http.StatusTooManyRequests错误。

熔断示例

Go-zero中没有像Hystrix那样有非常成熟的、开箱即用的熔断组件,但可以参考一些开源的熔断器实现来进行自定义熔断逻辑。以下是一个简单的模拟熔断器逻辑示例:

package main

import (
    "fmt"
    "time"
)

// 熔断器结构体
type CircuitBreaker struct {
    state       int32 // 熔断器状态,0表示关闭,1表示打开,2表示半打开
    errorCount  int   // 错误计数
    totalCount  int   // 总请求计数
    openTime    time.Time // 熔断器打开时间
    recoveryTime time.Duration // 熔断恢复时间
    threshold   float64 // 错误率阈值
}

// 初始化熔断器
func NewCircuitBreaker(recoveryTime time.Duration, threshold float64) *CircuitBreaker {
    return &CircuitBreaker{
       state: 0,
       errorCount: 0,
       totalCount: 0,
       openTime: time.Time{},
       recoveryTime: recoveryTime,
       threshold: threshold,
    }
}

// 执行请求
func (cb *CircuitBreaker) Execute(request func() error) error {
    if atomic.LoadInt32(&cb.state) == 1 {
       // 熔断器打开,直接返回错误
       return fmt.Errorf("service is unavailable")
    }

    err := request()
    cb.totalCount++
    if err!= nil {
       cb.errorCount++
       // 计算错误率
       errorRate := float64(cb.errorCount) / float64(cb.totalCount)
       if errorRate >= cb.threshold {
          // 达到错误率阈值,打开熔断器
          atomic.StoreInt32(&cb.state, 1)
          cb.openTime = time.Now()
       }
       return err
    }
    return nil
}

// 检查熔断器状态并尝试恢复
func (cb *CircuitBreaker) CheckState() {
    if atomic.LoadInt32(&cb.state) == 1 && time.Since(cb.openTime) >= cb.recoveryTime {
       // 进入半打开状态,尝试允许一个请求通过
       atomic.StoreInt32(&cb.state, 2)
    } else if atomic.LoadInt32(&cb.state) == 2 {
       // 半打开状态下,如果请求成功,关闭熔断器
       atomic.StoreInt32(&cb.state, 0)
       cb.errorCount = 0
       cb.totalCount = 0
    }
}

你可以这样使用它:

func main() {
    // 初始化熔断器,设置熔断恢复时间为5秒,错误率阈值为0.5
    cb := NewCircuitBreaker(5*time.Second, 0.5)

    // 模拟请求
    for i := 0; i < 10; i++ {
       err := cb.Execute(func() error {
          // 这里模拟服务调用,假设前5次调用失败
          if i < 5 {
             return fmt.Errorf("service error")
          }
          return nil
       })
       if err!= nil {
          fmt.Println("Request failed:", err)
       } else {
          fmt.Println("Request succeeded")
       }
       // 检查熔断器状态并尝试恢复
       cb.CheckState()
    }
}

上述代码中,CircuitBreaker结构体表示熔断器,包含了状态、错误计数、总请求计数、打开时间、恢复时间和错误率阈值等字段。Execute方法用于执行请求,并根据请求结果更新熔断器状态。CheckState方法用于定期检查熔断器状态并尝试恢复。在main函数中,初始化了一个熔断器,并模拟了10次请求,前5次请求模拟失败,以触发熔断器的打开和恢复逻辑。

面试可能会问的问题:

① Go-zero的限流中间件ratelimiter是如何工作的?

Go-zero的ratelimiter中间件是基于令牌桶算法来实现限流的。

下面详细介绍其工作原理。

令牌桶算法基础概念

令牌桶算法是一种常用的流量控制算法,其核心思想是有一个固定容量的桶,系统会以恒定的速率向桶中放入令牌。当有请求到来时,需要从桶中获取一个或多个令牌,如果桶中有足够的令牌,请求就会被处理,同时相应数量的令牌会从桶中移除;如果桶中没有足够的令牌,请求就会被限流(拒绝或等待)。

Go-zero中ratelimiter的工作流程

1. 初始化

在使用ratelimiter时,首先需要对其进行初始化。通常会指定两个重要的参数:

  • 每秒生成的令牌数(Rate):表示系统向令牌桶中添加令牌的速率。
  • 令牌桶的最大容量(Burst):即令牌桶能够容纳的最大令牌数量。

以下是一个简单的初始化示例:

import (
    "github.com/zeromicro/go-zero/core/limit"
    "golang.org/x/time/rate"
)

func main() {
    // 每秒生成100个令牌,令牌桶最大容量为300
    limiter := limit.NewTokenLimiter(100, 300)
}

2. 令牌生成

在初始化完成后,系统会按照设定的速率(Rate)向令牌桶中添加令牌。这个过程是自动进行的,并且会在后台持续运行。随着时间的推移,令牌桶中的令牌数量会逐渐增加,直到达到最大容量(Burst)。

3. 请求处理

当有请求到来时,ratelimiter会尝试从令牌桶中获取所需数量的令牌(通常为1个)。这个操作通过调用Allow()方法来完成:

if limiter.Allow() {
    // 有足够的令牌,处理请求
    // 业务逻辑代码
} else {
    // 没有足够的令牌,进行限流处理
    // 可以返回错误信息或者进行其他处理
}

Allow()方法会根据当前令牌桶中的令牌数量来判断是否允许请求通过。如果桶中有足够的令牌,它会立即扣除相应数量的令牌,并返回true,表示请求可以被处理;如果桶中没有足够的令牌,它会返回false,表示请求被限流。

4. 动态调整

在某些情况下,可能需要动态调整限流的速率和容量。ratelimiter提供了相应的方法来实现这一点,例如SetLimit()SetBurst()方法:

// 动态调整每秒生成的令牌数为200
limiter.SetLimit(rate.Limit(200))
// 动态调整令牌桶的最大容量为400
limiter.SetBurst(400)

代码示例

以下是一个完整的使用Go-zero的ratelimiter中间件进行限流的示例代码:

package main

import (
    "fmt"
    "github.com/zeromicro/go-zero/core/limit"
    "golang.org/x/time/rate"
    "net/http"
)

func main() {
    // 每秒生成10个令牌,令牌桶最大容量为30
    limiter := limit.NewTokenLimiter(10, 30)

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        if limiter.Allow() {
            fmt.Fprintf(w, "Request processed successfully")
        } else {
            http.Error(w, "Too many requests", http.StatusTooManyRequests)
        }
    })

    fmt.Println("Server started on :8080")
    http.ListenAndServe(":8080", nil)
}

在这个示例中,我们创建了一个简单的HTTP服务器,并使用ratelimiter中间件对所有请求进行限流。当请求到来时,会先检查令牌桶中是否有足够的令牌,如果有则处理请求并返回成功信息,否则返回429 Too Many Requests错误。

通过以上的工作流程,Go-zero的ratelimiter中间件能够有效地对系统的流量进行控制,防止系统因过多的请求而崩溃。

②你在项目中具体怎么做限流、熔断和降级、面试的时候怎么回答?具体的例子和回答技巧。

限流

在项目中,限流是控制进入系统的请求速率,防止系统因过载而崩溃。以下以Go语言结合Go-zero框架为例,说明如何进行限流。

基于令牌桶算法的限流

令牌桶算法是一种常见的限流算法,系统以固定速率向桶中添加令牌,请求需要从桶中获取令牌才能被处理。

package main

import (
    "github.com/zeromicro/go-zero/core/limit"
    "golang.org/x/time/rate"
    "net/http"
)

func main() {
    // 每秒生成100个令牌,令牌桶最大容量为300
    limiter := limit.NewTokenLimiter(100, 300)

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        if limiter.Allow() {
            w.WriteHeader(http.StatusOK)
            w.Write([]byte("Request processed successfully"))
        } else {
            w.WriteHeader(http.StatusTooManyRequests)
            w.Write([]byte("Too many requests"))
        }
    })

    http.ListenAndServe(":8080", nil)
}

在这个例子中,我们使用Go-zero的limit.NewTokenLimiter创建了一个令牌桶限流器,每秒生成100个令牌,桶的最大容量为300。当有请求到达时,通过limiter.Allow()方法判断是否有足够的令牌,如果有则处理请求,否则返回429 Too Many Requests错误。

熔断

熔断机制用于在服务出现故障或响应时间过长时,暂时切断对该服务的调用,避免故障扩散。

以下是一个模拟熔断示例。

package main

import (
    "errors"
    "fmt"
    "sync"
    "time"
)

// CircuitBreaker 熔断器结构体
type CircuitBreaker struct {
    state       int32 // 0: 关闭, 1: 打开, 2: 半开
    errorCount  int
    totalCount  int
    openTime    time.Time
    recoveryTime time.Duration
    threshold   float64
    mutex       sync.Mutex
}

// NewCircuitBreaker 创建新的熔断器
func NewCircuitBreaker(recoveryTime time.Duration, threshold float64) *CircuitBreaker {
    return &CircuitBreaker{
       state: 0,
       errorCount: 0,
       totalCount: 0,
       openTime: time.Time{},
       recoveryTime: recoveryTime,
       threshold: threshold,
    }
}

// Execute 执行请求
func (cb *CircuitBreaker) Execute(request func() error) error {
    cb.mutex.Lock()
    defer cb.mutex.Unlock()

    if cb.state == 1 {
       if time.Since(cb.openTime) > cb.recoveryTime {
          cb.state = 2
       } else {
          return errors.New("circuit breaker is open")
       }
    }

    err := request()
    cb.totalCount++
    if err != nil {
       cb.errorCount++
       errorRate := float64(cb.errorCount) / float64(cb.totalCount)
       if errorRate >= cb.threshold {
          cb.state = 1
          cb.openTime = time.Now()
       }
    } else {
       if cb.state == 2 {
          cb.state = 0
          cb.errorCount = 0
          cb.totalCount = 0
       }
    }
    return err
}

func main() {
    cb := NewCircuitBreaker(5*time.Second, 0.5)

    for i := 0; i < 10; i++ {
       err := cb.Execute(func() error {
          if i < 5 {
             return errors.New("simulated error")
          }
          return nil
       })
       if err != nil {
          fmt.Println("Request failed:", err)
       } else {
          fmt.Println("Request succeeded")
       }
    }
}

在这个示例中,我们定义了一个CircuitBreaker结构体,包含了熔断器的状态、错误计数、总请求计数等信息。Execute方法用于执行请求,并根据请求结果更新熔断器的状态。当错误率超过阈值时,熔断器打开,一段时间后进入半开状态,尝试允许部分请求通过,如果请求成功则关闭熔断器。

降级

降级是指在系统资源不足或服务出现问题时,放弃一些非核心业务功能,保证核心业务的正常运行。

以下是一个示例。

package main

import (
    "fmt"
    "net/http"
)

// 核心业务处理函数
func coreHandler(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("Core business processed successfully"))
}

// 非核心业务处理函数
func nonCoreHandler(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("Non-core business processed successfully"))
}

// 降级处理函数
func degradeHandler(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("System is under high load, non-core business is degraded"))
}

func main() {
    // 模拟系统负载过高
    isHighLoad := true

    http.HandleFunc("/core", coreHandler)
    if isHighLoad {
       http.HandleFunc("/non-core", degradeHandler)
    } else {
       http.HandleFunc("/non-core", nonCoreHandler)
    }

    http.ListenAndServe(":8080", nil)
}

在这个示例中,我们定义了核心业务处理函数coreHandler和非核心业务处理函数nonCoreHandler。当系统负载过高时,将非核心业务的请求路由到降级处理函数degradeHandler,返回降级提示信息。

面试回答技巧和示例回答

回答技巧

  • 原理阐述:先简要介绍限流、熔断和降级的基本概念和原理,让面试官了解你对这些技术的理解深度。
  • 项目实践:结合具体的项目经验,详细描述在项目中是如何实现限流、熔断和降级的,包括使用的技术、框架和工具。
  • 问题解决:分享在实践过程中遇到的问题以及解决方法,展示你的问题解决能力。
  • 效果评估:说明采取这些措施后对系统性能和稳定性的提升效果,让面试官了解这些技术的实际价值。

示例回答

问:请谈谈你在项目中是如何进行限流、熔断和降级的?

答:在我参与的[项目名称]中,我们采用了多种技术手段来实现限流、熔断和降级,以确保系统的稳定性和可靠性。

  • 限流:我们使用了基于令牌桶算法的限流策略。具体来说,我们使用了Go-zero框架的limit.NewTokenLimiter创建了一个令牌桶限流器,每秒生成100个令牌,令牌桶的最大容量为300。当有请求到达时,会先检查令牌桶中是否有足够的令牌,如果有则处理请求,否则返回429 Too Many Requests错误。通过这种方式,我们有效地控制了系统的请求速率,避免了因过多请求导致的系统崩溃。
  • 熔断:为了防止服务故障的扩散,我们实现了一个简单的熔断机制。我们定义了一个CircuitBreaker结构体,包含了熔断器的状态、错误计数、总请求计数等信息。当请求的错误率超过阈值时,熔断器会打开,暂时切断对该服务的调用。一段时间后,熔断器进入半开状态,尝试允许部分请求通过,如果请求成功则关闭熔断器。这样可以在服务出现问题时,快速隔离故障,保证系统的整体稳定性。
  • 降级:在系统资源不足或服务出现问题时,我们会进行降级处理。我们将业务功能分为核心业务和非核心业务,当系统负载过高时,会放弃一些非核心业务功能,保证核心业务的正常运行。例如,我们会将非核心业务的请求路由到降级处理函数,返回降级提示信息,而核心业务仍然可以正常处理。

通过这些措施,我们有效地提高了系统的容错能力和稳定性,在高并发场景下也能保证系统的正常运行。在实践过程中,我们也遇到了一些问题,比如如何准确设置限流的阈值和熔断的条件,我们通过不断的测试和优化,最终找到了合适的参数。这些经验也让我对限流、熔断和降级有了更深入的理解和实践经验。

欢迎关注 ❤

我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。

没准能让你能刷到自己意向公司的最新面试题呢。

感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:面试群。


王中阳讲编程
836 声望326 粉丝