UNIX 시스템의 SQLite WAL 동시 쓰기 성능

UNIX 시스템의 SQLite WAL 동시 쓰기 성능

두 가지 설정이 있습니다. 하나는 Windows 10(ntfs 파티션)에서 실행되고 다른 하나는 Debian(ext4 파티션)에서 실행됩니다. R 소스코드도 동일합니다. 기본 프로세스는 8개의 vcore에서 8개의 하위 프로세스(P-SOCKS)를 시작합니다. 이 프로세스는 모두 동일한 WAL 지원 sqlite 데이터베이스에 쿼리하고 씁니다.

Windows 10에서는 모든 프로세스에 100% CPU 로드가 분산됩니다. 데비안에서는 CPU 부하가 25%도 거의 되지 않습니다. 데비안에서 프로세스 모니터링 쓰기는 한 번에 하나의 프로세스만 vcore에서 100%에 도달하는 것을 볼 수 있기 때문에 병목 현상이 발생한다고 생각합니다. (다른 분들도 아마 글을 기다리고 있을 겁니다.)

각 연결은 PRAGMA busy_timeout = 60000;및 을(를) 사용하고 있습니다 PRAGMA journal_mode = WAL;.

나는 이것을 디버깅하려고 노력하고 있습니다. 나는 PRAGMA synchronous = OFF;그것이 와 관련이 있을 수 있다고 생각하려고 노력했지만 fsync()어떤 개선도 보이지 않습니다. 데비안 성능 저하의 원인이 될 수 있는 다른 제안 사항이 있습니까?

편집하다: 쓰기 캐시는 SCSI 디스크에서 활성화된 것으로 보이며( 로 확인 ) 및 및 sdparm같은 ext4 마운트 옵션을 조정해도 아무런 효과가 없는 것 같습니다.barrier=0data=writeback

벤치마킹

다음은 동시 쓰기를 벤치마킹하기 위한 몇 가지 간단한 코드입니다.

