กลับไปสู่ไมโครเซอร์วิสด้วย Istio ส่วนที่ 2

กลับไปสู่ไมโครเซอร์วิสด้วย Istio ส่วนที่ 2

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

นอกจากนี้เรายังเตือนคุณด้วยว่าบทความนี้ใช้การกำหนดค่า (รายการสำหรับ Kubernetes และ Istio) จากพื้นที่เก็บข้อมูล istio-เชี่ยวชาญ.

การจัดการจราจร

ด้วย Istio ความสามารถใหม่จะปรากฏในคลัสเตอร์เพื่อมอบ:

  • การกำหนดเส้นทางคำขอแบบไดนามิก: การเปิดตัวคานารี, การทดสอบ A/B;
  • โหลดบาลานซ์: เรียบง่ายและสม่ำเสมอ โดยยึดตามแฮช
  • การฟื้นตัวหลังการล้ม: หมดเวลา, ลองใหม่, เซอร์กิตเบรกเกอร์;
  • การแทรกข้อบกพร่อง: ความล่าช้า คำขอที่ตกหล่น ฯลฯ

ในขณะที่บทความดำเนินต่อไป ความสามารถเหล่านี้จะแสดงตัวอย่างโดยใช้แอปพลิเคชันที่เลือกไว้เป็นตัวอย่าง และแนวคิดใหม่ๆ จะถูกแนะนำไปพร้อมกัน แนวคิดดังกล่าวแรกจะเป็น DestinationRules (เช่น กฎเกี่ยวกับผู้รับการรับส่งข้อมูล/คำขอ - การแปลโดยประมาณ)ด้วยความช่วยเหลือในการเปิดใช้งานการทดสอบ A/B

การทดสอบ A/B: กฎปลายทางในทางปฏิบัติ

การทดสอบ A/B ใช้ในกรณีที่แอปพลิเคชันมีสองเวอร์ชัน (โดยปกติจะมีความแตกต่างกันทางสายตา) และเราไม่แน่ใจ 100% ว่าเวอร์ชันใดจะปรับปรุงประสบการณ์ผู้ใช้ได้ ดังนั้นเราจึงเรียกใช้ทั้งสองเวอร์ชันพร้อมกันและรวบรวมเมตริก

หากต้องการปรับใช้ฟรอนท์เอนด์เวอร์ชันที่สอง ซึ่งจำเป็นสำหรับการสาธิตการทดสอบ A/B ให้รันคำสั่งต่อไปนี้:

$ kubectl apply -f resource-manifests/kube/ab-testing/sa-frontend-green-deployment.yaml
deployment.extensions/sa-frontend-green created

รายการการปรับใช้สำหรับเวอร์ชันสีเขียวมีความแตกต่างกันในสองตำแหน่ง:

  1. รูปภาพนี้ใช้แท็กอื่น - istio-green,
  2. พ็อดมีฉลาก version: green.

เนื่องจากการปรับใช้ทั้งสองมีป้ายกำกับ app: sa-frontendคำขอที่กำหนดเส้นทางโดยบริการเสมือน sa-external-services สำหรับการบริการ sa-frontendจะถูกเปลี่ยนเส้นทางไปยังอินสแตนซ์ทั้งหมด และโหลดจะถูกกระจายผ่าน อัลกอริธึมแบบวนซ้ำซึ่งจะนำไปสู่สถานการณ์ดังต่อไปนี้:

กลับไปสู่ไมโครเซอร์วิสด้วย Istio ส่วนที่ 2
ไม่พบไฟล์ที่ร้องขอ

ไม่พบไฟล์เหล่านี้เนื่องจากมีการตั้งชื่อแตกต่างกันในเวอร์ชันต่างๆ ของแอปพลิเคชัน มาตรวจสอบสิ่งนี้กัน:

$ curl --silent http://$EXTERNAL_IP/ | tr '"' 'n' | grep main
/static/css/main.c7071b22.css
/static/js/main.059f8e9c.js
$ curl --silent http://$EXTERNAL_IP/ | tr '"' 'n' | grep main
/static/css/main.f87cd8c9.css
/static/js/main.f7659dbb.js

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

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

กฎปลายทาง

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

กลับไปสู่ไมโครเซอร์วิสด้วย Istio ส่วนที่ 2
การจัดการการรับส่งข้อมูลด้วยทรัพยากรของ Istio

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

ด้วยกฎปลายทาง เราสามารถกำหนดค่าการปรับสมดุลโหลดเพื่อใช้แฮชที่สอดคล้องกัน และให้แน่ใจว่าอินสแตนซ์บริการเดียวกันตอบสนองต่อผู้ใช้คนเดียวกัน การกำหนดค่าต่อไปนี้ช่วยให้คุณบรรลุเป้าหมายนี้ (ปลายทางกฎ-sa-frontend.yaml):

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: sa-frontend
spec:
  host: sa-frontend
  trafficPolicy:
    loadBalancer:
      consistentHash:
        httpHeaderName: version   # 1

1 - แฮชจะถูกสร้างขึ้นตามเนื้อหาของส่วนหัว HTTP version.

ใช้การกำหนดค่าด้วยคำสั่งต่อไปนี้:

$ kubectl apply -f resource-manifests/istio/ab-testing/destinationrule-sa-frontend.yaml
destinationrule.networking.istio.io/sa-frontend created

ตอนนี้ให้รันคำสั่งด้านล่างและตรวจสอบให้แน่ใจว่าคุณได้รับไฟล์ที่ถูกต้องเมื่อคุณระบุส่วนหัว version:

$ curl --silent -H "version: yogo" http://$EXTERNAL_IP/ | tr '"' 'n' | grep main

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

โดยทั่วไป DestinationRules มีความสามารถมากกว่าในด้านการทำโหลดบาลานซ์ - ตรวจสอบรายละเอียดใน เอกสารราชการ.

ก่อนที่จะศึกษา VirtualService เพิ่มเติม ให้ลบ "เวอร์ชันสีเขียว" ของแอปพลิเคชันและกฎทิศทางการรับส่งข้อมูลที่เกี่ยวข้องออกโดยการรันคำสั่งต่อไปนี้:

$ kubectl delete -f resource-manifests/kube/ab-testing/sa-frontend-green-deployment.yaml
deployment.extensions “sa-frontend-green” deleted
$ kubectl delete -f resource-manifests/istio/ab-testing/destinationrule-sa-frontend.yaml
destinationrule.networking.istio.io “sa-frontend” deleted

การมิเรอร์: บริการเสมือนจริงในทางปฏิบัติ

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

เพื่อทดสอบสถานการณ์นี้ในทางปฏิบัติ เรามาสร้างอินสแตนซ์ที่สองของ SA-Logic พร้อมข้อบกพร่อง (buggy) โดยรันคำสั่งต่อไปนี้:

$ kubectl apply -f resource-manifests/kube/shadowing/sa-logic-service-buggy.yaml
deployment.extensions/sa-logic-buggy created

และตอนนี้เรามารันคำสั่งเพื่อให้แน่ใจว่าทุกกรณีด้วย app=sa-logic นอกจากนี้ยังมีป้ายกำกับที่มีเวอร์ชันที่เกี่ยวข้องด้วย:

$ kubectl get pods -l app=sa-logic --show-labels
NAME                              READY   LABELS
sa-logic-568498cb4d-2sjwj         2/2     app=sa-logic,version=v1
sa-logic-568498cb4d-p4f8c         2/2     app=sa-logic,version=v1
sa-logic-buggy-76dff55847-2fl66   2/2     app=sa-logic,version=v2
sa-logic-buggy-76dff55847-kx8zz   2/2     app=sa-logic,version=v2

บริการ sa-logic กำหนดเป้าหมายพ็อดด้วยป้ายกำกับ app=sa-logicดังนั้นคำขอทั้งหมดจะถูกกระจายไปยังทุกกรณี:

กลับไปสู่ไมโครเซอร์วิสด้วย Istio ส่วนที่ 2

... แต่เราต้องการให้ส่งคำขอไปยังอินสแตนซ์ v1 และมิเรอร์ไปยังอินสแตนซ์ v2:

กลับไปสู่ไมโครเซอร์วิสด้วย Istio ส่วนที่ 2

เราจะบรรลุเป้าหมายนี้ผ่าน VirtualService ร่วมกับ DestinationRule โดยที่กฎจะกำหนดชุดย่อยและเส้นทางของ VirtualService ไปยังชุดย่อยเฉพาะ

การกำหนดชุดย่อยในกฎปลายทาง

เซตย่อย (เซตย่อย) ถูกกำหนดโดยการกำหนดค่าต่อไปนี้ (sa-logic-ชุดย่อย-destinationrule.yaml):

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: sa-logic
spec:
  host: sa-logic    # 1
  subsets:
  - name: v1        # 2
    labels:
      version: v1   # 3
  - name: v2
    labels:
      version: v2

  1. เจ้าภาพ (host) กำหนดว่ากฎนี้ใช้กับกรณีที่เส้นทางไปยังบริการเท่านั้น sa-logic;
  2. ชื่อเรื่อง (name) เซ็ตย่อยถูกใช้เมื่อกำหนดเส้นทางไปยังอินสแตนซ์เซ็ตย่อย
  3. ฉลาก (label) กำหนดคู่คีย์-ค่าที่อินสแตนซ์ต้องตรงกันจึงจะกลายเป็นส่วนหนึ่งของชุดย่อย

