Heim > Datenbank > Redis > Eine ausführliche Analyse des Sentinel-Failovers in Redis

Eine ausführliche Analyse des Sentinel-Failovers in Redis

青灯夜游
Freigeben: 2021-10-28 10:34:10
nach vorne
2032 Leute haben es durchsucht

Dieser Artikel führt Sie durch den Failover (Sentinel) in Redis. Ich hoffe, er wird Ihnen helfen!

Eine ausführliche Analyse des Sentinel-Failovers in Redis

Wenn zwei oder mehr Redis-Instanzen eine Master-Slave-Beziehung bilden, weist der von ihnen gebildete Cluster ein gewisses Maß an Hochverfügbarkeit auf: Wenn der Master ausfällt, kann der Slave zum neuen Master werden und externe Lese- und Schreibdienste bereitstellen. Dieser Betriebsmechanismus wird zu einem Failover. [Verwandte Empfehlungen: Redis-Video-Tutorial]

Wer wird also den Fehler des Masters entdecken und Failover-Entscheidungen treffen?

Eine Möglichkeit besteht darin, einen Daemo-Prozess zur Überwachung aller Master-Slave-Knoten aufrechtzuerhalten, wie in der folgenden Abbildung dargestellt:

Eine ausführliche Analyse des Sentinel-Failovers in Redis

In einem Redis-Cluster gibt es einen Master und zwei Slaves . Der Daemon ist jedoch ein einzelner Knoten und seine Verfügbarkeit kann nicht garantiert werden. Es müssen mehrere Daemons eingeführt werden, wie in der folgenden Abbildung dargestellt:

Eine ausführliche Analyse des Sentinel-Failovers in Redis

Mehrere Daemons lösen das Verfügbarkeitsproblem, es treten jedoch Konsistenzprobleme auf. Wie kann eine Einigung darüber erzielt werden, ob ein bestimmter Master verfügbar ist? Im obigen Bild sind beispielsweise die beiden Netzwerke Daemon1 und Master blockiert, aber die Verbindung zwischen Daemon und Master ist reibungslos. Muss der Masterknoten zu diesem Zeitpunkt ein Failover durchführen?

Redis‘ Sentinel bietet eine Reihe von Interaktionsmechanismen zwischen mehreren Daemons und wird zu einem Daemon-Cluster, der auch als Sentinel-Knoten bezeichnet wird. Wie in der Abbildung unten gezeigt:

Eine ausführliche Analyse des Sentinel-Failovers in Redis

Diese Knoten kommunizieren, wählen und verhandeln miteinander und zeigen Konsistenz bei der Fehlererkennung und der Failover-Entscheidung des Masterknotens.

Der Sentinel-Cluster überwacht eine beliebige Anzahl von Mastern und Slaves unter dem Master und rüstet den Offline-Master automatisch von einem Slave unter ihm auf einen neuen Master hoch, um weiterhin Befehlsanfragen zu verarbeiten.

Sentinel starten und initialisieren

Um einen Sentinel zu starten, können Sie den Befehl:

./redis-sentinel ../sentinel.conf
Nach dem Login kopieren

oder den Befehl:

./redis-server ../sentinel.conf --sentinel
Nach dem Login kopieren

verwenden. Wenn ein Sentinel startet, muss er die folgenden Schritte ausführen:

Initialisieren Sie den Server

Sentinel Essence Das Obige ist ein Redis-Server, der in einem speziellen Modus ausgeführt wird. Er führt andere Aufgaben aus als ein gewöhnlicher Redis-Server, und der Initialisierungsprozess ist nicht genau derselbe. Beispielsweise lädt die normale Redis-Serverinitialisierung RDB- oder AOF-Dateien, um Daten wiederherzustellen, Sentinel lädt sie jedoch nicht, wenn es gestartet wird, da Sentinel keine Datenbank verwendet.

Ersetzen Sie den von gewöhnlichen Redis-Servern verwendeten Code durch Sentinel-spezifische Codes.

Ersetzen Sie einige der von gewöhnlichen Redis-Servern verwendeten Codes durch Sentinel-spezifische Codes. Beispielsweise verwendet ein normaler Redis-Server server.c/redisCommandTable als Befehlstabelle des Servers:

