Go тілі: нөлден өз блогыңызға дейін 48-сабақ (барлығы 50)
Қайта жүктеуден аман өтетін қызмет: systemd және ұқыпты тоқтау
Go курсының қырық сегізінші сабағы. Бағдарламаны түймемен емес, сигналмен тоқтатады, әрі айырмашылық өлшенді: үзілісте оқырман EOF алады, ұқыпты тоқтауда — 270 мс өткенде 200 жауабы. Қоса нағыз systemd тексерген қызмет файлы, қайта жүктеуден кейінгі автоқосылу әрі жабу реті.
Не үшін керек
Блог терминалдан іске қосылған. Ноутбуктің қақпағын жаптыңыз — блог жоқ. Серверді қайта жүктедіңіз — блог жоқ. Нұсқаны жаңарттыңыз — сол сәтте мақала оқып отырған адам бет орнына қосылымның үзілуін алды.
Мұның бәрі екі нәрсемен емделеді. Біріншісі — қызмет: жүйе блогты жүктелгенде өзі іске қосады әрі құласа қайта көтереді. Екіншісі — ұқыпты тоқтау: бағдарлама одан аяқтауды сұрағанын біледі, басталған жауаптарды жазып бітіреді әрі содан кейін ғана кетеді.
Екіншісінен бастаймыз, өйткені онсыз біріншісі жағдайды нашарлатады ғана: қызмет блогты әр жаңартуда оқырмандарды үзе отырып, ұқыпты қайта қоса береді.
Екінші жартысы туралы бірден: systemd тек Linux-та бар. Сізде macOS не Windows болса, ұқыпты тоқтау дәл солай жұмыс істейді — бұл жүйелік баптау емес, Go коды, — ал қызмет файлын блогты шығарған серверде тексеріңіз. Сол жұмысты macOS-та launchd, Windows-та қызметтер істейді; файлы басқа, мәні сол.
Бірден тұтас
Жаңа қалта, go mod init sabaq45. Бағдарлама серверді көтереді, оқырманды бет үшін жібереді әрі жауаптың ортасында серверді төрт түрлі тәсілмен тоқтатады:
package main
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
// Сілтемені басып та қойған оқырман: жауап секундтың үштен бірінде дайындалады.
const answerTime = 300 * time.Millisecond
func main() {
fmt.Println("== үзіліс: сервер сөз ортасында жабылды")
report(stop(func(srv *http.Server) error { return srv.Close() }))
fmt.Println()
fmt.Println("== ұқыпты: Shutdown жауапты күтті")
report(stop(func(srv *http.Server) error {
return srv.Shutdown(context.Background())
}))
fmt.Println()
fmt.Println("== жауапқа керегінен аз уақыт берілді")
report(stop(func(srv *http.Server) error {
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
err := srv.Shutdown(ctx)
if err != nil {
// Өмірде осылай істейді: күтуге уақыт қалмады, қосылымдарды үземіз.
srv.Close()
}
return err
}))
fmt.Println()
fmt.Println("== systemd жіберетін сигнал")
signalDemo()
}
// result — оқырманда не шықты әрі сол кезде сервер не істеді.
type result struct {
reader string
waited time.Duration
shutdown error
}
// stop серверді көтереді, оқырманды бет үшін жібереді әрі жауаптың ортасында
// серверді берілген тәсілмен тоқтатады.
func stop(how func(*http.Server) error) result {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
panic(err)
}
srv := &http.Server{Handler: http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
time.Sleep(answerTime)
fmt.Fprint(w, "дайын")
})}
go srv.Serve(l)
done := make(chan string, 1)
go func() {
resp, err := http.Get("http://" + l.Addr().String() + "/")
if err != nil {
done <- "үзіліс: " + shortErr(err)
return
}
defer resp.Body.Close()
done <- fmt.Sprintf("жауап %d", resp.StatusCode)
}()
// Оқырманға өңдеушіге жетуге мүмкіндік беру: тоқтату жауаптың ортасында
// болуы керек, әйтпесе басқа нәрсені өлшейміз.
time.Sleep(50 * time.Millisecond)
start := time.Now()
shutdownErr := how(srv)
waited := time.Since(start)
return result{reader: <-done, waited: waited.Round(10 * time.Millisecond), shutdown: shutdownErr}
}
func report(r result) {
fmt.Printf("оқырман алғаны: %s\n", r.reader)
fmt.Printf("тоқтау уақыты: %v\n", r.waited)
if r.shutdown != nil {
fmt.Printf("тоқтау қайтарғаны: %v\n", r.shutdown)
return
}
fmt.Println("тоқтау қайтарғаны: nil")
}
// signalDemo бағдарламаның аяқтау өтінішін қалай білетінін көрсетеді. systemd
// SIGTERM жіберіп күтеді; мұнда оны өзімізге өзіміз жібереміз.
func signalDemo() {
ctx, stopSignals := signal.NotifyContext(context.Background(),
syscall.SIGTERM, os.Interrupt)
defer stopSignals()
start := time.Now()
if err := syscall.Kill(os.Getpid(), syscall.SIGTERM); err != nil {
fmt.Println("сигнал жіберілмеді:", err)
return
}
<-ctx.Done()
fmt.Printf("сигнал жетті: %s\n", instant(time.Since(start)))
fmt.Printf("контекст айтады: %v\n", ctx.Err())
fmt.Println("әрі қарай: Shutdown, содан соң дерекқорды жабу")
}
// shortErr қатеден кездейсоқ порты бар мекенжайды алып тастайды, сонда шығыс
// әр іске қосылғанда бірдей болады.
func shortErr(err error) string {
var ue *net.OpError
if errors.As(err, &ue) {
return ue.Err.Error()
}
if e, ok := err.(interface{ Unwrap() error }); ok {
return shortErr(e.Unwrap())
}
return err.Error()
}
// instant нақты санды жасырады: сигналдың бағдарлама бірдеңе істеп
// үлгергенше жететіні маңызды, нақты микросекунд саны емес.
func instant(d time.Duration) string {
if d < time.Millisecond {
return "лезде, миллисекундтан аз"
}
return d.Round(time.Millisecond).String()
}
Шығатыны:
== үзіліс: сервер сөз ортасында жабылды
оқырман алғаны: үзіліс: EOF
тоқтау уақыты: 0s
тоқтау қайтарғаны: nil
== ұқыпты: Shutdown жауапты күтті
оқырман алғаны: жауап 200
тоқтау уақыты: 270ms
тоқтау қайтарғаны: nil
== жауапқа керегінен аз уақыт берілді
оқырман алғаны: үзіліс: EOF
тоқтау уақыты: 50ms
тоқтау қайтарғаны: context deadline exceeded
== systemd жіберетін сигнал
сигнал жетті: лезде, миллисекундтан аз
контекст айтады: context canceled
әрі қарай: Shutdown, содан соң дерекқорды жабу
Талдау
Үзіліс пен жауап — бір жолдың айырмасы
Алғашқы екі блок бір шақырумен ерекшеленеді. srv.Close() бәрін дереу жабады: тоқтау 0s алды, ал оқырман бет орнына EOF алды — қосылым сөз ортасында үзілді.
srv.Shutdown(ctx) басқа нәрсе істейді: жаңа сұраныстарды қабылдауды тоқтатады, басталғандарын күтеді әрі қайтады. Тоқтау 270 мс алды — өңдеушіміз жауапты дәл сонша дайындаған еді, — әрі оқырман 200 алды.
Өмірде бұл былай көрінеді: сайтты жаңартқанда оқырмандардың жартысы «бет қолжетімсіз» дегенді көреді — немесе ерекше ештеңе байқамайды.
Елестетіп көріңіз. Дүкен жабылады. Жарықты сөндіріп, есікті адамдар ішінде тұрғанда құлыптауға болады, ал жаңаларын кіргізуді тоқтатып, кассада тұрғандардың шығуын күтуге де болады.
Уақыт жеткілікті болуы керек
Үшінші блок — сол ұқыпты тоқтау, бірақ 300 мс керек жауапқа 50 мс берілді. Shutdown context deadline exceeded қайтарды, ал бағдарламада қосылымдарды үзуден басқа амал қалмайды: оқырман тағы да EOF алды.
Осыдан ереже: тоқтау мерзімі ең ұзақ жауаптан үлкен болуы керек. Блогта бұл — мақала беті емес, сурет жүктеу. 33-қадамда 15 секунд тұр.
Сигнал — жүйенің бағдарламамен сөйлесу тәсілі
Төртінші блок «бағдарлама уақыт келгенін қайдан біледі?» деген сұраққа жауап береді. Оған сигнал жіберіледі — жүйеден келген қысқа хабар. SIGTERM «жұмысты аяқта» дегенді білдіреді; оны systemd те, docker те, хостинг панеліндегі «тоқтату» түймесі де жібереді. Терминалдағы Ctrl+C — SIGINT, мағынасы сол.
Go-да бұл бір жол:
ctx, stopSignals := signal.NotifyContext(context.Background(),
syscall.SIGTERM, os.Interrupt)
defer stopSignals()
NotifyContext сигналды болдырылмаған контекстке айналдырады — context сабағындағы сол құралға. Әрі қарай бағдарлама жай ғана <-ctx.Done() күтеді, әрі оған сигналдар туралы ерекше білім керек емес. Өлшенді: сигнал миллисекундтан аз уақытта жетеді.
Ұстауға болмайтын сигнал бар: SIGKILL (kill -9). Ол бағдарламаға жетпейді — процесті ядро өлтіреді. Сондықтан kill -9 тоқтату тәсілі емес, ең соңғы амал деп айтады.
Кодтан гөрі рет маңызды
Жиналған түрі мынадай, әрі мұндағы рет әшекей емес:
- сигнал келді — контекст жабылды;
srv.Shutdown(ctx)— жаңаны алмаймыз, басталғанын күтеміз;- әрі тек енді
store.Close()— дерекқор соңғы болып жабылады.
Дерекқорды бірінші жабу — күтіп отырған жауаптарымызды өз қолыңмен бұзу деген сөз. Қате жиі кездеседі әрі сырттай зиянсыз көрінеді: defer store.Close() main-нің басында тұр, әрі осы жеткілікті сияқты. Жеткілікті — бірақ тек defer Shutdown-нан кейін істейтіндіктен, оған дейін емес.
Қызмет файлы
Енді міндеттің бірінші жартысы. systemd — Linux-та бағдарламаларды іске қосатын әрі бақылайтын нәрсе. Оған бір файл керек; 33-қадамда ол толық жатыр, ал ондағы басты нәрсе — мына жолдар:
[Service]
User=blog
ExecStart=/opt/blog/blog
Environment=BLOG_ADDR=127.0.0.1:8080
Restart=always
RestartSec=2
KillSignal=SIGTERM
TimeoutStopSec=20
[Install]
WantedBy=multi-user.target
Restart=always — құлады, көтердік; RestartSec=2 — екі секундта бір реттен жиі емес, әйтпесе бұзылған бағдарлама минутына мың рет қайта қосылады. KillSignal=SIGTERM — жаңа ғана қабылдауды үйренгеніміз. TimeoutStopSec=20 — systemd процесті мүлде өлтірмес бұрын қанша күтеді; бұл сан біздің тоқтау мерзімімізден үлкен болуы керек, әйтпесе ұқыпты тоқтау үлгермей, үзіліске айналады.
WantedBy=multi-user.target — бұл «машина жүктелгенде іске қосу» дегеннің өзі.
Қадамдағы файлды нағыз серверде нағыз systemd тексерді:
$ systemd-analyze verify blog.service
blog.service: Command /opt/blog/blog is not executable: No such file or directory
Жалғыз ескертпе — сол машинада бағдарлама файлының өзі жоқ екені; қызметтің белгілеуіне қарсылық болмады.
Білуге тиіс үш пәрмен:
systemctl enable --now blog # іске қосу әрі автоқосылуды қосу
systemctl restart blog # жаңартудан кейін қайта қосу
journalctl -u blog -f # журналды тікелей қарау
Журналды сіз әлдеқашан жазып жүрсіз — қателер мен slog сабағынан бері. systemd бағдарлама шығысқа басып шығарғанның бәрін алады әрі journalctl ішіне салады; ешқандай журнал файлын ашудың қажеті жоқ.
Бір минут тұратын тағы бірнеше жол
Қызмет файлында ештеңеге тұрмайтын, бірақ көп нәрсені жабатын жолдар бар:
User=blog— блогroot-тан емес, өз пайдаланушысынан жұмыс істейді. Сонда бұзылған блог бұзылған машинаға тең емес;NoNewPrivileges=true— процесс іске қосылғанда алғанынан артық құқық ала алмайды;ProtectSystem=strictпенReadWritePaths=/var/lib/blog— оған бүкіл жүйе тек оқуға, дерекқор мен суреттер жатқан бір қалтадан басқасы;PrivateTmp=true— ортақ емес, өз/tmp-і.
Сабақ картасы
Өз сөзіңізбен айтыңыз
Қарамай, дауыстап немесе қағазға жауап беріңіз. Жауаптар — сабақтың соңында.
- Оқырман тұрғысынан
srv.Shutdownsrv.Close-тан несімен өзгеше? - Дерекқорды неге
Shutdown-нан кейін жабады, оған дейін емес? - Қызмет файлындағы
TimeoutStopSecнеге бағдарламадағы тоқтау мерзімінен үлкен болуы керек?
Тапсырма
Міндетті. Блогыңызды ұқыпты тоқтауға үйретіңіз: сигнал signal.NotifyContext арқылы ұсталады, Shutdown орынды мерзім алады, дерекқор соңғы болып жабылады. Тексеріңіз: блогты іске қосып, оған kill -TERM жіберіңіз, шығу кодын қараңыз — 0 болуы керек, ал журналда тоқтау туралы екі жол тұруы керек.
Мұның бәрі step-33 ішінде жасалған — өзіңіз жасағаннан кейін салыстырыңыз.
Қалауыңызша.
- Өз қызмет файлыңызды жазып, оны тексеріңіз:
systemd-analyze verify blog.service. TimeoutStopSec=1қойып, қайта қосқанда ұзақ жауапқа не болатынын қараңыз.Shutdown-ға күтуге тура келген қосылым саны бар журнал жолын қосыңыз.
Блогта бұл қайда тұрады
33-қадам 32-қадамнан бір нәрсемен ерекшеленеді: ол тоқтай алады. Айырмашылық терминалда тікелей өлшенеді:
32-қадам, SIGTERM: шығу коды 143 — процесс сол жерде өлтірілді,
журналдағы соңғы жол — біреудің сұранысы
33-қадам, SIGTERM: шығу коды 0
«тоқтау сигналы келді, күту 15s»
«бәрі жауап берілді, тоқтадық»
143 коды — бұл 128 + 15, яғни «процесті 15-нөмірлі сигнал өлтірді». 0 коды — «бағдарлама өзі әрі қалыпты аяқталды». systemd панелінде біріншісі failed, екіншісі inactive (dead) болып көрінеді, әрі құлау себебін кейін осы жол бойынша іздейді.
Қарыздар. Блог әлі күнге сұранысты оқу мен жазуға таймаут қоймайды, ал онсыз бір баяу қосылым ағынды ұзаққа ұстап тұра алады. Бұл — іске қосар алдындағы тексеру парағы, келесі сабақ.
Жауаптар
Жауаптарды көрсету
Closeқосылымдарды дереу үзеді: оқырман бет орнына үзілісті алады.Shutdownжаңа сұраныстарды қабылдауды тоқтатады, басталғандарын күтеді әрі содан кейін ғана қайтады — оқырман өз жауабын алады.- Өйткені күтіп отырған жауаптарымыз дерекқорға баруы мүмкін. Оны бірінші жауып, дәл сол күтіп отырған сұраныстарымызды бұзамыз.
- Өйткені systemd
TimeoutStopSecкүтеді де, процесті өлтіреді. Ол кіші болса, бағдарлама жауаптарды жазып үлгермейді, әрі ұқыпты тоқтау сол үзіліске айналады.
Пікірлер (0)
Пікір қалдыру үшін кіріңіз →
Әзірге пікір жоқ. Бірінші болыңыз.