JVM in Docker and PTRACE_ATTACH

Docker nowadays (since 1.10, the original pull request is here docker/docker/#17989) adds some security to running containers by wrapping them in both AppArmor (or presumably SELinux on RedHat systems) and seccomp eBPF based syscall filters (here’s a nice article about it). And ptrace is disabled in the default seccomp profile.

$ docker run alpine sh -c 'apk add -U strace && echo'
fetch http://dl-cdn.alpinelinux.org/alpine/v3.4/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.4/community/x86_64/APKINDEX.tar.gz
(1/1) Installing strace (4.11-r2)
Executing busybox-1.24.2-r11.trigger
OK: 6 MiB in 12 packages
strace: ptrace(PTRACE_TRACEME, ...): Operation not permitted
+++ exited with 1 +++

Why am I writing about this? Because some JDK tools depend on PTRACE_ATTACH on Linux. One of them is  very useful jmap.

Turning seccomp off (–security-opt seccomp=unconfined) is not recommended, but we can add just this one explicit capability  with –cap-add=SYS_PTRACE.

$ docker run --cap-add=SYS_PTRACE alpine sh -c 'apk add -U strace && strace echo'
fetch http://dl-cdn.alpinelinux.org/alpine/v3.4/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.4/community/x86_64/APKINDEX.tar.gz
(1/1) Installing strace (4.11-r2)
Executing busybox-1.24.2-r11.trigger
OK: 6 MiB in 12 packages
execve("/bin/echo", ["echo"], [/* 5 vars */]) = 0
arch_prctl(ARCH_SET_FS, 0x7feaca3a8b28) = 0
set_tid_address(0x7feaca3a8b60) = 10
mprotect(0x7feaca3a5000, 4096, PROT_READ) = 0
mprotect(0x558c47ec6000, 16384, PROT_READ) = 0
getuid() = 0
write(1, "\n", 1) = 1
exit_group(0) = ?
+++ exited with 0 +++

Docker Compose supports cap_add since version 1.1.0 (2015-02-25).

If you run into an issue with the jmap and jstack from OpenJDK failing with exception java.lang.RuntimeException: unknown CollectedHeap type : class sun.jvm.hotspot.gc_interface.CollectedHeap make sure you install openjdk-debuginfo package (or openjdk-8-dbg or something similiar depending on distro).

Play! Framework – część I

Z związku z moim zainteresowaniem Scalą i Akką naturalnym jest zajęcie się ostatnim elementem stosu reaktywnych technologii dla JVM od Typesafe – frameworkiem Play. Niniejszy wpis stanowi, mam nadzieję, pierwszy z serii wpisów opisujących moją przygodę z tym frameworkiem.

Czym jest Play?

Play jest moim zdaniem najlepszym frameworkiem ogólnego przeznaczenia dla JVM
Ogólnie rzeczy ujmując Play to implementacja wzorca MVC w oparciu o model aktów Akka połączona ze środowiskiem do zarządzania projektem i uruchamiania go

Co w nim tak bardzo mi się podoba?

Jednym z głównych filarów architektonicznych Play jest podejście bezstanowe i asynchroniczne.  W Play nie ma tradycyjnej „sesji”. Takie podejście nastręcza czasami pewne trudności, ale są one z nawiązką rekompensowane przez zalety takie jak łatwość horyzontalnego skalowania i wsparcie dla technologi reaktywnych. I Scala. Uwielbiam Scalę.

Zacznijmy jednak od podstaw.

Szkielet aplikacji Play można łatwo wygenerować za pomocą narzędzia Typesafe Activator  – activator new [name] [template-id] – ale nie jest dobry sposób na rozpoczęcie przygody z Play.

activator new my-play-app play-scala

Zacznijmy więc od minimalnej aplikacji Play do której będziemy dodawać kolejne elementy w miarę potrzeby. Minimalna aplikacja jest naprawdę niewielka i składa się z czterech plików z których jeden (application.conf) jest pusty a jeden (build.properties) tak naprawdę  opcjonalny.

$ ls -R
.:
build.properties build.sbt conf/ project/

./conf:
application.conf

./project:
plugins.sbt

build.sbt

name := """my-play-app"""

version := "1.0-SNAPSHOT"

lazy val root = (project in file(".")).enablePlugins(PlayScala)

scalaVersion := "2.11.7"

resolvers += "scalaz-bintray" at "http://dl.bintray.com/scalaz/releases"

project/plugins.sbt

// The Play plugin
addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.5.4")

project/build.properties

sbt.version=0.13.11

Nie jest to co prawda zbyt przydatna aplikacja – ale kompiluje się i uruchamia bez przeszkód o czym możemy się przekonać wydając polecenie activator ~run w katalogu aplikacji.

Skrypt wykonujący poszczególne krotki jest dostępny jako Gist

bash <(curl -fsSL https://git.io/vKW88) my-play-app

Jak ustawić parametr -XX:+HeapDumpOnOutOfMemoryError w serwerze WildFly

Należy w pliku „%JBOSS_HOME%\bin\standalone.conf.bat” ustawić parametry -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=<hprof-dump-dir zamieniając linię
set „JAVA_OPTS=-Xms64M -Xmx512M -XX:MaxPermSize=256M”set „JAVA_OPTS=-Xms64M -Xmx512M -XX:MaxPermSize=256M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=F:\memory-dumps”

Alternatywnym sposobem jest ustawienie zmiennej środowiskowej JAVA_OPTS – skrypt uruchamiający serwer standalone.bat ją uwzględnia.

Można to zrobić na działającym systemie korzystając z JConsole i operacji setVMOption mbeana com.sun.management:type=HotSpotDiagnostic lub korzystając z programu jinfo z JDK.

Sprawdzanie wersji plików JAR

Niedawno znalazłem się w sytuacji, gdy musiałem sprawdzić wersje plików JAR by zapewnić zgodność tworzonego release-a z Javą 6. Nie udało mi się znaleźć gotowego narzędzia, więc napisałem coś własnego: github.com/jarek-przygodzki/jarversion.

Program jest napisany w Scali, ale dla użytkownika nie ma to znaczenia. Po zbudowanie projektu użycia jest bardzo proste

$ jarversion

jarversion 1.x
Usage: jarversion [options]

  -j <jar1>,<jar2>... | --jars <jar1>,<jar2>...
        jars to include
  --verbose
        be verbose
  --help
        print usage text
Print jar file version number defined as the maximum version of all classes contained within jar file

$ jarversion --jars  "/apps/dev/eclipse-jee-mars-R/eclipse/plugins/com.ibm.icu_54.1.1.v201501272100.jar,/apps/dev/eclipse-jee-mars-R/eclipse/plugins/com.sun.el_2.2.0.v201303151357.jar"

/apps/dev/eclipse-jee-mars-R/eclipse/plugins/com.ibm.icu_54.1.1.v201501272100.jar = ClassVersion(49,0)
/apps/dev/eclipse-jee-mars-R/eclipse/plugins/com.sun.el_2.2.0.v201303151357.jar = ClassVersion(50,0)

Można oczywiście poprawić kilka rzeczy, w szczególności format wyjścia i prezentować informacje, że np. 45.0 to JDK 1.1 a 52.0 to J2SE 8 ale nawet w obecnej postaci aplikacja spełnia swoje zadanie

Lokalizacja (wersje językowe) komunikatów o błędach z wbudowanego w JRE silnika JavaScript-u w OSGi z wykorzystaniem extension:=extclasspath

Aplikacja nad którą pracuję umożliwia użytkownikom rozszerzenia funkcjonalności przez pisanie skryptów w JS (szczegóły nie są istotne). Oczywiście, gdy pisze się kod pojawiają się komunikaty o błędach a wymaganiem klienta było by były w języku polskim. Ponieważ domyślnie dostępne są dwie wersje językowe – angielska i francuska – stworzony został plik sun/org/mozilla/javascript/internal/resources/Messages_pl.properties z polskimi komunikatami który następnie został opakowany w plik jar sun.org.mozilla.javascript.nl.PL_pl.jar. W końcu plik ten został przekazany jako rozszerzenie bootclasspath aplikacji za pomocą opcji

-Xbootclasspath/a:lib\sun.org.mozilla.javascript.nl.PL_pl.jar

i rozwiązanie takie działo przed dłuższy czas (także w OSGi).

W trakcie migracji na Equinox p2 okazało się jednak, że z wielu względow -Xbootclasspath/a musi odejść – a mnie przypadło wymyślenie jak.

Pierwszym krokiem jest oczywiście ustalenie, dlaczego działało dotychczasowe rozwiązanie. Analiza wykazała, że komunikaty wczytywane są w klasie sun.org.mozilla.javascript.internal.ScriptRuntime wywołaniem

ResourceBundle.getBundle("sun.org.mozilla.javascript.internal.resources.Messages", locale)

a oryginalne lokalizacje (angielska i francuska) umieszczone są w <jre>/lib/resources.jar. Dalsza analiza wykazała, że w tym przypadku do wczytywania lokalizacji wykorzystywany jest tzw. classloader aplikacji. Żeby zrozumieć, dlaczego wyszukiwanie lokalizacji powodzi się trzeba pamiętać o trzech podstawowych faktach dotyczących classloader-ów

  • są zorganizowane są w strukturę drzewiastą i powiązane relacją dziecko-rodzic
  • zwykle przed wczytaniem klasy\zasobu samemu następuje próba delegacji do rodzica (parent-first delegation model – ale model child-first też jest popularny. Ważne jest to, że classloader „widzi” klasy i zasoby swoje i przodków – ale nie potomków
  • główne classloadery (boot, ext, app) zorganizowane są następującojvm-classloader-hierarchy

Podczas wyszukiwania zasobu sun/org/mozilla/javascript/internal/resources/Messages_pl.properties Application Classloader deleguje do Extension ClassLoader który z kolei deleguje do Boostrap ClassLoader który znajduje zasób dzięki -Xbootclasspath – i voilà , mamy polskie komunikaty.

Dla osób dobrze znających OSGi naturalnym sposobem na rozwiązanie przyjazne dla OSGi wydają się tzw. framework extensions czyli pakunki częściowe (fragmenty) których hostem jest pakunek systemowy – system.bundle – i które dodatkowo definiują tzw. punkt rozszerzeń. Zgodnie ze specyfikacją OSGi poprawne są dwie wartości

  • framework
  • bootclasspath

opisane np. tutaj i w specyfikacji OSGi
Pierwszy jest dla nas bezużyteczny (pytanie dla uważnych – dlaczego). Drugi wydaje się idealny jako odpowiednik parametru -Xbootclasspath ale w praktyce okazuje się, że frameworki OSGI nie implementują go z przyczyn technicznych (patrz np. Eclipse/127724). Co nam pozostaje? Okazuje się, że Equinox OSGi obsługuje niestandardowy punkt rozszerzeń który idealnie sprawdza się w tym przypadku. Można bowiem stworzyć fragment który rozszerzy classloader aplikacji korzystając ze składni Fragment-Host: system.bundle; extension:=extclasspath. Wystarczy stworzyć fragment z następującym manifestem

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Mozilla Rhino Localization PL
Bundle-SymbolicName: sun.org.mozilla.javascript.nl.PL_pl
Bundle-Version: 1.0.0.qualifier
Fragment-Host: system.bundle; extension:=extclasspath
Bundle-RequiredExecutionEnvironment: JavaSE-1.6

który zawiera zasób sun/org/mozilla/javascript/internal/resources/Messages_pl.properties

Jak Equinox OSGi obsługuje framework extensions

czyli jak mawia Linus Torvalds Talk is cheap. Show me the code. W tym przypadku kluczowe są dwa miejsca. Pierwszym jest metoda org.eclipse.osgi.internal.baseadaptor.BaseStorageHook.loadManifest gdzie wczytywany jest manifest pakunku

static void loadManifest(BaseData target, Dictionary<String, String> manifest) throws BundleException {
	try {
		target.setVersion(Version.parseVersion(manifest.get(Constants.BUNDLE_VERSION)));
	} catch (IllegalArgumentException e) {
		target.setVersion(new InvalidVersion(manifest.get(Constants.BUNDLE_VERSION)));
	}
	ManifestElement[] bsnHeader = ManifestElement.parseHeader(Constants.BUNDLE_SYMBOLICNAME, manifest.get(Constants.BUNDLE_SYMBOLICNAME));
	int bundleType = 0;
	if (bsnHeader != null) {
		target.setSymbolicName(bsnHeader[0].getValue());
		String singleton = bsnHeader[0].getDirective(Constants.SINGLETON_DIRECTIVE);
		if (singleton == null)
			singleton = bsnHeader[0].getAttribute(Constants.SINGLETON_DIRECTIVE);
		if ("true".equals(singleton)) //$NON-NLS-1$
			bundleType |= BundleData.TYPE_SINGLETON;
	}
	// check that the classpath is valid
	String classpath = manifest.get(Constants.BUNDLE_CLASSPATH);
	ManifestElement.parseHeader(Constants.BUNDLE_CLASSPATH, classpath);
	target.setClassPathString(classpath);
	target.setActivator(manifest.get(Constants.BUNDLE_ACTIVATOR));
	String host = manifest.get(Constants.FRAGMENT_HOST);
	if (host != null) {
		bundleType |= BundleData.TYPE_FRAGMENT;
		ManifestElement[] hostElement = ManifestElement.parseHeader(Constants.FRAGMENT_HOST, host);
		if (Constants.getInternalSymbolicName().equals(hostElement[0].getValue()) || Constants.SYSTEM_BUNDLE_SYMBOLICNAME.equals(hostElement[0].getValue())) {
			String extensionType = hostElement[0].getDirective("extension"); //$NON-NLS-1$
			if (extensionType == null || extensionType.equals("framework")) //$NON-NLS-1$
				bundleType |= BundleData.TYPE_FRAMEWORK_EXTENSION;
			else if (extensionType.equals("bootclasspath")) //$NON-NLS-1$
				bundleType |= BundleData.TYPE_BOOTCLASSPATH_EXTENSION;
			else if (extensionType.equals("extclasspath")) //$NON-NLS-1$
				bundleType |= BundleData.TYPE_EXTCLASSPATH_EXTENSION;
		}
	} else {
		String composite = manifest.get(COMPOSITE_HEADER);
		if (composite != null) {
			if (COMPOSITE_BUNDLE.equals(composite))
				bundleType |= BundleData.TYPE_COMPOSITEBUNDLE;
			else
				bundleType |= BundleData.TYPE_SURROGATEBUNDLE;
		}
	}
	target.setType(bundleType);
	target.setExecutionEnvironment(manifest.get(Constants.BUNDLE_REQUIREDEXECUTIONENVIRONMENT));
	target.setDynamicImports(manifest.get(Constants.DYNAMICIMPORT_PACKAGE));
}

drugim jest metoda org.eclipse.osgi.internal.baseadaptor.BaseStorage.processExtension gdzie dane rozszerzenie jest obsługiwane

protected void processExtension(BaseData bundleData, byte type) throws BundleException {
	if ((bundleData.getType() & BundleData.TYPE_FRAMEWORK_EXTENSION) != 0) {
		validateExtension(bundleData);
		processFrameworkExtension(bundleData, type);
	} else if ((bundleData.getType() & BundleData.TYPE_BOOTCLASSPATH_EXTENSION) != 0) {
		validateExtension(bundleData);
		processBootExtension(bundleData, type);
	} else if ((bundleData.getType() & BundleData.TYPE_EXTCLASSPATH_EXTENSION) != 0) {
		validateExtension(bundleData);
		processExtExtension(bundleData, type);
	}
}

OOM w Jetty z powodu zbyt dużej pamięci podręcznej zasobów (org.mortbay.jetty.ResourceCache$Content)

Niedawno zaczęliśmy wykorzystywać Jetty do hostowania repozytoriów P2. Szybko okazało się, że dotychczasowy rozmiar sterty który jak się wydawało w zupełności wystarczał do udostępniania statycznych zasobów (-Xmx256M) okazał się niewystarczający – podczas materializacji produktu P2 z repozytorium proces Jetty kończył się wyjątkiem java.lang.OutOfMemoryError. Doraźnym rozwiązaniem było podwojenie XMX, ale w końcu zrobiony automatycznie zrzut pamięci doczekał się analizy. Analiza to zresztą za dużo powiedziane, bo nigdy jeszcze nie widziałem tak ewidentnej przyczyny – po otwarciu zrzutu pamięci w Eclipse Memory Analyzer (MAT) raport Leak Suspects okazał się aż nadto jednoznaczny.

jetty_oom_leak_suspects

Co się stało? Okazuje się, że domyślne wartości w klasie org.mortbay.jetty.ResourceCache są bardzo konserwatywne

// jetty-server/src/main/java/org/eclipse/jetty/server/ResourceCache.java
    private int _maxCachedFileSize =4*1024*1024;
    private int _maxCachedFiles=2048;
    private int _maxCacheSize =32*1024*1024;

ale te w w bazowej konfiguracji domyślnego servletu już nie

   <init-param>
      <param-name>maxCacheSize</param-name>
      <param-value>256000000</param-value>
    </init-param>
    <init-param>
      <param-name>maxCachedFileSize</param-name>
      <param-value>200000000</param-value>
    </init-param>
    <init-param>
      <param-name>maxCachedFiles</param-name>
      <param-value>2048</param-value>
    </init-param>

Spodziewałbym się tutaj chociaż szczątkowej heurystyki – przeznaczenie 256000000 bajtów na pamięć podręczną gdy całkowity rozmiar stery jest niewiele większy (lub mniejszy) nie jest zbyt rozsądną decyzją. Trzymanie w pamięci 200M plików też wydaje się nieco dziwne. Rozwiązaniem jest ustawienie parametru maxCacheSize w WEB-INF/web.xml na wartość o tyle mniejszą od XMX by wystarczyło pamięci do normalnej pracy aplikacji

    <servlet>
    <servlet-name>default</servlet-name>
    <servlet-class>org.mortbay.jetty.servlet.DefaultServlet</servlet-class>
    <init-param>
      <param-name>maxCacheSize</param-name>
      <!-- 128M - wartość MUSI być mniejsza od XMX -->
      <param-value>134217728</param-value>
    </init-param>
    <init-param>
      <param-name>maxCachedFileSize</param-name>
      <!-- Nie umieszczaj w pamięci podręcznej plików większych niż 32M -->	  
      <param-value>33554432</param-value>
    </init-param>
   </servlet>

Wartość 32M jest empiryczna, przed jej dobraniem sprawdziłem rozmiar największych plików JAR w repozytorium poleceniem (Cygwin is Fun in Terminal)

$ find plugins -name "*.jar"  -type f | xargs ls -lhS | head -n 10

Kto wywołuje System.gc() ?

Każdy, kto miał do czynienia z aplikacjami Javy których rozmiar sterty liczy się w gigabajtach wie, że jedną z rzeczy których należy unikać jak ognia jest tzw. odśmiecanie pełne (Full GC). W większości przypadków można to osiągnąć przez rozważny wybór algorytmu GC i staranne dobranie jego parametrów. Nie rozwiązuje to jednak przypadku, gdy cykl pełnego odśmiecania wyzwalany jest przez jawne wywołanie System.gc() co manifestuje się wpisami w logach procesu odśmiecania zaczynających się od Full GC (System) (HotSpot) lub <sys> (IBM J9).
W przypadku maszyny wirtualnej IBM (IBM J9) w celu wykrycia miejsc skąd pochodzą te wywołania wystarczy dodać odpowiednią opcję do parametrów uruchomieniowych maszyny wirtualnej

-Xtrace:print=mt,methods={java/lang/System.gc},trigger=method{java/lang/System.gc,jstacktrace}

W przypadku maszyny Oracle HotSpot sprawa jest nieco trudniejsza. Początkowo planowałem wykorzystać instrumentalizację przez BCEL ale w końcu zdecydowałem się na wykorzystanie BTrace. W tym celu napisałem prosty próbnik

import com.sun.btrace.annotations.*; 
import static com.sun.btrace.BTraceUtils.*; 
@BTrace class SystemGcCalls {    
	@OnMethod(clazz="java.lang.System", method="gc")    
	public void printStack() {
		Threads.jstack();
		println("");    
	} 
}

i zainstalowałem go dla testu w działającej instalacji Eclipse (o które wiedziałem, że Full GC jest wywoływane bez potrzeby co wyłączyłem, świadom konsekwencji, za pomocą opcji -XX:+DisableExplicitGC).

$ ./btrace <pid> /Users/Jarek/Code/BTrace/SystemGcCalls.java

Wkrótce moim oczom ukazały się stosy wywołań

java.lang.System.gc(System.java)
org.eclipse.ui.internal.ide.application.IDEIdleHelper$3.run(IDEIdleHelper.java:181)
org.eclipse.core.internal.jobs.Worker.run(Worker.java:53)

Okazuje się, że klasa org.eclipse.ui.internal.ide.application.IDEIdleHelper próbuje wykrywać, kiedy IDE jest bezczynne i wywołuje wtedy System.gc(). Po prostu cudownie! Analizując kod klasy IDEIdleHelper okazuje się, że można doprowadzić do tego, że System.gc() nie będzie wywołane przez IDE ustawiając parametr „ide.gc” w eclipse.ini

-Dide.gc=false

Dalsza analiza wskazuje na błędy dotyczące tego zachowania zgłoszone do Eclipse Bugzilla: Bug 118335 i Bug 136855. Jest to kolejny przykład na to, że zakładanie że wie się lepiej od JVM kiedy przeprowadzić odśmiecanie rzadko prowadzi do czegokolwiek dobrego.