Commit cb767dbe authored by Matthias Käppler's avatar Matthias Käppler Committed by Heinrich Lee Yu

Metrics server can be spawned instead of forked

This provides better memory use for Puma, and we
will require this for running a non-Ruby server
in the future.

This does not actually start the server for Puma
yet.
parent d1fb9295
...@@ -9,4 +9,5 @@ raise "METRICS_SERVER_TARGET cannot be blank" if target.blank? ...@@ -9,4 +9,5 @@ raise "METRICS_SERVER_TARGET cannot be blank" if target.blank?
metrics_dir = ENV["prometheus_multiproc_dir"] || File.absolute_path("tmp/prometheus_multiproc_dir/#{target}") metrics_dir = ENV["prometheus_multiproc_dir"] || File.absolute_path("tmp/prometheus_multiproc_dir/#{target}")
wipe_metrics_dir = Gitlab::Utils.to_boolean(ENV['WIPE_METRICS_DIR']) || false wipe_metrics_dir = Gitlab::Utils.to_boolean(ENV['WIPE_METRICS_DIR']) || false
Process.wait(MetricsServer.spawn(target, metrics_dir: metrics_dir, wipe_metrics_dir: wipe_metrics_dir)) server = MetricsServer.new(target, metrics_dir, wipe_metrics_dir)
server.start
...@@ -6,8 +6,23 @@ require_relative 'dependencies' ...@@ -6,8 +6,23 @@ require_relative 'dependencies'
class MetricsServer # rubocop:disable Gitlab/NamespacedClass class MetricsServer # rubocop:disable Gitlab/NamespacedClass
class << self class << self
def spawn(target, metrics_dir:, wipe_metrics_dir: false, trapped_signals: []) def spawn(target, metrics_dir:, gitlab_config: nil, wipe_metrics_dir: false)
raise "Target must be one of [puma,sidekiq]" unless %w(puma sidekiq).include?(target) ensure_valid_target!(target)
cmd = "#{Rails.root}/bin/metrics-server"
env = {
'METRICS_SERVER_TARGET' => target,
'WIPE_METRICS_DIR' => wipe_metrics_dir ? '1' : '0'
}
env['GITLAB_CONFIG'] = gitlab_config if gitlab_config
Process.spawn(env, cmd, err: $stderr, out: $stdout, pgroup: true).tap do |pid|
Process.detach(pid)
end
end
def fork(target, metrics_dir:, wipe_metrics_dir: false, reset_signals: [])
ensure_valid_target!(target)
pid = Process.fork pid = Process.fork
...@@ -15,7 +30,7 @@ class MetricsServer # rubocop:disable Gitlab/NamespacedClass ...@@ -15,7 +30,7 @@ class MetricsServer # rubocop:disable Gitlab/NamespacedClass
# Remove any custom signal handlers the parent process had registered, since we do # Remove any custom signal handlers the parent process had registered, since we do
# not want to inherit them, and Ruby forks with a `clone` that has the `CLONE_SIGHAND` # not want to inherit them, and Ruby forks with a `clone` that has the `CLONE_SIGHAND`
# flag set. # flag set.
Gitlab::ProcessManagement.modify_signals(trapped_signals, 'DEFAULT') Gitlab::ProcessManagement.modify_signals(reset_signals, 'DEFAULT')
server = MetricsServer.new(target, metrics_dir, wipe_metrics_dir) server = MetricsServer.new(target, metrics_dir, wipe_metrics_dir)
# This rewrites /proc/cmdline, since otherwise tools like `top` will show the # This rewrites /proc/cmdline, since otherwise tools like `top` will show the
...@@ -29,6 +44,12 @@ class MetricsServer # rubocop:disable Gitlab/NamespacedClass ...@@ -29,6 +44,12 @@ class MetricsServer # rubocop:disable Gitlab/NamespacedClass
pid pid
end end
private
def ensure_valid_target!(target)
raise "Target must be one of [puma,sidekiq]" unless %w(puma sidekiq).include?(target)
end
end end
def initialize(target, metrics_dir, wipe_metrics_dir) def initialize(target, metrics_dir, wipe_metrics_dir)
...@@ -40,7 +61,7 @@ class MetricsServer # rubocop:disable Gitlab/NamespacedClass ...@@ -40,7 +61,7 @@ class MetricsServer # rubocop:disable Gitlab/NamespacedClass
def start def start
::Prometheus::Client.configure do |config| ::Prometheus::Client.configure do |config|
config.multiprocess_files_dir = @metrics_dir config.multiprocess_files_dir = @metrics_dir
config.pid_provider = proc { "#{@target}_exporter" } config.pid_provider = proc { name }
end end
FileUtils.mkdir_p(@metrics_dir, mode: 0700) FileUtils.mkdir_p(@metrics_dir, mode: 0700)
...@@ -57,16 +78,18 @@ class MetricsServer # rubocop:disable Gitlab/NamespacedClass ...@@ -57,16 +78,18 @@ class MetricsServer # rubocop:disable Gitlab/NamespacedClass
case @target case @target
when 'puma' when 'puma'
Gitlab::Metrics::Exporter::WebExporter.instance(**default_opts) Gitlab::Metrics::Exporter::WebExporter.instance(**default_opts)
else when 'sidekiq'
exporter_class = "Gitlab::Metrics::Exporter::#{@target.camelize}Exporter".constantize
settings = Settings.new(Settings.monitoring[name]) settings = Settings.new(Settings.monitoring[name])
exporter_class.instance(settings, **default_opts) Gitlab::Metrics::Exporter::SidekiqExporter.instance(settings, **default_opts)
end end
exporter.start exporter.start
end end
def name def name
"#{@target}_exporter" case @target
when 'puma' then 'web_exporter'
when 'sidekiq' then 'sidekiq_exporter'
end
end end
end end
...@@ -191,11 +191,11 @@ module Gitlab ...@@ -191,11 +191,11 @@ module Gitlab
return unless metrics_server_enabled? return unless metrics_server_enabled?
@logger.info("Starting metrics server on port #{sidekiq_exporter_port}") @logger.info("Starting metrics server on port #{sidekiq_exporter_port}")
@metrics_server_pid = MetricsServer.spawn( @metrics_server_pid = MetricsServer.fork(
'sidekiq', 'sidekiq',
metrics_dir: @metrics_dir, metrics_dir: @metrics_dir,
wipe_metrics_dir: wipe_metrics_dir, wipe_metrics_dir: wipe_metrics_dir,
trapped_signals: TERMINATE_SIGNALS + FORWARD_SIGNALS reset_signals: TERMINATE_SIGNALS + FORWARD_SIGNALS
) )
end end
......
...@@ -38,13 +38,7 @@ RSpec.describe 'bin/metrics-server', :aggregate_failures do ...@@ -38,13 +38,7 @@ RSpec.describe 'bin/metrics-server', :aggregate_failures do
config_file.write(YAML.dump(config)) config_file.write(YAML.dump(config))
config_file.close config_file.close
env = { @pid = MetricsServer.spawn(target, metrics_dir: metrics_dir, gitlab_config: config_file.path, wipe_metrics_dir: true)
'GITLAB_CONFIG' => config_file.path,
'METRICS_SERVER_TARGET' => target,
'WIPE_METRICS_DIR' => '1',
'prometheus_multiproc_dir' => metrics_dir
}
@pid = Process.spawn(env, 'bin/metrics-server', pgroup: true)
end end
after do after do
......
...@@ -303,7 +303,7 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stub_settings_source: true do # rubo ...@@ -303,7 +303,7 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stub_settings_source: true do # rubo
end end
it 'does not start a sidekiq metrics server' do it 'does not start a sidekiq metrics server' do
expect(MetricsServer).not_to receive(:spawn) expect(MetricsServer).not_to receive(:fork)
cli.run(%w(foo)) cli.run(%w(foo))
end end
...@@ -320,7 +320,7 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stub_settings_source: true do # rubo ...@@ -320,7 +320,7 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stub_settings_source: true do # rubo
end end
it 'does not start a sidekiq metrics server' do it 'does not start a sidekiq metrics server' do
expect(MetricsServer).not_to receive(:spawn) expect(MetricsServer).not_to receive(:fork)
cli.run(%w(foo)) cli.run(%w(foo))
end end
...@@ -350,7 +350,7 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stub_settings_source: true do # rubo ...@@ -350,7 +350,7 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stub_settings_source: true do # rubo
end end
it 'does not start a sidekiq metrics server' do it 'does not start a sidekiq metrics server' do
expect(MetricsServer).not_to receive(:spawn) expect(MetricsServer).not_to receive(:fork)
cli.run(%w(foo)) cli.run(%w(foo))
end end
...@@ -376,7 +376,7 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stub_settings_source: true do # rubo ...@@ -376,7 +376,7 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stub_settings_source: true do # rubo
end end
it 'does not start a sidekiq metrics server' do it 'does not start a sidekiq metrics server' do
expect(MetricsServer).not_to receive(:spawn) expect(MetricsServer).not_to receive(:fork)
cli.run(%w(foo)) cli.run(%w(foo))
end end
...@@ -406,9 +406,9 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stub_settings_source: true do # rubo ...@@ -406,9 +406,9 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stub_settings_source: true do # rubo
specify do specify do
if start_metrics_server if start_metrics_server
expect(MetricsServer).to receive(:spawn).with('sidekiq', metrics_dir: metrics_dir, wipe_metrics_dir: true, trapped_signals: trapped_signals) expect(MetricsServer).to receive(:fork).with('sidekiq', metrics_dir: metrics_dir, wipe_metrics_dir: true, reset_signals: trapped_signals)
else else
expect(MetricsServer).not_to receive(:spawn) expect(MetricsServer).not_to receive(:fork)
end end
cli.run(%w(foo)) cli.run(%w(foo))
...@@ -421,7 +421,7 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stub_settings_source: true do # rubo ...@@ -421,7 +421,7 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stub_settings_source: true do # rubo
let(:sidekiq_exporter_enabled) { true } let(:sidekiq_exporter_enabled) { true }
it 'does not start the server' do it 'does not start the server' do
expect(MetricsServer).not_to receive(:spawn) expect(MetricsServer).not_to receive(:fork)
cli.run(%w(foo --dryrun)) cli.run(%w(foo --dryrun))
end end
...@@ -434,7 +434,7 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stub_settings_source: true do # rubo ...@@ -434,7 +434,7 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stub_settings_source: true do # rubo
before do before do
allow(cli).to receive(:sleep).with(a_kind_of(Numeric)) allow(cli).to receive(:sleep).with(a_kind_of(Numeric))
allow(MetricsServer).to receive(:spawn).and_return(99) allow(MetricsServer).to receive(:fork).and_return(99)
cli.start_metrics_server cli.start_metrics_server
end end
...@@ -453,8 +453,8 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stub_settings_source: true do # rubo ...@@ -453,8 +453,8 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stub_settings_source: true do # rubo
allow(Gitlab::ProcessManagement).to receive(:all_alive?).with(an_instance_of(Array)).and_return(false) allow(Gitlab::ProcessManagement).to receive(:all_alive?).with(an_instance_of(Array)).and_return(false)
allow(cli).to receive(:stop_metrics_server) allow(cli).to receive(:stop_metrics_server)
expect(MetricsServer).to receive(:spawn).with( expect(MetricsServer).to receive(:fork).with(
'sidekiq', metrics_dir: metrics_dir, wipe_metrics_dir: false, trapped_signals: trapped_signals 'sidekiq', metrics_dir: metrics_dir, wipe_metrics_dir: false, reset_signals: trapped_signals
) )
cli.start_loop cli.start_loop
......
...@@ -36,13 +36,13 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath ...@@ -36,13 +36,13 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath
%w(puma sidekiq).each do |target| %w(puma sidekiq).each do |target|
context "when targeting #{target}" do context "when targeting #{target}" do
describe '.spawn' do describe '.fork' do
context 'when in parent process' do context 'when in parent process' do
it 'forks into a new process and detaches it' do it 'forks into a new process and detaches it' do
expect(Process).to receive(:fork).and_return(99) expect(Process).to receive(:fork).and_return(99)
expect(Process).to receive(:detach).with(99) expect(Process).to receive(:detach).with(99)
described_class.spawn(target, metrics_dir: metrics_dir) described_class.fork(target, metrics_dir: metrics_dir)
end end
end end
...@@ -58,13 +58,47 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath ...@@ -58,13 +58,47 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath
expect(server).to receive(:start) expect(server).to receive(:start)
end end
described_class.spawn(target, metrics_dir: metrics_dir) described_class.fork(target, metrics_dir: metrics_dir)
end end
it 'resets signal handlers from parent process' do it 'resets signal handlers from parent process' do
expect(Gitlab::ProcessManagement).to receive(:modify_signals).with(%i[A B], 'DEFAULT') expect(Gitlab::ProcessManagement).to receive(:modify_signals).with(%i[A B], 'DEFAULT')
described_class.spawn(target, metrics_dir: metrics_dir, trapped_signals: %i[A B]) described_class.fork(target, metrics_dir: metrics_dir, reset_signals: %i[A B])
end
end
end
describe '.spawn' do
let(:expected_env) do
{
'METRICS_SERVER_TARGET' => target,
'WIPE_METRICS_DIR' => '0'
}
end
it 'spawns a new server process and returns its PID' do
expect(Process).to receive(:spawn).with(
expected_env,
end_with('bin/metrics-server'),
hash_including(pgroup: true)
).and_return(99)
expect(Process).to receive(:detach).with(99)
pid = described_class.spawn(target, metrics_dir: metrics_dir)
expect(pid).to eq(99)
end
context 'when path to gitlab.yml is passed' do
it 'sets the GITLAB_CONFIG environment variable' do
expect(Process).to receive(:spawn).with(
expected_env.merge('GITLAB_CONFIG' => 'path/to/config/gitlab.yml'),
end_with('bin/metrics-server'),
hash_including(pgroup: true)
).and_return(99)
described_class.spawn(target, metrics_dir: metrics_dir, gitlab_config: 'path/to/config/gitlab.yml')
end end
end end
end end
...@@ -72,6 +106,14 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath ...@@ -72,6 +106,14 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath
end end
context 'when targeting invalid target' do context 'when targeting invalid target' do
describe '.fork' do
it 'raises an error' do
expect { described_class.fork('unsupported', metrics_dir: metrics_dir) }.to(
raise_error('Target must be one of [puma,sidekiq]')
)
end
end
describe '.spawn' do describe '.spawn' do
it 'raises an error' do it 'raises an error' do
expect { described_class.spawn('unsupported', metrics_dir: metrics_dir) }.to( expect { described_class.spawn('unsupported', metrics_dir: metrics_dir) }.to(
...@@ -81,26 +123,18 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath ...@@ -81,26 +123,18 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath
end end
end end
shared_examples 'a metrics exporter' do |target, expected_name|
describe '#start' do describe '#start' do
let(:exporter_class) { Class.new(Gitlab::Metrics::Exporter::BaseExporter) } let(:exporter_double) { double('exporter', start: true) }
let(:exporter_double) { double('fake_exporter', start: true) } let(:wipe_metrics_dir) { true }
let(:settings) { { "fake_exporter" => { "enabled" => true } } }
subject(:metrics_server) { described_class.new('fake', metrics_dir, true)} subject(:metrics_server) { described_class.new(target, metrics_dir, wipe_metrics_dir) }
before do
stub_const('Gitlab::Metrics::Exporter::FakeExporter', exporter_class)
expect(exporter_class).to receive(:instance).with(
settings['fake_exporter'], gc_requests: true, synchronous: true
).and_return(exporter_double)
expect(Settings).to receive(:monitoring).and_return(settings)
end
it 'configures ::Prometheus::Client' do it 'configures ::Prometheus::Client' do
metrics_server.start metrics_server.start
expect(prometheus_config.multiprocess_files_dir).to eq metrics_dir expect(prometheus_config.multiprocess_files_dir).to eq metrics_dir
expect(::Prometheus::Client.configuration.pid_provider.call).to eq 'fake_exporter' expect(::Prometheus::Client.configuration.pid_provider.call).to eq expected_name
end end
it 'ensures that metrics directory exists in correct mode (0700)' do it 'ensures that metrics directory exists in correct mode (0700)' do
...@@ -110,8 +144,6 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath ...@@ -110,8 +144,6 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath
end end
context 'when wipe_metrics_dir is true' do context 'when wipe_metrics_dir is true' do
subject(:metrics_server) { described_class.new('fake', metrics_dir, true)}
it 'removes any old metrics files' do it 'removes any old metrics files' do
FileUtils.touch("#{metrics_dir}/remove_this.db") FileUtils.touch("#{metrics_dir}/remove_this.db")
...@@ -120,7 +152,7 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath ...@@ -120,7 +152,7 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath
end end
context 'when wipe_metrics_dir is false' do context 'when wipe_metrics_dir is false' do
subject(:metrics_server) { described_class.new('fake', metrics_dir, false)} let(:wipe_metrics_dir) { false }
it 'does not remove any old metrics files' do it 'does not remove any old metrics files' do
FileUtils.touch("#{metrics_dir}/remove_this.db") FileUtils.touch("#{metrics_dir}/remove_this.db")
...@@ -141,4 +173,36 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath ...@@ -141,4 +173,36 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath
subject.start subject.start
end end
end end
describe '#name' do
let(:exporter_double) { double('exporter', start: true) }
subject(:name) { described_class.new(target, metrics_dir, true).name }
it { is_expected.to eq(expected_name) }
end
end
context 'for puma' do
before do
allow(Gitlab::Metrics::Exporter::WebExporter).to receive(:instance).with(
gc_requests: true, synchronous: true
).and_return(exporter_double)
end
it_behaves_like 'a metrics exporter', 'puma', 'web_exporter'
end
context 'for sidekiq' do
let(:settings) { { "sidekiq_exporter" => { "enabled" => true } } }
before do
allow(::Settings).to receive(:monitoring).and_return(settings)
allow(Gitlab::Metrics::Exporter::SidekiqExporter).to receive(:instance).with(
settings['sidekiq_exporter'], gc_requests: true, synchronous: true
).and_return(exporter_double)
end
it_behaves_like 'a metrics exporter', 'sidekiq', 'sidekiq_exporter'
end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment