ЁЯПл The SchoolтА║ЁЯЫая╕П SREтА║ЁЯУЯ рдзрдбрд╛ 04 тАФ On-call: рдбреНрдпреБрдЯреАрдЪрд╛ рдлреЛрди, рдЖрдгрд┐ рддреЛ рдЯрд┐рдХрд╛рдК рдареЗрд╡рдгреЗ
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯУЯ рдзрдбрд╛ 04 тАФ On-call: рдбреНрдпреБрдЯреАрдЪрд╛ рдлреЛрди, рдЖрдгрд┐ рддреЛ рдЯрд┐рдХрд╛рдК рдареЗрд╡рдгреЗ

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 04 ┬╖ рдорд╛рдЧреЗ: lesson-03-toil ┬╖ рдкреБрдвреЗ: lesson-05-canary-analysis


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбрд╛ 03, рдЕрдзрд┐рдХ рдбреНрдпреБрдЯреАрдЪрд╛ рдлреЛрди. Rotations, рдПрдХ shift рдХрд┐рддреА pages рд╕рд╣рди рдХрд░реВ рд╢рдХрддреЗ, рд░рд╛рддреНрд░реАрдЪреЗ pages, follow-the-sun, рдЖрдгрд┐ on-call рдиреЗ рдЖрдард╡рдбрд╛ рдЦрд╛рдКрди рдЯрд╛рдХрдгреНрдпрд╛рдЖрдзреА rotation рдордзреНрдпреЗ рдХрд┐рддреА рд▓реЛрдХ рд╣рд╡реЗрдд. Alert design (рдореБрд│рд╛рдд рдХрд╢рд╛рд╕рд╛рдареА page рдпрд╛рд╡рд╛) рд╣рд╛ Observability рдзрдбрд╛ 08 рдЪрд╛ рд╡рд┐рд╖рдп рдЖрд╣реЗ; рд╣рд╛ рдзрдбрд╛ рдЙрддреНрддрд░ рджреЗрдгрд╛рд▒реНрдпрд╛ рд▓реЛрдХрд╛рдВрдмрджреНрджрд▓ рдЖрд╣реЗ. sre/crew.py рдордзреАрд▓ page_times, shift_load, night_pages рдЖрдгрд┐ oncall_share, рдЖрдгрд┐ sre/demo.py рдордзреАрд▓ oncall().

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рдкрдердХрд╛рдХрдбреЗ рдПрдХ рдбреНрдпреБрдЯреАрдЪрд╛ рдлреЛрди рдЖрд╣реЗ. ЁЯУЯ рддреЛ рдЬреНрдпрд╛рдЪреНрдпрд╛рдХрдбреЗ рдЕрд╕реЗрд▓ рддрд┐рдиреЗ рдПрдЦрд╛рджреА рдЗрдорд╛рд░рдд рдмрд┐рдШрдбрд▓реА рдХреА рдЙрддреНрддрд░ рджреНрдпрд╛рдпрд▓рд╛рдЪ рд╣рд╡реЗ тАФ рджрд┐рд╡рд╕рд╛ рдХрд┐рдВрд╡рд╛ рд░рд╛рддреНрд░реА.

рд╕реБрд░реБрд╡рд╛рддреАрд▓рд╛ рдлрдХреНрдд рдХрддрд░рд┐рдирд╛, рджреАрдкрд┐рдХрд╛ рдЖрдгрд┐ рдРрд╢реНрд╡рд░реНрдпрд╛ рддреЛ рд╡рд╛рдЯреВрди рдШреЗрддрд╛рдд. рдкреНрд░рддреНрдпреЗрдХ рдЬрдг рддреЛ рдПрдХ рдЖрдард╡рдбрд╛ рдареЗрд╡рддреЗ, рдордЧ рдкреБрдвреЗ рджреЗрддреЗ. рдореНрд╣рдгрдЬреЗ рдкреНрд░рддреНрдпреЗрдХреАрдХрдбреЗ рддреЛ рддреАрди рдЖрдард╡рдбреНрдпрд╛рдВрддреВрди рдПрдХ рдЖрдард╡рдбрд╛ рдЕрд╕рддреЛ. рд╣реЗ рдЦреВрдк рдЖрд╣реЗ. рдлреЛрди рд╣рд╛рддрд╛рдд рдЕрд╕рддрд╛рдирд╛ рддреБрдореНрд╣реА рдореЛрдареЗ рдХрд╛рдо рд╕реБрд░реВ рдХрд░реВ рд╢рдХрдд рдирд╛рд╣реА, рдЖрдгрд┐ рдЭреЛрдкрд╣реА рдиреАрдЯ рд▓рд╛рдЧрдд рдирд╛рд╣реА.

