PostgreSQL High Availability: Streaming Replication, Patroni ו-Failover

מאת צוות מדיה דיל · 01.08.2026 · DevOps · 9 דק׳

ארכיטקטורת High Availability ל-PostgreSQL: streaming replication סינכרוני מול אסינכרוני, Patroni ו-etcd, ו-split-brain protection.

הבעיה עם PostgreSQL בפרודקשן היא שהוא, כמו כל מסד נתונים relational קלאסי, לא מגיע עם high availability מובנה מהקופסה — צריך לבנות אותה. מסד נתונים בודד הוא single point of failure: קריסת השרת, כשל דיסק, או אפילו restart מתוזמן להחלת עדכון גרסה משמעים downtime. הפתרון הוא ארכיטקטורת HA שמבוססת על replication ו-failover אוטומטי, אבל המימוש הנכון שלה דורש הבנה עמוקה של איך PostgreSQL replication עובד בפועל, לא רק הפעלת כלי מוכן. במאמר הזה נפרק את הארכיטקטורה: streaming replication, הכלים המובילים (Patroni), וההגנה מפני split-brain.

Streaming Replication: איך PostgreSQL בעצם משכפל נתונים

PostgreSQL מיישם replication דרך WAL (Write-Ahead Log) — כל שינוי בנתונים נכתב קודם ל-log רציף, ורק אחר כך מוחל בפועל על הטבלאות. Streaming replication שולח את ה-WAL הזה בזמן אמת מה-primary ל-standby servers, שמחילים אותו באופן רציף כדי לשמור עותק עדכני. זה שונה מהותית מ-logical replication (שמעביר שינויים ברמת שורה ומאפשר replication סלקטיבי בין גרסאות שונות) — streaming replication הוא byte-for-byte זהה ל-primary ומהיר בהרבה, אבל דורש שה-standby יריץ בדיוק את אותה גרסת PostgreSQL.

Synchronous מול Asynchronous: הטרייד-אוף המרכזי

ברירת המחדל היא asynchronous replication — ה-primary מאשר טרנזקציה ללקוח מיד אחרי כתיבה מקומית, בלי להמתין שה-standby יקבל אותה. זה מהיר אך מסוכן: אם ה-primary קורס בדיוק אחרי אישור הטרנזקציה אך לפני שה-WAL הגיע ל-standby, הטרנזקציה אבודה לצמיתות למרות שהלקוח קיבל אישור הצלחה. Synchronous replication פותר את זה — ה-primary ממתין לאישור מה-standby (synchronous_commit) לפני שהוא מאשר ללקוח, מה שמבטיח אפס אובדן נתונים (zero RPO) במחיר latency נוסף על כל כתיבה, בגובה ה-round trip network ל-standby. הבחירה הנכונה תלויה בעלות אובדן נתונים בפועל: מערכת פיננסית לרוב דורשת synchronous על לפחות standby אחד, בעוד רוב האפליקציות מסתפקות ב-asynchronous עם risk window מדוד ומקובל.

אפשרות ביניים חשובה היא synchronous_commit = remote_write — ה-primary ממתין רק שה-WAL הגיע לזיכרון ה-standby (לא בהכרח נכתב לדיסק שם), מה שמצמצם latency לעומת synchronous מלא תוך עדיין הפחתה משמעותית של סיכון אובדן נתונים לעומת אסינכרוני מלא.

Patroni ו-etcd: אורקסטרציית Failover

PostgreSQL עצמו לא כולל מנגנון failover אוטומטי — זה תפקידו של כלי חיצוני. Patroni הוא הפתרון המוביל כיום: הוא רץ כ-agent ליד כל node (primary ו-standbys), מנטר את בריאותם, ומשתמש ב-Distributed Consensus Store (בדרך כלל etcd, אך גם Consul או ZooKeeper נתמכים) כדי להסכים באופן מבוזר מי ה-primary הנוכחי. כשה-primary נופל, ה-consensus store (שדורש רוב — quorum — של nodes כדי לקבל החלטה) בוחר standby מתאים (בדרך כלל זה עם ה-WAL העדכני ביותר) ומקדם אותו ל-primary תוך שניות, ומעדכן את שאר ה-nodes להתחבר אליו כ-standbys חדשים.

הסיבה שדורשים consensus store נפרד (etcd) ולא רק תקשורת ישירה בין nodes של PostgreSQL היא בדיוק מניעת split-brain — מצב שבו network partition גורם לשני nodes "לחשוב" בו-זמנית שהם ה-primary. etcd עצמו בנוי על פרוטוקול Raft שדורש רוב מוחלט (majority quorum) כדי לקבל החלטה — אם יש partition שמפצל את ה-cluster לשני חלקים, רק החלק שמכיל את הרוב יכול "להצביע" ולבחור primary, בעוד החלק המיעוט נכנס אוטומטית למצב read-only ולא מקבל כתיבות. זו ההגנה המבנית שמונעת כתיבות סותרות משני "primaries" בו-זמנית.

Load Balancer בפני ה-Cluster: HAProxy ו-PgBouncer

אחרי שה-failover קורה, האפליקציה צריכה לדעת להתחבר ל-primary החדש בלי restart ידני. הפתרון הנפוץ הוא HAProxy שיושב בפני ה-cluster ומנתב תעבורה לפי health check שמתעדכן ברציפות — Patroni חושף REST API שמדווח מי כרגע ה-primary, ו-HAProxy שואל אותו כל כמה שניות ומעדכן את היעד בהתאם. שילוב עם PgBouncer מוסיף שכבת connection pooling מעל זה. סדר השכבות הנכון הוא: אפליקציה → HAProxy → PgBouncer → PostgreSQL nodes, כאשר כל שכבה אחראית על היבט אחר — HAProxy על ניתוב לפי מי ה-primary, PgBouncer על ניהול חיבורים יעיל.

Cascading Replication ו-Read Scaling

בקלאסטרים גדולים עם הרבה standbys, כל אחד שמקבל WAL ישירות מה-primary מוסיף עומס רשת ו-I/O עליו. Cascading replication מאפשר ל-standby אחד לשמש כמקור WAL עבור standbys נוספים ("standby of a standby"), מה שמפזר את העומס ומאפשר להוסיף replicas לקריאה בהיקף גדול בלי להעמיס ישירות על ה-primary. זה שימושי במיוחד עבור multi-region — standby אחד באזור מרוחק מקבל WAL מה-primary, וממשיך להזין standbys נוספים באותו אזור, מה שחוסך bandwidth בין אזורים.

Quorum Standbys ו-Priority

הגדרה מתקדמת יותר מ-synchronous standby בודד היא synchronous_standby_names עם רשימת מועמדים ומספר quorum — למשל "אחד מתוך שלושה" (ANY 1 (node1, node2, node3)), מה שמאפשר לכל אחד משלושת ה-standbys לספק את אישור ה-synchronous, כל עוד לפחות אחד מהם זמין. זה נותן חוסן גבוה יותר מקונפיגורציה עם standby יחיד קבוע, כי נפילה של standby ספציפי לא חוסמת כתיבות — כל עוד אחד אחר מהרשימה עדיין מגיב. ארגונים עם דרישות RPO קשיחות יותר יכולים להעלות את מספר ה-quorum (למשל "שניים מתוך שלושה") כדי לוודא שהנתונים קיימים בכמה עותקים לפני אישור, במחיר latency נוסף.

Timeline History ו-WAL Archiving

כל פעם שמתבצע failover, PostgreSQL יוצר timeline חדש — מעין "ענף" בהיסטוריית ה-WAL שמסמן שינוי primary. זה קריטי להבנה כי standby ישן שניסה להתחבר מחדש אחרי failover צריך "לדעת" לעקוב אחרי ה-timeline החדש ולא להישאר תקוע מנסה לקבל WAL מ-timeline שכבר לא קיים. כלים כמו Patroni מטפלים בזה אוטומטית ברוב המקרים, אך בהגדרות ידניות יותר (streaming replication בלי אורקסטרטור) זו מלכודת נפוצה שגורמת ל-standby "לתקוע" בלי הודעת שגיאה ברורה. לצד זה, WAL archiving לאחסון חיצוני (S3 ודומיו) הוא הכרחי לא רק ל-point-in-time recovery אלא גם כדי לאפשר ל-standby חדש "לתפוס" (catch up) מרחק WAL גדול בלי להעמיס ישירות על ה-primary דרך streaming בלבד.

Trade-offs: כמה Nodes באמת צריך

קלאסטר עם node אחד בלבד הוא לא HA כלל. קלאסטר עם שני nodes (primary + standby יחיד) מספק הגנה בסיסית אך חושף בעיה: אם ה-standby נופל, אין עוד לreplicate אליו, ואם ה-primary נופל בו-זמנית, יש אובדן נתונים מלא. שלושה nodes (או יותר) הם המינימום המומלץ בפועל — הם גם מספקים quorum אמיתי ל-consensus store, וגם מבטיחים שתמיד יש standby זמין גם כשאחד מהם נופל לתחזוקה. המחיר הוא כמובן פי שלושה עלות compute לעומת instance בודד, מה שהופך HA אמיתי לא-זול, אך הכרחי עבור מערכות שבהן downtime עולה יותר מהעלות הנוספת.

Multi-Region: אתגר ה-Latency

כשמפזרים standbys בין אזורי ענן שונים כדי להגן מפני כשל אזור שלם, נכנס לתמונה גורם שלרוב מתעלמים ממנו: latency רשת בין אזורים יכול להיות עשרות מילישניות, מה שהופך synchronous replication בין אזורים ל-יקר מאוד מבחינת ביצועים — כל כתיבה ממתינה לאותו round trip הבין-אזורי. הפתרון הנפוץ הוא ארכיטקטורה היברידית: synchronous replication בתוך אותו אזור (latency נמוך, כמה מילישניות), ו-asynchronous replication לאזור מרוחק (עבור disaster recovery בלבד, לא לצורכי HA שוטף). זה נותן איזון בין ביצועים (רוב הזמן) לבין חוסן אמיתי מפני אסון אזורי (המקרה הנדיר אך הקריטי).

