Skip to content

feat: filter + cosine-similarity core allocation with Redis accounting#62

Open
ga111o wants to merge 1 commit into
easy-cloud-Knet:feat/core-vm-allocationfrom
ga111o:refactor/core-vm-allocation
Open

feat: filter + cosine-similarity core allocation with Redis accounting#62
ga111o wants to merge 1 commit into
easy-cloud-Knet:feat/core-vm-allocationfrom
ga111o:refactor/core-vm-allocation

Conversation

@ga111o
Copy link
Copy Markdown
Member

@ga111o ga111o commented May 22, 2026

아직 검토 없이 gpt딸깍한 결과 + 실행 가능함만 확인한 결과입니다
검토는 적당히 시간 날 때.. 아마 시험 끝나고 하겠습니다
그래도 큰 문제가 생기지 않는 한 전반적인 흐름은 유지되지 않을까 싶습니다

Summary

VM 코어 할당 로직을 First-Fit → 하드 필터(가용성 AND) + 코사인 유사도 가중치로 교체하고, 자원 회계를 Redis 기반으로 정비합니다.

  • 코어 CPU 총량을 /getStatusHost(vcpu_status.total)로 실측·캐시 (기존 FreeCPU=9999 하드코딩 제거 → CPU가 실질 제약으로 동작)
  • 코어별 할당량을 Redis HASH(core:{ip}:{port}:alloc) 로 집계 (기존 Redis 6379 인스턴스 공용)
  • 오버커밋·여유분(reserve) 파라미터를 config.yaml/env로 도입
  • 주기적 헬스체크 goroutine 추가로 코어 가용성/용량을 갱신 (IsAlive stale 문제 해소)
  • CreateVM/DeleteVM의 예약·롤백·정리 경로 보강

Motivation

현행 코어 선택(service/vm.go의 First-Fit)은 다음 한계가 있었습니다.

  • 부하 분산 부재: 등록 순서대로 첫 적합 코어를 잡아 앞쪽 코어로 쏠림.
  • CPU 미제약: FreeCPU9999로 하드코딩되어 vCPU 오버커밋이 무제한.
  • 이중 진실 소스: 자원 회계가 인메모리 Free* 단일 카운터 + DB로 분산, 오버커밋/reserve 개념 없음.
  • stale 헬스체크: IsAlive가 시작 시 1회만 설정되어 죽은/복귀 코어를 반영하지 못함.

목표는 (1) CPU 용량 실측, (2) Redis 기반 할당 집계, (3) 필터+코사인으로 요청 형태에 맞는 코어 선택을 통해 자원 분산 개선 + CPU 실질 제약화입니다.

Approach

선택 알고리즘 (service/core_allocation.go, 신규)

  • 1단계 하드 필터(살아있는 코어 + 3개 AND):
    • req.cpu ≤ logical_cpu*overcommit − allocated_cpu
    • req.mem ≤ total_mem − (allocated_mem + total_mem*mem_reserve_pct)
    • req.disk ≤ total_disk − (allocated_disk + total_disk*disk_reserve_pct)
  • 2단계 코사인 유사도: 코어의 남은 자원 비율 벡터요청 비율 벡터의 코사인 최댓값 선택 → 타이트한 차원을 채우고 다른 차원 잔여를 남기는 패킹 효과.
  • 코사인은 크기를 무시하므로 타이브레이커로 보완: ① 코사인 최대 → ② 남은 자원 magnitude 최대(더 빈 코어) → ③ 낮은 인덱스.
  • 순수 결정 로직(chooseBest, feasible, cosineSimilarity, ratio)을 분리해 Redis 없이 단위 테스트.

자원 회계 (Redis 공용) (service/alloc_redis.go, 신규)

  • core:{ip}:{port}:alloc(HASH cpu/mem/disk). VM status 키(UUID)와 접두어가 달라 기존 Redis(6379) 인스턴스를 그대로 공용(별도 인스턴스 신설하지 않음).
  • GetCoreAlloc/IncrCoreAlloc(TxPipeline 3×HIncrBy)/SetCoreAlloc/RebuildCoreAllocFromDB(시작 시 DB 합계로 멱등 재구성, stale 코어 0 초기화).

더블 부킹 방지: Lock() → SelectCore → IncrCoreAlloc(+) → Unlock()을 한 임계구역으로 묶어 read-then-decide를 직렬화. 느린 I/O(CMS/Guacamole/Core /createVM)는 락 밖. 실패 시 cleanup()이 예약을 IncrCoreAlloc(-)로 롤백.

단일 Control 프로세스 가정 — 수평 확장 시 Lua CAS로 교체 필요(코드 주석 명시).

CPU 실측 (startup/init.go, client/model/vm.go): vcpu_status 응답 필드를 추가해 코어당 1회 실측·캐시. 실패 시 IsAlive=false.

헬스체크 (service/healthcheck.go, 신규): 30s 주기로 락 밖에서 /getStatusHost 조회 후 락 안에서 반영(CreateVM과 동일 규율). IsAlive 양방향(복귀 코어 재활성화).

대안 대비: 별도 alloc-Redis(6380)도 검토했으나 운영 단순화를 위해 기존 인스턴스 공용으로 결정(키 네임스페이스 분리로 충돌 없음).

단위는 전 구간 MiB 유지(추가 변환 금지), CPU는 논리 코어 수(uint32).

Type of Change

  • Bug fix
  • New feature
  • Refactoring
  • Docs / Config
  • CI/CD

Related Issue

Testing

  • Tested locally
  • No regression in existing functionality
go build ./...                  # OK
go vet ./...                    # clean (기존 init.go lock-copy 경고만, 본 변경과 무관)
go test -race -count=1 ./service/...   # ok
  • 단위 테스트(service/core_allocation_test.go): cosineSimilarity(방향/직교/영벡터 NaN), ratio(경계), feasible(차원별 경계 + overcommit/reserve), chooseBest(가드 제외/shape 최적/타이브레이커).
  • ⚠️ 수동 E2E 미실행: 코어 ≥2 + 실 DB + Redis가 필요한 시나리오(부하 분산, CPU 포화 거부, reserve 거부, cleanup 감산, 삭제 회수, 코어 down/up, 재시작 멱등)는 환경 제약으로 미수행. 머지 전 스테이징 확인 권장.

Checklist

  • Reviewers assigned
  • Related issue linked

변경 파일

신규: service/core_allocation.go, service/alloc_redis.go, service/healthcheck.go, service/core_allocation_test.go
수정: structure/vm.go, resources/config.yaml, startup/core_ip_config.go, startup/init.go, client/model/vm.go, client/vm.go, service/vm.go, main.go, .env.example

상세 설계/구현 노트: allocation-design.md, allocation-implementation.md

리뷰 포인트

  • config.yaml 기본값: cpu_overcommit: 4.0, mem_reserve_pct: 0.1, disk_reserve_pct: 0.1 (env CPU_OVERCOMMIT/MEM_RESERVE_PCT/DISK_RESERVE_PCT로 오버라이드).
  • Free* 필드는 표시용 캐시로 격하(할당 판단은 CoreInfoIdx − Redis alloc − reserve). 완전 제거는 후속 옵션.
  • DeleteVM에 기존 누락돼 있던 VMInfoIdx/VMLocation/AliveVM 정리를 함께 보완.

Replace First-Fit core selection with a hard availability filter followed
by cosine-similarity weighting, and move resource accounting into Redis so
CPU becomes a real constraint and load spreads across cores.

- selection (service/core_allocation.go): hard filter (alive + cpu/mem/disk
  AND, with overcommit and reserve) -> cosine similarity between a core's
  remaining-ratio vector and the request-ratio vector; tie-break by
  remaining magnitude then lowest index. Pure logic (chooseBest/feasible/
  cosineSimilarity/ratio) is split out and unit-tested.
- accounting (service/alloc_redis.go): per-core HASH core:{ip}:{port}:alloc
  on the existing Redis instance (6379), namespaced apart from VM-status
  keys. GetCoreAlloc / IncrCoreAlloc (TxPipeline) / SetCoreAlloc /
  RebuildCoreAllocFromDB (idempotent rebuild from DB sums on startup).
- double-booking: Lock -> SelectCore -> IncrCoreAlloc(+) -> Unlock as one
  critical section; slow I/O stays outside the lock; cleanup() rolls the
  reservation back on failure. Single-process assumption documented.
- CPU capacity: measure logical CPUs via /getStatusHost (vcpu_status.total)
  and cache per core; drop the FreeCPU=9999 hardcode.
- healthcheck (service/healthcheck.go): 30s goroutine refreshing core
  liveness/capacity; IsAlive is now bidirectional (recovered cores re-enable).
- config: cpu_overcommit / mem_reserve_pct / disk_reserve_pct in config.yaml
  with env overrides and safe defaults.
- DeleteVM: release alloc and clean up VMInfoIdx/VMLocation/AliveVM
  (previously leaked).

Free* fields are demoted to a display-only cache; availability is derived
from CoreInfoIdx - Redis alloc - reserve.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 22, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c8c0eb05-068c-4a20-ada0-b488782ca4e1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@kwonkwonn
Copy link
Copy Markdown
Contributor

kwonkwonn commented May 24, 2026

@ga111o
아직 개발 중인걸 알고있지만 추후 개발 더 진행하실때 고려해볼만한 부분이 있을 것 같아 코멘트 남깁니다. :)

  • 코사인 유사도를 활용하여 코어를 채택하겠다는 아이디어는 좋지만 복잡도나 직관성 측면에서 다른 사람이 해당 부분을 수정하려 할때의 진입장벽이 우려됩니다.

    • 코사인 유사도 알고리즘을 "여러 요소들을 고려하여 남은 자원과 비율이 비슷한 것을 선택하는 무언가"로 생각하고 pkg 와 같은 공용 디렉토리에 둔 다음 활용하는 방식이라면 알고리즘을 수정하거나 완성도를 높이는 작업이 수월해질 것 같습니다.
  • 알고리즘을 정확하게 파악하지는 못했지만, 자원별로 가중치를 둘 수 있는 방법이 있을까요? 예를 들어 disk 와 같은 경우 초과하지 않는 이상 cpu 와 ram 에 비해 중요도가 낮은 자원이라고 생각합니다.

  • 해당 pr 의 크기가 커서 리뷰하기에 어려움이 있을 것 같습니다. 헬스 체크, 새 알고리즘, allocation 로직 수정, 레디스 적재 등 한 pr 에서 다루기엔 영향도가 커 분리한다면 좀 더 빠르게 리뷰할 수 있을 것 같아요.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors VM core allocation from a simple first-fit approach to a hard-filter + cosine-similarity scoring model, and moves resource accounting to Redis-backed per-core allocation totals. It also introduces periodic health checks to refresh core availability/capacity and removes older repository/resource-manager abstractions in favor of methods directly on ControlContext.

Changes:

  • Add new core selection algorithm (hard feasibility filter + cosine similarity tie-breaking) and unit tests for the pure decision logic.
  • Introduce Redis HASH-based per-core allocation accounting and rebuild-on-startup reconciliation from DB.
  • Add a periodic healthcheck goroutine to refresh core liveness and capacity, and update VM create/delete flows to reserve/rollback allocations.

Reviewed changes

Copilot reviewed 26 out of 26 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
tests/docker-compose.test.yml Adjust MySQL healthcheck and container restart behavior for tests.
structure/vm.go Add allocation-tuning parameters to config (overcommit/reserve).
structure/resource_manager.go Remove legacy in-memory resource manager implementation.
structure/repository.go Remove legacy VM repository interface.
structure/mysql_vm_repository.go Remove legacy MySQL repository implementation (moved into ControlContext).
structure/control_infra.go Move VM DB persistence methods into ControlContext; add context mutex helpers.
startup/init.go Replace old repo/resource-manager initialization; add CPU total measurement via core API.
startup/core_ip_config.go Apply allocation defaults and env overrides for new config parameters.
service/vm.go Switch CreateVM/DeleteVM to new SelectCore + Redis reservation/rollback + in-memory tracking updates.
service/redis.go Store/read VM Redis records using client/model.VMRedisInfo directly.
service/network.go Update CMS subnet allocation calls to new CMS client API; remove CMS delete helper.
service/healthcheck.go New periodic core healthcheck to refresh liveness/capacity.
service/guacamole.go Update locking to use ControlContext mutex instead of removed Resources.
service/dto.go Remove service-layer DTOs; API now passes client model structs directly.
service/core_allocation.go New hard-filter + cosine similarity core selection logic.
service/core_allocation_test.go Unit tests for selection math/feasibility/tie-breaking.
service/cleanup.go Remove old cleanup-chain helper (replaced by inline cleanup closure).
service/alloc_redis.go New Redis HASH accounting for per-core alloc + DB-based rebuild on startup.
resources/config.yaml Add defaults for cpu_overcommit/mem_reserve_pct/disk_reserve_pct.
main.go Rebuild alloc-Redis on startup and start healthcheck goroutine.
client/vm.go Document and use /getStatusHost CPU-total retrieval for capacity.
client/model/vm.go Add vcpu_status.total support to host CPU status response; define VMRedisInfo/constants.
client/cms.go Rename/reshape CMS subnet request/response; remove CMS delete API.
api/get_vm_status.go Return client model status structs directly (removing API-specific wrappers).
api/create_vm.go Decode request directly into client/model.CreateVMRequest and call service with it.
.env.example Add env vars for allocation tuning parameters.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread service/vm.go
fmt.Printf("%s\n", subnetReq.IP)
fmt.Printf("%s\n", subnetReq.MacAddr)
fmt.Printf("%s\n", subnetReq.SdnUUID)

Comment thread service/vm.go
req.SdnUUID = subnetReq.SdnUUID
req.MacAddr = subnetReq.MacAddr
req.NetConf.NetType = 0
req.Users[0].SSHAuthorizedKeys = []string{publicKeyOpenSSH}
Comment thread service/vm.go
Comment on lines +110 to +113
fmt.Printf("%s\n", subnetReq.IP)
fmt.Printf("%s\n", subnetReq.MacAddr)
fmt.Printf("%s\n", subnetReq.SdnUUID)

Comment thread service/vm.go
cleanup.run()
log.Error("CreateVM: at least one user is required for Guacamole configuration", true)
return fmt.Errorf("CreateVM: at least one user is required")
uuid := vms.UUID(req.UUID.String().(string))
Comment thread service/vm.go
Comment on lines 228 to 246
coreClient := client.NewCoreClient(core)
if _, err := coreClient.DeleteVM(context.Background(), model.DeleteVMRequest{
_, err := coreClient.DeleteVM(ctx, model.DeleteVMRequest{
UUID: uuid,
Type: model.HardDelete,
}); err != nil {
})
if err != nil {
log.Error("error deleting VM %s on core %s: %v", uuid, core.IP, err)
return fmt.Errorf("DeleteVM: failed to delete VM %s on core %s: %w", uuid, core.IP, err)
}

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)
// CMS 삭제 실패는 로그만 남기고 삭제 자체는 성공으로 처리 (추가적인 수동 정리 필요)
err = contextStruct.DeleteInstance(uuid)
if err != nil {
log.Error("error deleting instance %s from ControlContext: %v", uuid, err)
return fmt.Errorf("DeleteVM: failed to delete instance %s: %w", uuid, err)
}
if cleanupErr := guacamole.Cleanup(string(uuid), contextStruct.GuacDB); cleanupErr != nil {
log.Error("Failed to cleanup Guacamole config during rollback: %v", cleanupErr)
}

Comment on lines 31 to +40
func (c *ControlContext) FindCoreByVmUUID(uuid UUID) *Core {
log := util.GetLogger()

// Searching in-memory cache
c.Resources.RLock()
if core, ok := c.Resources.VMLocation[uuid]; ok {
c.Resources.RUnlock()
return core
}
c.Resources.RUnlock()

// If not found in cache, query the repository
coreIdx, err := c.VMRepo.GetInstanceLocation(uuid)
coreIdx, err := c.GetInstanceLocation(uuid)
if err != nil {
log.Error("Core not found for VM UUID %s", uuid, true)
return nil
}
c.Resources.Lock()
defer c.Resources.Unlock()
if coreIdx < 0 || coreIdx >= len(c.Resources.Cores) {
log.Error("Core index %d out of range for VM UUID %s", coreIdx, uuid, true)
return nil
return &c.Cores[coreIdx]
}
Comment thread service/network.go
Comment on lines +37 to 49
last_subnet := ctx.Last_subnet
next_last_subnet := pkgnetwork.FindSubnet(last_subnet)
log.Info("NewCmsSubnet : next_last_subnet: %s", next_last_subnet)

//CMS 호출 전에 다음 서브넷을 선점하여 동시 호출 시 중복 할당 방지
_, err := ctx.DB.Exec("UPDATE subnet SET last_subnet = ? WHERE id = 1", nextLastSubnet)
// DB를 먼저 업데이트하여 서브넷을 선점한다.
// CMS 호출 전에 선점해야 실패 시 동일 서브넷이 중복 할당되는 것을 방지할 수 있다.
_, err := ctx.DB.Exec("UPDATE subnet SET last_subnet = ? WHERE id = 1", next_last_subnet)
if err != nil {
log.Error("Failed to update last_subnet in database: %v", err)
return nil, fmt.Errorf("NewCmsSubnet: failed to update last_subnet in DB: %w", err)
}
ctx.Last_subnet = nextLastSubnet
ctx.Last_subnet = next_last_subnet

Comment thread api/create_vm.go
util.RespondError(w, http.StatusBadRequest, "Memory, CPU, and Disk must be non-zero")
return
}

Comment on lines +187 to +193
var rows *sql.Rows
rows, err = tx.QueryContext(ctx, "SELECT info.uuid, loc.core, info.inst_ip, info.guac_pass, info.inst_vcpu, info.inst_mem, info.inst_disk FROM inst_loc loc JOIN inst_info info ON loc.uuid = info.uuid")
if err != nil {
log.Error("Failed to get joined instance info: %v", err)
return nil, nil, err
}

Comment thread client/model/vm.go
Usage float64 `json:"usage_percent"`
// Desc는 호스트 /getStatusHost(host_dataType=0) 응답에만 존재(runtime.NumCPU()).
// VM별 /getStatusUUID 응답에는 없으므로 포인터로 두어 미존재를 nil로 감지한다.
Desc *VCPUStatus `json:"vcpu_status"`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants