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 bs1M count256 # 设置正确的权限 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-intlnone \ --without-npm \ --without-dtrace \ --without-etw \ --without-report \ --without-node-snapshot--prefix/usr/local指定安装目录。--with-intlnone禁用国际化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 emqx127.0.0.1 cluster.discovery static cluster.static.seeds emqx127.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 ‘emqx127.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应用能稳定连接本机EMQX2) 能通过MQTT协议进行数据上报Pub和指令接收Sub3) 两者在资源受限的芒果派上可以协同完成一个完整的“感知-通信-控制”闭环。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-size20 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,10s6.3 进程意外退出与守护问题Node.js应用可能因为未捕获的异常而退出。解决使用进程守护工具如pm2。但pm2本身也有开销。更轻量级的选择是使用系统自带的systemd。创建systemd服务推荐为你的Node.js应用创建一个服务文件如/etc/systemd/system/iot-demo.service。[Unit] DescriptionIoT Edge Demo Service Afternetwork.target [Service] Typesimple Userroot WorkingDirectory/home/yourname/iot-edge-demo ExecStart/usr/local/bin/node index.js Restarton-failure RestartSec10 StandardOutputsyslog StandardErrorsyslog [Install] WantedBymulti-user.target然后使用systemctl start iot-demo启动systemctl enable iot-demo设置开机自启。这样应用崩溃后会自动重启。对于EMQX它本身已经通过./bin/emqx start以后台守护进程方式运行相对稳定。经过这一系列的部署、调优和问题打磨这台小小的芒果派MQ Quad已经从一个简单的开发板转变为一个能够执行特定边缘计算任务的微型服务器。整个过程充满了对资源极限的挑战也让我们深刻体会到在约束条件下进行软件设计和选型的重要性。这种经验对于开发真正的低成本、低功耗物联网设备是一笔宝贵的财富。