Service Discovery (การค้นหา Service)

เมื่อ คุณมีไมโครเซอร์วิสมากกว่าไม่กี่ตัวอยู่รอบๆ ความสนใจของคุณย่อมหันไปที่การรู้ว่าจริงๆ แล้วทุกอย่างอยู่ที่ไหนกันแน่ บางทีคุณอาจอยาก รู้ว่าอะไรกำลังรันอยู่ใน environment หนึ่งๆ เพื่อที่คุณจะรู้ว่าควรจะ monitor อะไร บางทีมันอาจง่ายๆ แค่การรู้ว่าไมโครเซอร์วิส Accounts ของคุณอยู่ที่ไหน เพื่อที่ผู้เรียกใช้ของมันจะได้รู้ว่าจะหามันเจอที่ไหน หรือบางทีคุณก็แค่อยากทำให้นักพัฒนาในองค์กรของคุณรู้ได้ง่ายว่า มี API อะไรบ้างที่พร้อมใช้ เพื่อที่พวกเขาจะได้ไม่ต้องคิดค้นสิ่งที่มีอยู่แล้วขึ้นมาใหม่ โดยกว้างๆ แล้ว use case ทั้งหมดนี้อยู่ภายใต้ ร่มของ service discovery และเช่นเคยกับไมโครเซอร์วิส เรามีตัวเลือกที่แตกต่างกันมากมายพอสมควรให้ใช้ในการจัดการเรื่องนี้

ทางออกทั้งหมดที่เราจะดูกันจัดการเรื่องต่างๆ ในสองส่วน อย่างแรก พวกมันมีกลไกบางอย่างสำหรับให้ instance ลงทะเบียนตัวเองและบอกว่า "ผมอยู่ตรงนี้!" อย่างที่สอง พวกมันมีวิธีค้นหา service เมื่อมันลงทะเบียนแล้ว อย่างไรก็ตาม service discovery จะซับซ้อนขึ้นเมื่อเรา กำลังพิจารณา environment ที่เรากำลังทำลายและ deploy instance ใหม่ของ service อยู่ตลอดเวลา ในอุดมคติ เรา อยากให้ทางออกใดก็ตามที่เราเลือกสามารถจัดการเรื่องนี้ได้

มาดูทางออกที่พบบ่อยที่สุดสำหรับการส่งมอบ service และพิจารณาตัวเลือกของเรากัน

Domain Name System (DNS)

เป็นเรื่อง ดีที่จะเริ่มต้นด้วยอะไรง่ายๆ DNS ให้เราเชื่อมโยงชื่อกับ IP address ของเครื่องหนึ่งเครื่องหรือมากกว่า เราอาจตัดสินใจ ตัวอย่างเช่น ว่าไมโครเซอร์วิส Accounts ของเราจะพบได้เสมอที่ accounts.musiccorp.net จากนั้นเราก็จะให้ entry point นั้นชี้ไปที่ IP address ของ host ที่รันไมโครเซอร์วิสนั้น หรือบางทีเราอาจให้มัน resolve ไปยัง load balancer ที่กระจาย load ข้าม instance หลายตัว นี่หมายความว่า เรา จะต้องจัดการอัปเดต entry เหล่านี้เป็นส่วนหนึ่งของการ deploy service ของเรา

เมื่อจัดการกับ instance ของ service ใน environment ที่ต่างกัน ผมเคยเห็น convention-based domain template ทำงานได้ดี ตัวอย่างเช่น เราอาจมี template ที่กำหนดเป็น <servicename>-<environment>.musiccorp.net ทำให้เรามี entry อย่าง accounts-uat.musiccorp.net หรือ accounts-dev.musiccorp.net

วิธีที่ก้าวหน้ากว่าในการจัดการ environment ที่แตกต่างกันคือการมี domain name server ที่แตกต่างกันสำหรับ environment เหล่านี้ ดังนั้นผมอาจสมมติได้ว่า accounts.musiccorp.net คือที่ที่ผมจะพบไมโครเซอร์วิส Accounts เสมอ แต่มันอาจ resolve ไปยัง host ที่ต่างกันขึ้นอยู่กับว่าผม lookup จากที่ไหน ถ้าคุณมี environment ของคุณอยู่ใน network segment ที่ต่างกันอยู่แล้ว และรู้สึกสบายใจกับการจัดการ DNS server และ entry ของคุณเอง นี่อาจเป็นทางออกที่ค่อนข้างเรียบร้อย แต่มันก็เป็นงานมากถ้าคุณไม่ได้ประโยชน์อื่นๆ จากการตั้งค่านี้

DNS มีข้อได้เปรียบมากมาย ข้อหลักคือมันเป็นมาตรฐานที่เข้าใจกันดีและใช้กันอย่างแพร่หลาย จน technology stack แทบทุกตัวรองรับมัน น่าเสียดายที่แม้จะมี service หลายตัวสำหรับจัดการ DNS ภายในองค์กร แต่มีเพียงไม่กี่ตัวที่ดูเหมือนจะถูกออกแบบมาสำหรับ environment ที่เรากำลังจัดการกับ host ที่ถูกทิ้งได้ง่ายมาก ทำให้การอัปเดต DNS entry ค่อนข้างเจ็บปวด service Route 53 ของ Amazon ทำหน้าที่นี้ ได้ค่อนข้างดี แต่ผมยังไม่เห็นตัวเลือกแบบ self-hosted ที่ดีเท่า แม้ว่า (อย่างที่เราจะพูดถึงในไม่ช้า) เครื่องมือ service discovery เฉพาะทางบางตัวอย่าง Consul อาจช่วยเราตรงนี้ได้ นอกเหนือจากปัญหาในการอัปเดต DNS entry แล้ว ตัว DNS specification เองก็อาจทำให้ เราเจอปัญหาบางอย่างได้

DNS entry สำหรับ domain name มี time to live (TTL) นี่คือระยะเวลาที่ client สามารถถือว่า entry ยังใหม่อยู่ เมื่อเราต้องการเปลี่ยน host ที่ domain name อ้างอิงถึง เราอัปเดต entry นั้น แต่เราต้องสมมติว่า client จะยึดติดกับ IP เก่าไว้เป็นเวลา อย่างน้อย เท่ากับที่ TTL ระบุ DNS entry สามารถถูก cache ไว้ในหลายที่ (แม้แต่ JVM ก็จะ cache DNS entry เว้นแต่คุณจะบอกไม่ให้ทำ) และยิ่ง มันถูก cache ไว้ในที่มากเท่าไหร่ entry ก็ยิ่งอาจเก่าล้าสมัยได้มากเท่านั้น

วิธีหนึ่งในการแก้ปัญหานี้คือให้ domain name entry สำหรับ service ของคุณชี้ไปที่ load balancer ซึ่งในทางกลับกันก็ชี้ไปที่ instance ของ service ของคุณ ดังแสดงใน Figure 5-5 เมื่อคุณ deploy instance ใหม่ คุณสามารถเอาตัวเก่าออกจาก load-balancer entry และเพิ่มตัวใหม่เข้าไปได้ บางคนใช้ DNS round-robining ซึ่ง DNS entry เองอ้างอิงไปยังกลุ่มของเครื่อง เทคนิคนี้มีปัญหาอย่างมาก เพราะ client ถูกซ่อนจาก host ที่อยู่เบื้องหลัง และดังนั้น จึงไม่สามารถหยุด route traffic ไปยัง host ตัวใดตัวหนึ่งได้ง่ายๆ ถ้ามันเกิดปัญหาขึ้น

bms2 0505

Figure 5-5. Using DNS to resolve to a load balancer to avoid stale DNS entries (การใช้ DNS resolve ไปยัง load balancer เพื่อหลีกเลี่ยง DNS entry ที่ล้าสมัย)

อย่างที่กล่าวไป DNS เป็นที่เข้าใจกันดีและได้รับการรองรับอย่างแพร่หลาย แต่มันก็มีข้อเสียหนึ่งหรือสองข้อ ผมขอแนะนำให้คุณตรวจสอบ ว่ามันเหมาะกับคุณหรือไม่ก่อนเลือกอะไรที่ซับซ้อนกว่า สำหรับสถานการณ์ที่คุณมีแค่ node เดียว การให้ DNS อ้างอิงไปยัง host โดยตรง ก็น่าจะไม่มีปัญหา แต่สำหรับสถานการณ์ที่คุณต้องการ instance ของ host มากกว่าหนึ่งตัว ให้ DNS entry resolve ไปยัง load balancer ที่สามารถจัดการการนำ host แต่ละตัวเข้าและออกจากการให้บริการได้ตาม ความเหมาะสม

Dynamic Service Registries (Service Registry แบบ Dynamic)

ข้อเสีย ของ DNS ในฐานะวิธีการค้นหา node ใน environment ที่เปลี่ยนแปลงมาก ได้นำไปสู่ระบบทางเลือกจำนวนหนึ่ง ซึ่งส่วนใหญ่เกี่ยวข้องกับ การที่ service ลงทะเบียนตัวเองกับ registry กลางบางตัว ซึ่งในทางกลับกันก็ให้ความสามารถในการค้นหา service เหล่านี้ในภายหลัง บ่อยครั้ง ระบบเหล่านี้ทำมากกว่าแค่ให้บริการลงทะเบียนและค้นหา service ซึ่งอาจเป็นเรื่องดีหรือไม่ดีก็ได้ นี่เป็นพื้นที่ที่แออัดมาก ดังนั้นเราจะดูแค่ตัวเลือกไม่กี่ตัวเพื่อให้คุณเห็นภาพว่ามีอะไรบ้าง

ZooKeeper

ZooKeeper ถูก พัฒนาขึ้นในตอนแรกเป็นส่วนหนึ่งของโปรเจกต์ Hadoop มันถูกใช้สำหรับ use case ที่หลากหลายเหลือเชื่อ รวมถึง configuration management, การซิงค์ข้อมูลระหว่าง service, leader election, message queue และ (มีประโยชน์สำหรับเรา) เป็น naming service

เหมือนกับระบบประเภทคล้ายๆ กันหลายตัว ZooKeeper พึ่งพาการรัน node จำนวนหนึ่งใน cluster เพื่อให้การรับประกันต่างๆ นี่หมายความว่า คุณควรคาดหวังที่จะรัน Zookeeper node อย่างน้อยสามตัว ความฉลาดส่วนใหญ่ใน ZooKeeper อยู่รอบๆ การทำให้แน่ใจว่าข้อมูลถูก replicate อย่างปลอดภัยระหว่าง node เหล่านี้ และสิ่งต่างๆ ยังคง consistent เมื่อ node ล้มเหลว

โดยแก่นแท้แล้ว ZooKeeper ให้ hierarchical namespace สำหรับการเก็บข้อมูล Client สามารถแทรก node ใหม่ใน hierarchy นี้ เปลี่ยนแปลงมัน หรือ query มันได้ นอกจากนี้ พวกมันสามารถเพิ่ม watch ไปยัง node เพื่อได้รับแจ้งเมื่อมันเปลี่ยนแปลง นี่หมายความว่า เราสามารถเก็บข้อมูลว่า service ของเราอยู่ที่ไหนในโครงสร้างนี้ และในฐานะ client ได้รับแจ้งเมื่อมันเปลี่ยนแปลง ZooKeeper มักถูกใช้ เป็น general configuration store ดังนั้นคุณก็สามารถเก็บ configuration เฉพาะ service ไว้ในนั้นได้ด้วย ทำให้คุณสามารถทำงานอย่าง การเปลี่ยน log level แบบ dynamic หรือปิด feature ของ system ที่กำลังรันอยู่ได้

ในความเป็นจริง มีทางออกที่ดีกว่าอยู่สำหรับ dynamic service registration ในระดับที่ผมจะหลีกเลี่ยง ZooKeeper อย่างจริงจัง สำหรับ use case นี้ในปัจจุบัน

Consul

เหมือนกับ ZooKeeper Consul รองรับทั้ง configuration management และ service discovery แต่มันไปไกลกว่า ZooKeeper ในการให้การรองรับมากขึ้นสำหรับ use case สำคัญเหล่านี้ ตัวอย่างเช่น มันเปิดเผย HTTP interface สำหรับ service discovery และหนึ่งใน killer feature ของ Consul คือมันมี DNS server ให้พร้อมใช้จริงๆ โดยเฉพาะ มันสามารถ serve SRV record ซึ่งให้คุณทั้ง IP และ port สำหรับชื่อหนึ่งๆ นี่หมายความว่าถ้าส่วนหนึ่งของระบบของคุณใช้ DNS อยู่แล้วและสามารถรองรับ SRV record ได้ คุณก็แค่ใส่ Consul เข้าไปและเริ่มใช้มันได้โดยไม่ต้องเปลี่ยนแปลงระบบที่มีอยู่ของคุณเลย

Consul ยังมี capability อื่นๆ ในตัวที่คุณอาจพบว่ามีประโยชน์ เช่น ความสามารถในการทำ health check บน node ดังนั้น Consul ก็อาจทับซ้อนกับ capability ที่มอบให้โดยเครื่องมือ monitoring เฉพาะทางอื่นๆ ได้ดี แม้ว่าคุณน่าจะใช้ Consul เป็นแหล่งข้อมูลนี้ แล้วดึงมันเข้าไปในการตั้งค่า monitoring ที่ครอบคลุมกว่ามากกว่า

Consul ใช้ RESTful HTTP interface สำหรับทุกอย่าง ตั้งแต่การลงทะเบียน service ไปจนถึงการ query key/value store หรือแทรก health check สิ่งนี้ทำให้การ integrate กับ technology stack ที่ต่างกันตรงไปตรงมามาก Consul ยังมีชุดเครื่องมือที่ทำงานได้ดี กับมัน ซึ่งช่วยเพิ่มประโยชน์ของมันมากขึ้น ตัวอย่างหนึ่งคือ consul-template ซึ่งให้วิธีอัปเดตไฟล์ข้อความตาม entry ใน Consul เมื่อมองแวบแรก สิ่งนี้ดูเหมือนจะไม่น่าสนใจนัก จนกว่าคุณจะพิจารณาความจริง ที่ว่าด้วย consul-template ตอนนี้คุณสามารถเปลี่ยนค่าใน Consul—บางทีอาจเป็นตำแหน่งของไมโครเซอร์วิส หรือค่า configuration— และให้ configuration file ทั่วทั้งระบบของคุณอัปเดตแบบ dynamic ได้ ทันใดนั้น program ใดก็ตามที่อ่าน configuration ของมันจาก ไฟล์ข้อความก็สามารถให้ไฟล์ข้อความของมันอัปเดตแบบ dynamic ได้โดยไม่ต้องรู้อะไรเกี่ยวกับตัว Consul เอง use case ที่ดีสำหรับสิ่งนี้ จะเป็นการเพิ่มหรือลบ node ไปยัง load balancer pool แบบ dynamic โดยใช้ software load balancer อย่าง HAProxy

อีกเครื่องมือหนึ่งที่ integrate กับ Consul ได้ดีคือ Vault ซึ่งเป็นเครื่องมือ secrets management ที่เราจะกลับมาดูอีกครั้งใน "Secrets" การจัดการ secrets อาจเป็นเรื่องปวดหัว แต่การรวมกันของ Consul และ Vault สามารถทำให้ชีวิตง่ายขึ้นได้อย่างแน่นอน

etcd and Kubernetes (etcd และ Kubernetes)

ถ้า คุณกำลังรันบน platform ที่จัดการ container workload ให้คุณ มีโอกาสสูงที่คุณจะมีกลไก service discovery ให้มาแล้ว Kubernetes ก็ไม่ต่างกัน และมันมาบางส่วนจาก etcd ซึ่งเป็น configuration management store ที่มาพร้อมกับ Kubernetes etcd มี capability คล้ายกับของ Consul และ Kubernetes ใช้มันในการจัดการข้อมูล configuration ที่หลากหลาย

เราจะสำรวจ Kubernetes ในรายละเอียดมากขึ้นใน "Kubernetes and Container Orchestration" แต่โดยสรุปแล้ว วิธีที่ service discovery ทำงานบน Kubernetes คือคุณ deploy container ใน pod แล้ว service ก็จะระบุแบบ dynamic ว่า pod ตัวไหนควรเป็นส่วนหนึ่งของ service โดยการจับคู่ pattern บน metadata ที่เกี่ยวข้องกับ pod นั้น มันเป็นกลไก ที่ค่อนข้างสง่างามและสามารถทรงพลังได้มาก Request ไปยัง service จะถูก route ไปยังหนึ่งใน pod ที่ประกอบขึ้นเป็น service นั้น

Capability ที่คุณได้มาพร้อมใช้กับ Kubernetes อาจทำให้คุณแค่อยากใช้สิ่งที่มาพร้อมกับ platform หลัก โดยไม่ใช้เครื่องมือเฉพาะทาง อย่าง Consul และสำหรับหลายคนสิ่งนี้ก็สมเหตุสมผลมาก โดยเฉพาะถ้า ecosystem ที่กว้างขึ้นของ tooling รอบๆ Consul ไม่ใช่สิ่งที่ คุณสนใจ อย่างไรก็ตาม ถ้าคุณกำลังรันใน environment แบบผสม ที่คุณมี workload รันอยู่บน Kubernetes และที่อื่นๆ การมีเครื่องมือ service discovery เฉพาะทางที่สามารถใช้ได้ข้ามทั้งสอง platform ก็อาจเป็นทางที่ควรไป

Rolling your own (การสร้างระบบของคุณเอง)

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

  • service = accounts

  • environment = production

  • version = 154

จากนั้นผมใช้ AWS API เพื่อ query instance ทั้งหมดที่เกี่ยวข้องกับ AWS account หนึ่งๆ เพื่อที่ผมจะได้หาเครื่องที่ผมสนใจเจอ ตรงนี้ AWS เองเป็นผู้จัดการการเก็บ metadata ที่เกี่ยวข้องกับแต่ละ instance และให้ความสามารถแก่เราในการ query มัน จากนั้น ผมสร้างเครื่องมือ command-line เพื่อ interact กับ instance เหล่านี้ และให้ graphical interface เพื่อดูสถานะ instance ได้ในแวบเดียว ทั้งหมดนี้กลายเป็นงานที่ค่อนข้างเรียบง่ายถ้าคุณสามารถรวบรวมข้อมูลเกี่ยวกับ service interface ได้แบบ programmatic

ครั้งสุดท้ายที่ผมทำแบบนี้ เราไม่ได้ไปไกลถึงขั้นให้ service ใช้ AWS API เพื่อค้นหา service dependency ของพวกมัน แต่ก็ไม่มี เหตุผลใดที่คุณจะทำแบบนั้นไม่ได้ แน่นอนว่า ถ้าคุณต้องการให้ upstream service ได้รับการแจ้งเตือนเมื่อตำแหน่งของ downstream service เปลี่ยนแปลง คุณต้องจัดการเอง

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

Don't Forget the Humans! (อย่าลืมมนุษย์!)

ระบบที่เราดูกันมาจนถึงตอนนี้ทำให้ง่ายสำหรับ service instance ที่จะลงทะเบียนตัวเองและค้นหา service อื่นๆ ที่มันต้องคุยด้วย แต่ในฐานะมนุษย์ บางครั้งเราก็ต้องการข้อมูลนี้ด้วยเหมือนกัน การทำให้ข้อมูลพร้อมใช้ในรูปแบบที่ให้มนุษย์เรียกใช้ได้ บางทีอาจโดยใช้ API เพื่อดึงรายละเอียดนี้เข้าไปใน humane registry (หัวข้อที่เราจะดูกันอีกสักครู่) อาจมีความสำคัญมาก