Sessions: Kondisi balapan di FilesystemStore

Dibuat pada 11 Jun 2013  ·  23Komentar  ·  Sumber: gorilla/sessions

Ada kondisi balapan di FilesystemStore yang ingin saya perbaiki tetapi saya ingin masukan Anda sebelum saya melanjutkan dan melakukannya. Pada dasarnya masalahnya adalah jika Anda memiliki permintaan bersamaan dari pengguna yang sama (sesi yang sama), hal berikut ini dimungkinkan:

  1. Permintaan 1 membuka sesi untuk melakukan operasi semi panjang
  2. Request 2 membuka sesi
  3. Permintaan 2 Menghapus data sesi untuk melakukan "keluar" atau serupa
  4. Minta 2 kali penyimpanan
  5. Minta 1 penyimpanan, yang membuatnya seolah-olah sesi tidak pernah keluar

Saya telah menambahkan kasus uji untuk kekurangan ini di cless / session @ f84abeda17de0b4fcd72d277412f3d3192f206f2

Cara paling mudah untuk memperbaikinya adalah dengan memperkenalkan kunci di tingkat sistem file. Namun golang tidak memiliki cara lintas platform untuk melakukan penguncian file. Itu mengekspos flock dalam syscall tetapi itu hanya berfungsi jika OS mendukungnya. Saya percaya perilaku flok mungkin juga berbeda pada unix yang berbeda meskipun saya tidak yakin bahwa ini masalahnya. Masalah lain dengan flock adalah bahwa flock mungkin tidak berfungsi di NFS.

Solusi yang sepenuhnya berbeda adalah menyimpan peta kunci di objek FilesystemStore itu sendiri. Ini memiliki kelemahan lain: Anda tidak dapat memiliki beberapa proses mengakses sesi sistem file yang sama dan Anda tidak dapat membuat banyak penyimpanan untuk sesi sistem file yang sama dalam satu aplikasi. Namun, kedua hal ini sudah tidak mungkin dilakukan tanpa menimbulkan masalah.

Pada akhirnya, saya pikir solusi terbaik adalah menyimpan peta kunci di objek penyimpanan karena semua kerugian dalam skenario itu dapat didokumentasikan dengan benar dan Anda dapat membalas dengan perilaku yang sama di berbagai sistem.

Backend penyimpanan lain yang didasarkan pada FilesystemStore mungkin menyalin cacat ini (Saya melihat masalah ini saat meninjau kode Redistore untuk proyek saya boj / redistore # 2)

bug stale

Semua 23 komentar

Alih-alih mengunci, bagaimana dengan sistem berbasis transaksi? Objek Session dapat membawa bidang lastModified. Ketika Anda mencoba untuk menyimpan sesi kembali ke penyimpanan sesi itu akan mengembalikan kesalahan yang menunjukkan itu telah dimodifikasi sejak terakhir dibaca. Programmer kemudian dapat memilih untuk mencoba lagi dengan mendapatkan Sesi lagi.

Itu bisa berhasil, memiliki keuntungan bahwa tidak ada kunci yang perlu dibersihkan dan tidak ada kemungkinan kebuntuan. Saya tidak yakin bahwa pengembang selalu dapat melakukan percobaan ulang yang berarti tergantung pada apa yang telah dilakukan permintaan. Saya pikir penguncian jelas merupakan opsi yang lebih aman, tetapi transaksi pasti dapat berfungsi jika API secara jelas mendokumentasikan kemungkinan kegagalan dan jika pengembang dengan rajin menangani kesalahan tersebut.

Meskipun demikian, meskipun FilesystemStore menggunakan transaksi, menurut saya API harus disiapkan untuk backend penyimpanan yang _do_ menggunakan penguncian, tetapi itu berarti tidak mengizinkan panggilan session.Save() lebih dari sekali _atau_ memperkenalkan session.Release() . Mana yang Anda pilih, atau Anda lebih suka tidak ada backend penyimpanan yang menggunakan penguncian?

Menyusul masalah ini karena berdampak pada RediStore. Terima kasih atas informasinya @cless

Saya tidak benar-benar memiliki preferensi apa pun saat ini selain melakukan hal yang benar dalam jangka panjang :) Tentu saja saya juga ingin mempertahankan API yang ada. Juga menambahkan kunci di semua tempat memperkenalkan banyak kerumitan dan overhead tambahan, yang juga sebaiknya dihindari.

