EHcache RESTful сервер, РНР и просто эксперименты…

    logoСегодня мы продолжим исследования различных новых и не очень технологий, необычного их применения или просто оригинальных вещей. Возможно, вы вспомните, я когда-то писал о проекте распределённого кеша EHcache для платформы Java. Сегодня настало время продолжить эту тему, однако в другом ракурсе — в виде отдельного RESTful сервера.


    Сперва еще раз упомянем о EHcache. Это высокопроизводительная и масштабируемая система кеширования для Java, которая является зрелым и серьезным проектом. Доступны варианты кеширования как в оперативной памяти, так и дискового кеша, а также комбинированные стратегии (также есть и вариант обеспечения сохранности данных при перезагрузках виртуальной машины или сервера). Масштабируемость реализована с помощью асинхронных репликаций и кластеризации кешей при помощи JGroup, JMS, RMI, также можно строить распределенные системы на базе сторонних продуктов (Terracota). Именно распределённость мне нравиться больше всего — индивидуальные настройки для каждого кеша (синхронная/асинхронная репликация) в паре с возможность запускать несколько различных экземпляров внутри одной JVM (или разных). Хотя следует заметить, что EHCache хранит данные в памяти JVM-процесса, соответственно, на его объем наложены некоторые ограничения (на 32-битных системах), однако дискового кеша никто не отменял, да и серьезные сервера все уже 64-битные. Известны инсталляции с 20 и боле Гб данных. Кроме этого, прекрасно поддерживается утилизация многопоточных возможностей, конкурентного доступа и многоядерности (хотя здесь можно поспорить немного, в JBoss Cache с этим вроде как ещё лучше, так как поддерживаются транзакции и кое-какие другие «вкусности», однако он сложнее в освоении, а его API достаточно сложен для понимания, у меня за два подхода никак не вышло с ним разобраться, в то время как EHcache запустился сразу).

    Кеш очень быстрый и по доступных в сети тестах обгоняет другие системы кеширования (однако в кластерах из 4-х машин JBoss Cache показывает немного лучший результат, но это система немного другого уровня), в том числе и популярнейший Memcached. Знаю, сравнение немного неверное, так как EHcache является in-process кешером, в то время как memcached отдельный демон и работает как внешний сетевой сервис (поэтому в нем нет таких ограничений на размер кеша). В то же время, если бы сравнивать, что ehcache все же более предпочтителен в силу гораздо большей гибкости и масштабируемости, комбинации памяти/диска и тонких настроек кешей. Вот здесь меня и посетила мысль… а можно ли использовать EHcache вместо memcached-а (или вместе с), при этом оставаясь на привычной мне платформе РНР? Да, можно!

    Все дело в том, что комьюнити разработчиков, кроме самого кеша, реализовало еще и кеширующий REST-сервер, доступ к которому можно получить через REST-интерфейс или SOAP. Сделано такое решение на базе embedded-версии сервера Sun GlassFish v3 Prelude и является самодостаточным, включая в себя все необходимые компоненты и зависимости. Работа с сервером происходит по протоколу HTTP, используя методы GET/POST/PUT/DELETE/OPTIONS/HEAD, либо через SOAP (также поверх HTTP) через XML. Поддерживаются все возможности HTTP/1.1, в том числе, keep-alive, а также Last-Modified, ETag, то есть, сервер отдает все корректные заголовки, поэтому часто можно использовать встроенное кеширование на промежуточных узлах при передаче или в самом клиенте. Интересным моментом является возможность работы с несколькими форматами данных, а если быть точным, возможность получить ответ в формате XML или JSON, для чего достаточно задать корректный заголовок MIME-type в запросе.

    И так, у нас есть возможность буквально в один клик запустить доступный по простому и понятному протоколу сервер кеширования, а также обращаться к нему с любого языка или платформы, которая поддерживает HTTP-запросы. Давайте попробуем!

    Загрузить последнюю версию сервера можно на SourceForge, однако я рекомендую параллельно загрузить и дистрибутив последней версии кеша, а затем обновить файлы сервера, так как в нем используется предыдущая версия кеша. Нас интересует ehcache-standalone-server, который на текущий момент имеет версию 0.7.

    В дистрибутиве уже есть скрипты для запуска, читайте README, или для запуска перейдите в каталог lib и выполните запуск вручную:

    1. java -jar ./ehcache-standalone-server-0.7.jar 8080 ../war
    * This source code was highlighted with Source Code Highlighter.


    После указания основного файла сервера стоит номер порта, по которому он будет доступен, а также путь к каталогу с веб-приложением (war-файлом). В консоли после запуска вы увидите ход подключения, а также информацию о запущенных сервисах и портах — например так я узнаю, на каком порту доступен JMX-сервис для управления. Так как используется встраиваемая версия сервера GlassFish (это и веб-сервер и сервер приложений), то его настроек и возможностей достаточно мало, но вы всегда можете развернуть полноценный сервер, не обязательно даже GlassFish, а потом использовать только сам EHcache-сервер, который доступен и отдельно, без веб-сервера.

    По-умолчанию, кеши доступны по адресу /ehcache/rest — зайдя браузером или выполнив GET-запрос, мы получим XML-документ с описанием всех текущих настроек кеша. Изначально в конфигурационном файле есть описания нескольких кешей, для примера, в том числе парочка распределенных. Для начала работы лучше всего удалить все базовые настройки и создать свои кеши. Простой кеш, без репликаций, мы сейчас сделаем.

    Все настройки кеша сконцентрированы в одном xml-файле — /war/WEB-INF/classes/ehcache.xml, который мы и будем редактировать. Внутри есть достаточно много комментариев и описаний всех опций, поэтому я только опишу кратко как сделать базовый кеш, чтобы продолжить эксперименты.

    Что означают эти опции:
    • name — имя кеша, которое будет использоваться для доступа (будет в URL, поэтому на содержание накладываются те же ограничения, лучше всего — краткое и четко обозначающее тип хранимых данных). В рамках одного сервера может быть множество кешей с разными именами.
    • maxElementsInMemory — максимальное количество элементов, которые размещаются в памяти
    • maxElementsOnDisk — максимальное количество элементов на диске (0 — без ограничений)
    • eternal — указывает, что можно игнорировать установки жизни кеша, тогда элементы будут всегда в кеше, пока вы их не удалите вручную
    • overflowToDisk — указывает, могут ли элементы быть вытеснены на диск, если достигнуто максимальное количество объектов в памяти
    • timeToIdleSeconds — время от последнего доступа к объекту до момента признания его невалидным.(если он не помечен как eternal). Опциональный параметр
    • timeToLiveSeconds — время жизни элемента (с версии 1.6, если не ошибаюсь, эта опция может быть задана для каждого отдельного объекта кеша
    • diskPersistent — указывает на сохранение состояния кеша на диске между рестартами
    • diskExpiryThreadIntervalSeconds — периодичность запуска процесса проверок объектов на диске на истечение TTL (времени жизни).
    • diskSpoolBufferSizeMB — объем пула, который выделен кешу для буферизации записи на диск. Когда пул заполнен, асинхронно вызывается команда записи на диск состояния кеша
    • memoryStoreEvictionPolicy — обозначает стратегию определения, какие объекты кеша должны быть вытеснены на диск. Может быть LRU (Least Recently Used), по дате последнего использования, FIFO (First In First Out, первый добавлен, первый вытеснен) и LFU (Less Frequently Used, по частоте использования)

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

    1. <cache name="testRestCache"
    2.       maxElementsInMemory="10000"
    3.       eternal="true"
    4.       timeToIdleSeconds="0"
    5.       timeToLiveSeconds="0"
    6.       overflowToDisk="true"
    7.       diskSpoolBufferSizeMB="4"
    8.       maxElementsOnDisk="1000000000"
    9.       diskPersistent="true"
    10.       diskExpiryThreadIntervalSeconds="3600"
    11.       memoryStoreEvictionPolicy="LFU"
    12.       />
    * This source code was highlighted with Source Code Highlighter.


    И так, наш кеш сконфигурирован так, чтобы держать в памяти 10 тыс. элементов, на диске 1 млрд, не использовать настройки времени жизни, обеспечить постоянство данных между перезагрузками. Объем пула для дисковой записи я выбрал достаточно небольшим, а время проверок на срок жизни дисковых элементов — очень большим (но думаю надо больше, в идеале — посмотрим, можно ли отключить вообще). Для чего именно такая конфигурация? Мне интересно попробовать на базе этого кеша сделать простую key-value базу данных (сегодня это очень популярная тема), при этом обеспечив себе возможность как прямого обращения к кешу из внешних сервисов, так и изнутри РНР веб-приложения. Один нюанс — даже если вам не нужна проверка на время жизни и необходим постоянный кеш, не устанавливайте параметры timeToIdle/timeToLive в 0, иначе сервер может не запускаться (вернее — сервис кеша, сам сервер стартует по выдает исправно 404 ошибку).

    Для проверки сохраните отредактированный файл ehcache.xml и перезапустите сервер. Теперь откроем в браузере URL: localhost:8080/ehcache/rest/testRestCache — вы должны получить XML-документ со всеми настройками кеша, а также текущую статистику использования кеша (объем, количество данных, процент попадания и промахов) — в дальнейшем это можно разбирать программно, чтобы выводить в нужном виде (например, в админке).

    В дальнейшем я буду рассматривать только REST-часть, для работы через SOAP вам надо поменять в URL rest на soap, получить описание сервисов в WSDL-формате и т.п. Для производительности я у себя просто отключил все неиспользуемое, в том числе ненужные мне кеши и доступ по SOAP-протоколу. Настройки сервлетов доступны в файле web.xml в директории /war/WEB-INF.

    Работа с кешем заключается в отправке запросов по HTTP-протоколу и разбору ответа. В случае ошибки, ответ будет в формате text/plain, а в теле запроса будет текст ошибки, HTTP-код будет 404 — например, вы обратились к несуществующему кешу или элементу, тогда ответом будет строка «Element not found: 333» (если вы запросили элемент с ключем 333). Но это справедливо для тех URL, которые обслуживаются сервлетом EHcache, если же ошибка будет в другой части, вы получите стандартную 404 страницу ошибки от GlassFish, которая менее приспособлена к автоматическому разбору.

    Работать можно как с сервером вообще (с менеджером кешей), так и индивидуально с каждым кешем и элементом, для этого просто дополните строку URL и используйте нужный метод с параметрами.

    Для всего кеш (CacheManager-а):

    • GET — возвращает в XML-формате список доступных кешей на сервере и их параметры. Это обычный запрос через браузер, как мы делали выше, например: localhost:8080/ehcache/rest/


    cache_manager_options


    Дальше по иерархии, если указать в URL конкретное имя кеша, над ним можно производить следующие операции:
    • OPTIONS — аналогично вышеописанному, возвращает WADL-описание доступных операций
    • HEAD — возвращает те же мета-данные с описанием параметров кеша, но в виде HTTP-заголовков, а не в теле ответа (как в GET)
    • GET — XML документ с параметрами кеша и его статистикой.
    • PUT — позволяет создать новый кеш (имя которого передано в строке URL) на основе настроек дефолтного кеша (задается в конфигурационном файле).
    • DELETE — удаляет указанный в URL кеш. Именно удаляет, а не очищает (для этого есть другая команда, как ни странно, среди операций над элементами кеша), похоже, до следующей перезагрузки сервера (но я не проверял еще этот момент).

    На уровне элементов кеша поддерживаются следующие операции:
    • OPTIONS — аналогично вышеописанному, возвращает WADL-описание доступных операций
    • HEAD — возвращает содержимое элемента в виде строки в HTTP-заголовке (здесь есть неоднозначность в справке, так как для остальных случаев HEAD дублирует GET, для элементов же кеша указано что он именно метаданные возвращает, а не значение).
    • GET — возвращает непосредственно содержимое элемента кеша в теле ответа.
    • PUT — ложит данные в кеш. Сами данные передаются в теле запроса, имя объекта — в URL, а дополнительный параметр, время жизни, можно передать в HTTP-заголовке с именем «ehcacheTimeToLiveSeconds», учитывайте, что если параметра нет, будет использован параметр из описания кеша, а интервал допустимых значений — 0 (вечно)… 2147483647 (69 лет примерно).
    • DELETE — удаляет указанный элемент. Если надо удалить все элементы кеша, используйте маску *, жаль, что другие методы такого не поддерживают (то есть, multi-get-а на уровне REST нет, хотя сам по себе кеш вполне его поддерживает в JavaAPI).

    Когда вы сохраняете элемент в кеш, можно указать его MIME-тип (из списка поддерживаемых), тогда при извлечении мы сразу получим нужные данные. Поддерживаются:
    • text/plain — обычный текст или произвольные данные
    • text/xml — XML-документ, согласно RFC 3023
    • application/json — самое интересное, JSON-формат (согласно RFC 4627)
    • application/x-java-serialized-object — сериализированный Java-объект

    Собственно, вот и все описание самого сервера, теперь практическая часть — как работать с сервером из веб-приложения на PHP. Первоначальной идеей было написание специального Cache Backend для Zend Framework, по аналогии с классом для Memcached-а, но я сначала решил просто экспериментировать, как это все может работать. Возможно, такой класс я все же напишу, если это кому-либо, кроме меня, будет интересно и полезно.

    Мы будем использовать Zend Framework для экспериментов, в частности, его классы для работы с HTTP-запросами (Zend_Http_Client) и класс для работы с JSON (Zend_Json).

    Для начала необходимо установить подключение к серверу. Zend_Http предоставляет для этого несколько возможностей, разные адаптеры, однако по тестах самым быстрым оказался Socket-адаптер, Curl я бы использовал в последнюю очередь, в случае, если сервер кеша удаленный и к нему другим способом не добраться (например, надо использовать SSL, но для кеша это странное требование, однако в ряде случаев такое необходимо, поправка — сокет также может использовать ssl).

    Опишем опции подключения, исходя из максимальной производительности, учитывая, что мы не один запрос в рамках страницы будет делать:

    1. $_config = Array(
    2.   'timeout' => 5,
    3.   'maxredirects' => 1,
    4.   'httpversion' => 1.1,
    5.   'adapter' => 'Zend_Http_Client_Adapter_Sockets',
    6.   'options' => array(
    7.      'persistent' => true,
    8.   ),
    9.   'keepalive' => true
    10. );
    * This source code was highlighted with Source Code Highlighter.


    Напомним, что наш основной URL следующий: $_url = 'http://localhost:8080/ehcache/rest/testRestCache';

    Для первого примера мы попробуем положить в кеш содержимое большого массива, в качестве которого выступит $_SERVER, при этом мы зададим JSON как тип данных (предварительно мы конвертируем массив в JSON перед отправкой).

    1. //создаем объект подключения
    2. $ehcache_connect = new Zend_Http_Client('http://localhost', $_config);
    3. //имя нашего объекта в кеше, его уникальный id
    4. $_chache_item_name = 'testitem1';
    5. // задаем полный путь к элементу
    6. // localhost:8080/ehcache/rest/testRestCache/testitem1
    7. $ehcache_connect->setUri($_url . '/' . $_chache_item_name);
    8. //укажем, что мы используем JSON
    9. $ehcache_connect->setHeaders('Content-type', 'application/json');
    10. //установим метод
    11. $ehcache_connect->setMethod(Zend_Http_Client::PUT);
    12. //добавляем данные с кодированием в JSON
    13. $ehcache_connect->setRawData(Zend_Json::encode($_SERVER));
    14. //Все! Выполняем запрос
    15. $response = $ehcache_connect->request();
    16. // мы получили ответ в виде объекта класса Zend_Http_Response
    17. if ($response->isSuccessful())
    18. {
    19.   //все ок, запрос успешен, сервер вернул правильный HTTP-ответ с кодом 200
    20.   echo 'Request OK!';
    21. }
    22. else
    23.    {
    24.      echo $response->getMessage();
    25.    }
    * This source code was highlighted with Source Code Highlighter.


    Теперь получим назад свой массив, для этого можно даже не менять URL, а только сменить тип запроса, остальное такое же, как в предыдущем коде:

    1. // URL нашего объекта
    2. $ehcache_connect->setUri($_url . '/' . $_chache_item_name);
    3.  
    4. // Метод
    5. $ehcache_connect->setMethod(Zend_Http_Client::GET);
    6.  
    7. //выполняем запрос
    8. $_result = $ehcache_connect->request();
    9.  
    10. //если все ОК
    11. if ($_result->isSuccessful())
    12. {
    13.    // получаем тело запроса и декодируем его из JSON обратно в Array
    14.    $_json_res = Zend_Json::decode($_result->getBody(), Zend_Json::TYPE_ARRAY);
    15.  
    16.    // Выведем на экран
    17.    Zend_Debug::dump($_json_res);
    18. }
    19. else
    20.    {
    21.      echo $response->getMessage();
    22.    }
    * This source code was highlighted with Source Code Highlighter.


    Остальные команды можно задать таким же способом. Первое, что немного ограничивает, что в Zend_Http нет поддержки HEAD-запросов, однако они обычно дублируют другие, так что большой надобности в них нет. Второе неудобство — метаданные о кеше или конкретных элементах отдаются в формате XML, хотя работать с элементами можно и в JSON. Статистика отдается вместе со всеми данными, хотя ее хорошо бы вынести в отдельную страницу. Третье неудобство — нет развитых возможностей по извлечению и добавлению данных. Нельзя сразу положить или запросить несколько элементов (хотя в самом Java API есть). А вот удалить все сразу вполне можно. Ну и безопасность никак не обеспечена, поэтому не храните конфиденциальные данные с доступном по HTTP наружу.

    В заключение расскажу о главной мысли этого исследования. Так как данные мы можем получить напрямую в JSON, а веб-сервер поддерживает все возможности HTTP, клиентское приложение, например, на AJAX, вполне может напрямую взаимодействовать с кешем, запрашивая данные и получая их в JSON, а серверная сторона будет асинхронно ложить новые данные, когда они есть. Клиент сперва сам может проверить, есть ли в кеше данные, а если нет — напрямую обратится к серверной части.

    Также достаточно просто реализовать шардинг кеша и балансировку нагрузки. Кстати, тогда лучше разворачивать сервер на базе полной версии GlassFish, так как в встраиваемой нет некоторых полезных возможностей, вроде админки, gzip-сжатия траффика и балансировщика нагрузки. Можно также использовать фронт-эндом nginx, который будет балансировать нагрузку между серверами, а они в фоне между собой реплицируются средствами Java. Протокол HTTP простой и достаточно гибкий, поэтому мы можем реализовать любую стратегию поведения кеширующего сервера, комбинируя возможности HTTP и платформы Java.

    P.S. Пара слов о производительности. Конечно, мои тесты далеки от реальных и никак не могут быть достоверными и вообще что-либо значить. Усредненная цифра, полученные на моей машине (ноутбук для разработки, 1.5 Гб RAM/Celeron M 1.7 Ггц, WinXP SP3) в процессе подготовки материала — 0.020 — 0.025 сек. на операции чтения/записи (если использовать cURL, то примерно в два раза дольше). Интересно конечно протестировать вариант с репликацией и балансировкой нагрузки, но это уже совсем другой уровень, но я бы с радостью принял участие и посмотрел на результаты.

    P.P.S. Отвечая на вопрос — а зачем это все? В некоторых случаях может заменить другие системы кеширования, тот же memcached, так как дает более гибкие настройки кешей, постоянство данных, различные системы репликации, отлично масштабируется и распределяется, данные могут быть получены клиентской системой напрямую (AJAX). В то же время, если у вас часть бекенда работает на Java, или даже весь, ей будет гораздо проще складывать туда данные. EHcache также может работать как отлично масштабируемая и надежная key-value база данных, обеспечивая именно репликацию и кластеризацию серьезного уровня, в отличие от множества новых решений, ehcache имеет уже продолжительную историю развития и оптимизации.

    Мне кажется, если взять только сам сервлет, обеспечивающий REST-интерфейс, и поставить его на какой-либо быстрый и максимально легкий веб-сервер, например, Tjws, добавив легкий балансировщик, выделив отдельную JVM под каждый кеш (развернув двух-узловой кластер на каждом физическом сервере по сути) — мы получим намного более быструю и легкую систему с отличной масштабируемостью. А если добавить свой сервлет, буквально несколько строк, можем организовать поддержку и других протоколов/форматов — очень интересен был бы такой кешер с возможностью получать данные через Thrift/Google ProtoBuff, учитывая, что клиенты под эти протоколы есть на клиентских машинах (на JS и ActionScript). Поле для исследований широкое и интересное, верно?
    • +18
    • 2,8k
    • 9
    Поделиться публикацией

    Похожие публикации

    Комментарии 9

      0
      Добротная статья, еще и написанная в 04:24 :)

      По ходу прочтения возникло пару вопросов:
      1. Как-то подозрительно, что по скорости он обгоняет memcached. Выделение памяти в JVM просиходит быстрее, чем обычный allocate операционной системы?

      2. Если импользовать этот кешер, какие накладные расходы по памяти могут возникнуть? Т.е. используюя например memcached ясно, что сервер занимает несколько килобайт (сотен килобайт) памяти. А как дело обстоит с ehcache?
        0
        спасибо :) писалась где-то с 21:00 вечера, и незаметно так дошло до четырех утра :)

        1. Я никак не берусь утверждать, что обгоняет. На java-платформе есть тесты (когда нативное Java-приложение обращается к EHCache и к Memcached) — там да, обгоняет, частично это связано с различной архитектурой. Не забываем также, что в случае репликаций ситуация также другая.

        2. В смысле — сколько сервер в памяти занимает или сам кеш? JVM запускается с ограничениями по памяти, да и по статистике можно получить текущий расход. Кроме этого, я бы не стал постоянно думать о памяти, а сосредоточился на количестве элементов — это более наглядная оценка, да и важная приложению (хотя когда как). При превышении памяти все будет идти на диск, так что это не будет сильной проблемой.
          0
          1. Я в Java не спец, не буду спорить :)
          2. Именно сколько сервер занимает места в памяти. Например, мне нужен 1Г кеш, а сервер+JVM занимает 150М (условно). Тогда, чтобы всё влезло в память и не свопилось, мне нужно смотреть, чтобы в системе было 1.15Г свободной памяти.

          >При превышении памяти все будет идти на диск, так что это не будет сильной проблемой

          То есть это будет не cache, а storage. И весь выигрыш в производительности будет съеден дисковыми операциями, от которых мы активно уклонялись. А полученная система будет всё больше похожа на RDBMS, где «горячие» данных хранятся в памяти, а всё остальное укладывается на диск.
            0
            в случае стандартного дистрибутива (я отключил все неиспользуемые кеши и SOAP) — 62.5 Мб. Попробую протестировать после переноса на Tjws, для интереса.

            Нет, это просто комбинированый кеш — можно конечно отключить диск вообще, но это уже по потребностям конкретным. До баз оно далеко все равно будет :) ближе к key/Value стораджам, чего я и хотел добиться, если честно (первоначальная идея вообще почему стал смотреть сюда)
              0
              Подтверждаю — если использовать ehcache-server (в виде war-файла) 0.8 версии (последняя на сегодня) и запускать под самым миниатюрным и быстрым сервером — Tjws, то java-процесс занимает 41 Мб памяти вместо 62.5 для сервера под GassFish :) скорость пока не тестировал
                0
                Всё равно эти цифры вызывают когнитивный диссонанс :)
                Хочется думать, что кешер — это что-то маленькое, шустрое, очевидное в понимании (понять как работает libevent совсем не сложно). А тут получается 41М кода крутится в памяти, и непонятно чем эта вся машинерия занимается. К слову, memcached занимает 2-3К на сервер-процесс.

                Всё сказанное выше — личное субъективное мнение. Уверен, что у ehcache есть своя ниша и он там успешно применяется.
                  0
                  более гибкое управление кешом.
                  когда я начинал исп кеширование были только гет, сет и делит… а теперь сколько там методов+ еще и бинарный протокол.

                  мир не стоит на месте, и этому проектту наверное будет зеленый свет.
            0
            JVM по определению не может обогнать написанные на Си программы.
            что-то с измерениями ни так на счет с memcached.
            скорее тормозит где-то в самом пшп, ну и для масштабируемых и высокопроизводительных систем ZF не лучший инструмент разработки.

            в nginx есть замечательный модуль memcached, который читает напрямую из кеша, что вполне можно построить для AJAX
            с записью в кеш посложнее, но я разработал модуль, который может писать в кеш, так что производительность не страдает.

            мемкеш, тоже может масштабироваться,

            за статью спасибо, интересный материал.
              0
              возможно, если бы она мне попалась раньше
              мы бы это дело обсосали бы на одном их Hi++ проектах

              пока обошлись AJAX с исп: nginx+ memceched + memcecheset {модуль проходит тестирование}

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое