Skip to content

Commit

Permalink
Fixes for SQLite concurrency issues
Browse files Browse the repository at this point in the history
In some busy workloads various SQLite operations would fail due to
"locked tables"; most of this is related to the session code:

- On every pageview a new session is either read (OK) or written (LOCK!)
- At the same time, the cron may be writing data to the db (LOCK!)

On smaller instances this isn't much of an issue since everything is
fast enough to not lock for too long, but on longer instances this can
be a problem.

Setting SetMaxOpenConns(1) solves this by limiting the connections
writing to the database.

Also set the default journal mode to WAL, which should give better
performance. Both of this is done in the zgo.at/zdb update.

Add "goatcounter help db" to document database usage a bit better.
  • Loading branch information
arp242 committed Jul 4, 2020
1 parent 8e2c83b commit 8ce6015
Show file tree
Hide file tree
Showing 9 changed files with 186 additions and 116 deletions.
22 changes: 11 additions & 11 deletions cmd/goatcounter/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,25 @@ Create a new site and user.
Required flags:
-domain Domain to host e.g. "stats.example.com". The site will be
available on this domain only, so "stats.example.com" won't be
available on "localhost".
-domain Domain to host e.g. "stats.example.com". The site will be
available on this domain only, so "stats.example.com" won't be
available on "localhost".
-email Your email address. Will be required to login.
-email Your email address. Will be required to login.
Other flags:
-password Password to log in; will be asked interactively if omitted.
-password Password to log in; will be asked interactively if omitted.
-parent Parent site; either as ID or domain.
-parent Parent site; either as ID or domain.
-db Database connection string. Use "sqlite://<dbfile>" for SQLite,
or "postgres://<connect string>" for PostgreSQL
Default: sqlite://db/goatcounter.sqlite3
-db Database connection: "sqlite://<file>" or "postgres://<connect>"
See "goatcounter help db" for detailed documentation. Default:
sqlite://db/goatcounter.sqlite3?_busy_timeout=200&_journal_mode=wal&cache=shared
-createdb Create the database if it doesn't exist yet; only for SQLite.
-createdb Create the database if it doesn't exist yet; only for SQLite.
-debug Modules to debug, comma-separated or 'all' for all modules.
-debug Modules to debug, comma-separated or 'all' for all modules.
`

func create() (int, error) {
Expand Down
67 changes: 66 additions & 1 deletion cmd/goatcounter/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,66 @@ Show help; use "help commands" to dispay detailed help for a command, or "help
all" to display everything.
`

const helpDatabase = `
GoatCounter can use SQLite and PostgreSQL. All commands accept the -db flag to
customize the database connection string.
You can select a database engine by using "sqlite://[..]" for SQLite, or
"postgresql://[..]" (or "postgres://[..]") for PostgreSQL.
There are no plans to support other database engines such as MySQL/MariaDB.
SQLite
This is the default database engine as it has no dependencies, and for most
small to medium usage it should be more than fast enough.
The SQLite connection string is usually just a filename, optionally prefixed
with "file:". Parameters can be added as a URL query string after a ?:
-db 'sqlite://mydb.sqlite?param=value&other=value'
See the go-sqlite3 documentation for a list of supported parameters:
https://github.com/mattn/go-sqlite3/#connection-string
_journal_mode=wal is always added unless explicitly overridden. Generally
speaking using a Write-Ahead-Log is more suitable for GoatCounter than the
default DELETE journaling.
The database is automatically created for the "serve" command, but you need
to add -createdb to any other commands to create the database. This is to
prevent accidentally operating on the wrong (new) database.
PostgreSQL
PostgreSQL provides better performance for large instances. If you have
millions of pageviews then PostgreSQL is probably a better choice.
The PostgreSQL connection string can either be as "key=value" or as an URL;
the following are identical:
-db 'postgresql://user=pqgotest dbname=pqgotest sslmode=verify-full'
-db 'postgresql://pqgotest:password@localhost/pqgotest?sslmode=verify-full'
See the pq documentation for a list of supported parameters:
https://pkg.go.dev/github.com/lib/pq?tab=doc#hdr-Connection_String_Parameters
You may want to consider lowering the "seq_page_cost" parameter; the query
planner tends to prefer seq scans instead of index scans for some operations
with the default of 4, which is much slower.
I found that 0.5 is a fairly good setting, you can set it in your
postgresql.conf file, or just for one database with:
alter database goatcounter set seq_page_cost=.5
The database isn't automatically created for PostgreSQL, you'll have to
manually create it first:
createdb goatcounter
psql goatcounter < ./db/schema.pgsql
`

