Connect with us

Разработка

Анализ инцидента при миграции Core Data: скрытые ловушки, которые мы упускаем из виду

В этой статье описывается инцидент с тайм-аутом при запуске приложения, вызванный миграцией модели Core Data, предлагается решение и проводится глубокий анализ основных причин.

Опубликовано

/

     
     

В отличие от некоторых фреймворков с открытым исходным кодом, Core Data и SwiftData, несмотря на официальную поддержку Apple, часто оставляют разработчиков беззащитными перед возникающими ошибками из-за своей природы «чёрного ящика», что затрудняет быстрое выявление проблем и поиск эффективных решений. В этой статье описывается инцидент с тайм-аутом при запуске приложения, вызванный миграцией модели Core Data, предлагается решение и проводится глубокий анализ основных причин.

Множественные жалобы пользователей в течение недели

Несколько дней назад мой друг-разработчик Чжан обратился ко мне. Всего за одну неделю он получил множество жалоб от пользователей: некоторые опытные пользователи столкнулись с проблемой белого экрана после обновления приложения, не имея доступа к интерфейсу, что делало приложение полностью непригодным для использования.

NotingPro Чжана — это приложение для заметок, разработанное специально для iPadOS. Многие постоянные пользователи накопили огромные объёмы данных, в отдельных учётных записях объём данных может достигать 20 ГБ.

Особенно тревожным было то, что проблемы появились именно после этого обновления, и затронутыми оказались в основном самые преданные пользователи приложения, что очень обеспокоило Чжана.

Настоящая причина белого экрана

NotingPro использует Core Data с CloudKit в качестве решения для локального и облачного хранения данных. В этом обновлении Чжан изменил модель данных, добавив две сущности и новый атрибут к существующей сущности.

Поскольку проблема возникла сразу после внесения изменений в модель, мы сразу же предположили, что её причиной является миграция данных. Однако изменения Чжана полностью соответствовали правилам облегчённой миграции, и большинство пользователей не пострадали, поэтому мы изначально исключили несовместимость моделей как причину.

Учитывая, что у затронутых пользователей были огромные объёмы локальных данных, мы задавались вопросом, не сбоит ли Core Data на эффективной миграции сверхбольших баз данных. Исходя из опыта, хотя объём данных в 10-20 ГБ не так уж мал, SQLite с ним вполне справляется. Чжан также создал локально тестовые данные объёмом в гигабайт, но не смог воспроизвести проблему.

Наконец, отчёты о сбоях IPS (iOS Problem Summary), предоставленные некоторыми пользователями, раскрыли правду: миграция Core Data заняла слишком много времени, превысив 20-секундный порог iOS Watchdog, что привело к принудительному завершению приложения системой.

Проще говоря, процесс миграции данных выполнялся в основном потоке, что приводило к появлению белых экранов из-за длительной блокировки.

Временное решение

После выявления проблемы, чтобы помочь пострадавшим пользователям быстро восстановиться, Чжан принял экстренное решение: перенес инициализацию базы данных в фоновый поток, переключаясь на обычный интерфейс только после завершения миграции, что позволило избежать длительной блокировки основного потока.

Я организовал этот подход в упрощённой версии, совместимой со SwiftUI:

@MainActor
final class Stack: ObservableObject {
    @Published var status = LoadingStatus.loading
    private let container: NSPersistentContainer

    init() {
        self.container = NSPersistentContainer(name: "ActorStack")
        loadStores()
    }
    
    private func loadStores() {
        DispatchQueue.global().async { [weak self] in
            guard let self else { return }
            
            self.container.loadPersistentStores { _, error in
                DispatchQueue.main.async {
                    if let error = error as NSError? {
                        self.status = .failed(error)
                        print("Core Data loading failed: \(error)")
                    } else {
                        self.configureContexts()
                        self.status = .success
                    }
                }
            }
        }
    }
    
    private func configureContexts() {
        container.viewContext.automaticallyMergesChangesFromParent = true
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
    }
  
    var viewContext: NSManagedObjectContext {
        container.viewContext
    }

    static let shared = Stack()
}

enum LoadingStatus {
    case loading
    case success
    case failed(NSError)
}

@main
struct ActorStackApp: App {
    @StateObject var stack = Stack.shared

    var body: some Scene {
        WindowGroup {
            switch stack.status {
            case .success:
                ContentView()
                    .environment(\.managedObjectContext, stack.viewContext)
            case .failed(let error):
                ErrorView(error: error) {
                    stack.retryLoading()
                }
            case .loading:
                LoadingView()
            }
        }
    }
}

Таким образом, независимо от длительности миграции, основной поток не будет заблокирован, и приложение перейдёт в обычный интерфейс только после её завершения.

После выхода обновлённой версии все затронутые пользователи вернулись к нормальной работе, хотя первый запуск мог потребовать более длительного ожидания.

Основная причина

Хотя проблема была решена, нам всё ещё предстояло выяснить, почему лёгкая миграция заняла так много времени. Загадка была решена только после того, как я изучил код конфигурации Core Data Stack Чжана.

Несколькими версиями ранее, чтобы повысить производительность записи (приложения для создания заметок часто генерируют большие объёмы данных за короткие промежутки времени), Чжан скорректировал конфигурацию SQLite:

storeDescription.setValue("WAL" as NSString, forPragmaNamed: "journal_mode")
storeDescription.setValue("PASSIVE" as NSString, forPragmaNamed: "wal_checkpoint")
storeDescription.setValue("100000000" as NSString, forPragmaNamed: "journal_size_limit")

Среди прочего, причиной медленной миграции стало включение пассивного режима wal_checkpoint.

WAL Mode и механизм контрольных точек

Режим WAL (Write-Ahead Logging) повышает производительность параллельного чтения и записи, записывая все изменения сначала в файлы логов WAL. Начиная с iOS 7, Core Data использует режим WAL по умолчанию.

Чтобы предотвратить бесконечное увеличение размера файлов WAL, SQLite необходимо периодически объединять данные с основной базой с помощью операций контрольных точек. Core Data по умолчанию автоматически выполняет эту операцию в подходящие моменты.

Настроенный Чжаном пассивный режим представлял потенциальные риски:

  • SQLite не проходил контрольные точки активно, объединяя WAL только при срабатывании других подключений.
  • Для однопроцессных мобильных приложений это означает, что механизм контрольных точек практически бесполезен.
  • Файлы WAL продолжали увеличиваться, и даже установка параметра journal_size_limit вряд ли сможет эффективно ограничить их размер.

Анализ цепочки проблем

  1. Пользователи накапливают данные с течением времени, файлы WAL неограниченно разрастаются до нескольких ГБ.
  2. Приложение запускается, Core Data необходимо сначала выполнить контрольную точку перед миграцией.
  3. Процесс слияния массивных данных WAL занимает чрезвычайно много времени, блокируя основной поток.
  4. Сторожевой таймер обнаруживает зависание основного потока и завершает работу приложения.

Нет абсолютно верного или неверного решения

Некоторые читатели могут подумать, что отсутствие настройки WAL позволит избежать этой проблемы. Действительно, настройки по умолчанию более универсальны и могут в значительной степени предотвратить подобные ситуации. Но для некоторых разработчиков действительно существуют особые требования. В случае Чжана, даже если бы он использовал PASSIVE режим, при условии регулярного выполнения операций слияния вручную в приложении, проблем бы не возникло. Ключевым моментом является четкое понимание и владение настройками, сценариями применения и областью влияния.

Этот инцидент напоминает нам: любая оптимизация должна быть тщательно реализована после оценки долгосрочных последствий. Для большинства приложений, использующих Core Data, я рекомендую:

  1. Отдать приоритет конфигурациям Core Data по умолчанию.
  2. Если требуются пользовательские настройки WAL:
    • Избегайте пассивного режима
    • Установите разумное ограничение на размер лога (например, 10–20 МБ)
    • Регулярно и активно выполняйте слияние контрольных точек
  3. Перенесите инициализацию базы данных в фоновые потоки, особенно в сценариях с большими объемами данных.
  4. Проведите тестирование пограничных случаях перед выпуском, чтобы убедиться в стабильности.

Заключение

Хотя большинство разработчиков, возможно, и не сталкиваются с подобными проблемами, я надеюсь, что благодаря этой публикации я смогу предоставить сообществу ценную информацию. Core Data — это не совсем «чёрный ящик» — главное, готовы ли мы исследовать и понимать механизмы его работы.

Хотя оптимизация производительности важна, стабильность всегда должна быть на первом месте — любые изменения конфигурации должны сопровождаться тщательным тестированием, чтобы гарантировать отсутствие неожиданных «цепных реакций».

Источник

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.
Telegram

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: