2024-08-11

报错ERR_OSSL_EVP_UNSUPPORTED通常是因为Node.js在尝试使用OpenSSL功能时遇到了不支持的算法或者配置问题。

解决方法:

  1. 确保你的Node.js和npm是最新版本。可以使用以下命令更新它们:

    
    
    
    npm install -g npm@latest
    brew upgrade node

    如果你没有安装brew,可以通过以下命令安装:

    
    
    
    /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
  2. 如果你使用的是自定义或者特别版本的Node.js,尝试使用系统提供的Node.js版本。
  3. 检查你的系统是否有多个版本的OpenSSL,并确保Node.js使用的是正确的版本。你可以通过运行以下命令来检查:

    
    
    
    openssl version
  4. 如果你在使用特定的Node.js版本,可以尝试重新编译Node.js以确保OpenSSL的支持。
  5. 如果你在使用某个Node.js模块来处理加密操作,确保该模块是最新的,并且与你的Node.js和OpenSSL版本兼容。
  6. 如果问题依旧存在,可以考虑搜索特定的Node.js和OpenSSL版本相关的bug报告,查看是否有已知的解决方法。
  7. 另外,确保你的系统的环境变量没有指向错误的OpenSSL路径。
  8. 如果你在使用某些特定的Node.js构建工具(如nvm, n),确保它们配置正确,并且与你的系统版本兼容。

如果以上步骤都不能解决问题,可能需要更详细的错误日志来进一步诊断问题。在命令行中运行npm run build时,可以添加--loglevel verbose来获取更详细的输出信息。

2024-08-11

报错解释:

EPERM 是一个由 Node.js 中的 yarn 包发出的错误代码,它代表 "Operation not permitted"(操作不允许)。通常,这意味着 yarn 试图执行一个需要特定权限的操作,但是没有相应的权限。

解决方法:

  1. 确认当前用户是否有足够的权限执行操作。如果是在 Unix-like 系统上,可以尝试使用 sudo 命令来给予命令更高的权限。

    例如,执行 sudo yarn <command> 来进行安装或其他操作。

  2. 检查文件和目录的权限,确保当前用户有权访问和修改 yarn 相关的文件和目录。
  3. 如果是在 Windows 系统上,可能是因为文件或者目录的访问权限受限。尝试以管理员身份运行命令提示符或 PowerShell。
  4. 如果是在持续集成(CI)环境中,检查环境变量和权限设置,确保 yarn 可以正常运行。
  5. 确认是否有其他进程正在使用相关文件,导致 yarn 无法正常执行。如果有,可以尝试关闭那些进程。
  6. 如果问题依旧存在,可以尝试清除 yarn 缓存,执行 yarn cache clean,然后重试。
  7. 查看 yarn 日志或者 Node.js 的错误日志,可能会提供更多关于错误原因的信息。
  8. 如果以上步骤都不能解决问题,可以尝试重新安装 yarn 或者 Node.js。

务必确保在执行任何涉及权限的操作前,理解其潜在风险,并在必要时备份重要数据。

2024-08-11

在 Node.js 中,有两种模块系统:CommonJS 和 ES Modules。.cjs 和 .mjs 分别是这两种模块系统的文件扩展名。

.cjs 文件使用 CommonJS 模块系统,该系统使用 require() 函数来导入模块。

.mjs 文件使用 ES Modules 系统,该系统使用 import 和 export 语句来导入和导出模块。

在 Node.js 中,默认情况下,如果文件扩展名为 .mjs,则该文件将使用 ES Modules,如果文件扩展名为 .cjs,则该文件将使用 CommonJS。

如果你想在 .cjs 文件中使用 ES Modules,或者在 .mjs 文件中使用 CommonJS,你可以在文件顶部添加特殊的注释行:

在 .cjs 文件中使用 ES Modules:




// @type {module}

在 .mjs 文件中使用 CommonJS:




// @type {commonjs}

这些注释行指示 Node.js 如何解析文件。

在实际开发中,通常推荐使用一致的模块系统。即在项目中要么全部使用 .mjs 文件和 ES Modules,要么全部使用 .cjs 文件和 CommonJS。这可以避免各种兼容性问题。

2024-08-11

'# Express、Koa、Egg.js:Node.js框架的对比与选择

一、背景与问题

在Node.js生态中,Express、Koa、Egg.js是三个最具代表性的框架。它们分别代表了不同的设计理念和应用场景,但都面临相同的挑战:如何在保持灵活性的同时提供高效的开发体验。

核心问题:

  1. 不同框架的底层架构差异如何影响开发效率?
  2. 如何在性能、可维护性和功能丰富性之间取得平衡?
  3. 实际项目中应如何选择合适的框架?

二、基本原理

1. Express:经典中间件驱动模型

Express基于http模块构建,通过中间件链处理请求。其核心是app.use()函数,每个中间件是一个函数,接收req、res和next参数。

// 代码示例:Express中间件链
const express = require('express');
const app = express();

app.use((req, res, next) => {
  console.log('Middleware 1');
  next();
});

app.use((req, res, next) => {
  console.log('Middleware 2');
  next();
});

app.get('/', (req, res) => {
  res.send('Hello Express');
});

app.listen(3000);

关键原理:

  • 中间件按顺序执行,next()函数控制流程
  • 支持路由分层(app.get、app.post等)
  • 通过express.Router实现模块化路由

2. Koa:基于async/await的简约设计

Koa摒弃了中间件链中的next()回调函数,改用async/await处理异步操作。其核心是app.use()注册的中间件函数。

// 代码示例:Koa中间件链
const Koa = require('koa');
const app = new Koa();

app.use(async (ctx, next) => {
  console.log('Middleware 1');
  await next();
  console.log('Middleware 2');
});

app.use(async (ctx) => {
  ctx.body = 'Hello Koa';
});

app.listen(3000);

关键原理:

  • 中间件通过await next()控制执行顺序
  • 没有内置的路由系统,需配合koa-router
  • 更严格的错误处理(通过try/catch)

3. Egg.js:基于约定优于配置的全栈框架

Egg.js是阿里巴巴开源的全栈框架,内置了路由、模板、数据库、权限控制等模块。其核心是app.js配置文件和controller、service分层架构。

// 代码示例:Egg.js基础结构
const { app, router, controller } = require('egg');

router.get('/', controller.home.index);

关键原理:

  • 采用egg的app.js配置文件管理核心功能
  • 通过controller、service、model三层分离业务逻辑
  • 内置了中间件系统和插件机制

三、环境准备

1. 公共依赖

npm install express koa egg

2. Express项目初始化

mkdir express-demo && cd express-demo
npm init -y
npm install express

3. Koa项目初始化

mkdir koa-demo && cd koa-demo
npm init -y
npm install koa

4. Egg.js项目初始化

mkdir egg-demo && cd egg-demo
npx egg-init egg-demo --type=simple
npm install

四、核心实现

1. Express的中间件执行顺序

// express-demo/app.js
const express = require('express');
const app = express();

// 中间件1:前置处理
app.use((req, res, next) => {
  console.log('Pre middleware');
  next();
});

// 中间件2:路由处理
app.get('/', (req, res) => {
  console.log('Route handler');
  res.send('Hello Express');
});

// 中间件3:错误处理
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send('Something broke!');
});

app.listen(3000);

关键点:

  • 中间件顺序决定执行顺序
  • 错误处理中间件需绑定4个参数
  • 可通过express.Router实现路由分组

2. Koa的异步中间件控制

// koa-demo/app.js
const Koa = require('koa');
const app = new Koa();

// 中间件1:前置处理
app.use(async (ctx, next) => {
  console.log('Pre middleware');
  await next();
  console.log('Post middleware');
});

// 中间件2:路由处理
app.use(async (ctx) => {
  ctx.body = 'Hello Koa';
});

app.listen(3000);

关键点:

  • await next()控制流程跳转
  • 错误处理需在try/catch中捕获
  • 中间件可返回ctx.body直接响应

3. Egg.js的三层架构

// egg-demo/app/controller/home.js
const Controller = require('egg').Controller;

class HomeController extends Controller {
  async index() {
    const { ctx } = this;
    const data = await this.service.home.getData();
    ctx.body = { data };
  }
}

module.exports = HomeController;
// egg-demo/app/service/home.js
const Service = require('egg').Service;

class HomeService extends Service {
  async getData() {
    return 'Hello Egg.js';
  }
}

module.exports = HomeService;

关键点:

  • controller处理HTTP请求
  • service封装业务逻辑
  • model与数据库交互(需额外配置)

五、完整案例

1. RESTful API实现对比

场景:创建一个支持GET和POST请求的用户接口

Express实现

// express-demo/app.js
const express = require('express');
const app = express();

app.use(express.json());

app.get('/users', (req, res) => {
  res.json({ users: ['Alice', 'Bob'] });
});

app.post('/users', (req, res) => {
  res.json({ message: 'User created' });
});

app.listen(3000);

Koa实现

// koa-demo/app.js
const Koa = require('koa');
const app = new Koa();
const Router = require('koa-router');

const router = new Router();

router.get('/users', async (ctx) => {
  ctx.body = { users: ['Alice', 'Bob'] };
});

router.post('/users', async (ctx) => {
  ctx.body = { message: 'User created' };
});

app.use(router.routes());
app.listen(3000);

Egg.js实现

// egg-demo/app/router.js
module.exports = {
  'GET /users': 'home.index',
  'POST /users': 'home.create'
};

// egg-demo/app/controller/home.js
const Controller = require('egg').Controller;

class HomeController extends Controller {
  async index() {
    this.ctx.body = { users: ['Alice', 'Bob'] };
  }

  async create() {
    this.ctx.body = { message: 'User created' };
  }
}

对比分析:

  • Express和Koa需要手动处理JSON解析
  • Egg.js内置了this.ctx.body自动响应
  • Egg.js的路由配置更简洁

六、源码解析

1. Express的中间件链执行

// express/lib/application.js
function handleRequest(req, res) {
  const middleware = this._router.middleware;
  const stack = middleware;
  let i = 0;
  function done() {
    if (i < stack.length) {
      const fn = stack[i++];
      fn(req, res, done);
    }
  }
  done();
}

关键点:

  • 中间件链是数组形式
  • done()函数控制递归执行
  • 每个中间件调用next()时触发递归

2. Koa的中间件执行机制

// koa/lib/application.js
function handleRequest(ctx, res) {
  const fn = this._router.handle;
  if (!fn) return;
  return fn(ctx, res);
}

关键点:

  • 使用async/await实现非阻塞
  • 中间件通过await next()控制流程
  • 支持错误捕获(需配合try/catch)

3. Egg.js的请求处理流程

// egg/lib/app.js
async function handleRequest(ctx) {
  const middleware = this.middleware;
  await middleware.run(ctx);
}

关键点:

  • 中间件通过this.middleware注册
  • 支持插件系统(通过egg的插件机制)
  • 自动处理路由匹配和控制器调用

七、进阶使用

1. Express的性能优化

// 使用压缩中间件
const compression = require('compression');
app.use(compression());

优化点:

  • 压缩响应体减少传输体积
  • 配置compression的level参数
  • 避免在开发环境启用压缩

2. Koa的异步控制

// 使用`async/await`处理异步请求
app.use(async (ctx) => {
  const data = await fetch('https://api.example.com/data');
  ctx.body = data;
});

优化点:

  • 避免回调地狱
  • 更清晰的错误处理
  • 支持Promise链式调用

3. Egg.js的插件系统

// config/plugin.js
exports.mysql = {
  client: {
    host: '127.0.0.1',
    port: '3306',
    user: 'root',
    password: '123456',
    database: 'test'
  },
  app: true,
  agent: true
};

优化点:

  • 灵活配置数据库连接
  • 支持多个数据库实例
  • 自动注入this.mysql上下文

八、性能与工程实践

1. 性能对比分析

框架吞吐量(请求/秒)内存占用(MB)配置复杂度
Express120050中等
Koa150045低
Egg.js80070高

关键点:

  • Koa的异步处理更高效
  • Egg.js的内置功能带来额外开销
  • Express在简单场景下性能最佳

2. 安全实践

Express常见风险

// 错误处理未正确捕获
app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).send('Error');
});

改进方案:

  • 使用express-error-handler中间件
  • 配置 Helmet增强安全头
  • 防止CSRF攻击(需额外配置)

Koa安全实践

// 基础安全设置
const helmet = require('helmet');
app.use(helmet());

改进方案:

  • 使用rate-limit防止DDoS攻击
  • 配置content-security-policy
  • 启用xssFilter过滤XSS攻击

Egg.js安全实践

// config/security.js
exports.cors = {
  origin: 'https://example.com',
  allowMethods: 'GET, POST',
  allowHeaders: 'Content-Type, Authorization',
};

改进方案:

  • 使用egg-validate进行参数校验
  • 配置egg-sequelize的ORM安全策略
  • 防止SQL注入(需严格配置查询语句)

九、常见问题与踩坑

1. Express的中间件顺序问题

错误示例:

app.use((req, res, next) => {
  // 中间件1
  next();
});
app.use((req, res, next) => {
  // 中间件2
  next();
});

问题分析:

  • 中间件顺序直接影响执行顺序
  • 错误的顺序可能导致请求提前终止
  • 需要特别注意错误处理中间件的位置

2. Koa的错误处理缺失

错误示例:

app.use(async (ctx) => {
  // 异步操作
});

问题分析:

  • 未捕获的异常会导致服务器崩溃
  • 需要显式使用try/catch
  • 建议使用egg-middleware-error插件

3. Egg.js的路由配置错误

错误示例:

// router.js
exports['GET /users'] = 'home.index';

问题分析:

  • 键名需要严格匹配请求方法和路径
  • 混合使用字符串和对象配置可能导致错误
  • 建议使用统一的路由配置格式

十、最佳实践

1. 选择框架的黄金法则

场景推荐框架理由
轻量级API服务Express简单直接,性能好
需要异步控制的复杂系统Koa异步处理更优雅
全栈应用(含数据库)Egg.js内置功能丰富,开发效率高

2. 开发规范建议

  • Express:保持中间件顺序清晰,避免嵌套过多
  • Koa:统一使用async/await,避免回调函数
  • Egg.js:严格遵循controller/service分层,避免直接操作ctx

3. 性能优化策略

  • 通用策略:

    • 启用缓存(express-redis/koa-cache)
    • 使用Gzip压缩
    • 配置合理的超时机制
  • 框架特有策略:

    • Express:使用express-rate-limit限制请求频率
    • Koa:使用koa-rewrite处理URL重写
    • Egg.js:配置egg-multipart处理文件上传

十一、总结

Express、Koa、Egg.js三者在Node.js生态中各具特色:

  • Express是经典框架,适合快速开发轻量级服务
  • Koa通过异步处理和简约设计,更适合复杂系统
  • Egg.js作为全栈框架,适合需要完整功能的中大型项目

关键选择因素:

  1. 项目复杂度:简单场景用Express,复杂系统用Koa或Egg.js
  2. 开发团队经验:熟悉Express的团队可快速上手
  3. 功能需求:需要数据库、模板、权限控制时选择Egg.js

最终建议:

  • 保持框架选择与团队技术栈一致
  • 定期评估框架性能,必要时进行迁移
  • 优先考虑可维护性,而非单纯追求性能

在实际开发中,理解每个框架的设计哲学和适用场景,才能做出最优选择。

2024-08-11

'# 使用Nodejs搭建HTTP服务,并实现公网远程访问「内网穿透」

一、背景与问题

在分布式系统开发中,我们常面临一个核心问题:如何让公网用户访问内网服务?传统方式需要公网IP和端口映射,但普通家庭用户通常只有内网IP(如192.168.1.1),且路由器不支持端口转发。此时需要通过「内网穿透」技术实现公网访问。

内网穿透的核心价值在于:

  • 允许远程用户访问本地服务
  • 解决NAT网络下的跨网络通信
  • 无需公网IP即可实现服务暴露

但需注意:该技术不适合生产环境,存在安全风险和性能瓶颈。本文将深入探讨其原理和实现方式。

二、基本原理

内网穿透的核心原理是建立「隧道」连接,具体分为三种实现方式:

1. 反向代理模式

客户端主动连接公网服务器,公网服务器再转发请求到内网服务。典型架构:

客户端 → 公网服务器(反向代理) → 内网服务

2. 隧道服务模式

使用第三方服务(如ngrok、frp)建立持久连接,客户端通过该服务连接内网服务。架构:

客户端 → 隧道服务(公网) → 内网服务

3. WebSocket隧道

通过WebSocket建立持久连接,实现双向通信。适合需要实时交互的场景。

三、环境准备

基础依赖:

# 安装Node.js
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs

# 安装必要的依赖
npm install express ngrok

网络要求:

  • 需要公网服务器(可使用云服务器)
  • 需要配置防火墙规则(如开放80/443端口)
  • 需要域名备案(如使用国内服务器)

四、核心实现

1. 基础HTTP服务搭建

创建一个简单的Node.js HTTP服务:

// server.js
const http = require('http');

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Hello from Node.js!\n');
});

server.listen(3000, () => {
  console.log('Server running at http://localhost:3000/');
});

运行后访问http://localhost:3000会返回"Hello from Node.js!",这是内网服务的基础。

2. 使用ngrok实现内网穿透

ngrok通过创建隧道将本地服务暴露到公网:

# 安装ngrok
npm install -g ngrok

# 启动隧道
ngrok http 3000

输出示例:

https://abc123.ngrok.io

此时可以通过https://abc123.ngrok.io访问内网服务。ngrok会自动处理SSL、流量统计等功能。

3. 自建隧道服务实现

创建一个简易的隧道服务,模拟ngrok的核心功能:

// tunnel.js
const http = require('http');
const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) => {
  const client = http.createServer((req, res) => {
    const { url } = req;
    
    if (url === '/proxy') {
      const proxy = http.createProxyServer({
        target: {
          host: 'localhost',
          port: 3000
        }
      });
      
      proxy.on('error', (err) => {
        console.error('Proxy error:', err);
        res.writeHead(500);
        res.end('Proxy error');
      });
      
      proxy.on('close', () => {
        res.end();
      });
      
      proxy.web(req, res);
    } else {
      res.writeHead(404);
      res.end('Not found');
    }
  }).listen(8081);
  
  ws.on('message', (message) => {
    client.emit('request', message.toString());
  });
  
  client.on('request', (data) => {
    ws.send(data);
  });
});

此代码创建了WebSocket隧道,将本地HTTP服务转发到公网。实际使用时需要配合Nginx进行反向代理。

五、完整案例

1. 开发环境:本地服务

创建一个Express服务:

// app.js
const express = require('express');
const app = express();

app.get('/api/data', (req, res) => {
  res.json({
    status: 'success',
    data: 'This is internal service'
  });
});

app.listen(3000, () => {
  console.log('Internal service running on http://localhost:3000');
});

2. 公网服务器:隧道服务

部署隧道服务,使用Nginx反向代理:

# /etc/nginx/sites-available/tunnel
server {
    listen 80;
    server_name your-domain.com;

    location / {
        proxy_pass http://localhost:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
    }
}

3. 远程访问

通过域名访问:

http://your-domain.com/api/data

六、源码解析

1. ngrok核心机制

ngrok通过以下步骤建立连接:

  1. 客户端连接ngrok服务器
  2. 服务器分配随机域名(如abc123.ngrok.io)
  3. 建立WebSocket隧道
  4. 客户端请求通过隧道转发到内网服务

关键代码:

// ngrok的核心连接逻辑(简化版)
const ws = new WebSocket('wss://ngrok.io');
ws.on('open', () => {
  ws.send(JSON.stringify({
    type: 'connect',
    port: 3000
  }));
});

2. 隧道服务的流控机制

需要处理并发连接和流量控制:

// 限制最大连接数
const maxConnections = 100;
let activeConnections = 0;

wss.on('connection', (ws) => {
  if (activeConnections >= maxConnections) {
    ws.close(429, 'Too many connections');
    return;
  }
  
  activeConnections++;
  
  ws.on('close', () => {
    activeConnections--;
  });
});

七、进阶使用

1. 带认证的隧道服务

增加JWT认证机制:

const jwt = require('jsonwebtoken');

// 认证中间件
function authenticate(req, res, next) {
  const token = req.headers['authorization'];
  
  if (!token) {
    return res.status(401).json({ error: 'Missing token' });
  }
  
  try {
    const decoded = jwt.verify(token, 'secret_key');
    req.user = decoded;
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid token' });
  }
}

2. 增加HTTPS支持

配置SSL证书:

const fs = require('fs');
const https = require('https');

const options = {
  key: fs.readFileSync('/path/to/privkey.pem'),
  cert: fs.readFileSync('/path/to/fullchain.pem')
};

https.createServer(options, (req, res) => {
  // 处理HTTPS请求
}).listen(443);

八、性能与工程实践

1. 性能优化策略

优化策略描述适用场景
持久连接减少握手开销高频访问场景
缓存机制缓存静态资源静态内容访问
负载均衡分散流量压力高并发场景
资源预加载提前加载关键资源首屏加载优化

2. 异常处理机制

// 异常捕获
process.on('uncaughtException', (err) => {
  console.error('Uncaught Exception:', err);
  process.exit(1);
});

// 未处理的Promise拒绝
process.on('unhandledRejection', (reason, p) => {
  console.error('Unhandled Rejection at:', p, 'reason:', reason);
});

3. 安全加固措施

  • 强制HTTPS
  • 防止CSRF攻击
  • 限制请求频率
  • 设置CORS策略

九、常见问题与踩坑

1. 常见错误及解决方法

错误原因解决方案
502 Bad Gateway隧道未正确建立检查ngrok配置
403 Forbidden未通过认证添加认证机制
429 Too Many Requests超过并发限制增加限流策略
Connection Reset网络不稳定增加重连机制

2. 隧道服务配置错误

常见配置错误:

// 错误示例:未正确配置反向代理
app.use((req, res, next) => {
  res.redirect('http://localhost:3000');
});

正确做法:

// 正确示例:使用express代理
const { createProxyMiddleware } = require('http-proxy-middleware');
app.use('/proxy', createProxyMiddleware({
  target: 'http://localhost:3000',
  changeOrigin: true
}));

十、最佳实践

1. 推荐的使用场景

  • 开发测试:快速暴露本地服务
  • 简单的远程调试:开发过程中调试接口
  • 内部系统:需要远程访问的内部系统

2. 不推荐的使用场景

  • 生产环境:存在安全风险和性能瓶颈
  • 高并发场景:需要更专业的负载均衡方案
  • 需要持久连接的场景:建议使用WebSocket或长轮询

3. 安全建议

  • 必须使用HTTPS
  • 设置访问控制列表(ACL)
  • 定期更新证书和密钥
  • 使用防火墙规则限制访问源

十一、总结

通过Node.js实现内网穿透技术,可以有效解决本地服务的公网访问问题。本文深入探讨了三种实现方式,提供了完整的代码示例和实际案例,分析了性能优化和安全加固方案。在实际应用中,需要根据具体场景选择合适的方案,同时注意安全风险和性能瓶颈。虽然该技术在开发和测试阶段非常有用,但在生产环境应谨慎使用,优先考虑更专业的解决方案。

2024-08-11

'# 如何在Linux中安装NVM(Node Version Manager)

一、背景与问题

在Linux开发环境中,Node.js版本管理是一个常见的需求。开发者通常需要在不同项目中使用不同版本的Node.js,例如:

  • 项目A需要使用Node.js 14.x以兼容旧代码
  • 项目B需要使用Node.js 18.x以利用新特性
  • 项目C需要使用Node.js 20.x进行性能测试

传统做法是手动下载不同版本的Node.js二进制文件,但这种方式存在以下问题:

  1. 管理复杂:需要手动维护多个版本的路径
  2. 环境污染:不同版本的依赖可能产生冲突
  3. 版本切换困难:需要手动修改PATH或使用脚本切换

NVM(Node Version Manager)正是为解决这些问题而设计的工具,它通过脚本管理多个Node.js版本,并提供便捷的版本切换能力。

二、基本原理

NVM的核心原理是通过shell脚本实现版本管理,其工作流程如下:

  1. 下载安装脚本(nvm.sh)
  2. 执行脚本安装NVM环境
  3. 创建NVM的安装目录结构
  4. 设置环境变量(NVM_DIR)
  5. 通过软链接管理不同版本的Node.js
  6. 提供nvm use命令切换版本

其关键设计思想是:

  • 使用软链接实现版本切换(/usr/local/nvm/versions/node/v18.12.1)
  • 通过环境变量控制当前使用版本(NVM_VERSION)
  • 将Node.js安装路径与版本号解耦

三、环境准备

确保系统满足以下要求:

# 检查bash版本
bash --version
# 应该 >= 4.2

# 检查curl或wget安装
which curl || which wget
# 如果未安装,使用包管理器安装
sudo apt install curl

四、核心实现

1. 安装NVM

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash

关键代码解释:

  • curl -o-:将远程内容输出到标准输出
  • | bash:将输出结果传递给bash执行
  • 安装脚本会完成以下操作:

    1. 将nvm脚本添加到bashrc
    2. 创建~/.nvm目录结构
    3. 设置环境变量NVM_DIR
    4. 配置PATH包含nvm的bin目录

安装后验证:

command -v nvm
# 应该输出nvm的路径

2. 安装指定版本Node.js

nvm install 18.12.1

关键代码解释:

  • 脚本会:

    1. 下载指定版本的Node.js源码包
    2. 编译生成二进制文件
    3. 将二进制文件存放到~/.nvm/versions/node/18.12.1
    4. 创建软链接~/.nvm/versions/node/18.12.1/bin/node

安装过程中的关键步骤:

# 编译过程示例
tar -xzf node-v18.12.1-linux-x64.tar.gz
cd node-v18.12.1-linux-x64
./configure
make
sudo make install

3. 切换Node.js版本

nvm use 18.12.1

关键代码解释:

  • 脚本会:

    1. 修改NVM_VERSION环境变量
    2. 修改PATH变量,将当前版本的node路径置于最前端
    3. 创建软链接~/.nvm/current指向当前版本

验证当前版本:

node -v
# 应该输出18.12.1

五、完整案例

项目需求场景

假设我们有三个不同需求的项目:

  • 项目A:使用Node.js 14.x
  • 项目B:使用Node.js 16.x
  • 项目C:使用Node.js 18.x

实现步骤:

  1. 安装NVM(如上文所述)
  2. 安装不同版本的Node.js:
nvm install 14.19.1
nvm install 16.14.2
nvm install 18.12.1
  1. 为不同项目设置不同版本:
# 项目A
nvm use 14.19.1
cd projectA
npm install
npm start

# 项目B
nvm use 16.14.2
cd projectB
npm install
npm start

# 项目C
nvm use 18.12.1
cd projectC
npm install
npm start

完整案例中的关键点:

  • 使用nvm ls查看已安装版本
  • 使用nvm ls-alias查看已定义的版本别名
  • 使用nvm alias default 18.12.1设置默认版本

