1. 程式人生 > 實用技巧 >PHP 疑難雜症:解決守護程序時 Redis 假死

PHP 疑難雜症:解決守護程序時 Redis 假死

內容簡介:背景:公司業務有一個常駐後臺執行的守護程序。在這個守護程序當中使用了 Redis List 結構儲存業務資料進行佇列消費。結果執行過程中,有時候半個月,有時候幾個月就會突然不再消費佇列裡面的資料。當時懷疑是 PHP 不適合編寫這種常駐後臺執行的守護程式。後來,我們發現進行心中檢測之後,程式的穩定性大大提高。至今沒有出現過假死。這段程式碼我們很容易看懂。它就是通過 Redis 的阻塞方法

背景:公司業務有一個常駐後臺執行的守護程序。在這個守護程序當中使用了 Redis List 結構儲存業務資料進行佇列消費。結果執行過程中,有時候半個月,有時候幾個月就會突然不再消費佇列裡面的資料。當時懷疑是

PHP不適合編寫這種常駐後臺執行的守護程式。後來,我們發現進行心中檢測之後,程式的穩定性大大提高。至今沒有出現過假死。

一、一個簡單的守護程序示例

<?php
$redis = new \Redis();
$redis->connect('localhost', 6379);
$redis->auth('xxxxx'); // Redis 密碼如果沒有設定為空字串。
$redis->select(1);

$queueKey    = 'redis_queue_services_key';     // 業務資料佇列。
$queueIngKey = 'redis_queue_services_ing_key'; // 處理中的佇列。

try {
    while (true) {
        $element = $redis->bRPopLPush($queueKey, $queueIngKey, 60);
        if ($element) {
            $data = json_decode($element, true);
            /**
             *
             ...... 此處省略業務邏輯 ......
             *
             */
        } else {
            usleep(100000); // 睡眠 0.1 秒。
        }
    }
} catch (\Exception $e) {
    exit("Error:{$e->getMessage()}");
}

這段程式碼我們很容易看懂。

它就是通過Redis的阻塞方法bRPopLPush迴圈從 Redis 佇列中取出資料並處理。如果沒有取到資料就休眠一秒。之所以休眠是為了保證 CPU 能得到充分的利用。因為,我們已經使用了阻塞方法阻塞 60 秒。所以,這個位置休眠與否並不重要。

當我們的業務出現任何錯誤,我們通過try catch進行異常捕獲然後將錯誤資訊直接輸出並退當前指令碼。

博主寒冰第一次編寫常駐後臺執行的守護程序時,就是如上這種方式寫的程式碼。結果,這段程式碼執行到 30s 的時候報錯了。提示我們 socket 流超時。於是我在這個指令碼頭部加了如下程式碼:

ini_set('default_socket_timeout', -1);

這樣我們的PHP就不會主動段掉我們與 Redis 的 socket 連線了。

但是,好景不長。過了一段時間,大概半個月吧。運維同學告訴我 Redis 佇列的資料出現了未消費的情況。然後,我查看了消費日誌。的確沒有產生新的消費日誌。因為我有一個習慣,每個消費消費的時候都會把成功消費的日誌寫到檔案中。消費失敗的也寫入日誌檔案中。這樣,我就知道失敗的具體原因。

但是,這次我真的沒有發現有任何的錯誤發生。

  • 常駐後臺程序處理存活狀態。並沒有變成孤兒程序。
  • 常駐後臺程序記憶體也沒有出現洩漏。
  • 系統 CPU/記憶體 資源都處理正在狀態。
  • 系統開啟的控制代碼資源也是低消狀態。
  • 頻寬也處理低消狀態。
  • 其它常駐程序也處理正常消費的工作狀態。也就排除了 Redis 故障的問題。

鄙人當時很氣餒。

我當時也懷疑過是不是像MySQL一樣常時間連線不進行任何操作,伺服器端會主動斷開連線。但是,MySQL 伺服器端主動段掉連線會提示:MySQL server has gone away的錯誤。但是,我們的 Redis 伺服器端沒有給我們報任何錯誤資訊呀。

我們公司用的是阿里雲的 Redis 產品。我懷疑是不是 Redis 版本太低造成的這個隱性 BUG。於是,我們將阿里雲的 Redis 服務升級到了阿里雲支援的最新版本。

結果還是失敗了。我們的 Redis 還是假死了。或者說我們的 Redis 處於偽活狀態。

你認為 Redis 活著,其實它早已經死了。你認為 Redis 死了,但是它卻沒有死亡的特徵。

最後,我冷靜下來。

我假定此時的 Redis 已經死了。只是沒有告訴客戶端而已。那麼我只需要每次檢測一下 Redis 連線是否存活就好了。

於是,我翻看了 Redis 的 API。發現它提供了一個ping()的方法來檢測連線是否存活。

於是,我迫不及待把這個程式碼加上去了。

程式碼如下:

二、一個不再假死(偽活)的 Redis 常駐程序示例

<?php

$redis = new \Redis();
$redis->connect('localhost', 6379);
$redis->auth('xxxxx'); // Redis 密碼如果沒有設定為空字串。
$redis->select(1);

$queueKey    = 'redis_queue_services_key';     // 業務資料佇列。
$queueIngKey = 'redis_queue_services_ing_key'; // 處理中的佇列。

try {
    while (true) {
        $element = $redis->bRPopLPush($queueKey, $queueIngKey, 60);
        if ($element) {
            $data = json_decode($element, true);
            /**
             *
             ...... 此處省略業務邏輯 ......
             *
             */
        } else {
            $pong = $redis->ping();
            if ($pong != '+PONG') {
                throw new \Exception('Redis ping failure!', 500);
            }
            usleep(100000); // 睡眠 0.1 秒。
        }
    }
} catch (\Exception $e) {
    exit("Error:{$e->getMessage()}");
}

通過程式碼對比,我們在第一版程式碼的基礎上加了如下程式碼:

$pong = $redis->ping();
if ($pong != '+PONG') {
    throw new \Exception('Redis ping failure!', 500);
}

我們向 Redis 伺服器傳送ping的時候,伺服器會返回+PONG字串。當然,這個是 Redis 擴充套件封裝過的方法。真正的 ping 是不會有 + 號的。

當我們每次 ping 的時候,Redis 伺服器就會認為我們的 Redis 客戶端連線處於存活狀態。就不會斷掉我們的連線了。

把程式碼進行改造之後,假死頭痛的問題再也沒出現了。