حملة Spring Native على Google Cloud

1. نظرة عامة

في هذا الدرس التطبيقي حول الترميز، سنتعرّف على مشروع Spring Native، وسننشئ تطبيقًا يستخدمه، ثم سننشره على Google Cloud.

سنتناول مكوّناته وسجلّه الحديث وبعض حالات الاستخدام، وبالطبع الخطوات المطلوبة لاستخدامه في مشاريعك.

مشروع Spring Native في مرحلة تجريبية حاليًا، لذا سيتطلّب بعض الإعدادات المحدّدة للبدء. ومع ذلك، وكما أعلنّا في مؤتمر SpringOne 2021، سيتم دمج Spring Native في الإصدار 6.0 من Spring Framework والإصدار 3.0 من Spring Boot مع توفير دعم من الدرجة الأولى، لذا هذا هو الوقت المثالي لإلقاء نظرة فاحصة على المشروع قبل بضعة أشهر من إطلاقه.

على الرغم من أنّ ميزة "التجميع أثناء التنفيذ" تم تحسينها بشكل كبير لتناسب حالات مثل العمليات الطويلة الأمد، هناك بعض حالات الاستخدام التي يكون فيها أداء التطبيقات المجمّعة مسبقًا أفضل، وسنتناول هذه الحالات خلال درس تطبيقي حول الترميز.

ستتعرَّف على كيفية إجراء ما يلي:

  • استخدام Cloud Shell
  • تفعيل Cloud Run API
  • إنشاء تطبيق Spring Native ونشره
  • نشر تطبيق من هذا النوع على Cloud Run

المتطلبات

استطلاع

كيف ستستخدم هذا البرنامج التعليمي؟

قراءة المحتوى فقط قراءة المحتوى وإكمال التمارين

كيف تقيّم تجربتك مع Java؟

مبتدئ متوسط متمكّن

ما هو تقييمك لتجربة استخدام خدمات Google Cloud؟

مبتدئ متوسط متقدّم

2. الخلفية

يستفيد مشروع Spring Native من العديد من التقنيات لتوفير أداء التطبيقات الأصلية للمطوّرين.

لفهم Spring Native بشكل كامل، من المفيد التعرّف على بعض هذه التقنيات المكوّنة وما تتيحه لنا وكيف تعمل معًا هنا.

التحويل البرمجي مسبقًا (AOT)

عندما ينفّذ المطوّرون الأمر javac بشكلٍ عادي في وقت الترجمة، تتم ترجمة الرمز المصدري ‎ .java إلى ملفات ‎ .class مكتوبة بلغة الرموز الثنائية. من المفترض أن تفهم آلة Java الافتراضية (JVM) فقط رمز البايت هذا، لذا سيتعين على JVM تفسير هذا الرمز على الأجهزة الأخرى لنتمكّن من تشغيل الرمز.

هذه العملية هي التي تمنح Java إمكانية نقل التوقيع، ما يتيح لنا "كتابة الرمز البرمجي مرة واحدة وتشغيله في أي مكان"، ولكنها مكلفة مقارنةً بتشغيل الرمز البرمجي الأصلي.

لحسن الحظ، تستخدم معظم عمليات تنفيذ JVM الترجمة الفورية للحد من تكلفة التفسير هذه. ويتم تحقيق ذلك من خلال احتساب عدد مرات استدعاء دالة معيّنة، وإذا تم استدعاؤها بشكل متكرّر بما يكفي لتجاوز الحدّ الأدنى ( 10,000 تلقائيًا)، يتم تجميعها في رموز برمجية أصلية في وقت التشغيل لمنع المزيد من عمليات التفسير المكلفة.

تتّبع عملية التجميع المسبق أسلوبًا معاكسًا، وذلك من خلال تجميع كل الرموز البرمجية التي يمكن الوصول إليها في ملف تنفيذي أصلي في وقت التجميع. يؤدي ذلك إلى استبدال إمكانية النقل بكفاءة الذاكرة ومكاسب أخرى في الأداء أثناء وقت التشغيل.

