深入解密Node共享内存:这个原生模块让你的多进程应用性能翻倍

来源:程序员指北

概述

Node-Shared-Cache 是一个用于 Node.js 的进程间共享内存缓存模块,它提供了在多个 Node.js 进程间高效共享数据的能力,可通过 npm 安装。

Native Module 函数调用链路

对于这种与 V8 函数直接交互开发的原生模块我们可以称之为 Native Module(原生模块) 或者 Native Addon(原生扩展)

技术栈

核心技术

  1. C/C++ : 模块核心功能使用 C/C++ 实现
  2. Node.js 原生模块: 通过 Node.js 的 N-API 暴露底层功能
  3. NAN (Native Abstractions for Node.js) : 提供跨 Node.js 版本的兼容性抽象
  4. 内存管理: 使用共享内存实现进程间通信
  5. 序列化/反序列化: 自定义 BSON 格式用于高效数据存储
  6. 锁机制: 实现跨进程数据同步

这其中几个核心技术我会下文会详细讲解

NAN 框架

NAN (Native Abstractions for Node.js) 可以理解为 Node.js 原生模块开发框架,它的主要作用:

  1. 版本兼容性:
  • 提供跨 Node.js 版本的 API 抽象
  • 处理不同 Node.js 版本的 V8 API 变化
  • 简化原生模块的开发和维护
  1. 主要功能:
  • 提供统一的V8 API封装
  • 处理异步操作
  • 管理对象生命周期
  • 提供类型转换工具
  1. 源码中典型 NAN 使用示例:JavaScript 属性获取器的实现
scss 复制代码
static NAN_PROPERTY_GETTER(getter) {
    // nan提供的API,用于处理属性获取,使用NAN宏生成属性作用域代码????
    PROPERTY_SCOPE(property, info.Holder(), ptr, fd, keyLen, keyBuf);
     // 使用BSON解析器
    bson::BSONParser parser;

    // 调用核心C++实现获取值
    cache::get(ptr, fd, keyBuf, keyLen, parser.val, parser.valLen);

    // 使用NAN抽象设置返回值
    if(parser.val) {
        info.GetReturnValue().Set(parser.parse());
    }
}
  1. NAN 简化例子(抽象不同Node.js版本的V8 API)
scss 复制代码
// 不使用 NAN 的版本 (针对特定 Node.js 版本)
void Add(const v8::FunctionCallbackInfo<v8::Value>& args) {
    v8::Isolate* isolate = args.GetIsolate();
    double value = args[0]->NumberValue() + args[1]->NumberValue();
    args.GetReturnValue().Set(v8::Number::New(isolate, value));
}

// 使用 NAN 的版本 (跨 Node.js 版本兼容)
NAN_METHOD(Add) {
    double value = Nan::To<double>(info[0]).FromJust() + 
                   Nan::To<double>(info[1]).FromJust();
    info.GetReturnValue().Set(value);
}

// 绑定到 JS 的方式也不同
// 不使用 NAN:
exports->Set(v8::String::NewFromUtf8(isolate, "add"), 
             v8::FunctionTemplate::New(isolate, Add)->GetFunction());

// 使用 NAN:
Nan::Set(exports, Nan::New("add").ToLocalChecked(), 
         Nan::GetFunction(Nan::New<v8::FunctionTemplate>(Add)).ToLocalChecked());
  1. 为什么选择 NAN
  • 稳定性好,维护活跃
  • 社区支持广泛
  • 简化了原生模块开发
  • 提供了跨版本兼容性(注意是编译时兼容,无需重新编译)

但是尽管如此,其实NAN的跨版本是有局限性的,这个项目我想应该是历史原因使用的NAN,每个Node.js版本都需要重新编译,维护成本较高,分发和部署复杂。其实Node官方和个人更推荐新项目中使用Node-API来开发原生Node模块。Node-API运行时兼容,无需多个版本分别编译,而且Node-API是Node项目中的一部分,不需安装模块。 Node-API无论是未来趋势还是兼容性以及维护成本都会更好些。
Node-API 官方地址:nodejs.org/api/n-api.h...

