Skip to content
This repository was archived by the owner on May 28, 2023. It is now read-only.

Commit 558a241

Browse files
authored
Merge pull request #408 from Fifciu/feature/#-Varnish-Cache
Varnish Cache with auto invalidation for ES
2 parents 481f49d + e8870f9 commit 558a241

15 files changed

+638
-8
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- Create attribute service that allows to fetch attributes with specific options - used for products aggregates - @gibkigonzo (https://github.com/DivanteLtd/vue-storefront/pull/4001, https://github.com/DivanteLtd/mage2vuestorefront/pull/99)
1515
- Add ElasticSearch client support for HTTP authentication - @cewald (#397)
1616
- Endpoint for reset password with reset token. Only for Magento 2 - @Fifciu
17+
- Varnish Cache with autoinvalidation by Cache tags as addon - @Fifciu
1718

1819
### Fixed
1920
- add es7 support for map url module and fixed default index for es config - @gibkigonzo

config/default.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,12 @@
116116
]
117117
}
118118
},
119+
"varnish": {
120+
"host": "185.246.52.88",
121+
"port": 80,
122+
"method": "BAN",
123+
"enabled": false
124+
},
119125
"redis": {
120126
"host": "localhost",
121127
"port": 6379,

docker-compose.nodejs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ services:
2626
- /var/www/dist
2727
ports:
2828
- '8080:8080'
29+

docker-compose.varnish.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
version: '3.0'
2+
services:
3+
varnish:
4+
build:
5+
context: .
6+
dockerfile: varnish/Dockerfile
7+
volumes:
8+
- ./docker/varnish/config.vcl:/usr/local/etc/varnish/default.vcl
9+
ports:
10+
- '1234:80'

docker/varnish/Dockerfile

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
FROM cooptilleuls/varnish:6.0-stretch
2+
3+
# install varnish-modules
4+
RUN apt-get update -y && \
5+
apt-get install -y build-essential automake libtool curl git python-docutils && \
6+
curl -s https://packagecloud.io/install/repositories/varnishcache/varnish60/script.deb.sh | bash;
7+
8+
RUN apt-get install -y pkg-config libvarnishapi1 libvarnishapi-dev autotools-dev;
9+
10+
RUN git clone https://github.com/varnish/varnish-modules.git /tmp/vm;
11+
RUN cd /tmp/vm; \
12+
git checkout 6.0; \
13+
./bootstrap && \
14+
./configure;
15+
16+
RUN cd /tmp/vm && \
17+
make && \
18+
make check && \
19+
make install;

docker/varnish/README.md

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
### Tutorial
2+
1. Create network with `docker network create <your_network>`
3+
2. Use `docker network ls` and find your network. It should have prefix!
4+
E.g. when I used `docker network create some-net`, I have network with name `vuestorefrontapi_some-net`
5+
3. Open docker-compose.yml:
6+
At the end:
7+
```yaml
8+
networks:
9+
vuestorefrontapi_some-net:
10+
external: true
11+
```
12+
Set vuestorefrontapi_some-net to your network name
13+
14+
4. Check each `docker-compose` file and set proper network name.
15+
5. In the docker-compose.nodejs.yml it should not have a prefix, e.g:
16+
```yaml
17+
networks:
18+
- some-net
19+
20+
networks:
21+
some-net:
22+
driver: bridge
23+
```
24+
You can find Docker Compose files with applied network settings inside docker/varnish/docker-compose
25+
26+
### How does it work?
27+
1. I add output tags to the VSF-API response:
28+
```js
29+
const tagsHeader = output.tags.join(' ')
30+
res.setHeader('X-VS-Cache-Tag', tagsHeader)
31+
```
32+
33+
2. After it invalidates cache in the Redis. I forward request to the:
34+
```js
35+
http://${config.varnish.host}:${config.varnish.port}/
36+
```
37+
With invalidate tag in headers:
38+
```js
39+
headers: {
40+
"X-VS-Cache-Tag": tag
41+
}
42+
```
43+
44+
I set Varnish invalidate method to `BAN` but you can change it in your config + varnish's config.
45+
46+
3. Configuration of BANning we have inside `docker/varnish/config.vcl` in `vcl_recv`.
47+
It tries to BAN resource which has `X-VS-Cache-Tag` header:
48+
```vcl
49+
# Logic for the ban, using the X-Cache-Tag header.
50+
if (req.http.X-VS-Cache-Tag) {
51+
ban("obj.http.X-VS-Cache-Tag ~ " + req.http.X-VS-Cache-Tag);
52+
}
53+
```
54+
55+
Below under BANning logic. I have to tell Varnish what to cache.
56+
```vcl
57+
if (req.url ~ "^\/api\/catalog\/") {
58+
if (req.method == "POST") {
59+
# It will allow me to cache by req body in the vcl_hash
60+
std.cache_req_body(500KB);
61+
set req.http.X-Body-Len = bodyaccess.len_req_body();
62+
}
63+
64+
if ((req.method == "POST" || req.method == "GET")) {
65+
return (hash);
66+
}
67+
}
68+
```
69+
70+
I am caching request that starts with `/api/catalog/`. As you can see I cache both POST and GET.
71+
This is because in my project I use huge ES requests to compute Faceted Filters. I would exceed HTTP GET limit.
72+
73+
Thanks to this line and `bodyaccess`, I can distinguish requests to the same URL by their body!
74+
```vcl
75+
std.cache_req_body(500KB);
76+
```
77+
78+
Then in `vcl_hash` I create hash for POST requests with `bodyaccess.hash_req_body()`:
79+
```vcl
80+
sub vcl_hash {
81+
# To cache POST and PUT requests
82+
if (req.http.X-Body-Len) {
83+
bodyaccess.hash_req_body();
84+
} else {
85+
hash_data("");
86+
}
87+
}
88+
```
89+
90+
By default, Varnish change each request to HTTP GET. We need to tell him to send POST requests to the VSF-API as POST - not GET.
91+
We will do it like that:
92+
```vcl
93+
sub vcl_backend_fetch {
94+
if (bereq.http.X-Body-Len) {
95+
set bereq.method = "POST";
96+
}
97+
}
98+
```
99+
100+
101+
### Caching Stock
102+
It might be a good idea to cache stock requests if you check it often (filterUnavailableVariants, configurableChildrenStockPrefetchDynamic) in VSF-PWA in visiblityChanged hook (product listing).
103+
In one project when I have slow Magento - it reduced Time-To-Response from ~2s to ~70ms.
104+
105+
```vcl
106+
if (req.url ~ "^\/api\/stock\/") {
107+
if (req.method == "GET") {
108+
# M2 Stock
109+
return (hash);
110+
}
111+
}
112+
```
113+
114+
Then in `vcl_backend_response` you should set safe TTL (Time to live) for your stock cache. I've set 15 minutes (900 seconds)
115+
```vcl
116+
sub vcl_backend_response {
117+
# Set ban-lurker friendly custom headers.
118+
if (beresp.http.X-VS-Cache && beresp.http.X-VS-Cache ~ "Miss") {
119+
set beresp.ttl = 0s;
120+
}
121+
if (bereq.url ~ "^\/api\/stock\/") {
122+
set beresp.ttl = 900s; // 15 minutes
123+
}
124+
set beresp.http.X-Url = bereq.url;
125+
set beresp.http.X-Host = bereq.http.host;
126+
}
127+
```
128+
129+
For X-VS-Cache, I set TTL 0s so it is permanent. Because it will be automaticly invalidated when needed.
130+
131+
### Caching Extensions
132+
You might want to cache response from various extensions.
133+
E.g. I am fetching Menus, Available Countries (for checkout) from M2 by VSF-API proxy.
134+
As in this project Magento is pretty slow. By caching responses I've changed response time from ~2s
135+
to around ~50ms.
136+
137+
How to do that?
138+
Inside `vcl_recv` add:
139+
```vcl
140+
# As in my case I want to cache only GET requests
141+
if (req.method == "GET") {
142+
# Countries for storecode GET - M2 - /directory/countries
143+
if (req.url ~ "^\/api\/ext\/directory\/") {
144+
return (hash);
145+
}
146+
147+
# Menus GET - M2 - /menus & /nodes
148+
if (req.url ~ "^\/api\/ext\/menus\/") {
149+
return (hash);
150+
}
151+
}
152+
```
153+
154+
How to invalidate extension's tag?
155+
You can do it by sending request with `X-VS-Cache-Ext` header.
156+
If value of this header is part of any cached URL - it will be invalidated.
157+
E.g. for menus extension:
158+
```
159+
/api/ext/menus
160+
```
161+
You could send:
162+
BAN `http://${config.varnish.host}:${config.varnish.port}/`
163+
headers: {
164+
"X-VS-Cache-Ext": "menus"
165+
}
166+
167+
But sending HTTP requests is not so handy. So I've extended Invalidate endpoint. To the same you could just open:
168+
```
169+
http://localhost:8080/invalidate?key=aeSu7aip&ext=menus
170+
```
171+
172+
As value of the `ext` will be searched inside `Cached URL`.
173+
If you would provide here `product` it would cache product's catalog. You should have it in mind.
174+
175+
### Banning permissions
176+
It will be allowed only from certain IPs. In my case I put here only VSF-API IP. But here we have `app` as Docker will resolve it as VSF-API IP:
177+
```vcl
178+
acl purge {
179+
"app"; // IP which can BAN cache - it should be VSF-API's IP
180+
}
181+
```
182+
183+
### What to cache
184+
We should provide to Varnish - IP & Port to cache, there we have it:
185+
```vcl
186+
backend default {
187+
.host = "app";
188+
.port = "8080";
189+
}
190+
```
191+
192+
### URL
193+
Varnish by default using port `80` but by Docker's port mapping we are using `1234`
194+
195+
### How to install on VPS
196+
1. Install Varnish
197+
2. Install Varnish Modules
198+
3. By using Reverse Proxy output `/api` from Varnish, to the world
199+
200+
I'll try to prepare more detailed tutorial (with commands) as I will probably do it again in the following month.

docker/varnish/config.vcl

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
2+
3+
vcl 4.0;
4+
5+
import std;
6+
import bodyaccess;
7+
8+
acl purge {
9+
"app"; // IP which can BAN cache - it should be VSF-API's IP
10+
}
11+
12+
13+
backend default {
14+
.host = "app";
15+
.port = "8080";
16+
}
17+
18+
sub vcl_recv {
19+
unset req.http.X-Body-Len;
20+
# Only allow BAN requests from IP addresses in the 'purge' ACL.
21+
if (req.method == "BAN") {
22+
# Same ACL check as above:
23+
if (!client.ip ~ purge) {
24+
return (synth(403, "Not allowed."));
25+
}
26+
27+
# Logic for the ban, using the X-Cache-Tags header.
28+
if (req.http.X-VS-Cache-Tag) {
29+
ban("obj.http.X-VS-Cache-Tag ~ " + req.http.X-VS-Cache-Tag);
30+
}
31+
if (req.http.X-VS-Cache-Ext) {
32+
ban("req.url ~ " + req.http.X-VS-Cache-Ext);
33+
}
34+
if (!req.http.X-VS-Cache-Tag && !req.http.X-VS-Cache-Ext) {
35+
return (synth(403, "X-VS-Cache-Tag or X-VS-Cache-Ext header missing."));
36+
}
37+
38+
# Throw a synthetic page so the request won't go to the backend.
39+
return (synth(200, "Ban added."));
40+
}
41+
42+
if (req.url ~ "^\/api\/catalog\/") {
43+
if (req.method == "POST") {
44+
# It will allow me to cache by req body in the vcl_hash
45+
std.cache_req_body(500KB);
46+
set req.http.X-Body-Len = bodyaccess.len_req_body();
47+
}
48+
49+
if ((req.method == "POST" || req.method == "GET")) {
50+
return (hash);
51+
}
52+
}
53+
54+
if (req.url ~ "^\/api\/ext\/") {
55+
if (req.method == "GET") {
56+
# Custom packs GET - M2 - /jimmylion/pack/${req.params.packId}
57+
if (req.url ~ "^\/api\/ext\/custom-packs\/") {
58+
return (hash);
59+
}
60+
61+
# Countries for storecode GET - M2 - /directory/countries
62+
if (req.url ~ "^\/api\/ext\/directory\/") {
63+
return (hash);
64+
}
65+
66+
# Menus GET - M2 - /menus & /nodes
67+
if (req.url ~ "^\/api\/ext\/menus\/") {
68+
return (hash);
69+
}
70+
}
71+
}
72+
73+
if (req.url ~ "^\/api\/stock\/") {
74+
if (req.method == "GET") {
75+
# M2 Stock
76+
return (hash);
77+
}
78+
}
79+
80+
return (pipe);
81+
82+
}
83+
84+
sub vcl_hash {
85+
# To cache POST and PUT requests
86+
if (req.http.X-Body-Len) {
87+
bodyaccess.hash_req_body();
88+
} else {
89+
hash_data("");
90+
}
91+
}
92+
93+
sub vcl_backend_fetch {
94+
if (bereq.http.X-Body-Len) {
95+
set bereq.method = "POST";
96+
}
97+
}
98+
99+
sub vcl_backend_response {
100+
# Set ban-lurker friendly custom headers.
101+
if (beresp.http.X-VS-Cache && beresp.http.X-VS-Cache ~ "Miss") {
102+
set beresp.ttl = 0s;
103+
}
104+
if (bereq.url ~ "^\/api\/stock\/") {
105+
set beresp.ttl = 900s; // 15 minutes
106+
}
107+
set beresp.http.X-Url = bereq.url;
108+
set beresp.http.X-Host = bereq.http.host;
109+
}
110+
111+
sub vcl_deliver {
112+
if (obj.hits > 0) {
113+
set resp.http.X-Cache = "HIT_1";
114+
set resp.http.X-Cache-Hits = obj.hits;
115+
} else {
116+
set resp.http.X-Cache = "MISS_1";
117+
}
118+
set resp.http.X-Cache-Expires = resp.http.Expires;
119+
unset resp.http.X-Varnish;
120+
unset resp.http.Via;
121+
unset resp.http.Age;
122+
unset resp.http.X-Purge-URL;
123+
unset resp.http.X-Purge-Host;
124+
# Remove ban-lurker friendly custom headers when delivering to client.
125+
unset resp.http.X-Url;
126+
unset resp.http.X-Host;
127+
# Comment these for easier Drupal cache tag debugging in development.
128+
unset resp.http.X-Cache-Tags;
129+
unset resp.http.X-Cache-Contexts;
130+
}

0 commit comments

Comments
 (0)