Грациозность — это не только замечательное человеческое качество: она также обязательна для любой прикладной программы, особенно когда она ложится тяжелым бременем на критически важные области.
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.javajava Kill |
Пока программа продолжает печатать:
|
1
2
3
4
|
runrunrun... |
нажмите Ctrl + C, чтобы увидеть, что происходит:
|
01
02
03
04
05
06
07
08
09
10
11
12
|
...runrun^CTERMbusyException in thread "main" java.lang.InterruptedException: sleep interrupted at java.lang.Thread.sleep(Native Method) at Kill.main(Kill.java:22)busybusybusyexit |
Выглядит неплохо.
После этого преобразование в полноценный Docker-контейнер заняло всего несколько минут, а следующий Dockerfile :
|
1
2
3
|
FROM openjdk:8-jre-alpineADD Kill*.class /ENTRYPOINT ["java", "Kill"] |
docker build -t kill:v1 .
Затем я запустил контейнер с новым изображением:
docker run -it --rm kill:v1
который дал ожидаемый результат:
|
1
2
3
4
|
runrunrun... |
Затем я отправил в 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 processesfunction 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/v1beta1kind: Deploymentmetadata: name: killspec: 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 againfunction findapp() { kubectl get pod -l k8s-app=$1 -oname | cut -b 6-;}function klog() { kubectl logs -f findapp $1;}# the final commandklog kill |
показывая вывод:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
|
runrun...runTERMbusyException in thread "main" java.lang.InterruptedException: sleep interrupted at java.lang.Thread.sleep(Native Method) at Kill.main(Kill.java:22)busybusybusyexit |
Черт, он все еще изящно выключается!
Так что не так с моим 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-alpineADD Kill*.class /# note the missing brackets and quotes, so that the command gets the default /bin/sh -c prefixENTRYPOINT java Kill |
и ура! Изящного отключения больше не было. Процесс Java был жестоко убит как в Docker ( docker stop Docker), так и в K8s (обнуление реплики).
Исследуя далее, я руководствовался Google этим популярным постом SE, в котором говорилось, что оболочка ( sh ) по умолчанию не передает полученные сигналы своим дочерним процессам. Предложенная альтернатива состояла в том, чтобы запустить внутреннюю команду как exec которая в основном заменила бы родительский процесс ( sh ) на дочерний ( java , в случае kill ):
|
1
2
3
|
FROM openjdk:8-jre-alpineADD 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 startupexec $JAVA_HOME/bin/java <classpath and other params> |
Как бы просто это ни казалось, этих двух exec было достаточно, чтобы вернуть ips-worker завершение работы ips-worker и, следовательно, нашей Интеграционной Платформе .
| Ссылка: | Изящное завершение работы Java в контейнерах: почему стоит перепроверить! от нашего партнера JCG Джанака Бандара в Рандомизде | Случайные мысли Сериализованный блог. |