홈으로 돌아가기

Picows: 타협 없는 asyncio용 가장 빠른 WebSocket

picows 라이브러리 아키텍처 분석 — asyncio용 가장 빠른 WebSocket 엔진. zero-copy 처리, callback 인터페이스 및 핫패스에서 async 거부 분석. 코드 예제 및 성능 비교.

Picows: 왜 Python에서 가장 빠른 WebSocket인가?
Advertisement 728x90

# picows가 asyncio용 가장 빠른 WebSocket 엔진이 된 비결: 타협 없는 아키텍처

picows 라이브러리는 Python의 WebSocket 성능 규칙을 완전히 바꿔놓았습니다. 편의성을 포기하고 순수한 속도를 추구한 결과입니다. async for나 자동 메시지 조립 같은 기능은 잊으세요—콜백 인터페이스를 통해 프레임에 직접 접근하고, zero-copy 처리와 버퍼에 대한 완전한 제어를 얻을 수 있습니다. 알고리즘 트레이딩처럼 매 밀리초가 돈으로 직결되는 분야에서 이는 단순한 최적화가 아니라 생존의 필수 요소입니다.

표준 솔루션의 한계

고빈도 데이터 처리—예를 들어 암호화폐 거래소의 시장 업데이트—에서 aiohttp와 websockets는 받아들일 수 없는 지연을 보입니다. 초당 10~20천 메시지(각 200바이트) 부하가 걸리면 내부 큐에 데이터가 쌓이기 시작하죠. 네트워크에서 패킷을 받고 사용자 비즈니스 로직에 전달하는 데 100ms 지연이 발생할 수 있으며, 이는 트레이딩 전략에 치명적입니다.

문제는 Python 자체가 아니라 아키텍처에 있습니다:

Google AdInline article slot
  • 데이터가 여러 번 복사됩니다: socket → parser buffer → queue → user coroutine.
  • 파서가 Python에서 실행됩니다(aiohttp의 부분적으로 C로 재작성된 파서 제외), 오버헤드가 추가됩니다.
  • TEXT 메시지가 str로 변환되며 추가 메모리 할당이 필요합니다.
  • 큰 메시지(예: 1MB) 전송 시 2~14바이트 헤더 추가를 위해 전체 페이로드를 복사해야 합니다.

이는 단순히 느린 수준이 아니라 예측 가능하게 비효율적입니다. 특히 asyncio.Protocol을 사용할 때 더 빠른 BufferedProtocol 대신 항상 새로운 bytes 객체를 넘겨주기 때문에 그렇습니다.

picows 아키텍처: Zero-Copy와 최소 추상화

picows는 다른 라이브러리들이 너무 저수준이라고 피할 원칙으로 만들어졌습니다:

  • 읽기 시 복사를 최소화하기 위해 asyncio.BufferedProtocol을 사용합니다.
  • C 파서가 프레임 헤더를 분석한 후 즉시 페이로드 경계를 사용자 코드에 전달합니다—중간 객체는 생성되지 않습니다.
  • 사용자가 memoryview나 bytearray로 직접 작업합니다—필요할 때만 str 변환.
  • 전송은 send_reuse_external_bytearray를 사용합니다—헤더를 페이로드 앞 예약 공간에 직접 쓰고, in-place로 마스킹을 적용합니다.

편의 기능 포기는 쉽지 않았지만 그만한 가치가 있습니다:

Google AdInline article slot
  • async for 없음—대신 on_ws_frame 콜백.
  • 자동 멀티프레임 메시지 조립 없음—사용자가 직접 연결 방식을 결정합니다.
  • 내장 permessage-deflate 지원 없음—앱 레벨에서 압축을 처리하세요.
  • send()는 async가 아닙니다—소켓이 바쁘면 큐에 넣되 블로킹하지 않습니다.

이건 버그가 아니라 기능입니다: 매 프레임마다 async 함수를 호출하면 수십 마이크로초 지연이 추가되기 때문입니다. picows는 asyncio 컨텍스트 스위치 없이 프레임을 처리할 수 있게 해줍니다.

코드 예제: picows를 사용한 클라이언트와 서버

최소 클라이언트:

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())

에코 서버:

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())

주의: on_ws_frame 내부에 await가 없습니다. 처리가 동기적으로 이뤄지며 컨텍스트 스위치가 발생하지 않습니다. async 동작이 필요하다면 create_task나 Queue를 사용하세요—이는 아키텍처가 강제하는 게 아니라 개발자의 선택입니다.

벤치마크: picows가 경쟁자를 압도하는 지점

테스트에서 다양한 메시지 크기의 에코 교환 초당 요청 수 차이가 확인됩니다:

  • 작은 메시지(64–512바이트)에서 picows는 aiohttp보다 3–5배, websockets보다 8–10배 빠릅니다.
  • 큰 메시지(1MB 이상)에서는 zero-copy 전송 덕분에 격차가 더 벌어져 최대 20배에 달합니다.
  • 초당 50K 요청에서도 프레임 수신부터 처리까지 지연이 100마이크로초 미만입니다.

주요 요인:

  • 불필요한 복사 없음—데이터를 제자리에서 읽고 씁니다.
  • 최소 객체 생성—중간 str, list, dict이 없습니다.
  • 프레임 처리 최소 추상화—헤더 파싱 직후 콜백이 발화합니다.
  • 최적화된 C 코드—파싱과 프레임 빌드가 모두 C에서 이뤄지며 Python 인터프리터가 관여하지 않습니다.

주요 요점

  • picows는 모든 사람을 위한 라이브러리가 아닙니다—편리한 API보다 최대 성능이 필요한 전문가들을 위한 도구입니다.
  • 아키텍처가 asyncio의 transport 모델을 따릅니다: transport/protocol 중심, 고수준 추상화가 아닙니다.
  • Zero-copy 처리와 직접 메모리 접근이 경쟁자 대비 결정적 강점입니다.
  • 핫패스에서 async를 생략해 지연을 몇 배 단축했습니다.
  • TLS 지원과 고급 시나리오(멀티프레임, 마스킹)도 속도 저하 없이 처리합니다.

언제 picows를 선택할까—언제 피할까

picows를 사용하세요, 만약:

  • 초당 수천 메시지 처리에 1ms 미만 지연이 핵심이라면.
  • 메모리와 성능 제어를 위해 편의성을 포기할 의향이 있다면.
  • 프레임 핸들러에서 지속적인 async/await 없이 로직을 실행할 수 있다면.
  • 바이너리 데이터나 memoryview에서 직접 파싱 가능한 JSON을 다룬다면.

피하세요, 만약:

  • 간단한 API와 빠른 시작을 우선한다면.
  • 자동 멀티프레임 메시지 조립을 기대한다면.
  • 내장 압축이나 WebSocket 확장이 필요하다면.
  • 부하가 가볍습니다(< 초당 1K 요청)고 아키텍처를 복잡하게 만들고 싶지 않다면.

picows는 aiohttp나 websockets의 대체재가 아닙니다. CPU 사이클마다 중요한 엣지 케이스를 위한 전문 도구입니다. 알고리즘 트레이딩, 게임 서버, IoT 게이트웨이, 고빈도 데이터 파이프라인 등에서 Python 생태계 최고의 선택지 중 하나입니다.

— Editorial Team

Advertisement 728x90

다음 읽기