Кэширование данных#

Рассмотрим ситуацию, когда необходимо реализовать следующие взаимодействия.

Клиент 1 отправляет запрос на backend, ответ от backend валидируется и возвращается как к клиенту. Кроме этого помещаем как запрос, так и ответ в кэш, чтобы другой поток (например, поток логирования) периодически забирал эти данные.

Создадим отдельный профиль для этого с названием profile_cache.

profile: profile_cache
system:
  conn_count: 4096
  wrk_count: 1
  optional:
    log_level: debug
    storages:
     - name: request
     - name: response
version: 2.0.3
service:
 service_http_proxy:
    - id: 1
      allowed_queries:
        - method: get
      url: /proxy
      listen:
        - port: 20208
      chains:
        request_chains:
          - actions:
            - action: SharedMapSetter
              params:
                shared_store_name: request
                shared_store_key:  $arg_Id
                operation: ADD
            message: "{\"message_id\": \"$arg_id\", \"type\": \"request\"}"
        response_chains:
            - actions:
               - action: JsonValidation
                 params:
                  validation_schema: {type: file, path: schemas/sel_schema_1.json}
              message: $clj_response_body
            - actions:
               - action: SharedMapSetter
                 params:
                  shared_store_name: response
                  shared_store_key:  $arg_Id
                  operation: ADD
              message: "{\"message_id\": \"$arg_id\", \"type\": \"response\", \"body\": $clj_response_body}"
      upstream_group_id: 1
      upstream_groups:
        - id: 1
          servers:
            - server: 127.0.0.1:40001
 service_static:
    - id: 1
      listen:
        - port: 20209
      #allowed_queries:
      #  - method: delete
      url:        /cache
      chains:
        request_chains:
        - actions:
          - action: SharedMapRemover
            conditions:
             - {var: $arg_type, operator: "=", val: "request"}
            params:
             operation: REMOVE_FIRST
             shared_store_name: request
            actions:
            - action: SetVarFromMessage
              params:
                variable: $clj_message
        - actions:
           - action: SharedMapRemover
             conditions:
              - {var: $arg_type, operator: "=", val: "response"}
             params:
              operation: REMOVE_FIRST
              shared_store_name: response
             actions:
              - action: SetVarFromMessage
                params:
                  variable: $clj_message
      response_generator:
        rules_file:  rules.yml
        content_type: application/json
        pattern_file: response.json

Рассмотрим отдельные элементы нашей конфигурации.

В секции optional раздела system определяем два разделяемых кэша для запросов и ответов соответственно - каждый из них по умолчанию имеет размер 10Mb.

Далее идет описание сервиса прокси:

service_http_proxy:
   - id: 1
     allowed_queries:
       - method: get
     url: /proxy
     listen:
       - port: 20208
     chains:
       request_chains:
         - actions:
           - action: SharedMapSetter
             params:
               shared_store_name: request
               shared_store_key:  $arg_Id
               operation: ADD
           message: "{\"message_id\": \"$arg_id\", \"type\": \"request\"}"
       response_chains:
           - actions:
              - action: JsonValidation
                params:
                 validation_schema: {type: file, path: schemas/sel_schema_1.json}
             message: $clj_response_body
           - actions:
              - action: SharedMapSetter
                params:
                 shared_store_name: response
                 shared_store_key:  $arg_Id
                 operation: ADD
             message: "{\"message_id\": \"$arg_id\", \"type\": \"response\", \"body\": $clj_response_body}"
     upstream_group_id: 1
     upstream_groups:
       - id: 1
         servers:
           - server: 127.0.0.1:40001

Здесь задаем, что для запроса допустим метод GET (в секции allowed_quieries), а также определяем цепочки действий для запроса и ответа. В цепочке обработки запроса помещаем в кэш request запись с идентификатором, равному значению параметра запроса id и телом, представляющим собой json из двух элементов. В цепочке обработки ответа, сначала валидируем ответ по схеме, которую мы задавали ранее:

{
    "$schema": "http://json-schema.org/draft-07/schema#",
    "type": "object",
    "properties": {
      "lastname": {
        "type": "string"
      },
      "firstname": {
        "type": "string"
      },
      "phone": {
        "type": "string"
      },
      "version": {
         "type": "string"
      }
    },
    "additionalProperties": false,
    "required": [
      "lastname", "firstname", "version"
    ]
  }

А затем помещаем в кэш элемент с ключом, соответствующим значению параметра id из запроса b и с данными вида : "{"message_id": "$arg_id", "type": "response", "body": $clj_response_body}".

Второй сервис, в зависимости от значения параметра запроса type, удаляет первый элемент кэша запроса или ответа и возвращает его с помощью генератора ответа.