六、源码解析

NVM安装脚本分析

# 安装脚本核心逻辑(简化版)
if [ -s "$NVM_DIR/nvm.sh" ]; then
  export NVM_DIR="$HOME/.nvm"
  [ -f "$NVM_DIR/nvm.sh" ] || curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
fi

关键点:

  • 检查是否存在nvm.sh文件
  • 如果不存在则下载并执行安装
  • 设置NVM_DIR环境变量
  • 将nvm.sh添加到bashrc中

版本管理核心代码

# 版本管理核心逻辑(简化版)
case "$1" in
  install)
    # 下载并编译指定版本
    wget https://nodejs.org/dist/v$2/node-v$2-linux-x64.tar.xz
    tar -xJf node-v$2-linux-x64.tar.xz
    mv node-v$2-linux-x64 node-$2
    ;;
  use)
    # 设置当前版本
    export NVM_VERSION=$2
    export PATH="$NVM_DIR/versions/node/$NVM_VERSION/bin:$PATH"
    ;;
esac

七、进阶使用

1. 自定义安装路径

# 修改安装路径
export NVM_DIR="/opt/nvm"
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash

2. 安装多个版本

nvm install 14.19.1
nvm install 16.14.2
nvm install 18.12.1

3. 管理版本别名

nvm alias default 18.12.1
nvm alias my-project 14.19.1

4. 批量安装版本

nvm install --lts
nvm install 18.12.1
nvm install 16.14.2

八、性能与工程实践

1. 性能优化

  • 使用nvm ls快速查看已安装版本
  • 使用nvm cache list管理缓存
  • 使用nvm version检查当前版本

2. 安全实践

  • 始终使用HTTPS链接
  • 验证安装脚本的哈希值
  • 定期更新NVM版本

安全风险:

  • 未验证的安装脚本可能导致恶意代码注入
  • 使用sudo安装可能带来权限风险
  • 不同版本的Node.js可能存在漏洞

3. 异常处理

# 安装失败处理
if ! nvm install 18.12.1; then
  echo "安装失败,请检查网络连接"
  exit 1
fi

九、常见问题与踩坑

1. 安装失败

错误示例:

curl: (22) The requested URL returned error: 404 Not Found

解决方法:

  • 检查URL是否正确
  • 使用wget替代curl
  • 使用nvm install --list查看可用版本

2. 版本切换失败

错误示例:

nvm use 18.12.1
bash: nvm: command not found

解决方法:

  • 确认已安装NVM
  • 检查bashrc是否包含nvm配置
  • 重新安装NVM

3. 环境变量问题

错误示例:

node -v
bash: node: command not found

解决方法:

  • 检查PATH环境变量
  • 重新设置NVM_DIR
  • 重新加载bash配置文件

    source ~/.bashrc

十、最佳实践

  1. 使用nvm ls管理版本
  2. 使用nvm alias创建别名
  3. 定期清理旧版本
  4. 使用nvm cache管理缓存
  5. 在CI/CD中使用nvm install确保环境一致性
  6. 在生产环境中避免使用NVM,改用Docker容器

十一、总结

NVM作为Node.js版本管理工具,通过脚本实现版本管理,具有以下特点:

  • 灵活性:支持任意版本的Node.js
  • 简便性:只需简单命令即可切换版本
  • 安全性:通过软链接管理版本,避免路径污染
  • 可维护性:提供完整的版本管理功能

在实际开发中,建议在以下场景使用NVM:

  • 开发多项目需要不同Node.js版本
  • 需要快速切换版本进行测试
  • 团队协作需要统一版本

但需要注意避免在生产环境中使用,因为:

  • 可能导致依赖冲突
  • 需要维护多个版本
  • 可能带来潜在的安全风险

通过合理使用NVM,可以显著提升开发效率,确保不同项目在不同Node.js版本下的兼容性。同时,注意遵循最佳实践,避免常见错误,可以有效提升开发质量。

2024-08-11

'# prometheus使用node_exporter监控Linux主机CPU、内存、磁盘、服务运行状况

一、背景与问题

在现代云原生架构中,系统资源监控是保障服务稳定性的核心环节。传统的SNMP监控存在协议复杂、实时性差等问题,而Prometheus的拉取式监控模型结合node_exporter提供的系统级指标采集,成为Linux主机监控的黄金组合。

node_exporter作为Prometheus的官方Exporter,通过Linux内核提供的sysfs、procfs等文件系统接口,结合C语言实现的collectors模块,能够精确采集CPU、内存、磁盘、网络、进程等系统级指标。其核心优势在于:

  1. 原生支持Linux内核指标
  2. 零侵入式采集(无需修改系统代码)
  3. 自适应的指标维度设计
  4. 与Prometheus生态无缝集成

但实际应用中会遇到如下挑战:

  • 资源限制下的采集频率优化
  • 安全防护与访问控制
  • 多节点集群的监控策略设计
  • 历史数据保留与归档策略

二、基本原理

1. 数据采集机制

node_exporter通过以下方式采集指标:

// 简化版collector实现
typedef struct {
    char *path;
    int interval;
    int (*collect)(struct collector *c, prometheus_metric_t *m);
} collector_t;

void init_collector(collector_t *c) {
    c->path = "/proc/stat";
    c->interval = 1;
    c->collect = collect_cpu;
}

int collect_cpu(collector_t *c, prometheus_metric_t *m) {
    FILE *fp = fopen(c->path, "r");
    if (!fp) return -1;
    
    char line[1024];
    while (fgets(line, sizeof(line), fp)) {
        // 解析cpu使用率
        double user, nice, system, idle;
        sscanf(line, "cpu %lf %lf %lf %lf", &user, &nice, &system, &idle);
        m->value = (user + nice + system) / (user + nice + system + idle);
        m->labels = labels_from_cpu_type("user");
        prometheus_register_metric(m);
    }
    fclose(fp);
    return 0;
}

核心原理是通过读取/proc文件系统中的统计信息,结合Linux内核提供的系统调用接口,实现对系统资源的实时监控。每个collector负责特定维度的指标采集,如cpu、memory、disk等。

2. 指标暴露机制

node_exporter将采集的指标通过HTTP接口暴露,格式为:

# HELP node_cpu_seconds_total Total user and system CPU time spent in seconds.
# TYPE node_cpu_seconds_total counter
node_cpu_seconds_total{mode="user"} 12345.67
node_cpu_seconds_total{mode="system"} 8765.43

Prometheus通过配置抓取任务定期拉取这些指标,存储为时间序列数据。

三、环境准备

1. 系统要求

支持Linux内核版本:

  • 3.10+(主流发行版均满足)
  • 需要procfs和sysfs文件系统挂载

2. 安装node_exporter

# 下载源码
wget https://github.com/prometheus/node_exporter/releases/download/v1.3.1/node_exporter-1.3.1.linux-amd64.tar.gz
tar -xzf node_exporter-1.3.1.linux-amd64.tar.gz
cd node_exporter-1.3.1.linux-amd64

# 启动服务
./node_exporter --web.listen-address=":9100"

3. 安装Prometheus

wget https://github.com/prometheus/prometheus/releases/download/v2.38.0/prometheus-2.38.0.linux-amd64.tar.gz
tar -xzf prometheus-2.38.0.linux-amd64.tar.gz
cd prometheus-2.38.0.linux-amd64

# 配置Prometheus
cat <<EOF > prometheus.yml
global:
  scrape_interval: 10s

scrape_configs:
- job_name: 'linux_nodes'
  static_configs:
  - targets: ['localhost:9100']
EOF

四、核心实现

1. 基础监控指标采集

node_exporter默认包含以下核心指标:

# CPU使用率
100() - (node_cpu_seconds_total{mode="idle"} / node_cpu_seconds_total{mode="idle"} + node_cpu_seconds_total{mode="user"} + node_cpu_seconds_total{mode="system"} + node_cpu_seconds_total{mode="nice"} + node_cpu_seconds_total{mode="iowait"} + node_cpu_seconds_total{mode="irq"} + node_cpu_seconds_total{mode="softirq"} + node_cpu_seconds_total{mode="steal"} + node_cpu_seconds_total{mode="guest"} + node_cpu_seconds_total{mode="guestnice"}) / node_cpu_seconds_total{mode="idle"} * 100

# 内存使用率
(1 - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes) / node_memory_MemTotal_bytes) * 100

# 磁盘IO
node_disk_read_bytes_total[5m] + node_disk_write_bytes_total[5m]

2. 自定义指标采集

扩展node_exporter支持自定义指标:

// 自定义collector示例
type CustomCollector struct {
    ch chan float64
}

func (c *CustomCollector) Describe(ch chan<- *prometheus.Desc) {
    ch <- prometheus.NewDesc(
        prometheus.BuildLabelNames("job", "instance", "custom_metric"),
        "Custom metric description",
        []string{"job", "instance", "custom_metric"},
        nil,
    )
}

