Gleichzeitige Schreibleistung von SQLite WAL auf UNIX-Systemen

Gleichzeitige Schreibleistung von SQLite WAL auf UNIX-Systemen

Ich habe zwei Setups: eines läuft unter Windows 10 (NTFS-Partition), das andere unter Debian (Ext4-Partition). Der R-Quellcode ist derselbe. Der Hauptprozess startet 8 Kindprozesse (P-SOCKS) – auf 8 virtuellen Kernen – die alle dieselbe WAL-fähige SQLite-Datenbank abfragen und in diese schreiben.

Unter Windows 10 habe ich 100 % CPU-Last, verteilt auf alle Prozesse. Unter Debian habe ich kaum 25 % CPU-Last. Wenn ich die Prozesse unter Debian überwache, denke ich, dass Schreibvorgänge die Engpässe sind, da ich immer nur einen Prozess sehe, der 100 % seines virtuellen Kerns erreicht. (Die anderen warten wahrscheinlich darauf, zu schreiben.)

Jede Verbindung verwendet PRAGMA busy_timeout = 60000;und PRAGMA journal_mode = WAL;.

Ich versuche, das zu debuggen. Ich habe versucht, PRAGMA synchronous = OFF;mir vorzustellen, dass es etwas damit zu tun haben könnte fsync(), aber ich sehe keine Verbesserung. Irgendwelche anderen Vorschläge, was die schlechte Leistung unter Debian verursachen könnte?

Bearbeiten: Der Schreibcache scheint auf der SCSI-Festplatte aktiviert zu sein (überprüft mit sdparm) und das Optimieren der Ext4-Mount-Optionen wie barrier=0und data=writebackscheint keine Wirkung zu haben.

Benchmarking

Hier ist ein einfacher Code zum Benchmarking gleichzeitiger Schreibvorgänge:

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

Der Code liest und schreibt einfach Blobs in die Datenbank. Führen Sie zunächst einige Probeläufe durch und lassen Sie die Datenbank auf einige GB anwachsen.

Auf meinem Windows 10-Laptop erhalte ich diese Ergebnisse (6 GB Datenbank):

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

Ich sehe deutlich 1 Vcore bei 100 %, dann 2 Vcores bei 100 %. Die Leistung ist fast doppelt so schnell, was zeigt, dass sich 2 gleichzeitige Prozesse nicht gegenseitig blockieren.

Unter Debian erhalte ich Folgendes:

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

Die beiden virtuellen Kerne werden nie voll ausgelastet. Außerdem gibt es keine Leistungsverbesserung, wenn zwei Prozesse verwendet werden – es ist sogar noch schlimmer, da sie sich gegenseitig zu blockieren scheinen. Und schließlich läuft Debian auf besserer (wenn auch virtualisierter) Hardware.

Antwort1

Bestätigt auf Ubuntu 18.04, nicht unter Windows getestet.

Ich habe Ihr Beispiel vereinfacht und Instrumentierungscode hinzugefügt. Das erste Diagramm zeigt die Anzahl der für jeden Unterprozess geschriebenen Blobs. Im ersten Diagramm zeigen die Plateaus Inaktivität auf allen Kernen für etwa 0,2 Sekunden an, und die steilen Anstiege sind Burst-Schreibvorgänge auf allen Kernen. Das zweite Diagramm zeigt die Rohdaten, die am nützlichsten mit Plotly sind, das in einer StackOverflow-Antwort nicht funktioniert.

Durch die Aktivierung gc()werden die Läufe länger, die Last wird jedoch gleichmäßiger verteilt (siehe zweites Diagramm unten).

Ich habe keine Ahnung, was los ist. Können Sie dieses Setup replizieren und weiter damit experimentieren? Ich würde mich über Feedback hier oder vielleicht im RSQLite-Issue-Tracker freuen.

Grundlauf, ohnegc()

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)

(Plotly funktioniert nicht auf StackOverflow)

Erstellt am 30.01.2020 vonReprex-Paket(Version 0.3.0)

Ergebnisse mitgc()

verwandte Informationen