如何在多台Ruby on Rails生产实例执行脚本并获取公网IP?
类变量@@banners是进程内存储的,每个Rails服务器实例都是独立进程,负载均衡只会把请求转发到其中一台,所以直接调用Banner.reload_banners只能更新当前实例的变量。以下是几种可行的解决方案:
方案1:改用分布式缓存存储(推荐)
把@@banners的内容迁移到Redis、Memcached这类共享缓存中,所有实例统一从缓存读取数据,更新时直接修改缓存,确保所有实例拿到的都是最新值。
修改Banner类示例:
class Banner # 从缓存获取 banners def self.banners Rails.cache.fetch('banners', expires_in: 24.hours) { load_default_banners } end # 重载并更新缓存 def self.reload_banners new_banners = fetch_latest_banners_from_source # 原有的 banners 获取逻辑 Rails.cache.write('banners', new_banners) end private def self.load_default_banners # 初始加载默认 banners 的逻辑 end def self.fetch_latest_banners_from_source # 从数据源(比如数据库、API)拉取最新 banners 的逻辑 end end
之后所有实例调用Banner.banners都会读取共享缓存,任意实例触发reload_banners都会更新全局缓存。
方案2:用消息队列触发全实例更新
通过Redis Pub/Sub或RabbitMQ等消息中间件,当一台实例执行重载时,发送广播消息,其他所有实例订阅该消息并执行自身的reload_banners。
1. 修改重载方法发送消息
class Banner def self.reload_banners new_banners = fetch_latest_banners_from_source @@banners = new_banners # 更新当前实例 # 发送重载广播 Redis.current.publish('banner_reload_topic', 'trigger_reload') end end
2. 实例启动时订阅消息
在config/initializers/banner_subscriber.rb中添加订阅逻辑:
# 单独起线程订阅消息,避免阻塞Rails启动 Thread.new do redis = Redis.current redis.subscribe('banner_reload_topic') do |on| on.message do |_channel, message| if message == 'trigger_reload' Banner.reload_banners end end end rescue Redis::BaseConnectionError => e Rails.logger.error("Banner subscriber connection failed: #{e.message}") end
生产环境需确保订阅线程稳定,可配合进程管理工具(如systemd、Sidekiq)维护。
方案3:彻底放弃进程内类变量
类变量本身不适合分布式部署场景,建议将可变状态完全迁移到共享存储(数据库、缓存),从根源避免进程间状态不一致问题。
以下是几种常用方法:
方法1:调用外部IP查询服务
通过HTTP请求外部服务获取公网IP,这是最直接的方式:
def get_public_ip require 'net/http' response = Net::HTTP.get(URI('https://icanhazip.com')) response.strip rescue StandardError => e Rails.logger.error("Failed to fetch public IP: #{e.message}") # 降级方案:尝试获取本地非私网IP(仅当服务器公网IP直接绑定在网卡时有效) Socket.ip_address_list.detect do |addr| addr.ipv4? && !addr.ipv4_private? end&.ip_address end
方法2:从环境变量读取(更可靠)
部署服务器时,将公网IP写入环境变量(如PUBLIC_IP),直接读取即可:
def get_public_ip ENV['PUBLIC_IP'] || fallback_local_ip end def fallback_local_ip Socket.ip_address_list.detect { |addr| addr.ipv4? && !addr.ipv4_private? }&.ip_address end
这种方式不依赖外部服务,稳定性更高,适合云服务器部署时提前配置。
方法3:查看服务器网络接口(仅适用于公网IP绑定本地网卡的场景)
如果服务器的公网IP直接绑定在网卡上,可通过读取网络接口信息获取:
def get_public_ip `curl -s ifconfig.me`.strip # 或用`ip addr`解析,不同系统命令可能不同 end
注意:部分云服务器采用NAT网络,本地网卡只有内网IP,此方法无效。
内容的提问来源于stack exchange,提问作者Vishal Sharma

