A good production ready service should have functionality for various cases:
Overload
Service should respond with HTTP 429 codes to some requests while still being able to handle the rest
Debugging of a running service
inspect logs
get more logs from the suspiciously behaving service and then turn the logging level back
profile memory usage
see requests in flight
Experiments
Should be a way to turn on/off arbitrary functionality without restarting the service
Metrics and Logs
Functional testing
This tutorial shows a configuration of a typical production ready service. For information about service interactions with other utilities and services in container see Deploy Environment Specific Configurations.
Static configs tend to become quite big, so it is a good idea to move changing parts of it into variables. To do that, declare a config_vars field in the static config and point it to a file with variables.
A file with config variables could look like this.
Now in static config you could use $variable-name to refer to a variable, *#fallback fields are used if there is no variable with such name in the config variables file:
# yaml
http-client:
fs-task-processor: fs-task-processor
user-agent: $server-name
user-agent#fallback: 'userver-based-service 1.0'
Task processors
A good practice is to have at least 3 different task processors:
# yaml
task_processors:
fs-task-processor: # for blocking operations
thread_name: fs-worker
worker_threads: $fs_worker_threads
worker_threads#fallback: 2
main-task-processor: # for nonblocking operations
thread_name: main-worker
worker_threads: $main_worker_threads
worker_threads#fallback: 6
monitor-task-processor: # for monitoring
thread_name: mon-worker
worker_threads: $monitor_worker_threads
worker_threads#fallback: 1
event_thread_pool: # ev pools to deal with OS events
threads: $event_threads
threads#fallback: 2
Moving blocking operations into a separate task processor improves responsiveness and CPU usage of your service. Monitor task processor helps to get statistics and diagnostics from server under heavy load or from a server with a deadlocked threads in the main task processor.
Warning
This setup is for an abstract service on an abstract 8 core machine. Benchmark your service on your hardware and hand-tune the thread numbers to get optimal performance.
This is a server::handlers::Ping handle that returns 200 if the service is OK, 500 otherwise. Useful for balancers, that would stop sending traffic to the server if it responds with codes other than 200.
# yaml
handler-ping:
path: /ping
method: GET
task_processor: main-task-processor # !!!
throttling_enabled: false
url_trailing_slash: strict-match
Note that the ping handler lives on the task processor of all the other handlers. Smart balancers may measure response times and send less traffic to the heavy loaded services.
Service starts with dynamic config values from dynamic-config.fs-cache-path file or from dynamic-config-client-updater.fallback-path file. Service updates dynamic values from a configs service.
# yaml
dynamic-config:
fs-cache-path: $config-cache
fs-task-processor: fs-task-processor
dynamic-config-client:
config-url: $config-server-url
http-retries: 5
http-timeout: 20s
service-name: $service-name
dynamic-config-client-updater:
config-settings: false
fallback-path: $config-fallback
full-update-interval: 1m
load-only-my-values: true
store-enabled: true
update-interval: 5s
Congestion Control
congestion_control::Component limits the active requests count. In case of overload it responds with HTTP 429 codes to some requests, allowing your service to properly process handle the rest.
With such setup you could poll the metrics from handler server::handlers::ServerMonitor that we've configured in previous section. However a much more mature approach is to write a component that pushes the metrics directly into the remote metrics aggregation service or to write a handle that provides the metrics in the native aggregation service format.
Secdist - secrets distributor
Storing sensitive data aside from the configs is a good practice that allows you to set different access rights for the two files.
server::handlers::TestsControl is a handle that allows controlling the service from test environments. That handle is used by the testsuite from functional tests to mock time, invalidate caches, testpoints and many other things. This component should be disabled in production environments.
components::TestsuiteSupport is a lightweight storage to keep minor testsuite data. This component is required by many high-level components and it is safe to use this component in production environments.
Functional tests are used to make sure that the service is working fine and implements the required functionality. A recommended practice is to build the service in Debug and Release modes and tests both of them, then deploy the Release build to the production, disabling all the tests related handlers.
Debug builds of the userver provide numerous assertions that validate the framework usage and help to detect bugs at early stages.
Typical functional tests for a service consist of a conftest.py file with mocks+configs for the sereffectivelyvice and a bunch of test_*.py files with actual tests. Such approach allows to reuse mocks and configurations in different tests.