wingedpig.com Open in urlscan Pro
2600:3c03::f03c:92ff:fe6e:ce0c  Public Scan

URL: http://wingedpig.com/
Submission: On February 24 via api from US — Scanned from DE

Form analysis 0 forms found in the DOM

Text Content

wingedpig
 * About
 * Posts




MARK FLETCHER'S BLOG


MIGRATED TO HUGO

By Mark Fletcher

December 24, 2022 - 2 minutes read - 362 words
Baobab Tree with ostriches in the distrance, Tarangire National Park, Tanzania
There's a new sun arisin' (In your eyes) I can see a new horizon (Realize) That
will keep me realizin' You're the biggest part of me Biggest Part Of Me,
Ambrosia

I have migrated this blog from Wordress to Hugo. I did so mainly because
Wordpress wouldn’t let me do a few things. I wanted to add a rel=me link so that
Mastodon would associate my @wingedpig@hachyderm.io account with this blog. And,
I wanted to be able to customize the blog more than Wordpress would allow me to
do. These are my rough notes on how I did the migration.

First, I exported my Wordpress blog. Then I used the Blogger To Markdown tool to
convert the exported Wordpress files to markdown files compatible with Hugo.

I am currently hosting the blog using Linode’s block storage system. I followed
the instructions in Deploy a Static Site using Hugo and Object Storage to set
that up.

I am using the Ananke theme, but I have customized it. The biggest change was
making the home page display full blog posts, instead of summaries. I did that
by creating a new layouts/index.html file, using the
themes/ananke/layouts/index.html file as a template.

Another important change I made was with the RSS file. Hugo defaults to having
the RSS file at /index.xml, but my Wordpress blog’s RSS feed was at /feed. I
didn’t want to lose my existing RSS subscribers. To change the RSS URL, I had to
make two changes. In the config.toml file, I had to add the following:

[mediaTypes]
  [mediaTypes.'application/rss']
    suffixes = []

[outputFormats]
  [outputFormats.RSS]
    mediatype = "application/rss"
    baseName = "feed"


I also had to copy the internal Hugo RSS template into the file
layouts/index.rss.

The final change I had to make was to set the blog post URL structure to match
the old Wordpress blog structure, so that links to old blog posts would not be
broken. To do that, I added the following to the config.toml file:

[permalinks]
  posts = '/:year/:month/:day/:title/'


--------------------------------------------------------------------------------

Groups.io is the best tool to get a bunch of people organized and sharing
knowledge. Start a free trial group today.

--------------------------------------------------------------------------------


JSON CONFIGURATION INCLUDES

By Mark Fletcher

December 13, 2022 - 2 minutes read - 298 words
Ceiling of a building in the Citadel, Amman, Jordan But the drum-beat strains of
the night remain In the rhythm of the newborn day You know sometime you're bound
to leave her But for now you're going to stay In the year of the cat - Year Of
The Cat, Al Stewart

Everything needs configuration data. Some people use .toml files, others use
.ini or .yaml. I don’t like any of those file formats and I also just prefer
JSON for data in general. So at groups.io all our configuration data is in JSON
formatted files. But I wanted the ability for config files to include other
config files, which is not something that JSON normally supports, so I wrote a
small utility function to support including JSON files in other JSON files. This
function is used by every program that runs the groups.io backend.

This file contains bidirectional Unicode text that may be interpreted or
compiled differently than what appears below. To review, open the file in an
editor that reveals hidden Unicode characters. Learn more about bidirectional
Unicode characters
Show hidden characters

