• Stars
    star
    113
  • Rank 309,167 (Top 7 %)
  • Language
    Go
  • License
    Apache License 2.0
  • Created almost 3 years ago
  • Updated 4 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A Kafka log inspired in-memory and append-only data structure

Go Reference Tests Latest Release Go Report Card codecov go.mod Go version Mentioned in Awesome Go

About

tl;dr

An easy to use, lightweight, thread-safe and append-only in-memory data structure modeled as a Log.

The Log also serves as an abstraction and building block. See sharded.Log for an implementation of a sharded variant of memlog.Log.

❌ Note: this package is not about providing an in-memory logging library. To read more about the ideas behind memlog please see "The Log: What every software engineer should know about real-time data's unifying abstraction".

Motivation

I keep hitting the same user story (use case) over and over again: one or more clients connected to my application wanting to read an immutable stream of data, e.g. events or sensor data, in-order, concurrently (thread-safe) and asynchronously (at their own pace) and in a resource (memory) efficient way.

There's many solutions to this problem, e.g. exposing some sort of streaming API (gRPC, HTTP/REST long-polling) based on custom logic using Go channels or an internal ring buffer, or putting data into an external platform like Kafka, Redis Streams or RabbitMQ Streams.

The challenges I faced with these solutions were that either they were too complex (or simply overkill) for my problem. Or, the system I had to integrate with and read data from did not have a nice streaming API or Go SDK, thus repeating myself writing complex internal caching, buffering and concurrency handling logic for the client APIs.

I looked around and could not find a simple and easy to use Go library for this problem, so I created memlog: an easy to use, lightweight (in-memory), thread-safe, append-only log inspired by popular streaming systems with a minimal API using Go's standard library primitives 🤩

💡 For an end-to-end API modernization example using memlog see the vsphere-event-streaming project, which transforms a SOAP-based events API into an HTTP/REST streaming API.

Usage

	ml, _ := memlog.New(ctx) // create log
	offset, _ := ml.Write(ctx, []byte("Hello World")) // write some data
	record, _ := ml.Read(ctx, offset) // read back data
	fmt.Printf(string(record.Data)) // prints "Hello World"

The memlog API is intentionally kept minimal. A new Log is constructed with memlog.New(ctx, options...). Data as []byte is written to the log with Log.Write(ctx, data).

The first write to the Log using default Options starts at position (Offset) 0. Every write creates an immutable Record in the Log. Records are purged from the Log when the history segment is replaced (see notes below).

The earliest and latest Offset available in a Log can be retrieved with Log.Range(ctx).

A specified Record can be read with Log.Read(ctx, offset).

💡 Instead of manually polling the Log for new Records, the streaming API Log.Stream(ctx, startOffset) should be used.

(Not) one Log to rule them all

One is not constrained by just creating one Log. For certain use cases, creating multiple Logs might be useful. For example:

  • Manage completely different data sets/sizes in the same process
  • Setting different Log sizes (i.e. retention times), e.g. premium users will have access to a larger history of Records
  • Partitioning input data by type or key

💡 For use cases where you want to order the log by key(s), consider using the specialised sharded.Log.

Full Example

package main

import (
	"context"
	"fmt"
	"os"

	"github.com/embano1/memlog"
)

func main() {
	ctx := context.Background()
	l, err := memlog.New(ctx)
	if err != nil {
		fmt.Printf("create log: %v", err)
		os.Exit(1)
	}

	offset, err := l.Write(ctx, []byte("Hello World"))
	if err != nil {
		fmt.Printf("write: %v", err)
		os.Exit(1)
	}

	fmt.Printf("reading record at offset %d\n", offset)
	record, err := l.Read(ctx, offset)
	if err != nil {
		fmt.Printf("read: %v", err)
		os.Exit(1)
	}

	fmt.Printf("data says: %s", record.Data)

	// reading record at offset 0
	// data says: Hello World
}

Purging the Log

The Log is divided into an active and history segment. When the active segment is full (configurable via WithMaxSegmentSize()), it is sealed (i.e. read-only) and becomes the history segment. A new empty active segment is created for writes. If there is an existing history, it is replaced, i.e. all Records are purged from the history.

See pkg.go.dev for the API reference and examples.

A stateless Log? You gotta be kidding!

True, it sounds like an oxymoron. Why would someone use (build) an in-memory append-only log that is not durable?

I'm glad you asked 😀

This library certainly is not intended to replace messaging, queuing or streaming systems. It was built for use cases where there exists a durable data/event source, e.g. a legacy system, REST API, database, etc. that can't (or should not) be changed. But the requirement being that the (source) data should be made available over a streaming-like API, e.g. gRPC or processed by a Go application which requires the properties of a Log.

memlog helps as it allows to bridge between these different APIs and use cases as a building block to extract and store data Records from an external system into an in-memory Log (think ordered cache).

These Records can then be internally processed (lightweight ETL) or served asynchronously, in-order (Offset-based) and concurrently over a modern streaming API, e.g. gRPC or HTTP/REST (chunked encoding via long polling), to remote clients.

As another example of such an in-memory log-structured design, DDlog follows a similar approach, where a DDlog program is used in conjunction with a persistent database, with database records being fed to DDlog as ground facts.

