原文地址:http://www.cnblogs.com/yizhu2000/archive/2007/12/04/982142.html
本人作为一位web工程师,着眼最多之处莫过于 性能与架构,本次幸得参与sd2.0大会,得以与同行广泛交流,于此二方面,有些心得,不敢独享,与众博友分享,本文是这次参会与众同撩交流的心得,有兴趣者可以查看视频
架构设计的几个心得:
一,不要过设计:never over design
这是一个常常被提及的话题,但是只要想想你的架构里有多少功能是根本没有用到,或者最后废弃的,就能明白其重要性了,初涉架构设计,往往倾向于设计大而化一的架构,希望设计出具有无比扩展性,能适应一切需求的增加架构,web开发领域是个非常动态的过程,我们很难预测下个星期的变化,而又需要对变化做出最快最有效的响应。。
ebay的工程师说过,他们的架构设计从来都不能满足系统的增长,所以他们的系统永远都在推翻重做。请注意,不是ebay架构师的能力有问题,他们设计的架构总是建立旧版本的瓶颈上,希望通过新的架构带来突破,然而新架构带来的突破总是在很短的时间内就被新增需求淹没,于是他们不得不又使用新的架构
web开发,是个非常敏捷的过程,变化随时都在产生,用户需求千变万化,许多方面偶然性非常高,较之软件开发,希望用一个架构规划以后的所有设计,是不现实的
二,web架构生命周期:web architecture‘s life cycle
既然要杜绝过设计,又要保证一定的前瞻性,那么怎么才能找到其中的平衡呢?希望下面的web架构生命周期能够帮到你
所设计的架构需要在1-10倍的增长下,通过简单的增加硬件容量就能够胜任,而在5-10倍的增长期间,请着手下一个版本的架构设计,使之能承受下一个10倍间的增长
google之所以能够称霸,不完全是因为搜索技术和排序技术有多先进,其实包括baidu和yahoo,所使用的技术现在也已经大同小异,然而,google能在一个月内通过增加上万台服务器来达到足够系统容量的能力确是很难被复制的
三,缓存:Cache
空间换取时间,缓存永远计算机设计的重中之重,从cpu到io,到处都可以看到缓存的身影,web架构设计重,缓存设计必不可少,关于怎样设计合理的缓存,jbosscache的创始人,淘宝的创始人是这样说的:其实设计web缓存和企业级缓存是非常不同的,企业级缓存偏重于逻辑,而web缓存,简单快速为好。。
缓存带来的问题是什么?是程序的复杂度上升,因为数据散布在多个进程,所以同步就是一个麻烦的问题,加上集群,复杂度会进一步提高,在实际运用中,采用怎样的同步策略常常需要和业务绑定
老钱为搜狐设计的帖子设计了链表缓存,这样既可以满足灵活插入的需要,又能够快速阅读,而其他一些大型社区也经常采用类此的结构来优化帖子列表,memcache也是一个常常用到的工具
钱宏武谈架构设计视频 http://211.100.26.82/CSDN_Live/140/qhw.flv
Cache的常用的策略是:让数据在内存中,而不是在比较耗时的磁盘上。从这个角度讲,mysql提供的heap引擎(存储方式)也是一个值得思考的方法,这种存储方法可以把数据存储在内存中,并且保留sql强大的查询能力,是不是一举两得呢?
我们这里只说到了读缓存,其实还有一种写缓存,在以内容为主的社区里比较少用到,因为这样的社区最主要需要解决的问题是读问题,但是在处理能力低于请求能力时,或者单个希望请求先被缓存形成块,然后批量处理时,写缓存就出现了,在交互性很强的社区设计里我们很容易找到这样的缓存
四,核心模块一定要自己开发:DIY your core module
这点我们是深有体会,钱宏武和云风也都有谈到,我们经常倾向于使用一些开源模块,如果不涉及核心模块,确实是可以的,如果涉及,那么就要小心了,因为当访问量达到一定的程度,这些模块往往都有这样那样的问题,当然我们可以把问题归结为对开源的模块不熟悉,但是不管怎样,核心出现问题的时候,不能完全掌握其代码是非常可怕的
五,合理选择数据存储方式:reasonable data storage
我们一定要使用数据库吗,不一定,雷鸣告诉我们搜索不一定需要数据库,云风告诉我们,游戏不一定需要数据库,那么什么时候我们才需要数据库呢,为什么不干脆用文件来代替他呢?
首先我们需要先承认,数据库也是对文件进行操作。我们需要数据库,主要是使用下面这几个功能,一个是数据存储,一个是数据检索,在关系数据库中,我们其实非常在乎数据库的复杂搜索的能力,看看一个统计用的tsql就知道了(不用仔细读,扫一眼就可以了)
select c.Class_name,d.Class_name_2,a.Creativity_Title,b.User_name,(select count(Id) from review where Reviewid=a.Id) as countNum from Creativity as a,User_info as b,class as c,class2 as d where a.user_id=b.id and a.Creativity_Class=c.Id and a.Creativity_Class_2=d.Id
select a.Id,max(c.Class_name),(max(d.Class_name_2),max(a.Creativity_Title),max(b.User_name),count(e.Id) as countNum from Creativity as a,User_info as b,class as c,class2 as d,review as e where a.user_id=b.id and a.Creativity_Class=c.Id and a.Creativity_Class_2=d.Id and a.Id=e.Reviewid group by a.Id ..............................................
我们可以看出需要数据库关联,排序的能力,这个能力在某些情况下非常重要,但是如果你的网站的常规操作,全是这样复杂的逻辑,那效率一定是非常低的,所以我们常常在数据库里加入许多冗余字段,来减小简单查询时关联等操作带来的压力,我们看看下面这张图,可以看到数据库的设计重心,和网站(指内容型社区)需要面对的问题实际是有一些偏差的
同样其他一些软件产品也遇到同样的问题所以具我了解,有许多特殊的运用都有自己设计的特殊数据存储结构与方法,比如有的大型服务程序采取树形数据存储结构,lucene使用文件来存储索引和文件
从另外一个角度上看,使用数据库,意味着数据和表现是完全分离的(这当然是经典的设计思路),也就是说当需要展示数据时,不得不需要一个转换的过程,也可以说是绑定的过程,当网站具备一定规模的时候,数据库往往成为效率的瓶颈,所以许多网站也采用直接书写静态文件的方法来避免读取操作时的绑定
这并不是说我们从今天起就可以把我们亲爱的数据库打入冷宫,而是我们在设计数据的持久化时,需要根据实际情况来选择存储方式,而数据库不过是其中一个选项
六,搞清楚谁是最重要的人:who's the most important guy
在用例需求分析的时候常常讲到涉众,就是和你的设计息息相关的人,在web中我们一定以为最重要的涉众莫过于用户了。,在一个传统的互动社区开发中,最重要的东西是内容,用户产生内容,所以用户就是上帝,至于内容挑选工具,不就是给坐我后面三排的妹妹们用的吗?凑或行了,实在有问题我就在数据里手动帮你加得了。。这大概是眼下许多小型甚至中型网站技术人员的普遍想法。钱宏武在他的讲座里谈到了这个问题:实际上网站每天产生的内容非常的多,普通人是不可能看完的,而编辑负责把精华的内容推荐到首页上,所以很多用户读到的内容其实都依赖于编辑的推荐,所以设计让编辑工作方便的工具也是非常重要,有时甚至是最重要的。
七,不要执着于文档:don't be crazy about document
web开发的文档重要吗?什么文档最重要?我的看法是web开发中交流>文档,
现在大的软件公司比较流行的做法是:
注重产品设计文档,在这种方法里,产品文档非常详尽,并且没有歧义,开发人员基于设计文档开发,测试人员基于设计文档制定测试方案,任何新人都可以通过阅读产品设计文档来了解项目的概况
而web项目从概念到实现的时间是非常短的,而且越短越好,并且由于变化迅速,要想写出完整的产品和需求文档是几乎不可能的,大多数情况是等你写出完备的文档,项目早就是另外一个样子,但是没有文档的问题是,如果团队发生变化,添加新成员怎样才能了解软件的结构和概念呢,一种是每个人都了解软件的整个结构,除非你的团队整体消失,否则任何一个人都能够担当培养新人的责任,这种face2face交流比文档有效率很多。
于是就有了前office开发者,现任yahoo中国某产品开发负责人的刘振飞所感觉到的落差,他说,我们的项目是吵出来的,我听完会心一笑
八,团队:team
不要专家团队,而要外科手术式的团队,你的团队里一定要有清道夫,需要有弓箭手,让他们和项目一起成长,才是项目负责人的最大成就
总结:
0)架构是一种权衡
1)web开发的特点是是:没有太复杂的技术难点,一切在于迅速的把握需求,其实这正式敏捷开发的要旨所在,一切都可以非常快速的建立,非常快速的重构,我们的开发工具,底层库和框架,包括搜索引擎和web文档提供的帮助,都提我们供给了敏捷的能力。
2)此外,相应的,最有效率的交流方式必须留给web开发,那就是face2face(面对面),不要太担心你的设计不能被完备的文档所保留下来,他们会以交流,代码和小卡片的方式保存下来
3)人的因素会更加重要,无论是对用户的需求,还是开发人员的素质。
另:有关web效率,有著名的14条规则,由yahoo性能效率小组所总结,并广为流传。业已出现相关插件(YSlow),针对具体网页按彼规则评分,这次该小组负责人Tenni Theurer也受邀来到此次大会,我把Tenni小姐(之前真的没有想到她是个女孩,并且如此年轻)和她的团队的14 rules列在下面
Make Fewer HTTP Requests
Use a Content Delivery Network
Add an Expires Header
Gzip Components
Put CSS at the Top
Move Scripts to the Bottom
Avoid CSS Expressions
Make JavaScript and CSS External
Reduce DNS Lookups
Minify JavaScript
Avoid Redirects
Remove Duplicate Scripts
Configure ETags
Make Ajax Cacheable
通过安装firebug和YSlow这两个firefox插件(请注意要先安装firebug再安装yslow,下载后拖动到firefox里即可)我们可以看到你的网页根据下面的规则的评分,这是我在博客园博客首页的评分截图,上面D表示总分,下面是单项评分,A最好F最差,不知道还有没有G :)
相关连接
yahoo性能团队:http://developer.yahoo.com/performance/
转发这篇文章主要是原作者BLOG的主题的字体太小,看得不太舒服~~
对于一个真正的企业级的应用来说,Caching肯定是一个不得不考虑的因素,合理、有效地利用Caching对于 增强应用的Performance(减少对基于Persistent storage的IO操作)、Scalability(将数据进行缓存,减轻了对Database等资源的压力)和Availability(将数据进行 缓存,可以应对一定时间内的网络问题、Web Service不可访问问题、Database的崩溃问题等等)。Enterprise Library的Caching Application Block为我们提供了一个易用的、可扩展的实现Caching的框架。借助于Caching Application Block,Administrator和Developer很容易实现基于Caching的管理和编程。由于Caching的本质在于将相对稳定的数据 常驻内存,以避免对Persistent storage的IO操作的IO操作,所以有两个棘手的问题:Load Balance问题;Persistent storage和内存中数据同步的问题。本篇文章提供了一个解决方案通过SqlDependency实现SQL Server中的数据和Cache同步的问题。
一、Cache Item的过期策略
在 默认的情况下,通过CAB(以下对Caching Application Block的简称,注意不是Composite UI Application Block )的CacheManager加入的cache item是永不过期的;但是CacheManager允许你在添加cache item的时候通过一个ICacheItemExpiration对象应用不同的过期策略。CAB定了一个以下一些class实现了 ICacheItemExpiration,以提供不同的过期方式:
- AbsoluteTime:为cache item设置一个cache item的绝对过期时间。
- ExtendedFormatTime:通过一个表达式实现这样的过期策略:每分钟过期(*****:5个*分别代表minute、hour、date、month、year);每个小时的第5分钟过期(5****);每个月的2号零点零分过期(0 0 2 * *)。
- FileDependency:将cache item和一个file进行绑定,通过检测file的最后更新时间确定file自cache item被添加以来是否进行过更新。如果file已经更新则cache item过期。
- NeverExpired:永不过期。
- SlidingTime:一个滑动的时间,cache item的每次获取都将生命周期延长到设定的时间端,当cache item最后一次获取的时间算起,超出设定的时间,则cache item过期。
对 于过期的cache item,会及时地被清理。所以要实现我们开篇提出的要求:实现Sql Server中的数据和Cache中的数据实现同步,我们可以通过创建基于Sql Server数据变化的cache item的过期策略。换句话说,和FileDependency,当Persistent storage(Database)的数据变化本检测到之后,对于得cache自动过期。但是,对于文件的修改和删除,我们和容易通过文件的最后更新日期 或者是否存在来确定。对于Database中Table数据的变化的探测就不是那么简单了。不过SQL Server提供了一个SqlDependency的组建帮助我们很容易地实现了这样的功能。
二、创建基于SqlDependency的ICacheItemExpiration
SqlDependency 是建立在SQL Server 2005的Service Broker之上。SqlDependency向SQL Server订阅一个Query Notification。当SQL Server检测到基于该Query的数据发生变化,向SqlDependency发送一个Notification,并触发SqlDependency 的Changed事件,我们就可以通过改事件判断对应的cache item是否应该过期。
我们现在就来创建这样的一个ICacheItemExpiration。我们先看看ICacheItemExpiration的的定义:
public interface ICacheItemExpiration
{
// Methods
bool HasExpired();
void Initialize(CacheItem owningCacheItem);
void Notify();
}
而 判断过期的依据就是根据HasExpired方法,我们自定义的CacheItemExpiration就是实现了该方法,根据 SqlDependency判断cache item是否过期。下面是SqlDependencyExpiration的定义(注:SqlDependencyExpiration的实现通过 Enterprise Library DAAB实现DA操作):
namespace Artech.SqlDependencyCaching
{
public class SqlDependencyExpiration : ICacheItemExpiration
{
private static readonly CommandType DefaultComamndType = CommandType.StoredProcedure;
public event EventHandler Expired;
public bool HasChanged
{ get; set; }
public string ConnectionName
{ get; set; }
public SqlDependencyExpiration(string commandText, IDictionary<string, object> parameters) :
this(commandText, DefaultComamndType, string.Empty, parameters)
{ }
public SqlDependencyExpiration(string commandText, string connectionStringName, IDictionary<string, object> parameters) :
this(commandText, DefaultComamndType, connectionStringName, parameters)
{ }
public SqlDependencyExpiration(string commandText, CommandType commandType, IDictionary<string, object> parameters) :
this(commandText, commandType, string.Empty, parameters)
{ }
public SqlDependencyExpiration(string commandText, CommandType commandType, string connectionStringName, IDictionary<string, object> parameters)
{
if (string.IsNullOrEmpty(connectionStringName))
{
this.ConnectionName = DatabaseSettings.GetDatabaseSettings(ConfigurationSourceFactory.Create()).DefaultDatabase;
}
else
{
this.ConnectionName = connectionStringName;
}
SqlDependency.Start(ConfigurationManager.ConnectionStrings[this.ConnectionName].ConnectionString);
using (SqlConnection sqlConnection = DatabaseFactory.CreateDatabase(this.ConnectionName).CreateConnection() as SqlConnection)
{
SqlCommand command = new SqlCommand(commandText, sqlConnection);
command.CommandType = commandType;
if (parameters != null)
{
this.AddParameters(command, parameters);
}
SqlDependency dependency = new SqlDependency(command);
dependency.OnChange += delegate
{
this.HasChanged = true;
if (this.Expired != null)
{
this.Expired(this, new EventArgs());
}
};
if (sqlConnection.State != ConnectionState.Open)
{
sqlConnection.Open();
}
command.ExecuteNonQuery();
}
}
private void AddParameters(SqlCommand command, IDictionary<string, object> parameters)
{
command.Parameters.Clear();
foreach (var parameter in parameters)
{
string parameterName = parameter.Key;
if (!parameter.Key.StartsWith("@"))
{
parameterName = "@" + parameterName;
}
command.Parameters.Add(new SqlParameter(parameterName, parameter.Value));
}
}
#region ICacheItemExpiration Members
public bool HasExpired()
{
bool indicator = this.HasChanged;
this.HasChanged = false;
return indicator;
}
public void Initialize(CacheItem owningCacheItem)
{ }
public void Notify()
{ }
#endregion
}
}
我们来简单分析一下实现过程,先看看Property定义:
private static readonly CommandType DefaultComamndType = CommandType.StoredProcedure;
public event EventHandler Expired;
public bool HasChanged
{ get; set; }
public string ConnectionName
{ get; set; }
通 过DefaultComamndType 定义了默认的CommandType,在这了我默认使用Stored Procedure;Expired event将在cache item过期时触发;HasChanged代表Database的数据是否被更新,将作为cache过期的依据;ConnectionName代表的是 Connection string的名称。
为了使用上的方便,我定义了4个重载的构造函 数,最后的实现定义在public SqlDependencyExpiration(string commandText, CommandType commandType, string connectionStringName, IDictionary<string, object> parameters)。parameters代表commandText的参数列表,key为参数名称,value为参数的值。首先获得真正的 connection string name(如果参数connectionStringName为空,就使用DAAB默认的connection string)
if (string.IsNullOrEmpty(connectionStringName))
{
this.ConnectionName = DatabaseSettings.GetDatabaseSettings(ConfigurationSourceFactory.Create()).DefaultDatabase;
}
else
{
this.ConnectionName = connectionStringName;
}
然 后通过调用SqlDependency.Start()方法,并传入connection string作为参数。该方法将创建一个Listener用于监听connection string代表的database instance发送过来的query notifucation。
SqlDependency.Start(ConfigurationManager.ConnectionStrings[this.ConnectionName].ConnectionString);
然 后创建SqlConnection,并根据CommandText和CommandType参数创建SqlCommand对象,并将参数加入到 command的参数列表中。最后将这个SqlCommand对象作为参数创建SqlDependency 对象,并注册该对象的OnChange 事件(对HasChanged 赋值;并触发Expired事件)。这样当我们执行该Cmmand之后,当基于commandtext的select sql语句获取的数据在database中发生变化(添加、更新和删除),SqlDependency 的OnChange 将会触发
SqlDependency dependency = new SqlDependency(command);
dependency.OnChange += delegate
{
this.HasChanged = true;
if (this.Expired != null)
{
this.Expired(this, new EventArgs());
}
};
这样在HasExpired方法中,就可以根据HasChanged 属性判断cache item是否应该过期了。
public bool HasExpired()
{
bool indicator = this.HasChanged;
this.HasChanged = false;
return indicator;
}
三、如何应用SqlDependencyExpiration
我 们现在创建一个简单的Windows Application来模拟使用我们创建的SqlDependencyExpiration。我们模拟一个简单的场景:假设我们有一个功能需要向系统所 有的user发送通知,而且不同的user,通知是不一样的,由于通知的更新的频率不是很高,我们需要讲某个User的通知进行缓存。
这是我们的表结构:Messages
我们通过下面的SP来获取基于某个User 的Message:
ALTER PROCEDURE [dbo].[Message_Select_By_User]
(@UserID VarChar(50))
AS
BEGIN
Select ID, UserID, [Message] From dbo.Messages Where UserID = @UserID
END
注:如何写成Select
* From dbo.Messages Where UserID = @UserID, SqlDependency 将不能正常运行;同
时Table的schema(dbo)也是必须的。
我 们设计如下的界面来模拟:通过Add按钮,可以为选择的User创建新的Message,而下面的List将显示基于某个User(Foo)的 Message List。该列表的获取方式基于Lazy Loading的方式,如果在Cache中,则直接从Cache中获取,否则从Db中获取,并将获取的数据加入cache。
我们先定义了3个常量,分别表示:缓存message针对的User,获取Message list的stored procedure名称和Cache item的key。
private const string UserName = "Foo";
private const string MessageCachingProcedure = "Message_Select_By_User";
private const string CacheKey = "__MessageOfFoo";
我们通过一个Property来创建或获取我们的上面定义的SqlDependencyExpiration 对象
private SqlDependencyExpiration CacheItemExpiration
{
get
{
IDictionary<string, object> parameters = new Dictionary<string, object>();
parameters.Add("UserID", UserName);
SqlDependencyExpiration expiration= new SqlDependencyExpiration(MessageCachingProcedure, parameters);
expiration.Expired += delegate
{
MessageBox.Show("Cache has expired!");
};
return expiration;
}
}
通过GetMessageByUser从数据库中获取基于某个User的Message List(使用了DAAB):
private List<string> GetMessageByUser(string userName)
{
List<string> messageList = new List<string>();
Database db = DatabaseFactory.CreateDatabase();
DbCommand command = db.GetStoredProcCommand(MessageCachingProcedure);
db.AddInParameter(command, "UserID", DbType.String, userName);
IDataReader reader = db.ExecuteReader(command);
while (reader.Read())
{
messageList.Add(reader.GetString(2));
}
return messageList;
}
通 过GetMessages获取User(Foo)的Message List:首先通过CacheManager检测message list是否存在于Cache,如何不存在,调用上面的GetMessageByUser方法从database中获取Foo的message list。并将其加入Cache中,需要注意的是这里使用到了我们的SqlDependencyExpiration 对象。
private List<string> GetMessages()
{
ICacheManager manager = CacheFactory.GetCacheManager();
if (manager.GetData(CacheKey) == null)
{
manager.Add(CacheKey, GetMessageByUser(UserName), CacheItemPriority.Normal, null, this.CacheItemExpiration);
}
return manager.GetData(CacheKey) as List<string>;
}
由于在我们的例子中需要对DB进行数据操作,来检测数据的变换是否应用Cache的过期,我们需要想数据库中添加Message。我们通过下面的方式现在message的添加。
private void CreateMessageEntry(string userName, string message)
{
Database db = DatabaseFactory.CreateDatabase();
string insertSql = "INSERT INTO [dbo].[Messages]([UserID],[Message])VALUES(@userID, @message)";
DbCommand command = db.GetSqlStringCommand(insertSql);
db.AddInParameter(command, "userID", DbType.String, userName);
db.AddInParameter(command, "message", DbType.String, message);
db.ExecuteNonQuery(command);
}
我 们的Add按钮的实现如下:基于我们选择的Username和输入的message的内容向DB中添加Message,然后调用 GetMessages()方法获取基于用户Foo的Message列表。之所以要在两者之间将线程休眠1s,是为了上SqlDependency有足够 的时间结果从Database传过来的Query Notification,并触发OnChanged事件并执行相应的Event Handler,这样调用GetMessages时检测Cache才能检测到cache item已经过期了。
private void buttonAdd_Click(object sender, EventArgs e)
{
this.CreateMessageEntry(this.comboBoxUserName.SelectedValue.ToString(), this.textBoxMessage.Text.Trim());
Thread.Sleep(1000);
this.listBoxMessage.DataSource = this.GetMessages();
}
由 于我们缓存了用户Foo的Message list,所以当我们为Foo创建Message的时候,下面的ListBox的列表能够及时更新,这表明我们的cache item已经过期了。而我们为其他的用户(Bar,Baz)创建Message的时候,cache item将不会过期,这一点我们可以通过弹出的MessageBox探测掉(expiration.Expired += delegate MessageBox.Show("Cache has expired!");}; ),只有前者才会弹出下面的MessageBox:
注:由于SqlDependency建立在Service Broker之上的,所以我们必须将service Broker开关打开(默认使关闭的)。否则我们将出现下面的错误:
打开service Broker可以通过如下的T-SQL:ALTER DATABASE MyDb SET ENABLE_BROKER ;