Конкурс промежуточного программного обеспечения Tianchi Обмен идеями Mesh версии сервиса Golang

Java задняя часть Микросервисы Go

Результаты предварительного и полуфинального конкурса 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 см. в следующих статьях:

Требования к вызову

  • Регистрация и обнаружение службы
  • Преобразование протоколов (это также ключ к реализации взаимосвязи между разными языками и разными фреймворками)
  • балансировки нагрузки
  • Ограничение тока, деградация, плавкие предохранители, сертификация безопасности (не требуется)

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

Пожалуйста, укажите источник перепечатки, прошу обратить внимание на мой публичный номер: техническое колесо Япу

亚普的技术轮子