% autor: Fridrich David

% Pro kompilaci po částech (viz projekt.tex), nutno odkomentovat a upravit
%\documentclass[../projekt.tex]{subfiles}
%\begin{document}

\chapter{Úvod}

V posledních letech se ``cloud computing'' stal populárnějším než kdy dřív. Možnost nechat uživatele využívat zdroje a služby bez fyzické infrastruktury. Jednou z takových platforem je právě open--source projekt Kubernetes, systém orchestrace kontejnerů, který automatizuje nasazení (deployment), škálování a správu kontejnerizovaných aplikací.

Jednim z problémů správy těchto aplikací je zajištění, že budou škálovány na správny počet instancí podle požadavků. Z tohoto důvodu existuje objekt HPA na platformě Kubernetes, který řeší tento problém tak, že škáluje podle lokálních zdrojů jako využití CPU nebo paměti samotných objektů. HPA však má své implementační limity, např.: malá možnost konfigurace, neschopnost škálovat podle jakýchkoliv komplexních formulí či pravidel nebo neschopnost škálovat až na 0 replik pro šetření zdrojů.

Za účelem vyřešení některých nedostatků vznikl projekt KEDA, který dovoluje implementovat externí škálovací komponenty, sbírat metriky ze všech připojených komponent a jednoduše je odesílat objektu HPA pro výpočet replik škálované aplikace. Tato práce navrhuje vylepšení infrastruktury KEDA, které umožní posbírané metriky z externích škálovacích komponent zpracovávat pomocí uživatelem definovaných formulí jako průměr, sčítaní, odečítaní, podmíněné výrazy nebo vše dohromady a následně škálovat podle vyhodnoceného výrazu. Za účelem zvýšení flexibility navíc k tomu navrhuje nové rozhraní externích výpočtů, které dovolí uživateli implementovat vlastní škálovací logiku, propojit ji pomocí technologie gRPC jako klient--server dvojici, kde KEDA je klient požadující metriky od uživatelem definovaného gRPC serveru a modifikovat či mutovat metriky pro specifické případy uživatele.

