Rails kann alles vieles. Und, zu den vielen Sachen die es kann, gehört auch das Ausliefern von Dateien. Diese Problematik erledigt das Framework in der Regel zwar ganz gut, aber sobald die Größe der Dateien zunimmt kann es mitunter zu merkwürdigem Fehlverhalten und Downloadfehlern kommen.

Eine Ursache dafür ist Rubys Gargabe Collector. Da Rails die Dateien, bevor es sie ausliefert, in den Speicher lädt, greift er ganz gerne mal dazwischen und sorgt für Unruhe im Speicherwald. Es scheint mir allerdings generell recht fragwürdig 100 Mb in den Speicher zu laden. Es muss also eine andere Lösung her.

Im Normalfall besteht ein Railsstack aus mehreren Mongrel Instanzen und einem lokalen Proxy der die Anfragen auf diese Instanzen verteilt. Sind mehrere App Server im Einsatz wird noch ein weiterer Proxy/Loadbalancer benötigt, der die Requests auf die einzelnen Server verteilt. Es bietet sich also an, die Dateien direkt durch den jeweiligen lokalen Proxy (i.R. Nginx, Lighty, Pound oder Apache) ausliefern zu lassen.

Um das zu erreichen, lassen wir Rails anstatt der eigentlichen Datei einen modifizierten, internen Header in die Antwort zum Client einsetzen. Diese wird vom Proxy erkannt und veranlasst ihn dann eine Datei aus einem definierten Ort zu verschicken. Dieser interne Header wird in der Regel vom Proxy aus den Headern entfernt, bevor die Antwort an den externen Client weitergereicht wird.

Am Beispiel Nginx könnte der Code dafür so aussehen:

1
2
3
4
5
6
7
8
9
10

case RAILS_ENV
when 'production'
  response.headers['X-Accel-Redirect'] = "/protected/" + File.basename(file.path)
  response.headers['Content-Disposition'] = "attachment; filename=file.tar"
  render :nothing => true
else
  # Directly send in development env
  send_data(file.read, :filename => File.basename(file.path))
end

 

Die Konfiguration des Nginx ist recht einfach:

1
2
3
4
5

location /protected/ {
  internal;
  alias /path/to/file/directory/;
}

 

In ‘Multi-Server’ Umgebungen sollte man darauf achten, dass Downloads nicht durch den First-Proxy geleitet werden, sondern direkt auf den jeweiligen App-Server. So lässt es sich vermeiden, dass der Loadbalancer die längeren Anfragen/Antworten als Timeouts deutet und die Verbindung kappt.

Leave a Reply