Aiohttp с пользовательскими DNS-серверами, Unbound и Docker
Разгрузите приложение Python aoihttp , добавив в локальную систему кэширующие DNS-резольверы.
Использование aiohttp выглядит так просто, но это не так. Это сбивает с толку. Документация 'Client Quickstart' начинается со следующего:
Примечание Не создавайте сессию для каждого запроса. Скорее всего, вам нужна сессия для каждого приложения, которое выполняет все запросы вместе. В более сложных случаях может потребоваться сессия для каждого сайта, например, одна для Github, а другая для Facebook APIs. В любом случае создавать сессию для каждого запроса - очень плохая идея. Сессия содержит внутри себя пул соединений. Повторное использование соединений и keep-alives (оба включены по умолчанию) могут ускорить общую производительность. |
Хммм... хорошо... повторите, пожалуйста...
В общем, проблема: мне нужно проверить много разных сайтов, и я также хочу использовать пользовательские DNS-серверы. Это означает, что на каждый сайт приходится одна сессия. Я не знаю, какие сайты, я просто получаю список урлов. Поэтому мы используем сессию для каждого урла. И все это с использованием Docker.
В этом посте мы кормим aiohttp AsyncResolver с IP addresses двух кэширующих DNS-резольверов Unbound, Cloudflare или Quad9, работающих на нашей локальной системе. Как обычно, моей системой разработки является Ubuntu 22.04.
Приложение Python
Ниже представлено наше (неполное) приложение Python . Оно использует aiohttp для проверки сайтов (Urls). Сценарий выполняется с помощью контейнера Python Docker . Я не буду утомлять вас настройкой образа и контейнера Docker Python .
Обратите внимание, что мы создаем AsyncResolver для каждого сеанса TCPConnector , используя IP addresses Cloudflare или Quad9.
# check_urls.py
import asyncio
import aiodns
import aiohttp
import logging
import os
import socket
import sys
def get_logger(
console_log_level=logging.DEBUG,
file_log_level=logging.DEBUG,
log_file=os.path.splitext(__file__)[0] + '.log',
):
logger_format = '%(asctime)s %(levelname)s [%(filename)-30s%(funcName)30s():%(lineno)03s] %(message)s'
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
if console_log_level:
# console
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(console_log_level)
console_handler.setFormatter(logging.Formatter(logger_format))
logger.addHandler(console_handler)
if file_log_level:
# file
file_handler = logging.FileHandler(log_file)
file_handler.setLevel(file_log_level)
file_handler.setFormatter(logging.Formatter(logger_format))
logger.addHandler(file_handler)
return logger
logger = get_logger(file_log_level=None)
async def check_url(task_number, url, nameservers=None):
logger.debug(f'[{task_number}] url = {url}, nameservers = {nameservers}')
resolver = None
if nameservers:
resolver = aiohttp.resolver.AsyncResolver(nameservers=nameservers)
connector=aiohttp.TCPConnector(
limit=1,
use_dns_cache=True,
ttl_dns_cache=300,
family=socket.AF_INET,
resolver=resolver
)
async with aiohttp.ClientSession(
connector=connector,
# if we want to reuse the connector with other sessions,
# we must not close it: connector_owner=False
connector_owner=True,
) as session:
async with session.get(
url,
) as client_response:
logger.debug(f'[{task_number}] status = {client_response.status}')
logger.debug(f'[{task_number}] url = {client_response.url}')
logger.debug(f'[{task_number}] content_type = {client_response.headers.get("Content-Type", None)}')
logger.debug(f'[{task_number}] charset = {client_response.charset}')
async def main():
logger.debug(f'()')
dns_cloudflare = ['1.1.1.1', '1.0.0.1']
dns_quad9 = ['9.9.9.9', '149.112.112.112']
sites = [
('http://www.example.com', dns_cloudflare),
('http://www.example.org', dns_quad9)
]
tasks = []
for task_number, site in enumerate(sites):
url, nameservers = site
task = asyncio.create_task(check_url(task_number, url, nameservers))
tasks.append(task)
for task in tasks:
await task
logger.debug(f'ready')
asyncio.run(main())
Проблема: Прямое подключение к удаленным DNS-серверам
Хотя описанное выше работает, оно имеет некоторые проблемы. Если мы проверяем большое количество урлов, то мы отправляем много (отдельных) запросов к DNS-серверам Cloudflare или Quad9.
Мы можем повторно использовать TCPConnector, например, создав пул TCPConnector, и использовать DNS-кэширование коннекторов. Это значительное улучшение, но оно все еще далеко от совершенства, поскольку наши коннекторы остаются "напрямую" подключенными к внешнему миру (через резолвер).
Решение: Локальные кэширующие DNS-серверы
Мы можем добиться большего, запустив один или несколько кэширующих DNS-серверов в нашей локальной системе и снабдив AsyncResolvers IP addresses наших кэширующих DNS-серверов.
Кэширующий DNS-сервер: Несвязанный
Существует множество Docker DNS-серверов images , и я выбрал 'Unbound DNS Server Docker Image', см. ссылки ниже. Почему? Он прост в использовании и по умолчанию перенаправляет запросы на удаленный DNS-сервер Cloudflare. Приятной особенностью является то, что мы можем использовать DNS over TLS (DoT). Это означает, что мы защищаем запросы от отслеживания (провайдером).
Поскольку мы хотим иметь более одного локального DNS-сервера, сначала скопируем некоторые конфигурационные файлы за пределы контейнера. В каталоге, где мы запускаем DNS-сервер, мы создадим новый каталог:
my_conf
, затем запускаем DNS-сервер:
docker run --name=my-unbound mvance/unbound:1.17.0
И в другом терминале копируем некоторые файлы из контейнера в нашу систему:
mkdir my_conf
docker cp my-unbound:/opt/unbound/etc/unbound/forward-records.conf my_conf
docker cp my-unbound:/opt/unbound/etc/unbound/a-records.conf my_conf
Останавливаем DNS-сервер, нажав 'CTRL-C'.
Я создал следующий файл docker-compose.yml:
version: '3'
services:
unbound_cloudflare_service:
image: "mvance/unbound:1.17.0"
container_name: unbound_cloudflare_container
networks:
- dns
volumes:
- type: bind
read_only: true
source: ./my_conf/forward-records.conf
target: /opt/unbound/etc/unbound/forward-records.conf
- type: bind
read_only: true
source: ./my_conf/a-records.conf
target: /opt/unbound/etc/unbound/a-records.conf
restart: unless-stopped
networks:
dns:
external: true
name: unbound_dns_network
volumes:
mydata:
Мы подключаемся из контейнера Python приложения Docker к контейнеру DNS-сервера, используя сеть Docker . Это означает, что нет необходимости указывать порты в файле docker-compose.yml. Отсутствие портов означает повышенную безопасность. Создать сеть Docker 'unbound_dns_network':
docker network create unbound_dns_network
Запустить DNS-сервер:
docker-compose up
Проверить, работает ли DNS-сервер.
Для этого я использую 'netshoot: a Docker + Kubernetes network trouble-shooting swiss-army container', см. ссылки ниже. При его запуске мы также подключаемся к 'unbound_dns_network':
docker run --rm -it --net=unbound_dns_network nicolaka/netshoot
Затем мы используем 'dig' для проверки работоспособности нашего DNS-сервера.
Обратите внимание, что здесь мы ссылаемся на имя службы Docker-compose , 'unbound_cloudflare_service':
dig @unbound_cloudflare_service -p 53 google.com
Результат:
---
; <<>> DiG 9.18.13 <<>> @unbound_cloudflare_service -p 53 google.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 55895
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;google.com. IN A
;; ANSWER SECTION:
google.com. 300 IN A 142.250.179.174
;; Query time: 488 msec
;; SERVER: 172.17.10.3#53(unbound_cloudflare_service) (UDP)
;; WHEN: Wed Jul 12 16:32:56 UTC 2023
;; MSG SIZE rcvd: 55
'РАЗДЕЛ ОТВЕТОВ' выдает IP address. Время выполнения запроса составляет 488 миллисекунд. Если мы выполним команду еще раз, то получим тот же результат, но время запроса будет (близко) к 0. Обратите внимание, что IP address нашего локального DNS-сервера также показан:
172.17.10.3
В общем, наш локальный DNS-сервер работает!
Мы можем повторить эти шаги для Quad9. Создайте новый каталог и скопируйте в него файлы из установки Cloudflare.
Отредактируйте файл docker-compose.yml и замените 'cloudflare' на 'quad9'.
Отредактируйте файл 'forward-records.conf':
- Закомментируйте строки для Cloudflare
- Декомментируйте строки для Quad9
И поднимайте!
Использование наших локальных DNS-серверов в скрипте Python
Это последний шаг. Мы должны сделать две вещи:
- Добавить 'unbound_dns_network' в наш контейнер Python Docker .
- Переводим имена 'unbound_cloudflare_service' и 'unbound_quad9_service' в IP addresses.
Добавить 'unbound_dns_network' в наш контейнер Python Docker очень просто. Мы делаем это так же, как и в файле Unbound docker-compose.yml.
Мы уже знаем IP address наших локальных служб DSN-сервера, но они могут меняться. Вместо того чтобы жестко кодировать IP addresses, мы переводим имена сервисов в IP addresses в нашем скрипте Python , изменяя следующий код в нашем скрипте, см. выше, от:
dns_cloudflare = ['1.1.1.1', '1.0.0.1']
dns_quad9 = ['9.9.9.9', '149.112.112.112']
на:
dns_cloudflare = [socket.gethostbyname('unbound_cloudflare_service')]
dns_quad9 = [socket.gethostbyname('unbound_quad9_service')]
Конечно, это работает только в том случае, если службы локального DNS-сервера работают.
Теперь все DNS-запросы от нашего приложения Python будут направляться на наши локальные DNS-серверы!
Резюме
Мы хотели вести себя дружелюбно и не перегружать удаленные DNS-серверы большим количеством соединений. Мы также хотели устранить прямое подключение нашего приложения Python к удаленным DNS-серверам. Для этого мы раскрутили локальные службы DNS-серверов и подключили к ним наш скрипт Python , используя сеть Docker .
Мы создали дополнительную депоненту - локальные DNS-серверы, а также удалили депоненту. Если удаленный DNS-сервер не работает (в течение некоторого времени), то наше приложение Python продолжает работать.
Ссылки / кредиты
Docker Container Published Port Ignoring UFW Rules
https://www.baeldung.com/linux/docker-container-published-port-ignoring-ufw-rules
netshoot: a Docker + Kubernetes network trouble-shooting swiss-army container
https://github.com/nicolaka/netshoot
Unbound
https://nlnetlabs.nl/projects/unbound/about
Unbound DNS Server Docker Image
https://github.com/MatthewVance/unbound-docker
Недавний
- График временного ряда с Flask, Bootstrap и Chart.js
- Использование IPv6 с Microk8s
- Использование Ingress для доступа к RabbitMQ на кластере Microk8s
- Простая видеогалерея с Flask, Jinja, Bootstrap и JQuery
- Базовое планирование заданий с помощью APScheduler
- Коммутатор базы данных с HAProxy и HAProxy Runtime API
Большинство просмотренных
- Использование PyInstaller и Cython для создания исполняемого файла Python
- Уменьшение времени отклика на запросы на странице Flask SQLAlchemy веб-сайта
- Используя Python pyOpenSSL для проверки SSL-сертификатов, загруженных с хоста
- Подключение к службе на хосте Docker из контейнера Docker
- Использование UUID вместо Integer Autoincrement Primary Keys с SQLAlchemy и MariaDb
- SQLAlchemy: Использование Cascade Deletes для удаления связанных объектов