PHP 中 Session阻塞及記憶體回收 Redis共用Session

來源:互聯網
上載者:User

Session 和 Cookie 有什麼關係

Cookie 也是由於 HTTP 無狀態的特點而產生的技術。也被用於儲存訪問者的身份標識和一些資料。每次用戶端發起 HTTP 要求時,會將 Cookie 資料加到 HTTP header 中,提交給服務端。這樣服務端就可以根據 Cookie 的內容知道訪問者的資訊了。

可以說,Session 和 Cookie 做著相似的事情,只是 Session 是將資料儲存在服務端,通過用戶端提交來的 session_id 來擷取對應的資料;而 Cookie 是將資料儲存在用戶端,每次發起請求時將資料提交給服務端的。

上面提到,session_id 可以通過 URL 或 cookie 來傳遞,由於 URL 的方式比 cookie 的方式更加不安全且使用不方便,所以一般是採用 cookie 來傳遞 session_id。

服務端產生 session_id,通過 HTTP 報文發送給用戶端(比如瀏覽器),用戶端收到後按指示建立儲存著 session_id 的 cookie。cookie 是以 key/value 形式儲存的,看上去大概就這個樣子的:

    PHPSESSID=e4tqo2ajfbqqia9prm8t83b1f2

在 PHP 中,儲存 session_id 的 cookie 名稱預設叫作 PHPSESSID,這個名稱可以通過 php.ini 中 session.name 來修改,也可以通過函數 session_name() 來修改。

 

為什麼不推薦使用 PHP 內建的 files 型 Session 處理器

在 PHP 中,預設的 Session 處理器是 files,處理器可以使用者自己實現(參見:自訂會話管理器)。我知道的成熟的 Session 處理器還有很多:Redis、Memcached、MongoDB……為什麼不推薦使用 PHP 內建的 files 類型處理器,PHP 官方手冊中給出過這樣一段 Note:

    無論是通過調用函數 session_start() 手動開啟會話, 還是使用配置項 session.auto_start 自動開啟會話, 對於基於檔案的會話資料儲存(PHP 的預設行為)而言, 在會話開始的時候都會給會話資料檔案加鎖, 直到 PHP 指令碼執行完畢或者顯式調用 session_write_close() 來儲存會話資料。 在此期間,其他指令碼不可以訪問同一個會話資料檔案。

上述引用參見:Session 的基本用法

為了證明這段話,我們建立一下 2 個檔案:

檔案:session1.php

<?phpsession_start();sleep(5);var_dump($_SESSION);?>


檔案:session2.php

<?phpsession_start();var_dump($_SESSION);?>


在同一個瀏覽器中,先訪問 http://127.0.0.1/session1.php,然後在當前瀏覽器新的標籤頁立刻訪問 http://127.0.0.1/session2.php。實驗發現,session1.php 等了 5 秒鐘才有輸出,而 session2.php 也等到了將近 5 秒才有輸出。而單獨訪問 session2.php 是秒開的。在一個瀏覽器中訪問 session1.php,然後立刻在另外一個瀏覽器中訪問 session2.php。結果是 session1.php 等待 5 秒鐘有輸出,而 session2.php 是秒開的。

分析一下造成這個現象的原因:上面例子中,預設使用 Cookie 來傳遞 session_id,而且 Cookie 的範圍是相同。這樣,在同一個瀏覽器中訪問這 2 個地址,提交給伺服器的 session_id 就是相同的(這樣才能標記訪問者,這是我們期望的效果)。當訪問 session1.php 時,PHP 根據提交的 session_id,在伺服器儲存 Session 檔案的路徑(預設為 /tmp,通過 php.ini 中的 session.save_path 或者函數 session_save_path() 來修改)中找到了對應的 Session 檔案,並對其加鎖。如果不顯式調用 session_write_close(),那麼直到當前 PHP 指令碼執行完畢才會釋放檔案鎖。如果在指令碼中有比較耗時的操作(比如例子中的 sleep(5)),那麼另一個持有相同 session_id 的請求由於檔案被鎖,所以只能被迫等待,於是就發生了請求阻塞的情況。

既然如此,在使用完 Session 後,立刻顯示調用 session_write_close() 是不是就解決問題了哩?比如上面例子中,在 sleep(5) 前面調用 session_write_close()。

確實,這樣 session2.php 就不會被 session1.php 所阻塞。但是,顯示調用了 session_write_close() 就意味著將資料寫到檔案中並結束當前會話。那麼,在後面代碼中要使用 Session 時,必須重新調用 session_start()。

例如:

<?phpsession_start();$_SESSION['name'] = 'Jing';var_dump($_SESSION);session_write_close();sleep(5);session_start();$_SESSION['name'] = 'Mr.Jing';var_dump($_SESSION);?>



官方給出的方案:

    對於大量使用 Ajax 或者並發請求的網站而言,這可能是一個嚴重的問題。 解決這個問題最簡單的做法是如果修改了會話中的變數, 那麼應該儘快調用 session_write_close() 來儲存會話資料並釋放檔案鎖。 還有一種選擇就是使用支援並行作業的會話儲存管理器來替代檔案會話儲存管理器。

我推薦的方式是使用 Redis 作為 Session 的處理器。

Session 資料是什麼時候被刪除的

這是一道經常被面試官問起的問題。

先看看官方手冊中的說明:

    session.gc_maxlifetime 指定過了多少秒之後資料就會被視為"垃圾"並被清除。 垃圾搜集可能會在 session 啟動的時候開始( 取決於 session.gc_probability 和 session.gc_divisor)。 session.gc_probability 與 session.gc_divisor 合起來用來管理 gc(garbage collection 記憶體回收)進程啟動的機率。此機率用 gc_probability/gc_divisor 計算得來。例如 1/100 意味著在每個請求中有 1% 的機率啟動 gc 進程。session.gc_probability 預設為 1,session.gc_divisor 預設為 100。

繼續用我上面那個不太恰當的比方吧:如果我們把物品放在超市的儲物箱中而不取走,過了很久(比如一個月),那麼保安就要清理這些儲物箱中的物品了。當然並不是超到期限了保安就一定會來清理,也許他懶,又或者他壓根就沒有想起來這件事情。

再看看兩段手冊的引用:

    如果使用預設的基於檔案的會話處理器,則檔案系統必須保持跟蹤訪問時間(atime)。Windows FAT 檔案系統不行,因此如果必須使用 FAT 檔案系統或者其他不能跟蹤 atime 的檔案系統,那就不得不想別的辦法來處理會話資料的記憶體回收。自 PHP 4.2.3 起用 mtime(修改時間)來代替了 atime。因此對於不能跟蹤 atime 的檔案系統也沒問題了。

    GC 的運行時機並不是精準的,帶有一定的或然性,所以這個設定項並不能確保舊的會話資料被刪除。某些會話儲存處理模組不使用此設定項。

對於這種刪除機制,我是存疑的。

比如 gc_probability/gc_divisor 設定得比較大,或者網站的請求量比較大,那麼 GC 進程啟動就會比較頻繁。

還有,GC 進程啟動後都需要遍曆 Session 檔案清單,對比檔案的修改時間和服務端的目前時間,判斷檔案是否到期而決定是否刪除檔案。

這也是我覺得不應該使用 PHP 內建的 files 型 Session 處理器的原因。而 Redis 或 Memcached 天生就支援 key/value 到期機制的,用於作為會話處理器很合適。或者自己實現一個基於檔案的處理器,當根據 session_id 擷取對應的單個 Session 檔案時判斷檔案是否到期。

 
為什麼重啟瀏覽器後 Session 資料就取不到了

    session.cookie_lifetime 以秒數指定了發送到瀏覽器的 cookie 的生命週期。值為 0 表示"直到關閉瀏覽器"。預設為 0。

其實,並不是 Session 資料被刪除(也有可能是,機率比較小,參見上一節)。只是關閉瀏覽器時,儲存 session_id 的 Cookie 沒有了。也就是你弄丟了開啟超市儲物箱的鑰匙(session_id)。