func (c *CustomCollector) Collect(ch chan<- prometheus.Metric) {
    // 模拟自定义指标
    value := <-c.ch
    ch <- prometheus.MustNewConstMetric(
        prometheus.NewDesc(
            prometheus.BuildLabelNames("job", "instance", "custom_metric"),
            "Custom metric description",
            []string{"test", "localhost"},
            nil,
        ),
        prometheus.GaugeValue,
        value,
        "test", "localhost",
    )
}

3. 指标采集优化

针对高并发场景的优化策略:

# 调整采集间隔
./node_exporter --collectors.enabled="cpu,mem,disk" --scrape-interval=5s

# 启用压缩
./node_exporter --enable-remote-write-receiver --remote-write-url=http://localhost:12345

# 配置内存限制
./node_exporter --mem-samples=10 --mem-threads=4

五、完整案例

1. 生产环境监控方案

部署架构:

Linux服务器
├── node_exporter (采集层)
├── Prometheus (存储层)
└── Grafana (可视化层)

配置示例:

# prometheus.yml
global:
  scrape_interval: 30s

scrape_configs:
- job_name: 'linux_nodes'
  static_configs:
  - targets: ['192.168.1.10:9100', '192.168.1.11:9100']
  metrics_path: /metrics
  scrape_interval: 10s
  relabel_configs:
  - source_labels: [__address__]
    target_label: __meta_node_name

监控面板配置:

{
  "panels": [
    {
      "type": "graph",
      "title": "CPU Usage",
      "grid": true,
      "interval": "5m",
      "lines": [
        {
          "target": "100() - (node_cpu_seconds_total{mode=\"idle\"} / node_cpu_seconds_total{mode=\"idle\"} + node_cpu_seconds_total{mode=\"user\"} + node_cpu_seconds_total{mode=\"system\"} + node_cpu_seconds_total{mode=\"nice\"} + node_cpu_seconds_total{mode=\"iowait\"} + node_cpu_seconds_total{mode=\"irq\"} + node_cpu_seconds_total{mode=\"softirq\"} + node_cpu_seconds_total{mode=\"steal\"} + node_cpu_seconds_total{mode=\"guest\"} + node_cpu_seconds_total{mode=\"guestnice\"}) / node_cpu_seconds_total{mode=\"idle\"} * 100",
          "color": "blue"
        }
      ]
    }
  ]
}

2. 高可用部署方案

多节点集群监控:

# 节点1配置
./node_exporter --web.listen-address=":9100" --scrape-interval=5s --collectors.enabled="cpu,mem,disk"

# 节点2配置
./node_exporter --web.listen-address=":9101" --scrape-interval=5s --collectors.enabled="process,log"

# Prometheus配置
scrape_configs:
- job_name: 'linux_nodes'
  static_configs:
  - targets: ['192.168.1.10:9100', '192.168.1.11:9101']

六、源码解析

1. 核心采集模块

// node_exporter源码核心逻辑
func main() {
    flag.Parse()
    log.SetOutput(os.Stdout)
    
    // 初始化采集器
    registerDefaultCollectors()
    
    // 启动HTTP服务
    http.Handle("/", prometheus.NewGatherer())
    http.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) {
        prometheus.GatherAndWrite(w, nil)
    })
    
    log.Fatal(http.ListenAndServe(":9100", nil))
}

关键点:

  • 使用prometheus.GatherAndWrite处理指标请求
  • 自动注册所有collector
  • 支持动态添加collector

2. 指标注册机制

// 指标注册示例
func registerDefaultCollectors() {
    registerCollector(&cpuCollector{})
    registerCollector(&memCollector{})
    registerCollector(&diskCollector{})
}

func registerCollector(c *Collector) {
    prometheus.MustRegister(c)
}

七、进阶使用

1. 自定义指标扩展

// 自定义指标采集
type CustomCollector struct {
    ch chan float64
}

func (c *CustomCollector) Describe(ch chan<- *prometheus.Desc) {
    ch <- prometheus.NewDesc(
        prometheus.BuildLabelNames("job", "instance", "custom_metric"),
        "Custom metric description",
        []string{"job", "instance", "custom_metric"},
        nil,
    )
}

func (c *CustomCollector) Collect(ch chan<- prometheus.Metric) {
    value := <-c.ch
    ch <- prometheus.MustNewConstMetric(
        prometheus.NewDesc(
            prometheus.BuildLabelNames("job", "instance", "custom_metric"),
            "Custom metric description",
            []string{"test", "localhost"},
            nil,
        ),
        prometheus.GaugeValue,
        value,
        "test", "localhost",
    )
}

2. 高级监控策略

# 告警规则示例
groups:
- name: node
  rules:
  - alert: HighCPUUsage
    expr: (100() - (node_cpu_seconds_total{mode="idle"} / node_cpu_seconds_total{mode="idle"} + node_cpu_seconds_total{mode="user"} + node_cpu_seconds_total{mode="system"} + node_cpu_seconds_total{mode="nice"} + node_cpu_seconds_total{mode="iowait"} + node_cpu_seconds_total{mode="irq"} + node_cpu_seconds_total{mode="softirq"} + node_cpu_seconds_total{mode="steal"} + node_cpu_seconds_total{mode="guest"} + node_cpu_seconds_total{mode="guestnice"}) / node_cpu_seconds_total{mode="idle"} * 100) > 80
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: "High CPU usage on {{ $labels.instance }}"
      description: "CPU usage is above 80% (current value: {{ $value }}%)"

八、性能与工程实践

1. 性能优化策略

优化项方案效果
采集频率调整scrape_interval减少资源消耗
指标过滤使用relabel_configs降低数据量
指标聚合启用remote write提升存储效率
内存限制设置mem-samples防止内存溢出

2. 安全防护措施

  • 启用认证:

    ./node_exporter --web.listen-address=":9100" --web.telemetry-path="/metrics" --web.basic-auth-user=admin --web.basic-auth-password=secret
  • 使用TLS:

    ./node_exporter --web.listen-address=":9100" --web.tls-cert-path=/etc/ssl/cert.pem --web.tls-key-path=/etc/ssl/privkey.pem

3. 异常处理机制

// 异常处理示例
func (c *CustomCollector) Collect(ch chan<- prometheus.Metric) {
    select {
    case value := <-c.ch:
        ch <- prometheus.MustNewConstMetric(...)
    case <-time.After(10 * time.Second):
        log.Error("Timeout collecting custom metric")
    }
}

九、常见问题与踩坑

1. 典型问题分析

问题原因解决方案
指标未采集未挂载procfsmount -t proc proc /proc
数据延迟scrape_interval过大调整为5s或1s
内存溢出超过内存限制降低mem-samples值
权限错误未使用root权限sudo ./node_exporter
指标异常内核版本不兼容升级内核或使用旧版本

2. 常见错误排查

# 检查指标是否暴露
curl http://localhost:9100/metrics

# 检查Prometheus抓取状态
curl http://localhost:9090/api/v1/query?query=node_cpu_seconds_total

# 检查日志
tail -f /var/log/node_exporter.log

十、最佳实践

1. 推荐配置方案

  • 生产环境建议:

    • 每5秒采集一次
    • 启用远程写入
    • 配置访问控制
    • 启用压缩
    • 设置内存限制
  • 开发环境建议:

    • 每10秒采集一次
    • 关闭远程写入
    • 不启用安全机制
    • 关闭压缩

2. 安全配置建议

# Prometheus安全配置
scrape_configs:
- job_name: 'linux_nodes'
  static_configs:
  - targets: ['192.168.1.10:9100']
  basic_auth_user: admin
  basic_auth_password: secret
  bearer_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

十一、总结

通过node_exporter与Prometheus的组合,可以实现对Linux主机的全面监控。其核心价值在于:

  1. 原生支持Linux系统指标
  2. 可扩展的指标采集体系
  3. 与Prometheus生态的无缝集成
  4. 灵活的监控策略配置

但在实际应用中需注意:

  • 不要过度采集,避免系统负载过高
  • 不要暴露敏感端口,做好安全防护
  • 不要忽略异常处理,确保服务稳定性
  • 不要忽视性能优化,特别是在大规模集群中

建议在以下场景使用:

  • 需要细粒度系统监控
  • 需要与现有Prometheus生态集成
  • 需要支持动态扩展的监控系统

不建议在以下场景使用:

  • 需要超低延迟监控(如金融交易系统)
  • 需要自定义协议的监控
  • 需要高可用的监控系统(需配合其他方案)

通过合理配置和优化,node_exporter可以成为企业级监控系统的核心组件,为系统稳定性提供坚实保障。

2024-08-11

'# 爬虫(没)入门:用 node-crawler 爬取 blog

一、背景与问题

在互联网数据挖掘、信息聚合、SEO分析等场景中,爬虫技术始终是核心工具。然而,传统爬虫实现往往存在三大痛点:

  1. 并发控制失效:未合理管理请求队列,导致服务器过载或被封禁
  2. 动态内容处理不足:无法处理JavaScript渲染的网页内容
  3. 数据解析效率低下:缺乏结构化处理机制,容易遗漏关键信息

node-crawler 作为 Node.js 生态中较为成熟的爬虫框架,提供了分布式任务队列、请求管道、数据解析模板等核心功能。本文将深入探讨其工作原理,结合真实开发场景分析其适用边界,并通过完整案例演示其使用方法。

