基於Yii2對RabbitMQ的封裝及程式管理實現細節(四)

so_easy發表於2020-09-02

前言

web程式管理是以寫檔案的形式管理消費者子程式。在程式管理檔案中的一個子程式對應的一行記錄類似:
89104_89102 = 07_14_delay_queue,07_14
<子程式>_<父程式> = <佇列名稱>,<檔案標誌>

之所以選用檔案進行管理的原因是因為實現起來簡單,並且如果真的出錯了人為操作檔案也非常簡便輕鬆。本著最初的設計思想開始了程式碼的實現。

實現的流程圖

程式管理分為了幾個部分實現,然後將這幾個部分拼接在一起最終實現了消費者程式的管理。
1、socekt部分:接收web頁面的請求並分發請求
2、Dispatcher部分:自身成為守護程式,並啟動AMQP消費者
3、程式檔案讀寫部分:讀寫檔案中的子程式資訊

每一部分的流程圖如下圖:

基於Yii2對RabbitMQ的封裝及程式管理實現細節(四)

但是實現的過程中有幾個問題需要解決:
1、程式檔案的讀寫在多程式情況下如何保障順序性?
2、子程式的管理及程式通訊問題

其實保障檔案讀寫的順序性問題很容易解決,本身寫的就是關於AMQP訊息佇列,所以很容易想到用訊息佇列即可實現。但是用哪種訊息佇列呢?類庫中實現了三種方式:amqp、beanstalk、redis。程式通訊也是使用的訊息佇列,除了上面的三種能夠複用之外還增加了一個pipe(檔案)。實現的過程中也並非一帆風順,多虧了在這裡釋出了“問答”才得以解決我碰到的“多代子程式無法接收SIGTERM訊號”的問題。這是一個有溫度的社群,也因此讓我決定在社群釋出這系列文章。
詳情問題可見:問答:多程式訊號處理相關:父程式的第一代子程式可以接收到SIGTERM訊號,...

消費者配置檔案

消費者檔案是啟動消費者程式的關鍵,配置如:

[consumer]
program = 07_14 ;檔案標誌
queueName = 07_14_delay_queue
qos = 1 ;消費者預處理數
duplicate = 1 ;佇列副本的數量,0或1都是直接取queueName的值
numprocs = 2 ;子程式的數量
script = /path/to/yii  ; Yii2的執行指令碼
request = amqp/consumer ; Yii2的Command控制器

; {php} 是執行php的命令佔位符,這個值在[common]模組下command已經配置過
; {script} php執行指令碼
; {request} 請求的控制器
; {queueName} 是queueName屬性的值
; {qos} 是qos屬性的值,預設為1
; command = {php} {script} {request} {queueName} {qos}
; 最終執行的命令如:`/usr/bin/php /path/to/yii amqp/consumer {queueName} 1` ,queueName會因為duplicate佇列副本的配置而改變。

amqp.ini配置檔案

amqp.ini配置檔案是啟動整個程式管理的關鍵,配置如:

; 公用配置屬性
[common]
; 日誌也支援按日期記錄 %Y-%m-%d
; %Y 年:2020,也可以寫成 %y:20
; %m 月:08
; %d 日:07 
; ../log/%Y-%m/access_%Y-%m-%d.log
access_log = ../log/access.log   ; ../log/%Y-%m/access_%Y-%m-%d.log
error_log = ../log/error.log
; 可選擇:DEBUG,INFO,NOTICE,WARNING,ERROR,CRITICAL,ALERT,EMERGENCY
; 選中的以逗號隔開,只記錄選中的型別日誌
level = INFO,WARNING,ERROR,NOTICE
; 儲存AMQP消費者守護程式的PID
pidfile = /var/run/amqp_master.pid
; 執行ExecDispatcher.php指令碼的命令
command = /usr/bin/php
; \pzr\amqp\cli\Server 啟動unix連線的本地檔案地址
unix = /var/run/amqp_consumer_serve.sock
; 程式檔案管理路徑
process_file = ./process_manager.ini

; AMQP消費者讀取的連線配置
[amqp]
host = 127.0.0.1
port = 5672
user = guest
password = guest

; 程式檔案處理:啟用beanstalk
[beanstalk]
host = 127.0.0.1
port = 11300

; 程式檔案處理:啟用redis
[redis]
host = 127.0.0.1
port = 6379
user = 
password = 

; 通知父程式的通訊方式
[pipe]
; 子程式和父程式通訊的檔案地址
pipe_file = /tmp/amqp_pipe

[communication]
; 可選擇:redis、amqp、beanstalk、pipe(預設)
class = pipe

[handler]
; 可選擇:beanstalk、redis、amqp
; 沒有預設的值,所以必須配置一個
class = beanstalk

[include]
files = ./consumer/*.ini

配置檔案中的相對路徑都是以amqp.ini的所在目錄為根本路徑。

後續

再此之前的三篇主要展示的是基於Yii2實現的RabbitMQ在使用上的簡易操作,對實現細節並未過多闡述。然而實際上對於RabbitMQ的基本操作封裝的實現細節似乎也比較容易理解。這裡且談在實現過程中記錄的點點滴滴。

為什麼要寫基於Yii2的RabbitMQ封裝呢?

原因很簡單:想要了解一些關於Yii2的實現細節。因為之前專案上沒有用過Yii2,所以準備自己看看摸索下。但是看總歸是看,所以決定動手做點什麼可能更有助於理解,於是打算結合RabbitMQ的實現對Yii2展開探索。
可是最後慢慢的就偏離了最開始的初衷。Yii2看了大概一兩星期後就覺得基本滿足我對RabbitMQ的實現了,於是動手開始封裝RabbitMQ卻用了一個來月(當然是空餘時間,那段時間回家的也晚,但很充實)。在實現之前大概看了下yii2-queue,發現它只能實現最簡單的佇列,但是它的很多實現方法值得借鑑,比如:訊息體的封裝思想,訊息體的序列化實現,事件訂閱思想等。訊息體的序列化實現是原封不動的搬過來了,訊息體的封裝思想和事件訂閱則沿用了思想但在實現上已經不一樣了。

基本用法包括哪些呢?

1、佇列實現
(1)各種佇列的實現包括普通佇列、延時佇列、優先佇列、RPC佇列。
(2)路由型別支援四種 direct、topic、fanout,header。
(3)預設情況下啟動的佇列都會啟動備份路由,可以防止訊息路由失敗而導致的丟失。
(4)預設情況下開啟了客戶端訊息確認機制,可以通過事件訂閱關閉。預設情況下開啟消費者確認機制,也可以通過事件訂閱關閉。
(5)增加了佇列副本概念,可以通過配置實現。
(6)訊息的釋出可以是單條釋出也可以是批量釋出。
(7)訊息體的序列化支援:json、igbinary、serialize。
2、消費者實現
消費者包括一般佇列的消費者和RPC佇列的消費者。由於RPC的實現和一般佇列的實現不同,所以在實現和消費者實現都是單獨的。
3、提供AMQP的api功能
通過api提供的一些資訊可以實現對AMQP服務監管,映象策略的實現、已有佇列的保護、消費者連線的監控及管理等。

unknown delivery_tag 趣事記錄

實現的過程中遇到一個有趣的問題,提了一個Issue給amqplib的作者。但是他們認為似乎並不是一個bug,更認為是一個錯誤的使用方式。

問題描述:分批多次釋出批量訊息時報錯 PHP Fatal error: Uncaught PhpAmqpLib\Exception\AMQPRuntimeException: Server ack'ed unknown delivery_tag "19"
問題原因:開啟客戶端訊息確認機制之後,訊息的釋出都會等待服務端的返回結果以此確認訊息是否傳送成功。但是每次傳送訊息RabbitMQ都會將next_delivery_tag置成1,而服務端返回的確認的訊息next_delivery_tag卻不會每次都置成1,表現是遞增。這也就導致最後傳送的tag和返回的tag不一致的問題。詳情見issue地址:github.com/php-amqplib/php-amqplib...

以此給碰到類似Server ack'ed unknown delivery_tag "19"這種問題的解決思路:報這個錯誤可能並不是因為消費者多次ack的問題,很有可能是因為多次批量傳送訊息的原因。(在網上尋求答案的時候都是清一色的說是ack重複,而且都是複製貼上的那種特別討厭!)

本作品採用《CC 協議》,轉載必須註明作者和本文連結
阿門阿前一棵葡萄樹 阿嫩阿嫩綠地剛發芽 蝸牛揹著那重重的殼呀 一步一步地往上爬

相關文章