From 7a338bc8912d839b795790c604c80736591ecf61 Mon Sep 17 00:00:00 2001 From: ga111o Date: Tue, 7 Jul 2026 14:55:29 +0900 Subject: [PATCH 1/5] refactor: enhance resource allocation logic in ResourceManager - Introduced loadScore function to calculate core load based on resource utilization. - Added utilRatio and saturationPenalty functions for improved resource management. - Updated SelectCore to select the core with the lowest load score that meets resource requirements. --- structure/resource_manager.go | 65 ++++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/structure/resource_manager.go b/structure/resource_manager.go index 553ee31..6085eca 100644 --- a/structure/resource_manager.go +++ b/structure/resource_manager.go @@ -1,6 +1,7 @@ package structure import ( + "math" "slices" "sync" ) @@ -40,8 +41,60 @@ type CoreSelectionResult struct { TotalCores int } -// SelectCore는 요청한 자원을 만족하는 첫 번째 살아있는 코어를 탐색. -// RLock 범위 내에서 코어 슬라이스를 순회 (네트워크 콜은 호출자가 락 밖에서 수행). +// 분산 기본 가중치 +// 자원 우선순위 - 메모리 > 디스크 > CPU. +// 현재는 하드코딩, 나중에 백엔드? 아니면 별도의 관리자 페이지 등에서 데이터페칭하는 방식도 괜찮을 거 같음. +const ( + loadWeightMemory = 0.6 + loadWeightDisk = 0.3 + loadWeightCPU = 0.1 + loadBalanceGain = 0.5 // 자원 간 사용률 분산 페널티 강도 + saturationBase = 1.01 // 비선형 페널티 기준, saturationPenalty()에서 사용. +) + +// utilRatio는 VM 배치 후 해당 자원의 사용률 [0,1]을 반환. +// total==0(정보 없음/불량 코어)이면 1(포화 취급 → 회피). +func utilRatio(total, free, req uint32) float64 { + if total == 0 { + return 1 + } + u := float64(total-free+req) / float64(total) // 호출부에서 free>=req, free<=total 보장 + if u < 0 { + u = 0 + } + if u > 1 { + u = 1 + } + return u +} + +// 100%에 가까울수록 부하 가중치 급격하게 커짐. +// U=0→~0.99, 0.8→~4.8, 0.9→~9.1, 0.99→50, 1.0→100. +func saturationPenalty(u float64) float64 { + return 1.0 / (saturationBase - u) +} + +// 코어의 부하 점수 반환 (낮을수록 우수). +// 가중 포화 페널티(자원 우선순위 + 포화 회피) + 다차원 균형(분산) 페널티로 구성. +func loadScore(c *Core, req HardwareRequirement) float64 { + uMem := utilRatio(c.CoreInfoIdx.Memory, c.FreeMemory, req.Memory) + uDisk := utilRatio(c.CoreInfoIdx.Disk, c.FreeDisk, req.Disk) + uCPU := utilRatio(c.CoreInfoIdx.Cpu, c.FreeCPU, req.CPU) + + weighted := loadWeightMemory*saturationPenalty(uMem) + + loadWeightDisk*saturationPenalty(uDisk) + + loadWeightCPU*saturationPenalty(uCPU) + + // 다차원 균형: 자원 간 사용률 분산이 클수록(고립 자원) 페널티 → stranded resource 방지 + mean := (uMem + uDisk + uCPU) / 3 + variance := ((uMem-mean)*(uMem-mean) + + (uDisk-mean)*(uDisk-mean) + + (uCPU-mean)*(uCPU-mean)) / 3 + + return weighted + loadBalanceGain*variance +} + +// SelectCore는 요청 자원을 만족하는 살아있는 코어 중 loadScore가 가장 낮은 코어를 선택. // 적합한 코어가 없으면 Core==nil로 반환하며, 진단 로그를 위한 카운트 정보를 함께 제공 func (rm *ResourceManager) SelectCore(req HardwareRequirement) CoreSelectionResult { rm.mu.RLock() @@ -51,6 +104,7 @@ func (rm *ResourceManager) SelectCore(req HardwareRequirement) CoreSelectionResu Index: -1, TotalCores: len(rm.Cores), } + bestScore := math.MaxFloat64 for i := range rm.Cores { core := &rm.Cores[i] @@ -59,10 +113,13 @@ func (rm *ResourceManager) SelectCore(req HardwareRequirement) CoreSelectionResu } result.AliveCount++ - if core.FreeMemory >= req.Memory && core.FreeCPU >= req.CPU && core.FreeDisk >= req.Disk { + if core.FreeMemory < req.Memory || core.FreeCPU < req.CPU || core.FreeDisk < req.Disk { + continue + } + if s := loadScore(core, req); s < bestScore { + bestScore = s result.Core = core result.Index = i - return result } } return result From d59ac2262f13a8580b1ff46921aaa398ab207e16 Mon Sep 17 00:00:00 2001 From: ga111o Date: Tue, 7 Jul 2026 15:10:26 +0900 Subject: [PATCH 2/5] refactor: add VcpuStatus struct and update CPU info retrieval in InitializeCoreData --- client/model/vm.go | 14 +++++++++++--- client/vm.go | 5 ++--- startup/init.go | 20 ++++++++++++-------- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/client/model/vm.go b/client/model/vm.go index 6cf1811..6890d08 100644 --- a/client/model/vm.go +++ b/client/model/vm.go @@ -83,10 +83,18 @@ type StartVMResponse struct { type DeleteVMResponse struct { } +type VcpuStatus struct { + Total uint32 `json:"total"` // 호스트 전체 vcpu 수 + Allocated uint32 `json:"allocated"` // 현재 vm들에 할당된 vcpu 수 + Sleeping uint32 `json:"sleeping"` // 할당되었지만 유휴 상태인 vcpu 수 + Idle uint32 `json:"idle"` // 할당되지 않은 vcpu 수 (total - allocated) +} + type CoreMachineCpuInfoResponse struct { - System float64 `json:"system_time"` - Idle float64 `json:"idle_time"` - Usage float64 `json:"usage_percent"` + System float64 `json:"system_time"` + Idle float64 `json:"idle_time"` + Usage float64 `json:"usage_percent"` + VcpuStatus *VcpuStatus `json:"vcpu_status,omitempty"` // MemInfo/DiskInfo 조회 시엔 null } type CoreMachineMemoryInfoResponse struct { diff --git a/client/vm.go b/client/vm.go index 923f6fb..d52c7c5 100644 --- a/client/vm.go +++ b/client/vm.go @@ -90,9 +90,8 @@ func (c *CoreClient) DeleteVM(context context.Context, req model.DeleteVMRequest return response, nil } -// 현재 미사용 중 -// 코어 vcpu 갯수 가져오는 함수 -// 코어에 문의 해봐야함 옛날에 구현 안됬다고 해서 컨트롤에서 9999로 하드코딩 했던거 같음 +// vcpu_status.total = 전체 vcpu 수 +// vcpu_status.idle = 미할당 vCPU 수. func (c *CoreClient) GetCoreMachineCpuInfo(context context.Context) (*model.CoreMachineCpuInfoResponse, error) { var response model.CoreResponse[model.CoreMachineCpuInfoResponse] err := c.doRequest(context, http.MethodGet, "/getStatusHost", model.GetMachineStatusRequest{ diff --git a/startup/init.go b/startup/init.go index 3a2cdc1..9159192 100644 --- a/startup/init.go +++ b/startup/init.go @@ -214,19 +214,23 @@ func InitializeCoreData(configPath string) (structure.ControlContext, error) { currentCore.IsAlive = false return fmt.Errorf("failed to get Disk info for core %s:%d: %w", currentCore.IP, currentCore.Port, err) } + cpuResp, err := coreClient.GetCoreMachineCpuInfo(ctx) + if err != nil { + currentCore.IsAlive = false + return fmt.Errorf("failed to get CPU info for core %s:%d: %w", currentCore.IP, currentCore.Port, err) + } + if cpuResp == nil || cpuResp.VcpuStatus == nil { + currentCore.IsAlive = false + return fmt.Errorf("core %s:%d returned no vcpu_status in CPU info", currentCore.IP, currentCore.Port) + } totalMemoryMiB := uint32(memResp.Total * 1024) totalDiskMiB := uint32(diskResp.Total * 1024) freeMemoryMiB := uint32(memResp.Available * 1024) freeDiskMiB := uint32(diskResp.Free * 1024) - var totalCpuCores uint32 - if currentCore.CoreInfoIdx.Cpu > 0 { - totalCpuCores = currentCore.CoreInfoIdx.Cpu - } else { - log.DebugInfo("currentCore.CoreInfoIdx.Cpu: %d", currentCore.CoreInfoIdx.Cpu) - totalCpuCores = 9999 // 음 코어를 현재 반환받지 못하는- - } + totalCpuCores := cpuResp.VcpuStatus.Total + freeCpuCores := cpuResp.VcpuStatus.Idle currentCore.CoreInfoIdx.Cpu = totalCpuCores currentCore.CoreInfoIdx.Memory = totalMemoryMiB @@ -234,7 +238,7 @@ func InitializeCoreData(configPath string) (structure.ControlContext, error) { currentCore.FreeDisk = freeDiskMiB currentCore.FreeMemory = freeMemoryMiB - currentCore.FreeCPU = totalCpuCores + currentCore.FreeCPU = freeCpuCores return nil }) From 1097519b5a9af5a59788e5f3ceea3f8614eea2d2 Mon Sep 17 00:00:00 2001 From: ga111o Date: Tue, 7 Jul 2026 15:17:04 +0900 Subject: [PATCH 3/5] refactor: consolidate core selection and resource reservation logic in CreateVM --- service/vm.go | 29 ++++++++++++++++------------- structure/resource_manager.go | 30 ++++++++++++++++++------------ 2 files changed, 34 insertions(+), 25 deletions(-) diff --git a/service/vm.go b/service/vm.go index 7904d3c..4968c89 100644 --- a/service/vm.go +++ b/service/vm.go @@ -26,22 +26,26 @@ func CreateVM(input CreateVMInput, contextStruct *vms.ControlContext, rdb *redis log.Info("func CreateVM() memory=%d GiB, cpu=%d, disk=%d GiB", hwReq.Memory, hwReq.CPU, hwReq.Disk, true) - // 1) 코어 선택 - selectedCore, selectedCoreIndex, err := selectCoreOrFail(contextStruct, hwReq) + // 1) 코어 선택 + 자원 예약 (원자적: 선택과 Free* 차감이 한 락 안에서 → TOCTOU 제거) + selectedCore, selectedCoreIndex, err := reserveCoreOrFail(contextStruct, hwReq) if err != nil { return err } + // 예약 직후 롤백 등록. 이후 모든 실패 경로는 cleanup.run()으로 예약을 되돌려야 함 + cleanup := &cleanupChain{} + cleanup.push(func() { + contextStruct.Resources.DeallocateResources(selectedCore, uuid, hwReq) + }) + // 2) SSH 키 생성 privateKeyPEM, publicKeyOpenSSH, err := internalssh.GenerateSSHKey() if err != nil { + cleanup.run() log.Error("GenerateSshKey() failed: %v", err, true) return fmt.Errorf("CreateVM: failed to generate SSH key: %w", err) } - // 단계별 롤백 등록을 위한 chain - cleanup := &cleanupChain{} - //사용자 수 확인 if len(input.Users) == 0 { cleanup.run() @@ -53,6 +57,7 @@ func CreateVM(input CreateVMInput, contextStruct *vms.ControlContext, rdb *redis cmsResp, isNewSubnet, err := allocateCmsSubnet(contextStruct, input.SubnetType, uuid) if err != nil { //TODO AllocateCmsSubnet cleanup logic implement + cleanup.run() log.Error("CreateVM: failed to allocate CMS subnet: %v", err, true) return fmt.Errorf("CreateVM: failed to allocate CMS subnet: %w", err) } @@ -83,11 +88,8 @@ func CreateVM(input CreateVMInput, contextStruct *vms.ControlContext, rdb *redis IP_VM: cmsResp.IP, } - // 5) 코어 자원 할당 (VMInfoIdx + Free* 원자적 갱신) - contextStruct.Resources.AllocateResources(selectedCore, uuid, newVM, hwReq) - cleanup.push(func() { - contextStruct.Resources.DeallocateResources(selectedCore, uuid, hwReq) - }) + // 5) 코어에 VM 메타데이터 부착 (Free* 예약은 ReserveCore에서 완료, 롤백은 위 cleanup이 담당) + contextStruct.Resources.AttachVMInfo(selectedCore, uuid, newVM) log.DebugInfo("core %s updated: FreeMemory=%d, FreeCPU=%d, FreeDisk=%d", selectedCore.IP, selectedCore.FreeMemory, selectedCore.FreeCPU, selectedCore.FreeDisk) @@ -164,12 +166,13 @@ func buildCoreCreateVMRequest(input CreateVMInput, cmsResp *client.CmsNewInstanc } } -// selectCoreOrFail은 코어 선택 + 실패 시 진단 로그 출력 캡슐화를 진행 -func selectCoreOrFail(contextStruct *vms.ControlContext, req vms.HardwareRequirement) (*vms.Core, int, error) { +// reserveCoreOrFail은 코어 선택+예약 + 실패 시 진단 로그 출력 캡슐화를 진행. +// 성공 반환 시 해당 코어의 Free* 자원은 이미 예약(차감)된 상태 - 호출자는 실패 경로에서 롤백해야 함. +func reserveCoreOrFail(contextStruct *vms.ControlContext, req vms.HardwareRequirement) (*vms.Core, int, error) { log := util.GetLogger() log.DebugInfo("core selection process. req: memory=%d GiB, cpu=%d, disk=%d", req.Memory, req.CPU, req.Disk) - result := contextStruct.Resources.SelectCore(req) + result := contextStruct.Resources.ReserveCore(req) if result.Core != nil { log.DebugInfo("core found: %s (idx=%d)", result.Core.IP, result.Index) diff --git a/structure/resource_manager.go b/structure/resource_manager.go index 6085eca..c94c35f 100644 --- a/structure/resource_manager.go +++ b/structure/resource_manager.go @@ -33,7 +33,7 @@ type HardwareRequirement struct { Disk uint32 // MiB } -// CoreSelectionResult는 SelectCore의 반환값으로 진단 정보를 내포 +// CoreSelectionResult는 ReserveCore의 반환값으로 진단 정보를 내포 type CoreSelectionResult struct { Core *Core Index int @@ -94,11 +94,14 @@ func loadScore(c *Core, req HardwareRequirement) float64 { return weighted + loadBalanceGain*variance } -// SelectCore는 요청 자원을 만족하는 살아있는 코어 중 loadScore가 가장 낮은 코어를 선택. -// 적합한 코어가 없으면 Core==nil로 반환하며, 진단 로그를 위한 카운트 정보를 함께 제공 -func (rm *ResourceManager) SelectCore(req HardwareRequirement) CoreSelectionResult { - rm.mu.RLock() - defer rm.mu.RUnlock() +// ReserveCore는 요청 자원을 만족하는 살아있는 코어 중 loadScore가 가장 낮은 코어를 +// 선택하고, 같은 Lock 구간 안에서 즉시 Free* 자원을 차감(예약)한다. +// fit 검사와 차감이 원자적이라 동시 호출 간 오버서브스크립션(Free* 언더플로)이 발생하지 않는다. +// 네트워크 콜은 호출자가 락 밖에서 수행. 적합한 코어가 없으면 Core==nil(예약 없음)로 반환하며, +// 진단 로그를 위한 카운트 정보를 함께 제공 +func (rm *ResourceManager) ReserveCore(req HardwareRequirement) CoreSelectionResult { + rm.mu.Lock() + defer rm.mu.Unlock() result := CoreSelectionResult{ Index: -1, @@ -122,11 +125,17 @@ func (rm *ResourceManager) SelectCore(req HardwareRequirement) CoreSelectionResu result.Index = i } } + + if result.Core != nil { // 락 안에서 즉시 예약 -> TOCTOU 제거 + result.Core.FreeMemory -= req.Memory + result.Core.FreeCPU -= req.CPU + result.Core.FreeDisk -= req.Disk + } return result } -// AllocateResources는 코어의 VMInfoIdx 맵에 VM을 등록하고 Free* 필드를 차감 -func (rm *ResourceManager) AllocateResources(core *Core, uuid UUID, vm *VMInfo, req HardwareRequirement) { +// AttachVMInfo는 예약된 코어에 VM 메타데이터를 등록 (Free* 차감은 ReserveCore에서 이미 완료). +func (rm *ResourceManager) AttachVMInfo(core *Core, uuid UUID, vm *VMInfo) { rm.mu.Lock() defer rm.mu.Unlock() @@ -134,12 +143,9 @@ func (rm *ResourceManager) AllocateResources(core *Core, uuid UUID, vm *VMInfo, core.VMInfoIdx = make(map[UUID]*VMInfo) } core.VMInfoIdx[uuid] = vm - core.FreeMemory -= req.Memory - core.FreeCPU -= req.CPU - core.FreeDisk -= req.Disk } -// DeallocateResources는 AllocateResources의 역연산 +// DeallocateResources는 ReserveCore(예약) + AttachVMInfo(부착)의 역연산 (롤백) func (rm *ResourceManager) DeallocateResources(core *Core, uuid UUID, req HardwareRequirement) { rm.mu.Lock() defer rm.mu.Unlock() From c2f311c71f8d2ebe36c15987c37f63446c3858ab Mon Sep 17 00:00:00 2001 From: ga111o Date: Tue, 7 Jul 2026 15:33:15 +0900 Subject: [PATCH 4/5] refactor: implement resource release logic for deleted VMs in ResourceManager --- service/vm.go | 6 ++++++ structure/resource_manager.go | 27 +++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/service/vm.go b/service/vm.go index 4968c89..6a05270 100644 --- a/service/vm.go +++ b/service/vm.go @@ -244,6 +244,12 @@ func DeleteVM(uuid vms.UUID, contextStruct *vms.ControlContext, rdb *redis.Clien return fmt.Errorf("DeleteVM: failed to delete VM %s on core %s: %w", uuid, core.IP, err) } + // Core 삭제 성공 -> 인메모리 자원 회수 (Free* 복구 + VMInfoIdx/VMLocation/AliveVM 정리) + if contextStruct.Resources.ReleaseVM(core, uuid) { + log.DebugInfo("released in-memory resources for VM %s on core %s: FreeMemory=%d, FreeCPU=%d, FreeDisk=%d", + uuid, core.IP, core.FreeMemory, core.FreeCPU, core.FreeDisk) + } + cmsClient := client.NewCmsClient() if err := DeleteCmsSubnet(cmsClient, contextStruct, uuid); err != nil { log.Error("DeleteVM: failed to delete CMS subnet for VM %s: %v", uuid, err) diff --git a/structure/resource_manager.go b/structure/resource_manager.go index c94c35f..ef4475c 100644 --- a/structure/resource_manager.go +++ b/structure/resource_manager.go @@ -156,6 +156,33 @@ func (rm *ResourceManager) DeallocateResources(core *Core, uuid UUID, req Hardwa core.FreeDisk += req.Disk } +// ReleaseVM은 VM이 점유하던 인메모리 자원을 모두 해제 (Reserve+Attach+Register의 역연산). +// Free* 복구 + VMInfoIdx/VMLocation/AliveVM 정리를 한 Lock 안에서 원자적으로 수행. +// 멱등: 이미 해제된 VM에 다시 호출해도 Free*가 이중 복구되지 않는다(VMInfoIdx 존재 여부로 가드). +// 해제할 자원을 찾았으면 true 반환. +func (rm *ResourceManager) ReleaseVM(core *Core, uuid UUID) bool { + rm.mu.Lock() + defer rm.mu.Unlock() + + released := false + if vm, ok := core.VMInfoIdx[uuid]; ok { + core.FreeMemory += vm.Memory + core.FreeCPU += vm.Cpu + core.FreeDisk += vm.Disk + delete(core.VMInfoIdx, uuid) + released = true + } + delete(rm.VMLocation, uuid) + + for i, v := range rm.AliveVM { + if v.UUID == uuid { + rm.AliveVM = slices.Delete(rm.AliveVM, i, i+1) + break + } + } + return released +} + // RegisterVM은 VMLocation 맵과 AliveVM 슬라이스에 VM을 동시에 등록 func (rm *ResourceManager) RegisterVM(uuid UUID, core *Core, vm *VMInfo) { rm.mu.Lock() From 996fab5978e3276db19a8b0eefa6abc2475b6391 Mon Sep 17 00:00:00 2001 From: ga111o Date: Tue, 14 Jul 2026 14:41:13 +0900 Subject: [PATCH 5/5] refactor: add cleanup logic for VM deletion on core after creation failure --- service/vm.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/service/vm.go b/service/vm.go index 6a05270..4b961fe 100644 --- a/service/vm.go +++ b/service/vm.go @@ -115,6 +115,24 @@ func CreateVM(input CreateVMInput, contextStruct *vms.ControlContext, rdb *redis return fmt.Errorf("CreateVM: failed to create VM on core %s: %w", selectedCore.IP, err) } + // 코어 생성 성공 이후의 실패 발생 시 처리 + cleanup.push(func() { + log.Info("clean up: requesting VM deletion on core %s for %s", selectedCore.IP, uuid, true) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + if _, err := coreClient.DeleteVM(ctx, model.DeleteVMRequest{ + UUID: uuid, + Type: model.HardDelete, + }); err != nil { + // 삭제 실패 자체가 롤백을 중단시키지 않도록. + log.Error("rollback: failed to delete VM %s on core %s (manual cleanup or core GC required): %v", + uuid, selectedCore.IP, err, true) + } + if err := RemoveVMInfoFromRedis(ctx, rdb, uuid); err != nil { + log.Warn("rollback: failed to remove vm info from redis: %v", err, true) + } + }) + // 8) DB에 인스턴스 정보 영속화 if err := contextStruct.VMRepo.AddInstance(newVM, selectedCoreIndex); err != nil { log.Error("Error database instance insertion failed: %v", err, true)