service_static:
   - id: 1
     listen:
       - port: 20209
     #allowed_queries:
     #  - method: delete
     url:        /cache
     chains:
       request_chains:
       - actions:
         - action: SharedMapRemover
           conditions:
            - {var: $arg_type, operator: "=", val: "request"}
           params:
            operation: REMOVE_FIRST
            shared_store_name: request
           actions:
           - action: SetVarFromMessage
             params:
               variable: $clj_message
       - actions:
          - action: SharedMapRemover
            conditions:
             - {var: $arg_type, operator: "=", val: "response"}
            params:
             operation: REMOVE_FIRST
             shared_store_name: response
            actions:
             - action: SetVarFromMessage
               params:
                 variable: $clj_message
     response_generator:
       rules_file:  rules.yml
       content_type: application/json
       pattern_file: response.json

Здесь для обеих цепочек осуществляется извлечение с удалением первого элемента по дате вставки из кэша . В зависимости от значения параметра type, элемент извлекается либо из кэша запросов, либо из кэша ответов. После чего результат извлечения помещается в переменную clj_message.

Секция response_generator здесь отвечает за генерацию ответа клиенту. Здесь правила генерации ответа, задаваемые в файле rules.yml, который имеет следующий вид:

rules:
  - condition:
     - var: $clj_shared_map_error
       operator: "!="
       val: ""
    replacements:
      - pattern: message
        val: "{\"status\": \"error\"}"
  - default: True
    replacements:
       - pattern: message
         val: $clj_message
    status: 200

Соответственно, правила генерации свидетельствуют о том, что если операция извлечения завершилась с ошибкой, то возвращаем ошибочный ответ с телом {"status": "error"}, который мы подставим в шаблон, определенный в файле response.json. Если же ошибки не было, то мы возвращаем ответ со статусом 200 и телом со значением, извлеченным в переменную clj_message.

Файл с шаблоном ответа response.json cодержит следующую подстановочную конструкцию:

{message}

Выполним следующие действия:

  1. Добавим профиль profile_cache.

  2. Скопируем файлы response.json и rules.yml в каталог /sowa/profile_storage/custom/profile_cache.

  3. Создадим каталог /sowa/profile_storage/custom/profile_cache/schemas.

  4. Скопируем sel_schema_1.json в каталог, созданный в п. 3.

  5. Сконфигурируем профиль.

  6. Запустим профиль.

Добавим в профиль заглушку сервис, возвращающий статичные данные:

- id: 5
     name: simple_5
     listen:
        - port: 40001
     url: /proxy
     optional:
       response_body: "{\"version\": \"v1\", \"lastname\": \"Ivanov\", \"firstname\": \"Ivan\"}"
       response_code: 200
     response_headers:
       - name: IDENT
         value: $server_addr:$server_port

Переконфигурируем профиль с заглушками и перезапустим его.

[sowacfg@741037ad695c chains_examples]$ curl -v http://localhost:20208/proxy?id=2
* About to connect() to localhost port 20208 (#0)
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 20208 (#0)
> GET /proxy?id=2 HTTP/1.1
> User-Agent: curl/7.29.0
> Host: localhost:20208
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Wed, 08 Apr 2020 10:07:33 GMT
< Content-Type: application/octet-stream
< Content-Length: 60
< Connection: keep-alive
< Server: nginx-clojure
< IDENT: 127.0.0.1:40001
< Accept-Ranges: bytes
<
* Connection #0 to host localhost left intact
{"version": "v1", "lastname": "Ivanov", "firstname": "Ivan"}[sowacfg@741037ad695c chains_examples]

Если обратимся к кэшу, то получим:

curl -v -X DELETE http://localhost:20209/cache?type=response
* About to connect() to localhost port 20209 (#0)
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 20209 (#0)
> DELETE /cache?type=response HTTP/1.1
> User-Agent: curl/7.29.0
> Host: localhost:20209
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Wed, 08 Apr 2020 10:08:54 GMT
< Content-Type: application/json
< Content-Length: 109
< Connection: keep-alive
< Server: SOWA
<
* Connection #0 to host localhost left intact
{"message_id": "1", "type": "response", "body": {"version": "v1", "lastname": "Ivanov", "firstname": "Ivan"}}

Общие рекомендации:

  1. Обратите внимание, что т.к. кэш in-memory, то все операции чувствительны к памяти, поэтому избегайте хранения больших объектов и операций над ними.

  2. Использование конструкций вида "{"message_id": "$arg_id", "type": "response", "body": $clj_response_body}" не рекомендовано для больших объектов, т.к. в отличии от простого обращения к clj_request_body, clj_response_body, такие сообщения не кэшируются при обработки в цепочках, что также накладывает повышенные требования к ресурсу памяти.