Powrót do strony głównej

Picows: najszybszy WebSocket dla asyncio bez kompromisów

Analiza architektury biblioteki picows — najszybszego silnika WebSocket dla asyncio. Analiza przetwarzania zero-copy, interfejsu callback i rezygnacji z async w hot-path. Przykłady kodu i porównanie wydajności.

Picows: dlaczego to najszybszy WebSocket w Python?
Advertisement 728x90

# Jak picows stał się najszybszym silnikiem WebSocket dla asyncio: architektura bez kompromisów

Biblioteka picows przełamała reguły wydajności WebSocket w Pythonie, rezygnując z wygody na rzecz prędkości. Zamiast async for i automatycznego składania wiadomości — bezpośredni dostęp do ramek przez interfejs callback, zero-copy przetwarzanie i pełna kontrola nad buforami. Dla algotradingu, gdzie każda milisekunda kosztuje pieniądze, to nie optymalizacja — to konieczność.

Dlaczego standardowe rozwiązania nie radzą sobie

Kiedy mowa o wysokoczęstotliwościowym przetwarzaniu danych — na przykład aktualizacjach rynkowych z giełd kryptowalut — aiohttp i websockets wykazują nieakceptowalne opóźnienia. Przy obciążeniu 10–20 tysiącami wiadomości na sekundę (po 200 bajtów każda) zaczynają gromadzić dane w wewnętrznych kolejkach. Opóźnienie między otrzymaniem pakietu z sieci a jego dostarczeniem do logiki biznesowej może osiągać 100 ms — fatalne dla strategii handlowych.

Problem nie w Pythonie samym w sobie, a w architekturze:

Google AdInline article slot
  • Dane są wielokrotnie kopiowane: od gniazda → do bufora parsera → do kolejki → do korutyny użytkownika.
  • Parsery działają w Pythonie (oprócz częściowo przepisanej parsery C w aiohttp), co tworzy narzut.
  • Wiadomości TEXT konwertowane są na str, co wymaga dodatkowego alokowania pamięci.
  • Wysyłanie dużych wiadomości (np. 1 MB) wymaga skopiowania całego payload tylko po to, by dodać 2–14 bajtów nagłówka.

To nie tylko wolne — to przewidywalnie nieefektywne. Szczególnie przy użyciu asyncio.Protocol, który zawsze przekazuje nowe obiekty bytes, zamiast szybszego BufferedProtocol, pozwalającego pracować z zewnętrznym buforem.

Architektura picows: zero-copy i minimalna abstrakcja

Picows zbudowany jest na zasadach, które w innych bibliotekach uważano by za zbyt niskopoziomowe:

  • Używa asyncio.BufferedProtocol do minimalizacji kopiowania przy odczycie.
  • Parser w C rozkłada nagłówek ramki i natychmiast przekazuje granice payload do kodu użytkownika — bez tworzenia pośrednich obiektów.
  • Użytkownik pracuje z memoryview lub bytearray bezpośrednio — bez konwersji na str, jeśli niepotrzebna.
  • Wysyłanie zaimplementowane przez send_reuse_external_bytearray — nagłówek zapisywany bezpośrednio w zarezerwowanej przestrzeni przed payload, maskowanie stosowane in-place.

Odmowa wygód nie była łatwa, ale uzasadniona:

Google AdInline article slot
  • Brak async for — zamiast tego callback on_ws_frame.
  • Brak automatycznego składania multi-frame wiadomości — użytkownik sam decyduje, jak je konkatenować.
  • Brak wbudowanego wsparcia permessage-deflate — kompresja musi być obsługiwana na poziomie aplikacji.
  • send() nie jest async — jeśli gniazdo zajęte, dane lądują w kolejce bez czekania.

To nie bug, a feature: wywołanie funkcji async na każdej ramce dodaje dziesiątki mikrosekund opóźnienia. Picows pozwala przetwarzać ramki bez przełączania kontekstu asyncio.

Przykłady kodu: klient i serwer na picows

Minimalny klient:

import asyncio
from picows import ws_connect, WSFrame, WSTransport, WSListener, WSMsgType, WSCloseCode

class ClientListener(WSListener):
    def on_ws_connected(self, transport: WSTransport):
        transport.send(WSMsgType.TEXT, b"Hello world")

    def on_ws_frame(self, transport: WSTransport, frame: WSFrame):
        print(f"Echo reply: {frame.get_payload_as_ascii_text()}")
        transport.send_close(WSCloseCode.OK)
        transport.disconnect()

async def main():
    transport, client = await ws_connect(ClientListener, "ws://127.0.0.1:9001")
    await transport.wait_disconnected()

if __name__ == '__main__':
    asyncio.run(main())

Echo-serwer:

Google AdInline article slot
import asyncio
from picows import ws_create_server, WSFrame, WSTransport, WSListener, WSMsgType, WSUpgradeRequest

class ServerClientListener(WSListener):
    def on_ws_connected(self, transport: WSTransport):
        print("New client connected")

    def on_ws_frame(self, transport: WSTransport, frame: WSFrame):
        if frame.msg_type == WSMsgType.CLOSE:
            transport.send_close(frame.get_close_code(), frame.get_close_message())
            transport.disconnect()
        else:
            transport.send(frame.msg_type, frame.get_payload_as_memoryview())

async def main():
    def listener_factory(r: WSUpgradeRequest):
        return ServerClientListener()

    server: asyncio.Server = await ws_create_server(listener_factory, "127.0.0.1", 9001)
    for s in server.sockets:
        print(f"Server started on {s.getsockname()}")

    await server.serve_forever()

if __name__ == "__main__":
    asyncio.run(main())

Zwróć uwagę: brak await wewnątrz on_ws_frame. Przetwarzanie odbywa się synchronicznie, bez przełączania kontekstu. Jeśli potrzebne zachowanie asynchroniczne — można użyć create_task lub Queue, ale to świadomy wybór developera, a nie narzucona architektura.

Benchmarki: gdzie picows zostawia konkurentów w tyle

Testy pokazują różnicę w RPS (zapytania na sekundę) przy echo-wymianie wiadomości różnej wielkości:

  • Dla małych wiadomości (64–512 bajtów) picows wyprzedza aiohttp 3–5 razy, websockets — 8–10 razy.
  • Dla dużych wiadomości (1 MB+) przewaga jeszcze większa — do 20x, dzięki zero-copy wysyłaniu.
  • Opóźnienie między otrzymaniem a przetworzeniem ramki — stabilnie poniżej 100 mikrosekund nawet przy obciążeniu 50K RPS.

Kluczowe czynniki:

  • Brak zbędnego kopiowania — dane odczytywane i zapisywane in-place.
  • Minimalne tworzenie obiektów — brak pośrednich str, list, dict.
  • Zerowa abstrakcja przy przetwarzaniu ramek — callback wywoływany natychmiast po parsowaniu nagłówka.
  • Zoptymalizowany kod C — cały parsing i formowanie ramek odbywa się w C bez udziału interpretera Pythona.

Co ważne

  • Picows nie dla wszystkich — dla tych, którym potrzebna maksymalna wydajność, a nie wygodny API.
  • Architektura oparta na modelu transportowym asyncio: transport/protocol, a nie wysokopoziomowych abstrakcjach.
  • Zero-copy przetwarzanie i direct memory access — kluczowe przewagi nad konkurentami.
  • Odmowa async w hot-path pozwoliła zmniejszyć opóźnienia o rzędy wielkości.
  • Wsparcie TLS i złożonych scenariuszy (multi-frame, masking) zaimplementowane bez utraty prędkości.

Kiedy wybierać picows, a kiedy nie

Używaj picows, jeśli:

  • Krytyczne opóźnienie < 1 ms przy przetwarzaniu tysięcy wiadomości na sekundę.
  • Gotów poświęcić wygodę dla kontroli nad pamięcią i wydajnością.
  • Twoja logika może być zaimplementowana bez ciągłego użycia async/await w handlerze ramek.
  • Pracujesz z danymi binarnymi lub JSON, który można parsować bezpośrednio z memoryview.

Nie używaj, jeśli:

  • Ważna prostota API i szybki start.
  • Oczekujesz automatycznego składania multi-frame wiadomości.
  • Potrzebna wbudowana obsługa kompresji lub rozszerzeń WebSocket.
  • Twoje obciążenie niewielkie (< 1K RPS), i nie chcesz komplikować architektury.

Picows — to nie zamiennik aiohttp czy websockets. To specjalistyczne narzędzie dla edge-case scenariuszy, gdzie każdy takt procesora ma znaczenie. Dla algotradingu, serwerów gier, bramek IoT i high-frequency data pipelines — jeden z najlepszych opcji w ekosystemie Pythona.

— Editorial Team

Advertisement 728x90

Czytaj dalej