Book Image

Distributed Computing with Go

By : V.N. Nikhil Anurag
Book Image

Distributed Computing with Go

By: V.N. Nikhil Anurag

Overview of this book

Distributed Computing with Go gives developers with a good idea how basic Go development works the tools to fulfill the true potential of Golang development in a world of concurrent web and cloud applications. Nikhil starts out by setting up a professional Go development environment. Then you’ll learn the basic concepts and practices of Golang concurrent and parallel development. You’ll find out in the new few chapters how to balance resources and data with REST and standard web approaches while keeping concurrency in mind. Most Go applications these days will run in a data center or on the cloud, which is a condition upon which the next chapter depends. There, you’ll expand your skills considerably by writing a distributed document indexing system during the next two chapters. This system has to balance a large corpus of documents with considerable analytical demands. Another use case is the way in which a web application written in Go can be consciously redesigned to take distributed features into account. The chapter is rather interesting for Go developers who have to migrate existing Go applications to computationally and memory-intensive environments. The final chapter relates to the rather onerous task of testing parallel and distributed applications, something that is not usually taught in standard computer science curricula.
Table of Contents (11 chapters)

Containers

Throughout the book, we will be writing Go programs that will be compiled to binaries and run directly on our system. However, in the latter chapters we will be using docker-compose to build and run multiple Go applications. These applications can run without any real problem on our local system; however, our ultimate goal is to be able to run these programs on servers and to be able to access them over the internet.

During the 1990s and early 2000s, the standard way to deploy applications to the internet was to get a server instance, copy the code or binary onto the instance, and then start the program. This worked great for a while, but soon complications began to arise. Here are a few of them:

  • Code that worked on the developer's machine might not work on the server.
  • Programs that ran perfectly on a server instance might fail upon applying the latest patch to the server's OS.
  • For every new instance added as part of a service, various installation scripts had to be run so that we can bring the new instance to be on par with all the other instances. This can be a very slow process.
  • Extra care had to be taken to ensure that the new instance and all the software versions installed on it are compatible with the APIs being used by our program.
  • It was also important to ensure that all config files and important environment variables were copied to the new instance; otherwise, the application might fail with little or no clue.
  • Usually the version of the program that ran on local system versus test system versus production system were all configured differently, and this meant that it was possible for our application to fail on one of the three types of systems. If such a situation occurred, we would end up having to spend extra time and effort trying to figure out whether the issue is specific to one particular instance, one particular system, and so on.

It would be great if we could avoid such a situation from arising, in a sensible manner. Containers try to solve this problem using OS-level virtualization. What does this mean?

All programs and applications are run in a section of memory known as user space. This allows the operating system to ensure that a program is not able to cause major hardware or software issues. This allows us to recover from any program crashes that might occur in the user space applications.

The real advantage of containers is that they allow us to run applications in isolated user spaces, and we can even customize the following attributes of user spaces:

  • Connected devices such as network adapters and TTY
  • CPU and RAM resources
  • Files and folders accessible from host OS

However, how does this help us solve the problems we stated earlier? For that, let's take a deeper look at Docker.

Docker

Modern software development makes extensive use of containers for product development and product deployment to server instances. Docker is a container technology promoted by Docker, Inc (https://www.docker.com), and as of this writing, it is the most predominantly used container technology. The other major alternative is rkt developed by CoreOS (https://coreos.com/rkt), though in this book, we will only be looking at Docker.

Docker versus Virtual Machine (VM)

Looking at the description of Docker so far, we might wonder if it is yet another Virtual Machine. However, this is not the case, because a VM requires us to run a complete guest OS on top of our machine, or hypervisor, as well as all the required binaries. In the case of Docker, we use OS level virtualization, which allows us to run our containers in isolated user spaces.

The biggest advantage of a VM is that we can run different types of OSes on a system, for example, Windows, FreeBSD, and Linux. However, in the case of Docker, we can run any flavor of Linux, and the only limitation is that it has to be Linux:

Docker container versus VM

The biggest advantage of Docker containers is that since it runs natively on Linux as a discrete process making it lightweight and unaware of all the capabilities of the host OS.

Understanding Docker

Before we start using Docker, let's take a brief look at how the Docker is meant to be used, how it is structured, and what are the major components of the complete system.

The following list and the accompanying image should help understand the architecture of Docker pipeline:

  • Dockerfile: It consists of instructions on how to build an image that runs our program.
  • Docker client: This is a command-line program used by the user to interact with Docker daemon.
  • Docker daemon: This is the Daemon application that listens for commands to manage building or running containers and pushing containers to Docker registry. It is also responsible for configuring container networks, volumes, and so on.
  • Docker images: Docker images contain all the steps necessary to build a container binary that can be executed on any Linux machine with Docker installed.
  • Docker registry: The Docker registry is responsible for storing and retrieving the Docker images. We can use a public Docker registry or a private one. Docker Hub is used as the default Docker registry.
  • Docker Container: The Docker container is different from the Container we have been discussing so far. A Docker container is a runnable instance of a Docker image. A Docker container can be created, started, stopped, and so on.
  • Docker API: The Docker client we discussed earlier is a command-line interface to interact with Docker API. This means that the Docker daemon need not be running on the same machine as does the Docker client. The default setup that we will be using throughout the book talks to the Docker daemon on the local system using UNIX sockets or a network interface:
Docker architecture

Testing Docker setup

Let's ensure that our Docker setup works perfectly. For our purpose, Docker Community Edition should suffice (https://www.docker.com/community-edition). Once we have it installed, we will check if it works by running a few basic commands.

Let's start by checking what version we have installed:

$ docker --version
Docker version 17.12.0-ce, build c97c6d6

Let's try to dig deeper into details about our Docker installation:

$ docker info
Containers: 38
Running: 0
Paused: 0
Stopped: 38
Images: 24
Server Version: 17.12.0-ce
On Linux, when you try to run docker commands, you might get Permission denied error. In order to interact with Docker, you can either prefix the command with sudo or you can create a "docker" user group and add your user to this group. See link for more details https://docs.docker.com/install/linux/linux-postinstall/.

Let's try to run a Docker image. If you remember the discussion regarding the Docker registry, you know that we do not need to build a Docker image using Dockerfile, to run a Docker container. We can directly pull it from Docker Hub (the default Docker registry) and run the image as a container:

$ docker run docker/whalesay cowsay Welcome to GopherLand!  

Unable to find image 'docker/whalesay:latest' locally Trying to pull repository docker.io/docker/whalesay ... sha256:178598e51a26abbc958b8a2e48825c90bc22e641de3d31e18aaf55f3258ba93b: Pulling from docker.io/docker/whalesay e190868d63f8: Pull complete 909cd34c6fd7: Pull complete 0b9bfabab7c1: Pull complete a3ed95caeb02: Pull complete 00bf65475aba: Pull complete c57b6bcc83e3: Pull complete 8978f6879e2f: Pull complete 8eed3712d2cf: Pull complete Digest: sha256:178598e51a26abbc958b8a2e48825c90bc22e641de3d31e18aaf55f3258ba93b Status: Downloaded newer image for docker.io/docker/whalesay:latest ________________________ < Welcome to GopherLand! > ------------------------ \ \ \ ## . ## ## ## == ## ## ## ## === /""""""""""""""""___/ === ~~~ {~~ ~~~~ ~~~ ~~~~ ~~ ~ / ===- ~~~ \______ o __/ \ __/ \__________/

The preceding command could also have been executed, as shown here though, merely using docker run ..., which is more convenient:

$ docker pull  docker/whalesay & docker run docker/whalesay cowsay Welcome to GopherLand!

Once we have a long set of built images, we can list them all and similarly for Docker containers:

$ docker images
REPOSITORY                         TAG            IMAGE ID            CREATED             SIZE
docker.io/docker/whalesay   latest         6b362a9f73eb    2 years ago         247 MB
$ docker container ls --all 
CONTAINER ID        IMAGE                COMMAND                  CREATED             STATUS                     PORTS               NAMES                                   
a1b1efb42130        docker/whalesay      "cowsay Welcome to..."   5 minutes ago       Exited (0) 5 minutes ago                       frosty_varahamihira 
  

Finally, it is important to note that as we keep using docker to build and run images and containers, we will start creating a backlog of "dangling" images, which we might not really use again. However, they will end up eating storage space. In order to get rid of such "dangling" images, we can use the following command:

$ docker rmi --force 'docker images -q -f dangling=true'
# list of hashes for all deleted images.

Dockerfile

Now that we have the basics of Docker under our belt, let's look at the Dockerfile file we will be using as a template in this book.

Next, let's look at an example:

FROM golang:1.10
# The base image we want to use to build our docker image from.
# Since this image is specialized for golang it will have GOPATH = /go

ADD . /go/src/hello
# We copy files & folders from our system onto the docker image

RUN go install hello
# Next we can create an executable binary for our project with the command,
'go install' ENV NAME Bob
# Environment variable NAME will be picked up by the program 'hello'
and printed to console.ENTRYPOINT /go/bin/hello
# Command to execute when we start the container # EXPOSE 9000 # Generally used for network applications. Allows us to connect to the
application running inside the container from host system's localhost.

main.go

Let's create a bare minimum Go program so that we can use it in the Docker image. It will take the NAME environmental variable and print <NAME> is your uncle. and then quit:

package main 
 
import ( 
    "fmt" 
    "os" 
) 
 
func main() { 
    fmt.Println(os.Getenv("NAME") + " is your uncle.") 
} 

Now that we have all the code in place, let's build the Docker image using the Dockerfile file:

$ cd docker
$ tree
.
├── Dockerfile
└── main.go"
0 directories, 2 files $ # -t tag lets us name our docker images so that we can easily refer to them $ docker build . -t hello-uncle Sending build context to Docker daemon 3.072 kB Step 1/5 : FROM golang:1.9.1 ---> 99e596fc807e Step 2/5 : ADD . /go/src/hello ---> Using cache ---> 64d080d7eb39 Step 3/5 : RUN go install hello ---> Using cache ---> 13bd4a1f2a60 Step 4/5 : ENV NAME Bob ---> Using cache ---> cc432fe8ffb4 Step 5/5 : ENTRYPOINT /go/bin/hello ---> Using cache ---> e0bbfb1fe52b Successfully built e0bbfb1fe52b $ # Let's now try to run the docker image. $ docker run hello-uncle Bob is your uncle. $ # We can also change the environment variables on the fly. $ docker run -e NAME=Sam hello-uncle Sam is your uncle.