NAN 不是绑定层,而是一个抽象层,它简化了 Node.js 原生模块的开发,使得开发者可以专注于业务逻辑而不是处理不同 Node.js 版本的API差异。(Node-API也是抽象层)

  1. 绑定层与抽象层的区别
  • 绑定层:通过一个绑定层以后,绑定层主要负责将C++函数导出 JavaScript(如NODE_MODULE)
  • 抽象层:一般使用 NAN,提供统一API来处理不同版本的 V8引擎差异(减少维护成本,避免针对不同 Node.js版本编写不同代码)

进程锁

进程锁是一种同步机制,用于进程锁是一种同步机制,用于协调多个进程对共享资源的访问,确保在任何时刻只有一个进程可以访问该资源。在node-shared-cache中,这个共享资源就是共享内存。

锁类型
  1. 互斥锁(Mutex):
  • 最基本的锁类型
  • 同一时间只允许一个进程访问资源
  • 其他进程必须等待锁释放
  1. 读写锁(Read-Write Lock):
  • 允许多个进程同时读取
  • 但写入时需要独占访问
  • 提高并发性能
  1. 自旋锁(Spin Lock):
  • 进程在等待锁时持续检查锁状态
  • 适用于短期等待场景
  • 避免进程切换开销
为什么需要进程锁?
  1. 数据一致性问题:
  • 没有进程锁,多个进程同时修改共享内存会导致数据不一致
  • 例如:进程 A 读取值为 10,进程 B 同时读取值为10AB 都加 1 并写回,最终结果为11 而不是预期的 12
  1. 竞态条件(Race Condition):
  • 操作的顺序会影响最终结果
  • 没有锁机制,操作顺序无法保证,导致不可预测的结果
  1. 原子性保证:
  • 复杂操作(如读取-修改-写入)需要作为一个整体完成
  • 锁确保这些操作不会被其他进程中断

举个例子:如果两个 Node.js 进程同时操作共享缓存:

css 复制代码
   进程A: 读取key="user1" -> 值为{visits:5}
   进程B: 同时读取key="user1" -> 也得到{visits:5}
   进程A: 增加visits并写回 -> {visits:6}
   进程B: 也增加visits并写回 -> {visits:6}  (覆盖了A的更新)

结果:用户访问计数丢失了一次增加。使用锁后:

css 复制代码
   进程A: 获取锁 -> 读取{visits:5} -> 写入{visits:6} -> 释放锁
   进程B: 等待锁被释放 -> 获取锁 -> 读取{visits:6} -> 写入{visits:7} -> 释放锁

node-shared-cache 中进程锁实现:

node-shared-cache 中的进程锁实现:

  1. 提供了基础的互斥锁和高级的读写锁 基础互斥锁:
scss 复制代码
typedef int32_t mutex_t;

// 原子操作宏定义
#define TSL(mutex) xchg(mutex, 1)
#define SPIN(mutex) while(TSL(mutex))

读写锁:

arduino 复制代码
typedef struct {
    mutex_t count_lock;    // 用于保护读者计数
    mutex_t mutex;         // 主互斥锁
    uint32_t readers;      // 当前读者数量
} rw_lock_t;
  1. 针对不同平台(Linux/其他)有优化实现
  2. 使用 RAII 方式管理锁的生命周期
  3. 通过原子操作保证锁的正确性

