EST. 2026 — บันทึกงานเซิร์ฟเวอร์จริง SYSADMIN · SECURITY · AI · DIRECTADMIN เล่มที่ 01
THE SYSADMIN ALMANAC
หน้าหลัก / Linux / ผ่าน sandbox ไม่ได้แปลว่าใช้ได้จริง — สัญญาณหลอก 7 แบบ
Linux

ผ่าน sandbox ไม่ได้แปลว่าใช้ได้จริง — สัญญาณหลอก 7 แบบ

โดย Ton-Or ·26 ต.ค. 2026·อ่าน 9 นาที
ผ่าน sandbox ไม่ได้แปลว่าใช้ได้จริง — สัญญาณหลอก 7 แบบ

sandbox มีไว้ให้ทดสอบว่ารูปแบบคำขอถูก ไม่ได้มีไว้ให้ทดสอบว่าระบบจะทำงานยังไงในโลกจริง ความผิดพลาดที่แพงที่สุดคือเอาพฤติกรรมของ sandbox ไปออกแบบหน้าจอและตรรกะตัดสินใจ

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

บทความนี้ไม่ระบุผู้ให้บริการ

อาการทั้งหมดที่เล่าเป็นรูปแบบที่เจอได้กับ sandbox แทบทุกเจ้า ผมตัดชื่อผู้ให้บริการ ชื่อผู้ใช้ทั้งฝั่งทดสอบและฝั่งจริง รายการที่อยู่ในบัญชีอนุญาต และเลขคำสั่งซื้อออกหมด ประเด็นอยู่ที่วิธีอ่านสัญญาณ ไม่ใช่ว่าใครทำ sandbox ห่วย

สัญญาณที่ 1 — ช้าจนคิดว่าโค้ดค้าง

คำสั่งสร้างรายการบน sandbox ใช้เวลาตอบกลับ 60 ถึง 120 วินาที ส่วน production ตอบกลับในไม่ถึง 5 วินาที ตอนแรกผมคิดว่า HTTP client ผมตั้งค่าอะไรผิด เพราะ timeout ปริยายของไลบรารีอยู่ที่ 30 วินาที ยิงกี่ครั้งก็โดนตัดทุกครั้ง

60-120 วิsandbox ตอบกลับ
5 วิproduction ตอบกลับ
180 วิtimeout ที่ต้องตั้ง
0 รายการค้นเจอหลังสร้างสำเร็จ

ก่อนจะไปแก้อะไร ผมจับเวลาจริงเก็บไว้ก่อน เพราะเถียงกับตัวเองด้วยความรู้สึกว่า "มันช้า" ไม่มีประโยชน์

# จับเวลาจริงหลายรอบ แล้วดูค่าที่แย่ที่สุด ไม่ใช่ค่าเฉลี่ย
for i in $(seq 1 5); do
  curl -sS -o /dev/null -w '%{http_code} %{time_total}\n' \
    --max-time 200 -X POST "$ENDPOINT/create" -d @payload.json
done
# 200 78.412
# 200 112.905
# 200 64.330

ทางแก้ไม่ใช่การไล่ลด timeout แต่ต้องแยกค่าตามสภาพแวดล้อมไปเลย แล้วตั้งฝั่ง sandbox ให้ยาวกว่าที่ช้าที่สุดที่เคยเจอเผื่อไว้อีกครึ่งหนึ่ง

// config/provider.php — แยกค่าตามสภาพแวดล้อม อย่าใช้ค่าเดียวกันทั้งสองฝั่ง
return [
    'endpoint' => env('PROVIDER_ENDPOINT'),
    'timeout'  => (int) env('PROVIDER_TIMEOUT', 180),  // sandbox ช้ากว่ามาก
    'retries'  => (int) env('PROVIDER_RETRIES', 0),    // คำสั่งที่ตัดเงิน ห้าม retry อัตโนมัติ
];
timeout ไม่เท่ากับล้มเหลว

คำสั่งที่มีผลกับโลกภายนอก เช่น สั่งซื้อหรือตัดเงิน เวลา client timeout ไม่ได้แปลว่าฝั่งโน้นไม่ได้ทำ ผมเจอเคสที่ cURL ตัดที่ 180 วินาทีพอดี แล้วอีกสองนาทีต่อมาอีเมลยืนยันคำสั่งซื้อเข้ามา แปลว่ามันสำเร็จ ถ้าโค้ดตั้ง retry อัตโนมัติไว้ตรงนั้น จะกลายเป็นสั่งซ้ำสองครั้ง

คำสั่งที่ตัดเงิน เมื่อ client ตัดการรอที่ 180 วินาที0 วิ180 วิราว 300 วิฝั่งเรารอคำตอบtimeout แล้ว retry ส่งซ้ำผู้ให้บริการทำคำสั่งต่อจนสำเร็จ โดย client ไม่รู้ผลลัพธ์ถ้า retry อัตโนมัติ = สั่งซ้ำสองครั้งอีเมลยืนยันเข้ามา
cURL ตัดที่ 180 วินาที แต่ฝั่งผู้ให้บริการทำคำสั่งสำเร็จแล้ว อีเมลยืนยันตามมาอีกราวสองนาที ถ้าโค้ดตั้ง retry อัตโนมัติไว้ตรงนั้นจะกลายเป็นสั่งซ้ำ

สัญญาณที่ 2 — บอกว่าของไม่ว่าง ทั้งที่ว่าง

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

สิ่งที่ผมทำคือแยกให้ชัดในโค้ดว่า "ไม่ว่าง" กับ "ตอบไม่ชัดเจน" เป็นคนละสถานะ แล้วเขียน log ให้เห็นทั้งคำขอและคำตอบดิบ เวลาย้อนกลับมาดูจะได้ไม่ต้องเดา

$r = $client->check($name);
match (true) {
    $r->available === true  => $this->proceed($name),
    $r->available === false => Log::info('unavailable', ['name' => $name, 'raw' => $r->raw]),
    default                 => Log::warning('ambiguous availability', ['raw' => $r->raw]),
};

สัญญาณที่ 3 — ชื่อที่มีคำว่า test ถูกปฏิเสธ

อันนี้ตลกดี sandbox ปฏิเสธคำขอที่ชื่อรายการมีคำว่า test, demo หรือ sandbox อยู่ข้างใน ซึ่งเป็นคำที่คนเทสระบบทุกคนพิมพ์เป็นอย่างแรก พอโดนปฏิเสธด้วยรหัสเดียวกับ "ชื่อไม่ถูกต้อง" ผมก็ไล่ตรวจตัวตรวจสอบรูปแบบฝั่งตัวเองอยู่เกือบชั่วโมง

วิธีที่ผมใช้แล้วจบคือให้ตัวสร้างชื่อสำหรับทดสอบ ออกชื่อที่หน้าตาเหมือนของจริงเสมอ แต่ยังแยกออกได้จากคำนำหน้ากับวันที่

function testName(string $prefix): string
{
    // หน้าตาเหมือนของจริง แต่ไล่ย้อนได้ว่าเกิดจากการทดสอบวันไหน
    return sprintf('%s-%s-%s', $prefix, date('Ymd'), bin2hex(random_bytes(3)));
}
// ตัวอย่างผลลัพธ์: acme-20261121-9f31c0

สัญญาณที่ 4 — สร้างสำเร็จ ตัดเงินแล้ว แต่ค้นไม่เจอ

อันนี้คือตัวที่หลอกหนักสุด ลำดับเหตุการณ์คือคำสั่งสร้างตอบว่าสำเร็จ ยอดเงินในบัญชีทดสอบถูกตัดจริง อีเมลสรุปคำสั่งซื้อเข้ามาจริง แต่พอเรียกคำสั่งดึงรายการทั้งหมดกลับได้ศูนย์รายการ

ผมเสียเวลาไปครึ่งวันกับการไล่ดูว่าส่ง parameter การแบ่งหน้าผิดหรือเปล่า สุดท้ายคำตอบคือ sandbox ไม่ได้บันทึกรายการเข้าบัญชีผู้ใช้ให้เสมอไป แม้คำสั่งสร้างจะสำเร็จทุกขั้น

ลำดับที่หลอกคนเขียนโค้ดcreateตอบสำเร็จ→balanceถูกตัดจริง→emailยืนยันเข้ามา→listคืน 0 รายการ

บทเรียนที่เอาไปใช้ได้จริงคือ อย่าใช้คำสั่งค้นรายการเป็นตัวยืนยันว่าสร้างสำเร็จ ให้ยืนยันจากสิ่งที่ผู้ให้บริการออกให้เป็นหลักฐานของรายการนั้น เช่นเลขอ้างอิงที่ติดมากับคำตอบหรือกับอีเมลสรุป แล้วเก็บเลขนั้นลง DB ฝั่งเราตั้งแต่วินาทีแรก

-- เก็บหลักฐานตั้งแต่ก่อนยิง แล้วค่อยเติมผลทีหลัง
INSERT INTO provider_orders (local_ref, payload_hash, status, created_at)
VALUES (?, ?, 'sending', NOW());

UPDATE provider_orders
   SET status = 'confirmed', remote_ref = ?, confirmed_at = NOW()
 WHERE local_ref = ? AND status = 'sending';

สัญญาณที่ 5 — ยอดที่ถูกตัดไม่ใช่ราคาจริง

รายการที่ production คิดประมาณสิบถึงสิบห้าหน่วย ฝั่ง sandbox ตัดไปสี่สิบกว่าหน่วย เพราะเขาพ่วงบริการเสริมเข้ามาให้ในโหมดทดสอบโดยไม่บอก ถ้าเอาตัวเลขนี้ไปคำนวณส่วนต่างหรือไปโชว์บนหน้าจอ จะพังทันทีวันสลับเป็นของจริง

sandbox

ใช้ยืนยันรูปแบบคำขอ

  • ตอบกลับช้ากว่าหลายสิบเท่า
  • ยอดที่ตัดเป็นเงินปลอมและไม่ตรงราคาจริง
  • ชื่อฟิลด์ราคาในแคตตาล็อกไม่เหมือน production
  • ข้อมูลอาจไม่ถูกบันทึกถาวร
production

ใช้ยืนยันพฤติกรรมจริง

  • ตอบกลับเร็วและคงเส้นคงวา
  • ยอดที่ตัดคือเงินจริง ทดสอบพลาดแล้วเสียเงิน
  • ชื่อฟิลด์ตรงกับเอกสาร
  • ข้อมูลคงอยู่จริง ค้นเจอทุกครั้ง

กฎที่ผมตั้งให้ตัวเองหลังจากนั้นคือ ห้ามออกแบบหน้าจอหรือเขียนตรรกะตัดสินใจจากตัวเลขที่ได้จาก sandbox ถ้ายังไม่มีตารางราคาจริง ให้หน้าจอแสดงว่ายังไม่มีข้อมูลราคาไปก่อน ดีกว่าแสดงเลขที่ผิด

สัญญาณที่ 6 — โครงสร้างข้อมูลแคตตาล็อกไม่เหมือนกัน

ต่อจากข้อที่แล้ว ตอนผมดึงรายการสินค้าพร้อมราคามาเตรียมทำหน้าจอ ตัวรวมผลบอกว่าหาราคาไม่เจอเลยสักรายการ ทั้งที่รายการมีมาเป็นพัน สาเหตุคือ sandbox กับ production ใช้ชื่อฟิลด์ราคาคนละชื่อ โค้ดที่อ่านฟิลด์ตรง ๆ เลยได้ค่าว่าง

วิธีกันคือเขียนตัวแปลงผลลัพธ์ให้ยอมรับได้หลายชื่อฟิลด์ แล้ว ดังให้สุดเวลาไม่เจอ ไม่ใช่เงียบแล้วคืนค่าศูนย์

function priceOf(array $row): ?float
{
    foreach (['price', 'retailPrice', 'yourPrice', 'amount'] as $k) {
        if (isset($row[$k]) && is_numeric($row[$k])) {
            return (float) $row[$k];
        }
    }
    Log::error('price field not found', ['keys' => array_keys($row)]);
    return null;   // คืน null ให้ชั้นบนตัดสินใจ ห้ามคืน 0
}

การคืนศูนย์ตรงนี้คือบั๊กที่อันตรายที่สุดในบทความนี้ เพราะศูนย์เป็นตัวเลขที่ดูสมเหตุสมผล แล้วมันจะไหลไปทั้งระบบโดยไม่มีใครสะดุด

สัญญาณที่ 7 — ทดสอบจากเครื่องตัวเองไม่ได้เลย

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

ผมไม่ไปแก้ด้วยการเปิดช่องเพิ่ม เพราะบ้านใช้ที่อยู่แบบเปลี่ยนไปเรื่อย ๆ ทางที่ผมใช้คือรันคำสั่งบนเครื่องที่ได้รับอนุญาตแล้วดู log สดไปพร้อมกัน

# รันบนเครื่องที่อยู่ในรายการอนุญาต แล้วดู log สดอีกหน้าต่าง
ssh [email protected] \
  'cd /srv/app && php artisan provider:demo-order --confirm-sandbox-mutation --name=acme-20261121-9f31c0'

ssh [email protected] 'tail -f /srv/app/storage/logs/laravel-$(date +%F).log'

สังเกตธงบังคับ --confirm-sandbox-mutation อันนั้นผมใส่เอง เป็นกติกาที่ผมตั้งไว้ว่าคำสั่งไหนที่ไปเปลี่ยนสถานะฝั่งผู้ให้บริการ ต้องพิมพ์ธงนี้ทุกครั้ง ไม่มีค่าปริยาย กันตัวเองเผลอกด enter ซ้ำจากประวัติคำสั่ง

สรุปเป็นรายการตรวจก่อนสลับเป็นของจริง

  1. แยกค่า endpoint, timeout, และ credential ของสองสภาพแวดล้อมออกจากกันคนละชุด อ่านจาก environment ล้วน
  2. ทุกคำสั่งที่มีผลภายนอก ต้องจองสถานะใน DB ฝั่งเราก่อนยิง แล้วเติมผลทีหลังแบบเขียนซ้ำได้
  3. ห้าม retry อัตโนมัติกับคำสั่งที่ตัดเงินหรือสร้างของ ให้ค้างไว้เป็นสถานะรอตรวจสอบแทน
  4. ยืนยันความสำเร็จจากเลขอ้างอิงที่ผู้ให้บริการออกให้ ไม่ใช่จากคำสั่งค้นรายการ
  5. ตัวเลขราคาและปริมาณทุกตัวที่ได้จาก sandbox ให้ถือว่าไม่จริง จนกว่าจะเห็นจาก production
  6. ตัวแปลงผลลัพธ์ต้องดังเมื่อหาฟิลด์ไม่เจอ ห้ามคืนค่าศูนย์หรือค่าว่างเงียบ ๆ
  7. ทดสอบครั้งแรกบน production ให้เลือกรายการที่ราคาต่ำสุดและยกเลิกได้ แล้วเฝ้า log สดตลอดการรัน
สิ่งที่ sandbox ยืนยันได้จริงมีอย่างเดียว

มันยืนยันได้ว่า รูปแบบคำขอของเราถูกต้องและระบบยืนยันตัวตนผ่าน เท่านั้น เรื่องเวลา เรื่องราคา เรื่องความคงอยู่ของข้อมูล และเรื่องกฎทางธุรกิจ ต้องไปพิสูจน์ที่ production ด้วยรายการจริงที่เล็กที่สุดเท่าที่จะเล็กได้

ผมเจอชุดนี้ตอนทำระบบสั่งซื้ออัตโนมัติ แต่พอไปทำงานต่อ API ฝั่งรับชำระเงินก็เจอแบบเดียวกันอีก ที่เล่าไว้ใน ต่อ Open API ธนาคารออก QR รับเงิน แปลว่ามันไม่ใช่เรื่องของผู้ให้บริการรายใดรายหนึ่ง แต่เป็นธรรมชาติของสภาพแวดล้อมทดสอบที่ไม่ได้ผูกกับของจริง ใครจะออกแบบรายการตรวจของตัวเองยาวกว่านี้ก็ตามสะดวกครับ ตรงไหนผมอธิบายตกหล่นหรือเจอสัญญาณหลอกแบบอื่นเพิ่ม เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ

อยากให้เราทำให้ไหม?

ให้เราช่วยติดตั้ง ตั้งค่า ย้ายระบบ หรือแก้ปัญหาเซิร์ฟเวอร์ เพื่อให้คุณมีเวลาไปทำงานที่สำคัญกว่า

คุยเรื่องงานกับเรา

เคยเจองานแบบนี้ไหม? ติดตรงไหน?

มาแลกวิธีกันต่อในบอร์ด LinuxThai

→ คุยต่อในบอร์ด

ความคิดเห็น 0

ยังไม่มีความคิดเห็น — เป็นคนแรกเลย