make.con <- function() {
  con <<- DBI::dbConnect(RSQLite::SQLite(), dbname = 'db.sqlite')
  DBI::dbExecute(con, 'PRAGMA journal_mode = WAL;')
  DBI::dbExecute(con, 'PRAGMA busy_timeout = 60000;')
  DBI::dbExecute(con, '
    CREATE TABLE IF NOT EXISTS tmp (
      id INTEGER NOT NULL,
      blob BLOB NOT NULL,
      PRIMARY KEY (id)
  )')
}
make.con()


fn <- function(x) {
  set.seed(x)
  # read
  random.blob.read <- RSQLite::dbGetQuery(con, 'SELECT blob FROM tmp WHERE id = (SELECT abs(random() % max(tm.id)) FROM tmp tm);')
  # write
  blob <- serialize(list(rand = runif(1000)), connection = NULL, xdr = FALSE)
  RSQLite::dbExecute(con, 'INSERT INTO tmp (blob) VALUES (:blob);', params = list('blob' = list(blob)))
}

n <- 30000L

parallel::setDefaultCluster(parallel::makeCluster(spec = 2L))
parallel::clusterExport(varlist = 'make.con')
invisible(parallel::clusterEvalQ(expr = {make.con()}))

microbenchmark::microbenchmark(
  lapply(1:n, fn),
  parallel::parLapplyLB(X = 1:n, fun = fn, chunk.size = 50L), 
  times = 2L
)

parallel::stopCluster(cl = parallel::getDefaultCluster())

코드는 단순히 blob을 읽고 데이터베이스에 씁니다. 먼저, 몇 가지 더미 실행을 수행하고 데이터베이스가 몇 GB까지 증가하도록 허용합니다.

내 Windows 10 노트북에서 다음 결과를 얻었습니다(6GB 데이터베이스).

Unit: seconds
                                                       expr      min       lq     mean   median       uq      max neval
                                            lapply(1:n, fn) 26.02392 26.02392 26.54853 26.54853 27.07314 27.07314     2
 parallel::parLapplyLB(X = 1:n, fun = fn, chunk.size = 50L) 15.73851 15.73851 16.44554 16.44554 17.15257 17.15257     2

분명히 100%에서는 1 vcore가 보이고 100%에서는 2 vcore가 보입니다. 성능은 거의 두 배 빨라졌으며 이는 2개의 동시 프로세스가 서로를 차단하지 않는다는 것을 보여줍니다.

데비안에서는 다음을 얻습니다:

Unit: seconds
                                                       expr      min       lq     mean   median       uq      max neval  
                                            lapply(1:n, fn) 39.96850 39.96850 40.14782 40.14782 40.32714 40.32714     2
 parallel::parLapplyLB(X = 1:n, fun = fn, chunk.size = 50L) 43.34628 43.34628 44.85910 44.85910 46.37191 46.37191     2

두 개의 vcore는 결코 최대값을 초과하지 않습니다. 또한 2개의 프로세스를 사용해도 성능이 향상되지 않습니다. 서로를 차단하는 것처럼 보이기 때문에 더욱 그렇습니다. 그리고 마지막으로 데비안은 (비록 가상화되었지만) 더 나은 하드웨어를 사용합니다.

답변1

Ubuntu 18.04에서 확인되었으며 Windows에서는 테스트되지 않았습니다.

귀하의 예를 단순화하고 계측 코드를 추가했습니다. 첫 번째 플롯은 각 하위 프로세스에 대해 작성된 Blob 수를 보여줍니다. 첫 번째 플롯에서 정체는 약 0.2초 동안 모든 코어의 비활성을 나타내며 가파른 상승은 모든 코어에 걸친 버스트 쓰기를 나타냅니다. 두 번째 플롯은 StackOverflow 답변에서 작동하지 않는 플롯에 가장 유용한 원시 데이터를 보여줍니다.

활성화하면 gc()실행 시간이 길어지지만 부하가 더 균등하게 분산됩니다. 아래의 두 번째 플롯입니다.

나는 무슨 일이 일어나고 있는지 전혀 모른다. 이 설정을 사용하여 추가로 복제하고 실험할 수 있습니까? 여기 또는 RSQLite 문제 추적기에서 피드백을 주시면 감사하겠습니다.

기본 실행, 없음gc()

make.con <- function() {
  options(digits.secs = 6)

  con <<- DBI::dbConnect(RSQLite::SQLite(), dbname = "db.sqlite")
  DBI::dbExecute(con, "PRAGMA journal_mode = WAL;")
  DBI::dbExecute(con, "PRAGMA busy_timeout = 60000;")
  DBI::dbExecute(con, "PRAGMA synchronous = OFF;")
  DBI::dbExecute(con, "
    CREATE TABLE IF NOT EXISTS tmp (
      id INTEGER NOT NULL,
      blob BLOB NOT NULL,
      PRIMARY KEY (id)
  )")
}
make.con()
#> [1] 0

blob <- serialize(list(rand = runif(1000)), connection = NULL, xdr = FALSE)

fn <- function(x) {
  time0 <- Sys.time()
  rs <- DBI::dbSendQuery(con, "INSERT INTO tmp (blob) VALUES (:blob);")
  time1 <- Sys.time()
  DBI::dbBind(rs, params = list("blob" = list(blob)))
  time2 <- Sys.time()
  DBI::dbClearResult(rs)
  time3 <- Sys.time()
  # gc()
  time4 <- Sys.time()
  list(pid = unix::getpid(), time0 = time0, time1 = time1, time2 = time2, time3 = time3, time4 = time4)
}

n <- 1000L

parallel::setDefaultCluster(parallel::makeCluster(8L))
parallel::clusterExport(varlist = c("make.con", "blob"))
invisible(parallel::clusterEvalQ(expr = {
  make.con()
}))

data <- parallel::parLapply(X = 1:n, fun = fn, chunk.size = 50L)

parallel::stopCluster(cl = parallel::getDefaultCluster())

library(tidyverse)

tbl <-
  data %>%
  transpose() %>%
  map(unlist, recursive = FALSE) %>%
  as_tibble() %>%
  rowid_to_column() %>%
  pivot_longer(-c(rowid, pid), names_to = "step", values_to = "time") %>%
  mutate(time = as.POSIXct(time, origin = "1970-01-01")) %>%
  mutate(pid = factor(pid)) %>%
  arrange(time)

tbl %>%
  group_by(pid) %>%
  mutate(cum = row_number()) %>%
  ungroup() %>%
  ggplot(aes(x = time, y = cum, color = pid)) +
  geom_line()

p <-
  tbl %>%
  ggplot(aes(x = time, y = factor(pid), group = 1)) +
  geom_path() +
  geom_point(aes(color = step))

p

plotly::ggplotly(p)

(StackOverflow에서는 플롯이 작동하지 않습니다)

작성자: 2020-01-30reprex 패키지(v0.3.0)

결과gc()

관련 정보