“ Kubernetes เพิ่มเวลาแฝง 10 เท่า”: ใครจะตำหนิเรื่องนี้?

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

“ Kubernetes เพิ่มเวลาแฝง 10 เท่า”: ใครจะตำหนิเรื่องนี้?

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

เมื่อสองสามสัปดาห์ที่ผ่านมา ทีมของฉันกำลังย้ายไมโครเซอร์วิสเดียวไปยังแพลตฟอร์มหลักที่รวม CI/CD, รันไทม์ที่ใช้ Kubernetes, ตัววัด และคุณสมบัติอื่นๆ การย้ายดังกล่าวมีลักษณะเป็นการทดลอง: เราวางแผนที่จะใช้เป็นพื้นฐานและโอนบริการเพิ่มเติมประมาณ 150 รายการในอีกไม่กี่เดือนข้างหน้า พวกเขาทั้งหมดมีหน้าที่รับผิดชอบในการดำเนินงานของแพลตฟอร์มออนไลน์ที่ใหญ่ที่สุดบางแห่งในสเปน (Infojobs, Fotocasa ฯลฯ)

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

เหตุใดเวลาแฝงใน Kubernetes จึงสูงกว่าใน EC2 มาก

เพื่อค้นหาปัญหาคอขวด เราได้รวบรวมตัวชี้วัดตามเส้นทางคำขอทั้งหมด สถาปัตยกรรมของเรานั้นเรียบง่าย: พร็อกซีเกตเวย์ API (Zuul) ร้องขอไปยังอินสแตนซ์ไมโครเซอร์วิสใน EC2 หรือ Kubernetes ใน Kubernetes เราใช้ NGINX Ingress Controller และแบ็กเอนด์ก็เป็นออบเจ็กต์ธรรมดาๆ การใช้งาน ด้วยแอปพลิเคชัน JVM บนแพลตฟอร์ม Spring

                                  EC2
                            +---------------+
                            |  +---------+  |
                            |  |         |  |
                       +-------> BACKEND |  |
                       |    |  |         |  |
                       |    |  +---------+  |                   
                       |    +---------------+
             +------+  |
Public       |      |  |
      -------> ZUUL +--+
traffic      |      |  |              Kubernetes
             +------+  |    +-----------------------------+
                       |    |  +-------+      +---------+ |
                       |    |  |       |  xx  |         | |
                       +-------> NGINX +------> BACKEND | |
                            |  |       |  xx  |         | |
                            |  +-------+      +---------+ |
                            +-----------------------------+

ดูเหมือนว่าปัญหาจะเกี่ยวข้องกับเวลาแฝงเริ่มต้นในแบ็กเอนด์ (ฉันทำเครื่องหมายพื้นที่ปัญหาบนกราฟเป็น "xx") บน EC2 การตอบสนองของแอปพลิเคชันใช้เวลาประมาณ 20 มิลลิวินาที ใน Kubernetes เวลาแฝงเพิ่มขึ้นเป็น 100-200 ms

เราไล่ผู้ต้องสงสัยที่เกี่ยวข้องกับการเปลี่ยนแปลงรันไทม์ออกอย่างรวดเร็ว เวอร์ชัน JVM ยังคงเหมือนเดิม ปัญหาเกี่ยวกับคอนเทนเนอร์ไม่ได้เกี่ยวข้องอะไรด้วย เนื่องจากแอปพลิเคชันทำงานได้สำเร็จแล้วในคอนเทนเนอร์บน EC2 กำลังโหลด? แต่เราสังเกตเห็นเวลาแฝงที่สูงแม้ที่ 1 คำขอต่อวินาที การหยุดเก็บขยะก็อาจละเลยได้เช่นกัน

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

สมมติฐานที่ 1: การจำแนกชื่อ DNS

สำหรับแต่ละคำขอ แอปพลิเคชันของเราจะเข้าถึงอินสแตนซ์ AWS Elasticsearch หนึ่งถึงสามครั้งในโดเมนที่คล้ายกัน elastic.spain.adevinta.com. ภายในภาชนะของเรา มีเปลือกอยู่เพื่อให้เราสามารถตรวจสอบว่าการค้นหาโดเมนใช้เวลานานจริงหรือไม่

