[PYTHON] Stellen Sie sich eine Cloud-native WebSocket-Anwendung vor, die unter AWS ausgeführt wird

Einführung

Das Wort Cloud Native ist beliebt. Gestern hat @chiyoppy [Artikel über Microservises und die Cloud] geschrieben (http://qiita.com/chiyoppy/items/2d405c755d0bb3bcf08e). Ich denke, es ist ein guter Artikel, der die wichtigen Dinge beim Entwerfen eines Webdienstes in der Cloud kurz zusammenfasst. Als Antwort darauf werde ich darüber nachdenken, wie eine Cloud-native WebSocket-Anwendung erstellt wird. In dem gestrigen Artikel ist HTTP zustandslos in Bezug auf Anwendungen, also ist es erstaunlich! Sie können skalieren! In diesem Artikel werde ich eine WebSocket-Anwendung betrachten, die schwieriger zu skalieren scheint als HTTP.

Übrigens bin ich 2015 ein neuer Absolvent und bei der Arbeit benutze ich Swift, um iOS-Apps zu schreiben. Und heute ist mein Geburtstag. Wir warten auf ein Geschenk. Außerdem schreibe ich in meiner Arbeit iOS-Apps in Swift, sodass dieser Artikel nichts mit meiner Arbeit in Dwango zu tun hat.

Der Grund, warum ich dieses Thema dieses Mal aufgegriffen habe, ist, dass ich derzeit Cloud Natives und Microservises bei einer Studiensitzung studiere, die etwa einmal pro Woche von einigen neuen Absolventen von Dwango abgehalten wird. Was soll ich während des Studiums tun? ** Wie erstelle ich eine WebSocket-Anwendung, bei der es einfach ist, einen Status auf dem Server selbst zu haben **? Die Frage stellte sich. In diesem Artikel wird das Entwerfen einer WebSocket-Anwendung in Betracht gezogen, die in der Cloud ausgeführt wird, während eine kleine Chat-Anwendung erstellt wird.

Cloud native

Einfach ausgedrückt bedeutet Cloud Native, eine Anwendung unter der Annahme zu entwerfen, dass sie in der Cloud ausgeführt wird. Sie müssen auch über Containerisierung, automatische Bereitstellung, automatische Skalierung und Anwendungsdesign nachdenken, um in der Cloud ausgeführt zu werden. In diesem Artikel werden wir das Entwerfen einer Anwendung betrachten, die in der Cloud ausgeführt wird. Was andere Dinge betrifft, hätte Chiyoppy gestern verschiedene Dinge zusammenfassen sollen, also werde ich es diesmal weglassen. In diesem Artikel werde ich davon ausgehen, dass es unter AWS ausgeführt wird, sodass ich immer mehr AWS-Begriffe verwenden werde.

Anforderungen für Cloud-native Anwendungen

Um eine Anwendung zu erstellen, die in der Cloud ausgeführt wird, müssen die Eigenschaften der Cloud berücksichtigt werden. Ein typisches Beispiel ist, dass die Komponente unzuverlässig ist. Als Voraussetzung für Anwendungen gibt es eine Voraussetzung, dass EC2 plötzlich fallen kann.

--EC2 hat keinen Zustand --EC2-Instanzen können plötzlich verschwinden --Design, das jederzeit zerstört werden kann

Da ist so etwas. Bei HTTP sind zustandslose Skalierungsanwendungen erforderlich. Die diesmal erstellte WebSocket-Anwendung hat eine ähnliche Idee

Ich werde auf Dinge zielen. Der dritte Punkt betrifft nur WebSocket, über das Sie bei HTTP nicht nachdenken müssen.

Einfach ausgedrückt befindet sich die WebSocket-Anwendung, die wir für diese Zeit anstreben, in einem Zustand, in dem ** zwei Instanzen A und B in zwei AZs platziert sind und der Benutzer nicht das Gefühl hat, dass die Anwendung gelöscht wurde, wenn A manuell gelöscht wird **. Ich will es sein. Die WebSocket-Anwendung behält die Verbindung bei. Daher muss die Verbindung, die aufgrund eines Serverausfalls unterbrochen wurde, erfolgreich erneut hergestellt werden. Der Zweck besteht darin, eine Anwendung zu erstellen, die nicht ausfällt (eine ausgefallene Anwendung, die der Benutzer nicht bemerkt), indem die Verbindung von A zu B automatisch unterbrochen und in der Zwischenzeit automatisch ein neues A gestartet wird. Dieses Mal ist das Ziel, den fett geschriebenen Teil zu realisieren.

Versuchen Sie zunächst zu schreiben, ohne sich der Cloud-Eingeborenen bewusst zu sein

Lassen Sie uns eine App schreiben, ohne vorerst etwas zu bemerken. Dieses Mal haben wir den Chat als Beispielanwendung übernommen. Der Code für diesen Status ist hier. https://github.com/kouki-dan/CloudChat/tree/basic-chat Beim Start wird ein Textfeld angezeigt. Wenn Sie dort einen Buchstaben eingeben, wird dieser mit anderen Benutzern synchronisiert. Dies ist genau ein Chat.

Screen Shot 2015-12-02 at 4.04.51 AM.png

Codebeschreibung

Diese Klasse ist für die WebSocket-Kommunikation verantwortlich.

chat.py


class ChatSocketHandler(tornado.websocket.WebSocketHandler):
    #Werden Sie eine Klassenvariable(ChatSocketHandler.Zugriff mit xxx, cls in der Klassenmethode.(Zugänglich mit xxx)
    waiters = set() #Eine Reihe von aktuell verbundenen Benutzern
    cache = [] #Chatverlauf

    def open(self): #Methode, die beim Öffnen von WebSocket aufgerufen wird
        ChatSocketHandler.waiters.add(self)
        self.write_message({"chats": ChatSocketHandler.cache}) #Verlauf beim Verbinden senden

    def on_close(self): #Methode, die aufgerufen wird, wenn WebSocket getrennt wird
        ChatSocketHandler.waiters.remove(self)

    @classmethod
    def update_cache(cls, chat):
        cls.cache.append(chat) #Verlauf hinzufügen

    @classmethod
    def send_updates(cls, chat): #Broadcast-Chat an alle Benutzer
        for waiter in cls.waiters:
            try:
                waiter.write_message(chat)
            except:
                logging.error("Error sending message", exc_info=True)

    def on_message(self, message): #Methode, die aufgerufen wird, wenn eine Nachricht über WebSocket empfangen wird
        parsed = tornado.escape.json_decode(message)
        chat = {
            "body": parsed["body"],
            }

        ChatSocketHandler.update_cache(chat)
        ChatSocketHandler.send_updates({
          "chats": [chat]
        })

Es tut mir leid, wenn Sie Python nicht lesen können. Bitte fühle Python ist eine relativ einfach zu lesende Sprache, daher sollten Sie es fühlen. Jetzt führen wir den Verlauf und die Übertragung, wenn ein Chat gesendet wird. Selbst in diesem Zustand funktioniert es so gut wie es selbst betrieben werden kann, aber wenn man bedenkt, dass es in der Cloud betrieben werden kann, gibt es im aktuellen Zustand die folgenden Probleme.

Versuchen wir zunächst, der Anwendung keinen Status (Cache) zu geben. Dieses Mal werden wir Redis verwenden und den Cache ändern, der in der Redis-Liste gespeichert werden soll. AWS ElastiCache ist praktisch, da Sie problemlos Redis verwenden können, die über mehrere AZs verbunden werden können.

Chatten Sie ohne Status in der Anwendung

Bisher wurden Chat-Daten im Speicher in einer Variablen namens Cache gespeichert. Lassen Sie es uns in eine Konfiguration ändern, die keine Daten verliert, selbst wenn EC2 fällt, indem Sie sie an Redis weitergeben. Das Ergebnis der Implementierung ist wie folgt https://github.com/kouki-dan/CloudChat/tree/cache-in-redis

Werfen wir einen Blick auf diff.

chat.diff


 class ChatSocketHandler(tornado.websocket.WebSocketHandler):
     waiters = set()
-    cache = [] #Der Cache wurde entfernt, da er in Redis gespeichert ist
+    redis = Redis(decode_responses=True) #↑ Es ist eine Instanz, um den Cache auf Redis zu speichern
 
     def open(self):
         ChatSocketHandler.waiters.add(self)
         #Geändert, um zu erhalten, was über die Methode direkt auf den Cache verweist
-        self.write_message({"chats": ChatSocketHandler.cache})
+        self.write_message({"chats": ChatSocketHandler.get_caches()})
 
     def on_close(self):
         ChatSocketHandler.waiters.remove(self)
 
     @classmethod
     def update_cache(cls, chat):
         #Geändert, um in Redis anstatt im Speicher zu speichern
-        cls.cache.append(chat)
+        chat_id = cls.redis.incr("nextChatId") #Holen Sie sich die Chat-ID von Redis
+        redis_chat_key = "chat:{}".format(chat_id) #Erstellen Sie einen Speicherzielschlüssel(chat:1, chat:2 wird wie sein)
+
+        cls.redis.set(redis_chat_key, json.dumps(chat)) #In Redis können nur Zeichenfolgen festgelegt werden. Speichern Sie sie daher als JSON-Zeichenfolgen
+        cls.redis.rpush("chats", redis_chat_key) #Chat-Schlüssel zur Chat-Liste hinzufügen
+
+    @classmethod
+    def get_caches(cls):
+      #Methode, um eine Chat-Liste von Redis zu erhalten
+      chat_ids = cls.redis.lrange("chats", 0, -1) #Holen Sie sich alle Chat
+      chats = []
+      for chat_id in chat_ids:
+        chat = json.loads(cls.redis.get(chat_id)) #Konvertieren von einer JSON-Zeichenfolge in einen von Python verwalteten Wörterbuchtyp
+        chats.append(chat)
+      return chats #Gibt ein Array vom Typ Wörterbuch zurück

Für diejenigen, die Python nicht lesen können, habe ich das meiste, was ich brauche, in den Kommentaren geschrieben. Durch die Verwendung von Redis für den Buchungsteil und den Erfassungsteil gehen die Daten auch dann nicht verloren, wenn EC2 ausfällt. Ein weiterer Nebeneffekt beim Platzieren der Daten außerhalb der Instanz besteht darin, dass mehrere Instanzen dieselben Daten sehen können. Dies bringt uns einer Scale-Out-Konfiguration einen Schritt näher. Versuchen Sie, mehrere Instanzen zu starten.

スクリーンショット 2015-12-07 15.57.31.png

Auf diesem Foto habe ich den Port geändert und zwei Anwendungen gestartet. In früheren Anwendungen wurde der Chat-Inhalt nicht zwischen Instanzen synchronisiert, aber beim Speichern der Daten in Redis wird derselbe Inhalt angezeigt. Dies ist jedoch noch kein perfektes Scale-Out. Der Chat-Verlaufsstatus wird beim Start synchronisiert, aber das Senden und Empfangen von Nachrichten in Echtzeit wird nicht zwischen Instanzen synchronisiert. Selbst wenn Sie einen Chat an localhost: 8888 senden (siehe Abbildung unten), erreicht das Ergebnis localhost: 8899 nicht. Es wird sein. Das Gegenteil ist auch der Fall. スクリーンショット 2015-12-07 16.13.19.png

Lösen wir nun dieses Problem und betrachten eine Konfiguration, die vollständig skaliert werden kann.

WebSocket-Anwendung zum Skalieren

Zum Skalieren müssen Sie andere Instanzen über Ereignisse informieren, die in einer Instanz auftreten. Im obigen Beispiel waren die vom Benutzer referenzierten Daten inkonsistent, da kein Mechanismus vorhanden war, um localhost: 8899 über das Ereignis zu informieren, bei dem der bei localhost: 8888 aufgetretene Chat veröffentlicht wurde. Sie können Redis auch verwenden, um dies zu lösen. Entwickeln wir uns mithilfe des von Redis bereitgestellten PubSub-Mechanismus zu einem Scale-Out-Chat.

Was ist PubSub?

PubSub steht für Publish (Pub) und Subscribe (Sub) und wird für die Kommunikation zwischen Prozessen und Apps verwendet. Sie können ein Ereignis in einer Verbindung veröffentlichen, und das Ereignis wird an die vorab abonnierte Verbindung benachrichtigt.

Chat zum Skalieren

Wir werden die Kommunikation zwischen Instanzen verbessern, damit sie mithilfe von PubSub skaliert werden kann. Der Code, der dies implementiert, ist unten https://github.com/kouki-dan/CloudChat/tree/pubsub-chat

Schauen wir uns nun den Diff an

chat.py


+class Listener(threading.Thread):
+    def __init__(self, r):
+        threading.Thread.__init__(self)
+        self.redis = r
+        self.pubsub = self.redis.pubsub()
+        self.pubsub.subscribe(["chats"])
+    def work(self, item):
+        if item["type"] == "message":
+            chat = json.loads(item["data"])
+            ChatSocketHandler.send_updates(chat)
+    def run(self):
+        for item in self.pubsub.listen():
+            if item['data'] == "KILL":
+                self.pubsub.unsubscribe()
+            else:
+                self.work(item)
+

Zuerst erstellen wir eine Klasse zum Abonnieren. Wenn Sie "Abonnieren" im Hauptthread verarbeiten, wird der Hauptthread gesperrt, sodass dieser Teil im Multithread behandelt wird. Wir verwenden das Threading-Modul des Python-Standardmoduls für Multithreading. Diese Klasse gibt "ChatSocketHandler.send_updates (chat)" aus, wenn die Nachricht veröffentlicht wird. Auf diese Weise können Sie ein Ereignis senden, bei dem ein Chat an alle mit jeder Instanz verbundenen Benutzer gesendet wurde.

Schauen wir uns als nächstes den Hauptteil des Chats an

chat.diff


 class ChatSocketHandler(tornado.websocket.WebSocketHandler):
     waiters = set()
     redis = Redis(decode_responses=True)
+    #Listener hinzufügen
+    client = Listener(redis) 
+    client.start() 

# ......
# ...... 
 
     def on_message(self, message):
         parsed = tornado.escape.json_decode(message)
         chat = {
             "body": parsed["body"],
         }

         ChatSocketHandler.update_cache(chat)
-        ChatSocketHandler.send_updates({
-          "chats": [chat]
-        })
-
+        ChatSocketHandler.redis.publish("chats", json.dumps({
+            "chats": [chat]
+        }))

Die einzige Änderung besteht darin, dass der Teil, der zum direkten Aufrufen von "send_updates" verwendet wurde, in "Redis Publish" geändert wurde. Infolgedessen wird die Kommunikation wie folgt durchgeführt. (Ich wollte eine Zeichnung machen, aber ich habe den Mangel an Werkzeugen aufgegeben. Ich könnte eines Tages eine Zeichnung zeichnen. TODO)

Nutzer
   ↓ (Chat über WebSocket senden)
Jede Instanz
   ↓ (Publish)
  Redis
   ↓ (Benachrichtigen Sie abonnierte Instanzen)
Instanz A.,Instanz B.,....(Alle Instanzen)
   ↓ (Senden Sie Chat-Nachrichten über WebSocket an Benutzer, die mit jeder Instanz verbunden sind)
Nutzer

Das Ergebnis ist eine Scale-Out-Chat-Anwendung, die vollständig zwischen Instanzen synchronisiert ist. Der Bewerbungsteil ist nun abgeschlossen. Was für eine Cloud-native Anwendung fehlt, ist ein Failover im Fehlerfall. Auf der Serverseite kann dies nicht behoben werden. Lassen Sie uns also die Verbindung zum Client wiederherstellen.

Failover der WebSocket-Anwendung

Bisher haben wir eine WebSocket-Anwendung erstellt, die skaliert werden kann. Um die Cloud zu verwenden, in der die Zuverlässigkeit in einem Fall nicht garantiert ist, ist es wichtig, wie im Falle eines Fehlers ein Failover durchgeführt wird. Dies kann erreicht werden, indem alle Verbindungen, die mit A verbunden waren, wieder verbunden wurden, als die Instanz von A auf die Instanz von B herunterfiel, vorausgesetzt, zwei Instanzen von AB wurden gestartet.

Wie üblich beginnen wir mit der URL, die dies implementiert. https://github.com/kouki-dan/CloudChat/tree/reconnect-for-failover

Bisher wurden alle Chats jedes Mal vom Server gesendet, wenn eine Verbindung hergestellt wurde. Da die Chat-Informationen jedoch beim erneuten Herstellen der Verbindung nicht erforderlich sind, wurde auch der serverseitige Code geringfügig geändert. Werfen wir einen Blick auf den Client-Code, der die Wiederverbindung unterstützt.

index.js


    window.addEventListener("load", function() {

      //Verbindung beim Öffnen der URL
      connect(function() {
        //Handler bei Fehler
        alert("connection error"); 
      }, closeFunction, function() {
        //Handler bei Verbindung
        //Fordern Sie beim ersten Herstellen einer Verbindung den Chatverlauf an
        socket.send(JSON.stringify({"type":"command", "command":"requestFirstChat"}))
      });

      //Funktion zum Herstellen einer Verbindung mit WebSocket
      function connect(errorFunction, closeFunction, openFunction) {
        var url = "ws://" + location.host + "/chatsocket";
        socket = new WebSocket(url);
        // |Wenn Sie eine Nachricht erhalten, ist dies ein häufiger Vorgang
        socket.onmessage = function(event) {
          receiveChat(JSON.parse(event.data)["chats"]) //In DOM reflektierte Funktionen
        }
        // |Da die Verarbeitung von Fehlern usw. zwischen dem ersten Mal und der erneuten Verbindung unterschiedlich ist, sollte dies als Argument angegeben werden
        socket.onclose = closeFunction;
        socket.onerror = errorFunction;
        socket.onopen = openFunction;
      }

      //Funktion, wenn die Verbindung geschlossen ist, weil EC2 herunterfällt usw.
      function closeFunction(event) {
        var retry = 0;
        setTimeout(function() {
          var callee = arguments.callee; //Um sich wiederholt mit setTimeout anzurufen
          connect(function() {
            //Funktion bei Fehler
            retry++;
            //Versuchen Sie, die Verbindung wiederherzustellen, indem Sie sich bis zu dreimal anrufen
            if(retry < 3) { 
              setTimeout(function() {
                callee();
              }, 3000);
            } else {
              alert("connection error");
            }
          }, null /*close kann zu diesem Zeitpunkt nicht eingegeben werden*/, function() {
            //Da close auch dann aufgerufen wird, wenn es aufgrund eines Fehlers geschlossen wird, setzen Sie close, wenn die Verbindung erfolgreich ist.
            // 
            socket.onclose = closeFunction;
          });
        }, 1000);
      };
// ........

Die Codemenge hat sich wie ursprünglich erwartet verdoppelt. Verbindet es sich nicht einfach wieder? Ich dachte leicht nach, aber als ich den Code schrieb, musste ich nacheinander schreiben, wie zum Beispiel den Unterschied zwischen dem ersten Start und der erneuten Verbindung, die Anzahl der erneuten Verbindungen und so weiter.

Wenn Sie nach dem Herstellen einer Verbindung die Anwendung mit Strg + c löschen, wird die Anwendung abgeschlossen, die erneut eine Verbindung zu einer anderen Instanz herstellt. Es ist notwendig, den Load Balancer und den DNS einzustellen und zu optimieren, um tatsächlich wie erwartet zu funktionieren. Hier werden wir jedoch nur über die Implementierung der Anwendung sprechen.

Ich habe den Code schon lange geschrieben, aber jetzt ist die Bewerbung, die ich anstrebte, vollständig. In der diesmal erstellten Anwendung geht die während der erneuten Verbindung gesendete Nachricht verloren, sodass nicht gesagt werden kann, dass sie vollständig abgeschlossen ist. Nehmen wir jedoch an, dass sie vorerst abgeschlossen ist. Sende-Nachrichten werden im Client zwischengespeichert. Für empfangene Nachrichten ist die Implementierungsmethode denkbar, z. B. das Empfangen der letzten vom Client an den Client gesendeten Nachrichten-ID und das erneute Senden der Nachrichten, die vom Server nicht empfangen werden konnten. Wenn Sie interessiert sind, schreiben Sie bitte und PR. Das war's für diesen Artikel. Danke für deine harte Arbeit. Der Code, der tatsächlich funktioniert, ist mit einem Tag mit github verknüpft.

Zusammenfassung

Bisher haben wir den Prozess der Entwicklung eines von Ihnen geschriebenen Chats gesehen, ohne an eine Anwendung zu denken, die in der Cloud funktioniert und gleichzeitig eine hohe Verfügbarkeit gewährleistet. Ich habe diesen Artikel beim Schreiben des Codes geschrieben, daher war ich erleichtert, dass ich die Verbindung wieder herstellen konnte, als ich die Instanz das letzte Mal gelöscht habe. Es gibt jedoch viele Dinge zu beachten, wenn Sie eine Anwendung schreiben, die tatsächlich funktioniert. So ist die Sitzung, und wie gesagt, der Chat, den ich hier gemacht habe, ist nicht wirklich perfekt. Senden Sie im Falle eines Chats eine direkte Nachricht. Es scheint schwierig zu verarbeiten. Das Ausführen von WebSocket in der Cloud kann sehr nachdenklich und schwierig sein. Ich habe zufällig die Möglichkeit, eine WebSocket-Anwendung persönlich zu schreiben. Daher möchte ich einen Artikel mit dem schreiben, was ich hier geschrieben habe: "Ich habe tatsächlich versucht, eine Echtzeit-Kommunikationsanwendung mit WebSocket mit Cloud-nativem Bewusstsein zu erstellen." Ich denke. Wie ich am Anfang dieses Artikels schrieb, ist das Wichtigste, dass ** mein Geburtstag heute ist **. Wir warten auf Ihre Glückwunschbotschaft und Ihr Geschenk.

Ich möchte das auch lesen

Die URL, auf die Sie verwiesen haben, die URL, die Sie lesen möchten usw.

Morgen

Es sieht so aus, als würde Hiroppy (@about_hiroppy) morgen ** etwas ** schreiben. Was ist etwas **? Ich freue mich darauf.

Recommended Posts

Stellen Sie sich eine Cloud-native WebSocket-Anwendung vor, die unter AWS ausgeführt wird
Erstellen Sie mit Chalice eine flaschen- / flaschenähnliche Webanwendung auf AWS Lambda
Startete eine Webanwendung auf AWS mit Django und wechselte Jobs
Führen Sie TensorFlow auf einer GPU-Instanz in AWS aus
Ein Memo, das ein Tutorial zum Ausführen von Python auf Heroku erstellt hat
Führen Sie regelmäßig Python-Programme auf AWS Lambda aus
Erstellen Sie mit pulumi eine WardPress-Umgebung auf AWS
Versuchen Sie Tensorflow mit einer GPU-Instanz unter AWS
Eine Geschichte über das Ausführen von Python auf PHP auf Heroku
Versuchen Sie, Schedule auszuführen, um Instanzen in AWS Lambda (Python) zu starten und zu stoppen.
Jupyter auf AWS
Eine kleine Geschichte, die beim Schreiben von Twilio-Anwendungen mit Python auf AWS Lambda süchtig macht
# 2 Erstellen Sie eine Python-Umgebung mit einer EC2-Instanz von AWS (ubuntu18.04).
So stellen Sie eine Django-Anwendung in der Alibaba-Cloud bereit
Lassen Sie einen Papagei LINE Bot mit AWS Cloud9 zurückgeben
Stellen Sie die Django-Anwendung in Google App Engine (Python3) bereit.
Richten Sie in 30 Minuten einen kostenlosen Server unter AWS ein
Vorgehensweise zum Erstellen eines Linienbot in AWS Lambda