构建更有效的WEB应用
在这篇文章中,作者Edward Salatovka介绍了一种他称为Train的设计架构。Train允许简化组合多用户请求为一次数据库或网络查询,因此改善应用性能和减少硬件需求。这种方式的好处已经通过类似实际应用的压力测试来验证。
想像一下铁路站点以这样一种方式操作,每一个买了票的乘客立刻得到一个专属于他的列车!这种操作方式在实际生活中是可笑的,但在WEB应用服务器和数据应该访问应用却是很常见的。常规的方式意味着每一个用户请求会得到一个自己的线程和数据库连接。每一个用户请求需要一个即时的数据库或网络资源交互。显然这儿应该使有一种聪明地处理外部通讯的方式而不是增加更多的硬件。让我们寻找一种简单但还未注意到的方式来增加应用的效率。【文章来自:开店乐KaiDianLe.Com】
在这篇文章中,我们采用一种新的方法即每一次与数据库或网络资源交互不是基于每一个请求而多个请求,这样高并发性的负效应如超时和死锁就会大大减少,而且大访问量下的性能衰退也可以忽略不计了。
为了使用这种方式-我们必须创建一个相应的运行应用和执行理论测试的环境。
创建一个黑盒
为了显示这种架构的优越性,我们会构建两个简单的功能上相同的servlet。两者都下发相同的包含从示例数据库获取数据的HTML页面。每一个servlet代表一种不同的实现—常规的和新式的。为了构建我们的servlet,我们需要一个WEB应用服务器,一个数据库和一个压力测试器。你可以自己选择相应的软件。这儿我使用的是Tomcat 4, JMeter和DB2。Tomcat 4和JMeter是免费的开源应用。选择DB2是为了尽可能模拟商业WEB环境。
创建数据库并添加随机内容
假设数据库名为“trdata”,我们来创建必要的方案:
//schema.sql
connect to trdata;
create table trentry (ID integer not null , NAME char(25), DESCR varchar(128), views integer with default 0, constraint p_trentry primary key (ID));
简单的JAVA应用PropSamples生成包含250000行随机数据的表trentry。
//PropSamples.java
ackage train;
import java.util.*;
import java.sql.*;
ublic class PropSamples {
public static String GetString(int size, Random rand) {
StringBuffer strBuff = new StringBuffer();
for (int i = 0; i < size; i++) {
char b = (char) (rand.nextInt(25) + 65);
strBuff.append(b);
}
return strBuff.toString();
}
public void Process() throws SQLException {
String sqlString = "insert into trentry values(?,?,?,?)";
Connection connection = Util.getDBConnection();
PreparedStatement stmt = connection.prepareStatement(sqlString);
Random rand = new Random();
for (int i = 1; i <= 250000; i++) {
stmt.clearParameters();
stmt.setInt(1, i);
stmt.setString(2, GetString(25, rand));
stmt.setString(3, GetString(128, rand));
stmt.setInt(4, 0);
stmt.execute();
if (i%1000 == 0) {
connection.commit();
System.out.println(i + " rows committed");
}
}
}
public static void main(String[] args) throws SQLException{
PropSamples propSamples = new PropSamples();
propSamples.Process();
}
}
这些代码是用来生成数据。Util类(可以从资源中下载的源程序中获得)包含DB2相关的属性,你可以根据自己的环境而改变。
创建常规的servlet
这儿servlet是最方便的:每一个用户请求由一个独立的轻量级线程来服务,数据库连接可以从连接池中获取,清晰地分离了模型层和视图层等等。下面看一下常规的ClassicServlet:
// ClassicServlet.java
ackage train;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;
import java.sql.*;
public class ClassicServlet
extends HttpServlet {
int mEntryLength = 250000;
Random mRand;
public void init() throws ServletException {
mRand = new Random(System.currentTimeMillis());
}
public void doGet(HttpServletRequest request, HttpServletResponse response) throws
ServletException, IOException {
PrintWriter out = response.getWriter();
Statement stmt = null;
Connection connection = null;
String name = "";
String descr = "";
int views = 0;
int id = 0;
boolean isError = false;
synchronized (mRand) {
id = mRand.nextInt(mEntryLength);
}
try {
connection = Util.getDBConnection();
stmt = connection.createStatement();
String sqlStr = "Select id, name, descr, views from trentry where id =" + id;
ResultSet rs = stmt.executeQuery(sqlStr);
while (rs.next()) { //retrieves data from from DB
name = rs.getString("NAME");
descr = rs.getString("DESCR");
id = rs.getInt("ID");
views = rs.getInt("VIEWS");
}
Statement stmtViews = connection.createStatement();
String sqlViewStr = "update trentry set views = views+1 where id =" + id;
stmtViews.executeUpdate(sqlViewStr); //updates number of the page views
}
catch (SQLException ex) {isError = true;}
finally {
try {
if (stmt != null) {
stmt.close();
}
if (connection != null) {
connection.commit();
connection.close();
}
}
catch (SQLException ex1) {isError = true;}
}
if (isError) {
out.println("System error");
}
else { //Delivers html page to browser
out.println("");
out.println("
ID: " + id + "
");Name: " + name + "
");Description: " + descr + "
");Views: " + views + "
");}
我发布了这个servlet,在游览器中输http://localhost:8080/train/classicservlet,就可以得到下面的页面:
ID: 178866
Name: XEIRVYPSFTNRXYEWQWSKOOPES
Description: JBGPGKSMDQKVXVPJCXKIMWLEWJABSGBNTOYR
XRKUMDBWOYOCIAKDWGGEBHKIFONGSRBIB
JIHSBNGEYIORKGFOVWYXYXXJKUBBLVBSKOKLF
CHIGRUGROKESIJQFERWJTV
Views: 0
就这些而已,但这已经很好地模拟了电子商务中的常规操作。Servlet从数据库中获取无意义的数据,更新访问者的数目并输出结果。请注意我们生成随机的键值来从trentry表中获取随机行数据。一个好的数据库管理系统通常会缓存数据。因此,第二次的相同的select语句会执行地更快。我们使用随机键的目的就是使我们的数据不被缓存从而使下面的性能测试更精确。
创建常规的servlet
这儿servlet是最方便的:每一个用户请求由一个独立的轻量级线程来服务,数据库连接可以从连接池中获取,清晰地分离了模型层和视图层等等。下面看一下常规的ClassicServlet:
// ClassicServlet.java
ackage train;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;
import java.sql.*;
public class ClassicServlet
extends HttpServlet {
int mEntryLength = 250000;
Random mRand;
public void init() throws ServletException {
mRand = new Random(System.currentTimeMillis());
}
public void doGet(HttpServletRequest request,
HttpServletResponse response) throws
ServletException, IOException {
PrintWriter out = response.getWriter();
Statement stmt = null;
Connection connection = null;
String name = "";
String descr = "";
int views = 0;
int id = 0;
boolean isError = false;
synchronized (mRand) {
id = mRand.nextInt(mEntryLength);
}
try {
connection = Util.getDBConnection();
stmt = connection.createStatement();
String sqlStr = "Select id, name, descr, views from
trentry where id =" + id;
ResultSet rs = stmt.executeQuery(sqlStr);
while (rs.next()) { //retrieves data from from DB
name = rs.getString("NAME");
descr = rs.getString("DESCR");
id = rs.getInt("ID");
views = rs.getInt("VIEWS");
}
Statement stmtViews = connection.createStatement();
String sqlViewStr = "update trentry set views = views+1
where id =" + id;
stmtViews.executeUpdate(sqlViewStr);
//updates number of the page views
}
catch (SQLException ex) {isError = true;}
finally {
try {
if (stmt != null) {
stmt.close();
}
if (connection != null) {
connection.commit();
connection.close();
}
}
catch (SQLException ex1) {isError = true;}
}
if (isError) {
out.println("System error");
}
else { //Delivers html page to browser
out.println("");
out.println("
ID: " + id + "
");Name: " + name + "
");Description: " + descr + "
");Views: " + views + "
");}
我发布了这个servlet,在游览器中输入http://localhost:8080/train/classicservlet,
就可以得到下面的页面:
ID: 178866
Name: XEIRVYPSFTNRXYEWQWSKOOPES
Description: JBGPGKSMDQKVXVPJCXKIMWLEWJABSGBNTOYR
XRKUMDBWOYOCIAKDWGGEBHKIFONGSRBIBJIH
SBNGEYIORKGFOVWYXYXXJKUBBLVBSKOKLFCHIGRUGROKESIJQFERWJTV
Views: 0
就这些而已,但这已经很好地模拟了电子商务中的常规操作。Servlet从数据库中获取无意义的数据,更新访问者的数目并输出结果。请注意我们生成随机的键值来从trentry表中获取随机行数据。一个好的数据库管理系统通常会缓存数据。因此,第二次的相同的select语句会执行地更快。我们使用随机键的目的就是使我们的数据不被缓存从而使下面的性能测试更精确。
行胜于言
图2中的时序图及步骤列表解释了这个设计的原理。
Figure 2. Sequence diagram of the Train pattern. Click on thumbnail to view full-sized image.
· TrainServlet实例化Job对象,发送Job对象到Dispatcher,然后暂停
· Dispatcher组合Job到批处理中。
· 对每一个批处理,Dispatche创建一个Worker类的实例。
· 在给定时间或批处理中包含一定数量的Job后,Worker生成SQL语句并与数据库交互。
· 每一个SQL语句执行批处理中与所有Job相关的任务。
· Worker中断TrainSevlet线程
· TrainSevlet下发结果给用户。
现在我们打算解剖一下这种设计的实际实现。
我们来看一下下面的servlet:
//TrainServlet.java
ackage train;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;
ublic class TrainServlet extends HttpServlet {
int mEntryLength=250000;
Dispatcher mDispatcher;
Random mRand;
public void init() throws ServletException {
mRand = new Random(System.currentTimeMillis());
mDispatcher = new Dispatcher();
Thread ht = new Thread(mDispatcher); //Instantiate and execute in the separate thread.
ht.start();
}
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
int id=0;
synchronized(mRand){
id=mRand.nextInt(mEntryLength);
}
Job job = new Job(String.valueOf(id)); //Each concurrent request creates job instance.
job.mJobThread = Thread.currentThread(); //Job should know the thread of the request.
PrintWriter out = response.getWriter(); //Job should know the output stream of the browser.
job.mOut = out;
mDispatcher.AddJob(job); //Job is sent to the dispatcher.
//Dispatcher is a container for all concurrent jobs.
try {
Thread.sleep(100000); //Let's wait until database interaction is finished.
System.out.println("Error: Request is timed out"); //Too bad. 100 seconds was not enough.
}
catch (InterruptedException ex2) {
//Success! Members of the Job instances are populated.
}
job.Marshall(); // Let's display the page in the browser.
}
}
新的servlet类似ClassicServlet,但有一些不同:用户请求被包含在Job类的实例中,与数据库的交互由Dispatcher类的实例来处理。
Job类显示如下:
//Job.java
ackage train;
import java.io.*;
ublic class Job {
String mName;
String mDescr;
int mViews;
String mID;
PrintWriter mOut;
Thread mJobThread;
boolean mHasFailed = false;
//Sorry, no getters and setters to save space
public Job(String id) {
mID = id;
}
public void Marshall(){ // displays html page
mOut.println("");
if(mHasFailed){
mOut.println("System error");
}else{
mOut.println("
ID: "+mID+"
");
mOut.println("
Name: "+mName+"
");
mOut.println("
Description: "+mDescr+"
");
mOut.println("
Views: "+mViews+"
");
}
mOut.println("");
}
}
Job是一个纯粹的用户请求的代表。
marshall()方法在浏览器中显示信息。mJobThread成员是一个由WEB应用服务器创建执行请求的线程。
Dispatcher类是最重要的但也很简单:
//Dispatcher.java
ackage train;
import java.util.*;
ublic class Dispatcher
implements Runnable {
private List mCurrentJobBatch = new ArrayList(); //Batch container
private int mJobBatchMaxSize = 5; //Maximum number of the jobs in the batch
private int mIntervalTime = 50; //Maximum time to wait before batch execution
public synchronized void AddJob(Job job) {
mCurrentJobBatch.add(job);
if (mCurrentJobBatch.size() == mJobBatchMaxSize) {
ProcessJobBatch(); //If batch is full, execute
}
}
private synchronized void ProcessJobBatch() {
if (mCurrentJobBatch.size() == 0) { return; }
Worker worker = new Worker(mCurrentJobBatch);
Thread ht = new Thread(worker);
ht.start();
mCurrentJobBatch = new ArrayList();
}
public void run() {
try {
while (true) {
Thread.sleep(mIntervalTime);
ProcessJobBatch(); // Each mIntervalTime milliseconds execute batch
}
}
catch (InterruptedException ex) { }
}
}
Dispatcher生存在一个独立的线程中并且是所有并发Job的容器。每一批处理在两种条件下会执行,不考虑先后:
1、 自上一次这种条件下执行后mIntervalTime毫秒并且至少有一个Job在批次中。
2、 批次的大小达到mJobBatchMaxSize
如果mJobBatchMaxSize值设为1,TrainServlet就退化为ClassicServlet。这是一个动态批处理的重要标识,这会在稍后解释。
方案的最后部分是Worker类:
//Worker.java
ackage train;
import java.util.*;
import java.sql.*;
public class Worker
implements Runnable {
List mJobs;
Map mJobMap; //Helper member for mapping the jobs
protected Worker(List jobs) {
mJobs = jobs;
mJobMap = new HashMap();
}
private void Process() {
boolean isError = false;
StringBuffer sqlBuff = new StringBuffer(
"Select id,name ,descr,views from trentry where id in ");
StringBuffer whereClause = CreateWhereClause(); //
sqlBuff.append(whereClause);
/* Now SQL statement is fully formed and looks like:
Select id,name ,descr,views from trentry where id in (3343,22222,5555).
This will allow us to fetch several user requests in one shot */
Connection connection = null;
Statement stmt = null;
try {
connection = Util.getDBConnection();
stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(sqlBuff.toString());
System.out.println(sqlBuff.toString());
Map result = new HashMap();
while (rs.next()) {
int id = rs.getInt("ID");
String name = rs.getString("NAME");
String descr = rs.getString("DESCR");
int views = rs.getInt("VIEWS");
Job job = (Job) mJobMap.get(String.valueOf(id));
//Populate instance of the Job with data retrieved from database
job.mName = name;
job.mDescr = descr;
job.mViews = views;
}
String sqlViewStr = "update trentry set views = views+1 where id in " +
whereClause;
// The same trick for Update statement
stmt.executeUpdate(sqlViewStr);
}
catch (SQLException ex) {
isError = true;
}
finally {
try {
if (stmt != null) {
stmt.close();
}
if (connection != null) {
if (isError) {
connection.rollback();
}
else {
connection.commit();
}
connection.close();
}
}
catch (SQLException ex1) { isError = true; }
}
FinishJobs(isError);
return;
}
private StringBuffer CreateWhereClause() {
StringBuffer clause = new StringBuffer("(");
for (int i = 0; i < mJobs.size(); i++) {
Job job = (Job) mJobs.get(i);
String id = job.mID;
if (i != 0) {
clause.append(",");
}
clause.append(id);
mJobMap.put(id, job);
}
clause.append(")");
return clause;
}
private void FinishJobs(boolean isError) {
for (int i = 0; i < mJobs.size(); i++) {
Job job = (Job) mJobs.get(i);
if (isError) {
job.mHasFailed=true; //Rudimentary error handling
}
job.mJobThread.interrupt();
/* Wake up the TrainServlet to deliver the page to the browser */
}
}
public void run() {
Process();
}
}
Worker类创建SQL语句,为一次与数据库的交互来组合Job,用来自数据库的信息生成Job实例并且唤醒Job线程。
Train servlet性能
我使用相同的JMeter配置来测试TrainServlet。是否我们在浪费时间来开发Train模式呢?当然不是,就如图3显示的那样。
Figure 3. TrainServlet performance. Click on thumbnail to view full-sized image.
我们提高了15倍的效率!结果可能与硬件/软件/特定的实现有关,但显然效率的提高是肯定的。
这个图描述了静态的50个用户并发访问的模拟环境。但更实际和有意义的数据可以通过动态回归分析来取得。对这种类型的分析,我模拟了不同的并发用户数据并且测量两种servlet对每个请求的性能。
图4显示了测量结果。
Figure 4. Comparative dynamic performance. Click on thumbnail to view full-sized image.
结果有点难心置信。相比ClassicServlet,TrainServlet在大量外部访问时并没有出现性能衰退。当然在小于3个并发用户时,ClassicServlet的响应更快一些。我称这种现象为低访问量惩罚,这出现在只有一个用户请求时。在这种情况下,我们需要等到下一次计划时间到达时才能执行,也就是说在只有一个请求时批处理并不能提供什么好处。
最后的探讨
这儿描述的这种方案因为简洁的原因写得相当简单,有一些地方明显值得改善。
前瞻性的访问量分析和动态批处理
在这里我们设置mIntervalTime的值为50毫秒。假设访问量很小,平均100毫秒才有一个新的请求,那么我们将会延迟每个请求将近25毫秒。我们如何才能以免这种低访问量惩罚呢?解决方案来自基于前瞻性访问量分析的动态批处理。换句话说,我们根据前一次会话中的用户请求数来可以改变批处理中的最工作数据。最简单的算法如下:
1、 记下最近50毫秒内服务的请求数量。
2、 如果数据小于5,则设置mJobBatchMaxSize为1否则为5。
如我上面讨论的,当mJobBatchMaxSize为1时,TrainServlet就与ClassicServle一样了。
多个命令的分配器
标准WEB应用服务器处理多个命令而不是一个,实际应用中Dispatcher和Worker应该能为所有的服务。每一个命令应该有自己的Worker类,Dispatcher应该作为一个单例来实现。然而,这儿两种可能的Dispatcher设计方式:
1、 每一个命令有一个自己的Dispatcher类。如果特定时间后,没有请求需要服务则Dispatcher线程就退出,因为我们不需要很多空闭的线程来消耗系统资源。因此对比在serverlet的init()方法中实例化Dispatcher,我们更倾向于调用:Dispatcher.getIntance().AddJob(job)。
2、 仅有一个类来管理来自不同命令的不同批处理并且通过命令名将他们与相关的工作者相关联。这时候,调用看起来更像:Dispatcher.getInstance().AddJob(this.getClass().getName(),job).
作为JDBC代理的Train模式
从实际的观点来看,想从运行中的标准应用或者放弃传统的想法来重新构建应用是困难的。那么如果构建某种JDBC代理使得来自应用的JDBC调用基本一致时会怎么样呢?可以用EJB作为包装器来处理。这种方式是有效的,因为他可以处理任何如并发用户访问网络资源或数据库的应用而不只是服务于WEB应用。但是这已经是另一个主题了。
小结
在这篇文章中,我们设计了一种新的称为Train的方式来开发有效的WEB应用。我们基于Train模式构建了一个实际的应用并且证明他可以在压力测试提供100%的性能改善。Train模式的使用并不限于WEB应用,通过减少软硬件的需求,他可以为任何处理并发用户访问网络资源或数据库的应用带来好处。
在这里特别感谢我的朋友们Mark Jackson, Sander Berents, Tom Griffin, and Eric Van Stegeren对我的支持和帮助。
【日期:2006-8-8】【作者:不祥】【转载自:开店乐】
相关文章:
最好的网上开店系统:凡人网络购物系统免费下载
Windows XP超强支持工具集全面接触
教你如何申请Windows Vista Beta 2的CD-KEY
15条Windows 2003操作技巧
将系统中的“病毒”Thumbs.db打尽
家庭版WinXP也能设置用户权限
无须动手 自动重命名MP3歌曲
Windows Live全新界面绚丽曝光
把Windows Vista的华丽外衣借给XP
从零讲起!走出注册表应用的迷雾
让Windows系统自动修复受损文件
将应用程序巧妙改变成“系统服务”
安装微软Vista Beta 2重要提示
XP跳过登录界面实现自动登录
让Windows系统自动删除临时文件
万事不求人 三招清除IE顽固病毒
Windows操作系统快速关机之谜
局域网维护和优化小技巧
日志中的秘密:Windows登录类型知多少?
可随身携带的迷你型系统工具-K1
破解还原精灵的几个小技巧
XP中分区或文件夹无法双击打开的处理方法
隐藏部分文件扩展名的技巧
打造可随意安装的完美Windows XP镜像
XP能直接运行 抢先体验Vista屏保程序
玩转Windows多重启动的重要文件
常用文件夹 出现在最需要的地方
菜鸟安全加倍:精通防火墙问与答
抛弃XP!细数升级Vista的五大理由
顺藤摸瓜-让疯狂的盗号者不再得逞
WinXP超强功能 我的文档也有秘密
Windows XP SP2系统网上邻居故障解决
进阶:按需定制你的系统还原
安全宝典:打造一个黑客也读不懂的密码
六种设置方法彻底优化你的IE浏览器
Win XP远程桌面控制中的“雕虫小技”
五年之痒 WindowsXP组件“改换门庭”
看清楚!买本本要注意的35个问题
“猫狗”合作为系统保驾护航
Windows批量升级补丁方法
7步删除Vista快捷方式
忘记Windows XP登入密码的9种解决办法
2000/XP/2003操作系统常见问题集锦
图解Windows Vista系统完整备份功能
你的电脑能很好地运行Vista吗?
浅谈Windows多重启动的重要文件
快速恢复XP管理员密码三大技巧
节省资源 Vista无缘 Glass风格
破解Windows XP组策略的锁死难题
恶意代码自己破解!两则攻击伎俩细剖析
全新风格 Vista RC1版新界面曝光!
微软推免费文件夹加密软件
还硬盘待机时的正常“休息”时间
快速重装Windows操作系统的众多要点
番茄花园 WinXP安装盘集成技术分析
如何百毒不侵?笔记本最新防意外手册
无线网攻击工具进攻方法及防范技巧
屏蔽WinXP中不需用到的功能
日常应用测试:Vista不输XP!
自我防护:给IE和系统找张超级“护身符”
绝对强悍 让Win XP自动维护系统
遭遇Word文档病毒的解决办法
控制面板中的选项也玩开始菜单
Win2000小心 持续木马攻击来袭
经验之谈:带张光盘去装机
XP空间 挖掘“任务计划”的另类功能
Win XP系统重新启动的另类方法
浅析WinXP“单击锁定”功能
Windows XP文件属性玩花样
解决Windows开关机没声音
Vista实用功能:网络地图
Vista趣图:Win 3.1“再现”
解析Windows操作系统两大进程
修复多系统启动菜单
解决Windows系统图标显示不全的问题
教你一招 30秒清除Windows系统所有垃圾
解析Vista与系统不协调的三大工具界面
Vista新技术介绍之磁盘碎片自动整理
Windows系统故障简单恢复技巧
安全基础:教你如何识别病毒现象
找回失落的加密文件!数据恢复软件有奇效
为Linux系统加装 ”防盗门”
Linux下用fstab配置文件系统
各类unix和linux密码丢失解决方法
Linux防火墙上的Apache反向代理
使用LINUX搭建局域网
Ubuntu Linux:Apache安装设置
Linux网络的IPv6应用
Linux系统防火墙防止DOS攻击
GRUB引导下进Linux单用户模式的三种方式
Linux系统下由论坛到SSH的入侵分析
Linux操作系统中x86的内联汇编方法
将 Linux 应用程序移植到 64 位系统上
通过Linux工具来全面保障无线网络安全
【转帖】Linux对I/O端口资源的管理 (1)
【转帖】Linux对I/O端口资源的管理 (2)
Linux系统套接字编程中存在的五个隐患 (1)
Linux系统套接字编程中存在的五个隐患 (2)
Linux系统套接字编程中存在的五个隐患 (3)
如何在Linux下配置Java开发环境详述
只需十分钟 Linux环境下快速搭建维基站(1)
版权所有:Kaidianle.Com 联系方式:Shnxn@Yhaoo.Com.Cn 京ICP备06028743号 在线留言