ทำความรู้จักและปรับจูนประสิทธิภาพ Index/Shard ใน Elasticsearch

Nutmos
KBTG Life
Published in
4 min readNov 5, 2020

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

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

ตัวระบบ Elasticsearch ถูกสร้างขึ้นจาก Lucene ซึ่งเป็นไลบรารี Java ตัวหนึ่งที่เน้นเรื่องการเสิร์ชและทำดัชนีข้อมูล ดูแลโดย Apache และผู้พัฒนา Elasticsearch ได้สร้างระบบครอบไลบรารีตัวนี้และออกแบบให้เป็น REST API เพื่อความง่ายในการใช้งานและทำให้ Lucene ที่เขียนด้วย Java สามารถใช้งานกับภาษาอะไรก็ได้

เมื่อเราต้องปรับแต่ง Elasticsearch ให้มีประสิทธิภาพสูงที่สุด ก็คงเลี่ยงไม่ได้ที่จะต้องลงไปแตะถึงระดับ Lucene บ้าง แต่เนื่องจาก Use Case ของ ​Elasticsearch ไม่เหมือนกัน บางคลัสเตอร์อาจต้องการเสิร์ชไวๆ บางคลัสเตอร์มีข้อมูลเข้าจำนวนมหาศาล จึงไม่มีสูตรสำเร็จที่จบในตัว

Shard หน่วยย่อยของ Elasticsearch

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

วิธีการกำหนดจำนวน Shard จะต้องทำตอนสร้าง Index โดยใช้คอมมานด์ด้านล่าง

PUT /<index_name>
{
"settings": {
"index": {
"number_of_shards": 3
}
}
}

Shard จะต้องพิจารณาตั้งแต่ตอนสร้าง Index ใน Elasticsearch เพราะ Index ไม่สามารถเพิ่มหรือลด Shard ได้ ถ้าจะทำต้อง Reindex เท่านั้น แต่สำหรับการเพิ่มก็มีทริกเล็กน้อย คือใช้ Rollover Index API เป็นการขึ้น Index ใหม่ แล้วใช้ Alias ชี้มาเสมือนว่ายังอยู่ใน Index เดิม

สำหรับตัว Shard ของ Elasticsearch คือ Lucene Index (อย่าสับสนกับ Elasticsearch Index นะครับ) ถ้าจะอธิบายคร่าวๆ คือ Shard เป็นหน่วยที่เล็กที่สุด ในมุมของ Elasticsearch ตัว Shard จะแยกกันอีกไม่ได้ แต่สำหรับในมุมของ Lucene ตัว Shard ก็คือ Lucene Index ซึ่งแยกกันได้อีกเป็น Lucene Segment

ภาพจาก Elastic

เนื่องจาก Shard ไม่สามารถแยกกันได้ ดังนั้น 1 Shard สามารถอยู่ได้ 1 โหนดของ Elasticsearch เท่านั้น และตัว Shard สามารถโยกย้ายไปมาระหว่างโหนดของ Elasticsearch ได้พร้อมกันทั้ง Shard ซึ่ง Elasticsearch จะกำหนดจำนวน Shard ที่แต่ละโหนดสามารถเก็บไว้ ค่าเริ่มต้นอยู่ที่ 1000 Shards ต่อโหนด โดยสามารถปรับเพิ่มลดได้

Shard แบ่งออกได้เป็น 2 ประเภทย่อย คือ Primary และ Replica ซึ่ง Replica จะเหมือน Primary ทุกประการ แต่อยู่คนละโหนด เรียกได้ว่าเป็น Copy เพื่อป้องกันโหนดใดโหนดหนึ่งหายไป จะได้ยังมี Shard ไว้ให้อ่านและเขียนข้อมูล แต่ Replica ก็ต้องแลกกับ Throughput ที่จะเพิ่มขึ้นและเวลาที่ใช้ในการเขียนข้อมูลที่นานขึ้นด้วยเช่นกัน

วิธีกำหนดว่าจะให้ Shard มี Replica เท่าไหร่ ให้ใช้คำสั่งนี้ (ถ้าใส่ค่าเป็น 0 คือไม่มี Replica Shard)

