From NDB Caching:
NDB manages caches for you. There are two caching levels: an
in-context cache and a gateway to App Engine’s standard caching
service, memcache. Both caches are enabled by default for all entity
types, but can be configured to suit advanced needs.
My app doesn’t make any ndb caching configuration change, so it must be using the defaults – both caching levels enabled.
I’m running some tests on my staging environment (a separate, dedicated GAE project) where I can totally isolate sequences of activity from any spurious external requests.
Each sequence of activity consists of a cascade of push tasks triggering each-other, creating some hundreds of volatile entities, modifying some of them a variable number of times, reading all of them a variable number of times and finally deleting them all.
Only a handful of other already existing entities are accessed during the sequence, all of them persisting after the sequence ends. The number of accesses to the persistent entities should be significantly lower than the number of accesses to the volatile ones.
The vast majority of read operations are entity lookups by key or ids, obtained from keys_only
queries or from other related entities.
I’m not using entity ancestry. Most of these tasks perform cross-group transactions and I do see quite often transaction failures/retries due to data contention on a few “hot” entities (I’d estimate some 200-400 of them in this run, hard to count in the Stackdriver log page).
After executing one such ~20 minutes sequence freshly after the daily quota reset, the app’s dashboard shows 3x more Cloud Datastore Read Operations (0.03 Millions) than Cloud Datastore Entity Writes (0.01 Millions). The number of volatile entities is indicated by the Cloud Datastore Entity Deletes (0.00089 Millions), if that matters.
The memcache hit rate was 81%, but I don’t know if that is for my app’s explicit memcache use only or if it includes the ndb’s memcache use.
Some earlier similar measurements but not in as clean environment produced similar results, I did this clean one as a verification.
These observations appear to suggest that the entity reads from the cache still count as datastore reads. But here I’m assuming that:
- there weren’t many memcache evictions during those ~20 min (the app doesn’t have a dedicated memcache bucket).
- each write op invalidates the cache thus requiring a datastore read to update the cache, but subsequent read ops should come from the cache (until the next write op), which should lead to rather comparable overall read and write counts if cached reads aren’t counted
- my (fairly complex) app code really does what I’m describing 🙂
I didn’t find anything about this in the docs, so wondering if someone knows if the cached ndb reads indeed count as datastore reads or can point me to flaws in my interpretation or some official documentation on the subject.
2
Answers
Following Jim's answer I did further digging, sharing my findings here as others may find them useful.
I congratulated myself for deciding from day one to build and use a generic wrapper class for all my datastore entities, further inherited by specific classes for each entity kind. That is instead of operating with model classes directly inheriting
ndb.Model
.I already had a
db_data
@property
in this generic class for reading the entity from the datastore on-demand, it was easy to plug in my own entity memcache-based caching scheme and tracking code to determine if the reads come from memcache or from ndb and if they're done inside transactions or not. This is how it looks like:With this in place I uncovered:
ndb
, most notably the sequence configuration entities which did not change at all during the entire sequence and were accessed hundreds of timesTransactionFailedError(The transaction could not be committed. Please try again.)
orTransactionFailedError: too much contention on these datastore entities. please try again
, caused by all these unnecessary reads from many transactions executed in parallel. I knew (or rather suspected) the root cause - see related Contention problems in Google App Engine - but with these details info I can now actually work towards reducing them.For the write side, I added to the generic class a
db_data_put()
method (with many checks and tracking support) and replaced all the.db_data.put()
calls scattered all over the app code with it. This is how it looks like:With it I uncovered some room for improvements and some lurking bugs:
After setting the
self.readonly
flag to all sequence config objects and a couple of more often unnecessarily read ones (to enable caching even inside transactions), serializing thehot
writing requests and fixed the most critical bugs found I re-tried the clean measurement test:hot
transactions which helped reduce the "noise" of the sequence and the overall number of logic state transitions and push queue tasksndb
has no clue if an entity accessed in a transaction will eventually be written to or not) and to serializing of thehot
transactionsThe rate of write to reads actually increased, invalidating my assumption that they should be comparable - the fact that most of the sequence activity happens in transactions matters in this case. I guess my assumption would only be valid in a non-transactional context.
The memcache hit rate includes the hits for entities written by ndb. It does not include the number of in-context cache hits by ndb.
Each write up to datastore invalidates the caches, so the next read will not be cached in memcache or in the in-context cache.
One other thing is that ndb creates a new in-context cache for each transaction, so the in-context cache isn’t very effective in the face of transactions.
The quick answer is that a memcache hit for a datastore entity is not charged as a datastore read for billing purposes.