<?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 releases)</title><link>https://seecoresoftware.com/</link><description></description><atom:link href="https://seecoresoftware.com/blog/categories/releases.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></channel></rss>