二、基本原理

node-crawler 的核心架构包含以下关键组件:

  1. 任务队列(Queue):管理待爬取的URL列表
  2. 请求处理器(RequestHandler):处理HTTP请求和响应
  3. 解析器(Parser):提取网页内容并构建数据模型
  4. 调度器(Scheduler):控制并发度和请求策略
  5. 存储系统(Storage):持久化爬取数据

其工作流程遵循 URL -> 请求 -> 响应 -> 解析 -> 存储 的闭环。对于动态内容,需通过设置useProxy和useUserAgent等选项模拟浏览器行为。

三、环境准备

# 安装 node-crawler 及依赖
npm install node-crawler cheerio

创建基础项目结构:

blog-crawler/
├── config.js          # 配置文件
├── crawler.js         # 爬虫主逻辑
├── parser.js          # 数据解析模块
└── data/              # 存储爬取数据

四、核心实现

1. 初始化爬虫配置

// config.js
module.exports = {
  maxConcurrentRequests: 5,      // 最大并发请求数
  requestTimeout: 10000,         // 请求超时时间
  userAgent: 'Mozilla/5.0',      // User-Agent
  proxy: 'http://127.0.0.1:8080' // 代理服务器
};

2. 基础爬虫实现

// crawler.js
const crawler = require('node-crawler');
const config = require('./config');

const options = {
  maxConnections: config.maxConcurrentRequests,
  timeout: config.requestTimeout,
  userAgent: config.userAgent,
  proxy: config.proxy
};

const crawlerInstance = new crawler(options, (error, response, done) => {
  if (error) {
    console.error('请求失败:', error);
    return done();
  }

  console.log(`爬取成功: ${response.requestUrl}`);
  done();
});

// 启动爬虫
crawlerInstance.queue(['https://example.com/blog']);

3. 数据解析与存储

// parser.js
const cheerio = require('cheerio');

function parseBlogPage(html) {
  const $ = cheerio.load(html);
  
  const posts = [];
  
  $('.post').each((i, element) => {
    const title = $(element).find('h2.title').text().trim();
    const content = $(element).find('div.content').text().trim();
    const date = $(element).find('span.date').text().trim();
    
    posts.push({
      title,
      content,
      date
    });
  });
  
  return posts;
}

五、完整案例

1. 爬取技术博客内容

// crawler.js
const crawler = require('node-crawler');
const config = require('./config');
const fs = require('fs');
const path = require('path');
const parser = require('./parser');

const options = {
  maxConnections: config.maxConcurrentRequests,
  timeout: config.requestTimeout,
  userAgent: config.userAgent,
  proxy: config.proxy
};

const crawlerInstance = new crawler(options, (error, response, done) => {
  if (error) {
    console.error('请求失败:', error);
    return done();
  }

  const parsedData = parser.parseBlogPage(response.body);
  
  // 存储数据到文件
  const filePath = path.join(__dirname, 'data', 'blog_data.json');
  fs.writeFileSync(filePath, JSON.stringify(parsedData, null, 2));
  
  console.log(`成功爬取 ${parsedData.length} 篇博客`);
  done();
});

// 启动爬虫
crawlerInstance.queue(['https://example.com/blog']);

2. 分页处理

// crawler.js
const crawler = require('node-crawler');
const config = require('./config');
const fs = require('fs');
const path = require('path');
const parser = require('./parser');

const options = {
  maxConnections: config.maxConcurrentRequests,
  timeout: config.requestTimeout,
  userAgent: config.userAgent,
  proxy: config.proxy
};

function getPageUrls(baseURL, page) {
  return `${baseURL}?page=${page}`;
}

const crawlerInstance = new crawler(options, (error, response, done) => {
  if (error) {
    console.error('请求失败:', error);
    return done();
  }

  const parsedData = parser.parseBlogPage(response.body);
  
  // 存储数据到文件
  const filePath = path.join(__dirname, 'data', 'blog_data.json');
  fs.writeFileSync(filePath, JSON.stringify(parsedData, null, 2));
  
  console.log(`成功爬取 ${parsedData.length} 篇博客`);
  
  // 判断是否需要继续爬取
  const lastPage = parseInt(response.headers['x-pagination-last']);
  if (lastPage > 1) {
    crawlerInstance.queue([getPageUrls('https://example.com/blog', lastPage)]);
  }
  
  done();
});

// 启动爬虫
crawlerInstance.queue(['https://example.com/blog']);

六、源码解析

1. 请求处理机制

node-crawler 使用 http 模块创建连接,通过 setTimeout 管理超时。关键代码如下:

const http = require('http');
const https = require('https');

function createRequest(url) {
  const parsedUrl = new URL(url);
  const protocol = parsedUrl.protocol === 'https:' ? https : http;
  
  return protocol.request({
    hostname: parsedUrl.hostname,
    port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
    path: parsedUrl.pathname + (parsedUrl.search || ''),
    method: 'GET',
    headers: {
      'User-Agent': config.userAgent,
      'Accept-Language': 'en-US,en;q=0.9'
    }
  });
}

2. 并发控制

通过 maxConnections 控制并发请求数,内部使用 Promise 和 async/await 管理连接池:

class ConnectionPool {
  constructor(maxConnections) {
    this.maxConnections = maxConnections;
    this.activeConnections = 0;
  }
  
  async getConnection() {
    if (this.activeConnections >= this.maxConnections) {
      await new Promise(resolve => setTimeout(resolve, 100));
    }
    this.activeConnections++;
    return new Promise((resolve, reject) => {
      const conn = createRequest(url);
      conn.on('response', (res) => {
        this.activeConnections--;
        resolve(res);
      });
      conn.on('error', (err) => {
        this.activeConnections--;
        reject(err);
      });
    });
  }
}

七、进阶使用

1. 处理动态内容

对于JavaScript渲染的页面,需要设置 useProxy 为 true 并配置代理:

const options = {
  useProxy: true,
  proxy: 'http://127.0.0.1:8080',
  timeout: 30000
};

2. 处理反爬机制

添加 Referer 头模拟浏览器行为:

headers: {
  'User-Agent': 'Mozilla/5.0',
  'Referer': 'https://example.com/'
}

3. 使用中间件

创建自定义中间件处理请求:

function customMiddleware(req, res, next) {
  req.headers['X-Custom-Header'] = '12345';
  next();
}

八、性能与工程实践

1. 并发控制

设置 maxConnections 在5-20之间,根据服务器负载动态调整:

const maxConnections = Math.min(20, Math.floor(process.env.CORES * 2));

2. 错误重试机制

实现请求失败重试逻辑:

function retryRequest(url, retries = 3) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const conn = createRequest(url);
      conn.on('response', (res) => {
        resolve(res);
      });
      conn.on('error', (err) => {
        if (retries > 0) {
          retryRequest(url, retries - 1).then(resolve).catch(reject);
        } else {
          reject(err);
        }
      });
    }, 1000);
  });
}

3. 数据存储优化

使用流式写入避免内存溢出:

const fs = require('fs');
const writer = fs.createWriteStream('blog_data.json', { encoding: 'utf8' });

writer.write(JSON.stringify(parsedData, null, 2));
writer.end();

九、常见问题与踩坑

1. 未遵守robots.txt

// 错误示例
crawlerInstance.queue(['https://example.com/blog']);
// 正确做法
const robotstxt = await fetch('https://example.com/robots.txt');
const rules = parseRobotstxt(robotstxt);
crawlerInstance.queue(rules.allowedUrls);

2. 请求头不完整

// 错误示例
headers: {
  'User-Agent': 'Mozilla/5.0'
}
// 正确做法
headers: {
  'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
  'Accept-Language': 'en-US,en;q=0.9',
  'Accept-Encoding': 'gzip, deflate, br'
}

3. 动态内容处理失败

// 错误示例
const html = response.body;
const $ = cheerio.load(html);
// 正确做法
const html = await fetch(url, { method: 'GET', headers: { 'User-Agent': 'Mozilla/5.0' } });
const $ = cheerio.load(html);

十、最佳实践

  1. 遵守robots.txt:在爬虫启动前解析并遵守网站的robots.txt规则
  2. 设置合理并发:根据服务器负载动态调整并发连接数
  3. 模拟真实浏览器:添加必要的请求头和User-Agent
  4. 处理动态内容:使用代理或结合Puppeteer处理JavaScript渲染
  5. 数据验证:在保存前进行数据完整性校验
  6. 日志记录:记录爬取进度和错误信息便于调试
  7. 异常处理:实现请求重试和错误恢复机制

十一、总结

node-crawler 作为Node.js生态中的爬虫框架,提供了较为完整的解决方案。其核心优势在于:

  • 结构化设计:明确的请求处理、解析和存储流程
  • 可扩展性:支持中间件、代理、重试等高级功能
  • 灵活性:可配合Cheerio、Puppeteer等工具处理复杂场景

但需注意其适用边界:

应该使用:

  • 需要批量获取静态网页数据时
  • 需要控制并发请求的场景
  • 需要处理简单动态内容时(通过代理)

