Node.js 创建FaaS服务

71 阅读5分钟

一、简单初始化

1.首先,创建一个新的目录并初始化一个 Node.js 项目:

mkdir faas-service
cd faas-service
npm init -y
npm install express body-parser vm2 multer

2.创建server.js

const express = require('express');
const bodyParser = require('body-parser');
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const { NodeVM } = require('vm2');

const app = express();
const upload = multer({ dest: 'uploads/' });

app.use(bodyParser.json());

// 路由:注册新函数
app.post('/functions', upload.single('function'), (req, res) => {
    const { functionName } = req.body;
    const file = req.file;

    if (!functionName || !file) {
        return res.status(400).json({ error: '函数名称和代码文件是必需的。' });
    }

    const destPath = path.join(__dirname, 'functions', `${functionName}.js`);

    fs.rename(file.path, destPath, (err) => {
        if (err) {
            return res.status(500).json({ error: '无法保存函数文件。' });
        }
        res.status(201).json({ message: `函数 ${functionName} 已注册。` });
    });
});

// 路由:调用函数
app.post('/invoke/:functionName', async (req, res) => {
    const { functionName } = req.params;
    const funcPath = path.join(__dirname, 'functions', `${functionName}.js`);

    if (!fs.existsSync(funcPath)) {
        return res.status(404).json({ error: '函数未找到。' });
    }

    const code = fs.readFileSync(funcPath, 'utf-8');

    const vm = new NodeVM({
        console: 'inherit',
        sandbox: {},
        timeout: 1000,
        require: {
            external: true,
            builtin: ['*'],
        },
    });

    try {
        const func = vm.run(code);
        const result = await func(req.body);
        res.json({ result });
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});

// 启动服务器
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    console.log(`FaaS 服务已启动,监听端口 ${PORT}`);
});

3.调整package.json

{
  "name": "faas-service",
  "version": "1.0.0",
  "description": "简单 FaaS 服务",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "keywords": [],
  "author": "wei",
  "license": "MIT",
  "dependencies": {
    "body-parser": "^1.20.3",
    "express": "^4.21.0",
    "multer": "^1.4.5-lts.1",
    "vm2": "^3.9.19"
  }
}
module.exports = async (event) => {
  const name = event.name || 'World';
  return { message: `Hello, ${name}!` };
};

二、调用示例函数

使用 Postman 或其他工具发送 POST 请求到 /invoke/hello:

{
  "name": "FaaS"
}

响应:

{
  "result": {
    "message": "Hello, FaaS!"
  }
}

三、注册新函数

同样使用 Postman 上传新的函数:

module.exports = async (event) => {
  const { a, b } = event;
  return { result: a + b };
};

响应:

{
  "message": "函数 add 已注册。"
}

然后调用新注册的函数:

{
  "a": 5,
  "b": 3
}
{
  "result": {
    "result": 8
  }
}

增加get请求支持

// 路由:调用函数(支持 GET 和 POST)
app.all('/invoke/:functionName', async (req, res) => {
    const { functionName } = req.params;
    const funcPath = path.join(__dirname, 'functions', `${functionName}.js`);

    if (!fs.existsSync(funcPath)) {
        return res.status(404).json({ error: '函数未找到。' });
    }

    const code = fs.readFileSync(funcPath, 'utf-8');

    // 根据请求方法获取输入数据
    let input;
    if (req.method === 'GET') {
        input = req.query;
    } else if (req.method === 'POST') {
        input = req.body;
    } else {
        return res.status(405).json({ error: '方法不允许。' });
    }

    try {
        const script = new VMScript(code, funcPath);
        const func = vm.run(script, funcPath);
        const result = await func(input);
        res.json({ result });
    } catch (error) {
        res.status(500).json({ error: error.message, stack: error.stack });
    }
});

四、增加日志模块

const rfs = require('rotating-file-stream'); // 引入 rotating-file-stream 库
const morgan = require('morgan'); // 引入 morgan 库
// 创建 Rotating File Stream 用于 HTTP 访问日志
const accessLogStream = rfs.createStream('access.log', {
    interval: '1d',      // 每天轮换一次
    size: '10M',         // 当日志文件达到10MB时进行轮换
    path: logDirectory,
    compress: 'gzip',    // 压缩旧的日志文件
    maxFiles: 10         // 保留最近10个轮换的日志文件
});

