Skip to content

Commit

Permalink
Merge pull request #11 from maxpert/nats
Browse files Browse the repository at this point in the history
Moving to NATS instead of dragonboat due to it's inefficiencies.
  • Loading branch information
maxpert authored Sep 25, 2022
2 parents 66e7964 + 342708c commit 851d6a1
Show file tree
Hide file tree
Showing 13 changed files with 299 additions and 2,258 deletions.
67 changes: 27 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,33 @@ A distributed SQLite replicator.

## What is it useful for right now?
If you are using SQLite as ephemeral storage, or a scenario where eventual consistency is fine for you.
Marmot can give you a solid replication between your nodes as Marmot builds on top of fault-tolerant
consensus protocol ([Multi-Raft](https://tikv.org/deep-dive/scalability/multi-raft/)), thus allowing
robust recovery and replication. This means if you are running a medium traffic website based on
SQLite you should be easily able to handle load without any problems. Read heavy workloads won't
be bottle-necked at all as Marmot serves as a side car letting you build replication cluster
without making any changes to your application code, and allows you to keep using to your
SQLite database file. In a typical setting your setup would look like this:
Marmot can give you a solid replication between your nodes as Marmot builds on top of fault-tolerant
[NATS](https://nats.io/), thus allowing robust recovery and replication. This means if you are
running a medium traffic website based on SQLite you should be easily able to handle load
without any problems. Read heavy workloads won't be bottle-necked at all as Marmot serves
as a side car letting you build replication cluster without making any changes to your
application code, and allows you to keep using to your SQLite database file.

![image](https://user-images.githubusercontent.com/22441/190715676-8b785596-f267-49a3-aa27-21afbe74d0be.png)
## Dependencies
Starting 0.4+ Marmot depends on [nats-server](https://nats.io/download/).

## Production status

**MARMOT IS IN RELEASE CANDIDATE STAGES, WHICH MEANS MOST OF ISSUES HAVE BEEN IRONED OUT, HOWEVER THERE MIGHT BE EDGE CASE RACE CONDITIONS THAT MIGHT CAUSE PROBLEMS. FOR NOW WE RECOMMEND USAGE FOR EPHEMERAL STORAGE USE CASE ONLY WHERE LOOSING DATA, OR CORRUPTION OF DB FILE IS NOT AN ISSUE.**
**MARMOT IS NOT READY FOR PRODUCTION USAGE**

Right now it's being used for ephemeral cache storage in production services, on a very read heavy site. This easily replicates cache values across
the cluster, keeping a fast local copy of cache database.
Right now it's being used for ephemeral cache storage in production services, on a very read heavy site.
This easily replicates cache values across the cluster, keeping a fast local copy of cache database.

## Features

- MultiRaft based consensus with ability to manually move a cluster around
- Built on top of NATS, abstracting stream distribution and replication
- Bidirectional replication with almost masterless architecture
- Ability to snapshot and fully recover from those snapshots
- SQLite based log storage

To be implemented for next GA:
- Command batching + compression for speeding up bulk load / commit commands to propagate quickly
- On the fly join and cluster rebalancing
- Per node database level command ordering
- Gossip and SRV based node discovery
- CDC output to NATS and log file

## Running

Expand All @@ -46,45 +43,35 @@ go build -o build/marmot ./marmot.go
Make sure you have 2 SQLite DBs with exact same schemas (ideally exact same state):

```shell
rm -rf /tmp/raft # Clear out previous raft state, only do for cold start
build/marmot -bootstrap [email protected]:8162 -bind 127.0.0.1:8161 -bind-pane localhost:6001 -node-id 1 -db-path /tmp/cache-1.db
build/marmot -bootstrap [email protected]:8161 -bind 127.0.0.1:8162 -bind-pane localhost:6002 -node-id 2 -db-path /tmp/cache-2.db
nats-server --jetstream
build/marmot -nats-url nats://127.0.0.1:4222 -node-id 1 -db-path /tmp/cache-1.db
build/marmot -nats-url nats://127.0.0.1:4222 -node-id 2 -db-path /tmp/cache-2.db
```

## Demos
Demos for `v0.4.x`:
- Scaling Pocketbase with Marmot [Coming soon]

Demos for `v0.3.x` (Legacy) with PocketBase `v0.7.5`:
- [Scaling Pocketbase with Marmot](https://youtube.com/video/VSa-VJso050)
- [Scaling Pocketbase with Marmot - Follow up](https://www.youtube.com/watch?v=Zapupe_FREc)

## Documentation

Marmot is picks simplicity, and lesser knobs to configure by choice. Here are command line options you can use to
Marmot picks simplicity, and lesser knobs to configure by choice. Here are command line options you can use to
configure marmot:

- `cleanup` - Just cleanup and exit marmot. Useful for scenarios where you are performing a cleanup of hooks and
change logs. (default: `false`)
- `db-path` - Path to DB from which given tables will be replicated. These tables can be specified in `replicate`
option. (default: `/tmp/marmot.db`)
- `replicate` - A comma seperated list of tables to replicate with no spaces in between (e.g. news,history)
(default: [empty]) **DEPRECATED after 0.3.11 now all tables are parsed and listed, this is required for
snapshots to recover quickly**
- `db-path` - Path to DB from which all tables will be replicated (default: `/tmp/marmot.db`)
- `node-id` - An ID number (positive integer) to represent an ID for this node, this is required to be a unique
number per node, and used for consensus protocol. (default: 0)
- `bind` - A `host:port` combination of listen for other nodes on (default: `0.0.0.0:8610`)
- `raft-path` - Path of directory to save consensus related logs, states, and snapshots (default: `/tmp/raft`)
- `log-replicas` - Number of copies to be committed for single change log. By default it set to `floor(shards/2) + 1`.
- `shards` - Number of shards over which the database tables replication will be distributed on. It serves as mechanism for
consistently hashing leader from Hash(<table_name> + <primary/composite_key>) for all the nodes. These partitions can
be assigned to various nodes in cluster which allows you to distribute leadership load over multiple nodes rather
than single master. By default, there are 16 shards which means you should be easily able to have upto 16 leader
nodes. Beyond that you should use this flag to create a bigger cluster. Higher shards also mean more disk space,
and memory usage per node. Marmot has basic a very rough version of adding shards, via control pane, but it
needs more polishing to make it idiot-proof.
- `bootstrap` - A comma seperated list of initial bootstrap nodes `<node_id>@<ip>:<port>` (e.g.
`[email protected]:8162,[email protected]:8163` will specify 2 bootstrap nodes for cluster).
- `bind-pane` - A `host:port` combination for control panel address (default: `localhost:6010`). All the endpoints
are basic auth protected which should be set via `AUTH_KEY` env variable (e.g. `AUTH_KEY='Basic ...'`). This
address should not be a public accessible, and should be only used for cluster management. This in future
will serve as full control panel hosting and cluster management API. **EXPERIMENTAL**
consistently hashing JetStream from `Hash(<table_name> + <primary/composite_key>)`. This will allow NATS servers to
distribute load and scale for wider clusters. Look at internal docs on how these JetStreams and subjects are named.
- `nats-url` - URL string for NATS servers, it can also point to multipule servers as long as its comma separated (e.g.
`nats://user:[email protected]:4222` or `nats://user:pass@host-a:4222, nats://user:pass@host-b:4222`)
- `verbose` - Specify if system should dump debug logs on console as well. Only use this for debugging.

For more details and internal workings of marmot [go to these docs](https://github.com/maxpert/marmot/blob/master/docs/overview.md).
Expand All @@ -94,8 +81,8 @@ Right now there are a few limitations on current solution:
- You can't watch tables selectively on a DB. This is due to various limitations around snapshot and restore mechanism.
- WAL mode required - since your DB is going to be processed by multiple process the only way to have multi-process
changes reliably is via WAL.
- Downloading snapshots of database is still WIP. However, if you can have start off node with same copies of DB it will
work flawlessly.
- Downloading snapshots of database is still WIP. However, it doesn't affect replication functionality as everything
is upsert or delete. Right snapshots are not restore, or initialized.
- Marmot is eventually consistent. This simply means rows can get synced out of order, and `SERIALIZABLE` assumptions
on transactions might not hold true anymore.

Expand Down
43 changes: 24 additions & 19 deletions db/change_log.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,29 @@ func (conn *SqliteStreamDB) Replicate(event *ChangeLogEvent) error {
return nil
}

func (conn *SqliteStreamDB) DeleteChangeLog(event *ChangeLogEvent) (bool, error) {
metaTableName := conn.metaTable(event.TableName, changeLogName)
rs, err := conn.Delete(metaTableName).
Where(goqu.Ex{
"state": Published,
"id": event.Id,
}).
Prepared(true).
Executor().
Exec()

if err != nil {
return false, err
}

count, err := rs.RowsAffected()
if err != nil {
return false, err
}

return count > 0, nil
}

func (conn *SqliteStreamDB) CleanupChangeLogs() (int64, error) {
total := int64(0)
for name := range conn.watchTablesSchema {
Expand Down Expand Up @@ -204,25 +227,7 @@ func (conn *SqliteStreamDB) publishChangeLog() {
conn.publishLock.Lock()
processed := uint64(0)

// TODO: Move cleanup logic to time based cleanup
// In order to reduce frequent writes, change the logic below
// to only do in place updates, and the periodically do
// table cleanup.
defer func() {
if processed > uint64(0) {
cnt, err := conn.CleanupChangeLogs()
if err != nil {
log.Warn().Err(err).Msg("Unable to cleanup change logs")
} else if cnt > int64(0) {
log.Debug().
Int64("cleaned", cnt).
Uint64("published", processed).
Msg("Cleaned up change log")
}
}

conn.publishLock.Unlock()
}()
defer conn.publishLock.Unlock()

for tableName := range conn.watchTablesSchema {
var changes []*changeLogEntry
Expand Down
47 changes: 6 additions & 41 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,55 +7,20 @@ require (
github.com/doug-martin/goqu/v9 v9.18.0
github.com/fsnotify/fsnotify v1.5.4
github.com/fxamacker/cbor/v2 v2.4.0
github.com/gin-gonic/gin v1.4.0
github.com/lni/dragonboat/v3 v3.3.5
github.com/mattn/go-sqlite3 v1.14.15
github.com/nats-io/nats.go v1.16.1-0.20220906180156-a1017eec10b0
github.com/rs/zerolog v1.27.0
github.com/samber/lo v1.27.0
)

require (
github.com/VictoriaMetrics/metrics v1.6.2 // indirect
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da // indirect
github.com/cockroachdb/errors v1.9.0 // indirect
github.com/cockroachdb/logtags v0.0.0-20211118104740-dabe8e521a4f // indirect
github.com/cockroachdb/pebble v0.0.0-20210331181633-27fc006b8bfb // indirect
github.com/cockroachdb/redact v1.1.3 // indirect
github.com/getsentry/sentry-go v0.12.0 // indirect
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.3-0.20201103224600-674baa8c7fc3 // indirect
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-immutable-radix v1.0.0 // indirect
github.com/hashicorp/go-msgpack v0.5.3 // indirect
github.com/hashicorp/go-multierror v1.0.0 // indirect
github.com/hashicorp/go-sockaddr v1.0.0 // indirect
github.com/hashicorp/golang-lru v0.5.0 // indirect
github.com/hashicorp/memberlist v0.2.2 // indirect
github.com/json-iterator/go v1.1.9 // indirect
github.com/juju/ratelimit v1.0.2-0.20191002062651-f60b32039441 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/lni/goutils v1.3.0 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/miekg/dns v1.1.26 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rogpeppe/go-internal v1.8.1 // indirect
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
github.com/ugorji/go/codec v1.1.7 // indirect
github.com/valyala/fastrand v1.0.0 // indirect
github.com/valyala/histogram v1.0.1 // indirect
github.com/nats-io/nats-server/v2 v2.9.1 // indirect
github.com/nats-io/nkeys v0.3.0 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/x448/float16 v0.8.4 // indirect
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0 // indirect
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
google.golang.org/protobuf v1.26.0 // indirect
gopkg.in/go-playground/validator.v8 v8.18.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 // indirect
)
Loading

0 comments on commit 851d6a1

Please sign in to comment.