最近开发了一个相册模块,用户上传照片后,通过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。
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;
}