Как стать автором
Обновить

100 тыс. посещений в день (разбор полетов и новый эксперимент)

Время на прочтение7 мин
Количество просмотров1.6K
couner В прошлой моей статье про скорость работы с данными GAE был встроен графический счетчик показов. Каждый мог видеть значение счетчика и израсходованные ресурсы CPU. Как я уже говорил, счетчик был довольно «тяжелым»: создаваемая им нагрузка эквивалентна отображению на странице 1000 записей из базы данных без использования кеширования.

Эксперимент со счетчиком оказался весьма полезным, а его результаты несколько неожиданными для меня (отличными от запросов с одного IP-адреса). Хочу поделиться результатами эксперимента и поставить новый эксперимент, уже с учетом пройденных ошибок. Кстати, исходный код нового графического счетчика доступен для всех и приведен в статье.



График запросов

image

Максимум 7 запросов в секунду (столько открывают главную страницу Хабра вечером). В дневное время, думаю, эта цифра поднимается до 10 запросов в секунду.

График расхода процессорного времени

image

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

Расход ресурсов за первый день

image

На графике нет расхода ресурсов до 10 часов утра MCK (так как в 10 часов счетчики ресурсов сбрасываются). До 10 часов было израсходовано примерно еще столько же. В общей сложности около 150 тыс. запросов и 500 МБайт трафика (часть запросов на статическую картинку, но их значительно меньше).

Количество запущенных инстанций

К сожалению, по ошибке удалил скрин с запущенными инстанциями. При 6 посетителях в секунду было запущено 16 инстанций (это максимум, который я зафиксировал).

Заторможенные запросы???

image

А здесь самое интересное. Спустя некоторое время возникла проблема: примерно каждый 5-й запрос начал исполняться в 5-10 раз дольше среднего (в среднем счетчик загружался 0.5 секунд). Вначале, даже при множестве посещений, такого не наблюдалось. Ситуация не изменилась до сих пор, когда посещений стало совсем мало.

Как объяснить эти ошибки? Варианты:

1. Warm up request. Нет, так как не прошло достаточно времени, чтобы инстанция успела выгрузиться (к примеру, на этом графике время между 3-мя ошибочными запросами менее минуты).
2. Одновременная модификация записи в хранилище с одинаковым ключом. Нет, так как между запросами прошло более 10 секунд времени.
3. Приложение было ограничено как неэффективное.

Остается последнее. Это притом, что в среднем запрос выполнялся менее 1 секунды (около 0.5 сек). Алгоритм определения неэффективности приложений не разглашается, поэтому сказать что-то определенное довольно сложно…

Честно сказать, для меня эти ошибки остаются загадкой.

Решение проблемы

Если действительно проблема в том, что приложение было ограничено как неэффективное, то проблема не повторится с новым кодом счетчика (ранее был применен org.toyz.litetext), который исполняется примерно в 7 раз быстрее. Формирование рисунка сделано очень просто, но довольно эффективно (код приведен ниже — можете использовать его на своем сайте).

Вторая редакция:

public final class DynamicImage
{
  // Пакет с картинками
  private static final String FOLDER = "resources";

  private static Image backgroundImage;

  private static final Map<String, Image> imageTable = new Hashtable<String, Image>();

  static
  {
    try
    {
      backgroundImage = makeImage("appengine.png");

      imageTable.put("1",
        makeImage("1.png"));
      imageTable.put("2",
        makeImage("2.png"));
      imageTable.put("3",
        makeImage("3.png"));
      imageTable.put("4",
        makeImage("4.png"));
      imageTable.put("5",
        makeImage("5.png"));
      imageTable.put("6",
        makeImage("6.png"));
      imageTable.put("7",
        makeImage("7.png"));
      imageTable.put("8",
        makeImage("8.png"));
      imageTable.put("9",
        makeImage("9.png"));
      imageTable.put("0",
        makeImage("0.png"));

      imageTable.put("(",
        makeImage("leftBracket.png"));
      imageTable.put(")",
        makeImage("rightBracket.png"));
    }
    catch (IOException exception)
    {
      throw new RuntimeException(exception);
    }
  }

  public Image drawText(String text,
    Anchor anchor,
    long backgroundColor)
  {
    if (null == text)
      throw new ArgumentNullException("text");

    if (null == anchor)
      throw new ArgumentNullException("anchor");

    Collection<Composite> compositeCollection = new ArrayList<Composite>();

    // background
    compositeCollection.add(ImagesServiceFactory.makeComposite(backgroundImage,
      0,
      0,
      1f,
      Anchor.TOP_LEFT));

    int xOffset = 0;

    for (int pos = 0; pos < text.length(); pos++)
    {
      String symbol = Character.toString(text.charAt(pos));

      if (!imageTable.containsKey(symbol))
        continue;

      Image image = imageTable.get(symbol);

      compositeCollection.add(ImagesServiceFactory.makeComposite(image,
        xOffset,
        0,
        1f,
        anchor));

      xOffset += image.getWidth();
    }

    return ImagesServiceFactory.getImagesService().composite(compositeCollection,
      backgroundImage.getWidth(),
      backgroundImage.getHeight(),
      backgroundColor);
  }

  private static Image makeImage(String imageName)
    throws IOException
  {
    InputStream inputStream = null;
    ByteArrayOutputStream outputStream = null;

    try
    {
      String filePath = FOLDER + File.separatorChar + imageName;
      inputStream = DynamicImage.class.getResourceAsStream(filePath);

      if (null == inputStream)
        throw new IllegalStateException("filePath=" + filePath);

      outputStream = new ByteArrayOutputStream();

      int length;
      byte[] buffer = new byte[1024];

      while ((length = inputStream.read(buffer,
        0,
        buffer.length)) > 0)
      {
        outputStream.write(buffer,
          0,
          length);
      }

      outputStream.flush();

      return ImagesServiceFactory.makeImage(outputStream.toByteArray());
    }
    finally
    {
      if (null != inputStream)
        inputStream.close();

      if (null != outputStream)
        outputStream.close();
    }
  }
}

* This source code was highlighted with Source Code Highlighter.


Прошу сообщество поучаствовать в эксперименте. О результатах обязательно сообщу в этой статье.

Результаты эксперимента меня обрадовали! За пол дня счетчик открыли около 100 тыс. человек (как и предполагалось). При этом был потрачен 1 Гб трафика (хорошо хоть картинку уменьшил, иначе было бы не вписался в лимит 3 Гб):

image

При этом максимальное количество запущенных инстанций было 9 шт. (при 8 запросах в секунду):

image

А запросы в этот момент исполнялись без задержек. Вот выдержка из лога:

image

При этом каждый запрос выполнял запись в базу данных и проверку уникальности IP-адреса. Вышло БЕСПЛАТНО! Хватило бесплатных ресурсов!

Вывод

При грамотном использовании GAE способен выдержать колоссальную нагрузку. Google действительно сделал то, о чем многие мечтали: оплата только за фактически использованные ресурсы (все честно: если не используете — ничего платить не нужно). Нам осталось только создавать сервисы, которые бы привлекали такое количество посетителей.
Теги:
Хабы:
+36
Комментарии20

Публикации

Истории

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн