Merge "Add devref on rolling upgrades"
This commit is contained in:
commit
dab8d53cd7
@ -36,6 +36,7 @@ Programming HowTos and Tutorials
|
|||||||
replication
|
replication
|
||||||
migration
|
migration
|
||||||
api.apache
|
api.apache
|
||||||
|
rolling.upgrades
|
||||||
|
|
||||||
Background Concepts for Cinder
|
Background Concepts for Cinder
|
||||||
------------------------------
|
------------------------------
|
||||||
|
328
doc/source/devref/rolling.upgrades.rst
Normal file
328
doc/source/devref/rolling.upgrades.rst
Normal file
@ -0,0 +1,328 @@
|
|||||||
|
..
|
||||||
|
Copyright (c) 2016 Intel Corporation
|
||||||
|
All Rights Reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
not use this file except in compliance with the License. You may obtain
|
||||||
|
a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
License for the specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
|
|
||||||
|
Upgrades
|
||||||
|
========
|
||||||
|
|
||||||
|
Starting from Mitaka release Cinder gained the ability to be upgraded without
|
||||||
|
introducing downtime of control plane services. Operator can simply upgrade
|
||||||
|
Cinder services instances one-by-one. To achieve that, developers need to make
|
||||||
|
sure that any introduced change doesn't break older services running in the
|
||||||
|
same Cinder deployment.
|
||||||
|
|
||||||
|
In general there is a requirement that release N will keep backward
|
||||||
|
compatibility with release N-1 and in a deployment N's and N-1's services can
|
||||||
|
safely coexist. This means that when performing a live upgrade you cannot skip
|
||||||
|
any release (e.g. you cannot upgrade N to N+2 without upgrading it to N+1
|
||||||
|
first). Further in the document N will denote the current release, N-1 a
|
||||||
|
previous one, N+1 the next one, etc.
|
||||||
|
|
||||||
|
Having in mind that we only support compatibility with N-1, most of the
|
||||||
|
compatibility code written in N needs to exist just for one release and can be
|
||||||
|
removed in the beginning of N+1. A good practice here is to mark them with
|
||||||
|
:code:`TODO` or :code:`FIXME` comments to make them easy to find in the future.
|
||||||
|
|
||||||
|
Please note that proper upgrades solution should support both
|
||||||
|
release-to-release upgrades as well as upgrades of deployments following the
|
||||||
|
Cinder master more closely. We cannot just merge patches implementing
|
||||||
|
compatibility at the end of the release - we should keep things compatible
|
||||||
|
through the whole release.
|
||||||
|
|
||||||
|
To achieve compatibility, discipline is required from the developers. There are
|
||||||
|
several planes on which incompatibility may occurr:
|
||||||
|
|
||||||
|
* **REST API changes** - these are prohibited by definition and this document
|
||||||
|
will not describe the subject. For further information one may use `API
|
||||||
|
Working Group guidelines
|
||||||
|
<https://specs.openstack.org/openstack/api-wg/guidelines/evaluating_api_changes.html>`_
|
||||||
|
for reference.
|
||||||
|
|
||||||
|
* **Database schema migrations** - e.g. if N-1 was relying on some column in
|
||||||
|
the DB being present, N's migrations cannot remove it. N+1's however can
|
||||||
|
(assuming N has no notion of the column).
|
||||||
|
|
||||||
|
* **Database data migrations** - if a migration requires big amount of data to
|
||||||
|
be transfered between columns or tables or converted, it will most likely
|
||||||
|
lock the tables. This may cause services to be unresponsive, causing the
|
||||||
|
downtime.
|
||||||
|
|
||||||
|
* **RPC API changes** - adding or removing RPC method parameter, or the method
|
||||||
|
itself, may lead to incompatibilities.
|
||||||
|
|
||||||
|
* **RPC payload changes** - adding, renaming or removing a field from the dict
|
||||||
|
passed over RPC may lead to incompatibilities.
|
||||||
|
|
||||||
|
Next sections of this document will focus on explaining last four points and
|
||||||
|
provide means to tackle required changes in these matters while maintaining
|
||||||
|
backward compatibility.
|
||||||
|
|
||||||
|
|
||||||
|
Database schema and data migrations
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
|
In general incompatible database schema migrations can be tracked to ALTER and
|
||||||
|
DROP SQL commands instruction issued either against a column or table. This is
|
||||||
|
why a unit test that blocks such migrations was introduced. We should try to
|
||||||
|
keep our DB modifications additive. Moreover we should aim not to introduce
|
||||||
|
migrations that cause the database tables to lock for a long period. Long lock
|
||||||
|
on whole table can block other queries and may make real requests to fail.
|
||||||
|
|
||||||
|
Adding a column
|
||||||
|
...............
|
||||||
|
|
||||||
|
This is the simplest case - we don't have any requirements when adding a new
|
||||||
|
column apart from the fact that it should be added as the last one in the
|
||||||
|
table. If that's covered, the DB engine will make sure the migraton won't be
|
||||||
|
disruptive.
|
||||||
|
|
||||||
|
Dropping a column not referenced in SQLAlchemy code
|
||||||
|
...................................................
|
||||||
|
|
||||||
|
When we want to remove a column that wasn't present in any SQLAlchemy model or
|
||||||
|
it was in the model, but model was not referenced in any SQLAlchemy API
|
||||||
|
function (this basically means that N-1 wasn't depending on the presence of
|
||||||
|
that column in the DB), then the situation is simple. We should be able to
|
||||||
|
safely drop the column in N release.
|
||||||
|
|
||||||
|
Removal of unnecessary column
|
||||||
|
.............................
|
||||||
|
|
||||||
|
When we want to remove a used column without migrating any data out of it (for
|
||||||
|
example because what's kept in the column is obsolete), then we just need to
|
||||||
|
remove it from the SQLAlchemy model and API in N release. In N+1 or as a
|
||||||
|
post-upgrade migration in N we can merge a migration issuing DROP for this
|
||||||
|
column (we cannot do that earlier because N-1 will depend on the presence of
|
||||||
|
that column).
|
||||||
|
|
||||||
|
ALTER on a column
|
||||||
|
.................
|
||||||
|
|
||||||
|
A rule of thumb to judge which ALTER or DROP migrations should be allowed is to
|
||||||
|
look in the `MySQL documentation
|
||||||
|
<https://dev.mysql.com/doc/refman/5.7/en/innodb-create-index-overview.html#innodb-online-ddl-summary-grid>`_.
|
||||||
|
If operation has "yes" in all 4 columns besides "Copies Table?", then it
|
||||||
|
*probably* can be allowed. If operation doesn't allow concurrent DML it means
|
||||||
|
that table row modifications or additions will be blocked during the migration.
|
||||||
|
This sometimes isn't a problem - for example it's not the end of the world if a
|
||||||
|
service won't be able to report it's status one or two times (and
|
||||||
|
:code:`services` table is normally small). Please note that even if this does
|
||||||
|
apply to "rename a column" operation, we cannot simply do such ALTER, as N-1
|
||||||
|
will depend on the older name.
|
||||||
|
|
||||||
|
If an operation on column or table cannot be allowed, then it is required to
|
||||||
|
create a new column with desired properties and start moving the data (in a
|
||||||
|
live manner). In worst case old column can be removed in N+2. Whole procedure
|
||||||
|
is described in more details below.
|
||||||
|
|
||||||
|
In aforementioned case we need to make more complicated steps streching through
|
||||||
|
3 releases - always keeping the backwards compatibility. In short when we want
|
||||||
|
to start to move data inside the DB, then in N we should:
|
||||||
|
|
||||||
|
* Add a new column for the data.
|
||||||
|
* Write data in both places (N-1 needs to read it).
|
||||||
|
* Read data from the old place (N-1 writes there).
|
||||||
|
* Prepare online data migration cinder-manage command to be run before
|
||||||
|
upgrading to N+1 (because N+1 will read from new place, so we need to make
|
||||||
|
sure all the records have new place populated).
|
||||||
|
|
||||||
|
In N+1 we should:
|
||||||
|
|
||||||
|
* Write data to both places (N reads from old one).
|
||||||
|
* Read data from the new place (N saves there).
|
||||||
|
|
||||||
|
In N+2
|
||||||
|
|
||||||
|
* Remove old place from SQLAlchemy.
|
||||||
|
* Read and write only to the new place.
|
||||||
|
* Remove the column as the post-upgrade migration (or as first migration in
|
||||||
|
N+3).
|
||||||
|
|
||||||
|
Please note that this is the most complicated case. If data in the column
|
||||||
|
cannot actually change (for example :code:`host` in :code:`services` table), in
|
||||||
|
N we can read from new place and fallback to the old place if data is missing.
|
||||||
|
This way we can skip one release from the process.
|
||||||
|
|
||||||
|
Of course real-world examples may be different. E.g. sometimes it may be
|
||||||
|
required to write some more compatibility code in the oslo.versionedobjects
|
||||||
|
layer to compensate for different versions of objects passed over RPC. This is
|
||||||
|
explained more in `RPC payload changes (oslo.versionedobjects)`_ section.
|
||||||
|
|
||||||
|
More details about that can be found in the `online-schema-upgrades spec
|
||||||
|
<http://specs.openstack.org/openstack/cinder-specs/specs/mitaka/online-schema-upgrades.html>`_.
|
||||||
|
|
||||||
|
|
||||||
|
RPC API changes
|
||||||
|
---------------
|
||||||
|
|
||||||
|
It can obviously break service communication if RPC interface changes. In
|
||||||
|
particular this applies to changes of the RPC method definitions. To avoid that
|
||||||
|
we assume N's RPC API compatibility with N-1 version (both ways -
|
||||||
|
:code:`rpcapi` module should be able to downgrade the message if needed and
|
||||||
|
:code:`manager` module should be able to tolerate receiving messages in older
|
||||||
|
version.
|
||||||
|
|
||||||
|
Below is an example RPC compatibility shim from Mitaka's
|
||||||
|
:code:`cinder.volume.manager`. This code allows us to tolerate older versions
|
||||||
|
of the messages::
|
||||||
|
|
||||||
|
def create_volume(self, context, volume_id, request_spec=None,
|
||||||
|
filter_properties=None, allow_reschedule=True,
|
||||||
|
volume=None):
|
||||||
|
|
||||||
|
"""Creates the volume."""
|
||||||
|
# FIXME(thangp): Remove this in v2.0 of RPC API.
|
||||||
|
if volume is None:
|
||||||
|
# For older clients, mimic the old behavior and look up the volume
|
||||||
|
# by its volume_id.
|
||||||
|
volume = objects.Volume.get_by_id(context, volume_id)
|
||||||
|
|
||||||
|
And here's a contrary shim in cinder.volume.rpcapi (RPC client) that downgrades
|
||||||
|
the message to make sure it will be understood by older instances of the
|
||||||
|
service::
|
||||||
|
|
||||||
|
def create_volume(self, ctxt, volume, host, request_spec,
|
||||||
|
filter_properties, allow_reschedule=True):
|
||||||
|
request_spec_p = jsonutils.to_primitive(request_spec)
|
||||||
|
msg_args = {'volume_id': volume.id, 'request_spec': request_spec_p,
|
||||||
|
'filter_properties': filter_properties,
|
||||||
|
'allow_reschedule': allow_reschedule}
|
||||||
|
if self.client.can_send_version('1.32'):
|
||||||
|
version = '1.32'
|
||||||
|
msg_args['volume'] = volume
|
||||||
|
else:
|
||||||
|
version = '1.24'
|
||||||
|
|
||||||
|
new_host = utils.extract_host(host)
|
||||||
|
cctxt = self.client.prepare(server=new_host, version=version)
|
||||||
|
request_spec_p = jsonutils.to_primitive(request_spec)
|
||||||
|
cctxt.cast(ctxt, 'create_volume', **msg_args)
|
||||||
|
|
||||||
|
As can be seen there's this magic :code:`self.client.can_send_version()` method
|
||||||
|
which detects if we're running in a version-heterogenious environment and need
|
||||||
|
to downgrade the message. Detection is based on dynamic RPC version pinning. In
|
||||||
|
general all the services (managers) report supported RPC API version. RPC API
|
||||||
|
client gets all the versions from the DB, chooses the lowest one and starts to
|
||||||
|
downgrade messages to it.
|
||||||
|
|
||||||
|
To limit impact on the DB the pinned version of certain RPC API is cached.
|
||||||
|
After all the services in the deployment are updated, operator should restart
|
||||||
|
all the services or send them a SIGHUP signal to force reload of version pins.
|
||||||
|
|
||||||
|
As we need to support only N RPC API in N+1 release, we should be able to drop
|
||||||
|
all the compatibility shims in N+1. To be technically correct when doing so we
|
||||||
|
should also bump the major RPC API version. We do not need to do that in every
|
||||||
|
release (it may happen that through the release nothing will change in RPC API
|
||||||
|
or cost of technical debt of compatibility code is lower than the cost of
|
||||||
|
complicated procedure of increasing major version of RPC APIs).
|
||||||
|
|
||||||
|
The process of increasing the major version is explained in details in `Nova's
|
||||||
|
documentation <https://wiki.openstack.org/wiki/RpcMajorVersionUpdates>`_.
|
||||||
|
Please note that in case of Cinder we're accessing the DB from all of the
|
||||||
|
services, so we should follow the more complicated "Mixed version environments"
|
||||||
|
process for every of our services.
|
||||||
|
|
||||||
|
In case of removing whole RPC method we need to leave it there in N's manager
|
||||||
|
and can remove it in N+1 (because N-1 will be talking with N). When adding a
|
||||||
|
new one we need to make sure that when the RPC client is pinned to a too low
|
||||||
|
version any attempt to send new message should fail (because client will not
|
||||||
|
know if manager receiving the message will understand it) or ensure the manager
|
||||||
|
will get updated before clients by stating the recommended order of upgrades
|
||||||
|
for that release.
|
||||||
|
|
||||||
|
RPC payload changes (oslo.versionedobjects)
|
||||||
|
------------------------------------------
|
||||||
|
|
||||||
|
`oslo.versionedobjects
|
||||||
|
<http://docs.openstack.org/developer/oslo.versionedobjects>`_ is a library that
|
||||||
|
helps us to maintain compatibility of the payload sent over RPC. As during the
|
||||||
|
process of upgrades it is possible that a newer version of the service will
|
||||||
|
send an object to an older one, it may happen that newer object is incompatible
|
||||||
|
with older service.
|
||||||
|
|
||||||
|
Version of an object should be bumped every time we make an incompatible change
|
||||||
|
inside it. Rule of thumb is that we should always do that, but well-thought
|
||||||
|
exceptions were also allowed in the past (for example releasing a NOT NULL
|
||||||
|
constraint).
|
||||||
|
|
||||||
|
Imagine that we (finally!) decide that :code:`request_spec` sent in
|
||||||
|
:code:`create_volume` RPC cast is duplicating data and we want to start to
|
||||||
|
remove redundant occurrences. When running in version-mixed environment older
|
||||||
|
services will still expect this redundant data. We need a way to somehow
|
||||||
|
downgrade the :code:`request_spec` before sending it over RPC. And this is were
|
||||||
|
o.vo come in handy. o.vo provide us the infrastructure to keep the changes in
|
||||||
|
object versioned and to be able to downgrade them to a particular version.
|
||||||
|
|
||||||
|
Let's take a step back - similarly to the RPC API situation we need a way to
|
||||||
|
tell if we need to send a backward-compatible version of the message. In this
|
||||||
|
case we need to know to what version to downgrade the object. We're using a
|
||||||
|
similar solution to the one used for RPC API for that. A problem here is that
|
||||||
|
we need a single identifier (that we will be reported to :code:`services` DB
|
||||||
|
table) to denote whole set of versions of all the objects. To do that we've
|
||||||
|
introduced a concept of :code:`CinderObjectVersionHistory` object, where we
|
||||||
|
keep sets of individual object versions aggregated into a single version
|
||||||
|
string. When making an incompatible change in a single object you need to bump
|
||||||
|
its version (we have a unit test enforcing that) *and* add a new version to
|
||||||
|
:code:`cinder.objects.base.CinderObjectVersionsHistory` (there's a unit test as
|
||||||
|
well). Example code doing that is below::
|
||||||
|
|
||||||
|
OBJ_VERSIONS.add('1.1', {'Service': '1.2', 'ServiceList': '1.1'})
|
||||||
|
|
||||||
|
This line adds a new 1.1 aggregated object version that is different from 1.0
|
||||||
|
by two objects - :code:`Service` in 1.2 and :code:`ServiceList` in 1.1. This
|
||||||
|
means that the commit which added this line bumped versions of these two
|
||||||
|
objects.
|
||||||
|
|
||||||
|
Now if we know that a service we're talking to is running 1.1 aggregated
|
||||||
|
version - we need to downgrade :code:`Service` and :code:`ServiceList` to 1.2
|
||||||
|
and 1.1 respectively before sending. Please note that of course other objects
|
||||||
|
are included in the 1.1 aggregated version, but you just need to specify what
|
||||||
|
changed (all the other versions of individual objects will be taken from the
|
||||||
|
last version - 1.0 in this case).
|
||||||
|
|
||||||
|
Getting back to :code:`request_spec` example. So let's assume we want to remove
|
||||||
|
:code:`volume_properties` from there (most of data in there is already
|
||||||
|
somewhere else inside the :code:`request_spec` object). We've made a change in
|
||||||
|
the object fields, we've bumped it's version (from 1.0 to 1.1), we've updated
|
||||||
|
hash in the :code:`cinder.tests.unit.test_objects` to synchronize it with the
|
||||||
|
current state of the object, making the unit test pass and we've added a new
|
||||||
|
aggregated object history version in :code:`cinder.objects.base`.
|
||||||
|
|
||||||
|
What else is required? We need to provide code that actually downgrades
|
||||||
|
RequestSpec object from 1.1 to 1.0 - to be used when sending the object to
|
||||||
|
older services. This is done by implementing :code:`obj_make_compatible` method
|
||||||
|
in the object::
|
||||||
|
|
||||||
|
from oslo_utils import versionutils
|
||||||
|
|
||||||
|
def obj_make_compatible(self, primitive, target_version):
|
||||||
|
super(RequestSpec, self).obj_make_compatible(primitive, target_version)
|
||||||
|
target_version = versionutils.convert_version_to_tuple(target_version)
|
||||||
|
if target_version < (1, 1) and not 'volume_properties' in primitive:
|
||||||
|
volume_properties = {}
|
||||||
|
# TODO: Aggregate all the required information from primitive.
|
||||||
|
primitive['volume_properties'] = volume_properties
|
||||||
|
|
||||||
|
Please note that primitive is a dictionary representation of the object and not
|
||||||
|
an object itself. This is because o.vo are of course sent over RPC as dicts.
|
||||||
|
|
||||||
|
With these pieces in place Cinder will take care of sending
|
||||||
|
:code:`request_spec` with :code:`volume_properties` when running in mixed
|
||||||
|
environment and without when all services are upgraded and will understand
|
||||||
|
:code:`request_spec` without :code:`volume_properties` element.
|
||||||
|
|
||||||
|
Note that o.vo layer is able to recursively downgrade all of its fields, so
|
||||||
|
when `request_spec` will be used as a field in other object, it will be
|
||||||
|
correctly downgraded.
|
Loading…
Reference in New Issue
Block a user