tobias-barth.net

Modernes Webdesign aus Köln

Node.js Performance und socket hangup

In dem Team bei einer großen deutschen Internetfirma, in dem ich gerade arbeite, haben wir ein bisher eher unerklärliches Problem, das in unseren Logfiles aufschlägt.

Wir haben dort eine Express-basierte Webapp, die den Content im Grunde vollständig auf dem Server rendert. Dafür werden unter anderem diverse Microservices angesprochen, d.h. bei jedem Request an die Express-App fragt diese per AJAX mehrere interne, separate Dienste nach Daten und rendert basierend auf deren Antworten den Content, der an den Client gesendet wird.

Das funkioniert auch ziemlich gut, allerdings haben wir in den Logfiles unserer App ungewöhnlich viele “ERROR: socket hang up”-Meldungen. Nicht so viele, dass wir ernsthafte Schwierigkeiten hätten, die auch sichtbar wären, aber, und das ist besonders interessant, es sind wesentlich mehr als bei anderen Webapps, die Java-basiert sind und dieselben Services bei jedem Request anfragen.

Es scheint also primär nichts mit den internen Services zu tun zu haben, sondern mit unserer Node-App. Bisher haben wir nicht so wirklich eine Idee, was es sein könnte. Für die AJAX-calls nutzen wir Axios und definieren damit auch ein Timeout, das bei 60ms liegt. Das kann es allerdings nicht sein, denn dann sähen wir timeout-Errors und nicht Socket-hang-ups.

Also habe ich mal das Internet danach durchforstet, was diese Socket-hangups bei Node erzeugen könnte. Dabei bin ich auf einen interessanten Blogpost gestoßen. Dort wird beschrieben, dass solche Fehler zwar auch von der Remote-Seite kommen können, aber eben auch von den Limitierungen des Systems, auf dem der Node-Server läuft. Es gibt ein Limit für die Zahl der gleichzeitig offenen Files, wobei dieses Limit pro Login=User gilt. Der Standardwert ist 1024. Ein Socket ist wie alles auf unixoiden Systemen eine Datei. Ein User (in dem Fall der User, dem der Node-Prozess gehört), kann also maximal 1024 Dateien gleichzeitig öffnen.

Das wäre auf jeden Fall mal ein Ansatz. Der Verfasser des erwähnten Blogartikels empfiehlt ein Limit von 10240, also das Zehnfache. Außerdem sollte der maxSockets-Wert des http.Agents von Node auf eine Zahl knapp unter dem ulimit gesetzt werden. Nodes Standard für maxSockets ist (mittlerweile) Infinity, aber bei uns ist es derzeit auf 25 gesetzt. Ich muss noch mal nachforschen, was der Grund für dieses Limit war.

Ohne dass ich weiß, wie unsere Java-Apps funktionieren oder konfiguriert sind, gefällt mir der Gedanke, dass unsere Node.js-App ankommende Anfragen so schnell verarbeitet und dementsprechend oft parallel die Services anspricht, dass es an Systemlimits stößt, die von den Java-Kreuzern nie tangiert werden.

Schauen wir mal.

Mein Blog

Endlich habe ich es doch geschafft. Mein Blog ist funktionsfähig und online.

Ich hatte ja schon im zugehörigen Github-Issue geschrieben, dass ich mich für das Hexo-Framework entschieden hatte. Ich fand es spannend, ein bisschen ungewöhnlich und natürlich toll, dass es in Nodejs geschrieben ist. So kann ich schnell Dinge ändern bzw. fixen, die mich stören.

Das erste habe ich schon gefixt, nämlich das hexo-generator-feed-Plugin. Das erzeugte in meiner Konfiguration (also Hexo-Blog in Unterordner) falsche Permalinks. Ich habe den Autor zwar unter dem betreffenden Commit gefragt, wofür der war (er hat es kaputt geändert), aber bisher keine Antwort. Ich werd’s wohl demnächst auch mal als Bug oder gleich als Pull Request filen. Die reparierte Version findet man zur Zeit bei mir: 4ndurils hexo-generator-feed.

Ich habe zwar schon länger ein privates Blog gehabt, in dem auch hin und wieder Web-Kram landete, aber ich wollte jetzt gern im richtigen Rahmen und öfter Sachen aus dem Frontend-Alltag (oder Säuremiene, wie manche sagen) posten. Zwei, drei alte Posts vom anderen Blog habe ich hier der Vollständigkeit halber importiert, aber die sind wirklich schon betagt.

