1. 项目概述:为什么要在芒果派上折腾Node.js和EMQX?
最近拿到一块芒果派MangoPi MQ Quad,这块基于全志D1s RISC-V芯片的小板子挺有意思。它主打的就是一个“小而全”,麻雀虽小,五脏俱全,该有的接口基本都有,价格也亲民。很多人拿到手可能会先跑个Linux,然后装个桌面环境玩玩。但我的想法不太一样,我琢磨着能不能把它变成一个轻量级的、常驻运行的物联网边缘服务器或消息网关。
这个想法直接引出了两个核心组件:Node.js和EMQX。Node.js不用多说,作为异步事件驱动的JavaScript运行时,它在处理高并发I/O、构建网络应用方面有天然优势,非常适合在资源受限但连接数可能不少的边缘场景下跑一些数据采集、协议转换或者简单的业务逻辑。而EMQX,作为一款开源的MQTT消息服务器,可以说是物联网领域的“普通话”翻译官和交通枢纽,设备通过MQTT协议上报数据、接收指令,都离不开它。
把这两者部署到芒果派MQ Quad上,就相当于在一个巴掌大的硬件上,构建了一个具备业务处理能力和消息路由能力的微型边缘节点。你可以用它来连接家里的几个传感器,做个环境监控;也可以作为智能家居中控,桥接不同协议的设备;甚至可以作为一个小型数据聚合点,预处理数据后再上报到云端。这个组合的潜力,就在于它的轻量、灵活和低成本。
2. 芒果派MQ Quad开发环境准备与系统选择
2.1 硬件特性与系统镜像选型考量
芒果派MangoPi MQ Quad的核心是全志D1s,这是一颗单核的RISC-V 64位处理器,主频1GHz,配备了64MB的DDR2内存。这个配置在今天看来确实非常基础,甚至有些“复古”,但正是这种极致的精简,让它成为了学习、轻量级应用和边缘实验的绝佳平台。64MB内存是最大的挑战,这意味着我们每一步操作都要精打细算,不能像在x86服务器上那样“挥霍”。
官方和社区提供了多个系统镜像,对于我们的目标,我强烈推荐使用Debian的最小化镜像(Minimal Image)。Ubuntu或其他带有桌面环境的镜像会占用过多内存,在64MB的环境下可能开机就所剩无几了。Debian以其稳定和轻量著称,其最小化安装只包含最核心的系统组件,为我们后续部署Node.js和EMQX留出了宝贵的运行内存。
选择镜像时,务必确认是适用于D1s的RISC-V架构版本。下载后,通常是一个.img或.gz文件,需要使用工具如balenaEtcher或dd命令将其烧录到TF卡中。
2.2 系统初始化与基础优化
首次启动并登录系统后(默认用户密码通常是root/mangopi),有几项关键的初始化操作必须做,这直接关系到后续软件的安装和运行体验。
首先是换源。默认的软件源可能速度较慢,将APT源替换为国内镜像(如清华、阿里云、中科大的Debian Ports源)能极大提升软件包下载速度。编辑/etc/apt/sources.list文件,注释掉原有行,添加类似下面的内容(以清华源为例):
deb https://mirrors.tuna.tsinghua.edu.cn/debian-ports/ sid main deb-src https://mirrors.tuna.tsinghua.edu.cn/debian-ports/ sid main注意,对于RISC-V这类较新的架构,稳定的bullseye或bookworm版本可能软件包不全,我们常使用sid(不稳定版)或trixie(测试版)来获取最新的软件支持。换源后执行apt update更新列表。
其次是内存优化。64MB内存下,我们需要启用交换分区(Swap)来防止应用因内存不足(OOM)而被系统强制终止。如果TF卡空间充足,可以创建一个256MB的交换文件:
# 创建一个256MB的空文件 dd if=/dev/zero of=/swapfile bs=1M count=256 # 设置正确的权限 chmod 600 /swapfile # 格式化为交换分区 mkswap /swapfile # 立即启用 swapon /swapfile为了让系统开机自动挂载,需要将/swapfile swap swap defaults 0 0这行添加到/etc/fstab文件中。
最后是基础工具安装。安装一些必备的工具,如网络诊断工具curl、wget,进程查看工具htop,版本控制工具git等。
apt install -y curl wget htop git vim注意:在资源如此紧张的环境下,
apt install每一个包前都要三思,只安装绝对必需的。像vim相比nano更强大但也更重,可根据习惯选择。
3. Node.js环境部署:从源码编译到版本管理
3.1 放弃包管理,拥抱源码编译
在常见的ARM或x86架构上,我们可以通过NodeSource等仓库直接安装预编译的Node.js二进制包,非常方便。但在RISC-V架构上,特别是像D1s这样的新平台,预编译的二进制包非常罕见,且版本可能很旧。因此,从源码编译是唯一可靠的选择。
编译Node.js需要消耗大量的时间和CPU资源,对于单核1GHz的D1s来说,这将是一个漫长的过程(可能需要数小时)。但这确保了编译出的Node.js与我们的硬件和系统库完美兼容。
首先,安装编译所需的依赖工具链和库:
apt install -y gcc g++ make python3 pkg-configNode.js的编译脚本需要Python 3,所以这里安装的是python3。
3.2 下载源码与配置编译选项
我们选择长期支持版本(LTS),例如Node.js 18.x或20.x。LTS版本拥有更长的维护周期和更好的稳定性,适合生产环境。避免使用最新的Current版本,可能包含未知问题。
以Node.js 18.19.0为例:
# 下载源码包 wget https://nodejs.org/dist/v18.19.0/node-v18.19.0.tar.gz # 解压 tar -xzf node-v18.19.0.tar.gz cd node-v18.19.0接下来是关键的一步:配置编译选项。默认配置会包含所有模块和文档,这会导致编译出的二进制文件非常大,占用更多内存。我们需要进行精简:
./configure --prefix=/usr/local \ --with-intl=none \ --without-npm \ --without-dtrace \ --without-etw \ --without-report \ --without-node-snapshot--prefix=/usr/local:指定安装目录。--with-intl=none:禁用国际化(i18n)支持,可以显著减小体积。如果你的应用不需要多语言,可以关闭。--without-npm:这是一个重要的权衡。NPM是Node.js的包管理器,但它本身就是一个庞大的JavaScript应用。在64MB内存中,同时运行Node应用和NPM进程非常吃力。我们可以先编译一个不包含NPM的Node.js,包管理通过其他方式解决(见下文)。- 其他
--without-*选项都是用于禁用一些高级的调试、报告功能,以进一步减小体积。
3.3 漫长编译与安装
配置完成后,开始编译。使用-j2参数可以稍微利用一下并行编译,但D1s是单核,效果有限,主要是为了遵循Makefile的规范。
make -j2这个过程会非常漫长,请保持耐心。编译完成后,安装到系统:
make install安装完成后,执行node -v和node -p "process.arch",应该能看到版本号并确认架构是riscv64。
3.4 替代NPM的包管理方案
既然我们编译的Node.js不带NPM,如何安装第三方库呢?这里有几个方案:
- 使用
npx直接运行:Node.js自带npx,它可以临时下载并运行命令。例如,你想使用http-server启动一个静态服务器,可以直接运行npx http-server。npx会从网络下载包,但不会在本地永久安装。适合一次性任务。 - 在其它机器上安装并拷贝
node_modules:这是最实用的方法。在一台x86的电脑上,使用npm install安装好项目所需的所有依赖,然后将整个node_modules目录通过SCP或SFTP拷贝到芒果派上。由于Node.js的许多原生模块(addon)是平台相关的,所以必须确保开发机也是Linux系统,并且Node.js版本尽量一致。对于纯JavaScript的包,则跨平台兼容性很好。 - 使用轻量级包管理器:如
pnpm或yarn,它们相比NPM更节省磁盘空间。你可以先在开发机上用npm安装pnpm,然后用pnpm来管理依赖,再拷贝node_modules。
实操心得:对于芒果派MQ Quad这种极限环境,我强烈推荐方案2。在资源充沛的开发机上完成依赖的解析和下载,芒果派只负责运行,完美规避了其计算和网络能力的短板。将项目代码和
node_modules打包后,整个部署过程会变得非常干净利落。
4. EMQX 5.0 消息服务器部署与调优
4.1 EMQX版本选择与部署策略
EMQX是一个用Erlang/OTP语言开发的高性能MQTT Broker。Erlang虚拟机(BEAM)本身对内存有一定需求,这让在64MB内存上运行EMQX听起来像是个“不可能的任务”。但得益于EMQX 5.0版本在架构上的优化和对边缘场景的重视,通过极致的配置调优,我们有机会让它跑起来。
首先,绝对不能使用默认配置。默认配置是为服务器环境设计的,内存消耗可能在数百MB。我们必须使用为嵌入式或边缘计算优化的最小化配置。
访问EMQX官网的下载页面,找到适用于Linux的包。由于是RISC-V架构,我们同样需要下载源码进行编译。但编译Erlang和EMQX的复杂度远超Node.js。幸运的是,EMQX提供了另一种更友好的方式:Docker容器。
如果你的系统已经安装了Docker,那么通过Docker运行一个针对ARM64或RISC-V优化过的EMQX镜像,是最快捷、依赖最清晰的方式。但需要注意的是,Docker本身也会带来一些内存开销。如果内存实在捉襟见肘,我们还有最终方案:使用EMQX的独立二进制包,并配合手动调优。
4.2 使用精简二进制包与手动配置
EMQX为一些平台提供了独立的、包含Erlang运行时的二进制包。虽然官方可能没有直接提供RISC-V版本,但我们可以尝试寻找社区构建的版本,或者使用在架构上兼容的ARMv7或ARMv8版本(通过QEMU用户态模拟运行,性能损耗大,是最后的选择)。
这里假设我们找到了一个能在Debian RISC-V上运行的emqx-5.0.0-debian11-riscv64.tar.gz包。
# 解压到/opt目录 tar -xzf emqx-5.0.0-debian11-riscv64.tar.gz -C /opt cd /opt/emqx关键步骤是修改配置文件etc/emqx.conf。我们需要大刀阔斧地削减其资源占用:
# 主要调优参数 node.name = emqx@127.0.0.1 cluster.discovery = static cluster.static.seeds = emqx@127.0.0.1 # 监听端口 - 只开启最必要的1883 (MQTT) 和 8083 (WebSocket) listeners.tcp.default.bind = 0.0.0.0:1883 listeners.ws.default.bind = 0.0.0.0:8083 # 关闭不必要的监听器,如SSL、Dashboard(非常耗资源) # listeners.ssl.default.bind = 0.0.0.0:8883 # dashboard.listener.http.bind = 0.0.0.0:18083 # 核心性能调优:限制Erlang VM资源 node.process_limit = 1024 node.max_ports = 2048 node.max_ets_tables = 1024 # 关闭所有非核心功能 sysmon.os.sysmem_high_watermark = 70% sysmon.os.procmem_high_watermark = 70% # 关闭规则引擎(除非你需要) rule_engine.ignore_sys_message = true # 关闭数据桥接(除非你需要) bridges = disabled # 关闭主题重写 rewrite = disabled # 关闭延迟发布 delayed = disabled # 关闭共享订阅 shared_subscription = disabled # 认证和ACL可以暂时关闭以测试性能 # authentication = [] # authorization.sources = []4.3 启动、验证与监控
使用以下命令以后台模式启动EMQX:
./bin/emqx start使用./bin/emqx_ctl status检查运行状态。如果看到“Node ‘emqx@127.0.0.1’ is started”,说明启动成功。
现在,我们可以从同一网络下的另一台电脑,使用MQTT客户端工具(如MQTTX、mosquitto_pub/sub)进行测试。
# 在另一台机器上订阅主题 mosquitto_sub -h [芒果派IP] -t "test/topic" -v # 在另一台机器上发布消息 mosquitto_pub -h [芒果派IP] -t "test/topic" -m "Hello from MangoPi"如果订阅端能收到消息,说明EMQX已正常工作。
在芒果派上,可以使用htop命令观察EMQX的内存占用。经过上述调优,一个空载的EMQX 5.0进程内存占用有望控制在30-50MB左右,这在启用Swap的64MB系统上是可以勉强运行的。
重要警告:这个配置是极度精简的,牺牲了安全性(无SSL)、可观测性(无Dashboard)和大部分高级功能。它仅能提供最基础的MQTT消息路由。任何新增的连接、消息吞吐都会增加内存消耗。因此,这只适用于连接数极少(个位数)、消息频率很低的原型验证或学习场景,绝不能用于任何正式生产环境。
5. 集成测试:构建一个简单的物联网数据桥接示例
环境部署好了,我们来做一个简单的集成测试,验证Node.js和EMQX能否协同工作。这个示例模拟一个常见边缘场景:Node.js程序模拟一个温度传感器,定期生成数据并发布到EMQX;同时,它又订阅一个控制主题,接收来自云端的指令(比如修改上报频率)。
5.1 准备Node.js项目
在芒果派上创建一个项目目录,并初始化package.json。由于我们没有NPM,我们手动创建这个文件。
mkdir ~/iot-edge-demo && cd ~/iot-edge-demo创建package.json:
{ "name": "iot-edge-demo", "version": "1.0.0", "description": "A simple IoT edge demo", "main": "index.js", "dependencies": { "mqtt": "^4.3.7" } }注意,我们在dependencies中声明了需要mqtt库。按照之前的方案,我们需要在另一台有NPM的机器(Linux开发机)上安装它。
在开发机上:
mkdir iot-edge-demo && cd iot-edge-demo # 创建相同的package.json npm init -y npm install mqtt # 安装后,将整个node_modules目录打包 tar -czf node_modules.tar.gz node_modules将node_modules.tar.gz和下面的index.js文件一起传到芒果派的~/iot-edge-demo/目录,然后解压:
tar -xzf node_modules.tar.gz5.2 编写数据桥接脚本
创建index.js文件:
const mqtt = require('mqtt'); // 连接到本地EMQX服务器 const client = mqtt.connect('mqtt://127.0.0.1:1883'); let reportInterval = 5000; // 默认5秒上报一次 client.on('connect', () => { console.log('Connected to EMQX'); // 订阅控制主题 client.subscribe('device/001/control', (err) => { if (!err) { console.log('Subscribed to control topic'); } }); // 启动模拟数据上报 simulateSensor(); }); client.on('message', (topic, message) => { if (topic === 'device/001/control') { try { const command = JSON.parse(message.toString()); if (command.interval && command.interval > 0) { reportInterval = command.interval * 1000; // 转换为毫秒 console.log(`Report interval changed to ${command.interval} seconds`); } } catch (e) { console.error('Failed to parse control message:', e.message); } } }); function simulateSensor() { setInterval(() => { const simulatedData = { deviceId: 'sensor-001', timestamp: Date.now(), temperature: (20 + Math.random() * 10).toFixed(2), // 20-30度随机数 humidity: (50 + Math.random() * 20).toFixed(2) // 50-70%随机数 }; const payload = JSON.stringify(simulatedData); client.publish('sensor/data', payload); console.log(`Published: ${payload}`); }, reportInterval); } // 优雅退出 process.on('SIGINT', () => { console.log('Disconnecting...'); client.end(); process.exit(); });5.3 运行与双向测试
首先,确保EMQX正在运行。然后在项目目录下启动Node.js程序:
node index.js如果看到“Connected to EMQX”和定期发布的消息日志,说明数据上报链路通了。
现在进行控制测试。从你的开发机,向控制主题发布一条修改间隔的指令:
mosquitto_pub -h [芒果派IP] -t "device/001/control" -m '{"interval": 2}'观察芒果派上Node.js程序的输出,应该会看到“Report interval changed to 2 seconds”的日志,并且消息发布的频率明显加快。
这个简单的demo验证了:1) Node.js应用能稳定连接本机EMQX;2) 能通过MQTT协议进行数据上报(Pub)和指令接收(Sub);3) 两者在资源受限的芒果派上可以协同完成一个完整的“感知-通信-控制”闭环。
6. 性能监控、问题排查与优化实录
在64MB内存的极限环境下运行,稳定性挑战极大。以下是我在实操中遇到的一些典型问题及解决方法。
6.1 内存不足(OOM)问题与排查
这是最常见的问题。系统会因内存耗尽而变得卡顿,甚至杀死进程(EMQX或Node.js)。
排查步骤:
- 使用
htop或free -m命令:第一时间查看系统总内存、已用内存、缓存、交换分区使用情况。如果free内存长期为0,且swap使用率很高,说明内存严重不足。 - 定位罪魁祸首:使用
ps aux --sort=-%mem | head -10命令,按内存使用率排序,查看是哪个进程占用最多。 - 分析EMQX内存:进入EMQX安装目录,使用
./bin/emqx_ctl status查看其状态信息,里面通常包含内存使用概览。
优化与解决:
- 增加交换分区:如前所述,将Swap文件扩大到512MB甚至1GB(如果TF卡空间允许),为系统提供缓冲。
- 进一步精简EMQX:回头检查
emqx.conf,关闭任何可能遗漏的可选功能。考虑降级到EMQX 4.4的最后一个版本。EMQX 4.x系列在资源消耗上通常比5.x更友好一些,适合嵌入式场景。 - 优化Node.js应用:
- 检查是否有内存泄漏。确保定时器(
setInterval)被正确清理,避免闭包导致的对象无法释放。 - 减少全局变量和大对象的长期持有。
- 使用
--max-old-space-size参数启动Node.js,限制其最大堆内存。例如node --max-old-space-size=20 index.js,将其限制在20MB以内。
- 检查是否有内存泄漏。确保定时器(
- 终极方案:二选一:如果经过所有优化,内存依然紧张,可能需要面对现实:在芒果派MQ Quad上同时稳定运行Node.js和EMQX非常困难。可以考虑将架构改为:
- 方案A:只运行EMQX,将业务逻辑(Node.js程序)移到网络内另一台性能更强的设备(如树莓派4、旧手机、甚至一台常开的电脑)上,该设备作为客户端连接芒果派上的EMQX。
- 方案B:只运行Node.js,使用一个极轻量级的MQTT Broker库,例如
mosca(已归档)或aedes,让Node.js本身内嵌一个Broker。这样只有一个进程,内存管理更简单。
6.2 网络连接不稳定
现象:MQTT客户端(Node.js或远程工具)频繁断开重连。
排查与解决:
- 检查基础连接:
ping [芒果派IP],看是否有丢包或延迟过高。确保芒果派和客户端在同一局域网,且Wi-Fi信号稳定(如果使用Wi-Fi)。 - 检查EMQX日志:
/opt/emqx/log目录下的日志文件,特别是emqx.log.1,查看是否有错误信息。 - 调整MQTT Keepalive:在Node.js的MQTT连接配置中,增加
keepalive参数(单位秒),并设置合理的connectTimeout。这告诉服务器,如果在这个时间内没有消息,客户端依然存活。const client = mqtt.connect('mqtt://127.0.0.1:1883', { keepalive: 60, connectTimeout: 4000 }); - 关闭EMQX的负载保护:在极限测试时,EMQX可能因为连接数或消息速率超过其低配能力而主动拒绝。可以在
emqx.conf中暂时调高或关闭相关阈值(生产环境慎用):zone.external.max_connections = 10 zone.external.rate_limit.conn_messages.in = 100,10s
6.3 进程意外退出与守护
问题:Node.js应用可能因为未捕获的异常而退出。
解决:
- 使用进程守护工具:如
pm2。但pm2本身也有开销。更轻量级的选择是使用系统自带的systemd。 - 创建systemd服务(推荐):为你的Node.js应用创建一个服务文件,如
/etc/systemd/system/iot-demo.service。
然后使用[Unit] Description=IoT Edge Demo Service After=network.target [Service] Type=simple User=root WorkingDirectory=/home/yourname/iot-edge-demo ExecStart=/usr/local/bin/node index.js Restart=on-failure RestartSec=10 StandardOutput=syslog StandardError=syslog [Install] WantedBy=multi-user.targetsystemctl start iot-demo启动,systemctl enable iot-demo设置开机自启。这样应用崩溃后会自动重启。
对于EMQX,它本身已经通过./bin/emqx start以后台守护进程方式运行,相对稳定。
经过这一系列的部署、调优和问题打磨,这台小小的芒果派MQ Quad已经从一个简单的开发板,转变为一个能够执行特定边缘计算任务的微型服务器。整个过程充满了对资源极限的挑战,也让我们深刻体会到在约束条件下进行软件设计和选型的重要性。这种经验,对于开发真正的低成本、低功耗物联网设备,是一笔宝贵的财富。