O slideshow foi denunciado.
Utilizamos seu perfil e dados de atividades no LinkedIn para personalizar e exibir anúncios mais relevantes. Altere suas preferências de anúncios quando desejar.

performance vamos dormir mais?

289 visualizações

Publicada em

"Quem já trabalhou numa startup sabe como funciona o processo de desenvolvimento. "Faça rápido e no futuro a gente arruma." Acontece que quando o "futuro" chega, ele chega rasgando, te acordando de madrugada com notificações e com clientes reclamando. Nessa apresentação vou mostrar várias técnicas não muito difundidas para melhorar a performance do seu app escrito com Rails e fazer você dormir mais.

Publicada em: Educação
  • Seja o primeiro a comentar

  • Seja a primeira pessoa a gostar disto

performance vamos dormir mais?

  1. 1. Performance vamos dormir mais!
  2. 2. Marcelo Cajueiro • 5 anos trabalhando com Ruby on Rails • Engenheiro e líder no Enjoei =P • Não toco nenhum instrumento • Nick no chat da uol: tatuado_25_na_cam ! • http://cajueiro.me
  3. 3. Disclaimer
  4. 4. ( °□° )
  5. 5. Barba + Relógio = Maior confiança 1 1 Fonte: broscience (papo de homem - http://goo.gl/ZzyKup)
  6. 6. Tarot da Startup
  7. 7. Réplica no banco de dados
  8. 8. gem 'octopus' class Badge < ActiveRecord::Base replicated_model end Badge.where(user_id: 1) # usa réplica @user = User.using(:shard1).find_by_name("Joao") PS.: use com sabedoria.
  9. 9. Rake tasks & réplica $ DATABASE_URL=postgresql://replica_url rake reports:all
  10. 10. para emails Sidekiq::Extensions::DelayedMailer. sidekiq_options queue: :read_only_default InviteMailer.delay.new_invite
  11. 11. para workers class ReportWorker include Sidekiq::Worker sidekiq_options queue: :read_only_default end
  12. 12. $ DATABASE_URL="postgresql://replica_url" sidekiq -c 10 -q read_only_default,2
  13. 13. Queries N+1 http://goo.gl/2j191w 2 2 http://blog.arkency.com/2013/12/rails4-preloading/
  14. 14. Usuário seguindo outros usuários
  15. 15. class User has_many :user_follows has_many :followers, through: :user_follows, class_name: 'User', foreign_key: 'follower_id' has_many :following, through: :user_follows, class_name: 'User', foreign_key: 'user_id' end
  16. 16. class UserFollow belongs_to :user belongs_to :follower, class_name: "User" end
  17. 17. Uma query por usuário User.limit(10).each do |user| # Eu estou seguindo esse usuário? if current_user.following.where(id: user.id).exists? # imprime o botão "deixar de seguir" else # imprime o botão "seguir" end end A solução mais simples geralmente não é a mais performática.
  18. 18. Salvando os ids de quem eu sigo # E se eu estiver seguindo 90mil usuários? following_ids = current_user.following_ids User.limit(10).each do |user| if following_ids.include?(user.id) # imprime o botão "deixar de seguir" else # imprime o botão "seguir" end end
  19. 19. Pegando os ids que estou listando e vendo quem eu sigo
  20. 20. users = User.limit(10) user_ids = users.map(&:id) # pluck? no :) following_ids = current_user. following.where(id: user_ids).pluck(:id) users.each do |user| if following_ids.include?(user.id) # imprime o botão "deixar de seguir" else # imprime o botão "seguir" end end
  21. 21. Fine tune
  22. 22. Com INNER JOIN following_ids = current_user. following.where(id: user_ids).pluck(:id) SELECT "users"."id" FROM "users" INNER JOIN "user_follows" ON "users"."id" = "user_follows"."user_id" WHERE "user_follows"."follower_id" = 1
  23. 23. Sem INNER JOIN following_ids = UserFollow. where(follower_id: current_user.id). pluck(:user_id) SELECT "user_follows"."user_id" FROM "user_follows" WHERE "user_follows"."follower_id" = 1 Nem sempre o que o Rails tem pronto é o melhor.
  24. 24. gem 'bullet' Chata! Alert, honeybadger, airbrake, slack, etc.
  25. 25. gem 'rack-mini-profiler'
  26. 26. gem 'newrelic_rpm' localhost:3000/newrelic
  27. 27. Dashboard • data da primeira compra • data da última compra • número de compras • total gasto • categorias compradas • marcas compradas
  28. 28. Informações triviais sozinhas class User def first_purchase_at orders.minimum(:sold_at) end def last_purchase_at orders.maximum(:sold_at) end def purchases_count orders.count end end
  29. 29. Informações triviais sozinhas class User def purchases_total_price orders.sum(:price) end def purchased_brands purchased_products.pluck(:brand).uniq end def purchased_brands purchased_products.pluck(:category).uniq end end
  30. 30. 6 consultas user.orders.minimum(:sold_at) user.orders.maximum(:sold_at) user.orders.count user.orders.sum(:price) user.purchased_products.pluck(:brand).uniq user.purchased_products.pluck(:category).uniq
  31. 31. SQL + Ruby aggregation purchases = user.orders. order(sold_at: :desc). joins(:product).select( "orders.sold_at", "orders.price", "products.brand", "products.category" ).to_a
  32. 32. SQL + Ruby aggregation purchases.sum(&:price) purchases.map(&:brand).uniq purchases.map(&:category).uniq purchases.first.sold_at purchases.last.sold_at purchases.count
  33. 33. SQL aggregation purchases = user.orders.select( "max(orders.sold_at) AS last_purchased_at", "min(orders.sold_at) AS first_purchased_at", "sum(orders.price) AS total_price", "count(orders.id) AS total" ).to_a.first
  34. 34. SQL aggregation # Query 1 purchases.total_price purchases.last_purchased_at purchases.first_purchased_at purchases.total # Query 2 user.purchased_products.pluck('DISTINCT(brand)') # Query 3 user.purchased_products.pluck('DISTINCT(category_id)')
  35. 35. Opções 1 - todas as informacões em queries separadas 2 - uma query com todos os dados separados e calculando com ruby 3 - uma query com os campos calculados e outras duas para o restante
  36. 36. Benchmark 3 3 https://github.com/evanphx/benchmark-ips
  37. 37. Calculating ------------------------------------- queries separadas 3.000 i/100ms cálculo SQL + queries 8.000 i/100ms SQL + cálculo ruby 1.000 i/100ms ------------------------------------------------- queries separadas 46.789 (± 8.5%) i/s - 234.000 cálculo SQL + queries 81.732 (± 9.8%) i/s - 408.000 SQL + cálculo ruby 2.314 (± 0.0%) i/s - 12.000 Comparison: cálculo SQL + queries: 81.7 i/s queries separadas: 46.8 i/s - 1.75x slower SQL + cálculo ruby: 2.3 i/s - 35.32x slower
  38. 38. Mais um exemplo photos.order('CASE WHEN selected = true THEN 0 ELSE COALESCE(position, 0) + 1 END') Ou photos.sort_by do |photo| if photo.selected? 0 else photo.position.to_i + 1 end end
  39. 39. Calculating ------------------------------------- SQL 1.000 i/100ms Array 1.000 i/100ms ------------------------------------------------- SQL 1.054k (± 8.1%) i/s - 5.074k Array 95.344 (± 9.4%) i/s - 468.000 Comparison: SQL: 1053.8 i/s Array: 95.3 i/s - 11.05x slower
  40. 40. Mais sobre ordenação
  41. 41. Ordenação na query + limit EXPLAIN SELECT * FROM messages WHERE user_id = 1 ORDER BY id DESC LIMIT 10 Limit (cost=539.60..539.63 rows=10 width=115) -> Sort (cost=539.60..539.94 rows=136 width=115) Sort Key: id -> Bitmap Heap Scan on messages (cost=5.49..536.67 rows=136 width=115) Recheck Cond: (user_id = 1) -> Bitmap Index Scan on index_messages_on_user_id (cost=0.00..5.45 rows=136 width=0) Index Cond: (user_id = 1)
  42. 42. Usando índice com ordenação CREATE INDEX index_messages_ordered_on_user_id ON messages (user_id, id DESC) Limit (cost=0.43..40.78 rows=10 width=115) -> Index Scan using index_messages_ordered_on_user_id on messages (cost=0.43..549.14 rows=136 width=115) Index Cond: (user_id = 1)
  43. 43. Cache • Fragment • Russian doll • Rack • HTTP Speed Up Your Rails App by 66% - The Complete Guide to Rails Caching 4 4 From Nate Berkopec
  44. 44. gem 'identity_cache' class Product < ActiveRecord::Base include IdentityCache has_many :photos belongs_to :category cache_has_many :photos cache_belongs_to :category end product = Product.fetch(1) product.fetch_photos product.fetch_category
  45. 45. class Photo < ActiveRecord::Base include IdentityCache cache_index :imageable_type, :imageable_id end class Product < ActiveRecord::Base def cached_images Photo. fetch_by_imageable_type_and_imageable_id( 'Product', id ) end end
  46. 46. Counter cache • Implementação do Rails class Order < ActiveRecord::Base belongs_to :customer, counter_cache: true end class Customer < ActiveRecord::Base has_many :orders end @customer.orders.size @customer.orders.count
  47. 47. Counter cache gem 'counter_culture' class Product < ActiveRecord::Base belongs_to :sub_category counter_culture :category, :column_name => Proc.new { |model| model.special? ? 'special_count' : nil } end @category.special_count
  48. 48. Paginação • usar o counter cache para calcular a numeração • remover a numeração das páginas current_user. followers. paginate(per_page: 10, page: params[:page], total_entries: current_user.followers_count)
  49. 49. Remover locks pessismistas desnecessários Comment.lock.find(1).destroy comment = Comment.find(1) comment.with_lock do comment.destroy! end
  50. 50. Admin no servidor do usuário final • Admin é sempre mais lento • Tem Relatórios • Cheio de ações demoradas Boa solução • Proxy reverso
  51. 51. Lazy load na interface • Por que carregar os comentários na ação principal? • Por que carregar os produtos relacionados na ação principal? • ...
  52. 52. Migration sem lock na tabela class AddIndexToMessages < ActiveRecord::Migration disable_ddl_transaction! def change add_index :messages, :user_id, algorithm: :concurrently end end
  53. 53. jobs.lever.co/enjoei
  54. 54. Muito obrigado! Sugestões? Perguntas?
  55. 55. Referência das imagens • https://en.wikipedia.org/wiki/Rider-Waitetarotdeck • http://www.maxi-geek.com/2015/05/the-flash-vs-superman- who-will-bleed.html • https://sobeso.com/beating-creative-block • http://www.motherhoodcenter.com/sleep-coach/ • http://9to5mac.com/2015/03/06/sources-offer-hands-on- apple-watch-details-battery-life-unannounced-features-and- more/
  56. 56. E mais referência das imagens • http://www.jackiealpers.com/mysteries-rituals/the-world- photograph-of-a-man-holding-a-tarot-card-by-jackie- alpers-728153.html • http://thezt2roundtable.com/search/5/? c=3&mid=3525512&month=4&year=2015 • http://www.fanpop.com/clubs/jaime-lannister/images/ 37085417/title/jaime-tyrion-lannister-photo/7

×