-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
channeldb: write through cache for the graph and channel state #5595
Conversation
21c5899
to
deeb284
Compare
ac7ceac
to
d0a9f12
Compare
Ready to remove the draft tag on this one? |
Yep, ready for a round of reviews imho. The thing I'm thinking about is if we want to speed up the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Amazing PR!
TBH, I was a bit skeptical when you mentioned you were going with a more generic approach here vs a more context-specific cache for the channel graph itself, but the ultimate implementation is a lot more compact than I anticipated.
I've completed an initial review pass, and a few things popped out to me:
- Do we have any tangible benchmarks on a mainnet-loaded node to gauge the perf increase, as well as the memory increase trade off? I'm a bit confined about the impact of caching larger buckets like the forwarding package state and channel revocation log.
- If it turns out to be prohibitive in practice, then at least we gain the added benefit of graph operations being mostly cached which should speed up path finding attempts (mainly the initial read at the start of each attempt).
- In the context of boltdb, how much faster is a vanilla read from the database (which is memory mapped) vs fetching an item from the cache?
|
||
type cacheBucket struct { | ||
seq *uint64 | ||
tree *btree.BTree |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yo dawg, I heard you like b-trees 🤣
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So with this, we always end up caching the contents of an entire bucket? I'd be concerned about the memory blow up here for larger nodes that have a large revocation log (ever growing append only log for channels), invoices, payments, etc.
Do we have any profiles that show the resident memory overhead of this patch on mainnet?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this simple implementation is only allowing us to cache whole buckets from top-level down to the leafes. This is because if we want to evict whole buckets from the memory when they're inacive (LRU strategy) we'd need a more compex architecture:
- single reader/writer since both can modify the cache
- need to bubble up and create the DB read buckets if the cache needs to read in an evicted bucket (this is needed, because a huge performance bottleneck alone is just getting the bucket key/values when structure is deep).
Current PR only caches channel-state. With the payments
bucket we really wouldn't want to read the whole bucket on start. Invoices bucket didn't seem to be very problematic in my benchmarks so far.
kvdb/cache.go
Outdated
} | ||
|
||
// Enforce that Cache implements the ExtendedBackend interface. | ||
var _ walletdb.DB = (*Cache)(nil) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move this down below where the definition of Cache
is first added?
kvdb/cache.go
Outdated
currRoot := root | ||
|
||
for { | ||
currBucket.ForEach(func(k, v []byte) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't return the error, seems to be assuming here that since we return nil
ourselves, an error can never happen here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed, ptal
) | ||
|
||
type cacheBucket struct { | ||
seq *uint64 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason this is a pointer vs the raw value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is to lazy initialize the seq
(no need to fetch it for all buckets when prefetching). Fortunately Sequence
is only part of the ReadWriteBucket
interface so we can do this.
) | ||
|
||
const ( | ||
treeDeg = 3 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Arbitrary value or was there some tuning here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not much testing yet (I tested with 3 and 5) maybe best to change it to be a parameter instead.
channeldb/channel.go
Outdated
@@ -2353,7 +2353,7 @@ func (c *OpenChannel) AdvanceCommitChainTail(fwdPkg *FwdPkg, | |||
|
|||
var newRemoteCommit *ChannelCommitment | |||
|
|||
err := kvdb.Update(c.Db, func(tx kvdb.RwTx) error { | |||
err := kvdb.Update(c.Db.chanCache, func(tx kvdb.RwTx) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we want to cache this bucket (or rather the operations in this DB closure) since we end up accessing an ever-growing bucket (all the past revoked states). On larger nodes this may be in the GBs range (would need to use the bolt browser on a volunteer to get a better picture of things).
With the current design, is it correct that we must use the cache here, since otherwise we'd have consistency issues since other buckets like the revocation state get modified here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay this is somehting I didn't see properly, but yes you're correct if some of these channel state buckets grow indefinitely then we need to do something about evicting on the fly. Yes for now we need things together for consistency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PR updated to skip some buckets.
channeldb/channel.go
Outdated
@@ -2528,7 +2528,7 @@ func (c *OpenChannel) LoadFwdPkgs() ([]*FwdPkg, error) { | |||
defer c.RUnlock() | |||
|
|||
var fwdPkgs []*FwdPkg | |||
if err := kvdb.View(c.Db, func(tx kvdb.RTx) error { | |||
if err := kvdb.View(c.Db.chanCache, func(tx kvdb.RTx) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar comment here re not caching this bucket as it may be very large for long lived larger routing nodes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PR updated to skip some buckets that grow indefinitely.
channeldb/db.go
Outdated
return err | ||
} | ||
|
||
if err := cache.AddTopLevelBucket(edgeIndexBucket); err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC, we already have a cache here there that was added to speed up the gossip queries...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I kept it there for the moment. If we end up liking the ideas in this PR I'll remove that. For now we need all top-level buckets to be cached since we do not read on-demand if the cache is empty for a bucket.
channeldb/db.go
Outdated
@@ -1337,9 +1381,11 @@ func fetchHistoricalChanBucket(tx kvdb.RTx, | |||
|
|||
// FetchHistoricalChannel fetches open channel data from the historical channel | |||
// bucket. | |||
func (d *DB) FetchHistoricalChannel(outPoint *wire.OutPoint) (*OpenChannel, error) { | |||
func (c *ChannelStateDB) FetchHistoricalChannel(outPoint *wire.OutPoint) ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok I see what type of refactors you were referring to now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we could really do more work re: separating things in channeldb. The idea would be to hide things behind intefaces in the end and never allow end-user code to access these buckets at all. This would also allow us to gradually refactor things to work better with other backends.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, awesome PR indeed! I did a high-level code only review to get the gist of what's needed. Going to try and performance test this on mainnet to see how much memory the graph cache takes. And then maybe run another test on regtest to see what 500 channels with many payments look like.
channeldb/nodes.go
Outdated
db kvdb.Backend | ||
} | ||
|
||
func (l *LinkNodeDB) GetBackend() kvdb.Backend { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't seem to be used at all?
channeldb/channel.go
Outdated
@@ -729,7 +729,7 @@ type OpenChannel struct { | |||
RevocationKeyLocator keychain.KeyLocator | |||
|
|||
// TODO(roasbeef): eww | |||
Db *DB | |||
Db *ChannelStateDB |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's finally time to remove that comment above 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still think it's somewhat eww 😃
channeldb/db.go
Outdated
// LinkNodeDB separates all DB operations on LinkNodes. | ||
LinkNodeDB | ||
// ChannelStateDB separates all DB operations on channel state. | ||
ChannelStateDB |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason not to embed this as a pointer?
Thanks for taking a look Oliver! Quick note about benchmarking: If you use the Bottlepay benchmark, ideally you'd need changes in the "wip etcd performance improvements PR" (#5392). I rebased that on top of this. My benchmarks showed great performance of the graph itself, since it's all in memory, we only pay the serializaton/deserializaton cost. We could mitigate that too, although it's a bigger refactor. Currently the main bottleneck remaining after this is the |
Thanks for the thorough review @Roasbeef !!
|
I'm wondering that given the recent DB changes maybe we could drop this? @Roasbeef |
@bhandras drop as in close? Has been a while since I've looked at this, but I think since then we've gone w/ the in-memory route for the graph which gave us a nice speed up and a reduction in the number of round trips. I think as we start to do more SQL specific stuff and add instrumentation to see on which operations we're waiting the longest on (which might still be in KV land) this could be useful in adding more caching to minimize network round trips there. So maybe it's an 0.17 thing? It's also been a while since I've run the bottlepay bechmarking stuff as well. |
So this might be a bit more relevant now, based on some of the profiles gathered in this issue: #6683. TL;DR: in the wild a user's script ends up fetching the channel state a bunch, this makes everything else slower as either operations are blocked on this, or other transactions are held up (?) due to the constant queries. |
Happy to resurrect the PR for 0.16 if you think it's relevant. I think the issue reference is wrong, as it points to the payment lifecycle refactor, but in general this change list is more relevant for the case when using a remote database, since bolt will mmap most of it anyway (so not much to gain with bbolt). |
@bhandras, remember to re-request review from reviewers when ready |
Closing due to inactivity |
2 similar comments
Closing due to inactivity |
Closing due to inactivity |
This PR adds a generic cache to the
kvdb
layer as well as demonstrates its use caching the channel state and the graph (graph cache being reworked to be more specific: #5642).With this cache we can prefetch frequently accessed buckets upon start and reduce DB operations to puts and deletes which will help with performance when LND is running on replicated remote databases (etcd, Postgres).