Hlavní navigace

Služby v distribuovaných systémech – konfigurace sítě

9. 6. 2021
Doba čtení: 11 minut

Sdílet

 Autor: Depositphotos
Pokud se jako informační systém připojím do sítě, jak zjistím, kdo je v síti zapojený, jaké služby poskytuje a kde jej najdu? To jsou zásadní otázky, na které musím najít nějakou uspokojivou odpověď.

Služby v distribuovaných systémech – konfigurace sítě

Vrátím se k úvodnímu článku série, kde jsem také napsal:

Jen pro připomenutí, pokud mluvím o distribuovaných systémech, pak se mně jedná především o nezávisle běžící informační systémy. Těch mohou být desítky nebo stovky, provozované různými subjekty ve vlastních provozních prostředích. Bude se tedy primárně jednat o spolupráci mezi systémy, která by měla být výhodná pro všechny zúčastněné.

Pokud se jako informační systém připojím do takovéto sítě, jak zjistím, kdo je v síti zapojený, jaké služby poskytuje a kde jej najdu? To jsou zásadní otázky, na které musím najít nějakou uspokojivou odpověď, jinak to fungovat nebude.

Jaké mám možnosti:

Statická konfigurace

Každý uzel bude mít informace o ostatních uzlech sítě a jejich službách zapsané ve vlastních konfiguračních souborech.

Výhodou takového řešení je jednoduchost. Navíc nepotřebuji žádnou centrální službu, která by mohla být úzkým místem pro fungování celé sítě.

Nevýhodou je obtížnost při rozšiřování sítě o další uzly. Pokud přidám nový uzel, musím změnit konfigurace všech uzlů, které by s novým členem měly komunikovat. Takto mám doposud nastaveny všechny ukázky v předchozích dílech, kdy role <applicant> má v konfiguraci uveden parametr applicant.destinations. Ten obsahuje seznam front identifikujících uzly poskytující služby.

Centrální konfigurace

Mohu si vytvořit nějakou centrální službu, která bude poskytovat informace o všech uzlech zapojených do sítě.

Výhodou tohoto přístupu je centrální správa konfigurace všech uzlů. Informaci o novém uzlu přidávám pouze na jedno místo.

Nevýhodou je to, že centrální konfigurace by byla úzkým místem pro fungování celé sítě. Navíc by každý uzel musel průběžně kontrolovat, zda se konfigurace uzlů nezměnila, a načítat si ji.

Dynamická konfigurace

A co využít vlastnosti centrálního message brokeru a držet se zásady, že síť tvoří nezávisle běžící informační systémy. Nově připojený uzel by mohl všem ostatním uzlům odeslat informaci o tom, že existuje, jaké poskytuje služby a jak je možné se s ním spojit. Tímto způsobem mohu přidávat nové uzly do sítě bez zásahů do konfigurace stávajících uzlů, a zároveň bez potřeby nějaké centrální správy a potenciálně nebezpečného úzkého místa.

Potenciální nevýhodou je nebezpečí, že se mně může objevit „podvodný“ uzel, který se začne tvářit jako někdo jiný. Proti této eventualitě je potřeba se technicky a případně organizačně zajistit (o technickém řešení se zmíním v některém z dalších pokračování).

Dále se budu zabývat poslední zmíněnou možností, tedy dynamickou konfigurací sítě a jejím možným řešením.

Dynamická konfigurace sítě

Předlohu pro fungování můžeme nalézt v běžném mezilidském styku. Pokud někde stojí hlouček lidí a přistoupí k němu neznámá osoba, pak seznámení proběhne pravděpodobně následovně.

Nově příchozí se představí, sdělí ostatním své jméno a kde jej mohli potkat dříve. No a pak očekává totéž od ostatních lidí v hloučku, tedy že se mu představí a sdělí nějaké základní informace o sobě. Po tomto úvodu již může debata volně probíhat, lidé se vzájemně oslovují dle kontextu, který probírají. Nebo prostě pronášejí své názory do kroužku a očekávají reakce od ostatních. V případě, že některá osoba chce opustit debatní kroužek, obvykle se zdvořile rozloučí. Ostatní tak vědí, že se již debaty účastnit nebude.

Jen pro dovětek. Pokud se lidé v hloučku vzájemně představují, pak všichni slyší, co která osoba o sobě říká. A to je docela dobrá pojistka pro to, aby někdo o sobě nezačal vykládat úplné nesmysly a nezačal se tvářit jako někdo jiný (a tím také naznačuji možnosti technického řešení obrany proti podvodné identitě).

Následující diagram zachycuje právě tu situaci, kdy v síti je zapojen node01 a node02. Do sítě se připojuje node03:

Z výše uvedeného je zřejmé, že nejlepším způsobem komunikace při představování bude téma. Pokud odešlu zprávu do tématu, pak jí dostanou všechny subjekty k odebírání tématu připojené. V mém případě je tím bodem topic:CONFIGURE. Ten je definován jako parametr v hlavním konfiguračním souboru, a je tedy známý pro všechny uzly zapojované do sítě.

Informaci do tématu předávám ve formě zprávy typu NodeConfiguration. Vzhledem k tomu, že se jedná o jednosměrnou komunikaci, odvozuji jí od třídy Message.

Ve zprávě mám definovány tři nové atributy:

  • queue – název fronty, na které je možné se s uzlem spojit. Na této frontě poskytuje uzel své služby a zároveň jej fronta identifikuje
  • alive – příznak, zda je uzel aktivní. Začátek své činnosti signalizuje uzel alive=true, ukončení činnosti pak alive=false
  • plea – příznak, že uzel požaduje ostatní, aby se mu také představili

Ten poslední příznak bude asi potřebovat vysvětlení. Pokud se uzel připojí do sítě a odešle zprávu o svém stavu do tématu, pak všechny stávající uzly dostanou informaci o jeho existenci. On sám ale o ostatních neví vůbec nic, a to by platilo do té doby, než by se ty ostatní uzly nerestartovaly. Proto nově se připojující uzel požádá ostatní, aby se také představili.

Role <configurable>

Uzel s touto rolí zahrnuje veškerou potřebnou funkcionalitu pro sdílení konfigurace mezi uzly.

Datové komponenty

Výše jsem si popsal, jak mohu za běhu uzlů sbírat informace o jejich konfiguraci. Jak ale s těmito informacemi naložit? Potřebuji je někam uložit, abych je po restartu měl k dispozici? Odpověď je taková, že nepotřebuji.

Všechny informace o uzlech potřebuji pouze po dobu vlastního běhu. Po ukončení činnosti uzlu již nemají žádný význam. Proto je mohu udržovat ve formě Java Bean. Jejich definici najdete v ConfigurableComponent:

@Component
@Profile(value = "configurable")
public class ConfigurableComponent {

    private static final Logger logger = LoggerFactory.getLogger(ConfigurableComponent.class);

    @Bean
    public Map<URI, NodeConfiguration> configurations() {
        return Collections.synchronizedMap(new HashMap<>());
    }

    @Autowired
    private ProducerTemplate producerTemplate;

    @Autowired
    TokenFactory factory;

    @Value("${provider.route.queue:#{null}}")
    private String queue;

    public void init() {
        NodeConfiguration config = factory.tokenInstance(NodeConfiguration.class);
        config.setQueue(queue);
        config.setAlive(true);
        config.setPlea(true);
        producerTemplate.sendBody("direct:configuration", config);
    }

    public void close() {
        NodeConfiguration config = factory.tokenInstance(NodeConfiguration.class);
        config.setAlive(false);
        config.setPlea(false);
        producerTemplate.sendBody("direct:configuration", config);
    }
}

Jsou zde definovány dvě metody zajišťující informace pro ostatní uzly o mém stavu.

Metoda init() se spouští po startu aplikačního Spring kontextu. Odesílá všem ostatním uzlům informaci o mém nastartování a základní údaje o spojení (název fronty). Současně také žádost o poskytnutí konfigurace ostatních uzlů.

Spouští se v rámci třídy Application, kde se jedná o tuto část:

@Autowired(required = false)
private ConfigurableComponent configurable;

@Override
public void run(ApplicationArguments args) throws Exception {
    if (configurable != null)
        configurable.init();
}

Metoda close() je spuštěna těsně před ukončením aplikačního kontextu a slouží k informování ostatních uzlů, že končím své fungování. Opět se jedná akci vyvolanou z třídy Application:

public static void main(String[] args) {
    context = SpringApplication.run(Application.class, args);
    addContextListeners();
}

private static final void addContextListeners() {
    context.addApplicationListener(applicationEvent -> {
        if (applicationEvent instanceof ContextClosedEvent) {
            try {
                context.getBean(ConfigurableComponent.class).close();
            }
            catch (NoSuchBeanDefinitionException e) { }
        }
    });
}

Komunikační funkce

Takto vypadá komunikační vrstva pro roli, třída ConfigurableCamelRoutes:

@Component
@Profile(value = "configurable")
public class ConfigurableCamelRoutes extends RouteBuilder {

    private static final Logger logger = LoggerFactory.getLogger(ConfigurableCamelRoutes.class);

    @Autowired
    private Map<URI, NodeConfiguration> configurations;

    @Value("${node.id}")
    private String nodeId;

    @Value("${provider.route.queue:#{null}}")
    private String queue;

    @Autowired
    TokenFactory factory;

    @Override
    public void configure() throws Exception {

        from("direct:configuration").routeId("configuration")
            .process(exchange -> logger.info("PUBLISHED: {}", exchange.getMessage().getBody(NodeConfiguration.class).toString()))
            .to("{{services.configurable.uri}}");

        from("{{services.configurable.uri}}").routeId("configurable")
            .process(exchange -> {
                NodeConfiguration config = exchange.getMessage().getBody(NodeConfiguration.class);
                exchange.getMessage().setBody(null);
                if (config != null) {
                    if (config.getAlive() != null && config.getAlive()) {
                        logger.info("CONFIGURATION: PUT {}", config.getNid().toString());
                        configurations.put(config.getNid(), config);
                        if (config.getPlea() != null && config.getPlea() && !config.getNid().toString().equals(nodeId)) {
                            NodeConfiguration myConfig = factory.tokenInstance(NodeConfiguration.class);
                            myConfig.setQueue(queue);
                            myConfig.setAlive(true);
                            myConfig.setPlea(false);
                            exchange.getMessage().setBody(myConfig);
                        }
                    }
                    else {
                        logger.info("CONFIGURATION: REMOVE {}", config.getNid().toString());
                        configurations.remove(config.getNid());
                    }
                }
            })
            .choice()
                .when(body().isNotNull())
                    .to("direct:configuration")
                .endChoice()
            .end();
    }
}

Jsou zde dvě cesty:

  • configuration – slouží k odeslání zprávy o mé konfiguraci do tématu CONFIGURE
  • configurable – připojuje se na téma CONFIGURE a přijímá všechny zprávy od ostatních uzlů (a vlastně i od sebe samého). Zprávy pak promítne do obsahu datové komponenty. V případě, že jiný uzel požaduje informaci o mé konfiguraci, odesílám jí jako poslední krok cesty.

Aplikační rozhraní

Pro vlastní fungování konfigurace není aplikační rozhraní potřeba. Udělal jsem si jej jako nástroj pro náhled do obsahu datové komponenty pro účely testování.

Implementace rozhraní je ve třídě ConfigurableServiceController:

@RestController
@Profile(value = "configurable")
public class ConfigurableServiceController {

    private static final Logger logger = LoggerFactory.getLogger(ConfigurableServiceController.class);

    @Autowired
    private Map<URI, NodeConfiguration> configurations;

    @RequestMapping(value = "/configuration")
    public ResponseEntity<Map<URI, NodeConfiguration>> getConfigurations() throws Exception {
        return new ResponseEntity<Map<URI, NodeConfiguration>>(configurations, HttpStatus.OK);
    }
}

Nastal čas na vyzkoušení

Ukázka základní funkčnosti

Nejdříve si vyzkoušíme konfiguraci uzlů tak, jak jsem je naznačil v sekvenčním diagramu výše. Spustím tedy uzly s těmito rolemi:

  • node01 – applicant, configurable
  • node02 – provider, configurable
  • node03 – provider, configurable

Příkazy spustit každý v samostatném terminálu:

java -jar target/distributed-services-guide-1.0.jar --spring.profiles.active=node01,configurable

A nyní se můžu podívat na obsah konfigurace pro uzel node01:

[raska@localhost ~]$ curl -s http://localhost:8081/configuration | jq .
{
  "local:node01": {
    "nid": "local:node01",
    "name": "node01",
    "tid": "uuid:3c64a42d-4abb-4eeb-99d1-f9c07152bf81",
    "ts": "2021-04-03T11:12:06.705+00:00",
    "queue": null,
    "alive": true,
    "plea": true
  }
}

Běží mně pouze jeden uzel, který má v konfiguraci záznam sám o sobě. Za povšimnutí stojí skutečnost, že uzel nemá definovanou frontu, na které by přijímal požadavky. To je v pořádku, protože tento uzel má přidělenu pouze roli <applicant>.

Mohu si vyzkoušet zavolat nějakou službu:

[raska@localhost ~]$ time curl -s http://localhost:8081/rest/appl02?text=hahaha | jq .

real    0m0.048s
user    0m0.047s
sys     0m0.009s

Nedostal jsem žádnou odpověď, protože v síti není žádný uzel s rolí <provider>. Navíc bych chtěl upozornit na dobu, než jsme dostali odpověď. Je to tak svižné, protože v konfiguraci nenašel žádný uzel pro oslovení.

Nyní si zkusím přidat další uzel (v samostatném terminálu):

java -jar target/distributed-services-guide-1.0.jar --spring.profiles.active=node02,configurable

A jak teď vypadá konfigurace:

[raska@localhost ~]$ curl -s http://localhost:8081/configuration | jq .
{
  "local:node01": {
    "nid": "local:node01",
    "name": "node01",
    "tid": "uuid:114bed94-f839-4d4b-b424-26e785eb5397",
    "ts": "2021-04-03T11:13:49.633+00:00",
    "queue": null,
    "alive": true,
    "plea": false
  },
  "local:node02": {
    "nid": "local:node02",
    "name": "node02",
    "tid": "uuid:67819f6a-08e4-4cc0-960b-d6754cbd65ca",
    "ts": "2021-04-03T11:13:49.513+00:00",
    "queue": "QUEUE-2",
    "alive": true,
    "plea": true
  }
}

V síti už vidím dva uzly, z nichž node02 má také definovanou frontu, takže by měl poskytovat služby. Můžeme vyzkoušet:

[raska@localhost ~]$ time curl -s http://localhost:8081/rest/appl02?text=hahaha | jq .
[
  {
    "nid": "local:node02",
    "name": "node02",
    "tid": "uuid:3bcc3bde-5c5e-4596-b9cf-306283c8d938",
    "ts": "2021-04-03T11:14:29.946+00:00",
    "code": "OK",
    "text": "text length: 6"
  }
]

real    0m0.185s
user    0m0.060s
sys     0m0.010s

Dostal jsem odpověď od uzlu node02. Navíc opět docela svižně, protože žadatel oslovuje pouze uzel, který je aktuálně v provozu.

A nyní si přidám ještě poslední uzel do základní ukázky, a to je node03:

java -jar target/distributed-services-guide-1.0.jar --spring.profiles.active=node03,configurable

A krátký pohled na konfiguraci:

[raska@localhost ~]$ curl -s http://localhost:8081/configuration | jq .
{
  "local:node03": {
    "nid": "local:node03",
    "name": "node03",
    "tid": "uuid:d3b0cc44-603e-4961-b3ee-39d6e239caf7",
    "ts": "2021-04-03T11:15:15.312+00:00",
    "queue": "QUEUE-3",
    "alive": true,
    "plea": true
  },
  "local:node01": {
    "nid": "local:node01",
    "name": "node01",
    "tid": "uuid:73ccef1a-20d8-48e7-ac8b-1fdd7193ad90",
    "ts": "2021-04-03T11:15:15.475+00:00",
    "queue": null,
    "alive": true,
    "plea": false
  },
  "local:node02": {
    "nid": "local:node02",
    "name": "node02",
    "tid": "uuid:7a72fcf5-bfe5-42bf-8358-15a30e1bce96",
    "ts": "2021-04-03T11:15:15.487+00:00",
    "queue": "QUEUE-2",
    "alive": true,
    "plea": false
  }
}

A ještě vyzkouším nějakou službu:

[raska@localhost ~]$ time curl -s http://localhost:8081/rest/appl01?value=1234 | jq .
[
  {
    "nid": "local:node02",
    "name": "node02",
    "tid": "uuid:6827c096-a9d8-4c30-88a9-257641b85114",
    "ts": "2021-04-03T11:16:30.929+00:00",
    "code": "OK",
    "result": 1527
  },
  {
    "nid": "local:node03",
    "name": "node03",
    "tid": "uuid:6827c096-a9d8-4c30-88a9-257641b85114",
    "ts": "2021-04-03T11:16:30.952+00:00",
    "code": "OK",
    "result": 1606
  }
]

real    0m0.127s
user    0m0.057s
sys     0m0.011s

Provedení je opět svižné, protože oslovuji pouze aktivní uzly.

Nyní můžu uzly postupně opět zastavovat, a uvidíte, že síť bude stále fungovat, odezvy na služby budou stále v rozumných mezích.

Rozšiřování sítě

No a když jsem v tom zkoušení, můžu si přidat do sítě ještě jeden uzel. Uvidíte, že stačí nový uzel nastartovat, a ostatní již budou vědět, jak jej použít.

Pro ukázku použiji definici uzlu node00 bez role v konfiguraci. Musím jí tedy přidat v rámci příkazové řádky:

root_podpora

java -jar target/distributed-services-guide-1.0.jar --spring.profiles.active=node00,provider,configurable --provider.queue=QUEUE-0

A mohu si vyzkoušet služby:

[raska@localhost ~]$ time curl -s http://localhost:8081/rest/appl01?value=1234 | jq .
[
  {
    "nid": "local:node03",
    "name": "node03",
    "tid": "uuid:9553a243-f9d3-4d3f-9539-3ec57b184225",
    "ts": "2021-04-03T11:18:17.755+00:00",
    "code": "OK",
    "result": 1777
  },
  {
    "nid": "local:node02",
    "name": "node02",
    "tid": "uuid:9553a243-f9d3-4d3f-9539-3ec57b184225",
    "ts": "2021-04-03T11:18:17.797+00:00",
    "code": "OK",
    "result": 1310
  },
  {
    "nid": "local:node00",
    "name": "node00",
    "tid": "uuid:9553a243-f9d3-4d3f-9539-3ec57b184225",
    "ts": "2021-04-03T11:18:17.833+00:00",
    "code": "OK",
    "result": 1283
  }
]

real    0m0.175s
user    0m0.051s
sys     0m0.012s

Ve článku jsou použity printscreeny a diagramy vytvořené autorem textu.

Byl pro vás článek přínosný?

Autor článku

Jiří Raška pracuje na pozici IT architekta. Poslední roky se zaměřuje na integrační a komunikační projekty ve zdravotnictví. Mezi jeho koníčky patří také paragliding a jízda na horském kole.