// 创建 Rotating File Stream 用于模块加载日志
const moduleLoadLogStream = rfs.createStream('module-load.log', {
    interval: '1d',      // 每天轮换一次
    size: '10M',         // 当日志文件达到10MB时进行轮换
    path: logDirectory,
    compress: 'gzip',    // 压缩旧的日志文件
    maxFiles: 10         // 保留最近10个轮换的日志文件
});

// 错误监听
accessLogStream.on('error', (err) => {
    console.error('Rotating File Stream (accessLogStream) 错误:', err);
});

moduleLoadLogStream.on('error', (err) => {
    console.error('Rotating File Stream (moduleLoadLogStream) 错误:', err);
});

// 使用 Morgan 进行 HTTP 请求日志记录
app.use(morgan('combined', { stream: accessLogStream }));

// 日志记录函数
function logModuleLoad(moduleName, status, type) {
    const message = `模块加载尝试: [${type}] "${moduleName}" - ${status}`;
    const timestamp = new Date().toISOString();
    moduleLoadLogStream.write(`${timestamp} ${message}\n`);
}

五、添加配置文件禁止模块的加载和模块白名单信息

# 配置文件

# 全局配置
allowedExternalModules:
  - lodash
  - axios

# 模块配置
moduleWhitelist:
  - lodash
  - axios
  - moment
  - path
  - buffer
  
# 内置模块配置
builtinWhitelist:
  - path
  - buffer

引入js-yaml解析配置文件

六、调整目录结构,提取方法

const fs = require("fs");
const path = require("path");
const logger = require("../util/logger"); // 引入 logger.js
const logModuleLoad = logger.logModuleLoad;

const { VMScript } = require("vm2"); // 假设你使用 vm2 来运行脚本
const { createVM, watchConfigFile } = require("../util/vmUtil");
// 初始化 NodeVM
let vm = createVM();
// 动态更新配置并重新初始化 Mock
watchConfigFile(vm);

// 路由:注册新函数
async function addFunction(req, res) {
  const { functionName, functionCode } = req.body;

  if (!functionName || !functionCode) {
    return res.status(400).json({ error: "函数名称和代码是必需的。" });
  }
  const sanitizedFunctionName = path
    .basename(functionName)
    .replace(/[^a-zA-Z0-9_]/g, "");
  const destPath = path.join(
    __dirname,
    "../functions",
    `${sanitizedFunctionName}.js`
  );
  const destDir = path.dirname(destPath);

  // 检查目标目录是否存在
  if (!fs.existsSync(destDir)) {
    try {
      fs.mkdirSync(destDir, { recursive: true });
      logModuleLoad("目录创建", `目录 "${destDir}" 已创建。`, "函数");
    } catch (mkdirErr) {
      logModuleLoad(
        "目录创建错误",
        `创建目录 "${destDir}" 时出错: ${mkdirErr.message}`,
        "函数"
      );
      return res.status(500).json({ error: "无法创建目标目录。" });
    }
  }
  // 检查目标文件是否已存在
  if (fs.existsSync(destPath)) {
    // 删除临时文件
    return res.status(409).json({ error: "函数已存在。" });
  }
  // 将字符串内容写入文件
  fs.writeFile(destPath, functionCode, "utf-8", (err) => {
    if (err) {
      logModuleLoad(
        "文件写入错误",
        `写入文件 "${destPath}" 时出错: ${err.message}`,
        "函数"
      );
      return res.status(500).json({ error: "无法保存函数文件。" });
    }
    logModuleLoad("函数注册", `函数 "${functionName}" 注册成功。`, "函数");
    res.status(201).json({ message: `函数 ${functionName} 注册成功。` });
  });
}
// 路由:注册新函数,上传文件
async function addFunctionFile(req, res) {
  const { functionName } = req.body;
  const file = req.file;

  if (!functionName || !file) {
    return res.status(400).json({ error: "函数名称和代码文件是必需的。" });
  }
  const sanitizedFunctionName = path
    .basename(functionName)
    .replace(/[^a-zA-Z0-9_]/g, "");
  const destPath = path.join(
    __dirname,
    "../functions",
    `${sanitizedFunctionName}.js`
  );
  const destDir = path.dirname(destPath);

  // 检查目标目录是否存在
  if (!fs.existsSync(destDir)) {
    try {
      fs.mkdirSync(destDir, { recursive: true });
      logModuleLoad("目录创建", `目录 "${destDir}" 已创建。`, "函数");
    } catch (mkdirErr) {
      logModuleLoad(
        "目录创建错误",
        `创建目录 "${destDir}" 时出错: ${mkdirErr.message}`,
        "函数"
      );
      return res.status(500).json({ error: "无法创建目标目录。" });
    }
  }
  // 检查目标文件是否已存在
  if (fs.existsSync(destPath)) {
    // 删除临时文件
    deleteFile(file);
    return res.status(409).json({ error: "函数已存在。" });
  }
  // 尝试重命名文件
  fs.rename(file.path, destPath, (err) => {
    if (err) {
      logModuleLoad(
        "文件重命名错误",
        `重命名文件 "${file.path}" 到 "${destPath}" 时出错: ${err.message}`,
        "函数"
      );
      // 删除临时文件
      deleteFile(file);
      return res.status(500).json({ error: "无法保存函数文件。" });
    }
    logModuleLoad("函数注册", `函数 "${functionName}" 注册成功。`, "函数");
    res.status(201).json({ message: `函数 ${functionName} 注册成功。` });

    // 删除临时文件
    deleteFile(file);
  });
}

async function updateFunction(req, res) {
  const { functionName } = req.params;
  const file = req.file;

  if (!functionName || !file) {
    return res.status(400).json({ error: "函数名称和代码文件是必需的。" });
  }

  const destPath = path.join(__dirname, "../functions", `${functionName}.js`);

  // 检查目标文件是否存在
  if (fs.existsSync(destPath)) {
    logModuleLoad("函数不存在", `函数 "${functionName}" 不存在。`, "函数");
    return res.status(409).json({ error: "函数不存在。" });
  }

  // 尝试重命名文件
  fs.rename(file.path, destPath, (err) => {
    if (err) {
      logModuleLoad(
        "文件重命名错误",
        `重命名文件 "${file.path}" 到 "${destPath}" 时出错: ${err.message}`,
        "函数"
      );
      // 删除临时文件
      deleteFile(file);
      return res.status(500).json({ error: "无法保存函数文件。" });
    }
    logModuleLoad("函数更新", `函数 "${functionName}" 更新成功。`, "函数");
    res.status(200).json({ message: `函数 ${functionName} 更新成功。` });
    // 删除临时文件
    deleteFile(file);
  });
}

async function deleteFunction(req, res) {
  const { functionName } = req.params;
  const funcPath = path.join(
    __dirname,
    "..",
    "functions",
    `${functionName}.js`
  );

  if (!fs.existsSync(funcPath)) {
    logModuleLoad("函数未找到", `函数 "${functionName}" 未找到。`, "函数");
    return res.status(404).json({ error: "函数未找到。" });
  }
  fs.unlink(funcPath, (err) => {
    if (err) {
      logModuleLoad(
        "函数删除错误",
        `删除函数 "${functionName}" 时出错: ${err.message}`,
        "函数"
      );
      return res.status(500).json({ error: "无法删除函数。" });
    }
    logModuleLoad("函数删除", `函数 "${functionName}" 删除成功。`, "函数");
    res.status(200).json({ message: `函数 ${functionName} 删除成功。` });
  });
}

// 查询所有
async function listFunctions(req, res) {
  const functionsDir = path.join(__dirname, "../functions");
  const functions = [];
  fs.readdirSync(functionsDir).forEach((file) => {
    if (path.extname(file) === ".js") {
      functions.push(path.basename(file, ".js"));
    }
  });
  res.json({ functions });
}

function deleteFile(file) {
  // 删除临时文件
  fs.unlink(file.path, (unlinkErr) => {
    if (unlinkErr) {
      logModuleLoad(
        "临时文件删除错误",
        `删除临时文件 "${file.path}" 时出错: ${unlinkErr.message}`,
        "函数"
      );
    }
  });
}

// 路由:调用函数
async function invoke(req, res) {
  const { functionName } = req.params;
  const funcPath = path.join(
    __dirname,
    "..",
    "functions",
    `${functionName}.js`
  );

  if (!fs.existsSync(funcPath)) {
    logModuleLoad("函数未找到", `函数 "${functionName}" 未找到。`, "函数");
    return res.status(404).json({ error: "函数未找到。" });
  }

  const code = fs.readFileSync(funcPath, "utf-8");

  // 根据请求方法获取输入数据
  let input;
  if (req.method === "GET") {
    input = req.query;
  } else if (req.method === "POST") {
    input = req.body;
  } else {
    logModuleLoad(
      "方法不允许",
      `函数 "${functionName}" 的请求方法 "${req.method}" 不允许。`,
      "函数"
    );
    return res.status(405).json({ error: "方法不允许。" });
  }

  try {
    logModuleLoad("执行函数", `执行函数 "${functionName}"`, "函数");
    const script = new VMScript(code, funcPath);
    const func = vm.run(script, funcPath);
    const result = await func(input);
    res.json({ result });
  } catch (error) {
    logModuleLoad(
      "执行错误",
      `执行函数 "${functionName}" 时出错: ${error.message}\n${error.stack}`,
      "函数"
    );
    res.status(500).json({ error: error.message, stack: error.stack });
  }
}

module.exports = {
  addFunction,
  addFunctionFile,
  deleteFunction,
  updateFunction,
  listFunctions,
  invoke,
};
const express = require("express");
const multer = require("multer");
const fs = require("fs");
const path = require("path");
const {
  addFunction,
  addFunctionFile,
  deleteFunction,
  updateFunction,
  listFunctions,
  invoke,
} = require("../controller/functions.js");
const router = express.Router();

// 创建上传目录(如果不存在)
const dir = path.resolve(__dirname, "..", "uploads");
if (!fs.existsSync(dir)) {
  fs.mkdirSync(dir);
}

const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, "uploads/"); // 文件保存路径
  },
  filename: function (req, file, cb) {
    cb(null, Date.now() + path.extname(file.originalname)); // 重命名文件以避免冲突
  },
});

const maxSizeInBytes = 3 * 1024 * 1024; // 3MB
const uploadMiddleware = multer({
  storage: storage,
  limits: { fileSize: maxSizeInBytes },
});

router.use((err, req, res, next) => {
  if (err instanceof multer.MulterError) {
    if (err.code === "LIMIT_FILE_SIZE") {
      return res.status(400).send("File size exceeds the limit of 3MB.");
    }
  }
  next(err);
});

router.get("/", (req, res) => {
  res.send("hello world");
});

// 路由:添加函数(支持 GET 和 POST)
router.post("/addFunction", uploadMiddleware.single("function"), addFunction);
router.post(
  "/addFunctionFlie",
  uploadMiddleware.single("function"),
  addFunctionFile
);
// 路由:删除函数
router.post("/deleteFunction", deleteFunction);
// 路由:更新函数
router.post(
  "/updateFunction",
  uploadMiddleware.single("function"),
  updateFunction
);
// 路由:查询所有函数
router.post("/listFunctions", listFunctions);
// 路由:调用函数
router.all("/api/:functionName", invoke);

module.exports = router;
const fs = require('fs');
const path = require('path');
const rfs = require('rotating-file-stream');
const moment = require('moment');

const logDirectory = path.join(__dirname,"..",'logs'); // 日志目录

// 确保日志目录存在
if (!fs.existsSync(logDirectory)) {
    fs.mkdirSync(logDirectory);
}

const pad = num => (num > 9 ? "" : "0") + num;
const generator = (time, index, baseName = "file") => {
    time = time || new Date();
//   if (!time) return `${baseName}.log`;
  var month = time.getFullYear() + "-" + pad(time.getMonth() + 1);
  var day = pad(time.getDate());
  return `${month}/${month}-${day}-${baseName}.log`;
};

// 创建 Rotating File Stream 用于 HTTP 访问日志
const accessLogStream = rfs.createStream((time, index) => generator(time, index, "http-access"), {
    interval: '1d',      // 每天轮换一次
    size: '10M',         // 当日志文件达到10MB时进行轮换
    path: logDirectory,
    compress: 'gzip',    // 压缩旧的日志文件
    maxFiles: 10         // 保留最近10个轮换的日志文件
});

// 创建 Rotating File Stream 用于模块加载日志
const moduleLoadLogStream = rfs.createStream((time, index) => generator(time, index, "module-load"), {
    interval: '1d',      // 每天轮换一次
    size: '10M',         // 当日志文件达到10MB时进行轮换
    path: logDirectory,
    compress: 'gzip',    // 压缩旧的日志文件
    maxFiles: 10         // 保留最近10个轮换的日志文件
});

// 错误监听
accessLogStream.on('error', (err) => {
    console.error('Rotating File Stream (accessLogStream) 错误:', err);
});

moduleLoadLogStream.on('error', (err) => {
    console.error('Rotating File Stream (moduleLoadLogStream) 错误:', err);
});

