[PYTHON] Envisagez une application WebSocket native pour le cloud qui s'exécute sur AWS

introduction

Le mot natif de nuage est populaire. Hier, @chiyoppy a écrit Article sur les microservises et le cloud. Je pense que c'est un bon article qui résume brièvement les éléments importants lors de la conception d'un service Web dans le cloud. En réponse à cela, je réfléchirai à la manière de créer une application WebSocket native pour le cloud. Dans l'article d'hier, HTTP est sans état sur les applications, donc c'est incroyable! Vous pouvez évoluer! Dans cet article, je considérerai une application WebSocket qui semble plus difficile à mettre à l'échelle que HTTP.

Au fait, je suis un nouveau diplômé en 2015 et au travail, j'utilise Swift pour écrire des applications iOS. Et c'est aujourd'hui mon anniversaire. Nous attendons un cadeau. De plus, j'écris des applications iOS dans Swift dans mon travail, donc cet article n'a rien à voir avec mon travail dans Dwango.

La raison pour laquelle j'ai choisi ce sujet cette fois est que j'étudie actuellement les natifs du cloud et les microservis lors d'une session d'étude organisée environ une fois par semaine par de nouveaux diplômés de Dwango. Tout en les étudiant, que dois-je faire ** Comment faire une application WebSocket qui soit facile à avoir un état sur le serveur lui-même **? La question s'est posée. Dans cet article, nous envisagerons de concevoir une application WebSocket qui s'exécute dans le cloud tout en créant une petite application de chat.

Cloud natif

Pour faire simple, le cloud natif consiste à concevoir une application en partant du principe qu'elle fonctionnera sur le cloud. Vous devez également penser à la conteneurisation, au déploiement automatique, à la mise à l'échelle automatique et à la conception d'applications à exécuter sur le cloud. Dans cet article, nous envisagerons de concevoir une application qui s'exécute dans le cloud. En ce qui concerne les autres choses, Chiyoppy aurait dû résumer diverses choses hier, je vais donc l'omettre cette fois. Dans cet article, je vais l'écrire en supposant qu'il fonctionnera sur AWS, donc j'utiliserai de plus en plus les termes AWS.

Conditions requises pour les applications cloud natives

Afin de créer une application qui s'exécute dans le cloud, il est nécessaire de prendre en compte les caractéristiques du cloud. Un exemple typique est que le composant n'est pas fiable. En tant qu'exigence pour les applications, il existe une prémisse selon laquelle EC2 peut soudainement tomber.

--EC2 n'a pas d'état

Il y a quelque chose comme ça. Dans le cas de HTTP, des applications de mise à l'échelle sans état sont nécessaires. L'application WebSocket créée cette fois a une idée similaire

Je viserai les choses. Le troisième point est propre à WebSocket, auquel vous n'avez pas besoin de penser dans le cas de HTTP.

Pour faire simple, l'application WebSocket que nous visons cette fois est dans un état où ** deux instances A et B sont placées dans deux AZ, et l'utilisateur n'a pas l'impression que l'application a été abandonnée lorsque A est abandonné manuellement **. Je vise à être. L'application WebSocket conserve la connexion, il est donc nécessaire de reconnecter avec succès la connexion qui a été perdue en raison de la panne du serveur. Le but est de créer une application qui ne s'arrête pas (une application qui tombe en panne mais que l'utilisateur ne remarque pas) en échouant automatiquement la connexion de A à B et en démarrant automatiquement un nouveau A entre-temps. Cette fois, le but est de réaliser la partie écrite en gras.

Tout d'abord, essayez d'écrire sans être conscient des natifs du cloud

Écrivons une application sans rien savoir pour le moment. Cette fois, nous avons adopté le chat comme exemple d'application. Le code de cet état est ici. https://github.com/kouki-dan/CloudChat/tree/basic-chat Une fois lancé, un champ de texte apparaîtra. Si vous tapez une lettre là-bas, elle sera synchronisée avec d'autres utilisateurs, ce qui est exactement un chat.

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

Description du code

Cette classe est responsable de la communication WebSocket.

chat.py


class ChatSocketHandler(tornado.websocket.WebSocketHandler):
    #Devenir une variable de classe(ChatSocketHandler.Accès avec xxx, cls dans la méthode de classe.(Accessible avec xxx)
    waiters = set() #Un ensemble d'utilisateurs actuellement connectés
    cache = [] #Historique du chat

    def open(self): #Méthode appelée lorsque WebSocket est ouvert
        ChatSocketHandler.waiters.add(self)
        self.write_message({"chats": ChatSocketHandler.cache}) #Envoyer l'historique lors de la connexion

    def on_close(self): #Méthode appelée lorsque WebSocket est déconnecté
        ChatSocketHandler.waiters.remove(self)

    @classmethod
    def update_cache(cls, chat):
        cls.cache.append(chat) #Ajouter l'historique

    @classmethod
    def send_updates(cls, chat): #Diffuser le chat à tous les utilisateurs
        for waiter in cls.waiters:
            try:
                waiter.write_message(chat)
            except:
                logging.error("Error sending message", exc_info=True)

    def on_message(self, message): #Méthode appelée lorsqu'un message est reçu via WebSocket
        parsed = tornado.escape.json_decode(message)
        chat = {
            "body": parsed["body"],
            }

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

