Docker Container’ların yaşam döngüleri çok iyi bir biçimde tanımlanmıştır. Docker Daemon, Docker Image’ından bir Container oluşturarak Image içinde tanımlanan uygulamaya (Command) kontrolü bıraktıktan sonra uygulama, kendi isteği ile çıkış yapana kadar çalıştırılmaya devam eder. Container’ın çalışmasına müdahale gerektiğinde, Docker CLI yardımı ile Docker Daemon’a gönderilen komutlar, Container’ın çalışması duraklatabilir (Pause), durdurabilir (Stop) veya yeniden başlatabilir (Restart).

Dockerfile’ını kendi hazırladığınız Container’ları, Docker CLI veya Docker Compose CLI ile durdurmaya çalıştığınızda Container’ın hemen durdurulamadığını fakat 10 saniye sonra ancak durdurulabildiğini bugüne kadar gözlemlemiş olabilirsiniz. Henüz bu duruma rastlamadıysanız, Docker ile gelişen maceralarınızda karşılaşacağınızı garanti edebilirim. Bu blog’da bu davranışın neden sadece bazı Container’larda oluştuğuna ve bu davranışı nasıl değiştirebileceğimizi göreceğiz.

Soru 2

Docker Hub’dan indirdiğiniz Nginx Docker Image’ının içine Dockerfile yardımı ile kendi sitenizdeki dosyaları kopyaladınız ve web sunucuyu başarılı bir şekilde ayağa kaldırdınız. Sitenizdeki yük fazla olduğu için birden fazla Nginx Container ayağa kaldırıyorsunuz ve bu Container’ları bir Load Balancer (Yük Dağıtıcı) arkasına koyarak gelen yükü eşit olarak dağıtıyorsunuz.

Operasyon takımı olarak, ara ara web sunucularda çıkan problemlerin hangi Container’dan kaynaklandığını öğrenmek ve hızlıca bu Container’ları tespit edebilmek üzere web sunucunun IP adresini görebilmek için ip-addr.html adlı bir dosya eklemek istediğinizi varsayalım.

Not: Buradaki senaryonun gerçekten işe yarayabilmesi için Load Balancer’da Sticky Session kullanıldığı varsayılmıştır.

Problemin Gösterimi

Öncelikle tasvir ettiğimiz bu problemi bir örnek ile gösterelim ve kendimiz test edelim.

Demo Ortamının Yaratılması

Bu bölümde problemin gösterimini yapacağımız örnek için gerekli ortamı yaratalım. Önlerinde bir Load Balancer (HAProxy) olan üç adet Nginx web sunucuyu HAProxy ile birlikte Docker Compose yardımıyla ayağa kaldıralım ve çalıştığını test edelim.

Belirtilen adımları siz de takip etmek istiyorsanız bu blog için hazırladığım Github Repository‘sini klonlayıp komut satırında repo içindeki Question-2/NoProblem/Demo klasörüne geçin. Yolumuz düşerse ve beğendiysek Github’da Star vermeyi ihmal etmeyelim :-)

Bu klasörde içeriği aşağıda verilen, web, curl ve lb adlı üç servis içeren Docker Compose dosyasının yanında,

  • Nginx Image’ını özelleştirdiğimiz Dockerfile-Nginx dosyası
  • HAProxy’nin konfigürasyonunu belirlediğimiz haproxy.cfg dosyası
  • Nginx’te sunacağımız basit web sitesini içeren site klasörü
  • Docker Compose komutlarını bir araya toplayarak işimizi kolaylaştıracak Makefile dosyası

ile karşılaşacaksınız.

version: "2"

services:
  web:
    build:
      context: .
      dockerfile: ./Dockerfile-Nginx
    
  curl:
    image: byrnedo/alpine-curl

  lb:
    image: haproxy:1.7.1
    volumes:
      - "./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg"
    ports:
      - "8080:8080"

Komut satırında make up komutunu vererek servisleri çalıştırın. Komut satırında işlemler tamamlandıktan sonra make ps komutunu vererek servislerin çalışıp çalışmadığını kontrol edebilirsiniz. Aşağıdakine benzer bir görüntü oluşmalı.

Gördüğünüz gibi HAProxy (demo_lb_1 isimli Container’a sahip olan lb servisi) 8080 numaralı portta istekleri bekliyor.

Demo $ make ps
docker-compose ps
   Name                 Command               State           Ports
----------------------------------------------------------------------------
demo_lb_1    /docker-entrypoint.sh hapr ...   Up      0.0.0.0:8080->8080/tcp
demo_web_1   nginx -g daemon off;             Up      443/tcp, 80/tcp
demo_web_2   nginx -g daemon off;             Up      443/tcp, 80/tcp
demo_web_3   nginx -g daemon off;             Up      443/tcp, 80/tcp

Tarayıcınızın adres çubuğuna http://localhost:8080 adresini girerek aşağıdaki gibi bir çıktı elde etmelisiniz.

Company Page in Browser

Eğer bu testi (Load Balancer’ın sayfaları verip vermediği testini) komut satırından yapmak isterseniz docker-compose.yml dosyasında gördüğünüz curl servisini aşağıdaki gibi çağırabilirsiniz.

Demo $ docker-compose run --rm curl http://lb:8080
<html>
    <head>
        <title>Company Site</title>
    </head>
    <body>
        <h1 style="text-align: center; color: red">Welcome to Company Page</h1>
        <h2 style="text-align: center; color: blue">You will find useful info here</h2>
    </body>
</html>
Problemi Göstermeden Bir Öncesi: Problem Olmayan Durum

Problemi görmek için biraz daha beklememiz gerekecek :-) Öncelikle henüz bozmamışken doğru davranışın ne olduğunu görelim.

Docker Compose ile başlattığımız servisleri durdurmak istediğimizi farz edelim. Makefile‘ımızda bunun için make clean komutunu kullanabiliriz. Komut satırından time make clean komutunu verdiğinizde işlemin 2.2 saniyede bittiğini gözlemleyebilirsiniz. Gördüğünüz gibi öncelikle servislerin içindeki Container’lar Stop edilmeye çalışılıyor ve sonrasında da Remove ediliyor.

Not: make clean komutunun başına time komutunu vermek make clean komutunun tamamlanma süresi ile ilgili bilgi almamızı sağladı.

Demo $ time make clean
docker-compose down -v
Stopping demo_lb_1 ... done
Stopping demo_web_3 ... done
Stopping demo_web_2 ... done
Stopping demo_web_1 ... done
Removing demo_lb_1 ... done
Removing demo_web_3 ... done
Removing demo_web_2 ... done
Removing demo_web_1 ... done
Removing network demo_default

real	0m2.201s
user	0m0.239s
sys	0m0.051s
Ve Nihayet Problemin Gösterimi

Buraya kadar hiçbir problem yoktu her şey istediğimiz şekilde ilerledi. Şimdi ise Nginx web sunuculara, sunucunun IP’sini gösterecek olan ip-addr.html sayfayı eklemeye çalışalım.

Adımları kendiniz de takip ediyorsanız komut satırında repo içindeki Question-2/Problem/Demo klasörüne geçin. Nginx Container’ın IP adresini Container ayağa kalkmadan öğrenemeyeceğimiz için web sunucunun altına koyacağımız ip-addr.html dosyasını Dockerfile‘da üretemeyiz. Aynı şekilde bu dosyayı Docker Container’ları koşturan Host’tan da Volume direktifi ile Mount edemeyiz. Yapmamız gereken şey Nginx Image’ının Command’ını bir Bash Script ile değiştirmek ve Bash Script’in içerisinde Nginx’i başlatmadan ip-addr.html dosyasını IP bilgisini alarak oluşturup sonra Nginx’i başlatmaktır. Aşağıdaki Bash Script’i bu işi yapmak üzere kullanılabilir.

Not: IP adresini bulmak için komut satırına Google’ın DNS sunucusuna erişmek için hangi Route’u kullanacağını soruyoruz ve buradan kendi IP adresimizi çekiyoruz.

#!/bin/bash -u

ip_addr=`ip -4 route get 8.8.8.8 | awk {'print $7'} | tr -d '\n'`;

