使用Beanstalk搭建隊列服務
Beanstalkd介紹
一個高性能、輕量級的分佈式內存隊列系統。高性能離不開異步,異步離不開隊列,而其內部都是Producer-Consumer模式的原理。
組成部分
組件 | 說明 |
---|---|
管道(tube) | 一個有名稱的任務隊列,用來存儲統一類型的job,是producer和consumer的操作對象 |
任務(job) | 一個需要異步處理的任務,需要放在tube中 |
生產者(producer) | job的生產者,通過put命令來將一個job放到一個tube中 |
消費者(consumer) | job的消費者,通過reserve、release、bury、delete命令來獲取job或改變job的狀態 |
特性
- 優先級: 可以設置任務的優先級
- 延遲: 設置任務多少秒後才允許被消費者使用
- 持久化: 定時刷新數據到文件,服務器掛掉後數據依舊存在
- 超時控制: 消費者必須在指定時間內完成任務,否則就會重新放入管道任務預留:消費者先暫時跳過任務不處理
- 分佈式容錯: 分佈式設計和Memcached類似,beanstalkd各個server之間並不知道彼此的存在,都是通過client來實現分佈式以及根據tube名稱去特定server獲取job。
任務狀態
狀態 | 說明 |
---|---|
ready | 已經準備好的任務,可以給消費者獲取 |
delayed | 延遲執行的任務,設置時候設置了延遲時間 |
reserved | 已被消費者獲取,正在執行的任務,Beanstalkd服務負責檢查任務是否 在TTR(time-to-run)內完成 |
buried | 保留的任務,任務不會被執行,也不會消失 |
delete | 消息被徹底刪除,Beanstalkd不再維護這些消息 |
適用場景
- 用作延時隊列: 比如可以用於如果用戶30分鐘內不操作,任務關閉。
- 用作循環隊列: 用release命令可以循環執行任務,比如可以做負載均衡任務分發。
- 用作兜底機制: 比如一個請求有失敗的概率,可以用Beanstalk不斷重試,設定超時時間,時間內嘗試到成功爲止。
- 用作定時任務: 比如可以用於專門的後臺任務。
- 用作異步操作: 這是所有消息隊列都最常用的,先將任務仍進去,順序執行。
服務安裝
- 下載beanstalkd-1.11
wget https://codeload.github.com/beanstalkd/beanstalkd/tar.gz/v1.11
- 安裝
tar xzvf beanstalkd-1.11.tar.gz
cd beanstalkd-1.11
make & make install
beanstalkd -v
- 啓動服務
beanstalkd -l 0.0.0.0 -p 11300 -b /log/beanstalkd/binlog -F
隊列應用(PHP)
composer安裝 Pheanstalk 類庫
//PHP版本要求 7.1+
composer require pda/pheanstalk:~4.0
執行composer後,在項目composer.json配置文件中將增加pda/pheanstalk依賴包
"require": {
"pda/pheanstalk": "~4.0"
}
Producer添加任務
//創建實例
$client = Pheanstalk::create($host, $port, $timeout);
//設置使用的tube,添加任務數據
//$data 任務數據
//$priority 任務優先級.小優先級數值的job將會排在大優先級 數值的job前面執行。
//最高優先級是0,最低優先級是4,294,967,295
//$delay 任務延遲執行秒數
//$ttr 允許一個消費者執行該job的秒數
$client->useTube($tube)->put($data, $priority, $delay, $ttr);
Consumer消費任務
ini_set('default_socket_timeout', 24*60*60);
$client = Pheanstalk::create($host, $port, $timeout);
$client->watchOnly($tube);
while (true) {
//阻塞獲取任務
$job = $client->reserve();
if (is_null($job)) {
continue;
}
//設置重新計算ttr
$client->touch($job);
//獲取任務數據
$data = $job->getData();
//開始執行任務
//任務執行邏輯
$res = true
//結束任務執行
//刪除任務
$client->delete($job);
if ($res === true) {
//任務執行成功,刪除任務
$client->delete($job);
} else {
//否則將任務重新放回隊列
$client->release($job, 1024, 10);
}
}
default_socket_timeout
這個參數是一定要加的,php 默認一般是 60s,假如您沒有在代碼裏面設置,採用默認的話(60s),60s 之內如果沒有 job 產生,腳本就會報 socket 錯誤。
客戶端操作類
以下基於pda/pheanstalk依賴包實現的Beanstalk操作類,供參考。。
<?php
namespace App\Libs;
use Pheanstalk\Pheanstalk;
/**
* Beanstalk工具類
* @since 2020-02-26
*/
class Beanstalk {
/**
* Beanstalk配置信息
* @var array
*/
protected $configs = [];
/**
* client實例
* @var array
*/
protected $clients = [];
/**
* 當前連接的服務端的配置名稱
* @var string
*/
protected $connection = 'default';
/**
* 連接超時時間
* @var int
*/
protected $clientTimeOut = 3000;
/**
* 初始化配置信息
* @param array $configs
* @return void
*/
public function __construct(array $configs = []) {
$this->configs = $configs;
}
/**
* 添加任務
* @param string $tube 隊列管道
* @param array $parameters
* @param int $priority 優先級
* @param int $delay 延遲執行時間
* @param int $ttr 任務超時時間
* @return bool|mixed
*/
public function addTask($tube, array $parameters,
$priority = Pheanstalk::DEFAULT_PRIORITY,
$delay = Pheanstalk::DEFAULT_DELAY,
$ttr = Pheanstalk::DEFAULT_TTR) {
$client = $this->createClient();
$stream = serialize($parameters);
$client->useTube($tube)->put($stream, $priority, $delay, $ttr);
}
/**
* 創建客戶端連接
* @return mixed
*/
public function createClient() {
if (! isset($this->clients[$this->connection])) {
$client = Pheanstalk::create($this->configs[$this->connection]['host'], $this->configs[$this->connection]['port'], $this->clientTimeOut);
$this->clients[$this->connection] = $client;
}
return $this->clients[$this->connection];
}
/**
* 創建後臺工作進程
* @param $tube
* @param array $service
* @return mixed
* @author huangweizhang
* @throws \Exception
*/
public function createWorker($tube, array $service) {
ini_set('default_socket_timeout', 24*60*60);
$client = Pheanstalk::create($this->configs[$this->connection]['host'], $this->configs[$this->connection]['port'], $this->clientTimeOut);
$client->watchOnly($tube);
while (true) {
if (count($service) != 2) {
throw new \Exception('parameter service error.');
}
list($classname, $method) = $service;
if (! class_exists($classname)) {
throw new \Exception('worker service class not exists.');
}
if (! method_exists($classname, $method)) {
throw new \Exception('worker service method not exists.');
}
//獲取任務
$job = $client->reserve();
if (is_null($job)) {
continue;
}
$client->touch($job);
//獲取任務參數
$stream = $job->getData();
$parameters = unserialize($stream);
try {
//執行任務
$class = new $classname();
call_user_func_array(array($class, $method), array($parameters));
unset($class);
//刪除任務
$client->delete($job);
} catch (\Exception $e) {
//任務執行失敗操作
}
}
}
/**
* 設置連接
* @param string $connection
*/
public function setConnection($connection) {
$this->connection = $connection;
}
/**
* 設置客戶端連接超時時間
* @param int $clientTimeOut
*/
public function setClientTimeOut($clientTimeOut) {
$this->clientTimeOut = $clientTimeOut;
}
}