Wednesday, December 31, 2008

[MySQL] The InnoDB Transaction Model and Locking

前段时间研究了sqlserver2005的事务与锁模式,最终的结果与自己以前的认识有相当大的出入,感觉受益非浅。因为我也明白,不同的数据库对ansi-sql定义的四个事务隔离级别的支持是不一样的,所以现在是时候看看mysql是怎么支持这些事务隔离级别的,它的锁行为又是怎么样的。
* 这里说的Mysql实际上是说Mysql5.0的InnoDB存储引擎,这是一个支持事务的引擎。
Mysql很早就实现了基于行版本的快照隔离级别(sqlserver2005中增加的实现),其实我也不清除这样的说法是不是正确,但是意思大致是相同的,就是数据库服务器会管理行数据的多个版本,每个事务只能获得一个快照。
Mysql支持不同粒度的锁,即row(record) lock和table lock. row lock表示应用到特定行上的锁, table lock表示应用到整个表上的锁。
LOCK TYPE
此外锁有两种类型:共享(S)允许事务读取一行,独占(X)允许事务更新或者删除一行。还有一种意向锁(intention locks),这种锁只会应用到table上,即这是一种table lock。它的作用就是用来说明当前事务(已经获得Is或者iX)想要请求表中某行的S或者X锁(这个概念与sqlserver的意向锁是相同的)。意向锁之间都是兼容的,即使IX与IX也是兼容的,这样多个事务之间可以同时在table上获得IX锁,但是只有一个事务可以获得某一行的X锁。而S和X锁即可以应用到行也可以应用到表,比如alter table就会在标上应用X锁。 关于锁的兼容性,可以参照下表:

X IX S IS
X Conflict Conflict Conflict Conflict
IX Conflict Compatible Conflict Compatible
S Conflict Conflict Compatible Compatible
IS Conflict Compatible Compatible Compatible
此外,Mysql有个很怪的行为,如果一个事务Ta读取记录R(select ...lock in share mode),获得了S锁,然后Tb在记录R上请求X锁(应该在表上应用IX锁),这个时候Tb会被阻塞,然后Ta要更新R,这个时候Ta会尝试从S锁升级到X锁(也会在表上应用IX锁,iX与IX是兼容的),这种情况下,我的理解是Tb会继续被阻塞,直到Ta事务结束释放所有的锁。但是mysql会抛出死锁异常,我都不知道怎么解释,难道先来先得,mysql必须先给Tb分配X锁??sqlserver采用的是阻塞行为,不会认为是死锁。

CONSISTENT NON-LOCKING READ

MySQL默认的隔离级别是repeatable read,如果要修改这个默认的隔离级别,可以通过修改my.cnf,设置如下:
[mysqld]
transaction-isolcation=REPETABLE READ
在READ COMMITTED和REPEATABLE READ的隔离级别下,MySQL默认会采用Consistent Read(一致读)。Consistent Read不会在行或者表上施加任何的锁(是指select...,不是select...lock in share mode或者select ... for update, 后面两种稍后会进行说明)。这样读不会阻塞写,写不会阻塞读,可以大大提高并发处理能力,但是数据不一致的风险也更大了。
在Read Committed下,select总是获得语句执行时数据库已提交数据的快照,而repeatable
Read下,同样的select语句,总是返回第一条select执行时数据库已提交数据的快照。比如事务Ta和事务Tb同时访问记录R,Tb稍后更新R到R1并且提交。如果Ta是Read Committed的,那么在Ta在Tb提交前后分别得到R和R1,而如果是repeatable read的话,Ta总是得到R。老实说,这种逻辑我很难理解,这里我觉得sqlserver处理的方式更合理。在sqlserver中
,如果Ta是Repeatable read,那么Ta在第一次读取R的时候就会获得R的S锁,并且不到事务结束不会释放,这样,Tb在想要修改的R的时候,会被阻塞,这样就保select...lock in share mode / select...for update证了Ta的可重复读。 而MySQL的这种处理方式有点怪,有种被蒙住眼睛,被欺骗的感觉。
select...lock in share mode / select...for update
Mysql默认会采用Consistent read,这样的select不会应用任何锁到资源(行或者表)上。但是可以显示的要求mysql的资源上应用锁,这就是select ... lock in share mode和select ...for update.
select ... lock in share mode会在匹配的行上应用S锁,直到事务结束才会释放。在什么情况下会需要应用S锁呢? 这里有表Parent和Child,每个Child中的记录必须关联到Parent中的一条记录。如果不采用S锁,我们在向Child中插入数据的时候,很可能Parent中关联行会被其他事务删除,这样数据就不一致了(当然也可以用数据库外键来保证)。如果我们在想child中进行插入的时候,先在parent管理记录上获得S锁,这样其他事务就无法修改这条记录了,就可以保证child与parent的一致性。
select ... for update这个与sqlserver的行为一致,不过sqlserver会加更新锁(U),而mysql会在资源上应用X锁。如果需要读取一个记录来进行更新的话,就可以使用select..for update。
InnoDB Record-level Lock

InnoDB has several types of record-level locks:

  • Record lock: 用来锁住一条索引记录。

  • Gap lock: This is a lock on a gap between index records, or a lock on the gap before the first or after the last index record.

  • Next-key lock: This is a combination of a record lock on the index record and a gap lock on the gap before the index record.

Record lock总是会锁住索引记录(index record),即使该表没有建立任何索引(这种情况下,mysql会自动创建一个clusterindex,然后record lock就会锁这些索引),实际上锁行就是锁索引。
默认情况下,事务隔离级别是repeatable read并且Innodb_locks_unsafe_for_binlog没有被激活,这个时候InnoDB会采用next-key lcok来搜索和扫描索引,从而避免幻影读取(为什么在repeatable read中避免幻影读取?)。 Next-key lock 组合了record lock和gap lock,除了会锁住索引记录以外,还会锁住索引之间的间隔。InnoDB会对访问到的每条记录加锁,尽管有些行根本不满足where子句的约定条件,这些记录会被加上Next-key lock。这就说明,我们在进行查询的时候,最好对索引行来查询,减少查询的访问范围,防止无辜的记录被锁上。如果针对一个没有索引的列来进行查询,那么会进行全表扫描,这就会导致整个表的记录都被锁上。
举例说明一下:
假设有表t,包含两列id int not null, name varchar(30) not null,其中id为primary key(即为一个cluster index),其中包含6条记录。
mysql> select * from t;
+----+------+
| i | name |
+----+------+
| 0 | t-0 |
| 2 | t-2 |
| 3 | t-3 |
| 10 | t-10 |
| 14 | t-14 |
| 15 | t-15 |
+----+------+
6 rows in set (0.00 sec)
首先在默认的事务隔离级别repeatable read下测试,假设两个事务Ta和Tb:
Ta>start transaction;
Ta>select * from t where name='t-2' for update;
Tb>start transaction;
Tb>insert into t(i,name) value(25,'t-25');
这个时候Tb会被阻塞,因为name不是索引字段,Ta会进行全表扫描,每条记录都被应用next-key lock(但是实际上这些锁又是锁在cluster indx record上的,这个概念和sqlserver应该是相同的,有cluster index的表就是cluster表,索引和记录是在一起的,相对于索引和记录分开的情况,cluster table会减少磁盘io,提高访问性能),所以Tb被阻塞。
接下来,我们使用索引进行查询。
Ta>start transaction;
Ta>select * from t where i=3 for update;
Tb>start transaction;
Tb>insert into t(i,name) value(25,'t-25');
这个时候Tb不会被阻塞,这里应该也并不表示Ta只会在i=3的索引记录上加锁,因为cluster index本质上是一个B树,所以应该是访问到的节点都被加锁(??)。

我们可以通过以下两种方式来取消gap lock(取消了gap lock,自然也取消了next-key lock,但是在readcommitted下仍然有next-key lock):
  • 如果事务的隔离级别 是read committed
  • 激活Innodb-locks-unsafe-for-binlog.

Locks Set by Different SQL Statements in InnoDB

实际上不论select还是update,delete都会把扫描的记录加锁,而不是仅仅将匹配where clause子句的记录加锁,可以通过下面的实验来验证。
Ta>set transaction isolation level read committed;
Tb>set transaction isolation level read committed;
Ta>start transaction;
Ta>update t set name='t-2-2' where name='t-2';
Tb>start transaction;
Tb>select * from t where name='t-1' for update;
这个时候Tb会被阻塞,因为Ta进行了全表扫描,所以的记录都被加上了X锁。为什么采取这种策略??SQLServer这里表现的更好,它只会锁住符合查询条件的记录。

InnoDB sets specific types of locks as follows:

  • SELECT ... FROM is a consistent read, reading a snapshot of the database and setting no locks unless the transaction isolation level is set to SERIALIZABLE. For SERIALIZABLE level, the search sets shared next-key locks on the index records it encounters.

  • SELECT ... FROM ... LOCK IN SHARE MODE sets shared next-key locks on all index records the search encounters.

  • SELECT ... FROM ... FOR UPDATE sets exclusive next-key locks on all index records the search encounters.

  • UPDATE ... WHERE ... sets an exclusive next-key lock on every record the search encounters.

  • DELETE FROM ... WHERE ... sets an exclusive next-key lock on every record the search encounters.

  • INSERT INTO ... VALUES (...) sets an exclusive lock on the inserted row. This lock is not a next-key lock and does not prevent other sessions from inserting into the gap before the inserted row. If a duplicate-key error occurs, a shared lock on the duplicate index record is set.

更多信息请参考Mysql5.0 Mannual:13.2.9 The InnoDB Transaction Model and Locking

Friday, December 26, 2008

JGroups - A toolkit of reliable multicast communication.

以前一直经常会看到一些框架依赖jgroups包,当时没有特别在意,以为又是jcp出来的某个规范的一个实现,因为精力有限,所以也没有用心去了解它到底是怎么一个东西。 这次因为用ibatis,然后研究ibatis的cache,而ibtis支持使用oscache。oscache是一个分布式的缓存系统,就是说,它是支持集群环境的,而对集群环境的支持就是通过jgroups来实现的。
终于说到jgroups,其实很多项目都用来jgroups来实现集群功能,包括jboss,jetty,jonas,tomcat等,当然还有oscache。
在开始深入研究jgroups之前,首先需要搞清除IP地址的意思和什么叫多播。

A类IP地址,B类IP地址,C类IP地址,E类IP地址

IP地址包括网络位和地址位,一般将ip地址与子网掩码进行&操作,就可以得到哪些是网络位。
1. A类IP地址 (1.0.0.0到126.255.255.255)
一个A类IP地址由1字节的网络地址和3字节主机地址组成,网络地址的最高位必须是“0”, 地址范围从1.0.0.0 到126.0.0.0。可用的A类网络有126个,每个网络能容纳1亿多个主机。
2. B类IP地址 (128.0.0.0到191.255.255.255)
一个B类IP地址由2个字节的网络地址和2个字节的主机地址组成,网络地址的最高位必须是“10”,地址范围从128.0.0.0到191.255.255.255。可用的B类网络有16382个,每个网络能容纳6万多个主机 。
3. C类IP地址 (192.0.0.0到223.255.255.255)
一个C类IP地址由3字节的网络地址和1字节的主机地址组成,网络地址的最高位必须是“110”。范围从192.0.0.0到223.255.255.255。C类网络可达209万余个,每个网络能容纳254个主机。
4. D类地址用于多点广播(Multicast)(224.0.0.0到239.255.255.255)
D类IP地址第一个字节以“lll0”开始,它是一个专门保留的地址。它并不指向特定的网络,目前这一类地址被用在多点广播(Multicast)中。多点广播地址用来一次寻址一组计算机,它标识共享同一协议的一组计算机。
5. E类IP地址 (240.0.0.0到247.255.255.255)
以“llll0”开始,为将来使用保留。
全零(“0.0.0.0”)地址对应于当前主机。全“1”的IP地址(“255.255.255.255”)是当前子网的广播地址。

在IP地址3种主要类型里,各保留了3个区域作为私有地址,其地址范围如下:
A类地址:10.0.0.0~10.255.255.255
B类地址:172.16.0.0~172.31.255.255
C类地址:192.168.0.0~192.168.255.255

广播(Broadcast),多播(Multicast)

广播就是向网络内的所有主机发送信息。广播地址一般是一个网段内的最大主机号,比如ip为192.168.0.120, 子网掩码为255.255.255.0,这是一个c类地址,前三个字节为网络地址,这个网段内的广播地址就是192.168.0.255.
多播(Multicast)是在一个网络上 一单个发送者和多个接收者之间的通信。多播技术在很多接收者同时想收听或看相同的源通过递送一单个信息流给很多接收者时被用于减少网络通信(因为广播会通知一个网络内所有的主机,而有些主机可能是对广播信息不感兴趣的,而多播就保证只有感兴趣的主机才会接受到信息)。对一个团体视 频会议,一个(n-1)次低的带宽被需要。“多播”被典型地用于指使用一个多播地址的IP多播。IPv6支持单播,多播,和任意播,但是在IPv6中广播 作为一个术语已经消失,但是被认为是一种形式的多播。但是那里有其它协议执行多播概念例如异步传输模式(ATM),它已经嵌入到“点-到-多点”或“多点-到-多点”连接机制中。
多播地址就是一个D类地址,从224.0.0.0到239.255.255.255,其中任意一个地址都可以作为多播地址。

Friday, December 19, 2008

SQLServer2005 基于行版本的隔离

Part 1:行版本控制隔离

行版本控制的隔离是SQL Server 2005一个新的隔离框架。使用行版本控制的隔离可以在大量并发的情况下,显著减少所得产生,并且与NoLock相比,它又可以显著降低肮脏读,幻影,丢失更新等现象的发生(READ_COMMITTED_SNAPSHOT)

当在基于行版本控制的隔离下运行的事务读取数据时,读取操作不会获取正被读取的数据上的共享锁(S 锁),因此不会阻塞正在修改数据的事务。另外,锁定资源的开销随着所获取的锁的数量的减少降至最低。使用行版本控制的已提交读隔离和快照隔离可以提供副本数据的语句级或事务级读取一致性。


行版本控制隔离:优势

使用行版本控制的隔离级别具有以下优点:
·读取操作检索一致的数据库快照。
·SELECT 语句在读取操作过程中不锁定数据(读取器不阻塞编写器,编写器也不阻塞读取器)。
·SELECT 语句可以在其他事务更新行时访问最后提交的行值,而不阻塞应用程序。
·死锁的数量减少。
·事务所需的锁的数量减少,这减少了管理锁所需的系统开销。
·锁升级的次数减少。

行版本控制隔离:原理

SQL Server 2005的行版本控制原理上很简单,就是在库表中每一行的记录上都悄悄的增加了一个类时间戳列(行版本列)。
当使用行版本控制的隔离时,SQL Server 2005 Database Engine 向使用行版本控制操作数据的每个事务分配一个事务序列号 (XSN)。事务在执行 BEGIN TRANSACTION 语句时启动。但是,事务序列号在执行 BEGIN TRANSACTION 语句后的第一次读/写操作时开始增加。事务序列号在每次分配时都增加 1。
当事务执行时,SQL Server根据行版本列,来提供的行的相应版本。

而SQLServer将维护所有在数据库中执行的数据修改的逻辑副本(版本)。特定的事务每次修改行时,数据库引擎 实例都存储以前提交的 tempdb 中行的图像版本。每个版本都标记有进行此更改的事务的事务序列号。已修改行的版本使用链接列表链接在一起。最新的行值始终存储在当前的数据库中并链接至版本存储区 tempdb 中存储的版本。(修改大型对象 (LOB) 时,只有已更改的片段才会复制到 tempdb 中的版本存储区, 对于短期运行的事务,已修改行的版本将可能保存在缓冲池中,而不会写入 tempdb 数据库的磁盘文件中。如果只是临时需要副本行,它将只是简单地从缓冲池中删除而不会引发 I/O 开销。)

MSDN上一再强调要关注提升tempdb的地位,不然因为tempdb的性能导致整个数据库性能下降,可就是无妄之灾了。

行版本控制隔离:种类

行版本控制分为两种已提交读快照隔离级别(READ_COMMITTED_SNAPSHOT)和快照隔离级别(ALLOW_SNAPSHOT_ISOLATION),他们属于行版本控制的隔离的范畴,但是实际上他们却有方方面面的不同。
下面是MSDN对他们的比较。


读取数据时的行为
SNAPSHOT隔离就像真实的快照,它会无视涉及行的变化。
在SNAPSHOT隔离下运行的事务将读取数据,然后由另一事务修改此数据。SNAPSHOT事务不阻塞由其他事务执行的更新操作,它忽略数据的修改继续从版本化的行读取数据。但是,当快照事务尝试修改已由其他事务修改的数据时,SNAPSHOT事务将生成错误并终止.

READ_COMMITTED_SNAPSHOT类似与以前的READ_COMMITTED隔离级别,
也就是说与快照事务相同的是,即使其他事务修改了数据,已提交读事务也将读取版本化的行。然而,与快照事务不同的是,已提交读将执行下列操作:

·在其他事务提交数据更改后,读取修改的数据。
·能够更新由其他事务修改的数据,而快照事务不能。

修改数据时的行为
在使用行版本控制的已提交读事务中,使用阻塞性扫描(其中读取数据值时将在数据行上采用更新锁(U 锁)完成选择要更新的行。这与不使用行版本控制的已提交读事务相同。如果数据行不符合更新标准,在该行上将释放更新锁并且将锁定下一行并对其进行扫描。
在快照隔离下运行的事务对数据修改采用乐观方法:直到数据被修改时才获取数据上的锁。不需要获取锁就可以选择要更新的行。当数据行符合更新标准时,快照事务将验证未被并发事务(在快照事务开始后提交)修改的数据行。如果数据行已在快照事务以外修改,则将出现更新冲突,同时快照事务也将终止。更新冲突由数据库引擎 处理,无法禁用更新冲突检测。

Reference: http://edu.itbulo.com/200612/110943.htm
Part 2: sqlserver的锁
这里我们说的都是没有激活快照隔离级别的情况下,得到的结果。
实际上在sqlserver2005支持行版本隔离以前,我是无法理解sqlserver对read committed隔离级别的支持的,我的理解就是一个事务如果正在修改某行数据(暂时不论是聚集表还是heap)的时候,另外一个事务如果要读取该行记录,应该是可以读取的,只不过读到的是已经持久到数据库中的数据(我在mysql上测试就得到了这样的结论)。
但是实际情况不是这样的,实际情况是另外那个读取的事务会被阻塞,直到更新的事务完成。为 资源上什么会这样呢? 因为第一个更新的事务会在该行记录(如果是聚集表,那么就是在key上,后面会详细说明聚集表和heap)上施加一个X锁,第二个事务读的时候会需要获得一个S锁,因为S锁和X锁是不兼容的,所以第二个事务必须等到第一个事务结束,所以会被阻塞。
在sqlserver中,一个事务在执行到某个sql语句的时候会要求获得相应的锁,然后会在事务结束(提交或者回滚)之后才释放这些锁。但是有个例外,如果事务的隔离级别是read committed或者read uncommitted,那么事务获得S锁会在读取了数据之后马上就释放。这里假设两个事务,它们都是先读取一条记录,然后更新这条记录, 在read committed下,这两个事务不会死锁,但是如果是repeatable read的话,就可能死锁。因为,在repeatable read的隔离级别下,事务读取记录后不会释放S锁,而是在事务结束后才会释放S锁,这样的话,假如第一个事务先读取了这条记录,然后第二个事务也读取了这条记录, 这个时候第一个事务要更新,更新的话需要获得X锁,而X锁和S锁是不兼容的,就表示第一个事务要等待第二个事务释放S锁,而同时第二个事务也要更新这条记录,它又等待第一个事务释放S锁,这样两个事务就死锁了。
为了解决这种死锁问题,sqlserver引入了更新锁U锁,U锁与S锁是兼容的,但是与U锁和X锁是不兼容的。U锁是用来处理读取数据进行更新这种情况,即先读取出数据,然后进行更新。还是说先前那个例子,两个事务都是先读取,但是在读取的时候必须显示说明申请UPDlock(select * from TABLE with updlock where ...),这样第一条记录在读取记录后就获得了该记录的U锁,第二个事务如果也要获得U锁,那么会被阻塞,但是能够获得S锁,然后第一个事务获得U锁后,就可以执行更新,这就避免了死锁。

sqlserver会采取如下两种方式之一来组织数据(http://msdn.microsoft.com/zh-cn/library/ms180978(SQL.90).aspx):
  • 聚集表是有聚集索引的表。
    数据行基于聚集索引键按顺序存储。聚集索引按 B 树索引结构实现,B 树索引结构支持基于聚集索引键值对行进行快速检索。索引中每个级别的页(包括叶级别的数据页)链接在一个双向链接的列表中。但是,通过使用键值来执行从一个级别到另一级别的导航。有关详细信息,请参阅聚集索引结构
  • 堆(heap)是没有聚集索引的表。
    数据行不按任何特殊的顺序存储,数据页也没有任何特殊的顺序。数据页不在链接列表内链接。有关详细信息,请参阅堆结构
索引分为聚集(cluster)索引和非聚集索引,它们具有相同的B树结构,但是有以下两点显著差别:
  • 非聚集索引的基础表的数据行不按非聚集键的顺序排序和存储。
  • 非聚集索引的叶层是由索引页而不是由数据页组成。
聚集索引结构
sqlserver中,索引页是按B树结构组织的,B树的每一个节点就是一个索引页。顶端节点称为根节点,根节点和叶节点之间的任何索引级别称为中间 级。
在聚集索引中,叶节点包含基础表的数据页。根节点和中间级别节点包含存有索引行的索引页。每个索引行包含一个键值和一个指针,该指针指向B树上某一中 间级叶或叶级索引上的某个数据行。每级索引的页均被链接在双向的链接列表中。


非聚集索引结构
聚集索引结构中,B树已经包含数据页(基础表包含在B树中?),而非索引结构中,索引与数据是分开的,非聚集索引中每个索引行都包含非聚集索引键值和行定位符。行定位符指向聚集索引或者堆中包含该键值的数据行。


什么是B树?B树就是常说的二叉搜索树,有以下三个特点:
  1. 所有非叶子节点至多拥有两个子节点
  2. 每个节点存储一个关键字
  3. 非叶子节点的左指针指向小于其关键字的子树,右指针指向大于其关键字的子树。

sqlserver在细粒度的资源上获得锁之后,一般会在更大粒度的资源上施加意向锁。比如如果在RID上获得了一个X锁,那么在该RID所属的KEy,和页上会施加IX锁,这样其他的事务访问这个页的时候,就知道该页下面有一个组件(key,rid)已经被其他事务施加了X锁。

什么是页(page)?
SQL Server 中数据存储的基本单位是页(Page)。数据库中的数据文件(.mdf 或 .ndf)分配的磁盘空间可以从逻辑上划分成页(从 0 到 n 连续编号)。磁盘 I/O 操作在页级执行。也就是说,SQL Server 每次读取或写入数据的最少数据单位是数据页。

  注意:日志文件不是用这种方式存储的,而是一系列日志记录。

数 据库被分成逻辑页面(每个页面8KB),并且在每个文件中,所有页面都被连续地从0到x编号,其中x是由文件的大小决定的。我们可以通过指定一个数据库 ID、一个文件ID、一个页码来引用任何一个数据页。当我们使用ALTER DATABASE命令来扩大一个文件时,新的空间会被加到文件的末尾。也就是说,我们所扩大文件的新空间第一个数据页的页码是x+1。当我们使用DBCC SHRINKDATABASE或DBCC SHRINKFILE命令来收缩一个数据库时,将会从数据库中页码最高的页面(文件末尾)开始移除页面,并向页码较低的页面移动。这保证了一个文件中的页码总是连续的。

每页的开头是 96 字节的标头,用于存储有关页的系统信息。此信息包括页码、页类型、页的可用空间以及拥有该页的对象的分配单元 ID。

  在数据页上,数据行紧接着标头按顺序放置。页的末尾是行偏移表,对于页中的每一行,每个行偏移表都包含一个条目。每个条目记录对应行的第一个字节与页首的距离。行偏移表中的条目的顺序与页中行的顺序相反。



Part 3: 实验
-- 激活sqlserver2005的快照隔离级别
--ALTER DATABASE NorthWind SET READ_COMMITTED_SNAPSHOT ON;
--ALTER DATABASE NorthWind SET ALLOW_SNAPSHOT_ISOLATION ON;

事务1:
set transaction isolation level read committed
--set transaction isolation level REPEATABLE READ
--set transaction isolation level SNAPSHOT

BEGIN TRANSACTION
select * from account where id=1;
--insert into account(id,name,age) values(6, 'liliming-6',30);
update account set age=age+1 where id=1;
WAITFOR DELAY '00:00:10'

-- 显示该事务获得的锁,只能在sqlserver2005或以后版本,并且创建的数据库兼容级别是sql90
SELECT L.request_session_id AS SPID,
DB_NAME(L.resource_database_id) AS DatabaseName,
O.Name AS LockedObjectName,
P.object_id AS LockedObjectId,
L.resource_type AS LockedResource,
L.request_mode AS LockType,
ST.text AS SqlStatementText,
ES.login_name AS LoginName,
ES.host_name AS HostName,
TST.is_user_transaction as IsUserTransaction,
AT.name as TransactionName,
CN.auth_scheme as AuthenticationMethod
FROM sys.dm_tran_locks L
JOIN sys.partitions P ON P.hobt_id = L.resource_associated_entity_id
JOIN sys.objects O ON O.object_id = P.object_id
JOIN sys.dm_exec_sessions ES ON ES.session_id = L.request_session_id
JOIN sys.dm_tran_session_transactions TST ON ES.session_id = TST.session_id
JOIN sys.dm_tran_active_transactions AT ON TST.transaction_id = AT.transaction_id
JOIN sys.dm_exec_connections CN ON CN.session_id = ES.session_id
CROSS APPLY sys.dm_exec_sql_text(CN.most_recent_sql_handle) AS ST
WHERE resource_database_id = db_id()
ORDER BY L.request_session_id

--WAITFOR DELAY '00:00:10'
SELECT * FROM account;
COMMIT


事务2:
set transaction isolation level read committed
--set transaction isolation level REPEATABLE READ
--set transaction isolation level SNAPSHOT

BEGIN TRANSACTION
select * from account;
--insert into account(id,name,age) values(6, 'liliming-6',30);
--update account set age=age+1 where id=1;
--WAITFOR DELAY '00:00:5'

-- 显示该事务获得的锁,只能在sqlserver2005或以后版本,并且创建的数据库兼容级别是sql90
SELECT L.request_session_id AS SPID,
DB_NAME(L.resource_database_id) AS DatabaseName,
O.Name AS LockedObjectName,
P.object_id AS LockedObjectId,
L.resource_type AS LockedResource,
L.request_mode AS LockType,
ST.text AS SqlStatementText,
ES.login_name AS LoginName,
ES.host_name AS HostName,
TST.is_user_transaction as IsUserTransaction,
AT.name as TransactionName,
CN.auth_scheme as AuthenticationMethod
FROM sys.dm_tran_locks L
JOIN sys.partitions P ON P.hobt_id = L.resource_associated_entity_id
JOIN sys.objects O ON O.object_id = P.object_id
JOIN sys.dm_exec_sessions ES ON ES.session_id = L.request_session_id
JOIN sys.dm_tran_session_transactions TST ON ES.session_id = TST.session_id
JOIN sys.dm_tran_active_transactions AT ON TST.transaction_id = AT.transaction_id
JOIN sys.dm_exec_connections CN ON CN.session_id = ES.session_id
CROSS APPLY sys.dm_exec_sql_text(CN.most_recent_sql_handle) AS ST
WHERE resource_database_id = db_id()
ORDER BY L.request_session_id

COMMIT


如果是聚集表,那么锁的最小粒度是key,因为数据页就是保存在索引结构中,如果是heap,那么锁的最小粒度为rid,即数据行,因为索引结构和数据页是分开的。

执行顺序:
先启动事务1,等待几秒钟,保证事务1已经执行了更新并且开始等待,这个时候执行事务2

--未激活快照隔离级别
结果1:数据库没有激活快照隔离级别
事务2会被阻塞,直到事务1结束。 因为事务1在更新id=1的记录,而事务2查询的结果集包含id=1的记录,就是说事务2需要获得所有满足条件的记录的S锁,但是事务1在id=1的记录上(聚集表则是key,heap为rid)已经获得了X锁,X锁和S锁不兼容,所以事务2会等待直到事务1结束。

结果2:数据库没有激活快照隔离级别
变体:事务1的update更改为insert
事务2会被阻塞,直到事务1结束。 因为事务1在insert后获得该记录的X锁,虽然还没有提交,但是事务2仍然会尝试获得该记录的S锁,所以会被阻塞(行记录只有一个版本,一切都由锁来控制)。

结果3:数据库没有激活快照隔离级别
变体:事务1和事务2都执行insert,并且插入的行id是一样的
事务2会被阻塞sqlserver应该会判断动作类型,由于是insert,所以第二个事务会等待第一个事务提交,然后才会跑主键冲突。如果并不阻塞,而直接就抛出这个异常,那么很可能,稍后事务1会回滚,这样的话抛出冲突就毫无道理了。

--激活快照隔离级别
结果4:
事务2不会被阻塞,会获得先前已经提交的数据。

结果5:
变体:事务1和事务2都执行insert,并且插入的行id是一样的
事务2会被阻塞,最后抛出主键冲突。

参考:
  • http://ramonli.blogspot.com/2007/01/understanding-transaction-isolation.html

Monday, September 22, 2008

[REST] http request method

》Http request method的特性
  • safe: 不改变服务器端状态,get和head是safe的操作
  • indepment:幂等性,就是多个请求和单个请求执行的效果是一样的,get,head,put,delete都是indepment,而post,options,trace是非indepment的。
》OPTIONS
获得request/response链上的通信选项,response不能缓存。
》GET
从服务器端取回资源。
》HEAD
与GET一样,但是response不能包含message-body。一般用来检验资源有效性,是否可访问,是否被修改,这样客户端可以采取进一步的动作,比如直接采用本地cache返回到用户。
》POST
The POST method is used to request that the origin server accept the entity enclosed in the request as a new
subordinate of the resource identified by the Request-URI in the Request-Line.
如果entity对应的资源已经被创建,那么返回201(created),message-body需要说明请求状态,对新资源的引用(服务器上已经创建的资源为新资源,不是指请求中携带的entity),以及指定Location header。
如果entity对应的资源不存在,那么创建这个资源,并且返回200(OK,response中携带message-body)或者204(NO CONTENT,response中没有message-body)。
response不应该被缓存。
》PUT
The PUT method requests that the enclosed entity be stored under the supplied Request-URI. 如果REquest-uri指向已经存在的资源,那么使用entity更新这个资源,否则创建资源。
如果entity对应的资源不存在,那么创建这个资源,并且返回200(OK,response中携带message-body)或者204(NO CONTENT,response中没有message-body)。
response不应该被缓存。
》DELETE
The DELETE method requests that the origin server delete the resource identified by the Request-URI.
request不应该包含message-body(http协议中并没有这样规定,是我使用HttpURLConnection时,设置request method为delete,setDoOutput(true)的时候包的错)。
response不应该被缓存。
》PUT vs POST
put与post最大的不同在于,post请求中的request-uri表示用来处理附带在request的entity的资源,比如一个data-accepting process,而put的request-uri就是表示附带在请求中的entity。
servlet API对他们的支持也不同,servlet APi对post提交的form-data-format(field1=value1&field2=value&...&fieldn=valuen)提供了内置的支持,使用HttpServletRequest.getParameter(fieldn)就可以得到值,而如果采用Put的请求方法,HttpServletRequest.getParameter(fieldn)只能得到null(只能使用HttpServletRequest.getReader()来获得message-body)。
* 如果是url中携带参数,那么HttpServletRequest.getParameter(fieldn)总能取得值,和request method没有关系
》TRACE
The TRACE method is used to invoke a remote, application-layer loop-back of the request message. 不太清楚。。。。
》HTTP
http协议是字符协议(http协议头必须是字符),但是http协议的消息体可以是文本字符也可以是二进制数据(当然也可以对二进制数据进行编码,比如Base64)。对于java来说,已经实现了对http协议的封装,客户端可以使用HttpURLConnection的getInputStream()和getOutputStream()来写出和读取二进制流,而在服务器端使用servlet API,request.getInputStream(), response.getOutputStream()也可以来读取和写出二进制流。
考虑背后的http协议解析过程,无论字符还是二进制最终都会以二进制方式在网路上传输,接受方会根据找到的第一个空行来区分http消息头和消息体,消息头总是字符,而消息体就取决于开发人员了....

NOTE: 详细信息可以参考rfc2616-Hypertext Transfer Protocol

》HttpUrlConnection
这个Api的确不好用,javadoc也没说清楚,很多事情是写代码实验才有结论。
  • 只有调用HttpUrlConnecton.getInputStream()或者其他从连接取回响应信息的方法,比如HttpURLConnection.getResponseCode()之类的方法之后,HttpURLConnection才会向服务器端提交数据,否则根本不会发送消息体数据。
  • DELETE请求方法不能有消息体
  • 如果向远程提交消息体数据,那么必须setDoOutput(true)。

Thursday, September 11, 2008

mysql实用技巧...

显示可用数据库 show databases;
显示表索引 show keys from TABLE_NAME;
显示表信息 show table status like 'TABLE_NAME';
授权 grant all priviledges on DB_NAME.* to 'USER_NAME'@'%' identified by 'PASSWORD' with grant option.
创建数据库 create database DB_NAME charact set utf8
创建表 carate table (...) type=INNODB
查看当前连接数 show processlist
查看服务器状态 show status

Ubuntu/Window shell实用技巧

LINUX ------------------------------------
找包含特定文本的文件:
grep -e *.log -R xplanner/ (在xplanner目录下查找所有包含.log字串的文件, -e表示使用pattern查询)
grep -F xplanner.log -R xplanner/ --line-number (在xplanner下查找包含固定字串xplanner.log的文件,并且显示行号)

找文件
find /home --name cruisecontrol.sh (在/home目录包含子目录下查找cruisecontrol.sh)

查看dpkg将安装包安装到什么地方了
dpkg -L mysql-server (列出mysql-server包被安装到系统什么地方了)
dpkg -l 列出所有安装的包

>>为脚本传入参数
$1表示传入的第一个参数,$2表示第二个...$@表示传入的所有参数,$0表示脚本自己的名称。
>>Vi的十六进制编辑模式
   首先以二进制方式编辑这个文件:

vim -b datafile

现在用 xxd 把这个文件转换成十六进制:

:%!xxd

最后, 用下面的命令把它转换回来:

:%!xxd -

WINDOW ------------------------------------

Friday, August 29, 2008

如何部署高可用性和高伸缩性的网站?

如何部署高可用性和高可伸缩性的网站?这是一个很复杂的问题,到目前为止,我还没有实际的经验,都是自己学习和思考得到的一点结果。
前段时间,长沙一个公司做了一个彩票投注的网站,上海那边的公司就想直接拿过去用,不想再重新开发一套系统。问题是开发人员多数都有一个毛病,认为别人做的不如自己做的,上海就有这样一个,他说给我一个月时间,我就可以做出比这个更好的网站。除去有关业务有关市场的东西不谈,这位兄弟说的话的确很难让人相信,业务人员不信,我觉得他更是没弄明白部署一个生产的网站所包含的工作,显然,他认为,部署一个网站就是写完代码,然后copy到服务器上就可以了。 事情比他想象的复杂的多,如果这个网站是一个intranet的应用,可能还好说些,但是这是一个面向彩民的网站,实现一年销售2亿的网站,在部署这样一个网站的时候,可用性和伸缩性是不能不考虑的问题。在最终得到的生产环境中,网站代码可能只是很小的一部分, 你需要集群,需要cache,需要分部文件系统dfs,需要负载均衡,需要数据库集群,以及配套的一对硬件,要把这些理清楚,如果只懂java,jsp是远远不够的。
我没有实际参与部署过这种为海量用户服务的网站,心里很忐忑,一旦我们运营的网站出现这样那样 的问题怎么办? 所幸找到了一个很好的案例,这个案例不仅仅是从软件设计上说如何支持可伸缩性,而是完整的介绍了整个网站的部署,它就是livejournal,一个很早开始blog服务的网站,这是一个有8百多万用户,每秒处理2000次点击的网站。
开发livejournal的这群人值得称赞,他们分享了网站开发和部署过程中的很多心得,并且开源了他们自己开发的很多支持网站运营的工具,其中最著名的大概就应该是memcache了,这个cache系统已经在很多大型网站得到了应用,可以从这里下载他们的一个演示文档。下面按照整个部署涉及到的环节说说我的体会。
1.perlbal
一个livejournal自己开发的reverse proxy,用来进行负载均衡(big-ip好象也是用来做负载均衡,但是没搞清楚和perlbal的关系)
2. mogilefs
一个livejournal自己开发的分布式文件系统(DFS, distributed file system),分布式文件系统又包括一些不同的类型,主要是看他们解决的问题,有的用来支持高并发任务,有的使用来支持高可用性,而mofilefs是同时支持高并发和高可用性两个特性,而且支持任何本地文件系统。 问题是为什么我们要用分布式文件系统??它应该是用来处理系统产生的数据文件,最直观的大概就是系统生成的日志了,在集群环境下,如果没有dfs,那么每台主机都会产生单独的分离的日志文件,如果采用dfs,那么所有集群中的应用可以向同一个文件输出日志,而且便于维护。(老实说,这个我没有弄太清楚)
dfs与raid的关系也需要提一下,一般来说raid只是硬盘的阵列,如果部署raid的主机崩溃,那么raid也没用了,除非是有san(store area networ)支持的raid,可以跨主机。 而dfs由于本身是分布式的,是跨主机的,不存在这个问题。
3. web server cluster
这个集群是基本的,相信了解集群的人都应该知道web server的集群是最基本的
4. memcache
一个livejournal开发的分布式缓存系统,目前的应用非常广泛,可以在多台主机启动多个memcache服务,它们会自动协作,对应用程序来说就好象只有一个memcache服务。可以存储任何形式的内容(需要做深入研究)
但是对什么内容进行缓存呢?
首先要是被经常读取的内容,而这些内容可以通过查看数据库的日志,比如那些对象查询的最多等等。mysql提供了slow log的功能。
5. mysql cluster
数据库集群这块是对我影响最大的。以前一直认为是要采用大型数据库的本身支持的集群(想来应该非常昂贵),不然是实在想不出还有什么集群方式,而且既然有数据库集群,那么前面是不是也要有一个load balancer? 不过这个load balancer应该是数据库服务器本身提供,就相当于有一个逻辑数据库,应用程序面向的是这个逻辑数据库,不需要知道后端的集群有多少数据库实例,这个逻辑数据库负责组装数据,分布请求,负载均衡。。。不过,这的确应该是一个昂贵的解决方案。
后来,了解了sharding,是一种数据库分区的概念,包含水平和垂直两种shard方式(集群也包括水平和垂直两种,垂直的就是单个应用,但是起先是运行在普通的web server中,后来是运行在小型机中,而水平集群就是采用便宜的web server阵列,每个主机都部署一个应用),水平就是将一个表的列进行拆分,可能数据量大的,或者不经常使用的列拆分到一个独立的表; 垂直就是按照一个规则将表中的数据进行分区,避免一个表中的数据过大,常见的是用户表(user),比如规则是按照id的奇偶来决定将user存入哪个表,那么可能在数据库中就存在usre_odd, user_even两个表,它们的表模式一模一样,但是一个用来保存id为奇数的用户,一个用来保存id为偶数的用户。shard需要进行额外的编码,但是会带来巨大的性能提升。
一般来说采用sharding就可以不用昂贵的数据库集群方案,而且后来想到的一个问题是,数据库服务器一般会采用raid,raid本身就提供了高速的io,以及数据冗余,所以我更加倾向与raid+sharding的方案了。
当时livejournal的方案显然更复杂,除了sharding之外,mysql也做了集群,并且每个用户组都有一个数据库集群,想来只有存在海量用户的系统需要这样,实在很壮观阿。 但是从它讨论的master-salve这种数据库集群方式中,我又想通了不少问题,所以在我原来raid+sharding的方案上,我又增加一个slave,也做一个数据库集群,但是slave不一定需要raid,看具体的应用,我是想在进行统计报表的时候就可以使用slave的数据,而不需要访问master,master处理在线数据,提供实时服务,嘿嘿。。。。如果slave也需要处理在线作业,那么也可以在slave上配置raid, 当然也可以象livejournal一样,master提供读写,而slave只提供读。
数据库这库学问太深,还有很多其他的方面,我也只想了这么多,想到哪里写到哪里。。。

参考资料:
Django Master Class: http://toys.jacobian.org/presentations/2007/oscon/tutorial/
Database sharding unraveled: http://lifescaler.com/2008/04/database-sharding-unraveled-part-i/
Scalability Best Practices: Lessons from eBay: http://www.infoq.com/articles/ebay-scalability-best-practices
Scaling Your Java EE Applications: http://www.theserverside.com/tt/articles/article.tss?l=ScalingYourJavaEEApplications

Wednesday, July 09, 2008

什么是好的构建工具?

从我开始接触到linux的时候,我就知道一个叫做makefile的东西,不过从来不敢改,不敢动,因为对linux不熟悉,对c语言不熟悉。后来据说因为makefile对平台依赖性太强,又除了一个autoconfig,那些都是linux+c的东西。
再到后来就是ant了,很多项目中也都采用了ant,非常灵活,可以说只有想不到没有做不到。但是也的确存在一个问题,在大型项目中,维护ant的构建脚本本身也会变成一个沉重的负担,需要有专门的人来负责。我曾经在一个erp公司做过,其中包含很多子项目,ant脚本到处都是,而且彼此引用,光是看那些脚本,就看到的我目瞪口呆:ant这么强大,ant这么复杂。
没有在具体的项目中应用过maven,只是想要看看为什么有了ant,人们还需要maven。 ant和maven在某种意义上是两个完全相反的东西,你可以随心所以的使用ant,编写各种插件,脚本,让它来适应你的习惯,你的哲学,但是maven,如果你想要舒服的使用maven,最好就是适应它的哲学,适应它的习惯,不然,可能不是那么好驾驭。 maven一个很重要的哲学就是‘惯例比配置更重要’,我很赞同,使用惯例我们可以减少大量的配置文件,也就减少大量的脚本,减少维护的工作。 此外,maven标准化了项目的构建工作,的确,几乎所有的项目都包含这样一个构建流程:编译,测试,打包,部署。 在ant中,每个项目都需要从头开始(其实不一定,我在使用ant的时候就借鉴了interface的思想,定义了一些标准的target,保证所有的项目这些都有同样的ant target,完成同样的任务,虽然实现可以不一样。。。。毕竟,这不是工具本身支持的,需要人们手工来做,需要相关人员的理解,共识)。
对maven还不是太熟悉,但是觉得不如ant那么容易上手,pom的结构太复杂,也有可能导致一大堆xml配置文件。 maven有一个完美的思想和比较蹩脚的实现,maven1出来的时候大受好评,但是等到maven2,人们开始越来越多的抱怨它。 mavne的依赖管理是先对于ant最出色的特性(现在ant可以和ivy接合来实现包的依赖管理),但是现在看来人们正是对这块不满意,不是说包依赖管理不好,而是这个功能实现的不好,经常会导致构建的失败。人们可能莫名其妙的发现,上一次好好好的成功的构建,在没有人们变动的情况下,怎么现在就构建失败了,其实很可能是包仓库中某个依赖变化了,而修正这个问题据说是一场噩梦。
maven是比ant更高级的构建工具,虽然有那么多问题,但是目前来看,maven还是首选,至少最流行的spring项目就是采用maven构建的。
此外,有一个ruby开发的java项目构建工具,名字叫buildr,也成为了apache的incubutor项目,但是对于很多java开发人员来讲,部署一个ruby应用可能不是那么简单,其他远不如copy一份ant或者maven那么简单。

使用diff和patch来制作补丁

首先说明使用diff和patch一般使用来处理文本文件的,对于开源项目,这两个工具是很好的帮手,比如linux的内核发布就包含很多patch,这样你不用每次都下载完整的源代码包,只需要下载目标版本和你已有版本之间的patch。
我会通过一个实际的操作示例来说明这两个工具的使用。有一个项目,名字叫testproject,已经发布了两个版本,2.2.0和2.2.1,其中上海生产环境部署了2.2.0,深圳开发中心已经发布了新版的2.2.1,现在生产环境需要升级到2.2.1,因为新版本修复了重要的bug。 方法有很多种,这里我们说明使用diff和patch的方式。
* 首先在当前工作目录下(假设为/home/jack/),从subversion上export两个版本。
> svn export http://192.168.0.253/svn/TcCenter/TcCenter/tag/2.2.0/sourcecode 2.2.0
>svn export http://192.168.0.253/svn/TcCenter/TcCenter/tag/2.2.1/sourcecode 2.2.1
* 执行diff
>diff -Nur 2.2.0 2.2.1 >220-221.patch
上面的命令会生成一个patch文件,其中-N表示如果目标目录不包含源目录中的文件,那么认为目标目录存在一个内容为空的同名文件。 -u表示生成统一的有上下文的patch文件(后面再说patch格式)。 -r表示递归处理,在处理目录的时候需要这个参数。
* 接下来,我们开始使用patch
>cp 220_221.patch 2.2.0
>cd 2.2.0
>patch -p1<220_221.patch>rm 220_221.patch
>cd ..
>diff -Nur 2.2.0 2.2.1 >220-221_new.patch
可以看到这次得到的220-221_new.patch是空文件,这就说明现在2.2.0的源代码已经被升级到2.2.1了。
* 好了,现在我们可以将这个patch文件从深圳发到上海,然后上海更新源代码,再次编译,测试,打包,部署....

===========================
subverion的diff
===========================
接下来我要试试patch能不能处理subversion的diff得到的patch文件。
* 生成subversion的diff文件,在当前工作目录下(假设为/home/jack/).
> svn diff http://192.168.0.253/svn/TcCenter/TcCenter/tag/2.2.0/sourcecode http://192.168.0.253/svn/TcCenter/TcCenter/tag/2.2.1/sourcecode > diff.patch
>cp diff.patch 2.2.0
>patch -p0rm diff.patch
>cd ..
>>diff -Nur 2.2.0 2.2.1 >220-221_new.patch
可以看到,220-221_new.patch文件为空,说明2.2.0已经升级到2.2.1了。
这里使用diff命令和使用‘svn diff’得到的patch文件格式有点不一样,使用diff得到的为:
diff -Nur 2.2.0/build.properties 2.2.1/build.properties
--- 2.2.0/build.properties 2008-04-30 15:09:14.000000000 +0800
+++ 2.2.1/build.properties 2008-06-27 15:39:36.000000000 +0800
@@ -8,7 +8,7 @@
#--------------------------------------------#
project.name=CentralPool
#DO NOT modify below setting.
-app.version=2.2.0
+app.version=2.2.1
app.vendor=Shenzhen Helper Science & Technology Co., Ltd.
builder.name=${project.name} Team

diff -Nur 2.2.0/CHANGELOG.txt 2.2.1/CHANGELOG.txt
--- 2.2.0/CHANGELOG.txt 2008-04-08 17:02:58.000000000 +0800
+++ 2.2.1/CHANGELOG.txt 2008-06-27 15:39:36.000000000 +0800
@@ -1,16 +1,16 @@
-CENTRALPOOL PROJECT CHANGELOG
+CENTRALPOOL PROJECT CHANGELOG
--------------------------------------
ChangeLog定义了当前版本相对于上一个版本之间系统发生的变更. 这些变更包括修正的bug, 系统新增的
功能,或者系统添加的补丁等等.

而使用‘svn diff’得到的patch文件为:
Index: sub-core/src/com/hengpeng/ante/game/GameFactory.java
===================================================================
--- sub-core/src/com/hengpeng/ante/game/GameFactory.java (.../2.2.0/sourcecode) (revision 241)
+++ sub-core/src/com/hengpeng/ante/game/GameFactory.java (.../2.2.1/sourcecode) (revision 241)
@@ -51,6 +51,9 @@

case Game.GAME_TYPE_SSC:
return new SSCGame(gameData);
+
+ case Game.GAME_TYPE_NUMLOTTO:
+ return new NumLottoGame(gameData);

default:
throw new HPException(AnteException.ErroCode_NoGame,
Index: sub-core/src/com/hengpeng/ante/game/Game.java
===================================================================
--- sub-core/src/com/hengpeng/ante/game/Game.java (.../2.2.0/sourcecode) (revision 241)
+++ sub-core/src/com/hengpeng/ante/game/Game.java (.../2.2.1/sourcecode) (revision 241)
@@ -75,7 +75,13 @@
*/
public static final int GAME_TYPE_SSC = 9;

+ /**
+ * 东方6+1玩法 {@value}
+ * @value 9
+ */
+ public static final int GAME_TYPE_NUMLOTTO = 10;

+
// GAME DEFINITION
public static final int GAME336 = 11;
public static final int GAME155 = 23;
幸运的是patch命令都可以处理,实际上两种都是正确的格式。

diff和patch在linux操作系统中是自带的,但是进入windows环境就没有那么容易了。不过好在有一个gunwin32的开源项目(gunwin32.sourceforge.net),gnu下的diff和patch都可以运行在window上了。

Thursday, June 19, 2008

RedHat Ubuntu python Mysql MySQLdb mantis+subversion

这几天搞subversion+mantis整合的事情,折腾的一天,目前貌似前进了一步,至少可能遇到的问题应该都被解决了。 将版本控制系统和缺陷管理系统整合起来是一件很值得的事情,通过整合,可以将需求和最终的实现形成一条可追踪的线, 而不是一些离散的软件开发活动。 我们可以分析用例,进行概要设计,并且将软件实现分解成较少工作量的任务,这些东西可以通过在缺陷管理系统中file一个总体的bug,说明要实现的是什么东西,然后每个分解的任务file一个bug,这些bug依赖于那个描述总体的bug。这种方式扩展了缺陷管理系统的概念,应该把它叫做制品管理系统,因为在这个系统中不再只有bug这一个概念,还包括了feature,enhancement,task等。 :(,觉得表达的有些混乱。
下面说说整个整合的过程。
一开始从网上找了文档,有专门说subversion和mantis怎么整合的,看了看,觉得大概没问题,因为我以前也在linux下配过不少东西,mysql, bugzilla,subversion,apache等,所以想事情应该不会太困难。
事实是实际的开发环境和文档中讲的不一样,几乎所有的文档中说的都是mantis和subversion都运行在linux上,这样subversion可以通过ssh来无交互地直接执行mantis机器上的命令。问题是,我们的mantis是跑在window上的。 妈妈的,我尝试在window上装了个windosshd,然后从linux下可以通过密码的方式访问mantis的机器,但是rsa(不需要输入密码)就怎么也不行,openssh本身不太懂。实在搞不定,后来想就直接在subversion的机器上用python来访问mantis的mysql数据库得了,这也行的通,何况我最近一直学习python,正好小试牛刀。

python访问mysql需要MySQLdb的库,我需要在subversion那台机器上安装这个库。 因为我一直用ubuntu,所以直接‘sudo apt-get install mysql-server', 居然提示我没有这个命令,有点昏。后来一查,subversion的机器是redhat enterprise linux4.4的,只好去MySQLdb的网站下载targz的包。 这个包是需要编译的,但是编译过程中总是提示找不到mysql头文件(mysql_config.h),这样看来我需要安装mysql了。
跑到mysql的网站上下载了redhat enterprise linux4的mysql4.1.22的rpm包,结果安装这个包的过程中总是提示文件冲突,烦死了,而且rpm包里面并没有要求的mysql_config.h文件。在后来我就下载了一个mysql的linux下的通用包,48M,这次好了,这个包里面是完整的mysql-server的目录,哈哈,看来要爽一回。
进入MySQLdb的安装目录,需要修改site.cfg文件,设定mysql_config到下载的这个mysql-server目录,即$mysql_server_home/bin, 这次执行python setup.py build就成功了。
mysql_config 是mysql的一个工具,可以查看mysql的很多编译安装信息,所以很多需要mysql头文件来编译的项目都会使用这个工具。在ubuntu下,这个工具要单独安装。
在控制台执行命令:
$python
>>> import MySQLdb
没有报错,安装成功,接下来就是写python代码了
--------------------------------
现在又碰到了中文编码的问题,由于svn提交的时候指定中文消息,这些消息被mantis显示出来的时候全是乱码。后来总算找到办法了,原来需要进行'中文'.decode('utf-8').encode('gbk'),但是subversion那台机器上的python是2.3的,没有中文解码包,需要安装python-cjkcodecs。 python2.4就自带这个包了。妈的!

Thursday, May 22, 2008

MoinMoin......

这些天在做一个Research,就是接下来的一个项目的技术选型,最终这些东西都汇总到一个openoffice的文档中。但是我越来越不喜欢这种写文档的方式,首先这些office的文档都是二进制的,对搜索引擎不友好,更重要的是,我觉得写文档和软件开发一定程度上是由共性的,就是说是一个迭代和增量的过程,需要不断的反馈,需要集体的协作。我想要的写文档的方式应该是多个人一起完成初稿,然后所有的相关人员都可以来浏览,来编辑,来提意见,就是wikipedia的方式,当然一个专职的文档维护人员(或者一个维护委员会)是必须的,由他来最终确定哪些修改是好的,哪些意见是应该被抛弃的。最后,文档应该是公开的,易于查看,相关文档应该彼此链接,不应该藏在svn的某个深深的角落。
WikiWiki可以解决这个问题,我觉得可以一个主要编写人员首先列出提纲,然后每个部分都有专业的人员来编写,而这个文档随时任何相关人员都可以查看,可以提意见,最终依靠集体的力量完成文档。传统的方式都是一个人完成文档,然后组织大家一起来讨论,然后继续修改,继续讨论。这种方式的效率不高,这要求审核人员必须在特定的时间段里去阅读文档,进行讨论,而审核人员到底付出多大的精力去阅读这篇文档很重要。就目前的经历来看,审核人员通常都不会在这件事情上花多少力气,所以审核的质量非常低。通过wiki的方式,大家可以更深刻的参与到文档的编写工作中来,每个人都随时可以查看文档的进度,可以添加注释,可以直接修改。
下面就说说wiki系统的安装,因为我决心要学习python,所以专门找了一个python的wiki系统,MoinMoin,很多大型网站都采用了这个wiki系统,尤其开源网站,它们更有动力采用wiki来促进社区的交流。

操作系统:Ubuntu 7.10 Desktop, linux kernel 2.6.22-14-generic
Python : 2.5.1
WebServer: lighttpd 1.4.18 (ssl)
MoinMoin: 1.6.3

::安装过程可以查看MoinMoin发布包中自带的Install.html
1. 确认python和lighttpd都已经安装成功。 一般来说ubuntu系统已经自带了python,而安装lighttpd也非常简单,执行'sudo apt-get install lighttpd'就可以了。
  • 在控制台执行'python', 出现python提示符'>>>'就表示python已经安装成功了。
  • 执行'lighttpd -D -f /etc/lighttpd/lighttpd.conf', 然后在浏览器中访问'http://localhost',看到html页面就表示lighttpd安装成功了。
2.下载MoinMoin, 解压缩发布包之后,进入安装文件目录,在控制太执行安装命令:
>>>python setup.py install --prefix='/usr/local' --record=install.log
其中, --prefix表示安装目标目录,如果不指定,那么会安装到默认目录,通常是/usr/lib/python2.5/site-pakcage/moin和/usr/share/moin; --record表示安装日志,可以从中看出哪些文件被copy到哪个目录了。
NOTE:更多信息可以参考moinmoin的install.html
3. ....关于moin的配置请参考moinmon的install.html
4. 配置FasgCGI。如果CGI方式,那么每个请求相应的cgi文件都会被重新load,这样会极大的影响的性能, 而FastCGI方式只需要在第一次请求的时候load脚本,然后这些脚本就会被缓存下来,这样可以大大提高性能,此外,FasgCGI是一个单独运行的网关进程,就算webserver崩溃也不会影响到它。在多核的环境下,FastCGI是个不错的选择。mod_python是运行在webserver中的一个线程,在超线程的环境下可能表现更好。lighttpd不支持mod_python。
lighttpd是一个高性能的,轻型的(相较与apache),高度模块化的webserver,它需要在配置文件lighttpd.conf中指定装载fcgi模块,并且配置fcgi相应的参数。
note: 可以参考install.html中的'deploying in lighttpd'
  • 设定装入fcgi模块:
    server.modules              = (
    "mod_access",
    "mod_rewrite", # <--- IMPORTANT! "mod_status", "mod_fastcgi", # <--- IMPORTANT! "mod_accesslog", "mod_redirect", "mod_auth", "mod_expire", )
  • 配置fasgcgi模块,
    $HTTP["host"] =~ "^127.0.0.1" {
    url.rewrite-once = (
    "^/robots.txt" => "/robots.txt",
    "^/favicon.ico" => "/favicon.ico",
    "^/moin_static163/(.*)" => "/$1", #所有的media文件url都是以moin_static163开头的
    "^/(.*)" => "/wiki-engine/$1" #除了media文件和robots,favicon外的请求都送到fastcgi server
    )
    server.document-root = "/usr/share/moin/htdocs/"
    #url以wiki-engine开始的都送到fastcgi。 url会被rewrite模块重写,可以看前面的定义url.rewrite-once
    $HTTP["url"] =~ "^/wiki-engine/" {
    fastcgi.server = ( "/wiki-engine" =>
    (( "docroot" => "/",
    "min-procs" => 10,
    "max-procs" => 10,
    "max-load-per-proc" => 2,
    # allocate successive port numbers for each process, starting with "port"
    "bin-path" => "/opt/mylib/moin-1.6.3/moin.fcg", #如果由lighttpd来启动fastcgi,那么需要指定这个值
    "host" => "127.0.0.1",
    "port" => 3060,
    "check-local" => "disable",
    ))
    )
    }
    }
一般来说,由于指定了bin-path参数,所以lighttpd会自动启动一个fastcgi进程,不需要额外的在外面手动启动一个fastcgi进程。 如果没有指定,那么需要手动在外面启动fastcgi进程,同时保证指定的host,port与fastcgi server的定义是一致的。 lighttpd和fastcgi server之间通过tcp socket通信,也可以直接通过unix-domain socket(IPC socket,进程间通信)通信,这个需要指定fastcgi.server的socket参数,host+port和socket只能采用一种。
5.这里反过来再提一下MoinMoin的配置。我的MoinMoin是安装到默认目录,即/usr/lib/python2.5/site-package/moin和/usr/share/moin,前面一个目录放置的是moinmoin的python源代码, /usr/share/moin放置的是模板代码(html,img,css等)和数据文件(保存用户数据,比如page, comments,user等)。然后我又把/usr/share/moin中的data, underlay,wikiconfig.py, moin.fcg复制到/opt/mylib/moin目录,这个目录叫做MoinMoin的一个实例。就是说你可以同时运行多个实例,它们分别有自己的名称空间,彼此不会影响,但是共用htdocs。
此外,需要注意的是权限问题,由于lighttpd是以www-data的用户运行,而fastcgi程也是它启动的,即fastcgi server也是以www-data的用户运行的,那么www-data必须具有/opt/mylib/moin目录读写执行权限。 总之要注意权限问题。但是如果fastcgi是通过手动单独在外面启动的,那么启动这个进程的用户必须具有读写执行/opt/mylib/moin的权限,这样的话,它是独立与lighttpd的权限系统之外的,可以另外设置权限规则。

Thursday, May 15, 2008

我的兴趣真是广泛......咳咳

对于我的职业规划,我一直是想做一名架构师,因为本身有对于技术的持续的热情和学习动力。而且兴趣广泛对于一名架构师来说应该算是优势,作为架构师保持宽阔的眼界应该是很重要的。 但是有时候在这些兴趣之间会有些迷失,而且热点变化的过快,导致了贪多嚼不烂。 比如这段时间, 由于工作上相对比较清闲,所以可以去看很多东西,包括document software architecture, agile development(scrum), python(jython), osgi, maven, flex,跨度可以说是相当的大,的确是开阔了眼界,但是沉淀的不多, 也许面试的时候到可以满口的胡说八道,让人目眩神迷。
我想是需要计划了, 应该给这些东西排个优先级别,然后各个击破,当然不是每个都要精通,比如flex(个人来说,我觉得这是比ajax更有前途的东西),就是所谓的divide and conquer。下面就列一下吧, 优先级从1-5,1便是优先级最高:
  1. document software architecture: 最切合职业规划.....P1
  2. agile development: 敏捷开发,其实还包括up,xp等......P2
  3. python:作为一个程序员,至少得会两门语言,这是我的要求.....P2
  4. flex: ria技术,相信比ajax更有前途.....P3
虽然flex的有限级别最低,但是我刚刚下载了flex sdk, 怎么说呢,现在等不及要尝试一下了。
.......
经过短暂的狂热,并且下载了flex_sdk_3,阅读了一些开发文档之后,狂热劲似乎慢慢消逝了。怎么说呢,flex框架包含了大量的漂亮的ui组件,而且对开放标准的支持也不错,比如使用css来定义组件外观,用mxml语言来定义界面,actionscript来定义逻辑,这种设计对程序员来说更加友好,毕竟让一个程序员来进行那种基于时间帧的设计有点牵强。我相信可以使用flex构建出炫目的应用,但是有一点,我觉得非常不足,就是flex与浏览器的整合太弱了,这里说的整合不是说浏览器是否都安装了flash插件这个问题,而是说flash与浏览器之间的互操作性,以及与html的dom模型之间的互操作性, flash虽然是运行在浏览器中,但是它们显然是两个世界的东西, 这让我觉得不太舒服, 可能我们以前使用浏览器的操作习惯在面对flash的时候都变的不一样,这可能会让人们困惑,从这个角度讲,我开始更倾向于ajax了。

Friday, May 09, 2008

读书笔记:Document software architecture

在读一本书,不是教你如何做架构设计,而是如果将系统架构记录下来,但这对架构设计还是很有帮助的,因为你知道了如何文档化一个系统架构,就说明你知道了架构应该包含哪些元素,应该从什么角度来描述架构,这真的很重要,是一种thinking层次的东西,比仅仅会技术还要带劲。
这篇博客是用来作为我的读书笔记, 英文文档已经看完了,但是有些部分太晦涩,用了太多不认识的单词,看的头晕,现在又弄了中文版来对照看。 中文版翻译的一般,有些地方我需要回过去看英文。

[architecture]
什么是架构???
A software architecture for a system is a structure of the system, which comprise elements, relation among them, and the external visible properties of those elements and relations.


[view]

[viewtype]

[style]

[如何描述架构?]
Architects need to
think about their software in three ways simultaneously(同时,一起):
1. How it is structured as a set of implementation units (module viewtype)
2. How it is structured as a set of elements that have runtime behavior and interactions(component-and-connector viewtype)
3. How it relates to nonsoftware structures in its environment (allocation viewtype)

咳咳,先做个记号,有空才写!

Thursday, May 08, 2008

关于字符(characters),字符集(character sets), 编码(encodings)

在程序中我们经常要处理字符的编码转换,比如两个系统之间传输包含中文字符的信息时候,或者向数据库存储/读取中文数据的时候,你都会碰到这个问题。对于缺乏经验的程序员,这是个烦恼和郁闷的问题。 起码我是经受过这样的反复折磨,每个项目都会碰到这个问题,而每次解决了之后还是觉得若有所示,因为我不清楚为什么这样就解决了,虽然我一直是这样解决这个问题的。 所以我期望把这个问题搞清楚,我希望知道为什么错,有为什么对。。。。

[字符,字符集,编码]

字符, 呵呵,就是字符,比如a,b,c,或者汉字的'我'.
字符集(比如unicode)定义了一种编码规范支持哪些字符,每个字符都会被分配一个编码(或者说整数,以免和后面要说的编码混淆,后面说的编码应该是一个动词),通过这个编码就可以映射到相应的字符. 在不同的字符集中,同一个字符的编码一般是不一样的.
而编码(比如utf-8)是定义了如何将这些字符保存在内存或者文件等。 GBK字符集包括2万多个汉字,每个字符都有一个码,编码就是如何将这些码保存下来。 unicode有些不同,因为unicode为每个字符定义了code point,编码标准,比如utf-8, ucs-2,定义了如何将code point保存下来。

[存在的编码方法]

这里说的编码方式不仅仅只中文,还包括其他,相信俄文,印度文等等都会有自己的类似gbk一类的编码标准。下面只列出我知道的,比较常用的编码方式:
  • ASCII: American Standard Code for Information Interchange, 这是英文字符的编码,使用了单字节,ASCII的最高bit位都为0,从0-127。
    ASCII码是7位编码,编码范围是0x00-0x7F。ASCII字符集包括英文字母、阿拉伯数字和标点符号等字符。其中0x00-0x20和0x7F共33个控制字符。

    只支持ASCII码的系统会忽略每个字节的最高位,只认为低7位是有效位。HZ字符编码就是早期为了在只支持7位ASCII系统中传输中文而设计的编码。早期很多邮件系统也只支持ASCII编码,为了传输中文邮件必须使用BASE64或者其他编码方式。
  • Latin-1(ISO-8859-1): 这是欧洲的编码,因为西欧语言中包含很多字符,比如á这种字符。它是ASCII的超集,那些古怪字符用的是128-255的范围,利用单字节存储。
  • Unicode(www.unicode.org):这是一个想要大一统的编码标准,要为世界上每种语言的每个字符都定义一个编码。它又是Latin-1的超集,用两个字节存储。当然不可能是所有字符,其实是语言的常用字符,比如汉字编码的范围是4e00-9fcf,大概2万个左右,不会包含古老的甲骨文汉字。 unicode是一个字符集,它有许多不同的编码方式,比如UCS-2是直接用两个自己来存储,而utf-16也是用两个自己存储,而utf-8是变长的,对0-127之间的用单个字节,128以上的用两个,三个甚至6个字节来表示。(CHECK:??字节的最高位第八位用来表示这个字符是单字节还是双字节
  • GBK:这是中国定义的汉字编码标准,最早是GB2312,只定义了常用汉字,大概2万个左右(unicode主要包含gb2312中的汉字),后来的gbk是对gb2312的扩展,加入了很多不常用的汉字,同时也支持了繁体字。中文字符的每个字节的最高位都为1。 在中国大陆使用的计算机上,它们的本地编码(ANSI编码)大多都是gbk。在一个国家的本地化系统中出现的一个字符,通过电子邮件传送到另外一个国家的本地化系统中,看到的就不是那个字符了,而是另个那个国家的一个字符或乱码。(http://www.btinternet.com/~jlonline/back/GBK.htm)
    GB2312是基于区位码设计的,区位码把编码表分为94个区,每个区对应94个位,每个字符的区号和位号组合起来就是该汉字的区位码。区位码一般 用10进制数来表示,如1601就表示16区1位,对应的字符是“啊”。在区位码的区号和位号上分别加上0xA0就得到了GB2312编码。

  • UTF-8: 首先 UCS 和 Unicode 只是分配整数给字符的编码表. 现在存在好几种将一串字符表示为一串字节的方法. 最显而易见的两种方法是将 Unicode 文本存储为 2 个 或 4 个字节序列的串. 这两种方法的正式名称分别为 UCS-2 和 UCS-4. 除非另外指定, 否则大多数的字节都是这样的(Bigendian convention). 将一个 ASCII 或 Latin-1 的文件转换成 UCS-2 只需简单地在每个 ASCII 字节前插入 0x00. 如果要转换成 UCS-4, 则必须在每个 ASCII 字节前插入三个 0x00.

    在 Unix 下使用 UCS-2 (或 UCS-4) 会导致非常严重的问题. 用这些编码的字符串会包含一些特殊的字符, 比如 '\0' 或 '/', 它们在 文件名和其他 C 库函数参数里都有特别的含义. 另外, 大多数使用 ASCII 文件的 UNIX 下的工具, 如果不进行重大修改是无法读取 16 位的字符的. 基于这些原因, 在文件名, 文本文件, 环境变量等地方, UCS-2 不适合作为 Unicode 的外部编码.

    在 ISO 10646-1 Annex R 和 RFC 2279 里定义的 UTF-8 编码没有这些问题. 它是在 Unix 风格的操作系统下使用 Unicode 的明显的方法.

    UTF-8 有以下特性:

    *UCS 字符 U+0000 到 U+007F (ASCII) 被编码为字节 0x00 到 0x7F (ASCII 兼容). 这意味着只包含 7 位 ASCII 字符的文件在 ASCII 和 UTF-8 两种编码方式下是一样的.
    *所有 >U+007F 的 UCS 字符被编码为一个多个字节的串, 每个字节都有标记位集. 因此, ASCII 字节 (0x00-0x7F) 不可能作为任何其他字符的一部分.
    *表示非 ASCII 字符的多字节串的第一个字节总是在 0xC0 到 0xFD 的范围里, 并指出这个字符包含多少个字节. 多字节串的其余字节都在 0x80 到 0xBF 范围里. 这使得重新同步非常容易, 并使编码无国界, 且很少受丢失字节的影响.
    *可以编入所有可能的 231个 UCS 代码
    *UTF-8 编码字符理论上可以最多到 6 个字节长, 然而 16 位 BMP 字符最多只用到 3 字节长.
    *Bigendian UCS-4 字节串的排列顺序是预定的.
    *字节 0xFE 和 0xFF 在 UTF-8 编码中从未用到.

    下列字节串用来表示一个字符. 用到哪个串取决于该字符在 Unicode 中的序号.
    U-00000000 - U-0000007F: 0xxxxxxx
    U-00000080 - U-000007FF: 110xxxxx 10xxxxxx
    U-00000800 - U-0000FFFF: 1110xxxx 10xxxxxx 10xxxxxx
    U-00010000 - U-001FFFFF: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
    U-00200000 - U-03FFFFFF: 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
    U-04000000 - U-7FFFFFFF: 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

    xxx 的位置由字符编码数的二进制表示的位填入. 越靠右的 x 具有越少的特殊意义. 只用最短的那个足够表达一个字符编码数的多字节串. 注意在多字节串中, 第一个字节的开头"1"的数目就是整个串中字节的数目.

    例如: Unicode 字符 U+00A9 = 1010 1001 (版权符号) 在 UTF-8 里的编码为:

    11000010 10101001 = 0xC2 0xA9

    而字符 U+2260 = 0010 0010 0110 0000 (不等于) 编码为:

    11100010 10001001 10100000 = 0xE2 0x89 0xA0

    这种编码的官方名字拼写为 UTF-8, 其中 UTF 代表 UCS Transformation Format. 请勿在任何文档中用其他名字 (比如 utf8 或 UTF_8) 来表示 UTF-8, 当然除非你指的是一个变量名而不是这种编码本身.

[编码转换]
实际上不存在什么编码转换地概念, 当你用什么编码方式写出的时候, 读入就应该用同样的编码方式,否则乱码就来找你了.先看下面一个例子:


String input = "S茅n茅gal";
System.out.println(HexCoder.bufferToHex(input.getBytes("GBK")));
>>53c3a96ec3a967616c
System.out.println(HexCoder.bufferToHex(input.getBytes("UTF-8")));
>>53e88c856ee88c8567616c
String utf8 = new String(input.getBytes("GBK"), "UTF-8");
System.out.println(utf8);
>>Sénégal

可以看到GBK和UTF8得到的字节数组是不一样的,有趣的是最有的变量'utf8',它的内容是法文'Sénégal', 而初始的变量'input'才是乱码.这个转换其实容易理解, input.getBytes('GBK')会得到'53c3a96ec3a967616c', 而用utf-8的方式来解码的话,就得到了'Sénégal'.这个例子让我们觉得乱码和原文之间可以互相转换, 某种程度上说是这样的.
但是, 再看看下面的例子:


String input = "我";
System.out.println(HexCoder.bufferToHex(input.getBytes("GBK")));
>>ced2
System.out.println(HexCoder.bufferToHex(input.getBytes("UTF-8")));
>>e68891
String utf8 = new String(input.getBytes("GBK"), "UTF-8");
System.out.println(utf8);
>>??
System.out.println(HexCoder.bufferToHex(utf8.getBytes("GBK")));
>>3f3f
System.out.println(HexCoder.bufferToHex(utf8.getBytes("UTF-16")));
>>fefffffdfffd
* feff is Unicode Byte Order Mark.

这个例子和第一个例子只有变量input的值不同, 但是可以看到变量utf8.getBytes("GBK")最后打印出来是??, 为什么? input.getBytes("GBK")后得到了ced2, 当用UTF8来解码的时候,utf8完全无法识别这个编码,所以用?(\ufffd, 不是问号\u003f)表示. 而最后一句'utf8.getBytes("GBK")', 打印出来的也就是3f3f(3f就是ascii中的?, 因为gbk的字符集中没有和\ufffd对应的字符)了.

还有一个问题,java中的字串是unicode, 这句话到底什么意思? 看下面的例子:
System.out.println("\u0048\u0065\u006C\u006C\u006F");
>>Hello
这里0048是字符H的unicode的code point,和编码(就是如何保存这个字串在内存/磁盘或者在网络中传输)没有关系.

It does not make sense to have a string without knowing what encoding it uses
[内码外码]
所谓内码就是指的gbk,unicode的编码,而外码指的是输入法中定义的字码,外码是可以随便定义,而内码是不能变的,输入法会映射外码到内码。

这里还是要强调需要搞清楚字符,字符集,编码的区别, 可以认为GBK的字符集和编码定义的值都是一样的.
看看在java中的表现.
char c = '甜';
System.out.println(Integer.toHexString((int)c));
// print out: 751c, 打印出来'甜'的unicode的codepoint(就是说在jvm内部是以codepoint(unicode字符集定义了每个字符的codepoint)来表示一个uncode字符。但是当需要和其他系统交换这个字符的时候,就需要采用编码规则来将字符编码成字节), 这
//也说明为什么我们总是说java中的字符都是unicode的. 也就是说,JVM内存中, 所有的字符都是以unicode的UTF16编码形式表示的
byte[] output = src.getBytes("UTF-8");
for (byte o : output){
System.out.println(Integer.toHexString(o));
}
// print out:
// ffffffe7
// ffffff94
// ffffff9c

output = src.getBytes("GBK");
for (byte o : output){
System.out.println(Integer.toHexString(o));
}
// print out:
// ffffffcc
// fffffff0
// 为什么能得GBK的编码值呢? java提供了一个CharsetDecoder的类用来执行这种转换(每一个charset都会有一个
// 对应的子类), 估计应该是首先从UTF16的编码得到unicode字符集的code point, 然后在unicode的字符集和
// gbk的字符集之间有一个映射(估计操作系统会管理这个映射,或者JVM自己管理,这个不是重点),最后对这个GBK的code point进行GBK编码.
// 就是说,字符集是不同编码之间进行转换的桥梁.

output = src.getBytes("UTF-16");
for (byte o : output){
System.out.println(Integer.toHexString(o));
}
// print out:
// fffffffe
// ffffffff
// 75
// 1c
// Java2平台内对unicode采用UTF16的编码方式, 这种编码方式GBK的特点, 编码出来的值和code point是一样的
// (只是指unicode的BMP部分:U+0000 - U+FFFF). 对大于U+FFFF的字符会用四个字节来进行编码,具体的可以参考
// UTF16编码规范. 为什么'tian'在utf16编码后会有四个字节呢????

更多信息可以参考:
  • 字符,字节和编码:http://www.regexlab.com/zh/encoding.htm
  • GBK, http://www.btinternet.com/~jlonline/back/GBK.htm
  • 字符编码笔记(

    http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html)
  • The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (http://www.joelonsoftware.com/articles/Unicode.html)

Tuesday, May 06, 2008

跳进python, Django

一直都想学习java以外的一门新语言,在学习java以前看过perl, php, 但是粗略的看一看,又没有地方来用,最终就淡忘了。 后来开始学python,也是一样的,有空看看书,但是用的机会不多,慢慢又模糊了, 真是浪费生命。 再以后开始用python写一些小工具来辅助开发,比如写了一个破解我老婆博客密码的小东东, 一个根据diff文件构建升级包的工具, 实践是最好的老师,一些模糊的记忆又慢慢开始清晰了。 java世界开始变得开放,比如jython的出现,这样python代码可以直接在java虚拟机上跑了。 更好的事情是grinder,一个开源的java性能测试工具, 它的测试脚本支持用jython来写, 于是就在项目里面小用了一下,很有意思, python可以在日常工作中发挥更大的作用了。
不过这里要写这篇blog是用来作为我学习python的笔记, 一些自己在使用python的过程经常出错的东西都要在这里记录下来,免得同一个石头绊我三次。

[内置的数据类型]
dictionary: key必须是常量 ,key大小写敏感,而且不能重复。 其中的元素是没有秩序的。
list::相当于php中的数组,可以保存不同类型的数据。 其中元素是有秩序的
tuple: 不可变的list, 可以用tuple()来将一个list转化为tuple; 反过来可以用list()将一个tuple转化为list

python是一种强类型的语言,就是说一旦将给一个变量赋值之后,这个值的类型是不能变化的,比如进行显式的强制转换,比如: int('1'), str(1); 对于弱类型的语言,这种转换可以自动完成,比如‘1’ + 2在python中会抛出异常,但是javascript中可以返回‘12’, 或者3,取决于你怎么使用。
python中有一个内置函数isinstance()可以检验某个值的数据类型(int, long, str, dict, list, tuple, float, Class etc),比如: isinstance(1, int) = True, isinstance('1', int) = False

python中有个types模块,其中定义了很多内置的数据类型:
>>> import types
>>> dir(types)

[内置函数]
[getattr]
使用 getattr 函数,可以得到一个直到运行时才知道名称的函数的引用。
>>> li = ["Larry", "Curly"]
>>> li.pop

>>> getattr(li, "pop")

>>> getattr(li, "append")("Moe")
>>> li
["Larry", "Curly", "Moe"]
[isinstance]
是否是某个类型的实例, isinstance(1, int) = true
[callable]
接收任何对象作为参数,如果参数对象是可调用的,返回 True;否则返回 False
[range ]
内置的 range 函数返回一个元素为整数的 list。这个函数的简化调用形式是接收一个上限值,然后返回一个初始值从 0 开始的 list,它依次递增,直到但不包含上限值。(如果您愿意,您可以传入其它的参数来指定一个非 0 的初始值和非 1 的步长。也可以使用 print range.__doc__ 来了解更多的细节。)
(MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY) = range(7)

[filter]

Python 有一个内建 filter 函数,它接受两个参数:一个函数和一个列表,返回一个列表。作为第一个参数传递给 filter 的函数本身应接受一个参数,filter 返回的列表将会包含被传入列表参数传递给 filter 所有可以令函数返回真 (true) 的元素。

>>> def odd(n):                 1
... return n % 2
...
>>> li = [1, 2, 3, 5, 9, 10, 256, -3]
>>> filter(odd, li) 2
[1, 3, 5, 9, -3]

[map]

使用内建 map 函数。它的工作机理和 filter 函数类似。

>>> def double(n):
... return n*2
...
>>> li = [1, 2, 3, 5, 9, 10, 256, -3]
>>> map(double, li) 1
[2, 4, 6, 10, 18, 20, 512, -6]

[other...]

获得定长字符串:

>>>s = 'xiix'

>>>s.ljust(8, '0')

'xiix0000'

>>>s.zfill(8)

'0000xiix'

[映射list]

Python 的强大特性之一是其对 list 的解析,它提供一种紧凑的方法,可以通过对 list 中的每个元素应用一个函数,从而将一个 list 映射为另一个 list。

>>> li = [1, 9, 8, 4]
>>> [elem*2 for elem in li] 1
[2, 18, 16, 8]
[过滤列表]
这种能力同过滤机制结合使用,使列表中的有些元素被映射的同时跳过另外一些元素。
[mapping-expression for element in source-list if filter-expression]
[使用lamba函数]
Python 支持一种有趣的语法,它允许你快速定义单行的最小函数。这些叫做 lambda 的函数,是从 Lisp 借用来的,可以用在任何需要函数的地方。
>>> (lambda x: x*2)(3) 2
6
lambda 函数可以接收任意多个参数 (包括可选参数) 并且返回单个表达式的值。lambda 函数不能包含命令,包含的表达式不能超过一个。

[多变量赋值]
>>>t = (1, 2, 3)
>>>(x, y, z) = t
x = 1, y =2, z =3
>>>l = [1, 2]
>>>[x,y] = l
x = 1, y = 2

[类的定义]
  • 当从你的类中调用一个父类的一个方法时,你必须包括 self 参数。
  • __init__ 方法是可选的,但是一旦你定义了,就必须记得显示调用父类的 __init__ 方法 (如果它定义了的话)。这样更是正确的:无论何时子类想扩展父类的行为,后代方法必须在适当的时机,使用适当的参数,显式调用父类方法。
class FileInfo(UserDict):
    "store file metadata"
def __init__(self, filename=None):
UserDict.__init__(self) 1
self["name"] = filename
2
  • 要在类的内部引用一个数据属性,我们使用 self 作为限定符。习惯上,所有的数据属性都在 __init__ 方法中初始化为有意义的值。然而,这并不是必须的,因为数据属性,像局部变量一样,当你首次赋给它值的时候突然产生
  • 专用类方法。Python 有一个与 __getitem__ 类似的 __setitem__ 专用方法,比如f['name']这个看上去就像你用来得到一个字典值的语法,事实上它返回你期望的值。下面是隐藏起来的一个环节:暗地里,Python 已经将这个语法转化为 f.__getitem__("name") 的方法调用。这就是为什么 __getitem__ 是一个专用类方法的原因,不仅仅是你可以自已调用它,还可以通过使用正确的语法让 Python 来替你调用。
  • 类属性和数据属性。 数据属性在__init__方法中通过'self.XXX'来定义,是一个特定类实例所拥有的变量;类属性是在class中直接定义, 是由类拥有的属性,当然所有该类的实例都拥有该属性。
[高级专用类方法]
  • __repr__ 是一个专用的方法,在当调用 repr(instance) 时被调用。repr 函数是一个内置函数,它返回一个对象的字符串表示。它可以用在任何对象上,不仅仅是类的实例。你已经对 repr 相当熟悉了,尽管你不知道它。在交互式窗口中,当你只敲入一个变量名,接着按ENTER,Python 使用 repr 来显示变量的值。自已用一些数据来创建一个字典 d ,然后用 print repr(d) 来看一看吧。(java.lang.Object#toString())
  • __cmp__ 在比较类实例时被调用。通常,你可以通过使用 == 比较任意两个 Python 对象,不只是类实例。有一些规则,定义了何时内置数据类型被认为是相等的,例如,字典在有着全部相同的关键字和值时是相等的。对于类实例,你可以定义 __cmp__ 方法,自已编写比较逻辑,然后你可以使用 == 来比较你的类,Python 将会替你调用你的 __cmp__ 专用方法。(java.lang.Object#equals(o, o)
  • __len__ 在调用 len(instance) 时被调用。len 是一个内置函数,可以返回一个对象的长度。它可以用于任何被认为理应有长度的对象。字符串的 len 是它的字符个数;字典的 len 是它的关键字的个数;列表或序列的 len 是元素的个数。对于类实例,定义 __len__ 方法,接着自已编写长度的计算,然后调用 len(instance),Python 将替你调用你的 __len__ 专用方法。
  • __delitem__ 在调用 del instance[key] 时调用 ,你可能记得它作为从字典中删除单个元素的方法。当你在类实例中使用 del 时,Python 替你调用 __delitem__ 专用方法。
这些其实就和java中的Object类相似, 因为java中所有类都是从Object继承的,那些方法都定义在Object中,而python没有这样的基类,通过这些专用方法来达到类似的效果。或者有些象设计模式中的模板方法(template method)

[xml解析]
  • 如果你打算在你的 Python 代码中保存非 ASCII 字符串,你需要在每个文件的顶端加入编码声明来指定每个 .py 文件的编码。这个声明定义了 .py 文件的编码为 UTF-8:
#!/usr/bin/env python
# -*- coding: UTF-8 -*-

  • 通过sys.getdefaultencoding()得到当前的默认编码形式, 在python启动以后就不能调用sys.setdefaultencoding('latin-1')来设定编码。 这里介绍一些简单的历史。ASCIi是英语字符的编码,只需要0-127, 但是欧洲,尤其是西欧有很多古怪的字符,比如西班牙语里有字符在n上面加一个波浪线,为了增加对这些字符的支持,出现了ISO-8859-1也就是Latin-1标准,所有那些字符都在128-255之间。 而unicode是一个想要统一天下所有字符的编码标准,地球上所有语言的任何一个字符都可以找到一个唯一的编码,使用两个字节,从0到65535(仅仅中日韩就不止65535个字符了吧,不够用)。 utf-8是对unicode进行的演化,因为对于很多iso-8859-1标准内定义的字符,它们用一个字符就可以表示了,如果用unicode来表示的话就相当于浪费一个字节,所以utf-8对ASCII等单字节的字符仍然只用一个字节表示,对其他的还是双字节。 而utf-16就是对所有字符都使用双字节。
  • iso-8859-1是ascii的超集,unicode是iso-8859-1的超集。 而中文又存在GBK和GB2312两个标准, gbk是gb2312的超集,其中增加了对繁体字,以及一些没有收录到gb2312中的汉字字符。 gb是中国的标准,在一定程度上,比如0-127段的定义和ascii是一致的。 更多信息可以参考维基(http://en.wikipedia.org/wiki/GBK)
[动态导入]
>>> sys = __import__('sys')           1
>>> os = __import__('os')
>>> re = __import__('re')
>>> unittest = __import__('unittest')
>>> sys 2
>>>
>>> os
>>>
1 内建 __import__ 函数与 import 语句的既定目标相同,但它是一个真正的函数,并接受一个字符串参数。
2 变量 sys 现在是 sys 模块,和 import sys 的结果完全相同。变量 os 现在是一个 os 模块,等等。

因此 __import__ 导入一个模块,但是是通过一个字符串参数来做到的。依此处讲,你用以导入的仅仅是一个硬编码性的字符串,但它可以是一个变量,或者一个函数调用的结果。并且你指向模块的变量也不必与模块名匹配。你可以导入一系列模块并把它们指派给一个列表。


[参考资源]

Monday, May 05, 2008

关于http协议中定义的缓存机制与网站性能

一直以来大家都在用http协议,但是有几个人真正的了解http协议?也许你会说,我只需要会用就可以了, 干吗要去读http协议规范? 某些情况下,我觉得这样说是对的, 甚至绝大多数情况下可能都是对的,在于你是要做什么。 但是, 我还是坚持,在你急急忙忙的完成了一个和http有关的功能之后,还是应该好好想想,你为什么要这样使用http协议,这个时候你应该静下心来好好读读http的协议规范, 作为一个真正的程序员, 你应该这么做。

这里有点搬起石头砸自己脚的意思,因为我自己也只是看了http协议规范中和cache有关的一部分,而且不敢保证是所有相关的部分都阅读过了,所以,下面说的东西仅供参考。

[caching mechanisms]
cache机制是分几个层次的,他们包括服务器端缓存,isp缓存,客户端缓存。
  • 服务端缓存,比如squid,还有本身很多web开发框架支持页面缓存, 这个缓存会影响所有访问这个网站的用户。
  • isp缓存,isp可能缓存网站的内容,这个缓存会影响所有通过这个isp接入的用户,可能所有通过这个isp接入,而且访问某个被缓存网站的用户的请求直接在isp就被返回了,不会到达请求的网站。
  • 客户端缓存,一般指浏览器缓存,这个缓存只影响单个用户, 这样用户直接从本地装载内容,或者只需要发送查询请求(if-modified-since)。
所有这些缓存机制可以极大的提高对用户的响应速度,同时也极大的降低了服务器的负荷,因为大量的请求还没有到达网站服务器就已经被处理了。

[Cache-Control]
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
cache-control是http1.1中定义的头,在http1.1之前(http1.0)是通过expire都来指定cache策略。就是说在http的request/response中指定的这些关于cache策略的头是用来影响前面提到的cache机制, 这些cache机制根据cache头来处理其中的cache内容。
具体的cache-control的写法就不详细说,可以自己参考http规范, 这里就说说几个比较常用的值。
[cache-control: public; max-age=21600]
public/private: public 表示这个响应可以被任何cache机制缓存下来,如果是private表示响应绝对不能被共享缓存(shared cache)cache下来,比如用户的登录进某个网站后看到的用户管理页面,如果这个页面被shared cache缓存下来,假设是被isp缓存下来了,那么所有通过这个isp访问该网页的用户都会得到某个其他用户的管理页面,这是绝对不允许的,这样泄漏了用户隐私信息。
max-age: 单位为秒,表示这个响应可以被缓存多长时间。 如果同时也指定了expire头,那么总是以max-age的值为准。 但是并不总是这样,因为有些缓存机制可能还不支持http1.1规范,那么它们就会忽略cache-control头,而仅仅采用expire头,所以在响应中总是同时指定expire头总是一个比较好的主意.
no-cache:

If the no-cache directive does not specify a field-name, then a cache MUST NOT use the response to satisfy a subsequent request without successful revalidation with the origin server.
就是说no-cache还必须指定可以被缓存的某个/某几个field-name,否则这个响应 不能被缓存下来。
no-store: 表示整个响应不能被保存下来,尤其是一些敏感信息。 请求和响应中都可以指定这个值。

[If-Modified-Since/Last-Modified]
last-modified: 表示请求的网页最后的修改时间(也许说是请求的资源最后修改时间更合适)。只会在响应中携带这个头。一般来说如果请求的是一个文件,这个头可能就是操作系统里面该文件的last-modified时间。
if-modified-since: 如果cache机制缓存了某个响应,这个响应中指定了last-modified头,那么下次有用户请求这个缓存的网页时, cache会向original server(产生这个资源的服务器)发送一个请求,请求中包含if-modified-since头,original server检查请求的资源,并且将服务器上该资源的last-modified和if-modified-since的值进行比较,如果服务器上的资源没有修改,那么返回304(这个响应只包括头),如果服务器上的资源已经有了修改,那么返回200,同时返回新修改过的资源。

如果想要弄清楚http的cache机制,动手实践是必不可少的。 我就是用firefox的一个tamper data的插件访问www.youtube.com来实验的, tamper data可以查看request/response中的头信息。 要说起来,如果做web开发,firefox真是个好东西,它的很多插件都很有意思,还可以推荐一个poster。
[ETag]
ETag 是实现与最近修改数据检查同样的功能的另一种方法:没有变化时不重新下载数据。其工作方式是:服务器发送你所请求的数据的同时,发送某种数据的 hash (在 ETag 头信息中给出)。hash 的确定完全取决于服务器。当第二次请求相同的数据时,你需要在 If-None-Match: 头信息中包含 ETag hash,如果数据没有改变,服务器将返回 304 状态代码。与最近修改数据检查相同,服务器仅仅 发送 304 状态代码;第二次将不为你发送相同的数据。在第二次请求时,通过包含 ETag hash,你告诉服务器:如果 hash 仍旧匹配就没有必要重新发送相同的数据,因为你还有上一次访问过的数据。

[压缩 (Compression)]

一个重要的 HTTP 特性是 gzip 压缩。 关于 HTTP web 服务的主题几乎总是会涉及在网络线路上传输的 XML。XML 是文本,而且还是相当冗长的文本,而文本通常可以被很好地压缩。当你通过 HTTP 请求一个资源时,可以告诉服务器,如果它有任何新数据要发送给我时,请以压缩的格式发送。在你的请求中包含 Accept-encoding: gzip 头信息,如果服务器支持压缩,它将返回由 gzip 压缩的数据并且使用 Content-encoding: gzip 头信息标记。




最后还说点和cache机制无关,但是也和网站性能有关的一个小话题。 用户访问一个网站,如何能做方便的到达他/她最关心的页面? 2次点击,4次点击? 似乎真是个小问题,但在我看来,至少涉及两个方面的问题,一个是易用性的问题,决定了用户是否会真正喜欢并且依赖你的站点; 另外一个是网站性能问题, 如果你把4次点击减少到2次点击,是不是意味着你的服务器承受的负荷也减少了一半? 如果你是在架构一个海量用户访问的站点,比如北京奥运的购票网站,那么这些问题绝对不是小问题。就算你在做小网站,这种努力也是很有价值的。

Thursday, January 03, 2008

关于负载均衡器

web应用服务器集群系统,是由一群同时运行同一个web应用的服务器组成的集群系统,在外界看来,就像是一个服务器一样。为了均衡集群服务器的负载,达 到优化系统性能的目的,集群服务器将众多的访问请求,分散到系统中的不同节点进行处理。从而实现了更高的有效性和稳定性,而这也正是基于Web的企业应用 所必须具备的特性。
  
  高可靠性可以看作为系统的一种冗余设定。对于一个特定的请求,如果所申请的服务器不能进行处理的话,那么其他的服务器能不能对之进行有效的处 理呢?对于一个高效的系统,如果一个Web服务器失败的话,其他的服务器可以马上取代它的位置,对所申请的请求进行处理,而且这一过程对用户来说,要尽可 能的透明,使用户察觉不到!
  
  稳定性决定了应用程序能否支持不断增长的用户请求数量,它是应用程序自身的一种能力。稳定性是影响系统性能的众多因素的一种有效的测量手段,包括机群系统所能支持的同时访问系统的最大用户数目以及处理一个请求所需要的时间。
  
  在现有众多的均衡服务器负载的方法中,广泛研究并使用的是以下两个方法:
  
  DNS负载平衡的方法RR-DNS(Round-Robin Domain Name System)
  负载均衡器
  以下,我们将就这两种方法进行讨论。
  
  DNS轮流排程 RR-DNS(Round-Robin Domain Name System)
  
  域名服务器(Domain Name Server)中的数据文件将主机名字映射到其IP地址。当你在浏览器中键入一个URL时(例如:www.loadbalancedsite.com), 浏览器则将请求发送到DNS,要求其返回相应站点的IP地址,这被称为DNS查询。当浏览器获得该站点的IP地址后,便通过该IP地址连接到所要访问的站 点,将页面展现在用户面前。
  
  域名服务器(DNS)通常包含一个单一的IP地址与该IP地址所映射的站点的名称的列表。在我们上面所假象的例子中,www.loadbalancedsite.com 这个站点的映射IP地址为203.24.23.3。
  
  为了利用DNS均衡服务器的负载,对于同一个站点来讲,在DNS服务器中同时拥有几个不同的IP地址。这几个IP地址代表集群中不同的机器, 并在逻辑上映射到同一个站点名。通过我们的例子可以更好的理解这一点,www.loadbalancedsite.com将通过下面的三个IP地址发布到 一个集群中的三台机器上:
  
  203.34.23.3
  
  203.34.23.4
  
  203.34.23.5
  
  在本例中,DNS服务器中包含下面的映射表:
  
  www.loadbalancedsite.com 203.34.23.3
  
  www.loadbalancedsite.com 203.34.23.4
  
  www.loadbalancedsite.com 203.34.23.5
  
  当第一个请求到达DNS服务器时,返回的是第一台机器的IP地址203.34.23.3;当第二个请求到达时,返回的是第二台机器的IP地址203.34.23.4,以此类推。当第四个请求到达时,第一台机器的IP地址将被再次返回,循环调用。
  
  利用上述的DNS Round Robin技术,对于某一个站点的所有请求将被平均的分配到及群中的机器上。因此,在这种技术中,集群中的所有的节点对于网络来说都是可见的。
  
  DNS 轮流排程的优势
  
   DNS Round Robin的最大的优点就是易于实现和代价低廉:
  
  代价低,易于建立。 为了支持轮流排程,系统管理员只需要在DNS服务器上作一些改动,而且在许多比较新的版本的DNS服务器上已经增加了这种功能。对于Web应用来说,不需要对代码作任何的修改;事实上,Web应用本身并不会意识到负载均衡配置,即使在它面前。
  简单. 不需要网络专家来对之进行设定,或在出现问题时对之进行维护。
  DNS 轮流排程的缺点
  
   这种基于软件的负载均衡方法主要存在两处不足,一是不实时支持服务期间的关联,一是不具有高可靠性。
[ refer to http://www.javaworld.com/cgi-bin/mailto/x_java.cgi]
  • No server affinity(姻亲关系:表示上一次请求送到了某个服务器,那么下个请求还是希望送到原来的那个服务器,否则有可能导致会话信息丢失) support. When a user receives an IP address, it is cached on the browser. Once the cache expires, the user makes another request for the IP address associated with a logical name. That second request can return the IP address of any other machine in the cluster, resulting in a lost session.
  • No HA support. Imagine a cluster of n servers. If one of those servers goes down, every nth request to the DNS server will go to the dead server.
  • Changes to the cluster take time to propagate to the rest of the Internet. Many corporations' and ISPs' DNS servers cache DNS lookups from their clients. Even if your DNS list of servers in the cluster could change dynamically, it would take time for the cached entries on other DNS servers to expire. For example, after a downed server is removed from your cluster's DNS list, AOL clients could still attempt to hit the downed server if AOL's DNS servers cached entries to the downed server. As a result, AOL users would not be able connect to your site even if other machines in the cluster were available.
  • No guarantee of equal client distribution across all servers in the cluster. If you don't configure cooperating DNS servers to support DNS load balancing, they could take only the first IP address returned from the initial lookup and use that for their client requests. Imagine a partner corporation with thousands of employees all pinned to a single server in your cluster!
  
   • 不支持服务器间的一致性。服务器一致性是负载均衡系统所应具备的一种能力,通过它,系统可以根据会话信息是属于服务器端的,还是底层数据库级别的,继而将 用户的请求导向相应的服务器。而DNS轮流排程则不具备这种智能化的特性。它是通过cookie、隐藏域、重写URL三种方法中的一种来进行相似的判断 的。当用户通过上述基于文本标志的方法与服务器建立连接之后,其所有的后续访问均是连接到同一个服务器上。问题是,服务器的IP是被浏览器暂时存放在缓存 中,一旦记录过期,则需要重新建立连接,那么同一个用户的请求很可能被不同的服务器进行处理,则先前的所有会话信息便会丢失。
  
  不支持高可靠性。设想一个具有N个节点的集群。如果其中的一个节点毁坏,那么所有的访问该节点的请求将不会有所回应,这是任何人都不愿意看到 的。比较先进的路由器可以通过每隔一定的时间间隔,对节点检查,如果有毁坏的节点,则将之从列表中去除的方法,解决这个问题。但是,由于在 Internet上,ISPs将众多的DNS存放在缓存中,以节省访问时间,因此,DNS的更新就会变得非常缓慢,以至于有的用户可能会访问一些已经不存 在的站点,或者一些新的站点得不到访问。所以,尽管DNS轮流排程在一定程度上解决了负载均衡问题,但这种状况的改变并不是十分乐观和有效的。
  除了上面介绍的轮流排程方法外,还有三种DNS负载均衡处理分配方法,将这四种方法列出如下:
  
  Ø Round robin (RRS): 将工作平均的分配到服务器 (用于实际服务主机性能一致)
  
  Ø Least-connections (LCS): 向较少连接的服务器分配较多的工作(IPVS 表存储了所有的活动的连接。用于实际服务主机性能一致。)
  
  Ø Weighted round robin (WRRS): 向较大容量的服务器分配较多的工作。可以根据负载信息动态的向上或向下调整。 (用于实际服务主机性能不一致时)
  
  Ø Weighted least-connections (WLC): 考虑它们的容量向较少连接的服务器分配较多的工作。容量通过用户指定的砝码来说明,可以根据装载信息动态的向上或向下调整。(用于实际服务主机性能不一致时)
  
  
  
  负载均衡器
  
  负载均衡器通过虚拟IP地址方法,解决了轮流排程所面临的许多问题。使用了负载均衡器集群系统,在外部看来,像是具有一个IP地址的单一服务 器一样,当然,这个IP地址是虚拟的,它映射了集群中的每一台机器的地址。所以,在某种程度上,负载均衡器是将整个集群的IP地址报漏给外部网络。
  
  当请求到达负载均衡器时,它会重写该请求的头文件,并将之指定到集群中的机器上。如果某台机器被从集群中移除了,请求不会别发往已经不存在的 服务器上,因为所有的机器表面上都具有同一个IP地址,即使集群中的某个节点被移除了,该地址也不会发生变化。而且,internet上缓存的DNS条目 也不再是问题了。当返回一个应答时,客户端看到的只是从负载均衡器上所返回的结果。也就是说,客户端操作的对象是负载均衡器,对于其更后端的操作,对客户 端来讲,是完全透明的。
  
  负载均衡器的优点
  
   • 服务器一致性. 负载均衡器读取客户端发出的每一个请求中所包含的cookies或url解释。基于所读出的这些信息,负载均衡器就可以重写报头并将请求发往集群中合适的 节点上,该节点维护着相应客户端请求的会话信息。在HTTP通信中,负载均衡器可以提供服务器一致性,但并不是通过一个安全的途径(例如:HTTPS)来 提供这种服务。当消息被加密后(SSL),负载均衡器就不能读出隐藏在其中的会话信息。
  
   • 通过故障恢复机制获得高可靠性. 故障恢复发生在当集群中某个节点不能处理请求,需将请求重新导向到其他节点时。主要有两种故障恢复:
  
  • 请求级故障恢复。当集群中的一个节点不能处理请求时(通常是由于down机),请求被发送到其他节点。当然,在导向到其他节点的同时,保存在原节点上的会话信息将会丢失。
  
  • 透明会话故障恢复。当一个引用失败后,负载均衡器会将之发送到集群中其他的节点上,以完成操作,这一点对用户来说是透明的。由于透明会话故障恢复需要节点 具备相应的操作信息,因此为了实现该功能,集群中的所有节点必须具有公共存储区域或通用数据库,存储会话信息数据,以提供每个节点在进行单独进程会话故障 恢复时所需要的操作信息。
  
   • 统计计量。既然所有的Web应用请求都必须经过负载均衡系统,那么系统就可以确定活动会话的数量,在任何实例访问中的活动会话的数目,应答的次数,高峰负 载次数,以及在高峰期和低谷期的会话的数目,还有其他更多的。所有的这些统计信息都可以被很好的用来调整整个系统的性能。
  
  负载均衡器的缺点
  
   硬件路由的缺点在于费用、复杂性以及单点失败的。由于所有的请求均是通过一个单一的硬件负载均衡器来传递,因此,负载均衡器上的任何故障都将导致整个站点的崩溃。
  
  HTTPS请求的负载均衡
  
   正如上面所提到的,很难在那些来自HTTPS的请求上进行负载均衡和会话信息维护处理。因为,这些请求中的信息已经被加密了。负载均衡器没有能力处理这类请求。不过,这里有两种方法可以解决这一问题:
  
  代理网络服务器
  硬件SSL解码器
   代理服务器位于服务器集群之前,首先由它接受所有的请求并对之进行解密,然后将这些处理后的请求根据头信息重新发往相应的节点上,这种方式不需要硬件上的支持,但会增加代理服务器的额外的负担。
  
   硬件SSL解码器,则是在请求到达负载均衡器之前,先经由它进行解密处理。这种方式比代理服务器的处理速度要快捷一些。但代价也高,而且实现比较复杂。

Reverse Proxy(就是对请求方显示单独的接口,对web server也是一样,但一般会携带原始请求的ip)

A reverse proxy or surrogate is a proxy server that is installed within the neighborhood of one or more servers. Typically, reverse proxies are used in front of Web servers. All connections coming from the Internet addressed to one of the Web servers are routed through the proxy server, which may either deal with the request itself or pass the request wholly or partially to the main web servers.

A reverse proxy dispatches in-bound network traffic to a set of servers, presenting a single interface to the caller. For example, a reverse proxy could be used for load balancing a cluster of web servers. In contrast, a forward proxy acts as a proxy for out-bound traffic. For example, an ISP may use a proxy to forward HTTP traffic from its clients to external web servers on the internet; it may also cache the results to improve performance.

Wednesday, January 02, 2008

(转)J2EE集群的神话

http://www.theserverside.com/tt/articles/article.tss?l=J2EEClustering

失效转移可以完全避免错误——否定

Jboss的文档中,整个章节都在警告你“你真的需要HTTP会话的复制吗?”。是的,有时没有失效转移的高可用性的解决方案也是可接受并且是廉价的。失效转移并不是你想象的那么强壮。

那么失效转移到底给你带来了什么?你可能想失效转移可以避免错误。你看,没有会话的失效转移,当一个服务器实例失效后,会话数据将丢失而导致错误。通过失效转移,会话可以从备份中恢复,而请求可以被其他服务器实例所处理,用户根本意识不到失效。这是事实,但这是有条件的!

回想一样我们定义的“失效转移”,我们定义了一个条件是失效转移是在“两个方法调用之间”发生的。这是说如果你有两个连续的对远程对象的方法调用,失效转移只会在第一调用成功后并且第二调用的请求发出前才能发生。

这样,当远程服务器在处理请求的过程中失效了会发生什么呢?答案是:多数情况处理将会停止而客户端将会看到错误信息。除非这个方法是等幂的(Idempotent),只有这个方法是等幂的,一些负载均衡器更智能,它会重试这些方法并将它失效转移到其他实例上。

为什么“等幂”重要呢,因为客户端不知道当失效发生的时候请求被执行到什么地方。是才刚刚初始化还是差不多就要完成了?客户端没法判断!如果方法不是等幂的,在相同方法上的两次调用可能会两次修改系统的状态,而使得系统出现不一致的情形。

你可能想所有在事务中的方法都是等幂的,毕竟,如果错误发生,事务将被回滚,事务状态的改变都将被复位。但事实上事务的边界可能不包括所有的远程方法调用过程。如果事务已经在服务器上提交了而返回给客户端时网络崩溃怎么办呢?客户端不知道服务器的事务是否是成功了。

在一些应用程序中,将所有的方法都做成等幂的是不可能的。这样,你只能通过失效转移减少错误,而不是避免它们。拿在线商店为例,假设每台服务器可以同时处理100个在线用户的请求,当一台服务器失效了,没有失效转移的解决方案将丢失100个用户的会话数据并激怒这些用户。而有失效转移的解决方案中,当失效发生的时候有20个用户正在处理请求,这样20个用户将被失效激怒。而其他80个用户正处于思考时间或在两个方法调用之间,这些用户可以透明地获得失效转移。这样,你就需做以下的考虑:

l 激怒20个用户和激怒100个用户造成影响的区别。

l 采用失效转移和不采用失效转移产品成本的区别

独立应用可以透明的迁移到集群结构中——否定

尽管一些供应商宣称他们的J2EE产品有这样的灵活性。不要相信他们!事实你要在开始系统设计时就要准备集群,而这将影响开发和测试的所有阶段。

Http Session

在集群环境中,如我前面提到的,使用HTTP Session有很多限制,这取决于你的应用程序服务器采用了那种会话失效转移的机制。第一个重要的限制就是所有保存的HTTP Session中的对象必须是可序列化的,这将限制设计和应用程序结构。一些设计模式和MVC框架会用HTTP Session保存一些不序列化的对象(如ServletContextEJB本地接口和WEB服 务引用),这样的设计不能在集群中工作。第二,对象的序列的反序列化对性能的影响很大,特别是数据库保存的方式。在这样的环境中,应该避免在会话中保存大 的或是众多的对象。如果你采用了内存复制的方式,如前所述你必须小心在会话中属性的交叉引用。其他在集群环境中的主要区别是在会话不管任何属性修改,你必 须调用“setAttribute()”方法。这个方法调用在独立的系统中是可选的。这个方法的目的是区别已修改的属性和那些没用到属性,这样系统可以只为失效转移备份必要的数据,从而提高性能。

缓存

我经历过的大多数J2EE项目都用了缓存来提高性能,同时流行的应用程序服务器也都提供了不同程度的缓存用来加快应用程序的速度。但这些缓存都是为那些典型的独立环境设计的,只能在单JVM实例中工作。我们需要缓存是因为一些对象很“重”,创建它需花费大量的时间和资源。因此我们维护了对象池用于重用这些对象,而不需要在后面创建。我们只有当维护缓存比创建对象更廉价时才能获得性能的提高。在集群环境,每个JVM实例都要维护一份缓存的拷贝,这些拷贝必须同步以维持所有服务器实例状态的一致性。有时这种类型的同步会比没有缓存带来更糟的性能。

Static变量

当我们设计J2EE应用程序时,在架构上经常会使用一些设计模式。这些如“Singleton”的设计模式会用到静态变量来在多对象之间共享状态。这种方式在单服务中工作得很好,但在集群环境将失效。集群中的每个实例都会在它的JVM实 例中维护一份静态变量的拷贝,这样就破坏了模式的机制。一个使用静态变量的例子就是用它来保持在线用户数。用静态变量来保存在线用户数是一个很简单的办 法,当用户进入或离开时就增加和减少它。这种方式在单服务器中绝对是好的,但在集群环境将失效。在集群中更好的方式是将所有状态保存到数据库。

外部资源

尽管J2EE规范不支持,但为各种目的仍然会用外部I/O的操作。例如,一些应用会使用文件系统来保存用户上传的文件,或是创建一个动态配置的XML文件。在集群环境是没有办法来在其他实例之间来复制这些文件的。为了在集群中工作,办法是用数据库作为外部文件的存放点,另外也可以使用SAN(存储区域网,Storage Area Network)作为存放点。

特殊服务

一 些特殊的服务只在独立的环境中才有意义,定时服务就一个很好例子,这种服务可以在一个固定的间隔时间有规律的触发。定时服务常用于执行一些自动化管理任 务。如日志文件滚动,系统数据备份,数据库一致性检查和冗余数据清理等。一些基于事件的服务也很难被迁移到集群环境中。初始化服务就是个好例子,它只在整 个系统启动时才发生。邮件通知服务也一样,它在一些警告条件下触发。

这些服务是被事件而不是被请求触发的,而且只被执行一次。这些服务使得负载均衡和失效转移在集群中没多少意义。

一些产品准备了这些服务,如Jboss使用了“集群单例设施”来协调所有实例,保证执行这些服务一次且仅有一次。基于你所选择的平台,一些特殊的服务可能会是把你的应用迁移到集群结构中的障碍。

分布式结构比并置结构更灵活——不一定

J2EE技术,尤其是EJB,天生就是用来做分布式计算。解耦业务功能,重用远程组件,这些使得多层应用非常流行。但是我们不能将所有的东西都分布。一些J2EE架构师认为Web层与EJB层并置得越近越好。这些计论后面会继续。先让我解释一下。

20 分布式结构

如图20所示,这是一个分布式结构。当请求来了,负载均衡器将请求分发到不同服务器中的不同WEB容器,如果请求包含了EJB调用,WEB容器将重发EJB调用到不同的EJB容器。这样,请求将被负载均衡和失效转移两次。

一些人看分布式结构,他们会指出:

l 第二次负载均衡没有必要,因为它不会使任务分配更平坦。每个服务器实例都有它们自己的WEB容器和EJB容器。把EJB容器用来处理来自其他实例WEB容器的请求比只在服务器内部调用并没有什么优势。

l 第二次失效转移没有必要,因为它不能是高可用性。多数供应商实现J2EE服务器都会在同一服务器中运行的WEB容器和EJB容器放在一个JVM实例中。如果EJB容器失效了,多数情况下在同一个JVM中的WEB容器也将同时失效。

l 性能将下降。想像一下对你的应用的一次调用包含一组对EJB的调用,如果你负载均衡了这些EJB,这将跨越每个服务器实例,导致许多不必要的服务器到服务器的交互。还有,如果这个方法在事务范围内,那么事务边界将包含许多服务器实例,这将严重影响性能。

实际上在运行期,多数的供应商(包括Sun JESWebLogicJboss)都会优化EJB调用机制,使请求首先选择同一个服务器中的EJB容器。这样,如图21所示,我们只在第一层(WEB层)做负载均衡,然后调用相同服务器上的服务。这种结构我们称之为并置结构。技术上说,并置结构是分布式结构的一种特例。

21 并置结构

一个有趣的问题是,既然多数的部署在运行期都演进成了并置结构,为什么不用本地接口代替远程接口,这将大提高性能。你当然可以,但是记住,当你使用本地接口后,WEB组件和EJB耦合得很紧,而方法调用也是直接的而不通过RMI/IIOP。负载均衡和失效转移分发器没有机会介入本地接口调用。“WEB+EJB”整体处理负载均衡和失效转移。

但不幸的是,在集群中使用本地接口在多数J2EE服务器中有局限性。使用本地接口的EJB是本地对象,是不可序列化的,这一个限制就使本地引用不能保存在HTTP Session中。一些产品,如Sun JES,会将本地接口区别看待,使它们可以序列化。这样就可以用在HTTP Session中。

另一个有趣的问题是,既然并置结构这么流行并且有好的性能,为什么还要分布式结构呢?这在多数情况下是有道理的,但有时分布式结构是不可替代的。

l EJB不仅被WEB容器使用,富客户端也会使用它。

l EJB组件和WEB组件需在不同的安全级别上,并需要物理分离。这样防火墙将被设置用于保护运行EJB的重要机器。

l WEB层和EJB层极端不对称使得分布式结构是更好的选择。比如,一些EJB组件非常复杂并且很消耗资源,它们只能运行在昂贵的大型服务器上,另一方面,WEB组件(HTMLJSPServlet)简单得只需廉价的PC服务器就能满足要求。在这种情况下,专门的WEB服务器可以用来接受客户端连接请求,很快处理静态数据(HTML和图像)和简单的WEB组件(JSPServlet)。大型服务器只被用来做复杂计算。这将更好的利用投资。

结论

集群与独立环境不同,J2EE供应商采用不同的方法来实现集群。如果你的项目为做到高伸缩性而使用集群,你应该在你的项目开始的时候就做准备。选择符合你的需求的正确的J2EE产品。选择正确的第三方软件和框架并确保它们能支持集群。最后设计正确的架构使得能从集群中受益而不是受害。


AppendixA: Classical cluster topology

The image “http://www.javaworld.com/jw-02-2001/images/jw-0223-extremescale7.gif” cannot be displayed, because it contains errors.


=================================================

ADDITIONAL INFORMATION

=================================================

垂直伸缩(scale vertically),应用部署在单个节点上,随着系统用户数的增多,可以增加系统的硬件资源,比如增加cpy,增加内存,增加带宽等。

水平伸缩(scale horizontally),应用可以部署在多个节点上,随着系统用户的增多,可以增加更多的处理节点,比如从一台节点增加到三台节点。 水平伸缩方式需要在每个节点上部署相同的应用,但是这种伸缩方式更加便宜,同时也提高了系统的可用性。

参考资料:http://www.theserverside.com/tt/articles/article.tss?l=ScalingYourJavaEEApplications