OpenHands 多账户方案(二): Nginx 反向代理与动态容器管理

本文将介绍如何设计和实现一套系统,为每个登录用户自动分配独立的OpenHands实例,确保用户数据和计算资源的隔离。

背景

传统的方案通常是预先创建固定数量的OpenHands实例,并通过Nginx进行路由。然而,这种方式缺乏灵活性,无法根据实际需求动态扩展。我们需要一个更智能的系统,可以在用户登录时按需分配资源,并在不使用时释放资源。

架构概述

动态实例分配系统包括三个核心组件:

  1. 前端登录系统 - 处理用户身份验证
  2. 实例管理服务 - 负责创建和分配OpenHands实例
  3. 反向代理系统 - 将用户请求路由到其专属实例

Multi Tenant Frontend Design

实现步骤详解

1. 创建实例管理服务

实例管理服务负责处理用户登录请求,为每个用户分配和管理独立的OpenHands实例。

const express = require('express');
const session = require('express-session');
const { exec } = require('child_process');
const fs = require('fs');
const path = require('path');
const app = express();
const port = 4000;

// 用户会话管理
app.use(session({
  secret: 'your-secret-key',
  resave: false,
  saveUninitialized: true,
  cookie: { secure: true }
}));

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// 存储用户实例信息
const userInstances = {};
let nextPort = 3001; // 起始端口号

// 用户登录处理
app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  
  // 实际应用中应有真实的用户验证逻辑
  
  // 检查用户是否已有实例
  if (!userInstances[username]) {
    // 分配新实例
    const instancePort = nextPort++;
    
    try {
      // 启动新的OpenHands实例
      await startNewInstance(username, instancePort);
      
      // 记录用户实例信息
      userInstances[username] = {
        port: instancePort,
        path: `/user/${username}`,
        lastAccess: Date.now()
      };
      
      // 更新Nginx配置
      await updateNginxConfig();
    } catch (error) {
      console.error(`启动实例失败: ${error.message}`);
      return res.status(500).json({ error: '无法启动您的实例' });
    }
  }
  
  // 设置用户会话
  req.session.username = username;
  req.session.instancePath = userInstances[username].path;
  
  // 重定向到用户的实例
  res.redirect(userInstances[username].path);
});

// 其他核心功能代码...
  1. 实现实例启动和管理 为每个用户启动独立的OpenHands实例,并管理它们的生命周期:
// 启动新实例的函数
function startNewInstance(username, port) {
  return new Promise((resolve, reject) => {
    // 创建用户专属的配置目录
    const userConfigDir = path.join(__dirname, 'user_configs', username);
    
    if (!fs.existsSync(userConfigDir)) {
      fs.mkdirSync(userConfigDir, { recursive: true });
    }
    
    // 启动Docker容器
    const cmd = `docker run -d --name openhands-${username} -p ${port}:3000 -v ${userConfigDir}:/app/config openhands-image`;
    
    exec(cmd, (error, stdout, stderr) => {
      if (error) {
        console.error(`执行错误: ${error}`);
        return reject(error);
      }
      console.log(`实例已启动,端口: ${port}, 输出: ${stdout}`);
      resolve();
    });
  });
}

// 定期清理不活跃的实例
setInterval(() => {
  const now = Date.now();
  const inactivityPeriod = 2 * 60 * 60 * 1000; // 2小时
  
  Object.keys(userInstances).forEach(username => {
    const instance = userInstances[username];
    if (now - instance.lastAccess > inactivityPeriod) {
      // 停止实例
      exec(`docker stop openhands-${username} && docker rm openhands-${username}`, () => {
        console.log(`已停止不活跃实例: ${username}`);
        delete userInstances[username];
        // 更新Nginx配置
        updateNginxConfig().catch(console.error);
      });
    }
  });
}, 15 * 60 * 1000); // 每15分钟检查一次
  1. 动态更新Nginx配置 系统能够动态更新Nginx配置,确保每个用户请求都被正确路由到其专属实例:
// 更新Nginx配置
function updateNginxConfig() {
  return new Promise((resolve, reject) => {
    let nginxConfig = `
server {
    listen 80;
    server_name aicoder.westus.cloudapp.azure.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name aicoder.westus.cloudapp.azure.com;

    ssl_certificate /etc/letsencrypt/live/aicoder.westus.cloudapp.azure.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/aicoder.westus.cloudapp.azure.com/privkey.pem;
    
    # 认证与实例管理服务
    location / {
        proxy_pass http://127.0.0.1:4000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
`;

    // 为每个用户添加配置
    Object.keys(userInstances).forEach(username => {
      const instance = userInstances[username];
      nginxConfig += `
    # ${username}的OpenHands实例
    location ${instance.path}/ {
        proxy_pass http://127.0.0.1:${instance.port}/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
`;
    });

    nginxConfig += `
}
`;

    fs.writeFile('/etc/nginx/sites-available/aicoder', nginxConfig, (err) => {
      if (err) {
        console.error(`更新Nginx配置失败: ${err}`);
        return reject(err);
      }
      
      // 重载Nginx配置
      exec('nginx -t && systemctl reload nginx', (error) => {
        if (error) {
          console.error(`重载Nginx失败: ${error}`);
          return reject(error);
        }
        resolve();
      });
    });
  });
}
  1. 创建友好的登录界面 为了提供良好的用户体验,我们需要创建一个直观的登录界面:
<!DOCTYPE html>
<html>
<head>
    <title>OpenHands - 登录</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
            background-color: #f5f5f5;
        }
        .login-container {
            background: white;
            padding: 2rem;
            border-radius: 8px;
            box-shadow: 0 4px 6px rgba(0,0,0,0.1);
            width: 100%;
            max-width: 400px;
        }
        /* 更多样式... */
    </style>
</head>
<body>
    <div class="login-container">
        <h1>欢迎使用 OpenHands</h1>
        <form action="/login" method="post">
            <div class="form-group">
                <label for="username">用户名</label>
                <input type="text" id="username" name="username" required>
            </div>
            <div class="form-group">
                <label for="password">密码</label>
                <input type="password" id="password" name="password" required>
            </div>
            <button type="submit">登录</button>
        </form>
    </div>
</body>
</html>

系统优势

我们的动态实例分配系统具有以下优势:

  1. 资源利用效率高 - 按需分配资源,避免资源浪费
  2. 用户隔离 - 每个用户都拥有独立的运行环境,确保安全性和稳定性
  3. 自动资源回收 - 系统会自动清理长时间不活跃的实例
  4. 可扩展性 - 基础设施可以根据实际用户需求进行扩展
  5. 一致的用户体验 - 用户每次登录都可以获得相同的环境配置

部署和维护注意事项

在实际部署过程中,需要注意以下几点:

资源管理

  • 设置服务器资源上限,避免过度分配
  • 监控系统资源使用情况,及时扩容

数据持久化

  • 确保用户数据正确保存到持久化存储
  • 实现定期备份机制,防止数据丢失

安全性

  • 加强用户认证系统,可考虑集成OAuth或其他身份验证方式
  • 限制每个实例的权限,避免权限逃逸

高可用性

  • 考虑实例管理服务的冗余部署
  • 实现实例状态监控和自动恢复机制

结语

通过这种动态实例分配方案,我们可以为每个用户提供独立的OpenHands环境,既保证了系统的安全性和稳定性,又实现了资源的高效利用。这种方法特别适合需要为多个用户提供隔离计算环境的场景,如在线编程平台、AI开发环境等。

未来,我们可以进一步优化这一系统,例如加入负载均衡机制、跨服务器实例分配等功能,使其更加强大和灵活。

留言与讨论