Napisaliśmy w Scali i bibliotece Akka nowoczesny, w pełni asynchroniczny silnik protokołu EPP (Extensible Provisioning Protocol) do bezpośredniej komunikacji z rejestrem NASK. Osiągnęliśmy czas odpowiedzi p99 < 80ms. Dziś udostępniamy pełne repozytorium na zasadach Open Source: github.com/adcelero/elasticname-epp-engine.
Wstęp: Świat, w którym domenami rządzą relikty lat 90.
Kiedy rejestrujesz domenę internetową .pl w dowolnym panelu hostingowym, pod maską dzieje się coś, o czym większość programistów nigdy nie musiała myśleć. Twoja przeglądarka wysyła żądanie do panelu rejestratora, panel rejestratora autoryzuje płatność, a następnie wysyła ustrukturyzowaną ramkę XML do centralnego rejestru NASK (Naukowa i Akademicka Sieć Komputerowa) za pośrednictwem protokołu EPP (Extensible Provisioning Protocol).
Większość rejestratorów w Polsce budowała swoje silniki EPP 10–15 lat temu. W architekturze typowego brokera znajdziecie:
- Jednowątkowe procesy w starym PHP lub perlowe skrypty blokujące połączenie na sockecie podczas oczekiwania na odpowiedź rejestru.
- Brak odporności na zerwanie sesji – co skutkuje błędem rejestracji podczas próby przechwycenia domeny w milisekundach.
- Ciężkie warstwy orkiestracji SOAP/WSDL, które narzucają potężny overhead pamięciowy i narzut na serializację/deserializację drzew DOM.
Budując Adcelero.pl, postawiliśmy sobie ambitny cel inżynieryjny: stworzyć najszybszy, w pełni asynchroniczny i odporny na błędy silnik rejestracji w Polsce, a następnie oddać go społeczności jako Open Source.
W tym artykule rozkładamy na czynniki pierwsze architekturę naszego silnika napisanego w Scali i bibliotece Akka, omawiamy specyfikę protokołu NASK EPP Schema 2.1 oraz pokazujemy, jak zeszliśmy z czasem przetwarzania p99 poniżej 80 ms.
1. Specyfika NASK EPP: Kiedy standard spotyka rzeczywistość
Oficjalny standard EPP zdefiniowany w RFC 5730 zakłada komunikację bezpośrednio przez gniazda TCP opakowane w TLS, ze stałym strumieniem danych binarnych poprzedzonych 4-bajtowym nagłówkiem długości ramki.
Rejestr NASK dla domeny .pl poszedł jednak inną, bardzo specyficzną drogą: EPP over HTTPS. Zamiast surowego gniazda TCP:
- Każde żądanie EPP to żądanie
POSTna endpointhttps://registry.dns.pl/registry/epp. - Treść żądania to czysty dokument XML zgodny ze schematami
epp-2.1.xsd,domain-2.1.xsd,contact-2.1.xsditp. - Stan sesji nie jest utrzymywany gniazdem TCP, lecz nagłówkiem ciasteczka HTTP (
SESSIONIDlubJSESSIONID).
Oto jak wygląda pełna sekwencja zestawienia sesji i wykonania operacji w naszym silniku:
Każde zerwanie sesji (np. po stronie load balancera NASK) skutkuje kodem błędu EPP 2002 (Command use error / Session expired). Klasyczny system synchroniczny w tym momencie rzuca wyjątkiem i przerywa transakcję użytkownika. U nas sprawę rozwiązuje stan maszyny aktora transportowego, który natychmiast wykonuje samonaprawiający się relogin bez przerywania strumienia żądań.
2. Model Aktorowy: Dlaczego Akka i Scala?
W architekturze I/O dla rejestratora domen występują trzy fundamentalne wyzwania:
- Wysoka współbieżność: Równoległe sprawdzanie dostępności setek domen, transfery i odpytywanie kolejki zdarzeń bez blokowania puli wątków systemowych.
- Dynamiczny routing: System musi w ułamku milisekundy przekierować operację do odpowiedniego rejestru na podstawie końcówki TLD (
.pl→registry.pl,.eu→registry.eu,.com→registry.com). - Złożona obsługa błędów i stanów asynchronicznych: Izolacja awarii pojedynczego rejestru od reszty platformy.
Zastosowaliśmy model aktorowy Akka z dynamicznym rozwiązywaniem referencji (actorSelection):
package pl.elasticname.core
import akka.actor.{ActorRef, ActorSystem}
import akka.pattern.ask
import akka.util.Timeout
import play.api.libs.json._
import scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Future}
class RegistryFactory(system: ActorSystem)(implicit ec: ExecutionContext) {
private implicit val registryTimeout: Timeout = 5.seconds
// Dynamiczne wyszukiwanie aktora dedykowanego dla danego TLD
private def resolveRegistry(tld: String): Future[ActorRef] = {
system.actorSelection(s"/user/registry.$tld").resolveOne()
}
def execute(cmd: Command): Future[JsValue] = {
resolveRegistry(cmd.domain.domainTld).flatMap { registryActor =>
(registryActor ? cmd).mapTo[JsValue].recover {
case _: akka.pattern.AskTimeoutException =>
// Buforowanie komendy w przypadku chwilowego braku odpowiedzi rejestru
CommandBuffer.save(cmd)
Json.obj("success" -> false, "code" -> 504, "msg" -> "Registry Timeout - Command Buffered")
}
}
}
}
Główny aktor pl.elasticname.dialect.pl.Registry zarządza przepływem biznesowym: waliduje dane wejściowe za pomocą validator.pl, deleguje ramkę XML do transportu Production, a po udanej rejestracji asynchronicznie wywołuje aktora user/dns w celu utworzenia strefy w BIND9 Anycast.
3. Walka o Milisekundy: Jak zoptymalizowaliśmy czas do <80ms
Osiągnięcie p99 poniżej 80 ms w komunikacji HTTPS z zewnętrznym rejestrem wymagało wyeliminowania każdego wąskiego gardła:
A. Reusing Połączeń TLS i Connection Pooling
Ustanowienie nowego połączenia TLS (zwłaszcza z weryfikacją certyfikatu klienta mTLS w magazynie JKS) zajmuje od 120 ms do nawet 300 ms.
Nasz komponent Production.scala utrzymuje stałą pulę aktywnych połączeń HTTP/1.1 Keep-Alive z endpointem NASK. Klient Play-WS / AsyncHttpClient został skonfigurowany pod kątem natychmiastowego ponownego używania otwartych socketów z wyłączonym algorytmem Nagle'a (TCP_NODELAY = true).
B. Type-Safe Lekka Serializacja XML
Zamiast ciężkich frameworków generujących XML przez refleksję (JAXB itp.), użyliśmy bezpośrednich, typowanych szablonów w Scali, które generują ramki z zerowym narzutem pamięciowym:
package pl.elasticname.dialect.pl.xml
object DomainCheckXml {
def render(domainName: String, clTRID: String = "adcelero-req"): String = {
s"""<?xml version="1.0" encoding="UTF-8"?>
<epp xmlns="http://www.dns.pl/nask-epp-schema/epp-2.1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.dns.pl/nask-epp-schema/epp-2.1 epp-2.1.xsd">
<command>
<check>
<domain:check xmlns:domain="http://www.dns.pl/nask-epp-schema/domain-2.1"
xsi:schemaLocation="http://www.dns.pl/nask-epp-schema/domain-2.1 domain-2.1.xsd">
<domain:name>$domainName</domain:name>
</domain:check>
</check>
<clTRID>$clTRID</clTRID>
</command>
</epp>""".stripMargin
}
}
C. Autonomiczna Obsługa Reloginu (Self-Healing Transport)
Gdy NASK zwraca kod 2002 (sesja wygasła), warstwa transportowa nie rzuca błędu do użytkownika. Aktor automatycznie wykonuje sekwencję relogin, uaktualnia ciasteczko w pamięci i natychmiast ponawia oryginalne żądanie w ramach tego samego Future.
4. Pętla Zdarzeń: Asynchroniczny Poll i Ochrona Portfela
Jedną z największych pułapek dla początkujących rejestratorów w NASK jest mechanizm Auto-Renew. Jeśli domena wygasa, a rejestrator nie ustawił explicite flagi blokady, NASK może automatycznie przedłużyć domenę na kolejny rok i obciążyć depozyt finansowy Partnera.
W naszym silniku rozwiązaliśmy to dwuetapowo:
- Automatyczny
autoRenewDisable: Każda udana rejestracja natychmiast asynchronicznie wyzwala komendę EPP nakładającą statusclientRenewProhibited. - Pętla
PollProcessing: Jeśli w odpowiedzi na dowolną komendę NASK zwróci nagłówek<msgQ count="X" id="Y"/>, dedykowany aktor odbiera zdarzenie z kolejki, analizuje je klasąReactTo.scalai natychmiast odsyła potwierdzeniepoll op="ack":
case class PollDomainAutoRenewed(domainName: String, newExpDate: LocalDate)
def handlePollMessage(xml: scala.xml.Elem): Unit = {
(xml \\ "pollDomainAutoRenewed").headOption.foreach { node =>
val name = (node \ "name").text
val expDate = LocalDate.parse((node \ "curExpDate").text)
pollActor ! PollDomainAutoRenewed(name, expDate)
}
}
5. Dlaczego otwarcie tego kodu to dopiero początek?
Wierzymy, że transparentność buduje zaufanie. Polski rynek domenowy zbyt długo tkwił w cieniu zamkniętych monopoli i horrendalnych cen odnowień sięgających 200–250 zł za domenę.
Otwierając silnik ElasticName / Adcelero EPP Engine, chcemy:
- Pokazać, że nowoczesna infrastruktura domenowa może być lekka, bezpieczna i niewiarygodnie szybka.
- Umożliwić każdemu deweloperowi i zespołowi integrację z NASK bez konieczności ponownego odkrywania koła.
- Dać jasny sygnał: Adcelero.pl stawia na otwarte standardy, uczciwe ceny (15,00 zł rejestracja .PL w Klubie PRO, 50,00 zł netto odnowienie At-Cost NASK w hurcie, 59,00 zł odnowienie Standard) i szacunek dla inżynierii.
Repozytorium jest dostępne na GitHubie!
Pobierz kompletny, produkcyjny silnik EPP w Scali. Sklonuj projekt, uruchom w środowisku OTE lub produkcyjnym i buduj własne integracje.
Czego uczy 15 lat budowania systemów rozproszonych
Autorski esej doktrynalny Artura Buczka: dlaczego w inżynierii sieciowej prostota i determinizm zawsze wygrywają z modnym przekombinowaniem oraz Dekalog Architekta na kolejne 15 lat.
Sklonuj, postaw, przetestuj i zostaw gwiazdkę ⭐. Czekamy na Wasze Pull Requesty, issues i dyskusję w komentarzach na Wykopie, Dev.to oraz X!