<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Rodrigo Rosenfeld Rosas — infrastructure</title><description>Articles tagged with infrastructure</description><link>https://rosenfeld.page/</link><language>en-us</language><item><title>Upgrading PostgreSQL from 9.6 to 10 with minimal downtime using pglogical</title><link>https://rosenfeld.page/articles/infrastructure/2017_11_10_upgrading_postgresql_from_9_6_to_10_with_minimal_downtime_using_pglogical/</link><guid isPermaLink="true">https://rosenfeld.page/articles/infrastructure/2017_11_10_upgrading_postgresql_from_9_6_to_10_with_minimal_downtime_using_pglogical/</guid><pubDate>Fri, 10 Nov 2017 15:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Once PostgreSQL 10 was released I wanted to upgrade our 9.6 cluster to the newest version. However,
it would require a lot of coordination effort to get a maintenance window to perform the migration
the way I was used to: put the application in maintenance mode, get a new dump and restore it to
the new cluster and switch off the maintenance mode.&lt;/p&gt;
&lt;p&gt;That means the application wouldn&amp;#39;t be available for an hour or so, maybe more. After reading once
more about &lt;a href=&quot;https://www.2ndquadrant.com/en/resources/pglogical/&quot;&gt;pglogical&lt;/a&gt;, I decided to finally
give it a try, which allowed me to switch from 9.6 to 10 in just a few seconds.&lt;/p&gt;
&lt;h2&gt;How it works - a higher level view&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;pglogical&lt;/em&gt; implements logical replication, which allows replicating databases among different
versions, which is not possible with the binary replication mechanism provided by PostgreSQL
itself. Well, PG 10 added some support to logical replication, but since we want to replicate
from 9.6, we&amp;#39;d need to resort to some external extension.&lt;/p&gt;
&lt;p&gt;A required condition from &lt;em&gt;pglogical&lt;/em&gt; is that all tables being replicated must have a primary key.
It doesn&amp;#39;t need to be a single column, but a primary key must exist. Superuser access must also
be provided for both databases for the replication agents. DDL replication is not supported.
Truncate cascades are not replicated. Nothing fancy, after all. It should allow us to replicate
most databases.&lt;/p&gt;
&lt;p&gt;You should pay special attention to the primary key requirement though, specially if you&amp;#39;re
using the ActiveRecord Ruby gem to manage the database migrations in older databases as the
&lt;em&gt;schema_migrations&lt;/em&gt; table didn&amp;#39;t have a primary key in the earlier days. If that&amp;#39;s your case:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;alter table schema_migrations add primary key (version);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The idea is to install a PostgreSQL package with support for the pglogical extension, then
create the new PG 10 cluster and restore the schema only in the new cluster. The current cluster
should be stopped and restarted using the pglogical-enabled installed PostgreSQL. The
clusters should be reachable to it other through TCP/IP. You&amp;#39;ll need to tell the provider
(the 9.6 database being upgraded) the IP and port for the subscriber (the new PG 10 database)
and vice-versa. The &lt;em&gt;pglogical&lt;/em&gt; extension is created in both databases, &lt;em&gt;postgresql.conf&lt;/em&gt; and
&lt;em&gt;pg_hba.conf&lt;/em&gt; are changed to enable logical replication and both databases are restarted.
Finally, some &lt;em&gt;pglogical&lt;/em&gt; statements are issued to create the provider, subscriber and
subscription, which starts the replication. Once the replication is finished, you may change
the port in the new cluster to match the old one, stop the old cluster and restart the new one.
Finally it would be a good idea to restart the applications as well, specially if you&amp;#39;re using
some custom types such as row types, as they will most likely have different OIDs and if you
have registered those row types it won&amp;#39;t work as expected until you reboot the application.
This would be the case if you&amp;#39;re using &lt;em&gt;DB.register_row_type&lt;/em&gt; using the Sequel Ruby gem, for
example.&lt;/p&gt;
&lt;p&gt;The final switch can happen in as quickly as a few seconds, which means minimal downtime.&lt;/p&gt;
&lt;h2&gt;How it works - hands on&lt;/h2&gt;
&lt;p&gt;We use Docker to run PostgreSQL in our servers (besides the apps), so this article also uses it to
demonstrate how the process works, but it should be easy to apply the instructions to other kind
of set-ups. The advantage of Docker as demonstration tool is that these procedures should be
easy to replicate as is and it also takes care of creating and running the databases as well.&lt;/p&gt;
&lt;p&gt;We assume the PostgreSQL client is installed in the host too for this article.&lt;/p&gt;
&lt;h3&gt;Prepare the images and start-up script&lt;/h3&gt;
&lt;p&gt;Create the following Dockerfiles in sub-directories pg96 and pg10 (look at the instructions
inside the Dockerfiles in order to replicate in your own environment if you&amp;#39;re not running
PostgreSQL in a Docker container):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;# pg96/Dockerfile
FROM postgres:9.6

RUN apt-get update &amp;amp;&amp;amp; apt-get install -y wget gnupg
RUN echo &amp;quot;deb [arch=amd64] http://packages.2ndquadrant.com/pglogical/apt/ jessie-2ndquadrant main&amp;quot; &amp;gt; /etc/apt/sources.list.d/2ndquadrant.list \
  &amp;amp;&amp;amp; wget --quiet -O - http://packages.2ndquadrant.com/pglogical/apt/AA7A6805.asc | apt-key add - \
  &amp;amp;&amp;amp; apt-get update \
  &amp;amp;&amp;amp; apt-get install -y postgresql-9.6-pglogical

RUN echo &amp;quot;host    replication          postgres                172.18.0.0/16   trust&amp;quot; &amp;gt;&amp;gt; /usr/share/postgresql/9.6/pg_hba.conf.sample
RUN echo &amp;quot;host    replication          postgres                ::1/128         trust&amp;quot; &amp;gt;&amp;gt; /usr/share/postgresql/9.6/pg_hba.conf.sample
RUN echo &amp;quot;shared_preload_libraries = &amp;#39;pglogical&amp;#39;&amp;quot; &amp;gt;&amp;gt; /usr/share/postgresql/postgresql.conf.sample
RUN echo &amp;quot;wal_level = &amp;#39;logical&amp;#39;&amp;quot; &amp;gt;&amp;gt; /usr/share/postgresql/postgresql.conf.sample
RUN echo &amp;quot;max_wal_senders = 20&amp;quot; &amp;gt;&amp;gt; /usr/share/postgresql/postgresql.conf.sample
RUN echo &amp;quot;max_replication_slots = 20&amp;quot; &amp;gt;&amp;gt; /usr/share/postgresql/postgresql.conf.sample
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;# pg10/Dockerfile
FROM postgres:10

RUN rm /etc/apt/trusted.gpg &amp;amp;&amp;amp; apt-get update &amp;amp;&amp;amp; apt-get install -y wget
RUN echo &amp;quot;deb [arch=amd64] http://packages.2ndquadrant.com/pglogical/apt/ stretch-2ndquadrant main&amp;quot; &amp;gt; /etc/apt/sources.list.d/2ndquadrant.list \
  &amp;amp;&amp;amp; wget --quiet -O - http://packages.2ndquadrant.com/pglogical/apt/AA7A6805.asc | apt-key add - \
  &amp;amp;&amp;amp; apt-get update \
  &amp;amp;&amp;amp; apt-get install -y postgresql-10-pglogical

RUN echo &amp;quot;host    replication          postgres                172.18.0.0/16   trust&amp;quot; &amp;gt;&amp;gt; /usr/share/postgresql/10/pg_hba.conf.sample
RUN echo &amp;quot;host    replication          postgres                ::1/128         trust&amp;quot; &amp;gt;&amp;gt; /usr/share/postgresql/10/pg_hba.conf.sample
RUN echo &amp;quot;shared_preload_libraries = &amp;#39;pglogical&amp;#39;&amp;quot; &amp;gt;&amp;gt; /usr/share/postgresql/postgresql.conf.sample
RUN echo &amp;quot;wal_level = &amp;#39;logical&amp;#39;&amp;quot; &amp;gt;&amp;gt; /usr/share/postgresql/postgresql.conf.sample
RUN echo &amp;quot;max_wal_senders = 20&amp;quot; &amp;gt;&amp;gt; /usr/share/postgresql/postgresql.conf.sample
RUN echo &amp;quot;max_replication_slots = 20&amp;quot; &amp;gt;&amp;gt; /usr/share/postgresql/postgresql.conf.sample
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&amp;#39;s assume both servers will run in the same machine with IP 10.0.1.10. The 9.6 instance is
running on port 5432 and the new cluster will be running initially (before the switch) in port
5433.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;cd pg96 &amp;amp;&amp;amp; docker build . -t postgresql-pglogical:9.6 &amp;amp;&amp;amp; cd -
cd pg10 &amp;amp;&amp;amp; docker build . -t postgresql-pglogical:10 &amp;amp;&amp;amp; cd -
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is not a tutorial on Docker, but if you&amp;#39;re actually using Docker, it would be a good idea to
push those images to your private registry.&lt;/p&gt;
&lt;p&gt;The first step is to stop the old 9.6 cluster and start the &lt;em&gt;pglogical&lt;/em&gt; enabled cluster with the
old data (taking a backup before is always a good idea by the way). Suppose your cluster data is
located at &amp;quot;/var/lib/postgresql/9.6/main/&amp;quot; and that your config files are located at
&amp;quot;/etc/postgresql/9.6/main/&amp;quot;. If &amp;quot;/etc/postgresql/9.6&amp;quot; and &amp;quot;/var/lib/postgresql/9.6&amp;quot; do not exist,
don&amp;#39;t worry, the script will create a new cluster for you (in case you want to try with new dbs,
first, which is a good idea by the way, and map some temp directories).&lt;/p&gt;
&lt;p&gt;Create the following script at &amp;quot;/sbin/pg-scripts/start-pg&amp;quot; and make it executable. It will run
the database from the container.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;#!/bin/bash
version=$1
net=$2
setup_db(){
  pg_createcluster $version main -o listen_addresses=&amp;#39;*&amp;#39; -o wal_level=logical \
        -o max_wal_senders=10 -o max_worker_processes=10 -o max_replication_slots=10 \
        -o hot_standby=on -o max_wal_senders=10 -o shared_preload_libraries=pglogical -- -A trust
  pghba=/etc/postgresql/$version/main/pg_hba.conf
  echo -e &amp;quot;host\tall\tappuser\t$net\ttrust&amp;quot; &amp;gt;&amp;gt; $pghba
  echo -e &amp;quot;host\treplication\tappuser\t$net\ttrust&amp;quot; &amp;gt;&amp;gt; $pghba
  echo -e &amp;quot;host\tall\tpostgres\t172.17.0.0/24\ttrust&amp;quot; &amp;gt;&amp;gt; $pghba
  echo -e &amp;quot;host\treplication\tpostgres\t172.17.0.0/24\ttrust&amp;quot; &amp;gt;&amp;gt; $pghba
  pg_ctlcluster $version main start
  psql -U postgres -c &amp;#39;\du&amp;#39; postgres|grep -q appuser || createuser -U postgres -l -s appuser
  pg_ctlcluster $version main stop
}
[ -d /var/lib/postgresql/$version/main ] || setup_db
exec pg_ctlcluster --foreground $version main start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This script will take care of creating a new cluster if one doesn&amp;#39;t already exist. Although not
really required for the replication to work, it also takes care of creating a new &amp;quot;appuser&amp;quot;
database superuser authenticated with &amp;quot;trust&amp;quot; for simplicity sake. It might be useful if you
decide to use this script for spawning new databases for testing purposes. Adapt the script to
suite your needs in that case, changing the user name or the authentication methods.&lt;/p&gt;
&lt;h3&gt;Run the containers&lt;/h3&gt;
&lt;p&gt;Let&amp;#39;s run the 9.6 cluster in port 5432 (feel free to run it in another port and use a temporary
directory in the mappings if you just want to give it a try):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;docker run --rm -v /sbin/pg-scripts:/pg-scripts -v /var/lib/postgresql:/var/lib/postgresql \
    -v /etc/postgresql:/etc/postgresql -p 5432:5432 postgres-pglogical:9.6 \
    /pg-scripts/start-pg 9.6 10.0.1.0/24
