<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="/assets/xml/rss.xsl" media="all"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Jason Madden (Posts about oracle)</title><link>https://seecoresoftware.com/</link><description></description><atom:link href="https://seecoresoftware.com/blog/categories/oracle.xml" rel="self" type="application/rss+xml"></atom:link><language>en</language><copyright>Contents © 2021 &lt;a href="mailto:jason@seecoresoftware.com"&gt;Jason Madden&lt;/a&gt; </copyright><lastBuildDate>Sat, 04 Dec 2021 18:43:34 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>RelStorage 3.0</title><link>https://seecoresoftware.com/blog/2019/11/relstorage-30.html</link><dc:creator>Jason Madden</dc:creator><description>&lt;figure&gt;&lt;img src="https://seecoresoftware.com/blog/2019/11/relstorage.jpg"&gt;&lt;/figure&gt; &lt;div&gt;&lt;p&gt;We're happy to announce the release of RelStorage 3.0, the relational
storage engine for ZODB. Compared to RelStorage 2, highlights include
a 30% reduction in memory usage, and up to 98% faster performance!
(Ok, yes, that's from one specific benchmark and not everything is 98%
faster, but improved performance was a major goal.)&lt;/p&gt;
&lt;p&gt;RelStorage 3.0 is a major release of RelStorage with a focus on
performance and scalability. It's the result of a concentrated development
effort spanning six months, with each pre-release being in production
usage with large databases.&lt;/p&gt;
&lt;p&gt;Read on to find out what's new.&lt;/p&gt;
&lt;!-- TEASER_END --&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title first"&gt;Contents&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#backwards-incompatible-changes" id="id7"&gt;Backwards Incompatible Changes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#major-changes" id="id8"&gt;Major Changes&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="reference internal" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#pickle-cache" id="id9"&gt;Pickle Cache&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#persistent-cache" id="id10"&gt;Persistent Cache&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#zodb-5-parallel-commit" id="id11"&gt;ZODB 5 Parallel Commit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#zodb-5-prefetch" id="id12"&gt;ZODB 5 Prefetch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#support-for-sqlite" id="id13"&gt;Support for SQLite&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#performance-improvements" id="id14"&gt;Performance Improvements&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="reference internal" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#writing" id="id15"&gt;Writing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#reading" id="id16"&gt;Reading&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#gevent" id="id17"&gt;gevent&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#minor-changes" id="id18"&gt;Minor Changes&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="reference internal" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#supported-versions" id="id19"&gt;Supported Versions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#the-blob-cache" id="id20"&gt;The Blob Cache&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#packing-and-gc" id="id21"&gt;Packing and GC&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#performance-grab-bag" id="id22"&gt;Performance Grab Bag&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#conclusion" id="id23"&gt;Conclusion&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;p class="rubric"&gt;Overview&lt;/p&gt;
&lt;p&gt;Please note that this document is only an overview. For details on the
extensive changes since the previous release, RelStorage 2.1, and this
one, please view the detailed &lt;a class="reference external" href="https://relstorage.readthedocs.io/en/latest/changelog.html#a1-2019-06-12"&gt;changelog&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you're not familiar with ZODB (the native Python object database)
and how it uses pluggable storage engines like RelStorage, please take
a moment to review &lt;a class="reference external" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html"&gt;this introduction&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This document will cover a few important things to know about
RelStorage 3.0, and then go over some of the most important changes in
it. Next, we'll show how those changes affect performance. Finally,
we'll wrap it up with a whirlwind tour of some of the minor changes.&lt;/p&gt;
&lt;div class="section" id="backwards-incompatible-changes"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#id7"&gt;Backwards Incompatible Changes&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Before we get to &lt;a class="reference internal" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#the-good-stuff"&gt;the good stuff&lt;/a&gt;, it's important to highlight the
small number of backwards incompatible changes and other things to be
aware of when migrating from RelStorage 2 to RelStorage 3.&lt;/p&gt;
&lt;div class="topic"&gt;
&lt;p class="topic-title first"&gt;Schema Changes&lt;/p&gt;
&lt;p&gt;In history preserving schemas, the &lt;tt class="docutils literal"&gt;empty&lt;/tt&gt; column of the
&lt;tt class="docutils literal"&gt;transaction&lt;/tt&gt; table has been renamed to &lt;tt class="docutils literal"&gt;is_empty&lt;/tt&gt;. In MySQL
8.0.4, &lt;tt class="docutils literal"&gt;empty&lt;/tt&gt; became a reserved word. The table is altered
automatically when first opened with RelStorage 3.0. This makes the
schema incompatible with opening under RelStorage 2. &lt;a class="footnote-reference" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#f1" id="id1"&gt;[1]&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;A new table, used during the commit process, is also automatically
added when first opened with RelStorage 3.&lt;/p&gt;
&lt;p&gt;Under MySQL, any remaining tables that were using the MyISAM engine
are converted to InnoDB when the schema is first opened. The only
tables remaining that were MyISAM were the pack tables and the
&lt;tt class="docutils literal"&gt;new_oid&lt;/tt&gt; table, all of which should ordinarily be empty, so this
conversion shouldn't take long.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="topic"&gt;
&lt;p class="topic-title first"&gt;Option Changes&lt;/p&gt;
&lt;p&gt;The &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;shared-blob-dir&lt;/span&gt;&lt;/tt&gt; default has changed from true to false. If
you were using a non-shared blob-dir, meaning that blobs were only
stored on the filesystem, you'll need to explicitly set this to
true. The previous default could easily lead to accidental data
loss, and there is now a performance penalty for a true value. See
&lt;a class="reference internal" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#blob-cache"&gt;blob_cache&lt;/a&gt; for more information.&lt;/p&gt;
&lt;p&gt;The previously deprecated option &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;poll-interval&lt;/span&gt;&lt;/tt&gt; has been removed.&lt;/p&gt;
&lt;p&gt;Several of the cache persistence options are now deprecated and
ignored. They'll generate a warning on startup if found in the
configuration.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="topic"&gt;
&lt;p class="topic-title first"&gt;Concurrent Deployment With RelStorage 2 Is Not Possible&lt;/p&gt;
&lt;div class="admonition caution"&gt;
&lt;p class="first admonition-title"&gt;Caution!&lt;/p&gt;
&lt;p class="last"&gt;It is not possible for RelStorage 3 to write to a
database at the same time that RelStorage 2 is writing
to it.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;The specifics around locking have changed entirely, and are not
compatible between the two versions. If RelStorage 3 and RelStorage 2
are both writing to a database, corruption is the very likely result.
For this reason, shutting down all RelStorage 2 instances, or at least
placing them into read-only mode, is required.&lt;/p&gt;
&lt;p&gt;RelStorage does not take specific steps to prevent this. It is up to
you to ensure any RelStorage 2 instances are shutdown or at least
read-only before deploying RelStorage 3.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="major-changes"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#id8"&gt;Major Changes&lt;/a&gt;&lt;/h2&gt;
&lt;div class="topic"&gt;
&lt;p class="topic-title first"&gt;Benchmarking Notes&lt;/p&gt;
&lt;p&gt;The benchmark data was collected with &lt;a class="reference external" href="https://zodbshootout.readthedocs.io/en/latest/"&gt;zodbshootout&lt;/a&gt; 0.8. Recent
revisions of zodbshootout have adopted &lt;a class="reference external" href="https://pyperf.readthedocs.io/en/latest/"&gt;pyperf&lt;/a&gt; as the underlying
benchamrk engine. This helps ensure much more stable, consistent
results. It also allows collecting a richer set of data for later
analysis. The data was passed through the &lt;a class="reference external" href="https://seaborn.pydata.org/index.html"&gt;seaborn&lt;/a&gt; statistical
visualization library, which is built using &lt;a class="reference external" href="https://pandas.pydata.org"&gt;pandas&lt;/a&gt;, &lt;a class="reference external" href="https://numpy.org"&gt;numpy&lt;/a&gt; and
&lt;a class="reference external" href="https://matplotlib.org"&gt;matplotlib&lt;/a&gt;, to produce the plots shown here.&lt;/p&gt;
&lt;p&gt;For the comparisons between RelStorage 3 and RelStorage 2, the
RDBMS servers (MySQL 8 and PostgreSQL 11) were run on one computer
running Gentoo Linux using SSD storage. The RelStorage client was
run on a different computer, and the two were connected with
gigabit ethernet. The machines were otherwise idle.&lt;/p&gt;
&lt;p&gt;The comparisons used Python 2.7.16 (because 2.7 was the only
version of Python that had a native code gevent driver compatible
with RelStorage 2). The database drivers were mysqlclient 1.4.4 and
psycopg2 2.8.4. The gevent driver used &lt;a class="reference external" href="https://github.com/NextThought/ultramysql/@6e4bf3af30493a3543e45b691db6132675421306"&gt;this version of ultramysql&lt;/a&gt;,
umysqldb 1.0.4.dev2, PyMySQL 0.9.3 and gevent 1.5a2.&lt;/p&gt;
&lt;p&gt;Why transaction of size 1, 5 and 20 objects? Review of a database
containing 60 million objects showed the average transaction
involved 2.6 objects with a standard deviation of 7.4. A database
with 30 million objects had an average transaction of 6.1 objects
and a standard deviation of 13.&lt;/p&gt;
&lt;p&gt;In all the examples that follow, results for PostgreSQL are green
while those for MySQL are in blue. The darker shades are RelStorage
3, while the lighter shades are RelStorage 2. The y axis is time,
and shorter bars are better (no units or tickmarks are shown
because the scale differs between graphs and we're generally
focused on deltas not absolute values); the black line that appears
in the middle of some bars is the confidence interval. Click for a
larger version.&lt;/p&gt;
&lt;p&gt;Most examples show a cross section of RDBMS server by concurrency
kind (threads or process) by concurrency level (1, 5, or 20
concurrent threads or processes) by object count (1, 5 or 20
objects).&lt;/p&gt;
&lt;/div&gt;
&lt;hr class="docutils"&gt;
&lt;p&gt;To accomplish its goals of improving performance (especially under
high loads distributed across many processes on many machines),
reducing memory usage, and being usable in small environments without
access to a full RDBMS server (such as containers or test
environments), RelStorage features several major internal changes.&lt;/p&gt;
&lt;div class="section" id="pickle-cache"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#id9"&gt;Pickle Cache&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The shared in-memory pickle cache has been redesigned to be precise
and MVCC based; it no longer uses the old checkpoint system. This
means that old revisions of objects can proactively be removed from
the cache when they are no longer needed. Together, this means that
connections within a process are able to share polling information,
with the upshot being that there are no longer large, stop-the-world
poll queries in order to rebuild checkpoints. Individual poll queries
are usually smaller too.&lt;/p&gt;
&lt;p&gt;It has also been changed to have substantially less overhead for each
cached object value. Previously, it would take almost 400 bytes of
overhead to store one cache entry. In examining a database of 60
million objects, it turned out that the average object size was only a
little over 200 bytes. Using 400 bytes to store 200 bytes was
embarrassing, and because the cache limit computations didn't take the
overhead into account it meant that if you configured an in-memory
cache size of 200MB, the cache could actually occupy up to 600MB.&lt;/p&gt;
&lt;p&gt;Now, storing a cached value needs only a little more than 100 bytes,
and the exact amount of overhead is included when enforcing the cache
limit (so a 200MB limit means 200MB of memory usage). The Python
cache implementation using a CFFI-based Segmented LRU and Python
lookup dictionary were replaced with a minimal Cython extension using
a C++ &lt;a class="reference external" href="https://www.boost.org/doc/libs/1_71_0/doc/html/intrusive/set_multiset.html"&gt;boost.intrusive list and map&lt;/a&gt;.
This also essentially eliminates the cache's impact on the Python
garbage collector, which should improve garbage collection times &lt;a class="footnote-reference" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#f6" id="id2"&gt;[2]&lt;/a&gt;.&lt;/p&gt;
&lt;!-- I don't have complete memory information for RelStorage 2, but it
should be similar to what I do have, which is RelStorage 3.0a11
(CFFI) and 3.0b3 (C++).

The memory usage comes from two machines with a master gunicorn
and 6 workers each (7 datapoints), as reported by __poll_old_oids_and_remove.
This is the last step of loading the persistent cache (around
800,000 objects) and is the total memory used by the process at
that point. This was after a code deployment, but opening the
database is the first step in booting a worker so this doesn't
include any application data sizes. Total memory with RelStorage
3.0a11 from all data points was 18,867MB. Total memory with
RelStorage 3.0b3 was 11,998MB, or about 64% - - - a reduction of
36%.

The more direct measurement of 'read_from_sqlite' gives data
points of 7902MB before and 7244MB after, or about 10%. (It's
faster too, from 3.3s to 0.9s for the '_bulk_update' and from 12s
to 6s for the enclosing 'read_from_sqlite'.) What's the difference
in memory usage between read_from_sqlite and poll_old_oids, which
is called essentially immediately after? I'm not entirely sure.
There's the 'minimum_oid' dict but that's about 55MB

Looking at prometheus stats
``sum(node_memory_Active_bytes{instance=~"ds.*"})`` shows a
similar reduction across all environments (restarts don't exactly
line up but a 20% or more change is clearly visible.) --&gt;
&lt;p&gt;In concrete terms, one set of production processes that required
almost 19GB of memory now requires only about 12GB: a 36% reduction.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="persistent-cache"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#id10"&gt;Persistent Cache&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Along with rearchitecting the in-memory cache, the on-disk persistent
cache has been rebuilt on top of SQLite. Its hit rate is much
improved and can easily reach 100% if nothing in the database changed.
If you haven't deployed the persistent cache before, now would be a
great time to give it a try.&lt;/p&gt;
&lt;p&gt;If you had used the persistent cache in the past, the new cache should
just work. Old cache files will be ignored and you might want to
manually remove any that exist to reclaim disk space.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="zodb-5-parallel-commit"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#id11"&gt;ZODB 5 Parallel Commit&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;RelStorage 3 now requires ZODB 5, and implements ZODB 5's parallel
commit feature. During most of the ZODB commit process, including
conflict resolution, only objects being modified are exclusively
locked. Objects that were provided to &lt;tt class="docutils literal"&gt;Connection.readCurrent()&lt;/tt&gt; are
locked only in share mode so they may be locked that way by several
transactions at the same time. (This fact is particularly important
because &lt;a class="reference external" href="https://github.com/zopefoundation/BTrees/blob/master/BTrees/_base.py#L932"&gt;BTrees call&lt;/a&gt; &lt;tt class="docutils literal"&gt;readCurrent()&lt;/tt&gt; for every node traversed while
searching for the correct leaf node to add/remove/update, meaning
there can be a surprising amount of contention.)&lt;/p&gt;
&lt;p&gt;Only at the very end of the ZODB commit process when it is time to
commit to the database server is a database-wide lock taken while the
transaction ID is allocated. This should be a very brief time, so
transactions that are operating on distinct sets of objects can
continue concurrently for much longer (especially if conflicts occur
that must be resolved; one thread resolving conflicts no longer
prevents other threads from resolving non-overlapping conflicts).&lt;/p&gt;
&lt;p&gt;This works on most databases, but it works best on a database that
supports &lt;tt class="docutils literal"&gt;NOWAIT&lt;/tt&gt; share locks, like MySQL 8 or PostgreSQL. SQLite
doesn't support object-level locking or parallel commit. Oracle
doesn't support shared object-level locks.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="zodb-5-prefetch"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#id12"&gt;ZODB 5 Prefetch&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;RelStorage 3 implements efficient object prefetching through
&lt;tt class="docutils literal"&gt;Connection.prefetch()&lt;/tt&gt;. This is up to 74% faster than reading
objects individually on demand.&lt;/p&gt;
&lt;div class="figure"&gt;
&lt;a class="reference external image-reference" href="https://seecoresoftware.com/images/blog/2019/11/perf_comp_cold_prefetch.png"&gt;&lt;img alt="Prefetching is much faster than reading serially." src="https://seecoresoftware.com/images/blog/2019/11/perf_comp_cold_prefetch.thumbnail.png"&gt;&lt;/a&gt;
&lt;p class="caption"&gt;RelStorage 2 did not implement prefetch so this benchmark falls
back to reading objects individually. RelStorage 3 is able to query
the database in a single bulk operation.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="support-for-sqlite"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#id13"&gt;Support for SQLite&lt;/a&gt;&lt;/h3&gt;
&lt;div class="admonition note"&gt;
&lt;p class="first admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;The SQLite support is relatively new and hasn't received much
production-level testing.&lt;/p&gt;
&lt;p class="last"&gt;On some systems, the underlying sqlite3 module may experience
crashes when lots of threads are used (even though a ZODB
connection and its RelStorage instance and sqlite connection are
not thread safe and must only ever be used from a single thread at
a time, the sequential use from multiple threads can still cause
issues).&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;RelStorage 3 can use a local SQLite3 database file. I'll quote the
&lt;a class="reference external" href="https://relstorage.readthedocs.io/en/latest/faq.html#sqlite"&gt;RelStorage FAQ&lt;/a&gt; to explain why:&lt;/p&gt;
&lt;div class="topic"&gt;
&lt;p class="topic-title first"&gt;Why does RelStorage support a SQLite backend? Doesn't that defeat the point?&lt;/p&gt;
&lt;p&gt;The SQLite backend fills a gap between FileStorage and an
external RDBMS server.&lt;/p&gt;
&lt;p&gt;FileStorage is fast, requires few resources, and has no external
dependencies. This makes it well suited to small applications,
embedded applications, or applications where resources are
constrained or ease of deployment is important (for example, in
containers).&lt;/p&gt;
&lt;p&gt;However, a FileStorage can only be opened by one process at a time.
Within that process, as soon as a thread begins committing, other
threads are locked out of committing.&lt;/p&gt;
&lt;p&gt;An external RDBMS server (e.g., PostgreSQL) is fast, flexible and
provides lots of options for managing backups and replications and
performance. It can be used concurrently by many clients on many
machines, any number of which can be committing in parallel. But
that flexibility comes with a cost: it must be setup and managed.
Sometimes running additional processes complicates deployment
scenarios or is undesirable (for example, in containers).&lt;/p&gt;
&lt;p&gt;A SQLite database combines the low resource usage and deployment
simplicity of FileStorage with the ability for many processes to
read from and write to the database concurrently. Plus, it's
typically faster than ZEO. The tradeoff: all processes using the
database must be on a single machine on order to share memory.&lt;/p&gt;
&lt;/div&gt;
&lt;hr class="docutils"&gt;
&lt;p&gt;I'll leave the rest to the FAQ, with the exception of these two
performance graphs showing SQLite slot in comfortably next to
FileStorage and ZEO.&lt;/p&gt;
&lt;div class="figure"&gt;
&lt;a class="reference external image-reference" href="https://seecoresoftware.com/images/blog/2019/11/perf_sqlite_add_objects.png"&gt;&lt;img alt="Adding objects in file-based storages." src="https://seecoresoftware.com/images/blog/2019/11/perf_sqlite_add_objects.thumbnail.png"&gt;&lt;/a&gt;
&lt;p class="caption"&gt;For small to medium sized write transactions, SQLite can actually
outperform FileStorage and ZEO when threads are in use. When
separate processes are in use, SQLite always beats ZEO.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="figure"&gt;
&lt;a class="reference external image-reference" href="https://seecoresoftware.com/images/blog/2019/11/perf_sqlite_read_cold.png"&gt;&lt;img alt="Reading objects in file-based storages." src="https://seecoresoftware.com/images/blog/2019/11/perf_sqlite_read_cold.thumbnail.png"&gt;&lt;/a&gt;
&lt;p class="caption"&gt;When reading objects, SQLite is always faster than ZEO, but slower
than FileStorage.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="performance-improvements"&gt;
&lt;span id="the-good-stuff"&gt;&lt;/span&gt;&lt;h2&gt;&lt;a class="toc-backref" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#id14"&gt;Performance Improvements&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Much effort was spent on improving RelStorage's performance, in terms
of overall speed and memory usage as well as concurrency and
scalability. Here, let's compare the performance of RelStorage 2.1.1 with
RelStorage 3.0 graphically from a speed perspective.&lt;/p&gt;
&lt;div class="section" id="writing"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#id15"&gt;Writing&lt;/a&gt;&lt;/h3&gt;
&lt;div class="admonition note"&gt;
&lt;p class="first admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;RelStorage 2 failed to complete a number of the write
benchmarks that used 20 objects and 20 concurrent processes
or threads: some tasks would fail to obtain the commit lock
using the default 10 second timeout. Those tasks were
excluded from the results.&lt;/p&gt;
&lt;p class="last"&gt;RelStorage 3 did not have this problem.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;We'll start with writing to the database:&lt;/p&gt;
&lt;a class="reference external image-reference" href="https://seecoresoftware.com/images/blog/2019/11/perf_comp_add_objects.png"&gt;&lt;img alt="Adding objects got much faster." src="https://seecoresoftware.com/images/blog/2019/11/perf_comp_add_objects.thumbnail.png"&gt;&lt;/a&gt;
&lt;p&gt;When simply adding new objects to the database, PostgreSQL is between
29% and 72% faster. MySQL is essentially within the margin of error
for the non-concurrent cases, and up to 79% faster for larger, more
concurrent tests.&lt;/p&gt;
&lt;p&gt;Updating existing objects:&lt;/p&gt;
&lt;a class="reference external image-reference" href="https://seecoresoftware.com/images/blog/2019/11/perf_comp_update_objects.png"&gt;&lt;img alt="Updating existing objects got much faster." src="https://seecoresoftware.com/images/blog/2019/11/perf_comp_update_objects.thumbnail.png"&gt;&lt;/a&gt;
&lt;p&gt;When updating objects that already existed in the database, The
difference for MySQL ranges between statistically insignificant up to
to 79% faster.. PostgreSQL likewise ranges from statistical
insignificance and up to 80% faster.&lt;/p&gt;
&lt;p&gt;Handling conflicts received extra attention.&lt;/p&gt;
&lt;div class="figure"&gt;
&lt;a class="reference external image-reference" href="https://seecoresoftware.com/images/blog/2019/11/perf_comp_update_conflicting_objects.png"&gt;&lt;img alt="Updating existing object that had conflicts got much faster." src="https://seecoresoftware.com/images/blog/2019/11/perf_comp_update_conflicting_objects.thumbnail.png"&gt;&lt;/a&gt;
&lt;p class="caption"&gt;Updating objects that have to resolve conflicts.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;Once again, some cases were statistically insignificant for both
databases. The cases that were statistically significant show a
15% to 84% improvement for PostgreSQL, with the range being from 11%
to 89% for MySQL. Not only is it faster, going by the much tighter
confidence intervals, it's also less volatile and more consistent.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="reading"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#id16"&gt;Reading&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;RelStorage, like ZEO, includes a secondary pickle cache that's shared
amongst all the connections in a process. Here's what it looks like to
read data out of that cache (not hitting the database at all).&lt;/p&gt;
&lt;div class="figure"&gt;
&lt;a class="reference external image-reference" href="https://seecoresoftware.com/images/blog/2019/11/perf_comp_read_hot.png"&gt;&lt;img alt="Reading objects from RelStorage's cache got much faster." src="https://seecoresoftware.com/images/blog/2019/11/perf_comp_read_hot.thumbnail.png"&gt;&lt;/a&gt;
&lt;p class="caption"&gt;Reading from the pickle cache, by concurrency kind and object
count.&lt;/p&gt;
&lt;div class="legend"&gt;
Ideally the bars for both databases of a particular RelStorage
release would be equal heights in any given test case because this
test is database independent. That's not quite the case. When they're
not, they're within each other's confidence intervals.&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Because the bottom row of graphs is separate processes, they don't
benefit much from the actual &lt;em&gt;sharing&lt;/em&gt; of the cache. The improvements
there, 30 – 40%, show the effect of the cache changes in isolation.
The top row of graphs shows the improvement in the intended use case,
when the cache is shared by multiple threads in a process. In that
case, the difference can be up to 90%. &lt;a class="footnote-reference" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#f4" id="id3"&gt;[5]&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Reading directly from the database, on the other hand, is harder to
qualify.&lt;/p&gt;
&lt;a class="reference external image-reference" href="https://seecoresoftware.com/images/blog/2019/11/perf_comp_read_cold.png"&gt;&lt;img alt="Reading directly from the database is mixed." src="https://seecoresoftware.com/images/blog/2019/11/perf_comp_read_cold.thumbnail.png"&gt;&lt;/a&gt;
&lt;p&gt;Oh no! It looks like RelStorage 3 actually got &lt;em&gt;slower&lt;/em&gt; when reading
directly from the database. That's one of its core tasks. How could
that be allowed to happen? (Spoiler alert: it didn't.)&lt;/p&gt;
&lt;p&gt;Look closely at the pattern. In the top row, when we're testing with
threads, RelStorage 3 is always at least as good as RelStorage 2 and
frequently better &lt;a class="footnote-reference" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#f3" id="id4"&gt;[4]&lt;/a&gt;. It's only the bottom row, dealing with
processes, that RelStorage 3 looks bad. But as you move further to the
right, where more processes are making more queries to load more
objects, the gap begins to close. At 20 objects in 5 processes, the
gap is essentially gone. (And then we fall off a cliff at 20 processes
querying 20 objects. It's not entirely clear exactly what's going on,
but that's more CPUs/hardware threads than either the client machine
or server machine has so it's not surprising that efficiency begins to
fall.)&lt;/p&gt;
&lt;p&gt;It turns out the benchmark includes the cost of opening a ZODB
connection. For processes, that's a connection using a whole new ZODB
instance, so there will be no prior connections open. But for threads,
the ZODB instance is shared, so there will be other connections in the
pool.&lt;/p&gt;
&lt;p&gt;Working with brand new RelStorage connections got a bit slower in
RelStorage 3 compared to RelStorage 2. They use more &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Prepared_statement"&gt;prepared
statements&lt;/a&gt; (especially on PostgreSQL), and they use more database
session state (especially on MySQL). Performing the first poll of the
database state may also be a bit more expensive. So when the
connection doesn't get used to do much before being closed, these
costs outweigh the other speedups in RelStorage 3. But somewhere
between making 5 and 20 queries for objects, the upfront costs are
essentially amortized away. As always in ZODB, connection pooling with
appropriate pool settings is important.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="gevent"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#id17"&gt;gevent&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In RelStorage 2, gevent was supported on Python 2 and Python 3 for
MySQL and PostgreSQL when using a pure-Python database driver
(typically PyMySQL for the former and pg8000 for the later). There was
special gevent support for MySQL using a custom database driver, but
only on Python 2. This driver took a hybrid approach, providing some C
acceleration of low-level operations, but delegating most operations to
PyMySQL.&lt;/p&gt;
&lt;p&gt;RelStorage 3 supports gevent-aware, fully native drivers, for both
PostgreSQL and MySQL on both Python 2 and Python 3. Moreover, the
MySQL driver has special support for RelStorage's two-phase commit
protocol, essentially boosting the priority of a greenlet that's
committing &lt;a class="footnote-reference" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#f5" id="id5"&gt;[6]&lt;/a&gt;. This avoid situations where a greenlet takes
database-wide locks and then yields control to a different greenlet
that starves the event loop, leaving the database locked for an
unacceptable amount of time and halting the forward progress of other
processes.&lt;/p&gt;
&lt;p&gt;Do these things make a difference? We can compare the performance of
gevent with MySQL to find out.&lt;/p&gt;
&lt;div class="figure"&gt;
&lt;a class="reference external image-reference" href="https://seecoresoftware.com/images/blog/2019/11/perf_gevent_add_objects.png"&gt;&lt;img alt="Adding objects is faster." src="https://seecoresoftware.com/images/blog/2019/11/perf_gevent_add_objects.thumbnail.png"&gt;&lt;/a&gt;
&lt;p class="caption"&gt;Adding objects, by concurrency type.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;Adding objects improved across the board for all concurrency types.&lt;/p&gt;
&lt;div class="figure"&gt;
&lt;a class="reference external image-reference" href="https://seecoresoftware.com/images/blog/2019/11/perf_gevent_update_objects.png"&gt;&lt;img alt="Updating objects is faster." src="https://seecoresoftware.com/images/blog/2019/11/perf_gevent_update_objects.thumbnail.png"&gt;&lt;/a&gt;
&lt;p class="caption"&gt;Updating objects, by concurrency type.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;Updating objects improved across the board for all concurrency types.
Updating conflicting objects shows a similar gain.&lt;/p&gt;
&lt;div class="figure"&gt;
&lt;a class="reference external image-reference" href="https://seecoresoftware.com/images/blog/2019/11/perf_gevent_read_cold.png"&gt;&lt;img alt="Reading from the database is largely unchanged." src="https://seecoresoftware.com/images/blog/2019/11/perf_gevent_read_cold.thumbnail.png"&gt;&lt;/a&gt;
&lt;p class="caption"&gt;Reading objects, by concurrency type.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;Reading individual objects, by contrast, shows no distinct trend. I
suspect that it's essentially unchanged (the transactional and polling
parts around it that were changed are not measured here), but we'd
need more samples to be able to properly show that.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="minor-changes"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#id18"&gt;Minor Changes&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This section documents some of the other changes in RelStorage 3.&lt;/p&gt;
&lt;div class="section" id="supported-versions"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#id19"&gt;Supported Versions&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Support for PostgreSQL 12 and MySQL 8 were added, as well as support
for Python 3.8.&lt;/p&gt;
&lt;p&gt;Support for MySQL 5.6 and PostgreSQL 9.5 were removed, as was support
for old versions of ZODB. Also, RelStorage no longer depends on ZEO
(so it's theoretically possible that Python 2.7.8 and earlier could
run RelStorage, but this isn't tested or recommended).&lt;/p&gt;
&lt;p&gt;Most tested database drivers were updated to newer versions, and in
some cases the minimum supported versions were updated.&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;mysqlclient must be 1.4, up from 1.3.7.&lt;/li&gt;
&lt;li&gt;psycopg2 must be 2.8, up from 1.6.1.&lt;/li&gt;
&lt;li&gt;psycopg2cffi must be 2.8.1, up from 2.7.4.&lt;/li&gt;
&lt;li&gt;cx_Oracle must be 6.0, up from 5.0.&lt;/li&gt;
&lt;li&gt;Support was removed for the Python 2-only driver &lt;tt class="docutils literal"&gt;umysqldb&lt;/tt&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This table summarizes the support various databases have for
RelStorage 3 features.&lt;/p&gt;
&lt;style&gt;
table {border-color: transparent}
&lt;/style&gt;&lt;table border="1" class="table table-striped colwidths-auto docutils"&gt;
&lt;caption&gt;Supported Features&lt;/caption&gt;
&lt;thead valign="bottom"&gt;
&lt;tr&gt;&lt;th class="head"&gt; &lt;/th&gt;
&lt;th class="head"&gt;PostgreSQL&lt;/th&gt;
&lt;th class="head"&gt;MySQL&lt;/th&gt;
&lt;th class="head"&gt;Oracle&lt;/th&gt;
&lt;th class="head"&gt;SQLite&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody valign="top"&gt;
&lt;tr&gt;&lt;td&gt;Parallel commit&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Shared readCurrent locks&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Non-blocking readCurrent locks&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Native on MySQL 8,
emulated on MySQL
5.7&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;N/A (there is
no distinction in lock type)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Streaming blobs&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No (emulated via
chunking)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No (consider configuring a
shared-blob-dir)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Central transaction ID allocation&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No (could probably be
implemented)&lt;/td&gt;
&lt;td&gt;N/A (but essentially yes
because it only involves one machine)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Atomic lock and commit without Python
involvement&lt;/td&gt;
&lt;td&gt;Yes (except
with
PG8000)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No (could probably be
implemented)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class="section" id="the-blob-cache"&gt;
&lt;span id="blob-cache"&gt;&lt;/span&gt;&lt;h3&gt;&lt;a class="toc-backref" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#id20"&gt;The Blob Cache&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Using a &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;shared-blob-dir&lt;/span&gt;&lt;/tt&gt; (where all blobs are &lt;em&gt;only&lt;/em&gt; stored on a
filesystem and never in the database) disables much of the parallel
commit features. This is because testing whether we can actually store
the blob successfully during the "vote" phase of ZODB's &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Two-phase_commit_protocol"&gt;two-phase commit&lt;/a&gt;
requires knowing the transaction ID, and knowing the transaction ID
requires taking the database-wide commit lock. This is much sooner
than is otherwise required and the lock is held for much longer (e.g.,
during conflict resolution).&lt;/p&gt;
&lt;p&gt;Increasing popularity, and ever-growing databases, make the
implementation of the blob &lt;em&gt;cache&lt;/em&gt; all the more important. This
release focused on blob cache maintenance, specifically the process
whereby the &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;blob-cache-size&lt;/span&gt;&lt;/tt&gt; (if any) is ensured.&lt;/p&gt;
&lt;p&gt;First, for history free databases, when a new revision of a blob is
uploaded to replace an older one, if RelStorage has the old revision
cached on disk and can determine that it's not in use, it will be
deleted as part of the commit process. This applies whether or not a
cache size limit is in place.&lt;/p&gt;
&lt;p&gt;If it becomes necessary to prune the blob cache, the process of doing
so has been streamlined. It spawns far fewer unnecessary threads than
it used to. If the process is using gevent, it uses an actual native
thread to do the disk scan and IO instead of a greenlet, which would
have blocked the event loop.&lt;/p&gt;
&lt;p&gt;Finally, if running the pruning process is still too expensive and the
thread interferes with the work of the process, there's a new option
that spawns a separate process to do the cleanup. This can also be
used manually to perform a cleanup without opening a storage.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="packing-and-gc"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#id21"&gt;Packing and GC&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;History-preserving databases now support &lt;a class="reference external" href="https://pypi.org/project/zc.zodbdgc/"&gt;zc.zodbdgc&lt;/a&gt; for
multi-database garbage collection.&lt;/p&gt;
&lt;p&gt;RelStorage's native packing is now safer for concurrent use in
history-free databases thanks to correcting several race conditions.&lt;/p&gt;
&lt;p&gt;For both types of databases, packing and pre-packing require
substantially less memory. Pre-packing a large database was measured to
use 9 times less memory on CPython 3 and 15 times less on CPython 2
(from 3GB to 200 MB).&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="performance-grab-bag"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#id22"&gt;Performance Grab Bag&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Here's a miscellaneous selection of interesting changes, mostly performance
related.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p class="first"&gt;Reduce the number of network communications with the database.&lt;/p&gt;
&lt;p&gt;RelStorage tries harder to avoid talking to the database more times
than necessary. Each round-trip introduces extra latency that was
measurable, even on fast connections. Also, native database drivers
usually release the GIL during a database operation, so there could
be extra overhead introduced acquiring it again. And under gevent,
making a query yields to the event loop, which is good, but it could
be an arbitrary amount of time before the greenlet regains control
to process the response. If locks are being held, too many queries
could spell disaster.&lt;/p&gt;
&lt;p&gt;This was accomplished in several ways. One way was to move larger
sequences of commands into stored procedures (MySQL and PostgreSQL
only). For example, previously to finish committing a transaction,
RelStorage required 7 database interactions in a history-preserving
database: 1 to acquire the lock, 1 to get the previous transaction
id, 1 to store transaction metadata, 1 to store objects, 1 to store
blobs, 1 to update the current object pointers, and finally one to
commit. Now, that's all handled by a single stored procedure using
one database operation. The Python process doesn't need to acquire
the GIL (or cycle through the event loop) to commit and release
locks, that happens immediately on the database server regardless of
how responsive the Python process is.&lt;/p&gt;
&lt;p&gt;More careful control of transactions eliminated several superflous
COMMIT or ROLLBACK queries in all databases. Similarly, more careful
tracking of allocated object identifiers (&lt;tt class="docutils literal"&gt;_p_oid&lt;/tt&gt;) eliminated
some unnecessary database updates.&lt;/p&gt;
&lt;p&gt;The use of &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Merge_(SQL)"&gt;upserts&lt;/a&gt;, which eliminate at least one query, was
previously limited to a select few places for PostgreSQL and MySQL.
That has been extended to more places for all supported databases.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Allocate transaction IDs on the database server.&lt;/p&gt;
&lt;p&gt;This was primarily about reducing database communications. However,
because transaction IDs are based on the current time, it also has
the important side-effect of ensuring that they're more consistently
meaningful with only one clock to consider.&lt;/p&gt;
&lt;p&gt;Previously, all transaction IDs could be at least as inaccurate as
the least-accurate clock writing to the database (if that clock was
in the future).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;PostgreSQL uses the &lt;tt class="docutils literal"&gt;COPY&lt;/tt&gt; command to upload data.&lt;/p&gt;
&lt;p&gt;Specifically, it uses the &lt;a class="reference external" href="https://www.postgresql.org/docs/11/sql-copy.html#id-1.9.3.55.9.4"&gt;binary format&lt;/a&gt; of the bulk-loading
&lt;tt class="docutils literal"&gt;COPY&lt;/tt&gt; command to stream data to the server. This can improve
storage times by 20% or so.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="conclusion"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#id23"&gt;Conclusion&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;RelStorage 3 represents a substantial change from RelStorage 2. The
pickle cache—both in-memory and on-disk—has been
completely rewritten, the locking process has been re-imagined in
support of parallel commit, time-sensitive logic moved into stored
procedures, and more. Despite that, it should be a drop-in replacement
in most situations.&lt;/p&gt;
&lt;p&gt;Although RelStorage 3 has been in production usage under heavy load at
NextThought for its entire development cycle, and we haven't
encountered any problems with it that could lead to data loss, it's
still software, and all software has bugs. Please exercise appropriate
care when upgrading. &lt;a class="reference external" href="https://github.com/zodb/relstorage/issues"&gt;Bug reports&lt;/a&gt; and &lt;a class="reference external" href="https://github.com/zodb/relstorage/pulls"&gt;pull requests&lt;/a&gt; are
encouraged and appreciated.&lt;/p&gt;
&lt;p&gt;We're very happy with the enhancements, especially around performance,
and hope those improvements are applicable to most users. We welcome
feedback on whether they are or are not, and also want to hear about
where else RelStorage could improve.&lt;/p&gt;
&lt;p&gt;Finally, I'd like to say thank you to everyone who has contributed to
the development of RelStorage 3, whether through testing pre-releases,
filing bug reports, or sharing enhancement ideas and use cases. It's
greatly appreciated.&lt;/p&gt;
&lt;p class="rubric"&gt;Footnotes&lt;/p&gt;
&lt;table class="docutils footnote" frame="void" id="f1" rules="none"&gt;
&lt;colgroup&gt;&lt;col class="label"&gt;&lt;col&gt;&lt;/colgroup&gt;
&lt;tbody valign="top"&gt;
&lt;tr&gt;&lt;td class="label"&gt;&lt;a class="fn-backref" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#id1"&gt;[1]&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Why not simply &lt;a class="reference external" href="https://dev.mysql.com/doc/refman/8.0/en/identifiers.html"&gt;"quote" the reserved word?&lt;/a&gt;
Much of the SQL queries that RelStorage uses are shared among
all supported databases. By default, MySQL uses a different,
non-standard quoting syntax that wouldn't work with the other
databases. That can be changed by altering the &lt;a class="reference external" href="https://dev.mysql.com/doc/refman/8.0/en/sql-mode.html"&gt;SQL Mode&lt;/a&gt;,
but I was trying to avoid having to do that. In the end it
turned out that another change &lt;a class="footnote-reference" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#f2" id="id6"&gt;[3]&lt;/a&gt; forced the alteration of the
mode, so I should have just done that in the first place. But
since there are good reasons to prevent RelStorage 2 and 3
from ever trying to use the same database, the
incompatibility didn't seem like a big deal.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table class="docutils footnote" frame="void" id="f6" rules="none"&gt;
&lt;colgroup&gt;&lt;col class="label"&gt;&lt;col&gt;&lt;/colgroup&gt;
&lt;tbody valign="top"&gt;
&lt;tr&gt;&lt;td class="label"&gt;&lt;a class="fn-backref" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#id2"&gt;[2]&lt;/a&gt;&lt;/td&gt;&lt;td&gt;In CPython, the &lt;a class="reference external" href="https://rushter.com/blog/python-garbage-collector/"&gt;generational (cyclic) garbage collector&lt;/a&gt; uses
time proportional to the number of objects in a generation
(all objects in a generation are stored in a linked list).
The more objects that exist, the longer it takes to perform a
collection. In RelStorage 2, storing a value in the cache
required creating several objects, and the Python garbage
collector would have to examine these. The RelStorage 3 cache
does not create objects that the garbage collector needs to
traverse. Similar remarks hold for PyPy.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table class="docutils footnote" frame="void" id="f2" rules="none"&gt;
&lt;colgroup&gt;&lt;col class="label"&gt;&lt;col&gt;&lt;/colgroup&gt;
&lt;tbody valign="top"&gt;
&lt;tr&gt;&lt;td class="label"&gt;&lt;a class="fn-backref" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#id6"&gt;[3]&lt;/a&gt;&lt;/td&gt;&lt;td&gt;That change was support for SQLite, which requires the entire
"transaction" table to be quoted. Amusingly, "transaction"
is a word reserved by the SQL standard, while "empty" is
not.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table class="docutils footnote" frame="void" id="f3" rules="none"&gt;
&lt;colgroup&gt;&lt;col class="label"&gt;&lt;col&gt;&lt;/colgroup&gt;
&lt;tbody valign="top"&gt;
&lt;tr&gt;&lt;td class="label"&gt;&lt;a class="fn-backref" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#id4"&gt;[4]&lt;/a&gt;&lt;/td&gt;&lt;td&gt;With the exception of PostgreSQL using one thread for 20
objects. The large error bar indicates an outlier event.
We're working with relatively small sample sizes, so that
throws things off.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table class="docutils footnote" frame="void" id="f4" rules="none"&gt;
&lt;colgroup&gt;&lt;col class="label"&gt;&lt;col&gt;&lt;/colgroup&gt;
&lt;tbody valign="top"&gt;
&lt;tr&gt;&lt;td class="label"&gt;&lt;a class="fn-backref" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#id3"&gt;[5]&lt;/a&gt;&lt;/td&gt;&lt;td&gt;RelStorage 2 implemented some of its cache functions using
native code called by &lt;a class="reference external" href="https://cffi.readthedocs.io"&gt;CFFI&lt;/a&gt;. When CFFI calls native
code, Python's GIL is dropped, allowing other threads to run
(as long as they weren't trying to use the cache, which used
Python locks for thread safety). In contrast, RelStorage 3
uses a thin layer of &lt;a class="reference external" href="https://cython.readthedocs.io"&gt;cython&lt;/a&gt; to call into C++ and does
&lt;em&gt;not&lt;/em&gt; drop the GIL—it depends on the GIL for thread
safety. The substantial speed improvement should outweigh the
loss of the tiny window where the GIL was dropped.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table class="docutils footnote" frame="void" id="f5" rules="none"&gt;
&lt;colgroup&gt;&lt;col class="label"&gt;&lt;col&gt;&lt;/colgroup&gt;
&lt;tbody valign="top"&gt;
&lt;tr&gt;&lt;td class="label"&gt;&lt;a class="fn-backref" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html#id5"&gt;[6]&lt;/a&gt;&lt;/td&gt;&lt;td&gt;&lt;p class="first"&gt;We don't have that much granular control over the PostgreSQL
driver (psycopg2). With MySQL (mysqlclient), on a
connection-by-connection basis we can control if a particular
query is going to yield to gevent or block. But with
psycopg2, whether it yields or not is global to the entire
process.&lt;/p&gt;
&lt;p class="last"&gt;The reverse is that psycopg2 actually gives us exactly the
same control as a gevent socket (yield each time any
read/write would block), whereas in mysqlclient we can only
wait for the first packet to arrive/go, after that it blocks
for the duration (server-side cursors give us a bit more
control, allowing yields between fetching groups of rows).&lt;/p&gt;
&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;&lt;/div&gt;</description><category>mysql</category><category>oracle</category><category>postgresql</category><category>python</category><category>releases</category><category>relstorage</category><category>sql</category><category>zodb</category><guid>https://seecoresoftware.com/blog/2019/11/relstorage-30.html</guid><pubDate>Tue, 12 Nov 2019 11:18:52 GMT</pubDate></item><item><title>Introduction to ZODB Data Storage</title><link>https://seecoresoftware.com/blog/2019/10/intro-zodb.html</link><dc:creator>Jason Madden</dc:creator><description>&lt;div&gt;&lt;p&gt;ZODB is a powerful native object database for Python, widely known for
its use in the &lt;a class="reference external" href="https://zope.readthedocs.io/en/latest/index.html"&gt;Zope&lt;/a&gt; web framework and
the &lt;a class="reference external" href="https://plone.org"&gt;Plone&lt;/a&gt; content management system. By
enabling transparent object graph persistence with no need to
predefine schemas, ZODB enables extremely flexible application
development. With pluggable storage engines such as &lt;a class="reference external" href="http://www.zodb.org/en/latest/reference/storages.html#filestorage"&gt;FileStorage&lt;/a&gt;,
&lt;a class="reference external" href="https://github.com/zopefoundation/ZEO"&gt;ZEO&lt;/a&gt;, and &lt;a class="reference external" href="https://relstorage.readthedocs.io"&gt;RelStorage&lt;/a&gt;, it also provides flexible ways to store data.&lt;/p&gt;
&lt;!-- TEASER_END --&gt;
&lt;p&gt;This post provides an introduction to ZODB, focusing on some of the
lower-level mechanics of storing data. This post doesn't discuss
&lt;a class="reference external" href="https://persistent.readthedocs.io"&gt;persistent objects&lt;/a&gt;.&lt;/p&gt;
&lt;div class="admonition admonition-disclaimer"&gt;
&lt;p class="first admonition-title"&gt;Disclaimer&lt;/p&gt;
&lt;p class="last"&gt;This was written in support of the &lt;a class="reference external" href="https://seecoresoftware.com/blog/2019/11/relstorage-30.html"&gt;RelStorage 3.0 release&lt;/a&gt; so it may be biased in that
direction. It is not an exhaustive list of all storage options. For
example, it doesn't discuss &lt;a class="reference external" href="https://neo.nexedi.com"&gt;NEO&lt;/a&gt;, a
distributed, redundant storage. Partial lists of included and
non-included storages may be found &lt;a class="reference external" href="http://www.zodb.org/en/latest/reference/storages.html"&gt;in the ZODB storage
documentation&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title first"&gt;Contents&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#what-is-zodb" id="id10"&gt;What is ZODB?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#what-is-a-zodb-storage" id="id11"&gt;What is a ZODB storage?&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="reference internal" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#filestorage" id="id12"&gt;FileStorage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#zeo" id="id13"&gt;ZEO&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#what-is-relstorage" id="id14"&gt;What is RelStorage?&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="reference internal" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#features" id="id15"&gt;Features&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#mvcc-and-history-free-storage" id="id16"&gt;MVCC and History Free Storage&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="reference internal" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#q-a" id="id17"&gt;Q &amp;amp; A&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#conflict-resolution" id="id18"&gt;Conflict Resolution&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#summary" id="id19"&gt;Summary&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#updates" id="id20"&gt;Updates&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="section" id="what-is-zodb"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#id10"&gt;What is ZODB?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a class="reference external" href="http://www.zodb.org/en/latest/"&gt;ZODB&lt;/a&gt; &lt;a class="footnote-reference" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#f1" id="id1"&gt;[1]&lt;/a&gt; is a native object database for Python, enabling
transparent object persistence. It provides the illusion of an
infinite memory space holding application-defined objects. That memory
space is shared between processes running at different times on the
same or different machine. Only those objects actually used are
brought into physical memory. Think of it something like operating
system &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Paging"&gt;paging&lt;/a&gt;, but for objects, and distributed across time and
space. (Apple's CoreData framework has a similar technique it calls "&lt;a class="reference external" href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreData/FaultingandUniquing.html"&gt;faulting&lt;/a&gt;".)&lt;/p&gt;
&lt;p&gt;In addition, ZODB provides a &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Database_transaction"&gt;transactional&lt;/a&gt; view of these objects with
&lt;a class="reference external" href="https://en.wikipedia.org/wiki/Multiversion_concurrency_control"&gt;snapshot isolation&lt;/a&gt;. Any given connection to the database sees a
consistent view of &lt;em&gt;all&lt;/em&gt; the objects in the database (whether it reads
or writes to any particular object or not) as-of the moment it began.
When adding or updating objects, no changes are published and made
visible to other connections until the writing connection &lt;em&gt;commits&lt;/em&gt;
its transaction, at which point either all the changes are made
visible or none of them are. Existing connections that continue
reading (or even writing!) will still not see those changes; they're
"stuck" at the snapshot view of the objects they started with. (The
ability for readers to continue to be able to retrieve old data that's
been replaced in newer transactions is known as multi-version
concurrency control, or &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Multiversion_concurrency_control"&gt;MVCC&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;Many connections may be reading and writing to the database at once.
ZODB uses &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Optimistic_concurrency_control"&gt;optimistic concurrency control&lt;/a&gt;. Readers don't block other
readers or writers, and writers are allowed to proceed as if they were
the only one making changes right up until they commit. Writes are
defined to occur in a strict order. If a writer discovers that an
earlier transaction had modified objects that it too wants to modify,
a conflict occurs. Instead of just rolling back the writing
transaction and forcing it to start over, taking the modified object
into account, ZODB gives the application the chance to resolve the
conflict using a &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Merge_(version_control)#Three-way_merge"&gt;three-way merge&lt;/a&gt; between the object as it existed
when the transaction began, the object that the connection wants to
commit, and the object that was committed by the other writer. Only if
it cannot do so is the transaction rolled back.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="what-is-a-zodb-storage"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#id11"&gt;What is a ZODB storage?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;ZODB uses a pluggable storage architecture, allowing different ways to
store the objects it manages. Storage engines are responsible for
allocating persistent object identifiers (OIDs) for each object ZODB
manages, storing object state data &lt;a class="footnote-reference" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#f2" id="id2"&gt;[2]&lt;/a&gt; when an object is added or
changed &lt;a class="footnote-reference" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#f2a" id="id3"&gt;[3]&lt;/a&gt;, and later retrieving the data for that particular object
given its OID. The storage is also responsible for implementing
snapshot isolation, ordering (serializing) writes and assigning
incrementing transaction identifiers (TIDs), and detecting and
handling conflicting writes.&lt;/p&gt;
&lt;div class="section" id="filestorage"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#id12"&gt;FileStorage&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Out of the box, in addition to a few different transient (in-memory)
storage engines, ZODB comes with one persistent (on-disk) storage
engine. &lt;a class="reference external" href="http://www.zodb.org/en/latest/reference/storages.html#filestorage"&gt;FileStorage&lt;/a&gt; uses a single file to store an append-only
transaction log for all the objects in the database. An additional
in-memory and on-disk structure is used to record the relationship
between objects (OIDs) and the transactions (TIDs) they appear in.&lt;/p&gt;
&lt;p&gt;As an append-only file, writing to FileStorage can be quite fast. It
requires memory (and extra storage space) proportional to the size of
the database to record object positions for fast access. If that extra
index data isn't saved to disk, it requires time proportional to the
size of the database to scan the file on startup to re-create that
index.&lt;/p&gt;
&lt;p&gt;Because of its append-only nature, previous versions of objects are
still found in the file and can be accessed by providing a proper TID.
A FileStorage is thus said to be "history preserving." That's how
snapshot isolation is implemented: each connection is explicitly
associated with a TID and when it needs to read an object it asks the
FileStorage to provide the revision of the object most recently
written &lt;em&gt;before&lt;/em&gt; that TID &lt;a class="footnote-reference" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#f3" id="id4"&gt;[4]&lt;/a&gt;. This can also be used like a
&lt;a class="reference external" href="https://en.wikipedia.org/wiki/Version_control"&gt;version control system&lt;/a&gt; to view and even recover or undo changes to
objects. Periodically, a FileStorage must be "packed" to remove
obsolete historical data and prevent the file from growing forever.&lt;/p&gt;
&lt;p&gt;FileStorage is widely deployed and has a long history of stability. It
can only be used by a single process at a time, however. Within that
process, only a single thread can be in the process of committing a
transaction at a time (FileStorage uses a database-wide lock to
provide serialization).&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="zeo"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#id13"&gt;ZEO&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;A common method to extend access to a FileStorage to more than one
process and/or to more than one machine is to deploy a &lt;a class="reference external" href="https://github.com/zopefoundation/ZEO"&gt;ZEO&lt;/a&gt; &lt;a class="footnote-reference" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#f4" id="id5"&gt;[5]&lt;/a&gt; server.
ZEO uses a client/server architecture. The server process opens one or
more storages (in practice, always a FileStorage &lt;a class="footnote-reference" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#f5" id="id6"&gt;[6]&lt;/a&gt;) and exposes a
network API to provide access to this storage. Client processes
connect to this server and send it read and write requests. The server
mediates access to the underlying storage for the clients.&lt;/p&gt;
&lt;p&gt;ZEO inherits many of the strengths and weaknesses of its underlying
storage and adds some of its own. For example, clients can be
configured with a persistent local cache for cheap access to common
objects or even read-only access when the server isn't available. But
the central ZEO process has to contend with Python's &lt;a class="reference external" href="https://speakerdeck.com/dabeaz/understanding-the-python-gil"&gt;GIL&lt;/a&gt;, which may
limit scalability, and it defaults to resolving conflicts by loading
application code into the server process, which can complicate
deployments due to the need to keep client and server processes all
running compatible code.&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external" href="https://github.com/zopefoundation/zc.zrs"&gt;ZRS&lt;/a&gt; &lt;a class="footnote-reference" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#f6" id="id7"&gt;[7]&lt;/a&gt; is a storage wrapper implemented in Python and commonly
wrapped around a ZEO storage that provides replication of data.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="what-is-relstorage"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#id14"&gt;What is RelStorage?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a class="reference external" href="https://relstorage.readthedocs.io"&gt;RelStorage&lt;/a&gt; is a ZODB storage engine that's meant to solve many of the
same problems as ZEO and ZRS, but taking a different approach with a
different set of tradeoffs. RelStorage uses a relational database—MySQL, PostgreSQL, Oracle, or SQLite—to provide the final
storage for object state data. It pushes the responsibility for OID
allocation, locks, transaction management and snapshot isolation, and
replication down to these systems.&lt;/p&gt;
&lt;p&gt;The next section is mostly a copy of RelStorage's own description of
its features. It makes references to ZEO and FileStorage described
above.&lt;/p&gt;
&lt;div class="section" id="features"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#id15"&gt;Features&lt;/a&gt;&lt;/h3&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;It is a drop-in replacement for FileStorage and ZEO, with several
enhancements:&lt;ul&gt;
&lt;li&gt;Supports undo, packing, and object history preservation just like
FileStorage.&lt;/li&gt;
&lt;li&gt;RelStorage can be configured &lt;em&gt;not&lt;/em&gt; to keep object histories for
reduced disk space usage and improved performance.&lt;/li&gt;
&lt;li&gt;Multiple processes on a single machine can read and write a local
ZODB database using SQLite without needing to start and manage
another process (i.e., ZEO).&lt;/li&gt;
&lt;li&gt;Blobs can be stored on a shared filesystem, or (recommended) in
the relational database and only cached locally.&lt;/li&gt;
&lt;li&gt;Multiple threads in the same process share a high-performance
in-memory pickle cache to reduce the number of queries to the
RDBMS. This is similar to ZEO, and the ZEO cache trace tools are
supported.&lt;/li&gt;
&lt;li&gt;The in-memory pickle cache can be saved to disk and read when a
process starts up. This can dramatically speed up site warmup time
by eliminating a flood of RDBMS queries. Unlike ZEO, this cache
is automatically shared by all processes on the machine (no need
to configure separate client identifiers.)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Ideal for large, high volume sites.&lt;ul&gt;
&lt;li&gt;Multiple Python processes on multiple machines can read and write
the same ZODB database concurrently. This is similar to ZEO, but
RelStorage does not require ZEO.&lt;/li&gt;
&lt;li&gt;Supports ZODB 5's parallel commit feature: Database writers only
block each other when they would conflict (except for a small
window at the end of the twophase commit protocol when the
transaction ID is allocated; that still requires a global database
lock).&lt;/li&gt;
&lt;li&gt;According to some tests, RelStorage handles concurrency better than
the standard combination of ZEO and FileStorage.&lt;/li&gt;
&lt;li&gt;Whereas FileStorage takes longer to start as the database grows
due to an in-memory index of all objects, RelStorage starts
quickly regardless of database size.&lt;/li&gt;
&lt;li&gt;Capable of failover to replicated SQL databases.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Tested integration with &lt;a class="reference external" href="http://gevent.org_"&gt;gevent&lt;/a&gt; for PostgreSQL and MySQL.&lt;/li&gt;
&lt;li&gt;There is a simple way (&lt;a class="reference external" href="https://relstorage.readthedocs.io/en/latest/zodbconvert.html_"&gt;zodbconvert&lt;/a&gt;) to (incrementally) convert
FileStorage to RelStorage and back again. You can also convert a
RelStorage instance to a different relational database. This is a
general tool that can be used to convert between any two ZODB
storage implementations.&lt;/li&gt;
&lt;li&gt;There is a simple way (&lt;a class="reference external" href="https://relstorage.readthedocs.io/en/latest/zodbpack.html_"&gt;zodbpack&lt;/a&gt;) to pack databases.&lt;/li&gt;
&lt;li&gt;Supports &lt;a class="reference external" href="https://relstorage.readthedocs.io/en/latest/zodburi.html"&gt;zodburi&lt;/a&gt; .&lt;/li&gt;
&lt;li&gt;Free, open source (ZPL 2.1)&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="section" id="mvcc-and-history-free-storage"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#id16"&gt;MVCC and History Free Storage&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;One thing in particular I'd like to highlight is that RelStorage can
implement snapshot isolation and conflict resolution &lt;em&gt;without&lt;/em&gt;
preserving history. To do this, it relies on the RDBMS's native
implementation of &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Multiversion_concurrency_control"&gt;MVCC&lt;/a&gt;, the &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Isolation_(database_systems)#Repeatable_reads"&gt;repeatable read&lt;/a&gt; isolation level, and
the read committed isolation level.&lt;/p&gt;
&lt;p&gt;When a transaction begins, a RDBMS transaction is opened on a
connection at the repeatable read (or higher) level. This connection
is used for loading data from the database. This isolation level
causes the RDBMS to establish its own snapshot view of the database
as-of that moment of time.&lt;/p&gt;
&lt;p&gt;A second connection is used to write data to the database. This
connection is in the &lt;em&gt;lower&lt;/em&gt; isolation level of simply read committed.
This level ensures that each query it makes to the database returns
the latest committed data. Objects being written are first placed in a
temporary table; they are moved to their final table (overwriting an
old revision for history free storages) only after any possible
conflicts have been found and resolved.&lt;/p&gt;
&lt;p&gt;The difference in the two connections' isolation levels matters
specifically because of conflict resolution, as does the use of a
temporary table. Recall that resolving conflicts needs three versions
of the object: the object that existed when the transaction began (the
original object), the object that is currently committed and was
changed by someone else (the committed object), and the object that
the writer would like to store (the new object). The task of the
conflict resolution is to find the delta between the original object
and the new object and apply those same changes to the committed
object. This produces a new object to store which will become the
committed object.&lt;/p&gt;
&lt;p&gt;Ignoring caches, the &lt;em&gt;only&lt;/em&gt; place that original object can come from
is that load connection at repeatable read isolation level. By
definition, any fresh connection or transaction that looked at the
database now would see the currently committed object (or something
even later)—the original object has been overwritten and that
change committed, so it's gone. RelStorage relies on the underlying
database to keep it visible to the load connection.&lt;/p&gt;
&lt;p&gt;Likewise, getting the currently committed object requires a connection
that can read the current state of the database. That's where the
second connection comes in. It can see the current data in the
database.&lt;/p&gt;
&lt;div class="section" id="q-a"&gt;
&lt;h4&gt;&lt;a class="toc-backref" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#id17"&gt;Q &amp;amp; A&lt;/a&gt;&lt;/h4&gt;
&lt;!-- ATTENTION AUTHOR: Topic text must be on a single line. Watch your wrapping! --&gt;
&lt;div class="topic"&gt;
&lt;p class="topic-title first"&gt;Why two connections? Why not put the data in the temporary table, commit, and begin a new transaction to update the current view of the database?&lt;/p&gt;
&lt;p&gt;Because that would lose access to the original object.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="topic"&gt;
&lt;p class="topic-title first"&gt;Why not preemptively store off all the original objects somewhere (e.g., download them or copy them to a temp table) before committing?&lt;/p&gt;
&lt;p&gt;Because ZODB uses an optimistic concurrency model. We assume
that conflicts are few and far between. If that's true, that would
be doing a bunch of extra work that we don't usually need to do.
Remember, there's no way to know if there's going to be a conflict
or not without a current view of the database.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="topic"&gt;
&lt;p class="topic-title first"&gt;Well then, why not just have a single shared connection for the current view of the database and use it to check for conflicts and only then save the original objects that have conflicts?&lt;/p&gt;
&lt;p&gt;Because that connection wouldn't know what objects to check for
conflicts on. Those objects are already in the database in
temporary tables that are connection specific and unreadable to a
different connection. We'd have to pass a list of object IDs back
to the database, and not all databases support array operations to
do that efficiently. Or we'd have to write to a persistent table,
which doesn't sound appealing (we'd have to arrange to delete from
it too.)&lt;/p&gt;
&lt;p&gt;Also, because RDBMS connections aren't thread-safe, that would
introduce a per-process lock into the commit process.&lt;/p&gt;
&lt;p&gt;Still, perhaps that's worth looking into more.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="topic"&gt;
&lt;p class="topic-title first"&gt;Couldn't a history-preserving database implement snapshot isolation just like FileStorage and use only one read committed connection?&lt;/p&gt;
&lt;p&gt;Quite possibly, yes. That could make for some moderately ugly or
inefficient SQL queries though.&lt;/p&gt;
&lt;pre class="code sql"&gt;&lt;a name="rest_code_3b3b230cefa14b94bc3d86499f41cb9e-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;a name="rest_code_3b3b230cefa14b94bc3d86499f41cb9e-2"&gt;&lt;/a&gt;&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;object_state&lt;/span&gt;
&lt;a name="rest_code_3b3b230cefa14b94bc3d86499f41cb9e-3"&gt;&lt;/a&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;zoid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;zoid&lt;/span&gt;
&lt;a name="rest_code_3b3b230cefa14b94bc3d86499f41cb9e-4"&gt;&lt;/a&gt;&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;tid&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;tid&lt;/span&gt;
&lt;a name="rest_code_3b3b230cefa14b94bc3d86499f41cb9e-5"&gt;&lt;/a&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;tid&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;a name="rest_code_3b3b230cefa14b94bc3d86499f41cb9e-6"&gt;&lt;/a&gt;&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;div class="topic"&gt;
&lt;p class="topic-title first"&gt;Why temp tables? Why not store directly to the final table?&lt;/p&gt;
&lt;p&gt;For history free databases, the final table is where we get the
data to resolve conflicts, so we can't overwrite it.&lt;/p&gt;
&lt;p&gt;For history preserving databases, we don't yet have the necessary
transaction ID we need to store to the final table. (The primary
key is (OID, TID), and the TID is a foreign key reference to
another tables as well).&lt;/p&gt;
&lt;p&gt;We could allocate the TID earlier, before storing temporary data,
but that defeats much of the benefit of ZODB 5 parallel commit.&lt;/p&gt;
&lt;p&gt;We could use a fake TID and update it in-place, but altering
primary keys tends to be expensive.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="conflict-resolution"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#id18"&gt;Conflict Resolution&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;RelStorage supports conflict resolution. Conflict resolution is
performed in each individual process in a distributed fashion. There's
no central server that has to be updated with application code in
order to resolve conflicts. ZEO 5 supports a similar feature.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="summary"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#id19"&gt;Summary&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;ZODB is a flexible and powerful object database for Python, supporting
transactions, optimistic concurrency, and conflict resolution. It uses
a layered architecture with the definition and serialization of
individual objects handled by the &lt;tt class="docutils literal"&gt;persistent&lt;/tt&gt; library, the
generic transactional API provided by &lt;a class="reference external" href="https://transaction.readthedocs.io"&gt;transaction&lt;/a&gt;, and data storage and MVCC
semantics provided by the pluggable storage layer.&lt;/p&gt;
&lt;p&gt;ZODB comes with a storage implementation using an append-only file, as
well as an in-memory &lt;a class="reference external" href="https://github.com/zopefoundation/ZODB/blob/master/src/ZODB/MappingStorage.py"&gt;dict-based&lt;/a&gt;
storage plus a change-tracking &lt;a class="reference external" href="https://github.com/zopefoundation/ZODB/blob/master/src/ZODB/DemoStorage.py"&gt;demo storage&lt;/a&gt;.
These are all restricted to a single process, but ZEO allows utilizing
them from multiple processes.&lt;/p&gt;
&lt;p&gt;RelStorage is a storage layer based on a SQL database, intended to be
highly scalable.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="updates"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#id20"&gt;Updates&lt;/a&gt;&lt;/h2&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;Add additional links to more resources about included and
third-party storages.&lt;/li&gt;
&lt;/ul&gt;
&lt;p class="rubric"&gt;Footnotes&lt;/p&gt;
&lt;table class="docutils footnote" frame="void" id="f1" rules="none"&gt;
&lt;colgroup&gt;&lt;col class="label"&gt;&lt;col&gt;&lt;/colgroup&gt;
&lt;tbody valign="top"&gt;
&lt;tr&gt;&lt;td class="label"&gt;&lt;a class="fn-backref" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#id1"&gt;[1]&lt;/a&gt;&lt;/td&gt;&lt;td&gt;ZODB may stand for "Zope Object Database," or &lt;a class="reference external" href="https://github.com/zopefoundation/ZODB/pull/137/files"&gt;it may stand for&lt;/a&gt; "Z Object Database."&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table class="docutils footnote" frame="void" id="f2" rules="none"&gt;
&lt;colgroup&gt;&lt;col class="label"&gt;&lt;col&gt;&lt;/colgroup&gt;
&lt;tbody valign="top"&gt;
&lt;tr&gt;&lt;td class="label"&gt;&lt;a class="fn-backref" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#id2"&gt;[2]&lt;/a&gt;&lt;/td&gt;&lt;td&gt;To the storage engine, the object state data is just an
opaque sequence of bytes. In reality, the ZODB Connection
uses Python's standard &lt;a class="reference external" href="https://docs.python.org/3/library/pickle.html"&gt;pickle&lt;/a&gt; protocol
to serialize objects into bytes.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table class="docutils footnote" frame="void" id="f2a" rules="none"&gt;
&lt;colgroup&gt;&lt;col class="label"&gt;&lt;col&gt;&lt;/colgroup&gt;
&lt;tbody valign="top"&gt;
&lt;tr&gt;&lt;td class="label"&gt;&lt;a class="fn-backref" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#id3"&gt;[3]&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Storages also handle non-object data in the form of &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Binary_large_object"&gt;BLOBs&lt;/a&gt;, each
of which is associated with an object and assigned an OID.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table class="docutils footnote" frame="void" id="f3" rules="none"&gt;
&lt;colgroup&gt;&lt;col class="label"&gt;&lt;col&gt;&lt;/colgroup&gt;
&lt;tbody valign="top"&gt;
&lt;tr&gt;&lt;td class="label"&gt;&lt;a class="fn-backref" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#id4"&gt;[4]&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Actually, the layer that implements snapshot isolation on
top of an arbitrary history preserving storage is
&lt;a class="reference external" href="https://github.com/zopefoundation/ZODB/blob/master/src/ZODB/mvccadapter.py"&gt;found in the core of ZODB&lt;/a&gt;.
This was one of the major changes in ZODB 5.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table class="docutils footnote" frame="void" id="f4" rules="none"&gt;
&lt;colgroup&gt;&lt;col class="label"&gt;&lt;col&gt;&lt;/colgroup&gt;
&lt;tbody valign="top"&gt;
&lt;tr&gt;&lt;td class="label"&gt;&lt;a class="fn-backref" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#id5"&gt;[5]&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Previously "&lt;a class="reference external" href="https://github.com/zopefoundation/ZEO/commit/6770c28e46ac1c6585894d20546cd2e11978d4d5#diff-88b99bb28683bd5b7e3a204826ead112L10"&gt;Zope Enterprise Objects&lt;/a&gt;".&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table class="docutils footnote" frame="void" id="f5" rules="none"&gt;
&lt;colgroup&gt;&lt;col class="label"&gt;&lt;col&gt;&lt;/colgroup&gt;
&lt;tbody valign="top"&gt;
&lt;tr&gt;&lt;td class="label"&gt;&lt;a class="fn-backref" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#id6"&gt;[6]&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Though one possibly wrapped in something like &lt;a class="reference external" href="http://www.zodb.org/en/latest/reference/storages.html#optional-layers"&gt;zlibstorage&lt;/a&gt;
to provide compression or &lt;tt class="docutils literal"&gt;cipher.encryptingstorage&lt;/tt&gt; to
provide encryption.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table class="docutils footnote" frame="void" id="f6" rules="none"&gt;
&lt;colgroup&gt;&lt;col class="label"&gt;&lt;col&gt;&lt;/colgroup&gt;
&lt;tbody valign="top"&gt;
&lt;tr&gt;&lt;td class="label"&gt;&lt;a class="fn-backref" href="https://seecoresoftware.com/blog/2019/10/intro-zodb.html#id7"&gt;[7]&lt;/a&gt;&lt;/td&gt;&lt;td&gt;"ZODB Replicated Storage"&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;&lt;/div&gt;</description><category>mysql</category><category>oracle</category><category>postgresql</category><category>python</category><category>relstorage</category><category>sql</category><category>zodb</category><guid>https://seecoresoftware.com/blog/2019/10/intro-zodb.html</guid><pubDate>Mon, 28 Oct 2019 18:07:52 GMT</pubDate></item></channel></rss>