Achso, zwei Dinge noch:

  1. Das hier ist nur die minimalist working version. Es gibt z.B. noch keine vernünftige Integration von Kategorien und Tags (sie werden zumindest noch nicht dargestellt). Das wird sich über die Zeit aber immer weiter verbessern. Die Grundfunktion eines Blogs sind Einträge und der Feed. Beides geht.
  2. Es gibt keine Kommentarsektion. Das ist auch Absicht und wird erstmal so bleiben. Ob ich meine Meinung ändere, weiß ich noch nicht, derzeit steht sie aber fest. Kommentare und Anmerkungen erhalte ich aber trotzdem sehr gern per Twitter oder Email.

Viel Spaß. Ich freu mich!

Vererbung von viewport-percentage lengths in Chrome

Ich stolperte neulich über ein Problem mit viewport-relativen Längen in CSS. Die Situation war konkret folgende:

Ich hatte eine ungeordnete Liste, deren Elemente je ein <div> enthielten, in dem sich wiederum ein <a> befand:

1
2
3
4
5
<ul>
<li>
<div><a href="#">Test</a></div>
</li>
</ul>

Zuerst ein Beispiel, wie es funktionieren sollte. Die <li>-Elemente sollen eine definierte Höhe haben und die <div>s sollten genauso hoch sein. Das CSS dazu:

1
2
3
4
5
6
7
8
9
10
* { margin:0; padding:0; }
ul { list-style-type:none; }
li {
background:blue;
height:10em;
}
div {
background:red;
height:100%; /* Genau so hoch wie sein Container */
}

Hier ist ein JS-Fiddle dazu. Wie erwartet bedeckt das rote <div> das gesamte Listenelement. Ruhig auch mal in verschiedenen Browsern ansehen.

Jetzt geben wir dem <li>-Element aber eine vom Viewport abhängige Größe:

1
2
3
li {
height:30vw; /* Die Höhe soll 30 Prozent der Breite des Viewports betragen */
}

Hier die geänderte Demo.

Im Firefox sieht es immer noch genau so aus wie vorher. Das würde man (ich) auch erwarten. Schließlich hat das Listenelement eine definierte Größe und ich sage, dass sein direktes Kindelement 100% dieser Größe haben soll. Sieht man sich das Ergebnis in Chrome an, zeigt sich aber ein anderes Bild.

Das <li> hat zwar die richtige Größe, aber das <div> ist nur so hoch, wie es sein Inhalt (eine Textzeile) erfordert. Sogar im IE9 wird es korrekt (wie im FF) dargestellt. Opera unterstützt derzeit keine viewport-related lengths, aber da auch dort demnächst ein Webkit rendert, wird es sich wohl auch nur mittelmäßig zum Guten ändern.

Anscheinend muss man derzeit also entweder für Chrome in solchen Fällen auch jedem Kindelement die v*-Größe zuweisen, oder mit anderen Längeneinheiten arbeiten. Schade.

jQuerys scrollTop() und border-box

Das Folgende betrifft soweit ich sehe nur jQuery < 1.8.

Letzte Woche habe ich den ersten Kandidaten für den irrsten Bug in diesem Jahr gefunden.

Der Fehler, den der Kunde berichtete, beruhte darauf, dass das ursprünglich verwendete $(document).scrollTop() nicht im IE8 funktionierte und nachdem er dies durch das funktionierende $('html').scrollTop() ersetzt hatte, lief es nicht mehr in Webkit-Browsern.

Die Lösung dafür war relativ schnell gefunden: Ich ersetzte einfach in dem betreffenden Script den einen scrollTop()-Aufruf durch den Ausdruck ($(document).scrollTop() || $('html').scrollTop()). Damit war der Drops gelutscht und jeder Browser konnte sich aussuchen, was ihm passte (bzw. was nicht gleich 0 war, da der Code nur beim Scrollen zur Anwendung kam, reichte das aus).

Aber was war die Ursache? Nach viel Ausprobieren blieb mir nichts übrig als in die jQuery-Source zu schauen und mir anzusehen, wie jQuery.scrollTop() definiert ist. Für das verwendete jQuery 1.6.4 sieht das so aus:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Create scrollLeft and scrollTop methods
jQuery.each( ["Left", "Top"], function( i, name ) {
var method = "scroll" + name;

jQuery.fn[ method ] = function( val ) {
var elem, win;

if ( val === undefined ) {
elem = this[ 0 ];

if ( !elem ) {
return null;
}

win = getWindow( elem );

// Return the scroll offset
return win ? ("pageXOffset" in win) ? win[ i ? "pageYOffset" : "pageXOffset" ] :
jQuery.support.boxModel &amp;&amp; win.document.documentElement[ method ] ||
win.document.body[ method ] :
elem[ method ];
}

// Set the scroll offset

// Interessiert uns hier nicht
};
});

