💡 TL;DR & Repozytorium

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:

  1. Każde żądanie EPP to żądanie POST na endpoint https://registry.dns.pl/registry/epp.
  2. Treść żądania to czysty dokument XML zgodny ze schematami epp-2.1.xsd, domain-2.1.xsd, contact-2.1.xsd itp.
  3. Stan sesji nie jest utrzymywany gniazdem TCP, lecz nagłówkiem ciasteczka HTTP (SESSIONID lub JSESSIONID).

Oto jak wygląda pełna sekwencja zestawienia sesji i wykonania operacji w naszym silniku:

⚡ NASK EPP over HTTPS Flow Sequence Schema 2.1
1
Adcelero Engine → NASK Rejestr
POST /registry/epp <epp><command><greet/></command></epp>
2
NASK Rejestr → Adcelero Engine
<greeting> (Supported schemas: epp-2.1, domain-2.1, contact-2.1)
3
Adcelero Engine → NASK Rejestr
POST <login><clID>ADC_PL</clID><pw>••••••••</pw></login>
4
NASK Rejestr → Adcelero Engine
HTTP 200 Set-Cookie: SESSIONID=9a8b7c6d5e4f3a2b1c (Atomowy zapis w pamięci aktora)
5
Adcelero Engine → NASK Rejestr (Sprawdzenie domeny)
POST /registry/epp (Cookie: SESSIONID=...) <domain:check><domain:name>koduj.pl</domain:name></domain:check>
6
NASK Rejestr → Adcelero Engine
<response><result code="1000"/><check:cd><check:name avail="1">koduj.pl</check:name></check:cd>

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 (.plregistry.pl, .euregistry.eu, .comregistry.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):

RegistryFactory.scala
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:

DomainCheckXml.scala
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ą status clientRenewProhibited.
  • 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.scala i natychmiast odsyła potwierdzenie poll op="ack":
PollProcessor.scala
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.

📦 Scala 2.12 / Akka / Play
🛡️ NASK EPP Schema 2.1
Latency p99 < 80ms
📜 Apache 2.0 License
git clone https://github.com/adcelero/elasticname-epp-engine.git
⭐ Zobacz Kod i Zostaw Gwiazdkę na GitHubie 🔍 Zarejestruj Domenę w Adcelero (.PL za 15 zł)
🏛️
Polecana Lektura: Warsztat Architekta

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.

Czytaj Pełny Esej Doktrynalny →

Sklonuj, postaw, przetestuj i zostaw gwiazdkę ⭐. Czekamy na Wasze Pull Requesty, issues i dyskusję w komentarzach na Wykopie, Dev.to oraz X!