文/沈羽   大型互联网应用的突出特点是应用本身规模大,结构复杂,用户访问量大。设计良好的日志系统,有助于分析流量趋势,帮助管理网络应用;有助于在应用出现问题时,快速查找问题,保证网络应用的可用时间。设计一套完善的日志系统,用于记录应用的内部行为,是一件很有价值的工作。 本文希望从设计和实现的角度,描述日志系统的构建。本文的描述,基于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) { 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中表示的两类信息。这样的日志系统仍然比较粗糙,方法调用栈信息和方法调用是否成功的信息被输出在同一个日志文件中。需要额外的工作提取相应的信息。
设计完善的日志系统
对于一个大型互联网应用,很重要的一点,是可以度量网站的访问量和访问趋势,系统出现异常可以准确及时的记录问题,并使用这些数据来帮助诊断网站出现的问题。为了满足这些需求,日志系统可以分为几个模块来记录相关信息。图一给出了一个日志系统的模块图
clip_image002
图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给出了本地日志系统的框架
clip_image004
图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给出了一个集中式日志系统的架构图。
clip_image006
图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都会被调用,这个函数使用模块的类名作为键值,而相应的引用计数会被递增。 为了显示静态和动态调用计数的值,可以设计一个管理员界面,显示模块的计数值。 有了以上各个层次的日志系统,一个网络应用的内部状况基本可以一览无余的展现出来。各个不同的应用还可以根据自己不同的需求,定制不同的日志系统展示出系统的内部行为。 作者简介:沈羽是ebay中国软件工程公司的软件工程师,从事JSP应用、JSP引擎、企业级网络应用方面的开发和研究。
Logo

20年前,《新程序员》创刊时,我们的心愿是全面关注程序员成长,中国将拥有新一代世界级的程序员。20年后的今天,我们有了新的使命:助力中国IT技术人成长,成就一亿技术人!

更多推荐