# since we&amp;#39;re running in the foreground with the --rm option, run this in another terminal:
docker run --rm -v /sbin/pg-scripts:/pg-scripts -v /var/lib/postgresql:/var/lib/postgresql \
    -v /etc/postgresql:/etc/postgresql -p 5433:5432 postgres-pglogical:10 \
    /pg-scripts/start-pg 10 10.0.1.0/24
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first argument to &lt;em&gt;start-pg&lt;/em&gt; is the PG version and the second and last argument is the
net used to create &lt;em&gt;pg_hba.conf&lt;/em&gt; if it doesn&amp;#39;t exist, to allow &amp;quot;appuser&amp;quot; to connect from using the
&amp;quot;trust&amp;quot; authentication method.&lt;/p&gt;
&lt;p&gt;If you&amp;#39;re curious about how to run a Docker container as a systemd service, let me know in the
comments section below and I may complement this article once I find some time, but it&amp;#39;s not hard.
There are plenty of documents explaining that in the internet, but our own service unit file is
a bit different from what I&amp;#39;ve seen in most tutorials, as it tries to check that the port is
indeed accepting connections when starting the service and it doesn&amp;#39;t pull the image from the
registry if it is available locally already.&lt;/p&gt;
&lt;h3&gt;Edit PostgreSQL configuration&lt;/h3&gt;
&lt;p&gt;Once you make sure the old cluster is running file with the postgresql-pglogical container, it&amp;#39;s
time to update your &lt;em&gt;postgresql.conf&lt;/em&gt; file and restart the container. Use the following
configuration as a start-point for both 9.6 and 10 clusters:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;wal_level = logical
max_worker_processes = 10
max_replication_slots = 10
max_wal_senders = 10
shared_preload_libraries = &amp;#39;pglogical&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For &lt;em&gt;pg_hba.conf&lt;/em&gt;, include the following lines (change the network settings if you&amp;#39;re not using
Docker, or if you&amp;#39;re running the containers in another net than the default one):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;host    all     postgres        172.17.0.0/24   trust
host    replication     postgres        172.17.0.0/24   trust
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Restart the servers and we should be ready for starting the replication.&lt;/p&gt;
&lt;h2&gt;Replicating the database&lt;/h2&gt;
&lt;h3&gt;Set up the provider&lt;/h3&gt;
&lt;p&gt;In the PG 9.6 database:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# take a dump from the schema that we&amp;#39;ll use to restore in PG 10
pg_dump -Fc -s -h 10.0.1.10 -p 5432 -U appuser mydb &amp;gt; mydb-schema.dump
psql -h 10.0.1.10 -p 5432 -c &amp;#39;create extension pglogical;&amp;#39; -U appuser mydb
psql -h 10.0.1.10 -p 5432 -c &amp;quot;select pglogical.create_node(node_name := &amp;#39;provider&amp;#39;, dsn := &amp;#39;host=10.0.1.10 port=5432 dbname=mydb&amp;#39;);&amp;quot; -U appuser mydb
psql -h 10.0.1.10 -p 5432 -c &amp;quot;select pglogical.replication_set_add_all_tables(&amp;#39;default&amp;#39;, ARRAY[&amp;#39;public&amp;#39;]);&amp;quot; -U appuser mydb

# I couldn&amp;#39;t get sequences replication to work, so I&amp;#39;ll suggest another method just before switching the database
# psql -h 10.0.1.10 -p 5432 -c &amp;quot;select pglogical.replication_set_add_all_sequences(&amp;#39;default&amp;#39;, ARRAY[&amp;#39;public&amp;#39;]);&amp;quot; -U appuser mydb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This mark all tables and sequences from the public schema to be replicated.&lt;/p&gt;
&lt;h3&gt;Set up the subscriber and subscription&lt;/h3&gt;
&lt;p&gt;In the PG 10 database:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# create and restore the schema of the database
createdb -U appuser -h 10.0.1.10 -p 5433 mydb
pg_restore -s -h 10.0.1.10 -p 5433 -U appuser -d mydb mydb-schema.dump
# install the pglogical extension and setup the subscriber and subscription
psql -h 10.0.1.10 -p 5433 -c &amp;#39;create extension pglogical;&amp;#39; -U appuser mydb
psql -h 10.0.1.10 -p 5433 -c &amp;quot;select pglogical.create_node(node_name := &amp;#39;subscriber&amp;#39;, dsn := &amp;#39;host=10.0.1.10 port=5433 dbname=mydb&amp;#39;);&amp;quot; -U appuser mydb
psql -h 10.0.1.10 -p 5433 -c &amp;quot;select pglogical.create_subscription(subscription_name := &amp;#39;subscription&amp;#39;, provider_dsn := &amp;#39;host=10.0.1.10 port=5432 dbname=mydb&amp;#39;);&amp;quot; -U appuser mydb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From now on you can follow the status of the replication with&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select pglogical.show_subscription_status(&amp;#39;subscription&amp;#39;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once the initialization is over and the databases are synced and replicating (this may take quite
a while depending on your database size) you may start the switch.&lt;/p&gt;
&lt;h3&gt;Replicating the sequence values&lt;/h3&gt;
&lt;p&gt;At this point the replication database is almost all set. I couldn&amp;#39;t figure out how to replicate
the sequence values, so, if you&amp;#39;re using serial integer primary key columns relying on sequences,
then you&amp;#39;ll also want to set proper values to the sequences otherwise you won&amp;#39;t be able to
insert new records while relying on the serial sequence next value. Here&amp;#39;s how you can do that.
Just to be sure, it&amp;#39;s inserting a 5000 gap so that you have enough time to stop the old server
after gererating the set-value statements in case your database is very write intensive. You
should probably review that gap value depending on how quickly your database might grow up between
running those scripts and stopping the server.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;psql -h 10.0.1.10 -p 5432 -U appuser -c &amp;quot;select string_agg(&amp;#39;select &amp;#39;&amp;#39;select setval(&amp;#39;&amp;#39;&amp;#39;&amp;#39;&amp;#39; || relname || &amp;#39;&amp;#39;&amp;#39;&amp;#39;&amp;#39;, &amp;#39;&amp;#39; || last_value + 5000 || &amp;#39;&amp;#39;)&amp;#39;&amp;#39; from &amp;#39; || relname, &amp;#39; union &amp;#39; order by relname) from pg_class where relkind =&amp;#39;S&amp;#39;;&amp;quot; -t -q -o set-sequences-values-generator.sql mydb
psql -h 10.0.1.10 -p 5432 -U appuser -t -q -f set-sequences-values-generator.sql -o set-sequences-values.sql mydb
# set the new sequence values in the new database (port 5433 in this example):
psql -h 10.0.1.10 -p 5433 -U appuser -f set-sequences-values.sql mydb
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Final switch steps&lt;/h3&gt;
&lt;p&gt;Then, basically, you should change the port for the PG10 cluster and set it to 5432 (or whatever
was the port the old cluster was using).  Then stop the 9.6 cluster (Ctrl+C in the example above)
and restart the new cluster. Finally, it&amp;#39;s a good idea to also restart the apps using the
database, just in case they are relying on some custom types whose conversion rules would depend
on the row type OID.&lt;/p&gt;
&lt;p&gt;This assumes your apps are able to gracefully handle disconnections for the connections in the
pool by using some connection validation before issuing any SQL statements. Otherwise, it&amp;#39;s
probably a good idea to restart the apps whenever you restart the database after tweaking
&amp;quot;postgresql.conf&amp;quot; and &amp;quot;pg_hba.conf&amp;quot;.&lt;/p&gt;
&lt;h2&gt;Clean-up&lt;/h2&gt;
&lt;p&gt;Once everything is running fine with the new database, you might want to clean things up. If that&amp;#39;s
the case:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select pglogical.drop_subscription(&amp;#39;subscription&amp;#39;);
select pglogical.drop_node(&amp;#39;subscriber&amp;#39;);
drop extension pglogical;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I hope that helps you getting your database upgraded with minimal downtime.&lt;/p&gt;
</content:encoded></item><item><title>A sample Ruby script to achieve fast incremental back-up on btrfs partition</title><link>https://rosenfeld.page/articles/infrastructure/2016_06_24_a_sample_ruby_script_to_achieve_fast_incremental_back_up_on_btrfs_partition/</link><guid isPermaLink="true">https://rosenfeld.page/articles/infrastructure/2016_06_24_a_sample_ruby_script_to_achieve_fast_incremental_back_up_on_btrfs_partition/</guid><pubDate>Fri, 24 Jun 2016 15:31:00 GMT</pubDate><content:encoded>&lt;p&gt;For some years I have been using &lt;a href=&quot;http://rsnapshot.org/&quot;&gt;rsnapshot&lt;/a&gt; to back up our databases
and documents using an incremental approach. We create a new back-up every hour and retain the
last 24 hours backup, one back-up per day for the past 7 days and one back-up per week for the
past 4 weeks.&lt;/p&gt;
&lt;p&gt;Rsnapshot is great. It uses hard-links to achieve incremental back-up, saving up a lot of space.
It&amp;#39;s a combination of &amp;quot;cp -al&amp;quot; and rsync.  But we were facing a problem related to free inodes
count on our ext4 partition. By the way, NewRelic does not monitor the free inodes count (df -i)
so I found this problem the hard way, after the back-up stopped working due to lack of free inodes.&lt;/p&gt;
&lt;p&gt;I&amp;#39;ve created a custom check in our own monitoring system to alert about low free inodes available
and then I tried to tweak some ext4 settings to avoid this problem again in the new partition.
We have 26GB spread on 2.6 million of individually gzipped documents (they are served directly by
nginx) which will create almost 100 million hard-links in that back-up partition. There are
hardlinks around the original documents as well as part of a smart strategy to save space when the
same document is used in multiple transactions (they are not changed). Otherwise they would take
some extra Gigabytes.&lt;/p&gt;
&lt;p&gt;Recently, my custom monitoring system sent me an alert that 75% of the inodes were used while
about only 30% of disk space was being actually used. So, I decided to investigate a bit more
about other filesystems which dealt with inodes dynamically.&lt;/p&gt;
&lt;h2&gt;The btrfs filesystem&lt;/h2&gt;
&lt;p&gt;That&amp;#39;s how I found btrfs, a modern file-system which not only does not have a limit on inodes but,
as I&amp;#39;ll describe, has some very interesting features for dealing with incremental back-up in a
faster and better way than rsnapshot.&lt;/p&gt;
&lt;p&gt;Initially I wasn&amp;#39;t thinking about replacing rsnapshot, but after reading about support for
subvolumes and snapshots in btrfs I changed my mind and decided to replace rsnapshot with a custom
script. I&amp;#39;ve tried to adapt rsnapshot for several hours to make the workflow I wanted work
without success though. Here&amp;#39;s &lt;a href=&quot;https://github.com/rsnapshot/rsnapshot/issues/20&quot;&gt;an issue&lt;/a&gt;
related to btrfs support.&lt;/p&gt;
&lt;p&gt;Before I talk about how btrfs helps our back-up system, let me explain a few issues I had with
rsnapshot.&lt;/p&gt;
&lt;h2&gt;Rsnapshot issues&lt;/h2&gt;
&lt;p&gt;I&amp;#39;ve been living with some issues with rsnapshot in the past years. I want the full back-up
procedure to take less than an hour so that we would be able to run it every hour. I had to
tweak its settings a few times in order to get the script to finish in less than an hour but
in the past days it was taking already almost 40 minutes to complete. A while back, before the
tweaks, I had to change the interval to back-up every two hours.&lt;/p&gt;
&lt;p&gt;One of the slow parts of rsnapshot is removing the last back-up snapshot when rotating. It doesn&amp;#39;t
matter if you use &amp;quot;rm -rf&amp;quot; or whatever other method. &lt;a href=&quot;http://blog.liw.fi/posts/rm-is-too-slow/&quot;&gt;Removing a big tree of files is slow&lt;/a&gt;. An alternative would be to move the latest snapshot to the
first one (hourly.0), since this would save the &amp;quot;rm -rf&amp;quot; time and also the &amp;quot;cp -al&amp;quot; time, skipping
to the rsync phase. But I wasn&amp;#39;t able to figure out how to make that happens with rsnapshot.&lt;/p&gt;
&lt;p&gt;Also, some of the procedures could be done in parallel to speed up the process but rsnapshot
doesn&amp;#39;t provide direct support to specify this and it&amp;#39;s hard to write proper shell script to
manage those cases.&lt;/p&gt;
&lt;h2&gt;The goal&lt;/h2&gt;
&lt;p&gt;After reading about btrfs I figured out that the back-up procedure could be made much faster and
be simplified. Then I created a Ruby script, which I&amp;#39;ll show in the next section, and integrated
it in our automation tools in one day. I&amp;#39;ve replaced rsnapshot with it in our back-up server,
with the new script and it&amp;#39;s running pretty well for the last two days taking about 8 minutes to
complete the procedure on each run.&lt;/p&gt;
&lt;p&gt;So, let me explain the strategy I wanted to implement to help you understanding the script.&lt;/p&gt;
&lt;p&gt;As I said, btrfs supports subvolumes. Btrfs implements copy-on-write (CoW), so basically, 
this allows to both create and delete snapshots from subvolumes instantly (constant time). That
means we replace the slow &amp;quot;rm -rf hourly.23&amp;quot; with the instantaneous
&amp;quot;btrfs subvolume delete hourly.23&amp;quot; and &amp;quot;cp -al ...&amp;quot; with the instantaneous
&amp;quot;btrfs subvolume snapshot ...&amp;quot;.&lt;/p&gt;
&lt;p&gt;In order for a regular user to delete subvolumes with btrfs, the user_subvol_rm_allowed fs option
must be used. Also, deleting a subvolume doesn&amp;#39;t work if there are other subvolumes inside it, so
they must be removed first. There&amp;#39;s no switch or tool in the btrfs-progs package that allows you
to delete them recursively. This is important to understand the script.&lt;/p&gt;
&lt;p&gt;Our back-up procedure consists of getting a recent dump of two production PostgreSQL databases
(the main database and the one used by Redmine) and syncing two directories containing files
(the main application files and the files uploaded to Redmine).&lt;/p&gt;
&lt;p&gt;The idea is to get them inside a static path as the first step. The main reason for that is that
if something goes wrong in the process after syncing the documents (the slowest part), for example,
we wouldn&amp;#39;t lose the transferred files the next time we try to run the script. So, basically
here&amp;#39;s how I implemented it (there&amp;#39;s a simpler strategy I&amp;#39;ll explain next):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;/var/backups/latest [regular directory]&lt;/li&gt;
&lt;li&gt;/var/backups/latest/postgres [subvolume] - the main db dump is stored here&lt;/li&gt;
&lt;li&gt;/var/backups/latest/tickets-db [subvolume] - the tickets db dump is stored here&lt;/li&gt;
&lt;li&gt;/var/backups/latest/docmanager [subvolume] - the 2.6 million documents are rsynced here&lt;/li&gt;
&lt;li&gt;/var/backups/latest/tickets-files [subvolume] - Redmine files go here&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After the procedure is finished to get them in the latest state it creates a tmp directory and
create a snapshot for each subvolume inside tmp and once everything works fine the back-ups are
rotated and tmp is moved to hourly.0. Removing hourly.23 in the rotation phase has to remove the
inner subvolumes first.&lt;/p&gt;
&lt;p&gt;After implementing this (it was an iterative process) I realized it could be simplified to use
a simpler infra-structure. &amp;quot;latest&amp;quot; would be a subvolume and everything inside it regular files
and directories. Than the &amp;quot;tmp&amp;quot; directory wouldn&amp;#39;t be used and after rotating a snapshot of
&amp;quot;latest&amp;quot; would be used to create &amp;quot;hourly.0&amp;quot;. I didn&amp;#39;t update the script yet because I&amp;#39;m not sure
if it worths changing, since the current layout is more modular, which is useful in case I want
to take some snapshot of just part of the back-up for some reason. So the sample back-up script
in the next section will use my current tested approach, which is the situation described first
above.&lt;/p&gt;
&lt;p&gt;The main database has over 500MB in PostgreSQL custom format, and it&amp;#39;s much faster to rsync it
than using scp. Initially those databases were not stored in the &amp;quot;latest&amp;quot; diretory and I used
&amp;quot;scp&amp;quot; to copy them directly to the &amp;quot;tmp&amp;quot; directory, but I changed the strategy to save some time
and bandwidth.&lt;/p&gt;
&lt;p&gt;The script should exit with a message and non zero exit status code when something fails so that
I would be notified if anything goes wrong by Cron (by setting the MAILTO=&lt;a href=&quot;mailto:my@email.com&quot;&gt;my@email.com&lt;/a&gt; in the
beggining of the crontab file). It shouldn&amp;#39;t affect the existing valid snapshots either in that
case.&lt;/p&gt;
&lt;p&gt;It shouldn&amp;#39;t run in case the previous procedure hasn&amp;#39;t finish, so there&amp;#39;s a simple lock mechanism
preventing that from happen in case it takes over an hour to complete. The second attempt will
fail and I should get an e-mail telling me that happened.&lt;/p&gt;
&lt;p&gt;It should also have a dry-run mode (which I call test mode) that will output the commands without
running it, which is useful while designing the back-up steps. It should also allow for commands
to run concurrently so it uses some indentation to show the order the commands are run.&lt;/p&gt;
&lt;p&gt;Finally, it will report in the logs the issued commands and their status (finished or failed) as
well as any commands output (STDOUT or STDERR) and the time each command took as well as the total
time in the end of the procedure.&lt;/p&gt;
&lt;p&gt;Finally, now that you understand what the script is supposed to do, here&amp;#39;s the actual
implementation.&lt;/p&gt;
&lt;h2&gt;The script&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;#!/usr/bin/env ruby

require &amp;#39;open3&amp;#39;
require &amp;#39;thread&amp;#39;
require &amp;#39;logger&amp;#39;
require &amp;#39;time&amp;#39;

class Backup
  def run(args)
    @start_time = Time.now
    @backup_root_path = File.expand_path &amp;#39;/var/backups&amp;#39;
    #@backup_root_path = File.expand_path &amp;#39;~/backups&amp;#39;
    @log_path = &amp;quot;#{@backup_root_path}/backup.log&amp;quot;
    @tmp_path = &amp;quot;#{@backup_root_path}/tmp&amp;quot;

    @exiting = false
    Thread.current[:indenting_level] = 0

    setup_logger

    lock_or_exit

    log &amp;#39;Starting back-up procedure&amp;#39;

    parse_args args.clone

    run_scripts if @action == &amp;#39;hourly&amp;#39;

    rotate
    unlock
    report_completed
  end

  private

  def setup_logger
    File.write @log_path, &amp;#39;&amp;#39; unless File.exist? @log_path
    logfile = File.open(@log_path, File::WRONLY | File::APPEND)
    logfile.sync = true
    @logger = Logger.new logfile
    @logger.level = Logger::INFO
    @logger.datetime_format = &amp;#39;%Y-%m-%d %H:%M:%S&amp;#39;
    @logger_mutex = Mutex.new
  end

  def lock_or_exit
    if File.exist?(pidfile) &amp;amp;&amp;amp; run_command(&amp;quot;kill -0 #{pid = File.read pidfile}&amp;quot;)
      abort &amp;quot;There&amp;#39;s another backup in progress. Pid: #{pid} (from #{pidfile}).&amp;quot;
    end
    File.write pidfile, Process.pid
  end

  def unlock
    File.unlink pidfile
  end

  def pidfile
    @pidfile ||= &amp;quot;#{@backup_root_path}/backup.pid&amp;quot;
  end

  def run_command!(cmd, sucess_in_test_mode = true, abort_on_stderr: false)
    run_command cmd, sucess_in_test_mode, abort_on_stderr: abort_on_stderr, abort_on_error: true
  end

  def run_command(cmd, sucess_in_test_mode = true, abort_on_stderr: false, abort_on_error: false)
    indented_cmd = &amp;#39; &amp;#39; * indenting_level + cmd
    Thread.current[:indenting_level] += 1
    if @test_mode
      @logger_mutex.synchronize{ puts indented_cmd}
      return sucess_in_test_mode
    end
    start = Time.now
    log &amp;quot;started:  &amp;#39;#{indented_cmd}&amp;#39;&amp;quot;
    stdout, stderr, status = Open3.capture3 cmd
    stdout = stdout.chomp
    stderr = stderr.chomp
    success = status == 0
    log stdout unless stdout.empty?
    log stderr, :warn unless stderr.empty?
    if (!success &amp;amp;&amp;amp; abort_on_error) || (abort_on_stderr &amp;amp;&amp;amp; !stderr.empty?)
      die &amp;quot;&amp;#39;#{cmd}&amp;#39; failed to run with exit status #{status}, aborting.&amp;quot;
    end
    log &amp;quot;finished: &amp;#39;#{indented_cmd}&amp;#39; (#{success ? &amp;#39;successful&amp;#39; : &amp;quot;failed with #{status}&amp;quot;}) &amp;quot; +
      &amp;quot;[#{human_duration Time.now - start}]&amp;quot;
    success
  end

  def indenting_level
    Thread.current[:indenting_level]
  end

  def log(msg, level = :info)
    return if @test_mode
    @logger_mutex.synchronize{ @logger.send level, msg }
  end

  VALID_OPTIONS = [&amp;#39;hourly&amp;#39;, &amp;#39;daily&amp;#39;, &amp;#39;weekly&amp;#39;].freeze
  def parse_args(args)
    args.shift if @test_mode = (args.first == &amp;#39;test&amp;#39;)
    unless args.size == 1 &amp;amp;&amp;amp; VALID_OPTIONS.include?(@action = args.first)
      abort &amp;quot;Usage: &amp;#39;backup [test] action&amp;#39;, where action can be hourly, daily or weekly.
            If test is specified the commands won&amp;#39;t run but will be shown.&amp;quot;
    end
  end

  def die(message)
    log message, :fatal
    was_exiting = @exiting
    @exiting = true
    delete_tmp_path_if_exists unless was_exiting
    unlock
    abort message
  end

  def create_tmp_path
    delete_tmp_path_if_exists
    create_subvolume @tmp_path
  end

  def create_subvolume(path, skip_if_exists = false)
    return if skip_if_exists &amp;amp;&amp;amp; File.exist?(path)
    run_script %Q{btrfs subvolume create &amp;quot;#{path}&amp;quot;}
  end

  def delete_tmp_path_if_exists
    delete_subvolume_if_exists @tmp_path, delete_children: true
  end

  def delete_subvolume_if_exists(path, delete_children: false)
    return unless File.exist?(path)
    Dir[&amp;quot;#{path}/*&amp;quot;].each{|s| delete_subvolume_if_exists s } if delete_children
    run_script %Q{btrfs subvolume delete -c &amp;quot;#{path}&amp;quot;}
  end

  def run_script(script)
    run_command! script
  end

  def run_scripts(scripts = all_scripts)
    case scripts
    when Par
      il = indenting_level
      last_il = il
      scripts.map do |s|
        Thread.start do
          Thread.current[:indenting_level] = il
          run_scripts s
          last_il = [Thread.current[:indenting_level], last_il].max
        end
      end.each &amp;amp;:join
      Thread.current[:indenting_level] = last_il
    when Array
      scripts.each{|s| run_scripts s }
    when String
      run_script scripts
    when Proc
      scripts[]
    else
      die &amp;quot;Invalid script (#{scripts.class}): #{scripts}&amp;quot;
    end
  end

  Par = Class.new Array
  def all_scripts
    [
      Par[-&amp;gt;{create_tmp_path}, &amp;quot;mkdir -p #{@backup_root_path}/latest&amp;quot;, dump_main_db_on_d1,
          dump_tickets_db_on_d1],
      Par[local_docs_sync, local_tickets_files_sync, local_main_db_sync, local_tickets_db_sync],
      Par[main_docs_script, tickets_files_script, main_db_script, tickets_db_script],
    ]
  end

  def dump_main_db_on_d1
    %q{ssh backup@backup-server.com &amp;quot;pg_dump -Fc -f /tmp/main_db.dump } +
      %q{main_db_production&amp;quot;}
  end

  def dump_tickets_db_on_d1
    %q{ssh backup@backup-server.com &amp;quot;pg_dump -Fc -f /tmp/tickets.dump redmine_production&amp;quot;}
  end

  def local_docs_sync
    [
      -&amp;gt;{ create_subvolume local_docmanager, true },
      &amp;quot;rsync -azHq --delete-excluded --delete --exclude doc --inplace &amp;quot; +
        &amp;quot;backup@backup-server.com:/var/main-documents/production/docmanager/ &amp;quot; +
        &amp;quot;#{local_docmanager}/&amp;quot;,
    ]
  end

  def local_docmanager
    @local_docmanager ||= &amp;quot;#{@backup_root_path}/latest/docmanager&amp;quot;
  end

  def local_tickets_files_sync
    [
      -&amp;gt;{ create_subvolume local_tickets_files, true },
      &amp;quot;rsync -azq --delete --inplace backup@backup-server.com:/var/redmine/files/ &amp;quot; +
        &amp;quot;#{local_tickets_files}/&amp;quot;,
    ]
  end

  def local_tickets_files
    @local_tickets_files ||= &amp;quot;#{@backup_root_path}/latest/tickets-files&amp;quot;
  end

  def local_main_db_sync
    [
      -&amp;gt;{ create_subvolume local_main_db, true },
      &amp;quot;rsync -azq --inplace backup@backup-server.com:/tmp/main_db.dump &amp;quot; +
        &amp;quot;#{local_main_db}/main_db.dump&amp;quot;,
    ]
  end

  def local_main_db
    @local_main_db ||= &amp;quot;#{@backup_root_path}/latest/postgres&amp;quot;
  end

  def local_tickets_db_sync
    [
      -&amp;gt;{ create_subvolume local_tickets_db, true },
      &amp;quot;rsync -azq --inplace backup@backup-server.com:/tmp/tickets.dump &amp;quot; +
        &amp;quot;#{local_tickets_db}/tickets.dump&amp;quot;,
    ]
  end

  def local_tickets_db
    @local_tickets_db ||= &amp;quot;#{@backup_root_path}/latest/tickets-db&amp;quot;
  end

  def main_docs_script
    create_snapshot_cmd local_docmanager, &amp;quot;#{@tmp_path}/docmanager&amp;quot;
  end

  def create_snapshot_cmd(from, to)
    &amp;quot;btrfs subvolume snapshot #{from} #{to}&amp;quot;
  end

  def main_db_script
    create_snapshot_cmd local_main_db, &amp;quot;#{@tmp_path}/postgres&amp;quot;
  end

  def tickets_db_script
    create_snapshot_cmd local_tickets_db, &amp;quot;#{@tmp_path}/tickets-db&amp;quot;
  end

  def tickets_files_script
    create_snapshot_cmd local_tickets_files, &amp;quot;#{@tmp_path}/tickets-files&amp;quot;
  end

  LAST_DIR_PER_TYPE = {
    &amp;#39;hourly&amp;#39; =&amp;gt; 23, &amp;#39;daily&amp;#39; =&amp;gt; 6, &amp;#39;weekly&amp;#39; =&amp;gt; 3
  }.freeze
  def rotate
    last = LAST_DIR_PER_TYPE[@action]
    path = -&amp;gt;(n, action = @action){ &amp;quot;#{@backup_root_path}/#{action}.#{n}&amp;quot; }
    delete_subvolume_if_exists path[last], delete_children: true
    n = last
    while (n -= 1) &amp;gt;= 0
      run_script &amp;quot;mv #{path[n]} #{path[n+1]}&amp;quot; if File.exist?(path[n])
    end
    dest = path[0]
    case @action
    when &amp;#39;hourly&amp;#39;
      run_script &amp;quot;mv #{@tmp_path} #{dest}&amp;quot;
    when &amp;#39;daily&amp;#39;, &amp;#39;weekly&amp;#39;
      die &amp;#39;last hourly back-up does not exist&amp;#39; unless File.exist?(hourly0 = path[0, &amp;#39;hourly&amp;#39;])
      create_tmp_path
      Dir[&amp;quot;#{hourly0}/*&amp;quot;].each do |subvolume|
        run_script create_snapshot_cmd subvolume, &amp;quot;#{@tmp_path}/#{File.basename subvolume}&amp;quot;
      end
      run_script &amp;quot;mv #{@tmp_path} #{dest}&amp;quot;
    end
  end

  def report_completed
    log &amp;quot;Backup finished in #{human_duration Time.now - @start_time}&amp;quot;
  end

  def human_duration(total_time_sec)
    n = total_time_sec.round
    parts = []
    [60, 60, 24].each{|d| n, r = n.divmod d; parts &amp;lt;&amp;lt; r; break if n.zero?}
    parts &amp;lt;&amp;lt; n unless n.zero?
    pairs = parts.reverse.zip(%w(d h m s)[-parts.size..-1])
    pairs.pop if pairs.size &amp;gt; 2 # do not report seconds when irrelevant
    pairs.flatten.join
  end
end

Backup.new.run(ARGV) if File.expand_path($PROGRAM_NAME) == File.expand_path(__FILE__)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, this is what I get running the test mode:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ ruby backup.rb test hourly
btrfs subvolume create &amp;quot;/home/rodrigo/backups/tmp&amp;quot;
mkdir -p /home/rodrigo/backups/latest
ssh backup@backup-server.com &amp;quot;pg_dump -Fc -f /tmp/main_db.dump main_db_production&amp;quot;
ssh backup@backup-server.com &amp;quot;pg_dump -Fc -f /tmp/tickets.dump redmine_production&amp;quot;
 btrfs subvolume create &amp;quot;/home/rodrigo/backups/latest/docmanager&amp;quot;
 btrfs subvolume create &amp;quot;/home/rodrigo/backups/latest/tickets-files&amp;quot;
 btrfs subvolume create &amp;quot;/home/rodrigo/backups/latest/postgres&amp;quot;
 btrfs subvolume create &amp;quot;/home/rodrigo/backups/latest/tickets-db&amp;quot;
  rsync -azHq --delete-excluded --delete --exclude doc --inplace backup@backup-server.com:/var/main-documents/production/docmanager/ /home/rodrigo/backups/latest/docmanager/
  rsync -azq --delete --inplace backup@backup-server.com:/var/redmine/files/ /home/rodrigo/backups/latest/tickets-files/
  rsync -azq --inplace backup@backup-server.com:/tmp/main_db.dump /home/rodrigo/backups/latest/postgres/main_db.dump
  rsync -azq --inplace backup@backup-server.com:/tmp/tickets.dump /home/rodrigo/backups/latest/tickets-db/tickets.dump
   btrfs subvolume snapshot /home/rodrigo/backups/latest/tickets-db /home/rodrigo/backups/tmp/tickets-db
   btrfs subvolume snapshot /home/rodrigo/backups/latest/tickets-files /home/rodrigo/backups/tmp/tickets-files
   btrfs subvolume snapshot /home/rodrigo/backups/latest/postgres /home/rodrigo/backups/tmp/postgres
   btrfs subvolume snapshot /home/rodrigo/backups/latest/docmanager /home/rodrigo/backups/tmp/docmanager
    mv /home/rodrigo/backups/tmp /home/rodrigo/backups/hourly.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &amp;quot;all_scripts&amp;quot; method is the one you should adapt for your needs.&lt;/p&gt;
&lt;h2&gt;Final notes&lt;/h2&gt;
&lt;p&gt;I hope that script will help you serving as a base for your own back-up script in Ruby in case I
was able to convince you to give this strategy a try. Unless you are already using some robust
back-up solution such as Bacula or other advanced systems, this strategy is very simple to
implement, takes little space and allows for fast incremental backups and might interest you.&lt;/p&gt;
&lt;p&gt;Please let me know if you have any questions in the comments section or if you&amp;#39;d suggest any
improvements over it. Or if you think you&amp;#39;ve found a bug I&amp;#39;d love to hear about it.&lt;/p&gt;
&lt;p&gt;Good luck dealing with your back-ups. :)&lt;/p&gt;
</content:encoded></item></channel></rss>