使用 mcrouter 水平擴展 memcached

使用 mcrouter 水平擴展 memcached

使用任何語言開發高負載專案都需要特殊的方法和使用特殊的工具,但是當涉及到 PHP 應用程式時,情況會變得非常糟糕,例如,您必須開發 自己的應用伺服器。在本文中,我們將討論 memcached 中分散式會話儲存和資料快取的常見痛點,以及我們如何在一個「ward」專案中解決這些問題。

這次活動的主角是一個基於 symfony 2.3 框架的 PHP 應用程序,但該公司不打算對其進行更新。除了完全標準的會話儲存之外,該項目還充分利用了 “快取所有內容”策略 在 memcached 中:對資料庫和 API 伺服器的請求的回應、各種標誌、用於同步程式碼執行的鎖定等等。在這種情況下,memcached 故障對於應用程式的運行將是致命的。此外,快取遺失會導致嚴重後果:DBMS 開始崩潰,API 服務開始禁止請求等。情況可能需要幾十分鐘才能穩定下來,在此期間服務會變得非常慢或完全不可用。

我們需要提供 輕鬆水平擴展應用程式的能力即對原始程式碼進行最少的更改並完全保留功能。使快取不僅具有容錯能力,而且盡量減少資料遺失。

memcached 本身有什麼問題?

總的來說,PHP 的 memcached 擴充功能支援開箱即用的資料和會話的分散式儲存。一致性金鑰雜湊機制允許資料均勻分佈在許多伺服器上,將每個特定金鑰唯一地定位到群組中的特定伺服器,並且內建的故障轉移工具可確保快取服務的高可用性(但不幸的是, 沒有數據).

會話儲存的情況稍微好一點:你可以配置 memcached.sess_number_of_replicas這樣,資料就會同時保存在多個伺服器上,如果一個 memcached 實例發生故障,資料就會從其他實例傳回。但是,如果伺服器在沒有資料的情況下重新上線(通常在重新啟動後發生),則某些金鑰將重新指派給它。事實上,這意味著 會話資料遺失因為如果發生遺失,就不可能「轉到」另一個副本。

標準庫工具主要針對 水平的 可擴展性:它們允許快取增長到巨大的規模,並可以從託管在不同伺服器上的程式碼進行存取。但在我們的情況中,儲存的資料量不會超過幾GB,一兩個節點的效能就足夠了。因此,標準工具唯一有用的事情就是確保 memcached 的可用性,同時保持至少一個快取執行個體正常運作。然而,即使是這個機會也無法利用......這裡值得回顧一下專案中使用的框架的古老性,因此不可能使應用程式與伺服器集區一起工作。我們不要忘記會話資料的遺失:大量使用者退出導致客戶的眼睛抽搐。

理想情況下,需要 memcached 寫複製和副本遍歷 以防出現遺漏或錯誤。我們能夠在以下方面實施這項策略 麥克路由器.

麥克路由器

這是Facebook為了解決他們的問題而開發的memcached路由器。它支援 memcached 文字協議,允許 擴充 memcached 安裝 達到瘋狂的尺寸。關於mcrouter的詳細描述可以參見 在此公告中。除此之外 廣泛的功能 他可以做我們需要做的事:

  • 複製記錄;
  • 發生錯誤時,恢復到群組內的其他伺服器。

幹正事吧!

mcrouter配置

我將直接進入配置:

