Fabio source code study part 1
Background
In this two-part blog series, I want to share the lessons learned from reading the soruce code of the project Fabio. In my previous blog, I shared with you how to use Fabio as a load balancing in the micro services applicatoins, in detail you can refer to this article.
Since Fabio is not a tiny project, it’s hard to cover everything inside this project. I will mainly focus on two aspects: firstly in the architecture design level, I will study how it can work as a load balancer without any configuration file (Part one), and secondly in the language level, I want to summarize the best practice of writing Golang programs by investigating which features of Golang it uses and how it uses (Part two).
Fabio architecture design
Let’s start by introducing some background about Fabio. Following paragraph is from its official document:
Fabio is an HTTP and TCP reverse proxy that configures itself with data from Consul. Traditional load balancers and reverse proxies need to be configured with a config file.
If you’re familiar with other load balancer service such as Nginx, it will be easy for you to understand how Fabio is different and why it seems interestring.
For example, if you’re using Nginx as your load balancer, you need to maintain a config file where the routing rules need to be defined as below
1 | server { |
But Fabio is a zero-conf load balancer. Cool, right? Let’s review the design and code to uncover the secrets under the hood.

Simply speaking, Fabio’s design can be divided into two parts: Consul monitor and proxy. Consul monitor forms and updates a route table by watching the data stored in Consul, and proxy distributes the request to target service instance based on the route table.
main function
1 | func main() { |
The main function defines Fabio’s workflow. To understand how Fabio works, we only need to focus on three points:
- initBackend() and watchBackend(): these two functions contain Consul monitoring logic.
- startServers(): this function is responsible to create the network proxy.
Consul monitoring
First, let’s review the initBackend function:
1 | func initBackend(cfg *config.Config) { |
This function is not hard to understand. Fabio supports various modes: file, static, consul and custom, and will select one mode to use based on the detailed condition inside the cfg parameter. In our case, we only need to focus on the consul mode.
Next let’s review watchBackend() function to check how it keeps watching consul’s data.
1 | func watchBackend(cfg *config.Config, first chan bool) { |
Firstly in line 24, we need to understand registry.Default.WatchServices(). Since initBackend function already decided we’re using Consul mode, so we need to check the WatchServices() function inside the Consul package as following:
1 | package consul |
The return value is svc which is just a string typed channel. And svc channel is passed into goroutine go m.watch() as an argument. This is a very typical usage in Golang programming where two goroutines need to communicate with each other via the channel. Let’s go on and check the Watch function:
1 | func (w *ServiceMonitor) Watch(updates chan string) { |
You can see updates <- w.makeConfig(passing) in Line 21, it just sends a message into the channel.
Another interestring point is w.client.Health().State("any", q) in line 11. This is one API provided in the consul/api package. If you check the implementation of it, you’ll find out in fact it just sends a HTTP get request to this endpoint /v1/health/state/ of Consul service which will return all the health check status for the services registered in Consul.
And the above logic runs inside a for loop, in this way Fabio keeps sending request to query the latest status from Consul. If new services are discovered, then the status will be updated dynamically as well, no need to restart Fabio.
So far you should understand how Fabio can work as a load balancer with any hardcoded routing config.
Let’s go back to the watchBackend function to continue the analysis.
After debugging, I find the message passed via the svc channel follows the following format:
1 | [route add demo-service /helloworld http://127.0.0.1:5000/ route add demo-service /foo http://127.0.0.1:5000/] |
Next step is converting this string message into the route table.
In line 46 and 51 of watchBackend function, you can find these two lines of code:
1 | t, err := route.NewTable(tableBuffer) // line 46 |
Everything will be clear after you check the implementation of the route package.
route.NewTable() function returns a Table type value which is map in fact. And the Table type declaration goes as following:
1 | // Table contains a set of routes grouped by host. |
That’s all for the consul monitor part. Simply speaking, Fabio keeps looping the latest service status from Consul and process the status information into a routing table.
Proxy
The second part is about network proxy, which is easier to understand than the first part.
Fabio supports various network protocols, but in this post let’s focus on HTTP/HTTPS case. In side the main.go file, you can find the following function:
1 | func newHTTPProxy(cfg *config.Config) http.Handler { |
The return value’s type is http.Handler, which is an interface defined inside Go standard library as following:
1 | type Handler interface { |
And the actual return value’s type is proxy.HTTPProxy which is a struct implementing the ServeHTTP method. You can find the code inside the proxy package in Fabio repo.
1 | type HTTPProxy struct { |
Another point needs to be mentioned is Lookup field of HTTPProxy struct:
1 | Lookup: func(r *http.Request) *route.Target { |
You don’t need to understand the details, just pay attention to route.GetTable() which is the routing table mentioned above. Consul monitor maintains the table and proxy consumes the table. That’s it.
In this article which is part one of this blog series , you learned how Fabio can serve as a load balancer without any config files by reviewing the design and reading the source code.
In part two, let’s review how Golang was used and try to summarize the best practise of wrting Golang programs.




