service-oriented design and implement with rails3

83
Service-Oriented Design and Implement with Rails 3 ihower @ Ruby Tuesday 2010/12/15

Post on 12-Sep-2014

14.168 views

Category:

Technology


7 download

DESCRIPTION

 

TRANSCRIPT

Page 1: Service-Oriented Design and Implement with Rails3

Service-Oriented Design and Implement

with Rails 3ihower @ Ruby Tuesday

2010/12/15

Page 2: Service-Oriented Design and Implement with Rails3

About Me• 張文鈿 a.k.a. ihower

• http://ihower.tw

• http://twitter.com/ihower

• Rails Developer since 2006

• The Organizer of Ruby Taiwan Community

• http://ruby.tw

• http://rubyconf.tw

Page 3: Service-Oriented Design and Implement with Rails3

Agenda• What’s SOA

• Why SOA

• Considerations

• The tool set overview

• Service side implement

• Client side implement

• Library packaging

• Caching

Page 4: Service-Oriented Design and Implement with Rails3

What’s SOAService oriented architectures

• “monolithic” approach is not enough

• SOA is a way to design complex applications by splitting out major components into individual services and communicating via APIs.

• a service is a vertical slice of functionality: database, application code and caching layer

Page 5: Service-Oriented Design and Implement with Rails3

a monolithic web app example

WebApps

Database

Load Balancer

request

Page 6: Service-Oriented Design and Implement with Rails3

a SOA example

Database

Services A

WebAppsfor User

Load Balancer

Services B

Database

WebAppfor Administration

request

request

Page 7: Service-Oriented Design and Implement with Rails3

Why SOA? Isolation

• Shared Resources

• Encapsulation

• Scalability

• Interoperability

• Reuse

• Testability

• Reduce Local Complexity

Page 8: Service-Oriented Design and Implement with Rails3

Shared Resources• Different front-web website use the same

resource.

• SOA help you avoiding duplication databases and code.

• Why not only shared database?

• code is not DRY

• caching will be problematic

WebAppsfor User

WebAppfor Administration

Database

Page 9: Service-Oriented Design and Implement with Rails3

Encapsulation

• you can change underly implementation in services without affect other parts of system

• upgrade library

• upgrade to Ruby 1.9

• upgrade to Rails 3

• you can provide API versioning

Page 10: Service-Oriented Design and Implement with Rails3

Scalability1: Partitioned Data Provides

• Database is the first bottleneck, a single DB server can not scale. SOA help you reduce database load

• Anti-pattern: only split the database

• model relationship is broken

• referential integrity

• increase code complexity

• Myth: database replication can not help you speed and consistency

WebApps

Database A

Database B

Page 11: Service-Oriented Design and Implement with Rails3

Scalability 2: Caching

• SOA help you design caching system easier

• Cache data at the right place and expire at the right times

• Cache logical model, not physical

• You do not need cache view everywhere

Page 12: Service-Oriented Design and Implement with Rails3

Scalability 3: Efficient

• Different components have different task loading, SOA can scale by service.

Load Balancer

Services A Services A

Load Balancer

Services B Services B Services B

WebApps

Services B

Page 13: Service-Oriented Design and Implement with Rails3

Security

• Different services can be inside different firewall

• You can only open public web and services, others are inside firewall.

Page 14: Service-Oriented Design and Implement with Rails3

Interoperability

• HTTP is the most common interface, SOA help you integrate them:

• Multiple languages

• Internal system e.g. Full-text searching engine

• Legacy database, system

• External vendors

Page 15: Service-Oriented Design and Implement with Rails3

Reuse

• Reuse across multiple applications

• Reuse for public APIs

• Example: Amazon Web Services (AWS)

Page 16: Service-Oriented Design and Implement with Rails3

Testability

• Isolate problem

• Mocking API calls

• Reduce the time to run test suite

Page 17: Service-Oriented Design and Implement with Rails3

Reduce Local Complexity

• Team modularity along the same module splits as your software

• Understandability: The amount of code is minimized to a quantity understandable by a small team

• Source code control

Page 18: Service-Oriented Design and Implement with Rails3

Design considerations

• Partition into Separate Services

• API Design

• Which Protocol

Page 19: Service-Oriented Design and Implement with Rails3

How to partition into Separate Services

• Partitioning on Logical Function

• Partitioning on Read/Write Frequencies

• Partitioning on Minimizing Joins

• Partitioning on Iteration Speed

Page 20: Service-Oriented Design and Implement with Rails3

on Iteration Speed

• Which parts of the app have clear defined requirements and design?

• Identify the parts of the application which are unlikely to change.

• For example: The first version data storage is using MySQL, but may change to NoSQL in the future without affecting front-app.

Page 21: Service-Oriented Design and Implement with Rails3

on Logical Function

• Higher-level feature services

• articles, photos, bookmarks...etc

• Low-level infrastructure services

• a shared key-value store, queue system

Page 22: Service-Oriented Design and Implement with Rails3

On Read/Write Frequencies

• Ideally, a service will have to work only with a single data store

• High read and low write: the service should optimize a caching strategy.

• High write and low read: don’t bother with caching

Page 23: Service-Oriented Design and Implement with Rails3

On Join Frequency

• Minimize cross-service joins.

• But almost all data in an app is joined to something else.

• How often particular joins occur? by read/write frequency and logical separation.

• Replicate data across services (For example: a activity stream by using messaging)

Page 24: Service-Oriented Design and Implement with Rails3

API Design Guideline

• Send Everything you need

• Unlike OOP has lots of finely grained method calls

• Parallel HTTP requests

• for multiple service requests

• Send as Little as Possible

• Avoid expensive XML

Page 25: Service-Oriented Design and Implement with Rails3

Versioning• Be able run multiple versions in parallel:

Clients have time to upgrade rather than having to upgrade both client and server in locks step.

• Ideally, you won’t have to run multiple versions for very long

• Two solutions:

• Including a Version in URIs

• Using Accept Headers for Versioning (disadvantage: HTTP caching)

Page 26: Service-Oriented Design and Implement with Rails3

Physical Models & Logical Models

• Physical models are mapped to database tables through ORM. (It’s 3NF)

• Logical models are mapped to your business problem. (External API use it)

• Logical models are mapped to physical models by you.

Page 27: Service-Oriented Design and Implement with Rails3

Logical Models

• Not relational or normalized

• Maintainability

• can change with no change to data store

• can stay the same while the data store changes

• Better fit for REST interfaces

• Better caching

Page 28: Service-Oriented Design and Implement with Rails3

Which Protocol?

• SOAP

• XML-RPC

• REST

Page 29: Service-Oriented Design and Implement with Rails3

RESTful Web services

• Rails way

• Easy to use and implement

• REST is about resources

• URI

• HTTP Verbs: GET/PUT/POST/DELETE

• Representations: HTML, XML, JSON...etc

Page 30: Service-Oriented Design and Implement with Rails3

The tool set

• Web framework

• XML Parser

• JSON Parser

• HTTP Client

• Model library

Page 31: Service-Oriented Design and Implement with Rails3

Web framework

• Ruby on Rails, but we don’t need full features. (Rails3 can be customized because it’s a lot more modular. We will discuss it later)

• Sinatra: a lightweight framework

• Rack: a minimal Ruby webserver interface library

Page 32: Service-Oriented Design and Implement with Rails3

ActiveResource

• Mapping RESTful resources as models in a Rails application.

• Use XML by default

• But not useful in practice, why?

Page 33: Service-Oriented Design and Implement with Rails3

XML parser

• http://nokogiri.org/

• Nokogiri (鋸) is an HTML, XML, SAX, and Reader parser. Among Nokogiri’s many features is the ability to search documents via XPath or CSS3 selectors.

Page 34: Service-Oriented Design and Implement with Rails3

JSON Parser

• http://github.com/brianmario/yajl-ruby/

• An extremely efficient streaming JSON parsing and encoding library. Ruby C bindings to Yajl

Page 35: Service-Oriented Design and Implement with Rails3

HTTP Client

• How to run requests in parallel?

• Asynchronous I/O

• Reactor pattern (EventMachine)

• Multi-threading

• JRuby

Page 36: Service-Oriented Design and Implement with Rails3

Typhoeushttp://github.com/pauldix/typhoeus/

• A Ruby library with native C extensions to libcurl and libcurl-multi.

• Typhoeus runs HTTP requests in parallel while cleanly encapsulating handling logic

Page 37: Service-Oriented Design and Implement with Rails3

Typhoeus: Quick example

response = Typhoeus::Request.get("http://www.pauldix.net")response = Typhoeus::Request.head("http://www.pauldix.net")response = Typhoeus::Request.put("http://localhost:3000/posts/1", :body => "whoo, a body")response = Typhoeus::Request.post("http://localhost:3000/posts", :params => {:title => "test post", :content => "this is my test"})response = Typhoeus::Request.delete("http://localhost:3000/posts/1")

Page 38: Service-Oriented Design and Implement with Rails3

Hydra handles requestsbut not guaranteed to run in any particular order

HYDRA = Typhoeus::HYDRA.new

a = nilrequest1 = Typhoeus::Request.new("http://example1")request1.on_complete do |response| a = response.bodyendHYDRA.queue(request1)

b = nilrequest2 = Typhoeus::Request.new("http://example1")request2.on_complete do |response| b = response.bodyendHYDRA.queue(request2)

HYDRA.run # a, b are set from here

Page 39: Service-Oriented Design and Implement with Rails3

a asynchronous method

def foo_asynchronously request = Typhoeus::Request.new( "http://example" ) request.on_complete do |response| result_value = ActiveSupport::JSON.decode(response.body) # do something yield result_value end self.hydra.queue(request) end

Page 40: Service-Oriented Design and Implement with Rails3

Usageresult = nilfoo_asynchronously do |i| result = iend

foo_asynchronously do |i| # Do something for iend

HYDRA.run# Now you can use result1 and result2

Page 41: Service-Oriented Design and Implement with Rails3

a synchronous method

def foo result = nil foo_asynchronously { |i| result = i } self.hydra.run result end

Page 42: Service-Oriented Design and Implement with Rails3

Physical Modelsmapping to database directly

• ActiveRecord

• DataMapper

• MongoMapper, MongoId

Page 43: Service-Oriented Design and Implement with Rails3

Logical Models

• ActiveModel: an interface and modules can be integrated with ActionPack helpers.

• http://ihower.tw/blog/archives/4940

Page 44: Service-Oriented Design and Implement with Rails3

integrated with helper?

• For example:

• link_to post_path(@post)

• form_for @post

• @post.errors

Page 45: Service-Oriented Design and Implement with Rails3

A basic model

class YourModel extend ActiveModel::Naming include ActiveModel::Conversion include ActiveModel::Validations

def persisted? false endend

Page 46: Service-Oriented Design and Implement with Rails3

without validations

class YourModel extend ActiveModel::Naming include ActiveModel::Conversion

def persisted? false end

def valid?() true end

def errors @errors ||= ActiveModel::Errors.new(self) endend

Page 47: Service-Oriented Design and Implement with Rails3

Many useful modules

• MassAssignmentSecurity

• Serialization

• Callback

• AttributeMethods

• Dirty

• Observing

• Translation

Page 48: Service-Oriented Design and Implement with Rails3

Serializersclass Person

include ActiveModel::Serializers::JSON include ActiveModel::Serializers::Xml

attr_accessor :name

def attributes @attributes ||= {'name' => 'nil'} end

end

person = Person.newperson.serializable_hash # => {"name"=>nil}person.as_json # => {"name"=>nil}person.to_json # => "{\"name\":null}"person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person...

Page 49: Service-Oriented Design and Implement with Rails3

Mass Assignment

class YourModel # ... def initialize(attributes = {}) if attributes.present? attributes.each { |k, v| send("#{k}=", v) if respond_to?("#{k}=") } end endend

YourModel.new( :a => 1, :b => 2, :c => 3 )

Page 50: Service-Oriented Design and Implement with Rails3

MassAssignmentSecurity

