Ingénierie chez Qumulo: API REST

Lorsque nous avons commencé à créer notre système de fichiers, nous savions que nous voulions pouvoir contrôler et inspecter le système de fichiers avec des outils de ligne de commande, une interface utilisateur et des tests automatisés. Utilisant un API REST exposer cette fonctionnalité était une solution naturelle, et nous avons réalisé que si nous voulions cette fonctionnalité dans Qumulo, nos clients le voudraient probablement aussi. Nous avons donc choisi de rendre notre API REST publique dès le début.

Dans cet article, nous explorerons les principes de notre API, les défis avec REST et la manière dont nous évoluons continuellement avec notre système de fichiers.

Les principes de l'API REST

Le transfert d'état représentatif (REST) ​​est un style architectural largement utilisé que nous supposons que vous connaissez déjà. Lors de la définition d'une nouvelle API à l'aide de REST, il y a de nombreux choix à faire en cours de route. Tout d'abord, vous devez décider du type de fonctionnalité à intégrer à votre API. Plan de contrôle (configuration du système et statistiques) ? Plan de données (fichiers et métadonnées stockés dans le système de fichiers) ? Nous avons choisi les deux, ainsi que des points de terminaison internes uniquement pour aider au développement des fonctionnalités. Tout ce qui peut être fait sur un cluster Qumulo peut être fait via l'API REST.

Ensuite, nous avons considéré le contenu de la réponse. Lorsque vous utilisez des protocoles de système de fichiers comme SMB ou NFS pour lire des métadonnées, vous obtenez l'interprétation de l'état du système de fichiers par ce protocole, et il peut être limité dans ce qu'il peut exprimer. Notre API REST, en revanche, renvoie une vérité terrain - les clients n'ont pas besoin d'interpréter les données renvoyées et nous renvoyons toutes les informations disponibles. Au fur et à mesure que nous étendons les capacités du système de fichiers (comme le stockage de métadonnées agrégées), nous augmentons nos points de terminaison pour exposer ces capacités.

Inspiré du livret de règles de conception de l'API REST, nous classons chacun de nos points de terminaison comme l'un de ces archétypes de ressources:

  • Document: un enregistrement de base de données, avec les deux champs et des liens vers des ressources connexes
  • Collection: un répertoire de ressources (généralement des ressources documentaires)
  • Contrôleur: une fonction exécutable (comme "send-mail")
  • Magasin: un référentiel de ressources géré par le client (généralement non utilisé dans notre API)

En structurant nos URI, nous voulions simplifier le script pour les développeurs ou les administrateurs par rapport à nos points de terminaison ou utiliser des outils comme cURL. Nous voulions également nous assurer que les clients ne se cassent pas accidentellement si le contrat d'un terminal change. Cela nous a conduit à mettre plus de contenu dans l'URI, comme le numéro de version, en privilégiant les contrats explicites par rapport aux contrats implicites. Par exemple, voici comment lire un répertoire:

/ v1 /des dossiers/ % 2F / entrées / ? limit = 1000

/ v1: la version du noeud final vient toujours en premier
/des dossiers/: le document, la collection ou le contrôleur auquel accéder. Dans ce cas, / files / est une collection.
% 2F: l'id d'un document dans la collection de fichiers. Dans ce cas, le répertoire racine du système de fichiers.
/ entrées /: l'action à effectuer sur le fichier / dossier spécifié.
? limit = 1000: enfin, paramètres de requête optionnels pour l'action.

Avec cette conception, le seul en-tête HTTP requis est l’autorisation des jetons de support de style OAuth2:

curl -k -X GET -H "Authorization: Bearer <token>" https://server:8000/v1/file-system

It’s worth noting that we did not try to mimic existing file system REST APIs. We wanted our API to be specific to our file system’s capabilities and give the user maximum control over the system. If at some point in future we want to support clients that talk S3, WebDAV, or whatever, we’ll add new ports for those protocols, keeping them separate from our core REST API.

Challenges with REST

Many of our configuration endpoints have straightforward behavior: you use GET to retrieve a document (e.g. GET /v1/users/123), and you use SET or PATCH to update the document. The requests take effect immediately, so that when you receive a 200 OK response, you know the change has been made.

But REST is neither stateful nor transactional, which can impact the user experience if not considered properly. Let’s say an administrator is editing a file share on the cluster using the built-in UI. Between the time the UI retrieves the file share details and when the administrator saves their changes, another user or process could change that file share. By default in our API, the last writer wins, so the administrator would unwittingly clobber these changes. That’s not the user experience we want, so we use ETag and If-Match HTTP headers for all of our documents to prevent accidental overwrites. When the UI retrieves a document, it reads the ETag response header (entity tag, or essentially a hashcode) and stores that. Later, when updating that same document, the UI sends an If-Match request header, which tells the cluster to only perform the action if the document is the same as we expect. If the document changed, we’ll get back a 12 Precondition Failed response, which allows us to build a better experience for the user.

Long-running actions also require special consideration. To keep our REST API response times predictable, we process short-running requests synchronously, and long-running requests asynchronously. We classify every endpoint in our API as short- or long-running, so that clients know what kind of response they need to handle to reduce complexity. All GET, PUT, and PATCH operations on documents and collections are short-running requests, returning 200 OK when successfully processed. In contrast, we always POST to a controller endpoint for long-running requests, which return 202 Accepted with a URI to poll for completion status. For example, when joining a cluster to Active Directory, the client invokes the controller like this:

Request POST /v1/ad/join
Request body {
"domain": "ad.server.com",
"user": "domain-user",
"password": "domain-password",

}

If the request is valid, the controller responds:

 

Request POST /v1/ad/join
202 Accepted {
"monitor_uri": "/v1/ad/monitor"
}

The client can then issue repeated GET /v1/ad/monitor calls while waiting for the join action to succeed or fail.

REST API Evolution

To ensure our REST API keeps pace with our file system’s capabilities, the endpoints are auto-generated from code. This means that the file system, API, and API documentation are always in sync. Our build system prevents us from accidentally making changes to internal data structures that would result in REST API changes that break API clients. And by putting our API documentation in code, it stays current with the code.

Two years into the development of our REST API, we realized we had a problem: the API had grown organically as different dev teams added functionality, which led to inconsistencies between endpoints and a questionable hierarchy that made it difficult to discover functionality. To address this, we did two things: we migrated to a new API namespace over a series of releases to fix consistency and discoverability issues, and we created an API roadmap for Qumulo engineers to follow that allows the API to evolve and remain consistent. An example of an API namespace improvement was the consolidation of all real-time analytics-related functionality under /v1/analytics. Previously, this functionality was scattered across the entire namespace, and when we heard from customers that they couldn’t find these features, we knew this was an area to improve.

Now that we’ve solidified our /v1 API, individual endpoints can change version if a breaking change is needed. (Breaking changes include things like adding new required fields to requests, or changing the semantics of data we return.) Even with this provision, breaking changes are a last resort. We strive to find ways to augment response data or introduce optional fields without impacting existing API clients..

In this post, we explored the tenets of Qumulo’s REST API, how we tackled some challenges with REST, and our approach for evolving the API in conjunction with the product.

Share this post