Skip to content

Instantly share code, notes, and snippets.

@pftg
Forked from pavlov200912/highload.md
Created October 12, 2022 18:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pftg/2e18aa4390e002381b78f0dde4ab5049 to your computer and use it in GitHub Desktop.
Save pftg/2e18aa4390e002381b78f0dde4ab5049 to your computer and use it in GitHub Desktop.
Конспект Проектирование Высоконагруженных систем 2021

package main

/* Билеты по Проектированию Высоко нагруженных систем

  • В этом конспекте нет автоматической грамматической проверки
  • Он написан для личного пользования, исключительно для подготовки к экзамену
  • Я извиняюсь за грамматические ошибки, которых здесь много
  • Править я их не буду, как натыкал, так и натыкал

1. Высоконагруженная система

Высокая нагрузка это что? Простой метод посчитать RPS - request per second 10 RPS много или мало? 1000 RPS много или мало? Ответ: "Это зависит"

High load наступает, когда мы не справляемся с нагрузкой. (это сильно зависит от требований пользователя и задачи)

2. Проактивные действия разработчика ВС

Как готовиться к High Load?

  • Хорошо понимать задачу
  • Дизайн (архитектура) - система должна масштабироваться
  • Мониторинг — система должна быть наблюдаема
  • Знать свои технологии / инструменты (+ алгосы и структуры данных)

Как не готовиться?

  • Преждевременные оптимизации (Возможно тратим время просто так, часто портят архитектуру, оптимизированный код иногда выглядит ужасно)

3. Пользовательские истории

Требования к системе. Короткими и ясными фразами. На естественном языке.

A user story is an informal, general explanation of a software feature written from the perspective of the end user. Its purpose is to articulate how a software feature will provide value to the customer.

  • Stories keep the focus on the user. (a collection of stories keeps the team focused on solving problems for real users.)
  • Stories enable collaboration. (With the end goal defined, the team can work together to decide how best to serve the user)
  • Stories drive creative solutions.
  • Stories create momentum. (With each passing story the development team enjoys a small challenges and a small win)

“As a [persona], I [want to], [so that].”

4. API. Виды API

API - Application Programming Interface Хотим сделать взаимодействие между различными частями. Нужен протокол/интерфейс. Нам нужна абстракция, например мы не хотим знать детали монитора, но хотим уметь с ним взаимодействовать Кроме этого, разграничиваем части системы, создав интерфейс.

Wiki: "It defines the kinds of calls or requests that can be made, how to make them, the data formats that should be used, the conventions to follow, etc. It can also provide extension mechanisms so that users can extend existing functionality in various ways and to varying degrees"

Виды API:

  • Библиотеки, фреймворки
  • Application Binary Interface (в такой-то кусок памяти положи то-то)
  • Remote (RPC, REST, JSON-API, взаимодействие удаленных узлов)
  • Таблицы, структуры данных
  • Очереди, шины

5. Remote Procedure Call. JSON-RPC, gRPC

Remote Procedure Call - вызов процедуры.

Remote procedure call (RPC) is when a computer program causes a procedure (subroutine) to execute in a different address space (commonly on another computer on a shared network), which is coded as if it were a normal (local) procedure call, without the programmer explicitly coding the details for the remote interaction.

RPC is a request–response protocol. An RPC is initiated by the client, which sends a request message to a known remote server to execute a specified procedure with supplied parameters. The remote server sends a response to the client, and the application continues its process.

Адресуем методы. Не говорим про объекты, структуры, сущности.

Какие бывают?

  • SOAP (сообщения в формате XML, объекты которые мы передаем в рамках протокола должны соотв. схеме)
  • JSON-RPC, XML-RPC (простые схемы)
  • gRPC (google RPC)
  • очень много других

JSON-RPC:

  • использует любой транспорт
  • сообщения - JSON объекты с определенной схемой
  • использует систему типов JSON

Пример запроса

{
 "jsonrpc": "2.0", // версия протокола
 "method": "substract",
 "params": [42, 43],
 "id": 1 // id запроса, будет копирован в ответ
}

Пример ответа

{
 "jsonrpc": "2.0",
 "result": 19, // вместо result может быть error с кодом и сообщением ошибки
 "id": 1
}

Используется в очень примитивных ситуациях, плохая поддержка в Go

gRPC

  • Протокол
  • Фреймворк для разных языков и платформ (есть набор инструментов)
  • Бинарная сериализация
  • Транспорт: HTTP/2
  • Поддерживает двунаправленные потоки (можно не диалог, а монолог, просто стримить сообщения)
  1. Определяем протокол (*.proto file)
  2. Генерируем код клиента и сервера
  3. Реализуем сервер и клиент

Пример proto файла:

syntax = "proto3";

option go_package = "github.com/mp-hl-2021/grpc-example/ api";

package echo;

service Echo {
  rpc Do(EchoRequest) returns (EchoReply) {}
}

message EchoRequest {
  string line = 1;
  int32 num = 2;
}

message EchoReply {
  string echo_line = 1;
}

Генерируем код

 protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative api/echo.proto

Нам сгенерируют интерфейсы и методы заглушки, нужно их реализовать

6. REST API. REST API как набор ограничений. Недостатки.

Representational State Transfer

Архитектурный стиль, в котором клиент state machine, а сервер движок текстовых квестов (вы в такой-то комнате, у вас дверь на север, на юг, куда кликните? и т.д)

REST как набор ограничений:

  • Separation of Client and Server (имплементации сервера и клиента могут не знать о друг друге, пока каждая сторона понимает формат общения, они могут изменяться независимо)
  • Statelessness (Сервер не должен знать ничего о состоянии клиента и наоборот, таким образом клиент и сервер могут воспринимать любые получаемые сообщения, даже без истории взаимодействия до)
  • Cacheable (Данные в запросах и ответах могут быть помечены как кешируемые, стороны могут использовать те же данные при дальнейших запросах, не обновляя их)
  • Uniform interface (
    • Identification of resources; (for example with URLs)
    • manipulation of resources through representations; (имея ресурс representation of a resource клиенту хватает информации чтобы модифицировать или удалить его)
    • Self-descriptive messages (Каждое сообщение содержит достаточно информации, чтобы его обработать)
    • Hypermedia as the engine of application state)
  • Layered system (Разделяем приложение на слои, ограничиваем каждую компоненту во взаимодействии: разрешаем только с компонентами ее слоя)

TODO: Недостатки REST? Он вообще ничего не сказал

7. HTTP и HTTPS: модель, методы и их семантика.

HTTP - HyperText Transfer Protocol (уровень общения application)

HyperText and HyperMedia - к каждому нашему медиа (текст, картинки) добавляем перекрестные ссылки, каждый ресурс получает идентификатор и мы можем использовать их внутри наших документов, чтобы ссылаться на другие ресурсы, ссылки могут быть нелинейные.

HTTP Model - все представляется в виде ресурсов, к каждому ресурсу можно получить доступ по URL (Uniform Resource Location)

Пример https://example.com:80/examples/1#test

Примерная грамматика URL: scheme://[userinfo@]host[:port]/path[?query][#fragment]

HTTP: Запрос

  • Метод (глагол)
  • Положение ресурса
  • Заголовки
  • Тело

Методы:

  • GET (получить представление ресурса)
  • HEAD - как GET но не возвращает тело (например метаданные взять из заголовка, когда ресурс обновился)
  • POST - передает серверу "нечто" в подчинение какому-то ресурсу (ресурсу по URL)
  • PUT - сохранить "нечто" с доступом по указанному URI
  • DELETE - удалить ресурс
  • etc

HTTP: ответ

  • Статус (некий код)
  • Заголовок
  • Тело

Статус коды:

Informational responses (100–199) Successful responses (200–299) Redirects (300–399) Client errors (400–499) Server errors (500–599)

HTTPS: HTTP over TLS (secured TCP), Алиса и Боб используют асимметричное шифрование (Открытый, приватный ключ у Алисы, но еще нужно подписывать сообщения у сторонних сервисов, которым все доверяют) Мы как владельцы домена приходим к Certification Authority, один раз валидируем свою личность, получаем подпись и кладем ее в конфиг сервера, теперь нам можно доверять (ну типа)

8. Аутентификация и авторизация.

Аутентификация - процесс, при котором система удостоверяется, что нечто является подлинным

  • ID соответствует пользователю
  • Сайт настоящий, а не поддельный

Авторизация - подтверждения права получить доступ к ресурсу. (и если есть доступ, то какой)

9. Аутентификация в Web. Basic HTTP Authentication. Form-based authentication. Cookies. JSON Web Token

Basic HTTP Authentication

(через HTTP заголовок) Клиент:

Authorization: Basic <credentials>
credentials := BASE64(id:password)
Authorization: Basic AkkBkerCdlaFASff6Adssf66a6f6jg891921bBbasdgvbadsC

Сервер (если не авторизован)

401 Unauthorized
WWW-Authenticate: Basic realm="User Visible Realm" // отвечает на что авторизация

Такой подход незащищенный, заголовки не шифруются, легко представится пользователем

Form-Based Authentication

  • Формочка, в нее записываем логин пароль
  • После этого например это в javascript записывается в json, отправляется POST запросом на сервер
  • Сервер валидирует данные этой формы, возвращает токен Надо как-то хранить токен, чтобы каждый раз не пришлось вводить логин-пароль Два варианта, про которые сказал Никита:
  • Cookies
  • Bearer Authentication

HTTP Cookie:

Механизм хранения состояния на стороне клиента

  • Аутентификация
  • Корзина товаров (так стараются не делать)
  • Персональные предпочтения (темная тема)
  • Слежка за пользователями (third party cookies, я так понял это хотят забанить)

Чтобы выставить Cookies сервер возвращает заголовки

Set-Cookie: theme=light
Set-Cookie: sessionToken=6dasdgafasdsa; Expires=Wed, 09 Jun 2021 10:18:14 GMT

В следующий раз мы (client) уже в заголовке посылаем

Cookie: theme=light; sessionToken=6dasdgafasdsa

Виды HTTP Cookie:

  • Session (удаляются после закрытия браузера)
  • Persistent (удаляются с течением времени, хранятся внутри браузера)
  • Secure (работают только по HTTPS)
  • Same-site (Отправлять куки только в рамках домена, защищает от Cross Site Scripting)
  • Third-party (Куки, установленные с других доменов)
  • HTTP-Only (Недоступно из клиентского JS)
  • ...

В Go для работы с куки есть http.Cookie

Cookie опасны! (XSS) XSS: Cross-site scripting (XSS) is a type of security vulnerability typically found in web applications. XSS attacks enable attackers to inject client-side scripts into web pages viewed by other users.

  • Требуйте HTTPS (secure cookies)
  • Помните, что на клиенте тоже могут подменить куки (сервер не должен всегда доверять содержимому куки, если мы хотим обезопасить себя, нужно хранить состояние клиента, в реальности корзину мы храним на сервере, session_id храним на сервере, и т.д в итоге от REST много где нам придется отказаться =((( )
  • Устанавливайте Domain

JSON Web Token

JWT, джот - кусок данных в формате JSON, использует подпись и/или шифрование Сервер вместе с джотом генерирует подпись, по которой может проверить, что JSON был сгенерирован им Обычно содержит информацию о правах доступа.

Обычно кодируют публичным и приватным ключом, приватный оставляют только у сервера. В Go это все делает либой rsa и с помощью openssl.

JWT не средство шифрования! Токен подписан, но любой сторонний сервис может узнать содержимое

10. Авторизация. Основная схема OAuth2.0.

Пример: Создать аккаунт с помощью Apple/Facebook/Google/Microsoft

  • Resource Owner (конечный пользователь, ресурс - мой акк в гугле)
  • Resource Server (хранит ресурсы, гугл)
  • Client (стороннее приложение, сокращатель ссылок)
  • Authorization Server - выдает клиенту токены, которые разрешают от имени RO получить доступ к ресурсам на RS

гугл = Resource Server

Client запрашивает разрешение у RO на авторизацию через гугл

RO выдает разрешение Client

Client показывает разрешение Authorization Server

Authorization Server выдает Client Access Token

Client показывает гугл Access Token

гугл выдает protected resource Client-у

Обновление токена:

Обычно вместе с Access Token выдается Refresh Token, Access Token работает какое-то время, используя Refresh Token Client может обновить Access Token

11. Работа с паролями.

  • Не храните пароли в открытом виде
  • Проверяйте сложность, валидируйте
  • Не изобретайте свои алгоритмы !!!
  • Защищайтесь от перебора
  • Подумайте о возможных уязвимостях (модель угроз)

Как хранить пароли?

  • В открытом виде (базу точно украдут и все пароли утекут)
  • Криптографические хеш функции (из байтов делаем мусор, функция необратима) (В базу сохраняем хэши паролей) (возможен перебор по таблицам паролей - хешей, радужные таблицы)
  • Добавляем соль к паролю (добавляем какую-то строку ко всем паролям и после считаем хеш) (сложно строить таблицы, но можно узнать соль)
  • Рандомизируем соль
  • Добавляем раунды хеширования
  • Не пишем ничего из этого, используем bcrypt

12. Тестирование и модульное тестирование. TDD и Extreme Programming.

Модульное тестирование - способ тестирования отдельных "единиц" кода (unit testing)

  • помогают находить ошибки раньше (сфокусированы на отдельных малых кусках кода)
  • заставляет писать маленькие функции с минимальными побочными эффектами
  • является документацией для программистов
  • добавляет смелости

Проблемы модульного тестирования:

  • не гарантируют, что программа корректно работает (вдруг слон войдет в комнату)
  • комбинаторный взрыв (вариантов данных очень много)
  • ошибка может быть не в единице кода, а во взаимодействии нескольких единиц

В Go это делается с помощью модуля testing

TDD:

  • Перед написанием кода пишем требования (тесты) к коду

  • Помогает справиться с проблемой пустой страницы (тесты писать просто)

  • Частично отвечает на вопрос (а надо ли тестировать тесты? когда изначально тест падает мы понимаем, он содержательный)

  • Помогает посмотреть на код с точки зрения пользователя кода

  • Улучшает покрытие тестами

  • [1] Пишем тесты

  • [2] Проверить падает ли тест (если не упал, возвращаемся к пункту [1])

  • [3] Написать столько кода, чтобы пройти этот один тест

  • [4] Проверить на других тестах, если упали пофиксить

  • [5] Переходим на пункт [1]

Extreme Programming:

  • Хорошие практики доведены до абсурда
  • Нужны тесты, пишем TDD
  • Нужно code review, делаем его постоянно (два разработчика у ноутбука)
  • Нужно уметь поставлять продукт пользователю, давайте сразу поставим пустой продукт и с каждым изменением кода будем обновлять
  • Идея: сокращать feedback loop, не ждать долго ответа от заказчика, постоянно создаем тестируем прототипы и выдаем

13. Domain-Driven Design. Основная сложность разработки ПО и как её преодолевать.

Стиль построения моделей

Концепция, согласно которой язык и структура программного кода должны соответствовать предметной области "бизнеса"

В какой-то момент разработки ПО мы должны себя спросить

  • Правильную ли проблему решаем?
  • Понимает ли нас специалист?
  • А мы специалиста?
  • Как справиться со сложностью? (число модулей быстро растет, код запутывается)

Язык, на котором мы говорим о предметной области, не совпадает с языком, на котором мы обсуждаем софт, который ее реализует

Ubiquitous language - язык, сфокусированный на модели предметной области и использующийся всеми в команде, с целью связать всю деятельность команды с софтом.

Прямо в коде должны быть термины, которые используют специалисты предметной области

Мы можем по-разному смоделировать предметную область. Не всегда очевидно, какую модель выбрать. (Плоская карта, против треугольной). Создавая модель мы должны: во-первых, общаться со специалистом, во-вторых, попробовать писать код этой модели. Вместе с программой модель должна постоянно модифицироваться.

Есть DDD Heuristics.

  • Предметной области нет дела до протокола и БД. (логике приложения плевать на линукс и виндовс, grcp, json-rcp, ядро с логикой не должно об этом знать)
  • Публичным интерфейсом объекта должно быть поведение, а не данные (если моделируем объекты в виде структур данных, мы жестко связываем участки кода, все завязано на поля, лучше предоставлять поведение, а не структуры данных)
  • В памяти держим только валидное состояние
  • В коде бизнес-логика отражается буквально (имена должны отражать предметную область)
  • Используйте ООП

Object-oriented Programming

Давайте вместо инструкций компьютера будем использовать другие способы абстракции. Будем для решения задачи использовать много маленьких компьютеров у которых есть 2-3 сложных метода, такие компьютеры назовем объеком. Они должны быть изолированы от других, но объекты могут общаться между собой сообщениями (вызов метода это сообщение к объекту).

14. Слоистые архитектуры.

Общий вид

  • Пользовательский интерфейс (http api)
  • Уровень приложения (use cases)
  • Предметная область (комната в чате - объект предметной области)
  • Инфраструктура (драйвер БД)

Схема: Нижний слой, который ни от чего не зависит - предметная область. Мы должны выразить ее с помощью средств языка и например ООП.

Следующий слой (выше) - приложение. Та среда, в которую помещаем объекты предметной области, чтобы они взаимодействовали.

Следующий слой (выше) - интерфейс. Отвечает за связь приложения с внешним миром.

От всех эти трех слоев зависит инфраструктура (линукс и т.д).

Все зависимости идут в одну сторону - в предметную область.

Примеры:

  • Предметная область (Entity, Value object, Service, Repository)
  • Инфраструктура (Драйверы, Фреймворки, Библиотеки)
  • Интерфейс (реализации репозиториев, http handlers)

15. Связность и сцепленность. Принципы SOLID.

Эти принципы мне не очень нравятся, поэтому я не стал их отдельно гуглить и вникать, если хотите нормально ответить этот билет на экзамене, лучше что-то еще прочитать.

Сцепленность - степень зависимости между модулями. (в го модулю == пакеты)

Связность (противопоставляется сцепленности) - степень зависимости между элементами внутри модуля. Насколько элементы модуля хорошо вместе решают общую цель. (пакет с утилитами имеет очень низкую связность)

Добиваемся низкой сцепленности и высокой связности!

SOLID (woow!)

  • Single Responsibility principle
  • Open-Closed principle
  • Liskov substitution principle
  • Interface Segregation principle
  • Dependency inversion principle

Помогают понять как из базовых конструкций собрать большую программу.

Single Responsibility Principle

Закон Конвея - Организации проектируют системы, которые копируют структуру коммуникаций в этой организации.

Модуль должен иметь одну причину для изменений. (Модуль должен отвечать за одного актора)

Пример:

Класс сотрудник у которого три метода:

  • расчет зарплаты
  • отчет по часам работы
  • сохранить

Кто пользуется классом?

  • Финансовый директор (интересуется расчетом зарплаты)
  • HR (интересуется отработанными часами)
  • Технический директор (интересует бэкапы, использует save)

Функции calculatePay и reportHours скорее всего будут использовать функцию вычисления часов.

Если финансовый директор захочет исправить принцип подсчета часов, мы можем испортить логику для HR.

Создали объект, изменение в котором затрагивает сразу двух клиентов.

Как починить? Создаем три класса PayCalculator, HourReporter, EmployeeSaver. Разделили ответственность. Создаем классы по принципу: это поведение для этого клиента (актора), это поведение для этого.

(сделав это мы возможно продублировали код) (немножко копипаста лучше, чем немножко зависимостей)

Open-Closed Principle

Програмный артефакт должен быть открыт для расширения, но закрыт для модификаций.

(а чо делать если модель плохо написали? а ничего, принцип лоховский)

До какого-то предела так можно делать, но в какой-то момент это придется нарушить. Принцип предлагает отложить эти изменения в будущее.

Например, добавляя логирование, лучше делать декораторы, а не вставлять строчки в код.

Пример:

В приложении надо сформировать финансовый отчет в виде веб-страницы. Отчет занимает несколько экранов, отрицательные числа подсвечиваем красным.

Приходит стейкхолдер и говорит : Хочу тот же отчет, но для печати на ЧБ. (теперь красные символы надо сделать жирными)

Мы могли сделать так: (dataflow)

  • Financial Data -> Financial Web Reported -> Web reporter

И чего теперь делать? переписывать вообще всё?

Надо было делать так: -Financial Data -> Financial Analyzer -> Financial Report Data -Financial Report Data -> Web Reporter -Financial Report Data -> Print Reporter

Боб Мартин предлагает супер сложную картинку, которую словами не описать.

Суть: мы должны стараться организовать код таким образом, чтобы в случае изменения требований можно было просто добавить новый модуль.

Liskov substitution principle

If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 substituted for o2 then S is a subtype of T.

Два типа взаимозаменяемы, если вызывающая сторона не видит разницы.

Короче принцип примерно в том, что лучше писать код, используя более общие типы и подставлять подтипы в последний момент.

Какой бы ЯП у нас не был, он не гарантирует соблюдение этого принципа. Взяв интерфейсы мы можем написать их некорректно, нарушив принцип.

Стараемся не использовать type assertions, т.е не выяснять конкретный тип объекта, лучше писать общий код.

Interface Segregation principle

Программные сущности не должны зависеть от методов, которые они не используют.

(очень похоже на первый принцип)

Если есть три пользователя (актора) user1, user2, user3 которые из класса OPS дергают op1(), op2(), op3() соответственно, то это нарушение принципа. Следует сделать промежуточные интерфейсы U1 OPS, U2 OPS, U3 OPS и связать их с нашими пользователями.

Не нужно зависеть от модулей, которые предоставляют больше, чем вам нужно. (Например поэтому фреймворки это плохо)

Как я понял, с примеров Никиты, это принцип советует делать промежуточные интерфейсы между актором и классом, содержащим много сторонних функций. Это упрощает тестирование и изменения кода.

Dependency inversion principle

  • Модули верхних уровней не должны зависеть от модулей нижних уровней.
  • Оба типа модулей должны зависеть от абстракций.
  • Абстракции не должны зависеть от деталей
  • Детали должны зависеть от абстракций

(Если меняем что-то в БД, логика менятся не должна, но если поменяли логику, возможно придется поменять БД)

(Почему инверсии зависимостей? Как текут данные в программе? Сначала мы открываем TCP соединение, читаем байты, разбираем заголовок, понимаем метод, который нам нужен и переходим к логике. Т.е мы начинаем с самых деталей и переходим к абстракциям. Это прямая последовательность, а зависимости наши должны быть в другом порядке. Dependencies = inverted DataFlow)

  • Создаем зависимости от абстракций (лучше от интерфейсов, а не от типов)
  • Отказываемся от перегрузки функций (??)
  • Граф зависимостей должен быть ацикличным.

ТеПеРь ВЫ зНАеТе Про СоЛиД

16. Процесс, поток, виртуальная память. Контейнер, образ, Docker, Docker-compose.

Какие проблемы решает контейнеризация: когда мы хотим поставить свое приложение кому-то другому, сталкиваемся с зависимостями и их версионированием. Всё это воспроизвести сложно, контейнеризация позволяет сделать слепок нашей системы с нашим приложением и теперь мы можем распространять точно такое же состояние, как у нас.

Процесс - способ изолировать программы разных разработчиков. Контейнер для ресурсов (регистры, счетчик команд, открытые файлы, открытые сетевые соединения, виртуальная память). Процессы делят: файловую систему, сетевой стек, иерархию процессов, что-то ещё. Процесс создает изолированное состояние памяти, изображает монопольное владение процессором и основной памятью.

Доведем эту идею до предела, изолируем файловую систему, дерево процессов, сетевую подсистему, получим контейнер. Контейнеры по-прежнему делят ОС, но большая часть системных вызовов ведет к работе с изолированными ресурсами.

Контейнер (это процесс) - средство изоляции процессов или групп процессов; средство контроля доступа групп процессов к ресурсам; стандартная единица поставки приложения.

Docker - система для управления и распространения контейнеров.

Docker Engine:

  • Docker Daemon (управляет контейнерами)
  • Docker API (чтобы стучаться внутрь докера и удаленно им управлять)
  • Docker CLI (интерфейс к апи)

Как создать контейнер? Docker Image - шаблон с инструкциями. Информация, позволяющая привести состояние внутри контейнера к нужному состоянию. Можно создавать свои образы на базе других.

Образ - на самом деле слепок состояний файловой системы. (такие образы персистенты, обновляя докер мы создаем diff и применяем его)

docker run -it debian bash // запустить контейнер с именем образа debian и запустить внутри процесс bash

Пример докер-образа для нашего приложения

FROM golang:1.16.2-alpine3.13 as builder // базовый образ
RUN mkdir /build
ADD . /build/ // все файлы из текущей рабочей директории помещаем в папку build/
WORKDIR /build
RUN go build -a -o chat-server cmd/chat-server/main.go // теперь в директории лежит бинарник

FROM alpine:3.13 // хотим образ без лишних файлов, только бинарник
COPY --from=builder /build/chat-sever .

ENTRYPOINT [ "./chat-server"] // точка входа в образ

Ещё надо примонтировать внутрь докера файлы с ключами, а также прокинуть порты.

Как общаться между контейнерами, если они изолированы?

  • Никак
  • Bridge (VLAN, виртуальная сеть)
  • Host (избавляемся от изоляции сети контейнера)
  • Overlay (несколько докер хостов, создаем оверлай сеть (??))
  • Plugins (можно еще различные плагины подключать)

Bridge - виртуальный свитч (штука из компьютерных сетей). У каждого контейнера появляется адрес. По умолчанию все контейнеры подключаются к стандартному мосту.

docker network ls // все сети в докерах

Создание своего моста позволяет делать автоматическое разрешение доменных имён. Можем по имени контейнера обращаться к другому контейнеру. Кроме этого, изолирует нас от других непонятных контейнеров.

Создаем мост:

docker network create --driver bridge chat-net

Потом по ключу --network указываем имя моста

Данные внутри контейнера сохраняются на новый слой. После смерти контейнера данные пропадают в небытие. Способы сохранять данные

  • Тома (докер выделяет папочки на хосте и там хранит свою инфу)
  • Монтирование (связываем папку на хосте и в докере)

Docker-compose: инструмент запуска многоконтейнерных приложений

(ну там похожим синтаксисом описываем несколько образов и типо они вместе запускаются)

17. Транзакции. ACID.

Книга с кабанчиком очень крутая, Никита советует прочитать про распределенные системы. (Designing Data-Intensive Applications, Martin Kleppmann)

  • НУЖНО ЛИ РАССКАЗЫВАТЬ ПРО ACID ТРАНЗАКЦИИ? СПРАШИВАЕТ НИКИТА
  • НЕТ, У НАС ЭТО БЫЛО НА КУРСЕ ПО БД. ОТВЕЧАЕТ ОЛЯ
  • НИКИТА ПРОПУСКАЕТ ВЕСЬ МАТЕРИАЛ ЭТОГО БИЛЕТА
  • СПАСИБО БОЛЬШОЕ ОЛЯ

Я нашел свой конспект по БД и там было следующее:

Транзакции: множество операций, выполняемое приложением, которое переводит БД из одного корректного состояния в другое при условии, что транзакция выполнена полностью.

begin;
-- все что тут написано происходит внутри одной транзакции
commit;

Транзакции нужны для защиты данных от множественных записей, неожиданных отключений подключения, некорректных промежуточных состояний если вы решили работать на live бд без бэкапов, используйте хотя бы транзакции, их можно откатить

ACID свойства транзакций:

  • Атомарность (либо транзакция полностью выполнена, либо не выполнена вовсе)
  • Согласованность (по мере выполнения транзакций данные переходят из одного корректного состояния в другое)
  • Isolation (конкурирующие за доступ транзакции выполняются последовательно и изолировано)
  • Долговечность (если транзакция завершена успешно, то те изменения данных, которые были ею произведены не могут быть потеряны ни при каких обстоятельствах)

Аномалии одновременного выполнения:

  • Потерянное обновление
  • Грязное чтение
  • Неповторяющееся чтение
  • Фантомное чтение

Потерянное обновление:

  • Операция разбивается на чтение и запись
  • Если мы тут читаем 1000, записываем 1100
  • update T set amount = amount + 100 where id = 1;
  • Одновременно в другом месте другой пользователь делает то же самое,
  • Он читает 1000, записывает 1100
  • Итого в поле лежит 1100
  • А где ещё 100 рублей? А все, нет 100 рублей.

Грязное чтение:

  • Чтение состояния базы в процессе выполнения другой транзакции
  • Данные явно могут изменится внутри транзакции, она еще не кончилась
  • Мы читаем грязные данные, неподтвержденные

Неповторяющееся чтение:

  • В рамках одной транзакции сделав один запрос два раза получаем разные данные
  • Из-за того, что их поменяла какая-то другая транзакция между моментами исполнения запроса

Фантомное чтение:

  • В рамках одной транзакции вызываем 2 раза запрос, например с агрегирующей функцией по данным
  • Между двумя выполнениями вклинилась транзакция, которая добавляет какую-то запись в наши данные
  • Опять два раза получили разные значения по одному казалось бы идемпотентному запросу
  • Даже repeatable read тут не помогает

Уровни изоляции:

  • Read uncommited: разрешается читать до коммита, но это грязные данные, они еще не подтверждены (например счетчик просмотров в youtube можно считать неточно, зато без блокировок)
  • Read commited: изменяемые внутри транзакции данные можно читать только после коммита (блокировка на меняемые данные, строки)
  • Repeatable read: блокировка на читаемые данные! (как-то сильно, зато запрещает невоспроизводимое чтение)
  • Serializable: запрещает добавление записей которые попадут в данные, с которыми взаимодействует другая транзакция

По умолчанию в СУБД используется read commited

Основной механизм выполнения одновременных транзакций раньше - механизм блокировки и синхронизации (MS SQL Server) В postgres когда начинается какая-та транзакция, делается snapshot данных и транзакция работает со своей версией данных, она не может их испортить в процессе выполнения Поэтому грязного чтения там быть не может

Добавлю еще текст со слайдов Никиты:

  • Atomicity (Если нужно выполнить группу операций, которые нужно выполнить в рамках одной транзакции, то должны быть выполнены либо все операции из группы, либо ноль)
  • Consistency (Любой объект должен находиться в непротиворечивом состоянии, например условия движка типо unique выполняются)
  • Isolation (в СУБД конкурентные транзакции должны выполняться так, будто они выполняются последовательно)
  • Durability (данные в транзакции должны быть сохранены в ПЗУ (постоянное запоминающее устройство))

18. Типы распределенных систем. Мотивация к созданию распределенных систем.

Распределенные системы - набор независимых узлов, которые выглядят для пользователя как один.

Распределенные системы:

  • Compute-Intensive systems (занимаемся машинкой, обучаем GPT-3)
  • Data-Intensive systems (предоставление каких-то сервисов, связанных с данными, пользователям: сообщения, какой-то интерактив, аналитика)

Мы рассматриваем второй тип распределенных систем.

Зачем говорить про распределенные системы?

  • Нужно обеспечить отказоустойчивость (фотка где горит дата-центр)
  • Много данных, большая нагрузка на чтение и запись, нужно справляться
  • Хотим уменьшить время отклика (сделать жизнь пользователя комфортной) ( вот тут мы же можем просто алгосы ускорить, но на самом деле мы в любом случае упираемся в задержку сети и тогда уже, чтобы уменьшить пинг, надо запускать сервис на всех континентах, получается распределенная система)

19. Типы масштабирования систем.

Масштабирование:

  • Вертикальное (купить железо получше) (Это: дорого, имеет свой потолок)
  • Горизонтально масштабирование (Переложить все на плечи провайдера, сделать много виртуальных машин)

Не всё так просто! В рамках одного узла у нас есть:

  • общая память
  • общие часы
  • ACID-транзакции
  • Надежная передача сообщений (пакет от одного процессора к другому пройдет без потерь)

Всего этого нет, когда у нас много узлов!

Кроме этого, ещё сложности:

  • все узлы работают конкурентно
  • узлы отказывают независимо
  • нет общего состояние (и его тяжело выработать)

Сеть ненадежна!

  • Любой запрос может потеряться
  • Ответ на запрос может прийти в непредсказуемое время
  • Узел мог упасть
  • Узел мог обработать ваш запрос, но ответ не пришел

Часы ненадежны:

  • У каждого процессора свой генератор часов, сложно настроить одинаковые часы на всех узлах
  • Таймауты наступают "не одновременно"
  • Два узла не могут выработать совместное представление о порядке событий (часов то нет...)

Основные типы горизонтального масштабирования:

  • Репликация
  • Партицирование

Эти штуки можно использовать одновременно, они дополняют друг друга

20. Репликация.

Дублирование данных, пытаемся создать копии идентичных данных на нескольких узлах.

  • Дублирование данных (пожар нам не страшен)
  • Добавляет избыточность
  • Увеличивает производительность (может часть пользователей отправить туда, часть сюда)

Типы репликации:

  • Ведущий/Ведомый
  • Много ведущих
  • Без ведущих

Ведущий / Ведомый (Leader / Follower):

Узлы делим на два типа: Leader, Follower.

  • Ведущий работает, обслуживает запросы на запись
  • Ведомый обслуживает запросы на чтение
  • Пользователь, изменяя состояние объекта, посылает запрос на ведущего, тот совершает транзакцию и транслирует свое состояние в ведомого
  • Если пользователь хочет прочитать данные, он пойдет в ведомого и будет оттуда вычитывать состояние

Как убедиться, что ведущий и ведомый находятся в одном состоянии? Как транслировать состояние от Leader to Follower?

Такая репликация делится на синхронную и асинхронную (ну тут картинку надо смотреть)

  • Приходит пользователь с данными, посылает их лидеру (L), тот начинает транзакцию
  • (Синхронный вариант) Ведущий отправляет запрос на запись в ведомого (F1), у которого начинается транзакция, как только она завершится, об этом узнает L, завершает свою транзакцию и уведомляет пользователя
  • (Асинхронный вариант) Ведущий отправляет запрос на запись в ведомого (F2), но не L дожидается завершение и сразу рапортует пользователю о завершении транзакции

Синхронная:

  • Нужно дождаться всех реплик (-)
  • Нужно ждать, пока на всех репликах появится последнее состояние (если какой-то последователь упал, то все его ждут) (-)
  • Читатель гарантированно получает последнее состояние, как в лидере (read-your-writes consistency) (+)
  • Нет потери данных после падения лидера (+)

Асинхронная:

  • Лидер может сразу ответить (+)
  • Реплики могут восстанавливаться фоново (+)
  • Читатель может получить старое состояние (-)
  • После падения лидера будет потеря данных (-)

На практике обычно используют оба типа репликаций.

Что делать при отказе узлов?

Отказал ведомый:

  • Поднялся
  • Посмотрел последнюю транзакцию
  • Попросил обновления

Отказал ведущий(Failover):

  • Определить, что лидер мертв (тайм-аутом, например)
  • Выбрать (leader election) или назначить нового
  • Сообщить всем, что лидер новый (важно не допустить существование двух лидеров, split brain)

Много ведущих:

Зачем?

  • Если у нас упал лидер, система становится нерабочей. Избавляемся от единой точки отказа

Проблема многих ведущих:

  • Один и тот же объект может модифицироваться в разных лидерах, нужно разрешать конфликты

Как разрешать конфликты (примерно как угодно):

  • Избегать конфликты (репликация с партицированием одного диска (???))
  • Назначить каждой записи свой ID (вместо разрешение конфликта сохраняем два состояние, fork, пусть пользователь сам делает merge потом)
  • Выбрать рандомно

Без ведущих:

ВыХодИт зА РамКи КуРса, читайте про blockchain

21. Партицирование (шардирование)

Разделяем данные по какому-то признаку на несколько непересекающихся частей, помещаем каждую часть в отдельный узел.

  • Добавляет ресурсы (каждый узел обрабатываем лишь часть данных)
  • Увеличивает производительность

Чего хотим добиться?

  • Равномерного распределения нагрузки по партициям
  • Равномерного распределения объектов по партициям

Способы шардирование (мы рассматриваем случай key-value storage, объекты доступны нам по ключу):

Случайное шардирование

  • пользователь создает новый объект в случайном шарде
  • если хотим по ключу, надо спросить у всех шардов, есть ли такой

(звучит безумно, но есть класс задач, которые называют аналитическими, когда нас интересует агрегация)

Шардирование по диапазону ключей

  • Книги разделяем по алфавиту, (A-G, H-P, Q-U, ...)

Шардирование по хешу ключа

  • Назначаем узлам диапазоны хешей

Hot spots

Ни один способ не позволяет избавиться от hot spots (вирусное видео, которое лежит на одном шарде, грузит только его и может уронить)

Можно:

  • перешардировать (выделить для hot spots один шард)
  • сделать репликацию: для горячего шарда, сделать много реплик

Шардирование вторичных ключей

  • Объекты - машины, шардируем по ID
  • Как эффективно искать машину по цвету?

Варианты:

  1. Индекс с цветами полностью положить на какой-то шард. (проблема: индекс будет расти)
  2. Партицируем один большой индекс. (В каждом шарде будет свой индекс по ключу цвета)

Ребалансировка

Рано или поздно:

  • Место в партициях закончится
  • Нагрузка вырастет
  • Машины устареют и сломаются

Нужно перемещать данные с одного узла на другой - ребалансировка

Требования:

  • Сохранить равномерность распределения данных
  • Во время балансировки система должна работать на чтение и запись
  • Свести к минимуму перемещение данных

Как это сделать?

  • Сделать очень много партиций (партиций >> числа узлов) (если добавляется нвоый узел, то с каждого узла забираем чуть-чуть париций)
  • Большие партиции делим на партиции поменьше (можно изображать дерево)

В этой теме придется разбираться, потому что шардирование реплицирование не решенная до конца проблема. (Если вы хотите написать диплом, отличная область, в которой ничего не умеют)

Нужно ли клиенту знать об узлах и партициях? Маршрутизация.

Подходы:

  • Клиент идет на произвольный узел, узел перенаправляет куда надо (каждый случай должен знать актуальное состояние топологии, как нормально ребалансировать?)
  • Клиент шлет запросы в рутер (через один узел проходит весь трафик, единая точка отказа, рутер надо реплицировать)
  • Клиент узнает, где нужная партиция (об изменениях топологии нужно уведомлять клиентов)

Что делать?

  • Есть готовый софт (Zookeeper, Consul, etcd, алгоритмы достижения консенсуса, внутри делают распределенные надежные транзакции)

22. Транзакции в распределенных системах. Двухфазный коммит (2PC).

Консенсус:

  • Выбор главного
  • Маршрутизация
  • Распределенные транзакции (транзакции, затрагивающие несколько узлов в распределенной системе)

Проблема распределенных транзакций:

  • Движок СУБД дает нам транзакции в рамках одного узла
  • Если мы совершим транзакцию на каждом узле, то это не гарантирует консистентность всей системы ( на одном узле может пройти, на другом нет)
  • Можно начать вложенные транзакции, что-то вроде блокировок. Можем легко заблокироваться навсегда, если где-то не пройдет транзакция
  • Завести третью БД, где отмечать принятые транзакции

Способ все же сделать это:

2PC (Two-Phase Commit)

  • (Write-data): Клиент вносит изменений в СУБД 1 и СУБД 2 (пока не помечены как завершенные транзакции)
  • СУБД возвращают клиенту ответ о том, что поняли его желаемые изменения
  • Фаза подготовки: клиент отправляет сигналы в оба узла, подготовьте пожалуйста коммит. (все данные подготовливаются в обоих узлах)
  • Клиент дожидается сообщения о том, что оба узла готовы соевршить транзакцию
  • Фаза коммита: Клиент отправляет сообщение коммит на обе СУБД. В момент, когда оба узла после фазы подготовки отметили, что готовы, они подписываются, что обязаны совершить транзакцию при сообщении коммит.
  • Обе СУБД делают коммит и сообщают об успешном звершении клиенту.

Чего мы добились?

  • Если какой-то из двух узлов упал на фазе write-data (первая), то все хорошо: клиент не получит подтверждение, попытается сделать еще раз
  • Если узел упадет на стадии подготовки, клиент не получит подтверждение о том, что узел подготовился и сможет повторить все заново
  • Если между prepare & commit что-то упадет, клиент не получит сообщение об успешном завершении коммита, нужно поднять упавший узел, в нем уже готово все для коммита, надо потребовать его сделать

Недостаток:

  • Если падает клиент, в базах могут остаться записанные, но не закоммиченные данные

23. CAP-теорема

Бонусная тема =) (а билет почему-то не бонусный....)

CAP:

  • Consistency - данные во всех узлах не противоречат друг другу (я хочу видеть, что записал)
  • Availability - получаем всегда корректный ответ. (необязательно одинаковый с разных узлов)
  • Partition tolerance - каждая изолированная секция ведет себя корректно (система восстанавливается после проблем со связностью)

Тут много слов, которые требуют доопределения в каждом конкретном случае

Теорема: одновременно может выполнять только 2 из 3 свойств

Available & Partition Tolerant System

  • хотим записать данные в узел 1, другой прочитать из узла 5
  • разрежем связь между 1 и 5
  • записали в 1 новое состояние, а из 5 получили старое состояние, т.к система всегда отвечает на запросы (A) несмотря на проблемы со связностью (P)
  • получили не (C) систему

Consistent & Partition Tolerant System

  • пишем в 1 новое состояние, разделяем сеть от 1 до 5
  • хотим прочитать из 5, но т.к система (C) она каким-то образом знает о том, что состояние у нее старое
  • система ничего не может ответить (корректного), потеряли (A)

Consistent & Available System

  • разделили сеть между 1 и 5
  • нет способа доставить состояние из 1 в 5, потеряли (P)

Проблемы с теоремой:

  • грубая модель распределенной системы
  • в модели используются крайности, простые идеальные состояния
  • обычно мы можем исполнять свойства не полностью, находить баланс между ними
  • не бывает CA систем (ну нет и нет)
  • короче теорему знать надо всем, но смысла от нее нет, но на экзамене мы вас про нее спросим (знать же надо!)

Тут дальше в лекциях началась какая-то еще одна бонусная тема про паттерны с одним узлом, я не знаю куда ее вставлять, надеюсь она не нужна

Pod - несколько контейнеров, которые делят общий ресурс. (Контейнер с контейнерами)

Паттерн Sidecar:

  • В рамках Pod у нас есть Application Container и Sidecar container (тележка от мотоцикла, какой-то вспомогательный контейнер)
  • например у нас есть legacy http service
  • переписывать его не хотим, создаем сбоку второй контейнер с SSL Proxy которые общаются между собой через localhost

Паттерн Ambassador:

  • Внутри pod: Application container связан с Ambassador container, который за нас общается с внешним миром
  • (ну например у нас странный протокол общения внутри исходного приложения, а от нас требуют grpc, сделаем представителя)

а.... тут еще потом много бонусных тем

в конспекте их не будет, ждем бонусный конспект

24. Мониторинг. Виды мониторингов.

Постоянное и систематическое наблюдение за каким-то явлением.

Мотивация:

  • поиск узких мест
  • поиск проблем (где-то стали слишком долго отвечать или вообще перестали отвечать -> мониторинг нам поможет)
  • спокойно спать по ночам (заранее можем понимать, увеличился ли объем записанных данных и упадет ли сервис ночью)

Мониторинг это, помимо прочего, числе перевод с языка "ценность для пользователя" на язык метрик.

Что будем наблюдать?

  • нормальное или ненормальное поведение системы (а наше API отвечает?)
  • потребление ресурсов
  • отладка (почему этот процент пользователей слишком мало времени проводит на странице)
  • инфраструктурные изменения (наличие жестких дисков, связь между сервисами)
  • сообщение о неожиданном поведении (алертинг)

Какие проблемы можем обнаружить?

  • переполнился диск -> нет новых данных, падает приложение
  • баг в приложении -> ошибки в ответе
  • высокая температура -> умирает железо
  • упала сеть -> не достучаться
  • используем мало памяти -> теряем деньги
  • используем много памяти -> приходит OOM Killer

Виды мониторинга:

  • Проверки (периодические запросы к системе с целью узнать, в каком она состоянии ping, cron)
    • (+): лучше, чем ничего, просто сделать
    • (-): наблюдаем черный ящик, нет контекста происходящего
  • Логи (записи о каких-то событиях в хронологическом порядке)
    • (+): можно добиться высокой детализации, легко производить
    • (-): потенциально дорого, сложно связывать логи разных систем
  • Трассировки (пошаговое исполнение сложного запроса: запрос в кеш, запрос к бекенду, запрос в user-serivce, запрос в redis, например можно добавлять дополнительные заголовки в запросы)
    • (+): можно легко находить узкие места
    • (-): сложно реализовать, дополнительная нагрузка в систему
  • Метрики (набор числовых значений с временными метками)
    • (+): дешево, можно агрегировать
    • (-): не очень детализировано, не отображаются межсервисные связи

Метрики (подробнее):

  • счетчик / counter (увеличивается со временем: количество обработанных запросов, возникших ошибок, интересно смотреть на производные)
  • измеритель / gauge (размер, количество, объем, количество запущенных горутин, количество запросов в обработке, количество потребляемой памяти)
  • гистограмма (раскладываем наблюдения по корзинам, время исполнения запроса)

Нужно заранее договориться о пороговых значениях метрики, понять зачем мы снимаем метрики изначально.

Prometheus - инструмент для мониторинга и алертинга

  • (+) простой, популярный, сбор метрик и хранение, инструментированное (не надо писать свои штуки), поиск алерты, дашборды
  • (-) плохо масштабируется, не умеет другие типы мониторинга, не умеет искать сам аномалии

25. Очередь сообщений и брокер.

Что было до этого (способы сервисам общаться)?

  • REST API
  • RPC

Зачем еще одна схема взаимодействий? Раньше: есть клиент, есть сервер, клиент посылает запрос на сервер и ждет ответа. В момент ожидания может отвалиться сеть, упасть сервер, произойти большая задержка. Клиент все это время не может бросить контекст и обязан дожидаться.

Потоковая обработка: каждое событие обрабатываем отдельно (противопоставлена пакетной обработке).

Потоки событий.

  • Событие - какой-то кусок данных
  • Будет лучше, если данные имеют структуру и типизированны
  • Производитель - тот, кто генерирует события
  • Потребитель - тот, кто получает события

Как реализовать такую систему обмена сообщениями?

  • Unix pipes, TCP (могут связать только пару)
  • Что делать, если создается больше событий, чем обрабатывается
    • отбрасывать события (UDP)
    • ставить события в очередь
      • а что если очередь растет?
    • применять контроль потока (unix pipes, tcp)
  • что если упадет один из узлов?

Брокер сообщений

  • Приложение, которое перенаправляет сообщения (от тех, кто публикует к тем, кто получает)
  • Использует формальные протоколы (AMQP)
  • Архитектурный стиль, позволяющий разъединить сервисы

Очередь сообщений - специализированная СУБД, которая оптимизирована для хранения и извлечения сообщений, с поддержкой хронологического порядка обработки

Очередь - одна из компонент брокера сообщений

Выгода от брокера:

  • Клиенты могут появляться, исчезать, падать, а система сохранит надежность
  • Клиенты становятся асинхронными (не нужно блокироваться и ждать ответа)
  • Гарантия отправки и обработки сообщений (если клиент отправил сообщений, он может быть уверен, что кто-то его обработает)

Основные паттерны:

Point-to-Point

  • Sender - Broker -Receiver (простой канал, ну типа)

Publish/Subscribe (Fan-out):

  • Sender - Broker (с внутренними topic) - Какое-то число Receiver
  • Receiver приходят к Broker и подписываются на интересные темы
  • Когда sender отправляет сообщение брокеру, он раздает его на всех подписчиков (Receiver) данной темы

Load Balancing:

  • Похоже на прошлый паттерн, но теперь брокер выбирает одного получателя, кому отправит сообщение, а не всем подписчикам
  • Например, сообщение - самолет задерживается, на такой тип сообщений подписано несколько групп: диспетчеры, погрузчики, но в каждой группе достаточно одного обработчика

Сфера применения:

  • Финансовые транзакции и обработка платежей (важно, чтобы каждое сообщение надежно доходило и обрабатывалось один раз)
  • Обработка выполнения заказа (склады, интернет магазины)
  • Чувствительные данные
  • Интернет вещей

Kafka:

  • распределенная платформа трансляции сообщений
  • использует партицированные логи для быстрой обработки сообщений (все события, которые делаем в кафке, пишутся в лог)
  • работает вместе с ZooKeeper(распределенное хранилище данных) (распределенная платформа, много узлов, репликация, нужен помощник)

Почему Кафка платформа?:

  • Предлагает изменить дизайн нашего приложения, резко контрастирует с классическим подходом к СУБД, где база хранит слепок текущего состояние (сколько товара на полке, в итоге часто товара нет в наличие)
  • Записываем все изменения, по логу вычисляем текущее состояние

26. Зачем нужен язык Go

  • Стремительно набирает популярность в серверной разработке
  • Хорошо иметь еще один инструмент в запасе (кроме петона)
  • Го простой язык, легко перейти с Java/C++/C
  • Документация Go, число деталей, намного меньше, чем у тех же C++/Java
  • Go стремится к простоте и единообразию (хочется всего один способ сделать вещь правильно)
  • Компилируется в нативный код (лучше чем JavaScript)
  • Автоматическое управление памятью (лучше чем C/C++)
  • Удобная поддержка конкуретного программирования (на уровне синтаксиса, в отличие от C++/Java)
    • Асинхронность из коробки (без фреймворков и библиотек)
    • Без костылей сверху (в питоне есть async/await, но во-первых, у него нет своего планировщика, во-вторых, заражает весь остальной код)
    • Хорошо утилизирует ресурсы машины (фиксированное число потоков и переключение в user-thread)
    • Асинхронный код выглядит почти как последовательный
  • Много инфраструктурного кода написано на Go (docker, kubernetes, etc)

The Why of Go?

go is not good:

  • too simple / lack of sugar
  • no generics
  • bad dependency management
  • stuck in 70/80s
  • error handling
  • no unused imports
  • too opinionated
  • too verbose
  • no ternary operator
  • no macros or templates

Go's 21st Century Characteristics

  • Concurrency (C++ at Google was awful with concurrency)
  • Distributed Systems
  • Garbage Collection
  • Memory Locality
  • Readability

27. Синтаксис Go. Система пакетов. Базовые типы.

Пакеты в Go - набор файликов, которые делят область видимости типов и переменных

package main - точка входа

Синтаксис учим тут

Packages

Every Go program is made up of packages.

Programs start running in package main.

This program is using the packages with import paths "fmt" and "math/rand".

By convention, the package name is the same as the last element of the import path. For instance, the "math/rand" package comprises files that begin with the statement package rand.

Functions

Пример функции, если кто-то вообще не открывал записи курса

func add(x int, y int) int {
	return x + y
}

Variables

The var statement declares a list of variables; as in function argument lists, the type is last.

Basic types

bool

string

int  int8  int16  int32  int64
uint uint8 uint16 uint32 uint64 uintptr

byte // alias for uint8

rune // alias for int32
     // represents a Unicode code point

float32 float64

complex64 complex128

Zero values

Variables declared without an explicit initial value are given their zero value. (0, false, "")

Arrays

The type [n]T is an array of n values of type T.

var a [10]int

Slices

An array has a fixed size. A slice, on the other hand, is a dynamically-sized, flexible view into the elements of an array. In practice, slices are much more common than arrays.

The type []T is a slice with elements of type T.

primes := [6]int{2, 3, 5, 7, 11, 13}
var s []int = primes[1:4]

A slice does not store any data, it just describes a section of an underlying array.

Changing the elements of a slice modifies the corresponding elements of its underlying array.

Slices can be created with the built-in make function; this is how you create dynamically-sized arrays.

The make function allocates a zeroed array and returns a slice that refers to that array:

a := make([]int, 5)  // len(a)=5

28. Горутины и планировщик. Параллельность, асинхронность.

Параллелизм != Concurrency

Переносим планирование в user space. Системные вызовы и переключение контекста это долго, давайте перенесем планировщик из пространства ядра в пользовательское пространство. Так мы сможем реализовать кооперативную многозадачность и управлять ей. Хотим чтобы задачи в рамках нашей программы не конкурировали за CPU.

Планировщик Go.

Термины и сокращения:

  • P - логический процессор (в runtime go есть структура, называющаяся процессор, условно соответсвутет физ. процессору)
  • M - machine - поток ОС
  • G - goroutine
  • GRQ - Global Run Queue
  • LRQ - Local Run Queue

Хотим максимально эффективно утилизировать физические ядра и не создавать конкуренции между нашими горутинами за них.

Общая схема:

  • На каждое физическое ядро Core создадим M, каждому M создадим структуру P.
  • На логическом процессоре P исполняется горутина G. Кроме этого, каждому P принадлежит своя LRQ, куда помещаются запланированные на этот процессор горутины.
  • В стороне остается GRQ, куда по-умолчанию встают горутины. Потом их забирают оттуда логические процессоры.

Когда запускается планировщик?

  • go-operator (запланировать горутину)
  • сборка мусора
  • системные вызовы (нужно дождаться ответа, поэтому эту горутину можно отпускать)
  • синхронизация и оркестрация (атомарные операции, блокировки, каналы)
  • пролог функции
  • по тайм-ауту в долгих вычислениях

Если коротко, есть некоторые моменты в нашей программе, когда планировщик понимает, что наша горутина готова отпустить процессор.

Асинхронные системные вызовы

Kогда системным вызовом сообщаем намерение получить или записать какие-то данные, операционная система выполняет ваш запрос, например читаете из сокета 20 байт, вещаете call и ждете, пока он вернется.

В момент асинхронного системного вызова, планировщик снимет нашу горутину G1 с процессора P и положит в структуру Net Poller, на процессор положим горутину из LRQ. G1 будет в отдельной очереди дожидаться, пока ему придут 20 байт.

Синхронные системные вызовы

Не отдают управление нашей программе, пока не выполнятся.

Если G1 выполняет синхронный системный вызов, планировщик отрывает от процессора P его тред M1, и из пулла тредов (создает или достает) тред M2 операционной системы, на котором выполняет G2 из LRQ. Тред M1 вместе с прикрепленной к нему горутиной G1 пока полежит в сторонке. Рано или поздно системный вызов в G1 разблокирует M1, тогда G1 возвращается в LRQ, а поток M1 убирается куда-то для дальнейшего нашего использование или удаляется за ненужностью.

Перехват задач

На одном процессоре P1 могут задачи кончится (в LRQ) сильно раньше, чем на P2.

Алгоритм перехват задач:

  • Редко проверяем GRQ (с вероятностью 1/61)
  • Проверяем LRQ (есть ли у нас задачи?)
  • Пробуем воровать у соседей (у других процессоров)
  • Проверяем GRQ
  • Опрашиваем сеть (Net poller, смотрим не случилось ли нужных callbacks)

29. Механизмы синхронизации в Go.

  • Wait Groups (что-то типо семафора)
  • Locks (Mutex, RWMutex)
  • Atomics
  • Channels

Каналы

ch := make(chan int) // канал int
ch <- 2 // записать в канал
x := <- ch // прочитать из канала

При записи в канал горутина блокируются, пока из него что-то не прочитают. Аналогично, при чтении из канала горутина блокируется, пока в канал что-то не запишут.

close(ch) // сигнал закрытия канала

Из закрытого канала можно читать, но писать туда нельзя (panic). После того, как мы вычитали из закрытого канала все валидные данные, мы читаем значения по умолчанию. (для того, чтобы это проверять на самом деле из канала можно читать x, ok <- ch)

Синтаксис создания горутины

go func() { // оператор go только помещает горутины в GRQ, больше ничего не гарантируется
	... some code ...
}

С помощью каналов можно реализовывать pipeline. Например, у нас crawler, мы делаем запросы и парсим страницы. Запросы это IO-bound задачи, а парсинг быстрые CPU-bound. Хорошая идея развязать эти этапы на несколько стадий.

  • Первая стадия - producer
  • Какие-то другие Action стадии (например стадия парсинга страниц)
  • Последняя стадия - consumer (тут например пишем результат в базу)

Простой pipeline:

  • Producer - генерирует натуральные числа
  • Action - возводить стадии в ^2
  • Consumer - выводить квадраты на экран
naturals := make(chan uint64)
squares := make(chan unit64)

go func() {
	for i := uint64(0); i++ {
		naturals <- i
	}
}

go func() {
	for {
		x := <- naturals
		squares <- x * x
	}
}

for {
	fmt.Println(<-squares)
	time.Sleep(1 * time.Second)
}

30. Управление памятью в Go.

Как мне показалось, этого особо не было в курсе. Я спросил у Никиты, что тут нужно.

  • Pavlov Ivan 1:27 PM
    • Добрый день! А что такое "30. Управление памятью в Go." в нашем списке тем? Кажется, явно эта тема в лекциях не поднималась. А если гуглить, то быстро становится страшно.
  • Никита Родионов 1:33 PM
    • Добрый! Поднималась. Но довольно поверхностно, да. Там ничего страшного нет. Вспомните операционные системы, С и С++. Добавляем к этому два простых факта: в Го есть сборщик мусора, который работает асинхронно с основным кодом компилятор сам решает, когда выделять память на стеке, а когда в куче Дальше, будут вопросы не на знания Го, а на подумать
  • Pavlov Ivan 1:34 PM
    • Т.е слова типа mcentral, mspan, mheap знать не нужно?
  • Никита Родионов 1:38 PM
    • Не, это было бы слишком

Потом никита скинул ссылку на эту серию статей и написал, что первых двух хватит

Читаем там!

*/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment