• Share
  • Email
  • Embed
  • Like
  • Save
  • Private Content
"Scala in Goozy", Alexey Zlobin
 

"Scala in Goozy", Alexey Zlobin

on

  • 607 views

Talk in Scala in Goozy (), by Alexey @CheatEx Zlobin, at scalaby#8

Talk in Scala in Goozy (), by Alexey @CheatEx Zlobin, at scalaby#8

Statistics

Views

Total Views
607
Views on SlideShare
607
Embed Views
0

Actions

Likes
0
Downloads
1
Comments
0

0 Embeds 0

No embeds

Accessibility

Categories

Upload Details

Uploaded via as Microsoft PowerPoint

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment
  • Данном докладе я расскажу о нашем с Юрой опыте использования Scala в живом проекте. Я не буду пытаться давать обзоры использованных технологий, в место этого доклад будет сконцентрирован на встреченных и найденных решениях.
  • Мой доклад будет проходить по следующему плану: 1. Сначала я кратко расскажу о сервисе который мы собственно разрабатываем. 2. Потом я рассмотрю место которое занимает Scala, точнее написанные на ней компоненты в проекте. И расскажу почему именно она была выбрана для их реализации. 3. Далее расскажу о том как в проекте используется известный фреймворк lift. 4. В четвёртой части я дам обзор логического устройства и того как на его становление повлиял широко разрекламированный cake pattern. 5. В пятой части будет рассказано про использовани библиотеки Scalaz в проекте. Закончу я  тем, что расскажу про основные усвоенные из этого проекта уроки.
  • Итак, наш проект Goozy - социальная сеть, построенная вокруг концепции стикеров дял интернет-страниц. Основаня идея в том, что пользователь может оставлять в сети заметки с помощью браузерных плагинов и делиться имим со своими друзьями. Тут важно заметить, что помимо собственно оставления и просмотра стикера центральной концепцией интерфейса сервиса является так называемая "лента" - поток стикеров и комментариев от друзей данного пользователя. Проект уже достаточно пожилой и пережил несколько итераций разработки. Текущая итерация началась зимой с амбициозной целью построения бэкэнда для этого сервиса, способного обслуживать до миллиона активных пользователей. Современная архитектура проета выглядит так:...
  • Хранение данных доверено популярной ныне MongoDB. Это документоориентированаая БД, со встроенной поддержкой шардинга и репликации. Непосредственно с ней общаются так анзываемые сервера API. Они экспортируют REST интервейсы доступа к данным. Кроме того они отвечают за взаимодействие с внешними сервисами, такими как S3 и системы полнотекстового поиска. Несколько серверов подключаются к балансировщику нагрузки. К нему же подключаютс сервера с развернутым порталом проекта. Портал не общается напряму с БД и делегирует большинство взаимодействий с внешними сервисами API. Во внешний мир смотрит специально настроенный интерфейс балансера, открывающий доступ к порталу и предназначенным для этого методам API. Клиентские приложения прозрачно общаются с API и порталом по протоколу HTTP.
  • Основные функции, которые он на себя берёт это: 1. Управление доступом к хранилищам данных. 2. Выполнение части задач обработки данных в фоне. 3. Отсылка писем. 4. Направление необходимых данных в текстовые индексы. Тут встаёт вопрос: почему же для его реализации был выбран язык Scala? У меня есть следующие ответы...
  • Во-первых программы на нём быстры. Плохой алгоритм он не выправит, но написанный хорошо скорее всего будет работать со скоростью не очень далёкой от максимальной. Во-вторых он краток. Он не выедает время на ввод текста и поиск по нему. Более менее целостный компонент приложения обычно умещается на одном экране. Это очень положительно влияет на скорость работы. Он выразиетен. Если на нём нужно что-то сказать можно взять и сказать именно это. С ним нужда в ритуалах, не связанных с решаемой проблемой, минимальна. В есть целый ряд инструментов отсутствующих в других ОО-языках. Многие из них радикально упрощают решение архитектурных проблем. В первую очередь это traits (Кстати где-то видел их перевод как "типажи". Кто-нибудь им пользуется?). Кроме того более глубокая поддержка обобщённых типов положительно влияет ан стандартную библиотеку и соответственно почти на весь код. В ней есть развитая поддержка функционального программирования. В первую очередь это простая работа с константами. Как следствие значительно проще, чем в Java пистаь корректный многопоточный код. Ну и наконец нам доступен весь огромный арсенах библиотек, написанных на Java. Прежде чем углубиться в технические моменты пара слов о том, что вызывает наибольшие страхи перед переходом на Scala. Это люди, которые ан ней должны писать...
  • Итак, команда наша состояла из 2 человек. Все имели заметный опыт работы с Java в самых разнообразных направлениях. Ну и что важно все фанаты своего дела и любят новые технологические игрушки. Как следствие вопросы вроде разобраться с новой штукой или продраться через трёхэтажную ругань компилятора на несходящиеся типы никогда не превращались в проблемы. Теперь можно переходить и к технической стороне вопроса. Первое о чём хочется рассказать - это опыт применения Lift'а для создания REST-сервиса.
  • В связи с узостью решаемой задачи из всего огромного вреймворка импользуется буквально два модуля. Это поддержка REST сервисов и автоматическая сериализация case классов в JSON. Несмотря на скромность используемого функционала мы натолкнулись на несколько проблем.
  • Две связаны с производительностью и одна эстетического плана. Во первых мы столкнулись с очень низкой производительностью встроенного механизма интернационализации. Большие критические секции в его коде привели к тому, что на топовом EC2 инстансе мы могли обрабатывать порядка 20 запросов в секунду. Надо сказать, что это был очень грустный момент. Крутая модная БД, быстрый язык, ооочень много ядер и 20 запросов :( Но проблема нашлась быстро, код был переведён на на стандартные ResourceBundles, что увиличило пропускную способность в 4 раза. Вторая проблема выявленная, но не решённая - это потребление памяти при сериализации в JSON. Если не вдаваться в детали, то одни и те-же данные там переупаковываются 3 раза,, что накладывает определённые ограничения на производительность... Ну и последнее это сложный исходный код самого Lift. Местами просто сборник худших стереотипов о Scala. На этапе освоения невозможность быстро читать исходный код сильно тормозила разработку.
  • Теперь давайте немного рассмотрим архитектуру нашего API сервера. Логически она разнесена на честные (кстати впервые в моей практике) три уровня. Контролер - это код непосредственно связанный с лифтом и осуществляющий преобразование данных из HTTP во внутренние модели данных и обратно. Сервисы это фасады для операций, реализуемых сервером. Ещё целостные в терминах API (одна экспортируемая наружу операция - один метод), но уже оперирующие внутренними моделями данных. Внутри себя они координируют работу нескольких служб более низкого уровня, таких как рассылка почты,текстовый поиск, взаимодействие с внешними сервисами. Сложные операции с данными делегируются третьему уровню. Он отвечает за сохранение моделей данных в БД и их загрузку оттуда. Каждый уровень состоит из нескольких компонентов, образованных операциями над близкими классами моделей: пользователями, стикерами, лентой и т.д. Обеспечение поддерживаемости всего этого хозяйства имеет определённые трудности...
  • Во-первых компоненты каждого уровня обычно зависят друг от друга. Скажем загрузка объекта стикера требует загрузки объекта пользователя, который его оставил и т.п. Вторая проблема сотоит в том, и от предыдущего уровня компоненты имеют несколько зависимостей. Кроме того внутри одного уровня обычно имеется большое количество разделяемых данных и объектов. Например сконфигурированный пул соединений с БД или сервисом поиска.
  • Решение этих труднойстей мы попробовали найти в популярном в сообществе cake pattern. (Уточнить нужны ли пояснения) Я попробую сформулировать основные результаты этого решения...
  • Из положительного мы имеем: 1. Архитектура приложения выражена непосредственно в коде объёмом около 100 строк. Имеется в виду собственно конструирование рантайм компонентом и описанием всех зависимостей между ними. 2. Корректность описания зависимостей проверяется компилятором. 3. Сами компоненты получились сравнительно компактными. Размер отдельных классов в основном не превышает 100 строк. Основные проблемы которые при этом наблюдаются: 1. Длинные (иногда очень длинные) списки зависимостей у отдельных компонентов. Видимо это проблема нашего дизайна, но при использовании например Spring её можно спрятать подальше. 2. Неявная зависимость от порядка перечисления примесей в описании рантайм конфигурации. Случайное изменение этого порядка рефакторинг связанный с переносом функционала между компонентами часто видёт к падению приложения при старте. К счастью этот класс ошибок гарантированно отлавливается простейшим тестом. 3. Не понятно что делать, если у нас есть несколько одинаковых по типу зависимостей, которые однако должны иметь разные имплементации в рантайме. Например у нас есть две очереди сообщений и компонент должен уметь обращаться к обоим. Создание отдельного класса для каждого назначения одного и того же интерфейса не кажется хорошей идеей. Теперь пора рассказать про нашу любимую игрушку в проекте:
  • Одной из проблем вставшей на ранней стадии разработки проекта был корректные ответы клиенту при ошибках. То есть ответ должен был содержать краткое её описание, например "входной параметр вне допустимого диапазона" и правильный http-код. Естественно если у нас обнаруживалось несколько проблем доложить хотелось обо всех, что крайне осложняло использование исключений для этой цели. В то время я как раз наткнулся на пост известного скала-блоггера Дебашиша Гоша. И однажды я бросил Юре ссылку на блогпост с описанием обработки ошибок с помощью апликативных функторов. День спокойно проработал над какой-то фичёй, а уже следующим утром при обновлении и общего репозитария получил код из того поста...
  • Как собственно осуществлялся переход. В начале у нас был набор наших исключений которые при случае выбрасывались. И некоторые из них преобразовывались в ответы клиенту. Выглядели они так.
  • Собственно первым шагом для использования валидаций всегда должно становится определение типа ошибки. Так как конечной целью всей этой затеи было отправлять корректные ответы по http, то были созданы вот такие вот классы (их естественно было много больше) которые несли в себе данные об ошибке и нужный http-код. Всё было хорошо мы начали использоать вот такие вот структуры для сообщений об ошибках в параметрах запросов. Потихоньку распространяли это вглубь, на логику работы с БД... Но тут до нас дошло
  • В JVM бывают исключения. Они бывают по разным причинам, именно их используют многие замечательные библиотеки вроде casbah. И оборачивать каждое обращение к ним в try/catch абсолютно нереально. Мало того, кода бросающего наши исключения уже было немало.
  • Для решения проблемы была выбрана следующая стратегия: определить функцию конвертер из всех исключений в наши ошибки. Написать небольшую бибилотеку которая упростила бы обращение к этой функции из нашего кода.
  • В первую очередь мы добавили ещё одну унверсальную ошибку. Полагая что непредвиденное и не имеющее соответствующего маппинга исключение - это наш косяк и его надо рапортавать 500 кодом.
  • Написали такой вот простой конвертер.
  • И маленький набор утилит. Свой алиас для валидаций с подставленным типом ошибки. Обратите внимание что это очень удобно делать в любом проекте, так как мы получаем kind с 1 параметром, что в силу особенностей scala радикально упрощает написание некоторых сигнатур. Посмотрите на пример использования sequence - 80% этого ужаса уходит если есть такой алиас. Здесь я использовал очень интересную и очень малоизвестную часть стандартной библиотеки scala.control.Exceptions. Она демонстрирует интересный подход к проблеме функциональной обработки исключений, советую посмотреть.
  • Ещё была написана куча утилит для безопасного парсинга разных типов данных, работы с mongo объектами и ещё чем-то... Я их сюда не вставлял, так как перед новым годом накатал изрядный текст не тему как их писать, они достаточно тривиальны.
  • Имея весть этот арсенал мы начали плавно двигать валидации глбже и глубже в наш код... Получилось вполне себе мило, была куча вот такого вот симпатичного кода. Хаскелисты должны прослезится...
  • Но в процессе появлялся и другой код...
  • Вот такой вот... Возможно причина его появления в моём низком мастерстве использования этого аппарата. Но мне кажется что это фундаментальная проблема: есть класс кода который значительно читабельнее в старой доброй императивной форме с исключениями.
  • Так это или нет, нов итоге сформировался такой подход. Нов итоге сформировался такой подход: там где это проще мы пользуемся исключениями. Но стараемся переходить на валидации как можно раньше (на более низких уровнях логики) для того чтобы высокоуровневая логика могла следовать букве ФП.
  • Итак, код, совершающий много всякого ввода-вывода из сображений производительности и читабельности живёт с исключениями. интерфейсные методы небезопасных компонентов заворачиваются в специальные обёртки. Они перехватывают исключения и преобразуют их по определённым правилам в ошибки валидации. Правила задаются в одном месте глобально для всего приложения. Кстати при этом использовался стандартный пакет control.exceptions. Уровень сервисов и контроллеры оперируют более менее надёжными значениями, однако и они не застрахованы от выброса исключений. Для того чтобы обеспечить надёжную обработки и репортинг об ошибках не уровне лифта встроен дополнительный обработчик исключений, который делегирует всё к тому-же методу преобразования. На переходе между контроллерами и лифтом встроен код, который знает как из ошибок строить корректные с точки зрения HTTP ответы. В общем данная схема показала свою работоспособность. И более того устойчивость к отдельным нарушениям описанных соглашений (в том смысле, что отчёты об ошибках становятся менее точны, но не сводятся к метаю в пользователя стэктрейсами).
  • Итак, каков наш опыт использования валидаций. Во-первых положительные моменты: 1. Более менее понятный способ аггрегации ошибок валидации. 2. Мало того это практически единственный способ который может работать в условиях нескольких десятков параметров с 2-3 ограничениями на каждый. 3. Также нам удолась выстроить достаточно унифицированный подход к рапортам об ошибках как во входящих параметрах, так и об ошибках времени выполнения. 4. Ну и корректность обработки ошибок проверяется автоматически компилятором. Из проблемных моментов можно выделить: 1. Монады и функторы требуют долгой медитации, которая не проходит бесследно для сознания. 2. Иногда бывает сложно найти общий язык с компилятором. 3. Ну и наиболее сложная для преодоления проблема...

"Scala in Goozy", Alexey Zlobin "Scala in Goozy", Alexey Zlobin Presentation Transcript

  • Scala in Goozy Alexey Zlobin, e-Legion [email_address] @CheatEx
  • Index
      • Goozy overview
      • Scala's place
      • Lift
      • Cake pattern in scale
      • Scalaz and other fancy stuff
      • Summary
  • What is Goozy?
    • A social network built around the concept of sticky note
      • A note could be left anywhere in the Web.
      • It is associated with particular page element.
    • Central UI concept: user's related feed with all new notes and comments
  • Top-level architecture
  • Scala's place
    • API server
    • Main functions:
      • Storage access
      • Background tasks (feed writing)
      • Email sending
      • Text indexing
  • Why scala?
      • Fast
      • Сoncise
      • Expressive
      • Advanced OO
      • Some functional stuff
        • Simple concurrency
      • All Java legacy available
  • The team
      • 2 persons
      • Strong Java background
      • Fancy about technologies
      • Love to try new things
  • Lift
    • Utilized features:
      • REST
      • JSON serialisation
    • That's it...
  • Lift: issues
    • Localisation performance
      • Hand-made localisation on standard resource bundles gave 4 times throughput improvement.
    • Very memory-consuming JSON serialisation.
      • Not so efficient PrettyPrinter is used
      • Functional-styled string escaping
    • Poor code style
      • Extremely long map-match-if hierarchies
      • Mutable, difficult to debug LiftRules design
  • Goozy API logical structure
    • Application is composed from three layers.
    • Each layer consists from several similar components. Like UserController, UserService, etc.
  • Conceptual problems
      • Components of each level depend from each other
      • Components most likely have several dependencies from the previous level
      •   A lot of common stuff inside a level
        • Every storage needs a DB connection
        • Every service needs an entire storage system and access to text indexes
        • Etc...
  • The solution: cake pattern
      • Each logically closed piece of functionality is represented as component
      • Dependencies are expressed as self-types
      • Common features expressed as mix-ins
      • Common combinations of functionality are expressed as mix-ins of several components
  • Cake pattern: consequences
    • + All top-level architecture is expressed in one less than 100 LOC file
    • + Compile-time dependency checks
    • + The biggest file is around 1000 LOC
    • - Long dependency lists
      • Poor design?
    • - Implicit dependency on mix-in order (linearisation strikes back)
      • Prefer def and lazy
    • - A bit unclear how to deal with several dependencies of the same type but different runtime implementation
  • scalaz.Validation
    • One sweet morning I sent a link to the colleague...
    • On the next morning we had a new dependency and totally refactored request parameters analysis.
    • Now everything is validated.
  • Initial solution: domain exceptions
    • class GoozzyException(
    •      val message: String) extends
    • Exception(message)
    • case class UserNotFound(
    •      userId: String) extends
    • GoozzyException(
    •      "user " + userId + " not found")
  • Error handling: own error type
    • abstract class Error(
    •      val httpCode: Int,
    •      val code: String)
    • abstract class PermissionError(
    •      code: String) extends
    • ValidationError(403, code)
    • case class Banned(
    •      userId: String,
    •     groupId: String) extends
    • PermissionError("BANNED")
  • Validations and exceptions
    • Problem: exceptions are here
    • Solution: catch'em and convert!
  • Validations and exceptions
      • Define the "converter"
      • Write some utilities to make them more accessible
  • One more error for whatever happen
    • case class ExceptionalError(
    •      @transient exception: Throwable) extends
    • InternalError
  • The converter
    • Function is a perfect abstraction!
    • val dispatchException: Function[
    •      Throwable,
    •      Error] = {
    •     case UserNotFound(name) =>
    •          MissedEntity(name, "user")
    •     ...
    •     //don't remove it!!!
    •     case t => ExceptionalError(t)
    •   }
  • Useful utils: generic
    • type GzVal[+OUT] = Validation[Error, OUT]
    • def safeVal[T]: Catch[GzVal[T]] =
    •      handling(classOf[Throwable]) by { e =>
    •          dispatchException(e).fail
    •      }
    • def safe[T](block: => T): GzVal[T] =
    •      safeVal( block.success )
    • def editData(up: Edit):
    •      GzVal[Data] = safeVal {
    •         //all the dangerous stuff here
    •      }
  • Useful utils: specific
    • Just a trivial code which parses strings, extracts values from mongo objects, etc.
    • See my blog for details...
  • Validation: the good thing
    • Some code was pretty:
    • for {
    •     data <- getData(recordId)
    •     userData <- getUserData(userId)
    •      permission <-
    •          checkPermission(data, userData)
    •     newData <- updateData(data)
    • } yield {
    •     //all dirty hacks there
    • }
  • Conversion: the problem
    • But some other code was...
  • Not so pretty
    • def readFields(rec: DBObject, ...): GzVal[Fields] = {
    •   val deletedBy =
    •     for (userId <- get[ObjectId](note, &quot;deleted_by&quot;);
    •             user <- getUserData(userId).toSuccess(MissedEntity(...)))
    •     yield user
    •   for {
    •      id <- get[String](rec, &quot;_id&quot;)
    •     content <- get[String](rec, &quot;content&quot;)
    •     updated <- asValidOption(get[DateTime](rec, &quot;upd&quot;))
    •     //twelve more
    •     user <- getUserById(userId, currentUserId map
    •        (new ObjectId(_)))
    •         .toSuccess(
    •            MissedEntity(userId.toString, &quot;user&quot;):
    •              ValidationError)
    •   } yield DataRecord(/*you won't see it*/)
    • }
  • Improved solution
      • Don't play haskell
      • Play java
      • ...when it is easier
  • Error handling: big picture
  • Validation: pros and cons
    • + Comprehensible error aggregation and reporting
    • + The only imaginable way to deal with 20+ request parameters with 2-3 constraints on each
    • + Unified approach to request validation and runtime error handling
    • + Type-level check for correct error handling
    • - Monads and Applicatives cause massive brain damage
    • - Complicated error reports from the compiler
    • - You can't just ignore possibility of runtime exception
  • Lessons
      • Always prefer simple tools
      • Options and Eithers (Validations): they really work
        • It is possible to live with both exceptions and eithers
        • Performance consequences are not clear
        • Some times I had to use things far behind my understanding (sequence, traverse)
      • Server-side testing is difficult
        • Testing approach should be established before any code is written
  • References
      • http://www.assembla.com/spaces/liftweb/wiki/REST_Web_Services  - REST support in Lift 
      • http://jonasboner.com/2008/10/06/real-world-scala-dependency-injection-di.html  - complete cake pattern intro
      • https://gist.github.com/970717  - easy Validation example
      •   http://www.scala-lang.org/api/current/scala/util/control/Exception$.html  - built-in DSL for exception handling