python / python/cpython

Inefficient ssl.SSLWantReadError exception slows down very common use-case

Ouverte
#123,954 12 commentaires 2 réactions 0 personnes assignées Voir sur GitHub

Personne n'a encore pris cette issue.

extension-modules performance topic-SSL type-bug
Langage dominant
Python
Étoiles
77.2k
Forks
35.9k
Métriques de merge des PR
Métriques de PR en attente

Description

Bug report

Bug description:

Event loops like uvloop, asyncio use nonblocking ssl. They typically

  1. read data from the socket when epoll returns that it is ready
  2. push data to the incoming MemoryBIO
  3. read from SSLObject until SSLWantReadError is thrown
  4. pass read data to the application protocol

when peers are exchanging relatively small messages, SSLObject.read is typically called 2 times . First call returns data, second - throws SSLWantReadError

perf shows that the second call is almost as expensive as the first call because of time spent on constructing new exception object.

Is it possible to optimize exception object creation for the second call?

I tried to avoid the second call by analyzing MemoryBIO.pending and SSLObject.pending values but they can't always reliably tell that we have to wait for more data.

For example, it is possible that incoming MemoryBIO.pending > 0, SSLObject.pending == 0. We call SSLObject.read and it throws because incoming MemoryBIO doesn't have the full ssl frame yet.

Example echo client that replicates internal logic in asyncio/uvloop:

import socket
import ssl
import select
from typing import Optional

ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE

ep = select.epoll(2)

incoming = ssl.MemoryBIO()
outgoing = ssl.MemoryBIO()

sock: Optional[socket.socket] = None
ssl_sock: Optional[ssl.SSLObject] = None


def wait_data():
    ep.poll()

    try:
        while True:
            chunk = sock.recv(1024)
            incoming.write(chunk)
    except BlockingIOError:
        pass


def wait_data_until_ssl_read_succeed():
    data = bytearray()
    try:
        wait_data()
        # while ssl_sock.pending() > 0 or incoming.pending > 0:
        while True:
            data += ssl_sock.read()
    except ssl.SSLWantReadError:
        pass

    return data


with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
    ep.register(sock.fileno(), select.EPOLLIN)

    ssl_sock = ssl_context.wrap_bio(incoming, outgoing, server_hostname='localhost')

    sock.connect(('127.0.0.1', 25000))
    sock.setblocking(False)

    handshake_complete = False
    message_sent = False

    msg = b"a" * 256

    # do handshake
    while True:
        try:
            ssl_sock.do_handshake()
            break
        except ssl.SSLWantReadError as ex:
            if outgoing.pending > 0:
                chunk = outgoing.read(outgoing.pending)
                sock.send(chunk)
            wait_data()

    # send message and wait for reply
    while True:
        ssl_sock.write(msg)
        chunk = outgoing.read(outgoing.pending)
        sock.send(chunk)

        data = wait_data_until_ssl_read_succeed()
        # print(data)

Perf output:

   17.41%     0.25%            43  python   _ssl.cpython-314-x86_64-linux-gnu.so     [.] _ssl__SSLSocket_read
            |          
             --17.16%--_ssl__SSLSocket_read
                       |          
                       |--8.09%--SSL_read_ex
                       |          |          
                       |           --7.74%--0x7b1fbef883f9
                       |                     |          
                       |                     |--4.83%--0x7b1fbefadc22
                       |                     |          |          
                       |                     |          |--0.93%--0x7b1fbefa6919
                       |                     |          |          |          
                       |                     |          |           --0.90%--EVP_DecryptUpdate
                       |                     |          |                     |          
                       |                     |          |                      --0.90%--0x7b1fbec90c8b
                       |                     |          |                                |          
                       |                     |          |                                 --0.87%--0x7b1fbec90b45
                       |                     |          |          
                       |                     |          |--0.79%--0x7b1fbefa64a5
                       |                     |          |          |          
                       |                     |          |           --0.61%--EVP_CIPHER_CTX_get_iv_length
                       |                     |          |          
                       |                     |          |--0.72%--0x7b1fbefa6721
                       |                     |          |          |          
                       |                     |          |           --0.60%--EVP_CipherInit_ex
                       |                     |          |          
                       |                     |          |--0.57%--0x7b1fbefa6750
                       |                     |          |          
                       |                     |           --0.52%--0x7b1fbefa68f0
                       |                     |          
                       |                     |--1.13%--0x7b1fbefadf94
                       |                     |          |          
                       |                     |           --0.52%--0x7b1fbefac3c7
                       |                     |          
                       |                      --0.68%--0x7b1fbefad750
                       |          
                       |--6.21%--PySSL_SetError.constprop.0
                       |          |          
                       |           --5.17%--fill_and_set_sslerror
                       |                     |          
                       |                     |--2.83%--PyUnicode_FromFormat
                       |                     |          |          
                       |                     |           --2.54%--unicode_from_format
                       |                     |                     |          
                       |                     |                      --1.23%--__sprintf_chk
                       |                     |                                __vsprintf_internal
                       |                     |                                |          
                       |                     |                                 --0.99%--__vfprintf_internal
                       |                     |          
                       |                      --1.01%--PyObject_SetAttr
                       |                                |          
                       |                                 --0.98%--PyObject_GenericSetAttr
                       |                                           |          
                       |                                            --0.52%--_PyObjectDict_SetItem
                       |          
                        --0.60%--SSL_get_error

To reproduce you would need some ssl echo server running on localhost 25000 port. After you have started it, run echo client code under perf.

$ perf record -F 999 -g --call-graph lbr --user-callchains -- python echo_client.py
$ perf report -G -n --stdio

Let it work for 15 seconds and then press Ctrl-C

CPython versions tested on:

CPython main branch

Operating systems tested on:

Linux

Linked PRs
  • gh-128391

Guide de contribution

Ouvrir le guide de contribution

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Piste de recherche

Commencez par SSLObject.read et le chemin _ssl__SSLSocket_read indiqué dans la sortie de perf, en particulier PySSL_SetError et fill_and_set_sslerror. Reproduisez le problème avec le client echo fourni et un serveur echo SSL local, puis exécutez les commandes perf fournies. Le travail est terminé lorsque le coût de construction de SSLWantReadError est réduit sans modifier le comportement de SSL non bloquant.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Évaluation

Stack technique
python
Domaine
networking, performance
Type d'issue
Bug
Difficulté
4/5
Temps estimé
3-5 jours
Activité
À l'abandon
Clarté
Plutôt claire
Accessibilité débutants
35/100

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.