{
 "pools": {
   "pool00": {
     "servers": [
       "mc-0.mc:11211",
       "mc-1.mc:11211",
       "mc-2.mc:11211"
   },
   "pool01": {
     "servers": [
       "mc-1.mc:11211",
       "mc-2.mc:11211",
       "mc-0.mc:11211"
   },
   "pool02": {
     "servers": [
       "mc-2.mc:11211",
       "mc-0.mc:11211",
       "mc-1.mc:11211"
 },
 "route": {
   "type": "OperationSelectorRoute",
   "default_policy": "AllMajorityRoute|Pool|pool00",
   "operation_policies": {
     "get": {
       "type": "RandomRoute",
       "children": [
         "MissFailoverRoute|Pool|pool02",
         "MissFailoverRoute|Pool|pool00",
         "MissFailoverRoute|Pool|pool01"
       ]
     }
   }
 }
}

為什麼有三個池?伺服器為何重複?讓我們弄清楚它是如何工作的。

  • 在這個設定中,mcrouter會根據請求命令來選擇將請求傳送到哪個路徑。那傢伙告訴他這件事 OperationSelectorRoute.
  • GET 請求傳遞給處理程序 RandomRoute,從數組中的物件中隨機選擇一個池或路由 children。該數組的每個元素又是一個處理程序 MissFailoverRoute,它將遍歷池中的每個伺服器,直到收到帶有資料的回應,然後將該回應傳回給客戶端。
  • 如果我們專門使用 MissFailoverRoute 擁有三個伺服器的池,那麼所有請求將首先到達第一個 memcached 實例,其餘實例將在沒有資料時以剩餘方式接收請求。這種方法將導致 清單中第一個伺服器負載過大,因此決定產生三個位址順序不同的池,並隨機選擇。
  • 所有其他請求(這是一筆記錄)都使用 AllMajorityRoute。該處理程序將請求發送到池中的所有伺服器並等待至少 N/2 + 1 個伺服器的回應。從使用 AllSyncRoute 對於寫入操作,必須放棄這種方法,因為這種方法需要來自 所有 群組伺服器 - 否則它將返回 SERVER_ERROR。雖然 mcrouter 會將資料放入可用的快取中,但呼叫 PHP 函數 將返回錯誤 並產生通知。 AllMajorityRoute 並不是那麼嚴格,並且允許您退役多達一半的單位,而不會出現上述問題。

主要缺點 方案是,如果快取中確實沒有數據,那麼對於來自客戶端的每個請求,實際上將執行 N 個對 memcached 的請求 - 這 對所有人 池中的伺服器。例如,可以將池中的伺服器數量減少到兩個:透過犧牲儲存可靠性,我們將獲得о更快的速度和更少的丟失密鑰請求負載。

NB:學習 mcrouter 的有用連結也可能是 維基百科中的文檔 и 專案問題 (包括封閉的),代表整個不同配置的寶庫。

建置並運行 mcrouter

我們的應用程式(以及 memcached 本身)在 Kubernetes 中運行,因此 mcrouter 也位於那裡。為了 貨櫃組裝 我們用 韋爾夫,其配置如下:

NB:文章中給出的清單已發佈在儲存庫中 flant/mcrouter.

configVersion: 1
project: mcrouter
deploy:
 namespace: '[[ env ]]'
 helmRelease: '[[ project ]]-[[ env ]]'
---
image: mcrouter
from: ubuntu:16.04
mount:
- from: tmp_dir
 to: /var/lib/apt/lists
- from: build_dir
 to: /var/cache/apt
ansible:
 beforeInstall:
 - name: Install prerequisites
   apt:
     name: [ 'apt-transport-https', 'tzdata', 'locales' ]
     update_cache: yes
 - name: Add mcrouter APT key
   apt_key:
     url: https://facebook.github.io/mcrouter/debrepo/xenial/PUBLIC.KEY
 - name: Add mcrouter Repo
   apt_repository:
     repo: deb https://facebook.github.io/mcrouter/debrepo/xenial xenial contrib
     filename: mcrouter
     update_cache: yes
 - name: Set timezone
   timezone:
     name: "Europe/Moscow"
 - name: Ensure a locale exists
   locale_gen:
     name: en_US.UTF-8
     state: present
 install:
 - name: Install mcrouter
   apt:
     name: [ 'mcrouter' ]

(werf.yaml)

…然後我們畫草圖 舵圖。有趣的是,這裡只有一個基於副本數量的配置產生器 (如果有人有更簡潔、更優雅的選擇,請在評論中分享):

{{- $count := (pluck .Values.global.env .Values.memcached.replicas | first | default .Values.memcached.replicas._default | int) -}}
{{- $pools := dict -}}
{{- $servers := list -}}
{{- /* Заполняем  массив двумя копиями серверов: "0 1 2 0 1 2" */ -}}
{{- range until 2 -}}
 {{- range $i, $_ := until $count -}}
   {{- $servers = append $servers (printf "mc-%d.mc:11211" $i) -}}
 {{- end -}}
{{- end -}}
{{- /* Смещаясь по массиву, получаем N срезов: "[0 1 2] [1 2 0] [2 0 1]" */ -}}
{{- range $i, $_ := until $count -}}
 {{- $pool := dict "servers" (slice $servers $i (add $i $count)) -}}
 {{- $_ := set $pools (printf "MissFailoverRoute|Pool|pool%02d" $i) $pool -}}
{{- end -}}
---
apiVersion: v1
kind: ConfigMap
metadata:
 name: mcrouter
data:
 config.json: |
   {
     "pools": {{- $pools | toJson | replace "MissFailoverRoute|Pool|" "" -}},
     "route": {
       "type": "OperationSelectorRoute",
       "default_policy": "AllMajorityRoute|Pool|pool00",
       "operation_policies": {
         "get": {
           "type": "RandomRoute",
           "children": {{- keys $pools | toJson }}
         }
       }
     }
   }

(10-mcrouter.yaml)

我們將其推廣到測試環境並檢查:

# php -a
Interactive mode enabled

php > # Проверяем запись и чтение
php > $m = new Memcached();
php > $m->addServer('mcrouter', 11211);
php > var_dump($m->set('test', 'value'));
bool(true)
php > var_dump($m->get('test'));
string(5) "value"
php > # Работает! Тестируем работу сессий:
php > ini_set('session.save_handler', 'memcached');
php > ini_set('session.save_path', 'mcrouter:11211');
php > var_dump(session_start());
PHP Warning:  Uncaught Error: Failed to create session ID: memcached (path: mcrouter:11211) in php shell code:1
Stack trace:
#0 php shell code(1): session_start()
#1 {main}
  thrown in php shell code on line 1
php > # Не заводится… Попробуем задать session_id:
php > session_id("zzz");
php > var_dump(session_start());
PHP Warning:  session_start(): Cannot send session cookie - headers already sent by (output started at php shell code:1) in php shell code on line 1
PHP Warning:  session_start(): Failed to write session lock: UNKNOWN READ FAILURE in php shell code on line 1
PHP Warning:  session_start(): Failed to write session lock: UNKNOWN READ FAILURE in php shell code on line 1
PHP Warning:  session_start(): Failed to write session lock: UNKNOWN READ FAILURE in php shell code on line 1
PHP Warning:  session_start(): Failed to write session lock: UNKNOWN READ FAILURE in php shell code on line 1
PHP Warning:  session_start(): Failed to write session lock: UNKNOWN READ FAILURE in php shell code on line 1
PHP Warning:  session_start(): Failed to write session lock: UNKNOWN READ FAILURE in php shell code on line 1
PHP Warning:  session_start(): Unable to clear session lock record in php shell code on line 1
PHP Warning:  session_start(): Failed to read session data: memcached (path: mcrouter:11211) in php shell code on line 1
bool(false)
php >

搜尋錯誤文字沒有得到任何結果,但查詢“mcrouter php「該專案最主要的未解決的問題是—— 缺乏支持 memcached 二進位協定。

NB:memcached 中的 ASCII 協定比二進位協定慢,且一致性金鑰雜湊的標準方法僅適用於二進位協定。但這對於具體案例來說並不會產生問題。

訣竅就在眼前:剩下的就是切換到 ASCII 協議,然後一切都會正常工作……然而,在這種情況下,尋找答案的習慣 php.net 上的文檔 開了一個殘酷的玩笑。你不會在那裡找到正確的答案……除非你滾動到最後,在部分 “用戶貢獻的筆記” 將會忠實地 被不公平地否決的答案.

是的,該選項的正確名稱是 memcached.sess_binary_protocol。必須將其停用,之後會話才能開始工作。剩下的就是將裝有mcrouter的容器放入裝有PHP的pod中了!

結論

因此,僅透過基礎設施的改變,我們就能夠解決問題:解決 memcached 容錯問題,並提高快取儲存的可靠性。除了應用程式的明顯優勢之外,這也為在平台上工作時提供了迴旋餘地:當所有組件都有儲備時,管理員的生活就會大大簡化。是的,這種方法有其缺點,它可能看起來像一個“拐杖”,但如果它能省錢,掩蓋問題並且不會引發新的問題 - 為什麼不呢?

聚苯乙烯

另請閱讀我們的博客:

來源: www.habr.com

為具有 DDoS 保護、VPS VDS 服務器的站點購買可靠的主機 🔥 購買具備 DDoS 防護的可靠網站寄存服務,包括 VPS 和 VDS 伺服器 | ProHoster