אופטימיזציה של אפליקציות Java ל-Cloud Run

במדריך הזה מתוארות אופטימיזציות לשירותי Cloud Run שנכתבו בשפת התכנות Java, וגם מידע רקע שיעזור לכם להבין את היתרונות והחסרונות של חלק מהאופטימיזציות. המידע בדף זה משלים את טיפים כלליים לאופטימיזציה, שחלים גם על Java.

אפליקציות אינטרנט רגילות מבוססות-Java מתוכננות לטפל בבקשות עם רמת מקביליות גבוהה וזמן אחזור נמוך, והן נוטות להיות אפליקציות שפועלות לאורך זמן. גם ה-JVM עצמו מבצע אופטימיזציה של קוד הביצוע לאורך זמן באמצעות JIT, כך שנתיבי ביצוע חמים עוברים אופטימיזציה והאפליקציות פועלות בצורה יעילה יותר לאורך זמן.

רבות משיטות העבודה והאופטימיזציות המומלצות ביישומי Java קונבנציונליים מבוססי-אינטרנט סובבות סביב:

  • טיפול בבקשות מקבילות (גם כאלה שמבוססות על שרשור וגם כאלה שלא חוסמות קלט/פלט)
  • הפחתת זמן האחזור של התגובה באמצעות איגום חיבורים ועיבוד באצווה של פונקציות לא קריטיות, למשל שליחת עקבות ומדדים למשימות ברקע.

בעוד שרבות מהאופטימיזציות הקונבנציונליות הללו פועלות היטב עבור יישומים ארוכי טווח, ייתכן שהן לא יעבדו באותה מידה בשירות Cloud Run, שפועל רק כאשר הוא מגיש בקשות באופן פעיל. בדף הזה מפורטות כמה אופטימיזציות שונות ופשרות שקשורות ל-Cloud Run, שבעזרתן אפשר לקצר את זמן ההפעלה ולצמצם את השימוש בזיכרון.

שימוש בהגברת מהירות המעבד בזמן ההפעלה כדי להפחית את זמן האחזור של ההפעלה

אתם יכולים להפעיל את האפשרות להגברת מהירות המעבד בזמן ההפעלה כדי להגדיל באופן זמני את הקצאת המעבד במהלך הפעלת המופע, וכך להקטין את זמן האחזור של ההפעלה.

המדדים של גוגל הראו שאפליקציות ג'אווה מרוויחות אם הן משתמשות ב-startup CPU boost, מה שיכול להפחית את זמני האתחול עד 50%.

אופטימיזציה של קובץ אימג' של קונטיינר של אפליקציית Java

על ידי אופטימיזציה של קובץ אימג' של קונטיינר, ניתן להפחית את זמני הטעינה וההפעלה. ניתן לייעל את התמונה על ידי:

  • מזעור קובץ אימג' של קונטיינר
  • הימנעות משימוש בקבצי JAR מקוננים של ארכיון ספריות
  • שימוש בג'יב

מזעור קובץ אימג' של קונטיינר

מידע נוסף על הבעיה הזו מופיע בדף הטיפים הכלליים בנושא צמצום הקונטיינר. דף הטיפים הכלליים ממליץ לצמצם את תוכן תמונת המכולה רק למה שנדרש. לדוגמה, ודא שקובץ אימג' של קונטיינר שלך אינה מכילה :

  • קוד מקור
  • מבנים של מייבן
  • כלי בניית
  • ספריות גיט
  • קבצים בינאריים או כלי עזר שאינם בשימוש

אם אתם בונים את הקוד מתוך Dockerfile, השתמשו בבנייה מרובת שלבים של Docker כך שתמונת המכולה הסופית תכיל רק את קובץ ה-JRE ואת קובץ ה-JAR של האפליקציה עצמו.

הימנעו מקבצי JAR מקוננים של ארכיוני ספריות

חלק מהמסגרות הפופולריות, כמו Spring Boot, יוצרות קובץ ארכיון של אפליקציה (JAR) שמכיל קובצי JAR נוספים של ספריות (JARs מוטבעים). צריך לפתוח את הקבצים האלה (לבטל את הדחיסה שלהם) בזמן ההפעלה, וזה עלול להשפיע לרעה על מהירות ההפעלה ב-Cloud Run. לכן, כשזה אפשרי, כדאי ליצור קובץ JAR דק עם ספריות חיצוניות. אפשר לעשות זאת באופן אוטומטי באמצעות Jib כדי להכניס את האפליקציה שלכם לקונטיינר.

השתמש בג'יב

השתמשו בתוסף Jib כדי ליצור קונטיינר מינימלי ולשטח את ארכיון האפליקציה באופן אוטומטי. ‫Jib פועל עם Maven ועם Gradle, ופועל עם אפליקציות Spring Boot ללא צורך בהגדרה. יכול להיות שחלק ממסגרות האפליקציות ידרשו הגדרות נוספות של Jib.

אופטימיזציות JVM עבור יישומי Cloud Run Java

אופטימיזציה של ה-JVM עבור שירות Cloud Run יכולה להביא לשיפור הביצועים וניצול הזיכרון.

השתמש בגרסאות JVM תואמות קונטיינרים

במכונות וירטואליות ובמכונות, כדי להקצות מעבד (CPU) וזיכרון, ה-JVM מבין את המעבד והזיכרון שהוא יכול להשתמש בהם ממיקומים ידועים, לדוגמה, ב-Linux,‏ /proc/cpuinfo ו-/proc/meminfo. עם זאת, בעת הפעלה בתוך קונטיינר, אילוצי המעבד והזיכרון מאוחסנים ב-/proc/cgroups/.... גרסה ישנה יותר של ה-JDK ממשיכה לחפש ב-/proc במקום ב-/proc/cgroups, מה שעלול לגרום לשימוש רב יותר במעבד ובשימוש בזיכרון ממה שהוקצה. זה יכול לגרום ל:

  • מספר מוגזם של שרשורים כי גודל מאגר השרשורים מוגדר על ידי Runtime.availableProcessors()
  • ערימה מקסימלית המוגדרת כברירת מחדל שחורגת ממגבלת זיכרון המכולה. ה-JVM משתמש בזיכרון באופן אגרסיבי לפני שהוא אוסף. זה יכול בקלות לגרום למכולה לחרוג ממגבלת זיכרון המכולה, ולקבל ביטול פעולה של OOMKilled.

אז, השתמש בגרסת JVM מודעת למכולות. גרסאות OpenJDK גדולות או שוות לגרסה 8u192 מודעות למכולות כברירת מחדל.

כיצד להבין את השימוש בזיכרון JVM

ניצול הזיכרון של ה-JVM מורכב מניצול זיכרון מקורי וניצול ערימה. זיכרון העבודה של האפליקציה שלך נמצא בדרך כלל בערימה (heap). גודל הערימה מוגבל על ידי תצורת ה-Max Heap. עם מופע RAM של Cloud Run בנפח 256MB, לא ניתן להקצות את כל 256MB ל-Max Heap, מכיוון שה-JVM ומערכת ההפעלה דורשים גם זיכרון מקורי, לדוגמה, מחסנית הליכים, מטמוני קוד, מזהי זיהוי קבצים, מאגרים וכו'. אם האפליקציה שלך מקבלת OOMKilled ואתה צריך לדעת את ניצול הזיכרון של ה-JVM (זיכרון מקורי + ערימה), הפעל את Native Memory Tracking כדי לראות שימושים לאחר יציאה מוצלחת של האפליקציה. אם האפליקציה שלך מקבלת תוקף OOMKilled, היא לא תוכל להדפיס את המידע. במקרה כזה, צריך להריץ את האפליקציה עם יותר זיכרון כדי שהיא תוכל ליצור את הפלט בהצלחה.

אי אפשר להפעיל את מעקב הזיכרון המקורי באמצעות משתנה הסביבה JAVA_TOOL_OPTIONS. צריך להוסיף את ארגומנט ההפעלה של שורת הפקודה של Java לנקודת הכניסה של קובץ אימג' של קונטיינר, כדי שהאפליקציה תופעל עם הארגומנטים האלה:

java -XX:NativeMemoryTracking=summary \
  -XX:+UnlockDiagnosticVMOptions \
  -XX:+PrintNMTStatistics \
  ...

