Upload
nicolas-martignole
View
866
Download
0
Tags:
Embed Size (px)
DESCRIPTION
Retour d'expérience sur le développement d'une application avec Play2/Scala/Redis pendant un an chez Zaptravel.
Citation preview
Voyager avec Play 2
Nicolas [email protected]
@nmartignoleScala.IO - 24/25 octobre 2013, Paris
samedi 26 octobre 13
Votre plan de vol
ZapTravel
WhatWhyHow
samedi 26 octobre 13
Avant de commencer...
Current status
samedi 26 octobre 13
ZapTravel
samedi 26 octobre 13
We search destinations & dates, then find the best price, hotel and
transport, so you don’t need to
samedi 26 octobre 13
We search destinations & dates, then find the best price, hotel and
transport, so you don’t need to
filter, map, reduce
samedi 26 octobre 13
ZapTravel
samedi 26 octobre 13
samedi 26 octobre 13
samedi 26 octobre 13
samedi 26 octobre 13
MobileAPI REST
samedi 26 octobre 13
samedi 26 octobre 13
samedi 26 octobre 13
samedi 26 octobre 13
samedi 26 octobre 13
RomanceShow medeals
samedi 26 octobre 13
FamilyShow medeals
samedi 26 octobre 13
samedi 26 octobre 13
</zaptravel>samedi 26 octobre 13
“There are known knowns; there are things we know that we know.There are known unknowns; that is to say, there are things that we now know we don't know.But there are also unknown unknowns – there are things we do not know we don't know.
”—United States Secretary of Defense, Donald Rumsfeld
samedi 26 octobre 13
Aware
KnowDon’t know
Not aware
samedi 26 octobre 13
Aware
KnowDon’t know
Not aware
samedi 26 octobre 13
Quelques chiffres
• 159 000 hôtels
• 1383 destinations
• 840 000 transports (avions/trains)
• 1.4To images sur S3
• 20600 prix chambres hôtels
samedi 26 octobre 13
redis 127.0.0.1:6379> hlen Hotel:Content:Short(integer) 158 041
samedi 26 octobre 13
Ce que je savais
samedi 26 octobre 13
Ce que je savais
• Play! Framework
samedi 26 octobre 13
Ce que je savais
• Play! Framework
•Web development
samedi 26 octobre 13
Ce que je savais
• Play! Framework
•Web development
•Hiring and training developers
samedi 26 octobre 13
Ce que je savais
• Play! Framework
•Web development
•Hiring and training developers
• Kiss-ass project managment
samedi 26 octobre 13
EquipePlay2 ScalaHTML CSS ScalaJavaZe Boss
samedi 26 octobre 13
Equipe
0
1,25
2,5
3,75
5
Mai 2012 Ete 2012 Sept 2012 Oct 2012 Nov 2012 Jan 2013 Oct 2013
Play2 ScalaHTML CSS ScalaJavaZe Boss
samedi 26 octobre 13
samedi 26 octobre 13
Comment apprendre Scala
(et désapprendre Java)
samedi 26 octobre 13
Scala et Zaptravel
• Scala => recrutement
• Facile à apprendre
• Scala c’est simple
samedi 26 octobre 13
Ah tu fais du Scala
samedi 26 octobre 13
Paradigme objet ET fonctionnel
http://parleys.com/p/51c1994ae4b0d38b54f4621b
samedi 26 octobre 13
Ce que j’ai évité
- 18
samedi 26 octobre 13
SBT
samedi 26 octobre 13
SBT
ScalaZ
samedi 26 octobre 13
Les choses que je ne savais pas
• Faut être gonflé
• Communauté
• Parallélisme, Reactivité
• Play2/Scala/Redis en PROD ???
• SEO
• JSON+Redis
• Typesafe / refactoring
samedi 26 octobre 13
samedi 26 octobre 13
Communauté Scala
Place de Scala, la communauté, par rapport à Java
samedi 26 octobre 13
94 516 VUEs
Scala Days 2013 on parleys.com
samedi 26 octobre 13
Parallélisme et concurrence
It’s the web, stupid
Response[HTML] = Fx(Request)
samedi 26 octobre 13
Typesafe
•HTML template•routes•config•LESS
samedi 26 octobre 13
Play2 -> ReactiveReactive In Practice
samedi 26 octobre 13
Play2 -> ReactiveReactive In Practice
samedi 26 octobre 13
août - sept 2012R.I.P
Iteratee, Enumeratee and Enumerator on Zaptravel
samedi 26 octobre 13
BusinessExceptionIteratee/Enumeratee/Enumerator c’est cool, mais nous n’en n’avons pas besoin pour le moment.
samedi 26 octobre 13
/**
* Server sent event streaming controller.
* Date: 06/08/12
* Time: 12:16
*/
object Streaming extends Controller {
// Streaming using server sent event
def stream(requestId: String) = Action {
// Define an implicit EventNameExtractor wich extract the "event" name from the Json event so that the EventSource() sets
the event in the message
implicit val eventNameExtractor: EventNameExtractor[JsValue]=EventNameExtractor[JsValue](eventName = (zepEvent)=>zepEvent.\
("event").asOpt[String])
// Streams.events is a composition of HotelPrice and AirfarePrice.
Ok.feed(Streams.events(requestId) &> EventSource()).as("text/event-stream")
}
implicit val eventNameExtractor: EventNameExtractor[JsValue] =EventNameExtractor[JsValue](eventName = (zepEvent)=>zepEvent.\("event").asOpt[String])
samedi 26 octobre 13
Akkafaukon
samedi 26 octobre 13
Akka•Cron Jobs
• Emails (Mailjet/Mailchimp)
• ElasticSearch index (proto)
• Sitemap
•Generate content (R.I.P.)
samedi 26 octobre 13
Akka
samedi 26 octobre 13
Dev ?
EC2 m2.2xlarge
Play2
Redis
RedisPrices
Prix Hotels, Avions,Voitures, Trains, Rating Hotel
27GB870k obj
EC2 m2.xlarge
Lieux, Destinations,Contenu, Routage, URLs, Places, Tags,
Webuser
1.2 GB450k obj
RedisStatic
slave-of
read-only
read/write
S3
samedi 26 octobre 13
Prod ?
Cloudfront ELBRoute53
EC2 c1.medium
wwwredis prices
EC2 m2.2xlarge
redis static
EC2 m2.xlarge
SimpleDB
logs
CloudWatch
S3
redis backup
samedi 26 octobre 13
Play2 + AWS
samedi 26 octobre 13
Idéal : c1.medium
2 vCPUs1.7 GB mémoire
samedi 26 octobre 13
Redis
samedi 26 octobre 13
Ce qu’il faut retenir
• IaaS versus PaaS pour Zaptravel
2300 USD / mois
samedi 26 octobre 13
Boarding...
samedi 26 octobre 13
Des cas d’usages
samedi 26 octobre 13
Fonctionnalités
samedi 26 octobre 13
Fonctionnalités• API REST
samedi 26 octobre 13
Fonctionnalités• API REST
samedi 26 octobre 13
Fonctionnalités• API REST
• Weather
samedi 26 octobre 13
Fonctionnalités• API REST
• Weather
• GeoIP
samedi 26 octobre 13
Fonctionnalités• API REST
• Weather
• GeoIP
• Semantic Search
samedi 26 octobre 13
Fonctionnalités• API REST
• Weather
• GeoIP
• Semantic Search
• Cache Redis
samedi 26 octobre 13
Fonctionnalités• API REST
• Weather
• GeoIP
• Semantic Search
• Cache Redis
• Mobile Web version
samedi 26 octobre 13
Fonctionnalités• API REST
• Weather
• GeoIP
• Semantic Search
• Cache Redis
• Mobile Web version
• Authentification
samedi 26 octobre 13
Fonctionnalités• API REST
• Weather
• GeoIP
• Semantic Search
• Cache Redis
• Mobile Web version
• Authentification
• Statistiques/Parcours visiteur
samedi 26 octobre 13
Fonctionnalités• API REST
• Weather
• GeoIP
• Semantic Search
• Cache Redis
• Mobile Web version
• Authentification
• Statistiques/Parcours visiteur
samedi 26 octobre 13
Quelques exemples
ZapTravel
samedi 26 octobre 13
Charger une donnée venant de Redis
ZapTravel
samedi 26 octobre 13
Architecture
LB
Web
Web
Web
HTTPHTTPS
RedisAir/Hotel/Cars/Ac
RedisResa/Users
RedisWeb Content
ZapTravel
samedi 26 octobre 13
Architecture
LB
Web
Web
Web
HTTPHTTPS
RedisAir/Hotel/Cars/Ac
RedisResa/Users
RedisWeb Content
Web
redis
ZapTravel
samedi 26 octobre 13
Cas d’usage
ZapTravel
Donne moi le label qui correspond à originId =380
samedi 26 octobre 13
Cas d’usage
ZapTravel
Donne moi le label qui correspond à originId =380
def getSlug(originId: Long): Option[String] = Redis.pool.withClient { client => Option(client.hget("Url:From:Rev", originId.toString))}
samedi 26 octobre 13
Cas d’usage
ZapTravel
Donne moi le label qui correspond à originId =380
def getSlug(originId: Long): Option[String] = Redis.pool.withClient { client => Option(client.hget("Url:From:Rev", originId.toString))}
samedi 26 octobre 13
Cas d’usage
ZapTravel
Donne moi le label qui correspond à originId =380
def getSlug(originId: Long): Option[String] = Redis.pool.withClient { client => Option(client.hget("Url:From:Rev", originId.toString))}
Driver Sedis https://github.com/pk11/sedissamedi 26 octobre 13
Un mot sur les Tests
samedi 26 octobre 13
https://gist.github.com/nicmarti/5064048
package models import org.specs2.mutable._ import play.api.test._import play.api.test.Helpers._ class OriginSpecs extends Specification { "An Origin" should { "returns the slug for a valid origin" in { running(FakeApplication()) { Origin.getSlug(380) mustEqual Some("from-london") Origin.getSlug(1) mustEqual Some("from-paris") Origin.getSlug(-9999) mustEqual None } } }}
samedi 26 octobre 13
Charger un objet
ZapTravel
Charge moi un Objet «Londres»
samedi 26 octobre 13
Charger un objet Origin
ZapTravel
def getOrigin(originId: Long): Option[Origin] = Redis.pool.withClient { client => Option(client.hget("Url:From:Rev", originId.toString)).map{ slug=> Option(client.hget("Places:Place:"+originId, "display").map { .... ... } }}
1) charger from-london
samedi 26 octobre 13
Code smells
ZapTravel
2) charger display...
def getOrigin(originId: Long): Option[Origin] = Redis.pool.withClient { client => Option(client.hget("Url:From:Rev", originId.toString)).map{ slug=> Option(client.hget("Places:Place:"+originId, "display").map{ .... ... } }}
samedi 26 octobre 13
Cas d’usage
ZapTravel
def getOrigin(originId: Long): Option[Origin] = Redis.pool.withClient { client => for(slug<-Option(client.hget("Url:From:Rev", originId.toString)); display<-Option(client.hget("Places:Place:"+originId,"display") )) yield Origin(originId,display,slug)
}}
2) charger display...
for-comprehensionhttps://gist.github.com/nicmarti/5064066
samedi 26 octobre 13
La Tour Eiffel
ZapTravel
1. Charger du JSON à partir de Redis2. Interpréter et retourner un objet PointOfInterest
samedi 26 octobre 13
{"name":"Eiffel Tower","address":"","latitude":"48.8582493546","longitude":"2.2945117950","website":"www.tour-eiffel.fr","rank":3,"photo":{"r":"eiffel-tower-paris-france","k":"6b56","e":"jpg","w":2406,"h":1600,"a":"Mirari Erdoiza","l":"http:\\/\\/www.fotopedia.com\\/items\\/anboto-RiKxAA3gE6I"},"sentences":{"gbs":[{"d":"The Eiffel Tower is one of the most famous monuments in the world (324 metres, 10,100 tonnes).","a":"Paris","l":"http:\\/\\/www.paris.com\\/paris_landmarks\\/monuments\\/eiffel_tower_paris"},{"d":"This is without doubt one of the most recognizable structures in the world.","a":"Frommers","l":"http:\\/\\/www.frommers.com\\/destinations\\/paris\\/A25288.html"},{"d":"If the Statue of Liberty is emblematic of New York, Big Ben is London, and the Kremlin is Moscow, then the Eiffel Tower is the symbol of Paris.","a":"Fodors","l":"http:\\/\\/www.fodors.com\\/world\\/europe\\/france\\/paris\\/review-97417.html"},{"d":"When it was built for the 1889 Exposition Universelle (World Fair), marking the centenary of the Revolution, the Tour Eiffel faced massive opposition from Paris' artistic and literary elite.","a":"Lonely Planet","l":"http:\\/\\/www.lonelyplanet.com\\/france\\/paris\\/sights\\/famous-landmark\\/eiffel-tower"}],"tips":[{"d":"It's pretty high!.","a":"annawelford","l":"http:\\/\\/www.lonelyplanet.com\\/france\\/paris\\/sights\\/famous-landmark\\/eiffel-tower","s":"Lonely Planet"},{"d":"Bigger than you think.","a":"anomolly","l":"http:\\/\\/www.lonelyplanet.com\\/france\\/paris\\/sights\\/famous-landmark\\/eiffel-tower","s":"Lonely Planet"},{"d":"Overcrowded.","a":"anshjain","l":"http:\\/\\/www.lonelyplanet.com\\/france\\/paris\\/sights\\/famous-landmark\\/eiffel-tower","s":"Lonely Planet"},{"d":"The restaurant on the first floor is an amazing experience!.","a":"ansofie","l":"http:\\/\\/www.lonelyplanet.com\\/france\\/paris\\/sights\\/famous-landmark\\/eiffel-tower","s":"Lonely Planet"}]},"tags":["Landmark","Memorials\\/Monuments","Sights","Famous landmark"]}
HGET Pois:PoisHash 52511
samedi 26 octobre 13
Play 2.1
• Définir une case class POI
• Définir un implicit Json.format[POI]
•C’est tout... ou presque
samedi 26 octobre 13
Play 2.0
case class POI(name: String, address: String, latitude: String, longitude: String, website: Option[String], photo: Option[SightPhoto] = None, sentences: Sentences, tags: Option[List[String]])
POI = Point of Interest = notre Tour Eiffel
samedi 26 octobre 13
Play 2.0
samedi 26 octobre 13
Play 2.1
samedi 26 octobre 13
Play 2.1(Parser lorsque le JSON stocké sur Redis utilise une déclaration différente
de la case class)
samedi 26 octobre 13
Appel Redis et interprétation JSON
samedi 26 octobre 13
Afficher une listeZapTravel
samedi 26 octobre 13
Afficher une liste
ZapTravel
samedi 26 octobre 13
Aller sur Redisdef allOrigins: List[Origin] = Redis.pool.withClient { client => // ... // ... }
Modèlesamedi 26 octobre 13
Préparer une listedef allUrlOrigins: Seq[(String, String)] = { Origin.allOrigins.map{ origin => (origin.slug, origin.label) }.sortBy(_._2)}
Contrôleursamedi 26 octobre 13
Envoyer la liste au template
<label for="location">Your travel origin is :</label>
@select( userForm("originCity"),
FolioCriteria.allUrlOrigins , '_label -> "Travel from origin", '_showConstraints -> false)
Code dans la page HTML
Vuesamedi 26 octobre 13
Afficher une liste
ZapTravel
samedi 26 octobre 13
Gérer l’authentification
samedi 26 octobre 13
Comment protéger l’accès à une ressource ?
My Info
samedi 26 octobre 13
Comment protéger l’accès à une ressource ?
My Info
samedi 26 octobre 13
Dans le Controllerobject Application extends Controller { def index = Action { implicit request => val username="test" Ok(html.index(username)) }
}
samedi 26 octobre 13
Dans le Controllerobject Application extends Controller with Secured { def index = ActionSecure { username => implicit request => Ok(html.index(username)) }
}
samedi 26 octobre 13
trait Secured {
def username(request: RequestHeader) = request.session.get(Security.username)
def onUnauthorized(request: RequestHeader) = Results.Redirect(routes.Auth.login)
def ActionSecure(f: => String => Request[AnyContent] => Result) = { Security.Authenticated(username, onUnauthorized) { user => Action{ request => f(user)(request) } } }
} Result
HTMLString Request[AnyContent]
samedi 26 octobre 13
Play2 et Sécurité
• Simple
•Composable
• Facile à tester
samedi 26 octobre 13
Optimiser l’indexation et le référencement
samedi 26 octobre 13
Indexation et référencement
•URLs propres et pondérées
•Mots clés
• Liens et Sitemap
•Microformat (Hotel, Avion, Lieux)
•Contenu non répété
samedi 26 octobre 13
samedi 26 octobre 13
routes
Compilé et validé
samedi 26 octobre 13
URL
GET /$origin<from-(.*)>/:classifier controllers.Frontoffice.home(origin:String, classifier: String)
/from-boston/quality
http://www.zaptravel.com/romance/weekend-deals/from-paris/to-athens/12-Apr-2013-to-14-Apr-2013/elite-athens-greece
samedi 26 octobre 13
Play2
• La séparation entre la partie routage et la partie contrôleur permet de créer des URLs «propres»
samedi 26 octobre 13
Sitemap
• Déclarer la table des matières de son site
•Optimise le référencement
• Permet de mettre en cache les pages
curl http://www.zaptravel.com/sitemap.xml
samedi 26 octobre 13
samedi 26 octobre 13
Problème : construire le sitemap de façon asynchrone
samedi 26 octobre 13
Solution : Async
Akka / Play2
samedi 26 octobre 13
def sitemap = Action { implicit request => val longCall = Akka.future { val today = ZapFormats.formatForJson(new DateTime()) val listOfSEOCards = FolioCard.allForSEO().flatMap { seo => val localURL = routes.Frontoffice.showFolioDynamically(seo.category, seo.duration, seo.origin, seo.destination, seo.dateRange, seo.segment).absoluteURL(secure = false) val updatedUrl = (for (l <- localAddress; p <- publicAddress) yield localURL.replaceAll(l, p)) updatedUrl } Ok(views.xml.Application.sitemap(today, listOfSEOCards)).as("text/xml") } Async { implicit val timeout = akka.util.Timeout(60 seconds) longCall } }
samedi 26 octobre 13
def sitemap = Action { implicit request => val longCall = Akka.future { val today = ... // some other code val listOfSEOCards = FolioCard.allForSEO().flatMap { seo => val localURL = routes.Frontoffice.showFolioDynamically(seo.category, seo.duration, seo.origin, seo.destination, seo.dateRange, seo.segment).absoluteURL(secure = false) val updatedUrl = (for (l <- localAddress; p <- publicAddress) yield localURL.replaceAll(l, p)) updatedUrl } Ok(views.xml.Application.sitemap(today, listOfSEOCards) ).as("text/xml") } Async { implicit val timeout = akka.util.Timeout(60 seconds) longCall } }
samedi 26 octobre 13
def sitemap = Action { implicit request => val longCall = Akka.future { val today = ZapFormats.formatForJson(new DateTime()) val listOfSEOCards = FolioCard.allForSEO().flatMap { seo => val localURL = routes.Frontoffice.showFolioDynamically(seo.category, seo.duration, seo.origin, seo.destination, seo.dateRange, seo.segment).absoluteURL(secure = false)
val updatedUrl = (for (l <- localAddress; p <- publicAddress) yield localURL.replaceAll(l, p))
updatedUrl } Ok(views.xml.Application.sitemap(today, listOfSEOCards)).as("text/xml") } Async { implicit val timeout = akka.util.Timeout(60 seconds) longCall } }
samedi 26 octobre 13
def sitemap = Action { implicit request => val longCall = Akka.future { val today = ZapFormats.formatForJson(new DateTime()) val listOfSEOCards = FolioCard.allForSEO().flatMap { seo => val localURL = routes.Frontoffice.showFolioDynamically(seo.category, seo.duration, seo.origin, seo.destination, seo.dateRange, seo.segment).absoluteURL(secure = false)
val updatedUrl = (for (l <- localAddress; p <- publicAddress) yield localURL.replaceAll(l, p))
updatedUrl } Ok(views.xml.Application.sitemap(today, listOfSEOCards)).as("text/xml") } Async { implicit val timeout = akka.util.Timeout(60 seconds) longCall } }
samedi 26 octobre 13
def sitemap = Action { implicit request => val longCall = Akka.future { val today = ZapFormats.formatForJson(new DateTime()) val listOfSEOCards = FolioCard.allForSEO().flatMap { seo => val localURL = routes.Frontoffice.showFolioDynamically(seo.category, seo.duration, seo.origin, seo.destination, seo.dateRange, seo.segment).absoluteURL(secure = false)
val updatedUrl = (for (l <- localAddress; p <- publicAddress) yield localURL.replaceAll(l, p))
updatedUrl } Ok(views.xml.Application.sitemap(today, listOfSEOCards)).as("text/xml") } Async { implicit val timeout = akka.util.Timeout(60 seconds) longCall } }
Bref...curl http://www.zaptravel.com/sitemap.xml
samedi 26 octobre 13
Gestion du cachesamedi 26 octobre 13
Comment améliorer les performances ?
samedi 26 octobre 13
Eviter de recharger la même page,
utilisez code 304 NotModified
Note: @rosstuck a fait une session sur HTTP à Confoo mercredi dernier
samedi 26 octobre 13
Exemple sur /from-paris/quality
Navigateur Play2
GET /from-paris/quality
samedi 26 octobre 13
Exemple sur /from-paris/qualityNavigateur Play2
OK
HTTP/1.1 200 OKContent-Type: text/html; charset=utf-8ETag: 11299930771Cache-Control: max-age=600, s-maxage=600, must-revalidateContent-Length: 103586......
ce n’est pas une erreur
samedi 26 octobre 13
Recharge /from-paris/quality
Navigateur Play2
GET /from-paris/qualityIf-None-Match: 112999307771
304 Not ModifiedContent-Length: 0
samedi 26 octobre 13
Optimisation 1
• Evitez de faire travailler votre serveur pour rien
• Déterminez des ETags «métiers»
• Attention à la gestion du cache et des serveurs mandataires.
samedi 26 octobre 13
Optimisation 2
Faire de la gestion de cache applicative
samedi 26 octobre 13
Cache applicatif ?
samedi 26 octobre 13
2 types de cacheCache technique type Varnish
Cache de Play2 ou Redis
- Process à part- Cache HTTP
- Code applicatif- utilise la mémoire de Play2 ou Redis
samedi 26 octobre 13
2 types de cache
• Facile à installer
• Evite de solliciter Play2
• Scalable
• Configurable
Cache technique type Varnish
samedi 26 octobre 13
2 types de cache
• Prend en compte le métier
• Permet de garder les pages «authentifiées»
• Pas aussi performant que la solution Varnish
Cache applicatif Play2/Redis
samedi 26 octobre 13
Sur Zaptravel• Page d’accueil
optimisé avec Cache de Play2
• Page Folio, section top Deal avec cache Play2
• Page Deal, cache avec Redis
samedi 26 octobre 13
Et pour terminer
samedi 26 octobre 13
Merci
@nmartignole
samedi 26 octobre 13