转载自:http://www.kuqin.com/web/20090824/67374.htmlhtml
原做者简介:沈羽是ebay中国软件工程公司的软件工程师,从事JSP应用、JSP引擎、企业级网络应用方面的开发和研究。java
大型互联网应用的突出特色是应用自己规模大,结构复杂,用户访问量大。设计良好的日志系统,有助于分析流量趋势,帮助管理网络应用;有助于在应用出现问题时,快速查找问题,保证网络应用的可用时间。设计一套完善的日志系统,用于记录应用的内部行为,是一件颇有价值的工做。web
本文但愿从设计和实现的角度,描述日志系统的构建。本文的描述,基于Java语言,以及使用Java语言实现的类库。读者能够根据本身的实际需求,替换成其余的实现。数据库
最简单的日志系统其实就是在系统内添加System.out.println(”Some log”)语句。如列表1所示。后端
1. public void someFunction() {服务器
2. System.out.println(”enter method”);网络
3. try {架构
4. throw new Exception(”Some exception”);框架
5. } catch (Exception e) {dom
6. System.out.println(”Failed: expcetion ” + e + ” thrown out”);
7. }
8. System.out.println(”Success”);
9. System.out.println(”exit method”);
10.}
列表1 用System.out.println实现的最简单的日志系统
这个简单的日志系统记录了两类重要信息,分别是方法调用的调用栈信息(line 1和 line 9),和方法调用的成功与否信息(line 6 和line 8)。可是由于过于简陋,这样的实现,并不能出如今实际的大型网络应用中。
对于Java开发人员来讲,比较熟悉的用于实现日志系统的框架包括,Log4J和Java 类库自带的包java.util.logging。
1. Logger logger =
2. Logger.getLogger(PackageMatcher.class.getSimpleName());
3. public void someFunctionWithLogger() {
4. logger.log(Level.INFO, “enter method”);
5. try {
6. throw new Exception(”Some exception”);
7. } catch (Exception e) {
8. logger.log(Level.SEVERE,
9. “Failed: expcetion ” + e + ” thrown out”);
10. }
11. logger.log(Level.INFO, “exit method”);
12. }
列表2 使用java.util.logging实现的日志系统
首先从性能上来讲,这两个框架能够知足大规模访问的需求,其次还添加了必要的基础设施,好比Level的概念(line 4, line 8 和line 11),Logger的parent/child关系等。但是这些框架的问题在于它们没有提供领域模型(domain model)。好比并无区分列表1中表示的两类信息。这样的日志系统仍然比较粗糙,方法调用栈信息和方法调用是否成功的信息被输出在同一个日志文件中。须要额外的工做提取相应的信息。
对于一个大型互联网应用,很重要的一点,是能够度量网站的访问量和访问趋势,系统出现异常能够准确及时的记录问题,并使用这些数据来帮助诊断网站出现的问题。为了知足这些需求,日志系统能够分为几个模块来记录相关信息。图一给出了一个日志系统的模块图
图1 大型互联网应用的日志系统
在这个设计中,日志系统有四个子系统。分别是路径追踪系统,本地日志系统,集中式日志系统,和引用计数系统。这四个子系统,分别记录不一样类别的信息,为管理和排错,提供支持。
一个大型网络应用,大都部署到大量的机器集群上边。对于每一台物理机器,当运行在其上的应用模块出现问题,首先要在本机器上记录下当前的错误现象。列表3给出了记录在本机上的日志片断
2008-12-7 21:57:24 61.171.218.225 SomeClass someFunctionWithLogger
INFO: enter method
2008-12-7 21:57:24 61.171.218.225 SomeClass someFunctionWithLogger
SEVERE: Failed: expcetion java.lang.Exception: Some exception thrown out.
2008-12-7 21:57:24 61.171.218.225 SomeClass someFunctionWithLogger
INFO: exit method
列表3 记录在本机上的日志片断
由于这是一个实时的记录系统,全部系统异常均可以被准确及时的记录下来。通常来讲,一个本地日志系统由四个主要部分组成,图2给出了本地日志系统的框架
图2 本地日志系统的构成
从图中能够看出,4个部分分别是记录条目(LogRecord)、记录器(Logger)、处理器(Handler)、过滤器(Filter)。记录器之间还能够有父子关系(好比使用Decorator 模式实现)。当子孙节点处理完日志后,能够继续交给父节点处理。列表4给出了一个本地日志系统的参考实现框架。
1. public class Logger {
// parent logger
2. private Logger parent;
// add filter
3. public void addFilter(Filter filter) {
4. }
// add handler
5. public void addHandler(Handler handler) {
6. }
// log
7. public void log(LogRecord record) {
8. }
9.}
10.public interface Handler {
11. void addFilter(Filter filter);
12.}
13.public interface Filter {
14. void filter(LogRecord record);
15.}
16.public class LogRecord {
// some fields
17.}
列表4 本地日志系统的参考实现框架
在实现本地日志系统时,能够在LogRecord类里面设计记录相关的信息。好比针对列表3给出的输出,能够在LogRecord增长Date (java.util.Date)、IP(String[])、ClassName(String)、MethodName(String)、Level(int)、Message(String)等域。其中Level用于标记日志消息的级别,高于某级别的消息才会被输出。Handler和Filter也作过滤工做,能够为日志系统提供很强的灵活性。
本地日志系统能够在某台机器上记录系统的运行状况。虽然及时、准确,可是因为在单一机器上的模块通常来讲只是整个系统的一部分,因此缺点是不能全局地监测系统。须要一个日志总线,将全部的日志信息集中起来,归并相同类型的日志,记录事务信息等等。图3给出了一个集中式日志系统的架构图。
图3 集中式日志系统的架构图
如图,集中式的日志系统能够设计成一个客户端/服务器架构。在每一个部署应用的服务器上,均有一个日志系统客户端,应用在一些关键点调用日志系统客户端,经过日志总线,客户端将日志信息提交到日志系统后端服务器。列表5给出了调用日志系统客户端的示例代码。
1. public void someMethodWithDistributedLogger() {
2. // Log transaction started once log client was created
3. Logger logger = CentralizedLoggerFactory.createLogger();
4. logger.setData(”key”, “value”);
5. try {
6. throw new Exception(”Some exception”);
7. } catch (Exception e) {
8. logger.setStatus(Status.FAIL,
9. new LogRecord(e.getMessage()));
10. }
11. logger.log(Level.INFO, new LogRecord(”exit method”));
12. logger.setStatus(Status.SUCCESS);
13. // Log transaction ended
14.}
列表5 集中式日志系统的客户端调用代码
当工厂方法建立日志系统客户端的时候,日志事务开始,在结束方法调用的时候,日志客户端实例的生命周期结束,日志事务也结束。这个示例代码中的一个细节是,一旦状态被设置(line 8-9),后续的状态设置将不会再起做用(line 12)。
也能够根据须要,将集中式的日志系统设计为peer-peer结构的。日志系统的后端数据库能够根据须要将数据聚合,进行数据挖掘等工做。
这个系统用于追踪已经在产品环境运行的网络系统的运行路径。它的功能能够包括记录模块和函数的调用路径、记录某一用户的身份信息、分析相应的用户数据流向等。
路径追踪系统的实现能够参考第三部分的本地日志系统地实现。列表6给出了一种使用用途——记录系统的运行路径:
1.public void doSomething(String value1, String value2) {
2. tracer.traceEnterMethod(new Object[]{value1, value2});
3. // do something
4. tracer.tranceExitMethod(new Object[]{value1, value2});
5.}
列表6记录系统的运行路径
与第四部分介绍的集中式日志系统相似,路径追踪系统也能够增长系统总线和后端服务器支持,加强系统的能力。
大型网站会有不少模块,每一个模块又会由更多更小的组件组成。好比用户界面模块可能会有搜索框、下拉框、用户输入条等等;后台组件好比用户权限认证、业务逻辑计算组件。统计这些组件的访问量信息能够很好地了解系统的运行状况,为维护、性能调优提供很好的帮助。
有两类引用信息是很是有价值的。一类称为静态引用记数,衡量的是一个组件在构建的时候,被其余的组件引用的次数。另一类称为动态调用记数,衡量的是运行过程当中被调用的次数。
静态引用记数
计算静态引用记数是一件很有难度的工做,须要在每一个引用到组件的代码处,递增引用计数。若是将这项工做交给组件的使用者,会发生诸如遗漏计数、错误计数等问题,也增长了使用者的负担。
解决的方法有多种。好比使用AOP技术在每次引用组件的地方增长计数。本文采用builder模式构建模块。每一个component在builder里面被构建的时候,都会调用一次计数方法。
1.public static Component buildComponent(Component[] components) {
2. for(int i = 0; i < components.length; i++) {
3. addComponent(components[i]);
4. com.corp.countStaticReference(
5. components[i].getClass().getName());
6. }
7.}
列表6在Builder里计数静态引用
动态调用记数
记录模块的动态调用次数,相对来讲简单一些。每个被调用模块通常来讲都会有个入口函数或是一个主控函数。记录组件的动态调用计数,只要在该函数里添加相应的计数操做便可。
1.public void handleBody() {
2. com.corp.countDynamicReference(this.getClass().getName());
3. // handleBody
4.}
列表7在模块的主控函数里添加计数操做
每次进入这个函数,静态计数函数countDynamicReference都会被调用,这个函数使用模块的类名做为键值,而相应的引用计数会被递增。
为了显示静态和动态调用计数的值,能够设计一个管理员界面,显示模块的计数值。
有了以上各个层次的日志系统,一个网络应用的内部情况基本能够尽收眼底的展示出来。各个不一样的应用还能够根据本身不一样的需求,定制不一样的日志系统展现出系统的内部行为。