Что такое изящный перезапуск
Когда онлайн-код необходимо обновить, наша обычная практика заключается в том, чтобы сначала закрыть службу, а затем перезапустить службу.В это время может обрабатываться большое количество онлайн-запросов.В это время, если мы напрямую закроем службу , все запросы будут прерваны, что повлияет на работу пользователя. ; Перед повторным перезапуском службы новые запросы будут поступать с кодом 502. На этом этапе необходимо решить две проблемы:
- Запрос, обрабатываемый старой службой, должен быть обработан до того, как он сможет выйти (изящный выход).
- Новые входящие запросы должны нормально обрабатываться, а службы не могут быть прерваны (плавный перезапуск)
В этой статье в основном объединены соответствующие реализации в Linux и Golang, чтобы показать, как выбирать и практиковать процесс.
изящно выйти
Одна из первых проблем, которую необходимо решить перед реализацией корректного перезапуска, заключается в том, как корректно завершить работу:
Мы знаем, что после go 1.8.x golang добавил метод shutdown в http для управления корректным выходом.
Многие библиотеки HTTP изящного динамического перезапуска и плавного перезапуска в сообществе в основном основаны на http.shutdown.
Анализ исходного кода отключения http
Давайте сначала посмотрим на логику реализации основного метода закрытия http. Используйте atomic в качестве состояния маркера выхода, затем закройте различные ресурсы, а затем заблокируйте все время ожидания бездействующих подключений, опрашивая каждые 500 мс.
var shutdownPollInterval = 500 * time.Millisecond
func (srv *Server) Shutdown(ctx context.Context) error {
// 标记退出的状态
atomic.StoreInt32(&srv.inShutdown, 1)
srv.mu.Lock()
// 关闭listen fd,新连接无法建立。
lnerr := srv.closeListenersLocked()
// 把server.go的done chan给close掉,通知等待的worekr退出
srv.closeDoneChanLocked()
// 执行回调方法,我们可以注册shutdown的回调方法
for _, f := range srv.onShutdown {
go f()
}
// 每500ms来检查下,是否没有空闲的连接了,或者监听上游传递的ctx上下文。
ticker := time.NewTicker(shutdownPollInterval)
defer ticker.Stop()
for {
if srv.closeIdleConns() {
return lnerr
}
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
}
}
}
…
是否没有空闲的连接
func (s *Server) closeIdleConns() bool {
s.mu.Lock()
defer s.mu.Unlock()
quiescent := true
for c := range s.activeConn {
st, unixSec := c.getState()
if st == StateNew && unixSec < time.Now().Unix()-5 {
st = StateIdle
}
if st != StateIdle || unixSec == 0 {
quiescent = false
continue
}
c.rwc.Close()
delete(s.activeConn, c)
}
return quiescent
}
Закройте server.doneChan и дескриптор прослушиваемого файла.
// 关闭doen chan
func (s *Server) closeDoneChanLocked() {
ch := s.getDoneChanLocked()
select {
case <-ch:
// Already closed. Don't close again.
default:
// Safe to close here. We're the only closer, guarded
// by s.mu.
close(ch)
}
}
// 关闭监听的fd
func (s *Server) closeListenersLocked() error {
var err error
for ln := range s.listeners {
if cerr := (*ln).Close(); cerr != nil && err == nil {
err = cerr
}
delete(s.listeners, ln)
}
return err
}
// 关闭连接
func (c *conn) Close() error {
if !c.ok() {
return syscall.EINVAL
}
err := c.fd.Close()
if err != nil {
err = &OpError{Op: "close", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err}
}
return err
}
После такой серии операций метод слушателя serv main сервера server.go также завершает работу.
func (srv *Server) Serve(l net.Listener) error {
...
for {
rw, e := l.Accept()
if e != nil {
select {
// 退出
case <-srv.getDoneChan():
return ErrServerClosed
default:
}
...
return e
}
tempDelay = 0
c := srv.newConn(rw)
c.setState(c.rwc, StateNew) // before Serve can return
go c.serve(ctx)
}
}
Так как же гарантировать, что пользователь закроет соединение после завершения запроса?
func (s *Server) doKeepAlives() bool {
return atomic.LoadInt32(&s.disableKeepAlives) == 0 && !s.shuttingDown()
}
// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
defer func() {
... xiaorui.cc ...
if !c.hijacked() {
// 关闭连接,并且标记退出
c.close()
c.setState(c.rwc, StateClosed)
}
}()
...
ctx, cancelCtx := context.WithCancel(ctx)
c.cancelCtx = cancelCtx
defer cancelCtx()
c.r = &connReader{conn: c}
c.bufr = newBufioReader(c.r)
c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)
for {
// 接收请求
w, err := c.readRequest(ctx)
if c.r.remain != c.server.initialReadLimitSize() {
c.setState(c.rwc, StateActive)
}
...
...
// 匹配路由及回调处理方法
serverHandler{c.server}.ServeHTTP(w, w.req)
w.cancelCtx()
if c.hijacked() {
return
}
...
// 判断是否在shutdown mode, 选择退出
if !w.conn.server.doKeepAlives() {
return
}
}
...
изящный перезапуск
Эволюция метода
С точки зрения системы Linux
- Использовать напрямую
exec
, заменить сегмент кода кодом новой программы, отбросить исходный сегмент данных и сегмент стека и выделить новый сегмент данных и сегмент стека для новой программы, останется только номер процесса.
Проблема, которая будет существовать таким образом, заключается в том, что старый процесс не может корректно завершиться, а запрос, обрабатываемый старым процессом, не может быть обработан нормально, а затем завершается.
И запуск службы нового процесса не мгновенный, новый процессlisten
послеaccept
Раньше новые соединения могли бытьsyn queue
Отклоняется, когда очередь заполнена (это случается редко, но возможно при высоком уровне параллелизма). Объединив процесс трехэтапного рукопожатия с TCP на рисунке ниже, возможно, будет легче понять, Лично я чувствую ясность.
- пройти через
fork
Заднийexec
создать новый процесс,exec
ранее переданный в старом процессеfcntl(fd, F_SETFD, 0);
чистыйFD_CLOEXEC
знак, послеexec
Новый процесс унаследует fd старого процесса и сможет использовать его напрямую.
После нового процесса и старого процессаlisten
Один и тот же fd предоставляет услуги в то же время.После того, как новый процесс запускает службу в обычном режиме, сигнал отправляется старому процессу, и старый процесс корректно завершается.
После того, как все запросы поступят в новый процесс, этот изящный перезапуск завершается. В сочетании с проблемами, существующими в реальной онлайн-среде: в это время новый дочерний процесс изменит свой родительский процесс на процесс № 1 из-за выхода родительского процесса, поскольку большинство служб в онлайн-среде обрабатываются черезsupervisor
управления, будет проблема,supervisor
Он подумает, что служба аварийно завершила работу, и перезапустит новый процесс.
-
Установив файловый дескриптор
SO_REUSEPORT
Флаг заставляет два процесса прослушивать один и тот же порт Проблема здесь в том, что здесь используются два разных FD для прослушивания одного и того же порта, когда старый процесс завершается.syn queue
Соединения в очереди, которые не были приняты, будут уничтожены ядром. -
пройти через
ancilliary data
Системные вызовы используют сокеты домена UNIX для передачи файловых дескрипторов между процессами, что также обеспечивает плавный перезапуск. Но эта реализация будет сложнее,HAProxy
Модель реализована в. -
непосредственный
fork
потомexec
Вызванный дочерний процесс наследует все файловые дескрипторы, открытые родительским процессом, а файловые дескрипторы, полученные дочерним процессом, увеличиваются с 3, а порядок совпадает с порядком открытия родительского процесса. дочерний процесс черезepoll_ctl
Зарегистрируйте fd и зарегистрируйте обработчик событий (здесь в качестве примера используется модель epoll), чтобы дочерний процесс мог прослушивать запросы с того же порта, что и родительский процесс (в это время родительский и дочерний процессы предоставляют услуги в то же время). Когда дочерний процесс запускается нормально и предоставляет услуги, отправьтеSIGHUP
Для родительского процесса, когда родительский процесс корректно завершается, дочерний процесс предоставляет услуги и завершает корректный перезапуск.
Реализация на Голанге
Из вышеизложенного относительно простой реализацией является прямаяfork
andexec
Способ самый простой, далее будет рассмотрена конкретная реализация на Golang.
Мы знаем, что fd сокета в Golang установлен по умолчанию.FD_CLOEXEC
флаги (net/sys_cloexec.goСправочный исходный код)
// Wrapper around the socket system call that marks the returned file
// descriptor as nonblocking and close-on-exec.
func sysSocket(family, sotype, proto int) (int, error) {
// See ../syscall/exec_unix.go for description of ForkLock.
syscall.ForkLock.RLock()
s, err := socketFunc(family, sotype, proto)
if err == nil {
syscall.CloseOnExec(s)
}
syscall.ForkLock.RUnlock()
if err != nil {
return -1, os.NewSyscallError("socket", err)
}
if err = syscall.SetNonblock(s, true); err != nil {
poll.CloseFunc(s)
return -1, os.NewSyscallError("setnonblock", err)
}
return s, nil
}
так вexec
После того, как фд будет закрыт системой, но мы можем напрямую пройтиos.Command
реализовать.
Некоторые люди здесь могут быть немного сбиты с толку, верно?FD_CLOEXEC
Если флаг установлен, fd, унаследованный только что запущенным дочерним процессом, будет закрыт.
Дело в томos.Command
Запущенный дочерний процесс может наследовать fd родительского процесса и использовать его, мы можем узнать, прочитав исходный кодos.Command
прошедшийStdout
,Stdin
,Stderr
а такжеExtraFiles
Переданный дескриптор будет очищен Golang по умолчанию.FD_CLOEXEC
войти черезStart
Метод возвращается, и мы можем подтвердить нашу идею. (syscall/exec_{GOOS}.go здесь — исходный код реализации macosСправочный исходный код)
// dup2(i, i) won't clear close-on-exec flag on Linux,
// probably not elsewhere either.
_, _, err1 = rawSyscall(funcPC(libc_fcntl_trampoline), uintptr(fd[i]), F_SETFD, 0)
if err1 != 0 {
goto childerror
}
Проблемы при совмещении супервайзера
В реальных проектах онлайн-сервисы обычно запускаются супервизором.Как упоминалось выше, если мы передаем родительский-дочерний процесс и выходим из родительского процесса после запуска дочернего процесса, проблема заключается в том, что дочерний процесс будет передан No. .1 процесс, в результате чего супервизор.Считается, что служба зависает и перезапускает службу.Чтобы избежать этой проблемы, мы можем использовать методы master и worker.
Основная идея этого метода такова: при запуске проекта программа запускается как мастер и слушает порт для создания дескрипторов сокета, но не предоставляет услуги внешнему миру, а затем проходитos.Command
Создайте дочерний процесс черезStdin
, Stdout
, Stderr
,ExtraFiles
иEnv
Передайте стандартные ошибки ввода и вывода, файловые дескрипторы и переменные среды.Через переменные среды дочерний процесс может узнать, что он является дочерним процессом, и передатьos.NewFile
зарегистрировать фд наepoll
Среда, созданная через fdTCPListener
объект, привязкаhandle
после процессораaccept
Примите запрос и обработайте его, обратитесь к псевдокоду:
f := os.NewFile(uintptr(3+i), "")
l, err := net.FileListener(f)
if err != nil {
return fmt.Errorf("failed to inherit file descriptor: %d", i)
}
server:=&http.Server{Handler: handler}
server.Serve(l)
Вышеупомянутый процесс просто запускает рабочий процесс и предоставляет услуги.Настоящий изящный перезапуск может быть выполнен через интерфейс (поскольку онлайн-среда может не иметь разрешения на публикацию машины, поэтому она может только спасти страну) или отправить сигнал в рабочий процесс, а рабочий отправляет сигнал мастеру и мастер-процессам.После получения сигнала запускается новый рабочий процесс, новый рабочий процесс запускается и предоставляет услуги в обычном режиме и отправляет сигнал мастеру, мастер отправляет сигнал выхода на старый рабочий, и старый рабочий уходит.
Проблема сбора логов, если лог самого проекта напрямую выводится в файл, то могут быть проблемы типа fd rolling (на данный момент досконально не исследовано).Текущее решение - выводить весь лог проекта на stdout, а супервизор соберет лог-файл и создаст воркер.Когда stdout и stderr могут быть унаследованы от прошлого, это решает проблему логирования.Если есть лучший способ обсудить среду вместе.
Справочная статья
Поговорите о вводном понимании сетевой библиотеки golang. Глубокое понимание невыполненной работы Linux TCP перейти к исследованию инструмента элегантного обновления/перезапуска Вспомните шокирующий опыт устранения неполадок с очередями TCP веб-сайта. Разница между accept и accept4