关于rails性能的那些小事儿

本文介绍如何通过正确使用Rails、减少内存消耗、优化代码等方法显著提升Rails应用的性能。涵盖避免使用内存密集型特性、利用数据库进行数据聚合、优化Unicorn配置等方面。

1 Introduction

I often hear that Rails is slow. This has become a common theme among the Ruby and Rails community. But it is actually a myth. It's easy to make your application up to 10x faster just by using Rails in the right way. Here's what you need to know to optimize your Rails application.

1.1 Steps for optimizing a Rails app

There are only two reasons why Rails application might be slow:

  • Ruby and Rails are used where other tool might be a much better choice.
  • Too much memory is consumed and hence garbage collection takes too long.

Rails is a nice framework, and Ruby is a simple and elegant language. But when misused, they hit performance hard. There are tasks that you'd better do with other tools. For example, databases are really good at big data processing, R language is better suited for statistics, and so on.

Memory is the #1 reason why any Ruby application is slow. The 80-20 rule of Rails performance optimization is: 80% of speedup comes from memory optimization, remaining 20% from everything else. Why memory consumption is important? Because the more memory you allocate, the more work Ruby GC (garbage collector) has to do. Rails already has a large memory footprint. Average app takes about 100M just after launch. If you are not careful with memory, it's possible that your process grows over 1G. With so much memory to collect, it's not unusual for GC to take 50% and more of application execution time.

2 How Can We Make A Rails App Faster?

There are three ways to make your app faster: scaling, caching, and code optimization.

Scaling is easy these days. Heroku basically does it for you, and Hirefire makes the process automatic. You can learn more about autoscaling here. Other hosted environments offer similar solutions. By all means, use them if you can. But keep in mind that scaling is not a silver performance bullet. If your app serves the single request in 5 minutes, no amount of scaling will help. Also, with Heroku + Hirefire it's almost too easy to break the bank. I've seen Hirefire scaling up one of my apps to 36 dynos and having me to pay a whopping $3100 for that. Sure thing, I went ahead, scaled down manually to 2 dynos, and optimized the code instead.

Rails caching is also easy to do. Rails fragment caching is very good in Rails 4. Rails docs is an excellent source of info on caching. Another good read is Cheyne Wallace's article on Rails performance. It's also easy to setup Memcached these days. But as with scaling, caching is not an ultimate solution to performance problems. If your code is not working optimally, then you'll find yourself spending more and more resources on caching, to the point when caching stops making things faster.

The only reliable way to make your Rails app faster is code optimization. In Rails' case it's memory optimization. And, of course, if you follow my advice and avoid Rails to do things it's not designed for, you'll have even less code to optimize.

Let me tell you how to optimize your Rails app in 7 simple steps.

2.1 Avoid Memory Intensive Rails Features

Some features in Rails take more memory than necessary resulting in extra garbage collection runs. Here's the list.

2.1.1 Serializers

Some features in Rails take more memory than necessary resulting in extra garbage collection runs. Here's the list.

2.1.1 Serializers

Serializer is a convenient way to represent strings from the database in a Ruby data type.

rubyclass Smth < ActiveRecord::Base 
    serialize :data, JSON 
end 

Smth.find(...).data 
Smth.find(...).data = { ... } 

#But convenience comes with 3x memory overhead. If you store 100M in data column, expect to allocate 300M just to read it from the database.

It's more memory efficient to do the serialization yourself like this:

rubyclass Smth < ActiveRecord::Base 
    def 
        data JSON.parse(read_attribute(:data)) 
    end 

    def data=(value) 
        write_attribute(:data, value.to_json) 
    end 
end

This will have only 2x memory overhead. Some people, including myself, have seen Rails' JSON serializers leak memory, about 10% of data size per request. I do not understand the reason behind this. Neither do I have a reproducible case.

2.1.2 Active Record

It's easy to manipulate the data with ActiveRecord. But ActiveRecord is essentially a wrapper on top of your data. If you have a 1G of data in the table, ActiveRecord representation of it will take 2G and, in some cases, more. Yes, in 90% of cases that overhead is justified by extra convenience that you get. But sometimes you don't need it.

One example where you can avoid ActiveRecord's overhead is bulk updates. The code below will neither instantiate any model nor run validations and callbacks.

rubyBook.where('title LIKE ?', '%Rails%').update_all(author: 'David')

Behind the scenes it will just execute SQL UPDATE statement.

rubyupdate books 
    set author = 'David' 
    where title LIKE '%Rails%' 

