Refactor store package for performance and simplicity - #633
Refactor store package for performance and simplicity#633Anton-Kalpakchiev wants to merge 10 commits into
store package for performance and simplicity#633Conversation
store package to be simpler and more performantstore package for performance and simplicity
d3ffa2a to
17bb3fc
Compare
01cd3d2 to
2683553
Compare
sambhav-jain-16
left a comment
There was a problem hiding this comment.
I have a bit of nits and some questions. Overall the implementation looks good.
I do have an overall comment on the test files, Will it be possible to refactor them to table driven formats and avoid redundant code?
| err := os.RemoveAll(incompleteDirPath) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("remove incomplete blobs left from a previous service run: %w", err) | ||
| } |
There was a problem hiding this comment.
Should we add retry logic here to avoid failing on a random flake?
There was a problem hiding this comment.
My understanding is that disk APIs don't have transient errors, so if we get an error, it is most likely a real error, so a retry might only put extra load on the disk. Therefore, I decided to "fail fast and early" to surface the error/bug, considering this bug happens on application startup, when a rollback would still be possible. WDYT?
| // - Crash-resistant - all state is restored upon restart (check [newStore] for details). | ||
| // | ||
| // - Supports directory sharding to speed up disk performance. | ||
| type Store struct { |
There was a problem hiding this comment.
nit: maybe rename to ScopedStore?
There was a problem hiding this comment.
This is the store that is exported to clients. So the import currently looks like disk.Store. I think disk.ScopedStore might be misleading, as it would imply there is another non-scoped store, but there isn't - clients have one single DiskStore to operate on and this is it, so I decided to just name it Store. The reason the file is called scoped_store.go is that internally within the disk package we have a non-scoped implementation, but it's not something we expose outside the package. WDYT?
I agree there's a lot of repetition in the code, but am not sure if table tests can fix this - each test case does something a little different. For example, one test case might call |
thijmv
left a comment
There was a problem hiding this comment.
Please also fix the lint errors before merging.
This commit designs the client-facing interface of the disk store and implements its core APIs. It establishes the core data structures used. It also adds tests.
- Implement the BanEviction and UnbanEviction APIs of the store that stop a blob from getting evicted - Finish other APIs that were half-implemented due to unevictable blobs not being implemented. - Change the API of Delete to now be able to delete evictable blobs. I looked through the current use of `Delete` for the ca_store and the only time it checked if it was trying to delete persisted blobs was during cleanup. Now that the store's APIs themselves do cleanup, there is no reason for Delete to respect whether a blob is evictable or not. - Add **extensive** testing for 1) the eviction logic and 2) other APIs. - Make some actions no-ops instead of returning errors (e.g. calling MarkComplete on an already complete blob) to make the interface more forgiving. The next commits will: - implement Metadata operations - add logic to recover state from disk after a crash.
Previously, the store used 3 maps to differentiate between different blob types: - complete blobs - unevictable blobs - incomplete blobs This was not a clean solution, as now all key-value APIs now needed to check all 3 maps for the existence of the key. Also APIs that transitioned a blob from one blob type to another required moving the blob between the maps. To remove this complexity, now we use a single map to manage all blobs. Instead of determining the blob's type by checking which map it belongs to, we create a `blob` struct with the blob's necessary data and store that in the map. I also fix issues in tests and improve the style.
As DiskStore is intended to be persistent, it should be able to survive crashes/restarts by rebooting any necessary state from disk to memory. This was already implemented, however, it was assumed that incomplete blobs do not need to be rebooted, as it was expected that the store users would forget about the incomplete blobs, resulting in a leak. This turned out to be false - while almost all Kraken services' storages drop incomplete blobs on system crash, the agent actually persists incomplete blobs, so it can resume downloading them after it comes back up. As DiskStore is intended to be a single DiskStore reused by all Kraken services that need disk storage, this commit adds support to reboot incomplete blobs after a crash. To do so, the client-provided size of the blob must be persisted on disk (currently implemented in a sidecar file), which this commit implements. The following alternatives were evaluated but discarded: 1. removing the agent's ability to resume downloads after a crash - will degrade p99 performance 2. truncating files to persist their size instead of using a sidecar file - client-provided blob sizes might differ by a few bytes from actual sizes 3. using `xattr` instead of a sidecar file - xattr is not supported on all filesystems 4. moving toward async eviction that measures the size of the directory directly - discarded, as measuring the size of a huge directory can be extremely slow (10s of minutes), which would block eviction and cause disk exhaustion AND could add extra disk IO pressure
Agent, origin, and build-index currently use CAStore (the disk cache implementation that DiskStore is replacing) and more specifically its functionality to scope an API to work on only a complete blob or an incomplete blob. Without this functionality, it is much harder to keep their code correct. Therefore, DiskStore must expose the same functionality in order to replace CAStore. This PR adds that logic. At first, I decided each API can take an extra arg for the scope, e.g. the following code would delete the blob with key `b7fe1643` only if it is complete. Otherwise, an error is returned. ```go key := "b7fe1643" err := store.Delete(key, ScopeCompleteBlobs) ``` This works, however, it adds complexity - each API call now requires an extra arg, while the user might not necessarily want to scope the API call. Therefore, I opted for the implementation in this PR, where we use constructor-like APIs like this: ```go key := "b7fe1643" err := store.ScopeComplete().Delete(key) // works only on complete blob err := store.ScopeIncomplete().Delete(key) // works only on incomplete blob err := store.Delete(key) // works on any blob ``` Now the APIs have a simpler interface (1 fewer arg) and scoping is optional.
- Add ListMetadata API (needed by proxy) - Add WriteAtMetadata API (needed by agent) - clean immovable metadata after marking a blob as complete. For context, some metadata is supposed to stay as long as the blob is alive, e.g. the MetaInfo metadata. However, other metadata is only needed while a blob is being downloaded into the DiskStore and can be discarded once the download is complete, e.g. `startedAtMetadata` (used to keep track of when an upload to the DiskStore started to enforce a TTL timeout). Up until now, we didn't clean up this metadata in MarkComplete, but after this commit we do. Also: - add more logs - add an extra test - emit an extra metric - fix typos
Build-index currently does not use directory sharding. It also does not store blobs, whose keys are blob digests (i.e. SHA256 strings). Therefore, to replace the existing SimpleStore used by build-index, we need to make sure sharding is configurable. This commit makes that change.
…d simplify naming - all DiskStore code is now under the `lib/store/disk` package. After I replace all storage code, I plan to have `lib/store/disk`, `lib/store/mem`, and `lib/store/tiered` for the 3 different cache implementations. - Since the package already has `disk` in its name, I made the naming more concise by renaming `DiskStore` to `Store` (the import will be `disk.Store`) - Added a `Config` struct to simplify the constructor interfaces and document the configurable parameters in a single place. - fix a bug where rebooting does not work when sharding is off. Add a test that catches the bug. - make `rebootKeys` take `complete bool` instead of `subDir string` to stay consistent with the other functions' signatures - make small improvements to tests and docs
1bcdd96 to
85e4790
Compare
TLDR
This PR is the first of several to refactor the
storepackage to 1) be more performant (transition from a TTI to an LRU eviction policy). and 2) be much simpler (go from 8K to <3K LOC, remove unnecessary abstractions, etc.). Please review each commit and its description separately (the PR itself will become quite large).NOTE - this PR is not plugged into production code, that will be done in a separate PR.
Justification
Why transition from a TTI to an LRU eviction policy (copied from this ERD)
Each origin replica in the hashring has a disk and a memory cache. Both caches are severely underutilized, bleeding performance. The disk cache has 30% util, while the mem cache has <10% util. By refactoring both caches to be LRU-based, we will achieve 100% utilization, significantly improving kraken-origin performance. There is absolutely 0 reason not to do this, as Kraken’s data is immutable, therefore it is optimal to evict data only when necessary, i.e. when making room to cache newer data.
Why refactor the code
The
storepackage has balooned to over ~8k lines with several layers of abstraction. I believe this is way beyond the package's inherent complexity. Changing the package, as proposed in this PR is non-trivial. At this point, it's easier to rewrite it from scratch, reusing existing parts wherever possible. I expect this to 1) significantly simplify the package's interface and 2) reduce the package's internal complexity (from ~8K LOC to <3K).Implementation (copied from the ERD)
Currently, the cache works as follows:
I propose the following changes:
The new flow is illustrated below:
