ILD

sqlite WAL performance issue
作者:Yuan Jianpeng 邮箱:yuanjp89@163.com
发布时间:2025-9-11 站点:Inside Linux Development

最近开发了一个相册模块,用户上传照片后,通过fanotify监视到新文件,然后将照片信息保存到sqlite数据库。


数据库使用WAL模式,操作流程是:

    sqlite3_open()

        -> BEGIN TRANSACTION

            -> insert

        -> END TRANSACTION

    sqlite3_close()


每次操作数据库都会打开,开始事务,插入数据、提交事务,然后关闭。

测试发现,在磁盘比较繁忙的时候,这套流程的耗时可能需要1s多。而仅仅只是插入几条数据,数据量可能也就几kB。


于是写一个demo,demo打开数据库,设置为WAL模式,然后循环执行事务,一个事务插入一定量的数据,将每个操作的耗时打印出来。

demo第一个参数表示一个事务插入多少数据,第二个参数表示执行多少次事务。


第一次执行:

# ./test_sqlite 10 2
open                : 4296174 us
---
begin transaction   : 7 us
insert              : 611 us
end transaction     : 111811 us
---
begin transaction   : 25 us
insert              : 386 us
end transaction     : 131 us
---
close               : 65592 us

第一次会创建数据库。可以发现,在open的时候,消耗了4s。


第二次执行:

# ./test_sqlite 10 2
open                : 1591 us
---
begin transaction   : 7 us
insert              : 438 us
end transaction     : 48153 us
---
begin transaction   : 25 us
insert              : 383 us
end transaction     : 121 us
---
close               : 43986 us


使用dd构造磁盘繁忙的情况,执行:

# dd if=/dev/zero of=tmp bs=4096

# ./test_sqlite 10 2
open                : 1987 us
---
begin transaction   : 7 us
insert              : 373 us
end transaction     : 7627371 us
---
begin transaction   : 28 us
insert              : 392 us
end transaction     : 129 us
---
close               : 24180207 us

可以发现,第一个commit transaction耗时了7s,然后close耗时了24s。


使用strace进行分析,发现

# strace -vfttT ./test_sqlite 10 2

17:38:38.134131 fdatasync(4)            = 0 <8.687928>


时间都消耗在了fdatasync。同时发现了一个规律,第二个transaction都非常快。strace里面显示,只有第一个transaction触发了fdatasync。


关于WAL

Write-Ahead Logging,https://www.sqlite.org/wal.html

它和传统的日志不一样,它将操作先写入wal文件,然后再日志达到1000个page的时候,再写回到主database。

这就引入了一个checkpoint机制,可以手动写回,也可以在commit transaction的自动写回,close数据库的时候,也会自动写回。

只有在写回的时候,才会触发fdatasync。


结论

为了避免频繁触发fdatasync,我们不能频繁打开关闭,打开后的第一个transaction必然触发写回。所以优化方案是,数据库保持打开的状态,即可。



demo源码:

#include <sqlite3.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <time.h>

#define SQL_JOURNAL_WAL		"PRAGMA journal_mode=WAL;"
#define SQL_SYNC_NORMAL		"PRAGMA synchronous=normal;"
#define SQL_BEGIN_TRANSACTION	"BEGIN TRANSACTION;"
#define SQL_END_TRANSACTION	"END TRANSACTION;"

#define SQL_CREAT_PHOTO		\
	"CREATE TABLE IF NOT EXISTS photo (" \
		"id INTEGER PRIMARY KEY AUTOINCREMENT," \
		"parent INTEGER,"	\
		"name TEXT," \
		"mode INTEGER," \
		"size INTEGER," \
		"msec INTEGER," \
		"mnsec INTEGER," \
		"shot_time INTEGER," \
		"flags INTEGER," \
		"latitude INTEGER," \
		"longitude INTEGER," \
		"duration INTEGER," \
		"city TEXT," \
		"poi TEXT" \
	");"

struct sqlite_photo
{
	uint64_t photo_id;
	uint64_t parent;
	const char *name;
	unsigned mode;
	uint64_t size;
	uint64_t mtime_sec;
	uint32_t mtime_nsec;
	uint64_t shot_time;
	uint32_t flags;
	uint32_t latitude;
	uint32_t longitude;
	uint32_t duration;
	const char *city;
	const char *poi;
};

static unsigned tsus()
{
	struct timespec ts;
	unsigned v;

	clock_gettime(CLOCK_MONOTONIC, &ts);

	v = ts.tv_sec * 1000000 + ts.tv_nsec / 1000;
	return v;
}

static char *sql_err(struct sqlite3 *db, int ret)
{
	static char errstr[512];
	snprintf(errstr, sizeof(errstr), "%d(%s): %s",
			ret, sqlite3_errstr(ret), sqlite3_errmsg(db));
	return errstr;
}

struct sqlite3 *sqlite_open(const char *db_path)
{
	struct sqlite3 *db = NULL;
	int ret;

	ret = sqlite3_open(db_path, &db);
	if (ret != SQLITE_OK) {
		fprintf(stderr, "sqlite3_open() open %s failed: %s\n",
			db_path, sqlite3_errstr(ret));
		return NULL;
	}

	ret = sqlite3_exec(db, SQL_JOURNAL_WAL, 0, 0, 0);
	if (ret != SQLITE_OK) {
		fprintf(stderr, "enable WAL failed: %s\n", sql_err(db, ret));
		return NULL;
	}

	ret = sqlite3_exec(db, SQL_SYNC_NORMAL, 0, 0, 0);
	if (ret != SQLITE_OK) {
		fprintf(stderr, "set sync failed: %s\n", sql_err(db, ret));
		return NULL;
	}

	ret = sqlite3_exec(db, SQL_CREAT_PHOTO, 0, 0, 0);
	if (ret != SQLITE_OK) {
		fprintf(stderr, "create photo table failed: %s\n", sql_err(db, ret));
		return NULL;
	}

	return db;
}

void sqlite_close(struct sqlite3 *db)
{
	int ret ;

	ret = sqlite3_close(db);
	if (ret != SQLITE_OK)
		fprintf(stderr, "sqlite3 close fialed: %s\n", sql_err(db, ret));
}

void sqlite_free_stmt(struct sqlite3_stmt *stmt)
{
	sqlite3_finalize(stmt);
}

int sqlite_begin_transaction(struct sqlite3 *db)
{
	int ret;

	ret = sqlite3_exec(db, SQL_BEGIN_TRANSACTION, 0, 0, NULL);
	if (ret != SQLITE_OK) {
		fprintf(stderr, "begin transaction failed: %d: %s: %s\n",
			ret, sqlite3_errstr(ret), sqlite3_errmsg(db));
		return -1;
	}

	return 0;
}

int sqlite_end_transaction(struct sqlite3 *db)
{
	int ret;

	ret = sqlite3_exec(db, SQL_END_TRANSACTION, 0, 0, NULL);
	if (ret != SQLITE_OK) {
		fprintf(stderr, "end transaction failed: %d: %s: %s\n",
			ret, sqlite3_errstr(ret), sqlite3_errmsg(db));
		return -1;
	}

	return 0;
}

int sqlite_insert_photo(struct sqlite3 *db, struct sqlite_photo *photo)
{
	sqlite3_stmt *stmt = NULL;
	int ret;
	int err = -1;
	const char *sql = "INSERT INTO photo"
			"(parent, name, mode, size, msec, mnsec, shot_time, "
				"flags, latitude, longitude, city, poi)"
			"VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);";

	ret = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
	if (ret != SQLITE_OK) {
		fprintf(stderr, "prepare insert photo failed: %s\n", sql_err(db, ret));
		return -1;
	}

	ret = 0;
	ret += sqlite3_bind_int64(stmt, 1, photo->parent);
	ret += sqlite3_bind_text(stmt, 2, photo->name, -1, SQLITE_STATIC);
	ret += sqlite3_bind_int(stmt, 3, photo->mode);
	ret += sqlite3_bind_int64(stmt, 4, photo->size);
	ret += sqlite3_bind_int64(stmt, 5, photo->mtime_sec);
	ret += sqlite3_bind_int(stmt, 6, photo->mtime_nsec);
	ret += sqlite3_bind_int64(stmt, 7, photo->shot_time);
	ret += sqlite3_bind_int(stmt, 8, photo->flags);
	ret += sqlite3_bind_int(stmt, 9, photo->latitude);
	ret += sqlite3_bind_int(stmt, 10, photo->longitude);
	ret += sqlite3_bind_text(stmt, 11, photo->city, -1, SQLITE_STATIC);
	ret += sqlite3_bind_text(stmt, 12, photo->poi, -1, SQLITE_STATIC);

	if (ret != SQLITE_OK) {
		fprintf(stderr, "bind insert photo failed\n");
		goto out;
	}

	ret = sqlite3_step(stmt);
	if (ret != SQLITE_DONE) {
		fprintf(stderr, "insert photo failed: %s\n", sql_err(db, ret));
		goto out;
	}

	photo->photo_id = sqlite3_last_insert_rowid(db);
	err = 0;

out:
	sqlite3_finalize(stmt);
	return err;
}

static int batch_insert(struct sqlite3 *db, int seq, int count)
{
	int ret;
	int i;
	unsigned ts;

	dprintf(1, "%-20s: ", "begin transaction");
	ts = tsus();
	ret = sqlite_begin_transaction(db);
	dprintf(1, "%u us\n", tsus() - ts);

	if (ret < 0)
		return -1;

	dprintf(1, "%-20s: ", "insert");

	ts = tsus();

	for (i = 0; i < count; i++) {
		struct sqlite_photo photo = {
			.parent = 0,
			.name = "test",
			.city = "shenzhen",
			.poi = "park",
		};

		ret = sqlite_insert_photo(db, &photo);
		if (ret < 0)
			return -1;
	}

	dprintf(1, "%u us\n", tsus() - ts);

	dprintf(1, "%-20s: ", "end transaction");

	ts = tsus();

	ret = sqlite_end_transaction(db);
	if (ret < 0)
		return -1;

	dprintf(1, "%u us\n", tsus() - ts);
	return 0;
}

int main(int argc, char **argv)
{
	struct sqlite3 *db;
	int count = 1;
	int repeat = 1;
	int i;
	unsigned ts;

	if (argc > 1)
		count = atoi(argv[1]);
	if (argc > 2)
		repeat = atoi(argv[2]);


	dprintf(1, "%-20s: ", "open");

	ts = tsus();
	db = sqlite_open("test.db");
	if (!db)
		return 1;

	dprintf(1, "%u us\n", tsus() - ts);

	for (i = 0; i < repeat; i++) {
		int ret;

		dprintf(1, "---\n");

		ret = batch_insert(db, i, count);
		if (ret < 0)
			break;
	}

	dprintf(1, "---\n");
	dprintf(1, "%-20s: ", "close");

	ts = tsus();
	sqlite_close(db);
	printf("%u us\n", tsus()-ts);

	return 0;
}


Copyright © linuxdev.cc 2017-2024. Some Rights Reserved.