在分布式系统中,高并发既是业务增长的标志,也是系统崩溃的导火索。
今天我们聚焦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
中间件是基于令牌桶算法来实现限流的。
下面详细介绍其工作原理。
令牌桶算法是一种常用的流量控制算法,其核心思想是有一个固定容量的桶,系统会以恒定的速率向桶中放入令牌。当有请求到来时,需要从桶中获取一个或多个令牌,如果桶中有足够的令牌,请求就会被处理,同时相应数量的令牌会从桶中移除;如果桶中没有足够的令牌,请求就会被限流(拒绝或等待)。
ratelimiter
的工作流程在使用ratelimiter
时,首先需要对其进行初始化。通常会指定两个重要的参数:
以下是一个简单的初始化示例:
import (
"github.com/zeromicro/go-zero/core/limit"
"golang.org/x/time/rate"
)
func main() {
// 每秒生成100个令牌,令牌桶最大容量为300
limiter := limit.NewTokenLimiter(100, 300)
}
在初始化完成后,系统会按照设定的速率(Rate
)向令牌桶中添加令牌。这个过程是自动进行的,并且会在后台持续运行。随着时间的推移,令牌桶中的令牌数量会逐渐增加,直到达到最大容量(Burst
)。
当有请求到来时,ratelimiter
会尝试从令牌桶中获取所需数量的令牌(通常为1个)。这个操作通过调用Allow()
方法来完成:
if limiter.Allow() {
// 有足够的令牌,处理请求
// 业务逻辑代码
} else {
// 没有足够的令牌,进行限流处理
// 可以返回错误信息或者进行其他处理
}
Allow()
方法会根据当前令牌桶中的令牌数量来判断是否允许请求通过。如果桶中有足够的令牌,它会立即扣除相应数量的令牌,并返回true
,表示请求可以被处理;如果桶中没有足够的令牌,它会返回false
,表示请求被限流。
在某些情况下,可能需要动态调整限流的速率和容量。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
,返回降级提示信息。
问:请谈谈你在项目中是如何进行限流、熔断和降级的?
答:在我参与的[项目名称]中,我们采用了多种技术手段来实现限流、熔断和降级,以确保系统的稳定性和可靠性。
limit.NewTokenLimiter
创建了一个令牌桶限流器,每秒生成100个令牌,令牌桶的最大容量为300。当有请求到达时,会先检查令牌桶中是否有足够的令牌,如果有则处理请求,否则返回429 Too Many Requests
错误。通过这种方式,我们有效地控制了系统的请求速率,避免了因过多请求导致的系统崩溃。CircuitBreaker
结构体,包含了熔断器的状态、错误计数、总请求计数等信息。当请求的错误率超过阈值时,熔断器会打开,暂时切断对该服务的调用。一段时间后,熔断器进入半开状态,尝试允许部分请求通过,如果请求成功则关闭熔断器。这样可以在服务出现问题时,快速隔离故障,保证系统的整体稳定性。通过这些措施,我们有效地提高了系统的容错能力和稳定性,在高并发场景下也能保证系统的正常运行。在实践过程中,我们也遇到了一些问题,比如如何准确设置限流的阈值和熔断的条件,我们通过不断的测试和优化,最终找到了合适的参数。这些经验也让我对限流、熔断和降级有了更深入的理解和实践经验。
我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。
没准能让你能刷到自己意向公司的最新面试题呢。
感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:面试群。