Skip to content

feat: decoupled prometheus exporter's calculation and output#12383

Merged
SkyeYoung merged 57 commits into
apache:masterfrom
SkyeYoung:young/perf/prometheus-exporter-concurrency
Jul 25, 2025
Merged

feat: decoupled prometheus exporter's calculation and output#12383
SkyeYoung merged 57 commits into
apache:masterfrom
SkyeYoung:young/perf/prometheus-exporter-concurrency

Conversation

@SkyeYoung

@SkyeYoung SkyeYoung commented Jun 26, 2025

Copy link
Copy Markdown
Member

Description

This PR decouples the calculation and output processes of the Prometheus exporter. The "calculation" is performed in the privileged agent process at intervals defined by the refresh_interval(default is 15s) and written to a shared dict, while the "output" (i.e., the /apisix/prometheus/metrics API) is moved to the worker process, which only reads and returns the cached data in the shared dict.

The above are just the core changes. In fact, I encountered many other problems, which have been commented or annotated in the corresponding positions, and will not be repeated here.

For the testing part, since the Prometheus exporter currently refreshes data every 15 seconds, I used a smaller interval in the relevant tests to pass the original tests.

Which issue(s) this PR fixes:

Fixes #

Stress Testing

How to

  1. install wrk2, git clone apisix
  2. deploy etcd(./test.sh init-etcd), nginx(as upstream, ./test.sh start-nginx)
  3. make run
  4. create 10k routes(./test.sh create)
  5. enable promethues(./test.sh enable-prometheus)
  6. run benchmark(./test.sh benchmark)

Results

Key Performance Indicators

Scenario CPU Usage Memory Usage P50 Latency P90 Latency P99 Latency
Prometheus Disabled 33.0% 144.3MB 3.58ms 5.99ms 8.99ms
Prometheus Enabled 38.3% (+5.2%) 100.2MB (-44.1MB) 4.22ms (+0.64ms) 7.07ms (+1.08ms) 10.66ms (+1.67ms)
1 Metrics Reqs 38.6% (+5.6%) 101.3MB (-42.9MB) 4.25ms (+0.68ms) 7.87ms (+1.87ms) 12.17ms (+3.18ms)
3 Metrics Reqs 39.1% (+6.1%) 100.1MB (-44.2MB) 3.88ms (+0.30ms) 7.17ms (+1.18ms) 11.92ms (+2.93ms)

Performance Impact Summary

Scenario CPU Impact Memory Impact P50 Latency Impact P90 Latency Impact P99 Latency Impact
Prometheus Disabled (Baseline) 0.0% 0.0MB 0.00ms 0.00ms 0.00ms
Prometheus Enabled +5.2% -44.1MB +0.64ms +1.08ms +1.67ms
1 Metrics Reqs +5.6% -42.9MB +0.68ms +1.87ms +3.18ms
3 Metrics Reqs +6.1% -44.2MB +0.30ms +1.18ms +2.93ms
worker_performance_individual latency_metrics_individual

Checklist

  • I have explained the need for this PR and the problem it solves
  • I have explained the changes or the new features added to this PR
  • I have added tests corresponding to this change
  • I have updated the documentation to reflect this change
  • I have verified that this change is backward compatible (If not, please discuss on the APISIX mailing list first)

Comment thread apisix/plugins/prometheus/exporter.lua
-- config server status
local vars = ngx.var or {}
local hostname = vars.hostname or ""
local hostname = core.utils.gethostname() or ""

@SkyeYoung SkyeYoung Jun 27, 2025

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because API disabled in the context of ngx.timer, context: ngx.timer.

Comment thread apisix/plugins/prometheus/exporter.lua
Comment thread apisix/plugins/prometheus/exporter.lua
Comment thread apisix/plugins/prometheus/exporter.lua
Comment thread apisix/plugins/prometheus/exporter.lua
Comment thread apisix/plugins/prometheus/exporter.lua Outdated
function _M.export_metrics()
if not prometheus then
core.response.exit(200, "{}")
return core.response.exit(200, "{}")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the current way is good to me

Comment thread conf/config.yaml.example Outdated
meta:
lua_shared_dict: # Nginx Lua shared memory zone. Size units are m or k.
prometheus-metrics: 15m
prometheus-cache: 10m

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pls add some comments, tell users when they need to modify it

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

Comment thread apisix/plugins/prometheus/exporter.lua Outdated
core.log.error("Failed to collect metrics: ", res)
return
end
shdict_prometheus_cache:set(CACHED_METRICS_KEY, res)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to capture the return value, it maybe fail

if err, tell user what is the reason, and tell the user to change the default size if the shdict is small

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@membphis
membphis self-requested a review July 23, 2025 01:34
@SkyeYoung
SkyeYoung requested a review from bzp2010 July 23, 2025 01:44
Comment thread apisix/plugins/prometheus/exporter.lua Outdated
local _, err, forcible = shdict_prometheus_cache:set(CACHED_METRICS_KEY, res)

if err then
core.log.error("Failed to save metrics to shdict: ", err)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need more information, eg: the name of shdict and the size of value

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the value is to large, will fail when we call set method

Comment thread apisix/plugins/prometheus/exporter.lua Outdated

core.response.set_header("content_type", "text/plain")
return 200, core.table.concat(prometheus:metric_data())
local cached_metrics_text = shdict_prometheus_cache:get(CACHED_METRICS_KEY)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just confirm, no "err" is returned?

@SkyeYoung SkyeYoung Jul 23, 2025

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked the part code and found that the second return value, marked as "flags" in its documentation, actually returns an error when an error occurs.

Let me fix this part of the code.

Comment thread apisix/plugins/prometheus/exporter.lua Outdated
Comment thread apisix/plugins/prometheus/exporter.lua Outdated
@dosubot dosubot Bot added size:XL This PR changes 500-999 lines, ignoring generated files. and removed size:L This PR changes 100-499 lines, ignoring generated files. labels Jul 23, 2025
@SkyeYoung
SkyeYoung requested a review from membphis July 23, 2025 04:43
membphis
membphis previously approved these changes Jul 23, 2025

@membphis membphis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Comment thread apisix/plugins/prometheus/exporter.lua Outdated

-- Clear the cached data after cache_exptime to prevent stale data in case of an error.
local _, err, forcible = shdict_prometheus_cache:set(CACHED_METRICS_KEY, res, cache_exptime)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this blank line

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread apisix/plugins/prometheus/exporter.lua Outdated
Comment thread apisix/plugin.lua
require("apisix.plugins.prometheus.exporter").http_init(prometheus_enabled_in_stream)
elseif not is_http and core.table.array_find(stream_plugin_names, "prometheus") then
require("apisix.plugins.prometheus.exporter").stream_init()
if is_http and (enabled_in_http or enabled_in_stream) then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SkyeYoung
SkyeYoung merged commit 30ae5df into apache:master Jul 25, 2025
39 of 40 checks passed
@SkyeYoung
SkyeYoung deleted the young/perf/prometheus-exporter-concurrency branch July 25, 2025 01:30
Comment thread apisix/plugin.lua


function _M.init_worker()
-- someone's plugin needs to be initialized after prometheus

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @SkyeYoung, I'm using Prometheus metrics in my custom plugin, and with this change, there is no guarantee that the prometheus plugin is loaded before mine. What is the correct approach for defining custom metrics now?

Sorry for commenting on a closed PR

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @yurkovoznyak, could you please file an issue describing your problem in detail?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants