Ingeniería en Qumulo: REST API

Cuando comenzamos a construir nuestro sistema de archivos, sabíamos que queríamos poder controlar e inspeccionar el sistema de archivos con herramientas de línea de comandos, UI y pruebas automatizadas. Usando un REST API exponer esta funcionalidad fue un ajuste natural, y nos dimos cuenta de que si queríamos esta funcionalidad dentro de Qumulo, es probable que nuestros clientes también la deseen. Entonces, decidimos hacer pública nuestra API REST desde el principio.

En esta publicación, exploraremos los principios de nuestra API, los desafíos con REST y cómo evolucionamos continuamente la API junto con nuestro sistema de archivos.

Principios API REST

Representational State Transfer (REST) ​​es un estilo arquitectónico ampliamente utilizado con el que asumimos que ya está familiarizado. Al definir una nueva API usando REST, hay muchas opciones que tomar en el camino. Primero, debe decidir qué tipo de funcionalidad incluye su API. Plano de control (configuración del sistema y estadísticas)? Plano de datos (archivos y metadatos almacenados en el sistema de archivos)? Elegimos ambos, además de los puntos finales solo internos para ayudar al desarrollo de funciones. Todo lo que se puede hacer en un clúster de Qumulo se puede hacer a través de la API REST.

A continuación, consideramos el contenido de la respuesta. Cuando se utilizan protocolos del sistema de archivos como SMB o NFS para leer metadatos, se obtiene la interpretación de ese protocolo del estado del sistema de archivos, y puede estar limitado en lo que puede expresar. Nuestra API REST, por el contrario, devuelve la verdad fundamental: los clientes no necesitan interpretar los datos devueltos y nosotros devolvemos toda la información disponible. A medida que ampliamos las capacidades del sistema de archivos (como almacenar metadatos agregados), aumentamos nuestros puntos finales para exponer estas capacidades.

Inspirados en el libro de reglas de diseño de API REST, clasificamos cada uno de nuestros puntos finales como uno de estos arquetipos de recursos:

  • Documento: un registro de base de datos, con ambos campos y enlaces a recursos relacionados
  • Recaudación: un directorio de recursos (generalmente recursos de documentos)
  • Normativa: una función ejecutable (como "enviar correo")
  • Tienda: un repositorio de recursos administrado por el cliente (generalmente no se utiliza en nuestra API)

Al estructurar nuestros URI, quisimos simplificar la tarea de los desarrolladores o administradores de hacer una secuencia de comandos en nuestros puntos finales o utilizar herramientas como cURL. También queríamos asegurarnos de que los clientes no se rompan accidentalmente si se modifica el contrato de un punto final. Esto nos llevó a poner más contenido en el URI, como el número de versión, favoreciendo los contratos explícitos sobre los implícitos. Por ejemplo, aquí está cómo leer un directorio:

/ v1 / archivos / % 2F / entradas / ? limit = 1000

/ v1: la versión del punto final siempre viene primero
/ archivos /: el documento, la colección, o el controlador para acceder. En este caso, / files / es una colección.
% 2F: el id de un documento en la colección de archivos. En este caso, el directorio raíz del sistema de archivos.
/ entradas /: la acción a tomar en el archivo / carpeta especificada.
? limit = 1000: finalmente, los parámetros de consulta opcionales a la acción.

Con ese diseño, el único encabezado HTTP que requerimos es Autorización para tokens de portador de estilo 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