0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SDKってなんだ? — APIを直接叩くかSDKを使うか、実測で決める

0
Posted at

この記事の対象読者

  • requests や fetch で外部サービスのAPIを叩いたことがある
  • 公式SDKを入れるべきか、自分でHTTPを書くべきかで手が止まったことがある
  • Pythonのコードが読める

レベルはここ1つに固定します。SDKを自作する人向けの話は出てきません。

この記事で得られること

  • 目の前の外部サービス連携について、SDKを使うか素のHTTPで書くかを、その場で選べるようになります
  • SDKが自分の代わりに何をしているかを、実測値で説明できるようになります
  • SDKを採用したとき、どの設定を明示的に固定すべきかが分かるようになります

この記事で扱わないこと

  • 個別SDKの全メソッドリファレンス
  • SDKの自作方法とパッケージ公開の手順
  • モバイルSDKやゲームエンジンSDK固有の事情

なぜ私がこれを書くのか

外部APIを叩くだけの小さなバッチを書いていて、SDKを入れるかどうかで30分悩んだことがあります。「POSTリクエスト1本に26MBのパッケージを足すのか」という抵抗感と、「リトライを自分で書いて本番で事故るのが怖い」という恐怖が正面から衝突しました。結局そのときは勘で決めました。勘で決めたのが気持ち悪くて、今回測りました。

考えてほしいのは、SDKの採否は好みではなく、失敗時の挙動を誰が書くかという責任分界点の問題だという点です。私はここを設計判断として扱っていませんでした。計測はXeon 1コア・メモリ4GBという非力なLinux環境で行っています。非力だからこそ、インストールサイズやimport時間といった普段は誤差として無視される数字がはっきり見えました。


1. SDKとは何か ── 道具一式であって、ライブラリ単体ではない

このセクションで分かること:SDKという言葉が指す範囲と、API・ライブラリ・フレームワークとの境界線。

SDKは Software Development Kit の略で、要点は最後のK、つまりキット(道具一式)にあります。ライブラリ1個を指す言葉ではありません。

この記事では全体を通して、外部サービスの利用を海外の役所に書類を出す手続きにたとえます。対応は次の通りです。

たとえ 実際の技術
役所の窓口と受付ルール Web API(エンドポイントと仕様)
手続きを代行してくれる現地の業者 SDK
自分で現地の言葉を調べて窓口に並ぶ 素のHTTPクライアントで直接呼ぶ
記入例が印字済みの申請用紙 型定義
窓口が閉まっていたら時間を置いて並び直す リトライとバックオフ
5枚綴りの書類を全部まとめて取ってくる ページネーションの自動追跡
業者が連れてくるスタッフ 依存パッケージ

代行業者が持ってくるのは書類の書き方だけではありません。印鑑も、説明書も、窓口までの地図も一式です。SDKも同じで、クライアントライブラリのほかにコマンドラインツールを含むことがあります。Googleの公式ドキュメントは、Android SDKの構成要素をこう説明しています。

Android SDK Platform-Tools is a component for the Android SDK. It includes tools that interface with the Android platform, primarily adb and fastboot.
(和訳:Android SDK Platform-Tools は Android SDK の構成要素の1つです。Androidプラットフォームと接続するためのツール、主に adb と fastboot が含まれます)

出典は次のページです。

一方、いま実務で「SDKを入れる」と言うとき、その中身はたいていクライアントライブラリ1個です。OpenAIのPythonライブラリは、自らをこう説明しています。

The OpenAI Python library provides convenient access to the OpenAI REST API from any Python 3.10+ application. The library includes type definitions for all request params and response fields (中略) It is generated from our OpenAPI specification.
(和訳:OpenAI Python ライブラリは、Python 3.10以降のあらゆるアプリケーションから OpenAI の REST API へ簡便にアクセスする手段を提供します。このライブラリには全リクエストパラメータとレスポンスフィールドの型定義が含まれます。……これは当社の OpenAPI 仕様から生成されています)

ここに重要な事実が2つ入っています。型定義を含むことと、API仕様書から自動生成されていることです。手書きのラッパーではなく、窓口の様式が変われば機械的に刷り直される代行マニュアルだと考えると実態に近くなります。

用語 何を指すか たとえでの位置
API 呼び出し側と提供側の取り決めそのもの 窓口の受付ルール
ライブラリ 呼べば仕事をしてくれる部品 代行業者の社員1人
SDK ある基盤を使うための道具一式 代行業者そのもの
フレームワーク あなたのコードを呼び出す側に回る土台 役所側の業務システム

