Téléchargement en plusieurs parties sur S3 avec jclouds

1. Objectif

Dans le lien:/2013/04/01/upload-on-s3-with-jclouds/[le précédent article sur le téléchargement de S3], nous avons examiné comment utiliser les API génériques Blob de jclouds pour télécharger du contenu vers S3. Dans cet article, nous utiliserons l’API asynchrone spécifique à S3 de jclouds ** pour télécharger du contenu et exploiter la fonctionnalité d’envoi en plusieurs parties http://aws.typepad.com/aws/2010/11/amazon-s3-multipart-upload.html fourni par S3].

2. Préparation

2.1. Configurer l’API personnalisée

La première partie du processus de téléchargement consiste à créer l’API jclouds. Il s’agit d’une API personnalisée pour Amazon S3:

public AWSS3AsyncClient s3AsyncClient() {
   String identity = ...
   String credentials = ...

   BlobStoreContext context = ContextBuilder.newBuilder("aws-s3").
      credentials(identity, credentials).buildView(BlobStoreContext.class);

   RestContext<AWSS3Client, AWSS3AsyncClient> providerContext = context.unwrap();
   return providerContext.getAsyncApi();
}

2.2. Détermination du nombre de pièces pour le contenu

Amazon S3 a une limite de 5 Mo pour chaque partie à télécharger. En tant que tel, la première chose à faire est de déterminer le nombre exact de parties dans lesquelles nous pouvons diviser notre contenu, de manière à ne pas avoir de parties inférieures à cette limite de 5 Mo:

public static int getMaximumNumberOfParts(byte[]byteArray) {
   int numberOfParts= byteArray.length/fiveMB;//5** 1024** 1024
   if (numberOfParts== 0) {
      return 1;
   }
   return numberOfParts;
}

2.3. Briser le contenu en plusieurs parties

Allaient diviser le tableau d’octets en un nombre défini de parties:

public static List<byte[]> breakByteArrayIntoParts(byte[]byteArray, int maxNumberOfParts) {
   List<byte[]> parts = Lists.<byte[]> newArrayListWithCapacity(maxNumberOfParts);
   int fullSize = byteArray.length;
   long dimensionOfPart = fullSize/maxNumberOfParts;
   for (int i = 0; i < maxNumberOfParts; i++) {
      int previousSplitPoint = (int) (dimensionOfPart **  i);
      int splitPoint = (int) (dimensionOfPart **  (i + 1));
      if (i == (maxNumberOfParts - 1)) {
         splitPoint = fullSize;
      }
      byte[]partBytes = Arrays.copyOfRange(byteArray, previousSplitPoint, splitPoint);
      parts.add(partBytes);
   }

   return parts;
}

Nous allons tester la logique de fragmentation du tableau d’octets - nous allons générer des octets, scinder le tableau d’octets, le recomposer en utilisant Guava et vérifier que nous récupérons l’original:

@Test
public void given16MByteArray__whenFileBytesAreSplitInto3__thenTheSplitIsCorrect() {
   byte[]byteArray = randomByteData(16);

   int maximumNumberOfParts = S3Util.getMaximumNumberOfParts(byteArray);
   List<byte[]> fileParts = S3Util.breakByteArrayIntoParts(byteArray, maximumNumberOfParts);

   assertThat(fileParts.get(0).length + fileParts.get(1).length + fileParts.get(2).length,
      equalTo(byteArray.length));
   byte[]unmultiplexed = Bytes.concat(fileParts.get(0), fileParts.get(1), fileParts.get(2));
   assertThat(byteArray, equalTo(unmultiplexed));
}

Pour générer les données, nous utilisons simplement le support de Random :

byte[]randomByteData(int mb) {
   byte[]randomBytes = new byte[mb **  1024 **  1024];
   new Random().nextBytes(randomBytes);
   return randomBytes;
}

** 2.4. Création des données utiles

**

Maintenant que nous avons déterminé le nombre correct de parties pour notre contenu et que nous avons réussi à diviser le contenu en parties, nous devons générer les objets Payload pour l’API jclouds:

public static List<Payload> createPayloadsOutOfParts(Iterable<byte[]> fileParts) {
   List<Payload> payloads = Lists.newArrayList();
   for (byte[]filePart : fileParts) {
      byte[]partMd5Bytes = Hashing.md5().hashBytes(filePart).asBytes();
      Payload partPayload = Payloads.newByteArrayPayload(filePart);
      partPayload.getContentMetadata().setContentLength((long) filePart.length);
      partPayload.getContentMetadata().setContentMD5(partMd5Bytes);
      payloads.add(partPayload);
   }
   return payloads;
}

3. Télécharger

Le processus de téléchargement est un processus flexible en plusieurs étapes - cela signifie:

  • le téléchargement peut être démarré avant d’avoir toutes les données - les données peuvent être

téléchargé comme il arrive les données sont téléchargées en morceaux ** - si l’une de ces opérations échoue,

peut simplement être récupéré des morceaux peuvent être téléchargés en parallèle ** - cela peut considérablement augmenter la

vitesse de téléchargement, surtout dans le cas de gros fichiers

3.1. Lancer l’opération de téléchargement

La première étape de l’opération de téléchargement consiste à initier le processus .

Cette demande à S3 doit contenir les en-têtes HTTP standard, notamment l’en-tête Content MD5 doit être calculée. Allaient utiliser le support de la fonction de hachage Guava ici:

Hashing.md5().hashBytes(byteArray).asBytes();

Ceci est le md5 hash du tableau d’octets entier, pas encore des parties

Pour lancer le téléchargement et pour toutes les interactions ultérieures avec S3, nous allons utiliser AWSS3AsyncClient - l’API asynchrone que nous avons créée précédemment:

ObjectMetadata metadata = ObjectMetadataBuilder.create().key(key).contentMD5(md5Bytes).build();
String uploadId = s3AsyncApi.initiateMultipartUpload(container, metadata).get();

La clé est le descripteur attribué à l’objet. Il doit s’agir d’un identificateur unique spécifié par le client.

Notez également que, même si nous utilisons la version asynchrone de l’API, nous bloquons le résultat de cette opération - c’est que nous aurons besoin du résultat de l’initialisation pour pouvoir avancer.

Le résultat de l’opération est un ID de téléchargement renvoyé par S3 - cela identifiera le téléchargement tout au long de son cycle de vie et sera présent dans toutes les opérations de téléchargement ultérieures.

3.2. Téléchargement des pièces

La prochaine étape est de télécharger les pièces . Notre objectif ici est d’envoyer ces demandes en parallèle , l’opération de téléchargement de pièces représentant l’essentiel du processus de téléchargement:

List<ListenableFuture<String>> ongoingOperations = Lists.newArrayList();
for (int partNumber = 0; partNumber < filePartsAsByteArrays.size(); partNumber++) {
   ListenableFuture<String> future = s3AsyncApi.uploadPart(
      container, key, partNumber + 1, uploadId, payloads.get(partNumber));
   ongoingOperations.add(future);
}

Les numéros de pièce doivent être continus mais l’ordre dans lequel les demandes sont envoyées n’est pas pertinent.

Une fois que toutes les demandes de pièces de téléchargement ont été soumises, nous devons attendre leurs réponses afin de pouvoir collecter la valeur ETag individuelle de chaque partie:

Function<ListenableFuture<String>, String> getEtagFromOp =
  new Function<ListenableFuture<String>, String>() {
   public String apply(ListenableFuture<String> ongoingOperation) {
      try {
         return ongoingOperation.get();
      } catch (InterruptedException | ExecutionException e) {
         throw new IllegalStateException(e);
      }
   }
};
List<String> etagsOfParts = Lists.transform(ongoingOperations, getEtagFromOp);

Si, pour une raison quelconque, l’une des opérations de la partie de téléchargement échoue, l’opération peut être retentée jusqu’à ce qu’elle aboutisse. La logique ci-dessus ne contient pas le mécanisme de nouvelle tentative, mais sa construction devrait être assez simple.

3.3. Terminer l’opération de téléchargement

La dernière étape du processus de téléchargement consiste à terminer l’opération en plusieurs parties . L’API S3 nécessite les réponses des précédentes pièces chargées en tant que Map , que nous pouvons maintenant créer facilement à partir de la liste d’ETags que nous avons obtenue ci-dessus:

Map<Integer, String> parts = Maps.newHashMap();
for (int i = 0; i < etagsOfParts.size(); i++) {
   parts.put(i + 1, etagsOfParts.get(i));
}

Et enfin, envoyez la demande complète:

s3AsyncApi.completeMultipartUpload(container, key, uploadId, parts).get();

Cela retournera l’ETag final de l’objet fini et complétera le processus de téléchargement complet.

4. Conclusion

Dans cet article, nous avons créé une opération de téléchargement entièrement parallèle vers S3, activée par plusieurs parties, à l’aide de l’API jclouds personnalisée de S3. Cette opération est prête à être utilisée telle quelle, mais elle peut être améliorée de plusieurs manières.

Tout d’abord, la nouvelle tentative doit être ajoutée autour des opérations de téléchargement pour mieux gérer les échecs.

Ensuite, pour les fichiers très volumineux, même si le mécanisme envoie toutes les demandes de téléchargement multipart en parallèle, un mécanisme de limitation devrait toujours limiter le nombre de demandes parallèles envoyées. Cela permet à la fois d’éviter que la bande passante ne devienne un goulet d’étranglement et de s’assurer qu’Amazon ne déclare pas le processus de téléchargement comme dépassant la limite autorisée de demandes par seconde ( Guava RateLimiter peut potentiellement très bien convenir à cela.