Skip to content

Commit 7d4dffe

Browse files
fix: wip
1 parent fff7431 commit 7d4dffe

12 files changed

Lines changed: 68 additions & 186 deletions

File tree

benchmarks/bench.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,12 @@
2525

2626
import aioboto3
2727

28-
# Object sizes
28+
# Object sizes for realistic single-part uploads
2929
SIZES = {
3030
"tiny": 1024, # 1 KB
3131
"small": 64 * 1024, # 64 KB
3232
"medium": 1024 * 1024, # 1 MB
3333
"large": 10 * 1024 * 1024, # 10 MB
34-
"xlarge": 100 * 1024 * 1024, # 100 MB
35-
"huge": 1024 * 1024 * 1024, # 1 GiB
3634
}
3735

3836
# Endpoints

benchmarks/docker-compose.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,9 @@ services:
6868
S3PROXY_ENCRYPT_KEY: benchmark-test-key-32-bytes-!!
6969
AWS_ACCESS_KEY_ID: benchmarkadminuser
7070
AWS_SECRET_ACCESS_KEY: benchmarkadminpassword
71-
# Higher limits for benchmarking
72-
S3PROXY_MAX_CONCURRENT_UPLOADS: "50"
73-
S3PROXY_MAX_CONCURRENT_DOWNLOADS: "50"
71+
# Throttle for realistic single-part uploads (10MB files)
72+
# Memory: 10MB + 64MB = 74MB per upload, 10 concurrent = 740MB < 1GB
73+
S3PROXY_THROTTLING_REQUESTS_MAX: "10"
7474
S3PROXY_NO_TLS: "true"
7575
S3PROXY_LOG_LEVEL: WARNING # Reduce log noise during benchmarks
7676
S3PROXY_REDIS_URL: redis://redis:6379/0
@@ -82,8 +82,8 @@ services:
8282
# Required for py-spy profiling
8383
cap_add:
8484
- SYS_PTRACE
85-
mem_limit: 512m
86-
memswap_limit: 512m
85+
mem_limit: 1g
86+
memswap_limit: 1g
8787
healthcheck:
8888
test: ["CMD", "curl", "-f", "http://localhost:4433/readyz"]
8989
interval: 2s

e2e/docker-compose.e2e.yml

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -37,35 +37,23 @@ services:
3737
ports:
3838
- "8080:4433"
3939
environment:
40-
# S3 backend configuration
4140
S3PROXY_HOST: http://minio:9000
4241
S3PROXY_REGION: us-east-1
43-
44-
# Encryption key (32 bytes for AES-256)
4542
S3PROXY_ENCRYPT_KEY: test-key-for-e2e-testing-32bytes
46-
47-
# AWS credentials for MinIO access
48-
AWS_ACCESS_KEY_ID: minioadmin
49-
AWS_SECRET_ACCESS_KEY: minioadmin
50-
51-
# Concurrency limits
52-
S3PROXY_MAX_CONCURRENT_UPLOADS: "5"
53-
S3PROXY_MAX_CONCURRENT_DOWNLOADS: "5"
54-
55-
# Server configuration
5643
S3PROXY_NO_TLS: "true"
5744
S3PROXY_LOG_LEVEL: INFO
58-
59-
# Redis configuration
6045
S3PROXY_REDIS_URL: redis://redis:6379/0
46+
S3PROXY_THROTTLING_REQUESTS_MAX: "10"
47+
S3PROXY_MAX_UPLOAD_SIZE_MB: "45"
48+
AWS_ACCESS_KEY_ID: minioadmin
49+
AWS_SECRET_ACCESS_KEY: minioadmin
6150
depends_on:
6251
minio:
6352
condition: service_healthy
6453
redis:
6554
condition: service_healthy
66-
# Hard memory limit (same as Kubernetes)
67-
mem_limit: 512m
68-
memswap_limit: 512m # Same as mem_limit = no swap = hard OOM kill
55+
mem_limit: 1280m
56+
memswap_limit: 1280m
6957
healthcheck:
7058
test: ["CMD", "curl", "-f", "http://localhost:4433/readyz"]
7159
interval: 5s

