feat:添加了并发处理和提前启动容器的功能
This commit is contained in:
parent
05a7d85dfc
commit
f6e0a21d4e
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
// 数据库配置(增加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);
|
||||
}
|
||||
|
||||
if (!isReady) throw new Error(`Database not ready within ${CONTAINER_TIMEOUT} seconds`);
|
||||
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秒执行一次维护
|
||||
|
Loading…
Reference in New Issue
Block a user