Einführung
Ibco ist eine C/C-Coroutine-Bibliothek, die in großem Umfang im WeChat-Backend verwendet wird. Sie läuft seit 2013 stabil auf Zehntausenden von Maschinen im WeChat-Backend. Libco wurde 2013 zum ersten Mal als eines der sechs großen Open-Source-Projekte von Tencent veröffentlicht. Wir haben kürzlich ein großes Update durchgeführt, das unter https://github.com/tencent/libco synchronisiert ist. libco unterstützt das Backend-Programmiermodell im agilen Synchronisierungsstil und bietet gleichzeitig hohe Parallelitätsfähigkeiten des Systems.
Von libco unterstützte Funktionen
Es ist nicht erforderlich, in die Geschäftslogik einzudringen, Multiprozess- und Multi-Thread-Dienste in Coroutine-Dienste umzuwandeln, und die Parallelitätsfähigkeit wird um das Hundertfache verbessert 🎜> Unterstützung des CGI-Frameworks, einfache Erstellung von Webdiensten (neu);
Unterstützung häufig verwendeter Drittbibliotheken wie gethostbyname, mysqlclient, ssl (neu);
Optionaler Shared-Stack-Modus, einzelne Maschine kann Einfacher Zugriff auf zig Millionen Verbindungen (Neu);
Perfekte und übersichtliche Coroutine-Programmierschnittstelle
– Pthread-ähnliches Schnittstellendesign, die Erstellung und Wiederherstellung von Coroutinen kann über einfache und klare Schnittstellen abgeschlossen werden wie co_create, co_resume; – Klasse __thread Coroutine private Variablen, Coroutine-Semaphor co_signal für die Kommunikation zwischen Coroutinen (Neu); – Lambda-Implementierung auf Nicht-Sprachebene, kombiniert mit Coroutine-In-situ-Schreiben und Ausführung asynchroner Hintergrundaufgaben (Neu); – Implementierung basierend auf epoll/kqueue Kleines und leichtes Netzwerk-Framework, Hochleistungs-Timer basierend auf Zeitroulette;
Hintergrund von libco
In den frühen Tagen hatte das WeChat-Backend ein komplexes und sich änderndes Geschäft Anforderungen und schnelle Iteration der Produktanforderungen verwenden die meisten Module ein halbsynchrones und halbasynchrones Modell. Die Zugriffsschicht ist ein asynchrones Modell und die Geschäftslogikschicht ist ein synchrones Multiprozess- oder Multithread-Modell. Die Parallelitätsfähigkeit der Geschäftslogik beträgt nur zehn bis Hunderte. Mit dem Wachstum des Geschäfts von WeChat wird der Systemumfang immer größer und jedes Modul wird leicht durch Back-End-Service-/Netzwerk-Jitter beeinträchtigt.
Die Wahl der asynchronen Transformation
Um die Parallelitätsfähigkeit des WeChat-Backends zu verbessern, besteht der allgemeine Ansatz darin, alle Dienste im bestehenden Netzwerk auf ein asynchrones Modell umzustellen. Dieser Ansatz erfordert einen enormen Arbeitsaufwand, vom Framework bis zum Geschäftslogikcode, der eine vollständige Transformation erfordert, was zeitaufwändig, arbeitsintensiv und riskant ist. Also begannen wir über die Verwendung von Coroutinen nachzudenken.
Die Verwendung von Coroutinen wird jedoch mit den folgenden Herausforderungen konfrontiert:
Industrie-Coroutinen haben keine Erfahrung mit groß angelegten Anwendungen in C/C-Umgebungen.
Wie man die Coroutinenplanung steuert;
Wie man mit synchronen API-Aufrufen wie Socket, MySQLclient usw. umgeht; Wie man mit der Verwendung vorhandener globaler Variablen und privater Thread-Variablen umgeht; > Schließlich haben wir es durch libco gelöst. Alle oben genannten Probleme erreichen eine nicht aufdringliche asynchrone Transformation der Geschäftslogik. Wir haben libco verwendet, um Hunderte von WeChat-Backend-Modulen in Coroutinen und asynchrone Transformationen umzuwandeln. Während des Transformationsprozesses blieb der Geschäftslogikcode im Wesentlichen unverändert. Bisher handelt es sich bei den meisten Diensten im WeChat-Backend um Multiprozess- oder Multithread-Coroutine-Modelle. Die Parallelitätsfähigkeiten wurden im Vergleich zu früher qualitativ verbessert, und libco ist zum Eckpfeiler des WeChat-Backend-Frameworks geworden.
Libco-Framework
Das Libco-Framework ist in drei Schichten unterteilt, nämlich die Schnittstellenschicht, die Systemfunktions-Hook-Schicht und die ereignisgesteuerte Schicht.
Verarbeitung von APIs im synchronen Stil
Bei APIs im synchronen Stil, hauptsächlich synchronen Netzwerkaufrufen, besteht die Hauptaufgabe von libco darin, die Ressourcenbelegung durch diese Wartezeiten zu beseitigen und die Parallelität des Systems zu verbessern Leistung. Bei einem regulären Netzwerkhintergrunddienst können wir Schritte wie Verbinden, Schreiben und Lesen durchlaufen, um eine vollständige Netzwerkinteraktion abzuschließen. Wenn diese APIs synchron aufgerufen werden, bleibt der gesamte Thread hängen und wartet auf eine Netzwerkinteraktion.
Obwohl die Parallelitätsleistung des synchronen Programmierstils nicht gut ist, bietet er die Vorteile einer klaren Codelogik und eines einfachen Schreibens und kann eine schnelle Geschäftsiteration und eine agile Entwicklung unterstützen. Um die Vorteile der synchronen Programmierung weiterhin aufrechtzuerhalten, ohne den vorhandenen Geschäftslogikcode online zu ändern, hat libco die Netzwerkaufrufschnittstelle (Hook) innovativ übernommen und die Übergabe und Wiederherstellung der Coroutine als Ereignis im asynchronen Netzwerk-IO mit Rückrufen registriert . Wenn die Geschäftsverarbeitung auf eine synchrone Netzwerkanforderung stößt, registriert die Libco-Schicht die Netzwerkanforderung als asynchrones Ereignis, diese Coroutine gibt die CPU-Belegung auf und die CPU wird zur Ausführung an andere Coroutinen übergeben. Libco setzt die Coroutine-Ausführung automatisch fort, wenn ein Netzwerkereignis auftritt oder eine Zeitüberschreitung auftritt.
Wir haben die meisten APIs im Synchronisierungsstil über die Hook-Methode übernommen, und libco plant die Coroutine so, dass sie die Ausführung zum richtigen Zeitpunkt wieder aufnimmt.
Zigmillionen von Coroutinen werden unterstützt
Standardmäßig verfügt libco über einen exklusiven Laufstapel für jede Coroutine. Wenn die Coroutine erstellt wird, wird ein Speicher fester Größe aus dem Heap-Speicher als Coroutine zugewiesen . Stapel ausführen. Wenn wir eine Coroutine verwenden, um eine Zugriffsverbindung am Front-End abzuwickeln, wird bei einem massiven Zugriffsdienst die Parallelitätsgrenze unseres Dienstes leicht durch den Speicher begrenzt. Zu diesem Zweck bietet libco auch einen stapellosen Coroutine-Sharing-Stack-Modus, der es Ihnen ermöglicht, mehrere Coroutinen so einzurichten, dass sie denselben laufenden Stack gemeinsam nutzen. Beim Wechsel zwischen Coroutinen unter demselben gemeinsam genutzten Stack muss der aktuell ausgeführte Stack-Inhalt in den privaten Speicher der Coroutine kopiert werden. Um die Anzahl solcher Speicherkopien zu reduzieren, erfolgt die Speicherkopie des gemeinsam genutzten Stapels nur beim Wechsel zwischen verschiedenen Coroutinen. Wenn sich der Beleger des gemeinsam genutzten Stapels nicht geändert hat, besteht keine Notwendigkeit, den laufenden Stapel zu kopieren.
Der Shared-Coroutine-Stack-Modus von libco-Coroutine macht es einer einzelnen Maschine einfach, auf zig Millionen Verbindungen zuzugreifen, indem einfach genügend Coroutinen erstellt werden. Wir erstellen 10 Millionen Coroutinen (E5-2670 v3 bei 2,30 GHz * 2, 128 GB Speicher) im Libco-Shared-Stack-Modus. Jeweils 100.000 Coroutinen belegen 128 GB Speicher. Der gesamte Speicherverbrauch des stabilen Echo-Dienstes beträgt ungefähr 66 GB.
Private Coroutine-Variablen
Wenn ein Multiprozessprogramm in ein Multithread-Programm umgewandelt wird, können wir __thread verwenden, um globale Variablen schnell zu ändern. In der Coroutine-Umgebung haben wir die Coroutine erstellt Die Prozessvariable ROUTINE_VAR vereinfacht die Arbeitsbelastung der Coroutine-Transformation erheblich.
Da Coroutinen im Wesentlichen seriell innerhalb eines Threads ausgeführt werden, kann es beim Definieren einer privaten Thread-Variablen zu Wiedereintrittsproblemen kommen. Wenn wir beispielsweise eine private Thread-Variable von __thread definieren, wollten wir ursprünglich, dass jede Ausführungslogik exklusiven Zugriff auf diese Variable hat. Wenn unsere Ausführungsumgebung jedoch auf Coroutinen migriert wird, kann dieselbe private Thread-Variable von mehreren Coroutinen bedient werden, was zu dem Problem des Eindringens von Variablen führt. Aus diesem Grund haben wir bei der asynchronen Transformation von libco die meisten privaten Thread-Variablen in private Variablen auf Coroutine-Ebene geändert. Private Coroutine-Variablen weisen die folgenden Merkmale auf: Wenn der Code in einer Multithread-Nicht-Coroutine-Umgebung ausgeführt wird, ist die Variable Thread-privat. Wenn der Code in einer Coroutine-Umgebung ausgeführt wird, ist diese Variable Coroutine-privat. Die zugrunde liegenden privaten Coroutine-Variablen ermitteln automatisch die Ausführungsumgebung und geben den erforderlichen Wert korrekt zurück.
Private Coroutine-Variablen spielen eine entscheidende Rolle bei der Umstellung der bestehenden Umgebung von Synchronisation auf Asynchronisation. Gleichzeitig haben wir eine sehr einfache und bequeme Methode zum Definieren privater Coroutine-Variablen definiert Zeile des Deklarationscodes.
Hook-Methode von gethostbyname
Für vorhandene Netzwerkdienste kann es erforderlich sein, DNS abzufragen, um die tatsächliche Adresse über die gethostbyname-API-Schnittstelle des Systems zu erhalten. Während der Coroutine-Transformation haben wir festgestellt, dass die Socket-Familienfunktion unseres Hooks nicht auf gethostbyname anwendbar ist. Wenn eine Coroutine gethostbyname aufruft, wartet sie synchron auf das Ergebnis, was dazu führt, dass die Ausführung anderer Coroutinen im selben Thread verzögert wird. Wir haben den gethostbyname-Quellcode von glibc untersucht und festgestellt, dass der Hook hauptsächlich deshalb keine Wirkung zeigt, weil glibc intern die __poll-Methode definiert, um auf Ereignisse zu warten. Gleichzeitig definiert glibc auch eine private Thread-Variable. die von verschiedenen Coroutinen verwendet werden können, kann zu Wiedereintritten führen, was zu ungenauen Daten führt. Schließlich wird die Asynchronisierung der gethostbyname-Coroutine durch die Hook-__poll-Methode und die Definition privater Coroutine-Variablen gelöst.
Gethostbyname ist eine von glibc bereitgestellte synchrone Abfrage-DNS-Schnittstelle. Es gibt viele hervorragende asynchrone Lösungen für gethostbyname in der Branche, aber diese Implementierungen erfordern die Einführung einer Bibliothek eines Drittanbieters und erfordern, dass die zugrunde liegende Schicht eine asynchrone Schnittstelle bereitstellt Rückrufbenachrichtigungsmechanismus. Durch die Hook-Methode realisiert libco die Asynchronisierung von gethostbyname, ohne den Glibc-Quellcode zu ändern.
Coroutine-Semaphor
In einer Multithread-Umgebung müssen wir beispielsweise auf das Signal eines anderen Threads warten , wir Dies wird normalerweise mit pthread_signal gelöst. In libco definieren wir das Coroutine-Semaphor co_signal, um die Parallelitätsanforderungen zwischen Coroutinen zu erfüllen. Eine Coroutine kann entscheiden, eine wartende Coroutine zu benachrichtigen oder alle wartenden Coroutinen über co_cond_signal und co_cond_broadcast aufzuwecken.
Zusammenfassung
Libco ist eine effiziente C/C-Coroutine-Bibliothek, die eine vollständige Coroutine-Programmierschnittstelle, häufig verwendete Funktions-Hooks der Socket-Familie usw. bereitstellt und es Unternehmen ermöglicht, synchrone Programmiermodelle schnell zu verwenden iterative Entwicklung. Mit seinem stabilen Betrieb in den letzten Jahren hat libco als Eckpfeiler des Backend-Frameworks von WeChat eine entscheidende Rolle gespielt.