mongodb查詢速度慢是什么原因?
通過mongodb客戶端samus代碼研究解決問題 最近有項目需要用到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 = "mongodb://localhost"; ),然后發現有兩個重要的參數:
1:ConnectionLifetime=300000,從字面意思來看,是說連接的生命周期,而它的數值設置如此大,顯然說明此連接不會被立即關閉,這和sql server的做法有所區別;
2ooled=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本身問題,也非網絡,非數據問題指點。,而是在於沒有正確使用好客戶端連接,不容易啊。