echo "<center><h2>Serving Web from IP address: ${ip_addr}</h2></center>" > /usr/share/nginx/html/ip-addr.html

nginx -g 'daemon off;'

Question-2/Problem/Demo klasöründe make up komutunu verin ve servislerin ayağa kalkmasını bekleyin. Sonra IP adresinin doğru gelip gelmediğini test etmek için tarayıcınızın adres çubuğuna http://localhost:8080/ip-addr.html adresini girerek aşağıdaki gibi bir çıktı elde etmelisiniz. Sayfayı birkaç kez yenileyerek IP adresinin değiştiğini görebilirsiniz.

Nginx IP Address

Sonunda geldik problemin gösterimine :-) Servisleri durdurmak için time make clean komutunu verin, aşağıdaki gibi bir çıktı elde etmeniz gerekir.

Bir önceki denemenin aksine bu kez servislerin durdurulma işleminin tamamlanması 2.2 saniye yerine 12 saniye sürdü. Şimdi aradaki 10 saniyelik farkın nereden kaynaklandığını anlamaya çalışalım.

Demo $ time make clean
docker-compose down -v
Stopping demo_lb_1 ... done
Stopping demo_web_2 ... done
Stopping demo_web_3 ... done
Stopping demo_web_1 ... done
Removing demo_lb_1 ... done
Removing demo_web_2 ... done
Removing demo_web_3 ... done
Removing demo_web_1 ... done
Removing network demo_default

real	0m12.027s
user	0m0.286s
sys	0m0.067s

Problemin gösteriminin ikinci şekli muhtemelen daha çok karşılaşılan şeklidir. Docker CLI veya Docker Compose CLI ile bir Container’ı terminaline Attach olarak başlattıktan sonra Ctrl+C ile durdurmak istediğimizde bazen Container’ın hemen durmadığını ve 10 saniyelik bir gecikme ile durduğunu fark ederiz. Şimdi bunu örnekleyelim. Question-2/Problem/Demo klasöründe iken sadece web servisinden bir Container’ı Docker Compose ile başlatın ve sonra Ctrl+C ile durdurmaya çalışın, davranışı gözlemleyebilmelisiniz.

Demo $ docker-compose up web
Creating network "demo_default" with the default driver
Building web
Step 1/2 : FROM nginx:1.10
 ---> 5acd1b9bc321
Step 2/2 : COPY ./site /usr/share/nginx/html/
 ---> 4fc3746cd5b4
Removing intermediate container 94e4397ad538
Successfully built 4fc3746cd5b4
WARNING: Image for service web was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Creating demo_web_1
Attaching to demo_web_1
^CGracefully stopping... (press Ctrl+C again to force)
Stopping demo_web_1 ... done

Bir sonraki test için servisleri temizlemek için make clean komutunu verin.

Problemin Kök Sebebi

Yaptıklarımızı kısaca özetlemek gerekirse, Docker Image’ının Command’ını değiştirdik ve nginx programını bir script init-wrapper.sh içinden çalıştırdık. Bu değişiklik bize çalıştırdığımız Docker Container’ı durdurmaya çalışırken 10 saniyelik bir gecikme olarak yansıdı.

Problemin neden kaynaklandığını anlamak için Question-2/NoProblem/Demo klasörüne (evet problem olmayan demo’nun olduğu klasöre) geçin. make up komutunu vererek servisleri ayağa kaldırın. docker-compose exec web /bin/bash komutunu vererek Nginx Container’larının birinin içine bağlanın. Container’ın içinde ise ps auxww komutunu verin, aşağıdakine benzer bir çıktı elde etmeniz gerekir.

Demo $ docker-compose exec web /bin/bash
root@93edb2b11c22:/# ps auxww
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.1  0.2  31684  5104 ?        Ss   21:32   0:00 nginx: master process nginx -g daemon off;
nginx        5  0.0  0.1  32068  2924 ?        S    21:32   0:00 nginx: worker process
root         6  0.3  0.1  20244  3084 ?        Ss   21:32   0:00 /bin/bash
root        11  0.0  0.1  17500  2152 ?        R+   21:33   0:00 ps auxww

Yukarıdaki çıktıdan görebileceğiniz gibi nginx -g 'daemon off;', PID 1 ile çalıştırılmış. Bu bilgiyi not ederek bir de problemli durumdaki aynı çıktıya bakalım. make clean komutunu vererek bu kez Question-2/Problem/Demo klasörüne geçin. Bu klasörde önce make up, sonra docker-compose exec web /bin/bash ve en sonra da ps auxww komutunu verin.

Demo $ docker-compose exec web /bin/bash
root@bbdf34004077:/# ps auxww
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.1  20048  2756 ?        Ss   21:36   0:00 /bin/bash -u /script/init-wrapper.sh
root         9  0.0  0.2  31684  5068 ?        S    21:36   0:00 nginx: master process nginx -g daemon off;
nginx       10  0.0  0.1  32068  3032 ?        S    21:36   0:00 nginx: worker process
root        11  0.5  0.1  20244  2988 ?        Ss   21:37   0:00 /bin/bash
root        15  0.0  0.1  17500  2056 ?        R+   21:37   0:00 ps auxww

Yukarıdaki çıktıda bu kez nginx -g 'daemon off;'‘un PID 9 ile çalıştırıldığını. PID 1’in ise Docker Image’ına verdiğimiz yeni Command yani /bin/bash -u /script/init-wrapper.sh olduğunu görüyoruz.

Bu iki testten yapmamız gereken ilk çıkarım, Docker’ın Image Command’lerini PID 1 (yani init Process) olarak başlatmasıdır.

Docker CLI’dan bir Docker Container için stop komutu verildiğinde Docker Engine, ilgili Container’ın init Process’ine SIGTERM sinyali göndermekte 10 saniye (ayarlanabilir bir değer) beklemekte ve sonrasında eğer Container kendi isteği ile çıkış yapmazsa yine init Process’ine bu kez SIGKILL sinyalini göndermekte ve Container’ı sonlandırmaktadır. Docker Daemon, init Process’e SIGTERM gönderdikten sonra 10 saniye bekleyerek Container’ın (varsa) halihazırda yaptığı bir işi sonlandırmasını beklemektedir. Bu davranış geleneksel Linux/Unix sistem mimarisi ile uyumlu bir davranıştır. Linux/Unix’te bir Process veya tüm sistem kapatılmak istendiğinde Process’lere SIGTERM sinyali gönderilir ve 5-10 saniye boyunca kendilerinin çıkış yapması beklenir, Process’ler çıkış yapmazsa SIGKILL veya sistem kapatılması suretiyle sonlandırılırlar.

Nginx’i bir Bash Script ile başlattığımızda SIGTERM sinyali, init Process’i olan bash Process’ine gönderilmekte ve sinyal Nginx’e aktarılmadığı için Nginx çalışmaya devam etmektedir.

Bir sonraki test için servisleri temizlemek için make clean komutunu verin.

Cevap 2

Soruyu, ziyade detayı ile ortaya koyduktan sonra şimdi iki cevap alternatifi ile birlikte konuşabiliriz.

Alternatif 1

En az zahmetle ve yukarıda verilen problemi en doğru şekilde çözmemize olanak tanıyacak yöntem, bash Process’i yerine nginx Process’ini PID 1 yani init Process’i yapmaktır. init-wrapper.sh dosyasında nginx‘in başlatılma şeklini aşağıdaki gibi değiştirirsek yani başına exec komutunu eklersek nginx -g 'daemon off;', PID 1 olarak çalıştırılacak ve Container doğru bir şekilde Stop edilebilecektir. exec komutu Linux/Unix işletim sistemlerinde o anda koşturulmakta olan Process’in Image’ını kendisine parametre olarak verilen programın Image’ı ile değiştirir. Dolayısıyla bizim durumumuzda, Docker Daemon’ın init Process olarak başlattığı /bin/bash -u /script/init-wrapper.sh Process’i nginx -g 'daemon off;' tarafından ezilecek ve yeni init Process olacaktır.

exec nginx -g 'daemon off;'

Öncelikle çözümü test etmek için make up komutunu vererek servisleri ayağa kaldırın. Servisleri durdurmak için time make clean komutunu verin. Gördüğümüz gibi servislerin durdurulması toplam 2.3 saniye sürdü yani gerçekten de problem çözüldü.

Demo $ time make clean
docker-compose down -v
Stopping demo_lb_1 ... done
Stopping demo_web_3 ... done
Stopping demo_web_2 ... done
Stopping demo_web_1 ... done
Removing demo_lb_1 ... done
Removing demo_web_3 ... done
Removing demo_web_2 ... done
Removing demo_web_1 ... done
Removing network demo_default

real	0m2.308s
user	0m0.238s
sys	0m0.058s

Alternatif 1’in Artıları ve Eksileri

Bu alternatifin ilk artısı gördüğünüz gibi çok kolay adapte edilebilmesi ve problemimizi herhangi bir eksiklik bırakmadan tam anlamı ile çözmesidir.

Bizim örneklediğimiz durumda bu alternatifin kullanılabilmesine olanak tanıyan şey, init-wrapper.sh Script’inin bazı hazırlıklar yapıp kontrolü nginx Process’ine devredilmesidir. Eğer nginx Process’i başlatıldıktan sonra Script içinde başka bazı işlemler yapmamız gerekseydi bu yöntemi kullanamayacaktık çünkü init-wrapper.sh Script’imiz exec nginx -g 'daemon off;' çağrıldığı noktadan sonraki kısımları işletemeyecektir (Replace edildiği için).

Bu durumda aynı Docker Container içerisinde iki adet Process başlatmamız gerekirse bu yöntemi kullanamayacağımız açıktır.

Alternatif 2

İkinci alternatif tahmin edebildiğiniz gibi Bash Process’inin kendisine gelen sinyalleri başlattığı Child (çocuk) Process’lere iletmesidir. Bu durumda yapmamız gereken init-wrapper.sh Script’inde, başlatılan Child Process’lerin ID’lerini saklamak ve SIGTERM sinyallerini dinleyerek SIGTERM sinyali geldiğinde bu Process’lere iletmektir. Aşağıda güncellenen init-wrapper.sh Script’i tam olarak bunu yapmaktadır.

#!/bin/bash -u

child_pid=

# handler for SIGTERM
term_handler() {
  echo "SIGTERM signal is caught!";
  kill -TERM "$child_pid" 2>/dev/null;
}

trap term_handler SIGTERM;

ip_addr=`ip -4 route get 8.8.8.8 | awk {'print $7'} | tr -d '\n'`;

echo "<center><h2>Serving Web from IP address: ${ip_addr}</h2></center>" > /usr/share/nginx/html/ip-addr.html;

# start the process
nginx -g 'daemon off;' &

# save the child process id
child_pid=$! 

# wait for it until exit
wait "$child_pid"

Çözümü test etmek için make up komutunu vererek servisleri ayağa kaldırın. Servisleri durdurmak için time make clean komutunu verin. Gördüğümüz gibi servislerin durdurulması yine toplam 2.3 saniye sürdü yani yine gerçekten de problem çözüldü.

Demo $ time make clean
docker-compose down -v
Stopping demo_lb_1 ... done
Stopping demo_web_3 ... done
Stopping demo_web_2 ... done
Stopping demo_web_1 ... done
Removing demo_lb_1 ... done
Removing demo_web_3 ... done
Removing demo_web_2 ... done
Removing demo_web_1 ... done
Removing network demo_default

real	0m2.324s
user	0m0.231s
sys	0m0.055s

Alternatif 2’nin Artıları ve Eksileri

Alternatif 2, alternatif 1’de belirtilen sadece tek Process başlatabilme kısıtına sahip değildir. Bir Docker Container içerisinde birden fazla Process başlatılabilir ve bu Process’ler istendiğinde durdurulabilir.

Yukarıda verilen artısının yanında bu yöntem bir öncekine göre biraz daha kompleks’tir.

Kapanış

Bu blog’daki yer verilen problemin çözümünü tamamıyla anlamak için bir miktar Linux ve Bash bilgisi gerekse de verilen çözümler, yeterli Linux ve Bash bilgisi olmadan da rahatlıkla uygulanabilir. Bir sonraki blog’da buluşmak üzere.