🧠 异常处理为什么重要
在微服务架构中,服务之间的网络调用是不可靠的。任何时刻都可能出现网络抖动、超时、服务宕机等问题。简单粗暴地让错误一路传播,就像骨牌一样,会导致雪崩效应。我在做 Kitex 项目时,曾经因为没有正确处理异常,导致一个下游服务的间歇性故障引发了整个链路的级联失败。这次经历让我深刻体会到,异常处理不是锦上添花,而是必须的系统防护。
📖 理解异常分类:第一步是搞清楚错在哪
在处理异常之前,最重要的是搞清楚这个错误是哪一层的问题。我曾经在项目中遇到过这样的场景:支付服务的一个接口突然开始返回"余额不足"错误。我的团队当时没分清异常类型,盲目地配置了自动重试。结果系统重试了几百次,每次都消耗了数据库连接,最后导致整个支付系统瘫痪。事后才意识到,“余额不足"是业务异常,根本不应该重试。这个教训让我深刻认识到:错误分类不清,再多的容错机制也只会火上浇油。
Kitex 将异常分成两大类,每类的处理策略完全不同。
📦 框架异常:RPC 链路的问题
框架异常是指 RPC 调用链路上的错误,这是框架层面的技术问题,与业务逻辑无关。典型场景包括网络超时、服务实例不可用、序列化失败等。
框架异常的系统分类:
网络与连接层:
- 超时(
ErrRPCTimeout)— 网络延迟或处理慢 - 连接失败(
ErrRemoteOrNetwork)— 网络丢包或对端不可达
服务发现与路由层:
- 找不到服务实例(
ErrNoResolver)— 服务发现配置错误或所有实例都不可用 - 负载均衡失败 — 所有实例都故障
协议与序列化层:
- 序列化/反序列化失败 — 客户端和服务端接口定义不一致
- Thrift 协议错误 — 消息格式不正确
服务端应用层:
🔍 如何识别框架异常?
Kitex 在 pkg/kerrors 包中定义了所有的框架异常。我推荐使用三种方法进行精确判断:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| import (
"errors"
"github.com/cloudwego/kitex/pkg/kerrors"
)
resp, err := cli.Echo(ctx, req)
if err != nil {
// 方法一:通用检查 - 快速判断是否为 Kitex 内部框架错误
// 适用于你只关心"是否是框架异常"的场景
if kerrors.IsKitexError(err) {
log.Printf("检测到框架层异常: %v", err)
}
// 方法二:精确判断具体错误类型(推荐用于生产环境)
// 这样可以根据不同错误类型采取精准的处理策略
if errors.Is(err, kerrors.ErrRPCTimeout) {
log.Printf("RPC 请求超时,可以考虑重试(检查网络和下游服务状态)")
} else if errors.Is(err, kerrors.ErrNoResolver) {
log.Printf("服务发现失败,不应该重试,应快速告警(检查配置和服务注册)")
} else if errors.Is(err, kerrors.ErrRemoteOrNetwork) {
log.Printf("网络连接失败,可以考虑切换实例重试")
}
// 方法三:超时错误的快捷检查 - 更简洁但不如方法二精确
if kerrors.IsTimeoutError(err) {
log.Printf("检测到超时类错误")
}
}
|
📄 获取详细错误信息
如果你需要更详细的诊断信息(比如完整堆栈、精确错误类型分类),可以提取 DetailedError 结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| var de *kerrors.DetailedError
if errors.As(err, &de) {
// 获取 Kitex 内部分类的错误类型
errorType := de.ErrorType()
log.Printf("错误分类: %v", errorType)
// 如果是服务端 panic,堆栈信息非常有价值
// 可以快速定位到服务端的具体故障行
if errorType == kerrors.ErrPanic {
log.Printf("服务端发生 panic\n堆栈信息:\n%s", de.Stack())
// 这里应该立即告警,因为这是服务端的严重故障
} else if errorType == kerrors.ErrTimeout {
log.Printf("超时错误的详细信息: %s", de.Error())
}
}
|
🌲 框架异常处理决策树
我把多年的经验总结成了一个决策表,可以帮助你快速判断:
1
2
3
4
5
6
7
| 错误类型 处理策略 原因说明
─────────────────────────────────────────────────────
超时 ✓ 可重试 临时故障,重试可能恢复
连接失败 ✓ 可重试 网络抖动,切换实例重试
无服务实例 ✗ 不重试 配置错误,快速告警
序列化失败 ✗ 不重试 接口版本不一致
Panic ✗ 不重试 服务端崩溃,需要人工介入
|
提示
框架异常的处理原则
根据我的实战经验,这些是必须遵守的原则:
可重试的异常(临时性故障)
- 超时错误:限制重试次数 2-3 次,使用指数退避
- 连接失败:可以切换到其他实例后重试
不可重试的异常(持久性问题)
- 配置错误(如 ErrNoResolver):直接失败并告警,重试徒劳无功
- 序列化错误:说明客户端和服务端的 IDL 定义不一致,重试不会改变结果
- Panic 错误:服务端代码故障,等待修复后才能恢复
监控和告警:不同类型的异常应该触发不同级别的告警
- 临时故障:记录日志但不告警
- 持久性故障:立即告警通知运维
我踩过的坑:曾经对所有框架异常都盲目重试,结果在下游服务故障时引发了大量无效请求,反而加速了故障扩散。
🔢 Thrift 错误码详解
在使用 Thrift 协议时,框架会返回标准的 Application Exception 错误码。了解这些错误码有助于精准定位问题:
| 错误码 | 名称 | 含义 | 处理建议 |
|---|
| 0 | UnknownApplicationException | 未知错误 | 记录日志,排查根因 |
| 1 | UnknownMethod | 未知方法 | 检查 IDL 定义是否一致 |
| 2 | InvalidMessageTypeException | 无效的消息类型 | 检查协议配置 |
| 3 | WrongMethodName | 错误的方法名 | 检查调用的方法名 |
| 4 | BadSequenceID | 错误的包序号 | 可能是并发问题,重试 |
| 5 | MissingResult | 返回结果缺失 | 服务端逻辑问题 |
| 6 | InternalError | 内部错误 | 服务端异常,查看服务端日志 |
| 7 | ProtocolError | 协议错误 | 检查传输协议配置 |
提示
版本说明:
- Kitex < v0.2.0:所有错误码统一上报为 119
- Kitex >= v0.2.0:上报上表中对应的具体错误码(更精准)
⚙️ 业务异常:逻辑层面的错误
业务异常是指业务逻辑层面的错误,比如用户不存在、余额不足、权限不够等。从 RPC 角度看,请求是成功的(返回了结果),只是业务层面返回了错误状态。
为什么要区分业务异常?
这是我在实际项目中遇到的一个坑:我们的监控系统把所有 error != nil 都统计为请求失败,结果监控大盘上错误率飙升到 30%。后来一查,大部分都是"用户不存在"这种正常的业务逻辑,根本不是系统异常。
📐 BizStatusError 接口定义
Kitex 提供了两个核心接口用于业务异常处理:
1
2
3
4
5
6
7
8
9
10
11
12
13
| // 业务异常接口
type BizStatusErrorIface interface {
BizStatusCode() int32 // 业务错误码
BizMessage() string // 业务错误信息
BizExtra() map[string]string // 额外的业务信息
Error() string // 标准 error 接口
}
// gRPC 扩展接口(可选)
type GRPCStatusIface interface {
GRPCStatus() *status.Status // 获取 gRPC Status
SetGRPCStatus(status *status.Status) // 设置 gRPC Status
}
|
🎯 服务端返回业务异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| package main
import (
"context"
"github.com/cloudwego/kitex/pkg/kerrors"
"github.com/cloudwego/kitex/server"
"github.com/cloudwego/kitex/pkg/transmeta"
)
type MyServiceHandler struct{}
func (h *MyServiceHandler) GetUser(ctx context.Context, req *Request) (*Response, error) {
user := findUserByID(req.UserId)
if user == nil {
// 返回业务异常(404 用户不存在)
return nil, kerrors.NewBizStatusError(404, "用户不存在")
}
// 也可以附带额外信息
if !user.IsActive {
return nil, kerrors.NewBizStatusErrorWithExtra(
403,
"用户已被禁用",
map[string]string{
"reason": "违规操作",
"banned_at": "2026-07-01",
},
)
}
return &Response{User: user}, nil
}
func main() {
// 服务端必须配置 MetaHandler 才能传递业务异常
svr := myservice.NewServer(
&MyServiceHandler{},
server.WithMetaHandler(transmeta.ServerTTHeaderHandler),
)
svr.Run()
}
|
🖥️ 客户端处理业务异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
| package main
import (
"context"
"log"
"github.com/cloudwego/kitex/client"
"github.com/cloudwego/kitex/pkg/kerrors"
"github.com/cloudwego/kitex/pkg/transmeta"
"github.com/cloudwego/kitex/pkg/remote/trans/nphttp2/codes"
)
func main() {
// 客户端也必须配置 MetaHandler
cli := myservice.MustNewClient(
"myservice",
client.WithTransportProtocol(transport.TTHeader),
client.WithMetaHandler(transmeta.ClientTTHeaderHandler),
)
resp, err := cli.GetUser(ctx, &Request{UserId: 123})
if err != nil {
// 尝试解析为业务异常
if bizErr, ok := kerrors.FromBizStatusError(err); ok {
// 这是业务异常,不要重试!
switch bizErr.BizStatusCode() {
case 404:
log.Printf("用户不存在: %s", bizErr.BizMessage())
// 返回友好的前端提示
return nil, fmt.Errorf("用户不存在")
case 403:
log.Printf("用户被禁用: %s", bizErr.BizMessage())
extra := bizErr.BizExtra()
log.Printf("禁用原因: %s", extra["reason"])
return nil, fmt.Errorf("用户已被禁用")
default:
log.Printf("业务错误 %d: %s", bizErr.BizStatusCode(), bizErr.BizMessage())
}
return nil, err
}
// 不是业务异常,可能是框架异常(可以考虑重试)
if kerrors.IsTimeoutError(err) {
log.Printf("超时,尝试重试")
// 重试逻辑...
}
return nil, err
}
// 请求成功
log.Printf("用户信息: %+v", resp.User)
}
|
🔌 gRPC 场景下的业务异常
如果你使用 gRPC 协议,并且需要传递更丰富的业务信息,可以使用 gRPC Status Detail:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| // 服务端:使用 gRPC Status Detail
func (*Handler) GetUser(ctx context.Context, req *Request) (*Response, error) {
user := findUserByID(req.UserId)
if user == nil {
// 创建带 gRPC Status 的业务异常
bizErr := kerrors.NewGRPCBizStatusError(404, "用户不存在")
// 添加 Detail 信息(可以是任意 protobuf 消息)
grpcStatusErr := bizErr.(kerrors.GRPCStatusIface)
st, _ := grpcStatusErr.GRPCStatus().WithDetails(&echo.Echo{
Str: "用户ID不存在于数据库",
})
grpcStatusErr.SetGRPCStatus(st)
return nil, bizErr
}
return &Response{User: user}, nil
}
// 客户端:解析 gRPC Status Detail
cli := myservice.MustNewClient("client", client.WithTransportProtocol(transport.GRPC))
resp, err := cli.GetUser(ctx, req)
if err != nil {
if bizErr, ok := kerrors.FromBizStatusError(err); ok {
log.Printf("业务错误码: %d", bizErr.BizStatusCode())
log.Printf("业务错误信息: %s", bizErr.BizMessage())
// 获取 gRPC Status Detail
if grpcErr, ok := bizErr.(kerrors.GRPCStatusIface); ok {
details := grpcErr.GRPCStatus().Details()
if len(details) > 0 {
if echo, ok := details[0].(*echo.Echo); ok {
log.Printf("详细信息: %s", echo.Str)
}
}
}
}
}
|
🔀 在中间件中处理业务异常
业务异常的传递机制比较特殊:Kitex 会将 handler 返回的 BizStatusError 放入 rpcinfo,并向上层返回 nil error。这意味着在中间件中,你不能直接从 error 中获取业务异常。
在中间件中获取业务异常:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| import (
"context"
"github.com/cloudwego/kitex/pkg/rpcinfo"
"github.com/cloudwego/kitex/pkg/endpoint"
)
func MyMiddleware(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, req, resp interface{}) (err error) {
// 执行下游逻辑
err = next(ctx, req, resp)
// err 为 nil,但可能有业务异常
if err == nil {
// 从 rpcinfo 中获取业务异常
bizErr := rpcinfo.GetRPCInfo(ctx).Invocation().BizStatusErr()
if bizErr != nil {
// 记录业务异常日志
log.Printf("业务异常: code=%d, msg=%s",
bizErr.BizStatusCode(),
bizErr.BizMessage(),
)
// 可以根据业务错误码做特殊处理
}
}
return err
}
}
|
在中间件中返回业务异常:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| func AuthMiddleware(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, req, resp interface{}) (err error) {
// 权限检查
token := getTokenFromContext(ctx)
if token == "" {
// 在中间件中设置业务异常
bizErr := kerrors.NewBizStatusError(401, "未授权:缺少 token")
ri := rpcinfo.GetRPCInfo(ctx)
if setter, ok := ri.Invocation().(rpcinfo.InvocationSetter); ok {
setter.SetBizStatusErr(bizErr)
// 返回 nil,框架会自动处理
return nil
}
}
// 继续执行
return next(ctx, req, resp)
}
}
|
提示
业务异常的最佳实践
- 不要重试业务异常:业务逻辑决定的错误,重试 100 次也是一样的结果
- 监控分离:业务异常应该统计为"请求成功,业务失败",而不是"请求失败"
- 统一错误码:团队内部应该维护统一的业务错误码表
- 必须配置 MetaHandler:客户端和服务端都要配置,否则业务异常无法传递
我踩过的坑:
- 忘记配置
MetaHandler,导致客户端拿不到业务异常,全都被当成框架异常处理 - 把业务异常当成临时故障去重试,导致大量无效请求
- 在中间件中直接用
error 判断业务异常,结果永远拿不到(应该用 rpcinfo)
📊 异常分类对比
理解了三类异常后,来看看它们的对比:
| 异常类型 | 典型错误 | 是否重试 | 处理方式 |
|---|
| 框架异常 | 超时、无实例、序列化失败 | 部分可重试 | 根据具体错误类型决定 |
| 业务异常 | 用户不存在、余额不足、权限不够 | 不重试 | 返回业务错误给调用方 |
警告
异常处理的黄金法则
如果你记不住上面那么多细节,记住这一条就够了:
只对临时性的框架异常(如超时、连接失败)进行重试,对配置错误和业务逻辑错误快速失败并记录日志。
盲目重试是系统稳定性的大敌!
🔄 重试策略详解
重试是最常见的容错手段,但重试策略有讲究。乱用重试反而会加重系统压力。
🔎 何时应该重试
提示
只重试临时性异常。对于业务异常(4xx、业务逻辑错误),重试毫无意义。
在 Kitex 中配置重试很简单:
1
2
3
4
5
6
7
8
9
| import "github.com/cloudwego/kitex/pkg/retry"
client, err := echo.NewClient("echo",
client.WithRetryPolicy(
retry.NewRetryPolicyBuilder().
WithMaxAttempts(3).
Build(),
),
)
|
但这样配置后,所有请求都会重试。我们需要更精细的控制:
1
2
3
4
5
6
7
8
9
10
11
12
| // 只对特定异常重试
retryPolicy := retry.NewRetryPolicyBuilder().
WithMaxAttempts(3).
WithBackoffPolicy(retry.NewBackoffPolicy(retry.ExponentialBackoff, 10, 100)).
Build()
// 或者使用更灵活的方式
backoffPolicy := retry.NewBackoffPolicy(
retry.ExponentialBackoff, // 指数退避
10 * time.Millisecond, // 初始延迟
100 * time.Millisecond, // 最大延迟
)
|
🛠️ 退避策略
连续快速重试很可能失败,甚至加重问题。退避策略让重试间隔逐步增加:
1
2
3
4
5
6
7
8
9
10
| // 三种退避策略
// 1. 固定延迟:每次都等待固定时间
retry.NoBackoff // 立即重试(不推荐)
// 2. 线性退避:延迟线性增加
retry.LinearBackoff(10 * time.Millisecond)
// 3. 指数退避:延迟指数级增加(推荐)
retry.ExponentialBackoff(10 * time.Millisecond, 100 * time.Millisecond)
|
指数退避是最常用的,它能有效减少对系统的冲击。比如:
1
2
3
| 第 1 次重试:等待 10ms
第 2 次重试:等待 20ms
第 3 次重试:等待 40ms(上限 100ms)
|
❌ 重试的坑
我遇过几个重试相关的问题,值得分享:
坑1:无限重试导致的级联延迟
1
2
3
4
5
| // ❌ 不好的做法
client.WithMaxRetries(math.MaxInt32) // 无限重试
// 问题:如果下游服务故障,请求会被不停重试,
// 导致超时时间极长,最终引发上游超时
|
坑2:重试放大了流量
1
2
3
4
5
| // 假设有 1000 个并发请求,每个重试 3 次
// 实际流量 = 1000 * 3 = 3000
// 这可能压垮下游服务
// 解决方案:结合熔断器使用
|
坑3:重试导致的非幂等性问题
1
2
3
4
5
6
7
| // ❌ 不安全的操作
// POST /transfer // 转账请求
// 第一次失败,重试
// 可能导致转账两次
// ✅ 解决方案:幂等性设计
// 使用 request_id,服务端去重
|
🚨 熔断器:防止级联故障
当下游服务故障时,继续发送请求是徒劳的。熔断器的作用就是及时"熔断"故障链路,快速失败,避免资源浪费。
🎭 熔断器的三个状态
熔断器像一个智能开关,有三个状态:
1
2
3
4
5
6
7
| 故障率高于阈值
┌──────────────────────┐
│ ↓
CLOSED ──────→ OPEN ──→ HALF_OPEN
(正常) 故障触发 (熔断中) │ 成功 → 关闭
↑ │ 失败 → 打开
└──────────────────┘
|
| 状态 | 含义 | 行为 |
|---|
| CLOSED | 熔断器关闭 | 正常转发请求,计数器清零 |
| OPEN | 熔断器打开 | 快速失败,不转发请求 |
| HALF_OPEN | 半开状态 | 允许少量请求通过,用来探测故障是否已修复 |
🔧 熔断器配置
在 Kitex 中配置熔断器:
1
2
3
4
5
6
7
8
9
10
11
| import "github.com/cloudwego/kitex/pkg/circuitbreak"
client, err := echo.NewClient("echo",
client.WithCircuitBreaker(
circuitbreak.NewCircuitBreakerPolicy(
circuitbreak.WithFailureRate(0.5), // 故障率达到 50% 时熔断
circuitbreak.WithMinimumNumberOfCalls(100), // 至少 100 个请求才开始计算
circuitbreak.WithSuccessThreshold(2), // HALF_OPEN 状态下 2 个成功请求才关闭
),
),
)
|
🎲 熔断策略最佳实践
基于我的经验,这些配置能有效防止故障扩大:
1
2
3
4
5
6
7
8
9
10
11
12
| // 保守的熔断策略(推荐用于关键业务)
policyA := circuitbreak.NewCircuitBreakerPolicy(
circuitbreak.WithFailureRate(0.3), // 30% 故障率就熔断
circuitbreak.WithMinimumNumberOfCalls(50), // 快速反应
circuitbreak.WithTimeoutDuration(30 * time.Second),
)
// 激进的熔断策略(用于容错能力强的场景)
policyB := circuitbreak.NewCircuitBreakerPolicy(
circuitbreak.WithFailureRate(0.7), // 70% 故障率才熔断
circuitbreak.WithMinimumNumberOfCalls(1000), // 让更多请求通过
)
|
📌 Fallback 降级机制
熔断器打开后,请求会立即失败。但我们可以提供一个备选方案来返回一个可接受的结果,这就是 Fallback。
🎪 Fallback 的应用场景
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| // 场景1:返回默认值
func CallWithFallback(ctx context.Context) (*Response, error) {
resp, err := client.Call(ctx, req)
if err != nil {
// 返回缓存数据或默认值
return &Response{
Code: 200,
Data: DefaultData, // 事先准备好的数据
}, nil
}
return resp, nil
}
// 场景2:调用备用服务
func CallWithBackupService(ctx context.Context) (*Response, error) {
resp, err := primaryClient.Call(ctx, req)
if err != nil {
// 尝试调用备用服务
return backupClient.Call(ctx, req)
}
return resp, nil
}
// 场景3:返回降级响应
func CallWithDegradation(ctx context.Context) (*Response, error) {
resp, err := client.Call(ctx, req)
if err != nil {
// 返回一个简化版响应
return &Response{
Code: 200,
Data: DegradedData, // 功能降级但不中断
}, nil
}
return resp, nil
}
|
🚧 实战中的降级陷阱
警告
降级不是为了隐瞒故障,而是为了保证用户体验。一个不合适的 Fallback 可能比直接失败更糟。
我见过这样的问题:
1
2
3
4
5
6
7
8
9
10
11
| // ❌ 不好的降级:返回错误的结果
// 用户看到了旧数据,以为业务正常,实际是在走降级逻辑
// 这导致用户基于错误的信息做决策
// ✅ 好的降级:清楚地标记降级状态
type Response struct {
Code int32
Data interface{}
IsStale bool // 标记数据是否过时
UpdatedAt int64 // 数据的更新时间
}
|
⏱️ 超时控制策略
超时是防止资源泄漏的重要手段。一个没有超时的请求,可能永远得不到响应,导致连接和内存被占用。
🌍 Kitex 中的多层超时
Kitex 支持多个层面的超时控制:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // 1. 连接超时:建立连接的超时
client.WithConnectTimeout(5 * time.Second)
// 2. 读写超时:单次读写操作的超时
client.WithReadWriteTimeout(3 * time.Second)
// 3. RPC 请求超时:整个请求的超时
// 通过 context 控制
ctx, cancel := context.WithTimeout(context.Background(), 10 * time.Second)
defer cancel()
resp, err := client.Call(ctx, req)
// 4. 空闲超时:连接长时间不用时的超时
client.WithIdleTimeout(60 * time.Second)
|
💻 超时配置最佳实践
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // 为不同的请求设置不同的超时
func CallWithDynamicTimeout(ctx context.Context, priority string) (*Response, error) {
timeout := 10 * time.Second
switch priority {
case "high":
timeout = 30 * time.Second // 重要请求给更多时间
case "low":
timeout = 3 * time.Second // 非关键请求快速失败
}
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
return client.Call(ctx, req)
}
// 层级式超时设置
const (
ConnectTimeout = 5 * time.Second // 连接快速失败
RequestTimeout = 10 * time.Second // 整体超时
ReadWriteTimeout = 8 * time.Second // 单次操作超时
)
|
🧪 超时设置的常见错误
1
2
3
4
5
6
7
8
9
10
| // ❌ 错误1:超时设置过长
timeout = 60 * time.Second // 等待一分钟?太久了
// ❌ 错误2:忘记在服务端也设置超时
// 只在客户端设置超时,服务端可能还在处理
// 导致资源继续被占用
// ✅ 正确做法:客户端和服务端都要设置合理的超时
server.WithReadWriteTimeout(8 * time.Second)
client.WithReadWriteTimeout(10 * time.Second) // 客户端稍大于服务端
|
🧩 异常处理的完整方案
现在让我把前面讲的所有机制组合在一起,构建一个完整的容错方案。
🔗 综合配置示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| import (
"github.com/cloudwego/kitex/client"
"github.com/cloudwego/kitex/pkg/circuitbreak"
"github.com/cloudwego/kitex/pkg/retry"
"github.com/cloudwego/kitex/pkg/rpcinfo"
"time"
)
func NewEchoClient() (echo.Client, error) {
return echo.NewClient(
"echo",
// 超时配置
client.WithConnectTimeout(5 * time.Second),
client.WithReadWriteTimeout(3 * time.Second),
client.WithIdleTimeout(60 * time.Second),
// 重试配置
client.WithRetryPolicy(
retry.NewRetryPolicyBuilder().
WithMaxAttempts(3).
WithBackoffPolicy(
retry.NewBackoffPolicy(
retry.ExponentialBackoff,
10 * time.Millisecond,
100 * time.Millisecond,
),
).
Build(),
),
// 熔断配置
client.WithCircuitBreaker(
circuitbreak.NewCircuitBreakerPolicy(
circuitbreak.WithFailureRate(0.5),
circuitbreak.WithMinimumNumberOfCalls(100),
circuitbreak.WithMaxConcurrentRequests(1000),
),
),
// 其他配置
client.WithMiddleware(func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, req, resp interface{}) error {
// 可以在这里添加日志、监控等逻辑
return next(ctx, req, resp)
}
}),
)
}
|
🎬 请求级别的错误处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| func CallWithComprehensiveErrorHandling(ctx context.Context, req *Request) (*Response, error) {
// 设置请求超时
ctx, cancel := context.WithTimeout(ctx, 10 * time.Second)
defer cancel()
resp, err := client.Call(ctx, req)
if err == nil {
return resp, nil
}
// 分类处理不同的异常
switch {
case errors.Is(err, context.DeadlineExceeded):
// 超时异常
log.Warnf("request timeout: %v", err)
// 可以选择降级处理
return getFallbackResponse(), nil
case isCircuitBreakerOpen(err):
// 熔断器打开,快速失败
log.Warnf("circuit breaker open: %v", err)
return nil, err
case isBusinessError(err):
// 业务异常,直接返回
return nil, err
default:
// 其他框架异常
log.Errorf("unexpected error: %v", err)
return nil, err
}
}
func isCircuitBreakerOpen(err error) bool {
return strings.Contains(err.Error(), "circuit breaker is open")
}
func isBusinessError(err error) bool {
// 根据异常类型判断是否为业务异常
return false // 具体实现根据你的业务定义
}
|
💭 异常处理的设计思路
这些所有的机制背后,都遵循一个核心思想:故障隔离和快速响应。
1
2
3
4
5
6
7
| 快速故障发现
↓
熔断 / 降级
↓
释放资源
↓
避免级联失败
|
📋 异常处理的优先级
- 快速失败优于缓慢成功:一个快速的 503 错误好过一个迟到的 200 响应
- 明确的错误优于沉默的失败:清楚的错误信息便于问题诊断
- 自动恢复优于人工介入:利用重试、熔断自动恢复,而不是等人工干预
- 预防优于治疗:通过超时、熔断预防问题扩大,而不是事后补救
✅ 检查清单
部署包含异常处理的 Kitex 服务前,检查以下要点:
🤔 常见问题解答
Q: 超时时间应该设多长?
A: 这取决于你的业务特点。一般来说:
- 关键业务:10-30 秒
- 普通业务:3-10 秒
- 快速业务:1-3 秒
建议从实际的 P99 延迟出发,设置为 P99 的 2-3 倍。然后通过监控不断调整。
Q: 重试会导致流量放大吗?
A: 会的。如果配置不当,会加重系统压力。解决方案:
- 限制重试次数(通常 2-3 次)
- 使用指数退避策略
- 结合熔断器使用
- 对重要操作使用幂等设计
Q: 应该在哪一层处理异常?
A: 最好在调用点处理。理由:
- 了解业务含义,能做出正确的决策
- 便于生成详细的日志和监控
- 便于为不同的调用采用不同的策略
Q: Fallback 应该返回什么?
A: 返回一个对用户有帮助的、不至于中断业务流程的结果。比如:
- 返回缓存数据
- 返回降级数据
- 调用备用服务
- 返回一个特殊标记让上层应用自己处理
📝 总结与建议
Kitex 的异常处理机制很完整,提供了从检测、隔离到恢复的全套工具。但工具再好也需要正确的使用。
我的建议是:
- 先从超时开始。这是最基础、最重要的防护
- 然后加上重试。对临时性异常的自动恢复
- 最后加上熔断。防止故障扩大
- 定期测试。模拟故障场景,验证你的处理策略是否有效
一开始不需要所有东西都配上,而是根据服务的重要性逐步完善。关键是要有异常处理的意识,知道自己的系统在故障面前是否足够健壮。