├── 01_tarantool ├── 01_tarantool.md └── images │ ├── 10k-rps-txt.png │ ├── 10k-rps.png │ ├── 12k-rps-txt.png │ ├── 12k-rps.png │ ├── 14k-rps-txt.png │ ├── 14k-rps.png │ ├── 5k-rps-txt.png │ ├── 5k-rps.png │ ├── cache-fault.png │ ├── cache.png │ ├── max-rps-vs-latency.png │ ├── optimal-rps-vs-latency.png │ ├── quantilies.png │ ├── rps-vs-latency.png │ ├── tarantool.png │ └── total.png ├── 02_protocols ├── 02_protocols.md └── images │ ├── arp.png │ ├── big_network.png │ ├── change.png │ ├── dhcp.png │ ├── dns.png │ ├── ethernet.png │ ├── global.png │ ├── ip4.png │ ├── local.png │ ├── mitm.png │ ├── nat.png │ ├── network.png │ ├── protocols.png │ ├── rest.png │ ├── router.png │ ├── routes.png │ ├── switch.png │ ├── tcp_api.png │ ├── tcp_connect.png │ ├── tcp_frame.png │ ├── tcp_ops.png │ ├── tcp_vs_udp.png │ └── udp.png ├── 03_load_balancing ├── 03_load_balancing.md └── code │ ├── .gitignore │ ├── balance.lua │ ├── server.lua │ └── single.lua ├── 04_swim_raft └── 04_swim_raft.md ├── 05_replication_and_sharding └── 05_replication_and_sharding.md ├── 06_availability └── 06_availability.md ├── 07_cartridge └── 07_cartridge.md ├── 08_deploy ├── 08_deploy.md └── practice │ ├── Vagrantfile │ ├── hosts.test.yml │ └── playbook.test.yml ├── 09_testing_monitoring ├── 09_testing_monitoring.md └── materials │ ├── monitoring │ ├── project │ │ ├── .cartridge.yml │ │ ├── .editorconfig │ │ ├── .luacheckrc │ │ ├── .luacov │ │ ├── Dockerfile │ │ ├── Dockerfile.build.cartridge │ │ ├── Dockerfile.cartridge │ │ ├── app │ │ │ └── roles │ │ │ │ └── custom.lua │ │ ├── cartridge.post-build │ │ ├── cartridge.pre-build │ │ ├── cluster │ │ │ ├── helper.lua │ │ │ ├── init.lua │ │ │ └── integration │ │ │ │ └── bootstrap_test.lua │ │ ├── deps.sh │ │ ├── init.lua │ │ ├── instances.yml │ │ ├── project-scm-1.rockspec │ │ ├── stateboard.init.lua │ │ └── test │ │ │ ├── helper.lua │ │ │ ├── helper │ │ │ ├── integration.lua │ │ │ └── unit.lua │ │ │ ├── integration │ │ │ └── api_test.lua │ │ │ └── unit │ │ │ └── sample_test.lua │ ├── prometheus │ │ └── prometheus.yml │ └── wrk │ │ └── script.lua │ └── testing │ ├── .gitignore │ ├── deps.sh │ ├── init.lua │ └── test │ └── crud_test.lua ├── 10_queue └── 10_queue.md ├── LICENSE.md └── README.md /01_tarantool/images/10k-rps-txt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/01_tarantool/images/10k-rps-txt.png -------------------------------------------------------------------------------- /01_tarantool/images/10k-rps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/01_tarantool/images/10k-rps.png -------------------------------------------------------------------------------- /01_tarantool/images/12k-rps-txt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/01_tarantool/images/12k-rps-txt.png -------------------------------------------------------------------------------- /01_tarantool/images/12k-rps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/01_tarantool/images/12k-rps.png -------------------------------------------------------------------------------- /01_tarantool/images/14k-rps-txt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/01_tarantool/images/14k-rps-txt.png -------------------------------------------------------------------------------- /01_tarantool/images/14k-rps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/01_tarantool/images/14k-rps.png -------------------------------------------------------------------------------- /01_tarantool/images/5k-rps-txt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/01_tarantool/images/5k-rps-txt.png -------------------------------------------------------------------------------- /01_tarantool/images/5k-rps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/01_tarantool/images/5k-rps.png -------------------------------------------------------------------------------- /01_tarantool/images/cache-fault.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/01_tarantool/images/cache-fault.png -------------------------------------------------------------------------------- /01_tarantool/images/cache.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/01_tarantool/images/cache.png -------------------------------------------------------------------------------- /01_tarantool/images/max-rps-vs-latency.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/01_tarantool/images/max-rps-vs-latency.png -------------------------------------------------------------------------------- /01_tarantool/images/optimal-rps-vs-latency.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/01_tarantool/images/optimal-rps-vs-latency.png -------------------------------------------------------------------------------- /01_tarantool/images/quantilies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/01_tarantool/images/quantilies.png -------------------------------------------------------------------------------- /01_tarantool/images/rps-vs-latency.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/01_tarantool/images/rps-vs-latency.png -------------------------------------------------------------------------------- /01_tarantool/images/tarantool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/01_tarantool/images/tarantool.png -------------------------------------------------------------------------------- /01_tarantool/images/total.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/01_tarantool/images/total.png -------------------------------------------------------------------------------- /02_protocols/02_protocols.md: -------------------------------------------------------------------------------- 1 | ### Сети 2 | 3 | ![](images/big_network.png) 4 | 5 | ### Сетевые протоколы 6 | 7 | ![](images/protocols.png) 8 | 9 | ### Ethernet 10 | 11 | ![](images/ethernet.png) 12 | 13 | EtherType - тип протокола следующего уровня: IPv4 - 0x0800, IPv6 - 86DD, ARP - 0x0806 14 | 15 | ### MAC (Media Access Control) 16 | 17 | Уникальный идентификатор, присваиваемый каждой единице активного оборудования или некоторым их интерфейсам в компьютерных сетях Ethernet (также Hardware Address, также физический адрес) 18 | 19 | - Состоит из шести байт, обычно отображаемых в шестнадцатеричном формате 20 | - Присваевается сетевой карте производителем 21 | - Состоит из части идентификатора производителя OUI (Organizationally Unique Identifier) и идентификатора присваиваемого производителем 22 | 23 | #### Локальная сеть 24 | 25 | ![](images/local.png) 26 | 27 | 1. Клиент собирается отправить пакет на адрес 192.168.1.10 28 | 2. Чтобы отделить номер сети от номера компьютера, применяется маска подсети логическим умножением (AND) 29 | 3. Используя маску подсети клиент сравнивает адрес сервера со своим и видит, что они находятся в одной сети 30 | 4. Для передачи пакета используется ARP-протокол 31 | 32 | #### ARP (Address Resolution Protocol) 33 | 34 | ![](images/arp.png) 35 | 36 | Протокол по которому определяется MAC-адрес узла по его IP-адресу, не проходит через маршрутизаторы 37 | 38 | 1. Компьютер отправляет широковещательный запрос всем участникам локальной сети 39 | 2. Запрос содержит IP-адрес требуемого компьютера и собственный MAC-адрес 40 | 3. Все компьютеры извлекают и запоминают IP и MAC-адреса отправителя запроса 41 | 4. Компьютер с указанным IP-адресом, понимает, что запрос пришел к нему и в ответ высылает свой MAC-адрес на тот, который пришел в запросе 42 | 43 | ##### ARP-таблица 44 | 45 | ``` 46 | Protocol Address Age (min) Hardware Addr Type Interface 47 | Internet 192.168.1.1 - 0060.5C16.3B01 ARPA FastEthernet0/0 48 | Internet 192.168.1.2 6 00E0.F73D.E561 ARPA FastEthernet0/0 49 | Internet 192.168.2.1 - 0060.5C16.3B02 ARPA FastEthernet0/1 50 | Internet 192.168.2.2 7 0002.179D.455A ARPA FastEthernet0/1 51 | ``` 52 | 53 | #### Router, switch, hub 54 | 55 | ![](images/network.png) 56 | 57 | ##### Switch (коммутатор) 58 | 59 | ![](images/switch.png) 60 | 61 | - Предназначен для объединения узлов в пределах одного или нескольких сегментов сети 62 | - Хранит в памяти таблицу коммутации, в которой указывается соответствие MAC-адреса узла порту коммутатора 63 | - Отсылает пакеты данных конкретному получателю 64 | 65 | ##### Hub 66 | 67 | - Предназначен для объединения узлов в пределах одного или нескольких сегментов сети 68 | - Отсылает пакеты данных на все узлы 69 | - Вытеснен свичами 70 | 71 | ##### Router (маршрутизатор) 72 | 73 | ![](images/router.png) 74 | 75 | - Пересылает пакеты, в том числе между разными сегментами сети 76 | - Принимает решения по отправке пакетов на основании правил, заданных при настройке и топологии сети 77 | - Как правило имеет возможность настройки Firewall, DHCP, NAT, и т.д. 78 | 79 | ##### Таблица маршрутизации 80 | 81 | ![](images/routes.png) 82 | 83 | > Чем меньше metric, тем предпочтительней маршрут 84 | 85 | #### NAT (Network Address Translation)/PAT (Port Address Translation) 86 | 87 | ![](images/nat.png) 88 | 89 | - Позволяет сэкономить IP-адреса транслируя несколько внутренних IP-адресов в один внешний публичный IP-адрес 90 | - Скрывает внутреннюю структуру сети 91 | - Нет возможности из вне создать подключение к хосту за NAT 92 | - Некоторые протоколы не работают или работают плохо 93 | - Сложности с идентификацией пользователей (с вашего IP уже было соединение) 94 | 95 | #### Несколько сетей 96 | 97 | ![](images/global.png) 98 | 99 | 1. Клиент собирается отправить пакет на адрес 192.168.2.30, сравнивая адрес сервера со своим он видит, что они находятся в разных сетях, значит нужно отправить пакет на MAC-адрес шлюза (gateway) 100 | 2. Клиент создаёт пакет, указывая в нём в качестве IP отправителя свой адрес – 191.168.1.10, а в качестве IP получателя адрес сервера – 192.168.2.30 101 | 3. Пакет заворачивается во фрейм, в котором MAC-адрес отправителя AAA, а в качестве MAC-адреса получателя стоит адрес шлюза – BBB 102 | 4. Шлюз-маршрутизатор по MAC BBB понимает, что фрейм ему, достаёт из него пакет и согласно своей таблице маршрутизации, принимает решение о пересылке дальше 103 | 5. Маршрутизатор запаковывает тот же пакет но в новый фрейм с MAC отправителя – BBB, MAC получателя – CCC, содержимое заголовка IP пакета не меняется – в нём по-прежнему адрес отправителя 192.168.1.10, а адрес получателя – 192.168.2.30 104 | 6. Другой маршрутизатор получает фрейм, по MAC определяет, что фрейм предназначается ему, распаковывает и обработатывает фрейм 105 | 7. Из фрейма извлекается IP пакет. Из адреса получателя видно, что пакет идёт в сеть 192.168.2.x, которая непосредственно подключена к этому шлюзу-маршрутизатору 106 | 8. Пакет переупаковывается в новый фрейм в котором MAC-адрес отправителя CCC, а в качестве MAC-адреса получателя стоит DDD 107 | 9. Фрейм отправляется в последнюю локальную сеть 192.168.3.x 108 | 10. Сервер получает фрейм, по MAC (DDD) определяет, что это фрейм для него, распаковывает фрейм и достаёт из него пакет, в пакете его IP (192.168.2.30), значит можно передать фрейм локально на обработку 109 | 110 | ### IP (Internet Protocol) 111 | 112 | - Без гарантии доставки 113 | - Без сохранения порядка следования сообщений 114 | 115 | ![](images/ip4.png) 116 | 117 | - Время жизни (TTL, Time To Live) – максимальное время, в течение которого пакет может перемещаться по сети. Введено для предотвращения «бесконечного» продвижения пакетов. Каждый маршрутизатор уменьшает значение на 1 118 | - Протокол: TCP – 6, UDP – 17, ICMP – 1 119 | 120 | ### Протокол ICMP (Internet Control Message Protocol) 121 | 122 | Так как протокол IP предоставляет сервис передачи данных без гарантии доставки, то в случае ошибки при передаче пакета никаких действий не предпринимается 123 | 124 | - Оповещение об ошибках на сетевом уровне 125 | - Тестирование работоспособности сети 126 | 127 | #### Тестирование работы сети: 128 | 129 | ##### ping 130 | 131 | Проверка доступности компьютера в сети: 132 | - Эхо-запрос 133 | - Эхо-ответ 134 | 135 | ``` 136 | ping 127.0.0.1 137 | ``` 138 | 139 | ``` 140 | PING 127.0.0.1 (127.0.0.1): 56 data bytes 141 | 64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.074 ms 142 | 64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.043 ms 143 | ^C 144 | --- 127.0.0.1 ping statistics --- 145 | 2 packets transmitted, 2 packets received, 0.0% packet loss 146 | round-trip min/avg/max/stddev = 0.043/0.058/0.074/0.015 ms 147 | ``` 148 | 149 | ##### traceroute 150 | 151 | - Определяет маршрут от отправителя к получателю (делает серию запроса с ttl=1 постепенно увеличивая значение) 152 | - Находит адреса всех маршрутизаторов, через которые проходит пакет 153 | 154 | ``` 155 | traceroute ya.ru 156 | ``` 157 | 158 | ``` 159 | traceroute to ya.ru (87.250.250.242), 64 hops max, 52 byte packets 160 | 1 gpon.net (192.168.1.1) 2.400 ms 1.807 ms 1.163 ms 161 | 2 100.94.0.1 (100.94.0.1) 3.039 ms 3.555 ms 2.734 ms 162 | 3 mpts-a197-51.msk.mts-internet.net (212.188.1.106) 5.434 ms 4.421 ms 5.173 ms 163 | 4 a197-cr04-be12.51.msk.mts-internet.net (212.188.1.105) 13.603 ms 4.434 ms 4.396 ms 164 | 5 212.188.33.199 (212.188.33.199) 3.856 ms 3.797 ms 3.721 ms 165 | 6 10.4.6.1 (10.4.6.1) 145.973 ms * * 166 | 7 ya.ru (87.250.250.242) 14.935 ms 7.382 ms 7.348 ms 167 | ``` 168 | 169 | ### Ручная установка IP 170 | 171 | ```bash 172 | ip addr show 173 | ``` 174 | 175 | ```bash 176 | ip a 177 | ``` 178 | 179 | ``` 180 | lo0: flags=8049 mtu 16384 181 | inet 127.0.0.1/8 lo0 182 | inet6 ::1/128 183 | inet6 fe80::1/64 scopeid 0x1 184 | en0: flags=8863 mtu 1500 185 | ether 8c:85:90:7f:de:62 186 | inet 192.168.1.8/24 brd 192.168.1.255 en0 187 | ``` 188 | 189 | > lo0 - loopback - виртуальный сетевой интерфейс, любой трафик посланный на него возвращается обратно 190 | > inet - ip v4 191 | > inet6 - ip v6 192 | > ether - ethernet, он же mac-адрес 193 | 194 | ```bash 195 | ip addr add 192.168.1.88 dev en0 196 | ``` 197 | 198 | ```bash 199 | ip addr del 192.168.1.88 dev en0 200 | ``` 201 | 202 | ### DHCP (Dynamic Host Configuration Protocol) 203 | 204 | ![](images/dhcp.png) 205 | 206 | Сетевой протокол, позволяющий сетевым устройствам автоматически получать IP-адрес и другие параметры, необходимые для работы в сети TCP/IP. Требуется DHCP сервер 207 | 208 | ### DNS (Domain Name System) 209 | 210 | Система DNS позволяет преобразовывать имена компьютеров в IP-адреса. Представляет собой распределенное хранилище ключей и значений, если сервер не может предоставить значение по ключу, то происходит делегирование другому серверу 211 | 212 | ``` 213 | ya.ru -> 87.250.250.242 214 | ``` 215 | 216 | ##### /etc/hosts 217 | 218 | ``` 219 | # Host Database 220 | # 221 | # localhost is used to configure the loopback interface 222 | # when the system is booting. Do not change this entry. 223 | ## 224 | 127.0.0.1 localhost 225 | 255.255.255.255 broadcasthost 226 | ::1 localhost 227 | # Added by Docker Desktop 228 | # To allow the same kube context to work on the host and the container: 229 | 127.0.0.1 kubernetes.docker.internal 230 | # End of section 231 | ``` 232 | 233 | ##### Система DNS 234 | 235 | - Нет единого сервера, на котором описываются имена хостов 236 | - Пространство имен разделено на отдельные части – домены 237 | - За каждый домен отвечает отдельная организация 238 | - Распределением доменных имен занимаются регистраторы 239 | - Регистратор корневого домена один - Internet Corporation for Assigned Names and Numbers (ICAN) 240 | 241 | ![](images/dns.png) 242 | 243 | ``` 244 | dig ya.ru 245 | ``` 246 | 247 | ``` 248 | ;; QUESTION SECTION: 249 | ;ya.ru. IN A 250 | 251 | ;; ANSWER SECTION: 252 | ya.ru. 128 IN A 87.250.250.242 253 | ``` 254 | 255 | Имя хоста 256 | 257 | Время сколько нужно держать значение в кеше перед повторным запросом, секунд (TTL) 258 | 259 | Класс записи - в каких сетях используется: 260 | - IN - Интернет 261 | 262 | Типы записей для IP-адресов: 263 | - A - IPv4 адрес компьютера 264 | - AAAA - IPv6 адрес компьютера 265 | - NS - имя следующего DNS сервера 266 | 267 | Адрес хоста 268 | 269 | ``` 270 | dig +trace ya.ru 271 | ``` 272 | 273 | ``` 274 | . 81982 IN NS j.root-servers.net. 275 | . 81982 IN NS h.root-servers.net. 276 | . 81982 IN NS c.root-servers.net. 277 | . 81982 IN NS k.root-servers.net. 278 | . 81982 IN NS e.root-servers.net. 279 | . 81982 IN NS a.root-servers.net. 280 | . 81982 IN NS b.root-servers.net. 281 | . 81982 IN NS d.root-servers.net. 282 | . 81982 IN NS l.root-servers.net. 283 | . 81982 IN NS f.root-servers.net. 284 | . 81982 IN NS i.root-servers.net. 285 | . 81982 IN NS g.root-servers.net. 286 | . 81982 IN NS m.root-servers.net. 287 | 288 | ru. 172800 IN NS a.dns.ripn.net. 289 | ru. 172800 IN NS b.dns.ripn.net. 290 | ru. 172800 IN NS d.dns.ripn.net. 291 | ru. 172800 IN NS e.dns.ripn.net. 292 | ru. 172800 IN NS f.dns.ripn.net. 293 | 294 | ya.ru. 600 IN A 87.250.250.242 295 | ya.ru. 7200 IN NS ns1.yandex.ru. 296 | ya.ru. 7200 IN NS ns2.yandex.ru. 297 | ``` 298 | 299 | ##### MX запись 300 | 301 | ``` 302 | dig ya.ru mx 303 | ``` 304 | 305 | ``` 306 | ya.ru. 3461 IN MX 10 mx.yandex.ru. 307 | ``` 308 | 309 | Связывает доменное имя с почтовым сервером 310 | 311 | ##### CNAME запись (canonical name) 312 | 313 | ``` 314 | dig www.ya.ru 315 | ``` 316 | 317 | ``` 318 | www.ya.ru. 76 IN CNAME ya.ru. 319 | ya.ru. 213 IN A 87.250.250.242 320 | ``` 321 | 322 | - Связывает поддомен с каноническим именем домена 323 | - Можно использовать для создания псевдонимов 324 | 325 | ##### TXT запись 326 | 327 | Любой текст, например, используется для проверки прав на владение доменом сторонними сервисами: 328 | 329 | ``` 330 | _acme-challenge.sub.domain.ru. T4Fk9N0KCLiJelOcXjE_ycCLK8SwVaqAFHYoCt0sEn8 331 | ``` 332 | 333 | ### TCP (Transmission Control Protocol) 334 | 335 | ![](images/tcp_frame.png) 336 | 337 | Протокол надежной передачи потока байт: 338 | 339 | - Гарантии доставки 340 | - Сохранения порядка следования 341 | 342 | Для гарантии доставки TCP использует подтверждение получения данных. Получатель, после приема очередной порции данных, передает отправителю подтверждения о получении. В случае, если подтверждение не пришло, отправитель передает данные еще раз. 343 | 344 | Для сохранения порядка следования сообщений используется нумерация сообщений. Нумерация сообщений позволяет расставить перепутанные сегменты в правильном порядке, а также не учитывать дублирующиеся сегменты. 345 | 346 | #### Зарезервированные номера портов 347 | 348 | Port | Protocol 349 | -----|----------------------------------------- 350 | 22 | Secure Shell (SSH) 351 | 25 | Простой протокол передачи почты (SMTP) 352 | 53 | Система доменных имен (DNS) 353 | 80 | Протокол передачи гипертекста (HTTP) 354 | 123 | Протокол сетевого времени (NTP) 355 | 143 | Internet Message Access Protocol (IMAP) 356 | 161 | Простой протокол управления сетью (SNMP) 357 | 443 | HTTP Secure (HTTPS) 358 | 359 | #### Процесс передачи данных в TCP 360 | 361 | ![](images/tcp_connect.png) 362 | 363 | - Установка соединения 364 | - Отправитель посылает сообщение SYN + номер байта 365 | - Получатель отвечает сообщением SYN + номер следующего ожидаемого байта, ACK 366 | - Отправитель посылает сообщение, ACK 367 | - Соединение установлено 368 | - Передача данных 369 | - Разрыв соединения 370 | - Соединение в TCP дуплексное 371 | - Данные могут передаваться в обе стороны 372 | - Схема разрыва соединения 373 | - Одновременное (обе стороны разорвали соединение) 374 | - Одностороннее (одна сторона прекращает передавать данные, но может принимать) 375 | - Варианты разрыва соединения 376 | - Одностороннее закрытие (FIN) 377 | - Отправитель посылает сообщение FIN 378 | - Получатель отвечает сообщение ACK 379 | - Получатель посылает сообщение FIN 380 | - Отправитель посылает сообщение ACK 381 | - Разрыв из-за критической ситуации (RST) 382 | - Отправитель посылает сообщение RST 383 | 384 | #### Программный интерфейс сокетов 385 | 386 | ![](images/tcp_api.png) 387 | 388 | ![](images/tcp_ops.png) 389 | 390 | > read = receive 391 | > write = send 392 | 393 | ### UDP (User Datagram Protocol) 394 | 395 | - Нет соединения 396 | - Нет гарантии доставки данных 397 | - Нет гарантии сохранения порядка сообщений 398 | 399 | ![](images/udp.png) 400 | 401 | > Розовым выделены поля необязательные к использованию в IPv4 402 | 403 | ### TCP vs UDP 404 | 405 | ![](images/tcp_vs_udp.png) 406 | 407 | ### lsof 408 | 409 | ``` 410 | lsof -i :9000 411 | ``` 412 | 413 | ``` 414 | COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME 415 | tarantool 10153 m.trempoltcev 12u IPv6 0xf476a11fe6679cfd 0t0 TCP localhost:cslistener (LISTEN) 416 | ``` 417 | 418 | ### Telnet (teletype network) 419 | 420 | Сетевой протокол для передачи текстовых сообщений при помощи транспорта TCP 421 | 422 | ### HTTP (Hypertext Transfer Protocol) 423 | 424 | #### URI (Uniform Resource Identifier) 425 | 426 | ``` 427 | протокол://доменное имя/путь 428 | ``` 429 | 430 | ``` 431 | http://www.example.ru/same/path 432 | https://www.youtube.com/ 433 | ftp://example.com 434 | http://www.ietf.org/rfc/rfc959.txt 435 | ``` 436 | 437 | #### Структура запроса и ответа 438 | 439 | ``` 440 | [METHOD] /[RESOURCE] HTTP/[VERSION] 441 | GET / HTTP/1.1 442 | ``` 443 | 444 | ``` 445 | HTTP/[VERSION] [CODE] [STATUS NAME] 446 | 200 ОК. 447 | ``` 448 | 449 | #### Заголовки 450 | 451 | ``` 452 | [HEADER NAME]: [VALUE] 453 | ``` 454 | 455 | ``` 456 | Host: www.yandex.ru 457 | Content-Type: text/html; charset=UTF-8 458 | Content-Length: 5161 459 | Connection: keep-alive 460 | If-Modified-Since: Wed, 25 May 2016 06:13:24 GMT 461 | ``` 462 | 463 | #### Методы HTTP 464 | 465 | Метод HTTP является идемпотентным, если повторный идентичный запрос, сделанный один или несколько раз подряд, имеет один и тот же эффект, не изменяющий состояние сервера 466 | 467 | Метод HTTP является безопасным, если не изменяет состояние сервера 468 | 469 | Метод | Описание | Идемпотентность | Безопасный 470 | --------|------------------------------------------------|-----------------|----------- 471 | GET | Запрос данных | Да | Да 472 | POST | Передача данных | Нет | Нет 473 | HEAD | Запрос заголовка страницы | Да | Да 474 | PUT | Помещение ресурса на сервер | Да | Нет 475 | DELETE | Удаление ресурса с сервера | Да | Нет 476 | TRACE | Трассировка | Да | Да 477 | OPTIONS | Запрос поддерживаемых методов HTTP для ресурса | Да | Да 478 | CONNECT | Подключение к серверу через прокси | Нет | Нет 479 | 480 | #### Коды HTTP 481 | 482 | Код | Класс | Назначение 483 | ----|-----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 484 | 1xx | Информационный | Информирование о процессе передачи. В HTTP/1.1 — клиент должен быть готов принять этот класс сообщений как обычный ответ, но ничего отправлять серверу не нужно 485 | 2xx | Успех | Информирование о случаях успешного принятия и обработки запроса клиента 486 | 3xx | Перенаправление | Сообщает клиенту, что для успешного выполнения операции необходимо сделать другой запрос (как правило по другому URI). Из данного класса пять кодов 301, 302, 303, 305 и 307 относятся непосредственно к перенаправлениям (редирект). Адрес, по которому клиенту следует произвести запрос, сервер указывает в заголовке Location 487 | 4xx | Ошибка клиента | Указание ошибок со стороны клиента. При использовании всех методов, кроме HEAD, сервер должен вернуть в теле сообщения гипертекстовое пояснение для пользователя 488 | 5xx | Ошибка сервера | Информирование о случаях неудачного выполнения операции по вине сервера. Для всех ситуаций, кроме использования метода HEAD, сервер должен включать в тело сообщения объяснение для клиента 489 | 490 | ##### Наиболее распространенные коды 491 | 492 | Код | Значение 493 | ----|---------------------- 494 | 200 | OK 495 | 201 | Created 496 | 301 | Moved Temporarily 497 | 304 | Not Modified 498 | 400 | Bad Request 499 | 401 | Unauthorized 500 | 404 | Not Found 501 | 500 | Internal Server Error 502 | 502 | Bad Gateway 503 | 503 | Service Unavailable 504 | 504 | Gateway Timeout 505 | 506 | ### HTTPS 507 | 508 | Расширение протокола HTTP для поддержки шифрования в целях повышения безопасности 509 | 510 | ![](images/mitm.png) 511 | 512 | #### SSL/TLS (Transport Layer Security) 513 | 514 | Протоколы защиты транспортного уровня 515 | 516 | - Клиент подключается к серверу, поддерживающему TLS, и запрашивает защищённое соединение 517 | - Клиент предоставляет список поддерживаемых алгоритмов шифрования и хеш-функций 518 | - Сервер выбирает из списка, предоставленного клиентом, наиболее надёжные алгоритмы среди тех, которые поддерживаются сервером, и сообщает о своём выборе клиенту 519 | - Сервер отправляет клиенту цифровой сертификат для собственной аутентификации. Обычно цифровой сертификат содержит имя сервера, имя удостоверяющего центра сертификации и открытый ключ сервера 520 | - Клиент, до начала передачи данных, проверяет валидность (аутентичность) полученного серверного сертификата относительно имеющихся у клиента корневых сертификатов удостоверяющих центров (центров сертификации). Клиент также может проверить, не отозван ли серверный сертификат, связавшись с сервисом доверенного удостоверяющего центра 521 | - Для шифрования сессии используется сеансовый ключ. Получение общего секретного сеансового ключа клиентом и сервером проводится по протоколу Диффи-Хеллмана. Существует исторический метод передачи сгенерированного клиентом секрета на сервер при помощи шифрования асимметричной криптосистемой RSA (используется ключ из сертификата сервера). Данный метод не рекомендован, но иногда продолжает встречаться на практике 522 | 523 | ![](images/change.png) 524 | 525 | ### REST (Representational state transfer) 526 | 527 | Несколько принципов построения интерфейса обмена данных с сервером 528 | 529 | ![](images/rest.png) 530 | 531 | - Используем методы HTTP 532 | - Добавляем данные методом POST 533 | - Получаем методом GET 534 | - Изменяем методом PATCH 535 | - Удаляем методом DELETE 536 | - Данные передаем и получаем через тело (body) 537 | - Передаем аргументы через HTTP Query String 538 | 539 | #### Query String 540 | 541 | ``` 542 | /api/search?user_id=3987 543 | /api/search?start=50&limit=50 544 | ``` 545 | 546 | ### Домашнее задание 547 | 548 | Написать на Тарантуле прокси-сервер работающий по протоколу HTTP: 549 | - Сервер должен открыть порт для подключения 550 | - Полученный запрос передать на указанный хост 551 | - Полученный ответ вернуть клиенту 552 | - Настройки хранить в файле config.yml в формате YAML 553 | 554 | Пример config.yml: 555 | 556 | ```yaml 557 | --- 558 | proxy: 559 | port: 80 560 | bypass: 561 | host: localhost 562 | port: 8080 563 | ``` 564 | -------------------------------------------------------------------------------- /02_protocols/images/arp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/02_protocols/images/arp.png -------------------------------------------------------------------------------- /02_protocols/images/big_network.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/02_protocols/images/big_network.png -------------------------------------------------------------------------------- /02_protocols/images/change.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/02_protocols/images/change.png -------------------------------------------------------------------------------- /02_protocols/images/dhcp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/02_protocols/images/dhcp.png -------------------------------------------------------------------------------- /02_protocols/images/dns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/02_protocols/images/dns.png -------------------------------------------------------------------------------- /02_protocols/images/ethernet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/02_protocols/images/ethernet.png -------------------------------------------------------------------------------- /02_protocols/images/global.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/02_protocols/images/global.png -------------------------------------------------------------------------------- /02_protocols/images/ip4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/02_protocols/images/ip4.png -------------------------------------------------------------------------------- /02_protocols/images/local.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/02_protocols/images/local.png -------------------------------------------------------------------------------- /02_protocols/images/mitm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/02_protocols/images/mitm.png -------------------------------------------------------------------------------- /02_protocols/images/nat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/02_protocols/images/nat.png -------------------------------------------------------------------------------- /02_protocols/images/network.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/02_protocols/images/network.png -------------------------------------------------------------------------------- /02_protocols/images/protocols.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/02_protocols/images/protocols.png -------------------------------------------------------------------------------- /02_protocols/images/rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/02_protocols/images/rest.png -------------------------------------------------------------------------------- /02_protocols/images/router.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/02_protocols/images/router.png -------------------------------------------------------------------------------- /02_protocols/images/routes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/02_protocols/images/routes.png -------------------------------------------------------------------------------- /02_protocols/images/switch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/02_protocols/images/switch.png -------------------------------------------------------------------------------- /02_protocols/images/tcp_api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/02_protocols/images/tcp_api.png -------------------------------------------------------------------------------- /02_protocols/images/tcp_connect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/02_protocols/images/tcp_connect.png -------------------------------------------------------------------------------- /02_protocols/images/tcp_frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/02_protocols/images/tcp_frame.png -------------------------------------------------------------------------------- /02_protocols/images/tcp_ops.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/02_protocols/images/tcp_ops.png -------------------------------------------------------------------------------- /02_protocols/images/tcp_vs_udp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/02_protocols/images/tcp_vs_udp.png -------------------------------------------------------------------------------- /02_protocols/images/udp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtrempoltsev/tarantool_highload/864334719a45d4f62e0afe04a95daf23b2636130/02_protocols/images/udp.png -------------------------------------------------------------------------------- /03_load_balancing/03_load_balancing.md: -------------------------------------------------------------------------------- 1 | ### Балансировка нагрузки 2 | 3 | Вопрос балансировки нагрузки остро встает, когда мощностей одного сервера 4 | перестает хватать для обслуживания входящих запросов. 5 | В таком случае есть два варианта: 6 | - Вертикальное масштабирование — увеличение мощности одного сервера. 7 | Это может достигаться покупкой более мощного сервера или апгрейдом уже 8 | существующего. В любом случае это достаточно затратный путь — невозможно бесконечно 9 | масштабировать один сервер. Кроме того, сервер представляет из себя единую точку отказа — 10 | запросы перестают обрабатываться, если такой сервер по какой-то причине выйдет из строя. 11 | - Горизонтальное масштабирование — наращивание мощности путем увеличения количества серверов 12 | и распределения нагрузки между ними. В данном случае мы можем не вкладывать деньги в 13 | один большой сервер, а просто покупать несколько «средних» по мощности серверов. 14 | При этом если из строя выйдет один из серверов, то запросы всё равно продолжат обрабатываться, 15 | просто увеличится нагрузка на другие сервера. 16 | 17 | Балансировка нагрузки бывает двух типов: 18 | - Аппаратная 19 | - Программная 20 | 21 | Аппаратная балансировка нагрузки производится специальным устройством, 22 | специально предназначенным для распределения сетевого трафика. 23 | Так же такие устройства обычно заточены для работы с шифрованным трафиком, 24 | и это является довольно привлекательной возможностью - аппаратное 25 | шифрование обычно быстрее программного. 26 | Однако в нашем курсе мы не будем останавливаться на аппаратной 27 | балансировке нагрузки. 28 | 29 | Программная балансировка нагрузки производится при помощи 30 | специального программного обеспечения. Этот подход 31 | является в чем-то более гибким — требуется просто зарезервировать 32 | отдельный сервер, который будет распределять входящие запросы по другим серверам. 33 | Перечень подобного программного обеспечения в настоящее время довольно широк [1]. 34 | Поэтому при выборе решения стоит сразу учитывать специфику своей задачи 35 | и выбирать [2]: 36 | - Уровень, на котором будет происходить распределение входящего трафика. 37 | - Алгоритм балансировки 38 | 39 | ### Три уровня балансировки 40 | 41 | ![image](https://user-images.githubusercontent.com/8830475/111399035-97ed4f00-86d5-11eb-9efd-3c60d72c8e6a.png) 42 | 43 | Процедура балансировки осуществляется при помощи целого комплекса алгоритмов и методов, соответствующим следующим уровням модели OSI: 44 | - сетевому; 45 | - транспортному; 46 | - прикладному. 47 | 48 | #### Балансировка на сетевом уровне (L3) 49 | Балансировка на сетевом уровне предполагает решение следующей задачи: нужно сделать так, чтобы за один конкретный IP-адрес сервера отвечали разные физические машины. Такая балансировка может осуществляться с помощью множества разнообразных способов. 50 | 51 | * DNS-балансировка. На одно доменное имя выделяется несколько IP-адресов. 52 | Сервер, на который будет направлен клиентский запрос, обычно определяется с помощью алгоритма Round Robin 53 | (о методах и алгоритмах балансировки будет подробно рассказано ниже). 54 | * Балансировка по IP с использованием дополнительного маршрутизатора. 55 | * Балансировка по территориальному признаку осуществляется путём размещения одинаковых 56 | сервисов с одинаковыми адресами в территориально различных регионах Интернета. 57 | 58 | #### Балансировка на транспортном уровне (L4) 59 | Этот вид балансировки является самым простым: клиент обращается к балансировщику, 60 | тот перенаправляет запрос одному из серверов, который и будет его обрабатывать. 61 | Выбор сервера, на котором будет обрабатываться запрос, может осуществляться в соответствии с самыми разными алгоритмами 62 | (об этом ещё пойдёт речь ниже): путём простого кругового перебора, путём выбора наименее загруженного сервера из пула и т.п. 63 | Иногда балансировку на транспортном уровне сложно отличить от балансировки на сетевом уровне. 64 | 65 | Различие между уровнями балансировки можно объяснить следующим образом. 66 | К сетевому уровню относятся решения, которые не терминируют на себе пользовательские сессии. 67 | Они просто перенаправляют трафик и не работают в проксирующем режиме. 68 | На сетевом уровне балансировщик просто решает, на какой сервер передавать пакеты. 69 | Сессию с клиентом осуществляет сервер. 70 | 71 | На транспортном уровне общение с клиентом замыкается на балансировщике, который работает как прокси. 72 | Он взаимодействует с серверами от своего имени, передавая информацию о клиенте в дополнительных данных и заголовках. 73 | Таким образом работает, например, популярный программный балансировщик HAProxy. 74 | 75 | #### Балансировка на прикладном уровне (L7) 76 | При работе на прикладном уровне балансировщик работает в режиме «умного прокси». 77 | Он анализирует клиентские запросы и перенаправляет их на разные серверы в зависимости от характера запрашиваемого контента. 78 | Так работает, например, веб-сервер Nginx, распределяя запросы между фронтендом и бэкендом. 79 | За балансировку в Nginx отвечает модуль Upstream. 80 | 81 | ### Алгоритмы балансировки нагрузки 82 | Существует много различных алгоритмов и методов балансировки нагрузки. Выбирая конкретный алгоритм, нужно исходить, во-первых, из специфики конкретного проекта, а во-вторых — из целей. которые мы планируем достичь. 83 | 84 | В числе целей, для достижения которых используется балансировка, нужно выделить следующие: 85 | 86 | - справедливость: нужно гарантировать, чтобы на обработку каждого запроса выделялись системные ресурсы и не допустить возникновения ситуаций, когда один запрос обрабатывается, а все остальные ждут своей очереди; 87 | - эффективность: все серверы, которые обрабатывают запросы, должны быть заняты на 100%; желательно не допускать ситуации, когда один из серверов простаивает в ожидании запросов на обработку (сразу же оговоримся, что в реальной практике эта цель достигается далеко не всегда); 88 | - сокращение времени выполнения запроса: нужно обеспечить минимальное время между началом обработки запроса (или его постановкой в очередь на обработку) и его завершения; 89 | - сокращение времени отклика: нужно минимизировать время ответа на запрос пользователя. 90 | 91 | Очень желательно также, чтобы алгоритм балансировки обладал следующими свойствами: 92 | 93 | - предсказуемость: нужно чётко понимать, в каких ситуациях и при каких нагрузках алгоритм будет эффективным для решения поставленных задач; 94 | - равномерная загрузка ресурсов системы; 95 | - масштабируемость: алгоритм должен сохранять работоспособность при увеличении нагрузки. 96 | 97 | #### Round Robin 98 | 99 | ![image](https://user-images.githubusercontent.com/8830475/112002930-7525c500-8b31-11eb-9c4a-e2a83aeff44f.png) 100 | 101 | Round Robin, или алгоритм кругового обслуживания, представляет собой перебор по круговому циклу: первый запрос передаётся одному серверу, затем следующий запрос передаётся другому и так до достижения последнего сервера, а затем всё начинается сначала. 102 | Алгоритм достаточно прост, но имеет целый ряд существенных недостатков. 103 | Чтобы распределение нагрузки по этому алгоритму отвечало упомянутым выше критериями справедливости и эффективности, нужно, чтобы у каждого сервера был в наличии одинаковый набор ресурсов. 104 | При выполнении всех операций также должно быть задействовано одинаковое количество ресурсов. 105 | В реальной практике эти условия в большинстве случаев оказываются невыполнимыми. 106 | 107 | Также при балансировке по алгоритму Round Robin совершенно не учитывается загруженность того или иного сервера в составе кластера. 108 | Представим себе следующую гипотетическую ситуацию: один из узлов загружен на 100%, в то время как другие — всего на 10 - 15%. 109 | Алгоритм Round Robin возможности возникновения такой ситуации не учитывает в принципе, поэтому перегруженный узел все равно будет получать запросы. 110 | Ни о какой справедливости, эффективности и предсказуемости в таком случае не может быть и речи. 111 | В силу описанных выше обстоятельств сфера применения алгоритма Round Robin весьма ограничена. 112 | 113 | #### Weighted Round Robin 114 | 115 | ![image](https://user-images.githubusercontent.com/8830475/112003029-91296680-8b31-11eb-85cb-b52b59f7aacc.png) 116 | 117 | Это — усовершенствованная версия алгоритма Round Robin. 118 | Суть усовершенствований заключается в следующем: каждому серверу присваивается весовой коэффициент в соответствии с его производительностью и мощностью. 119 | Это помогает распределять нагрузку более гибко: серверы с большим весом обрабатывают больше запросов. 120 | Однако всех проблем с отказоустойчивостью это отнюдь не решает. 121 | Более эффективную балансировку обеспечивают другие методы, 122 | в которых при планировании и распределении нагрузки учитывается большее количество параметров. 123 | 124 | #### Least Connections 125 | 126 | ![image](https://user-images.githubusercontent.com/8830475/112003340-dfd70080-8b31-11eb-80a7-04c0f5ca6a06.png) 127 | 128 | В предыдущем разделе мы перечислили основные недостатки алгоритма Round Robin. 129 | Назовём ещё один: в нём совершенно не учитывается количество активных на данный момент подключений. 130 | 131 | Рассмотрим практический пример. Имеется два сервера — обозначим их условно как А и Б. 132 | К серверу А подключено меньше пользователей, чем к серверу Б. 133 | При этом сервер А оказывается более перегруженным. 134 | Как это возможно? Ответ достаточно прост: подключения к серверу А поддерживаются в течение более долгого времени по сравнению с подключениями к серверу Б. 135 | 136 | Описанную проблему можно решить с помощью алгоритма, известного под названием least connections (сокращённо — leastconn). 137 | Он учитывает количество подключений, поддерживаемых серверами в текущий момент времени. 138 | Каждый следующий запрос передаётся серверу с наименьшим количеством активных подключений. 139 | 140 | Существует усовершенствованный вариант этого алгоритма, предназначенный в первую очередь для использования в кластерах, 141 | состоящих из серверов с разными техническими характеристиками и разной производительностью. 142 | Он называется Weighted Least Connections и учитывает при распределении нагрузки не только количество активных подключений, 143 | но и весовой коэффициент серверов. 144 | 145 | В числе других усовершенствованных вариантов алгоритма Least Connections следует прежде всего выделить Locality-Based Least Connection Scheduling и Locality-Based Least Connection Scheduling with Replication Scheduling. 146 | 147 | Первый метод был создан специально для кэширующих прокси-серверов. 148 | Его суть заключается в следующем: наибольшее количество запросов передаётся серверам с наименьшим количеством активных подключений. 149 | За каждым из клиентских серверов закрепляется группа клиентских IP. 150 | Запросы с этих IP направляются на «родной» сервер, если он не загружен полностью. 151 | В противном случае запрос будет перенаправлен на другой сервер (он должен быть загружен менее чем наполовину). 152 | 153 | В алгоритме Locality-Based Least Connection Scheduling with Replication Scheduling каждый IP-адрес или группа IP-адресов закрепляется не за отдельным сервером, 154 | а за целой группой серверов. 155 | Запрос передаётся наименее загруженному серверу из группы. 156 | Если же все серверы из «родной» группы перегружены, то будет зарезервирован новый сервер. 157 | Этот новый сервер будет добавлен к группе, обслуживающей IP, с которого был отправлен запрос. 158 | В свою очередь наиболее загруженный сервер из этой группы будет удалён — это позволяет избежать избыточной репликации. 159 | 160 | #### Destination Hash Scheduling и Source Hash Scheduling 161 | Алгоритм Destination Hash Scheduling был создан для работы с кластером кэширующих прокси-серверов, 162 | но он часто используется и в других случаях. 163 | В этом алгоритме сервер, обрабатывающий запрос, выбирается из статической таблицы по IP-адресу получателя. 164 | 165 | Алгоритм Source Hash Scheduling основывается на тех же самых принципах, что и предыдущий, только сервер, 166 | который будет обрабатывать запрос, выбирается из таблицы по IP-адресу отправителя. 167 | 168 | #### Sticky Sessions 169 | 170 | ![image](https://user-images.githubusercontent.com/8830475/112003752-3e03e380-8b32-11eb-8158-1c60a67023bf.png) 171 | 172 | Sticky Sessions — алгоритм распределения входящих запросов, при котором соединения передаются на один и тот же сервер группы. Он используется, например, в веб-сервере Nginx. 173 | Сессии пользователя могут быть закреплены за конкретным сервером с помощью метода IP hash. 174 | С помощью этого метода запросы распределяются по серверам на основе IP-адреса клиента. Как указано в документации, «метод гарантирует, что запросы одного и того же клиента будет передаваться на один и тот же сервер». 175 | Если закреплённый за конкретным адресом сервер недоступен, запрос будет перенаправлен на другой сервер. 176 | 177 | Применение этого метода сопряжено с некоторыми проблемами. 178 | Проблемы с привязкой сессий могут возникнуть, если клиент использует динамический IP. 179 | В ситуации, когда большое количество запросов проходит через один прокси-сервер, балансировку вряд ли можно назвать эффективной и справедливой. 180 | Описанные проблемы, однако, можно решить, используя cookies. 181 | 182 | ### Пример 183 | Начнем с простого примера. Он довольно далек от реальной жизни, 184 | но демонстрирует проблему, возникающую перед нами. 185 | Создадим простой http-сервер. 186 | 187 | ```lua 188 | #!/usr/bin/env tarantool 189 | 190 | local digest = require('digest') 191 | local http_server = require('http.server') 192 | 193 | local function handler() 194 | local str = string.format("I'm %s", 8080) 195 | for _ = 1, 1e2 do 196 | digest.sha512_hex(str) 197 | end 198 | 199 | return { 200 | body = str, 201 | status = 200, 202 | } 203 | end 204 | 205 | local httpd = http_server.new('0.0.0.0', '8080', {log_requests = false}) 206 | httpd:route({method = 'GET', path = '/'}, handler) 207 | httpd:start() 208 | ``` 209 | 210 | Сервер просто считает 100 раз хэш от строки. 211 | При этом хэш криптографический, а значит вычислительно трудоемкий. 212 | Для того, чтобы продемонстрировать, сколько запросов может такой сервер выполнить 213 | мы воспользуемся специальной программой - wrk. Это инструмент нагрузочного тестирования. 214 | 215 | ```lua 216 | -- load.lua 217 | function request() 218 | return wrk.format('GET', '/') 219 | end 220 | ``` 221 | 222 | Запустим сервер `tarantool -i single.lua`, а затем начнем делать запросы - 223 | `wrk -c 60 -d 15s -t 4 -s load.lua http://localhost:8080`. 224 | 225 | Получается следующий результат (сильно зависящий от того, где именно вы тестируете): 226 | ``` 227 | Running 15s test @ http://localhost:8080 228 | 4 threads and 60 connections 229 | Thread Stats Avg Stdev Max +/- Stdev 230 | Latency 84.54ms 146.38ms 1.95s 96.88% 231 | Req/Sec 234.64 45.47 435.00 80.23% 232 | 14027 requests in 15.10s, 1.71MB read 233 | Socket errors: connect 0, read 0, write 0, timeout 3 234 | Requests/sec: 929.02 235 | Transfer/sec: 116.13KB 236 | ``` 237 | 238 | При этом, заглянув во время теста в `htop`, мы увидим, что наш сервер грузит 239 | CPU на 100%. Попробуем в первом приближении решить данную проблему. 240 | 241 | ### Свой балансировщик на Tarantool 242 | 243 | #### Iproto 244 | 245 | В лекции про протоколы мы рассматривали различные способы 246 | взаимодействия между клиентом и сервером, 247 | в частности на примере протокола HTTP. 248 | В Tarantool есть свой протокол общения инстансов 249 | друг с другом - [iproto](https://www.tarantool.io/en/doc/latest/dev_guide/internals/box_protocol/). 250 | 251 | Iproto работает поверх TCP. И в отличие от HTTP v1.1 он бинарный и асинхронный. 252 | При этом передаем не просто какие-то сырые бинарные данные, 253 | а в специальном формате MessagePack. 254 | 255 | ![image](https://user-images.githubusercontent.com/8830475/111397695-f6fd9480-86d2-11eb-97d3-d75eb179022b.png) 256 | 257 | На практике, вызов `box.cfg({listen = uri})` поднимает 258 | iproto-сервер. Далее мы можем подключиться к этому серверу 259 | с помощью встроенного модуля `net.box`. 260 | 261 | ``` 262 | -- Instance 1 263 | box.cfg{listen = 3301} 264 | box.schema.user.passwd('admin', 'test') 265 | 266 | function sum(a, b) 267 | local res = a + b 268 | print(res) 269 | return res 270 | end 271 | 272 | box.schema.space.create('test') 273 | box.space.test:create_index('pk') 274 | box.space.test:replace({1, 2, 3}) 275 | ``` 276 | 277 | ``` 278 | -- Instance 2 279 | netbox = require('net.box') 280 | 281 | -- Устанавливаем соединение 282 | c1 = netbox.connect('admin:test@localhost:3301') 283 | -- или 284 | c2 = netbox.connect('localhost:3301', {user = 'admin', password = 'test'}) 285 | 286 | -- Вызов хранимых процедур 287 | c2:call('sum', {1, 2}, {timeout = 5}) 288 | 289 | -- Ping 290 | c2:ping() 291 | 292 | -- box-like доступ к данным 293 | c2.space.test:select() 294 | c2.space.test:update({1}, {{'+', 2, 1}}) 295 | ``` 296 | 297 | #### Сам код 298 | 299 | Чуть модифицируем сервер. 300 | ```lua 301 | #!/usr/bin/env tarantool 302 | 303 | local fio = require('fio') 304 | local digest = require('digest') 305 | local port = tonumber(arg[1]) 306 | if port == nil then 307 | error('Invalid port') 308 | end 309 | 310 | local work_dir = fio.pathjoin('data', port) 311 | fio.mktree(work_dir) 312 | box.cfg({ 313 | listen = port, 314 | work_dir = work_dir, 315 | }) 316 | box.schema.user.passwd('admin', 'test') 317 | 318 | function exec() 319 | local str = string.format("I'm %s", port) 320 | for _ = 1, 1e2 do 321 | digest.sha512_hex(str) 322 | end 323 | return str 324 | end 325 | ``` 326 | 327 | Задача балансировщика — отправка запрос на один из трех серверов. 328 | С помощью алгоритма round robin. 329 | ```lua 330 | #!/usr/bin/env tarantool 331 | 332 | local log = require('log') 333 | local netbox = require('net.box') 334 | local http_server = require('http.server') 335 | 336 | local hosts = { 337 | 'admin:test@localhost:3301', 338 | 'admin:test@localhost:3302', 339 | 'admin:test@localhost:3303', 340 | } 341 | 342 | local connections = {} 343 | for _, host in ipairs(hosts) do 344 | local conn = netbox.connect(host) 345 | assert(conn) 346 | log.info('Connected to %s', host) 347 | table.insert(connections, conn) 348 | end 349 | 350 | local req_num = 1 351 | local function handler() 352 | local conn = connections[req_num] 353 | if req_num == #connections then 354 | req_num = 1 355 | else 356 | req_num = req_num + 1 357 | end 358 | 359 | local result = conn:call('exec') 360 | 361 | return { 362 | body = result, 363 | status = 200, 364 | } 365 | end 366 | 367 | local httpd = http_server.new('0.0.0.0', '8080', {log_requests = false}) 368 | httpd:route({method = 'GET', path = '/'}, handler) 369 | httpd:start() 370 | ``` 371 | 372 | Какие проблемы у данного балансировщика? 373 | Как их можно решить? 374 | 375 | Тем не менее результат лучше, чем в случае с одним инстансом: 376 | ``` 377 | ➜ wrk -c 60 -d 15s -t 4 -s load.lua http://localhost:8080 378 | Running 15s test @ http://localhost:8080 379 | 4 threads and 60 connections 380 | Thread Stats Avg Stdev Max +/- Stdev 381 | Latency 24.21ms 13.07ms 89.46ms 66.54% 382 | Req/Sec 631.96 82.51 830.00 76.50% 383 | 37908 requests in 15.08s, 4.63MB read 384 | Requests/sec: 2514.24 385 | Transfer/sec: 314.28KB 386 | ``` 387 | 388 | ### HAProxy 389 | 390 | ![image](https://user-images.githubusercontent.com/8830475/111398778-244b4200-86d5-11eb-9462-fb0600ed2a08.png) 391 | 392 | ```bash 393 | apt install haproxy 394 | yum install haproxy 395 | brew install haproxy 396 | git clone https://github.com/haproxy/haproxy 397 | ``` 398 | 399 | Файл конфигурации - `/etc/haproxy/haproxy.cfg`. 400 | 401 | Тестовая конфигурация: 402 | ``` 403 | global 404 | log /dev/log local0 405 | log /dev/log local1 notice 406 | chroot /var/lib/haproxy 407 | stats timeout 30s 408 | user 409 | group 410 | daemon 411 | 412 | defaults 413 | log global 414 | mode http 415 | option httplog 416 | option dontlognull 417 | timeout connect 5000 418 | timeout client 50000 419 | timeout server 50000 420 | 421 | frontend http_front 422 | bind *:8080 423 | stats uri /haproxy?stats 424 | default_backend http_back 425 | 426 | backend http_back 427 | balance roundrobin 428 | server server_name1 localhost:8081 check 429 | server server_name2 localhost:8082 check 430 | server server_name3 localhost:8083 check 431 | ``` 432 | 433 | Тестовые сервера будут всё так же на Tarantool. 434 | ```lua 435 | #!/usr/bin/env tarantool 436 | 437 | local port = tonumber(arg[1]) 438 | if port == nil then 439 | error('Invalid port') 440 | end 441 | 442 | local http_server = require('http.server') 443 | 444 | local function handler() 445 | return { 446 | body = string.format("I'm %s", port), 447 | status = 200, 448 | } 449 | end 450 | 451 | local httpd = http_server.new('0.0.0.0', port, {log_requests = false}) 452 | httpd:route({path = '/'}, handler) 453 | httpd:start() 454 | ``` 455 | 456 | И поднимем наши http-сервера на портах 8081-8083. 457 | Спама в логах бояться не стоит — проблема известная: https://github.com/tarantool/http/issues/71. 458 | Запустим haproxy при помощи `haproxy -f haproxy.cfg -V -d` - в режиме дебага. 459 | Теперь совершая запросы на порт 8080 можно видеть, что наши запросы балансируются 460 | с помощью алгоритма round robin между нашими серверами. 461 | Статистика по запросам будет доступна на `http://localhost:8080/haproxy?stats`. 462 | 463 | #### Конфигурация 464 | Настройка HAProxy обычно сосредоточена вокруг пяти разделов: global, defaults, frontend, backend, реже listen. 465 | 466 | Раздел **global** определяет общую конфигурацию для всего HAProxy 467 | 468 | **Defaults** определяет настройки по-умолчанию для остальных разделов проксирования. 469 | 470 | Раздел **listen** объединяет в себе описание для фронтенда и бэкенда и содержит полный список прокси. Он полезен для TCP трафика. 471 | 472 | Раздел **Frontend** определяет, каким образом перенаправлять запросы к бэкенду в зависимости от того, что за запрос поступил от клиента. 473 | 474 | Секция **Backend** содержит список серверов и отвечает за балансировку нагрузки между ними в зависимости от выбранного алгоритма 475 | 476 | ##### Frontend 477 | * В нашем примере у секции frontend выбран **mode** __http__ - мы работаем на прикладном уровне. 478 | Кроме этого, доступен и __tcp__ mode для работы с транспортным уровнем. 479 | 480 | * bind. IP Адрес и порт, которые HAProxy должен слушать «на входе». 481 | 482 | * default_backend. Название конфигурации группы серверов, к которым надо направить запрос для обработки. В данном случае запросы пойдут к backend с названием http_back. 483 | 484 | ##### Backend 485 | В данной секции указан алгоритм балансировки - RoundRobin. 486 | А также список серверов, между которыми мы будем балансировать запросы. 487 | 488 | Список возможностей не ограничивается только лишь балансировкой. 489 | Типичными кейсами являются также и редирект пользователей с HTTP на HTTPS. 490 | Или с одного адреса на другой. 491 | Можно добавить небольшой фрагмент в конфиг и убедиться, 492 | что теперь при запросе на :8079 мы будем получать 301 и редирект на 8080. 493 | ``` 494 | frontend http_front_redirect 495 | bind *:8079 496 | mode http 497 | http-request redirect code 301 prefix localhost:8080 498 | ``` 499 | 500 | Более подробно о настройке HAProxy можно почитать в документации - 501 | https://www.haproxy.org/download/2.4/doc/configuration.txt. 502 | 503 | Кроме того, функциональность HAProxy под ваши нужды можно расширить 504 | при помощи скриптов на языке Lua - https://www.haproxy.com/blog/5-ways-to-extend-haproxy-with-lua/. 505 | (Для этого, правда, скорее всего придется собрать HAProxy из исходного кода 506 | со специальным флагом). 507 | 508 | 509 | ### Nginx 510 | 511 | ![image](https://user-images.githubusercontent.com/8830475/111398705-0251bf80-86d5-11eb-8fda-6731e61c2ec6.png) 512 | 513 | Этот веб-сервер считается одним из самых популярных и производительных решений, 514 | ведь имеет широчайший функционал и гибкость при настройке. 515 | В том числе Nginx часто используется для балансировки нагрузки. 516 | 517 | ```bash 518 | apt install nginx 519 | yum install nginx 520 | brew install nginx 521 | git clone https://github.com/nginx/nginx 522 | ``` 523 | 524 | Конфигурация лежит в `/usr/local/etc/nginx/nginx.conf`. 525 | Проверить корректность конфигурации `nginx -t` и перезапустить сервер `nginx -s reload`. 526 | 527 | ``` 528 | events { 529 | worker_connections 1024; 530 | } 531 | 532 | http { 533 | upstream myapp1 { 534 | server localhost:8081; 535 | server localhost:8082; 536 | server localhost:8083; 537 | } 538 | 539 | server { 540 | listen 8080; 541 | 542 | location / { 543 | proxy_pass http://myapp1; 544 | } 545 | } 546 | } 547 | ``` 548 | 549 | [Доступные опции](https://nginx.org/en/docs/http/load_balancing.html): 550 | 551 | 552 | * **least_conn** - задаёт для группы метод балансировки нагрузки, при котором запрос передаётся серверу с наименьшим числом активных соединений, с учётом весов серверов. Если подходит сразу несколько серверов, они выбираются циклически (в режиме round-robin) с учётом их весов. 553 | ``` 554 | upstream myapp1 { 555 | least_conn; 556 | server localhost:8081; 557 | server localhost:8082; 558 | server localhost:8083; 559 | } 560 | ``` 561 | 562 | * **weight** - задаёт вес сервера, по умолчанию 1. 563 | 564 | ``` 565 | upstream myapp1 { 566 | server localhost:8081 weight=3; 567 | server localhost:8082; 568 | server localhost:8083; 569 | } 570 | ``` 571 | 572 | В качестве тестовых серверов будем использовать HTTP-сервера на Tarantool, 573 | код которых приведен в предыдущем примере. 574 | 575 | 576 | ### Домашнее задание 577 | 578 | Написать балансировщик на Tarantool: 579 | - Запросы принимаются по протоколу HTTP - GET на `/`; 580 | - Каждый запрос должен возвращать ответ в формате `: <количество запросов за последнюю секунду на данный сервер>`; 581 | - Предусмотреть ситуацию, когда один из серверов может упасть — система должна продолжить распределять запросы между оставшимися серверами; 582 | - (*) Ограничить количество одновременных запросов, приходящих на балансировщик (по умолчанию 1000, но сделать конфигурируемым). 583 | Возвращать status code 429 при превышении лимита; 584 | - Предусмотреть возможность динамически (через консоль) добавлять и удалять сервера. 585 | 586 | 587 | (*) - необязательная часть задания для получения доп. баллов. 588 | 589 | ### Ссылки 590 | 591 | [1] https://xakep.ru/2014/03/15/62207/ 592 | [2] https://selectel.ru/blog/balansirovka-nagruzki-osnovnye-algoritmy-i-metody/ 593 | -------------------------------------------------------------------------------- /03_load_balancing/code/.gitignore: -------------------------------------------------------------------------------- 1 | data 2 | -------------------------------------------------------------------------------- /03_load_balancing/code/balance.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | local log = require('log') 4 | local netbox = require('net.box') 5 | local http_server = require('http.server') 6 | 7 | -- 8 | -- HTTP server that distributes requests between IProto servers. 9 | -- to run `tarantool -i balance.lua` 10 | -- Following instances should be started before this HTTP server. 11 | -- 12 | 13 | local hosts = { 14 | 'admin:test@localhost:3301', 15 | 'admin:test@localhost:3302', 16 | 'admin:test@localhost:3303', 17 | } 18 | 19 | local connections = {} 20 | for _, host in ipairs(hosts) do 21 | local conn = netbox.connect(host) 22 | assert(conn) 23 | log.info('Connected to %s', host) 24 | table.insert(connections, conn) 25 | end 26 | 27 | local req_num = 1 28 | local function handler() 29 | local conn = connections[req_num] 30 | 31 | if req_num == #connections then 32 | req_num = 1 33 | else 34 | req_num = req_num + 1 35 | end 36 | 37 | local result = conn:call('exec') 38 | 39 | return { 40 | body = result, 41 | status = 200, 42 | } 43 | end 44 | 45 | local httpd = http_server.new('0.0.0.0', '8080', {log_requests = false}) 46 | httpd:route({method = 'GET', path = '/'}, handler) 47 | httpd:start() 48 | -------------------------------------------------------------------------------- /03_load_balancing/code/server.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | -- 4 | -- IProto-server 5 | -- Should be used to distribute input requests using load balancer. 6 | -- to run `tarantool -i server.lua `. should be 3301, 3302, 3303. 7 | -- 8 | 9 | local fio = require('fio') 10 | local digest = require('digest') 11 | local port = tonumber(arg[1]) 12 | if port == nil then 13 | error('Invalid port') 14 | end 15 | 16 | local work_dir = fio.pathjoin('data', port) 17 | fio.mktree(work_dir) 18 | box.cfg({ 19 | listen = port, 20 | work_dir = work_dir, 21 | }) 22 | box.schema.user.passwd('admin', 'test') 23 | 24 | function exec() 25 | local str = string.format("I'm %s", port) 26 | for _ = 1, 1e2 do 27 | digest.sha512_hex(str) 28 | end 29 | return str 30 | end 31 | -------------------------------------------------------------------------------- /03_load_balancing/code/single.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | -- 4 | -- Single server that processes user's requests. 5 | -- 6 | 7 | local digest = require('digest') 8 | local http_server = require('http.server') 9 | 10 | local function handler() 11 | local str = string.format("I'm %s", 8080) 12 | for _ = 1, 1e2 do 13 | digest.sha512_hex(str) 14 | end 15 | 16 | return { 17 | body = str, 18 | status = 200, 19 | } 20 | end 21 | 22 | local httpd = http_server.new('0.0.0.0', '8080', {log_requests = false}) 23 | httpd:route({method = 'GET', path = '/'}, handler) 24 | httpd:start() 25 | -------------------------------------------------------------------------------- /04_swim_raft/04_swim_raft.md: -------------------------------------------------------------------------------- 1 | ## SWIM 2 | 3 | ### Gossip-протоколы 4 | 5 | Одной из проблем, возникающей при работе с распределенными системами 6 | является достижение консенсуса. 7 | Т.е. возможность принятия одного решения всеми узлами кластера. 8 | Но, кроме этого, перед нами стоит и другая проблема — обнаружение отказов 9 | в распределенных системах. 10 | Мы не застрахованы от проблем с электричеством, оборудованием, программным 11 | обеспечением и т.д. Поэтому необходимо своевременно обнаруживать отказы 12 | узлов кластера и как-то на них реагировать: исправлять, выводить из кластера, 13 | добавлять новые узлы... 14 | Нам необходим алгоритм, который бы мог всё это делать. 15 | Решением "в лоб" является широковещательная рассылка сообщений всем узлам кластера. 16 | Однако это создает проблемы масштабирования нашей системы - 17 | у таких алгоритмов квадратичная сложность, которая создает 18 | нежелательную нагрузку на сеть. Поэтому с такими алгоритмами обычно 19 | никто не работает. 20 | Вместо этого предлагается использовать т.н. "gossip"-алгоритмы - 21 | алгоритмы распространения слухов. 22 | ![image](https://user-images.githubusercontent.com/8830475/108864759-90211a00-7603-11eb-93d7-c458188daeda.png) 23 | 24 | Данные алгоритмы дают линейную нагрузку на сеть и 25 | в общем случае позволяют распространять любую информацию 26 | в кластере. Однако обычно нас интересует именно обнаружение отказов. 27 | 28 | ![image](https://user-images.githubusercontent.com/8830475/108866233-0e31f080-7605-11eb-8327-5370eed87dff.png) 29 | 30 | Также при использовании протокола UDP мы не можем быть уверены, 31 | что наш PING будет доставлен. Кроме этого, сеть между узлами может 32 | быть повреждена - в случае прямых пингов узлы думали, что те, 33 | кому они не смогли отправить PING не работают. 34 | По факту же между узлами просто нет сети. 35 | Решить данную проблему можно с помощью непрямых (indirect) пингов. 36 | 37 | ![image](https://user-images.githubusercontent.com/8830475/108866468-4a655100-7605-11eb-872a-279ad6d12d61.png) 38 | 39 | Гарантии, которые обычно дают нам данные протоколы: 40 | * Константное время получение информации хотя бы одним узлом - O(1) 41 | * Логарифмическое время распространения информации по всему кластеру - O(lnN) 42 | N - количество узлов в кластере. 43 | 44 | ![image](https://user-images.githubusercontent.com/8830475/108867895-b5fbee00-7606-11eb-82f7-21a59efd360d.png) 45 | 46 | 47 | ### SWIM (Scalable Weakly-consistent Infection-style Process Group Membership Protocol) 48 | 49 | * Scalable - как и любой gossip-протокол SWIM хорошо масштабируется; 50 | * Weakly-consistent - между узлами нет явной синхронизации; узлы рано или поздно синхронизуются с помощью слухов; 51 | * Infection-style Process - "gossip"; 52 | * Group Membership - каждый узел имеет список узлов, которые его видят. 53 | 54 | В нормально функционирующем SWIM'e узлы просто отправляют друг другу PING'и. 55 | Узлы считаются живыми - "alive". 56 | Однако если в какой-то момент один узел не ответил на PING, он становится подозрительным - 57 | "suspected". Нода обращается к соседним нодам с просьбой пингануть данный узел. 58 | Если хотя бы одна нода смогла пингануть данный узел, слух не подтверждается. 59 | Ввиду нестабильности UDP (потеря пакетов нормальна), узел даже не сразу 60 | будет считаться мертвым - "dead". Какое-то время его ещё будут пытаться пинговать. 61 | 62 | ![image](https://user-images.githubusercontent.com/8830475/108868977-ccef1000-7607-11eb-94fe-b4fa34ef16fc.png) 63 | 64 | ### Инкарнация 65 | 66 | Инкарнация состоит из двух частей - generation и version. 67 | Generation - персистентная часть инкарнации, пользователь может задавать её 68 | вручную (по умолчанию используется время в момент создания инстанса). 69 | Version - часть, которая изменяется автоматически — увеличивается при 70 | некоторых событиях. 71 | 72 | ![image](https://user-images.githubusercontent.com/8830475/108869689-7afaba00-7608-11eb-941b-8a56ec2ad2c6.png) 73 | 74 | Допустим, у нас есть 3 узла. Узел B отказал. И узел A считает узел B мертвым, а 75 | узел С считает B живым. 76 | Они обмениваются этой информацией. Предполагается худшее — узел С начнет считать B 77 | мертвым. 78 | При этом, если B начнет слать "опровержения" слухов, 79 | никто ему верить не будет - мы не знаем, действительно ли узел B жив или 80 | это старые затерявшиеся UDP пакеты начали до нас доходить. 81 | 82 | Для опровержения слухов B, если поймет, что его считают мертвым, 83 | должен будет увеличивать счетчик инкарнации (а конкретно версию). 84 | Увеличение generation будет свидетельствовать, например, о том, 85 | что инстанс перезапустили — при этом UDP пакеты, 86 | отправленные предыдущей версией будут отбрасываться. 87 | 88 | ![image](https://user-images.githubusercontent.com/8830475/108870699-65d25b00-7609-11eb-9d88-570682f6e3bc.png) 89 | 90 | Несколько выводов из вышесказанного: 91 | * Инкарнация необходима для опровержения ложных слухов 92 | * Используется, в том числе, и как защита от проблем с UDP 93 | * Если слухи равны — предполагаем худшее 94 | 95 | #### Реализация в Tarantool (встроенный модуль SWIM) 96 | 97 | Сам по себе алгоритм достаточно простой, однако при реализации в Tarantool он имеет несколько особенностей: 98 | * Работа делится на раунды — нет "честной" рандомизации, есть "справедливая". 99 | В начале "раунда" таблица участников перемешивается и далее в соответствии с получившейся 100 | очередью рассылаются PING'и; 101 | * Адресация узлов по UUID. Т.е. uri, IP, port - можно менять; 102 | * Реализован на C, но доступен в виде Lua-модуля. 103 | 104 | #### Антиэнтропия 105 | 106 | Мы рассмотрели случай отказа узлов, но в общем случае SWIM 107 | подходит для распространения информации о событиях в кластере. 108 | Допустим, у нас было 2 узла, и мы добавили третий. 109 | Теперь узел B знает про A и С, С знает про B, а А только про B. 110 | Как теперь узлу A узнать про C? B должен сообщить A о новом узле, 111 | но у нас нет гарантии, что это сообщение когда-либо дойдет. 112 | Кроме этого, событие будет распространяться 113 | в течение ограниченного времени — не бесконечно. 114 | В таком случае возможна ситуация, что узел А так никогда не узнает про С. 115 | 116 | Решение — антиэнтропия. 117 | Мы добавляем в UDP-пакет некоторую часть таблицы участников кластера. 118 | Таким образом рано или поздно узел А узнает об узле C. 119 | Работает данная возможность автоматически и не требует никакой настройки. 120 | 121 | ![image](https://user-images.githubusercontent.com/8830475/108873118-ed20ce00-760b-11eb-8601-5a56eb94fde1.png) 122 | 123 | #### Payload 124 | 125 | Возможность передавать некоторую полезную информацию по UDP. 126 | Например, адрес узла для подключения по TCP. 127 | Payload ограничен по размеру — работаем с UDP. 128 | 129 | #### Шифрование 130 | 131 | SWIM можно использовать в любой открытой сети, а передаваемый трафик шифровать. 132 | Ключи шифрования необходимо указывать самостоятельно для каждого узла. 133 | 134 | #### Выход из кластера 135 | 136 | Если узел выводят из кластера, то в оригинальном SWIM 137 | узел, перед тем, как считаться мертвым должны будут пингануть, 138 | признать мертвым - т.е. это продолжительный процесс. 139 | Однако если мы руками выводим ноду из кластера и не хотим создавать 140 | лишних слухов, есть возможность сделать это "вежливо". 141 | 142 | ### Демонстрация 143 | 144 | Запустим следующий код 145 | ```lua 146 | -- tarantool -i init.lua {3301,3302,3303} 147 | 148 | local port = tonumber(arg[1]) 149 | if port == nil then 150 | error('Invalid port') 151 | end 152 | 153 | _G.swim = require('swim').new() 154 | 155 | local instance_uuid = '804f00ed-271c-47fa-844e-df4c6e0d' .. tostring(port) 156 | 157 | _G.swim:cfg({ 158 | uuid = instance_uuid, 159 | uri = port, 160 | heartbeat_rate = 1, 161 | ack_timeout = 0.1, 162 | gc_mode = 'off', 163 | }) 164 | 165 | function get_members() 166 | local result = {} 167 | for k, v in _G.swim:pairs() do 168 | result[k] = v 169 | end 170 | return result 171 | end 172 | ``` 173 | 174 | Здесь специально `ack_timeout` выставлен в небольшое значение (по умолчанию `30`), 175 | чтобы эффект от манипуляций был виден сразу. 176 | `gc_mode = 'off'` означает, что модуль не будет автоматически удалять мертвых member'ов. 177 | 178 | Посмотрим на `get_members` на одном из инстансов. 179 | ```lua 180 | tarantool> get_members() 181 | --- 182 | - 804f00ed-271c-47fa-844e-df4c6e0d3303: 183 | uri: 127.0.0.1:3303 184 | status: alive 185 | incarnation: cdata {generation = 1614776861620271ULL, version = 0ULL} 186 | uuid: 804f00ed-271c-47fa-844e-df4c6e0d3303 187 | payload_size: 0 188 | ... 189 | ``` 190 | 191 | Попробуем добавить новых member'ов. 192 | ```lua 193 | tarantool> swim:probe_member(3301) 194 | --- 195 | - true 196 | ... 197 | 198 | tarantool> swim:add_member({uuid = '804f00ed-271c-47fa-844e-df4c6e0d3302', uri = 3302}) 199 | --- 200 | - true 201 | ... 202 | 203 | tarantool> get_members() 204 | --- 205 | - 804f00ed-271c-47fa-844e-df4c6e0d3301: 206 | uri: 127.0.0.1:3301 207 | status: alive 208 | incarnation: cdata {generation = 1614776857103623ULL, version = 0ULL} 209 | uuid: 804f00ed-271c-47fa-844e-df4c6e0d3301 210 | payload_size: 0 211 | 804f00ed-271c-47fa-844e-df4c6e0d3303: 212 | uri: 127.0.0.1:3303 213 | status: alive 214 | incarnation: cdata {generation = 1614776861620271ULL, version = 0ULL} 215 | uuid: 804f00ed-271c-47fa-844e-df4c6e0d3303 216 | payload_size: 0 217 | 804f00ed-271c-47fa-844e-df4c6e0d3302: 218 | uri: 127.0.0.1:3302 219 | status: alive 220 | incarnation: cdata {generation = 1614776859674371ULL, version = 0ULL} 221 | uuid: 804f00ed-271c-47fa-844e-df4c6e0d3302 222 | payload_size: 0 223 | ... 224 | ``` 225 | 226 | Теперь, если вызвать `get_members` на других узлах, мы увидим такой же результат. 227 | Здесь операция добавления нового member'a была сделана двумя разными путями. 228 | `probe_member` принимает только uri, но не дает гарантий, что будет 229 | что-то добавлено. Если member'a с таким uri не существует, мы всё равно получим `true`. 230 | `add_member` принимает ещё и uuid, но добавляет member'a в таблицу даже если его физически 231 | не существует. 232 | 233 | Попробуем выключить одну из нод. Спустя какое-то время увидим, что `status` у этой ноды изменился на `dead`. 234 | Если включить ноду обратно, то увидим, что она станет `alive`, при этом `generation` изменится. 235 | 236 | Это один из примеров события. Есть и другие — например, изменение payload'a, 237 | появление/удаление инстанса из кластера, изменение статуса... 238 | Обрабатывать эти события можно с помощью специального триггера. 239 | `swim:on_member_event(function(m, e) ... end)` - устанавливает функцию-обработчик, 240 | member'a - источник события и объект-событие. 241 | 242 | ```lua 243 | function on_event(m, e) 244 | if e:is_new_status() then 245 | print('is_new_status:', m:uuid(), m:status()) 246 | end 247 | if e:is_new_uri() then 248 | print('is_new_uri:', m:uuid(), m:uri()) 249 | end 250 | if e:is_new_incarnation() then 251 | print('is_new_incarnation:', m:uuid(), m:incarnation()) 252 | end 253 | if e:is_new_payload() then 254 | print('is_new_payload:', m:uuid(), m:payload()) 255 | end 256 | if e:is_drop() then 257 | print('is_drop:', m:uuid()) 258 | end 259 | end 260 | swim:on_member_event(on_event) 261 | ``` 262 | 263 | ### Модуль membership 264 | 265 | Существует реализация протокола SWIM на чистом Lua. Она появилась исторически раньше, и 266 | является используемой в текущих проектах. 267 | Подробнее в репозитории [tarantool/membership](https://github.com/tarantool/membership). 268 | Интерфейс похож на встроенный модуль, но является синглтоном — этого достаточно. 269 | 270 | ### Альтернативы 271 | 272 | SWIM используется во многих продуктах, причем обычно с 273 | различными улучшениями и дополнениям (часто это зависит от задачи). 274 | Существуют и другие типы gossip-протоколов. Так, например, 275 | Cassandra работает по похожему принципу: ноды периодически 276 | пингуют друг друга. Однако выделяется особый класс узлов - 277 | "seed nodes", которые служат внешними координаторами в 278 | процессе распространения слухов и при старте кластера. 279 | 280 | ## RAFT 281 | 282 | ### Необходимость в лидере 283 | При работе в распределенных системах пред нами достаточно остро стоит 284 | вопрос синхронизации данных между узлами. 285 | Начнем с репликации. Репликация — процесс синхронизации данных между несколькими 286 | узлами. Всего существует 2 типа репликации — асинхронная и синхронная. 287 | Асинхронная репликация не дает нам особых гарантий сохранности записанных данных. 288 | Нам просто необходимо записать данные в локальный журнал. Далее эти данные асинхронно 289 | реплицируются на другие узлы. При этом у нас нет гарантий, что данные-таки будут 290 | записаны на эти узлы — сразу после подтверждения транзакции инстанс может выйти из строя, 291 | не успев отреплицировать данные. Клиент получил ОК — транзакция закоммичена. 292 | Но по факту со смертью узла мы потеряли эту транзакцию. 293 | 294 | Более надежный подход — использование синхронной репликации. 295 | Все "пишущие" транзакции приходят на инстанс-лидер. Который не просто записывает 296 | транзакции в локальный журнал, но и дожидается, пока часть других инстансов 297 | применит эту транзакцию у себя. 298 | 299 | Проблемы начинаются в тот момент, когда лидер выходит из строя. 300 | В этом случае необходимо выбрать нового лидера. Это можно делать и руками, 301 | но всё-таки хочется это делать автоматически. Тогда нам на помощь 302 | приходит алгоритм RAFT. Это не единственный алгоритм, как и в случае с 303 | gossip-протоколами, существует целое семейство таких алгоритмов. 304 | 305 | Raft включает в себя синхронную репликацию журнала и выборы с гарантией 306 | единственности лидера на протяжении всего времени существования кластера. 307 | Обычно кластер выглядит так: один узел находится в состоянии leader и может писать в журнал, 308 | применять транзакции и так далее. Все остальные узлы находятся в состоянии follower и применяют всё, 309 | что получают от лидера. Если follower получит по каналу репликации какую-то транзакцию от не-лидера, 310 | он её проигнорирует. 311 | 312 | Часть протокола, касающаяся репликации журнала, достаточно проста. 313 | Текущий лидер рассылает всем членам кластера запросы AppendEntries, содержащие новые записи. 314 | Как только более половины членов кластера успешно применят записи, 315 | отправленные лидером, эти записи считаются подтверждёнными. 316 | 317 | Всё время существования кластера разделено на логические блоки, называемые термами (term). 318 | Они пронумерованы целыми числами начиная с 1, и каждый терм начинается с выборов нового лидера. 319 | После того как лидер выбран, он принимает запросы и сохраняет в журнал новые записи, 320 | которые рассылает остальным членам кластера. 321 | 322 | Чтобы узел был выбран лидером, за него должны проголосовать более половины узлов в кластере. 323 | Это гарантирует, что в каждом терме будет выбран либо единственный лидер, либо никто. 324 | В некоторых термах выборы могут так и не закончиться назначением лидера. Такое может произойти, 325 | если все узлы проголосовали, но ни один из кандидатов не получил большинство голосов. 326 | В таком случае начнётся новый терм, а значит будут проведены новые выборы. 327 | Все узлы проголосуют заново. Таким образом, рано или поздно один из узлов станет лидером. 328 | 329 | Согласно Raft каждый узел может быть в одном из трёх состояний — **follower, candidate, leader**. 330 | 331 | **Follower** — состояние, в котором узел может только отвечать на запросы AppendEntries 332 | от лидера и RequestVote от кандидатов. Если follower давно не получал ничего от лидера 333 | (в течение так называемого Election Timeout), то он переходит в состояние candidate и начинает новый терм, 334 | а вместе с тем и новые выборы. 335 | 336 | **Candidate** — состояние узла, инициировавшего новые выборы. В этом случае узел голосует сам за себя, 337 | а затем рассылает всем членам кластера запросы RequestVote. Ответ на этот запрос — поле VoteGranted, 338 | принимающее значение true, если узел проголосовал за кандидата. 339 | Сам кандидат никогда не отвечает на запросы RequestVote других кандидатов. 340 | Он уже проголосовал за себя и больше ни за кого не голосует. 341 | Кандидат ведёт подсчёт отданных за него голосов. Как только их становится больше, 342 | чем половина всех узлов в кластере, кандидат становится лидером, о чём сообщает всем рассылкой пустого запроса 343 | AppendEntries (своеобразный хартбит, который может послать только лидер). 344 | 345 | **Leader** — единственное состояние, в котором узел может писать в журнал. 346 | Лидер реплицирует этот журнал с помощью запроса AppendEntries. 347 | Кроме того, в случае, когда новые запросы не поступают, лидер периодически рассылает хартбиты 348 | (пустые запросы AppendEntries), чтобы избежать наступления таймаута и выдвижения новых кандидатов. 349 | 350 | ![image](https://user-images.githubusercontent.com/8830475/109615037-76705d00-7b44-11eb-8647-e4742e5a1013.png) 351 | 352 | ![image](https://user-images.githubusercontent.com/8830475/109615160-9ef85700-7b44-11eb-81cc-1501e139008b.png) 353 | 354 | ![image](https://user-images.githubusercontent.com/8830475/109615208-b33c5400-7b44-11eb-9ffe-28a2455f1aa0.png) 355 | 356 | ### Визуализация 357 | 358 | https://raft.github.io/ 359 | 360 | ### RAFT в Tarantool 361 | Для понимания того, как работает RAFT необходимо начать с синхронной репликации. 362 | Синхронными являются отдельные транзакции, которые затрагивают синхронные 363 | спейсы. Спейс синхронный, если при его создании был задан флаг `is_sync = true`. 364 | Информация об операциях над данными попадает в журнал. При этом для случая 365 | асинхронной репликации у нас были обычные - update/delete/replace/..., то 366 | в синхронном случае Tarantool сначала записывает транзакцию локально, потом после 367 | получения подтверждения записывает команду - COMMIT. В случае неудачи - ROLLBACK. 368 | 369 | Для настройки синхронной репликации у нас есть 2 опции: 370 | ```lua 371 | box.cfg{ 372 | replication_synchro_quorum = 'N/2 + 1', 373 | replication_synchro_timeout = 30, 374 | } 375 | ``` 376 | 377 | И для настройки работы протокола RAFT - 2: 378 | ```lua 379 | box.cfg{ 380 | election_mode = 'off|voter|candidate', 381 | election_timeout = 5, 382 | } 383 | ``` 384 | 385 | ### Демонстрация 386 | Запустим следующий скрипт на нескольких инстансах. 387 | 388 | ```lua 389 | -- tarantool init.lua {3301,3302,3303,3304} 390 | #!/usr/bin/env tarantool 391 | local fio = require('fio') 392 | local port = tonumber(arg[1]) 393 | if port == nil then 394 | error('Invalid port') 395 | end 396 | 397 | local work_dir = fio.pathjoin('data', port) 398 | fio.mktree(work_dir) 399 | 400 | box.cfg({ 401 | listen = port, 402 | work_dir = work_dir, 403 | read_only = (port ~= 3301), 404 | replication = {'3301', '3302', '3303', '3304'}, 405 | replication_synchro_quorum = 'N / 2 + 1', 406 | election_mode = 'off' 407 | }) 408 | 409 | if port == 3301 then 410 | box.schema.user.passwd('admin', 'test') 411 | box.schema.user.grant('guest', 'replication', nil, nil, {if_not_exists=true}) 412 | box.schema.user.grant('guest', 'read,write,execute', 'universe', nil, {if_not_exists=true}) 413 | end 414 | 415 | require('console').start() 416 | ``` 417 | 418 | С помощью `replication={'3301', '3302', '3303', '3304'}` мы объединяем наши инстансы в один репликасет. 419 | При этом запись возможна только на инстансе, у которого порт `3301`. 420 | Изначально raft отключен. Включим его, вызвав на первом инстансе `box.cfg({election_mode = 'candidate'})`, 421 | на других `box.cfg({election_mode = 'voter'})`. Спустя какое-то время первый инстанс будет выбран лидером. 422 | Убедиться в этом можно с помощью `box.info.election`. 423 | Теперь стоит попробовать сделать кандидатом другой инстанс, а первый выключить. 424 | Видно, что инстанс будет переизбран новым лидером. При этом если в строй вернуть старый, то он будет иметь 425 | состояние `follower`. 426 | 427 | ### CAP-теорема 428 | 429 | ![image](https://user-images.githubusercontent.com/8830475/109415853-2e2e2f00-79cc-11eb-8545-910a7009e6fc.png) 430 | 431 | #### Формулировка 432 | 433 | В распределенной системе можно обеспечить только два свойства из трех: согласованность, 434 | доступность и устойчивость к разделению. 435 | 436 | **Согласованность данных (consistency)** - во всех узлах в каждый момент времени данные согласованы друг с другом, 437 | то есть не противоречат друг другу. 438 | Если в одном из узлов в ячейке базы данных есть данные, такие же данные есть на всех остальных узлах. 439 | 440 | **Доступность (availability)** - любой запрос может быть обработан системой, вне зависимости от ее состояния. 441 | 442 | **Устойчивость к разделению (partition tolerance)** - расщепление системы на несколько изолированных секций 443 | не приводит к некорректному отклику от каждой из секций: отвалилась сеть между двумя узлами, 444 | но каждый из них может корректно отвечать своим клиентам. 445 | 446 | Ниже представлены примеры того, что могут представлять собой системы без одного из этих свойств: 447 | 448 | **+Availability +Consistency -Partition tolerance.** 449 | Система, которая рассчитывает на то, что сеть абсолютно надёжна, 450 | и благодаря этому может обеспечить консистентность данных на всех живых узлах. 451 | Или, в вырожденном случае, система из одного узла, где неконсистентности быть не может, 452 | и которая доступна всегда, когда доступен её узел. 453 | Другими словами, на практике таких систем или не существует, или они не являются распределёнными. 454 | 455 | **+Consistency +Partition tolerance -Availability.** 456 | Система с несколькими мастер-базами, которые обновляются синхронно. 457 | Она всегда корректна, потому что транзакция отрабатывает, 458 | только если изменения удалось распространить по всем серверам БД. 459 | Она продолжает корректно работать по крайней мере на чтение, если один из серверов падает. 460 | А вот попытки записи будут обрываться или сильно задерживаться, 461 | пока система не убедится в своей консистентности. 462 | 463 | **+Availability +Partition tolerance -Consistency**. 464 | Система с несколькими серверами, каждый из которых может принимать данные, 465 | но не обязуется в тот же момент распространять их по всему кластеру. 466 | Система переживает падения части серверов, но когда они входят в строй, 467 | они будут выдавать пользователям старые данные. 468 | 469 | #### На практике 470 | 471 | ![image](https://user-images.githubusercontent.com/8830475/109415918-78afab80-79cc-11eb-9fd9-6b4cd8571c11.png) 472 | 473 | 474 | На практике при рассмотрении этой модели могут возникать проблемы. 475 | При наличии асинхронной репликации у нас нет строгой консистентности — транзакция может быть 476 | подтверждена, но при этом часть пользователей, запрашивающие данные с реплик всё ещё могут получать устаревшие данные. 477 | Тогда такие системы просто P. 478 | 479 | #### PACELC 480 | 481 | ![image](https://user-images.githubusercontent.com/8830475/109416181-ac3f0580-79cd-11eb-9e37-25be13e89504.png) 482 | 483 | В случае разделения сети (P) в распределённой компьютерной системе необходимо выбирать между доступностью (A) и согласованностью (C) (согласно теореме CAP), 484 | но в любом случае, даже если система работает нормально в отсутствии разделения, 485 | нужно выбирать между задержками (L) и согласованностью (C). 486 | 487 | Основная цель теоремы PACELC — 488 | обратиться тезису «Игнорирование необходимости выбора между согласованностью и задержкой в реплицируемых системах является основным упущением [в рамках CAP], поскольку необходимость этого выбора присутствует при работы системы всегда, в то время как CAP имеет отношение только к дискутируемо редкому случаю разделения сети». 489 | 490 | ### Домашнее задание. Двухфазный коммит (2PC). 491 | 492 | ``` 493 | Coordinator Participant 494 | QUERY TO COMMIT 495 | --------------------------------> 496 | VOTE YES/NO prepare*/abort* 497 | <------------------------------- 498 | commit*/abort* COMMIT/ROLLBACK 499 | --------------------------------> 500 | ACKNOWLEDGMENT commit*/abort* 501 | <-------------------------------- 502 | end 503 | ``` 504 | 505 | Изменение конфигурации в распределенных системах должно происходить синхронно 506 | и атомарно. 507 | Одним из подходов, реализующих эту идею, является двухфазный коммит. 508 | 509 | Один из инстансов (координатор) получает новую конфигурацию. 510 | После этого он отправляет эту конфигурацию на каждый из инстансов кластера 511 | для её валидации. Валидация гарантирует, что следующая фаза не закончится неудачей. 512 | 513 | Если валидация успешна, каждый из инстансов отправляет подтверждение координатору. 514 | После получения подтверждения от каждого инстанса координатор начинает фазу применения 515 | конфига. 516 | 517 | Если валидация неуспешна хоть на одном из инстансов, координатор отправляет "abort" - 518 | остановку применения конфигурации. 519 | 520 | В домашнем задании предлагается реализовать двухфазный коммит. 521 | 522 | #### Условие 523 | 524 | - Поднимите N инстансов Tarantool и присвойте каждому порядковый номер/uuid; 525 | - На каждом инстансов поднимите HTTP - сервер, который будет возвращать свой 526 | идентификатор на GET запрос; 527 | - Каждый из инстансов должен быть способен выступать координатором при применении 528 | конфигурации; 529 | - Топология: адреса, порты, логины и пароли должны быть собраны в 530 | конфигурационном файле; 531 | - (*) Не хранить логины и пароли для доступа в конфигурационном файле, 532 | использовать для распространения этой информации модуль swim. 533 | 534 | Применение конфигурации - изменение порта, который слушает сервер 535 | (старый сервер при этом необходимо будет выключить): 536 | ``` 537 | { 538 | [1] = '8080', 539 | [2] = '8083', 540 | [3] = '8082', 541 | [4] = box.NULL, -- отключение сервера 542 | } 543 | ``` 544 | 545 | На стадии валидации каждый из серверов проверяет, 546 | сможет ли он слушать данный порт или нет. 547 | 548 | Небольшая подсказка, как это можно проверить: 549 | ```lua 550 | local socket = require('socket') 551 | local function can_use_port(port) 552 | local sock = socket('AF_INET', 'SOCK_STREAM', 'tcp') 553 | local ok = sock:bind('0.0.0.0', port) 554 | local err = sock:error() 555 | sock:close() 556 | if not ok then 557 | return false, err 558 | end 559 | return true 560 | end 561 | ``` 562 | 563 | Подумайте о том, какие ещё случаи стоило бы валидировать, 564 | кроме очевидного "порт слушает кто-то другой". 565 | 566 | В случае успеха на стадии "commit" отключаем старый сервер, включаем новый. 567 | В случае неудачи пишем в лог "Failed to change port from to ". 568 | 569 | Если адрес не изменяется или инстанс не указан в конфиге, 570 | никаких действий делать не нужно. 571 | 572 | Стоит подумать и над ситуацией, когда порт должен быть свободным, 573 | но предыдущий владелец ещё не успел его освободить. 574 | 575 | Рекомендация: не стоит устанавливать соединения между инстансами прямо на старте. 576 | Делайте это при необходимости. 577 | -------------------------------------------------------------------------------- /05_replication_and_sharding/05_replication_and_sharding.md: -------------------------------------------------------------------------------- 1 | ### Репликация 2 | 3 | Репликация — это процесс создания копий данных из одного хранилища в другом. 4 | Каждая копия называется репликой. Репликация может использоваться, если нужно получить резервную копию, 5 | реализовать hot standby (выполнять запросы на чтение на репликах) 6 | или горизонтально масштабировать систему. 7 | А для этого необходимо иметь возможность использовать одни и те же данные на разных узлах вычислительной сети кластера. 8 | 9 | Классификация репликации: 10 | 11 | * Направление: **master-master** или **master-slave**. 12 | Master-slave репликация — это самый простой вариант. 13 | У вас есть один узел, на котором вы меняете данные. 14 | Эти изменения вы транслируете на остальные узлы, где они применяются. 15 | При master-master репликации изменения вносятся сразу на нескольких узлах. 16 | В этом случае каждый узел и сам изменяет свои данные, и применяет к себе изменения, сделанные на других узлах. 17 | 18 | * Режим работы: **асинхронная** или **синхронная**. Синхронная репликация подразумевает, 19 | что данные не будут зафиксированы и пользователю не будет подтверждено выполнение репликации до тех пор, 20 | пока изменения не распространятся хотя бы по минимальному заданному количеству узлов кластера. 21 | В асинхронной репликации фиксация транзакции (её коммит) и взаимодействие с пользователем — 22 | это два независимых процесса. 23 | Для коммита данных требуется только, чтобы они попали в локальный журнал, 24 | и уже потом эти изменения каким-либо образом транслируются на другие узлы. 25 | Очевидно, что из-за этого у асинхронной репликации есть ряд побочных эффектов. 26 | 27 | ### Немного терминологии 28 | 29 | * LSN (Log sequence number) - порядковый номер операции на сервере. 30 | Каждый сервер при выполнении операции каждой полученной строке лога присваивает увеличивающийся номер. 31 | 32 | * Vclock - это вектор последних lsn, применённых относительно каждого узла кластера. 33 | 34 | ![image](https://user-images.githubusercontent.com/8830475/113600920-5c490380-9649-11eb-9454-f76413ad3277.png) 35 | 36 | Работа с vclock в общем случае происходит по следующим правилам: 37 | - Локальное событие приводит к увеличению компоненты vclock'a. 38 | - При получении сообщения с vclock'ом вычисляем новый vclock - расчет 39 | покомпонентного максимума локального и "удаленного" vclock'ов. 40 | 41 | ### Репликация в Tarantool 42 | 43 | ![image](https://user-images.githubusercontent.com/8830475/109418286-b3b7dc00-79d8-11eb-9297-d67f2fb34642.png) 44 | 45 | * Она строится из базовых кирпичиков, с помощью которых вы можете создать кластер любой топологии. 46 | Каждый такой базовый элемент конфигурации является однонаправленным, 47 | то есть у вас всегда есть master и slave. 48 | Master выполняет какие-то действия и формирует лог операций, который применяется на реплике. 49 | Репликация мастер-мастер в этом случае - просто создание ещё одного направления репликации в обратную сторону. 50 | 51 | ![image](https://user-images.githubusercontent.com/8830475/109418238-5c197080-79d8-11eb-9d77-23cd8bab4c7d.png) 52 | 53 | * По умолчанию репликация в Tarantool асинхронная, однако есть возможность создания 54 | синхронных спейсов. Все транзакции, которые оперируют с такими спейсами будут синхронными. 55 | В асинхронном случае система подтверждает вам коммит независимо от того, 56 | сколько реплик эту транзакцию увидели, сколько её к себе применили и получилось ли вообще это сделать. 57 | Синхронный вариант требует, чтобы транзакцию подтвердили как минимум несколько узлов 58 | (данный параметр конфигурируем). 59 | Также синхронная репликация работает только в случае master-slave. 60 | 61 | * Ещё одно свойство репликации в Tarantool — она построчная (row-based). 62 | Tarantool ведёт внутри себя журнал операций (WAL). Операция попадает туда построчно, 63 | то есть при изменении какого-то кортежа из спейса эта операция записывается в журнал как одна строка. 64 | Далее в случае асинхронной репликации операция сразу подтверждается, 65 | после этого фоновый процесс считывает эту строку из журнала и отправляет её реплике. 66 | Сколько у master‘а реплик, столько у него фоновых процессов (relay). 67 | То есть каждый процесс репликации на разные узлы кластера выполняется асинхронно от других. 68 | В случае синхронной репликации запись попадает специальную очередь — limbo, 69 | где дожидаются своей репликации на кворум узлов. 70 | Когда кворум подтверждений собран, транзакция коммитится, иначе откатывается. 71 | Только после этого управление возвращается пользователю. 72 | 73 | * Каждый узел кластера имеет свой уникальный идентификатор (instance_uuid), 74 | который генерируется при создании узла. 75 | Кроме того, узел имеет также идентификатор в кластере. 76 | Это численная константа, которая присваивается реплике при подключении к кластеру, 77 | и она остаётся вместе с репликой в течение всего времени её существования в кластере. 78 | 79 | ### Настройка репликации 80 | Попробуем настроить репликацию master-slave. 81 | ```lua 82 | -- master.lua 83 | box.cfg({ 84 | listen = 3301, 85 | replication = { 86 | 'replicator:password@127.0.0.1:3301', -- URI мастера 87 | 'replicator:password@127.0.0.1:3302', -- URI реплики 88 | }, 89 | read_only = false, 90 | }) 91 | 92 | box.schema.user.create('replicator', {password = 'password', if_not_exists = true}) 93 | box.schema.user.grant('replicator', 'replication', nil, nil, {if_not_exists = true}) 94 | ``` 95 | 96 | ```lua 97 | -- slave.lua 98 | box.cfg({ 99 | listen = 3302, 100 | replication = { 101 | 'replicator:password@127.0.0.1:3301', -- URI мастера 102 | 'replicator:password@127.0.0.1:3302', -- URI реплики 103 | }, 104 | read_only = true, 105 | }) 106 | ``` 107 | 108 | Запустим оба скрипта с помощью `tarantool -i file`. 109 | Файлы необходимо запускать в разных директориях. 110 | Или конфигурировать отдельно, где они будут хранить свои WAL (параметр `work_dir`). 111 | 112 | Попытка выполнить (почти) любую пишущую операцию на реплике приведет к ошибке 113 | `Can't modify data because this instance is in read-only mode`. 114 | Пишущие операции над некоторыми типами спейсов (temporary и local) доступны и на репликах. 115 | 116 | По умолчанию, пока все реплики не подключены, управлению пользователю не вернется. 117 | Это можно конфигурировать с помощью опции `replication_connect_quorum`. 118 | При выставлении этой опции в 0 мы не будем дожидаться подключения всех реплик. 119 | 120 | Состояние репликации можно мониторить через `box.info.replication`: 121 | ```yaml 122 | --- 123 | - 1: 124 | id: 1 125 | uuid: e697205f-b7d2-4a19-a8b5-3d738649744d 126 | lsn: 6 127 | 2: 128 | id: 2 129 | uuid: 5f4ad913-fb8f-476c-9278-f3d8c505d2e4 130 | lsn: 0 131 | upstream: 132 | status: follow 133 | idle: 0.49817899998743 134 | peer: replicator@127.0.0.1:3302 135 | lag: 0.0001680850982666 136 | downstream: 137 | status: follow 138 | idle: 0.13905499997782 139 | vclock: {1: 6} 140 | ... 141 | ``` 142 | 143 | Попробуем добавить ещё один инстанс в наш репликасет. 144 | Для этого сделаем небольшое исправление в скрипте `slave.lua` - поменяем порт, который этот инстанс будет слушать. 145 | ```yaml 146 | --- 147 | - 1: 148 | id: 1 149 | uuid: e697205f-b7d2-4a19-a8b5-3d738649744d 150 | lsn: 7 151 | 2: 152 | id: 2 153 | uuid: 5f4ad913-fb8f-476c-9278-f3d8c505d2e4 154 | lsn: 0 155 | upstream: 156 | status: follow 157 | idle: 0.78189400001429 158 | peer: replicator@127.0.0.1:3302 159 | lag: 8.702278137207e-05 160 | downstream: 161 | status: follow 162 | idle: 0.53659000000334 163 | vclock: {1: 7} 164 | 3: 165 | id: 3 166 | uuid: fd09b908-5162-42fe-96aa-7d8736165673 167 | lsn: 0 168 | downstream: 169 | status: follow 170 | idle: 0.50591900001746 171 | vclock: {1: 7} 172 | ... 173 | ``` 174 | 175 | Вот так достаточно просто получилось добавить новую реплику. 176 | Попробуем создать спейс и добавить в него запись (на мастере). 177 | Теперь если мы попытаемся посмотреть содержимое спейса, то мы увидим, 178 | что данные попали на реплики. 179 | Напомню, что репликация асинхронная. 180 | Мы получили ответ раньше, чем данные были сохранены на репликах. 181 | 182 | Отключим наши реплики. 183 | ```yaml 184 | --- 185 | - 1: 186 | id: 1 187 | uuid: e697205f-b7d2-4a19-a8b5-3d738649744d 188 | lsn: 11 189 | 2: 190 | id: 2 191 | uuid: 5f4ad913-fb8f-476c-9278-f3d8c505d2e4 192 | lsn: 0 193 | upstream: 194 | peer: replicator@127.0.0.1:3302 195 | lag: 0.00014305114746094 196 | status: disconnected 197 | idle: 11.628826999979 198 | message: connect, called on fd 18, aka 127.0.0.1:60770 199 | system_message: Connection refused 200 | downstream: 201 | status: stopped 202 | message: 'unexpected EOF when reading from socket, called on fd 18, aka [::ffff:127.0.0.1]:3301, 203 | peer of [::ffff:127.0.0.1]:' 204 | system_message: Broken pipe 205 | 3: 206 | id: 3 207 | uuid: fd09b908-5162-42fe-96aa-7d8736165673 208 | lsn: 0 209 | downstream: 210 | status: stopped 211 | message: 'unexpected EOF when reading from socket, called on fd 23, aka [::ffff:127.0.0.1]:3301, 212 | peer of [::ffff:127.0.0.1]:' 213 | system_message: Broken pipe 214 | ... 215 | ``` 216 | Попытка записать что-то новое в спейс не вызовет ошибку. 217 | При этом если мы поднимем наши реплики заново, то увидим, что через какое-то 218 | время данные на мастере и репликах будут синхронизированы. 219 | 220 | Теперь попробуем создать синхронный спейс. 221 | И выставим `box.cfg{replication_synchro_quorum = 3, replication_synchro_timeout = 5}`. 222 | Количество реплик, которые должно подтвердить транзакцию - 3 за 5 секунд. 223 | Теперь если мы попробуем выключить реплики и сделать какие-то изменения, мы получим следующую ошибку: 224 | `Quorum collection for a synchronous transaction is timed out` - за 5 секунд мы не получили подтверждение от 225 | трех реплик (сам инстанс считается за реплику), поэтому транзакция откатилась. 226 | 227 | Подробнее остановимся на том, что мы видим в `box.info.replication`: 228 | * Сущность `upstream`. Атрибут `status follow` означает, что инстанс следует за master'ом. 229 | `Idle` — время, которое прошло локально с момента последнего взаимодействия с этим master'ом. 230 | Мы не шлём поток непрерывно, master отправляет дельту, только когда на нём происходят изменения. 231 | Когда мы отправляем какой-то ACK, мы тоже осуществляем взаимодействие. 232 | Если idle становится большим (секунды, минуты, часы), то что-то не так. 233 | * Атрибут `lag`. Кроме `lsn` и `server id` каждая операция в логе маркируется еще и timestamp’ом — локальным временем, 234 | в течение которого данная операция была записана в vclock на master'е, который её выполнил. 235 | Slave при этом сравнивает свой локальный timestamp с timestamp’ом дельты, которую он получил. 236 | Последний текущий timestamp, полученный для последней строчки, slave выводит в мониторинге. 237 | * Атрибут `downstream`. Он показывает то, что master знает о своём конкретном slave'е. 238 | Это ACK, который slave ему отправляет. 239 | 240 | ![image](https://user-images.githubusercontent.com/8830475/110230491-667cc280-7f22-11eb-99e6-32926dd30645.png) 241 | 242 | С точки зрения же системы на каждое соединение у нас порождается поток (thread). 243 | Для источника этот поток называется relay, для приемника - applier. 244 | 245 | #### Конфликты репликации 246 | 247 | Попробуем сделать следующие шаги: 248 | 249 | - Отключить репликацию между инстансами; 250 | - На каждом из инстансов сделаем insert кортежа с одинаковым ключом; 251 | - Вернем репликацию обратно. 252 | 253 | Ожидается, что мы увидим следующее сообщение в логах: 254 | ```log 255 | 2021-04-01 21:48:27.760 [3242] main/130/applier/replicator@127.0.0.1:3301 applier.cc:289 E> error applying row: {type: 'INSERT', replica_id: 1, lsn: 14, space_id: 512, index_id: 0, tuple: [4, 4, 4]} 256 | 2021-04-01 21:48:27.760 [3242] main/130/applier/replicator@127.0.0.1:3301 I> can't read row 257 | 2021-04-01 21:48:27.760 [3242] main/130/applier/replicator@127.0.0.1:3301 memtx_tree.cc:779 E> ER_TUPLE_FOUND: Duplicate key exists in unique index 'pk' in space 'test' 258 | 2021-04-01 21:48:27.760 [3242] main/103/init.lua C> failed to synchronize with 1 out of 2 replicas 259 | ``` 260 | 261 | И в `box.info.replication` 262 | ```yaml 263 | tarantool> box.info.replication 264 | --- 265 | - 1: 266 | id: 1 267 | uuid: 8e64c535-9368-4b8e-83a1-5496389b495a 268 | lsn: 13 269 | upstream: 270 | peer: replicator@127.0.0.1:3301 271 | lag: 28.781993865967 272 | status: stopped 273 | idle: 18.933447999996 274 | message: Duplicate key exists in unique index 'pk' in space 'test' 275 | downstream: 276 | status: stopped 277 | message: 'unexpected EOF when reading from socket, called on fd 16, aka [::ffff:127.0.0.1]:3302, 278 | peer of [::ffff:127.0.0.1]:' 279 | system_message: Broken pipe 280 | 2: 281 | id: 2 282 | uuid: 3285c1c2-11d8-4af9-98aa-007efcfa3fbf 283 | lsn: 1 284 | ... 285 | ``` 286 | 287 | Так и выглядят конфликты репликации. 288 | И это дает небольшое представление о том, как именно репликация работает. 289 | Тарантул реплицирует отдельные операции над кортежами - insert, replace, update. 290 | И в данном случае после того, как был сделан один апдейт на инстансе, 291 | на него реплицируется другой - это приводит к ошибке - insert возможен лишь если такого 292 | первичного ключа не существует. 293 | 294 | Как решать эту проблему? 295 | 296 | - Использовать операции, которые не приводят к конфликтам - replace вместо insert; 297 | - Использовать before_replace триггер для решения конфликта. 298 | 299 | ### Спейсы без репликации 300 | 301 | При создании можно указать флаг `is_local`, тогда содержимое 302 | спейса не будет реплицироваться. Также доступен флаг `temporary` - 303 | данные сохраняются в WAL, поэтому содержимое таких спейсов также не реплицируется. 304 | 305 | То, что содержимое спейсов не реплицируется не значит, что не реплицируется 306 | факт создания таких спейсов (DDL). Одновременное создание таких спейсов 307 | на нескольких узлах также может привести к конфликтам репликации 308 | (информация о спейсах хранится в системном спейсе `_space`). 309 | 310 | ### Шардинг 311 | 312 | ![image](https://user-images.githubusercontent.com/8830475/113601654-4851d180-964a-11eb-9d73-90151d2ee781.png) 313 | 314 | Если репликация служит для масштабирования вычислений, то шардинг — для масштабирования данных. 315 | (Имея несколько вычислительных узлов, мы можем параллельно проводить на них вычисления, 316 | но что если наши данные перестают помещаться на одном сервере). 317 | 318 | Шардинг делится еще на два типа: шардинг диапазонами и шардинг хешами. 319 | 320 | При шардинге диапазонами мы от каждой записи в кластере вычисляем некоторый шард-ключ. 321 | Эти шард-ключи проецируются на прямую линию, которая делится на диапазоны, 322 | которые мы складываем на разные физические узлы. 323 | 324 | ![image](https://user-images.githubusercontent.com/8830475/109938534-30043500-7ce1-11eb-8f24-88db55f4c3cc.png) 325 | 326 | Метод, используемый в Tarantool - шардирование хэшами. 327 | Модуль, используемый для этого - [vshard](https://github.com/tarantool/vshard). 328 | Шардинг хешами проще: от каждой записи в кластере считаем хеш-функцию, 329 | записи с одинаковым значением хеш-функции складываем на один физический узел. 330 | 331 | Начнем с простого примера. У нас есть какой-то предмет, например, книга, 332 | и у книги есть некоторый числовой идентификатор. При этом книги с четным id мы складываем на первый сервер, 333 | а книги с нечетным id на другой сервер. 334 | Теперь, если нам нужна "четная" книга, то мы должны искать на первом сервере, 335 | иначе на другом. Если нам нужно найти книгу по автору, то мы должны выполнять этот поиск 336 | на обоих серверах. 337 | 338 | Данный пример демонстрирует саму концепцию шардинга. 339 | Однако обладает, как минимум, одной проблемой: что делать, если нам потребовалось добавить ещё один сервер? 340 | В общем случае пересчитывать шард-функцию для всех записей и выполнять решардинг - физический перенос 341 | данных с одного сервера на другой. Причем хотелось бы, чтобы это происходило автоматически и не требовало 342 | никаких действий со стороны пользователя. 343 | 344 | Концептуально vshard решает такую проблему по-другому. 345 | Да, мы продолжаем вычислять некоторую хэш-функцию от нашего ключа шардирования (в примере выше - id), 346 | но привязываем результат не к физическим серверам, а к некоторым виртуальным бакетам. 347 | Количество бакетов задается самим пользователем при настройке кластера и не может меняться в будущем, 348 | поэтому рекомендуемое значение - много больше, чем физических серверов. 349 | Далее каждый бакет привязывается к физическому серверу. 350 | Решардинг в таком случае не будет требоваться совсем - он заменяется другой операцией - ребалансированием. 351 | Переносом бакетов на другой сервер. 352 | 353 | Вернемся к нашему примеру с книгами. 354 | Выберем количество бакетов равное 3000 и функцию шардирования - остаток от деления на 3000. 355 | Далее при наличии 2х серверов у нас на каждом будет находиться по 1500 бакетов. 356 | Если мы добавляем 3, 4, 5, ... сервера, нам необходимо просто переместить бакеты со старых серверов на новые. 357 | И в случае с 3 серверами у нас будет находиться по 1000 бакетов на каждом. 358 | 359 | VShard состоит из двух подмодулей: vshard.storage и vshard.router. 360 | Их можно независимо создавать и масштабировать даже на одном инстансе. 361 | При обращении к кластеру мы не знаем, где какой bucket лежит, и за нас его по bucket id будет искать vshard.router. 362 | 363 | ![image](https://user-images.githubusercontent.com/8830475/110206819-199cdb80-7e91-11eb-9d6a-831ff6501711.png) 364 | 365 | Работа с данными сводится в итоге к следующим действиям: 366 | * (на сторадже) Прежде всего создать у спейса с данными индекс `bucket_id` (иначе спейс не будет учитываться vshard'ом). 367 | * (на роутере) Вычислить bucket_id нужного репликасета 368 | * Сделать запрос к данному репликасету 369 | * Запрос на запись будет отправлен на мастер 370 | * Запрос на чтение будет отправлен на одну из доступных реплик 371 | 372 | Для демонстрации зайдем в репозиторий vshard и запустим [пример](https://github.com/tarantool/vshard/tree/master/example): 373 | 374 | ```bash 375 | git clone https://github.com/tarantool/vshard/tree/master/vshard 376 | tarantoolctl rocks make 377 | cd example 378 | make start 379 | make enter 380 | ``` 381 | 382 | #### Конфигурация 383 | 384 | В этой же директории в файле `localcfg.lua` можно увидеть 385 | конфигурацию роутера и в `storage.lua` - для стораджа: 386 | ```lua 387 | cfg = { 388 | listen = 3301, 389 | memtx_memory = 100 * 1024 * 1024, 390 | sharding = { 391 | ['cbf06940-0790-498b-948d-042b62cf3d29'] = { -- replicaset #1 392 | replicas = { 393 | ['8a274925-a26d-47fc-9e1b-af88ce939412'] = { 394 | uri = 'storage:storage@127.0.0.1:3301', 395 | name = 'storage_1_a', 396 | master = true 397 | }, 398 | ['3de2e3e1-9ebe-4d0d-abb1-26d301b84633'] = { 399 | uri = 'storage:storage@127.0.0.1:3302', 400 | name = 'storage_1_b' 401 | } 402 | }, 403 | }, -- replicaset #1 404 | ['ac522f65-aa94-4134-9f64-51ee384f1a54'] = { -- replicaset #2 405 | replicas = { 406 | ['1e02ae8a-afc0-4e91-ba34-843a356b8ed7'] = { 407 | uri = 'storage:storage@127.0.0.1:3303', 408 | name = 'storage_2_a', 409 | master = true 410 | }, 411 | ['001688c3-66f8-4a31-8e19-036c17d489c2'] = { 412 | uri = 'storage:storage@127.0.0.1:3304', 413 | name = 'storage_2_b' 414 | } 415 | }, 416 | }, -- replicaset #2 417 | }, -- sharding 418 | replication_connect_quorum = 0, 419 | } 420 | 421 | -- для роутера 422 | vshard.router.cfg(cfg) 423 | 424 | -- для стораджа 425 | vshard.storage.cfg(cfg, instance_uuid) 426 | ``` 427 | 428 | Важно понимать, что конфигурация не является статической. 429 | Рано или поздно что-то упадет, сломается или просто закончится место. 430 | Потребуется либо убрать, либо добавить инстанс. 431 | Это потребует изменения конфигурации на **всех** инстансах. 432 | Кроме этого, желательно, чтобы это происходило синхронно. 433 | 434 | 435 | #### Работа с данными 436 | 437 | Мы подключимся к роутеру, на стораджах при этом будет подготовлена схема данных для работы. 438 | Попробуем вставить какой-то объект: 439 | ```lua 440 | customer = { 441 | customer_id = 1, 442 | bucket_id = nil, 443 | name = 'Ivan', 444 | accounts = { 445 | { 446 | account_id = 1, 447 | name = 'First', 448 | }, 449 | { 450 | account_id = 2, 451 | name = 'Second', 452 | } 453 | }, 454 | } 455 | 456 | customer.bucket_id = vshard.router.bucket_id_mpcrc32(customer.customer_id) 457 | 458 | vshard.router.callrw(customer.bucket_id, 'customer_add', {customer}) 459 | ``` 460 | 461 | Теперь если мы захотим прочитать данные нашего customer'a по id, нам нужно 462 | вызвать функцию `customer_lookup` на нужном репликасете. 463 | 464 | ``` 465 | customer_id = 1 466 | bucket_id = vshard.router.bucket_id_mpcrc32(customer_id) 467 | vshard.router.callro(bucket_id, 'customer_lookup', {customer_id}) 468 | ``` 469 | 470 | На что стоит обратить внимание? 471 | В качестве функции шардирования используется одна из встроенных функций - `bucket_id_mpcrc32`. 472 | При желании можно использовать любую свою функцию, главное, 473 | чтобы она возвращала значение не превышающее количество бакетов. 474 | При этом желательно, чтобы данные распределялись равномерно между стораджами - 475 | если один из стораджей загружен полностью, а другой пуст, то толку от такого шардинга мало. 476 | 477 | Кроме этого, видно, что вместе с сущностью `customer` лежат ещё и `account`s. 478 | Причем все счета одного пользователя имеют тот же bucket id, что и этот пользователь. 479 | Это не просто так. Подобный подход обеспечивает атомарность - у нас есть гарантия, что 480 | все счета пользователя будут лежать на одном сторадже вместе с этим пользователем. 481 | 482 | И в этом есть свой компромисс: с одной стороны, нам приходится вручную вычислять bucket_id, 483 | с другой, это бывает достаточно удобно, когда есть какая-то "главная сущность" и одна или несколько 484 | зависимых от неё - в этом случае подобный подход позволяет держать эти данные на одном сторадже. 485 | 486 | #### Зоны и доступность на чтение 487 | 488 | ![image](https://user-images.githubusercontent.com/8830475/112019553-993cd280-8b40-11eb-9f22-31a79e9d5dbd.png) 489 | 490 | Каждой реплике и каждому роутеру присваиваем номер зоны и задаем таблицу, 491 | в которой указываем расстояние между каждой парой зон. 492 | Когда роутер принимает решение, куда отправить запрос на чтение, он выберет реплику в той зоне, 493 | которая ближе всего к его собственной. 494 | 495 | ![image](https://user-images.githubusercontent.com/8830475/112019693-bd98af00-8b40-11eb-9092-94915c2fd339.png) 496 | 497 | В общем случае можно обращаться и к произвольной реплике, но если кластер большой и сложный, 498 | очень сильно распределенный, тогда зонирование сильно пригодится. 499 | Зонами могут быть разные серверные стойки, чтобы не загружать сеть трафиком. 500 | Или это могут быть географически удаленные друг от друга точки. 501 | 502 | Также зонирование помогает при разной производительности реплик. 503 | К примеру, у нас в каждом replica set’е есть одна бэкап-реплика, 504 | которая должна не принимать запросы, а только хранить копию данных. 505 | Тогда мы делаем её в зоне, которая будет очень далеко от всех роутеров в таблице, 506 | и они станут обращаться к ней в самом крайнем случае. 507 | 508 | 509 | #### Failover (аварийное переключение) 510 | 511 | Выборы нового мастера в vshard'e не реализованы, поэтому в случае необходимости их 512 | придется делать это самостоятельно. 513 | Когда мы его каким-то образом выбрали, нужно, чтобы этот инстанс теперь взял на себя полномочия мастера. 514 | Обновляем конфигурацию, указав для старого мастера `master = false`, а для нового — `master = true`, 515 | применим через VShard.storage.cfg и раскатаем на хранилища. 516 | Дальше всё происходит автоматически. 517 | Старый мастер перестает принимать запросы на запись и начинает синхронизацию с новым, 518 | потому что могут быть данные, которые уже применились на старом мастере, 519 | а на новый ещё не доехали. 520 | После этого новый мастер вступает в роль и начинает принимать запросы, 521 | а старый мастер становится репликой. Так работает write failover в VShard. 522 | 523 | ``` 524 | replicas = new_cfg.sharding[uud].replicas 525 | replicas[old_master_uuid].master = false 526 | replicas[new_master_uuid].master = true 527 | vshard.storage.cfg(new_cfg) 528 | ``` 529 | 530 | ### Домашнее задание 531 | 532 | * Создать шардированный спейс (с помощью модуля `vshard`) `products` со следующими полями: 533 | * uuid - генерировать с помощью модуля `uuid` (первичный ключ); 534 | * category - категория продукта (молоко, сметана, помидоры) (неуникальное поле); 535 | * name - название (например, "Домик в деревне", "Простоквашино", ...); 536 | * weight - вес одной упаковки; 537 | * count - количество упаковок на складе. 538 | 539 | * Реализовать следующий интерфейс для работы с данными: 540 | * put - сохранить информацию о товаре (если товар уже существует - ошибка); 541 | * update - обновить информацию о товаре (если товара не существует - ошибка); 542 | * get - получить информацию об товаре по uuid; 543 | * calculate_weight(category) - посчитать суммарный вес товаров в данной категории; 544 | * get_products_by_category(category, limit[(*), after]) - получить список товаров указанной категории; 545 | 546 | #### Комментарий 547 | Подробнее рассмотрим функцию `get_products_by_category`. 548 | 549 | Предположим, у нас 3 стораджа с данными: 550 | 551 | ``` 552 | Storage 1 Storage2 Storage3 553 | Pepsi Baikal 7UP 554 | Coca-cola Dr.Pepper Tarrago 555 | Fanta Duchess Mountain Dew 556 | ``` 557 | 558 | Запрос `get_products_by_category('soda', 2)` должен вернуть нам 559 | результат: `[7UP, Baikal]`, 560 | `get_products_by_category('soda', 4)` - `[7UP, Baikal, Dr.Pepper, Duchess]`. 561 | 562 | При этом должна быть возможность получать объект после какого-то: 563 | `get_products_by_category('soda', 2, {Baikal, uuid_of_Baikal})` - `[Dr.Pepper, Duchess]`. 564 | 565 | Комментарии к реализации: 566 | 567 | * Категория не является уникальным полем, однако порядок должен поддерживаться. 568 | Единственным уникальным полем является UUID товара, внутри себя Tarantool при сортировке 569 | по вторичному ключу учитывает ещё и первичный. Стоит сразу добавить поле UUID во вторичный ключ. 570 | Тогда after можно будет использовать следующим образом - `space:pairs(after)`. 571 | * Использовать опцию `offset` в функции `select` запрещено - она работает за `O(N)`. 572 | * (*) Запрос `get_products_by_category` будет возвращать отсортированный массив данных 573 | с каждого стораджа. Подумайте, как оптимальнее всего объединять несколько 574 | отсортированных массивов. 575 | 576 | Комментарии к заданию: 577 | 578 | * Не стоит задумываться о том, как должна вести себя система в случае неполадок - 579 | в данном случае считаем, что всё надежно и работает; 580 | * В качестве основы можно использовать пример прямо из репозитория; 581 | * Места с меткой (*) являются доп.заданиями для получения большего балла. 582 | -------------------------------------------------------------------------------- /06_availability/06_availability.md: -------------------------------------------------------------------------------- 1 | # Высокая доступность кластера 2 | 3 | Поговорим о таком свойстве системы как доступность. 4 | **Доступность (availability)** - любой запрос может быть обработан системой, вне зависимости от ее состояния. 5 | 6 | ![image](https://user-images.githubusercontent.com/32142520/114453293-101a3800-9be2-11eb-8e6d-76e654dc4595.png) 7 | 8 | Построение распределенной системы позволяет решить многие проблемы, которые возникают при разработке высоконагруженных приложений. 9 | Но вместе с тем распределенные системы значительно сложнее приложений, которые работают в рамках одного сервера. 10 | Эта сложность порождает невообразимое множество проблем, которые могут привести систему в нерабочее состояние самым неожиданным образом. 11 | 12 | Логично предположить, что высоконагруженные приложения должны предоставлять пользователям гарантии. 13 | Система, которая находится в нерабочем состоянии и теряет данные пользователей довольно быстро не выдержит конкуренции и перестанет быть высоконагруженной. 14 | 15 | Одно из достоинств распределенных приложений это то что потенциально при отказе части системы она может продолжать работать так, чтобы конечный пользователь не 16 | почувствовал разницы. 17 | Но добиться этого не так-то просто. 18 | 19 | В реальной жизни распределенное приложение почти никогда не пребывает в состоянии, когда все узлы работают нормально. 20 | Поэтому при проектировании нужно продумать, как заставить систему работать таким образом, чтобы она выполняла свое предназначение и гарантировала конечному пользователю целостность его данных. 21 | 22 | ## Создание надежной системы из ненадежных компонентов 23 | 24 | На первый взгляд кажется, что система не может быть надежнее, чем самый ненадежный ее компонент. 25 | Но на деле практика построение надежной системы из ненадежных компонентов широко распространена в IT. 26 | Например, TCP-протокол увеличивает надежность отправки пакетов через ненадежный IP, но и он не может устранить сетевые задержки. 27 | Использование самокорректирующихся кодов (вроде кода Хемминга) позволяет исправлять ошибки в нескольких битах, но не поможет при потере большей части слова. 28 | 29 | Такой подход устраняет некоторые сбои, делая систему более стабильной и упрощая обработку ошибок на других уровнях. 30 | 31 | ## Возможные причины сбоев 32 | 33 | ![image](https://user-images.githubusercontent.com/32142520/114438066-f243d780-9bcf-11eb-9227-0fbc0e4a6a37.png) 34 | 35 | Для того чтобы представить себе, какие проблемы необходимо предусмотреть, нужно максимально пессимистично взглянуть на все части системы и мысленно разломать все что только можно. 36 | 37 | **Если что-то может пойти не так, оно пойдёт не так.** 38 | 39 | ### Сетевые проблемы 40 | 41 | В распределенной системе связь между узлами осуществляется по сети. 42 | Обмен данными происходит исключительно по сети, узлы не имеют доступа к данным других узлов. 43 | 44 | Существует два подхода передачи данных по сети: 45 | 46 | * асинхронный - данные считаются переданными без подтверждения со стороны адресата; 47 | * синхронный - отправитель ожидает подтверждения. 48 | 49 | Хотя синхронный подход выглядит более надежным, поскольку обеспечивает передачу всех пакетов в правильном порядке, он совершенно не помогает в локализации проблем с сетью и не способствует их решению. 50 | Отсутствие ответа может означать что угодно: 51 | 52 | * запрос был потерян; 53 | * запрос попал в очередь и будет отправлен позже; 54 | * запрос был доставлен, но на принимающей стороне произошла необработанная ошибка; 55 | * запрос был доставлен, но ответ не дошел до отправителя или попал в очередь и будет доставлен позже. 56 | 57 | Распространенная практика решения таких проблем - установка времени ожидания, после которого запрос считается не доставленным. 58 | В таком случае, система должна корректно обрабатывать повторные запросы, чтобы не прийти в нерабочее состояние. 59 | 60 | ЦОДы в основном используют асинхронные протоколы. 61 | Узел отправляет запрос, но сеть не дает гарантий, что запрос будет доставлен быстро и будет ли доставлен вообще. 62 | 63 | Сетевые сбои происходят регулярно и по самым разным причинам: 64 | 65 | * При перестройке топологии сети могут возникать длительные задержки. 66 | * Сетевой интерфейс может совершенно неожиданно перестать обрабатывать входящие запросы, но продолжать отправлять исходящие. Такую проблему очень сложно диагностировать. 67 | * Физические повреждения составных частей сети - акула может прокусить подводный кабель, ЦОД может быть затоплен или обесточен, в него может врезаться автомобиль. 68 | 69 | Ситуация, когда часть сети становится отрезанной от остальной сети называется 70 | **нарушением связности сети (network partition, netsplit)**. 71 | 72 | Система должна предполагать, что сетевые сбои будут случаться неизбежно и должны быть корректно обработаны. 73 | Они не должны приводить к блокировке узлов, все ошибки должны быть корректно обработаны, пользователь должен получить адекватное сообщение об ошибке. 74 | 75 | Хорошей практикой является тестирование системы на устойчивость к различным сетевым сбоям и проработка сценариев восстановления после таких сбоев. 76 | 77 | ## Обнаружение сбоев 78 | 79 | Система должна обнаруживать неисправные узлы и перестраиваться таким образом, чтобы наладить работу. 80 | Балансировщик нагрузки должен перестать отправлять запросы на узлы, которые долго не отвечают. 81 | При использовании репликации после выведения из строя мастера одна из реплик должна занять его место. 82 | 83 | Дополнительные сложности вносят проблемы с сетью - не всегда понятно, вышел ли узел из строя или с ним просто потеряна связь. 84 | 85 | Возможные подходы к определению нерабочих узлов: 86 | 87 | * Если при запросе на машину ни один процесс не прослушивает запрошенный порт, операционная система будет закрывать сетевые соединения или отказывать их устанавливать. 88 | * При сбое узла операционная система может собрать данные о сбое и оповестить остальные узлы, что нужно перераспределить задачи. 89 | * Доступ к коммутаторам сети через административный интерфейс позволяет установить проблемы на аппаратном уровне. 90 | * Маршрутизатор может определить недоступность IP-адреса узла и вернуть ошибку Destination Unreachable. 91 | 92 | В случае проблем с подключением к удаленному узлу может быть получена ошибка на одном из уровней стека. 93 | При этом есть смысл попытаться отправить запрос и дождаться ответа еще несколько раз. 94 | 95 | ### А сколько ждать? 96 | 97 | Выбор адекватного времени ожидания непрост. 98 | При большом времени ожидания приложение может долгое время находиться в нерабочем состоянии, пользователи будут получать ошибки. 99 | Слишком маленькое время ожидания может привести к объявлению рабочего узла нерабочим. 100 | Если узел при этом выполняет какую-либо задачу, то в случае признания узла неисправным, мы рискуем продублировать эту задачу. 101 | В некоторых случаях такая ситуация крайне нежелательна - вряд ли пользователи будут рады продублированному списанию средств с карты. 102 | 103 | Если узел отвечает медленно из-за сильной нагрузки, то выведение этого узла из эксплуатации только усугубит ситуацию. 104 | 105 | В случае, если время отправки запроса ограничено сверху (пакет доставляется или теряется в течение фиксированного времени) и обработка запроса занимает некоторое известное время, можно подобрать оптимальное время ожидания. 106 | 107 | К сожалению, в реальности все не так прозрачно. 108 | Асинхронные сети не имеют ограничений по задержкам, а обработка запросов за определенное время почти никогда не гарантирована. 109 | Для того чтобы обнаружить проблемы, необходимо чтобы система работала исправно большую часть времени. 110 | Тогда скачки времени на отправку и обработку запросов будут заметны при небольшом времени ожидания. 111 | 112 | ### Что влияет на время отклика? 113 | 114 | * При большом количестве отправляемых пакетов может образоваться очередь. 115 | Как следствие, сеть перегружена, и время отклика увеличено. 116 | Если очередь заполнена пакетами, то новые пакеты удаляются и должны быть отправлены заново. 117 | * Большое количество принимаемых пакетов ведет к загрузке CPU и входящие запросы тоже отправляются в очередь. 118 | * Виртуальные машины могут блокироваться, пока CPU используется другой машиной. 119 | * TCP может ограничивать число отправляемых сообщений, из-за чего запросы попадают в очередь. 120 | 121 | ## Chaos Engineering 122 | 123 | **Хаос-инжиниринг — это подход, предусматривающий проведение экспериментов над production-системой, чтобы убедиться в ее способности выдерживать различные помехи, возникающие во время работы.** 124 | 125 | Компания Netflix, занимающаяся онлайн-распространением видео-контента, придерживается довольно жестких принципов для поддержания доступности системы. 126 | Доступность измеряется как отношение успешных попыток запустить фильм к общему числу попыток. 127 | Целевое значение - 0,9999. 128 | И довольно часто оно достигается. 129 | 130 | Компания использует несколько приложений, которые провоцируют проблемы в системе и позволяют выявить проблемы на раннем этапе [1]. 131 | 132 | * Chaos Monkey - выбирает случайный инстанс на продакшене и убивает его. 133 | * Chaos Gorilla - отключает одну из зон доступности в AWS. 134 | * Chaos Kong - отключает регион AWS. 135 | 136 | ![image](https://user-images.githubusercontent.com/32142520/114440238-90d13800-9bd2-11eb-90be-2cf52e2e438b.png) 137 | 138 | ## Отказ реплики: Recovery 139 | 140 | При отказе реплики (или после разрыва соединения с мастером) данные могут быть восстановлены следующим образом. 141 | После рестарта реплики из лога могут быть определены последние изменения, которые произошли перед падением. 142 | Далее, реплика подключается к лидеру и запрашивает изменения, которые произошли после отказа. 143 | После того как реплика "догнала" мастера, она может продолжить принимать данные в обычном режиме. 144 | 145 | ## Отказ лидера: Failover 146 | 147 | Если в приложении с master-slave репликацией отказал лидер, то должно произойти **аварийное переключение (failover)**: 148 | 149 | * одна из реплик становится новым лидером; 150 | * запросы на запись должны быть перенаправлены на нового лидера; 151 | * реплики должны начать реплицировать данные с нового лидера. 152 | 153 | Failover может быть произведен вручную или автоматически. 154 | Нас интересует автоматический процесс. 155 | 156 | 1. Первое, что нужно сделать - **обнаружить падение лидера**. 157 | Не существует достоверного способа установить падение узла, поэтому большинство систем используют таймауты. 158 | Если узел не отвечает в течение выделенного таймаута, он считается неисправным. 159 | 160 | 2. Далее происходит **выбор нового лидера**. 161 | Он может быть выбран в соответствии с приоритетностью оставшихся реплик 162 | или назначен заранее определенным координатором. 163 | Наилучшим кандидатом, как правило, является реплика с наиболее актуальными данными. 164 | 165 | 3. **Перестройка системы** в соответствии с новым лидером. 166 | Запросы должны быть перенаправлены на нового лидера. 167 | Если старый лидер вернется в строй, он должен стать репликой и подключиться с новому лидеру. 168 | 169 | ### Failover: Что может пойти не так? 170 | 171 | * При асинхронной репликации *новый лидер может не успеть получить последние изменения от старого*. 172 | Что делать с этими изменениями после возвращения старого лидера? 173 | Чаще всего они сбрасываются, поскольку новый лидер мог успеть обработать конфликтующие запросы на запись. 174 | 175 | * Дополнительную опасность в таких случаях представляет использование внешних систем, которые координируются теми же данными. 176 | В 2012 году произошел инцидент на GitHub. 177 | "Отстающая" реплика была назначена новым лидером, но при этом последние значения autoincremented primary key были переиспользованы. 178 | Эти ключи использовались внешним хранилищем для идентификации данных пользователей, что привело к раскрытию приватных данных между юзерами. 179 | 180 | * Может случиться так, что два узла считают себя лидерами - **split-brain**. 181 | В таком случае если оба узла получат запросы на запись, то может случиться конфликт или потеря данных. 182 | Некоторые системы в таких случаях отключают один из узлов, но если этот механизм работает неточно, то можно потерять оба узла. 183 | 184 | Простого решения у этих проблем нет. 185 | Часто failover требует ручного вмешательства даже при наличии автоматики. 186 | 187 | ## Предотвращение split-brain 188 | 189 | Одна из необходимых абстракций, которые помогают приложению обрабатывать различные сбои - это **консенсус**. 190 | Идея очень проста - *все узлы системы должны быть согласны в чем-то*. 191 | 192 | Чтобы исключить ситуацию, когда несколько узлов одновременно считают себя лидерами, используется lock. 193 | Каждая нода пытается захватить lock, и первый узел, который сможет это сделать, становится лидером. 194 | Детали реализации не имеют значения, но этот lock должен обладать следующим свойством - все узлы должны четко понимать, кто завладел данным lock'ом. 195 | 196 | В качестве таких lock'ов часто используются такие сервисы, как etcd, Apache ZooKeeper и Consul. 197 | Их часто называют "распределенными key-value хранилищами" или “сервисами конфигурации и координации". 198 | Такие сервисы используют алгоритмы консенсуса, которые позволяют реализовать отказоустойчивые **линеаризуемые** операции со значениями. 199 | Основная идея таких операций - все действия происходят как будто бы атомарно и существует как будто бы одна единственная копия данных. 200 | 201 | Использование внешнего координатора - необходимое, но не достаточное условие для реализации правильного переключения лидера. 202 | 203 | ## Fencing 204 | 205 | При использовании lock'ов существует ряд проблем. 206 | 207 | Допустим, у нас есть сторадж, в который может одновременно писать один клиент, чтобы не была нарушена целостность данных. 208 | Если попытаться реализовать это с помощью внешнего lock-сервиса, то возможно следующая ситуация: 209 | 210 | ![image](https://user-images.githubusercontent.com/32142520/114463817-9f791880-9bed-11eb-8b6c-c16840846203.png) 211 | 212 | * первый клиент берет lock на запись в файл и "подвисает"; 213 | * за время подвисания lock освобождается, его захватывает второй клиент и выполняет запись в файл; 214 | * первый клиент возвращается и в полной уверенности, что lock принадлежит ему, тоже пишет в файл. 215 | 216 | Чтобы предотвратить подобные ситуации, мы должны убедиться, что узел не просто верит в то, что он "избранный", а он действительно такой. 217 | Этого легко достичь при использовании такой техники как **fencing**. 218 | 219 | ![image](https://user-images.githubusercontent.com/32142520/114464828-f4695e80-9bee-11eb-85aa-5d2b920567b9.png) 220 | 221 | Каждый успешный lock возвращает монотонно возрастающий *fencing token*. 222 | Каждая запись в сторадж требует от клиента актуальный токен. 223 | 224 | * первый клиент берет lock на запись в файл, получает токен *33* и "подвисает"; 225 | * за время подвисания lock освобождается, его захватывает второй клиент уже с токеном *34* и выполняет запись в файл, передав стораджу токен *34*; 226 | * первый клиент возвращается и в полной уверенности, что lock принадлежит ему, тоже пишет в файл с устаревшим токеном *33*. 227 | Этот запрос отклоняется - уже был получаен запрос с более новым токеном. 228 | 229 | Обратите внимание, что проверки токенов на стороне клиентов недостаточно, нужна проверка со стороны сервера. 230 | 231 | Для построения корректного алгоритма фенсинга, нужно соблюсти следующие свойства: 232 | 233 | * уникальность - для каждого запроса возвращается уникальный токен; 234 | * монотонность - токен запроса, произошедшего позже, всегда больше токена запроса, который произошел раньше; 235 | * доступность - если узел запросил токен, то он его получит, если неожиданно не прекратит работу. 236 | 237 | Алгоритм является корректным, если он удовлетворяет всем этим свойствам в любой момент времени. 238 | Но в случае если все узлы вышли из строя или задержки сети стали бесконечно долгими, ни один алгоритм не будет работать. 239 | 240 | ## Безопасность и живучесть 241 | 242 | Уникальность и монотонность - свойства безопасности. 243 | Доступность - свойство живучести. 244 | 245 | Безопасность - **ничего плохого не произойдет**. 246 | Если свойство безопасности было нарушено, то мы можем определить момент времени, когда это произошло, и начиная с этого момента нанесенный урон не может быть устранен. 247 | 248 | Живучесть - **рано или поздно произойдет что-то хорошее** (eventually). 249 | Свойство может не выполняться в конкретный момент времени, но через какое-то время оно может быть достигнуто. 250 | 251 | ## Внешние координаторы - etcd, ZooKeeper, Consul 252 | 253 | API подобных сервисов, как правило, позволяет читать и записывать значения по ключу и итерироваться по представленным ключам. 254 | Но все же это не база данных. 255 | 256 | Внешние хранилища позволяют хранить небольшое количество данных в памяти (конечно, эти данные также записываются на диск). 257 | Эти данные распространяются среди подключенных узлов при помощи отказоустойчивых алгоритмов. 258 | Получается что-то похожее на репликацию - всякое изменение данных применяется ко всем узлам. 259 | 260 | Внешние координаторы часто используются для **service discovery**, когда требуется получить IP-адрес сервиса. 261 | Например, при старте виртуальной машины, она регистрирует свой IP-адрес в **service registry**. 262 | Обычно для этих целей используется DNS, где используется кеширование и прочие оптимизации. 263 | Небольшое "устаревание" данных из DNS обычно не является проблемой, важнее то, что DNS устойчив к перебоям в сети и отличается высокой доступностью. 264 | 265 | **ZooKeeper**: 266 | 267 | * инструмент для service discovery и хранения конфигурации; 268 | * изначально использовался в Hadoop кластерах, наиболее зрелое решение; 269 | * высокая производительность 270 | * поддержка Kafka; 271 | 272 | **etcd**: 273 | 274 | * надежное key-value хранилище; 275 | * open-source; 276 | * новое решение, которое отличается простотой и удобством использования; 277 | 278 | **Consul**: 279 | 280 | * сервис для хранения конфигурации, распределенной синхронизации; 281 | * наиболее широкое решение, включает в себя встроенный service discovery; 282 | * высоко доступный service discovery; 283 | * health checking; 284 | * gossip протоколы для кластера; 285 | * интеграция с Docker. 286 | 287 | ## Практика 288 | 289 | Реализуем распределенный lock для выбора лидера при помощи etcd. 290 | 291 | ## Ссылки 292 | 293 | [1]: https://netflixtechblog.com/chaos-engineering-upgraded-878d341f15fa 294 | 295 | ## Домашнее задание 296 | 297 | Реализовать аварийное переключение лидеров при помощи etcd. 298 | Написать тест, который проверяет, что после отказа лидера кластер продолжает функционировать и конфигурация VShard одинакова на всех инстансах. 299 | 300 | (*) Для получения доп. баллов предлагается написать стресс-тесты - пусть приложение принимает данные в течение некоторого времени. Во время переключения лидера часть данных будет потеряна. Нужно померить количество потерянных данных. 301 | 302 | ### Детали 303 | 304 | * Поднимаем приложение из примера - там один роутер и 2 сторадж-репликасета. Для чистоты эксперимента лучше добавить еще по одному стораджу, чтобы было по 2 реплики в каждом. Так получится протестировать, что выбирается одна из двух доступных реплик при отключении мастера. 305 | 306 | * Добавляем простое API (можно HTTP, можно по iproto) - put и get. (API предоставляет роутер, для каждого запроса он выбирает нужный сторадж и выполняет на нем insert/get). 307 | 308 | * Убиваем мастера одного из репликасетов - put запросы перестают работать, get запросы работают. 309 | 310 | * Через некоторое время срабатывает наш failover - put запросы начинают работать. 311 | 312 | * В качестве основы можно взять [example](https://github.com/tarantool/vshard/tree/master/example) из репозитория vshard. 313 | 314 | * Тесты могут быть любыми. Единственное условие - в `README.md` должна быть внятная инструкция по их запуску (желательно одной командой). Можно использовать Makefile, bash-скрипт, pytest, что угодно. 315 | 316 | ### Подсказки 317 | 318 | * [Документация](https://www.tarantool.io/ru/doc/latest/reference/reference_rock/vshard/vshard_api/) может ответить на многие вопросы. 319 | 320 | * Для записи/чтения нужно использовать `vshard.router.call{rw,ro}(bucket_id, func_name, func_args)`. 321 | 322 | * `func_name` - функция на сторадже, которая выполняет insert/get на сторадже. Чтобы эту функцию можно было вызывать с роутера, ее нужно положить в `_G.`. 323 | 324 | * `bucket_id` считаем от primary key (в целом можно считать как угодно, лишь бы всегда одинаково). 325 | -------------------------------------------------------------------------------- /07_cartridge/07_cartridge.md: -------------------------------------------------------------------------------- 1 | ## Cartridge 2 | 3 | Мы познакомились с тем, как масштабировать Tarantool. 4 | С тем, как работает репликация и шардинг. 5 | Однако, по-прежнему, это не решает всех проблем и не дает всех средств для 6 | построения надежной отказоустойчивой системы. 7 | 8 | Одна из фундаментальных проблем - распространение и поддержание 9 | единой конфигурации на всех инстансах кластера. 10 | Кроме этого, необходимы были средства для удобного управления конфигурацией, 11 | обеспечения отказоустойчивости. 12 | При этом только разработкой дело не должно было ограничиваться - 13 | необходим набор инструментов, которые бы позволяли доставлять 14 | и разворачивать приложения. Это будет подробно рассмотрено в 15 | следующий раз. Пока что из важного - для того, чтобы это было возможно, 16 | необходимо поддерживать некоторую общую структуру проекта. 17 | 18 | Tarantool является платформой для написания сервисов. 19 | Обычно в проектах не работают с единичными инстансами. 20 | Главным образом из-за однопоточности - довольно скоро инстанс начинает 21 | загружать CPU на 100% - единственным решением проблемы является масштабирование. 22 | При этом для локального тестирования и при разработке имеет смысл разворачивать 23 | все сервисы на единственном инстансе. 24 | 25 | Так появился [Cartridge](https://github.com/tarantool/cartridge) - фреймворк для написания приложений на Tarantool, 26 | который вводит понятие ролей. Роль, по сути, является Lua-модулем. 27 | По контракту роль должна предоставить набор функций - для инициализации/деинициализации, 28 | валидации конфигурации и её применения. 29 | 30 | Основные возможности Tarantool Cartridge: 31 | * автоматизированное оркестрирование кластера; 32 | * расширение функциональности приложения с помощью новых ролей; 33 | * шаблон приложения для разработки и развертывания; 34 | * встроенное автоматическое шардирование; 35 | * управление кластером с помощью WebUI и API; 36 | * инструменты упаковки, тестирования и деплоя; 37 | * failover & switchover; 38 | 39 | ![image](https://user-images.githubusercontent.com/8830475/110234778-7ce44780-7f3d-11eb-94d1-5006ce00c247.png) 40 | 41 | Установить Tarantool Cartridge можно с помощью `tarantoolctl`: 42 | 43 | ```bash 44 | tarantoolctl rocks install cartridge 2.5.1 45 | ``` 46 | 47 | Рекомендуется явно указывать желаемую версию т.к. иначе 48 | будет установлена версия с `master` ветки репозитория. 49 | При этом все зависимости (в том числе и WebUI) будут устанавливаться 50 | на Вашем компьютере. 51 | 52 | ### Пре-реквизиты 53 | 54 | Нам понадобится приложение [cartridge-cli](https://github.com/tarantool/cartridge-cli). 55 | Не только на этом занятии, но и на следующих, а также при выполнении домашних заданий. 56 | 57 | *Cartridge-cli* - приложение, которое упрощает создание, сборку, упаковку проектов и 58 | управление ими. 59 | 60 | Запуск приложения `cartridge [command]`. Но не стоит ассоциировать `cartridge` 61 | как фреймворк для разработки приложений и `command-line interface` для него. 62 | Это два отдельных приложения со своим релизным циклом. 63 | При этом если cartridge большей частью написан на Lua, 64 | то cartridge-cli на Go. 65 | 66 | ### Первое приложение 67 | 68 | Запустим команду `cartridge create`, чтобы предсоздать небольшой шаблон для проекта. 69 | Соберем проект - `cartridge build`. Сразу запустим проект `tarantool init.lua`. 70 | При этом мы запустили один инстанс, теперь он запущен на `localhost:8081`. 71 | 72 | Рассмотрим структуру проекта детальнее (актуально для версии 2.8.0). 73 | 74 | ``` 75 | . 76 | ├── Dockerfile.build.cartridge 77 | ├── Dockerfile.cartridge 78 | ├── README.md 79 | ├── app 80 | │   ├── admin.lua 81 | │   └── roles 82 | │   └── custom.lua 83 | ├── cartridge.post-build 84 | ├── cartridge.pre-build 85 | ├── deps.sh 86 | ├── init.lua 87 | ├── instances.yml 88 | ├── myapp-scm-1.rockspec 89 | ├── replicasets.yml 90 | ├── stateboard.init.lua 91 | ├── test 92 | │   ├── helper.lua 93 | │   ├── integration 94 | │   │   └── api_test.lua 95 | │   └── unit 96 | │   └── sample_test.lua 97 | └── tmp 98 | 99 | 6 directories, 16 files 100 | ``` 101 | 102 | * Файлы "Dockerfile.*" нужны для сборки и запуска нашего 103 | проекта с помощью docker. Подробнее на следующих занятиях; 104 | * app - наше приложение; 105 | * cartridge.* - специальные хуки, которые вызываются при сборке; 106 | * deps.sh - скрипт для установки dev-зависимостей. Например, модулей тестирования; 107 | * init.lua - входная точка для запуска нашего проекта; 108 | * instances.yml/replicasets.yml - файлы для запуска и конфигурирования инстансов с проектов. 109 | instances.yml отвечает за запуск, а replicasets.yml - за сборку этих инстансов в репликасеты, назначение ролей и т.д; 110 | * myapp-scm-1.rockspec - файл, описывающий проект для менеджера пакетов luarocks 111 | (tarantoolctl rocks). Здесь указываются зависимости нашего проекта, его имя и версия; 112 | * stateboard.init.lua - файл для запуска stateboard - внешнего координатора для stateful failover; 113 | * test - директория с заготовками для написания тестов с помощью фреймворка luatest; 114 | * tmp - директория, куда будут складываться файлы, связанные с запущенными инстансами: 115 | сокеты для подключения, xlog'и и snap'ы. 116 | 117 | Для старта приложения позовем `cartridge start`. 118 | Для нас запускаются несколько инстансов. 119 | Важно понимать, что данная команда подходит лишь для 120 | локальной разработки. В случае, если какой-то инстанс упадет 121 | по какой-то причине, он не будет перезапущен. 122 | 123 | Сформировать репликасеты и назначить им роли можно с помощью 124 | команды `cartridge replicasets setup`. 125 | 126 | ### Роли 127 | 128 | Попробуем написать собственные роли на базе примеров из vshard'a - у нас будут 129 | 2 роли: storage и router. Никаких особых настроек от нас не потребуется, 130 | мы просто добавим в зависимости встроенные роли `vshard-router` и `vshard-storage`. 131 | 132 | И сразу стоит обратить внимание на то, какая у ролей структура: 133 | `init`, `validate_config`, `apply_config`, `stop`. 134 | При этом все настройки картриджа производятся с помощью `cartridge.cfg()` - это некоторая 135 | обертка над box.cfg(). Соответственно в дальнейшем использовать `box.cfg{}` не разрешается. 136 | 137 | После таких несложных шагов у нас есть почти готовое простое приложение из 138 | стораджей и роутера. При этом мы можем достаточно просто изменять 139 | топологию нашего кластера - добавлять или отключать роли. 140 | 141 | В шаблоне приложения используются встроенные роли - vshard-storage и vshard-router, 142 | а также заготовка для нашей кастомной роли. 143 | В лекции про vshard говорилось о важности того, 144 | чтобы на каждом из инстансов была идентичная конфигурация. 145 | Обеспечением этого занимается картридж. 146 | 147 | ### Vshard-группы 148 | 149 | При этом иногда возникает необходимость хранить данные не в одном сторадже, а в нескольких. 150 | Т.е. есть не одно хранилище данных, а несколько. При этом хочется иметь возможность обращаться 151 | к каждому хранилищу из своего приложения. Это тоже реализуется достаточно несложно и называется 152 | vshard-группы. 153 | Допишем просто в `cartridge.cfg{ vshard_groups = {'default', 'new'} }` - default существует по умолчанию, 154 | все запущенные до этого стораджа находятся в этой группе. 155 | А в группу `new` мы добавим 2 новых инстанса - мастера и реплику. 156 | 157 | Получить доступ к различным инстансам роутера можно с помощью 158 | `cartridge.service_get('vshard-router').get('new')`. 159 | 160 | #### Remote procedure call 161 | Кроме указанных выше обязательных функций роль может определять 162 | свои собственные публичные функции. Т.е. роль может предоставить некоторый набор функций - 163 | интерфейс, с помощью которого другие роли могли бы взаимодействовать с ней. 164 | Объявляются эти функции в том же месте, что и системные. 165 | При этом одна роль может обратиться к другой с помощью функции `cartridge.rpc_call(role_name, fn_name, {args}, {opts})`. 166 | 167 | Внутри, конечно, используется тот же netbox - стандартный способ общения Tarantool инстансов друг с другом. 168 | Однако есть и несколько опций, влияющий на то, на каком конкретно инстансе данная функция будет вызвана. 169 | Например, `prefer_local=true` не будет делать сетевой запрос в случае, если в рамках 170 | нашего инстанса доступна роль, функцию которой мы хотим вызвать. `prefer_leader=true` гарантирует 171 | нам, что вызов будет сделан к инстансу-лидеру репликасета с включенной данной ролью - 172 | это необходимо, если мы делаем пишущий запрос. 173 | 174 | ### Конфигурация 175 | 176 | Вся конфигурация картриджа хранится в yaml-файлах. Т.е. в случае неполадок, 177 | если по какой-то причине доступ к инстансу потерян, нам не нужен доступ к спейсам 178 | и инстансу Tarantool как таковому - достаточно обновить файл, лежащий на 179 | файловой системе. 180 | 181 | Часть конфигурации является публичной - в первую очередь это секции, 182 | которые задает сам пользователь и, например, схема данных. 183 | Часть секций приватная - топология, пользователи... 184 | К этой части конфигурации пользователи имеют доступ через Lua API, 185 | GraphQL API и UI, но скачивать или обновлять напрямую (через `config_patch_clusterwide`) 186 | эти секции не получится. 187 | 188 | В общем случае для модификации конфигурации есть несколько способов. 189 | * Загрузка `yml`-файла через UI; 190 | * Редактирование и загрузка через вкладку "Code"; 191 | * Из кода приложения загрузка через Lua API или обновление с помощью `cartridge.config_patch_clusterwide`. 192 | 193 | Далее картридж сам рассылает конфигурацию на каждый инстанс, валидирует и применяет. 194 | Это делается с помощью двухфазного коммита. 195 | Поэтому если возникает ошибка на этапе validate_config, то конфигурация не применяется совсем. 196 | Ошибки на этапе init или apply_config приводят к переходу инстанса в состояние, 197 | в котором доступны лишь повторное применения конфигурации, либо требуется ручное вмешательство. 198 | 199 | Стоит отметить тот факт, что до применения конфигурации пользователь в состоянии 200 | модифицировать конфиг (например, установить значения по умолчанию для каких-либо секций). 201 | Делается это с помощью модуля `cartridge.twophase` и его функции `on_patch`, 202 | принимающей пользовательский обработчик. 203 | 204 | Отдельным файлом конфигурации является `.tarantool.cookie`. В нем хранится cluster-cookie - 205 | некоторый секрет, с помощью которого инстансы авторизуют друг друга. 206 | По факту, данная строка является паролем к пользователю `admin` - именно под этим 207 | пользователем инстансы общаются друг с другом. 208 | 209 | #### GraphQL API 210 | 211 | До этого мы рассматривали только REST API - получение, обновление, удаление, добавление 212 | с помощью GET, PUT, DELETE, POST. 213 | При этом у нас нет возможности выбрать какое-то определенное подмножество полей. 214 | Кроме этого, обычно у нас на одну сущность отдельный эндпоинт. 215 | Эту проблему решает язык запросов GraphQL. 216 | 217 | ![image](https://user-images.githubusercontent.com/8830475/112021672-9642e180-8b42-11eb-82fb-034c91b2b06b.png) 218 | 219 | Подробно узнать о данном языке запросов можно в [официальной документации](https://graphql.org/), 220 | нас же будут интересовать конкретные примеры запросов. 221 | 222 | Их бывает несколько видов, но мы будем рассматривать `query` и `mutation` - 223 | запросы "читающие" и "пишущие". 224 | 225 | ```graphql 226 | query OperationName { 227 | servers { 228 | uri 229 | } 230 | } 231 | ``` 232 | 233 | ```graphql 234 | mutation turnAuth($enabled: Boolean) { 235 | cluster { 236 | authParams: auth_params(enabled: $enabled) { 237 | enabled 238 | } 239 | } 240 | } 241 | ``` 242 | 243 | GraphQL запрос, сделанный с помощью утилиты curl: 244 | ```bash 245 | curl --location --request POST 'localhost:8080/admin/api' \ 246 | --header 'Content-Type: application/json' \ 247 | --data-raw '{"query":"mutation turnAuth($enabled: Boolean) { cluster { authParams: auth_params(enabled: $enabled) { enabled } } }","variables":{"enabled":true}}' 248 | ``` 249 | 250 | ### Failover 251 | 252 | ![image](https://user-images.githubusercontent.com/8830475/110230737-f5d6a580-7f23-11eb-932a-cfb2a7148c5e.png) 253 | 254 | Никто не застрахован от проблем с сетью, проблем с питанием или ошибок в ПО. 255 | При этом подобные сбои не должны оказывать видимого влияния на нашу систему. 256 | Для того, чтобы устранять подобные проблемы, нам необходим механизм 257 | аварийного переключения - `failover`. 258 | По умолчанию, если инстанс перестает работать, никаких действий не производится. 259 | Продемонстрируем остановив какой-либо инстанс с помощью команды `kill -18 `. 260 | (`pid` можно посмотреть в файлах `tmp/run/..pid`). 261 | Если мы остановим мастера стораджа, то спустя какое-то время он перейдет в состояние `dead`. 262 | При этом записывать что-либо на данный репликасет уже не получится. 263 | При этом у нас есть реплика. И в целом, если мастера в репликасете больше нет, 264 | то мы можем делать пишущие запросы на реплику, предварительно переключив её в `read_only=false`. 265 | 266 | Этим и должен заниматься `failover`. Вернем инстанс обратно - `kill -19 `. 267 | И откроем вкладку `failover`. 268 | 269 | ![image](https://user-images.githubusercontent.com/8830475/110309507-7b388380-8012-11eb-92ea-82bf3b4b974d.png) 270 | 271 | Нам доступно несколько вариантов. Начнем с `eventual` режима. Этот режим появился исторически раньше, 272 | не является надежным, но в целом подходит, для локальной разработки, например, т.к. не требует 273 | никаких зависимостей. Теперь при остановке сервера мы увидим, что лидер в репликасете поменялся, 274 | т.е. пишущие запросы не будут падать. Восстановив работоспособность прежнего мастера, мы увидим, 275 | что новый мастер перестанет быть таковым и вернет "корону" прежнему мастеру. 276 | 277 | Почему это плохо? 278 | Работоспособность данного `failover` основывается на алгоритме SWIM, который 279 | не гарантирует строгой консистентности в какой-то момент времени. 280 | Поэтому не исключена ситуация, при которой в репликасете будут 2 лидера - 281 | что может привести к потере данных и конфликтам репликации. 282 | 283 | Более надежным является `stateful failover` в отличие от `eventual` он дает 284 | больше гарантий консистентности. Это происходит, главным образом, за счет того, что 285 | лидер определяется не с помощью слухов в кластере, а при помощи внешнего координатора. 286 | Таким образом в кластере будет всегда один лидер. 287 | Но это требует того самого внешнего координатора - пока доступны всего 2. 288 | Это отдельный инстанс Tarantool - `stateboard` или `etcd2`. 289 | Также требуется назначить роль `failover-coordinator` - данная роль взаимодействует со 290 | `state provider` и принимает все решения. В случае, если запущено несколько таких ролей - 291 | принимать решения сможет тот, кто самый первый захватит блокировку. 292 | 293 | Теперь проделаем тоже, что и в предыдущем примере - отключим инстанс, убедимся, что мастер 294 | действительно переключается, восстановим инстанс, и увидим, что старый мастер стал простой 295 | репликой. И это не просто так. За время, пока он не был доступен, могло произойти достаточно большое 296 | количество изменений, поэтому нельзя просто взять и переключить мастера обратно - 297 | это может привести к потере данных. 298 | Для переключения мастера есть специальный механизм - `switchover`. 299 | Который бывает консистентным и нет. Консистентность значит, что в случае асинхронной репликации 300 | мы не переключим мастера, пока данные не будут синхронизированы. 301 | Без консистентности у нас нет таких гарантий, а значит и часть данных может быть потеряна. 302 | 303 | Stateful failover не дает защиты от проблемы split-brain - разделения кластера 304 | на независимые части. Для решения этой проблемы реализован fencing - функциональность, 305 | которая следит за тем, чтобы был собран кворум. При потере кворума инстансы автоматически 306 | переключаются в read-only режим. 307 | 308 | Наконец, стоит вспомнить алгоритм `RAFT`, который обсуждался до этого. 309 | Да, с помощью него можно всё это организовать. 310 | При этом данная функциональность достаточно новая - в ней всё ещё есть недоделки, 311 | она не будет работать со старыми версиями, добавить в `Cartridge` её просто не успели. 312 | 313 | ### Домашнее задание 314 | 315 | Переписать приложение из [5](/05_replication_and_sharding/05_replication_and_sharding.md) домашнего задания 316 | на [Tarantool Cartridge](https://github.com/tarantool/cartridge). 317 | -------------------------------------------------------------------------------- /08_deploy/practice/Vagrantfile: -------------------------------------------------------------------------------- 1 | boxes = [ 2 | { 3 | :name => "vm1", 4 | :ip => "172.19.0.2", 5 | :ports => [8181, 8182], 6 | }, 7 | { 8 | :name => "vm2", 9 | :ip => "172.19.0.3", 10 | :ports => [8191, 8192], 11 | }, 12 | ] 13 | 14 | Vagrant.configure("2") do |config| 15 | config.vm.provider "virtualbox" do |v| 16 | v.memory = 2048 17 | end 18 | 19 | # Base Vagrant VM configuration 20 | config.vm.box = "centos/7" 21 | config.ssh.insert_key = false 22 | config.vm.synced_folder ".", "/vagrant", disabled: true 23 | 24 | # Configure all VMs 25 | boxes.each_with_index do |box, index| 26 | config.vm.define box[:name] do |box_config| 27 | box_config.vm.hostname = box[:hostname] 28 | box_config.vm.network "private_network", ip: box[:ip] 29 | box[:ports].each do |port| 30 | box_config.vm.network "forwarded_port", 31 | guest: port, 32 | host: port, 33 | autocorrect: true 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /08_deploy/practice/hosts.test.yml: -------------------------------------------------------------------------------- 1 | all: 2 | vars: 3 | ansible_user: vagrant 4 | 5 | # may be useful for vagrant 6 | ansible_ssh_private_key_file: ~/.vagrant.d/insecure_private_key 7 | ansible_ssh_common_args: "-o IdentitiesOnly=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" 8 | 9 | hosts: 10 | vm1: 11 | ansible_host: "172.19.0.2" 12 | 13 | vm2: 14 | ansible_host: "172.19.0.3" 15 | -------------------------------------------------------------------------------- /08_deploy/practice/playbook.test.yml: -------------------------------------------------------------------------------- 1 | - name: Configure machines 2 | gather_facts: false 3 | become: true 4 | become_user: root 5 | hosts: all 6 | tasks: 7 | - name: Install unzip 8 | yum: 9 | name: unzip 10 | state: latest 11 | 12 | - name: Place some file 13 | copy: 14 | content: | 15 | Hi, I am here! 16 | dest: "/tmp/hello.txt" 17 | mode: "644" 18 | -------------------------------------------------------------------------------- /09_testing_monitoring/09_testing_monitoring.md: -------------------------------------------------------------------------------- 1 | ## Тестирование 2 | 3 | На предыдущем занятии мы рассмотрели деплой. 4 | Мы поняли, что делать с приложением после написания, 5 | как доставлять его до серверов, запускать, эксплуатировать. 6 | 7 | В этот раз будет нечто среднее. С одной стороны, мы отойдем на 8 | шаг назад, с другой стороны посмотрим, что происходит с системой 9 | уже после деплоя. 10 | 11 | Итак, у нас есть большое-большое приложение. Мы делаем в нем очередное изменение 12 | и... какая-то часть функционала перестает работать. 13 | Если ошибка появляется уже во время промышленной эксплуатации - страдают 14 | пользователи, уходят от нас, что приводит к финансовым и репутационным потерям. 15 | 16 | И данную проблему помогает решить тестирование. 17 | По сути, после каждого изменения нам нужно проверять, а 18 | не сломали ли мы что-то. И это можно делать руками - но это долго. 19 | Особенно если в нашем приложении сотни тысяч - миллионы строк кода. 20 | Так мы приходим к необходимости автоматического тестирования. 21 | Оно возможно не всегда - например, проверять интерфейсы лучше руками. 22 | Но при работе с серверными приложениями автоматизированное тестирование 23 | чаще всего возможно. 24 | 25 | Итак, пока мы можем классифицировать тестирование по двум направлениям - 26 | ручное и автоматическое. Но, на самом деле, всё куда обширнее и тестирование 27 | может производится с разным знанием о системе (методом черного/серого/белого 28 | ящика) или тестироваться могут разные компоненты системы (отдельно/в связке 29 | друг с другом...). 30 | 31 | Было сказано много слов, но в общем данное занятие не ставит целью 32 | рассказать максимально подробно про тестирование. Мы будем разбирать 33 | инструменты, с помощью которых можно писать свои тесты. 34 | 35 | ### Тестирование на примере Python 36 | 37 | Каким бы языком вы не начали пользоваться, 38 | обычно существуют уже готовые фреймворки для тестирования. 39 | Так в языке Python есть встроенный модуль unittest. 40 | 41 | Попробуем написать какой-нибудь простой тест. 42 | Будем тестировать некоторый абстрактный стек. 43 | Даже без запуска можно пройтись по примитивам, которые 44 | используются в тестах: 45 | 46 | * test fixture - подготовка, необходимая для выполнения тестов и все необходимые действия для очистки после выполнения тестов. 47 | Это может включать, например, создание временных баз данных или запуск серверного процесса. 48 | * test case - минимальный блок тестирования. Он проверяет ответы для разных наборов данных. 49 | * test suite - несколько тестовых случаев, наборов тестов или и того и другого. 50 | Используется для объединения тестов, которые должны быть выполнены вместе. 51 | * test runner - компонент, который управляет выполнением тестов и предоставляет пользователю результат. 52 | Исполнитель может использовать графический или текстовый интерфейс или возвращать специальное значение, которое сообщает о результатах выполнения тестов. 53 | 54 | ```py 55 | import unittest 56 | import sys 57 | import os 58 | sys.path.append(os.path.dirname(os.path.dirname(__file__))) 59 | from stack import Stack 60 | 61 | 62 | class TestStack(unittest.TestCase): 63 | def setUp(cls): 64 | print('Test started') 65 | 66 | def test_empty(self): 67 | stack = Stack() 68 | self.assertIsNone(stack.pop()) 69 | self.assertIsNone(stack.top()) 70 | 71 | def test_push_pop(self): 72 | stack = Stack() 73 | input_values = [1, 'abc', 'test', {}] 74 | output_values = input_values.copy() 75 | output_values.reverse() 76 | 77 | for value in input_values: 78 | stack.push(value) 79 | self.assertEqual(value, stack.top()) 80 | 81 | for expected in output_values: 82 | value = stack.pop() 83 | self.assertEqual(expected, value) 84 | 85 | self.assertIsNone(stack.pop()) 86 | self.assertIsNone(stack.top()) 87 | 88 | def test_clear(self): 89 | stack = Stack() 90 | stack.push('value') 91 | self.assertIsNotNone(stack.top()) 92 | stack.clear() 93 | self.assertIsNone(stack.top()) 94 | ``` 95 | 96 | Запуск выглядит примерно так: 97 | ```bash 98 | python3 -m unittest discover -v test 99 | test_clear (test_stack.TestStack) ... ok 100 | test_empty (test_stack.TestStack) ... ok 101 | test_push_pop (test_stack.TestStack) ... ok 102 | 103 | ---------------------------------------------------------------------- 104 | Ran 3 tests in 0.000s 105 | 106 | OK 107 | ``` 108 | 109 | Как следует из названия unittest в первую очередь предназначен для тестирования 110 | изолированных модулей. Для интеграционного тестирования обычно используются 111 | другие фреймворки, например [Pytest](https://docs.pytest.org/). 112 | 113 | ### Тестирование в Tarantool 114 | 115 | #### TAP 116 | 117 | Для написания простых тестов Tarantool имеет встроенный модуль `tap` - [Test Anything Protocol](https://testanything.org/). 118 | 119 | ```lua 120 | #!/usr/bin/env tarantool 121 | local semver = require('common.semver') 122 | 123 | local tap = require('tap') 124 | 125 | local test = tap.test('semver tests') 126 | 127 | test:plan(1) 128 | 129 | test:test('validate_operator', function(t) 130 | local subject = semver.validate_operator 131 | t:plan(8) 132 | t:is(subject('=='), true) 133 | t:is(subject('<='), true) 134 | t:is(subject('>='), true) 135 | t:is(subject('<'), true) 136 | t:is(subject('>'), true) 137 | t:is(subject('='), false) 138 | t:is(subject('<>'), false) 139 | t:is(subject('~'), false) 140 | end) 141 | 142 | os.exit(test:check() and 0 or 1) 143 | ``` 144 | 145 | Результат: 146 | ```log 147 | TAP version 13 148 | 1..1 149 | # validate_operator 150 | 1..8 151 | ok - nil 152 | ok - nil 153 | ok - nil 154 | ok - nil 155 | ok - nil 156 | ok - nil 157 | ok - nil 158 | ok - nil 159 | # validate_operator: end 160 | ok - validate_operator 161 | ``` 162 | 163 | #### Test-run 164 | 165 | Написанный на Python, фреймворк для тестирования самого Tarantool - https://github.com/tarantool/test-run. 166 | 167 | #### Luatest 168 | 169 | Фреймворк для тестирования, написанный на Lua. 170 | 171 | ```lua 172 | local server = luatest.Server:new({ 173 | command = '/path/to/executable.lua', 174 | -- arguments for process 175 | args = {'--no-bugs', '--fast'}, 176 | -- additional env vars to pass to process 177 | env = {SOME_FIELD = 'value'}, 178 | -- passed as TARANTOOL_WORKDIR 179 | workdir = '/path/to/test/workdir', 180 | -- passed as TARANTOOL_HTTP_PORT, used in http_request 181 | http_port = 8080, 182 | -- passed as TARANTOOL_LISTEN, used in connect_net_box 183 | net_box_port = 3030, 184 | -- passed to net_box.connect in connect_net_box 185 | net_box_credentials = {user = 'username', password = 'secret'}, 186 | }) 187 | server:start() 188 | -- Wait until server is ready to accept connections. 189 | -- This may vary from app to app: for one server:connect_net_box() is enough, 190 | -- for another more complex checks are required. 191 | luatest.helpers.retrying({}, function() server:http_request('get', '/ping') end) 192 | 193 | -- http requests 194 | server:http_request('get', '/path') 195 | server:http_request('post', '/path', {body = 'text'}) 196 | server:http_request('post', '/path', {json = {field = value}, http = { 197 | -- http client options 198 | headers = {Authorization = 'Basic ' .. credentials}, 199 | timeout = 1, 200 | }}) 201 | 202 | -- This method throws error when response status is outside of then range 200..299. 203 | -- To change this behaviour, path `raise = false`: 204 | t.assert_equals(server:http_request('get', '/not_found', {raise = false}).status, 404) 205 | t.assert_error(function() server:http_request('get', '/not_found') end) 206 | 207 | -- using net_box 208 | server:connect_net_box() 209 | server.net_box:eval('return do_something(...)', {arg1, arg2}) 210 | 211 | server:stop() 212 | ``` 213 | 214 | Основные возможности: 215 | * Набор функций для управления инстансами Tarantool; 216 | * Подходит не только для юнит, но и для интеграционных тестов; 217 | * Можно запускать с помощью Tarantool - не требуется дополнительных зависимостей. 218 | 219 | #### Упражнение 220 | 221 | Написать простое CRUD-приложение (1 инстанс) и протестировать его. 222 | 223 | ### Нефункциональное тестирование 224 | 225 | Обычно, кроме работающей логики, нас ещё и интересует, насколько 226 | быстро она выполняется. 227 | В частности, мы хотим оценивать сколько запросов за единицу времени 228 | способен обработать наш сервис. 229 | Для таких тестов используются другие фреймворки - их существует 230 | достаточно большое количество и с достаточно большим разбросом функциональности. 231 | В курсе мы будем рассматривать [wrk](https://github.com/wg/wrk). 232 | С помощью небольших луа-скриптов мы сможем формировать запросы, 233 | которые затем будут посылаться на сервер. 234 | В конце мы получаем небольшой отчет следующего формата: 235 | ``` 236 | # wrk -s script.lua -c 1000 -t 20 -d 1m http://localhost:8081 237 | Running 1m test @ http://localhost:8081 238 | 20 threads and 1000 connections 239 | Thread Stats Avg Stdev Max +/- Stdev 240 | Latency 153.47ms 61.78ms 1.99s 72.15% 241 | Req/Sec 102.51 81.21 594.00 75.83% 242 | 114860 requests in 1.00m, 14.67MB read 243 | Socket errors: connect 0, read 8157, write 31, timeout 352 244 | Non-2xx or 3xx responses: 12188 245 | Requests/sec: 1911.07 246 | Transfer/sec: 250.00KB 247 | ``` 248 | 249 | У самого скрипта есть специальный формат: 250 | ```lua 251 | local function read_file(path) 252 | local file = io.open(path, 'r') 253 | if file == nil then 254 | error(('Failed to open file %s'):format(path)) 255 | end 256 | local buf = file:read('*a') 257 | file:close() 258 | return buf 259 | end 260 | 261 | id = 1 262 | function setup(thread) 263 | thread:set('ctn', id) 264 | thread:set('id', id) 265 | id = id + 1 266 | thread:set('body', read_file('test_data.json')) 267 | end 268 | 269 | function request() 270 | ctn = ctn + id 271 | local req = wrk.format('POST', '/http', { ['Content-Type'] = 'application/json' }, body) 272 | return req 273 | end 274 | 275 | function done(summary, latency, requests) 276 | print(summary, latency, requests) 277 | end 278 | ``` 279 | 280 | Более подробно и с примерами можно посмотреть [тут](https://github.com/wg/wrk/tree/master/scripts). 281 | 282 | Выбор инструментов для нагрузочного тестирования довольно широк. 283 | Из довольно известных стоит отметить [Yandex.Tank](https://github.com/yandex/yandex-tank) и [JMeter](https://jmeter.apache.org/). 284 | 285 | ## Мониторинг 286 | 287 | Работающее приложение - не является чем-то статическим. 288 | Более того, его нельзя просто запустить и забыть о нем. 289 | Необходимо отслеживать состояние приложения, чтобы своевременно 290 | реагировать на различного рода ситуации (в том числе аварийные). 291 | 292 | Это решается с помощью мониторинга. 293 | Ваше приложение должно предоставлять некоторый набор метрик 294 | (например, количество удачно обработанных запросов, количество неудачно обработанных 295 | запросов, количество запросов, обрабатываемых за единицу времени), 296 | также бывают интересны метрики платформы, на которой работает приложение, например, 297 | количество свободной памяти. 298 | 299 | Давайте рассмотрим мониторинг с двух позиций - хранения и визуализация. 300 | 301 | ### Prometheus 302 | 303 | [Prometheus](https://prometheus.io/) - это time-series database - база данных 304 | для хранения временных рядов. Т.е. данная база предназначена для 305 | хранения некоторых показаний, изменяющихся во времени. 306 | Кроме этого, предполагается, наличие специальных запросов к таким данным. 307 | 308 | ![image](https://user-images.githubusercontent.com/8830475/111695099-c2f5b100-8843-11eb-8759-1902d6e207ee.png) 309 | 310 | ![image](https://user-images.githubusercontent.com/8830475/111695146-d30d9080-8843-11eb-92dd-890bf1d9c259.png) 311 | 312 | Откуда берутся данные? Есть две стратегии - Push и Pull - т.е. само приложение может присылать 313 | метрики или наоборот Prometheus может периодически опрашивать приложение. 314 | Поведением по умолчанию является именно второй вариант. 315 | Обычно приложение выставляет некоторый HTTP-эндпоинт, куда в специальном 316 | формате выгружаются метрики. 317 | ``` 318 | # HELP tnt_info_memory_data Memorydata 319 | # TYPE tnt_info_memory_data gauge 320 | tnt_info_memory_data 1025016 321 | # HELP tnt_info_memory_index Memoryindex 322 | # TYPE tnt_info_memory_index gauge 323 | tnt_info_memory_index 3162112 324 | # HELP tnt_info_memory_lua Memorylua 325 | # TYPE tnt_info_memory_lua gauge 326 | tnt_info_memory_lua 51735433 327 | # HELP tnt_info_memory_net Memorynet 328 | # TYPE tnt_info_memory_net gauge 329 | tnt_info_memory_net 63799584 330 | ``` 331 | 332 | В Prometheus используются следующие типы метрик: 333 | 334 | * Счётчик (counter) — хранит значения, которые увеличиваются с течением времени 335 | (например, количество запросов к серверу); 336 | * Шкала (gauge) — хранит значения, которые с течением времени могут как увеличиваться, 337 | так и уменьшаться (например, объём используемой оперативной памяти или количество операций ввода-вывода); 338 | * Гистограмма (histogram) — хранит информацию об изменении некоторого параметра в течение определённого промежутка 339 | (например, общее количество запросов к серверу в период с 11 до 12 часов и количество запросов к этому же серверов в период с 11.30 до 11.40); 340 | * Cводка результатов (summary) — как и гистограмма, хранит информацию об изменении значения некоторого параметра за временной интервал, 341 | но также позволяет рассчитывать квантили для скользящих временных интервалов. 342 | 343 | В Tarantool для работы с метриками есть специальный модуль - [metrics](https://github.com/tarantool/metrics). 344 | Модуль позволяет экспортировать практически все метрики Tarantool (box.info(), box.stat(), box.slab(), ...) в 345 | различных форматах (в том числе и Prometheus). 346 | Также пользователю доступен инструментарий для создания своих метрик. 347 | 348 | Попробуем запустить Prometheus. 349 | Для этого нам понадобиться конфигурационный файл: 350 | ```yaml 351 | global: 352 | scrape_interval: 1m # Set the scrape interval to every 15 seconds. Default is every 1 minute. 353 | evaluation_interval: 1m # Evaluate rules every 15 seconds. The default is every 1 minute. 354 | # scrape_timeout is set to the global default (10s). 355 | 356 | # Alert manager configuration 357 | alerting: 358 | alertmanagers: 359 | - static_configs: 360 | - targets: 361 | # - alertmanager:9093 362 | 363 | # Load rules once and periodically evaluate them according to the global 'evaluation_interval'. 364 | rule_files: 365 | # - "first_rules.yml" 366 | # - "second_rules.yml" 367 | 368 | # A scrape configuration containing exactly one endpoint to scrape: 369 | # Here it's Prometheus itself. 370 | scrape_configs: 371 | # The job name is added as a label `job=` to any timeseries scraped from this config. 372 | - job_name: "prometheus" 373 | 374 | # metrics_path defaults to '/metrics' 375 | # scheme defaults to 'http'. 376 | 377 | static_configs: 378 | - targets: ["localhost:9090"] 379 | 380 | - job_name: "example_project" 381 | static_configs: 382 | - targets: 383 | - "localhost:8081" 384 | - "localhost:8082" 385 | - "localhost:8083" 386 | - "localhost:8084" 387 | - "localhost:8085" 388 | metrics_path: "/metrics/prometheus" 389 | ``` 390 | 391 | Здесь мы задаем то, где будет работать Prometheus (`localhost:9090`), 392 | какие приложения будет опрашивать (`localhost:8081-8085`), 393 | с какой периодичностью и по какому адресу. 394 | 395 | Запуск - `prometheus --config.file="prometheus.yml"`. 396 | 397 | После этого мы получаем доступ к UI на `localhost:9090`. 398 | 399 | Можем посмотреть за тем, каково состояние инстансов, которые мы мониторим: 400 | ![image](https://user-images.githubusercontent.com/8830475/111697012-0d782d00-8846-11eb-9ce1-a7158a648c2a.png) 401 | 402 | Можем делать запросы с помощью специального языка [PromQL](https://prometheus.io/docs/prometheus/latest/querying/basics/): 403 | 404 | ![image](https://user-images.githubusercontent.com/8830475/111697403-93947380-8846-11eb-8568-548465586faf.png) 405 | 406 | Понятно, что воспринимать цифры достаточно сложно, поэтому требуется 407 | некоторый UI, который бы мог использовать Prometheus в качестве источника данных. 408 | 409 | ### Grafana 410 | 411 | 412 | 413 | [Grafana](https://grafana.com/) — это платформа с открытым исходным кодом для визуализации, 414 | мониторинга и анализа данных. 415 | Grafana позволяет пользователям создавать дашборды с панелями, 416 | каждая из которых отображает определенные показатели в течение установленного периода времени. 417 | Каждый дашборд универсален, 418 | поэтому его можно настроить для конкретного проекта или с учетом любых потребностей разработки и/или бизнеса. 419 | 420 | ![image](https://user-images.githubusercontent.com/8830475/111698158-8b890380-8847-11eb-99bc-aac9c47c7277.png) 421 | 422 | ![image](https://user-images.githubusercontent.com/8830475/111698911-73fe4a80-8848-11eb-9e32-ae35f025701d.png) 423 | 424 | В качестве демонстрации рассмотрим [tarantool/grafana-dashboard](https://github.com/tarantool/grafana-dashboard). 425 | 426 | Рекомендация: посмотреть на дашборд какого-либо приложения под нагрузкой. 427 | 428 | ### Tracing 429 | 430 | Ещё один подход к интроспекции запросов - трассировка запросов. 431 | В систему поступает запрос, он делится на некоторые стадии (парсинг запроса, вычисления, сохранение в БД и т.д), 432 | мы хотим узнать, какая из стадий сколько выполняется. 433 | Особенно интересно видеть поведение системы под нагрузкой - оно может отличаться от поведения 434 | без нагрузки. 435 | 436 | #### OpenTracing 437 | 438 | 439 | 440 | [OpenTracing](https://opentracing.io/) - спецификация, цель которой - унификация инструментов и методов 441 | для трассировки запросов вне зависимости от платформы и языка программирования. 442 | 443 | ![image](https://user-images.githubusercontent.com/8830475/112750541-da663400-8fd1-11eb-8d15-13350f7ab348.png) 444 | 445 | Рассмотрим примитивы, с которыми имеет дело OpenTracing. 446 | 447 | * Trace - путь нашего запроса 448 | * Span - блок кода, который трассируется 449 | * SpanContext - контекст, который хранит в себе информацию о том, как Span'ы должны быть связаны между собой: 450 | trace_id, span_id, parent_id... 451 | 452 | После сбора информация отправляется в специальную систему, которая занимается 453 | хранением, сборов и визуализацией трейсов. Примерами таких систем являются [Zipkin](https://zipkin.io/) и [Jaeger](https://www.jaegertracing.io/). 454 | 455 | ![image](https://user-images.githubusercontent.com/8830475/112750971-13070d00-8fd4-11eb-9a59-d5bf5a1dd74c.png) 456 | 457 | В Tarantool работа с трейсингом возможна с помощью модуля [tracing](https://github.com/tarantool/tracing). 458 | 459 | #### Домашнее задание 460 | 461 | Для домашнего задания "CRUD" настроить экспорт метрик в формате Prometheus и Grafana Dashbord. 462 | Самостоятельно выбрать список метрик и их типы 463 | (написать в README.md краткий отчет по ним + скриншоты). 464 | Провести нагрузочное тестирование каждой из операций (insert/get/...) - 465 | предоставить отчет и скрипты для тестирования. 466 | -------------------------------------------------------------------------------- /09_testing_monitoring/materials/monitoring/project/.cartridge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | run_dir: 'tmp' 3 | -------------------------------------------------------------------------------- /09_testing_monitoring/materials/monitoring/project/.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | 9 | [CMakeLists.txt] 10 | indent_style = space 11 | indent_size = 4 12 | 13 | [*.cmake] 14 | indent_style = space 15 | indent_size = 4 16 | 17 | [*.lua] 18 | indent_style = space 19 | indent_size = 4 20 | 21 | [*.{h,c,cc}] 22 | indent_style = tab 23 | tab_width = 8 24 | -------------------------------------------------------------------------------- /09_testing_monitoring/materials/monitoring/project/.luacheckrc: -------------------------------------------------------------------------------- 1 | std = { 2 | read_globals = {'require', 'debug', 'pcall', 'xpcall', 'tostring', 3 | 'tonumber', 'type', 'assert', 'ipairs', 'math', 'error', 'string', 4 | 'table', 'pairs', 'os', 'io', 'select', 'unpack', 'dofile', 'next', 5 | 'loadstring', 'setfenv', 6 | 'rawget', 'rawset', '_G', 7 | 'getmetatable', 'setmetatable', 8 | 'print', 'tonumber64', 'arg' 9 | 10 | }, 11 | globals = {'box', 'vshard', 'package', 12 | 'applier' 13 | } 14 | } 15 | redefined = false 16 | -------------------------------------------------------------------------------- /09_testing_monitoring/materials/monitoring/project/.luacov: -------------------------------------------------------------------------------- 1 | statsfile = 'tmp/luacov.stats.out' 2 | reportfile = 'tmp/luacov.report.out' 3 | exclude = { 4 | '/test/', 5 | } 6 | -------------------------------------------------------------------------------- /09_testing_monitoring/materials/monitoring/project/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM tarantool/tarantool:1.x-centos7 2 | 3 | WORKDIR /app 4 | 5 | RUN yum install -y git \ 6 | cmake \ 7 | make \ 8 | gcc 9 | COPY . . 10 | RUN mkdir -p tmp 11 | 12 | RUN tarantoolctl rocks make 13 | RUN tarantoolctl rocks install luatest 0.5.0 14 | -------------------------------------------------------------------------------- /09_testing_monitoring/materials/monitoring/project/Dockerfile.build.cartridge: -------------------------------------------------------------------------------- 1 | # Simple Dockerfile 2 | # Used by `pack` command as a base for build image 3 | # when --use-dcoker option is specified 4 | 5 | # The base image must be centos:8 6 | FROM centos:8 7 | 8 | # Here you can install some packages required 9 | # for your application build 10 | 11 | # RUN set -x \ 12 | # && curl -sL https://rpm.nodesource.com/setup_10.x | bash - \ 13 | # && yum -y install nodejs 14 | -------------------------------------------------------------------------------- /09_testing_monitoring/materials/monitoring/project/Dockerfile.cartridge: -------------------------------------------------------------------------------- 1 | # Simple Dockerfile 2 | # Used by `pack docker` command as a base for runtime image 3 | 4 | # The base image must be centos:8 5 | FROM centos:8 6 | 7 | # Here you can install some packages required 8 | # for your application in runtime 9 | # 10 | # For example, if you need to install some python packages, 11 | # you can do it this way: 12 | # 13 | # COPY requirements.txt /tmp 14 | # RUN yum install -y python3-pip 15 | # RUN pip3 install -r /tmp/requirements.txt 16 | -------------------------------------------------------------------------------- /09_testing_monitoring/materials/monitoring/project/app/roles/custom.lua: -------------------------------------------------------------------------------- 1 | local cartridge = require('cartridge') 2 | local config = require('cartridge.argparse') 3 | local fiber = require('fiber') 4 | 5 | local function init(opts) 6 | local local_cfg = config.get_opts({ 7 | user = 'string', 8 | password = 'string' 9 | }) 10 | 11 | local metrics = cartridge.service_get('metrics') 12 | local http_middleware = metrics.http_middleware 13 | 14 | local http_collector = http_middleware.build_default_collector('summary') 15 | 16 | local httpd = cartridge.service_get('httpd') 17 | httpd:route( 18 | { method = 'GET', path = '/hello' }, 19 | http_middleware.v1( 20 | function() 21 | fiber.sleep(0.02) 22 | return { status = 200, body = 'Hello world!' } 23 | end, 24 | http_collector 25 | ) 26 | ) 27 | httpd:route( 28 | { method = 'GET', path = '/hell0' }, 29 | http_middleware.v1( 30 | function() 31 | fiber.sleep(0.01) 32 | return { status = 400, body = 'Hell0 world!' } 33 | end, 34 | http_collector 35 | ) 36 | ) 37 | httpd:route( 38 | { method = 'POST', path = '/goodbye' }, 39 | http_middleware.v1( 40 | function() 41 | fiber.sleep(0.005) 42 | return { status = 500, body = 'Goodbye cruel world!' } 43 | end, 44 | http_collector 45 | ) 46 | ) 47 | 48 | if opts.is_master then 49 | local sp = box.schema.space.create('MY_SPACE', { if_not_exists = true }) 50 | sp:format({ 51 | { name = 'key', type = 'number', is_nullable = false }, 52 | { name = 'value', type = 'string', is_nullable = false }, 53 | }) 54 | sp:create_index('pk', { parts = { 'key' }, if_not_exists = true }) 55 | 56 | if local_cfg.user and local_cfg.password then 57 | -- cluster-wide user privileges 58 | box.schema.user.create(local_cfg.user, { password = local_cfg.password, if_not_exists = true }) 59 | box.schema.user.grant(local_cfg.user, 'read,write,execute', 'universe', nil, { if_not_exists = true }) 60 | end 61 | end 62 | 63 | return true 64 | end 65 | 66 | local function stop() 67 | end 68 | 69 | local function validate_config(conf_new, conf_old) -- luacheck: no unused args 70 | return true 71 | end 72 | 73 | local function apply_config(conf, opts) -- luacheck: no unused args 74 | -- if opts.is_master then 75 | -- end 76 | 77 | return true 78 | end 79 | 80 | return { 81 | role_name = 'app.roles.custom', 82 | dependencies = { 'cartridge.roles.metrics' }, 83 | init = init, 84 | stop = stop, 85 | validate_config = validate_config, 86 | apply_config = apply_config, 87 | } 88 | -------------------------------------------------------------------------------- /09_testing_monitoring/materials/monitoring/project/cartridge.post-build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Simple post-build script 4 | # Will be ran after `tarantoolctl rocks make` on application packing 5 | # Could be useful to remove some build artifacts from result package 6 | 7 | # For example: 8 | # rm -rf third_party 9 | # rm -rf node_modules 10 | # rm -rf doc 11 | -------------------------------------------------------------------------------- /09_testing_monitoring/materials/monitoring/project/cartridge.pre-build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Simple pre-build script 4 | # Will be ran before `tarantoolctl rocks make` on application build 5 | # Could be useful to install non-standart rocks modules 6 | 7 | # For example: 8 | # tarantoolctl rocks make --chdir ./third_party/my-custom-rock-module 9 | -------------------------------------------------------------------------------- /09_testing_monitoring/materials/monitoring/project/cluster/helper.lua: -------------------------------------------------------------------------------- 1 | local fio = require('fio') 2 | local lt = require('luatest') 3 | local json = require('json') 4 | local Server = require('cartridge.test-helpers.server') 5 | 6 | local helper = {} 7 | 8 | helper.root = fio.dirname(fio.abspath(package.search('init'))) 9 | helper.datadir = fio.pathjoin(helper.root, 'dev') 10 | helper.server_command = fio.pathjoin(helper.root, 'cluster/init.lua') 11 | 12 | lt.before_suite(function() 13 | fio.rmtree(helper.datadir) 14 | fio.mktree(helper.datadir) 15 | end) 16 | 17 | function Server:build_env() 18 | return { 19 | TARANTOOL_ALIAS = self.alias, 20 | TARANTOOL_WORKDIR = self.workdir, 21 | TARANTOOL_HTTP_PORT = self.http_port, 22 | TARANTOOL_ADVERTISE_URI = self.advertise_uri, 23 | TARANTOOL_CLUSTER_COOKIE = self.cluster_cookie, 24 | TARANTOOL_LOG_LEVEL = 6, 25 | TZ = 'Europe/Moscow' 26 | } 27 | end 28 | 29 | Server.constructor_checks.api_host = '?string' 30 | Server.constructor_checks.api_port = '?string' 31 | function Server:api_request(method, path, options) 32 | method = method and string.upper(method) 33 | if not self.http_client then 34 | error('http_port not configured') 35 | end 36 | options = options or {} 37 | local body = options.body or (options.json and json.encode(options.json)) 38 | local http_options = options.http or {} 39 | local url = self.api_host .. ':' .. self.api_port .. path 40 | local response = self.http_client:request(method, url, body, http_options) 41 | local ok, json_body = pcall(json.decode, response.body) 42 | if ok then 43 | response.json = json_body 44 | end 45 | if not options.raw and response.status ~= 200 then 46 | error({type = 'HTTPRequest', response = response}) 47 | end 48 | return response 49 | end 50 | 51 | return helper 52 | -------------------------------------------------------------------------------- /09_testing_monitoring/materials/monitoring/project/cluster/init.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | local fio = require('fio') 4 | if os.getenv('ENABLE_TEST_COVERAGE') == 'true' then 5 | local cfg_luacov = dofile(fio.pathjoin('.luacov')) 6 | cfg_luacov.statsfile = fio.pathjoin('luacov.stats.out') 7 | 8 | local coverage = require('luacov.runner') 9 | coverage.init(cfg_luacov) 10 | rawset(_G, 'coverage', coverage) 11 | end 12 | 13 | local function get_base_dir() 14 | return fio.abspath(fio.dirname(arg[0]) .. '/app/') 15 | end 16 | 17 | local function extend_path(path) 18 | package.path = package.path .. ';' .. path 19 | end 20 | 21 | local function extend_cpath(path) 22 | package.cpath = package.cpath .. ';' .. path 23 | end 24 | 25 | local function set_base_load_paths(base_dir) 26 | extend_path(base_dir .. '/?.lua') 27 | extend_path(base_dir .. '/?/init.lua') 28 | extend_cpath(base_dir .. '/?.dylib') 29 | extend_cpath(base_dir .. '/?.so') 30 | end 31 | 32 | local function set_rocks_load_paths(base_dir) 33 | extend_path(base_dir..'/.rocks/share/tarantool/?.lua') 34 | extend_path(base_dir..'/.rocks/share/tarantool/?/init.lua') 35 | extend_cpath(base_dir..'/.rocks/lib/tarantool/?.dylib') 36 | extend_cpath(base_dir..'/.rocks/lib/tarantool/?.so') 37 | end 38 | 39 | local function set_load_paths(base_dir) 40 | set_base_load_paths(base_dir) 41 | set_rocks_load_paths(base_dir) 42 | end 43 | 44 | set_load_paths(get_base_dir()) 45 | 46 | require('init') 47 | -------------------------------------------------------------------------------- /09_testing_monitoring/materials/monitoring/project/cluster/integration/bootstrap_test.lua: -------------------------------------------------------------------------------- 1 | local test = require('luatest') 2 | local group = test.group('cluster') 3 | 4 | local helper = require('test.helper') 5 | local cluster_helpers = require('cartridge.test-helpers') 6 | 7 | local app = 'example-app' 8 | local log_level = 6 9 | 10 | local cluster_alias = { 11 | tnt_router = 'tnt_router', 12 | tnt_storage_1_master = 'tnt_storage_1_master', 13 | tnt_storage_1_replica = 'tnt_storage_1_replica', 14 | tnt_storage_2_master = 'tnt_storage_2_master', 15 | tnt_storage_2_replica = 'tnt_storage_2_replica', 16 | } 17 | 18 | local instances = {} 19 | 20 | local replicasets = { 21 | { 22 | uuid = cluster_helpers.uuid('a'), 23 | roles = { 'vshard-router', 'app.roles.custom' }, 24 | servers = { 25 | { 26 | instance_uuid = cluster_helpers.uuid('a', 1), 27 | alias = cluster_alias.tnt_router, 28 | env = { 29 | ['TARANTOOL_APP_NAME'] = app, 30 | ['TARANTOOL_LOG_LEVEL'] = log_level, 31 | } 32 | } 33 | }, 34 | }, 35 | { 36 | uuid = cluster_helpers.uuid('b'), 37 | roles = { 'vshard-storage', 'app.roles.custom' }, 38 | servers = { 39 | { 40 | instance_uuid = cluster_helpers.uuid('b', 1), 41 | alias = cluster_alias.tnt_storage_1_master, 42 | env = { 43 | ['TARANTOOL_APP_NAME'] = app, 44 | ['TARANTOOL_LOG_LEVEL'] = log_level, 45 | } 46 | }, 47 | { 48 | instance_uuid = cluster_helpers.uuid('b', 2), 49 | alias = cluster_alias.tnt_storage_1_replica, 50 | env = { 51 | ['TARANTOOL_APP_NAME'] = app, 52 | ['TARANTOOL_LOG_LEVEL'] = log_level, 53 | } 54 | }, 55 | } 56 | }, 57 | { 58 | uuid = cluster_helpers.uuid('c'), 59 | roles = { 'vshard-storage', 'app.roles.custom' }, 60 | servers = { 61 | { 62 | instance_uuid = cluster_helpers.uuid('c', 1), 63 | alias = cluster_alias.tnt_storage_2_master, 64 | env = { 65 | ['TARANTOOL_APP_NAME'] = app, 66 | ['TARANTOOL_LOG_LEVEL'] = log_level, 67 | } 68 | }, 69 | { 70 | instance_uuid = cluster_helpers.uuid('c', 2), 71 | alias = cluster_alias.tnt_storage_2_replica, 72 | env = { 73 | ['TARANTOOL_APP_NAME'] = app, 74 | ['TARANTOOL_LOG_LEVEL'] = log_level, 75 | } 76 | } 77 | }, 78 | } 79 | } 80 | 81 | test.before_suite(function() 82 | group.cluster = cluster_helpers.Cluster:new({ 83 | datadir = helper.datadir, 84 | server_command = helper.server_command, 85 | use_vshard = true, 86 | replicasets = replicasets 87 | }) 88 | 89 | group.cluster:start() 90 | group.cluster:upload_config({ 91 | metrics = { 92 | export = { 93 | { 94 | path = '/metrics/json', 95 | format = 'json' 96 | }, 97 | { 98 | path = '/metrics/prometheus', 99 | format = 'prometheus' 100 | } 101 | } 102 | } 103 | }) 104 | 105 | for instance, alias in pairs(cluster_alias) do 106 | instances[instance] = group.cluster:server(alias) 107 | end 108 | end) 109 | 110 | test.after_suite(function() 111 | group.cluster:stop() 112 | end) 113 | 114 | -- Space operations constants 115 | local SELECT = 'select' 116 | local INSERT = 'insert' 117 | local UPDATE = 'update' 118 | local UPSERT = 'upsert' 119 | local REPLACE = 'replace' 120 | local DELETE = 'delete' 121 | 122 | -- HTTP methods constants 123 | local GET = 'get' 124 | local POST = 'post' 125 | 126 | local function http_request(server, method, endpoint, count) 127 | if count <= 0 then 128 | return 129 | end 130 | 131 | for _ = 1, count do 132 | server:http_request(method, endpoint, { raise = false }) 133 | end 134 | end 135 | 136 | local last_key = 1 137 | 138 | local charset = {} -- [0-9a-zA-Z] 139 | for c = 48, 57 do table.insert(charset, string.char(c)) end 140 | for c = 65, 90 do table.insert(charset, string.char(c)) end 141 | for c = 97, 122 do table.insert(charset, string.char(c)) end 142 | 143 | local function random_string(length) 144 | if not length or length <= 0 then return '' end 145 | math.randomseed(os.clock()^5) 146 | return random_string(length - 1) .. charset[math.random(1, #charset)] 147 | end 148 | 149 | local function space_operations(server, operation, count) 150 | if count <= 0 then 151 | return 152 | end 153 | 154 | local space = server.net_box.space.MY_SPACE 155 | 156 | if operation == SELECT then 157 | for _ = 1, count do 158 | space:select({}, { limit = 1 }) 159 | end 160 | 161 | elseif operation == INSERT then 162 | for _ = 1, count do 163 | space:insert{ last_key, random_string(5) } 164 | last_key = last_key + 1 165 | end 166 | 167 | elseif operation == UPDATE then 168 | for _ = 1, count do 169 | local key = space:select({}, { limit = 1 })[1][1] 170 | space:update(key, {{ '=', 2, random_string(5) }}) 171 | end 172 | 173 | elseif operation == UPSERT then 174 | for _ = 1, count do 175 | local tuple = space:select({}, { limit = 1 })[1] 176 | space:upsert(tuple, {{ '=', 2, random_string(5) }}) 177 | end 178 | 179 | elseif operation == REPLACE then 180 | for _ = 1, count do 181 | local key = space:select({}, { limit = 1 })[1][1] 182 | space:replace{ key, random_string(5) } 183 | end 184 | 185 | elseif operation == DELETE then 186 | for _ = 1, count do 187 | local key = space:select({}, { limit = 1 })[1][1] 188 | space:delete{ key } 189 | end 190 | end 191 | end 192 | 193 | group.test_cluster = function() 194 | test.helpers.retrying({ timeout = math.huge }, 195 | function() 196 | -- Generate some HTTP traffic 197 | http_request(instances.tnt_router, GET, '/hello', math.random(5, 10)) 198 | http_request(instances.tnt_router, GET, '/hell0', math.random(1, 2)) 199 | http_request(instances.tnt_router, POST, '/goodbye', math.random(0, 1)) 200 | http_request(instances.tnt_storage_1_master, GET, '/hello', math.random(2, 5)) 201 | http_request(instances.tnt_storage_1_master, GET, '/hell0', math.random(0, 1)) 202 | http_request(instances.tnt_storage_1_master, POST, '/goodbye', math.random(0, 1)) 203 | http_request(instances.tnt_storage_1_replica, GET, '/hello', math.random(1, 3)) 204 | http_request(instances.tnt_storage_1_replica, GET, '/hell0', math.random(0, 1)) 205 | http_request(instances.tnt_storage_1_replica, POST, '/goodbye', math.random(0, 1)) 206 | http_request(instances.tnt_storage_2_master, GET, '/hello', math.random(2, 5)) 207 | http_request(instances.tnt_storage_2_master, GET, '/hell0', math.random(0, 1)) 208 | http_request(instances.tnt_storage_2_master, POST, '/goodbye', math.random(0, 1)) 209 | http_request(instances.tnt_storage_2_replica, GET, '/hello', math.random(1, 3)) 210 | http_request(instances.tnt_storage_2_replica, GET, '/hell0', math.random(0, 1)) 211 | http_request(instances.tnt_storage_2_replica, POST, '/goodbye', math.random(0, 1)) 212 | 213 | -- Generate some space traffic 214 | space_operations(instances.tnt_router, INSERT, math.random(1, 3)) 215 | space_operations(instances.tnt_router, UPDATE, math.random(1, 3)) 216 | space_operations(instances.tnt_storage_1_master, INSERT, math.random(5, 10)) 217 | space_operations(instances.tnt_storage_1_master, SELECT, math.random(10, 20)) 218 | space_operations(instances.tnt_storage_1_master, UPDATE, math.random(5, 10)) 219 | space_operations(instances.tnt_storage_1_master, UPSERT, math.random(5, 10)) 220 | space_operations(instances.tnt_storage_1_master, REPLACE, math.random(5, 10)) 221 | space_operations(instances.tnt_storage_1_master, DELETE, math.random(1, 2)) 222 | space_operations(instances.tnt_storage_1_replica, SELECT, math.random(3, 5)) 223 | space_operations(instances.tnt_storage_2_master, INSERT, math.random(5, 10)) 224 | space_operations(instances.tnt_storage_2_master, SELECT, math.random(10, 20)) 225 | space_operations(instances.tnt_storage_2_master, UPDATE, math.random(5, 10)) 226 | space_operations(instances.tnt_storage_2_master, UPSERT, math.random(5, 10)) 227 | space_operations(instances.tnt_storage_2_master, REPLACE, math.random(5, 10)) 228 | space_operations(instances.tnt_storage_2_master, DELETE, math.random(1, 2)) 229 | space_operations(instances.tnt_storage_2_replica, SELECT, math.random(3, 5)) 230 | 231 | -- Fail this function so cluster don't stop 232 | error('running cluster') 233 | end 234 | ) 235 | end 236 | -------------------------------------------------------------------------------- /09_testing_monitoring/materials/monitoring/project/deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Call this script to install test dependencies 3 | 4 | set -e 5 | 6 | # Test dependencies: 7 | tarantoolctl rocks install luatest 0.5.0 8 | tarantoolctl rocks install luacov 0.13.0 9 | tarantoolctl rocks install luacheck 0.25.0 10 | -------------------------------------------------------------------------------- /09_testing_monitoring/materials/monitoring/project/init.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | require('strict').on() 4 | 5 | if package.setsearchroot ~= nil then 6 | package.setsearchroot() 7 | else 8 | -- Workaround for rocks loading in tarantool 1.10 9 | -- It can be removed in tarantool > 2.2 10 | -- By default, when you do require('mymodule'), tarantool looks into 11 | -- the current working directory and whatever is specified in 12 | -- package.path and package.cpath. If you run your app while in the 13 | -- root directory of that app, everything goes fine, but if you try to 14 | -- start your app with "tarantool myapp/init.lua", it will fail to load 15 | -- its modules, and modules from myapp/.rocks. 16 | local fio = require('fio') 17 | local app_dir = fio.abspath(fio.dirname(arg[0])) 18 | print('App dir set to ' .. app_dir) 19 | package.path = app_dir .. '/?.lua;' .. package.path 20 | package.path = app_dir .. '/?/init.lua;' .. package.path 21 | package.path = app_dir .. '/.rocks/share/tarantool/?.lua;' .. package.path 22 | package.path = app_dir .. '/.rocks/share/tarantool/?/init.lua;' .. package.path 23 | package.cpath = app_dir .. '/?.so;' .. package.cpath 24 | package.cpath = app_dir .. '/?.dylib;' .. package.cpath 25 | package.cpath = app_dir .. '/.rocks/lib/tarantool/?.so;' .. package.cpath 26 | package.cpath = app_dir .. '/.rocks/lib/tarantool/?.dylib;' .. package.cpath 27 | end 28 | 29 | local cartridge = require('cartridge') 30 | local metrics = require('cartridge.roles.metrics') 31 | 32 | local ok, err = cartridge.cfg({ 33 | workdir = 'tmp/db', 34 | roles = { 35 | 'cartridge.roles.vshard-storage', 36 | 'cartridge.roles.vshard-router', 37 | 'cartridge.roles.metrics', 38 | 'app.roles.custom' 39 | }, 40 | cluster_cookie = 'project-cluster-cookie', 41 | }) 42 | 43 | metrics.set_export({ 44 | { 45 | path = '/metrics/prometheus', 46 | format = 'prometheus', 47 | } 48 | }) 49 | 50 | assert(ok, tostring(err)) 51 | -------------------------------------------------------------------------------- /09_testing_monitoring/materials/monitoring/project/instances.yml: -------------------------------------------------------------------------------- 1 | project.router: 2 | workdir: ./tmp/db_dev/3301 3 | advertise_uri: localhost:3301 4 | http_port: 8081 5 | 6 | project.s1-master: 7 | workdir: ./tmp/db_dev/3302 8 | advertise_uri: localhost:3302 9 | http_port: 8082 10 | 11 | project.s1-replica: 12 | workdir: ./tmp/db_dev/3303 13 | advertise_uri: localhost:3303 14 | http_port: 8083 15 | 16 | project.s2-master: 17 | workdir: ./tmp/db_dev/3304 18 | advertise_uri: localhost:3304 19 | http_port: 8084 20 | 21 | project.s2-replica: 22 | workdir: ./tmp/db_dev/3305 23 | advertise_uri: localhost:3305 24 | http_port: 8085 25 | 26 | project-stateboard: 27 | workdir: ./tmp/db_dev/3310 28 | listen: localhost:3310 29 | password: passwd 30 | -------------------------------------------------------------------------------- /09_testing_monitoring/materials/monitoring/project/project-scm-1.rockspec: -------------------------------------------------------------------------------- 1 | package = 'project' 2 | version = 'scm-1' 3 | source = { 4 | url = '/dev/null', 5 | } 6 | -- Put any modules your app depends on here 7 | dependencies = { 8 | 'tarantool', 9 | 'lua >= 5.1', 10 | 'checks == 3.0.1-1', 11 | 'http == 1.1.0-1', 12 | 'cartridge == 2.1.2-1', 13 | 'metrics == 0.7.0-1' 14 | } 15 | build = { 16 | type = 'none'; 17 | } 18 | -------------------------------------------------------------------------------- /09_testing_monitoring/materials/monitoring/project/stateboard.init.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | require('strict').on() 4 | 5 | if package.setsearchroot ~= nil then 6 | package.setsearchroot() 7 | else 8 | -- Workaround for rocks loading in tarantool 1.10 9 | -- It can be removed in tarantool > 2.2 10 | -- By default, when you do require('mymodule'), tarantool looks into 11 | -- the current working directory and whatever is specified in 12 | -- package.path and package.cpath. If you run your app while in the 13 | -- root directory of that app, everything goes fine, but if you try to 14 | -- start stateboard with "tarantool myapp/stateboard.init.lua", it will fail to load 15 | -- its modules, and modules from myapp/.rocks. 16 | local fio = require('fio') 17 | local app_dir = fio.abspath(fio.dirname(arg[0])) 18 | print('App dir set to ' .. app_dir) 19 | package.path = app_dir .. '/?.lua;' .. package.path 20 | package.path = app_dir .. '/?/init.lua;' .. package.path 21 | package.path = app_dir .. '/.rocks/share/tarantool/?.lua;' .. package.path 22 | package.path = app_dir .. '/.rocks/share/tarantool/?/init.lua;' .. package.path 23 | package.cpath = app_dir .. '/?.so;' .. package.cpath 24 | package.cpath = app_dir .. '/?.dylib;' .. package.cpath 25 | package.cpath = app_dir .. '/.rocks/lib/tarantool/?.so;' .. package.cpath 26 | package.cpath = app_dir .. '/.rocks/lib/tarantool/?.dylib;' .. package.cpath 27 | end 28 | 29 | require('cartridge.stateboard').cfg() 30 | -------------------------------------------------------------------------------- /09_testing_monitoring/materials/monitoring/project/test/helper.lua: -------------------------------------------------------------------------------- 1 | -- This file is required automatically by luatest. 2 | -- Add common configuration here. 3 | 4 | local fio = require('fio') 5 | local t = require('luatest') 6 | 7 | local helper = {} 8 | 9 | helper.root = fio.dirname(fio.abspath(package.search('init'))) 10 | helper.datadir = fio.pathjoin(helper.root, 'tmp', 'db_test') 11 | helper.server_command = fio.pathjoin(helper.root, 'init.lua') 12 | 13 | t.before_suite(function() 14 | fio.rmtree(helper.datadir) 15 | fio.mktree(helper.datadir) 16 | end) 17 | 18 | return helper 19 | -------------------------------------------------------------------------------- /09_testing_monitoring/materials/monitoring/project/test/helper/integration.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | 3 | local cartridge_helpers = require('cartridge.test-helpers') 4 | local shared = require('test.helper') 5 | 6 | local helper = {shared = shared} 7 | 8 | helper.cluster = cartridge_helpers.Cluster:new({ 9 | server_command = shared.server_command, 10 | datadir = shared.datadir, 11 | use_vshard = false, 12 | replicasets = { 13 | { 14 | alias = 'api', 15 | uuid = cartridge_helpers.uuid('a'), 16 | roles = {'app.roles.custom'}, 17 | servers = {{ instance_uuid = cartridge_helpers.uuid('a', 1) }}, 18 | }, 19 | }, 20 | }) 21 | 22 | t.before_suite(function() helper.cluster:start() end) 23 | t.after_suite(function() helper.cluster:stop() end) 24 | 25 | return helper 26 | -------------------------------------------------------------------------------- /09_testing_monitoring/materials/monitoring/project/test/helper/unit.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | 3 | local shared = require('test.helper') 4 | 5 | local helper = {shared = shared} 6 | 7 | t.before_suite(function() box.cfg({work_dir = shared.datadir}) end) 8 | 9 | return helper 10 | -------------------------------------------------------------------------------- /09_testing_monitoring/materials/monitoring/project/test/integration/api_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local g = t.group('integration_api') 3 | 4 | local helper = require('test.helper.integration') 5 | local cluster = helper.cluster 6 | 7 | g.test_sample = function() 8 | local server = cluster.main_server 9 | local response = server:http_request('post', '/admin/api', {json = {query = '{}'}}) 10 | t.assert_equals(response.json, {data = {}}) 11 | t.assert_equals(server.net_box:eval('return box.cfg.memtx_dir'), server.workdir) 12 | end 13 | -------------------------------------------------------------------------------- /09_testing_monitoring/materials/monitoring/project/test/unit/sample_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local g = t.group('unit_sample') 3 | 4 | require('test.helper.unit') 5 | 6 | g.test_sample = function() 7 | t.assert_equals(type(box.cfg), 'table') 8 | end 9 | -------------------------------------------------------------------------------- /09_testing_monitoring/materials/monitoring/prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | # my global config 2 | global: 3 | scrape_interval: 1m # Set the scrape interval to every 15 seconds. Default is every 1 minute. 4 | evaluation_interval: 1m # Evaluate rules every 15 seconds. The default is every 1 minute. 5 | # scrape_timeout is set to the global default (10s). 6 | 7 | # Alertmanager configuration 8 | alerting: 9 | alertmanagers: 10 | - static_configs: 11 | - targets: 12 | # - alertmanager:9093 13 | 14 | # Load rules once and periodically evaluate them according to the global 'evaluation_interval'. 15 | rule_files: 16 | # - "first_rules.yml" 17 | # - "second_rules.yml" 18 | 19 | # A scrape configuration containing exactly one endpoint to scrape: 20 | # Here it's Prometheus itself. 21 | scrape_configs: 22 | # The job name is added as a label `job=` to any timeseries scraped from this config. 23 | - job_name: "prometheus" 24 | 25 | # metrics_path defaults to '/metrics' 26 | # scheme defaults to 'http'. 27 | 28 | static_configs: 29 | - targets: ["localhost:9090"] 30 | 31 | - job_name: "example_project" 32 | static_configs: 33 | - targets: 34 | - "localhost:8081" 35 | - "localhost:8082" 36 | - "localhost:8083" 37 | - "localhost:8084" 38 | - "localhost:8085" 39 | metrics_path: "/metrics/prometheus" 40 | -------------------------------------------------------------------------------- /09_testing_monitoring/materials/monitoring/wrk/script.lua: -------------------------------------------------------------------------------- 1 | function request() 2 | local url 3 | 4 | if math.random() < 0.1 then 5 | url = '/hell0' 6 | else 7 | url = '/hello' 8 | end 9 | 10 | local req = wrk.format('GET', url, { 11 | ['Content-Type'] = 'application/json', 12 | }) 13 | return req 14 | end 15 | -------------------------------------------------------------------------------- /09_testing_monitoring/materials/testing/.gitignore: -------------------------------------------------------------------------------- 1 | .rocks 2 | .xlog 3 | .snap 4 | -------------------------------------------------------------------------------- /09_testing_monitoring/materials/testing/deps.sh: -------------------------------------------------------------------------------- 1 | tarantoolctl rocks install luatest 0.5.2 2 | -------------------------------------------------------------------------------- /09_testing_monitoring/materials/testing/init.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | box.cfg { 4 | listen = 3301, 5 | wal_mode = 'none', 6 | } 7 | 8 | box.schema.user.passwd('admin', 'crud') 9 | 10 | local function init() 11 | box.schema.space.create('customer', {if_not_exists = true}) 12 | box.space.customer:format({ 13 | {name = 'uuid', type = 'string'}, 14 | {name = 'name', type = 'string'}, 15 | {name = 'group', type = 'string'}, 16 | }) 17 | box.space.customer:create_index('uuid', {parts = {{field = 'uuid'}}, if_not_exists = true}) 18 | box.space.customer:create_index('group', {parts = {{field = 'group'}}, if_not_exists = true, unique = false}) 19 | end 20 | 21 | -- data = {uuid = '123', name = 'Oleg', group = 'developer'} 22 | -- tuple = {'123', 'Oleg', 'developer'} 23 | local function flatten(format, data) 24 | local tuple = {} 25 | for _, field in ipairs(format) do 26 | table.insert(tuple, data[field.name]) 27 | end 28 | return tuple 29 | end 30 | 31 | local function unflatten(format, tuple) 32 | local data = {} 33 | for i, field in ipairs(format) do 34 | data[field.name] = tuple[i] 35 | end 36 | return data 37 | end 38 | 39 | local function create(customer) 40 | local space = box.space.customer 41 | local tuple = flatten(space:format(), customer) 42 | space:replace(tuple) 43 | end 44 | 45 | local function update(uuid, updates) 46 | local space = box.space.customer 47 | local update_list = {} 48 | 49 | for key, value in pairs(updates) do 50 | table.insert(update_list, {'=', key, value}) 51 | end 52 | 53 | space:update({uuid}, update_list) 54 | end 55 | 56 | local function delete(uuid) 57 | box.space.customer:delete({uuid}) 58 | end 59 | 60 | local function get(uuid) 61 | local space = box.space.customer 62 | local tuple = space:get({uuid}) 63 | if tuple == nil then 64 | return nil 65 | end 66 | 67 | local object = unflatten(space:format(), tuple) 68 | return object 69 | end 70 | 71 | _G.create = create 72 | _G.update = update 73 | _G.delete = delete 74 | _G.get = get 75 | 76 | init() 77 | -------------------------------------------------------------------------------- /09_testing_monitoring/materials/testing/test/crud_test.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | local t = require('luatest') 4 | local g = t.group('test.crud') 5 | local fio = require('fio') 6 | 7 | g.before_all(function() 8 | local tmpdir = fio.tempdir() 9 | g.server = t.Server:new({ 10 | command = 'init.lua', 11 | -- additional envars to pass to process 12 | env = {SOME_FIELD = 'value'}, 13 | -- passed as TARANTOOL_WORKDIR 14 | workdir = tmpdir, 15 | -- passed as TARANTOOL_HTTP_PORT, used in http_request 16 | http_port = 8080, 17 | -- passed as TARANTOOL_LISTEN, used in connect_net_box 18 | net_box_port = 3301, 19 | -- passed to net_box.connect in connect_net_box 20 | net_box_credentials = {user = 'admin', password = 'crud'}, 21 | }) 22 | g.server:start() 23 | t.helpers.retrying({}, function() 24 | g.server:connect_net_box() 25 | end) 26 | end) 27 | 28 | g.after_each(function() 29 | g.server.net_box:eval([[ 30 | box.space.customer:truncate() 31 | ]]) 32 | end) 33 | 34 | g.after_all(function() 35 | g.server:stop() 36 | end) 37 | 38 | function g.test_get() 39 | local uuid = '978046d5-f768-4c08-9757-02cb041ea7b0' 40 | 41 | local customer = g.server.net_box:call('get', {uuid}) 42 | t.assert_equals(customer, nil) 43 | 44 | g.server.net_box:call('create', { 45 | { uuid = uuid, name = 'Ivan', group = 'developer'} 46 | }) 47 | 48 | customer = g.server.net_box:call('get', {uuid}) 49 | t.assert_not_equals(customer, nil) 50 | t.assert_equals(customer.uuid, uuid) 51 | t.assert_equals(customer.name, 'Ivan') 52 | t.assert_equals(customer.group, 'developer') 53 | end 54 | -------------------------------------------------------------------------------- /10_queue/10_queue.md: -------------------------------------------------------------------------------- 1 | ### Асинхронная обработка 2 | 3 | ![image](https://user-images.githubusercontent.com/8830475/112840431-005e0800-90a8-11eb-865c-52acc7c03501.png) 4 | 5 | Начать стоит с простого примера: у нас есть e-mail рассылка и у нас есть пользователи, которые 6 | должны получить уведомления от нашего сайта, например, о появлении записи в блоге, 7 | на который они подписаны. Отправить одновременно несколько тысяч или миллионов сообщений 8 | невозможно - мы просто положим наш почтовый сервер. 9 | Также не имеет особого смысла получит наш пользователь уведомление сейчас или 10 | спустя 2-3 минуты. 11 | 12 | ![image](https://user-images.githubusercontent.com/8830475/112845852-ffc87000-90ad-11eb-9d2c-eae1f82e6a62.png) 13 | 14 | Это пример асинхронного взаимодействия - у нас есть некая очередь, 15 | из которой мы что-то забираем и отправляем. Самое важное, конечно то, 16 | что самое естественное состояние очереди - быть пустой (к этому мы должны стремиться, 17 | не допуская неограниченного роста). 18 | 19 | В нашей очереди есть сущности, которые что-то складывают в неё - producer 20 | и сущности, которые вычитывают - consumer. В примере выше у нас был всего один 21 | почтовый сервер, но ничто не мешало бы добавить ещё один и тем самым масштабировать 22 | нашу систему. В дальнейшем ничто не мешает нам обобщить пример и сказать, 23 | что в работать с очередью может кто угодно - отправка смс-сообщений, 24 | система для сжатия картинок... 25 | 26 | ![image](https://user-images.githubusercontent.com/8830475/112841947-bd049900-90a9-11eb-8b95-86b463c389ac.png) 27 | 28 | #### Message Queue vs. Message Broker 29 | 30 | Часто встречаются такие определения "очередь сообщений" и "брокер сообщений", разберемся в чем разница. 31 | 32 | - Очередь сообщений - просто контейнер, структура, хранящая какие-то данные. 33 | Нет логики для обработки сообщений в очереди, и мы не говорим о том, как именно данные 34 | туда доставляются или оттуда забираются. 35 | 36 | - Брокер сообщений - система над очередью сообщений, которая отвечает и за транспорт, 37 | и за логику, и за распределение сообщений между бизнес-системами. 38 | 39 | #### Семантика доставки 40 | 41 | Вернемся к примеру с почтовой рассылкой. Что будет, если по какой-то причине мы отправим 42 | одно и тоже письмо дважды? Пользователь скорее всего проигнорирует его, и ничего 43 | страшного не случиться. Если же мы работали с деньгами, то произошло 44 | бы два списание/пополнения средств - это уже проблема. 45 | 46 | Но что произойдет, если мы совсем не отправим сообщение? 47 | С почтовой рассылкой по-прежнему скорее всего ничего страшного. 48 | Если это СМС-рассылка о смене тарифа у пользователя? 49 | Или пользователь подключил уведомления, заплатил за них, но 50 | мы не прислали его - тоже проблема. 51 | 52 | Здесь мы подошли к требованиям к нашей системе и познакомились (хоть и неявно) 53 | с семантиками доставки: 54 | 55 | * At least once (1+) 56 | * At most once (0, 1) 57 | * Exactly once (1) 58 | 59 | Обеспечение семантики доставки "exactly once" является тут самым 60 | трудным, хотя и в иных системах необходимым. 61 | 62 | #### Apache Kafka 63 | 64 | Пожалуй, самым известным брокером сообщений является Apache Kafka. 65 | 66 | ![image](https://user-images.githubusercontent.com/8830475/112846491-c93f2500-90ae-11eb-8401-146866049559.png) 67 | 68 | Отдельный сервер Кафки именуется брокером. Брокеры образуют собой кластер, 69 | в котором один из этих брокеров выступает контроллером, 70 | берущим на себя некоторые административные операции. 71 | 72 | За выбор брокера-контроллера, в свою очередь, отвечает отдельный сервис – ZooKeeper, 73 | который также осуществляет service discovery брокеров, 74 | хранит конфигурации и принимает участие в распределении новых читателей по брокерам и в большинстве случаев хранит 75 | информацию о последнем прочитанном сообщении для каждого из читателей. 76 | Это важный момент, изучение которого требует опуститься на уровень ниже и рассмотреть, 77 | как отдельный брокер устроен внутри. 78 | 79 | ##### Commit log 80 | 81 | Структура данных, лежащая в основе Kafka, называется commit log или журнал фиксации изменений. 82 | 83 | ![image](https://user-images.githubusercontent.com/8830475/112847064-5bdfc400-90af-11eb-974b-e84db19d74f2.png) 84 | 85 | Новые элементы, добавляемые в commit log, помещаются строго в конец, и их порядок после этого не меняется, 86 | благодаря чему в каждом отдельном журнале элементы всегда расположены в порядке их добавления. 87 | 88 | Свойство упорядоченности журнала фиксаций позволяет использовать его, например, для репликации по принципу eventual 89 | consistency между репликами БД: в них хранят журнал изменений, производимых над данными в мастер-ноде, 90 | последовательное применение которых на слейв-нодах позволяет привести данные в них к согласованному с мастером виду. 91 | В Кафке эти журналы называются партициями, а данные, хранимые в них, называются сообщениями. 92 | 93 | Что такое сообщение? Это основная единица данных в Kafka, представляющая из себя просто набор байт, 94 | в котором вы можете передавать произвольную информацию – ее содержимое и структура не имеют значения для Kafka. 95 | Сообщение может содержать в себе ключ, так же представляющий из себя набор байт. 96 | Ключ позволяет получить больше контроля над механизмом распределения сообщений по партициям. 97 | 98 | #### Партиции и топики 99 | 100 | ![image](https://user-images.githubusercontent.com/8830475/112848352-aada2900-90b0-11eb-9338-ea2f891568e0.png) 101 | 102 | Так вот в Кафке функцию очереди выполняет не партиция, а topic. 103 | Он нужен для объединения нескольких партиций в общий поток. 104 | Сами же партиции, как мы сказали ранее, хранят сообщения в упорядоченном виде согласно структуре данных commit log. 105 | Таким образом, сообщение, относящееся к одному топику, может хранится в двух разных партициях, 106 | из которых читатели могут вытаскивать их по запросу. 107 | 108 | Следовательно, единицей параллелизма в Кафке выступает не топик, а партиция. 109 | За счет этого Кафка может обрабатывать разные сообщения, относящиеся к одному топику, 110 | на нескольких брокерах одновременно, а также реплицировать не весь топик целиком, а только отдельные партиции, 111 | предоставляя дополнительную гибкость и возможности для масштабирования. 112 | 113 | #### Pull и Push 114 | 115 | Обратите внимание, что я не случайно использовал слово “вытаскивает” по отношению к читателю. 116 | В описанных ранее брокерах доставка сообщений осуществляется путем их проталкивания (push) получателям через условную трубу в виде очереди. 117 | В Кафке процесса доставки как такового нет: каждый читатель сам ответственен за вытягивание (pull) сообщений из партиций, которые он читает. 118 | 119 | ![image](https://user-images.githubusercontent.com/8830475/112848605-eecd2e00-90b0-11eb-8d7b-b81d4778e5b8.png) 120 | 121 | Производители, формируя сообщения, прикрепляют к нему ключ и номер партиции. Номер партиции может быть выбран рандомно (round-robin), если у сообщения отсутствует ключ. 122 | 123 | Если вам нужен больший контроль, к сообщению можно прикрепить ключ, а затем использовать hash-функцию или написать свой алгоритм, по которому будет выбираться партиция для сообщения. После формирования, производитель отправляет сообщение в Кафку, которая сохраняет его на диск, помечая, к какой партиции оно относится. 124 | 125 | Каждый получатель закреплен за определенной партицией (или за несколькими партициями) в интересующем его топике, и при появлении нового сообщения получает сигнал на вычитывание следующего элемента в commit log, при этом отмечая, какое последнее сообщение он прочитал. Таким образом при переподключении он будет знать, какое сообщение ему вычитать следующим. 126 | 127 | #### Consumer Group 128 | 129 | Чтобы избежать ситуации с чтением одной партиции конкурентными читателями, 130 | в Кафке принято объединять несколько реплик одного сервиса в consumer Group, 131 | в рамках которого Zookeeper будет назначать одной партиции не более одного читателя. 132 | 133 | Так как читатели привязываются непосредственно к партиции 134 | (при этом читатель обычно ничего не знает о количестве партиций в топике), 135 | ZooKeeper при подключении нового читателя производит перераспределение участников в Consumer Group таким образом, 136 | чтобы каждая партиция имела одного и только одного читателя. 137 | Читатель обозначает свою Consumer Group при подключении к Kafka. 138 | 139 | ![image](https://user-images.githubusercontent.com/8830475/112849018-53888880-90b1-11eb-8c8a-1c282aedc13e.png) 140 | 141 | В то же время ничего не мешает вам повесить на одну партицию несколько читателей с разной логикой обработки. Например вы храните в топике список событий по действиям пользователей и хотите использовать эти события для формирования нескольких представлений одних и тех же данных и последующей отправкой их в соответствующие хранилища. 142 | 143 | Но здесь мы можем столкнуться с другой проблемой, порожденной тем, что Кафка использует структуру из топиков и партиций. Я напомню, что Кафка не гарантирует упорядоченность сообщений в рамках топика, только в рамках партиции, что может оказаться критичным, например, при формировании отчетов о действиях по пользователю и отправке их в хранилище as is. 144 | 145 | ![image](https://user-images.githubusercontent.com/8830475/112849138-70bd5700-90b1-11eb-9e94-d8ae1ddd59c5.png) 146 | 147 | Чтобы решить эту проблему, мы можем пойти от обратного: если все события, относящиеся к одной сущности (например, все действия относящиеся к одному user_id), будут всегда добавляться в одну и ту же партицию, они будут упорядочены в рамках топика просто потому, что находятся в одной партиции, порядок внутри которой гарантирован Кафкой. 148 | Для этого нам и нужен ключ у сообщений: например, если мы будем использовать для выбора партиции, в которую будет добавлено сообщение, алгоритм, вычисляющий хэш от ключа, то сообщения с одинаковым ключом будут гарантированно попадать в одну партицию, а значит и вытаскивать получатель сообщения с одинаковым ключом в порядке их добавления в топик. 149 | В кейсе с потоком событий о действиях пользователей ключом партицирования может выступать user_id. 150 | 151 | ### Очередь в Tarantool 152 | 153 | Применение Tarantool как очереди довольно частый кейс. 154 | Сама по себе структура хранения (B-TREE) в совокупности с возможностью писать логику 155 | позволяет хранить в упорядоченном виде данные и обрабатывать их так, 156 | чтобы можно было организовывать различные очереди - с приоритетами, таймаутами и т.д. 157 | 158 | Есть несколько активно используемых реализаций: 159 | - [tarantool/queue](https://github.com/tarantool/queue) 160 | - [moonlibs/xqueue](https://github.com/moonlibs/xqueue) 161 | - [tarantool/sharded-queue](https://github.com/tarantool/sharded-queue) - попытка написать распределенную очередь 162 | поверх vshard. 163 | 164 | ```lua 165 | box.cfg{} 166 | queue = require('queue') 167 | -- Queue types: "fifo", "fifottl", "limfifottl", "utubettl" 168 | queue.create_tube('my_tube', 'fifo', {temporary = true}) 169 | queue.tube.my_tube:put({'my_tube'}) 170 | --- 171 | - [0, 'r', ['message']] 172 | ... 173 | queue.tube.my_tube:take() 174 | --- 175 | - [0, 't', ['message']] 176 | ... 177 | ``` 178 | 179 | ## Кэширование 180 | 181 | В сфере вычислительной обработки данных кэш – это высокоскоростной уровень хранения, 182 | на котором требуемый набор данных, как правило, временного характера. 183 | Доступ к данным на этом уровне осуществляется значительно быстрее, чем к основному месту их хранения. 184 | С помощью кэширования становится возможным эффективное повторное использование ранее полученных или вычисленных данных. 185 | 186 | Данные в кэше обычно хранятся на устройстве с быстрым доступом, 187 | таком как ОЗУ (оперативное запоминающее устройство), 188 | и могут использоваться совместно с программными компонентами. 189 | Основная функция кэша – ускорение процесса извлечения данных. 190 | Он избавляет от необходимости обращаться к менее скоростному базовому уровню хранения. 191 | 192 | Пример приложения без кэширования. 193 | ![image](https://user-images.githubusercontent.com/8830475/113515136-51c03880-957b-11eb-9896-503a350915e6.png) 194 | 195 | При добавлении кэширования. 196 | ![image](https://user-images.githubusercontent.com/8830475/113515167-787e6f00-957b-11eb-9472-498b49abf023.png) 197 | 198 | Основная проблема кэширования - инвалидация кэша. 199 | 200 | ### Memcached 201 | 202 | Memcached – это удобное высокопроизводительное хранилище данных в памяти. 203 | Memcached широко применяется для поддержки рекламных технологий, площадок интернет-коммерции, игровых, мобильных и интернет-приложений, а также других приложений, работающих в режиме реального времени. 204 | 205 | Memcached сохраняет данные в оперативной памяти. 206 | Поскольку Memcached, как и другие хранилища данных типа «ключ-значение» в памяти, 207 | не нуждается в доступе к диску, это исключает задержки, связанные с поиском, и обеспечивает доступ к данным за микросекунды. 208 | Кроме того, хранилище Memcached является распределенным, поэтому его можно просто масштабировать путем добавления новых узлов. 209 | Многопоточность Memcached позволяет быстро наращивать вычислительную мощность. 210 | Благодаря высокой скорости, масштабируемости, простоте, эффективности управления памятью и поддержке API для большинства распространенных языков программирования Memcached широко применяется для создания масштабного кэша с высокой производительностью. 211 | 212 | Основные аттрибуты, которые задаются данным в кеше: 213 | 214 | | Аттрибут | Описание | 215 | |----------|:---------------------------------------------------------------------------------:| 216 | | key | имя ключа, максимальная длина 250 байт | 217 | | flag | 32-х битная целочисленное, обычно можно установить в 0 | 218 | | exptime | время жизни объекта в кеше в секундах — 0 хранить всегда | 219 | | bytes | кол-во байт, которые необходимо выделить для хранения значения | 220 | | noreply | опциональный параметр, что бы сервер не оптравлял ответа после выполнения запроса | 221 | | value | значение, которое необходимо добавить в кеш | 222 | 223 | Команды: 224 | 225 | * set 226 | * get 227 | * add 228 | * delete 229 | 230 | #### Memcached protocol 231 | 232 | Memcached протокол является текстовым, что позволяет писать свои реализации 233 | данного протокола на различных языках программирования. 234 | В том числе и для Tarantool существует реализация memcached протокола - 235 | https://github.com/tarantool/memcached. 236 | 237 | #### Кэширование в Tarantool 238 | 239 | Часто Tarantool используют не только в качестве основного 240 | хранилища данных, но и в качестве кэша. 241 | Схема взаимодействия в таком случае довольно прозрачна. 242 | Допустим, основные данные лежат в PostgreSQL - перед походом 243 | мы проверяем, не лежит ли значение в Tarantool: если да, то 244 | сразу отдаем его пользователю, если нет, то идем в основное хранилище, 245 | но перед выдачей результата пользователю сохраняем его в Tarantool. 246 | Всё взаимодействие в таком случае происходит через коннектор на 247 | каком-либо языке, чтобы не терять в производительности на сериализацию 248 | данных в Lua. 249 | 250 | Для инвалидации кэша пользователю всё-таки придется написать 251 | какую-то логику. Но обычно, если это инвалидация по времени, 252 | то существуют готовые модули, которые в бэкграунде 253 | проходят по спейсу и удаляют кортежи, удовлетворяющие 254 | определенному условию: 255 | - [tarantool/expirationd](https://github.com/tarantool/expirationd) 256 | - [moonlibs/indexpiration](https://github.com/moonlibs/indexpiration) 257 | - [sonntex/tarantool-capped-expirationd](https://github.com/sonntex/tarantool-capped-expirationd) - expirationd на С. 258 | 259 | ### Домашнее задание 260 | 261 | Написать очередь (семантика доставки at least once): 262 | - queue.new(opts) - создание очереди (должна быть возможность указать движок хранения); 263 | - queue.get(name) - получить экземпляр очереди; 264 | - queue_obj:grant(...) - выдача прав на использование очереди; 265 | - queue_obj:put(data) - положить сообщение в очередь; 266 | - queue_obj:take({timeout}) - получить сообщение из очереди. Если очередь пустая, то ждать timeout до получения сообщения. 267 | Вернуть nil - если пустая; 268 | - queue_obj:stats() - Получение статистики по очереди (PUT/TAKE/LISTENERS). 269 | 270 | Тесты можно написать с помощью luatest. 271 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Maksim Trempoltsev 4 | 5 | Copyright (c) 2021 Elizaveta Dokshina 6 | 7 | Copyright (c) 2021 Oleg Babin 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Курс лекций для студентов МГУ и МФТИ, посвященный проектированию высоконагруженных систем на примере Tarantool. 2 | --------------------------------------------------------------------------------