错误码设计原则
- 唯一性:每个错误码在整个系统范围内必须唯一,避免混淆。例如,不同微服务中的错误码不能重复,可采用一定的命名空间规则,如
serviceName_errorCode
的格式。
- 可读性:错误码应具有一定的语义,便于开发人员快速理解错误含义。例如,
USER_NOT_FOUND
就比单纯的数字 1001
更易理解。
- 分类明确:按照错误类型进行分类,如业务错误、系统错误、网络错误等。比如业务错误可以用
10000 - 19999
区间,系统错误用 20000 - 29999
区间。
- 扩展性:设计错误码时要考虑到系统未来的扩展,预留一定的区间用于新增错误码。
错误日志记录策略
- 详细信息记录:日志中应包含足够的上下文信息,如时间戳、错误发生的服务名称、请求ID、错误码、错误信息等。例如:
[2024-01-01 12:00:00] [serviceA] [requestID: 12345] [errorCode: USER_NOT_FOUND] User not found in database
- 日志级别:根据错误的严重程度设置不同的日志级别,如
DEBUG
用于开发调试阶段详细记录错误信息,ERROR
用于生产环境记录严重错误。
- 集中管理:将各微服务的错误日志集中收集到一个日志管理系统(如ELK Stack),便于统一查询和分析。
跨服务调用中错误传递与处理
- RPC调用:在Go语言的RPC框架(如gRPC)中,服务端可以在响应中直接返回错误信息和错误码。客户端接收到错误后,根据错误码进行相应处理。例如:
// 服务端
func (s *MyService) MyMethod(ctx context.Context, req *MyRequest) (*MyResponse, error) {
if someErrorCondition {
return nil, status.Error(codes.NotFound, "resource not found")
}
return &MyResponse{}, nil
}
// 客户端
resp, err := client.MyMethod(ctx, &MyRequest{})
if err != nil {
if status, ok := status.FromError(err); ok {
if status.Code() == codes.NotFound {
// 处理资源未找到的错误
}
}
}
- HTTP调用:服务端可以通过HTTP状态码和响应体传递错误信息。例如,返回
404
状态码表示资源未找到,并在响应体中包含错误码和详细错误信息。客户端根据状态码和响应体进行处理。
// 服务端
http.Error(w, `{"errorCode": "RESOURCE_NOT_FOUND", "errorMsg": "resource not found"}`, http.StatusNotFound)
// 客户端
resp, err := http.Get(url)
if err != nil {
// 处理网络错误
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
var errorResp struct {
ErrorCode string `json:"errorCode"`
ErrorMsg string `json:"errorMsg"`
}
json.NewDecoder(resp.Body).Decode(&errorResp)
// 根据errorCode处理错误
}
防止级联故障
- 熔断模式:可以使用熔断器模式(如
circuitbreaker
库)。当一个服务调用失败次数达到一定阈值时,熔断器打开,后续请求不再调用该服务,而是直接返回一个默认值或错误,避免大量无效请求加重故障服务负担。例如:
cb := circuitbreaker.NewCircuitBreaker(circuitbreaker.Settings{
FailureThreshold: 5,
RecoveryTimeout: time.Minute,
})
result, err := cb.Execute(func() (interface{}, error) {
return client.MyMethod(ctx, &MyRequest{})
})
if err != nil {
if err == circuitbreaker.ErrOpen {
// 返回默认值或错误
}
}
- 限流:通过限制每个服务的请求速率,防止过多请求压垮服务,进而引发级联故障。Go语言中可以使用
golang.org/x/time/rate
包实现限流。例如:
limiter := rate.NewLimiter(rate.Every(time.Second), 10)
if limiter.Allow() {
// 处理请求
} else {
// 返回限流错误
}
- 隔离:采用资源隔离的方式,如线程池隔离、信号量隔离等。在Go语言中,可以通过
sync.WaitGroup
和 channel
来实现简单的资源隔离,避免一个服务的异常影响其他服务。例如,为每个服务调用分配固定数量的 goroutine
,当调用失败时,不会占用过多资源。
var wg sync.WaitGroup
semaphore := make(chan struct{}, 10)
for i := 0; i < 10; i++ {
semaphore <- struct{}{}
wg.Add(1)
go func() {
defer func() { <-semaphore; wg.Done() }()
// 服务调用
}()
}
wg.Wait()