同理,瀏覽器 Cookie 被手動清除或者其他軟體清除也會造成這個結果。

為什麼瀏覽器開著,我很久沒有操作就被登出了

這個是稱為“防呆”,為了保護使用者賬戶安全的。

這個小節放進來,是因為這個功能的實現可能和 Session 的刪除機制有關(之所以說是可能,是因為這個功能不一定要借住 Session 實現,用 Cookie 也同樣可以實現)。

說簡單一點,就是長時間沒有操作,服務端的 Session 檔案到期被刪除了。

 
一個有意思的事情

在我實驗的過程中,發現了小有意思的事情:我把 GC 啟動的機率設定為 100%。如果只有一個訪問者請求,該訪問者即使過了很久(超過了到期時間)後才發起第二次請求,那麼 Session 資料也還是存在的('session.save_path' 目錄下面的 Session 檔案存在)。是的,明明就超過了到期時間,卻沒有被 GC 刪除。這時,我用另外一個瀏覽器訪問時(相對於另一個訪問者),這次請求產生了新的 Session 檔案,而上一個瀏覽器請求產生的那個 Session 檔案終於沒有了(之前那個 Session 檔案在 'session.save_path' 目錄下面的消失了)。

還有,發現 Session 檔案被刪除後,再次請求,還是會產生和之前檔案名稱相同的 Session 檔案(因為瀏覽器並沒有關閉,再次請求發送的 session_id 是相同的,所以重建的 Session 檔案的檔案名稱還是一樣的)。但是,我不理解的是:這個重新出現的檔案的建立時間竟然是第一次的那個建立時間,難道它是從資源回收筒中回來的?(確實,我做這個實驗時是在 window 下進行的)

我猜測的原因是這樣:當啟動會話後,PHP 根據 session_id 找到並開啟了對應的 Session 檔案,然後才啟動 GC 進程。GC 進程就只檢查除了當前這個 Session 檔案外的其他檔案,發現到期的就幹掉。所有,即使當前這個 Session 檔案已經到期了,GC 也沒有刪除它。

我認為這個不合理的。

由於發生這種情況影響也不大(畢竟線上請求很多,當前請求的到期檔案被其他請求喚起的 GC 幹掉的可能性是比較大的),我沒有信心去看 PHP 原始碼,我並不線上上使用 PHP 內建的 files 型 Session 處理器。所以,這個問題我就沒有深入研究了,請諒解。

<?php// 到期時間設定為 30 秒ini_set('session.gc_maxlifetime', '30');// GC 啟動機率設定為 100%ini_set('session.gc_probability', '100');ini_set('session.gc_divisor', '100');session_start();$_SESSION['name'] = 'Jing';var_dump($_SESSION);?>





如何設定一個嚴格30分鐘到期的Session

第一種回答

那麼, 最常見的一種回答是: 設定Session的到期時間, 也就是session.gc_maxlifetime, 這種回答是不正確的, 原因如下:

1. 首先, 這個PHP是用一定的機率來運行session的gc的, 也就是session.gc_probability和session.gc_divisor(介紹參看 深入理解PHP原理之Session Gc的一個小機率Notice), 這個預設的值分別是1和100, 也就是有1%的機會, PHP會在一個Session啟動時, 運行Session gc. 不能保證到30分鐘的時候一定會到期.

2. 那設定一個大機率的清理機會呢? 還是不妥, 為什麼? 因為PHP使用stat Session檔案的修改時間來判斷是否到期, 如果增大這個機率一來會降低效能, 二來, PHP使用”一個”檔案來儲存和一個會話相關的Session變數, 假設我5分鐘前設定了一個a=1的Session變數, 5分鐘後又設定了一個b=2的Seesion變數, 那麼這個Session檔案的修改時間為添加b時刻的時間, 那麼a就不能在30分鐘的時候, 被清理了. 另外還有下面第三個原因.

