gaborgsomogyi opened a new pull request, #29499:
URL: https://github.com/apache/flink/pull/29499

   ## What is the purpose of the change
   
   S3-compatible object stores, and S3 itself per prefix, limit the request 
rate and shed load with
   `503 SlowDown` ("Please reduce your request rate") answers; some also close 
the connection. A sink with
   many parallel writers runs into that limit easily, because every writer 
uploads its multipart parts
   synchronously.
   
   The native S3 connector retries with the AWS SDK's standard strategy, in 
which every request retries on
   its own. Once the limit is reached, all writers back off for similar, short 
times and come back together,
   so the request rate stays above the limit. A request that meets the limit on 
every attempt runs out of
   attempts, the write fails, and with it the job. More retries do not fix that 
at higher parallelism. On an
   object store with a request-rate quota, a 1 TiB streaming write failed in 
all three runs with the standard
   strategy, each time on a single `UploadPart` request whose attempts all 
ended in a 503 answer or in a
   connection closed by the store:
   
   | Writers | `s3.retry.max-num-retries` | Failed after |
   |--------:|---------------------------:|-------------:|
   | 256     | 10                         | 31 s         |
   | 256     | 20                         | 2 min 20 s   |
   | 32      | 10                         | ~5 min       |
   
   This PR adds the option `s3.retry.mode`:
   
   - `standard`: the behavior so far.
   - `adaptive` (the new default): the SDK's adaptive retry strategy. It makes 
the same attempts with the same
     delays as `standard`, and adds a client-side rate limiter that is shared 
by all requests of an S3 client
     (one per TaskManager). The limiter does nothing until the first throttling 
answer (HTTP 429 or S3
     `SlowDown`). From then on, throttling answers reduce its send rate and 
successful answers let it recover,
     so the writers settle at the rate the store accepts instead of all 
retrying at full speed. A request may
     wait before its first attempt: the mode trades latency for success.
   
   The circuit-breaker setting (`s3.retry.circuit-breaker.enabled`, off by 
default) applies to both modes.
   
   Because the limiter stays out of the way until the store throttles, a 
deployment whose store does not
   throttle is not affected by the new default. Where the store does throttle, 
requests are slowed down instead
   of failing. `s3.retry.mode: standard` restores the previous behavior.
   
   ## Brief change log
   
     - `NativeS3FileSystemFactory`: the option `s3.retry.mode` (`adaptive` | 
`standard`) with the nested enum
       `RetryMode`, passed on to the client provider.
     - `S3RetryStrategyFactory` (new, `@Internal`): creates the SDK retry 
strategy of a mode, with a `switch`
       that fails on an unknown mode. Both modes use the same number of 
attempts, the same delays and the same
       circuit-breaker setting.
     - `S3ClientProvider`: carries the mode and takes the strategy from the 
factory, for the synchronous and
       the asynchronous client.
     - Module README: the option is added to the "Retries and Timeouts" table.
   
   ## Verifying this change
   
   This change added tests and can be verified as follows:
   
     - `S3RetryStrategyFactoryTest` (new): the strategy type of each mode; the 
number of attempts is the
       number of retries plus one in both modes; the circuit breaker stays off 
unless it is enabled, and stops
       the retries when it is, in both modes.
     - `S3ClientProviderRetryModeTest` (new): a real SDK client against a local 
HTTP stub that answers
       `503 SlowDown`, in both modes. A throttled request is retried until the 
store answers, and a request
       that stays throttled fails after the configured number of retries.
     - `NativeS3FileSystemFactoryTest` and `S3ClientProviderTest`: the default 
is `adaptive`; the option can be
       set, also by the lower-case value of a Flink configuration file 
(`s3.retry.mode: standard`).
     - All unit tests of the module pass.
     - Manually, end to end, on an S3-compatible object store with a 
request-rate quota (details below).
   
   **Manual verification.** A streaming SQL job, `datagen` into the 
`filesystem` sink (`raw` format, 1 KiB
   rows, 128 MiB part files, multipart parts of about 6 MB), on Flink 2.2 on 
Kubernetes, checkpoint interval
   5 minutes, `s3.retry.max-num-retries: 10` and the restart strategy `none`, 
so that a request that runs out
   of attempts fails the job. The 100 GiB data was read back by a batch job 
that counts rows and characters
   and fails on any difference. MB/s is 10^6 bytes per second. The runs below 
differ only in the retry mode.
   Important to mention that this S3 backend is artificially configured for 
high throttling and low speed so
   read the following numbers considering that.
   
   100 GiB, 16 writers on 2 TaskManagers, `standard` against `adaptive`:
   
   |                                               | `standard`  | `adaptive`  
| Change |
   
|-----------------------------------------------|------------:|------------:|-------:|
   | Write duration                                | 17 min 15 s | 14 min 40 s 
| -15%   |
   | Write throughput                              | 104 MB/s    | 122 MB/s    
| +18%   |
   | S3 calls (a call the SDK repeats counts once) | 20,080      | 20,096      
| same   |
   | Throttled responses                           | 2,164       | 743         
| -66%   |
   | Retried attempts                              | 4,351       | 1,350       
| -69%   |
   | HTTP attempts (calls + retries)               | 24,431      | 21,446      
| -12%   |
   | Throttled share of the attempts               | 8.9%        | 3.5%        
|        |
   | Completed calls per second (IOPS)             | 19.4        | 22.8        
| +18%   |
   | Rejected (throttled) requests per second      | 2.1         | 0.8         
| -60%   |
   | Read-back duration                            | 3 min 20 s  | 3 min 44 s  
| +12%   |
   | Read-back throughput                          | 537 MB/s    | 480 MB/s    
| -11%   |
   | Read-back throttled responses                 | 551         | 395         
| -28%   |
   | Read-back completed calls per second (IOPS)   | 12.0        | 10.7        
| -11%   |
   
   With `adaptive` the writers sent 12% fewer HTTP attempts, got 66% fewer 
throttled responses and finished
   the write sooner, because they spent less time in backoff. The total request 
rate is about the same (24 HTTP
   attempts per second in both runs); with `adaptive` more of them complete 
(19.4 to 22.8 calls per second) and
   fewer are rejected (2.1 to 0.8 per second). The read-back, which has no 
writers to
   coordinate, was not faster. These are single runs against a shared store, so 
the absolute rates vary from
   run to run.
   
   ## Notes for reviewers
   
     - **Internal SDK class.** The public `AdaptiveRetryStrategy.Builder` (SDK 
2.44.4) has no switch for the
       circuit breaker, and with the breaker left on, a throttling storm would 
drain its retry quota and stop
       the retries even though `s3.retry.circuit-breaker.enabled` is `false`. 
`S3RetryStrategyFactory` therefore
       casts the builder to the SDK's `DefaultAdaptiveRetryStrategy.Builder`, 
which has it. The circuit-breaker
       tests run against both modes, so an SDK upgrade that changes this fails 
the build instead of the job.
     - **Default.** The default changes from the standard to the adaptive 
strategy. If you would rather roll it
       out in two steps, the default is one line in 
`NativeS3FileSystemFactory#RETRY_MODE`.
     - **Scope of the limiter.** It belongs to one S3 client, so one 
TaskManager; there is no coordination
       between TaskManagers, each one adapts to the throttling it sees. All 
requests of a client share it,
       whichever bucket they go to. The AWS CRT async client has its own 
retries and ignores the option (said in
       the option description).
     - **Cost.** The limiter can delay a request before its first attempt, and 
a client
       that is throttled on its very first requests starts from the SDK's floor 
of 0.5 requests per second,
       which is why the stub-based tests take about 2 s longer in `adaptive` 
mode.
     - **Not covered.** Connection errors are retried but are not throttling 
answers, so they do not slow the
       limiter. Treating a connection
       closed by the store as a throttling signal could be a follow-up.
   
   ## Does this pull request potentially affect one of the following parts:
   
     - Dependencies (does it add or upgrade a dependency): no. The adaptive 
strategy is in the SDK artifact
       that already provides `StandardRetryStrategy`.
     - The public API, i.e., is any changed class annotated with 
`@Public(Evolving)`: no.
       `NativeS3FileSystemFactory` is `@Experimental`; it gains one option and 
one nested enum.
     - The serializers: no
     - The runtime per-record code paths (performance sensitive): no
     - Anything that affects deployment or recovery: JobManager (and its 
components), Checkpointing,
       Kubernetes/Yarn, ZooKeeper: no
     - The S3 file system connector: yes
   
   ## Documentation
   
     - Does this pull request introduce a new feature? yes
     - If yes, how is the feature documented? docs (the module README) and 
JavaDocs (the option description)
   
   ---
   
   ##### Was generative AI tooling used to co-author this PR?
   
   - [X] Yes (please specify the tool below)
   
   Generated-by: Claude Code
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to