RFC: IIIF Metrics for efficiency, insight and bot defence - #161
Conversation
donaldgray
left a comment
There was a problem hiding this comment.
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). |
There was a problem hiding this comment.
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). |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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? |
There was a problem hiding this comment.
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 (
io1orio2) 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. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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?
What does this change?
This RFC describes changes/enhancements to DLCS to tackle two distinct problems:
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.