truct redisCommand redisCommandTable[] = {
    {"module",moduleCommand,-2,"as",0,NULL,0,0,0,0,0},
    {"get",getCommand,2,"rF",0,NULL,1,1,1,0,0},
    {"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0},
    {"setnx",setnxCommand,3,"wmF",0,NULL,1,1,1,0,0},
    {"setex",setexCommand,4,"wm",0,NULL,1,1,1,0,0},
    {"psetex",psetexCommand,4,"wm",0,NULL,1,1,1,0,0},
    {"append",appendCommand,3,"wm",0,NULL,1,1,1,0,0},
    .....
    {"del",delCommand,-2,"w",0,NULL,1,-1,1,0,0},
    {"unlink",unlinkCommand,-2,"wF",0,NULL,1,-1,1,0,0},
    {"exists",existsCommand,-2,"rF",0,NULL,1,-1,1,0,0},
    {"setbit",setbitCommand,4,"wm",0,NULL,1,1,1,0,0},
    {"getbit",getbitCommand,3,"rF",0,NULL,1,1,1,0,0},
    {"bitfield",bitfieldCommand,-2,"wm",0,NULL,1,1,1,0,0},
    {"setrange",setrangeCommand,4,"wm",0,NULL,1,1,1,0,0},
    {"getrange",getrangeCommand,4,"r",0,NULL,1,1,1,0,0},
    {"substr",getrangeCommand,4,"r",0,NULL,1,1,1,0,0},
    {"incr",incrCommand,2,"wmF",0,NULL,1,1,1,0,0},
    {"decr",decrCommand,2,"wmF",0,NULL,1,1,1,0,0},
    {"mget",mgetCommand,-2,"rF",0,NULL,1,-1,1,0,0},
    {"rpush",rpushCommand,-3,"wmF",0,NULL,1,1,1,0,0},
    {"lpush",lpushCommand,-3,"wmF",0,NULL,1,1,1,0,0}
    ......
    }
Nach dem Login kopieren

Sentinel verwendet sentinel.c/sentinelcmds als Serverliste, wie unten gezeigt:

struct redisCommand sentinelcmds[] = {
    {"ping",pingCommand,1,"",0,NULL,0,0,0,0,0},
    {"sentinel",sentinelCommand,-2,"",0,NULL,0,0,0,0,0},
    {"subscribe",subscribeCommand,-2,"",0,NULL,0,0,0,0,0},
    {"unsubscribe",unsubscribeCommand,-1,"",0,NULL,0,0,0,0,0},
    {"psubscribe",psubscribeCommand,-2,"",0,NULL,0,0,0,0,0},
    {"punsubscribe",punsubscribeCommand,-1,"",0,NULL,0,0,0,0,0},
    {"publish",sentinelPublishCommand,3,"",0,NULL,0,0,0,0,0},
    {"info",sentinelInfoCommand,-1,"",0,NULL,0,0,0,0,0},
    {"role",sentinelRoleCommand,1,"l",0,NULL,0,0,0,0,0},
    {"client",clientCommand,-2,"rs",0,NULL,0,0,0,0,0},
    {"shutdown",shutdownCommand,-1,"",0,NULL,0,0,0,0,0},
    {"auth",authCommand,2,"sltF",0,NULL,0,0,0,0,0}
}
Nach dem Login kopieren

Initializing Sentinel status

Der Server initialisiert eine sentinel.c/sentinelState-Struktur (speichert den gesamten Status im Zusammenhang mit Sentinel-Funktionen im Server).

struct sentinelState {
 
    char myid[CONFIG_RUN_ID_SIZE+1]; /* This sentinel ID. */
    
    //当前纪元,用于实现故障转移
    uint64_t current_epoch;         /* Current epoch. */
    
    //监视的主服务器
    //字典的键是主服务器的名字
    //字典的值则是一个指向sentinelRedisInstances结构的指针
    dict *masters;      /* Dictionary of master sentinelRedisInstances.
                           Key is the instance name, value is the
                           sentinelRedisInstance structure pointer. */
    //是否进入tilt模式
    int tilt;           /* Are we in TILT mode? */
    
    //目前正在执行的脚本数量
    int running_scripts;    /* Number of scripts in execution right now. */
    
    //进入tilt模式的时间
    mstime_t tilt_start_time;       /* When TITL started. */
    
    //最后一次执行时间处理器的时间
    mstime_t previous_time;         /* Last time we ran the time handler. */
    
    // 一个FIFO队列,包含了所有需要执行的用户脚本
    list *scripts_queue;            /* Queue of user scripts to execute. */
    
    char *announce_ip;  /* IP addr that is gossiped to other sentinels if
                           not NULL. */
    int announce_port;  /* Port that is gossiped to other sentinels if
                           non zero. */
    unsigned long simfailure_flags; /* Failures simulation. */
    int deny_scripts_reconfig; /* Allow SENTINEL SET ... to change script
                                  paths at runtime? */
}
Nach dem Login kopieren

Initialisieren Sie die Überwachungs-Master-Serverliste von Sentinel basierend auf der angegebenen Konfigurationsdatei.

Die Initialisierung des Sentinel-Status löst die Initialisierung des Master-Wörterbuchs aus, und die Initialisierung des Master-Wörterbuchs basiert auf der geladenen Sentinel-Konfigurationsdatei fortfahren.

Der Schlüssel des Wörterbuchs ist der Name des überwachenden Hauptservers, und der Wert des Wörterbuchs ist die Struktur sentinel.c/sentinelRedisInstance, die dem überwachten Hauptserver entspricht.

Einige Attribute der sentinelRedisInstance-Struktur sind wie folgt:

typedef struct sentinelRedisInstance {
    //标识值,记录了实例的类型,以及该实例的当前状态
    int flags;      /* See SRI_... defines */
    
    //实例的名字
    //主服务器的名字由用户在配置文件中设置
    //从服务器以及Sentinel的名字由Sentinel自动设置
    //格式为ip:port,例如“127.0.0.1:26379”
    char *name;     /* Master name from the point of view of this sentinel. */
    
    //实例运行的ID
    char *runid;    /* Run ID of this instance, or unique ID if is a Sentinel.*/
    
    //配置纪元,用于实现故障转移
    uint64_t config_epoch;  /* Configuration epoch. */
    
    //实例的地址
    sentinelAddr *addr; /* Master host. */
    
    //sentinel down-after-milliseconds选项设定的值
    //实例无响应多少毫秒之后才会被判断为主观下线(subjectively down)
    mstime_t down_after_period; /* Consider it down after that period. */
    
    //sentinel monitor <master-name> <ip> <redis-port> <quorum>选项中的quorum
    //判断这个实例为客观下线(objective down)所需的支持投票的数量
    unsigned int quorum;/* Number of sentinels that need to agree on failure. */  
    //sentinel parallel-syncs <master-name> <numreplicas> 选项的numreplicas值
    //在执行故障转移操作时,可以同时对新的主服务器进行同步的从服务器数量
    int parallel_syncs; /* How many slaves to reconfigure at same time. */
    
    //sentinel failover-timeout <master-name> <milliseconds>选项的值
    //刷新故障迁移状态的最大时限
    mstime_t failover_timeout;      /* Max time to refresh failover state. */
}
Nach dem Login kopieren

Beim Starten von Sentinel wird beispielsweise die folgende Konfigurationsdatei konfiguriert:

# sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor master1 127.0.0.1 6379 2

# sentinel down-after-milliseconds <master-name> <milliseconds>
sentinel down-after-milliseconds master1 30000

# sentinel parallel-syncs <master-name> <numreplicas>
sentinel parallel-syncs master1 1

# sentinel failover-timeout <master-name> <milliseconds>
sentinel failover-timeout master1 900000
Nach dem Login kopieren

Dann erstellt Sentinel die Instanzstruktur wie unten gezeigt für den Hauptserver-Master1:

Eine ausführliche Analyse des Sentinel-Failovers in Redis

Sentinel-Status Und die Organisation des Master-Wörterbuchs ist wie folgt:

Eine ausführliche Analyse des Sentinel-Failovers in Redis

Erstellen Sie eine Netzwerkverbindung zum Hauptserver.

Erstellen Sie eine Netzwerkverbindung zum überwachten Hauptserver. Sentinel wird zum Client des Hauptservers und senden Sie Nachrichten an den Hauptserverbefehl und erhalten Sie Informationen aus Befehlsantworten.

Sentinel erstellt zwei asynchrone Netzwerkverbindungen zum Hauptserver:

  • Befehlsverbindung, die zum Senden von Befehlen an den Hauptserver und zum Empfangen von Befehlsantworten verwendet wird.
  • Abonnementverbindung, zum Abonnieren des Kanals _sentinel_:hello des Hauptservers

Eine ausführliche Analyse des Sentinel-Failovers in Redis

Sentinel发送信息和获取信息

  • Sentinel默认会以每十秒一次的频率,通过命令连接向被监视的master和slave发送INFO命令

    通过master的回复可获取master本身信息,包括run_id域记录的服务器运行ID,以及role域记录的服务器角色。另外还会获取到master下的所有的从服务器信息,包括slave的ip地址和port端口号。Sentinel无需用户提供从服务器的地址信息,由master返回的slave的ip地址和port端口号,可以自动发现slave。

    当Sentinel发现master有新的slave出现时,Sentinel会为这个新的slave创建相应的实例外,Sentinel还会创建到slave的命令连接和订阅连接。

    根据slave的INFO命令的回复,Sentinel会提取如下信息:

    1.slave的运行ID run_id

    2.slave的角色role

    3.master的ip地址和port端口

    4.master和slave的连接状态master_link_status

    5.slave的优先级slave_priority

    6.slave的复制偏移量slave_repl_offset

  • Sentinel在默认情况下会以每两秒一次的频率,通过命令连接向所有被监视的master和slave的_sentinel_:hello频道发送一条信息

    发送以下格式的命令:

     PUBLISH _sentinel_:hello   "<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>,<m_ip>,<m_port>,<m_epoch>"
Nach dem Login kopieren

以上命令相关参数意义:

参数意义
s_ipSentinel的ip地址
s_portSentinel的端口号
s_runidSentinel的运行ID
s_runidSentinel的运行ID
m_name主服务器的名字
m_ip主服务器的IP地址
m_port主服务器的端口号
m_epoch主服务器当前的配置纪元
  • Sentinel与master或者slave建立订阅连接之后,Sentinel就会通过订阅连接发送对_sentinel_:hello频道的订阅,订阅会持续到Sentinel与服务器的连接断开为止

命令如下所示:

SUBSCRIBE sentinel:hello

Eine ausführliche Analyse des Sentinel-Failovers in Redis

如上图所示,对于每个与Sentinel连接的服务器 ,Sentinel既可以通过命令连接向服务器频道_sentinel_:hello频道发送信息,又通过订阅连接从服务器的_sentinel_:hello频道接收信息。

  • sentinel间会相互感知,新加入的sentinel会向master的_sentinel_:hello频道发布一条消息,包括自己的消息,其它该频道订阅者sentinel会发现新的sentinel。随后新的sentinel和其它sentinel会创建长连接。

相互连接的各个Sentinel可以进行信息交换。Sentinel为master创建的实例结构中的sentinels字典保存了除Sentinel本身之外,所有同样监视这个主服务器的其它Sentinel信息。

前面也讲到sentinel会为slave创建实例(在master实例的slaves字典中)。现在我们也知道通过sentinel相互信息交换,也创建了其它sentinel的实例(在master实例的sentinels字典中)。我们将一个sentinel中保存的实例结构大概情况理一下,如下图所示:

Eine ausführliche Analyse des Sentinel-Failovers in Redis

从上图可以看到slave和sentinel字典的键由其ip地址和port端口组成,格式为ip:port,其字典的值为其对应的sentinelRedisInstance实例。

master的故障发现

主观不可用

默认情况下Sentinel会以每秒一次的频率向所有与它创建了命令连接的master(包括master、slave、其它Sentinel)发送PING命令,并通过实例返回的PING命令回复来判断实例是否在线。

PING命令回复分为下面两种情况:

  • 有效回复:实例返回 +PONG、-LOADING、-MASTERDOWN三种回复的一种

  • 无效回复:除上面有效回复外的其它回复或者在指定时限内没有任何返回

Sentinel配置文件中的设置down-after-milliseconds毫秒时效内(各个sentinel可能配置的不相同),连续向Sentinel返回无效回复,那么sentinel将此实例置为主观下线状态,在sentinel中维护的该实例flags属性中打开SRI_S_DOWN标识,例如master如下所示:

Eine ausführliche Analyse des Sentinel-Failovers in Redis

客观不可用

在sentinel发现主观不可用状态后,它会将“主观不可用状态”发给其它sentinel进行确认,当确认的sentinel节点数>=quorum,则判定该master为客观不可用,随后进入failover流程。

上面说到将主观不可用状态发给其它sentinel使用如下命令:

SENTINEL is-master-down-by-addr <ip> <port> <current_epoch> <runid>
Nach dem Login kopieren

各个参数的意义如下:

  • ip:被sentinel判断为主观下线的主服务器的ip地址
  • port: 被sentinel判断为主观下线的主服务器的port地址
  • current_epoch:sentinel的配置纪元,用于选举领头Sentinel
  • runid:可以为*号或者Sentinel的运行ID,*号代表检测主服务器客观下线状态。Sentinel的运行ID用于选举领头Sentinel

接受到以上命令的sentinel会反回一条包含三个参数的Multi Bulk回复

1)<down_state> 目标sentinel对该master检查结果,1:master已下线 2:master未下线

2)<leader_runid> 两种情况,*表示仅用于检测master下线状态 ,否则表示局部领头Sentinel的运行ID(选举领头Sentinel)

3)<leader_epoch> 当leader_runid为时,leader_epoch始终为0。不为时则表示目标Sentinel的局部领头Sentinel的配置纪元(用于选举领头Sentinel)

其中节点数量限制quorum为sentinel配置文件中配置的

sentinel monitor <master-name> <ip> <redis-port> <quorum>
Nach dem Login kopieren

quorum选项,不同的sentinel配置的可能不相同。

当sentinel认为master为客观下线状态,则会将master属性中的flags的SRI_O_DOWN标识打开,例如master如下图所示:

Eine ausführliche Analyse des Sentinel-Failovers in Redis

Sentinel-Anführer wählen

Wenn ein Master ausfällt, können mehrere Sentinel-Knoten durch gleichzeitige Interaktion den „subjektiven Nichtverfügbarkeitszustand“ des anderen erkennen und bestätigen, gleichzeitig den „objektiven Nichtverfügbarkeitszustand“ erreichen und dies planen ein Failover einleiten. Am Ende kann es aber nur einen Sentinel-Knoten als Failover-Initiator geben, dann muss ein „Sentinel Leader“ gewählt werden und ein Sentinel Leader-Wahlprozess muss gestartet werden. Der Sentinel-Mechanismus von Redis verwendet etwas Ähnliches wie das Raft-Protokoll, um diesen Wahlalgorithmus zu implementieren:

1 Die Epochenvariable von sentinelState ähnelt dem Begriff (Wahlrunde) im Raft-Protokoll.

2. Jeder Sentinel-Knoten, der bestätigt, dass der Master „objektiv nicht verfügbar“ ist, sendet seine eigene Wahlanfrage an die Umgebung ; , current_epoch ist seine eigene Konfigurationsepoche, run_id ist seine eigene laufende ID)

3 Wenn jeder Sentinel-Knoten keine anderen Wahlanfragen erhalten hat, wird die Absicht auf den ersten Kandidaten-Sentinel gesetzt und darauf antworten (Wer zuerst kommt, mahlt zuerst); wenn die Absicht in dieser Runde geäußert wurde, werden andere Kandidaten abgelehnt und die bestehende Absicht wird beantwortet (wie oben für die drei Parameter erwähnt). Multi-Bulk-Antwort, down_state ist 1, Leader_runid ist die laufende ID des Quell-Sentinels, der die zum ersten Mal empfangene Wahlanfrage initiiert hat, Leader_epoch ist die Konfigurationsepoche des Quell-Sentinels, der die zum ersten Mal empfangene Wahlanfrage initiiert hat)

4 ​​Jeder Initiierungsparameter If Der Sentinel-Knoten, der die Anfrage auswählt, erhält mehr als die Hälfte der Absichten, einem teilnehmenden Sentinel zuzustimmen (vielleicht sich selbst), dann wird der Sentinel als Anführer bestimmt. Wenn diese Runde lange genug dauert und kein Anführer ausgewählt wird, wird die nächste Runde gestartet

