0
votes

Performance de la piscine multithreading d'exécutéservice

J'utilise la bibliothèque de la bibliothèque de la concurrence de Java, exécutorservice pour exécuter mes tâches. Le seuil d'écriture à la base de données est de 200 qps, cependant, ce programme ne peut atteindre que 20 QP avec 15 threads. J'ai essayé 5, 10, 20, 30 threads, et ils étaient encore plus lents que 15 threads. Voici le code:

ExecutorService executor = Executors.newFixedThreadPool(15);
List<Callable<Object>> todos = new ArrayList<>();

for (final int id : ids) {
    todos.add(Executors.callable(() -> {
        try {
            TestObject test = testServiceClient.callRemoteService();
    SaveToDatabase();
        } catch (Exception ex) {}
    }));
}
try {
    executor.invokeAll(todos);
} catch (InterruptedException ex) {} 
executor.shutdown();


10 commentaires

Est-il possible de poster le type de requêtes en cours d'exécution? C'est à dire. sont-ils tous accessibles ou modifiant la même rangée ou la même table? Sont-ils tous lisent, tous écrit, ou une combinaison des deux?


Vous faites un appel distant, cela prendra probablement beaucoup de temps (frais de tête de réseau, etc.). Le nombre de threads est limité par le nombre de cœurs que vous avez (vous avez 4 CPU, mais combien de cœurs?). Si l'appel à distance prend beaucoup de temps, le noyau attend une réponse et ne sera disponible pour rien d'autre. Créer plus de threads que de cœurs ralentit probablement votre système entier au fur et à mesure qu'il doit commencer à commuter.


@ M.Deinum "Le nombre de threads est limité par le nombre de noyaux que vous avez ... Si l'appel à distance prend beaucoup de temps, le noyau attend une réponse et ne sera disponible pour rien d'autre." - Ceci est au moins trompeur. Bien sûr, vous pouvez avoir beaucoup plus threads que cpu cœurs à tout moment de temps; et surtout quand un threads doit attendre des E / S (réseau, disque, ...) ("bloqué") Un autre thread ("runnable") sera échangé pour utiliser la CPU dans le entre temps. Par conséquent, surtout si vous avez relativement beaucoup d'attente dans un fil que vous souhaitez utiliser plus threads pour garder la CPU occupé.


Cela dépend de votre architecture de la CPU. De plus, plus de threads conduiront éventuellement à échanger des threads, ce qui, en Java, entraînera un coup de performances à mesure que la pile et tout le reste doit être sérialisée / désérialisée lors de la pause et de la désempause le fil. En utilisant également beaucoup de threads pour io Heavy Operations, c'est rarement une bonne solution (c'est pour les opérations intensives de la CPU). Donc, en effet, il n'est pas limité par le nombre de cœurs, mais créant 200 threads où vous n'avez que 16 cœurs permettra probablement de rendre votre programme encore plus lent puis avec seulement 20 threads.


@ M.Deinum "Les performances frappent comme la pile et tout le reste doit être sérialisée / désériorialisée lors de la pause et de la non-consommation du fil." - Je n'ai aucune idée de ce dont vous parlez. Java threads Carte 1: 1 sur des threads du système d'exploitation et il n'existe tout simplement pas de "sérialisation / désérialisement" la pile, ou "pause" un fil. Et comme je l'ai dit, vous ne gagnerez pas de performances en utilisant de nombreux threads si chaque thread peut déjà utiliser 100% de processeur. Multi-threading est juste le moyen de masquer les latences d'E / S.


@ M.Deinum "Création de 200 threads dans lesquels vous n'avez que 16 cœurs permettra probablement de rendre votre programme encore plus lent puis avec seulement 20 threads." - Si chacun de ces threads dépense 90% de son temps en attente d'un réseau ou d'autres E / S, 20 threads n'utiliseront que 20 * (100% -90%%) = 2 cœurs CPU ...


Sur un noyau de niveau 1 du matériel 1 gérera 1 fil. S'il doit basculer vers un autre fil, l'état appartenant à ce fil doit être stocké en mémoire (cela prend du temps). Ensuite, il effacera l'état au niveau de la CPU et le remplira pour le fil qu'il doit procéder ensuite. Ce changement de contexte est une opération lourde. C'est ce que je voulais dire avec sérialisé / désérialisation.


Chaque thread en Java allouera également de la mémoire supplémentaire en fonction de vos paramètres JVM, la création de nombreux threads allouera également beaucoup de mémoire. Ce qui pourrait entraîner des cycles de GC excessives et des pauses menant à une dégradation de la performance supplémentaire.


@ M.Deinum Un changement de contexte sur un processeur moderne engage une très petite pénalité. La plus grande question est souvent la perte de localité d'accès à la mémoire pouvant plus ou moins invalider le cache de données. Mais cela n'a rien à voir avec Java et n'est pas une grosse problème dans la plupart des cas. Par exemple. Même si la pénalité était de 1 MS au total, une réponse de 5 ms pour une réponse réseau est toujours un bon moment pour passer à un autre fil.


@ M.Deinum "Créer de nombreux threads allouera également beaucoup de mémoire." - Cela dépend de ce que vous appelez "beaucoup". IIRC, la taille de la pile par défaut est / utilisée pour être de 1 Mo par fil. "Ce qui pourrait conduire à des cycles de GC excessifs" - Non, la pile d'un thread est pas alloué à partir du tas dans lequel les objets Java résident et ne provoquent donc aucun cycles de GC.


4 Réponses :


0
votes

À propos de l'épissage des lots: exécutantservice a une file d'attente intérieure pour stocker des tâches. Dans votre cas ExecuTorservice Executor = exécuteurs.NewfixedTheadpool (15); a 15 threads SO max 15 tâches s'exécutera simultanément et d'autres seront stockées dans la file d'attente. La taille de la file d'attente peut être paramétrée. Par taille par défaut, la taille augmente jusqu'à max int. Appel InvokeLl à l'intérieur de la méthode Exécuter et cette méthode placera des tâches dans la file d'attente lorsque toutes les threads fonctionnent.

IMHO Il y a 2 scénarios possibles pourquoi la CPU n'est pas à 100%:

  1. Essayez d'agrandir la piscine de fil
  2. thread est en attente de testServiceclient.callremoteservice () à Complete et pendant ce temps, le processeur est Starwing

0 commentaires

1
votes

Il n'y a rien dans ce code qui empêche ce taux de requête, à l'exception de la création et de la destruction d'une piscine de thread est très coûteux. Je suggère d'utiliser l'API de flux qui est non seulement plus simple mais réutilise une piscine de thread intégrée

    ExecutorService es = Executors.newFixedThreadPool(8);
    for (int t = 0; t < 5; t++) {
        long start = System.nanoTime();
        int[] ids = new int[5000];
        List<Future> futures = new ArrayList<>(ids.length);
        for (int id : ids) {
            futures.add(es.submit(() -> {
                try {
                    Socket s = new Socket("localhost", ss.getLocalPort());
                    s.getOutputStream().write(id);
                    s.getInputStream().read();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }));
        }
        for (Future future : futures) {
            future.get();
        }
        long time = System.nanoTime() - start;
        System.out.println("Throughput " + (int) (ids.length * 1e9 / time) + " connects/sec");
    }
    es.shutdown();


4 commentaires

Il ne s'agit que de forkjoinpool ne devrait pas être utilisé pour io, je pense que


@Grzegorzpiwowowarek peut-être pas, mais il est simple à utiliser. L'utilisation d'un service d'exécution est le plus susceptible d'être meilleure après que vous obteniez cela fonctionnant.


Il peut être intéressant de noter que vous utilisez "localhost" dans votre exemple, ce qui est probablement des ordres de grandeur plus rapides que la mise en réseau à distance, en faisant essentiellement les tâches complètement liées à la CPU. Si chaque connexion réseau a provoqué chaque thread doit attendre quelques ms pour la réponse, l'effet d'échelle de plus de threads serait plus apparent.


@JIMMYB et la demande de DB sont susceptibles de prendre encore plus d'avantages supplémentaires à une plus grande taille de la piscine. +1



0
votes

Le problème de QPS peut-être est la limite de bande passante ou l'exécution de la transaction (elle verrouille la table ou la ligne). Donc, vous n'augmentez que la taille de la piscine n'est pas travaillée. En plus, vous pouvez essayer d'utiliser le modèle producteur-consommateur.


0 commentaires

1
votes

Pourquoi vous limitez-vous à un si faible nombre de threads?

Vous manquez des opportunités de performance de cette façon. Il semble que vos tâches soient vraiment pas CPU-liées. Les opérations réseau (requête de base de données à distance +) peuvent prendre la majeure partie de temps pour chaque tâche à terminer. Pendant ces périodes, lorsqu'une tâche unique / fil doit attendre un événement (réseau, ...), un autre thread peut utiliser la CPU. Plus vous accumulez de threads à la disposition du système, plus les threads peuvent attendre que leurs E / S du réseau terminent tout en ayant encore des threads utilisent la CPU en même temps.

Je vous suggère de réduire considérablement le nombre de threads pour l'exécuteur exécutif. Comme vous le dites que les deux serveurs distants sont plutôt sous-utilisés, je suppose que l'hôte que votre programme est exécuté, c'est le goulot d'étranglement pour le moment. Essayez d'augmenter (double?) Le nombre de threads jusqu'à ce que votre utilisation de la CPU approche à 100% ou à la mémoire ou que le côté éloigné devienne le goulot d'étranglement.

D'ailleurs, vous Shutdown L'exécuteur, mais attendez-vous que les tâches se terminent? Comment mesurez-vous le "QPS"?

Une dernière chose me vient à l'esprit: comment les connexions DB sont-elles traitées? C'est à dire. Comment SavetodoDatabase () s synchronisé? Tous les threads partagent-ils (et concourez-vous) une seule connexion? Ou, pire, chaque thread va-t-il créer une nouvelle connexion à la DB, faire sa chose, puis fermez la connexion à nouveau? Cela peut être un goulot d'étranglement grave car l'établissement d'une connexion TCP et la pratique de la poignée de main d'authentification peut prendre autant de temps que d'exécuter une simple déclaration SQL.

Si le nombre d'identifiants dans IDS est supérieur à 50000, est-ce une bonne idée d'utiliser invokeAll? Devrions-nous le diviser en lots plus petits, tels que 5000 chacun lot?

Alors que @vaclav Stenglavl a déjà écrit, les exécuteurs ont des files d'attente internes dans lesquelles ils s'effectuent et à partir desquels ils traitent les tâches. Donc, pas besoin de vous inquiéter de celui-là. Vous pouvez également appeler Soumettre pour chaque tâche unique dès que vous l'avez créée. Cela permet aux premières tâches de commencer déjà à exécuter pendant que vous créez / prépare des tâches ultérieures, ce qui a du sens, surtout lorsque chaque tâche la création prend relativement de long, mais ne va pas mal dans tous les autres cas. Pensez à invokeall comme méthode de commodité pour les cas où vous avez déjà une collection de tâches. Si vous créez les tâches successivement vous-même et que vous avez déjà accès au exécutantservice pour les exécuter, juste Soumettre () eux A.S.A.P.


0 commentaires