Thématiques principales

Affichage des articles dont le libellé est architecture. Afficher tous les articles
Affichage des articles dont le libellé est architecture. Afficher tous les articles

mardi 28 avril 2020

Network, VM et IaC avec Vagrant

Introduction

Qui n'a jamais eut besoin de faire des tests dans un environnement sain et isolé du poste de développement? Personne je crois en tout cas si ce n'est pas le cas, vous y viendrais un jour, c'est sur car a un moment et si vous utiliser pas Docker, il va falloir confronter vos exe a l'environnement d’exécution de la Prod!

Deux approches s'offrent généralement:
  • bénéficier d'un environnement de test dédié, c'est a dire avoir une machine physique [bare-metal], pré-installé comme en prod sur laquelle il est possible de déployer votre application et exécuter des tests dessus. on se doute que cette approche est coûteuse en matériel et en maintenance (temps de nettoyage/ réinstallation des machine etc...)
  • utiliser des machines virtuelles [machine-virtuelle].

La virtualisation

Cette deuxième approche [machine-virtuelle], que nous allons un peu plus détailler, offre de nombreux avantages car elle virtualise d'une part le matériel, donc pas de ressources physique a gérer mais elle permet de disposer d'environnement iso, réutilisable, ajustable et jetable rapidement. On pourra du coup évoquer bien sur le cas extrême de cette démarche par l'utilisation de Docker [Docker]. Pourtant il est important de ne pas confondre les deux outils et leurs objectifs respectifs. Le container isole un processus, une machine virtuelle isole un environnement. Cette dichotomie est importante car elle pousse a réfléchir a ce que l'on souhaite mettre en œuvre et pas faire un choix dogmatiquement.

Ainsi pour en revenir aux machines virtuelles, leur intérêt est donc par l'abstraction de l’environnement d’exécution de permettre de disposer a la demande de cet environnement dans un état de configuration maîtrisé.

Jusque ici l'utilisation des machines passe généralement par le choix d'un hyperviseur, en gros une couche logicielle permettant d’allouer sur la stack physique de l'OS de la machine des ressources simulées. Cette démarche permet de créer des CPU virtuelle, de la mémoire virtuelle, du stockage virtuel ou même encore du réseau virtuel (nous reviendrons sur cette partie dans un autre article car c'est un sujet en soit).

L’inconvenant de l'utilisation d'un superviseur, c'est qu'il faut faire un choix. Selon le système d'exploitation qui est utiliser, on ne disposera pas des mêmes. De façon général, on notera malgré tout que l'offre est riche: qemu-KVM/libvirt [qemu], [virtualbox], hyper-v [hyper-v] ou encore vmware sont les plus connu. Pourtant et malgré des années d’existences, ces derniers n'offrent toujours pas d'API homogènes rendant leur utilisation au sein d'un même contexte compliqué car il faudra alors prendre en considération les spécificités de chacun d'eux.

Ainsi non seulement, souvent ces machines sont utilisé a la main mais lorsqu'il s'agit de gérer logiciellement le cycle de vie des VM et l'automatiser dans une démarche IaC (infrastructure as a code [IaC]), alors il faudra se limiter. Le quoi? l'IaC? Ce n'est pas important, nous y reviendrons!

Vagrant

Une solution est pourtant possible! Vagrant [vagrant]! Bon ce n'est pas une solution miracle, mais en terme de provisionning de machine virtuel, cet outil permet d'abstraire bon nombre de caracteristique propres aux hyperviseurs (en dehors de son choix propre mais cela reste variabilisable).

Vagrant a surtout pour interet d'etre extremement simple d'emploi pour des actions basiques sur le cycle de vie d'une VM. En gros construire la VM, la lancer, l'arreter, ou encore la detruire se realise en juste quelques commandes. Bien sur qu'il permet d'ee faire plus mais globalement ca sera pour des utilisations en marge de celle utile au quotidien.

Ainsi pour realiser une VM, il va falloir en premier lieu la definir. Pour cela, on va realiser un fichier vagrantfile. Ce fichier s'appuie sur le language Ruby. sans etre un grand expert de Ruby, en fait ce fichier va etre essentiellement descriptif: Il va contenir un bloc "do" permettant de specifier la version vagrant utilisé. Dans ce bloc il va ensuite falloir decrire la VM en specifiant:
  • son image de base qui sera issu d'une bibliotheque d'image hebergé par vagrant [vagrant-images].
  • un nom qui servira de hostname,
  • une config reseau selon 3 modes possibles:
    • en port mapping permettant de router les paquet arrivant sur un port de la machine hote sur un port de la machine virtuelle
    • avec un reseau privé, c'est a dire qu'un reseau virtuel sera construit dans laquelle la machine virtuelle aura une adressse IP. Ce reseau sera accessible alors par la machine hote et seulement celle ci (la machine host disposera alors d'un nouvelle interface avec une IP dans le reseau virtuel)
    • avec un reseau publique, c'est a dire qu'une interface sera creer dans la machine virtuelle mais celle ci sera bridgé sur la machine hote dans le meme reseau que celle ci.
  • un provider c'est a dire l'hyperviseur employé, et on pourra alors preciser le nombre de CPU virtuel et de memoire que l'on souhaite lui allouer.

Pour exemple, voici une allocation de VM possible:


1
2
3
4
5
6
7
8
9
Vagrant.configure("2") do |config|
  config.vm.box = "generic/ubuntu1804"
  config.vm.hostname = "myvmtest"
  config.vm.network "public_network", :dev => "eno2", :mode => 'bridge', mac: "52:54:00:B2:14:8E", use_dhcp_assigned_default_route: true
  config.vm.provider "libvirt" do |vb|
    vb.memory = "1000"
    vb.cpus = "1"
  end
end

Pour lancer cette VM, rien de plus simple:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
$ vagrant up
Bringing machine 'default' up with 'libvirt' provider...
==> default: Checking if box 'generic/ubuntu1804' is up to date...
==> default: Creating image (snapshot of base box volume).
==> default: Creating domain with the following settings...
==> default:  -- Name:              vagrant-template_default
==> default:  -- Domain type:       kvm
==> default:  -- Cpus:              1
==> default: 
==> default:  -- Feature:           acpi
==> default:  -- Feature:           apic
==> default:  -- Feature:           pae
==> default:  -- Memory:            1000M
==> default:  -- Management MAC:    
==> default:  -- Loader:            
==> default:  -- Base box:          generic/ubuntu1804
==> default:  -- Storage pool:      default
==> default:  -- Image:             /var/lib/libvirt/images/vagrant-template_default.img (32G)
==> default:  -- Volume Cache:      default
==> default:  -- Kernel:            
==> default:  -- Initrd:            
==> default:  -- Graphics Type:     vnc
==> default:  -- Graphics Port:     -1
==> default:  -- Graphics IP:       127.0.0.1
==> default:  -- Graphics Password: Not defined
==> default:  -- Video Type:        cirrus
==> default:  -- Video VRAM:        256
==> default:  -- Sound Type:
==> default:  -- Keymap:            en-us
==> default:  -- TPM Path:          
==> default:  -- INPUT:             type=mouse, bus=ps2
==> default: Creating shared folders metadata...
==> default: Starting domain.
==> default: Waiting for domain to get an IP address...
==> default: Waiting for SSH to become available...
    default: 
    default: Vagrant insecure key detected. Vagrant will automatically replace
    default: this with a newly generated keypair for better security.
    default: 
    default: Inserting generated public key within guest...
    default: Removing insecure key from the guest if it's present...
    default: Key inserted! Disconnecting and reconnecting using new SSH key...
==> default: Setting hostname...
==> default: Configuring and enabling network interfaces...

Cela va avoir pour consequence de provisionner la VM les ressources, faire l'allocation de l'IP via le DHCP et de fournir une interface en plus pour la maintenance.

Networking

Aisni, du coté host, on a un certain nombre d'interfaces reseaux "mysterieuse" qui ont ete crée, virbr0, vnet, macvtap0@eno2 on va revenir dessus:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
$ ip a
1: lo: [...]
2: eno2: <broadcast> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 04:92:26:1d:b2:e8 brd ff:ff:ff:ff:ff:ff
    inet 192.168.0.57/24 brd 192.168.0.255 scope global dynamic noprefixroute eno2
       valid_lft 36031sec preferred_lft 36031sec
3: wlo1: [...]
41: virbr0: <broadcast> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 52:54:00:d7:c3:3c brd ff:ff:ff:ff:ff:ff
    inet 192.168.121.1/24 brd 192.168.121.255 scope global virbr0
       valid_lft forever preferred_lft forever
42: virbr0-nic: <broadcast> mtu 1500 qdisc fq_codel master virbr0 state DOWN group default qlen 1000
    link/ether 52:54:00:d7:c3:3c brd ff:ff:ff:ff:ff:ff
71: vnet0: <broadcast> mtu 1500 qdisc fq_codel master virbr0 state UNKNOWN group default qlen 1000
    link/ether fe:54:00:51:d8:29 brd ff:ff:ff:ff:ff:ff
72: macvtap0@eno2: <broadcast> mtu 1500 qdisc fq_codel state UP group default qlen 500
    link/ether 52:54:00:b2:14:8e brd ff:ff:ff:ff:ff:ff

Mais dans la VM, on a que deux interfaces:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ vagrant ssh 
$ ip a
1: lo: [...]
2: eth0: <broadcast> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 52:54:00:51:d8:29 brd ff:ff:ff:ff:ff:ff
    inet 192.168.121.245/24 brd 192.168.121.255 scope global dynamic eth0
       valid_lft 1943sec preferred_lft 1943sec
3: eth1: <broadcast> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 52:54:00:b2:14:8e brd ff:ff:ff:ff:ff:ff
    inet 192.168.0.110/24 brd 192.168.0.255 scope global dynamic eth1
       valid_lft 41543sec preferred_lft 41543sec

Pour expliquer un peu tout cela. Lors de la creation de la VM, vagrant va construire 2 interfaces, l'une pour la maintenance est utilisée lorsque l'on realise "vagrant ssh" (eth0) et l'autre est celle qu'on lui a demandé de creer dans le vagrantfile (eth0).

Du coté du host c'est un peu plus compliqué. On sait que l'interface que nous avons demandé de constuire a vagrant est une interface public mais par defaut, celle qu'il construit pour lui, ou celle de maintenance, est une interface privé.
On a donc deux approches differentes et forcement deux solutions (avant cela, je vous invite a aller relire l'article sur l'adressage [addressage], ca peut aider).

Reseau Privé


Pour les reseau privé, vagrant va constuire un network virtuel. Pour cela il va creer une interface associé present dans la VM (ici vnet0), de meme, il va creer une interface pour le host (ici virbr0-nic). Enfin, vagrant va construire un bridge [bridge] (ici virbr0) ou pont permettant d'alluer une ip au host, dans un plan d'addressage specifique.

C'est cette manipualtion qui va permettre la creation de ce reseau privé entre le host et la machien virtuelle permettant aux deux de communiquer ensemble. On peu le verifier en utilisant les commande suivante:


1
2
3
4
$ brctl show 
bridge name bridge id  STP enabled interfaces
virbr0  8000.525400d7c33c yes  virbr0-nic
       vnet0

La commande brctl n'etant plus officiellement supporté sous certains OS, l'alternative est:


1
2
3
4
5
6
7
8
$ ip link show type bridge_slave
42: virbr0-nic: <broadcast> mtu 1500 qdisc fq_codel master virbr0 state DOWN mode DEFAULT group default qlen 1000
    link/ether 52:54:00:d7:c3:3c brd ff:ff:ff:ff:ff:ff
73: vnet0: <broadcast> mtu 1500 qdisc fq_codel master virbr0 state UNKNOWN mode DEFAULT group default qlen 1000
    link/ether fe:54:00:20:a2:a1 brd ff:ff:ff:ff:ff:ff
$ ip link show type bridge
41: virbr0: <broadcast> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:d7:c3:3c brd ff:ff:ff:ff:ff:ff

Reseau Publique


Concernant l'interface publique, c'est plus simple car il existe deja une interface dans le reseau public, c'est eno2. Du coup au lieu de construire un bridge comme precedement, ici dans le host, vagrant va en construire une autre sorte, le macvtap, entre l'interface reseau de la VM identifié par son adresse mac (qui sera alors transposé sur le macvtap) et l'interface en02 de la machine host.

On peut le voir grace a la commande suivante:


1
2
3
ip link show type macvtap
74: macvtap0@eno2: <broadcast> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 500
    link/ether 52:54:00:b2:14:8e brd ff:ff:ff:ff:ff:ff

L'interet ici contrairement a l'interface public, c'est que l'adresse mac de la VM est visible depuis le reseau publique et donc qu'elle peut etre gerer par un service DHCP.

En resumé, tout cela donne le reseau suivant:



Il y aurait encore beaucoup a dire sur les aspects reseaux (le vlan par exemple, etc..) ou sur vagrant qui par le biais de plugin va permettre le provision automatique du contenu logiciel de la VM ainsi creer (via Ansible par exemple).

Ceci est un autre probleme que nous traiterons dans d'autres articles.

Références

  • [bare-metal] https://www.ionos.fr/digitalguide/serveur/know-how/serveur-bare-metal-definition-et-structure/
  • [machine-virtuelle] https://fr.wikipedia.org/wiki/Machine_virtuelle
  • [Docker] https://www.docker.com/
  • [qemu] https://www.qemu.org/
  • [virtualbox] https://www.virtualbox.org/
  • [hyper-v] https://docs.microsoft.com/fr-fr/virtualization/hyper-v-on-windows/about/
  • [vmware] https://www.vmware.com/fr.html
  • [IaC] https://www.lebigdata.fr/infrastructure-as-code-definition
  • [vagrant] https://www.vagrantup.com
  • [vagrant-images] https://app.vagrantup.com/boxes/search
  • [ip-addr] https://memo-linux.com/ip-la-commande-linux-pour-gerer-son-interface-reseau/
  • [addressage] https://un-est-tout-et-tout-est-un.blogspot.com/2020/04/networking-adressage.html
  • [bridge] https://seravo.fi/2012/virtualized-bridged-networking-with-macvtap

samedi 4 avril 2020

Docker : Swarm

Introduction

Problématique Microservices

Dans le contexte des infrastructures microservices, docker [docker] est la reference! Jusqu’a maintenant nous avions surotut vu son utilisation dans le cadre du processus de developpement voir d’integration, mais nous ne sommes pas allé sur le terrain de la mise en production.

Qu’est ce que je veux dire? Ce que je veux dire, c’est qu’une architecture microservice necessite forcement plusieurs micorservice et c’est pour cela d’ailleur que nous avons vite utilisé [docker-compose] afin de nous faciliter la vie en lancant l’ensemble de ces microservices ensemble, pour ainsi dire "orchestrer".
Pourtant docker-compose n’est pas suffisant, en effet celui ci nous permet de mettre en place une deployement cible de nos microservices mais ces derniers n’evolent alors que dans la machine dans laquelle docker-compose a ete invoqué!
Dans le cas ou nous souhaiterions deployer l’ensemble de nos microservice selon des regles specifique de deploiement, (par exemple les composants front ensemble, les back ensemble etc…​) alors il nous faut d’une part etre capable de deploier dans un ensemble de machine de facon transparente mais en plus de pouvoir facilement administrer ce deploiement!
La solution a cette problematique c’est le sujet de cet article, docker-swarm [docker-swarm]

Architecture

Docker swarm [docker-swarm-concepts] est un outil d’orchestration inclu dans docker engine. Un Swarm est un ensemble machine contenant docker et fonctionnant en mode swarm Danse ce cluster, on trouvera alors un manager (ou n selon la redondance souhaité) et des worker (le manager pouvant aussi cumuler le rôle de worker).




Comme dans docker-compose, on déclare des services correspondant a un etat descriptif de la configuration souhaitée auprès du manager qui va produire des taches de déploiement dans les workers.

Fonctionnalités

Docker Swarm permet:
  • de gérer un cluster de nœud définit dans un réseau de machine potentiellement distribué et heterogene.
  • de fournir une description du déploiement orienté service comme docker compose
  • de scaler les instances pour ajouter de la redondance logique
  • de faire du monitoring et de la réconciliation: en cas de crash, swarm sera capable de détecter les instances éventuellement manquante et se chargera de les redémarrer
  • de faire des mise a jour a chaud pour realiser des modifications sur la configuration de déploiement comme des mise a jour applicative et docker se chargera de réconcilier l’état reel avec l’etat voulu.
  • de loadbalancer la charge des flux sur les n instances (les replicas que nous verrons plus loin)

Creation du cluster swarm

Architecture cible





Dans notre archi, on va jouer avec 1 manager principal redondé une fois et 5 workers (incluant les 2 managers).

On va vouloir faire un server de fichier via http (avec [nginx]) dont les données de configuration sont partagées via un partage [nfs] global au cluster. Par contre dans notre exemple nous considererons que les données a exposer ne sont disponible que sur manager et node1.

Nos instances nginx devront donc se trouver sur l’une ou ces deux workers.
Process

Pour creer le swarm, on va suivre la doc! [create-swarm]

On va sur la machine qui sera le manager et on execute la commande:

1
$docker swarm init --advertise-addr 192.168.0.60

On obtient une reponse de la forme:

1
$docker swarm join --token SWMTKN-1- 192.168.0.60:2377

Cette commande il faudra aller l’exécuter sur chaque machine ou est installé docker-engine. Avec la commande suivante il est possible de verifier que les noeuds ont bien eté ajouté:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
  $ docker info
  Client:
  Debug Mode: false

  Server:
  Containers: 5
    Running: 0
    Paused: 0
    Stopped: 5
  Images: 21
  Server Version: 19.03.6
  [...]
  Swarm: active
    NodeID: tntgi3hb72z9ly8rgir8p1j9p
    Is Manager: true
    ClusterID: wr0n081vcbdyxaabz40y3k3n8
    Managers: 1
    Nodes: 4
    Default Address Pool: 10.0.0.0/8  
    SubnetSize: 24
    Data Path Port: 4789
    [...]

Pour redonder le manager il faut s’ajouter dans cluster comme noeud manager secondaire pour rendre possible l’execution des commandes en local (le poste de dev en fait) et non devoir les executer sur le manger en remote.

Sur le poste de dev, on execute la commande d’ajout d’un noeud classique:

1
$ docker swarm join --token SWMTKN-1-<token-value> 192.168.0.60:2377

Et sur la machine manager on realise une promotion a la machine de dev (dev etant le nom hostname de la machine):

1
$docker node promote dev

Pour obtenir des informations sur les noeuds:

1
2
3
4
5
6
7
  $ docker node ls
  ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
  tntgi3hb72z9ly8rgir8p1j9p *   manager            Ready               Active              Leader              19.03.6
  xbaxr3icsfc08a3liv4dr3v7p     node1              Ready               Active                                  19.03.8
  fyvyl153mrly3jz12xt0bblb3     node2              Ready               Active                                  19.03.8
  vgqfp4qxuguma7u11ghyw847i     node3              Ready               Active                                  19.03.8
  s4h7oyar5tnothrwp0p6ukews *   dev                Ready               Active              Reachable           19.03.5

En preparation et pour respecter la typologie a venir, on va ajouter un label a nos noeuds [add-label]. Nous verons plus tard pourquoi et comment cela va impacter le deploiement.

1
2
$docker node update --label-add http=active manager
$docker node update --label-add http=active node1

Deploiement d’un service dans le swarm

Process

Pour deploier nos service nginx, on va definir un ficheir docker-compose un peu booster et integrant quelques specifications supplementaire [deploy-swarm].

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
  tc-ngnix:
    image: nginx:1.17.2-alpine
    volumes:
      - "/media/nfs_storage/tc-nginx/conf:/etc/nginx:ro"
      - "/mnt/raid/data:/usr/share/nginx/html/data:ro"
    ports:
      - 80:80
    deploy:
      replicas: 2
      placement:
        constraints:
          - "node.labels.http==active"

Ce fichier va s’appuyer sur une image nginx evidemement, declarer les points de montage de conf et de data et definir :
  • le nombre de replicas c’est a dire le nombre d’instance souhaité du container
  • la strategie de placement via une contrainte permetant de n’utiliser que les nodes sur lesquels on a mis le label http
Pour deployer le service http-services (son petit nom a nous):

1
$docker stack deploy --compose-file docker-compose.yml http-services

Pour consulter le service

1
2
3
  $ docker stack services http-services
  ID                  NAME                        MODE                REPLICAS            IMAGE                               PORTS
  d0g78u03joll        tc-infra-base_tc-ngnix      replicated          2/2                 nginx:1.17.2-alpine   *:80->80/tcp

Nous dit que nous avons bien deployer nos instances. Pour savoir ou? nous allons demander directement a docker avec un docker ps sur le manager:

1
2
3
  $ docker ps -a
  CONTAINER ID        IMAGE                          COMMAND                  CREATED              STATUS                      PORTS               NAMES
  e7e989c8b5f5        nginx:1.17.2-alpine            "nginx -g 'daemon of…"   About a minute ago   Up About a minute           80/tcp              http-services_tc-ngnix.1.xjww2x4g3ic3fs0h156vxneg0

Il en manque un! mais non il est sur le node1:


1
2
3
  $ docker ps -a
  CONTAINER ID        IMAGE                          COMMAND                  CREATED              STATUS                      PORTS               NAMES
  a1e06407c418        nginx:1.17.2-alpine            "nginx -g 'daemon of…"   About a minute ago   Up About a minute           80/tcp              http-services_tc-ngnix.2.ej3w74okhms3ir3gufbnockz7

Le plus simpa avec ca du coup c’est que notre server est accessible que ce soit:
  • http://node1
  • http://manager
mais aussi en:
  • http://dev
  • http://node2
  • http://node3
Parfait nous voila avec un server nginx up en double instance, loadbalancé et accessible via tous les noeuds du cluster!

Point de vigilance

Je n’entre pas dans le detail mais sur certains points quelques interrogation peuvent etre soulevé, concernant le partage de la conf et des datas a nginx via un point de montage [nfs]. Ce n’est pas une solution ideale mais elle a le merite d’etre rapide et simple a mettre en oeuvre mais attention alors a la securité…​.

Pour aller un peu plus loin…​

Pour voir un exemple similaire avec quelques manip en plus genre la mise a jour a chaud ou le scaling horizontal aller voir cet article [swarm-fun]

Ensuite si vous voulez entrer dans la partie monitoring de vos containeurs et service avec des outils comme Prometheus ou Grafana, vous pouvez consulter [swarm-monitoring]

References

  • [docker-swarm-concepts] https://docs.docker.com/engine/swarm/key-concepts/
  • [docker-swarm] https://docs.docker.com/engine/swarm/
  • [create-swarm] https://docs.docker.com/engine/swarm/swarm-tutorial/create-swarm/
  • [deploy-swarm] https://docs.docker.com/engine/swarm/stack-deploy/
  • [swarm-mode] https://docs.docker.com/engine/swarm/how-swarm-mode-works/services/#replicated-and-global-services
  • [add-label] https://docs.docker.com/engine/reference/commandline/node_update/#add-label-metadata-to-a-node
  • [docker] https://docs.docker.com/
  • [docker-compose] https://docs.docker.com/compose/
  • [nfs] https://doc.ubuntu-fr.org/nfs
  • [nginx] https://www.nginx.com/
  • [swarm-fun] https://dzone.com/articles/fun-with-docker-swarm?edition=334876&utm_source=Daily%20Digest&utm_medium=email&utm_campaign=Daily%20Digest%202017-11-15
  • [swarm-monitoring] https://dzone.com/articles/monitoring-docker-swarm?edition=451233&utm_source=Daily%20Digest&utm_medium=email&utm_campaign=Daily%20Digest%202019-02-11

samedi 15 juin 2019

ReST : Introduction

ReST pour Representational State Transfert [wiki] est un style d’architecture formalisé par [Thomas Fielding] en 2000 dans sa thèse de doctorat. Le but de ReST est de formaliser et simplifier les échanges d’informations et la communication entre les applications sous la forme d’une API [rest-quick].

En se basant sur HTTP (v1 et maintenant v2 [http2-rest] ) Rest bénéficie d’un mode de communication simple et universel (technologiquement parlant) dont les mécanismes de sécurisations et d’attaques sont connus (ssl, tls, dos, basic authent, SAML, OAuth2, JWT, la liste est longue et nous y reviendrons ....). 

Dans le principe, l’idée est simple: exposer et fournir une ressource au travers d’HTTP. Conformément à ce protocole ceci est réalisé grâce à son URL (Uniform Resource Locator) [url-uri] qui permet au client d’identifier et de manipuler cette ressource.

Lorsque l’on parle de ressource, il va de soit que cela se conçoit dans le cadre de HTTP. Ainsi la ressource est un objet virtuel présent côté serveur dont le cycle de vie sera gérer par celui ci mais avec lequel le client aura la possibilité d’interagir en manipulant les verbe du protocole: (GET, POST, PUT, DELETE, PATCH).



De manière générale ces interactions du client avec la ressource c’est à dire les verbes HTTP pourront suivre une logique de gestion de type CRUD et ainsi voir l’interface serveur Rest comme une interface d'accès à des données comme on le ferait avec une BDD.

Ainsi, pour être plus explicite, lorsque le client réalisera une requête GET, cela pourra être vu comme une requête Read sur la ressource si celle ci était une base de donnée. Si le client réalise une requête de type de POST, cela équivaudra à une requête Create, s'il réalise un PUT, cela équivaut à un update de la ressource [put-vs-post] et s’il fait un DELETE, cela est comme on pourra s’en douter un Delete de la ressource.

Il est évident que cette vision mappant ReST et CRUD est un raccourci abusif [rest-not-crud] car une API ReST doit être spécifié et construire selon un besoin utilisateur clair et non dans une logique où l’on pourra tout faire sur le même endpoint! Ainsi si, dans le cas des modèles simples, adopter une logique CRUD ne sera probablement pas une bêtise, rapidement, il faudra penser l’API ReST comme étant une fenêtre exposant des ressources orientés d’un même modèle de données.

De même, les interactions sont l’occasion de réaliser des échanges d’informations. Ainsi lorsque le client réalise un GET sur une ressource, son but est de disposer des informations la concernant. De même lorsque qu’il réalise un POST, sont but est de pousser les informations dont il dispose afin d’enregistrer ces informations sous la forme d’une nouvelle ressource dans le server. Mais encore une fois, cela ne préjuge pas de la structure des données qui seront réellement mise en base de données, (si celle ci existe).

Comme nous l’avons déjà dit Rest s’appuie sur HTTP et c’est ainsi qu’est formalisé la manière d'échanger de l’informations, quoi? non pas en html! (bien que dans l’absolu ça ne soit pas impossible) mais plutôt en spécifiant le type MIME dans le header de la transaction.

Pour rappel, dans le protocole http, le type MIME permet au client et au server de se mettre d’accord sur le format des données qu’ils vont s'échanger. Ainsi, dans le web classique, on trouve de html, du text des images… Ici avec ReST on trouvera essentiellement de l’XML et plus probablement du JSON (nous reviendrons sur cela).

De l’XML? mais c’est comme SOAP alors! ? et ba loupé! [soap-vs-rest]

En effet Rest et Soap sont souvent comparés et traité à un même niveau sauf que s’ils sont tous deux des protocoles de communication s’appuyant sur HTTP, il est existe une différence importante entre les deux:

  • Rest tend à respecter les standard du web dans l’utilisation de http et utilise ce dernier comme un protocole applicatif où les verbe sont utilisé pour accéder convenablement aux ressources, elle même correctement formalisé selon des URL adaptée.  
  • SOAP n’utilise HTTP que comme un protocole de transport et toute la logique applicative est dans le message SOAP

Pour mieux comprendre ces différences il convient de lire l’article de Martin Flowers sur Richardson Maturity Model [richardson], [richardson-comment].

Ainsi en tant que style d’architecture, en fournissant un interface virtualisant la consommation ou la production de ressources, facilite le découplage des différentes parties applicatives du systèmes conduisant aux architectures microservices (mais cela est une autre histoire).

Ainsi ces architectures à base ReST en s'appuyant sur http impliquent (ou permettent de bénéficier) différentes propriétés qu’il convient de prendre en compte afin de se prémunir d’erreur de conception.
Entre autre:

  • ReST est un style d’architecture basé sur le modèle Client Serveur impliquant que chacun à un rôle différent, le premier consomme les informations, le second, les fournit ou les enregistre.
  • ReST est sans etat. Cela est important dans le sens où les requêtes du client sur le serveur sont indépendantes et doivent se suffirent à elle même pour satisfaire le besoin. De son côté, le serveur ne construit pas de session.
  • ReST autorise la mise en place de cache pour garantir les performances du système. Ce(s) cache(s) peuvent être implémenté(s) en tout lieu de la chaîne de commande (le client, le serveur où tout autre composant intermédiaire)
  • ReST doit fournir une interface uniforme et homogène de façon à faciliter l'accès et le parcours des données
  • ReST peut être conçu en couche autorisant l’utilisation de proxy et reverse proxy ainsi que la délégation d’appel à des sous API ReST. 

Un dernier point spécifie que ReST, optionnellement, peut fournir du code à la demande. J’admet que ce point reste pour ma part assez nébuleux et je ne l’aborderais pas tant j’ai du mal à concevoir l’utilisation de ce point, sa mise en oeuvre et les éventuelles problèmes de sécurité que cela peut poser.

Bien maintenant que l’on en à parler, ca serait bien d’y gouter aussi un peu à du ReST!

Du coup prenons le cas d’une bibliothèque présentant divers resources selon différents supports.

Alors ici on va commencer par un cas particulier, celui de la ressource unique (ou de type singleton) qui va nous permettre d’obtenir des informations sur des ressources accessibles dans l’API.

Ainsi, sur le endpoint on va trouver un premier chemin accessible tel que:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
> GET: /bibliotheque
< 200 : 
{
  nom : Bibliotheque de la mairie
  resources : 
  [
    {
       nom : Les livres
       type: Livre
       url :  /bibliotheque/livres
    },
    {
       nom : Les videos
       type: Video
       url :  /bibliotheque/videos
    }

  ] 
} 


Ici on à donc grace à cette premiere requette la connaissance des resources disposibles. Celle ci de type Livre et Video sont identifiable et accessible via une url. On notera que la nomenclature de nomage est au pluriel car comme on va le voir la ressource associée (contrairement à la bibliotheque qui est un singleton pour sa part) est multiple.

Intéressons nous aux livres:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
> GET : /bibliotheque/books
< 200 :
[
  {
    id:1
    name: “Clean Code”,
    ISBN-13: 978-0132350884,
    nbrChapitre: 17
  },
  ...
]


On obtient donc une liste dans laquelle on trouvera le livre Clean Code. A noter qu’avec cette requête, on obtient donc tous les livres! Si on veut limiter cette requete, il est alors possible de faire une requete avec un param:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
> GET : /bibliotheque/books?nbrChapitre=17
< 200 :
[
  {
    id:1
    name: “Clean Code”,
    ISBN-13: 978-0132350884,
    nbrChapitre: 17
  },
  ...
]


Dans ce cas, on obtient toujours une liste de libre mais seul ceux contenant que 17 chapitres… (bon ok comme recherche c’est pas ce qu’il y à de plus courant mais c’est pour l’exemple) De même si l’on fait la même recherche mais sur le nom, on obtient:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
> GET : /bibliotheque/books?name=Clean Code
< 200 :
[
  {
    id:1
    name: “Clean Code”,
    ISBN-13: 978-0132350884,
    nbrChapitre: 17
  },
  ...
]


Ce qui revient au même… mais on notera que l’on obtient quand même une liste… pourquoi? car il s’agit de l'équivalent de faire une recherche avec un filtre, du coup ce n’est pas faire une récupération spécifique et si l’on cherchait à récupérer que le livre Clean Code, alors on a mal penser sa requête. Ce qu’il aurait fallu faire c’est :


1
2
3
4
5
6
7
8
> GET : /bibliotheque/books/1
< 200 :
 {
    id:1
    name: “Clean Code”,
    ISBN-13: 978-0132350884,
    nbrChapitre: 17
  }


La on ne récupère qu’un élément et c’est celui identifié par son identifiant unique, l’id. (équivalent à un clef primaire)

Nous n’avons jusque là fait que des GET, et on remarque qu’il manque Clean Architecture… bon ok, ajoutons le:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
> POST : /bibliotheque/books
{
    name: “Clean Architecture”,
    ISBN-13: 978-0134494166,
    nbrChapitre: 1
}
< 201 :  /bibliotheque/books/2
 {
    id:2
    name: “Clean Code”,
    ISBN-13: 978-0132350884,
    nbrChapitre: 17
  }


Bien on note plusieurs choses: d’une part on ne post pas le livre avec un identifiant, on laisse le serveur se charger de le faire. Ainsi c’est pour cela que la réponse au POST est un code 201, nous spécifiant que la ressource à bien été créée et à cela est ajouté sa localisation (afin de savoir comment la récupérer) mais aussi la ressource elle-même.

Par contre on constate une erreur, ce livre ne contient pas qu’un chapitre mais 34… Il nous faut donc corriger cela. Le hic c’est que si l’on tente de refaire un POST, il y à des chances que l’on nous réponde que la ressource existe déjà… (je vous laisse chercher le code http associé) voir peut être qu’il nous en créer une seconde en faisant par ce biais un doublon. non ce que l’on veut c’est modifier la valeur associée à la ressource.

Bon pour cela, on pourrait faire un DELETE sur l’id et refaire le POST…. bon vous avez compris, ça fonctionnerait mais c’est lourd… non il existe des façon de faire plus efficace.

Entre il est possible d’utiliser (je vous le donne en mille) soit le verbe PUT en fournissant l'intégralité de la ressource ou alors PATCH permettant de cibler la modification à réaliser. Voyons les deux:


1
2
3
4
5
6
7
8
> PUT : /bibliotheque/books/2
{
    id:2,
    name: “Clean Architecture”,
    ISBN-13: 978-0134494166,
    nbrChapitre: 34
}
< 204 


Ici dans ce cas, le PUT va donc surcharger complètement la ressource. Seul un code 204 sera retourné pour spécifier que l'opération à été réalisée (mais qu’il n’y à pas d’information supplémentaire à attendre, en effet avec un PUT on connait déjà le localisation de la ressource sinon on aurait pas pu exécuter ce verbe)

Avec le PATCH, c’est un peu plus délicat car il faut spécifier que ce qui est modifier. On prendra garde à ce que l’id ne soit pas surcharger bêtement (à noter d’ailleur que l’id est inutile dans la ressource elle-même si le location du retour du POST est correctement fourni, le seul hic ici est que l’objet ne porte alors pas son identifiant unique, on verra que ceci est alors surtout un problème de représentation des données au sein du modèle ReST. On réfléchira à ce point lorsque ne traiterons d’exemples)

Enfin donc le PATCH: (on considérant que nous n’avons pas fait le PUT précédent)


1
2
3
4
5
> PATCH : /bibliotheque/books/2
{
    nbrChapitre: 34
}
< 204 


Voilà les grandes lignes de l’utilisation de ReST et de son design afin d’en tirer le meilleur. Maintenant il reste à le formaliser et pour cela rien de tel que quelques exemples. Du coup on tachera de faire du ReST avec quelques langages histoire de bien en montrer l'intérêt et surtout que finalement, le langage en question ne change rien aux fondamentaux et aux propriétés portées par ReST!

Références

[http2-rest] https://dzone.com/articles/benefits-of-rest-apis-with-http2?edition=352091&utm_source=Daily%20Digest&utm_medium=email&utm_campaign=Daily%20Digest%202018-01-10
[Thomas Fielding] https://www.ics.uci.edu/~fielding/
[wiki] https://en.wikipedia.org/wiki/Representational_state_transfer
[rest-tuto] https://www.restapitutorial.com/
[soap-vs-rest] https://dzone.com/articles/differences-in-performance-apis-amp-more?edition=286955&utm_source=Daily%20Digest&utm_medium=email&utm_campaign=dd%202017-03-31
[richardson] https://martinfowler.com/articles/richardsonMaturityModel.html
[richardson-comment] https://blog.xebia.fr/2010/06/25/rest-richardson-maturity-model/
[swagger] https://dzone.com/articles/api-first-with-swagger?edition=330491&utm_source=Daily%20Digest&utm_medium=email&utm_campaign=Daily%20Digest%202017-10-12
[put-vs-post] https://restfulapi.net/rest-put-vs-post/
[rest-not-crud] https://medium.com/@marinithiago/guys-rest-apis-are-not-databases-60db4e1120e4
[rest-quick] https://blog.nicolashachet.com/niveaux/confirme/larchitecture-rest-expliquee-en-5-regles/
[url-uri] https://www.java67.com/2013/01/difference-between-url-uri-and-urn.html

samedi 26 janvier 2019

La haute disponibilité, les Webservices et JMS dans weblogic

Un article court aujourd'hui pour parler d'architecture JEE et de haute disponibilité dans le cadre JMS.

Nous avons déjà parlé du fonctionnement de JMS mais nous n'avons pas forcement traité la question de la haute disponibilité. Mais qu'est ce que la haute disponibilité?

La haute disponibilité [HD] est la capacité d'un système a fournir un service selon un taux de disponibilité pre-déterminer.

Cela semble un peu de la paraphrase mais cela se traduit par quelles solutions doivent ou peuvent être mises en oeuvre afin de garantir le rendu d'un service par un système?

Pour répondre a cette problématique, il existe une approche simple, la duplication. il suffit de fournir le service de manière répliquée de façon à palier à des éventuelles erreurs et crash.

Dans le cadre des systèmes a base de web-services, souvent la haute disponibilité est couplé a des systèmes de répartition de charge [weblogic-WS-LB]. Le flux http/https est adressé sur un load-balancer qui a pour charge de rediriger ces flux successivement sur les différents serveurs/containeurs d'un cluster construis pour rentre le service attendu.



dimanche 25 novembre 2018

Liquibase sources

En juin dernier, j'avais écris un article sur Liquibase [1] avec des extraits de code, seulement depuis, j'ai lancer un repository git contenant les sources des articles. Du coup cet article n'est que la que pour signaler que le code source de cet ancien article est disponible dans le repository [2]

[1] https://un-est-tout-et-tout-est-un.blogspot.com/2018/06/sgbd-liquibase.html
[2] https://github.com/collonville-tom/tc-un-est-tout-et-tout-est-un

jeudi 8 novembre 2018

Design Pattern : Whiteboard

Pourquoi le pattern whiteboard?

Le pattern Whiteboard [1, 2] est un pattern un peu spécifique, il à la particularité d'être intimement associé à la technologie OSGI [3, 5] et comme il était question dans ce blog de parler de ce framework, nous voici donc avec une petite introduction de ce dernier via ce pattern de conception.

Au delà du prétexte de l'écriture d’un article introductif à OSGI, ce pattern pattern à bien entendu une raison d'être qui lui est propre: proposer une alternative raisonnable au pattern listener dans la conception de logiciel à base de microservice comme le permet OSGI [4].

On entrevoit donc ici les raisons de l’utilisation du pattern mais pour etre plus precis, nous regarderons avant cela les concepts de base de OSGI afin d’en comprendre les besoins et avant de détailler plus en avant le pattern whiteboard, nous regarderons pourquoi le pattern listener n’est plus satisfaisant dans certaines situations.

Techno OSGI dans les grandes lignes

La technologie OSGI ou Open Service Gateway Initiative est un framework Java pour la construction d’application à base de composants et de services. Il s’agit d’un framework ayant pour vocation initial la réalisation d’application à taille réduite où la gestion mémoire est millimétré.

Il permet techniquement de gérer simultanément des librairies java évoluant dans des versions différentes ainsi que de charger et décharger ces librairies dynamiquement sans nécessiter le redémarrage de la JVM. Par contre, pour permettre ces capacités, OSGI “impose” (mais en fait c’est un bien) un paradigme de modélisation impliquant la construction de nos application selon une architecture un peu spécifique à base de composant (les bundles) et de services.

Avec cette petite introduction, on perçoit rapidement l'intérêt de ce framework! Nous y reviendrons plus tard, nous en avons vu l’essentiel pour l’instant. Il faut surtout en retenir que OSGI nous permet de construire des applications modulaires à base de composants et de services. Dédié initialement à l’embarqué, il est sortie rapidement de son contexte d’utilisation initial et s’est alors confronté aux méthodes et techniques de construction des architectures logiciels classiques.

Pourtant avec une architecture tel que proposée par OSGI, ce pattern comporte de nombreuses limites.

En effet, dans une architecture à composant ou les dépendances entre composant ne peuvent être forte, il est nécessaire de permettre à deux objets de communiquer sans forcément qu’ils aient connaissance de l’un et de l’autre directement.

Pattern Listener et limites

Entre autre, le cas du pattern Listener est caractéristique car dans le cadre de son utilisation dans le cadre OSGI, il comporte quelques limites.

Nous avons traité le pattern listener dans un autre article [6], je n’y reviendrais pas ici. disons simplement que ce pattern est un pattern d’architecture et comportemental permettant le découplage d’un observateur et d’un observer tout en formalisant son moyen de communication via un objet de type événement. Il s’agit en fait du pattern observateur mais simplifié.

Ce pattern est souvent utilisé pour sa nature événementielle dans le cadre de la gestion des IHM. Ainsi, par exemple, lorsque utilisateur sollicite la souris de son ordinateur, le contrôleur associé va générer des événements permettant de suivre ses déplacements. L'utilisation du pattern listener est en première approche une solution intéressante pour traiter ces événements surtout lorsque les sources possibles sont multiples. Pourtant c’est là sa limite également. Car alors si ces sources produisent de multiples événements simultanément alors le système peut être soumis à un “EventStorm” [1] amenant à une utilisation élevée de la mémoire et du CPU, perdre gravement en performance et dans le pire des cas conduire à un crash.

Whiteboard pattern

Le pattern Whiteboard est une solution apportée comme alternative au pattern listener dans le cadre du framework OSGI.

Il est intimement lié à OSGI cependant comme nous verrons ce framework par la suite il me semble plus pertinent d'éviter d’entrer dans trop de détails d'implémentation lié à cette technologie. Ainsi nous nous attacherons à une présentation d’un modèle abstrait du pattern (sous la forme d’un type PIM, Plateform Independant Model dans le MDA [7])

Ce pattern propose de découpler la source des événements du listener en introduisant un objet supplémentaire entre les deux servant de registre et transformant la relation entre les deux éléments par une relation de service et de consommateur de service.

Ce qui servait donc de listener devient un composant fournissant un service qui sera enregistrer dans un registre. La source des événements aura alors la tâche de demander au registre le service adéquat afin de pouvoir lui transmettre les événements produits.




L'intérêt de l'approche en ajoutant ce registre qui finalement réalisé la réification de la relation listener/sources des événements est de permettre de contrôler cette relation et son utilisation par la source des événements.

Il va être alors possible :
  • de réaliser du monitoring sur le flux d’informations voir même de le debugger
  • de rendre indisponible le service du listener si celui ci est trop utilisé et implique une consommation des ressources trop importante
  • d’adjoindre des droits spécifiques d’utilisations en ajoutant sur le registre une couche de sécurité et des permissions afin de ne pas le rendre disponible à n’importe quel consommateur du service venu
  • injecter des propriétés spécifique pour customiser le mapping (comme par exemple pour préciser un quota en événement par seconde à traiter ou pour proposer une taille de tampon d'événements, etc…)

Exemple

Pour illustrer ces mécanismes voici dans un contexte hors OSGI ce qu proposerait une implémentation du pattern listener (observateur) et ensuite son équivalent dans la philosophie du pattern Whiteboard. On précise ici que l’on reste dans une implémentation de type PIM afin de ne pas perturber la compréhension du pattern avec les spécificités technologiques du framework OSGI

Avec Listener


package listener

class Event{
    String message

    Event(String m){
        this.message=m
    }
}

interface IObserver{
    def update(Event e)
}

interface IObserved{
    def notifyAll(Event e)
}

class TrucQuiEcoute implements IObserver{
    def update(Event e){
        println("Reception d'un message")
        println(">>"+e.message)
    }
}

class TrucQuiFait implements IObserved{
    List obs=new ArrayList<Observer>()

    def notifyAll(Event e) {
        for( Observer o in obs){
            o.update(e)
        }
    }

    def makeSomething() {
        def e = new Event("Ceci est le message")
        println("Envoie d'un message")
        this.notifyAll(e)
    }
}


def ob=new TrucQuiEcoute()
def obd=new TrucQuiFait()
obd.obs.add(ob)
obd.makeSomething()

Avec Whiteboard


package whiteboard

interface IService{
    def serve(Event e)
}

class ServiceRegister{
    Map services=new HashMap<String,IService>()
}

class Event{
    String message

    Event(String m){
        this.message=m
    }
}

class TrucQuiEcoute implements IService{
    def serve(Event e){
        println("Reception d'un message")
        println(">>"+e.message)
    }
}

class TrucQuiFait implements IService{
    ServiceRegister register;

    TrucQuiFait(ServiceRegister register)
    {
        this.register=register;
    }

    def serve(Event serviceNameEvent) {
        def e = new Event("Ceci est le message")
        println("Envoie d'un message")
        this.register.services.get(serviceNameEvent.getMessage()).serve(e)
    }
}

def reg=new ServiceRegister()
reg.services.put("TrucQuiEcoute",new TrucQuiEcoute())
reg.services.put("TrucQuiFait",new TrucQuiFait(reg))

reg.services.get("TrucQuiFait").serve(new Event("TrucQuiEcoute"))

Conclusion

Voilà nous avons fait le tour de ce pattern un peu spécial enfin, surtout un peu spécialisé, qu’est le pattern Whiteboard. Celui-ci est très associé à OSGI mais il peut se comprendre sans et surtout mieux comprendre certains mécanisme de ce framework que nous verrons dans un prochain article.

Références

[1] https://www.osgi.org/wp-content/uploads/whiteboard1.pdf
[2] https://en.wikipedia.org/wiki/Whiteboard_Pattern
[3] https://www.osgi.org/
[4] https://enroute.osgi.org/FAQ/400-patterns.html
[5] https://www.theserverside.com/news/1363820/The-Whiteboard-Pattern-for-OSGi
[6] https://un-est-tout-et-tout-est-un.blogspot.com/2017/11/design-pattern-observateur.html
[7] https://laine.developpez.com/tutoriels/alm/mda/generalites-approche-mda/



mardi 28 août 2018

Les Streams avec Java 8

De la plomberie?

Je vous avais dit que nous reviendrions sur les Streams définis dans l'API Java 8. J'avais évoqué cette API en novembre dernier lorsque nous avions fait un tour d'horizon de Java 8 [1]. Je sais ça fait un peu loin et pas mal d'eau a coulé sous les ponts, cependant je pense que même si cet article arrive un peu tard (même si nous avons quand même vu pas mal d'autres choses depuis à la place) cela ne fera pas de mal de regarder un peu plus dans le détail cette API qui est incontournable.

Tout d'abord il faut dire que le Stream est un pattern de conception, nous ne l’avions pas traité dans le blog, il serait intéressant de le traité mais il s'agit essentiellement d'un paradigme de modélisation de la gestion de flux qu'une manière d’implémenter ces dits flux. Dans ce paradigme, tout comme dans l'API [2], bien sur nous y retrouverons des traits comme la capacité de construire des pipelines de traitement.



En fait du coup, un stream, c'est quoi? un tuyau! ou plusieurs mis bout a bout entre lesquels divers filtres, transformations et traitement sont appliqués. Les éléments ainsi introduit dans le stream va alors subir les différents traitements successivement (ce n'est pas aussi simple car le stream peut avoir a traiter plusieurs éléments en même temps, voir tous les éléments) et ce tant qu'il y a des éléments. Grace a cette propriété (le tant que) les streams sont capables de traiter des listes ou des ensembles d’éléments de taille non connu (pour ne pas dire infini qui n'aurait pas beaucoup de sens...) initialement.

Dans Java 8, les streams vont donc être un complément au pattern Iterator qui se cache derrière nos structures foreach. D'une part en permettant des traitements potentiellement infini mais aussi de façon a simplifier les boucles et les traitements. En effet le constat est que les manipulations sur les listes dans les boucles sont généralement des traitements produisant des résultats sans liens avec un besoin quelconque de modifier la liste elle-même (chose généralement à bannir sans risque d'effets de bords désagréables, d'ou l'utilisation de liste immutable).

Initialisation d'un stream

Ainsi, grâce aux streams, le traitement sur un ensemble d’éléments (liste, ensemble, infini ou non) peut être infini si le stream est construit comme tel, et cela ne pose aucun problème. Par exemple on peut créer des streams finis voir vide:

1
2
3
4
5
6
7
8
9
Stream emptyStream = Stream.empty();

Collection<String> collection = Arrays.asList("a", "b", "c");
Stream<String> streamOFromArray = collection.stream();

String[] arr = new String[]{"a", "b", "c"};
Stream<String> streamOFromArray2 = Arrays.stream(arr);

IntStream streamOfChars = "abc".chars();

ou des streams infinis (vous verrons comment les limiter), en compréhension

1
2
3
Stream<String> genStream = Stream.generate(() -> "1");
IntStream s=IntStream.range(1,10);
Stream<String> iterateStream = Stream.iterate("thomas",(x) -> x.toUpperCase());

Au passage on constate que les Streams s'appuient aussi beaucoup sur les nouvelles API fonctionnelles apportées aussi par Java 8 avec les lambdas expression (sujet traité dans [1]).

On peut aussi construire des streams statiquement avec des builders comme on le ferait avec un StringBuilder (pour ceux qui connaissent)

1
Stream<String> phrase=Stream.<String>builder().add("Ceci ").add("est ").add("une ").add("phrase.").build();

Ou a partir de chaîne de caractères pour en faciliter le parsing ou la dissection avec des RegEx ( StreamSupport)

Filtration

Maintenant que l'on a des streams sous la main, le tout est d'en faire quelque chose. Comme nous en avions parlé, différents traitements sont possibles et chacun d'eux nous permettra de construire alors un nouveau stream. Il importe donc de bien comprendre l'emploi de ces differentes fonctions car comme nous l'avons évoqué, ces traitements vont se cascader, impliquant que l'ordre d’exécution à une importance significative.

Par exemple :

Considérons un stream produisant des entiers dont on ne gardera que les éléments pair. On voudra ne conserver que les 20 premiers éléments du calcul. Pour répondre à ce problème, il nous faudra employer la fonction filter et la fonction limit.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
System.out.println("Stream1");
Stream<Integer> stream = Stream.iterate(0,(x)-> x+1 );
stream.limit(10).filter((x) -> (x % 2) == 0).forEach(System.out::println);

System.out.println("Stream2");
Stream<Integer> stream2 = Stream.iterate(0,(x)-> x+1 );
stream2.filter((x) -> (x % 2) == 0).limit(10).forEach(System.out::println);

System.out.println("Stream3");
IntStream stream3=new Random().ints(0,100);
stream3.limit(10).filter((x) -> (x % 2) == 0).sorted().forEach(System.out::println);

Avec cet exemple simple, on voit bien que l'ordre d’exécution des fonctions a une importance. Le résultat est affiché via la fonction foreach grâce a laquelle il est possible de passer en paramètre une fonction qui sera appliqué a tous les éléments produit par le stream.

Manipulation sur les streams

A la place et comme suit, il est possible d'utiliser la fonction collect qui renvoi un élément de l'API collection et aggregant l'ensemble des éléments produits par le stream (une fois celui-ci vidé et trié avec la fonction sorted).

1
2
3
System.out.println("Stream4");
Stream<Integer> stream4=new Random().ints(0,100).boxed();// on fait un boxed pour traduire de IntStream
Set<Integer> set=stream4.filter((x) -> (x % 2) == 0).limit(10).sorted().collect(Collectors.toSet());

Voyons maintenant les fonctions un peu plus complexe que sont map et reduce. Ces deux fonctions sont un peu le cœur des streams car elles vont nous permettre d'appliquer des opérations de traitements, transformations et calculs.

Prenons par exemple le besoin de calculer un ensemble de point suivant une droite tel que y=a*x+b avec a=4 et b=3. Avec l'API Stream, cela nous donne le code suivant:

1
2
Stream<Double> stream= DoubleStream.iterate(0.00, x -> x + 1 ).boxed();
Set<Double> s=stream.map(x -> 4*(x+Math.random())+3).limit(200000).collect(Collectors.toSet());

De la même manière, il va être possible de calculer la moyenne des différents points avec la fonction reduce :

1
2
Integer value=IntStream.range(1,10).reduce((x,y)->x+y).getAsInt();
System.out.println(value); //45

Avec ces deux exemples simples, on comprend assez facilement le role des fonctions map/reduce.

La première va construire un stream constitué des résultats de la fonction passée en paramètre (un peu comme foreach sans être une fonction finale pour le stream).

A noter que des variantes de la fonction map existent : des fonctions comme mapToObject, boxed, mapToDouble,etc...permettent de faciliter la conversion d'un Stream d'un type d'objet en un Stream contenant un autre type d'objets.

La seconde, la fonction reduce, a l'inverse va appliquer de façon globale la fonction passée en paramètre afin de restituer un resultat unitaire (ici une moyenne).

Un peu plus loin

Afin afin de permettre des traitements spécifiques, il est possible d'employer des Streams spécifiques dédiés a la manipulation des entiers, des doubles, des longs etc... Ces Streams permettent alors de récupérer pas exemple des statistiques sur les données du stream comme avec l’objet IntSummaryStatistics

1
2
3
4
5
6
Stream<Integer> stream4=new Random().ints(0,100).boxed();
IntSummaryStatistics stats=stream4.mapToInt(x -> x).summaryStatistics();
System.out.println("Plus grand" + stats.getMax());
System.out.println("Plus petit" + stats.getMin());
System.out.println("La somme" + stats.getSum());
System.out.println("La moyenne" + stats.getAverage());

Voila nous sommes a la fin de la présentation des Streams. Dernier point a évoquer est la possibilité d'utiliser la fonction parallel qui s'appuie sur l'API Java 7 fork/join [3] et fourni la possibilité de paralléliser les traitements sur les éléments du stream afin d'en améliorer les performances d’exécution. Il sera peut être utile dans un article futur de nous intéresser a cette fameuse API fork/join.


1
2
Stream<Double> stream2= DoubleStream.iterate(0.00, x -> x + 1 ).boxed();
Set<Double> s2=stream2.parallel().map(x -> 4*(x+Math.random())+3).limit(200000).collect(Collectors.toSet());

Voila, nous avons fait le tour de l'API Stream de Java 8. Elle utilise largement les lambda expressions et implémente une nouvelle manière de penser le traitement des données tout en fournissant des outils simples de manipulation (map/reduce) sans omettre le besoin de performance (parallel). Pour ceux voulant d'autres exemples d'utilisation je vous invite a consulter [4], [5], [6] et [7].

Pour avoir le code source (en vrac) c'est ici [8].

Référence:

  • [1] https://un-est-tout-et-tout-est-un.blogspot.com/2017/11/evolution-java-8.html
  • [2] https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html
  • [3] http://blog.paumard.org/2011/07/05/java-7-fork-join/
  • [4] https://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/
  • [5] https://blog.axopen.com/2014/05/java-8-api-stream-introduction-collections/
  • [6] https://www.tutorialspoint.com/java8/java8_streams.htm
  • [7] https://www.baeldung.com/java-8-streams
  • [8] https://github.com/collonville-tom/tc-un-est-tout-et-tout-est-un/tree/master/Java8-Streams/src