#Another example is iteration over a large dataset. Sometimes you need only the data. No typecasting, no updates. This snippet just runs the query and avoids ActiveRecord altogether: 

result = ActiveRecord::Base.execute 'select * from books' 
result.each do |row| 
    # do something with row.values_at('col1', 'col2') 
end

2.1.3 String Callbacks

Rails callbacks like before/after save, before/after action and so on are heavily used. But the way you write them may kill your performance. Here are the 3 ways you can write, for example, before_save callback:

rubybefore_save :update_status 
before_save do |model| 
    model.update_status 
end 
before_save "self.update_status"

First two ways are good to go, third is not. Why? Because to execute this callback Rails need to store execution context (variables, constants, global instances, etc.) that is present at the time of the callback. If your application is large, you end up copying a lot of data in the memory. Because callback can be executed any time, the memory cannot be reclaimed until your program finishes.

Having symbol callbacks saved me in one occasion 0.6 sec per request.

2.2 Write Less Ruby

This is my favorite step. As my university CS class professor liked to say, the best code is the one that doesn't exist. Sometimes the task at hand is better done with other tools. Most commonly it's a database. Why? Because Ruby is bad at processing large data sets. Like, very very bad. Remember, Ruby takes a large memory footprint. So, for example, to process 1G of data you might need 3G and more of memory. It will take several dozen seconds to garbage collect 3G. Good database can process the data in under a second. Let me show a few examples.

2.2.1 Attribute Preloading

Sometimes after denormalization attributes of the model get stored in another database table. For example, imagine we're building a TODO list that consists of task. Each task can be tagged with one or several tags. Denormalized data model will look like this:

rubyid 
name 

Tags 
id 
name 

Tasks_Tags 
tag_id 
task_id

To load tasks and their tags in Rails you would do this:

rubytasks = Task.find(:all, :include => :tags) 
    > 0.058 sec

The problem with the code is that it creates an object for every Tag. That takes memory. Alternative solution is to do tag preloading in the database.

rubytasks = Task.select <<-END 
    *, 
    array( select tags.name from tags inner join tasks_tags on (tags.id = tasks_tags.tag_id) where tasks_tags.task_id=tasks.id ) as tag_names 
    END 
    > 0.018 sec

This requires memory only to store an additional column that has an array of tags. No wonder it's 3x faster.

2.2.2 Data Aggregation

By data aggregation I mean any code that to summarizes or analyzes datasets. These operations can be simple sums, or anything more complex. Let's take group rank for example. Let's assume we have a dataset with employees, departments and salaries and we want to calculate the employee's rank within a department by salary.

rubySELECT * FROM empsalary; 
depname | empno | salary 
-----------+-------+------- 
develop | 6 | 6000 
develop | 7 | 4500 
develop | 5 | 4200 
personnel | 2 | 3900 
personnel | 4 | 3500 
sales | 1 | 5000 
sales | 3 | 4800

You can calculate the rank in Ruby:

rubysalaries.sort_by! { |s| [s.depname, s.salary] } 
key, counter = nil, nil 
salaries.each do |s| 
    if s.depname != key 
        key, counter = s.depname, 0 
    end 
    counter += 1 
    s.rank = counter 
end

With 100k records in empsalary table this program finishes in 4.02 seconds. Alternative Postgres query using window functions does the same job more than 4 times faster in 1.1 seconds.

rubySELECT depname, empno, salary, rank() 
OVER (PARTITION BY depname ORDER BY salary DESC) 
FROM empsalary; 
depname | empno | salary | rank 
-----------+-------+--------+------ 
develop | 6 | 6000 | 1 
develop | 7 | 4500 | 2 
develop | 5 | 4200 | 3 
personnel | 2 | 3900 | 1 
personnel | 4 | 3500 | 2 
sales | 1 | 5000 | 1 
sales | 3 | 4800 | 2

4x speedup is already impressive, but sometimes you get more, up to 20x. One example from my own experience. I had a 3-dimensional OLAP cube with 600k data rows. My program did slicing and aggregation. When done in Ruby, it took 1G of memory and finished in about 90 seconds. Equivalent SQL query finished in 5 seconds.

2.3 Fine-tune Unicorn

Chances are you are already using Unicorn. It's the fastest web server that you can use with Rails. But you can make it even faster.

2.3.1 App Preloading

Unicorn lets you preload your Rails application before forking worker processes. This has two advantages. First, with copy-on-write friendly GC (Ruby 2.0 and later) workers can share data loaded into memory by master process. That data will be transparently copied by an operating system in case worker changes it. Second, preloading reduces worker startup time. It's normal for Rails workers to be restarted (we'll talk about this in a moment), so the faster they restart, the better performance you get.

