Практика ByteDance в сетевой библиотеке Go

Архитектура
Практика ByteDance в сетевой библиотеке Go

Эта статья выбрана из серии статей «Практика инфраструктуры Byte Beat».

Серия статей «ByteDance Infrastructure Practice» представляет собой техническую галантерею, созданную техническими командами и экспертами отдела инфраструктуры ByteDance, в которой мы делимся с вами практическим опытом и уроками команды в процессе развития и эволюции инфраструктуры, а также всеми техническими студентами. общаться и расти вместе.

Являясь важной частью системы исследований и разработок, структура RPC несет почти весь служебный трафик. В этой статье мы кратко познакомим вас с дизайном и применением собственной сетевой библиотеки netpoll, разработанной ByteDance, а также с проблемами, с которыми мы столкнулись, и их решениями, в надежде предоставить вам некоторую справку.

предисловие

Группа ByteDance Framework в основном отвечает за разработку и поддержку инфраструктуры RPC внутри компании. Являясь важной частью системы исследований и разработок, структура RPC несет почти весь служебный трафик. С ростом использования языка Go в компании бизнес-требования к фреймворку становятся все выше и выше, но нативная сетевая библиотека Go не может обеспечить достаточную производительность и контроль, такие как неспособность воспринимать статус соединения и коэффициент использования из-за большого количества подключений Низкий, невозможно контролировать количество сопрограмм и т. д. Чтобы получить полный контроль над сетевым уровнем и в то же время провести некоторые исследования перед бизнесом и, наконец, включить бизнес, команда фреймворка запустила новую сетевую библиотеку собственной разработки на основе epoll — netpoll и разработала байт на основе Это внутри нового поколения фреймворка Golang KiteX.

Поскольку принцип epoll был описан во многих статьях, в этой статье мы лишь кратко познакомим вас со структурой netpoll, затем мы попытаемся разобраться в некоторых практиках, которые мы сделали на основе netpoll, и, наконец, поделимся проблемой, с которой мы столкнулись, и как мы решили это идеи. В то же время к нам могут присоединиться студенты, интересующиеся языком и фреймворком Go!

Новый дизайн сетевой библиотеки

Reactor — ядро ​​мониторинга и диспетчеризации событий

Ядром netpoll является планировщик прослушивателя событий Reactor.Основная функция заключается в использовании epoll для прослушивания файлового дескриптора (fd) соединения и запуска трех событий чтения, записи и закрытия соединения с помощью механизма обратного вызова.

Сервер - реализация Reactor master-slave

netpoll объединяет Reactor в режиме ведущий-ведомый в формате 1:N.

  1. MainReactor в основном управляет прослушивателями, которые отвечают за прослушивание портов и установление новых соединений;
  2. SubReactor отвечает за управление подключениями, мониторинг всех назначенных подключений и отправку всех инициированных событий в пул сопрограмм для обработки.
  3. Netpoll представляет активное управление памятью в задаче ввода-вывода, предоставляет интерфейс вызова NoCopy для верхнего уровня и, таким образом, поддерживает NoCopy RPC.
  4. Используйте пул сопрограмм для централизованной обработки задач ввода-вывода, чтобы уменьшить количество сопрограмм и накладные расходы на планирование.

Клиент — возможности Shared Reactor

Сторона клиента и сторона сервера совместно используют SubReactor, а netpoll также реализует номеронабиратель, обеспечивающий возможность создания соединений. Подобно net.Conn на стороне клиента, netpoll обеспечивает низкоуровневую поддержку обратного вызова записи -> ожидания чтения.

Nocopy Buffer

Зачем вам нужен Nocopy Buffer?

В дизайне Reactor и I/O Task, упомянутом выше, метод запуска epoll повлияет на дизайн I/O и буфера, который можно грубо разделить на два способа:

  1. При горизонтальном запуске (LT) вам необходимо синхронно завершить ввод-вывод после запуска события и предоставить буферы непосредственно коду верхнего уровня.
  2. С запуском по фронту (ET) вы можете управлять только уведомлениями о событиях (такими как проектирование сети), а код верхнего уровня завершает ввод-вывод и управляет буферами.

Оба метода имеют свои преимущества и недостатки.Netpoll использует первую стратегию, которая имеет лучшую своевременность горизонтального запуска, высокую отказоустойчивость, активный ввод-вывод может централизовать использование памяти и управление, обеспечивать операции без копирования и уменьшать сборщик мусора. Фактически, некоторые популярные сетевые библиотеки с открытым исходным кодом также разработаны с использованием первого метода, например, easygo, evio, gnet и т. д.

