feat:添加了并发处理和提前启动容器的功能

This commit is contained in:
fly6516 2025-02-24 20:00:40 +08:00
parent 05a7d85dfc
commit f6e0a21d4e

View File

@ -3,231 +3,309 @@
import Docker from 'dockerode';
const docker = new Docker();
const CONTAINER_TIMEOUT = 60; // 容器超时时间(秒)
const CONTAINER_TIMEOUT = 60; // 容器操作超时时间(秒)
const PREWARM_COUNT = 2; // 预热容器数量配置
const SQL_SERVER_STARTUP_DELAY = 15000; // SQL Server启动等待时间毫秒
// 安全容器操作函数
async function safeContainerStop(container: Docker.Container) {
// 端口管理配置
const PORT_CONFIG = {
min: 5000,
max: 6000,
current: 5000
};
// 容器池数据结构
type ContainerPool = {
[key: string]: {
free: Docker.ContainerInfo[];
used: Map<string, Docker.ContainerInfo>;
}
};
const containerPool: ContainerPool = {
sqlserver: { free: [], used: new Map() },
mysql: { free: [], used: new Map() }
};
// 初始化预热容器
(async () => {
await Promise.all([
...Array(PREWARM_COUNT).fill(null).map(() => createContainer('sqlserver')),
...Array(PREWARM_COUNT).fill(null).map(() => createContainer('mysql'))
]);
console.log('预热容器初始化完成');
})();
// 安全容器操作方法
async function safeStopContainer(container: Docker.Container) {
try {
await container.stop();
} catch (error: any) {
if (![409, 404].includes(error.statusCode)) {
if (![404, 409].includes(error.statusCode)) {
console.error('停止容器失败:', error);
throw error;
}
}
}
async function safeContainerRemove(container: Docker.Container) {
async function safeRemoveContainer(container: Docker.Container) {
try {
await container.remove();
} catch (error: any) {
if (error.statusCode !== 404) {
console.error('删除容器失败:', error);
throw error;
}
}
}
export default async function serverAction(
sql: string,
databaseType: string
): Promise<string> {
let dockerImage: string;
let command: string;
let env: string[] = [];
let container: Docker.Container;
// 配置数据库参数
switch (databaseType) {
case 'sqlserver':
process.env.SQL_SERVER_SA_PASSWORD = 'YourStrong!Passw0rd';
dockerImage = 'mcr.microsoft.com/mssql/server:2019-latest';
command = `/opt/mssql-tools18/bin/sqlcmd -S localhost -U SA -P ${process.env.SQL_SERVER_SA_PASSWORD} -C -Q "${sql}"`;
env = ['ACCEPT_EULA=Y', `SA_PASSWORD=${process.env.SQL_SERVER_SA_PASSWORD}`];
break;
case 'mysql':
process.env.MYSQL_ROOT_PASSWORD = 'YourStrong!Passw0rd';
dockerImage = 'mysql:latest';
command = `mysql -u root -p${process.env.MYSQL_ROOT_PASSWORD} -e "${sql}"`;
env = [
`MYSQL_ROOT_PASSWORD=${process.env.MYSQL_ROOT_PASSWORD}`,
'MYSQL_TCP_PORT=3306',
'MYSQL_ROOT_HOST=%',
];
break;
default:
throw new Error('Unsupported database type');
}
// 容器创建逻辑
async function createContainer(dbType: string) {
const port = getAvailablePort();
const config = getDatabaseConfig(dbType);
try {
// 创建并启动容器
container = await docker.createContainer({
Image: dockerImage,
Env: databaseType === 'sqlserver'
? [...env, 'MSSQL_AGENT_ENABLED=True', 'MSSQL_TCP_PROTOCOL=1']
: env,
const container = await docker.createContainer({
Image: config.image,
Env: config.env,
HostConfig: {
AutoRemove: true,
PortBindings: {
[databaseType === 'sqlserver' ? '1433/tcp' : '3306/tcp']: [
{ HostPort: databaseType === 'sqlserver' ? '1433' : '3306' }
[dbType === 'sqlserver' ? '1433/tcp' : '3306/tcp']: [
{ HostPort: port.toString() }
]
},
...(databaseType === 'sqlserver' && {
Memory: 2147483648
...(dbType === 'sqlserver' && {
Memory: 2147483648,
NetworkMode: 'bridge'
})
}
});
if (databaseType === 'sqlserver') {
await container.start();
// 安装依赖工具链
await new Promise((resolve, reject) => {
const exec = container.exec({
Cmd: [
'/bin/bash',
'-c',
'echo "YourStrong!Passw0rd" | su -c "apt-get update && apt-get install -y curl && curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - && curl https://packages.microsoft.com/config/ubuntu/20.04/prod.list > /etc/apt/sources.list.d/mssql-release.list"'
],
AttachStdout: true,
AttachStderr: true
}, (err, exec) => {
if (err) return reject(err);
exec?.start({}, (err, stream) => {
if (err) return reject(err);
stream?.on('end', () => resolve(null));
stream?.resume();
});
});
});
// 安装核心组件
await new Promise((resolve, reject) => {
const exec = container.exec({
Cmd: [
'/bin/bash',
'-c',
'ACCEPT_EULA=Y apt-get install -y msodbcsql17 mssql-tools && echo "export PATH=$PATH:/opt/mssql-tools/bin" >> ~/.bashrc && source ~/.bashrc'
],
AttachStdout: true,
AttachStderr: true
}, (err, exec) => {
if (err) return reject(err);
exec?.start({}, (err, stream) => {
if (err) return reject(err);
stream?.on('end', () => resolve(null));
stream?.resume();
});
});
});
await new Promise(resolve => setTimeout(resolve, 5000));
} else {
await container.start();
// SQL Server需要额外启动等待时间
if (dbType === 'sqlserver') {
await new Promise(resolve => setTimeout(resolve, SQL_SERVER_STARTUP_DELAY));
}
// 健康检查逻辑
let isReady = false;
const startTime = Date.now();
while (Date.now() - startTime < CONTAINER_TIMEOUT * 1000) {
// 执行健康检查
if (await healthCheck(container, dbType)) {
const info = await container.inspect();
containerPool[dbType].free.push({
...info,
Port: port
});
console.log(`已创建 ${dbType} 容器:${info.Id}`);
}
} catch (error) {
console.error(`创建 ${dbType} 容器失败:`, error);
throw error;
}
}
// 健康检查逻辑(增强版)
async function healthCheck(container: Docker.Container, dbType: string) {
const config = getDatabaseConfig(dbType);
const containerInfo = await container.inspect();
const containerIp = containerInfo.NetworkSettings.Networks.bridge?.IPAddress;
if (!containerIp) {
console.error('无法获取容器IP地址');
await safeRemoveContainer(container);
return false;
}
const testCommand = dbType === 'sqlserver'
? `/opt/mssql-tools18/bin/sqlcmd -S ${containerIp},1433 -U SA -P ${config.password} -Q "SELECT 1" -C -N -l 30`
: `mysql -h ${containerIp} -u root -p${config.password} -e "SELECT 1"`;
try {
const healthCheckCmd = databaseType === 'sqlserver'
? [
'/opt/mssql-tools18/bin/sqlcmd',
'-S', 'localhost',
'-U', 'SA',
'-P', process.env.SQL_SERVER_SA_PASSWORD,
'-C',
'-Q', 'SELECT 1'
]
: [
'sh',
'-c',
`mysql -h 127.0.0.1 -u root -p${process.env.MYSQL_ROOT_PASSWORD} -e "SELECT 1" 2>&1 | grep -v "Using a password"`
];
const exec = await container.exec({
Cmd: healthCheckCmd,
Cmd: ['sh', '-c', testCommand],
AttachStdout: true,
AttachStderr: true
});
const output = await new Promise<string>((resolve, reject) => {
exec.start({}, (err, stream) => {
// 设置30秒超时
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('健康检查超时')), 30000)
);
await Promise.race([
new Promise((resolve, reject) => {
exec.start((err, stream) => {
if (err) return reject(err);
let data = '';
stream?.on('data', (chunk: Buffer) => data += chunk.toString());
stream?.on('end', () => resolve(data));
stream?.on('end', resolve);
stream?.on('error', reject);
stream?.resume();
});
});
}),
timeoutPromise
]);
if (output.includes("ERROR")) throw new Error(output);
isReady = true;
break;
} catch {
await new Promise(resolve => setTimeout(resolve, 2000));
return true;
} catch (error) {
console.error(`健康检查失败: ${error}`);
await safeRemoveContainer(container);
return false;
}
}
if (!isReady) throw new Error(`Database not ready within ${CONTAINER_TIMEOUT} seconds`);
// 数据库配置增加SSL参数
function getDatabaseConfig(dbType: string) {
const password = 'YourStrong!Passw0rd';
return {
password,
image: dbType === 'sqlserver'
? 'mcr.microsoft.com/mssql/server:2019-latest'
: 'mysql:latest',
env: dbType === 'sqlserver' ? [
'ACCEPT_EULA=Y',
`SA_PASSWORD=${password}`,
'MSSQL_AGENT_ENABLED=True',
'MSSQL_TCP_PORT=1433',
'MSSQL_LCID=1033',
'MSSQL_MEMORY_LIMIT_MB=2048',
'MSSQL_OPTION_FLAGS=-C' // 启用信任服务器证书
] : [
`MYSQL_ROOT_PASSWORD=${password}`,
'MYSQL_TCP_PORT=3306',
'MYSQL_ROOT_HOST=%'
]
};
}
// 端口分配
function getAvailablePort() {
PORT_CONFIG.current =
PORT_CONFIG.current >= PORT_CONFIG.max
? PORT_CONFIG.min
: PORT_CONFIG.current + 1;
return PORT_CONFIG.current;
}
// 主服务函数(关键修复点)
export default async function serverAction(
sql: string,
dbType: string
): Promise<string> {
let containerInfo: Docker.ContainerInfo | undefined;
try {
// 获取或创建容器
if (containerPool[dbType].free.length === 0) {
await createContainer(dbType);
}
containerInfo = containerPool[dbType].free.pop()!;
containerPool[dbType].used.set(containerInfo.Id, containerInfo);
const container = docker.getContainer(containerInfo.Id);
const containerIp = containerInfo.NetworkSettings.Networks.bridge?.IPAddress;
if (!containerIp) {
throw new Error('无法获取容器IP地址');
}
// 构建执行命令增加SSL参数
const command = dbType === 'sqlserver'
? `/opt/mssql-tools18/bin/sqlcmd -S ${containerIp},1433 -U SA -P ${getDatabaseConfig(dbType).password} -Q "${sql}" -C -N -l 30`
: `mysql -h ${containerIp} -u root -p${getDatabaseConfig(dbType).password} -e "${sql}"`;
// 执行SQL命令
const execResult = await new Promise<string>((resolve, reject) => {
container.exec({
Cmd: [
databaseType === 'sqlserver' ? '/bin/bash' : '/bin/sh',
'-c',
command
],
const exec = await container.exec({
Cmd: ['sh', '-c', command],
AttachStdout: true,
AttachStderr: true
}, (err, exec) => {
if (err) return reject(err);
});
exec?.start({}, (err, stream) => {
if (err) return reject(err);
// 增加执行状态跟踪
let isCompleted = false;
const cleanup = async () => {
if (!isCompleted && containerInfo) {
console.log('执行未完成,执行清理...');
await safeRemoveContainer(container);
containerPool[dbType].used.delete(containerInfo.Id);
await createContainer(dbType);
}
};
const result = await new Promise<string>((resolve, reject) => {
let output = '';
exec.start({}, async (err, stream) => {
if (err) {
await cleanup();
return reject(err);
}
stream?.on('data', (chunk: Buffer) => {
output += chunk.toString();
});
stream?.on('end', () => {
if (databaseType === 'mysql') {
stream?.on('end', async () => {
try {
if (dbType === 'mysql') {
output = output.replace(/Warning: Using a password on the command line interface can be insecure.\n/g, '');
}
isCompleted = true;
// 安全归还容器
await safeStopContainer(container);
containerPool[dbType].free.push(containerInfo!);
containerPool[dbType].used.delete(containerInfo!.Id);
resolve(output);
} catch (cleanupError) {
await cleanup();
reject(cleanupError);
}
});
stream?.on('error', async (error) => {
await cleanup();
reject(error);
});
stream?.resume();
});
});
});
// 安全停止容器
await safeContainerStop(container);
// 兜底状态检查
setTimeout(async () => {
try {
const info = await container.inspect();
if (info.State.Running) {
await safeContainerStop(container);
}
} catch (error: any) {
if (error.statusCode !== 404) {
console.error('Post-execution cleanup check failed:', error.message);
}
}
}, 2000);
return execResult;
return result;
} catch (error) {
// 错误处理流程
if (container) {
await safeContainerStop(container);
await safeContainerRemove(container);
if (containerInfo) {
await safeRemoveContainer(docker.getContainer(containerInfo.Id));
containerPool[dbType].used.delete(containerInfo.Id);
await createContainer(dbType);
}
throw error instanceof Error ? error : new Error('Unknown error');
console.error('执行错误:', error);
throw error instanceof Error ? error : new Error('未知错误');
}
}
// 后台维护任务
setInterval(async () => {
try {
for (const dbType of ['sqlserver', 'mysql']) {
// 维持预热容器数量
while (containerPool[dbType].free.length < PREWARM_COUNT) {
await createContainer(dbType);
}
// 清理无效容器
containerPool[dbType].free = (await Promise.all(
containerPool[dbType].free.map(async (info) => {
try {
const container = docker.getContainer(info.Id);
await container.inspect();
return info;
} catch {
return null;
}
})
)).filter(Boolean) as Docker.ContainerInfo[];
}
} catch (error) {
console.error('维护任务出错:', error);
}
}, 30000); // 每30秒执行一次维护