Web services in go
Published on January 31, 2015
Table of contents
I’ve been writing quite a few different web services in my current job. Initially, they were all written in rails. As we shift towards a microservices / services oriented / whatchamacallit architecture, I’ve started experimenting with other languages.
Right now we have about 12 services, half of which are ruby (rails or sinatra), with the other half being written in go. I’m going to talk a bit about my experience writing go web services.
Configuration
In our rails applications, we have our configurations in yaml files, in the
config
directory. Database credentials live in config/database.yml
, mailer
options in the config/mailer.yml
, and so forth. These do not live in the code
repository, but instead are created the first time each service is deployed to
a server.
To maintain some consistency between services, I would like to have something similar across web services. However, the previous approach posed some problems.
First, go does not have a yaml parser implementation right of the bat, as will probably be the case for most languages. There are some packages for it (go-yaml, go-gypsy), but looking for the right implementation every time we try a new language seems more trouble than it is worth. JSON, on the other hand, is widely supported. Rust, nodejs, go, and PHP all have native json capabilities, and none of them has yaml support.
Second, having everything in its own file makes it a bit harder to deploy.
Ideally, I’d like to have some sort of central configuration system, like
etcd, from which each service fetches its
configuration periodically, or with some kind of notification system. In this
kind of setup, it makes little sense having configuration spread over multiple
files. Instead, I opted for a single file, config/settings.json
.
Eventually, I’d like to replace rails configuration yaml files with a single json file as well.
Deployment
The first question here was deciding wether we should compile the binaries on the server or on the development machine. There are a couple of drawbacks to each approach.
Compiling on the server requires a go compiler on every server, with the code being compiled once per server.
Compiling on the development machine would require a cross compilation toolchain, since our development machines are all macs and the servers are all running linux.
I opted for the first option. It seemed less troublesome at the time, and it kind of matches our ruby deployments, as in ruby the full source code gets deployed to the server.
We try to make deployment as homogeneous as possible, so we turned to capistrano as our deployment tool. It’s our tool of choice when deploying ruby services, so we saw no reason to use anything else.
Here’s a sample deploy.rb file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
set :application, 'binfo'
set :repo_url, 'company.com:web/binfo.git'
set :deploy_to, '/path/to/app'
set :scm, :git
set :ssh_options, { forward_agent: true }
set :default_env, { path: "/usr/local/go/bin:$PATH" }
set :linked_files, %w{ config/settings.json }
set :service, 'service nlife-binfo'
namespace :go do
desc 'Build go application'
task :build do
on roles(:app), in: :sequence, wait: 5 do
within release_path do
with gopath: release_path do
execute :sh, 'build.sh'
end
end
end
end
end
namespace :deploy do
desc 'Restart application'
task :restart do
on roles(:app), in: :sequence, wait: 5 do
sudo fetch(:service), :restart
end
end
after :publishing, :restart
after :finishing, 'deploy:cleanup'
after :updated, 'go:build'
end
You’ll notice that I’m using service nlife-binfo restart
to restart the
webservice. Usually we deploy services to ubuntu, so we use upstart scripts to
manage the service lifetime.
Upstart scripts are relatively simple (although this will probably have to be migrated to systemd once it replaces upstart, but that should take a while). Here’s a sample script:
1
2
3
4
5
6
7
8
9
10
11
description "nlife binfo service"
start on (local-filesystems and runlevel [2345])
stop on runlevel [06]
setuid binfo-runner
setgid binfo-runner
chdir /path/to/app/current
exec bin/binfo
In the future, I’d like to give goagain a try. Its goal is to avoid downtime while the service is reloading. It requires rewriting most of the http server code, or combining it with manners. This work has been done by other developers, in mannersagain.
With this in play, the capistrano recipe would need to be changed to reload the service instead of restarting it, and no requests would be dropped.