Shanraq.org Shanraq.org
Блогты адамдарға жеткізу: тегін хостинг
IT

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 тарихы бәрін есінде сақтайды.

Сабақ картасы

Сабақ картасы: порт, интерфейс және диск

Өз сөзіңізбен айтыңыз

Қарамай, дауыстап немесе қағазға жауап беріңіз. Жауаптар — сабақтың соңында.

  1. 127.0.0.1 тыңдайтын сервис неге сырттан жауап бермейді?
  2. /healthz неге дерекқорға бармауы керек?
  3. Тегін тарифте деплой кезінде 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-і де жоқ — келесі сабақ.

Жауаптар

Жауаптарды көрсету
  1. Өйткені 127.0.0.1 — ілмек, машинаның өзімен сөйлесуі; желіден келген пакет ол жерге түспейді. Барлық мекенжайды тыңдау керек — порттың алдында хостсыз қос нүкте.
  2. Өйткені баяу немесе бос емес дерекқор сау сервисті өліге ұқсатады, әрі хостинг жұмыс істеп тұрған бағдарламаны қайта қосады. Тірілік тексеруі тез жауап беруі әрі ештеңеге тәуелді болмауы керек; дайындықты, керек болса, бөлек мекенжаймен тексереді.
  3. Ол контейнердің дискісімен бірге жоғалады: блог таза күйінде көтеріледі, мақалалар себіндіден қайтады, ал оқырмандар жазғанның бәрі жоғалады.

Дереккөздер

Мәтінде қате не теру қатесі кездессе, бізге айтыңыз

Тапсырманы тексеру

Алдымен VS Code-та шешіп, іске қосыңыз — редактор қатені сол жерде көрсетеді. Дайын шешімді осында қойыңыз. Тексеретін — модель: ол қатені атап көрсетеді, бірақ дайын жауапты бермейді.

Тексеру үшін кіру керек. Кіру

Пікірлер (0)

Әзірге пікір жоқ. Бірінші болыңыз.