// ReadConfig reads in a JSON-formatted configuration file, looking for any //
include directives, and recursively reading those files as well. Include
directives // are specified as a list keyed on the "Includes" key. func
ReadConfig(filename string, configuration interface{}) error { // grab any path
to the config file to add to the include names var prefix string slashindex :=
strings.LastIndex(filename, "/") if slashindex != -1 { prefix =
filename[:slashindex+1] } file, err := os.Open(filename) if err != nil {
log.Println("Can't read", filename) return err } decoder :=
json.NewDecoder(file) if err := decoder.Decode(configuration); err != nil {
log.Printf("Error in %s\n", filename) log.Println(err) return err } v :=
reflect.ValueOf(configuration).Elem() f := v.FieldByName("Includes") // make
sure that this field is defined, and can be changed. if !f.IsValid() { //
configuration doesn't have an Includes slice return nil } if f.Kind() !=
reflect.Slice { return nil } myincludes, ok := f.Interface().([]string) if !ok {
// configuration doesn't have an Includes slice return nil } // clear out the
slice f.Set(reflect.MakeSlice(f.Type(), 0, f.Cap())) for _, include := range
myincludes { var incfilename string if slashindex != -1 { incfilename = prefix +
include } else { incfilename = include } if err := ReadConfig(incfilename,
configuration); err != nil { return err } } return nil }

view raw readconfig.go hosted with ❤ by GitHub

What this function does is take the name of a JSON file as well as a struct to
read it into. In the struct, it looks for a string slice called Includes. That
slice contains the names of other JSON files to read in. It does this
recursively. I’ll illustrate with an example.

We have a web server that needs to talk to the user database. Here is the
configuration data needed for the web server.

This file contains bidirectional Unicode text that may be interpreted or
compiled differently than what appears below. To review, open the file in an
editor that reveals hidden Unicode characters. Learn more about bidirectional
Unicode characters
Show hidden characters

package main type Config struct { Includes []string UserDbConfig userdb.Config
Port int }

view raw main.go hosted with ❤ by GitHub

And here is the user database configuration data.

This file contains bidirectional Unicode text that may be interpreted or
compiled differently than what appears below. To review, open the file in an
editor that reveals hidden Unicode characters. Learn more about bidirectional
Unicode characters
Show hidden characters

package userdb type Config struct { URL string MaxIdleConnections int
MaxOpenConnections int ConnMaxLifetime int Replica bool }

view raw userdb.go hosted with ❤ by GitHub

Here is a config file for the user database.

This file contains bidirectional Unicode text that may be interpreted or
compiled differently than what appears below. To review, open the file in an
editor that reveals hidden Unicode characters. Learn more about bidirectional
Unicode characters
Show hidden characters

{ "UserDbConfig": { "URL": "user=groupsio password=dev host=127.0.0.1
dbname=userdb", "MaxIdleConnections": 1, "MaxOpenConnections": 10,
"ConnMaxLifetime": 0, "Replica": false } }

view raw userdb_conf.json hosted with ❤ by GitHub

And here is the config file for the webserver.

This file contains bidirectional Unicode text that may be interpreted or
compiled differently than what appears below. To review, open the file in an
editor that reveals hidden Unicode characters. Learn more about bidirectional
Unicode characters
Show hidden characters

{ "Includes": [ "userdb_conf.json" ], "Port": 3000 }

view raw web_conf.json hosted with ❤ by GitHub

You see that the webserver config file has an Includes slice that references the
userdb config file. Calling ReadConfiguration() will populate the webserver
config struct with the userdb config information.

--------------------------------------------------------------------------------

Groups.io is the best tool to get a bunch of people organized and sharing
knowledge. Start a free trial group today.

--------------------------------------------------------------------------------


UPGRADING POSTGRES USING PGLOGICAL

By Mark Fletcher

December 8, 2022 - 5 minutes read - 903 words
On a ferry to Balestrad, Norway I was born the son of a lawless man Always spoke
my mind with a gun in my hand Lived nine lives Gunned down ten Gonna ride like
the wind Ride Like The Wind, Christopher Cross

Recently, I upgraded the Groups.io database from postgres version 9.6 to 14,
using pglogical to do so. These are my rough notes, mainly for myself to refer
to the next time I need to do an upgrade, but perhaps they will be useful to
others as well.

Postgres major versions are supported for 5 years. We had been on 9.6 since it
was released; support ended a year ago. Upgrading Postgres major versions can be
done in place, with a bit of downtime. But I wanted to be able to test the
upgrade before switching over, and an in place upgrade would not allow that
easily. Enter pglogical, an extension that streams SQL commands between
different Postgres instances. That means that you can sync different major
versions of Postgres.

The plan was to create a new Postgres 14 cluster, and replicate to it from our
main cluster using pglogical. We could then test the new cluster over several
weeks, by routing read-only production traffic to it, and see how it behaved in
production. Once we were convinced that the new cluster was behaving well, we
would have a short downtime and switch production writes over to it, and retire
the old cluster. In the end, this worked out very well for us.


SETTING UP PGLOGICAL

Install the pglogical extension on your current database and make sure it’s
included in the shared_preload_libraries line of postgresql.conf.

Set up a new Postgres 14 instance, also with pglogical installed.

Ensure that the 14 instance is listed in the 9.6 database’s pg_hba.conf file
with replication privileges.

Run the following on the 14 instance for each database, to get the database
schema:

pg_dump -h <ORIGINAL DB IP> -U <user> --schema-only --exclude-schema=pglogical userdb > userdb.schema.sql 


Import that schema into the 14 instance:

psql -d userdb < userdb.schema.sql


On the 9.6 database:

create extension pglogical;
SELECT pglogical.create_node(
               node_name := 'userdb_pg96',
               dsn := 'host=<NEW DB IP> port=5432 dbname=userdb');
SELECT pglogical.replication_set_add_all_tables('default', ARRAY['public']);
SELECT pglogical.replication_set_add_all_sequences('default', ARRAY['public']);


Then on the 14 database:

create extension pglogical;
SELECT pglogical.create_node(
               node_name := 'userdb_pg14',
               dsn := 'host=<ORIGINAL DB IP> port=5432 dbname=userdb'
           );
SELECT pglogical.create_subscription(
               subscription_name := 'userdb_pg96_sub',
               provider_dsn := 'host=<ORIGINAL DB IP> user=replicator password=root port=5432 dbname=userdb'
           );
SELECT pglogical.wait_for_subscription_sync_complete('userdb_pg96_sub');


This creates the sync link between databases and starts the sync process, and
waits until the initial sync is complete. You can monitor the sync process via
the log on the 14 instance.

At this point, I replicated the 14 instance to create the new production
cluster. I then gradually moved read-only production queries over to it and
monitored how it behaved over the course of a couple of weeks.

One thing that surprised me is that autovacuum will not be triggered on the 14
cluster and none of the database stats that you can monitor related to dead
tuples will be updated. This is ok. Once you break the pglogical sync,
autovacuum will proceed as expected.


LOGICAL REPLICATION

We use logical replication to stream database changes to our elasticsearch
cluster. Our setup is detailed here, and it’s worked great over the years.
Unfortunately, the logical slots used for this are not replicated using
pglogical, so you have to recreate them on the 14 instance. This is not
difficult, but there was one thing that tripped me up.

I created a small utility to recreate the logical slots on the new instance and
used it to recreate the slots on the 14 instance. When connecting to a logical
slot, you provide a starting LSN to begin replication. What tripped me up was
that I was initially using the LSN from the 9.6 instance, and was seeing no
replication at all. But LSNs are different on the new instance. So when
initially connecting to the logical slot on the new instance, use 0 for the
starting LSN.

I wrote an upgrade checklist for things to do to move to the new instance. Once
I was confident in the new cluster, I announced a one hour downtime on the site.


FINAL PRODUCTION CUTOVER

Bring up a test web server that talks to the new cluster, and ensure that it
works.

Take the site down.

Turn off pgbouncer to ensure no one was talking with the 9.6 database.

On the 9.6 instance you need to do a final sync of all replicated sequences:

SELECT pglogical.synchronize_sequence( seqoid ) FROM pglogical.sequence_state;


I then ran a script that dumped a bunch of values from the 9.6 and 14 instances
and compared them, as a sanity check.

Turn off pglogical replication, on the 14 instance:

SELECT pglogical.alter_subscription_disable('userdb_pg96_sub');
SELECT pglogical.drop_subscription('userdb_pg96_sub');
SELECT pglogical.drop_node('userdb_pg14');
DROP extension pglogical;


Breaking the sync should cause autovacuum to run immediately; check to make sure
it does.

Log into the test web server and make some changes, ensure that they work.

Turn off the 9.6 cluster.

Update the database config files to point to the new 14 cluster.

Bring up the site.


THE END

The downtime lasted about half an hour (it was announced as an hour downtime). I
took the rest of the day off to decompress (upgrades are nerve wracking!).

--------------------------------------------------------------------------------

Groups.io is the best tool to get a bunch of people organized and sharing
knowledge. Start a free trial group today.

--------------------------------------------------------------------------------


MORE


THE GROUPS.IO ENGINEERING PHILOSOPHY


WHY WE'RE NOT LEAVING THE CLOUD


RE: INTRODUCTION


REACTIVATING THE BLOG

All Posts
© wingedpig 2023