连接 MongoDB 的 .NET 最佳实践?

73

我最近在使用GitHub上的C#驱动程序玩MongoDB(它快得惊人)。我的单线程控制台应用程序一切正常,我正在测试中。我能够在不到8秒的时间内添加100万个文档(是的,百万级别),但只有在连接超出for循环范围时才能获得这种性能。换句话说,在每次插入时保持连接打开,而不是为每次插入连接。显然这很牵强。

我想要进一步提高效率,看看它如何处理多个线程。我这样做是因为我需要模拟具有多个并发请求的网站。我同时启动15到50个线程,在所有情况下插入了总共150,000个文档。如果我让线程运行,每个线程创建一个新的连接进行每个插入操作,性能就会停滞不前。

显然,我需要找到一种共享、锁定或池化连接的方法。这就是问题所在。在连接MongoDB方面,什么是最佳实践?连接应该保持应用程序的生命周期开放(每个操作打开和关闭TCP连接的延迟很大)吗?

是否有任何与MongoDB以及特定底层连接相关的真实世界或生产经验?

这是我的线程示例,使用为插入操作锁定的静态连接。请提供在Web环境中最大化性能和可靠性的建议!

private static Mongo _mongo;

private static void RunMongoThreaded()
{
    _mongo = new Mongo();
    _mongo.Connect();

    var threadFinishEvents = new List<EventWaitHandle>();

    for(var i = 0; i < 50; i++)
    {
        var threadFinish = new EventWaitHandle(false, EventResetMode.ManualReset);
        threadFinishEvents.Add(threadFinish);

        var thread = new Thread(delegate()
            {
                 RunMongoThread();
                 threadFinish.Set();
            });

        thread.Start();
    }

    WaitHandle.WaitAll(threadFinishEvents.ToArray());
    _mongo.Disconnect();
}

private static void RunMongoThread()
{
    for (var i = 0; i < 3000; i++)
    {
        var db = _mongo.getDB("Sample");
        var collection = db.GetCollection("Users");
        var user = GetUser(i);
        var document = new Document();
        document["FirstName"] = user.FirstName;
        document["LastName"] = user.LastName;

        lock (_mongo) // Lock the connection - not ideal for threading, but safe and seemingly fast
        {
            collection.Insert(document);
        }
    }
}

3
最终你决定了什么?面对同样的问题... - Andrew Bullock
4
好消息是我不需要做出决定。Mongodb-csharp和NoRM驱动程序都添加了对连接池的支持。这两个库都有良好设计的、线程安全的机制来针对mongod或mongos进程池化连接。这两个库也将在不久的将来添加副本集支持。 - Tyler Brinks
@TylerBrinks,你能展示一下你是如何在8秒内插入100万个文档的例子吗?我无法在单线程上达到那样的速度。 - hackp0int
6个回答

147

大多数回答已经过时了,随着.net驱动程序的成熟和添加了无数功能,它们不再适用。

查看此处新2.0驱动程序的文档: http://mongodb.github.io/mongo-csharp-driver/2.0/reference/driver/connecting/

.net驱动现在是线程安全的,并且处理连接池。根据文档

建议将MongoClient实例存储在全局位置中,可以是静态变量或具有单例生命周期的IoC容器中。


2
这个回答需要被推到顶部。拿走我的赞! - Zignd
这是最新文档版本的链接:https://www.mongodb.com/docs/drivers/csharp/current/faq/#why-does-the-driver-throw-a-timeout-during-server-selection-:~:text=Create%20a%20client%20once%20for%20each%20process%2C%20and%20reuse%20it%20for%20all%20operations.%20It%20is%20a%20common%20mistake%20to%20create%20a%20new%20client%20for%20each%20request%2C%20which%20is%20very%20inefficient. - undefined

9
记住静态连接是共享在所有线程之间的。您需要的是每个线程一个连接。

你可能没有注意到我说的是每个线程只有一个连接会明显变慢。我认为这不是高流量网站的最佳答案。 - Tyler Brinks
5
对于您的示例,在分组内容时,每个线程一个是您能做到的最好选择。静态的、共享的连接会创建死锁,就像您所看到的那样。您的另一种选择是进行连接池管理。虽然 SQL Server 提供程序内置了这个功能,但对于 MongoDB,您需要自己构建它,并且要想正确使用并不容易。 - Joel Coehoorn
1
今天再次查看,您可能有太多线程。理想情况下,您需要一个共享的、线程安全的队列来处理工作项,只需要少量的线程(确切的数量取决于您的系统,但最大的因素是处理器核心数)。每个线程从队列中获取项目。这将减少连接数量,使其不再成为瓶颈。 - Joel Coehoorn

