最近有项目需要用到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的做法有所区别;
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本身问题,也非网络,非数据问题,而是在于没有正确使用好客户端连接,不容易啊,在此谢谢老代的指点。