Machbarkeit Multiple-File-Access aus einer Thread Applikation



  • Guten Morgen und danke für die schnelle Antwort ;-).

    Bisher war ich auch immer der Meinung, dass die (Posix) Threads unter Linux problemlos vom Kernel behandelt werden, Alle meine früheren Programme hatten aber nicht annähernd soviel gleichzeitig offene Dateien.
    Der folgende Code ist absolut rough, entstanden ist die Datei quasi direkt im VIM aus einer Demo-Datei des FastCGI Paketes, welches die Thread Erstellung schon fertig hatte. Es ist hier die "fread-Version" mit einem zusätzlichen 24 MB Puffer (setvbuf), welche durch den großen RAM-Verbrauch noch am ehesten gut läuft. Die MMAP und (Posix) READ Versionen unterscheiden sich jeweils nur um die Art wie die Dateien gelesen werden, besonders in MMAP hatte ich große Hoffnungen gesetzt, hier waren die Probleme aber trotz passenden Compiler Advices und großem Readahead am größten.
    Die auskommentierte Einschränkung der Reader mit ein paar Mutex's ist im COde auch noch vorhanden.

    Um es nochmals etwas einzuschränken: Dieser (nicht wirklich fehlerbehandelte) Code läuft problemlos, funktioniert aber nur mit einem riesigen extra Puffer halbwegs brauchbar.
    Die System-Last und Rest-Performance des RAIDs ist aber gegenüber einer simplen Apache-PHP Lösung viel schlechter (wo überhaupt nicht auf Caches geachtet wird, lediglich der in allen Fällen vorhandene Readahead des Systems ist vorhanden, welcher von fread und Co meiner Info nach auch genutzt werden sollte).
    An der FCGX-lib wird es kaum liegen, der Load entsteht in jedem Fall beim lesen der Daten. Mit zufallsmässig erzeugten Dummy Files (ohne lesen von Dateien aber der gleichen Output-Menge via Socket Richtung NGINX) schnurrt das System mit minimaler CPU-Last und maximaler Bandbreite Richtung Netzwerk (1GBit/s) vor sich hin.

    #ifndef lint
    static const char rcsid[] = "$Id: main.c$";
    #endif /* not lint */
    
    #include "fcgi_config.h"
    
    #include <pthread.h>
    #include <sys/types.h>
    
    #ifdef HAVE_UNISTD_H
    #include <unistd.h>
    #endif
    
    #ifdef __APPLE__
    #elif __GNUC__
    #include <fcntl.h>
    #include <malloc.h>
    #endif
    
    #include "fcgiapp.h"
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/stat.h>
    #include "hhlp.h"
    
    #define READ_BUFFER 24 * 1024 * 1024
    #define WRITE_BUFFER 1 * 1024 * 1024
    #define MAX_POST_SIZE 4096
    /*
    #define NUM_FILE_READERS 16
    */
    #define THREAD_COUNT 500
    
    #define FORBIDDEN(stream) \
        FCGX_FPrintF(stream, "Status: 403 Forbidden\r\nContent-Type: text/html\r\n\r\n<h1>403 Forbidden</h1>\n");
    #define NOTFOUND(stream) \
        FCGX_FPrintF(stream, "Status: 404 Not Found\r\nContent-Type: text/html\r\n\r\n<h1>404 Not Found</h1>\n");
    
    struct runtimes
    {
        char file[255];
        int started;
    };
    
    static long requests = 0l;
    static struct runtimes running[THREAD_COUNT];
    static char *server_name = NULL;
    static char *servers[3];
    
    static pthread_mutex_t accept_mutex = PTHREAD_MUTEX_INITIALIZER;
    static pthread_mutex_t counts_mutex = PTHREAD_MUTEX_INITIALIZER;
    /*
    static pthread_mutex_t readers[NUM_FILE_READERS];
    */
    
    static void *doit(void *a)
    {
        int rc, i, c;
        long thread_id = (long) a;
        pid_t pid = getpid();
    
        char *query_string = NULL;
    
        // for file reader
        struct stat buf;
        char filename[2048];
        char filetmp[256];
        long filesize = 0l;
        long pos = 0;
        long bytesread = 0;
        char *read_buffer = NULL;
        long read_buffer_size = 0;
        char *write_buffer = NULL;
        long write_buffer_size = 0;
    
        char *content = NULL;
        unsigned long content_length = 0l;
        char *content_length_str = NULL;
        FILE *file = NULL;
        char *method = NULL;
    
        struct CGI_DATA *GET_values = NULL;
        struct CGI_DATA *POST_values = NULL;
        struct CGI_DATA *GCI_runner = NULL;
    
        FCGX_Request request;
        FCGX_InitRequest(&request, 0, 0);
    
        for (;;)
        {
            pthread_mutex_lock(&accept_mutex);
            rc = FCGX_Accept_r(&request);
            pthread_mutex_unlock(&accept_mutex);
    
            if (rc < 0)
                break;
    
            running[(int) thread_id].started = time(NULL);
    
            pthread_mutex_lock(&counts_mutex);
            requests++;
            pthread_mutex_unlock(&counts_mutex);
    
            if (server_name == NULL)
            {
                pthread_mutex_lock(&counts_mutex);
                server_name = FCGX_GetParam("SERVER_NAME", request.envp);
                pthread_mutex_unlock(&counts_mutex);
            }
    
            method = FCGX_GetParam("REQUEST_METHOD", request.envp);
    
            // GET values
            if ((query_string = FCGX_GetParam("QUERY_STRING", request.envp)) != NULL)
            {
                HHLP_hex2ascii(query_string);
                HHLP_cleaninput(query_string);
                GET_values = HHLP_parseinput(query_string);
            }
    
            // POST values
            if (strcmp(method, "POST") == 0)
            {
                if ((content_length_str = FCGX_GetParam("CONTENT_LENGTH", request.envp)) == NULL)
                {
                    FORBIDDEN(request.out);
                    FCGX_Finish_r(&request);
                    return NULL;
                }
    
                if (content_length_str != NULL)
                {
                    content_length = (unsigned long) atol(content_length_str);
    
                    if (content_length > MAX_POST_SIZE)
                    {
                        FORBIDDEN(request.out);
                        FCGX_Finish_r(&request);
                        return NULL;
                    }
    
                    content = (char *) malloc(content_length + 1);
                    FCGX_GetStr(content, content_length, request.in);
    
                    HHLP_hex2ascii(content);
                    HHLP_cleaninput(content);
                    POST_values = HHLP_parseinput(content);
                }
            }
    
            if ((HHLP_getvalue("dbg", GET_values)) != NULL && strcmp(HHLP_getvalue("dbg", GET_values), "showme") == 0)
            {
                FCGX_FPrintF(request.out, "HTTP/1.1 200 OK\r\n");
                FCGX_FPrintF(request.out, "Content-Type: text/plain; charset=UTF-8\r\n");
                FCGX_FPrintF(request.out, "\r\n");
                FCGX_FPrintF(request.out, "Requests: %ld\n", requests);
                FCGX_FPrintF(request.out, "Running:  %d\n", threadsrunning());
                FCGX_FPrintF(request.out, "File:  %s\n", HHLP_getvalue("file", GET_values));
                FCGX_FPrintF(request.out, "Server:  %d: %s\n", (int) atoi(HHLP_getvalue("s", GET_values)), servers[(int) atoi(HHLP_getvalue("s", GET_values))]);
            }
            else if (HHLP_getvalue("fn", GET_values) != NULL && HHLP_getvalue("file", GET_values) != NULL && (int) atoi(HHLP_getvalue("s", GET_values)) > 0)
            {
                sprintf(filename, "%s%s", servers[(int) atoi(HHLP_getvalue("s", GET_values))], HHLP_getvalue("fn", GET_values));
                file = fopen(filename, "r");
    
                if (file != NULL)
                {   
                    stat(filename, &buf);
                    filesize = buf.st_size;
    
                    read_buffer_size = ((long) READ_BUFFER < filesize) ? (long) READ_BUFFER : filesize;
                    read_buffer = malloc(sizeof (char) * read_buffer_size);
                    setvbuf(file, read_buffer, _IOFBF, read_buffer_size + 8);
    
                    write_buffer_size = ((long) WRITE_BUFFER < filesize) ? (long) WRITE_BUFFER : filesize;
                    write_buffer = malloc(sizeof (char) * write_buffer_size);                
    
                    FCGX_FPrintF(request.out, "HTTP/1.1 200 OK\r\n");
                    FCGX_FPrintF(request.out, "Content-Length: %ld\r\n", filesize);
                    FCGX_FPrintF(request.out, "Pragma: public\r\n");
                    FCGX_FPrintF(request.out, "Expires: 0\r\n");
                    FCGX_FPrintF(request.out, "Cache-Control: must-revalidate, post-check=0, pre-check=0\r\n");
                    FCGX_FPrintF(request.out, "Content-Type: application/octet-stream\r\n");
                    FCGX_FPrintF(request.out, "Content-Transfer-Encoding: binary\r\n");
                    FCGX_FPrintF(request.out, "Content-Disposition: attachment; filename=\"%s\"\r\n", HHLP_getvalue("file", GET_values));
                    FCGX_FPrintF(request.out, "\r\n");
    
                    pos = 0;
                    bytesread = 0;
    
                    while (FCGX_GetError(request.out) == 0 && pos < filesize)
                    {
                        //pthread_mutex_lock(&readers[thread_id % NUM_FILE_READERS]);
                        //fseek(file, pos, SEEK_SET);
                        bytesread = fread(write_buffer, sizeof (char), write_buffer_size, file);
                        //pthread_mutex_unlock(&readers[thread_id % NUM_FILE_READERS]);
    
                        pos += bytesread;
                        FCGX_PutStr(write_buffer, bytesread, request.out);
                    }
    
                    read_buffer = realloc(read_buffer, 1);
                    free(read_buffer);
                    read_buffer = NULL;
    
                    write_buffer = realloc(write_buffer, 1);
                    free(write_buffer);
                    write_buffer = NULL;
    
                    fclose(file);
                }
                else
                {
                    NOTFOUND(request.out);
                }
            }
            else
            {
                NOTFOUND(request.out);
            }
    
            HHLP_deletevalues(GET_values);
            HHLP_deletevalues(POST_values);
            free(content);
    
    #ifdef __APPLE__
    #elif __GNUC__
            malloc_trim(0);
    #endif
    
            running[(int) thread_id].started = 0;
            FCGX_Finish_r(&request);
        }
    
        return NULL;
    }
    
    int main(void)
    {
        long i;
        pthread_t id[THREAD_COUNT];
    
        servers[0] = "";
        servers[1] = "/srv/_tst/d1/";
        servers[2] = "/srv/_tst/d2/";
    
        printf("%s %s", servers[0], servers[1]);
    
    /*
        for (i = 0; i < NUM_FILE_READERS; i++)
        {
            pthread_mutex_init(&readers[i], NULL);
        }
    */
    
        FCGX_Init();
    
        for (i = 0; i < THREAD_COUNT; i++)
        {
            strcpy(running[i].file, "");
            running[i].started = 0;
    
            pthread_create(&id[i], NULL, doit, (void*) i);
        }
    
        doit(0);
    
        return 0;
    }
    
    int threadsrunning()
    {
        int i, runningthreads = 0;
    
        for (i = 0; i < THREAD_COUNT; i++)
        {
            if (running[i].started > 0)
            {
                runningthreads++;
            }
        }
    
        return runningthreads;
    }
    


  • PHP fread gegen C fread, hab ich noch nie probiert. Dürfte aber wohl auf denselben Systemcall gehen.
    Der numblock Parameter von fread/fwrite sollte aus Performanzsicht immer auf 1 stehen (bei bekannter Gesamtgröße), du machst genau das Gegenteil.
    Das setvbuf lasse erstmal weg, bei Multithreads weiß das Betriebssystem eh besser über Filecaching Bescheid als du in deinem Thread.
    Hast du schon mal über einen Threadpool nachgedacht?
    Nehme erstmal 1 Thread und tune diesen für ein paar realitätsnahe Terabytes, multithreaden kannst du dann immer noch.
    Ich bin auch der Meinung, die C Variante sollte schneller und weitaus ressourcenschonender sein als PHP, meine C Programme waren dies gegenüber irgendwelchen Skriptsprachen bisher jedenfalls immer 😉



  • PHP erzeugt intern einen "PHP-Stream", der aber allem Anschein nach wieder mit MMAP oder fread arbeitet (MMAP scheinbar für kleinere Operationen, ganz schlau bin ich aus dem Code durch Portabilität der PHP-Sourcen nicht geworden).

    Die Blockangabe war mir als Fehler komplett nicht klar 😕, vielen Dank für den Tip. Die Änderung mache ich gleich klar und berichte dann.

    Mit einem Thread hatte ich es mittels Lighttpd getestet, da dieser die Prozesse selber bis zur Menge X ausspannen kann. Allerdings läuft Lighty dank der fehlenden Blockade des FastCGI Puffers irgendwann voll. Bisher konnte ich keine Möglichkeit für FastCGI finden, wie man herausfindet, ob der Webserver schon für den User zwischenpuffert, oder auf weitere Daten wartet. Bei NGINX passiert genau dieser Sachverhalt nicht, ich konnte aber hier bisher keine Single-Prozesse direkt nutzen, da die FastCGI-Api dort immer einen Multithread-Prozess zu erwarten scheint.
    Mit einem Threadpool meinst du die Reader für die Dateien? Ich hatte als Test einen Pool von Reader-Threads erzeugt, welche den eigentlichen Threads für die Ausgabe die Daten geliefert haben. Das Ergebnis war vergleichbar mit den aktuell auskommentierten Mutex's.

    Da die Umsetzung für die Änderung des Projekts von mir kam, geht es auch um die Meinung "das muss doch schneller sein" ;-). Sparsamer ist es in jedem Fall, da die meist 300x22MB Apache + PHP-Modul einfach den RAM unnötig überreizen.

    EDIT:
    bringt auch leider keinen Unterschied. Aber selbst ein Test mittels Lighttpd bringt bei 200 laufenden Prozessen selbiges Ergebnis:
    Das RAID geht unter der aggressiven Menge von Anfragen nach ein paar Minuten unter.
    Auch mit iotop und lsof sieht man lediglich die offenen Dateien/Anfragen nach Daten. Für mich wirken diese Anfragen aber wesentlich aggressiver, als die des PHP-freads. Als ob das eingesetzte Readahead des Kernel überhaupt nicht greifen würde.
    Bei einem Blockdev -setra 16384 /dev/sda sehe ich via iotop z.b. für jeden Apache-Prozess immer konstante Anfragen von 8MB/s, bei einer Erhöhung auf 32768 werden passend 16mb/s verlangt.
    Meine eigenen Prozesse fragen quer alles an. Mal große Mengen mit passenden Angaben zum readahead, mal Kilobyte-Mengen, die mir gar nicht einleuchten.
    (Die Aussagen beziehen nur auf die ersten Sekunden/Minuten solange das RAID noch sauber liefern kann, danach bricht sowieso die gelieferte Menge an Daten zusammen und die Prozesse bekommen nur noch Kilobyte-Mengen).



  • Ich bin mir jetzt nicht sicher wie viel FastCGI dir da an Arbeit abnimmt, aber hast du die Möglichkeit eine Verbindung direkt über Sockets herzustellen?



  • cooky451 schrieb:

    Ich bin mir jetzt nicht sicher wie viel FastCGI dir da an Arbeit abnimmt, aber hast du die Möglichkeit eine Verbindung direkt über Sockets herzustellen?

    Der Spezialist wieder. 😃



  • Hallo,

    FastCGI dient eigentlich "nur" (Soll nicht abwertend klingen, mir fällt nur nichts besseres ein ;)) die Verbindung zum Weserver her. Die Idee selber einen aufzusetzen war in jedem Fall im Raum, aber die Faulheit hat am Ende gesiegt, da der Aufwand doch nicht zu unterschätzen wäre.

    Den Tag über bin ich weiterhin am testen und konnte mittels dem normalen open und read mit einem posix_fadvice für sequentielles lesen bei einem Puffer von 16MB ein recht ruhig laufendes System erstellen, welches aber durch sich überlappende "Nachlade-Threads" (aktuell limitiert auf 12) immer wieder kurz ins Wackeln und Stocken gerät.

    Gibt es ausser den Beschreibungen von GNU.org und den Linux Dokus noch andere Quellen, an denen man sich mit Themen wie dem Read-Ahead auseinandersetzen kann?
    Mir leuchtet weiterhin die Kunst nicht ein, mit welcher einmal bei wesentlich geringerer Blockgröße (Apache-PHP) ein ruhig laufendes System entsteht, und an anderer Stelle mit den vermeindlich gleichen Werkzeugen bei sogar größerer Blockgröße/Puffer eine recht große Systemlast entsteht. Das Finetuning hierfür ist allerdings für mich in den PHP-Quellen nicht so einfach ersichtlich :(.



  • Nur ums mal in den Raum zu werfen: Hast du schon von HipHop gehört?



  • Nabend ;),

    ja, in der Tat, Hiphop hatten wir im Gespräch. Es scheiterte aber der fehlenden Threadfähigkeit um wiederum von NGINX angesteuert zu werden. Mir ist auch bis heute kein Weg bekannt wie man eine auf einen Socket höhrende Anwendung in PHP schreiben könnte, die dann Anfragen auf verschiedene Threads verteilt.
    Ansonsten juckt mich gerade die Möglichkeit nach dem Komplieren die Lesefähigkeiten zu testen (ob diese überhaupt noch in der gleichen Form vorhanden sind).



  • Ich würde ja gerne etwas beisteuern, aber mit meiner 100Mbit LAN Leitung schaffe ich es nicht mal auf 1% Prozessorauslastung zu kommen, da gestalten sich Tests irgendwie schwierig. 🙄



  • Auch nach dem Wochenende bin ich nicht schlauer.

    Aber ggf. weiss jemand von Euch eine Antwort auf diese Frage ;):

    Muss ich unter Linux bei Verwendung des GCC den Read-Ahead anders ansteuern als den systemweiten, welcher quasi jede Applikation bedient (die auch in C geschrieben ist)?

    Meiner Meinung nach sollte der systemweite (via blockdev oder /proc eingestellt) den Apache in gleichen Bereichen beeinflussen wie eine normale C-Applikation die mittels (f)open an Dateien rangeht (wie meine).
    Um genau das zu testen habe ich gestern unter großerer Last PHP nachgeahmt und schlicht und einfach mittels read() (ohne jeden advice an den Kernel) 512kb Daten blind an den Webserver geschickt. Das Ergebnis war im Gegensatz zu den PHP reads ein vollkommen zappeliger Server, welcher nach ein paar Minuten auf 100%wa war.

    Irgendwas lässt mich nicht los, dass das Read-Ahead überhaupt nicht funktioniert. Die Doku von GNU sagt zum GGC absolut nichts aus was darauf passt. Lediglich die posix_fadvice sind verzeichnet, aber wohl auch nicht zwingend notwendig (bringen aber auch keine Besserung wenn man sie in meinem Fall verwendet).



  • Guten Abend!

    Die "Lösung" für das Problem war ein erneutes Lesen der PHP-Stream Sourcefiles. In einem Kommentar stand etwas wie "Check if buffer is already filled".

    Die PHP-Macher haben ein Thread-basiertes Backgrounding zum Cachen implementiert.
    Der Aufwand dafür war mir schlicht zu groß, aber für den GCC gibt es die AIO-Lib (Async Input/Output http://lse.sourceforge.net/io/aio.html), welche quasi genau das Verhalten ermöglicht.

    Zusätzlich lässt sich die Priorität des Reader senken, das Ergebnis ist ein sehr ruhig laufender Lesevorgang, welcher minimal so gut ist wie der von PHP, allerdings mit einer stark gesteigerten RAM-Kontrolle für die Caches und einen Speichergewinn durch die fehlenden PHP-Parser und den Webserver.

    Danke an alle Beteiligten für die ganzen Denkanstöße ;-).


Anmelden zum Antworten