Java & Spring Startup Time
Java start-up time มี overhead หลายจุด จากความพยายามที่ผมพยายาม optimize ให้ระบบของบริษัท start ได้เร็ว เพื่อทำให้ productivity ของ engineer ดีขึ้น (ปัจจุบัน start up time ~30-40sec) แต่ก็ยังไม่สำเร็จ แต่ก็มีหลายเรื่องได้เรียนรู้ระหว่างที่ทำ
จากที่ลองทำ Profiler ออกมา เจอว่าตอนที่ java app ใช้ CPU ไปกับสิ่งเหล่านี้ในตอน start (เรียงจากมากไปน้อย)
- load class file
- generate bytecode
- parse XML
Load class file
เมื่อ start spring boot application ผ่าน IntelliJ จะไม่เหมือนกับ start ผ่าน jar ตรงๆ มีความต่างกันประมาณนี้
# | IntelliJ | Jar file |
---|---|---|
1 | scan class จาก target/classes และ file jar | unzip file jar และ scan จาก path /BOOT-INF/ |
2 | ใช้ class path จาก module setting ของ IDE | อ่าน class path จากใน jar เพียงอย่างเดียว |
3 | class path มีจำนวนมาก ใช้ disk lookup หลายครั้ง | มีแค่ file jar แต่เสียเวลา unzip + lookup ใน zip file |
ปกติ JVM จะมี class loader เอาไว้ load code ของ class ต่างๆ เข้า memory ซึ่งจะทำงานเป็น hierarchy อ่านต่อ ตัว JVM จะ load class ก็ต่อเมื่อมี code เรียกใช้งาน class นั้นๆ โดยจะไป scan หาใน class path ที่ set เอาไว้เมื่อตอน start java app
ทุก class จะ load ขึ้นมาใหม่เสมออย่างน้อย 1 ครั้งเมื่อ start java app ซึ่งทำตอน runtime ไม่ใช่ compile time ทำให้มี overhead ในการ start (ตามตาราง)
code ของบริษัทมี dependency เยอะมากๆ และ code ของบริษัทเองก็เยอะมากๆ
- Jar ที่รวม dependency แล้ว ~300MB
- Jar ที่ไม่รวม dependency ~ 50MB
- Webapp source files ~ 7000 files
- Webapp generated class files ~ 8100 files
เมื่อจำนวน class เยอะ JVM ก็จะ start ช้า และตอนนี้ยังหาวิธี optimize ไม่ได้ แต่ solution ที่เป็นไปได้มี 2 ทาง
- เขียน class loader เองที่ load class files ให้ครบตั้งแต่ start โดยไม่ต้องเสียเวลา lookup หลายๆ ครั้ง
- ใช้ GraalVM ซึ่งทำ Ahead-of-time compiler และสร้างเป็น native image เหมือนเวลา build C หรือ Go program ทำให้ไม่ต้องพึ่ง class loader ในตอน start
วิธีที่ 1 น่าจะช่วยได้น้อย และใช้ได้กับ IntelliJ เท่านั้น ถ้าจะใช้กับ Jar file ก็ต้องทำเพิ่ม เช่น repack jar ใหม่ให้ load class ได้เร็วขึ้น
วิธีที่ 2 ลองแล้วแต่ยังใช้งานกับ Java 15 และ Spring boot ใหม่ๆ ไม่ได้ และมี feature ไม่ support เยอะ
Generate bytecode
Spring + Hibernate ทำ code gen หลายจุด
@Transactional
- custom aspect ต่างๆ ที่เขียนขึ้นมาเอง
- lazy init bean
- Hibernate entity class
- Hibernate lazy initialize
ณ ตอน start spring และ hibernate จะต้อง generate class เพื่อ wrap class เหล่านี้ ทำให้เกิด overhead ในตอน start ซึ่งขึ้นอยู่กับจำนวน class ที่มี จากที่นับดูใน project มีประมาณ 500 class
การ generate bytecode ณ runtime อาจสร้างปัญหากับ GraalVM อีกด้วย
Parse XML
ที่บริษัท config spring โดยใช้ XML และ annotation ร่วมกัน ส่วน hibernate ใช้ XML ล้วน
ตัว Java XML parser ดูเหมือนจะมีปัญหา performance อยู่พอสมควร เนื่องจากต้อง validate schema ให้ถูกต้อง และอาจจะต้อง lookup schema จาก remote endpoint มา
จุดนี้ยังไม่มั่นใจว่าหากเลิกใช้ XML แล้วใช้ Annotation อย่างเดียวจะลด overhead ได้มากขนาดไหน
Random things
ก่อนหน้าที่จะดู 3 เรื่องด้านบน ก็ได้ optimize compile speed ไปแล้ว
ระบบมี machine learning model อยู่ด้วย ซึ่ง load เมื่อตอน start ถ้าทำ lazy load หรือย้ายออกจะลดเวลาได้แค่ ~1-2sec
initialize connection pool ใช้เวลา ~1sec
ลบ model hibernate ออกหมด เร็วขึ้น ~1-2sec
เป้าที่อยากจะไปให้ถึงคือ น้อยกว่า 10secs ดูแล้วน่าจะเป็นไปไม่ได้ นอกเสียจากทำให้ระบบมัน super slim
Tools ที่ใช้
- IntelliJ Ultimate มี Async profiler ซึ่ง config sampling rate ไว้ที่ 998us (วัด On-CPU อย่างเดียว)
- ทดลอง sampling ด้วย Flight recorder พบว่าไม่ต่างกันมาก (วัด Off-CPU ได้)