curl: mehrere Dateien von einem Server laden



  • Hallo,

    ich lade von einem Server viele kleine Dateien. Mit Curl klappt das ganz gut, allerdings ist das damit ziemlich langsam. Momentan lade ich zwar alle Dateien gleichzeitig, allerdings jede in einem eigenen Thread und muss demnach jeweils eine Verbindung aufbauen.

    Hier die Methode, mit der ich den Download starte:

    void etc_lib::download_file(string url, string download_path)
    {
    	if (url.size() > 0){
    		FILE* fp;
    		CURLcode res;
    		CURL* curl = curl_easy_init();
    		if (curl){
    			fp = fopen(Converter().atocstring(&download_path),"wb");
    			curl_easy_setopt(curl, CURLOPT_URL, Converter().atocstring(&url));
    			curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_data);
    			curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
    			res = curl_easy_perform(curl);
    			curl_easy_cleanup(curl);
    			fclose(fp);
    		}
    	}
    }
    

    Wobei Converter().atocstring(string*) hier lediglich string::c_str() liefert.

    Bei 400 Dateien mit jeweils ca 5kB brauche ich momentan allerdings fast 1 Minute.

    Gibt es eine Möglichkeit, gleich mehrere Dateien (am besten gleichzeitig) auf einmal herunterzuladen, ohne jeweils ein Thread zu nutzen?

    Ich fürchte in diesem Zusammenhang auch noch ein anderes Problem zu haben: will ich beispielsweise 500 Threads starten, bricht das Programm mit hoher Wahrscheinlichkeit ab (bei ca 100 Threads gleichzeitig läuft meist alles problemlos). Es kann eigentlich nur an Curl oder zu vielen Threads liegen, da ich lediglich die Downloads in die Threads packe. Kennt jemand das Problem?


  • Administrator

    libcurl bietet neben der Easy noch die Multi Schnittstelle an:
    http://curl.haxx.se/libcurl/c/libcurl-multi.html

    Ich frage mich allerdings, ob es wirklich an libcurl liegt. Hast du es genau nachgeprüft? Das schreiben auf die Festplatte ist es definitiv nicht? Ich habe die Erfahrung gemacht, dass die Betriebsysteme sehr langsam sein können, wenn man viele kleine Files schreiben möchte.

    Grüssli



  • Danke, ich werde mir das mal anschauen. Eine Auslastung der Festplatte oder CPU ist fast nicht zu erkennen. (und noch unwahrscheinlicher, weil ich eine SSD habe)

    Wenn ich den Download auskommentiere (die alten Dateien vom vorherigen Durchlauf sind ja schon auf der Platte), dann läuft das Programm auch flink durch.

    Edit:

    Habe mal multi-handle ausprobiert, allerdings scheint kein Transfer stattzufinden:

    void etc_lib::download_files(vector< string > urls, vector< string > download_paths)
    {
    	CURL *handles[urls.size()];
    	CURLM *multi_handle;
    	int still_running;
    	CURLMsg *msg; 
    	int msgs_left;
    
    	multi_handle = curl_multi_init();		
    
    	FILE* fp [urls.size()];
    	for (int i = 0; i < urls.size(); i++){
    		fp[i] = fopen(Converter().atocstring(&(download_paths[i])),"wb");
    		handles[i] = curl_easy_init();	
    		curl_easy_setopt(handles[i], CURLOPT_URL, Converter().atocstring(&(urls[i])));
    		curl_easy_setopt(handles[i], CURLOPT_WRITEFUNCTION, write_data);
    		curl_easy_setopt(handles[i], CURLOPT_WRITEDATA, fp);
    		curl_multi_add_handle(multi_handle, handles[i]);
    	}
    	curl_multi_perform(multi_handle, &still_running);
    
    	while (still_running){
    		usleep(1000000);
    	}
    
    	while ((msg = curl_multi_info_read(multi_handle, &msgs_left))){
    		if (msg->msg == CURLMSG_DONE){
    			int idx = 0;
    			for (idx = 0; idx < urls.size(); idx++)
    				if(msg->easy_handle == handles[idx])
    					break;
    
    			switch (idx){
    			case 0:
    				printf("transfer completed with status %d\n", msg->data.result);
    				break;
    			}
    		}
    	}
    
    	curl_multi_cleanup(multi_handle);
    	for (int i = 0; i < urls.size(); i++){
    		curl_easy_cleanup(handles[i]);
    		fclose(fp[i]);
    	}
    }
    

    Die Methode hängt in der Schleife while (still_running) fest. Ich sehe auch, dass die Dateien zwar angelegt wurden, allerdings alle leer sind.

    Habe ich da etwas falsch umgesetzt?



  • Du musst multi_perform ggf. öfter aufrufen, schau dir die Doku an....



  • Tut mir Leid, in Zeile 17 muss fp[i] hin und es läuft.

    curl_multi_info_read liefert ja ein struct, mit welchen der Fehlercode ausgelesen werden kann (curl_multi_info_read->data.result). Wisst ihr, wo ich die Übersetzung finden kann?

    Wenn ich beispielsweise 10 URLs gleichzeitig lade, klappt das wunderbar. Nehme ich aber 1000 URLs, werden fast alle nicht geladen und ich erhalte als Fehlercode meist 6.

    Ist es vllt möglich, dass der Server nur eine Handvoll Downloads zulässt und den Rest blockiert?


  • Administrator

    Ulf schrieb:

    Wisst ihr, wo ich die Übersetzung finden kann?

    Ja, in der Dokumentation:
    curl_multi_info_read

    When msg is CURLMSG_DONE, the message identifies a transfer that is done, and then result contains the return code for the easy handle that just completed.

    libcurl error codes

    Der Wert 6 bedeutet: CURLE_COULDNT_RESOLVE_HOST

    Grüssli



  • Danke, das ist ja interessant. 100 Downloads gleichzeitig klappen jedes Mal (Code 0). Bei 300 Downloads erhalte ich vereinzelt den Code 6, wenn ich gar 1000 Downloads ausführen will, erhalte ich nahezu bei allen den Code 6.

    Das gleiche Bild, wenn ich bspw. die google Startseite downloaden will.

    Kommt curl nicht bei so vielen Handles klar, oder liegt das am Server?


  • Administrator

    Probier allenfalls mal CURLMOPT_MAXCONNECTS zu setzen und dadurch die maximale Anzahl an Verbindungen zu beschränken. Ebenfalls könnte CURLMOPT_PIPELINING hilfreich sein.

    Grüssli



  • Ich danke dir, mit letzterem funktioniert es.

    Die Geschwindigkeit ist zwar trotzdem noch recht langsam (ca 3 Files pro Sekunde), aber deutlich schneller als vorher. Damit spare ich immerhin schon mal eine Größenordnung in der Zeit. Wenn man jetzt noch parallelisiert, wird es sicher recht flott gehen.

    Vielen Dank!



  • Ich habe das ganze jetzt mal parallelisiert, klappt zwar wunderbar, alle Dateien werden vollständig geladen, egal wie viele es sein sollen, allerdings verstehe ich die resultierende Laufzeit nicht.

    Ich will zum Test 100 nahezu identische Dateien downloaden und die benötigte Zeit messen.
    Wenn ich nur einen Thread benutze, benötige ich dazu ca. 65 Sekunden. Also knapp 2 Dateien pro Sekunde, das ist schon deutlich besser als alle nur mit der Easy-Schnittstelle zu laden.
    Verteile ich die Downloads auf 10 Threads, benötige ich ca. 50 Sekunden, selbst wenn ich 100 Threads nutze und jedem Thread eine Datei zum Downloaden gebe, benötige ich auch hier etwa 50 Sekunden.

    Eine Datei alleine ist in knapp 2 Sekunden heruntergeladen, wieso brauchen dann 100 Threads für jeweils eine Datei ca 50 Sekunden? Liegt es an der Option CURLMOPT_PIPELINING, dass es nur eine Pipeline gibt, die sich alle Threads teilen und daher warten müssen?

    Da die Dateien sehr klein sind (ca 5kB pro Datei), fallen die eigentlichen Downloadzeiten nicht ins Gewicht.


  • Administrator

    Ulf schrieb:

    Liegt es an der Option CURLMOPT_PIPELINING, dass es nur eine Pipeline gibt, die sich alle Threads teilen und daher warten müssen?

    Wenn jeder Thread ein eigenes curl Handle hat, dann denke ich eher nicht, da mir neu wäre, dass curl über die Handle-Grenze hinweg Dinge austauschen kann.

    Ich vermute immer noch, dass es an der Festplatte, bzw. am Betriebsystem liegt. Du hast zwar alles schön parallelisiert, aber z.B. ein CreateFile in der WinAPI führt dazu, dass deine Threads synchronisiert werden, da dies jeweils nur einer nach dem anderen machen kann. Auch sind gerade solche Operationen meistens nicht sehr billig.

    Kürzlich hatte ich einen Fall auf Linux, wo ich ebenfalls viele kleine Dateien erstellen und beschreiben musste. Da brachte ebenfalls nicht einmal eine RAMDisk wesentliche Verbesserungen in der Geschwindigkeit, einfach weil das OS selbst viel zu stark gebremst hat.

    Probier mal die Dateien nicht zu schreiben. Also einfach nur herunterladen und die Daten gleich wieder verwerfen. Keine neuen Dateien erstellen und auch keine Daten in diese Dateien schreiben.

    Grüssli



  • Ich konnte keine signifikante Änderung erkennen, wenn ich die Platte komplett außen vor lasse. Aber ich wüsste nicht, wieso das so sein soll. Wenn eine SSD für 100 Dateien zu je 5kB ein paar Sekunden braucht, dann ist die aber schon längst ein Garantiefall 😉

    Beim letztem Benchmark hat 4K-Schreibtest ordentliche >30MB/s ergeben. Die 100 Dateien sollten also in ein paar Millisekunden geschrieben worden sein.



  • Hat niemand eine Idee, woran das liegen könnte?

    Wenn ich auf curl verzichten würde und selbst mittels Sockets den Dateitransfer starte, weiß jemand, ob es dann ähnliche Effekte gibt? Falls nein, könnte man sich ja überlegen, das vielleicht ohne curl zu realisieren.


  • Administrator

    Ulf schrieb:

    Hat niemand eine Idee, woran das liegen könnte?

    Das Problem dürfte sein, dass es ein munteres Rätselraten ist. Mich würde es erstaunen, wenn es an curl liegt, weil ich hatte damit nie irgendwelche Geschwindigkeitsprobleme. Meine Vermutung war das Betriebsystem und das Dateisystem. Daher spielt es auch keine Rolle, dass du eien SSD hast. Wie gesagt, ich hatte das Problem sogar mit einer RAMDisk festgestellt, aber die Rahmenbedingungen sind wohl doch zu unterschiedlich.

    Von daher kann es von meiner Seite aus alles mögliche sein. Ich habe kurz auf der Mailing Liste von curl gesucht, konnte aber nur eine Diskussion in den letzten Jahren über zu langsamen Download finden.

    Grüssli



  • ist der Server gedrosselt?



  • Nein, das ist unabhängig vom Server. Selbst wenn ich spaßenshalber eine Seite vom google-Server nehme, dauert das recht lang, obwohl die Zeiten dort schon viel kleiner sind (habe jetzt keinen genauen Zahlen mehr im Kopf, aber das ging ca 25% schneller). Trotzdem kann ich auch mit schnellen google-Servern nicht ansatzweise meine Bandbreite ausnutzen, die Threads trödeln vor sich hin


Anmelden zum Antworten