задний план
Как мы все знаем, основным языком программирования серверной части сообщества Zhihu является Python.
В связи с быстрым ростом числа пользователей Zhihu и постоянным увеличением сложности бизнеса трафик основного бизнеса за последний год увеличился в несколько раз, а также увеличивается нагрузка на соответствующий сервер. По мере развития бизнеса мы обнаружили, что Python как динамически интерпретируемый язык постепенно обнажал проблемы, вызванные его низкой операционной эффективностью и высокими затратами на последующее обслуживание:
- менее эффективная работа. В настоящее время у Zhihu недостаточно места в компьютерном зале.В соответствии с текущими темпами роста пользователей и трафика можно предвидеть, что в краткосрочной перспективе ресурсов сервера будет не хватать (для этого Zhihu модернизирует архитектуру с одним компьютерным залом до внешняя многоактивная архитектура);
- Чрезмерно гибкие языковые функции Python приводят к высоким затратам на совместную работу нескольких человек и поддержку проекта.
Благодаря развитию сообществ с открытым исходным кодом и популяризации ключевых технологий, таких как контейнеры, в последние годы выбор основных технологий платформы Zhihu всегда был относительно открытым. Помимо открытых стандартов, для каждого языка доступно зрелое промежуточное ПО с открытым исходным кодом. Это позволяет бизнесу выбирать более подходящие инструменты в соответствии со сценарием проблемы, а язык тот же.
Исходя из этого, чтобы решить проблему занятости ресурсов и стоимости обслуживания динамического языка, мы решили попробовать использовать статический язык для реконструкции основного бизнеса с чрезвычайно высоким потреблением ресурсов.
Почему выбирают Голанг
Как упоминалось выше, Zhihu относительно открыта в выборе серверных технологий. В последние несколько лет, помимо Python в качестве основного языка разработки, Zhihu также разрабатывал проекты на таких языках, как Java, Golang, NodeJS и Rust.
Golang в настоящее время является одним из наиболее активно обсуждаемых языков программирования в Zhihu.Учитывая следующие моменты, мы решили попробовать использовать Golang для реконструкции основного бизнеса с высоким внутренним параллелизмом:
- Естественное преимущество параллелизма, особенно подходящее для приложений с интенсивным вводом-выводом
- Версия Golang внутренних основных компонентов Zhihu относительно завершена.
- Статический тип, совместная разработка и обслуживание нескольких человек более безопасны и надежны.
- После сборки нужен только один исполняемый файл, что удобно для развертывания
- Стоимость обучения низкая, а эффективность разработки ненамного ниже, чем у Python.
По сравнению с другим отличным языком-кандидатом — Java, Golang лучше по внутренней экологической среде Zhihu, простоте развертывания и заинтересованности инженеров.В итоге мы решили выбрать Golang в качестве языка разработки.
Результаты трансформации
До сих пор услуги члена сообщества Zhihu (RPC, пиковые сотни тысяч запросов в секунду), комментарии (RPC + HTTP) и службы вопросов и ответов (RPC + HTTP) были переписаны через Golang. В то же время, в связи с дальнейшим совершенствованием базовых компонентов Golang в процессе Golangization, некоторые новые предприятия сразу выбрали Golang для внедрения в начале разработки, Golang стал одним из рекомендуемых языков для технического отбора. новых проектов в Zhihu.
По сравнению с тем, что было до преобразования, в настоящее время улучшены следующие моменты:
- Экономьте более 80% ресурсов сервера. Поскольку наша система развертывания использует сине-зеленое развертывание, несколько служб, которые ранее занимали самые высокие ресурсы сервера, не могут быть развернуты одновременно из-за ресурсов контейнера, и их необходимо развертывать последовательно. После реконструкции ресурсы сервера оптимизируются, и проблема ресурсов сервера эффективно решается.
- Затраты на многопользовательскую разработку и поддержку проекта значительно снизились. Предположительно всем, кто поддерживает большой проект Python, часто необходимо подтверждать тип параметра и возвращаемое значение функции на трех уровнях и на трех уровнях снаружи. В Golang все ориентируются на определение интерфейса, а затем реализуют его в соответствии с интерфейсом, что делает процесс кодирования более безопасным, а многие проблемы, которые можно обнаружить только при выполнении кода Python, можно обнаружить во время компиляции.
- Завершены внутренние базовые компоненты Golang. Как упоминалось ранее, версия Golang внутренних базовых компонентов Zhihu относительно полна, что является одним из предварительных условий для нашего выбора Golang. Однако в процессе рефакторинга мы обнаружили, что некоторые базовые компоненты все еще несовершенны или даже отсутствуют. Поэтому мы также улучшили и предоставили многие базовые компоненты, которые облегчат трансформацию Golang других проектов в будущем.
Процесс реализации
Благодаря тщательности микросервисов Zhihu, каждому независимому микросервису очень удобно менять язык, мы можем легко трансформировать один бизнес, и почти никакая внешняя проверяющая сторона не сможет это воспринять.
Внутри Zhihu каждый независимый микросервис имеет свои независимые ресурсы. Между службами нет ресурсной зависимости. Все взаимодействия осуществляются через RPC-запросы. Каждая группа контейнеров, предоставляющая службы (HTTP или RPC) во внешний мир, использует услуги внешнему миру. Типичная структура микросервиса выглядит следующим образом:
Поэтому наше преобразование Голанга разделено на следующие этапы:
Шаг 1. Рефакторинг логики с помощью Golang
Сначала создадим новый микросервис и рефакторим бизнес-логику через Golang, но:
- Протокол, предоставляемый новой службой (HTTP, определение интерфейса RPC и возвращаемые данные), такой же, как и раньше (важно поддерживать согласованность протокола, и позже будет удобнее выполнить миграцию проверяющей стороны).
- Новый сервис не имеет собственных ресурсов и использует ресурсы рефакторингового сервиса:
Шаг 2. Проверьте правильность новой логики
Когда рефакторинг кода завершен, мы проверяем корректность нового сервиса перед переключением трафика на новую логику.
Для интерфейса чтения, поскольку он идемпотентный, множественные вызовы не имеют побочных эффектов, поэтому, когда будет реализована новая версия интерфейса, мы запустим сопрограмму для запроса новой службы, когда старая служба получит запрос, и сравним новую и старые сервисы. Соответствуют ли данные:
- Когда запрос достигает старой службы, сразу же запускается сопрограмма для запроса новой службы, и в то же время основная логика старой службы будет выполняться в обычном режиме.
- Когда запрос возвращается, он сравнивает, совпадают ли возвращенные данные старой службы и вновь реализованной службы.Если они отличаются, они будут записывать + записи журнала.
- По индикаторам управления и логам инженер нашел ошибки новой логики реализации, и продолжил проверку после исправления (собственно, на этом шаге мы также нашли много ошибок в исходной реализации Python).
Для интерфейса записи большинство из них не являются идемпотентными, поэтому интерфейс записи нельзя проверить, как указано выше. Для интерфейса записи мы в основном обеспечим логическую эквивалентность старого и нового с помощью следующих средств:
- Гарантия модульного тестирования
- Проверка разработчика
- проверка качества
Шаг 3. Оттенки серого
Когда все будет проверено, мы начнем пересылать трафик согласно процентам.
В это время запрос по-прежнему будет проксироваться в группу контейнеров старой службы, но старая служба больше не будет обрабатывать запрос, а перенаправит запрос в новую службу и напрямую вернет данные, возвращенные новой службой.
Причина, по которой он не переключается напрямую с портала трафика, заключается в обеспечении стабильности и быстрого отката при возникновении проблемы.
Шаг 4. Сократите вход трафика
Когда объем предыдущего шага достигает 100 %, несмотря на то, что запрос по-прежнему будет проксироваться в старую группу контейнеров, все возвращенные данные будут сгенерированы новой службой. На этом этапе мы можем переключить запись трафика непосредственно на новый сервис.
Шаг 5. Отключите старый сервис
На этом рефакторинг в основном подошел к концу. Однако ресурсы нового сервиса по-прежнему находятся в старом сервисе, а старый сервис без трафика еще не отключен.
На этом этапе вы можете напрямую настроить атрибуцию ресурсов старой службы новой службе, а затем перевести старую службу в автономный режим.
На данный момент реконструкция завершена.
Практика проекта Голанг
В процессе рефакторинга мы наступили на множество ям, вот некоторые из них, которыми хочу с вами поделиться. Если у вас есть похожие потребности в рефакторинге, вы можете просто обратиться к нему.
Предпосылкой языкового рефакторинга является понимание бизнеса.
Не переводите бездумно исходный код и не исправляйте бездумно проблемную реализацию. В первые дни рефакторинга мы нашли некоторые моменты, которые казались лучше, но после небольшой доработки возникли какие-то странные проблемы. Последний опыт заключается в том, что вы должны понимать бизнес и исходную реализацию перед рефакторингом. Лучше всего, чтобы инженеры соответствующего бизнеса также участвовали во всем процессе рефакторинга.
Структура проекта
Что касается соответствующей структуры проекта, мы на самом деле прошли через множество окольных путей.
В начале, исходя из практического опыта в Python, интерфейс взаимодействия предоставляется непосредственно между слоями через функции. Тем не менее, быстро стало очевидно, что Golang так же легко тестировать на обезьянах, как Python.
После постепенной эволюции и ссылок на различные проекты с открытым исходным кодом структура нашего кода выглядит примерно так:
.
├── bin --> 构建生成的可执行文件
├── cmd --> 各种服务的 main 函数入口( RPC、Web 等)
│ ├── service
│ │ └── main.go
│ ├── web
│ └── worker
├── gen-go --> 根据 RPC thrift 接口自动生成
├── pkg --> 真正的实现部分(下面详细介绍)
│ ├── controller
│ ├── dao
│ ├── rpc
│ ├── service
│ └── web
│ ├── controller
│ ├── handler
│ ├── model
│ └── router
├── thrift_files --> thrift 接口定义
│ └── interface.thrift
├── vendor --> 依赖的第三方库( dep ensure 自动拉取)
├── Gopkg.lock --> 第三方依赖版本控制
├── Gopkg.toml
├── joker.yml --> 应用构建配置
├── Makefile --> 本项目下常用的构建命令
└── README.md
Они есть:
- bin: исполняемый файл, сгенерированный сборкой, обычно онлайн-запуск `bin/xxxx-service`
- cmd: запись основной функции различных служб (RPC, Web, автономные задачи и т. д.), которая обычно выполняется отсюда.
- gen-go: thrift компилирует автоматически сгенерированный код, обычно настраивает Makefile и генерирует его напрямую с помощью `make thrift` (у этого метода есть недостаток: сложно обновить версию thrift)
- pkg: реальная бизнес-реализация (подробно ниже)
- thrift_files: определяет протокол интерфейса RPC.
- поставщик: зависимые сторонние библиотеки
Среди них реальная логическая реализация проекта находится в pkg, а его структура такова:
pkg/
├── controller
│ ├── ctl.go --> 接口
│ ├── impl --> 接口的业务实现
│ │ └── ctl.go
│ └── mock --> 接口的 mock 实现
│ └── mock_ctl.go
├── dao
│ ├── impl
│ └── mock
├── rpc
│ ├── impl
│ └── mock
├── service --> 本项目 RPC 服务接口入口
│ ├── impl
│ └── mock
└── web --> Web 层(提供 HTTP 服务)
├── controller --> Web 层 controller 逻辑
│ ├── impl
│ └── mock
├── handler --> 各种 HTTP 接口实现
├── model -->
├── formatter --> 把 model 转换成输出给外部的格式
└── router --> 路由
Стоит отметить, что в приведенной выше структуре у нас обычно есть два пакета, impl и mock, между каждым слоем.
Это сделано потому, что Golang не может динамически имитировать реализацию так же легко, как в Python, и его нельзя легко протестировать. Мы придаем большое значение тестированию, и уровень покрытия тестами, реализованный Golang, остается выше 85%. Поэтому мы абстрагируем интерфейс между слоями (например, ctl.go выше), и верхний уровень вызывает нижний уровень через соглашение об интерфейсе. Во время выполнения реальная бизнес-логика запускается путем привязки реализации интерфейса в impl посредством внедрения зависимостей, а при тестировании реализация интерфейса в макете должна достигать цели реализации макета более низкого уровня.
В то же время, чтобы облегчить развитие бизнеса, мы также внедрили каркас проекта Golang, с помощью которого можно более удобно напрямую создавать службу Golang, содержащую запись HTTP и RPC. Этот каркас был интегрирован в ZAE (Zhihu App Engine), и после создания проекта Golang генерируется код шаблона по умолчанию. Для новых проектов, разработанных с помощью Golang, создается готовый фреймворк.
Статическая проверка кода, чем раньше, тем лучше
Мы реализовали введение статической инспекции кода только на более позднем этапе разработки, на самом деле лучше всего использовать ее вовремя в начале проекта и обеспечить качество кода основной ветки с более строгими стандартами.
Проблема, появившаяся на поздних стадиях разработки, заключалась в том, что уже было слишком много кода, не соответствующего стандарту. Так что нам пришлось игнорировать множество проверок в краткосрочной перспективе.
Есть много очень простых и даже глупых ошибок, которых люди не могут избежать в 100% случаев, в этом ценность существования линтера.
На практике мы используемgometalinter. Сам gometalinter не проверяет код, а интегрирует различные линтеры для обеспечения унифицированной конфигурации и вывода. Мы интегрируем три проверки: vet, golint и errcheck.
alecthomas/gometalinterпонижение рейтинга
Какова именно степень детализации понижения? Точка зрения некоторых инженеров на этот вопрос — вызовы RPC, а наш ответ — «фичи».
В процессе рефакторинга мы понизили рейтинг всех функциональных точек, допускающих деградацию, в соответствии с точкой зрения «если эта функция недоступна, как это повлияет на пользователей» и добавили соответствующие индикаторы и сигналы тревоги для всех понижений. Конечным эффектом является то, что если все внешние RPC-зависимости Q&A отключены (включая базовые службы, такие как участник и проверка подлинности), Q&A сам по-прежнему может нормально просматривать вопросы и ответы.
Наше понижение находится наcircuitНа основе , он инкапсулирует такие функции, как сбор индикаторов и вывод журнала. Twitch также использует эту библиотеку в продакшене, и мы не сталкивались с какими-либо проблемами за более чем полгода использования.
cep21/circuitanti-pattern: panic - recover
После того, как большинство людей начинают разрабатывать с помощью Golang, очень неудобным моментом является его обработка ошибок. Простая реализация интерфейса HTTP может выглядеть так:
func (h *AnswerHandler) Get(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
loginId, err := auth.GetLoginID(ctx)
if err != nil {
zapi.RenderError(err)
---> return
}
answer, err := h.PrepareAnswer(ctx, r, loginId)
if err != nil {
zapi.RenderError(err)
---> return
}
formattedAnswer, err := h.ctl.FormatAnswer(ctx, loginId, answer)
if err != nil {
zapi.RenderError(err)
---> return
}
zapi.RenderJSON(w, formattedAnswer)
}
Как и выше, за каждой строкой кода следует оценка ошибки. Во-вторых, громоздкость, основная проблема в том, что если оператор return после обработки ошибки будет забыт, логика не будет заблокирована, а код продолжит выполняться вниз. В реальном процессе разработки мы действительно допускали подобные ошибки.
С этой целью мы используем слой промежуточного программного обеспечения для захвата паники на внешнем уровне фреймворка.Если recovery является ошибкой, определенной фреймворком, она будет преобразована в соответствующую ошибку HTTP и отображена, в противном случае она будет продолжать выдаваться. к верхнему слою. Измененный код становится таким:
func (h *AnswerHandler) Get(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
loginId := auth.MustGetLoginID(ctx)
answer := h.MustPrepareAnswer(ctx, r, loginId)
formattedAnswer := h.ctl.MustFormatAnswer(ctx, loginId, answer)
zapi.RenderJSON(w, formattedAnswer)
}
Как и выше, в бизнес-логике, где RenderError ранее возвращался и возвращался напрямую, теперь он будет паниковать напрямую, когда снова столкнется с ошибкой. Эта паника будет перехвачена на уровне HTTP-фреймворка. Если это ошибка HTTPError, определенная в проекте, она будет преобразована в соответствующий интерфейсный формат 4xx JSON и возвращена во внешний интерфейс. В противном случае она будет продолжать выбрасываться вверх и в конечном итоге стать 5xx вернулся на передок.
Упомянутая здесь реализация не рекомендуется для всех, Golang официально не рекомендует это использование. Тем не менее, это эффективно решает некоторые проблемы, и предлагается здесь для справки.
начало горутины
При построении модели большая часть логики на самом деле не зависит друг от друга и может выполняться одновременно. В настоящее время запуск нескольких горутин для одновременного получения данных может значительно сократить время отклика.
Тем не менее, яма горутины, на которую могут легко наступить люди, которые только используют Golang, заключается в том, что если горутина паникует, ее родительская горутина не может быть восстановлена - строго говоря, нет понятия горутин родитель-потомок. горутина.
Поэтому вы должны быть очень осторожны здесь, если ваша только что запущенная горутина может запаниковать, вы должны восстановиться в этой горутине. Конечно, лучший способ — сделать слой инкапсуляции вместо того, чтобы запускать горутины в бизнес-коде.
Поэтому мы обращаемся к функции Future в Java и делаем простую инкапсуляцию. Там, где нужно запустить горутину, она запускается через инкапсулированное Future, а Future обрабатывает различные ситуации, такие как паника.
http.Response Тело не закрыто, что приводит к утечке горутины
С течением времени мы обнаружили, что количество служебных горутин продолжало увеличиваться с течением времени и сразу же уменьшалось при перезапуске контейнера. Итак, мы предполагаем, что в коде есть утечка горутины.
Из-за стека goroutine и печати журналов в зависимой библиотеке последняя проблема заключается в том, что внутренняя базовая библиотека использует http.Client, но не имеет `resp.Body.Close()`, что приводит к утечке goroutine.
Один из уроков здесь заключается в том, что вместо использования http.Get непосредственно в рабочей среде лучше создать экземпляр http-клиента самостоятельно и установить время ожидания.
После устранения этой проблемы все работает нормально:
Хотя эта проблема была описана в нескольких простых предложениях, шаги по ее обнаружению заняли у нас много времени Мы можем начать новую статью позже, чтобы представить процесс устранения неполадок, связанных с утечками горутин.
В конце концов
Реконструкция основного бизнеса на основе Golang — это цель, достигнутая командой по бизнес-архитектуре сообщества и технической группой по контенту сообщества благодаря усилиям во втором и третьем кварталах 2018 года. Вот некоторые члены обеих команд:
@yaogangqiang @Adam Wen @ВАНЦИПИНГ @ченжэн @yetingsky @wangzhizhao @Чай Сяомиао @xlzd
Группа бизнес-архитектуры сообщества отвечает за решение проблем и задач, вызванных быстрым ростом сложности бизнеса и одновременным масштабированием серверной части сообщества Zhihu. С быстрым ростом масштабов бизнеса и числа пользователей Zhihu, а также с постоянным увеличением сложности бизнеса, технические проблемы, с которыми сталкивается наша команда, также растут. В настоящее время мы внедряем многокомпьютерную комнату и удаленную мультиактивную архитектуру сообщества Zhihu, а также прилагаем все усилия для обеспечения и улучшения качества и стабильности серверной части Zhihu. Мы приветствуем друзей, которые интересуются технологиями и готовы решать технические задачи, присоединиться к нам и внести свой вклад в создание стабильной и надежной серверной части Zhihu.