Comment utiliser Netty pour recevoir 350000 objets par seconde sur une seule machine

Il existe de nombreuses démos sur Internet qui utilisent netty et protostuff pour transmettre des objets rpc. La plupart d'entre elles sont taillées dans un moule. J'en ai également copié une au début. Le test local n'a pas été entravé et aucune anomalie ne s'est produite.

Après le déploiement de l'environnement de pré-version, après le test de résistance, il y a eu beaucoup de problèmes et divers rapports d'erreurs sont apparus à l'infini. Bien sûr, j'ai utilisé une grande quantité de données pendant le test de résistance et envoyé des requêtes très intensives. Une seule machine envoie 20 000 objets dans les 100 premières ms par seconde, et se repose pendant 900 ms et envoie dans une boucle sans fin. Un total de 40 machines sont utilisées comme clients, et 2 machines netty sont envoyées en même temps. Le serveur du serveur envoie des objets, donc en moyenne, chaque serveur reçoit environ 400 000 objets par seconde. En raison de la logique métier derrière, la logique ne peut traiter que 350 000 mesures réelles par seconde.

Le code sur Internet a été modifié à plusieurs reprises et testé à plusieurs reprises. En fin de compte, il n'a généré aucune erreur ni exception. Une seule machine peut recevoir plus de 350 000 objets par seconde. Par conséquent, écrivez un article à enregistrer. Le code dans le texte sera conforme à la logique en ligne.

Sérialisation et désérialisation de protostuff

Cela n'a rien de spécial, il suffit de trouver un outil en ligne.

Introduire pom

<protostuff.version>1.7.2</protostuff.version>
<dependency>
    <groupId>io.protostuff</groupId>
    <artifactId>protostuff-core</artifactId>
    <version>${protostuff.version}</version>
</dependency>

<dependency>
    <groupId>io.protostuff</groupId>
    <artifactId>protostuff-runtime</artifactId>
    <version>${protostuff.version}</version>
</dependency>
public class ProtostuffUtils {
    /**
     * 避免每次序列化都重新申请Buffer空间
     * 这句话在实际生产上没有意义,耗时减少的极小,但高并发下,如果还用这个buffer,会报异常说buffer还没清空,就又被使用了
     */
//    private static LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
    /**
     * 缓存Schema
     */
    private static Map<Class<?>, Schema<?>> schemaCache = new ConcurrentHashMap<>();
 
    /**
     * 序列化方法,把指定对象序列化成字节数组
     *
     * @param obj
     * @param <T>
     * @return
     */
    @SuppressWarnings("unchecked")
    public static <T> byte[] serialize(T obj) {
        Class<T> clazz = (Class<T>) obj.getClass();
        Schema<T> schema = getSchema(clazz);
        LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
        byte[] data;
        try {
            data = ProtobufIOUtil.toByteArray(obj, schema, buffer);
//            data = ProtostuffIOUtil.toByteArray(obj, schema, buffer);
        } finally {
            buffer.clear();
        }
 
        return data;
    }
 
    /**
     * 反序列化方法,将字节数组反序列化成指定Class类型
     *
     * @param data
     * @param clazz
     * @param <T>
     * @return
     */
    public static <T> T deserialize(byte[] data, Class<T> clazz) {
        Schema<T> schema = getSchema(clazz);
        T obj = schema.newMessage();
        ProtobufIOUtil.mergeFrom(data, obj, schema);
//        ProtostuffIOUtil.mergeFrom(data, obj, schema);
        return obj;
    }
 
    @SuppressWarnings("unchecked")
    private static <T> Schema<T> getSchema(Class<T> clazz) {
        Schema<T> schema = (Schema<T>) schemaCache.get(clazz);
        if (Objects.isNull(schema)) {
            //这个schema通过RuntimeSchema进行懒创建并缓存
            //所以可以一直调用RuntimeSchema.getSchema(),这个方法是线程安全的
            schema = RuntimeSchema.getSchema(clazz);
            if (Objects.nonNull(schema)) {
                schemaCache.put(clazz, schema);
            }
        }
 
        return schema;
    }
}

Il y a un trou ici, c'est-à-dire que la plupart des codes en ligne en haut utilisent des tampons statiques. Il n'y a pas de problème dans le cas monothread. Dans le cas du multithreading, il est très facile de faire en sorte qu'un tampon soit à nouveau utilisé par un autre thread sans être effacé après une utilisation, et une exception sera levée. Le soi-disant évitement de la demande d'espace tampon à chaque fois, l'impact sur les performances mesuré est extrêmement faible.

De plus, les deux ProtostuffIOUtil ont changé en ProtobufIOUtil, car il y avait aussi une anomalie, et il n'y avait pas d'anomalie après la modification.

Méthode de sérialisation personnalisée

Décodeur décodeur:

import com.jd.platform.hotkey.common.model.HotKeyMsg;
import com.jd.platform.hotkey.common.tool.ProtostuffUtils;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
 
import java.util.List;
 
/**
 * @author wuweifeng
 * @version 1.0
 * @date 2020-07-29
 */
public class MsgDecoder extends ByteToMessageDecoder {
    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf in, List<Object> list) {
        try {
 
            byte[] body = new byte[in.readableBytes()];  //传输正常
            in.readBytes(body);
 
            list.add(ProtostuffUtils.deserialize(body, HotKeyMsg.class));
 
//            if (in.readableBytes() < 4) {
//                return;
//            }
//            in.markReaderIndex();
//            int dataLength = in.readInt();
//            if (dataLength < 0) {
//                channelHandlerContext.close();
//            }
//            if (in.readableBytes() < dataLength) {
//                in.resetReaderIndex();
//                return;
//            }
//
//            byte[] data = new byte[dataLength];
//            in.readBytes(data);
//
//            Object obj = ProtostuffUtils.deserialize(data, HotKeyMsg.class);
//            list.add(obj);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Encodeur

import com.jd.platform.hotkey.common.model.HotKeyMsg;
import com.jd.platform.hotkey.common.tool.Constant;
import com.jd.platform.hotkey.common.tool.ProtostuffUtils;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
 
/**
 * @author wuweifeng
 * @version 1.0
 * @date 2020-07-30
 */
public class MsgEncoder extends MessageToByteEncoder {
 
    @Override
    public void encode(ChannelHandlerContext ctx, Object in, ByteBuf out) {
        if (in instanceof HotKeyMsg) {
            byte[] bytes = ProtostuffUtils.serialize(in);
            byte[] delimiter = Constant.DELIMITER.getBytes();
 
            byte[] total = new byte[bytes.length + delimiter.length];
            System.arraycopy(bytes, 0, total, 0, bytes.length);
            System.arraycopy(delimiter, 0, total, bytes.length, delimiter.length);
 
            out.writeBytes(total);
        }
    }
}

Regardez d'abord le décodeur Decoder, il est utilisé pour décoder le message après réception du message par netty et convertir l'octet en objet (HotKeyMsg personnalisé). Il y a un tas de messages que j'ai commentés, commentés, et les messages qui devraient être trouvés sur Internet sont écrits comme ça. Cette méthode en elle-même ne pose aucun problème dans les scènes ordinaires, et le décodage est toujours normal, mais elle est très sujette à des problèmes de collage lorsqu'il y en a des centaines de milliers. J'ai donc ajouté un décodeur délimiteur DelimiterBasedFrameDecoder avant ce décodeur.

Lorsqu'un message est reçu, le décodeur de délimitation est passé en premier, puis en ce qui concerne MsgDecoder, c'est un flux d'octets d'objet qui a été séparé et il peut être désérialisé directement avec la classe d'outil proto. Constant.DELIMITER est une chaîne spéciale que je personnalise, utilisée comme séparateur.

Regardez l'encodeur, l'encodeur, sérialisez d'abord l'objet à transmettre dans l'octet [] avec ProtostuffUtils, puis accrochez mon séparateur personnalisé sur la queue. De cette façon, lorsque l'objet est envoyé vers l'extérieur, l'encodeur sera utilisé et le séparateur sera ajouté.

Le code côté serveur correspondant est à peu près comme ceci:

Ensuite, l'objet transféré peut être utilisé directement dans le gestionnaire.

Regardez à nouveau côté client

C'est la même chose que côté serveur, mais aussi ces codecs, il n'y a pas de différence. En raison de la communication entre netty et le serveur, j'utilise la même définition d'objet.

La même chose est vraie pour les gestionnaires.

Machine unique et cluster

Une fois que tout ce qui précède est écrit, nous pouvons réellement le tester. Nous pouvons démarrer un client, un serveur, puis faire une boucle sans fin pour envoyer cet objet au serveur, puis vous pouvez directement envoyer l'objet au serveur après avoir reçu l'objet Ecrivez-le et envoyez-le au client tel quel. Vous constaterez qu'il fonctionne correctement, l'envoi de N millions par seconde n'est pas un problème, le codec est normal et les côtés client et serveur sont relativement normaux. La prémisse actuelle est que la classe d'outils de ProtoBuf est la même que la mienne. Ne partagez pas le tampon. Les articles trouvés sur Internet sont essentiellement terminés à ce stade, et il est normal d'envoyer quelques messages au hasard. Cependant, en fait, après la mise en ligne de ce type de code, il sera piqué.

En fait, les tests locaux sont également très faciles: démarrez quelques clients, tous ensemble avec un serveur, puis envoyez-leur des objets dans une boucle sans fin, puis voyez s'il y a des exceptions aux deux extrémités. Dans ce cas, la différence par rapport au premier est en fait la même côté client, mais côté serveur. Auparavant, un seul client recevait un message à la fois. Il est maintenant envoyé à deux clients en même temps. Cette étape se produira si vous ne faites pas attention. Il y a un problème, il est recommandé de l'essayer vous-même.

Après cela, ajoutons quelque chose de plus. J'ai démarré deux serveurs avec deux ports respectivement. Il y a en fait deux serveurs différents en ligne. Le client enverra des objets aux deux serveurs dans une boucle sans fin en même temps, comme indiqué dans le code ci-dessous.

Pour envoyer des messages, nous utilisons généralement channel.writeAndFlush (). Vous pouvez supprimer la synchronisation et exécuter le code pour voir. Vous trouverez des bosses anormalement projetées. Nous avons évidemment envoyé des messages à deux canaux différents, mais l'heure était au même moment, et en conséquence, de sérieux paquets persistants se sont produits. De nombreux messages reçus par le serveur sont irréguliers et un grand nombre d'erreurs sera signalé. Si l'intervalle entre les deux canaux est de 100 ms, la situation est résolue. Bien sûr, à la fin, nous pouvons utiliser la synchronisation pour envoyer de manière synchrone, de sorte qu'aucune exception ne sera levée.

Le code ci-dessus a été testé: avec 40 clients et 2 serveurs, chaque serveur reçoit environ 400 000 objets par seconde en moyenne, qui peuvent fonctionner en continu et de manière stable.

Je suppose que tu aimes

Origine blog.csdn.net/qq_46388795/article/details/108664404
conseillé
Classement