Skip to content

RFC: IIIF Metrics for efficiency, insight and bot defence - #161

Draft
tomcrane wants to merge 8 commits into
mainfrom
rfcs/xxx-iiif-metrics
Draft

RFC: IIIF Metrics for efficiency, insight and bot defence#161
tomcrane wants to merge 8 commits into
mainfrom
rfcs/xxx-iiif-metrics

Conversation

@tomcrane

Copy link
Copy Markdown
Contributor

What does this change?

This RFC describes changes/enhancements to DLCS to tackle two distinct problems:

  • dealing with increasing and increasingly complex automated traffic ("bots")
  • ensuring the best image serving performance for human users (even if at the expense of response times for automated callers)

A side-effect is that it gives us a very rich store of reporting data for image traffic, for other analytic purposes.

It is very much a work in progress and is opened as a draft initially.

@tomcrane
tomcrane requested a review from donaldgray July 30, 2026 14:54

@donaldgray donaldgray left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

As a whole, I'm not sure I agree with deferred orchestration as the default unless the s3 tile serving is really efficient. Even then it's a pretty fundamental change in Orchestrator - we're no longer just looking at the incoming request to determine how to serve it, it needs to use something else to determine that. That could be the ImageLocation record - it's no longer where it is on S3, it's where it is but also how it should be served (from disk, via s3)? The nas column is potentially that but might need another column too.

'UX' wise it's a change, currently the first request takes a slight hit but then subsequent should be fast, we even have the notion of "Orchestrate after ingest" in the Engine so that it pre-warms the cache but this flips that. With deferred execution every asset starts cold, so every request takes a hit until proven populer. Not necessarily bad but doesn't feel quite right. Would it be a 'mode' we can run in?

Is an option to have normal running (standard request rates, hotdisk has plenty of room) we Orchestrate on first request. Under load (request rates high/suspected bots and/or hotdisk filling) switch to deferred orchestration? Or we can conditionally do this based on incoming request, CF can flag known bots with a specific HTTP header (via CustomRequestHandling) like x-suspected-bot: true or x-suspected-bot: gptbot, or just proxy the user-agent itself and we can have a known list to check for (bots and scripts)? If it's a bot or script, we'll serve you from fast disk if we have it but we won't orchestrate for you.

Orchestration, particularly at Wellcome as the images are small, isn't that expensive - we can replace the entire Lustre volume without there being any discernible drop in average performance. I think the overall issue isn't necessarily the churn of the hotdisk, it's the currently inefficient means of scavenging. The small images are the issue - there's lots of them. Finding the files to evict is relatively expensive without an external metrics tracker.
If we metrics could just say select path_on_disk from metrics order by last_access desc limit 100; we would be able to keep up.


Throttling in Orchestrator is not a good idea. Delaying a response means holding the HTTP connection, the [Kestrel](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel?view=aspnetcore-10.0) request slot, and any upstream resources open for the duration. Under exactly the load conditions where we would want to throttle, "tar-pitting" multiplies our concurrency problem. The cheap responses are `429 Too Many Requests` with `Retry-After`, or outright blocking. Considerate bots will honour Retry-After; inconsiderate ones get escalated to blocking anyway.

Bursts of image requests for deep zoom tiles are normal IIIF traffic, generated by humans zooming into and panning around high resolution tiled images. We mustn't mistake these for automated traffic. But some bots/scripts imitate such traffic; it's trivial to write a IIIF tile-stitcher and unleash it against a collection. [There](https://docs.rs/crate/dezoomify-rs/latest) [are](https://labs.onb.ac.at/gitlab/labs-team/iiif-image-manipulation/-/tree/master/stitching) [many](https://github.com/baurls/TileStitcher) [examples](https://labs.onb.ac.at/gitlab/labs-team/iiif-image-manipulation/-/blob/6764e673fe95cf408dc396514076b428140858d9/iiif_modifier/IIIFImageStitcher.py).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bursts of image requests for deep zoom tiles are normal IIIF traffic, generated by humans zooming into and panning around high resolution tiled images. We mustn't mistake these for automated traffic.

IMO if we get our limits correct and someone is zooming and panning enough to hit them then it's fine to block them. Being a human doesn't allow unrestricted use. It's unlikely someone would hit those limits but I think some accidental blocking is okay. Doesn't have much bearing on this overall.


If requests get through WAF, and Orchestrator (or some monitor of the metrics database) thinks they are problematic, how do they get blocked? Does Orchestrator do it? Or does Orchestrator tell WAF?

The latter is possible: AWS WAF IP sets are updatable via API, and rate-based rules can be scoped to URI patterns. A DLCS _Metrics Monitor_ (or whatever watches the metrics database) pushes offending IPs/CIDRs into an IP set with a TTL; CloudFront+WAF enforces it at the edge, before traffic ever reaches Orchestrator. This is much better than blocking in Orchestrator itself, because blocked traffic costs us nothing. Orchestrator-level decisions then only need to cover the softer responses (don't-orchestrate, degrade to slower path).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The latter is possible: AWS WAF IP sets are updatable via API, and rate-based rules can be scoped to URI patterns. A DLCS Metrics Monitor (or whatever watches the metrics database) pushes offending IPs/CIDRs into an IP set with a TTL; CloudFront+WAF enforces it at the edge, before traffic ever reaches Orchestrator.

IP alone might be too blunt? Thinking of a dodgy script running at a uni, don't want to punish everyone - might need to use user-agent + IP combo.

This is much better than blocking in Orchestrator itself, because blocked traffic costs us nothing.

Tiny note - it is a small cost in comparison to compute but CF does still incur a charge for every inspected request - blocked or not. It needs to look at it to know whether to let it in or not.

| other_quality | number of requests where quality != default |
| sizes_??? | Some way of capturing the spread of sizes being asked for? |

This can join to the existing DLCS Images table for:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It's not explicitly stated here but implied by "join". I don't think this should be in the actual DLCS DB, that should be owned, managed + accessed by the DLCS alone. If we did need to join to Images table we could do that by consuming asset created/updated events and getting the latest values and storing an external record of them, or using replication etc. It could bloat db, add resource overhead and could complicate rollouts where we do hot DB switching.


### Lustre Alternative

Should we consider EBS multi-attach volumes instead of Lustre? What are the pros/cons?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

IMO the short answer is no. Did some digging as it's been a while since I looked at it but it still doesn't look like it's worth it.

  • Lower latency, likely better perf.
  • Needs to be provisioned iops (io1 or io2) EBS, 2.4TB is only marginally cheaper pretty than 2.4TB Lustre.
  • Can provision more IOPS (more cost), independant of storage (Lustre can do same, again cost)
  • Restricted to single AZ.
  • Need to use cluster-aware file system like gfs2, then it looks like you need additional services on every instance that accesses it.


If we capture `x-proxy-destination` / `x-asset-id` via an origin-response Lambda, we only ever see requests that missed the CloudFront cache. For _cost analysis_ that's the traffic we care about, but for bot detection we need the full request picture — a bot hammering a cached tile is invisible to origin-response. Viewer-response fires on every request but adds latency and cost to every request.

CloudFront real-time logs (Kinesis-native, no Lambda@Edge at all) record every request including cache hits, with `x-edge-result-type` telling you hit/miss. **BUT** they don't carry custom origin response headers! No `x-proxy-destination` and friends.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The Kinsesis -> Firehose -> S3 stuff looks sensible. One thing to mention is that the AWS docs (https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/real-time-logs.html) say "We recommend that you use the logs to understand the nature of the requests for your content, not as a complete accounting of all requests.". Kinesis logs can be dropped, it's more of an indicative sampling of requests than complete record, which should be fine but if we did need total accounting the S3 logging, albeit delayed, would be most accurate.

The key will be getting enough data that we can make informed decisions. Do we need to make a list of things we want to know, to then know how best to store + partition data?


All requests to Protagonist (current DLCS version) are via CloudFront. We already log access requests to S3, which we can query using Athena. CloudFront is already tracking usage requests - can we hook into this to get access requests? Can these be stored in a _Metrics Database_ - in PostgreSQL?

For a raw per-request table, Postgres will work at first but billions of rows of append-heavy, scan-heavy data is better handled by a *columnar* store. In the diagram above, S3 receives CloudFront data in partitioned [Parquet](https://en.wikipedia.org/wiki/Apache_Parquet) format. This is queryable forever via Athena, and is very cheap. Then, an **Aggregator** ([Athena CTAS](https://docs.aws.amazon.com/athena/latest/ug/ctas.html) or a small batch job) writes per-asset rows to PostgreSQL. PostgreSQL is the right place for the per-asset aggregate table (~60m rows max) and for joins against the existing Images table. If we later need fast ad-hoc queries on raw events, we could use [ClickHouse](https://clickhouse.com/). But we may never need it if Athena-over-Parquet covers retrospective analysis and the aggregator covers near-real-time.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Athena CTAS or a small batch job

Small, looks like Athena CTAS can only read/write to S3 so we'd need something to write that to PG. Maybe CTAS summarises then that's blatted into postgres?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants