תוכנה דינמית: הדרך לכתוב code scalable ב-JVM

כתיבת קוד scalable ב-JAVA איננה משימה פשוטה. בין השימוש ב-Threads, Locks & synchronization לצורך קידוד מקבילי או הצורך להכפיל את יכולות החישוב ע”י הרצת אותו קוד במכונות שונות במקביל… תוכנה שהיא גם scalable, גם זמינה וגם יכולה להבריא את עצמה ע”י ניטור והשגחה מתמשכים היא חלומם של כל אנשי הפיתוח אז הנה אחת מהן, AKKA.

shutterstock_121063264

הפוסט נכתב על ידי ליאור פרי, מפתח JAVA בכיר ב-Tikal.

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

חוק מור

בשנת 1965 ניבא גורדון מור (מנהל מו”פ בחברת  Fairchild Semiconductor) כי צפיפות הטרנזיסטורים במעגלים משולבים ובמעבד בפרט תוכפל כל שמונה עשרה עד עשרים וארבע חודשים – חוק מור הציג תחזית התפתחות של תעשיית המעבדים והמחשבים ומשמש גם מעין מפת דרכים לתעשייה עצמה.

חוק זה איננו פיזיקלי ואיננו חוק טבע ובהחלט מפתיעה העובדה שהוא מתקיים בדיוק טוב למדי מעל ארבעים שנה.

tikal1

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

כיום יכולות המיזעור של שבב הסיליקון מגיעות לקצה היכולת הפיזיקלית ולכן הפתרון המאפשר להמשיך ולקיים את חוק מור הינו שימוש במעבד מרובה ליבות – Multi core processor.

מעבד מרובה ליבות

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

עיבוד מקבילי

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

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

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

2

Scaling Up

משמעות מונח זה הינו להשתמש בליבות של המעבד כדי לאפשר Scale באותה מכונה – לצורך זה אנו נשתמש בתיכנות מקבילי כדי (ניצול יכולות המעבד המקבילי).

Scaling Out

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

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

מה בין קוד מקבילי Parallel code ולבין קוד משתף פעולה Concurrent?

קוד משתף פעולה Concurrent

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

היתרון במודל זה הינו פשטות הכתיבה שמתחילה בכתיבת שלד המבוסס קוד רציף (sequential) ורק אח”כ משלבת אלמנטים מקבילים.

הסכנה במודל זה היא נעילות – Thread starvation, dead lock, live lock המונעות התקדמות.

קוד מקבילי Parallel code

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

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

הסכנה במודל זה היא מורכבות הפיתוח, הבדיקות. בעולם ה-JAVA אין מודל פשוט ואחיד לתאר פתרון בעיות בשימוש בעיבוד מקבילי (אין בשפה מבנה כגון map reduce).

Java – קוד מקבילי עם J.U.C

שפת הפיתוח JAVA ועימה ה-JVM התפתחו רבות בתחום הקוד מקבילי ובאמצעות ספריית Java.util.concurrent מאפשרת שימוש במבנים הנועדו להקל על פיתוח מקבילי כגון:

Locks, Executors, Futures, CountDownLatch, CyclicBarrier, Semaphore

המבנים הללו הינם fine grained components המאפשרים לקודד משימות פרטניות במקביל אולם אינם מספקים כלים מלאים או תפיסה ארכיטקטונית חדשה לפיתוח קוד מקבילי שהוא גם Scalable UP וגם scalable out, אינם נותנים פתרון לניטור וטיפול אחיד בתקלות וגם לא לנושא זמינות גבוהה High availability.

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

בהמשך המאמר נסקור תשתית ששמה לה למטרה:

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

מודל השחקנים Actor Model

בשנת 1973 הגה Carl Hewitt מודל לוגי-מתמטי במדעי המחשב שנועד לתת אבסטרקציה לפיתוח קוד מקבילי. מקורות המודל נבעו ממודלים פיזיקלים בהם כל מבנה פיזיקלי החזיק את המצב הפיזי (physical state) שלו באופן פרטי והמבנים השונים ביצעו אינטראקציה באמצעות הודעות (פוטונים או אלקטרונים מייצגים אינטראקציה בין האטומים למשל).

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

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

AKKA Library

ספריית AKKA הינה ספריית קוד פתוח שנכתבה בשפת סקאלה בשנת 2009 ע”י המפתח Jonas Bonér. מטרת ספרייה זו הינה לספק ארגז כלים המאפשר פיתוח קוד scalable תוך שימוש באחד או יותר מכלי העבודה הבאים:

  • Composable futures
  • DataFlows
  • Agents
  • Actor

ACTORS model

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

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

יצירת שחקן על ידי המערכת:

final ActorSystem system = ActorSystem.create(“MySystem”);
final ActorRef myActor = system.actorOf(Props.create(MyUntypedActor.class),
“myactor”);

תיבת דואר

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

שליחת הודעה

שליחת הודעה ע”י שחקן אחד למשנהו איננה פעולה ממתינה – blocking ומרגע שהשחקן שולח את ההודעה הוא חופשי להמשיך לקבל הודעות אפילו טרם קבלת ההודעה ע”י השחקן הנמען.

הודעות אינם בנות שינוי

הודעה חייבת להיות בלתי ניתנת לשינוי – immutable – לקריאה בלבד.

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

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

פונקצית מענה של שחקן על הודעה:

import akka.actor.UntypedActor;
import akka.event.Logging;
import akka.event.LoggingAdapter;

public class MyUntypedActor extends UntypedActor {
LoggingAdapter log = Logging.getLogger(getContext().system(), this);

public void onReceive(Object message) throws Exception {
if (message instanceof String) {
log.info(“Received String message: {}”, message);
getSender().tell(message, getSelf());
} else
unhandled(message);
}
}

הפצת הודעות – dispatcher

הפצת הודעות מספקת את היכולת לנצל בצורה אופטימלית את משאבי המערכת – CPU cores.

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

  • Dispatcher: הפצה סטנדרטית מיועדת עבור הודעות א-סינכרוניות מהירות (Event driven messages delivery).
  • Pinned Dispatcher: הפצה בה Thread משוייך ישירות לשחקן – מיועדת לתהליכים נועליים (blocking actors).
  • Balancing dispatcher: הפצה המנסה לאזן עומסים בין שחקנים עמוסים וכאלו שאינם.

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

היררכיה בין שחקנים

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

השגחת הורים

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

אסטרטגית טיפול בתקלות צאצאים

private static SupervisorStrategy strategy =
new OneForOneStrategy(10, Duration.create(“1 minute”),

new Function<Throwable, Directive>() {
@Override
public Directive apply(Throwable t) {
if (t instanceof ArithmeticException) {
return resume();
} else if (t instanceof NullPointerException) {
return restart();
} else if (t instanceof IllegalArgumentException) {
return stop();
} else {
return escalate();
}
}
});

@Override
public SupervisorStrategy supervisorStrategy() {
return strategy;
}

ניטור שחקנים

שחקן אב המייצר שחקנים בנים אחראי לניטור תקלות, ניתן כמו כן להירשם לאירועי מוות – termination של שחקנים אחרים (שאינם בנים) כדי לנקוט פעולות שונות.

ניטור סיום חיי שחקן

public class WatchActor extends UntypedActor {

final ActorRef child = this.getContext().actorOf(Props.empty(), “child”);
{
this.getContext().watch(child);
}
ActorRef lastSender = getContext().system().deadLetters();

@Override
public void onReceive(Object message) {
if (message.equals(“kill”)) {
getContext().stop(child);
lastSender = getSender();
} else if (message instanceof Terminated) {

final Terminated t = (Terminated) message;
if (t.getActor() == child) {
lastSender.tell(“finished”, getSelf());
}
} else {
unhandled(message);
}
}
}

שקיפות מיקום ריצה

AKKA מאפשרת שקיפות מיקום יצירת השחקנים – הקוד העסקי המוגדר ע”י השחקן בלתי קשור למיקום הפיזי (JVM) בו יבצע השחקן את משימותיו. התובנה שבחירת המיקום הפיזי היא החלטת Deployment הופכת את השחקנים ב-AKKA ל-Scalable Outwards בצורה שקופה מאוד.

הפרדה הפיזית בין הקוד העסקי לבין הקוד המבצע חשובה ביותר – היא המאפשרת את שמירת הסטטוס הפנימי (internal state ) של כל שחקן לעצמו ואין לשחק אחר גישה (API) כלשהי. הדרך היחידה לפנות לשחקן היא ע”י הבקשות:

Tell – בקשה בלתי סינכרונית השולחת הודעה לשחקן וממשיכה מייד הלאה.
Ask – בקשה בלתי סינכרונית השולחת הודעה לשחקן וממתינה לכל היותר זמן מוגדר מראש בטרם ממשיכה הלאה.

ניתוב הודעות

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

RoundRobinRouter: נתב המאפשר שליחת הודעות לשחקנים בסדר מעגלי חוזר.
RandomRouter: נתב השולח את ההודעות באופן אקראי לשחקנים.
SmallestMailboxRouter: נתב השולח את ההודעות לשחקן עם המספר המועט ביותר של הודעות בתיבת הדואר.
BroadcastRouter: נתב השולח את ההודעות בשידור משותף לכל השחקנים.
ConsistentHashingRouter: נתב השולח את ההודעות לפי מפתח Hash אחיד לשחקן.

תוכנה המרפאת עצמה – Self-Heel

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

ארגז הכלים של AKKA מאפשר לבצע זאת במספר אמצעים פשוטים:

  • ניטור שחקנים
  • היררכית שחקנים
  • השגחה הורית

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

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

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

אימוץ שימוש ב- AKKA actors במוצר קיים

כדי להקל על אימוץ פלטפורמת השחקנים במוצר קיים יצרו עבורנו את השחקנים המסויימים (typed actors). הללו הם גשר בין העולם הפנימי הסטנדרטי של Object Oriented – מחלקות וממשקים שאינם שחקנים המדברים באמצעות הפעלות מתודות, ובין עולם השחקנים המורכב מהודעות ושחקנים בעלי סטטוס פנימי (inner state).

