Compare commits
1 Commits
main
...
actency-my
Author | SHA1 | Date |
---|---|---|
|
ea50e49a7e |
72
Makefile
72
Makefile
|
@ -1,72 +1,14 @@
|
||||||
cdir = $(shell pwd)
|
cdir = $(shell pwd)
|
||||||
|
|
||||||
check-lint:
|
up:
|
||||||
which golangci-lint || (GO111MODULE=off go get -u github.com/golangci/golangci-lint/cmd/golangci-lint)
|
sudo -S docker-compose -f ./cicd/docker-compose.yml up -d --build
|
||||||
|
|
||||||
lint: check-lint
|
down: shutdown clean
|
||||||
@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 \
|
|
||||||
./...
|
|
||||||
|
|
||||||
app-up:
|
shutdown:
|
||||||
docker-compose -f ./cicd/dc_app.yml up -d --build
|
sudo -S docker-compose -f ./cicd/docker-compose.yml down
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
clean:
|
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) (не начата)
|
Разместить приложение на любом хостинге. Например, heroku.
|
||||||
### [08. Разделение монолита на сервисы](test/dz008/README.md) (не начата)
|
|
||||||
### [09. Отказоустойчивость приложений](test/dz009/README.md) (не начата)
|
ДЗ принимается в виде исходного кода на github и демонстрации проекта на хостинге.
|
||||||
### [10. Сервис счетчиков](test/dz010/README.md) (не начата)
|
|
||||||
### [11. Внедрение docker и consul](test/dz011/README.md) (не начата)
|
Критерии оценки: Оценка происходит по принципу зачет/незачет.
|
||||||
### [12. Мониторинг](test/dz012/README.md) (не начата)
|
|
||||||
### [13. Проектная работа](test/dz013/README.md) (не начата)
|
###Требования:
|
||||||
|
- Есть возможность регистрации, создавать персональные страницы, возможность подружиться, список друзей.
|
||||||
|
- Отсутствуют 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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/codegangsta/martini"
|
"github.com/codegangsta/martini"
|
||||||
"github.com/codegangsta/martini-contrib/binding"
|
"github.com/codegangsta/martini-contrib/binding"
|
||||||
"github.com/codegangsta/martini-contrib/render"
|
"github.com/codegangsta/martini-contrib/render"
|
||||||
"github.com/codegangsta/martini-contrib/sessions"
|
"github.com/codegangsta/martini-contrib/sessions"
|
||||||
_ "github.com/go-sql-driver/mysql"
|
_ "github.com/go-sql-driver/mysql"
|
||||||
|
|
||||||
"github.com/tiburon-777/OTUS_HighLoad/internal/application"
|
"github.com/tiburon-777/OTUS_HighLoad/internal/application"
|
||||||
"github.com/tiburon-777/OTUS_HighLoad/internal/auth"
|
"github.com/tiburon-777/OTUS_HighLoad/internal/auth"
|
||||||
"github.com/tiburon-777/OTUS_HighLoad/internal/handlers"
|
"github.com/tiburon-777/OTUS_HighLoad/internal/handlers"
|
||||||
"github.com/tiburon-777/OTUS_HighLoad/pkg/dataset"
|
"github.com/tiburon-777/OTUS_HighLoad/pkg/dataset"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -27,11 +25,11 @@ func init() {
|
||||||
func main() {
|
func main() {
|
||||||
log.Println("Starting...")
|
log.Println("Starting...")
|
||||||
m := martini.Classic()
|
m := martini.Classic()
|
||||||
app, err := application.New("application.conf", "APP")
|
app, err := application.New("", "APP")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(fmt.Errorf("can't build app: %w", err).Error())
|
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(log.New(os.Stdout, "[app]", log.Lshortfile))
|
||||||
m.Map(app)
|
m.Map(app)
|
||||||
|
@ -42,7 +40,7 @@ func main() {
|
||||||
Extensions: []string{".tmpl"},
|
Extensions: []string{".tmpl"},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
auth.RedirectURL = "/login"
|
auth.RedirectUrl = "/login"
|
||||||
auth.RedirectParam = "next"
|
auth.RedirectParam = "next"
|
||||||
|
|
||||||
m.Get("/404", func(r render.Render) {
|
m.Get("/404", func(r render.Render) {
|
||||||
|
@ -74,11 +72,6 @@ func main() {
|
||||||
m.Get("/search", handlers.GetUserList)
|
m.Get("/search", handlers.GetUserList)
|
||||||
m.Post("/search", handlers.PostUserSearch)
|
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) {
|
m.NotFound(func(r render.Render) {
|
||||||
r.HTML(404, "404", nil)
|
r.HTML(404, "404", nil)
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,47 +3,30 @@ package application
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/tiburon-777/modules/core/config"
|
|
||||||
|
|
||||||
"github.com/tiburon-777/OTUS_HighLoad/internal/models"
|
"github.com/tiburon-777/OTUS_HighLoad/internal/models"
|
||||||
|
"github.com/tiburon-777/modules/core/config"
|
||||||
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
Config *models.Configuration
|
Config *models.Configuration
|
||||||
DBMaster *sql.DB
|
DB *sql.DB
|
||||||
DBSlave1 *sql.DB
|
|
||||||
DBSlave2 *sql.DB
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(configFile, envPrefix string) (app App, err error) {
|
func New(configFile, envPrefix string) (App, error) {
|
||||||
app.Config, err = configure(configFile, envPrefix)
|
conf, err := configure(configFile, envPrefix)
|
||||||
if err != nil {
|
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 {
|
if err != nil {
|
||||||
return App{}, err
|
return App{}, err
|
||||||
}
|
}
|
||||||
if err = dbInit(app.DBMaster); err != nil {
|
if err = dbInit(db); err != nil {
|
||||||
return App{}, err
|
return App{}, err
|
||||||
}
|
}
|
||||||
if app.Config.DSN.Slave1 != "" {
|
return App{Config: conf, DB: db}, nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func configure(fileName string, envPrefix string) (*models.Configuration, error) {
|
func configure(fileName string, envPrefix string) (*models.Configuration, error) {
|
||||||
|
@ -77,7 +60,7 @@ func dbInit(db *sql.DB) error {
|
||||||
City varchar(255) DEFAULT NULL,
|
City varchar(255) DEFAULT NULL,
|
||||||
Interests varchar(255) DEFAULT NULL,
|
Interests varchar(255) DEFAULT NULL,
|
||||||
PRIMARY KEY (Id)
|
PRIMARY KEY (Id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_unicode_ci`); err != nil {
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8`); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS relations (
|
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 {
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8`); err != nil {
|
||||||
return err
|
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")
|
log.Println("All tables exists")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,21 +2,19 @@ package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/codegangsta/martini"
|
"github.com/codegangsta/martini"
|
||||||
"github.com/codegangsta/martini-contrib/render"
|
"github.com/codegangsta/martini-contrib/render"
|
||||||
"github.com/codegangsta/martini-contrib/sessions"
|
"github.com/codegangsta/martini-contrib/sessions"
|
||||||
|
|
||||||
"github.com/tiburon-777/OTUS_HighLoad/internal/application"
|
"github.com/tiburon-777/OTUS_HighLoad/internal/application"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
// These are the default configuration values for this package. They
|
// These are the default configuration values for this package. They
|
||||||
// can be set at anytime, probably during the initial setup of Martini.
|
// can be set at anytime, probably during the initial setup of Martini.
|
||||||
var (
|
var (
|
||||||
// RedirectURL should be the relative URL for your login route
|
// RedirectUrl should be the relative URL for your login route
|
||||||
RedirectURL string = "/login"
|
RedirectUrl string = "/login"
|
||||||
|
|
||||||
// RedirectParam is the query string parameter that will be set
|
// RedirectParam is the query string parameter that will be set
|
||||||
// with the page the user was trying to visit before they were
|
// with the page the user was trying to visit before they were
|
||||||
|
@ -41,10 +39,10 @@ type User interface {
|
||||||
Logout()
|
Logout()
|
||||||
|
|
||||||
// Return the unique identifier of this user object
|
// Return the unique identifier of this user object
|
||||||
UniqueID() interface{}
|
UniqueId() interface{}
|
||||||
|
|
||||||
// Populate this user object with values
|
// 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
|
// 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.
|
// user type.
|
||||||
func SessionUser(newUser func() User) martini.Handler {
|
func SessionUser(newUser func() User) martini.Handler {
|
||||||
return func(s sessions.Session, c martini.Context, l *log.Logger, app application.App) {
|
return func(s sessions.Session, c martini.Context, l *log.Logger, app application.App) {
|
||||||
userID := s.Get(SessionKey)
|
userId := s.Get(SessionKey)
|
||||||
user := newUser()
|
user := newUser()
|
||||||
|
|
||||||
if userID != nil {
|
if userId != nil {
|
||||||
err := user.GetByID(app, userID)
|
err := user.GetById(app, userId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Printf("Login Error: %v\n", err)
|
l.Printf("Login Error: %v\n", err)
|
||||||
} else {
|
} else {
|
||||||
|
@ -90,8 +88,8 @@ func Logout(s sessions.Session, user User) {
|
||||||
// authenticated, they will be redirected to /login with the "next" get parameter
|
// authenticated, they will be redirected to /login with the "next" get parameter
|
||||||
// set to the attempted URL.
|
// set to the attempted URL.
|
||||||
func LoginRequired(r render.Render, user User, req *http.Request) {
|
func LoginRequired(r render.Render, user User, req *http.Request) {
|
||||||
if !user.IsAuthenticated() {
|
if user.IsAuthenticated() == false {
|
||||||
path := fmt.Sprintf("%s?%s=%s", RedirectURL, RedirectParam, req.URL.Path)
|
path := fmt.Sprintf("%s?%s=%s", RedirectUrl, RedirectParam, req.URL.Path)
|
||||||
r.Redirect(path, 302)
|
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
|
// 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.
|
// is made to the user model that needs to persist across requests.
|
||||||
func UpdateUser(s sessions.Session, user User) error {
|
func UpdateUser(s sessions.Session, user User) error {
|
||||||
s.Set(SessionKey, user.UniqueID())
|
s.Set(SessionKey, user.UniqueId())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,13 +2,12 @@ package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/tiburon-777/OTUS_HighLoad/internal/application"
|
"github.com/tiburon-777/OTUS_HighLoad/internal/application"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserModel struct {
|
type UserModel struct {
|
||||||
ID int64 `db:"id" form:"id"`
|
Id int64 `db:"id" form:"id"`
|
||||||
Username string `db:"username" form:"username"`
|
Username string `db:"username" form:"username"`
|
||||||
Password string `db:"password" form:"password"`
|
Password string `db:"password" form:"password"`
|
||||||
Name string `db:"name" form:"name"`
|
Name string `db:"name" form:"name"`
|
||||||
|
@ -46,14 +45,14 @@ func (u *UserModel) IsAuthenticated() bool {
|
||||||
return u.authenticated
|
return u.authenticated
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UserModel) UniqueID() interface{} {
|
func (u *UserModel) UniqueId() interface{} {
|
||||||
return u.ID
|
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
|
var v string
|
||||||
query := fmt.Sprintf("SELECT username, name, surname, birthdate, gender, city, interests FROM users WHERE id=%d", id)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -61,6 +60,6 @@ func (u *UserModel) GetByID(app application.App, id interface{}) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
u.ID = id.(int64)
|
u.Id = id.(int64)
|
||||||
return nil
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/codegangsta/martini-contrib/render"
|
"github.com/codegangsta/martini-contrib/render"
|
||||||
"github.com/codegangsta/martini-contrib/sessions"
|
"github.com/codegangsta/martini-contrib/sessions"
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
// MySQL driver
|
|
||||||
_ "github.com/go-sql-driver/mysql"
|
_ "github.com/go-sql-driver/mysql"
|
||||||
|
|
||||||
"github.com/tiburon-777/OTUS_HighLoad/internal/application"
|
"github.com/tiburon-777/OTUS_HighLoad/internal/application"
|
||||||
"github.com/tiburon-777/OTUS_HighLoad/internal/auth"
|
"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) {
|
func GetHome(app application.App, r render.Render, user auth.User) {
|
||||||
h := user.(*auth.UserModel).BirthDate
|
h := user.(*auth.UserModel).BirthDate
|
||||||
user.(*auth.UserModel).YearsOld = int(time.Since(h).Hours() / 8760)
|
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 users []auth.UserModel
|
||||||
var tmp auth.UserModel
|
var tmp auth.UserModel
|
||||||
var tmpTime string
|
var tmpTime string
|
||||||
var results, err = app.DBMaster.Query(`SELECT
|
var results, err = app.DB.Query(`SELECT
|
||||||
users.id as id,
|
users.id as id,
|
||||||
users.name as name,
|
users.name as name,
|
||||||
users.surname as surname,
|
users.surname as surname,
|
||||||
|
@ -45,13 +35,13 @@ func GetHome(app application.App, r render.Render, user auth.User) {
|
||||||
relations.friendId=users.Id
|
relations.friendId=users.Id
|
||||||
AND relations.userId=?
|
AND relations.userId=?
|
||||||
GROUP BY users.Id`,
|
GROUP BY users.Id`,
|
||||||
user.(*auth.UserModel).ID)
|
user.(*auth.UserModel).Id)
|
||||||
if err != nil || results == nil {
|
if err != nil || results == nil {
|
||||||
err500("can't get user list from DB: ", err, r)
|
err500("can't get user list from DB: ", err, r)
|
||||||
}
|
}
|
||||||
defer results.Close()
|
defer results.Close()
|
||||||
for results.Next() {
|
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 {
|
if err != nil {
|
||||||
err500("can't scan result from DB: ", err, r)
|
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
|
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)
|
r.HTML(200, "index", doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,7 +79,7 @@ func PostSignup(app application.App, postedUser auth.UserModel, r render.Render)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err500("can't generate password hash: ", err, r)
|
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 (?, ?, ?, ?, ?, ?, ?, ?)`,
|
values (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
postedUser.Username,
|
postedUser.Username,
|
||||||
pHash,
|
pHash,
|
||||||
|
@ -123,18 +96,136 @@ func PostSignup(app application.App, postedUser auth.UserModel, r render.Render)
|
||||||
r.Redirect("/login")
|
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) {
|
func PostLogin(app application.App, session sessions.Session, postedUser auth.UserModel, r render.Render, req *http.Request) {
|
||||||
user := auth.UserModel{}
|
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))
|
err2 := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(postedUser.Password))
|
||||||
if err1 != nil || err2 != nil {
|
if err1 != nil || err2 != nil {
|
||||||
doc := map[string]interface{}{
|
doc := map[string]interface{}{
|
||||||
"msg": "Wrong user or password. You may sign in.",
|
"msg": "Wrong user or password. You may sign in.",
|
||||||
}
|
}
|
||||||
r.HTML(200, "login", doc)
|
r.HTML(200, "login", doc)
|
||||||
r.Redirect(auth.RedirectURL)
|
r.Redirect(auth.RedirectUrl)
|
||||||
return
|
return
|
||||||
}
|
} else {
|
||||||
err := auth.AuthenticateSession(session, &user)
|
err := auth.AuthenticateSession(session, &user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err500("can't auth session: ", err, r)
|
err500("can't auth session: ", err, r)
|
||||||
|
@ -144,6 +235,44 @@ func PostLogin(app application.App, session sessions.Session, postedUser auth.Us
|
||||||
r.Redirect(redirect)
|
r.Redirect(redirect)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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 {
|
func str2Time(s string, r render.Render) time.Time {
|
||||||
t, err := time.Parse("2006-01-02 15:04:05", s)
|
t, err := time.Parse("2006-01-02 15:04:05", s)
|
||||||
|
@ -154,7 +283,7 @@ func str2Time(s string, r render.Render) time.Time {
|
||||||
}
|
}
|
||||||
|
|
||||||
func err500(s string, err error, r render.Render) {
|
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)
|
log.Println(e)
|
||||||
doc := map[string]interface{}{
|
doc := map[string]interface{}{
|
||||||
"Error": e,
|
"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("/")
|
|
||||||
|
|
||||||
}
|
|
|
@ -11,9 +11,7 @@ type Server struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type DSN struct {
|
type DSN struct {
|
||||||
Master string
|
Host string
|
||||||
Slave1 string
|
|
||||||
Slave2 string
|
|
||||||
Port string
|
Port string
|
||||||
User string
|
User string
|
||||||
Pass string
|
Pass string
|
||||||
|
|
|
@ -2,13 +2,12 @@ package dataset
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"github.com/mdigger/translit"
|
||||||
"log"
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mdigger/translit"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Person struct {
|
type Person struct {
|
||||||
|
@ -31,13 +30,9 @@ func NewPerson() (p Person) {
|
||||||
p.FirstName = womanNames[rand.Intn(len(womanNames))]
|
p.FirstName = womanNames[rand.Intn(len(womanNames))]
|
||||||
p.SecondName = secondNames[rand.Intn(len(secondNames))] + "а"
|
p.SecondName = secondNames[rand.Intn(len(secondNames))] + "а"
|
||||||
}
|
}
|
||||||
charSet := "abcdedfghijklmnopqrstABCDEFGHIJKLMNOP0123456789"
|
t := make([]byte, 16)
|
||||||
var output strings.Builder
|
rand.Read(t)
|
||||||
for i := 0; i < 16; i++ {
|
p.Password = string(t)
|
||||||
random := rand.Intn(len(charSet))
|
|
||||||
output.WriteString(string(charSet[random]))
|
|
||||||
}
|
|
||||||
p.Password = output.String()
|
|
||||||
p.City = cities[rand.Intn(len(cities))]
|
p.City = cities[rand.Intn(len(cities))]
|
||||||
for i := 0; i < (rand.Intn(4) + 3); i++ {
|
for i := 0; i < (rand.Intn(4) + 3); i++ {
|
||||||
p.Interests = append(p.Interests, interests[rand.Intn(len(interests))])
|
p.Interests = append(p.Interests, interests[rand.Intn(len(interests))])
|
||||||
|
@ -54,7 +49,7 @@ func FillDB(db *sql.DB, lim int) {
|
||||||
}
|
}
|
||||||
uCount = lim - uCount
|
uCount = lim - uCount
|
||||||
if uCount <= 0 {
|
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)
|
log.Printf("Try to generate %d rows and fill the DB...", uCount)
|
||||||
for i := 1; i < uCount; i++ {
|
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 now live in <b>{{ .user.City }}</b></p>
|
||||||
<p>You interests is: <b>{{ .user.Interests }}</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>
|
<h2> You have friends:</h2>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -55,11 +49,8 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<h2> Your posts:</h2>
|
<h2>Available actions:</h2>
|
||||||
{{ range $post:=.posts }}
|
<input type="button" onclick="location.href='/list';" value="User list" />
|
||||||
<a><b>{{ $post.Subject }}</b><br/><a>{{ $post.Body }}</a>
|
<input type="button" onclick="location.href='/logout';" value="Logout" /><br />
|
||||||
<br/><br/>
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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