channeldb: write through cache for the graph and channel state#5595
channeldb: write through cache for the graph and channel state#5595bhandras wants to merge 5 commits intolightningnetwork:masterfrom
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 |
Roasbeef
left a comment
There was a problem hiding this comment.
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?
kvdb/cache.go
Outdated
There was a problem hiding this comment.
yo dawg, I heard you like b-trees 🤣
There was a problem hiding this comment.
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.
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
There was a problem hiding this comment.
Move this down below where the definition of Cache is first added?
kvdb/cache.go
Outdated
There was a problem hiding this comment.
Doesn't return the error, seems to be assuming here that since we return nil ourselves, an error can never happen here?
kvdb/cache.go
Outdated
There was a problem hiding this comment.
Any reason this is a pointer vs the raw value?
There was a problem hiding this comment.
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.
kvdb/cache.go
Outdated
There was a problem hiding this comment.
Arbitrary value or was there some tuning here?
There was a problem hiding this comment.
Not much testing yet (I tested with 3 and 5) maybe best to change it to be a parameter instead.
channeldb/channel.go
Outdated
There was a problem hiding this comment.
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.
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.
PR updated to skip some buckets.
channeldb/channel.go
Outdated
There was a problem hiding this comment.
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.
PR updated to skip some buckets that grow indefinitely.
channeldb/db.go
Outdated
There was a problem hiding this comment.
IIRC, we already have a cache here there that was added to speed up the gossip queries...
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Ok I see what type of refactors you were referring to now.
There was a problem hiding this comment.
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.
guggero
left a comment
There was a problem hiding this comment.
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
There was a problem hiding this comment.
This doesn't seem to be used at all?
channeldb/channel.go
Outdated
There was a problem hiding this comment.
I think it's finally time to remove that comment above 😅
There was a problem hiding this comment.
I still think it's somewhat eww 😃
channeldb/db.go
Outdated
There was a problem hiding this comment.
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 !!
|
|
Added support for a hybrid cache which is able to read-through and write-through nested (and top level) buckets. This will alow us to spare a lot of memory for huge buckets that we don't want to cache but are still part of a partially cached structure. |
This commits adds a new generic Cache to kvdb which is capable of prefetching large buckets recursively while skipping arbitrary buckets at the same time. This mechanism lets us use such cache for buckets that don't grow indefinitely but are accessed very frequently, essentially reducing DB operations to just the puts and deletes.
|
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
kvdblayer 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).