- Struktur berbasis file SQLite memang sederhana, tetapi saat beberapa operasi tulis berjalan bersamaan, konflik penguncian (locking) dapat terjadi
- Jellyfin telah lama menggunakan SQLite, namun pada beberapa sistem muncul masalah aplikasi berhenti karena kesalahan database terkunci selama transaksi
- Dengan memanfaatkan fitur interceptor di EF Core, mereka menerapkan tiga strategi penguncian (No-Lock, Optimistic, Pessimistic) untuk meredakan masalah
- Pendekatan Optimistic meminimalkan penurunan performa lewat mekanisme retry, sementara pendekatan Pessimistic meningkatkan stabilitas dengan konsekuensi penurunan kecepatan
- Pendekatan ini memiliki struktur yang mudah diterapkan juga pada aplikasi EF Core lainnya, sehingga menawarkan alternatif praktis untuk mengatasi masalah konkurensi SQLite
Struktur dasar dan batasan SQLite
- SQLite adalah mesin basis data relasional berbasis file yang berjalan di dalam aplikasi
- Semua data disimpan dalam satu file, sehingga tidak memerlukan aplikasi server terpisah
- Karena satu file tersebut sepenuhnya dikelola oleh aplikasi, ada risiko benturan jika beberapa proses mengaksesnya secara bersamaan
- Karena itu, aplikasi yang menggunakan SQLite harus memastikan hanya ada satu operasi tulis yang berjalan pada saat yang sama
Mode Write-Ahead-Log (WAL)
- SQLite meredakan batasan konkurensi melalui fitur WAL (Write-Ahead-Log)
- File WAL berperan sebagai file jurnal yang mencatat perubahan pada basis data
- Beberapa operasi tulis dapat diantrekan secara paralel, lalu perubahan dalam WAL diterapkan saat pembacaan
- Namun WAL juga tidak sempurna, dan dalam situasi tertentu konflik penguncian tetap terjadi
Masalah transaksi di SQLite
- Transaksi bertugas menjamin atomisitas operasi perubahan dan mengendalikan pemblokiran pembacaan
- Pada sebagian sistem Jellyfin, saat transaksi berlangsung, SQLite mengembalikan error “database is locked” lalu langsung berhenti
- Masalah ini dilaporkan terlepas dari sistem operasi, kecepatan disk, maupun apakah berjalan di lingkungan virtualisasi
- Karena sulit direproduksi dan muncul tidak teratur, akar penyebabnya sulit diidentifikasi
Cara Jellyfin menggunakan SQLite dan titik masalahnya
- Pada lingkungan yang direkomendasikan (storage non-jaringan, SSD), masalah jarang muncul, tetapi karena bug pembatasan pekerjaan paralel di versi sebelum 10.11
- pekerjaan pemindaian library berjalan terlalu paralel sehingga memicu ribuan permintaan tulis secara bersamaan
- hal ini melampaui batas retry dan timeout milik mesin SQLite, sehingga menyebabkan beban berlebih pada database dan error
- Transaksi yang panjang dan query yang tidak efisien juga memperburuk keadaan
Solusi berbasis EF Core
- Saat Jellyfin memigrasikan codebase ke EF Core, kontrol yang lebih terstruktur menjadi memungkinkan
- Dengan memanfaatkan Interceptor di EF Core, semua perintah dan eksekusi transaksi dapat dicegat untuk menerapkan kontrol penguncian yang transparan
- Tiga strategi penguncian diperkenalkan
- No-Lock: mode dasar, tanpa penguncian tambahan. Digunakan di sebagian besar kasus untuk mencegah penurunan performa
- Optimistic Locking: jika gagal, dilakukan retry menggunakan library Polly
- Pessimistic Locking: sebelum semua operasi tulis, seluruh basis data dikunci dengan ReaderWriterLockSlim
Cara kerja Optimistic Locking
- Pendekatan ini mengasumsikan operasi akan berhasil, lalu melakukan retry jika terjadi kegagalan
- Jika dua operasi tulis saling bentrok, salah satunya gagal lalu menunggu sejenak sebelum mencoba lagi
- Dengan menggunakan library Polly, hanya kegagalan akibat penguncian yang diperlakukan sebagai target retry
- Dibanding pendekatan Pessimistic, cara ini memiliki overhead lebih kecil dan kehilangan performa yang lebih rendah
Cara kerja Pessimistic Locking
- Pada setiap operasi tulis, seluruh basis data dikunci
- Selama penulisan, semua operasi baca dan tulis lain akan diblokir
- Pendekatan ini paling stabil, tetapi juga paling lambat
- Misalnya, walaupun secara teori dimungkinkan menulis ke tabel “Bob” saat membaca tabel “Alice”, pendekatan ini tetap tidak mengizinkannya
- Dengan ReaderWriterLockSlim, banyak pembacaan diperbolehkan, tetapi hanya satu penulisan yang diizinkan
Rencana berikutnya: Smart Locking
- Sedang dipertimbangkan penerapan Smart Locking yang menggabungkan pendekatan Optimistic dan Pessimistic
- Tujuannya adalah menggabungkan keunggulan keduanya untuk menyeimbangkan performa dan stabilitas
Hasil dan kemungkinan penerapan
- Hasil pengujian awal menunjukkan kedua mode penguncian efektif dalam mengatasi masalah
- Walau akar penyebab mendasarnya masih belum jelas, pengguna kini memiliki opsi untuk menggunakan Jellyfin dengan lebih stabil
- Di internet juga banyak laporan error serupa, tetapi belum ada solusi yang benar-benar tuntas
- Implementasi Jellyfin memiliki struktur berbasis interceptor EF Core yang mudah disalin dan diterapkan
- Pemanggil tidak perlu menyadari bagaimana mekanisme penguncian bekerja di dalam
- Ini juga bisa langsung dimanfaatkan pada aplikasi EF Core lain yang mengalami masalah konkurensi SQLite yang sama
2 komentar
Komentar Hacker News
Dulu pernah mengalami masalah blocking di SQLite, dan penyebabnya ternyata fragmentasi disk
Pengguna yang memakai aplikasi di tablet Android lama selama bertahun-tahun, 8 jam sehari, mulai mengeluhkan perlambatan dan error lock
Saat data disalin dan dikirim untuk diperiksa, masalahnya tidak bisa direproduksi, tetapi setelah perangkatnya diterima langsung dan dicek, ternyata masalah hilang sepenuhnya setelah DB file di-"defrag" dengan cara menyalinnya ke lokasi baru lalu mengembalikannya ke nama semula
Peningkatan performa yang sama juga dialami pada DB Jellyfin
Transaksi SQLite secara default dimulai dalam mode "deferred"
Artinya, write lock tidak diambil sampai benar-benar mencoba melakukan operasi tulis
Error
SQLITE_BUSYterjadi saat transaksi baca mencoba berubah menjadi tulis, tetapi transaksi lain sudah lebih dulu memegang write lockSolusinya adalah mengatur
busy_timeoutdan memulai transaksi yang mencakup penulisan dalam mode "immediate"Penjelasan terkait dirangkum dengan baik di posting blog ini
SQLITE_BUSY. Saya mengumpulkan kasus-kasus terkait di siniSQLITE_BUSYsebagai semacam bau arsitektur. Dalam mode WAL, saya memisahkan connection pool read-only dan connection pool tulis tunggal. Dengan begitu, status kepemilikan lock bisa dipahami dengan jelas dan situasi kontensi bisa dirancang sejak awalbusy_timeouttidak berlaku dalam kasus ini. Dalam mode WAL, page ditambahkan ke satu file log, jadi jika mencoba berubah dari baca ke tulis saat sedang membaca, SQLite langsung gagal demi menjaga jaminan serialisasi. Mode "immediate" mencegah hal ituSQLITE_BUSY, mungkin karena konfigurasinya belum diaturSepertinya sebagian penjelasan di artikel itu keliru
SQLite mengelola lock sendiri, jadi aplikasi tidak perlu mengendalikan akses file secara langsung
Selain itu, WAL tidak memungkinkan beberapa penulisan paralel. WAL hanya memungkinkan pembacaan dan satu penulisan berjalan bersamaan
SQLite adalah database yang luar biasa, tetapi sayang nilai default-nya terlalu konservatif
Untuk benar-benar dipakai di layanan produksi, berbagai pengaturan PRAGMA perlu disesuaikan
Begitu fitur baru hctree di SQLite sudah stabil, saya berencana hanya memakai SQLite
hcpada namanya kemungkinan singkatan dari High ConcurrencyTautan dokumentasi resmi
Melihat tulisan seperti ini, terasa seperti hanya berhenti pada solusi tambal sulam, bukan analisis akar masalah
Rasanya akan lebih bernilai jika penyebab pastinya diungkap lewat debugging dan riset yang lebih dalam
Sepertinya penulis juga belum memahami bahwa mode WAL pada akhirnya tetap merupakan struktur single writer, multiple readers
Penulisan paralel tidak mungkin dilakukan; WAL hanya mencegah transaksi baca diblokir oleh penulisan
Akan bagus kalau ada MVCC penuh, tetapi struktur saat ini juga bekerja sangat baik asalkan prinsipnya dipahami
Saya juga pernah mengalami masalah serupa di Jellyfin
Biasanya berjalan normal, tetapi dalam kondisi tertentu DB menjadi macet dalam keadaan terkunci
Log hanya menyisakan pesan "database is locked", dan akhirnya harus me-restart container Docker agar pulih
Ini biasanya terjadi saat beberapa tombol di UI TV ditekan cepat secara berurutan
Sedikit topik lain, tetapi jika memakai in-memory DB SQLite untuk operasi insert/delete dalam jumlah besar, penggunaan memori terus meningkat
Misalnya jika 100 ribu baris dimasukkan lalu dihapus setiap 5 menit selama beberapa hari, memori di macOS bisa naik sampai 1GB
Penasaran apakah ada pengaturan yang layak disesuaikan untuk kasus seperti ini
auto_vacuumaktifDokumentasi VACUUM
SQLite memang bagus, tetapi melihat masalah seperti ini kadang terasa lebih baik langsung memakai Postgres
Jika bukan demi portabilitas file tunggal atau penggunaan embedded, Postgres menangani masalah konkurensi dengan lebih sederhana
Hah? Ada bagian yang bikin bingung, jadi saya langsung cek komentar dulu, dan seperti dugaan...