כדי לאמץ את הייתרונות הגלומים בשחקנים אך עם זאת לשמור על המודל הקיים, עלינו להמשיך לעבוד במודל של ממשק – Interface המגדיר את הפעולות ואת המימוש שלו Implementation. Typed Actor עוטף כל ממשק ב- java dynamic proxy וכל הפעלת פונקציה גורמת למעשה לשליחת הודעה אל השחקן המממש את הממשק הנ”ל Implementer.

API בלתי-סינכרוני

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

Void: פונקציה שאיננה מחזירה דבר (לא ממתינה(- בדומה לשחקן השולח הודעה אה-סינכרונית.
Future<?>: פונקציה לא ממתינה המחזירה Future ומאפשרת לבצע פעולות על ה-Future.
Option: פונקציה ממתינה לתשובה (פרק זמן קבוע מראש) ומחזירה אובייקט Option.
AnyOther<?> return value: פונקציה ממתינה לתשובה (פרק זמן קבוע מראש) ומחזירה אובייקט המוגדר בחתימת הפונקציה.

דוגמאות:

שחקנים מטיפוס מסויים – Typed Actors

ממשק סטנדרטי – Interface:

public interface Squarer {
void squareDontCare(int i); //fire-forget

Future square(int i); //non-blocking send-request-reply

Option squareNowPlease(int i);//blocking send-request-reply

int squareNow(int i); //blocking send-request-reply
}

מימוש הממשק – implementation:

class SquarerImpl implements Squarer {
private String name;

public SquarerImpl() {this.name = “default”;}

public SquarerImpl(String name) {this.name = name;}

//fire-forget
public void squareDontCare(int i) {int sq = i * i;}
//non-blocking send-request-reply
public Future square(int i) {return Futures.successful(i * i);}
//blocking send-request-reply
public Option squareNowPlease(int i) {return Option.some(i * i);}
//blocking send-request-reply
public int squareNow(int i) {return i * i;}
}

יצירת השחקן – הקשר היחיד עם ה-API של AKKA:

Squarer childSquarer =
TypedActor.get(TypedActor.context()).typedActorOf(
new TypedProps(Squarer.class, SquarerImpl.class)
);
//Use “childSquarer” as a Squarer

קרדיט תמונה: processor via Shutterstock.

היה מעניין? כותב הפוסט יעביר בקרוב הרצאה בנושא AKKA במסגרת אירוע ספטמבר של קבוצת המיטאפ של טיקל.

הכתבה בחסות Tikal

טיקל היא חברת תוכנה הפועלת מעל 12 שנים ומסייעת לחברות פורצות דרך להגיע למצוינות בתחומן. אנו מחויבים לשיפור מתמיד של השירותים שלנו ולמענה מקצועי ואיכותי ללקוחותינו.
המומחים של טיקל מנוסים בפיתוח של מגוון מערכות תוכנה, שימוש במגוון כלים וטכנולוגיות בתחומי ה- Java, Python, .NET, Javascript, RoR, Mobile, ALM. לטיקל 3 ערכי ליבה:
קוד הפתוח
גישת הקוד הפתוח שלנו מאפשרת ללקוחותינו פיתוח מהיר והתאמה אישית תוך ביטול התלות בספק אחד, טכנולוגיות מתקדמות ועדכניות, נגישות למידע, תמיכה שוטפת, הוצאות נמוכות.
חדשנות
אנו לא יועצים תאורטיים, אלא מומחים בעלי ידע מעשי וניסיון רב. אנו מנהלים שגרת העשרה ולמידה כדי להחדש ולעדכן את מאגר הידע שלנו. לומדים ומכירים מקרוב כל פיתוח חדש ונמצאים באופן תמידי בחזית המרוץ הטכנולוגי.
אחריות
העקרון המנחה את העבודה שלנו הוא איזון בין אחריות מקצועית וסגירת פערים טכנולוגיים, בין אחריות לתוצאות והשגת יעדים.

Avatar

Tikal

צוות המומחים של טיקל יעזרו לכל צוות פיתוח לסיים כל משימת פיתוח בזמן ובאפקטיביות גבוהה. החל מהתאמה ומעבר לטכנולוגיות חדשות, דרך תכנון ובניית פרוייקטים ואופטימיזציה בתחומי: JAVA, Javascript, Ruby, ALM & .NET

הגב

3 תגובות על "תוכנה דינמית: הדרך לכתוב code scalable ב-JVM"

avatar
Photo and Image Files
 
 
 
Audio and Video Files
 
 
 
Other File Types
 
 
 

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

סידור לפי:   חדש | ישן | הכי מדורגים
דור
Guest

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

אבי תשובה
Guest

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

אולף
Guest

מה אתם מסתלבטים!?! הרי ברור שבוצע כאן גוגל טרנסלייט רצחני! אני מתכנת בgroovy/scala/python עם ניסיון של 9 שנים ולא הצלחתי לקרוא כאן פסקה אחת שלמה ולהבין את חוט המחשבה. זה פשוט תרגום גרוע שאף אחד לא עבר עליו.

כל מי ש”התלהב” כאן בתגובות פשוט רצה להראות חכם

wpDiscuz

תגיות לכתבה: