Грациозность — это не только замечательное человеческое качество: она также обязательна для любой прикладной программы, особенно когда она ложится тяжелым бременем на критически важные области.
UltraESB имеет хорошую историю поддержания грациозности в течение всего времени работы, включая отключение . Новый UltraESB-X продолжил традицию и осуществил плавное отключение в своем выпуске 17.07 .
Когда мы ips-worker
образ Docker ips-worker
для нашей Интеграционной платформы (IPS) как адаптированную версию UltraESB-X, мы могли гарантировать, что ESB, работающие на платформе, будут корректно завершать работу — или мы так думали.
К сожалению нет.
Как только мы повторно развернем или изменим счетчик репликации кластера, все экземпляры ESB, работающие в кластере, прекратят работу (и новые экземпляры появятся на их месте). Прекращение должно быть изящным; ESB сначала прекращают принимать любые новые входящие сообщения и удерживают внутреннюю последовательность выключения на несколько секунд, пока не завершится обработка сообщений в полете, или тайм-аут не завершит удержание.
В нашем основном выпуске IPS на основе Kubernetes мы извлекаем журналы экземпляров ESB (pods) через API K8s, а также через приложение базы данных, чтобы мы могли проанализировать их позже. Анализируя журналы, мы заметили, что мы никогда не видели журналы выключения ESB, независимо от того, насколько велико хранилище журналов. Казалось, что ESB были жестоко убиты, как только был получен сигнал завершения.
Чтобы исследовать проблему, я начал с упрощенной Java-программы: программы, которая регистрирует хук отключения — всемирно известный способ реализации изящного завершения работы в Java, который мы использовали в обоих наших ESB, — и продолжает работать вечно, печатая некоторый текст периодически (чтобы указать, что main
поток активен). Как только запускается ловушка отключения, я прерывал main
поток, изменял вывод, чтобы указать, что мы выключаемся, и позволяю обработчику завершить работу через несколько секунд (в соответствии с «ложным» изящным отключением).
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
class Kill { private static Thread main; public static void main(String[] a) throws Exception { Runtime.getRuntime().addShutdownHook( new Thread( new Runnable() { public void run() { System.out.println( "TERM" ); main.interrupt(); for ( int i = 0 ; i < 4 ; i++) { System.out.println( "busy" ); try { Thread.sleep( 1000 ); } catch (Exception e) {} } System.out.println( "exit" ); } })); main = Thread.currentThread(); while ( true ) { Thread.sleep( 1000 ); System.out.println( "run" ); } } } |
Тестировать это довольно просто:
1
2
|
javac Kill.java java Kill |
Пока программа продолжает печатать:
1
2
3
4
|
run run run ... |
нажмите Ctrl + C, чтобы увидеть, что происходит:
01
02
03
04
05
06
07
08
09
10
11
12
|
... run run ^CTERM busy Exception in thread "main" java.lang.InterruptedException: sleep interrupted at java.lang.Thread.sleep(Native Method) at Kill.main(Kill.java: 22 ) busy busy busy exit |
Выглядит неплохо.
После этого преобразование в полноценный Docker-контейнер заняло всего несколько минут, а следующий Dockerfile :
1
2
3
|
FROM openjdk: 8 -jre-alpine ADD Kill*. class / ENTRYPOINT [ "java" , "Kill" ] |
docker build -t kill:v1 .
Затем я запустил контейнер с новым изображением:
docker run -it --rm kill:v1
который дал ожидаемый результат:
1
2
3
4
|
run run run ... |
Затем я отправил в TERM
сигнал TERM
(который отображается на Ctrl + C на обычном жаргоне и является триггером по умолчанию для ловушки завершения работы Java) с помощью команды kill
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
# pardon the fancy functions; # they are quite useful for me when dealing with processes function pid() { ps -ef | grep $ 1 | grep -v grep | awk '{print $2}' } function killsig() { for i in pid $ 2 ; do sudo kill $ 1 $i done } alias termit= 'killsig -15' # with all the above in place, I just have to run: termit Kill |
Как и ожидалось, отключающий хук вызывался и выполнялся плавно.
Идя дальше, я превратил все это в отдельный модуль K8s (поддерживаемый развертыванием с одной репликой):
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
apiVersion: extensions/v1beta1 kind: Deployment metadata: name: kill spec: selector: matchLabels: k8s-app: kill template: metadata: labels: k8s-app: kill spec: containers: - name: kill image: kill:v1 |
и попробовал то же самое, на этот раз путем обнуления spec.replicas
(так же, как мы делаем это в IPS) с помощью kubectl edit deployment
вместо ручного kill -TERM
:
1
2
3
4
5
|
kubectl edit deployment kill # vi is my default editor # set "replicas" to 0 (line 20 in my case ) # <ESC>:wq<ENTER> |
имея консольный хвост в отдельном окне:
01
02
03
04
05
06
07
08
09
10
11
12
|
# fancy stuff again function findapp() { kubectl get pod -l k8s-app=$ 1 -oname | cut -b 6 -; } function klog() { kubectl logs -f findapp $ 1 ; } # the final command klog kill |
показывая вывод:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
run run ... run TERM busy Exception in thread "main" java.lang.InterruptedException: sleep interrupted at java.lang.Thread.sleep(Native Method) at Kill.main(Kill.java: 22 ) busy busy busy exit |
Черт, он все еще изящно выключается!
Так что не так с моим ips-worker
?
Просто чтобы проверить, я получил кластер с одной репликой, работающий на IPS, вручную изменил образ ( spec.template.spec.containers[0].image
) и команду запуска ( spec.template.spec.containers[0].command
) о развертывании K8s с помощью kubectl edit
( kubectl edit
всех других факторов — таких как переменные среды и монтирование объема) — и опробовал ту же последовательность обнуления;
Тот же результат! Изящное отключение!
Затем мне пришло в голову, что, хотя мой контейнер kill
просто использует команду ips-worker
java Kill
, ips-worker
использует немного более сложную команду:
/bin/sh -c <copy some files> && <run some custom command> && <run ultraesb-x.sh>
где, в последней части, мы создаем (со специально изготовленным classpath и некоторыми параметрами JVM) и выполняем довольно длинную команду java
которая запускает зверя UltraESB-X.
В конечном итоге, последняя команда live в контейнере сводится к:
/bin/sh -c <basepath>/ultraesb-x.sh
Поэтому я попробовал команду оболочки в моем контейнере kill
, слегка изменив Dockerfile:
слегка изменив Dockerfile:
1
2
3
4
|
FROM openjdk: 8 -jre-alpine ADD Kill*. class / # note the missing brackets and quotes, so that the command gets the default /bin/sh -c prefix ENTRYPOINT java Kill |
и ура! Изящного отключения больше не было. Процесс Java был жестоко убит как в Docker ( docker stop
Docker), так и в K8s (обнуление реплики).
Исследуя далее, я руководствовался Google этим популярным постом SE, в котором говорилось, что оболочка ( sh
) по умолчанию не передает полученные сигналы своим дочерним процессам. Предложенная альтернатива состояла в том, чтобы запустить внутреннюю команду как exec
которая в основном заменила бы родительский процесс ( sh
) на дочерний ( java
, в случае kill
):
1
2
3
|
FROM openjdk: 8 -jre-alpine ADD Kill*. class / ENTRYPOINT exec java Kill |
За kill
это сразу же сработало.
Для ips-worker
все было немного по-другому, так как было два уровня вызова: команда контейнера, вызывающая цепочку команд через /bin/sh -c
, и встроенный ultraesb-x.sh
вызывающий конечную команду java
. Следовательно, мне пришлось включить exec
в двух местах:
Однажды в конце цепочки команд:
1
2
3
4
|
/bin/sh -c \ <copy some files> && \ <run some custom command> && \ exec <basepath>/ultraesb-x.sh |
И снова в конце ultraesb-x.sh
:
1
2
3
|
# do some magic to compose the classpath and other info for ESB startup exec $JAVA_HOME/bin/java <classpath and other params> |
Как бы просто это ни казалось, этих двух exec
было достаточно, чтобы вернуть ips-worker
завершение работы ips-worker
и, следовательно, нашей Интеграционной Платформе .
Ссылка: | Изящное завершение работы Java в контейнерах: почему стоит перепроверить! от нашего партнера JCG Джанака Бандара в Рандомизде | Случайные мысли Сериализованный блог. |