рдЪрд╛рд░ рдЖрдард╡рдбреНрдпрд╛рдВрдд рдлреЛрди рд░рд╛рддреНрд░реА 17 рд╡реЗрд│рд╛ рд╡рд╛рдЬрд▓рд╛. рдердХрд▓реЗрд▓реА рдорд╛рдгрд╕реЗ рдЪреБрдХрд╛ рдХрд░рддрд╛рдд.

рдореНрд╣рдгреВрди рдкрдердХ рджреЛрди рдмрджрд▓ рдХрд░рддреЗ. рдкрд╣рд┐рд▓рд╛, rotation рдордзреНрдпреЗ рдЬрд╛рд╕реНрдд рд▓реЛрдХ рд╕рд╛рдореАрд▓ рд╣реЛрддрд╛рдд: рдЖрда рдЬрдг рдЕрд╕рд▓реЗ, рддрд░ рдкреНрд░рддреНрдпреЗрдХ рдЬрдг рдЖрда рдЖрдард╡рдбреНрдпрд╛рдВрддреВрди рдлрдХреНрдд рдПрдХ рдЖрдард╡рдбрд╛ рдлреЛрди рдареЗрд╡рддреЗ. рджреБрд╕рд░рд╛, рддреНрдпрд╛рдВрдирд╛ рдЬрдЧрд╛рдЪреНрдпрд╛ рджреБрд╕рд▒реНрдпрд╛ рдмрд╛рдЬреВрд▓рд╛ рдПрдХ sister crew рдорд┐рд│рддреЗ. рдкреБрдгреНрдпрд╛рдд рд░рд╛рддреНрд░ рдЕрд╕рддреЗ рддреЗрд╡реНрд╣рд╛ рддрд┐рдереЗ рджрд┐рд╡рд╕ рдЕрд╕рддреЛ. рдкреБрдгреЗ рдЖрдкрд▓реНрдпрд╛ рджрд┐рд╡рд╕рд╛ рдлреЛрди рдареЗрд╡рддреЗ, sister crew рдЖрдкрд▓реНрдпрд╛ рджрд┐рд╡рд╕рд╛. рдЖрддрд╛ рдХреЛрдгрд╛рдЪрд╛рд╣реА рдлреЛрди рд░рд╛рддреНрд░реА рд╡рд╛рдЬрдд рдирд╛рд╣реА.

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart LR
    pages["ЁЯУЯ 55 pages in 4 weeks<br/>0.98 per 12-hour shift"] --> one["one Pune crew, 24 h<br/>17 night pages"]
    pages --> fts["follow-the-sun<br/>Pune 07тАУ19 ┬╖ sister crew 19тАУ07<br/>0 night pages"]
    pages --> busy["4 of 56 shifts<br/>had more than 2 pages"]
    rot["rotation size"] --> r3["3 people: 33.3% of weeks тЬЧ"]
    rot --> r8["8 people: 12.5% of weeks тЬУ"]

ЁЯЧ║я╕П рдХрд╛рдврд▓реЗрд▓реА рдЖрд╡реГрддреНрддреА + рдПрдХ lab: https://school-edh.pages.dev/sre/lesson-diagrams.html#l04

тЭУ рдХрд╛рдп

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг on-call рдореНрд╣рдгрдЬреЗ рдЬрд┐рдереЗ reliability рдЖрдгрд┐ рдорд╛рдгрд╕реЗ рднреЗрдЯрддрд╛рдд. рдЦреВрдк рд▓рд╣рд╛рди rotation рд▓реЛрдХрд╛рдВрдирд╛ рдердХрд╡реВрди рдЯрд╛рдХрддреЗ, рдЖрдгрд┐ рдердХрд▓реЗрд▓реЗ рд▓реЛрдХ рдкреБрдврдЪреЗ outage рдЕрдзрд┐рдХ рд╡рд╛рдИрдЯ рдХрд░рддрд╛рдд. рд░рд╛рддреНрд░реАрдЪреЗ pages рдХрд┐рддреНрдпреЗрдХ рджрд┐рд╡рд╕ рдЭреЛрдк рдмрд┐рдШрдбрд╡рддрд╛рдд. рдПрдХрд╛ shift рдордзреНрдпреЗ рдЦреВрдк pages рдореНрд╣рдгрдЬреЗ рдХреЛрдгрддреНрдпрд╛рдЪ page рдХрдбреЗ рдЦрд░реЗ рд▓рдХреНрд╖ рдЬрд╛рдд рдирд╛рд╣реА, рдлрдХреНрдд рдПрдХ рдЭрдЯрдкрдЯ restart тАФ рдЖрдгрд┐ рдХрд╛рд░рдг рддрд╕реЗрдЪ рд░рд╛рд╣рддреЗ. рдЗрдерд▓реЗ рдЖрдХрдбреЗрдЪ рддреБрдореНрд╣рд╛рд▓рд╛ рдкреБрд░рд╛рд╡реНрдпрд╛рд╕рд╣ "рдЖрдкрд▓реНрдпрд╛рд▓рд╛ рдЖрдгрдЦреА рдЪрд╛рд░ рд▓реЛрдХ рд╣рд╡реЗрдд" рдХрд┐рдВрд╡рд╛ "рд╣реЗ alerts рджреБрд░реБрд╕реНрдд рд╡реНрд╣рд╛рдпрд▓рд╛рдЪ рд╣рд╡реЗрдд" рдЕрд╕реЗ рдореНрд╣рдгреВ рджреЗрддрд╛рдд.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

sre/crew.py рдордзреАрд▓ page_times(weeks=4, rate=0.07, seed=11) page рдЪреНрдпрд╛ рддрд╛рд╕рд╛рдВрдЪреА seed рджрд┐рд▓реЗрд▓реА рдпрд╛рджреА рдмрдирд╡рддреЗ (hour 0 = рдкреБрдгреНрдпрд╛рдд рд╕реЛрдорд╡рд╛рд░ 00:00; рдПрдХрд╛ рддрд╛рд╕рд╛рдд рдЬрд╛рд╕реНрддреАрдд рдЬрд╛рд╕реНрдд рдПрдХ page). shift_load(pages) рдкреНрд░рддреНрдпреЗрдХ 12 рддрд╛рд╕рд╛рдВрдЪреНрдпрд╛ shift рдордзреАрд▓ pages рдореЛрдЬрддреЗ (07:00тАУ19:00 рдЖрдгрд┐ 19:00тАУ07:00); night_pages(pages, offset_h) offset_h рддрд╛рд╕ рджреВрд░ рдЕрд╕рд▓реЗрд▓реНрдпрд╛ crew рд╕рд╛рдареА 22:00 рддреЗ 07:00 рдордзреАрд▓ pages рдореЛрдЬрддреЗ; oncall_share(people) рдореНрд╣рдгрдЬреЗ рдЖрдард╡рдбреНрдпрд╛рдЪреНрдпрд╛ rotation рдордзреНрдпреЗ рдкреНрд░рддреНрдпреЗрдХ рд╡реНрдпрдХреНрддреА рдХрд┐рддреА рдЯрдХреНрдХреЗ рдЖрдард╡рдбреЗ on call рдЕрд╕рддреЗ. oncall() рдордзреАрд▓ follow-the-sun рдЪреА рдУрд│ рдкреБрдгреНрдпрд╛рд▓рд╛ 07тАУ19 рдЪреЗ pages рджреЗрддреЗ рдЖрдгрд┐ sister crew рд▓рд╛ (12 рддрд╛рд╕ рджреВрд░) рдЙрд░рд▓реЗрд▓реЗ.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

python3 sre/demo.py oncall
python3 - <<'EOF'
import sys; sys.path.insert(0, "sre"); from crew import page_times, shift_load, night_pages
for rate in (0.03, 0.07, 0.15):
    pages = page_times(weeks=4, rate=rate)
    load = shift_load(pages)
    over = sum(1 for n in load.values() if n > 2)
    print(f"rate {rate:.2f}/h тЖТ {len(pages):>3} pages ┬╖ busiest shift {max(load.values())} ┬╖ shifts over 2: {over:>2} ┬╖ night pages (one site): {night_pages(pages):>2}")
EOF

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

oncall рд╣реЗ print рдХрд░рддреЗ:

тФАтФА 4 weeks of pages for the timetable service: 55 pages ┬╖ 0.98 per 12-hour shift on average
   busiest shift 3 pages ┬╖ shifts with more than 2 pages: 4 of 56
   one Pune crew, 24 h on call: 17 pages between 22:00 and 07:00
   follow-the-sun: Pune takes 07:00тАУ19:00, a sister crew 12 h away takes the rest in its own daytime тЖТ night pages 0 + 0
   weekly rotation of 3: each person is on call 33.3% of weeks  тЖР over 25%: no time left for engineering
   weekly rotation of 5: each person is on call 20.0% of weeks
   weekly rotation of 8: each person is on call 12.5% of weeks

рддреБрдордЪрд╛ snippet рд╣реЗ print рдХрд░рддреЛ:

rate 0.03/h тЖТ  20 pages ┬╖ busiest shift 2 ┬╖ shifts over 2:  0 ┬╖ night pages (one site):  5
rate 0.07/h тЖТ  55 pages ┬╖ busiest shift 3 ┬╖ shifts over 2:  4 ┬╖ night pages (one site): 17
rate 0.15/h тЖТ 114 pages ┬╖ busiest shift 5 ┬╖ shifts over 2: 21 ┬╖ night pages (one site): 48

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

Shift рдорд╛рдЧреЗ 0.98 pages рдЪреА рд╕рд░рд╛рд╕рд░реА рдареАрдХ рджрд┐рд╕рддреЗ, рдкрдг рддрд░реАрд╣реА 4 shifts 2 рдЪреНрдпрд╛ рд╡рд░ рдЧреЗрд▓реНрдпрд╛ тАФ pages рдШреЛрд│рдХреНрдпрд╛рдиреЗ рдпреЗрддрд╛рдд, рдореНрд╣рдгреВрди рд╕рд░рд╛рд╕рд░реА рдирд╡реНрд╣реЗ рддрд░ рд╕рд░реНрд╡рд╛рдд рд╡реНрдпрд╕реНрдд shifts рдмрдШрд╛. рддрд╛рд╕рд╛рд▓рд╛ 0.15 pages рд╡рд░ 56 рдкреИрдХреА 21 shifts рд╡рд░ рдЬрд╛рд╕реНрдд рднрд╛рд░ рдЕрд╕рддреЛ: рд╣рд╛ alert рдЪрд╛ рдкреНрд░рд╢реНрди рдЖрд╣реЗ (alerts рдЖрдгрд┐ рдХрд╛рд░рдгреЗ рджреБрд░реБрд╕реНрдд рдХрд░рд╛), рдорд╛рдгрд╕рд╛рдВрдЪреНрдпрд╛ рд╕рдВрдЦреНрдпреЗрдЪрд╛ рдирд╛рд╣реА. Follow-the-sun рдиреЗ рд░рд╛рддреНрд░реАрдЪреЗ 17 pages рд╢реВрдиреНрдпрд╛рд╡рд░ рдЖрдгрд▓реЗ, рдЖрдгрд┐ 8 рдЬрдгрд╛рдВрдЪреНрдпрд╛ rotation рдиреЗ рддреАрдирдкреИрдХреА-рдПрдХ-рдЖрдард╡рдбрд╛ рдпрд╛рд▓рд╛ рдЖрдардкреИрдХреА-рдПрдХ рдХреЗрд▓реЗ. рджреЛрдиреНрд╣реАрдВрдЪрд╛ рдЦрд░рд╛ рдЦрд░реНрдЪ рдЖрд╣реЗ тАФ рджреБрд╕рд░реА site, рдЬрд╛рд╕реНрдд рдкреНрд░рд╢рд┐рдХреНрд╖рд┐рдд рд▓реЛрдХ тАФ рдЖрдгрд┐ рджреЛрдиреНрд╣реА рдкрдердХ рдЧрдорд╛рд╡рдгреНрдпрд╛рдкреЗрдХреНрд╖рд╛ рд╕реНрд╡рд╕реНрдд рдЖрд╣реЗрдд.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд

рдмрд╣реБрддреЗрдХ teams PagerDuty, Opsgenie, Grafana OnCall рдХрд┐рдВрд╡рд╛ incident.io рд╡рд╛рдкрд░реВрди pages рдЪреЗ рд╡реЗрд│рд╛рдкрддреНрд░рдХ рдЖрдгрд┐ routing рдХрд░рддрд╛рдд. рд╡реЗрд│рд╛рдкрддреНрд░рдХ рдореНрд╣рдгрдЬреЗ data: рдПрдХ rotation, handoff рдЪреА рд╡реЗрд│, рдПрдХ primary рдЖрдгрд┐ рдПрдХ secondary. рдЦрд▒реНрдпрд╛ account рд╡рд░ тАФ PagerDuty рдЪрд╛ REST API рдЖрддреНрддрд╛ рдХреЛрдг on call рдЖрд╣реЗ рддреЗ рджрд╛рдЦрд╡рддреЛ:

curl -s -H "Authorization: Token token=$PD_TOKEN" -H "Accept: application/vnd.pagerduty+json;version=2" \
  "https://api.pagerduty.com/oncalls?schedule_ids[]=$SCHEDULE_ID" | jq -r '.oncalls[].user.summary'

Prometheus Alertmanager рдЪрд╛ route рдлрдХреНрдд рддрд╛рддрдбреАрдЪреЗ alerts pager рд▓рд╛ рдЖрдгрд┐ рдмрд╛рдХреАрдЪреЗ ticket рд░рд╛рдВрдЧреЗрдХрдбреЗ рдкрд╛рдард╡реВ рд╢рдХрддреЛ, рдЖрдгрд┐ рдкреБрдирд░рд╛рд╡реГрддреНрддреА рд░реЛрдЦреВ рд╢рдХрддреЛ:

route:
  receiver: tickets
  group_by: [alertname, service]
  repeat_interval: 4h
  routes:
    - matchers: [severity="page"]
      receiver: pagerduty-timetable
receivers:
  - name: pagerduty-timetable
    pagerduty_configs: [{routing_key: "<secret>"}]
  - name: tickets
    webhook_configs: [{url: "https://tickets.school.example/hook"}]

рдЖрдард╡рдбреНрдпрд╛рдЪрд╛ on-call review (15 рдорд┐рдирд┐рдЯреЗ) рдкреНрд░рддреНрдпреЗрдХ page рдкрд╛рд╣рддреЛ: рддреНрдпрд╛рд╡рд░ рдХреГрддреА рдХрд░рддрд╛ рдпреЗрдгреНрдпрд╛рд╕рд╛рд░рдЦреА рд╣реЛрддреА рдХрд╛, runbook рдмрд░реЛрдмрд░ рд╣реЛрддреЗ рдХрд╛, рддреНрдпрд╛рдРрд╡рдЬреА рддреЗ ticket рдЕрд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ рд╣реЛрддреЗ рдХрд╛?

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ рдЖрд╣реЗ: рдЧреЗрд▓реНрдпрд╛ рдорд╣рд┐рдиреНрдпрд╛рддрд▓реЗ shift рдорд╛рдЧрдЪреЗ pages, рдЖрдгрд┐ рд╡реНрдпрдХреНрддреАрдорд╛рдЧрдЪреЗ рд░рд╛рддреНрд░реАрдЪреЗ pages рдореЛрдЬрд╛. Team рд╕рдореЛрд░ рд╕рд░рд╛рд╕рд░реА рдирд╡реНрд╣реЗ, рддрд░ рд╕рд░реНрд╡рд╛рдд рд╡реНрдпрд╕реНрдд shifts рдареЗрд╡рд╛. рдордЧ рднрд░рддреА рдХрд░рдгреНрдпрд╛рдЖрдзреА рд╕рд░реНрд╡рд╛рдд рдЧреЛрдВрдЧрд╛рдЯ рдХрд░рдгрд╛рд░рд╛ alert рджреБрд░реБрд╕реНрдд рдХрд░рд╛.

тПня╕П рдкреБрдвреЗ

рдмрд░реЗрдЪ pages рдПрдЦрд╛рджреНрдпрд╛ рдмрджрд▓рд╛рдкрд╛рд╕реВрди рд╕реБрд░реВ рд╣реЛрддрд╛рдд. рдкреБрдврдЪрд╛ рдзрдбрд╛ рдПрдЦрд╛рджрд╛ рдмрджрд▓ рдЖрдзреА рд╢рд╛рд│реЗрдЪреНрдпрд╛ рдЫреЛрдЯреНрдпрд╛ рднрд╛рдЧрд╛рд╡рд░ рдХрд░реВрди рдкрд╛рд╣рддреЛ, рдЖрдгрд┐ рдЖрдХрдбреНрдпрд╛рдВрд╡рд░реВрди рддреНрдпрд╛рдЪрд╛ рдирд┐рд░реНрдгрдп рдШреЗрддреЛ.

git checkout lesson-05-canary-analysis

ЁЯУЯ Lesson 04 тАФ On-call: the duty phone, and keeping it sustainable

ЁЯУН You are here: Lesson 04 of 12 ┬╖ Previous: lesson-03-toil ┬╖ Next: lesson-05-canary-analysis


ЁЯУж What's in this branch

Lesson 03, plus the duty phone. Rotations, how many pages a shift can take, night pages, follow-the-sun, and how many people a rotation needs before on-call eats the week. Alert design (what should page at all) belongs to Observability lesson 08; this lesson is about the people who answer. page_times, shift_load, night_pages and oncall_share in sre/crew.py and oncall() in sre/demo.py.

ЁЯзТ Explain like I'm 5

The crew has one duty phone. ЁЯУЯ Whoever holds it must answer when a building breaks тАФ day or night.

At first, only Katrina, Dipika and Aishwarya share it. Each holds it for a week, then passes it on. So each of them holds it one week in three. That is a lot. When you hold the phone, you cannot start big work, and you sleep badly.

The phone rang 17 times at night in four weeks. Tired people make mistakes.

So the crew makes two changes. First, more people join the rotation: with eight, each person holds the phone only one week in eight. Second, they find a sister crew on the other side of the world. When it is night in Pune, it is day there. Pune holds the phone in its day, the sister crew in theirs. Now nobody's phone rings at night.

ЁЯЧ║я╕П Diagram

flowchart LR
    pages["ЁЯУЯ 55 pages in 4 weeks<br/>0.98 per 12-hour shift"] --> one["one Pune crew, 24 h<br/>17 night pages"]
    pages --> fts["follow-the-sun<br/>Pune 07тАУ19 ┬╖ sister crew 19тАУ07<br/>0 night pages"]
    pages --> busy["4 of 56 shifts<br/>had more than 2 pages"]
    rot["rotation size"] --> r3["3 people: 33.3% of weeks тЬЧ"]
    rot --> r8["8 people: 12.5% of weeks тЬУ"]

ЁЯЧ║я╕П Drawn version + a lab: https://school-edh.pages.dev/sre/lesson-diagrams.html#l04

тЭУ What

ЁЯдФ Why

Because on-call is where reliability meets people. A rotation that is too small burns people out, and burned-out people make the next outage worse. Night pages damage sleep for days. Too many pages per shift means none gets real attention, only a quick restart тАФ and the cause stays. The numbers here are what let you say "we need four more people" or "these alerts must be fixed" with evidence.

ЁЯФз How (in this repo)

page_times(weeks=4, rate=0.07, seed=11) in sre/crew.py makes a seeded list of page hours (hour 0 = Monday 00:00 in Pune; at most one page per hour). shift_load(pages) counts pages per 12-hour shift (07:00тАУ19:00 and 19:00тАУ07:00); night_pages(pages, offset_h) counts pages between 22:00 and 07:00 for a crew offset_h hours away; oncall_share(people) is the share of weeks each person is on call in a weekly rotation. The follow-the-sun line in oncall() gives Pune the 07тАУ19 pages and the sister crew (12 hours away) the rest.

