Skip to content

Commit efbcce2

Browse files
[Backport release-1.7] fix(seaweedfs): build SeaweedFS in-tree with a bounded S3 read-ahead (#5028)
Backport of #4819 to `release-1.7`. The five commits are cherry-picked in order from 1913104, d2c6b85, 5592a3f, ecdc043 and e8635f6. One conflict, in `.github/renovate.json`: the rule #4819 adds sits next to two e2e sandbox rules from #4867, which is not on this branch. I kept this branch's rules and added only the seaweedfs rule. The diff is the same nine files, +281/-2, as #4819. The bug is the same here. This branch runs the upstream `chrislusf/seaweedfs:4.45` image, and its S3 gateway is OOM-killed while serving a large object (#3782). This needs to land before rc.2 is cut, otherwise rc.2 and v1.7.0 ship the upstream image.
2 parents 2c3ff51 + 7f4c346 commit efbcce2

9 files changed

Lines changed: 281 additions & 2 deletions

File tree

‎.gitattributes‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
# Unified diff context has a marker before Go tabs and blank lines.
22
packages/system/capi-providers-infraprovider/images/cluster-api-provider-kubevirt/patches/*.diff -whitespace
33
packages/system/cilium/images/cilium-operator/patches/*.diff -whitespace
4+
packages/system/seaweedfs/images/seaweedfs/patches/*.diff -whitespace

‎.github/renovate.json‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@
8585
"matchFileNames": ["packages/system/cilium/images/cilium-operator/**"],
8686
"enabled": false
8787
},
88+
{
89+
"description": "The seaweedfs rebuild compiles one upstream release with the Go toolchain upstream's docker/Dockerfile.go_build uses at that tag, lays it over the upstream image of that tag, and `make update` in packages/system/seaweedfs re-pins the builder, the base image digest and the source checksum from there. A Renovate bump of any of them builds a combination upstream never shipped, and the next `make update` silently reverts it. Updates are disabled here; all of them move with the SeaweedFS version.",
90+
"matchManagers": ["dockerfile"],
91+
"matchFileNames": ["packages/system/seaweedfs/images/seaweedfs/**"],
92+
"enabled": false
93+
},
8894
{
8995
"description": "kubevirt.io/csi-driver is pinned to an untagged pseudo-version commit and publishes no release tags, so Renovate's go datasource returns no-result and the lookup failure lingers on the Dependency Dashboard. Disable it; this module is advanced deliberately alongside the kubevirt-csi-driver image build.",
9096
"matchManagers": ["gomod"],

‎Makefile‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ build: build-deps
3737
make -C packages/system/capi-providers-infraprovider image
3838
make -C packages/system/multus image
3939
make -C packages/system/bucket image
40+
make -C packages/system/seaweedfs image
4041
make -C packages/system/objectstorage-controller image
4142
make -C packages/system/securitygroup-controller image
4243
make -C packages/system/grafana-operator image

‎hack/build-matrix_test.bats‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,15 +110,15 @@ load test_helper
110110
tmp=$(mktemp)
111111
echo "packages/system/seaweedfs/values.yaml" > "$tmp"
112112
out=$(hack/build-matrix.sh "$tmp")
113-
[ "$out" = '["packages/system/objectstorage-controller"]' ]
113+
[ "$out" = '["packages/system/seaweedfs","packages/system/objectstorage-controller"]' ]
114114
rm -f "$tmp"
115115
}
116116

117117
@test "seaweedfs change does not duplicate an already-selected objectstorage-controller" {
118118
tmp=$(mktemp)
119119
printf 'packages/system/seaweedfs/values.yaml\npackages/system/objectstorage-controller/values.yaml\n' > "$tmp"
120120
out=$(hack/build-matrix.sh "$tmp")
121-
[ "$out" = '["packages/system/objectstorage-controller"]' ]
121+
[ "$out" = '["packages/system/seaweedfs","packages/system/objectstorage-controller"]' ]
122122
rm -f "$tmp"
123123
}
124124

‎packages/system/seaweedfs/Makefile‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,36 @@
11
export NAME=seaweedfs-system
2+
SEAWEEDFS_TAG=$(shell grep -F 'ARG VERSION=' images/seaweedfs/Dockerfile | cut -f2 -d=)
23

34
include ../../../hack/common-envs.mk
45
include ../../../hack/package.mk
56

7+
.PHONY: image
8+
image:
9+
docker buildx build images/seaweedfs \
10+
$(call image-tags,seaweedfs,$(SEAWEEDFS_TAG)) \
11+
$(call cache-args,seaweedfs) \
12+
--metadata-file images/seaweedfs.json \
13+
$(BUILDX_ARGS)
14+
REPOSITORY="$(REGISTRY)/seaweedfs" \
15+
yq -i '.seaweedfs.image.repository = strenv(REPOSITORY)' values.yaml
16+
TAG=$(IMAGE_TAG)@$$(yq e '."containerimage.digest"' images/seaweedfs.json -o json -r) \
17+
yq -i '.seaweedfs.image.tag = strenv(TAG)' values.yaml
18+
rm -f images/seaweedfs.json
19+
620
test:
721
helm unittest .
822
$(MAKE) test-guard-fail-closed
23+
$(MAKE) test-image-version
24+
25+
# The image replaces weed in the upstream image of ARG VERSION, so a chart moved
26+
# by `make update` without the Dockerfile would run the previous release's
27+
# binary under the new chart.
28+
.PHONY: test-image-version
29+
test-image-version:
30+
@chart=$$(yq e '.appVersion' charts/seaweedfs/Chart.yaml); \
31+
[ "$(SEAWEEDFS_TAG)" = "$$chart" ] \
32+
|| { echo "FAIL: images/seaweedfs/Dockerfile builds $(SEAWEEDFS_TAG), the vendored chart is $$chart"; exit 1; }
33+
@echo "ok: images/seaweedfs builds the chart's appVersion $(SEAWEEDFS_TAG)"
934

1035
# The naming guard refuses to render an UPGRADE that cannot see the cluster
1136
# (templates/naming-guard.yaml): a client-side lookup silently returns nothing,
@@ -37,4 +62,34 @@ update:
3762
patch --no-backup-if-mismatch -p4 < patches/s3-service-name-consumers.patch
3863
patch --no-backup-if-mismatch -p4 < patches/cluster-scoped-names-per-namespace.patch
3964
#patch --no-backup-if-mismatch -p4 < patches/retention-policy-delete.yaml
65+
$(MAKE) update-image
66+
67+
# Re-pins images/seaweedfs/Dockerfile to the vendored chart's appVersion: the
68+
# source archive checksum, the upstream image digest and upstream's builder
69+
# toolchain for that tag. The patches are Go, so they are checked against the
70+
# whole source tree of the tag; once a release carries a fix, its patch stops
71+
# applying here and is dropped.
72+
.PHONY: update-image
73+
update-image:
74+
version=$$(yq e '.appVersion' charts/seaweedfs/Chart.yaml) && \
75+
upstream=$$(docker buildx imagetools inspect "chrislusf/seaweedfs:$${version}" --format '{{json .Manifest}}' | yq e '.digest' -) && \
76+
case "$$upstream" in sha256:*) ;; *) echo "ERROR: no digest for chrislusf/seaweedfs:$${version}" >&2; exit 1;; esac && \
77+
$(SED_INPLACE) -e "s/^ARG VERSION=.*/ARG VERSION=$${version}/" -e "s/^ARG UPSTREAM_DIGEST=.*/ARG UPSTREAM_DIGEST=$${upstream}/" images/seaweedfs/Dockerfile && \
78+
tmpdir=$$(mktemp -d) && \
79+
{ curl --fail --silent --show-error --location -o "$$tmpdir/source.tar.gz" \
80+
"https://github.com/seaweedfs/seaweedfs/archive/refs/tags/$${version}.tar.gz" && \
81+
mkdir "$$tmpdir/src" && tar -xzf "$$tmpdir/source.tar.gz" --strip-components=1 -C "$$tmpdir/src" || { rm -rf "$$tmpdir"; exit 1; }; } && \
82+
source=$$(sha256sum "$$tmpdir/source.tar.gz" | cut -d' ' -f1) && \
83+
$(SED_INPLACE) "s/^ARG SOURCE_SHA256=.*/ARG SOURCE_SHA256=$${source}/" images/seaweedfs/Dockerfile && \
84+
{ git -C "$$tmpdir/src" apply --check $(CURDIR)/images/seaweedfs/patches/*.diff || \
85+
{ echo "ERROR: images/seaweedfs/patches/*.diff no longer apply to seaweedfs $${version}; regenerate them, or drop the ones the new tag already carries" >&2; \
86+
rm -rf "$$tmpdir"; exit 1; }; } && \
87+
golang=$$(sed -n 's/^FROM --platform=\$$BUILDPLATFORM \([^ ]*\) AS builder$$/\1/p' "$$tmpdir/src/docker/Dockerfile.go_build") && \
88+
{ [ -n "$$golang" ] || { echo "ERROR: no builder FROM line in upstream docker/Dockerfile.go_build $${version}" >&2; rm -rf "$$tmpdir"; exit 1; }; } && \
89+
builder=$$(docker buildx imagetools inspect "$$golang" --format '{{json .Manifest}}' | yq e '.digest' -) && \
90+
{ case "$$builder" in sha256:*) ;; *) echo "ERROR: no digest for $${golang}" >&2; rm -rf "$$tmpdir"; exit 1;; esac; } && \
91+
$(SED_INPLACE) "s|^FROM --platform=\$$BUILDPLATFORM [^ ]* AS builder$$|FROM --platform=\$$BUILDPLATFORM $${golang}@$${builder} AS builder|" images/seaweedfs/Dockerfile && \
92+
{ grep -qF "FROM --platform=\$$BUILDPLATFORM $${golang}@$${builder} AS builder" images/seaweedfs/Dockerfile || \
93+
{ echo "ERROR: the builder FROM line in images/seaweedfs/Dockerfile was not re-pinned; its shape no longer matches the update recipe" >&2; rm -rf "$$tmpdir"; exit 1; }; } && \
94+
rm -rf "$$tmpdir"
4095

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Rebuild the weed binary from the upstream sources with the Cozystack patches
2+
# applied, and lay it over the upstream image of the same tag. Only the Go
3+
# binary changes: the Rust volume server and worker, the entrypoint and the
4+
# runtime packages are the upstream image's own, byte for byte.
5+
#
6+
# patches/reader-cache-prefetch-window.diff bounds the read-ahead of a
7+
# sequential chunk reader to the prefetch count it was given. Upstream counts
8+
# only the downloads it newly starts toward that window and runs the prefetch
9+
# after every ReadAt, so the S3 GET path, reading 256 KiB at a time out of
10+
# 4 MiB chunks, pushes the window four chunks further on each read until the
11+
# ReaderCache limit of 256 downloaders is reached. The gateway then holds up to
12+
# about a gigabyte of a single object in memory, is OOM-killed under the 512Mi
13+
# limit the S3 deployment gets from its resources preset, and the client sees
14+
# the response cut off tens of megabytes in:
15+
# https://github.com/cozystack/cozystack/issues/3782. Upstream fixed it in
16+
# seaweedfs/seaweedfs#11674 after 4.48; drop the patch on the first vendored
17+
# release that carries it.
18+
#
19+
# `make update` re-pins VERSION from the vendored chart, SOURCE_SHA256 from the
20+
# source archive of that tag, UPSTREAM_DIGEST from the upstream image of that
21+
# tag and the builder from upstream's docker/Dockerfile.go_build, and checks
22+
# that the patches still apply.
23+
ARG VERSION=4.45
24+
ARG SOURCE_SHA256=800a91693e2a4e974ef0e2a157b6a04eff9f4df507303b1cff31b0601081fc30
25+
ARG UPSTREAM_DIGEST=sha256:fc9f76fa993ad69966ffeb2f65d0318fcae39c6f8e20cf68ef7b3a5cb97769e5
26+
27+
# The Go toolchain of upstream's builder (docker/Dockerfile.go_build), which
28+
# compiles the upstream weed.
29+
FROM --platform=$BUILDPLATFORM golang:1.26-alpine@sha256:3082400e369fa24d5fc60bca20edab3f6d604e0c5a690ec66b295eff4dd87ade AS builder
30+
ARG GOPROXY
31+
ARG VERSION
32+
ARG SOURCE_SHA256
33+
34+
RUN apk add --no-cache git curl
35+
36+
WORKDIR /go/src/github.com/seaweedfs/seaweedfs
37+
38+
RUN curl -fsSL -o /tmp/source.tar.gz https://github.com/seaweedfs/seaweedfs/archive/refs/tags/${VERSION}.tar.gz && \
39+
echo "${SOURCE_SHA256} /tmp/source.tar.gz" | sha256sum -c - && \
40+
tar -xzf /tmp/source.tar.gz --strip-components=1 && rm /tmp/source.tar.gz
41+
42+
COPY patches /patches
43+
RUN git apply /patches/*.diff
44+
45+
ENV CGO_ENABLED=0
46+
47+
# go build never compiles a _test.go, so nothing else runs the tests the
48+
# patches carry. The list is read into a variable first: a for loop drops the
49+
# exit status of a command substitution, so a patch git cannot read would leave
50+
# the list empty and the step green.
51+
RUN files=$(git apply --numstat /patches/*.diff) || exit 1; \
52+
for dir in $(printf '%s\n' "$files" | cut -f3 | grep '\.go$' | xargs -r -n1 dirname | sort -u); do \
53+
go test "./$dir" || exit 1; \
54+
done
55+
56+
# Declared after the tests so they run once on the build platform rather than
57+
# once per target platform.
58+
ARG TARGETOS
59+
ARG TARGETARCH
60+
61+
# The source archive carries no git history for upstream's `git rev-parse`, so
62+
# the commit field of `weed version` names this build instead.
63+
RUN cd weed && GOOS=$TARGETOS GOARCH=$TARGETARCH go build \
64+
-ldflags "-X github.com/seaweedfs/seaweedfs/weed/util/version.COMMIT=cozystack-patched" \
65+
-o /out/weed .
66+
67+
FROM chrislusf/seaweedfs:${VERSION}@${UPSTREAM_DIGEST}
68+
COPY --from=builder /out/weed /usr/bin/weed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
diff --git a/weed/filer/reader_cache.go b/weed/filer/reader_cache.go
2+
--- a/weed/filer/reader_cache.go
3+
+++ b/weed/filer/reader_cache.go
4+
@@ -75,8 +75,10 @@
5+
return
6+
}
7+
8+
- cached := 0
9+
- for x := chunkViews; x != nil && cached < count; x = x.Next {
10+
+ // The window is the next count chunks, whether or not a download for them
11+
+ // is already running: counting only newly started downloads would push the
12+
+ // window count chunks further on every call.
13+
+ for x, i := chunkViews, 0; x != nil && i < count; x, i = x.Next, i+1 {
14+
chunkView := x.Value
15+
if _, found := rc.downloaders[chunkView.FileId]; found {
16+
continue
17+
@@ -98,7 +100,6 @@
18+
go cacher.startCaching()
19+
<-cacher.cacheStartedCh
20+
rc.downloaders[chunkView.FileId] = cacher
21+
- cached++
22+
}
23+
24+
return
25+
diff --git a/weed/filer/reader_at_prefetch_window_test.go b/weed/filer/reader_at_prefetch_window_test.go
26+
new file mode 100644
27+
--- /dev/null
28+
+++ b/weed/filer/reader_at_prefetch_window_test.go
29+
@@ -0,0 +1,73 @@
30+
+package filer
31+
+
32+
+import (
33+
+ "context"
34+
+ "fmt"
35+
+ "strconv"
36+
+ "strings"
37+
+ "sync/atomic"
38+
+ "testing"
39+
+
40+
+ "github.com/seaweedfs/seaweedfs/weed/util/chunk_cache"
41+
+ util_http "github.com/seaweedfs/seaweedfs/weed/util/http"
42+
+)
43+
+
44+
+// A sequential reader that consumes a chunk in several small ReadAt calls, as
45+
+// the S3 GET path does with its 256 KiB copy buffer over 4 MiB chunks, must
46+
+// keep its read-ahead at prefetchCount chunks. Each ReadAt triggers a
47+
+// prefetch; if chunks already being fetched are not counted toward the
48+
+// window, every call starts prefetchCount more downloads and the read-ahead
49+
+// runs to the ReaderCache downloader limit, buffering most of the object.
50+
+func TestChunkReadAtSequentialPrefetchStaysWithinWindow(t *testing.T) {
51+
+ const chunkSize = 64 << 10
52+
+ const chunkCount = 64
53+
+ const sliceSize = 16 << 10
54+
+
55+
+ var readerChunk atomic.Int64
56+
+ var maxLead atomic.Int64
57+
+ rc := NewReaderCache(256, (*chunk_cache.TieredChunkCache)(nil), func(context.Context, string) ([]string, error) {
58+
+ return []string{"unused"}, nil
59+
+ }, nil)
60+
+ defer rc.destroy()
61+
+ rc.fetchChunkDataFn = func(_ context.Context, buffer []byte, _ []string, _ []byte, _ bool, _ bool, _ int64, fileId string, _ util_http.RefreshUrlsFunc) (int, error) {
62+
+ idx, err := strconv.Atoi(strings.TrimPrefix(fileId, "chunk"))
63+
+ if err != nil {
64+
+ return 0, err
65+
+ }
66+
+ lead := int64(idx) - readerChunk.Load()
67+
+ for {
68+
+ cur := maxLead.Load()
69+
+ if lead <= cur || maxLead.CompareAndSwap(cur, lead) {
70+
+ break
71+
+ }
72+
+ }
73+
+ return len(buffer), nil
74+
+ }
75+
+
76+
+ views := NewIntervalList[*ChunkView]()
77+
+ for i := 0; i < chunkCount; i++ {
78+
+ views.AppendInterval(&Interval[*ChunkView]{
79+
+ StartOffset: int64(i * chunkSize),
80+
+ StopOffset: int64((i + 1) * chunkSize),
81+
+ Value: &ChunkView{
82+
+ FileId: fmt.Sprintf("chunk%d", i),
83+
+ ViewSize: chunkSize,
84+
+ ViewOffset: int64(i * chunkSize),
85+
+ ChunkSize: chunkSize,
86+
+ },
87+
+ })
88+
+ }
89+
+ reader := NewChunkReaderAtFromClient(context.Background(), rc, views, chunkSize*chunkCount, DefaultPrefetchCount)
90+
+
91+
+ buf := make([]byte, sliceSize)
92+
+ for off := int64(0); off < chunkSize*chunkCount; off += sliceSize {
93+
+ readerChunk.Store(off / chunkSize)
94+
+ if _, err := reader.ReadAt(buf, off); err != nil && off+sliceSize < chunkSize*chunkCount {
95+
+ t.Fatalf("ReadAt(%d): %v", off, err)
96+
+ }
97+
+ }
98+
+
99+
+ if got := maxLead.Load(); got > DefaultPrefetchCount {
100+
+ t.Fatalf("read-ahead reached %d chunks past the reader, want at most %d", got, DefaultPrefetchCount)
101+
+ }
102+
+}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
suite: seaweedfs image
2+
3+
# Every SeaweedFS component must run the in-tree build from images/seaweedfs,
4+
# which carries the patches in images/seaweedfs/patches. The vendored chart
5+
# falls back to chrislusf/seaweedfs at its appVersion when the image keys are
6+
# unset, and that is the unpatched upstream binary. The reference must carry a
7+
# digest, as every first-party reference does (docs/agents/image-refs.md).
8+
9+
templates:
10+
- charts/seaweedfs/templates/master/master-statefulset.yaml
11+
- charts/seaweedfs/templates/volume/volume-statefulset.yaml
12+
- charts/seaweedfs/templates/filer/filer-statefulset.yaml
13+
- charts/seaweedfs/templates/s3/s3-deployment.yaml
14+
# filer-statefulset includes it for its checksum annotation.
15+
- charts/seaweedfs/templates/s3/s3-secret.yaml
16+
17+
release:
18+
name: seaweedfs
19+
namespace: cozy-seaweedfs
20+
21+
tests:
22+
- it: runs the in-tree image in the master
23+
template: charts/seaweedfs/templates/master/master-statefulset.yaml
24+
asserts: &inTreeImage
25+
- matchRegex:
26+
path: spec.template.spec.containers[0].image
27+
pattern: ^ghcr\.io/cozystack/cozystack/seaweedfs:[^@]+@sha256:[0-9a-f]{64}$
28+
29+
- it: runs the in-tree image in the volume servers
30+
template: charts/seaweedfs/templates/volume/volume-statefulset.yaml
31+
asserts: *inTreeImage
32+
33+
- it: runs the in-tree image in the filer
34+
template: charts/seaweedfs/templates/filer/filer-statefulset.yaml
35+
asserts: *inTreeImage
36+
37+
- it: runs the in-tree image in the S3 gateway
38+
template: charts/seaweedfs/templates/s3/s3-deployment.yaml
39+
asserts: *inTreeImage

‎packages/system/seaweedfs/values.yaml‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ seaweedfs:
2525
# chart directly from its ExternalArtifact without re-rendering extra/seaweedfs.
2626
# The guard also refuses to render at all if this pin is ever lifted.
2727
fullnameOverride: seaweedfs
28+
# Built in-tree from images/seaweedfs, which carries the patches in
29+
# images/seaweedfs/patches. Without this block the vendored chart falls back
30+
# to chrislusf/seaweedfs at its appVersion, the unpatched upstream build.
31+
# `make image` rewrites both keys on every build.
32+
image:
33+
repository: "ghcr.io/cozystack/cozystack/seaweedfs"
34+
tag: "4.45@sha256:2a063deb9992ac0c06f8c09589ae063a30ac8c87af2c6aa1f4d4420cc544535c"
2835
master:
2936
# At the shipped replicationFactor of 2 the size limit and
3037
# extraEnvironmentVars.WEED_MASTER_VOLUME_GROWTH_COPY_2 below are one

0 commit comments

Comments
 (0)