Firestore 获取数据时性能缓慢的问题

Posted

技术标签:

【中文标题】Firestore 获取数据时性能缓慢的问题【英文标题】:Firestore slow performance issue on getting data 【发布时间】:2018-03-24 21:08:08 【问题描述】:

与 1/10 比率的实时数据库相比,我在检索存储在文档中的基本数据时遇到了 Firestore 性能缓慢的问题。

使用 Firestore,第一次调用平均需要 3000 毫秒

 this.db.collection(‘testCol’)
   .doc(‘testDoc’)
   .valueChanges().forEach((data) => 
     console.log(data);//3000 ms later
 );

使用实时数据库,第一次调用平均需要300毫秒

 this.db.database.ref(‘/test’).once(‘value’).then(data => 
     console.log(data); //300ms later
 );

这是网络控制台的截图:

我正在使用 AngularFire2 v5.0 rc.2 运行 javascript SDK v4.50。

有人遇到过这个问题吗?

【问题讨论】:

如果您进行第二次调用(对不同的文档/集合),您会看到什么性能?如果您不使用 angularfire,您会看到同样的问题吗? 我也有类似的经历。第一次通话有点慢,有时5-10秒。我正在制作一个聊天应用程序 - 第一个消息需要一段时间才能传递,但随后的消息几乎是即时的。 Firestore 仍是测试版,他们可能仍在整理怪癖。 这里有类似的经历。首先 onSnapShot 花费了非常多的时间——对于一些导致我们的应用程序无法使用的用户来说,最多需要 2 分钟 同样的问题,非常令人沮丧。有些人报告说写操作会释放“挂起”的查询。 同样的问题,使用简单的 collection.get(document) 最多 1.5 分钟 【参考方案1】:

更新:2018 年 2 月 12 日 - iOS Firestore SDK v0.10.0

与其他一些评论者类似,我也注意到第一次获取请求的响应较慢(后续请求大约需要 100 毫秒)。对我来说,它没有 30 秒那么糟糕,但当我有良好的连接性时可能会在 2-3 秒左右,这足以在我的应用启动时提供糟糕的用户体验。

Firebase 已告知他们已经意识到这个“冷启动”问题,并且他们正在努力解决这个问题 - 遗憾的是没有 ETA。我认为这是一个单独的问题,当我的连接性较差时,get 请求决定从缓存中读取可能需要很长时间(超过 30 秒)。

在 Firebase 修复所有这些问题的同时,我开始使用新的 disableNetwork()enableNetwork() 方法(在 Firestore v0.10.0 中可用)来手动控制 Firebase 的在线/离线状态。虽然我在代码中使用它时必须非常小心,因为存在一个 Firestore 错误,可能会在某些情况下导致崩溃。


更新:2017 年 11 月 15 日 - iOS Firestore SDK v0.9.2

现在似乎已经修复了性能缓慢的问题。我重新运行了下面描述的测试,Firestore 返回 100 个文档所需的时间现在似乎始终在 100 毫秒左右。

不确定这是最新 SDK v0.9.2 中的修复还是后端修复(或两者兼而有之),但我建议每个人都更新他们的 Firebase pod。我的应用的响应速度明显更快 - 类似于它在 Realtime DB 上的方式。


我还发现 Firestore 比 Realtime DB 慢得多,尤其是在读取大量文档时。

更新的测试(使用最新的 iOS Firestore SDK v0.9.0):

我使用 RTDB 和 Firestore 在 ios Swift 中建立了一个测试项目,并在每个项目上运行了 100 次顺序读取操作。对于 RTDB,我在 100 个***节点中的每一个上测试了 observeSingleEvent 和 observe 方法。对于 Firestore,我对 TestCol 集合中的 100 个文档中的每个文档都使用了 getDocument 和 addSnapshotListener 方法。我在打开和关闭磁盘持久性的情况下运行了测试。请参考附图,其中显示了每个数据库的数据结构。

我在同一设备和稳定的 wifi 网络上为每个数据库运行了 10 次测试。现有的观察者和监听者在每次新的运行之前被销毁。

实时数据库observeSingleEvent方法:

func rtdbObserveSingle() 

    let start = UInt64(floor(Date().timeIntervalSince1970 * 1000))
    print("Started reading from RTDB at: \(start)")

    for i in 1...100 
        Database.database().reference().child(String(i)).observeSingleEvent(of: .value)  snapshot in
            let time = UInt64(floor(Date().timeIntervalSince1970 * 1000))
            let data = snapshot.value as? [String: String] ?? [:]
            print("Data: \(data). Returned at: \(time)")
        
    

实时数据库观察方法:

func rtdbObserve() 

    let start = UInt64(floor(Date().timeIntervalSince1970 * 1000))
    print("Started reading from RTDB at: \(start)")

    for i in 1...100 
        Database.database().reference().child(String(i)).observe(.value)  snapshot in
            let time = UInt64(floor(Date().timeIntervalSince1970 * 1000))
            let data = snapshot.value as? [String: String] ?? [:]
            print("Data: \(data). Returned at: \(time)")
        
    

Firestore getDocument 方法:

func fsGetDocument() 

    let start = UInt64(floor(Date().timeIntervalSince1970 * 1000))
    print("Started reading from FS at: \(start)")

    for i in 1...100 
        Firestore.firestore().collection("TestCol").document(String(i)).getDocument()  document, error in

            let time = UInt64(floor(Date().timeIntervalSince1970 * 1000))
            guard let document = document, document.exists && error == nil else 
                print("Error: \(error?.localizedDescription ?? "nil"). Returned at: \(time)")
                return
            
            let data = document.data() as? [String: String] ?? [:]
            print("Data: \(data). Returned at: \(time)")
        
    

Firestore addSnapshotListener 方法:

func fsAddSnapshotListener() 

    let start = UInt64(floor(Date().timeIntervalSince1970 * 1000))
    print("Started reading from FS at: \(start)")

    for i in 1...100 
        Firestore.firestore().collection("TestCol").document(String(i)).addSnapshotListener()  document, error in

            let time = UInt64(floor(Date().timeIntervalSince1970 * 1000))
            guard let document = document, document.exists && error == nil else 
                print("Error: \(error?.localizedDescription ?? "nil"). Returned at: \(time)")
                return
            
            let data = document.data() as? [String: String] ?? [:]
            print("Data: \(data). Returned at: \(time)")
        
    

每个方法本质上都是在方法开始执行时以毫秒为单位打印 unix 时间戳,然后在每个读取操作返回时打印另一个 unix 时间戳。我取了初始时间戳和最后一个时间戳之间的差来返回。

结果 - 磁盘持久性已禁用:

结果 - 启用磁盘持久性:

数据结构:

当 Firestore getDocument / addSnapshotListener 方法卡住时,它似乎卡住了大约 30 秒的倍数的持续时间。也许这可以帮助 Firebase 团队找出它在 SDK 中卡住的地方?

【讨论】:

所以 firestore 更贵也更慢。希望 firebase 团队看到这一点 [Firebaser here] 感谢您抽出宝贵时间提供如此详细的数据,我们一直很感激。问题不在于系统“较慢”,而是极少数查询卡住或需要大量时间才能返回。我们即将推出一些修复程序,我们相信这些修复程序会改善这种情况。 感谢您为我们提供最新信息。我为最新的 Firestore SDK v0.9.0 添加了一些新结果,这可能有助于您的团队找出问题的根源。我还遇到了快照侦听器的另一个问题:***.com/questions/46710371/… 根本原因可能与此主题相关,也可能不相关,但如果 Firebase 团队可以查看它,那就太好了。非常感谢! 我们在 web sdk 上也遇到了“卡住”的查询。挂起 10-20 秒,然后数据到达(在 v4.8.0 上)。 我最近注意到了这一点,并向 Firebase 报告。他们知道“冷启动”问题并正在努力修复。与此同时,我正在尝试上面更新中详述的解决方法,并且我已经取得了不同程度的成功。【参考方案2】:

更新日期 2018 年 3 月 2 日

这似乎是一个已知问题,Firestore 的工程师正在努力修复。在与 Firestore 工程师就此问题进行了几次电子邮件交流和代码共享后,这是他今天的回复。

“您实际上是正确的。经过进一步检查,getDocuments() API 的这种缓慢是 Cloud Firestore 测试版中的已知行为。我们的工程师知道这个性能问题标记为“冷启动”,但不要担心我们正在尽最大努力提高 Firestore 查询性能。

我们已经在进行长期修复,但目前我无法分享任何时间表或细节。虽然 Firestore 仍处于测试阶段,但预计会有更多改进。”

所以希望这很快就会被淘汰。


使用 Swift / iOS

在处理了大约 3 天之后,问题似乎肯定是 get(),即 .getDocuments 和 .getDocument。我认为的事情导致了极端但间歇性的延迟,但似乎并非如此:

    网络连接不太好 通过循环 .getDocument() 重复调用 链接 get() 调用 Firestore 冷启动 获取多个文档(获取 1 个小文档导致 20 秒延迟) 缓存(我禁用了离线持久性,但这没有任何作用。)

我能够排除所有这些问题,因为我注意到这个问题并没有发生在我进行的每个 Firestore 数据库调用中。仅使用 get() 进行检索。为了踢球,我用 .addSnapshotListener 替换了 .getDocument 来检索我的数据,瞧。每次即时检索,包括第一次呼叫。没有冷启动。到目前为止,.addSnapshotListener 没有问题,只有 getDocument(s)。

现在,我只是删除时间至关重要的 .getDocument() 并用 .addSnapshotListener 替换它,然后使用

for document in querySnapshot!.documents
// do some magical unicorn stuff here with my document.data()

...为了继续前进,直到 Firestore 解决这个问题。

【讨论】:

我也看到了相同的行为,但仅在 android 中。现在我也开始使用快照了。但是如果get查询的性能是一致的就好了。 我还发现使用 addSnapshotListener 的 FirebaseUI 回收器适配器的性能也很慢。 这个“冷启动”问题还存在吗?我对您 3 月份的更新提到 Firebase 工程师知道他们将其标记为“冷启动”问题感到有些困惑,因为在您的原始答案中,您写道您已经排除了“4. Firestore 冷启动”是问题? 我仍然看到 android 的性能缓慢和大量内存问题。你们打算提供有关此修复的任何更新 最新版本的 firestore i 仍然会出现此问题。 iOS 和使用快照监听器就像一个魅力。很棒的发现。【参考方案3】:

直到今天早上我都遇到了这个问题。我通过 iOS/Swift 进行的 Firestore 查询大约需要 20 秒才能完成一个简单的、完全索引的查询 - 返回的 1 个项目的查询时间不成比例 - 一直到 3,000 个。

我的解决方案是禁用离线数据持久性。就我而言,它不适合我们的 Firestore 数据库的需求——它的大部分数据每天都会更新。

iOS 和 Android 用户默认启用此选项,而 Web 用户默认禁用此选项。如果您要查询大量文档,它会使 Firestore 看起来异常缓慢。基本上它会缓存您正在查询的任何数据的副本(以及您正在查询的任何集合 - 我相信它会缓存其中的所有文档),这可能导致高内存使用。

在我的情况下,它会导致每次查询都需要等待很长时间,直到设备缓存了所需的数据 - 因此,从完全相同的集合返回越来越多的项目的查询时间不成比例。这是因为在每个查询中缓存集合所花费的时间相同。

Offline Data - from the Cloud Firestore Docs

我执行了一些基准测试以显示来自同一个查询集合的这种效果(启用离线持久性),但使用 .limit 参数返回的项目数量不同:

现在返回 100 个项目(禁用离线持久性),我的查询不到 1 秒即可完成。

我的 Firestore 查询代码如下:

let db = Firestore.firestore()
self.date = Date()
let ref = db.collection("collection").whereField("Int", isEqualTo: SomeInt).order(by: "AnotherInt", descending: true).limit(to: 100)
ref.getDocuments()  (querySnapshot, err) in
    if let err = err 
        print("Error getting documents: \(err)")
     else 
        for document in querySnapshot!.documents 
            let data = document.data()
            //Do things
        
        print("QUERY DONE")
        let currentTime = Date()
        let components = Calendar.current.dateComponents([.second], from: self.date, to: currentTime)
        let seconds = components.second!
        print("Elapsed time for Firestore query -> \(seconds)s")
        // Benchmark result
    

【讨论】:

这些测试编号是来自 Web、iOS 还是 Android? 基准来自 iOS,但我希望在所有平台上都有性能提升 - 取决于您查询的集合的大小 不知道您是 Firebase 团队的一员!这个查询的好处是内存使用量会达到很高的数字(600-700mb ram),我假设我们的集合存储在缓存中。查询将始终挂起,然后在内存逐渐上升然后达到同一点(700mb-ish)时完成。禁用持久性后,这种效果就停止了,我们的内存仍如预期(100-150mb),同时以超快的速度返回我们的结果。如果您需要更多信息,请询问。 嗯,这很有趣。您能否创建一个示例 Xcode 项目来复制它并将其通过电子邮件发送给我?如果是这样,那就是 google dot com 的 samstern。我很想仔细看看。 @SamStern :这已经有一段时间了,但我在 Android 上遇到了完全相同的问题。关于可能导致这种情况的任何线索?我尝试构建一个最小的项目,但该项目没有这个问题!【参考方案4】:

差不多 3 年后,firestore 已经完全退出测试版,我可以确认这个可怕的问题仍然存在;-(

在我们的移动应用上,我们使用 javascript / node.js firebase 客户端。经过大量测试以找出为什么我们的应用程序的启动时间约为 10 秒后,我们确定了将 70% 的时间归因于...好吧,对于 firebase 和 firestore 的性能和冷启动问题:

firebase.auth().onAuthStateChanged() 大约触发。 1.5 - 2 秒后,已经很糟糕了。 如果它返回一个用户,我们使用它的 ID 从 firestore 获取用户文档。这是对 firestore 的第一次调用,相应的 get() 需要 4 - 5 秒。相同或其他文档的后续 get() 大约需要。 500 毫秒。

所以用户初始化总共需要 6 - 7 秒,完全不可接受。我们对此无能为力。我们无法测试禁用持久性,因为在 javascript 客户端中没有这样的选项,默认情况下始终启用持久性,因此不调用 enablePersistence() 不会改变任何内容。

【讨论】:

这个问题有什么解决方法吗?一些指向哪里可以寻找答案的指针将不胜感激。 在 javascript 客户端中它默认关闭:For the web, offline persistence is disabled by default. To enable persistence, call the enablePersistence method 但我可以确认关闭它时,我们的初始请求时间从大约 8 秒减少到大约 500 毫秒 firebase.google.com/docs/firestore/manage-data/enable-offline 正确,在javascript中它默认是关闭的,我上面提到的时间是默认设置的。在我们的例子中,我们需要新鲜和更新的用户配置文件数据,所以使用持久性不是一种选择。【参考方案5】:

嗯,根据我目前正在做的和研究,在模拟器和真正的安卓手机华为 P8 中使用 nexus 5X,

Firestore 和 Cloud Storage 都让我头疼的是响应慢 当我做第一个 document.get() 和第一个 storage.getDownloadUrl()

它给我每个请求超过 60 秒的响应时间。缓慢的响应只发生在真正的安卓手机上。不在模拟器中。另一个奇怪的事情。 初次相遇后,休息请求顺利。

这是我遇到响应缓慢的简单代码。

var dbuserref = dbFireStore.collection('user').where('email','==',email);
const querySnapshot = await dbuserref.get();

var url = await defaultStorage.ref(document.data().image_path).getDownloadURL();

我还发现了正在研究相同的链接。 https://reformatcode.com/code/android/firestore-document-get-performance

【讨论】:

以上是关于Firestore 获取数据时性能缓慢的问题的主要内容,如果未能解决你的问题,请参考以下文章

Firestore:仅当有更新时才从服务器获取数据,否则从缓存中获取

从firestore动态获取数据时RenderItem不渲染[重复]

从 StreamBuilder BloC firestore Flutter 获取数据时出错

在 Firebase 函数中从 Firestore 获取数据时遇到问题

在获取数据期间索引超出部分中的行数范围

Flutter web无法从firestore获取数据