และแล้ว วันที่ผมรอคอย (รวมถึงชาว .NET Core บน. AWS Lambda) ก็มาถึง เมื่อ AWS ประกาศรองรับ .NET Core 3.1 เป็น Official Runtime แล้วจ้า (จริงๆ ประกาศตั้งแต่ 31 มีนาคม 2563 แต่ผมเห็นช้าไปหน่อย แหะๆ) ต้องขอทดสอบสักหน่อยว่าดีขึ้นแค่ไหน แต่เฉลยไว้ก่อนเลยว่า ลดเวลาการเกิด Cold Start จาก .NET Core 2.1 ได้ถึง 50% เลยทีเดียว

ผมเคยเขียนในบล็อกเรื่อง ทดสอบความเร็ว .NET Core 3.0 และฟีเจอร์ ReadyToRun (R2R) ในการทำ AWS Lambda ซึ่งตอนนั้น ได้ทำเป็น Custom Runtime และผลลัพธ์ที่ได้ .NET Core 3.0 เร็วกว่า 2.1 นิดเดียว แทบไม่เห็นความต่าง ดีขึ้นแค่เสถียร เพราะมันไปช้าที่ AWS Custom Library ดังนั้นผมจึงแนะนำให้ทุกคนไปใช้ LambdaNative แทน แต่ก็ยังมีปัญหาว่า Microsoft.DotNet.ILCompiler ไม่รองรับการทำงานที่ซับซ้อน อย่างการเรียกใช้งาน Database ร่วมกับ EFCore หรือ Dapper จึงทำให้ต้องเลี่ยงไปใช้ต่อตรงด้วย ADO.NET แทน

ปัจจัยที่ทำให้ .NET Core 3.1 ทำงานได้ไวใน AWS Lambda