class YourModel # ... include ActiveModel::MassAssignmentSecurity

attr_accessible :first_name, :last_name

def initialize(attributes = {}) if attributes.present? sanitize_for_mass_assignment(attributes).each { |k, v| send("#{k}=", v) if respond_to?("#{k}=") } end endend

Page 51: Service-Oriented Design and Implement with Rails3

Scenario we want to implement

• an Users web service, which provide basic CRUD functions.

• an web application with the Users client library

Page 52: Service-Oriented Design and Implement with Rails3

Service implement

Page 54: Service-Oriented Design and Implement with Rails3

# config/appliction.rb

%w( active_record action_controller action_mailer).each do |framework| begin require "#{framework}/railtie" rescue LoadError endend

Page 55: Service-Oriented Design and Implement with Rails3

# config/application.rb[ Rack::Sendfile, ActionDispatch::Flash, ActionDispatch::Session::CookieStore, ActionDispatch::Cookies, ActionDispatch::BestStandardsSupport, Rack::MethodOverride, ActionDispatch::ShowExceptions, ActionDispatch::Static, ActionDispatch::RemoteIp, ActionDispatch::ParamsParser, Rack::Lock, ActionDispatch::Head].each do |klass| config.middleware.delete klassend

# config/environments/production.rbconfig.middleware.delete ActiveRecord::ConnectionAdapters::ConnectionManagement

Page 56: Service-Oriented Design and Implement with Rails3

# /app/controllers/application_controller.rbclass ApplicationController < ActionController::Baseclass ApplicationController < ActionController::Metal include AbstractController::Logger include Rails.application.routes.url_helpers include ActionController::UrlFor include ActionController::Rendering include ActionController::Renderers::All include ActionController::MimeResponds

if Rails.env.test? include ActionController::Testing # Rails 2.x compatibility include ActionController::Compatibility end

end

http://ihower.tw/blog/archives/4561

Page 57: Service-Oriented Design and Implement with Rails3

APIs design best practices (1)

• Routing doesn't need Rails resources mechanism , but APIs design should follow RESTful. (This is because we don't have view in service and we don't need URL helpers. So use resources mechanism is too overkill)

• RESTful APIs is stateless, each APIs should be independent. So, requests which have dependency relationship should be combined into one API request. (atomic)

Page 58: Service-Oriented Design and Implement with Rails3

• The best format in most case is JSON. ( one disadvantage is we can’t return binary data directly. )

• Use Yajl as parser.

• Don't convert data to JSON in Model, the converting process to JSON should be place in Controller.

# config/application.rbActiveSupport::JSON.backend = "Yajl"

APIs design best practices (2)

Page 59: Service-Oriented Design and Implement with Rails3

• I suggest it shouldn't include_root_in_json

• Please notice “the key is JSON must be string”.whether you use symbol or string in Ruby, after JSON encode should all be string.

• related key format should be xxx_id or xxx_ids,for example:

• return user_uri field in addition to the user_id field if need

# config/application.rbActiveRecord::Base.include_root_in_json = false

{ "user_id" => 4, "product_ids" => [1,2,5] }.to_json

APIs design best practices (3)

Page 60: Service-Oriented Design and Implement with Rails3

a return data examplemodel.to_json and model.to_xml is easy to use, but not useful in practice.

# collection{ :collection => [ { :name => "a" } , { :name => "b" } ], :total => 123 }.to_json

# one record{ :name => "a" }.to_json

If you want to have pagination, you need total number.

Page 61: Service-Oriented Design and Implement with Rails3

• except return collection, we can also provide Multi-Gets API. through params : ids. ex. /users?ids=2,5,11,23

• client should sort ID first, so we can design cache mechanism much easier.

• another topic need to concern is the URL length of GET. So this API can also use POST.

APIs design best practices (4)

Page 62: Service-Oriented Design and Implement with Rails3

an error message return example

{ :message => "faild", :error_codes => [1,2,3], :errors => ["k1" => "v1", "k2" => "v2" ] }.to_json

Page 63: Service-Oriented Design and Implement with Rails3

