feat:添加了并发处理和提前启动容器的功能
This commit is contained in:
parent
05a7d85dfc
commit
f6e0a21d4e
@ -3,231 +3,309 @@
|
|||||||
import Docker from 'dockerode';
|
import Docker from 'dockerode';
|
||||||
|
|
||||||
const docker = new Docker();
|
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 {
|
try {
|
||||||
await container.stop();
|
await container.stop();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (![409, 404].includes(error.statusCode)) {
|
if (![404, 409].includes(error.statusCode)) {
|
||||||
|
console.error('停止容器失败:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function safeContainerRemove(container: Docker.Container) {
|
async function safeRemoveContainer(container: Docker.Container) {
|
||||||
try {
|
try {
|
||||||
await container.remove();
|
await container.remove();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.statusCode !== 404) {
|
if (error.statusCode !== 404) {
|
||||||
|
console.error('删除容器失败:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function serverAction(
|
// 容器创建逻辑
|
||||||
sql: string,
|
async function createContainer(dbType: string) {
|
||||||
databaseType: string
|
const port = getAvailablePort();
|
||||||
): Promise<string> {
|
const config = getDatabaseConfig(dbType);
|
||||||
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');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 创建并启动容器
|
const container = await docker.createContainer({
|
||||||
container = await docker.createContainer({
|
Image: config.image,
|
||||||
Image: dockerImage,
|
Env: config.env,
|
||||||
Env: databaseType === 'sqlserver'
|
|
||||||
? [...env, 'MSSQL_AGENT_ENABLED=True', 'MSSQL_TCP_PROTOCOL=1']
|
|
||||||
: env,
|
|
||||||
HostConfig: {
|
HostConfig: {
|
||||||
AutoRemove: true,
|
AutoRemove: true,
|
||||||
PortBindings: {
|
PortBindings: {
|
||||||
[databaseType === 'sqlserver' ? '1433/tcp' : '3306/tcp']: [
|
[dbType === 'sqlserver' ? '1433/tcp' : '3306/tcp']: [
|
||||||
{ HostPort: databaseType === 'sqlserver' ? '1433' : '3306' }
|
{ HostPort: port.toString() }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
...(databaseType === 'sqlserver' && {
|
...(dbType === 'sqlserver' && {
|
||||||
Memory: 2147483648
|
Memory: 2147483648,
|
||||||
|
NetworkMode: 'bridge'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (databaseType === 'sqlserver') {
|
await container.start();
|
||||||
await container.start();
|
|
||||||
|
|
||||||
// 安装依赖工具链
|
// SQL Server需要额外启动等待时间
|
||||||
await new Promise((resolve, reject) => {
|
if (dbType === 'sqlserver') {
|
||||||
const exec = container.exec({
|
await new Promise(resolve => setTimeout(resolve, SQL_SERVER_STARTUP_DELAY));
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 健康检查逻辑
|
// 执行健康检查
|
||||||
let isReady = false;
|
if (await healthCheck(container, dbType)) {
|
||||||
const startTime = Date.now();
|
const info = await container.inspect();
|
||||||
while (Date.now() - startTime < CONTAINER_TIMEOUT * 1000) {
|
containerPool[dbType].free.push({
|
||||||
try {
|
...info,
|
||||||
const healthCheckCmd = databaseType === 'sqlserver'
|
Port: port
|
||||||
? [
|
});
|
||||||
'/opt/mssql-tools18/bin/sqlcmd',
|
console.log(`已创建 ${dbType} 容器:${info.Id}`);
|
||||||
'-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,
|
|
||||||
AttachStdout: true,
|
|
||||||
AttachStderr: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const output = await new Promise<string>((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?.resume();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (output.includes("ERROR")) throw new Error(output);
|
|
||||||
isReady = true;
|
|
||||||
break;
|
|
||||||
} catch {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`创建 ${dbType} 容器失败:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!isReady) throw new Error(`Database not ready within ${CONTAINER_TIMEOUT} seconds`);
|
// 健康检查逻辑(增强版)
|
||||||
|
async function healthCheck(container: Docker.Container, dbType: string) {
|
||||||
|
const config = getDatabaseConfig(dbType);
|
||||||
|
const containerInfo = await container.inspect();
|
||||||
|
const containerIp = containerInfo.NetworkSettings.Networks.bridge?.IPAddress;
|
||||||
|
|
||||||
// 执行SQL命令
|
if (!containerIp) {
|
||||||
const execResult = await new Promise<string>((resolve, reject) => {
|
console.error('无法获取容器IP地址');
|
||||||
container.exec({
|
await safeRemoveContainer(container);
|
||||||
Cmd: [
|
return false;
|
||||||
databaseType === 'sqlserver' ? '/bin/bash' : '/bin/sh',
|
}
|
||||||
'-c',
|
|
||||||
command
|
|
||||||
],
|
|
||||||
AttachStdout: true,
|
|
||||||
AttachStderr: true
|
|
||||||
}, (err, exec) => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
|
|
||||||
exec?.start({}, (err, stream) => {
|
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 exec = await container.exec({
|
||||||
|
Cmd: ['sh', '-c', testCommand],
|
||||||
|
AttachStdout: true,
|
||||||
|
AttachStderr: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置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);
|
if (err) return reject(err);
|
||||||
let output = '';
|
stream?.on('end', resolve);
|
||||||
|
stream?.on('error', reject);
|
||||||
stream?.on('data', (chunk: Buffer) => {
|
|
||||||
output += chunk.toString();
|
|
||||||
});
|
|
||||||
|
|
||||||
stream?.on('end', () => {
|
|
||||||
if (databaseType === 'mysql') {
|
|
||||||
output = output.replace(/Warning: Using a password on the command line interface can be insecure.\n/g, '');
|
|
||||||
}
|
|
||||||
resolve(output);
|
|
||||||
});
|
|
||||||
|
|
||||||
stream?.resume();
|
stream?.resume();
|
||||||
});
|
});
|
||||||
|
}),
|
||||||
|
timeoutPromise
|
||||||
|
]);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 exec = await container.exec({
|
||||||
|
Cmd: ['sh', '-c', command],
|
||||||
|
AttachStdout: true,
|
||||||
|
AttachStderr: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// 增加执行状态跟踪
|
||||||
|
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', 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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 安全停止容器
|
return result;
|
||||||
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;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 错误处理流程
|
if (containerInfo) {
|
||||||
if (container) {
|
await safeRemoveContainer(docker.getContainer(containerInfo.Id));
|
||||||
await safeContainerStop(container);
|
containerPool[dbType].used.delete(containerInfo.Id);
|
||||||
await safeContainerRemove(container);
|
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