Translate

четвер, 28 лютого 2013 р.

HighLoad FileDistributor: Part 1 - FileCacheAdapter


On previous meetup we discussed theoretical issue: How to develop application which allows to download few dozens small files from server, write some logs about it and execute http call if download was successful?
Actually task is quite easy but... we have few restrictions
1) We have only one Tomcat (means no cluster)
2) Expected number of downloads about 10 000 000  per day

So together we decided
1) Put files in cache
2) Execute async call to remote server
Pretty simple.

So I started implementing solution. First question was how to put file in cache. I didn't find any implemented  solution and only one article.
Below my version of caching files
 
Lets analyze step by step how it works. Fetching file from cache
public InputStream getFile(String fileName) throws IOException {
    StringBuilder lookupFile = new StringBuilder(filePath);
    lookupFile.append("\\");
    lookupFile.append(fileName);
    Path filePath = Paths.get(lookupFile.toString());
    boolean isExist = Files.exists(filePath);
    if (!isExist) {
      log.debug("File with fileName: {} was removed!", fileName);
      remove(fileName);
      return null;
    }
    long lastModified = Files.getLastModifiedTime(filePath).toMillis();
    FileStamp fileStamp = get(fileName);
    if (fileStamp != null && fileStamp.getLastModified() == lastModified) {
      return new ByteArrayInputStream(fileStamp.getContent());
    }
    updateFile(fileName);
    return getFile(fileName);
  }

I use new NIO API just to avoid file lock. As you see I check if file is present on filesystem and return null if there is no such file.
Then I check if file was modified and updated cache if it is needed.

Next method, actually three methods are responsible for inserting file in cache

private void updateFile(String fileName) throws IOException{
    remove(fileName);
    putFile(fileName);
  }

  public void putFile(String fileName) throws IOException {
    tryWriteLockOnKey(fileName, 2000L);
    try {
      if (get(fileName) != null) {
        return;
      }
      StringBuilder lookupFile = new StringBuilder(filePath);
      lookupFile.append(fileName);
      Path filePath = Paths.get(lookupFile.toString());
      byte[] fileContent = getFileContent(filePath);
      long lastModified = Files.getLastModifiedTime(filePath).toMillis();
      FileStamp fileStamp = new FileStamp(fileName, lastModified, fileContent);
      put(fileName, fileStamp);
    } finally {
      releaseWriteLockOnKey(fileName);
    }
  }

  private byte[] getFileContent(Path path) throws IOException {
    byte[] fileContent = Files.readAllBytes(path);
    return fileContent;
  }

Just to avoid parallel cache updating I lock cache record
tryWriteLockOnKey(fileName, 2000L);
In the next part will be described async call to remote server

23 коментарі:

  1. треба переврити результату tryWriteLockOnKey,
    бо можна щось не то релізнути або потерти

    але головне питання - навіщо ці файли класти в кеш? нехай собі на харді живуть

    ВідповістиВидалити
    Відповіді
    1. 2) Expected number of downloads about 10 000 000 per day

      Видалити
    2. http://ehcache.org/documentation/apis/explicitlocking
      tryWriteLockOnKey не довзоляє записувати в кеш одночасно для одного і того ж ключа

      Видалити
    3. ага, але може так статися, що лок ми так і не отримаємо за 2 секунди - а далі код процесається без урахування цього моменту

      Видалити
    4. Якщо потік не ортримає доступу за дві секунди то впаде LockTimeoutException, закачка зафейлиться
      Враховуючи що файли маленькі то це не повинно траплятись часто

      Видалити
    5. * If can't get it in timeout millis then
      * return a boolean telling that it didn't get the lock
      треба дослідити уважніше це питання, бо у них якийсь трохи мес в документації... з одного боку обіцяють повернути булівський індекатор, а в документації на LockTimeoutException обіцють викинути ексепшин...

      Видалити
    6. ага, ну і щодо ХТТП пеквесту на якийсь там урл - для цього nginx дозволяє конфігурити постакшин:
      post_action /after_download;

      location /after_download {
      proxy_pass http://10.25.0.111/servlet.do?FileName=$request&ClientIP=$remote_addr......
      }

      Видалити
    7. @Konstantin Kudryavtsev
      Згідно сорсів ехкеша - впаде LockTimeoutException

      мабуть nginx би і підійшов, але це не шлях Чака Норріса ;)

      Видалити
  2. Швидкість віддачі інфи з кеша(пам"ять) вища ніж з харда.

    ВідповістиВидалити
    Відповіді
    1. How to develop application which allows to download few dozens small files from server, write some logs about it and execute http call if download was successful?

      яка відача? кому віддача? нащо? такого німає в постановці питання:)

      Видалити
    2. є 50 маленьких файлів на серваку пратягом дня їх скачують близько 10 млн раз

      Видалити
    3. хм, а як же солюшини типу nginx, varnish? вони для цього і є

      Видалити
    4. в нас задача трекати скачування і писати статистику я не в курсі як це можна зробити за допомогою nginx чи varnish, якщо є варіанти - кажи

      Видалити
    5. nginx пише лог, його можна кастомізувати http://articles.slicehost.com/2010/8/27/customizing-nginx-web-logs
      можливо, вам слід буде робити якийсь постпроцесінг цих логів, щоб отримати потрібну статистику, але загалом цього має бути досить

      Видалити
    6. не думаю що процесінг файлів з логами буде ефективніший ніж поточне рішення, хоч як варіант можливо, але знову ж таки читабельність, гнучкість впаде та й підтримка підозрюю буде складною

      Видалити
    7. ключовий момент - те як конекшини на відадчу файлів будуть хендлитися томкетом та нджінксом.
      по суті, це як віддача звичайних статичних файл аля скриптів чи стилів - є серйозні причини використовувати проксі для цього

      короче, nginx wins by default :)

      Видалити
    8. ну ти ж знаєш як воно, потім схочуть з UI вибирати шлях до стореджа, а потім ще щось і виявиться що на nginx це все дуже непросто

      Видалити
    9. розкажи потім, чи вдається віддавати 500-700 файлів на секунду

      Видалити
  3. FileStamp could be made immutable (all fields final, remove setters, remove parameterless constructor).

    "execute http call if download was successful" - so, how did you do that?

    ВідповістиВидалити
  4. P.S.
    if (fileStamp != null && fileStamp.getLastModified() == lastModified) {
    return new ByteArrayInputStream(fileStamp.getContent());
    }
    updateFile(fileName);
    return getFile(fileName);
    Looks a bit whacky - multiple returns, unnecessary recursion. Why not just have:

    FileStamp fileStamp = get(fileName);
    if (!(fileStamp != null && fileStamp.getLastModified() == lastModified)) { updateFile(filename); fileStamp = get(fileName); }

    return fileStamp!=null?new ByteArrayInputStream(fileStamp.getContent()):null;

    But it's a minor thing of course.

    ВідповістиВидалити
    Відповіді
    1. my version is more readable perhaps except return getFile(fileName);
      should be return get(fileName);

      Видалити
    2. of course should be return new ByteArrayInputStream(get(fileName).getContent());

      Видалити