To turn on application preloading, simply include this line into your unicorn configuration file:

rubypreload_app true

2.3.2 GC Between Requests

Remember, GC amounts for up to 50% of your application runtime. That's not the only problem. GC is also unpredictable and happens exactly when you don't want it. What can we do about that?

First thing that comes to mind, what if we disable GC altogether? It turns out that's a bad idea. Your application can easily take 1G of memory before you notice. If you run several workers on your server, you will run out of memory even on a self-hosted hardware. Not to mention Heroku with its default 512M limit.

There's a better idea. If we cannot avoid GC, we can make it more predictable and run when there's nothing else to do. For example, between requests. It's actually easy to do this with Unicorn.

For Ruby < 2.1 there's OobGC Unicorn module:

rubyrequire 'unicorn/oob_gc' 
use(Unicorn::OobGC, 1) 
# "1" here means "force GC after every 1 request" For Ruby >= 2.1 it's better to use [gctools](https://github.com/tmm1/gctools): 
require 'gctools/oobgc' 
use(GC::OOB::UnicornMiddleware)

GC between requests has its caveats. Most importantly, this improves only perceived application performance. Meaning that the users will definitely see the optimization. You server hardware will actually have to do more work. Instead of doing GC as necessary, you will force your server to do it more often. So make sure you have enough resources to run GC and enough workers to serve incoming requests while other workers are busy with GC.

2.4 Limit Growth

I gave you several examples already where your application can grow up to 1G of memory. Taking a huge piece of memory by itself might not be a problem if you have a lot of memory. The real problem is that Ruby might not give that memory back to the operating system. Let me explain why.

Ruby allocates memory in two heaps. All Ruby objects go to Ruby's own heap. Each object has 40 bytes (on a 64-bit system) to store its data. When object needs to store more, it will allocate space in operating system's heap. When object is garbage collected and then freed, the space in the operating system's heap goes back to the operating system of course. But the space reserved for the object itself in Ruby heap is simply marked as free.

This means that Ruby heap can only grow. Imagine, you read 1 million rows, 10 columns in each row from the database. For that you allocate at least 10 million objects to store the data. Usually, Rails workers in average applications take 100M after start. To accommodate the data, the worker will grow by additional 400M (10 million objects, 40 bytes each). Even after you done with the data and garbage collect it, the worker will stay at 500M.

Disclaimer, Ruby GC does have the code to shrink the heap. I have yet to see that happen in reality because the conditions for heap to shrink rarely happen in production applications.

If your workers can only grow, the obvious solution is to restart the ones that get too big. Some hosting services, Heroku for example, do that for you. Let's see the other ways to do that.

2.4.1 Internal Memory Control

Trust in God, but lock your car. There're 2 types of memory limits your application can control on its own. I call them kind and hard.

Kind memory limit is the one that's enforced after each request. If worker is too big, it can quit and Unicorn master will start the new one. That's why I call it "kind", it doesn't abrupt your application.

To get the process memory size, use RSS metric on Linux and MacOS or OS gem on Windows. Let me show how to implement this limit in Unicorn configuration file:

rubyclass Unicorn::HttpServer
    KIND_MEMORY_LIMIT_RSS = 150 #MB

    alias process_client_orig process_client
    undef_method :process_client
    def process_client(client)
        process_client_orig(client)
        rss = `ps -o rss= -p #{Process.pid}`.chomp.to_i / 1024
        exit if rss > KIND_MEMORY_LIMIT_RSS
    end
end

Hard memory limit is set by asking the operating system to kill your worker process if it grows too much. On Unix you can call setrlimit to set the RSS limit. To my knowledge, this only works on Linux. MacOS implementation was broken at some point. I'd appreciate any new information on that matter.

This the snippet from Unicorn configuration file with the hard limit:

rubyafter_fork do |server, worker| 
    worker.set_memory_limits 
end 

class Unicorn::Worker 

    HARD_MEMORY_LIMIT_RSS = 600 #MB 
    def set_memory_limits 
        Process.setrlimit(Process::RLIMIT_AS, HARD_MEMORY_LIMIT * 1024 * 1024) 
    end 
end

2.4.2 External Memory Control

Self-control does not save you from occasional OOM (out of memory). Usually you should setup some external tools. On Heroku there's no need as they have their own monitoring. But if you're self-hosting, it's a good idea to use monit, god, or any other monitoring solution.

2.5 Tune Ruby GC

In some cases you can tune Ruby GC to improve its performance. I'd say that these GC tuning is becoming less and less important, as the default settings in Ruby 2.1 and later are already good for most people.

To fine-tune GC you need to know how it works. This is a separate topic that does not belong to this article. To learn more, read a thorough Demystifying the Ruby GC article by Sam Saffron. In my upcoming Ruby Performance book I dig into even deeper details of Ruby GC. Subscribe here and I will send you email when I finish at least the beta version of the book.

My best advice is, probably, do not change GC settings unless you know what you are doing and have a good theory on how that can improve performance. This is especially true for Ruby 2.1 and later users.

I know only of one case when GC tuning helps. It's when you load large amounts of data in a single go. Here's when you can decrease the frequency of GC runs by changing the following environment variables:

rubyRUBY_GC_HEAP_GROWTH_FACTOR
RUBY_GC_MALLOC_LIMIT
RUBY_GC_MALLOC_LIMIT_MAX
RUBY_GC_OLDMALLOC_LIMIT
RUBY_GC_OLDMALLOC_LIMIT

Note, these are variables from Ruby 2.1 and later. Previous versions have less variables, and often use different names.

RUBY_GC_HEAP_GROWTH_FACTOR, default value 1.8, controls how much the Ruby heap grows when there's not enough heap space to accommodate new allocations. When you work with large amounts of objects, you want your heap space grow faster. In this case, increase the heap growth factor.

Memory limits define how often GC is triggered when you allocate memory on operating system's heap. Default limit values in Ruby 2.1 and later are:

rubyMaximum new generation malloc limit RUBY_GC_MALLOC_LIMIT_MAX 32M 
Old generation malloc limit RUBY_GC_OLDMALLOC_LIMIT 16M 
Maximum old generation malloc limit RUBY_GC_OLDMALLOC_LIMIT_MAX 128M

Let me briefly explain what these values mean. With the configuration as above, for every 16M to 32M allocated by new objects, and for every 16M to 128M allocated by old objects ("old" means that an object survived at least 1 garbage collection call) Ruby will run GC. Ruby dynamically adjust the current limit value depending on your memory allocation pattern .

So, if you have small number of objects that take large amounts of memory (for example, read large files into one string), you can increase the limits to trigger GC less often. Remember to increase all 4 limits, better proportionally to their default values.

My advice, however, differs from what other people recommend. What worked for me might not work for you.

2.6 Profile

Sometimes none of the ready-to-use advice can help you, and you need to figure out what's wrong yourself. That's when you use the profiler. Ruby-Prof is what everybody uses in Ruby world.

To learn more on profiling, read Chris Heald's and my own articles on using ruby-prof with Rails. Later also has, albeit outdated, advice on memory profiling.

2.7 Write Performance Tests

The last, but by all means not the least important step in making Rails faster is to make sure that slowdowns do not happen again after you fix them. Rails 3.x has performance testing and profiling framework bundled in. For Rails 4 you can use the same framework extracted into rails-perftest gem.

3 Closing Thoughts

It's not possible to cover everything on Ruby and Rails performance optimization in one post. So lately I decided to summarize my knowledge in a book. If you find my advice helpful, please sign up for the mailinglist and I'll notify you once I have an early preview of this book ready. Now, go ahead, and make your Rails application faster!

源码地址: https://pan.quark.cn/s/a4b39357ea24 欧姆龙触摸屏编程软件MPTST 5.02是专门为欧姆龙品牌的工业触摸屏而研发的编程解决方案,它赋予用户在直观界面上构建、修改以及排错触摸屏应用程序的能力。 该软件在工业自动化领域具有不可替代的地位,特别是在生产线监视、设备操控以及人机互动系统中发挥着核心作用。 欧姆龙MPTST(Machine Process Terminal Software Touch)5.02版本配备了多样化的功能,旨在应对不同种类的触摸屏项目要求。 以下列举了若干核心特性:1. **图形化编程**:MPTST 5.02采用图形化的编程模式,允许用户借助拖拽动作来设计屏幕布局,设定按钮、滑块、指示灯等组件,显著简化了编程流程,并提升了工作效率。 2. **兼容性**:该软件能够适配欧姆龙的多个触摸屏产品线,包括CX-One、NS系列、NJ/NX系列等,使用户可以在同一个平台上完成对不同硬件的编程任务。 3. **数据通信**:MPTST 5.02具备与PLC(可编程逻辑控制器)进行数据交互的能力,通过将触摸屏作为操作界面,实现生产数据的显示与输入,以及设备状态的监控。 4. **报警与事件管理**:软件中集成了报警和事件管理机制,可以设定多种报警标准,一旦达到预设条件,触摸屏便会展示对应的报警提示,助力操作人员迅速做出响应。 5. **模拟测试**:在设备实际连接之前,MPTST 5.02支持用户进行脱机模拟测试,以此验证程序的正确性与稳定性。 6. **项目备份与恢复**:为了防止数据遗失,MPTST 5.02提供了项目文件的备份及还原功能,对于多版本控制与团队协作具有显著价值。 7. **多语言支持**:针对全球化的应...
本资源包为流体力学与化学传质交叉领域的研究提供了一套完整的数值模拟解决方案,重点针对湍流条件下通道内溶解物质的输运与分布规律进行定量分析。该工具集专为高等院校理工科专业的教育与科研需求设计,尤其适合计算机科学、电子工程及数学等相关学科的本科生在完成课程项目、综合设计或学位论文时使用。 软件环境兼容多个版本的MatLAB平台,包括2014a、2019b及后续的2024b发行版,确保了在不同实验室或个人计算环境中的可移植性。资源包内预置了经过验证的示例数据集,用户可直接调用主程序执行计算,显著降低了初始学习成本,使初学者能够迅速掌握基本操作流程。 代码架构采用模块化与参数驱动设计。所有关键物理参数(如流速、扩散系数、边界条件等)均集中于独立的配置模块,用户无需深入底层算法即可灵活调整计算条件,从而高效模拟多种湍流溶解场景。程序逻辑结构清晰,各功能段均配有详尽的说明注释,既阐述了数值方法的理论依据,也解释了关键步骤的实现意图,便于使用者理解模型构建过程并进行针对性修改。 在学术训练方面,本工具能够帮助学生将抽象的流体动力学与传质理论转化为可视化的数值实验结果,深化对湍流混合、浓度边界层等概念的理解。对于毕业设计或专题研究,其参数化框架支持用户嵌入自定义模型,开展创新性数值实验,为深入研究复杂流动中的溶解机制提供可靠的技术支撑。 总体而言,该MATLAB分析工具集通过结构化的代码设计、完备的案例支持与广泛的版本兼容性,为流体溶解现象的数值研究提供了一个高效、可扩展的计算平台,兼具教学示范与科研探索的双重价值。 资源来源于网络分享,仅用于学习交流使用,请勿用于商业,如有侵权请联系我删除!
标题JSPM自行车个性化改装推荐系统研究AI更换标题第1章引言介绍自行车个性化改装推荐系统的研究背景、意义及国内外研究现状。1.1研究背景与意义阐述自行车个性化改装需求增长及推荐系统的重要性。1.2国内外研究现状分析国内外自行车改装推荐系统的研究进展及不足。1.3研究方法及创新点概述JSPM系统的设计方法及相较于其他系统的创新点。第2章相关理论介绍与自行车个性化改装推荐系统相关的理论基础。2.1个性化推荐理论阐述个性化推荐的基本原理和常用算法。2.2自行车改装知识介绍自行车结构、部件及改装选项等基础知识。2.3用户偏好分析理论讨论如何分析用户偏好以实现精准推荐。第3章JSPM系统设计详细介绍JSPM自行车个性化改装推荐系统的设计方案。3.1系统架构设计阐述系统的整体架构、模块划分及功能。3.2数据库设计介绍系统数据库的设计思路、表结构及关系。3.3推荐算法设计详细介绍基于用户偏好的推荐算法实现过程。第4章系统实现与测试介绍JSPM系统的实现过程及测试方法。4.1系统开发环境与工具说明系统开发所使用的环境、工具及技术栈。4.2系统实现过程阐述系统从设计到实现的具体步骤和关键代码。4.3系统测试与优化介绍系统的测试方法、测试结果及优化措施。第5章研究结果与分析展示JSPM系统的实验分析结果并进行讨论。5.1实验数据与指标介绍实验所采用的数据集、评估指标及实验环境。5.2实验结果展示通过图表等形式展示实验结果,包括推荐准确率等。5.3结果分析与讨论对实验结果进行详细分析,讨论系统的优缺点及改进方向。第6章结论与展望总结JSPM自行车个性化改装推荐系统的研究成果并展望未来。6.1研究结论概括本文的主要研究成果,包括系统设计、实现及实验结果。6.2展望指出系统存在的不足,提出未来研究的方向和改进措施。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值