предисловие
Окончательный результат глуп, и гигант может напрямую просмотреть результат.Эта статья предназначена только для записи метода, использованного для поиска ошибки.
Я поддерживаю библиотеку конфигурации операционной платформы компании в будние дни, поскольку меры по даунгрейду лучше, а у компании много экземпляров контейнера, я не отреагировал вовремя, когда возникли ошибки. Эта проблема должна была существовать с момента рождения сервиса. Причина, по которой он не появился, должна заключаться в том, что в прошлом было много итераций, и он будет выпускаться четыре-пять раз в неделю. Кроме того, количество пользователей Платформа была относительно небольшой в прошлом году, но позже она постепенно стала основным продуктом компании и использовалась многими отделами, поэтому возникла проблема.
первая догадка
«Почему сопрограмма этого сервиса вдруг взлетела до небес?»
Когда первая корутина взлетела, я обнаружил, что один экземпляр внезапно упал на колени, количество сопрограмм резко возросло, и память также резко возросла, Первая реакция была, что медленный запрос в бд, но на самом деле. И глядя на мониторинг, нет особого многоинтерфейсного наколенника, как показано на рисунке.
На самом деле очевидно, что количество сопрограмм с несколькими экземплярами резко возросло.
На самом деле, из соображений дизайна сервиса ожидается, что интерфейсов на коленях будет не так много, потому что другие интерфейсы реализовали кеш таблиц с полной памятью.Что касается того, почему реализован кеш таблиц с полной памятью, мы поговорим об этом позже. С помощью позиционирования pprof легко обнаружить, что у нескольких интерфейсов на коленях есть одна общая черта — утечка сопрограмм.В это время я взглянул на код.За исключением того, что нет кеша, другие реализации тех интерфейсов вполне разумны.Поскольку я подозревал, что это медленный запрос, я попытался просканировать таблицу.Это почти 3w данных, поэтому я думаю, что поток занят из-за сканирования этой таблицы, и когда несколько машин одновременно читают интерфейс, из-за того, что медленный запрос заблокирован, нет настройки тайм-аута, поэтому пропускная способность получения сопрограммы не пропорциональна.
Поэтому я соскользнул вниз и сделал кеш памяти для этого интерфейса, что временно разгрузило его на две-три недели, но через несколько недель произошла необъяснимая утечка сопрограммы. .
вторая догадка
Когда утечка сопрограммы происходит снова, нет другого способа исследовать, кроме как читать код, не из-за чтения библиотеки, а почему?
Когда мне пришла в голову эта идея, я посмотрел на код операции записи, возможно ли, что эта проблема возникает из-за того, что чтение и запись не разделены, а время записи истекло.
Потому что время, когда сопрограмма снова слилась, было очень странным, в 4 часа ночи я вдруг проснулся от телефона. Так что по прихоти я пошел проверить работу платформы в 4 утра в тот день с моими коллегами, и обнаружил, что действительно, один человек подал заявку на предмет в 16:00 ночи, написал в библиотеку, и время совпало.
Так что это действительно было из-за проблемы с записью данных, поэтому я еще раз взглянул на код и обнаружил, что вся библиотечная таблица читается из основной библиотеки, а подчиненная библиотека никогда не использовалась, эммммммм. Поэтому для сервиса сделано разделение чтения-записи, что не сложно.
Через две недели, как и ожидалось, проблема вспыхнула снова. .
третья догадка
Прежде всего, я задаюсь вопросом, действительно ли работает тайм-аут в коде.Почему он все еще истекает, даже если время тайм-аута установлено, и не только код устанавливает тайм-аут через фреймворк, но и платформа db также устанавливает тайм-аут, по логике должен быть kill off. Не будет потому что библиотек больше и мрази в определенное время больше, поэтому я становлюсь на колени, но по факту qps операции не высокий.С сомнениями я обнаружил, что в потоке установлено всего 10 потоков Для этого сервиса 10 потоков мало, поэтому я пошел расширять потоки.
Как и ожидалось, через неделю-две проблема повторилась. .
Наконец нашел проблему
На этот раз она внезапно появлялась два-три раза подряд посреди ночи, и частота была очень высокой, после переноса инстанса через одну-две минуты происходила утечка сопрограммы инстанса. Вот и получается, что все операции, вызывающие внезапную утечку сопрограмм, вызваны вызовом интерфейса для записи данных. Я несколько раз проверял интерфейс, но не видел проблемы. Но я понял одну вещь, этот интерфейс вызывает БД три раза, и я установил время ожидания для запроса/записи БД на 3 с, но апстрим сбрасывается через 10 с, поэтому тайм-аут должен быть после 9 с, я чувствую, когда что-то пошло не так.
Поэтому, когда сопрограмма снова просочилась, я пока не стал мигрировать экземпляр, а поднялся на экземпляр, чтобы просмотреть журнал экземпляра, и обнаружил, что строка журнала отладки не была выполнена. Кажется, проблема сузилась до нескольких строк кода.
Все верно, я начинаю подозревать, что проблема с фреймворком gorm, и я посмотрел исходный код. Я обнаружил, что код, который мы реализовали для поиска максимального значения, был потрясающим, он оказалсяrows
. код показывает, как показано ниже.
func GetMax(ctx context.Context) (int64, error) {
var max int64 = 0
db, err := GetConnection(ctx, DatabaseName)
if err != nil {
return max, err
}
rows, err := db.Table(TableName).Select("max(xxx) as max").Rows()
if err != nil {
return max, err
}
if rows.Next() {
err := rows.Scan(&max)
if err != nil {
return max, err
}
}
return max, nil
}
На первый взгляд кажется, что нет никакой проблемы, посмотритеnext
, когда нет следующей строки данных, он не будетrows close
, код такой, код взят из исходников go 1.13.4, только китайские комментарии добавлены от себя:
func (rs *Rows) Next() bool {
var doClose, ok bool
withLock(rs.closemu.RLocker(), func() {
doClose, ok = rs.nextLocked()
})
if doClose { // 这里当需要close的时候会将线程释放掉
rs.Close()
}
return ok // 而返回的true/false是决定能不能拿到数据的
}
func (rs *Rows) nextLocked() (doClose, ok bool) {
if rs.closed {
return false, false
}
// Lock the driver connection before calling the driver interface
// rowsi to prevent a Tx from rolling back the connection at the same time.
rs.dc.Lock()
defer rs.dc.Unlock()
if rs.lastcols == nil {
rs.lastcols = make([]driver.Value, len(rs.rowsi.Columns()))
}
rs.lasterr = rs.rowsi.Next(rs.lastcols)
if rs.lasterr != nil {
// Close the connection if there is a driver error.
if rs.lasterr != io.EOF {
return true, false
}
nextResultSet, ok := rs.rowsi.(driver.RowsNextResultSet)
if !ok {
return true, false
}
// The driver is at the end of the current result set.
// Test to see if there is another result set after the current one.
// Only close Rows if there is no further result sets to read.
if !nextResultSet.HasNextResultSet() { // 当没有下一行数据时,next是false,close是true
doClose = true
}
return doClose, false
}
return false, true
}
эй нетclose
Неужели нет проблем?
Давайте посмотрим на это сноваrows
Откуда вы его взяли?Правильно, пул потоков, так что если вы его не закроете, этот поток не будет помещен обратно в пул потоков? Давайте посмотрим на код закрытия, код выглядит следующим образом:
func (rs *Rows) close(err error) error {
rs.closemu.Lock()
defer rs.closemu.Unlock()
if rs.closed {
return nil
}
rs.closed = true
if rs.lasterr == nil {
rs.lasterr = err
}
withLock(rs.dc, func() {
err = rs.rowsi.Close()
})
if fn := rowsCloseHook(); fn != nil {
fn(rs, &err)
}
if rs.cancel != nil {
rs.cancel()
}
if rs.closeStmt != nil {
rs.closeStmt.Close()
}
rs.releaseConn(err) // 这里会释放连接
return err
}
На самом деле так и должно быть. Так что тут надо решать. Решенный код выглядит следующим образом:
func GetMax(ctx context.Context) (int64, error) {
var max int64 = 0
db, err := GetConnection(ctx, DatabaseName)
if err != nil {
return max, err
}
rows, err := db.Table(TableName).Select("max(xxx) as max").Rows()
if err != nil {
return max, err
}
defer rows.Close() // 只因为少了这一行
if rows.Next() {
err := rows.Scan(&max)
if err != nil {
return max, err
}
}
return max, nil
}
Но на самом деле для максимального значения существует только одно значение Зачем использовать строки вместо строк? неизвестный. Потому что, согласно исходному коду go, если вы напишете это так, вам не нужно закрывать поток самостоятельно.
for rows.Next() { // 这里改成循环一直走下一个
err := rows.Scan(&max)
if err != nil {
return max, err
}
}
Другой вопрос, почему это происходит раз в две недели, если посчитать, то экземпляров около 15, и операция проверки макса сработает только при применении определенной конфигурации, а в пуле потоков их 10. Потоки, поэтому, если предположить, что запрос сбалансирован, требуется более 100 приложений, чтобы начать сталкиваться с этой проблемой. Причем эта проблема возникает только на машинах с недостаточным количеством потоков db, кроме того, этот сервис относительно стабилен, и на него давно нет спроса, поэтому контейнер не будет перезапущен и память не будет сброшена. Что касается того, может ли GC решить эту проблему, то не должен, потому что решение только уменьшит количество ваших потоков.
Эй, это действительно блюдо, проблема позиционирования такая сложная. Основная причина в том, что у меня нет опыта. Все интерфейсы сообщали об ошибках. Сначала я не мог запустить. И только когда я проснулся ранним утром от звонка будильника, у меня возникла внезапная идея найти проблема.
Полное хранилище таблиц в памяти
Зачем проектировать с памятью? Во-первых, таблиц не так много, а во-вторых, данных не слишком много, и каждая таблица в среднем всего 5 тыс. Более того, поскольку не ожидается, что апстрим должен будет запрашивать этот сервис каждый раз, когда он получает данные, это необходимо сканировать таблицу. По вышеуказанным причинам сервис не кэшируется с помощью redis.Дизайн сервиса выглядит следующим образом.
Каковы преимущества и недостатки этой конструкции
преимущество:
- Давление базы данных небольшое, и при сканировании таблицы не возникает особых проблем, когда объем данных невелик.
- Когда есть много вышестоящих сервисов, экземпляр действует как Redis, и у БД нет давления запросов от вышестоящего.
- Нет сложности сериализации и десериализации
недостаток:
- Стоимость разработки высока, а в go нет дженериков, чтобы уменьшить сериализацию, код пишется более жестко
- Эффективность обновления медленная, зависит от времени ротации данных, подходит для данных, которые не нужно обновлять вовремя
Эпилог
Об этой услуге я узнал, когда впервые пришел в компанию, не ожидал, что будут скрытые проблемы, а она так долго скрывалась.
Дизайн сервиса довольно хорош, но особенно отвратительна разработка.
Или ты слишком хорош, научись чему-то большему.