manifests/templates/configmap.yaml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,8 @@ data:
1414
S3PROXY_IP: "0.0.0.0"
1515
S3PROXY_PORT: {{ .Values.server.port | quote }}
1616
S3PROXY_NO_TLS: {{ .Values.server.noTls | quote }}
17-
S3PROXY_MAX_CONCURRENT_UPLOADS: {{ .Values.performance.maxConcurrentUploads | quote }}
18-
S3PROXY_MAX_CONCURRENT_DOWNLOADS: {{ .Values.performance.maxConcurrentDownloads | quote }}
19-
S3PROXY_AUTO_MULTIPART_MB: {{ .Values.performance.autoMultipartMb | quote }}
17+
S3PROXY_THROTTLING_REQUESTS_MAX: {{ .Values.performance.throttlingRequestsMax | quote }}
18+
S3PROXY_MAX_UPLOAD_SIZE_MB: {{ .Values.performance.maxUploadSizeMb | quote }}
2019
{{- if index .Values "redis-ha" "enabled" }}
2120
S3PROXY_REDIS_URL: "redis://{{ .Release.Name }}-redis-ha-haproxy:6379/0"
2221
{{- else }}

manifests/values.yaml

Lines changed: 26 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,32 @@
11
# S3Proxy Helm Chart Values
22

3-
# Deployment settings
43
replicaCount: 3
54

6-
# Container image
75
image:
8-
# IMPORTANT: Change to your image registry
9-
# Example: ghcr.io/myorg/sseproxy-python or your private registry
106
repository: ghcr.io/YOUR_USERNAME/sseproxy-python
11-
# IMPORTANT: Never use 'latest' in production - use specific version tags
12-
# Example: "v0.1.0", "v0.2.0", etc.
137
tag: latest
148
pullPolicy: IfNotPresent
159

16-
# S3 configuration (used only when minio.enabled=false)
17-
# Ignored if MinIO is enabled - MinIO will be used as the S3 backend
10+
# S3 configuration (used when minio.enabled=false)
1811
s3:
19-
# S3-compatible endpoint: AWS S3, DigitalOcean Spaces, etc.
20-
# Examples:
21-
# - "s3.amazonaws.com" (AWS S3)
22-
# - "s3.us-west-2.amazonaws.com" (AWS S3 specific region)
23-
# - "nyc3.digitaloceanspaces.com" (DigitalOcean Spaces)
2412
host: "s3.amazonaws.com"
25-
# AWS region (ignored for non-AWS S3 services)
2613
region: "us-east-1"
2714

28-
# Server settings
2915
server:
30-
port: 4433 # Listen port (should match service.port)
31-
noTls: true # TLS termination handled by Ingress or Load Balancer
16+
port: 4433
17+
noTls: true
3218

33-
# Performance tuning settings
3419
performance:
35-
maxConcurrentUploads: 10 # Max concurrent upload operations
36-
maxConcurrentDownloads: 10 # Max concurrent download operations
37-
autoMultipartMb: 16 # Chunk size in MB for multipart uploads
20+
throttlingRequestsMax: 10
21+
maxUploadSizeMb: 45
3822

39-
# MinIO configuration (embedded S3 backend)
40-
# Set enabled: false to use external S3 (AWS, DigitalOcean Spaces, etc.)
41-
# When disabled, configure s3.host and set secrets.awsAccessKeyId/awsSecretAccessKey
23+
# Embedded MinIO (set enabled: false to use external S3)
4224
minio:
43-
enabled: true # For production, consider external S3 service
25+
enabled: true
4426
image:
4527
repository: minio/minio
46-
tag: latest # Specify version for production, e.g., "RELEASE.2024-01-16T16-07-38Z"
28+
tag: latest
4729
pullPolicy: IfNotPresent
48-
# IMPORTANT: Change credentials in production!
49-
# Default credentials below are for development only
5030
rootUser: "minioadmin"
5131
rootPassword: "minioadmin"
5232
resources:
@@ -57,38 +37,22 @@ minio:
5737
cpu: "500m"
5838
memory: "512Mi"
5939

60-
# Redis cache for upload state management
61-
# Choose one: redis-ha (for HA) or externalRedis (for managed services)
40+
# External Redis (for managed services)
6241
externalRedis:
63-
# Use this for managed Redis: AWS ElastiCache, Azure Cache, Redis Cloud, etc.
64-
# Leave empty to use redis-ha instead
65-
# Format: redis://host:port/db or redis://:password@host:port/db
66-
url: "" # e.g., "redis://my-elasticache.abc123.cache.amazonaws.com:6379/0"
67-
# Include password in URL if needed: redis://:mypassword@host:port/db
68-
# TTL for upload state in hours
42+
url: ""
6943
uploadTtlHours: 24
7044

71-
# Redis HA configuration (embedded high-availability Redis)
72-
# Uses dandydev/redis-ha chart with Sentinel for automatic failover
73-
# Set enabled: false if using externalRedis instead
45+
# Redis HA (embedded)
7446
redis-ha:
75-
enabled: true # Disable if using managed Redis service
76-
# Number of Redis replicas (1 master + N-1 replicas)
47+
enabled: true
7748
replicas: 3
49+
existingSecret: ""
7850

79-
# Use existing secret for Redis password (RECOMMENDED for production)
80-
# If provided, auth and authKey below are ignored
81-
# Create with: kubectl create secret generic redis-password --from-literal=redis-password="your-password"
82-
existingSecret: "" # Name of existing secret with key "redis-password"
83-
84-
# Persistence configuration
8551
persistentVolume:
8652
enabled: true
8753
size: 10Gi
88-
storageClass: "" # Use default storage class, or specify e.g., "gp3", "standard"
54+
storageClass: ""
8955

90-
# HAProxy for single-endpoint access (recommended)
91-
# This allows standard redis:// URLs without sentinel-aware client code
9256
haproxy:
9357
enabled: true
9458
replicas: 2
@@ -100,7 +64,6 @@ redis-ha:
10064
cpu: "200m"
10165
memory: "128Mi"
10266

103-
# Sentinel configuration
10467
sentinel:
10568
port: 26379
10669
quorum: 2
@@ -109,22 +72,17 @@ redis-ha:
10972
failover-timeout: 180000
11073
parallel-syncs: 5
11174

112-
# Redis configuration
11375
redis:
11476
port: 6379
11577
config:
11678
maxmemory-policy: volatile-lru
11779
min-replicas-to-write: 1
11880
min-replicas-max-lag: 5
11981

120-
# Security - Redis password authentication (ignored if existingSecret is set above)
121-
auth: false # Set to true to enable password protection
122-
authKey: "" # Redis password (required if auth=true and no existingSecret, generate with: openssl rand -base64 32)
123-
124-
# Pod anti-affinity for HA (spread across nodes)
82+
auth: false
83+
authKey: ""
12584
hardAntiAffinity: true
12685

127-
# Resource limits for Redis pods
12886
resources:
12987
requests:
13088
cpu: "100m"
@@ -133,54 +91,30 @@ redis-ha:
13391
cpu: "500m"
13492
memory: "512Mi"
13593

136-
# Secret Configuration
137-
# IMPORTANT: Never commit actual secrets to git!
138-
# Priority: existingSecrets > create static secret
139-
94+
# Secrets (use existing secret in production)
14095
secrets:
141-
# Option 1: Use existing Secret (RECOMMENDED for production)
142-
# Reference an existing Kubernetes secret and optionally map its keys
143-
# If using default key names, just set the secret name:
144-
# kubectl create secret generic my-s3-secrets \
145-
# --from-literal=S3PROXY_ENCRYPT_KEY="$(openssl rand -base64 32)" \
146-
# --from-literal=AWS_ACCESS_KEY_ID="AKIA..." \
147-
# --from-literal=AWS_SECRET_ACCESS_KEY="..."
14896
existingSecrets:
14997
enabled: false
150-
name: "" # Name of existing Kubernetes secret
151-
# Optional: Map secret keys if using different key names
98+
name: ""
15299
keys:
153-
encryptKey: "S3PROXY_ENCRYPT_KEY" # Secret key name for encryption key
154-
awsAccessKeyId: "AWS_ACCESS_KEY_ID" # Secret key name for access key
155-
awsSecretAccessKey: "AWS_SECRET_ACCESS_KEY" # Secret key name for secret key
156-
157-
# Option 2: Create new secret from values (use only for development)
158-
# For production, use existingSecrets with a pre-created secret
159-
# Provide values via helm --set or secure values file, never hardcode here
100+
encryptKey: "S3PROXY_ENCRYPT_KEY"
101+
awsAccessKeyId: "AWS_ACCESS_KEY_ID"
102+
awsSecretAccessKey: "AWS_SECRET_ACCESS_KEY"
160103

161-
# S3PROXY_ENCRYPT_KEY: AES-256-GCM encryption key (base64-encoded 32 bytes)
162-
# Generate with: openssl rand -base64 32
163104
encryptKey: ""
164-
165-
# AWS/S3 credentials (ignored if minio.enabled=true and using MinIO defaults)
166-
# Only needed when: minio.enabled=false AND using external S3
167105
awsAccessKeyId: ""
168106
awsSecretAccessKey: ""
169107

170-
# Logging
171-
logLevel: "INFO" # Options: DEBUG, INFO, WARNING, ERROR
108+
logLevel: "INFO"
172109

173-
# Resource limits for s3proxy pods
174-
# Adjust based on your workload and cluster capacity
175110
resources:
176111
requests:
177112
cpu: "100m"
178-
memory: "256Mi"
113+
memory: "512Mi"
179114
limits:
180115
cpu: "1000m"
181-
memory: "512Mi"
116+
memory: "1Gi"
182117

183-
# Kubernetes Service configuration
184118
service:
185-
type: ClusterIP # Use LoadBalancer for external access, or configure Ingress
186-
port: 4433 # Service port (container also runs on this port)
119+
type: ClusterIP
120+
port: 4433

s3proxy/config.py

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,11 @@ class Settings(BaseSettings):
2727
cert_path: str = Field(default="/etc/s3proxy/certs", description="TLS certificate directory")
2828

2929
# Performance settings
30-
throttling_requests_max: int = Field(default=0, description="Max concurrent requests")
31-
max_single_encrypted_mb: int = Field(default=16, description="Max single-part object size (MB)")
32-
auto_multipart_mb: int = Field(default=16, description="Auto-multipart threshold (MB)")
33-
max_concurrent_uploads: int = Field(default=10, description="Max concurrent uploads")
34-
max_concurrent_downloads: int = Field(default=10, description="Max concurrent downloads")
35-
36-
# Feature flags
37-
allow_multipart: bool = Field(default=False, description="Allow unencrypted multipart")
30+
# Memory usage: file_size + ~64MB per concurrent upload
31+
# For 1GB pod with 10MB files: ~13 concurrent safe, default 10 for margin
32+
# Files >16MB automatically use multipart encryption
33+
throttling_requests_max: int = Field(default=10, description="Max concurrent requests (0=unlimited)")
34+
max_upload_size_mb: int = Field(default=45, description="Max single-request upload size (MB)")
3835

3936
# Redis settings (for distributed state)
4037
redis_url: str = Field(default="redis://localhost:6379/0", description="Redis connection URL")
@@ -55,14 +52,9 @@ def kek(self) -> bytes:
5552
return hashlib.sha256(self.encrypt_key.encode()).digest()
5653

5754
@property
58-
def max_single_encrypted_bytes(self) -> int:
59-
"""Max single encrypted object size in bytes."""
60-
return self.max_single_encrypted_mb * 1024 * 1024
61-
62-
@property
63-
def auto_multipart_bytes(self) -> int:
64-
"""Auto-multipart threshold in bytes."""
65-
return self.auto_multipart_mb * 1024 * 1024
55+
def max_upload_size_bytes(self) -> int:
56+
"""Max upload size in bytes."""
57+
return self.max_upload_size_mb * 1024 * 1024
6658

6759
@property
6860
def s3_endpoint(self) -> str:

s3proxy/handlers/objects.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -264,11 +264,13 @@ async def handle_put_object(self, request: Request, creds: S3Credentials) -> Res
264264
if needs_chunked_decode:
265265
body = decode_aws_chunked(body)
266266

267-
if self.settings.auto_multipart_bytes > 0 and len(body) > self.settings.auto_multipart_bytes:
268-
return await self._put_multipart(client, bucket, key, body, content_type)
267+
# Reject if exceeds max upload size
268+
if len(body) > self.settings.max_upload_size_bytes:
269+
raise HTTPException(413, f"Max upload size: {self.settings.max_upload_size_mb}MB")
269270

270-
if len(body) > self.settings.max_single_encrypted_bytes:
271-
raise HTTPException(413, f"Max size: {self.settings.max_single_encrypted_mb}MB")
271+
# Auto-use multipart for files >16MB to split encryption into parts
272+
if len(body) > crypto.PART_SIZE:
273+
return await self._put_multipart(client, bucket, key, body, content_type)
272274

273275
encrypted = crypto.encrypt_object(body, self.settings.kek)
274276
etag = hashlib.md5(body).hexdigest()
@@ -385,10 +387,11 @@ async def upload_part(data: bytes) -> None:
385387
total_plaintext_size += len(chunk)
386388

387389
# Upload when buffer reaches PART_SIZE
390+
# Process immediately without intermediate variable to reduce memory
388391
while len(buffer) >= crypto.PART_SIZE:
389-
part_data = bytes(buffer[:crypto.PART_SIZE])
392+
# Extract, upload, then clear - minimizes peak memory
393+
await upload_part(bytes(buffer[:crypto.PART_SIZE]))
390394
del buffer[:crypto.PART_SIZE]
391-
await upload_part(part_data)
392395

393396
# Upload remaining data
394397
if buffer:

0 commit comments

Comments
 (0)