// 自定义日志格式化函数
const customFormat = (tokens, req, res) => {
    const timestamp = moment().format('YYYY-MM-DD HH:mm:ss');
    return [
      tokens['remote-addr'](req, res), // 替换 tokens.ip 为 tokens['remote-addr']
      '-',
      tokens['remote-user'](req, res),
      '[' + timestamp + ']',
      '"' + tokens.method(req, res) + ' ' + tokens.url(req, res) + ' ' + tokens['http-version'](req, res) + '"',
      tokens.status(req, res),
      tokens.res(req, res, 'content-length'),
      '-',
      tokens['referrer'](req, res),
      tokens['user-agent'](req, res)
    ].join(' ');
};
// 日志记录函数
function logModuleLoad(moduleName, status, type) {
    const message = `模块加载尝试: [${type}] "${moduleName}" - ${status}`;
    const timestamp = moment().format('YYYY-MM-DD HH:mm:ss');
    moduleLoadLogStream.write(`${timestamp} ${message}\n`);
}

module.exports = {
    accessLogStream,
    moduleLoadLogStream,
    customFormat,
    logModuleLoad
};
const fs = require('fs');
const path = require('path');
const rfs = require('rotating-file-stream');
const moment = require('moment');

const logDirectory = path.join(__dirname,"..",'logs'); // 日志目录

// 确保日志目录存在
if (!fs.existsSync(logDirectory)) {
    fs.mkdirSync(logDirectory);
}

const pad = num => (num > 9 ? "" : "0") + num;
const generator = (time, index, baseName = "file") => {
    time = time || new Date();
//   if (!time) return `${baseName}.log`;
  var month = time.getFullYear() + "-" + pad(time.getMonth() + 1);
  var day = pad(time.getDate());
  return `${month}/${month}-${day}-${baseName}.log`;
};

// 创建 Rotating File Stream 用于 HTTP 访问日志
const accessLogStream = rfs.createStream((time, index) => generator(time, index, "http-access"), {
    interval: '1d',      // 每天轮换一次
    size: '10M',         // 当日志文件达到10MB时进行轮换
    path: logDirectory,
    compress: 'gzip',    // 压缩旧的日志文件
    maxFiles: 10         // 保留最近10个轮换的日志文件
});

// 创建 Rotating File Stream 用于模块加载日志
const moduleLoadLogStream = rfs.createStream((time, index) => generator(time, index, "module-load"), {
    interval: '1d',      // 每天轮换一次
    size: '10M',         // 当日志文件达到10MB时进行轮换
    path: logDirectory,
    compress: 'gzip',    // 压缩旧的日志文件
    maxFiles: 10         // 保留最近10个轮换的日志文件
});

// 错误监听
accessLogStream.on('error', (err) => {
    console.error('Rotating File Stream (accessLogStream) 错误:', err);
});

moduleLoadLogStream.on('error', (err) => {
    console.error('Rotating File Stream (moduleLoadLogStream) 错误:', err);
});

// 自定义日志格式化函数
const customFormat = (tokens, req, res) => {
    const timestamp = moment().format('YYYY-MM-DD HH:mm:ss');
    return [
      tokens['remote-addr'](req, res), // 替换 tokens.ip 为 tokens['remote-addr']
      '-',
      tokens['remote-user'](req, res),
      '[' + timestamp + ']',
      '"' + tokens.method(req, res) + ' ' + tokens.url(req, res) + ' ' + tokens['http-version'](req, res) + '"',
      tokens.status(req, res),
      tokens.res(req, res, 'content-length'),
      '-',
      tokens['referrer'](req, res),
      tokens['user-agent'](req, res)
    ].join(' ');
};
// 日志记录函数
function logModuleLoad(moduleName, status, type) {
    const message = `模块加载尝试: [${type}] "${moduleName}" - ${status}`;
    const timestamp = moment().format('YYYY-MM-DD HH:mm:ss');
    moduleLoadLogStream.write(`${timestamp} ${message}\n`);
}

module.exports = {
    accessLogStream,
    moduleLoadLogStream,
    customFormat,
    logModuleLoad
};
const express = require('express');
const morgan = require('morgan'); // 引入 morgan 库
const logger = require('./util/logger'); // 引入 logger.js
const router = require('./router/index'); // 引入路由模块
const app = express();

// 使用内置的 JSON 解析中间件
app.use(express.json());
// 使用 Morgan 进行 HTTP 请求日志记录
// app.use(morgan('combined', { stream: accessLogStream }));

// 使用自定义格式的日志记录中间件
app.use(morgan(logger.customFormat, { stream: logger.accessLogStream }));

app.use(router);

// 启动服务器
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    console.log(`服务已启动,端口 ${PORT}`);
});

七、使用postman测试

第一次调用

module.exports = async (event) => {
  const name = event.name || 'World';
  return { message: `Hello, ${name}!` };
};

再次调用报错

调用刚上传的test接口查看