SQLite并发写入性能

huangapple go评论90阅读模式
英文:

Sqlite concurrent writing performance

问题

我正在使用Golang和Sqlite3编写一个网站,并且预计每天有大约1000个并发写入操作,每天持续几分钟。所以我进行了以下测试(忽略错误检查以使代码更简洁):

t1 := time.Now()
tx, _ := db.Begin()
stmt, _ := tx.Prepare("insert into foo(stuff) values(?)")
defer stmt.Close()

for i := 0; i < 1000; i++ {
    _, _ = stmt.Exec(strconv.Itoa(i) + " - ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789,./;'[]-=<>&?:()*&^%$#@!~`")
}

tx.Commit()
t2 := time.Now()
log.Println("写入时间:", t2.Sub(t1))

写入时间大约为0.1秒。然后我修改了循环部分:

for i := 0; i < 1000; i++ {
    go func(stmt *sql.Stmt, i int) {
        _, err = stmt.Exec(strconv.Itoa(i) + " - ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789,./;'[]-=<>&?:()*&^%$#@!~`")
        if err != nil {
            log.Fatal(err)
        }
    }(stmt, i)
}

这样做花费了我整整46.2秒!我运行了多次,每次都超过40秒!有时甚至超过一分钟!由于Golang并发处理每个用户,这是否意味着我必须切换数据库才能使网页正常工作?谢谢!

英文:

I'm writing a website with Golang and Sqlite3, and I expect around 1000 concurrent writings per second for a few minutes each day, so I did the following test (ignore error checking to look cleaner):

t1 := time.Now()
tx, _ := db.Begin()
stmt, _ := tx.Prepare(&quot;insert into foo(stuff) values(?)&quot;)
defer stmt.Close()

for i := 0; i &lt; 1000; i++ {
	_, _ = stmt.Exec(strconv.Itoa(i) + &quot; - ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789,./;&#39;[]-=&lt;&gt;?:()*&amp;^%$#@!~`&quot;)
}

tx.Commit()
t2 := time.Now()
log.Println(&quot;Writing time: &quot;, t2.Sub(t1))

And the writing time is about 0.1 second. Then I modified the loop to:

for i := 0; i &lt; 1000; i++ {
	go func(stmt *sql.Stmt, i int) {
		_, err = stmt.Exec(strconv.Itoa(i) + &quot; - ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789,./;&#39;[]-=&lt;&gt;?:()*&amp;^%$#@!~`&quot;)
		if err != nil {
			log.Fatal(err)
		}
	}(stmt, i)
}

This gives me holy 46.2 seconds! I run it many times and every time is beyond 40 seconds! Sometimes even over a minute! Since Golang handles each user concurrently, does it mean I have to switch database in order to make the webpage working? Thanks!

答案1

得分: 34

我最近在一个网络应用中评估了Go中SQLite3的性能,并了解到在使用之前需要进行一些设置。

开启写日志模式

你需要使用WAL PRAGMA journal_mode=WAL。这就是为什么你会得到如此糟糕的性能的主要原因。使用WAL,我可以在几秒钟内进行10000个并发写操作,而无需使用事务。在事务中,它的速度将非常快。

禁用连接池

我使用的是mattn/go-sqlite3,它使用SQLITE_OPEN_FULLMUTEX标志打开数据库。这意味着每个SQLite调用都将受到锁的保护。所有操作都将被串行化。这实际上是你在SQLite中想要的。在这种情况下,Go的问题在于你会随机收到告诉你数据库被锁定的错误。原因是sql/DB内部的工作方式。它为你管理连接池,因此它会打开多个SQLite连接,而你不希望这样做。为了解决这个问题,我不得不基本上禁用连接池。调用db.SetMaxOpenConns(1)就可以解决。即使在非常高的负载下,有成千上万个并发读写操作,它也可以正常工作。

另一种解决方案可能是使用SQLITE_OPEN_NOMUTEX以多线程模式运行SQLite,并让它为你管理。但是SQLite在多线程应用程序中并不真正工作。读操作可以并行进行,但每次只能进行一次写操作。你会偶尔遇到busy错误,这在SQLite中是完全正常的,但你可能不希望在发生这种情况时完全停止写操作。这就是为什么大多数时候人们要么同步地使用SQLite,要么将调用发送到一个专门用于SQLite的单独线程中。

英文:

I recently evaluated SQLite3 performance in Go myself for a network application and learned that it needs a bit of setup before it even remotely usable.

Turn on the Write-Ahead Logging

You need to use WAL PRAGMA journal_mode=WAL. That's mainly why you get such a bad performance. With WAL I can do 10000 concurent writes without transactions in a matter of seconds. Within transaction it will be lightning fast.

Disable connections pool

I use mattn/go-sqlite3 and it opens a database with SQLITE_OPEN_FULLMUTEX flag. It means that every SQLite call will be guarded with a lock. Everything will be serialized. And that's actually what you want with SQLite. The problem with Go in this situation is that you will get random errors that tell you that the database is locked. And the reason why is because of the way the sql/DB works inside. Inside it manages pool of connections for you, so it will open multiple SQLite connections and you don't want to do that. To solve this I had to, basically, disable the pool. Call db.SetMaxOpenConns(1) and it will work. Even on very high loads with tens of thousands of concurent reads and writes it works without a problem.

Other solution might be to use SQLITE_OPEN_NOMUTEX to run SQLite in multi-threaded mode and let it manage that for you. But SQLite doesn't really work in multi-threaded apps. Reads can happen in parallel but only one write at a time. You will get occasional busy errors which are completely normal for SQLite but will require you to do something with them - you probably don't want to stop a write operation completely when that happens. That's why most of the time people work with SQLite either synchronously or by sending calls to a separate thread just for the SQLite.

答案2

得分: 2

我在go1.18上测试了写入性能,以查看并行性是否有效。

开箱即用

我使用了3个Golang线程,递增同一记录的不同整数列。

并行性结论:

  • 读取代码5的百分比为2.5%
  • 写入代码5的百分比为518%(在尝试之间等待5倍)
  • 写入吞吐量:每秒2,514次写入

代码5表示“数据库被锁定(5)(SQLITE_BUSY)”。

几年前,在Node.js上,只有并发而不是并行时,驱动程序会崩溃,除非我将写入序列化,即写入并发=1。

序列化写入

使用golang的github.com/haraldrudell/parl.NewModerator(1, context.Background())进行序列化写入:

序列化结果:

  • 读取代码5:0.005%
  • 写入代码5:0.02%
  • 每秒3,032次写入(+20%)

读取操作没有序列化,但会被同一线程中的写入操作阻塞。写入操作似乎比读取操作昂贵208倍。

在golang中序列化写入可以提高写入性能20%。

PRAGMA journal_mode

启用sqlDB.Exec("PRAGMA journal_mode = WAL")(从默认值:journalMode: delete)

将写入性能提高到18,329次/秒,即再提高6倍。

  • 代码5变为0

多个进程

使用3个进程x 3个线程,每个进程的写入操作进行序列化,会降低约5%的写入吞吐量,并将代码5提高到200%。好消息是,在macOS 12.3.1 apfs上,文件锁定可以正常工作,没有错误。

英文:

I tested the write performance on go1.18 to see if parallelism works

&emsp;

Out of Box

I used 3 golang threads incrementing different integer columns of the same record

Parallelism Conclusions:

  • Read code 5 percentage 2.5%
  • Write code 5 percentage 518% (waiting 5x in between attempts)
  • Write throughput: 2,514 writes per second

code 5 is “database is locked (5) (SQLITE_BUSY)”

A few years ago on Node.js the driver crashes with only concurrency, not parallelism, unless I serialized the writes, ie. write concurrency = 1

&emsp;

Serialized Writes

With golang is used github.com/haraldrudell/parl.NewModerator(1, context.Background()), ie. serialized writes:

Serialized results:

  • read code 5: 0.005%
  • write code 5: 0.02%
  • 3,032 writes per second (+20%)

Reads are not serialized, but they are held up by writes in the same thread. Writes seems to be 208x more expensive than reads.

Serializing writes in golang increases write performance by 20%

&emsp;

PRAGMA journal_mode

Enabling sqlDB.Exec("PRAGMA journal_mode = WAL")
(from default: journalMode: delete)

increases write performance to 18,329/s, ie. another 6x

  • code 5 goes to 0

&emsp;

Multiple Processes

Using 3 processes x 3 threads with writes serialized per process lowers write throughput by about 5% and raises code 5 up to 200%. Good news is that file locking works without errors macOS 12.3.1 apfs

huangapple
  • 本文由 发表于 2016年3月5日 03:50:49
  • 转载请务必保留本文链接:https://go.coder-hub.com/35804884.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定