+ All Categories
Home > Technology > Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb,...

Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb,...

Date post: 10-May-2015
Category:
Upload: chris-richardson
View: 1,486 times
Download: 0 times
Share this document with a friend
Description:
It’s no longer acceptable to develop large, monolithic, single-language, single-framework Web applications. In this session, you will learn how to use the scale cube to decompose your monolithic Web application into a set of narrowly focused, independently deployable services. The presentation discusses how a modular architecture makes it easy to adopt newer and better languages and technologies. You will learn about the various communication mechanisms—synchronous and asynchronous—that these services can use.
Popular Tags:
87
@crichardson Chris Richardson Author of POJOs in Action Founder of the original CloudFoundry.com @crichardson [email protected] http://plainoldobjects.com Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability
Transcript
Page 1: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Chris Richardson

Author of POJOs in ActionFounder of the original CloudFoundry.com @crichardson [email protected] http://plainoldobjects.com

Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability

Page 2: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Presentation goalHow decomposing applications

improves deployability and scalability

and simplifies the adoption of new

technologies

Page 3: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

(About Chris)

Page 4: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

About Chris()

Page 5: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

About Chris

http://www.theregister.co.uk/2009/08/19/springsource_cloud_foundry/

Page 6: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

About Chris

Page 7: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Agenda

The (sometimes evil) monolith

Decomposing applications into services

API gateway design

Refactoring the monolith

Page 8: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Let’s imagine you are building an online store

Page 9: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Tomcat

Traditional application architecture

Browser

WAR/EAR

MySQL Database

Review Service

Product InfoService

Recommendation Service

StoreFrontUI

developtest

deploy

Simple to

Apache/Load

balancer

scale

Spring MVC

SpringHibernate

Order Service

Page 10: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

But there are problems with a monolithic architecture

Page 11: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Users expect a rich, dynamic and interactive

experience

Page 12: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Browser

WAR

StoreFrontUI

Model

View Controller

Presentation layer evolution....

HTML / HTTP

Old style UI architecture isn’t good enough

Page 13: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Browser - desktop and mobile Web application

RESTfulEndpoints

Model

View Controller

...Presentation layer evolution

JSON-REST

HTML 5 - JavaScript

Native mobile clientIoS or Android

Event publisher

Events

Static content

No elaborate, server-side web framework required

Page 14: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Intimidates developers

Page 15: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Obstacle to frequent deployments

Need to redeploy everything to change one component

Interrupts long running background (e.g. Quartz) jobs

Increases risk of failure

Fear of change

Updates will happen less often - really long QA cycles

e.g. Makes A/B testing UI really difficult

Eggs in one basket

Page 16: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Overloads your IDE and container

Slows down development

Page 17: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Shipping team

Accounting Engineering

Obstacle to scaling development

E-commerce application

Page 18: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

WAR

Review service

Product Info service

Recommendation service

StoreFrontUI

Reviews team

Product Info team

Recommendations team

UI team

Obstacle to scaling development

Order serviceOrders team

Page 19: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Lots of coordination and communication required

Obstacle to scaling development

I want to update the UI

But the backend is not working

yet!

Page 20: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Requires long-term commitment to a technology stack

Page 21: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Agenda

The (sometimes evil) monolith

Decomposing applications into services

API gateway design

Refactoring the monolith

Page 22: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Page 23: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

The scale cube

X axis - horizontal duplication

Z axis

- data

partit

ioning

Y axis - functional

decomposition

Scale b

y split

ting s

imilar

thing

s

Scale by splitting

different things

Page 24: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Y-axis scaling - application level

WAR

ReviewService

Product InfoService

RecommendationService

StoreFrontUI

OrderService

Page 25: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Y-axis scaling - application level

Store front application

reviews application

recommendations application

Apply X axis cloning and/or Z axis partitioning to each service

product info application

ReviewService

Product InfoService

RecommendationService

StoreFrontUI

OrderService

orders application

Page 26: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Partitioning strategies...

Partition by noun, e.g. product info service

Partition by verb, e.g. shipping service

Single Responsibility Principle

Unix utilities - do one focussed thing well

Page 27: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Partitioning strategies

Too few

Drawbacks of the monolithic architecture

Too many - a.k.a. Nano-service anti-pattern

Runtime overhead

Potential risk of excessive network hops

Potentially difficult to understand system

Something of an art

Page 28: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Inter-service communication options

Synchronous HTTP ⇔ asynchronous AMQP

Formats: JSON, XML, Protocol Buffers, Thrift, ...

Asynchronous is preferred but REST is popular too

JSON is fashionable but binary format is more efficient

