深入解密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 分钟前
k8s常用总结
运维·后端·k8s
TheITSea15 分钟前
后端开发 SpringBoot 工程模板
spring boot·后端
Asthenia041217 分钟前
编译原理中的词法分析器:从文本到符号的桥梁
后端
朴拙数科27 分钟前
技术长期主义:用本分思维重构JavaScript逆向知识体系(一)Babel、AST、ES6+、ES5、浏览器环境、Node.js环境的关系和处理流程
javascript·重构·es6
Asthenia041239 分钟前
用RocketMQ和MyBatis实现下单-减库存-扣钱的事务一致性
后端
Pasregret1 小时前
04-深入解析 Spring 事务管理原理及源码
java·数据库·后端·spring·oracle
Micro麦可乐1 小时前
最新Spring Security实战教程(七)方法级安全控制@PreAuthorize注解的灵活运用
java·spring boot·后端·spring·intellij-idea·spring security
returnShitBoy1 小时前
Go语言中的defer关键字有什么作用?
开发语言·后端·golang
拉不动的猪1 小时前
vue与react的简单问答
前端·javascript·面试