Программисты должны больше знать об эффективности кэшированных данных

Любой инженер-программист, заботящийся о производительности своего программного обеспечения, должен прочитать книгу Хеннесси и Паттерсона о количественных подходах по компьютерным наукам, даже если он не хочет разрабатывать набор инструкций ЦП. Это увлекательное чтение, и в нем очень легко найти идеи.

Авторы тратят много времени на глубокий анализ способов измерения производительности ЦП, уделяя особое внимание показателям SPECint и SPECfp. Информация, которую они раскрывают, позволяет понять не только то, как современные процессоры стали такими быстрыми, но и то, как мы можем использовать те же методы для улучшения наших современных веб-сервисов.

Основная проблема заключается в том, что существует множество способов улучшить производительность. Хеннесси и Паттерсон объясняют, что вы можете оценить соотношение затрат и выгод для ваших улучшений — и, поскольку у нас нет неограниченных усилий, можем ли мы быть практичными в отношении того, на что мы тратим наше время?

Если вы меня потерпите, позвольте мне заложить основу, а затем я расскажу о том, как это влияет на решения о кэшировании для моих веб-сервисов.

Закон Амдала

Мне нравится начинать с закона Амдала, подробно описанного в книге и очень ценного для понимания настройки производительности. Он имеет строгую математическую формулировку, а также несколько допущений:

  • Оптимизация влияет на часть программы и заставляет эту часть работать быстрее.
  • Чтобы узнать преимущества оптимизации, вы должны знать долю программы, на которую она влияет (P), плюс ускорение этой доли программы (S).
  • Повышение (ы) = 1 / ((1 - P) + (P / S))

Если бы один из ваших программистов показал вам оптимизацию, которая сделала код ключевой функции в 5 раз быстрее, вы были бы в восторге! Затем вы посмотрите, какая часть вашего приложения использует эту функцию. Допустим, ответ составляет 10% от вашей программы (P), а поскольку ускорение в 5 раз быстрее (S), вот что говорит нам закон:

  • Увеличение (ы) = 1 / ((1–10%) + (10% / 5X))
  • Повышение = 1 / (90%) + (2%)
  • Таким образом, ваш общий прирост составляет около 1,086x, а не 5x.

Это своего рода отрезвляющая мысль, которая напоминает нам, как много еще предстоит сделать, чтобы настроиться на производительность. Когда у нас есть множество идей по улучшению, мы можем ранжировать потенциальные проекты по тому, какая доля кода в них составляет: проект, который может улучшить производительность приложения на 20%, должен быть предпочтительнее проекта, который повышает производительность на 5%.

Однако часто проблема заключается в другом — какова стоимость каждого проекта?

Преобразование в эффективность

Чтобы повысить эффективность, нам нужен некоторый тип расчета затрат. Давайте сделаем простой, основанный на постоянных и периодических затратах:

  • Эффективность буста — это ускорение всей задачи (S), сбалансированное с ее стоимостью.
  • Стоимость повышения включает в себя как первоначальные затраты на разработку (D), которые амортизируются в течение срока действия проекта (L), так и текущие расходы в виде ежемесячных счетов или платы за единицу (R).
  • Эффективность (повышение) = (D + (R * L)) / S

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

Используя эту формулу, можно сравнивать различные виды работ по настройке. Вот гипотетический пример:

  • Алиса предлагает хранить кешированную информацию о сеансе в Redis. Фактический объем хранимых данных будет низким, и на его создание уйдет месяц. Она считает, что это приведет к увеличению производительности на 1% для всех вызовов API при стоимости 300 долларов в месяц и 1 месяце работы в небольшой команде (60 000 долларов).
  • Боб предлагает провести масштабный проект по кодированию, чтобы поместить массу данных в предварительно вычисленный слой. Он измерил прирост своей производительности, и он в шесть раз больше, чем у Алисы. Серверы Боба будут стоить 2500 долларов в месяц, и его большой команде потребуется 3 месяца, чтобы построить их (300 000 долларов).

В этом надуманном примере короткий период времени, такой как пять лет, будет благоприятствовать подходу Боба к «большому шагу», тогда как длинный период времени, такой как десять лет, предпочтет низкие текущие затраты Алисы. Мы даже не учитывали другие внешние факторы, такие как репутация: может быть, Боб обычно пересматривал свои оценки, а Алиса постоянно попадала в свои. Может быть, один проект закладывает основу для будущих усилий.

Дело не в том, чтобы сделать математику точной, а в том, чтобы мы могли сравнить различные виды производительности на равных условиях.

Реальные показатели эффективности с кэшем

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

Учитывая, что в моей программе есть почти сотня отдельных кешей для оценки, это идеальный вариант использования. Я могу оценить каждый кеш на основе его эффективности по сравнению с его повторяющимися затратами с точки зрения использования ОЗУ и экземпляра REDIS.

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

Я не смогу представить все свои результаты в этой статье, но давайте поделюсь некоторыми интересными инсайтами, которые я обнаружил в ходе этой работы.

  • Мое приложение имеет несколько слоев кэширования, некоторые в памяти и некоторые удаленные.
  • Стоимость уровня L1, кэширование в памяти, является бесплатным до тех пор, пока он не заполнит память сервера, после чего элементы должны быть отброшены или увеличена оперативная память.
  • Стоимость уровня L2, кластера Redis, имеет известную стоимость за байт. Если я хочу увеличить размер кеша, я просто добавляю экземпляры в кластер.
  • Извлечение элемента из кеша имеет разную производительность для каждого уровня (F1 и F2). Если мы не находим его, мы называем это «промахом» с ненулевым штрафом за промах (М1 и М2).
  • Мои кеши предназначены для пересчета работы, если элемент не найден ни в кеше L1, ни в L2 — мы можем назвать это W, работа по пересчету.
  • Пересчет данных также может привести к штрафу базы данных, который следует рассчитывать отдельно от W (назовем его Dp).

Когда я начал изучать эти элементы, я обратил внимание на множество интересных наблюдений.

  • В C#/AspNetCore кеш в памяти имеет время попадания/промаха 0,005–0,010 миллисекунды.
  • В некоторых средах кэширование Redis имеет время попадания/промаха порядка 1–3 миллисекунд, но это значение будет меняться в зависимости от объема извлекаемых данных.
  • Время попадания и время промаха почти всегда сопоставимы. Объем усилий, необходимых для оценки кэша, часто не зависит от того, возвращаются данные или нет.
  • Redis можно масштабировать, добавляя узлы, но мой облачный провайдер позволяет увеличивать локальную память только в два раза, что также удваивает затраты каждый раз, когда мы увеличиваем размеры локальных экземпляров.
  • Из-за этих соотношений затрат для объекта данных важно иметь другое время жизни кэша локально по сравнению с Redis.
  • Чем больше у вас кеша, тем более важной становится предсказуемость аннулирования кеша. Когда объект компании изменяется, вам может потребоваться быстро распространить аннулирование кэша для этого объекта компании на десятки компьютеров. Здесь может помочь публикация/подписка Redis, а также сокращение времени жизни кеша.
  • Кэшированные данные, которые всегда находятся в памяти (например, кэш без доли промахов), труднее оценить, чем данные, в которых есть доля промахов. В этом случае вы должны оценивать только количество обращений к кешу по сравнению с его стоимостью в памяти.
  • Многие простые кэши выборки из базы данных требуют столько же времени для выборки данных из кэша, сколько и для повторной выборки данных из базы данных. В одном примере было измерено от 1 до 3 мс на выборку из Redis и 1,5 мс на повторную выборку из базы данных.
  • Чрезмерное использование выборки из базы данных может вывести систему из строя, даже если каждая отдельная выборка выполняется быстрее через базу данных, чем с использованием кэша.
  • Для снижения затрат на серверы непрерывной интеграции может потребоваться установка разных пороговых значений кэша для каждой среды.

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