first commit
This commit is contained in:
0
.gitignore
vendored
Normal file
0
.gitignore
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
6
.idea/misc.xml
generated
Normal file
6
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectRootManager">
|
||||||
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/cmii-uav-watchdog.iml" filepath="$PROJECT_DIR$/cmii-uav-watchdog.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
17
.idea/remote-targets.xml
generated
Normal file
17
.idea/remote-targets.xml
generated
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="RemoteTargetsManager">
|
||||||
|
<targets>
|
||||||
|
<target name="wdd-dev-35.70" type="ssh/sftp" uuid="b5c4b741-dd6b-4d58-819b-42e3f5b7c308">
|
||||||
|
<config>
|
||||||
|
<option name="projectRootOnTarget" value="/root/wdd/cmii-uav-watchdog" />
|
||||||
|
<option name="serverName" value="wdd-dev-35.70" />
|
||||||
|
<option name="useRsync" value="true" />
|
||||||
|
</config>
|
||||||
|
<ContributedStateBase type="BashSupportLanguageRuntime">
|
||||||
|
<config />
|
||||||
|
</ContributedStateBase>
|
||||||
|
</target>
|
||||||
|
</targets>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
27
cmii-uav-watchdog-agent/build_and_run_watchdog_agent.ps1
Normal file
27
cmii-uav-watchdog-agent/build_and_run_watchdog_agent.ps1
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
Write-Host "1. Building binary exec file..." -ForegroundColor Cyan
|
||||||
|
& "C:\Users\wdd\go\bin\gox.exe" -osarch="linux/amd64" -output "build/cmii-watchdog_linux_amd64"
|
||||||
|
|
||||||
|
# 执行远程ssh命令 ssh root@192.168.35.70 "rm /root/cmii-watchdog"
|
||||||
|
Write-Host "2. Cleaning old binary file..." -ForegroundColor Yellow
|
||||||
|
ssh root@192.168.35.70 "rm -f /root/wdd/cmii-watchdog/*"
|
||||||
|
|
||||||
|
Write-Host "3. Uploading binary exec..." -ForegroundColor Green
|
||||||
|
scp build/cmii-watchdog_linux_amd64 root@192.168.35.70:/root/wdd/cmii-watchdog/
|
||||||
|
scp java/* root@192.168.35.70:/root/wdd/cmii-watchdog/
|
||||||
|
|
||||||
|
Write-Host "4. Exec the command ..." -ForegroundColor Blue
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host ""
|
||||||
|
ssh root@192.168.35.70 "cd /root/wdd/cmii-watchdog/ && chmod +x cmii-watchdog_linux_amd64 && sudo bash build_cmii_watchdog.sh"
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
Write-Host "操作失败: $_" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
9
cmii-uav-watchdog-agent/cmii-uav-watchdog-agent.iml
Normal file
9
cmii-uav-watchdog-agent/cmii-uav-watchdog-agent.iml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="Go" enabled="true" />
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$/cmii-uav-watchdog-agent" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
3
cmii-uav-watchdog-agent/go.mod
Normal file
3
cmii-uav-watchdog-agent/go.mod
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module cmii-uav-watchdog-agent
|
||||||
|
|
||||||
|
go 1.23
|
||||||
15
cmii-uav-watchdog-agent/java-run/Dockerfile
Normal file
15
cmii-uav-watchdog-agent/java-run/Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
FROM alpine:3.18
|
||||||
|
|
||||||
|
RUN apk add --no-cache openjdk17
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY wdd-springboot-test-0.0.1-SNAPSHOT.jar /app/jarfile.jar
|
||||||
|
|
||||||
|
COPY cmii-watchdog_linux_amd64 /app/cmii-watchdog
|
||||||
|
|
||||||
|
CMD ["chmod", "+x", "/app/cmii-watchdog"]
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/cmii-watchdog", "-business-program-type", "java", "-business-program-path", "/app/jarfile.jar"]
|
||||||
16
cmii-uav-watchdog-agent/java-run/build_cmii_watchdog.sh
Normal file
16
cmii-uav-watchdog-agent/java-run/build_cmii_watchdog.sh
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
cd /root/wdd/cmii-watchdog
|
||||||
|
|
||||||
|
pwd
|
||||||
|
|
||||||
|
current_date=$(date +"%Y-%m-%d-%H-%M-%S")
|
||||||
|
|
||||||
|
|
||||||
|
docker build -t cmii-watchdog-java:${current_date} .
|
||||||
|
|
||||||
|
|
||||||
|
docker run -d -p 9000:8080 --name=cmii-watchdog-java-${current_date} cmii-watchdog-java:${current_date}
|
||||||
|
|
||||||
|
|
||||||
|
#docker run -d -p 9000:8080 cmii-watchdog-java:2025-02-25
|
||||||
Binary file not shown.
1
cmii-uav-watchdog-agent/main.go
Normal file
1
cmii-uav-watchdog-agent/main.go
Normal file
@@ -0,0 +1 @@
|
|||||||
|
package cmii_uav_watchdog_agent
|
||||||
9
cmii-uav-watchdog-center/cmii-uav-watchdog-center.iml
Normal file
9
cmii-uav-watchdog-center/cmii-uav-watchdog-center.iml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="Go" enabled="true" />
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$/cmii-uav-watchdog-center" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
3
cmii-uav-watchdog-center/go.mod
Normal file
3
cmii-uav-watchdog-center/go.mod
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module cmii-uav-watchdog-center
|
||||||
|
|
||||||
|
go 1.23
|
||||||
9
cmii-uav-watchdog-project.iml
Normal file
9
cmii-uav-watchdog-project.iml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="Go" enabled="true" />
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
27
cmii-uav-watchdog/build_and_run_watchdog.ps1
Normal file
27
cmii-uav-watchdog/build_and_run_watchdog.ps1
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
Write-Host "1. Building binary exec file..." -ForegroundColor Cyan
|
||||||
|
& "C:\Users\wdd\go\bin\gox.exe" -osarch="linux/amd64" -output "build/cmii-watchdog_linux_amd64"
|
||||||
|
|
||||||
|
# 执行远程ssh命令 ssh root@192.168.35.70 "rm /root/cmii-watchdog"
|
||||||
|
Write-Host "2. Cleaning old binary file..." -ForegroundColor Yellow
|
||||||
|
ssh root@192.168.35.70 "rm -f /root/wdd/cmii-watchdog/*"
|
||||||
|
|
||||||
|
Write-Host "3. Uploading binary exec..." -ForegroundColor Green
|
||||||
|
scp build/cmii-watchdog_linux_amd64 root@192.168.35.70:/root/wdd/cmii-watchdog/
|
||||||
|
scp java/* root@192.168.35.70:/root/wdd/cmii-watchdog/
|
||||||
|
|
||||||
|
Write-Host "4. Exec the command ..." -ForegroundColor Blue
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host ""
|
||||||
|
ssh root@192.168.35.70 "cd /root/wdd/cmii-watchdog/ && chmod +x cmii-watchdog_linux_amd64 && sudo bash build_cmii_watchdog.sh"
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
Write-Host "操作失败: $_" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
9
cmii-uav-watchdog/cmii-uav-watchdog.iml
Normal file
9
cmii-uav-watchdog/cmii-uav-watchdog.iml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="Go" enabled="true" />
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$/cmii-uav-watchdog" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
3
cmii-uav-watchdog/go.mod
Normal file
3
cmii-uav-watchdog/go.mod
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module cmii-uav-watchdog
|
||||||
|
|
||||||
|
go 1.23
|
||||||
121
cmii-uav-watchdog/main.go
Normal file
121
cmii-uav-watchdog/main.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
businessProgramType = flag.String("business-program-type", "", "Type of business program (java or python)")
|
||||||
|
businessProgramPath = flag.String("business-program-path", "", "Path to the business program file")
|
||||||
|
stopRequested bool
|
||||||
|
currentCmd *exec.Cmd
|
||||||
|
mu sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
func startBusinessProcess(programType, programPath string) *exec.Cmd {
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
switch programType {
|
||||||
|
case "java":
|
||||||
|
cmd = exec.Command("java", "-jar", programPath)
|
||||||
|
case "python":
|
||||||
|
cmd = exec.Command("python", programPath)
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unsupported business program type: %s", programType)
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// C:\Users\wdd\go\bin\gox.exe -osarch="linux/amd64" -output "build/watchdog"
|
||||||
|
func main() {
|
||||||
|
// 解析命令行参数
|
||||||
|
flag.Parse()
|
||||||
|
if *businessProgramType == "" || *businessProgramPath == "" {
|
||||||
|
log.Fatal("Missing required flags: -business-program-type and -business-program-path must be specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 信号处理
|
||||||
|
signalChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
for sig := range signalChan {
|
||||||
|
log.Printf("Received signal: %v", sig)
|
||||||
|
mu.Lock()
|
||||||
|
stopRequested = true
|
||||||
|
if currentCmd != nil && currentCmd.Process != nil {
|
||||||
|
// 发送 SIGTERM 给业务进程
|
||||||
|
if err := currentCmd.Process.Signal(syscall.SIGTERM); err != nil {
|
||||||
|
log.Printf("Failed to send SIGTERM to process: %v", err)
|
||||||
|
}
|
||||||
|
// 等待 10 秒后强制杀死进程
|
||||||
|
time.AfterFunc(10*time.Second, func() {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
if currentCmd != nil && currentCmd.Process != nil {
|
||||||
|
log.Println("Graceful shutdown timeout, sending SIGKILL")
|
||||||
|
currentCmd.Process.Kill()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 主循环
|
||||||
|
for {
|
||||||
|
mu.Lock()
|
||||||
|
if stopRequested {
|
||||||
|
mu.Unlock()
|
||||||
|
log.Println("Shutting down due to stop request")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
cmd := startBusinessProcess(*businessProgramType, *businessProgramPath)
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
// 启动业务进程
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
log.Printf("Failed to start business process: %v", err)
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
currentCmd = cmd
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
// 等待业务进程退出
|
||||||
|
err := cmd.Wait()
|
||||||
|
mu.Lock()
|
||||||
|
currentCmd = nil
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
// 程序异常退出,或者重启
|
||||||
|
// 定位分析问题
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Business process exited with error: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Println("Business process exited normally")
|
||||||
|
}
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
if stopRequested {
|
||||||
|
mu.Unlock()
|
||||||
|
log.Println("Shutting down due to stop request")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
// 等待 5 秒后重启
|
||||||
|
log.Println("Restarting business process in 5 seconds...")
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
0
设计结构/TOTP详细流程.md
Normal file
0
设计结构/TOTP详细流程.md
Normal file
0
设计结构/系统架构设计.md
Normal file
0
设计结构/系统架构设计.md
Normal file
0
设计结构/软件开发标准流程.md
Normal file
0
设计结构/软件开发标准流程.md
Normal file
427
设计结构/风险点-docker获取主机信息.md
Normal file
427
设计结构/风险点-docker获取主机信息.md
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
|
||||||
|
在一个docker镜像中, 用golang构建的二进制程序作为守护进程或者watchdog。然后还需要运行java -jar或者python main.py作为业务程序。守护进程可以控制业务程序的启停,请实现上述的功能
|
||||||
|
|
||||||
|
|
||||||
|
### 关键要点
|
||||||
|
- 研究表明,在 Docker 镜像中,可以使用 Go 语言编写的二进制程序作为守护进程或看门狗来管理业务程序。
|
||||||
|
- 它似乎很可能通过 Go 程序启动并监控 Java -jar 或 Python main.py 业务程序,并能在需要时重启。
|
||||||
|
- 证据倾向于认为,Go 程序应处理信号以优雅地停止业务程序,并确保 Docker 容器内的可靠运行。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 实现功能
|
||||||
|
|
||||||
|
#### 概述
|
||||||
|
要在 Docker 镜像中实现一个 Go 语言编写的守护进程或看门狗来控制 Java -jar 或 Python main.py 业务程序的启停,我们需要创建一个 Go 程序来管理业务程序的生命周期,并构建一个包含所有必要依赖的 Docker 镜像。
|
||||||
|
|
||||||
|
#### Go 程序设计
|
||||||
|
Go 程序将作为 Docker 容器的主进程,负责以下功能:
|
||||||
|
- **启动业务程序**:根据配置,启动 Java JAR 文件(如 `java -jar jarfile.jar`)或 Python 脚本(如 `python main.py`)。
|
||||||
|
- **监控和重启**:如果业务程序退出,等待一段时间(例如 5 秒)后重启,确保业务程序持续运行。
|
||||||
|
- **信号处理**:处理 SIGTERM 和 SIGINT 信号,发送 SIGTERM 给业务程序以优雅停止,并在 10 秒后发送 SIGKILL 如果业务程序未退出。
|
||||||
|
|
||||||
|
Go 程序通过命令行参数配置业务程序的类型(Java 或 Python)和路径,例如:
|
||||||
|
- 使用 `-business-program-type java -business-program-path /app/jarfile.jar` 启动 Java 程序。
|
||||||
|
- 使用 `-business-program-type python -business-program-path /app/main.py` 启动 Python 程序。
|
||||||
|
|
||||||
|
#### Docker 镜像构建
|
||||||
|
Docker 镜像需要包含:
|
||||||
|
- Go 二进制文件(watchdog)。
|
||||||
|
- 业务程序文件(JAR 文件或 Python 脚本)。
|
||||||
|
- 运行业务程序所需的运行时环境(如 Java 或 Python)。
|
||||||
|
|
||||||
|
例如:
|
||||||
|
- 对于 Java 业务程序,使用 Alpine Linux 基础镜像,安装 OpenJDK 11。
|
||||||
|
- 对于 Python 业务程序,使用官方 Python 3.10 slim 镜像。
|
||||||
|
|
||||||
|
镜像的工作目录设置为 `/app`,并通过 ENTRYPOINT 启动 Go 程序。
|
||||||
|
|
||||||
|
#### 示例配置
|
||||||
|
- **Java 业务程序的 Dockerfile**:
|
||||||
|
```dockerfile
|
||||||
|
FROM alpine:3.18
|
||||||
|
RUN apk add --no-cache openjdk11
|
||||||
|
WORKDIR /app
|
||||||
|
COPY jarfile.jar /app/jarfile.jar
|
||||||
|
COPY watchdog /usr/local/bin/watchdog
|
||||||
|
ENTRYPOINT ["watchdog", "-business-program-type", "java", "-business-program-path", "/app/jarfile.jar"]
|
||||||
|
```
|
||||||
|
- **Python 业务程序的 Dockerfile**:
|
||||||
|
```dockerfile
|
||||||
|
FROM python:3.10-slim
|
||||||
|
WORKDIR /app
|
||||||
|
COPY main.py /app/main.py
|
||||||
|
COPY watchdog /usr/local/bin/watchdog
|
||||||
|
ENTRYPOINT ["watchdog", "-business-program-type", "python", "-business-program-path", "/app/main.py"]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 意外细节
|
||||||
|
一个可能未预料到的细节是,Go 程序通过信号处理实现了优雅的关闭机制,允许业务程序在收到 SIGTERM 后有 10 秒时间优雅退出,这在生产环境中非常实用,尤其是在容器需要快速关闭时。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 详细报告
|
||||||
|
|
||||||
|
#### 背景与需求分析
|
||||||
|
用户要求在 Docker 镜像中实现一个 Go 语言编写的守护进程或看门狗,用于管理 Java -jar 或 Python main.py 业务程序的启停。关键需求包括:
|
||||||
|
- Go 程序作为看门狗,监控业务程序。
|
||||||
|
- 支持两种业务程序类型(Java 或 Python),并能控制其启动和停止。
|
||||||
|
- 确保在 Docker 容器内的可靠运行。
|
||||||
|
|
||||||
|
从 2025 年 2 月 24 日的当前技术角度来看,这种设计在容器化环境中非常常见,尤其是在需要高可用性和故障恢复的场景中。
|
||||||
|
|
||||||
|
#### Go 程序的实现细节
|
||||||
|
Go 程序的设计需要满足以下功能:
|
||||||
|
1. **配置灵活性**:
|
||||||
|
- 通过命令行参数 `-business-program-type` 和 `-business-program-path` 配置业务程序的类型和路径。
|
||||||
|
- 示例:`watchdog -business-program-type java -business-program-path /app/jarfile.jar`。
|
||||||
|
|
||||||
|
2. **业务程序启动**:
|
||||||
|
- 使用 `exec.Command` 启动业务程序。例如,对于 Java,命令为 `java -jar /app/jarfile.jar`;对于 Python,命令为 `python /app/main.py`。
|
||||||
|
- 将业务程序的 stdout 和 stderr 连接到容器的 stdout 和 stderr,确保日志可见。
|
||||||
|
|
||||||
|
3. **监控与重启**:
|
||||||
|
- Go 程序进入循环,启动业务程序后调用 `cmd.Wait()` 等待其退出。
|
||||||
|
- 如果业务程序退出(例如因错误),记录日志后等待 5 秒,然后重启,确保持续运行。
|
||||||
|
|
||||||
|
4. **信号处理与优雅关闭**:
|
||||||
|
- 使用 `os/signal` 包捕获 SIGTERM 和 SIGINT 信号。
|
||||||
|
- 信号处理 goroutine 在收到信号时:
|
||||||
|
- 设置 `stopRequested` 标志。
|
||||||
|
- 发送 SIGTERM 给当前运行的业务程序,允许其优雅退出。
|
||||||
|
- 启动一个定时器,10 秒后如果业务程序仍未退出,则发送 SIGKILL 强制终止。
|
||||||
|
- 主循环在每次 `cmd.Wait()` 返回后检查 `stopRequested`,如果为真则退出程序。
|
||||||
|
|
||||||
|
5. **并发与同步**:
|
||||||
|
- 使用 `sync.Mutex` 保护对当前业务进程 `currentCmd` 的访问,确保信号处理 goroutine 和主循环不会发生竞争条件。
|
||||||
|
- 在启动新进程时更新 `currentCmd`,在进程退出后清空。
|
||||||
|
|
||||||
|
以下是关键代码片段的伪代码表示:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func main() {
|
||||||
|
// 解析命令行参数
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// 信号处理
|
||||||
|
signalChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
for sig := range signalChan {
|
||||||
|
log.Print("Received signal:", sig)
|
||||||
|
stopRequested = true
|
||||||
|
mu.Lock()
|
||||||
|
if currentCmd != nil && currentCmd.Process != nil {
|
||||||
|
currentCmd.Process.Signal(syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
mu.Lock()
|
||||||
|
if currentCmd != nil && currentCmd.Process != nil {
|
||||||
|
currentCmd.Process.Kill()
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 主循环
|
||||||
|
for {
|
||||||
|
if stopRequested {
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
cmd := startBusinessProcess(businessProgramType, businessProgramPath)
|
||||||
|
cmd.Start()
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
mu.Lock()
|
||||||
|
currentCmd = cmd
|
||||||
|
mu.Unlock()
|
||||||
|
cmd.Wait()
|
||||||
|
mu.Lock()
|
||||||
|
currentCmd = nil
|
||||||
|
mu.Unlock()
|
||||||
|
if stopRequested {
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Docker 镜像的构建
|
||||||
|
Docker 镜像需要包含以下内容:
|
||||||
|
- **基础镜像**:根据业务程序类型选择合适的基础镜像。
|
||||||
|
- Java 程序:使用 `alpine:3.18`,安装 OpenJDK 11。
|
||||||
|
- Python 程序:使用 `python:3.10-slim`,包含 Python 运行时。
|
||||||
|
- **文件拷贝**:
|
||||||
|
- 将业务程序文件(JAR 文件或 Python 脚本)拷贝到 `/app` 目录。
|
||||||
|
- 将编译好的 Go 二进制文件 `watchdog` 拷贝到 `/usr/local/bin`。
|
||||||
|
- **工作目录与入口点**:
|
||||||
|
- 设置工作目录为 `/app`,确保路径一致。
|
||||||
|
- 通过 ENTRYPOINT 启动 Go 程序,传递必要的参数。
|
||||||
|
|
||||||
|
以下是两种情况的 Dockerfile 示例:
|
||||||
|
|
||||||
|
| 业务程序类型 | Dockerfile 内容 |
|
||||||
|
|--------------|--------------------------------------------------------------------------------|
|
||||||
|
| Java | FROM alpine:3.18<br>RUN apk add --no-cache openjdk11<br>WORKDIR /app<br>COPY jarfile.jar /app/jarfile.jar<br>COPY watchdog /usr/local/bin/watchdog<br>ENTRYPOINT ["watchdog", "-business-program-type", "java", "-business-program-path", "/app/jarfile.jar"] |
|
||||||
|
| Python | FROM python:3.10-slim<br>WORKDIR /app<br>COPY main.py /app/main.py<br>COPY watchdog /usr/local/bin/watchdog<br>ENTRYPOINT ["watchdog", "-business-program-type", "python", "-business-program-path", "/app/main.py"] |
|
||||||
|
|
||||||
|
#### 运行与测试
|
||||||
|
- 构建 Docker 镜像:
|
||||||
|
- 对于 Java:`docker build -t my-watchdog-java .`
|
||||||
|
- 对于 Python:`docker build -t my-watchdog-python .`
|
||||||
|
- 运行容器:
|
||||||
|
- `docker run -d my-watchdog-java`
|
||||||
|
- 容器会启动 Go 程序,管理 Java 业务程序,确保其持续运行。
|
||||||
|
- 停止容器:
|
||||||
|
- 发送 SIGTERM 信号(如 `docker stop <container_id>`),Go 程序会尝试优雅关闭业务程序。
|
||||||
|
|
||||||
|
#### 潜在问题与解决方案
|
||||||
|
1. **业务程序退出频繁**:
|
||||||
|
- 如果业务程序频繁退出,可能导致快速重启循环。可以通过增加重启延迟(当前为 5 秒)或添加重试计数来限制。
|
||||||
|
2. **权限问题**:
|
||||||
|
- 确保 Docker 镜像中的用户有权限运行 Java 或 Python。当前假设使用 root 用户运行,可根据需要调整为非 root 用户。
|
||||||
|
3. **日志管理**:
|
||||||
|
- 业务程序的输出通过 `cmd.Stdout` 和 `cmd.Stderr` 连接到容器日志,确保通过 `docker logs` 可查看。
|
||||||
|
|
||||||
|
#### 意外发现
|
||||||
|
一个可能未预料到的细节是,Go 程序的信号处理机制允许业务程序有 10 秒的优雅退出时间,这在生产环境中非常实用,尤其是在需要快速关闭容器时(如 Kubernetes 部署中的滚动更新)。这确保了业务程序有机会保存状态或完成当前任务。
|
||||||
|
|
||||||
|
#### 适用场景
|
||||||
|
这种设计适用于需要高可用性的场景,例如:
|
||||||
|
- Web 服务后端,确保业务程序始终运行。
|
||||||
|
- 批处理任务,自动重启失败的任务。
|
||||||
|
- 容器化微服务架构中,作为监控和恢复机制。
|
||||||
|
|
||||||
|
#### 结论
|
||||||
|
通过上述实现,我们可以在 Docker 镜像中创建一个可靠的看门狗系统,管理 Java 或 Python 业务程序的生命周期,确保其稳定运行,并支持优雅关闭。这满足了用户对功能的需求,并提供了灵活的配置和扩展能力。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 关键引文
|
||||||
|
- [Go 官方文档 exec 包](https://pkg.go.dev/os/exec)
|
||||||
|
- [Docker 官方文档 Dockerfile 参考](https://docs.docker.com/engine/reference/builder/)
|
||||||
|
- [Alpine Linux 官方镜像](https://hub.docker.com/_/alpine)
|
||||||
|
- [Python 官方镜像](https://hub.docker.com/_/python)
|
||||||
|
|
||||||
|
|
||||||
|
### 问题分析
|
||||||
|
提供的 `main()` 函数代码虽然功能逻辑清晰,但在实际运行中可能存在以下问题:
|
||||||
|
|
||||||
|
1. **未定义的变量**:
|
||||||
|
- `stopRequested`、`mu`、`currentCmd`、`businessProgramType` 和 `businessProgramPath` 在代码片段中未定义。
|
||||||
|
- 这些变量需要显式声明并初始化。
|
||||||
|
|
||||||
|
2. **信号处理中的潜在竞争条件**:
|
||||||
|
- 信号处理 goroutine 中的嵌套 goroutine(用于延迟 SIGKILL)未绑定到特定的 `cmd`,可能导致在快速信号处理时,错误的进程被杀死。
|
||||||
|
- `mu.Lock()` 和 `mu.Unlock()` 的使用虽然保护了 `currentCmd`,但嵌套 goroutine 的延迟逻辑可能在锁释放后访问已更改的 `currentCmd`。
|
||||||
|
|
||||||
|
3. **错误处理不足**:
|
||||||
|
- `cmd.Start()` 和 `cmd.Wait()` 的返回值未检查,可能导致启动失败或进程退出状态未被正确处理。
|
||||||
|
- 如果业务程序启动失败,程序会陷入无限循环而未记录问题。
|
||||||
|
|
||||||
|
4. **信号处理逻辑的优雅性**:
|
||||||
|
- 当前信号处理逻辑在收到信号后直接设置 `stopRequested` 并尝试杀死进程,但没有等待进程实际退出,导致主循环可能提前退出。
|
||||||
|
|
||||||
|
5. **资源泄露风险**:
|
||||||
|
- 如果 `cmd.Start()` 成功但进程未正确清理,可能会导致文件描述符或其他资源未释放。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 修改后的代码
|
||||||
|
以下是修复上述问题的完整版代码,增加了必要的变量定义、错误处理和改进的信号处理逻辑:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
businessProgramType = flag.String("business-program-type", "", "Type of business program (java or python)")
|
||||||
|
businessProgramPath = flag.String("business-program-path", "", "Path to the business program file")
|
||||||
|
stopRequested bool
|
||||||
|
currentCmd *exec.Cmd
|
||||||
|
mu sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
func startBusinessProcess(programType, programPath string) *exec.Cmd {
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
switch programType {
|
||||||
|
case "java":
|
||||||
|
cmd = exec.Command("java", "-jar", programPath)
|
||||||
|
case "python":
|
||||||
|
cmd = exec.Command("python", programPath)
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unsupported business program type: %s", programType)
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// 解析命令行参数
|
||||||
|
flag.Parse()
|
||||||
|
if *businessProgramType == "" || *businessProgramPath == "" {
|
||||||
|
log.Fatal("Missing required flags: -business-program-type and -business-program-path must be specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 信号处理
|
||||||
|
signalChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
for sig := range signalChan {
|
||||||
|
log.Printf("Received signal: %v", sig)
|
||||||
|
mu.Lock()
|
||||||
|
stopRequested = true
|
||||||
|
if currentCmd != nil && currentCmd.Process != nil {
|
||||||
|
// 发送 SIGTERM 给业务进程
|
||||||
|
if err := currentCmd.Process.Signal(syscall.SIGTERM); err != nil {
|
||||||
|
log.Printf("Failed to send SIGTERM to process: %v", err)
|
||||||
|
}
|
||||||
|
// 等待 10 秒后强制杀死进程
|
||||||
|
time.AfterFunc(10*time.Second, func() {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
if currentCmd != nil && currentCmd.Process != nil {
|
||||||
|
log.Println("Graceful shutdown timeout, sending SIGKILL")
|
||||||
|
currentCmd.Process.Kill()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 主循环
|
||||||
|
for {
|
||||||
|
mu.Lock()
|
||||||
|
if stopRequested {
|
||||||
|
mu.Unlock()
|
||||||
|
log.Println("Shutting down due to stop request")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
cmd := startBusinessProcess(*businessProgramType, *businessProgramPath)
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
// 启动业务进程
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
log.Printf("Failed to start business process: %v", err)
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
currentCmd = cmd
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
// 等待业务进程退出
|
||||||
|
err := cmd.Wait()
|
||||||
|
mu.Lock()
|
||||||
|
currentCmd = nil
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Business process exited with error: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Println("Business process exited normally")
|
||||||
|
}
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
if stopRequested {
|
||||||
|
mu.Unlock()
|
||||||
|
log.Println("Shutting down due to stop request")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
// 等待 5 秒后重启
|
||||||
|
log.Println("Restarting business process in 5 seconds...")
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 修改的主要改进
|
||||||
|
|
||||||
|
1. **变量定义和初始化**:
|
||||||
|
- 定义了全局变量 `businessProgramType` 和 `businessProgramPath` 作为命令行参数。
|
||||||
|
- 使用 `flag` 包解析参数,并添加了参数校验。
|
||||||
|
- 定义了 `stopRequested`、`currentCmd` 和 `mu` 作为全局变量。
|
||||||
|
|
||||||
|
2. **改进信号处理**:
|
||||||
|
- 信号处理 goroutine 使用 `time.AfterFunc` 替代嵌套 goroutine,确保延迟 SIGKILL 的逻辑更可靠。
|
||||||
|
- 在发送 SIGTERM 时检查错误并记录。
|
||||||
|
- 主循环和信号处理逻辑通过 `mu` 同步访问 `currentCmd`,避免竞争条件。
|
||||||
|
|
||||||
|
3. **错误处理**:
|
||||||
|
- 检查 `cmd.Start()` 的返回值,并在失败时记录错误并重试。
|
||||||
|
- 检查 `cmd.Wait()` 的返回值,区分正常退出和错误退出。
|
||||||
|
- 如果命令行参数缺失,程序会优雅退出并提示错误。
|
||||||
|
|
||||||
|
4. **优雅关闭**:
|
||||||
|
- 主循环在每次迭代开始和 `cmd.Wait()` 后检查 `stopRequested`,确保在收到信号后能及时退出。
|
||||||
|
- 信号处理允许业务进程有 10 秒的优雅退出时间,超时后强制杀死。
|
||||||
|
|
||||||
|
5. **日志记录**:
|
||||||
|
- 添加了详细的日志输出,方便调试和监控,例如进程启动失败、退出状态和重启信息。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 使用方法
|
||||||
|
|
||||||
|
1. **编译程序**:
|
||||||
|
```bash
|
||||||
|
go build -o watchdog main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **构建 Docker 镜像**:
|
||||||
|
- 对于 Java 业务程序:
|
||||||
|
```dockerfile
|
||||||
|
FROM alpine:3.18
|
||||||
|
RUN apk add --no-cache openjdk11
|
||||||
|
WORKDIR /app
|
||||||
|
COPY jarfile.jar /app/jarfile.jar
|
||||||
|
COPY watchdog /usr/local/bin/watchdog
|
||||||
|
ENTRYPOINT ["watchdog", "-business-program-type", "java", "-business-program-path", "/app/jarfile.jar"]
|
||||||
|
```
|
||||||
|
- 对于 Python 业务程序:
|
||||||
|
```dockerfile
|
||||||
|
FROM python:3.10-slim
|
||||||
|
WORKDIR /app
|
||||||
|
COPY main.py /app/main.py
|
||||||
|
COPY watchdog /usr/local/bin/watchdog
|
||||||
|
ENTRYPOINT ["watchdog", "-business-program-type", "python", "-business-program-path", "/app/main.py"]
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **运行容器**:
|
||||||
|
```bash
|
||||||
|
docker build -t my-watchdog-java .
|
||||||
|
docker run -d my-watchdog-java
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 测试验证
|
||||||
|
- **正常运行**:启动容器后,业务进程会运行并输出日志到容器 stdout/stderr。
|
||||||
|
- **异常退出**:如果业务进程退出(如 Python 脚本抛出异常),5 秒后会自动重启。
|
||||||
|
- **优雅关闭**:运行 `docker stop <container_id>`,业务进程会收到 SIGTERM,10 秒后若未退出则被强制杀死。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 结论
|
||||||
|
修改后的代码解决了原始代码的问题,增加了健壮性、错误处理和优雅关闭机制,适合在 Docker 容器中作为看门狗运行,管理 Java 或 Python 业务程序的启停。
|
||||||
427
设计结构/风险点-golang作为启动程序spy.md
Normal file
427
设计结构/风险点-golang作为启动程序spy.md
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
|
||||||
|
在一个docker镜像中, 用golang构建的二进制程序作为守护进程或者watchdog。然后还需要运行java -jar或者python main.py作为业务程序。守护进程可以控制业务程序的启停,请实现上述的功能
|
||||||
|
|
||||||
|
|
||||||
|
### 关键要点
|
||||||
|
- 研究表明,在 Docker 镜像中,可以使用 Go 语言编写的二进制程序作为守护进程或看门狗来管理业务程序。
|
||||||
|
- 它似乎很可能通过 Go 程序启动并监控 Java -jar 或 Python main.py 业务程序,并能在需要时重启。
|
||||||
|
- 证据倾向于认为,Go 程序应处理信号以优雅地停止业务程序,并确保 Docker 容器内的可靠运行。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 实现功能
|
||||||
|
|
||||||
|
#### 概述
|
||||||
|
要在 Docker 镜像中实现一个 Go 语言编写的守护进程或看门狗来控制 Java -jar 或 Python main.py 业务程序的启停,我们需要创建一个 Go 程序来管理业务程序的生命周期,并构建一个包含所有必要依赖的 Docker 镜像。
|
||||||
|
|
||||||
|
#### Go 程序设计
|
||||||
|
Go 程序将作为 Docker 容器的主进程,负责以下功能:
|
||||||
|
- **启动业务程序**:根据配置,启动 Java JAR 文件(如 `java -jar jarfile.jar`)或 Python 脚本(如 `python main.py`)。
|
||||||
|
- **监控和重启**:如果业务程序退出,等待一段时间(例如 5 秒)后重启,确保业务程序持续运行。
|
||||||
|
- **信号处理**:处理 SIGTERM 和 SIGINT 信号,发送 SIGTERM 给业务程序以优雅停止,并在 10 秒后发送 SIGKILL 如果业务程序未退出。
|
||||||
|
|
||||||
|
Go 程序通过命令行参数配置业务程序的类型(Java 或 Python)和路径,例如:
|
||||||
|
- 使用 `-business-program-type java -business-program-path /app/jarfile.jar` 启动 Java 程序。
|
||||||
|
- 使用 `-business-program-type python -business-program-path /app/main.py` 启动 Python 程序。
|
||||||
|
|
||||||
|
#### Docker 镜像构建
|
||||||
|
Docker 镜像需要包含:
|
||||||
|
- Go 二进制文件(watchdog)。
|
||||||
|
- 业务程序文件(JAR 文件或 Python 脚本)。
|
||||||
|
- 运行业务程序所需的运行时环境(如 Java 或 Python)。
|
||||||
|
|
||||||
|
例如:
|
||||||
|
- 对于 Java 业务程序,使用 Alpine Linux 基础镜像,安装 OpenJDK 11。
|
||||||
|
- 对于 Python 业务程序,使用官方 Python 3.10 slim 镜像。
|
||||||
|
|
||||||
|
镜像的工作目录设置为 `/app`,并通过 ENTRYPOINT 启动 Go 程序。
|
||||||
|
|
||||||
|
#### 示例配置
|
||||||
|
- **Java 业务程序的 Dockerfile**:
|
||||||
|
```dockerfile
|
||||||
|
FROM alpine:3.18
|
||||||
|
RUN apk add --no-cache openjdk11
|
||||||
|
WORKDIR /app
|
||||||
|
COPY jarfile.jar /app/jarfile.jar
|
||||||
|
COPY watchdog /usr/local/bin/watchdog
|
||||||
|
ENTRYPOINT ["watchdog", "-business-program-type", "java", "-business-program-path", "/app/jarfile.jar"]
|
||||||
|
```
|
||||||
|
- **Python 业务程序的 Dockerfile**:
|
||||||
|
```dockerfile
|
||||||
|
FROM python:3.10-slim
|
||||||
|
WORKDIR /app
|
||||||
|
COPY main.py /app/main.py
|
||||||
|
COPY watchdog /usr/local/bin/watchdog
|
||||||
|
ENTRYPOINT ["watchdog", "-business-program-type", "python", "-business-program-path", "/app/main.py"]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 意外细节
|
||||||
|
一个可能未预料到的细节是,Go 程序通过信号处理实现了优雅的关闭机制,允许业务程序在收到 SIGTERM 后有 10 秒时间优雅退出,这在生产环境中非常实用,尤其是在容器需要快速关闭时。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 详细报告
|
||||||
|
|
||||||
|
#### 背景与需求分析
|
||||||
|
用户要求在 Docker 镜像中实现一个 Go 语言编写的守护进程或看门狗,用于管理 Java -jar 或 Python main.py 业务程序的启停。关键需求包括:
|
||||||
|
- Go 程序作为看门狗,监控业务程序。
|
||||||
|
- 支持两种业务程序类型(Java 或 Python),并能控制其启动和停止。
|
||||||
|
- 确保在 Docker 容器内的可靠运行。
|
||||||
|
|
||||||
|
从 2025 年 2 月 24 日的当前技术角度来看,这种设计在容器化环境中非常常见,尤其是在需要高可用性和故障恢复的场景中。
|
||||||
|
|
||||||
|
#### Go 程序的实现细节
|
||||||
|
Go 程序的设计需要满足以下功能:
|
||||||
|
1. **配置灵活性**:
|
||||||
|
- 通过命令行参数 `-business-program-type` 和 `-business-program-path` 配置业务程序的类型和路径。
|
||||||
|
- 示例:`watchdog -business-program-type java -business-program-path /app/jarfile.jar`。
|
||||||
|
|
||||||
|
2. **业务程序启动**:
|
||||||
|
- 使用 `exec.Command` 启动业务程序。例如,对于 Java,命令为 `java -jar /app/jarfile.jar`;对于 Python,命令为 `python /app/main.py`。
|
||||||
|
- 将业务程序的 stdout 和 stderr 连接到容器的 stdout 和 stderr,确保日志可见。
|
||||||
|
|
||||||
|
3. **监控与重启**:
|
||||||
|
- Go 程序进入循环,启动业务程序后调用 `cmd.Wait()` 等待其退出。
|
||||||
|
- 如果业务程序退出(例如因错误),记录日志后等待 5 秒,然后重启,确保持续运行。
|
||||||
|
|
||||||
|
4. **信号处理与优雅关闭**:
|
||||||
|
- 使用 `os/signal` 包捕获 SIGTERM 和 SIGINT 信号。
|
||||||
|
- 信号处理 goroutine 在收到信号时:
|
||||||
|
- 设置 `stopRequested` 标志。
|
||||||
|
- 发送 SIGTERM 给当前运行的业务程序,允许其优雅退出。
|
||||||
|
- 启动一个定时器,10 秒后如果业务程序仍未退出,则发送 SIGKILL 强制终止。
|
||||||
|
- 主循环在每次 `cmd.Wait()` 返回后检查 `stopRequested`,如果为真则退出程序。
|
||||||
|
|
||||||
|
5. **并发与同步**:
|
||||||
|
- 使用 `sync.Mutex` 保护对当前业务进程 `currentCmd` 的访问,确保信号处理 goroutine 和主循环不会发生竞争条件。
|
||||||
|
- 在启动新进程时更新 `currentCmd`,在进程退出后清空。
|
||||||
|
|
||||||
|
以下是关键代码片段的伪代码表示:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func main() {
|
||||||
|
// 解析命令行参数
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// 信号处理
|
||||||
|
signalChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
for sig := range signalChan {
|
||||||
|
log.Print("Received signal:", sig)
|
||||||
|
stopRequested = true
|
||||||
|
mu.Lock()
|
||||||
|
if currentCmd != nil && currentCmd.Process != nil {
|
||||||
|
currentCmd.Process.Signal(syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
mu.Lock()
|
||||||
|
if currentCmd != nil && currentCmd.Process != nil {
|
||||||
|
currentCmd.Process.Kill()
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 主循环
|
||||||
|
for {
|
||||||
|
if stopRequested {
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
cmd := startBusinessProcess(businessProgramType, businessProgramPath)
|
||||||
|
cmd.Start()
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
mu.Lock()
|
||||||
|
currentCmd = cmd
|
||||||
|
mu.Unlock()
|
||||||
|
cmd.Wait()
|
||||||
|
mu.Lock()
|
||||||
|
currentCmd = nil
|
||||||
|
mu.Unlock()
|
||||||
|
if stopRequested {
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Docker 镜像的构建
|
||||||
|
Docker 镜像需要包含以下内容:
|
||||||
|
- **基础镜像**:根据业务程序类型选择合适的基础镜像。
|
||||||
|
- Java 程序:使用 `alpine:3.18`,安装 OpenJDK 11。
|
||||||
|
- Python 程序:使用 `python:3.10-slim`,包含 Python 运行时。
|
||||||
|
- **文件拷贝**:
|
||||||
|
- 将业务程序文件(JAR 文件或 Python 脚本)拷贝到 `/app` 目录。
|
||||||
|
- 将编译好的 Go 二进制文件 `watchdog` 拷贝到 `/usr/local/bin`。
|
||||||
|
- **工作目录与入口点**:
|
||||||
|
- 设置工作目录为 `/app`,确保路径一致。
|
||||||
|
- 通过 ENTRYPOINT 启动 Go 程序,传递必要的参数。
|
||||||
|
|
||||||
|
以下是两种情况的 Dockerfile 示例:
|
||||||
|
|
||||||
|
| 业务程序类型 | Dockerfile 内容 |
|
||||||
|
|--------------|--------------------------------------------------------------------------------|
|
||||||
|
| Java | FROM alpine:3.18<br>RUN apk add --no-cache openjdk11<br>WORKDIR /app<br>COPY jarfile.jar /app/jarfile.jar<br>COPY watchdog /usr/local/bin/watchdog<br>ENTRYPOINT ["watchdog", "-business-program-type", "java", "-business-program-path", "/app/jarfile.jar"] |
|
||||||
|
| Python | FROM python:3.10-slim<br>WORKDIR /app<br>COPY main.py /app/main.py<br>COPY watchdog /usr/local/bin/watchdog<br>ENTRYPOINT ["watchdog", "-business-program-type", "python", "-business-program-path", "/app/main.py"] |
|
||||||
|
|
||||||
|
#### 运行与测试
|
||||||
|
- 构建 Docker 镜像:
|
||||||
|
- 对于 Java:`docker build -t my-watchdog-java .`
|
||||||
|
- 对于 Python:`docker build -t my-watchdog-python .`
|
||||||
|
- 运行容器:
|
||||||
|
- `docker run -d my-watchdog-java`
|
||||||
|
- 容器会启动 Go 程序,管理 Java 业务程序,确保其持续运行。
|
||||||
|
- 停止容器:
|
||||||
|
- 发送 SIGTERM 信号(如 `docker stop <container_id>`),Go 程序会尝试优雅关闭业务程序。
|
||||||
|
|
||||||
|
#### 潜在问题与解决方案
|
||||||
|
1. **业务程序退出频繁**:
|
||||||
|
- 如果业务程序频繁退出,可能导致快速重启循环。可以通过增加重启延迟(当前为 5 秒)或添加重试计数来限制。
|
||||||
|
2. **权限问题**:
|
||||||
|
- 确保 Docker 镜像中的用户有权限运行 Java 或 Python。当前假设使用 root 用户运行,可根据需要调整为非 root 用户。
|
||||||
|
3. **日志管理**:
|
||||||
|
- 业务程序的输出通过 `cmd.Stdout` 和 `cmd.Stderr` 连接到容器日志,确保通过 `docker logs` 可查看。
|
||||||
|
|
||||||
|
#### 意外发现
|
||||||
|
一个可能未预料到的细节是,Go 程序的信号处理机制允许业务程序有 10 秒的优雅退出时间,这在生产环境中非常实用,尤其是在需要快速关闭容器时(如 Kubernetes 部署中的滚动更新)。这确保了业务程序有机会保存状态或完成当前任务。
|
||||||
|
|
||||||
|
#### 适用场景
|
||||||
|
这种设计适用于需要高可用性的场景,例如:
|
||||||
|
- Web 服务后端,确保业务程序始终运行。
|
||||||
|
- 批处理任务,自动重启失败的任务。
|
||||||
|
- 容器化微服务架构中,作为监控和恢复机制。
|
||||||
|
|
||||||
|
#### 结论
|
||||||
|
通过上述实现,我们可以在 Docker 镜像中创建一个可靠的看门狗系统,管理 Java 或 Python 业务程序的生命周期,确保其稳定运行,并支持优雅关闭。这满足了用户对功能的需求,并提供了灵活的配置和扩展能力。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 关键引文
|
||||||
|
- [Go 官方文档 exec 包](https://pkg.go.dev/os/exec)
|
||||||
|
- [Docker 官方文档 Dockerfile 参考](https://docs.docker.com/engine/reference/builder/)
|
||||||
|
- [Alpine Linux 官方镜像](https://hub.docker.com/_/alpine)
|
||||||
|
- [Python 官方镜像](https://hub.docker.com/_/python)
|
||||||
|
|
||||||
|
|
||||||
|
### 问题分析
|
||||||
|
提供的 `main()` 函数代码虽然功能逻辑清晰,但在实际运行中可能存在以下问题:
|
||||||
|
|
||||||
|
1. **未定义的变量**:
|
||||||
|
- `stopRequested`、`mu`、`currentCmd`、`businessProgramType` 和 `businessProgramPath` 在代码片段中未定义。
|
||||||
|
- 这些变量需要显式声明并初始化。
|
||||||
|
|
||||||
|
2. **信号处理中的潜在竞争条件**:
|
||||||
|
- 信号处理 goroutine 中的嵌套 goroutine(用于延迟 SIGKILL)未绑定到特定的 `cmd`,可能导致在快速信号处理时,错误的进程被杀死。
|
||||||
|
- `mu.Lock()` 和 `mu.Unlock()` 的使用虽然保护了 `currentCmd`,但嵌套 goroutine 的延迟逻辑可能在锁释放后访问已更改的 `currentCmd`。
|
||||||
|
|
||||||
|
3. **错误处理不足**:
|
||||||
|
- `cmd.Start()` 和 `cmd.Wait()` 的返回值未检查,可能导致启动失败或进程退出状态未被正确处理。
|
||||||
|
- 如果业务程序启动失败,程序会陷入无限循环而未记录问题。
|
||||||
|
|
||||||
|
4. **信号处理逻辑的优雅性**:
|
||||||
|
- 当前信号处理逻辑在收到信号后直接设置 `stopRequested` 并尝试杀死进程,但没有等待进程实际退出,导致主循环可能提前退出。
|
||||||
|
|
||||||
|
5. **资源泄露风险**:
|
||||||
|
- 如果 `cmd.Start()` 成功但进程未正确清理,可能会导致文件描述符或其他资源未释放。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 修改后的代码
|
||||||
|
以下是修复上述问题的完整版代码,增加了必要的变量定义、错误处理和改进的信号处理逻辑:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
businessProgramType = flag.String("business-program-type", "", "Type of business program (java or python)")
|
||||||
|
businessProgramPath = flag.String("business-program-path", "", "Path to the business program file")
|
||||||
|
stopRequested bool
|
||||||
|
currentCmd *exec.Cmd
|
||||||
|
mu sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
func startBusinessProcess(programType, programPath string) *exec.Cmd {
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
switch programType {
|
||||||
|
case "java":
|
||||||
|
cmd = exec.Command("java", "-jar", programPath)
|
||||||
|
case "python":
|
||||||
|
cmd = exec.Command("python", programPath)
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unsupported business program type: %s", programType)
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// 解析命令行参数
|
||||||
|
flag.Parse()
|
||||||
|
if *businessProgramType == "" || *businessProgramPath == "" {
|
||||||
|
log.Fatal("Missing required flags: -business-program-type and -business-program-path must be specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 信号处理
|
||||||
|
signalChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
for sig := range signalChan {
|
||||||
|
log.Printf("Received signal: %v", sig)
|
||||||
|
mu.Lock()
|
||||||
|
stopRequested = true
|
||||||
|
if currentCmd != nil && currentCmd.Process != nil {
|
||||||
|
// 发送 SIGTERM 给业务进程
|
||||||
|
if err := currentCmd.Process.Signal(syscall.SIGTERM); err != nil {
|
||||||
|
log.Printf("Failed to send SIGTERM to process: %v", err)
|
||||||
|
}
|
||||||
|
// 等待 10 秒后强制杀死进程
|
||||||
|
time.AfterFunc(10*time.Second, func() {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
if currentCmd != nil && currentCmd.Process != nil {
|
||||||
|
log.Println("Graceful shutdown timeout, sending SIGKILL")
|
||||||
|
currentCmd.Process.Kill()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 主循环
|
||||||
|
for {
|
||||||
|
mu.Lock()
|
||||||
|
if stopRequested {
|
||||||
|
mu.Unlock()
|
||||||
|
log.Println("Shutting down due to stop request")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
cmd := startBusinessProcess(*businessProgramType, *businessProgramPath)
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
// 启动业务进程
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
log.Printf("Failed to start business process: %v", err)
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
currentCmd = cmd
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
// 等待业务进程退出
|
||||||
|
err := cmd.Wait()
|
||||||
|
mu.Lock()
|
||||||
|
currentCmd = nil
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Business process exited with error: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Println("Business process exited normally")
|
||||||
|
}
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
if stopRequested {
|
||||||
|
mu.Unlock()
|
||||||
|
log.Println("Shutting down due to stop request")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
// 等待 5 秒后重启
|
||||||
|
log.Println("Restarting business process in 5 seconds...")
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 修改的主要改进
|
||||||
|
|
||||||
|
1. **变量定义和初始化**:
|
||||||
|
- 定义了全局变量 `businessProgramType` 和 `businessProgramPath` 作为命令行参数。
|
||||||
|
- 使用 `flag` 包解析参数,并添加了参数校验。
|
||||||
|
- 定义了 `stopRequested`、`currentCmd` 和 `mu` 作为全局变量。
|
||||||
|
|
||||||
|
2. **改进信号处理**:
|
||||||
|
- 信号处理 goroutine 使用 `time.AfterFunc` 替代嵌套 goroutine,确保延迟 SIGKILL 的逻辑更可靠。
|
||||||
|
- 在发送 SIGTERM 时检查错误并记录。
|
||||||
|
- 主循环和信号处理逻辑通过 `mu` 同步访问 `currentCmd`,避免竞争条件。
|
||||||
|
|
||||||
|
3. **错误处理**:
|
||||||
|
- 检查 `cmd.Start()` 的返回值,并在失败时记录错误并重试。
|
||||||
|
- 检查 `cmd.Wait()` 的返回值,区分正常退出和错误退出。
|
||||||
|
- 如果命令行参数缺失,程序会优雅退出并提示错误。
|
||||||
|
|
||||||
|
4. **优雅关闭**:
|
||||||
|
- 主循环在每次迭代开始和 `cmd.Wait()` 后检查 `stopRequested`,确保在收到信号后能及时退出。
|
||||||
|
- 信号处理允许业务进程有 10 秒的优雅退出时间,超时后强制杀死。
|
||||||
|
|
||||||
|
5. **日志记录**:
|
||||||
|
- 添加了详细的日志输出,方便调试和监控,例如进程启动失败、退出状态和重启信息。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 使用方法
|
||||||
|
|
||||||
|
1. **编译程序**:
|
||||||
|
```bash
|
||||||
|
go build -o watchdog main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **构建 Docker 镜像**:
|
||||||
|
- 对于 Java 业务程序:
|
||||||
|
```dockerfile
|
||||||
|
FROM alpine:3.18
|
||||||
|
RUN apk add --no-cache openjdk11
|
||||||
|
WORKDIR /app
|
||||||
|
COPY jarfile.jar /app/jarfile.jar
|
||||||
|
COPY watchdog /usr/local/bin/watchdog
|
||||||
|
ENTRYPOINT ["watchdog", "-business-program-type", "java", "-business-program-path", "/app/jarfile.jar"]
|
||||||
|
```
|
||||||
|
- 对于 Python 业务程序:
|
||||||
|
```dockerfile
|
||||||
|
FROM python:3.10-slim
|
||||||
|
WORKDIR /app
|
||||||
|
COPY main.py /app/main.py
|
||||||
|
COPY watchdog /usr/local/bin/watchdog
|
||||||
|
ENTRYPOINT ["watchdog", "-business-program-type", "python", "-business-program-path", "/app/main.py"]
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **运行容器**:
|
||||||
|
```bash
|
||||||
|
docker build -t my-watchdog-java .
|
||||||
|
docker run -d my-watchdog-java
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 测试验证
|
||||||
|
- **正常运行**:启动容器后,业务进程会运行并输出日志到容器 stdout/stderr。
|
||||||
|
- **异常退出**:如果业务进程退出(如 Python 脚本抛出异常),5 秒后会自动重启。
|
||||||
|
- **优雅关闭**:运行 `docker stop <container_id>`,业务进程会收到 SIGTERM,10 秒后若未退出则被强制杀死。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 结论
|
||||||
|
修改后的代码解决了原始代码的问题,增加了健壮性、错误处理和优雅关闭机制,适合在 Docker 容器中作为看门狗运行,管理 Java 或 Python 业务程序的启停。
|
||||||
Reference in New Issue
Block a user