3. PHP預設的(Linux為例), 是使用/tmp 作為Session的預設儲存目錄, 並且手冊中也有如下的描述:

    Note: 如果不同的指令碼具有不同的 session.gc_maxlifetime 數值但是共用了同一個地方儲存會話資料,則具有最小數值的指令碼會清理資料。此情況下,與 session.save_path 一起使用本指令。

也就是說, 如果有倆個應用都沒有指定自己獨立的save_path, 一個設定了到期時間為2分鐘(假設為A), 一個設定為30分鐘(假設為B), 那麼每次當A的Session gc啟動並執行時候, 就會同時刪除屬於應用B的Session files.

所以, 第一種答案是不”完全嚴格”正確的.

第二種答案

還有一種常見的答案是: 設定Session ID的載體, Cookie的到期時間, 也就是session.cookie_lifetime. 這種回答也是不正確的, 原因如下:

這個到期只是Cookie到期, 換個說法這點就考察Cookie和Session的區別, Session到期是伺服器到期, 而Cookie到期是用戶端(瀏覽器)來保證的, 即使你設定了Cookie到期, 這個只能保證標準瀏覽器到期的時候, 不會發送這個Cookie(包含著Session ID), 而如果通過構造請求, 還是可以使用這個Session ID的值.

第三種答案

使用memcache, redis等, okey, 這種答案是一種正確答案. 不過, 很顯然出題者肯定還會接著問你, 如果只是使用PHP呢?

第四種答案

當然, 面試不是為了難道你, 而是為了考察思考的周密性. 在這個過程中我會提示出這些陷阱, 所以一般來說, 符合題意的做法是:

1. 設定Cookie到期時間30分鐘, 並設定Session的lifetime也為30分鐘.
2. 自己為每一個Session值增加Time stamp.
3. 每次訪問之前, 判斷時間戳記.

最後, 有同學問, 為什麼要設定30分鐘的到期時間: 這個, 首先這是為了面試, 第二, 實際使用情境的話, 比如30分鐘就到期的優惠??



為什麼不能用memcached儲存Session

Titas Norkūnas是DevOps諮詢服務提供者Bear Mountain的聯合創始人。由於看到Ruby/Rails社區忽略了Dormando那兩篇文章所指出的問題,所以他近日撰文對此進行了進一步的闡述。他認為問題的根本在於,memcached是一個設計用於快取資料而不是儲存資料的系統,因此不應該用於儲存Session。

對於Dormando的那兩篇文章,他認為第一篇文章給出的原因很容易理解,而人們經常會對第二篇文章給出的原因認識不足。因此他對這個原因進行了詳細地闡述:

    Memcached使用“最近最少使用(LRU)”演算法回收緩衝。但memcached的LRU演算法針對每個slab類執行,而不是針對整體。

    這意味著,如果所有Session的大小大致相同,那麼它們會分成兩三個slab類。所有其它大小大致相同的資料也會放入同一些slab,與Session爭用儲存空間。一旦slab滿了,即使更大的slab中還有空間,資料也會被回收,而不是放入更大的slab中……在特定的slab中,Session最老的使用者將會掉線。使用者將會開始隨機掉線,而最糟糕的是,你很可能甚至都不會注意到它,直至使用者開始抱怨……

另外,Norkūnas提到,如果Session中增加了新資料,那麼Session變大也可能會導致掉線問題出現。

有人提出將Session和其它資料分別使用單獨的memcached緩衝。不過,由於memcached的LRU演算法是局部的,那種方式不僅導致記憶體使用量率不高,而且也無法消除使用者因為Session回收而出現隨機掉線的風險。

如果讀者非常希望藉助memcached提高Session讀取速度,那麼可以借鑒Norkūnas提出的memcached+RDBMS(在有些情況下,NoSQL也可以)的模式:

    當使用者登入時,將Session “set”到memcached,並寫入資料庫;
    在Session中增加一個欄位,標識Session最後寫入資料庫的時間;
    每個頁面載入的時候,優先從memcached讀取Session,其次從資料庫讀取;
    每載入N頁或者Y分鐘後,再次將Session寫入資料庫;
    從資料庫中擷取到期Session,優先從memcached中擷取最新資料。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.