Skip to content

Reduce dependencies through pluggable hashing #57

Description

@lrstanley

I'd like httprate to explore reducing dependencies, preferably to zero. While the current dependencies are very small/lightweight (and one is effectively an extension of stdlib), zero dependencies are still preferred, especially as supply chain attacks continue to grow in complexity and spread.

The 2 primary dependencies right now:

  • golang.org/x/sync -- just used in tests right now (errgroup in particular), and quite easy to do right now without errgroup itself, especially since it's inside of tests. This should be a simple thing to remove, and I can open a PR to do that, if its OK.
  • github.com/zeebo/xxh3 -- used for key hashing, a particularly popular choice for something like a rate limiter.

I propose replacing xxh3 with an implementation of FNV-1a, and potentially a Option parameter (or similar) that allows customizing it, if there is a specific desire (more reasoning below).

I've tested 3 variants:

  • github.com/zeebo/xxh3
    • pros: fast, relatively consistent timing growth.
    • cons: extra dependency.
  • hash/fnv.New64a
    • pros: stdlib, don't have to maintain anything.
    • cons: longer ns/op as the key length grows. stdlib implementation requires allocations/op. performance isn't good as-is.
  • custom implementation of hash/fnv.New64a
    • pros:
      • 2-5x faster than xxh3 for what I suspect is most-common length (IPv4/IPv6 addresses).
      • ~5-10% slower for likely-worst-case (ip + endpoint, ip + request ID, ip + uuid).
      • all stdlib, ~13 LOC.
    • cons:
      • longer ns/op as the key length grows, but nowhere near as bad as stdlib. ~2-3x worse than xxh3 around 70 chars (but still totally reasonable for almost any application).
      • may be a "good enough" non-dependency implementation, but if we provide an Option to allow customizing the hash function, it still allows people to reach for something more performant.

Implementation of each:

// LimitCounterKeyXXH3 hashes key with XXH3, matching httprate's local counter keying.
func LimitCounterKeyXXH3(key string) uint64 {
	h := xxh3.New()
	_, _ = h.WriteString(key)
	return h.Sum64()
}

// LimitCounterKeyFNV64aStdlib hashes key with FNV-1a 64-bit via [hash/fnv.New64a].
func LimitCounterKeyFNV64aStdlib(key string) uint64 {
	h := fnv.New64a()
	_, _ = io.WriteString(h, key)
	return h.Sum64()
}

// Ref: https://cs.opensource.google/go/go/+/refs/tags/go1.26.1:src/hash/fnv/fnv.go;l=31-40,130-138
const (
	offset64 uint64 = 14695981039346656037
	prime64  uint64 = 1099511628211
)

// LimitCounterKeyFNV64aInline hashes key with FNV-1a 64-bit using the same algorithm as
// [hash/fnv] digest64, without allocating a [hash.Hash64] each call.
func LimitCounterKeyFNV64aInline(key string) uint64 {
	h := offset64
	for i := 0; i < len(key); i++ {
		h ^= uint64(key[i])
		h *= prime64
	}
	return h
}

Benchmarks, comparing each solution above, over 5 iterations:

goos: linux
goarch: amd64
pkg: github.com/go-chi/httprate
cpu: AMD Ryzen 9 9900X3D 12-Core Processor          
                                    │ bench_results.txt │
                                    │      sec/op       │
LimitCounterKey_XXH3/1-12                  11.17n ± ∞ ¹
LimitCounterKey_XXH3/10-12                 14.39n ± ∞ ¹
LimitCounterKey_XXH3/25-12                 14.89n ± ∞ ¹
LimitCounterKey_XXH3/50-12                 15.36n ± ∞ ¹
LimitCounterKey_XXH3/100-12                16.97n ± ∞ ¹
LimitCounterKey_XXH3/200-12                18.06n ± ∞ ¹
LimitCounterKey_FNV64aStdlib/1-12          15.67n ± ∞ ¹
LimitCounterKey_FNV64aStdlib/10-12         19.40n ± ∞ ¹
LimitCounterKey_FNV64aStdlib/25-12         30.80n ± ∞ ¹
LimitCounterKey_FNV64aStdlib/50-12         53.02n ± ∞ ¹
LimitCounterKey_FNV64aStdlib/100-12        96.63n ± ∞ ¹
LimitCounterKey_FNV64aStdlib/200-12        172.1n ± ∞ ¹
LimitCounterKey_FNV64aInline/1-12         0.5086n ± ∞ ¹
LimitCounterKey_FNV64aInline/10-12         2.500n ± ∞ ¹
LimitCounterKey_FNV64aInline/25-12         8.005n ± ∞ ¹
LimitCounterKey_FNV64aInline/50-12         22.22n ± ∞ ¹
LimitCounterKey_FNV64aInline/100-12        46.85n ± ∞ ¹
LimitCounterKey_FNV64aInline/200-12        115.4n ± ∞ ¹
geomean                                    19.09n

                                    │ bench_results.txt │
                                    │       B/op        │
LimitCounterKey_XXH3/1-12                   0.000 ± ∞ ¹
LimitCounterKey_XXH3/10-12                  0.000 ± ∞ ¹
LimitCounterKey_XXH3/25-12                  0.000 ± ∞ ¹
LimitCounterKey_XXH3/50-12                  0.000 ± ∞ ¹
LimitCounterKey_XXH3/100-12                 0.000 ± ∞ ¹
LimitCounterKey_XXH3/200-12                 0.000 ± ∞ ¹
LimitCounterKey_FNV64aStdlib/1-12           16.00 ± ∞ ¹
LimitCounterKey_FNV64aStdlib/10-12          24.00 ± ∞ ¹
LimitCounterKey_FNV64aStdlib/25-12          40.00 ± ∞ ¹
LimitCounterKey_FNV64aStdlib/50-12          72.00 ± ∞ ¹
LimitCounterKey_FNV64aStdlib/100-12         120.0 ± ∞ ¹
LimitCounterKey_FNV64aStdlib/200-12         216.0 ± ∞ ¹
LimitCounterKey_FNV64aInline/1-12           0.000 ± ∞ ¹
LimitCounterKey_FNV64aInline/10-12          0.000 ± ∞ ¹
LimitCounterKey_FNV64aInline/25-12          0.000 ± ∞ ¹
LimitCounterKey_FNV64aInline/50-12          0.000 ± ∞ ¹
LimitCounterKey_FNV64aInline/100-12         0.000 ± ∞ ¹
LimitCounterKey_FNV64aInline/200-12         0.000 ± ∞ ¹

                                    │ bench_results.txt │
                                    │     allocs/op     │
LimitCounterKey_XXH3/1-12                   0.000 ± ∞ ¹
LimitCounterKey_XXH3/10-12                  0.000 ± ∞ ¹
LimitCounterKey_XXH3/25-12                  0.000 ± ∞ ¹
LimitCounterKey_XXH3/50-12                  0.000 ± ∞ ¹
LimitCounterKey_XXH3/100-12                 0.000 ± ∞ ¹
LimitCounterKey_XXH3/200-12                 0.000 ± ∞ ¹
LimitCounterKey_FNV64aStdlib/1-12           2.000 ± ∞ ¹
LimitCounterKey_FNV64aStdlib/10-12          2.000 ± ∞ ¹
LimitCounterKey_FNV64aStdlib/25-12          2.000 ± ∞ ¹
LimitCounterKey_FNV64aStdlib/50-12          2.000 ± ∞ ¹
LimitCounterKey_FNV64aStdlib/100-12         2.000 ± ∞ ¹
LimitCounterKey_FNV64aStdlib/200-12         2.000 ± ∞ ¹
LimitCounterKey_FNV64aInline/1-12           0.000 ± ∞ ¹
LimitCounterKey_FNV64aInline/10-12          0.000 ± ∞ ¹
LimitCounterKey_FNV64aInline/25-12          0.000 ± ∞ ¹
LimitCounterKey_FNV64aInline/50-12          0.000 ± ∞ ¹
LimitCounterKey_FNV64aInline/100-12         0.000 ± ∞ ¹
LimitCounterKey_FNV64aInline/200-12         0.000 ± ∞ ¹

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions