PCG logo
Artikel

Große Dateien von Cloud Storage zu Google Drive übertragen

Die Dienste Google Drive (Drive) und Google Cloud Storage (GCS) sind beide ein Produkt des Google Cloud Portfolios. Man sollte denken, ein Transfer von Daten zwischen den beiden Diensten innerhalb von Google lässt sich problemlos realisieren. Falsch gedacht, ganz so einfach ist es nicht. In diesem Blogpost möchte ich daher ein Cloud-Native Konzept für Google Drive Resumable Uploads in Kombination mit Google Cloud Storage und Google Cloud Functions in NodeJS vorstellen.

Herausforderung

Google Cloud Storage ist ein Dienst der Google Cloud Platform, bestimmt für maschinellen Zugriff, wohingegen Google Drive ein Dienst von Google Workspace ist, der von Menschen genutzt wird. Dies ist vermutlich der Grund der fehlenden Integration zueinander. Während sich Dateien mit einem simplen Aufruf zwischen BucketsExternal Link oder innerhalb DriveExternal Link hin-und-her kopieren lassen, existiert kein Google eigener Service zum (internen) kopieren oder übertragen von Dateien. Dienste wie Google Cloud Storage Transfer Service oder Dataflow besitzen viele Connectoren, aber keinen Connector für Google Drive.

Um Dateien zu übertragen, müssen Objekte immer von GCS heruntergeladen und dann wieder auf Drive hochgeladen werden. Im Worst-Case verlassen sie dazu das Google Netzwerk, um dann direkt wieder dorthin geschickt zu werden.

In einem konkreten Fall ging es aber darum, Dateien mit bis zu 10 GiB zu verschieben, um den Vorteil des im Google Workspace Enterprise Tarifs enthaltenen unbegrenzten Google Drive Speicherplatz zu nutzen.

Damit fingen die Herausforderungen an. Einfache Beispiele und Codeschnipsel hierzu lassen sich schnell googeln, jedoch findet man kaum konkrete Implementierungen oder Hilfestellungen für große Dateien.

Lösung: Der Cloud-Native Ansatz

Obwohl es mehrere Tools oder Scripte gibt, die den obigen Ansatz bereits befolgen, benötigt man fast immer umfangreiche Ressourcen, abhängig von der maximalen Datenmenge. Trotzdem hat jede Laufzeitumgebung Limitierungen. Dies kann die Rechenleistung, die Bandbreite oder die Menge des verfügbaren Speichers sein, sodass eine Skalierung benötigt wird.

Die Idee einer Cloud-Native Anwendung “ist ein verteiltes, beobachtbares, elastisches und auf horizontale Skalierbarkeit optimiertes Service-of-Services System, das seinen Zustand in (einem Minimum an) zustandsbehafteten Komponenten isoliert.” [WikipediaExternal Link]

Obwohl die Skalierung ein Bestandteil des Cloud-NativeExternal Link Konzepts ist, hat man aber immer noch das Problem dieser Limitierungen, denn ohne Anpassung des Übertragungskonzepts benötigt man die Skalierung einer einzelnen Instanz (vertikal) und nicht die des gesamten Systems (horizontal). Das Konzept der Microservices sieht gerade nicht vor, dass man vertikal skaliert, bis es nicht mehr weitergeht, sondern eine Aufgabe in den kleinstmöglichen Schritten mit den kleinstmöglichen Ressourcen ausführt.

Egal wie weit man vertikal skalierte, entweder ist die zu übertragende Datei zu groß, der Speicherplatz reicht nicht aus oder die Bandbreite ist zu gering.

Bei Serverless-Umgebungen wie Apps Script, Cloud Function oder Cloud Run existiert zudem kein persistenter Speicher, sondern das Dateisystem liegt im Arbeitsspeicher, wodurch Dateien nie größer sein können als der Arbeitsspeicher.

Lösungsideen

Der erste Gedanke ist nun: Führe das Script auf einer Compute Engine aus, die bspw. 4 CPUs und 16 GB Ram hat. Dazu noch eine 1TB große Disk und schon ist das Problem nicht mehr da. Ist das Cloud-Native? Nein!

Die Prämisse muss sein, dass die Lösung serverless in der Cloud (also nah an der Quelle und am Ziel) betrieben wird und keine völlig überdimensionierten Ressourcen braucht, die ständig gemanagt und upgedatet werden müssen.

Cloud Functions

Da es sich um einen simplen Use-Case mit simplen Schritten handelt und die Leistung nur selten benötigt wird, ist die Entscheidung gefallen, Cloud Functions als Runtime zu nutzen und deren Limitierungen auszureizen.

Warum; Cloud Functions laufen nur wenn ich sie brauche, sie sind günstig, sind komplett gemanagt, sind ein Cloud-Native Dienst, lassen sich leicht aufrufen, bringen integrierte Authentifizierung usw.

Aber; Cloud Functions haben Limitierungen. Dies liegt aber nicht an der Dateigröße, die wir übertragen wollen, sondern an der Architektur an sich!

Wie kann ich aber belieb große Dateien mit Hilfe eines stark limitierten Systems übertragen?

Byte-Ranges und Resumable Uploads

Warum die gesamte Datei auf einmal herunterladen und dann wieder hochladen? Gibt es da nicht schon Lösungen? Doch. In beiden Diensten bzw. APIs gibt es Möglichkeiten, mit Datei-Abschnitten zu arbeiten. Damit lässt sich zwar nicht die Gesamtgröße der Datei oder die Gesamtlaufzeit beeinflussen, aber es lässt sich zielsicher die maximale Größe oder sogar die max. Laufzeit pro Lauf steuern und das ist es, was wir für die Nutzung der Cloud Function benötigen.

Google Cloud Storage API

Die Google Cloud Storage API bietet die Möglichkeit, nur bestimmte “Byte-RangesExternal Link” eines Objektes abzurufen. Damit kann ein Objekt häppchenweise abgerufen und gespeichert werden.

javascript
Code kopiert!copy-button
asyncfunctiongcs_download_file(bucketName, fileName, targetFile, startByte, endByte) {
await gcs_init_client();

asyncfunctiondownloadFile() {
const options = {
destination: targetFile,
start: startByte,
end: endByte,
};

// Downloads the file
let content = await storage.bucket(bucketName).file(fileName).download(options);

console.log(
`gs://${bucketName}/${fileName} downloaded to ${targetFile}.`
);

return content;
}

var destFileName = await downloadFile().catch(console.error);
return destFileName;
}

Google Drive API

In der Google Drive REST API habe ich mir die Funktion “Resumable UploadsExternal Link” zu Nutze gemacht. Beim fortsetzbaren Upload können Uploads einer Datei in mehreren Schritten durchgeführt werden. Hierzu wird in einem ersten “leeren” Upload-Aufruf eine “Session” gestartet. Dieser Aufruf gibt eine “Upload-URL” für die Vervollständigung des begonnenen Uploads zurück. Diese URL kann solange mit Daten “bepumpt” werden, bis der Upload vollständig ist. Die Upload URL ist 7 Tage gültig, somit kann man sich 7 Tage Zeit lassen, bis man die Datei vollständig hochgeladen hat.

Leider funktioniert der Upload nur sequentiell. Man kann also nicht parallel mehrere Datei-Abschnitte hochladen.

1. Initialisierung der Upload Session

javascript
Code kopiert!copy-button
async function drive_upload_resumable_init(fileName, folderId, totalSize) {
let result = await client.request(
{
method: "POST",
url:
"https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable&supportsAllDrives=true",
headers: {
"Content-Type": "application/json",
"X-Upload-Content-Length": totalSize
},
body: JSON.stringify({
name: fileName, parents: [folderId],
supportsTeamDrives: true,
})
});

return result.headers.location;
}

2. Upload der Datei-Abschnitte

javascript
Code kopiert!copy-button
asyncfunctiondrive_upload_resumable(url, sourceFile, size, startByte, endByte) {
var result = null;

try {
result = await client.request(
{
method: "PUT",
url: url,
headers: { "Content-Range": `bytes ${startByte}-${endByte}/${size}` },
body: fse.readFileSync(sourceFile)
}
)
} catch (e) {
if (e.response && e.response.status == 308) {
console.log("Status:", e.response.status, e.response.statusText);
return e.response;
} else {
console.error("Catched error", e);
}
}
console.log("Status:", result.status, result.statusText);
return result.data;
}

Cloud-Native Architektur

Basierend auf diesen Informationen habe ich folgendes simple Cloud-Native Architekturkonzept erarbeitet.

  • Definiere eine Chunk Größe
  • Ermittle die Dateigröße des Objekts in Cloud Storage
  • Berechne die Anzahl der nötigen Chunks und die Ranges.
  • In einer Schleife, solange HTTP Status 308 empfangen wird:
    • Lade die Daten einer Byte-Range in eine temporäre Datei herunter.
    • Lade die temporäre Datei per Google Drive Resumable Upload hoch.
    • Lösche die temporäre Datei
  • Schleife Ende, wenn HTTP Status 200 empfangen wurde.
  • Lösche die Quelldatei (optional)

Ziel dabei ist, dass die Größe des aktuellen Abschnitts die Speichergröße der Cloud Function nicht überschreitet.

Erfahrungen

Theorie und Praxis liegen jedoch bekanntlich weit auseinander, weswegen ich hier gerne auch meine Erfahrungen mit der Lösungsfindung und typischen Fehlern teilen möchte. Eine genaue Lektüre der Dokumentationen empfehle ich ebenfalls.

Das wichtigste vorweg: Es funktioniert sehr exakt, wenn man es nur richtig macht. Jeder Fehler ist daher ein Hinweis darauf, dass man etwas nicht richtig gemacht hat und nicht dafür, dass die Google APIs nicht richtig funktionieren.

HTTP Status 308

Der Fehlerstatus 308 ist unser Freund und eigentlich kein Fehler. Er besagt, dass der Teil-Upload erfolgreich, der gesamte Upload aber noch nicht abgeschlossen ist. Für unseren Client ist eine Rückgabe jedoch ein Fehler, welcher eine Exception wirft. Diese muss daher gefangen und der 308er Status in der Exception verarbeitet werden.

Invalid Range

Hat man vermeintlich 100000 Bytes hochgeladen, aber der folgende Request liefert folgende Header:

Request Header

‚Content-Range‘: ‚bytes 0-1999999/5528489‘

Response Header

range: ‚bytes=0-1835007‘

Dann hat man entweder weniger Bytes übertragen oder es wurden weniger Bytes verarbeitet, denn die Anzahl der verarbeiteten Bytes ist im Response Header ersichtlich.

In diesem Fall würde der nächste Request folgenden Fehler liefern:

Invalid request. According to the Content-Range header, the upload offset is 2000000 byte(s), which exceeds already uploaded size of 1835008 byte(s)

Obwohl die temporäre Quelldatei exakt 200000 Bytes hatte und die Range mit 0-1999999 angegeben war, hat die Drive API nur 1835008 verarbeitet.

Man könnte nun – wie ich – auf die Idee kommen, die Start-Range auf den Endwert der Rückgabe anzupassen, damit der Upload die nächste Range akzeptiert. Dies funktioniert wunderbar. Man hat am Ende dann halt eine korrupte Datei, da die nicht verarbeiteten Bytes einfach fehlen. Je nach Dateiformat fällt das dann früher oder später auf.

Korrekt ist es, konsequent mit den errechneten Byte-Offsets zu arbeiten. Wenn man einen Fehler erhält, dann weil man irgendwo was falsch gemacht hat. Im obigen Fall spätestens beim nächsten Chunk.

Die Ursache hierfür, hatte ich einfach überlesen. Es ist sehr wichtig, daß die Upload Chunk Größe ein Vielfaches von 256kb ist, also 1024 * 256 * X, denn ansonsten schneidet die Drive API einfach die Bytes darüber ab und es kommt zum genannten Problem. Die GCS API kann hingegen jede beliebige Range zurückgeben.

Failed to parse Content-Range header

Gibt der letzte Upload einen “Failed to parse Content-Range header.” Fehler zurück, dann liegt dies daran, dass das Ende der Range denselben Wert hat wie die Größe.

Bspw. ‚Content-Range‘: ‚bytes 0-262144/262144’

Hier muss man zwischen der Anzahl der Bytes (beginnen bei 1) und der Range von 0 bis X unterscheiden.

Bei 10 Bytes sind die Range 0-9 und die Größe 10, man muss also den Header wie folgt definieren. ‚Content-Range‘: ‚bytes 0-9/10’

Nachdem ich diese Probleme identifiziert und behoben hatte, konnte ich mit den berechneten Byte-Offsets arbeiten, ohne diese bei jedem Loop neu zu verifizieren oder zu berechnen.

Erkenntnis

Wenn ich aus GCS 1.048.576 Bytes herunterlade, dann hat die temporäre Datei auch 1.048.576 Bytes und wenn ich diese 1.048.576 Bytes nach Drive hochladen, dann werden auch 1.048.576 Bytes verarbeitet.

Verifizierung

Beide Dienste stellen Checksummen in den Metadaten bereit, ohne dass man die Datei herunterladen muss. So lassen sich die Dateien nach dem Transfer exakt vergleichen. Einen Wert, den beide Dienste nutzen, ist der MD5 Hash.

Die Google Cloud Storage API gibt den Hash Base64 formatiert zurück, während die Drive API einen HEX Wert ausgibt. Damit man beide Werte vergleichen kann, kann man den GCS Hash wie folgt umrechnen

javascript
Code kopiert!copy-button
let md5Hash = Buffer.from(object_metadata.md5Hash, 'base64').toString("hex");

Performance und Limits

Die Laufzeitbegrenzung einer Cloud Function liegt bei 9 Minuten (1. Generation) bzw. 60 Minuten (2. Generation. Die in dieser Zeit übertragbaren Daten sind unser erstes Limit.

Die Menge des Speicherplatzes bei einer Cloud Funktion lässt sich auf 8 GB (1. Generation) oder bis zu 32 GB (2. Generation) festlegen. Jedoch wird dieser Speicher dann immer allokiert, egal ob die Datei 200kb oder 20GB groß sein wird. Der verfügbare Speicher ist unser zweites Limit.

Die Bandbreite der Cloud Function ist leider nicht sehr hoch, weswegen ich geprüft habe, welche Übertragungsraten eine Cloud Function erreichen kann.

Dafür wurde die Übertragung einer 10 GiB Datei in 500MiB Chunks getestet. Die Übertragung besteht aus dem Download und dem folgenden Upload. Tatsächlich wird also die doppelte Datenmenge über das Netzwerk geschickt.

1. Generation vs. 2. Generation

Die Google Cloud Function gibt es in zwei Generationen, eine native 1. Version und eine auf Cloud Run basierende 2. Version.

Eine Cloud Function der ersten Generation mit 4 GB Speicher schafft etwa 1 GiB pro Minute. Die 10 GB Datei ließ sich also auf diesem Wege nicht zuverlässig übertragen, da die Cloud Function maximal 9 Minuten läuft. Für kleinere Dateien bis ca. 7 GiB war die Übertragung erfolgreich.

Die zweite Generation der Cloud Functions basiert auf Cloud Run. Der Vorteil ist, dass das Zeitlimit, analog zu Cloud Run, 60 Minuten ist und dass man bis zu 32 GB Speicher zuweisen kann und auch bis zu 8 CPUs. Bei Compute Ressourcen erhöht sich der NetzwerkdurchsatzExternal Link mit der Leistung des Systems, so daß hier Hoffnung auf eine schnellere Übertragungsgeschwindigkeit aufkeimte. Eine solche Skalierung konnte ich bei den Cloud Functions aber leider nicht rekonstruieren. Egal ob mit 1 oder 8 CPUs, die Übertragungsrate liegt ebenfalls bei etwa 1 GiB/Minute.

Unsere 10 GiB Datei konnte mit einer Cloud Function der 2. Generation erfolgreich übertragen werden.

Die rechnerische maximalgröße für die Übertragung ist 60 Minuten * 1 GiB = 60 GiB pro Lauf.

Kosten

Da es sich beim Upload zu Drive um Google internen Traffic handelt zahlt man nur den Egress aus GCS (Download) die Laufzeit der Cloud Function 🙂

Ausblick – Even more Cloud-Native

Will man die Serverless-Architektur beibehalten, die mit den verfügbaren Limits auskommt, dann muss man selbst die Chunks auf separate Ausführungen auslagern. Denkbar wäre hier eine Cloud Function welche entweder die Anzahl der nötigen Chunks und deren Offsets ermittelt und anschließend

  1. Cloud Tasks angelegt, die dann nacheinander ausgeführt werden. Wichtig ist, daß der Drive Upload sequentiell in der richtigen Reihenfolge stattfinden muss.
  2. Eine Task-List erstellt, welche von einem regelmäßig ausgeführten Script nach und nach abgearbeitet wird.

Erinnern wir uns an die 7-tägige Gültigkeit des Uploadlinks, wären bei einer rein sequentiellen Ausführung in Cloud Functions rechnerisch somit Dateigrößen bis 10 TB möglich.

Allerdings gibt es noch diverse andere Limits in Google und den Google Diensten wie die max. Dateigröße (max 5 TB Dateigröße) oder Upload-Limits (750GB/Tag), wodurch sich auch diese Lösung nicht unendlich skalieren kann.

Und wieder einmal hat sich gezeigt: “Eine unendliche Skalierung gibt es nicht.”

Quellcode

Der gesamte Quellcode der Cloud Function lässt sich in unserem öffentlichen GitHub RepositoryExternal Link herunterladen.

Jetzt Kontakt aufnehmen

Sie haben Fragen zum Artikel oder wollen mit Cloud-Native Anwendungen starten bzw. diese optimieren? Nehmen Sie einfach Kontakt zu uns auf!


Genutzte Services

Weiterlesen

Neuigkeiten
Ippen Digital setzt auf Google Workspace

So funktioniert die digitale Transformation - moderne Zusammenarbeit von überall mit Google Workspace.

Mehr erfahren
Neuigkeiten
Ausgezeichnet als Cloud Native Rockstar 2022

Unser “Viessmann-Projekt” belegt in der Kategorie “New Work & Collaboration” den 1. Platz.

Mehr erfahren
Artikel
Frage eine Trainee im Bereich Google Cloud Consulting - Hassata

Interview mit Hassata, Teilnehmerin am Google Cloud Consulting Trainee-Programm, in dem sie ihren Hintergrund, ihre Erfahrungen und ihre Arbeit an der Public Cloud Academy bespricht.

Mehr erfahren
Artikel
Wir stellen vor: Die neue Google Cloud Backup und DR Lösung

Erkunden Sie die Bedeutung von Backup- und Disaster-Recovery-Strategien im Kontext der digitalen Transformation und IT-Sicherheit und betonen Sie die Vorteile der Verwendung einer Public-Cloud-Lösung wie Google Cloud.

Mehr erfahren
Alles sehen

Gemeinsam durchstarten

United Kingdom
Arrow Down