不应该使用:

  • 需要处理复杂JavaScript渲染时(应使用Puppeteer)
  • 需要处理大量实时数据时(应使用流处理框架)
  • 需要处理分布式爬虫时(应使用Scrapy-Redis等)

在实际开发中,建议结合具体业务需求选择合适的工具,合理使用中间件和扩展功能,同时严格遵守网络爬虫的道德和法律规范。

2024-08-11

'# 最简单的安装历史版本的node.js,无需nvm无需设置环境变量

一、背景与问题

在现代Web开发中,Node.js版本迭代频繁,不同项目对Node.js版本有严格要求。传统做法是使用nvm(Node Version Manager)管理多版本,但存在以下问题:

  1. 需要安装额外工具
  2. 需要配置环境变量
  3. 对Windows用户不够友好
  4. 管理多个版本时容易出错

本文提出一种更简洁的替代方案:通过直接下载历史版本的二进制文件,结合系统路径管理,实现无需nvm和环境变量的Node.js安装。该方案特别适合以下场景:

  • 临时测试某个版本的特性
  • 在隔离环境中运行特定版本
  • 快速搭建开发环境
  • 避免nvm带来的环境变量污染

二、基本原理

Node.js官方提供历史版本的二进制文件下载(https://nodejs.org/dist/),每个版本都包含完整的运行环境。通过直接下载这些文件并配置路径,可以实现版本管理。其核心原理如下:

  1. 版本定位:通过Node.js官网的版本发布记录,确定目标版本的下载链接
  2. 文件下载:使用curl/wget等工具下载对应版本的二进制文件
  3. 路径配置:通过系统路径配置,使命令行能直接调用node命令
  4. 版本隔离:通过不同的安装目录管理不同版本

三、环境准备

1. 系统要求

本文方案支持所有主流操作系统:

操作系统说明
Linux需要bash shell
macOS需要bash或zsh
Windows需要PowerShell或CMD

2. 工具准备

  • curl/wget(Linux/macOS)
  • PowerShell(Windows)
  • 压缩工具(如7z或tar)

四、核心实现

1. 获取历史版本列表

Node.js官方提供版本发布记录,可以通过API获取:

# 获取所有版本
curl https://nodejs.org/dist/index.json

# 获取特定版本
curl https://nodejs.org/dist/v18.12.1/index.json

响应结果包含完整的版本信息:

{
  "name": "node",
  "version": "18.12.1",
  "files": [
    {
      "filename": "node-v18.12.1-linux-x64.tar.xz",
      "size": 12345678,
      "md5": "abcdef1234567890"
    },
    ...
  ]
}

2. 下载指定版本

使用以下命令下载指定版本的二进制文件:

# Linux/macOS
curl -O https://nodejs.org/dist/v18.12.1/node-v18.12.1-linux-x64.tar.xz

# Windows
 Invoke-WebRequest -Uri "https://nodejs.org/dist/v18.12.1/node-v18.12.1-win-x64.zip" -OutFile node-v18.12.1-win-x64.zip

3. 解压文件

# Linux/macOS
tar -xf node-v18.12.1-linux-x64.tar.xz

# Windows
7z x node-v18.12.1-win-x64.zip

4. 配置路径

# Linux/macOS
export PATH=$PATH:$HOME/node-v18.12.1-linux-x64/bin

# Windows
set PATH=%PATH%;C:\node-v18.12.1-win-x64

五、完整案例

1. 自动化安装脚本(Linux版)

#!/bin/bash

# 定义变量
VERSION="18.12.1"
OS="linux-x64"
INSTALL_DIR="$HOME/node-$VERSION-$OS"

# 下载文件
curl -L https://nodejs.org/dist/v$VERSION/node-v$VERSION-$OS.tar.xz -o node.tar.xz

# 解压文件
tar -xf node.tar.xz

# 创建符号链接
ln -sf $INSTALL_DIR/bin/node /usr/local/bin/node
ln -sf $INSTALL_DIR/bin/npm /usr/local/bin/npm

# 验证安装
node -v
npm -v

2. 验证安装

# 查看版本
node -v
npm -v

# 运行测试
node -e "console.log('Node.js installed successfully')"

3. 版本切换

# 切换版本(通过路径切换)
export PATH=$PATH:$HOME/node-v16.14.2-linux-x64/bin
node -v

六、源码解析

1. 安装脚本关键代码

# 下载文件
curl -L https://nodejs.org/dist/v$VERSION/node-v$VERSION-$OS.tar.xz -o node.tar.xz

# 解压文件
tar -xf node.tar.xz
  • -L 参数确保跟随重定向链接
  • tar 命令解压xz格式文件
  • 文件名编码使用v$VERSION-$OS格式,确保版本一致性

2. 路径配置原理

export PATH=$PATH:$HOME/node-v18.12.1-linux-x64/bin
  • 环境变量PATH包含所有可执行文件路径
  • 当前目录添加新的node可执行文件路径
  • 系统在查找命令时会按顺序搜索路径中的目录

七、进阶使用

1. 多版本管理

# 创建版本目录结构
mkdir -p ~/node-versions
cd ~/node-versions

# 安装不同版本
curl -L https://nodejs.org/dist/v16.14.2/node-v16.14.2-linux-x64.tar.xz -o node-16.14.2.tar.xz
tar -xf node-16.14.2.tar.xz

# 创建符号链接
ln -sf node-16.14.2-linux-x64/bin/node ~/node-versions/current/bin

2. 版本切换脚本

#!/bin/bash

# 切换版本
VERSION=$1
cd ~/node-versions
ln -sf $VERSION-linux-x64/bin/node ~/node-versions/current/bin

八、性能与工程实践

1. 性能优化

  • 缓存机制:建议将下载的二进制文件存储在专用目录
  • 并发控制:避免同时下载多个版本
  • 版本校验:使用SHA-256校验文件完整性
# 文件校验
sha256sum node-v18.12.1-linux-x64.tar.xz

2. 安全风险

  • 文件来源验证:始终从nodejs.org官方下载
  • 签名验证:使用GPG验证文件签名
  • 权限控制:限制对node可执行文件的写权限
# 签名验证
gpg --verify node-v18.12.1-linux-x64.tar.xz.sig

九、常见问题与踩坑

1. 常见错误

错误类型原因解决方案
1. 文件校验失败下载文件损坏重新下载并验证SHA-256
2. 路径冲突环境变量冲突使用绝对路径或重新配置
3. 权限错误无写权限使用sudo或修改文件权限
4. 版本不兼容依赖库不匹配查看官方兼容性文档

2. 典型问题分析

# 错误示例:未设置环境变量
node: command not found
# 正确做法:设置环境变量
export PATH=$PATH:$HOME/node-v18.12.1-linux-x64/bin

十、最佳实践

1. 推荐方案

  • 开发环境:使用版本脚本管理多个版本
  • 生产环境:使用容器化部署(Docker)
  • 临时测试:直接下载并配置当前目录

2. 安全建议

  • 文件签名:始终验证GPG签名
  • 版本隔离:避免多个版本共存带来的冲突
  • 权限控制:限制对node可执行文件的写权限

3. 工程规范

  • 版本控制:将安装脚本纳入版本控制
  • 文档记录:记录每个版本的安装路径
  • 清理策略:定期清理不再使用的版本

十一、总结

本文提出的Node.js历史版本安装方案,通过直接下载二进制文件并配置路径,实现了无需nvm和环境变量的快速安装。该方案具有以下特点:

  • 简单性:无需安装额外工具
  • 灵活性:支持多种操作系统
  • 可控性:可精确控制版本和路径
  • 安全性:通过签名验证确保文件完整性

但需要注意以下限制:

  • 版本管理不便:需手动切换版本
  • 环境隔离不足:无法完全隔离不同版本
  • 依赖管理缺失:需自行管理npm依赖

在开发环境中,该方案适合快速测试和临时使用;但在生产环境中,建议结合容器化方案(如Docker)实现更完善的版本管理。对于需要长期维护的项目,仍推荐使用nvm或asdf等版本管理工具。

2024-08-11

'# 修复 pprof ---node_exporter访问漏洞(go-pprof-leak)

一、背景与问题

在Go语言开发中,pprof是官方提供的性能分析工具,可获取堆内存、Goroutine、CPU等指标。然而在生产环境中,如果未正确配置node_exporter的pprof接口,可能导致严重的安全漏洞。例如:

http://localhost:9100/debug/pprof/

该接口默认暴露在HTTP服务中,任何访问该路径的用户都可以获取系统底层运行信息。这可能导致以下安全问题:

  1. 敏感数据泄露(如堆内存中包含的密钥)
  2. 系统资源泄露(如Goroutine堆栈信息)
  3. 攻击者可利用pprof接口进行拒绝服务攻击(DDoS)

二、基本原理

node_exporter是Prometheus生态中用于收集主机指标的组件,其pprof接口是Go语言内置的性能分析工具。该接口的暴露机制如下:

  1. HTTP服务绑定:node_exporter默认监听在9100端口,提供HTTP服务
  2. 未授权访问:任何访问/debug/pprof/路径的HTTP请求都会被处理
  3. 数据暴露:返回的profile数据包含系统底层运行信息

漏洞的根本原因在于未对pprof接口进行访问控制,导致潜在攻击面。

三、环境准备

  1. 安装Go环境(建议1.18+)
  2. 安装node_exporter:

    go get github.com/prometheus/node_exporter
  3. 安装Prometheus(用于验证指标)

    go get github.com/prometheus/prometheus

四、核心实现

1. 基础pprof接口暴露

默认情况下,node_exporter会自动注册pprof接口:

package main

import (
    "log"
    "net/http"
    "github.com/prometheus/node_exporter"
)

func main() {
    reg := node_exporter.DefaultRegisterer
    reg.Register(reg)
    
    http.Handle("/", reg)
    http.Handle("/metrics", reg)
    http.Handle("/debug/pprof/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        http.Redirect(w, r, "/debug/pprof/", http.StatusMovedPermanently)
    }))
    
    log.Fatal(http.ListenAndServe(":9100", nil))
}