func help() (int, error) {
if len(os.Args) == 2 {
fmt.Fprint(stdout, usage[""])
Expand All @@ -24,7 +84,12 @@ func help() (int, error) {

if os.Args[2] == "all" {
fmt.Fprint(stdout, usage[""], "\n")
for _, h := range []string{"help", "version", "migrate", "serve", "create", "reindex", "monitor"} {
for _, h := range []string{
"help", "version",
"migrate", "create", "serve",
"reindex", "monitor",
"database",
} {
head := fmt.Sprintf("─── Help for %q ", h)
fmt.Fprintf(stdout, "%s%s\n\n", head, strings.Repeat("─", 80-utf8.RuneCountInString(head)))
fmt.Fprint(stdout, usage[h], "\n")
Expand Down
37 changes: 21 additions & 16 deletions cmd/goatcounter/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,16 @@ var (
)

var usage = map[string]string{
"": usageTop,
"help": usageHelp,
"serve": usageServe,
"create": usageCreate,
"migrate": usageMigrate,
"saas": usageSaas,
"reindex": usageReindex,
"monitor": usageMonitor,
"": usageTop,
"help": usageHelp,
"serve": usageServe,
"create": usageCreate,
"migrate": usageMigrate,
"saas": usageSaas,
"reindex": usageReindex,
"monitor": usageMonitor,
"database": helpDatabase,
"db": helpDatabase,

"version": `
Show version and build information. This is printed as key=value, separated by
Expand All @@ -62,17 +64,20 @@ Usage: goatcounter [command] [flags]
Commands:
help Show help; use "help <topic>" or "help all" for more details.
version Show version and build information and exit.
migrate Run database migrations.
create Create a new site and user.
serve Start HTTP server.
help Show help; use "help <topic>" or "help all" for more details.
version Show version and build information and exit.
migrate Run database migrations.
create Create a new site and user.
serve Start HTTP server.
Advanced commands:
reindex Re-create the cached statistics (*_stats tables) from the hits.
This is generally rarely needed and mostly a development tool.
monitor Monitor for pageviews.
reindex Recreate the index tables (*_stats, *_count) from the hits.
monitor Monitor for pageviews.
Extra help topics:
db Documentation on the -db flag.
See "help <topic>" for more details for the command.
`
Expand Down
10 changes: 5 additions & 5 deletions cmd/goatcounter/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ Run database migrations and exit.
Flags:
-db Database connection string. Use "sqlite://<dbfile>" for SQLite,
or "postgres://<connect string>" for PostgreSQL
Default: sqlite://db/goatcounter.sqlite3
-db Database connection: "sqlite://<file>" or "postgres://<connect>"
See "goatcounter help db" for detailed documentation. Default:
sqlite://db/goatcounter.sqlite3?_busy_timeout=200&_journal_mode=wal&cache=shared
-createdb Create the database if it doesn't exist yet; only for SQLite.
-createdb Create the database if it doesn't exist yet; only for SQLite.
-debug Modules to debug, comma-separated or 'all' for all modules.
-debug Modules to debug, comma-separated or 'all' for all modules.
Positional arguments are names of database migrations, either as just the name
("2020-01-05-2-foo") or as the file path ("./db/migrate/sqlite/2020-01-05-2-foo.sql").
Expand Down
12 changes: 6 additions & 6 deletions cmd/goatcounter/monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ log if it's 0.
Flags:
-db Database connection string. Use "sqlite://<dbfile>" for SQLite,
or "postgres://<connect string>" for PostgreSQL
Default: sqlite://db/goatcounter.sqlite3
-db Database connection: "sqlite://<file>" or "postgres://<connect>"
See "goatcounter help db" for detailed documentation. Default:
sqlite://db/goatcounter.sqlite3?_busy_timeout=200&_journal_mode=wal&cache=shared
-debug Modules to debug, comma-separated or 'all' for all modules.
-debug Modules to debug, comma-separated or 'all' for all modules.
-period Check every n seconds. Default: 120.
-period Check every n seconds. Default: 120.
-site Limit the check to just one site; makes the query faster.
-site Limit the check to just one site; makes the query faster.
`

func monitor() (int, error) {
Expand Down
30 changes: 15 additions & 15 deletions cmd/goatcounter/reindex.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,28 +43,28 @@ Avoiding race conditions
Flags:
-db Database connection string. Use "sqlite://<dbfile>" for SQLite,
or "postgres://<connect string>" for PostgreSQL
Default: sqlite://db/goatcounter.sqlite3
-db Database connection: "sqlite://<file>" or "postgres://<connect>"
See "goatcounter help db" for detailed documentation. Default:
sqlite://db/goatcounter.sqlite3?_busy_timeout=200&_journal_mode=wal&cache=shared
-debug Modules to debug, comma-separated or 'all' for all modules.
-debug Modules to debug, comma-separated or 'all' for all modules.
-pause Number of seconds to pause after each day, to give the server
some breathing room on large sites. Default: 0.
-pause Number of seconds to pause after each day, to give the server
some breathing room on large sites. Default: 0.
-since Reindex only statistics since this date instead of all of them;
as year-month-day in UTC.
-since Reindex only statistics since this date instead of all of them;
as year-month-day in UTC.
-to Reindex only statistics up to and including this day; as
year-month-day in UTC. The default is yesterday.
-to Reindex only statistics up to and including this day; as
year-month-day in UTC. The default is yesterday.
-table Which tables to reindex: hit_stats, hit_counts, browser_stats,
system_stats, location_stats, ref_counts, size_stats, or all
(default).
-table Which tables to reindex: hit_stats, hit_counts, browser_stats,
system_stats, location_stats, ref_counts, size_stats, or all
(default).
-site Only reindex this site ID. Default is to reindex all.
-site Only reindex this site ID. Default is to reindex all.
-quiet Don't print progress.
-quiet Don't print progress.
`

func reindex() (int, error) {
Expand Down
100 changes: 49 additions & 51 deletions cmd/goatcounter/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,85 +33,83 @@ with -dev.
Flags:
-static Serve static files from a diffent domain, such as a CDN or
cookieless domain. Default: not set.
-static Serve static files from a diffent domain, such as a CDN or
cookieless domain. Default: not set.
-port Port your site is publicly accessible on. Only needed if it's
not 80 or 443.
-port Port your site is publicly accessible on. Only needed if it's
not 80 or 443.
` + serveAndSaasFlags

const serveAndSaasFlags = `
-db Database connection string. Use "sqlite://<dbfile>" for SQLite,
or "postgres://<connect string>" for PostgreSQL
Default: sqlite://db/goatcounter.sqlite3
-db Database connection: "sqlite://<file>" or "postgres://<connect>"
See "goatcounter help db" for detailed documentation. Default:
sqlite://db/goatcounter.sqlite3?_busy_timeout=200&_journal_mode=wal&cache=shared
-listen Address to listen on. Default: localhost:8081
-listen Address to listen on. Default: localhost:8081
-dev Start in "dev mode".
-dev Start in "dev mode".
-tls Serve over tls. This is a comma-separated list with any of:
-tls Serve over tls. This is a comma-separated list with any of:
none Don't serve any TLS.
none Don't serve any TLS.
path/to/file.pem TLS certificate and keyfile, in one file.
path/to/file.pem TLS certificate and keyfile, in one file.
acme[:cache] Create TLS certificates with ACME, this can
optionally followed by a : and a cache
directory name (default: acme-secrets).
acme[:cache] Create TLS certificates with ACME, this can
optionally followed by a : and a cache
directory name (default: acme-secrets).
tls Accept TLS connections on -listen.
tls Accept TLS connections on -listen.
rdr Redirect port 80 to the -listen port. ACME
verification requires the server to be
available on port 80.
rdr Redirect port 80 to the -listen port. ACME
verification requires the server to be
available on port 80.
Examples:
Examples:
acme Create ACME certs but serve HTTP,
useful when serving behind proxy
which can use the certs.
acme Create ACME certs but serve HTTP,
useful when serving behind proxy
which can use the certs.
acme:/home/gc/.acme As above, but with custom cache dir.
acme:/home/gc/.acme As above, but with custom cache dir.
./example.com.pem,tls,rdr Always use the certificate in the
file, serve over TLS, and redirect
port 80.
./example.com.pem,tls,rdr Always use the certificate in the
file, serve over TLS, and redirect
port 80.
Default: "acme,tls,rdr", blank when -dev is given.
Default: "acme,tls,rdr", blank when -dev is given.
-smtp SMTP server, as URL (e.g. "smtp://user:pass@server").
-smtp SMTP server, as URL (e.g. "smtp://user:pass@server").
A special value of "stdout" means no emails will be sent and
emails will be printed to stdout only. This is the default.
A special value of "stdout" means no emails will be sent and
emails will be printed to stdout only. This is the default.
If this is blank emails will be sent without using a relay;
this should work fine, but deliverability will usually be worse
(i.e. it will be more likely to end up in the spam box). This
usually requires rDNS properly set up, and GoatCounter will
*not* retry on errors. Using stdout, a local smtp relay, or a
mailtrap.io box is probably better unless you really know what
you're doing.
If this is blank emails will be sent without using a relay; this
should work fine, but deliverability will usually be worse (i.e.
it will be more likely to end up in the spam box). This usually
requires rDNS properly set up, and GoatCounter will *not* retry
on errors. Using stdout, a local smtp relay, or a mailtrap.io box
is probably better unless you really know what you're doing.
-email-from From: address in emails. Default: <user>@<hostname>
-email-from From: address in emails. Default: <user>@<hostname>
-errors What to do with errors; they're always printed to stderr.
-errors What to do with errors; they're always printed to stderr.
mailto:to_addr[,from_addr] Email to this address; the
from_addr is optional and sets
the From: address. The default is
to use the same as the to_addr.
mailto:to_addr[,from_addr] Email to this address; the
from_addr is optional and sets the
From: address. The default is to
use the same as the to_addr.
Default: not set.
Default: not set.
-debug Modules to debug, comma-separated or 'all' for all modules.
-debug Modules to debug, comma-separated or 'all' for all modules.
-automigrate Automatically run all pending migrations on startup.
-automigrate Automatically run all pending migrations on startup.
Environment:
TMPDIR Directory for temporary files; only used to store CSV exports
at the moment. On Windows it will use the first non-empty value
of %TMP%, %TEMP%, and %USERPROFILE%.
TMPDIR Directory for temporary files; only used to store CSV exports at
the moment. On Windows it will use the first non-empty value of
%TMP%, %TEMP%, and %USERPROFILE%.
`

func serve() (int, error) {
Expand Down
Loading

0 comments on commit 8ce6015

Please sign in to comment.