วิธีปรับปรุงประสิทธิภาพของแอป ASP.NET ในเว็บฟาร์มด้วยการแคช

เผยแพร่แล้ว: 2022-03-11

มีเพียงสองสิ่งที่ยากในวิทยาการคอมพิวเตอร์: การทำให้แคชใช้ไม่ได้และการตั้งชื่อ

  • ผู้เขียน: ฟิล คาร์ลตัน

ข้อมูลเบื้องต้นเกี่ยวกับการแคช

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

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

การแคชเป็นเทคนิคที่ทรงพลังสำหรับการเพิ่มประสิทธิภาพ

แคชในหน่วยความจำ ASP.NET นั้นเร็วมาก
และสมบูรณ์แบบในการแก้ปัญหาแคชเว็บฟาร์มแบบกระจาย
ทวีต

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

ในบทความนี้ เราเน้นที่บทบาทของการแคชในการรับประกันปริมาณงานและประสิทธิภาพของเว็บแอปพลิเคชันที่ออกแบบมาเพื่อรองรับงานจำนวนมาก และฉันจะใช้ประสบการณ์จากโครงการใดโครงการหนึ่งของฉันและจัดหาโซลูชันที่ใช้ ASP.NET เป็นภาพประกอบ

ปัญหาในการจัดการโหลดสูง

ปัญหาจริงที่ฉันต้องแก้ไขไม่ใช่ปัญหาเดิม งานของฉันคือสร้างต้นแบบเว็บแอปพลิเคชันเสาหิน ASP.NET MVC ให้สามารถจัดการกับโหลดสูงได้

ขั้นตอนที่จำเป็นในการปรับปรุงความสามารถในการรับส่งข้อมูลของเว็บแอปพลิเคชันแบบเสาหินคือ:

  • เปิดใช้งานเพื่อเรียกใช้เว็บแอปพลิเคชันหลายชุดพร้อมกัน หลังโหลดบาลานเซอร์ และให้บริการคำขอที่เกิดขึ้นพร้อมกันทั้งหมดอย่างมีประสิทธิภาพ (กล่าวคือ ทำให้สามารถปรับขนาดได้)
  • สร้างโปรไฟล์แอปพลิเคชันเพื่อเปิดเผยปัญหาคอขวดด้านประสิทธิภาพในปัจจุบันและเพิ่มประสิทธิภาพ
  • ใช้การแคชเพื่อเพิ่มปริมาณการประมวลผลคำขออ่าน เนื่องจากโดยทั่วไปแล้วจะเป็นส่วนสำคัญของโหลดแอปพลิเคชันโดยรวม

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

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

ปัญหาทั้งหมดเหล่านี้มีความเกี่ยวข้องในกรณีของฉัน ดังนั้นฉันจึงต้องหาทางเลือกอื่น

การแคชทำงานอย่างไร

แคชในหน่วยความจำ ASP.NET ในตัว ( System.Web.Caching.Cache ) ทำงานเร็วมากและสามารถใช้งานได้โดยไม่ต้องทำซีเรียลไลซ์เซชั่นและโอเวอร์เฮดในการดีซีเรียลไลซ์เซชั่น ทั้งในระหว่างการพัฒนาและขณะรันไทม์ อย่างไรก็ตาม แคชในหน่วยความจำของ ASP.NET มีข้อเสียของตัวเองเช่นกัน:

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

หากการโหลดระดับชั้นฐานข้อมูลเพิ่มเติมไม่ทำให้เกิดปัญหาคอขวดโดยตัวมันเอง การใช้แคชที่กระจายอย่างเหมาะสมดูเหมือนเป็นงานง่ายที่จะจัดการใช่ไหม ไม่ใช่เรื่อง ง่าย แต่ก็เป็น ไปได้ ในกรณีของฉัน การวัดประสิทธิภาพแสดงให้เห็นว่าระดับฐานข้อมูลไม่ควรมีปัญหา เนื่องจากงานส่วนใหญ่เกิดขึ้นในระดับของเว็บ ดังนั้นฉันจึงตัดสินใจใช้แคชในหน่วยความจำ ASP.NET และมุ่งเน้นที่การใช้การซิงโครไนซ์ที่เหมาะสม

ขอแนะนำโซลูชันที่ใช้ ASP.NET

ตามที่อธิบายไว้ วิธีแก้ปัญหาของฉันคือใช้แคชในหน่วยความจำ ASP.NET แทนเซิร์ฟเวอร์แคชเฉพาะ สิ่งนี้ทำให้แต่ละโหนดของเว็บฟาร์มมีแคชของตัวเอง การสืบค้นฐานข้อมูลโดยตรง การคำนวณที่จำเป็น และการจัดเก็บผลลัพธ์ในแคช ด้วยวิธีนี้ การทำงานของแคชทั้งหมดจะรวดเร็วอย่างเห็นได้ชัดด้วยลักษณะหน่วยความจำในหน่วยความจำของแคช โดยปกติ รายการที่แคชจะมีอายุการใช้งานที่ชัดเจนและจะไม่อัปเดตเมื่อมีการเปลี่ยนแปลงหรือเขียนข้อมูลใหม่ ดังนั้น จากตรรกะของเว็บแอปพลิเคชัน มักจะชัดเจนเมื่อรายการแคชควรทำให้ใช้งานไม่ได้

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

ที่นี่ Redis สามารถเข้ามาเล่นได้ พลังของ Redis เมื่อเทียบกับโซลูชันอื่นๆ มาจากความสามารถของ Pub/Sub ลูกค้าทุกรายของเซิร์ฟเวอร์ Redis สามารถสร้างช่องและเผยแพร่ข้อมูลบางส่วนได้ ลูกค้ารายอื่นสามารถฟังช่องสัญญาณนั้นและรับข้อมูลที่เกี่ยวข้องได้ คล้ายกับระบบที่ขับเคลื่อนด้วยเหตุการณ์ ฟังก์ชันนี้สามารถใช้เพื่อแลกเปลี่ยนข้อความการทำให้แคชใช้ไม่ได้ระหว่างโหนด ดังนั้นโหนดทั้งหมดจะสามารถยกเลิกการใช้งานแคชได้เมื่อจำเป็น

กลุ่มของโหนดระดับเว็บ ASP.NET ที่ใช้ Redis backplane

แคชในหน่วยความจำของ ASP.NET นั้นตรงไปตรงมาในบางวิธีและซับซ้อนในบางวิธี โดยเฉพาะอย่างยิ่ง มันตรงไปตรงมาโดยทำงานเป็นแมปของคู่คีย์/ค่า แต่มีความซับซ้อนมากมายที่เกี่ยวข้องกับกลยุทธ์การทำให้เป็นโมฆะและการขึ้นต่อกัน

โชคดีที่กรณีการใช้งานทั่วไปนั้นเรียบง่ายเพียงพอ และเป็นไปได้ที่จะใช้กลยุทธ์การยกเลิกที่เป็นค่าเริ่มต้นสำหรับรายการทั้งหมด ทำให้แต่ละรายการในแคชมีการอ้างอิงสูงสุดเพียงรายการเดียว ในกรณีของฉัน ฉันลงเอยด้วยรหัส ASP.NET ต่อไปนี้สำหรับอินเทอร์เฟซของบริการแคช (โปรดทราบว่านี่ไม่ใช่รหัสจริง เนื่องจากฉันละเว้นรายละเอียดบางอย่างเพื่อความเรียบง่ายและสิทธิ์ใช้งานที่เป็นกรรมสิทธิ์)

 public interface ICacheKey { string Value { get; } } public interface IDataCacheKey : ICacheKey { } public interface ITouchableCacheKey : ICacheKey { } public interface ICacheService { int ItemsCount { get; } T Get<T>(IDataCacheKey key, Func<T> valueGetter); T Get<T>(IDataCacheKey key, Func<T> valueGetter, ICacheKey dependencyKey); }

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

ในการทำให้รายการแคชเป็นโมฆะ ฉันได้แนะนำบริการแยกต่างหากซึ่งมีลักษณะดังนี้:

 public interface ICacheInvalidator { bool IsSessionOpen { get; } void OpenSession(); void CloseSession(); void Drop(IDataCacheKey key); void Touch(ITouchableCacheKey key); void Purge(); }

นอกจากวิธีการพื้นฐานในการดรอปรายการด้วยข้อมูลและปุ่มสัมผัส ซึ่งมีเฉพาะรายการข้อมูลที่อ้างอิงเท่านั้น มีวิธีการสองสามวิธีที่เกี่ยวข้องกับ "เซสชัน" บางประเภท

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

บริการแคชและตัวล้างแคชมีวงจรชีวิตที่แตกต่างกันอย่างมากเกี่ยวกับ IoC บริการแคชได้รับการลงทะเบียนเป็นซิงเกิล (หนึ่งอินสแตนซ์ แชร์ระหว่างไคลเอนต์ทั้งหมด) ในขณะที่ตัวตรวจสอบแคชถูกลงทะเบียนเป็นอินสแตนซ์ต่อคำขอ (อินสแตนซ์ที่แยกต่างหากถูกสร้างขึ้นสำหรับคำขอที่เข้ามาแต่ละรายการ) ทำไม?