Je suis désolé si vous ne pouvez pas lire Python. S'il vous plaît ressentez Python est un langage relativement facile à lire, vous devriez donc le ressentir. Maintenant, ce que nous faisons, c'est garder l'historique et la diffusion lorsque le chat est envoyé. Même dans cet état, il fonctionne autant qu'il peut être exploité par lui-même, mais étant donné qu'il peut être exploité sur le cloud, il existe les problèmes suivants dans l'état actuel.

--L'application a un état --Dispose d'un cache (historique des discussions) en mémoire

Tout d'abord, essayons de ne pas donner à l'application un état (cache). Cette fois, nous utiliserons Redis et changerons le cache pour qu'il soit stocké dans la liste de Redis. AWS ElastiCache est pratique car vous pouvez facilement utiliser Redis qui peut être connecté à partir de plusieurs zones de disponibilité.

Discuter sans état dans l'application

Jusqu'à présent, les données de discussion étaient stockées en mémoire dans une variable appelée cache. Changeons-le en une configuration qui ne perd pas de données même si EC2 tombe en le donnant à Redis. Le résultat de la mise en œuvre est le suivant https://github.com/kouki-dan/CloudChat/tree/cache-in-redis

Jetons un coup d'œil à diff.

chat.diff


 class ChatSocketHandler(tornado.websocket.WebSocketHandler):
     waiters = set()
-    cache = [] #Le cache a été supprimé car il est stocké dans Redis
+    redis = Redis(decode_responses=True) #↑ C'est une instance pour enregistrer le cache sur redis
 
     def open(self):
         ChatSocketHandler.waiters.add(self)
         #Modifié pour obtenir ce qui faisait directement référence au cache via la méthode
-        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):
         #Changé pour enregistrer dans Redis au lieu de dans la mémoire
-        cls.cache.append(chat)
+        chat_id = cls.redis.incr("nextChatId") #Obtenir l'identifiant de chat de Redis
+        redis_chat_key = "chat:{}".format(chat_id) #Créer une clé de destination d'enregistrement(chat:1, chat:2 sera comme)
+
+        cls.redis.set(redis_chat_key, json.dumps(chat)) #Seules les chaînes peuvent être définies dans Redis, donc enregistrez-les en tant que chaînes JSON
+        cls.redis.rpush("chats", redis_chat_key) #Ajouter une clé de chat à la liste de chat
+
+    @classmethod
+    def get_caches(cls):
+      #Méthode pour obtenir la liste de discussion de Redis
+      chat_ids = cls.redis.lrange("chats", 0, -1) #Obtenez tout le chat
+      chats = []
+      for chat_id in chat_ids:
+        chat = json.loads(cls.redis.get(chat_id)) #Conversion d'une chaîne JSON en type de dictionnaire géré par Python
+        chats.append(chat)
+      return chats #Renvoie un tableau de type dictionnaire

Pour ceux qui ne peuvent pas lire Python, j'ai écrit la plupart de ce dont j'ai besoin dans les commentaires. En utilisant Redis pour la partie comptabilisation et la partie acquisition, les données ne sont pas perdues même si EC2 tombe. Un autre effet secondaire du placement des données en dehors de l'instance est que plusieurs instances peuvent voir les mêmes données. Cela nous rapproche d'une configuration évolutive. Essayez de lancer plusieurs instances.

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

Sur cette photo, j'ai changé le port et lancé deux applications. Dans les applications précédentes, le contenu du chat n'était pas synchronisé entre les instances, mais l'enregistrement des données dans Redis affichera le même contenu. Mais ce n'est pas encore une extension parfaite. Le statut de l'historique des discussions est synchronisé au démarrage, mais l'envoi et la réception des messages en temps réel ne sont pas synchronisés entre les instances, donc même si vous envoyez une discussion à localhost: 8888 comme indiqué dans l'image ci-dessous, le résultat n'atteint pas localhost: 8899. Ce sera. L'inverse est également vrai. スクリーンショット 2015-12-07 16.13.19.png

Résolvons maintenant ce problème et considérons une configuration qui peut être complètement mise à l'échelle.

Application WebSocket pour évoluer