שיקול נוסף במעבר בין אזורים הוא ה-egress cost — תעבורת רשת בין אזורי ענן שונים (ולעיתים גם בין ספקי ענן) נחויבת בתעריפים משמעותיים בהיקפי נתונים גדולים. WAL streaming רציף לאזור מרוחק, גם אם asynchronous, עדיין מייצר תעבורה מתמדת שצריך לקחת בחשבון בתקציב התשתית, במיוחד עבור מסדי נתונים עם קצב כתיבה גבוה.

אימות ה-Failover: Chaos Testing

ההגדרה הטכנית הנכונה של HA לא מבטיחה שהיא תעבוד בפועל — רק תרגול אמיתי מוכיח זאת. גישת chaos testing מומלצת: הריגת ה-primary process באופן יזום (לא רק restart מסודר) ומדידת זמן ה-failover בפועל, ניתוק רשת מלאכותי בין nodes כדי לוודא שה-quorum מתנהג כצפוי, ובדיקה שהאפליקציה בפועל מתאוששת ומתחברת מחדש בלי מעורבות ידנית. תרגילים כאלה, שרצים באופן תקופתי (לא רק פעם אחת בזמן ההקמה), חושפים רגרסיות שנכנסות עם הזמן — שדרוג גרסת Patroni, שינוי הגדרות רשת, או תלות חדשה שאיש לא שקל.

שדרוגי גרסה בלי Downtime

שדרוג גרסת PostgreSQL (למשל מ-15 ל-16) הוא אתגר HA בפני עצמו. הגישה הנפוצה ביותר היא rolling upgrade על ה-standbys קודם: משדרגים standby אחד בכל פעם, מוודאים שהוא תקין ומצטרף מחדש ל-replication, ורק בסוף מבצעים switchover מבוקר — לא failover חירום אלא מעבר יזום ומתוזמן — כדי להפוך standby משודרג ל-primary החדש, ואז משדרגים את מה שהיה primary ישן. זה שונה מ-failover חירום בכך שהוא מתוכנן ומבוקר לחלוטין, אך עדיין דורש שהאפליקציה תתמודד בחן עם ניתוק קצר בזמן ה-switchover עצמו.

טעויות נפוצות בפרודקשן

הטעות הראשונה היא הרצת etcd cluster במספר nodes זוגי (כמו 2 או 4) — Raft consensus דורש רוב מוחלט, ומספר זוגי מגדיל את הסיכון לתיקו שלא ניתן לפתור אוטומטית; מספר אי-זוגי (3, 5) הוא הכלל. השנייה היא הזנחת בדיקת failover בפועל — ארגונים רבים מגדירים Patroni ולעולם לא מדמים כשל אמיתי, ומגלים רק בזמן תקרית אמיתית שההגדרה שגויה. השלישית היא synchronous replication עם standby יחיד — אם ה-standby היחיד נופל, כל כתיבה נחסמת (כי אין למי synchronous לאשר), מה שהופך את מנגנון ההגנה מפני אובדן נתונים לגורם downtime בעצמו; הפתרון הוא synchronous_standby_names עם quorum גמיש שמאפשר המשך כתיבה גם אם standby אחד נופל. הרביעית היא אי-ניטור replication slots — slot שנשכח (למשל אחרי הסרת standby בלי ניקוי ה-slot שלו) גורם ל-WAL להצטבר בלי הגבלה ב-primary עד למילוי הדיסק.

מתי HA מלא נדרש

עבור סביבות פיתוח, staging, או מערכות פנימיות עם downtime מקובל, single-node PostgreSQL עם backup תקין מספיק. HA מלא עם Patroni ו-etcd משתלם כשיש SLA אמיתי, תעבורת פרודקשן חיה, ועלות downtime משמעותית. שילוב עם ארכיטקטורת production database הרחבה יותר — connection pooling, monitoring, disaster recovery — הוא מה שהופך HA ברמת מסד הנתונים לפתרון מלא ולא רק חלק אחד בפאזל.

סיכום

PostgreSQL high availability נבנית על שלוש שכבות: streaming replication עם בחירה מודעת בין synchronous ל-asynchronous, אורקסטרציית failover דרך Patroni ו-etcd עם quorum אמיתי שמונע split-brain, ו-load balancer שמעדכן ניתוב באופן דינמי. הצוותים שמצליחים בזה הם אלה שבודקים failover בפועל לפני שהוא קורה בתקרית אמיתית, ולא מסתמכים על הגדרה תיאורטית שמעולם לא נבחנה תחת כשל אמיתי.

תגיות: PostgreSQL · High Availability · Patroni · etcd · Streaming Replication · Failover · Database Architecture

← חזרה לבלוג · צור קשר