mirror of
https://github.com/suitenumerique/docs.git
synced 2026-09-08 18:57:54 +02:00
⚡️(backend) optimize media_auth cpu usage
Once the sql queries improved we have still a bottleneck on large concurrent requests on this endpoint. We notive in the profiles generated that lot of time was spent in creating a new s3 client instance on each request. django_storage use a thread local cache for signed and unsigned connection, but using uvicorn we have a new thread for each request, so on each request a new s3 client is generated and it appears to be an expensive operation. To fix this issue, we cache the client and share it accross all the thread and requests.
This commit is contained in:
@@ -17,6 +17,7 @@ and this project adheres to
|
||||
|
||||
- ♿️(frontend) use semantic `<dl>` structure in document info card #2379
|
||||
- 💄(frontend) use the same highlight color for cells and moves #2575
|
||||
- ⚡️(backend) optimize media_auth endpoint
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ import botocore
|
||||
from lasuite.oidc_login.decorators import refresh_oidc_access_token
|
||||
from rest_framework.throttling import BaseThrottle
|
||||
|
||||
from core.utils.s3 import get_s3_client, get_unsigned_s3_client
|
||||
|
||||
|
||||
def nest_tree(flat_list, steplen):
|
||||
"""
|
||||
@@ -76,14 +78,14 @@ def generate_s3_authorization_headers(key):
|
||||
- access control is truly realtime
|
||||
- the object storage service does not need to be exposed on internet
|
||||
"""
|
||||
url = default_storage.unsigned_connection.meta.client.generate_presigned_url(
|
||||
url = get_unsigned_s3_client().generate_presigned_url(
|
||||
"get_object",
|
||||
ExpiresIn=0,
|
||||
Params={"Bucket": default_storage.bucket_name, "Key": key},
|
||||
)
|
||||
request = botocore.awsrequest.AWSRequest(method="get", url=url)
|
||||
|
||||
s3_client = default_storage.connection.meta.client
|
||||
s3_client = get_s3_client()
|
||||
# pylint: disable=protected-access
|
||||
credentials = s3_client._request_signer._credentials # noqa: SLF001
|
||||
frozen_credentials = credentials.get_frozen_credentials()
|
||||
|
||||
@@ -73,6 +73,7 @@ from core.tasks.mail import send_ask_for_access_mail
|
||||
from core.utils.analytics import PosthogEventName, posthog_capture
|
||||
from core.utils.dicts import lowercase_keys
|
||||
from core.utils.paths import filter_descendants
|
||||
from core.utils.s3 import get_s3_client
|
||||
from core.utils.s3_response_stream import content_stream
|
||||
from core.utils.treebeard import create_tree_node_with_retry
|
||||
from core.utils.users import users_sharing_documents_with
|
||||
@@ -2043,8 +2044,12 @@ class DocumentViewSet(
|
||||
logger.debug("User '%s' lacks permission for attachment", user)
|
||||
raise drf.exceptions.PermissionDenied()
|
||||
|
||||
# Check if the attachment is ready
|
||||
s3_client = default_storage.connection.meta.client
|
||||
# Check if the attachment is ready. Use the process-global S3 client
|
||||
# (see core.utils.s3): django-storages caches its client per thread, so
|
||||
# relying on default_storage.connection here rebuilds the boto3 client
|
||||
# on every fresh thread -- profiling showed that client construction,
|
||||
# not the DB, dominated this endpoint's CPU under load.
|
||||
s3_client = get_s3_client()
|
||||
bucket_name = default_storage.bucket_name
|
||||
try:
|
||||
head_resp = s3_client.head_object(Bucket=bucket_name, Key=key)
|
||||
|
||||
@@ -12,6 +12,7 @@ from django.utils import timezone
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
from botocore.client import BaseClient
|
||||
from freezegun import freeze_time
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Process-global boto3 S3 client accessors.
|
||||
|
||||
django-storages caches its S3 connection on **thread-local** storage
|
||||
(``S3Storage.connection`` / ``unsigned_connection``). Under a multi-threaded or
|
||||
async-threadpool server, every thread that has not served this storage yet
|
||||
rebuilds the boto3 client from scratch — and building a client makes botocore
|
||||
load its service model off disk (millions of ``stat``/``listdir`` syscalls plus
|
||||
a JSON decode of the model). Profiling the ``media-auth`` hot path showed that
|
||||
this client construction, not PostgreSQL, dominated its CPU: ~2.6 fresh clients
|
||||
were built per call, one for ``head_object`` and two more inside
|
||||
``generate_s3_authorization_headers`` (signed + unsigned).
|
||||
|
||||
boto3 *clients* are thread-safe once built (only *resources* are not, and we use
|
||||
``.meta.client`` exclusively), and their attached credential provider still
|
||||
refreshes rotating credentials on demand. So we can build each variant **once
|
||||
per process** and share it across every thread and request, which removes the
|
||||
per-request/per-thread rebuild entirely.
|
||||
|
||||
The clients are captured from ``default_storage`` so they inherit its exact
|
||||
configuration (endpoint, region, signature version, credentials, TLS).
|
||||
"""
|
||||
|
||||
import threading
|
||||
|
||||
from django.core.files.storage import default_storage
|
||||
|
||||
_LOCK = threading.Lock()
|
||||
_CLIENTS = {}
|
||||
|
||||
|
||||
def _cached(name, factory):
|
||||
"""Return the process-global client ``name``, building it once via factory."""
|
||||
client = _CLIENTS.get(name)
|
||||
if client is None:
|
||||
with _LOCK:
|
||||
# Re-check inside the lock: another thread may have built it while we
|
||||
# waited (double-checked locking).
|
||||
client = _CLIENTS.get(name)
|
||||
if client is None:
|
||||
client = factory()
|
||||
_CLIENTS[name] = client
|
||||
return client
|
||||
|
||||
|
||||
def get_s3_client():
|
||||
"""Return a process-global signed S3 client (thread-safe, built once)."""
|
||||
return _cached("signed", lambda: default_storage.connection.meta.client)
|
||||
|
||||
|
||||
def get_unsigned_s3_client():
|
||||
"""Return a process-global unsigned S3 client, for presigning URLs."""
|
||||
return _cached("unsigned", lambda: default_storage.unsigned_connection.meta.client)
|
||||
Reference in New Issue
Block a user