คำตอบเกี่ยวข้องกับความละเอียดอ่อนเพิ่มเติมที่เราต้องจัดการ เว็บแอปพลิเคชันกำลังใช้สถาปัตยกรรม Model-View-Controller (MVC) ซึ่งช่วยในการแยกข้อกังวลด้าน UI และตรรกะเป็นหลัก ดังนั้น แอ็คชันคอนโทรลเลอร์ทั่วไปจึงถูกรวมไว้ในคลาสย่อยของ ActionFilterAttribute ในเฟรมเวิร์ก ASP.NET MVC แอตทริบิวต์ C#- ดังกล่าวใช้เพื่อตกแต่งตรรกะการดำเนินการของคอนโทรลเลอร์ในบางวิธี คุณลักษณะเฉพาะนั้นมีหน้าที่รับผิดชอบในการเปิดการเชื่อมต่อฐานข้อมูลใหม่และเริ่มต้นธุรกรรมเมื่อเริ่มต้นการดำเนินการ นอกจากนี้ ในตอนท้ายของการดำเนินการ คลาสย่อยแอตทริบิวต์ตัวกรองมีหน้าที่รับผิดชอบในการทำธุรกรรมในกรณีที่ประสบความสำเร็จและย้อนกลับในกรณีที่เกิดความล้มเหลว

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

นั่นคือจุดประสงค์ที่แท้จริงของส่วนที่เกี่ยวข้องกับ "เซสชัน" ในตัวลบแคช นั่นคือจุดประสงค์ของการผูกมัดตลอดชีวิตกับคำขอ รหัส ASP.NET มีลักษณะดังนี้:

 class HybridCacheInvalidator : ICacheInvalidator { ... public void Drop(IDataCacheKey key) { if (key == null) throw new ArgumentNullException("key"); if (!IsSessionOpen) throw new InvalidOperationException("Session must be opened first."); _postponedRedisMessages.Add(new Tuple<string, string>("drop", key.Value)); } ... public void CloseSession() { if (!IsSessionOpen) return; _postponedRedisMessages.ForEach(m => PublishRedisMessageSafe(m.Item1, m.Item2)); _postponedRedisMessages = null; } ... }

วิธี PublishRedisMessageSafe ที่นี่รับผิดชอบในการส่งข้อความ (อาร์กิวเมนต์ที่สอง) ไปยังแชนเนลเฉพาะ (อาร์กิวเมนต์แรก) อันที่จริง มีช่องทางแยกกันสำหรับการดร็อปและการสัมผัส ดังนั้นตัวจัดการข้อความสำหรับแต่ละรายการจึงรู้ว่าต้องทำอย่างไร - วาง/แตะคีย์เท่ากับเพย์โหลดข้อความที่ได้รับ

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

 class HybridCacheService : ... { ... public void Initialize() { try { Multiplexer = ConnectionMultiplexer.Connect(_configService.Caching.BackendServerAddress); ... Multiplexer.ConnectionFailed += (sender, args) => UpdateConnectedState(); Multiplexer.ConnectionRestored += (sender, args) => UpdateConnectedState(); ... } catch (Exception ex) { ... } } private void UpdateConnectedState() { if (Multiplexer.IsConnected && _currentCacheService is NoCacheServiceStub) { _inProcCacheInvalidator.Purge(); _currentCacheService = _inProcCacheService; _logger.Debug("Connection to remote Redis server restored, switched to in-proc mode."); } else if (!Multiplexer.IsConnected && _currentCacheService is InProcCacheService) { _currentCacheService = _noCacheStub; _logger.Debug("Connection to remote Redis server lost, switched to no-cache mode."); } } }

ที่นี่ ConnectionMultiplexer เป็นประเภทจากไลบรารี StackExchange.Redis ซึ่งรับผิดชอบการทำงานที่โปร่งใสกับ Redis พื้นฐาน ส่วนสำคัญในที่นี้คือ เมื่อโหนดใดโหนดหนึ่งขาดการเชื่อมต่อกับ Redis โหนดจะกลับไปสู่โหมดไม่มีแคชเพื่อให้แน่ใจว่าจะไม่มีคำขอใดได้รับข้อมูลเก่า หลังจากกู้คืนการเชื่อมต่อแล้ว โหนดจะเริ่มใช้แคชในหน่วยความจำอีกครั้ง

นี่คือตัวอย่างของการดำเนินการโดยไม่ใช้บริการแคช ( SomeActionWithoutCaching ) และการดำเนินการที่เหมือนกันซึ่งใช้งาน ( SomeActionUsingCache ):

 class SomeController : Controller { public ISomeService SomeService { get; set; } public ICacheService CacheService { get; set; } ... public ActionResult SomeActionWithoutCaching() { return View( SomeService.GetModelData() ); } ... public ActionResult SomeActionUsingCache() { return View( CacheService.Get( /* Cache key creation omitted */, () => SomeService.GetModelData() ); ); } }

ข้อมูลโค้ดจากการใช้งาน ISomeService อาจมีลักษณะดังนี้:

 class DefaultSomeService : ISomeService { public ICacheInvalidator _cacheInvalidator; ... public SomeModel GetModelData() { return /* Do something to get model data. */; } ... public void SetModelData(SomeModel model) { /* Do something to set model data. */ _cacheInvalidator.Drop(/* Cache key creation omitted */); } }

การเปรียบเทียบและผลลัพธ์

หลังจากที่ตั้งค่ารหัส ASP.NET สำหรับแคชแล้ว ก็ถึงเวลาที่จะใช้ในตรรกะของเว็บแอปพลิเคชันที่มีอยู่ และการเปรียบเทียบอาจสะดวกต่อการตัดสินใจว่าจะใช้ความพยายามส่วนใหญ่ในการเขียนโค้ดใหม่เพื่อใช้การแคชที่ใด สิ่งสำคัญคือต้องเลือกกรณีการใช้งานทั่วไปหรือที่สำคัญบางกรณีเพื่อทำการเปรียบเทียบ หลังจากนั้น เครื่องมืออย่าง Apache jMeter สามารถใช้ได้สองสิ่ง:

  • เพื่อเปรียบเทียบกรณีการใช้งานคีย์เหล่านี้ผ่านคำขอ HTTP
  • เพื่อจำลองโหลดสูงสำหรับโหนดเว็บที่ทดสอบ

เพื่อให้ได้โปรไฟล์ประสิทธิภาพ คุณสามารถใช้ตัวสร้างโปรไฟล์ใดๆ ที่สามารถแนบกับกระบวนการของผู้ปฏิบัติงาน IIS ได้ ในกรณีของฉัน ฉันใช้ JetBrains dotTrace Performance หลังจากใช้เวลาทดลองเพื่อกำหนดพารามิเตอร์ jMeter ที่ถูกต้อง (เช่น จำนวนคำขอพร้อมกันและจำนวนคำขอ) จะเริ่มรวบรวมสแนปชอตประสิทธิภาพได้ ซึ่งจะมีประโยชน์มากในการระบุฮอตสปอตและปัญหาคอขวด

ในกรณีของฉัน กรณีการใช้งานบางกรณีแสดงให้เห็นว่าใช้เวลาดำเนินการโค้ดโดยรวมประมาณ 15%-45% ในการอ่านฐานข้อมูลโดยมีจุดคอขวดที่ชัดเจน หลังจากที่ฉันใช้การแคช ประสิทธิภาพเกือบสองเท่า (กล่าวคือ เร็วกว่าสองเท่า) สำหรับส่วนใหญ่

ที่เกี่ยวข้อง: แปดเหตุผลที่ว่าทำไม Microsoft Stack ยังคงเป็นทางเลือกที่ใช้งานได้

บทสรุป

ดังที่คุณอาจเห็น กรณีของฉันอาจดูเหมือนเป็นตัวอย่างของสิ่งที่เรียกว่า "การประดิษฐ์วงล้อใหม่": เหตุใดจึงต้องพยายามสร้างสิ่งใหม่ ๆ ในเมื่อมีแนวทางปฏิบัติที่ดีที่สุดที่นำไปใช้กันอย่างแพร่หลายอยู่แล้ว เพียงแค่ตั้งค่า Memcached หรือ Redis แล้วปล่อยมันไป

ฉันเห็นด้วยอย่างยิ่งว่าการใช้แนวทางปฏิบัติที่ดีที่สุดมักจะเป็นตัวเลือกที่ดีที่สุด แต่ก่อนที่จะใช้แนวทางปฏิบัติที่ดีที่สุดอย่างสุ่มสี่สุ่มห้า เราควรถามตัวเองว่า “แนวปฏิบัติที่ดีที่สุด” นี้มีความเหมาะสมเพียงใด? พอดีกับกรณีของฉันดีหรือไม่?

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

ในท้ายที่สุด ด้วยการแคชที่เหมาะสม ฉันจึงได้รับประสิทธิภาพเพิ่มขึ้นเกือบ 50% จากโซลูชันเริ่มต้น