Pour évoluer, vous devez notifier les autres instances des événements qui se produisent dans une instance. Dans l'exemple ci-dessus, les données référencées par l'utilisateur étaient incohérentes car il n'y avait aucun mécanisme en place pour notifier localhost: 8899 de l'événement dans lequel le chat qui s'est produit sur localhost: 8888 a été publié. Vous pouvez également utiliser Redis pour résoudre ce problème. Passons à un chat évolutif en utilisant le mécanisme PubSub fourni par Redis.

Qu'est-ce que PubSub

PubSub signifie Publish (Pub) et Subscribe (Sub) et est utilisé pour la communication entre les processus et les applications. Vous pouvez publier un événement sur une connexion, et l'événement sera notifié à la connexion pré-abonnée.

Chattez pour évoluer

Nous améliorerons la communication entre les instances afin qu'elle puisse être mise à l'échelle à l'aide de PubSub. Le code qui implémente ceci est ci-dessous https://github.com/kouki-dan/CloudChat/tree/pubsub-chat

Jetons maintenant un coup d'œil au diff

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)
+

Tout d'abord, nous créons une classe à souscrire. Si vous gérez Subscribe dans le thread principal, il verrouille le thread principal, donc cette partie est gérée en multi-thread. Nous utilisons le module de threading du module standard Python pour le multithreading. Cette classe émet «ChatSocketHandler.send_updates (chat)» lorsque le message est publié. Cela vous permet d'envoyer un événement indiquant qu'une discussion a été envoyée à tous les utilisateurs connectés à chaque instance.

Ensuite, jetons un coup d'œil à la partie principale du chat

chat.diff


 class ChatSocketHandler(tornado.websocket.WebSocketHandler):
     waiters = set()
     redis = Redis(decode_responses=True)
+    #Ajouter un auditeur
+    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]
+        }))

Le seul changement est que la partie qui appelait directement send_updates a été changée en redis publish. En conséquence, la communication est effectuée comme suit. (Je voulais faire un dessin, mais j'ai renoncé au manque d'outils. Je pourrais dessiner un dessin un jour. TODO)

utilisateur
   ↓ (Envoyer un chat via WebSocket)
Chaque instance
   ↓ (Publish)
  Redis
   ↓ (Notifier les instances abonnées)
Instance A,Instance B,....(Toutes les instances)
   ↓ (Envoyer des messages de discussion via WebSocket aux utilisateurs connectés à chaque instance)
utilisateur

Le résultat est une application de chat évolutive qui est entièrement synchronisée entre les instances. La partie application est maintenant terminée. Ce qui manque pour viser une application cloud native, c'est le basculement en cas de panne. Cela ne peut pas être aidé du côté du serveur, alors allons-y pour nous reconnecter depuis le client.

Basculement de l'application WebSocket

Jusqu'à présent, nous avons créé une application WebSocket qui peut être mise à l'échelle. Afin d'utiliser le cloud où la fiabilité n'est pas garantie dans une instance, il est important de savoir comment basculer en cas de panne. Ceci peut être réalisé en reconnectant toutes les connexions qui étaient connectées à A lorsque l'instance de A est descendue vers l'instance de B, en supposant que deux instances d'AB ont été démarrées.

Comme d'habitude, nous commencerons par l'URL qui implémente cela. https://github.com/kouki-dan/CloudChat/tree/reconnect-for-failover

Jusqu'à présent, toutes les discussions étaient envoyées depuis le serveur à chaque fois qu'une connexion était établie, mais comme les informations de discussion ne sont pas requises lors de la reconnexion, le code côté serveur a également été légèrement modifié. Jetons un coup d'œil au code client qui prend en charge la reconnexion.

index.js


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

      //Connexion à l'ouverture de l'URL
      connect(function() {
        //Gestionnaire en cas d'erreur
        alert("connection error"); 
      }, closeFunction, function() {
        //Gestionnaire à la connexion
        //Demander l'historique des discussions lors de la première connexion
        socket.send(JSON.stringify({"type":"command", "command":"requestFirstChat"}))
      });

      //Fonction de connexion à WebSocket
      function connect(errorFunction, closeFunction, openFunction) {
        var url = "ws://" + location.host + "/chatsocket";
        socket = new WebSocket(url);
        // |Lorsque vous recevez un message, c'est un processus courant
        socket.onmessage = function(event) {
          receiveChat(JSON.parse(event.data)["chats"]) //Fonctions reflétées dans DOM
        }
        // |Étant donné que le traitement des erreurs, etc. est différent entre la première fois et la reconnexion, il doit être donné en argument
        socket.onclose = closeFunction;
        socket.onerror = errorFunction;
        socket.onopen = openFunction;
      }

      //Fonctionne lorsque la connexion est fermée en raison d'une chute d'EC2, etc.
      function closeFunction(event) {
        var retry = 0;
        setTimeout(function() {
          var callee = arguments.callee; //Pour vous appeler à plusieurs reprises avec setTimeout
          connect(function() {
            //Fonction en cas d'erreur
            retry++;
            //Essayez de vous reconnecter en vous appelant jusqu'à 3 fois
            if(retry < 3) { 
              setTimeout(function() {
                callee();
              }, 3000);
            } else {
              alert("connection error");
            }
          }, null /*close ne peut pas être entré à ce stade*/, function() {
            //Puisque close est appelé même s'il est fermé en raison d'une erreur, mettez close lorsque la connexion est réussie.
            // 
            socket.onclose = closeFunction;
          });
        }, 1000);
      };
// ........

La quantité de code a doublé comme prévu initialement. N'est-ce pas juste une reconnexion? Je réfléchissais légèrement, mais au fur et à mesure que j'écrivais le code, j'ai dû écrire les uns après les autres, comme la différence entre le premier démarrage et la reconnexion, le nombre de reconnexions, etc.

Enfin, après avoir établi la connexion, si vous supprimez l'application avec ctrl + c, l'application qui se reconnectera à une autre instance est terminée. Il est nécessaire de définir et d'ajuster l'équilibreur de charge et le DNS pour fonctionner réellement comme prévu, mais ici nous ne parlerons que de la mise en œuvre de l'application.

J'ai écrit le code pendant longtemps, mais maintenant l'application que je visais est terminée. Dans l'application créée cette fois, le message envoyé lors de la reconnexion sera perdu, donc on ne peut pas dire qu'il est complètement terminé, mais disons qu'il est terminé pour le moment. Les messages envoyés sont mis en cache dans le client. Pour les messages reçus, la méthode de mise en œuvre est concevable, telle que la réception du dernier identifiant de message envoyé au client par le client et le fait que le serveur renvoie les messages qui n'ont pas pu être reçus. Si vous êtes intéressé, veuillez écrire et PR. Voilà donc pour cet article. Je vous remercie pour votre travail acharné. Le code qui fonctionne réellement est lié à github avec une balise, veuillez donc vous y référer.

Résumé

Jusqu'à présent, nous avons vu le processus d'évolution d'un chat que vous avez écrit sans penser à une application qui fonctionne dans le cloud tout en maintenant une haute disponibilité. J'écrivais cet article en écrivant le code, donc j'étais soulagé de pouvoir me reconnecter lors de la dernière suppression de l'instance. Cependant, il y a beaucoup de choses à prendre en compte lors de l'écriture d'une application qui fonctionne réellement. Il en va de même pour la session, et comme je l'ai dit, la discussion que j'ai faite ici n'est pas vraiment parfaite. Dans le cas du chat, envoyez un message direct. Cela semble difficile à traiter. L'exécution de WebSocket dans le cloud peut être très réfléchie et difficile. J'ai l'occasion d'écrire personnellement une application WebSocket, donc j'aimerais écrire un article en utilisant ce que j'ai écrit ici «J'ai en fait essayé de créer une application de communication en temps réel en utilisant WebSocket avec une conscience native du cloud». Je pense. Aussi, comme je l'ai écrit au début de cet article, le plus important est que ** mon anniversaire est aujourd'hui **. Nous attendons votre message de félicitations et présent.

Je veux lire ça aussi

L'URL à laquelle vous vous référez, l'URL que vous souhaitez lire, etc.

demain

Il semble que Hiroppy (@about_hiroppy) écrira ** quelque chose ** demain. Qu'est-ce que ** quelque chose **? J'ai hâte d'y être.

Recommended Posts

Envisagez une application WebSocket native pour le cloud qui s'exécute sur AWS
Créer une application Web de type Flask / Bottle sur AWS Lambda avec Chalice
Lancement d'une application Web sur AWS avec django et modification des tâches
Exécutez TensorFlow sur une instance GPU sur AWS
Un mémo qui a fait un tutoriel pour exécuter python sur heroku
Exécutez régulièrement des programmes Python sur AWS Lambda
Créez un environnement WardPress sur AWS avec Pulumi
Essayez Tensorflow avec une instance GPU sur AWS
Une histoire sur l'exécution de Python sur PHP sur Heroku
Essayez d'exécuter Schedule pour démarrer et arrêter des instances dans AWS Lambda (Python)
Jupyter sur AWS
Une petite histoire à savoir comme un point addictif lors de l'écriture d'applications Twilio à l'aide de Python sur AWS Lambda
# 2 Créez un environnement Python avec une instance EC2 d'AWS (ubuntu18.04)
Comment déployer une application Django dans le cloud Alibaba
Créer un robot LINE de retour de perroquet avec AWS Cloud9
Déployer l'application Django sur Google App Engine (Python3)
Configurez un serveur gratuit sur AWS en 30 minutes
Procédure de création d'un Line Bot sur AWS Lambda