تحسين الأداء
تحسين الأداء هو عمل هندسي منظومي—يتضمن ثلاثة أبعاد: JVM وقاعدة البيانات وتجمع الاتصالات. فقط من خلال تحديد الاختناقات بدقة يمكننا معالجة الأسباب الجذرية.
1. ما ستتعلمه
- ضبط معاملات JVM: حجم الكومة / اختيار خوارزمية GC (G1GC / ZGC)
- تحسين معاملات تجمع اتصالات HikariCP واكتشاف تسرب الاتصالات
- تحسين استعلامات JPA: مشكلة N+1 والحلول
@EntityGraph/fetch join - استراتيجيات فهرسة قاعدة البيانات وتحليل سجل الاستعلامات البطيئة
- Alice حسنت زمن استجابة P99 لواجهة قائمة طلبات OrderFlow من 500 مللي ثانية إلى 50 مللي ثانية
2. قصة حقيقية لمهندس أداء
(1) نقطة الألم: واجهة برمجة التطبيقات بطيئة كالحلزون
أبلغ Charlie أن زمن استجابة P99 لواجهة قائمة طلبات OrderFlow كان 500 مللي ثانية، مما أدى إلى تجربة مستخدم سيئة للغاية. كشف تحليل Alice ما يلي: 1) توقفات Full GC متكررة في JVM تستمر 200 مللي ثانية؛ 2) مشكلة N+1 تحدث عندما يستعلم JPA عن قائمة الطلبات (100 طلب = 101 استعلام SQL)؛ 3) حجم تجمع اتصالات HikariCP غير كافٍ، مما يتسبب في انتهاء مهلة الطلبات أثناء انتظار الاتصال.
(2) حلول تحسين المنظومة
تحسين طبقة تلو الأخرى لمعالجة الاختناقات بدقة:
| المستوى | الاختناق | حل التحسين |
|---|---|---|
| JVM | توقف Full GC | G1GC + ضبط حجم الكومة |
| تجمع الاتصالات | اتصالات غير كافية | تحسين معاملات HikariCP |
| ORM | استعلامات N+1 | JOIN FETCH / EntityGraph |
| قاعدة البيانات | مسح كامل للجدول | إضافة فهرس |
(3) الإيرادات
بعد أن قامت Alice بتحسين كل مكون على حدة، انخفض زمن استجابة P99 من 500 مللي ثانية إلى 50 مللي ثانية، وزادت الإنتاجية عشرة أضعاف.
3. ضبط معاملات JVM
(1) مقارنة خوارزميات GC
| خوارزمية GC | أقصى وقت توقف | السيناريوهات المناسبة | معاملات JVM |
|---|---|---|---|
| G1GC | 10-200 مللي ثانية | عام (افتراضي JDK 17) | -XX:+UseG1GC |
| ZGC | < 1 مللي ثانية | زمن استجابة منخفض (JDK 17+) | -XX:+UseZGC |
| SerialGC | طويل | كومة صغيرة (< 200MB) | -XX:+UseSerialGC |
| ParallelGC | متوسط | الأولوية للإنتاجية | -XX:+UseParallelGC |
graph LR
A["ضبط JVM"] --> B["حجم الكومة<br/>-Xms = -Xmx"]
A --> C["خوارزمية GC<br/>G1 / ZGC"]
A --> D["سجل GC<br/>-Xlog:gc*"]
B --> B1["حاوية: MaxRAMPercentage"]
C --> C1["الافتراضي: G1GC"]
C --> C2["زمن استجابة منخفض: ZGC"]
(1) ▶ مثال: تهيئة JVM في بيئة Docker
ENTRYPOINT ["java", \
"-XX:+UseG1GC", \
"-XX:MaxRAMPercentage=75.0", \
"-XX:InitialRAMPercentage=50.0", \
"-XX:+UseStringDeduplication", \
"-Xlog:gc*:file=/app/logs/gc.log:time,uptime,level,tags", \
"-jar", "app.jar"]
الناتج:
// التنفيذ ناجح
| المعامل | المعنى | القيمة الموصى بها |
|---|---|---|
MaxRAMPercentage |
الحد الأقصى للكومة كنسبة مئوية من ذاكرة الحاوية | 75.0 |
InitialRAMPercentage |
النسبة المئوية من ذاكرة الحاوية التي تشغلها الكومة الأولية | 50.0 |
+UseG1GC |
استخدام جامع القمامة G1 | ممكّن افتراضيًا |
+UseStringDeduplication |
إزالة التكرار من السلاسل لتوفير الذاكرة | موصى به |
MaxGCPauseMillis |
وقت توقف GC المستهدف | 200 (G1) / لا يوجد (ZGC) |
(2) ▶ مثال: تهيئة ZGC منخفضة زمن الاستجابة
java -XX:+UseZGC \
-XX:MaxRAMPercentage=75.0 \
-XX:+ZGenerational \
-Xlog:gc*:file=gc.log \
-jar app.jar
الناتج:
# تم تنفيذ الأمر بنجاح
4. تحسين تجمع اتصالات HikariCP
(1) معاملات تجمع الاتصالات
| المعامل | القيمة الافتراضية | الوصف | القيمة الموصى بها |
|---|---|---|---|
maximumPoolSize |
10 | الحد الأقصى لعدد الاتصالات | عدد أنوية المعالج x 2 + عدد الأقراص |
minimumIdle |
= maxPool | الحد الأدنى للاتصالات الخاملة | = maxPoolSize |
connectionTimeout |
30,000 مللي ثانية | مهلة الاتصال | 3,000 مللي ثانية |
idleTimeout |
600,000 مللي ثانية | مهلة الاتصال الخامل | 600,000 مللي ثانية |
maxLifetime |
1,800,000 مللي ثانية | الحد الأقصى لعمر الاتصال | 1,800,000 مللي ثانية |
leakDetectionThreshold |
0 (معطل) | اكتشاف تسرب الاتصالات | 60,000 مللي ثانية |
(1) ▶ مثال: تهيئة HikariCP للإنتاج
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 20
connection-timeout: 3000
idle-timeout: 600000
max-lifetime: 1800000
leak-detection-threshold: 60000
pool-name: OrderFlowHikariCP
الناتج:
أصبحت التهيئة سارية المفعول.
(2) ▶ مثال: اكتشاف تسرب الاتصالات
# تفعيل اكتشاف التسرب (تحذير في السجل إذا احتُجز الاتصال لأكثر من 60 ثانية)
spring:
datasource:
hikari:
leak-detection-threshold: 60000
WARN - Connection leak detection triggered for {conn-12345}
Stack trace of the call site that acquired the connection:
at com.orderflow.service.OrderService.getOrder(OrderService.java:45)
...
5. تحسين استعلامات JPA
(1) مشكلة N+1
(1) ▶ مثال: عرض مشكلة N+1
// سيئ: مشكلة N+1
// 1 استعلام SQL لجلب 100 طلب + 100 استعلام SQL لجلب عناصر كل طلب = 101 استعلام SQL!
List<Order> orders = orderRepository.findAll();
for (Order order : orders) {
order.getItems().size(); // يحفز التحميل الكسول لكل طلب
}
الناتج:
// التنفيذ ناجح
(2) ▶ مثال: استخدام JOIN FETCH لحل مشكلة N+1
// جيد: استعلام واحد باستخدام JOIN FETCH
@Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.items i JOIN FETCH i.product WHERE o.status = :status")
List<Order> findByStatusWithItems(@Param("status") String status);
الناتج:
// التنفيذ ناجح
(3) ▶ مثال: التحميل التعريفي باستخدام @EntityGraph
@Entity
@NamedEntityGraph(
name = "Order.withItemsAndProduct",
attributeNodes = {
@NamedAttributeNode("items"),
@NamedAttributeNode(value = "items", subgraph = "item-product")
},
subgraphs = {
@NamedSubgraph(name = "item-product", attributeNodes = @NamedAttributeNode("product"))
}
)
public class Order { /* ... */ }
// الاستخدام في المستودع
@EntityGraph(value = "Order.withItemsAndProduct", type = EntityGraphType.LOAD)
List<Order> findByStatus(String status);
الناتج:
// التنفيذ ناجح
| الحل | عدد عبارات SQL | السيناريوهات المطبقة | قابلية الصيانة |
|---|---|---|---|
| التحميل الكسول الافتراضي | N+1 | استعلام كائن واحد | عالية |
| JOIN FETCH | 1 | استعلام محدد | متوسطة |
| @EntityGraph | 1 | خطة تحميل قابلة لإعادة الاستخدام | عالية |
| @BatchSize | N/batchSize | تحميل دفعي | عالية |
6. تحسين فهرسة قاعدة البيانات
(1) استراتيجية الفهرسة
(1) ▶ مثال: إضافة فهرس قاعدة بيانات
-- فهرس لاستعلامات حالة الطلب
CREATE INDEX idx_order_status ON orders(status);
-- فهرس مركب لنمط الاستعلام الشائع
CREATE INDEX idx_order_status_created ON orders(status, created_at DESC);
-- فهرس للبحث عن المنتجات
CREATE INDEX idx_product_name ON products(name);
-- فهرس للبحث عن منتج عناصر الطلب
CREATE INDEX idx_order_item_product ON order_items(product_id);
الناتج:
CREATE TABLE
| نوع الفهرس | حالة الاستخدام | مثال |
|---|---|---|
| فهرس عمود واحد | استعلام بشرط واحد | WHERE status = 'PENDING' |
| فهرس مركب | استعلام متعدد الشروط | WHERE status = ? AND created_at > ? |
| فهرس فريد | قيد فريد | UNIQUE(sku) |
| فهرس تغطي | الفهرس يحتوي على جميع أعمدة الاستعلام | SELECT status FROM orders WHERE id = ? |
(2) ▶ مثال: تحليل الاستعلامات البطيئة
-- تهيئة سجل الاستعلامات البطيئة في MySQL
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1; -- تسجيل الاستعلامات التي تتجاوز ثانية واحدة
SET GLOBAL log_queries_not_using_indexes = ON;
-- تحليل خطة تنفيذ الاستعلام
EXPLAIN SELECT o.*, i.*
FROM orders o
JOIN order_items i ON o.id = i.order_id
WHERE o.status = 'PENDING'
AND o.created_at > '2024-01-01'
ORDER BY o.created_at DESC
LIMIT 20;
الناتج:
CREATE TABLE
| حقل EXPLAIN | المعنى | النقاط الرئيسية |
|---|---|---|
type |
نوع الوصول | ALL (مسح كامل للجدول) يحتاج إلى تحسين |
key |
الفهرس المستخدم | NULL يشير إلى عدم استخدام فهرس |
rows |
عدد الصفوف الممسوحة | كلما قل كان أفضل |
Extra |
معلومات إضافية | Using filesort يحتاج إلى تحسين |
7. مثال شامل: انخفاض P99 لـ OrderFlow من 500 مللي ثانية إلى 50 مللي ثانية
# application-prod.yml (محسّن)
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 20
connection-timeout: 3000
leak-detection-threshold: 60000
jpa:
hibernate:
ddl-auto: validate
show-sql: false
properties:
hibernate:
default_batch_fetch_size: 100
jdbc:
batch_size: 50
order_inserts: true
order_updates: true
# معاملات JVM (Docker ENTRYPOINT)
java -XX:+UseG1GC \
-XX:MaxRAMPercentage=75.0 \
-XX:InitialRAMPercentage=50.0 \
-XX:+UseStringDeduplication \
-XX:MaxGCPauseMillis=100 \
-Xlog:gc*:file=/app/logs/gc.log:time,uptime,level,tags \
-jar app.jar
// OrderRepository محسّن مع EntityGraph والتحميل الدفعي
public interface OrderRepository extends JpaRepository<Order, Long> {
@EntityGraph(value = "Order.withItemsAndProduct", type = EntityGraphType.LOAD)
@Query("SELECT o FROM Order o WHERE o.status = :status ORDER BY o.createdAt DESC")
Page<Order> findByStatusPaged(@Param("status") String status, Pageable pageable);
@EntityGraph(value = "Order.withItemsAndProduct", type = EntityGraphType.LOAD)
Optional<Order> findById(Long id);
}
// تهيئة حجم الدفعة في Hibernate تزيل مشكلة N+1 للمجموعات
// application.yml: hibernate.default_batch_fetch_size=100
| قبل التحسين | بعد التحسين | عامل التحسين |
|---|---|---|
| P99: 500 مللي ثانية | P99: 50 مللي ثانية | 10x |
| SQL/طلب: 101 | SQL/طلب: 1 | 100x |
| توقف GC: 200 مللي ثانية | توقف GC: 50 مللي ثانية | 4x |
| انتظار الاتصال: 100 مللي ثانية | انتظار الاتصال: 2 مللي ثانية | 50x |
❓ أسئلة شائعة
batch_size في Hibernate؟hibernate.jdbc.batch_size=50، يتم تنفيذ عبارات INSERT/UPDATE على دفعات (50 سجل لكل دفعة)، مما يقلل عدد الرحلات إلى قاعدة البيانات. يعمل بشكل أفضل عند استخدامه مع order_inserts=true./metrics للتحقق من مدة طلبات HTTP؛ 2) حلل سجلات GC لتحديد توقفات GC؛ 3) مقاييس HikariCP للتحقق من حالة تجمع الاتصالات؛ 4) سجل الاستعلامات البطيئة في MySQL لتحديد الاستعلامات البطيئة؛ 5) أدوات APM (SkyWalking/Zipkin) للتتبع الشامل.📖 ملخص
- ضبط JVM: G1GC كافٍ افتراضيًا؛ ZGC مصمم لزمن استجابة فائق الانخفاض؛ MaxRAMPercentage يتحكم في ذاكرة الحاوية
- HikariCP: حجم تجمع الاتصالات = عدد أنوية المعالج x 2 + عدد الأقراص؛ فعّل leakDetection لاستكشاف تسربات الذاكرة
- مشكلة N+1: JOIN FETCH في عبارة SQL واحدة، تحميل تعريفي بـ @EntityGraph، تحميل دفعي بـ @BatchSize
- فهارس قاعدة البيانات: أضف فهارس لشروط الاستعلام المتكررة؛ استخدم EXPLAIN لتحليل خطة التنفيذ
- المعالجة الدفعية: hibernate.jdbc.batch_size + order_inserts يقلل عدد الرحلات إلى قاعدة البيانات
- التحسين المنهجي: حدد الاختناقات ← حدد الخطوط الأساسية ← حسّن طبقة تلو الأخرى ← تحقق من النتائج
📝 تمارين
-
تمرين أساسي (الصعوبة: ⭐): هيئ معاملات JVM G1GC ومعاملات تجمع اتصالات HikariCP لـ OrderFlow، وفعّل تسجيل GC واكتشاف تسرب الاتصالات.
-
مسألة متقدمة (الصعوبة ⭐⭐): استخدم
@EntityGraphوJOIN FETCHلحل مشكلة N+1 في استعلام قائمة الطلبات، وقارن عدد عبارات SQL وزمن الاستجابة قبل وبعد التحسين. -
تحدٍ (الصعوبة: ⭐⭐⭐): استخدم JMeter لإجراء اختبار حمل على واجهة قائمة طلبات OrderFlow، وأنشئ خطًا أساسيًا للأداء، وحسّنه تدريجيًا (JVM + تجمع الاتصالات + N+1 + فهارس). سجّل التغيرات في P50 وP99 لكل خطوة تحسين، وفي النهاية حسّن P99 إلى أقل من 50 مللي ثانية.