\chapter{Kubernetes}
\label{k8s}
Kubernetes (zkráceně \texttt{K8s}) je open-source projekt, který slouží jako platforma pro orchestraci kontejnerů\footnote{Orchestrace kontejnerů automatizuje nasazení a řízení kontejnerů a jejich životní cyklus \url{https://www.ibm.com/topics/container-orchestration}}. 

\section{Základní princip}

Kubernetes je ``správce'' clusterů. Je založen na kontejnerech (podkapitola \ref{container-runtime}) a je schopen automatizovat nasazení, řízení, správu a škálování kontejnerizovaných aplikací \cite{cite:kubernetes}. Tyto aplikace běží na jednom nebo více uzlech (uzly jsou fyzické či virtuální stroje). Každý uzel clusteru většinou obsahuje kontejnerové prostředí (container runtime) jako Docker a ostatní potřebné komponenty pro podporu kontejnerizovaných aplikací včetně úložného prostoru nebo sítové konfigurace. Při nasazení aplikace na Kubernetes se automaticky naplánuje spuštění potřebných kontejnerů na jednom či více uzlech. To je rozhodováno podle informací jako připojení k síti, dostupnost zdrojů nebo vyrovnávání pracovní zátěže (anglicky workload balancing). Platforma mimo jiné monitoruje tyto aplikace a jejich komponenty a může přesouvat kontejnery mezi uzly nebo vytvářet zcela nové uzly pokud je to vyžadováno. 

\subsection{Kontejnerizované prostředí}
\label{container-runtime}

Kontejner je základní stavební jednotkou pro ``cloud--native'' aplikace\footnote{Aplikace navržené tak aby využívali cloudovou infrastrukturu a služby jako kontejnery nebo Kubernetes prostředí} založené na Kubernetes. Pro vytvoření kontejneru je potřeba mít tzv. ``container image'' neboli volně přeloženo jako kontejnerový obraz. Tento obraz je samostatný spustitelný balíček, který obsahuje zdrojový kód, definované prostředí, systémové nástroje, knihovny, specifické nastavení a vše ostatní co je potřebné pro úspěšné spuštění přípustného softwaru. 

\begin{figure}[H]
    \centering
    \includesvg[width=1\textwidth]{obrazky-figures/container_evolution.svg}
    \caption{Porovnání způsobu nasazení aplikací. Zdroj:\url{https://kubernetes.io/docs/concepts/overview/}}
    \label{fig:kubernetes-deploment-comparison}
\end{figure}

Pro porovnání s Javou, kontejnerový obraz je jako třída a kontejner je jako objekt. Stejně jako můžeme rozšířit třídy, abychom efektivně upravili jejich chování, můžeme vytvářet kontejnerové obrazy, které nějak rozšiřují jiné obrazy. Podobně jako můžeme dělat kompozici objektů a využívat jejich funkčnost, můžeme využívat kompozici kontejnerů tím, že je dáme dohromady do jednoho Podu \cite{cite:roland-book}.

Využívání kontejnerových obrazů je velmi vhodné hned z několika důvodů. Obsahuje vše potřebné pro běh softwaru, má definované závislosti, je možné ho předem nakonfigurovat a lehce se škáluje. Jakmile je obraz vytvořen, nelze nijak změnit. Toto je hojně využitelné např. při vývoji aplikace, kde je zachována konzistence napříč všemi možnými vývojovými prostředími a při vývoji, testování nebo v produkci bez potřeby jakékoliv další konfigurace. Obrazy je převážně vytvářeny pomocí souborů typu Dockerfile (výpis \ref{listing:dockerfile}), což je textový soubor, který obsahuje specifické příkazy a konfigurace jak obraz vytvořit. Vytvořený objekt lze uložit do registru kontejnerů, databáze dostupných kontejnerových obrazů. Mezi často používané patří registr Docker Hub nebo Google Container Registry (GCR). Tyto registry mohou být jak veřejné tak privátní a sdílení či opakované používání obrazů je díky nim velmi snadné a efektivní.

\begin{lstlisting}[label={listing:dockerfile},caption={Příklad jednoduchého Dockerfilu s Ubuntu prostředím, který zkompiluje golang projekt v aktuální složce s jeho potřebnými závislostmi a případně i spustí kontejner s tímto nově vytvořeným obrazem}]
FROM golang AS builder
COPY . /build
WORKDIR /build
RUN go build -o main .
FROM ubuntu
COPY --from=builder /build/main /main
COPY --from=builder /build/env /env
CMD ["./main"]
\end{lstlisting}

Samotný kontejner je potom instance tohoto kontejnerového obrazu a je vytvořen při jeho spuštění. Je to izolované a bezpečné prostředí, které obsahuje vše potřebné pro spuštění aplikace jako zdrojový kód, knihovny a další potřebné nástroje. Kontejnery jsou podobné virtuálním strojům (VS), ale na rozdíl od nich kontejnery sdílí jádro operačního systému na kterém běží. I přes to jsou aplikace v kontejnerech izolovány jak od ostatních aplikací, tak od svého operačního systému. Díky tomuto využívají méně zdrojů a považují se za ``lightweight''. Podobně jako VS mají např. svůj souborový systém (filesystem), sdílí CPU nebo paměť. Pro jejich vytváření, správu a běh se využívá technologií jako Docker nebo \texttt{K8s}. Kontejner je možné kdykoliv bezpečně smazat a nastartovat jiný se stejným kontejnerovým obrazem, čímž zaručíme konzistenci prostředí uvnitř samotného kontejneru. Toto kontejnerové prostředí je izolované od svého hostujícího operačního systému a za normálních podmínek není možné z kontejneru interagovat s hostujícím OS, avšak specifická zranitelnost kontejneru nebo jádra OS by mohla dovolit používanému kontejneru přístup k jeho hostovi pomocí tzv. ``privilege escalation attack'' nebo při špatné konfiguraci samotného prostředí či jejich sdílených zdrojů. 

\subsection{Objekty}
\label{k8s-object}
Jedním ze základních prvků Kubernetes jsou tzv. Kubernetes objekty. \texttt{K8s} objekt je perzistentní entita, která reprezentuje stav specifické komponenty v Kubernetes clusteru. Typickým příkladem takových objektů jsou např.:
\begin{itemize}
    \item HPA -- automatizuje škálování aplikací (podkapitola \ref{HPA}) 
    \item Pod -- nejmenší nasaditelný prvek v \texttt{K8s}, který může obsahovat jeden nebo více kontejnerů
    \item Deployment -- objekt vyšší úrovně, jehož účelem je řízení a správa množiny Podů
    \item PersistentVolume -- perzistentní úložiště uvnitř Kubernetes clusteru
\end{itemize}
    
Objekty se dají vytvořit, upravit nebo smazat přes Kubernetes API server, případně pomocí nástroje na příkazové řádce \texttt{kubectl}, který komunikuje s Kubernetes API místo uživatele.

Pro vytvoření objektu je potřeba definovat jeho konfiguraci pomocí YAML nebo JSON souboru, případně přes zmíněné \texttt{K8s} API.

\begin{lstlisting}[label={k8s-objekt-def},caption={Příklad definice Podu pomocí yaml souboru},language=yaml]
apiVersion: v1
kind: Pod   # type of object 
metadata:
  name: my-pod
spec:
  containers:
  - name: my-container
    image: nginx:latest   #image to run in container
    ports:
    - containerPort: 80
    resources:   # resources limitations
      requests:
        cpu: "100m"
        memory: "256Mi"
      limits:
        cpu: "200m"
        memory: "512Mi"
\end{lstlisting}

\subsubsection{Definice vlastních objektů}

Kubernetes poskytuje API rozhraní pro správu a nasazení kontejnerizovaných aplikací za pomocí předdefinovaných objektů a řady typů zdrojů, které jsou nativní pro platoformu. V některých situacích uživatelé potřebují objekty se specifickými definicemi a funkcionalitou, které v Kubernetes neexistují. V takových případech \texttt{K8s} nabízí možnost rozšíření API rozhraní definováním vlastních zdrojů. Typ takového vlastního objektu se nazývá \texttt{CustomResourceDefinition} \cite{cite:kubernetes-crd}.

CRD se skládá z YAML souboru, který definuje nový Kubernetes objekt se všemi potřebnými pravidly a dodatečnými koncovými body, které mají být dostupné. Uživatel musí vytvořít vlastní controller, který sleduje samotný objekt pro změny stavu a provádí specifické akce spojené s tímto objektem. 

\begin{lstlisting}[label={listing-crd},caption={Příklad definice vlastního CRD objektu ``Resources'' pro správu zdrojů uživatelské aplikace},language=yaml]
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: resources.myapp.example.com
spec:
  group: myapp.example.com
  version: v1
  scope: Namespaced
  names:
    plural: resources
    singular: resource
    kind: Resource
    shortNames:
    - res
  subresources:
    status: {}
  validation:
    openAPIV3Schema:
      type: object
      required:
      - metadata
      - spec
      properties:
        metadata:
          type: object
          properties:
            name:
              type: string
        spec:
          type: object
          properties:
            type:
              type: string
            replicas:
              type: integer
              minimum: 1
\end{lstlisting}

Příklad: Existuje aplikace, která spravuje různé druhy zdrojů jako databáze, cache, zprostředkovatele zpráv a podobně. CRD (výpis \ref{listing-crd}) zjednodušuje uživateli v takové aplikaci definování typů, které reprezentují tyto zdroje pro jednoduchou správu a škálování v clusteru.

Po úspěšném vytvoření CRD definice (výpis \ref{listing-crd}) v clusteru je možné pro uživatele vytvořit objekt typu ``Resource'' (výpis \ref{listing-crd-object}). Objekt má název \texttt{mydb} a obsahuje specifikace s typem databáze \texttt{spec/type} a počtem replik, které mají aktivně běžet v clusteru.

\begin{lstlisting}[label={listing-crd-object},caption={Ukázka definice objektu typu Resource definovaného pomocí CRD},language=yaml]
apiVersion: myapp.example.com/v1
kind: Resource   # custom type
metadata:
  name: mydb
spec:
  type: postgresql
  replicas: 3
\end{lstlisting}



\subsection{Controllery}
Jednou z jádrových složek platformy \texttt{K8s} a její architektury jsou tzv. ``Controllery'' (volně přeloženo jako ovladače). Jsou to softwarové cykly zodpovědné za životní cyklus komponent uvnitř clusteru. Neustále monitorují a porovnávají aktuální stav se stavem požadovaným a provádějí akce aby rovnost stavů zachovali, jinými slovy neustále se snaží přiblížit aktuální stav požadovanému aktualizací, modifikací, vytvářením nebo mazáním zdrojů které sledují.

Každy ovladač má specifickou sadu úkolů, které provádí pro zaručení udržení zdravého stavu. Např. ovladač typu ReplicaSet zajišťuje, že specifický počet replik objektu typu Pod je nasazen a aktivní po celou dobu, zatímco ovladač typu Deployment se stará o aktualizaci nových verzí objektu Pod. Controllery jsou navrženy tak, aby automatizovali běžné úkoly a řídili životní cyklus Kubernetes clusteru a všech jeho objektů. Mezi tyto úkoly patří:

\begin{itemize}
    \item Sledování stavu zdrojů, porovnávání a aktivní snaha změnit aktuální stav na požadovaný stav
    \item Škálování nahoru a dolů s ohledem na využití zdrojů či jiných metrik
    \item Automatické zotavení zdrojů při detekci chyb
    \item Garbage collection -- zodpovědnost za mazání objektů, které již nejsou potřeba např. zničení Podů, které jsou součástí Deploymentu, který byl smazán.
    \item Využití a monitorování logovacích nástrojů jako Prometheus pro sběr a analýzu záznamů a dat z clusteru
    \item Možnost připojení jiných nástrojů pro automatizaci zálohování a obnovení dat (např. Velero)
\end{itemize}

Běžné využití je spravování prostředků jako Deploymenty, ReplicaSety, Joby a další. Ovladače jsou z tohoto hlediska zodpovědné za to, aby požadovaný počet replik objektů byl aktivní a udržoval si ``healthy state'' (zdravý stav). V Kubernetes existují 2 druhy kontroly objektů. Živost a připravenost. Z angličtiny se překládá jako sonda a celý název je potom ``liveness probe'' a ``readiness probe''. Je to mechanismus, který pomáhá určit, zda aplikace v kontejneru úspěšně běží a je aktivní. Kubernetes kontrolery ze základu kontrolují jestli Pod běží a pokud ne, restartují všechny kontejnery uvnitř podle uvedené zásady pro restartování -- \texttt{restartPolicy}, což je volitelný údaj při definici Podu. V některých případech může Pod úspěšně běžet, ale aplikace uvnitř ne. V tomto případě může sonda živosti poskytnout doplňující informace. Sonda připravenosti potom udává, zda je aplikace připravena přijímat síťové požadavky. Pro aktivaci nějaké specifické kontroly ji stačí přidat do definice Podu (výpis \ref{pod-definition-with-health-check} níže) pod specifický kontejner s názvem struktury \texttt{livenessProbe}. 

\begin{lstlisting}[label={pod-definition-with-health-check},caption={Příklad definice Podu s kontrolou živosti. Zdroj: \url{https://komodor.com/blog/kubernetes-health-checks-everything-you-need-to-know/}},language=yaml]
apiVersion: v1
kind: Pod   # object of type Pod
metadata:
  labels:
    test: liveness
  name: liveness-http
spec:
  containers:
  - name: liveness
    image: k8s.gcr.io/liveness   # image for the container
    args:
    - /server
    livenessProbe:
      httpGet:
        path: /healthz
        port: 8080
      initialDelaySeconds: 3
      periodSeconds: 3
\end{lstlisting}

Kontrola stavu může být definována více způsoby a může být prováděna v intervalech. Při definici se zapíše koncový bod, případně příkaz který se má spustit. Příkaz může být uživatelem definovaný skript, který se spustí. V případě definice typu sondy s koncovým bodem může být využit HTTP požadavek nebo pokus o TCP spojení. Tyto typy podporují volitelnou možnost definovat jméno portu pro každý Pod a potom na něj referovat jeho jménem i v této kontrole. Očekává se tedy buď HTTP kód 200, navázání TCP spojení nebo výstupní kód příkazu 0, které určují úspěch. Jako poslední typ je kontrola stavu gRPC, která je již součástí Kubernetes od verze vyšší než 1.23 ze základu. Dalšími nastavitelnými metrikami jsou interval, který určuje dobu mezi 2 kontrolami a timeout který určuje jak dlouho bude controller čekat na provedení samotné kontroly. 

\subsubsection{Vlastní ovladače}

Kubernetes umožňuje uživateli přístup k API pomocí kterého může definovat své vlastní ovladače a rozšířit funkcionalitu \texttt{K8s}. Takové zdroje mohou být nakonfigurovány pro monitorování a provádění úkolů, které nejsou dostupné v samotném Kubernetes. Pro definici takového objektu musí uživatel napsat kus kódu, který interaguje s Kubernetes API serverem a monitoruje zdroje a jejich změnu stavu podle které následně provádí specifikované akce. Běžný způsob vytvoření vlastních zdrojů je pomocí Kubernetes knihoven, které poskytují abstrakční vrstvu pro interakci s API serverem.

\subsubsection{Správce kontrolerů}

Na nejvyšší řídící úrovni Kubernetes je tzv. správce kontrolerů, který spravuje životní cyklus všech ovladačů běžící v celém \texttt{K8s} clusteru. Je zodpovědný za nasazení, ukončení a aktualizaci těchto zdrojů dle potřeby. Poskytuje přehled životnosti a stavu všech kontrolerů na vysoké úrovni. Správce funguje jako sada procesů, které běží na řadě hlavních uzlů Kubernetes a je designovaná tak aby byla tolerantní k chybám.

\subsubsection{Designové Vzory}

Vzory jsou prověřené a ozkoušené způsoby implementace, které jsou nejlepší praxí. Vzory poskytují způsob struktury kódu a definování chování jednoduchý pro pochopení a testování. Mezi takové běžné designy patří:

\begin{itemize}
    \item Informátor -- vzor, který se používá pro monitorování změn Kubernetes zdrojů. Ukládá je do mezipaměti lokálně uvnitř ovladače a následně může podle těchto informací provádět změny bez dalšího API volání. 
    \item Smiřovatel -- Vzor porovnávající aktuální a požadovaný stav, který modifikuje objekty na základě tohoto rozdílu a stará se tak, aby objekty byly neustálne v požadovaném stavu a to i kdyby objekty byly modifikovány externě. Pokud jsou nějaké objekty monitorovány ovladačem tohoto typu, nelze je přepsat bez volání na Kubernetes API.
    \item Operátor -- používá se pro automatizaci a správu komplexních aplikací. Operátory využívají vlastní kontrolery pro změny v aplikaci a zařizují, že je neustále aktivní a běží správně v požadovaném stavu.
\end{itemize}

\section{Škálování}
\label{k8s-scaling}

Kubernetes podporuje automatizované škálování aplikací dvěma způsoby (obrázek \ref{fig:horizontal-vs-vertical-scaling}), horizontální (více v sekci \ref{HPA}), které upravuje počet podů, které je možné využívat a
vertikální (podkapitola \ref{VPA}), které popisuje škálování jako přidávání zdrojů již existujícím strojům. Kromě těchto dvou škálovacích objektů \texttt{K8s} podporuje také ``Cluster Autoscaling'', který umožňuje škálování samotných uzlů v clusteru.

\begin{figure}[H]
    \centering
    \includegraphics[width=1\textwidth]{obrazky-figures/horizontal-vs-vertical-scaling-diagram.png}
    \caption{Rozdíl horizontálního a vertikálního škálování. Zdroj:\url{https://www.section.io/blog/scaling-horizontally-vs-vertically/}}
    \label{fig:horizontal-vs-vertical-scaling}
\end{figure}


\subsection{HPA}
\label{HPA}
HPA (Horizontal Pod Autoscaler) je objekt, kterému lze definovat základní pravidla podle kterých následně škáluje aplikaci na základě využití CPU, paměti nebo vlastní metriky definované pro konkrétní aplikace \cite{cite:hpa-scaling}. Např. počet požadavků za sekundu, počet aktivních uživatelů nebo je možné využít zcela vlastní metriky.

\begin{figure}[H]
\centering
\includesvg[width=0.6\textwidth]{obrazky-figures/hpa_2.svg}
\caption{Vizualizace funkce HPA -- Horizontal Pod Autoscaler}
\end{figure}

Příklad jednoduchého YAML souboru (výpis níže \ref{HPA-def}), který definuje HPA objekt s názvem \texttt{my-hpa}, který škáluje objekt typu \texttt{Deployment} s názvem \texttt{my-deployment}. Minimum replik je nastaven na 2, maximum na 5. Metrika podle které bude objekt škálovat je jeho využití CPU a nastavuje cíl využití (utilization target) na 50\%. To znamená, že pokud využití všech podů pod objektem \texttt{my-deployment} překročí hodnotu 50\%, HPA zvýší počet replik postupně až na maximální počet.

\begin{lstlisting}[label={HPA-def},caption={Příklad definice HPA objektu},language=yaml]
apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
  name: my-hpa   # name of object
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-deployment
  minReplicas: 2   # min number of replicas, cant be lower
  maxReplicas: 5   # max number of replicas, cant exceed
  metrics:
  - type: Resource   # resource trigger
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 50
\end{lstlisting}

\subsubsection{Škálovací algoritmus}
\label{k8s-equation}

$desiredReplicas = ceil[currentReplicas * ( currentMetricValue / desiredMetricValue )]$
\vspace{0.5cm}

Tento algoritmus\footnote{algoritmus: \url{https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/\#algorithm-details}} popisuje způsob škálování, který se využívá v Kubernetes objektem HPA pro výpočet požadovaného počtu replik a nepřímo ho využívá i KEDA, jelikož si vytváří vlastní HPA objekty.

\begin{itemize}
    \item currentReplicas -- počet replik škálovaného objektu
    \item currentMetricValue -- aktuální hodnota monitorvané metriky (CPU využití, síťový provoz apod.)
    \item desiredMetricValue -- cílová hodnota metriky specifikované v konfiguraci pro HPA
    \item ceil -- funkce zaokrouhlující nahoru na nejbližší integer 
    \item desiredReplicas -- Výsledek rovnice, který určuje požadovaný počet replik
\end{itemize}

\subsection{VPA}
\label{VPA}
VPA (Vertical Pod Autoscaler) je objekt podobný HPA. Rozdílem je, že VPA monitoruje využití kontejnerů a  upravuje limity a požadavky (hodnoty ``requests'' a ``limits'' v YAML souboru (výpis \ref{k8s-objekt-def})) těchto kontejnerů, které jsou součástí podu. 

Podle výchozího nastavení Kubernetes každý kontejner má nastavené požadavky pro CPU a paměť podle šablony, ale tyto hodnoty nemusí vždy stačit při zvyšující se zátěži. Z tohoto důvodu, VPA objekt se využije pro sbírání informací o využití zdrojů v jednotlivých kontejnerech uvnitř podu a určí optimální hodnoty polí ``requests'' a ``limits''. Stejně tak umí VPA snížit tyto hodnoty, jestliže se zátěž sníží z důvodu šetření těchto zdrojů nebo je jejich možné využití jinde. Zkráceně, VPA upravuje využití zdrojů kontejnerů v Podu pro co nejvyšší efektivitu z metrik které mu jsou k dispozici.

\chapter{KEDA}
\label{keda}

Kubernetes Event-driven Autoscaling (KEDA)\footnote{https://keda.sh/} je open--source projekt, který slouží jako nádstavba Kubernetes pro škálování aplikací na základě počtu přijatých událostí (events) nebo zpráv z externích systémů. Externím systémem mohou být např. fronty zpráv (anglicky message queues) nebo toky událostí (event streams) \cite{cite:keda}. Ve vztahu k \texttt{K8s} je KEDA operátor, který rozšiřuje platformu o novou funkcionalitu. Komponenta může být přídána do každého Kubernetes clusteru. Využívá již existujících zdrojů v \texttt{K8s}, ale také definuje své vlastní. Pomocí CRD (Custom Resource Definition) vytváří nový Kubernetes objekt ScaledObject (podkapitola \ref{keda-scaledObject}) (zkráceně SO), který definuje specifikace chování škálování (kapitola \ref{keda-scaling}) podle konkrétních zdrojů událostí. Flexibilní architektura (kapitola ref{keda-architecture}) dovoluje využití řady zdrojů událostí nebo systému zpráv, jako například MySQL, Kafka nebo Azure Event Hubs. Mimo vlastní zdroje je možné škálovat na základě metrik známých v samotném Kubernetes, jako CPU nebo využití pamětí. Toho je docíleno skrz využití objektu HPA, který je vytvořen dynamicky podle SO objektu. Operátor podporuje více než 60 škálovacích komponent.

Operátor při instalaci vytvoří 4 CRD objekty (custom resource definition) umožňující propojit zdroje událostí (a jejich autentizaci) s Kubernetes objekty jako Deployment, Job nebo jiný vlastní zdroj pro škálování. 
\begin{itemize}
    \item \texttt{scaledobjects.keda.sh} -- reprezentuje požadované mapování mezi zdrojem událostí a \texttt{K8s} objektem Deployment
    \item \texttt{scaledjobs.keda.sh} -- velmi podobný prvnímu, mapuje zdroje s objektem Job
\end{itemize}
Tyto 2 CRD mohou ještě dodatečně referencovat vlastní KEDA zdroje
\begin{itemize}
    \item \texttt{triggerauthentications.keda.sh} 
    \item \texttt{clustertriggerauthentications.keda.sh}
\end{itemize}
obsahující autentizační konfigurace, tajemství nebo důvěrné informace (secret) pro pozorování zdrojů událostí~\cite{cite:keda-architecture}.

\section{Architektura}
\label{keda-architecture}

Jádro architektury je složeno z několika komponent, které dohromady umožňují využití automatického škálovacího mechanismu. KEDA Controller (kapitola \ref{keda-controller}) je zodpovědný za implementaci logiky škálování a monitorování externích zdrojů a upravuje počet replik na základě získaných metrik. Slouží jako kontroler životního cyklu všech KEDA komponent. KEDA Metrics Server sbírá metriky z různých zdrojů, jak nativních pro Kubernetes, tak externích. Tyto metriky dále zpracovává a poskytuje je pomocí API. Novou komponentou je tzv. ``Admission Webhooks'' (kapitola \ref{keda-webhooks}), která existuje pro validaci změněných nebo nově vytvořených objektů za účelem předejití nasazení těchto objektů se špatnou konfigurací. Po instalaci KEDA je funkčnost demonstrována pomocí komponent \texttt{keda-admission}, \texttt{keda-metrics-apiserver} a \texttt{keda-operator} \cite{cite:keda-architecture}, které běží jako objekty typu Deployment na clusteru pří úspěšné instalaci v jmenném prostoru ``keda''.

%%% SVG FILE
% \begin{figure}[H]
%     \centering
%     \includesvg[width=1\textwidth]{obrazky-figures/keda-arch.svg}
%     \caption{Architektura KEDY. Zdroj:\url{https://keda.sh/docs/2.10/concepts/\#architecture}}
%     \label{fig:keda-architecture}
% \end{figure}

%%% PNG FILE
\begin{figure}[H]
    \centering
    \includegraphics[width=1\textwidth]{obrazky-figures/keda-arch.png}
    \caption{Architektura KEDY. Zdroj:\url{https://keda.sh/docs/2.10/concepts/\#architecture}}
    \label{fig:keda-architecture}
\end{figure}

\subsection{Controller}
\label{keda-controller}

KEDA Controller je základní součást architektury. Zajišťuje zdravý životní cyklus všech KEDA komponent. Je zodpovědný za instalaci a případné aktualizace samotných komponent, jejich konfiguraci a správu. Komponenty monitoruje a v případě chyby se ji snaží vyřešit např. restartováním Podu. Pokud existují jíž nepotřebné zdroje KEDY, operátor je zodpovědný za jejich mazání a uvolňování. Stejně tak je odpovědný za nakonfigurované a škálované objekty typu Deployment pomocí triggerů. Konstantně kontroluje stav metrických hodnot a v případě změny inicializuje akci komunikací s Kubernetes API serverem se novým počtem replik.  

\subsection{Metrics Server}
\label{keda-metrics-server}

Další důležitá komponenta se jmenuje KEDA Metrics Server (zkráceně Keda MS). Je zodpovědná za sbírání metrik jak z Kubernetes-nativních tak i externích systémů a zajišťuje k nim přístup. MS je nasazen separátně jako samostatný Pod, který zpřístupňuje RESTful API pro ostatní komponenty platformy. Metrics Server strukturalizuje a normalizuje tyto sesbírané metriky do společného formátu tak, aby je mohl Controller (kapitola \ref{keda-controller}) využít. KEDA MS podporuje škálu zdrojů metrik, jako Prometheus nebo Azure Monitor a díky tomu je flexibilní a dovoluje vybrat nejvhodnější zdroj pro konkrétní případ. Mimo externí události dovoluje škálovat podle základních Kubernetes zdrojů, tedy vše lze konfigurovat na jednom místě. Zvyšuje výkon aplikací efektivním škálováním a zjednodušuje samotnou konfiguraci s využitím YAML souborů.

\subsection{Objekt ScaledObject}
\label{keda-scaledObject}

ScaledObject (SO) je Kubernetes CRD (Custom Resource Definition) objekt, který může být vytvořen uživatelem přímo nebo nepřímo daným Controllerem na základě dané konfigurace. Pokud chce uživatel použít vlastní škálování pro svůj projekt, vytvoří komponentu SO, pomocí které definuje konfiguraci škálování a samotné spouštěče. Konkrétně tedy SO definuje jak využít automatický škálovací mechanismus na základě nastavených spouštěčů. Je to v podstatě konfigurační objekt, který říká KEDĚ jak monitorovat specifické metriky a jak škálovat odpovídající Kubernetes zdroje na základě těchto metrik. YAML soubor definující SO objekt obsahuje, stejně jako ostatní objekty v Kubernetes, základní metadata o sobě samotném, jako název nebo jmenný prostor ve kterém se nachází a dále popisuje konfiguraci škálování, objektu HPA, který bude na základě toho vygenerován, nebo dalších pokročilých nástrojů, jako např. záložní mechanismus škálování neboli \texttt{fallback} (podkapitola \ref{keda-fallback}). Všechny důležité škálovací informace jsou obsaženy v poli \texttt{spec} YAML souboru (výpis níže \ref{listing:scaledobject}).

\begin{lstlisting}[label={listing:scaledobject},caption={YAML definice objektu \texttt{ScaledObject} s názvem \texttt{so-composite} v jmenném prostoru \texttt{test}. Stará se o škálování Deploymentu \texttt{depl-composite}},language=yaml]
apiVersion: keda.sh/v1alpha1
kind: ScaledObject   # type of object
metadata:
  name: so-composite
  namespace: test   # namespace where to deploy this SO
  labels:
    app: depl-composite
spec:
  advanced:
    horizontalPodAutoscalerConfig:
      behavior:
        scaleDown:
          stabilizationWindowSeconds: 1
  pollingInterval: 1   # how long to wait before asking for metrics again
  scaleTargetRef:   # what object is scaled by this SO
    name: depl-composite
  minReplicaCount: 0
  maxReplicaCount: 5
  cooldownPeriod: 1
  triggers:   # specified triggers
  - type: kubernetes-workload
    metadata:
      podSelector: 'pod=workload-test'
      value: '1'
  - type: metrics-api
    metadata:
      targetValue: "2"
      url: "https://mockbin.org/bin/336a8d99-9e09-4f1f-979d-851a6d1b1423"
      valueLocation: "tasks"
\end{lstlisting}

V YAML souboru jsou k dispozici veškeré informace. Tento konfigurační soubor YAML vytvoří objekt typu \texttt{ScaledObject} se jménem \texttt{so--composite} nasazený ve jmenném prostoru \texttt{test}. Pole spec následně obsahuje
\begin{itemize}
    \item \texttt{horizontalPodAutoscalerConfig} -- pokročilé nastavení vygenerovaného HPA, kterému říká pomocí pole \texttt{stabilizationWindowSeconds} dobu, jak dlouho má čekat než začne znovu škálovat dolů po předchozím škálování (pole \texttt{scaleDown} a \texttt{scaleUp} rozlišují chování podle směru -- škálování nahoru či dolů). Tedy pokud nastane nárust žádostí v jeden moment a počet replik se změní např. na maximální počet \texttt{5}, tato hodnota udává jak dlouho má Controller čekat, než naškáluje aplikaci zpět dolů. Toto slouží pro předejití škálování podle instantních růstů nebo pádů žádostí a stabilizuje tak aplikaci
    \item Pole \texttt{pollingInterval} udává frekvenci dotazů na metriky např. pokud chce uživatel synchronizovat frekvenci s produkcí nové metriky od externího zdroje
    \item \texttt{scaleTargetRef} referencuje jménem objekt typu Deployment, který bude škálován podle pravidel tohoto konfiguračního souboru. 
    \item \texttt{minReplicaCount} a \texttt{maxReplicaCount} určuje minimální a maximální počet replik, na které je možné aplikaci naškálovat. Počet instancí nikdy nepřekročí tyto hodnoty. 
\end{itemize}
Na spodní části YAML souboru se nachází pole \texttt{triggers} popisující spouštěče konkrétních metrik.
\begin{itemize}
    \item Typ \texttt{kubernetes--workload} umožňuje škálování aplikace podle jiného škálovatelného objektu v specifickém poměru jako 1 ku 1, 2 ku 1 a podobně. Tedy pokud se počet replik specifikovaného objektu zvýší, změní se s ním i počet mé aplikace dle daného poměru
    \item Druhý typ \texttt{metrics--api} získává metriku z dané url adresy pomocí API. Toto může být např. v JSON formátu, kde potom \texttt{valueLocation} je cesta k JSON hodnotě určené pomocí GJSON notace\footnote{GJSON je Golang balíček, který implementuje způsob získávání informací z JSON \url{https://github.com/tidwall/gjson\#path-syntax}}
\end{itemize}
        
\subsubsection{Spouštěč}
\label{keda-trigger}

Spouštěč neboli trigger je součástí YAML souboru pro vytvoření objektu ScaledObject, který definuje škálovací chování aplikace. Každý spouštěč má odpovídající škálovací komponentu (scaler), kterých KEDA aktuálně podporuje 61 a dělí se do 2 kategorií: ``Resource'' a ``External''. První typ označuje škálování podle zdrojů již existujících v Kubernetes, tedy podle využití CPU nebo paměti. Kromě základní funkcionality kterou posytují objekty HPA a VPA je pomocí KEDY možné docílit lepší konfigurace při formulaci požadavků škálování nebo definování minimálního a maximálního počtu replik. Externí scalery jsou potom rozšířením, které KEDA nabízí. Všechny scalery definují, jak má cluster reagovat na události vygenerované spouštěčem. Mezi spouštěče patří např. Apache Kafka, Cron plánovač, CPU a Paměť, Google Cloud Platform Storage, MongoDB, MySQL, Prometheus nebo RabbitMQ Queue. Je možné nakonfigurovat více spouštěčů zároveň a přizpůsobit tím odpovídající škálovací mechanismus potřebám konkrétní aplikace.

\begin{lstlisting}[label={listing:triggers-kafka-kubeworkload},caption={2 spouštěče typů \texttt{kafka} a \texttt{kubernetes-workload}},language=yaml]
triggers:
- type: kafka    # kafka trigger
  metadata:
    brokers: "kafka-broker-1:9092,kafka-broker-2:9092"
    topic: my-kafka-topic
    consumerGroup: my-kafka-consumer-group
    lagThreshold: "10"
- type: kubernetes-workload   # kubernetes-workload trigger
  metadata:
    podSelector: 'pod=workload'
    value: '1'
\end{lstlisting}

\begin{lstlisting}[label={listing:triggers-3},caption={3 spouštěče typu kafka, který škáluje podle počtu zpráv v specifickém Kafka topicu, Azure Queue spouštěč, který škáluje aplikaci podle zpráv v specifické Azure frontě a Prometheus trigger, který škáluje podle počtu HTTP požadavků na specifický koncový bod. Všechny 3 obsahuji \texttt{authenticationRef}, což referencuje jméno objektu, který musí existovat a obsahovat pověření k přístupu k danému triggeru.},language=yaml]
triggers:
- type: kafka
  metadata:
    topic: my-topic
    bootstrapServers: my-kafka-broker:9092
    consumerGroup: my-consumer-group
  authenticationRef:
    name: my-kafka-auth-secret
- type: azure-queue
  metadata:
    queueName: my-queue
    storageAccountName: my-storage-account
    storageAccountKey: my-storage-account-key
  authenticationRef:
    name: my-azure-auth-secret
- type: prometheus
  metadata:
    query: 'sum(rate(http_requests_total{deployment="my-deployment"}[2m]))'
  authenticationRef:   # reference to authentication object by name
    name: my-prometheus-auth-secret
\end{lstlisting}

\subsection{Validační Webhooky}
\label{keda-webhooks}

Komponenta využívá mechanismus ověřovacích kontrolerů na platformě Kubernetes. Tento kontroler popisuje kus kódu, který zachytí příchozí požadavky na Kubernetes API server dříve, než je objekt nasazen do clusteru, ale zároveň až po autentizaci a autorizaci. Kontrolery mají více typů -- validační, mutující nebo jsou složené z obou dohromady. Mutující kontrolery, oproti validačním, mohou modifikovat související objekty, které připouštějí\footnote{přijímací kontrolery v K8s: \url{https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/}}.

Proces přijímací kontroly se skláda ze dvou fází. Obecně jsou v první fází spuštěny mutační kontrolery a ve druhé validační. Výjimkou jsou ty, které mají oba druhy dohromady, kdy se tyto fáze prolínají. Jestliže kdykoliv v těchto fázích dojde k zamítnuní požadavku, celý požadavek je ihned zamítnut a uživateli je vrácena chyba.

Tato KEDA komponenta automaticky validuje zdroje KEDY, které uživatel vytváří nebo aktualizuje v clusteru. Validací se prochází před nasazením zdrojů do samotného clusteru pro předejití chybné konfiguraci na základě definovaných pravidel a testů. Můžeme tak předejít např. definování více objektů typu \texttt{ScaledObject}, které by měli za cíl škálovat jeden a ten stejný objekt. To by vyústilo v nedeterministické chování, jelikož tyto 2 objekty by se snažili škálovat podle svých vlastních metrik a velmi pravděpodobně by počet požadovaných replik nebyl stejný a proto by se neustále přetlačovali.

\section{Škálování}
\label{keda-scaling}

Mezi rozdíly škálování KEDY oproti stávajícímu mechanismu v Kubernetes patří možnost využít externích metrik pro určení počtu replik aplikace místo využívání pouze již existujích zdrojů, kterými jsou v Kubernetes jen dva -- CPU a Paměť, které jsou využitelné v nativním Kubernetes pro škálování pomocí objektů HPA a VPA. KEDA integruje funkčnost HPA a dovoluje uživatelům využít specifický scaler pro CPU nebo paměť. Z tohoto hlediska je KEDA škálování nádstavbou nad již existující infrastrukturou mechanismu HPA a dovoluje uživatelům jednoduše využít obě funkcionality dohromady. Navíc přidává konfigurační možnosti, které nejsou dostupné v základním Kubernetes. Také přináší schopnost škálování až na 0 replik pro šetření zdrojů v situaci kdy se žádné žádosti, události nebo zprávy neprocesují či nepřicházejí (determinováno podle konfigurace spouštěčů), což není možné v samotném \texttt{K8s}, kde minimální počet replik pro nasazenou aplikaci je 1.

\subsection{Aktivační a škálovací práh}
\label{keda-activation}

Pro konzistentní řešení rozdělení práce mezi objektem HPA a operátorem KEDA, existují 2 rozdílné fáze, ve kterých se může aplikace nacházet při automatizovaném škálovacím procesu. KEDA dovoluje definovat hodnotu, která bude reprezentovat práh pro obě fáze zvlášť podle konkrétní situace. Každá škálovací komponenta má vlastní definice podle svého využití, ale lze je poznat podle jména. Aktivační hodnota bude mít stejný název jako škálovací hodnota s přidáním prefixu \texttt{activation}. Například jako škálovací hodnota \texttt{threshold} a \texttt{activationThreshold} pro aktivaci.

\subsubsection{Aktivační (deaktivační) fáze}
První fáze se nazývá \texttt{Aktivační} (případně deaktivační) fáze. V tomto stádiu se operátor KEDA nachází, jestliže je právě v momentu, kdy rozhoduje zda má aplikace být škálována tzv. ``from/to 0'' neboli dolů na 0 replik nebo naopak nahoru z 0 na 1 repliku. KEDA si přebírá zodpovědnost za toto rozhodnutí na základě hodnoty funkce \texttt{IsActive} škálovací komponenty a aplikuje se pouze na škálování $0 \leftarrow \rightarrow 1$. Existují situace, kdy aktivační hodnota nemusí být nutně 0 a to např. když je aplikace škálována podle komponenty Prometheus, kde se hodnoty pohybují od $-X$ do $X$. 

Aktivační práh je standardně označen polem \texttt{activationThreshold} v YAML souboru. Práh není nutnou součástí definice. Pokud není zadán, ve výchozím stavu se očekává že hodnota je vždy 0.

\subsubsection{Škálovací fáze}
Škálovací fáze popisuje situaci, kdy je rozhodnuto, že aplikace je definitivně naškálována na 1 instanci a HPA kontroler určuje o dalších škálovacích rozhodnutích. Dělá tak podle konfiguračních detailů definovaných ve vygenerovaném HPA objektu, který byl vytvořen z dat v objektu \texttt{ScaledObject} a také pomocí metrik získaných z komponenty Metrics Server. K této fázi se váže škálování $1 \leftarrow \rightarrow N$ počtu replik.

Oproti aktivačnímu prahu, škálovací je povinný údaj při definování scaleru. Nachází--li se KEDA v situaci, kdy jsou definované oba prahy s různými hodnotami, aktivační práh má vždy přednost. V situaci kdy \texttt{threshold} je roven 5, \texttt{activationThreshold} je 30 a aktuální počet zpráv, které jsou ve frontě je 20, scaler je neaktivní a aplikace bude naškálována na 0 instancí i přesto, že HPA požaduje instance 4.

\subsection{Záložní mechanismus}
\label{keda-fallback}

Záložní mechanismus (anglicky fallback) je funkcionalita, která slouží pro případ kdy KEDA nedokáže získat metriky z monitorovaných externích scalerů. Pokud je tato funkcionalita nakonfigurována v definičním souboru YAML objektu SO, při chybě ve škálovací komponentě je aktivována, čímž dovolí pokračování ve funkci a celkově nesníží dostupnost aplikace. Sekce definující tento mechanismus se nachází v poli \texttt{spec}
\begin{lstlisting}[label={keda-fallback-listing},caption={Příklad sekce definující fallback v YAML souboru objektu ScaledObject},language=yaml]
fallback:
  failureThreshold: 3
  replicas: 6
\end{lstlisting}
a obsahuje dvě nutné hodnoty pro správnou definici. KEDA si zaznamenává počet za sebou jdoucích chyb pro každou škálovací komponentu, jinými slovy kolikrát nedokázala získat metriky zasebou z externího zdroje. Jakmile tento počet dosáhne čísla \texttt{failureThreshold} místo posílání prázdných metrik objektu HPA, jakožto základní chování při chybě, scaler vrátí vlastní upravenou hodnotu za pomocí nové formule -- požadovaná hodnota metriky cíle vynásobená číslem \texttt{replicas} v sekci \texttt{fallback} v konfiguraci, které udává žádaný počet replik pro danou aplikaci.

KEDA vytvoří tuto hodnotu a pošle ji objektu HPA čímž předstírá chování fungujícího scaleru a aplikace může dále běžet. Jelikož objekt HPA má metriky typu \texttt{AverageValue} tato funkcionalita nelze použít se scalery, které mají metriky typu \texttt{Value} nebo škálují podle nativních zdrojů jako využití CPU nebo paměti. Pro tyto škálovací komponenty bude KEDA předpokládat, že mechanismus není použitý. 

\subsection{Příklad nasazení a škálování}

\subsubsection{Prerekvizity}

Prvním krokem je úspěšně nastavený a spuštěný Kubernetes cluster. Tohoto lze docílit několika způsoby: Využít \texttt{Kubernetes playground} jednoduše v prohlížeči\footnote{zdroj: \url{https://labs.play-with-k8s.com/}}, pomocí programu \texttt{Minikube}\footnote{zdroj Minikube: \url{https://minikube.sigs.k8s.io/docs/start/}}, čímž lze nastavit cluster lokálně, což bude mít zdrojové požadavky na stroj uživatele na kterém cluster běží. Program \texttt{Kind}\footnote{zdroj Kind: \url{https://kind.sigs.k8s.io/docs/user/quick-start/}}, který je podobný Minikube a využívá se pro běh lokálního clusteru za pomocí uzlů Docker kontejnerů. KEDA dokumentace popisuje nasazení pomocí nástroje \texttt{Helm}\footnote{zdroj Helm: \url{https://helm.sh/}}, využití webové konzole \texttt{OperatorHub}\footnote{zdroj OperatorHub: \url{https://operatorhub.io/}}, která za pomocí operátorů rozšiřuje Kubernetes cluster a lidé z komunity je mohou sdílet mezi sebou. Případně je možné využít deklaračních souborů YAML, které lze nasadit pomocí nástroje pro příkazovou řádku \texttt{kubectl}, která zjednodušuje komunikace s Kubernetes API Serverem pomocí příkazů. 

\subsubsection{Nasazení}

Nasazení aplikací do clusteru např. pomocí příkazové řádky nástrojem \texttt{kubectl}. Uživatel si stáhne KEDU z GitHubu, vytvoří YAML soubory pro své objekty, jako Deployment, ScaledObject pro jeho škálování apod. Nainstaluje KEDU a pomocí \texttt{kubectl} vytvoří objekty na clusteru s jeho YAML definicí.

\subsubsection{Škálování}

Aplikace bude typicky mít základní počet replik nastavený na 0 nebo 1. V tomto příkladu je počet nastavený na 0, takže aplikace nemá žádne běžící instance. Na clusteru běží v Controlleru tzv. ``\texttt{Reconcile Loop}'', neboli cyklus, který neustále kontroluje stav objektů a snaží se aktuální stav přiblížit požadovanému stavu. Metrics Server získá sbírané metriky, KEDA tedy zjistí, že se počet zvýšil a nastaví požadované množství replik na 1, čímž aktivuje objekt HPA. Pokud je požadované množství instancí větší než 1, KEDA jej zvýší na 1 a následně se kontrola škálování ``předá'' vygenerovanému objektu HPA, který se od tohoto bodu stará o škálování. Pokud množství požadavků všech spouštěčů klesne, HPA naškáluje aplikace dolů až na 1 repliku. V případě, že aplikace nebude mít žádný provoz (pro některé spouštěče nemusí být aktivační hodnota 0 pokud uživatel specifikuje jinak) KEDA si převezme kontrolu nad škálováním (stav objektu HPA nastaví na neaktivní) a změní počet běžících instancí na 0. Takto zůstane, dokud běžící cyklus kontrolující rovnováhu aktuálního a požadovaného stavu nezaznamená zvýšení počtu událostí a celá operace se opakuje. 

\chapter{Složená škálovací komponenta}

Motivace pro vytvoření složené škálovací komponenty, které se v této práci také referuje jako \texttt{Composite scaler}, vychází z uživatelských potřeb pro využítí složitějších a více flexibilních způsobů jak kombinovat metriky a škálovat podle nich aplikace. Aktuální implementace škálovacích komponent a následné zpracování jejich metrik je velmi užitečné, především oproti nativnímu škálování v Kubernetes. Má to však stále své limity, jako možnost uživatele zacházet s komplexními scénáři, kde je potřeba vzít v úvahu dohromady více metrik, kombinovat a vytvářet kondice mezi scalery samotnými.

Za účelem zvýšení flexibility a vyřešení tohoto problému tato práce navrhuje rozšíření \texttt{Composite scaler}. Tato složená škálovací komponenta umožňuje uživatelům definovat výpočetní formuli, která je součástí definičního YAML souboru objektu ScaledObject a s její pomocí lze definovat jak matematické tak kondiční rovnice se specifikovanými spouštěči a vytvořit tím výpočet metrik pro specifickou situaci kterou uživatel potřebuje.

\section{Proces zpracování metrik}

Aktuálně KEDA sesbírá všechny metriky a odešle hodnoty objektu HPA (demonstrační obrázek \ref{fig:keda-scale}), který jednoduše vybere nejvyšší číslo a vypočítá požadovaný počet replik na naškálování podle rovnice (podkapitola \ref{k8s-equation}).
Na základě implementace této nové formule lze vzít externí metriky, které jsou získané z Metrics Serveru, a z nich vypočtené hodnoty, zformulovat z nich jednotnou hodnotu a tu odeslat objektu HPA. Veškeré operace se tedy budou vykonávát v KEDĚ a z pohledu Kubernetes nebudou změny zaznamenány. Metriky se upraví interně a pouze finální hodnota bude odhalena. 

\begin{figure}[H]
    \centering
    \includegraphics[width=0.7\textwidth]{obrazky-figures/keda_scale.png}
    \caption{Diagram demonstrující vytváření a sbírání metrik v KEDA architektuře}
    \label{fig:keda-scale}
\end{figure}

Když je KEDA nainstalována uživatelem do Kubernetes clusteru, nasadí se 2 hlavní kontejnery \texttt{keda-operator} jako Controller a \texttt{keda-operator-metrics-apiserver} jako Metrics Server. S těmito kontejnery jsou vytvořené nové vlastní CRD objekty, pomocí kterých je docíleno funkčnosti KEDY. Jedním z těchto objektů je typ ScaledObject, který lze definovat YAML souborem. Při definici takového souboru, controllery v clusteru detekují změnu stavu a vytvoří potřebné objekty jako HPA, scalery (které jsou součástí SO). 

\subsubsection{Kolekce metrik}
Pro sbírání metrik KEDA spoléhá na Kubernetes API Server komponentu, vbudovanou funkcionalitu platformy Kubernetes. Toto API dovoluje KEDĚ získat metriky z \texttt{K8s} objektů jako Pody, uzly nebo vlastní zdroje. Metrics Adaptér je KEDA komponenta zodpovědná za sběr dat z externích zdrojů a jejich zpřístupnění pro ostatní KEDA komponenty. Využívá konkrétních způsobů (jako koncové body API) pro získání těchto metrik dle specifického scaleru. Metrics Adaptér je nasazený jako separátní \texttt{K8s} objekt typu Deployment v Kubernetes clusteru a poslouchá pro HTTP požadavky na specifickém portu.

\subsubsection{Zpracování metrik}
Další nasazený objekt Deployment je KEDA Controller, ve kterém běží nekonečný cyklus ``Reconcile loop'', který periodicky odesílá požadavky na koncový bod Metrics Serveru aby získal hodnoty specifických metrik. Jestliže definice SO obsahuje více spouštěčů, controller se periodicky táže na každou hodnotu pro konkrétní metriku zvlášť. Jakmile sesbírá všechny metriky, strukturalizuje a zformuluje je a odešle objektu HPA, který vybere nejvyšší z nich a podle algoritmu (podkapitola \ref{k8s-equation}) získá požadovaný počet replik na který následně naškáluje cílenou aplikaci.

\subsection{Nové rozhraní}
Tato práce navrhuje rozšíření implementováním rozhraní do tohoto procesu získávání metrik na straně KEDY za účelem transformace metrik předtím, než se odešlou objektu HPA. Na základě aktuálního stavu a implementace komponentů byly zhodnoceny 2 možné přístupy jak toto rozšíření implementovat vzhledem k objektu HPA a k programatickým metodám pro získávání metrik.

\begin{figure}[H]
    \centering
    \includegraphics[width=0.7\textwidth]{obrazky-figures/keda_scale_new.png}
    \caption{Implementace nového rozhraní v relaci s ostatními KEDA komponenty}
    \label{fig:keda-scale-new}
\end{figure}

Při generování objektu HPA z definičních souborů pro objekty typu ScaledObject jsou součástí jeho definice spouštěče. Pro každou metriku je v Kubernetes odhalen koncový bod, který slouží jako propojení s Kubernetes API Serverem. Jestliže HPA bude požadovat hodnotu dané metriky, vytvoří požadavek na specifikovaný koncový bod a komunikací s API Serverem získá hodnotu pro danou metriku za předpokladu že v průběhu nedošlo k chybě.

\subsubsection{Scénář 1}
Prvním přístupem k řešení je zachování stávajících externích scalerů v definici objektu HPA. Z pohledu objektu ScaledObject bude změna zaznamenána pouze přidáním pole určující, že \texttt{CompositeScaler} je definován. Z pohledu HPA bude zachována funkčnost na povrchu a veškerá logika by se udávala uvnitř samotných metod pro získání metrik. Objekt HPA vytvoří požadavky na každou metriku zvlášť a jestliže je tento složený scaler definován, hodnota se podle dané formule vypočítá vnitřně a každý požadavek vrátí fiktivní hodnotu metriky udávající výsledek výpočetní formule. Chováním tedy bude KEDA předstírat, že všechny metriky vrací stejnou hodnotu, čímž by bylo zaručeno, že HPA naškáluje aplikaci na požadovaný počet replik, jelikož objekt HPA dostane tu samou hodnotu pouze vícekrát. Při využití tohoto scénáře by bylo nutné dělat výpočet formule pro každé volání metriky, jelikož by všechny musely vracet stejnou hodnotu. Tato možnost limituje zásah do změny fungování procesu získávání metrik avšak rychle zvyšuje počet volání API z důvodu opakovaného výpočtu metriky pro složenou škálovací komponentu.


% \begin{figure}[H]
%     \centering
%     \includegraphics{}
%     \caption{Ilustrace prvního scénáře možné implementace nového rozšíření. HPA se táže na každou metriku zvlášť, a každá metrika vrací stejnou upravenou hodnotu vnitřně zpracovanou.}
%     \label{fig:new-scanario-1-not-working}
% \end{figure}

\subsubsection{Scénář 2}

Druhou možností je definovat formuli pro výpočet, stejně jako v prvním případu jako součástí definice objektu ScaledObject. Při detekci, že tento komplexní škálovací prvek je zadán, objekt HPA není vygenerován s jeho standardními externími scalery, ale s jedním novým definovaným jako \texttt{composite-scaler}, který má vlastní koncový bod pro požadavky. Je tak zaručeno zjednodušení a zefektivnění volání na pouze jeden koncový bod. Při vytvoření dotazu na API pro získání metriky jsou interně získány všechny metriky (a pouze jednou) podle existujících externích spouštěčů, zpracovány podle dané formule a je vrácena jedna finální hodnota reprezentující metriku této složené komponenty. Scénář 2 upravuje samotný generovaný HPA objekt, API, se kterým komunikuje a logiku zpracování metrik, ale efektivně limituje zbytečné požadavky na Kubernetes API Server.

% \begin{figure}[H]
%     \centering
%     \includegraphics{}
%     \caption{Ilustrace druhého scénáře možné implementace nového rozšíření. Je vytvořen nový \texttt{CompositeScaler} pro HPA s jeho API. Objekt HPA se táže na koncový bod a logika je vytvořena uvnitř, kde metriky jsou zpracovány a hodnota vrácena podle formule}
%     \label{fig:new-scanario-2-working}
% \end{figure}

\subsubsection{Rozhodnutí}
\label{decision-logic}
Po zvážení obou námětů pro implementaci složené škálovací komponenty je nakonec rozhodnuto, že možnost 2 bude nejlepší řešení.

Hlavní důvod pro výběr druhého scénáře je aktuální způsob implementace objektu HPA a sbírání metrik. Implementováním scénáře 1 se HPA dotazuje každou externí metriku individuálně a následně uvnitř nově vytvořeného rozhraní se upraví proces jejího zpracování. Při definici 3 spouštěčů HPA vytvoří tři požadavky na Kubernetes API a každý požadavek interně vytvoří další dva (pro získání zbylých metrik) jelikož jsou to nezávislé události. V porování s druhým scénářem, kde se implementuje nová struktura pro scaler, která se předá objektu HPA. Škálovací komponenta následně vytvoří pouze 1 API požadavek. Vnitřně se vytvoří požadavky pro každou metriku celkově jedenkrát a ty se poté interně zpracují a odpověď na požadavek je finální vypočtená hodnota metriky po aplikaci formule. Tímto způsobem má KEDA přístup ke všem metrikám a může přidávat podmíněné příkazy do výpočetní formule. Šetří se počet dotazů na Kubernetes API Server a metriky se zpracovávájí  efektivním způsobem. je zaručeno robustní řešení pro možné budoucí rozšíření.

\section{Výpočetní formule}
% \subsection{Výpočetní formule}

Za účelem implementace nové formule pro složenou škálovací komponentu aby mohli uživatelé definovat vlastní výpočetní formuli pro škálování je potřeba vytvořit způsob validace, zpracování a evaluace této formule. Formule je definována polem typu string v definici objektu ScaledObject. Pro tuto práci byly zváženy parsovací a evaluační nástroje v jazyce Go.

% \subsection{Zpracování formule}

\subsubsection{Parsovací balíček \texttt{parser}}
Balíček \texttt{parser} je populární knihovna pro parsování zdrojových souborů Go. Přijímá několik vstupů v různých formách dle specifických funkcí. Výstup je abstraktní syntetický strom reprezentující soubor. Konverze souborů na datové struktury je jedna z hlavních funkcí balíčku, ale mimo to umí analyzovat i výrazy. Pomocí využití funkcí jako \texttt{ParseExpr} zavolá balíček \texttt{scanner} pro toknizaci vstupního stringu a následně rozparsuje tyto tokeny do abstraktního stromu. Poskytuje jednoduché rozhraní pro parsování a analýzů výrazu s možností využití rekurzivní sestupové analýzy. Analyzátor zvládá různé matematické operace jako sčítání, odčítání, násobení, dělení a logaritmické nebo goniometrické funkce.

\begin{lstlisting}[label={listing-parser},caption={Příklad užití Go balíčku \texttt{parser}},language=Go]
// Define the formula string
formula := "(A + B) / 2"

// Parse the formula string using ParseExpr
expr, err := parser.ParseExpr(formula)

// Create the AST (Abstract Syntax Tree) for the formula
ast.Print(token.NewFileSet(), expr)

// Implement evaluation and pass variable expr
\end{lstlisting}

Deficit knihovny je neschopnost vyhodnotit vyparsovaný string pro získání hodnoty vstupní formule. Rovnice se rozparsuje a ztokenizuje a uloží do data do struktur, ale finální evaluace musí být manuálně vytvořena pro každou funkci zvlášť.

\subsubsection{Knihovna \texttt{Sprig}}
Sprig je populární open-source knihovna pro jazyk Go poskytující pomocné funkce různých variací, které usnadňují práci s šablonami. Je často používána v ekosystému Kubernetes jako rozšíření funkcionality nástrojů jako např. \texttt{Helm}\footnote{zdroj: \url{https://helm.sh/}} nebo \texttt{Kustomize}\footnote{zdroj: \url{https://kustomize.io/}}. Samotná knihovna je vytvořena týmem Masterminds a je pod licencí MIT. Knihovna obsahuje široký výběr pomocných funkcí, které zahrnují např. manipulaci s řetězci, formátování dat nebo logické operace. Mimo to balíček také obsahuje několik užitečných funkcí pro práci s Kubernetes, např. získávání konfiguračínch dat z \texttt{K8s} zdrojů.

Mezi využitelné funkce pro problém, který řeší tato práce patří \texttt{math} která s argumenty dokáže prováděť různé matematické operace, jako sčítání, násobení, dělení apod. Další funkcí je \texttt{ternary}, která dokáže vyhodnocovat podmíněné příkazy a Booleovské podmínky.

Šablona pro vyhodnocení rovnice $(A + B) / 2$ v jazyce Go (výpis \ref{listing-sprig}).

\begin{lstlisting}[label={listing-sprig},caption={Ukázka šablony matematické funkce Sprig knihovny}]
{{- $A := 10 -}}
{{- $B := 20 -}}
{{- $result := div (add $A $B) 2 -}}
{{- $result -}}
\end{lstlisting}

Nedostatek při použití této knihovny je nutnost dodatečné implementace parseru a jeho následné propojení. Implementace by potom spoléhala na konkrétních implementovaných funkcích případně parseru, což není velmi efektivní a zvýší možnost chyby při využití dalšího balíčku. Knihovna obsahuje přes 70 různých funkcí, které by nebyly zcela využity pro toto konkrétní řešení.

\subsubsection{Evaluační balíček \texttt{expr}}
Balíček \texttt{expr} je open-source knihovna Go, která parsuje a vyhodnocuje libovolné výrazy dynamickým způsobem. Jedna z hlavních funkcionalit a účel balíčku je umožnění uživatelům využít výrazy s konfigurací a komplexní logikou. Knihovna mimo základní aritmetické operace podporuje také podmíněné příkazy, cykly, funkční volání a přístup k proměnným a polím (ukázka příkladů ve výpisu \ref{listing-expr}). Díky schopnosti parsování a evaluace výrazů, možnosti využití proměnných pro substituci hodnot a vytváření podmínek je tento balíček dobrá volba pro řešení problému této práce. Mimo jiné je tento projekt aktivně sledován, upravován a aktualizován komunitou na stránce GitHub, což je další z faktorů pro výběr tohoto balíčku.

\begin{lstlisting}[label={listing-expr},caption={Příklady možných formulí pro využití pomocí projektu \texttt{expr} v jazyce Go},language=Go]
// mathematical expression
result, err := expr.EvaluateFloat("1 + 2 * 3")

// conditional expression
context := map[string]interface{}{
  "x": 3,
  "y": 5,
}
result, err := expr.EvaluateBool("x > y", context)

// custom functions for use in expressions
fn := func(a, b int) int {
    return a + b
}
env := &expr.Env{
    Funcs: map[string]interface{}{
        "sum": fn,
    },
}
result, err := expr.EvaluateInt("sum(3, 4)", env)
\end{lstlisting}

\section{Implementační detaily}

\subsection{Definice struktur}

K projektu KEDA aktuálně patří přes 400 modulárně strukturovaných zdrojových souborů jazyka Go. Složka \texttt{apis} obsahuje definice Kubernetes API zdrojů pro celý KEDA balíček, jako definice vlastních zdrojů (CRD) a jejich odpovídající typy rozhraní API. Součástí adresáře je soubor \texttt{apis/keda/v1alpha1/scaledobject\_types.go} obsahující definice struktur mimo jiné pro vlastní zdroje. Součástí rozšíření složené škálovací komponenty je definice nového pole struktury popisující definiční YAML soubor objektu ScaledObject.

\subsubsection{Definice objektu a anotace struktur}

Pokud chce uživatel využít tuto novou škálovací komponentu, přidá její definici do YAML souboru objektu SO (ukázka definice ve výpisu \ref{listing-composite-scaler}). Pole je součástí pokročilého nastavení objektu, které mimo jiné popisuje konfiguraci vygenerovaného objektu HPA. Nově vytvořené pole \texttt{CompoundScalingLogic} je struktura typu \texttt{CompositeScalingLogic} definující škálovací formuli a cílenou požadovanou metriku, která se použije v rovnici pro výpočet počtu replik škálované aplikace.

\begin{lstlisting}[label={listing-composite-scaler},caption={Struktura \texttt{AdvancedConfig} definující pokročilé nastavení s nově přidanou strukturou komponenty \texttt{CompoundScalingLogic} v jazyce Go},language=Go]
// AdvancedConfig specifies advance scaling options
type AdvancedConfig struct {
  HorizontalPodAutoscalerConfig *HorizontalPodAutoscalerConfig `json:"horizontalPodAutoscalerConfig,omitempty"`
  
  RestoreToOriginalReplicaCount bool `json:"restoreToOriginalReplicaCount,omitempty"`
  
  // new added struct
  CompoundScalingLogic CompositeScalingLogic `json:"compositeScalingLogic,omitempty"`  
}
\end{lstlisting}

Protože je projekt KEDA je psaný v jazyce Go, podporuje použivání tzv. štítků pro struktury (struct tags) pro anotaci polí struktur s dalšími metadaty. Běžný způsob použití těchto štítků je pro poskytování dalších informací např. pro kódování či dekódování hodnot mezi Go jazykem a formáty JSON, YAML, XML a dalšími. KEDA využívá JSON anotace pro propojení Go struktur s definičními soubory Kubernetes objektů. Ve struktuře výše (výpis \ref{listing-composite-scaler}) je hodnota \texttt{CompoundScalingLogic}, která definuje složený škálovací mechanismus. Má vepsanou anotaci pro objekt typu ScaledObject. Název pole v YAML souboru je \texttt{compositeScalingLogic} a \texttt{omitempty} hodnota znamená, že pokud je tato proměnná prázdná, nezapisuje se do definičního souboru, jinými slovy přeloženo jako ``vynechej prázdnou hodnotu''. Jestliže \texttt{omitempty} není v anotaci specifikováno, hodnota proměnné musí být nenulová pro správnou definici struktury, tedy zdůrazňuje nutnost položky v definičním souboru. 

Nezbytnou položkou v definici objektu ScaledObject je např. pole \texttt{triggers} definující spouštěče, kterými se škálování řídí nebo hodnota \texttt{FailureThreshold} ve struktuře definující \texttt{Fallback} mechanismus (kapitola \ref{keda-fallback}). Samotná struktura není požadována, ale její hodnoty ano. To znamená, že záložní funkcionalita není vyžadována každým objektem SO, ale pokud definována je, její hodnota \texttt{FailureThreshold} musí být nenulová (ukázka ve výpisu \ref{listing-composite-so}).

\subsubsection{Objekt ScaledObject}

\begin{lstlisting}[label={listing-composite-so},caption={Definice objektu SO, který obsahuje \texttt{fallback} mechanismus, 2 spouštěče a novou škálovací formuli pro pokročilé škálování},language=yaml]
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: so-composite
  namespace: test
  labels:
    app: depl-composite
spec:
  advanced:
    horizontalPodAutoscalerConfig:
      behavior:
        scaleDown:
          stabilizationWindowSeconds: 1
    compositeScalingLogic:
      target: "2"
      formula: "(kw_trig + mapi_trig)"
  pollingInterval: 1
  scaleTargetRef:
    name: depl-composite
  minReplicaCount: 0
  maxReplicaCount: 5
  cooldownPeriod: 1
  triggers:
  - type: kubernetes-workload
    name: kw_trig
    metadata:
      podSelector: 'pod=workload-test'
      value: '1'
  - type: metrics-api
    name: mapi_trig
    metadata:
      targetValue: "2"
      url: "https://mockbin.org/bin/336a8d99-9e09-4f1f-979d-851a6d1b1423"
      valueLocation: "tasks"
\end{lstlisting}

Tento ScaledObject (výpis \ref{listing-composite-so}) obsahuje pole specifikace pokročilého nastavení \texttt{spec} $\rightarrow$ \texttt{advanced}. Se specifickým nastavením pro škálovací chování objektu HPA v \texttt{horizontalPodAutoscalerConfig} obsahuje také nové nastavení \texttt{compositeScalingLogic}. Tento prvek má dva povinné prvky.

\begin{itemize}
    \item \texttt{target} -- float hodnota popisující požadovanou metriku (ekvivalentní k cílené metrice ve spouštěči)
    \item \texttt{formula} -- string hodnota reprezentující škálovací formuli
\end{itemize}

Při definici formule pro složenou škálovací komponentu je možné zahrnout samotné spouštěče (výpis \ref{listing-scaler-multi-metrics}). Každý spouštěč má volitelné pole pro název, které při běžném používání není velmi využívané. Jestliže uživatel referencuje spouštěč ve formuli, ale nedefinuje jeho jméno v poli, dostane chybu při instalaci. Název spouštěče je v tomto případě nezbytnou hodnotou. 

\subsubsection{Více metrik v jedné škálovací komponentě}

V objektu ScaledObject (výpis výše \ref{listing-composite-so}) existují 2 triggery, každý s jednou metrikou. Reference spouštěče ve formuli jeho jménem je vhodná pokud mají právě jednu metriku, ale existují i škálovací komponenty u kterých je možné sbírat metrik více. Aktuálně existují 2 komponenty (z 61), které sledují 2 metriky. Implementace pro vyřešení tohoto okrajového případu je enumerace názvu spouštěče. Identifikační číslo se připojí na konec řetězce jména škálovací komponenty s více metrikami (výpis \ref{listing-scaler-multi-metrics}).

\begin{lstlisting}[label={listing-scaler-multi-metrics},caption={Formule a spouštěče s více metrikami součástí YAML definice objektu SO},language=yaml]
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
...
spec:
  scaleTargetRef:
    name: depl-composite
  advanced:
    compositeScalingLogic:
      target: "2"
      formula: "(solaceTrigger1 + solaceTrigger2 + mapiTrigger)/3"
...
  triggers:
  - type: solace-event-queue
    name: solaceTrigger
    metadata:
      solaceSempBaseURL:        http://solace_broker:8080
      messageVpn:               message-vpn
      queueName:                queue_name
      messageCountTarget:       '100'
      messageSpoolUsageTarget:  '1000'
  - type: metrics-api
    name: mapiTrigger
    metadata:
      targetValue: "2"
      url: "https://mockbin.org/bin/336a8d99-9e09-4f1f-979d-851a6d1b1423"
      valueLocation: "tasks"
\end{lstlisting}

Škálovací komponenta \texttt{solace} sbírá v této definici dvě metriky. 
\begin{itemize}
    \item \texttt{messageCountTarget}, který určuje kolik zpráv bude mít objekt Pod na starost. Počet replik se zvýší jestliže je fronta nevyřízených zpráv delší než tato hodnota pro každý Pod. 
    \item \texttt{messageSpoolUsageTarget} je integer hodnota reprezentující počet MB. Tato hodnota popisuje jaké množství zpráv může být dočasně uloženo. Udává kolik paměti využívají zprávy čekající na odeslání. Jakmile využití paměti dosáhne této hodnoty, počet replik se navýší.
\end{itemize}

Způsob rozlišení těchto metrik ve složené škálovací formuli je zřetězením čísla na konec řetězce s názvem spouštěče a zakládá si na implementaci škálovacích komponent. Když KEDA volá metodu pro získání metrik, metoda deterministicky připojí obě hodnoty za sebe a vrátí pole těchto hodnot. Pořadí, ve kterém se nachází není determinováno pořadím v uživatelem definovaném YAML souboru, ale implementací metody získávání metrik, která se shoduje s pořadím v dokumentaci škálovací komponenty. 

V konkrétním příkladu škálovací komponenty \texttt{solace} kdy jsou definované obě metriky a název spouštěče je \texttt{solaceTrigger}. První sledovaná metrika \texttt{messageCountTarget} je ve formuli definována jako \texttt{solaceTrigger1} a \texttt{messageSpoolUsageTarget} je pod názvem \texttt{solaceTrigger2}. V nejnovější verzi dokumentace\footnote{zdroj dokumentace scaleru solace: \url{https://keda.sh/docs/2.10/scalers/solace-pub-sub/}} je ukázka definice spouštěče, která uvádí stejné pořadí. Toto pořadí je deterministické a řídí se jím implementace na které je založeno samotné číslování.

\subsection{Získávání metrik}

Funkce související s implementací škálovacích komponent se nachází v souboru \texttt{scale\_handler.go}. Metody se využívají pro správu škálovacích požadavků pro projekt KEDA, čímž jsou zodpovědné za škálování monitorovaných Kubernetes objektů. Přijímají a zpracovávají požadavky od Kubernetes API serveru, kvůli tomu jsou považovány za vstupní bod pro příchozí požadavky. Mimo to modul obsahuje funkce pro vytváření a mazání nových škálovacích komponent, jejich životní cyklus a zdravý stav objektů ScaledObject. Metoda \texttt{GetScaledObjectMetrics} je volána při vytvoření požadavku pro získání jedné specifické metriky a volání se opakuje pro každou metriku definovanou v objektu ScaledObject.

Při definici složené škálovací komponenty v objektu ScaledObject je definován vlastní scaler. Díky této implementaci je v objektu HPA referencován pouze 1 externí scaler, na který se vytváří požadavky na Kubernetes API server. To znamená, že metoda \texttt{GetScaledObjectMetrics}, která je volána pro získání metriky, je volána pouze 1 a zamezuje tak zbytečným  požadavkům na \texttt{K8s} API server.

\subsection{Validace před nasazením}

Pro správnou instalaci projektu KEDA s novou složenou škálovací komponentou v objektu ScaledObject musí konfigurace projít validací v komponentě Přijímacích Webhooků (podkapitola \ref{keda-webhooks}) jestli je součástí instalace (Přijímací Webhooky jsou nová komponenta. Uživatelé mohou a nemusí využít binární soubory pro instalaci s touto komponentou). 

Jestli složená škálovací formule obsahuje názvy spouštěčů, musí být také definovány v sekci pro samotné spouštěče. Objekt ScaledObject musí obsahovat externí škálovací komponenty pro funkčnost rozšíření. 

\subsubsection{Možné rozšíření}

Možné rozšíření pro uživatelsky přívětivější používání formule může být dodatečná definice samotných metrik zvlášť, případně připojení abreviace metriky za název spouštěče (kapitola o rozšíření \ref{extensions}). Toto rozšíření by vyžadovalo definovat způsob jak referovat konkrétní metriky pro využití ve formuli pro spojení názvu metriky a její hodnoty při výpočtu. Jedním možným způsobem je využít specifický podřetězec (případně celý řetězec), který bude odpovídat názvu sledované metriky v poli spouštěčů. Například \texttt{messageCountTarget} pro \texttt{solace} škálovací komponentu, která udává počet nezpracovaných zpráv.

\begin{lstlisting}[label={extensions},caption={Úryvek z definičního souboru ScaledObject s využitím navrhovaného způsobu definice},language=yaml]
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
...
spec:
  scaleTargetRef:
    name: depl-composite
  advanced:
    compositeScalingLogic:
      target: "5"
      formula: "(solaceTrigger.messageCount + solaceTrigger.spoonUsage + mapiTrigger)/3"
...
  triggers:
  - type: solace-event-queue
    name: solaceTrigger
    metadata:
      solaceSempBaseURL:        http://solace_broker:8000
      messageVpn:               message-vpn
      queueName:                queue_name
      messageCountTarget:       '50'
      messageSpoolUsageTarget:  '800'
  - type: metrics-api
    name: mapiTrigger
    metadata:
      targetValue: "2"
      url: "https://mockbin.org/bin/336a8d99-9e09-4f1f-979d-851a6d1b1423"
      valueLocation: "tasks"
\end{lstlisting}

Toto rozšíření by bylo vhodné pokud by se zvýšil počet externích škálovacích komponent, které sbírají 2 a více metrik pro jednodušší odlišení a více intuitivní způsob zapisování formule. Jelikož KEDA má převážně scalery, které monitorují každý poze jednu metriku, potom stačí spojovat metriky s jejich názvem ve spouštěči pro jejich referenci. Toto rozšíření řeší okrajový případ a zatím není potřebným v krátké budoucnosti.

\chapter{Externí kalkulace škálování}

KEDA umožňuje škálovat podle základních zdrojů v Kubernetes a implementovaných externích škálovacích komponent ze kterých umí sbírat metriky. S implementací složené škálovací komponenty je možné modifikovat metriky z externích škálovacích komponent při použítí formulí pro jejich výpočet. S implementací rozšíření externí kalkulace může uživatel definovat kompletně vlastní způsob výpočtu externích metrik. Toto rozšíření je implementováno pomocí gRPC technologie, kterou KEDA již využívá pro získání metrik. V relaci k gRPC (podkapitola \ref{grpc}) je KEDA klient a uživatelem definovaná metoda výpočtu je server. KEDA získá externí metriky a místo jejich výpočtu je odešle gRPC serveru odkud očekává zpracovanou finální hodnotu.

\section{gRPC}
\label{grpc}

gRPC (``g'' znamená něco jiného v každé nové verzi\footnote{ význám ``g'' v gRPC \url{https://github.com/grpc/grpc/blob/master/doc/g_stands_for.md}}) je meziprocesní komunikační technologie, které umožňuje připojovat, vyvolávat, provozovat a ladit distribuované heterogenní aplikace stejně jednoduše jako volání lokální funkce. Hlavním cílem tohoto vysoce výkonného open-source rámce vzdáleného volání procedur (RPC) je usnadnit vývojářům vytváření efektivních a škálovatelných distribuovaných systémů~\cite{cite:grpc}. 

\begin{figure}[H]
    \centering
     \includesvg[width=0.8\textwidth]{obrazky-figures/grpc.svg}
    \caption{Ukázka gRPC v nasazení}
    \label{fig:grpc}
\end{figure}

Na straně klienta i serveru mohou běžet aplikace v různých prostředích a komunikovat spolu např. ze serveru uvnitř Google platformy na lokální počítač. Uživatel může jednoduše vytvořit gRPC server v jazyce C++ s klienty v Ruby, Javě nebo Go. Nejnovější Google API podporují gRPC verze jejich rozhraní, čímž dovolí uživatelům lehce využít funkcionality od Google platforem ve svých aplikacích.

\subsection{Servisní rozhraní}

První věc která je zapotřebí při vývoji gRPC aplikace je definice servisního rozhraní. Tato definice obsahuje informace o tom, jak mohou uživatelé používat vytvořený servis, jaké metody mohou uživatelé vzdáleně volat, jaké parametry tyto metody mají, jaké formáty zpráv používat při jejich vyvolávání a tak dále. Servisní definice se specifikuje pomocí jazyku ``pro definici rozhraní'' (interface definition language -- IDL). Využívá se pro komunikaci mezi odlišnými softwarovými komponenty. Strukturalizuje způsob definice pro možnost komunikace mezi různými softwarovými komponenty, jako programovací jazyky a platformy mezi sebou.

Softwarový rámec gRPC využívá protokolové vyrovnávací paměti (Protocol Buffers)\footnote{zdroj PB \url{https://protobuf.dev/}}, které popisují binární serializační formát bez specifikace programovacího jazyku pro serializaci strukturovaných dat. Protokolové vyrovnávací paměti jsou kompaktnější a efektivnější alternativou k jiným formátům pro výměnu dat, jako XML nebo JSON. Jsou rozšířitelné, mají verzovací systém a nejsou závislé na žádném jazyku, takže je velmi snadné s nimi vytvářet rozhraní API, které mohou být využity několika programovacími jazyky. Mezi podporované jazyky patří C--sharp, C++, Go, Java, PHP nebo Python a jedna z klíčových výhod gRPC je možnost propojení programů mezi těmito různymi jazyky. Vývojáři tedy mohou psát kód v jazyce, který jim nejvíc vyhovuje a stále mohou komunikovat s rúznými službami napsanými v jiných jazycích. 

\subsubsection{Proto soubor}

IDL soubor \texttt{ProductInfo.proto} (výpis \ref{protobuf-example}), který definuje službu s názvem \texttt{ProductInfo} obsahující definici rozhraní se specifikací metod, jejich typované vstupní a výstupní parametry. 

\begin{lstlisting}[caption={Definice IDL souboru pro servis pod názvem \texttt{ProductInfo} \cite{cite:grpc}},label={protobuf-example},language=protobuf]
//ProductInfo.proto file

syntax = "proto3"; // (1)
package ecommerce; // (2)

service ProductInfo { // (3)
  rpc addProduct(Product) returns (ProductID); // (4)
  rpc getProduct(ProductID) returns (Product); // (5)
}

message Product { // (6)
  string id = 1; // (7)
  string name = 2;
  string description = 3;
}

message ProductID { // (8)
  string value = 1;
}
\end{lstlisting}

Detailní popis definice podle reference uzávorkovaných čísel ve výpisu \ref{protobuf-example} výše: 
\begin{itemize}
    \item 1 -- Definice služby začíná se specifikací verzi protokolového bufferu (proto3)
    \item 2 -- Název balíčku se využíví pro rozlíšení a prevenci duplikací nebo nesoulad typů které by byly použity pro vytvoření zdrojového kódu
    \item 3 -- Definice rozhraní gRPC služby
    \item 4 -- Vzdálená metoda pro přidání produktu, která vrací ID produktu jako odpověď
    \item 5 -- Vzdálená metoda pro získání produktu na základě jeho ID
    \item 6 -- Definice formátu (typu) zprávy struktury \texttt{Product}
    \item 7 -- Pole (dvojice ``key--value''), které reprezentuje identifikační hodnotu produktu jako unikátní číslo v binárním formátu  
    \item 8 -- Uživatelem definovaný typ pro identifikační číslo produktu
\end{itemize}

Servis je tedy kolekce metod, které mohou být vzdáleně vyvolány. Každá metoda má vstupní parametry a specifikované typy, které vrací. Ty jsou definovány jako část celé služby nebo můžou být importovány do protokolových bufferů jako definice. 

Vstupní a výstupní parametry mohou být typy uživatelem definovány nebo již předdefinované z protokolových bufferů. Tyto typy jsou strukturované jako zprávy, kde každá zpráva je malý logický záznam informací obsahující páry ve formátu název--hodnota, které se nazývají pole. Pomocí této definice se nastavují obě strany komunikace, klient i server.

\subsection{Komunikace mezi komponenty}

Jakmile jsou \texttt{proto} soubory definovány, mohou být využity pro vygenerování zdrojového kódu jak pro klienta, tak server za pomocí kompilátoru protokolových bufferů \texttt{protoc}. Standardně lze vygenerovat regulerní kód protokolové vyrovnávací paměti pro serializaci, zapsání a získání typů zpráv. 

Tok zpráv začíná vyvoláním gRPC servisu klientem. Knihovna na straně klienta za pomocí protokolových bufferů (PB) zabalí formát vzdáleného volání procedury, který je následně poslán přes HTTTP/2. Na straně serveru je požadavek rozbalen a příslušná metoda je provedena. Zpětný proces náseduje velmi podobnou strukturu. Server vytvoří a odešle odpověď zpět jako zprávu obsahující výsledek operace a příslušná metadata.

\subsubsection{Server}

Na straně serveru je implementována definice a je nasazen gRPC server pro zpracování příchozích požadavků od klienta. Je potřeba implementovat servisní logiku generované kostry struktury přepsáním základní třídy samotné služby a spustit gRPC server aby poslouchal pro požadavky a vracel správné odpovědi. Vygenerovaná kostra struktur metod pro servis \texttt{ProductInfo} (výpis \ref{grpc-server}) je vytvořena s jazykem Go. Uvnitř těchto funkcí uživatel implementuje logiku každé metody.

\begin{lstlisting}[label={grpc-server},caption={Úryvek kódu v Go popisující kostru vygenerované struktury pro metody na straně serveru},language=Go]
// Server Go file

import (
  "context"
  "google.golang.org/grpc"
  ...
)

func (s *server) AddProduct(ctx context.Context, in *pb.Product) (*pb.ProductID, error) {
  // implement business logic here
}

func (s *server) GetProduct(ctx context.Context, in *pb.ProductID) (*pb.Product, error) {
  // impement business logic here
}
\end{lstlisting}

Jakmile budou metody implementovány, je potřeba spustit server, který bude poslouchat na příchozí požadavky a odesílat odpovědi klientovi (výpis \ref{grpc-run-server}). Při úspěšném spuštění gRPC serveru tímto způsobem je tato část aplikace úspěšně nastavena a běží. 

\begin{lstlisting}[label={grpc-run-server},caption={Nasazení gRPC serveru v jazyce Go},language=Go]
func main() {
  list,_ := net.Listen("tcp",port)
  s := grpc.NewServer()
  pb.RegisterProductInfoServer(s,&server{})
  if err := s.Serve(lis); err != nil {
    log.Fatalf("failed to serve: "%v",err)
  }
}
\end{lstlisting}

\subsubsection{Klient}

Podobně jako na straně serveru, zdrojový kód pro klienta se vygeneruje pomocí servisní definice. Klient poskytuje stejné metody jako server a tento kód na straně klienta může tyto metody vyvolávat. Metody se převedou na vzdálené funkční síťová volání na server. Kód může být vygenerován pro jiný jazyk než server díky gRPC funkcionalitě, tzn. klient může být implementován např. v jazyce Java a jediné co je potřeba zařídit je nastavení spojení s vzdáleným serverem, připojení klienta k tomuto spojení a vyvolání implementovaných metod (výpis \ref{grpc-client}).

\begin{lstlisting}[label={grpc-client},caption={Úryvek kódu gRPC klienta pro vyvolávání metod služby \cite{cite:grpc}},language=Java]
// Client Java file

// create a channel 
ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost",8080)
  .usePlaintext(true)
  .build();

// initialize blocking stub using the channel
ProductInfoGrpc.ProductInfoBlockingStub stub =ProductInforpc.newBlockingStub(channel);

// call remote method
StringValue productID = stub.addProduct(
  Product.newBuilder()
  .setName("Apple iPhone 14")
  .setDescription("Cool new iPhone 14." +
    "Ultra Bright Night Mode.")
  .build());
\end{lstlisting}

\section{gRPC v projektu KEDA}

KEDA již využívá gRPC pro komunikaci se škálovacím backendem, který je zodpovědný za zpracovávání metrik a určuje vhodnou akci pro škálování. K vytvoření komunikace se využívá standardních protokolových bufferů, které zajišťují efektivnější způsob zakódování dat oproti tradičním formátům jako JSON. KEDA odhaluje API na základě gRPC, které zahrnuje několik metod pro řízení škálovacího chování systému. Tyto metody dovolují backendu získat informace o aktuálním stavu objektu v clusteru, vytvářet nové škálovací komponenty, spouštěče a získat aktualizované metriky když jsou dostupné. 

KEDA Operátor komunikuje s KEDA Metrics Serverem přes gRPC posíláním požadavků a očekáváním odpovědí. Když operátor škáluje nějaký Kubernetes objekt, pošle gRPC požadavek na komponentu MS aby získala aktuální hodnoty sledovaných metrik podle kterých škáluje aplikaci pro výpočet replik. MS odesílá odpoveďi s požadovanými metrikami a Operátor podle těchto metrik vypočítá kolik replik by měla daná aplikace mít na základě škálovací konfigurace. 

\subsection{Definice nových struktur}

Pro implementaci nové funkcionality externích kalkulací je třeba upravit objekt ScaledObject (výpis \ref{so-external-calc}) pro definici, kdy chce uživatel tuto funkcionalitu využít.

\begin{lstlisting}[label={so-external-calc-struct},caption={Struktura definující novou funkcionalitu externích kalkulací},language=Go]
// struct define for external calculations
type ExternalCalculation struct {
  Name    string `json:"name"`
  Url     string `json:"url"`
  Timeout string `json:"timeout"`
  // +optional
  Fallback Fallback `json:"fallback,omitempty"`
}
\end{lstlisting}

Struktura definuje pole \texttt{Name}, které slouží jako identifikační číslo. Položka \texttt{Url} určuje koncový bod se kterým se KEDA pokusí navázat spojení jako gRPC klient--server. Pole \texttt{Timeout} určuje dobu čekání na získání metrik, která když uplyne, tak se vyhodnotí, že metriky nebyly úspěšné získány. A jako poslední nepovinný údaj je pole \texttt{Fallback} určující mechanismus, stejně jako u standardních škálovacích komponent, jestliže KEDA selže získat metriky několikrát po sobě, nastaví počet replik aplikace na zde definovanou hodnotu.

\begin{lstlisting}[label={so-external-calc},caption={Objekt ScaledObject s definicí funkcionality externích kalkulací},language=yaml]
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: so-external-calculus
  namespace: test
  labels:
    app: my-deployment
spec:
  advanced:
    externalCalculation:    # new external calculation field
    - name: my-time-convertor
      url: locahost:50034
      timeout: 10
      fallbacK:
        failureThreshold: 5
        replicas: 3
    - name: history-calc
      url: localhost:50035
      timeout: 15
  scaleTargetRef:
    name: my-deployment
  minReplicaCount: 0
  maxReplicaCount: 10
  triggers:
  - type: rabbitmq
    name: rabbit_trigger
    metadata:
      protocol: amqp
      queueName: queue-name
      mode: QueueLength
      value: "20"
    authenticationRef:
      name: keda-trigger-rabbitmq-auth
  - type: kafka
    name: kafka_trigger
    metadata:
      bootstrapServers: localhost:9092
      consumerGroup: my-group
      topic: my-topic
      lagThreshold: "50"
\end{lstlisting}

Definice objektu ScaledObject s názvem \texttt{so-external-calculus} (výpis \ref{so-external-calc}) popisuje objekt, který má 2 spouštěče \texttt{kafka} a \texttt{rabitmq} monitorující každý jednu metriku. V sekci pokročilého nastavení je struktura \texttt{externalCalculation}, která reprezentuje pole nových struktur pro externí výpočty. Uživatel může zadat více gRPC serverů na které bude KEDA vytvářet požadavky a libovolně s metrikami manipulovat.

Položka \texttt{externalCalculation} je pole hodnot z důvodu flexibility a uživatelské preference. Tímto způsobem může uživatel definovat jakékoliv škálovací chování, které může řetězit zasebou. Například může definovat objekt ScaledObject se 4 externími škálovacími komponenty a 2 externími kalkulátory. Vytvoří a definuje 2 gRPC servery, kde první z nich modifikuje 2 ze 4 daných metrik a druhý vezme v úvahu již modifikované metriky a provede s nimi další jinou mutaci. Na kalkulátory budou vytvářeny požadavky vždy ve stejném pořadí, v jakém jsou definovány v definičním YAML souboru objektu SO.

\subsection{Protokolové buffery}

Pro implementaci gRPC serveru musí uživatel znát jaké metody, formáty struktur a pole uvnitř těchto struktur se používají aby mohl vytvořit takové, které jim přesně odpovídají.  Proto pomocí proto souboru jsou všechny důležité prvky definovány následovně (výpis \ref{external-calc-new-def}).

\begin{lstlisting}[label={external-calc-new-def},language=protobuf,caption={Definice IDL souboru pro nové rozhraní externích výpočtů}]
// use syntax of proto version 3
syntax = "proto3";

// define package name
package externalCalculation;

// define service for calculation
service ExternalCalculation {
  rpc Calculate (MetricsList) returns (Response) {};
}

// define list of metrics
message MetricsList {
  repeated Metric metrics = 1;
}

// define one metric via name and its value
message Metric {
  string name = 1;
  int64 value = 2;
}

// response from the server
message Response {
  MetricsList list = 1;
  string error = 2;
}    
\end{lstlisting}

Soubor (výpis \ref{external-calc-new-def}) obsahuje definice servisu, struktur a všech polí potřebných pro komunikaci mezi klientem a serverem pomocí gRPC technologie. Servis \texttt{ExternalCalculation} definuje metodu \texttt{Calculate} která přijímá argument \texttt{MetricsList} a vrací definovanou strukturu \texttt{Response}. Vstupní argument je list metrik definovaný jako dvojice \texttt{name} a \texttt{value} popisující název a hodnotu metriky a jsou typu \texttt{string} a \texttt{int64} v tomto pořadí. Klíčové slovo \texttt{repeated} definuje pole, které se může opakovat 0 a vícekrát v správně definované zprávě \texttt{message} a jejich pořadí je zachováno. 

Pro správnou funkčnost nového mechanismu musí být splněno několik podmínek předem (navíc k základním podmínkám jako úspěšné nasazení samotné KEDY).

\begin{itemize}
    \item KEDA úspěšně získá externí metriky ze škálovacích komponent pomocí komponenty Metrics Server
    \item Rozhraní pro externí výpočet je správně definované v objektu ScaledObject
    \item Uživatel správně definoval gRPC server a jeho metody
    \item KEDA dokáže úspěšně navázat spojení s externím gRPC serverem
\end{itemize}

Po splnění všech podmínek je možné přidat rozhraní externích výpočtů do běžícího clusteru a uživatel může využít svoji vlastní logiku pro škálovací chování.

\subsection{Příklad nasazení s externí kalkulací}

\subsubsection{Objekt ScaledObject}

Pro nasazení KEDY s využitím externí kalkulace je potřeba definovat pole pro rozšíření v objektu ScaledObject (výpis \ref{last-example-so}).

\begin{lstlisting}[label={last-example-so},caption={},language=yaml]
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
...
spec:
  advanced:
    externalCalculation: #external calculation interface
    - name: exponential-smoothing
      url: "localhost:50051" #this is endpoint for gRPC server
      timeout: 15
...
  triggers: # triggers
  - type: kubernetes-workload
    name: triggerKW
    metadata:
      podSelector: 'pod=workload-test'
      value: '1'
  - type: metrics-api
    name: triggerAPI
    metadata:
      targetValue: "2"
      url: "https://mockbin.org/bin/336a8d99-9e09-4f1f-979d-851a6d1b1423"
      valueLocation: "tasks"
\end{lstlisting}

\subsubsection{Generace knihoven a protokolový buffer}

Po úspěšném nasazení KEDY, definice objektu ScaledObject a cílené aplikace na škálování je třeba mít funkční gRPC server aby se KEDA mohla k němu připojit. Pomocí \texttt{proto} souboru pro definici (výpis \ref{external-calc-new-def}) se zdrojové soubory vygenerují pomocí kompilátoru \texttt{protoc} pomocí příkazové řádky. Před spuštěním příkazu je očekáváno vytvořená, že jsou předem vytvořené složky \texttt{client} a \texttt{server}. Například jako: 

\begin{verbatim}
protoc --go_out=client --python_out=server file.proto
\end{verbatim}

\subsubsection{Klient a Server}

kde metody pro klienta se vytvoří v jazyce Go a server v jazyce Python. Soubor \texttt{file.proto} je IDL soubor definice, ze které se tyto zdrojové soubory generují. Uživatel vytvoří modul v Python, který importuje vygenerovanou knihovnu čímž může využít její metody a implementuje logiku pro volanou funkci \texttt{Calculate}. Kód na straně klienta musí být začleněn do KEDA architektury aby bylo možné volat požadovanou metodu. Pokud je možné se připojit na koncový bod definovaný dříve, jakmile KEDA získá externí metriky, místo odeslání objektu HPA zavolá metodu \texttt{Calculate}.

Uvnitř metody je na straně serveru implementován statistický algoritmus \texttt{Exponential smoothing}. Metoda pro predikci časových řad využívající vážený průměr minulých hodnot pro predikci hodnot budoucích. Podstatou je, že čím starší jsou hodnoty, tím menší mají váhu. Očekávaným výsledkem je vyhlazená predikce škálovaných replik. Při aktuálním způsobu implementace se může vzít průměr ze 2 škálovacích komponent který se zkombinuje do 1 hodnotu v určitém čase. Při zavolání metody se na straně serveru hodnoty uloží do lokálního souboru, kde se zaznamenává historie škálování podle které se predikce vytváří. 

\subsubsection{Škálování}

Jakmile je finální hodnota vypočtena, vrátí klientovi (KEDA) strukturu \texttt{Response}, kde klient ověří, zda došlo k chybě v procesu výpočtu. Jestli k chybě nedošlo a není definovaná další externí výpočetní metoda, odešle vrácenou hodnotu objektu HPA, který následně naškáluje hodnotu na vypočtený počet replik.

\subsubsection{Limity a rozšíření}

Možným rozšířením je vylepšením servisní definice v IDL souboru pro zaznamenávání ještě časového razítka pro lehčí využití s metodami, které pracují s časem (výpis \ref{timestamp-add}).

\begin{lstlisting}[label={timestamp-add},language=protobuf,caption={Možné vylepšení struktury}]
message Metric {
  string name = 1;
  int64 value = 2;
  Timestamp time = 3;
}
\end{lstlisting}

Potencionálním limitem je doba, kterou bude trvat výpočet na straně serveru (při dotazování se databáze, složitost algoritmu apod.). Kdyby doba dotazování na metriky byla kratší než doba výpočtu mohlo by dojít k nepřesnostem ve škálování. Implementace očekává, že uživatel vyřeší potencionální neshody v takovém případě. Dalším možným rozšířením je implementace kontrolního mechanismu, který by předcházel případným neshodám v časování.

\chapter{Závěr}

Cílem bakalářské práce bylo navržení rozšíření jádra KEDY za účelem vytvoření nového rozhraní poskytující uživateli možnost definovat škálovací formuli, která zachytí sbírané metriky ze všech externích škálovacích komponent a pomocí aritmetických či podmíněných výrazů je upraví pro následný výpočet replik. Druhá část navrhuje rozšíření o kompletně externí způsob výpočtu či modifikaci metrik pomocí gRPC technologie.

V rámci implementace formule bylo potřeba nejprve definovat nové struktury pro objekt ScaledObject definující škálovací komponenty a jejich chování. Zároveň s tím bylo potřeba upravit konfiguraci generovaného objektu HPA. První větší rozhodnutí při modifikaci jádra KEDY bylo rozhodnutí, jakým způsobem bude celý proces modifikován. Po zvážení 2 hlavních možnosti bylo rozhodnuto o implementaci vlastní škálovací komponenty, již bude objekt HPA žádat o metriky. Jedná se o implementaci 1 externího scaleru, čímž docílíme snížení počtu volání API serveru. Odesílání metrik objektu HPA je přerušeno po jejich získání, kde se metriky modifikují podle definované formule.

KEDA byla následně rozšířena o implementaci rozhraní pro metody externích výpočtů pomocí gRPC technologie s protokolovými buffery. Toto rozšíření dovoluje uživateli využít gRPC technologie pro vytvoření vlastní metody pro modifikaci díky implementaci gRPC serveru. Uživatel definuje koncový bod, na který se KEDA (klient) může připojit a žádat o metriky pro škálování.

Motivací pro tato rozšíření byly zpětné ohlasy zákazníků, kterým předešlé limitované škálování bez jakékoliv komplexních výrazů nestačila. Těmito rozšířeními KEDA dovolí uživatelúm mnohem větší flexibility při rozhodování o škálovacím chování jejich aplikací případně definování kompletně vlastních výpočetních metod pro modifikaci metrik, jako různorodé, prediktivní algoritmy či neuronové sítě.  
%===============================================================================

% Pro kompilaci po částech (viz projekt.tex) nutno odkomentovat
%\end{document}
