Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Sharding para Rails apps

Sponsored · SiteGround - Reliable hosting with speed, security, and support you can count on. →
Avatar for Lenon Marcel Lenon Marcel
September 23, 2016

Sharding para Rails apps

Palestra apresentada na RubyConf Brasil 2016.

Avatar for Lenon Marcel

Lenon Marcel

September 23, 2016

Other Decks in Programming

Transcript

  1. • Problemas enfrentados com uma arquitetura single-tenant • Como migramos

    para uma arquitetura multi-tenant • Problemas de escalabilidade • Estratégias de sharding • Implementando sharding no Rails Agenda
  2. Email Marketing Locaweb (~2013) models workers web bounces opt-in mongo

    db Rails engine Rails apps Main database Sidekiq
  3. Problemas Tempo de resposta muito alto Downtimes constantes Difícil de

    escalar, apesar do hardware caro Clientes insatisfeitos
  4. Raízes dos Problemas Dado relacional em um banco de documentos

    Coleções enormes e muitos relacionamentos Arquitetura multi-tenant fazia mais sentido
  5. PostgreSQL Schemas Um schema é uma coleção de tabelas e

    funciona como um namespace. https://www.postgresql.org/docs/current/static/ddl-schemas.html
  6. create schema schema1; create table schema1.products (name text); insert into

    schema1.products (name) values ('product on schema 1'); create schema schema2; create table schema2.products (name text); insert into schema2.products (name) values ('product on schema 2'); PostgreSQL Schemas
  7. select * from schema1.products; name --------------------- product on schema 1

    (1 row) select * from schema2.products; name --------------------- product on schema 2 (1 row) PostgreSQL Schemas
  8. create schema schema1; set search_path = schema1; create table products

    (name text); insert into products (name) values ('product on schema 1'); create schema schema2; set search_path = schema2; create table products (name text); insert into products (name) values ('product on schema 2'); PostgreSQL Schemas
  9. set search_path = schema1; select * from products; name ---------------------

    product on schema 1 (1 row) set search_path = schema2; select * from products; name --------------------- product on schema 2 (1 row) PostgreSQL Schemas
  10. PostgreSQL Schemas database public accounts domains senders account_1 contacts messages

    reports account_2 contacts messages reports account_3 contacts messages reports account_n contacts messages reports
  11. ApplicationRecord.connection.schema_search_path = 'schema1' Product.create(name: 'product on schema1') Product.all.pluck(:name) #=> ["product

    on schema1"] ApplicationRecord.connection.schema_search_path = 'schema2' Product.create(name: 'product on schema2') Product.all.pluck(:name) #=> ["product on schema2"] ActiveRecord & Schemas
  12. class ApplicationRecord < ActiveRecord::Base self.abstract_class = true def self.using(schema_name) previous_search_path

    = connection.schema_search_path connection.schema_search_path = schema_name yield ensure connection.schema_search_path = previous_search_path end end ActiveRecord & Schemas
  13. Estratégias de Sharding Via particionamento de tabelas ? Através de

    uma coluna da tabela (key based) ? Através de um serviço de "lookup" ?
  14. # config/database.yml default: &default adapter: postgresql encoding: unicode development: #

    [...] other_db: <<: *default database: other_db Sharding via particionamento de tabelas
  15. contacts name email account_id created_at Sharding através de uma coluna

    da tabela Shard A Shard B Shard C 0 ≤ account_id < 10 10 ≤ account_id < 20 20 ≤ account_id < 30
  16. Sharding através de uma coluna da tabela MySQL, MongoDB, etc

    suportam PostgreSQL com pg_shard/citus Transparente para a app
  17. Requer código no lado da app Sharding através de um

    lookup service Funciona através de um mapeamento usuário x shard ė Altamente escalável e permite migração entre os shards
  18. Sharding através de um lookup service Migrar dados novamente não

    seria viável PostgreSQL tem suporte limitado para escala horizontal pg_shard/citus não suporta shard por schema
  19. Conexões no ActiveRecord O Rails e outras libs assumem 1

    database apenas É possível 1 modelo conectado em outro database Não é possível um mesmo modelo conectado em vários databases
  20. # config/database.yml default: &default adapter: postgresql encoding: unicode development: #

    [...] other_db: <<: *default database: other_db Conexões no ActiveRecord
  21. Connection pool A connection pool synchronizes thread access to a

    limited number of database connections. http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/Connection
  22. Connection pool connection pool connection 3 thread 1 request 1

    connection 1 request 2 thread 2 connection 2
  23. Connection Handler (Rails 4.2) Contact.find(123) # active_record/connection_handling.rb:90 def connection retrieve_connection

    end # activerecord/lib/active_record/core.rb:129 self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new mágica + mágica
  24. class ConnectionHandler def initialize @owner_to_pool = ThreadSafe::Cache.new(:initial_capacity => 2) do

    |h,k| h[k] = ThreadSafe::Cache.new(:initial_capacity => 2) end @class_to_pool = ThreadSafe::Cache.new(:initial_capacity => 2) do |h,k| h[k] = ThreadSafe::Cache.new end end def connection_pool_list owner_to_pool.values.compact end def connection_pools ActiveSupport::Deprecation.warn(<<-MSG.squish) In the next release, this will return the same as `#connection_pool_list`. (An array of pools, rather than a hash mapping specs to pools.) MSG Hash[connection_pool_list.map { |pool| [pool.spec, pool] }] end def establish_connection(owner, spec) @class_to_pool.clear raise RuntimeError, "Anonymous class is not allowed." unless owner.name owner_to_pool[owner.name] = ConnectionAdapters::ConnectionPool.new(spec) end def active_connections? connection_pool_list.any?(&:active_connection?) end def clear_active_connections! connection_pool_list.each(&:release_connection) end def clear_reloadable_connections! connection_pool_list.each(&:clear_reloadable_connections!) end def clear_all_connections! connection_pool_list.each(&:disconnect!) end def retrieve_connection(klass) pool = retrieve_connection_pool(klass) raise ConnectionNotEstablished, "No connection pool for #{klass}" unless pool conn = pool.connection raise ConnectionNotEstablished, "No connection for #{klass} in connection pool" unless conn conn end def connected?(klass) conn = retrieve_connection_pool(klass) conn && conn.connected? end def remove_connection(owner) if pool = owner_to_pool.delete(owner.name) @class_to_pool.clear pool.automatic_reconnect = false pool.disconnect! pool.spec.config end end def retrieve_connection_pool(klass) class_to_pool[klass.name] ||= begin until pool = pool_for(klass) klass = klass.superclass break unless klass <= Base end class_to_pool[klass.name] = pool end end private def owner_to_pool @owner_to_pool[Process.pid] end def class_to_pool @class_to_pool[Process.pid] end def pool_for(owner) owner_to_pool.fetch(owner.name) { if ancestor_pool = pool_from_any_process_for(owner) establish_connection owner, ancestor_pool.spec else owner_to_pool[owner.name] = nil end } end def pool_from_any_process_for(owner) owner_to_pool = @owner_to_pool.values.find { |v| v[owner.name] } owner_to_pool && owner_to_pool[owner.name] end end Connection Handler (Rails 4.2)
  25. def retrieve_connection(klass) pool = retrieve_connection_pool(klass) raise ConnectionNotEstablished unless pool conn

    = pool.connection raise ConnectionNotEstablished unless conn conn end Connection Handler (Rails 4.2)
  26. def retrieve_connection_pool(klass) class_to_pool[klass.name] ||= begin until pool = pool_for(klass) klass

    = klass.superclass break unless klass <= Base end class_to_pool[klass.name] = pool end end Connection Handler (Rails 4.2)
  27. class Author < ActiveRecord::Base; end class BankAccount < ActiveRecord::Base; end

    class Book < ActiveRecord::Base establish_connection 'library_db' end Connection Handler (Rails 4.2) connection pool do ActiveRecord::Base connection pool da classe Book
  28. pool shard 1 connection 1 connection 2 connection 3 Connection

    Switcher connection pool connection 1 connection 2 connection 3 thread n pool shard 2 connection 1 connection 2 connection 3 master shards
  29. class Handler def setup @cache = {} database_configs.each do |name,

    _| name = name.to_sym handler = ConnectionHandler.new handler.establish_connection(self, spec(name)) @cache[name] = handler end end def connection_handler_for(name) @cache.fetch(name.to_sym) end end Connection Switcher
  30. module ShardHandler def using_shard(shard) old_shard = ThreadRegistry.current_shard ThreadRegistry.current_shard = shard

    yield ensure clear_active_connections! ThreadRegistry.current_shard = old_shard end def connection_handler return super if use_master_connection? handler.connection_handler_for(current_shard) end end Connection Switcher
  31. class ShardedModel < ActiveRecord::Base self.abstract_class = true extend ShardHandler end

    class Contact < ShardedModel end class Message < ShardedModel end Connection Switcher
  32. troca do schema account = Account.find(123) ShardedModel.using_shard(account.shard_name) do ApplicationRecord.using(account.schema_name) do

    Contact.find_by(email: '[email protected]') end end request identificação da conta troca do database execução da ação restaura p/ schema anterior restaura p/ database anterior resposta
  33. desc "Migrate the database" task :migrate => [:environment, :load_config] do

    using('master', 'public') do ActiveRecord::Migrator.migrate('db/public') end Account.all.each do |acc| using(acc.shard_name, acc.schema_name) do ActiveRecord::Migrator.migrate('db/accounts') end end end Rails Tasks
  34. class ConnectionHandler def initialize @owner_to_pool = Concurrent::Map.new(:initial_capacity => 2) do

    |h,k| h[k] = Concurrent::Map.new(:initial_capacity => 2) end end def connection_pool_list owner_to_pool.values.compact end alias :connection_pools :connection_pool_list def establish_connection(spec) owner_to_pool[spec.name] = ConnectionAdapters::ConnectionPool.new(spec) end def active_connections? connection_pool_list.any?(&:active_connection?) end def clear_active_connections! connection_pool_list.each(&:release_connection) end def clear_reloadable_connections! connection_pool_list.each(&:clear_reloadable_connections!) end def clear_all_connections! connection_pool_list.each(&:disconnect!) end def retrieve_connection(spec_name) pool = retrieve_connection_pool(spec_name) raise ConnectionNotEstablished, "No connection pool with id #{spec_name} found." unless pool conn = pool.connection raise ConnectionNotEstablished, "No connection for #{spec_name} in connection pool" unless conn conn end def connected?(spec_name) conn = retrieve_connection_pool(spec_name) conn && conn.connected? end def remove_connection(spec_name) if pool = owner_to_pool.delete(spec_name) pool.automatic_reconnect = false pool.disconnect! pool.spec.config end end def retrieve_connection_pool(spec_name) owner_to_pool.fetch(spec_name) do if ancestor_pool = pool_from_any_process_for(spec_name) establish_connection(ancestor_pool.spec).tap do |pool| pool.schema_cache = ancestor_pool.schema_cache if ancestor_pool.schema_cache end else owner_to_pool[spec_name] = nil end end end private def owner_to_pool @owner_to_pool[Process.pid] end def pool_from_any_process_for(spec_name) owner_to_pool = @owner_to_pool.values.find { |v| v[spec_name] } owner_to_pool && owner_to_pool[spec_name] end end Connection Handler (Rails 5)
  35. def retrieve_connection(spec_name) pool = retrieve_connection_pool(spec_name) raise ConnectionNotEstablished unless pool conn

    = pool.connection raise ConnectionNotEstablished unless conn conn end Connection Handler (Rails 5)
  36. def retrieve_connection_pool(spec_name) owner_to_pool.fetch(spec_name) do if ancestor_pool = pool_from_any_process_for(spec_name) establish_connection(ancestor_pool.spec).tap do

    |pool| pool.schema_cache = ancestor_pool.schema_cache if ancestor_pool.schema_cache end else owner_to_pool[spec_name] = nil end end end Connection Handler (Rails 5)
  37. class Contact < ActiveRecord::Base def self.connection_specification_name # alguma specification name

    declarada no arquivo # config/database.yml end end Connection Handler (Rails 5)
  38. Arquitetura atual core workers web bounces opt-in shard A Rails

    engine Rails apps Sidekiq master shard B PostgreSQL shard C