จากที่ทดสอบเอง, ดู Benchmark, และอ่านๆ จากหลายแหล่ง พบว่ามีหลักๆ มี 2 เรื่อง ที่เป็นฟีเจอร์ใหม่ใน .NET Core 3 ที่ช่วยให้ AWS Lambda ทำงานได้ไวขึ้น คือ

  1. Built-in JSON support : ซึ่งปกติผู้เขียน C# เวลาต้องแปลง Object, Array ออกไปเป็น JSON จะต้องใช้ Library อย่างเช่น Newtonsoft.Json ซึ่งมันดีนะ แต่มันก็คือการจัดการ String บน .NET แบบ UTF-16 ซึ่งจะเปลืองทรัพยากรมากในการ convert ไปๆมาๆ

    แต่สำหรับ .NET Core 3.0 เป็นต้นมา ได้ built-in JSON Converter เข้าไปเลย และทำงานแบบ UTF-8 encoder ตั้งแต่ต้น จึงไวขึ้น และเมื่อ Core มันมี JSON Converter มาแล้ว ก็ไม่ต้องเรียก 3rd Party Library จึงทำให้ AWS Lambda เราตัวเล็กลงด้วย (ดูการเปรียบเทียบ The Battle of C# to JSON Serializers in .NET Core 3)
  2. ReadyToRun : เป็นฟีเจอร์ที่ Microsoft ภูมิใจนำเสนอมา ตั้งแต่ปล่อยตัว .NET Core 3.0 Preview 6 แล้ว นั่นคือทำให้ .NET Core application แปลงเป็น Native App ส่งผลให้มีการทำงานที่ไวขึ้น มี startup time ที่ดีขึ้น (AOT – ahead-of-time) เพราะไปลดปริมาณงานที่ต้องแปลง Byte Code เป็น Executable Code ลง (JIT – Just in Time)

ดังนั้น แม้ว่า 3.1 จะเร็วขึ้นมาบ้าง แต่ก็ยังแนะนำให้ทำ AWS Lambda เป็น ReadyToRun นะครับ

การ Migrate ไปใช้ .NET Core 3.1 Runtime บน AWS Lambda

แก้แค่ 3 จุดหลักๆ (ส่วนโค้ดอื่นๆ ที่ไม่รองรับหลังจากทำ 3+1 ข้อนี้ ก็ไปตามแก้กันต่ออีกนะ)

1. เปลี่ยน Target Framework ใหม่

<TargetFramework>netcoreapp3.1</TargetFramework>

2. เปลี่ยนการเรียกใช้ JSON ใหม่

ใช้ JSON Package ใหม่ชื่อ

Amazon.Lambda.Serialization.SystemTextJson

และระบุใน Class Handler ใหม่เป็น

[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

3. ใน Deployment Config เปลี่ยน Function Runtime ใหม่ (aws-lambda-tools-defaults.json)

"framework" : "netcoreapp3.1",
"function-runtime":"dotnetcore3.1",

ในกรณี ต้องการทำเป็น ReadyToRun จะต้องเพิ่มใน Csproj file ด้วยดังนี้

<PublishReadyToRun>true</PublishReadyToRun>
<TieredCompilation>false</TieredCompilation>
<TieredCompilationQuickJit>false</TieredCompilationQuickJit>

ที่ต้องปิด Tiered Compilation เพราะจะเกิด Startup Time สูงขึ้น และใช้ Memory เยอะขึ้นโดยใช่เหตุ (อ้างอิง Performance จาก .NET Core 3.0 AWS Lambda Benchmarks and Recommendations และหากใครต้องการเข้าใจ What’s new in .NET Core 3.0)

และถ้าจะใช้ ReadyToRun จะต้อง Build และ Publish เป็น Linux Runtime นะครับ ไม่สามารถ Build Image บน Windows/Mac แล้วเอาขึ้นไปทำงานบน AWS Lambda ได้ เนื่องจากบนนั้นจะเป็น Amazon Linux 2 (ไปดูลิงค์ Demo ของผมที่ด้านล่างได้ ว่าทำอย่างไร)

ผลทดสอบ Cold Start ของ AWS Lambda .NET Core 2.1 เมื่อเทียบกับ 3.1 และ 3.1 ReadyToRun

ผมใช้โค้ดทดสอบตัวเดิมกับที่เคยทดสอบในบล็อก ทดสอบความเร็ว .NET Core 3.0 และฟีเจอร์ ReadyToRun (R2R) ในการทำ AWS Lambda ซึ่งได้ลองรัน 2.1 ก่อน จากนั้น migrate ไปเป็น 3.1 เพื่อเก็บข้อมูล และสุดท้ายก็ Compile ไปเป็น 3.1 ReadyToRun เพื่อเก็บข้อมูล

โดยการทำงานของโค้ดจะมีการเชื่อมต่อ MySQL Database 5.6 โดยมี table ชื่อ members ที่มี 3 รายการ (ค่าจะผันผวนบ้าง อาจเกิดจากการเรียกใช้ MySQL Database)

จะสังเกตได้ว่า แค่แปลงไปใช้ Runtime 3.1 และ Built-in JSON ก็ลดเวลาการเกิด Cold Start ลงไปหนึ่งเท่าตัวเลยทีเดียว

และเมื่อทำเป็น 3.1 ReadyToRun ก็เร็วขึ้นอีกราวๆ 100ms และเมื่อเพิ่ม config เพื่อปิดการใช้ Tiered Compilation ก็ทำให้เร็วขึ้นไปอีก 100-300ms เมื่อใช้ Memory 128MB และ 256MB

แต่มีข้อสังเกตว่า การทำ ReadyToRun มีความเสถียรขึ้น รันครั้งที่ 2 เป็นต้นไป ไม่เจอว่าเกิน 100ms เลย (อย่าลืมว่า AWS Lambda คิดเงินทุกๆ 100ms ดังนั้นเมื่อเกิน มันจะปัดเศษขึ้นไปเป็นอีก 100ms) ส่วนความเร็วที่ผันผวนขึ้นลง 3-5ms ผมคิดว่ามีความต่างน้อยมากๆ ไม่จำเป็นต้องใส่ใจก็ได้ ถ้างานเราไม่ได้ซีเรียสกับเวลาแค่นี้

และเมื่อเราไม่ใช้ LambdaNative กับ Microsoft.DotNet.ILCompiler  ตัวปัญหาแล้ว มันจึงทำให้เราใช้ Library ต่างๆ อย่าง Dapper ได้แล้วครับ ไม่ต้องใช้ ADO มา Mapping Object เองอีกต่อไป แต่ก็ต้องแลกกับเวลาที่มากขึ้นอีก 15-30%

สรุป

ใครใช้ .NET Core 2.1 บน AWS Lambda อยู่ หรือใช้ LambdaNative หรือใช้ Custom Runtime ตามที่ผมเคยเขียนถึง แนะนำให้เปลี่ยนมาใช้ .NET Core 3.1 ของ AWS Official Runtime ครับ ไวกว่ามาก และถ้าเป็นไปได้ ก็ควรทำให้เป็น ReadyToRun Image ด้วย เพื่อประสิทธิภาพที่ดีขึ้นครับ

ส่วนใครอยากลองดูโค้ดตัวอย่าง ดูได้จากด้านล่างนี้เลย

Code Demo

อ้างอิง

Published by iFew

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

Leave a comment

Your email address will not be published. Required fields are marked *

Exit mobile version