ใช้การกำหนดค่าด้วยคำสั่งต่อไปนี้:

$ kubectl apply -f resource-manifests/istio/shadowing/sa-logic-subsets-destinationrule.yaml
destinationrule.networking.istio.io/sa-logic created

เมื่อกำหนดเซ็ตย่อยแล้ว เราก็สามารถดำเนินการต่อและกำหนดค่า VirtualService เพื่อใช้กฎกับคำขอไปยัง sa-logic เพื่อให้:

  1. ถูกส่งไปยังเซตย่อย v1,
  2. มิเรอร์ไปยังเซตย่อย v2.

ประกาศต่อไปนี้ช่วยให้คุณบรรลุแผนของคุณ (sa-logic-ชุดย่อย-shadowing-vs.yaml):

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: sa-logic
spec:
  hosts:
    - sa-logic          
  http:
  - route:
    - destination:
        host: sa-logic  
        subset: v1      
    mirror:             
      host: sa-logic     
      subset: v2

ไม่จำเป็นต้องมีคำอธิบาย ดังนั้นเรามาดูการทำงานกัน:

$ kubectl apply -f resource-manifests/istio/shadowing/sa-logic-subsets-shadowing-vs.yaml
virtualservice.networking.istio.io/sa-logic created

มาเพิ่มโหลดด้วยการเรียกคำสั่งต่อไปนี้:

$ while true; do curl -v http://$EXTERNAL_IP/sentiment 
    -H "Content-type: application/json" 
    -d '{"sentence": "I love yogobella"}'; 
    sleep .8; done

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

กลับไปสู่ไมโครเซอร์วิสด้วย Istio ส่วนที่ 2
การตอบสนองที่ประสบความสำเร็จของบริการ sa-logic เวอร์ชันต่างๆ

อันดับแรกเราจะได้เห็นว่า VirtualService นำไปใช้กับทูตบริการของเราอย่างไร: เมื่อใด sa-web-app ทำการร้องขอ sa-logicโดยจะผ่าน Sidecar Envoy ซึ่ง - ผ่าน VirtualService - ได้รับการกำหนดค่าเพื่อกำหนดเส้นทางคำขอไปยังชุดย่อย v1 และสะท้อนคำขอไปยังชุดย่อย v2 ของบริการ sa-logic.

ฉันรู้ว่าคุณอาจคิดอยู่แล้วว่า Virtual Services นั้นเรียบง่าย ในส่วนถัดไป เราจะขยายความโดยบอกว่าพวกมันก็ยอดเยี่ยมเช่นกัน

การเปิดตัว Canary

Canary Deployment เป็นกระบวนการในการเปิดตัวแอปพลิเคชันเวอร์ชันใหม่ให้กับผู้ใช้จำนวนไม่มาก ใช้เพื่อให้แน่ใจว่าไม่มีปัญหาในการเผยแพร่และหลังจากนั้นเมื่อมีความมั่นใจในคุณภาพ (การเผยแพร่) แล้วจึงแจกจ่ายให้กับผู้ใช้รายอื่นоผู้ชมจำนวนมากขึ้น

เพื่อสาธิตการเปิดตัว Canary เราจะดำเนินการกับชุดย่อยต่อไป buggy у sa-logic.

อย่าเสียเวลากับเรื่องเล็ก ๆ น้อย ๆ และส่งผู้ใช้ 20% ไปยังเวอร์ชันที่มีข้อบกพร่องทันที (ซึ่งจะแสดงถึงการเปิดตัว Canary ของเรา) และอีก 80% ที่เหลือเป็นบริการปกติ เมื่อต้องการทำเช่นนี้ ใช้ VirtualService ต่อไปนี้ (sa-logic-ชุดย่อย-canary-vs.yaml):

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: sa-logic
spec:
  hosts:
    - sa-logic    
  http:
  - route: 
    - destination: 
        host: sa-logic
        subset: v1
      weight: 80         # 1
    - destination: 
        host: sa-logic
        subset: v2
      weight: 20 # 1

1 คือน้ำหนัก (weight) ซึ่งระบุเปอร์เซ็นต์ของคำขอที่จะถูกส่งไปยังผู้รับหรือกลุ่มย่อยของผู้รับ

มาอัปเดตการกำหนดค่า VirtualService ก่อนหน้าสำหรับ sa-logic ด้วยคำสั่งต่อไปนี้:

$ kubectl apply -f resource-manifests/istio/canary/sa-logic-subsets-canary-vs.yaml
virtualservice.networking.istio.io/sa-logic configured

... และเราจะเห็นทันทีว่าคำขอบางรายการนำไปสู่ความล้มเหลว:

$ while true; do 
   curl -i http://$EXTERNAL_IP/sentiment 
   -H "Content-type: application/json" 
   -d '{"sentence": "I love yogobella"}' 
   --silent -w "Time: %{time_total}s t Status: %{http_code}n" 
   -o /dev/null; sleep .1; done
Time: 0.153075s Status: 200
Time: 0.137581s Status: 200
Time: 0.139345s Status: 200
Time: 30.291806s Status: 500

VirtualServices เปิดใช้งานการเปิดตัว canary: ในกรณีนี้ เราได้จำกัดผลกระทบที่อาจเกิดขึ้นจากปัญหาให้แคบลงเหลือ 20% ของฐานผู้ใช้ มหัศจรรย์! ในตอนนี้ ในทุกกรณีที่เราไม่แน่ใจในโค้ดของเรา (หรืออีกนัยหนึ่งคือ - เสมอ...) เราสามารถใช้การมิเรอร์และการเปิดตัว Canary ได้

หมดเวลาและลองอีกครั้ง

แต่จุดบกพร่องไม่ได้จบลงที่โค้ดเสมอไป ในรายการจาก "8 ความเข้าใจผิดเกี่ยวกับคอมพิวเตอร์แบบกระจาย“ประการแรกคือความเชื่อที่ผิดที่ว่า “เครือข่ายมีความน่าเชื่อถือ” ในความเป็นจริงเครือข่าย ไม่ เชื่อถือได้ และด้วยเหตุนี้เราจึงต้องมีการหมดเวลา (หมดเวลา) และลองอีกครั้ง (ลองใหม่).

สำหรับการสาธิต เราจะใช้เวอร์ชันปัญหาเดิมต่อไป sa-logic (buggy) และเราจะจำลองความไม่น่าเชื่อถือของเครือข่ายโดยมีความล้มเหลวแบบสุ่ม

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

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

  1. เพิ่มการหมดเวลาหากบริการใช้เวลาตอบสนองนานกว่า 8 วินาที
  2. ลองอีกครั้งหากคำขอล้มเหลว

สำหรับการนำไปใช้งาน เราจะใช้คำจำกัดความของทรัพยากรต่อไปนี้ (sa-logic-ลองใหม่-หมดเวลา-vs.yaml):

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: sa-logic
spec:
  hosts:
    - sa-logic
  http:
  - route: 
    - destination: 
        host: sa-logic
        subset: v1
      weight: 50
    - destination: 
        host: sa-logic
        subset: v2
      weight: 50
    timeout: 8s           # 1
    retries:
      attempts: 3         # 2
      perTryTimeout: 3s # 3

  1. การหมดเวลาสำหรับการร้องขอถูกตั้งค่าเป็น 8 วินาที
  2. มีการลองส่งคำขออีกครั้ง 3 ครั้ง
  3. และความพยายามแต่ละครั้งจะถือว่าไม่สำเร็จหากเวลาตอบสนองเกิน 3 วินาที

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

ใช้การกำหนดค่าที่อัปเดตด้วยคำสั่งต่อไปนี้:

$ kubectl apply -f resource-manifests/istio/retries/sa-logic-retries-timeouts-vs.yaml
virtualservice.networking.istio.io/sa-logic configured

และตรวจสอบกราฟ Grafana ว่าจำนวนการตอบกลับที่สำเร็จเพิ่มขึ้นด้านบน:

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

ก่อนที่จะไปยังส่วนถัดไป (หรือมากกว่านั้นไปที่ส่วนถัดไปของบทความเพราะในนี้จะไม่มีการทดลองเชิงปฏิบัติอีกต่อไป - การแปลโดยประมาณ), ลบ sa-logic-buggy และ VirtualService โดยการรันคำสั่งต่อไปนี้:

$ kubectl delete deployment sa-logic-buggy
deployment.extensions “sa-logic-buggy” deleted
$ kubectl delete virtualservice sa-logic
virtualservice.networking.istio.io “sa-logic” deleted

เซอร์กิตเบรกเกอร์และรูปแบบกั้น

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

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

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

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

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

อ่านเพิ่มเติมในบล็อกของเรา:

ที่มา: will.com

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