שקול להשתמש במחשבון זיכרון של Java בקוד פתוח כדי להעריך את צורכי הזיכרון.

כבה את מהדר האופטימיזציה

כברירת מחדל, ל-JVM יש כמה שלבים של הידור JIT. למרות ששלבים אלה משפרים את יעילות האפליקציה שלך לאורך זמן, הם יכולים גם להוסיף תקורה לשימוש בזיכרון ולהאריך את זמן ההפעלה.

באפליקציות ללא שרת (serverless) שפועלות לזמן קצר (לדוגמה, פונקציות), כדאי להשבית את שלבי האופטימיזציה כדי להקטין את זמן ההפעלה על חשבון יעילות לטווח ארוך.

עבור שירות Cloud Run, הגדר את המשתנה הסביבתי:

JAVA_TOOL_OPTIONS="-XX:+TieredCompilation -XX:TieredStopAtLevel=1"

השתמש בשיתוף נתוני מחלקה של יישומים

כדי להפחית עוד יותר את זמן ה-JIT ואת השימוש בזיכרון, שקלו להשתמש בשיתוף נתוני מחלקות יישומים (AppCDS) כדי לשתף את מחלקות ה-Java שהורכבו מראש כארכיון. ניתן לעשות שימוש חוזר בארכיון AppCDS בעת הפעלת מופע אחר של אותו יישום Java. ה-JVM יכול לעשות שימוש חוזר בנתונים שחושבו מראש מהארכיון, מה שמקצר את זמן האתחול.

השיקולים הבאים חלים על השימוש ב-AppCDS:

  • יש לשכפל את ארכיון ה-AppCDS שיש להשתמש בו שוב באמצעות אותה הפצה, גרסה וארכיטקטורה של OpenJDK ששימשו במקור לייצורו.
  • עליך להפעיל את היישום שלך לפחות פעם אחת כדי ליצור את רשימת המחלקות לשיתוף, ולאחר מכן להשתמש ברשימה זו כדי ליצור את ארכיון AppCDS.
  • הכיסוי של המחלקות תלוי בנתיב הקוד שמופעל במהלך הרצת האפליקציה. כדי להגדיל את הכיסוי, מפעילים באופן פרוגרמטי עוד נתיבי קוד.
  • היישום חייב להיסגר בהצלחה כדי ליצור רשימת מחלקות זו. שקול ליישם דגל יישום המשמש לציון יצירת ארכיון AppCDS, וכך הוא יוכל להיסגר באופן מיידי.
  • ניתן לעשות שימוש חוזר בארכיון AppCDS רק אם מפעילים מופעים חדשים בדיוק באותו אופן שבו נוצר הארכיון.
  • ארכיון AppCDS עובד רק עם חבילת קבצי JAR רגילה; לא ניתן להשתמש בקבצי JAR מקוננים.

דוגמה לאתחול ספרינג באמצעות קובץ JAR מוצלל

אפליקציות Spring Boot משתמשות כברירת מחדל ב-uber JAR מוטמע, שלא יפעל ב-AppCDS. אז אם אתם משתמשים ב-AppCDS, עליכם ליצור קובץ JAR מוצלל. לדוגמה, שימוש ב-Maven ובתוסף Maven Shade:

<build>
  <finalName>helloworld</finalName>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-shade-plugin</artifactId>
      <configuration>
        <keepDependenciesWithProvidedScope>true</keepDependenciesWithProvidedScope>
        <createDependencyReducedPom>true</createDependencyReducedPom>
        <filters>
          <filter>
            <artifact>*:*</artifact>
            <excludes>
              <exclude>META-INF/*.SF</exclude>
              <exclude>META-INF/*.DSA</exclude>
              <exclude>META-INF/*.RSA</exclude>
            </excludes>
          </filter>
        </filters>
      </configuration>
      <executions>
        <execution>
          <phase>package</phase>
          <goals><goal>shade</goal></goals>
          <configuration>
            <transformers>
              <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                <resource>META-INF/spring.handlers</resource>
              </transformer>
              <transformer implementation="org.springframework.boot.maven.PropertiesMergingResourceTransformer">
                <resource>META-INF/spring.factories</resource>
              </transformer>
              <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                <resource>META-INF/spring.schemas</resource>
              </transformer>
              <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
              <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                <mainClass>${mainClass}</mainClass>
              </transformer>
            </transformers>
          </configuration>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

אם קובץ ה-JAR המוצלל שלך מכיל את כל התלויות, תוכל ליצור ארכיון פשוט במהלך בניית המכולה באמצעות Dockerfile:

# Use Docker's multi-stage build
FROM eclipse-temurin:11-jre as APPCDS

COPY target/helloworld.jar /helloworld.jar

# Run the application, but with a custom trigger that exits immediately.
# In this particular example, the application looks for the '--appcds' flag.
# You can implement a similar flag in your own application.
RUN java -XX:DumpLoadedClassList=classes.lst -jar helloworld.jar --appcds=true

# From the captured list of classes (based on execution coverage),
# generate the AppCDS archive file.
RUN java -Xshare:dump -XX:SharedClassListFile=classes.lst -XX:SharedArchiveFile=appcds.jsa --class-path helloworld.jar

FROM eclipse-temurin:11-jre

# Copy both the JAR file and the AppCDS archive file to the runtime container.
COPY --from=APPCDS /helloworld.jar /helloworld.jar
COPY --from=APPCDS /appcds.jsa /appcds.jsa

# Enable Application Class-Data sharing
ENTRYPOINT java -Xshare:on -XX:SharedArchiveFile=appcds.jsa -jar helloworld.jar

הקטנת גודל ערימת החוטים

רוב יישומי האינטרנט של Java מבוססים על thread-per-connection. כל הליך (thread) בג'אווה צורך זיכרון מקורי (לא בערימה). זה ידוע כמחסנית השרשור, ובררת המחדל היא 1MB לכל השרשור. אם היישום שלך מטפל ב-80 בקשות בו זמנית, ייתכן שהוא מכיל לפחות 80 הליכים, מה שמתורגם ל-80MB של שטח אחסון הליכים בשימוש. הזיכרון מתווסף לגודל הערימה. ייתכן שברירת המחדל גדולה מהנדרש. ניתן להקטין את גודל ערימת החוטים.

אם תצמצמו יותר מדי, תראו java.lang.StackOverflowError. ניתן ליצור פרופיל של האפליקציה שלך ולמצוא את גודל ערימת השלבים האופטימלי להגדרה.

עבור שירות Cloud Run, הגדר את המשתנה הסביבתי:

JAVA_TOOL_OPTIONS="-Xss256k"

הפחתת מספר ה-threads לשיפור הביצועים של אפליקציות Java

כדי לבצע אופטימיזציה של הזיכרון, אפשר לצמצם את מספר השרשורים, להשתמש באסטרטגיות ריאקטיביות לא חוסמות ולהימנע מפעילויות ברקע.

צמצום מספר השרשורים

כל הליך בג'אווה עשוי להגדיל את ניצול הזיכרון עקב ערימת ה-Thread. Cloud Run מאפשר מקסימום של 1,000 בקשות בו זמנית. במודל של שרשור לכל חיבור, צריך לכל היותר 1,000 שרשורים כדי לטפל בכל הבקשות בו-זמנית. רוב שרתי האינטרנט והמסגרות מאפשרים לך להגדיר את המספר המרבי של הליכים וחיבורים. לדוגמה, ב-Spring Boot, ניתן להגביל את מספר החיבורים המקסימלי בקובץ applications.properties:

server.tomcat.max-threads=80

כתיבת קוד ריאקטיבי שאינו חוסם כדי לייעל את הזיכרון ואתחול

כדי להפחית באמת את מספר השרשורים, כדאי לאמץ מודל תגובתי של תכנות לא חוסם, כך שאפשר יהיה להפחית באופן משמעותי את מספר השרשורים תוך טיפול ביותר בקשות בו-זמניות. מסגרות יישומים כמו Spring Boot עם Webflux, Micronaut ו-Quarkus תומכות ביישומי אינטרנט ריאקטיביים.

בדרך כלל, למסגרות תגובתיות כמו Spring Boot עם Webflux, ‏ Micronaut ו-Quarkus יש זמני הפעלה מהירים יותר.

אם תמשיכו לכתוב קוד חסימה במסגרת שאינה חסימה, התפוקה ושיעורי השגיאה יהיו גרועים משמעותית בשירות Cloud Run. הסיבה לכך היא שבמסגרות לא חוסמות יש רק כמה שרשורים, למשל 2 או 4. אם הקוד שלך חוסם, הוא יכול להתמודד עם מעט מאוד בקשות בו זמנית.

יכול להיות שהמסגרות האלה שאינן חוסמות גם יעבירו קוד חוסם למאגר שרשורים לא מוגבל – כלומר, למרות שהן יכולות לקבל הרבה בקשות בו-זמנית, הקוד החוסם יפעל בשרשורים חדשים. אם השרשורים מצטברים בלי הגבלה, תנצלו את כל משאבי המעבד ותתחילו להשתמש בזיכרון הווירטואלי. זמן ההשהיה יושפע קשות. אם אתם משתמשים במסגרת שאינה חסימה, ודאו שאתם מבינים את מודלי מאגר ה-thread וכובלים את המאגרים בהתאם.

הגדר חיוב מבוסס מופעים אם אתה משתמש בפעילויות רקע

פעילות ברקע היא כל פעילות שמתרחשת אחרי שהתגובה של HTTP נמסרת. עומסי עבודה מסורתיים שיש להם משימות רקע דורשים תשומת לב מיוחדת בעת הפעלתם ב-Cloud Run.

הגדרת חיוב מבוסס מופעים

אם ברצונך לתמוך בפעילויות רקע בשירות Cloud Run שלך, הגדר את שירות Cloud Run לחיוב מבוסס מופעים כדי שתוכל להפעיל פעילויות רקע מחוץ לבקשות ועדיין לקבל גישה למעבד.

הימנעו מפעילויות רקע אם אתם משתמשים בחיוב מבוסס בקשה

אם עליך להגדיר את השירות שלך לחיוב מבוסס בקשה, עליך להיות מודע לבעיות פוטנציאליות בפעילויות ברקע. לדוגמה, אם אתם אוספים מדדי אפליקציה ואוספים את המדדים ברקע לשליחה מעת לעת, מדדים אלה לא יישלחו כאשר חיוב מבוסס בקשה מוגדר. אם האפליקציה שלך מקבלת בקשות באופן קבוע, ייתכן שתראה פחות בעיות. אם ליישום שלך יש QPS נמוך, ייתכן שהמשימה ברקע לעולם לא תתבצע.

כמה דפוסים ידועים ברקע שעליכם לשים לב אליהם אם תבחרו בחיוב מבוסס בקשה:

  • מאגרי חיבורים של JDBC – ניקויים ובדיקות חיבורים מתבצעים בדרך כלל ברקע
  • שולחי עקבות מבוזרים - עקבות מבוזרות בדרך כלל נשלחות בקבוצות מחזוריות או כאשר המאגר מלא ברקע.
  • שולחי מדדים – בדרך כלל המדדים נשלחים בקבוצות באופן תקופתי ברקע.
  • עבור Spring Boot, כל המתודות המבושרות עם ביאור של @Async
  • טיימרים – יכול להיות שהפעלות שמבוססות על טיימרים (למשל, ScheduledThreadPoolExecutor,‏ Quartz או @Scheduled Spring annotation) לא יתבצעו כשהחיוב מבוסס על בקשות.
  • מקלטי הודעות - לדוגמה, לקוחות משיכה של סטרימינג של Pub/Sub, לקוחות JMS או לקוחות Kafka, בדרך כלל פועלים בתהליכים ברקע ללא צורך בבקשות. אלה לא יעבדו כאשר לאפליקציה שלך אין בקשות. קבלת הודעות בדרך זו אינה מומלצת ב-Cloud Run.

אופטימיזציות של יישומים

בקוד שירות Cloud Run שלך, תוכל גם לבצע אופטימיזציה לזמני הפעלה מהירים יותר וניצול זיכרון מהיר יותר.

הפחתת משימות ההפעלה

במהלך ההפעלה, יישומי אינטרנט של Java צריכים לעיתים קרובות להתמודד עם מספר משימות כמו טעינה מוקדמת של נתונים, חימום מטמונים ויצירת מאגרי חיבורים. כאשר אתה מבצע משימות אלה ברצף, היישום שלך עלול להפוך לאט. כדי לבצע משימות אלה במקביל, יש להגדיל את מספר ליבות המעבד.

Cloud Run שולח בקשת משתמש אמיתית להפעלת מופע של הפעלה קרה. משתמשים שהוקצו להם בקשה למופע שזה עתה הופעל עלולים לחוות עיכובים ארוכים.

עבור יישומים עם זמני אתחול ארוכים, שקול להשתמש בגשש אתחול. בדיקת אתחול מבטיחה ש-Cloud Run ישלח בקשות משתמש למופע רק לאחר שהוא מאותחל במלואו ועובר את בדיקת תקינות האתחול. למידע נוסף, ראה הגדרת בדיקות תקינות של מכולות עבור שירותים.

השתמש באיגוד חיבורים

אם אתם משתמשים במאגרי חיבורים, שימו לב שמאגרי חיבורים עלולים לסלק חיבורים מיותרים ברקע (ראו הימנעות ממשימות רקע). אם האפליקציה שלכם כוללת QPS נמוך ויכולה לסבול זמן אחזור גבוה, כדאי לשקול לפתוח ולסגור חיבורים לכל בקשה. אם באפליקציה יש QPS גבוה, יכול להיות שפינוי ברקע ימשיך להתבצע כל עוד יש בקשות פעילות.

בשני המקרים, הגישה של האפליקציה למסד הנתונים תוגבל על ידי מספר החיבורים המקסימלי שמותרים במסד הנתונים. מחשבים את מספר החיבורים המקסימלי שאפשר ליצור לכל מכונת Cloud Run, ומגדירים את מספר המכונות המקסימלי ב-Cloud Run כך שמספר המכונות המקסימלי כפול מספר החיבורים לכל מכונה יהיה קטן ממספר החיבורים המקסימלי המותר.

אם משתמשים ב-Spring Boot

אם אתם משתמשים ב-Spring Boot, עליכם לשקול את האופטימיזציות הבאות

השתמש ב-Spring Boot גרסה 2.2 או מתקדמת יותר

החל מגרסה 2.2, Spring Boot עבר אופטימיזציה רבה למהירות ההפעלה. אם אתם משתמשים בגרסאות של Spring Boot שקטנות מ-2.2, כדאי לשדרג או להחיל אופטימיזציות ספציפיות באופן ידני.

השתמש באתחול עצל

יש דגל אתחול עצל גלובלי שניתן להפעיל ב-Spring Boot 2.2 ומעלה. זה ישפר את מהירות ההפעלה, אך עם הפשרה, הבקשה הראשונה עשויה להיות בעלת זמן השהייה ארוך יותר מכיוון שהיא תצטרך להמתין לאתחול הרכיבים בפעם הראשונה.

אפשר להפעיל את האתחול המדורג בapplication.properties:

spring.main.lazy-initialization=true

או באמצעות משתנה סביבה:

SPRING_MAIN_LAZY_INITIALIZATIION=true

עם זאת, אם אתם משתמשים ב-min-instances, אז אתחול עצל לא יעזור, מכיוון שהאתחול היה אמור להתרחש כאשר ה-min-instance התחיל.

הימנעו מסריקת כיתות

סריקת כיתות תגרום לקריאות נוספות מהדיסק ב-Cloud Run, כי ב-Cloud Run הגישה לדיסק בדרך כלל איטית יותר מאשר במכונה רגילה. ודא שסריקת הרכיבים מוגבלת או נמנעת לחלוטין.

השתמש בכלי פיתוח Spring Boot שאינם בסביבת ייצור

אם אתם משתמשים ב-Spring Boot Developer Tool במהלך הפיתוח, ודאו שהוא אינו ארוז בתמונת המכולה של הייצור. יכול להיות שהבעיה הזו מתרחשת אם יצרתם את אפליקציית Spring Boot בלי התוספים של Spring Boot build (לדוגמה, באמצעות התוסף Shade או באמצעות Jib ליצירת קונטיינר).

במקרים אלה, ודא שכלי הבנייה אינו כולל במפורש את כלי Spring Boot Dev. או, כבו את כלי המפתחים של Spring Boot במפורש).

המאמרים הבאים

טיפים נוספים