Больше месяца потребовалось, чтобы завершить изучение книги Linux Firewalls. Впечатления от неё остались смешанные, хотя и в общем положительные.
В первую очередь необходимо отметить, что, в отличие от многих других книг на схожую тематику, данная книга уделяет много внимания именно работе с утилитами
Несмотря на то, что настройка фаервола красной нитью проходит через всю книгу, около половины страниц посвящены общим вопросам защиты Linux-систем: наблюдению за сетевой активностью, реагированию на инциденты, отслеживанию изменений файловой системы и иным мерам обеспечения безопасности. Все эти темы, безусловно, важны, однако исходя из названия книги я скорее ожидал обсуждения сетевого стека Linux, нежели поверхностного затрагивания смежных тем. Более того, чтобы составить полную картину взаимодействия сетевого пакета с фаерволом пришлось искать другие источники, и из найденных хочется выделить следующие три:
1. отличное описание архитектуры netfilter;
2. схема прохождения цепочек правил сетевым пакетом;
3. иной взгляд на ту же схему.
Вместе с приведёнными ссылками и man iptables книга становится отличным введением в тему настройки сетевых экранов.
Как обычно, дальше оставляю выбранные мысли из книги.
1. IP-пакет может иметь адрес 0.0.0.0 только в качестве адреса отправителя в протоколе DHCP.
2. Фаервол должен собирать весь сетевой пакет перед его фильтрацией. В противном случае правила, опирающиеся на значения из заголовков следующего протокола, можно обойти, отправляя достаточно мелкие IP-пакеты - фаервол не сможет принять решение для первого пакета, не содержащего нужных полей, и может пропустить весь поток.
3. Использование модуля
4. Модуль
5. Обойти механизм
6. Механизм
7. TCP-соединение может отклоняться как отправкой TCP RST ACK, так и отправкой ICMP-ответа. При этом ICMP-пакет будет содержать в теле заголовок TCP-пакета, на который он отвечает - так получатель поймёт, к какому TCP-потоку относится этот ICMP-пакет.
8. Трафик
9. Цепочка
10. В отличие от
11. Шифрование, выполняемое на сетевом уровне, обычно несовместимо с NAT.
12. При настройке фаервола стоит сразу запускать утилиты с ключами, отключающими разрешение DNS-имён. В противном случае утилиты могут зависать в ожидании ответа DNS-сервера, доступ к которому ещё запрещён.
13.
В первую очередь необходимо отметить, что, в отличие от многих других книг на схожую тематику, данная книга уделяет много внимания именно работе с утилитами
iptables
и nftables
, подробно описывая правила построения команд - именно этого, как мне кажется, не достаёт официальной документации. Автор довольно подробно описывает синтаксис и принципы работы утилит и лежащего в их основе сетевого фильтра netfilter
- за этим я и обращался к книге.Несмотря на то, что настройка фаервола красной нитью проходит через всю книгу, около половины страниц посвящены общим вопросам защиты Linux-систем: наблюдению за сетевой активностью, реагированию на инциденты, отслеживанию изменений файловой системы и иным мерам обеспечения безопасности. Все эти темы, безусловно, важны, однако исходя из названия книги я скорее ожидал обсуждения сетевого стека Linux, нежели поверхностного затрагивания смежных тем. Более того, чтобы составить полную картину взаимодействия сетевого пакета с фаерволом пришлось искать другие источники, и из найденных хочется выделить следующие три:
1. отличное описание архитектуры netfilter;
2. схема прохождения цепочек правил сетевым пакетом;
3. иной взгляд на ту же схему.
Вместе с приведёнными ссылками и man iptables книга становится отличным введением в тему настройки сетевых экранов.
Как обычно, дальше оставляю выбранные мысли из книги.
1. IP-пакет может иметь адрес 0.0.0.0 только в качестве адреса отправителя в протоколе DHCP.
2. Фаервол должен собирать весь сетевой пакет перед его фильтрацией. В противном случае правила, опирающиеся на значения из заголовков следующего протокола, можно обойти, отправляя достаточно мелкие IP-пакеты - фаервол не сможет принять решение для первого пакета, не содержащего нужных полей, и может пропустить весь поток.
3. Использование модуля
conntrack
, отслеживающего установленные соединения, позволяет быстрее принимать решения по фильтрации пакета. С другой стороны, при большом числе короткоживущих соединений он только вредит.4. Модуль
-m state
команды iptables
сейчас является упрощённым синонимом к модулю conntrack
.5. Обойти механизм
conntrack
можно, навесив на пакет метку notrack
.6. Механизм
conntrack
полезен даже для UDP-соединений: после отправки пакета фаервол какое-то время будет ожидать ответа от сервера.7. TCP-соединение может отклоняться как отправкой TCP RST ACK, так и отправкой ICMP-ответа. При этом ICMP-пакет будет содержать в теле заголовок TCP-пакета, на который он отвечает - так получатель поймёт, к какому TCP-потоку относится этот ICMP-пакет.
8. Трафик
RELATED
- связанный с соединением. Для TCP-соединения это может быть соответствующий ICMP-трафик, для FTP - контрольное соединение.9. Цепочка
INPUT
обрабатывает входящие пакеты, FORWARD
- пришедшие пакеты, перенаправляемые на другой интерфейс, OUTPUT
- пакеты, сгенерированные локальными приложениями.10. В отличие от
iptables
, nftables
не содержит предопределённых цепочек правил.11. Шифрование, выполняемое на сетевом уровне, обычно несовместимо с NAT.
12. При настройке фаервола стоит сразу запускать утилиты с ключами, отключающими разрешение DNS-имён. В противном случае утилиты могут зависать в ожидании ответа DNS-сервера, доступ к которому ещё запрещён.
13.
Recv-Q
показывает количество байтов, полученных интерфейсом, но не прочитанных приложением, Send-Q
- количество байтов, отправленных приложением, но ещё не подтверждённых получателем.O’Reilly Online Learning
Linux® Firewalls: Enhancing Security with nftables and Beyond, Fourth Edition
Каждый первый инженер, сталкивающийся с docker-контейнерами, знает, что образы состоят из слоёв. Слои накладываются на предыдущие, добавляя и удаляя файлы. На этом описании книги и статьи и останавливаются, не погружаясь в детали реализации, и зря: современные утилиты, например, skopeo, поддерживают работу с разными форматами образов, и без этого описания отличия транспорта
В настоящее время можно встретить образы, соответствующие 2 стандартам: традиционной спецификации docker-образов и родившейся от него спецификации OCI. Поскольку OCI - проект, направленный на унификацию формата образов, начать стоит с него. Для получения образа воспользуемся уже упомянутой утилитой
Итак, первым делом скачаем и распакуем подопытный архив:
Теперь директория
Файл
Для указания на файлы стандарт использует Content Addressable storage: файлы в директории
Общие правила именования файлов описываются стандартом.
Директория
Файл конфигурации содержит переменные окружения, entrypoint, cmd, лейблы и прочие параметры запуска контейнера, а также историю команд, из которых образ был создан (её же можно посмотреть командой `docker history nginx:latest`):
oci-archive
от ostree
могут казаться туманными. А ещё могут возникнуть неожиданные ошибки при попытке подписать образ, и разобраться с ними без изучения внутренностей образа будет трудно.В настоящее время можно встретить образы, соответствующие 2 стандартам: традиционной спецификации docker-образов и родившейся от него спецификации OCI. Поскольку OCI - проект, направленный на унификацию формата образов, начать стоит с него. Для получения образа воспользуемся уже упомянутой утилитой
skopeo
, поддерживающей множество форматов. Также в заметке будет упоминаться немало sha256-сумм, которые для читабельности я буду сокращать до пары первых и последних символов.Итак, первым делом скачаем и распакуем подопытный архив:
$ skopeo copy docker://nginx:latest oci-archive:nginx.tar
$ ls -rtlh
total 68M
-rw-rw-r-- 1 mint mint 68M сен 1 21:05 nginx.tar
$ mkdir -p nginx && tar -xf nginx.tar -C nginx
Теперь директория
nginx
содержит файлы, составляющие OCI-образ Nginx:$ cd nginx/
$ ls -F1
blobs/
index.json
oci-layout
Файл
oci-layout
скучен и содержит версию стандарта, согласно которой собран образ, директория blobs
содержит слои и их описание, и, наконец, файл index.json
является точкой входа для работы с образом:$ jq . index.json
{
"schemaVersion": 2,
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:5f0...b7f",
"size": 2295
}
]
}
Для указания на файлы стандарт использует Content Addressable storage: файлы в директории
blobs
названы как собственная хеш-сумма:$ sha256sum blobs/sha256/5f0...b7f
5f0...b7f blobs/sha256/5f0...b7f
Общие правила именования файлов описываются стандартом.
Директория
blobs
содержит файлы разного формата, и как их читать становится ясно из поля mediaType
, указанного рядом с каждой ссылкой на blob. Первым делом index.json
ссылается на JSON-файл blobls/sha256/5f0...b7f
, который уже содержит ссылки на слои и конфигурацию образа:$ cd blobls/sha256
$ jq . '5f0...b7f'
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": "sha256:5ef...03c",
"size": 7486
},
"layers": [
...
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:23f...1e0",
"size": 1398
}
],
...
}
Файл конфигурации содержит переменные окружения, entrypoint, cmd, лейблы и прочие параметры запуска контейнера, а также историю команд, из которых образ был создан (её же можно посмотреть командой `docker history nginx:latest`):
$ jq . 5ef...03c
{
"architecture": "amd64",
"config": {
"ExposedPorts": {
"80/tcp": {}
},
"Env": [
"NGINX_VERSION=1.27.1",
...
],
"Entrypoint": [
"/docker-entrypoint.sh"
],
"Cmd": [
"nginx",
"-g",
"daemon off;"
],
...
},
"created": "2024-08-14T21:31:12Z",
"history": [
...
{
"created": "2024-08-14T21:31:12Z",
"created_by": "COPY 30-tune-worker-processes.sh /docker-entrypoint.d # buildkit",
"comment": "buildkit.dockerfile.v0"
},
...
],
"os": "linux",
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:985...034",
...
]
}
}
Посмотрев на манифесты, можно изучить сами слои файловой системы, из которых образ был собран. Возьмём последний непустой слой, ссылку на который мы видели в файле
Поле
Вот мы и добрались до обещанного слоя ФС, который действительно представляет собой простой tar-архив с добавляемыми слоем файлами. При создании файловой системы контейнера архив не просто распаковывается в предыдущую директорию: иногда слой должен удалить файл, и в таком случае он напротив содержит дополнительный пустой файл, названный по имени удаляемого, но с префиксом
Конечно же, исследовать образы, вооружившись командами
Команда читает описание образа из удалённого регистра, содержащего образы для разных платформ, поэтому в полученном JSON-объекте можно найти ссылку как на только что изученную сборку под архитектуру amd64, так и на другие - например, ARMv5. Выше мы рассматривали образ, предварительно сохранив его на локальный хост: по умолчанию утилиты не скачивают части образа, которые не пригодятся при запуске, поэтому и видели мы только часть, которую можно запустить на локальном хосте.
Прочую конфигурацию образа можно посмотреть командами
Самостоятельно прочитать OCI-образ контейнера оказалось не так и сложно - непонятно, почему книги об этом умалчивают.
5f0...b7f
:$ jq .layers[-1] 5f0...b7f
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:23f...1e0",
"size": 1398
}
Поле
mediaType
подсказывает, как прочитать соответствующий blob:$ mkdir -p last_layer
$ tar -xf 23f...1e0 -C last_layer
$ cd last_layer
$ ls -RF
.:
docker-entrypoint.d/
./docker-entrypoint.d:
30-tune-worker-processes.sh*
$ head docker-entrypoint.d/30-tune-worker-processes.sh
#!/bin/sh
# vim:sw=2:ts=2:sts=2:et
set -eu
LC_ALL=C
ME=$(basename "$0")
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
[ "${NGINX_ENTRYPOINT_WORKER_PROCESSES_AUTOTUNE:-}" ] || exit 0
Вот мы и добрались до обещанного слоя ФС, который действительно представляет собой простой tar-архив с добавляемыми слоем файлами. При создании файловой системы контейнера архив не просто распаковывается в предыдущую директорию: иногда слой должен удалить файл, и в таком случае он напротив содержит дополнительный пустой файл, названный по имени удаляемого, но с префиксом
.wh.
(ибо "whiteout"). Очевидно, просто распаковать такой архив для удаления файла будет недостаточно: архив создаст лишний файл, а не удалит нужный. Также становится понятно, почему удаление файлов отдельной операцией при сборке не уменьшает итоговый размер образа, а наоборот, только увеличивает его вес.Конечно же, исследовать образы, вооружившись командами
jq
, tar
и cat
слишком утомительно, поэтому сами утилиты docker
или skopeo
позволяют изучать те или иные части образа менее сложным способом. Например, прочитать первый манифест можно следующей командой:$ docker manifest inspect nginx:latest
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 2295,
"digest": "sha256:5f0...b7f",
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
...
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 2297,
"digest": "sha256:628...b82",
"platform": {
"architecture": "arm",
"os": "linux",
"variant": "v5"
}
},
...
]
Команда читает описание образа из удалённого регистра, содержащего образы для разных платформ, поэтому в полученном JSON-объекте можно найти ссылку как на только что изученную сборку под архитектуру amd64, так и на другие - например, ARMv5. Выше мы рассматривали образ, предварительно сохранив его на локальный хост: по умолчанию утилиты не скачивают части образа, которые не пригодятся при запуске, поэтому и видели мы только часть, которую можно запустить на локальном хосте.
Прочую конфигурацию образа можно посмотреть командами
docker image inspect
и docker history
. Аналогичную информацию соберёт и команда skopeo
:$ skopeo inspect docker://nginx:latest
{
"Name": "docker.io/library/nginx",
"Digest": "sha256:447...add",
"RepoTags": [
"1",
"1-alpine",
...
"stable-otel",
"stable-perl"
],
"Created": "2024-08-14T21:31:12Z",
"DockerVersion": "",
"Labels": {
"maintainer": "NGINX Docker Maintainers \u003cdocker-maint@nginx.com\u003e"
},
"Architecture": "amd64",
"Os": "linux",
"Layers": [
"sha256:e4f...305",
...
],
"Env": [
...
"NGINX_VERSION=1.27.1",
...
]
}
Самостоятельно прочитать OCI-образ контейнера оказалось не так и сложно - непонятно, почему книги об этом умалчивают.
После знакомства со стандартом OCI будет логично повторить упражнение с docker-образом - путешествие покажется очень знакомым. Сторонние утилиты для этого не потребуются, команда
Спецификация docker-образов легла в основу спецификации OCI, и на её фоне выглядит совсем простой.
Подопытным будет тот же Nginx:
Файл
Манифест отсылает к JSON-файлу образа и слоям. Начнём с первого, он содержит привычную конфигурацию образа: историю, entrypoint, cmd и так далее:
Как именуются директории, в которых лежат слои, уже не столь очевидно. Согласно спецификации, имена должны состоять из 64 hex-символов и однозначно определяться содержимым слоя. Занятно, однако, что в качестве ссылок на слои указывается путь до
Помимо файла
И вот мы добрались до файлов, добавляемых слоем:
Правила создания и удаления файлов каждым слоем в практически неизменном виде перекочевали из спецификации Docker в OCI: даже пример создания слоя толком не поменялся.
docker save
сохраняет образы как раз в этом формате. Длинные хеш-суммы, как и раньше, сократим до нескольких символов.Спецификация docker-образов легла в основу спецификации OCI, и на её фоне выглядит совсем простой.
Подопытным будет тот же Nginx:
$ docker save nginx:latest > nginx.tar
$ mkdir nginx
$ tar -xf nginx.tar -C nginx
$ cd nginx
$ ls -F1
146...a8e/
2f6...32e/
6bf...1dd/
7ee...7d6/
b60...6ef/
caa...f3a/
df7...6ca/
e78...070.json
manifest.json
repositories
Файл
repositories
использовался в первой версии стандарта и сейчас создаётся только для обратной совместимости, современные реализации смотреть туда не будут. Как и в случае с OCI, начинать нужно с файла manifest.json
:$ jq . manifest.json
[
{
"Config": "e78...070.json",
"RepoTags": [
"nginx:latest"
],
"Layers": [
...
"caa...f3a/layer.tar"
]
}
]
Манифест отсылает к JSON-файлу образа и слоям. Начнём с первого, он содержит привычную конфигурацию образа: историю, entrypoint, cmd и так далее:
$ jq . e78...070.json
{
"architecture": "amd64",
"config": {
"ExposedPorts": {
"80/tcp": {}
},
"Env": [
...
"NGINX_VERSION=1.25.5",
...
],
"Entrypoint": [
"/docker-entrypoint.sh"
],
"Cmd": [
"nginx",
"-g",
"daemon off;"
],
"Labels": {
"maintainer": "NGINX Docker Maintainers <docker-maint@nginx.com>"
},
"StopSignal": "SIGQUIT",
"ArgsEscaped": true
},
"created": "2024-05-03T19:49:21Z",
"history": [
...
{
"created": "2024-05-03T19:49:21Z",
"created_by": "COPY 30-tune-worker-processes.sh /docker-entrypoint.d # buildkit",
"comment": "buildkit.dockerfile.v0"
},
...
],
"os": "linux",
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:5d4...e25",
...
]
}
}
Как именуются директории, в которых лежат слои, уже не столь очевидно. Согласно спецификации, имена должны состоять из 64 hex-символов и однозначно определяться содержимым слоя. Занятно, однако, что в качестве ссылок на слои указывается путь до
layer.tar
, а не просто имя директории:$ jq .[0].Layers[-1] manifest.json
"caa...f3a/layer.tar"
Помимо файла
layer.tar
директория содержит файлы json
и VERSION
, и снова ни один из них не используется в современном стандарте.И вот мы добрались до файлов, добавляемых слоем:
$ cd caa...f3a
$ tar -xf layer.tar -C last_layer/
$ cd last_layer/
$ ls -RF
.:
docker-entrypoint.d/
./docker-entrypoint.d:
30-tune-worker-processes.sh*
$ head docker-entrypoint.d/30-tune-worker-processes.sh
#!/bin/sh
# vim:sw=2:ts=2:sts=2:et
set -eu
LC_ALL=C
ME=$(basename "$0")
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
[ "${NGINX_ENTRYPOINT_WORKER_PROCESSES_AUTOTUNE:-}" ] || exit 0
Правила создания и удаления файлов каждым слоем в практически неизменном виде перекочевали из спецификации Docker в OCI: даже пример создания слоя толком не поменялся.
Формат OCI поддерживается Docker, однако включается эта поддержка не совсем тривиально. Попытка собрать образ OCI без подготовки завершается ошибкой:
Для сборки OCI-образа нужно подключить соответствующий драйвер:
После выполнения этой команды сборка уже пройдёт успешно:
Команда самостоятельно загрузит образ сборщика, сконфигурирует его и создаст образ нашего приложения.
Больше о фреймворке сборки образов в Docker можно узнать в документации и на GitHub.
$ cat Dockerfile
FROM ubuntu
$ docker buildx build --output=type=oci . > img.tar
[+] Building 0.0s (0/0) docker:default
ERROR: OCI exporter is not supported for the docker driver.
Switch to a different driver, or turn on the containerd image store, and try again.
Learn more at https://docs.docker.com/go/build-exporters/
Для сборки OCI-образа нужно подключить соответствующий драйвер:
$ docker buildx create \
--driver docker-container \
--driver-opt image=moby/buildkit:master,network=host \
--use
sleepy_beaver
После выполнения этой команды сборка уже пройдёт успешно:
$ docker buildx build --output=type=oci . > img.tar
[+] Building 20.4s (6/6) FINISHED docker-container:sleepy_beaver
=> [internal] booting buildkit 15.5s
=> => pulling image moby/buildkit:master 12.0s
=> => creating container buildx_buildkit_sleepy_beaver0 3.5s
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 51B 0.0s
=> [internal] load metadata for docker.io/library/ubuntu:latest 1.8s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [1/1] FROM docker.io/library/ubuntu:latest@sha256:8a3...3ee 2.9s
=> => resolve docker.io/library/ubuntu:latest@sha256:8a3...3ee 0.0s
=> => sha256:31e...356 29.71MB / 29.71MB 2.9s
=> exporting to oci image format 3.0s
=> => exporting layers 0.0s
=> => exporting manifest sha256:9e2...9e6 0.0s
=> => exporting config sha256:0db...509 0.0s
=> => sending tarball
Команда самостоятельно загрузит образ сборщика, сконфигурирует его и создаст образ нашего приложения.
Больше о фреймворке сборки образов в Docker можно узнать в документации и на GitHub.
В прошедшем месяце неожиданно для самого себя решил познакомиться с Java. Основной целью было начать понимать готовый код, пока без глубокого погружения в детали и изучения популярных фреймворков, за чем было решено обратиться к 9 изданию книги Java: A Beginner's Guide за авторством небезызвестного Herbert Schildt.
По структуре и подаче материала книга очень напоминает издание C++ Primer Plus Стивена Праты, также плавно проводя читателя от самых основ к обсуждению продвинутых умений языка. Шилдт описывает базовые типы переменных, управление потоком исполнения, поддержку ООП и работу с исключениями, после чего акцент смещается уже в сторону действительно специфичных для Java тем: пакетам и модулям, многопоточному программированию, дженерикам, лямбдам и интерфейсам. К сожалению, в книге не обсуждается структура проекта, системы сбороки, внутреннее устройство JVM, профилирование и оптимизация кода, java-агенты и прочие смежные темы.
После чуть более глубокого знакомства с несколькими языками становится особенно заметно их влияние друг на друга, легко проводятся аналогии. Например, интерфейсы Go совершенно аналогичны интерфейсам Java, а оператор Java try-with-resources уж очень напоминает контекстный менеджер языка Python. Не секрет, что удачные идеи быстро распространяются и реализуются соседними языками, и становится интересно, кто же первым предложил тот или иной подход. В книге об этом, конечно, не говорится, и найти концы и новые переплетения было бы интересно.
Автор также имеет и расширенную версию книги: Java: The Complete Reference. Первая её треть содержит все те же темы, что и в описываемой, остальная часть уже рассказывает о стандартной библиотеке и применениях Java. Если у вас уже есть опыт в программировании и вы хотите изучить Java подробнее, то, кажется, стоит обратиться сразу ко второй книге.
На фоне огромной популярности книги удивительно, что она содержит примеры кода сомнительного качества. Опытному читателю подобные примеры будут резать глаза, неопытного же учить плохим практикам. Не раз в книге встречается паттерн:
И ладно, если бы паттерн использовался повсеместно - другие примеры написаны корректно:
Другим подобным примером будет сравнение чисел с плавающей точкой через оператор
Несмотря на возникшие замечания книга остаётся стоящей для плавного погружения в тему, читается легко и быстро. Список традиционных заметок здесь приводить было бы странно, однако пару интересных пунктов всё-таки оставить можно:
1. Oracle выпускает новую минорную версию компилятора каждые 6 месяцев, LTS-версия - каждая четвёртая. Бесплатная поддержка LTS осуществляется в течение 4-5 лет.
2. Java поддерживает возможность интерактивного написания и исполнения кода (REPL) - JShell.
По структуре и подаче материала книга очень напоминает издание C++ Primer Plus Стивена Праты, также плавно проводя читателя от самых основ к обсуждению продвинутых умений языка. Шилдт описывает базовые типы переменных, управление потоком исполнения, поддержку ООП и работу с исключениями, после чего акцент смещается уже в сторону действительно специфичных для Java тем: пакетам и модулям, многопоточному программированию, дженерикам, лямбдам и интерфейсам. К сожалению, в книге не обсуждается структура проекта, системы сбороки, внутреннее устройство JVM, профилирование и оптимизация кода, java-агенты и прочие смежные темы.
После чуть более глубокого знакомства с несколькими языками становится особенно заметно их влияние друг на друга, легко проводятся аналогии. Например, интерфейсы Go совершенно аналогичны интерфейсам Java, а оператор Java try-with-resources уж очень напоминает контекстный менеджер языка Python. Не секрет, что удачные идеи быстро распространяются и реализуются соседними языками, и становится интересно, кто же первым предложил тот или иной подход. В книге об этом, конечно, не говорится, и найти концы и новые переплетения было бы интересно.
Автор также имеет и расширенную версию книги: Java: The Complete Reference. Первая её треть содержит все те же темы, что и в описываемой, остальная часть уже рассказывает о стандартной библиотеке и применениях Java. Если у вас уже есть опыт в программировании и вы хотите изучить Java подробнее, то, кажется, стоит обратиться сразу ко второй книге.
На фоне огромной популярности книги удивительно, что она содержит примеры кода сомнительного качества. Опытному читателю подобные примеры будут резать глаза, неопытного же учить плохим практикам. Не раз в книге встречается паттерн:
if (a == b)
return true;
else
return false;
И ладно, если бы паттерн использовался повсеместно - другие примеры написаны корректно:
return a == b;
Другим подобным примером будет сравнение чисел с плавающей точкой через оператор
==
- есть даже сайт, подробно разбирающий ошибку.Несмотря на возникшие замечания книга остаётся стоящей для плавного погружения в тему, читается легко и быстро. Список традиционных заметок здесь приводить было бы странно, однако пару интересных пунктов всё-таки оставить можно:
1. Oracle выпускает новую минорную версию компилятора каждые 6 месяцев, LTS-версия - каждая четвёртая. Бесплатная поддержка LTS осуществляется в течение 4-5 лет.
2. Java поддерживает возможность интерактивного написания и исполнения кода (REPL) - JShell.
Заметка дня #3
Для поиска полного пути к исполняемому файлу я привык пользоваться командой
Команда представляет из себя shell-скрипт длиной в 63 строки, обходящий директории из переменной
Оказывается, при помощи Bash ту же задачу можно выполнить, не вызывая системных утилит: с этим справится встроенная команда
Использовать встроенную команду кажется корректнее, чем стороннюю утилиту: команда будет пользоваться той же логикой, что и сама оболочка при поиске пути к файлу. Также встроенные средства Bash оказываются значительно быстрее аналогов-утилит:
Выигрыш в тысячные доли секунды здесь не играет никакой роли, однако в скриптах выигрыш может оказаться существенным - именно из-за этого, например, Bash поддерживает команду
#tip
Для поиска полного пути к исполняемому файлу я привык пользоваться командой
which
:$ which sshd
/usr/sbin/sshd
Команда представляет из себя shell-скрипт длиной в 63 строки, обходящий директории из переменной
PATH
в поисках переданных аргументов:$ cat /usr/bin/which
#! /bin/sh
set -ef
...
Оказывается, при помощи Bash ту же задачу можно выполнить, не вызывая системных утилит: с этим справится встроенная команда
command
. Изначально она предназначалась для выполнения команд с диска в ситуациях, когда их затеняет функция Bash с тем же именем, однако она же показывает полный путь к утилитам:$ command -v sshd
/ust/bin/sshd
$ command -V sshd
sshd is /usr/sbin/sshd
Использовать встроенную команду кажется корректнее, чем стороннюю утилиту: команда будет пользоваться той же логикой, что и сама оболочка при поиске пути к файлу. Также встроенные средства Bash оказываются значительно быстрее аналогов-утилит:
$ time which sshd
/usr/sbin/sshd
real 0m0,002s
user 0m0,002s
sys 0m0,000s
$ time command -v sshd
/usr/sbin/sshd
real 0m0,000s
user 0m0,000s
sys 0m0,000s
Выигрыш в тысячные доли секунды здесь не играет никакой роли, однако в скриптах выигрыш может оказаться существенным - именно из-за этого, например, Bash поддерживает команду
echo
, дублирующую системную утилиту.#tip
Вообще говоря, я сторонник использования стандартных утилит: я совершенно уверен, что многие идеи были переизобретены из-за недостаточного владения существующими (вот чем
Насколько книга "Docker: Up & Running" впечатлила пару месяцев назад, настолько же Podman in Action оказалась открытием. Изучать книгу будет особенно полезно инженерам, уже знакомым с Docker - и вынести из неё можно немало полезного даже если планов перейти на Podman нет.
Автор книги - Daniel Walsh, специалист информационной безопасности в Red Hat, и под его же началом Podman и был рождён. Область интересов Дена наложила отпечаток и на архитектуру Podman: впервые познакомившись с системой Docker, Ден увидел множество сомнительных и уязвимых мест в её архитектуре. Так, пользователи демона
Podman действительно выглядит выигрышно с учётом комментариев Дена, и позволяет исправить множество мелких шероховатостей, возникающих при использовании Docker. Более того, авторы утилиты поддерживают строгую совместимость с CLI
В заметках я постараюсь сделать акцент на особенностях Podman, пожертвовав в их пользу описаниями разнообразных мелких, но приятных улучшений CLI.
1. В отличие от Docker,
2.
3. Командой
4. При наличии флага монтирования
5.
6. При помощи команды
7. В отличие от Docker,
8. По умолчанию контейнеры
9.
10. Rootless
ls
не угодил?). И всё же дать шанс Podman решился - и не пожалел. Только что одним адептом Podman стало больше :)Насколько книга "Docker: Up & Running" впечатлила пару месяцев назад, настолько же Podman in Action оказалась открытием. Изучать книгу будет особенно полезно инженерам, уже знакомым с Docker - и вынести из неё можно немало полезного даже если планов перейти на Podman нет.
Автор книги - Daniel Walsh, специалист информационной безопасности в Red Hat, и под его же началом Podman и был рождён. Область интересов Дена наложила отпечаток и на архитектуру Podman: впервые познакомившись с системой Docker, Ден увидел множество сомнительных и уязвимых мест в её архитектуре. Так, пользователи демона
dockerd
часто могут легко получить права администратора на хосте, замести следы своей активности и даже скрыться от auditd
. И все описанные автором проблемы успешно и прозрачно для пользователя устранены в новом решении.Podman действительно выглядит выигрышно с учётом комментариев Дена, и позволяет исправить множество мелких шероховатостей, возникающих при использовании Docker. Более того, авторы утилиты поддерживают строгую совместимость с CLI
docker
, описывая её как alias docker=podman
. За счёт неё переход на использование Podman значительно упрощается. Перечислять достоинства утилиты можно долго, однако эту задачу оставим её автору, меня убедить он смог :)В заметках я постараюсь сделать акцент на особенностях Podman, пожертвовав в их пользу описаниями разнообразных мелких, но приятных улучшений CLI.
1. В отличие от Docker,
podman
не нужен запущенный демон, все контейнеры являются прямыми потомками пользовательского процесса podman
.2.
podman
позволяет указать регистр для использования по умолчанию, в то время как docker
всегда обращается к docker.io.3. Командой
podman image mount
можно смонтировать образ в файловую систему хоста и исследовать его, не запуская контейнер. Подобное свойство может оказаться удобным для аудита.4. При наличии флага монтирования
U
podman
изменит владельца смонтированных файлов на пользователя в контейнере. Также podman
может использовать idmap, если эту опцию поддерживает рантайм.5.
podman
может запускать Поды. Помимо этого он может запускать контейнеры на основе манифестов k8s, и наоборот, генерировать манифесты, с помощью которых можно перенести уже запущенные на хосте контейнеры в k8s.6. При помощи команды
podman unshare
можно получить доступ к командной оболочке, запущенной в пространстве имён пользователя. Другими словами, можно оценить, как увидит систему процесс, запущенный в контейнере с текущими настройками.7. В отличие от Docker,
podman
не отказывается от интеграции с SystemD. Более того, podman
позволяет запускать контейнеры, содержащие SystemD, автоматически выполняя нужную для этого конфигурацию файловой системы при старте контейнера.8. По умолчанию контейнеры
podman
отправляют логи в journald.9.
podman
можно запускать и в режиме ожидания команд, как dockerd
. Более того, благодаря тесной интеграции с SystemD сервис podman
может подниматься только при первом обращении к нему, экономя ресурсы системы, выполнять обновления контейнеров по таймеру и даже откатывать контейнеры при неудачном обновлении к предыдущему состоянию, используя сервис sd_notify
.10. Rootless
podman
выделяет часть UID пользователю и по умолчанию создаёт все контейнеры пользователя в одном пространстве имён. В то же время каждому контейнеру можно назначить своё подпространство, и в таком случае процесс одного из контейнеров не сможет атаковать другие контейнеры этого пользователя.Нередко процессу необходимо следить за содержимым файла и реагировать на изменения в нём. Реализовать эту логику можно по-разному: перечитывать файл раз в несколько секунд, прерываться на чтение в определённых точках исполнения или по сигналу, или как-либо ещё. Если перезагрузку файла нужно выполнять без участия человека, то популярным вариантом оказывается применение подсистемы ядра inotify.
API inotify позволяет незамедлительно получать уведомления об изменениях в файлах и директориях и не тратить процессорное время на лишнюю периодическую загрузку этих файлов. Приложению остаётся только перечитывать их при необходимости.
Именно такую механику использует, например, демон flagd, который использует файл для управления feature-флагами приложений. Все изменения в конфиге демона подхватываются на лету и становятся сразу доступны читателям - по крайней мере, так задумывалось. В действительности демон упорно не замечал изменения в файле, внесённые через
Разгадка крылась в том, как
Видно, inode стал другим. Под капотом
Хоть имя файла и осталось прежним, в действительности мы создали другой файл с обновлённым содержимым. Поэтому и
Как и стоило ожидать,
Подобная ситуация может проявляться и в других сценариях. Команды
API inotify позволяет незамедлительно получать уведомления об изменениях в файлах и директориях и не тратить процессорное время на лишнюю периодическую загрузку этих файлов. Приложению остаётся только перечитывать их при необходимости.
Именно такую механику использует, например, демон flagd, который использует файл для управления feature-флагами приложений. Все изменения в конфиге демона подхватываются на лету и становятся сразу доступны читателям - по крайней мере, так задумывалось. В действительности демон упорно не замечал изменения в файле, внесённые через
vim
, и требовал постоянной перезагрузки. Подобное поведение казалось особенно удивительным, поскольку редактирование того же конфига при помощи VSCode работало (и заставляло переживать, не может же vim
быть хуже VSCode!).Разгадка крылась в том, как
vim
фиксирует изменения в буфере: при редактировании vim
не записывает сразу изменения в оригинальный файл. Вместо этого редактор создаёт временную копию файла, и при успешной записи подменяет оригинальный файл изменённой версией. Проверяется это легко наблюдением за inode редактируемого файла:$ touch test.txt
$ ls -i test.txt
21038329 test.txt
# Write something, save and quit
$ vim test.txt
$ ls -i test.txt
21038337 test.txt
Видно, inode стал другим. Под капотом
vim
переименовал оригинал и записал новое содержимое по старому пути. Убедиться в этом можно, традиционно расчехлив strace
:$ strace -e unlink,rename,openat -o vim.trace vim test.txt
$ cat vim.trace
...
unlink("test.txt~") = -1 ENOENT (No such file or directory)
rename("test.txt", "test.txt~") = 0
openat(AT_FDCWD, "test.txt", O_WRONLY|O_CREAT, 0664) = 3
unlink("test.txt~") = 0
...
Хоть имя файла и осталось прежним, в действительности мы создали другой файл с обновлённым содержимым. Поэтому и
flagd
не видел изменений: формально, мы просто удалили его конфиг и создали новый на прежнем месте, и об этом процесс узнать уже не мог. Как и стоило ожидать,
vim
всё-таки позволяет управлять описанным процессом и при необходимости отключать его. Ситуация с flagd
наладилась сразу, стоило выполнить команду :set backupcopy=yes
:$ ls -i test.txt
21038336 test.txt
$ cat test.txt
123
# Type `: set backupcopy=yes' before updates
$ vim test.txt
$ ls -i test.txt
21038336 test.txt
$ cat test.txt
456
Подобная ситуация может проявляться и в других сценариях. Команды
sed -i
, sponge и прочие тоже могут приводить к пересозданию файлов, поэтому стоит особенно внимательно следить за их применением в схожих случаях.Заметка дня #4
Довольно часто возникает необходимость отследить, сколько ресурсов потребляет процесс.
Если процесс долгоживущий - например, если это веб-сервер - то вариантов много. Самый наглядный ответ даст, наверное, process_exporter с симпатичной витриной Grafana, хотя и требует этот подход наибольших вложений.
Следующей на ум приходит команда top, фильтры которой позволяют вывести интересующие метрики процессов. Если результат команды нужно использовать в скрипте, то для задачи лучше подойдёт утилита
Свежие версии утилиты даже поддерживают спецификатор
Ни один из предложенных вариантов не подойдёт для процессов, живущих лишь пару секунд - мы просто не успеем узнать его PID, чтобы подставить в
Утилита GNU
Конечно же, параметры можно вывести выборочно и красиво:
Важно обратить внимание на способ вызова утилиты. Во множестве оболочек имеется встроенная команда
Лично мне эта команда пригодилась недавно, чтобы оценить, сколько памяти потребляет Python-скрипт, работающий долю секунды. Оказывается, списки действительно сильно экономнее словарей :)
#tip
Довольно часто возникает необходимость отследить, сколько ресурсов потребляет процесс.
Если процесс долгоживущий - например, если это веб-сервер - то вариантов много. Самый наглядный ответ даст, наверное, process_exporter с симпатичной витриной Grafana, хотя и требует этот подход наибольших вложений.
Следующей на ум приходит команда top, фильтры которой позволяют вывести интересующие метрики процессов. Если результат команды нужно использовать в скрипте, то для задачи лучше подойдёт утилита
ps
, ключ -o
которой принимает через запятую параметры, которые надо показать - например, самую базовую статистику для процесса можно получить так:# $$ is PID of the current shell.
# Use the one you need or like instead
$ ps -o pcpu,pmem,comm -p $$
%CPU %MEM COMMAND
1.0 0.0 bash
Свежие версии утилиты даже поддерживают спецификатор
docker
, выводящий id контейнера, в котором запущен процесс.Ни один из предложенных вариантов не подойдёт для процессов, живущих лишь пару секунд - мы просто не успеем узнать его PID, чтобы подставить в
ps
, и тем более не сможем посмотреть на полную статистику по завершении процесса. И, сколько бы неожиданно это ни звучало, задачу отлично решает утилита time.Утилита GNU
time
, вызванная с ключом -v
, показывает не только время работы команды, но и потребление системных ресурсов. Выглядит это так:# Hide output of `ls'
$ /bin/time -v ls >/dev/null
Command being timed: "ls"
User time (seconds): 0.00
System time (seconds): 0.00
Percent of CPU this job got: 50%
Elapsed (wall clock) time (h:mm:ss or m:ss): 0:00.00
Average shared text size (kbytes): 0
Average unshared data size (kbytes): 0
Average stack size (kbytes): 0
Average total size (kbytes): 0
Maximum resident set size (kbytes): 2560
Average resident set size (kbytes): 0
Major (requiring I/O) page faults: 0
Minor (reclaiming a frame) page faults: 120
Voluntary context switches: 1
Involuntary context switches: 0
Swaps: 0
File system inputs: 0
File system outputs: 0
Socket messages sent: 0
Socket messages received: 0
Signals delivered: 0
Page size (bytes): 4096
Exit status: 0
Конечно же, параметры можно вывести выборочно и красиво:
$/bin/time -f '"%C" took %E\n%M kbytes used' ls -R >/dev/null
"ls -R" took 0:03.41
3840 kbytes used
Важно обратить внимание на способ вызова утилиты. Во множестве оболочек имеется встроенная команда
time
, просто замеряющая время исполнения пайплайна и затеняющая утилиту GNU time
, поэтому во избежание путаницы лучше прописывать полный путь до утилиты.Лично мне эта команда пригодилась недавно, чтобы оценить, сколько памяти потребляет Python-скрипт, работающий долю секунды. Оказывается, списки действительно сильно экономнее словарей :)
#tip
Grafana - отличный инструмент, законодатель мод в мире наблюдаемости, и вообще важнейший компонент любой IT-системы. В то же время её документация местами оставляет желать лучшего, и одним из таких узких мест оказываются витрины Grafana.
Сложности начали преследовать меня ещё пару лет назад, когда я писал Ansible-роль для установки Grafana. Почему-то автоматически добавить несколько заготовленных витрин не удавалось: то мешали конфликты ID витрин, то datasource не подходил, то модуль Ansible сыпал ошибками. Хоть победить проблемы и удалось, подробности вспомнить уже не выйдет. С тех времён остался лишь вгоняющий в уныние скрипт
Недавно схожие симптомы начали встретили меня снова, но уже при переносе витрин между парой серверов Grafana, и документация Grafana снова толком не помогла. Опуская часть про исследование и чтение исходников, перечислю основные выводы и советы по работе с дашбордами, которые могут спасти читателю несколько часов работы.
1. Grafana разделяет "внешний" и "внутренний" экспорт витрин. Внешний экспорт позволяет переносить витрины между серверами графаны, внутренний - просто сохраняет текущую витрину как файл. Если вы хотите перенести витрину или поделиться ей, обязательно укажите Export for sharing externally. Обычная копия витрины не заработает на другом сервере, с чем я и столкнулся.
2. UID datasource локален для сервера. Grafana позволяет просматривать схему витрины в её настройках, и UID источников данных, встречающихся там, не имеют смысла на другом сервере Grafana.
3. Дашборды, экспортированные наружу, содержат особые поля. Экспортированная наружу витрина должна содержать поля:
1.
2.
3.
Эти поля интерпретируются Grafana при загрузке витрины и сразу же удаляются.
4. Grafana не умеет экспортировать витрины наружу через API. Внешним экспортом витрин занимается UI Grafana, а поддержка совершенно серьёзно предлагает самостоятельно изучить код, добавляя, что документации к нему особо и нет.
5. Не мы первыми решаем эту задачу. Например, эта витрина просто рендерит кнопку, которая скачивает архив со всеми экспортированными наружу дашбордами. К сожалению, работает витрина не везде: по умолчанию настройки безопасности Grafana не позволяют исполнять JS-код в текстовых полях витрин. А QIWI пробовал реализовать dashboards-as-code.
Актуальное описание схемы витрин брать, видимо, нужно из кода Grafana. Комментарии к полям там, к слову, гораздо подробнее и полезнее официальной документации.
Сложности начали преследовать меня ещё пару лет назад, когда я писал Ansible-роль для установки Grafana. Почему-то автоматически добавить несколько заготовленных витрин не удавалось: то мешали конфликты ID витрин, то datasource не подходил, то модуль Ansible сыпал ошибками. Хоть победить проблемы и удалось, подробности вспомнить уже не выйдет. С тех времён остался лишь вгоняющий в уныние скрипт
fix_dashboard.sh
, который обязательно надо было запускать после внесения любых правок в дашборд:cat "$1" | tr '\n' ' ' |
sed -E 's/"datasource"[^}]*}/"datasource": {"uid": "prometheus"}/g' |
jq '.' > fixed.json.$$
mv fixed.json.$$ "$1"
Недавно схожие симптомы начали встретили меня снова, но уже при переносе витрин между парой серверов Grafana, и документация Grafana снова толком не помогла. Опуская часть про исследование и чтение исходников, перечислю основные выводы и советы по работе с дашбордами, которые могут спасти читателю несколько часов работы.
1. Grafana разделяет "внешний" и "внутренний" экспорт витрин. Внешний экспорт позволяет переносить витрины между серверами графаны, внутренний - просто сохраняет текущую витрину как файл. Если вы хотите перенести витрину или поделиться ей, обязательно укажите Export for sharing externally. Обычная копия витрины не заработает на другом сервере, с чем я и столкнулся.
2. UID datasource локален для сервера. Grafana позволяет просматривать схему витрины в её настройках, и UID источников данных, встречающихся там, не имеют смысла на другом сервере Grafana.
3. Дашборды, экспортированные наружу, содержат особые поля. Экспортированная наружу витрина должна содержать поля:
1.
__inputs
: список источников данных, используемых витриной;2.
__requires
: список плагинов, используемых витриной.3.
__elements
: ни разу не встречал поле заполненным, упоминается только в разделе про API;Эти поля интерпретируются Grafana при загрузке витрины и сразу же удаляются.
4. Grafana не умеет экспортировать витрины наружу через API. Внешним экспортом витрин занимается UI Grafana, а поддержка совершенно серьёзно предлагает самостоятельно изучить код, добавляя, что документации к нему особо и нет.
5. Не мы первыми решаем эту задачу. Например, эта витрина просто рендерит кнопку, которая скачивает архив со всеми экспортированными наружу дашбордами. К сожалению, работает витрина не везде: по умолчанию настройки безопасности Grafana не позволяют исполнять JS-код в текстовых полях витрин. А QIWI пробовал реализовать dashboards-as-code.
Актуальное описание схемы витрин брать, видимо, нужно из кода Grafana. Комментарии к полям там, к слову, гораздо подробнее и полезнее официальной документации.
Заметка дня #5
Про
Полная документация
В то же время искать конкретный параметр по всей страничке
Ключ командной строки
С помощью этой справки нет необходимости листать полную документацию каждый раз, вспоминая, каким ключом выбрать версию HTTP-протокола, изменить DNS-сервер или путь к доверенным сертификатам. А ещё это позволяет лишний раз окинуть взглядом все возможности утилиты: с её помощью можно не только отправлять HTTP-запросы, но и читать и отправлять почту, копировать файлы по протоколам WebDav и
А ваш httpie так может? :)
#tip
Про
curl
рассказывать можно вечно. Однажды мне уже повезло наткнуться на презентацию автора проекта, длилась она долгих 4 часа и рассказывала о многих непопулярных особенностях утилиты. Своими заметками я уже поделился, и там же я упоминал о встроенной документации curl
, соревноваться с которой может разве что kubectl explain.Полная документация
curl
вшита в утилиту и выводится по команде curl --manual
. Это та самая страница man curl, которой так иногда не хватает во время отладки внутри контейнера - к счастью, вместо man curl
можно вызвать curl --manual | less
и всё-таки найти нужный ключик.В то же время искать конкретный параметр по всей страничке
man curl
может быть затруднительно: описание версии 7.81.0 растянуто на 5088 строк. И тут на помощь приходит полуинтерактивный справочник, также встроенный в утилиту.Ключ командной строки
--help all
предлагает однострочные описания самых популярных ключей, а команда curl --help <category>
сужает вывод лишь до выбранного набора опций. Сам список категорий тоже можно посмотреть:$ curl --help category
Usage: curl [options...] <url>
auth Different types of authentication methods
connection Low level networking operations
curl The command line tool itself
dns General DNS options
file FILE protocol options
ftp FTP protocol options
http HTTP and HTTPS protocol options
imap IMAP protocol options
misc Options that don't fit into any other category
output Filesystem output
pop3 POP3 protocol options
post HTTP Post specific options
proxy All options related to proxies
scp SCP protocol options
sftp SFTP protocol options
smtp SMTP protocol options
ssh SSH protocol options
telnet TELNET protocol options
tftp TFTP protocol options
tls All TLS/SSL related options
upload All options for uploads
verbose Options related to any kind of command line output of curl
С помощью этой справки нет необходимости листать полную документацию каждый раз, вспоминая, каким ключом выбрать версию HTTP-протокола, изменить DNS-сервер или путь к доверенным сертификатам. А ещё это позволяет лишний раз окинуть взглядом все возможности утилиты: с её помощью можно не только отправлять HTTP-запросы, но и читать и отправлять почту, копировать файлы по протоколам WebDav и
scp
и многое другое.А ваш httpie так может? :)
$ curl -k sftp://demo:password@test.rebex.net:22
drwx------ 2 demo users 0 Mar 31 2023 .
drwx------ 2 demo users 0 Mar 31 2023 ..
drwx------ 2 demo users 0 Mar 31 2023 pub
-rw------- 1 demo users 379 Sep 19 2023 readme.txt
#tip
Следующей попавшей мне в руки книгой стала CPython internals, рассказывающая о внутреннем устройстве главного интерпретатора Python - CPython.
Книга - уникальная в своём роде, написана она в первую очередь для инженеров, которые планируют внести свой вклад в развитие языка. В книге автор рассказывает, из каких частей состоит интерпретатор Python, как можно его расширить и отладить, и как можно принять участие в развитии проекта. Подобные задачи маловероятно встретятся большинству Python-разработчиков, и в то же время знакомство с интерпретатором окажется полезным при разработке модулей-расширений или углублённом профилировании кода.
Автор иллюстрирует описываемые темы, добавляя в интерпретатор поддержку новой языковой конструкции. Для этого приходится внести правки в исходные файлы CST/AST-парсеров, компиятора и самого интерпретатора - то есть задеть все основные движущиеся части языка. На мой взгляд, пример получился наглядным и удачным, изучать учебник было интересно.
Помимо прочего, в книге я ожидал увидеть описания оптимизаций, таких как кеширование целых чисел или эффективная конкатенация строк, но, к сожалению, о них автор умолчал. Прикладные темы практически не были затронуты, автор не выходил за пределы внутренностей CPython. По этой же причине заметки к книге прикладными скорее всего назвать не удастся.
1. Среди прочего,
2. Команда
3. В отличие от многих других языков, CPython компилирует код сначала в CST, и лишь затем в AST.
4. Единица компиляции в CPython - модуль.
5. Технически, начиная с версии 3.6 Python инструкции интерпретатора могут занимать более 1 байта, поэтому "bytecode" корректнее будет называть "wordcode".
6. Интерпретатор использует отдельный стек для вызовов функций и отдельный для операций с переменными.
7. Реализация команды
8. Модуль
9. Рантайм CPython может включать несколько интерпретаторов, каждый из которых может использовать несколько потоков исполнения.
10. В будущие версии языка скорее всего войдёт модуль subinterpreters.
11. Словари хранят ключи и значения в отдельно друг от друга и бывают "split" и "combined". В первом случае указатели на значения хранятся в отдельном атрибуте, во втором - как значения ключей.
Книга - уникальная в своём роде, написана она в первую очередь для инженеров, которые планируют внести свой вклад в развитие языка. В книге автор рассказывает, из каких частей состоит интерпретатор Python, как можно его расширить и отладить, и как можно принять участие в развитии проекта. Подобные задачи маловероятно встретятся большинству Python-разработчиков, и в то же время знакомство с интерпретатором окажется полезным при разработке модулей-расширений или углублённом профилировании кода.
Автор иллюстрирует описываемые темы, добавляя в интерпретатор поддержку новой языковой конструкции. Для этого приходится внести правки в исходные файлы CST/AST-парсеров, компиятора и самого интерпретатора - то есть задеть все основные движущиеся части языка. На мой взгляд, пример получился наглядным и удачным, изучать учебник было интересно.
Помимо прочего, в книге я ожидал увидеть описания оптимизаций, таких как кеширование целых чисел или эффективная конкатенация строк, но, к сожалению, о них автор умолчал. Прикладные темы практически не были затронуты, автор не выходил за пределы внутренностей CPython. По этой же причине заметки к книге прикладными скорее всего назвать не удастся.
1. Среди прочего,
python3
поддерживает такой формат запуска скриптов: cat script.py | python3
.2. Команда
python3 -m sysconfig
показывает параметры компиляции интерпретатора.3. В отличие от многих других языков, CPython компилирует код сначала в CST, и лишь затем в AST.
4. Единица компиляции в CPython - модуль.
5. Технически, начиная с версии 3.6 Python инструкции интерпретатора могут занимать более 1 байта, поэтому "bytecode" корректнее будет называть "wordcode".
6. Интерпретатор использует отдельный стек для вызовов функций и отдельный для операций с переменными.
7. Реализация команды
LOAD_FAST
на C включает несколько операций, использует if
и несколько вызовов функций. Что ж, это во многом объясняет отличия в скоростях языков :)8. Модуль
tracemalloc
позволяет изучить, какие строки кода потребляют память и в каком количестве.9. Рантайм CPython может включать несколько интерпретаторов, каждый из которых может использовать несколько потоков исполнения.
10. В будущие версии языка скорее всего войдёт модуль subinterpreters.
11. Словари хранят ключи и значения в отдельно друг от друга и бывают "split" и "combined". В первом случае указатели на значения хранятся в отдельном атрибуте, во втором - как значения ключей.
Во время работы на удалённом хосте по SSH часто возникает необходимость выполнить пару команд локально. Daniel J. Barrett в своей книге Efficient Linux at the Command Line советует на каждую новую задачу открывать новый терминал - этот подход решит проблему, однако не всегда: выполнив ещё одно подключение уже с удалённой машины, нам может потребоваться вернуться на неё же, а новый терминал открывается локально.
За примером далеко ходить не надо: предположим, мы хотим склонировать репозиторий Git на третьем хосте, имея ключ аутентификации на удалённой машине. Подключившись к последнему хосту, мы обнаружим, что не добавили ключ в ssh-агент, и за ним нужно вернуться. Конечно, можно так и поступить: набрав
Команда
В примере выше поступить надо чуть хитрее: набрав
Описанный способ возвращения на предыдущий хост в цепочке подключений плохо масштабируется с количеством прыжков, поскольку требует много раз нажать на клавишу
Важно заметить, что сброшенная в фон сессия протухает относительно быстро. Время ожидания зависит от Keepalive-настроек сервера, но в любом случае ожидания на пару команд всегда хватает с запасом.
Наконец, говоря об escape-последовательностях
За примером далеко ходить не надо: предположим, мы хотим склонировать репозиторий Git на третьем хосте, имея ключ аутентификации на удалённой машине. Подключившись к последнему хосту, мы обнаружим, что не добавили ключ в ssh-агент, и за ним нужно вернуться. Конечно, можно так и поступить: набрав
exit
или ctrl+D
, вернуться на удалённый хост, добавить ключ и подключиться снова, однако можно поступить элегантнее.Команда
ssh
(по крайней мере, от OpenSSH) поддерживает несколько полезных комбинаций клавиш, о чём подробнее пишется в разделе Escape Characters странички man ssh. Среди них есть и нужная нам комбинация ~^Z
, сбрасывающая текущую SSH-сессию в фон. Чтобы отправить эту последовательность, надо нажать клавиши Enter ~ Ctrl z
, чтобы подключиться к сброшенной сессии - выполнить fg
.В примере выше поступить надо чуть хитрее: набрав
~^Z
, мы свернём само подключение к удалённому хосту, а не то, которое мы открыли с него. В действительности нам нужно отправить комбинацию ~^Z
на удалённый хост, набрав символ ~
дважды: Enter ~ ~ Ctrl z
. Последовательность перемещений и вводимых команд будет выглядеть так:# Connect to remote host: host2
host1 $ ssh host2
# Now go to the third host with ssh-agent forwarding
host2 $ ssh -a host3
host3 $ cd do/some/work/and/cd/deeply
# Forgot to add our key :(
host3 $ git clone git@gitlab.com:ExAnimo/books.git
Cloning into 'books'...
git@gitlab.com: Permission denied (publickey).
fatal: Could not read from remote repository.
Please make sure you have the correct access rights
and the repository exists.
# Indeed no key :(
host3 $ ssh-add -l
The agent has no identities.
# Return to host2 to get the key
# Enter ~ ~ Ctrl z
host2 $ ssh-add ~/.ssh/git_key
# Go back (workdir has not changed since ^Z!)
# and finish the work!
host2 $ fg
host3 $ git clone git@gitlab.com:ExAnimo/books.git
Cloning into 'books'...
remote: Enumerating objects: 239, done.
remote: Counting objects: 100% (9/9), done.
remote: Compressing objects: 100% (9/9), done.
remote: Total 239 (delta 2), reused 0 (delta 0), pack-reused 230 (from 1)
Receiving objects: 100% (239/239), 51.48 KiB | 8.58 MiB/s, done.
Resolving deltas: 100% (76/76), done.
Описанный способ возвращения на предыдущий хост в цепочке подключений плохо масштабируется с количеством прыжков, поскольку требует много раз нажать на клавишу
~
, чтобы добраться до последнего хоста. Если переключения неизбежны и их ожидается множество, то, чтобы не путаться, можно переопределить escape-последовательность при помощи ключа -e
, и предыдущие ssh-сессии будут игнорировать её.Важно заметить, что сброшенная в фон сессия протухает относительно быстро. Время ожидания зависит от Keepalive-настроек сервера, но в любом случае ожидания на пару команд всегда хватает с запасом.
Наконец, говоря об escape-последовательностях
ssh
, нельзя обойти стороной последовательность ~.
. Из-за проблем с сетью сессия может зависать, и ssh
не отпустит терминал до тех пор, пока не наступит таймаут. По умолчанию наибольшее время ожидания соответствует TCP-таймауту, поэтому, чтобы не ждать час и не закрывать окно принудительно, гораздо удобнее просто прервать сессию, введя Enter ~ .