идти плавно перезапустить отбор исследований и проектную практику

Go

Оригинальная ссылка github

Что такое изящный перезапуск

Когда онлайн-код необходимо обновить, наша обычная практика заключается в том, чтобы сначала закрыть службу, а затем перезапустить службу.В это время может обрабатываться большое количество онлайн-запросов.В это время, если мы напрямую закроем службу , все запросы будут прерваны, что повлияет на работу пользователя. ; Перед повторным перезапуском службы новые запросы будут поступать с кодом 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 на рисунке ниже, возможно, будет легче понять, Лично я чувствую ясность.

image.png

  • пройти через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Для родительского процесса, когда родительский процесс корректно завершается, дочерний процесс предоставляет услуги и завершает корректный перезапуск.

Реализация на Голанге

Из вышеизложенного относительно простой реализацией является прямаяforkandexecСпособ самый простой, далее будет рассмотрена конкретная реализация на 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Создайте дочерний процесс черезStdinStdoutStderr,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 могут быть унаследованы от прошлого, это решает проблему логирования.Если есть лучший способ обсудить среду вместе.

Оригинальная ссылка github

Справочная статья

Поговорите о вводном понимании сетевой библиотеки golang. Глубокое понимание невыполненной работы Linux TCP перейти к исследованию инструмента элегантного обновления/перезапуска Вспомните шокирующий опыт устранения неполадок с очередями TCP веб-сайта. Разница между accept и accept4