WIADOMOŚCI

Naprawa narzędzia

Published on:21 / December / 2015

Gdy Java nie działan

Większość platformy obsługi wiadomości i usług sieciowych firmy Infobip napisana jest w języku Java. Ta powszechnie stosowana technologia zapewnia stabilność i łatwość programowania, ze względu na liczne biblioteki pomocnicze oraz dobre narzędzia do do debugowania i profilowania, jednak czasami znajdzie się tak irytujący defekt, że aż nie chce się wierzyć w jego istnienie. A on istnieje: w Linuksie nie widać nazwy wątku Java podczas przeglądania procesów natywnych. Ponieważ wątki Java mapują jeden do jednego wątki w systemie natywnym, byłoby dobrze, gdyby można po prostu przejść na górę i zobaczyć, jaki wątek Java najbardziej w danym momencie używa procesora, bez konieczności uruchomienia narzędzia do debugowania. Co jeszcze bardziej irytujące, kiedyś działało to w Oracle JRockit 6 i jakoś zagubiło się przy przejściu do OpenJDK 7 i 8. A najgorsze dla dumnego użytkownika Linuksa jest to, że w Windowsie działa!

Co w takim razie ma zrobić przeciętny koder Java? W konfrontacji z chłopakami z Oracle i ich oprogramowaniem można stracić rezon, łatwo się poddać i żyć z tym. Można też zakwestionować istniejący stan rzeczy, wysilić się do granic możliwości i dalej próbować rozwiązać problem. W Infobip pracują właśnie tacy ludzie.

Zrób własną Javę

Krótkie dochodzenie ujawniło, że istnieje rozszerzenie GNU do API POSIX , służące do ustawienia nazwy wątku w Linuksie, o nazwie pthread_setname_np. Z dokumentacji wynika, że Linux ogranicza nazwy procesów (opisy, nazwy poleceń, jak by nie nazwać) w /proc/[pid]/comm do 15 znaków. Narzędzia Unix, takie jak ps i top, stosują to ograniczenie. Zobaczmy, jak to działa:


$ ps | grep $$ # $$ is current PID
7880 pts/3 00:00:00 bash
$ echo -n "Hi mum, look at me!" > /proc/$$/comm
$ ps | grep $$
7880 pts/3 00:00:00 Hi mum, look at me!

U góry można nacisnąć klawisz „c”, aby przełączyć między wierszem polecenia a wyświetleniem nazwy polecenia.

Teraz tylko zmuśmy Javę, żeby użyła tego API do ustawienia natywnej nazwy wątku. Łatwo powiedzieć, co? OpenJDK to narzędzie z otwartego źródła, więc pobierzmy najpierw źródła:

$ hg clone http://hg.openjdk.java.net/jdk8u/jdk8u60
$ cd jdk8u60
$ sh get_source.sh

Spróbujmy ustalić, gdzie java.lang.Thread.setName(String arg0) się kończy. Trzeba w tym celu odświeżyć zapomnianą umiejętność czytania C++, ale dość łatwo można znaleźć ten kawałek kodu:

hotspot/src/os/linux/vm/os_linux.cpp
void os::set_native_thread_name(const char *name) {
// Not yet implemented.
return;
}
 
bool os::distribute_processes(uint length, uint* distribution) {
// Not yet implemented.
return false;
}
 
bool os::bind_to_processor(uint processor_id) {
// Not yet implemented.
return false;
}

Przy okazji, tutaj można też zobaczyć, że ustawienie koligacji procesorów na pewno nie zadziała, ale nie będę się tym zajmować, bo materiału wystarczyłoby na jeszcze jeden wpis na blogu.

Jeśli nie czujesz się swobodnie modyfikując kod JVM, nie jesteś sam(a) – ktoś już to wcześniej zrobił. W 2011 roku otwarto zgłoszenie dotyczące tej poprawki wraz z działającym rozwiązaniem, pod którym rozwinęła się długa i dość akademicka dyskusja prowadząca do otwarcia kilku kolejnych zgłoszeń z prośbą do glibc o zlikwidowanie ograniczenia do 15 znaków. Tak się nie stanie, bo ograniczenie to jest głęboko zakodowane w jądrze Linuksa. Wygląda na to, że poprawka została uwzględniona w wersji Java 9, ale dla niecierpliwych tutaj podajemy łącze do łaty.

Applying the patch
jdku60$ patch -d hotspot -p1 <../patches/thread_name.cpp.patch

Budowanie OpenJDK jest zaskakująco łatwe. Java 9 przeszła na cmake, ale w wersji Java 8 trzeba będzie sobie radzić z narzędziami autotools:

$ sh conigure.sh 
# <do whatever the script tells you to do>
$ make

Powstały plik build/linux-x86_64-normal-server-release/hotspot/dist/jre/lib/amd64/server/libjvm.so można wykorzystać jako zamiennik tego samego pliku w standardowej dystrybucji JVM. Teraz wypróbuj to magiczne rozwiązanie:

ThreadNameTest.java
public class ThreadNameTest {
    public static void main(String[] args) throws Exception {
        Thread.currentThread().setName("Hi mum, look at me!");
        Thread.sleep(100_000);
    }
}
Java with thread name support
$ javac ThreadNameTest.java
$ java ThreadNameTest &
[1] 19391
$ for PID in `ls /proc/$!/task/`; do echo $PID - `cat /proc/$!/task/$PID/comm`; done
19563 - java
19564 - Hi mum, look at
19565 - java
19566 - java
19567 - java
19568 - java
19569 - java
19570 - java
19571 - java
19572 - java
19573 - java
19574 - Reference Handl
19575 - Finalizer
19576 - Signal Dispatch
19577 - C2 CompilerThre
19578 - C2 CompilerThre
19579 - C2 CompilerThre
19580 - C1 CompilerThre
19581 - Service Thread
19582 - java
$ kill -s 15 $!
[1]+  Exit 143                java ThreadNameTest
Vanilla Java
$ javac ThreadNameTest.java
$ java ThreadNameTest &
[1] 19391
$ for PID in `ls /proc/$!/task/`; do echo $PID - `cat /proc/$!/task/$PID/comm`; done
19563 - java
19564 - java
19565 - java
19566 - java
19567 - java
19568 - java
19569 - java
19570 - java
19571 - java
19572 - java
19573 - java
19574 - java
19575 - java
19576 - java
19577 - java
19578 - java
19579 - java
19580 - java
19581 - java
19582 - java
$ kill -s 15 $!
[1]+  Exit 143                java ThreadNameTest

(Autor: Milan Mimica, inżynier oprogramowania / lider zespołu)