Component internal state
As we established before, one of the important features of software components which we wanted to include in the Aeolus model was their life-cycle. Most software components are not static from the system administration point of view, they have a dynamically changing internal state that a_ects their properties and behaviour. Motivation Intuitively in almost any service which we deploy in a distributed system we can distinguish at least two obvious internal states that we could call: not running and running. Of course this is a very simplified and high-level way of understanding the concept of an “internal state” of a piece of software. If we wanted to enter more deeply into modelling the details of functioning of any program, the representation of its internal state might get very complex. In the extreme case we can imagine that it could encompass everything down to the level of the full dump of the contents of its memory at any given moment of time, etc. Getting into so much detail is definitely not our aim here. In the Aeolus model we rather want to abstract all the low-level details and stick to the high-level view of how the services operate.
For us the important aspects of a software component’s internal state are those which are relevant from the distributed system administration point of view. Usually this means that we tend to care only about those internal state changes which really a_ect a given component’s properties in a way that directly concerns other components and thus may introduce connection in order to function properly and the database component is being shut down for maintenance reasons (which we can simply model as being switched to the not-running state) the aforementioned component will not work correctly any more and we will end up with a broken system. As we can see in this case, the order in which we switch on and o_ di_erent components can be important when we are reconfiguring our distributed system. Hence we would like to keep track of this kind of internal state information in the model. In practice the states that we include in the model resemble closely to the typical life-cycle of a real service: usually the set of possible states will contain at least elements like uninstalled, installed and running (or initial, configured, running). In some cases we will also add some more specific states in order to model more complex inter-component interactions requiring many stages or to allow more advanced types of service orchestration. Adding additional states is also natural if a certain component has many modes of functioning (e.g. running-as-master and running-as-slave). There is one more aspect of the problem that we should keep in mind: the evolution of the internal state of a given real service is generally not random, it follows some strict rules. For example many services cannot go directly from being uninstalled to being running, they have to pass through the intermediary installed state (where they are ready to work, but have not been launched yet). Including this kind of restrictions in the model is necessary if we really want to reproduce the real services’ behaviour.
State machine
After describing the background and explaining the reasons for key design decisions concerning the component internal state in Aeolus model, let us move on to the actual implementation of this feature. In the Aeolus model each component is fitted with a finite state machine. Every state of that machine represents a certain internal state of the corresponding real service operating in the distributed system. Although in most cases these are simply the “big” steps of the service’s life-cycle (e.g. uninstalled, installed, running, as depicted in figure 4.3), sometimes we may want to include also more subtle stages, like phases of a complex reconfiguration process (e.g. authenticated, synchronized) or multiple modes of functioning, as depicted in figures 4.4a and 4.4b. At every given moment each component is in a single precise internal state, known as its current state. The current state can evolve, following the transitions available in the state machine. Each transition on the model level corresponds to some actions happening in the real system and leading to the change of the internal state of the matching service. Mostly, what we call “actions” here refers to specific local administration tasks concerning the service in question. For example, passing from the uninstalled to installed state may be equivalent to installing an appropriate software package on a certain machine, and then passing from the installed to running state may simply correspond to launching the service (e.g. starting its daemon in the background). This kind of relation between the model-level state changes and the system-level actions is illustrated in figure 4.5. We do not prohibit at all some degree of communication with other services happening during these actions. For instance, when a web application is switching its state from installed to running it may contact the database and query some information, then the database will respond, etc. The action is meant to be local from the administrative point of view: on the model level it should have e_ect only in the scope of the component which changes his state2.
Meaning of port arities
Let us see now what our redundancy and capacity constraints on the model level are supposed to mean in the context of the corresponding real distributed system. Typical use The require arities are obviously useful to model all the one-requires-many relationships. We should note however, that these are not supposed to be functional requirements (i.e. one requires three to work correctly), but rather non-functional ones (i.e. one could work fine with one, but requires three because of high workload or fail safety reasons). So the require arities of our ports usually correspond not to real hard prerequisites, but more to our arbitrary deployment policy. Typical utilizations of this mechanism may encompass simply implementing fail-safe solutions based on redundancy (e.g. although the component X requires to be bound with two di_erent components, in reality it is using only one of them at any given time, but it will immediately switch to the other one if the current used one fails) and all variants of master-slave or frontend-backend patterns (e.g. the master component is dividing the work between multiple slaves, which permits it to handle bigger workloads e_ciently). Conversely the provide arities can be used to determine roughly what amount of workload a certain component should be able to support, given that we can estimate the amount of workload which each of the components using it will demand.
Deployment policy aspect The require and provide arities given to ports should not be automatically considered as inherent properties of their components, as they often belong more to the domain of the deployment policy of the whole system. Their exact values may depend strongly on the circumstances in which the components operate and usually only make sense in the context of a given distributed system. Di_erent systems based on similar components may be designed to satisfy di_erent non-functional requirements. For example, the components used to model a toy small-scale version of a certain system can be almost exactly the same as the ones used to model a serious large-scale one (providing that the considered distributed architecture scales up easily), but their require and provide arities will generally change in order to reflect the bigger expected workload and more strict fail safety requirements. In practice, a tool using the Aeolus model as an internal representation may well decide to maintain di_erent profiles for the arities associated to some components, and create an instance of the model using a given profile only when a particular analysis must be performed. Varying arities Another interesting feature of the require and provide arities as we defined them, is the fact that their values for a given port can vary from state to state. This gives quite a lot of flexibility and permits to model software components which can be configured in multiple di_erent ways and their non-functional (and functional possibly too) requirements depend on their mode of functioning.
Initial deployment
This is the first reconfiguration which every distributed system has to undergo, passing from an empty environment to a fully configured, working system providing certain functionalities. Usually we start with nothing (a number of clean machines or simply a public or private cloud) and we have some kind of a description of the final system that should be implemented. Then we install and set up components in the right order and we orchestrate them to work together in order to attain that final desired state. The main regularity particular to the initial deployment is that the general direction of the reconfiguration is intuitively simple and constant: up. We are building the system: we install components, configure them, make them running, establish connections between them, etc. Very rarely any destructive action or even change of existing configuration is required. We simply add all the elements of the puzzle in the right order until we attain the goal. The other specificity of this phase of the system’s life is that we usually do not expect the whole to be operative and functional until the very end of the deployment process. We can safely assume that during the entire time of the initial reconfiguration there are no clients, users or external applications actively utilizing the system and depending on it to work. We can imagine, that nobody from the outside will access it until we decide that the set-up is over and we give a green light (e.g. open the ports on the external firewall). Update After the system has been deployed, it has to be maintained.
This entails performing changes on an already working system. In some situations it means, that we are required to keep it actively running and providing services during the whole time of the reconfiguration. In others we are allowed to have some downtime, when the system is not expected to be fully operational. There are a few typical reasons of performing changes on a system that has been already set up. One of the most common reasons are software updates. Software, and especially highly popular open-source software, tends to evolve quickly: bugs are fixed, performances are improved, new features are introduced. In order to keep our system safe and up to date, we have to patch its elements regularly. Even if we are pretty conservative in this matter and prefer stability than supporting new features, we have to at least apply the most important security updates or we risk making it vulnerable to attacks. Other reason of reconfiguring a running system is to scale it up or down in order to adapt its capacities to the current or expected workload.
This is particularly habitual in case of systems deployed on clouds, as cloud-based solutions are often chosen specifically to be easily and frequently scalable. Both these cases have one thing in common: although preparing and performing them correctly may require careful planning and complete knowledge of all the component relations in the system (especially if we need to keep it running without an interruption), the introduced changes are typically quite restricted and they preserve the general structure of the whole deployment. Updates are usually limited to a single component at a time (unless they introduce some backward incompatibilities between versions) and scaling up and down entails adding or removing instances of components that already exist in the system. It is also possible sometimes that a major modification, which significantly restructures the system, needs to be performed. In these cases however keeping the system running all the time is rarely required (or possible), so it often happens that such substantial overhauls are performed in three phases: building a new system (separately from the existing one), migrating all the data to it and then tearing the old system down. In fact we should note, that in some environments this kind of procedure is also a common practice in case of much smaller updates, especially if we are not really sure if the updated system will work correctly and we have mastered a sure method of swiftly switching from the old system to the new one.
1 Résumé |