Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions client/model/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 2 additions & 3 deletions client/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
53 changes: 40 additions & 13 deletions service/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// 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()
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)

Expand All @@ -113,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)
Expand Down Expand Up @@ -164,12 +184,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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the diagnostic units.

HardwareRequirement.Memory and Disk are MiB, but this log prints GiB, which makes capacity failures look 1024x different from the actual request.

Suggested fix
-	log.DebugInfo("core selection process. req: memory=%d GiB, cpu=%d, disk=%d", req.Memory, req.CPU, req.Disk)
+	log.DebugInfo("core selection process. req: memory=%d MiB, cpu=%d, disk=%d MiB", req.Memory, req.CPU, req.Disk)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
log.DebugInfo("core selection process. req: memory=%d GiB, cpu=%d, disk=%d", req.Memory, req.CPU, req.Disk)
log.DebugInfo("core selection process. req: memory=%d MiB, cpu=%d, disk=%d MiB", req.Memory, req.CPU, req.Disk)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/vm.go` at line 174, The debug log in the core selection path is using
the wrong units for the request values. Update the log message in the core
selection flow that uses `log.DebugInfo` so it reports
`HardwareRequirement.Memory` and `Disk` in MiB, not GiB, while keeping `CPU`
unchanged. Make sure the message in the selection logic reflects the actual
units used by `req.Memory`, `req.CPU`, and `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)
Expand Down Expand Up @@ -241,6 +262,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)
}

Comment on lines +265 to +270

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Don’t remove the in-memory VM location before later fatal cleanup can fail.

ReleaseVM deletes VMLocation/AliveVM, but VMRepo.DeleteInstance can still return an error afterward. A retry then may fail at FindCoreByVmUUID, leaving stale DB state without a normal retry path. Keep enough in-memory deletion state until fatal cleanup succeeds, or make the later DB failure non-fatal with a reconciliation/retry path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/vm.go` around lines 247 - 252, The cleanup flow in ReleaseVM is
removing VMLocation/AliveVM too early, before VMRepo.DeleteInstance can still
fail afterward. Update the VM deletion path in ReleaseVM and its caller so the
in-memory VM mapping is only cleared after the fatal DB cleanup succeeds, or
otherwise make the later DeleteInstance failure non-fatal by preserving enough
state for a retry/reconciliation path. Use the ReleaseVM and
VMRepo.DeleteInstance flow to keep FindCoreByVmUUID retryable and avoid leaving
stale DB state with no recovery path.

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)
Expand Down
20 changes: 12 additions & 8 deletions startup/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,27 +214,31 @@ 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)
}
Comment on lines +217 to +225

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== startup/init.go around InitializeCoreData ==\n'
nl -ba startup/init.go | sed -n '140,280p'

printf '\n== client/model/vm.go around VcpuStatus ==\n'
nl -ba client/model/vm.go | sed -n '70,120p'

printf '\n== client/vm.go around GetCoreMachineCpuInfo ==\n'
nl -ba client/vm.go | sed -n '80,140p'

printf '\n== search for errgroup usage in startup/init.go ==\n'
rg -n "errgroup|Wait\(\)|Go\(" startup/init.go

Repository: easy-cloud-Knet/KWS_Control

Length of output: 254


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== startup/init.go around InitializeCoreData ==\n'
python3 - <<'PY'
from pathlib import Path
p = Path("startup/init.go")
for i, line in enumerate(p.read_text().splitlines(), 1):
    if 140 <= i <= 280:
        print(f"{i:4d}\t{line}")
PY

printf '\n== client/model/vm.go around VcpuStatus ==\n'
python3 - <<'PY'
from pathlib import Path
p = Path("client/model/vm.go")
for i, line in enumerate(p.read_text().splitlines(), 1):
    if 70 <= i <= 120:
        print(f"{i:4d}\t{line}")
PY

printf '\n== client/vm.go around GetCoreMachineCpuInfo ==\n'
python3 - <<'PY'
from pathlib import Path
p = Path("client/vm.go")
for i, line in enumerate(p.read_text().splitlines(), 1):
    if 80 <= i <= 140:
        print(f"{i:4d}\t{line}")
PY

printf '\n== search for errgroup usage in startup/init.go ==\n'
python3 - <<'PY'
from pathlib import Path
for i, line in enumerate(Path("startup/init.go").read_text().splitlines(), 1):
    if "errgroup" in line or "Wait()" in line or ".Go(" in line:
        print(f"{i:4d}\t{line}")
PY

Repository: easy-cloud-Knet/KWS_Control

Length of output: 9872


Single core CPU lookup still aborts startup for all cores. GetCoreMachineCpuInfo errors are returned from the errgroup goroutine, so g.Wait() fails InitializeCoreData instead of just marking that core IsAlive = false. If this is meant to be a per-core health update, keep the failure local and continue.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@startup/init.go` around lines 217 - 225, Handle GetCoreMachineCpuInfo
failures locally inside the per-core goroutine in InitializeCoreData instead of
returning the error to errgroup and aborting g.Wait(). In the block that sets
currentCore.IsAlive and calls coreClient.GetCoreMachineCpuInfo, keep marking the
specific core as not alive and then continue processing the other cores; only
return from the goroutine for truly fatal setup errors. Use the existing
InitializeCoreData, GetCoreMachineCpuInfo, and currentCore.IsAlive flow to keep
the failure scoped to that core.


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
currentCore.CoreInfoIdx.Disk = totalDiskMiB

currentCore.FreeDisk = freeDiskMiB
currentCore.FreeMemory = freeMemoryMiB
currentCore.FreeCPU = totalCpuCores
currentCore.FreeCPU = freeCpuCores

return nil
})
Expand Down
120 changes: 105 additions & 15 deletions structure/resource_manager.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package structure

import (
"math"
"slices"
"sync"
)
Expand Down Expand Up @@ -32,25 +33,81 @@ type HardwareRequirement struct {
Disk uint32 // MiB
}

// CoreSelectionResult는 SelectCore의 반환값으로 진단 정보를 내포
// CoreSelectionResult는 ReserveCore의 반환값으로 진단 정보를 내포
type CoreSelectionResult struct {
Core *Core
Index int
AliveCount int
TotalCores int
}

// SelectCore는 요청한 자원을 만족하는 첫 번째 살아있는 코어를 탐색.
// RLock 범위 내에서 코어 슬라이스를 순회 (네트워크 콜은 호출자가 락 밖에서 수행).
// 적합한 코어가 없으면 Core==nil로 반환하며, 진단 로그를 위한 카운트 정보를 함께 제공
func (rm *ResourceManager) SelectCore(req HardwareRequirement) CoreSelectionResult {
rm.mu.RLock()
defer rm.mu.RUnlock()
// 분산 기본 가중치
// 자원 우선순위 - 메모리 > 디스크 > 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
}

// 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,
TotalCores: len(rm.Cores),
}
bestScore := math.MaxFloat64

for i := range rm.Cores {
core := &rm.Cores[i]
Expand All @@ -59,30 +116,36 @@ 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
}
}

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()

if core.VMInfoIdx == nil {
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()
Expand All @@ -93,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()
Expand Down