Anthillo /blog

ENUM i EIR bez przestojów: odporne serwisy sygnalizacyjne core network w Javie

Anthillo Team

ENUM i EIR to dwa serwisy, o których żaden użytkownik nigdy nie usłyszy, a mimo to przez jeden z nich przechodzi każde połączenie do przeniesionego numeru i każda rejestracja urządzenia w sieci komórkowej. Zbudowaliśmy oba dla polskiego operatora mobilnego jako część infrastruktury sieci rdzeniowej — klientami są tu elementy core network (MME, węzły SIP/IMS), nie ludzie z telefonami. Gdy taki serwis pada, połączenia do przeniesionych numerów lecą do złego operatora albo MME w ogóle nie wie, czy wpuścić urządzenie do sieci. Dlatego całą architekturę projektowaliśmy wokół jednego pytania: co się dzieje, gdy zawiedzie coś innego niż sam serwis.

Co robią ENUM i EIR

ENUM to serwer DNS odpowiadający na zapytania NAPTR w domenie e164.arpa — brama do NPDB, bazy przenośności numerów. Na podstawie numeru MSISDN zwraca Routing Number i mapuje go na docelową domenę SIP operatora, do którego numer został przeniesiony (mobile number portability). EIR — u nas nazywany DEIR, bo mówimy o wariancie na Diameter — odbiera zapytania 3GPP S13 (komenda ME-Identity-Check, command code 324) od MME i odpowiada statusem IMEI: whitelist, blacklist albo greylist, na podstawie danych w LDAP. Cele przepustowości: 3 tysiące TPS dla ENUM, 4 tysiące TPS dla EIR, w dwóch lokalizacjach jednocześnie.

Cache jako siatka bezpieczeństwa, nie tylko przyspieszacz

Oba serwisy mają ten sam problem: LDAP stojący za NPDB albo EIR nie może stać się pojedynczym punktem awarii dla ruchu sygnalizacyjnego. Rozwiązaniem jest Hazelcast, wbudowany jako rozproszona mapa w pamięci każdej instancji (TCP/IP discovery, TTL 10 minut, jeden backup). Pełni dwie role naraz: przy szczytowym TPS odciąża LDAP, bo większość powtarzających się zapytań o ten sam numer czy IMEI trafia do cache; a gdy LDAP jest niedostępny, serwis nadal odpowiada — ostatnią znaną wartością zamiast timeoutem. Cache ma też REST endpoint do włączania, wyłączania i podglądu rozmiaru, co w praktyce oznacza, że można go wyłączyć na produkcji bez restartu, kiedy trzeba zdebugować, czy problem leży w cache, czy w LDAP.

To świadomy kompromis: embedded klaster Hazelcast bez zewnętrznego serwera to prostsze wdrożenie, ale słabsza spójność cache między lokalizacjami. Site A i site B trzymają osobne kopie — jeśli numer zostanie przeniesiony i zaktualizowany w NPDB, każda lokalizacja dowie się o tym z osobnym opóźnieniem wynikającym z TTL. Przy usłudze read-heavy, gdzie dane zmieniają się dużo rzadziej niż są odczytywane, uznaliśmy to za akceptowalne.

Wniosek. Cache w systemie krytycznym trzeba projektować pod awarię źródła danych, nie tylko pod jego obciążenie — to zupełnie inny zestaw wymagań.

Dwie lokalizacje, jedna usługa

Georedundancja nie kończy się na dwóch instancjach serwisu za load balancerem. Eureka, nasz service discovery, działa dwustanowiskowo — profile site A i site B zamiast jednego klastra rozciągniętego między lokalizacjami. Konfigurację dostarcza Spring Cloud Config Server z osobnym panelem administracyjnym i backendem w PostgreSQL, sparametryzowany per lokalizacja i per maszyna.

Słaby punkt tego modelu to sam config server — jeśli on padnie akurat w chwili, gdy proces usługi się restartuje, usługa zostaje bez konfiguracji. Dlatego dodaliśmy FallbackConfigRefreshListener: gdy Config Server nie odpowiada, usługa podmienia źródło property na lokalny plik z ostatnią znaną, dobrą konfiguracją. To ten sam wzorzec co Hazelcast-jako-fallback, tylko zastosowany do configu zamiast do danych biznesowych.

Ożywianie 12-letniej biblioteki Diameter

EIR wymaga działającego stosu Diameter w JVM — połączeń peer, sesji, słownika AVP, obsługi 3GPP S13. Naturalnym wyborem była RestComm jDiameter (dawniej Mobicents), ale to biblioteka z około 2013 roku, formalnie przekazana w 2023 do Mobius Software i praktycznie nieutrzymywana — trudno o nią nawet w oczekiwanych wersjach z Maven Central. Alternatywą był komercyjny stos Diameter, ale to inny koszt i inny cykl integracji, niewspółmierny do skali projektu.

Zdecydowaliśmy się zvendorować całą bibliotekę do repozytorium — moduły core, ha i mux — budować ją lokalnie i instalować do lokalnego repo Mavena. Rozbudowaliśmy też słownik AVP (dictionary.xml) do blisko 7900 linii: standardowe aplikacje 3GPP (Rf, Ro, Cx/Dx, Sh, Gx, Rx, S6a) plus własne AVP dla S13 — Terminal-Information (1401), IMEI (1402), Equipment-Status (1445).

MeIdentityCheckHandler.java
public class MeIdentityCheckHandler implements NetworkReqListener {

    @Override
    public Answer processRequest(Request request) {
        if (request.getCommandCode() != 324) { // ME-Identity-Check
            return null;
        }

        AvpSet avps = request.getAvps();
        String imei = avps.getAvp(1402).getUTF8String(); // IMEI, np. 490154203237518

        EquipmentStatus status = eirLookupService.lookup(imei); // LDAP, fallback z Hazelcast

        Answer answer = request.createAnswer(ResultCode.DIAMETER_SUCCESS);
        answer.getAvps().addAvp(1445, status.getCode(), VENDOR_ID_3GPP, true, false);
        return answer;
    }
}

Utrzymanie forka legacy biblioteki open source to realny koszt — poprawek od nikogo z zewnątrz nie dostaniemy. Ale to była jedyna realna droga do działającego serwera S13 w Javie bez podpisywania kontraktu na komercyjny stos Diameter.

Spring Boot czy Quarkus do S13

Obok produkcyjnego deir-service (Spring Boot) zbudowaliśmy prototyp tej samej funkcji na Quarkusie, z rozszerzeniem io.quarkiverse.jdiameter. Różnica w ergonomii kodu jest wyraźna. W Spring Boot trzeba samemu sprawdzić command code, wyciągnąć surowe AVP po numerach i złożyć odpowiedź AVP-po-AVP. W Quarkusie dostępne jest wyspecjalizowane, typowane API: ServerS13SessionListener, JMEIdentityCheckRequest/Answer, S13SessionFactoryImpl — kod domenowy jest czytelniejszy i mniej podatny na pomyłkę w numerze AVP.

Mimo to na produkcję poszedł Spring Boot. Reszta monorepo — Spring Cloud Config, Eureka, integracja Hazelcast-spring — już tam była, więc spójność ekosystemu wygrała z lepszym doświadczeniem developerskim pojedynczego komponentu. Prototyp na Quarkusie nigdy nie dostał pełnej integracji z LDAP i cache i pozostał eksperymentem.