文章目錄
前些日子,我們的NHibernateQQ群上,一位老兄(未徵求其同意,不好透露他的資訊)碰到了一個問題:NHibernate映射Blob欄位(他用Binary映射SQL Server的Image類型,用來隱藏檔),當處理的檔案大小比較大的時候(大於10M)速度就極慢了,不能忍受。呼籲我們一起解決這個問題。
我先作了一個不用NHibernate的程式:
資料表:
CREATE TABLE [dbo].[FileBlob] (
[FileID] [int] IDENTITY (1, 1) NOT NULL ,
[FileName] [varchar] (255) COLLATE Chinese_PRC_CI_AS NOT NULL ,
[CreateTime] [smalldatetime] NOT NULL ,
[Stream] [image] NULL
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
資料插入:
public FileBlob Add(FileBlob blob)
{
SqlConnection conn=new SqlConnection(m_ConnectionString);
conn.Open();
SqlCommand cmd=conn.CreateCommand();
string sql="Insert Into FileBlob(FileName,CreateTime,Stream) "+
"Values(@FileName,@CreateTime,@Stream);"+
"Select Scope_Identity()";
cmd.CommandText=sql;
cmd.Parameters.Add("@FileName",blob.FileName);
cmd.Parameters.Add("@CreateTime",blob.CreateTime);
cmd.Parameters.Add("@Stream",SqlDbType.Image,blob.Stream.Length,"Stream");
if(blob.Stream==null)
{
cmd.Parameters["@Stream"].Value=DBNull.Value;
}
else
{
cmd.Parameters["@Stream"].Value=blob.Stream;
}
object id=cmd.ExecuteScalar();
conn.Close();
return Get(int.Parse(id.ToString()));
}
資料擷取:
public FileBlob Get(int id)
{
SqlConnection conn=new SqlConnection(m_ConnectionString);
conn.Open();
SqlCommand cmd=conn.CreateCommand();
string sql="Select * from FileBlob Where FileID=@FileID";
cmd.CommandText=sql;
cmd.Parameters.Add("@FileID",id);
SqlDataReader reader=cmd.ExecuteReader();
if(reader.Read())
{
FileBlob blob=new FileBlob();
blob.ID=int.Parse(reader["FileID"].ToString());
blob.FileName=reader["FileName"].ToString();
blob.CreateTime=DateTime.Parse(reader["CreateTime"].ToString());
if(reader["Stream"]!=DBNull.Value)
{
blob.Stream=reader["Stream"] as byte[];
}
reader.Close();
return blob;
}
else
{
reader.Close();
conn.Close();
return null;
}
}
用SqlCommand進行標準的(當然是相對於NHibernate來說)進行資料訪問,測試資料如下:
測試檔案
aaa.rar 17.0 MB
插入
次數
時間(秒)
第1次
2.07
第2次
1.73
第3次
2.64
第4次
1.57
第5次
1.95
平均
1.992
擷取
次數
時間(秒)
第1次
0.093
第2次
0.109
第3次
0.171
第4次
0.109
第5次
0.172
平均
0.1308
可見,直接使用SqlCommand進行Blog類型的資料訪問效能是不錯的。
那麼接下來,我使用NHibernate進行同樣的測試:
資料持久化類:
[ClassAttribute("FileBlob")]
public class FileBlob : Persistency
{
private int m_FileID;
private string m_FileName;
private System.DateTime m_CreateTime;
private System.Byte[] m_Stream;
public FileBlob()
{
}
[Id("FileID", "native", "0")]
public new virtual int ID
{
get
{
return this.m_FileID;
}
set
{
this.m_FileID = value;
}
}
[PropertyAttribute("FileName")]
public virtual string FileName
{
get
{
return this.m_FileName;
}
set
{
this.m_FileName = value;
}
}
[PropertyAttribute("CreateTime")]
public virtual System.DateTime CreateTime
{
get
{
return this.m_CreateTime;
}
set
{
this.m_CreateTime = value;
}
}
[PropertyAttribute("Stream")]
public virtual System.Byte[] Stream
{
get
{
return this.m_Stream;
}
set
{
this.m_Stream = value;
}
}
}
對應檔:
<class name="NHibernateBlobTest.FileBlob, NHibernateBlobTest" table="FileBlob">
<id name="ID" type="Int32" column="FileID" access="property" unsaved-value="0">
<generator class="native" />
</id>
<property name="FileName" column="FileName" access="property" type="String" />
<property name="CreateTime" column="CreateTime" access="property" type="DateTime" />
<property name="Stream" type="BinaryBlob">
<column name="Stream" sql-type="Image"></column>
</property>
</class>
測試資料如下:
測試檔案
aaa.rar 17.0 MB
插入
次數
時間(秒)
第1次
28.8
第2次
26.94
第3次
16.26
第4次
18.5
第5次
14.68
平均
21.03
擷取
基本上和死機一樣,不必測了,一定有什麼地方出問題了
解決過程:
提出問題的那個老兄說去看代碼找問題,我是很懶的人,看代碼對我來說太慢太沒效率了。所以決定用更直接的方法,用效能分析工具直接定位效能地下的代碼塊。
效能分析工具有很多種,比如:Intel的VTune——最近Intel在大力推廣,不過我對它沒什麼好感,從網上下載的7.2版本安裝都不成功,7.0版本好不容易安成了,使用時候卻老報錯。另外呢,還有DevPartner——包括效能分析、程式碼涵蓋範圍等等測試,我就用它了(以後我會寫其它文章具體介紹DevPartner的使用方法)。
首先測試插入問題,我用DevePartner的錄製操作過程,然後停止錄製後,它會產生一個測試結果,包括整個程式運行調用的源碼和引用的Dll的全部方法啟動並執行時間。在結果中我看到,在NHibernate 中的各個方法調用時間都比較正常,反而使Log4Net(NHibernate使用Log4Net進行日誌記錄,記錄NHibernate操作資料的各個步驟)的一個Debug方法已耗用時間超常。我突然明白了,原來NHibernate用Log4Net記錄所有插入的資料,也就是說為的17M大小的檔案被Log4Net寫入的日誌。等找到了Log檔案,嚇我一跳,已經239M了,難怪這麼慢呢。於是我把App.Config內Log4Net的配置改為不記錄任何:
<log4net threshold="OFF">
再次測試插入資料,平局插入時間大約在3~4秒,雖然還是遠不及直接用SqlCommand,但是已經有了非常大的提高。
提出問題的那個老兄也發現了資料擷取的瓶頸所在:原來,在NHibernate的原始碼NHibernate.Type.BinaryType類的Get()方法,有如下代碼片斷:
MemoryStream outputStream = new MemoryStream(1024);
byte[ ] buffer = new byte[1024];
long fieldOffset = 0;
try
{
while( true )
{
long amountRead = rs.GetBytes( index, fieldOffset, buffer, 0, 1024 );
if( amountRead == 0 )
{
break;
}
fieldOffset += amountRead;
outputStream.Write( buffer, 0, ( int ) amountRead );
}
outputStream.Close();
}
catch( IOException ioe )
{
throw new HibernateException( "IOException occurred reading a binary value", ioe );
}
問題就在這裡了,NHibernate用MemoryStream的建構函式分配了它的初始大小1024位元組,而我們的測試檔案大小是17M,那麼用這樣的大小進行迴圈,就需要17*1024*1024/1024=17408次,如此大的迴圈,能不慢麼。找到問題了,改起來就容易了,我們修改一下NHibernate的原始碼,把MemoryStream不分配初始大小,而每次讀取的buffer大小為1024*1024。測試速度發現,每次擷取大約在1.5秒以內,提高效能非常巨大(當然比SqlCommand讀取到DataReader還是太慢了,為了O/R犧牲一些效能吧)。
結論
NHibernate在效能上還是有些缺陷的,而且目前的版本0.7 Beta還是不是很成熟(當然大部分應用沒有問題)。這也可以使我們理解微軟在VS2005中放棄ObjectSpaces的原因了。
但是,作為一個堅定的物件導向、領域模型驅動的信仰者,使用事務指令碼和表模式的資料訪問方式進行商務邏輯構建是我不能接受的,儘管我同意在一些效能要求極高的系統中應當放棄O/R。這就要求我們能夠更多的參與到開源工程中,不僅僅是使用,更要理解、改進。使我們的設計能夠更好的降低軟體次要複雜度,讓我們更多的精力投入到解決主要複雜度的事情上來。