Rails 在多个 instances/servers 上重新加载动态路由
Rails reload dynamic routes on multiple instances/servers
我们如何在多个 servers/instances 上强制 Rails 到 reload_routes?
我们在 Google App-Engine 运行 中有一个多租户平台,在 5 个以上的实例上,我们希望我们所有的站点都从后端定义自己的一组路由。每当我们有一个新站点时,我们目前都必须重新启动所有服务器才能访问新路由。
我们遵循了这个 guide 但它只适用于本地环境并且不会在不重新启动服务器的情况下更新 所有 生产服务器上的路由。
我们的路由文件如下所示:
routes.rb
Frontend::Application.routes.draw do
root 'home#index'
...
DynamicRoutes.load
end
lib/dynamic_routes.rb
def self.load
Frontend::Application.routes.draw do
Site.all.each do |site|
site.routes.each do |custom_route|
route_name = custom_route[0]
route = custom_route[1]
# write the route with the host constraint
self.constraints(:host => site.hostname) do
case route_name
when :contact_form
mapper.match "#{route}", to: 'contact_forms#new' as: "contact_#{site.id}"
end
...
end
end
end
end
end
def self.reload
Frontend::Application.reload_routes!
end
每次更新路线或创建新站点后,我们都会 运行 DynamicRoutes::reload
假设您没有共享存储:您可以编写一个操作来重新加载该特定实例的路由。当您触发 DynamicRoutes::reload 时,您将向其他实例的重新加载操作发出请求。
如果您确实有共享存储,请编写一个 before_action 以在特定文件已 "touched" 时重新加载路由,如果您想让所有实例重新加载路由,请触摸该文件。
我们终于找到了一个运行良好且对性能影响不大的解决方案。我们使用这样一个事实,即生产中的线程会跨请求保持状态。所以我们决定创建一个中间件来检查路由更改的最新时间戳,以防时间戳与 Thread.current
中保存的时间戳不同,我们强制执行 Frontend::Application.reload_routes!
config/production.rb
Frontend::Application.configure do
...
config.middleware.use RoutesReloader
...
end
app/middleware/routes_reloader.rb
class RoutesReloader
SKIPPED_PATHS = ['/assets/', '/admin/']
def initialize(app)
@app = app
end
def call(env)
if reload_required?(env)
timestamp = Rails.cache.read(:routes_changed_timestamp)
if Thread.current[:routes_changed_timestamp] != timestamp
Frontend::Application.reload_routes!
Thread.current[:routes_changed_timestamp] = timestamp
end
end
@app.call(env)
end
private
def reload_required?(env)
SKIPPED_PATHS.none? { |word| env['PATH_INFO'].include?(word) }
end
end
app/model/routes.rb
class Routes < ActiveRecord::Base
after_save :save_timestamp
private
def save_timestamp
ts = Time.zone.now.to_i
Rails.cache.write(:routes_changed_timestamp, ts, expires_in: 30.minutes)
end
end
好处:
- 您可以排除某些路径上的重新加载,例如 /assets/ 和 /admin/
- 线程服务器多次请求并且重新加载只发生一次
- 您可以在任何您喜欢的模型上实现它
注意事项:
- 新线程将加载路由两次
- 如果您清除 Rails 缓存,所有线程将重新加载路由(您可以通过持久解决方案解决这个问题;例如,将时间戳保存到 mysql 中,然后保存到缓存中)
但总体而言,我们没有发现任何性能下降。
多年来我们一直在努力解决这个问题,上面的解决方案是第一个真正帮助我们在多线程上重新加载路由的解决方案。
我们如何在多个 servers/instances 上强制 Rails 到 reload_routes?
我们在 Google App-Engine 运行 中有一个多租户平台,在 5 个以上的实例上,我们希望我们所有的站点都从后端定义自己的一组路由。每当我们有一个新站点时,我们目前都必须重新启动所有服务器才能访问新路由。
我们遵循了这个 guide 但它只适用于本地环境并且不会在不重新启动服务器的情况下更新 所有 生产服务器上的路由。
我们的路由文件如下所示:
routes.rb
Frontend::Application.routes.draw do
root 'home#index'
...
DynamicRoutes.load
end
lib/dynamic_routes.rb
def self.load
Frontend::Application.routes.draw do
Site.all.each do |site|
site.routes.each do |custom_route|
route_name = custom_route[0]
route = custom_route[1]
# write the route with the host constraint
self.constraints(:host => site.hostname) do
case route_name
when :contact_form
mapper.match "#{route}", to: 'contact_forms#new' as: "contact_#{site.id}"
end
...
end
end
end
end
end
def self.reload
Frontend::Application.reload_routes!
end
每次更新路线或创建新站点后,我们都会 运行 DynamicRoutes::reload
假设您没有共享存储:您可以编写一个操作来重新加载该特定实例的路由。当您触发 DynamicRoutes::reload 时,您将向其他实例的重新加载操作发出请求。
如果您确实有共享存储,请编写一个 before_action 以在特定文件已 "touched" 时重新加载路由,如果您想让所有实例重新加载路由,请触摸该文件。
我们终于找到了一个运行良好且对性能影响不大的解决方案。我们使用这样一个事实,即生产中的线程会跨请求保持状态。所以我们决定创建一个中间件来检查路由更改的最新时间戳,以防时间戳与 Thread.current
中保存的时间戳不同,我们强制执行 Frontend::Application.reload_routes!
config/production.rb
Frontend::Application.configure do
...
config.middleware.use RoutesReloader
...
end
app/middleware/routes_reloader.rb
class RoutesReloader
SKIPPED_PATHS = ['/assets/', '/admin/']
def initialize(app)
@app = app
end
def call(env)
if reload_required?(env)
timestamp = Rails.cache.read(:routes_changed_timestamp)
if Thread.current[:routes_changed_timestamp] != timestamp
Frontend::Application.reload_routes!
Thread.current[:routes_changed_timestamp] = timestamp
end
end
@app.call(env)
end
private
def reload_required?(env)
SKIPPED_PATHS.none? { |word| env['PATH_INFO'].include?(word) }
end
end
app/model/routes.rb
class Routes < ActiveRecord::Base
after_save :save_timestamp
private
def save_timestamp
ts = Time.zone.now.to_i
Rails.cache.write(:routes_changed_timestamp, ts, expires_in: 30.minutes)
end
end
好处:
- 您可以排除某些路径上的重新加载,例如 /assets/ 和 /admin/
- 线程服务器多次请求并且重新加载只发生一次
- 您可以在任何您喜欢的模型上实现它
注意事项:
- 新线程将加载路由两次
- 如果您清除 Rails 缓存,所有线程将重新加载路由(您可以通过持久解决方案解决这个问题;例如,将时间戳保存到 mysql 中,然后保存到缓存中)
但总体而言,我们没有发现任何性能下降。
多年来我们一直在努力解决这个问题,上面的解决方案是第一个真正帮助我们在多线程上重新加载路由的解决方案。