# 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:
- 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:
- 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:
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
Brak komentarzy.