Page 29: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Example micro-serviceclass RegistrationSmsServlet extends RegistrationSmsScalatraStack {

post("/") { val phoneNumber = request.getParameter("From") val registrationUrl = System.getenv("REGISTRATION_URL") + "?phoneNumber=" + encodeForUrl(phoneNumber) <Response> <Sms>To complete registration please go to {registrationUrl} </Sms> </Response> } }

For more on micro-services see http://oredev.org/2012/sessions/micro-

service-architecture

Page 30: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Real world examples

http://highscalability.com/amazon-architecture

http://techblog.netflix.com/

http://www.addsimplicity.com/downloads/eBaySDForum2006-11-29.pdf

http://queue.acm.org/detail.cfm?id=1394128

Page 31: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

There are drawbacks

Page 32: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Complexity

See Steve Yegge’s Google Platforms Rant re Amazon.com

Page 33: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Multiple databases &

Transaction management

Page 34: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Implementing features that span multiple services

Page 35: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

When to use it?In the beginning: •You don’t need it •It will slow you down

Later on:•You need it•Refactoring is painful

Page 36: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

But there are many benefits

Page 37: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Smaller, simpler apps

Easier to understand and develop

Reduced startup time - important for GAE

Less jar/classpath hell

Who needs OSGI?

Page 38: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Scales development: develop, deploy and scale each service independently

Page 39: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Improves fault isolation

Page 40: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Eliminates long-term commitment to a single technology stack

Modular, polyglot, multi-framework applications

Page 41: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Two levels of architectureSystem-level

ServicesInter-service glue: interfaces and communication mechanisms

Slow changing

Service-level

Internal architecture of each serviceEach service could use a different technology stack

Pick the best tool for the jobRapidly evolving

Page 42: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Easily try other technologies

... and fail safely

Page 43: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

The human body as a system

Page 44: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

50 to 70 billion of your cells die each day

Page 45: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Yet you (the system) remain you

Page 46: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Can we build software systems with these characteristics?

http://dreamsongs.com/Files/WhitherSoftware.pdf

http://dreamsongs.com/Files/DesignBeyondHumanAbilitiesSimp.pdf

Page 47: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Agenda

The (sometimes evil) monolith

Decomposing applications into services

API gateway design

Refactoring the monolith

Page 48: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Directly connecting the front-end to the backend

Model

View Controller Product Infoservice

RecommendationService

Reviewservice

REST

REST

AMQP

Model

View Controller

Browser/Native App

Traditional web application

Chatty API

Web unfriendly protocols

Page 49: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Use an API gateway

Model

View Controller Product Infoservice

RecommendationService

Reviewservice

REST

REST

AMQP

APIGateway

Model

View Controller

Browser/Native App

Single entry point

Client specific APIs

Protocol translation

Traditional web application

Page 50: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Optimized client-specific APIs

DesktopBrowser

MobileApp

NodeJS

APIGateway

RESTproxy

Event publishing

Product Infoservice

RecommendationService

Reviewservice

REST

REST

AMQP

getProductInfo()getRecomm...()getReviews()

getProductDetails()

Page 51: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Netflix API Gateway

http://techblog.netflix.com/2013/01/optimizing-netflix-api.html

Device specific end points

Page 52: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Developing an API gateway

Java EE web technologies

Netty-based technology stack

Non-Java options: e.g. Node.JS

Page 53: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

API gateway design issues

Page 54: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

The need for parallelism

ProductDetails

API

Product Info

Recommendations

Reviews

getProductDetails()

getRecomendations()

getReviews()

Call in parallel

get ProductDetails

Page 55: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Futures are a great concurrency abstraction

An object that will contain the result of a concurrent computationhttp://en.wikipedia.org/wiki/Futures_and_promises

Future<Integer> result = executorService.submit(new Callable<Integer>() {... });

Java has basic futures. We can do much better....

Page 56: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Better: Futures with callbacks

val f : Future[Int] = Future { ... } f onSuccess { case x : Int => println(x) } f onFailure { case e : Exception => println("exception thrown") }

Guava ListenableFutures, Java 8 CompletableFuture, Scala Futures

Page 57: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Even better: Composable Futuresval f1 = Future { ... ; 1 }val f2 = Future { ... ; 2 }

val f4 = f2.map(_ * 2)assertEquals(4, Await.result(f4, 1 second))

val fzip = f1 zip f2assertEquals((1, 2), Await.result(fzip, 1 second))

def asyncOp(x : Int) = Future { x * x}val f = Future.sequence((1 to 5).map { x => asyncOp(x) })assertEquals(List(1, 4, 9, 16, 25), Await.result(f, 1 second))

Scala Futures

Transforms Future

Combines two futures

Transforms list of futures to a future containing a list

Page 58: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Composing concurrent requests using Scala Futures

class ProductDetailsService @Autowired()(....) {

def getProductDetails(productId: Long) = { val productInfoFuture = productInfoService.getProductInfo(productId) val recommendationsFuture = recommendationService.getRecommendations(productId) val reviewsFuture = reviewService.getReviews(productId)

for (((productInfo, recommendations), reviews) <- productInfoFuture zip recommendationsFuture zip reviewsFuture) yield ProductDetails(productInfo, recommendations, reviews) }

}

Asynchronously creates a Future containing result

Page 59: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Handling partial failures

ProductDetails

Controller

Product Info

Recommendations

Reviews

getProductDetails()

getRecomendations()

getReviews()

get ProductDetails X

Page 60: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

About Netflix> 1B API calls/day

1 API call ⇒ average 6 service calls

Fault tolerance is essential

http://techblog.netflix.com/2012/02/fault-tolerance-in-high-volume.html

Page 61: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

How to run out of threads

Tomcat

Execute thread pool

HTTP Request

Thread 1

Thread 2

Thread 3

Thread n

API Gateway

Code

RecommendationService

If service is down then thread will

be blocked

XXXXX

Eventually all threads will be blocked

Page 62: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Their approachNetwork timeouts

Invoke remote services via a bounded thread pool

Use the Circuit Breaker pattern

On failure:

return default/cached data

return error to caller

https://github.com/Netflix/Hystrix

Page 63: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Agenda

The (sometimes evil) monolith

Decomposing applications into services

API gateway design

Refactoring the monolith

Page 64: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

How do you decompose your big, scary monolithic

application?

Page 65: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Strategy #1: Stop digging

Page 66: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

New functionality = service

Monolith ServiceAnti-corruption layer

Glue code

Pristine

Page 67: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Sounds simple but...

Dependencies between monolith and service

e.g. Common entities

Building the anti-corruption layer can be challenging

Must replicate data between systems

...

Page 68: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Strategy #2: Split front-end & backend

Page 69: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Split front-end & backend

MonolithPresentation

layer

Backend

Front-end

Backend

Independently deployable

Page 70: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Strategy #3: Decompose front-end

Page 71: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Decompose front-endFront-end

Catalog Checkout Account Orders

Catalog Checkout Account Orders

/catalog /checkout /account /orders

Page 72: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Issues and challenges

Server-side Session state:

Need to use a separate session state server

...

Page 73: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Strategy #4: extract services

Page 74: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Module ⇒ service ...

WAR

Module

Page 75: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

... Module ⇒ service

WAR ServiceAnti-corruption layer

Page 76: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

What to extract?

Have the ideal partitioned architecture in mind:

Partitioned by verb or noun

Start with troublesome modules:

Frequently updated

Stateful components that prevent clustering

Conflicting resource requirements

Page 77: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Service

Domain model 2

Monolith

Untangling dependencies

API A API B API C

Domain model 1

A C

B

X

Z

Y

Trouble!

Page 78: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Useful idea: Bounded contextDifferent services have a different view of a domain object, e.g.

User Management = complex view of user

Rest of application: User = PK + ACL + Name

Different services can have a different domain model

Services exchange messages to synchronize data

Page 79: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Customer management

Untangling orders and customers

Order management

Order Service

placeOrder()

Customer Service

availableCredit()updateCustomer()

Customer

creditLimit...

has ordersbelongs toOrder

total

Invariant:sum(order.total) <= creditLimit

available credit= creditLimit - sum(order.total)Trouble!

Page 80: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Customer management

Replicating the credit limit

Order management

Order Service

placeOrder()

Customer

creditLimit...

Order

total

Customer’

creditLimit

CreditLimitChangedEvent

sum(order.total) <= creditLimit

Customer Service

updateCustomer()

Simplified

Page 81: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Customer management

Maintaining the openOrderTotal

Order management

Order Service

placeOrder()

Customer Service

availableCredit()

Customer

creditLimit

...

Order

customerIdtotal

= creditLimit - openOrderTotal

OpenOrderTotalUpdated

openOrderTotal

Page 82: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Refactoring a monolith is not easy

BUT

the alternative is far worse

Page 83: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Summary

Page 84: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Monolithic applications are simple to develop and deploy

BUT have significant drawbacks

Page 85: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Apply the scale cube

Modular, polyglot, and scalable applications

Services developed, deployed and scaled independently

Page 86: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Use a modular, polyglot architecture

Model

View Controller Product Infoservice

RecommendationService

Reviewservice

REST

REST

AMQP

APIGateway

Model

View Controller

Traditional web application

Page 87: Decompose That WAR! Architecting for Adaptability, Scalability, and Deployability (jmaghreb, jmaghreb2013)

@crichardson

Questions?

@crichardson [email protected]

http://plainoldobjects.com


Recommended