Skip to content

Commit de34805

Browse files
committed
add feature flag
1 parent b419e47 commit de34805

4 files changed

Lines changed: 303 additions & 3 deletions

File tree

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2013 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
package com.netflix.priam.aws;
18+
19+
import com.netflix.priam.utils.SystemUtils;
20+
21+
/** Class for holding part data of a backup file, which will be used for multi-part uploading */
22+
public class DataPart {
23+
private final String bucketName;
24+
private final String uploadID;
25+
private final String s3key;
26+
private int partNo;
27+
private byte[] partData;
28+
private byte[] md5;
29+
30+
public DataPart(String bucket, String s3key, String mUploadId) {
31+
this.bucketName = bucket;
32+
this.uploadID = mUploadId;
33+
this.s3key = s3key;
34+
}
35+
36+
public DataPart(int partNumber, byte[] data, String bucket, String s3key, String mUploadId) {
37+
this(bucket, s3key, mUploadId);
38+
this.partNo = partNumber;
39+
this.partData = data;
40+
this.md5 = SystemUtils.md5(data);
41+
}
42+
43+
public String getBucketName() {
44+
return bucketName;
45+
}
46+
47+
public String getUploadID() {
48+
return uploadID;
49+
}
50+
51+
public String getS3key() {
52+
return s3key;
53+
}
54+
55+
public int getPartNo() {
56+
return partNo;
57+
}
58+
59+
public byte[] getPartData() {
60+
return partData;
61+
}
62+
63+
public byte[] getMd5() {
64+
return md5;
65+
}
66+
}

priam/src/main/java/com/netflix/priam/aws/S3FileSystem.java

Lines changed: 126 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,12 +131,21 @@ private ObjectMetadata getObjectMetadata(File file) {
131131

132132
private long uploadMultipart(AbstractBackupPath path, Instant target)
133133
throws BackupRestoreException {
134+
if (config.useReusableBufferForMultipartUploads()) {
135+
return uploadMultipartWithBuffers(path, target);
136+
} else {
137+
return uploadMultipartLegacy(path, target);
138+
}
139+
}
140+
141+
private long uploadMultipartWithBuffers(AbstractBackupPath path, Instant target)
142+
throws BackupRestoreException {
134143
Path localPath = Paths.get(path.getBackupFile().getAbsolutePath());
135144
String remotePath = path.getRemotePath();
136145
long chunkSize = getChunkSize(localPath);
137146
String prefix = config.getBackupPrefix();
138147
if (logger.isDebugEnabled())
139-
logger.debug("Uploading to {}/{} with chunk size {}", prefix, remotePath, chunkSize);
148+
logger.debug("Uploading to {}/{} with chunk size {} (using ByteBuffer implementation)", prefix, remotePath, chunkSize);
140149
File localFile = localPath.toFile();
141150
long fileSize = localFile.length();
142151

@@ -244,13 +253,74 @@ public UploadPartResult retriableCall() {
244253
}
245254
}
246255

256+
private long uploadMultipartLegacy(AbstractBackupPath path, Instant target)
257+
throws BackupRestoreException {
258+
Path localPath = Paths.get(path.getBackupFile().getAbsolutePath());
259+
String remotePath = path.getRemotePath();
260+
long chunkSize = getChunkSize(localPath);
261+
String prefix = config.getBackupPrefix();
262+
if (logger.isDebugEnabled())
263+
logger.debug("Uploading to {}/{} with chunk size {} (using legacy implementation)", prefix, remotePath, chunkSize);
264+
File localFile = localPath.toFile();
265+
InitiateMultipartUploadRequest initRequest =
266+
new InitiateMultipartUploadRequest(prefix, remotePath)
267+
.withObjectMetadata(getObjectMetadata(localFile));
268+
String uploadId = s3Client.initiateMultipartUpload(initRequest).getUploadId();
269+
DataPart part = new DataPart(prefix, remotePath, uploadId);
270+
List<PartETag> partETags = Collections.synchronizedList(new ArrayList<>());
271+
272+
try (InputStream in = new FileInputStream(localFile)) {
273+
Iterator<byte[]> chunks = new ChunkedStream(in, chunkSize, path.getCompression());
274+
int partNum = 0;
275+
AtomicInteger partsPut = new AtomicInteger(0);
276+
long compressedFileSize = 0;
277+
278+
while (chunks.hasNext()) {
279+
byte[] chunk = chunks.next();
280+
rateLimiter.acquire(chunk.length);
281+
dynamicRateLimiter.acquire(path, target, chunk.length);
282+
DataPart dp = new DataPart(++partNum, chunk, prefix, remotePath, uploadId);
283+
S3PartUploader partUploader = new S3PartUploader(s3Client, dp, partETags, partsPut);
284+
compressedFileSize += chunk.length;
285+
executor.submit(partUploader);
286+
}
287+
288+
executor.sleepTillEmpty();
289+
logger.info("{} done. part count: {} expected: {}", localFile, partsPut.get(), partNum);
290+
Preconditions.checkState(partNum == partETags.size(), "part count mismatch");
291+
CompleteMultipartUploadResult resultS3MultiPartUploadComplete =
292+
new S3PartUploader(s3Client, part, partETags).completeUpload();
293+
checkSuccessfulUpload(resultS3MultiPartUploadComplete, localPath);
294+
295+
if (logger.isDebugEnabled()) {
296+
final S3ResponseMetadata info = s3Client.getCachedResponseMetadata(initRequest);
297+
logger.debug("Request Id: {}, Host Id: {}", info.getRequestId(), info.getHostId());
298+
}
299+
300+
return compressedFileSize;
301+
} catch (Exception e) {
302+
new S3PartUploader(s3Client, part, partETags).abortUpload();
303+
throw new BackupRestoreException("Error uploading file: " + localPath.toString(), e);
304+
}
305+
}
306+
247307
protected long uploadFileImpl(AbstractBackupPath path, Instant target)
248308
throws BackupRestoreException {
249309
File localFile = Paths.get(path.getBackupFile().getAbsolutePath()).toFile();
250310

251311
if (localFile.length() >= config.getBackupChunkSize())
252312
return uploadMultipart(path, target);
253313

314+
if (config.useReusableBufferForMultipartUploads()) {
315+
return uploadFileWithByteBuffer(path, target);
316+
} else {
317+
return uploadFileWithByteArray(path, target);
318+
}
319+
}
320+
321+
private long uploadFileWithByteBuffer(AbstractBackupPath path, Instant target)
322+
throws BackupRestoreException {
323+
File localFile = Paths.get(path.getBackupFile().getAbsolutePath()).toFile();
254324
ByteBuffer buffer = getFileByteBuffer(path);
255325
// C* snapshots may have empty files. That is probably unintentional.
256326
if (buffer.remaining() > 0) {
@@ -261,7 +331,7 @@ protected long uploadFileImpl(AbstractBackupPath path, Instant target)
261331
new BoundedExponentialRetryCallable<PutObjectResult>(1000, 10000, 5) {
262332
@Override
263333
public PutObjectResult retriableCall() {
264-
return s3Client.putObject(generatePut(path, buffer));
334+
return s3Client.putObject(generatePutFromBuffer(path, buffer));
265335
}
266336
}.call();
267337
} catch (Exception e) {
@@ -270,7 +340,29 @@ public PutObjectResult retriableCall() {
270340
return buffer.remaining();
271341
}
272342

273-
private PutObjectRequest generatePut(AbstractBackupPath path, ByteBuffer buffer) {
343+
private long uploadFileWithByteArray(AbstractBackupPath path, Instant target)
344+
throws BackupRestoreException {
345+
File localFile = Paths.get(path.getBackupFile().getAbsolutePath()).toFile();
346+
byte[] chunk = getFileContents(path);
347+
// C* snapshots may have empty files. That is probably unintentional.
348+
if (chunk.length > 0) {
349+
rateLimiter.acquire(chunk.length);
350+
dynamicRateLimiter.acquire(path, target, chunk.length);
351+
}
352+
try {
353+
new BoundedExponentialRetryCallable<PutObjectResult>(1000, 10000, 5) {
354+
@Override
355+
public PutObjectResult retriableCall() {
356+
return s3Client.putObject(generatePutFromByteArray(path, chunk));
357+
}
358+
}.call();
359+
} catch (Exception e) {
360+
throw new BackupRestoreException("Error uploading file: " + localFile.getName(), e);
361+
}
362+
return chunk.length;
363+
}
364+
365+
private PutObjectRequest generatePutFromBuffer(AbstractBackupPath path, ByteBuffer buffer) {
274366
File localFile = Paths.get(path.getBackupFile().getAbsolutePath()).toFile();
275367
ObjectMetadata metadata = getObjectMetadata(localFile);
276368
metadata.setContentLength(buffer.remaining());
@@ -286,6 +378,22 @@ private PutObjectRequest generatePut(AbstractBackupPath path, ByteBuffer buffer)
286378
return put;
287379
}
288380

381+
private PutObjectRequest generatePutFromByteArray(AbstractBackupPath path, byte[] chunk) {
382+
File localFile = Paths.get(path.getBackupFile().getAbsolutePath()).toFile();
383+
ObjectMetadata metadata = getObjectMetadata(localFile);
384+
metadata.setContentLength(chunk.length);
385+
PutObjectRequest put =
386+
new PutObjectRequest(
387+
config.getBackupPrefix(),
388+
path.getRemotePath(),
389+
new ByteArrayInputStream(chunk),
390+
metadata);
391+
if (config.addMD5ToBackupUploads()) {
392+
put.getMetadata().setContentMD5(SystemUtils.toBase64(SystemUtils.md5(chunk)));
393+
}
394+
return put;
395+
}
396+
289397
@VisibleForTesting
290398
public ByteBuffer getFileByteBuffer(AbstractBackupPath path) throws BackupRestoreException {
291399
File localFile = Paths.get(path.getBackupFile().getAbsolutePath()).toFile();
@@ -314,4 +422,19 @@ public ByteBuffer getFileByteBuffer(AbstractBackupPath path) throws BackupRestor
314422
throw new BackupRestoreException("Error reading file: " + localFile.getName(), e);
315423
}
316424
}
425+
426+
private byte[] getFileContents(AbstractBackupPath path) throws BackupRestoreException {
427+
File localFile = Paths.get(path.getBackupFile().getAbsolutePath()).toFile();
428+
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
429+
InputStream in = new BufferedInputStream(new FileInputStream(localFile))) {
430+
Iterator<byte[]> chunks =
431+
new ChunkedStream(in, config.getBackupChunkSize(), path.getCompression());
432+
while (chunks.hasNext()) {
433+
byteArrayOutputStream.write(chunks.next());
434+
}
435+
return byteArrayOutputStream.toByteArray();
436+
} catch (Exception e) {
437+
throw new BackupRestoreException("Error reading file: " + localFile.getName(), e);
438+
}
439+
}
317440
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Copyright 2013 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
package com.netflix.priam.aws;
18+
19+
import com.amazonaws.AmazonClientException;
20+
import com.amazonaws.services.s3.AmazonS3;
21+
import com.amazonaws.services.s3.model.*;
22+
import com.netflix.priam.backup.BackupRestoreException;
23+
import com.netflix.priam.utils.BoundedExponentialRetryCallable;
24+
import com.netflix.priam.utils.SystemUtils;
25+
import java.io.ByteArrayInputStream;
26+
import java.util.List;
27+
import java.util.concurrent.atomic.AtomicInteger;
28+
import org.slf4j.Logger;
29+
import org.slf4j.LoggerFactory;
30+
31+
public class S3PartUploader extends BoundedExponentialRetryCallable<Void> {
32+
private final AmazonS3 client;
33+
private final DataPart dataPart;
34+
private final List<PartETag> partETags;
35+
private AtomicInteger partsUploaded = null; // num of data parts successfully uploaded
36+
37+
private static final Logger logger = LoggerFactory.getLogger(S3PartUploader.class);
38+
private static final int MAX_RETRIES = 5;
39+
private static final int DEFAULT_MIN_SLEEP_MS = 200;
40+
41+
public S3PartUploader(AmazonS3 client, DataPart dp, List<PartETag> partETags) {
42+
super(DEFAULT_MIN_SLEEP_MS, BoundedExponentialRetryCallable.MAX_SLEEP, MAX_RETRIES);
43+
this.client = client;
44+
this.dataPart = dp;
45+
this.partETags = partETags;
46+
}
47+
48+
public S3PartUploader(
49+
AmazonS3 client, DataPart dp, List<PartETag> partETags, AtomicInteger partsUploaded) {
50+
super(DEFAULT_MIN_SLEEP_MS, BoundedExponentialRetryCallable.MAX_SLEEP, MAX_RETRIES);
51+
this.client = client;
52+
this.dataPart = dp;
53+
this.partETags = partETags;
54+
this.partsUploaded = partsUploaded;
55+
}
56+
57+
private Void uploadPart() throws AmazonClientException, BackupRestoreException {
58+
UploadPartRequest req = new UploadPartRequest();
59+
req.setBucketName(dataPart.getBucketName());
60+
req.setKey(dataPart.getS3key());
61+
req.setUploadId(dataPart.getUploadID());
62+
req.setPartNumber(dataPart.getPartNo());
63+
req.setPartSize(dataPart.getPartData().length);
64+
req.setMd5Digest(SystemUtils.toBase64(dataPart.getMd5()));
65+
req.setInputStream(new ByteArrayInputStream(dataPart.getPartData()));
66+
UploadPartResult res = client.uploadPart(req);
67+
PartETag partETag = res.getPartETag();
68+
if (!partETag.getETag().equals(SystemUtils.toHex(dataPart.getMd5())))
69+
throw new BackupRestoreException(
70+
"Unable to match MD5 for part " + dataPart.getPartNo());
71+
partETags.add(partETag);
72+
if (this.partsUploaded != null) this.partsUploaded.incrementAndGet();
73+
return null;
74+
}
75+
76+
public CompleteMultipartUploadResult completeUpload() throws BackupRestoreException {
77+
CompleteMultipartUploadRequest compRequest =
78+
new CompleteMultipartUploadRequest(
79+
dataPart.getBucketName(),
80+
dataPart.getS3key(),
81+
dataPart.getUploadID(),
82+
partETags);
83+
return client.completeMultipartUpload(compRequest);
84+
}
85+
86+
// Abort
87+
public void abortUpload() {
88+
AbortMultipartUploadRequest abortRequest =
89+
new AbortMultipartUploadRequest(
90+
dataPart.getBucketName(), dataPart.getS3key(), dataPart.getUploadID());
91+
client.abortMultipartUpload(abortRequest);
92+
}
93+
94+
@Override
95+
public Void retriableCall() throws AmazonClientException, BackupRestoreException {
96+
logger.debug(
97+
"Picked up part {} size {}", dataPart.getPartNo(), dataPart.getPartData().length);
98+
return uploadPart();
99+
}
100+
}

priam/src/main/java/com/netflix/priam/config/IConfiguration.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1138,4 +1138,15 @@ default int getBlockForPeersTimeoutInSecs() {
11381138
default boolean skipMetaFileValidationOnRestore() {
11391139
return false;
11401140
}
1141+
1142+
/**
1143+
* Use reusable ByteBuffer implementation for S3 multipart uploads.
1144+
* When true, uses thread-local ByteBuffers to reduce memory allocation overhead.
1145+
* When false, uses the legacy byte array based implementation.
1146+
*
1147+
* @return true to use reusable ByteBuffer implementation, false for legacy implementation
1148+
*/
1149+
default boolean useReusableBufferForMultipartUploads() {
1150+
return false;
1151+
}
11411152
}

0 commit comments

Comments
 (0)