TL;DR: Given massive scale and many producers, we saw the need to develop a buffer between our application and Kafka — until we met bruce
Apache Kafka, the open source messages broker originally developed by LinkedIn, has gained in popularity and became a major player in big data pipeline infrastructures.
It's easy to see why: Kafka simplifies messages transportation between applications, and brings many advantages to the table when compared to its competitors. It has high throughput of up to 170, 000 messages per second per thread, simple messages partitioning, disk persistence, and easy data replays.
Kafka is a good choice to use when:
Before diving in, it's important to understand four key terms:
At HUMAN, we use Kafka for multiple purposes, delivering messages on different topics with producer clients written in Python, Node.js, Scala, and Go, transporting tens of thousands messages every second.
When our Kafka cluster got bigger, and with the growing amount of different producers, we wanted to ensure that our data pipeline is fault tolerant. One of the data junctions we wanted to improve is the pipe from the producers to Kafka.
Kafka has many producers, publishing messages on multiple topics. These producers are usually server-side services or apps that receive data from a different source, process it, and push it to Kafka.
The dependency between your application and Kafka is very strong. Kafka handles the backpressure of the application, enabling it to handle real-time requests and queuing backend processing. If Kafka is unreachable for some reason, such as due to a failure or network issues, the messages your application was supposed to deliver will be lost.
To decouple this dependency, we started thinking about creating a buffer layer between our application and Kafka. Here's what we did:
Finally, any I/O managed by your application run-time has its own set of problems. In a large scale app, you need to worry about your database, cache server, configurations load, file system ops, and other factors. Having fewer I/O pipes reduces application complexity.
Then we ran into Bruce.
Bruce is a light, open-source Unix service that runs on the same machine as your application, and decouples messages producing responsibilities from the client. Bruce takes care of all the things you don’t want to handle in your applications.
Key features include:
Sound good? That doesn’t mean it was easy. We encountered these issues:
The changes we need to make in the setup in order for Bruce to work included:
Below we demonstrate a basic usage, in a syntax based on Node.js, of how we communicate with Bruce.
$ cd /path/to/Dockerfile
$ docker build -t Bruce .
$ docker run -d --name Bruce -v /path/to/bruce_conf.xml:/etc/bruce/bruce_conf.xml:ro -v /path/to/bruce.socket:/root Bruce
const bruceClient = require("./bruce_client").Bruce;
const unix = require("unix-dgram");
const BRUCE_SOCKET = "/root/bruce.socket";
const client = unix.createSocket("unix_dgram");
client.on("connect", () => {
const msg = "hello from Bruce!";
const topic = "bruce_topic";
const bruceMsg = bruceClient.createAnyPartitionMsg(topic, Date.now(), topic, msg);
client.send(msg);
});
client.connect(BRUCE_SOCKET);
Source files for the Dockerfile, Node.js client and Bruce client are located in our Github Repo.
With the increase in Kafka usage among modern backend services, server applications must deal with messages transportation (redundancy, retries, backups and protocol implementation). Using Bruce, it is possible to separate the transportation responsibility from the server application and encapsulate it in a dedicated service. That adds up to more reliable messaging.