Но использование LT также создает другую проблему, а именно: лежащий в основе активный ввод-вывод и код верхнего уровня одновременно обрабатывают буфер, что приводит к дополнительным издержкам параллелизма. Например: буфер чтения данных ввода/вывода и буфер чтения верхнего кода имеют одновременное чтение и запись, и наоборот. Чтобы обеспечить правильность данных и не вводить конкуренцию блокировок, существующие сетевые библиотеки с открытым исходным кодом обычно используют такие методы, как синхронная обработка буфера (easygo, evio) или копирование буфера и предоставление его коду верхнего уровня (gnet ), которые не подходят для бизнес-обработки. Или возникают накладные расходы на копирование.

С другой стороны, общие библиотеки буферов, такие как bytes, bufio и ringbuffer, имеют проблемы, связанные с тем, что для роста требуется копирование данных исходного массива, и они могут только расширяться, но не сжиматься, и занимают много памяти. Поэтому мы надеемся представить новую форму буфера для решения двух вышеуказанных проблем одним махом.

Дизайн и преимущества буфера Nocopy

Nocopy Buffer реализован на основе массива связанных списков.Как показано на рисунке ниже, мы абстрагируем массив []byte в блок и объединяем блоки в Nocopy Buffer в форме объединения связанных списков.Подсчет ссылок, nocopy API и пул объектов также представлены.

По сравнению с обычными байтами, bufio, кольцевым буфером и т. д. Nocopy Buffer имеет следующие преимущества:

  1. Чтение и запись без блокировки, поддержка потокового чтения и записи без копирования
    • Чтение и запись работают с указателями головы и хвоста отдельно, не мешая друг другу.
  2. Эффективное расширение и сжатие
    • На этапе расширения новый блок можно добавить сразу после хвостового указателя, не копируя исходный массив.
    • В фазе сжатия указатель головы будет непосредственно освобождать используемый узел блока, чтобы завершить сжатие. Каждый блок имеет независимый счетчик ссылок.Когда освобожденный блок больше не имеет ссылок, узел блока активно освобождается.
  3. Гибкий буфер нарезки и сплайсинга (функция связанного списка)
    • Поддерживает произвольное чтение сегментов (nocopy), а код верхнего уровня может обрабатывать сегменты потока данных параллельно без копирования, не заботясь о жизненном цикле, посредством подсчета ссылок GC.
    • Поддерживает произвольное сращивание (без копирования), а буфер записи поддерживает форму сращивания с хвостовым указателем через блок без копирования, чтобы гарантировать, что данные записываются только один раз.
  4. Объединение буферов Nocopy для уменьшения GC
    • Рассматривайте каждый массив []byte как узел блока, создавайте пул объектов для поддержки свободных блоков и повторно используйте блоки, чтобы уменьшить использование памяти и сборщик мусора.

На основе Nocopy Buffer мы реализуем Nocopy Thrift, который обеспечивает нулевое выделение и нулевое копирование памяти во время кодирования и декодирования.

мультиплексирование соединения

Вызовы RPC обычно принимают форму коротких соединений или длинных пулов соединений, и один вызов привязан к одному соединению за раз.Когда масштабы восходящего и нисходящего потоков велики, количество соединений в сети увеличивается со скоростью MxN, что приводит к огромному давлению на планирование, а накладные расходы на вычисления затрудняют управление услугами. Поэтому мы хотим ввести форму «параллельной обработки вызовов по одному постоянному соединению», чтобы уменьшить количество соединений в сети, схему, известную как «мультиплексирование соединений».

В настоящее время в отрасли существует несколько решений мультиплексирования соединений с открытым исходным кодом, которые ограничены ограничениями на уровне кода.Все эти решения требуют буферов копирования для реализации пакетирования и слияния данных, что приводит к неудовлетворительной фактической производительности. Вышеупомянутый буфер Nocopy, основанный на его гибких функциях нарезки и сплайсинга, поддерживает пакетирование и слияние данных nocopy, что позволяет реализовать высокопроизводительную схему мультиплексирования соединений.