• error_codes & errors is optional, you can define it if you need.

• errors is used to put model's validation error : model.errors.to_json

APIs design best practices (5)

Page 64: Service-Oriented Design and Implement with Rails3

HTTP status codeWe should return suitable HTTP status code

• 200 OK

• 201 Created ( add success)

• 202 Accepted ( receive success but not process yet, in queue now )

• 400 Bad Request ( ex. Model Validation Error or wrong parameters )

• 401 Unauthorized

Page 65: Service-Oriented Design and Implement with Rails3

class PeopleController < ApplicationController

def index @people = Person.paginate(:per_page => params[:per_page] || 20, :page => params[:page]) render :json => { :collection => @people, :total => @people.total_entries }.to_json end

def show @person = Person.find( params[:id] ) render :json => @person.to_json end

def create @person = Person.new( :name => params[:name], :bio => params[:bio], :user_id => params[:user_id] )

@person.save! render :json => { :id => @person.id }.to_json, :status => 201 end

def update @person = user_Person.find( params[:id] ) @person.attributes = { :name => params[:name], :bio => params[:bio], :user_id => params[:user_id] }

@person.save! render :status => 200, :text => "OK" end

def destroy @person = Person.find( params[:id] ) @person.destroy

render :status => 200, :text => "OK" end

end

Page 66: Service-Oriented Design and Implement with Rails3

Client implement

Page 67: Service-Oriented Design and Implement with Rails3

Note• No active_record, we get data from service

through HTTP client (typhoeus)

• Model can include some ActiveModel, modules so we can develop more efficiently.

• This model is logical model, mapping to the data from API, not database table. It's different to service's physical model ( ORM-based)

Page 68: Service-Oriented Design and Implement with Rails3

# config/appliction.rb

%w( action_controller action_mailer).each do |framework| begin require "#{framework}/railtie" rescue LoadError endend

Page 69: Service-Oriented Design and Implement with Rails3

Setup a global Hydry

# config/initializers/setup_hydra.rb,HYDRA = Typhoeus::Hydra.new

Page 70: Service-Oriented Design and Implement with Rails3

An example you can inherited from (1)

class LogicalModel extend ActiveModel::Naming include ActiveModel::Conversion include ActiveModel::Serializers::JSON include ActiveModel::Validations include ActiveModel::MassAssignmentSecurity self.include_root_in_json = false

# continued...end

Page 71: Service-Oriented Design and Implement with Rails3

class LogicalModel # continued...

def self.attribute_keys=(keys) @attribute_keys = keys attr_accessor *keys end def self.attribute_keys @attribute_keys end class << self attr_accessor :host, :hydra end def persisted? !!self.id end def initialize(attributes={}) self.attributes = attributes end def attributes self.class.attribute_keys.inject(ActiveSupport::HashWithIndifferentAccess.new) do |result, key| result[key] = read_attribute_for_validation(key) result end end def attributes=(attrs) sanitize_for_mass_assignment(attrs).each { |k, v| send("#{k}=", v) if respond_to?("#{k}=") } end

An example you can inherited from (2)

Page 72: Service-Oriented Design and Implement with Rails3

Model usage example

class Person < LogicalModel self.attribute_keys = [:id, :name, :bio, :user_id, :created_at, :updated_at] self.host = PEOPLE_SERVICE_HOST self.hydra = HYDRA validates_presence_of :title, :url, :user_id

# ...end

Page 73: Service-Oriented Design and Implement with Rails3

class Person < LogicalModel # ... def self.people_uri "http://#{self.host}/apis/v1/people.json" end def self.async_paginate(options={}) options[:page] ||= 1 options[:per_page] ||= 20 request = Typhoeus::Request.new(people_uri, :params => options) request.on_complete do |response| if response.code >= 200 && response.code < 400 log_ok(response) result_set = self.from_json(response.body) collection = result_set[:collection].paginate( :total_entries => result_set[:total] ) collection.current_page = options[:page] yield collection else log_failed(response) end end self.hydra.queue(request) end

def self.paginate(options={}) result = nil async_paginate(options) { |i| result = i } self.hydra.run result end end

paginate

Page 74: Service-Oriented Design and Implement with Rails3

will_paginate hack!• in order to use will_paginate's helper, we

must set current_page manually, so we hack this way:

# /config/initializers/hack_will_paginate.rb# This is because our search result via HTTP API is an array and need be paginated.# So we need assign current_page, unless it will be always 1.

module WillPaginate class Collection def current_page=(s) @current_page = s.to_i end endend

Page 75: Service-Oriented Design and Implement with Rails3

class LogicalModel # ... def self.from_json(json_string) parsed = ActiveSupport::JSON.decode(json_string) collection = parsed["collection"].map { |i| self.new(i) } return { :collection => collection, :total => parsed["total"].to_i } end def self.log_ok(response) Rails.logger.info("#{response.code} #{response.request.url} in #{response.time}s") end def self.log_failed(response) msg = "#{response.code} #{response.request.url} in #{response.time}s FAILED: #{ActiveSupport::JSON.decode(response.body)["message"]}" Rails.logger.warn(msg) end def log_ok(response) self.class.log_ok(response) end def log_failed(response) self.class.log_failed(response) end end

from_json & logging

Page 76: Service-Oriented Design and Implement with Rails3

findclass Person < LogicalModel # ...

def self.person_uri(id) "http://#{self.host}/apis/v1/people/#{id}.json" end

def self.async_find(id) request = Typhoeus::Request.new( person_uri(id) ) request.on_complete do |response| if response.code >= 200 && response.code < 400 log_ok(response) yield self.new.from_json(response.body) else log_failed(response) end end self.hydra.queue(request) end def self.find(id) result = nil async_find(id) { |i| result = i } self.hydra.run result end end

This from_json is defined by ActiveModel::Serializers::JSON

Page 77: Service-Oriented Design and Implement with Rails3

create&updateclass Person < LogicalModel # ...

def create return false unless valid?

response = Typhoeus::Request.post( self.class.people_uri, :params => self.attributes ) if response.code == 201 log_ok(response) self.id = ActiveSupport::JSON.decode(response.body)["id"] return self else log_failed(response) return nil end end def update(attributes) self.attributes = attributes return false unless valid? response = Typhoeus::Request.put( self.class.person_uri(id), :params => self.attributes ) if response.code == 200 log_ok(response) return self else log_failed(response) return nil end endend

Normally data writes do not need to occur in parallel

Or write to a messaging system asynchronously

Page 78: Service-Oriented Design and Implement with Rails3

delete&destroy

class Person < LogicalModel # ... def self.delete(id) response = Typhoeus::Request.delete( self.person_uri(id) ) if response.code == 200 log_ok(response) return self else log_failed(response) return nil end end def destroy self.class.delete(self.id) end end

Page 79: Service-Oriented Design and Implement with Rails3

Service client Library packaging

• Write users.gemspec file

• gem build users.gemspec

• distribution

• http://rubygems.org

• build your local gem server

• http://ihower.tw/blog/archives/4496

Page 80: Service-Oriented Design and Implement with Rails3

About caching

• Internally

• Memcached

• Externally: HTTP Caching

• Rack-Cache, Varnish, Squid

Page 81: Service-Oriented Design and Implement with Rails3

The End感謝聆聽

Page 82: Service-Oriented Design and Implement with Rails3
Page 83: Service-Oriented Design and Implement with Rails3

References• Books&Articles:

• Service-Oriented Design with Ruby and Rails, Paul Dix (Addison Wesley)

• Enterprise Rails, Dan Chak (O’Reilly)

• RESTful Web Services, Richardson&Ruby (O’Reilly)

• RESTful WEb Services Cookbook, Allamaraju&Amundsen (O’Reilly)

• Blog:

• Rails3: ActiveModel 實作 http://ihower.tw/blog/archives/4940

• Rubygems 套件管理工具 http://ihower.tw/blog/archives/4496

• Rails3: Railtie 和 Plugins 系統 http://ihower.tw/blog/archives/4873

• Slides:

• Distributed Ruby and Railshttp://ihower.tw/blog/archives/3589