Dynamically extend and patch Tyk using native Go plugins
Here at Tyk, we care a lot about the extensibility of our products. We already have a built-in JSVM engine, embedded Python interpreter, and even gRPC plugins, which can be written almost in any language. So why we should we care about native Go plugins? Let’s try to find out.
Why use plugins?
The common reasons for using any plugins are either fixing existing behavior, adding some new functionality, or integrating with third-party systems. When we think about designing the Tyk plugin ecosystem, we always try to offer a balance between ease of use, performance, reliability, and capability.
You can get started quickly with a built-in JSVM plugin, just by using plain files. Need an advanced XML parser? Write a Python script using a built-in interpreter, and sacrifice with a bit more complicated installation. Need to use your home-grown Java library? Write a plugin in the language of your choice, and maintain your gRPC process, which communicates with Tyk instance.
But why use Go plugins?
The main idea behind the native plugin is that it is a compiled dynamic library, which can be loaded in runtime to the existing process and can alter or extend its behavior.
From a performance point of view, the code inside plugin runs at the same speed as its parent binary. It happens because Go plugins communicate with main binary almost without any overhead, like if plugin code was built-in. Additionally, plugins share exactly same data types, so you do not have to build a “bridge” like if you used gRPC or passed objects between scripting engine and Go runtime.
Native Go plugins fit really well to our vision when you need to go beyond standard plugin capabilities. There are features and pitfalls of any plugin approach – here are those we’ve seen for Native Go plugins:
- Extreme performance: recently we had a PoC requirement to do custom request validation, which ends up to be extremely inefficient from the performance point of view. Moreover, on top of that requirements was the ability to handle 60k requests per second, with max few ms of added latency. JSVM built-in library capabilities were not enough. Python version was too slow, and added latency was too big since Python is single threaded by design. gPRC plugin written in Go was close to the goal, but its communication channel overhead was too big as well. In the end, we just provided a custom build of Tyk with this functionality built-in and added this algorithm as a feature for next release. With native Go plugin, it would look like just loading plugin file to existing production binary of Tyk.
- Extreme flexibility: our tracker is full of requests asking whether Tyk plugins support X or Y. Can it access to request context, environment variables, logger, etc.? No matter how actively we’ll be improving our various plugins, they’ll obviously not be able to access the same functionality as our internal Go code has. With native Go plugins, you can access exactly the same data structures, and use the same libraries, as our code does. And you can do it right away, without waiting for when it will be available in JSVM, Python or gRPC plugins.
- Complexity of maintaining: Compiling a plugin which is compatible with existing binary is a minefield. If the plugin is compiled with a different Go version, different dependency versions, or even different directory structure on your machine, it will fail to load. This also means that if your plugin was built for Tyk 2.9 and you upgrade to Tyk 2.10, your plugin will stop working, and you have to recompile it for the new Tyk version. However not all is bad, and there are approaches to help simplify this process. For example, it’s possible to share the same build environment, as used for building our Tyk binaries, via Docker or some HTTP service, which always build a compatible plugin. That way, an example workflow can be as simple as: `tyk-build plugin ./path-to-plugin –version 2.9`
Patching Tyk in runtime
We have not covered patching functionality yet, and if you thought that native plugins are cool, let me show you our plans on patching Tyk in runtime.
As a software vendor, our release cycles vary from 3-4 months for a major release and a few weeks for a patch releases. Which means that even in good case scenario, if some bug is found, and you need a fix, optimistic estimation is at least a week. If you need some major feature, your best estimation time is a few months. It happens because a release is usually a culmination of multiple tasks, all released altogether. Each release should come through full the QA cycle, ensuring that each of the changes has not introduced side effects.
If someone has an emergency issue or feature request, waiting for a next release can be very nervous and costly, and at the same time, same applies to doing an emergency release containing the single fix. Native Go plugins are here to help. We plan to not only “extend” Tyk with Pre/Post plugins but actually to allow override behavior of built-in components. Found a bug in built-in middleware? You can get a binary patch right after it is fixed, and attach it to the currently Tyk binary, just by copying it to the `patches` folder and Tyk will pick it up. How cool is that?!
Our current support for native Go plugins is in active progress, and our goal is to make them available with our upcoming 2.9 release. However, we would like to share our vision and experience on how to build reliable application and infrastructure architecture which allows you dynamically extend and dynamically patch your binary distributions. Our technically-savvy readers can check our guide on writing native Go plugins for more information.