違いの本質は呼ぶ方向です。ライブラリとSDKはあなたが呼びます。フレームワークはあなたが呼ばれます。


2. SDKは何を肩代わりしているのか ── 2回失敗するAPIで測る

前節で、SDKは認証・リトライ・ページネーション・型変換を引き受ける層だと整理しました。問題は、その引き受けが具体的にどれだけの量かが数字で見えないことです。見えないものは設計判断の材料になりません。だから測ります。

このセクションで分かること:同じ失敗するAPIに対して、素のHTTP・自前リトライ・SDKの3通りがそれぞれ何回リクエストを投げ、何行で書けるか。

実験の仕掛けはこうです。最初の2回は必ずHTTP 500を返し、3回目から200を返す擬似APIサーバを立てます。窓口が2回続けて「本日はシステム障害です」と言う役所です。

# mock_api.py(抜粋)最初のFAIL_TIMES回は500、それ以降は200を返す
FAIL_TIMES = 2
HITS = defaultdict(int)

class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        trial = self.headers.get("X-Trial", "default")
        with _lock:
            HITS[trial] += 1
            n = HITS[trial]
        self.rfile.read(int(self.headers.get("Content-Length", 0) or 0))
        if n <= FAIL_TIMES:
            self._send(500, {"error": {"message": "mock server error"}})
        else:
            self._send(200, OK_BODY)

    def do_GET(self):                      # 到達回数の読み出し用
        self._send(200, dict(HITS))

この窓口に3通りの方法で挑みます。

# retry_bench.py(抜粋)H1/H2/H3 は到達回数を試行別に数えるための識別ヘッダ
H1, H2, H3 = ({"X-Trial": k} for k in ("plain", "hand", "sdk"))

def plain_requests():                        # 素のHTTP: 1回投げて諦める
    r = requests.post(URL, json=PAYLOAD, headers=H1, timeout=10)
    r.raise_for_status()

def handmade_retry():                        # 自前で指数バックオフを書く
    for attempt in range(3):
        r = requests.post(URL, json=PAYLOAD, headers=H2, timeout=10)
        if r.status_code < 500:
            break
        if attempt < 2:
            time.sleep(0.5 * (2 ** attempt) + random.uniform(0, 0.1))

def sdk_default():                           # SDKの既定設定のまま呼ぶ
    client = OpenAI(api_key="dummy", base_url=BASE, default_headers=H3)
    client.chat.completions.create(**PAYLOAD)

結果です。行数は例外処理を含めた実装全体で数えています。

呼び方 サーバに届いた回数 最終結果 所要時間 呼び出し側の行数
素のrequests 1 失敗 0.004秒 7行
自前リトライ 3 成功 1.654秒 11行
openai SDK 既定のまま 3 成功 1.470秒 3行

読み取れることは3つあります。

1つ目。素のHTTPは1回で諦めます。書いていないことはしてくれません。本番でこれをやると、サービス側の一時的な500がそのまま自分のバッチの失敗になります。

2つ目。自前リトライとSDKは、届いた回数が同じ3回です。SDKは魔法を使っているわけではなく、自前で書いたのとまったく同じことをしています。違うのは、それが自分のリポジトリにあるか、相手のリポジトリにあるかだけです。

3つ目。差は行数に出ました。11行対3行、この8行が、SDKを入れるかどうかで自分の管理下に入るコード量です。

ここで1つ、私の予想が外れました。自前リトライ(1.654秒)のほうがSDK(1.470秒)より遅かったのです。理由は単純で、私が何も考えずに置いた基準0.5秒の待ち時間が、SDKの既定値よりわずかに長かっただけです。ただ、「自分で書いたほうが状況に合わせて賢くできるはず」という感覚が最初の一撃で崩れたのは収穫でした。既定値は、だいたい自分の勘より丁寧にチューニングされています。


3. リトライの正体 ── 指数バックオフの式と、実測した待ち時間

前節で、SDKと自前実装は到達回数が同じ3回で、差は所要時間にわずかに出ると確認しました。つまり差分は待ち時間の決め方に閉じ込められています。ここを開けないと、既定値を信じてよいか判断できません。

このセクションで分かること:指数バックオフの計算式と、SDKが実際に空けている待ち時間の実測値。

リトライの待ち時間は、一般に指数バックオフと呼ばれる形で決まります。式の前に記号を1つずつ導入します。

  • $n$(リトライ回数、0から始まる整数)は、何回目の並び直しかを表します。初回の失敗直後が $n = 0$ です。
  • $b$(基準待ち時間、秒のスカラー)は、最初の並び直しまでに空ける時間です。たとえでは「出直すまでの基本の間隔」に当たります。
  • $c$(上限、秒のスカラー)は、どれだけ失敗しても待ち時間をこれ以上伸ばさないという天井です。
  • $U(\alpha, \beta)$(ゆらぎ、区間 $[\alpha, \beta]$ の一様乱数)は、同時に失敗した全クライアントが同じ瞬間に並び直して窓口を潰さないための、意図的なばらつきです。

この4つを組むと、$n$ 回目の待ち時間 $d_n$ はこうなります。

d_n = \min\left( b \cdot 2^{\,n},\; c \right) \times U(0.5,\; 1.0)

この式が言っているのは、失敗が続くほど待ち時間を倍々にしつつ、天井で頭打ちにし、最後にゆらぎを掛けるということです。役所のたとえなら、1回目は30分後に出直し、2回目は1時間後、ただし何回目でも半日以上は待たず、他の人と同じ時刻に押しかけないよう少し時刻をずらす運用です。

では、SDKが実際に空けている時間はいくつか。擬似サーバ側で到着時刻を記録して測りました。

# backoff_probe.py 到着時刻をサーバ側で記録して差分を取る
stamps, orig = [], mock_api.Handler.do_POST

def patched(self):
    stamps.append(time.perf_counter())
    orig(self)

mock_api.Handler.do_POST = patched
mock_api.serve()

client = OpenAI(api_key="d", base_url="http://127.0.0.1:8931/v1")
client.chat.completions.create(model="m", messages=[{"role": "user", "content": "hi"}])
print([round(stamps[i] - stamps[i - 1], 3) for i in range(1, len(stamps))])

3回走らせた結果です。

試行 1回目→2回目の間隔 2回目→3回目の間隔
1 0.501秒 0.755秒
2 0.451秒 0.962秒
3 0.412秒 0.797秒

2回目までの間隔がおよそ0.5秒、3回目までがおよそ0.8秒から0.96秒。倍になりきらないのは、式の末尾の $U(0.5, 1.0)$ が効いているからだと考えられます。なお、これは外から観測した挙動であって、実装を読んで確認した係数ではありません。式の形が観測と矛盾しない、という以上のことは主張できません。

公式ドキュメントの記述はこうです。

Certain errors are automatically retried 2 times by default, with a short exponential backoff. Connection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict, 429 Rate Limit, and >=500 Internal errors are all retried by default.
(和訳:一部のエラーは既定で2回、短い指数バックオフを挟んで自動的に再試行されます。接続エラー、408 Request Timeout、409 Conflict、429 Rate Limit、および500番台のエラーは、いずれも既定で再試行の対象です)

再試行されるのは「同じリクエストをもう一度送っても安全な場合」だけです。同じ公式ドキュメントは、ストリーミング応答の消費中は自動再試行しないと明記しています。受け取り済みの出力を重複させるためです。副作用のある処理を自前でリトライするときも、同じ危険が自分側に発生します。


ここまでのまとめ

分かったこと 数字
素のHTTPは1回で諦める 到達1回・失敗
SDKと自前実装はやっていることが同じ 到達3回・両方成功
差が出るのはコード量 11行対3行
SDKの待ち時間は倍々にゆらぎを掛けた形 約0.5秒→約0.8秒

ここまでは、SDKが引き受けてくれる側の話でした。次は請求書のほうを見ます。


4. SDKのコスト ── その1行のimportで何を抱え込むか

前節までで、SDKは自前で書けば11行になる処理を3行に畳んでくれると分かりました。ただしこれは得の側だけを数えた計算です。代行業者を雇えば、業者が連れてくるスタッフの分も事務所に入ってきます。その人数と占有面積を数えない限り、採否は判断できません。

このセクションで分かること:SDKを1つ入れたときのインストールサイズ、依存パッケージ数、import時間の実測値。

素のHTTPクライアント requests と、SDKである openai を、それぞれ独立したディレクトリに入れて比較しました。

# cost_bench.py(抜粋)サイズ・依存数・import時間を測る
def dir_size_mb(d):
    total = sum(os.path.getsize(os.path.join(r, f))
                for r, _, fs in os.walk(d) for f in fs)
    return round(total / 1024 / 1024, 1)

def dist_count(d):                       # .dist-info の数 = 入ったパッケージ数
    return len([x for x in os.listdir(d) if x.endswith(".dist-info")])

def _timeit(code):                       # 別プロセスで実行して所要時間を取る
    t0 = time.perf_counter()
    subprocess.run([sys.executable, "-c", code], check=True)
    return time.perf_counter() - t0

