0Pricing
R Academy · 강의

병렬 코드 디버깅과 부하 분산

병렬 작업자의 오류를 처리하고 불균등한 작업량을 효과적으로 분산합니다.

병렬 코드 디버깅과 부하 분산은(는) CoddyKit의 무료 R Academy 강의입니다. 이것은 4개 중 4번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 R Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. R Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

병렬 디버깅이 어려운 이유

병렬 코드의 디버깅이 어려운 이유는 작업자가 별도의 프로세스에서 실행되기 때문입니다. print()문은 주 프로세스에서 보이지 않고, browser() 같은 대화형 디버거는 작업자 내부에서 작동하지 않으며, 오류는 직렬화된 후 주 프로세스에서 다시 발생하므로 원래의 호출 스택 추적이 사라집니다.

library(parallel)

# Problem: cat() inside worker is invisible in the master
cl <- makeCluster(2)
clusterExport(cl, c())

result <- parLapply(cl, 1:4, function(i) {
  # This cat() goes to the worker's stdout, NOT the master
  cat('Worker processing', i, '\n')  # you will NOT see this
  i^2
})

cat('Master received:', unlist(result), '\n')
stopCluster(cl)

parLapply의 오류 전파

작업자에서 오류가 발생하면 parLapply()가 오류를 감싼 뒤 주 프로세스에서 다시 발생시킵니다. 오류 메시지는 보존되지만 원격 호출 스택 추적은 보존되지 않습니다. 병렬화하기 전에 항상 함수를 먼저 순차적으로 테스트하십시오.

library(parallel)
cl <- makeCluster(2)

# Step 1: test sequentially first!
my_fn <- function(x) {
  if (x == 3) stop('bad value: 3')
  sqrt(x)
}

# Sequential test reveals the bug before parallelising
tryCatch(
  lapply(1:5, my_fn),
  error = function(e) cat('Sequential error caught:', e$message, '\n')
)

# Fix: guard the bad case
my_fn_safe <- function(x) {
  if (x <= 0) return(NA_real_)
  sqrt(x)
}
clusterExport(cl, 'my_fn_safe')
cat(unlist(parLapply(cl, 1:5, my_fn_safe)), '\n')

stopCluster(cl)

작업자 함수 내부의 tryCatch

작업자 함수 본문을 tryCatch()로 감싸면 전체 병렬 작업을 중단하지 않고 요소별 오류를 포착하여 기록할 수 있습니다. 실패 시에는 NA 같은 표시 값을 반환하여 후처리에서 문제가 있는 입력을 식별할 수 있게 하십시오.

library(parallel)
cl <- makeCluster(2)

safe_compute <- function(x) {
  tryCatch({
    if (x == 3) stop('simulated error on element 3')
    list(value = x^2, error = NULL)
  }, error = function(e) {
    list(value = NA_real_, error = conditionMessage(e))
  })
}
clusterExport(cl, 'safe_compute')

results <- parLapply(cl, 1:5, safe_compute)

for (i in seq_along(results)) {
  r <- results[[i]]
  if (is.null(r$error)) {
    cat('Element', i, ': value =', r$value, '\n')
  } else {
    cat('Element', i, ': ERROR -', r$error, '\n')
  }
}

stopCluster(cl)

tryCatch를 사용한 future::value()

future 패키지에서 value(f)는 원격 오류를 다시 발생시킵니다. 이를 tryCatch()로 감싸면 다른 future의 결과를 계속 수집하면서 개별 future의 실패를 처리할 수 있습니다.

library(future)
plan(multisession, workers = 2)

# Create futures — some will fail
inputs <- list(4, -1, 9, 'x', 16)

futures <- lapply(inputs, function(val) {
  future({
    if (!is.numeric(val)) stop('non-numeric input')
    if (val < 0) stop('negative input')
    sqrt(val)
  })
})

# Collect with individual error handling
results <- lapply(futures, function(f) {
  tryCatch(
    value(f),
    error = function(e) paste('ERROR:', e$message)
  )
})

for (i in seq_along(results)) {
  cat('Input:', inputs[[i]], '-> Result:', as.character(results[[i]]), '\n')
}

plan(sequential)

foreach와 %dopar%, .errorhandling

foreach 패키지의 %dopar% 연산자는 등록된 백엔드에 반복 실행을 분산합니다. .errorhandling 인수는 오류 발생 시의 동작을 제어합니다. 'stop'(기본값), 'remove'(건너뛰기), 'pass'(조건 객체 포함) 중에서 선택할 수 있습니다.

library(foreach)
library(doParallel)

cl <- makeCluster(2)
registerDoParallel(cl)

# .errorhandling = 'pass': failed elements return the condition
results <- foreach(
  x = 1:6,
  .errorhandling = 'pass'
) %dopar% {
  if (x == 4) stop('bad element')
  x^2
}

