Hei, jeg er Dmitrij Logvineko â Data Engineer i analytikkavdelingen til konsernet «Vezot».
Jeg vil fortelle deg om et fantastisk verktĂžy for utvikling av ETL-prosesser â Apache Airflow. Men Airflow er sĂ„ allsidig og mangfoldig at du bĂžr vurdere det selv om du ikke jobber med dataflyt, men har behov for Ă„ kjĂžre visse prosesser med jevne mellomrom og overvĂ„ke deres utfĂžrelse.
Og ja, jeg vil ikke bare fortelle, men ogsÄ vise: programmet inneholder mye kode, skjermbilder og anbefalinger.

Hva ser du vanligvis nÄr du googler ordet Airflow / Wikimedia Commons
Innholdsfortegnelse
Innledning
Apache Airflow â det er akkurat som Django:
- skrevet i Python,
- har et utmerket administrasjonspanel,
- utvides uendelig,
â bare bedre, og laget for helt andre formĂ„l, nemlig (som skrevet fĂžr kat):
- kjÞring og overvÄking av oppgaver pÄ et ubegrenset antall maskiner (sÄ mange som Celery/Kubernetes og din samvittighet tillater)
- med dynamisk generering av arbeidsflyter fra ekstremt lettfattelig og skrivbar Python-kode
- og muligheten til Ä knytte sammen bÄde databaser og API-er med hjelp av bÄde ferdige komponenter og egendefinerte plugins (som er ekstremt enkelt Ä lage).
Vi bruker Apache Airflow slik:
- samler data fra ulike kilder (mange forekomster av SQL Server og PostgreSQL, forskjellige API-er med applikasjonsmetrikker, til og med 1C) i DWH og ODS (i vÄrt tilfelle Vertica og Clickhouse).
- som en avansert
cron, som kjÞrer prosessene for datakonsolidering pÄ ODS, samt overvÄker deres vedlikehold.
Tidligere ble vÄre behov dekket av én liten server med 32 kjerner og 50 GB RAM. I Airflow fungerer derimot:
- over 200 DAG-er (de reelle arbeidsflytene hvor vi har fylt oppgaver),
- i hver av dem i gjennomsnitt 70 oppgaver,
- kjÞres dette gode greiene (ogsÄ i gjennomsnitt) en gang i timen.
Og om hvordan vi utvidet, vil jeg skrive nedenfor, men nĂ„ la oss definere ĂŒber-oppgaven vi skal lĂžse:
Det finnes tre SQL Servere, hver med 50 databaser â instanser av ett prosjekt, sĂ„ de har nesten identisk struktur (for det meste, muahaha), og derfor har hver av dem en tabell kalt Orders (heldigvis kan du bruke en tabell med sĂ„nt navn i hvilken som helst virksomhet). Vi henter data og legger til hjelpefelt (kilde-server, kilde-database, ETL-oppgave-ID) og naivt kaster dem inn i, si, Vertica.
La oss gÄ!
Hoveddelen, praktisk (og litt teoretisk)
Hvorfor trenger vi det (og du ogsÄ)
Da trĂŠrne var store og jeg var enkel SQL-er i en russisk detaljhandel, kjĂžrte vi ETL-prosesser aka datastreams med hjelp av to verktĂžy vi hadde tilgjengelig:
- Informatica Power Center â et ekstremt omfattende system, svĂŠrt effektivt, med eget maskinvare og egen versjonering. Jeg brukte knapt 1% av dens muligheter. Hvorfor? Vel, for det fĂžrste, dette grensesnittet er psykologisk anstrengende og fĂžles som det er fra tidlig 2000-tallet. For det andre, denne tingen er laget for ekstremt komplekse prosesser, rasende gjenbruk av komponenter og andre viktige enterprise-funksjoner. Hva den koster? Den koster som en vinge av en Airbus A380 i Ă„ret, men det sier vi ikke hĂžyt.
VĂŠr forsiktig, skjermbildet kan gjĂžre folk under 30 litt vondt.

- SQL Server Integration Server â denne karen brukte vi i vĂ„re interne prosesser. For real: vi bruker allerede SQL Server, og Ă„ ikke bruke ETL-verktĂžyene deres ville vĂŠrt uhensiktsmessig. Alt ved det er bra: grensesnittet er pent, og rapportene for utfĂžrelse⊠Men vi elsker ikke programvareprodukter for det, oh nei. Vi kan versjonere
dtsx(som er et XML-dokument med noder som blandes sammen ved lagring), men hva sÄ? à lage en oppgavepakke som kan flytte hundre tabeller fra én server til en annen? Hva med hundre, etter tjue stykker vil pekefingeren din falle av nÄr du klikker pÄ musen. Men det ser definitivt mer stilfullt ut:
Vi lette absolutt etter lÞsninger. Saken kom til og med sÄ langt at vi laget en egen generator for SSIS-pakker⊠ingenting ⊠og sÄ fikk jeg ny jobb. Og her traff jeg Apache Airflow.
Da jeg fikk vite at beskrivelser av ETL-prosesser er enkel Python-kode, danset jeg nesten av glede. Slik ble datastreams versjonert og diffet, og Ă„ legge sammen tabeller med enhetlig struktur fra hundre databaser til ett mĂ„l ble til Python-kode pĂ„ 13â skjermer.
Da eg fann ut at beskrivingar av ETL-prosessar er berre enkel Python-kode, dansa eg nÊrare glad. Slik vart datastreamar versjonert og diffast, og Ä samle tabellar med einingsstruktur frÄ hundre databasar til eitt mÄl er blitt eit spÞrsmÄl om Python-kode pÄ 13" skjermar.
Samler en klynge
La oss ikke lage et barnehage, og ikke snakke om de helt Äpenbare tingene, som installasjon av Airflow, den valgte databasen, Celery og andre saker beskrevet i dokumentene.
For at vi skal kunne begynne eksperimentene med det samme, har jeg skisset docker-compose.yml der:
- La oss heve selve Airflow: Scheduler, Webserver. Der vil ogsÄ Flower kjÞre for overvÄkning av Celery-oppgaver (fordi den allerede er presset inn i
apache/airflow:1.10.10-python3.7, og vi har ikke noe imot det); - PostgreSQL, hvor Airflow vil skrive sin logg (planleggers data, ytelsesstatistikk osv.), mens Celery vil merke fullfĂžrte oppgaver;
- Redis, som vil fungere som oppgavebroker for Celery;
- Celery worker, som vil ta seg av selve utfĂžrelsen av oppgavene.
- I mappen
.\/dagsvil vi legge filene vÄre med beskrivelse av dags. De vil bli fanget opp automatisk, sÄ det er ikke nÞdvendig Ä trekke hele stakken etter hver lille endring.
Noen steder er koden i eksemplene ikke fullstendig (for Ä unngÄ Ä overbelaste teksten), og noen ganger endres den i prosessen. Hele fungerende kodeeksempler kan ses i depotet. .
docker-compose.yml
version: '3.4'
x-airflow-config: &airflow-config
AIRFLOW__CORE__DAGS_FOLDER: /dags
AIRFLOW__CORE__EXECUTOR: CeleryExecutor
AIRFLOW__CORE__FERNET_KEY: MJNz36Q8222VOQhBOmBROFrmeSxNOgTCMaVp2_HOtE0=
AIRFLOW__CORE__HOSTNAME_CALLABLE: airflow.utils.net:get_host_ip_address
AIRFLOW__CORE__SQL_ALCHEMY_CONN: postgres+psycopg2://airflow:airflow@airflow-db:5432/airflow
AIRFLOW__CORE__PARALLELISM: 128
AIRFLOW__CORE__DAG_CONCURRENCY: 16
AIRFLOW__CORE__MAX_ACTIVE_RUNS_PER_DAG: 4
AIRFLOW__CORE__LOAD_EXAMPLES: 'False'
AIRFLOW__CORE__LOAD_DEFAULT_CONNECTIONS: 'False'
AIRFLOW__EMAIL__DEFAULT_EMAIL_ON_RETRY: 'False'
AIRFLOW__EMAIL__DEFAULT_EMAIL_ON_FAILURE: 'False'
AIRFLOW__CELERY__BROKER_URL: redis://broker:6379/0
AIRFLOW__CELERY__RESULT_BACKEND: db+postgresql://airflow:airflow@airflow-db/airflow
x-airflow-base: &airflow-base
image: apache/airflow:1.10.10-python3.7
entrypoint: /bin/bash
restart: always
volumes:
- ./dags:/dags
- ./requirements.txt:/requirements.txt
services:
# Redis som en Celery broker
broker:
image: redis:6.0.5-alpine
# Database for metadataen til Airflow
airflow-db:
image: postgres:10.13-alpine
environment:
- POSTGRES_USER=airflow
- POSTGRES_PASSWORD=airflow
- POSTGRES_DB=airflow
volumes:
- ./db:/var/lib/postgresql/data
# Hovedbeholder med Airflow Webserver, Scheduler, Celery Flower
airflow:
<<: *airflow-base
environment:
<
-c " sleep 10 &&
pip install --user -r /requirements.txt &&
/entrypoint initdb &&
(/entrypoint webserver &) &&
(/entrypoint flower &) &&
/entrypoint scheduler"
ports:
# Celery Flower
- 5555:5555
# Airflow Webserver
- 8080:8080
# Celery worker, vil bli skalert med `--scale=n`
worker:
<<: *airflow-base
environment:
<
-c " sleep 10 &&
pip install --user -r /requirements.txt &&
/entrypoint worker"
depends_on:
- airflow
- airflow-db
- brokerMerknader:
- I oppsettet av komposisjon har jeg i stor grad basert meg pĂ„ det kjente bildet â sĂžrg for Ă„ ta en titt. Kanskje du ikke trenger mer i livet.
- Alle Airflow-innstillinger er tilgjengelige ikke bare gjennom
airflow.cfg, men ogsÄ via miljÞvariabler (takk til utviklerne), noe jeg flittig har utnyttet. - SelvfÞlgelig er den ikke produksjonsklar: jeg har med vilje ikke satt opp hjerterytmer pÄ beholderne, jeg har ikke tatt meg av sikkerheten. Men jeg har laget et minimum som passer for vÄre eksperimenter.
- VÊr oppmerksom pÄ at:
- Mappen med dags mÄ vÊre tilgjengelig bÄde for planleggeren og arbeidsmennene.
- Det samme gjelder for alle tredjepartsbiblioteker â de mĂ„ alle vĂŠre installert pĂ„ maskinene med planlegger og arbeidere.
Men nÄ er det enkelt:
$ docker-compose up --scale worker=3NÄr alt er oppe, kan man se pÄ webgrensesnittene:
- Airflow:
- Flower:
Grunnleggende begreper
Hvis du ikke forstod noe av alle disse «dagene», her er en kort ordbok:
- Scheduler â den viktigste fyren i Airflow, som sĂžrger for at robotene jobber, ikke menneskene: han overvĂ„ker tidsplanen, oppdaterer dager og kjĂžrer oppgaver.
Egentlig, i eldre versjoner, hadde han minneproblemer (nei, ikke amnesi, men minnelekkasjer) og i konfigurasjonene var det til og med en legacy-parameter
run_durationâ intervallet for hans omstart. Men nĂ„ fungerer alt bra. - DAG (som ogsĂ„ kalles «dag») â «rettet acyklisk graf», men en slik definisjon sier lite for de fleste, og det er i bunn og grunn en container for oppgaver som interagerer med hverandre (se under) eller en analog til Package i SSIS og Workflow i Informatica.
Foruten dager kan det ogsÄ vÊre subdager, men vi vil mest sannsynlig ikke komme oss dit.
- DAG Run â en initialisert dag, som har fĂ„tt sin
execution_date. Dagrunninger av en dag kan godt kjĂžre samtidig (hvis du selvsagt har gjort oppgavene dine idempotente). - Operator â dette er biter av kode ansvarlig for Ă„ utfĂžre en spesifikk handling. Det finnes tre typer operatĂžrer:
- action, som for eksempel vÄr favoritt
PythonOperator, som kan kjĂžre hvilken som helst (gyldig) Python-kode; - transfer, som frakter data fra ett sted til et annet, for eksempel,
MsSqlToHiveTransfer; - sensor vil tillate Ă„ reagere eller bremse videre utfĂžrelse av dagen inntil en bestemt hendelse inntreffer.
HttpSensorkan trekke fra den angitte endepunktet, og nÄr den fÄr det Þnskede svaret, starte overfÞringenGoogleCloudStorageToS3Operator. Det nysgjerrige sinnet vil spÞrre: «hvorfor? For det er mulig Ä gjÞre repetisjoner direkte i operatÞren!» Men sÄ, for ikke Ä tette opp oppgavepuljen med hengende operatÞrer. Sensoren starter, sjekker, og dÞr til neste forsÞk.
- action, som for eksempel vÄr favoritt
- Task â erklĂŠrte operatĂžrer uavhengig av type og knyttet til dagen oppgraderes til oppgave.
- Task instance â nĂ„r generalplanleggeren beslutter at oppgavene bĂžr sendes i kamp til utĂžverne-arbeidere (rett pĂ„ stedet, hvis vi bruker
LocalExecutoreller til en ekstern node i tilfelle medCeleryExecutor), gir han dem en kontekst (dvs. et sett med variabler â kjĂžreparametere), utvikler kommando- eller forespĂžrselsskabloner og plasserer dem i puljen.
Genererer oppgaver
FĂžrst vil vi definere den generelle planen for dagen, og deretter dykke mer og mer inn i detaljene, fordi vi anvender noen ikke-trivielle lĂžsninger.
SĂ„, i sin enkleste form vil en slik dag se slik ut:
fra datetime import timedelta, datetime
fra airflow import DAG
fra airflow.operators.python_operator import PythonOperator
fra commons.datasources import sql_server_ds
dag = DAG('orders',
schedule_interval=timedelta(hours=6),
start_date=datetime(2020, 7, 8, 0))
def workflow(**context):
print(context)
for conn_id, schema in sql_server_ds:
PythonOperator(
task_id=schema,
python_callable=workflow,
provide_context=True,
dag=dag)La oss finne ut av dette:
- FĂžrst importerer vi de nĂždvendige biblioteken og noen andre ting;
sql_server_dsâ erListe[namedtuple[str, str]]med navnene pĂ„ tilkoblinger fra Airflow Connections og databasene vi skal hente tabellen vĂ„r fra;dager erklĂŠringen av DAG-en vĂ„r, som mĂ„ ligge iglobals(), ellers finner ikke Airflow den. Vi mĂ„ ogsĂ„ si til DAG-en:- at den heter
ordersâ dette navnet vil senere dukke opp i webgrensesnittet, - at den vil begynne Ă„ kjĂžre fra midnatt den 8. juli,
- og den bĂžr kjĂžre omtrent hver sjette time (her kan de kule bruke
timedelta()til en strincronâ for de mindre kule er det tillatt Ă„ bruke et uttrykk som0 0 0/6 ? * * *@dailyworkflow());
- at den heter
vil gjÞre hovedarbeidet, men ikke nÄ. NÄ skal vi bare logge konteksten vÄr.Og nÄ, litt magi med Ä opprette oppgaver:- vi gÄr gjennom kildene vÄre;
- initializerer
- , som vil utfÞre vÄr tomme funksjon
PythonOperator. Husk Ä gi et unikt (innenfor DAG-en) navn pÄ oppgaven og knytte selve DAG-en. Flaggetvil gjÞre hovedarbeidet, men ikke nÄ. NÄ skal vi bare logge konteksten vÄr.provide_contextvil i sin tur gi ekstra argumenter til funksjonen, som vi forsiktig samler med hjelp av**contextDet er alt for nÄ. Hva har vi fÄtt:.
en ny DAG i webgrensesnittet,
- hundre og fĂžrtito oppgaver som vil bli kjĂžrt parallelt (hvis Airflow, Celery og serverkraften tillater det).
- Vel, nesten fÄtt det.
Hvem skal sette opp avhengighetene?

For Ă„ forenkle dette har jeg lagt inn
behandlingen docker-compose.yml pÄ alle nodene. requirements.txt NÄ setter vi i gang:
De grÄ firkantene er oppgaveinstanser behandlet av planleggeren.

Vi venter litt, oppgavene blir plukket opp av arbeiderne:
De grĂžnne, logisk nok, er de som har kjĂžrt vellykket. De rĂžde â ikke sĂ„ vellykket.

Forresten, pÄ produksjonen vÄr finnes det ingen mappe
, som synkroniseres mellom maskinene â alle DAG-er ligger i
.\/dagspĂ„ vĂ„r Gitlab, og Gitlab CI distribuerer oppdateringene til maskinene ved sammenslĂ„ing igitMens arbeiderne jobber med oppgavene vĂ„re, la oss minnes et annet verktĂžy som kan gi oss noen innsikter â Flower.master.
Litt om Flower
Den aller fĂžrste siden med oppsummerende informasjon om nodene-arbeidere:
Den mest innholdsrike siden med oppgavene som har blitt sendt til behandling:

Den mest innhaldsrike sida med oppgÄver som har sendt seg i arbeid:

Den kjedeligste siden med status for megleren vÄr:

Den mest fargerike siden â med grafer over oppgave-status og deres utfĂžrelsestid:

Last opp det som ikke er lastet opp
SĂ„, alle oppgavene har kjĂžrt, vi kan ta med de skadde.

Det viste seg Ă„ vĂŠre mange skadde â av ulike grunner. Ved korrekt bruk av Airflow indikerer disse firkantene at dataene absolutt ikke har nĂ„dd frem.
Vi mÄ se pÄ loggen og starte pÄ nytt de mislykkede oppgave-instansene.
Ved Ä klikke pÄ hvilken som helst firkant, vil vi se tilgjengelige handlinger for oss:

Vi kan ta og gjÞre Clear pÄ den mislykkede. Det vil si at vi glemmer at noe har strandet, og den samme oppgave-instansen vil gÄ til planleggeren.

Det er klart at det ikke er sĂŠrlig human Ă„ gjĂžre dette med musen pĂ„ alle de rĂžde firkantene â det er ikke det vi forventer fra Airflow. SelvfĂžlgelig har vi et massedrapsvĂ„pen: Browse/Task Instances

Vi velger alt pÄ en gang og nullstiller ved Ä trykke pÄ riktig punkt:

Etter opprydning ser vÄre oppgaver slik ut (de kan nesten ikke vente med Ä bli planlagt av scheduler):