def import_sec(d, mod):                  # 5回測って最小値を採用する
    code = f"import sys; sys.path.insert(0, {d!r}); import {mod}"
    return round(min(_timeit(code) for _ in range(5)), 3)

base = min(_timeit("pass") for _ in range(5))   # 素のPython起動時間

結果です。import時間は、Python自体の起動時間(実測0.011秒)を差し引いた値です。

対象 インストールサイズ 入るパッケージ数 import時間
requests 2.9 MB 5 0.115秒
openai 26.8 MB 14 0.480秒

サイズで9.2倍、パッケージ数で2.8倍、import時間で4.2倍でした。openai 側の14個は、本体のほかに pydantic、pydantic_core、httpx2、httpcore2、anyio、jiter などです。型定義を持つとは、その型を検証する仕組みを丸ごと抱えることでもあります。

この数字が効く場面は限られますが、効くときははっきり効きます。

  • サーバーレス関数のコールドスタート。import時間の0.4秒差は呼び出しのたびに乗ります
  • Dockerイメージのサイズ。デプロイ時間と転送量に直結します
  • 脆弱性追従。14個は14個分の更新通知を受け取るということです

逆に、長く動き続けるWebアプリやバッチサーバでは、0.4秒のimportも26MBもまず問題になりません。このコストは絶対値ではなく、起動回数とデプロイ頻度に掛け算されて初めて意味を持つと考えてください。


5. 既定値は黙って変わる ── バージョンを固定すべき理由

前節で、SDKのコストは起動回数に掛かると分かりました。見落としがちなのは、掛け算の相手であるSDK側の挙動が固定されていない点です。代行業者の業務マニュアルは、こちらに相談なく改訂されます。

このセクションで分かること:SDKの既定値が実際に変わった事例と、それに備えるための固定のしかた。

実例があります。AWS SDK for Python(Boto3)は、2026年5月20日付の公式アナウンスでリトライ既定値の変更を予告しました。

When you set AWS_NEW_RETRIES_2026=true, the default retry mode changes from legacy to standard. Several other retry defaults also update.
(和訳:AWS_NEW_RETRIES_2026=true を設定すると、既定のリトライモードが legacy から standard に変わります。ほかのリトライ既定値もいくつか同時に更新されます)

変更内容は次の通りです。

設定 変更前 変更後
既定のリトライモード legacy standard
一時的エラーの基準待ち時間 1,000 ms 50 ms
最大試行回数 5 3
リトライ枠 なし 500トークン

オプトインは botocore 1.43.3以降で可能、既定としての展開は2026年11月以降とされています。注目すべきは最大試行回数が5から3に減る点です。アナウンス自身がこう書いています。

Max attempts decreased. If your workload relied on a higher retry count to eventually succeed, you may see more errors surfaced to your code.
(和訳:最大試行回数が減りました。最終的に成功するために多めの再試行回数に依存していたワークロードでは、より多くのエラーがコードまで表面化する可能性があります)

つまり自分のコードを1行も変えていないのに、本番の失敗率が変わりうるということです。第2節で見たとおりSDKと自前実装は同じことをしています。違うのは、自前なら勝手に変わらない点だけです。

SemVerがあるから大丈夫とも言い切れません。OpenAIのPythonライブラリは、バージョニング方針をこう書いています。

This package generally follows SemVer conventions, though certain backwards-incompatible changes may be released as minor versions.
(和訳:本パッケージはおおむね SemVer の慣習に従いますが、後方互換性のない一部の変更がマイナーバージョンとしてリリースされる場合があります)

対策は2つです。1つ目は、バージョンを範囲ではなく実数で固定すること。

# requirements.txt
openai==3.16.2

2つ目は、挙動を当てにしている設定を、既定に任せず自分で書くことです。

from openai import OpenAI

# 既定が2でも、そのつもりなら2と書く。将来の既定値変更に巻き込まれない
client = OpenAI(max_retries=2, timeout=30.0)

既定値に依存したコードは、テストにも表れません。「今は2回リトライしているはず」という前提はどこにも書かれていないため、変更されても差分に出ません。数字を書くことが、そのままドキュメントになります。


6. 判定フロー ── どちらを選ぶか

前節までで、SDKの利得(8行分の実装と既定の再試行)と負債(26.8 MB・14パッケージ・変わりうる既定値)が両方とも数字になりました。あとは案件に当てはめるだけです。

このセクションで分かること:SDKを使うか素のHTTPで書くかを、3つの問いで決める手順。

判断の分かれ目を言葉にすると次の通りです。

状況 選ぶもの 理由
認証が複雑(署名・トークン更新・多段認証) SDK 自前実装の事故率が最も高い領域
エンドポイントが1つだけで、失敗しても手動再実行でよい 素のHTTP 26.8 MBに見合う仕事がない
ページネーションを跨いで全件取得する SDK 打ち切り漏れが静かに起きる
コールドスタートが課金と体験に直結する 素のHTTP import時間が呼び出し回数に乗る

実務では同じ判断を別の言語でも下すことになります。TypeScriptの公式SDKでも、押さえる場所は同じです。

import OpenAI from "openai";

// 既定値に任せず、Python版と同じ方針で明示する
const client = new OpenAI({ maxRetries: 2, timeout: 30_000 });

const res = await client.chat.completions.create({
  model: "gpt-4o-mini",
  messages: [{ role: "user", content: "hi" }],
});

言語が変わっても、固定すべきものは変わりません。バージョンと、リトライ回数と、タイムアウトの3つです。


7. トラブルシューティング

症状 原因として多いもの 対処
ローカルでは動くが本番で時々失敗する 既定のリトライ対象外のエラーが返っている 返ってきたステータスコードをログに出し、再試行対象かを確認する
SDK更新後、失敗率が上がった 既定の最大試行回数が減った max_retries や max_attempts を明示的に指定する
リクエストが二重に処理された リトライで同じ操作が2回届いた 冪等キーを付ける。対応SDKなら既定で付与される
コールドスタートが遅い SDKのimportに時間がかかっている python -X importtime で内訳を出し、遅延importを検討する
一覧取得の件数が想定より少ない ページネーションを追い切れていない SDKの自動ページャを使うか has_next_page 相当を確認する
429が延々と返り続ける 全クライアントが同時刻に再試行している ゆらぎ付きのバックオフを使う。自前実装ならゆらぎの項を必ず入れる

8. 用語集

用語 定義 たとえでの対応
SDK ある基盤を特定の言語から使うための道具一式 手続き代行業者そのもの
API 呼び出し側と提供側の取り決め 窓口の受付ルール
指数バックオフ 失敗のたびに待ち時間を倍々にする再試行戦略 出直す間隔を倍に延ばす
ジッタ 再試行時刻を意図的にばらつかせる乱数 他の人と同じ時刻に押しかけない
冪等性 同じ操作を何度実行しても結果が変わらない性質 二度出しても受理は1件のまま
ページネーション 大量の結果を分割して返す仕組み 5枚綴りの書類
SemVer 3つの数字で互換性を表すバージョン規約 業務マニュアルの改訂番号
リトライ枠 失敗多発時に再試行そのものを抑える予算 出直し回数を全体で管理する

9. 学習ロードマップ

次に踏むとよい段はOpenAPI仕様です。この記事で見た型定義もページャも、仕様書から生成されたものでした。仕様を書く側に回ると、SDKが何を生成できて何を生成できないかが分かります。

関連記事として、呼び出される側の取り決めそのものはAPI、配布形態を支える文化はOSS、SDK経由で叩く相手として最も増えている対象はLLMに置いています。


まとめ

SDKは魔法ではありませんでした。実測で確かめたのは次の点です。

  • SDKと自前実装は、サーバに届くリクエスト回数が同じ3回で、やっていることは同一
  • 差はコード量に出て、今回は11行対3行
  • 代償はサイズ9.2倍、依存パッケージ2.8倍、import時間4.2倍
  • その代償の重さは、起動回数とデプロイ頻度に掛け算されて決まる
  • SDKの既定値は変わる。2026年にBoto3の最大試行回数は5から3になる予定

判断は第6節のフロー図1枚に畳んであります。迷ったら、「失敗したときの挙動を自分のリポジトリに置きたいか」という1問に翻訳してください。置きたいなら素のHTTP、置きたくないならSDKです。どちらを選んでも、リトライ回数とタイムアウトとバージョンだけは自分で書く。それが今回の結論です。

私自身、測るまでは「SDKは便利だから入れる」で止まっていました。8行という数字が出たので、次からは30分悩まずに済みそうです。

計測環境:Ubuntu 24.04.4 LTS / Xeon 1コア / メモリ4.0 GB / Python 3.12.3 / openai 3.16.2 / requests 2.34.2 / pydantic 2.13.5。擬似APIサーバは標準ライブラリの http.server を127.0.0.1で動かしています。ネットワーク遅延を含まないため、所要時間の絶対値は実環境より小さく出ます。


参考文献


計測に使ったスクリプトの話や、次に測る予定のものはXに流しています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?