Compare commits
1 Commits
main
...
actency-my
Author | SHA1 | Date |
---|---|---|
|
ea50e49a7e |
72
Makefile
72
Makefile
|
@ -1,72 +1,14 @@
|
|||
cdir = $(shell pwd)
|
||||
|
||||
check-lint:
|
||||
which golangci-lint || (GO111MODULE=off go get -u github.com/golangci/golangci-lint/cmd/golangci-lint)
|
||||
up:
|
||||
sudo -S docker-compose -f ./cicd/docker-compose.yml up -d --build
|
||||
|
||||
lint: check-lint
|
||||
@echo "+ $@"
|
||||
@golangci-lint run -v --timeout 3m \
|
||||
--fast \
|
||||
--issues-exit-code=0 \
|
||||
--print-issued-lines=false \
|
||||
--enable=gocognit \
|
||||
--enable=gocritic \
|
||||
--enable=prealloc \
|
||||
--enable=unparam \
|
||||
--enable=nakedret \
|
||||
--enable=scopelint \
|
||||
--disable=deadcode \
|
||||
--disable=unused \
|
||||
--enable=gocyclo \
|
||||
--enable=golint \
|
||||
--enable=varcheck \
|
||||
--enable=structcheck \
|
||||
--enable=maligned \
|
||||
--enable=errcheck \
|
||||
--enable=dupl \
|
||||
--enable=ineffassign \
|
||||
--enable=interfacer \
|
||||
--enable=unconvert \
|
||||
--enable=goconst \
|
||||
--enable=gosec \
|
||||
--enable=megacheck \
|
||||
./...
|
||||
down: shutdown clean
|
||||
|
||||
app-up:
|
||||
docker-compose -f ./cicd/dc_app.yml up -d --build
|
||||
|
||||
app-down:
|
||||
docker-compose -f ./cicd/dc_app.yml down
|
||||
|
||||
app-reload: app-down app-up
|
||||
|
||||
db-up:
|
||||
rm -rf /opt/mysql_master/* ; \
|
||||
rm -rf /opt/mysql_slave1/* ; \
|
||||
rm -rf /opt/mysql_slave2/* ; \
|
||||
docker-compose -f ./cicd/dc_db.yml up -d --build ; \
|
||||
./cicd/init.sh
|
||||
|
||||
db-down:
|
||||
docker-compose -f ./cicd/dc_db.yml down
|
||||
|
||||
client-up:
|
||||
docker-compose -f ./cicd/dc_client.yml up -d --build
|
||||
|
||||
client-down:
|
||||
docker-compose -f ./cicd/dc_client.yml down
|
||||
|
||||
prom-up:
|
||||
docker-compose -f ./test/monitor/docker-compose.yml up -d --build
|
||||
|
||||
prom-down:
|
||||
docker-compose -f ./test/monitor/docker-compose.yml down
|
||||
|
||||
up: db-up app-up prom-up
|
||||
|
||||
down: prom-down app-down db-down
|
||||
shutdown:
|
||||
sudo -S docker-compose -f ./cicd/docker-compose.yml down
|
||||
|
||||
clean:
|
||||
docker rmi $$(sudo docker images -a | grep '<none>' | awk '{print $$3}')
|
||||
sudo docker rmi $(sudo docker images | grep '<none>' | awk '{print $3}')
|
||||
|
||||
.PHONY: app-up app-down app-reload prom-up prom-down up down
|
||||
.PHONY: up down
|
53
README.md
53
README.md
|
@ -1,20 +1,41 @@
|
|||
## Домашние задания по курсу [Highload Architect](https://otus.ru/lessons/highloadarchitect/)
|
||||
### [01. Заготовка для социальной сети](test/dz001/README.md) (ЗАЧЕТ)
|
||||
### [02. Производительность индексов](test/dz002/README.md) (ЗАЧЕТ)
|
||||
### [03. Полусинхронная репликация](test/dz003/README.md) (ЗАЧЕТ)
|
||||
# Заготовка для социальной сети
|
||||
Цель: В результате выполнения ДЗ вы создадите базовый скелет социальной сети, который будет развиваться в дальнейших ДЗ.
|
||||
|
||||
-----
|
||||
###В данном задании тренируются навыки:
|
||||
- декомпозиции предметной области;
|
||||
- построения элементарной архитектуры проекта
|
||||
Требуется разработать создание и просмотр анект в социальной сети.
|
||||
|
||||
### [04. Масштабируемая подсистема диалогов](test/dz004/README.md) (не начата)
|
||||
### [05. Лента новостей социальной сети](test/dz005/README.md) (в процессе)
|
||||
###Функциональные требования:
|
||||
- Авторизация по паролю.
|
||||
- Страница регистрации, где указывается следующая информация:
|
||||
- Имя
|
||||
- Фамилия
|
||||
- Возраст
|
||||
- Пол
|
||||
- Интересы
|
||||
- Город
|
||||
- Страницы с анкетой.
|
||||
|
||||
-----
|
||||
###Нефункциональные требования:
|
||||
- Любой язык программирования
|
||||
- В качестве базы данных использовать MySQL
|
||||
- Не использовать ORM
|
||||
- Программа должна представлять из себя монолитное приложение.
|
||||
- Не рекомендуется использовать следующие технологии:
|
||||
- Репликация
|
||||
- Шардинг
|
||||
- Индексы
|
||||
- Кэширование
|
||||
|
||||
### [06. Репликация из MySQL в tarantool](test/dz006/README.md) (не начата)
|
||||
### [07. Онлайн обновление ленты новостей](test/dz007/README.md) (не начата)
|
||||
### [08. Разделение монолита на сервисы](test/dz008/README.md) (не начата)
|
||||
### [09. Отказоустойчивость приложений](test/dz009/README.md) (не начата)
|
||||
### [10. Сервис счетчиков](test/dz010/README.md) (не начата)
|
||||
### [11. Внедрение docker и consul](test/dz011/README.md) (не начата)
|
||||
### [12. Мониторинг](test/dz012/README.md) (не начата)
|
||||
### [13. Проектная работа](test/dz013/README.md) (не начата)
|
||||
Верстка не важна. Подойдет самая примитивная.
|
||||
Разместить приложение на любом хостинге. Например, heroku.
|
||||
|
||||
ДЗ принимается в виде исходного кода на github и демонстрации проекта на хостинге.
|
||||
|
||||
Критерии оценки: Оценка происходит по принципу зачет/незачет.
|
||||
|
||||
###Требования:
|
||||
- Есть возможность регистрации, создавать персональные страницы, возможность подружиться, список друзей.
|
||||
- Отсутствуют SQL-инъекции.
|
||||
- Пароль хранится безопасно.
|
|
@ -0,0 +1,2 @@
|
|||
CREATE DATABASE IF NOT EXISTS `app` CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||||
GRANT ALL ON `app`.* TO 'app'@'%' identified by 'app';
|
|
@ -1,18 +0,0 @@
|
|||
version: '3'
|
||||
services:
|
||||
|
||||
app:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: ./cicd/app/Dockerfile
|
||||
restart: always
|
||||
environment:
|
||||
APP_SERVER_ADDRESS: 0.0.0.0
|
||||
APP_SERVER_PORT: 8080
|
||||
APP_DSN_MASTER: mysql_master
|
||||
APP_DSN_PORT: 3306
|
||||
APP_DSN_USER: app
|
||||
APP_DSN_PASS: app
|
||||
APP_DSN_BASE: app
|
||||
ports:
|
||||
- "8080:8080"
|
|
@ -1,13 +0,0 @@
|
|||
version: '3'
|
||||
services:
|
||||
|
||||
mysql_client:
|
||||
image: mysql:5.7
|
||||
hostname: "mysql_client"
|
||||
container_name: mysql_client
|
||||
restart: always
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
MYSQL_PORT: 3306
|
||||
volumes:
|
||||
- ./../test/dz003/scripts:/scripts
|
|
@ -1,45 +0,0 @@
|
|||
version: '3'
|
||||
services:
|
||||
|
||||
mysql_master:
|
||||
image: mysql:5.7
|
||||
hostname: "mysql_master"
|
||||
container_name: mysql_master
|
||||
restart: always
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
MYSQL_PORT: 3306
|
||||
MYSQL_LOWER_CASE_TABLE_NAMES: 0
|
||||
volumes:
|
||||
- ./mysql/mysql_master.conf:/etc/mysql/conf.d/mysql.conf.cnf
|
||||
- /opt/mysql_master:/var/lib/mysql
|
||||
|
||||
mysql_slave1:
|
||||
image: mysql:5.7
|
||||
hostname: "mysql_slave1"
|
||||
container_name: mysql_slave1
|
||||
restart: always
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
MYSQL_PORT: 3306
|
||||
MYSQL_LOWER_CASE_TABLE_NAMES: 0
|
||||
depends_on:
|
||||
- mysql_master
|
||||
volumes:
|
||||
- ./mysql/mysql_slave1.conf:/etc/mysql/conf.d/mysql.conf.cnf
|
||||
- /opt/mysql_slave1:/var/lib/mysql
|
||||
|
||||
mysql_slave2:
|
||||
image: mysql:5.7
|
||||
hostname: "mysql_slave2"
|
||||
container_name: mysql_slave2
|
||||
restart: always
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
MYSQL_PORT: 3306
|
||||
MYSQL_LOWER_CASE_TABLE_NAMES: 0
|
||||
depends_on:
|
||||
- mysql_master
|
||||
volumes:
|
||||
- ./mysql/mysql_slave2.conf:/etc/mysql/conf.d/mysql.conf.cnf
|
||||
- /opt/mysql_slave2:/var/lib/mysql
|
|
@ -0,0 +1,52 @@
|
|||
version: '3'
|
||||
services:
|
||||
|
||||
mysql_master:
|
||||
image: actency/docker-mysql-replication:5.7
|
||||
hostname: "mysql_master"
|
||||
container_name: mysql_master
|
||||
restart: always
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
MYSQL_USER: app
|
||||
MYSQL_PASSWORD: app
|
||||
MYSQL_DATABASE: app
|
||||
REPLICATION_USER: replication_user
|
||||
REPLICATION_PASSWORD: myreplpassword
|
||||
volumes:
|
||||
- /opt/mysql_master:/var/lib/mysql
|
||||
- ./mysql/master_init:/docker-entrypoint-initdb.d
|
||||
|
||||
mysql_slave:
|
||||
image: actency/docker-mysql-replication:5.7
|
||||
hostname: "mysql_slave"
|
||||
container_name: mysql_slave
|
||||
restart: always
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
MYSQL_USER: app
|
||||
MYSQL_PASSWORD: app
|
||||
MYSQL_DATABASE: app
|
||||
REPLICATION_USER: replication_user
|
||||
REPLICATION_PASSWORD: myreplpassword
|
||||
MASTER_HOST: mysql_master
|
||||
depends_on:
|
||||
- mysql_master
|
||||
volumes:
|
||||
- /opt/mysql_slave:/var/lib/mysql
|
||||
|
||||
app:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: ./cicd/app/Dockerfile
|
||||
restart: always
|
||||
environment:
|
||||
APP_SERVER_ADDRESS: 0.0.0.0
|
||||
APP_SERVER_PORT: 8080
|
||||
APP_DSN_HOST: mysql_master
|
||||
APP_DSN_PORT: 13306
|
||||
APP_DSN_USER: app
|
||||
APP_DSN_PASS: app
|
||||
APP_DSN_BASE: app
|
||||
ports:
|
||||
- "8080:8080"
|
55
cicd/init.sh
55
cicd/init.sh
|
@ -1,55 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
until docker exec mysql_master sh -c 'export MYSQL_PWD=root; mysql -u root -e ";"'
|
||||
do
|
||||
echo "Waiting for mysql_master database connection..."
|
||||
sleep 4
|
||||
done
|
||||
|
||||
priv_stmt='INSTALL PLUGIN rpl_semi_sync_master SONAME "semisync_master.so"; CREATE DATABASE IF NOT EXISTS app CHARACTER SET utf8 COLLATE utf8_general_ci; GRANT ALL ON app.* TO "app"@"%" IDENTIFIED BY "app"; GRANT REPLICATION SLAVE ON *.* TO "mydb_slave_user"@"%" IDENTIFIED BY "mydb_slave_pwd"; FLUSH PRIVILEGES;'
|
||||
docker exec mysql_master sh -c "export MYSQL_PWD=root; mysql -u root -e '$priv_stmt'"
|
||||
|
||||
until docker exec mysql_slave1 sh -c 'export MYSQL_PWD=root; mysql -u root -e ";"'
|
||||
do
|
||||
echo "Waiting for mysql_slave1 database connection..."
|
||||
sleep 4
|
||||
done
|
||||
|
||||
until docker exec mysql_slave2 sh -c 'export MYSQL_PWD=root; mysql -u root -e ";"'
|
||||
do
|
||||
echo "Waiting for mysql_slave2 database connection..."
|
||||
sleep 4
|
||||
done
|
||||
|
||||
priv_stmt='INSTALL PLUGIN rpl_semi_sync_slave SONAME "semisync_slave.so"; CREATE DATABASE IF NOT EXISTS app CHARACTER SET utf8 COLLATE utf8_general_ci; GRANT ALL ON app.* TO "app"@"%" IDENTIFIED BY "app"; FLUSH PRIVILEGES;'
|
||||
docker exec mysql_slave1 sh -c "export MYSQL_PWD=root; mysql -u root -e '$priv_stmt'"
|
||||
docker exec mysql_slave2 sh -c "export MYSQL_PWD=root; mysql -u root -e '$priv_stmt'"
|
||||
|
||||
docker-ip() {
|
||||
docker inspect --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$@"
|
||||
}
|
||||
|
||||
MS_STATUS=`docker exec mysql_master sh -c 'export MYSQL_PWD=root; mysql -u root -e "SHOW MASTER STATUS"' | grep mysq`
|
||||
CURRENT_LOG=`echo $MS_STATUS | awk '{print $1}'`
|
||||
CURRENT_POS=`echo $MS_STATUS | awk '{print $2}'`
|
||||
|
||||
start_slave_stmt="CHANGE MASTER TO MASTER_HOST='$(docker-ip mysql_master)',MASTER_USER='mydb_slave_user',MASTER_PASSWORD='mydb_slave_pwd',MASTER_LOG_FILE='$CURRENT_LOG',MASTER_LOG_POS=$CURRENT_POS; START SLAVE;"
|
||||
start_slave_cmd='export MYSQL_PWD=root; mysql -u root -e "'
|
||||
start_slave_cmd+="$start_slave_stmt"
|
||||
start_slave_cmd+='"'
|
||||
|
||||
docker exec mysql_slave1 sh -c "$start_slave_cmd"
|
||||
echo "Checking slave1 status"
|
||||
docker exec mysql_slave1 sh -c "export MYSQL_PWD=root; mysql -u root -e 'SHOW SLAVE STATUS \G' | grep Slave_"
|
||||
echo "Checking slave1 GTID mode"
|
||||
sudo docker exec mysql_slave1 sh -c "export MYSQL_PWD=root; mysql -u root -e 'SHOW VARIABLES' | grep gtid"
|
||||
echo "Checking slave1 semisync"
|
||||
sudo docker exec mysql_slave1 sh -c "export MYSQL_PWD=root; mysql -u root -e 'SHOW VARIABLES' | grep semi_sync"
|
||||
|
||||
docker exec mysql_slave2 sh -c "$start_slave_cmd"
|
||||
echo "Checking slave2 status"
|
||||
docker exec mysql_slave2 sh -c "export MYSQL_PWD=root; mysql -u root -e 'SHOW SLAVE STATUS \G' | grep Slave_"
|
||||
echo "Checking slave2 GTID mode"
|
||||
sudo docker exec mysql_slave2 sh -c "export MYSQL_PWD=root; mysql -u root -e 'SHOW VARIABLES' | grep gtid"
|
||||
echo "Checking slave2 semisync"
|
||||
sudo docker exec mysql_slave2 sh -c "export MYSQL_PWD=root; mysql -u root -e 'SHOW VARIABLES' | grep semi_sync"
|
|
@ -1,17 +0,0 @@
|
|||
[mysqld]
|
||||
|
||||
skip-host-cache
|
||||
skip-name-resolve
|
||||
|
||||
server-id = 1
|
||||
log_bin = /var/log/mysql/mysql-bin.log
|
||||
binlog_do_db = app
|
||||
|
||||
binlog_format=ROW
|
||||
binlog-checksum=crc32
|
||||
|
||||
gtid-mode=on
|
||||
enforce-gtid-consistency=true
|
||||
|
||||
loose-rpl_semi_sync_master_enabled = 1
|
||||
loose-rpl_semi_sync_master_timeout=1000
|
|
@ -1,18 +0,0 @@
|
|||
[mysqld]
|
||||
|
||||
skip-host-cache
|
||||
skip-name-resolve
|
||||
|
||||
server-id = 2
|
||||
log_bin = /var/log/mysql/mysql-bin.log
|
||||
relay-log = /var/log/mysql/mysql-relay-bin.log
|
||||
binlog_do_db = app
|
||||
|
||||
binlog_format=ROW
|
||||
binlog-checksum=crc32
|
||||
|
||||
gtid-mode=on
|
||||
enforce-gtid-consistency=true
|
||||
binlog-rows-query-log_events=1
|
||||
|
||||
loose-rpl_semi_sync_slave_enabled=1
|
|
@ -1,18 +0,0 @@
|
|||
[mysqld]
|
||||
|
||||
skip-host-cache
|
||||
skip-name-resolve
|
||||
|
||||
server-id = 3
|
||||
log_bin = /var/log/mysql/mysql-bin.log
|
||||
relay-log = /var/log/mysql/mysql-relay-bin.log
|
||||
binlog_do_db = app
|
||||
|
||||
binlog_format=ROW
|
||||
binlog-checksum=crc32
|
||||
|
||||
gtid-mode=on
|
||||
enforce-gtid-consistency=true
|
||||
binlog-rows-query-log_events=1
|
||||
|
||||
loose-rpl_semi_sync_slave_enabled=1
|
23
cmd/main.go
23
cmd/main.go
|
@ -2,22 +2,20 @@ package main
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/codegangsta/martini"
|
||||
"github.com/codegangsta/martini-contrib/binding"
|
||||
"github.com/codegangsta/martini-contrib/render"
|
||||
"github.com/codegangsta/martini-contrib/sessions"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/application"
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/auth"
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/handlers"
|
||||
"github.com/tiburon-777/OTUS_HighLoad/pkg/dataset"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -27,11 +25,11 @@ func init() {
|
|||
func main() {
|
||||
log.Println("Starting...")
|
||||
m := martini.Classic()
|
||||
app, err := application.New("application.conf", "APP")
|
||||
app, err := application.New("", "APP")
|
||||
if err != nil {
|
||||
log.Fatal(fmt.Errorf("can't build app: %w", err).Error())
|
||||
}
|
||||
go dataset.FillDB(app.DBMaster, 1000000)
|
||||
go dataset.FillDB(app.DB, 1000000)
|
||||
|
||||
m.Map(log.New(os.Stdout, "[app]", log.Lshortfile))
|
||||
m.Map(app)
|
||||
|
@ -42,7 +40,7 @@ func main() {
|
|||
Extensions: []string{".tmpl"},
|
||||
}))
|
||||
|
||||
auth.RedirectURL = "/login"
|
||||
auth.RedirectUrl = "/login"
|
||||
auth.RedirectParam = "next"
|
||||
|
||||
m.Get("/404", func(r render.Render) {
|
||||
|
@ -74,11 +72,6 @@ func main() {
|
|||
m.Get("/search", handlers.GetUserList)
|
||||
m.Post("/search", handlers.PostUserSearch)
|
||||
|
||||
m.Get("/addPost", auth.LoginRequired, handlers.GetAddPost)
|
||||
m.Post("/addPost", auth.LoginRequired, handlers.PostAddPost)
|
||||
|
||||
m.Get("/feed", auth.LoginRequired, handlers.GetFeed)
|
||||
|
||||
m.NotFound(func(r render.Render) {
|
||||
r.HTML(404, "404", nil)
|
||||
})
|
||||
|
|
|
@ -3,47 +3,30 @@ package application
|
|||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/tiburon-777/modules/core/config"
|
||||
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/models"
|
||||
"github.com/tiburon-777/modules/core/config"
|
||||
"log"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
Config *models.Configuration
|
||||
DBMaster *sql.DB
|
||||
DBSlave1 *sql.DB
|
||||
DBSlave2 *sql.DB
|
||||
DB *sql.DB
|
||||
}
|
||||
|
||||
func New(configFile, envPrefix string) (app App, err error) {
|
||||
app.Config, err = configure(configFile, envPrefix)
|
||||
func New(configFile, envPrefix string) (App, error) {
|
||||
conf, err := configure(configFile, envPrefix)
|
||||
if err != nil {
|
||||
return App{}, fmt.Errorf("can't apply config: %w", err)
|
||||
return App{}, fmt.Errorf("can't apply config: %w\n", err)
|
||||
}
|
||||
|
||||
app.DBMaster, err = sql.Open("mysql", app.Config.DSN.User+":"+app.Config.DSN.Pass+"@tcp("+app.Config.DSN.Master+":"+app.Config.DSN.Port+")/"+app.Config.DSN.Base+"?charset=utf8&collation=utf8_unicode_ci")
|
||||
db, err := sql.Open("mysql", conf.DSN.User+":"+conf.DSN.Pass+"@tcp("+conf.DSN.Host+":"+conf.DSN.Port+")/"+conf.DSN.Base)
|
||||
if err != nil {
|
||||
return App{}, err
|
||||
}
|
||||
if err = dbInit(app.DBMaster); err != nil {
|
||||
if err = dbInit(db); err != nil {
|
||||
return App{}, err
|
||||
}
|
||||
if app.Config.DSN.Slave1 != "" {
|
||||
app.DBSlave1, err = sql.Open("mysql", app.Config.DSN.User+":"+app.Config.DSN.Pass+"@tcp("+app.Config.DSN.Slave1+":"+app.Config.DSN.Port+")/"+app.Config.DSN.Base+"?charset=utf8&collation=utf8_unicode_ci")
|
||||
if err != nil {
|
||||
return App{}, err
|
||||
}
|
||||
}
|
||||
|
||||
if app.Config.DSN.Slave2 != "" {
|
||||
app.DBSlave2, err = sql.Open("mysql", app.Config.DSN.User+":"+app.Config.DSN.Pass+"@tcp("+app.Config.DSN.Slave2+":"+app.Config.DSN.Port+")/"+app.Config.DSN.Base+"?charset=utf8&collation=utf8_unicode_ci")
|
||||
if err != nil {
|
||||
return App{}, err
|
||||
}
|
||||
}
|
||||
return app, nil
|
||||
return App{Config: conf, DB: db}, nil
|
||||
}
|
||||
|
||||
func configure(fileName string, envPrefix string) (*models.Configuration, error) {
|
||||
|
@ -77,7 +60,7 @@ func dbInit(db *sql.DB) error {
|
|||
City varchar(255) DEFAULT NULL,
|
||||
Interests varchar(255) DEFAULT NULL,
|
||||
PRIMARY KEY (Id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_unicode_ci`); err != nil {
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8`); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS relations (
|
||||
|
@ -86,18 +69,6 @@ func dbInit(db *sql.DB) error {
|
|||
) ENGINE=InnoDB DEFAULT CHARSET=utf8`); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS posts (
|
||||
Id INT(11) NOT NULL AUTO_INCREMENT,
|
||||
Author INT(11) NULL DEFAULT NULL,
|
||||
Created TIMESTAMP NULL DEFAULT NULL,
|
||||
Subject VARCHAR(50) NULL DEFAULT NULL,
|
||||
Body MEDIUMTEXT NULL DEFAULT NULL,
|
||||
PRIMARY KEY (Id) USING BTREE,
|
||||
INDEX AuthorID (author) USING BTREE,
|
||||
CONSTRAINT AuthorID FOREIGN KEY (author) REFERENCES app.users (Id) ON UPDATE RESTRICT ON DELETE RESTRICT
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_unicode_ci`); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Println("All tables exists")
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -2,21 +2,19 @@ package auth
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/codegangsta/martini"
|
||||
"github.com/codegangsta/martini-contrib/render"
|
||||
"github.com/codegangsta/martini-contrib/sessions"
|
||||
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/application"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// These are the default configuration values for this package. They
|
||||
// can be set at anytime, probably during the initial setup of Martini.
|
||||
var (
|
||||
// RedirectURL should be the relative URL for your login route
|
||||
RedirectURL string = "/login"
|
||||
// RedirectUrl should be the relative URL for your login route
|
||||
RedirectUrl string = "/login"
|
||||
|
||||
// RedirectParam is the query string parameter that will be set
|
||||
// with the page the user was trying to visit before they were
|
||||
|
@ -41,10 +39,10 @@ type User interface {
|
|||
Logout()
|
||||
|
||||
// Return the unique identifier of this user object
|
||||
UniqueID() interface{}
|
||||
UniqueId() interface{}
|
||||
|
||||
// Populate this user object with values
|
||||
GetByID(app application.App, id interface{}) error
|
||||
GetById(app application.App, id interface{}) error
|
||||
}
|
||||
|
||||
// SessionUser will try to read a unique user ID out of the session. Then it tries
|
||||
|
@ -55,11 +53,11 @@ type User interface {
|
|||
// user type.
|
||||
func SessionUser(newUser func() User) martini.Handler {
|
||||
return func(s sessions.Session, c martini.Context, l *log.Logger, app application.App) {
|
||||
userID := s.Get(SessionKey)
|
||||
userId := s.Get(SessionKey)
|
||||
user := newUser()
|
||||
|
||||
if userID != nil {
|
||||
err := user.GetByID(app, userID)
|
||||
if userId != nil {
|
||||
err := user.GetById(app, userId)
|
||||
if err != nil {
|
||||
l.Printf("Login Error: %v\n", err)
|
||||
} else {
|
||||
|
@ -90,8 +88,8 @@ func Logout(s sessions.Session, user User) {
|
|||
// authenticated, they will be redirected to /login with the "next" get parameter
|
||||
// set to the attempted URL.
|
||||
func LoginRequired(r render.Render, user User, req *http.Request) {
|
||||
if !user.IsAuthenticated() {
|
||||
path := fmt.Sprintf("%s?%s=%s", RedirectURL, RedirectParam, req.URL.Path)
|
||||
if user.IsAuthenticated() == false {
|
||||
path := fmt.Sprintf("%s?%s=%s", RedirectUrl, RedirectParam, req.URL.Path)
|
||||
r.Redirect(path, 302)
|
||||
}
|
||||
}
|
||||
|
@ -99,6 +97,6 @@ func LoginRequired(r render.Render, user User, req *http.Request) {
|
|||
// UpdateUser updates the User object stored in the session. This is useful incase a change
|
||||
// is made to the user model that needs to persist across requests.
|
||||
func UpdateUser(s sessions.Session, user User) error {
|
||||
s.Set(SessionKey, user.UniqueID())
|
||||
s.Set(SessionKey, user.UniqueId())
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -2,13 +2,12 @@ package auth
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/application"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UserModel struct {
|
||||
ID int64 `db:"id" form:"id"`
|
||||
Id int64 `db:"id" form:"id"`
|
||||
Username string `db:"username" form:"username"`
|
||||
Password string `db:"password" form:"password"`
|
||||
Name string `db:"name" form:"name"`
|
||||
|
@ -46,14 +45,14 @@ func (u *UserModel) IsAuthenticated() bool {
|
|||
return u.authenticated
|
||||
}
|
||||
|
||||
func (u *UserModel) UniqueID() interface{} {
|
||||
return u.ID
|
||||
func (u *UserModel) UniqueId() interface{} {
|
||||
return u.Id
|
||||
}
|
||||
|
||||
func (u *UserModel) GetByID(app application.App, id interface{}) error {
|
||||
func (u *UserModel) GetById(app application.App, id interface{}) error {
|
||||
var v string
|
||||
query := fmt.Sprintf("SELECT username, name, surname, birthdate, gender, city, interests FROM users WHERE id=%d", id)
|
||||
err := app.DBMaster.QueryRow(query).Scan(&u.Username, &u.Name, &u.Surname, &v, &u.Gender, &u.City, &u.Interests)
|
||||
err := app.DB.QueryRow(query).Scan(&u.Username, &u.Name, &u.Surname, &v, &u.Gender, &u.City, &u.Interests)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -61,6 +60,6 @@ func (u *UserModel) GetByID(app application.App, id interface{}) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.ID = id.(int64)
|
||||
u.Id = id.(int64)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/codegangsta/martini-contrib/render"
|
||||
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/application"
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/auth"
|
||||
)
|
||||
|
||||
func GetFeed(app application.App, r render.Render, user auth.User) {
|
||||
h := user.(*auth.UserModel).BirthDate
|
||||
user.(*auth.UserModel).YearsOld = int(time.Since(h).Hours() / 8760)
|
||||
doc := make(map[string]interface{})
|
||||
doc["user"] = user.(*auth.UserModel)
|
||||
var tmpTime string
|
||||
var post Post
|
||||
var posts []Post
|
||||
var results, err = app.DBMaster.Query(`SELECT
|
||||
posts.ID AS Id,
|
||||
users.Username AS Author,
|
||||
posts.Created AS Created,
|
||||
posts.Subject AS Subject,
|
||||
posts.Body AS Body
|
||||
FROM
|
||||
users JOIN relations JOIN posts
|
||||
WHERE
|
||||
relations.friendId=users.Id
|
||||
AND posts.Author=relations.friendId
|
||||
AND relations.userId=?
|
||||
ORDER by Created DESC`,
|
||||
user.(*auth.UserModel).ID)
|
||||
if err != nil || results == nil {
|
||||
err500("can't get feed from DB: ", err, r)
|
||||
}
|
||||
defer results.Close()
|
||||
for results.Next() {
|
||||
err = results.Scan(&post.ID, &post.Author, &tmpTime, &post.Subject, &post.Body)
|
||||
if err != nil {
|
||||
err500("can't scan result from DB: ", err, r)
|
||||
}
|
||||
post.Created = str2Time(tmpTime, r)
|
||||
posts = append(posts, post)
|
||||
}
|
||||
doc["posts"] = posts
|
||||
|
||||
r.HTML(200, "feed", doc)
|
||||
}
|
||||
|
|
@ -2,28 +2,18 @@ package handlers
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/codegangsta/martini-contrib/render"
|
||||
"github.com/codegangsta/martini-contrib/sessions"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
// MySQL driver
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/application"
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/auth"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Post struct{
|
||||
ID int `db:"Id"`
|
||||
Author string `db:"Author"`
|
||||
Created time.Time `db:"Created"`
|
||||
Subject string `db:"Subject"`
|
||||
Body string `db:"Body"`
|
||||
}
|
||||
|
||||
func GetHome(app application.App, r render.Render, user auth.User) {
|
||||
h := user.(*auth.UserModel).BirthDate
|
||||
user.(*auth.UserModel).YearsOld = int(time.Since(h).Hours() / 8760)
|
||||
|
@ -32,7 +22,7 @@ func GetHome(app application.App, r render.Render, user auth.User) {
|
|||
var users []auth.UserModel
|
||||
var tmp auth.UserModel
|
||||
var tmpTime string
|
||||
var results, err = app.DBMaster.Query(`SELECT
|
||||
var results, err = app.DB.Query(`SELECT
|
||||
users.id as id,
|
||||
users.name as name,
|
||||
users.surname as surname,
|
||||
|
@ -45,13 +35,13 @@ func GetHome(app application.App, r render.Render, user auth.User) {
|
|||
relations.friendId=users.Id
|
||||
AND relations.userId=?
|
||||
GROUP BY users.Id`,
|
||||
user.(*auth.UserModel).ID)
|
||||
user.(*auth.UserModel).Id)
|
||||
if err != nil || results == nil {
|
||||
err500("can't get user list from DB: ", err, r)
|
||||
}
|
||||
defer results.Close()
|
||||
for results.Next() {
|
||||
err = results.Scan(&tmp.ID, &tmp.Name, &tmp.Surname, &tmpTime, &tmp.Gender, &tmp.City)
|
||||
err = results.Scan(&tmp.Id, &tmp.Name, &tmp.Surname, &tmpTime, &tmp.Gender, &tmp.City)
|
||||
if err != nil {
|
||||
err500("can't scan result from DB: ", err, r)
|
||||
}
|
||||
|
@ -61,23 +51,6 @@ func GetHome(app application.App, r render.Render, user auth.User) {
|
|||
}
|
||||
doc["table"] = users
|
||||
|
||||
var post Post
|
||||
var posts []Post
|
||||
results, err = app.DBMaster.Query(`SELECT Id, Created, Subject, Body FROM posts WHERE Author=? ORDER BY Created DESC;`, user.(*auth.UserModel).ID)
|
||||
if err != nil || results == nil {
|
||||
err500("can't get user list from DB: ", err, r)
|
||||
}
|
||||
defer results.Close()
|
||||
for results.Next() {
|
||||
err = results.Scan(&post.ID, &tmpTime, &post.Subject, &post.Body)
|
||||
if err != nil {
|
||||
err500("can't scan result from DB: ", err, r)
|
||||
}
|
||||
post.Created = str2Time(tmpTime, r)
|
||||
posts = append(posts, post)
|
||||
}
|
||||
doc["posts"] = posts
|
||||
|
||||
r.HTML(200, "index", doc)
|
||||
}
|
||||
|
||||
|
@ -106,7 +79,7 @@ func PostSignup(app application.App, postedUser auth.UserModel, r render.Render)
|
|||
if err != nil {
|
||||
err500("can't generate password hash: ", err, r)
|
||||
}
|
||||
_, err = app.DBMaster.Exec(`INSERT INTO users (username, password, name, surname, birthdate, gender, city, interests)
|
||||
_, err = app.DB.Exec(`INSERT INTO users (username, password, name, surname, birthdate, gender, city, interests)
|
||||
values (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
postedUser.Username,
|
||||
pHash,
|
||||
|
@ -123,26 +96,182 @@ func PostSignup(app application.App, postedUser auth.UserModel, r render.Render)
|
|||
r.Redirect("/login")
|
||||
}
|
||||
|
||||
func GetUserList(app application.App, r render.Render) {
|
||||
doc := make(map[string]interface{})
|
||||
doc["UsersFound"] = 0
|
||||
var tmp int
|
||||
if err := app.DB.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&tmp); err != nil {
|
||||
err500("can't get total of user profiles from DB: ", err, r)
|
||||
}
|
||||
doc["UsersTotal"] = tmp
|
||||
r.HTML(200, "list", doc)
|
||||
}
|
||||
|
||||
func PostUserList(app application.App, user auth.User, r render.Render, req *http.Request) {
|
||||
postName := req.FormValue("name")
|
||||
postSurname := req.FormValue("surname")
|
||||
doc := make(map[string]interface{})
|
||||
doc["user"] = user.(*auth.UserModel)
|
||||
var users []auth.UserModel
|
||||
var tmp auth.UserModel
|
||||
var tmpTime string
|
||||
var results, err = app.DB.Query(`SELECT
|
||||
users.id as id,
|
||||
users.name as name,
|
||||
users.surname as surname,
|
||||
users.birthdate as birthdate,
|
||||
users.gender as gender,
|
||||
users.city as city
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
NOT users.id=?
|
||||
AND users.id NOT IN (
|
||||
SELECT
|
||||
relations.friendId
|
||||
FROM
|
||||
relations
|
||||
WHERE
|
||||
relations.userId=?)
|
||||
AND ( users.Name LIKE concat(?, '%') AND users.Surname LIKE concat(?, '%') )`,
|
||||
user.(*auth.UserModel).Id,
|
||||
user.(*auth.UserModel).Id,
|
||||
postName,
|
||||
postSurname,
|
||||
)
|
||||
if err != nil || results == nil {
|
||||
err500("can't get user list from DB: ", err, r)
|
||||
}
|
||||
defer results.Close()
|
||||
for results.Next() {
|
||||
err = results.Scan(&tmp.Id, &tmp.Name, &tmp.Surname, &tmpTime, &tmp.Gender, &tmp.City)
|
||||
if err != nil {
|
||||
err500("can't scan result from DB: ", err, r)
|
||||
}
|
||||
tmp.BirthDate = str2Time(tmpTime, r)
|
||||
tmp.YearsOld = int(time.Since(tmp.BirthDate).Hours() / 8760)
|
||||
users = append(users, tmp)
|
||||
if len(users) >= 100 {
|
||||
doc["msg"] = "( Too much rows in result. We will display only the first 100. )"
|
||||
break
|
||||
}
|
||||
}
|
||||
doc["table"] = users
|
||||
doc["UsersFound"] = len(users)
|
||||
var uTotal int
|
||||
if err := app.DB.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&uTotal); err != nil {
|
||||
err500("can't get total of user profiles from DB: ", err, r)
|
||||
}
|
||||
doc["UsersTotal"] = uTotal
|
||||
r.HTML(200, "list", doc)
|
||||
}
|
||||
|
||||
func PostUserSearch(app application.App, r render.Render, req *http.Request) {
|
||||
postName := req.FormValue("name")
|
||||
postSurname := req.FormValue("surname")
|
||||
doc := make(map[string]interface{})
|
||||
var users []auth.UserModel
|
||||
var tmp auth.UserModel
|
||||
var tmpTime string
|
||||
var results, err = app.DB.Query(`SELECT
|
||||
users.id as id,
|
||||
users.name as name,
|
||||
users.surname as surname,
|
||||
users.birthdate as birthdate,
|
||||
users.gender as gender,
|
||||
users.city as city
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
( users.Name LIKE concat(?, '%') AND users.Surname LIKE concat(?, '%') )`,
|
||||
postName,
|
||||
postSurname,
|
||||
)
|
||||
if err != nil || results == nil {
|
||||
err500("can't get user list from DB: ", err, r)
|
||||
}
|
||||
defer results.Close()
|
||||
for results.Next() {
|
||||
err = results.Scan(&tmp.Id, &tmp.Name, &tmp.Surname, &tmpTime, &tmp.Gender, &tmp.City)
|
||||
if err != nil {
|
||||
err500("can't scan result from DB: ", err, r)
|
||||
}
|
||||
tmp.BirthDate = str2Time(tmpTime, r)
|
||||
tmp.YearsOld = int(time.Since(tmp.BirthDate).Hours() / 8760)
|
||||
users = append(users, tmp)
|
||||
if len(users) >= 100 {
|
||||
doc["msg"] = "( Too much rows in result. We will display only the first 100. )"
|
||||
break
|
||||
}
|
||||
}
|
||||
doc["table"] = users
|
||||
doc["UsersFound"] = len(users)
|
||||
var uTotal int
|
||||
if err := app.DB.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&uTotal); err != nil {
|
||||
err500("can't get total of user profiles from DB: ", err, r)
|
||||
}
|
||||
doc["UsersTotal"] = uTotal
|
||||
r.HTML(200, "list", doc)
|
||||
}
|
||||
|
||||
func PostLogin(app application.App, session sessions.Session, postedUser auth.UserModel, r render.Render, req *http.Request) {
|
||||
user := auth.UserModel{}
|
||||
err1 := app.DBMaster.QueryRow("SELECT id, password FROM users WHERE username=?", postedUser.Username).Scan(&user.ID, &user.Password)
|
||||
err1 := app.DB.QueryRow("SELECT id, password FROM users WHERE username=?", postedUser.Username).Scan(&user.Id, &user.Password)
|
||||
err2 := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(postedUser.Password))
|
||||
if err1 != nil || err2 != nil {
|
||||
doc := map[string]interface{}{
|
||||
"msg": "Wrong user or password. You may sign in.",
|
||||
}
|
||||
r.HTML(200, "login", doc)
|
||||
r.Redirect(auth.RedirectURL)
|
||||
r.Redirect(auth.RedirectUrl)
|
||||
return
|
||||
} else {
|
||||
err := auth.AuthenticateSession(session, &user)
|
||||
if err != nil {
|
||||
err500("can't auth session: ", err, r)
|
||||
}
|
||||
params := req.URL.Query()
|
||||
redirect := params.Get(auth.RedirectParam)
|
||||
r.Redirect(redirect)
|
||||
return
|
||||
}
|
||||
err := auth.AuthenticateSession(session, &user)
|
||||
if err != nil {
|
||||
err500("can't auth session: ", err, r)
|
||||
}
|
||||
|
||||
func GetSubscribe(app application.App, r render.Render, user auth.User, req *http.Request) {
|
||||
sid, ok := req.URL.Query()["id"]
|
||||
if !ok {
|
||||
err500("can't parce URL query", nil, r)
|
||||
}
|
||||
params := req.URL.Query()
|
||||
redirect := params.Get(auth.RedirectParam)
|
||||
r.Redirect(redirect)
|
||||
return
|
||||
did, err := strconv.Atoi(sid[0])
|
||||
if err != nil {
|
||||
err500("can't convert URL query value: ", err, r)
|
||||
}
|
||||
_, err = app.DB.Exec(`REPLACE INTO relations (userId, friendId) values (?, ?)`, user.(*auth.UserModel).Id, did)
|
||||
if err != nil {
|
||||
err500("can't create relation in DB: ", err, r)
|
||||
}
|
||||
_, err = app.DB.Exec(`REPLACE INTO relations (userId, friendId) values (?, ?)`, did, user.(*auth.UserModel).Id)
|
||||
if err != nil {
|
||||
err500("can't create relation in DB: ", err, r)
|
||||
}
|
||||
r.Redirect("/list")
|
||||
}
|
||||
|
||||
func GetUnSubscribe(app application.App, r render.Render, user auth.User, req *http.Request) {
|
||||
sid, ok := req.URL.Query()["id"]
|
||||
if !ok {
|
||||
err500("can't parce URL query", nil, r)
|
||||
}
|
||||
did, err := strconv.Atoi(sid[0])
|
||||
if err != nil {
|
||||
err500("can't convert URL query value: ", err, r)
|
||||
}
|
||||
_, err = app.DB.Exec(`DELETE FROM relations WHERE (userId,friendId) IN ((?, ?),(?, ?))`, user.(*auth.UserModel).Id, did, did, user.(*auth.UserModel).Id)
|
||||
if err != nil {
|
||||
err500("can't remove relation from DB: ", err, r)
|
||||
}
|
||||
r.Redirect("/")
|
||||
|
||||
}
|
||||
|
||||
func str2Time(s string, r render.Render) time.Time {
|
||||
|
@ -154,7 +283,7 @@ func str2Time(s string, r render.Render) time.Time {
|
|||
}
|
||||
|
||||
func err500(s string, err error, r render.Render) {
|
||||
e := fmt.Errorf("%s %w", s, err)
|
||||
e := fmt.Errorf("s% %w", s, err)
|
||||
log.Println(e)
|
||||
doc := map[string]interface{}{
|
||||
"Error": e,
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/codegangsta/martini-contrib/render"
|
||||
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/application"
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/auth"
|
||||
)
|
||||
|
||||
func GetAddPost(app application.App, r render.Render) {
|
||||
r.HTML(200, "postadd", nil)
|
||||
}
|
||||
|
||||
func PostAddPost(app application.App, user auth.User, r render.Render, req *http.Request) {
|
||||
postSubj := req.FormValue("subj")
|
||||
postBody := req.FormValue("body")
|
||||
var results, err = app.DBMaster.Query(`INSERT INTO posts (Author, Created, Subject, Body) VALUES (
|
||||
?, ?, ?, ?)`,
|
||||
user.(*auth.UserModel).ID,
|
||||
time.Now().Format("2006-01-02 15:04:05"),
|
||||
postSubj,
|
||||
postBody,
|
||||
)
|
||||
if err != nil || results == nil {
|
||||
err500("can't add new post: ", err, r)
|
||||
}
|
||||
r.Redirect("/",302)
|
||||
}
|
|
@ -1,135 +0,0 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/codegangsta/martini-contrib/render"
|
||||
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/application"
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/auth"
|
||||
)
|
||||
|
||||
func GetUserList(app application.App, r render.Render) {
|
||||
doc := make(map[string]interface{})
|
||||
doc["UsersFound"] = 0
|
||||
var tmp int
|
||||
if err := app.DBMaster.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&tmp); err != nil {
|
||||
err500("can't get total of user profiles from DB: ", err, r)
|
||||
}
|
||||
doc["UsersTotal"] = tmp
|
||||
r.HTML(200, "list", doc)
|
||||
}
|
||||
|
||||
func PostUserList(app application.App, user auth.User, r render.Render, req *http.Request) {
|
||||
postName := req.FormValue("name")
|
||||
postSurname := req.FormValue("surname")
|
||||
doc := make(map[string]interface{})
|
||||
doc["user"] = user.(*auth.UserModel)
|
||||
var users []auth.UserModel
|
||||
var tmp auth.UserModel
|
||||
var tmpTime string
|
||||
var results, err = app.DBMaster.Query(`SELECT
|
||||
users.id as id,
|
||||
users.name as name,
|
||||
users.surname as surname,
|
||||
users.birthdate as birthdate,
|
||||
users.gender as gender,
|
||||
users.city as city
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
NOT users.id=?
|
||||
AND users.id NOT IN (
|
||||
SELECT
|
||||
relations.friendId
|
||||
FROM
|
||||
relations
|
||||
WHERE
|
||||
relations.userId=?)
|
||||
AND ( users.Name LIKE concat(?, '%') AND users.Surname LIKE concat(?, '%') )`,
|
||||
user.(*auth.UserModel).ID,
|
||||
user.(*auth.UserModel).ID,
|
||||
postName,
|
||||
postSurname,
|
||||
)
|
||||
if err != nil || results == nil {
|
||||
err500("can't get user list from DB: ", err, r)
|
||||
}
|
||||
defer results.Close()
|
||||
for results.Next() {
|
||||
err = results.Scan(&tmp.ID, &tmp.Name, &tmp.Surname, &tmpTime, &tmp.Gender, &tmp.City)
|
||||
if err != nil {
|
||||
err500("can't scan result from DB: ", err, r)
|
||||
}
|
||||
tmp.BirthDate = str2Time(tmpTime, r)
|
||||
tmp.YearsOld = int(time.Since(tmp.BirthDate).Hours() / 8760)
|
||||
users = append(users, tmp)
|
||||
if len(users) >= 100 {
|
||||
doc["msg"] = "( Too much rows in result. We will display only the first 100. )"
|
||||
break
|
||||
}
|
||||
}
|
||||
doc["table"] = users
|
||||
doc["UsersFound"] = len(users)
|
||||
var uTotal int
|
||||
if err := app.DBMaster.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&uTotal); err != nil {
|
||||
err500("can't get total of user profiles from DB: ", err, r)
|
||||
}
|
||||
doc["UsersTotal"] = uTotal
|
||||
r.HTML(200, "list", doc)
|
||||
}
|
||||
|
||||
func PostUserSearch(app application.App, r render.Render, req *http.Request) {
|
||||
db := app.DBMaster
|
||||
if app.Config.DSN.Slave1!="" {
|
||||
db = app.DBSlave1
|
||||
}
|
||||
|
||||
postName := req.FormValue("name")
|
||||
postSurname := req.FormValue("surname")
|
||||
doc := make(map[string]interface{})
|
||||
var users []auth.UserModel
|
||||
var tmp auth.UserModel
|
||||
var tmpTime string
|
||||
var results, err = db.Query(`SELECT
|
||||
users.id as id,
|
||||
users.name as name,
|
||||
users.surname as surname,
|
||||
users.birthdate as birthdate,
|
||||
users.gender as gender,
|
||||
users.city as city
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
( users.Name LIKE concat(?, '%') AND users.Surname LIKE concat(?, '%') )`,
|
||||
postName,
|
||||
postSurname,
|
||||
)
|
||||
if err != nil || results == nil {
|
||||
err500("can't get user list from DB: ", err, r)
|
||||
}
|
||||
defer results.Close()
|
||||
for results.Next() {
|
||||
err = results.Scan(&tmp.ID, &tmp.Name, &tmp.Surname, &tmpTime, &tmp.Gender, &tmp.City)
|
||||
if err != nil {
|
||||
err500("can't scan result from DB: ", err, r)
|
||||
}
|
||||
tmp.BirthDate = str2Time(tmpTime, r)
|
||||
tmp.YearsOld = int(time.Since(tmp.BirthDate).Hours() / 8760)
|
||||
users = append(users, tmp)
|
||||
if len(users) >= 100 {
|
||||
doc["msg"] = "( Too much rows in result. We will display only the first 100. )"
|
||||
break
|
||||
}
|
||||
}
|
||||
doc["table"] = users
|
||||
doc["UsersFound"] = len(users)
|
||||
var uTotal int
|
||||
if err := db.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&uTotal); err != nil {
|
||||
err500("can't get total of user profiles from DB: ", err, r)
|
||||
}
|
||||
doc["UsersTotal"] = uTotal
|
||||
r.HTML(200, "list", doc)
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/codegangsta/martini-contrib/render"
|
||||
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/application"
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/auth"
|
||||
)
|
||||
|
||||
func GetSubscribe(app application.App, r render.Render, user auth.User, req *http.Request) {
|
||||
sid, ok := req.URL.Query()["id"]
|
||||
if !ok {
|
||||
err500("can't parce URL query", nil, r)
|
||||
}
|
||||
did, err := strconv.Atoi(sid[0])
|
||||
if err != nil {
|
||||
err500("can't convert URL query value: ", err, r)
|
||||
}
|
||||
_, err = app.DBMaster.Exec(`REPLACE INTO relations (userId, friendId) values (?, ?)`, user.(*auth.UserModel).ID, did)
|
||||
if err != nil {
|
||||
err500("can't create relation in DB: ", err, r)
|
||||
}
|
||||
_, err = app.DBMaster.Exec(`REPLACE INTO relations (userId, friendId) values (?, ?)`, did, user.(*auth.UserModel).ID)
|
||||
if err != nil {
|
||||
err500("can't create relation in DB: ", err, r)
|
||||
}
|
||||
r.Redirect("/list")
|
||||
}
|
||||
|
||||
func GetUnSubscribe(app application.App, r render.Render, user auth.User, req *http.Request) {
|
||||
sid, ok := req.URL.Query()["id"]
|
||||
if !ok {
|
||||
err500("can't parce URL query", nil, r)
|
||||
}
|
||||
did, err := strconv.Atoi(sid[0])
|
||||
if err != nil {
|
||||
err500("can't convert URL query value: ", err, r)
|
||||
}
|
||||
_, err = app.DBMaster.Exec(`DELETE FROM relations WHERE (userId,friendId) IN ((?, ?),(?, ?))`, user.(*auth.UserModel).ID, did, did, user.(*auth.UserModel).ID)
|
||||
if err != nil {
|
||||
err500("can't remove relation from DB: ", err, r)
|
||||
}
|
||||
r.Redirect("/")
|
||||
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
package models
|
||||
|
||||
type Configuration struct {
|
||||
Server Server
|
||||
DSN DSN
|
||||
Server Server
|
||||
DSN DSN
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
|
@ -11,11 +11,9 @@ type Server struct {
|
|||
}
|
||||
|
||||
type DSN struct {
|
||||
Master string
|
||||
Slave1 string
|
||||
Slave2 string
|
||||
Port string
|
||||
User string
|
||||
Pass string
|
||||
Base string
|
||||
Host string
|
||||
Port string
|
||||
User string
|
||||
Pass string
|
||||
Base string
|
||||
}
|
||||
|
|
|
@ -2,13 +2,12 @@ package dataset
|
|||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/mdigger/translit"
|
||||
"log"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mdigger/translit"
|
||||
)
|
||||
|
||||
type Person struct {
|
||||
|
@ -31,13 +30,9 @@ func NewPerson() (p Person) {
|
|||
p.FirstName = womanNames[rand.Intn(len(womanNames))]
|
||||
p.SecondName = secondNames[rand.Intn(len(secondNames))] + "а"
|
||||
}
|
||||
charSet := "abcdedfghijklmnopqrstABCDEFGHIJKLMNOP0123456789"
|
||||
var output strings.Builder
|
||||
for i := 0; i < 16; i++ {
|
||||
random := rand.Intn(len(charSet))
|
||||
output.WriteString(string(charSet[random]))
|
||||
}
|
||||
p.Password = output.String()
|
||||
t := make([]byte, 16)
|
||||
rand.Read(t)
|
||||
p.Password = string(t)
|
||||
p.City = cities[rand.Intn(len(cities))]
|
||||
for i := 0; i < (rand.Intn(4) + 3); i++ {
|
||||
p.Interests = append(p.Interests, interests[rand.Intn(len(interests))])
|
||||
|
@ -54,7 +49,7 @@ func FillDB(db *sql.DB, lim int) {
|
|||
}
|
||||
uCount = lim - uCount
|
||||
if uCount <= 0 {
|
||||
log.Printf("Ok. We have more users then %d.", lim)
|
||||
log.Printf("Ok. We have more users then %s.", lim)
|
||||
}
|
||||
log.Printf("Try to generate %d rows and fill the DB...", uCount)
|
||||
for i := 1; i < uCount; i++ {
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border: 1px solid black;
|
||||
width: 100%;
|
||||
background-color: azure;
|
||||
}
|
||||
table th {
|
||||
border: 1px solid black;
|
||||
background-color: aquamarine;
|
||||
}
|
||||
table td {
|
||||
border: 1px solid black;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Feed of your friend's news</h2>
|
||||
{{ range $post:=.posts }}
|
||||
<a><b>{{ $post.Author }} wrote at {{ $post.Created }}:</b></a><br/>
|
||||
<a><b>{{ $post.Subject }}</b><br/><a>{{ $post.Body }}</a>
|
||||
<br/><br/>
|
||||
{{ end }}
|
||||
|
||||
<h2>Available actions:</h2>
|
||||
<input type="button" onclick="location.href='/';" value="Home" />
|
||||
</body>
|
||||
</html>
|
|
@ -25,12 +25,6 @@
|
|||
<p>You now live in <b>{{ .user.City }}</b></p>
|
||||
<p>You interests is: <b>{{ .user.Interests }}</b></p>
|
||||
|
||||
<h2>Available actions:</h2>
|
||||
<input type="button" onclick="location.href='/addPost';" value="Create Post" />
|
||||
<input type="button" onclick="location.href='/feed';" value="Friend Feed" />
|
||||
<input type="button" onclick="location.href='/list';" value="User list" />
|
||||
<input type="button" onclick="location.href='/logout';" value="Logout" /><br />
|
||||
|
||||
<h2> You have friends:</h2>
|
||||
<table>
|
||||
<thead>
|
||||
|
@ -55,11 +49,8 @@
|
|||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2> Your posts:</h2>
|
||||
{{ range $post:=.posts }}
|
||||
<a><b>{{ $post.Subject }}</b><br/><a>{{ $post.Body }}</a>
|
||||
<br/><br/>
|
||||
{{ end }}
|
||||
|
||||
<h2>Available actions:</h2>
|
||||
<input type="button" onclick="location.href='/list';" value="User list" />
|
||||
<input type="button" onclick="location.href='/logout';" value="Logout" /><br />
|
||||
</body>
|
||||
</html>
|
|
@ -1,23 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.3/moment.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Create post</h2>
|
||||
<p style="color: red;"><b>{{ .msg }}</b></p>
|
||||
<form method="POST">
|
||||
<table>
|
||||
<tr>
|
||||
<td>Тема</td>
|
||||
<td><input type="text" name="subj" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Текст</td>
|
||||
<td><textarea rows="10" cols="45" name="body"></textarea></td>
|
||||
</tr>
|
||||
</table>
|
||||
<button>Отправить</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
|
@ -1,45 +0,0 @@
|
|||
#ЗАЧТЕНА: [ЧАТ](https://otus.ru/learning/61597/#/homework-chat/12337/) / [РАБОЧИЙ СЕРВИС](http://lab.tiburon.su:8080/)
|
||||
|
||||
-----
|
||||
|
||||
# Заготовка для социальной сети
|
||||
Цель: В результате выполнения ДЗ вы создадите базовый скелет социальной сети, который будет развиваться в дальнейших ДЗ.
|
||||
|
||||
###В данном задании тренируются навыки:
|
||||
- декомпозиции предметной области;
|
||||
- построения элементарной архитектуры проекта
|
||||
Требуется разработать создание и просмотр анект в социальной сети.
|
||||
|
||||
###Функциональные требования:
|
||||
- Авторизация по паролю.
|
||||
- Страница регистрации, где указывается следующая информация:
|
||||
- Имя
|
||||
- Фамилия
|
||||
- Возраст
|
||||
- Пол
|
||||
- Интересы
|
||||
- Город
|
||||
- Страницы с анкетой.
|
||||
|
||||
###Нефункциональные требования:
|
||||
- Любой язык программирования
|
||||
- В качестве базы данных использовать MySQL
|
||||
- Не использовать ORM
|
||||
- Программа должна представлять из себя монолитное приложение.
|
||||
- Не рекомендуется использовать следующие технологии:
|
||||
- Репликация
|
||||
- Шардинг
|
||||
- Индексы
|
||||
- Кэширование
|
||||
|
||||
Верстка не важна. Подойдет самая примитивная.
|
||||
Разместить приложение на любом хостинге. Например, heroku.
|
||||
|
||||
ДЗ принимается в виде исходного кода на github и демонстрации проекта на хостинге.
|
||||
|
||||
Критерии оценки: Оценка происходит по принципу зачет/незачет.
|
||||
|
||||
###Требования:
|
||||
- Есть возможность регистрации, создавать персональные страницы, возможность подружиться, список друзей.
|
||||
- Отсутствуют SQL-инъекции.
|
||||
- Пароль хранится безопасно.
|
|
@ -0,0 +1,13 @@
|
|||
#/bin/sh
|
||||
|
||||
docker run --rm -v /root/scripts:/scripts williamyeh/wrk -t1 -c1 -d1m --timeout 30s http://lab.tiburon.su:8080/search -s /scripts/post.lua -- debug true
|
||||
|
||||
docker run --rm -v /root/scripts:/scripts williamyeh/wrk -t1 -c10 -d10m --timeout 30s http://lab.tiburon.su:8080/search -s /scripts/post.lua -- debug true
|
||||
|
||||
docker run --rm -v /root/scripts:/scripts williamyeh/wrk -t1 -c20 -d10m --timeout 30s http://lab.tiburon.su:8080/search -s /scripts/post.lua -- debug true
|
||||
|
||||
docker run --rm -v /root/scripts:/scripts williamyeh/wrk -t1 -c30 -d10m --timeout 30s http://lab.tiburon.su:8080/search -s /scripts/post.lua -- debug true
|
||||
|
||||
docker run --rm -v /root/scripts:/scripts williamyeh/wrk -t1 -c40 -d10m --timeout 30s http://lab.tiburon.su:8080/search -s /scripts/post.lua -- debug true
|
||||
|
||||
docker run --rm -v /root/scripts:/scripts williamyeh/wrk -t1 -c50 -d10m --timeout 30s http://lab.tiburon.su:8080/search -s /scripts/post.lua -- debug true
|
|
@ -1,35 +0,0 @@
|
|||
#ЗАЧТЕНА: [ЧАТ](https://otus.ru/learning/61597/#/homework-chat/12338/) / [ОТЧЕТ](REPORT.md)
|
||||
|
||||
-----
|
||||
|
||||
# Производительность индексов
|
||||
Цель: В результате выполнения ДЗ вы создадите набор тестовых данных для проведения нагрузочного тестирования, подберете наиболее подходящие индексы и проведете тесты производительности.
|
||||
|
||||
### В данном задании тренируются навыки:
|
||||
- генерация тестовых данных;
|
||||
- работа с индексами;
|
||||
- нагрузочное тестирование;
|
||||
|
||||
### План выполнения:
|
||||
1) Сгенерировать любым способ 1,000,000 анкет. Имена и Фамилии должны быть реальными (чтобы учитывать селективность индекса)
|
||||
2) Реализовать функционал поиска анкет по префиксу имени и фамилии (одновременно) в вашей социальной сети (запрос в форме firstName LIKE ? and secondName LIKE ?). Сортировать вывод по id анкеты. Использовать InnoDB движок.
|
||||
3) С помощью wrk провести нагрузочные тесты по этой странице. Поиграть с количеством одновременных запросов. 1/10/100/1000.
|
||||
4) Построить графики и сохранить их в отчет
|
||||
5) Сделать подходящий индекс.
|
||||
6) Повторить пункт 3 и 4.
|
||||
7) В качестве результата предоставить отчет в котором должны быть:
|
||||
- графики latency до индекса;
|
||||
- графики throughput до индекса;
|
||||
- графики latency после индекса;
|
||||
- графики throughput после индекса;
|
||||
- запрос добавления индекса;
|
||||
- explain запросов после индекса;
|
||||
- объяснение почему индекс именно такой;
|
||||
|
||||
ДЗ принимается в виде отчета по выполненной работе.
|
||||
Критерии оценки: Оценка происходит по принципу зачет/незачет.
|
||||
|
||||
###Требования:
|
||||
Правильно выбраны индексы.
|
||||
Нагрузочное тестирование проведено и результаты адекватны.
|
||||
Рекомендуем сдать до: 01.02.2021
|
|
@ -1,44 +0,0 @@
|
|||
### 1. Графики Latency и Kb/s от кол-ва одновременных запросов, до и после установки индексов.
|
||||

|
||||
### 2. В случае использования индекса indNameSurname:
|
||||
- #### Запрос на добавление индексов:
|
||||
```
|
||||
ALTER TABLE `users` ADD INDEX `indNameSurname` (`Name`, `Surname`);
|
||||
```
|
||||
- #### Explain запроса:
|
||||
```
|
||||
EXPLAIN SELECT
|
||||
users.id as id,
|
||||
users.name as name,
|
||||
users.surname as surname,
|
||||
users.birthdate as birthdate,
|
||||
users.gender as gender,
|
||||
users.city as city
|
||||
FROM
|
||||
users
|
||||
WHERE users.Name LIKE "ан%" AND users.Surname LIKE "ан%"
|
||||
ORDER BY id
|
||||
```
|
||||

|
||||
### 3. В случае использования индекса indSurnameName:
|
||||
- #### Запрос на добавление индексов:
|
||||
```
|
||||
ALTER TABLE `users` ADD INDEX `indSurnameName` (`Surname`, `Name`);
|
||||
```
|
||||
- #### Explain запроса:
|
||||
```
|
||||
EXPLAIN SELECT
|
||||
users.id as id,
|
||||
users.name as name,
|
||||
users.surname as surname,
|
||||
users.birthdate as birthdate,
|
||||
users.gender as gender,
|
||||
users.city as city
|
||||
FROM
|
||||
users
|
||||
WHERE users.Name LIKE "ан%" AND users.Surname LIKE "ан%"
|
||||
ORDER BY id
|
||||
```
|
||||

|
||||
#### 4. Резюме
|
||||
Для конечной оптимизации используется составной индекс `('Surname', 'Name')` потому, что в запросе используется объединение AND условий LIKE ?%. Mysql в этом случае ищет по составному индексу и объединяет строки без сортировки. Именно поэтому, приходится принудительно сортировать результаты. Порядок полей выбран с точки зрения селективности. В реальной обстановке поле «Фамилия» все же более селективно чем «Имя». Плюс к этому, мы видим в EXPLAIN запроса, что в случае использования индекса `('Surname', 'Name')` mysql применяет Multi-Range Read оптимизацию, позволяющую линеаризовать процедуру чтения с диска. Возможно за этот счет значительно повысилась скорость передачи данных? по сравнению с индексом `('Name', 'Surname')`.
|
|
@ -1,13 +0,0 @@
|
|||
#/bin/sh
|
||||
|
||||
docker run --rm -v dz002:/scripts williamyeh/wrk -t1 -c1 -d1m --timeout 30s http://lab.tiburon.su:8080/search -s /scripts/post.lua -- debug true
|
||||
|
||||
docker run --rm -v dz002:/scripts williamyeh/wrk -t1 -c10 -d10m --timeout 30s http://lab.tiburon.su:8080/search -s /scripts/post.lua -- debug true
|
||||
|
||||
docker run --rm -v dz002:/scripts williamyeh/wrk -t1 -c20 -d10m --timeout 30s http://lab.tiburon.su:8080/search -s /scripts/post.lua -- debug true
|
||||
|
||||
docker run --rm -v dz002:/scripts williamyeh/wrk -t1 -c30 -d10m --timeout 30s http://lab.tiburon.su:8080/search -s /scripts/post.lua -- debug true
|
||||
|
||||
docker run --rm -v dz002:/scripts williamyeh/wrk -t1 -c40 -d10m --timeout 30s http://lab.tiburon.su:8080/search -s /scripts/post.lua -- debug true
|
||||
|
||||
docker run --rm -v dz002:/scripts williamyeh/wrk -t1 -c50 -d10m --timeout 30s http://lab.tiburon.su:8080/search -s /scripts/post.lua -- debug true
|
Binary file not shown.
Before Width: | Height: | Size: 56 KiB |
Binary file not shown.
Before Width: | Height: | Size: 20 KiB |
Binary file not shown.
Before Width: | Height: | Size: 20 KiB |
|
@ -1,5 +0,0 @@
|
|||
wrk.method = "POST"
|
||||
wrk.body = "name=ан&surname=ан"
|
||||
wrk.headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
response = function(status, headers, body)
|
||||
end
|
|
@ -1,38 +0,0 @@
|
|||
#ЗАЧТЕНА: [ЧАТ](https://otus.ru/learning/61597/#/homework-chat/12339/) / [ОТЧЕТ](REPORT.md)
|
||||
|
||||
-----
|
||||
|
||||
#Полусинхронная репликация
|
||||
|
||||
В результате выполнения ДЗ вы настроите полусинхронную репликацию, протестируете ее влияние на производительность системы и убедитесь, что теперь вы не теряете транзакции в случае аварии.
|
||||
|
||||
### В данном задании тренируются навыки:
|
||||
- обеспечение отказоустойчивости проекта;
|
||||
- администрирование MySQL;
|
||||
- настройка репликации;
|
||||
- проведение нагрузочных тестов.
|
||||
|
||||
### План выполнения ДЗ:
|
||||
1) Настраиваем асинхронную репликацию.
|
||||
2) Выбираем 2 любых запроса на чтения (в идеале самых частых и тяжелых по логике работы сайта) и переносим их на чтение со слейва.
|
||||
3) Делаем нагрузочный тест по странице, которую перевели на слейв до и после репликации. Замеряем нагрузку мастера (CPU, la, disc usage, memory usage).
|
||||
4) ОПЦИОНАЛЬНО: в качестве конфига, который хранит IP реплики сделать массив для легкого добавления реплики. Это не самый правильный способ балансирования нагрузки. Поэтому опционально.
|
||||
5) Настроить 2 слейва и 1 мастер.
|
||||
6) Включить row-based репликацию.
|
||||
7) Включить GTID.
|
||||
8) Настроить полусинхронную репликацию.
|
||||
9) Создать нагрузку на запись в любую тестовую таблицу. На стороне, которой нагружаем считать, сколько строк мы успешно записали.
|
||||
10) С помощью kill -9 убиваем мастер MySQL.
|
||||
11) Заканчиваем нагрузку на запись.
|
||||
12) Выбираем самый свежий слейв. Промоутим его до мастера. Переключаем на него второй слейв.
|
||||
13) Проверяем, есть ли потери транзакций.
|
||||
|
||||
Результатом сдачи ДЗ будет в виде исходного кода на github и [отчета в текстовом виде](REPORT.md), где вы отразите как вы выполняли каждый из пунктов.
|
||||
Критерии оценки: Оценка происходит по принципу зачет/незачет.
|
||||
|
||||
### Требования:
|
||||
- В отчете корректно описано, как настроена репликация.
|
||||
- 2 запроса переведено на чтение со слейва.
|
||||
- Нагрузочное тестирование показало, что нагрузка перешла на слейв.
|
||||
- В отчете описано как включить row-based репликацию и GTID
|
||||
- Проведен эксперимент по потере и непотере транзакций при аварийной остановке master.
|
|
@ -1,226 +0,0 @@
|
|||
### 1. Настраиваем асинхронную репликацию.
|
||||
Я решил сделать сразу три хоста - один мастер и два слейва, чтобы к этому не возвращаться в 5-м пункте.
|
||||
- #### [my.cnf мастера](../../cicd/mysql/mysql_master.conf):
|
||||
```
|
||||
[mysqld]
|
||||
skip-host-cache
|
||||
skip-name-resolve
|
||||
server-id = 1
|
||||
log_bin = /var/log/mysql/mysql-bin.log
|
||||
binlog_do_db = app
|
||||
```
|
||||
- #### [my.cnf первого слэйва](../../cicd/mysql/mysql_slave1.conf):
|
||||
```
|
||||
[mysqld]
|
||||
skip-host-cache
|
||||
skip-name-resolve
|
||||
server-id = 2
|
||||
log_bin = /var/log/mysql/mysql-bin.log
|
||||
relay-log = /var/log/mysql/mysql-relay-bin.log
|
||||
binlog_do_db = app
|
||||
```
|
||||
- #### [my.cnf первого слэйва](../../cicd/mysql/mysql_slave2.conf):
|
||||
```
|
||||
[mysqld]
|
||||
skip-host-cache
|
||||
skip-name-resolve
|
||||
server-id = 3
|
||||
log_bin = /var/log/mysql/mysql-bin.log
|
||||
relay-log = /var/log/mysql/mysql-relay-bin.log
|
||||
binlog_do_db = app
|
||||
```
|
||||
- #### [Инициализация кластера](../../cicd/init.sh):
|
||||
- на мастере создаем базу, пользователя для работы приложения и пользователя для репликации:
|
||||
```
|
||||
CREATE DATABASE IF NOT EXISTS app CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||||
GRANT ALL ON app.* TO "app"@"%" IDENTIFIED BY "app";
|
||||
GRANT REPLICATION SLAVE ON *.* TO "mydb_slave_user"@"%" IDENTIFIED BY "mydb_slave_pwd";
|
||||
FLUSH PRIVILEGES;
|
||||
```
|
||||
- на обоих слэйвах создаем базу и пользователя для работы приложения:
|
||||
```
|
||||
CREATE DATABASE IF NOT EXISTS app CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||||
GRANT ALL ON app.* TO "app"@"%" IDENTIFIED BY "app"; FLUSH PRIVILEGES;
|
||||
```
|
||||
- определяем текущий IP адрес мастера:
|
||||
```
|
||||
docker inspect --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "mysql_master"
|
||||
```
|
||||
- определяем текущий лог файл мастера и позицию в нем:
|
||||
```
|
||||
mysql -u root -e "SHOW MASTER STATUS"' | grep mysq | awk '{print $1}'
|
||||
mysql -u root -e "SHOW MASTER STATUS"' | grep mysq | awk '{print $2}'
|
||||
```
|
||||
- на обоих слейвах назначаем мастера и запускаем репликацию:
|
||||
```
|
||||
CHANGE MASTER TO MASTER_HOST={IP_мастера},MASTER_USER='mydb_slave_user',MASTER_PASSWORD='mydb_slave_pwd',MASTER_LOG_FILE={текущий_лог},MASTER_LOG_POS={текущая_позиция};
|
||||
START SLAVE;
|
||||
```
|
||||
### 2. Выбираем 2 любых запроса на чтения (в идеале самых частых и тяжелых по логике работы сайта) и переносим их на чтение со слейва.
|
||||
Я решил поэкспериментировать с теми же поисковыми запросами, над которыми мы экспериментировали в [ДЗ002](../dz002/REPORT.md). Тем более, что все инструменты для этого уже есть в наличии (wrk).
|
||||
- #### Добавил в [структуру конфига](../../internal/models/config.go) адреса мастера и двух слейвов:
|
||||
```
|
||||
type DSN struct {
|
||||
Master string
|
||||
Slave1 string
|
||||
Slave2 string
|
||||
Port string
|
||||
User string
|
||||
Pass string
|
||||
Base string
|
||||
}
|
||||
```
|
||||
- #### В [хэндлере страницы app:port/search](../../internal/handlers/handlers.go) создал селектор, который направляет запросы чтения на первый слейв, если он присутствует в конфигурации:
|
||||
```
|
||||
db := app.DBMaster
|
||||
if app.Config.DSN.Slave1!="" {
|
||||
db = app.DBSlave1
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Делаем нагрузочный тест по странице, которую перевели на слейв до и после репликации. Замеряем нагрузку мастера (CPU, la, disc usage, memory usage).
|
||||
- #### Разворачиваем prom&grafana в docker настраиваем dashboard grafana на docker контейнеры
|
||||
```
|
||||
sudo make prom-up
|
||||
```
|
||||
- #### Запускаем приложение с подключением только к мастеру и с помощью wrk и [lua скрипта](scripts/post.lua) нагружаем страницу app:port/search:
|
||||
```
|
||||
sudo make app-up
|
||||
sudo docker run --rm -v /root/scripts:/scripts williamyeh/wrk -t1 -c10 -d5m --timeout 30s http://localhost:8080/search -s /scripts/post.lua -- debug true
|
||||
```
|
||||
- #### Идем в [графану](http://localhost:3001/) и наблюдаем нагрузку на мастер.
|
||||
- #### Ждем пока wrk отработает:
|
||||
```
|
||||
Running 5m test @ http://192.168.1.66:8080/search
|
||||
1 threads and 10 connections
|
||||
Thread Stats Avg Stdev Max +/- Stdev
|
||||
Latency 5.83s 1.74s 13.29s 77.58%
|
||||
Req/Sec 3.57 4.74 30.00 82.50%
|
||||
511 requests in 5.00m, 18.69MB read
|
||||
Requests/sec: 1.70
|
||||
Transfer/sec: 63.79KB
|
||||
```
|
||||
- #### Подключаем первый слэйв в приложении, добавлением в docker-compose.yml переменной окружения:
|
||||
```
|
||||
APP_DSN_SLAVE1: mysql_slave1
|
||||
```
|
||||
- #### Перезапускаем контейнер с приложением и нагружаем ту же страницу тем же запросом, с помощью wrk:
|
||||
```
|
||||
sudo make app-reload
|
||||
sudo docker run --rm -v /root/scripts:/scripts williamyeh/wrk -t1 -c10 -d5m --timeout 30s http://localhost:8080/search -s /scripts/post.lua -- debug true
|
||||
```
|
||||
- #### Идем в [графану](http://localhost:3001/) и наблюдаем нагрузку на слэйв.
|
||||
- #### Ждем пока wrk отработает:
|
||||
```
|
||||
Running 5m test @ http://192.168.1.66:8080/search
|
||||
1 threads and 10 connections
|
||||
Thread Stats Avg Stdev Max +/- Stdev
|
||||
Latency 32.12ms 31.59ms 561.62ms 90.65%
|
||||
Req/Sec 373.34 147.37 610.00 68.45%
|
||||
110834 requests in 5.00m, 433.90MB read
|
||||
Non-2xx or 3xx responses: 110834
|
||||
Requests/sec: 369.39
|
||||
Transfer/sec: 1.45MB
|
||||
```
|
||||
- #### Получаем график нагрузки:
|
||||

|
||||
|
||||
### 4. ОПЦИОНАЛЬНО: в качестве конфига, который хранит IP реплики сделать массив для легкого добавления реплики. Это не самый правильный способ балансирования нагрузки. Поэтому опционально.
|
||||
Не очень понял, что именно нужно сделать. Если речь о конфиге приложения, я пока сделал отдельные переменные для каждого сервера БД. Если потребуется, заменю срезом. Пока пусть останется так.
|
||||
### 5. Настроить 2 слейва и 1 мастер.
|
||||
Пропускаю этот пункт, т.к. все уже было сделано в п.1
|
||||
### 6. Включить row-based репликацию.
|
||||
- #### Добавляем в my.cnf мастера и обоих слейвов, строки:
|
||||
```
|
||||
binlog_format=ROW
|
||||
binlog-checksum=crc32
|
||||
```
|
||||
### 7. Включить GTID.
|
||||
- #### Добавляем в my.cnf мастера, строки:
|
||||
```
|
||||
gtid-mode=on
|
||||
enforce-gtid-consistency=true
|
||||
```
|
||||
- #### Добавляем в my.cnf обоих слейвов, строки:
|
||||
```
|
||||
gtid-mode=on
|
||||
enforce-gtid-consistency=true
|
||||
binlog-rows-query-log_events=1
|
||||
```
|
||||
### 8. Настроить полу синхронную репликацию.
|
||||
- #### Включаем динамическую загрузку модулей и полу синхронную репликацию с таймаутом 1с в my.cnf на мастере:
|
||||
```
|
||||
loose-rpl_semi_sync_master_enabled=1
|
||||
loose-rpl_semi_sync_master_timeout=1000
|
||||
```
|
||||
- #### Включаем динамическую загрузку модулей и полу синхронную репликацию в my.cnf на обоих слейвах:
|
||||
```
|
||||
loose-rpl_semi_sync_slave_enabled=1
|
||||
```
|
||||
- #### Устанавливаем semisync плагин на мастере:
|
||||
```
|
||||
INSTALL PLUGIN rpl_semi_sync_master SONAME "semisync_master.so";
|
||||
```
|
||||
- #### Устанавливаем semisync плагин на обоих слейвах:
|
||||
```
|
||||
INSTALL PLUGIN rpl_semi_sync_slave SONAME "semisync_slave.so";
|
||||
```
|
||||
- #### Проверяем на всех хостах, установлен ли плагин:
|
||||
```
|
||||
SELECT PLUGIN_NAME, PLUGIN_STATUS FROM INFORMATION_SCHEMA.PLUGINS WHERE PLUGIN_NAME LIKE '%semi%';
|
||||
```
|
||||
- #### Запускаем кластер и приложение:
|
||||
```
|
||||
sudo make app-up
|
||||
```
|
||||
### 9. Создать нагрузку на запись в любую тестовую таблицу. На стороне, которой нагружаем считать, сколько строк мы успешно записали.
|
||||
- #### Запускаем кластер:
|
||||
```
|
||||
sudo make db-up
|
||||
```
|
||||
- #### Запускаем контейнер с mysql-client:
|
||||
```
|
||||
sudo make client-up
|
||||
```
|
||||
- #### Заходим внутрь контейнера и запускаем скрипт:
|
||||
```
|
||||
/scripts/dz003_2.sh
|
||||
```
|
||||
### 10. С помощью kill -9 убиваем мастер MySQL.
|
||||
- #### Т.к. образ mysql:5.7 не содержит команды ps, не мудрим и грохаем контейнер с мастером:
|
||||
```
|
||||
sudo docker kill mysql_master
|
||||
```
|
||||
### 11. Заканчиваем нагрузку на запись.
|
||||
- #### Возвращаемся в контейнер клиента и запоминаем, знаение счетчика строк успешно записанных в таблицу мастера
|
||||
В нашем случае, скрипт тормазнул после добавления 10233 строк
|
||||
### 12. Выбираем самый свежий слейв. Промоутим его до мастера. Переключаем на него второй слейв.
|
||||
- #### Определяем свежайшую реплику:
|
||||
```
|
||||
sudo docker exec mysql_slave1 sh -c "export MYSQL_PWD=root; mysql -u root -e 'SHOW SLAVE STATUS\G' | grep Master_Log_File"
|
||||
sudo docker exec mysql_slave2 sh -c "export MYSQL_PWD=root; mysql -u root -e 'SHOW SLAVE STATUS\G' | grep Master_Log_File"
|
||||
```
|
||||
В нашем случае оба слейва остановились на mysql-bin.000003, поэтому мастером будем промоутить mysql_slave1
|
||||
|
||||
З.Ы. Еще можно было бы поиграть с [отложенной репликацией](https://dev.mysql.com/doc/refman/5.6/en/replication-delayed.html), но не в рамках данного эксперимента
|
||||
|
||||
- #### Останавливаем на всех слейвах потоки получения обновлений бинарного лога:
|
||||
```
|
||||
sudo docker exec mysql_slave1 sh -c "export MYSQL_PWD=root; mysql -u root -e 'STOP SLAVE IO_THREAD; SHOW PROCESSLIST;'"
|
||||
sudo docker exec mysql_slave2 sh -c "export MYSQL_PWD=root; mysql -u root -e 'STOP SLAVE IO_THREAD; SHOW PROCESSLIST;'"
|
||||
```
|
||||
- #### Полностью останавливаем слэйв который будем промоутить до мастера и сбрасываем его бинлог:
|
||||
```
|
||||
sudo docker exec mysql_slave1 sh -c "export MYSQL_PWD=root; mysql -u root -e 'STOP SLAVE; RESET MASTER;'"
|
||||
```
|
||||
- #### Оставшийся слэйв переключаем на новый мастер:
|
||||
```
|
||||
sudo docker exec {slave_to_promote} sh -c "export MYSQL_PWD=root; mysql -u root -e 'STOP SLAVE; CHANGE MASTER TO MASTER_HOST='{slave_to_promote}'; START SLAVE;'"
|
||||
```
|
||||
### 13. Проверяем, есть ли потери транзакций:
|
||||
- #### Проверяем количество строк в тестовой таблице на новом мастере:
|
||||
```
|
||||
sudo docker exec {slave_to_promote} sh -c "export MYSQL_PWD=root; mysql -u root -e 'USE app; SELECT count(*) from test;'"
|
||||
```
|
||||
В нашем случае, потерь транзакций не наблюдается. В таблице нового мастера те же 10233 строки.
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
#/bin/sh
|
||||
|
||||
docker run --rm -v /root/scripts:/scripts williamyeh/wrk -t1 -c10 -d5m --timeout 30s http://localhost:8080/search -s /scripts/post.lua -- debug true
|
Binary file not shown.
Before Width: | Height: | Size: 183 KiB |
|
@ -1,15 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
mysql -uroot -proot -h mysql_master -t app -e 'CREATE TABLE IF NOT EXISTS test (`id` INT(11), `name` VARCHAR(255));'
|
||||
|
||||
|
||||
let i=1
|
||||
let noerr=0
|
||||
|
||||
while [ $noerr = 0 ]
|
||||
do
|
||||
mysql -uroot -proot -h mysql_master -t app -e 'INSERT INTO test (`id`,`name`) VALUES ('$i',"string_value_'$1'")' || ((noerr=1 ))
|
||||
echo $i
|
||||
((i++))
|
||||
done
|
||||
echo "Err"
|
|
@ -1,5 +0,0 @@
|
|||
wrk.method = "POST"
|
||||
wrk.body = "name=ан&surname=ан"
|
||||
wrk.headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
response = function(status, headers, body)
|
||||
end
|
|
@ -1,27 +0,0 @@
|
|||
#НЕ НАЧАТА: [ЧАТ](https://otus.ru/learning/61597/#/homework-chat/12342/) / [ОТЧЕТ](REPORT.md)
|
||||
|
||||
-----
|
||||
|
||||
# Масштабируемая подсистема диалогов
|
||||
|
||||
В результате выполнения ДЗ вы создадите базовый скелет микросервиса, который будет развиваться в дальнейших ДЗ.
|
||||
|
||||
### В данном задании тренируются навыки:
|
||||
- декомпозиции предметной области;
|
||||
- построения элементарной архитектуры проекта;
|
||||
|
||||
### План выполнения:
|
||||
1) Необходимо написать систему диалогов между пользователями.
|
||||
2) Обеспечить горизонтальное масштабирование хранилищ на запись с помощью шардинга.
|
||||
3) Предусмотреть:
|
||||
- Возможность решардинга.
|
||||
- “Эффект Леди Гаги” (один пользователь пишет сильно больше среднего).
|
||||
- Наиболее эффективную схему.
|
||||
|
||||
ДЗ принимается в виде исходного кода на github и отчета по выполненной работе.
|
||||
|
||||
Требования:
|
||||
- Верно выбран ключ шардирования с учетом "эффекта Леди Гаги"
|
||||
- В отчете описан процесс решардинга без даунтайма
|
||||
|
||||
###Рекомендуем сдать до: 17.11.2021
|
|
@ -1,29 +0,0 @@
|
|||
#В ПРОЦЕССЕ: [ЧАТ](https://otus.ru/learning/61597/#/homework-chat/12340/) / [ОТЧЕТ](REPORT.md)
|
||||
|
||||
-----
|
||||
|
||||
#Лента новостей социальной сети
|
||||
Цель: В результате выполнения ДЗ вы создадите ленту новостей социальной сети
|
||||
### В данном задании тренируются навыки:
|
||||
- работа с кешами;
|
||||
- работа с очередями;
|
||||
- проектирование масштабируемых архитектур.
|
||||
|
||||
### План выполнения:
|
||||
1) Разработать страницу добавления поста.
|
||||
2) Разработать ленту новостей, содержащую посты пользователей на которых подписан текущий пользователь.
|
||||
- Создается отдельная страница, куда пишутся все обновления друзей. Для этого нужно хранить подписчиков.
|
||||
- Лента формируется на уровне кешей.
|
||||
- Формирование ленты производить через постановку задачи в очередь на часть подписчиков, чтобы избежать эффекта леди Гаги.
|
||||
- В ленте держать последние 1000 обновлений друзей.
|
||||
- Лента должна кешироваться.
|
||||
|
||||
ДЗ сдается в виде ссылки на github и демонстрации работающего проекта, развернутого в интернете.
|
||||
Критерии оценки: Оценка происходит по принципу зачет/незачет.
|
||||
|
||||
### Требования:
|
||||
- Верно работает инвалидация кеша.
|
||||
- Обновление лент работает через очередь.
|
||||
- Есть возможность перестройки кешей из СУБД.
|
||||
|
||||
### Рекомендуем сдать до: 17.02.2021
|
|
@ -1,27 +0,0 @@
|
|||
#НЕ НАЧАТА: [ЧАТ](https://otus.ru/learning/61597/#/homework-chat/12341/) / [ОТЧЕТ](REPORT.md)
|
||||
|
||||
-----
|
||||
|
||||
#Репликация из MySQL в tarantool
|
||||
Цель: В результате выполнения ДЗ вы настроите репликацию из MySQL в tarantool, а также напишите запрос на lua.
|
||||
### В данном задании тренируются навыки:
|
||||
- администрирование MySQL;
|
||||
- администрирование tarantool;
|
||||
- разработка хранимых процедур для tarantool;
|
||||
|
||||
# План выполнения:
|
||||
1) Выбрать любую таблицу, которую мы читаем с реплик MySQL.
|
||||
2) С помощью программы https://github.com/tarantool/mysql-tarantool-replication настроить реплицирование в tarantool (лучше всего версии 1.10).
|
||||
3) Выбрать любой запрос и переписать его на lua-процедуру на tarantool.
|
||||
4) Провести нагрузочное тестирование, сравнить tarantool и MySQL по производительности.
|
||||
|
||||
ДЗ сдается в виде ссылки на гитлаб и отчета о выполнении ДЗ.
|
||||
Критерии оценки: Оценка происходит по принципу зачет/незачет.
|
||||
|
||||
### Требования:
|
||||
- Репликация из MySQL в tarantool работает.
|
||||
- Хранимые процедуры в tarantool написаны корректно.
|
||||
- Хранимые процедуры выполнены по code style на примере репозитория Mail.Ru.
|
||||
- Нагрузочное тестирование проведено.
|
||||
|
||||
### Рекомендуем сдать до: 22.12.2021
|
|
@ -1,26 +0,0 @@
|
|||
#НЕ НАЧАТА: [ЧАТ](https://otus.ru/learning/61597/#/homework-chat/12343/) / [ОТЧЕТ](REPORT.md)
|
||||
|
||||
-----
|
||||
|
||||
# Онлайн обновление ленты новостей
|
||||
|
||||
Цель: В результате выполнения ДЗ вы научитесь обновлять ленту новостей без перезагрузки страницы.
|
||||
|
||||
### В данном задании тренируются навыки:
|
||||
- работа с WebSocket;
|
||||
|
||||
### План выполнения:
|
||||
1) Разработать компонент, куда будет подключаться клиент при открытии страницы ленты. Сервис должен слушать очередь обновлений ленты. При получении подписанным клиентом сообщения, отправлять его в браузер по WebSocket.
|
||||
2) Учесть возможность масштабирования сервиса. То есть сообщение должно доставляться только на тот экземпляр компонента, куда соединен клиент. Для этого можно использовать, например Routing Key из Rabbitmq.
|
||||
|
||||
ДЗ принимается в виде исходного кода на github, документации по архитектуре и демонстрации работоспособности развернутого в интернете приложения.
|
||||
|
||||
Критерии оценки: Оценка происходит по принципу зачет/незачет.
|
||||
|
||||
### Требования:
|
||||
- При добавлении поста у друга, лента должна обновляться автоматически (с небольшой задержкой).
|
||||
- Корректная работа сервиса вебсокетов.
|
||||
- Линейная масштабируемость сервиса вебсокетов.
|
||||
- Описан процесс масштабирования RabbitMQ.
|
||||
|
||||
### Рекомендуем сдать до: 27.12.2021
|
|
@ -1,28 +0,0 @@
|
|||
#НЕ НАЧАТА: [ЧАТ](https://otus.ru/learning/61597/#/homework-chat/12344/) / [ОТЧЕТ](REPORT.md)
|
||||
|
||||
-----
|
||||
|
||||
# Разделение монолита на сервисы
|
||||
|
||||
Цель: В результате выполнения ДЗ вы перенесете бизнес-домен монолитного приложения в отдельный сервис.
|
||||
###В данном задании тренируются навыки:
|
||||
- декомпозиции предметной области;
|
||||
- разделение монолитного приложения;
|
||||
- работа с HTTP;
|
||||
- работа с REST API и gRPC;
|
||||
|
||||
### План выполнения:
|
||||
1) Вынести систему диалогов в отдельный сервис.
|
||||
2) Взаимодействия монолитного сервиса и сервиса чатов реализовать на Rest API или gRPC.
|
||||
3) Организовать сквозное логирование запросов.
|
||||
4) Предусмотреть то, что не все клиенты обновляют приложение быстро и кто-то может ходить через старое API.
|
||||
|
||||
ДЗ сдается в виде исходного кода на github и отчета по устройству системы.
|
||||
Критерии оценки: Оценка происходит по принципу зачет/незачет.
|
||||
|
||||
### Требования:
|
||||
- Описан протокол взаимодействия.
|
||||
- Поддержаны старые клиенты.
|
||||
- Новые клиенты верно ходят через новый API.
|
||||
|
||||
### Рекомендуем сдать до: 10.01.2022
|
|
@ -1,29 +0,0 @@
|
|||
#НЕ НАЧАТА: [ЧАТ](https://otus.ru/learning/61597/#/homework-chat/12345/) / [ОТЧЕТ](REPORT.md)
|
||||
|
||||
-----
|
||||
|
||||
# Отказоустойчивость приложений
|
||||
Цель: В результате выполнения ДЗ вы уменьшите число точек отказа в вашем приложении.
|
||||
###В данном задании тренируются навыки:
|
||||
- проектирование отказоустойчивых архитектур;
|
||||
- настройка nginx;
|
||||
- настройка HAProxy;
|
||||
- Поднять несколько слейвов MySQL;
|
||||
|
||||
### План выполнения:
|
||||
1) Реализовать соединение со слейвами mysql через haproxy.
|
||||
2) Поднять несколько приложений и обеспечить их балансировку через nginx.
|
||||
3) Воспроизвести нагрузку.
|
||||
4) Под нагрузкой с помощью "kill -9" отключить один из слейвов MySQL. Убедится, что система осталась работоспособной.
|
||||
5) Под нагрузкой с помощью "kill -9" отключить один из инстансов бэкенда. Убедится, что система осталась работоспособной.
|
||||
|
||||
ДЗ принимается в виде отчета по выполненным пунктам.
|
||||
Критерии оценки: Оценка происходит по принципу зачет/незачет.
|
||||
|
||||
### Требования:
|
||||
- В отчете верно описана конфигурация haproxy.
|
||||
- В отчете верно описана конфигурация nginx.
|
||||
- В отчете верно описаны условия эксперимента.
|
||||
- В отчете должны быть логи работы системы.
|
||||
|
||||
### Рекомендуем сдать до: 17.01.2022
|
|
@ -1,26 +0,0 @@
|
|||
#НЕ НАЧАТА: [ЧАТ](https://otus.ru/learning/61597/#/homework-chat/12346/) / [ОТЧЕТ](REPORT.md)
|
||||
|
||||
-----
|
||||
|
||||
# Сервис счетчиков
|
||||
|
||||
Цель: В результате выполнения ДЗ вы создадите сервис счетчиков. Сервис будет хранить такие счетчики, как число непрочитанных сообщений.
|
||||
### В данном задании тренируются навыки:
|
||||
- разработка отказоустойчивых сервисов;
|
||||
- использование кешей;
|
||||
|
||||
### План выполнения:
|
||||
1) Разработайте сервис счетчиков.
|
||||
2) Учтите то, что на этот сервис будет большая нагрузка, особенно на чтение.
|
||||
3) Продумайте, как обеспечить консистентность между счетчиком и реальным числом непрочитанных сообщений. Например, используйте паттерн SAGA.
|
||||
4) Внедрите сервис для отображения счетчиков.
|
||||
|
||||
ДЗ сдается в виде демонстрации работоспособности сервиса, ссылки на репозиторий github, отчета по архитектуре.
|
||||
|
||||
Критерии оценки: Оценка происходит по принципу зачет/незачет.
|
||||
|
||||
### Требования:
|
||||
- Верно описан выбранный паттерн обеспечения консистентности.
|
||||
- Выбранная архитектура сервиса подходит для решения задачи.
|
||||
|
||||
### Рекомендуем сдать до: 24.01.2022
|
|
@ -1,29 +0,0 @@
|
|||
#НЕ НАЧАТА: [ЧАТ](https://otus.ru/learning/61597/#/homework-chat/12347/) / [ОТЧЕТ](REPORT.md)
|
||||
|
||||
-----
|
||||
|
||||
# Внедрение docker и consul
|
||||
|
||||
Цель: В результате выполнения ДЗ вы интегрируете в ваш проект социальной сети docker и auto discovery сервисов с помощью consul
|
||||
### В данном задании тренируются навыки:
|
||||
- использование docker;
|
||||
- использование consul;
|
||||
- построение auto discovery;
|
||||
|
||||
### План выполнения:
|
||||
1) Обернуть сервис диалогов в docker
|
||||
2) Развернуть consul в вашей системе
|
||||
3) Интегрировать auto discovery в систему диалогов
|
||||
4) Научить монолитное приложение находить и равномерно нагружать все поднятые узлы сервиса диалогов
|
||||
5) Опционально можно использовать nomad
|
||||
|
||||
6) ДЗ сдается в виде репозитория с исходными кодами на github и отчетом о выполненных шагах.
|
||||
|
||||
Критерии оценки: Оценка происходит по принципу зачет/незачет.
|
||||
|
||||
### Требования:
|
||||
- Верно настроен docker.
|
||||
- Обеспечено распределение нагрузки по экземплярам сервиса.
|
||||
- Описан процесс развертки новых экземпляров.
|
||||
|
||||
### Рекомендуем сдать до: 31.01.2022
|
|
@ -1,30 +0,0 @@
|
|||
#НЕ НАЧАТА: [ЧАТ](https://otus.ru/learning/61597/#/homework-chat/12348/) / [ОТЧЕТ](REPORT.md)
|
||||
|
||||
-----
|
||||
|
||||
# Мониторинг
|
||||
|
||||
Цель: В результате выполнения ДЗ вы организуете мониторинг своего сервиса чатов.
|
||||
### В данном задании тренируются навыки:
|
||||
- эксплутация prometheus;
|
||||
- эксплутация grafana;
|
||||
- эксплутация zabbix;
|
||||
-
|
||||
### План выполнения:
|
||||
1) развернуть zabbix;
|
||||
2) развернуть prometheus;
|
||||
3) развернуть grafana;
|
||||
4) начать писать в prometheus бизнес-метрики сервиса чатов по принципу RED;
|
||||
5) начать писать в zabbix технические метрики сервера с сервисом чатов;
|
||||
6) организовать дашборд в grafana.
|
||||
|
||||
ДЗ сдается в виде отчета со скриншотами.
|
||||
|
||||
Критерии оценки: Оценка происходит по принципу зачет/незачет.
|
||||
|
||||
### Требования:
|
||||
- Сбор технических метрик осуществляется верно.
|
||||
- Сбор бизнес метрик осуществляется верно по принципу RED.
|
||||
- В grafana организован дашборд.
|
||||
|
||||
### Рекомендуем сдать до: 16.02.2022
|
|
@ -1,21 +0,0 @@
|
|||
#НЕ НАЧАТА: [ЧАТ](https://otus.ru/learning/61597/#/homework-chat/12349/) / [ОТЧЕТ](REPORT.md)
|
||||
|
||||
-----
|
||||
|
||||
# Разработать MVP по данной архитектуре
|
||||
|
||||
### Варианты проектов:
|
||||
- Новостной ресурс
|
||||
- Сайт знакомств
|
||||
- Ресурс для персональных блогов
|
||||
- Интернет-магазин
|
||||
- Или любой другое проект, кроме социальной сети
|
||||
|
||||
Необходимо выбрать тему проекта и отправить её в чат с преподавателем.
|
||||
|
||||
### План выполнения:
|
||||
1) В начале проекта необходимо с наставником согласовать требования.
|
||||
2) Разработать MVP по данной архитектуре.
|
||||
3) Итогом будет защита архитектуры и MVP
|
||||
|
||||
Критерии оценки: Готовый проект + защита проекта
|
|
@ -1,11 +0,0 @@
|
|||
route:
|
||||
receiver: 'slack'
|
||||
|
||||
receivers:
|
||||
- name: 'slack'
|
||||
slack_configs:
|
||||
- send_resolved: true
|
||||
text: "{{ .CommonAnnotations.description }}"
|
||||
username: 'Prometheus'
|
||||
channel: '#<channel-name>'
|
||||
api_url: 'https://hooks.slack.com/services/<webhook-id>'
|
|
@ -1,39 +0,0 @@
|
|||
:9090 {
|
||||
basicauth / {$ADMIN_USER} {$ADMIN_PASSWORD}
|
||||
proxy / prometheus:9090 {
|
||||
transparent
|
||||
}
|
||||
|
||||
errors stderr
|
||||
tls off
|
||||
}
|
||||
|
||||
:9093 {
|
||||
basicauth / {$ADMIN_USER} {$ADMIN_PASSWORD}
|
||||
proxy / alertmanager:9093 {
|
||||
transparent
|
||||
}
|
||||
|
||||
errors stderr
|
||||
tls off
|
||||
}
|
||||
|
||||
:9091 {
|
||||
basicauth / {$ADMIN_USER} {$ADMIN_PASSWORD}
|
||||
proxy / pushgateway:9091 {
|
||||
transparent
|
||||
}
|
||||
|
||||
errors stderr
|
||||
tls off
|
||||
}
|
||||
|
||||
:3000 {
|
||||
proxy / grafana:3000 {
|
||||
transparent
|
||||
websocket
|
||||
}
|
||||
|
||||
errors stderr
|
||||
tls off
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
GF_SECURITY_ADMIN_USER=admin
|
||||
GF_SECURITY_ADMIN_PASSWORD=changeme
|
||||
GF_USERS_ALLOW_SIGN_UP=false
|
|
@ -1,117 +0,0 @@
|
|||
version: '3'
|
||||
services:
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus:v2.24.1
|
||||
container_name: prometheus
|
||||
volumes:
|
||||
- ./prometheus:/etc/prometheus
|
||||
- prometheus_data:/prometheus
|
||||
command:
|
||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||
- '--storage.tsdb.path=/prometheus'
|
||||
- '--web.console.libraries=/etc/prometheus/console_libraries'
|
||||
- '--web.console.templates=/etc/prometheus/consoles'
|
||||
- '--storage.tsdb.retention.time=200h'
|
||||
- '--web.enable-lifecycle'
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- 9090
|
||||
labels:
|
||||
org.label-schema.group: "monitoring"
|
||||
|
||||
alertmanager:
|
||||
image: prom/alertmanager:v0.21.0
|
||||
container_name: alertmanager
|
||||
volumes:
|
||||
- ./alertmanager:/etc/alertmanager
|
||||
command:
|
||||
- '--config.file=/etc/alertmanager/config.yml'
|
||||
- '--storage.path=/alertmanager'
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- 9093
|
||||
labels:
|
||||
org.label-schema.group: "monitoring"
|
||||
|
||||
nodeexporter:
|
||||
image: prom/node-exporter:v1.1.0
|
||||
container_name: nodeexporter
|
||||
volumes:
|
||||
- /proc:/host/proc:ro
|
||||
- /sys:/host/sys:ro
|
||||
- /:/rootfs:ro
|
||||
command:
|
||||
- '--path.procfs=/host/proc'
|
||||
- '--path.rootfs=/rootfs'
|
||||
- '--path.sysfs=/host/sys'
|
||||
- '--collector.filesystem.ignored-mount-points=^/(sys|proc|dev|host|etc)($$|/)'
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- 9100
|
||||
labels:
|
||||
org.label-schema.group: "monitoring"
|
||||
|
||||
cadvisor:
|
||||
image: gcr.io/cadvisor/cadvisor:v0.38.7
|
||||
container_name: cadvisor
|
||||
volumes:
|
||||
- /:/rootfs:ro
|
||||
- /var/run:/var/run:rw
|
||||
- /sys:/sys:ro
|
||||
- /var/lib/docker:/var/lib/docker:ro
|
||||
#- /cgroup:/cgroup:ro #doesn't work on MacOS only for Linux
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- 8080
|
||||
labels:
|
||||
org.label-schema.group: "monitoring"
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:7.4.0
|
||||
container_name: grafana
|
||||
volumes:
|
||||
- grafana_data:/var/lib/grafana
|
||||
- ./grafana/provisioning/dashboards:/etc/grafana/provisioning/dashboards
|
||||
- ./grafana/provisioning/datasources:/etc/grafana/provisioning/datasources
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_USER=${ADMIN_USER:-admin}
|
||||
- GF_SECURITY_ADMIN_PASSWORD=${ADMIN_PASSWORD:-admin}
|
||||
- GF_USERS_ALLOW_SIGN_UP=false
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- 3000
|
||||
labels:
|
||||
org.label-schema.group: "monitoring"
|
||||
ports:
|
||||
- "3001:3000"
|
||||
|
||||
pushgateway:
|
||||
image: prom/pushgateway:v1.4.0
|
||||
container_name: pushgateway
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- 9091
|
||||
labels:
|
||||
org.label-schema.group: "monitoring"
|
||||
|
||||
caddy:
|
||||
image: stefanprodan/caddy
|
||||
container_name: caddy
|
||||
ports:
|
||||
- "3000:3000"
|
||||
- "9090:9090"
|
||||
- "9093:9093"
|
||||
- "9091:9091"
|
||||
volumes:
|
||||
- ./caddy:/etc/caddy
|
||||
environment:
|
||||
- ADMIN_USER=${ADMIN_USER:-admin}
|
||||
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-admin}
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
org.label-schema.group: "monitoring"
|
||||
|
||||
volumes:
|
||||
prometheus_data: { }
|
||||
grafana_data: { }
|
File diff suppressed because it is too large
Load Diff
|
@ -1,12 +0,0 @@
|
|||
apiVersion: 1
|
||||
|
||||
providers:
|
||||
- name: 'Prometheus'
|
||||
orgId: 1
|
||||
folder: ''
|
||||
type: file
|
||||
disableDeletion: false
|
||||
editable: true
|
||||
allowUiUpdates: true
|
||||
options:
|
||||
path: /etc/grafana/provisioning/dashboards
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -1,398 +0,0 @@
|
|||
{
|
||||
"id": null,
|
||||
"title": "Nginx",
|
||||
"description": "Nginx exporter metrics",
|
||||
"tags": [
|
||||
"nginx"
|
||||
],
|
||||
"style": "dark",
|
||||
"timezone": "browser",
|
||||
"editable": true,
|
||||
"hideControls": false,
|
||||
"sharedCrosshair": true,
|
||||
"rows": [
|
||||
{
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"height": "250px",
|
||||
"panels": [
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"datasource": "Prometheus",
|
||||
"decimals": 2,
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"grid": {
|
||||
"threshold1": null,
|
||||
"threshold1Color": "rgba(216, 200, 27, 0.27)",
|
||||
"threshold2": null,
|
||||
"threshold2Color": "rgba(234, 112, 112, 0.22)"
|
||||
},
|
||||
"id": 3,
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"alignAsTable": true,
|
||||
"avg": true,
|
||||
"current": true,
|
||||
"max": true,
|
||||
"min": true,
|
||||
"rightSide": true,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": true
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "connected",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"span": 12,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum(irate(nginx_connections_processed_total{stage=\"any\"}[5m])) by (stage)",
|
||||
"hide": false,
|
||||
"interval": "",
|
||||
"intervalFactor": 10,
|
||||
"legendFormat": "requests",
|
||||
"metric": "",
|
||||
"refId": "B",
|
||||
"step": 10
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Requests/sec",
|
||||
"tooltip": {
|
||||
"msResolution": false,
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "cumulative"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"show": true
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": 0,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"datasource": "Prometheus",
|
||||
"decimals": 2,
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"grid": {
|
||||
"threshold1": null,
|
||||
"threshold1Color": "rgba(216, 200, 27, 0.27)",
|
||||
"threshold2": null,
|
||||
"threshold2Color": "rgba(234, 112, 112, 0.22)"
|
||||
},
|
||||
"id": 2,
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"alignAsTable": true,
|
||||
"avg": true,
|
||||
"current": true,
|
||||
"max": true,
|
||||
"min": true,
|
||||
"rightSide": true,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": true
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "connected",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"span": 12,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum(nginx_connections_current) by (state)",
|
||||
"interval": "",
|
||||
"intervalFactor": 2,
|
||||
"legendFormat": "{{state}}",
|
||||
"metric": "",
|
||||
"refId": "A",
|
||||
"step": 2
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Connections",
|
||||
"tooltip": {
|
||||
"msResolution": false,
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "cumulative"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"show": true
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": 0,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"datasource": "Prometheus",
|
||||
"decimals": 2,
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"grid": {
|
||||
"threshold1": null,
|
||||
"threshold1Color": "rgba(216, 200, 27, 0.27)",
|
||||
"threshold2": null,
|
||||
"threshold2Color": "rgba(234, 112, 112, 0.22)"
|
||||
},
|
||||
"id": 1,
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"alignAsTable": true,
|
||||
"avg": true,
|
||||
"current": true,
|
||||
"max": true,
|
||||
"min": true,
|
||||
"rightSide": true,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": true
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "connected",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"span": 12,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum(irate(nginx_connections_processed_total{stage!=\"any\"}[5m])) by (stage)",
|
||||
"hide": false,
|
||||
"interval": "",
|
||||
"intervalFactor": 10,
|
||||
"legendFormat": "{{stage}}",
|
||||
"metric": "",
|
||||
"refId": "B",
|
||||
"step": 10
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Connections rate",
|
||||
"tooltip": {
|
||||
"msResolution": false,
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "cumulative"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"show": true
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": 0,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"title": "Nginx exporter metrics"
|
||||
},
|
||||
{
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"height": "250px",
|
||||
"panels": [
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"datasource": null,
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"grid": {
|
||||
"threshold1": null,
|
||||
"threshold1Color": "rgba(216, 200, 27, 0.27)",
|
||||
"threshold2": null,
|
||||
"threshold2Color": "rgba(234, 112, 112, 0.22)"
|
||||
},
|
||||
"id": 4,
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"alignAsTable": true,
|
||||
"avg": true,
|
||||
"current": true,
|
||||
"max": true,
|
||||
"min": true,
|
||||
"rightSide": true,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": true
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "connected",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"span": 12,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum(rate(container_cpu_usage_seconds_total{name=~\"nginx\"}[5m])) / count(node_cpu_seconds_total{mode=\"system\"}) * 100",
|
||||
"intervalFactor": 2,
|
||||
"legendFormat": "nginx",
|
||||
"refId": "A",
|
||||
"step": 2
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "CPU usage",
|
||||
"tooltip": {
|
||||
"msResolution": false,
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "cumulative"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"show": true
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"title": "Nginx container metrics"
|
||||
}
|
||||
],
|
||||
"time": {
|
||||
"from": "now-15m",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"time_options": [
|
||||
"5m",
|
||||
"15m",
|
||||
"1h",
|
||||
"6h",
|
||||
"12h",
|
||||
"24h",
|
||||
"2d",
|
||||
"7d",
|
||||
"30d"
|
||||
]
|
||||
},
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"annotations": {
|
||||
"list": []
|
||||
},
|
||||
"refresh": "10s",
|
||||
"schemaVersion": 12,
|
||||
"version": 9,
|
||||
"links": [],
|
||||
"gnetId": null
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Prometheus
|
||||
type: prometheus
|
||||
access: proxy
|
||||
orgId: 1
|
||||
url: http://prometheus:9090
|
||||
basicAuth: false
|
||||
isDefault: true
|
||||
editable: true
|
|
@ -1,22 +0,0 @@
|
|||
# Prometheus on EC2 & ECS:
|
||||
|
||||
Some helpers for anyone configuring Prometheus on ECS and AWS EC2.
|
||||
|
||||
To get started on AWS ECS and EC2:
|
||||
|
||||
*For EC2/ECS nodes*:
|
||||
- Import the ecs task definition and add cadvisor and node-exporter service/task definition and run them on each host you want to be monitored
|
||||
- Any hosts which have "Monitoring: On" tag will be automatically added in the targets
|
||||
- Expose ports 9100 and 9191 to your Prometheus host
|
||||
|
||||
*For Prometheus host*:
|
||||
|
||||
- Copy prometheus.yml configuration present here to base prometheus configuration to enable EC2 service discovery
|
||||
- `docker compose up -d`
|
||||
|
||||
**Note**:
|
||||
Set query.staleness-delta to 1m make metrics more realtime
|
||||
|
||||
|
||||
### TODO
|
||||
- Add alerting rules based on ECS
|
|
@ -1,78 +0,0 @@
|
|||
{
|
||||
"family": "cadvisor",
|
||||
"containerDefinitions": [
|
||||
{
|
||||
"name": "cadvisor",
|
||||
"image": "google/cadvisor",
|
||||
"cpu": 10,
|
||||
"memory": 300,
|
||||
"portMappings": [
|
||||
{
|
||||
"containerPort": 9191,
|
||||
"hostPort": 9191
|
||||
}
|
||||
],
|
||||
"essential": true,
|
||||
"privileged": true,
|
||||
"mountPoints": [
|
||||
{
|
||||
"sourceVolume": "root",
|
||||
"containerPath": "/rootfs",
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"sourceVolume": "var_run",
|
||||
"containerPath": "/var/run",
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"sourceVolume": "sys",
|
||||
"containerPath": "/sys",
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"sourceVolume": "var_lib_docker",
|
||||
"containerPath": "/var/lib/docker",
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"sourceVolume": "cgroup",
|
||||
"containerPath": "/cgroup",
|
||||
"readOnly": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"volumes": [
|
||||
{
|
||||
"name": "root",
|
||||
"host": {
|
||||
"sourcePath": "/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "var_run",
|
||||
"host": {
|
||||
"sourcePath": "/var/run"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "sys",
|
||||
"host": {
|
||||
"sourcePath": "/sys"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "var_lib_docker",
|
||||
"host": {
|
||||
"sourcePath": "/var/lib/docker/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cgroup",
|
||||
"host": {
|
||||
"sourcePath": "/cgroup"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
{
|
||||
"family": "prometheus",
|
||||
"containerDefinitions": [
|
||||
{
|
||||
"portMappings": [
|
||||
{
|
||||
"hostPort": 9100,
|
||||
"containerPort": 9100,
|
||||
"protocol": "tcp"
|
||||
}
|
||||
],
|
||||
"essential": true,
|
||||
"name": "node_exporter",
|
||||
"image": "prom/node-exporter",
|
||||
"cpu": 0,
|
||||
"privileged": null,
|
||||
"memoryReservation": 150
|
||||
}
|
||||
],
|
||||
"volumes": [],
|
||||
"networkMode": "host"
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
global:
|
||||
scrape_interval: 15s
|
||||
evaluation_interval: 15s
|
||||
|
||||
# Attach these labels to any time series or alerts when communicating with
|
||||
# external systems (federation, remote storage, Alertmanager).
|
||||
external_labels:
|
||||
monitor: 'docker-host-alpha'
|
||||
|
||||
# Load and evaluate rules in this file every 'evaluation_interval' seconds.
|
||||
rule_files:
|
||||
- "targets.rules"
|
||||
- "hosts.rules"
|
||||
- "containers.rules"
|
||||
|
||||
# A scrape configuration containing exactly one endpoint to scrape.
|
||||
scrape_configs:
|
||||
- job_name: 'nodeexporter'
|
||||
scrape_interval: 5s
|
||||
static_configs:
|
||||
- targets: ['nodeexporter:9100']
|
||||
|
||||
- job_name: 'cadvisor'
|
||||
scrape_interval: 5s
|
||||
static_configs:
|
||||
- targets: ['cadvisor:8080']
|
||||
|
||||
- job_name: 'prometheus'
|
||||
scrape_interval: 10s
|
||||
static_configs:
|
||||
- targets: ['localhost:9090']
|
||||
|
||||
|
||||
# sample scrape configuration for AWS EC2
|
||||
- job_name: 'nodeexporter'
|
||||
ec2_sd_configs:
|
||||
- region: us-east-1
|
||||
port: 9100
|
||||
relabel_configs:
|
||||
# Only monitor instances which have a tag called Monitoring "Monitoring"
|
||||
- source_labels: [__meta_ec2_tag_Monitoring]
|
||||
regex: On
|
||||
action: keep
|
||||
|
||||
- job_name: 'cadvisor'
|
||||
ec2_sd_configs:
|
||||
- region: us-east-1
|
||||
port: 9010
|
||||
relabel_configs:
|
||||
# Only monitor instances which have a tag called Monitoring "Monitoring"
|
||||
- source_labels: [__meta_ec2_tag_Monitoring]
|
||||
regex: On
|
||||
action: keep
|
|
@ -1,70 +0,0 @@
|
|||
groups:
|
||||
- name: targets
|
||||
rules:
|
||||
- alert: monitor_service_down
|
||||
expr: up == 0
|
||||
for: 30s
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
summary: "Monitor service non-operational"
|
||||
description: "Service {{ $labels.instance }} is down."
|
||||
|
||||
- name: host
|
||||
rules:
|
||||
- alert: high_cpu_load
|
||||
expr: node_load1 > 1.5
|
||||
for: 30s
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "Server under high load"
|
||||
description: "Docker host is under high load, the avg load 1m is at {{ $value}}. Reported by instance {{ $labels.instance }} of job {{ $labels.job }}."
|
||||
|
||||
- alert: high_memory_load
|
||||
expr: (sum(node_memory_MemTotal_bytes) - sum(node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes) ) / sum(node_memory_MemTotal_bytes) * 100 > 85
|
||||
for: 30s
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "Server memory is almost full"
|
||||
description: "Docker host memory usage is {{ humanize $value}}%. Reported by instance {{ $labels.instance }} of job {{ $labels.job }}."
|
||||
|
||||
- alert: high_storage_load
|
||||
expr: (node_filesystem_size_bytes{fstype="aufs"} - node_filesystem_free_bytes{fstype="aufs"}) / node_filesystem_size_bytes{fstype="aufs"} * 100 > 85
|
||||
for: 30s
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "Server storage is almost full"
|
||||
description: "Docker host storage usage is {{ humanize $value}}%. Reported by instance {{ $labels.instance }} of job {{ $labels.job }}."
|
||||
|
||||
- name: containers
|
||||
rules:
|
||||
- alert: jenkins_down
|
||||
expr: absent(container_memory_usage_bytes{name="jenkins"})
|
||||
for: 30s
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
summary: "Jenkins down"
|
||||
description: "Jenkins container is down for more than 30 seconds."
|
||||
|
||||
- alert: jenkins_high_cpu
|
||||
expr: sum(rate(container_cpu_usage_seconds_total{name="jenkins"}[1m])) / count(node_cpu_seconds_total{mode="system"}) * 100 > 10
|
||||
for: 30s
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "Jenkins high CPU usage"
|
||||
description: "Jenkins CPU usage is {{ humanize $value}}%."
|
||||
|
||||
- alert: jenkins_high_memory
|
||||
expr: sum(container_memory_usage_bytes{name="jenkins"}) > 1200000000
|
||||
for: 30s
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "Jenkins high memory usage"
|
||||
description: "Jenkins memory consumption is at {{ humanize $value}}."
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
global:
|
||||
scrape_interval: 15s
|
||||
evaluation_interval: 15s
|
||||
|
||||
# Attach these labels to any time series or alerts when communicating with
|
||||
# external systems (federation, remote storage, Alertmanager).
|
||||
external_labels:
|
||||
monitor: 'docker-host-alpha'
|
||||
|
||||
# Load and evaluate rules in this file every 'evaluation_interval' seconds.
|
||||
rule_files:
|
||||
- "alert.rules"
|
||||
|
||||
# A scrape configuration containing exactly one endpoint to scrape.
|
||||
scrape_configs:
|
||||
- job_name: 'nodeexporter'
|
||||
scrape_interval: 5s
|
||||
static_configs:
|
||||
- targets: ['nodeexporter:9100']
|
||||
|
||||
- job_name: 'cadvisor'
|
||||
scrape_interval: 5s
|
||||
static_configs:
|
||||
- targets: ['cadvisor:8080']
|
||||
|
||||
- job_name: 'prometheus'
|
||||
scrape_interval: 10s
|
||||
static_configs:
|
||||
- targets: ['localhost:9090']
|
||||
|
||||
- job_name: 'pushgateway'
|
||||
scrape_interval: 10s
|
||||
honor_labels: true
|
||||
static_configs:
|
||||
- targets: ['pushgateway:9091']
|
||||
|
||||
|
||||
alerting:
|
||||
alertmanagers:
|
||||
- scheme: http
|
||||
static_configs:
|
||||
- targets:
|
||||
- 'alertmanager:9093'
|
||||
|
||||
# - job_name: 'nginx'
|
||||
# scrape_interval: 10s
|
||||
# static_configs:
|
||||
# - targets: ['nginxexporter:9113']
|
||||
|
||||
# - job_name: 'aspnetcore'
|
||||
# scrape_interval: 10s
|
||||
# static_configs:
|
||||
# - targets: ['eventlog-proxy:5000', 'eventlog:5000']
|
Loading…
Reference in New Issue