แบบสอบถาม DNS จากคอนเทนเนอร์:

[root@be-851c76f696-alf8z /]# while true; do dig "elastic.spain.adevinta.com" | grep time; sleep 2; done
;; Query time: 22 msec
;; Query time: 22 msec
;; Query time: 29 msec
;; Query time: 21 msec
;; Query time: 28 msec
;; Query time: 43 msec
;; Query time: 39 msec

คำขอที่คล้ายกันจากหนึ่งในอินสแตนซ์ EC2 ที่แอปพลิเคชันทำงานอยู่:

bash-4.4# while true; do dig "elastic.spain.adevinta.com" | grep time; sleep 2; done
;; Query time: 77 msec
;; Query time: 0 msec
;; Query time: 0 msec
;; Query time: 0 msec
;; Query time: 0 msec

เมื่อพิจารณาว่าการค้นหาใช้เวลาประมาณ 30 มิลลิวินาที เห็นได้ชัดว่าการแก้ไข DNS เมื่อเข้าถึง Elasticsearch มีส่วนทำให้เวลาแฝงเพิ่มขึ้นอย่างแน่นอน

อย่างไรก็ตาม นี่เป็นเรื่องแปลกด้วยเหตุผลสองประการ:

  1. เรามีแอปพลิเคชัน Kubernetes มากมายที่โต้ตอบกับทรัพยากร AWS โดยไม่ได้รับผลกระทบจากเวลาแฝงที่สูง ไม่ว่าจะด้วยเหตุผลใดก็ตาม เกี่ยวข้องกับกรณีนี้โดยเฉพาะ
  2. เรารู้ว่า JVM ทำการแคช DNS ในหน่วยความจำ ในภาพของเรา ค่า TTL จะถูกเขียนไว้ $JAVA_HOME/jre/lib/security/java.security และตั้งเป็น 10 วินาที: networkaddress.cache.ttl = 10. กล่าวอีกนัยหนึ่ง JVM ควรแคชการสืบค้น DNS ทั้งหมดเป็นเวลา 10 วินาที

เพื่อยืนยันสมมติฐานแรก เราจึงตัดสินใจหยุดการโทร DNS ชั่วคราวและดูว่าปัญหาหายไปหรือไม่ อันดับแรก เราตัดสินใจกำหนดค่าแอปพลิเคชันใหม่เพื่อให้สื่อสารโดยตรงกับ Elasticsearch ด้วยที่อยู่ IP แทนที่จะสื่อสารผ่านชื่อโดเมน สิ่งนี้จะต้องมีการเปลี่ยนแปลงรหัสและการใช้งานใหม่ ดังนั้นเราจึงแมปโดเมนกับที่อยู่ IP ของมัน /etc/hosts:

34.55.5.111 elastic.spain.adevinta.com

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

การวินิจฉัยผ่านเครือข่าย

เราตัดสินใจวิเคราะห์การรับส่งข้อมูลจากคอนเทนเนอร์โดยใช้ tcpdumpเพื่อดูว่าเกิดอะไรขึ้นบนเครือข่าย:

[root@be-851c76f696-alf8z /]# tcpdump -leni any -w capture.pcap

จากนั้นเราได้ส่งคำขอหลายรายการและดาวน์โหลดการจับภาพ (kubectl cp my-service:/capture.pcap capture.pcap) เพื่อการวิเคราะห์เพิ่มเติมใน Wireshark.

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

“ Kubernetes เพิ่มเวลาแฝง 10 เท่า”: ใครจะตำหนิเรื่องนี้?

หมายเลขพัสดุจะแสดงอยู่ในคอลัมน์แรก เพื่อความชัดเจน ฉันได้กำหนดรหัสสีให้กับสตรีม TCP ต่างๆ

สตรีมสีเขียวที่เริ่มต้นด้วยแพ็กเก็ต 328 แสดงให้เห็นว่าไคลเอนต์ (172.17.22.150) สร้างการเชื่อมต่อ TCP กับคอนเทนเนอร์ (172.17.36.147) อย่างไร หลังจากการจับมือครั้งแรก (328-330) ก็นำพัสดุ 331 มาด้วย HTTP GET /v1/.. — คำขอเข้ามาใช้บริการของเรา กระบวนการทั้งหมดใช้เวลา 1 มิลลิวินาที

สตรีมสีเทา (จากแพ็กเก็ต 339) แสดงว่าบริการของเราส่งคำขอ HTTP ไปยังอินสแตนซ์ Elasticsearch (ไม่มีการแฮนด์เชค TCP เนื่องจากใช้การเชื่อมต่อที่มีอยู่) ใช้เวลา 18 มิลลิวินาที

จนถึงตอนนี้ทุกอย่างเรียบร้อยดี และเวลาโดยประมาณสอดคล้องกับความล่าช้าที่คาดไว้ (20-30 มิลลิวินาที เมื่อวัดจากไคลเอนต์)

อย่างไรก็ตาม ส่วนสีน้ำเงินใช้เวลา 86ms เกิดอะไรขึ้นในนั้น? ด้วยแพ็กเก็ต 333 บริการของเราส่งคำขอ HTTP GET ไปที่ /latest/meta-data/iam/security-credentialsและหลังจากนั้น จะมีการร้องขอ GET อีกรายการหนึ่งไปยังการเชื่อมต่อ TCP เดียวกัน /latest/meta-data/iam/security-credentials/arn:...

เราพบว่าสิ่งนี้เกิดขึ้นซ้ำกับทุกคำขอตลอดการติดตาม การแก้ไข DNS นั้นช้ากว่าเล็กน้อยในคอนเทนเนอร์ของเรา (คำอธิบายสำหรับปรากฏการณ์นี้ค่อนข้างน่าสนใจ แต่ฉันจะบันทึกไว้สำหรับบทความแยกต่างหาก) ปรากฎว่าสาเหตุของความล่าช้าเป็นเวลานานคือการเรียกบริการ AWS Instance Metadata ในแต่ละคำขอ

สมมติฐานที่ 2: การเรียก AWS โดยไม่จำเป็น

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

/ # curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
arn:aws:iam::<account_id>:role/some_role

คำขอที่สองขอสิทธิ์ชั่วคราวจากปลายทางที่สองสำหรับอินสแตนซ์นี้:

/ # curl http://169.254.169.254/latest/meta-data/iam/security-credentials/arn:aws:iam::<account_id>:role/some_role`
{
    "Code" : "Success",
    "LastUpdated" : "2012-04-26T16:39:16Z",
    "Type" : "AWS-HMAC",
    "AccessKeyId" : "ASIAIOSFODNN7EXAMPLE",
    "SecretAccessKey" : "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
    "Token" : "token",
    "Expiration" : "2017-05-17T15:09:54Z"
}

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

AWS Java SDK ควรเข้ามารับผิดชอบในการจัดการกระบวนการนี้ แต่สิ่งนี้ไม่เกิดขึ้นด้วยเหตุผลบางประการ

หลังจากค้นหาปัญหาบน GitHub เราก็พบปัญหา #1921. เธอช่วยเรากำหนดทิศทางที่จะ "ขุด" ต่อไป

AWS SDK อัปเดตใบรับรองเมื่อมีเงื่อนไขข้อใดข้อหนึ่งต่อไปนี้เกิดขึ้น:

  • วันหมดอายุ (Expiration) ล้มลง EXPIRATION_THRESHOLDฮาร์ดโค้ดนานถึง 15 นาที
  • เวลาผ่านไปนับตั้งแต่ความพยายามครั้งล่าสุดในการต่ออายุใบรับรองมากกว่า REFRESH_THRESHOLDฮาร์ดโค้ดเป็นเวลา 60 นาที

หากต้องการดูวันหมดอายุที่แท้จริงของใบรับรองที่เราได้รับ เราได้รันคำสั่ง cURL ข้างต้นจากทั้งคอนเทนเนอร์และอินสแตนซ์ EC2 ระยะเวลาที่ถูกต้องของใบรับรองที่ได้รับจากคอนเทนเนอร์นั้นสั้นกว่ามาก: 15 นาทีพอดี

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

เหตุใดระยะเวลาที่มีผลบังคับใช้ของใบรับรองจึงสั้นลง

ข้อมูลเมตาของอินสแตนซ์ AWS ได้รับการออกแบบมาเพื่อทำงานกับอินสแตนซ์ EC2 ไม่ใช่ Kubernetes ในทางกลับกัน เราไม่ต้องการเปลี่ยนอินเทอร์เฟซของแอปพลิเคชัน สำหรับสิ่งนี้เราใช้ เคียม - เครื่องมือที่ใช้เอเจนต์บนแต่ละโหนด Kubernetes ช่วยให้ผู้ใช้ (วิศวกรปรับใช้แอปพลิเคชันกับคลัสเตอร์) เพื่อกำหนดบทบาท IAM ให้กับคอนเทนเนอร์ในพ็อดราวกับว่าเป็นอินสแตนซ์ EC2 KIAM สกัดกั้นการเรียกไปยังบริการ AWS Instance Metadata และประมวลผลจากแคช โดยที่ได้รับจาก AWS ก่อนหน้านี้ จากมุมมองของแอปพลิเคชันไม่มีอะไรเปลี่ยนแปลง

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

ดังนั้นหากคุณวางค่าเริ่มต้นทั้งสองทับซ้อนกันก็จะเกิดปัญหาขึ้น ใบรับรองแต่ละใบที่มอบให้กับแอปพลิเคชันจะหมดอายุหลังจากผ่านไป 15 นาที อย่างไรก็ตาม AWS Java SDK บังคับให้ต่ออายุใบรับรองใดๆ ที่ยังเหลือเวลาน้อยกว่า 15 นาทีก่อนวันหมดอายุ

ด้วยเหตุนี้ ใบรับรองชั่วคราวจึงถูกบังคับให้ต่ออายุพร้อมกับคำขอแต่ละรายการ ซึ่งทำให้เกิดการเรียก AWS API สองครั้ง และส่งผลให้เวลาแฝงเพิ่มขึ้นอย่างมาก เราพบใน AWS Java SDK คำขอคุณสมบัติซึ่งกล่าวถึงปัญหาที่คล้ายกัน

การแก้ปัญหากลายเป็นเรื่องง่าย เราเพียงแค่กำหนดค่า KIAM ใหม่เพื่อขอใบรับรองที่มีระยะเวลาใช้งานได้นานขึ้น เมื่อสิ่งนี้เกิดขึ้น คำขอเริ่มไหลโดยไม่ต้องมีส่วนร่วมของบริการ AWS Metadata และเวลาแฝงลดลงถึงระดับที่ต่ำกว่าใน EC2 อีกด้วย

ผลการวิจัย

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

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

ในกรณีของเรา เวลาแฝงที่สูงไม่ได้เป็นผลมาจากจุดบกพร่องหรือการตัดสินใจที่ไม่ดีใน Kubernetes, KIAM, AWS Java SDK หรือไมโครเซอร์วิสของเรา เป็นผลมาจากการรวมการตั้งค่าเริ่มต้นสองรายการเข้าด้วยกัน: รายการหนึ่งใน KIAM และอีกรายการใน AWS Java SDK เมื่อพิจารณาแยกกัน พารามิเตอร์ทั้งสองก็สมเหตุสมผล: นโยบายการต่ออายุใบรับรองที่ใช้งานอยู่ใน AWS Java SDK และระยะเวลาใช้งานสั้นของใบรับรองใน KAIM แต่เมื่อคุณรวมเข้าด้วยกัน ผลลัพธ์ก็คาดเดาไม่ได้ โซลูชันที่เป็นอิสระและตรรกะสองรายการไม่จำเป็นต้องสมเหตุสมผลเมื่อนำมารวมกัน

ปล.จากผู้แปล

คุณสามารถเรียนรู้เพิ่มเติมเกี่ยวกับสถาปัตยกรรมของยูทิลิตี้ KIAM สำหรับการผสานรวม AWS IAM กับ Kubernetes ได้ที่ บทความนี้ จากผู้สร้าง

อ่านในบล็อกของเราด้วย:

ที่มา: will.com

เพิ่มความคิดเห็น