ЁЯзк Try it

python3 sre/demo.py oncall
python3 - <<'EOF'
import sys; sys.path.insert(0, "sre"); from crew import page_times, shift_load, night_pages
for rate in (0.03, 0.07, 0.15):
    pages = page_times(weeks=4, rate=rate)
    load = shift_load(pages)
    over = sum(1 for n in load.values() if n > 2)
    print(f"rate {rate:.2f}/h тЖТ {len(pages):>3} pages ┬╖ busiest shift {max(load.values())} ┬╖ shifts over 2: {over:>2} ┬╖ night pages (one site): {night_pages(pages):>2}")
EOF

тЬЕ Verify тАФ what you should see

oncall prints:

тФАтФА 4 weeks of pages for the timetable service: 55 pages ┬╖ 0.98 per 12-hour shift on average
   busiest shift 3 pages ┬╖ shifts with more than 2 pages: 4 of 56
   one Pune crew, 24 h on call: 17 pages between 22:00 and 07:00
   follow-the-sun: Pune takes 07:00тАУ19:00, a sister crew 12 h away takes the rest in its own daytime тЖТ night pages 0 + 0
   weekly rotation of 3: each person is on call 33.3% of weeks  тЖР over 25%: no time left for engineering
   weekly rotation of 5: each person is on call 20.0% of weeks
   weekly rotation of 8: each person is on call 12.5% of weeks

Your snippet prints:

rate 0.03/h тЖТ  20 pages ┬╖ busiest shift 2 ┬╖ shifts over 2:  0 ┬╖ night pages (one site):  5
rate 0.07/h тЖТ  55 pages ┬╖ busiest shift 3 ┬╖ shifts over 2:  4 ┬╖ night pages (one site): 17
rate 0.15/h тЖТ 114 pages ┬╖ busiest shift 5 ┬╖ shifts over 2: 21 ┬╖ night pages (one site): 48

ЁЯПБ What you just proved

An average of 0.98 pages a shift looks fine, but 4 shifts still went over 2 тАФ pages come in clumps, so look at the busiest shifts, not the mean. At 0.15 pages an hour, 21 of 56 shifts are overloaded: that is an alert problem (fix the alerts and the causes), not a staffing problem. Follow-the-sun turned 17 night pages into zero, and a rotation of 8 turned one-week-in-three into one-in-eight. Both are real costs тАФ a second site, more trained people тАФ and both are cheaper than losing the crew.

тЪая╕П Common mistakes

ЁЯПн In production

Most teams schedule and route pages with PagerDuty, Opsgenie, Grafana OnCall or incident.io. The schedule is data: a rotation, a handoff time, a primary and a secondary. On a real account тАФ PagerDuty's REST API lists who is on call now:

curl -s -H "Authorization: Token token=$PD_TOKEN" -H "Accept: application/vnd.pagerduty+json;version=2" \
  "https://api.pagerduty.com/oncalls?schedule_ids[]=$SCHEDULE_ID" | jq -r '.oncalls[].user.summary'

A Prometheus Alertmanager route can send only urgent alerts to the pager and the rest to a ticket queue, and hold back repeats:

route:
  receiver: tickets
  group_by: [alertname, service]
  repeat_interval: 4h
  routes:
    - matchers: [severity="page"]
      receiver: pagerduty-timetable
receivers:
  - name: pagerduty-timetable
    pagerduty_configs: [{routing_key: "<secret>"}]
  - name: tickets
    webhook_configs: [{url: "https://tickets.school.example/hook"}]

A weekly on-call review (15 minutes) looks at every page: was it actionable, was the runbook right, should it be a ticket instead?

ЁЯПн Why this matters in production: count last month's pages per shift, and the night pages per person. Put the busiest shifts in front of the team, not the average. Then fix the top noisy alert before hiring.

тПня╕П Next

Many pages start with a change. The next lesson tries a change on a small part of the school first, and judges it with numbers.

git checkout lesson-05-canary-analysis
тЖР PrevioustoilNext тЖТcanary analysis

This page is the lesson's README from the lesson-04-on-call branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.