Go тілі: нөлден өз блогыңызға дейін 46-сабақ (барлығы 50)
Блогты адамдарға жеткізу: тегін хостинг
Go курсының қырық алтыншы сабағы. Хостинг сіз үшін үш нәрсені шешеді: порт PORT айнымалысында келеді, машинаның барлық мекенжайын тыңдау керек, ал тегін тарифтің дискі әр деплойда жоғалады — өлшенді: сол құпиясөзбен кіру бұрын 303, кейін 401. Әрі бір статикалық файлға жинау.
Не үшін керек
Блог сіздің ноутбугіңізде жұмыс істейді. Сол жерде тұрғанда оны ешкім аша алмайды: 127.0.0.1 — «мен өзім» деген мекенжай, әрі көршіде ол көршіге апарады.
Блогты ашу үшін әрқашан қосулы тұратын, интернетте мекенжайы бар компьютер керек. Ондайды айына екі-үш мың теңгеге сатып алуға болады — бұл келесі сабақ. Бірақ тегін тарифтен бастаған жөн: ол бірнеше хостингте бар, он минутта көтеріледі әрі бағдарламаны әлемге шығару неге тұратынын шынайы көрсетеді.
Бір ескерту бірден, ренжімес үшін. Тегін тариф — витрина, үй емес. Оның нақты нені сақтамайтынын әрі неге сақтамайтынын сабақтың соңында өлшейміз.
Бірден тұтас
Жаңа қалта, go mod init sabaq43. Бағдарлама блог емес, хостингтің кез келген бағдарламаға қоятын үш сұрағы:
package main
import (
"fmt"
"net"
"net/http"
"os"
"time"
)
// start — процестің іске қосылған уақыты. Тегін тариф сервисті ұйықтатады,
// әрі ұйқыдан кейінгі алғашқы оқырман дәл осыны күтеді: старт пен алғашқы жауап.
// Жазулар тұрақтыға шығарылды: шығыста порт нөмірі де, микросекунд та
// болмауға тиіс — олар әр іске қосылғанда өзгереді әрі ештеңе түсіндірмейді.
const (
LABEL_ONLY_SELF = "тек 127.0.0.1-ді тыңдаймыз:"
LABEL_ALL = "барлық мекенжайды тыңдаймыз:"
LABEL_START = "процесс стартынан алғашқы жауапқа дейін:"
)
var start = time.Now()
func main() {
// 1. Портты бағдарлама емес, хостинг таңдайды: ол оны PORT-та береді.
fmt.Println("== порт сырттан келеді")
fmt.Printf("ортадағы PORT: %q\n", os.Getenv("PORT"))
fmt.Printf("тыңдайтын мекенжай: %q\n", addr())
// 2. Тек өзін тыңдағанды сырттан ешкім естімейді.
fmt.Println()
fmt.Println("== сырттан кімді естиді")
outside := lanIP()
fmt.Printf("машинаның сырттағы мекенжайы: %s\n", outside)
local, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
fmt.Println("портты алу мүмкін болмады:", err)
return
}
defer local.Close()
fmt.Printf("%-30s %s\n", LABEL_ONLY_SELF, knock(outside, port(local)))
all, err := net.Listen("tcp", ":0")
if err != nil {
fmt.Println("портты алу мүмкін болмады:", err)
return
}
defer all.Close()
fmt.Printf("%-30s %s\n", LABEL_ALL, knock(outside, port(all)))
// 3. Ұйқыдан кейін алғашқы оқырман қанша күтеді.
fmt.Println()
fmt.Println("== суық старт")
srv, ready := serve()
defer srv.Close()
resp, err := http.Get("http://" + srv.Addr().String() + "/healthz")
if err != nil {
fmt.Println("сұраныс өтпеді:", err)
return
}
resp.Body.Close()
fmt.Printf("алғашқы жауап: %d\n", resp.StatusCode)
fmt.Printf("%s %s\n", LABEL_START, fast(<-ready))
}
// addr мекенжайды PORT-тан жинайды. Хостсыз қос нүкте — «машинаның барлық
// мекенжайы», әрі хостинг сервисімізді тек осылай көреді.
func addr() string {
p := os.Getenv("PORT")
if p == "" {
p = "8080"
}
return ":" + p
}
// lanIP — машинаның желідегі мекенжайы, ілмек емес. Сервис көтерілді ме деп
// тексергенде хостинг дәл осыған қағады.
func lanIP() string {
addrs, _ := net.InterfaceAddrs()
for _, a := range addrs {
if n, ok := a.(*net.IPNet); ok && !n.IP.IsLoopback() && n.IP.To4() != nil {
return n.IP.String()
}
}
return "127.0.0.1"
}
func port(l net.Listener) string {
_, p, _ := net.SplitHostPort(l.Addr().String())
return p
}
// knock — сырттан қағу: қосыла алды ма, жоқ па.
func knock(host, port string) string {
c, err := net.DialTimeout("tcp", net.JoinHostPort(host, port), time.Second)
if err != nil {
return "жауап жоқ"
}
c.Close()
return "жауап береді"
}
// serve серверді көтереді әрі процесс стартынан алғашқы қызмет көрсетілген
// сұранысқа дейін қанша уақыт өткенін хабарлайды.
func serve() (net.Listener, <-chan time.Duration) {
ready := make(chan time.Duration, 1)
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
panic(err)
}
mux := http.NewServeMux()
// /healthz — оқырманға емес, хостингке арналған бет: ол осы бойынша сервис
// тірі ме, әлде қайта қосу керек пе деп шешеді.
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
select {
case ready <- time.Since(start):
default:
}
w.Write([]byte("ok"))
})
go http.Serve(l, mux)
return l, ready
}
// fast нақты санды жасырады: бағдарламаның старты не он миллисекундқа
// сыяды, не сыймайды — маңыздысы осы.
func fast(d time.Duration) string {
if d < 10*time.Millisecond {
return "10 мс-тен аз"
}
return d.Round(time.Millisecond).String()
}
Іске қосу — хостинг қалай істесе, солай, орта айнымалысы арқылы:
$ PORT=10000 go run .
ортадағы PORT: "10000"
тыңдайтын мекенжай: ":10000"
== сырттан кімді естиді
машинаның сырттағы мекенжайы: 192.168.1.200
тек 127.0.0.1-ді тыңдаймыз: жауап жоқ
барлық мекенжайды тыңдаймыз: жауап береді
== суық старт
алғашқы жауап: 200
процесс стартынан алғашқы жауапқа дейін: 10 мс-тен аз
Талдау
Портты сіз таңдамайсыз
Бірінші таңқалдыратыны: портты хостинг өзі шешеді әрі оны PORT айнымалысында хабарлайды. Сіздің ісіңіз — оны оқып, дәл сол жерде тыңдау.
p := os.Getenv("PORT")
if p == "" {
p = "8080"
}
Бұл — конфигурация сабағындағы сол байланыс: өз машинаңызға арналған әдепкі мән, бөтен машинаға арналған орта айнымалысы. Жалауша жөндеу үшін жоғарыда қалады, бірақ хостингте оны ешкім бермейді.
Кодқа жазылған 8080 порты — бірінші деплойдың жауап бермеуінің ең жиі себебі. Хостинг сізді өз портында күтеді, ал сіз өзіңіздікін тыңдап отырсыз.
Өзін тыңдау — ешкімге жауап бермеу деген сөз
Шығыстың екінші блогы — пайымдау емес, өлшенген факт:
тек 127.0.0.1-ді тыңдаймыз: жауап жоқ
барлық мекенжайды тыңдаймыз: жауап береді
Екі бағдарлама да жұмыс істейді. Екеуі де тыңдайды. Айырмашылық біреу: біріншісінің портының алдында 127.0.0.1 тұр, екіншісінің алдында ештеңе жоқ.
127.0.0.1 — ілмек, машинаның өзімен сөйлесуі. Желіден келген пакет ол жерге ешқашан түспейді. Хостсыз қос нүкте — «осы машинаның барлық мекенжайы», әрі хостинг сізді тек сонда көреді.
Хостинг панеліндегі no open ports detected дәл осылай көрінеді: сервис қосулы, журнал таза, ал порт «ашылмаған».
Елестетіп көріңіз. Қалалық желіден ажыратылған пәтердегі телефон. Пәтердің ішінде шырылдайды, сырттан ешкім қоңырау шала алмайды.
/healthz — оқырманға емес, хостингке арналған бет
Хостинг бір бетке үнемі қағып тұрады әрі оның жауабы бойынша шешеді: сервис тірі ме, әлде қайта қосу керек пе.
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
})
Оған үш ереже. Ол дерекқорға бармайды — әйтпесе баяу дерекқор өлі сервис сияқты көрінеді, әрі хостинг сау бағдарламаны қайта қосады. Ол үлгі жинамайды — адамға көрсететін ештеңесі жоқ. Әрі ол тез жауап береді: тексерудің таймауты бар, әдетте секундпен саналады.
Дәлірек айтқанда, тексеру екеу болады. /livez — «процесс тірі ме» — ештеңеге тәуелді емес, сол бойынша қайта қосады. /readyz — «оқырман қабылдауға дайын ба» — дерекқорға дәл сол қарайды, әрі осы данаға сұраныс жіберу-жібермеуді сол шешеді. Дана біреу болып, хостингтің жалғыз әрекеті қайта қосу болғанда біріншісі жетеді; екіншісі даналар бірнешеу болғанда керек.
/healthz деген ат — Kubernetes-тен келген дәстүр; соңындағы z әрпі атты нағыз беттерден ажырату үшін ғана тұр.
Бір файл — әрі бәрі
Go бағдарламасын PHP немесе Python сияқты шығармайды: «бастапқы кодты салып, керек нұсқа бар шығар деп үміттену» емес. Компилятор бір файл жасайды, әрі ішінде бәрі бар:
кәдімгі жинақ: 17.8 МБ
-ldflags "-s -w": 12.2 МБ
не шықты: ELF 64-bit, x86-64, statically linked
блогтың оянуы: алғаш рет 55 мс (көшірулер), содан кейін 13–14 мс
CGO_ENABLED=0 — SQLite сабағында Go-да жазылған драйверді алуымыздың себебі. C-дағы бірде-бір жолсыз бинарник статикалық болып шығады: оған сол машинадағы жүйелік кітапхана да, орнатылған Go да, тіпті ешнәрсе де керек емес.
GOOS=linux GOARCH=amd64 — өз Mac-іңізден бөтен Linux-қа жинау. Ештеңе орнатудың қажеті жоқ: Go компиляторы мұны қорапта істей алады.
-ldflags "-s -w" жөндеушіге арналған кестелерді алып тастайды — өндірісте ешкімге керек емес 5,6 МБ.
Dockerfile: екі стадия
Тегін хостингтердің көбі Dockerfile қабылдайды. Ол 31-ші қадамда толық тұр, ал идеясы екі стадияда:
FROM golang:1.26-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o /blog .
FROM alpine:3.22
COPY --from=build /blog /app/blog
CMD ["/app/blog"]
Бірінші стадия — бүкіл бастапқы кодымен бірге тұтас Go, екіншісі — тап-таза жүйе мен біздің жалғыз файл. Компиляторға керек болғанның бәрі соңғы образға түспейтін қабатта қалады.
Қадамда көрінетін екі ұсақ нәрсе. go.mod пен go.sum қалғанынан бөлек әрі ертерек көшіріледі: қабат ол жиналған файлдар өзгергенде ғана қайта жиналады — өңдеушідегі түзету енді барлық тәуелділікті қайта жүктеуге апармайды. Әрі контейнер root-тан емес жұмыс істейді: бір жол adduser, әрі бұзылған блог бұзылған машинаға тең емес.
Диск жоғалады, әрі бұл ақау емес
Тегін тарифтегі ең маңыздысы — тіркелу формасы үндемейтін нәрсе. Мұны сабақтың соңында өз блогымызда өлшедік: адам тіркеледі, құпиясөзбен кіреді — 303. Деплой дискіні өшіреді. Блог ештеңе болмағандай көтеріледі, 200 береді, себіндінің үш мақаласын көрсетеді — әрі сол құпиясөз 401 береді. Адам жоқ.
Дәлірегі былай: контейнер жазатын қабат осы контейнерге тиесілі. Сол контейнерді қайта қосу оны әдетте сақтайды; өшіру немесе ауыстыру — ал жаңа деплой дәл сол — онымен бірге алып кетеді. Тегін тарифтер әр жолы жаңа контейнер көтереді, сондықтан оларда blog.db файлы қайтадан басталады. Дерек деплойдан аман өтуі үшін оны контейнерден шығарады: бұл не том (volume), не сыртқы дерекқор.
Бұл жағдайда не істейді, бағасы өсу ретімен:
- тарифті витрина деп санау: блог себіндінің мақалаларын көрсетсін, ал оқырман деректері онда мүлде болмасын;
- деректі сыртқа шығару: тегін Postgres немесе SQLite-сервис ретінде — файл контейнерде тұрмайды;
- ақылы диск: сол хостингтердегі бірінші ақылы жол, әдетте бір-екі доллар.
Блогта SQLite тұрғанда, шыншыл жауап — біріншісі. Екіншісі — бөлек әңгіме, әрі ол Go туралы емес, дерекқордың қай жерде тұратыны туралы.
Сервис ұйықтайды
Тегін тарифтің екінші ерекшелігі: сұраныссыз ол ұйықтайды, ал ұйқыдан кейінгі алғашқы оқырман контейнердің қайта көтерілуін күтеді. Әдетте бұл ондаған секунд.
Осы уақыттың біздің үлесіміз өлшенді: алғаш рет 55 мс — дерекқор құрылып, көшірулер жүреді, — әрі әрі қарай 13–14 мс. Қалған күтудің бәрі сіздің бағдарламаңызға емес, хостингке тиесілі, әрі оны кодтан тездету мүмкін емес.
Ұйқыны әр бес минут сайын сырттан пинг жіберіп алдауға тырыспаңыз: хостингтерде бұл ережеде тікелей жазылған, ал тегін минуттар осымен бітеді. Бағасын біліп, таңдаған дұрыс: витрина — тегін, тірі сайт — ақшаға.
Құпиялар git-ке кетпейді
Дерекқордың құпиясөзі, боттың токені, төлем жүйесінің кілті — бұның бәрі хостинг панелінде орта айнымалысы ретінде беріледі, ал кодта сол os.Getenv арқылы оқылады. Репозиторийде олар ешқашан болмайды, әрі бұл ереженің ерекшелігі жоқ: репозиторий бір күні ашық болады, ал git тарихы бәрін есінде сақтайды.
Сабақ картасы
Өз сөзіңізбен айтыңыз
Қарамай, дауыстап немесе қағазға жауап беріңіз. Жауаптар — сабақтың соңында.
127.0.0.1тыңдайтын сервис неге сырттан жауап бермейді?/healthzнеге дерекқорға бармауы керек?- Тегін тарифте деплой кезінде SQLite дерекқорына не болады?
Тапсырма
Міндетті. Блогты кез келген тегін тарифке сілтемемен ашылатындай етіп шығарыңыз: мекенжай PORT-тан алынады, барлық мекенжай тыңдалады, /healthz ok деп жауап береді, жинақ Dockerfile бойынша жүреді. Хостинг журналынан порттың сәйкес келгенін тексеріңіз.
Мұның бәрі step-31 ішінде жасалған — өзіңіз жасағаннан кейін салыстырыңыз.
Қалауыңызша.
- Өзіңіз шығарған блогта тіркеліңіз, жаңа деплой жасаңыз әрі енді кіре алмайтыныңызға көз жеткізіңіз. Бұл жағымсыз, әрі мұны нағыз блогта емес, оқу блогында білген жақсы.
- Бинарникті
-ldflags "-s -w"арқылы әрі онсыз жинап, өз машинаңызда өлшемдерін салыстырыңыз. /healthz-кеruntime/debug.ReadBuildInfoарқылы нұсқа нөмірін қосыңыз — сонда браузерден дәл қай коммит жұмыс істеп тұрғанын көресіз.
Блогта бұл қайда тұрады
31-ші қадамда блогта нақты үш нәрсе пайда болды: PORT-тан алынған мекенжай, /healthz беті әрі Dockerfile. Міне, олар, өлшенген түрде:
== порт және денсаулық
PORT-тан алынған мекенжай: :8093
GET /healthz 200 text/plain; charset=utf-8
== сол файлмен қайта қосу
сол құпиясөзбен кіру: 303 — адам орнында
== тегін тарифтегі деплой: диск өшірілді
блогтың өзі: 200 — тірі
басты беттегі мақала: 3 — себінді, сіздікі емес
сол құпиясөзбен кіру: 401 — адам жоқ
Үшінші блок — осы сабақтың жазылу себебі. Блог тірі, ал адам жоқ.
Қарыздар. Блог әлі күнге дейін ұқыпты тоқтай алмайды: хостинг сигнал жібереді, ал біз сұраныстарды жартысынан үзіп тастаймыз — бұл systemd сабағы. Өз домені мен HTTPS-і де жоқ — келесі сабақ.
Жауаптар
Жауаптарды көрсету
- Өйткені
127.0.0.1— ілмек, машинаның өзімен сөйлесуі; желіден келген пакет ол жерге түспейді. Барлық мекенжайды тыңдау керек — порттың алдында хостсыз қос нүкте. - Өйткені баяу немесе бос емес дерекқор сау сервисті өліге ұқсатады, әрі хостинг жұмыс істеп тұрған бағдарламаны қайта қосады. Тірілік тексеруі тез жауап беруі әрі ештеңеге тәуелді болмауы керек; дайындықты, керек болса, бөлек мекенжаймен тексереді.
- Ол контейнердің дискісімен бірге жоғалады: блог таза күйінде көтеріледі, мақалалар себіндіден қайтады, ал оқырмандар жазғанның бәрі жоғалады.
Пікірлер (0)
Пікір қалдыру үшін кіріңіз →
Әзірге пікір жоқ. Бірінші болыңыз.