Apakah Anda memiliki contoh framework sesi lain yang menyelesaikan masalah ini? Saya ingin tahu apa yang mereka lakukan.

Saya tahu penangan sesi php default menggunakan penguncian berbasis sistem file (di tarball php 5.4 saya baru saja memeriksa ini ada di file ext/session/mod_files.c . Ini menggunakan flock yang disediakan oleh ext/standard/flock_compat.c ).

Saya tidak yakin apakah php adalah contoh yang baik mengingat reputasinya, tapi saya khawatir itu satu-satunya yang saya sadari saat ini. Saya akan mencoba melihat-lihat untuk melihat apakah saya dapat menemukan beberapa kerangka kerja lain dan melihat bagaimana mereka menyelesaikan masalah.

Saya ingin tahu bagaimana Flask, Pyramid, atau Django menangani hal-hal ini. Saya akan memeriksanya jika saya punya waktu. Rails juga akan menarik jika ada yang akrab dengan basis kode itu.

Baik Pyramid dan Flask tampaknya menggunakan cookie untuk menyimpan data, saya curiga keduanya tidak menangani kondisi balapan. Saya hanya membaca dokumentasinya secara singkat jadi saya bisa saja salah.

Django mendukung data sesi sisi server jadi saya akan melihat kode itu sebentar lagi.

Bawaan sesi Django baku adalah basisdata dan ini menggunakan lapisan abstraksi basisdata Django yang tidak saya kenal sehingga sulit bagi saya untuk menafsirkan. Namun, saya menemukan komentar ini di backend sistem file:

        # Write the session file without interfering with other threads
        # or processes.  By writing to an atomically generated temporary
        # file and then using the atomic os.rename() to make the complete
        # file visible, we avoid having to lock the session file, while
        # still maintaining its integrity.
        #
        # Note: Locking the session file was explored, but rejected in part
        # because in order to be atomic and cross-platform, it required a
        # long-lived lock file for each session, doubling the number of
        # files in the session storage directory at any given time.  This
        # rename solution is cleaner and avoids any additional overhead
        # when reading the session data, which is the more common case
        # unless SESSION_SAVE_EVERY_REQUEST = True.
        #
        # See ticket #8616.

Ini sepertinya menyarankan mereka memastikan konten file sesi tidak pernah rusak tetapi tidak ada kondisi balapan yang dapat terjadi. Jika ada yang memiliki pengalaman Django akan menyenangkan melihat kasus percobaan seperti cless / session @ f84abed
Stackoverflow juga tampaknya mengindikasikan Django mungkin memiliki kondisi balapan dalam sesi: http://stackoverflow.com/search?q=django+session+race+condition

Baiklah, FileSystemStore sudah memiliki mutex berbutir kasar untuk mencegah kerusakan penyimpanan sesi. Saya tidak yakin berapa banyak perlindungan tambahan yang layak dimasukkan ke perpustakaan karena itu akan menambah banyak biaya tambahan untuk setiap permintaan, hanya untuk membuat segalanya lebih konsisten untuk beberapa permintaan. Tentu saja skenario di sini layak, tetapi saya pikir ini mungkin terlalu rumit untuk ditangani secara umum. Saya dapat melihat ada banyak kasus di mana apa yang terjadi sekarang benar-benar baik-baik saja, sedangkan dalam beberapa aplikasi bisa menjadi masalah. Saya menduga sebagian besar waktu Anda hanya akan memiliki satu permintaan dalam penerbangan untuk sesi tertentu.

Sungguh ini adalah masalah yang ada di sumber daya web apa pun, hal yang sama akan terjadi jika Anda memiliki GET diikuti oleh PUT berdasarkan informasi, hal-hal mungkin telah berubah untuk sementara.

Bagaimanapun, itu adalah pemikiran saya saat ini tetapi saya senang untuk berdiskusi lebih lanjut.

Tampaknya dalam jangka panjang ini lebih merupakan masalah domain aplikasi.

Mengingat skenario cless diposting, jika Permintaan 1 diharapkan berjalan cukup lama sehingga Permintaan 2 dapat terjadi secara paralel (katakanlah, Aplikasi Halaman Tunggal yang ditulis dalam Angular.js, atau aplikasi Node.js asinkron yang menjalankan beberapa API), tampaknya permintaan yang berjalan lama harus dipisahkan dari sesi dan dianggap sebagai proses pekerja independen pada saat itu.

Tentu saja tidak satu pun dari ini secara langsung membahas masalah, tetapi seperti yang disebutkan kisielk, melakukan segala jenis penguncian menambahkan banyak overhead untuk apa yang tampaknya merupakan kasus marjinal.

berikut adalah beberapa contoh dunia nyata dari dampak kondisi balapan sesi. Bug sulit untuk di-debug, muncul secara acak dan mengganggu baik pengembang maupun pengguna. Mengingat aplikasi yang cukup besar yang membuat penggunaan ajax ekstensif, kondisi balapan cepat atau lambat akan muncul.
http://www.hiretheworld.com/blog/tech-blog/codeigniter-session-race-conditions
http://www.chipmunkninja.com/Troubles-with-Asynchronous-Ajax-Requests-g@

Saya telah membaca seluruh utas di EllisLab / CodeIgniter # 1746 yang menangani masalah serupa, tetapi masalah mereka diperumit oleh fakta bahwa CodeIgniter secara paksa membuat ulang id sesi sesekali dan sebagian besar diskusi di sana berkisar pada fakta ini .

Saya memahami keengganan Anda untuk mengubah sesi dengan cara yang tidak kompatibel atau untuk memperkenalkan perbedaan halus dalam API yang mungkin merusak aplikasi yang sudah ada. Namun, saya masih berpikir harus ada beberapa penguncian opsional yang terlibat. Bagaimana dengan menambahkan dua fungsi ke API toko seperti ini:

type Store interface {
    Get(r *http.Request, name string) (*Session, error)
    New(r *http.Request, name string) (*Session, error)
    Save(r *http.Request, w http.ResponseWriter, s *Session) error
    Lock(r *http.Request, name string) error
    Release(r *http.Request, name string) error
}

Penggunaan fungsi ini akan sepenuhnya opsional (meskipun saya pribadi akan mendorong penguncian di setiap permintaan yang akan menulis ke sesi) dan tidak berdampak pada aplikasi yang ada.

Ada masalah dengan antarmuka penguncian yang diusulkan: jika kunci ditahan di database seperti MySQL dan koneksi database Anda terputus setelah Anda memperoleh kunci, maka Anda tidak akan pernah bisa melepaskannya dan sesi Anda secara efektif menemui jalan buntu. Kunci harus kedaluwarsa dalam beberapa cara, tetapi saya tidak sepenuhnya yakin bagaimana itu harus diungkapkan kepada pengembang.

Maaf saya melewatkan balasan Boj ketika saya memposting milik saya:
Penting untuk diingat bahwa permintaan jangka panjang bukanlah persyaratan untuk memicu kondisi balapan. Tidur 500 ms dalam kasus pengujian saya hanya untuk memastikan bahwa kondisi balapan dipicu. Di dunia nyata, kondisi balapan ini juga akan terjadi untuk permintaan "singkat", hanya lebih jarang, dan itulah yang membuatnya sulit untuk di-debug.

Anda juga harus ingat bahwa di bawah beban tinggi, bahkan permintaan singkat mungkin memerlukan waktu untuk dieksekusi.

@less Poin bagus.

Saya suka perubahan antarmuka yang Anda usulkan.

Kedaluwarsa kunci Anda akan relatif mudah diterapkan untuk RediStore, Redis memiliki perintah EXPIRE yang memungkinkan TTL disetel untuk kunci.

Bukankah penguncian mendetail seperti ini masih cukup tidak efisien untuk banyak jenis penyimpanan sesi? Dan kedaluwarsa juga menjadi masalah.

Tampaknya bagian dari masalahnya adalah bahwa Save dapat berhasil bahkan jika sesi lebih lama ada, bukankah itu tidak akan membantu menyelesaikan masalah jika bukan itu masalahnya?

@ Kisielk , dapatkah Anda memberikan contoh toko yang tidak efisien? Sebagian besar database nilai-kunci memiliki beberapa bentuk operasi atom yang memungkinkan Anda membuat kunci. Database SQL biasanya memiliki akses ke penguncian berbasis baris (meskipun saya harus mengatakan saya tidak terbiasa dengan detailnya di sini).
Metode penguncian penyimpanan nilai kunci seperti redis memerlukan beberapa perjalanan bolak-balik ke database, mungkin itu yang Anda maksud?
Ada masalah dengan penyimpanan sistem file karena go tidak memberi kami cara lintas platform untuk mengakses kunci file. Saya kira cgo akan memungkinkan Anda membuat satu yang mendukung platform utama, tetapi itu mungkin solusi yang akan saya hindari.

Saya tidak berpikir menyimpan ketika sesi tidak ada lagi adalah masalah nyata kecuali Anda berbicara tentang menghapus sesi saat ini dan menggantinya dengan sesi baru untuk mencegah fiksasi sesi, tetapi jujur ​​saja itu adalah masalah yang sama sekali berbeda.

@ Boj , masalahnya adalah programmer memerlukan beberapa cara untuk memverifikasi bahwa kunci belum kedaluwarsa sebelum dia mencoba untuk menyimpan. Berapa seharusnya waktu kedaluwarsa pada gembok?

Ini adalah salah satu cara yang mungkin untuk melakukannya, tetapi saya tidak yakin saya sangat menyukainya. Anda bergantung pada jam yang tidak melompat maju atau mundur secara tiba-tiba dan itu bukan asumsi yang aman untuk dibuat:

func Lock(expiration time.Duration) time.Time {
    // Block here until the lock becomes your. Set the lock to expire after
    // dur+50ms. This extra 50 ms ensures that you never assume you own an
    // expired lock
    return time.Now().Add(expiration)
}

func main() {
    end := Lock(1000 * time.Millisecond)

    // Do your thing here ...
    // time.Sleep(1100 * time.Millisecond)

    if time.Now().After(end) {
        fmt.Println("Lock expired, throw an error")
    } else {
        fmt.Println("Good to go, save the session")
    }
}

@cless Seluruh komentar Anda meringkas apa yang tampaknya menjadi kekhawatiran @kisielk . X melakukan ini, Y melakukan itu, Z mungkin tidak mungkin kecuali beberapa hal ekstrem terlibat. Meskipun kondisi balapan tidak diinginkan mengingat kasus yang jarang terjadi, setidaknya desain gorila / sesi saat ini sangat sederhana dan mengikuti elemen filosofi yang paling tidak mengejutkan.

Ini bisa sangat mudah untuk mengimplementasikan bagian antarmuka dari gorilla / sesi seperti yang Anda usulkan, namun, Anda membuat skenario di mana Anda mungkin atau mungkin tidak dapat dengan mudah menukar backend kecuali mereka kebetulan mengimplementasikan Lock / Release dengan cara yang sama mudahnya. dan cara yang efisien. Tampaknya terlalu banyak bagaimana jika berkenaan dengan apa yang Anda usulkan, yang paling utama adalah "dapatkah backend menerapkan ini tanpa menggunakan pemrograman hackish?", Diikuti oleh "Saya mencoba menukar dari FileSystemStore ke FooBarStore, tetapi tidak tampaknya menerapkan Lock / Release dan program saya tidak berfungsi seperti yang diharapkan. "

Ada artikel bagus tentang nuansa penguncian per sesi di sini:

http://thwartedefforts.org/2006/11/11/race-conditions-with-ajax-and-php-sessions/

Seperti yang telah kita simpulkan di sini, batas waktu untuk penguncian jika terjadi kesalahan adalah salah satu masalah utama yang mengimplementasikan skema semacam ini. Ini membutuhkan lebih banyak pemikiran dan sangat bergantung pada backend

Sejauh menerapkan Kunci / Rilis, kami dapat membuat penerapan kunci per sesi opsional dengan memiliki antarmuka terpisah. Backend kemudian dapat diterapkan pada antarmuka itu dan jika tidak menerapkannya, kami dapat menyediakan implementasi default menggunakan kunci dalam memori.

Masalah lain tentu saja adalah bahwa pengguna perpustakaan perlu memperbarui kode mereka untuk menggunakan Kunci dan Rilis, tetapi saya kira tanpa itu mereka akan memiliki perilaku yang sama seperti yang mereka lakukan sekarang.

"kami dapat menyediakan implementasi default menggunakan kunci dalam memori"

Ini tidak akan berarti dalam lingkungan multi-server di mana permintaan diseimbangkan bebannya.

Saya suka ide untuk menyediakan antarmuka yang sama sekali berbeda untuk penguncian karena ini memungkinkan Anda lebih banyak kebebasan untuk menerapkannya dengan benar tanpa mengubah perilaku kode yang ada yang menggunakan sesi.

Seperti yang dicatat oleh @boj, Anda tidak dapat benar-benar memberikan kunci memori pada penyimpanan database dengan cara apa pun yang berarti. Beban menyediakan fungsionalitas kunci mungkin harus ditanggung oleh pengembang toko. Jika antarmuka kunci tidak ada, Anda masih dapat mengembalikan kesalahan saat sesi mencoba menggunakannya. Itu seharusnya tidak menjadi masalah, jika toko default menyediakan penguncian maka pengembang lain akan mengikutinya dan pengguna akan minimal, jika sama sekali, tidak nyaman.

Kunci memori masih dapat diterima di beberapa toko, dan menurut saya FilesystemStore adalah kandidat yang baik untuk mereka. Secara realistis Anda tidak akan menggunakan penyimpanan sistem file dalam situasi beban seimbang (meskipun secara teoritis mungkin dengan sistem file jaringan). Kunci sistem file tampaknya lebih disukai, tetapi tampaknya sulit untuk diterapkan lintas platform saat berjalan, dan selain itu, kunci sistem file juga tidak dijamin dapat berfungsi dengan NFS.

@boj : setuju, tetapi seperti yang @cless tunjukkan, itu berdasarkan jenis toko yang Anda miliki. FilesystemStore sudah mengandalkan RWMutex untuk mencegah modifikasi bersamaan dari penyimpanan sesi. Saya berharap sebagian besar backend akan benar-benar menerapkan antarmuka penguncian setidaknya pada akhirnya, tetapi memiliki default akan memungkinkan pengembang untuk merilis yang sesuai untuk lingkungan server tunggal untuk sementara. FilesystemStore akan menjadi salah satunya sampai situasi penguncian lintas platform di pustaka Go menjadi lebih baik.

Saya setuju sepenuhnya dengan @cless dan ingin menekankan pentingnya penguncian sesi dan platform dewasa (seperti PHP) menerapkannya di toko sesi mereka. Pada file menggunakan syscall 'flock' dan di memcache menggunakan nilai kembali dari operasi 'add' dan kunci tambahan dengan akhiran '.lock' dan set kedaluwarsa. Saya juga setuju dengan pendekatan @kisielk untuk memiliki antarmuka baru (memiliki fungsi 'Lock' dan 'Release' yang masuk akal. Bisakah kita meminta ini? Go harus menjadi bahasa yang digunakan untuk kinerja dan keandalan tinggi. Saya bersedia untuk menyumbangkan beberapa implementasi (FileSystem, Redis dan Memcache) sebagai contoh. Perhatikan bahwa kami memerlukan beberapa opsi konfigurasi baru:

  • spinLockWait: milidetik untuk tidur di antara upaya untuk mendapatkan kunci (default: 150)
  • lockMaxWait: detik untuk menunggu kunci (default: 30)
  • lockMaxAge: detik kunci dapat disimpan sebelum dilepaskan secara otomatis (default: lockMaxWait)

Masalah ini secara otomatis ditandai sebagai basi karena belum melihat pembaruan terkini. Ini akan ditutup secara otomatis dalam beberapa hari.

Apakah halaman ini membantu?
0 / 5 - 0 peringkat