当前位置:网站首页>Série de microservices - compréhension approfondie des principes sous - jacents et des pratiques de conception du CPR

Série de microservices - compréhension approfondie des principes sous - jacents et des pratiques de conception du CPR

2022-01-15 02:05:41 Danny... Idea

Dans les microservices,Les appels à distance entre les services doivent tenir compte d'une variété de scénarios,Voici quelques exemples d'exceptions:

  • Appel de temporisation

  • Échec de retry

  • Avis de fin de service

  • Avis de mise en ligne du service

  • Groupes de services

  • File d'attente des demandes

Attendez un peu!…

Il y a aussi des technologues nationaux qui ont une vision claire de ces technologies et qui ont une connaissance précoce de ces technologies,Le développement d'un intergiciel d'invocation de service à distance a donc commencé très tôt.Doucement.,Certaines grandes usines nationales ont fait leurs propres recherchesRPCLe cadre d'appel a commencé à se transformer en un produit qui a été mis sur le marché.

Au début de cette année,J'ai passé environ un mois et demi de mon temps libre à peaufiner moi - mêmeRPCCadre,Après avoir essayé de pratiquer, j'ai découvert que,Pour vraiment mettre en place unRPCLe cadre est vraiment plus difficile que prévu.Cet article ne va pas trop loin pour expliquer comment le code source sous - jacent d'un Middleware sur le marché est exécuté et écrit,Plus à travers la combinaison de quelques principes de conception de bas niveau de l'intergiciel Comment j'ai conçu unRPCCadre.

Travaux préparatoires

Pour écrire unRPCCadre,J'ai probablement préparé ces travaux techniques:

  • Je l'ai lu.DubboBeaucoup de conception de code source à l'intérieur.

  • En savoir plusRPCLes difficultés et les points douloureux de la conception du cadre.(Merci beaucoupnxMon frère est.,Après avoir suivi son cours, de nombreux points techniques ont acquis une nouvelle compréhension et une nouvelle perception)

  • Pratique et test continus.

  • Comment les intergiciels écrits par vous - même peuvent être accédés élégammentSpringConteneur.

RPCL'idée de conception globale de

Au début, il était en train de concevoirRPCQuand le cadre est appelé à distance, L'idée principale de la conception est d'adopter le producteur classique - L'esprit du consommateur .Le client envoie la demande, Une fois reçu par le serveur, il correspond aux méthodes de service locales existantes pour l'exécution du traitement .

Mais en arrivant au sol, , La complexité technique est bien plus grande que prévu ~~

Les résultats définitifs sont présentés ci - dessous:

Organisation de la structure des paquets pour l'ensemble du projet
Insérer la description de l'image ici
Appel du client:
Insérer la description de l'image ici
Utilisation côté serveur:
Insérer la description de l'image ici
ps: Chacun de ces api Et la plupart des idées de design sont imitées Dubbo Conception du code source à l'intérieur du cadre et adaptation partielle .

Conception de l'agent local

Pour s'assurer que les appels de méthodes distantes sont aussi simples à utiliser que les appels de méthodes locales , Il est généralement possible d'utiliser le mode proxy pour implémenter . Le modèle proxy du scénario est bon 2Grandes catégories:Agents statiques et dynamiques, L'agent statique doit être implémenté par codage dur ,C'est irréaliste., Ce n'est pas juste ici .

Il existe deux types principaux d'agents dynamiques :

  • JDKAgents
  • CGLIBAgents

JavaLe proxy dynamique est donné,Les agents dynamiques ont les caractéristiques suivantes:

1.ProxyL'objet n'a pas besoin deimplementsInterface;
2.ProxyUtilisation de la construction d'objetsJDKDeApi,InJVMConstruction dynamique en mémoireProxyObjet.À utiliserjava.lang.reflect.ProxyClassenewProxyInstanceInterface


public static <T> T getProxy(final Class interfaceClass, ReferenceConfig referenceConfig) {
    

        return (T) Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class[]{
    interfaceClass}, new InvocationHandler() {
    
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
                // Ceci est rappelé chaque fois que la méthode cible est exécutée invokeDirection des méthodes
                return null;
            }
        });
    }

JDKExigences relatives aux agents dynamiquestargetL'objet est l'implémentation d'une interface,SitargetL'objet n'est qu'un seul objet,Aucune interface n'est implémentée,C'est là queCglibAgents(Code Generation Library),En construisant un objet de sous - classe,Afin de réalisertargetProxy of Objects,L'objet cible ne peut donc pas êtrefinalCatégorie(Erreur signalée),Et la méthode de l'objet cible ne peut pas êtrefinalOustatic(Ne pas exécuter la fonction Proxy).


 //Créer un objet proxy pour l'objet cible
    public Object getProxyInstance() {
    
        //Classe d'outils
        Enhancer en = new Enhancer();
        //Définir la classe mère
        en.setSuperclass(target.getClass());
        //Définir la fonction de rappel
        en.setCallback(this);
        //Créer un objet proxy de sous - classe
        return en.create();
    }

    public Object intercept(Object object, Method method, Object[] arg2, MethodProxy proxy) throws Throwable {
    

        System.out.println("before");
        Object obj = method.invoke(target);
        System.out.println("after");
        return obj;
    }

J'ai finalement choisiJDK En tant qu'implémentation de base de proxy dynamique , La sélection technique initiale n'a pas choisi la solution la plus parfaite , Au lieu de cela, la technologie la plus simple et la plus familière .

Si le lecteur est intéressé , Lisez mon introduction précédente aopArticle sur les principes, Explication détaillée à l'intérieur cglib Détails du principe sous - jacent . Cliquez ici pour sauter

Transfert de données pour les appels à distance

Une fois que l'agent local a été conçu , Il faut réfléchir à la façon d'envoyer les données au serveur .Au rez - de - chaussée,nettyCadre, Pour éviter les problèmes de collage et de déballage ,J'ai essayé d'utiliserObjectEncoderEtObjectDecoderDeux.nettyComposants intégrés.

À propos denetty Un sac collant apparaît à l'intérieur , La solution au phénomène du déballage , Vous pouvez lire cet article en détail :

https://www.cnblogs.com/rickiyang/p/12904552.html

Quels champs doivent être conçus à l'intérieur du corps du Protocole ?
J'ai dû trier le Code , La structure de base est la suivante: :

public class IettyProtocol implements Serializable {
    

    private static final long serialVersionUID = -7523782352702351753L;
    /** * Nombre magique */
    protected long MAGIC = 0;

    /** * Demande du clientid */
    private String requestId;

    /** * nettyExclusif */
    private ChannelHandlerContext channelHandlerContext;

    /** * 0Demande 1Réponse * @see CommonConstants.ReqOrRespTypeEnum */
    protected byte reqOrResp = 0;

    /** * 0 Les données doivent être retournées du serveur  1 Il n'est pas nécessaire de répondre aux données du serveur  */
    protected final byte way = 0;

    /** * 0C'est le rythme cardiaque.,1 Pas un battement de cœur  */
    private byte event = 0;

    /** * Type de sérialisation */
    private String serializationType;

    /** * Statut */
    private short status;

    /** *  Format du type de données retourné  */
    private Type type;

    /** * Corps du message  Type de fonction envoyé par le demandeur , L'information sur les paramètres existe ici ,  L'information sur la réponse du destinataire existe également ici  */
    private byte[] body;
}

Expliquer quelques champs :

requestId Demande du clientid( Utilisé pour demander une réponse nécessaire , Comme indiqué ci - dessous )

reqOrResp Type de paquet de protocole ( Indique si le paquet est un type de demande ou de réponse )

type Renvoie le type de format de données de retour qui appelle la méthode (Par exempleint,String, Les types de retour sont très utiles lors de la sérialisation des données )

body C'est la clé. , Nom du Service d'appel principal ,Paramètres, Les détails tels que la méthode sont d'abord convertis en un tableau d'octets , Puis il est envoyé sur le réseau .

Comment convertir des données de différents formats en tableaux d'octets

Type numérique

Convertir les types de nombres en binaires , Dans un article précédent, j'a i écrit un mécanisme d'implémentation sous - jacent détaillé , Le noyau est en déplaçant le nombre binaire d'un nombre vers la droite 8Bits,Et le déposer dansbyteDans le tableau.Le Code de base est:

/** *  Octets en chiffres  int La taille est4Octets * * @param bytes * @return */
    public static int byteToInt(byte[] bytes) {
    
        if (bytes.length != 4) {
    
            return 0;
        }
        return (bytes[0]) & 0xff | (bytes[1] << 8) & 0xff00 | (bytes[2] << 16) & 0xff0000 | (bytes[3] << 24) & 0xff000000;
    }

    /** *  Nombre en octets  int La taille est4Octets * * @param n * @return */
    public static byte[] intToByte(int n) {
    
        byte[] buf = new byte[4];
        for (int i = 0; i < buf.length; i++) {
    
            buf[i] = (byte) (n >> (8 * i));
        }
        return buf;
    }

Type de chaîne

Convertir une chaîne en un tableau de caractères correspondant , Et puis chaque tableau charType d'utilisationasc Le Code correspond au nombre , Puis il y a le retour à l'idée de conversion numérique. .

Ensemble,Types d'objets complexes

Ces types peuvent essayer de passer en premier jsonConvertir en chaîne, Puis convertissez la chaîne en charTableau, Conversion au type de tableau numérique , Et revenir à l'idée de la conversion numérique .

Conception de la réception et de la réponse des données

C'est déjà fait. RPC Quand la communication a été conçue , Adopter un modèle simple producteur - consommateur . Voici quelques - uns des points que j'ai pensé au début de ma mise en oeuvre :
Envoyer les données de façon synchrone
Insérer la description de l'image ici
Dans ce cas de conception de transmission synchrone ,consumer Après l'envoi des données ,consumerSera toujours en attente, Attendez que les données arrivent provider Une fois terminé ,consumer La fin va continuer .

Les inconvénients d'une telle conception sont évidents :
consumerEtprovider Le débit n'est pas élevé , Et une fois qu'une interface a un temps d'arrêt, il peut affecter le blocage des appels d'autres interfaces .
consumer Envoi asynchrone Final ,provider Traitement de réception asynchrone Terminal
Deux nouveaux concepts doivent être introduits ici ,ioThreads et Business Threads. La conception générale est illustrée ci - dessous :
Insérer la description de l'image ici
Quand le client envoie les données , Ce n'est plus un état d'attente , Il suffit de mettre les données dans une file d'attente de demandes locale .Clientio Le thread essaie constamment de récupérer les données de la file d'attente , Puis envoyez le réseau . Le serveur aura aussi un io Le thread est responsable de la réception de ces données , Les données sont ensuite placées dans un tampon de file d'attente côté serveur , Ensuite, il est remis au pool de Threads d'affaires du serveur pour consommer lentement les données à l'intérieur de la file d'attente tampon du serveur .

La conception de base du serveur est la suivante :
Insérer la description de l'image ici
provider Comment retourner correctement après le traitement des données ?
Pour résoudre ce problème, J'ai essayé de lire. DubboCode source sous - jacent pour, Ensuite, nous avons utilisé l'idée de conception pour réaliser une vague .

Comment un client reçoit une réponse
L'essence de son noyau est que le client génère un requestId, Puis le client, après avoir envoyé les données ,Il y en aura un.MapEnsemble(key- Oui.requestId,value Est la valeur de réponse de l'interface ) L'interface de gestion répond aux données , Le thread d'appel du client doit constamment écouter après avoir effectué l'écriture des données à la file d'attente d'envoi MapCorrespondance dans l'ensemblerequestIdDevalueY a - t - il une valeur?, Si aucune donnée n'est disponible au - delà du temps spécifié , Alors Lancez l'exception Timeout , Si les données de réponse sont reçues, elles sont retournées normalement. .

Réponse du serveur
Le code local du serveur écrit les données à un MapEnsemble,Côté serveurio Le fil va continuer à tourner autour de ce MapEnsemble(key C'est le client qui a envoyé requestId,value Est les données écrites après le traitement du code local ), Si vous trouvez requestId Il y a des données de retour écrites , Il sera envoyé au client .

La conception globale est approximativement comme indiqué ci - dessous :
Insérer la description de l'image ici

Conception du filtre

Le lien d'appel de base est comme décrit ci - dessus. . Voici quelques modules de fonctions étendues .
Certains emballages décoratifs sont nécessaires pour l'expédition , Et les fonctions connexes de filtrage . À ce stade, la conception peut être effectuée de manière responsable. .
Insérer la description de l'image ici
J'ai probablement deux types de filtres , L'un est le filtre utilisé par le consommateur , L'un est un filtre exclusif au fournisseur de services .
La conception de la partie filtre a été réalisée principalement avec le modèle de la chaîne de responsabilité ,C'est plus simple., Je n'ai pas l'intention de faire trop de présentations .
Insérer la description de l'image ici

Conception des tâches retardées

Dans l'intergiciel pour les appels de microservice , Les tâches retardées sont une conception souvent utilisée , Par exemple, retry dans le délai imparti , Battements de cœur chronométrés envoyés , La publication du Registre n'a pas réussi à réessayer et dans d'autres scénarios . Son noyau commun est d'exécuter une tâche à un moment donné après l'horodatage actuel . J'ai vu ce genre de design. JDKÀ l'intérieurTimerEtDelayedQueue Principes de conception .

NormalJDK De java.util.Timer Et DelayedQueue Classe d'outils égaux, Des tâches simples et chronométrées peuvent être réalisées , Au rez - de - chaussée, on utilise une structure de données en tas , La complexité de l'accès est O(nlog(n)), Impossible de supporter une tâche de synchronisation massive .

Et il y a beaucoup de tâches à accomplir 、Scénario exigeant des performances élevées, Pour réduire la complexité du temps d'accès et d'annulation des tâches à O(1), Le système de rotation du temps sera utilisé .
Dans sa propre réalisationRPCDans le cadre, On a tenté d'utiliser le mécanisme de la roue temporelle pour réaliser la partie d'envoi du paquet Heartbeat .
Insérer la description de l'image ici
Qu'est - ce que la roue du temps

Un modèle d'ordonnancement efficace des tâches de gestion des lots . La roue temporelle est généralement une structure circulaire , Comme une horloge , Divisé en beaucoup de fentes , Une fente représente un intervalle de temps , Chaque fente utilise une liste bidirectionnelle pour stocker les tâches programmées . Le pointeur bat périodiquement , Sauter dans une fente , Exécutez la tâche de synchronisation pour cette fente .
Insérer la description de l'image ici
Dubbo La mise en œuvre de la roue temporelle pour dubbo-common Du module org.apache.dubbo.common.timer Dans le sac, Si les amis intéressés peuvent lire en profondeur la conception et la mise en oeuvre du code source interne .

Introduction du Registre

Afin de s'assurer que les appelants de chaque service sont informés en temps opportun après la publication du service , La conception du registre est essentielle .En plus de ça,, Le rôle du registre permet également une meilleure coordination de certains paramètres de configuration entre les différents appels de microservice , Par exemple, poids ,Groupe, Propriétés telles que l'isolement des versions .

Dans le processus de réalisation de l'atterrissage ,J'ai choisizookeeper En tant que registre par défaut . Pour faciliter les extensions ultérieures , C'est aussi une référence Dubbo Réflexions internes sur la réalisation du registre ,Par unRegistry Résumé de l'interface pour , Des extensions aléatoires de quelques classes de modèles, etc . La conception générale est illustrée ci - dessous :
Insérer la description de l'image ici
Le Code d'interface d'enregistrement de service global est le suivant :

public interface RegistryService {
    


    /** * Inscriptionurl * * Oui.dubbo Le Service écrit au noeud du registre  *  Une pratique de retry appropriée est nécessaire en cas de nervosité du réseau  * Services d'inscriptionurl Doit être écrit dans le fichier de persistance  * * @param url */
    void register(URL url);

    /** * Service hors ligne * *  Le noeud persistant ne peut pas être déconnecté du Service  *  Les services hors ligne doivent garantir url C'est une correspondance complète  *  Supprimer certaines informations de contenu du fichier persistant  * * @param url */
    void unRegister(URL url);

    /** *  Services d'abonnement des consommateurs  * * @param urlStr * @param providerServiceName */
    void subscribe(String urlStr,String providerServiceName);

    /** *  Aviser ici après la mise à jour des propriétés du noeud  * * @param url */
    void doSubscribeAfterUpdate(URL url);


    /** *  Aviser ici après l'ajout d'un nouveau noeud  * * @param url */
    void doSubscribeAfterAdd(URL url);


    /** *  Exécuter la logique interne de désabonnement  * * @param url */
    void doUnSubscribe(URL url);
}

Pour éviter que le registre ne soit suspendu , Le service ne peut pas communiquer , Chaque nœud de communication zk L'information sur le noeud d'enregistrement du service est pré - persistante localement à l'avance pour mettre en scène une donnée , Afin de garantir la disponibilité d'un service .
Insérer la description de l'image ici
Insérer la description de l'image ici

Mise en œuvre de la stratégie d'équilibrage des charges

Quand le cluster appelle , Il y aura inévitablement des problèmes d'équilibrage de la charge , J'ai fait référence à cette logique de conception Dubbo L'idée de conception de spi La façon dont les composants sont chargés pour l'injection du cadre .

L'unité extrait un LoadBalanceInterface pour, Ensuite, la couche inférieure met en œuvre une stratégie spécifique d'équilibrage de la charge :

public class WeightLoadBalance implements LoadBalance {
    

    public static Map<String, URL[]> randomWeightMap = new ConcurrentHashMap<>();

    public static Map<String, Integer> lastIndexVisitMap = new ConcurrentHashMap<>();

    @Override
    public void doSelect(Invocation invocation) {
    
        URL[] weightArr = randomWeightMap.get(invocation.getServiceName());
        if (weightArr == null) {
    
            List<URL> urls = invocation.getUrls();
            Integer totalWeight = 0;
            for (URL url : urls) {
    
                //weight Si le réglage est trop grand , Il est facile de provoquer une utilisation excessive de la mémoire ,Alors...weight La taille maximale de la limite unifiée devrait être 100
                Integer weight = Integer.valueOf(url.getParameters().get("weight"));
                totalWeight += weight;
            }
            weightArr = new URL[totalWeight];
            RandomList<URL> randomList = new RandomList(totalWeight);
            for (URL url : urls) {
    
                int weight = Integer.parseInt(url.getParameters().get("weight"));
                for (int i = 0; i < weight; i++) {
    
                    randomList.randomAdd(url);
                }
            }
            int len = randomList.getRandomList().size();
            for (int i = 0; i < len; i++) {
    
                URL url = randomList.getRandomList().get(i);
                weightArr[i] = url;
            }
            randomWeightMap.put(invocation.getServiceName(), weightArr);
        }
        Integer lastIndex = lastIndexVisitMap.get(invocation.getServiceName());
        if (lastIndex == null) {
    
            lastIndex = 0;
        }
        if (lastIndex >= weightArr.length) {
    
            lastIndex = 0;
        }
        URL referUrl = weightArr[lastIndex];
        lastIndex++;
        lastIndexVisitMap.put(invocation.getServiceName(), lastIndex);
        invocation.setReferUrl(referUrl);
    }

}

La mise en œuvre de l'équilibrage de la charge n'est pas une idée de calcul en temps réel , Au lieu de cela, un ensemble de séquences d'appels est calculé au hasard à l'avance , Ensuite, chaque fois qu'une demande est faite, les appels de service sont envoyés à tour de rôle selon ce tableau déjà aléatoire .

Cela évite les frais généraux de performance qui nécessitent des calculs de filtrage en temps réel de la machine à chaque demande .

SPI Conception du mécanisme d'extension

En fait...Spi La clé de la mise en œuvre du chargement est d'écrire un profil dans le format spécifié , Et à travers un loader L'objet charge chaque classe à l'intérieur du fichier de configuration dans une copie à l'avance MapPour gérer.

Je vais vous donner un exemple simple de ma propre écriture. , Mais ne contient pas d'adaptation spiChargement etspi Fonction d'injection automatique interne .

public class ExtensionLoader {
    

    /** * Extension du stockagespiDemap,key- Oui.spi Écrit dans le fichier key */
    private static Map<String, Class<?>> extensionClassMap = new ConcurrentHashMap<>();

    private static final String EXTENSION_LOADER_DIR_PREFIX = "META-INF/ietty/";

    public static  Map<String, Class<?>> getExtensionClassMap(){
    
        return extensionClassMap;
    }

    public void loadDirectory(Class clazz) throws IOException {
    
        synchronized (ExtensionLoader.class){
    
            String fileName = EXTENSION_LOADER_DIR_PREFIX + clazz.getName();
            ClassLoader classLoader = this.getClass().getClassLoader();
            Enumeration<URL> enumeration = classLoader.getResources(fileName);
            while (enumeration.hasMoreElements()) {
    
                URL url = enumeration.nextElement();
                InputStreamReader inputStreamReader = new InputStreamReader(url.openStream(), "utf-8");
                BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
                String line;
                while ((line = bufferedReader.readLine()) != null) {
    
                    if(line.startsWith("#")){
    
                        continue;
                    }
                    String[] keyClassInstance = line.split("=");
                    try {
    
                        extensionClassMap.put(keyClassInstance[0],Class.forName(keyClassInstance[1],true,classLoader));
                    } catch (ClassNotFoundException e) {
    
                        e.printStackTrace();
                    }

                }
            }
        }
    }

    public static <T>Object initClassInstance(String className) {
    
        if(extensionClassMap!=null && extensionClassMap.size()>0){
    
            try {
    
                return (T)extensionClassMap.get(className).newInstance();
            } catch (InstantiationException e) {
    
                e.printStackTrace();
            } catch (IllegalAccessException e) {
    
                e.printStackTrace();
            }
        }
        return null;
    }

}

Composants de communication sous - jacents

Ensemble completRPC La partie inférieure du cadre est Netty Mise en œuvre par les composants , L'écriture principale est en fait générique netty Il n'y a pas beaucoup de différence dans la programmation , Voici une capture d'écran du Code :

Client:
Insérer la description de l'image ici
Serveur:
Insérer la description de l'image ici

Résumé

Peut - être que tout l'article a été écrit , Beaucoup de détails techniques et d'implémentations ne peuvent pas être bien montrés en raison de problèmes d'espace . Mais plusieurs grandes difficultés de conception globale et des solutions aux difficultés sont essentiellement affichées , J'espère pouvoir vous inspirer .

J'ai l'impression qu'il y a eu beaucoup de perte de cheveux après que tout l'intergiciel de base ait été écrit , Parce qu'il y a tellement de détails au rez - de - chaussée , Qu'il s'agisse de la conception de la structure ,Problèmes de concurrence des données, La conception du traitement asynchrone et beaucoup d'autres choses à considérer , C'est donc un défi très complet .

版权声明
本文为[Danny... Idea]所创,转载请带上原文链接,感谢
https://chowdera.com/2022/01/202201080559030021.html

随机推荐