Результаты предварительного и полуфинального конкурса Tianchi Middleware Performance Competition точно第五名
, Неожиданно, так как Golang является «дефицитным видом» в этом соревновании, на этот раз мне посчастливилось выжить между боссом C и боссом Java в первой десятке.
Что касается сложности этого предварительного раунда «Сервисная сетка для Dubbo», сложность относительно проста по сравнению с полуфиналом «Проектирование хранилища одной машины с миллионной очередью сообщений», и окончательный результат таков:6983分
, потому что некоторые друзья Golang в основном застряли на отметке 6000 баллов во время одновременного стресс-теста 512 в официальном соревновании.Здесь я в основном делюсь с вами некоторыми из своего опыта и подводных камней в этой версии Golang.
По рабочим причинам конкурс проводится только по выходным.В следующей статье я найду время, чтобы разобраться с идеями и планами «Дизайн хранилища одномашинной очереди сообщений на миллион» для полуфинала. Я лично чувствую, что план реализации также в финальной команде довольно особенный.
Что такое сервисная сетка?
В Service Mesh используется другой подход: процесс реализации управления сервисом не требует изменения самого сервиса. Через агента, развернутого в виде прокси или sidecar, весь трафик, входящий и исходящий из службы, будет перехватываться и обрабатываться агентом, так что различные возможности управления службами в сценарии микрослужбы могут выполняться агентом, что значительно снижает стоимость службы. преобразование, сложность и стоимость. Более того, в качестве посредника между двумя сервисами Агент также может играть роль преобразования протоколов, что позволяет сервисам, основанным на разных технических платформах и протоколах связи, достигать взаимосвязи, что сложно реализовать в рамках традиционной микросервисной инфраструктуры.
На следующем рисунке представлена официальная оценочная структура.Вся сцена состоит из 5 экземпляров Docker (синие прямоугольники), которые запускают etcd, Consumer, Provider service и Agent соответственно. Поставщик является поставщиком услуг, Потребитель является потребителем услуг, а Потребитель потребляет услуги, предоставляемые Поставщиком. Агент является агентом услуг Потребителя и Поставщика, и каждого Потребителя или Поставщика будет сопровождать Агент. etcd — это служба реестра, которая записывает информацию о регистрации службы. Как видно из рисунка, общение между Потребителем и Провайдером осуществляется не напрямую, а через Агента. Эта, казалось бы, избыточная связь привнесла важные изменения в эволюцию микросервисной архитектуры.
Дополнительные сведения о Service Mesh см. в следующих статьях:
- What Is a Service Mesh?
- Что такое сервисная сетка и зачем она мне нужна? (китайский перевод)
- Рассказываем о новом поколении микросервисной технологии Service Mesh
Требования к вызову
- Регистрация и обнаружение службы
- Преобразование протоколов (это также ключ к реализации взаимосвязи между разными языками и разными фреймворками)
- балансировки нагрузки
- Ограничение тока, деградация, плавкие предохранители, сертификация безопасности (не требуется)
Конечно, самым важным в Agent Proxy является его универсальность и масштабируемость: он может поддерживать больше служб приложений, добавляя различные преобразования протоколов.最后Agent Proxy的资源占用率一定要小,因为Agent与服务是共生的,服务一旦失去响应,Agent即使拥有再好的性能也是没有意义的。
Почему голанг?
Лично я думаю, что выбор Service Mesh определенно будет между Cpp и Golang, что должно относиться к стеку технологий компании. Если вы стремитесь к максимальной производительности, Cpp по-прежнему является первым выбором, который может избежать проблемы Gc. Поскольку ссылки Service Mesh длиннее, чем традиционные Rpcs, прокси-сервер агента должен быть легким, стабильным и хорошо работать.
Почему Golang занимается выбором технологий? Это не просто возможность попрактиковаться в собственном Голанге, конечно, по следующим причинам:
- Накоплен опыт некоторых крупных фабрик, таких как Ant Sofa Mesh, Sina Motan Mesh и др.
- K8s и docker очень популярны в сфере микросервисов, а развертывание агентов в будущем должно опираться на k8s, поэтому Go — хороший выбор с высокой аффинностью.
- Go имеет сопрограммы и высококачественные сетевые библиотеки и должен иметь преимущество в плане высокой производительности.
Анализ точек оптимизации
Официальный предоставляет демо-версию Java на основе Netty.Поскольку это блокирующая версия, производительность невелика.Конечно, это также благо для игроков Java, и вы можете быстро начать работу. Другие языки запускаются относительно медленно, и все приходится заново реализовывать сами по себе.
Независимо от языка, большинство идей по оптимизации одинаковы. Вот очень подробное изложение идей Кирито Сюй Цзинфэна (версия для Java):Итоги оптимизации dubboMesh на конкурсе промежуточного программного обеспечения Tianchi (количество запросов в секунду от 1000 до 6850), вы можете использовать его в качестве ссылки.
На следующем рисунке в основном показаны все работы по оптимизации во всем агенте Зеленые стрелки на рисунке — это все, что пользователи могут реализовать самостоятельно.
-
Весь процесс становится
异步非阻塞、无锁
, все запросы принимают форму асинхронных обратных вызовов. Это также самое большое улучшение. -
Реализуйте синтаксический анализ Http-сервиса самостоятельно.
-
Для связи между агентами используется простейший пользовательский протокол.
-
передача по сети
ByteBuffer复用
. -
Связь между агентами
批量打包
Отправить.
ForBlock: for { httpReqList[reqCount] = req agentReqList[reqCount] = &AgentRequest{ Interf: req.interf, Method: req.callMethod, ParamType: ParamType_String, Param: []byte(req.parameter), } reqCount++ if reqCount == *config.HttpMergeCountMax { break } select { case req = <-workerQueue: default: break ForBlock } } ```
-
Балансировка нагрузки провайдера: циклический взвешенный алгоритм,
最小响应时间
(эффект не очень заметен) -
Балансировка нагрузки соединения Tcp: поддерживает выбор соединений Tcp в соответствии с минимальным количеством запросов.
-
Даббо запрос
批量encode
. -
Оптимизация параметров Tcp: включите TCP_NODELAY (отключите алгоритм Nagle) и отрегулируйте размер буфера Tcp для отправки, чтения и записи.
if err = syscall.SetsockoptInt(fd, syscall.IPPROTO_TCP, syscall.TCP_NODELAY, *config.Nodelay); err != nil { logger.Error("cannot disable Nagle's algorithm", err) } if err := syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_SNDBUF, *config.TCPSendBuffer); err != nil { logger.Error("set sendbuf fail", err) } if err := syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_RCVBUF, *config.TCPRecvBuffer); err != nil { logger.Error("set recvbuf fail", err) }
Горькая история сети —— (прогрев игры 256 одновременный стресс-тест 4400~4500)
Поскольку в Go есть сопрограммы и высококачественные сетевые библиотеки, стоимость переключения сопрограмм невелика, поэтому в большинстве сценариев рекомендуемый метод сетевой игры Go заключается в том, что каждое соединение использует соответствующую сопрограмму для чтения и записи.
Эта версия сетевой модели также дала относительно объективные результаты, при этом максимальное количество запросов в секунду составляет около 4400–4500. Краткое резюме этого выбора сети сделано:
- Поскольку в Go есть горутины, для решения проблем параллелизма можно использовать несколько сопрограмм.
- Сетевая библиотека Go в Linux также использует epoll в качестве драйвера приемопередатчика данных нижнего уровня.
- В базовой реализации сети Go также есть работа по «переключению контекста», но работа по переключению выполняется планировщиком времени выполнения.
Горькая история сети —— (официальная игра 512 одновременного стресс-теста)
Тем не менее, наша программа не добилась устойчивого улучшения в 512 одновременных стресс-тестах в официальном соревновании, около 5500 ~ 5600,cpu的资源占用率也是比较高的,高达约100%
.
Разбор секрета получения высокого балла:
- Давление на агента-потребителя сильное, поэтому уменьшите давление на агента-потребителя.
- Из-за низкой производительности Потребителя Потребитель и Агент Потребителя сосуществуют в экземпляре Docker (4C 8G) Максимальная производительность может быть достигнута только за счет предотвращения конкуренции за ресурсы.
- Во время стресс-теста загрузка ЦП потребителя достигает примерно 350%.
- Чтобы избежать конкуренции за ресурсы с Потребителем, использование ресурсов Агента Потребителя должно быть сведено к минимуму.
С помощью приведенного выше анализа мы определили основные цели оптимизации:尽可能降低Consumer Agent的资源开销
.
А. План оптимизации 1: пул сопрограмм + очередь задач (заброшена)
Это относительно простая и часто используемая идея оптимизации, похожая на пулы потоков. Несмотря на то, что произошел прорыв, он не дал желаемого эффекта, и загрузка ЦП по-прежнему составляет около 70–80%. Хотя накладные расходы горутины очень малы, в конце концов, переключение контекста в ситуациях с высокой степенью параллелизма все еще требует определенных затрат, и мы можем найти только способы найти некоторые прорывы в производительности.
经过慎重思考,我最终还是决定尝试采用类似netty的reactor网络模型
. Изучение архитектуры Netty не будет повторяться здесь, и рекомендуется поделиться некоторыми резюме коллег.Блог Флэша.
б. Схема оптимизации 2: Сетевая модель реактора
Перед выбором я посоветовался с несколькими хорошими знакомыми, и все они жаловались. Конечно, они не могли понять дилемму, что у меня было менее 50% доступных ресурсов процессора, и в конце концов решительно пошли по этому альтернативному пути.
После несложного поиска я нашел стороннюю библиотеку с открытым исходным кодом, которая выглядит вполне надежной (Github Star2000, без PR).evio, но на практике ям слишком много, а функция очень простая. Я не могу отделаться от ощущения, что Java действительно счастлива иметь Netty! Причина успеха Java в том, что его экология настолько зрелая, а языку Go еще нужно время на закалку, а качественных ресурсов слишком мало.
Конечно, полностью отказаться от evio нельзя, его можно использовать как хороший ресурс для изучения сети. Давайте взглянем на простое введение функции на Github:
evio is an event loop networking framework that is fast and small. It makes direct epoll and kqueue syscalls rather than using the standard Go net package, and works in a similar manner as libuv and libevent.
说明:关于kqueue是FreeBSD上的一种的多路复用机制,推荐学习。
Для достижения максимальной производительности я внес множество изменений в evio:
- Поддерживается активное подключение (по умолчанию поддерживается только пассивное подключение)
- Поддерживает несколько протоколов
- Уменьшите количество недействительных пробуждений
- Поддержка асинхронной записи для повышения пропускной способности
- Исправление проблем с производительностью, вызванных многими ошибками в Linux.
Сетевая модель после преобразования также добилась хороших результатов, которых можно достичь6700+
, но этого далеко недостаточно, и нужно найти какие-то прорывы.
в. Повторное использование EventLoop
Еще раз разберем оптимизированный сетевой режим (см. рис. ниже):
EventLoop можно понимать как поток ввода-вывода.До этого каждая сетевая коммуникация c->ca, ca->pa и pa->p использовала eventLoop отдельно.如果入站的io协程和出站的io协程使用相同的协程,可以进一步降低Cpu切换的开销
. Итак, я сделал последнюю оптимизацию сетевой модели:复用EventLoop
и обрабатывать различные логические запросы, оценивая тип соединения.
func CreateAgentEvent(loops int, workerQueues []chan *AgentRequest, processorsNum uint64) *Events {
events := &Events{}
events.NumLoops = loops
events.Serving = func(srv Server) (action Action) {
logger.Info("agent server started (loops: %d)", srv.NumLoops)
return
}
events.Opened = func(c Conn) (out []byte, opts Options, action Action) {
if c.GetConnType() != config.ConnTypeAgent {
return GlobalLocalDubboAgent.events.Opened(c)
}
lastCtx := c.Context()
if lastCtx == nil {
c.SetContext(&AgentContext{})
}
opts.ReuseInputBuffer = true
logger.Info("agent opened: laddr: %v: raddr: %v", c.LocalAddr(), c.RemoteAddr())
return
}
events.Closed = func(c Conn, err error) (action Action) {
if c.GetConnType() != config.ConnTypeAgent {
return GlobalLocalDubboAgent.events.Closed(c, err)
}
logger.Info("agent closed: %s: %s", c.LocalAddr(), c.RemoteAddr())
return
}
events.Data = func(c Conn, in []byte) (out []byte, action Action) {
if c.GetConnType() != config.ConnTypeAgent {
return GlobalLocalDubboAgent.events.Data(c, in)
}
if in == nil {
return
}
agentContext := c.Context().(*AgentContext)
data := agentContext.is.Begin(in)
for {
if len(data) > 0 {
if agentContext.req == nil {
agentContext.req = &AgentRequest{}
agentContext.req.conn = c
}
} else {
break
}
leftover, err, ready := parseAgentReq(data, agentContext.req)
if err != nil {
action = Close
break
} else if !ready {
data = leftover
break
}
index := agentContext.req.RequestID % processorsNum
workerQueues[index] <- agentContext.req
agentContext.req = nil
data = leftover
}
agentContext.is.End(data)
return
}
return events
}
Повторное использование цикла обработки событий привело к относительно стабильному повышению производительности.Количество ресурсов цикла обработки событий на каждом этапе установлено равным 1, а уровень занятости ресурсов ЦП при финальном стресс-тесте 512 одновременных операций составляет около 50%.
Некоторые попытки оптимизации на уровне языка Go
На финальном этапе я могу только бешено искать некоторые детали, поэтому я также сделал несколько попыток на уровне языка:
- Кольцевой буфер вместо канала Go для распределения задач
RingBuffer имеет немного лучшую производительность, чем Channel, в сценарии с большим числом одновременных распределений задач, но с инженерной точки зрения я все же рекомендую более элегантный подход канала Go.
- Пакет encoding/json, поставляемый с Go, реализован на основе отражения, и его производительность вызывает критику.
Используйте строки для самостоятельной сборки данных Json, так что чем больше данных вам нужно протестировать, тем больше времени вы сможете сэкономить.
-
Связывание потоков горутин
runtime.LockOSThread() defer runtime.UnlockOSThread()
-
Измените размер временного интервала планировщика по умолчанию и скомпилируйте язык Go самостоятельно (без эффекта)
Суммировать
- Меч взял уклон и потратил много времени на преобразование сети.Усилия окупились, и результат порадовал.
- Golang достаточно хорош с точки зрения высокой производительности, и его стоит изучить досконально.
-
性能优化离不开的一些套路:异步、去锁、复用、零拷贝、批量等
.
Наконец, я выбрасываю несколько проблем с сетью Go, которые я хочу продолжить обсуждать, и обсудить со всеми Опытные друзья также надеюсь дать некоторые подсказки:
- В случае ограниченных ресурсов, как бы вы выбрали сетевую модель для обработки большого количества одновременных запросов? (Предположим, что параллелизм составляет 1 Вт длинного или короткого соединения)
- Как будет проходить отбор в масштабе одного миллиона подключений?
Пожалуйста, укажите источник перепечатки, прошу обратить внимание на мой публичный номер: техническое колесо Япу