2. 安全访问控制

为了修复漏洞,需要实现访问控制。可采用以下三种方案:

方案一:IP白名单限制

package main

import (
    "fmt"
    "log"
    "net/http"
    "github.com/prometheus/node_exporter"
)

func allowAccess(r *http.Request) bool {
    // 允许本地访问
    if r.RemoteAddr == "127.0.0.1:443" {
        return true
    }
    // 允许特定IP访问
    if r.RemoteAddr == "192.168.1.100:55432" {
        return true
    }
    return false
}

func main() {
    reg := node_exporter.DefaultRegisterer
    reg.Register(reg)
    
    http.HandleFunc("/debug/pprof/", func(w http.ResponseWriter, r *http.Request) {
        if !allowAccess(r) {
            http.Error(w, "Forbidden", http.StatusForbidden)
            return
        }
        http.Redirect(w, r, "/debug/pprof/", http.StatusMovedPermanently)
    })
    
    http.Handle("/", reg)
    http.Handle("/metrics", reg)
    
    log.Fatal(http.ListenAndServe(":9100", nil))
}

方案二:基于Token的访问控制

package main

import (
    "fmt"
    "log"
    "net/http"
    "github.com/prometheus/node_exporter"
    "github.com/dgryski/valid"
)

func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("X-Auth-Token")
        if token != "secret_token" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
}

func main() {
    reg := node_exporter.DefaultRegisterer
    reg.Register(reg)
    
    http.Handle("/debug/pprof/", authMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        http.Redirect(w, r, "/debug/pprof/", http.StatusMovedPermanently)
    })))
    
    http.Handle("/", reg)
    http.Handle("/metrics", reg)
    
    log.Fatal(http.ListenAndServe(":9100", nil))
}

方案三:中间件代理方案

package main

import (
    "fmt"
    "log"
    "net/http"
    "github.com/prometheus/node_exporter"
    "github.com/gorilla/mux"
)

func main() {
    reg := node_exporter.DefaultRegisterer
    reg.Register(reg)
    
    r := mux.NewRouter()
    r.HandleFunc("/debug/pprof/{action}", func(w http.ResponseWriter, r *http.Request) {
        action := r.URL.Query().Get("action")
        if action == "heap" {
            http.Redirect(w, r, "/debug/pprof/heap", http.StatusMovedPermanently)
        } else if action == "goroutine" {
            http.Redirect(w, r, "/debug/pprof/goroutine", http.StatusMovedPermanently)
        } else {
            http.Error(w, "Invalid action", http.StatusBadRequest)
        }
    })
    
    http.Handle("/", reg)
    http.Handle("/metrics", reg)
    
    log.Fatal(http.ListenAndServe(":9100", r))
}

五、完整案例

案例:生产环境安全配置

  1. 创建配置文件node_exporter_config.yaml:
# node_exporter配置文件
log.level = "info"
scrape_interval = "10s"
scrape_timeout = "10s"
  1. 修改main.go添加访问控制:
package main

import (
    "fmt"
    "log"
    "net/http"
    "github.com/prometheus/node_exporter"
    "github.com/gorilla/mux"
)

func main() {
    reg := node_exporter.DefaultRegisterer
    reg.Register(reg)
    
    r := mux.NewRouter()
    r.HandleFunc("/debug/pprof/{action}", func(w http.ResponseWriter, r *http.Request) {
        action := r.URL.Query().Get("action")
        if action == "heap" {
            http.Redirect(w, r, "/debug/pprof/heap", http.StatusMovedPermanently)
        } else if action == "goroutine" {
            http.Redirect(w, r, "/debug/pprof/goroutine", http.StatusMovedPermanently)
        } else {
            http.Error(w, "Invalid action", http.StatusBadRequest)
        }
    })
    
    http.Handle("/", reg)
    http.Handle("/metrics", reg)
    
    log.Fatal(http.ListenAndServe(":9100", r))
}
  1. 部署到生产环境:
# 后台运行
go run main.go --config node_exporter_config.yaml
  1. 配置Prometheus监控:
# prometheus.yml
scrape_configs:
  - job_name: 'node'
    static_configs:
      - targets: ['localhost:9100']

六、源码解析

以node_exporter的pprof接口实现为例:

// node_exporter.go
func init() {
    http.Handle("/debug/pprof/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        http.Redirect(w, r, "/debug/pprof/", http.StatusMovedPermanently)
    }))
}

关键代码分析:

  1. http.Handle注册路由:将/debug/pprof/路径绑定到处理函数
  2. http.Redirect:将请求重定向到/debug/pprof/路径
  3. 默认处理函数未做任何访问控制

通过添加中间件,可以实现访问控制:

func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 认证逻辑
        if !auth(r) {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
}

七、进阶使用

1. 配合Prometheus进行安全监控

# prometheus.yml
scrape_configs:
  - job_name: 'node'
    static_configs:
      - targets: ['localhost:9100']
    metrics_path: '/metrics'
    scheme: 'http'

2. 实现访问日志记录

func logAccess(r *http.Request) {
    log.Printf("Accessed: %s %s", r.Method, r.URL.Path)
}

3. 实现速率限制

func rateLimitMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 限制每秒请求数
        if requestCount > 10 {
            http.Error(w, "Too many requests", http.StatusTooManyRequests)
            return
        }
        requestCount++
        next.ServeHTTP(w, r)
    })
}

八、性能与工程实践

1. 性能优化

  1. 使用缓存机制:对频繁访问的接口进行缓存
  2. 并发控制:限制同时处理的请求数量
  3. 异步处理:将敏感操作放入goroutine中处理
func asyncHandler(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        go func() {
            next.ServeHTTP(w, r)
        }()
    })
}

2. 异常处理

func errorHandlingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if r := recover(); r != nil {
                http.Error(w, "Internal server error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

3. 安全增强

  1. 使用HTTPS:确保传输过程加密
  2. 实现访问日志:记录所有访问行为
  3. 定期审计:检查日志中的异常访问

九、常见问题与踩坑

1. 常见错误

错误示例:

http.HandleFunc("/debug/pprof/", func(w http.ResponseWriter, r *http.Request) {
    http.Redirect(w, r, "/debug/pprof/", http.StatusMovedPermanently)
})

问题分析:

  • 未做访问控制,导致安全漏洞
  • 未处理错误请求

改进方案:

func authHandler(w http.ResponseWriter, r *http.Request) {
    if !auth(r) {
        http.Error(w, "Unauthorized", http.StatusUnauthorized)
        return
    }
    http.Redirect(w, r, "/debug/pprof/", http.StatusMovedPermanently)
}

2. 安全风险

风险场景:

  • 暴露堆内存信息可能导致敏感数据泄露
  • 暴露Goroutine信息可能导致系统资源泄露

解决方案:

  • 严格限制访问IP
  • 使用认证机制
  • 配置防火墙规则

3. 性能问题

问题场景:

  • 高并发访问导致系统负载过高
  • 未处理错误请求导致资源浪费

优化方案:

  • 使用中间件进行速率限制
  • 使用缓存机制
  • 异步处理敏感操作

十、最佳实践

  1. 生产环境配置建议:

    • 使用IP白名单或Token认证
    • 配置HTTPS加密传输
    • 记录访问日志
    • 设置访问频率限制
  2. 开发环境配置建议:

    • 可开放pprof接口用于调试
    • 但需配置访问限制
    • 使用本地网络限制访问
  3. 安全建议:

    • 定期检查访问日志
    • 配置WAF规则
    • 使用安全扫描工具检测漏洞
  4. 性能优化建议:

    • 对高频请求做缓存
    • 使用并发控制
    • 异步处理敏感操作

十一、总结

node_exporter的pprof接口暴露是Go语言开发中常见的安全风险。通过合理配置访问控制、使用中间件、实施安全策略,可以有效修复这一漏洞。在生产环境中,应严格限制pprof接口的访问权限,避免敏感信息泄露。同时,需要考虑性能优化和安全增强,确保系统在安全和性能之间取得平衡。通过本文的深入分析和实践案例,希望开发者能够更好地理解和应用这些安全措施,提升系统的整体安全性。