在线代码运行
代码运行流程:
- 用户输入代码
- 将代码写入宿主机的临时文件中
- 启动一个Docker容器
- 将临时文件复制到容器中
- 利用容器执行编译运行代码
- 返回执行结果
- 清理临时文件和Docker容器
镜像构建
Dockerfile
:
# Create from ubuntu image
FROM ubuntu:20.04
# Change default shell to bash
SHELL ["/bin/bash", "-c"]
# 设置为中国国内源
RUN sed -i s@/ports.ubuntu.com/@/mirrors.aliyun.com/@g /etc/apt/sources.list
RUN sed -i s@/archive.ubuntu.com/@/mirrors.aliyun.com/@g /etc/apt/sources.list
RUN sed -i s@/security.ubuntu.com/@/mirrors.aliyun.com/@g /etc/apt/sources.list
RUN apt-get clean
# Update the image to the latest packages
RUN apt-get update
RUN apt-get upgrade -y
# Install required packages
RUN apt-get install software-properties-common -y
RUN apt-get install zip unzip curl wget tar -y
# Install python
RUN apt-get install python python3-pip -y
# Install C
RUN apt-get install gcc -y
# Install C++
RUN apt-get install g++ -y
# Install java
RUN apt-get install default-jre -y
RUN apt-get install default-jdk -y
# Install nodejs
RUN curl -fsSL https://deb.nodesource.com/setup_16.x | bash -
RUN apt-get install nodejs -y
# Install golang
RUN apt-get install golang -y
ENV GOCACHE /box
ENV GOTMPDIR /box
# Update packages
RUN apt-get clean -y
RUN apt-get autoclean -y
RUN apt-get autoremove -y
# Set default workdir
WORKDIR /box
构建镜像:
docker build . -t codesandbox:latest
运行后查看占用情况:
docker stats
Java操作Docker
<!-- https://mvnrepository.com/artifact/com.github.docker-java/docker-java -->
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java</artifactId>
<version>3.3.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.github.docker-java/docker-java-transport-httpclient5 -->
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java-transport-httpclient5</artifactId>
<version>3.3.0</version>
</dependency>
请求实体
@Data
public class ExecuteRequest {
private String language;
private String code;
}
执行结果实体
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class ExecuteMessage {
private boolean success;
private String message;
private String errorMessage;
}
可运行语言枚举
/**
* 编程语言 cmd 枚举
* 不需要编译的语言编译的 cmd 设置为空即可
*/
@Getter
public enum LanguageCmdEnum {
JAVA("java", "Main.java", new String[]{"javac", "-encoding", "utf-8", "Main.java"}, new String[]{"java", "-Dfile.encoding=UTF-8", "Main"}),
CPP("cpp", "main.cpp", new String[]{"g++", "-finput-charset=UTF-8", "-fexec-charset=UTF-8", "-o", "main", "main.cpp"}, new String[]{"./main"}),
C("c", "main.c", new String[]{"gcc", "-finput-charset=UTF-8", "-fexec-charset=UTF-8", "-o", "main", "main.c"}, new String[]{"./main"}),
PYTHON3("python", "main.py", null, new String[]{"python3", "main.py"}),
JAVASCRIPT("javascript", "main.js", null, new String[]{"node", "main.js"}),
TYPESCRIPT("typescript", "main.ts", null, new String[]{"node", "main.ts"}),
GO("go", "main.go", null, new String[]{"go", "run", "main.go"}),
;
private final String language;
/**
* 保存的文件名
*/
private final String saveFileName;
private final String[] compileCmd;
private final String[] runCmd;
LanguageCmdEnum(String language, String saveFileName, String[] compileCmd, String[] runCmd) {
this.language = language;
this.saveFileName = saveFileName;
this.compileCmd = compileCmd;
this.runCmd = runCmd;
}
/**
* 根据 language 获取枚举
*
* @param language 值
* @return {@link LanguageCmdEnum}
*/
public static LanguageCmdEnum getEnumByValue(String language) {
if (StringUtils.isBlank(language)) {
return null;
}
for (LanguageCmdEnum languageCmdEnum : LanguageCmdEnum.values()) {
if (languageCmdEnum.language.equals(language)) {
return languageCmdEnum;
}
}
return null;
}
}
Docker沙箱执行
@Slf4j
@Data
@ConfigurationProperties(prefix = "codesandbox.config")
@Configuration
public class DockerSandbox {
private static final DockerClient DOCKER_CLIENT = DockerClientBuilder.getInstance().build();
/**
* 代码沙箱的镜像,Dockerfile 构建的镜像名,默认为 codesandbox:latest
*/
private String image = "codesandbox:latest";
/**
* 内存限制,单位为字节,默认为 1024 * 1024 * 60 MB
*/
private long memoryLimit = 1024 * 1024 * 60;
private long memorySwap = 0;
/**
* 最大可消耗的 cpu 数
*/
private long cpuCount = 1;
private long timeoutLimit = 1;
private TimeUnit timeUnit = TimeUnit.SECONDS;
/**
* 执行代码
*
* @param languageCmdEnum 编程语言枚举
* @param code 代码
* @return {@link ExecuteMessage}
*/
public ExecuteMessage execute(LanguageCmdEnum languageCmdEnum, String code) {
// 写入文件
String userDir = System.getProperty("user.dir");
String language = languageCmdEnum.getLanguage();
String globalCodePathName = userDir + File.separator + "tempCode" + File.separator + language;
// 判断全局代码目录是否存在,没有则新建
File globalCodePath = new File(globalCodePathName);
if (!globalCodePath.exists()) {
boolean mkdir = globalCodePath.mkdirs();
if (!mkdir) {
log.info("创建全局代码目录失败");
}
}
// 把用户的代码隔离存放
String userCodeParentPath = globalCodePathName + File.separator + UUID.randomUUID();
String userCodePath = userCodeParentPath + File.separator + languageCmdEnum.getSaveFileName();
FileUtil.writeString(code, userCodePath, StandardCharsets.UTF_8);
String containerId = createContainer(userCodePath);
// 编译代码
String[] compileCmd = languageCmdEnum.getCompileCmd();
ExecuteMessage executeMessage;
// 不为空则代表需要编译
if (compileCmd != null) {
executeMessage = execCmd(containerId, compileCmd);
log.info("编译完成...");
// 编译错误
if (!executeMessage.isSuccess()) {
// 清理文件和容器
cleanFileAndContainer(userCodeParentPath, containerId);
return executeMessage;
}
}
executeMessage = execCmd(containerId, languageCmdEnum.getRunCmd());
log.info("运行完成...");
// 清理文件和容器
cleanFileAndContainer(userCodeParentPath, containerId);
return executeMessage;
}
/**
* 清理文件和容器
*
* @param userCodePath 用户代码路径
* @param containerId 容器 ID
*/
private static void cleanFileAndContainer(String userCodePath, String containerId) {
// 清理临时目录
FileUtil.del(userCodePath);
// 关闭并删除容器
DOCKER_CLIENT.stopContainerCmd(containerId).exec();
DOCKER_CLIENT.removeContainerCmd(containerId).exec();
}
/**
* 执行命令
*
* @param containerId 容器 ID
* @param cmd CMD
* @return {@link ExecuteMessage}
*/
private ExecuteMessage execCmd(String containerId, String[] cmd) {
// 正常返回信息
ByteArrayOutputStream resultStream = new ByteArrayOutputStream();
// 错误信息
ByteArrayOutputStream errorResultStream = new ByteArrayOutputStream();
// 结果
final boolean[] result = {true};
try (ResultCallback.Adapter<Frame> frameAdapter = new ResultCallback.Adapter<Frame>() {
@Override
public void onNext(Frame frame) {
StreamType streamType = frame.getStreamType();
byte[] payload = frame.getPayload();
if (StreamType.STDERR.equals(streamType)) {
try {
result[0] = false;
errorResultStream.write(payload);
} catch (IOException e) {
throw new RuntimeException(e);
}
} else {
try {
result[0] = true;
resultStream.write(payload);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
super.onNext(frame);
}
}) {
ExecCreateCmdResponse execCompileCmdResponse = DOCKER_CLIENT.execCreateCmd(containerId)
.withCmd(cmd)
.withAttachStderr(true)
.withAttachStdin(true)
.withAttachStdout(true)
.exec();
String execId = execCompileCmdResponse.getId();
DOCKER_CLIENT.execStartCmd(execId).exec(frameAdapter).awaitCompletion(timeoutLimit, timeUnit);
return ExecuteMessage
.builder()
.success(result[0])
.message(resultStream.toString())
.errorMessage(errorResultStream.toString())
.build();
} catch (IOException | InterruptedException e) {
log.info(e.getMessage());
return ExecuteMessage
.builder()
.success(false)
.errorMessage(e.getMessage())
.build();
}
}
/**
* 创建容器
*
* @return {@link String}
*/
private String createContainer(String codeFile) {
CreateContainerCmd containerCmd = DOCKER_CLIENT.createContainerCmd(image);
HostConfig hostConfig = new HostConfig();
hostConfig.withMemory(memoryLimit);
hostConfig.withMemorySwap(memorySwap);
hostConfig.withCpuCount(cpuCount);
CreateContainerResponse createContainerResponse = containerCmd
.withHostConfig(hostConfig)
.withNetworkDisabled(true)
.withAttachStdin(true)
.withAttachStderr(true)
.withAttachStdout(true)
.withTty(true)
.exec();
// 启动容器
String containerId = createContainerResponse.getId();
DOCKER_CLIENT.startContainerCmd(containerId).exec();
// 将代码复制到容器中
DOCKER_CLIENT.copyArchiveToContainerCmd(containerId)
.withHostResource(codeFile)
.withRemotePath("/box")
.exec();
return containerId;
}
}
Spring中使用
在resources/META-INF/spring
目录下,新建一个org.springframework.boot.autoconfigure.AutoConfiguation.imports
文件
里面加载Bean:com.yunfei.codesandbox.executor.DockerSandbox
其他项目使用时添加配置:
codesandbox:
config:
cpu-count: 1
image: codesandbox:latest
memory-limit: 1024 * 1024 * 60
memory-swap: 0
time-unit: seconds
timeout-limit: 1
容器池设计
创建一个容器池,保留一下可用的Docker容器,如果没有可用的容器,就需要等待,如果等待的过多 ,就需要扩容。
DockerDao
@Slf4j
@Data
@Configuration
@ConfigurationProperties(prefix = "codesandbox.config")
public class DockerDao {
/**
* 代码沙箱的镜像,Dockerfile 构建的镜像名,默认为 codesandbox:latest
*/
private String image = "codesandbox:latest";
/**
* 内存限制,单位为字节,默认为 1024 * 1024 * 60 MB
*/
private long memoryLimit = 1024 * 1024 * 60;
private long memorySwap = 0;
/**
* 最大可消耗的 cpu 数
*/
private long cpuCount = 1;
private long timeoutLimit = 1;
private TimeUnit timeUnit = TimeUnit.SECONDS;
private static final DockerClient DOCKER_CLIENT = DockerClientBuilder.getInstance().build();
/**
* 执行命令
*
* @param containerId 容器 ID
* @param cmd CMD
* @return {@link ExecuteMessage}
*/
public ExecuteMessage execCmd(String containerId, String[] cmd) {
// 正常返回信息
ByteArrayOutputStream resultStream = new ByteArrayOutputStream();
// 错误信息
ByteArrayOutputStream errorResultStream = new ByteArrayOutputStream();
// 结果
final boolean[] result = {true};
final boolean[] timeout = {true};
try (ResultCallback.Adapter<Frame> frameAdapter = new ResultCallback.Adapter<Frame>() {
@Override
public void onComplete() {
// 是否超时
timeout[0] = false;
super.onComplete();
}
@Override
public void onNext(Frame frame) {
StreamType streamType = frame.getStreamType();
byte[] payload = frame.getPayload();
if (StreamType.STDERR.equals(streamType)) {
try {
result[0] = false;
errorResultStream.write(payload);
} catch (IOException e) {
throw new RuntimeException(e);
}
} else {
try {
resultStream.write(payload);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
super.onNext(frame);
}
}) {
ExecCreateCmdResponse execCompileCmdResponse = DOCKER_CLIENT.execCreateCmd(containerId)
.withCmd(cmd)
.withAttachStderr(true)
.withAttachStdin(true)
.withAttachStdout(true)
.exec();
String execId = execCompileCmdResponse.getId();
DOCKER_CLIENT.execStartCmd(execId).exec(frameAdapter).awaitCompletion(timeoutLimit, timeUnit);
// 超时
if (timeout[0]) {
return ExecuteMessage
.builder()
.success(false)
.errorMessage("执行超时")
.build();
}
return ExecuteMessage
.builder()
.success(result[0])
.message(resultStream.toString())
.errorMessage(errorResultStream.toString())
.build();
} catch (IOException | InterruptedException e) {
return ExecuteMessage
.builder()
.success(false)
.errorMessage(e.getMessage())
.build();
}
}
public ContainerInfo startContainer(String codePath) {
CreateContainerCmd containerCmd = DOCKER_CLIENT.createContainerCmd(image);
HostConfig hostConfig = new HostConfig();
hostConfig.withMemory(memoryLimit);
hostConfig.withMemorySwap(memorySwap);
hostConfig.withCpuCount(cpuCount);
// hostConfig.withReadonlyRootfs(true);
// hostConfig.setBinds(new Bind(codePath, new Volume("/box")));
CreateContainerResponse createContainerResponse = containerCmd
.withHostConfig(hostConfig)
.withNetworkDisabled(true)
.withAttachStdin(true)
.withAttachStderr(true)
.withAttachStdout(true)
.withTty(true)
.exec();
String containerId = createContainerResponse.getId();
log.info("containerId: {}", containerId);
// 启动容器
DOCKER_CLIENT.startContainerCmd(containerId).exec();
return ContainerInfo
.builder()
.containerId(containerId)
.codePathName(codePath)
.lastActivityTime(System.currentTimeMillis())
.build();
}
/**
* 复制文件到容器
*
* @param codeFile 代码文件
* @param containerId 容器 ID
*/
public void copyFileToContainer(String containerId, String codeFile) {
DOCKER_CLIENT.copyArchiveToContainerCmd(containerId)
.withHostResource(codeFile)
.withRemotePath("/box")
.exec();
}
public void cleanContainer(String containerId) {
DOCKER_CLIENT.stopContainerCmd(containerId).exec();
DOCKER_CLIENT.removeContainerCmd(containerId).exec();
}
}
CotainerInfo
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ContainerInfo {
private String containerId;
private String codePathName;
private long lastActivityTime;
/**
* 错误计数,默认为 0
*/
private int errorCount = 0;
}
ContainerPoolExecutor
@Slf4j
@Configuration
@Data
@ConfigurationProperties(prefix = "codesandbox.pool")
public class ContainerPoolExecutor {
private Integer corePoolSize = Runtime.getRuntime().availableProcessors() * 10;
private Integer maximumPoolSize = Runtime.getRuntime().availableProcessors() * 20;
private Integer waitQueueSize = 200;
private Integer keepAliveTime = 5;
private TimeUnit timeUnit = TimeUnit.SECONDS;
/**
* 容器池
* key: 容器 id
* value:上次活跃时间
*/
private BlockingQueue<ContainerInfo> containerPool;
/**
* 容器使用排队计数
*/
private AtomicInteger blockingThreadCount;
/**
* 可扩展的数量
*/
private AtomicInteger expandCount;
@Resource
private DockerDao dockerDao;
@PostConstruct
public void initPool() {
// 初始化容器池
this.containerPool = new LinkedBlockingQueue<>(maximumPoolSize);
this.blockingThreadCount = new AtomicInteger(0);
this.expandCount = new AtomicInteger(maximumPoolSize - corePoolSize);
// 初始化池中的数据
for (int i = 0; i < corePoolSize; i++) {
createNewPool();
}
// 定时清理过期的容器
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
scheduleExpirationCleanup(scheduledExecutorService);
}
private void createNewPool() {
// 写入文件
String userDir = System.getProperty("user.dir");
String codePathName = userDir + File.separator + "tempCode";
// 把用户的代码隔离存放
UUID uuid = UUID.randomUUID();
codePathName += File.separator + uuid;
// 判断代码目录是否存在,没有则新建
File codePath = new File(codePathName);
if (!codePath.exists()) {
boolean mkdir = codePath.mkdirs();
if (!mkdir) {
log.info("创建代码目录失败");
}
}
ContainerInfo containerInfo = dockerDao.startContainer(codePathName);
boolean result = containerPool.offer(containerInfo);
if (!result) {
log.error("current capacity: {}, the capacity limit is exceeded...", containerPool.size());
}
}
private boolean expandPool() {
log.info("超过指定数量,触发扩容");
if (expandCount.decrementAndGet() < 0) {
log.error("不能再扩容了");
return false;
}
log.info("扩容了");
createNewPool();
return true;
}
private ContainerInfo getContainer() throws InterruptedException {
if (containerPool.isEmpty()) {
// 增加阻塞线程计数
try {
if (blockingThreadCount.incrementAndGet() >= waitQueueSize && !expandPool()) {
log.error("扩容失败");
return null;
}
log.info("没有数据,等待数据,当前等待长度:{}", blockingThreadCount.get());
// 阻塞等待可用的数据
return containerPool.take();
} finally {
// 减少阻塞线程计数
log.info("减少阻塞线程计数");
blockingThreadCount.decrementAndGet();
}
}
return containerPool.take();
}
/**
* 清理过期容器
*/
private void cleanExpiredContainers() {
long currentTime = System.currentTimeMillis();
int needCleanCount = containerPool.size() - corePoolSize;
if (needCleanCount <= 0) {
return;
}
// 处理过期的容器
containerPool.stream().filter(containerInfo -> {
long lastActivityTime = containerInfo.getLastActivityTime();
lastActivityTime += timeUnit.toMillis(keepAliveTime);
return lastActivityTime < currentTime;
}).forEach(containerInfo -> {
boolean remove = containerPool.remove(containerInfo);
if (remove) {
String containerId = containerInfo.getContainerId();
expandCount.incrementAndGet();
if (StringUtils.isNotBlank(containerId)) {
dockerDao.cleanContainer(containerId);
}
}
});
log.info("当前容器大小: " + containerPool.size());
}
private void scheduleExpirationCleanup(ScheduledExecutorService scheduledExecutorService) {
scheduledExecutorService.scheduleAtFixedRate(() -> {
log.info("定时清理过期容器...");
cleanExpiredContainers();
// 每隔 20 秒执行一次清理操作
}, 0, 20, TimeUnit.SECONDS);
}
private void recordError(ContainerInfo containerInfo) {
if (containerInfo != null) {
containerInfo.setErrorCount(containerInfo.getErrorCount() + 1);
}
}
public ExecuteMessage run(Function<ContainerInfo, ExecuteMessage> function) {
ContainerInfo containerInfo = null;
try {
containerInfo = getContainer();
if (containerInfo == null) {
return ExecuteMessage.builder().success(false).message("不能处理了").build();
}
log.info("有数据,拿到了: {}", containerInfo);
ExecuteMessage executeMessage = function.apply(containerInfo);
if (!executeMessage.isSuccess()) {
recordError(containerInfo);
}
return executeMessage;
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
if (containerInfo != null) {
ContainerInfo finalContainerInfo = containerInfo;
dockerDao.execCmd(containerInfo.getContainerId(), new String[]{"rm", "-rf", "/box"});
CompletableFuture.runAsync(() -> {
try {
// 更新时间
log.info("操作完了,还回去");
String codePathName = finalContainerInfo.getCodePathName();
FileUtil.del(codePathName);
// 错误超过 3 次就不放回,重新运行一个
if (finalContainerInfo.getErrorCount() > 3) {
CompletableFuture.runAsync(() -> {
dockerDao.cleanContainer(finalContainerInfo.getContainerId());
this.createNewPool();
});
return;
}
finalContainerInfo.setLastActivityTime(System.currentTimeMillis());
containerPool.put(finalContainerInfo);
log.info("容器池还剩: {}", containerPool.size());
} catch (InterruptedException e) {
log.error("无法放入");
}
});
}
}
}
}
DockerSandbox
@Service
@Slf4j
public class DockerSandbox {
@Resource
private DockerDao dockerDao;
@Resource
private ContainerPoolExecutor containerPoolExecutor;
public ExecuteMessage execute(LanguageCmdEnum languageCmdEnum, String code) {
return containerPoolExecutor.run(containerInfo -> {
try {
String containerId = containerInfo.getContainerId();
String codePathName = containerInfo.getCodePathName();
String codeFileName = codePathName + File.separator + languageCmdEnum.getSaveFileName();
FileUtil.writeString(code, codeFileName, StandardCharsets.UTF_8);
dockerDao.copyFileToContainer(containerId, codeFileName);
// 编译代码
String[] compileCmd = languageCmdEnum.getCompileCmd();
ExecuteMessage executeMessage;
// 不为空则代表需要编译
if (compileCmd != null) {
executeMessage = dockerDao.execCmd(containerId, compileCmd);
log.info("compile complete...");
// 编译错误
if (!executeMessage.isSuccess()) {
return executeMessage;
}
}
String[] runCmd = languageCmdEnum.getRunCmd();
executeMessage = dockerDao.execCmd(containerId, runCmd);
log.info("run complete...");
return executeMessage;
} catch (Exception e) {
return ExecuteMessage.builder()
.success(false)
.errorMessage(e.getMessage())
.build();
}
});
}
}
优雅关闭
@Component
public class CleanContainerListener implements ApplicationListener<ContextClosedEvent> {
@Resource
private DockerDao dockerDao;
@Resource
private ContainerPoolExecutor containerPoolExecutor;
@Override
public void onApplicationEvent(ContextClosedEvent event) {
// 清理所有的容器以及残余文件
containerPoolExecutor
.getContainerPool()
.forEach(containerInfo -> {
FileUtil.del(containerInfo.getCodePathName());
dockerDao.cleanContainer(containerInfo.getContainerId());
});
System.out.println("container clean end...");
}
}