文章转载自:http://www.cnblogs.com/lovecindywang/archive/2012/12/23/2829828.html
后端架构:
设计层面:
分层架构:
分层架构的根本目的就是为了职能分离,所以对于小项目,为节约开发成本可不需要架构分层。分层架构的最经典架构为三层:表现层、业务逻辑层、数据访问层,分别处理面向用户的、面向逻辑处理的和面向数据库存取数据的三大关注点。
在分层架构中除了分层之外还需要对每一层都提取出自己需要的模型,比如表现层的视图模型、业务逻辑层的业务模型或领域模型以及数据访问层的实体,当然如果还有服务层的话还有服务层的契约或数据传输对象。
我们要明确,每一层都只是做自己层职责上的事情,不应该去越权,并且要做的事情一定要做好。例如数据判断,异常处理等,每层都要完善自己的职责,架构才够健壮。
高内聚低耦合原则:
- 任何一个方法都检查一遍是不是只做了一件事情。如果一段代码又在做界面展现,又有SQL语句,或是又在处理员工的工资又在处理员工的考勤,那么往往表示这段代码做了过多不是自己的事情了。它或许应该分成两个方法,甚至是不同类型的两个方法。(职责单一原则)
- 任何一个类型都检查是不是只做了一件事情。这是非常重要的一个检查,如果一个类型做了太多的事情,很明显你没有对类型的职责进行一个明确的划分(当然这个类型就是来负责协调的作为门面类型例外),请检查类中的所有功能是不是符合类名,如果一个类型叫做NetworkingAdapter而其中有处理XML的代码的话显然不合适,是否应该使用其它继承的类型来处理XML,或者是使用类似于策略模式来处理不同的数据?(命名清楚规范)
- 任何一段代码都检查是不是只在一个地方出现。这也是非常重要的一个检查,往往我们可以用这一条金标准来做重构。如果一段代码在多处出现要么这是一段和业务无关的代码,可以AOP解决,要么就有大问题了,一段相同的业务逻辑在多个地方出现,一旦业务逻辑有改有多少人知道要改两个甚至更多的地方,很明显应该把代码提取出来封装在一个地方。(最小职责原则)
- 任何一个类型都检查是不是依赖过多的类型。除非是门面类型否则一个类型应该不会依赖过多的类型,暂且不说类型的依赖可以通过IOC来解决,如果一个类型依赖了过多的类型,它就会和其它类型进行比较多的强耦合。可以考虑使用门面模式来梳理类型之间的关系,尽量减少和太多的类型之间发生关联。
- 如果一个类型都检查是不是依赖的类型也同样依赖自己。如果产生这种双向依赖的话,建议使用中间人来处理两个类型之间的依赖,不要让两个类型相互调用相互依赖。在有的时候我们可以考虑类型A把数据写到一个地方,类型B从这个中间点去获取数据然后进行处理处理后还是把结果存在这个中间点,类型A再从这个中间点获取结果。这样的话A和B其实没有很强的依赖,大家都依赖中间点获取结果,即使把B替换成C只要它最终输出的东西是大同小异的那么A就不需要修改。
设计模式:
- 应该对每一个设计模式都了解,然后你自然可以想到在合适的时候使用合适的模式。
- 不要去滥用设计模式,好的设计模式可以增强代码的高内聚低耦合也可以让代码对扩展友好,但设计模式滥用可能会导致代码的性能问题以及不必要的编码。
- 作为开发gof的设计模式应该熟读,只是读还不够,自己想办法去在今后的编码过程中体会应用设计模式,如果一个设计模式没用那么他就在书上,如果用了那么他就在你脑子里。
面向对象:
- 可以从设计模式入手,设计模式是OO的一个总结,学习了设计模式有助于理解OO的三大理念。
- 可以多阅读优秀的框架或类库的代码,优秀的开源框架一定是对扩展友好的,因此它的设计往往是非常OO的,通过阅读改造开源代码可以提升OOD的能力。
- 多重构多思考,根据上面提到的高内聚低耦合中说的那些问题想办法去重构代码,你会发现很多优化只能通过OO的手段进行,思考重构然后再重构来进步。
系统层面:
- 高可用性
所谓高可用性也就是通过避免单独故障加上快速故障转移实现一旦某台物理服务器出现故障能实现故障快速恢复。一般来说,可以采用两种方式,如果可以做业务可以做负载均衡则通过负载均衡实现集群,然后针对每一台服务器进行监控,一旦发生故障则从集群中移除;如果业务只能有单点入口那么可以通过实现Standby机加上虚拟IP机制,实现Active机在出现故障之后虚拟IP转移到Standby的快速故障转移。一般可以使用KeepAlived或HeartBeat实现高可用(当然,硬件手段实现也可以,这里不展开讨论)。
- 高伸缩性
所谓高伸缩性也就是横向伸缩性,通过扩展机器数量而不是增加机器配置来实现系统处理能力的扩容。负载均衡就是典型的高伸缩性的架构,此外还可以把业务进行拆分由不同的服务器实现不同的业务也是一种伸缩性的方案。一般来说对于没有状态的Web服务比较容易实现负载均衡,而数据库层面,特别是数据库的写操作比较难以实现横向伸缩。一般可以使用LVS或HAPROXY实现负载均衡(当然,硬件手段实现也可以,这里不展开讨论)。
- 反向代理
对于网站前端一般会使用反向代理来为服务器实现缓存和负载均衡的工作。这个缓存不同数据缓存,是把用于输出的HTML或HTML片段进行内存或磁盘的缓存,以减少Web服务器的压力。一般可以使用SQUID或VARNISH实现反向代理。
- CDN
为了进一步增加网站页面的访问速度,可以为静态资源、图片甚至动态资源进行CDN。CDN供应商在全国的骨干节点都设有服务器,可以让全国各地的用户都可以高速访问到这些静态资源,当然静态资源第一次访问是需要通过我们的静态资源服务器的,之后就会在CDN的服务器上进行一段时间的缓存。CDN不但可以加速客户端的访问速度还可以减少服务器的压力。如果网站的页面又实现了CDN,又通过反向代理缓存,那么更新起来就会比较麻烦,因为这样的话可能会在客户端、CDN服务端以及反向代理端都有缓存,此时需要通过一些工具来判断到底是哪个环节有缓存。
- 操作系统参数
在拿到服务器之后,操作系统的配置可能是默认的,此时应该检查操作系统是否修改了诸如TCP连接数量、最大文件句柄数量等系统参数,避免因为操作系统的限制不能发挥程序的最佳性能。
- 服务器优化
不管是诸如Nginx或Apache的Web服务器还是诸如Tomcat或JBoss的Java服务器,都有一些参数设置,需要根据服务器的配置结合网上的一些最佳实践进行一些参数的修改,往往默认配置是不适合配置比较高的服务器的。比如,Java是一种基于垃圾回收的语言,过大的堆可能会导致垃圾回收的时间过长,因此往往会针对大内存的服务器配置多个32bit的JVM而不是统一使用一个64bit的JVM并分配16GB以上的内存给它。我们需要明白服务器中相关参数的意义,有理有据进行参数设置。
语言层面:
- 运行时元数据获取
所谓运行时元数据获取也就是在程序运行的时候通过代码动态获得类型、方法、属性的信息,然后可以动态获得属性的值,执行方法等等,在有的语言中称为反射。反射不一定是高效的,但是在写框架程序的时候反射是一种很有用的技术,并且反射的性能开销往往是可以通过诸如缓存等手段来最小化的。比如在ORM中,根据实体类的信息动态获得所有的属性,然后取得其值,生成要到数据库中执行的SQL语句。理解反射熟练掌握反射的使用以及性能优化是编写框架类代码很重要的一点。
- 错误处理
任何后端语言都有其错误处理机制,.NET和Java的异常处理机制也被更多的语言所吸纳,虽然每种语言的异常机制不一定都相同,但是也是大同小异。错误处理和异常处理的原则个人总结如下:
- 不要把错误的结果吃掉,不要把捕获的异常吃掉。所谓吃掉也就是针对错误结果或异常不做任何的处理,这样就没有人知道这个异常的存在这是很危险的。其实所谓异常就是代码不能执行代码语句本身所隐含的含义,比如有一个方法从名字上看是创建文件的,那么不能创建文件的时候方法应该是要抛出异常的,因为它办不了它应该办的事情,作为类库的编写者应该要这么做,有的时候出了问题我们会很迷茫为什么会出问题,作为类库的使用者应该在这样的方法周围进行异常处理,并且记录这样的异常,那么我们就很容易通过异常信息来找到根源问题,而不是去猜。有的时候根本没办法通过猜来解决问题,因为语言的类库是和操作系统打交道的一首环节,只有类库才知道操作系统有什么问题,比如是因为权限原因不能创建文件还是因为磁盘损坏,如果把这个异常信息舍弃的话是很难知道根本原因的。
- 具体怎么进行错误处理不能一视同仁需要看情况而定的。并且一般不建议直接捕获最大的异常,能细化的尽量细化,捕获所有异常意味着不能进行处理。一般情况下可以记录日志,或是重新包装后抛出,或是很明确地进行错误处理。
- 未处理的异常往往会随着调用栈往上升,升到最上层如果Web服务器发现异常还是未处理的话会导致各种颜色的错误页面。个人认为出现错误页面不一定是坏事情,很多人喜欢把所有的异常都吃掉不出现错误页,这不是解决问题的根本办法,根本办法是找到异常出现的原因,从代码角度解决它,而不是掩耳盗铃。当然,错误页是不应该让终端用户看到的,应该替换成友好的页面,在这个页面上可以什么都不写,也可以写一个异常ID,如果用户觉得这个操作是很要紧的话可以拿这个异常ID来和客服反馈,阐述其操作过程帮助我们解决问题,当然这个大前提是能把所有的异常都记录到数据库或方便查询的文本文件中。很多框架都具有统一收集未处理异常的入口点,在这里我们可以统一把未处理的异常汇总记录。
- 垃圾回收
除了C/C++之外大多数脚本语言和具有虚拟机的编译型语言都是自动垃圾回收的。虽然通过垃圾回收机制不需要手动来处理对象的释放,但垃圾回收不是万能的,可能是会导致内存泄露问题的。当然,这里说的内存泄露和C/C++的内存泄露其实不太一样,一般而言垃圾回收通过向上回溯对象引用根来判断对象是否可以被回收,如果我们程序写的不当导致对象始终存在引用根的话可能就会导致对象不能得到释放产生不断的内存膨胀,虽然说对象是存在指针指向的,并不是没有任何指针指向的野对象,但是其实我们是遗忘这个对象的,因此也可以说是是有内存泄露。因此,即使有垃圾回收我们也要特别注意一下静态对象的使用,是不是一定要是静态的,是不是可以是弱引用的,尽量避免使用声明周期过长的根。
- 多线程
大多数的编译型语言都支持用代码编写多线程,多线程是一个很有用的技术,可以用来让主线程、UI线程不因为其它操作停止响应,可以用来同时执行多个任务来提高任务执行的速度,充分利用多核CPU的处理能力,当然也可以把一个任务直接分割成多个任务并行执行,实现有多少CPU就可以执行多快。由于在编码的时候不能预测多线程的程序在执行时候的调度,所以我们在编码的时候就要特别小心多线程带来的问题:
- 如果多线程同时访问一个资源,比如同时对一个数字进行累加,那么很可能不到达到我们的预期。
- 类库所提供的类型不一定都是线程安全的,我们在使用的时候务必要阅读相关的资料确认是不是线程安全的,如果不是那么我们要通过诸如锁之类的手段来确保能够线程安全,否则很可能在调用类库的时候会出现异常或者说不能实现正确的代码。
- 多线程的程序调试起来也是比较麻烦的,因为多个线程可能会穿插执行不同的代码,此时我们可以通过记录日志、挂起某些线程或是临时切换为单线程程序来增加代码的调试性。
- 我们在编码的时候需要意识到什么是线程,一个线程所消耗的资源有多少,因为不要认为多线程可以提高效率,什么操作都用一堆线程来做,随便开启很多线程,这是得不偿失的,线程虽然比进程的代价小但是代价也不是这么小,因为每一个线程都有一个不小的线程栈,因此如果你发现你的程序开启了上千个线程的话,那么或许要想一下这样是否合理了。
- 在开启多线程的时候千万要记得不要遗忘这个线程,不要让线程在哪里什么都不干空循环,对于后台任务类线程,可以在一个地方记录我们程序中开启的线程,对于其它可以结束的线程,要确保线程中的代码能正常结束。
- 如果线程中的代码出现异常的话,大多数运行时或虚拟机会认为这是一个比较严重的问题,因为可能会导致整个系统中的状态不能保持一致,比如涉及到钱的系统这就是一个严重问题了,因此对于这种情况宁肯直接终止整个应用程序的进程也不要让这个问题没有人发现,错误的状态得到扩散,如果你觉得线程其实只是做一些无关重要状态的操作的话,务必确保线程不会出现未处理的异常。
- 代码生成
代码生成的作用很多,比如可以通过代码生成来减少我们代码的书写量,也可以通过代码生成实现AOP之类的切面操作。一般而言有两种代码生成的方式:
- 动态生成:代码是在程序运行的时候动态生成的,生成的代码在动态编译后动态加载到运行环境中动态执行,这比较适合根据程序的逻辑动态生成一些代码来执行,比如动态生成代理类,代理类的接口如果是事先无法确定的,那么我们也不可能在编译前就生成代码。
- 静态生成:一般是在编译前生成,然后直接进行编译的。比如自动根据XML中的相关数据定义生成CRUD的操作代码,既可以避免手写代码,又可以得到和手写代码相同的效率,因为这个代码其实还是死的还是固定的,并不是在运行时动态组织的,因此它的效率是最高的。
框架层面:
- SOA
在这一篇中会逐个介绍一下自己对这些XXX的理解,其实每一个理念都不是莫名其妙产生的而是有产生背景的,这些时髦的名词不是用来炫耀的,而是真正要理解它们是干什么的,并且框架千万不能乱用理念也千万不能乱用,并不是把所有的这些都用上你的系统才是一个牛逼的系统,一定要适合才是最好的,并且要保持简单可靠的原则。所谓SOA,字面上来说是面向服务的架构。有的人不说SOA其实他已经SOA了,有的人大谈SOA但其实只是在用Web服务,SOA可大可小。你可以认为服务调用就是SOA了,也可以认为服务调用只是SOA中的很小一部分。个人觉得SOA的理念是说,把业务逻辑从类库的封装提升到服务的封装,并且通过不同的信道不同的编码格式来提供服务,使异构系统之间也能重用服务,说白了就是让代码从本机重用提升到了跨机器的重用。从最简单的来说,把组件封装成自制的服务,通过交换消息来使用服务提供的功能,那么就是SOA了,但这也仅仅是SOA的开始,比如你是否想过下面的问题怎么解决?
- 你的服务是否可以被外部的异构系统使用?
- 如果你的服务又需要调用另外的服务,怎么处理复杂的调用关系,怎么监控整个调用过程?
- 服务调用怎么纳入事务的控制中?
- 服务越来越多,接口越来越多,怎么知道应该调用哪个服务哪个接口?
- 接口的版本升级了怎么办?
- 服务有太多人调用,怎么进行负载均衡,怎么限制调用人数?
- 怎么做安全,限制服务的匿名调用?
- 服务怎么进行测试?
- 服务调用失败后怎么办?
- 服务怎么进行升级和重新部署?
因此,SOA其实不仅仅是调用服务,在监控和治理上有很多事情要做,SOA可大可小。不管怎么说,从理念上来说,SOA确实是一种进步,复杂逻辑都封装成服务,一个复杂的系统可能调用不同的服务就可以完成了,每一个服务都是自治的,很好的降低了系统整体的复杂度。其实一个复杂的系统如果复杂度在10000的话,那么分成四层实现10*10*10*10就可以大大降低复杂度,每一层只要做好10复杂度就好了,由上层来调用下层。OSI如果不是七层模型,而是一层模型的话很难想象怎么去处理整个复杂的过程。
- RPC
字面上来说就是远程过程调用,RPC(这里我们说广义上的RPC)也可以是SOA实现的一种方式,SOA并不一定都通过Web服务实现。从实现原理上来说RPC其实并不是很复杂,无非就是客户端有一个代理,收集客户端要调用的远程方法以及参数,然后序列化成消息提交到远程,到了远程之后把消息反序列化,动态执行客户端所需要的方法(当然也包括创建对应的对象),然后把结果通过另外一个消息返回给客户端,当然其中的细节太多了。从客户端和服务端交换数据依赖的信道来说一般使用TCP或HTTP,也可能会是UDP。
- ORM
对象关系映射解决面向对象系统和数据库阻抗不匹配的问题,大家都知道的ORM的定义。这里我想说的是ORM的出发点是好的,而且在某些应用中ORM确实可以改善代码可读性,增加编码效率,个人认为ORM是有使用情况的,不是所有的数据访问都适合使用ORM框架的,Java的SSH也有那么一点误导,Struts不是实现MVC的唯一方法,Spring不是实现IOC的唯一方法,Hibernate也不是实现数据访问的唯一方法,我觉得ORM:
- 它适合领域复杂的,表多的,表之间关系多的,访问量相对较小的,企业应用。
- 它不适合业务相对简单,表之间关系不多,访问量超大的互联网应用。
对于ORM来说要入门是简单的,但是要用好不是这么容易的:
- 如果没有正确使用很可能导致产生大量的SQL语句,而使用者还不知道。
- 如果没有正确使用很可能会拉取过多不必要的数据,而使用者还不知道。
- 如果没有正确使用很可能会产生性能不高的SQL语句,而使用者还不知道。
因此,即便是使用ORM也要对ORM产生的SQL语句张一个心眼,并且我们需要了解ORM中是如何做延迟加载、级联加载、主键缓存的,只有了解了这些机制才能真正用好ORM,如果对ORM不熟悉,如果在做互联网系统我觉得还是太平点吧,SQL语句精准高效。当然有的人要说了用SQL的话业务逻辑就可能不在代码中了,其实这个还是看SQL语句怎么写的,如果把SQL只是当做数据库和程序的沟通桥梁的话这不是问题,如果在SQL里面做一些判断做一些计算那就是自己的事情了。
- IOC
控制反转依赖注入不是嘴上说说的,它是一个非常实用的理念。有的人即使在使用了IOC之后还没有意识到为什么要使用IOC,我总喜欢在面试的时候问为什么要用容器来创建对象,自己手动new出来有什么不好?有的人回答是手动new出来的性能不高,容器创建的是唯一的对象,那我就会问自己写一个单例的对象也是一样的,为什么要容器创建?其实这样的理解是有误的。个人认为管理对象的生命周期只是IOC容器的一个作用,IOC容器的意义在于:
- 管理了对象之间的关系。在OO中组合很有用,如果对象之间有复杂关系的话,那么我们就必须在new对象的时候来构建这种关系,这些关系其实都写死在代码中了。并且我们通过代码会直接把实际的类型写死,降低了针对接口编程的意义。如果能在配置文件中根据自己的需求来配置这种关系,由容器动态创建对象和对象之间关系的话,那么我们就把代码中依赖提取到了外部,由外部注入进去。在一个分层的应用程序中,我们不仅仅注入平行层级的对象,还可以注入下级对象,实现从上到下的自动注入,整个系统就非常灵活。
- 管理了对象的生命周期。有的时候出了单例或是new出来的对象,还会有根据线程、根据请求来的特殊声明周期的对象,如果手写代码来管理声明周期一来很麻烦,二来也不方便调整,通过容器来管理对象的声明周期简单高效,并且我们很容易对容器进行扩展提供不同的声明周期的字典即可扩展生命周期的类型。
- AOP
面向切面编程对于职责分离和提高可测试性都很重要。一般来说代码有两种方式织入,静态的和动态的。所谓静态的就是在编译之前直接改了要包装的类,然后把关注点的入口方法封装进去一起编译的编译时增强。所谓动态的就是在运行的时候动态修改代码动态编译的运行时增强。一旦有了AOP,那么我们的事务、日志、权限、缓存之类和业务无关的代码就可以不出现和混在业务代码中了,根据需要进行配置就可以对任意的代码进行这些横切关注点的增强。一旦这些代码有修改,我们也只需要修改配置,而不是在代码中的几千个几万个地方去查找修改。
- MVC
MVC对于网站特别是互联网网站来说是一种非常好的里面,MVC的优点体现在:
- 职责分离,不再是所有的代码都混在一起了,V和C的分离尤其重要。
- 职责分离早就了可以进行单元测试,可测试性也是重要的一环,对C可以进行单元测试是非常重要的。
- 大多数MVC框架都提供了AOP的织入点,在实现职责分离的时候还能实现横切关注点的自动执行和可替换性。
在实现MVC的时候我们不仅仅是能把MVC框架用上去就好了,要尽可能实现:
- C中的诸如缓存、权限、日志之类的横切关注点务必提取出来通过AOP实现,不要和C的其它业务性逻辑混在一起。
- C中的Action可以重用的尽量重用,Action的结果可以重用的也尽量重用。
- 可以考虑MVC和IOC相结合,把C用到一些服务或DAO动态注入进来。
- 对于V尽量确保V中不要有后端代码,V可以让前端开发直接编辑,由后端开发嵌入比较简单的Tag。如果VM明确的话,前端甚至可以直接写V。
- TDD
测试驱动开发又是一个不小的概念。各种平台也有各种框架来实现Mock来实现单元测试。工具再好没有正确的编码理念也是没用的,要实现所有的代码都能通过单元测试来验证必要的几个因素包括:
- 程序没有很复杂的上下文环境,或者说上下文环境是可以被模拟的,否则怎么测试?
- 程序模块的职责是单一的清晰的,如果一个模块做了几十件事情,有几百个分支的话是很难测试,测试的粒度越是细越是容易测试。
针对现在大多数项目无法进行TDD,甚至无法进行单元测试的原因是因为往往时间很赶,只能有时间实现最基本的业务逻辑,而单元测试其实编码的时间不会比实际的编码少的,甚至更多。个人认为可以根据项目的性质不同来看,如果这个项目本来就是CRUD的,或者是一个临时的项目,单元测试TDD的意义不大。但如果这个项目是一个要被很多人使用的类库或者框架性质的项目,没有单元测试是无法想象的:
- 如果一个类库提供了100个功能,你不可能在修改了一个点之后就去手动测试一下这100个功能,必须通过自动化的手段进行。
- 类库需要确保在所有的边界情况都考虑到,手动测试或者黑盒测试是很难覆盖所有情况的。
- 如果你的类库没有提供单元测试,类库的使用者怎么在他们的执行环境来验证类库的可用性呢?因此还不仅仅是给自己用,别人还要使用你的单元测试来确保类库在自己的运行环境下可以正常工作。
- 其它
在这里先想总结一下,个人觉得这么多XXX中最需要有的就是MVC和IOC了,至于AOP有了当然更好,至于ORM和SOA则是看需求了,对于TDD么如果你在写一个类库或框架那是必须的,否则很难想象这套代码的稳定性会有多高。除了上面提到那那些XXX其实还有很多作为基础框架需要实现的东西,以前画过一个脑图参考http://www.cnblogs.com/lovecindywang/archive/2012/01/11/2318973.html。作为框架我觉得有一些要素是需要满足的:
- 尽量保持框架对外的接口是很简单的,复杂的东西应该在框架内部处理掉,对框架的使用者透明。
- 框架的异常处理要完善,日志记录要完善,框架由于是对开发者透明的,因此最好在异常信息和日志信息中写明要调用者和框架的使用者怎么样去做才能解决这个问题而不是仅仅是说出了什么错。
- 一个完善的框架应该内建性能检测机制,或者提供http的接口可以让外部了解到框架内部的运行状况。
- 框架要做好测试,确保在不同的线程环境和不同的配置情况下框架都能正常运行。
- 框架最好有性能测试,让使用者明确这个框架能提供的性能,以便正确判断是否可以满足自己的需求。
架构层次:
- 日志集中
所谓日志集中就是把程序的所有日志和异常信息的记录都汇总到一起,在只有一台服务器的时候我们记录本地文件问题也不是最大,但是在负载均衡环境下再记录本地日志的话就出现问题了。在想查看网站日志的时候到哪台机器去查都不知道,难道有100台机器就100台机器逐一远程连上去看?因此,把这些数据汇总在一起保存对于大型网站系统来说是很必要的,这样我们就可以直接进行查看、搜索,也很明确可以知道是哪台机器的业务出了问题。至于这种日志数据是写到RDBMS还是NOSQL甚至是搜索引擎这就看需要了,总之避免写本地的文本文件,否则真的是灾难。当然写一个日志远远没有想的这么简单:
- 为了达到比较好的性能,日志是否先写本地内存队列然后定时刷到数据库中去?
- 各种日志混在一起也难以搜索,是否要添加一些搜索字段?比如分模块?
- 如果数据库不可用的话是不是先写本地日志以后再汇总过去?
- 日志是否易于辨明问题还是看日志怎么记录,如果日志中只写错误那么记录了也白记录,一定要写清楚这是哪个模块哪里出现了问题,有条件的话还可以写上一些参数信息和当前的状态。
- 对于未处理的异常信息不太可能手动去记录,一般而言很多框架或服务器都会提供一个接口点可以回调我们自己的代码,在这里我们就可以收集这些未处理异常然后统一记录,最后把用户带到友好的错误页面。不记录任何一个未处理异常都是可怕的事情,想象一下用户已经看到了色彩斑斓的页面,而开发还一无所知,这样的话这个问题始终会存在,即使用户投诉了反馈了我们也很难重现。
- 配置集中
配置集中和日志集中的道理是一样的,就是统一管理。任何一个系统其实或多或少都会有一些不能在程序中写死的参数(比如至少的数据库连接字符串和外部服务地址),一般情况下会写到配置文件中,这样存在几个问题,第一就是在多服务器的负载集群情况下要修改配置需要逐一修改每一台服务器的配置,第二就是在修改配置还可以需要重启服务或网站才会生效,第三无法统一管理也无法知道是否所有网站都统一配置了相同参数。解决的办法还是一样的就是汇总保存在统一的地方比如保存在数据库中,然后每一个网站都从数据库中获取配置的值,在实现的时候简单有简单的做法,复杂有复杂的做法,比如要考虑以下问题:
- 参数的值是直接保存强类型的那还是保存字符串在使用的时候转?甚至说值是支持对象和数组的,而不是简单类型。
- 参数的值不可能每一次获取都从数据库取,怎么做缓存,缓存多长时间,值修改了怎么同步回来?
- 程序是否允许修改值?还是说程序只是读取,不允许修改,修改参数的值只能通过数据库或后台进行。
- 是否是需要根据不同的部署环境、用户的语言、服务器的IP来设置不同的值。
不管怎么样,至少一个最简单的配置服务,从数据库中读取参数值,哪怕读取后永远缓存只有重启服务才能生效,也会比直接从本地配置文件中读要好很多。
- 缓存
缓存这个架构手段实在是太常用了,几乎所有的人都知道缓存这个设计和性能优化手段。对于一个网站系统来说又有太多的地方可以做缓存,真正能把缓存用好,在合理的地方用缓存,监控缓存的命中率,想办法提高缓存的命中率其实不是这么容易的,一般来说有这些地方可以做缓存,从上到下:
- 浏览器和CDN缓存:通过这两种整页的缓存可以尽量减少请求打到网站服务器的机会,也就提高了网站服务器的负载能力,当然一般来说静态的资源比较容易走这种缓存,动态的资源其实也可以走,只不过要能根据访问的条件做出合适的Key,对于和每一个用户都独立的页面可能要直接进行页面缓存比较难一点。
- 反向代理的缓存:反向代理作为服务器的代理可以进行整页或是片段页面的缓存,可以提高直接把请求达到网站服务器的机会提高性能。
- 数据的缓存:如果请求真正到了网站服务器,那么我们也不一定要所有的数据都从数据库中取,可以尝试把部分数据保存在分布式缓存中。只要Key合理,并且请求有规律那么可以保证比较高的命中率,从而减轻数据库的压力,也减轻网站服务器的压力。
- 大块数据的内存中缓存:对于有一些大块的数据是无法保存在分布式缓存中的,那么可以直接在网站启动的时候把这种不太会改变的大块数据全部加到内存中来,这样这快数据的访问效率和计算效率就很高了。
对于缓存的更多内容可以参考我之前做的一个分享:http://www.cnblogs.com/lovecindywang/archive/2010/07/19/1780589.html
- 分布式缓存
所谓分布式缓存就是缓存的数据是分布在多个节点上的,好处是一来可以尽量利用服务器的资源,比如一台服务器可以有1GB内存空闲,找50台服务器就是50GB了,如果不用这50台服务器其实也就是这么多内存空着(一般来说网站服务器使用的内存是相对固定的,主要业务不怎么变化的话,而且诸如Memcached之类分布式缓存对CPU的使用是很低的,完全可以把Memcached寄居在大内存的Web服务器或是应用服务器上,实现神不知鬼不觉的分布式缓存);第二个好处就是可以减少单点故障带来的影响,一般来说分布式缓存都有类似于一致性哈希的算法,即使有单点故障的话也只是少部分缓存数据会不命中,损失不是太大。在使用分布式缓存的时候要牢记下面几点:
- 缓存就是缓存,数据是允许排出和丢失的,当成文件系统用的话就错了。
- 分布式缓存的数据是通过网络存取的,数据传输走网络和本机内存中效率不能比,而且数据需要序列化和反序列化要考虑到性能开销。
- 缓存的Key生成的策略是很关键的,Key生成的参数过多的话很可能命中率会几乎为0,这种缓存做了也白做,因此需要针对分布式缓存的命中率有监控。
- 队列
队列也是实现高性能架构的一个必不可少的利器,通过把执行时间比较长的任务在队列中进行排队,通过限制队列的最大容纳项目数,实现一个抗高压的能力,并且队列后端的点也能有一个比较平稳的压力。队列说到底也只是一个容器,如果前端的压力永远比后端处理能力大的话,队列总是要爆的,因此队列也不是万能。往往对于网站系统即使后端的业务有队列,前端的页面如果扛不住高压力,甚至是验证码之类的都刷不出来的话也是没用的,架构上就是这样系统中任何一个点都会拖累整站的架构,架构优化针对最薄弱的地方而不是最强的地方。形式上来说队列有两种:
- 生产者和消费者:生产者生产出来的数据只能被一个消费者消费,也就是任务只能执行一次的。往往这种形式的数据是需要持久化的。
- 发布和订阅:任何一个事件都允许有多个发布者和多个订阅者,订阅者订阅自己感兴趣的东西,只要发布者发布了订阅者感兴趣的内容就会传播到所有的订阅者。一般来说这种形式的数据可以是允许丢失的不需要持久化的。
- 池技术
所谓的数据库连接池,线程池都是池技术的一个应用。池技术说到底就是把创建开销比较大的对象缓存在池中避免重复创建和销毁,对象用的时候从池拿出来用,用好了重置后还到池里面别人还可以接着用。比如说数据库连接池就是避免了频繁创建代价高的TCP连接,线程池就是避免了频繁创建代价高的线程。虽然说原理上是这样,但池其实也有一些算法需要考虑的:
- 创建的富裕的对象的回收策略怎么做?
- 对象损坏怎么处理?
一般来说可以参考网上的池实现方式来实现一个通用的池,这样各种对象都可以进行管理了。
- 分布式文件系统
分布式文件系统并不是所有网站都必须的,一般小网站会把用户上传的图片直接保存在Web服务器本地,这么做是可以的,但量大了之后会有问题,首先一台服务器保存不下怎么办,怎么知道哪个图片在哪个服务器上?其次读的请求怎么进行分离,怎么把图片同步到其它服务器上去。分布式文件系统就是来解决这个问题的,通过把一组服务器当做一个文件系统使得我们的文件资源可以分散保存在多个服务器上并且确保有一定的数据备份。在网站规模不是很大的时候其实可以保存在单台服务器上,然后使用文件同步工具同步到另一台服务器,之前再架反向代理解决,数据量再大一定要分布式的话就要上分布式文件系统了。从原理上来说分布式文件系统其实不是很复杂的,但选型的时候也要进行稳定性和性能的考量。有的人是把数据库保存在数据库中的,虽然这样可以实现单点虽然这样可以实现备份,但这显然不是很合理的,会极大增加数据库的压力。
- 分布式搜索引擎
如果有站内搜索需求的话就要上搜索引擎了,现在开源的搜索引擎非常多,不过很多都是Lucene的封装,在选型的时候要根据自己的需求进行选型。所谓分布式也就是如果单点的索引和查询不能满足性能容量需求的话就需要分布到多点了。从原理上来说搜索引擎主要还是一个分词和倒排索引,但是搜索引擎在内容的排序等细节上点还是有很多算法的,除非必要不推荐自己去实现搜索引擎,可以直接针对开源组件进行封装和改良。搜索引擎做的好其实远远不止全文搜索这么简单,甚至可以根据用户搜索的内容给予搜索的建议,可以根据用户搜索的内容进行索引的自完善,还可以根据用户的搜索结果进行大量的用户行为分析,为网站的产品进行改良,如果站点具有自己的搜索模块的话其实有很多事情可以做的。
- NOSQL
NOSQL就是非关系型的数据库,NOSQL不是用来取代关系型数据库的,之所以NOSQL这么火是因为其性能。从本质上来说,程序就是一组代码,对于相同的硬件配置来说,程序性能的高低也就取决于实现相同的操作要有多少代码在CPU中执行一遍,要有多少IO操作要在磁盘上过一遍,往往通用性的东西就会有比较多的计算和IO,往往定制化的东西性能就会比较高。我们说RDBMS性能不高,但是我们也应该看到RDBMS要确保数据的完整性,持久性,要实现通用的功能,要把它的性能和IO达到内存中然后定期刷磁盘的组件来比就不合理了,因此我们要根据业务在合理的地方使用合理的NOSQL,对于资金相关的业务需要事务的业务不太适合NOSQL,对于允许延迟允许数据丢失需要大访问量的业务比较适合NOSQL。任何一种NOSQL往往都是某个大型公司针对自己的需要定制出来的产品,只有这样定制化的东西才可能实现高性能,因此市面上才会有这么多NOSQL,那么我们在选择的时候也要看这个NOSQL针对的应用场景是否就是符合我们业务需要的。另外,NOSQL逼近是一个新新事物,其用户群不可能有MYSQL或ORACLE这么多,因此我们也不能期望其稳定性能达到非常高的标准,而且NOSQL由于其定制化的特点,在使用的时候不一定都可以通过标准SQL来使用对于学习成本也是我们需要考虑的因素。不管怎么样在该需要用的时候还是要用,有的应用单靠RDBMS是没有可能实现这么高性能的,不用NOSQL就是死路一条,用了即使它不稳定还有可能活。
- NOSQL之Mongodb
Mongodb是一款性能超高的文档型数据库,之所以它这么火不仅仅是因为性能高,而且它功能也很全,相比其它NOSQL它几乎可以实现90%以上的SQL操作,并且也有丰富的高可用性的配置方式。总结下来Mongodb是一款性能几倍于传统RDBMS的最接近于RDBMS功能的NOSQL。对于对性能要求很高,特别是写数据并发要求很高的应用来说使用Mongodb是比较适合的,比如存业务日志或系统日志。值得一提的是:
- Mongodb也不是万能的,对于超大级别的数据量如果不进行数据分区,那么Mongodb也救不了你,并且不要期望Mongodb的自动Sharding功能能有多好,自动的毕竟没有手动这么准确,如果你能想到怎么进行数据分片的话还是推荐手动进行。
- Mongodb的性能是很好的,但这也是有限度的。只要拥有比较大的内存,那么热数据可以在内存中保存,读取性能不会太差,但是数据量达到一定的限度之后,如果你的索引数据都在内存中放不下的话,那么其性能会很差的,其实我们也很容易想到为什么,举个例子,我们在查字典的时候需要翻索引的,至于字典真正的内容在哪里其实问题不大的,如果一本百科全书即使由100本书构成,只要索引在手里查到了哪一本再去取问题不大,如果这个索引也是由100本书构成,查索引就不能一次性在手里查完还需要翻不同书的话这个性能就会非常差。
- NOSQL之Redis
Redis于其说是一款NOSQL还不如说是一款缓存组件,在大多数时候把NOSQL当成一个服务端可以做计算的,存储复杂类型的,又具有一些诸如队列、管道等小功能的升级版本的Memcached是不错的。我个人使用Redis的心得是:
- Redis不像Memcached,它可以保存多种形式的数据,而不仅仅是一个字符串,这是很有亮点的,意味着我们直接可以在服务端针对大量数据进行一个计算,然后服务端直接返回计算结果,而不是要从缓存中把所有数据都取出来在客户端进行一番计算然后再保存回缓存中的(这里的客户端是指使用NOSQL的客户端,不是浏览器),也就是说要用好Redis是要写一些定制性的代码的,把我们真正的业务逻辑嵌入到Redis中去,如果只是存KeyValue的话性能不一定比Memcached高多少的。
- 一般情况下不建议过多依赖Redis的磁盘VM的,有16GB内存保存32GB的数据是可以的,有16GB的内存用Redis保存1TB的数据那就是用错Redis了。NOSQL的产品其实原理上来说都不是特别复杂的,使用NOSQL之前最好熟悉一下它的原理,这样我们更容易用好NOSQL产品。总之我的观点还是这样,由于内存和磁盘性能的巨大差异,如果说能在内存中做大部分事情的话,这个性能就会比较好,如果要来回在内存和磁盘上翻腾的话这个性能也好不到哪里去。
- NOSQL之HBase
HBase、Hadoop适合的是超大数据量的存储和计算,它其实是真正的一个分布式的概念,数据不再能在一个单点上保存了,需要分散到很多机器上,然后计算结果也是分别计算后汇总在一起。对于这套东西我的体会是不要轻易引入:
- 除非是亿级以上的数据量并且数据的格式比较简单,否则要考虑是否适合引入,HBase的学习曲线不低的,而且最好是在社区熟悉过一段时间的,否则连用哪套版本的Hadoop什么的都搞不清楚,版本没用对要么有很多难以解决的BUG要么就是连基本的连通性都搞不起来。
- 要想有很好的性能,至少有20台以上的服务器再来搞HBase,一两台机器就算了,还没到分布式的这个需求。想想也知道所谓通过并行来提高效率首先是你有这么多资源可以把数据并行分散出去,然后同时进行计算汇总才能节约时间,如果根本数据都没分出去怎么可能节省时间。
性能层次:
- 性能分析
我觉得性能分析的话要注意几个要点:
- 不要去猜:对于自己写的代码你是否知道你的代码要执行多久,是不是还在用时间相减来测试代码执行时间?现在有很多自动化的工具可以在程序运行的时候,测试代码中每一句语句的执行时间,可以有效分析出代码的性能瓶颈。对于比较重要的业务逻辑建议采用类似的工具来进行性能分析,有的时候性能慢的代码不一定是自己写的还可能是框架内提供的,如果没有一个丰富的编码经验是不太可能知道这些点的,但是通过这样的分析工具你就能知道这个地方会慢,虽然框架的代码我们不能改,但是我们可以进行缓存或者是可以换一种方法写。打个比方你可能认为获取当前机器的名字是很快的操作,但如果框架内部通过一定的操作系统接口访问这个操作需要5毫秒的话就很夸张了,很可能这个操作在你整个请求中运行了10次获取了10次机器名,那么不知不觉50毫秒就耗去了,这个时候你还以为是数据库慢,这就永远找不到问题所在了(解决就相对简单了只要缓存机器名就好了)。因此,除了加点方式来测算代码性能,更科学的是采用一些工具来测算,或许这个工具本身就是通过植入无数个加点代码来实现这种测算,但这总比自己一次又一次手动改代码来的好。总而言之,有一些性能问题不是你自己写代码引起的,或者是外部代码或者是框架内的代码,猜是猜不出来的。
- 注意区分优先级:一般我们可以认为硬件由CPU、内存和IO构成,其中IO分网络和磁盘,不管是网络还是磁盘其性能是最差的也是最可能达到系统中性能瓶颈的地方,知道这一点我们就知道了性能分析的优先级了,忽略数据库层面、网络层面的问题把时间浪费和纠结在for/foreach的性能这种纯语言层面的东西则不是那么有效果。这里举几个例子典型的立竿见影的性能相关的例子,同时也可以反映出通过一定的手段看到问题而不是去猜是多么有效:
-
- 遇到过在使用ORM的时候ORM是乐观并发机制的,把所有的字段的值都作为where条件传了过去,有一个表有近百个字段当然更新的效率低了,通过查看体积到数据库的SQL语句就可以查明这个问题,解决的办法是不把相关字段列入检查或采用悲观并发。
- 遇到过在使用Memcached客户端的时候没有把应该作为静态单例的工厂对象作为单例而是每次使用都创建一遍,导致每次都会花2秒创建一个连接池,性能极其低下,而且导致占用了几万个网络端口,通过基本的操作系统命令检查磁盘网络相关的参数即可查明这个问题,解决的办法是把工厂单例。
- 遇到过一个程序只运行几个小时就会占用到10GB内存导致崩溃,看代码无法查明问题,通过在运行性能监视工具检测虚拟机不久就可以查明问题,解决的办法是修复这个BUG。
- 一个网站使用的模板每次都是从网络上获取的,导致每次请求其实都在服务端再发起一次请求到其它网站去下载页面然后再处理,性能极其低下秒并发只能在10左右。要查明这个问题其实不难,运行一次就耗时1秒,对模板进行缓存即可解决问题。
3. 注意观察日志:对于很多组件(比如Mongodb),在其初始化的时候都会对系统基本的配置进行检测,如果发现因为某个配置不合理组件无法达到最优会给出提示,因此要注意这些组件的日志,以便及早发现性能问题。
- 压力测试
虽然说某些工具可以帮助我们分析程序性能,但是有一些性能问题必定是在高压力下才会产生的。很典型的就是为了线程安全使用的锁,不管是程序层面的锁还是数据库层面的表锁行锁,锁意味着串行,串行意味着在压力增大的情况下压力越大排队越厉害越慢。当然,压力测试还可能测试出因为多线程环境带来的线程安全问题。系统整体的性能是受最差的那个点拖累的,如果系统各个模块抗压能力差距很大的话,压力测试比较容易测出问题。作为网站开发我们应该有意识,对于普通的服务器,一般每秒能处理多少请求,一般每秒数据库能处理多少操作,有一个熟练级的认识,如果你发现经过压力测试,Web服务器每秒最大只能处理10个并发,那么系统肯定存在性能问题的。压力测试不但可以测试出系统的问题,还可以测试出一定的硬件条件下,系统可以承受的最佳的访问人数在多少,低于这个人数系统跑不满,超过这个人数系统的响应走下坡路,根据这个结果可以有理有据去选择集群的数量。
- 线上问题排查
对于已经在线上的代码不太可能去运行什么性能分析工具,因为这些通过这些工具执行的代码会比直接运行代码慢好几倍,在线上这么做可能导致网站直接崩溃。
- 简单的办法可以通过加点,然后把代码灰度部署到某些机器查看日志来分析原因。
- 或者还可以直接到某台机器抓取进程样本来做静态分析(特别适合有虚拟机的语言)。通过抓取一个进程的样本可以了解进程中哪些对象占用了绝大部分内存,垃圾回收各个区的状态来分析内存性能问题。通过抓取多个进程样本可以比较哪些线程耗时比较厉害,耗时厉害多线程主要集中在执行什么代码来分析CPU性能问题。
- 除了这种内在的分析办法还可以通过外部来分析,比如通过Fiddler之类的工具来观察慢是因为网络原因还是因为并发数的原因还是因为服务端的原因。比如通过数据库的监控工具来监控每一个SQL语句的执行时间抓出最慢的SQL。比如通过观察虚拟机的状态,观察操作系统的性能指标,观察Windows的性能计数器等可以帮助判断大概存在的性能问题。
安全层面:
- SQL注入
SQL注入是一个古老的安全问题,现在任何程序都不应该再出现这样的问题了,其原理非常简单,在过去大多数程序都是直肠子通数据库的,因此如果拼接SQL并且在参数上没有做好过滤或者没有使用参数形式来生成SQL语句的话可能会导致用户在页面上输入的恶意代码直接在数据库中执行。SQL注入的危害点在于整个网站有1000个数据点,如果其中有1个点有漏洞那么整站的数据其实都有危险了,很多开发会注重资金相关的模块但是忽略新闻相关的模块,如果都是使用一套数据库的话那么一个不重要模块的漏洞就会影响整站。也有一些开发比较会注意GET参数的验证但是会忽略POST参数的验证,这也是很容易出问题的,POST不会因为不在URL上显示就安全多少的,其内容还是用户在表单中输入提交的。在这里提SQL注入的问题是希望开发人员都有这个意识,整站的安全系数是以整站安全最薄弱的模块来衡量的,就像Windows的体验评分是以最低的那个数字来作为结果的而不是最高的那个数字作为结果的。
- 客户端信任
做网站开发要有这个意识,任何客户端提交过来的数据都是不值得信任的,这包括GET参数、POST参数、COOKIE的内容。所有的这些都是可以伪造的,浏览器只是为了方便提供的一个HTTP的数据提交器和HTTP标签的解析器,黑客是不会使用浏览器的,HTTP头和主体中的内容可以随便改的。因此,对于任何客户端传过来的数据都需要进行严格的过滤和检查,脑子里始终有这么一个想法的话可以避免很多安全问题。另外,由于COOKIE是存在用户本地的也是客户端传过来的,其中的安全问题包括不能随便相信COOKIE中存的值,也不能随便把敏感的数据保存在客户端以免让用户自己的隐私信息得到泄露。一个简单的例子,如果我们通过COOKIE中的用户名来判断用户是否已经登陆的话,用明码的用户名肯定是不合理的,如果用户随便改一个管理员的用户名难道系统就认为是管理员登陆了?解决的办法是存一个加密后的用户名,但这样也是不安全的,因为一旦加密方法别破解的话还是会产生巨大的漏洞,因此可以在COOKIE中同时存用户名密码,也就是说网站其实是用你的用户名和密码帮你在一段时间内自动登陆,而不是真正记住你的登陆,由于我们还要确保用户的密码不会得到泄露(如果用户机器上有密码的话那么存明码的密码也是对用户的不负责),因此我们需要在COOKIE中存加密后的用户名和哈希后的密码,为了安全还可以把这两个数据再加密一次保存在COOKIE中。