Re: Ann Arbor Team's Flexible I/O Proposals (Ceph Next)

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



On Fri, Apr 15, 2016 at 5:05 PM, Adam C. Emerson <aemerson@xxxxxxxxxx> wrote:
> Ceph Developers,
>
> We've put together a few of the main ideas from our previous work in a
> brief form that we hope people will be able to digest, consider, and
> debate. We'd also like to discuss them with you at Ceph Next this
> Tuesday.
>
> Thank you.
>
>
> ---8<---
>
>
> We have been looking at improvements to Ceph, particularly RADOS,
> while focusing on flexibility (allowing users to do more things)
> and performance. We have come up with a few proposals with these two
> things in mind. Sessions and read-write transactions aim to allow
> clients to batch up multiple operations in a way that is safe and
> correct, while allowing clients to gain the advantages of atomic
> read-write operations without having to lock. Sessions also provide
> a foundation for flow-control which ultimately improves performance
> by preventing an OSD from being ground into uselessness under a
> storm of impossible requests. The CLS proposal is a logical follow-on
> from the read-write proposal, as we attempt to address some problems
> of correctness that exist now and consider how to integrate the
> facility into an asynchronous world.
>
> Flexible Placement, as you would expect from the name, is about
> allowing users more control, as are Flexible Semantics. They both
> have profound performance implications, as tuning placement to better
> match a workload can increase throughput, and relaxed consistency can
> decrease latency. The proposed Interfaces are meant to support both as
> well as work currently being done to allow an asynchronous OSD and to
> hide details like locking and thread pools so that backends can be
> written with different forms of concurrency and load-balancing
> across processors.
>
> Finally, Map Partitioning is not directly related to code paths within
> the OSD itself, but does affect everything that can be done with Ceph.
> People are beginning to run into limits on how large a Ceph cluster can
> grow and how many ways they can be partitioned, and both these problems
> fundamentally derive from the way the OSD map is handled by the monitors.
>
> There are also some notes at the end. They are not critical, but if you
> find yourself asking "What were they thinking?" the notes might help.
>
> # Sessions and Read-Write #
>
> From `ReplicatedPG.cc`.
>
> ```c++
> // Write operations aren't allowed to return a data payload because
> // we can't do so reliably. If the client has to resend the request
> // and it has already been applied, we will return 0 with no
> // payload.  Non-deterministic behavior is no good.  However, it is
> // possible to construct an operation that does a read, does a guard
> // check (e.g., CMPXATTR), and then a write.  Then we either succeed
> // with the write, or return a CMPXATTR and the read value.
>
> if (ctx->op_t->empty() || result < 0) {
>
>   if (ctx->pending_async_reads.empty()) {
>     complete_read_ctx(result, ctx);
>   } else {
>     in_progress_async_reads.push_back(make_pair(op, ctx));
>     ctx->start_async_reads(this);
>   }
>   return;
> }
>
> // issue replica writes
> ceph_tid_t rep_tid = osd->get_tid();
>
> RepGather *repop = new_repop(ctx, obc, rep_tid);
>
> issue_repop(repop, ctx);
> eval_repop(repop);
> ```
>
> As you can see, if we have any writes (all mutations end up in the
> `op_t` transaction), we just flat out don't do the requested read
> operations. If we don't have any writes, we perform the read
> operations and return.  This is justified in the comment above because
> of the non-deterministic behavior of resent read-write operations.
>
> This is not an unsolved problem and we can bootstrap a solution on our
> existing `Session` infrastructure.
>
> ## An upgraded session ##
>
> Behold, `OSDSession`:
> ```c++
> struct Session : public RefCountedObject {
>   EntityName entity_name;
>   OSDCap caps;
>   int64_t auid;
>   ConnectionRef con;
>   WatchConState wstate;
>
> };
> ```
>
> This structure exists once for every connection to the OSD. Where they
> are created depends on who is doing the creation. In the case of
> clients (what we're interested in) it occurs in `ms_handle_authorizeri`
> ```c++
>
> isvalid = authorize_handler->verify_authorizer(cct, monc->rotating_secrets,
>                                                authorizer_data, authorizer_reply, name, global_id, caps_info, session_key, &auid);
>
> if (isvalid) {
>   Session *s = static_cast<Session *>(con->get_priv());
>   if (!s) {
>     s = new Session(cct);
>     con->set_priv(s->get());
>     s->con = con;
>     dout(10) << " new session " << s << " con=" << s->con << " addr=" << s->con->get_peer_addr() << dendl;
>   }
>
>   s->entity_name = name;
>   if (caps_info.allow_all)
>     s->caps.set_allow_all();
>   s->auid = auid;
>
> }
> ```
>
> In order to solve this problem, we propose a new data structure,
> modelled on NFSv4.1
> ```c++
> struct OpSlot {
>   uint64_t seq;
>   int r;
>   MOSDOpReplyRef cached; // Nullable
>   bool completed;
> };
> ```
>
> We do not want to give the OSD an unbounded obligation to hang on to
> old message replies: that way lies madness. So, the additions to
> `Session` we might make are:
>
> ```c++
> struct Session : public RefCountedObject {
>
>   uint32_t maxslots; // The maximum number of operations this client
>                      // may have in flight at once;
>   std::vector<OpSlot> slots // The vector of in-progress operations
>   ceph::timespan slots_expire; // How long we wait to hear from a
>                                // client before the OSD is free to
>                                // drop session resources
>   cepu::coarse_mono_time last_contact; // When (by our measure) we
>                                        // last received an operation
>                                        // from the client.
> };
> ```
>
> ## Message Additions ##
>
> The OSD needs to communicate this information to the client. The most
> useful way to do this is with an addition to `MOSDOpReply`.
>
> ```c++
> class MOSDOpReply : public Message {
>
>   uint32_t this_slot;
>   uint64_t this_seq;
>   uint32_t max_slot;
>   ceph::timespan timeout;
>
> };
> ```
>
> This overlaps with the function of the transaction ID, since the
> slot/sequence/OSD triple uniquely identifies an operation. Unlike the
> transaction ID, this provides consistent semantics and a measure of
> flow control.
>
> To match our reply, the `MOSDOp` would need to be amended.
> ```c++
> class MOSDOp : public Message {
>
>   uint32_t this_slot;
>   uint64_t this_seq;
>   bool please_cache;
>
> };
> ```
>
> ## Operations ##
>
> ### Connecting ###
>
> A client, upon connecting to an OSD for the first time should send a
> `this_slot` of 0 and a `this_seq` of 0. If it reconnects to an OSD it
> should use the `this_slot` and `this_seq` values from before it lost
> its connection. If an OSD has state for a client and receives a
> `(slot,seq) = (0,0)` then it should feel free to free any saved state
> and start anew.
>
> ### OSD Feedback ###
>
> In every `MOSDOpReply` the OSD should send `this_slot` and `this_seq` to
> the value from the `MOSDOp` to which we're replying.
>
> More usefully, the OSD can inform the client how many operations it is
> allowed to send concurrently with `max_slot`. The client must **not**
> send a slot value higher than `max_slot`. (The OSD should error if it
> does.)
>
> The OSD may increase the number of operations allowed in-flight
> if it has capacity by increasing `max_slot`. If it finds itself
> lacking capacity, it may decrease `max_slot`. If it does, the client
> should respect the new bound. (The OSD should feel free to free the
> rescinded slots as soon as the client sends another `MOSDOp` with a
> slot value equal to one on which the new `max_slot` has been sent.)
>
> If the client sends a `this_seq` lower than the one held for a slot by
> the OSD, the OSD should error. If it is more than one greater than the
> current `this_seq`, the OSD should error.
>
> ### Caching ###
>
> The client is in an excellent position to know whether it **requires**
> the output of a previous operation of mixed reads and writes on
> resend, or whether it merely needs the status on resend. Thus, we let
> the client set `please_cache` to request that the OSD store a
> reference to the sent message in the appropriate `OpSlot`.
>
> The OSD is in an excellent position to know how loaded it is. It can
> calculate a bound on how large a given reply will be before executing
> it. Thus, the OSD can send an error if the client has requested it
> cache something larger than it feels comfortable caching.
>
> Assuming no errors, the behavior, for any slot, is this: If the client
> sends an `MOSDOp` with a `this_seq` one greater than the current value
> of `OpSlot::seq`, that represents a new operation. Increment
> `OpSlot::seq`, clear `OpSlot::completed` and begin the operation. When
> the operation finishes, set `OpSlot::completed`. If `please_cache` has been
> set, store the `MOSDOpReply` in `OpSlot::cached`. Otherwise simply store the
> result code in `OpSlot::r`.
>
> If the client sends an `MOSDOp` with a `this_seq` equal to
> `OpSlot::seq` and `OpSlot::completed` is false, drop the request. (We
> will reply when it completes.) If it has completed, send the stored
> `OpSlot::MOSDOpReply` if there is one, otherwise send just a replay
> with just `OpSlot::r`.
>
> ### Reconnection ###
>
> Currently the `Session` is destroyed on reset and a new one is created
> on authorization. In our proposed system the `Session` will not be
> destroyed on reset, it will be moved to a structure where it can be
> looked up and destroyed after `timeout` since the last message
> received.
>
> On connection, the OSD should first look up a `Session` keyed
> on the entity name and create one if that fails.
>
> # Read as a part of Transaction #
>
> We don't have code examples here since most of the obvious interface
> changes are obvious. Codes and parameters would be added to
> `PGBackend::Transaction` and executing a transaction would have to
> return data.
>
> ## Motivation ##
>
> -   Mixed reads and writes are an efficiency win, since a client can
>     save round trips by batching up operations in a single request.
>     Current Ceph does not allow them for reasons which are quoted and
>     addressed in the preceding section.
> -   Mixed reads and writes are a semantic win. If an `MOSDOp` is
>     atomic (it is in current Ceph), read-after-write can often remove
>     the need for explicit locking.
> -   Transactional reads may seem complicated, but the Erasure Coding
>     backend already has to execute complex read transactions to
>     reassemble or recover data. We want an asynchronous read capability
>     in the Store anyway and there's no reason not to have it be shared
>     with our asynchronous write path.
> -   While it might seem that separating reads and writes, as we do
>     now, allows us to simplify code and rule out edge cases, we would
>     like to point out the existence of CLS, which can have problems if
>     two method calls occur in the same `MOSDOp`.
>
> ## Sketch ##
>
> The main problem with mixed read-write transactions is that replicas
> need to write but not read. The key to handling this is dependency
> checking. Outside CLS (which will be discussed below) it is very easy
> to see whether reads and writes are independent. (Simply go down the
> ops and see if their ranges overlap and whether getattrs and setattrs
> have keys in common.) Reads coming after overlapping writes depend on
> the previous writes. Then:
> -   If an op that's all reads, simply do all the reads. We don't have
>     to get write locks or anything.
> -   If an op is all writes, it's no different than a replicated
>     operation now.
> -   For mixed reads and writes, if the reads aren't dependent on the
>     writes, dispatch the writes and do the reads before, after, or
>     concurrently with the writes on the primary.  (So long as we
>     prevent writes from other transactions from intervening.)
> -   Dependent reads are the difficult case. For erasure coding it
>     shouldn't any difference since we'd have to dispatch reads and
>     writes to all stripes anyway. For replication, we would want to
>     execute the mixed read-write transaction on the local store in
>     strict order and dispatch one consisting of only writes to the
>     remotes.
>
> # CLS #
>
> ## Current Problem ##
>
> The CLS API works by making an ops vector and handing it to
> `do_osd_ops`.
>
> ```c++
> int cls_cxx_getxattr(cls_method_context_t hctx, const char *name,
>                      bufferlist *outbl)
> {
>   ReplicatedPG::OpContext **pctx = (ReplicatedPG::OpContext **)hctx;
>   bufferlist name_data;
>   vector<OSDOp> nops(1);
>   OSDOp& op = nops[0];
>   int r;
>
>   op.op.op = CEPH_OSD_OP_GETXATTR;
>   op.indata.append(name);
>   op.op.xattr.name_len = strlen(name);
>   r = (*pctx)->pg->do_osd_ops(*pctx, nops);
>   if (r < 0)
>     return r;
>
>   outbl->claim(op.outdata);
>   return outbl->length();
> }
>
> int cls_cxx_setxattr(cls_method_context_t hctx, const char *name,
>                      bufferlist *inbl)
> {
>   ReplicatedPG::OpContext **pctx = (ReplicatedPG::OpContext **)hctx;
>   bufferlist name_data;
>   vector<OSDOp> nops(1);
>   OSDOp& op = nops[0];
>   int r;
>
>   op.op.op = CEPH_OSD_OP_SETXATTR;
>   op.indata.append(name);
>   op.indata.append(*inbl);
>   op.op.xattr.name_len = strlen(name);
>   op.op.xattr.value_len = inbl->length();
>   r = (*pctx)->pg->do_osd_ops(*pctx, nops);
>
>   return r;
> }
> ```
>
> The `do_osd_ops` function performs reads inline, synchronously, right
> then and there for replicated pools. (Erasure coded pools are more
> limited.) Writes are batched up and added to the transaction
> associated with the current `OpContext`.
>
> This is bad. If one has a CLS method that performs a read-modify-write
> and one calls it twice in the same `MOSDOp`, it becomes a
> read-modify-read-modify-write-write which may produce incorrect
> results.
>
> ## Desiderata ##
>
> -   CLS operations should be composable. We should be able to have many
>     of them in a single operation.
> -   They should remain transactional. If a CLS operation does some
>     reads and hits an error, it stops and nothing is written to the
>     store. We should not allow situations where a CLS method can write
>     a partial result to the store then error.
> -   They should be capable. We should not put too many restrictions on
>     what an operation is allowed to do. It should be possible to run
>     them on Erasure Coded Pools once ECOverwrite is in place. (At
>     least some subset of them).
> -   They should be consistent. A CLS operation should be able to call
>     rand or generate a UUID without each replica holding a different
>     value. (This rules out solutions like calling the method on each
>     replica.)
> -   They should be efficient and optimizable.
> -   They should work in an asynchronous framework.
>
> There are several ways we could change their implementation to address
> these.
>
> ## Futures ##
>
> This is an attractive way to think about CLS. It allows things to
> proceed asynchronously and would solve the RMRMWW problem. One would
> simply make every I/O operation in the CLS API a call returning a
> future and write each method in continuation passing style. Executing
> the transaction in the primary OSD (on a replicated pool) would create
> a write-only that could then be sent to replicas. (Having the
> execution of a CLS method also compile a write-only transaction is a
> propery of any composable design.)
> -   Tracking dependencies before the operation is executed would be
>     problematic. There would be no way to know whether later reads
>     overlapped with previous writes before doing them. This could lead
>     to an unbounded obligation on the OSD to maintain state to
>     evaluate OSDOp, including potentially large writes, before
>     actually committing in order for CLS methods to remain
>     transactional.
>
> Futures are, on their own, insufficient to provide everything we need
> from CLS, largely because they are opaque to the OSD. They could be
> combined with…
>
> ## Pre-declaration ##
>
> We could remove some of the generality. A simple way of doing so would
> be to have methods declare, as a part of their signature, everything
> that they may ever read or write, with the expectation that methods
> will name the fewest resources required. This doesn't mean that every
> method will always write to and read from everything it mentions,
> merely that we have a known bound of the maximum it will ever use.
>
> This makes analysis easier for the OSD, and in the composition case,
> it could go in two passes. In the first, it would execute CLS calls
> and pre-stage results and in the second it would pass its compiled
> write transaction into the store.
>
> This is the most attractive solution, but depends on pre-declaration
> being done well on the resources used in pre-staging.
>
> One could make things easier by being even more restrictive and
> imposing ordering:
> 1.  The method declares in advance all read operations it might ever
>     perform.
> 2.  The method declares in advance all write operations it might ever
>     perform.
> 3.  The method examines the parameters passed by the client and
>     indicates which subset of the named inputs and outputs it will
>     use.
> 4.  The method performs its read operations and denotes exactly which
>     output operations it will perform. (not the data to be output, but
>     ranges and names.)
> 5.  The method performs write operations.
>
> The most restrictive form of this would operate in two phases. First,
> the CLS method would be presented with its parameters and all of the
> things it plans to read or write (objects with ranges and attribute
> keys.) In the second it would be called with the contents of all the
> reads it requested and supply the data for all the writes it requested.
>
> This would obviate the need for futures or other asynchronous I/O,
> and make evaluation very easy. This approach would disallow some
> operations, like indirecting through an attribute key to read another,
> but is very appealing.
>
> ## Be Transactional ##
>
> Our transactions are pre-checked and must succeed. If we want the most
> expressive version of CLS consistent with our other goals, then we
> should add commit and rollback. EC Overwrite will already require some
> form of commit and rollback, so it's not beyond the realm of thought.
>
> It could also be a foundation for some future multi-object-transaction
> supporting backend.
>
> This idea might have appeal on its own, but the concerns of CLS are
> not sufficient to motivate it.
>
> ## Domain Specific Language ##
>
> One could make a domain specific language, based on something simple,
> that the OSD can execute to perform CLS methods. The OSD could then
> analyze each method to see what I/O operations a method calls and try
> to track them
> -   Dependency tracking for compilers is a major area of research. It
>     would be a whole lot of fun, but as a short term solution it is
>     not really practical.
> -   We still wouldn't be able to rule out problems in the general
>     case.
>
> This approach would be interesting as a long term academic research
> project, but is not suitable for a short-range improvement.
>
> # Flexible Placement #
>
> This is a large topic which should be discussed on its own, but it
> motivates the interface designs below, so we shall briefly mention why
> it's interesting.
>
> CRUSH/PG is a fine placement system for several workloads, but it has
> two well-known limitations.
>
> ## Motivation ##
>
> -   Data distribution can be much less uniform than one might like,
>     giving uneven use of disks. This has caused some Ceph developers
>     to experiment with Monte Carlo based placement algorithms.
> -   Data distribution can be much more uniform than one would
>     like. This is the fundamental cause of Ceph's slow sequential read
>     performance. More generally, unrelated workloads contend
>     with each other due to a lack of affinity for related data. The effects are
>     especially pronounced on spinning disk (due to seek times), but
>     still exist on Flash (due to bus/network contention.)  This is a
>     tension between competing goods. CRUSH gains wide dispersion and
>     uniformity to defend against correlated failures but this imposes
>     a tradeoff.
>
> ## Goal ##
>
> Ceph should support placement methods other than CRUSH/PG. Currently,
> the OSD dispatches operations based on placement group ID, which will
> need to be varied,
>
> We also need some way to get new types of functions into the cluster.
>
> ## Proposal ##
>
> Our proposal is, in a way, CRUSH taken to its logical
> conclusion. Instead of distributing CRUSH rules, we propose to
> distribute general computable functions from (oid, volume/dataset) pairs to
> sequences of OSDs with their supporting data structures.  One of our
> ongoing research projects has been an in-process executor for these
> functions based on Google's NaCl. The benefits are:
> -   Administrators can fine-tune placement functions to fit their
>     workloads well.
> -   They can also experiment easily without having to recompile all of
>     Ceph and make heavy architectural changes.
> -   Entirely new placement strategies can be deployed without having
>     to upgrade every machine in the cluster. Or any machine in the
>     cluster, once they've been upgraded to a Flexible Placement
>     capable version.
> -   Possibilities for annealing and machine learning to gradually
>     adapt placement in response to load data become available
> -   NaCl builds on LLVM which has a rich set of tools for optimizations
>     like partial evaluation.
> -   NaCl is fast.
>
> # Flexible Semantics #
>
> Another motivating example. Originally, Ceph did replication and only
> replication under a very specific consistency model. There has been
> desire for more flexibility.
> -   Erasure Coding. it still follows the Ceph consistency model
>     (though leaves out many operations) but is very different in
>     back-end dispatch, enough so that it inspired a major rewrite of
>     the OSD's bottom half.
> -   Append-only immutable objects have been discussed.
> -   Many people have asked for relaxed consistency to improve
>     performance. This is not be suitable for all workloads, but people
>     have repeatedly asked for the ability to set up low-latency,
>     relaxed-consistency volumes that still provide Ceph's ability to
>     easily use new storage and scale well.
> -   Transactional storage. As mentioned above, cross-object
>     transactional semantics are a thing people may have desired.
>
> # Interfaces #
>
> Right now our class hierarchy is a bit of a mess. Eventually we'll do
> something about `PG` and `ReplicatedPG`, refactor, support
> asynchronous I/O, reduce lock contention, support in core affinity,
> and build Jerusalem here in England's green and pleasant land.
>
> While we're stringing up our bows of burning gold, we should support
> non-PG based placement and flexible semantics. Right now, parts of the
> PG and the OSD (since the OSD manages the collection of PGs, spins
> them up, and manages thread pools shared by sets of PGs) are
> intertwined. Thus, we need to abstract out both pieces.
>
> As we also want to support having multiple "logical" OSDs running in a
> single `ceph-osd` process, this would be a natural time to add that
> capability.
>
> Both these are sketches and should be considered a work in progress.
>
> ## `DataSetInterface` ##
>
> Here is a sketch of what a flexible abstraction based on PG could look
> like, at least parts of one. Not being informed about Scrub,
> Recovery, or Cache Tiering, having only focused on the object
> operation path, we won't include those details here.
>
> We also leave out functions called from the PG itself or other objects
> invoked from ownstack.
>
> ```c++
> class DataSetInterface {
> protected:
>   LogicalOSD& losd; // LogicalOSD is a means to have different
>                     // stores/semantics run in the same process.
>
>   MapPartRef curmap; // Subset of map relevant to this DSI
> public:
>   // The OSD (things Up the Stack, generally) should not call 'lock'
>   // on us. If we have locking of some sort things down the stack that
>   // we have some relationship with (friend or whatever) could lock or
>   // unlock us, but that should not be baked in as part of the interface.
>
>   // Things like the info struct and details about loading the Place
>   // wouldn't actually be here. As there is an intimate relation
>   // between the LogicalOSD and an implementation of DataSetInterface (it
>   // holds all those loaded in memory and controls dispatch), they
>   // would not need to be part of the generic interface.
>
>   const coll_t coll; // The subdivision of the Store we control
>
>   // In the PG case we always know we're the primary or not for
>   // anything within the same pgid. That is not expected to be the
>   // case generally.
>   bool is_primary(const OpRequest&) = 0;
>   // No 'is_replica' since 'replica' may not be applicable
>   // generally. It's a bit off even in the erasure coded case.
>   bool is_acting(const OpRequest&) = 0;
>   bool is_inactive() = 0;
>
>  public:
>   // No identifier. The descendent will take that.
>   DataSetInterface(LogicalOSD& o, OSDMapRef curmap);
>   virtual ~DataSetInterface();
>
>   DataSetInterface(const DataSetInterface&) = delete;
>   DataSetInterface& operator =(const DataSetInterface&) = delete;
>   DataSetInterface(DataSetInterface&&) = delete;
>   DataSetInterface& operator =(DataSetInterface&&) = delete;
>
>   virtual void on_removal(ObjectStore::Transaction *t) = 0;
>
>   // Yes, there's no 'queue' and no 'do_op' or any of
>   // that. This is intentional. There's no dequeue or do_op because
>   // those functions are either called only by the PG currently OR
>   // they're called in OSD functions called by the PG as part of the
>   // thread switch. They should not be part of the public interface.
>
>   // There's no queue because we can either put queue here or we can
>   // put queue in LogicalOSD. (We could do both, but that seems bad to
>   // me.) If there is some combination of locking and checking that
>   // must be done before queueing an operation, it seems that it's
>   // better to do it in LogicalOSD so that it doesn't leak out and
>   // become part of the abstraction for other implementations.
> };
> ```
>
> ## `LogicalOSD` ##
>
> The OSD class itself (representing the single OSD process) should have
> a map (*perhaps* a Boost.Intrusive.Set?) mapping OSD IDs to to
> `LogicalOSD` instances.
>
> ```c++
> class LogicalOSD {
>   OSD& osd;
>   ObjectStore& store;
>
>   // Look up the DataSetInterface instance appropriate to the given
>   // OpRequest.
>   virtual future<DataSetInterface,int> get_place_for(const OpRequest&) = 0;
>
>   // Every logical OSD will have its own watchers as well as slot
>   // cache. Someone familiar with flow control should check this
>   // idea. Since LogicalOSDs will, ideally, share messengers we might
>   // want them to share the same slot cache. In that case we should
>   // just re-dimension watchers within Session
>   SessionRef session_for(const entity_name_t& name);
>
>   void queue(DataSetInterfaceRef&& pi, OpRequestRef&& to_queue);
>   void queue_front(DataSetInterfaceRef&& pi, OpRequestRef&& to_queue);
>
>   // Dequeue and the like are currently called in the PG itself and so
>   // have no place in the interface presented to the OSD.
>
>   void pause();
>   void resume();
>   void drain();
> };
> ```
>
> ## Library ##
>
> Both these interfaces are quite thin and intentionally so. Scrubbing
> and recovery have not been addressed at all, as mentioned, so those
> parts will be expanded.  Asynchrony should allow us simpler interfaces
> since some complexity of requeing will be handled by futures and
> continuations.
>
> We obviously do not want to rewrite all our existing code. Instead
> most of the existing work on `PG` and `ReplicatedPG` should be
> refactored into a templated library from which implementations of
> `LogicalOSD` and `DataSetInterface` can be constructed.
>
> # Map Partitioning #
>
> There are two huge problems with scalability in Ceph.
> 1.  The OSDMap knows too many things
> 2.  A single monitor manages all updates of everything and replicates them to
>     other monitors.
>
> ## Too Big to Not Fail ##
>
> The monitor map and MDS maps are fine. Each holds data needed to
> locate servers and that's it. It would be very hard to put enough data
> in them to cause problems. The OSD map however contains a trove of data that
> must be updated serially in Paxos and propagated to every OSD,
> monitor, MDS, and client in the cluster.
>
> Pools are a notorious example. We can't create as many pools as users
> would like. Pools are heavyweight, and while they depend on other
> items in the OSD map (like erasure code profiles), it would be nice if
> we divide them between several monitor clusters, each of which would
> hold a subset of pools. We would need to make sure that clients had up
> to date versions of whatever pools they are using along with the
> status of the OSDs they're speaking to, but that's not
> impossible. Likewise, we should split placement rules out of the OSD
> map, especially once we get into larger numbers of potentially larger
> Flexible Placement style functions.
>
> Nodes should then only need to subscribe to the set of pools and
> placement functions they need to access their data. Changes like these
> should allow users to create the number of pools they want without
> causing the cluster difficulty.
>
> ### Consistency ###
>
> Partitioning makes consistency harder. A simple remedy might be to
> stop referring to data by name or integer. An erasure code profile
> should be specified by UUID and version. So should pools and placement
> functions. When sending a request to the OSD, a client should send the
> versions of the pool, the ruleset, and the OSDMap it used and the OSD
> should check that all three are current.
>
> ## The OSD Set ##
>
> The complicating case here is the OSD status set.  Running this
> through a single Paxos limits the number of OSDs that can coexist in a
> cluster.  We ought split the set of OSDs between multiple masters to
> distribute the load. Each 'Up' or 'Down' event is independent of
> others, so all we require is that events get propagated into the
> correct OSDs and primaries and followers act as they're supposed to.
>
> Versioning is a bigger problem here. We might have all masters
> increment their version when one increments its version if that could
> be managed without inefficiency. We might send a compound version with
> `MOSDOp`s, but combining that with the compound version above might be
> unwieldly. (Feedback on this issue would be greatly appreciated.)
>
> ### Subscription ###
>
> For a large number of OSDs, it would be nice if not everyone were
> notified of all state changes.
>
> For a pool whose placement rule spans only a subset of all OSDs,
> clients using that pool should be able to subscribe to a subset of the
> OSD set corresponding to that pool. This should be fairly easy so long
> as the subset is explicit.
>
> In the case of pools not providing an explicit subset, a monitor (or
> perhaps a proxy in front of a set of monitors) could look at common
> patterns of subscription requests and merge those with significant
> overlap together, so as to give clients a subset without being
> destroyed by the irresistible force of combinatorial explosion.
>
> # Notes #
>
> These are notes taken when reviewing the code and thinking out
> ideas. You don't have to read them, but they are provided as a
> supplement in case you wanted to know what we were thinking and why.
>
> ## ShardedOpWQ ##
>
> -   What is the purpose of `sdata_op_ordering_lock`? A shard is not a
>     PG, so why do things need to be ordered within shards as well as
>     within PGs?
> -   `sdata_lock` pairs up with the condition variable
>
> ## OSD Upper Half ##
>
> ### Regular Dispatch ###
>
> -   Does not overlap with `fast_dispatch`. Operations in
>     `ms_can_fast_dispatch` are not handled in `_dispatch` and vice versa.
> -   Lock the entire OSD
> -   If another dispatch is executing, go to sleep and wait for it to
>     finish. What the heck?
> -   Do Waiters
>     * Waiters are a list of `OpRequestRef`s called `finished` for some
>           reason
>     * Whenever we activate an `OSDMap` the requests waiting for the
>       map get put onto 'finished'
> -   Call `_dispatch`
> -   Do some more waiters
> -   Wake up other dispatch threads
> -   Unlock the entire OSD
>
> #### `_dispatch`? ####
>
> A giant case statement that does a bunch of things.
>
> In the case of `OSDOp`, if we have an `OSDMap`, create an `OpRequest` and
> pass it to `dispatch_op`. This is for things like PG commands, not
> actual object operations.
>
> #### `dispatch_op` ####
>
> Another giant case statement.
>
> ### Fast Dispatch ###
>
> #### `ms_fast_preprocess` ####
>
> Update the map epoch if an OSD sends us an OSDMap.
>
> #### `ms_fast_dispatch` ####
>
> -   Make an `OpRequest`
> -   A bit weird and convoluted, it looks like we use the 'op waiting
>     for map' stuff to queue up an op on a reserved map and remove the
>     reservation preventing it from running before we return.
> -   Specifically we mark the op as waiting for its PG in the `Session`
>     and then mark the `Session` as waiting for the new map.
> -   Ultimately things end up in `dispatch_op_fast`
>
> #### `dispatch_op_fast` ####
>
> Shovels operations into type specific calls like…
>
> #### `handle_op` ####
>
> -   Set up map share (if needed)
> -   Calculate the True PGID and Pool (sanity check against the client?)
> -   Either get the pointer to the PG (a base class) or, if it hasn't
>     been loaded in, queue the session to wait for it
> -   If we have the PG, `enqueue_op` (which just calls `PG::queue_op`)
>
> ## OSD Lower Half (Currently PG) ##
>
> `ReplicatedPG` and `PG` are separate for historical reasons and actual
> differentiation occurs in choice of backend according to Word of Sam.
>
> PGs with different consistency properties are explicitly a goal
> now. The idea of a `PGInterface` has been floated to facilitate their
> creation and `ReplicatedPG` would become a child of that.
>
> ### `PG::queue_op` ###
>
> -   Delay if other people are waiting for maps (to preserve the PG Ordering)
> -   Enqueue in `op_wq` (owned by the OSD)
> -   (Why call into the PG then? Just to enforce the map ordering?)
> -   The work queue gathers operations which, during `_process` are later
>     reassembled into a list of work to be done.
> -   `_process` is called by a worker thread in the thread pool, so the
>     call to `dequeue_op` is in worker thread. Since it's sharded, we
>     get multiple groups of threads each serving some subset of PGs.
>
> ### `OSD::dequeue_op` ###
>
> -   After a bit of fiddling about sharing maps, call `PG::do_op`
>
> ### `PG::do_op` ###
>
> -   Sam says he plans to rewrite this to allow read asynchrony
> -   We want to see reads and writes share the same transaction
>     structure and similar semantics.
> -   We also want to allow reads and writes in the same operation and
>     to use a session mechanism to allow that.
> -   We'll need transaction transforms to, for example, filter out
>     reads before sending an operation to a replicating OSD. This
>     shouldn't be too hard, since the output of read operations can't
>     be the input for write operations. (Except in CLS?)
> -   `do_op` is a virtual function, but the only implementation is in
>     ReplicatedPG.
> -   Here looks to be where we apply ordering to Writes
> -   `execute_ctx` actually performs the operations after `do_op` has set
>     everything up
>
> ### `execute_ctx` ###
>
> -   May be called multiple times on the same `OpContext`
> -   In the case of clone operations (that's the only thing that takes
>     `src_obc`?), get a read-lock for the object context
> -   it's called `ondisk` but I'm not sure why, it doesn't look like they get serialized
> -   Then we have a brief detour into `prepare_transatcion`
> -   Here's the read-write restriction. ReplicatedPG.cc:2975. Later we can
>     create a better session abstraction to fix that.
> -   For reads
>     *   `do_osd_op_effects`!
>     *   If all our reads were synchronous (or there were none)
>         `complete_read_ctx`, which creates and sends the reply
>     -   Otherwise, `start_async_reads`, which passes the pending reads off
>         to `objects_read_async`
>     -   Once the backend completes, we go to `finish_read`, which calls
>         `complete_read_ctx`
> -   Trim the PG Log
> -   Hey, cool, there's a lambda! Register an `onack` closure that sends a reply
> -   And `oncommit`. And `onsuccess`. And `finish`.
> -   Package up the `OpContext` and its transaction and whatnot into a
>     `RepOp`. This is where all the mutations get done.
> -   Call `issue_repop`
> -   Call `eval_repop`
> -   Adam really wishes we would use `boost::intrusive_ptr` everywhere
>     and stop using explicit gets and puts.
>
> ### `prepare_transaction` ###
>
> -   `do_osd_ops`!
> -   If we're not full, `finish_ctx`.
>
> ### `do_osd_ops` ###
>
> -   Loop over the ops in a gigantic case statement
> -   If we hit any modification ops set the `user_modify` flag. This is
>     used to update the object version as part of the transaction
> -   On EC pools, do reads asynchronously, pushing them onto a list of
>     reads to complete.
> -   Otherwise do the reads synchronously
> -   CLS calls can be tricky since they read or write depending on the
>     method invoked
> -   It looks like operations performed by CLS are done by calling each
>     operation individually with `do_osd_ops` with reads being done
>     immediately and writes being queued up as part of the transaction
> -   Making the CLS API futures-based interface may be a good thing to do.
> -   Cache ops like flushing seem to be about shovelling triggers to do
>     perform actions into the `onack`/`oncomplete` lists.
> -   For write operations, stuff them into the Transaction
> -   In the case of CLS operations which do both reads and writes
>     (which some of them do), it appears that putting two CLS operations
>     in the same OSDOp might lead to weird results since all the reads
>     will happen then all the writes.
>
> ### `finish_ctx` ###
>
> -   Fiddle with object state and logs to update snapshot foo and to make
>     sure the object exists in the form we need it
> -   Update user version if we modified the object
> -   Save the updated `object_info_t`
> -   Append the updated object info to the `PGLog`
> -   Apply context stats
>
> ### `do_osd_op_effects`
>
> -   Add watches if we need to add watches
> -   If there's notifies, notify the watchers
> -   Why do we ack notifies?
>
> ### `issue_repop` ###
>
> -   Acquire locks (I'm still not clear why they're called `ondisk`. Is
>     it a lock acquired to use the store and thus it locks the on-disk
>     representation?)
> -   Apply built up attributes (likely verions and things that had been
>     stuck in the PGLog before.)
> -   Submit transaction to the PG Backend. Which is where it gets
>     divided up for Erasure Coding or sent out for replication. I'll
>     count that as Bottom End for the moment alongside the Store,
>     Changes to the backend will be for new consistency models.
>
>     We might be able to get a separation of concerns by varying what
>     is now ReplicatedPG to support differnet 'gridding' of objects on
>     the OSD and rejigger things so the consistency model is purely a
>     property of the backend. That's appealing from a maintenance
>     perspective, but breaks down if we want things like explicitly
>     marked transactions across multiple for some volumes while not
>     paying for them on others. It might not be workable in the general
>     case.
> -   That's also where local application takes place.
>
> ### `eval_repop` ###
>
> -   This function just sends notifications and cleans up when we finish.
> -   Its name is not very appropriate for what it does.
> -   If we're already done, return.
>     *   This isn't bad, but it's specifically necessary because `eval_repop`
>         gets called from several places including the handlers for our
>         subservient OSDs completing an operation.
> -   If everyone's ack'd, fire off our ack handlers. If everyone's
>     completed, fire off our completion handlers.
> -   Notify anyone waiting for the version we've committed…
> -   And for those waiting on the one we've applied
> -   If we've done everything, update usage stats
>     *   Fire off `on_success` callbacks
>     *   Remove ourselves
>
> ## Flex Points ##
>
> ### PlacementGroup/FlexiblePlacement/OtherConsistencyStrategy ###
>
> -   Fast Dispatch currently shoves requests into a PG.
> -   `handle_op` calculates a pgid and actually gets the pointer to or
>     queues the session to wait on the associated PG
> -   If we implement `queue_op` in FlexiblePlacement we can do whatever we
>     want with it. We can ignore the WorkQueue.
> -   Much of the code in `ReplicatedPG` is useful even with other
>     semantic models than PG-ordered replication
> -   We might want to make `ReplicatedPG` a template and
>     supply the `PG` specific parts as a class instantiation. Then we
>     could create more classes for other partition/dispatch models.
> -   We will want a consitency/semantic variation orthogonal to the
>     partition/dispatch model.
>         * In this divide dividing objects into PGs where every all
>           operations are dispatched into the PG for whatever objet they
>       effect would be partition/gridding
>     * Whereas the total ordering on PG operations and constraints on
>           when a request blocks versus being served are the
>           consistency/semantics
>
> ### Allocation/Locking/Dispatch ###
>
> -   `OpRequest` (currently allocated in `do_op` and other structures
>     might be allocated at various points. IN our earlier prototype we
>     allocated OpRequest and another structure alongside the MOSDOp and
>     reused MOSDOps rather than deallocating them to cut down on
>     allocator use in the fast path.
>
>         That might fight with also promising designs using core-affine
>     memory management, unless we can determine core affinity quickly
>     before allocating the message. (Maybe peeking into the undecoded
>     bytes?)
> -   Lock freedom should be orthogonal to flexible placement. There may
>     be situations where we want lockful systems in flexible placement
>     (since flexible placement can have a variety of sync behaviors.)
>     and we know that Sam and others are interested in pursuing
>     lock-free designs in in PG-placement.
> -   In a lock-free design, if PGs are core-affine,
>     `enqueue_op` could just submit a message to a core without locking
>     or some of the thread/worker complexity.
> -   For Volumes, where the volume itself may be partitioned across cores
>     `enqueue_op` would have to look at the object name to find its target.
> -   Thus, we would want to pull that logic into a separate function
>     giving our dispatch target.
>
> ### Read-Write Symmetry ###
>
> -   Thankfully, `init_op_flags` is happy to set both read and write
> -   CLS in particular falls afoul of this. Futures might be the best
>     way to deal with it.
>
> ### Things we know we had to do anyway from previous work ###
>
> -   Use `std::map` less as a parameter/return type, same for std::set
> -   Objecter improvements
>     *   Less allocation, change data structures. A dual to some of the
>         work we want to do to make the EC interface less memory
>         intensive.
>     -   If we have zero copy there should be a way to materialize that
>         at the level of the client.
> -   See about bootstrapping client-side EC from EC overwrite
> -   Librados4 should be more like Objecter than it is like librados3
>
> ## Sam and `do_op` (♪ Doo-Wop? ♪) ##
>
> ### Discussion ###
>
> Notes taken during a BlueJeans call between Adam Emerson and Sam
> Just. (Sorry for any mistakes, recording a conversation while having
> it is tricky.)
>
> -   We should never have to block for I/O
> -   It's not `do_op` per se, though we are rewriting that to put it into a
>     continuation passing style with trampolines
> -   Various bits should be allowed to block, but whether they do or
>     don't should not effect the caller's code-flow.
> -   Once we've got to that point, everything after is easier
> -   We have to make sure we don't introduce so much overhead that it's measurable
> -   Eventually plans to go to a lock-free/sharded/partitioned style like Seastar
> -   We are not using Seastar's system because, when you fulfil a
>     promise you don't want to have the promise fulfilled in that
>     thread, it should be easy to fulfill it in a different thread.
> -   Also adapting an existing codebase to Seastar is much harder than
>     writing one from scratch to use it.
> -   It should also allow us to run all the OSDs in the same process
> -   We might want to have one messenger per logical OSD and have those share
>     threads (loses some efficiency gains but is backwards compatible.)
> -   These sorts of changes will also make EC overwrites much easier.
> -   Any refactors in the code should move us in this direction as a side effect
> -   The sooner the better, so if it does cause performance problems we
>     can find out soon
> -   Branch is wip-do-op in athanatos
>
> ### Brief Exploration of the code ###
>
> Adam Emerson looked briefly through the `wip-do-op` branch in
> `https://github.com/athanatos/ceph.git` to see what the general design
> looked like and how it matched up with our goals.
>
> -   Getting rid of the 'ondisk lock' looks good, someone good at
>     scheduling (Matt?) should review the queue. It should not use
>     `std::list`, though.
> -   The `do_replica_safe_reads` refactor isn't bad but doesn't seem to
>     have an immediate effect. Sam described it as providing safety
>     shunting things replicas could do into their own function, so
>     should make future development and refactor easier.
> -   It reinforces the idea that reads inhabit a separate magisterium
>     with its own law and dispensation from writes and is the oposite
>     direction from the read/write transactions we want. At least
>     potentially, we could use it as a fast/safe path and have it do a
>     more specialized transaction dispatch for reads, maybe.
> -   The `do_op`/`do_replica_op` split seems reasonable for the
>     replicated case, since in that one we want to transform the
>     transaction before sending it to the replicas. If we want to allow
>     CLS methods on EC pools (which we do, in principle) or mixed
>     read-write, then the distinction between primary and replica might
>     break down.
> -   Not sure if the error channel is better pe se, but since we
>     currently have a bunch of functions that return `int` to indicate
>     errors, it might be easier to integrate.
> -   C++ should have a `void` type a bit more like unit so you could
>     explicitly return `void()` from void functions. You'd think they
>     could put *that* in C++17 since their list of things to add to the
>     standard now consists entirely of "3 to the version number".
> -   The `future` implementation looks promising. I'll need to review
>     how it's put together in more detail later, how it's used is more
>     pertinent at the moment.
> -   Things make sense from a gradualist position. Given the desire for
>     a progression from from here to _A Really Fast OSD_ where we have
>     _A Working OSD_ at every point along the way, this approach makes
>     sense. Restructuring everything around a blocking-agnostic futures
>     design then opens the way to introducing asynchronous, lock-free code.
> -   This is also compatible with flexing, since we can have multiple
>     `LogicalOSD` implementations with different locking strategies or
>     core affinity.
> -   `aio_read` looks to be less aio than the name would suggest. This
>     isn't bad, it's reasonable to do a transform by having things call
>     blocking procedures in a way that will work if they become non-blocking.
> -   Reimplementing the blocking calls in terms of nonblocking calls is
>     smart.
> -   `OSDReactor` looks like it could be adapted, at least the public
>     interface, into LogicalOSD once we made it less PG specific.
> -   In principle it's a good idea. A LogicalOSD would have to be bound
>     closely to the DataSetInterface it worked with since they're two
>     halves of a queueing mechanism.
> -   The futures stuff definitely isn't naïve. We need to understand
>     the blockers and other details.  The idea of having a future yield
>     when it needs to wait for something is a good one.
> -   It uses `std::list` though.
>
> ## Why librados is not wonderful ##
>
> Not that we hate RADOS, we just like Objecter way more
> -   Does not support read and write in same op. Neither does RADOS, to
>     be fair, but we plan to fix that.
> -   Takes a giant lock with every operation. Yuck.
> -   Has its own 'callback' interface
> -   Its handing of asynchronous operations seems very heavyweight and
>     not natural.
> -   Hides the internal structure of RADOS operations
> -   Does not expose object locator in a useful way
> -   Does way too many allocations
> -   The dimensioning of the interface is weird, like binding the IoCtx
>     to a pool

As librados is today, it encapsulates it's own networking and event
code. This is pretty frustrating to apps that already have their own
eventloop that want to integrate it.

> --
> To unsubscribe from this list: send the line "unsubscribe ceph-devel" in
> the body of a message to majordomo@xxxxxxxxxxxxxxx
> More majordomo info at  http://vger.kernel.org/majordomo-info.html



-- 
Milosz Tanski
CTO
16 East 34th Street, 15th floor
New York, NY 10016

p: 646-253-9055
e: milosz@xxxxxxxxx
--
To unsubscribe from this list: send the line "unsubscribe ceph-devel" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at  http://vger.kernel.org/majordomo-info.html




[Index of Archives]     [CEPH Users]     [Ceph Large]     [Information on CEPH]     [Linux BTRFS]     [Linux USB Devel]     [Video for Linux]     [Linux Audio Users]     [Yosemite News]     [Linux Kernel]     [Linux SCSI]
  Powered by Linux