目錄:
1. 系統架構:API接口服務,業務處理服務
2. 開發環境:Redis,ActiveMQ
3. API服務:Java + SpringBoot + ActiveMQ
4. 業務處理服務:Python + Django + ActiveMQ
5. 任務處理請求信息發送和接收
6. 業務處理服務集成Celery任務調度
7. ActiveMQ常見問題和解決方法
8. Celery常見問題和解決方法
附錄1,JMS規範定義的2類消息發送接收模型
附錄2,JMS規範定義的5類消息
一,系統架構
SpringBoot是Java開發時常用框架,有非常豐富的組件和易用的功能。
Python在AI領域是主流開發語言,Django是應用廣泛的開源框架,在實際系統開發中,我們經常面臨的是跨平臺的業務處理需求。
ActiveMQ是一個非常流行的消息隊列服務中間件,基於JMS(Java Message Service)規範。
Celery是一個靈活可靠的分佈式系統,用於異步任務調度,系統通常將一些耗時的操作任務提交給Celery去異步執行,典型系統架構示意圖如下:
本文基於Java + SpringBoot,Python + Django,集成ActiveMQ和Celery,搭建起一個跨平臺異步任務調度系統。
API接口服務:https://github.com/jextop/StarterApi
├── controller
│ └── CheckController.java
├── mq
│ └── MqService.java
│ └── MqConsumer.java
業務處理服務:https://github.com/jextop/starter_service
├── celery.py
├── tasks.py
├── mq
│ └── mq_service.py
│ └── mq_listener.py
二,開發環境
系統依賴ActiveMQ和Redis運行,手動安裝配置稍顯繁瑣,可以使用Docker一鍵部署,下載資源編排代碼後運行腳本:docker-compose up -d
開發環境部署:https://github.com/rickding/HelloDocker/tree/master/data
├── docker-compose.yml
├── up.sh
三,API服務:Java + SpringBoot + ActiveMQ
SpringBoot集成ActiveMQ只需簡單配置,下載項目代碼,開發步驟如下:
代碼文件 | 功能要點 | |
SpringBoot集成ActiveMQ | pom.xml | 引入ActiveMQ依賴:spring-boot-starter-activemq |
application.yml | 配置ActiveMQ服務器:broker-url, user, passworkd | |
MqConfig.java | 配置Bean: ActiveMQQueue, ActiveMQTopic, 還有JmsListenerContainerFactory | |
封裝服務 | MqService.java | 調用ActiveMQ發送消息:JmsMessagingTemplate.convertAndSend()發送Queue和Topic |
接收處理消息 | MqConsumer.java | 接收ActiveMQ消息,@JmsListener()聲明處理函數 |
單元測試 | MqServiceTest.java | 測試封裝的ActiveMQ發送接收功能 |
功能調用 | CheckController.java | 增加REST接口/chk/mq,調用ActiveMQ發送消息 |
1,在application.yml中配置ActiveMQ服務器信息:
spring:
activemq:
broker-url: tcp://127.0.0.1:61616
user: admin
password: admin
in-memory: false
packages:
trust-all: true
pool:
enabled: false
2,MqService封裝了消息發送功能,詳見代碼MqService.java,注意Java環境下使用文本消息TextMessage,發送時將Map轉換爲JSON字符串,和Python環境下STOMP簡單文本協議對應。
3,MqConsumer.java接收任務處理狀態消息,使用的是發佈訂閱消息Topic,附錄1中解釋Queue和Topic兩類消息的區別:
@Component
public class MqConsumer {
@JmsListener(destination = "starter.status", containerFactory = "jmsTopicListenerContainerFactory")
public void listenTopic(Message msg) {
Map<String, ?> msgMap = MqUtil.parseMsg(msg);
LogUtil.info("Receive status msg", msgMap);
}
}
4,配置完成後,啓動API服務,運行單元測試驗證消息發送接收功能。
@SpringBootTest
public class MqServiceTest {
@Autowired
MqService mqService;
@Test
public void testSendQueue() {
mqService.sendQueue(new HashMap<String, Object>() {{
put("msg", "test active queue from java");
put("date", new Date().toString());
}});
}
@Test
public void testSendTopic() {
mqService.sendTopic(new HashMap<String, Object>() {{
put("msg", "test active topic from java");
put("date", new Date().toString());
}});
}
}
5,API服務接收到的狀態信息:
四,業務處理服務:Python + Django + ActiveMQ
Python集成ActiveMQ使用stomp.py,基於STOMP協議(端口爲61613),簡單(流)文本消息,開發步驟如下:
代碼文件 | 功能要點 | |
Python集成ActiveMQ | requirements.txt | 安裝stomp.py: stomp.py >= 5.0.1 |
封裝服務 | mq_serivce.py | 封裝ActiveMQ的消息發送和處理功能。在Django框架下,將地址等配置在settings.py中集中管理,注意端口爲61613 |
接收處理消息 | mq_listener.py | 增加消息接收處理類,繼承stomp.ConnectionListener |
啓動消息監聽服務 | mq.py | 在Django框架下,將啓動服務代碼封裝成command,方便調用和維護。 |
單元測試 | test_mq_serivce.py | 測試封裝的功能函數 |
功能調用 | views.py | 增加REST接口/chk/mq,調用mq_service發送消息 |
1,將ActiveMQ服務器地址等信息配置在settings.py中,方便維護管理。
MQ_URL = '127.0.0.1'
MQ_PORT = 61613
MQ_USER = 'admin'
MQ_PASSWORD = 'admin'
MQ_QUEUE = '/queue/starter.process'
MQ_TOPIC = '/topic/starter.status'
2,爲了增加代碼的兼容和容錯能力,封裝輔助函數send_msg(), consume_msg(), get_conn(), close_conn(),詳見代碼文件mq_service.py
3,增加mq_listener.py,聲明消息處理類,繼承stomp.ConnectionListener,在on_message()函數中將消息字符串解析爲JSON,注意STOMP協議只支持簡單文本協議,所以此步轉換是必須的。
4,根據接收到的消息內容創建一個異步任務。
from __future__ import absolute_import, unicode_literals
import json
import logging
import stomp
from ..tasks import do_task
log = logging.getLogger(__name__)
class MqListener(stomp.ConnectionListener):
def on_message(self, headers, msg_str):
log.info('Receive msg: %s, %s, %s' % (type(msg_str), msg_str, headers))
msg_dict = None
try:
msg_dict = json.loads(msg_str)
except Exception as e:
log.warning('Exception when parse msg: %s' % str(e))
log.info('Parsed msg: {}, {}'.format(type(msg_dict), msg_dict))
do_task(msg_dict)
def on_error(self, headers, msg_str):
log.info('Error msg: %s, %s, %s' % (type(msg_str), msg_str, headers))
5,封裝一個Django Command,調用comsume_msg啓動消息監聽服務,代碼在目錄management/commands下的mq.py
import logging
from django.core.management.base import BaseCommand
from starter_service.mq import mq_service as mq
from starter_service.mq.mq_listener import MqListener
log = logging.getLogger(__name__)
class Command(BaseCommand):
help = 'mq starts listener'
def handle(self, *args, **options):
log.info("mq starts")
return mq.consume_msg(MqListener())
6,運行命令python manage.py mq,看到消息提示,啓動監聽服務成功。
7,增加測試test_mq_service.py,發送消息:
import logging
from django.test import TestCase
from ..mq import mq_service as mq
log = logging.getLogger(__name__)
class MQServiceTest(TestCase):
def test_send_msg(self):
msg_dict = {'content': 'test msg dict', 'msg': 'queue from python'}
mq.send_msg_to_queue(msg_dict)
mq.send_msg_to_topic({'msg': "test topic from python"})
8,運行測試python manage.py test,看到消息發送和接收:
五,任務處理請求信息發送和接收
現在API服務和業務處理服務都已經能夠發送和接收ActiveMQ消息,將它們連接起來:
1,API服務:REST接口處理客戶端請求時,發送業務處理消息,使用點對點消息Queue, CheckController.java
@GetMapping(path = "/chk/mq")
public Object mq(@RequestAttribute(required = false) String ip) {
String msg = String.format("check mq from java, %s, 消息隊列", ip);
// to service
mqService.sendQueue(new HashMap<String, Object>() {{
put("msg", msg);
put("date", DateUtil.format(new Date()));
}});
return new HashMap<String, Object>() {{
put("chk", "mq");
put("msg", msg);
}};
}
2,業務處理服務:消息監聽服務接收到請求消息,調用Celery創建一個異步任務,代碼mq_listener.py,調用tasks.py中do_task()函數:
def do_task(param_dict):
log.info('do_task: %s, %s' % (type(param_dict), param_dict))
job = dispatch_task(task, param_dict)
param_dict['status'] = 'waiting'
param_dict['task'] = job.id
send_msg_to_topic(param_dict)
return job
3,將異步任務創建功能封裝爲dispatch_task()函數:
from __future__ import absolute_import, unicode_literals
import json
import logging
log = logging.getLogger(__name__)
def dispatch_task(task_func, param_dict):
param_json = json.dumps(param_dict)
try:
return task_func.apply_async(
[param_json],
retry=True,
retry_policy={
'max_retries': 1,
'interval_start': 0,
'interval_step': 0.2,
'interval_max': 0.2,
},
)
except Exception as ex:
log.info(ex)
raise
4,Celery異步任務處理時,發送狀態信息到API服務,代碼tasks.py,Celery的集成方法見下一部分。
5,API服務接收到信息,更新狀態並通知客戶端,代碼MqConsumer.java
消息格式轉換過程:
基本時序圖如下:
六,業務處理服務集成Celery
Django集成Celery配置方法步驟如下:
代碼文件 | 功能要點 | |
Django集成Celery | requirements.txt | 安裝Celery, Redis和工具包: celery == 4.2.1 flower == 0.9.2 redis == 3.2.0 eventlet == 0.24.1 |
celery.py | 配置Celery,依賴的消息中間件broker和後端backend地址配置在settings.py中集中維護。 | |
__init__.py | 配置項目加載celery.app | |
聲明異步任務 | tasks.py | 聲明Celery可調度的任務@shared_task |
封裝工具task_util | task_util.py | 異步任務創建和分發 |
啓動異步任務處理服務 | 腳本celery.sh | 運行命令:celery -A hello_celery worker -l info -P eventlet |
單元測試 | test_task_util.py | 測試異步任務創建和分發功能 |
創建異步任務 | views.py | 增加REST接口/chk/job |
1. 增加celery.py,配置信息:
from __future__ import absolute_import, unicode_literals
import os
from celery import Celery, platforms
from django.conf import settings
# set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'starter_service.settings')
app = Celery(
'starter_service',
include=['starter_service.tasks'],
broker=settings.CELERY_BROKER,
backend=settings.CELERY_BACKEND
)
# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
# should have a `CELERY_` prefix.
app.config_from_object('django.conf:settings', namespace='CELERY')
app.conf.update(
CELERY_ACKS_LATE=True,
CELERY_ACCEPT_CONTENT=['pickle', 'json'],
CELERYD_FORCE_EXECV=True,
CELERYD_MAX_TASKS_PER_CHILD=500,
BROKER_HEARTBEAT=0,
)
# Optional configuration, see the application user guide.
app.conf.update(
CELERY_TASK_RESULT_EXPIRES=3600, # celery任務執行結果的超時時間,即結果在backend裏的保存時間,單位s
)
# Load task modules from all registered Django app configs.
app.autodiscover_tasks()
platforms.C_FORCE_ROOT = True
2. 打開settings.py,配置BROKER和BACKEND地址:
CELERY_BROKER = 'redis://127.0.0.1:6379/2'
CELERY_BACKEND = 'redis://127.0.0.1:6379/3'
3. 打開__init__.py,增加代碼:
from __future__ import absolute_import, unicode_literals
from .celery import app as celery_app
__all__ = ['celery_app']
4. 增加tasks.py,聲明異步任務。任務處理過程中將調用mq_service發送狀態信息到API服務,Topic類型的文本信息。
from __future__ import absolute_import, unicode_literals
import logging
import json
import time
from celery import shared_task
from .util.task_util import dispatch_task
from .mq.mq_service import send_msg_to_topic
log = logging.getLogger(__name__)
@shared_task
def task(param_str):
log.info('task starts: %s, %s' % (type(param_str), param_str))
param_dict = None
try:
param_dict = json.loads(param_str)
except Exception as e:
log.warning('Exception when parse param: %s' % str(e))
log.info('parsed param: {}, {}'.format(type(param_dict), param_dict))
param_dict['status'] = 'processing'
send_msg_to_topic(param_dict)
# do something
time.sleep(3)
param_dict['status'] = 'finished'
send_msg_to_topic(param_dict)
return 'finished'
5. 正確配置後,運行命令啓動celery worker異步任務處理服務:celery -A hello_celery worker -l info -P eventlet,注意Win10環境中需要增加eventlet,Celery成功啓動信息:
6. 增加單元測試test_task_util.py,創建一個任務:
import logging
from django.test import TestCase
from ..tasks import task
from ..util.task_util import dispatch_task, get_task_status
log = logging.getLogger(__name__)
class TasksTest(TestCase):
def test_get_task_status(self):
job = dispatch_task(task, {'msg': 'test_task'})
self.assertIsNotNone(job)
ret = get_task_status(task, job.id)
log.info('task status: %s,%s, %s' % (ret, job.id, str(task)))
self.assertIsNotNone(ret.get('status'))
7. 運行python manage.py test,Celery服務將執行測試函數創建的任務:
七,ActiveMQ常見問題和解決方法
l 發送消息時異常:MessageConversionException
Could not convert 'GenericMessage [payload=java.lang.Object@472096, headers={id=bcbe6b1d-b340-bbb5-3121-41417cdc5e35, timestamp=1581671689357}]'; nested exception is org.springframework.jms.support.converter.MessageConversionException: Cannot convert object of type [java.lang.Object] to JMS message. Supported message payloads are: String, byte array, Map<String,?>, Serializable object.
解決:發送的消息時Object對象實例時,需要實現Serializable序列化接口。
原因:ActiveMQ支持的消息內容:String, byte[], Map<>, 可序列化的類。另外注意類所在包路徑發送方和接收方應該一致。
l 啓動服務錯誤:[transport.py: 787, attempt_connection] Could not connect to host 127.0.0.1, port 61613
解決:檢查ActiveMQ是否正常啓動,特別注意是否開啓STOMP協議端口61613
原因:Python連接ActiveMQ使用STOMP協議,端口默認61613
l 發送消息時錯誤:TypeError: message should be a string or bytes, found <class 'dict'>
解決:將消息內容序列化爲JSON,發送時調用json.dumps(),接收時調用json.loads()
原因:Python連接ActiveMQ使用的是STOMP協議,消息格式爲簡單文本。
l 跨系統對接時接收到的消息類型不是TextMessage
Python開發的業務處理服務 -> Java開發的API服務,接收到的消息類型爲BytesMessage,Python發送時設置conn.send('xx', msg_str, content_type="text/plain")仍然接收不到期望的類型TextMessage
解決:stomp建立連接時配置參數conn = stomp.Connection10([("localhost", 61613)], auto_content_length=False)
原因:Python連接ActiveMQ使用STOMP協議,消息格式爲簡單文本,不攜帶類型信息,只通過header中的content-length來判斷TextMessage和BytesMessage,所以發送消息時不在header中添加content-length就可以了。
八,Celery常見問題和解決方法
l 啓動Celery: celery -A hello_celery worker -l info,運行出錯:
Unrecoverable error: VersionMismatch('Redis transport requires redis-py versions 3.2.0 or later. You have 2.10.6',)
解決:指定Redis使用3.2.0或更高pip install redis>=3.2.0
原因:Redis版本兼容問題。
l 啓動Celery: celery -A hello_celery worker -l info,運行出錯:
Task handler raised error: ValueError('not enough values to unpack (expected 3, got 0)',)
解決:安裝eventlet:pip install eventlet,然後更新命令行:celery -A hello_celery worker -l info -P eventlet
原因:Celery4.x在win10上需要安裝 eventlet包。
l 啓動Celery Flower任務管理工具: celery flower --broker=redis://127.0.0.1:6379/2,運行出錯:
ATTRIBUTEERROR: MODULE ‘TORNADO.WEB’ HAS NO ATTRIBUTE 'ASYNCHRONOUS’
解決:安裝tornado5.1.1版本:pip uninstall -y tornado; pip install tornado==5.1.1
原因:tornado6.0開始使用coroutine並刪除了tornado.web.asynchronous,5.1版本能正常調用。
附錄1:JMS規範定義的2類消息發送接收模型
JMS規範定義了2類消息發送接收模型:點對點queue,發佈訂閱topic,區別是能夠重複消費和是否保存。在任務調度系統中,請求處理消息使用queue,任務處理狀態消息使用topic。
1,點對點queue:不可重複消費,消息被消費前一直保存。
生產者發送消息到queue,一個消費者取出並消費消息。
消息被消費後,queue中不再保存,所有隻有一個消費者能夠取到消息。
queue支持多個消費者存在,但是一個消息只有一個消費者可以消費。
當前沒有消費者時,消息一直保存,直到被消費者消費。
2,發佈訂閱topic:可重複消費,發佈給所有訂閱者。
生產者發佈消息到topic中,多個訂閱者收到並消費消息。
和queue不同,發佈到topic中的消息會被所有訂閱者消費。
當生產者發佈消息時,不管是否有訂閱者,都不保存消息。
JMS規範定義的2類消息傳輸模型queue和topic比較:
Queue | Topic | |
模型 | 點對點Point-to-Point | 發佈訂閱publish/subscribe |
有無狀態 | queue消息在消費前被一直保存在mq服務器上的文件或者配置DB | topic數據默認不保存,是無狀態的。 |
完整性保障 | queue保證每條消息都被消費者接收到 | topic不保證生產者發佈的每條消息都被訂閱者接收到 |
消息是否會丟失 | 生產者發送消息到queue,消費者接收到消息。如果沒有消費者,將一直保存,不會丟失。 | 生產者發佈消息到topic時,當前的訂閱者都能夠接收到消息。如果當前沒有訂閱者,該消息就丟失。 |
消息發佈接收策略 | 一對一的消息發佈接收策略,一個生產者發送的消息只被一個消費者接收。mq服務器收到回覆後,將這個消息刪除。 | 一對多的消息發佈接收策略,同一個topic的多個訂閱者都能收到生產者發佈的消息。 |
附錄2:JMS規範定義的5類消息
字符串TextMessage,
鍵值對MapMessage,
序列化對象ObjectMessage
字節流BytesMessage
數據流StreamMessage
ActiveMQ支持5類JMS消息,增加了二進制大文件消息BlobMessage:
--------------------------------
如果您覺得這篇文章對您有幫助,請點個“贊”,博主感激不盡!
Jext技術社區專注領域:軟件工程實踐,JIRA研發管理,分佈式系統架構,軟件質量保障。