系统的可靠性比性能、高并发更重要。没人希望整天分析错误数据、修复错误数据。任何一个错误都可能导致客户的损失。
成熟可靠的系统和不可靠系统之间的差别很大程度取决于:异常的正确处理。经验丰富的程序员和经验不丰富的程序员之间的差别是是否考虑到并正确处理可能发生的各种异常。这和处理用户输入数据的验证非常相似。
可以发现即使是运行多年,知名的互联网公司的大型系统都或多或少存在异常处理的问题。
- Try catch 的内部实现
- 异常必然发生
- 异常处理的性能考虑
- 几个理念:Fail fast vs Retry vs Let it crash
- 异常和错误的类型
- 守门员:全局异常处理
- 异常处理总结和一些原则<
- 一些有用的资料
Try catch 的内部实现
这篇文章有比较简单的解释。
异常必然发生
原因很多种:比如,网络不稳定、硬件故障、软件 BUG 等等。很多程序员如果不接触系统运维的话,很难考虑到并处理这些异常。这是问题原因的一部分。从这个角度看全栈工程师还是非常有用的。
异常处理的性能考虑
Donald Knuth: “premature optimization is the root of all evil”.
考虑到这个问题其实就已经进入的误区。该交给编译器的事情还是不要过多考虑了。
应该使用 try catch 的情况
说道 try catch 的性能,可能很多人会想到这会让程序变慢。其实不然,虽然它对应用性能的影响取决于编译器的实现,但是大部分情况下不会影响不抛异常情况下的执行速度,但是会有稍微内存占用的提升。并且,假如异常很少触发,使用 try catch 取代错误码然后 if 判断的方式可以提升速度,因为你不需要没次都判断这个错误码。使用异常代替判断还可以让程序更可读。
不应该使用 try catch 的情况
异常处理不应该取代 if else 用来做流程控制。
几个理念:Fail fast vs Retry vs Let it crash
Let it crash & Fail Fast
可以让错误停止扩散,保证整个系统的健康。可以重置系统状态、清理变量和内存占用等等。让系统恢复到原始状态。这对于恢复系统异常非常有用。
重试 Retry
在操作幂等的情况下,假如第一次操作失败,重试是处理异常的很好的方式。但是注意自动重试在处理不当的情况下会引起操作数量持续增长导致系统雪崩。
异常和错误的类型
第一种划分:
1. 编译错误:很明显,易处理。
2. 应用逻辑错误:最难发现问题;(类型系统有助于避免逻辑错误。Let it crash 主要针对逻辑错误和 BUG。)
3. 运行时错误:终止进程。
4. 生成的异常:throw 异常,尽早发现问题。
第二种划分:
1. 内部异常:可以内部恢复的异常;一般记录日志或打印相关信息告知用户错误。
2. 外部异常:Error,只能从外部恢复;一般打印堆栈信息并退出。还包括 RuntimeException, 软件 BUG,逻辑错误,错误使用 API。比如 NullPointerException。Let it crash 就是主要针对逻辑错误。这需要人工介入及时修复 BUG 。
异常区分的简单原则:
如果应用可以从异常中自动恢复,则为内部异常。如果不能,则为外部异常。
守门员:全局异常处理
这是处理不可预见异常的非常有用的一种方式。应用初成,难免考虑不到所有需要处理的异常。可以用全局异常处理记录日志、发现问题。
Node.js:
process.on('uncaughtException', function(err) { console.log(err) });
Java:
package cn.eood.blog.example; public class ExceptionHandlerExample { public static void main(String[] args) throws Exception { Handler handler = new Handler(); Thread.setDefaultUncaughtExceptionHandler(handler); Thread t = new Thread(new MyThread(), "My Thread"); t.start(); Thread.sleep(100); throw new RuntimeException("Thrown from Main"); } } class Handler implements Thread.UncaughtExceptionHandler { public void uncaughtException(Thread t, Throwable e) { System.out.println("Throwable: " + e.getMessage()); System.out.println(t.toString()); } } class MyThread implements Runnable { public void run() { throw new RuntimeException("Thrown From Thread"); } }
PHP:
function exception_handler($exception) { echo "Uncaught exception: " , $exception->getMessage(), "\n"; } set_exception_handler('exception_handler'); throw new Exception('Uncaught Exception'); echo "Not Executed\n"; ?>
异常处理总结和一些原则
- 异常处理是互联网架构中很重要的一部分。需要在架构中集成到日志系统、监控系统、通知系统。(可能很多公司的“架构”还没有这几个必须的系统,它们是可靠性的基础。)
- 正确区分内部处理异常和外部处理异常。能内部处理的内部处理;否则 Fail fast 交由外部处理。
- 异常可以大致分为:系统层异常和应用层异常。正确区分系统层异常和应用层异常。这也和内部处理和外部处理对应。
- 不要用异常处理掩盖错误,不用吞噬异常,至少需要记录日志,以供查看分析。
- Web 系统异常情
每个架构师设计架构的时候都应该考虑的几条原则:高性能 High Performance,可扩展 Scalable,可维护 Maintainable。
网站前端的架构也是如此。
可扩展
前端开发也从服务器端开发借鉴了很多东西:比如 MVC 的分层,小内核和模块化。
MVC 的抽象
如果服务器端对 MVC 抽象隔离类似,在浏览器端也可以区分 M-V-C。比如 Backbone 之类的 JS 库。
小内核和模块化
小内核和模块化的设计。
每个模块可以独立存在,多个模块可以并存而不相互依赖和影响(低耦合),模块可以复用。模块直接可以通信调用。
模块化架构的典范就是 Drupal,如果不采用模块化的架构,Drupal 不会发展得如此庞大。
由于前端的特性,考虑到加载文件的大小和数量,一般更应该权衡是否用第三方库。
可维护
协同开发需要有统一的规范,以利于不同人方便的沟通交流。建立 HTML,CSS 和 JS 的 Code Style Guide。
保持 HTML CSS JS 的相互独立。可配置,保持配置文件或者变量与程序的独立。
使用自动化工具: 测试,构建,压缩,部署的自动化。
高性能
浏览器和服务器端的不同在于:所有执行都是单线程的,即任何耗时的执行都会 Block 整个浏览器。
一般 100ms 是用户感觉不到执行延迟的极限,不过推荐控制在 50ms 以内,否则需要给用户等待的反馈信息。
浏览器在下载,解析,执行 内嵌JS文件的时候会 Block 后续的渲染。所以一般吧 JS 文件放在页面的尾部,
或者采用异步加载 JS 文件的方式。