#
Flume 是一个分布式、可靠和高可用的服务,用于收集、聚合以及移动大量日志数据,使用一个简单灵活的架构,就流数据模型。这是一个可靠、容错的服务。
CONTROL-M简单介绍
http://blog.sina.com.cn/s/blog_53d02f2f01012ha7.html
Packt - BMC Control-M 7 Oct 2012 PDF
Automate job scheduling to run more jobs faster
本文主要是从HBase应用程序设计与开发的角度,总结几种常用的性能优化方法。有关HBase系统配置级别的优化,这里涉及的不多,这部分可以参考:淘宝Ken Wu同学的博客。
1. 表的设计
1.1 Pre-Creating Regions
默认情况下,在创建HBase表的时候会自动创建一个region分区,当导入数据的时候,所有的HBase客户端都向这一个region写数据,直到这个region足够大了才进行切分。一种可以加快批量写入速度的方法是通过预先创建一些空的regions,这样当数据写入HBase时,会按照region分区情况,在集群内做数据的负载均衡。
有关预分区,详情参见:Table Creation: Pre-Creating Regions,下面是一个例子:
publicstaticbooleancreateTable(HBaseAdmin admin, HTableDescriptor table,
byte[][] splits)
throwsIOException {
try{
admin.createTable(table, splits);
returntrue;
}
catch(TableExistsException e) {
logger.info("table "+ table.getNameAsString() +" already exists");
// the table already exists
returnfalse;
}
}
publicstaticbyte[][] getHexSplits(String startKey, String endKey,intnumRegions) {
byte[][] splits =newbyte[numRegions-1][];
BigInteger lowestKey =newBigInteger(startKey,16);
BigInteger highestKey =newBigInteger(endKey,16);
BigInteger range = highestKey.subtract(lowestKey);
BigInteger regionIncrement = range.divide(BigInteger.valueOf(numRegions));
lowestKey = lowestKey.add(regionIncrement);
for(inti=0; i < numRegions-1;i++) {
BigInteger key = lowestKey.add(regionIncrement.multiply(BigInteger.valueOf(i)));
byte[] b = String.format("%016x", key).getBytes();
splits[i] = b;
}
returnsplits;
}
1.2 Row Key
HBase中row key用来检索表中的记录,支持以下三种方式:
通过单个row key访问:即按照某个row key键值进行get操作;
通过row key的range进行scan:即通过设置startRowKey和endRowKey,在这个范围内进行扫描;
全表扫描:即直接扫描整张表中所有行记录。
在HBase中,row key可以是任意字符串,最大长度64KB,实际应用中一般为10~100bytes,存为byte[]字节数组,一般设计成定长的。
row key是按照字典序存储,因此,设计row key时,要充分利用这个排序特点,将经常一起读取的数据存储到一块,将最近可能会被访问的数据放在一块。
举个例子:如果最近写入HBase表中的数据是最可能被访问的,可以考虑将时间戳作为row key的一部分,由于是字典序排序,所以可以使用Long.MAX_VALUE – timestamp作为row key,这样能保证新写入的数据在读取时可以被快速命中。
1.3 Column Family
不要在一张表里定义太多的column family。目前Hbase并不能很好的处理超过2~3个column family的表。因为某个column family在flush的时候,它邻近的column family也会因关联效应被触发flush,最终导致系统产生更多的I/O。感兴趣的同学可以对自己的HBase集群进行实际测试,从得到的测试结果数据验证一下。
1.4 In Memory
创建表的时候,可以通过HColumnDescriptor.setInMemory(true)将表放到RegionServer的缓存中,保证在读取的时候被cache命中。
1.5 Max Version
创建表的时候,可以通过HColumnDescriptor.setMaxVersions(int maxVersions)设置表中数据的最大版本,如果只需要保存最新版本的数据,那么可以设置setMaxVersions(1)。
1.6 Time To Live
创建表的时候,可以通过HColumnDescriptor.setTimeToLive(int timeToLive)设置表中数据的存储生命期,过期数据将自动被删除,例如如果只需要存储最近两天的数据,那么可以设置setTimeToLive(2 * 24 * 60 * 60)。
1.7 Compact & Split
在HBase中,数据在更新时首先写入WAL 日志(HLog)和内存(MemStore)中,MemStore中的数据是排序的,当MemStore累计到一定阈值时,就会创建一个新的MemStore,并且将老的MemStore添加到flush队列,由单独的线程flush到磁盘上,成为一个StoreFile。于此同时, 系统会在zookeeper中记录一个redo point,表示这个时刻之前的变更已经持久化了(minor compact)。
StoreFile是只读的,一旦创建后就不可以再修改。因此Hbase的更新其实是不断追加的操作。当一个Store中的StoreFile达到一定的阈值后,就会进行一次合并(major compact),将对同一个key的修改合并到一起,形成一个大的StoreFile,当StoreFile的大小达到一定阈值后,又会对 StoreFile进行分割(split),等分为两个StoreFile。
由于对表的更新是不断追加的,处理读请求时,需要访问Store中全部的StoreFile和MemStore,将它们按照row key进行合并,由于StoreFile和MemStore都是经过排序的,并且StoreFile带有内存中索引,通常合并过程还是比较快的。
实际应用中,可以考虑必要时手动进行major compact,将同一个row key的修改进行合并形成一个大的StoreFile。同时,可以将StoreFile设置大些,减少split的发生。
2. 写表操作
2.1 多HTable并发写
创建多个HTable客户端用于写操作,提高写数据的吞吐量,一个例子:
staticfinalConfiguration conf = HBaseConfiguration.create();
staticfinalString table_log_name = “user_log”;
wTableLog =newHTable[tableN];
for(inti =0; i < tableN; i++) {
wTableLog[i] =newHTable(conf, table_log_name);
wTableLog[i].setWriteBufferSize(5*1024*1024);//5MB
wTableLog[i].setAutoFlush(false);
}
2.2 HTable参数设置
2.2.1 Auto Flush
通过调用HTable.setAutoFlush(false)方法可以将HTable写客户端的自动flush关闭,这样可以批量写入数据到HBase,而不是有一条put就执行一次更新,只有当put填满客户端写缓存时,才实际向HBase服务端发起写请求。默认情况下auto flush是开启的。
2.2.2 Write Buffer
通过调用HTable.setWriteBufferSize(writeBufferSize)方法可以设置HTable客户端的写buffer大小,如果新设置的buffer小于当前写buffer中的数据时,buffer将会被flush到服务端。其中,writeBufferSize的单位是byte字节数,可以根据实际写入数据量的多少来设置该值。
2.2.3 WAL Flag
在HBae中,客户端向集群中的RegionServer提交数据时(Put/Delete操作),首先会先写WAL(Write Ahead Log)日志(即HLog,一个RegionServer上的所有Region共享一个HLog),只有当WAL日志写成功后,再接着写MemStore,然后客户端被通知提交数据成功;如果写WAL日志失败,客户端则被通知提交失败。这样做的好处是可以做到RegionServer宕机后的数据恢复。
因此,对于相对不太重要的数据,可以在Put/Delete操作时,通过调用Put.setWriteToWAL(false)或Delete.setWriteToWAL(false)函数,放弃写WAL日志,从而提高数据写入的性能。
值得注意的是:谨慎选择关闭WAL日志,因为这样的话,一旦RegionServer宕机,Put/Delete的数据将会无法根据WAL日志进行恢复。
2.3 批量写
通过调用HTable.put(Put)方法可以将一个指定的row key记录写入HBase,同样HBase提供了另一个方法:通过调用HTable.put(List<Put>)方法可以将指定的row key列表,批量写入多行记录,这样做的好处是批量执行,只需要一次网络I/O开销,这对于对数据实时性要求高,网络传输RTT高的情景下可能带来明显的性能提升。
2.4 多线程并发写
在客户端开启多个HTable写线程,每个写线程负责一个HTable对象的flush操作,这样结合定时flush和写buffer(writeBufferSize),可以既保证在数据量小的时候,数据可以在较短时间内被flush(如1秒内),同时又保证在数据量大的时候,写buffer一满就及时进行flush。下面给个具体的例子:
for(inti =0; i < threadN; i++) {
Thread th =newThread() {
publicvoidrun() {
while(true) {
try{
sleep(1000);//1 second
}catch(InterruptedException e) {
e.printStackTrace();
}
synchronized(wTableLog[i]) {
try{
wTableLog[i].flushCommits();
}catch(IOException e) {
e.printStackTrace();
}
}
}
}
};
th.setDaemon(true);
th.start();
}
3. 读表操作
3.1 多HTable并发读
创建多个HTable客户端用于读操作,提高读数据的吞吐量,一个例子:
staticfinalConfiguration conf = HBaseConfiguration.create();
staticfinalString table_log_name = “user_log”;
rTableLog =newHTable[tableN];
for(inti =0; i < tableN; i++) {
rTableLog[i] =newHTable(conf, table_log_name);
rTableLog[i].setScannerCaching(50);
}
3.2 HTable参数设置
3.2.1 Scanner Caching
通过调用HTable.setScannerCaching(int scannerCaching)可以设置HBase scanner一次从服务端抓取的数据条数,默认情况下一次一条。通过将此值设置成一个合理的值,可以减少scan过程中next()的时间开销,代价是scanner需要通过客户端的内存来维持这些被cache的行记录。
3.2.2 Scan Attribute Selection
scan时指定需要的Column Family,可以减少网络传输数据量,否则默认scan操作会返回整行所有Column Family的数据。
3.2.3 Close ResultScanner
通过scan取完数据后,记得要关闭ResultScanner,否则RegionServer可能会出现问题(对应的Server资源无法释放)。
3.3 批量读
通过调用HTable.get(Get)方法可以根据一个指定的row key获取一行记录,同样HBase提供了另一个方法:通过调用HTable.get(List)方法可以根据一个指定的row key列表,批量获取多行记录,这样做的好处是批量执行,只需要一次网络I/O开销,这对于对数据实时性要求高而且网络传输RTT高的情景下可能带来明显的性能提升。
3.4 多线程并发读
在客户端开启多个HTable读线程,每个读线程负责通过HTable对象进行get操作。下面是一个多线程并发读取HBase,获取店铺一天内各分钟PV值的例子:
publicclassDataReaderServer {
//获取店铺一天内各分钟PV值的入口函数
publicstaticConcurrentHashMap getUnitMinutePV(longuid,longstartStamp,longendStamp){
longmin = startStamp;
intcount = (int)((endStamp - startStamp) / (60*1000));
List lst =newArrayList();
for(inti =0; i <= count; i++) {
min = startStamp + i *60*1000;
lst.add(uid +"_"+ min);
}
returnparallelBatchMinutePV(lst);
}
//多线程并发查询,获取分钟PV值
privatestaticConcurrentHashMap parallelBatchMinutePV(List lstKeys){
ConcurrentHashMap hashRet =newConcurrentHashMap();
intparallel =3;
List<List<String>> lstBatchKeys =null;
if(lstKeys.size() < parallel ){
lstBatchKeys =newArrayList<List<String>>(1);
lstBatchKeys.add(lstKeys);
}
else{
lstBatchKeys =newArrayList<List<String>>(parallel);
for(inti =0; i < parallel; i++ ){
List lst =newArrayList();
lstBatchKeys.add(lst);
}
for(inti =0; i < lstKeys.size() ; i ++ ){
lstBatchKeys.get(i%parallel).add(lstKeys.get(i));
}
}
List >> futures =newArrayList >>(5);
ThreadFactoryBuilder builder =newThreadFactoryBuilder();
builder.setNameFormat("ParallelBatchQuery");
ThreadFactory factory = builder.build();
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(lstBatchKeys.size(), factory);
for(List keys : lstBatchKeys){
Callable< ConcurrentHashMap > callable =newBatchMinutePVCallable(keys);
FutureTask< ConcurrentHashMap > future = (FutureTask< ConcurrentHashMap >) executor.submit(callable);
futures.add(future);
}
executor.shutdown();
// Wait for all the tasks to finish
try{
booleanstillRunning = !executor.awaitTermination(
5000000, TimeUnit.MILLISECONDS);
if(stillRunning) {
try{
executor.shutdownNow();
}catch(Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}catch(InterruptedException e) {
try{
Thread.currentThread().interrupt();
}catch(Exception e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
// Look for any exception
for(Future f : futures) {
try{
if(f.get() !=null)
{
hashRet.putAll((ConcurrentHashMap)f.get());
}
}catch(InterruptedException e) {
try{
Thread.currentThread().interrupt();
}catch(Exception e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}catch(ExecutionException e) {
e.printStackTrace();
}
}
returnhashRet;
}
//一个线程批量查询,获取分钟PV值
protectedstaticConcurrentHashMap getBatchMinutePV(List lstKeys){
ConcurrentHashMap hashRet =null;
List lstGet =newArrayList();
String[] splitValue =null;
for(String s : lstKeys) {
splitValue = s.split("_");
longuid = Long.parseLong(splitValue[0]);
longmin = Long.parseLong(splitValue[1]);
byte[] key =newbyte[16];
Bytes.putLong(key,0, uid);
Bytes.putLong(key,8, min);
Get g =newGet(key);
g.addFamily(fp);
lstGet.add(g);
}
Result[] res =null;
try{
res = tableMinutePV[rand.nextInt(tableN)].get(lstGet);
}catch(IOException e1) {
logger.error("tableMinutePV exception, e="+ e1.getStackTrace());
}
if(res !=null&& res.length >0) {
hashRet =newConcurrentHashMap(res.length);
for(Result re : res) {
if(re !=null&& !re.isEmpty()) {
try{
byte[] key = re.getRow();
byte[] value = re.getValue(fp, cp);
if(key !=null&& value !=null) {
hashRet.put(String.valueOf(Bytes.toLong(key,
Bytes.SIZEOF_LONG)), String.valueOf(Bytes
.toLong(value)));
}
}catch(Exception e2) {
logger.error(e2.getStackTrace());
}
}
}
}
returnhashRet;
}
}
//调用接口类,实现Callable接口
classBatchMinutePVCallableimplementsCallable>{
privateList keys;
publicBatchMinutePVCallable(List lstKeys ) {
this.keys = lstKeys;
}
publicConcurrentHashMap call()throwsException {
returnDataReadServer.getBatchMinutePV(keys);
}
}
3.5 缓存查询结果
对于频繁查询HBase的应用场景,可以考虑在应用程序中做缓存,当有新的查询请求时,首先在缓存中查找,如果存在则直接返回,不再查询HBase;否则对HBase发起读请求查询,然后在应用程序中将查询结果缓存起来。至于缓存的替换策略,可以考虑LRU等常用的策略。
3.6 Blockcache
HBase上Regionserver的内存分为两个部分,一部分作为Memstore,主要用来写;另外一部分作为BlockCache,主要用于读。
写请求会先写入Memstore,Regionserver会给每个region提供一个Memstore,当Memstore满64MB以后,会启动 flush刷新到磁盘。当Memstore的总大小超过限制时(heapsize * hbase.regionserver.global.memstore.upperLimit * 0.9),会强行启动flush进程,从最大的Memstore开始flush直到低于限制。
读请求先到Memstore中查数据,查不到就到BlockCache中查,再查不到就会到磁盘上读,并把读的结果放入BlockCache。由于BlockCache采用的是LRU策略,因此BlockCache达到上限(heapsize * hfile.block.cache.size * 0.85)后,会启动淘汰机制,淘汰掉最老的一批数据。
一个Regionserver上有一个BlockCache和N个Memstore,它们的大小之和不能大于等于heapsize * 0.8,否则HBase不能启动。默认BlockCache为0.2,而Memstore为0.4。对于注重读响应时间的系统,可以将 BlockCache设大些,比如设置BlockCache=0.4,Memstore=0.39,以加大缓存的命中率。
有关BlockCache机制,请参考这里:HBase的Block cache,HBase的blockcache机制,hbase中的缓存的计算与使用。
4.数据计算
4.1 服务端计算
Coprocessor运行于HBase RegionServer服务端,各个Regions保持对与其相关的coprocessor实现类的引用,coprocessor类可以通过RegionServer上classpath中的本地jar或HDFS的classloader进行加载。
目前,已提供有几种coprocessor:
Coprocessor:提供对于region管理的钩子,例如region的open/close/split/flush/compact等;
RegionObserver:提供用于从客户端监控表相关操作的钩子,例如表的get/put/scan/delete等;
Endpoint:提供可以在region上执行任意函数的命令触发器。一个使用例子是RegionServer端的列聚合,这里有代码示例。
以上只是有关coprocessor的一些基本介绍,本人没有对其实际使用的经验,对它的可用性和性能数据不得而知。感兴趣的同学可以尝试一下,欢迎讨论。
4.2 写端计算
4.2.1 计数
HBase本身可以看作是一个可以水平扩展的Key-Value存储系统,但是其本身的计算能力有限(Coprocessor可以提供一定的服务端计算),因此,使用HBase时,往往需要从写端或者读端进行计算,然后将最终的计算结果返回给调用者。举两个简单的例子:
PV计算:通过在HBase写端内存中,累加计数,维护PV值的更新,同时为了做到持久化,定期(如1秒)将PV计算结果同步到HBase中,这样查询端最多会有1秒钟的延迟,能看到秒级延迟的PV结果。
分钟PV计算:与上面提到的PV计算方法相结合,每分钟将当前的累计PV值,按照rowkey + minute作为新的rowkey写入HBase中,然后在查询端通过scan得到当天各个分钟以前的累计PV值,然后顺次将前后两分钟的累计PV值相减,就得到了当前一分钟内的PV值,从而最终也就得到当天各个分钟内的PV值。
4.2.2 去重
对于UV的计算,就是个去重计算的例子。分两种情况:
如果内存可以容纳,那么可以在Hash表中维护所有已经存在的UV标识,每当新来一个标识时,通过快速查找Hash确定是否是一个新的UV,若是则UV值加1,否则UV值不变。另外,为了做到持久化或提供给查询接口使用,可以定期(如1秒)将UV计算结果同步到HBase中。
如果内存不能容纳,可以考虑采用Bloom Filter来实现,从而尽可能的减少内存的占用情况。除了UV的计算外,判断URL是否存在也是个典型的应用场景。
4.3 读端计算
如果对于响应时间要求比较苛刻的情况(如单次http请求要在毫秒级时间内返回),个人觉得读端不宜做过多复杂的计算逻辑,尽量做到读端功能单一化:即从HBase RegionServer读到数据(scan或get方式)后,按照数据格式进行简单的拼接,直接返回给前端使用。当然,如果对于响应时间要求一般,或者业务特点需要,也可以在读端进行一些计算逻辑。
5.总结
作为一个Key-Value存储系统,HBase并不是万能的,它有自己独特的地方。因此,基于它来做应用时,我们往往需要从多方面进行优化改进(表设计、读表操作、写表操作、数据计算等),有时甚至还需要从系统级对HBase进行配置调优,更甚至可以对HBase本身进行优化。这属于不同的层次范畴。
总之,概括来讲,对系统进行优化时,首先定位到影响你的程序运行性能的瓶颈之处,然后有的放矢进行针对行的优化。如果优化后满足你的期望,那么就可以停止优化;否则继续寻找新的瓶颈之处,开始新的优化,直到满足性能要求。
以上就是从项目开发中总结的一点经验,如有不对之处,欢迎大家不吝赐教。
一、添加分区
以下代码给SALES表添加了一个P3分区
ALTER TABLE SALES ADD PARTITION P3 VALUES LESS THAN(TO_DATE('2003-06-01','YYYY-MM-DD'));
注意:以上添加的分区界限应该高于最后一个分区界限。
以下代码给SALES表的P3分区添加了一个P3SUB1子分区
ALTER TABLE SALES MODIFY PARTITION P3 ADD SUBPARTITION P3SUB1 VALUES('COMPLETE');
二、删除分区
以下代码删除了P3表分区:
ALTER TABLE SALES DROP PARTITION P3;
在以下代码删除了P4SUB1子分区:
ALTER TABLE SALES DROP SUBPARTITION P4SUB1;
注意:如果删除的分区是表中唯一的分区,那么此分区将不能被删除,要想删除此分区,必须删除表。
三、截断分区
截断某个分区是指删除某个分区中的数据,并不会删除分区,也不会删除其它分区中的数据。当表中即使只有一个分区时,也可 以截断该分区。通过以下代码截断分区:
ALTER TABLE SALES TRUNCATE PARTITION P2;
通过以下代码截断子分区:
ALTER TABLE SALES TRUNCATE SUBPARTITION P2SUB2;
四、合并分区
合并分区是将相邻的分区合并成一个分区,结果分区将采用较高分区的界限,值得注意的是,不能将分区合并到界限较低的分 区。以下代码实现了P1 P2分区的合并:
ALTER TABLE SALES MERGE PARTITIONS P1,P2 INTO PARTITION P2;
五、拆分分区
拆分分区将一个分区拆分两个新分区,拆分后原来分区不再存在。注意不能对HASH类型的分区进行拆分。
ALTER TABLE SALES SBLIT PARTITION P2 AT(TO_DATE('2003-02-01','YYYY-MM-DD')) INTO (PARTITION P21,PARTITION P22);
六、接合分区(coalesca)
结合分区是将散列分区中的数据接合到其它分区中,当散列分区中的数据比较大时,可以增加散列分区, 然后进行接合,值得注意的是,接合分区只能用于散列分区中。通过以下代码进行接合分区:
ALTER TABLE SALES COALESCA PARTITION;
七、重命名表分区
以下代码将P21更改为P2
ALTER TABLE SALES RENAME PARTITION P21 TO P2;
八、相关查询
跨分区查询
select sum( *) from
(select count(*) cn from t_table_SS PARTITION (P200709_1)
union all
select count(*) cn from t_table_SS PARTITION (P200709_2)
);
查询表上有多少分区
SELECT * FROM useR_TAB_PARTITIONS WHERE TABLE_NAME='tableName'
查询索引信息
select object_name,object_type,tablespace_name,sum(value)
from v$segment_statistics
where statistic_name IN ('physical reads','physical write','logical reads')and object_type='INDEX'
group by object_name,object_type,tablespace_name
order by 4 desc
--显示数据库所有分区表的信息:
select * from DBA_PART_TABLES
--显示当前用户可访问的所有分区表信息:
select * from ALL_PART_TABLES
--显示当前用户所有分区表的信息:
select * from USER_PART_TABLES
--显示表分区信息 显示数据库所有分区表的详细分区信息:
select * from DBA_TAB_PARTITIONS
--显示当前用户可访问的所有分区表的详细分区信息:
select * from ALL_TAB_PARTITIONS
--显示当前用户所有分区表的详细分区信息:
select * from USER_TAB_PARTITIONS
--显示子分区信息 显示数据库所有组合分区表的子分区信息:
select * from DBA_TAB_SUBPARTITIONS
--显示当前用户可访问的所有组合分区表的子分区信息:
select * from ALL_TAB_SUBPARTITIONS
--显示当前用户所有组合分区表的子分区信息:
select * from USER_TAB_SUBPARTITIONS
--显示分区列 显示数据库所有分区表的分区列信息:
select * from DBA_PART_KEY_COLUMNS
--显示当前用户可访问的所有分区表的分区列信息:
select * from ALL_PART_KEY_COLUMNS
--显示当前用户所有分区表的分区列信息:
select * from USER_PART_KEY_COLUMNS
--显示子分区列 显示数据库所有分区表的子分区列信息:
select * from DBA_SUBPART_KEY_COLUMNS
--显示当前用户可访问的所有分区表的子分区列信息:
select * from ALL_SUBPART_KEY_COLUMNS
--显示当前用户所有分区表的子分区列信息:
select * from USER_SUBPART_KEY_COLUMNS
--怎样查询出oracle数据库中所有的的分区表
select * from user_tables a where a.partitioned='YES'
--删除一个表的数据是
truncate table table_name;
--删除分区表一个分区的数据是
alter table table_name truncate partition p5;
@import url(http://www.blogjava.net/CuteSoft_Client/CuteEditor/Load.ashx?type=style&file=SyntaxHighlighter.css);@import url(/css/cuteeditor.css);
摘要: 运行SPRING BATCH JOB 的时候,有可能出错,如果能有相关的错误处理机制,则这些错误就能及时得到处理。
SPRING BATCH 提供了监听器,可配置在JOB执行完后,或执行JOB前,要执行的方法。
JOB的定义及BEAN的配置文件:
Code highlighting produced by Actipro CodeHighlighter (...
阅读全文
敏捷协作——Daily Scrum 的重要性
我不仅发挥了自己的全部能力,还将我所仰仗的人的能力发挥到极致。
——伍德罗·威尔逊,美国第28任总统(1865—1924)
只要是具备一定规模的项目,就必然需要一个团队。靠单打独斗在自家车库里面开发出一个完整产品的时代早已不再。然而,在团队中工作与单兵作战,二者是完全不同的。任何一个人的行为都会对团队以及整个项目的生产效率和进度产生影响。
项目的成功与否,依赖于团队中的成员如何一起有效地工作,如何互动,如何管理他们的活动。全体成员的行动必须要与项目相关,反过来每个人的行为又会影响项目的环境。
高效的协作是敏捷开发的基石,面对面的会议是最有效的沟通的方式。每日例会(Daily Scrum)是最早引入并被极限编程所强调的一个实践。它是将团队召集起来,并让每个人了解当前项目进展状况的一种会议。它是一个快速的会议,每个参与者只能被给予很少的发言时间(大约两分钟)来介绍自己的项目进展概要。为了保证会议议题不会发散,每个人都应该只回答三个问题:
- 昨天有什么收获?
- 今天计划要做哪些工作?
- 面临着哪些障碍?
Daily Scrum 有诸多好处:
- 让大家尽快投入到一天的工作中来。
- 如果某个开发人员在某一点上有问题,他可以趁此机会将问题公开,并积极寻求帮助。
- 帮助团队带头人或管理层了解哪些领域需要更多的帮助,并重新分配人手。
- 让团队成员知道项目其他部分的进展情况。
- 帮助团队识别是否在某些东西上有重复劳动而耗费了精力,或者是不是某个问题有人已有现成的解决方案。
- 通过促进代码和思路的共享,来提升开发速度。
- 鼓励向前的动力:开到别人报告的进度都在前进,会对彼此形成激励。
总之,Daily Scrum 能帮助所有的团队成员全心投入到项目中,并且一起向着正确的方向努力。IBM® Rational® Team Concert (RTC)对于团队来说,已经被证明是一种在软件开发过程中进行协作的高效方式。RTC 实现了源代码管理与工作项管理的完美集成。它能够帮助进行敏捷计划、并生成报告,方便管理工作项,并且它还提供了一种有效的框架来支持每日例会(Daily Scrum)。下面,本文将介绍三种使用 RTC 进行 Daily Scrum 的方式。
在 RTC 里使用默认的 sprint backlog 进行 Daily Scrum
双击打开项目当前所处于的 sprint backlog(sprint 是 scrum 中的术语,指敏捷开发周期中的一个迭代计划),如图 1 所示,在窗口底部选择“Planned Item”标签,在窗口右侧选中 Schedule Risk 单选按钮,窗口将呈现将列出当前 sprint 中的所有工作任务项 story 和 task。在进行 Daily Scrum 时,团队成员可以根据这个窗口,逐一更新这些任务的状态。
图 1. 默认的 sprint backlog 窗口
用户可以展开任务项来显示其各个子任务,了解子任务是由谁负责,进展等详细信息。如图 2 所示。
图 2. 展开的默认 sprint backlog 窗口
这种召开 daily scrum 的方式非常简便。它能够展示整个项目的进展和最近的变化,但是任务项不是按照团队成员分组的,不太适应于了解各个团队成员状态。为了解决这个问题,本文下一章介绍另一种用 RTC 进行 daily scrum 的方式。
在 RTC 里定制 sprint backlog 进行 Daily Scrum
定制的 sprint backlog 又称为“开发者任务一览表”。一览表按照团队成员展示任务,每一行表示一个正被开发的任务。任务显示在第一列,其余几列显示其子任务的开发状态:ToDo(将要做),In Progress(正在做)和 Done(完成)。并且,各任务根据其当前状态,分别用不同的颜色显示,一目了然。定制 sprint backlog 的具体步骤如下:
- 打开项目所在的当前 sprint backlog,点击 Copy 拷贝这个计划的模式。如图 3 所示:
图 3. 拷贝当前计划的模式
- 修改某些选项的值。比如修改定制 sprint backlog 的名字为“Developer's Taskboard”,风格选择“Taskboard”,分组选择“Owner”,排序选择“Creation Date”,进度条选择“Progress”。如图 4 所示:
图 4. 修改某些选项
- 修改视图的布局。从窗口底部选择“View Layout”标签,从左侧列表中选择“Effort Tracking”和“Owner”到右侧列表。如图 5 所示:
图 5. 修改视图布局
- 为 sprint backlog 添加色彩。从窗口底部选择“Colorize”标签,根据自己的需要添加、修改、删除各种颜色。如图 6 所示:
图 6. 添加色彩
- 保存所做的修改,用户将得到自己专属的 sprint backlog。显示的效果如图 7 所示:
图 7. 用户定制的 sprint backlog 显示结果
当工作任务项不是很多的时候,这种方式非常适合进行 daily scrum。但是,如果当迭代计划中的工作任务项很多时,这种方式就不再适合了。为了解决这个问题,下一章将介绍最后一种用 RTC 进行 daily scrum 的方式。
在 RTC 里创建自定义的查询进行 Daily Scrum
一般在进行 daily scrum 时,项目管理者需要查询出最近正在被修改的任务,这包括状态是“New”和“In progress”的任务。创建这种自定义的查询具体步骤如下:
- 在“Work Items”下的“My Queries”上点击鼠标右键,选择“New Query…”,如图 8 所示:
图 8. 创建一个查询
在打开的窗口中点击“start from scratch”,如图 9 所示:
图 9. 从零开始创建一个查询
- 在打开的窗口底部选择“Conditions”标签,在窗口右上角点击加号,选择“Add Conditions…”添加查询条件,如图 10 所示:
图 10. 添加查询条件
根据需要选择一些查询条件,为这个查询取一个名字,保存。如图 11 所示:
图 11. 添加如下查询条件
- 共享刚刚创建的这个查询,供每个团队成员使用。如图 12 所示,在窗口底部选择“Details”标签,在窗口右上角点击“Share”,选择“Team or Project Area…”。
图 12. 共享查询
在弹出的窗口中选择共享这个查询给哪个团队,如图 13 所示,然后点击 OK,保存。
图 13. 选择共享团队
- 根据需要定制查询结果的布局,包括选择显示哪些列,按照哪些列排序等,如图 14 所示:
图 14. 定制查询结果的布局
- 显示查询结果。如果按照上述配置,查询结果将在“Work Items”标签下显示,如图 15 所示:
图 15. 查询结果
这种召开 daily scrum 的方式能够列出在最后一天工作任务项的变化,以及哪些工作任务项还没有完成。但是它们都是以列表的方式显示出来,界面友好性和可读性不是很好。
结束语
本文介绍了 daily scrum 在团队项目开发中的重要性,以及三种用 IBM Rational Team Concert 进行 daily scrum 的方式:默认的 sprint backlog,定制 sprint backlog,和创建自定义查询。这三种方式各有其优缺点:
- 当团队人员比较少,一般小于 5 人,并且只是关注当前 sprint task 时,比较适合采用第一种方式进行 daily scrum。使用它可以清楚的看到 Task 与 User Story 之间的层次关系,以及 User Story 的开发进度。
- 当团队人员比较多,规模比较大,有自定义的 RTC Task 或者有子 Scrum Team 时,比较适合采用第二种方式进行 daily scrum。使用它与第一种方式一样,也是只关注当前 sprint task,但它还可以按照自定义的方式分组显示,更清楚的了解每个团队成员的 task 状态。
- 当希望关注团队中所有 Task,而不仅仅是当前 sprint task 时,前两种方式都无法满足 sprint plan 的显示需求,可以考虑使用第三种方式自定义创建查询,进行 daily scrum。与前两种方式相比,它更加灵活,建立查询的条件非常丰富,可以根据需要创建多个查询同时使用。
请用户根据自己的需要选择不同的方式进行 daily scrum,进行高效的团队项目开发。
@import url(http://www.blogjava.net/CuteSoft_Client/CuteEditor/Load.ashx?type=style&file=SyntaxHighlighter.css);@import url(/css/cuteeditor.css);
Rational Team Concert 是一个建立在可伸缩,可扩展平台上的团队协作开发工具,提供了很多功能,整合了软件开发项目生命周期的所有任务,包括计划、迭代、流程定义、变更管理、缺陷跟踪、源代码控制和源代码管理、产品构建自动化,和各种各样的分析报告等。
1 介绍 Rational Team Concert
Rational Team Concert 是建立在 Jazz 技术平台上,支持若干种 Agile 开发模型。Jazz 技术平台使软件开发更加灵活,支持团队成员分布在不同地理位置,提供从小型团队到大型企业的可扩展的软件开发解决方案。Rational Team Concert,有时简称 RTC,具有如下特性 :
使用 Rational Team Concert,在软件开发中,能够实现信息的交换和信息集成,如果某个需求变化了,团队成员就会自动收到通知,团队成员也可以通过多种方式了解这种变化。Rational Team Concert 中的各种视图可以让你更详细地了解信息,跟进团队的开发进度和活动。
Rational Team Concert 使开发团队能够轻松和有效地执行和定制流程,这个流程是角色、实践活动、规则、权限的集合。
Rational Team Concert 中,变更管理的主要特点是用工作项跟踪和协调各种任务,这些任务包括故事(story)、缺陷(defect)、计划项(plan item)、以及普通任务(task)等。工作项和工作流程是灵活可定制的,工作项也可以与其他的变更管理系统进行整合和集成。
Rational Team Concert 中提供了工具来保证计划管理能力,对于项目团队,这些工具能够计划、跟踪、平衡项目的工作量,以反映团队的实际状态。对于 Scrum,可以创建和管理迭代计划。
Rational Team Concert 内置的源代码控制管理系统是基于组件的和建立在 Jazz 平台上的,它支持并行和敏捷开发,支持分布在不同地理位置的团队开发,同时它紧密地集成了缺陷跟踪、构建、和以流程为中心的自动化。
对于开发和测试团队,Rational Team Concert 提供了自动地构建识别、构建控制和构建可追溯性。团队成员可以跟踪构建的进度,查看构建的警告信息和结果,提交构建请求,并跟踪构建过程。
Rational Team Concert 的报告组件能够显示项目的进展和项目状态,可以容易地分析某些可能被隐藏的趋势;软件开发过程中的可视化数据和各种分析报告,能够支持有效的决策。
- Eclipse 客户端,Visual Studio 客户端和 Web 界面
这些客户端界面为开发者提供了一个灵活的集成开发环境。
2 Rational Team Concert 与 Scrum
Rational Team Concert 是个非常优秀的 Agile 敏捷开发管理工具,内置了几个过程模板,可以用来支持一些敏捷开发方法,比如 Scrum 过程、OpenUP 过程和 Eclipse Way 过程等等。本文分享一些使用 Rational Team Concert 实现 Scrum 敏捷开发的使用经验。
Scrum 是一个典型的迭代式增量的敏捷软件开发过程。整个开发过程由很多次迭代组成,每一次迭代是一个 Sprint,每个 Sprint 的周期一般是 2 周到 4 周。在 Scrum 中,用 Product Backlog 来管理产品功能或项目的需求,用 Sprint backlog 管理每个 Sprint 的任务。在每个 Sprint 中,Scrum 产品负责人从 Product Backlog 中挑选最高优先级的需求,在 Sprint planning 会议上由团队成员讨论,估算工作量,确定 Sprint backlog 任务列表。在项目进行中,每天要举行 Scrum 例会(Daily Scrum meeting)。在每个 Sprint 结束时,Scrum 团队提交增量的可交付物。每个 Sprint 结束时,团队成员进行总结和回顾,吸取本次 Sprint 的经验教训,为下一个 Sprint 做准备。请参考图 1 Scrum 模型。
图 1. Scrum 模型
Scrum 由以下几个部分组成:
- 角色(Roles)
- 产品负责人(Product owner)
- Scrum 负责人(Scrum master)
- 团队成员(Team member)
- 各种仪式和会议(Ceremonies)
- 每天 Scrum 例会(Daily Scrum meeting)
- Sprint 计划会议(Sprint Planning meeting)
- Sprint 评审会议(Sprint Review meeting)
- Sprint 回顾会议(Sprint Retrospective meeting)
- 工件(Artifacts)
- Product Backlog
- Sprint Backlog
- Burndown Chart
- Impediments List
在 Scrum 中,产品功能或项目的需求会列在 Product Backlog 中。Product Backlog 是一个项目所需的所有需求或功能的优先级列表,这个列表条目常常以用户故事(user story)的形式体现。产品负责人(product owner)维护这个列表,根据项目的进展和商业环境的变化修改优先级列表。产品负责人对产品的成功负责,定义产品特性和产品发布时间表,负责确定各种功能的商业价值,不断完善和优化 Product Backlog。
Scrum 负责人(Scrum master)管理 Scrum 过程,确保 Scrum 的做法是正确的,并且让团队成员理解 Scrum 的价值,消除项目进展中遇到的障碍,并保护团队成员不受外界干扰。
在每个 Sprint 开始的时候,小组举行 Sprint Planning 会议。在 Sprint Planning 会议上,产品负责人为即将到来的 Sprint 展示最想要实现的产品功能或项目需求,让团队成员把握和分析需要实现的功能,产品负责人和团队成员在本次 Sprint 中的目标达成一致,确定未来 2 周到 4 周的工作重点。然后团队成员决定如何完成这次 Sprint 的目标,并分解成所需的任务,这些任务就组成了 Sprint Backlog 的任务列表。在 Sprint Backlog 中,每个任务按小时预估完成时间,团队成员确定是否可以按时完成开发任务,如果没有足够的时间完成某个功能,可以将该功能从当前的 Sprint Backlog 中返回到 Product Backlog。Sprint Backlog 中列出了团队成员已经承诺在本次 Sprint 期间完成的工作。根据团队经验来评估工作量,而不是由 Scrum 负责人或产品负责人决定,这是 Scrum 的一个特点。
在每个 Sprint 结束,需要召开 Sprint Review 会议,评审已经完成的工作。Sprint Review 会议是一个简短和非正式的会议,任何感兴趣的人都可以参加,并从参与者得到一些反馈。
团队成员可以举行 Sprint 回顾会议(Sprint Retrospective),分析项目的经验。通过本次 Sprint 回顾会议,不断改进团队工作方式和不断提高工作效率,为下一个 Sprint 做好准备。
Rational Team Concert 中提供了 Product Backlog 和 Sprint Backlog 的功能,它们同 Scrum 敏捷开发中的重要工件 Product Backlog 和 Sprint Backlog 一致。
在 Rational Team Concert 1.0 中,如果创建 Product Backlog,切换至 Team Artifacts 视图,并在项目区域中,选择 Release 1.0,然后选择 New > Plan。在 New Plan 窗口中,输入 Product Backlog 作为名字。选择 Product Backlog 作为 Plan Type。
在 Rational Team Concert 3.0 中,在 PlansAll plansMain DevelopmentBacklog 下,有默认的 Product Backlog。打开 Product Backlog,点击 Planned Items 项,可以为 Product Backlog 添加工作项,这些工作项的类型为 Epic 和 story,对于 Scrum,类型为 story 和 epics 的工作项,描述了 Agile 中的用户故事,包括项目需求或产品功能。
在添加所有的工作项之后,产品负责人要为工作项设置优先级,优先级属性有 High、Medium 和 Low,这可以定义实现工作项的优先级顺序。
图 2. Product Backlog
图 2 大图
在 Rational Team Concert 中,Sprint Backlog 中包含了来自于 Product Backlog 相关的具体工作项。RTC3.0 含有默认的 Sprint,例如 Sprint1,Sprint2,并且有默认的 Sprint Backlog,对于有多个 Sprint 的项目,如果要创建新的 Sprint Backlog,首先需要创建 Sprint,然后才能为该 Sprint 创建 Sprint Backlog。打开 Sprint Backlog,在 Sprint Backlog 的 Notes 页面上,能够填写 Sprint 的目标,在 Planned Items 页面上,可以为 Sprint 添加工作项,然后,详细分解工作项,定义任务。
图 3. Sprint Backlog
还有另外一种方法为 Sprint 添加工作项,在 RTC 中,打开 Product Backlog 窗口,选择相关的工作项,然后右击并选择 Plan For,把这个工作项指定给某个 Sprint。
图 4. 给 Sprint 添加工作项
图 4 大图
在 Sprint Planning 会议中,团队成员分析需要完成的任务,为每个任务估计时间,
当估计完所有的工作项之后,可以看到每一个故事的总体估计值,以及整个 Sprint 阶段的总体时间估计值。
一般来说,Scrum 负责人分配任务给团队成员,团队成员也可以主动领取,在 Sprint Backlog 的列表内,可以实现将任务分配给团队成员。
图 5. 给工作项分配所有者
图 5 大图
在每个 Sprint,团队成员要每天更新 Sprint Backlog 中的工作项状态和时间估计,这样,根据更新后的工作项,RTC 便可以产生一个 Sprint Burndown 图表,这个 Sprint Burndown 图表以图形方式显示剩余的工作项和工作量,显示项目的进展,预测项目的未来情况。
3 使用 Rational Team Concert 有效地进行每天的 Scrum 会议
Agile 敏捷开发实践中,强调团队的自我管理。在 Scrum 中,自我团队管理体现在每天的 Scrum 会议中和日常的协同工作,在每天的 Scrum 例会中,团队成员一般回答一下几个问题 :
- 昨天完成了什么?
- 今天要做什么?
- 项目进展中,遇到了什么障碍和问题?
整个会议应该少于 15 分钟。这种经常性的沟通,让团队成员能够了解每个人都在做什么,他们正面临着什么问题,有什么事情需要其他团队成员帮助解决,提高团队成员的协作。
在 Scrum 中,要坚持举行每天的 Scrum 会议 , 了解团队成员的工作进展和遇到的问题,Scrum 负责人要维护一个障碍列表(Impediments List),帮助解决团队成员遇到的阻碍和问题,保证项目顺利进行。
每天的 Scrum 会议可以增加团队成员之间的沟通,并帮助团队成员更有效地工作。
在 Rational Team Concert 中,通过集成的视图,团队成员能够了解各种任务、计划、工作项,也可以查看当天需要完成的工作项,当团队成员更新每日的工作项时,其他成员也可以看到。
在 Rational Team Concert 中,通过持续跟进工作项,团队成员可以更好地了解工作项的优先级,集中精力在优先级高的工作项上,保证项目的正常进展。团队成员还可以规划自己的工作内容和更新剩下的工作项。例如,在 Rational Team Concert 的迭代计划编辑器中,团队成员可以直接看到今天或本周的工作项。这些都有助于每天的 Scrum 会议,了解每天的 Scrum 会议中的问题。
Team Central 视图中的 Team Load 部分也可以显示团队工作负荷,在每天的 Scrum 会议之前,Scrum 负责人可以监控团队成员工作负荷。
Rational Team Concert 提供了 My Work 视图以帮助每一位团队成员查看和跟踪自己的工作项状态。在 Sprint 阶段,团队开发人员可以在 My Work 视图中看到任务和工作项。
图 6. My work 视图
Planned Time 视图可以查看工作项的剩余时间,支持 Daily Scrum 会议,团队成员可以根据 Planned Time 视图讨论哪些已经完成和哪些还没有完成。为了帮助跟进每个工作项的工作量,团队成员应该在 RTC 中每一天更新每个任务的剩余时间。
图 7. Planned Time 视图
图 7 大图
开发员的任务面板也可以分配和监视工作项,它显示了每个团队成员的任务。
团队成员可以使用查询来监视工作的进展状况,Rational Team Concert 已经有很多可用的预定义查询,还可以轻松创建新的查询。预定义的查询,预定义了一些查询条件,可以直接用来查询工作项,比如,'Open assigned to me','Recently modified'等,这些预定义的查询可以用于每天的 Scrum 会议,团队成员和 Scrum 负责人可以快速了解每个工作项的进展。
图 8. 创建一个新查询
对于地理位置上分散的团队,每天的 Scrum 会议有时通过电话或视频会议进行。Rational Team Concert 可以更好地辅助管理每天的 Scrum 会议,可以很快捕捉和处理阻碍项目的事情,然后,通过团队成员的协作,完成这些工作项或重新分配这些工作项。
4 在 Scrum 中,用 Rational Team Concert 进行软件源代码控制管理
在 Agile 敏捷开发最佳实践中,持续集成和自动化构建可以保证高质量的软件开发,持续地交付有价值的软件产品来提高客户的满意度。作为软件开发项目,需要一个高效和协作的软件源代码控制管理系统。Rational Team Concert 就是这种源代码控制管理系统,可以进行变更管理和配置管理,帮助开发团队管理源代码、管理文档、跟踪代码和共享代码的各种变化,并保持整个开发团队的高效协同工作;同时,Rational Team Concert 提供了自动地构建增量可交付物的功能,实现了软件开发的持续集成和自动化构建,实现高效的敏捷开发。
下图显示了在 Rational Team Concert 中源代码控制流转过程,开发人员把变更的源代码检入到存储库工作空间;然后提交到共享的开发流中,其他开发人员接受这些变更的源代码,并且装载到存储库工作空间中。
图 9. 源代码控制流转过程
在 Rational Team Concert 中进行源代码检入(check in)和检出(check out),需要连接存储库和项目区域,下载源代码到本地存储库工作空间中。首先把源代码从开发流装载到本地存储库工作空间,然后,在本地存储库工作空间修改源代码,提交变更的源代码到开发流中。开发人员在 Pending Changes 视图中,展开 Unresolved 节点,检入源代码,加上一些注解,然后,在 Outgoing 节点下,通过提交(Deliver)的功能,就可以把变更的源代码提交到开发流中。
当开发人员提交了变更的源代码到开发流中,团队中其他成员就可以接受这些变更,把变更的源代码同步到自己的本地存储库工作空间中。开发人员在 Pending Changes 视图中的 Incoming 节点下,选择变更集,然后,接受(Accept)这些变更,这些变更的源代码就进入了自己的存储库工作区。
5 在 Scrum 中,灵活使用 Rational Team Concert 的工作项
在 Agile 敏捷开发中,以用户故事(user story)的形式定义各种需求,Rational Team Concert 为 Agile 敏捷开发提供了类型为故事(story)和历史(Epic)的工作项,可以定义用户故事,定义的工作项会显示在 Product Backlog 中。在每个 Sprint,Sprint Backlog 中的任务也是一种类型的工作项,同时,工作项也是跟踪、协调开发任务和工作流转的基本机制,它是各种部件和元素之间的联系枢纽。
在 Scrum 过程中,Rational Team Concert 提供了一些常用的预定义工作项类型,这些类型的工作项全面支持 Scrum 敏捷开发。
- 缺陷(defect):定义缺陷和跟踪缺陷。
- 回顾(retrospective):记录先前正常但在最近完成的迭代中不再正常的内容。
- 故事(story):描述用户故事和需求。
- 历史(Epic):用户故事或需求很大而需要在多个迭代(Sprint)中完成,或者由于未知情况过多而无法估计工作量的用户故事。
- 任务(task):描述特定的工作任务。
- 障碍(impediment):跟踪导致无法取得进展的因素。
在 Rational Team Concert 中,Product Backlog 中的用户需求或产品需求是通过工作项来描述,在 Scrum 的每次迭代中,用户需求或产品需求会被分解成为足够小的类型为任务的工作项,放在 Sprint Backlog 里,并且每一个任务是被赋予了优先级。
在工作项中有很多属性,充分和准确使用这些属性,Rational Team Concert 可以让 Scrum 敏捷开发更有效率。
图 10. 工作项属性
摘要(Summary)字段是一个工作项的简短总结和标题,可以让 Scrum 成员和 Scrum 负责人快速理解工作项内容。
工作项的类型(Type)定义了工作项的特性,包括缺陷(defect),任务(Task),故事(Story)等,不同的类型有不同的属性和不同的状态变化。类型为故事(story)的工作项,可以描述 Scrum 的 Product backlog 中的用户需求或产品需求。类型为任务(task)的工作项可以定义 Scrum 中每一次迭代(Sprint)的任务。类型为缺陷(defect)的工作项可以记录每个 Sprint 中测试验证阶段的缺陷,跟踪缺陷的修复状态和进展。
严重级别(Severity)定义了工作项的严重等级。
描述(Description)字段详细描述了工作项的目标和相关信息,描述 Scrum 中的需求和任务。
所有者(owner),显示这个工作项当前的拥有者或执行者。在 Scrum 中,团队成员可以通过这个字段知道自己负责的任务,Scrum 负责人可以分配任务和了解团队成员的任务情况,在每天的 Scrum meeting 时,可以监控 Sprint 的进展。
优先级(Priority)属性指定这个工作项的重要性和优先顺序。高优先级的工作项将会被优先开发并确保完成。低优先级的任务有可能被转入下一个迭代(Sprint)周期继续开发。
计划目标(Planned for)属性指定这个工作项属于某个 Sprint。
状态 / 解决(State/Resolution),显示这个工作项的当前状态。
在 RTC 中,灵活使用工作项,可以提高 Scrum 的执行效率,下面介绍一些使用工作项的技巧:
在摘要(Summary),描述(Description)和讨论(Discussion)字段中,支持粗体("bold")和斜体("italic"),也可以创建与其他工作项的链接。在工作项中,可以选择一些文本,使用上下文菜单中提取工作项的功能(Extract Work Item),提取相关的工作项内容。
在讨论(Discussion)字段中,添加评论,也可以和评论的作者进行聊天会话或发送邮件。
在快速信息(Quick Information)部分 , 可以通过上下文菜单添加订阅者、附件、链接到其他工作项,也可以附加屏幕截图。
可以使用"查找"对话框,搜索包括摘要(Summary),描述(Description)和讨论(Discussion)部分的内容。
在编辑器的工具栏中,可以使用'寻找潜在的重复工作项'(Find Potential Duplicates),发现可能重复定义的工作项。
6 总结
Rational Team Concert 是一个建立在可伸缩和可扩展平台上的团队协作开发工具,整合了软件开发项目生命周期的所有任务,包括计划、迭代、流程定义、变更管理、缺陷跟踪、源代码控制和源代码管理、产品构建自动化,和各种各样的分析报告等。Rational Team Concert 有力地支持了一些 Agile 敏捷开发方法,利用 Rational Team Concert 进行 Scrum 敏捷开发,能够开发出高质量的产品和项目,能够进行高效率的协同工作,持续的集成和自动化构建交付物。
@import url(http://www.blogjava.net/CuteSoft_Client/CuteEditor/Load.ashx?type=style&file=SyntaxHighlighter.css);@import url(/css/cuteeditor.css);
SPRING BATCH 柯以测试的内容:JOB, STEP, INTEMPROCESSOR, ITEMREADER, ITEMWRITER。
JOB, STEP属于功能测试(黑盒)的范畴,INTEMPROCESSOR, ITEMREADER, ITEMWRITER属于单元测试(白盒)的范畴。
/*
* Copyright 2006-2007 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import java.util.Date;
import java.util.concurrent.Callable;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.item.ExecutionContext;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemStream;
import org.springframework.batch.item.NonTransientResourceException;
import org.springframework.batch.item.ParseException;
import org.springframework.batch.item.UnexpectedInputException;
import org.springframework.batch.test.JobLauncherTestUtils;
import org.springframework.batch.test.MetaDataInstanceFactory;
import org.springframework.batch.test.StepScopeTestExecutionListener;
import org.springframework.batch.test.StepScopeTestUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
@ContextConfiguration(locations = { "/test-context.xml",
"classpath:/META-INF/spring/batch/hello-tasklet-context.xml",
"classpath:/META-INF/spring/batch/jdbc-job-context.xml",
"classpath:/META-INF/spring/integration/hello-integration-context.xml"})
@RunWith(SpringJUnit4ClassRunner.class)
//测试ITEMREADER/ITEMPROCESSOR/ITEMWRITER时用到
@TestExecutionListeners( { DependencyInjectionTestExecutionListener.class,
StepScopeTestExecutionListener.class })
public class HelloTaskletTests {
@Autowired
private JobLauncher jobLauncher;
@Autowired
private Job helloWorldJob;
@Autowired
private JobLauncherTestUtils jobLauncherTestUtils;//测试JOB/STEP的入口
@Autowired
private ItemReader xmlReader;
public void testLaunchJobWithJobLauncher() throws Exception {
JobExecution jobExecution = jobLauncher.run(helloWorldJob, new JobParameters());
assertEquals(BatchStatus.COMPLETED, jobExecution.getStatus());
}
/**
* Create a unique job instance and check it's execution completes
* successfully - uses the convenience methods provided by the testing
* superclass.
*/
@Test
public void testLaunchJob() throws Exception {
JobExecution jobExecution = jobLauncherTestUtils.launchJob(jobLauncherTestUtils.getUniqueJobParameters());
assertEquals(BatchStatus.COMPLETED, jobExecution.getStatus());
}
public void testIntegration()
{
while(true)
{
}
}
/**
* 测试某个STEP
*/
@Test
public void testSomeStep()
{
JobExecution jobExecution = jobLauncherTestUtils.
launchStep("xmlFileReadAndWriterStep",getJobParameters());
assertEquals(BatchStatus.COMPLETED, jobExecution.getStatus());
}
/**
* 测试READER的方式1时,所需的方法
* @return
*/
public StepExecution getStepExecution() {
StepExecution execution = MetaDataInstanceFactory
.createStepExecution(getJobParameters());
return execution;
}
/**
* 测试READER的方式1
* @throws Exception
*/
@Test
@DirtiesContext
public void testReader() throws Exception {
int count = StepScopeTestUtils.doInStepScope(getStepExecution(),
new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int count = 0;
try {
((ItemStream) xmlReader)
.open(new ExecutionContext());
while (xmlReader.read() != null) {
count++;
}
return count;
} finally {
((ItemStream) xmlReader).close();
}
}
});
assertEquals(3, count);
}
/**
* 测试READER的方式2
* @throws UnexpectedInputException
* @throws ParseException
* @throws NonTransientResourceException
* @throws Exception
*/
@Test
@DirtiesContext
public void testReader2() throws UnexpectedInputException, ParseException, NonTransientResourceException, Exception
{
assertNotNull(xmlReader.read());
}
/**
* 测试READER的方式2时,必须加的方法
*/
@Before
public void setUp() {
((ItemStream) xmlReader).open(new ExecutionContext());
}
/**
*
* @return
*/
private JobParameters getJobParameters() {
String inputFile = "/Users/paul/Documents/PAUL/DOWNLOAD/SOFTWARE/DEVELOP/"
+ "SPRING BATCH/spring-batch-2.1.9.RELEASE/samples/"
+ "spring-batch-simple-cli/file/trades1.xml";
String outputFile = "/Users/paul/Documents/PAUL/DOWNLOAD/SOFTWARE/DEVELOP/"
+ "SPRING BATCH/spring-batch-2.1.9.RELEASE/samples/"
+ "spring-batch-simple-cli/file/output/out.xml";
JobParameters jobParameters = new JobParametersBuilder()
.addString("input.file.path", inputFile)
.addString("output.file.path", outputFile)
.addDate("date", new Date()).toJobParameters();
return jobParameters;
}
}
参考例子:
前段时间招聘。因为我一直在我的部门推行一些有效却被绝大多数中国公司忽视的开发理念,比如平级人事结构、测试驱动开发、制度化绩效、设计先行、迭代开发等等,所以招软件设计师非常困难。最终问题还算解决了吧。以下是我的面试总结。
一般来说,作为迎接面试的人,我会借着询问路况、接引进会议室或者索要简历,来表达出自己的礼貌,让对方有一定紧张感。这样我认为有利于对方表现出自然状态的思路。
然后我会根据简历,大概咨询下对方工作中所做过的设计工作。因为我在招软件设计师,所以只问设计,看看他对设计的理解是什么样子。通过这种询问,可以考察对方的简历是否作假,如果作假那么无法明确讲述其过往工作。还需要看对方的表达能力,即主动展现自己思路的能力。按照我这一批面试的人看,大多数人会讲述其项目经历的业务或者架构。只有一个人能够把软件设计和架构设计、软件开发分离出来。
然后就会开始做面试题了。面试题附在下边。我会先让其看第一大题,设计能力,请ta选择一道题目作答。看题之后,对方一般会陷入思考沉默。那么根据对方眼神不再在题目间扫动,表示对方已经针对某一个题目思考。当然,如果是沟通能力好的人,这这之前会主动告诉我,ta准备作答哪道题目。此时需要打断沉默,对对方说,希望对方谈一谈想法,任何一点想法都可以说出来。这个时候如果对方能够针对题目问一些具体没表示明白的细节,或者自行设定细节,都表明此人沟通能力极好,否则应该认为其沟通能力打折扣。
当对方陆陆续续讲述自己的设计时,作为面试的人,需要指出其没有思考到的地方,或者赞扬对方想的很合理。一般来说,面试的人在经历了之前的客套和紧张之后,不太容易沉下心来仔细思考。凡是这时候依然能保持有序高效的思考能力,说明这个人抗压能力极强,至少是心理调节能力极好。通过这时的回答,就可以看出此人对设计是否了解,设计能力怎么样。
以 1.1 为例,我随便说几个要点。比如说,我们应该抽象出牌局状态这么一个类,作为传输给每一个玩家的内容。比如说,我们可以抽象出一张牌这么一类,作为出牌的玩家上报的内容。比如说,此场景应该有一个短连接请求处理类,还应该有一个房间控制类;房间控制类里边维护一个个开通的房间;当每一个进入房间的请求来临时,都应该通过房间控制类,将连接转移到这个对应的房间编号;那么房间编号可以由客户端通过参数传递上来。比如说,每一个房间能够维护一组长连接,这可以开一个线程来处理;线程由一个单例的线程维护器对象来启动。比如说,房间在新线程中执行的代码,应该是轮询每个长连接看谁发来了“牌”,然后调用数据计算模块,通过牌和原有牌局(保存在房间对象里),得到新牌局,发送给每一个连接。当然了,如果能对断链识别什么的做出设计就更好了。不过控制此题时间不要超过 10 分钟,所以一般不可能讲述出太多内容。
接下来,要根据刚才设计方面的能力,中转到 2 或者 3 。如果设计能力不错,则应该转移到 3 ;如果设计能力不好,则应该转移到 2 。这个设计能力的好不好,是根据面试者觉得达标不达标,够用不够用来说的。
先来说说转到 2 编程能力测试的情况。首先,应让其在 2.1 和 2.2 中选一道作答。2.1 的要点,是建立合适的数据结构。最佳的是 money 与 id 双排序的基础对象构建的 TreeSet ,通过 NavigableSet 提供的子树功能,以 size() 获取名次。自己前后几位可通过正反向迭代器取得。当然,也可以使用链表和散列集合的二重结构。这要慢不少,但是也还算在可接受范围内能实现功能。
此题非常多的面试者会联想到 SQL 的 order by 子句。问题是这些人应该设计不出合适的二维表结构,配合 SQL 实现想要的功能。如果面试者能够实现,那也至少算能够做出来一种实现,不应苛求。
2.2 我认为用二维表传统关系型数据库很合适。应该有二个表,一个是历史表,一个是实际操作记录表。每次受影响玩家登录,都会激活从实际操作记录表到历史记录表的总结过程;这是一种最小型的数据挖掘。受试者能够正确写出总结过程,能够正确写出读取历史表的合并过程,则是满分。
编程过程测试,可以随时提供设计思路指导。旨在督促受试者利用短暂的面试时间,认真思考,发挥出最强的思维能力,以检测其通过自己思考解决问题的水平。如果能在不断的设计方案提示下,指出语句写法,则应该认为设计能力为零,但编程基本功很扎实。如果算法也需要提示,则认为编写程序的技能本身不好,但语言知识尚可。
做完 2.1 和 2.2 其中一题,应准备查资料的条件,也就是连接互联网的浏览器,让其做第 3 题。如果其对 Java5.0 多线程类库非常了解,则认为此人关注技术新闻,有很强的技术敏感性和学习能力。否则则应通过观察其查资料选用关键词的方式、查看内容的耐心敏感度,来判断此人的学习能力和心性。至此,您应该对面试的人有了较全面的认识。推荐记录下来,以便比较不同的面试者,谁综合看来更适用。
我们再来说说直接从 1 题转移到 3 题的情况。同样是 3.1 和 3.2 选做一题,来测试受试者的架构能力。以和设计能力测评同样的节奏进入互动,然后探寻受试者思路。架构的核心,在于指派分布,即指派哪些逻辑运行在什么地方,以及这些“地方”的布局方式。非常多的受试者无法分清软件设计和架构设计的区别。每当这个时候,面试官就应该主动给受试者讲述架构的含义,同时记录受试者不具备可用的架构工作经验,但应根据接下来的思路表现,来判断其架构见识、思路以及资质。
3.1 的要点在于网络存档应该与应用服务分离。应用服务做负载均衡的话,应制作单独的数据服务以支持网络存档。这样可以保证无论是运营后台还是玩家都能够看到全部的数据。如果受试者无法摆脱分区的概念,则应指出,运营后台页面,能够选择分区,每次选择,程序能够连接到不同区的数据服务。
3.2 首先这种全球性质或者打地理范围的系统,肯定要使用地域分流的域名服务(DNS)负载均衡。那么也需要建立专门的用户数据传输机制,以保证用户大范围移动物理位置之时,能够在对应区域取得自己的数据。为此,也就需要设置统一的数据中心,以登记不同区域的位置关系以及服务器地址。考虑到系统不可能一次性完成,使用能够自动加载的云基础结构,应该会是一个最合理的选择。
通过架构测评,如果受试者能够展现出关于自由软件的很多知识,则说明此人在技术进步上有较高追求。架构这边如果用时很短,也就是受试者表示出了对架构的恐惧和放弃,应该认为受试者面对困难问题的解决决心坚韧程度打折扣。这时应将其引导至 2.3 题,以考察其通过查资料解决问题的水平和心性。面试完毕之后,您应该记录面试情况,以便比较不同的面试者,谁综合看来更适用。
最后我想说一句。很多面试官都相信自己对受试者的感觉。其实面试流程的目的,就是通过流程,让受试者更多地表现自己,以丰富面试官的感觉,以防以偏概全、认识不足等情况发生。
下边附面试题目
1 设计能力(选做其一)
---------- ---------- ---------- ----------
1.1 现有多人卡牌游戏,由用户根据场上情况出牌。玩家出的牌可被其它玩家看到。每一种游戏牌将会对场上局面造成某些影响。请设计本游戏服务器端关于数据传输部分;其它部分如需指明,也可以指出。
1.2 现有某军事对战型网络游戏。架构设计安排在战斗发起之前,通过短连接方式进行信息处理。对战为一对一,对战开始之后,进行长连接,传输双方对军队控制的操作。请设计本游戏开始建立长连接以及传输操作数据部分的服务器程序。
2 编程能力(2.1 和 2.2 选作其一)
---------- ---------- ---------- ----------
2.1 数据结构
有一种数据,结构是
{
id: 38,
name: "Shane",
money: 3010.50
}
数据量大概有 100 0000 份。
请你设计一种方式,能够支持以下要求。
首先,需要能往已有数据集里边追加数据。如果 id 重复,则为修改。
其次,需要得到某指定 id 对应的人,全部数据按照 money 排序,其所在的名次。此过程不能太慢。
最后,能得到这个 id 对应的人,全部数据按照 money 排序,其名次的前、后几名的 name 。此过程不能太慢。
2.2 逻辑
一个社交游戏,玩家可以互相访问,并在访问时对对方进行某些操作。被访问的人在登录时统一接收上次登录到这次登录之间被访问的报告。
获取记录通过 InteractSysRecord.INST.getRecord(int userId) 方法,获得一个 List<ActRecord> 。
ActRecord 具有如下属性
int fromUserId
int toUserId
Kind actKind
int actEffect
其中 Kind 是一个枚举,包括一些类型,比如“赠送礼金”、“伤害”、“偷钱”等。
玩家获取的记录,需要按人整理。也就是说,在一个玩家登录之时,与其上次登录之间,某一另外玩家多次访问此玩家生成的多个 ActRecord ,应该合成一条。
请编程完成记录整理。允许设计合理的 InteractSysRecord 结构。
2.3 多线程(允许查资料,希望观点能独特、精辟、有实效)
请简述 Java5.0 多线程框架的机制和要点
3 架构能力(选做其一)
---------- ---------- ---------- ----------
3.1 请为如下功能需求设计架构。文字或图示都行。
现有某多客户端弱联网网络游戏,需要实现网络存档。请简述网络游戏存档的架构方案。注意,后台操作人员应该能任意查看、修改任何人的存档。要求说明设计理由。
3.2 现有基于位置的移动网络游戏。在游戏界面中,当玩家离开自己实际位置之时,就会在游戏中受到一个吸引力,吸引游戏中的角色回到玩家现实中的位置。此吸引力随着距离增加而增加。所有的玩家在统一的世界地图上进行对战。请设计此网络游戏的架构方案。并说明设计理由。
@import url(http://www.blogjava.net/CuteSoft_Client/CuteEditor/Load.ashx?type=style&file=SyntaxHighlighter.css);@import url(/css/cuteeditor.css);