@@ -5,4 +5,199 @@ uses [ReactPHP Promise](https://github.com/reactphp/promise) implementation for
5
5
this purposes.
6
6
7
7
> This package is still on progress. Subscribe to the repository to be aware of
8
- > updates and follow future stable versions
8
+ > updates and follow future stable versions
9
+
10
+ ## POV
11
+
12
+ This package is part of a Prove of Concept with the main goal of working with
13
+ Promises in Symfony. Before that, let's check where are we. Let's start from the
14
+ beginning and talk about what Symfony is made for, the regular usage is usually
15
+ used for and how we can really improve ** SO MUCH** the performance of our
16
+ applications.
17
+
18
+ > As a disclaimer, this POV is focused on APIs, so we are not adding here any
19
+ > Twig support. Once the package becomes tested and a little bit stable, then we
20
+ > will add this feature.
21
+
22
+ ## Symfony on top of Apache / Nginx
23
+
24
+ When we talk about Symfony, we usually talk about a small stack of technologies
25
+ that, and not only with Symfony but with almost every single PHP 7 framework and
26
+ project.
27
+
28
+ We can think about Apache or Nginx as a server, Doctrine as a Database layer and
29
+ maybe Redis as cache or key-value persistent storage. That's great. If we could
30
+ ask every single Symfony project in this world, we would find mainly this
31
+ configuration.
32
+
33
+ Let's check this configuration in a performance point of view. And when we mean
34
+ performance, we mean about CPU consumption, Memory usage and Response times.
35
+ Everything matters here, right? At least, everything should matter in terms of
36
+ software economy.
37
+
38
+ ** Step 1** - Apache server receives a new request and sends it directly to the
39
+ app.php file.
40
+ ** Step 2** - Symfony kernel boots. That means that, in the better of the cases, the
41
+ container is cached properly. The Kernel is created * (once again)* , the
42
+ container configuration is loaded from this cache, and a new Request object is
43
+ created with the data received from Apache.
44
+ ** Step 3** - The Kernel handles this Request, waiting for a Response.
45
+ ** Step 4** - Some framework magic (resolve controller, resolve dispatch some
46
+ events...)
47
+ ** Step 5** - We call the controller entry point. Remember that we MUST return a
48
+ Response instance (remember that we don't use Views here, so we discard
49
+ returning an array here. Anyway, would be the same).
50
+ ** Step 6** - We do our logic. For example, we call a repository to get an array of
51
+ values from Redis.
52
+ ** Step 7** - Redis returns an array of values, where the controller return a new
53
+ Response with these values, where the Kernel, after some extra event dispatches,
54
+ return this Response to Apache, which return the response to the final client.
55
+
56
+ This is one natural Request / Response workflow in one of our applications.
57
+ Fast, isn't it? Let's check in terms of performance.
58
+
59
+ ** Step 1** - We must have Apache server installed. By adding Apache as a man in
60
+ the middle, we spend some time. Even if it's ** 1ms** , we will see later that
61
+ each single ** 1ms** can be so much important here.
62
+ ** Step 2** - Symfony kernel is booted every time. Once and again. Every single
63
+ request. Let's say... ** 15ms** ? ** 20ms?** Something like that. Let's say
64
+ ** 15ms** being SO optimists.
65
+ ** Step 7** - Imagine a Redis call as a representation of any external call. This
66
+ could be a redis one (fast one), or an HTTP one, slow one. This action will
67
+ last the time this operation lasts. Let's say ** 50ms** .
68
+
69
+ If we consider that the PHP application can be around ** 3ms** , let's calculate
70
+ the total time of our requests from the final user point of view. This time is
71
+ ** 70ms** per thread. Because both Apache or Nginx can manage several threads at
72
+ the same time, we can say that the final response will be around 70ms per
73
+ request. I would say not bad, but for people that respect performance, 70ms are
74
+ very bad numbers.
75
+
76
+ ## Symfony on top of ReactPHP
77
+
78
+ The main goal of this layer is to delete Apache. Why? Well, if you review the
79
+ numbers below, you will find that booting the kernel each time is so expensive.
80
+ We said around ** 15ms** as a symbolic number, but these numbers can easily
81
+ increase so much depending on the server.
82
+
83
+ The main goal is to be sure that we keep the kernel built and running forever
84
+ and ever, listening new Requests, and returning new Responses.
85
+
86
+ For this, we must know a project called [ ReactPHP] ( https://github.com/reactphp ) .
87
+ It is important to know that project because they have a nice HTTP Server to
88
+ handle requests and return responses. Is based on Promises, but, as always, you
89
+ don't have to work with Promises to work with Promises.
90
+
91
+ By using this server, we would remove the ** Step 1** and the ** Step 2** . The
92
+ kernel is booted only once (can really be booted before the first request), and
93
+ after that, each requests would start at ** Step 3 ** .
94
+
95
+ Time elapsed? Well, ** 54ms** per each server. In that case, the server would be
96
+ the same PHP. The problem? Well, we only have one single thread here, so this
97
+ server would be completely blocking here, allowing only to return around 20
98
+ requests per second (while the HTTP blocking call is not resolved, the thread is
99
+ waiting for it).
100
+
101
+ We can easily solve this problem by emulating what Apache does internally,
102
+ having multiple threads or workers, and balancing between them as long as they
103
+ are not available.
104
+
105
+ You can check a project called [ PHP-PM] ( https://github.com/php-pm/php-pm ) . This
106
+ server creates as many ReactPHP servers you need and use them all in a smart
107
+ way.
108
+
109
+ Performance review. Well. If you have so many requests per second, and you have
110
+ very hard I/O operations, you might want to add many workers there. Otherwise,
111
+ you will experiment timeouts. Remember that you will still having several
112
+ threads with blocking calls. A good approach, but not as good as a person that
113
+ cares about performance while doing PHP wants to see.
114
+
115
+ ## Symfony & Promises
116
+
117
+ So what about Promises? Can I work with Symfony and Promises at the same time?
118
+ Yes you can. And is very easy. There is only one condition here. You can async
119
+ everything you want, even I/O operations by using some Client like
120
+ [ BuzzClient] ( https://github.com/clue/reactphp-buzz ) , but as long as you get
121
+ returned to the controller, you will have to turn this promises asynchronous and
122
+ and get returned their value. You can do that by using
123
+ [ ReactPHP Block] ( https://github.com/clue/reactphp-block ) . Remember that the
124
+ Symfony Kernel ** MUST** return a Response object, and the same for the
125
+ Controller.
126
+
127
+ So will it be really asynchronous if the event loop is only shared by one single
128
+ thread? At all.
129
+
130
+ ## Symfony Async Kernel
131
+
132
+ So on one side we have a Server called ReactPHP Http Server that work with a
133
+ running loop, and on the other side, we have a domain built on top of Promises,
134
+ with some non-blocking clients like the HTTP one or a Redis one.
135
+
136
+ This is the workflow.
137
+
138
+ ** Step 1** - ReactPHP receives it's own Request, and creates a Symfony request.
139
+ ** Step 2** - The Kernel handles this Request, waiting for a Response.
140
+ ** Step 3** - Some framework magic (resolve controller, resolve dispatch some
141
+ events...)
142
+ ** Step 4** - We call the controller entry point. Remember that we MUST return a
143
+ Response instance.
144
+ ** Step 5** - We do our logic. For example, we call a repository to get an array
145
+ of values from Redis.
146
+ ** Step 6** - Redis returns a ** Promise** of values. This promise is returned to
147
+ the controller, and has to be resolved. Once is resolved, returns a Response to
148
+ the Kernel.
149
+ ** Step 7** - The Kernel returns the Response to the ReactPHP server, which
150
+ creates a new promise with that Response.
151
+
152
+ When checking performance, we see that the I/O, even if it's asynchronous, is
153
+ blocked in the controller by the application, so lasts the same 50ms than
154
+ before. Our server is still blocking.
155
+
156
+ So, can we all see that this Symfony Kernel is the blocking part of the whole
157
+ application?
158
+
159
+ What if would have a way of, instead of returning a Response, our Kernel could
160
+ be able to handle Promises? In that case, we should'nt have to wait for any
161
+ Promise response, passing the promise created by the I/O async client directly
162
+ to the ReactPHP server.
163
+
164
+ Let's check the workflow.
165
+
166
+ ** Step 1** - ReactPHP receives it's own Request, and creates a Symfony request.
167
+ ** Step 2** - The Kernel handles ** asynchronously** this Request, waiting for a
168
+ Promise containing a Response.
169
+ ** Step 3** - Some framework magic (resolve controller, resolve dispatch some
170
+ events...)
171
+ ** Step 4** - We call the controller entry point. Now we can return a Promise
172
+ instead of a Response instance.
173
+ ** Step 5** - We do our logic. For example, we call a repository to get an array
174
+ of values from Redis.
175
+ ** Step 6** - Redis returns a Promise of values. This promise is returned to
176
+ the controller. No need to resolve anything. Returning the Promise to the
177
+ Kernel.
178
+ ** Step 7** - The Kernel returns the Promise to the ReactPHP server. Directly.
179
+
180
+ And checking the performance? Easy. The response will still return in 54ms. We
181
+ can improve this time by improving your networking interface or by adding some
182
+ cache. By the time spent in the server for that request?
183
+
184
+ ** 4ms** .
185
+ Only ** 4ms** .
186
+
187
+ And the most important part.
188
+
189
+ With the old implementation:
190
+ - (time 0) Request 1 * (slow)*
191
+ - (time 1) Request 2 * (ultrafast)*
192
+ - (time 2) Request 3 * (fast)*
193
+ - (time 80) Response 1
194
+ - (time 81) Response 2
195
+ - (time 91) Response 3
196
+
197
+ With the new implementation
198
+ - (time 0) Request 1 * (slow)*
199
+ - (time 1) Request 2 * (ultrafast)*
200
+ - (time 1) Response 2
201
+ - (time 2) Request 3 * (fast)*
202
+ - (time 12) Response 3
203
+ - (time 80) Response 1
0 commit comments