5042e8e62a05a27.png

هذا بالطبع حل وسط، ولا يستحقّ اتّخاذه دائمًا. ومع ذلك، يمكن أن تتفوّق عملية تجميع AOT في بعض حالات الاستخدام، مثل:

  • التطبيقات التي لا تدوم طويلاً والتي يكون فيها وقت بدء التشغيل مهمًا
  • البيئات التي تكون فيها الذاكرة محدودة للغاية وقد يكون تجميع JIT مكلفًا جدًا

للعلم، تم طرح تجميع AOT كـ ميزة تجريبية في JDK 9، ولكن كان من الصعب الحفاظ على هذا التنفيذ، ولم يحظَ بشعبية كبيرة، لذا تم إزالته بهدوء في Java 17 لصالح استخدام المطوّرين GraalVM فقط.

GraalVM

‫GraalVM هي حزمة توزيع JDK مفتوحة المصدر ومحسَّنة بشكل كبير، وتتميّز بأوقات بدء تشغيل سريعة للغاية، وتجميع الصور الأصلية AOT، وإمكانات متعددة اللغات تتيح للمطوّرين دمج لغات متعددة في تطبيق واحد.

يتم تطوير GraalVM بشكل نشط، ويتم إضافة إمكانات جديدة وتحسين الإمكانات الحالية باستمرار، لذا أنصح المطوّرين بمتابعة آخر الأخبار.

في ما يلي بعض الإنجازات الأخيرة:

  • إخراج جديد وسهل الاستخدام لإنشاء الصور الأصلية ( 2021-01-18)
  • إتاحة Java 17 ( 18-01-2022)
  • تفعيل التجميع المتعدد المستويات تلقائيًا لتحسين أوقات تجميع اللغات المتعددة ( 2021-04-20)

Spring Native

ببساطة، تتيح Spring Native استخدام برنامج التجميع native-image من GraalVM لتحويل تطبيقات Spring إلى ملفات تنفيذية أصلية.

تتضمّن هذه العملية إجراء تحليل ثابت لتطبيقك في وقت الترجمة البرمجية للعثور على جميع الطرق في تطبيقك التي يمكن الوصول إليها من نقطة الدخول.

يؤدي ذلك بشكل أساسي إلى إنشاء مفهوم "العالم المغلق" لتطبيقك، حيث يُفترض أنّه يتم التعرّف على جميع الرموز البرمجية في وقت الترجمة، ولا يُسمح بتحميل أي رموز برمجية جديدة في وقت التشغيل.

من المهم ملاحظة أنّ إنشاء الصور الأصلية هو عملية تتطلّب الكثير من الذاكرة وتستغرق وقتًا أطول من تجميع تطبيق عادي، كما تفرض قيودًا على جوانب معيّنة من Java.

في بعض الحالات، لا يلزم إجراء أي تغييرات على الرموز البرمجية لكي يعمل التطبيق مع Spring Native. ومع ذلك، تتطلّب بعض الحالات إعدادات أصلية محدّدة لتعمل بشكلٍ سليم. في هذه الحالات، غالبًا ما يوفّر Spring Native تلميحات حول التطبيقات الأصلية لتبسيط هذه العملية.

3- الإعداد/العمل التحضيري

قبل البدء في تنفيذ Spring Native، علينا إنشاء تطبيقنا ونشره لتحديد مستوى أداء أساسي يمكننا مقارنته بالإصدار الأصلي لاحقًا.

1. إنشاء المشروع

سنبدأ بالحصول على تطبيقنا من start.spring.io:

curl https://start.spring.io/starter.zip -d dependencies=web \
           -d javaVersion=11 \
           -d bootVersion=2.6.4 -o io-native-starter.zip

يستخدم هذا التطبيق الأوّلي الإصدار 2.6.4 من Spring Boot، وهو أحدث إصدار يتوافق معه مشروع spring-native في وقت كتابة هذا المستند.

يُرجى العِلم أنّه منذ إصدار GraalVM 21.0.3، يمكنك استخدام Java 17 لهذا النموذج أيضًا. سنستمر في استخدام Java 11 في هذا البرنامج التعليمي للحدّ من الإعدادات المطلوبة.

بعد الحصول على ملف zip في سطر الأوامر، يمكننا إنشاء دليل فرعي لمشروعنا وفك ضغط المجلد فيه:

mkdir spring-native

cd spring-native

unzip ../io-native-starter.zip

2. تغييرات الرمز

بعد فتح المشروع، سنضيف بسرعة إشارة إلى أنّه يعمل وسنعرض أداء Spring Native بعد تشغيله.

عدِّل ملف DemoApplication.java ليتطابق مع ما يلي:

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.Duration;
import java.time.Instant;

@RestController
@SpringBootApplication
public class DemoApplication {
    private static Instant startTime;
    private static Instant readyTime;

    public static void main(String[] args) {
        startTime = Instant.now();
                SpringApplication.run(DemoApplication.class, args);
    }

    @GetMapping("/")
    public String index() {
        return "Time between start and ApplicationReadyEvent: "
                + Duration.between(startTime, readyTime).toMillis()
                + "ms";
    }

    @EventListener(ApplicationReadyEvent.class)
    public void ready() {
                readyTime = Instant.now();
    }
}

في هذه المرحلة، يكون تطبيقنا الأساسي جاهزًا، لذا يمكنك إنشاء صورة وتشغيلها على جهازك للحصول على فكرة عن وقت بدء التشغيل قبل تحويله إلى تطبيق أصلي.

لإنشاء الصورة، اتّبِع الخطوات التالية:

mvn spring-boot:build-image

يمكنك أيضًا استخدام docker images demo للحصول على فكرة عن حجم الصورة الأساسية: 6ecb403e9af1475e.png

لتشغيل تطبيقنا، يُرجى اتّباع الخطوات التالية:

docker run --rm -p 8080:8080 demo:0.0.1-SNAPSHOT

3. نشر تطبيق أساسي

بعد أن أصبح لدينا تطبيق، سننشره ونسجّل أوقات التشغيل التي سنقارنها لاحقًا بأوقات تشغيل التطبيق الأصلي.

استنادًا إلى نوع التطبيق الذي تنشئه، تتوفّر عدة طرق مختلفة لاستضافة محتواك.

ومع ذلك، بما أنّ مثالنا هو تطبيق ويب بسيط جدًا ومباشر، يمكننا إبقاء الأمور بسيطة والاعتماد على Cloud Run.

إذا كنت تتّبع الخطوات على جهازك الخاص، تأكَّد من تثبيت أداة gcloud CLI وتحديثها.

إذا كنت تستخدم Cloud Shell، سيتم الاهتمام بكل ذلك ويمكنك ببساطة تنفيذ ما يلي في دليل ملفات المصدر:

gcloud run deploy

4. إعدادات التطبيق

1. إعداد مستودعات Maven

بما أنّ هذا المشروع لا يزال في مرحلة التجربة، علينا إعداد تطبيقنا لنتمكّن من العثور على العناصر التجريبية التي لا تتوفّر في مستودع Maven المركزي.

سيتضمّن ذلك إضافة العناصر التالية إلى ملف pom.xml، ويمكنك إجراء ذلك في المحرّر الذي تختاره.

أضِف الأقسام التالية repositories وpluginRepositories إلى ملف pom:

<repositories>
    <repository>
        <id>spring-release</id>
        <name>Spring release</name>
        <url>https://repo.spring.io/release</url>
    </repository>
</repositories>

<pluginRepositories>
    <pluginRepository>
        <id>spring-release</id>
        <name>Spring release</name>
        <url>https://repo.spring.io/release</url>
    </pluginRepository>
</pluginRepositories>

2. إضافة التبعيات

بعد ذلك، أضِف الاعتمادية spring-native، وهي مطلوبة لتشغيل تطبيق Spring كصورة أصلية. ملاحظة: هذه الخطوة غير ضرورية إذا كنت تستخدم Gradle

<dependencies>
    <!-- ... -->
    <dependency>
        <groupId>org.springframework.experimental</groupId>
        <artifactId>spring-native</artifactId>
        <version>0.11.2</version>
    </dependency>
</dependencies>

3. إضافة المكوّنات الإضافية أو تفعيلها

الآن، أضِف مكوّن AOT الإضافي لتحسين التوافق مع الصور الأصلية وحجمها ( مزيد من المعلومات):

<plugins>
    <!-- ... -->
    <plugin>
        <groupId>org.springframework.experimental</groupId>
        <artifactId>spring-aot-maven-plugin</artifactId>
        <version>0.11.2</version>
        <executions>
            <execution>
                <id>generate</id>
                <goals>
                    <goal>generate</goal>
                </goals>
            </execution>
        </executions>
    </plugin>
</plugins>

سنعدّل الآن spring-boot-maven-plugin لتفعيل إمكانية استخدام الصور الأصلية واستخدام أداة الإنشاء paketo لإنشاء الصورة الأصلية:

<plugins>
    <!-- ... -->
    <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
            <image>
                <builder>paketobuildpacks/builder:tiny</builder>
                <env>
                    <BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
                </env>
            </image>
        </configuration>
    </plugin>    
</plugins>

يُرجى العِلم أنّ صورة أداة الإنشاء الصغيرة هي مجرد خيار واحد من بين عدة خيارات. وهو خيار جيد لحالة الاستخدام لدينا لأنّه يتضمّن عددًا قليلاً جدًا من المكتبات والأدوات الإضافية، ما يساعد في تقليل مساحة الهجوم.

إذا كنت، على سبيل المثال، بصدد إنشاء تطبيق يحتاج إلى الوصول إلى بعض مكتبات C الشائعة، أو إذا لم تكن متأكدًا بعد من متطلبات تطبيقك، قد يكون برنامج الإنشاء الكامل خيارًا أفضل.

5- إنشاء تطبيق أصلي وتشغيله

بعد إعداد كل ذلك، من المفترض أن نتمكّن من إنشاء الصورة وتشغيل التطبيق المدمج مع المحتوى والمترجَم.

قبل تشغيل الإصدار، إليك بعض النقاط التي يجب وضعها في الاعتبار:

  • سيستغرق ذلك وقتًا أطول من عملية الإنشاء العادية (بضع دقائق) d420322893640701.png
  • يمكن أن تستغرق عملية التصميم هذه الكثير من الذاكرة (بضع غيغابايت) cda24e1eb11fdbea.png
  • تتطلّب عملية التصميم هذه إمكانية الوصول إلى برنامج Docker الخفي
  • في هذا المثال، سنستعرض العملية يدويًا، ولكن يمكنك أيضًا ضبط مراحل التصميم على تفعيل ملف تصميم أصلي تلقائيًا.

لإنشاء الصورة، اتّبِع الخطوات التالية:

mvn spring-boot:build-image

بعد إنشاء التطبيق الأصلي، يصبح جاهزًا للاستخدام.

لتشغيل تطبيقنا، يُرجى اتّباع الخطوات التالية:

docker run --rm -p 8080:8080 demo:0.0.1-SNAPSHOT

في هذه المرحلة، نكون في وضع جيد لمعرفة الجانبين الإيجابي والسلبي لتطبيقات الأجهزة الجوّالة.

لقد استغرقنا وقتًا أطول قليلاً واستخدمنا ذاكرة إضافية أثناء وقت التجميع، ولكن في المقابل، حصلنا على تطبيق يمكن تشغيله بسرعة أكبر بكثير، ويستهلك ذاكرة أقل بكثير (حسب عبء العمل).

إذا نفّذنا docker images demo لمقارنة حجم الصورة الأصلية بالصورة التي تم تحويلها إلى تنسيق WebP، سنلاحظ انخفاضًا كبيرًا في الحجم:

e667f65a011c1328.png

يجب أيضًا ملاحظة أنّه في حالات الاستخدام الأكثر تعقيدًا، يجب إجراء تعديلات إضافية لإعلام برنامج الترجمة البرمجية مسبقًا (AOT) بما سيفعله تطبيقك في وقت التشغيل. لهذا السبب، قد تكون بعض أحمال العمل التي يمكن توقّعها (مثل مهام الدفعات) مناسبة جدًا لذلك، بينما قد تتطلّب أحمال عمل أخرى جهدًا أكبر.

6. نشر تطبيقنا الأصلي

لنشر تطبيقنا على Cloud Run، علينا إدخال الصورة الأصلية في أداة إدارة حِزم مثل Artifact Registry.

1. إعداد مستودع Docker

يمكننا بدء هذه العملية من خلال إنشاء مستودع على النحو التالي:

gcloud artifacts repositories create native-image-docker-repo --repository-format=docker \
--location=us-central1 --description="Repository for our native images"

بعد ذلك، علينا التأكّد من أنّنا مصادقون على إرسال البيانات إلى سجلّنا الجديد.

يمكن أن تسهّل أداة سطر الأوامر gcloud هذه العملية إلى حدٍّ كبير:

gcloud auth configure-docker us-central1-docker.pkg.dev

2. نقل صورتنا إلى Artifact Registry

بعد ذلك، سنضع علامة على الصورة:

export PROJECT_ID=$(gcloud config list --format 'value(core.project)')


docker tag  demo:0.0.1-SNAPSHOT \
us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2

بعد ذلك، يمكننا استخدام docker push لإرساله إلى Artifact Registry:

docker push us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2

3- النشر على Cloud Run

نحن الآن مستعدون لنشر الصورة التي خزّناها في Artifact Registry إلى Cloud Run:

gcloud run deploy --image us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2

بما أنّنا أنشأنا تطبيقنا ونشرناه كصورة أصلية، يمكننا الاطمئنان إلى أنّ تطبيقنا يستفيد بشكل ممتاز من تكاليف بنيتنا الأساسية أثناء تشغيله.

يمكنك مقارنة أوقات بدء تشغيل تطبيقنا الأساسي بتطبيقنا الجديد الأصلي.

6dde63d35959b1bb.png

7. الملخّص/التنظيف

تهانينا على إنشاء تطبيق Spring Native ونشره على Google Cloud.

نأمل أن يشجّعك هذا البرنامج التعليمي على التعرّف أكثر على مشروع Spring Native وأن تضعه في اعتبارك إذا كان يلبي احتياجاتك في المستقبل.

اختياري: تنظيف الخدمة و/أو إيقافها

سواء أنشأت مشروعًا على Google Cloud لهذا الدرس التطبيقي حول الترميز أو كنت تعيد استخدام مشروع حالي، احرص على تجنُّب الرسوم غير الضرورية من الموارد التي استخدمناها.

يمكنك حذف أو إيقاف خدمات Cloud Run التي أنشأناها، أو حذف الصورة التي استضفناها، أو إيقاف المشروع بأكمله.

8. مراجع إضافية

على الرغم من أنّ مشروع Spring Native هو حاليًا مشروع جديد وتجريبي، تتوفّر بالفعل مجموعة كبيرة من المراجع الجيدة لمساعدة المستخدمين الأوائل في تحديد المشاكل وحلّها والمشاركة في المشروع:

مراجع إضافية

في ما يلي مراجع على الإنترنت قد تكون ذات صلة بهذا البرنامج التعليمي:

الترخيص

يخضع هذا العمل لترخيص المشاع الإبداعي مع نسب العمل إلى مؤلفه 2.0 Generic License.