在分布式系统里,功能能跑通只是起点,稳定地跑下去才是硬指标。一次慢调用、一次依赖抖动、一次重复提交,都可能把一个看似正常的 Go 服务拖进雪崩。
这一节我们聚焦稳定性设计里最常用也最容易踩坑的几个点:超时、重试、熔断、限流、降级、幂等。文章不仅讲概念,也给出完整可运行的代码示例,帮助你把设计原则真正落到工程实践里。
一、为什么稳定性设计不能靠“出问题再补”
很多线上事故并不是因为核心业务逻辑写错,而是因为没有把异常场景当成默认场景来设计:
- 下游接口偶发变慢,没有超时控制,协程大量堆积
- 请求失败后直接无限重试,导致雪上加霜
- 依赖服务已经异常,调用方还在持续打流量
- 突发流量进入系统,没有限流和降级措施
- 用户重复提交订单,服务端没有幂等保护
稳定性设计的目标,不是“绝不失败”,而是:
- 失败要可控
- 故障要可隔离
- 性能退化时要有保护手段
- 重复请求不能产生脏数据
二、超时设计:先让请求有边界
超时是稳定性设计的第一道防线。没有超时,系统就没有“止损点”。
2.1 context 超时传递
在 Go 里,context.Context 是跨函数、跨服务传递超时、取消信号、链路信息的标准方式。
一个常见原则是:
- 上游入口创建超时边界
- 下游函数只接收
ctx,不要偷偷重新造无限期上下文 - 所有 I/O 操作都尽量感知
ctx.Done()
下面是一个完整的可运行示例,演示请求级超时如何一路传递到下游调用。
package main
import (
"context"
"errors"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1500*time.Millisecond)
defer cancel()
if err := CreateOrder(ctx); err != nil {
fmt.Println("create order failed:", err)
return
}
fmt.Println("create order success")
}
func CreateOrder(ctx context.Context) error {
if err := checkInventory(ctx); err != nil {
return fmt.Errorf("checkInventory: %w", err)
}
if err := createPayment(ctx); err != nil {
return fmt.Errorf("createPayment: %w", err)
}
return nil
}
func checkInventory(ctx context.Context) error {
select {
case <-time.After(400 * time.Millisecond):
fmt.Println("inventory checked")
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func createPayment(ctx context.Context) error {
select {
case <-time.After(1300 * time.Millisecond):
fmt.Println("payment created")
return nil
case <-ctx.Done():
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
return fmt.Errorf("payment timeout: %w", ctx.Err())
}
return ctx.Err()
}
}
运行结果通常会是支付阶段超时,因为整个链路总预算只有 1.5 秒。
这段代码体现了两个关键点:
- 超时预算由入口统一管理
- 下游逻辑不自行决定“等多久”,而是服从上游预算
2.2 客户端超时 vs 服务端超时
很多人只给 HTTP 客户端设置一个 Timeout,觉得这样就够了。其实不够。
客户端超时和服务端超时关注的是两个不同问题。
| 维度 | 客户端超时 | 服务端超时 |
|---|---|---|
| 作用位置 | 调用方 | 被调用方 |
| 核心目标 | 避免调用方无限等待 | 避免服务端连接与处理资源被长期占用 |
| 常见配置 | http.Client.Timeout、请求 context.WithTimeout |
ReadHeaderTimeout、ReadTimeout、WriteTimeout、业务处理超时 |
| 解决的问题 | 下游太慢、网络异常、响应迟迟不返回 | 慢连接攻击、客户端上传过慢、处理时间失控 |
| 是否可互相替代 | 不能 | 不能 |
客户端超时解决“我不能一直等”;服务端超时解决“我不能一直被占着”。线上系统通常需要两边都配置。
2.3 HTTP 客户端超时示例
下面的示例演示了如何同时使用请求级 context 与 http.Client.Timeout。
package main
import (
"context"
"fmt"
"io"
"net/http"
"time"
)
func main() {
client := &http.Client{
Timeout: 2 * time.Second,
}
ctx, cancel := context.WithTimeout(context.Background(), 1500*time.Millisecond)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://httpbin.org/delay/3", nil)
if err != nil {
panic(err)
}
resp, err := client.Do(req)
if err != nil {
fmt.Println("request failed:", err)
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println("status:", resp.Status)
fmt.Println(string(body))
}
经验上可以这样理解:
context.WithTimeout更适合表达“本次业务请求预算”http.Client.Timeout更像一个客户端总保险丝- 如果要细粒度控制连接建立、TLS 握手、响应头等待,还可以继续配置
Transport
2.4 HTTP 服务端超时示例
下面是一个带有服务端超时配置的 HTTP 服务。
package main
import (
"context"
"fmt"
"log"
"net/http"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/work", workHandler)
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 2 * time.Second,
ReadTimeout: 5 * time.Second,
WriteTimeout: 3 * time.Second,
IdleTimeout: 30 * time.Second,
}
log.Println("server listening on :8080")
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}
func workHandler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 1200*time.Millisecond)
defer cancel()
if err := doBusiness(ctx); err != nil {
http.Error(w, err.Error(), http.StatusGatewayTimeout)
return
}
fmt.Fprintln(w, "ok")
}
func doBusiness(ctx context.Context) error {
select {
case <-time.After(2 * time.Second):
return nil
case <-ctx.Done():
return ctx.Err()
}
}
这里要注意两层超时:
http.Server的超时是网络层、连接层保护workHandler内的context.WithTimeout是业务处理保护
生产实践里,这两层一般都要有。
三、重试策略:不是所有失败都值得立刻重来
重试的核心问题不是“要不要重试”,而是“什么错误可以重试、重试几次、间隔多久、会不会把下游压垮”。
3.1 什么场景适合重试
适合重试的典型场景:
- 网络瞬时抖动
- 临时性超时
- 返回 502、503、504 等短暂错误
- 读请求或天然幂等操作
不适合盲目重试的场景:
- 参数错误
- 业务校验失败
- 非幂等写操作且没有幂等保护
- 下游已经明显过载
3.2 固定间隔、指数退避、抖动(Jitter)
三种常见重试等待策略如下。
| 策略 | 公式示意 | 优点 | 风险 |
|---|---|---|---|
| 固定间隔 | 每次等 200ms | 实现简单 | 多实例同时重试,容易形成重试风暴 |
| 指数退避 | 100ms、200ms、400ms、800ms | 能快速拉开重试节奏 | 多实例仍可能同频重试 |
| 抖动 Jitter | 指数退避基础上增加随机性 | 最适合分布式场景 | 实现略复杂 |
结论很明确:
- 小规模简单系统可以先用固定间隔
- 分布式系统优先指数退避 + Jitter
3.3 可运行的重试实现
下面给出一个完整示例,支持固定间隔、指数退避和 Jitter。
package main
import (
"context"
"errors"
"fmt"
"math/rand"
"time"
)
type BackoffStrategy int
const (
Fixed BackoffStrategy = iota
Exponential
)
type RetryConfig struct {
MaxAttempts int
BaseDelay time.Duration
MaxDelay time.Duration
UseJitter bool
Strategy BackoffStrategy
}
func main() {
rand.Seed(time.Now().UnixNano())
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
attempt := 0
err := Retry(ctx, RetryConfig{
MaxAttempts: 5,
BaseDelay: 200 * time.Millisecond,
MaxDelay: 2 * time.Second,
UseJitter: true,
Strategy: Exponential,
}, func() error {
attempt++
fmt.Println("do attempt", attempt)
if attempt < 4 {
return errors.New("temporary network error")
}
return nil
})
fmt.Println("final result:", err)
}
func Retry(ctx context.Context, cfg RetryConfig, fn func() error) error {
if cfg.MaxAttempts <= 0 {
return errors.New("MaxAttempts must be > 0")
}
if cfg.BaseDelay <= 0 {
cfg.BaseDelay = 100 * time.Millisecond
}
if cfg.MaxDelay <= 0 {
cfg.MaxDelay = time.Second
}
var lastErr error
for i := 1; i <= cfg.MaxAttempts; i++ {
if err := fn(); err != nil {
lastErr = err
if i == cfg.MaxAttempts {
break
}
delay := nextDelay(cfg, i)
fmt.Println("retry after", delay)
select {
case <-time.After(delay):
case <-ctx.Done():
return ctx.Err()
}
continue
}
return nil
}
return fmt.Errorf("all retries failed: %w", lastErr)
}
func nextDelay(cfg RetryConfig, attempt int) time.Duration {
delay := cfg.BaseDelay
if cfg.Strategy == Exponential {
delay = cfg.BaseDelay * time.Duration(1<<(attempt-1))
}
if delay > cfg.MaxDelay {
delay = cfg.MaxDelay
}
if cfg.UseJitter {
jitter := time.Duration(rand.Int63n(int64(delay / 2)))
delay = delay/2 + jitter
}
return delay
}
这段代码里最值得注意的点有三个:
- 重试必须受
context控制,不能无限重试 - 间隔应有上限,避免指数退避无限增大
- Jitter 能显著减少多个实例同时打爆下游的概率
3.4 重试的工程建议
实践中建议遵循下面几条:
- 只对临时性错误重试
- 写操作必须先保证幂等,再考虑自动重试
- 重试次数一般不宜过大,2 到 5 次通常就够了
- 重试要结合熔断与限流一起看,不能单独设计
四、熔断器模式:不要在故障时持续放大故障
当下游依赖已经明显异常时,继续请求只会让问题更严重。熔断器的本质,就是在故障期快速失败,保护调用方和被调用方。
4.1 熔断器状态机
典型熔断器有三个状态:
| 状态 | 含义 | 行为 |
|---|---|---|
| Closed | 闭合 | 正常放行请求,同时统计成功失败情况 |
| Open | 打开 | 直接拒绝请求,快速失败 |
| Half-Open | 半开 | 允许少量探测请求,判断依赖是否恢复 |
状态流转通常是:
- Closed 下失败率或连续失败次数达到阈值,进入 Open
- Open 到达冷却时间后,进入 Half-Open
- Half-Open 探测成功,回到 Closed
- Half-Open 探测失败,重新回到 Open
4.2 使用 gobreaker 的完整示例
gobreaker 比较轻量,适合很多 Go 服务直接接入。
package main
import (
"errors"
"fmt"
"math/rand"
"time"
"github.com/sony/gobreaker"
)
func main() {
rand.Seed(time.Now().UnixNano())
cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{
Name: "inventory-service",
MaxRequests: 2,
Interval: 10 * time.Second,
Timeout: 3 * time.Second,
ReadyToTrip: func(counts gobreaker.Counts) bool {
return counts.ConsecutiveFailures >= 3
},
OnStateChange: func(name string, from gobreaker.State, to gobreaker.State) {
fmt.Printf("breaker %s state change: %v -> %v\n", name, from, to)
},
})
for i := 1; i <= 12; i++ {
result, err := cb.Execute(func() (interface{}, error) {
return callRemoteService()
})
fmt.Printf("request %02d, result=%v, err=%v\n", i, result, err)
time.Sleep(700 * time.Millisecond)
}
}
func callRemoteService() (string, error) {
// 模拟 70% 概率失败
if rand.Intn(100) < 70 {
return "", errors.New("remote service failed")
}
return "success", nil
}
运行前先初始化依赖:
go mod init gobreaker-demo
go get github.com/sony/gobreaker
go run .
4.3 使用 go-hystrix 的完整示例
如果你的团队更偏向命令隔离、回退函数、熔断统计一体化方案,可以看 hystrix-go。
package main
import (
"errors"
"fmt"
"math/rand"
"time"
"github.com/afex/hystrix-go/hystrix"
)
func main() {
rand.Seed(time.Now().UnixNano())
hystrix.ConfigureCommand("user-profile", hystrix.CommandConfig{
Timeout: 1000,
MaxConcurrentRequests: 100,
SleepWindow: 3000,
RequestVolumeThreshold: 5,
ErrorPercentThreshold: 50,
})
for i := 1; i <= 15; i++ {
err := hystrix.Do("user-profile", func() error {
return callUserProfile()
}, func(err error) error {
fmt.Println("fallback triggered, err:", err)
return nil
})
fmt.Printf("request %02d finished, err=%v\n", i, err)
time.Sleep(500 * time.Millisecond)
}
}
func callUserProfile() error {
cost := time.Duration(300+rand.Intn(1200)) * time.Millisecond
time.Sleep(cost)
if cost > 900*time.Millisecond {
return errors.New("downstream timeout")
}
if rand.Intn(100) < 40 {
return errors.New("downstream internal error")
}
fmt.Println("call success, cost:", cost)
return nil
}
运行前先初始化依赖:
go mod init hystrix-demo
go get github.com/afex/hystrix-go/hystrix
go run .
4.4 熔断不是替代重试,而是限制重试边界
重试和熔断经常一起出现,但职责不同:
- 重试解决瞬时失败
- 熔断解决持续失败
如果没有熔断,重试可能把故障放大。 如果没有重试,很多瞬时抖动本来可以自动恢复。
正确姿势通常是:
- 先判断错误是否值得重试
- 重试次数有限
- 超过阈值后通过熔断快速失败
- 再通过降级兜底
五、限流算法:不要让系统被流量一次打穿
限流的本质是控制进入系统的请求速率,防止资源被瞬时耗尽。
5.1 令牌桶
令牌桶适合“允许一定突发,但要限制平均速率”的场景,是服务端最常见的限流算法之一。
下面用 golang.org/x/time/rate 给出一个完整示例。
package main
import (
"fmt"
"log"
"net/http"
"time"
"golang.org/x/time/rate"
)
func main() {
limiter := rate.NewLimiter(2, 4) // 每秒 2 个令牌,桶容量 4
http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
http.Error(w, "too many requests", http.StatusTooManyRequests)
return
}
fmt.Fprintf(w, "accepted at %s\n", time.Now().Format(time.RFC3339Nano))
})
log.Println("listen :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
运行前先初始化依赖:
go mod init token-bucket-demo
go get golang.org/x/time/rate
go run .
5.2 漏桶
漏桶更强调“以固定速率流出”,适合需要平滑处理速度的场景,比如异步任务消费。
下面示例模拟一个简单漏桶。
package main
import (
"fmt"
"time"
)
func main() {
bucket := make(chan int, 5)
go func() {
for i := 1; i <= 10; i++ {
select {
case bucket <- i:
fmt.Println("accepted request", i)
default:
fmt.Println("dropped request", i)
}
time.Sleep(150 * time.Millisecond)
}
close(bucket)
}()
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
for range ticker.C {
item, ok := <-bucket
if !ok {
fmt.Println("all requests handled")
return
}
fmt.Println("processed request", item)
}
}
5.3 滑动窗口
滑动窗口比固定窗口更平滑,适合按“最近 N 秒内请求数”来判断是否超限。
下面是一个基于时间戳切片的简单滑动窗口示例。
package main
import (
"fmt"
"net/http"
"sync"
"time"
)
type SlidingWindowLimiter struct {
mu sync.Mutex
window time.Duration
limit int
records []time.Time
}
func NewSlidingWindowLimiter(window time.Duration, limit int) *SlidingWindowLimiter {
return &SlidingWindowLimiter{
window: window,
limit: limit,
}
}
func (l *SlidingWindowLimiter) Allow() bool {
l.mu.Lock()
defer l.mu.Unlock()
now := time.Now()
cutoff := now.Add(-l.window)
idx := 0
for _, t := range l.records {
if t.After(cutoff) {
l.records[idx] = t
idx++
}
}
l.records = l.records[:idx]
if len(l.records) >= l.limit {
return false
}
l.records = append(l.records, now)
return true
}
func main() {
limiter := NewSlidingWindowLimiter(3*time.Second, 5)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
http.Error(w, "rate limited", http.StatusTooManyRequests)
return
}
fmt.Fprintln(w, "ok")
})
fmt.Println("listen on :8080")
http.ListenAndServe(":8080", nil)
}
5.4 三种限流算法如何选
| 算法 | 适合场景 | 特点 |
|---|---|---|
| 令牌桶 | API 网关、在线服务 | 支持突发流量,工程实践最常见 |
| 漏桶 | 异步消费、平滑处理 | 输出速率稳定,削峰效果好 |
| 滑动窗口 | 精细限流、统计最近时间段请求数 | 控制更平滑,但实现和存储成本更高 |
六、降级策略:先保核心,再保体验
降级不是“系统坏了”,而是“系统在压力或故障下主动收缩能力,优先保住核心路径”。
6.1 服务降级的常见触发条件
降级一般在下面这些情况下触发:
- 下游接口超时率过高
- 熔断器已打开
- 错误率持续上升
- CPU、内存、线程池、连接池等资源逼近阈值
- 非核心功能流量过大,影响主链路
一个很实用的优先级原则是:
- 核心链路必须保住,比如下单、支付、登录
- 非核心能力优先降级,比如推荐、画像、排行榜、埋点增强能力
6.2 降级实现思路
降级的常见实现包括:
- 返回默认值
- 返回缓存数据
- 返回静态兜底文案
- 关闭非核心功能模块
- 将同步调用改为异步补偿
6.3 可运行的降级示例
下面示例演示一个商品详情接口:推荐服务正常时返回实时推荐,失败或超时时返回缓存兜底数据。
package main
import (
"context"
"encoding/json"
"fmt"
"math/rand"
"net/http"
"time"
)
type ProductPage struct {
ProductID string `json:"product_id"`
Title string `json:"title"`
Recommendations []string `json:"recommendations"`
Degraded bool `json:"degraded"`
}
func main() {
rand.Seed(time.Now().UnixNano())
http.HandleFunc("/product", productHandler)
fmt.Println("listen on :8080")
http.ListenAndServe(":8080", nil)
}
func productHandler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 800*time.Millisecond)
defer cancel()
page := ProductPage{
ProductID: "sku-1001",
Title: "Go 微服务实战",
}
recs, err := fetchRecommendations(ctx, page.ProductID)
if err != nil {
page.Recommendations = []string{"编辑精选", "基础入门", "稳定性专题"}
page.Degraded = true
} else {
page.Recommendations = recs
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(page)
}
func fetchRecommendations(ctx context.Context, productID string) ([]string, error) {
cost := time.Duration(200+rand.Intn(1200)) * time.Millisecond
select {
case <-time.After(cost):
if rand.Intn(100) < 50 {
return nil, fmt.Errorf("recommend service error")
}
return []string{"推荐 A", "推荐 B", "推荐 C"}, nil
case <-ctx.Done():
return nil, ctx.Err()
}
}
这个例子体现了降级设计的关键思想:
- 主体页面仍然可用
- 非核心推荐信息允许退化
- 用户拿到的是“可接受结果”,而不是 500
七、幂等设计:让重复请求只产生一次有效结果
幂等是写操作稳定性的核心。网络重试、消息重复投递、用户重复点击,都会导致服务端收到重复请求。没有幂等保护,就可能出现重复扣款、重复下单、重复发货。
7.1 什么是幂等
幂等可以简单理解为:
- 同一个请求执行一次和执行多次,最终结果一致
这里的“结果一致”,不是响应文案一模一样,而是业务状态不能被重复破坏。
7.2 幂等 key
最常见做法是让客户端或服务端生成一个幂等 Key,例如:
- 支付请求号
- 订单提交唯一流水号
- 消息唯一 ID
- 业务侧请求去重号
处理逻辑一般是:
- 请求进入服务时先读取幂等 Key
- 查询该 Key 是否已处理
- 已处理则直接返回历史结果或当前状态
- 未处理则执行业务,并记录结果
7.3 唯一约束
仅靠内存去重远远不够,生产环境一般还需要数据库唯一约束兜底,因为多实例部署下,请求可能落在不同节点。
一个典型表示例:
CREATE TABLE payments (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
idem_key VARCHAR(64) NOT NULL,
order_id VARCHAR(64) NOT NULL,
amount BIGINT NOT NULL,
status VARCHAR(32) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_idem_key (idem_key)
);
这样即使并发重复请求同时进入,也只有一个事务能成功插入。
7.4 状态机校验
幂等不仅是“是否处理过”,还包括“状态是否允许重复推进”。
比如订单状态流转可以约束为:
INIT -> PAID -> SHIPPED -> FINISHED
那么:
- 已经
PAID的订单不能再次支付 - 已经
SHIPPED的订单不能回退到PAID
也就是说,状态机校验是幂等和一致性的第二层保护。
7.5 可运行的幂等示例
下面给出一个完整可运行的 HTTP 示例,演示幂等 Key + 唯一记录 + 状态机校验的思路。
package main
import (
"encoding/json"
"fmt"
"net/http"
"sync"
)
type PaymentRecord struct {
IdemKey string `json:"idem_key"`
OrderID string `json:"order_id"`
Amount int64 `json:"amount"`
Status string `json:"status"`
}
type PaymentStore struct {
mu sync.Mutex
records map[string]PaymentRecord
orders map[string]string
}
func NewPaymentStore() *PaymentStore {
return &PaymentStore{
records: make(map[string]PaymentRecord),
orders: make(map[string]string),
}
}
func (s *PaymentStore) CreatePayment(idemKey, orderID string, amount int64) (PaymentRecord, bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
if rec, ok := s.records[idemKey]; ok {
return rec, true, nil
}
if status, ok := s.orders[orderID]; ok {
if status == "PAID" {
return PaymentRecord{}, false, fmt.Errorf("order already paid")
}
return PaymentRecord{}, false, fmt.Errorf("invalid order state: %s", status)
}
rec := PaymentRecord{
IdemKey: idemKey,
OrderID: orderID,
Amount: amount,
Status: "PAID",
}
s.records[idemKey] = rec
s.orders[orderID] = rec.Status
return rec, false, nil
}
func main() {
store := NewPaymentStore()
http.HandleFunc("/pay", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
idemKey := r.Header.Get("Idempotency-Key")
if idemKey == "" {
http.Error(w, "missing Idempotency-Key", http.StatusBadRequest)
return
}
orderID := r.URL.Query().Get("order_id")
if orderID == "" {
http.Error(w, "missing order_id", http.StatusBadRequest)
return
}
rec, duplicated, err := store.CreatePayment(idemKey, orderID, 19900)
if err != nil {
http.Error(w, err.Error(), http.StatusConflict)
return
}
w.Header().Set("Content-Type", "application/json")
if duplicated {
w.Header().Set("X-Idempotent-Replay", "true")
}
json.NewEncoder(w).Encode(rec)
})
fmt.Println("listen on :8080")
http.ListenAndServe(":8080", nil)
}
你可以这样测试:
curl -X POST "http://localhost:8080/pay?order_id=order-1" -H "Idempotency-Key: pay-001"
curl -X POST "http://localhost:8080/pay?order_id=order-1" -H "Idempotency-Key: pay-001"
curl -X POST "http://localhost:8080/pay?order_id=order-1" -H "Idempotency-Key: pay-002"
预期现象:
- 第一次请求成功创建支付
- 第二次使用相同幂等 Key,会返回第一次结果
- 第三次虽然幂等 Key 不同,但订单已经支付过,会被状态机校验拦住
八、把这些能力串起来,才是完整的稳定性方案
实际工程中,这些机制不是孤立存在的,而是要形成一套配合关系:
- 超时:给每个请求设定时间边界,避免无限等待
- 重试:对短暂故障做有限次恢复
- 熔断:在持续故障时快速失败,阻止故障放大
- 限流:在流量冲击时保护系统容量
- 降级:优先保核心链路,牺牲非核心能力
- 幂等:保证重试和重复请求不会写出脏数据
如果只做其中一个,往往不够。
举个常见链路:
- 用户发起支付请求
- 网关先做限流
- 业务服务为支付链路设置超时
- 调用下游支付渠道时做有限重试
- 渠道持续异常时熔断
- 熔断后触发降级,比如提示“支付通道繁忙,请稍后重试”
- 所有支付请求都带幂等 Key,避免重复扣款
这才是一条完整的稳定性防线。
九、总结
稳定性设计不是某一个中间件,也不是某一个框架参数,而是一整套故障控制思维。
在 Go 服务中,你至少应该建立下面这几个基础习惯:
- 每个外部调用都带超时
- 每个重试都有边界,并优先使用指数退避 + Jitter
- 每个关键依赖都考虑熔断保护
- 每个入口都考虑限流
- 每个非核心能力都准备降级方案
- 每个关键写操作都设计幂等机制
当你把这些能力逐步落地之后,系统面对慢请求、突发流量、依赖异常和重复提交时,就不再是“碰运气扛住”,而是“按设计稳定运行”。
📝 版权声明:本文为原创技术博客,转载请注明出处。
如文章中存在错误或不准确之处,欢迎在评论区指正,感谢您的阅读与支持!