Der Fall des IE8 sollte mit der Zeile abgedeckt werden, die mit jQuery.support.boxModel beginnt. Diese Eigenschaft dient eigentlich dazu, zu überprüfen, ob das W3C-Box-Modell vom Browser unterstützt wird. Das Problem ist, in diesem Fall gibt sie false zurück, obwohl das Dokument in Ordnung ist und der Browser sich im Standardmodus befindet. Warum? Also nachsehen, wie jQuery.support.boxModel gesetzt wird:

1
2
3
// Figure out if the W3C box model works as expected
div.style.width = div.style.paddingLeft = "1px";
support.boxModel = div.offsetWidth === 2;

Es wird ein Test-Div erzeugt, dem verschiedene Eigenschaften zugewiesen und das dann an den body angehängt wird. Da das Element hier einen Pixel breit ist und außerdem ein Padding von einem Pixel erhält, sollte offsetWidth, das die Gesamtbreite enthält, 2px zurückgeben.

Tut es aber nicht.

Der Grund dafür ist, dass ich im CSS box-sizing: border-box; gesetzt habe. Das ist eigentlich eine prima Sache, denn dadurch sind Elemente, denen man eine bestimmte Breite zuweist, auch wirklich so breit – egal ob sie Innenabstände oder Rahmen enthalten. Aber in diesem Fall passiert dann folgendes: Wir geben einem Element den linken Innenabstand 1px und sagen dann, dass das gesamte Element, mit Innenabständen und Rahmen, 1px breit sein soll. Das bedeutet, für den eigentlichen Inhalt ist kein Platz mehr, was egal ist, weil das Element sowieso keinen Inhalt hat und nur zum Testen da ist. Aber das bedeutet auch, dass offsetWidth nun auch 1 enthält, das ist schließlich die Gesamtbreite des Elements. Damit schlägt der Test div.offsetWidth === 2 natürlich fehl, und scrollTop() gibt nicht mehr window.document.documentElement.scrollTop zurück, sondern 0.

Darauf muss man erstmal kommen

Ab jQuery 1.8 besteht das Problem nicht mehr, weil dann nicht mehr auf Box-Model-Support getestet wird bzw. dieser Test nicht mehr in scrollTop() abgerufen wird.

Warum Webkit übrigens $('html').scrollTop() nicht versteht, ist mir noch nicht so ganz klar.

.htaccess-Spielereien

Mit .htaccess-Dateien kann man den Zugang zu den einzelnen Dateien oder Dokumenten auf seinem (Apache-)Webserver sehr bequem regeln. Es lassen sich zum Beispiel einzelne Verzeichnisse nur für Nutzer mit einer bestimmten IP freigeben. Oder man kann Passwörter für den Zugang vergeben. Vor ein paar Tagen habe ich herausgefunden, dass man mit ihnen auch wunderbar beliebige andere Servervariablen abfragen kann.

Mit dem Modul mod_setenvif nämlich lassen sich mit Hilfe der zwei Direktiven BrowserMatch und SetEnvIf (und deren Varianten, denen Groß- und Kleinschreibung egal ist) Umgebungsvariablen abfragen und davon abhängig den Zugang zu Ressourcen regeln.

Ich habe das benutzt, um folgendes zu tun. Ein PHP-Dokument bindet mit require eine HTML-Datei ein und zwar abhängig vom Rückgabewert eines If-Statements:

1
if (…) { require "eingebunden.html";}

Auch wenn ich den Namen der HTML-Datei so wähle, dass er schwer erraten werden kann, möchte ich sichergehen, dass niemand einfach die Adresse der Datei in den Browser eingeben und so ihren Inhalt anzeigen kann. Das geht jetzt ziemlich einfach, indem ich eine .htaccess-Datei in das Verzeichnis lege, in dem sich die fragliche HTML-Datei befindet und sie mit diesem Inhalt versehe:

1
2
SetEnvIf Request_URI "eingebunden\.html$" verboten
Deny from env=verboten

Mit der SetEnvIf-Direktive setze ich eine Variable namens „verboten“ genau dann wenn der URI-String, den der Browser als Request gesendet hat, mit dem regulären Ausdruck "eingebunden\.html$" übereinstimmt. Als nächstes sage ich dem Server, dass er Anfragen, für die er diese Variable erzeugt hat, ablehnen soll. Versucht man jetzt die HTML-Datei direkt im Browser aufzurufen, ist das einzige, was man sieht, ein 403.

Flattr this