Tilkoblinger, hooks og andre variabler
Det er pÄ tide Ä se pÄ den neste DAG-en, update_reports.py:
from collections import namedtuple
from datetime import datetime, timedelta
from textwrap import dedent
from airflow import DAG
from airflow.contrib.operators.vertica_operator import VerticaOperator
from airflow.operators.email_operator import EmailOperator
from airflow.utils.trigger_rule import TriggerRule
from commons.operators import TelegramBotSendMessage
dag = DAG('update_reports',
start_date=datetime(2020, 6, 7, 6),
schedule_interval=timedelta(days=1),
default_args={'retries': 3, 'retry_delay': timedelta(seconds=10)})
Report = namedtuple('Report', 'source target')
reports = [Report(f'{table}_view', table) for table in [
'reports.city_orders',
'reports.client_calls',
'reports.client_rates',
'reports.daily_orders',
'reports.order_duration']]
email = EmailOperator(
task_id='email_success', dag=dag,
to='{{ var.value.all_the_kings_men }}',
subject='DWH Reports updated',
html_content=dedent("""KjĂŠre alle, rapportene er oppdatert"""),
trigger_rule=TriggerRule.ALL_SUCCESS)
tg = TelegramBotSendMessage(
task_id='telegram_fail', dag=dag,
tg_bot_conn_id='tg_main',
chat_id='{{ var.value.failures_chat }}',
message=dedent("""
Natasha, vÄkne, vi har krasjet {{ dag.dag_id }}
"""),
trigger_rule=TriggerRule.ONE_FAILED)
for source, target in reports:
queries = [f"TRUNCATE TABLE {target}",
f"INSERT INTO {target} SELECT * FROM {source}"]
report_update = VerticaOperator(
task_id=target.replace('reports.', ''),
sql=queries, vertica_conn_id='dwh',
task_concurrency=1, dag=dag)
report_update >> [email, tg]Har ikke alle laget en rapportoppdatering en gang? Det er igjen: det er en liste over kilder for Ä hente data; det er en liste over hvor de skal plasseres; ikke glem Ä gi beskjed nÄr alt skjer eller gÄr i stykker (men det er ikke oss, nei).
La oss gÄ gjennom filen igjen og se pÄ de nye uklare tingene:
from commons.operators import TelegramBotSendMessageâ ingenting hindrer oss i Ă„ lage vĂ„re egne operatorer, noe vi utnyttet ved Ă„ lage et lite wrapper for Ă„ sende meldinger i Unblocked. (Vi vil snakke mer om denne operatoren senere);default_args={}â DAG-en kan dele de samme argumentene til alle sine operatorer;to='{{ var.value.all_the_kings_men }}'â feltettovil ikke vĂŠre hardkodet, men dynamisk generert ved hjelp av Jinja og en variabel med en liste over e-poster som jeg forsiktig har plassert iAdmin/Variables;trigger_rule=TriggerRule.ALL_SUCCESSâ betingelsen for Ă„ aktivere operatoren. I vĂ„rt tilfelle vil e-posten bli sendt til sjefene bare hvis alle avhengigheter kjĂžrer succesfully;tg_bot_conn_id='tg_main'â argumenteneconn_idtar inn identifikatorene for tilkoblingene som vi oppretter iAdmin/Connections;trigger_rule=TriggerRule.ONE_FAILEDâ meldinger vil bare bli sendt til Telegram hvis det er mislykkede oppgaver;task_concurrency=1â forhindrer samtidig kjĂžring av flere task-instanser av Ă©n oppgave. Ellers vil vi fĂ„ samtidige kjĂžringer av flereVerticaOperator(som ser pĂ„ Ă©n tabell);report_update >> [email, tg]â alleVerticaOperatorvil samles om Ă„ sende e-post og meldinger, pĂ„ denne mĂ„ten:

Men siden varsler-operatÞrene har forskjellige aktiveringsbetingelser, vil bare én fungere. I Tree View ser det noe mindre oversiktlig ut:

Jeg vil si et par ord om makroer og deres venner â variabler.
Makroer er Jinja-plassholdere som kan sette inn nyttig informasjon i argumentene til operatorene. For eksempel slik:
SELECT
id,
payment_dtm,
payment_type,
client_id
FROM orders.payments
WHERE
payment_dtm::DATE = '{{ ds }}'::DATE{{ ds }} vil utvides til innholdet i konteksten variabel execution_date i formatet YYYY-MM-DD: 2020-07-14. Det mest behagelige er at kontekstvariabler er knyttet til en bestemt oppgaveinstans (rutenett i Tree View), og ved omstart vil plassholderne utvides til de samme verdiene.
De tildelte verdiene kan ses ved hjelp av Rendered-knappen pÄ hver oppgaveinstans. Slik ser det ut for oppgaven som sender e-post:

Og slik ser det ut for oppgaven med Ă„ sende meldinger:

Den fullstendige listen over innebygde makroer for den siste tilgjengelige versjonen finnes her:
Videre kan vi med hjelp av plugins erklÊre vÄre egne makroer, men det er en helt annen historie.
I tillegg til forhÄndsdefinerte ting, kan vi sette inn verdier fra vÄre variabler (jeg har allerede brukt dette i koden ovenfor). La oss lage i Admin/Variables et par ting:

Det er alt, nÄ kan vi bruke det:
TelegramBotSendMessage(chat_id='{{ var.value.failures_chat }}')I verdien kan det vĂŠre en skalar, eller det kan ligge en JSON. I tilfelle av JSON:
bot_config
{
"bot": {
"token": 881hskdfASDA16641,
"name": "Verter"
},
"service": "TG"
}bare bruk stien til den nĂždvendige nĂžkkelen: {{ var.json.bot_config.bot.token }}.
Jeg skal si ett ord og vise et skjermbilde om tilkoblinger. Her er alt elementÊrt: pÄ siden Admin/Connections oppretter vi en tilkobling, legger inn brukernavn/passordene vÄre og mer spesifikke parametere. Slikt:

Passord kan krypteres (mer grundig enn i standardalternativet), eller man kan unngĂ„ Ă„ spesifisere tilkoblingstypen (som jeg gjorde for tg_main) â saken er at listen over typer er hardkodet i Airflow-modellene og kan ikke utvides uten Ă„ gĂ„ inn i kildekoden (hvis jeg tilfeldigvis har glemt noe â vennligst korriger meg), men Ă„ fĂ„ kreditt enkelt ved navn er det ingenting som hindrer oss.
Og man kan ogsÄ lage flere tilkoblinger med samme navn: i sÄ fall vil metoden BaseHook.get_connection(), som henter tilkoblinger etter navn, returnere en tilfeldig ut av flere med samme navn (det ville vÊrt mer logisk Ä gjÞre Round Robin, men la oss la det vÊre opp til utviklerne av Airflow).
Variabler og Tilkoblinger er absolutt flotte verktÞy, men det er viktig Ä ikke miste balansen: hvilke deler av strÞmmen din lagrer du faktisk i koden, og hvilke deler gir du til Airflow for lagring. PÄ den ene siden kan det vÊre praktisk Ä raskt endre verdien, for eksempel e-postadressen pÄ et nyhetsbrev, via UI. PÄ den andre siden er det tross alt en tilbakevending til museklikk, som vi (jeg) Þnsket Ä bli kvitt.
Arbeid med tilkoblinger er en av oppgavene hookene. Generelt er Airflow-hooks tilkoblingspunktene til dets eksterne tjenester og biblioteker. For eksempel, JiraHook vil Äpne en klient for oss for Ä samhandle med Jira (vi kan flytte oppgaver frem og tilbake), og ved hjelp av SambaHook kan man laste opp en lokal fil til smb-punkt.
Analyzerer en tilpasset operator
Og vi har nÄ kommet nÊrt til Ä se pÄ hvordan TelegramBotSendMessage
Kode commons/operators.py ser ut med selve operatĂžren:
fra typing import Union
fra airflow.operators import BaseOperator
fra commons.hooks import TelegramBotHook, TelegramBot
class TelegramBotSendMessage(BaseOperator):
"""Send melding til chat_id ved hjelp av TelegramBotHook
Eksempel:
>>> TelegramBotSendMessage(
... task_id='telegram_fail', dag=dag,
... tg_bot_conn_id='tg_bot_default',
... chat_id='{{ var.value.all_the_young_dudes_chat }}',
... message='{{ dag.dag_id }} mislyktes :(',
... trigger_rule=TriggerRule.ONE_FAILED)
"""
template_fields = ['chat_id', 'message']
def __init__(self,
chat_id: Union[int, str],
message: str,
tg_bot_conn_id: str = 'tg_bot_default',
*args, **kwargs):
super().__init__(*args, **kwargs)
self._hook = TelegramBotHook(tg_bot_conn_id)
self.client: TelegramBot = self._hook.client
self.chat_id = chat_id
self.message = message
def execute(self, context):
print(f'Send "{self.message}" til chat {self.chat_id}')
self.client.send_message(chat_id=self.chat_id,
message=self.message)Her, som med alt annet i Airflow, er alt ganske enkelt:
- Vi arvet fra
BaseOperator, som implementerer ganske mange Airflow-spesifikke greier (ta en titt pÄ fritiden) - Vi deklarerte feltene
template_fields, der Jinja vil se etter makroer for behandling. - Vi organiserte riktige argumenter for
__init__(), satt opp standardverdier der det var nĂždvendig. - Vi glemte ikke Ă„ initialisere forelderens klasse.
- Vi Äpnet den tilsvarende hooken
TelegramBotHook, og fikk klientobjektet fra den. - Vi overstyrte metoden
BaseOperator.execute(), som Airflow vil kalle nĂ„r det er pĂ„ tide Ă„ kjĂžre operatoren â i den implementerer vi hovedhandlingen, men glemmer ikke Ă„ logge. (Vi logger oss faktisk inn istdoutogstderrâ Airflow vil fange alt, pakke det pent inn, og fordele det til riktig sted.)
La oss se hva vi har i commons/hooks.py. Den fĂžrste delen av filen, med selve hooken:
fra typing import Union
fra airflow.hooks.base_hook import BaseHook
fra requests_toolbelt.sessions import BaseUrlSession
class TelegramBotHook(BaseHook):
"""Telegram Bot API hook
Merknad: legg til en tilkobling med tom Conn Type og glem ikke
Ă„ fylle ut Extra:
{"bot_token": "YOuRAwEsomeBOtToKen"}
"""
def __init__(self,
tg_bot_conn_id='tg_bot_default'):
super().__init__(tg_bot_conn_id)
self.tg_bot_conn_id = tg_bot_conn_id
self.tg_bot_token = None
self.client = None
self.get_conn()
def get_conn(self):
extra = self.get_connection(self.tg_bot_conn_id).extra_dejson
self.tg_bot_token = extra['bot_token']
self.client = TelegramBot(self.tg_bot_token)
return self.clientJeg vet ikke engang hva jeg skal forklare her, bare merk deg de viktige punktene:
- Vi arver, tenker pĂ„ argumentene â i de fleste tilfeller vil det vĂŠre ett:
conn_id; - Vi overstyrer standardmetodene: jeg begrenset meg til
get_conn(), der jeg fÄr tilkoblingsparametrene etter navn og bare henter seksjonenextra(dette felt er for JSON), der jeg (etter mine egne instruksjoner!) la Telegram-bot-tokenet:{"bot_token": "YOuRAwEsomeBOtToKen"}. - Jeg oppretter et eksempel av vÄr
TelegramBot, og gir den allerede et spesifikt token.
Det er alt. Du kan fÄ klienten fra haken med hjelp av TelegramBotHook().client eller TelegramBotHook().get_conn().
Og den andre delen av filen, der jeg laget en mikroinnpakning for Telegram REST API, for ikke Ä mÄtte ta med den samme for bare én metode sendMessage.
class TelegramBot:
"""Telegram Bot API-wrapper
Eksempler:
>>> TelegramBot('YOuRAwEsomeBOtToKen', '@myprettydebugchat').send_message('Hei, kjĂŠre')
>>> TelegramBot('YOuRAwEsomeBOtToKen').send_message('Hei, kjĂŠre', chat_id=-1762374628374)
"""
API_ENDPOINT = 'https://api.telegram.org/bot{}/'
def __init__(self, tg_bot_token: str, chat_id: Union[int, str] = None):
self._base_url = TelegramBot.API_ENDPOINT.format(tg_bot_token)
self.session = BaseUrlSession(self._base_url)
self.chat_id = chat_id
def send_message(self, message: str, chat_id: Union[int, str] = None):
method = 'sendMessage'
payload = {'chat_id': chat_id or self.chat_id,
'text': message,
'parse_mode': 'MarkdownV2'}
response = self.session.post(method, data=payload).json()
if not response.get('ok'):
raise TelegramBotException(response)
class TelegramBotException(Exception):
def __init__(self, *args, **kwargs):
super().__init__((args, kwargs))Den riktige veien er Ă„ samle alt dette:
TelegramBotSendMessage,TelegramBotHook,TelegramBotâ i et plugin, legge det i et offentlig repo, og gi det ut som Open Source.
Mens vi studerte alt dette, klarte oppdateringene vĂ„re Ă„ gjĂžre en feilmelding og sende meg en melding i kanalen. Jeg skal sjekke hva som igjen er galtâŠ

Noe har gÄtt galt i dagene vÄre! Var ikke det hva vi ventet pÄ? Nettopp!
Skal du helle noe?
FÞler du at jeg har gÄtt glipp av noe? Jeg lovet visst Ä overfÞre data fra SQL Server til Vertica, og sÄ skled jeg unna temaet, skurk!
Gjerningen var med vilje, jeg mÄtte bare forklare deg noe terminologi. NÄ kan vi fortsette.
Planen vÄr var slik:
- Lag en dag
- Generere oppgaver
- Se hvordan alt ser pent ut
- Tilordne sesjonsnumre til opplastninger
- Hent data fra SQL Server
- Legg dataene i Vertica
- Samle statistikk
SÄ, for Ä sette i gang dette, har jeg laget et lite tillegg til vÄr docker-compose.yml:
docker-compose.db.yml
versjon: '3.4'
x-mssql-base: &mssql-base
image: mcr.microsoft.com/mssql/server:2017-CU21-ubuntu-16.04
restart: alltid
environment:
ACCEPT_EULA: Y
MSSQL_PID: Express
SA_PASSWORD: SayThanksToSatiaAt2020
MSSQL_MEMORY_LIMIT_MB: 1024
tjenester:
dwh:
image: jbfavre/vertica:9.2.0-7_ubuntu-16.04
mssql_0:
<<: *mssql-base
mssql_1:
<<: *mssql-base
mssql_2:
<<: *mssql-base
mssql_init:
image: mio101/py3-sql-db-client-base
command: python3 ./mssql_init.py
depends_on:
- mssql_0
- mssql_1
- mssql_2
environment:
SA_PASSWORD: SayThanksToSatiaAt2020
volumes:
- ./mssql_init.py:/mssql_init.py
- ./dags/commons/datasources.py:/commons/datasources.pyDer hever vi opp:
- Vertica som vert
dwhmed de mest standardinnstillingene, - tre instanser av SQL Server,
- fyller databasene med noen siste data (ikke se i
mssql_init.py!)
Vi starter opp alt med en litt mer kompleks kommando enn sist:
$ docker-compose -f docker-compose.yml -f docker-compose.db.yml up --scale worker=3Det som ble generert av vÄr mirakelslaver, kan man bruke ved Ä gÄ til Data Profiling/Ad Hoc Query:

Det viktigste er Ă„ ikke vise dette til analystene
Jeg vil ikke gÄ nÊrmere inn pÄ ETL-Þktene der, alt er trivielt: vi lager databasen, i den en tabell, pakker alt i en kontekstbehandler, og sÄ gjÞr vi slik:
with Session(task_name) as session:
print('Load', session.id, 'started')
# Last workflow
...
session.successful = True
session.loaded_rows = 15session.py
fra sys import stderr
class Session:
"""ETL arbeidsflytĂžktjon
Eksempel:
med Session(task_name) som session:
print(session.id)
session.successful = True
session.loaded_rows = 15
session.comment = 'Godt jobbet'
"""
def __init__(self, connection, task_name):
self.connection = connection
self.connection.autocommit = True
self._task_name = task_name
self._id = None
self.loaded_rows = None
self.successful = None
self.comment = None
def __enter__(self):
return self.open()
def __exit__(self, exc_type, exc_val, exc_tb):
if any(exc_type, exc_val, exc_tb):
self.successful = False
self.comment = f'{exc_type}: {exc_val}n{exc_tb}'
print(exc_type, exc_val, exc_tb, file=stderr)
self.close()
def __repr__(self):
return (f'<{self.__class__.__name__} '
f'id={self.id} '
f'task_name="{self.task_name}">')
@property
def task_name(self):
return self._task_name
@property
def id(self):
return self._id
def _execute(self, query, *args):
with self.connection.cursor() som cursor:
cursor.execute(query, args)
return cursor.fetchone()[0]
def _create(self):
query = """
OPPRETT TABLE IF NOT EXISTS sessions (
id SERIAL NOT NULL PRIMARY KEY,
task_name VARCHAR(200) NOT NULL,
started TIMESTAMPTZ NOT NULL DEFAULT current_timestamp,
finished TIMESTAMPTZ DEFAULT current_timestamp,
successful BOOL,
loaded_rows INT,
comment VARCHAR(500)
);
"""
self._execute(query)
def open(self):
query = """
INSERT INTO sessions (task_name, finished)
VALUES (%s, NULL)
RETURNING id;
"""
self._id = self._execute(query, self.task_name)
print(self, 'Äpnet')
return self
def close(self):
if not self._id:
raise SessionClosedError('Ăkten er ikke Ă„pen')
query = """
UPDATE sessions
SET
finished = DEFAULT,
successful = %s,
loaded_rows = %s,
comment = %s
WHERE
id = %s
RETURNING id;
"""
self._execute(query, self.successful, self.loaded_rows,
self.comment, self.id)
print(self, 'lukket',
', vellykket: ', self.successful,
', Lastet: ', self.loaded_rows,
', kommentar:', self.comment)
class SessionError(Exception):
pass
class SessionClosedError(SessionError):
passTiden er inne Ä hente vÄre data fra vÄre hundre og femti bord. La oss gjÞre det med noen enkle linjer:
source_conn = MsSqlHook(mssql_conn_id=src_conn_id, schema=src_schema).get_conn()
query = f"""
SELECT
id, start_time, end_time, type, data
FROM dbo.Orders
WHERE
CONVERT(DATE, start_time) = '{dt}'
"""
df = pd.read_sql_query(query, source_conn)- Ved hjelp av hooken vil vi fÄ fra Airflow
pymssql-tilkobling - Vi skal legge en databegrensning i forespĂžrselen â den vil bli lagt inn av templateringsverktĂžyet.
- Vi mater forespÞrselen vÄr
pandas, som skaffer ossDataFrameâ det vil bli nyttig for oss senere.
Jeg bruker substitusjon
{dt}i stedet for forespÞrselparameter%sikke fordi jeg er en ond Pinokkio, men fordipandaskan ikke hÄndterepymssqlog putter det til den sisteparams: Liste, selv om han veldig gjerne viltuple.
VÊr ogsÄ oppmerksom pÄ at utviklerenpymssqlhar besluttet Ä ikke stÞtte det lenger, og det er pÄ hÞy tid Ä flytte tilpyodbc.
La oss se hva Airflow har fylt funksjonsargumentene vÄre med:

Hvis det ikke er noen data, gir det ikke mening Ă„ fortsette. Men det er heller merkelig Ă„ si at innlastingen var vellykket. Men dette er heller ikke en feil. Ă h, hva skal vi gjĂžre?! Her er hva:
if df.empty:
raise AirflowSkipException('Ingen rader Ă„ laste')AirflowSkipException vil si at det ikke er noen feil, og at vi hopper over oppgaven. I grensesnittet vil det ikke vĂŠre en grĂžnn og ikke en rĂžd firkant, men i stedet en rosa.
La oss gi dataene vÄre flere kolonner:
df['etl_source'] = src_schema
df['etl_id'] = session.id
df['hash_id'] = hash_pandas_object(df[['etl_source', 'id']])Nemlig:
- DB-en som vi har hentet bestillingene fra,
- Identifikatoren for innlastingsÞkten vÄr (den vil vÊre forskjellig for hver oppgave),
- Hash fra kilden og bestillingsidentifikatoren â slik at vi i den endelige databasen (hvor alt samlet i Ă©n tabell) har en unik bestillingsidentifikator.
Det gjenstĂ„r ett skritt: laste alt inn i Vertica. Ironisk nok er en av de mest effektive mĂ„tene Ă„ gjĂžre dette pĂ„ â via CSV!
# Export data to CSV buffer
buffer = StringIO()
df.to_csv(buffer,
index=False, sep='|', na_rep='NUL', quoting=csv.QUOTE_MINIMAL,
header=False, float_format='%.8f', doublequote=False, escapechar='\')
buffer.seek(0)
# Push CSV
target_conn = VerticaHook(vertica_conn_id=target_conn_id).get_conn()
copy_stmt = f"""
COPY {target_table}({df.columns.to_list()})
FROM STDIN
DELIMITER '|'
ENCLOSED '"'
ABORT ON ERROR
NULL 'NUL'
"""
cursor = target_conn.cursor()
cursor.copy(copy_stmt, buffer)- Vi lager en spesialmottaker
StringIO. pandasvil vennligst samle vÄrDataFramesomCSV-linjer.- Vi Äpner forbindelsen til vÄr kjÊre Vertica ved hjelp av et hook.
- Og nÄ ved hjelp av
copy()sender vi dataene vÄre direkte til Vertica!
Vi henter hvor mange linjer som ble lastet opp fra driveren, og forteller sesjonslederen at alt er OK:
session.loaded_rows = cursor.rowcount
session.successful = TrueDet var det.
I produksjon oppretter vi mÄltabellen manuelt. Her har jeg tillatt meg en liten automasjon:
create_schema_query = f'CREATE SCHEMA IF NOT EXISTS {target_schema};'
create_table_query = f"""
CREATE TABLE IF NOT EXISTS {target_schema}.{target_table} (
id INT,
start_time TIMESTAMP,
end_time TIMESTAMP,
type INT,
data VARCHAR(32),
etl_source VARCHAR(200),
etl_id INT,
hash_id INT PRIMARY KEY
);"""
create_table = VerticaOperator(
task_id='create_target',
sql=[create_schema_query,
create_table_query],
vertica_conn_id=target_conn_id,
task_concurrency=1,
dag=dag)Jeg bruker
VerticaOperator()til Ă„ opprette databasen og tabellen (hvis de ikke allerede finnes, selvfĂžlgelig). Det viktigste er Ă„ plassere avhengighetene riktig:
for conn_id, schema in sql_server_ds:
load = PythonOperator(
task_id=schema,
python_callable=workflow,
op_kwargs={
'src_conn_id': conn_id,
'src_schema': schema,
'dt': '{{ ds }}',
'target_conn_id': target_conn_id,
'target_table': f'{target_schema}.{target_table}'},
dag=dag)
create_table >> loadOppsummering
â Vel, â sa musen, â er det ikke sant, nĂ„
Har du funnet ut at jeg er det mest skremmende dyret i skogen?
Julia Donaldson, «Gruffalo»
Jeg tror at hvis jeg og kollegene mine hadde arrangert en konkurranse: hvem som raskest kan sette opp og kjÞre en ETL-prosess fra bunnen av: de med sine SSIS og mus, og jeg med Airflow⊠Og sÄ sammenlignet vi lettheten av vedlikehold⊠Jeg tror du vil vÊre enig i at jeg ville overgÄtt dem pÄ alle fronter!
Hvis vi skal bli litt mer seriĂžse, sĂ„ har Apache Airflow â takket vĂŠre beskrivelsen av prosessene som programkode â gjort jobben min mye enklere og mer behagelig.
Den ubegrensede utvidbarheten: bĂ„de nĂ„r det gjelder plugins og som en predisposisjon for skalerbarhet â gir deg muligheten til Ă„ bruke Airflow nesten overalt: enten i den komplette syklusen av innsamling, forberedelse og behandling av data, eller til og med til Ă„ skyte opp raketter (til Mars, selvfĂžlgelig).
Avsluttende del, informativ
Fallgruvene vi har samlet for deg
start_date. Ja, dette er allerede et lokalt meme. Gjennom hovedargumentet til dagstart_dategĂ„r alle. Kort fortalt, hvis du angir istart_dateden nĂ„vĂŠrende datoen, og ischedule_intervalâ Ă©n dag, vil DAG bli kjĂžrt i morgen tidligst.start_date = datetime(2020, 7, 7, 0, 1, 2)Og sĂ„ flere problemer.
Det er ogsÄ knyttet til en annen feil under kjÞring:
Task is missing the start_date parameter, som oftest indikerer at du har glemt Ä knytte til operatÞren i dag.- Alt pÄ én maskin. Ja, bÄde databasen (til Airflow selv og vÄr innpakning), webserveren, planleggeren, og arbeidere. Og det fungerte faktisk. Men etter hvert som antallet oppgaver i tjenestene Þkte, og nÄr PostgreSQL begynte Ä levere svar med indeksen pÄ 20 ms i stedet for 5 ms, tok vi det og fjernet det.
- LocalExecutor. Ja, vi bruker det fremdeles, og vi har allerede nÄdd kanten av avgrunnen. LocalExecutor har vÊrt tilstrekkelig for oss, men nÄ er det pÄ hÞy tid Ä utvide med minst én arbeider, og vi mÄ gjÞre en innsats for Ä flytte til CeleryExecutor. Og ettersom det kan brukes selv pÄ én maskin, er det ingenting som hindrer bruken av Celery selv ikke pÄ serveren, som «selvfÞlgelig aldri kommer til Ä gÄ i produksjon, jeg lover!»
- Ikke bruk innebygde verktĂžy:
- Connections for Ă„ lagre legitimasjon for tjenester,
- SLA Misses for Ä hÄndtere oppgaver som ikke har fullfÞrt i tide,
- XCom for Ă„ utveksle metadata (jeg sa metadata!) mellom oppgavene i dag.
- Misbruk av e-post. Hva skal man si? Det ble satt opp varsler for alle gjentakelser av nedlagte oppgaver. NĂ„ har jeg over 90k e-poster fra Airflow i min arbeids-Gmail, og nettgrensesnittet til e-posten nekter Ă„ ta imot og slette mer enn 100 om gangen.
Flere fallgruver:
VerktĂžy for enda stĂžrre automatisering
For at vi skal kunne jobbe mer med hodet enn med hendene, har Airflow forberedt dette for oss:
- â det har fortsatt status som eksperimentelt, noe som ikke hindrer det i Ă„ fungere. Med det kan man ikke bare hente informasjon om DAGer og oppgaver, men ogsĂ„ stoppe/starte en DAG, lage en DAG-kjĂžring eller en pulje.
- â mange verktĂžy er tilgjengelige via kommandolinjen, som ikke bare er upraktiske Ă„ bruke gjennom WebUI, men ikke eksisterer i det hele tatt. For eksempel:
backfillsom er nÞdvendig for Ä kjÞre oppgavene pÄ nytt.
For eksempel, analytikerne kom, og sier: «à h, du har tull i dataene fra 1. til 13. januar! Fiks det, fiks det, fiks det!». Og du sier:airflow backfill -s '2020-01-01' -e '2020-01-13' orders- Vedlikehold av databasen:
initdb,resetdb,upgradedb,checkdb. run, som lar deg kjĂžre en enkelt instans av oppgaven, uten Ă„ ta hensyn til alle avhengigheter. Dessuten kan den kjĂžres gjennomLocalExecutor, selv om du har et Celery-klynge.- Det gjĂžr omtrent det samme som
test, bare at den ikke skriver noe i databasen. connectionslar deg massivt opprette tilkoblinger fra shell.
- â en ganske hardcore mĂ„te Ă„ interagere pĂ„, som er ment for plugins, og ikke for Ă„ rote med det manuelt. Men hvem kan hindre oss i Ă„ gĂ„ til
/home/airflow/dags, kjÞreipythonog begynne Ä eksperimentere? Man kan for eksempel eksportere alle tilkoblinger med koden:from airflow import settings from airflow.models import Connection fields = 'conn_id conn_type host port schema login password extra'.split() session = settings.Session() for conn in session.query(Connection).order_by(Connection.conn_id): d = {field: getattr(conn, field) for field in fields} print(conn.conn_id, '=', d) - Tilkobling til Airflows metadata-database. Jeg anbefaler ikke Ä skrive til den, men Ä hente tilstanden pÄ oppgaver for forskjellige spesifikke metrikker er mye raskere og enklere enn gjennom noen av APIene.
La oss si, ikke alle oppgavene vÄre er idempotente, og de kan av og til feile, og det er greit. Men flere feil er mistenkelig, og det bÞr sjekkes.
Forsiktig, SQL!
MED sist_execution AS ( SELECT task_id, dag_id, execution_date, state, row_number() OVER ( PARTITION BY task_id, dag_id ORDER BY execution_date DESC) AS rn FROM public.task_instance WHERE execution_date > now() - INTERVAL '2' DAGAR ), failure AS ( SELECT task_id, dag_id, execution_date, state, CASE WHEN rn = row_number() OVER ( PARTITION BY task_id, dag_id ORDER BY execution_date DESC) THEN TRUE END AS last_fail_seq FROM sist_execution WHERE state IN ('failed', 'up_for_retry') ) SELECT task_id, dag_id, count(last_fail_seq) AS unsuccessful, count(CASE WHEN last_fail_seq AND state = 'failed' THEN 1 END) AS failed, count(CASE WHEN last_fail_seq AND state = 'up_for_retry' THEN 1 END) AS up_for_retry FROM failure GROUP BY task_id, dag_id HAVING count(last_fail_seq) > 0
Lenker
Og selvfĂžlgelig de fĂžrste ti lenkene fra Googles sĂžkeresultat â innholdet i mappen Airflow fra bokmerkene mine.
- â selvsagt mĂ„ man begynne med den offisielle dokumentasjonen, men hvem leser bruksanvisninger?
- â men du bĂžr i det minste lese anbefalingene fra skaperne.
- â begynnelsen: brukergrensesnittet med bilder
- â grunnleggende begreper er godt beskrevet, hvis (mot alle odds!) du ikke forsto noe av det jeg sa.
- â en kort guide for Ă„ sette opp en Airflow-klynge.
- â omtrent en like interessant artikkel, bare mer formalitet og fĂŠrre eksempler.
- â om samarbeidet med Celery.
- â om idempotens av oppgaver, lasting etter ID i stedet for dato, transformasjoner, filstruktur og andre interessante ting.
- â avhengigheter mellom oppgaver og Trigger Rule, som jeg bare nevnte i forbifarten.
- â hvordan man overkommer noen «fungerer som planlagt» med planleggeren, laste tapt data og prioritere oppgaver.
- â nyttige SQL-spĂžrringer til Airflows metadata.
- â det er en nyttig seksjon om Ă„ lage egendefinert sensor.
- â en kort interessant notis om Ă„ bygge infrastruktur pĂ„ AWS for Data Science.
- â vanlige feil (nĂ„r noen faktisk ikke leser instruksjoner).
- â smil, se hvordan folk krangler med passordlagring, nĂ„r man bare kan bruke Connections.
- â implisitt DAG-omdirigering, kontekstoverfĂžring i funksjoner, igjen om avhengigheter, og dessuten om Ă„ hoppe over kjĂžringer av oppgaver.
- â om bruken av
standardargumenterogparamsi malene, samt om Variabler og Tilkoblinger. - â en fortelling om hvordan planleggeren er gjort klar for Airflow 2.0.
- â en litt utdatert artikkel om distribusjon av vĂ„r klynge i
docker-compose. - â dynamiske oppgaver ved hjelp av maler og kontekstoverfĂžring.
- â standard- og tilpassede varsler via e-post og Slack.
- â Forgrening av oppgaver, makroer og XCom.
Og lenkene som brukes i artikkelen:
- â tilgjengelige for bruk i malene.
- â Vanlige feil ved opprettelse av DAG-er.
- â
docker-composefor eksperimentering, feilsĂžking og mer. - â Python-wrapper for Telegram REST API.
Kilde: habr.com