PUT /<index_name>/_settings
{
"index" : {
"number_of_replicas" : 1
}
}

Elasticsearch มีความสามารถในการโยกย้าย Shard ด้วยตัวเองอยู่แล้ว ค่าเริ่มต้นคือการที่เกลี่ยให้จำนวน Shard ในแต่ละโหนดมีจำนวนเท่าๆ กัน แต่ถ้าเราต้องการกำหนดให้ Shard ไปลงโหนดที่กำหนดไว้ก็ได้ สามารถเซ็ทค่าnode.attr ที่ไฟล์ elasticsearch.yml ของโหนด เช่น node.attr.rack: rack1 และกำหนดค่าที่ Index เช่น

PUT /<index_name>/_settings
{
"index.routing.allocation.include.rack": "rack1,rack2"
}

การอ่านและเขียนข้อมูล

การเขียนข้อมูลของ Elasticsearch (ตามภาษาของ Elasticsearch เรียกว่า Indexing) จะเขียนตาม Index และ Index จะกระจายข้อมูลตาม Shard โดยวิธีเลือก Shard จะนำค่าในฟิลด์ _routing มาแฮช ซึ่งค่าเริ่มต้นของฟิลด์นั้นคือ Document ID ที่เก็บไว้ในฟิลด์ _id ของ Elasticsearch

สำหรับค่า Document ID เป็นค่า Unique ภายใน Index ของ Elasticsearch หมายความว่าถ้า Document ID เป็นค่าเดียวกัน Elasticsearch จะถือเป็น Document รายการเดียวกัน ดังนั้นถ้ามีมากกว่า 1 รายการที่มี Document ID เดียวกันเข้ามา Elasticsearch จะยึดรายการที่เข้ามาหลังสุดเป็นรายการที่ถูกต้องแทนที่ตัวเก่าไปเลย

ปกติแล้วค่า Document ID จะ Generate ขึ้นมาเมื่อรายการมาถึง Elasticsearch ข้อเสียคือถ้าส่ง Document ที่มีข้อมูลทุกอย่างเหมือนกันทุกประการเข้ามามากกว่า 1 ครั้ง Elasticsearch ก็จะ Generate ค่า Document ID ให้เป็นคนละค่า

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

อย่างที่อธิบายข้างต้นว่า Routing กำหนดให้ใช้ฟิลด์ Document ID เป็นค่าเริ่มต้น ดังนั้น Routing สามารถกำหนดเป็นฟิลด์อื่นแทนได้ตามต้องการ แต่ถ้าเปลี่ยน Routing ไปใช้ฟิลด์อื่นนอกจาก Document ID แล้ว Elasticsearch จะไม่การันตีว่า Document ID จะไม่ซ้ำกันภายใน Index

จากนั้น Elasticsearch จะส่งข้อมูลลง Shard ตาม Routing ที่กำหนด เมื่อแต่ละ Shard ได้รับข้อมูลมาแล้ว ระบบก็จะเขียนลง Translog และบัฟเฟอร์ในเมมโมรี่ก่อน พอถึงรอบก็จะรีเฟรชและทำการเขียนเข้าไปใน Lucene Segment ตัวใหม่ในเมมโมรี่

Lucene Segment จะเพิ่มขึ้นเรื่อย ๆ ตอนเสิร์ช Elasticsearch ก็จะเสิร์ชไล่ตาม Lucene Segment ทีละไฟล์ (ภาพโดย Ngoc Tran)

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

เมื่อเขียน Lucene Segment ตัวใหม่ขึ้นไปเรื่อยๆ ทีนี้ถ้าเราต้องการลบข้อมูลหรือต้องการแก้ไขข้อมูลเก่าจะทำอย่างไรดี?

สำหรับ Elasticsearch นั้นจะไม่สามารถเข้าไปลบหรือแก้ไขข้อมูลเก่าได้ จะทำได้เฉพาะการนำ Document ใหม่เข้าไปแทนที่ตัวเก่าเท่านั้น การลบหรือแก้ไขข้อมูลของ Elasticsearch คือการเขียนข้อมูลใหม่ด้วย Document ID เดิมลงไปใน Lucene Segment ใหม่ (ถ้าเป็นการลบข้อมูลก็จะเขียน Document ด้วย ID เดิมว่า Document นี้ถูกลบไปแล้ว) จากนั้นเมื่อถึงเวลารวม Lucene Segment ระบบก็จะยึดข้อมูลที่ใหม่สุดเป็นหลัก

Elasticsearch จะรวม Lucene Segment ให้อัตโนมัติ ถ้ามีข้อมูลเข้าตลอด Segment Count จะขึ้นๆ ลงๆ ลักษณะนี้

วิธีรีดประสิทธิภาพจาก Lucene Segment คือเมื่อ Elasticsearch ค่อนข้างว่างจากการทำงานแล้ว สามารถสั่งรวม Lucene Segment ย่อยๆ เข้าด้วยกันได้โดยรันคำสั่ง

POST /<index_name>/_forcemerge

หรือว่าจะเขียน Policy ในการ Force Merge ให้ Index Lifecycle ของ Elasticsearch ทำงานนี้แทนก็ได้

การสั่ง Force Merge อาจจะช่วยลดขนาด Index ของ Elasticsearch ได้ เนื่องจากตอนเขียน Lucene Segment ใหม่ที่เกิดจากการรวม Lucene Segment ย่อยๆ เข้าด้วยกัน หากเจอ Document ID ซ้ำกัน Elasticsearch จะเขียนเฉพาะ Document ที่ใหม่ที่สุดตัวเดียวเท่านั้น

ข้อควรระวัง: คำสั่ง Force Merge ควรรันเมื่อ Elasticsearch มีทราฟฟิกน้อยๆ เท่านั้น ถ้ารันตอนทราฟฟิกมากๆ อาจได้ผลลัพธ์ตรงกันข้าม

จากที่กล่าวไปก่อนหน้านี้คือเมื่อถึงรอบรีเฟรช Elasticsearch จะสร้าง Lucene Segment ขึ้นใหม่ ดังนั้นวิธีป้องกันไม่ให้ Lucene Segment เยอะเกินไปคือปรับรอบรีเฟรชของ Elasticsearch จากค่าเริ่มต้นที่จะรีเฟรชทุก ๆ 1 วินาที (ซึ่งถี่มากๆ) อาจจะปรับเป็น 1 นาที, 10 นาที หรือสั่งไม่ให้มีรอบรีเฟรชเลยก็ได้ แล้วให้ไปรีเฟรชที่สคริปต์ในการโหลดข้อมูล ตามตัวอย่าง

POST /<index_name>/_settings
{
"index.refresh_interval": "60s"
}

ในกรณีที่ไม่ต้องการให้มีรอบรีเฟรช ให้กำหนดค่าด้านบนนี้เป็น -1 แล้วใช้คำสั่ง POST /<index_name>/_refresh เพื่อสั่งรีเฟรชเอง

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

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

สรุป

จากที่เห็นรูปแบบการเก็บข้อมูลและอ่านเขียนข้อมูลของ Elasticsearch ก็พอจะทำให้เห็นไอเดียคร่าวๆ ว่าเบื้องหลังการทำงานของระบบนั้นเป็นอย่างไร

ดังที่เขียนไว้ข้างต้น สูตรสำเร็จในการปรับจูน Elasticsearch นั้นไม่มีอยู่จริงเนื่องจากลักษณะการเขียนอ่านข้อมูลที่ไม่เหมือนกัน แต่หลังจากที่ได้อ่านบทความนี้แล้ว คงจะพอได้ไอเดียเบื้องต้นไปปรับแต่งให้ Elasticsearch ที่ใช้งานอยู่เพื่อให้ได้ประสิทธิภาพที่ดีที่สุดกันนะครับ :D

สำหรับชาวเทคคนไหนที่สนใจเรื่องราวดีๆ แบบนี้ หรืออยากเรียนรู้เกี่ยวกับ Product ใหม่ๆ ของ KBTG สามารถติดตามรายละเอียดกันได้ที่เว็บไซต์ www.kbtg.tech

--

--