The material is intended mainly for beginner web programmers.

Introduction.

I am often approached by clients who have self-written CMS installed or modules written by novice web programmers who do not understand what is needed to protect data and often copy filtering functions without thinking about how they work and what exactly needs to be done with them.

Here I will try to describe in as much detail as possible. common mistakes when filtering data in PHP script and give simple tips how to properly filter data.

There are a lot of articles on the net about filtering data, but they are, as it should be, not complete and without detailed examples.

Debriefing.

Filtration. Mistake #1
For numeric variables, the following check is used:
$number = $_GET["input_number"]; if (intval($number)) ( ... execute SQL query... )
Why does it lead to SQL injection? The point is that the user can specify in a variable input_number meaning:
1"+UNION+SELECT
In such cases, the check will be successfully passed, because the intval function gets the integer value of the variable, i.e. 1, but in the variable itself $number nothing has changed, so malicious code will be passed to the SQL query.
Correct filtering:
$number = intval($_GET["input_number"]); if ($number) ( ... execute SQL query... )
Of course, the condition can change, for example if you need to get only a certain range:
if ($number >= 32 AND $number<= 65)

If you are using checkboxes or multi-selects with numeric values, check this:
$checkbox_arr = array_map("intval", $_POST["checkbox"]);
array_map
I also meet filtering in the form:
$number = htmlspecialchars(intval($_GET["input_number"]));
htmlspecialchars
Or:
$number = mysql_escape_string(intval($_GET["input_number"]));
mysql_escape_string

Nothing but a smile can cause it :)

Filtration. Mistake #2.
For string variables, the following filtering is used:
$input_text = addslashes($_GET["input_text"]);
The addslashes function escapes the spec. characters, but it does not take into account the database encoding and it is possible to bypass the filtering. I will not copy the text of the author who described this vulnerability and will simply give a link to Chris Shiflett (you can search for the translation in Runet).

Use mysql_escape_string or mysql_real_escape_string function, example:
$input_text = mysql_escape_string($_GET["input_text"]);
If you do not intend to enter html tags, then it is best to do the following filtering:
$input_text = strip_tags($_GET["input_text"]); $input_text = htmlspecialchars($input_text); $input_text = mysql_escape_string($input_text);
strip_tags - strips html tags.
htmlspecialchars - converts special. characters in the html entity.
This is how you protect yourself from XSS attacks, in addition to SQL injection.
If you need html tags, but only for displaying the source code, then it is enough to use:
$input_text = htmlspecialchars($_GET["input_text"]); $input_text = mysql_escape_string($input_text);

If it is important for you that the value of the variable is not empty, then use the trim function, for example:
$input_text = trim($_GET["input_text"]); $input_text = htmlspecialchars($input_text); $input_text = mysql_escape_string($input_text);

Filtration. Mistake #3.
It's about searching the database.
To search by numbers, use the filtering described in the first error.
To search by text, use the filtering described in the second error, but with reservations.
In order to prevent the user from performing a logical error, you need to remove or escape the special. SQL characters.
Example without add. line processing:
$input_text = htmlspecialchars($_GET["input_text"]); // Search: "%" $input_text = mysql_escape_string($input_text);
As a result, we get a query like:
... WHERE text_row LIKE "%".$input_text."%" ... // WHERE text_row LIKE "%%%"
This will significantly increase the load on the base.
In my script, I use a function that removes characters I don't want from the search:
function strip_data($text) ( $quotes = array ("\x27", "\x22", "\x60", "\t", "\n", "\r", "*", "%", "<", ">", "?", "!"); $goodquotes = array ("-", "+", "#"); $repquotes = array ("\-", "\+", "\#"); $text = trim(strip_tags($text)); $text = str_replace($quotes, "", $text); $text = str_replace($goodquotes, $repquotes, $text); $text = ereg_replace(" +" , " ", $text); return $text; )
Of course, not all of the above symbols are dangerous, but in my case they are not needed, so I perform a search and replace.
An example of using filtering:
$input_text = strip_data($_GET["input_text"]); $input_text = htmlspecialchars($input_text); $input_text = mysql_escape_string($input_text);
I also advise you to make a limit on the number of characters in the search, at least not less than 3, because. if you have a large number of records in the database, then searching for 1-2 characters will significantly increase the load on the database.
Filtration. Mistake #4.
Variable values ​​are not filtered $_COOKIE. Some people think that since this variable cannot be passed through the form, then this is a security guarantee.
This variable is very easy to fake by any browser by editing the site's cookies.
For example, in one well-known CMS there was a check of the site template used:
if (@is_dir (MAIN_DIR . "/template/" . $_COOKIE["skin"]))( $config["skin"] = $_COOKIE["skin"]; ) $tpl->dir = MAIN_DIR . "/template/" . $config["skin"];
In this case, you can change the value of the variable $_COOKIE["skin"] and raise an error, as a result of which you will see the absolute path to the site folder.
If you use the value of cookies to save to the database, then use one of the above described filtrations, the same applies to the variable $_SERVER.
Filtration. Mistake #5.
Directive included register_globals. Be sure to turn it off if it's on.
In some situations, you can pass the value of a variable that should not have been passed, for example, if the site has groups, then for group 2 the $group variable should be empty or equal to 0, but it is enough to fake the form by adding the code:

Variable in PHP script $group will be equal to 5 if it was not declared with a default value in the script.
Filtration. Mistake #6.
Check downloaded files.
Check for the following:
  1. File extension. It is advisable to disable the loading of files with extensions: php, php3, php4, php5, etc.
  2. Is the file uploaded to the server move_uploaded_file
  3. file size
Examination. Mistake #1.
I came across cases when for an AJAX request (for example: increasing reputation) a username or ID was passed (to whom the reputation is increased), but PHP itself did not check for the existence of such a user.
For example:
$user_id = intval($_REQUEST["user_id"]); ... INSERT INTO REPLOG SET uid = "($user_id)", plus = "1" ... ... UPDATE Users SET reputation = reputation+1 WHERE user_id = "($user_id)" ...
It turns out we create a record in the database, which is completely useless to us.
Examination. Mistake #2.
When performing various actions (adding, editing, deleting) with data, do not forget to check the user's rights to access this function and additional features (html usage tags or the ability to publish material without verification).

For a long time I fixed a similar error in one forum module, when any user could edit the administration message.

Examination. Mistake #3.
When using multiple php files do a simple check.
In file index.php(or in any other main file) write this line before including other php files:
define("READFILE", true);
At the beginning of other php files write:
if (! defined ("READFILE")) ( exit ("Error, wrong way to file.
Go to main."); }
This will restrict access to files.
Examination. Mistake #4.
Use hashes for users. This will help prevent a particular function from being called by XSS.
An example of compiling a hash for users:
$secret_key = md5(strtolower("http://site.ru/" . $member["name"] . sha1($password) . date("Ymd"))); // $secret_key is our hash
Next, in all important forms, substitute the input with the value of the user's current hash:

During script execution, check:
if ($_POST["secret_key"] !== $secret_key) ( exit ("Error: secret_key!"); )
Examination. Mistake #5.
When outputting SQL errors, make a simple restriction on access to information. For example, set a password for GET variable:
if ($_GET["passsql"] == "password") ( ... SQL error output... ) else ( ... Just error information, no details... )
This will hide from the hacker information that can help him in hacking the site.
Examination. Mistake #5.
Try not to include files by getting file names from outside.
For example:
if (isset($_GET["file_name"])) ( include $_GET["file_name"] .".php"; )
Use a switch

I'm creating one simple list in PHP where the user can add name, age, emails, etc. I've also added a delete option, but I want to add a confirmation message when the user clicks the delete button.

I tried googling but only found jQuery and JavaScript solutions. Is there a way to do this with PHP only?

NameAge"; while($query2=mysql_fetch_array($query1)) ( echo " ".$query2["name"].""; echo " ".$query2["age"].""; echo " Edit"; echo " x"; } ?>

Delete.php

in

If you want to do it in PHP only, you need to add "steps" to your script, like so:

Step1 (show form) -> step2 (ask validation) -> step3 (validate)

To do this, you can use sessions to save the content of the form and a GET parameter to keep track of this step. Otherwise, the most simple solution is to use javascript:

echo" x"; //use double quotes for js inside php!

This is what you need

While($query2=mysql_fetch_array($query1)) ( echo " ".$query2["name"].""; echo " ".$query2["age"].""; echo " Edit"; echo " x"; ) in while($query2=mysql_fetch_array($query1)) ( echo " ".$query2["name"].""; echo " ".$query2["age"].""; echo " Edit"; echo " x"; }

and create a javascript function

Function confirmationDelete(anchor) ( var conf = confirm("Are you sure want to delete this record?"); if(conf) window.location=anchor.attr("href"); )

trust me, it's work 🙂

Add an onClick event to trigger the dialog and javascript:return confirm("are you sure you want to delete this?");

echo" x";

//add onclick event onclick="return deleteconfig()"

work for me but change this:

onclick="javascript:confirmationDelete($(this));return false;"

onclick="confirmationDelete(this);return false;"

Below is a variant of the above which gives a confirmation field and passes the variable from PHP to Javascript and back to PHP.
I used this to select a radio button to remove a file from the file list.
See OnClick trigger function named php $fileName in Javascript, confirm with filename and if so pass in href with variables for $_GET

PHP/HTML code:

$fileName $extn $size $modtime "; ?>

The article is not about clusters, not about sharding with replication, and not even about clouds. The article is about building a highly reliable computing architecture in which the number of users and their requests can grow like an avalanche. And it is critical for a business that the web service accepts each request, processes it correctly and to the end (regardless of failures and falls of some components), and is guaranteed to deliver a response to the client. And, of course, without the “space” costs for equipment and salaries for system administrators.

In other words, first of all, think - "do I need it." If someone has an online store that sells talking hamsters with a turnover of 100 orders per month, probably not. And if you are planning to run a business that can accommodate hundreds of thousands and millions of users, requires a large amount of calculations, works with high-value data, guarantees the transactionality of every business process, and needs parallel processing of data, this is it.

The financial sector, large online stores with a range of hundreds of thousands of units, online auctions, hotel and airline reservation systems, new cloud or social services with ambitions to get a million user base the next day after the start of an advertising campaign are potentially interested in a highly reliable system for your web services.

To whom this material is addressed

1. Developers of large web projects who are interested in creating highly loaded and fault-tolerant computing services.

2. Owners of new or expanding businesses that expect an "explosive" growth of the user base, and place high demands on the computing part.

3. Technical leaders and managers of large web projects who are not satisfied with the current state and are thinking about a fundamental reorganization.

Why so many words about “calculations”?

Because the immediate future of large web projects lies in the field of “ big data” (“Big Data”) is a trend that was noted as one of the top ones in 2011, along with virtualization, energy saving and monitoring, and since 2013 it has firmly taken its place in the industry and even became one of the academic subjects in large foreign universities.

The volume of data to be processed to solve business problems is constantly increasing today, and in the future this process will only accelerate. If ten years ago it was enough to show the user an “online storefront” with a product, a year ago it was enough to analyze the user’s path through the site and show him a strictly relevant product (the so-called “behavioral technologies”), today it is considered the norm to know everything about the user, including height, weight, age, and the name of your favorite dog.

The laws of natural competition dictate that if you want to provide customers with more options than your competitors, then you need more data and more calculations. And sooner or later, your project, without proper approaches, can drown in them - just like various projects are drowning everywhere now: someone because of the complexity of support, someone because of simply bad code, someone because of the high connectivity of modules , someone due to the use of unreliable components.

Therefore, everyone who today starts a web project with great ambitions will get more advantages over rivals if they initially consider their project not only as a program code, but also as a computing environment - a system with its own laws of existence and development. The more attention you pay computing management at the start, the more likely you will be to overtake competitors in the coming years.

The author of these lines is convinced that the “classic” modern approach to building highly loaded web services has a number of serious drawbacks. Let's see why. First, consider a typical modern scheme:

The classic approach to building a highly loaded web service

1. Many servers divided into roles.

2. Part of the servers (the role of Frontend) is designed to return static resources (images, CSS, JS-files) and “distribute” the front of incoming traffic to downstream nodes. The main software is usually Nginx.

3. Downstream nodes (Backend role) are engaged in dynamic calculations. Simply put, it can be a typical Apache + PHP bundle.

4. Another group of servers is designed for data storage. These are MySQL, Memcache, Redis and so on.

5. The web service code itself (in this example– PHP code) is equally copied to all nodes where there is Apache + PHP, and equally processes those requests that “fall” on one or another node.

6. Databases are “smeared” over their group of servers using some form of sharding, and the load on them is balanced in a similar way.

An attentive reader will notice that DNS balancing and CDN are not mentioned in the scheme, but the author deliberately omitted them so as not to overcomplicate the scheme. The principle of operation of these pieces is very similar to the above.

Disadvantages of the classical approach

1. The main trend is complication. Over the life of the project, the “classical” scheme becomes more and more complicated. With the addition of each new server, you need to enter it into the traffic distribution scheme. Of course, in large projects, introducing a server into a role is a “one-button” action, but nevertheless, this is an increase in the infrastructure that needs to be supported. And even more so - in the case when the current capacities for the database are no longer enough, and you need to “live” (without stopping the service) transfer or distribute the database to new servers.

But the main problem is that increasing the traffic distribution cluster does not lead to a decrease in the complexity of the program code. You can build a cluster flawlessly, but the code will remain the same.

2. Deploy is not atomic. In plain words, laying out new version the project on combat servers takes some time. It is necessary to physically download files to N-twenty machines, make changes to the database, reset caches (many different caches), and at the same time on all servers finish processing requests with the “old code” and start processing with the “new code”. Otherwise, many small conflicts can arise when part of the user's request is processed in the old way, part in a new way, some part went into the database according to the “old scheme”, part “according to the new one”, and so on.

Therefore, ideally, everyone wants to pause the service for the duration of the update (a few seconds or tens of seconds), and then turn it on again. In reality, with a flow of at least 1000 requests per second, no one does this, preferring to regularly fix minor collisions “by hand”, or covering them with defensive programming that supports backward compatibility “up to the seventh generation”. About how regular support backward compatibility complicates the life of programmers (and increases the cost of the project as a whole) - a smart reader can think for himself.

3. Used by HTTP. The HTTP protocol is obviously obsolete morally and technically, and if you follow (for example) mobile development- you know that it is being superseded everywhere by more lightweight protocols. But the main drawback is different: the HTTP protocol “in the browser” requires the completion of the loop - it requires a response in a limited time. This obliges the service to calculate and prepare the response strictly within the small timeout that the browser allows it. If the service is this moment overloaded - the request will be lost forever.

Therefore, in “typical web projects” they resort to various tricks such as Long polling or some other form of periodic requests, which not only complicates the architecture, but also overloads the service with unnecessary “wasted pulling”.

4. Script initialization per request. This is a consequence of the use of HTTP and scripting languages ​​such as PHP, which, according to a long-established tradition, are restarted in response to each request. Yes, yes, in response to each of 1000 requests per second, the PHP script will start again, initialize all variables again, and establish connections to the database again. In practice, it happens that it takes 0.005 seconds to process a request, and the script is initialized in the order of 0.05 seconds - ten times longer!

In other words, 90% of the time your servers are busy not processing client requests, but useless initialization of scripts. Try converting it to money. Therefore, a lot of workarounds have been invented, like OPcode caching, persistent database connections, local caches like Memcache or Redis, designed to offset this unpleasant effect.

5. Monolithic Application. No matter how you divide the application into modules, no matter how hard you try to spread the code over a complex directory structure, no matter what lazy autoloading you use, there is only one criterion: if you need to upload the entire application to upload at least one change, you have a monolithic application. Nothing else is given.

The disadvantages of monolithic applications are massively described in the literature. One of the main ones can be briefly mentioned: if you want even the smallest feature to be in production within an hour, you need to fit the entire production chain into one hour. Problem definition, implementation, backwards compatibility checking, test writing, documentation writing, running through the manual testing department, fixing bugs - all within an hour.

Because if you upload the entire application at exactly 00 minutes of each hour, then by the end of each hour you should bring the whole application to a stable state.

6. The web interface is rendered by the backend. In a typical case, appearance(and, accordingly, the HTML code) of the project pages is rendered on the Backend side, as a rule, in response to each request. This is an excessive, unjustified expenditure of resources and money.

7. Political division of departments. Department system administrators is responsible for ensuring that the incoming traffic front is “smeared” over a bunch of servers on which the PHP code is running. The programming department is responsible for the PHP code. If the PHP code did not have time to process a specific request, then it is not clear who is responsible for this - either the admin, who “let” too much traffic to the server and overloaded it, or the programmer who wrote a non-optimal script. If the database starts to slow down, then it is also unclear who remains extreme: the admin, who did not realize in time to “shard” it, or the programmer, who could also figure it out.

If the examples seem exaggerated and “not from real life” to you, remember that the author worked in a project that was hosted on 200 servers, and 60 of them were occupied by the database. It's scary to remember how many people were employed to service this project.

Main disadvantage

We repeat the above thought: the main drawback of the classical scheme, according to the author, is that technical specialists optimize the wrong thing. They optimize the incoming front of requests, actually “smearing” it over a large group of machines, instead of optimizing the very essence - the computing part. And it's much easier than it looks.

theoretical ideal

Oh, how we wish we could:

1. Get rid of a bunch of expensive servers and use one or two small groups.

2. Abandon the Nginx->Apache->PHP scheme with its wild “overhead” in terms of consumed resources that cost money.

3. Eliminate the monstrous cost of initializing PHP scripts, for the same reason.

4. Refuse the need to “render” pages on the backend. It would be quite a dream if a web service could work with an unstable or no Internet connection (for example, when using mobile network on the road).

5. Get rid of the HTTP timeout “loop”, deliver the response to the client only when this response is ready, and with a guarantee of delivery.

6. Update the project in small parts, without stopping and without losing a single client request.

7. Don't worry about lost requests if a part of the project (some component) "fell" or was temporarily turned off for debugging purposes.

Unreal? Easily!

First steps towards excellence

First, let's learn what must be done, therefore we will discuss - how.

1. Design the whole system as SOA(service-oriented architecture) with ESB (enterprise messaging bus), abandoning the monolithic approach, so that each independent part of the business logic is processed by a separate “service”, and they would communicate with each other via an independent exchange bus.

2. Let go of synchronicity. For example, in a synchronous "request - processing - response" scheme, this is one HTTP loop that does not have strict termination control and can easily be interrupted. In asynchronous, there are three separate processes: request (sent and confirmed), processing (retrying in case of failure), response delivery (with guarantee).

3. Divide the project into two applications- Frontend and Backend. In the case of a web service, the frontend is (usually) a JavaScript application. The bottom line is that applications work asynchronously and decoupled from each other, exchanging messages over a two-way communication protocol.

4. Opt out of HTTP in favor of WebSocket. The WebSocket protocol has fantastic performance compared to HTTP, does not have any "loops with timeouts", and allows you to transfer any data (including binary data) in both directions.

5. Provide a "storage" for running queries. Once the request is received from the client, say “Confirm” to the client and save this request. As soon as the backend is free from the previous processing cycle, pass the request to it. As long as the request is “walking” between backend nodes, keep it from the moment it “entered” the node and commit it as soon as it “left” the node. Thus, if some node “falls”, the system will not lose the request and immediately send it back to processing. At the end of processing, send the result to the client, and store it until the client says “Confirm”.

6. Ensure parallel computing for those operations that can be performed in parallel in terms of business logic, and sequence for those that must be performed strictly sequentially. This paragraph was written not with a claim to “Captain Obviousness”, but to show that not any business process can be “blindly put” on multi-threaded code.

7. Give up scripting in favor of “daemons”. A daemon is a process that starts once and then constantly “hanging” in memory. Since it runs all the time, it doesn't need to spend time re-initializing on every request. Looking ahead, I will say that the PHP daemon (when using modern versions of PHP) has no fundamental differences from a regular PHP script.

SOA Design Approach

SOA - Service Oriented Architecture - is not newfangled. This modular approach to software development was initiated by IBM back in the last century, and is currently being supported and promoted by industry leaders, mainly in "enterprise-level" products in .NET and JAVA.

In the classical approach to programming web services in PHP languages and similar - design starts from models, their properties and operations on them. Models represent real-world objects, and operations represent actions on objects. However, as practice shows, the real world is much more multifaceted and complex, and is much more effectively described by the language of events and reactions to them (for more details, see post #1593 with a description and examples).

The real world consists of events that occur simultaneously (in terms of programming - “in parallel”) and mostly without our participation, and to which various reactions occur or do not occur. Reactions, in turn, can generate the following events. SOA is ideal for “real world programming”, as it is most convenient to operate with events, relationships between them and reactions to them. Moreover, at right approach to the organization of the architecture, and events and reactions to them will occur in parallel, even if you use a “single-threaded” programming language like PHP.

You need to design a SOA product based on what events occur in your business logic, how they are related to each other or should follow each other, what reactions should occur in response to certain events, and who exactly will process these or those events. other events. The implementation of data models and actions on them fades into the background (encapsulated in a “service”), and the list of “services” and the plan of interaction between them (inter-service API) are put in the foreground.

Implementation: first approximation

1. Frontend as an independent application. Any popular JavaScript-MVC-framework is suitable for implementation. However, I note from practice, if you start to ache when you combine the words “JavaScript, MVC and framework” in one sentence, it will be difficult for you. The application must be able to draw all of its “screens” without referring to the backend, give the user navigation (transitions between “screens”) also without referring to the backend, and support a bidirectional communication channel with the backend.

2. Entry point for WebSocket connection to backend. There are a number of ready-made solutions in NodeJS, which also support fallback requests for long-polling and ajax for ideologically obsolete browsers. I note for the future that this node can also be accessed on pure HTTP, if you need to write some gateways with other people's services, however, to simplify, you can write a separate “pure HTTP” node.

The entry point will provide a bidirectional communication channel with the front-end application (in other words, with the browser), accepting requests from it and returning responses to it.

3. Storing running requests in the system. For this, the popular AMQP server is the best fit, providing message queues and routing between them. As soon as the next request comes from the client, put it in the “incoming” queue. Further, it will be extracted from this queue by the daemon, which will analyze the contents of the request and send it to “routing” throughout the system (which actually means transferring it to one or more queues according to certain algorithms). Each daemon, dealing with its own part of the business logic, will receive this or that message from “its” incoming queue, process it and place the response in the “outgoing” queue.

I note that in the terminology of the popular RabbitMQ broker there is no concept of outgoing queues. Messages are published in the exchange (exchanger), from where the broker itself is transferred to specific queues according to the routing rules. Here it is so written for conditional understanding that the answer is not sent directly to the requester.

4. Demon control(supervisor). To run a simple PHP script as a daemon, just wrap the executable code in while(true) (...) and type in command line something like “php your-script.php”. But it’s better to use any suitable supervisor for this, which will essentially do the same, but also provide the necessary state of the environment, monitor the state of the process, and do some more useful things.

In real life, the PHP daemon is a little more complicated: it must receive control signals and reconfiguration messages, it must not leak memory, it must maintain (or restore dropped) connections to the database - but in general it is no more complicated than the PHP scripts you are used to .

One step closer to reality: event-driven approach in SOA

Some (in the author's opinion, outdated) approaches to building modular applications are based on the RPC (Remote Procedure Calling) principle, which implies a direct call to specific methods or procedures in a remote project component. This approach completely destroys all the advantages of SOA, as it usually means a direct and hard connection between the sending and executing nodes. In the design and implementation of a complex product, the principle of loosely coupled components should be adhered to as much as possible, since it is the complexity of the architecture and code that will ultimately determine the cost of ownership (corrections and changes to the product after its launch).

The event-driven approach in SOA assumes that components (services) communicate with each other by sending asynchronous events (“events”, from the word Event). An event is a message (for example, in AMQP terminology) that has a name (name) and a set of parameters. An event is intended to tell the system that something has happened, or to "ask a question" to the system. In the general case, events are sent “to the system” (more precisely, to the common ESB bus) addressless - that is, without specific intentions for delivery to specific nodes or performers.

On the contrary, specific nodes (components, services) listen to the common bus for certain events to which they are ready to react. This may mean that the service is ready to hear some event and perform the appropriate action. Or the service has some knowledge (for example, it owns a database with information about users) and is ready to provide them in response to a “request”. In both cases, the result of responding to an event is to generate a new event (with a different name and different parameters), which can also be heard by other interested services.

With the proper organization of the general ESB tires, services send and receive events asynchronously without waiting for each other. This means that the sending service can send any number of events to the ESB without time delays, and move on to solving the following tasks. Compare this with classic HTTP, which means waiting for a response in the current processing cycle, and you will see the benefits. And the receiving services will also receive new events asynchronously, independently of each other, immediately upon completion of processing the previous event.

SOA event model in code

In short, you need to look at your code not as classes with functions (methods), but as events and actions that occur in response to these events. Moreover, the results of actions are also events. In relation to the architecture under discussion, we can say that local events are events that occurred inside a particular PHP script, and remote events are events that came to this script from the AMQP queue (or are sent there as a result). If you treat all your code in this way, it will immediately lead to a surprising and very important effect:

If local and remote events are the same, then local and remote event handlers are the same!

Why is it so important? Because the programmers of your team continue to write regular PHP code, without thinking about where this or that event will be processed - right there in this or a neighboring PHP script, or somewhere on the other end of the system, in another daemon, at least on the other programming language. If you are making a project with a public API, then any third-party participant will be able to “sign” their code to your events (and process them), or vice versa - send you their own so that you process its events as requests (and get paid for it if you using a pay-per-use SAAS business model like Amazon).

Remember what we called the main drawback of classic large web projects - continuously growing complexity, and therefore the cost of ownership, the cost of support and changes. In the case of an event-driven SOA architecture − complexity is continuously decreasing, since “complex nodes” can be easily divided into independent services (in this case, demons), while the principles of the system remain unchanged, and its performance only increases.

Deploy a new version without losing current processes

Since you no longer have a monolithic system, you do not need to deploy the entire system. Moreover, deployment of a component (service, daemon) can take any time, within reasonable limits, of course. The important thing is that during the deployment time (those few seconds or several tens of seconds) of the component, the entire project does not interrupt service for a moment. How it's done?

You simply turn off the service that needs to be updated. Update its code and database structure (if necessary), then run it again. All current requests to this service will wait in the AMQP queue until the service comes up. I note that since the services are small (a small amount of code needed to solve only a small part of the business logic) - this is much faster than deploying an entire monolithic application. But in any case, there will be no losses.

Problems with the web interface

A fast, responsive web interface is a prerequisite for a high-load project. Let's see why the web interface can “slow down” in general with the classical approach to implementation:

1. The interface is drawn on the backend, which is overloaded and does it slowly. Transition between pages is slow. Even with AJAX, blocks redraw too slowly.

2. The source code of the interface (HTML, CSS, JS) is redundant and slowly transmitted over communication channels, especially if this is done on loading each page during the user's navigation through the interface.

3. The interface contains a large amount of unoptimized JavaScript logic, which is slow on weak devices (primarily on mobile devices).

Let's try to solve these problems:

How to make a fast and responsive web interface

1. First, and most importantly, source interface should transmitted to the client no more than once. The only civilized way to achieve this is to make a full-fledged JavaScript application. It is downloaded to the client once (in this case, you can show a beautiful animated preloader), and then for the entire time of working with the service, the client will no longer need to wait for the download.

2. All transitions between the "screens" of the web interface must be performed inside a JavaScript application, and in no case as separate requests to the backend. There is a term for it, “single page web application”, in which navigation is essentially done by switching “screens”, while the content address bar changes dynamically, creating the full feel of classic “page transitions”.

3. Sending messages (events) to the backend, and receiving responses, must be decoupled from each other and from user navigation(asynchronous). The aforementioned WebSocket inherently “recommends” just such an implementation. All lengthy operations should also not block the interface, unless specifically done to do so.

Thus, the user only needs an internet connection for the initial download of the application (a few seconds). Further, it can work with the service even when there is a temporary lack of connection (for example, from a mobile device in the subway, outside the city, in an overcrowded hotel abroad, etc.) - the application will record requests and try to send them as soon as the Internet appears, and thus same way will receive answers.

Naturally, this does not relieve the developer from the need to optimize and minimize the code. However, as practice shows (for example, the Trello service), this task is no more difficult than others.

Note for doubting developers of web services for mobile devices: according to the author's practice, in 2013 single-page JavaScript applications on websocket transport successfully work on the iPad.

User work from multiple devices

From work, he uses your service from work desktop computer, on the way home he takes out an iPhone, and at home he turns on the tablet. If the user sent some command from the interface to the service, then he is waiting for a response about the processing results. It is easy to understand that if (when) the processing took some tangible time, the response should be sent to the device that the user uses (sorry for the pun) at the moment response delivery, not at the time of the request.

The problem is that it is impossible to say unequivocally whether the user has stopped using (sorry again) this or that specific device. Maybe he closed the browser. Maybe his battery is dead. Maybe he left for the subway tunnel, where there is no connection, and in half a minute he will appear again. There are a lot of options, and the author is unknown The best way definitions. However, here's what you might find useful:

1. Record (on the backend) all the user's devices and the time of the last activity from each of them.

2. Classify system events that need to be reported to the user into those that need to be delivered only to active devices and those that need to be delivered “broadcast” (to all devices).

3. Enter extra layer abstractions - a service that will intercept certain events that are interesting to the user and form messages from them. Thus, you can easily broadcast the same message about the success of the operation - in several types: a short notification in mobile device, a little more authentic - to the browser, a detailed message - by e-mail.

4. Provide queues for sending to each user on each separate communication channel (web interface, mobile device, mail). The standard AMQP functionality will also help you with message expiration timeouts so that they lie there no longer than a certain time and do not clog the system. When a user comes online through a particular channel, fresh, pending messages of that particular type will be delivered to them.

The author can add that on the basis of the same system, it is possible to build a delayed sending of notifications (which will be sent no earlier than a certain date), and even sending real paper periodic correspondence (acts, payments, etc.), but this is a topic for a separate article .

I will emphasize the main thing: do not consider the delivered messages as some kind of notifications that you are used to on Facebook or Vkontakte. Delivered messages are query results user! All user actions in the interface, implying some kind of requests to the backend, receive responses in a uniform form of “delivered messages”, through a single unified communication channel. And then the web application algorithm understands what needs to be done with this or that message - draw a notification with text, add a line in the table, switch something in the interface, and so on.

Parallel and Sequential Computing

It would be useless to design a fast frontend web interface if you have a slow backend. No, this is not about streams, not about forks, and not about Erlang. We remain on the usual PHP, available to any beginner/intermediate programmer.

Why do we need parallelism at all? Even if we do not talk about the shortcomings of single-threaded languages ​​in general, parallelism significantly speeds up the calculation of the task, which means that it significantly reduces the requirements for hardware resources (hardware) and increases user satisfaction from working in the interface (they get results faster).

Take any fairly complex business process in your web project and draw it as a chain of steps. You will get a sequence of actions in the system from request to response. Most likely, first there will be some checks, then the execution of the main task, then the secondary subtasks, and finally the output of the result. Look closely: can any of the actions be performed in parallel?

Let me give you an example: let's say a user wants to purchase some service that is included as an additional paid option in his tariff plan. The number of options is limited. If the option is turned on successfully, then you need to send a notification to the user in the browser, send a duplicate email, deduct money from his billing account, and notify the client department. Draw a chain:

1. The system received a request to enable the option.
2. We authorize the user and find out his tariff plan.
3. We check whether it is possible to enable this option at all by tariff plan user.
4. Check if the user has enough money in the account.
5. Check if this option conflicts with any other settings.
6. If everything is OK, then enable the option.
7. We send a notification to the browser.
8. We send a notification by mail.
9. Write off money in billing.
10. We notify the client department.

An attentive reader may find fault with the sequence of actions, but the author will remind you that this is an approximate example.

What do we see? Note that there is no reason to perform all steps sequentially. It would be much more correct to “parallelize” 3,4,5 into three threads, and at the end - 7,8,9,10 into four threads.

Thinking about flows and forks? In vain, you have SOA!

How to do parallel computing in SOA

For readers who have just scrolled through the article to this point, I will explain that this is not about parallelizing the same task in SOA - for this, in the general case, it is enough to run the daemon in N instances and take care of the contention of access to the database.

So, in this example, we have three-four-several different tasks that are performed by different services, and which we want to execute in parallel. It is not difficult to send them to parallel processing: it is enough to send one event “can the username user enable option X?”, and all services subscribed to this event will catch it, perform their checks, and dispatch the resulting events.

The problem is precisely to collect these resulting events when we need the total result of their work in order to move on. For example, in the list above, we need the result 3+4+5, and 7+8+9+10 can be ignored.

In fact, with high requirements for the mandatory completion of transactions, you need to control each chain to the end, but we will discuss this later.

Naturally, if our daemon will “hang and wait”, consuming resources (the so-called “idle”), then you can’t build any highly loaded service like that. The point is just that the demon solves other tasks and serves other requests of other clients, while three separate “threads” (3,4,5) are solving their subtasks. Difficulties are also added by the fact that the resulting events can come in an arbitrary order. However, all this is solved easily and simply:

As far as the author knows, none of the out-of-the-box AMQP implementations that exist today allows expecting and “gluing” several events into one in order to receive only it - one result. So you have to take care of it yourself, like this:

1. Before sending an event to AMQP, commit to fast memory(use any suitable in-memory storage) a list of the names of the resulting events that the service expects to receive, as well as the name of the event (let's call it “R”) that needs to be dispatched with the sum of the results.

2. After that, the service ends the processing cycle of the current event and is freed for the next tasks.

3. As soon as any event from the list that we have stored in memory arrives, the service “unpacks” it and stores its contents in memory, attributing it to the event name. At the same time, it checks if there are other events in this list for which no response has been received. If it is, the loop ends at that point.

4. Otherwise, that is, if all the events in the list received a response, the service glues them together and dispatches the glued result under the name of the “R” event remembered earlier. After that, in order to save memory, the list with the results is simply deleted from memory - it is no longer needed.

5. The same service, or some other (at the discretion of the system designer) receives the resulting event “R” with all the results of parallel processing. What follows is obvious.

If from the description it seemed to you that this is a long time, then I will explain - we are talking about thousands and tens of thousands of events per second (!) On one average server.

The use of in-memory storage implies that even if the service stops (falls, updates), the current business process will not be lost. After the service is started again, it will continue to receive events from the ESB and process them according to the algorithm described above.

Transactionality, rollback of operations (rollback) and fail scenarios in SOA

Because in SOA, peer events “walk” through the ESB, you need some kind of indication to indicate that “this answer here” refers to “that request over there”. There is no need to reinvent the wheel here - in the specifications of any popular protocol, you will find a parameter with a name like correlation_id. Typically, this is a string. It must be contained in the parameters of all events of each individual business process, from entry to exit, in order to identify the message chain that belongs to this business process. Looking from the side of the web interface, the web application stores the current active (sent) requests and, by correlation_id, “understands” in response to which request each specific response came.

Let's deal with the terminology: transactionality is the property of a system to perform several actions as one common operation that makes sense and can only be completed completely. Since it is physically impossible to perform several operations atomically in a distributed system with parallel threads, the system provides for so-called fail-scenarios and rollbacks.

A fail scenario is generally an action to take when an error occurs. Rollback is, in this context, a script of actions that needs to be performed to “undo” a series of previous actions that ultimately led to an error. Roughly speaking, rollbacks are processes that are the reverse of normal business processes in the system.

Rollbacks are not always needed and not always possible. For example, if you connected some option to the user and then “fell” on the billing, then the option can be disabled back. Well, then you can try again, automatically or by a second command from the user. And if you physically deleted the content, and some of the subsequent operations did not work ... The situation is ambiguous.

Therefore, fail scenarios and rollbacks need to be approached wisely. The author may recommend the following path: Always write fail scripts, but don't always write rollbacks. Your failures will be instantly reflected in monitoring - and the support team will be able to quickly fix the situation with their hands, gain experience and formulate technical requirements for programmers. While writing a uniquely correct rollback from scratch can be very, very difficult.

Nevertheless, it should be understood that rollbacks, rollbacks, handling of erroneous situations are the same business processes as everything else. They are also based on events passing through the system. It will be nice if you initially lay down two opposite paths (forward and backward, success and fail) in each event and each handler in order to implement them in specific tasks.

SOA scaling

Any system will need to be expanded sooner or later. In the case of SOA, this is done easily and naturally:

1. Duplicate Entry Point. This refers to the same WebSocket gateway that we considered at the beginning of the article. It can be duplicated an unlimited number of times, since communication between it and the client is unified and decoupled from the internals of the system, and communication between it and the system, in turn, is decoupled from communications with the client.

2. Duplicate Instances(instances) of services. Services that do not require a database or only “read” from them are seamlessly duplicated. And the regular functionality of RabbitMQ will allow you to subscribe N instances to the same queue, messages from which will randomly arrive in one or another instance. When duplicating services that deal with external applications (databases, third-party software), you need to consider how these applications provide transactional requests from several parallel clients.

3. Duplication of data stores. Here you are free to use any sharding known to you. If you're dealing with 10 million users and that seems like a lot to you, divide by 10 million bases (for example, based on the CRC32 from the user's login or some other round-robin method). If the database of one service continuously grows and becomes more complex, split it into two services.

4. Duplicating an AMQP Broker and in-memory storage. From the author's practice, RabbitMQ and Redis perfectly fulfill their role. If you have equipment in more than one DC rack, choose a rabbit mode that is tolerant of network connection failures.

5. Full machine duplication. FROM modern technologies virtualization (KVM) and configuration (Chef), the task of “raising the same machine” comes down to pressing one button.

Encryption of traffic between frontend and backend

It is recommended to organize a WebSocket connection over SSL. On top of that, it increases "penetration" against backward office providers that block "any weird traffic" except HTTP[S].

However, if this seems insufficient to you, you can arrange the generation of key pairs with each client login (one pair on the front end, the second on the back end), exchange public keys and further encrypt all traffic with regular RSA.

Protection against DDOS and similar abuse

The author deliberately omits the question of “low-level” protection when it comes to SYN flooding and flooding channels with hundreds of gigabits, since hundreds of books of specialized literature have been written about this. Let's talk about how to protect the system already inside, at its logical levels, when an attacker has found a way to “flood” your system (SOA + ESB) with thousands of events.

1. First rule: nothing should be processed until its validity has been confirmed. If you expect a small text in JSON wrapped in BASE64 as input, then the incoming string longer than a megabyte should be explicitly discarded - do not try to unpack it. A string containing “non-Latin” characters is similar. When you have unpacked the string, don't try to do json_decode right away, first check the number and parity of parentheses. And so on.

It looks like paranoia, but otherwise you can easily be “filled up from memory”, that is, lead to a failure of the service, forcing it to take up all the RAM available to it.

2. The service that processes incoming messages should not write anything to memory, to the database, and other storages. The reason is the same. First make sure that the message as a whole is valid, and only then let it “deeper” into the system.

3. A service that can be forced to “go to the database” often must be protected by caching. A simple example is a user authorization service. If it is not protected, then an attacker can send thousands of authorization requests in a row, thereby overloading the database.

4. At the entrance to the system, you need a service that rejects requests from “suspicious” sources, for example, from IP addresses from the “black list”.

Sounds like classic advice, so what's the deal? The main problem is that if we put an analyzer-filtering service at the entrance to the system (as is done in classic web projects), then the performance of the entire system will not be higher than the performance of this analyzer-filter. It is extremely expensive to review each report for suspicion “in real time”. You can and should do this after the fact, and here's how:

1. Make a service that will “listen” to all messages in the system. In RabbitMQ this is achieved by subscribing to the routing key "#".

2. Teach this service certain rules by which it can diagnose "suspicious" senders. For example, these are senders who send too many similar messages in a time interval, or who send repeated messages, or who send messages on behalf of the same user from different IP addresses ... There are a lot of options, turn on your imagination. At the same time, it does not matter how quickly such a service works (within reasonable limits, of course) - it does not affect the speed of the entire system.

3. As soon as the service concludes that such and such a sender is suspicious, it dispatches this event to the system and continues to do its own thing.

4. Put a very simple and fast daemon at the input - a filtering service, the task of which will simply be to "stupidly" block suspicious senders. No analysis, no parsing, no extra costs. It is easy to guess who is considered suspicious: the service will learn about them from the events described in the previous paragraph and add them to its internal black list.

End of the first part. Continued: SOA: distributed architecture and its maintenance.

I am a mentor in IT projects. This means that if you are an owner or leader, I can help you take new heights. Clean up processes, understand the motivation of the team, implement tools and achieve specific goals. I do not teach how to do business, but only help to get around the generously scattered rake on your way. .