Checkpointing

Given the data source needs to be durable in this design, one can optionally build periodic checkpointing logic using the Record Offset as the checkpoint value.

💡 When running in Kubernetes, kvstore provides a nice abstraction on top of a ConfigMap for such requirements.

If the memlog process crashes, it can then resume from the last checkpointed Offset, load the changes since then from the source and resume streaming.

💡 This approach is quiet similar to the Kubernetes ListerWatcher() pattern. See memlog_test.go for some inspiration.

Benchmark

I haven't done any extensive benchmarking or code optimization. Feel free to chime in and provide meaningful feedback/optimizations.

One could argue, whether using two slices (active and history data []Record as part of the individual segments) is a good engineering choice, e.g. over using a growable slice as an alternative.

The reason I went for two segments was that for me dividing the Log into multiple segments with fixed size (and capacity) was easier to reason about in the code (and I followed my intuition from how log-structured data platforms do it). I did not inspect the Go compiler optimizations, e.g. it might actually be smart and create one growable slice under the hood. 🤓

These are some results on my MacBook using a log size of 1,000 (records), i.e. where the Log history is constantly purged and new segments (slices) are created.

go test -v -run=none -bench=. -cpu 1,2,4,8,16 -benchmem
goos: darwin
goarch: amd64
pkg: github.com/embano1/memlog
cpu: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
BenchmarkLog_write
BenchmarkLog_write              11107804               103.0 ns/op            89 B/op          1 allocs/op
BenchmarkLog_write-2            11115896               107.1 ns/op            89 B/op          1 allocs/op
BenchmarkLog_write-4            11419497               105.7 ns/op            89 B/op          1 allocs/op
BenchmarkLog_write-8            10253677               109.6 ns/op            89 B/op          1 allocs/op
BenchmarkLog_write-16           10865994               107.7 ns/op            89 B/op          1 allocs/op
BenchmarkLog_read
BenchmarkLog_read               24461548                49.49 ns/op           32 B/op          1 allocs/op
BenchmarkLog_read-2             25002574                46.63 ns/op           32 B/op          1 allocs/op
BenchmarkLog_read-4             23829378                47.47 ns/op           32 B/op          1 allocs/op
BenchmarkLog_read-8             22936821                47.47 ns/op           32 B/op          1 allocs/op
BenchmarkLog_read-16            24121807                48.25 ns/op           32 B/op          1 allocs/op
PASS
ok      github.com/embano1/memlog       12.541s

More Repositories

1

codeconnect-vm-operator

Toy VM Operator using kubebuilder for educational purposes presented at VMware Code Connect 2020
Go
102
star
2

ci-demo-app

Demo app showing an end-to-end CI pipeline with Github Actions, goreleaser and ko
Go
98
star
3

gotutorials

Personal repo for random Go stuff
Go
15
star
4

dontdisturb

Small program to enable "Do not disturb" mode on OSX for X minutes
Go
15
star
5

vsphere-event-streaming

Prototype to show how to transform an existing (SOAP) API into a modern streaming API
Go
9
star
6

vsphere-alarm-server

Kubernetes application which enriches vSphere Alarm Events with AlarmInfo details
Go
8
star
7

k8s-meetup-04-05-2023

Code for the E2E Framework talk at Kubernetes Meetup Leipzig
Go
8
star
8

kopf-operator-vmworld

vSphere VM operator for educational purposes using the Python Kopf Operator Framework
Python
8
star
9

go-cicd

The World's most basic CI/CD Tool
Go
7
star
10

funcy-ops

Introduction to Functional Options in Go
Go
6
star
11

tw

A Minimalistic Twitter CLI Client
Go
6
star
12

Vagrant_Docker_Enterprise

Shell
4
star
13

golangmulti

Test repo for multi-stage Docker builds and Golang
Go
4
star
14

k8s-webhook-validator

Example for Kubernetes Dynamic Admission Webhooks using kubewebhook from Slok
Go
4
star
15

faastagger

Go
3
star
16

aktion

Github Actions playground
Go
3
star
17

gotagfn

OpenFaaS Function for Tagging VMs based on vCenter Events
Go
3
star
18

whalesay-operator

Introduction to the Operator-SDK for Kubernetes
Go
3
star
19

govcsim

Build and Dockerfile for https://github.com/vmware/govmomi/tree/master/vcsim
Dockerfile
3
star
20

waitgroup

A simple wrapper around sync.WaitGroup with support for specifying a timeout
Go
2
star
21

go-meetup-lej-04-2020

Examples, references and snippets for the short (virtual) presentation at the Leipzig Golang Meetup.
Go
2
star
22

pytagfn

OpenFaaS Function for Tagging VMs based on vCenter Events
Python
2
star
23

blog

Configuration for Hugo
2
star
24

yamlcmd

Proof of work for a good colleague to use yaml files as os.exec (cmd) input. No complains please ,)
Go
1
star
25

broadcaster

Simple broadcasting example using Go channels
Go
1
star
26

dropbox-link-cleaner

Remove all public links from your Dropbox
Python
1
star
27

pubsub_autoscaler

Tiny microservices (publish/ subscribe/ autoscale) built for a Kubernetes environment. Mainly for demo purposes.
Go
1
star