ヒトリ歩き

愚痴とかいろいろ書きます

HTTPリクエストの同期/非同期の処理動作

HTTPリクエストの同期/非同期の動きについて確認する。より、多くのリクエストをより早く処理するためには非同期の存在は不可欠だと考える。

なぜ、非同期が必要なのか?

リクエストの応答待ちをしている間に他の処理を動かして、効率よく処理をしたいから。
リクエストの応答がなかなか返ってこない間、他の処理は止まってしまう。その間にリクエストを受信してしまうと サーバー側は処理しきれなくなってしまう。
そのため、リクエストの応答を待っている間にリクエストの受信して他のリクエストを処理できるようにするために 非同期での実行が必要になる。

aiohttpとは

非同期で動作するHTTPクライアント/サーバー。
asyncioを使って動作する。 HTTPリクエストのモジュールでは、requestsがメジャーだと思う。
自分も仕事ではrequestsばかり使っている。

aiohttpのインストール

aiohttpモジュールをインストールするだけ。

pip install aiohttp

ハローワールド

GETリクエストのハローワールド。aiohttpのページに書かれているのawaitを使うように 少し変更。

import aiohttp
import asyncio


async def main():
    async with aiohttp.ClientSession() as session:
        resp = await session.get("http://httpbin.org/get")
        print(resp.status)
        print(await resp.text())


asyncio.run(main())

同期処理と非同期でのリクエスト受信時の動作確認

ここでは、同期で別のマイクロサービスにリクエストを送信する場合と非同期でリクエストを送信した場合の リクエストの処理の違いを確認する。 Webフレームワークは、FastAPIを使用する。

同期処理で別のマイクロサービスにリクエストを送信

リクエストを送信した際に、受信側が別のマイクロサービスに同期でリクエストを送信するケースを実験する。
送信元は、task1とtask2をほぼ同時にリクエストを送信しているが、リクエストはtask1の応答が返ってきて その後にtask2の応答結果が返ってきている。
これは、リクエストを同期で処理しているため、task1から順番に処理したことがわかる。

# 送信元
2024-04-20 07:59:04,569 - --- task1 -----
2024-04-20 07:59:04,569 - --- task1 Send!!
2024-04-20 07:59:04,570 - --- task2 -----
2024-04-20 07:59:04,570 - --- task2 Send!!
2024-04-20 07:59:14,606 - --- task1 status = 200
2024-04-20 07:59:14,606 - --- task1{"res":{"message":"success"}}
2024-04-20 07:59:24,612 - --- task2 status = 200
2024-04-20 07:59:24,612 - --- task2{"res":{"message":"success"}}

# 受信側
2024-04-20 07:59:04,572 - Received task1 Request!!
2024-04-20 07:59:14,604 - Send task1 response
INFO:     127.0.0.1:52673 - "GET /task1 HTTP/1.1" 200 OK
2024-04-20 07:59:14,606 - Received task2 Request!!
2024-04-20 07:59:24,611 - Send task2 response
INFO:     127.0.0.1:52674 - "GET /task2 HTTP/1.1" 200 OK

非同期で別のマイクロサービスにリクエストを送信

リクエストを非同期で処理したケースでは、さきほどと同様に送信元はtask1とtask2のリクエストを ほぼ同時に送信している。
リクエストを受信した側も同様にほぼ同時にリクエストを受信しており、応答結果もほぼ同時に返却している ことがわかる。

# 送信元
2024-04-20 08:01:19,726 - --- task1 -----
2024-04-20 08:01:19,726 - --- task1 Send!!
2024-04-20 08:01:19,726 - --- task2 -----
2024-04-20 08:01:19,726 - --- task2 Send!!
2024-04-20 08:01:29,740 - --- task1 status = 200
2024-04-20 08:01:29,740 - --- task1{"res":{"message":"success"}}
2024-04-20 08:01:29,740 - --- task2 status = 200
2024-04-20 08:01:29,740 - --- task2{"res":{"message":"success"}}

# 受信側
2024-04-20 08:01:19,728 - Received task1 Request!!
2024-04-20 08:01:19,732 - Received task2 Request!!
2024-04-20 08:01:29,738 - Send task1 response
INFO:     127.0.0.1:53064 - "GET /task1 HTTP/1.1" 200 OK
2024-04-20 08:01:29,739 - Send task2 response
INFO:     127.0.0.1:53065 - "GET /task2 HTTP/1.1" 200 OK

まとめ

aiohttpとrequestsモジュールを使って、同期と非同期時にHTTPリクエストの処理の仕方を 確認した。実際にプログラムを動かしてみることで、送信元と受信側の動きを理解することができた。
また、aiohttpの存在は知らなかったので、知ることが出来てよかった。
Postリクエストはデータの登録や更新、削除で使用されるため、同期処理した方が良さそうだが、 Getリクエストは非同期でより多くのリクエストを処理できるようにした方がよいと思う。
実際にプロジェクトの仕様にもよるが、aiohttpが使えることは頭に入れておこうと思う。

参考

  • 送信元
import aiohttp
import asyncio
import logging
from logging import getLogger, StreamHandler, Formatter

logger = getLogger("LogTest")
logger.setLevel(logging.DEBUG)
stream_handler = StreamHandler()
stream_handler.setLevel(logging.DEBUG)
handler_format = Formatter("%(asctime)s - %(message)s")
stream_handler.setFormatter(handler_format)
logger.addHandler(stream_handler)


async def send(name):
    logger.info("--- " + name + " -----")
    async with aiohttp.ClientSession() as session:
        logger.info("--- " + name + " Send!!")
        url = "http://127.0.0.1:8001/" + str(name)
        resp = await session.get(url)
        logger.info("--- " + name + " status = " + str(resp.status))
        logger.info("--- " + name + str(await resp.text()))


async def main():
    task1 = asyncio.create_task(send("task1"))
    task2 = asyncio.create_task(send("task2"))
    await task1
    await task2


asyncio.run(main())
  • app

uvicorn received_app:app --port 8001 で起動

from fastapi import FastAPI
import requests
import aiohttp
import logging
from logging import getLogger, StreamHandler, Formatter

logger = getLogger("LogTest")
logger.setLevel(logging.DEBUG)
stream_handler = StreamHandler()
stream_handler.setLevel(logging.DEBUG)
handler_format = Formatter("%(asctime)s - %(message)s")
stream_handler.setFormatter(handler_format)
logger.addHandler(stream_handler)

app = FastAPI()


@app.get("/task2")
async def get_task2():

    logger.info("Received task2 Request!!")
    # response = requests.get("http://127.0.0.1:8000")
    # logger.info("Send task2 response")
    # return {"res": response.json()}
    async with aiohttp.ClientSession() as session:
        response = await session.get("http://127.0.0.1:8000")
        json_res = await response.json()
        logger.info("Send task2 response")
        return {"res": json_res}


@app.get("/task1")
async def get_task1():

    logger.info("Received task1 Request!!")
    # response = requests.get("http://127.0.0.1:8000")
    # logger.info("Send task1 response")
    # return {"res": response.json()}
    async with aiohttp.ClientSession() as session:
        response = await session.get("http://127.0.0.1:8000")
        json_res = await response.json()
        logger.info("Send task1 response")
        return {"res": json_res}
  • app2

uvicorn late_app:app --port 8000 で起動

from fastapi import FastAPI
import time
import logging
from logging import getLogger, StreamHandler, Formatter

logger = getLogger("LogTest")
logger.setLevel(logging.DEBUG)
stream_handler = StreamHandler()
stream_handler.setLevel(logging.DEBUG)
handler_format = Formatter("%(asctime)s - %(message)s")
stream_handler.setFormatter(handler_format)
logger.addHandler(stream_handler)

app = FastAPI()


@app.get("/")
def call_api():
    logger.info("Received request in late app.")
    time.sleep(10)
    response = {"message": "success"}
    return response