Fix ports for docker
This commit is contained in:
parent
4a82783b84
commit
ba322e084d
3 changed files with 218 additions and 20 deletions
|
|
@ -179,6 +179,48 @@ func setupContainers(config Config, clientTitle string, clientInfo *ClientInfo,
|
|||
}
|
||||
if !containerRunning {
|
||||
startContainer(containerFullName, composeFile, containerDir)
|
||||
} else {
|
||||
// Проверяем порты только если контейнер запущен недавно (менее 5 минут)
|
||||
// Это позволяет исправить ситуацию когда контейнер поднялся без портов
|
||||
// но не трогает давно работающие контейнеры
|
||||
startedAt, err := getContainerStartedAt(containerFullName)
|
||||
if err == nil && time.Since(startedAt) < 5*time.Minute {
|
||||
portsOK := true
|
||||
containerPorts := []PortMapping{}
|
||||
for _, res := range resources {
|
||||
ports, ok := resourcePortMap[res.L7ResourceID]
|
||||
if !ok || containerNum-1 >= len(ports) {
|
||||
continue
|
||||
}
|
||||
containerPorts = append(containerPorts, ports[containerNum-1])
|
||||
}
|
||||
for _, pm := range containerPorts {
|
||||
for _, dockerPort := range pm.HTTPPorts {
|
||||
if !checkPortListening("127.0.0.1", dockerPort) {
|
||||
log.Printf(" ⚠ Контейнер %s запущен недавно но порт %d не слушает — пересоздаю", containerFullName, dockerPort)
|
||||
portsOK = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !portsOK {
|
||||
break
|
||||
}
|
||||
for _, dockerPort := range pm.HTTPSPorts {
|
||||
if !checkPortListening("127.0.0.1", dockerPort) {
|
||||
log.Printf(" ⚠ Контейнер %s запущен недавно но порт %d не слушает — пересоздаю", containerFullName, dockerPort)
|
||||
portsOK = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !portsOK {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !portsOK {
|
||||
recreateContainer(containerFullName, composeFile, containerDir, clientTitle, resources, resourcePortMap, containerNum)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if containerChanges[containerNum] {
|
||||
log.Printf(" Конфигурация изменилась, проверяем и перезагружаем nginx в %s", containerFullName)
|
||||
|
|
@ -196,17 +238,80 @@ func setupContainers(config Config, clientTitle string, clientInfo *ClientInfo,
|
|||
}
|
||||
} else {
|
||||
log.Printf(" Запуск нового контейнера %s", containerName)
|
||||
cmd := exec.Command("docker-compose", "-f", composeFile, "up", "-d")
|
||||
cmd.Dir = containerDir
|
||||
output, _ := cmd.CombinedOutput()
|
||||
log.Printf(" → Вывод docker-compose: %s", string(output))
|
||||
time.Sleep(5 * time.Second)
|
||||
_, isRunning := checkContainerStatus(containerFullName)
|
||||
if !isRunning {
|
||||
log.Printf(" ⚠ Контейнер %s не запустился. Попробуйте вручную: cd %s && docker-compose up -d", containerFullName, containerDir)
|
||||
|
||||
// Собираем порты которые должен занять контейнер
|
||||
portsToCheck := []int{}
|
||||
for _, res := range resources {
|
||||
ports, ok := resourcePortMap[res.L7ResourceID]
|
||||
if !ok || containerNum-1 >= len(ports) {
|
||||
continue
|
||||
}
|
||||
pm := ports[containerNum-1]
|
||||
for _, dockerPort := range pm.HTTPPorts {
|
||||
portsToCheck = append(portsToCheck, dockerPort)
|
||||
}
|
||||
for _, dockerPort := range pm.HTTPSPorts {
|
||||
portsToCheck = append(portsToCheck, dockerPort)
|
||||
}
|
||||
}
|
||||
|
||||
// Ждём освобождения портов перед запуском
|
||||
if len(portsToCheck) > 0 {
|
||||
portsReady := false
|
||||
for attempt := 0; attempt < 30; attempt++ {
|
||||
busy := false
|
||||
for _, port := range portsToCheck {
|
||||
if isPortBusyByAnyProcess(port) {
|
||||
if attempt == 0 {
|
||||
log.Printf(" → Порт %d занят, ожидаем освобождения...", port)
|
||||
}
|
||||
busy = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !busy {
|
||||
portsReady = true
|
||||
if attempt > 0 {
|
||||
log.Printf(" ✓ Порты освободились (попытка %d)", attempt+1)
|
||||
}
|
||||
break
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
if !portsReady {
|
||||
log.Printf(" ⚠ Порты не освободились за 30 секунд, пробуем запустить всё равно")
|
||||
}
|
||||
}
|
||||
|
||||
const maxRetries = 3
|
||||
started := false
|
||||
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||
cmd := exec.Command("docker-compose", "-f", composeFile, "up", "-d")
|
||||
cmd.Dir = containerDir
|
||||
output, err := cmd.CombinedOutput()
|
||||
log.Printf(" → Вывод docker-compose: %s", string(output))
|
||||
if err != nil && strings.Contains(string(output), "address already in use") {
|
||||
log.Printf(" ⚠ Попытка %d/%d: порт занят, жду 3 секунды и повторяю...", attempt, maxRetries)
|
||||
time.Sleep(3 * time.Second)
|
||||
continue
|
||||
}
|
||||
time.Sleep(5 * time.Second)
|
||||
_, isRunning := checkContainerStatus(containerFullName)
|
||||
if !isRunning {
|
||||
log.Printf(" ⚠ Попытка %d/%d: контейнер %s не запустился", attempt, maxRetries, containerFullName)
|
||||
if attempt < maxRetries {
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
continue
|
||||
}
|
||||
log.Printf(" ✓ Контейнер %s успешно запущен (попытка %d)", containerName, attempt)
|
||||
started = true
|
||||
break
|
||||
}
|
||||
if !started {
|
||||
log.Printf(" ⚠ Контейнер %s не запустился после %d попыток. Попробуйте вручную: cd %s && docker-compose up -d", containerFullName, maxRetries, containerDir)
|
||||
continue
|
||||
}
|
||||
log.Printf(" ✓ Контейнер %s успешно запущен", containerName)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,63 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
// isAngieEstabOnPort проверяет есть ли активные ESTAB соединения Angie на данном порту
|
||||
func isAngieEstabOnPort(port int) bool {
|
||||
cmd := exec.Command("ss", "-tnap")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
portStr := fmt.Sprintf(":%d", port)
|
||||
for _, line := range strings.Split(string(output), "\n") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 4 {
|
||||
continue
|
||||
}
|
||||
if fields[0] != "ESTAB" {
|
||||
continue
|
||||
}
|
||||
localAddr := fields[3]
|
||||
if strings.HasSuffix(localAddr, portStr) && strings.Contains(line, "angie") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isPortBusyByAnyProcess проверяет занят ли порт процессом который мешает биндингу.
|
||||
// Проверяет только LISTEN сокеты и docker-proxy ESTAB — соединения Angie не блокируют биндинг Docker.
|
||||
func isPortBusyByAnyProcess(port int) bool {
|
||||
cmd := exec.Command("ss", "-tnap")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
portStr := fmt.Sprintf(":%d", port)
|
||||
for _, line := range strings.Split(string(output), "\n") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 4 {
|
||||
continue
|
||||
}
|
||||
localAddr := fields[3] // Local address:port
|
||||
if !strings.HasSuffix(localAddr, portStr) {
|
||||
continue
|
||||
}
|
||||
state := fields[0]
|
||||
// LISTEN — порт занят, нельзя биндить
|
||||
if state == "LISTEN" {
|
||||
return true
|
||||
}
|
||||
// ESTAB от docker-proxy — старый контейнер ещё не остановился
|
||||
if state == "ESTAB" && strings.Contains(line, "docker-proxy") {
|
||||
return true
|
||||
}
|
||||
// TIME-WAIT — Docker может биндить поверх TIME-WAIT через SO_REUSEADDR
|
||||
// поэтому не блокируем
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func recreateContainer(containerFullName, composeFile, containerDir, clientTitle string, resources []ResourceData, resourcePortMap map[int][]PortMapping, containerNum int) {
|
||||
log.Printf(" docker-compose.yml изменился, пересоздаём контейнер %s...", containerFullName)
|
||||
portsToFree := make([]int, 0)
|
||||
|
|
@ -58,20 +115,39 @@ func recreateContainer(containerFullName, composeFile, containerDir, clientTitle
|
|||
}
|
||||
}
|
||||
|
||||
// Reload Angie после остановки контейнера чтобы закрыть keepalive ESTAB соединения
|
||||
log.Printf(" → Reload Angie для закрытия keepalive соединений к остановленному контейнеру...")
|
||||
reloadAngie()
|
||||
|
||||
// Ждём пока Angie закроет ESTAB соединения к портам старого контейнера
|
||||
log.Printf(" → Ожидаем закрытия ESTAB соединений Angie на портах %v...", portsToFree)
|
||||
for attempt := 0; attempt < 30; attempt++ {
|
||||
angieEstab := false
|
||||
for _, port := range portsToFree {
|
||||
if isAngieEstabOnPort(port) {
|
||||
angieEstab = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !angieEstab {
|
||||
log.Printf(" ✓ Все ESTAB соединения Angie закрыты (попытка %d)", attempt+1)
|
||||
break
|
||||
}
|
||||
if attempt%5 == 0 && attempt > 0 {
|
||||
log.Printf(" → Angie ещё держит соединения (попытка %d/30)...", attempt+1)
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
if len(portsToFree) > 0 {
|
||||
log.Printf(" → Ожидаем освобождения %d портов...", len(portsToFree))
|
||||
allPortsFree := false
|
||||
for attempt := 0; attempt < 180; attempt++ { // Увеличено с 60 до 180 секунд
|
||||
usedPorts, err := getUsedPorts()
|
||||
if err != nil {
|
||||
log.Printf(" ⚠ Ошибка проверки портов: %v", err)
|
||||
time.Sleep(1 * time.Second)
|
||||
continue
|
||||
}
|
||||
for attempt := 0; attempt < 180; attempt++ {
|
||||
portsBusy := 0
|
||||
busyPortsList := make([]int, 0)
|
||||
for _, port := range portsToFree {
|
||||
if usedPorts[port] {
|
||||
if isPortBusyByAnyProcess(port) {
|
||||
portsBusy++
|
||||
busyPortsList = append(busyPortsList, port)
|
||||
}
|
||||
|
|
@ -82,12 +158,12 @@ func recreateContainer(containerFullName, composeFile, containerDir, clientTitle
|
|||
break
|
||||
}
|
||||
if attempt%5 == 0 && attempt > 0 {
|
||||
log.Printf(" → Ещё заняты %d портов: %v (попытка %d/180)", portsBusy, busyPortsList, attempt+1) // Обновлено с 60 на 180
|
||||
log.Printf(" → Ещё заняты %d портов: %v (попытка %d/180)", portsBusy, busyPortsList, attempt+1)
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
if !allPortsFree {
|
||||
log.Printf(" ⚠ Порты не освободились за 180 секунд!") // Обновлено с 60 на 180
|
||||
log.Printf(" ⚠ Порты не освободились за 180 секунд!")
|
||||
log.Printf(" → Проверьте вручную: sudo lsof -i :%d", portsToFree[0])
|
||||
return
|
||||
}
|
||||
|
|
@ -129,6 +205,20 @@ func startContainer(containerFullName, composeFile, containerDir string) {
|
|||
log.Printf(" ✓ Контейнер %s запущен", containerFullName)
|
||||
}
|
||||
|
||||
// getContainerStartedAt возвращает время запуска контейнера
|
||||
func getContainerStartedAt(containerName string) (time.Time, error) {
|
||||
cmd := exec.Command("docker", "inspect", "--format", "{{.State.StartedAt}}", containerName)
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("ошибка получения времени запуска: %w", err)
|
||||
}
|
||||
startedAt, err := time.Parse(time.RFC3339Nano, strings.TrimSpace(string(output)))
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("ошибка парсинга времени: %w", err)
|
||||
}
|
||||
return startedAt, nil
|
||||
}
|
||||
|
||||
func checkContainerStatus(containerName string) (exists bool, running bool) {
|
||||
cmd := exec.Command("docker", "ps", "-a", "--filter", fmt.Sprintf("name=^%s$", containerName), "--format", "{{.Names}}")
|
||||
output, err := cmd.Output()
|
||||
|
|
|
|||
5
ports.go
5
ports.go
|
|
@ -114,7 +114,8 @@ func (pa *PortAllocator) allocatePortsForResources(resourcesData []ResourceData,
|
|||
func getUsedPorts() (map[int]bool, error) {
|
||||
usedPorts := make(map[int]bool)
|
||||
|
||||
cmd := exec.Command("ss", "-tuln")
|
||||
// Используем -tuanl чтобы видеть все состояния: LISTEN, ESTAB, TIME-WAIT и др.
|
||||
cmd := exec.Command("ss", "-tuanl")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return usedPorts, fmt.Errorf("ошибка выполнения ss: %w", err)
|
||||
|
|
@ -123,10 +124,12 @@ func getUsedPorts() (map[int]bool, error) {
|
|||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
fields := strings.Fields(line)
|
||||
// Формат ss: Netid State Recv-Q Send-Q Local-Address:Port Peer-Address:Port
|
||||
if len(fields) < 5 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Local address в 5м поле (индекс 4)
|
||||
localAddr := fields[4]
|
||||
parts := strings.Split(localAddr, ":")
|
||||
if len(parts) < 2 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue