days
-4
-8
hours
0
-9
minutes
-5
-8
seconds
-4
-1
search
Decisions, decisions

From Python to Go: Why Stream switched to Go

Thierry Schellenbach
Go

© Shutterstock / Jackson

Stream recently changed the back end of their core service from Python to Go. Although they are still using Python where it makes sense, the company has decided to write all performance-intensive code in Go from now on. In this article, Thierry Schellenbach, Stream’s CEO and founder explains why the company decided to switch to Go.

Choosing a language for a project or product is driven by many factors. As with any technology decision, there are tradeoffs and, of course, no perfect answer. We recently changed the back end of our core service from Python to Go, for a handful of reasons, which has already had many benefits.

To understand the importance of this recent change, understanding our product is crucial. Stream is an API for building, scaling, and personalizing news feeds and activity streams. We serve about one billion API requests per month for over 300 million end users. We care very deeply about performance and reliability, which drives every technical decision we make.

Performance

Perhaps the easiest sell for Go was its performance, both at runtime and compile time. It is comparable to Java or C++ in most computing benchmarks. In our real-world usage, we’ve found it to be approximately 30 times faster than Python.

Choosing fast tools is very important (we have optimized Cassandra, PostgreSQL, Redis and many other technologies). However, at times we found that the bottleneck in our system was indeed Python, our programming language of choice. Computationally heavy tasks like serialization, ranking and aggregation were sometimes taking much longer than retrieving the data from a data store over the network. We knew we could do better. Switching from Python to Go brought that time back down so that the application code was once again more like the glue between services and not a major bottleneck in optimization.

The Go compiler (which itself is written in Go!) is also incredibly fast. Stream’s most complex microservice written in Go still takes just six seconds to compile, which is a major win compared to toolchains like Java and C++ which are infamous for sluggish compile times of minutes or even hours for more complex full builds.

SEE ALSO: The meteoric rise of Go and why HashiCorp is betting on it

Does what it says on the tin

Go, to put it bluntly, is somewhat boring.

This is absolutely a good trait, though! Reading through Go code tends to be very straightforward and, dare I say obvious. We have migrated from a Python codebase with many different authors and lots of opinions when it comes to code style and frameworks, each of which adds a small amount of magic. Go, on the other hand, forces a clean style and nudges authors away from being clever. This sometimes comes at the cost of more verbosity, but the benefit of being much easier to read and reason about outweighs that cost. Getting up to speed on reading each other’s code (or one’s own code from some time ago…) is much faster and picking up the context is far easier.

Native concurrency

Concurrency is baked right into the language with goroutines and channels. The concepts are heavily influenced by the ideas in Tony Hoare’s Communicating Sequential Processes and make concurrency patterns trivial to implement as a programmer.

Goroutines are conceptually similar to an operating system thread but are incredibly cheap to spin up – costing just a few KB of stack space each. The Go runtime can handle many, and intelligently multiplex goroutines over a number of underlying OS threads. This all happens under the hood and is transparent to the programmer. It’s not uncommon for a single program to have many thousands of goroutines. The server in the net/http package, for instance, spins up a goroutine per incoming HTTP request.

And in true Go fashion, goroutines are incredibly simple: just prepend a function call with the `go` keyword to have it run in its very own goroutine.

The conventional wisdom in the Go world is “do not communicate by sharing memory; instead, share memory by communicating.” The primitive for communicating between goroutines are channels, and they are just as easy to use as goroutines. Channels have a type and can pass data effortlessly between goroutines with an intuitive arrow syntax. Though simple, channels are incredibly powerful. With some forethought, making massively concurrent systems is a breeze compared with more traditional systems.

With the simple concurrency tooling, one can tackle those complex problems which often lead to complex bugs. Go ships with a built-in race detector that makes it easier to detect race conditions in asynchronous code.

The ecosystem

Go is still a relative newcomer to the compiled language scene, trailing traditional languages like C++ and Java in popularity. Though only ~5% of programmers know Go, it’s a growing number and that growth is partly spurred by how easy it is to pick up the language. Though fast and powerful, the language only has 25 reserved words (compared to C++’s 92 or Java’s 53) and, for most developers, will only introduce a handful of new concepts.

Building a team of Go developers is easier than most other languages because of how easy it is to pick up. Getting up to speed with the language won’t take long even for someone who hasn’t ever touched the language. This makes it easier to hire developers that may have other backgrounds.

The built-in libraries that ship with Go are well thought and powerful right out of the box. Making an HTTP service using the `net/http` package takes only a few lines of code and has support for things like http/2, TLS and websockets natively. The ecosystem of community packages is stellar as well, with libraries shipped and stable for Redis, RabbitMQ, PostgreSQL, templating, and RocksDB.

SEE ALSO: Go has earned companies’ trust: More developers use it at work now, survey shows

Other benefits

I mentioned how Go discourages programmers from being clever by not shipping with features that (at first) might appear to be time savers. Lookin’ at you, nestable ternary operators.

Another way Go saves time is over the holy war of tabs vs. spaces in favor of the One True Way of formatting code – Gofmt. It is a command line tool that integrates with most editors and automatically formats code to its own de facto standard. Code still compiles if improperly formatted, but pull requests won’t even be looked at unless the code has been run through gofmt to keep the entire codebase consistently formatted. This allows code reviewers to focus solely on the code rather than spending time on nit-picky formatting. If the Go core team could now just explain that vim is the One True Editor…

Developing in Go lends itself to working on a microservice architecture. We’ve found that gRPC and Google’s protocol buffers are a great way to manage communication between microservices, and Go has first-class support. As developers, we define a service in a manifest file and the tooling generates code for both client and server, which is performant and has very little network overhead. The manifest can be used by other languages to generate their own client and server code. So, if we do decide to replace pieces of the architecture with other technologies, it’s a much easier task. The best part of gRPC and ProtoBuf is it obviates the need for ambiguous RESTful services for internal traffic, meaning no more POSTing to /delete/ endpoints and getting a 200 back.

Python vs. Go

One powerful feature in the Stream service is that of ranked feeds. Ranked feeds allow our users to specify a scoring function to a feed in order to control how it’s sorted when fetched. The scoring algorithm can be fed many variables to determine ranking, but a good example based on popularity might look like this:

{
  "functions":{
    "simple_gauss":{
      "base":"decay_gauss",
      "scale":"5d",
      "offset":"1d",
      "decay":"0.3"
    },
    "popularity_gauss":{
      "base":"decay_gauss",
      "scale":"100",
      "offset":"5",
      "decay":"0.5"
    }
  },
  "defaults": {
      "popularity": 1
  },
  "score":"simple_gauss(time)*popularity"
}
  1. To support this ranking method, both the Python and Go code need to:
    Parse the expression for the score. In this case, we want to turn the string simple_gauss(time)*popularity” into a function that takes an activity as input and returns a score as output.
  2. Create partial functions based on the JSON config. For example, we want “simple_gauss” to call “decay_gauss” with a scale of five days, offset of one day, and a decay factor of 0.3.
  3. Parse the “defaults” configuration so you have a fallback if a certain field is not defined on an activity.
  4. Use the function from Step 1 to score all activities in the feed.

Developing the Python version of the ranking code took roughly three days of writing code, unit tests and documentation. Next, the team spent approximately two weeks optimizing the code. One of the optimizations made was translating the score expression (simple_gauss(time)*popularity) into an abstract syntax tree. The team also implemented caching logic, which pre-computed the score for certain times in the future.

In contrast, developing the Go version of this code took roughly four days, and the performance didn’t require any further optimization. While the initial bit of development was actually faster in Python, the Go version ultimately required substantially less work.

This huge difference in time savings when optimizing the codebase is due to Go’s language performance. With Python, we had to parse the expression into abstract syntax trees and optimize/profile every function that we exposed via ranking. Since Go is so much faster than Python we didn’t need to invest in more optimizations. The end result was that the vanilla Go code performed roughly 40 times faster than the well-optimized Python code.

Some other components of Stream’s system took substantially more time to build in Go than in Python. As a general trend, developing Go code takes slightly more effort, but teams will spend much less time optimizing the code for performance.

Conclusion

Go is a great language for writing microservices. It is very fast, has native concurrency primitives, excellent support for existing tools and is downright fun to develop in. Writing Go might take longer to write up front compared to scripting languages like Ruby or Python, but the maintenance costs are far lower and you will save lots of time which would otherwise be spent on optimizing code.

It’s important to note that Stream still uses Python where it makes sense. For instance, our dashboard, web site and machine learning for personalized feeds use Python since the tooling is much better. We won’t be saying goodbye to Python anytime soon, but going forward we’ll be writing all performance-intensive code in Go.

Author

Thierry Schellenbach

Thierry graduated cum laude with honours from Erasmus university with a degree in business. At the age of 14, he started his first profitable site and discovered a passion for technology. Before founding Stream with Tommaso, he co-founded Fashiolista, which grew to be one of the world’s largest social networks for fashion. He’s the author of several widely used Python packages including Stream-Framework and Django-facebook and has more than 5 years of experience managing technology companies.


Leave a Reply

Be the First to Comment!

avatar
400
  Subscribe  
Notify of