这种脚本接口一旦上线,往往要支撑大面积流量和长周期运行,平时看似正常的代码,在某次特殊输入、上游抖动或资源耗尽时,才会暴露出真实成色。你会发现,很多程序不是被多么复杂的逻辑打败的,而是被一个空指针、一段异常格式的数据、一个忘了释放的连接、一次没有超时保护的外部调用慢慢拖垮。这类问题,本质上不是"功能有没有实现"的问题,而是"代码健不健壮"的问题。这也是我写这篇理论篇第二篇的原因:把"健壮性"这件事从定义到做法拆开讲清楚,让处于从"能跑"到"可靠"阶段的开发者,能有一套可复用的检查思路。
1. 先从"健壮性"的定义下手:它比"不报错"复杂得多
1.1 一个反直觉的事实:代码不报错不代表健壮
很多人对健壮性有个误解,觉得"程序没崩溃、没抛异常,就是健壮"。这个理解非常危险。一个程序正常运行,可能只是因为当前输入恰好都在预期范围内,并没有经历过真正的考验。
举个例子。你写了一个函数,从订单金额中计算折扣:
python复制def calc_discount(amount):
return amount * 0.9
输入 100 返回 90.0,测试也过了,看起来一切正常。但如果传入的是 None、字符串 'abc'、负数 -50,甚至是一个超大的浮点数 1e308,这个函数会怎样?有的语言直接抛异常,有的语言会返回一个荒谬的结果,还有的会因为参与运算的类型不对,直接把整个进程拖入状态错乱。代码"不报错",很可能只是因为还没碰到让它出错的数据。
真正的健壮性,要能在"输入不符合预期、外部依赖出问题、运行环境产生变化"时,依然保持可控、可恢复、可诊断。它不是你写了几百行防御代码就一定算好,而是要在"防崩溃、能兜底、能自愈"三个层次上都站得住。
1.2 健壮性的三个层次:防崩溃、能兜底、能自愈
我习惯把健壮性拆成三层来看,每一层的目标和检查方法都不一样。
第一层是防崩溃。程序遇到非法输入、空值、资源不足、并发竞争时,不会发生进程级崩溃、死锁或者永久性卡死。这里的核心是边界控制——把所有可能"越界"的地方都拦截下来。比如数组下标访问前判断长度、除法运算前判断分母、解析字符串前判断格式。
第二层是能兜底。即使某个环节确实出错了,系统也要能给用户一个合理的结果,而不是直接白屏、报错或者返回一堆乱码。最常见的兜底手段是降级策略:接口超时了返回缓存数据,库存服务挂了直接提示"稍后重试"而不是把整个下单流程卡死。这里的核心不是"不能出错",而是"出错后依然可用"。
第三层是能自愈。错误发生后,系统能通过重试、回滚、补偿任务等方式自行恢复到正常状态,而不是留下一个永远处于中间态的数据,或者需要人工半夜爬起来重启。比如数据库写入失败时,事务能回滚;消息队列消费失败时,能自动重新入队;调用第三方服务超时后,能有一个后台任务做对账补偿。
用一个生活化的类比来理解:一辆车性能好,是指在平坦路面上能加速快、操控好;而一辆车健壮,是指在烂路上不散架、爆胎后还能靠备胎开去维修站。编程领域也一样,功能需求解决的是"在正常情况下把事做对",健壮性解决的是"在异常情况下不把事情搞砸"。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 输入防线:把"脏数据"拦在第一道门外
2.1 永远不要信任任何输入来源
大部分健壮性问题,根源都是"过度信任"。你信任用户会填合法格式,信任上游接口会返回契约字段,信任配置文件里不会出现空值,信任数据库不会返回脏数据。现实是,这些信任几乎都会被打破。
我做系统时有一条原则:外部输入不可信,内部代码可置信。所谓外部输入,包括所有跨越模块边界的数据,不只是用户通过表单提交的内容,上游服务返回的 JSON、消息队列里收到的消息、配置中心的参数、数据库表里被另一个团队写入的值,全都算。只要不是当前进程内部自己生成并严格校验过的数据,就默认它可能是脏的。
这条原则在工程上表现为两个动作:入口统一校验,出口统一兜底。在请求进入核心业务逻辑之前,先做一次完整的合法性检查,不合法就直接拒绝,或者转换成一个安全的默认值。在下游依赖返回数据之后,也要校验一遍,不能拿到就用。
2.2 类型、范围、格式:三个必查维度
具体校验哪些东西?我总结了三个必查维度:类型、范围、格式。
类型检查,是看数据确实是你预期的类型,而不是一个 None、一个空字符串、一个意外的对象。Python 里经常遇到 NoneType 导致的各种异常,Java 里则是 NullPointerException 重灾区。一个健壮的接口,在第一步就会把这类问题拦掉。
范围检查,是看数据是否落在业务允许的取值区间。年龄不可能是负数,库存不可能是负数,页码不能是 0 或者 -1,单次查询条数不能超过 1000。很多业务事故不是逻辑写错了,而是数据超了边界之后,后续代码基于错误值继续计算,最终产生了荒谬的订单金额、重复的转账、错乱的排名。
格式检查,是看数据是否符合业务约定的格式规范。是邮箱就得像邮箱,是手机号就得是合法的号段,是日期就得是 YYYY-MM-DD,是 JSON 就得能被正常解析。格式错乱的数据,往往会让后续的序列化、反序列化、字符串拼接直接崩掉。
下面这段代码展示了在接口入口处做三层校验的常见姿势:
python复制def register_user(name, age, email):
# 类型检查:None 直接拒绝
if name is None or not isinstance(name, str):
raise ValueError("name must be a non-empty string")
# 范围检查:年龄必须是合理区间
if not isinstance(age, int) or isinstance(age, bool):
raise ValueError("age must be an integer")
if age < 0 or age > 150:
raise ValueError("age must be between 0 and 150")
# 格式检查:邮箱必须匹配基本规则
if not isinstance(email, str) or "@" not in email:
raise ValueError("email format is invalid")
# 通过校验后,才能进入真正的业务逻辑
return create_account(name, age, email)
有些读者会觉得这样写太啰嗦,每个字段都要判断一遍。但实际上,这种"啰嗦"恰恰是健壮性的代价。尤其是那些提供基础能力、被多个业务方复用的函数,入口处多花三行代码做校验,能省掉后面排查问题的一晚上。
2.3 从一次接口事故看"脏数据"带来的连锁反应
之前我排查过一个线上问题。某个交易系统的订单查询接口,偶尔会返回一条错误信息,但这个问题在测试环境无论如何都复现不了。后来查日志发现,上游订单系统在一次版本升级后,把订单号字段从字符串类型改成了数字类型,个别历史订单的订单号里含有字母,转换失败后就返回了 null。
流到下游查询接口时,代码直接用 orderId.toUpperCase() 去处理,直接抛了空指针。因为异常被拦截后错误码映射不对,用户看到的是"系统繁忙",实际上数据根本查不出来。整个问题的根因不是查询逻辑写错了,而是没有对上游返回的订单号做"是否为空、是否合规"的校验。
这类事故在分布式环境里特别常见。你无法控制上游团队什么时候改字段类型、什么时候不兼容格式,唯一能做的就是在自己系统的入口处把好关。数据一旦进入核心流程,它就已经"被信任"了,后续每一个环节都会基于这个错误数据继续加工,问题就会像滚雪球一样越滚越大。这就解释了为什么优秀代码的第一道防线,永远是校验。
3. 异常处理:从try-catch到降级、恢复的完整链条
3.1 三种"典型的异常处理错误"你占了几个
写代码的人没有不会用 try-catch 的,但绝大多数人的异常处理都写得有问题。我总结了三个最常见的错误。
第一个错误,catch 之后什么都不做,或者只打一条日志然后继续。很多代码里能看到这样的写法:
java复制try {
sendEmail(user);
} catch (Exception e) {
// do nothing
}
这种"吞异常"的做法危害最大,因为错误并没有消失,只是被藏了起来。用户没收到邮件,他不会知道是发送失败了;系统也没有任何记录,你事后复盘时完全找不到线索。与其这样,不如直接让错误抛出去,至少能快速暴露问题。
第二个错误,用基类 Exception 一把抓,把所有异常都当作同一类处理。这样做最大的问题是丢失了异常类型背后的业务语义。FileNotFoundException、SQLException、TimeoutException 这三者的处理方式完全不一样,前者可能是配置错误需要报警,中者可能是数据库故障需要切换,后者可能是网络抖动需要重试。全部 catch 之后走同一个逻辑,必然导致某些错误被错误地降级,某些错误被错误地重试。
第三个错误,拿 try-catch 控制正常业务流转。比如用户登录、校验、查询账户信息,都是为了检查密码是否正确,这个功能本身更应该用条件判断或返回值处理,而不是用一个 try { checkPassword() } catch (InvalidPasswordException e) { return false; }。异常机制是为"真正的异常"设计的,用来控制频繁出现的正常分支,既影响性能,也让代码逻辑变得难以阅读。
3.2 可恢复与不可恢复:异常分层才是正确姿势
健壮的异常处理,第一件事是要对异常做分类。我通常把异常分成两类:一类是可恢复的,一类是不可恢复的。
可恢复异常,代表当前这次操作虽然失败了,但系统整体还是健康的,可以通过降级、重试等方式让业务继续往前走。典型的包括:网络超时、下游服务返回 500、缓存击穿、消息暂时不可达。这类异常需要做的不是终止,而是"绕行"。
不可恢复异常,代表系统进入了一种无法继续执行的状态,继续尝试也没有意义。比如:配置文件缺失、数据库驱动加载失败、内存溢出、业务数据出现根本性冲突。这类异常需要做的是快速失败、保留现场、触发告警,而不是硬着头皮重试,把问题越搞越大。
代码层面,可以给异常打上语义标记,或者直接通过自定义异常类来区分:
python复制class RecoverableError(Exception):
"""可恢复异常:网络抖动、临时故障、下游超时"""
class FatalError(Exception):
"""不可恢复异常:配置缺失、非法状态、数据损坏"""
在业务层处理时,看到 RecoverableError 就进入重试或降级分支,看到 FatalError 就记录完整错误上下文并触发告警。这个分类一旦建立起来,整个项目的异常处理观感会提升一大截。
3.3 降级策略:给用户一个"能用的结果"而不是一个"错误"
降级是健壮性里最重要的一环,但也是最容易被忽略的一环。很多程序员写代码时只考虑"成功路径",出错了就往上抛,最终展示给用户的只有"系统异常"四个大字。
真正健壮的系统,应该在部分能力缺失时,依然提供核心价值。电商大促时优惠券服务挂了,系统可以降级为"不使用优惠券但正常下单";推荐服务超时了,首页可以展示一个固定的默认商品列表;实时天气获取失败,就返回上一次成功缓存的数据,哪怕缓存已经过了十几分钟,也比直接报错强。
这里的关键是,降级不是逃避错误,而是主动承担的止损方案。前提是你必须清楚什么能降、什么不能降。比如支付环节就绝对不能降级成"直接置为已支付",这会带来严重的资金问题。降级策略还必须在日志里明确记录"当前处于降级模式,原因是什么,什么时候开始的",否则上线后你根本不知道系统什么时候进入过降级状态。
3.4 恢复策略:重试、回滚与补偿
降级是"当前不能做就先不做",恢复是"过了这阵子要能把事补上"。一个健壮的系统,不能永远是残缺的状态。
恢复策略里最常用的是重试。但重试不是简单地把代码再跑一遍,而是要考虑几个问题:重试的时机是什么?最多重试多少次?重试之间间隔多久?如果重试了依然失败怎么办?不带退避策略的疯狂重试,会在下游服务本来就脆弱的时候,制造出更大的访问压力,最终演变成雪崩。
另一种恢复策略是回滚。在数据库写操作里,如果后面的步骤失败,前面已经执行的步骤需要一起撤销,保证数据回到事务开始前的状态。这就是事务的 ACID 特性。放到分布式场景下,通常用 saga 模式做长事务补偿,每一步记录操作日志,如果要回滚,就执行反向补偿操作。
无论哪种恢复方式,都要留下痕迹。因为恢复动作本身也是系统行为,如果它失败了,后续排查必须知道"发生过一次回滚,回滚到什么程度,是否成功"。没有这些日志,整个恢复机制基本等于白做。
4. 外部依赖要"有限信任":超时、重试与熔断的工程化做法
4.1 不设超时,等于把系统命脉交给别人
任何一次对外部系统的调用,都隐含着一个问题:它多久没返回,我该继续等还是放弃?很多线上事故就是这么发生的——A 服务调用 B 服务,B 服务因为某种原因卡住了,A 服务的线程全部挂在等待上,新的请求不断涌入,线程池被耗尽,最终 A 服务也挂了,接着 C 服务调 A 也卡住,整个链路像多米诺骨牌一样倒下。
超时是控制这种情况最基本的手段。用一句话说,就是"我最多等你多久,等不到就当你不存在"。在配置超时时要区分连接超时和读取超时:连接超时是建立 TCP 连接能等多久,读取超时是连接建立后等待响应数据能等多久。很多初学者的超时形同虚设,因为只设置了连接超时,没设置读取超时,结果连接瞬间建立,但后端一直不吐数据,线程照样被长时间占用。
超时阈值不能拍脑袋,要根据上下游的实际耗时来定。如果上游 P99 耗时是 200ms,那你的读取超时至少得 2 到 3 倍,也就是 400-600ms,否则正常的慢请求也会被误杀。同时要区分不同接口,查询接口可以放宽,写入接口必须严格,因为超时后你不知道服务端到底有没有执行成功,贸然重试可能造成重复写入。
4.2 重试要带退避,否则雪崩给你看
有了超时,接下来自然引入重试。面对网络抖动、瞬时负载高、进程重启中的下游,一次失败不代表永远失败,重试确实能带来明显的稳定性收益,但前提是重试的策略必须正确。
不加控制的重试是最危险的。假设下游已经处于过载边缘,你的服务收到了 100 个失败请求,如果每个请求都立刻重试,那下游要面对的就是 200 个请求,压力瞬间翻倍。下游处理性能进一步下降,又导致更多的超时失败,更多超时又触发更多重试,整个系统在两三轮循环内就能被打崩。这种因为重试导致的流量放大,是分布式系统雪崩最常见的诱因之一。
正确做法是使用指数退避加抖动。第一次重试等待 200ms,第二次等 400ms,第三次等 800ms,同时每次等待时间加上一个随机扰动,防止大量请求在同一时刻集中重试。同时要设置一个合理的最大重试次数,通常是 2 到 3 次,重试之间还要考虑业务属性:查询接口可以放心重试,写操作要带上幂等键,否则一次失败的写请求被重试三次,就可能在对方系统里留下三份重复数据。
4.3 用最简单的熔断开关保护核心链路
超时和重试解决了"单次调用"的健壮性,但它们都治标不治本。如果下游已经雪崩了,你还在按退避策略重试,虽然流量被摊开了,但每一次重试依然在消耗系统资源。
这时候需要熔断机制。它的思路和家里电路里的保险丝一样:当电流超过阈值时,保险丝先熔断,切断电路,保护后面的电器。放在系统里就是:当某个下游连续失败率达到阈值时,熔断器打开,后续请求不再真正调用下游,而是直接返回一个快速的错误或降级结果。经过一段冷却时间后,再放少量请求试探下游是否恢复,如果恢复则关闭熔断,否则继续保持打开状态。
实现一个简单的熔断器并不需要引入额外框架,核心就是三个状态、两个阈值。三个状态是关闭、打开、半开,两个阈值是失败率和时间窗口。下面是一个极简的例子:
python复制class CircuitBreaker:
def __init__(self, fail_threshold=5, cooldown_seconds=10):
self.fail_threshold = fail_threshold
self.cooldown_seconds = cooldown_seconds
self.fail_count = 0
self.state = "closed"
self.last_fail_time = None
def call(self, func):
if self.state == "open":
if time.time() - self.last_fail_time > self.cooldown_seconds:
self.state = "half_open"
else:
raise DownstreamUnavailableError()
if self.state == "half_open":
self.state = "closed"
self.fail_count = 0
try:
result = func()
self.fail_count = 0
self.state = "closed"
return result
except Exception:
self.fail_count += 1
if self.fail_count >= self.fail_threshold:
self.state = "open"
self.last_fail_time = time.time()
raise
熔断器的好处是,它把"故障隔离"变成了系统行为的一部分。下游出了故障,你的系统不会被拖死,而是快速失败并进入降级逻辑;下游恢复了,系统也能自动恢复,不需要人工干预。这才是真正意义上的自愈能力。
5. 资源管理:最容易漏掉但最致命的健壮性暗坑
5.1 资源泄漏,是典型的"慢性死亡"问题
异常处理、超时、熔断这些话题在讨论健壮性时往往被反复提及,但资源管理这个方向经常被忽略,直到生产环境出故障。这里的资源包括文件句柄、数据库连接、网络连接、内存、线程等。它们有一个共同特点:数量是有限的,用完之后必须归还,否则会导致后续申请资源的人拿不到。
一个经典的泄漏场景是:代码里打开了文件流,但读取过程中发生了异常,close() 语句没有执行,文件句柄一直占着。在高峰期,请求量大,很快就把进程允许打开的文件数撑满,后续所有需要打开文件的请求全部失败。数据库连接也是一样,如果每次查询都新建连接而不归还连接池,连接池很快就会被耗尽。
最让人头疼的是,这类问题往往是慢性发作的。程序刚上线一两天完全正常,随着运行时间增加,资源一点点被消耗,直到某个临界点突然崩溃。这种故障在监控不到位的情况下非常难定位,因为它不是某个明确的时间点发生的,而是循序渐进地恶化。
5.2 用 with / finally 确保资源释放
资源管理的正确做法很简单:申请资源和使用资源必须成对出现,释放资源的代码一定要写在 finally 块中,或者使用语言提供的自动资源管理语法。
Java 里的 try-with-resources,Python 里的 with 语句,都是为这个目的设计的。以 Python 为例:
python复制def read_first_line(file_path):
with open(file_path, "r", encoding="utf-8") as f:
return f.readline()
用 with 打开文件后,无论代码块内部是正常结束还是抛了异常,文件都会在离开 with 块时被自动关闭。这比手动调用 close() 要可靠得多,因为你不需要花精力去保证每条执行路径上都写了 close()。
数据库连接池的使用也有类似原则:从连接池里拿连接,用完必须归还。下面这段代码就是典型的反面教材:
python复制def get_user(uid):
conn = pool.get_connection()
cursor = conn.cursor()
cursor.execute("select * from users where uid=?", (uid,))
return cursor.fetchone()
# conn 从未归还
正确写法是把连接释放放在 finally 中,或者在上下文管理器里处理:
python复制def get_user(uid):
with pool.get_connection() as conn:
with conn.cursor() as cursor:
cursor.execute("select * from users where uid=?", (uid,))
return cursor.fetchone()
除了代码层面的规范,运维层面也要有配套手段。在 Linux 下可以用 lsof -p <pid> | wc -l 查看进程文件句柄数是否持续增长,MySQL 里可以用 show processlist 检查连接数,Java 应用可以用 jstat 检查 GC 和内存变化。资源问题通常不靠"感觉",而靠这些指标曲线来暴露。
5.3 关注静态资源和线程池的清理
文件、连接是显式的资源,还有一类隐式资源更容易被忽视:全局缓存、线程池、定时任务。
全局缓存如果不设上限,不断往里写数据,占用内存就会无限增长。很多"内存泄漏"问题,其实是程序员的缓存设计不当导致的。解决办法要么给缓存设置容量上限,用 LRU 等淘汰策略,要么给缓存条目设置过期时间。
线程池的使用也一样。如果一个任务执行时长很长,而且任务提交的速度远大于执行速度,线程池里的任务队列就会堆积。这时候,系统不会马上崩溃,但请求响应时间会越来越长,最终表现为整个服务无响应。要防止这种情况,一方面要设置线程池的最大线程数、队列长度和拒绝策略;另一方面,在提交任务时也要考虑:这个任务如果一直不结束,是否应该设置超时中断机制。定时任务和后台任务运行时,要确保它们在异常终止后能被及时发现和重启,避免服务看起来活着,实际上核心调度早就不工作了。
6. 用日志和测试给健壮性上"双保险"
6.1 日志不是打了就行,要带上可搜索的上下文
写健壮代码的人,多数都有过深夜被叫起来排查线上问题的经历。那时候你唯一能依赖的就是日志。如果你的日志只有一句"处理失败",是什么数据导致的、发生在哪个环节、当时的参数是什么,全都不知道,那这个日志基本等于没打。
合格的日志至少要包含几个要素:时间戳、请求标识、业务关键参数、异常堆栈、当前处理的节点或流程标识。分布式系统里尤其需要请求 id,同一个请求经过网关、服务 A、服务 B、数据库,日志系统里通过这个 id 就能串起整条链路。我习惯在每个接口入口处生成一个 trace_id,通过日志上下文传递,所有业务日志都带上这个标识。这样排查一次调用问题,只需要按 trace_id 查一遍日志就清楚了。
日志级别也要用好。debug 用于本地开发,info 记录正常业务操作,warn 记录可能存在问题但系统能兜住的情况,error 记录真正需要人工关注的错误。很多团队会把 warn 和 error 混为一谈,导致 error 日志量巨大,真正有价值的信息反而被淹没。有一个思路可以参考:error 级别的日志,必须能回答"这次失败是否需要立即处理、影响面是什么、有没有自动恢复机制"。如果回答不了,说明日志还不够完整。
6.2 边界测试:你以为的"正常范围"往往不完整
健壮性光靠写代码时的意识还不够,还得靠测试去验证。边界测试是检验"输入防线"是否有效的最直接手段。
所谓边界值,就是那些恰好落在合法区间边缘的值,以及那些刚好越过边缘的值。拿年龄举例,合法区间是 0 到 150,那么测试数据至少应该包含 -1、0、1、149、150、151、None、"abc"、1.5 这几种。别嫌多,很多线上事故恰恰发生在这些"差一点"的位置上。
边界测试的思路可以固化成一张清单,每次新写一个接口或函数时逐项过一遍:
- 输入为 null / None / 空字符串时,代码是否按预期处理?
- 输入为最大值、最小值、负数、超长字符串时,是否存在溢出、截断、OOM 风险?
- 输入集合为空、只有一个元素、元素数量极大时,循环和递归是否能正常结束?
- 输入包含特殊字符、HTML 标签、SQL 关键字时,是否存在注入或转义问题?
- 并发调用同一个函数时,是否存在共享状态竞争?
除了边界测试,模糊测试也值得尝试。给它输入一些随机的、半合法的数据,观察程序是否崩溃、是否产生不可预期的行为。模糊测试的初衷不是代替边界测试,而是作为补充,去覆盖你"没想到"的那些输入组合。很多成熟的库都会内置 fuzz 测试工具,用来发现异常处理路径上隐藏的缺陷。
6.3 故障注入:把"万一"变成"经常"
比边界测试更进一步的做法,是故障注入。它主动制造故障,验证系统在故障发生时是否能像设计的那样降级、恢复,而不是等到线上真的出了问题才去验证。
最简单的故障注入实践是,在测试环境把某个下游接口改成一个随机返回超时或 500 的 mock,然后观察你的系统是否依然能返回降级结果、是否能正确记录日志、是否在冷却时间后自动恢复。更进一步,可以随机杀掉系统中的某个节点,验证整体链路是否有冗余,或者对磁盘、CPU、内存做施加压力,看系统在资源紧张时的表现。
我之前在一个核心交易链路里做过一次这样的演练:把订单服务的数据库连接池手动缩到 1,然后同时发起 50 个并发请求。系统的正常行为应该是,请求排队或快速失败并返回"繁忙告警",而不是大量线程阻塞导致整个服务无响应。那次演练果然暴露了一个问题:某些代码路径里的数据库操作没有设置查询超时,导致连接池耗尽后所有请求都在无限等待。这次故障注入,直接催生了一个全链路的 SQL 超时治理项目。
这个案例说明,健壮性不能只靠"觉得没问题",它必须被验证过才算数。故障注入就是一种低成本的验证方法,能在真正出事前,把那些隐藏的薄弱环节挖出来。
6.4 用代码诊断工具做日常兜底
在人工测试之外,静态代码扫描工具也能给健壮性提供一层保障。比如有的工具会检查出空指针解引用、未关闭的资源、异常的 catch 吞掉、明显越界访问等措施。这类工具的价值在于,它们可以在代码合并之前就拦截一部分明显的健壮性缺陷,减少人肉 code review 的负担。
不过要明白,扫描工具只能发现问题,不能替代设计和测试。它更擅长找"已定义的错误模式",而对那些需要结合业务语义判断的场景帮助有限。比如,某个上游返回值需要降级处理,这是工具无法判断的。真正有效的组合拳是:设计时把边界问题想清楚,编码时把异常处理和资源管理做规范,运行时用日志和监控观察趋势,再配合测试和诊断工具做自动防线。这四层都做到,健壮性才不会停留在口号层面。
如果让我从所有经验里总结一句,那就是:写代码时多问自己一句"如果这里输入非法、外部超时、资源不足,会怎样",很多事故其实可以在上线前就避免。这个习惯一旦养成,你写出的代码质量会有很明显的提升。
