We liepen ertegenaan dat bij onze grotere projecten de gemiddelde pipelineduur 50 minuten bedroeg en gestaag opliep naar een uur uitvoeringstijd. Dit kan vooral vervelend zijn als je gewend bent de CI te gebruiken als een snelle tool om te testen of je wijziging iets beïnvloedt. Ongeacht hoe een CI in het ontwikkelproces past; we vonden als team, dit kan beter!

Als Scenius ontwikkelen we op maat gemaakte software in meerdere projecten. De optimalisaties die in deze blogpost worden beschreven, zijn ook toegepast op de meeste van onze andere projecten. We richten ons echter eerst op de langste pipeline. Het bevat bijna 40 taken, docker-builds, Selenium/TestCafe E2E-tests, Cobertura, SonarQube, xUnit driven, deployment steps en integration en unit test suites.
De eerste stap was het meten van het totale verbruik van CI-minuten per taak. We hebben de implementatie- en handmatige taken uitgesloten, alleen de tijd die nodig is voor de meeste commits.
Dus we hebben ongeveer twee volle uren ‘CI-tijd’ nodig per gepushte commit.
Sommigen van ons vinden het prettig om te committen en tegelijkertijd te pushen. Terugdenkend aan SVN, data replication of gewoon uit gewoonte. We zien heel vaak dat gebruikers naar de origin pushen, maar niet echt een update willen op hun merge request, noch geïnteresseerd zijn in het resultaat van de pipeline. De oplossing voor ons was om die commits te filteren door te eisen dat er een merge request moet worden geopend voor feature branches om een pipeline te starten.
interruptible: true
rules:
- if: '($CI_COMMIT_BRANCH == "dev") || ( $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TITLE !~ /(?i)(^DRAFT.*)/ )'
when: always
- when: manualVoor de meest resource intensieve taken hebben we vereist dat merge requests met de tag ‘DRAFT:’ handmatig moeten worden gestart om ervoor te zorgen dat de developer daadwerkelijk geïnteresseerd is in het resultaat. Natuurlijk zodra een merge request open staat zonder Draft zal deze alle wijzigingen gelijk testen. Dus mocht er wel interesse zijn dan kan de ontwikkelaar ‘WIP:’ gebruiken.
De tweede verbetering was om pipelines te stoppen die niet langer relevant waren. Dit kan gebeuren wanneer iemand snel twee commits op dezelfde branch pusht. Hiervoor moesten we de ‘Auto-cancel’ inschakelen in de interface van Gitlab (Instellingen → CI/CD) en alle taken ‘taggen’ met “interruptible: true”

Voorheen waren onze build-taken voor artifacts parallel ingericht voor horizontale schaalbaarheid. Op het moment dat we met deze optimalisatie begonnen, hadden we meer runners dan ontwikkelaars die aan het project werkten, dus we hadden een nieuwe aanpak nodig.
Per E2E-test draaien we alle services en dependencies hetzelfde als in productie. Dit betekent dat elke E2E-run een volledige database-instantie, Rabbit MQ, Redis en dezelfde imagestags die uiteindelijk naar productie gaan. Daarom wacht de E2E-taak geduldig.
Hierin kwam Directed Acyclic Graph (DAG) van toepassing. In plaats van te wachten tot alle builds zijn voltooid, specificeren we welke Docker-images nodig zijn voor bepaalde E2E-tests, zodat ze kunnen starten voordat andere build-taken zijn voltooid.
De build-taken werden opgesplitst in “Build .NET required E2E” voor de belangrijkste services die worden gebruikt door E2E-containers en “Build .NET optional” voor images zonder testbare frontend.
Toen stuitte we op “Containerize an app with dotnet publish – .NET | Microsoft Learn“. Dit kon onze pipeline aanzienlijk versnellen.
Hier is de resulterende Gitlab CI/CD-taakdefinitie:
Build .NET required E2E:
stage: build
image: [our-registry]/scenius/docker-dotnet:7.0
interruptible: true
tags:
- BUILD2
script:
- docker login [our-registry]
- cd project.Portal/project.Portal.API
- rm project.sln
- dotnet new sln --name project
- dotnet sln add project.Portal/project.Portal.csproj [and a lot more :)]
- dotnet restore
# Transactions API
- dotnet publish project.Transactions --no-restore --os linux --arch x64 /t:PublishContainer --configuration Release -p:PublishProfile=DefaultContainer
- docker tag project-transactions:1.0.0 $HAVEN_REG/$IMAGE_TRANSACTIONS_NAME:$CI_COMMIT_SHORT_SHA
- docker tag project-transactions:1.0.0 $HAVEN_REG/$IMAGE_TRANSACTIONS_NAME:$CI_PIPELINE_ID
- docker push $HAVEN_REG/$IMAGE_TRANSACTIONS_NAME:$CI_COMMIT_SHORT_SHA
- docker push $HAVEN_REG/$IMAGE_TRANSACTIONS_NAME:$CI_PIPELINE_ID
# Portal API
- dotnet publish project.Portal --no-restore --os linux --arch x64 /t:PublishContainer --configuration Release -p:PublishProfile=DefaultContainer
- docker tag project-portal:1.0.0 $HAVEN_REG/$IMAGE_API_NAME:$CI_COMMIT_SHORT_SHA
- docker tag project-portal:1.0.0 $HAVEN_REG/$IMAGE_API_NAME:$CI_PIPELINE_ID
- docker push $HAVEN_REG/$IMAGE_API_NAME:$CI_COMMIT_SHORT_SHA
- docker push $HAVEN_REG/$IMAGE_API_NAME:$CI_PIPELINE_ID
[Repeat for each required container for E2E]Aangezien we meer dan 50 projecten in deze oplossing hebben, creëren we de SLN opnieuw om alleen het project op te nemen waarvoor we dotnet restore (Package Manager) en dotnet build willen uitvoeren om de build te versnellen.
Direct was de build-tijd teruggebracht tot 5 minuten voor alle vereiste .NET-containers.
Aan de zijde van het E2E-sjabloon hebben we de DAG-‘needs’-verwijzing opgenomen.
needs:
- job: Build .NET required E2EDit was een moeilijke keuze. Het opstarten van de E2E dependencies vergt nogal wat resources, maar na enkele metingen hebben we besloten dat dit de beste manier vooruit was. Natuurlijk hebben we eerst gekeken naar manieren om een enkele taak te versnellen, zoals een Selenium-grid of het versnellen van de ‘browser-simulatie’. Helaas zou de eerste optie de opzet te gecompliceerd maken en de tweede werd niet aanbevolen door Selenium, dus dat was geen optie. Uiteindelijk hebben we het opgesplitst in taken van maximaal 7,5 minuut.

Zoals je misschien gemerkt hebt, hebben we de gemeten verbeteringen nog niet besproken. Dit komt voornamelijk doordat een enkele verbetering niet kan worden toegeschreven aan de volledige prestatieverbetering. Wat we ook hebben gemerkt, is dat het hebben van snellere rekenkracht, niet noodzakelijk beter is voor de prestaties van de pipeline. Op dit punt hebben we de CI-minuten die nodig zijn voor hetzelfde resultaat gehalveerd en is de ‘verstopping’ aanzienlijk verminderd. Maar in onze volgende post laten we zien hoe we de CI-tijd terug hebben gebracht naar minder dan 10 minuten.
In deel twee van deze blogpost zullen we kijken naar een ander zeer belangrijk aspect van een snelle CI, compute.