Guest
Public paste!

Ben

By: a guest | Jan 22nd, 2010 | Syntax: Perl | Size: 10.52 KB | Hits: 84 | Expires: Never
Copy text to clipboard
  1. #!/usr/bin/env perl
  2.  
  3. #////////////////////////////////////////////////////////////////////////////////////
  4. #
  5. #   memCron - DreamHost PS Memory Manager
  6. #
  7. #   Author   : Yaosan Yeo
  8. #   Version  : 0.4.1
  9. #   Date     : 10 August 2009
  10. #   URL      : http://memcron.com/
  11. #
  12. #   History  :
  13. #
  14. #   0.5 [2010.01.22] - Replaced most external calls with builtins
  15. #
  16. #   0.4.1 [2009.08.10 ] - Fixed: Memory calculation due to the 2x increase of swap memory by DreamHost
  17. #
  18. #   0.4 [2009.06.28] - Added: New parameter "$max_memory" and "$min_memory" to limit the memory range that memCron should operate within
  19. #                    - Added: New parameter "$mem_threshold" to set a memory threshold, which when exceeded, will send out notification to $email
  20. #                    - Added: 4 new subroutines - round(), get_mem_size(), get_mem_total(), get_mem_target() to simplify the code structure
  21. #                    - Added: Base and swap memory size to function output
  22. #                    - Added: Resize information to function output
  23. #                    - Changed: "$debug" parameter moved from config.cfg to this file
  24. #                    - Removed: "$username" parameter, now DreamHost API only needs API key for authentication
  25. #                    - Fixed: Memory target are now computed more correctly during downsize
  26. #
  27. #   0.3 [2009.05.27] - Added: New parameter "$downsize_resistance" to prevent rapid fluctuations from causing multiple resizes within short period of time
  28. #                    - Changed: 0.3 to 0.7 ratio between "$mem_mean" and "$mem_used instead" of 0.5 to 0.5 when calculating "$mem_free_tolerance"
  29. #                    - Changed: More descriptive error message when log file cannot be opened
  30. #                    - Fixed: "$unix_time" and "$cpu_load" will now be reevaluated until it is defined to ensure data point is always logged (removed in 0.5)
  31. #                    - Fixed: Memory target calculations now correctly accounts max and swap memory, which is capped at 4000 MB and 450MB respectively
  32. #
  33. #   0.2 [2009.05.03] - Fixed: Account for "$num_data" in z-score calculation to ensure number of memory changes is minimized to within daily limits
  34. #                    - Changed: "$use_api" is changed to "$debug", which if set disables all log outputs and API calls
  35. #
  36. #   0.1 [2009.04.30] - Initial release
  37. #
  38. #////////////////////////////////////////////////////////////////////////////////////
  39.  
  40. use strict;
  41. use warnings;
  42. use diagnostics;
  43.  
  44. use LWP 5.64;
  45. use Mail::Sendmail;
  46. use File::Basename;
  47.  
  48. #////////////////////////////////////////////////// get configurations
  49.  
  50. my $debug = 0;
  51.  
  52. # settings from config file
  53.  
  54. our( $key, $ps, $cron_interval, $max_memory, $min_memory, $mem_threshold,
  55.   $email, $mem_free_confidence, $mem_free_to_used_ratio, $downsize_resistance,
  56.   $num_data, $num_data_csv );
  57.  
  58. my $dir = dirname($0);
  59.  
  60. do "$dir/config.cfg";
  61.  
  62. # include z-score function
  63. require "$dir/zscore.pl";
  64.  
  65. # Log directory name
  66. my $log_dir = "$dir/logs";
  67.  
  68. # DreamHost PS limit
  69. my $daily_change_limit = 30;
  70. my $runs_per_day = 1440 / $cron_interval;
  71.  
  72. # DreamHost PS defaults
  73. if ( $max_memory > 4000 ) { $max_memory = 4000; }
  74. if ( $min_memory < 150 ) { $min_memory = 150; }
  75.  
  76. # Statistics values
  77. my $mem_used_zscore = abs( normsinv( ( $daily_change_limit * $num_data / $runs_per_day ) / $runs_per_day ) ); #1.89;
  78. my $mem_free_zscore = abs( normsinv( $mem_free_confidence ) ); #3.09;
  79.  
  80.  
  81. #////////////////////////////////////////////////// main program starts
  82.  
  83. #////////////// get CPU load (5 min average) and unix timestamp
  84.  
  85. sub cpu_load {
  86.     open(LOAD,"/proc/loadavg") || die "Unable to open /proc/loadavg";
  87.     my @l = split /\s+/, <LOAD>;
  88.     close(LOAD);
  89.     return @l;
  90. }
  91.  
  92. sub meminfo {
  93.     open(MEM,"/proc/meminfo") || die "Unable to open /proc/meminfo";
  94.     my %mem = map { (/(\w+):\s*(\w+)/) } <MEM>;
  95.     close(MEM);
  96.     return \%mem;
  97. }
  98.  
  99. my ($cpu_load) = cpu_load();
  100. my $unix_time = time();
  101.  
  102. my $log_memory = "$log_dir/$ps.log";
  103.  
  104. my($mem_total, $mem_used, $mem_free, $mem_size, $mem_swap);
  105.  
  106. # take 60 samples over 1 minute duration
  107. foreach (1..60) {
  108.         my $mem = meminfo();
  109.         $mem_size = int($mem->{MemTotal}  / 1024);
  110.         $mem_swap = int($mem->{SwapTotal} / 1024);
  111.         $mem_total = $mem_size + $mem_swap;
  112.         $mem_used += ($mem_total - int( ($mem->{MemFree} + $mem->{SwapFree}) / 1024 )  );
  113.         sleep 1;
  114. }
  115.  
  116. $mem_used = round($mem_used / 60);
  117. $mem_free = $mem_total - $mem_used;
  118.  
  119. # write to memory log file
  120. unless ($debug) {
  121.         open(LOG, ">>$log_memory") || die "Cannot open memory log file!";
  122.         print LOG "$mem_used\n";
  123.         close(LOG);
  124. }
  125.  
  126.  
  127. #////////////// truncate memory log
  128.  
  129. system("tail -n $num_data $log_memory > $log_memory.temp");
  130. rename "$log_memory.temp", $log_memory;
  131.  
  132.  
  133. #////////////// calculate the amount of memory to be reserved based on past data
  134.  
  135. open(LOG, "$log_memory") || die "Cannot open memory log file!";
  136. my @data = <LOG>;
  137. close(LOG);
  138.  
  139. # construct a short list to be used when downsizing
  140. my @shortlist = @data[(scalar(@data) - $downsize_resistance)..(scalar(@data) - 1)];
  141.  
  142. # make sure we have enough data points before proceeding
  143. if ( scalar(@data) < round($num_data * 0.1) ) { exit 0; }
  144.  
  145. my ( $mem_stattotal, $mem_mean, $mem_median, $mem_stdev ) = stats(@data);
  146.  
  147. $mem_mean = round($mem_mean);
  148. $mem_stdev = round($mem_stdev,2);
  149.  
  150. my $mem_used_tolerance = round($mem_used_zscore * $mem_stdev);
  151. my $mem_free_tolerance = round($mem_free_zscore * $mem_stdev);
  152.  
  153. unless ( $mem_free_tolerance > ($mem_mean*0.3 + $mem_used*0.7) * $mem_free_to_used_ratio ) {
  154.         $mem_free_tolerance = round( ($mem_mean*0.3 + $mem_used*0.7) * $mem_free_to_used_ratio );
  155. }
  156.  
  157.  
  158. #////////////// calculate the memory required
  159.  
  160. my $mem_required = $mem_used + $mem_free_tolerance;
  161. my $mem_target = get_mem_target($mem_required);
  162. my $set = ( abs($mem_total - $mem_required) > $mem_used_tolerance ) ? 1 : 0;
  163.  
  164.  
  165. #////////////// downsize resistance
  166.  
  167. my $downsize_count = 0;
  168.  
  169. my $log_downsize = "$log_dir/downsize.log";
  170.  
  171. if ( ( $mem_target < $mem_size ) && $set ) {
  172.         if (-e "$log_downsize") {
  173.                 open(LOG, "$log_downsize") || die "Cannot open downsize log file!";
  174.                 my @data = <LOG>;
  175.                 close(LOG);
  176.                 $downsize_count = shift(@data);
  177.                 $downsize_count++;
  178.         } else {
  179.                 $downsize_count = 1;
  180.         }
  181.        
  182.         if ( $downsize_count >= $downsize_resistance ) {
  183.                 $downsize_count = 0;
  184.                
  185.                 $mem_required = round( (stats(@shortlist))[1] ) + $mem_free_tolerance;
  186.                 $mem_target = get_mem_target($mem_required);
  187.         } else {
  188.                 $set = 0;
  189.         }
  190. }
  191.  
  192. unless ($debug) {
  193.         open(LOG, ">$log_downsize") || die "Cannot open downsize log file!";
  194.         print LOG "$downsize_count";
  195.         close(LOG);
  196. }
  197.  
  198.  
  199. #////////////// set memory using DreamHost API, output result
  200.  
  201. my $sendmail;
  202. my $date = Mail::Sendmail::time_to_date(time());
  203.  
  204. print "\nDate: $date\n\n";
  205. print "Total: $mem_total\t Used: $mem_used\t Free: $mem_free\n";
  206. print "Mem: $mem_size\t Swap: $mem_swap\n";
  207. print "Mean: $mem_mean\t Stdev: $mem_stdev\t Load: $cpu_load\n";
  208. print "Tolerance: $mem_used_tolerance \t Tolerance (free): $mem_free_tolerance\n";
  209.  
  210. if ($mem_target == $mem_size) {
  211.         $set = 0;
  212. }
  213.  
  214. if ($set) {
  215.         print "Target: $mem_target\n";
  216.        
  217.         print "\nMemory was resized from $mem_size MB to $mem_target MB!\n";
  218.        
  219.         # check against memory threshold, send notification email if it is exceeded
  220.         if ( $mem_threshold && $mem_target > $mem_threshold ) {
  221.                 print "\n*** Warning! Threshold exceeded! ***\n";
  222.                 $sendmail = 1;
  223.         }
  224.  
  225.         unless ($debug) {
  226.                 # send notification if necessary
  227.                 if ( $sendmail && $email ) {
  228.                         my $top = `top -b -n 1`; chomp($top);
  229.                        
  230.                         my $message = <<MESSAGE;
  231. Date: $date
  232.  
  233. Total: $mem_total       Used: $mem_used Free: $mem_free
  234. Mean: $mem_mean Stdev: $mem_stdev       Load: $cpu_load
  235. Mem: $mem_size  Swap: $mem_swap
  236. Tolerance: $mem_used_tolerance  Tolerance (free): $mem_free_tolerance
  237. Target: $mem_target
  238.  
  239. Memory was resized from $mem_size MB to $mem_target MB!
  240.  
  241. -----
  242.  
  243. List of running processes:
  244.  
  245. $top
  246.  
  247. This report is generated by memCron at $date.
  248. MESSAGE
  249.                         my %mail = (
  250.                                 To => $email,
  251.                                 From => 'memCron <noreply@memcron.com>',
  252.                                 Message => $message,
  253.                                 Subject => "Memory threshold exceeded for $ps"
  254.                         );
  255.  
  256.                         sendmail(%mail) or die $Mail::Sendmail::error;
  257.                        
  258.                         print "\nNotification sent to $email.\n";
  259.                 }
  260.                
  261.                 # use API to resize memory
  262.                 my $url = "https://panel.dreamhost.com/api/?key=" . $key . "&cmd=dreamhost_ps-set_size&unique_id=" . `uuidgen` . "&ps=" . $ps . "&size=" . $mem_target;
  263.                 my $browser = LWP::UserAgent->new;
  264.                 my $response = $browser->get($url);
  265.  
  266.                 $response->is_success || die "Error at $url\n ", $response->status_line, "\n Aborting";
  267.                 print "\n" , $response->content, "\n";
  268.         }
  269. } else {
  270.         print "Target: $mem_target\n";
  271.        
  272.         print "\nNo need to adjust memory" , ($downsize_count) ? " yet! ($downsize_count/$downsize_resistance)" : "!" , "\n";
  273. }
  274.  
  275. print "\n-----\n";
  276.  
  277.  
  278. #////////////// write memory usage and cpu load to csv file
  279.  
  280. my $log_csv = "$log_dir/$ps.csv";
  281.  
  282. unless ($debug) {
  283.         open(LOG, ">>$log_csv") || die "Cannot open csv file!";
  284.  
  285.  
  286.         if ($set) {
  287.                 print LOG "$unix_time,",get_mem_total($mem_target),",$mem_used,$cpu_load,$mem_mean,$mem_stdev,$mem_used_tolerance,$mem_free_tolerance,$downsize_count\n";
  288.         } else {
  289.                 print LOG "$unix_time,$mem_total,$mem_used,$cpu_load,$mem_mean,$mem_stdev,$mem_used_tolerance,$mem_free_tolerance,$downsize_count\n";
  290.         }
  291.                
  292.         close(LOG);
  293. }
  294.  
  295.  
  296. #////////////// truncate csv log
  297.  
  298. if ( -e $log_csv ) {
  299.     system("tail -n $num_data_csv $log_csv > $log_csv.temp");
  300.     system("mv $log_csv.temp $log_csv");
  301. }
  302.  
  303.  
  304. #////////////// subroutines
  305.  
  306. sub round {
  307.      my ($val,$places) = @_;
  308.      $places ||= 0;
  309.      return sprintf("%.${places}f", $val || 0);
  310. }
  311.  
  312. sub get_mem_total {
  313.         my $mem_target = shift;
  314.         return ($mem_target < 1350) ? $mem_target * 3 : $mem_target + 900;
  315. }
  316.  
  317. sub get_mem_target {
  318.         my $mem_required = shift;
  319.         my $mem_swap = ($mem_required < 1350) ? round($mem_required / 3 * 2) : 900;
  320.         my $mem_target = $mem_required - $mem_swap;
  321.        
  322.         # handle max and min memory limit
  323.         if ( $mem_target < $min_memory ) {
  324.                 $mem_target = $min_memory;
  325.         } elsif ( $mem_target > $max_memory ) {
  326.                 $mem_target = $max_memory;
  327.         }
  328.        
  329.         return (round($mem_target))
  330. }
  331.  
  332. sub stats {
  333.     my $total = 0;
  334.     foreach my $v (@_) { $total += $v; }
  335.  
  336.     my $average = $total / @_;
  337.     my $median = @_ % 2        ? $_[(@_-1)/2]  
  338.                :                  ($_[@_/2-1]+$_[@_/2])/2
  339.                           ;
  340.  
  341.     my $sqtotal = 0;
  342.     foreach my $v (@_) {
  343.         $sqtotal += ($average-$v) ** 2;
  344.     }
  345.     my $stdev = ($sqtotal / @_) ** 0.5;
  346.  
  347.     return ( $total, $average, $median, $stdev );
  348. }