Skip to content

Commit c08814f

Browse files
author
Martin Fris
authored
APCng storage - removed APCUIterator usage while collecting metrics, removed metrics collecting lag (#96)
* APCng storage - removed APCUIterator usage while collecting metrics, removed metrics collecting lag Signed-off-by: Rastusik <[email protected]> * fixed tests for APCng storage changes with no need for TTL for meta info generation Signed-off-by: Rastusik <[email protected]> * allowed phpstan extension installer plugin in composer, so CI pipeline can work again Signed-off-by: Rastusik <[email protected]> * APCng - some little optimisations for persistent memory workloads (e.g. swoole), better concurrent data updates, fixed tests so they work with floats correctly Signed-off-by: Rastusik <[email protected]> * small fixes for phpstan runner to pass Signed-off-by: Rastusik <[email protected]> * acpng store fix - all data have fixed ttl of 0, so no global apcu setting can make the metrics expire Signed-off-by: Rastusik <[email protected]> Signed-off-by: Rastusik <[email protected]>
1 parent df77bbc commit c08814f

15 files changed

+317
-201
lines changed

.github/workflows/tests.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535
with:
3636
php-version: ${{ matrix.php }}
3737
extensions: redis, apcu
38-
ini-values: apc.enable_cli='On'
38+
ini-values: apc.enable_cli='On',apc.shm_size = 256M
3939
coverage: none
4040

4141
- name: Install dependencies

README.APCng.md

-2
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,3 @@ Without going into excruciating detail (you can read the source for that!), the
6262
The approach `APCng` takes is to keep a "metadata cache" which stores an array of all the metadata keys, so instead of doing a scan of APCu looking for all matching keys, we just need to retrieve one key, deserialize it (which turns out to be slow), and retrieve all the metadata keys listed in the array. Once we've done that, there is some fancy handwaving which is used to deterministically generate possible sub-keys for each metadata item, based on LabelNames, etc. Not all of these keys exist, but it's quicker to attempt to fetch them and fail, then it is to run another APCUIterator looking for a specific pattern.
6363

6464
Summaries, as mentioned before, have a third nested APCUIterator in them, looking for all readings w/o expired TTLs that match a pattern. Again, slow. Instead, we store a "map", similar to the metadata cache, but this one is temporally-keyed: one key per second, which lists how many samples were collected in that second. Once this is done, an expensive APCUIterator match is no longer needed, as all possible keys can be deterministically generated and checked, by retrieving each key for the past 600 seconds (if it exists), extracting the sample-count from the key, and then generating all the APCu keys which would refer to each observed sample.
65-
66-
There is the concept of a metadata cache TTL (default: 1 second) which offers a trade-off of performance vs responsiveness. If a collect() call is made and then a new metric is subsequently tracked, the new metric won't show up in subsequent collect() calls until the metadata cache TTL is expired. By keeping this TTL short, we avoid hammering APCu too heavily (remember, deserializing that metainfo cache array is nearly as slow as calling APCUIterator -- it just doesn't slow down as you add more keys to APCu). However we want to cap how long a new metric remains "hidden" from the Prometheus scraper. For best performance, adjust the TTL as high as you can based on your specific use-case. For instance if you're scraping every 10 seconds, then a reasonable TTL could be anywhere from 5 to 10 seconds, meaning a 50 to 100% chance that the metric won't appear in the next full scrape, but it will be there for the following one. Note that the data is tracked just fine during this period - it's just not visible yet, but it will be! You can set the TTL to zero to disable the cache. This will return to `APC` engine behavior, with no delay between creating a metric and being able to collect() it. However, performance will suffer, as the metainfo cache array will need to be deserialized from APCu each time collect() is called -- which might be okay if collect() is called infrequently and you simply must have zero delay in reporting newly-created metrics.

composer.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@
5050
}
5151
},
5252
"config": {
53-
"sort-packages": true
53+
"sort-packages": true,
54+
"allow-plugins": {
55+
"phpstan/extension-installer": true
56+
}
5457
},
5558
"prefer-stable": true
5659
}

php-fpm/docker-php-ext-apcu-cli.ini

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
apc.enable_cli = On
2+
apc.shm_size = 256M

src/Prometheus/RenderTextFormat.php

+7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace Prometheus;
66

7+
use RuntimeException;
8+
79
class RenderTextFormat implements RendererInterface
810
{
911
const MIME_TYPE = 'text/plain; version=0.0.4';
@@ -65,6 +67,11 @@ private function escapeAllLabels(array $labelNames, Sample $sample): array
6567

6668
$labels = array_combine(array_merge($labelNames, $sample->getLabelNames()), $sample->getLabelValues());
6769

70+
/** @phpstan-ignore-next-line */
71+
if ($labels === false) {
72+
throw new RuntimeException('Unable to combine labels.');
73+
}
74+
6875
foreach ($labels as $labelName => $labelValue) {
6976
$escapedLabels[] = $labelName . '="' . $this->escapeLabelValue((string)$labelValue) . '"';
7077
}

src/Prometheus/Storage/APC.php

+11-11
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
namespace Prometheus\Storage;
66

7-
use APCUIterator;
7+
use APCuIterator;
88
use Prometheus\Exception\StorageException;
99
use Prometheus\Math;
1010
use Prometheus\MetricFamilySamples;
@@ -178,7 +178,7 @@ public function wipeStorage(): void
178178
// .+ | at least one additional character
179179
$matchAll = sprintf('/^%s:.+/', $this->prometheusPrefix);
180180

181-
foreach (new APCUIterator($matchAll) as $key => $value) {
181+
foreach (new APCuIterator($matchAll) as $key => $value) {
182182
apcu_delete($key);
183183
}
184184
}
@@ -241,7 +241,7 @@ private function metaData(array $data): array
241241
private function collectCounters(): array
242242
{
243243
$counters = [];
244-
foreach (new APCUIterator('/^' . $this->prometheusPrefix . ':counter:.*:meta/') as $counter) {
244+
foreach (new APCuIterator('/^' . $this->prometheusPrefix . ':counter:.*:meta/') as $counter) {
245245
$metaData = json_decode($counter['value'], true);
246246
$data = [
247247
'name' => $metaData['name'],
@@ -250,7 +250,7 @@ private function collectCounters(): array
250250
'labelNames' => $metaData['labelNames'],
251251
'samples' => [],
252252
];
253-
foreach (new APCUIterator('/^' . $this->prometheusPrefix . ':counter:' . $metaData['name'] . ':.*:value/') as $value) {
253+
foreach (new APCuIterator('/^' . $this->prometheusPrefix . ':counter:' . $metaData['name'] . ':.*:value/') as $value) {
254254
$parts = explode(':', $value['key']);
255255
$labelValues = $parts[3];
256256
$data['samples'][] = [
@@ -272,7 +272,7 @@ private function collectCounters(): array
272272
private function collectGauges(): array
273273
{
274274
$gauges = [];
275-
foreach (new APCUIterator('/^' . $this->prometheusPrefix . ':gauge:.*:meta/') as $gauge) {
275+
foreach (new APCuIterator('/^' . $this->prometheusPrefix . ':gauge:.*:meta/') as $gauge) {
276276
$metaData = json_decode($gauge['value'], true);
277277
$data = [
278278
'name' => $metaData['name'],
@@ -281,7 +281,7 @@ private function collectGauges(): array
281281
'labelNames' => $metaData['labelNames'],
282282
'samples' => [],
283283
];
284-
foreach (new APCUIterator('/^' . $this->prometheusPrefix . ':gauge:' . $metaData['name'] . ':.*:value/') as $value) {
284+
foreach (new APCuIterator('/^' . $this->prometheusPrefix . ':gauge:' . $metaData['name'] . ':.*:value/') as $value) {
285285
$parts = explode(':', $value['key']);
286286
$labelValues = $parts[3];
287287
$data['samples'][] = [
@@ -304,7 +304,7 @@ private function collectGauges(): array
304304
private function collectHistograms(): array
305305
{
306306
$histograms = [];
307-
foreach (new APCUIterator('/^' . $this->prometheusPrefix . ':histogram:.*:meta/') as $histogram) {
307+
foreach (new APCuIterator('/^' . $this->prometheusPrefix . ':histogram:.*:meta/') as $histogram) {
308308
$metaData = json_decode($histogram['value'], true);
309309
$data = [
310310
'name' => $metaData['name'],
@@ -318,7 +318,7 @@ private function collectHistograms(): array
318318
$data['buckets'][] = '+Inf';
319319

320320
$histogramBuckets = [];
321-
foreach (new APCUIterator('/^' . $this->prometheusPrefix . ':histogram:' . $metaData['name'] . ':.*:value/') as $value) {
321+
foreach (new APCuIterator('/^' . $this->prometheusPrefix . ':histogram:' . $metaData['name'] . ':.*:value/') as $value) {
322322
$parts = explode(':', $value['key']);
323323
$labelValues = $parts[3];
324324
$bucket = $parts[4];
@@ -380,7 +380,7 @@ private function collectSummaries(): array
380380
{
381381
$math = new Math();
382382
$summaries = [];
383-
foreach (new APCUIterator('/^' . $this->prometheusPrefix . ':summary:.*:meta/') as $summary) {
383+
foreach (new APCuIterator('/^' . $this->prometheusPrefix . ':summary:.*:meta/') as $summary) {
384384
$metaData = $summary['value'];
385385
$data = [
386386
'name' => $metaData['name'],
@@ -392,11 +392,11 @@ private function collectSummaries(): array
392392
'samples' => [],
393393
];
394394

395-
foreach (new APCUIterator('/^' . $this->prometheusPrefix . ':summary:' . $metaData['name'] . ':.*:value$/') as $value) {
395+
foreach (new APCuIterator('/^' . $this->prometheusPrefix . ':summary:' . $metaData['name'] . ':.*:value$/') as $value) {
396396
$encodedLabelValues = $value['value'];
397397
$decodedLabelValues = $this->decodeLabelValues($encodedLabelValues);
398398
$samples = [];
399-
foreach (new APCUIterator('/^' . $this->prometheusPrefix . ':summary:' . $metaData['name'] . ':' . str_replace('/', '\\/', preg_quote($encodedLabelValues)) . ':value:.*/') as $sample) {
399+
foreach (new APCuIterator('/^' . $this->prometheusPrefix . ':summary:' . $metaData['name'] . ':' . str_replace('/', '\\/', preg_quote($encodedLabelValues)) . ':value:.*/') as $sample) {
400400
$samples[] = $sample['value'];
401401
}
402402

0 commit comments

Comments
 (0)