Схема мультиплексирования соединений, основанная на netpoll, показана на рисунке ниже: мы абстрагируем Nocopy Buffer (и его сегменты) как виртуальные соединения, чтобы код верхнего уровня поддерживал тот же процесс вызова, что и net.Conn. В то же время в базовом коде данные о реальном соединении гибко выделяются для виртуального соединения посредством субподряда протокола или данные виртуального соединения объединяются и отправляются посредством кодирования протокола.

Схема мультиплексирования соединений содержит следующие основные элементы:

  1. виртуальное соединение

    • По сути, Nocopy Buffer предназначен для замены реального соединения и предотвращения копирования памяти.
    • Бизнес-логика/кодек верхнего уровня завершается в виртуальном соединении, и логика верхнего уровня может выполняться асинхронно и независимо параллельно.
  2. Shared map

    • Внедрите блокировку осколков, чтобы уменьшить силу блокировки.
    • На стороне вызывающей стороны используйте идентификатор последовательности, чтобы пометить запрос, и используйте блокировку сегмента, чтобы сохранить обратный вызов, соответствующий идентификатору.
    • Получив данные ответа, найдите соответствующий обратный вызов по идентификатору последовательности и выполните его.
  3. Пакетирование и кодирование протокола

    • То, как идентифицировать полный пакет данных «запрос-ответ», является ключом к осуществимости схемы мультиплексирования соединений, поэтому необходимо ввести протокол.
    • Здесь используется протокол протокола Thrift Header, целостность пакета данных оценивается по заголовку сообщения, а соответствующая связь между запросом и ответом отмечается идентификатором последовательности.

ZeroCopy

Упомянутая здесь функция ZeroCopy относится к возможности ZeroCopy, предоставляемой Linux. В предыдущей главе мы говорили о нулевой копии бизнес-уровня.Как мы все знаем, когда мы вызываем системный вызов sendmsg для отправки пакета, на самом деле все еще создается копия данных, и потребление этой копии очень очевиден в сценарии с большими пакетами. Взяв в качестве примера 100M, perf может увидеть следующие результаты:

Это просто занятость обычной отправки пакетов tcp.В нашем сценарии большинство сервисов будут подключены к Service Mesh, поэтому при отправке одного пакета будет всего 3 копии: бизнес-процесс в ядро, ядро ​​​​в sidecar, sidecar и затем в ядро. Это делает загрузку ЦП, вызванную копированием, особенно очевидной для предприятий с большими требованиями к пакетам, как показано на следующем рисунке:

Чтобы решить эту проблему, мы решили использовать API ZeroCopy, предоставляемый Linux (отправка поддерживается с версии 4.14; получение поддерживается с версии 5.4). Но это создает дополнительную инженерную проблему: API отправки ZeroCopy несовместим с исходным вызовом и не может сосуществовать должным образом. Вот краткое введение в рабочий метод отправки ZeroCopy: после того, как бизнес-процесс вызовет sendmsg, sendmsg запишет адрес iovec и немедленно вернется.В это время бизнес-процесс не может освободить эту память и должен ждать Ядро для обратного вызова сигнала через epoll, чтобы указать, что определенный сегмент iovec был отправлен. Он может быть выпущен только после успешной отправки. Поскольку мы не хотим менять метод использования бизнес-стороны, нам необходимо предоставить верхнему уровню интерфейс для синхронной отправки и получения, поэтому трудно обеспечить как ZeroCopy, так и не-ZeroCopy абстракции на основе существующего API; и поскольку ZeroCopy имеет потери производительности в сценариях с небольшими пакетами, поэтому вы не можете использовать его по умолчанию.

Таким образом, группа инфраструктуры ByteDance сотрудничает с группой ядра ByteDance, а группа ядра обеспечивает синхронный интерфейс: когда вызывается sendmsg, ядро ​​​​будет отслеживать и перехватывать обратный вызов, изначально переданный ядром бизнесу, и разрешать обратный вызов. быть завершена после завершения обратного вызова. Это позволяет нам легко получить доступ к отправке ZeroCopy без изменения исходной модели. В то же время группа ядра ByteDance также реализует ZeroCopy на основе сокета домена unix, что обеспечивает связь без копирования между бизнес-процессами и вспомогательными компонентами Mesh.

После использования отправки ZeroCopy perf видит, что ядро ​​больше не занято копированием:

Из значения занятости ЦП ZeroCopy может сэкономить половину ЦП по сравнению с не-ZeroCopy в сценарии с большим пакетом.

Совместное использование проблем с задержкой, вызванных планированием Go

На практике мы обнаружили, что, хотя наша недавно написанная сетевая библиотека превосходила нативную сетевую библиотеку Go с точки зрения средней задержки, задержки p99 и max в целом были немного выше, чем нативная сетевая библиотека Go, и пики были бы более очевидными, как показано на рисунке. ниже (Go 1.13, синий — это netpoll + мультиплексирование, зеленый — netpoll + длинное соединение, желтый — сетевая библиотека + длинное соединение):

Мы пробовали много способов оптимизации, но без особого успеха. В итоге мы выяснили, что эта задержка вызвана не накладными расходами самого netpoll, а планированием перехода, например:

  1. Поскольку в netpoll сам SubReactor также является горутиной, на которую влияет планирование, и нельзя гарантировать ее выполнение сразу после обратного вызова EpollWait, поэтому этот блок будет отложен;
  2. В то же время, поскольку SubReactor, используемый для обработки событий ввода-вывода, и MainReactor, используемый для обработки мониторинга соединений, также являются горутинами, на самом деле трудно гарантировать, что эти Reactor могут выполняться параллельно в случае многоядерности; даже в самых крайних случаях, Может быть, эти Реакторы будут висеть под тем же Р, и в итоге станут серийным исполнением, не способным в полной мере использовать преимущества многоядерности;
  3. Поскольку события ввода-вывода обрабатываются последовательно в SubReactor после обратного вызова EpollWait, у последнего события может быть проблема с длинным хвостом;
  4. В сценарии мультиплексирования соединений, поскольку каждое соединение привязано к SubReactor, задержка полностью зависит от планирования этого SubReactor, что приводит к более очевидным пикам.

Так как Go сделал специальные оптимизации для сетевой библиотеки во время выполнения, в сетевой библиотеке не будет описанной выше ситуации; в то же время сетевая библиотека представляет собой модель горутины для каждого соединения, поэтому она может гарантировать выполнение запросов. параллельно, не влияя друг на друга.

Для вышеуказанной проблемы у нас в настоящее время есть две идеи для решения:

  1. Измените исходный код среды выполнения Go, зарегистрируйте обратный вызов в среде выполнения Go, вызовите EpollWait каждый раз, когда он запланирован, и передайте полученный fd обратному вызову для выполнения;
  2. Работает с командой ядра ByteDance для поддержки одновременного пакетного чтения/записи нескольких подключений для решения последовательных проблем. Кроме того, после наших тестов Go 1.14 может сделать задержку немного ниже и стабильнее, но предел QPS, которого можно достичь, ниже. Мы надеемся, что наши идеи могут послужить ориентиром для студентов отрасли, которые также сталкиваются с этой проблемой.

постскриптум

Надеюсь, что приведенный выше обмен может быть полезен сообществу. В то же время мы также ускоряем создание netpoll и KiteX, нового фреймворка на основе netpoll. Приглашаем всех заинтересованных студентов присоединиться к нам и вместе построить языковую экосистему Go!

использованная литература

  1. http://man7.org/linux/man-pages/man7/epoll.7.html
  2. https://golang.org/src/runtime/proc.go
  3. https://github.com/panjf2000/gnet
  4. https://github.com/tidwall/evio

поделиться больше

Инструменты трассировки ядра (2): отслеживание времени выполнения состояния ядра.

Избиение байтов, краткое изложение практики инженерии хаоса

Усеченный файл gdb coredump советы по устранению неполадок

Команда инфраструктуры ByteDance

Команда инфраструктуры ByteDance — важная команда, которая поддерживает бесперебойную работу множества пользовательских продуктов ByteDance, включая Douyin, Today's Toutiao, Xigua Video и Volcano Small Video, Стабильная разработка обеспечивает гарантии и импульс.

В компании команда инфраструктуры в основном отвечает за построение частного облака ByteDance, управление кластерами из десятков тысяч серверов, а также отвечает за десятки тысяч гибридных развертываний вычислений/хранилищ и гибридных развертываний онлайн/офлайн, поддерживая стабильное хранилище. нескольких массивных данных ЭП.

В культурном плане команда активно использует открытый исходный код и инновационные аппаратные и программные архитектуры. Мы давно набираем студентов по направлению инфраструктура.Подробности смотрите на job.bytedance.com ("Читать исходный текст" в конце статьи).Если вам интересно, вы можете связаться с guoxinyu .0372@bytedance.com.

Добро пожаловать в техническую команду ByteDance