for (i in seq_along(results)) {
  if (inherits(results[[i]], 'error')) {
    cat('Element', i, ': ERROR -', results[[i]]$message, '\n')
  } else {
    cat('Element', i, ': value =', results[[i]], '\n')
  }
}

stopCluster(cl)

작업 분배: 정적 방식과 동적 방식

정적 일정 계획은 같은 크기의 작업 묶음을 작업자에게 미리 할당합니다. 동적 일정 계획은 작업자마다 한 번에 하나의 작업을 할당하므로, 작업을 빨리 끝낸 작업자가 더 많은 작업을 가져갑니다. 작업 시간의 편차가 큰 경우에는 동적 방식이 더 적합합니다.

library(parallel)
cl <- makeCluster(2)

# Simulate unequal task durations (element i takes i*0.05 seconds)
unequal_task <- function(i) {
  Sys.sleep(i * 0.05)
  i
}
clusterExport(cl, 'unequal_task')

# Static: parLapply distributes in fixed chunks
static_time <- system.time(
  parLapply(cl, 1:6, unequal_task)
)[['elapsed']]

# Dynamic: clusterApplyLB assigns one task per available worker
dynamic_time <- system.time(
  clusterApplyLB(cl, 1:6, unequal_task)
)[['elapsed']]

cat('Static LB: ', round(static_time, 2), 's\n')
cat('Dynamic LB:', round(dynamic_time, 2), 's\n')

stopCluster(cl)

작업 묶음으로 오버헤드 줄이기

프로세스 간 통신에는 작업마다 고정 오버헤드가 발생합니다. 작은 항목을 많이 처리할 때는 항목을 더 큰 묶음으로 그룹화하십시오. 그러면 각 작업자 호출이 메시지 하나당 더 많은 작업을 수행하여 오버헤드와 계산량의 비율을 낮출 수 있습니다.

library(parallel)
cl <- makeCluster(2)

# Naive: 1000 tiny tasks — high overhead
tiny_task <- function(x) x^2
clusterExport(cl, 'tiny_task')

t1 <- system.time(parLapply(cl, 1:1000, tiny_task))[['elapsed']]

# Chunked: 10 tasks of 100 items each — low overhead
chunk_task <- function(chunk) sapply(chunk, function(x) x^2)
chunks <- split(1:1000, cut(1:1000, 10, labels = FALSE))
clusterExport(cl, 'chunk_task')

t2 <- system.time(parLapply(cl, chunks, chunk_task))[['elapsed']]

cat('Unchunked:', round(t1, 4), 's\n')
cat('Chunked:  ', round(t2, 4), 's\n')

stopCluster(cl)

큰 객체 전송 피하기

큰 객체(데이터 프레임, 행렬, 모델)를 작업자에게 직렬화하여 보내는 작업은 비용이 큽니다. 대신 인덱스만 전달하고 공유 원본(파일, 데이터베이스 또는 작업자 내부에 미리 적재한 데이터)에서 데이터를 읽으십시오. 작업자에게 전달하는 데이터는 작게 유지하십시오.

library(parallel)
cl <- makeCluster(2)

# BAD: sends the entire large data frame to every worker
big_df <- data.frame(x = rnorm(1e5), y = rnorm(1e5))
clusterExport(cl, 'big_df')  # 800 KB shipped to each worker

# BETTER: workers generate/read their own data slice
clusterEvalQ(cl, {
  set.seed(Sys.getpid())  # unique per worker
  local_data <- data.frame(x = rnorm(500), y = rnorm(500))
})

# Each worker uses its own local_data without receiving it from master
result <- parLapply(cl, 1:2, function(i) {
  coef(lm(y ~ x, data = local_data))
})
print(result)

stopCluster(cl)

작업자에서 기록 남기기

작업자는 주 프로세스의 콘솔에 쓸 수 없으므로 makeCluster(outfile = '/path/to/log')를 사용하여 작업자별 출력을 작업자별 로그 파일로 리디렉션하십시오. Unix에서는 /dev/null 또는 작업자마다 별도로 지정한 로그 파일로 리디렉션할 수 있습니다.

library(parallel)

# Route all worker stdout/stderr to a log file
log_file <- tempfile(fileext = '.log')
cl <- makeCluster(2, outfile = log_file)

clusterEvalQ(cl, {
  cat('[Worker', Sys.getpid(), '] started\n')
})

parLapply(cl, 1:4, function(i) {
  cat('[Worker] processing item', i, '\n')  # goes to log file
  i * 10
})

stopCluster(cl)

# Read the log
log_content <- readLines(log_file)
cat(head(log_content, 8), sep = '\n')

병렬 코드 프로파일링

system.time()을 사용하여 전체 실제 경과 시간을 측정하고, 작업자 오버헤드와 계산 시간을 수동으로 나누어 분석하십시오. 더 자세히 분석하려면 먼저 profvis::profvis()로 대상 함수를 순차 실행하여 확인한 다음 병목 구간을 병렬화하십시오.

library(parallel)

# Profile the task sequentially first
target_fn <- function(n) {
  x <- rnorm(n)
  list(
    mean = mean(x),
    sd   = sd(x),
    q95  = quantile(x, 0.95)
  )
}

# Measure sequential baseline
t_seq <- system.time(lapply(rep(1000, 20), target_fn))[['elapsed']]

# Measure parallel speedup
cl <- makeCluster(2)
clusterExport(cl, 'target_fn')
t_par <- system.time(parLapply(cl, rep(1000, 20), target_fn))[['elapsed']]
stopCluster(cl)

cat('Sequential:', round(t_seq, 4), 's\n')
cat('Parallel:  ', round(t_par, 4), 's\n')
cat('Efficiency:', round(t_seq / (t_par * 2) * 100, 1), '%\n')

완전한 견고성 패턴

모범 사례를 모두 결합하면 다음과 같습니다. 작업을 묶음으로 나누고, 부하가 균등하게 분배되도록 할당하며, 요소별 오류를 처리하고, 파일에 기록하고, on.exit()으로 클러스터 정리를 보장합니다.

library(parallel)

robust_parallel <- function(items, fn, workers = 2) {
  cl <- makeCluster(workers, outfile = tempfile())
  on.exit(stopCluster(cl), add = TRUE)

  clusterExport(cl, 'fn', envir = environment())

  safe_fn <- function(x) {
    tryCatch(
      fn(x),
      error = function(e) list(error = e$message, value = NA)
    )
  }
  clusterExport(cl, 'safe_fn', envir = environment())

  # Load-balanced assignment for unequal task times
  clusterApplyLB(cl, items, safe_fn)
}

results <- robust_parallel(1:8, function(x) {
  if (x == 5) stop('deliberate error')
  x^3
})

cat('Results:\n')
for (i in seq_along(results)) {
  cat(' [', i, ']', ifelse(is.na(results[[i]]$value),
    paste('ERROR:', results[[i]]$error),
    results[[i]]
  ), '\n')
}

빠른 확인

실행 시간이 매우 크게 달라지는 병렬 작업 100개가 있습니다(일부는 10ms, 다른 일부는 500ms). 전체 실제 경과 시간을 최소화하려면 어떤 일정 계획 전략을 사용해야 할까요?

복습: 병렬 코드 디버깅

핵심 요점:

  • 병렬화하기 전에 함수를 순차적으로 테스트하십시오. 먼저 lapply()를 사용합니다
  • 작업자 본문을 tryCatch()로 감싸 작업을 중단하지 않고 요소별 오류를 포착하십시오
  • future::value()와 tryCatch()를 함께 사용하면 개별 future 실패를 안정적으로 처리할 수 있습니다
  • .errorhandling = 'pass'로 설정한 foreach %dopar%는 결과 목록에 오류 객체를 반환합니다
  • 작업 시간이 서로 다를 때 동적 부하 분산을 사용하려면 clusterApplyLB()를 사용하십시오
  • 통신 오버헤드를 줄이려면 작은 작업을 묶음으로 처리하십시오
  • 작업자에게 전달하는 데이터는 최소화하십시오. 큰 데이터 객체가 아니라 인덱스를 전달합니다
  • makeCluster(outfile=)을 통해 작업자 출력을 파일에 기록하십시오
# Canonical debugging workflow
# 1. Test sequentially
# lapply(inputs, my_fn)

# 2. Wrap in tryCatch
# safe_fn <- function(x) tryCatch(my_fn(x), error = function(e) NA)

# 3. Parallelise the safe version
# cl <- makeCluster(2); on.exit(stopCluster(cl))
# parLapply(cl, inputs, safe_fn)

cat('Debugging workflow: sequential -> tryCatch -> parallel\n')

자주 묻는 질문

“병렬 코드 디버깅과 부하 분산” 강의는 무료인가요?

네 — “병렬 코드 디버깅과 부하 분산” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 R Academy 강의 전체를 잠금 해제할 수 있습니다. R Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

“병렬 코드 디버깅과 부하 분산”에서 뭘 배우나요?

병렬 작업자의 오류를 처리하고 불균등한 작업량을 효과적으로 분산합니다. 브라우저에서 직접 실행하는 실습 코드로 R Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

R Academy을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 R Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 4번째 강의입니다.

“병렬 코드 디버깅과 부하 분산” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 R Academy 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 R Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. parallel 패키지와 detectCores()
  2. future 프레임워크
  3. furrr: 병렬 purrr 연산
  4. 병렬 코드 디버깅과 부하 분산
← R Academy(으)로 돌아가기