Данная история с одной стороны поучительная и показывает почему внимательность к мелочам важна, а с другой стороны
обучающая и показывает как подходить к решению задач. Началось все с того, что при донастройке nginx в dev окружении,
когда я проводил эксперименты, были замечены ответы от сервера с кодами 5xx. То HLS чанк не отдался с первого раза,
то плейлист не загрузился. Вроде как на продакшне проблемы не наблюдаются… Но я решил закопаться глубже. Забегая
вперед отмечу, что эта придирка позволила обнаружить некорректное поведение на всех серверах где был выключен IPv6,
но часть сервисов использующих glibc
для резолвинга адресов по прежнему пыталась достучаться по IPv6.
Немного про архитектуру: на том же сервере где стоял nginx приствует сервис и nginx репроксирует запросы на него.
Сервис в одном экземпляре и в списке upstream
‘ов он один. nginx кэширует ответы и с помощью модуля
auth проверяет разрешения на просмотр. Именно из-за
активного кэширования на продакшне было сложно словить эту ошибку.
Упрощенная конфигурация nginx приведена ниже. Я убрал HLS-specific вещи и оставил минимальный набор.
worker_processes 1;
error_log /var/log/nginx/error.log debug;
events {
worker_connections 1024;
}
http {
upstream nginx-controller {
server localhost:8888 max_fails=0;
}
server {
listen 80;
location / {
proxy_method 'GET';
proxy_intercept_errors on;
proxy_next_upstream 'off';
proxy_connect_timeout '1s';
proxy_pass http://nginx-controller;
}
}
}
Демо сервисом будет обычный netcat.
$ while true ; do nc -l -4 -p 8888 -c 'echo -e "HTTP/1.1 200 OK\n\n"'; done
Netcat будет слушать порт 8888, используя только ipv4 интерфейс. Убедиться в этом можно с помощью curl:
$ curl -i -g -6 "http://[::1]:8888/"
curl: (7) Failed connect to ::1:8888; Connection refused
$ curl -i "http://localhost:8888"
HTTP/1.1 200 OK
Теперь же попробуем обратиться через nginx.
[root@spotter_edge /]# curl -I -s "http://localhost" | head -1
HTTP/1.1 200 OK
[root@spotter_edge /]# curl -I -s "http://localhost" | head -1
HTTP/1.1 502 Bad Gateway
[root@spotter_edge /]# curl -I -s "http://localhost" | head -1
HTTP/1.1 200 OK
[root@spotter_edge /]# curl -I -s "http://localhost" | head -1
HTTP/1.1 502 Bad Gateway
Демо пример специально подготовлен для явной демонстрации поведения что я наблюдал. С реальным конфигом заметить было сложнее и на протяжении месяца в продакшне не было замечено подобных отказов. Они были, но в меньшем обьеме и трактовались как проблемы с доставляемости контента до конкретного клиента.
Первым делом я обратился к логам nginx и там обнаружил такие записи
2018/03/20 10:11:49 [error] 4130#4130: *11 connect() failed (111: Connection refused) while connecting to upstream,
client: 127.0.0.1, server: , request: "GET / HTTP/1.1", upstream: "http://[::1]:8888/" , host: "localhost"
Запись upstream: "http://[::1]:8888/"
намекает, что использовался IPv6, который netcat
не слушал, как и продакшн
сервис. Но на хосте выключен IPv6!
В этом можно убедиться посмотрев параметры ядра и вывод ifconfig
.
$ sysctl net.ipv6.conf.all.disable_ipv6 net.ipv6.conf.default.disable_ipv6
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
$ ifconfig -a
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.101.5 netmask 255.255.255.0 broadcast 0.0.0.0
ether 02:42:c0:a8:65:05 txqueuelen 0 (Ethernet)
RX packets 15789549 bytes 2553533549 (2.3 GiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 9783999 bytes 1318627420 (1.2 GiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
loop txqueuelen 1 (Local Loopback)
RX packets 606047 bytes 67810892 (64.6 MiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 606047 bytes 67810892 (64.6 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
Будь бы IPv6 включен, ifconfig показал бы, что интерфейсы слушают и IPv6 (наличие строки inet6
в выводе):
$ ifconfig -a
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.101.5 netmask 255.255.255.0 broadcast 0.0.0.0
inet6 fe80::42:c0ff:fea8:6505 prefixlen 64 scopeid 0x20<link>
ether 02:42:c0:a8:65:05 txqueuelen 0 (Ethernet)
RX packets 15787641 bytes 2553216408 (2.3 GiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 9782965 bytes 1318487919 (1.2 GiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10<host>
loop txqueuelen 1 (Local Loopback)
RX packets 605949 bytes 67799887 (64.6 MiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 605949 bytes 67799887 (64.6 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
Когда я сталкиваюсь с непонятным поведением, то сразу начинаю яростно strace
‘ить. Так и сделал в этот раз.
Чтобы не цепляться к процессу nginx я его остановил и запустил их в паре.
$ nginx -s stop
$ strace -t -f nginx -g 'daemon off;'
Важно! strace должен запускаться с опцией -f
, в противном случае не увидим вывода worker’а nginx. Вот вырезка из
man:
-f Trace child processes as they are created by currently traced processes as a result of the fork(2), vfork(2) and clone(2) system calls. Note that -p PID -f will attach all threads of process PID if it is multi-threaded, not only thread with thread_id = PID.
В выводе сразу видны
10:24:47 open("/etc/host.conf", O_RDONLY|O_CLOEXEC) = 5
10:24:47 fstat(5, {st_mode=S_IFREG|0644, st_size=9, ...}) = 0
10:24:47 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f94b68fb000
10:24:47 read(5, "multi on\n", 4096) = 9
10:24:47 read(5, "", 4096) = 0
10:24:47 close(5) = 0
10:24:47 open("/etc/resolv.conf", O_RDONLY|O_CLOEXEC) = 5
10:24:47 fstat(5, {st_mode=S_IFREG|0644, st_size=38, ...}) = 0
10:24:47 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f94b68fb000
10:24:47 read(5, "nameserver 127.0.0.11\noptions nd"..., 4096) = 38
10:24:47 read(5, "", 4096) = 0
10:24:47 close(5) = 0
10:24:47 open("/etc/hosts", O_RDONLY|O_CLOEXEC) = 5
10:24:47 fstat(5, {st_mode=S_IFREG|0644, st_size=202, ...}) = 0
10:24:47 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f94b68fb000
10:24:47 read(5, "127.0.0.1\tlocalhost\n::1\tlocalhos"..., 4096) = 202
10:24:47 read(5, "", 4096) = 0
10:24:47 close(5) = 0
10:24:47 open("/etc/gai.conf", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
10:24:47 futex(0x7f94b5320420, FUTEX_WAKE_PRIVATE, 2147483647) = 0
10:24:47 socket(AF_INET, SOCK_DGRAM, IPPROTO_IP) = 5
10:24:47 connect(5, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
10:24:47 getsockname(5, {sa_family=AF_INET, sin_port=htons(52285), sin_addr=inet_addr("127.0.0.1")}, [16]) = 0
10:24:47 close(5) = 0
10:24:47 socket(AF_INET6, SOCK_DGRAM, IPPROTO_IP) = 5
10:24:47 connect(5, {sa_family=AF_INET6, sin6_port=htons(0), inet_pton(AF_INET6, "::1", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, 28) = 0
10:24:47 getsockname(5, {sa_family=AF_INET6, sin6_port=htons(57409), inet_pton(AF_INET6, "::1", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, [28]) = 0
10:24:47 close(5) = 0