developmentseed / titiler

Build your own Raster dynamic map tile services
https://developmentseed.org/titiler/
MIT License
755 stars 156 forks source link

boto3 slows down Titiler's GET /cog/tiles? #792

Closed glostis closed 5 months ago

glostis commented 5 months ago

Hi,

Thanks for this amazing library!

I've been trying to fine-tune the performance of titiler, and encountered a strange behavior. I'm not sure where exactly in the stack the problem lies — if it's at titiler's level, or rio-tiler, or rasterio. Feel free to redirect me if this is not the appropriate place to post such an issue.

TL;DR

The short version is that, when trying to get tiles from a COG stored on S3, if boto3 is installed, the time to get the tiles is roughly doubled compared to when boto3 is not installed (note: titiler still manages to access files on S3 without boto3).

This performance drop may be related to the specific setup I have (which I will describe below), but in any case I would like to have your opinion on what is going on.

Reproducing locally

  1. I have a COG stored on a local MinIO instance at s3://bucket/cog.tif.

    Details to reproduce locally ```bash docker run -p 9000:9000 -p 9001:9001 --rm --name minio minio/minio:latest server /data --console-address ":9001" # Create a bucket called `bucket` docker exec -it minio mc mb --ignore-existing data/bucket ``` Then go to [the MinIO dashboard](http://localhost:9001/browser/bucket) and upload a `cog.tif` file.
  2. In all further steps, I have exported the following environment variables:

    export AWS_ACCESS_KEY_ID=minioadmin
    export AWS_SECRET_ACCESS_KEY=minioadmin
    export AWS_S3_ENDPOINT="127.0.0.1:9000"
    export AWS_VIRTUAL_HOSTING="FALSE"
    export AWS_HTTPS="FALSE"
  3. I have a minimal titiler instance, defined as below, and launched with python minititiler.py:

    Code for minimal titiler ```python import logging import uvicorn from fastapi import FastAPI from titiler.core.errors import DEFAULT_STATUS_CODES, add_exception_handlers from titiler.core.factory import TilerFactory from titiler.core.middleware import LoggerMiddleware, TotalTimeMiddleware logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) app = FastAPI(title="My simple app") try: import boto3 from rasterio.session import AWSSession cog = TilerFactory(environment_dependency=lambda: {"session": AWSSession(boto3.Session())}) except ImportError: cog = TilerFactory() app.include_router(cog.router, tags=["Cloud Optimized GeoTIFF"], prefix="/cog") app.add_middleware(LoggerMiddleware, headers=True, querystrings=True) app.add_middleware(TotalTimeMiddleware) add_exception_handlers(app, DEFAULT_STATUS_CODES) if __name__ == "__main__": uvicorn.run(app, log_level="info") ```
  4. I use the below script to profile the access time to get tiles, launched with python profile.py

    Code for profiling ```python import argparse import asyncio import random import statistics from collections import Counter import aiohttp import morecantile from rio_tiler.io import COGReader random.seed(42) async def fetch(session, url, cog_url): async with session.get(url, params={"url": cog_url}) as response: return float(response.headers["server-timing"].split("=")[1]), response.status async def main(titiler_url, cog_url, tile_number): tilematrixset = morecantile.tms.get("WebMercatorQuad") with COGReader(cog_url, tms=tilematrixset) as cog: minzoom = cog.minzoom maxzoom = cog.maxzoom w, s, e, n = cog.geographic_bounds urls = [] for zoom in range(minzoom, maxzoom + 1): ul_tile = tilematrixset.tile(w, n, zoom) lr_tile = tilematrixset.tile(e, s, zoom) for x in range(ul_tile.x, lr_tile.x + 1): for y in range(ul_tile.y, lr_tile.y + 1): urls.append(f"{titiler_url}/cog/tiles/{zoom}/{x}/{y}") urls = random.choices(urls, k=tile_number) async with aiohttp.ClientSession() as session: tasks = [fetch(session, url, cog_url) for url in urls] results = await asyncio.gather(*tasks) times = [el[0] for el in results] codes = [el[1] for el in results] print("Got the following response codes:") for code, nb in Counter(codes).items(): print(f"- HTTP {code}: {nb} responses") mean = statistics.mean(times) stdev = statistics.stdev(times) print() print(f"Server response time per tile: {mean:.0f} ± {stdev:.0f} ms") if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--titiler", help="URL to the titiler server", default="http://localhost:8000") parser.add_argument("--cog", help="URL to the Cloud Optimized GeoTIFF", default="s3://bucket/cog.tif") parser.add_argument("--number", type=int, help="Number of tiles to fetch", default=50) args = parser.parse_args() asyncio.run(main(args.titiler, args.cog, args.number)) ```

Results

  1. Do you have an explanation for the slowdown caused by the presence of boto3? It's most certainly related to this conditional import in rasterio, but I don't understand why it impacts the performance of titiler.
  2. As you can see in my code for minititiler.py, I've tried to use the environment_dependency field from the TileFactory to pass an already instanciated rasterio.session.AWSSession to titiler's tiling route, but it doesn't seem to make much of a difference. Maybe I'm using it wrong?