ToCode
رفتن به کانال در Telegram
1 419
مشترکین
اطلاعاتی وجود ندارد24 ساعت
-17 روز
+230 روز
آرشیو پست ها
1 419
# צעדים ראשונים עם grpc
ספריית gRPC היא ספריית קוד פתוח מבית גוגל שתפקידה לחבר בין סרביסים במערכת שלכם בצורה יעילה ומהירה. אפשר לחשוב עליה בתור תחליף מהיר יותר ל REST APIs כאשר הרווח המרכזי של gRPC מבחינת ביצועים מגיע מהשימוש ב HTTP/2 ובשליטה טובה יותר של הספריה בפרוטוקול. מבחינת חווית משתמש הספריה מאפשרת כתיבת תוכניות מבוזרות כאילו אנחנו רצים על אותה מכונה באמצעות שימוש נרחב ב Stub-ים.
הספריה מגיעה עם תמיכה רשמית ב 11 שפות תכנות ואפשר למצוא חיבורים לעוד עשרות בחיפוש פשוט ברשת. בפוסט זה אציג שתי דוגמאות בשפת Python לשימוש ב gRPC בהתחלה כדי לכתוב שרת Echo ולאחר מכן להפוך אותו ל Chat.
## התקנה
בהנחה שיש לכם python3 מקונפג כמו שצריך אפשר להתקין הכל עם פקודת pip פשוטה:
$ pip install grpclib protobuf grpcio-tools
בכתיבת קוד gRPC אנחנו מתחילים בכתיבת קובץ פרוטוקול (עם סיומת proto) ואז משתמשים בשפה שבחרנו כדי ליצור קוד סטאב עבור הפרוטוקול לפרויקט שלנו. זה אומר שלכל קובץ פרוטוקול יהיו לנו שני קבצים מג'ונרטים אותם נטען מהקוד, וכל פעם שמשנים את הפרוטוקול צריך להפעיל מחדש את הפקודה שמייצרת את אותם שני קבצים.
קובץ פרוטוקול של gRPC מתאר את התקשורת בין השרת ללקוח - ובטרמינולוגיה הזאת הלקוח פונה לשרת ומבקש להריץ קוד מסוים, השרת מריץ את הקוד ושולח חזרה תשובה. פרוטוקול התקשורת מורכב מרשימת הפונקציות שהשרת יכול להפעיל ורשימת הפרמטרים של כל פונקציה.
## תוכנית ראשונה: שרת Echo
הנה לדוגמה תוכנית ראשונה תהיה תוכנית Echo, כלומר תהיה לנו פונקציה אחת על השרת שבסך הכל תשלח ללקוח חזרה בדיוק את מה שהוא שלח אליי. קובץ הפרוטוקול של התוכנית הוא:
syntax = "proto3";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string text = 1;
}
message HelloReply {
string message = 1;
}
בתרגום לעברית:
1. המילה service מגדירה אוסף פעולות ששרת יודע לעשות (אפשר לחשוב על זה כ Class בשרת).
2. המילה message מגדירה הודעה שפעולה יודעת לקבל או להחזיר.
3. בתוך message אפשר לכתוב מספר שדות שיהיו בהודעה. לכל שדה יש מספר וסוג.
בדוגמה שלנו המחלקה על השרת נקראת Greeter והיא חושפת פונקציה אחת בשם SayHello. פונקציה זו מקבלת הודעה מסוג HelloRequest ומחזירה הודעה מסוג HelloReply.
שתי ההודעות מוגדרות בהמשך הקובץ, לשתיהן שדה אחד מסוג טקסט אבל עם שם שונה.
מיטיבי הלכת בין הקוראים מוזמנים למצוא את ההוראות המלאות לכתיבת קבצי proto בתיעוד שלהם בקישור: https://developers.google.com/protocol-buffers/docs/proto3.
אחרי שכתבנו את קובץ ה proto אנחנו צריכים לייצר ממנו את קבצי הסטאב. בפייתון אני רושם משורת הפקודה:
$ python -m grpc_tools.protoc -I. --python_out=. --grpclib_python_out=. sayhello.proto
זה ייצור את הקבצים sayhello_grpc.py ו sayhello_pb2.py, שניהם לא נועדו לבני אדם. אנחנו פשוט נשלב אותם בתוכנית ה Python שלנו כדי לחבר בין השרת ללקוח.
בשביל לבנות קוד צד שרת gRPC אני צריך להגדיר מחלקה שתממש את הפונקציות שהגדרתי בפרוטוקול. אני משתמש בבסיס שנוצר עבורי בקובץ sayhello_grpc.py ומרחיב אותו. מחלקת צד שרת נראית כך:
class Greeter(GreeterBase):
async def SayHello(self, stream):
request = await stream.recv_message()
message = f'You Said: "{request.text}"'
await stream.send_message(HelloReply(message=message))
הפונקציה SayHello מתאימה לפונקציה שהגדרתי בקובץ הפרוטוקול. הפרמטר stream הוא שמאפשר לי לבצע פעולות הקשורות לפרוטוקול ובמקרה שלנו stream.recv_message() מקבלת את הבקשה מהלקוח ו stream.send_message שולחת את התשובה. זה מבנה מעניין כי בדרך כלל אנחנו חושבים על פונקציה בתור משהו ש"מחזיר" ערך, ופה בגלל שמדובר בפעולה מבוזרת החזרת הערך היא למעשה שליחת הודעה חזרה עם הערך החוזר, במקום פקודת return.
קוד צד השרת המלא עם הקוד שמפעיל את המחלקה נראה כך:
# File: server.py
import asyncio
from grpclib.utils import graceful_exit
from grpclib.server import Server
# generated by protoc
from sayhello_pb2 import HelloReply
from sayhello_grpc import GreeterBase
class Greeter(GreeterBase):
async def SayHello(self, stream):
request = await stream.recv_message()
message = f'You Said: "{request.text}"'1 419
# זהירות - קוד עובד לפניך!
המדד הכי גרוע לאיכות קוד הוא "עובד/לא עובד" ואני תמיד אעדיף קוד נכון שלא עובד על פני קוד עובד ולא נכון. ועם זה הלכתי לקשקש קצת עם JavaScript היום ובפרט עם testing-library. שימו לב לבדיקה הבאה שעוברת:
it('Increases after I click the button', async () => {
const screen = render(Counter);
const button = screen.getByRole('button', { name: 'Click Here' });
await userEvent.click(button);
expect(screen.getByText(/clicked 1 times/)).toBeTruthy();
});
זה קוד בדיקה למונה לחיצות שמוודא שאחרי שלחצתי על כפתור יופיע הטקסט clicked 1 times.
חדי העין שבין הקוראים יכולים לראות את הבעיה בשורה הרביעית של הקוד:
await userEvent.click(button);
זה רעיון יפה רק ש userEvent.click לא מחזיר Promise. קוד הבדיקה הבא (שגם עובד) הוא שקול לתוכנית הראשונה:
it('Increases after I click the button', async () => {
const screen = render(Counter);
const button = screen.getByRole('button', { name: 'Click Here' });
userEvent.click(button);
await undefined;
expect(screen.getByText(/clicked 1 times/)).toBeTruthy();
});
אה וצריך לציין - אם מוחקים את שורת ה await ונשארים רק עם זה:
it('Increases after I click the button', async () => {
const screen = render(Counter);
const button = screen.getByRole('button', { name: 'Click Here' });
userEvent.click(button);
expect(screen.getByText(/clicked 1 times/)).toBeTruthy();
});
הבדיקה נכשלת.
למעשה הקריאה ל await undefined, בדיוק כמו גירסה מבוססת הבטחות של setTimeout(0) היא דרך לפצל את הפונקציה לשני חלקים ולתת למנוע "לנשום" בין שניהם כדי שהוא יוכל לסיים פעולות אסינכרוניות אחרות. וזה רק עניין של מזל ושל מימוש מנוע JavaScript ספציפי בגללו הקוד הזה עבד.
מה צריך במקום? להבין שהבעיה היא לא ב click אלא בזמן ש Vue צריך אחרי ה click כדי לעדכן את הממשק, ולהוסיף את ההמתנה בשורה הבאה שמחפשת את האלמנט. קוד ברור ונכון יותר לאותה בדיקה יראה כך:
it('Increases after I click the button', async () => {
const screen = render(Counter);
const button = screen.getByRole('button', { name: 'Click Here' });
userEvent.click(button);
expect(screen.findByText(/clicked 1 times/)).toBeTruthy();
});1 419
# עושה לי צרבת
אף פעם לא הצלחתי להבין למה טכנולוגיות מסוימות נראו לי מושלמות מהשניה הראשונה איתן (אליקסיר, וים, פרל), ואחרות (גיט, פייתון, קוברנטס) עשו לי חשק להתחבא מאחורי איזה סלע עד שהטרנד יעבור והן יעלמו מהעולם. אבל זה המצב ואני בטוח שגם אתם מכירים את שתי התחושות.
עם התחושה הראשונה אין בעיה - את התיעוד מהאתר של אליקסיר קראתי תוך כדי נסיעה באוטובוס. יום אחד פשוט נתקלתי במשהו על אליקסיר עוד כשחיכיתי בתחנה וכשהגיע האוטובוס זה כבר היה מאוחר מדי להפסיק. אני חושב שהעברתי את רוב אותו הלילה בכתיבת התוכניות הראשונות.
התחושה השניה זה סיפור אחר - כשאין קליק לטכנולוגיה לוקח הרבה יותר זמן בכלל למצוא את הזמן כדי ללמוד אותה, וגם הלימוד הרבה פחות אפקטיבי. קצב ההתקדמות בטכנולוגיה כזאת יכול להיות איטי פי 2 או 3, ורק בגלל איזה רושם ראשוני או State Of Mind לא נכון.
וברגע שרואים את זה הפיתרון פשוט: במקום להכריח את עצמך ללמוד טכנולוגיה שעושה לך צרבת, עדיף למצוא את הדרך להתאהב בטכנולוגיה (גם אם זה לוקח זמן), לקרוא עליה יותר, לשמוע הרצאות מכנסים, להבין מה אנשים אחרים מוצאים בזה שאולי גם את תאהבי; אפילו השקעה של כמה חודשים רק בשבירת המחסום הפסיכולוגי תחזיר את עצמה מהר מאוד בזמן לימוד קצר ואפקטיבי בהרבה.
1 419
# חשבתי שאצליח להסתדר בלי
חשבתי שאצליח להסתדר בלי לדעת עד הסוף איך z-index עובד. כל פעם שנתקעתי בבעיה איתו התיעוד היה נראה לי הרבה יותר מדי ארוך והיה לי יותר קל לחפש פיתרון ספציפי ב Stack Overflow לבעיה שלי.
חשבתי שאצליח להסתדר בלי להבין עד הסוף SQL. ממילא אצלנו בצוות עובדים עם ORM והקוד שאני כותב מתורגם אוטומטית לשאילתות.
חשבתי שאצליח להסתדר בלי להבין עד הסוף את מערכת ההרשאות של AWS. תמיד אפשר להוסיף עוד הרשאות עד שדברים מתחילים לעבוד.
אז חשבתי.
אבל להצליח להסתדר זה לא להצליח לכתוב מערכת: את קוד ה CSS המסורבל שכתבתי אף אחד כבר לא יצליח להציל. בגלל הביצועים הגרועים של השאילתות שלי צריך לשכתב כמעט את כל השרת מחדש, ומערכת ההרשאות? אולי אם הייתי מבין אותה עד הסוף המערכת שלי היתה נפרצת פחות בקלות.
המטרה שלנו בכתיבת קוד היא לא להכניס כמה שיותר פיצ'רים כמה שיותר מהר; היא לבנות פיצ'רים בצורה חכמה וטובה כדי שהמערכת תשרוד. אי אפשר לעשות את זה כשמדלגים מעל הבסיס.
1 419
const data = { type: 'PaymentReceived', payload: req.body };
channel.publish('', 'payments-received', Buffer.from(JSON.stringify(data), 'utf8'));
res.sendStatus(201);
} catch (err) {
console.log(err);
res.sendStatus(500);
}
});
לא חייבים לבדוק את ערך ההחזר של publish כי גם אם הוא false ההודעה עדיין נשלחה לתור. כן צריך לטפל ב Exception שאומר שהערוץ או החיבור נסגר.
## קבלת הודעות
בצד המקבל עדיין כדאי להשתמש במחלקה RabbitMQ שעוטפת את ה connection ומתחברת מחדש, אבל עם טוויסט: הפעם אנחנו צריכים להפעיל מחדש את הפונקציה consume כל פעם שמתחברים מחדש ל RabbitMQ כדי להאזין מחדש להודעות. בשביל זה אני מוסיף למחלקה שדה מידע נוסף שיחזיק מערך של תורים ופונקציות טיפול בהודעות בכל תור, ומפעיל את consume כל פעם אחרי שהתחברתי מחדש. הקוד נראה כך:
class RabbitMQ {
constructor(url) {
this.url = url;
this.messageHandlers = [];
}
async init() {
while (true) {
try {
this.connection = await amqp.connect(this.url);
this.connection.on('close', () => {
console.log('Connection closed, reconnecting in 1 second');
this.init();
});
break;
} catch (err) {
console.log('Connection failed, retrying in 1 second');
console.log(err);
await sleep(1000);
}
}
const channel = await this.connection.createChannel();
this.messageHandlers.forEach(({ queue, callback }) => {
channel.consume(queue, callback.bind(null, channel));
});
return this.connection;
}
addMessageHandler(queue, callback) {
this.messageHandlers.push({ queue, callback });
}
}
בשביל להשתמש בו אני יכול להפעיל אותו מתוך פונקציה כזאת:
exports.start = async function start() {
const rabbit = new RabbitMQ(RABBITMQ_URL);
rabbit.addMessageHandler('payments-received', (channel, msg) => {
console.log(`Received message: "${msg.content.toString()}"`);
channel.ack(msg);
});
await rabbit.init();
}
והתוצאה היא קוד שרת שמאזין להודעות וכל פעם שמקבל הודעה דרך RabbitMQ הוא ידפיס את התוכן שלה למסך, ויותר חשוב - אם RabbitMQ התנתק או לא זמין זמנית אז השרת ימשיך לנסות להתחבר מחדש עד ש Rabbit יעלה שוב.
## נ.ב. למה לא להפיל את כולם בדומינו?
גישה חלופית נפוצה בעבודה עם קונטיינרים היא "שיטת הדומינו" (אני חושב שהרגע המצאתי את השם הזה, אז אל תשתמשו בו בשיחות עם אנשים מבחוץ), הרעיון בגדול זה שאני ממילא רץ בקונטיינר שרץ בתוך איזה מנהל קונטיינרים, אז כשיש בעיה אני יכול לשבור את הקונטיינר שלי ומנהל הקונטיינרים כבר יפעיל קונטיינר חדש עם הקוד. בגישה כזאת אני משתמש בסקריפט כמו wait-for-it בעליה בשביל לוודא ש Rabbit זמין וככה אני יודע שהקונטיינר שלי יעלה שוב רק כש Rabbit גם עולה שוב. אני קורא לה שיטת הדומינו כי היא אומרת שכשסרביס נופל אז נפיל איתו את כל הסרביסים שקשורים אליו ועוד מעט מנהל הקונטיינרים שלנו ירים את הכל מחדש והכל יסתדר.
אני לא אוהב את הגישה הזאת משתי סיבות:
1. מבחינת משאבים לסגור ולפתוח קונטיינרים כל היום זה בזבזני, ובמיוחד כשלא חייבים.
2. מבחינת המערכת, אם אני סוגר את כל הקונטיינר של השרת עד ש Rabbit יהיה זמין, אולי אני מפסיד נתיבים שהייתי יכול כן לטפל בהם שלא צריכים כתיבה ל Rabbit, אבל עכשיו יחזירו שגיאה כי הורדתי את הקונטיינר.
מחשבה על הבעיות שיקרו וטיפול בהן בקוד עוזרים לנו לכתוב סרביסים שיהיו חסינים יותר לבעיות וכך יתרמו ליציבות הכללית של המערכת.1 419
# איך שולחים ומקבלים הודעות בתור RabbitMQ מתוך שרת Node.JS Express
כשאני כותב קוד מדי פעם אני נתקע על שטות שאני חושב שצריכה להיות מאוד ברורה. השטות של היום היתה RabbitMQ ובפרט איך לחבר אותו ל Express. הנה הקוד שבסוף עבד לי בצירוף כמה אילוצים ששווה לשים לב אליהם כשאתם מקודדים דברים דומים.
## על מה חשוב להסתכל
מצאתי המון מדריכים ברשת שמראים איך להתחבר משרת Express ל RabbitMQ אבל אף אחד מהם לא התאים בדיוק, ולכן לקח לי יחסית הרבה זמן לבנות את האינטגרציה. אלה הנקודות המרכזיות שחיפשתי בפיתרון:
1. פתיחת Connection ל RabbitMQ היא יקרה. אנחנו רוצים לעשות את זה גג פעם אחת ביישום.
2. פתיחת Channel היא לא יותר מדי יקרה, ו RabbitMQ ממש אוהב לסגור לנו Channel-ים באמצע החיים, לפעמים סתם בשביל להרגיז. ה Channel מייצג רצף תקשורת מסוים והגישה של RabbitMQ היא שכל הודעה לא במקום והוא סוגר את ה Channel. לכן חשוב מאוד לא להשתמש באותו Channel בכמה Thread-ים, או במקרה של Express לא משתפים Connection בין כמה בקשות.
3. גם אם publish מחזיר false הוא עדיין שלח את ההודעה. אבל אם publish זרק Exception כן כדאי לדווח על זה הלאה כי זה אומר שה Connection סגור.
4. והכי חשוב הוא שגם RabbitMQ יכול להיות למטה מכל מיני סיבות. כשזה קורה אנחנו צריכים להחזיר שגיאה למשתמשים שמנסים לבצע פעולה בנתיב שמשדר לתור, אבל עדיין להשאיר את השרת זמין לטיפול בנתיבים אחרים וכל הזמן לנסות להתחבר מחדש כדי שנוכל לחזור לעבוד כש Rabbit יעלה חזרה.
עם ארבעת הנקודות מול העיניים אפשר להמשיך לכתוב את הקוד.
## קוד שמטפל בחיבור ל Rabbit
ספריית amqplib של Node.JS לא מטפלת בחיבור מחדש אחרי ניתוק אז אנחנו צריכים לסגור בשבילם את הפינה. אפשר לבנות מחלקה שעוטפת Connection שתדאג לזה בצורה, כך שה Connection יישמר בתור שדה מידע במחלקה וכל פעם שהוא ייסגר אוטומטית ננסה להתחבר מחדש. הנה הקוד:
class RabbitMQ {
constructor(url) {
this.url = url;
}
async init() {
while (true) {
try {
this.connection = await amqp.connect(this.url);
this.connection.on('close', () => {
console.log('Connection closed, reconnecting in 1 second');
this.init();
});
break;
} catch (err) {
console.log('Connection failed, retrying in 1 second');
await sleep(1000);
}
}
return this.connection;
}
}
function sleep(ms) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, ms);
});
}
במצב ששרת RabbitMQ זמנית לא זמין אוביקט ה connection ייסגר ואז כל הפעולות שננסה לעשות עליו ייכשלו. אבל, בזכות ה Event Handler שהתקנתי בצורה אוטומטית ננסה להתחבר מחדש ואחרי שיהיה שוב חיבור נמשיך לעבוד עם ה Connection החדש.
## קוד שרת ששולח הודעות
אז איך עובדים עם המחלקה RabbitMQ אתם שואלים? המשחק הוא פשוט - בעליה של השרת אני רוצה להפעיל את init ולחכות שיצליח, ואחרי זה בכל בקשה נכנסת אנסה לפתוח Channel עבור אותה בקשה. אם הניסיון מצליח אפשר יהיה להמשיך לטפל בבקשה ואם הוא נכשל (למשל כי Rabbit זמנית לא זמין) אז נחזיר שגיאה.
את הפעולה הזאת אני עושה ב Middleware שיופעל עבור כל הבקשות שצריכות את RabbitMQ. ה Middleware ואיתחול השרת נראים כך:
const rabbit = new RabbitMQ(RABBITMQ_URL);
async function setup(app) {
await rabbit.init();
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
console.log('setup');
app.use(async function(req, _res, next) {
try {
// Fetches the active connection or re-connect
const connection = rabbit.connection;
const channel = await connection.createChannel();
req.locals = req.locals || {};
req.locals.channel = channel;
return next();
} catch (err) {
console.log(err);
return next(err);
}
});
app.use('/', indexRouter);
}
setup(app);
צריך להגיד - בדוגמת הקוד כאן ה Middleware פועל על כל הנתיבים, אבל בגלל שזה Middleware מאוד קל להוסיף הגבלה שיפעל רק על נתיבים מסוימים או על Router מסוים.
בתוך קוד הטיפול בנתיב אני יכול למשוך את ה Channel ולפרסם אליו הודעות באופן הבא:
router.all('/ipn', function(req, res) {
try {
const { channel } = req.locals;1 419
# שלושה דברים שאהבתי בקוברנטס (ואחד שממש לא)
בתיאוריה, תרגום פרויקט מ Docker Compose ל Kubernetes הוא הדבר הכי קל בעולם: מפעילים סקריפט אחד שמתרגם את קבצי ה docker-compose.yml לקבצי ההגדרות של k8s, מעלים את התוצאה לקלאסטר קיים ונהנים מהחיים. המציאות לקחה לי קצת יותר עבודה ודרשה מספר שינויים בקוד המערכת - אבל בצד החיובי סיימתי עם קוד טוב יותר מזה שהיה בהתחלה וגם הבנה טובה יותר של הארכיטקטורה. מהתהליך גם למדתי לחבב מספר דברים בקוברנטס, והנה השלושה המרכזיים:
## ה Dashboard
ל Kubernetes יש לוח בקרה מובנה שמאפשר במכה אחת לראות את כל המכונות, את כל התוכניות שרצות על כל מכונה וגם את הלוגים, חיבורי הרשת ואפילו מספק אפשרות להתחבר ולקבל Shell על כל מכונה. למרות שאני מאוד אוהב את העבודה משורת הפקודה, גיליתי שלוח בקרה שבנוי טוב יכול להראות בעיות שבשורת הפקודה יהיה קשה למצוא, ושיש משהו מעודד בתמונה של העיגולים הירוקים.
לוח הבקרה של k8s מתעדכן בזמן אמת על כל שינוי בקלאסטר, ויש משהו קסום בלהחזיק Terminal על חצי מסך וחלון דפדפן שמראה את לוח הבקרה על החצי השני ולראות איך פקודות kubectl שאני מפעיל במסוף מעדכנות בזמן אמת את התצוגה בדפדפן.
## התיעוד
אין כל כך Tutorials טובים על קוברנטס אבל התיעוד עצמו מעולה וכל עוד אתם מוכנים להשקיע את הזמן בקריאה שלו (זה ארוך כי k8s עצמו סופר מורכב), אז בסוף תמצאו את כל מה שאתם צריכים והרבה יותר.
אם אתם מתחילים לעבוד עם קוברנטס תעשו לעצמכם טובה וקחו כמה ימים רק לקרוא את התיעוד מקצה לקצה ובמיוחד להבין את המושגים. זה יחסוך לכם שעות של חיפוש פיתרונות באינטרנט לשאלות לא הגיוניות. שווה להתחיל כאן:
https://kubernetes.io/docs/concepts/.
## עדכונים חיים והודעות שגיאה
אחרי יותר מדי שעות עם docker compose אני כבר די מכיר את כל השטויות שלו: אני יודע מתי הוא רק עושה כאילו הוא בונה אימג' אבל בעצם לוקח מידע ישן ולא רלוונטי מה Cache ולמה הוא מתכוון כשהוא כותב לי הודעות שגיאה קריפטיות.
מרענן ונחמד היה לראות את קוברנטס שלוקח גישה הפוכה - שם אם לא כתבתי משהו טוב הוא יצעק, ולפחות מהמשחקים עד עכשיו היה לי תמיד מאוד ברור מה אומרת השגיאה ואיך לתקן. ומה שבאמת מלהיב הוא העדכון החי וזיהוי השינויים כך שאני יכול פשוט להפעיל
kubectl apply -f . אחרי שאני מעדכן קונפיגורציות והקלאסטר פשוט ידע מה לעשות.
## ואחד שממש לא
אז מה יש לא לאהוב? האבסטרקציות המוגזמות כמובן. כמה עשרות שורות של קובץ docker-compose.yml הפכו בלי לשים לב לכמעט אלף שורות של קבצי הגדרות של קוברנטס. חלק מזה קורה כי קוברנטס נותן לי יותר שליטה על הקלאסטר והמכונות, אבל הרבה זה פשוט בניה מוזרה של קבצי ההגדרות והתעקשות על שטויות. הנה דוגמה קטנה מאחת ההגדרות:
backend:
service:
name: test
port:
number: 80
אני בטוח שהיתה להם סיבה טובה להוסיף את המפתח number בתוך port, אבל מוכן להמר שאפילו הם כבר שכחו אותה.
(או בגיזרת האבסטרקציות - הקונספט של Ingress Controller שאומר שאני צריך לכתוב הגדרות NGINX ב DSL שלהם שהופך לקובץ הגדרות של nginx, רק בשביל שאפשר יהיה להשתמש בעתיד בכלי אחר במקום nginx הוא לא הגיוני, כי בסוף גם בתוך ה DSL ההגדרות הן מאוד ספציפיות ל nginx).
אם אתם רוצים ללמוד קצת על קוברנטס ולא בטוחים איפה להתחיל אני ממליץ להתקין minikube אצלכם על המכונה לפי המדריך כאן:
https://kubernetes.io/docs/tutorials/hello-minikube/
ואחרי זה להשתמש ב kompose כדי להפוך פרויקט Docker Compose לפרויקט k8s ולנסות להעלות את הפרויקט על הקלאסטר שיצרתם. בדרך שווה להשקיע כמה ימים בקריאת התיעוד.1 419
# טיפים לשיחות מכירה טובות יותר
שיחת מכירה היא השיחה האחרונה המסוכנת לפני שלקוח סוגר אתך עיסקה. היא הרגע שבו הלקוח מחליט "לוקח" או "לא לוקח", ושיחות מכירה הן מהדברים שפרילאנסרים הכי נלחצים מהם. אבל שיחות מכירה לא צריכות להיות מלחיצות, לפחות לא אם מתיחסים אליהן בצורה נכונה ומבינים את הרצף שלהן בתוך תהליך המכירה. שיחות המכירה הטובות שהשתתפתי בהן היו נעימות לשני הצדדים - ולמעשה במיוחד ללקוח, שקיבל מידע מועיל ושמח להתקדם (כולל במקרים שאני הייתי הלקוח). הנה כמה טיפים שיעזרו לנו למכור ולקנות טוב יותר:
1. שיחת מכירה מתחילה כשלקוח מגיע אליכם עם בעיה ומחפש אליה פיתרון. זה לא פירסומת בפייסבוק שלקוח במקרה לחץ ו-הופ אתם בשיחה, זו סיטואציה הרבה יותר ממוקדת. לכן הדבר הראשון שאנחנו צריכים לעשות זה להבין מה הבעיה של הלקוח.
2. אם הבנתי את הבעיה אני יכול להחליט האם אני רוצה/יכול לפתור אותה. אם לקוח הגיע אליי כדי שאני אפתור לו בעיית ביצועים ב React זה בסדר להחליט שאני רוצה לפתור לו את הבעיה או לא רוצה. אם אני לא רוצה או לא יכול לפתור לו את הבעיה רצוי לסיים בנימוס את השיחה. שום דבר מלבד פיתרון הבעיה של הלקוח לא מעניין כרגע.
3. אם הבנתי את הבעיה ואני רוצה לפתור אותה אני יכול להתקדם ולדבר על הפיתרון, ופה הרגע שאנחנו מתחילים ליהנות מהשיחה. אם לקוח הגיע אליי עם בעיית ביצועים בריאקט ואני רוצה לעזור לו בפיתרון אני אבדוק איתו מה הוא ניסה בינתיים, באיזה עוד ספריות הוא משתמש, אתן לו טיפים מה אני הייתי עושה במצב הזה ומתאר ללקוח מפת דרכים מדויקת של איך הבעיה שלו הולכת להיפתר. כמה שתתנו פה יותר מידע ותענו על יותר שאלות ככה שני הצדדים ירוויחו: אתם מגדילים את הסיכויים שהלקוח יקנה מכם בסוף, והלקוח מרוויח מידע יקר ערך המבוסס על הניסיון והידע הייחודיים שלכם.
4. לפעמים אחרי שהעברתי ללקוח את כל מפת הדרכים לפיתרון אותו לקוח עדיין לא יהיה מספיק בטוח שאני מסוגל לפתור לו את הבעיה. במצב כזה כדאי להוסיף 2-3 סיפורי מקרה על בעיות דומות שפתרתם ולהציע ללקוח לדבר עם האנשים הרלוונטיים איתם עבדתם. בסיפור על בעיית הביצועים בריאקט ממקודם, אחרי שאני מתאר ללקוח רשימה של כלים וטכניקות שאני הייתי משתמש בהם כדי לזהות ולפתור את הבעיה אני יכול להציע לו לדבר עם ראשי צוותים או מנהלי קבוצות בחברות אחרות שעבדתי איתן ובהן פתרתי בעיות ביצועים דומות.
הסוד לשיחת מכירה טובה הוא בסך הכל מיקוד: לדבר על הבעיה, לתאר ללקוח איך יראה הפיתרון ולהציע ללקוח דרך ללמוד יותר על היכולת שלכם לספק את אותו פיתרון. אז מה יכול להשתבש?
1. אם אתם לא מזהים נכון את הבעיה של הלקוח, אתם הולכים לתאר לו פיתרון לבעיה שלא מעניינת אותו והשיחה תהיה מאוד חד-צדדית.
2. אם מפת הדרכים שאתם מתארים לפיתרון לא נראית ללקוח הגיונית אז הוא יתקוף אתכם בשאלות והשיחה תהיה מאוד לא נעימה. מצב כזה נובע מהתנגשות בין הדרך שאתם רגילים לפתור בעיות לבין איך שהלקוח רוצה לפתור את הבעיה, ואני במצב כזה מסיים את השיחה ומחכה ללקוחות שיותר מתאימים לתפיסת העולם שלי.
1 419
# איך לבדוק פעולות הקשורות לזמן עם Jest ו JavaScript
סט בדיקות טוב יכול לחסוך לנו הרבה זמן בתחזוקה ופיתוח של המערכת, כי כל פעם שאנחנו מגיעים להוסיף פיצ'ר חדש אנחנו יכולים לקבל הודעה מיידית כשקוד חדש שובר פיצ'רים ישנים שבנינו, וכך לתקן תוך כדי תנועה. בשביל פידבק ברמה הזאת סט הבדיקות צריך להיות מהיר וחסכוני במשאבים, כך שאוכל להריץ אותו על המחשב שלי תוך כדי פיתוח אפילו כל פעם שאני לוחץ Save ב IDE, ולקבל תשובה מהירה מה נשבר.
סוג אחד של פעולות שעלול לקחת יותר זמן בבדיקות הוא בדיקות שעונים: אם יש פעולה שצריכה לקרות "בעוד 30 שניות", למשל אם התחלתי פעולת תקשורת ובעוד 30 שניות היא צריכה להתבטל בגלל Timeout, אז לא הייתי רוצה שהבדיקה תצטרך לחכות 30 שניות עד לקבלת התשובה. זה זמן שהמחשב לא עושה בו כלום ואם אני כותב עשרות או מאות בדיקות אז המספרים האלה מצטברים.
ב Jest יש מנגנון חמוד שעוזר לנו לבדוק פעולות הקשורות בזמן שנקרא Fake Timers. זה עובד ככה - נניח שיש לכם את הקוד הבא למימוש של משחק זיכרון:
import _ from 'lodash';
export default class MemoryGame {
constructor(size) {
this.cards = _.shuffle(_.range(size).flatMap(i => [i, i]).map(i => ({ value: i, visible: false })));
this.foundPairIndexes = new Set();
this.activeCardIndex = -1;
this.hideTimer = null;
}
play = (index) => {
const card = this.cards[index];
if (card.visible) {
return;
}
card.visible = true;
clearTimeout(this.hideTimer);
this.hideTimer = null;
if (this.activeCardIndex >= 0) {
if (this.cards[this.activeCardIndex].value === card.value) {
// Found a match
this.foundPairIndexes.add(this.activeCardIndex);
this.foundPairIndexes.add(index);
this.activeCard = null;
} else {
this.hideTimer = setTimeout(this.hideAllCards.bind(this), 2000);
}
}
this.activeCardIndex = index;
}
isVisible(index) {
if (this.foundPairIndexes.has(index)) {
return true;
}
return this.cards[index].visible;
}
hideAllCards() {
this.cards.forEach(c => { c.visible = false; });
}
}
המשחק מגריל N קלפים לפי הגודל שביקשתם ומערבב אותם, והפונקציה play מטפלת בלוגיקה של המשחק. אם אין קלפים גלויים על השולחן היא הופכת קלף ואם יש כבר קלף גלוי אז היא בודקת אם הקלף שהפכנו עכשיו זהה למה שכבר מופיע, אם כן מציגה את שניהם ואם לא אז מדליקה Timer כדי להפוך את שני הקלפים חזרה בעוד שתי שניות.
אגב - במשחק יש כבר באג. נסו לקרוא את הקוד ולראות אם אתם מבינים מהו.
החלק שקשה לבדוק במשחק הוא השעון, ופה jest יכול מאוד לעזור לנו. הפונקציה jest.useFakeTimers() משתלטת על השעונים ומאפשרת לנו לזוז קדימה בזמן מהר יותר; הפונקציה jest.useRealTimers() מחזירה את המצב לקדמותו. אני אוהב להתחיל את בלוק הבדיקות שלי ב:
afterEach(function() {
jest.useRealTimers();
});
עכשיו מי שירצה יוכל להשתמש ב useFakeTimers ומי שלא לא, ובכל מקרה בסוף כל בדיקה המצב יחזור להתנהגות הרגילה.
אחרי שיש לנו Fake Timers אנחנו יכולים להשתמש בפונקציה advanceTimersByTime של jest כדי לרוץ קדימה בזמן. הנה הבדיקה שמוודאת ששתי שניות אחרי שהפכנו שני קלפים לא תואמים הם יתהפכו חזרה:
it('Turns back the cards when pair is not found', () => {
jest.useFakeTimers();
const g = new MemoryGame(5);
const i1 = 0;
const i2 = g.cards.findIndex(c => c.value !== g.cards[0].value);
g.play(i1);
g.play(i2);
expect(g.isVisible(0)).toBeTruthy();
expect(g.isVisible(i2)).toBeTruthy();
jest.advanceTimersByTime(2500);
expect(g.isVisible(0)).toBeFalsy();
expect(g.isVisible(i2)).toBeFalsy();
});
עכשיו אתם - מצאתם את הבאג? ספרו מה הוא היה בתגובות. לא מצאתם עדיין? נסו להריץ אצלכם את jest עם הקוד ותוכנית הבדיקה ונסו לבדוק כמה שיותר תרחישים עד שתמצאו אותו.1 419
# התחלות
המילה הראשונה שלמדתי ברוסית היתה mama; אחריה למדתי את motor ואז businessman. המשמעות של שלושתן זהה לאנגלית וכל מה שצריך בשביל ללמוד להגיד אותן הוא להוסיף את המבטא.
וכמו בשפות גם בטכנולוגיה ההתחלה היא הרבה פעמים החלק הכי קל: גם ילד (או מבוגר) שלא כתב קוד מעולם יצליח תוך מספר שעות לבנות תוכנית פשוטה בסקרץ', ואם אתם כבר יודעים לתכנת אז אני בטוח שתצליחו לכתוב את התוכנית הראשונה ב"לא-משנה-איזו-שפה" תוך מספר שעות. עם אתרים כמו replit.com וכלים כמו דוקר אפילו התיק של ההתקנות די מאחורינו.
ואפילו שאנחנו יודעים את כל זה אנחנו עדיין נתקעים לפני שמתחילים בגלל כל מיני פחדים - בגלל שאולי צריך לדעת יותר לפני שאפשר יהיה לקחת את הצעד הראשון (ואז הולכים לקרוא 5 ספרים על קוברנטס במקום פשוט להרים מערכת קטנה), בגלל שאולי אם אני אקח את הצעד אני אצטרך לאמץ את הטכנולוגיה החדשה והמפחידה ההיא ואז זה יסבך אותי בעתיד (כן גיט אני מסתכל עליך), בגלל שאולי אגלה שאימקס הוא לא כזה גרוע ואז מה אני אספר לכל החברים מהקבוצה של ה-וים.
בואו נפריד. הפחד שם, הוא לא ילך לשום מקום. ולידו גם היכולת לקחת צעד ראשון בטכנולוגיה אותה אתם רוצים ללמוד. הבחירה בפחד משאירה אותנו באותו מקום; הבחירה לקחת את הצעד הראשון פותחת את הדלת להכיר שפה חדשה ושולחת את הפחד לשמור שלא נתקרב לטכנולוגיה אחרת.
1 419
# טיפ באש: השתמשו ב null glob כשצריך
את הלולאה הקצרה הזאת כתבתי בסקריפט בלי לחשוב יותר מדי:
#!/bin/bash -e
for json_file in /data/*
do
echo "Importing JSON file $json_file"
python import_json.py "$json_file"
echo "-- Done"
done
ולמה בלי לחשוב? כי אם הסקריפט import_json.py ינסה לפתוח את הקובץ שהוא מקבל לקריאה, והתיקיה ריקה, אנחנו נקבל את השגיאה:
FileNotFoundError: [Errno 2] No such file or directory: '/data/*'
הסיבה לשגיאה המוזרה היא ש bash מפענח את הביטוי /data/* להיות הטקסט הפשוט /data/* ולא רשימת שמות הקבצים בתיקיה שם - בגלל שאין קבצים באותה תיקיה. אפשר לראות את זה בדוגמה פשוטה יותר בלי לולאה או פייתון אם פשוט נכתוב את שתי הפקודות:
# this will print all the files in /etc
$ echo /etc/*
# but this will just print "/data/*"
$ echo /data/*
הפקודה הראשונה באמת תדפיס את כל הקבצים והתיקיות מתוך /etc, והשניה תדפיס פשוט /data/*. כמובן שהמחרוזת שהודפסה מהפקודה השניה אינה באמת שם קובץ ולכן אי אפשר לפתוח אותה.
הדרך הכי קלה לצאת מזה היא להשתמש ב nullglob, שזה אומר שאם הביטוי לא מתאים לכלום אז באש יחזיר מחרוזת ריקה. נסו את זה:
$ shopt -s nullglob
$ echo /data/*
והפעם לא מקבלים שום פלט. בתוך הלולאה אני מקבל בדיוק את ההתנהגות הרצויה:
#!/bin/bash -e
shopt -s nullglob
for json_file in /data/*
do
echo "Importing JSON file $json_file"
python import_json.py "$json_file"
echo "-- Done"
done
הפעם הסקריפט לא ינסה לפתוח קובץ עם השם המוזר /data* ופשוט לא יבצע אף פעם את גוף הלולאה כי באמת אין קבצים שמתאימים לביטוי.
اکنون در دسترس! پژوهش تلگرام ۲۰۲۵ — مهمترین بینشهای سال 
