目录:
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研发管理,分布式系统架构,软件质量保障。