När vi sökte efter en revers proxy som ingångspunkt för vår serverinfrastruktur stötte vi på det utmärkta Traefik. Vi ville naturligtvis ha lastbalansering och, naturligtvis, ville vi kunna tilldela vikter; så vi skummade igenom dokumentationen och hittade snabbt vad vi sökte efter. Traefik stödjer lastbalansering i form av Vägd Round Robin (WRR), som leder webbtrafik i cykler genom de tillgängliga tjänsterna. Detta lät precis som det vi ville ha. Men, som vi upptäckte, var det inte riktigt så. Vår resa börjar här.
Uppdatering: En dag efter att vi föreslog vår workaround i deras GitHub-repo, började Traefik-teamet arbeta på funktionen. Den kommer slutligen att landa i Traefik v3.0.
TL;DR, Du kanske vill hoppa direkt till lösningen.
Tjänster versus Servrar
Det var något vi hade missat: en liten hint i dokumentationen att WRR endast är tillgänglig för tjänster, inte för servrar.
Denna strategi är endast tillgänglig för att belastningsbalansera mellan tjänster och inte mellan servrar.
Initialt måste vi lära oss hur man konfigurerar Traefik korrekt och hade ingen aning om vad tjänster och servrar betydde i Traefiks kontext. Nu när vi vet, ville vi servera en enskild tjänst på flera servrar som följer:
services:
my_service:
loadBalancer:
servers:
- url: "https://server1:1234/"
- url: "https://server2:1234/"
- url: ...
Eftersom varje server har olika nivåer av kraft, ville vi också väga varje server. Emellertid är det inte möjligt att tilldela vikter till servrarna i detta setup, och det finns även ett öppet GitHub-ärende här sedan 2019.
Bara Traefik v1 hade en egenskap för vikt, men vi ville inte lita på äldre mjukvara.
Idéer
Vi hade olika idéer om hur vi skulle komma över denna saknade funktion.
Idé 1: Missbruka Hälsokontroller för att simulera dåliga servrar
Idén var att härma en ohälsosam server när den nått sin maximala kapacitet. På detta sätt skulle Traefik sluta dirigera trafik till servern när den inte hade några fria platser kvar.
# Example health check configuration
http:
services:
my-service:
loadBalancer:
healthCheck:
path: /health
interval: "5s"
timeout: "3s"
<pre class="wp-block-syntaxhighlighter-code"># Example FastAPI endpoint
@app.get('/health')
def health():
if COMPUTE_CAPABILITY / job_q.qsize() < 1:
return Response('Too Many Requests', status_code=429)
return Response('OK', status_code=200)</pre>
När Jobbkön skulle överstiga serverns beräkningsförmåga, skulle den rapporteras ohälsosam till Traefik inom högst åtta sekunder. Traefik skulle sedan ta bort den från Round Robin och låta andra servrar betjäna begäran. Vi testade denna lösning och det visade sig att den orsakade problem.
Först och främst är serverutfallet långsamt. Även när överbelastad, får servrarna fortfarande förfrågningar tills hälsokontrollrapporten faller. Åtta sekunder kan vara en lång tid, och att reducera intervallet var inte ett alternativ för att hålla serverbelastningen hanterbar.
För det andra, och mest överraskande, stötte vi på felmeddelanden NS_ERROR_NET_PARTIAL_TRANSFER för vissa förfrågningar. Vi undersökte inte vidare, men vi tror att detta inträffade på grund av servrar som fastnat mellan att serva långvariga förfrågningar och rapporterades som ohälsosamma, vilket orsakade att klientanslutningen bröts. Att upprätthålla en sådan infrastruktur är inte önskvärt.
För det tredje, vår tjänst är tillståndsberoende. Det betyder att vi använder klibbiga sessioner, vilket säkerställer att varje klient kommunicerar endast med en dedikerad server som tilldelats vid sessionens början. Om en av dessa servrar blir otillgänglig under en session ställer det till problem som vi trodde vi kunde lösa. Vi kunde inte. Vi drog tillbaka Idé 1 istället.
Idé 2: Manipulera Sessionen
När vi etablerar en session använder vi JavaScript-fetch() med krediter inbegripna:
await fetch('https://edge_server/', {credentials: 'include'})
På detta sätt tilldelar Traefik en ny sessionscookie till klienten med Set-Cookie-huvuden på den initiala fetch-förfrågan.
# Example Traefik configuration with sticky sessions enabled
services:
my_service:
loadBalancer:
sticky:
cookie:
name: session_name
httpOnly: true
Om vi utelämnar autentiseringsuppgifterna i begäran, skulle Traefik tilldela en ny session vid varje nytt förfrågan genom Round Robin-proceduren. Idé 2 var att låta klienten samla in nya sessioner tills den hittade en server med lediga platser kvar.
Vi behövde bara definiera en ny FastAPI-dispatch-slutpunkt som informerar klienten om servern bakom den aktuella sessionen har lediga platser, samt en fetch-återförsök-metod i JavaScript för att hitta rätt server. För slutpunkten kunde vi återanvända health()-metoden från ovan. För fetch-återförsök-metoden ville vi också hantera fallet när alla servrar var upptagna. Vi kom på följande:
<pre class="wp-block-syntaxhighlighter-code">async function fetch_retry(...args) {
for (let i = 0; i < 300; i++) {
for (let s = 0; s < N_SERVERS; s++) {
const response = await fetch(...args);
if (response.status === 503) {
// Waiting for a free slot...
if (s === (N_SERVERS - 1)) {
await new Promise(r => setTimeout(r, 20000));
}
continue;
}
return response;
}
}
return response;
}
</pre>
Nu, innan något annat anrop till fetch(), ringde vi vår dispatch-slutpunkt utan autentiseringsuppgifter:
await fetch_retry('https://edge_server/dispatch');
Och verkligen, vi fick en ny Set-Cookie-header för varje loop-iteration.
Tyvärr uppdateras inte den befintliga session-cookien när fetch_retry()-anropet görs, eftersom detta bara händer när autentiseringsuppgifter ingår, åtminstone i Firefox. Vi försökte sedan inkludera dessa och rensa sessionStorage när vi har en session för en ”dålig” server. Tyvärr klarar inte sessionStorage.clear() av att rensa autentiserings-cookien, även om den också är en session-cookie. Detta gäller även när httpOnly sätts till false (även om detta inte rekommenderas av säkerhetsskäl ändå).
Så var vi tvungna att sova en natt till innan vi kom på en annan idé.
Den Slutgiltiga Idén: Falska Servrar
Den slutgiltiga idén byggde på frågan: Hur hanterar Traefik server-URL:er?
Om vi kunde peka flera olika URL:er som Traefik ser som separata till samma server, kunde vi emulera servrar med olika vikt genom att skapa flera server-URL:er som alla pekar på samma server. Och det fungerade precis som förväntat. Vi behövde bara lägga till en icke-existerande sökväg, såsom ”/1/” eller ”/2/”, till URL:erna, och Traefik hanterade dem separat men dirigerade dem utan fel och utan att bifoga sökvägen till rätt server.
services:
my_service:
loadBalancer:
servers:
- url: "https://server_A:1234/1/"
- url: "https://server_A:1234/2/"
- url: "https://server_B:1234/"
Vi behövde bara se till att skapa rätt antal redundant fiktiva servrar-URL:er motsvarande respektive servers förmåga. Så om vi hade en server A, till exempel, som var dubbelt så kapabel som server B, då behövde vi lägga till två URL:er för server A. På detta sätt skulle server A under Round Robin få dubbelt så många förfrågningar som server B.