เหตุผลที่เราเลิกใช้ SDK

@alvinsng
อังกฤษ2 วันที่ผ่านมา · 14 ก.ค. 2569
536K
820
66
62
1.2K

TL;DR

Alvin Sng อธิบายเหตุผลที่ทีมของเขาเปลี่ยนจากการใช้ SDK ของผู้ให้บริการมาเป็น HTTP client แบบกำหนดเอง เพื่อปรับปรุงการดีบั๊ก รวมศูนย์การตรวจสอบระบบ และลดปัญหาความซับซ้อนของ dependency

เราหยุดใช้ Stripe, WorkOS และ Slack client SDK แล้ว และกำลังทยอยย้ายส่วนที่เหลือออกไป แทนที่ด้วยการเรียก REST APIs โดยตรงผ่านคลาส wrapper ขนาดเล็กที่เราเรียกว่า HttpBaseClient

ฟังดูเหมือนถอยหลังเข้าคลอง SDK ควรจะช่วยประหยัดเวลาให้คุณ แต่คุณคงได้ยินเรื่อง AI ที่พอร์ตโค้ดให้รันแบบเนทีฟมากขึ้นเรื่อยๆ แนวคิดเดียวกันนี้ใช้กับ SDK ด้วย ทำไมล่ะ?

  • SDK ซ่อนรายละเอียดดิบๆ เช่น HTTP response headers และ raw response bodies ซึ่งเป็นสิ่งสำคัญสำหรับเอเจนต์ที่กำลังดีบักปัญหาหรือส่งรายละเอียดให้ทีม upstream สอบสวน
  • SDK คาดหวังการตอบสนองที่ถูกต้อง แต่ในความเป็นจริงแล้ว response ที่ผิดรูปกลับมาจากไฟร์วอลล์, load balancer และ gateway ซึ่งเป็นการปิดบังรายละเอียดการดีบักที่เราต้องการ
  • SDK ข้าม forcing function ของจุดศูนย์กลางสำหรับ retries, error handling และ observability ซึ่งทำให้เอเจนต์สามารถเขียนรูปแบบของตัวเองขึ้นมาใหม่ได้
  • SDK มี bloat จำนวนมาก ซึ่งมักจะถูกสร้างขึ้นอัตโนมัติจาก OpenAPI ในขณะที่เราใช้เพียง endpoints เล็กน้อยเท่านั้น

ลองคิดดูว่าเหตุใด SDK จึงถือกำเนิดขึ้น พวกมันเกิดเพราะบริษัทต่างๆ ต้องการให้การนำไปใช้งานรู้สึกง่าย: ส่ง shared library, ประหยัดเวลาของลูกค้า และซ่อนงาน integration ที่ซ้ำซาก แต่ AI ได้เปลี่ยนเส้นต้นทุนนั้นไปแล้ว การ integrate กับ SDK ในปัจจุบันมักใช้工作量พอๆ กับการเรียก HTTP API โดยตรง และการเขียน REST client ที่ปรับแต่งเฉพาะสำหรับ endpoints ที่เราใช้เท่านั้นพร้อมกับให้ observability ที่ดีกว่าแบบครบวงจรนั้นมีต้นทุนถูก

เกม Whack-a-Mole ที่ไม่มีวันจบ

Alvin Sng - inline image

"JSON" responses สุดสวย

นี่คือลักษณะของ Sentry error dashboard ที่เราเคยเห็น เราจะพบว่ามีเปอร์เซ็นต์เล็กน้อยของ requests ที่ล้มเหลวจาก code path บางอัน จากนั้นเราจะ monkey-patch เฉพาะ path นั้นเพื่อจัดการกับความผิดพลาดของ SDK ให้กับผู้ใช้ ในวันถัดไป code path อื่นก็จะสะดุดกับ error ที่ไม่คาดคิดอีก เราก็จะ patch อันนั้นอีก และเกมนี้ก็ไม่มีวันสิ้นสุด

SDK เปราะบางในโหมดความล้มเหลวในการผลิตทั่วไป: เซิร์ฟเวอร์บอกว่ามีบางอย่างผิดปกติ แต่ response ไม่ใช่รูปทรง JSON ที่ SDK คาดหวัง

Nginx gateway ที่โอเวอร์โหลดจะส่ง HTML ที่ไม่คาดคิด Cloudflare บล็อก request ไฟร์วอลล์แสดงหน้า rate-limit API ของผู้ขายมักจะเป็น JSON แต่สิ่งที่อยู่ข้างหน้านั้นไม่ใช่ API ของผู้ขายเสมอไป

เมื่อเกิดเหตุการณ์นั้น SDK หลายตัวพยายาม parse response, ล้มเหลวด้วย generic parsing error และทิ้งส่วนที่มีประโยชน์ไป raw body หายไป status text ถูกฝัง HTTP headers มักจะไม่ถูกส่งกลับมาในรูปแบบที่ใช้งานได้ หากฝ่ายสนับสนุนของผู้ขายขอ request ID จาก HTTP header เราจะหมดหวังเพราะ SDK ไม่ส่งค่านั้นมา

เอเจนต์ใช้ประโยชน์จากความตรงไปตรงมาของ SDK ในทางที่ผิด

การเรียก client SDK ของเราเป็นเหมือนดินแดนตะวันตกที่ไร้กฎหมาย Stripe มีรูปแบบหนึ่ง WorkOS มีอีกรูปแบบหนึ่ง Slack มีลักษณะเฉพาะของตัวเอง Integration อื่นๆ มี raw fetch, การเรียก SDK, retry logic แบบเฉพาะกิจ หรือไม่มี retry logic เลย

SDK ทำให้สิ่งที่ผิดดูเหมือนง่าย เอเจนต์สามารถตรงไปยัง client ของผู้ขายและเรียก stripe.customers.create(...) จาก route ใดก็ได้ ซึ่งดูเหมือนมีประสิทธิภาพ แต่มันข้ามจุดที่ใช้ร่วมกันซึ่ง auth, retries, metrics, logs และ error translation ควรจะอยู่ เรามี closure wrappers แบบนี้เกลื่อนใน codebase:

typescript
1const response = await catchRateLimitError(() =>
2 stripe.customers.retrieve(stripeCustomerId)
3);

ถ้าคุณพลาดจุดใดจุดหนึ่งและลืม wrap การเรียก SDK คุณจะมีวันที่แย่ Stripe rate limit และ WorkOS rate limit มีความหมายเหมือนกันสำหรับผลิตภัณฑ์ของเรา: upstream กำลังขอให้เราลดความเร็วลง แต่ในระดับ type พวกมันเป็นวัตถุที่แตกต่างกันโดยสิ้นเชิง โค้ดบางส่วนจับ exception เฉพาะของ SDK บางส่วนจับ generic Error บางส่วนไม่จับอะไรเลย ซึ่งกลายเป็นเกม Whack-a-Mole ของ Sentry: แก้ไข 429 หนึ่งครั้งที่ call site หนึ่ง รอให้ความล้มเหลวชนิดเดียวกันเกิดขึ้นที่อื่น

SDK มี bloat

แพ็คเกจ NPM openai และ anthropic-ai/sdk ถูกสร้างขึ้นอัตโนมัติจาก OpenAPI specs โดย Stainless Stripe ก็ถูกสร้างขึ้นจาก OpenAPI spec ของ Stripe เช่นกัน นั่นคือวิธีที่คุณปรับขนาดการบำรุงรักษา SDK สาธารณะสำหรับ API ขนาดใหญ่ในภาษาโปรแกรมต่างๆ มากมาย

แต่ SDK สาธารณะที่ยอดเยี่ยมต้องให้บริการทุกคน Backend ของเราไม่ต้องการ SDK ของทุกคน มันต้องการ Stripe แปด endpoints, WorkOS user และ org endpoints และเมธอด Slack ที่เราเรียกจริงๆ Stripe มีขนาด 6.5 MB, workos-inc/node มีขนาด 6.9 MB, slack/web-api มีขนาด 7.7 MB และ linear/sdk มีขนาด 34 MB ในกรณีที่ไกลที่สุด googleapis มีขนาดถึง 198 MB

Generated SDKs นำแพลตฟอร์มทั้งหมดมาด้วย: หลายร้อยเมธอด, overloads, pagination helpers, retry behavior, environment detection, compatibility shims และพื้นผิวเก่าที่ไม่สามารถหายไปได้เพราะยังมีคนบางที่ที่ยังพึ่งพามันอยู่ ภายใน backend ของเราเอง ความกว้างขวางนั้นมักจะเป็น bloat ยิ่งกว่านั้น มันยังขวางอยู่ระหว่างเรากับสายไฟ

เราลืมไปว่า HTTP APIs คือสัญญา API

บางครั้งผู้คนพูดถึง HTTP APIs ราวกับว่ามันเป็นรายละเอียดการใช้งานระดับล่าง และ SDK เป็นอินเทอร์เฟซที่เสถียรจริงๆ REST API สาธารณะคือสัญญา ผู้ขายไม่สามารถละเมิดมันได้ง่ายๆ ผู้เขียน SDK ก็รู้เช่นกัน เพราะพวกเขาไม่สามารถบังคับให้ลูกค้าทุกคนอัปเกรดได้ ลูกค้าจำนวนมากยังคงใช้ SDK เวอร์ชันเก่ามาหลายปีในโพรดักชัน ซึ่งหมายความว่าสัญญา wire แบบเก่าต้องทำงานต่อไปอยู่ดี

HttpBaseClient ของเราเอง

HttpBaseClient คือสิ่งทดแทน client SDK ของเรา Subclass ของ Provider จะจัดหาชิ้นส่วนเฉพาะของผู้ขาย: base URL, auth headers, content type, error mapping และเมธอดที่จำกัดสำหรับ endpoints ที่เราใช้จริง HttpBaseClient เป็นเจ้าของส่วนที่เหลือ: serialization, parsing, transport errors, structured logs, metrics, status mapping และ duration tracking สิ่งนี้รวม observability เข้าด้วยกันเพื่อให้ผู้ขายทุกรายปฏิบัติตามมาตรฐานที่สอดคล้องกัน นี่คือโครงสร้างแบบย่อ:

typescript
1abstract class HttpBaseClient<TEndpoint extends string> {
2 protected abstract readonly baseUrl: string;
3
4 protected constructor(private readonly dependency: string) {}
5
6 protected abstract buildAuthHeaders(): Promise<Record<string, string>>;
7
8 protected async request<TBody, TResponse>(config: {
9 method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
10 path: string;
11 endpoint: TEndpoint;
12 body?: TBody;
13 }): Promise<TResponse> {
14 const url = `${this.baseUrl}${config.path}`;
15 const headers = await this.buildAuthHeaders();
16 const labels = {
17 dependency: this.dependency,
18 endpoint: config.endpoint,
19 method: config.method,
20 };
21 const start = performance.now();
22
23 logInfo('upstream request starting', labels);
24
25 try {
26 const response = await callWithMetrics(
27 () =>
28 fetch(url, {
29 method: config.method,
30 headers,
31 body: config.body === undefined ? undefined : JSON.stringify(config.body),
32 }),
33 this.dependency,
34 labels
35 );
36
37 const body = await parseBody(response);
38 if (!response.ok) throw this.mapHttpError(response, body);
39
40 logInfo('upstream request succeeded', {
41 ...labels,
42 statusCode: response.status,
43 durationMs: performance.now() - start,
44 });
45
46 return body as TResponse;
47 } catch (cause) {
48 logWarn('upstream request failed', {
49 ...labels,
50 durationMs: performance.now() - start,
51 cause,
52 });
53 throw cause;
54 }
55 }
56}
57
58// จากนั้น Stripe wrapper ก็มีขนาดเล็กและชัดเจน:
59enum StripeEndpoint {
60 CustomersCreate = 'stripe/customers/create',
61}
62
63class StripeHttpClient extends HttpBaseClient<StripeEndpoint> {
64 protected readonly baseUrl = 'https://api.stripe.com';
65
66 constructor(private readonly apiKey: string) {
67 super(ClientVendor.Stripe);
68 }
69
70 createCustomer(body: { email: string; name: string }) {
71 return this.request<typeof body, Stripe.Customer>({
72 method: 'POST',
73 path: '/v1/customers',
74 endpoint: StripeEndpoint.CustomersCreate,
75 body,
76 });
77 }
78
79 // เพิ่ม endpoints ได้ที่นี่
80}

นั่นคือเคล็ดลับ คลาสนี้ไม่ได้พยายามจำลอง Stripe ทั้งหมด มันจำลองพฤติกรรม HTTP ที่เราต้องการให้การเรียกผู้ขายทุกครั้งมี Stripe ยังคงได้รับการเข้ารหัสแบบฟอร์ม WorkOS ยังคงได้รับ bearer auth และ JSON bodies Slack ยังคงได้รับพฤติกรรม ok: false แปลกๆ บน HTTP 200 แต่ backend ที่เหลือของเราเห็นรูปร่างที่สอดคล้องกันเพียงหนึ่งเดียว

