后端开发中,异常捕获和日志记录是保障系统稳定性的两大核心环节。很多开发者要么把异常一股脑吞掉不处理,要么日志打得满屏都是却找不到关键信息。真正的最佳实践是:分层捕获异常、结构化记录日志、关联请求上下文、设置合理的告警阈值。这四件事做到位,线上出了问题你能在几分钟内定位根因,而不是翻几百兆的日志文件。
本文从异常分类、捕获策略、日志格式、存储方案、监控告警五个维度,给出一套可直接落地的完整方案。不管你用Java、Python、Go还是Node.js,核心思路都通用。
一、先搞清楚异常的类型和层级后端异常大致分三类:业务异常、系统异常、第三方服务异常。业务异常比如用户余额不足、订单状态冲突,这类是可预期的,应该用自定义异常类明确表达。系统异常比如空指针、数组越界、数据库连接超时,这类是代码缺陷或环境问题。第三方异常比如支付接口返回500、短信服务不可用,这类是外部依赖故障。
关键原则是:不要用一个通用的Exception捕获所有错误。不同层级的异常应该有不同的处理方式。业务异常直接返回给前端友好提示,系统异常需要记录详细堆栈并触发告警,第三方异常需要做降级和重试。
二、分层捕获异常的具体写法以Java Spring Boot为例,推荐用全局异常处理器加局部try-catch的组合方式。全局处理器负责兜底,局部捕获负责精细处理。
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusiness(BusinessException e) {
log.warn("业务异常: code={}, msg={}", e.getCode(), e.getMessage());
return ResponseEntity.status(e.getCode()).body(new ErrorResponse(e.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleUnexpected(Exception e, HttpServletRequest req) {
log.error("未预期异常: uri={}, method={}, traceId={}",
req.getRequestURI(), req.getMethod(), MDC.get("traceId"), e);
return ResponseEntity.status(500).body(new ErrorResponse("系统繁忙,请稍后重试"));
}
}
Python的写法类似,用装饰器或中间件统一处理:
class AppException(Exception):
def __init__(self, code, message):
self.code = code
self.message = message
@app.errorhandler(AppException)
def handle_app_error(e):
logger.warning(f"业务异常 code={e.code} msg={e.message}")
return jsonify({"error": e.message}), e.code
@app.errorhandler(Exception)
def handle_all_error(e):
logger.error(f"未预期异常: {str(e)}", exc_info=True)
return jsonify({"error": "内部错误"}), 500
Go语言没有传统的try-catch,靠返回error值判断,建议封装一个统一的错误处理中间件:
func ErrorHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Error("panic recovered", zap.Any("error", err), zap.String("path", r.URL.Path))
w.WriteHeader(500)
json.NewEncoder(w).Encode(map[string]string{"error": "内部错误"})
}
}()
next.ServeHTTP(w, r)
})
}
三、日志记录的核心原则:结构化和上下文
很多团队的日志就是一句话加一个时间戳,出了问题根本没法查。结构化日志是指每条日志都有固定的字段,比如时间、级别、服务名、请求ID、用户ID、操作描述、错误堆栈。这样日志才能被检索、聚合和分析。
推荐用JSON格式输出日志,不要用纯文本。以Java的Logback配置为例:
<appender name="JSON" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/app.json</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/app.%d{yyyy-MM-dd}.json</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeMdcKeyName>traceId</includeMdcKeyName>
<includeMdcKeyName>userId</includeMdcKeyName>
</encoder>
</appender>
Python用structlog库也能轻松实现:
import structlog
log = structlog.get_logger()
log.info("用户下单", order_id="ORD20240101", user_id=10086, amount=299.0)
log.error("支付失败", error=str(e), trace_id=request.headers.get("X-Trace-Id"))
上下文关联是重中之重。每个请求进来时生成一个唯一的traceId,贯穿整个调用链。从网关到业务层到数据库操作,所有日志都带上这个ID。这样你在日志平台里搜一个traceId,就能看到这个请求完整的生命周期。
四、日志存储和生命周期管理日志不是写到本地文件就完事了。本地文件会占满磁盘,而且多实例部署后日志分散在不同机器上,排查问题非常痛苦。正确做法是:应用把日志输出到标准输出stdout,由容器编排平台或日志采集agent统一收集。
常见的日志收集方案有ELK(Elasticsearch + Logstash + Kibana)、Loki + Grafana、或者云厂商的日志服务。核心指标是:日志保留时间、查询响应速度、存储成本。
建议分级保留:ERROR级别日志保留90天,WARN保留30天,INFO保留7天,DEBUG只在开发环境开启。生产环境绝对不要打DEBUG日志,一条DEBUG日志的信息量可能是INFO的十倍,会直接打爆你的存储和索引。
另外,敏感信息必须脱敏。用户手机号、身份证号、银行卡号、密码这些字段,在记录日志前必须做掩码处理。很多数据泄露事件就是因为日志里明文记录了敏感数据。
五、异常监控和告警机制光记录日志不够,还得有人看。但人不可能24小时盯着日志,所以要配告警规则。核心告警指标包括:错误率突增(比如5分钟内错误率超过5%)、特定异常类型频繁出现(比如数据库连接超时超过10次/分钟)、响应时间P99飙升。
告警要分级:P0是服务完全不可用,电话通知;P1是核心功能受损,短信加即时通讯通知;P2是非核心异常,邮件或工单通知。避免告警风暴,一条规则触发后要有冷却时间,不要同一问题重复发几百条通知。
推荐在代码里埋点关键业务指标,比如订单创建成功率、支付回调成功率,这些指标配合日志一起看,能快速判断是代码问题还是外部依赖问题。
六、容易踩的坑和独到建议第一个坑:catch了异常却不处理。很多开发者写catch(Exception e) {},这等于把问题藏起来了,线上出事根本不知道。至少要打一条ERROR日志,把堆栈信息记录下来。
第二个坑:日志里打太多无关信息。比如每次请求都记录完整的请求体和响应体,这会让日志量暴增。只在ERROR级别或需要排查时才记录完整payload,正常请求记录关键参数就够了。
第三个坑:异常信息对用户暴露太多。返回给前端的错误信息要友好,但不能包含堆栈、SQL语句、内部路径这些技术细节,否则等于给攻击者送情报。
我的建议是:建立团队统一的异常编码体系。比如10001代表参数校验失败,20001代表数据库操作异常,30001代表第三方服务超时。这样前端可以根据错误码做对应的用户提示,后端也能通过错误码快速分类统计。
最后一点,异常处理不是一次性的工作。每次线上事故复盘后,都要回来补充对应的异常捕获和日志埋点。很多团队的异常处理代码是出了事故才补的,这其实是最好的时机——因为你清楚知道哪里需要更细粒度的日志。
七、总结后端异常捕获和日志记录的最佳实践,本质上就是三件事:捕获要分层、日志要结构化、监控要自动化。不要试图用一个try-catch解决所有问题,也不要觉得日志打得越多越好。精准、可检索、有关联,才是高质量日志的标准。把这套体系建好,你的线上问题定位效率至少提升一个数量级。