模块支持平台

  • Linux` (使用 -lrt 库)
  • MacOS
  • Windows

模块核心功能

1. 共享内存缓存

  • 通过系统级共享内存实现进程间数据共享
  • 使用 LRU (最近最少使用) 算法管理缓存内容
  • 自动内存管理,防止内存泄漏

2. 数据操作

  • 属性的读取与写入 (obj.key = value, obj.key)
  • 属性的删除 (delete obj.key)
  • 遍历缓存内容 (for(var k in obj), Object.keys(obj))
  • 原子增加操作 (increase(obj, key, value))
  • 原子交换操作 (exchange(obj, key, newValue))
  • 快速读取,不影响 LRU 顺序 (fastGet(obj, key))

3. 内存管理

  • 支持定制内存块大小 (从 64 字节到 16KB)
  • 支持内存清理与释放
  • 内存使用效率高,采用块式分配

技术实现

核心模块组成

  1. binding.cc: 实现 Node.js 和 C++ 的桥接层
    • 创建 Cache 对象
    • 处理 JS 对象属性的获取与设置
    • 绑定原生方法到 JS 接口
  2. memcache.cc/h: 实现内存缓存的核心功能
    • 内存块分配与回收
    • 哈希表数据结构
    • LRU 缓存算法
    • 锁同步机制
  3. bson.cc/h: 实现数据序列化与反序列化
    • 支持基本类型: null, undefined, true, false, int32, number
    • 支持复杂类型: string, array, object
    • 处理循环引用 (circular reference)
  4. lock.h: 实现跨平台的锁机制
    • Unix/Linux: 使用文件锁 (flock)
    • Windows: 使用互斥锁 (Mutex)

内存结构

scss 复制代码
+----------------+
| 哈希表 (65536) |
+----------------+
| 元数据信息     |  - 魔数 (magic)
|                |  - 总块数 (blocks_total)
|                |  - 可用块数 (blocks_available)
|                |  - 脏标志 (dirty)
|                |  - 块大小 (block_size_shift)
+----------------+
| 数据块区域     |  - 键值对存储
|                |  - LRU 链表
+----------------+

同步机制

为了防止多进程访问冲突,模块实现了两种锁:

  1. 读锁 (read_lock) : 多个进程可同时获得读锁
  2. 写锁 (write_lock) : 独占锁,确保只有一个进程可以修改数据

性能优化

  1. 哈希表: O(1) 的键查找性能
  2. LRU 算法: 高效管理内存使用
  3. 原子操作: 增加和交换支持原子性
  4. 快速读取: 提供不影响 LRU 顺序的快速读取方法

使用场景

  1. 多进程 Web 服务器: 共享会话数据、用户信息等
  2. 分布式计数器: 跨进程原子计数
  3. 分布式锁: 通过 exchange 方法实现
  4. 进程间通信: 用于进程间高效数据交换
  5. 高性能缓存: 替代 Redis 等外部缓存系统,减少网络开销

局限性

  1. 键长度限制: 依赖于块大小,默认情况下最大为 16 字符
  2. 内存大小限制: 由于设计限制,最大支持 128MB 共享内存
  3. 崩溃恢复: 需要谨慎处理进程崩溃情况下的锁释放
  4. 平台差异: 不同平台的实现细节存在差异

结论

Node-Shared-Cache 是一个高性能的进程间共享内存解决方案,通过 C++ 实现的底层优化提供了优异的性能表现。其核心价值在于解决了 Node.js 多进程架构下的数据共享问题,特别适合需要高性能缓存和进程间通信的应用场景。

相比于使用外部缓存系统 (如 Redis),它具有更低的延迟和更简单的部署优势,但也有内存大小和数据持久性等方面的局限性。

github地址:github.com/kyriosli/no...

相关推荐
星星在线2 小时前
MusicFree:一个「All in One」的个人音乐服务器,让听歌回归简单
前端·后端
IT_陈寒3 小时前
Redis的SETNX并发问题让我加了三天班
前端·人工智能·后端
demo007x3 小时前
Docling 文档转换以及技术架构分析
前端·后端·程序员
京东云开发者4 小时前
京东市民服务又“上新”!这次是黑龙江“龙易办”
前端
袋鱼不重5 小时前
我的神奇同事,AI 用多了居然写了个 Open In Codex
前端·后端·ai编程
用户8356290780515 小时前
使用 Python 操作 Word 内容控件
后端·python
像我这样帅的人丶你还5 小时前
啥? 前端也要会干Java?🛵🛵🛵
后端
竹林8185 小时前
Web3表单签名验证:我用 wagmi 和 ethers 给 DApp 加了一个“免密登录”,踩坑记录全在这了
javascript
Hommy885 小时前
【剪映小助手】添加贴纸接口(Add Sticker)
后端·github·剪映小助手·视频剪辑自动化·剪映api
用户6990304848755 小时前
try catch使用场景 处理同步代码错误兼容用的
javascript·uni-app