Leader Sentinel Nach der Bestätigung wählt

Leader Sentinel nach bestimmten Regeln einen aus allen Slaves des Masters als neuen Master aus.

Failover-Failover

Nachdem der Sentinel-Anführer gewählt wurde, führt der Sentinel-Anführer ein Failover auf dem Offline-Master durch:

Der Sentinel-Anführer wählt einen

aus allen Slaves des Offline-Masters aus, der in gutem Zustand ist, einen Slave mit vollständigen Daten

, und senden Sie dann den Befehl:
    SLAVEOF no one
  • an diesen Slave, um diesen Slave in einen Master umzuwandeln.

    Schauen wir uns an, wie der neue Master ausgewählt wird? Der Sentinel-Anführer speichert alle Offline-Slaves in einer Liste und filtert und filtert dann nach den folgenden Regeln: Der Standardwert ist 100. Je niedriger die Replikatpriorität, desto höher die Priorität. 0 ist eine besondere Priorität, was bedeutet, dass kein Upgrade auf Master möglich ist.

    Wenn mehrere Slaves mit gleichen Prioritäten vorhanden sind, wird der Slave mit dem größten Kopierversatz ausgewählt

    (die Daten sind vollständiger)

    Wenn mehrere Slaves mit gleichen Prioritäten vorhanden sind, wird der größte Kopierversatz ausgewählt ausgewählt Wenn der Slave mit dem größten Volumen ausgewählt ist, dann
  • Wählen Sie den Slave mit der kleinsten laufenden ID aus

  • Nachdem Sie den Slave ausgewählt haben, der auf einen neuen Master aktualisiert werden muss, sendet der Sentinel Leader den Befehl SLAVEOF no one zu diesem Sklaven.
  • Danach sendet Sentinel einmal pro Sekunde INFO an den aktualisierten Slave (normalerweise alle zehn Sekunden). Wenn die Antwortrolle von Slave zu Master wechselt, weiß der Sentinel-Anführer, dass er zum Master aktualisiert wurde.

    Sentinel Leader sendet den SLAVEOF-Befehl (SLAVEOF ) an den Slave unter dem Offline-Master, um
  • den neuen Master zu kopieren
  • .

    Legen Sie den alten Master als Slave des neuen Masters fest und überwachen Sie ihn weiterhin. Wenn er wieder online ist, führt Sentinel den Befehl aus, um ihn zum Slave des neuen Masters zu machen.

Zusammenfassung

  • Sentinel ist eine Hochverfügbarkeitslösung für Redis. Die Anzahl der Knoten im Sentinel-Cluster muss >= 3 sein.

    Standardmäßig führt Sentinel alle zehn Sekunden Informationen zum Master und Slave aus um Master-Änderungsinformationen zu ermitteln und neue Slave-Knoten zu entdecken.

    Standardmäßig sendet Sentinel alle zwei Sekunden über Befehlsverbindungen eine Nachricht an den _sentinel_:hello-Kanal aller überwachten Master und Slaves, um mit anderen Sentinels zu interagieren.
  • Standardmäßig sendet Sentinel jede Sekunde PING an Master, Slave und andere Sentinels Befehl zur Feststellung, ob die andere Partei offline ist

    Sentinel Leader wird nach bestimmten Regeln gewählt.
Der Sentinel Leader führt einen Failover-Vorgang durch und wählt einen neuen Master, der den Offline-Master ersetzt.

Weitere Kenntnisse zum Thema Programmierung finden Sie unter: Einführung in die Programmierung

! !

Das obige ist der detaillierte Inhalt vonEine ausführliche Analyse des Sentinel-Failovers in Redis. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Verwandte Etiketten:
Quelle:juejin.cn
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage