Implementing streaming mode for kafka plugin
- Dominant language
- Go
- Stars
- 89
- Forks
- 74
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 8
Description
Recently, we have been facing issues with rate-limits for Kafka Plugin which hits the Kafka REST v3 endpoint to send alerts.
We have a druid datasource, and some we have some rules defined. Now periodically, grafana carries out evaluations on data fetched from druid and sends an event to one of our kafka topics with all the necessary metadata, which is processed further to send an alert.
For every (cluster, rule) we have, grafana sends a unique event to kafka. This can lead to the influx of large volume of events to our kafka topic. Hence we are hitting rate limit errors on our kafka topic.
I took a look at the grafana [codebase](https://github.com/grafana/alerting/blob/c17ec6241a66c0e96196777ad9e4058525f29eff/receivers/kafka/kafka.go#L115) and here are my observations:
Grafana sends the event to kafka topic using the Kafka REST V3/V2 API as defined here (notifyWithAPIV3). We use V3 for our case.
This function call at the end reaches [this](https://github.com/grafana/grafana/blob/b4d7c484c71d2e0894021398827bfdb43a564a3a/pkg/services/notifications/webhook.go#L50) file finally calling the sendWebRequestSync , essentially making an HTTP POST request.
The following client is used, along with the defined transport:
```
var netTransport = &http.Transport{
TLSClientConfig: &tls.Config{
Renegotiation: tls.RenegotiateFreelyAsClient,
},
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 5 * time.Second,
}
var netClient WebhookClient = &http.Client{
Timeout: time.Second * 30,
Transport: netTransport,
}
```
The `http.Client` internally maintains a pool of persistent TCP connections per host to improve efficiency of requests, which can be controlled using some transport parameters.
In grafana’s case, The transport does not define these two parameters: `MaxIdleConnsPerHost` , `MaxConnsPerHost`.
Hence the default values are used: `MaxIdleConnsPerHost = 2` , `MaxConnsPerHost = 0`
From the [documentation](https://pkg.go.dev/net/http?utm_source=godoc#Transport):
```
// MaxIdleConns controls the maximum number of idle (keep-alive)
// connections across all hosts. Zero means no limit.
MaxIdleConns int
// MaxIdleConnsPerHost, if non-zero, controls the maximum idle
// (keep-alive) connections to keep per-host. If zero,
// DefaultMaxIdleConnsPerHost is used.
MaxIdleConnsPerHost int
```
When grafana receives a large number of events that it needs to send to our kafka topic, it can create as many connections to the kafka REST host in order to fulfil the requests.
And since it can only maintain maximum of two connections in its idle pool, all the other connections get created and are not reused.
For example, if we receive 15 concurrent requests, the client will create 15 connections to the host, out of which the 13 will be closed soon after since the max open connections we can have is set to 2.
Our kafka has a [rate limit](https://docs.confluent.io/cloud/current/clusters/cluster-types.html#cluster-limit-comparison) of 25 connections per second. Hence the limit gets breached in case of huge volume of events. It is also recommended to [close](https://docs.confluent.io/cloud/current/kafka-rest/kafka-rest-cc.html#connection-bias-and-request-limits) connection after every request but this may not be feasible.
Tentative solutions:
*Using the Kafka V3 streaming mode*: The V3 API supports [streaming](https://docs.confluent.io/cloud/current/kafka-rest/kafka-rest-cc.html#streaming-mode-recommended) using which we can send 1000 requests per second. So we can modify the code to send multiple request bodies over a single HTTP connection at the application level. I have [implemented](https://goplay.tools/snippet/cLf6nsXnLNA) a basic way for streaming just to do a POC, but even this is not entirely correct. I am able to send responses over a single connection (streaming), however, I am not able to read responses in streaming mode. The entire response is read all at once. So if I send 200 requests over the connection, the response always waits till all 200 are sent and only then I am able to read the responses, all 200 together.
Looking at the grafana [code](https://github.com/grafana/alerting/blob/main/receivers/kafka/kafka.go), integrating it with the current implementation would need a big overhaul. As of now, each thread receives a single alert request, and we send the post request.
2. We also would need to specify the parameters MaxIdleConnsPerHost , MaxConnsPerHost to set an appropriate limit per host to take into consideration the rate limits.
Looking for inputs on how to go about this. Thank you for your valuable time!
Contributor guide
Research direction
Start by reading receivers/kafka/kafka.go, especially notifyWithAPIV3, and the referenced webhook.go sendWebRequestSync path. Review the linked streaming proof of concept to understand the request and response behavior. Done means integrating Kafka V3 streaming into the current alert flow while correctly reading individual streaming responses and considering connection limits.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, kafka
- Domain
- backend, distributed-systems
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100