6
使用mongodb-csharp时,您可以像使用ADO连接一样对待它。 当您创建Mongo对象时,它会从池中借用一个连接,并拥有该连接,直到被处理。因此,在使用块后,连接将返回到池中。 创建Mongo对象是便宜且快速的。
示例:
for(var i=0;i<100;i++) 
{ 
        using(var mongo1 = new Mongo()) 
        using(var mongo2 = new Mongo()) 
        { 
                mongo1.Connect(); 
                mongo2.Connect(); 
        } 
} 

数据库日志
2021年6月2日20:54:21,来自127.0.0.1:58214的连接被接受 #1
2021年6月2日20:54:21,来自127.0.0.1:58215的连接被接受 #2
2021年6月2日20:54:21,127.0.0.1:58214的消息端口接收()错误编号:0,无错误
2021年6月2日20:54:21,结束与127.0.0.1:58214的连接
2021年6月2日20:54:21,127.0.0.1:58215的消息端口接收()错误编号:0,无错误
2021年6月2日20:54:21,结束与127.0.0.1:58215的连接

注意,它只打开了两个连接。

我使用mongodb-csharp论坛整理了这些信息。 http://groups.google.com/group/mongodb-csharp/browse_thread/thread/867fa78d726b1d4


1
到目前为止最佳答案。谢谢! :) - Jo Smo

1

有点意思但仍然值得关注的是CSMongo,这是由jLinq的开发者创建的用于MongoDB的C#驱动程序。以下是一个示例:

//create a database instance
using (MongoDatabase database = new MongoDatabase(connectionString)) {

    //create a new document to add
    MongoDocument document = new MongoDocument(new {
        name = "Hugo",
        age = 30,
        admin = false
    });

    //create entire objects with anonymous types
    document += new {
        admin = true,
        website = "http://www.hugoware.net",
        settings = new {
            color = "orange",
            highlight = "yellow",
            background = "abstract.jpg"
        }
    };

    //remove fields entirely
    document -= "languages";
    document -= new[] { "website", "settings.highlight" };

    //or even attach other documents
    MongoDocument stuff = new MongoDocument(new {
        computers = new [] { 
            "Dell XPS", 
            "Sony VAIO", 
            "Macbook Pro" 
            }
        });
    document += stuff;

    //insert the document immediately
    database.Insert("users", document);

}

0

我正在使用csharp-mongodb驱动程序,但它并没有帮助我处理连接池:(每个Web请求大约有10-20个对mongodb的请求。(平均在线150个用户) 我甚至无法监视统计信息或从shell连接到mongodb,因为它会向我抛出异常。

我创建了一个存储库,每个请求打开和释放连接。我依赖于以下事情: 1)驱动程序具有连接池 2)经过我的研究(我在用户组中发布了一些问题),我了解到创建mongo对象并打开连接不是重操作,所以不会很耗费时间。

但今天我的生产环境崩溃了:( 也许我必须保存每个请求的打开连接...

这里是用户组的链接 http://groups.google.com/group/mongodb-user/browse_thread/thread/3d4a4e6c5eb48be3#


如果您释放了连接,实际上就是在与连接池作斗争——池无法回收已释放的连接,并且必须经过建立全新连接的开销来处理每个请求。只需使用您的连接,并在完成后关闭它即可。 - Curt

0

连接池应该是你的答案。

该功能正在开发中(请参见http://jira.mongodb.org/browse/CSHARP-9以获取更多详细信息)。

目前,对于Web应用程序,最佳实践是在BeginRequest时连接并在EndRequest时释放连接。但对我来说,每个请求都执行这个操作太昂贵了,没有连接池。因此,我决定拥有全局Mongo对象,并将其用作每个线程的共享资源(如果您现在从github获取最新的C#驱动程序,它们还会稍微提高并发性能)。

我不知道使用全局Mongo对象的缺点。所以让我们等待另一个专家对此发表评论。

但我认为在完成功能(连接池)之前,我可以使用它。


你在连接SQL Server/MySQL时使用相同的方式吗?我认为连接池的最佳实践仍然是“尽量晚打开,尽早关闭”,在请求期间多次打开/关闭连接几乎不会产生任何成本。 - Tien Do

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接