使用 mcrouter 水平擴展 memcached

使用 mcrouter 水平擴展 memcached

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

這次的主角是基於 symfony 2.3 框架的 PHP 應用程序,該應用程式根本沒有包含在更新的業務計劃中。 除了相當標準的會話儲存之外,該項目還充分利用了 「緩存一切」政策 在 memcached 中:回應對資料庫和 API 伺服器的請求、各種標誌、用於同步程式碼執行的鎖定等等。 在這種情況下,memcached 的故障對於應用程式的運作來說是致命的。 此外,快取遺失會導致嚴重後果:DBMS 開始爆裂、API 服務開始禁止請求等。 穩定局勢可能需要數十分鐘,在此期間服務將非常慢或完全不可用。

我們需要提供 輕鬆水平擴展應用程式的能力, IE。 對原始程式碼進行最少的更改並保留全部功能。 使快取不僅能夠抵抗故障,而且還要盡量減少由此造成的資料遺失。

memcached 本身有什麼問題?

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

會話儲存的情況要好一些:您可以配置 memcached.sess_number_of_replicas,這樣一來,資料將同時儲存在多台伺服器上,並且當其中一個 memcached 實例出現故障時,資料將從其他伺服器轉移。 但是,如果伺服器在沒有資料的情況下重新上線(通常在重新啟動後發生),則某些金鑰將按照有利於伺服器的方式重新分配。 事實上這意味著 會話資料遺失,因為如果發生遺失,就無法「轉到」另一個副本。

標準庫工具主要針對 水平的 縮放:它們允許您將快取增加到巨大的大小,並提供從不同伺服器上託管的程式碼對其的存取。 不過,在我們的情況下,儲存的資料量不超過幾GB,一兩個節點的效能就已經足夠了。 因此,唯一有用的標準工具可能是確保 memcached 的可用性,同時維持至少一個快取執行個體處於工作狀態。 然而,甚至不可能利用這個機會......這裡值得回顧一下專案中使用的框架的古老性,這就是為什麼不可能讓應用程式與伺服器集區一起工作的原因。 我們也不要忘記會話資料的遺失:客戶的眼睛因使用者大量的註銷而抽搐。

理想情況下是必要的 複製 memcached 中的記錄並繞過副本 如果出現錯誤或錯誤。 幫助我們實施這項策略 微路由器.

微路由器

這是Facebook為了解決其問題而開發的memcached路由器。 它支援memcached文本協議,允許 擴充 memcached 安裝 達到瘋狂的程度。 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 的有用鏈接 維基百科上的文檔 и 專案問題 (包括封閉的),代表了一個各種配置的整個倉庫。

建置並運行微處理器

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

NB:文章中給出的清單已發佈在儲存庫中 平面/微路由器.

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 >

搜尋錯誤的文字沒有給出任何結果,但是對於查詢“微型計算機PHP「最重要的是該專案最古老的未解決問題 - 缺乏支持 memcached 二進位協定。

NB:memcached 中的 ASCII 協定比二進位協定慢,且一致性金鑰雜湊的標準方法僅適用於二進位協定。 但這不會對特定情況造成問題。

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

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

結論

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

聚苯乙烯

另請閱讀我們的博客:

來源: www.habr.com

添加評論