標籤:
最近有項目需要用到mongodb,於是在網上下載了mongodb的源碼,根據樣本寫了測試代碼,但發現一個非常奇怪的問題:插入記錄的速度比擷取資料的速度還要快,而且最重要的問題是擷取資料的速度無法讓人接受。
測試情境:主文件儲存人員基本資料,子文檔一儲存學生上課合約資料集合,這個集合多的可達到幾百,子文檔二儲存合約的付款記錄集合,集合大小一般不會超過50。根據人員ID查詢人員文檔,序列化後的大小為180K不到,但消耗的時間在400ms以上。
我的主要問題在於不能接收穫取一個180K的記錄需要400ms以上,這比起傳統的RDBMS都沒有優勢,而且mongodb也是記憶體映射機制,沒道理效能如此之差,而且網路上關於它的效能測試資料遠遠好於我的測試結果。
排除方式一:是不是因為有子文檔的原因?
找一個沒有任何合約記錄的文檔查詢,發現結果依舊,沒有明顯的改善;
排除方式二:沒有建立索引?
在搜尋列ID上建立索引,結果依舊;
排除方式三:是不是文檔數量過大?
一萬多行只是小數目,沒理由,mongodb管理上千萬的文檔都是沒有問題的,於時還是決定試一試,將記錄全部刪除,插入一條記錄然後查詢,結果依舊;
排除方式四:是不是由於用戶端序列化的問題?
由於我儲存的是自訂的對象,不是預設的Document,所以決定嘗試直接儲存Document,Document就兩個欄位,擷取速度還是需要180ms。
排除方式五:是否由於客戶機器是32位,而mongodb服務是64?
將程式放在64位機器上測試,問題依舊。
排除方式六:是否由於網路傳輸問題?
沒道理啊,測試的用戶端以及服務端均在同一區域網路,但還是嘗試將用戶端程式直接在mongodb伺服器上執行,問題一樣;
上面的六種方式都已經嘗試過,沒有解決,最後決定求助於老代,畢竟是用過mongodb的高人,給我兩個建議就搞定了:
排除方式七:查看mongodb資料檔案,看是否已經很大?
經查看,總大小才64M,這比32位檔案上限的2G來講,可以基本忽略;
排除方式八:連接字串。
Servers=IP:27017;ConnectTimeout=30000;ConnectionLifetime=300000;MinimumPoolSize=8;MaximumPoolSize=256;Pooled=true
我一看到這個參考字串,第一印象是,我的寫法和它不一樣(string connectionString =""; ),然後發現有兩個重要的參數:
1:ConnectionLifetime=300000,從字面意思來看,是說串連的生命週期,而它的數值設定如此大,顯然說明此串連不會被立即關閉,這和sql server的做法有所區別;
2:Pooled=true,從字面意思來看,應該是有串連池的概念。
分析:從上面的串連參數來看,我之前所理解的串連,就是用戶端與服務端之間的串連,它需要在使用完之後馬上關閉,即用戶端與服務端不在有tcp串連。但我沒有很好的理解串連池的作用。串連池實際上從儲存很多個已經和服務端建立tcp串連的connection,在它的生命週期內一直保持和服務端的串連,生命週期過後會變成失效串連等待回收。
重新修改連接字串再進行測試,問題解決,只有第一次請求時,由於需要建立tcp串連,效能會受影響,後面的請求,因為有串連池的存在,效能得到成倍提高。
最後看了下samus源碼,就可以看出它是如何使用串連池的。
先看下我寫的一個mongodb的協助類:裡面有建立Mongo對象等常規操作。
public class MongodbFactory2<T>: IDisposable where T : class
{
//public string connectionString = "mongodb://10.1.55.172";
public string connectionString = ConfigurationManager.AppSettings["mongodb"];
public string databaseName = "myDatabase";
Mongo mongo;
MongoDatabase mongoDatabase;
public MongoCollection<T> mongoCollection;
public MongodbFactory2()
{
mongo = GetMongo();
mongoDatabase = mongo.GetDatabase(databaseName) as MongoDatabase;
mongoCollection = mongoDatabase.GetCollection<T>() as MongoCollection<T>;
mongo.Connect();
}
public void Dispose()
{
this.mongo.Disconnect();
}
/// <summary>
/// 配置Mongo,將類T映射到集合
/// </summary>
private Mongo GetMongo()
{
var config = new MongoConfigurationBuilder();
config.Mapping(mapping =>
{
mapping.DefaultProfile(profile =>
{
profile.SubClassesAre(t => t.IsSubclassOf(typeof(T)));
});
mapping.Map<T>();
});
config.ConnectionString(connectionString);
return new Mongo(config.BuildConfiguration());
}
從上面的代碼中可以看到有這麼一句:mongo.Connect(),我第一印象就是建立用戶端與服務端的串連,其實有了串連池,這個操作並非每次都建立遠端連線,有的情況只是從串連池中直接返回可用連線物件而已。
從源碼分析是如何利用串連池,串連是如何建立的。
1:Mongo類的Connect函數:需要跟蹤_connection對象。
/// <summary>
/// Connects to server.
/// </summary>
/// <returns></returns>
/// <exception cref = "MongoDB.MongoConnectionException">Thrown when connection fails.</exception>
public void Connect()
{
_connection.Open();
}
2:再看這句:return new Mongo(config.BuildConfiguration());
/// <summary>
/// Initializes a new instance of the <see cref = "Mongo" /> class.
/// </summary>
/// <param name = "configuration">The mongo configuration.</param>
public Mongo(MongoConfiguration configuration){
if(configuration == null)
throw new ArgumentNullException("configuration");
configuration.ValidateAndSeal();
_configuration = configuration;
_connection = ConnectionFactoryFactory.GetConnection(configuration.ConnectionString);
}
上面代碼的最後一句有_connection的產生過程。
3:可以跟蹤到最終產生connection的函數,終於看到builder.Pooled這個參數了,這的值就是串連串中的參數。
/// <summary>
/// Creates the factory.
/// </summary>
/// <param name="connectionString">The connection string.</param>
/// <returns></returns>
private static IConnectionFactory CreateFactory(string connectionString){
var builder = new MongoConnectionStringBuilder(connectionString);
if(builder.Pooled)
return new PooledConnectionFactory(connectionString);
return new SimpleConnectionFactory(connectionString);
}
4:再看PooledConnectionFactory是如何建立串連的:這的作用就是將可用串連放入串連池中,而最終真正建立串連的函數是CreateRawConnection()
/// <summary>
/// Ensures the size of the minimal pool.
/// </summary>
private void EnsureMinimalPoolSize()
{
lock(_syncObject)
while(PoolSize < Builder.MinimumPoolSize)
_freeConnections.Enqueue(CreateRawConnection());
}
5:真正遠端連線部分。
View Code
/// <summary>
/// Creates the raw connection.
/// </summary>
/// <returns></returns>
protected RawConnection CreateRawConnection()
{
var endPoint = GetNextEndPoint();
try
{
return new RawConnection(endPoint, Builder.ConnectionTimeout);
}catch(SocketException exception){
throw new MongoConnectionException("Failed to connect to server " + endPoint, ConnectionString, endPoint, exception);
}
}
private readonly TcpClient _client = new TcpClient();
private readonly List<string> _authenticatedDatabases = new List<string>();
private bool _isDisposed;
/// <summary>
/// Initializes a new instance of the <see cref="RawConnection"/> class.
/// </summary>
/// <param name="endPoint">The end point.</param>
/// <param name="connectionTimeout">The connection timeout.</param>
public RawConnection(MongoServerEndPoint endPoint,TimeSpan connectionTimeout)
{
if(endPoint == null)
throw new ArgumentNullException("endPoint");
EndPoint = endPoint;
CreationTime = DateTime.UtcNow;
_client.NoDelay = true;
_client.ReceiveTimeout = (int)connectionTimeout.TotalMilliseconds;
_client.SendTimeout = (int)connectionTimeout.TotalMilliseconds;
//Todo: custom exception?
_client.Connect(EndPoint.Host, EndPoint.Port);
}
接著我們來看下,串連的生命週期是如何?的:主要邏輯在PooledConnectionFactory,如果發現串連已經到期,則將串連放入不可用隊列,將此串連從空閑串連中刪除掉。
View Code
/// <summary>
/// Checks the free connections alive.
/// </summary>
private void CheckFreeConnectionsAlive()
{
lock(_syncObject)
{
var freeConnections = _freeConnections.ToArray();
_freeConnections.Clear();
foreach(var freeConnection in freeConnections)
if(IsAlive(freeConnection))
_freeConnections.Enqueue(freeConnection);
else
_invalidConnections.Add(freeConnection);
}
}
/// <summary>
/// Determines whether the specified connection is alive.
/// </summary>
/// <param name="connection">The connection.</param>
/// <returns>
/// <c>true</c> if the specified connection is alive; otherwise, <c>false</c>.
/// </returns>
private bool IsAlive(RawConnection connection)
{
if(connection == null)
throw new ArgumentNullException("connection");
if(!connection.IsConnected)
return false;
if(connection.IsInvalid)
return false;
if(Builder.ConnectionLifetime != TimeSpan.Zero)
if(connection.CreationTime.Add(Builder.ConnectionLifetime) < DateTime.Now)
return false;
return true;
}
最後我們來看我最上面的mongodb幫忙類的如下方法:即釋放串連,而這裡的釋放也不是直接意義上將串連從用戶端與服務端之間解除,只不過是將此串連從忙隊列中刪除,重新迴歸到可用隊列:
public void Dispose()
{
this.mongo.Disconnect();
}
再看看mongo.Disconnect()
/// <summary>
/// Disconnects this instance.
/// </summary>
/// <returns></returns>
public bool Disconnect()
{
_connection.Close();
return _connection.IsConnected;
}
繼續往下就會定位到如下核心內容:
View Code
/// <summary>
/// Returns the connection.
/// </summary>
/// <param name = "connection">The connection.</param>
public override void Close(RawConnection connection)
{
if(connection == null)
throw new ArgumentNullException("connection");
if(!IsAlive(connection))
{
lock(_syncObject)
{
_usedConnections.Remove(connection);
_invalidConnections.Add(connection);
}
return;
}
lock(_syncObject)
{
_usedConnections.Remove(connection);
_freeConnections.Enqueue(connection);
Monitor.Pulse(_syncObject);
}
}
總結:經過各位不同的嘗試,終於解決了mongodb查詢慢的原因,並非mongodb本身問題,也非網路,非資料問題,而是在於沒有正確使用好用戶端串連,不容易啊,在此謝謝老代的指點。
參考資料:
MongoDB學習筆記
http://www.360doc.com/content/16/0720/17/35239163_577069265.shtml
monogodb find 方法調用javascript where
MongoDB下samus源碼初探
Mongodb 與sql 語句對照
在MongoDB中實現開放式並行存取控制
Mongodb insert save 區別
通過mongodb用戶端samus代碼研究解決查詢慢問題