คุณสามารถดูเวอร์ชันยาวขึ้นของ HttpBaseClient ของเราที่นี่ ซึ่งถูกปรับเปลี่ยนให้เป็น generic และอ่านง่ายขึ้นเพื่อใช้เป็นตัวอย่างโค้ด

จุดที่เรายังคงใช้ SDK

แนวทางปัจจุบันของเราเป็นแบบไฮบริด: ใช้ HTTP client ของเราเองในรันไทม์ แต่ยังคงรักษา SDK ไว้ในที่ที่ type ของมันยังช่วยประหยัดเวลา StripeHttpClient สามารถส่งคืน Stripe.Customer, SlackHttpClient สามารถยืมประเภทอาร์กิวเมนต์ของ slack/web-api และประเภทของ WorkOS ยังคงสามารถอธิบาย response ทาง wire ได้

ฉันคาดว่าเราจะค่อยๆ เลิกใช้ส่วนนี้เช่นกัน เมื่อ AI มีความสามารถในการสร้างและบำรุงรักษาประเภท request และ response ที่แน่นอนตามที่เราต้องการได้ดีขึ้น เหตุผลในการเก็บแพ็คเกจ SDK ทั้งหมดไว้เพียงเพื่อ types ก็จะยิ่งอ่อนแอลง แต่พฤติกรรมรันไทม์เป็นส่วนที่เจ็บปวด ดังนั้นนั่นคือจุดเริ่มต้นของการย้าย

เรายังคงใช้ SDK เมื่อ SDK เป็นขอบเขตของผลิตภัณฑ์ ไม่ใช่แค่ wrapper รอบ REST observability เป็นตัวอย่างที่ชัดเจนที่สุด สำหรับ Sentry, SDK จะจัดการ runtime instrumentation, error capture, scope propagation, release metadata และ integrations ที่เราไม่อยากทำเอง นั่นแตกต่างจากการใช้ SDK ของผู้ขายเป็น thin client สำหรับการเรียก HTTP backend ทั่วไป

ไม่ใช่ทุก API จะเป็น REST บน HTTP และนั่นก็ไม่เป็นไร การเรียกฐานข้อมูลเป็นตัวอย่างที่ดี abstraction ระดับต่ำกว่าของเราคือ BaseClient: มันให้ client แต่ละตัวมี metrics, logging และ error-handling contract เดียวกัน ในขณะที่อนุญาตให้ child class แทนที่ความหมายของ "fetch" สำหรับ transport ของมัน

ทิศทางที่เรากำลังมุ่งหน้าไป

นักพัฒนาจะถือว่าเอกสาร API เป็นแนวทางการ integration ที่แท้จริง และถือว่า SDK เป็น reference implementations: แม่แบบสำหรับ auth, payloads, pagination, retries และกรณีขอบ เวอร์ชันถัดไปของ "ส่ง SDK" อาจเป็น "ส่ง agent skill" ที่สอนเอเจนต์ถึงวิธีการเรียก API อย่างถูกต้อง ใช้รูปแบบที่ถูกต้องซ้ำ และหลีกเลี่ยงการผลักดันการเรียก rันไทม์ทุกครั้งผ่านแพ็คเกจของผู้ขาย

บันทึกในคลิกเดียว

อ่านบทความไวรัลเชิงลึกด้วย AI ใน YouMind

บันทึกแหล่งที่มา ถามคำถามที่ตรงประเด็น สรุปข้อโต้แย้ง และเปลี่ยนบทความไวรัลให้เป็นโน้ตที่นำกลับมาใช้ได้ใน AI เวิร์กสเปซเดียว

สำรวจ YouMind
สำหรับครีเอเตอร์

เปลี่ยน Markdown ของคุณให้เป็นบทความ 𝕏 ที่สะอาดตา

เวลาคุณเผยแพร่งานเขียนยาวของตัวเอง การจัดรูปแบบรูปภาพ ตาราง และบล็อกโค้ดให้เข้ากับ 𝕏 นั้นน่าปวดหัว YouMind เปลี่ยนร่าง Markdown ทั้งฉบับให้เป็นบทความ 𝕏 ที่สะอาดตาและพร้อมโพสต์ทันที

ลอง Markdown เป็น 𝕏

แพตเทิร์นให้ถอดรหัสเพิ่มเติม

บทความไวรัลล่าสุด

สำรวจบทความไวรัลเพิ่มเติม