bmanan7 opened a new issue, #70867:
URL: https://github.com/apache/airflow/issues/70867
### Description
`WasbRemoteLogIO.write()` appends to a task log by rewriting the entire
object. It checks whether the
blob exists, downloads the complete existing log, joins it with the new
content in memory, and uploads
the whole result again as a block blob with `overwrite=True`. The whole
retained log therefore crosses
the network in both directions on every upload.
One task attempt can be uploaded to many times, because the remote key comes
from `try_number` and
some lifecycles reuse a single attempt. A reschedule-mode sensor is the
clearest case: every poke is a
separate worker process, and `UP_FOR_RESCHEDULE` does not change
`try_number`, so all of them write to
the same `attempt=N.log`. The poke count is bounded by the sensor's timeout,
but that bound is high:
`[sensors] default_timeout` is 604800 seconds, so at the default
`poke_interval` of 60 seconds a
single attempt can reach around 10,000 uploads before it times out.
What I would like is for a new segment of log to be sent as a new segment,
without reading back and
rewriting everything already stored.
### Preferred approach: Azure AppendBlob
Append blobs are Azure's native answer to this. They keep one logical log
object per attempt, so
nothing on the read path or in the UI has to change in the normal case, and
the diff is contained
inside this provider.
The local-segment behavior proposed in #70860 makes the flow below possible.
With that change, the
local log file holds exactly the current lifecycle's output: on the same
worker the previous
successful upload truncated it, and on a different worker it starts empty.
So the local file is the
segment to append, with no offset bookkeeping needed.
1. Read the local file. That is the new segment.
2. If the blob does not exist, create an empty append blob.
3. If it does exist, read its current length, which is the append position.
4. Append the segment with `append_block()`, passing an append-position
condition.
5. Truncate the local file only after the append succeeds.
6. If the object already exists as a block blob, use the current write path
for it and never convert
it in place.
Step 4 has to use explicit append calls rather than
`BlobClient.upload_blob(blob_type="AppendBlob")`. In
`_upload_helpers.upload_append_blob` the
condition object is built as
`AppendPositionAccessConditions(append_position=None)` and a
caller-supplied `appendpos_condition` is ignored, so the first block of
every call carries no
position guard. Since `azure-core` retries connection errors by default, a
block that the service
committed but whose response was lost would be appended twice.
`append_block()` accepts the condition
directly.
#### Rollout
Worth making this opt-in to begin with, with block blob remaining the
default, so the change can land
and be exercised before it becomes the behaviour everyone gets:
```
remote_wasb_log_write_mode = block_blob # default, current behaviour
remote_wasb_log_write_mode = append_blob # opt-in
```
The name needs checking against current conventions in the
`[azure_remote_logging]` section, which
today holds `remote_wasb_log_container`, and against the `from_config()`
work in #70265 and #70268,
since that is where the option would be read.
#### Constraints to handle and document
- An append-position mismatch has to fail safely rather than silently write
at the wrong offset.
- Segments larger than the service block limit need chunking.
- A partial failure part way through a multi-block append needs explicit
recovery, so the next attempt
neither duplicates a committed block nor skips one.
- An append blob holds at most 50,000 blocks, so a very long-lived attempt
needs a rollover to a
sibling key.
- Rollover naming and read ordering both need validating. The reader already
lists by prefix and
concatenates in sorted order, so the mechanism exists, but the naming has
to sort correctly
alongside the existing `.trigger.<job_id>.log` suffix, and the extra
objects will show up in the
log source list.
- Append blobs cannot be moved to cool or archive by a lifecycle policy.
Lifecycle delete rules still
work, but anyone tiering old task logs would need to stay on block blobs.
- Objects already written as block blobs stay readable and keep using the
current write path. Azure
does not support changing an existing blob's type in place; versioned
blobs make replacement even
more restrictive. So conversion is not an option anyway.
- UTF-8 and newline handling across a segment boundary needs to be right,
since the reader decodes
the assembled object as text.
### Alternatives considered
**Immutable segment objects.** Each lifecycle writes a new
`...part<NNNNN>.log` and nothing is ever
modified. Attractive because it is not Azure-specific and would fix S3, GCS,
OSS and HDFS too, none of
which have a native append. But it is a cross-provider architecture change
and it alters the
on-storage layout for anything reading these logs outside Airflow, so it is
a much larger discussion
than this provider.
**Block blob block lists.** `Put Block` plus `Put Block List`, re-committing
the previous block ids.
Keeps block blob semantics, so lifecycle tiering still works. Against it:
the client has to track the
block id list across separate worker processes, concurrent writers have to
be handled explicitly, and
`Put Block List` is a version-creating operation, so accounts with
versioning enabled would still
accumulate a version per upload. It also still caps at 50,000 blocks.
### Use case/motivation
I want a long-running sensor to cost roughly its own log size in traffic,
rather than a multiple of it
that grows with how long the sensor ran.
The growth is quadratic. With N uploads of `s` new bytes each, the
cumulative upload is `s*N(N+1)/2`
while the log actually retained is only `N*s`. Measured against the current
class with an instrumented
fake blob store, using a fresh local log directory per lifecycle so only the
rewrite behaviour is in
play:
| uploads | log retained | uploaded | downloaded | uploaded / retained |
|---|---|---|---|---|
| 1 | 20 B | 20 B | 0 B | 1.0x |
| 10 | 210 B | 1,146 B | 936 B | 5.5x |
| 20 | 430 B | 4,456 B | 4,026 B | 10.4x |
| 40 | 870 B | 17,676 B | 16,806 B | 20.3x |
| 80 | 1,750 B | 70,516 B | 68,766 B | 40.3x |
Projecting that, on the illustrative assumption of 1 KiB of new log per poke
(I measured 758 bytes for
a deliberately minimal reschedule sensor on Airflow 3.3; other Dags may
generate more):
| | 24 hours at the default 60 s interval | default 7 day sensor timeout |
|---|---|---|
| uploads | 1,440 | 10,080 |
| log retained | 1.4 MiB | 9.8 MiB |
| uploaded | 1,013 MiB | 48.5 GiB |
| downloaded | 1,012 MiB | 48.4 GiB |
| total moved | 2.0 GiB | 96.9 GiB |
| storage operations | 4,319 | 30,239 |
Total bytes moved works out to roughly N times the size of the log being
stored.
Two things follow from rewriting the whole object, beyond the byte count.
Memory. Each upload holds the entire log in memory to concatenate it, so
peak usage tracks the log
size rather than the size of the new segment. The same problem on the S3
handler was reported as a
worker OOM in #67144.
Latency at the end of every lifecycle. The upload happens as the task
finishes and gets slower as the
log grows, so a sensor's teardown time increases over the life of the
attempt.
On storage accounts with blob versioning enabled it is worse again, because
`Put Blob` creates a new
version each time. One attempt can leave thousands of full versions of a
single blob, against
Microsoft's guidance to stay under 1,000 versions per blob.
### Testing this would need
- Both configuration modes, including that the default is unchanged.
- Successive lifecycles on the same worker, and on different workers.
- Retry of a failed append, confirming no duplicated or skipped block.
- An existing block blob, confirming it stays readable and keeps the current
write path.
- A segment larger than one block, and a partial failure part way through a
multi-block append.
- The 50,000 block limit and whatever rollover is chosen, including that
reads come back in order.
- UTF-8 across segment boundaries, and newline handling where one segment
ends and the next begins.
- Reading the assembled log through the UI and the API.
- Bytes and operations before and after, so the improvement is measured
rather than asserted.
The core flow can be tested with Azurite. Conditional retry, append-position
conflict, and block-limit
behavior should be validated against Azurite's supported behavior and
supplemented with mocks or a real
Azure Storage account where necessary.
### Related issues
- #70860 proposes a fix for a separate content-duplication defect in the
same code path, where a local
log file that survived between lifecycles was re-uploaded in full. This
issue is the remaining half:
even with every line stored exactly once, each upload still rewrites the
whole object. That proposal
is also what makes the design above straightforward, since the local file
becomes a clean segment.
- #22496 proposed append blobs for this handler in 2022. It was closed by
the stale bot without
review and its author noted they had not tested it. It went through
`load_string(blob_type='AppendBlob')`, so it inherits the unguarded first
block described above, and
it does not deal with existing block blobs or the block limit.
- #67144 fixed the duplication half for S3 and is where the OOM symptom was
first reported.
- #68304, a closed follow-up draft to that fix, noted that "the Google and
Azure object store logging
may have a similar memory bug due to a download, append, upload process".
- #45079 is the read side of the same shape, running out of memory serving
large logs.
- #70265 and #70268 cover the `from_config()` migration for this handler,
which is where a new option
would be wired in.
- #15907 and the PRs around it are the history of how append behaviour
reached its current form after
the move to azure-storage-blob v12.
### Are you willing to submit a PR?
- [ ] Yes I am willing to submit a PR!
### Code of Conduct
- [X] I agree to follow this project's [Code of
Conduct](https://github.com/apache/airflow/blob/main/CODE_OF_CONDUCT.md)
--
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]