Rendimiento de escritura concurrente de SQLite WAL en sistemas UNIX

Rendimiento de escritura concurrente de SQLite WAL en sistemas UNIX

Tengo dos configuraciones: una se ejecuta en Windows 10 (partición ntfs) y la otra en Debian (partición ext4). El código fuente de R es el mismo. El proceso principal inicia 8 procesos secundarios (P-SOCKS), en 8 núcleos virtuales, que consultan y escriben en la misma base de datos sqlite habilitada para WAL.

En Windows 10 obtengo el 100 % de la carga de la CPU distribuida en todos los procesos. En Debian, apenas obtengo una carga de CPU del 25 %. Monitoreando los procesos en Debian Creo que las escrituras son los cuellos de botella ya que solo veo un proceso alcanzando el 100% en su vcore a la vez. (Los demás probablemente estén esperando para escribir).

Cada conexión utiliza PRAGMA busy_timeout = 60000;y PRAGMA journal_mode = WAL;.

Estoy intentando depurar esto. Intenté PRAGMA synchronous = OFF;pensar que podría tener algo que ver con fsync(), pero no veo ninguna mejora. ¿Alguna otra sugerencia sobre lo que podría estar causando el bajo rendimiento en Debian?

Editar: Write-Cache parece estar habilitado en el disco SCSI (marcado con sdparm) y ajustar las opciones de montaje ext4 como barrier=0y data=writebackno parece tener ningún efecto.

Evaluación comparativa

Aquí hay un código simple para comparar escrituras simultáneas:

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

El código simplemente lee y escribe blobs en la base de datos. Primero, haga algunas ejecuciones ficticias y permita que la base de datos aumente a unos pocos GB.

En mi computadora portátil con Windows 10 obtengo estos resultados (base de datos de 6 GB):

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

Veo claramente 1 vcore al 100%, luego 2 vcores al 100%. El rendimiento es casi el doble de rápido, lo que demuestra que dos procesos simultáneos no se bloquean entre sí.

En Debian me sale esto:

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

Los dos vcores nunca llegan al máximo. Además, no hay mejora en el rendimiento cuando se utilizan 2 procesos; es aún peor ya que parecen bloquearse entre sí. Y, por último, Debian tiene un hardware mejor (aunque virtualizado).

Respuesta1

Confirmado en Ubuntu 18.04, no lo he probado en Windows.

Simplifiqué su ejemplo y agregué código de instrumentación. El primer gráfico muestra la cantidad de blobs escritos para cada subproceso. En el primer gráfico, las mesetas indican inactividad en todos los núcleos durante aproximadamente 0,2 segundos, y los aumentos pronunciados son escrituras en ráfagas en todos los núcleos. El segundo gráfico muestra los datos sin procesar, más útiles con el gráfico que no funciona en una respuesta de StackOverflow.

Habilitar gc()hace que las ejecuciones sean más largas pero distribuye la carga de manera más uniforme, segundo gráfico a continuación.

No tengo idea de lo que está pasando. ¿Puedes replicar y experimentar más con esta configuración? Agradecería sus comentarios aquí o quizás en el rastreador de problemas de RSQLite.

Ejecución básica, singc()

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)

(la trama no funciona en StackOverflow)

Creado el 30-01-2020 por elpaquete reprex(v0.3.0)

Resultados congc()

información relacionada