Compare commits

..

1 Commits

67 changed files with 338 additions and 9582 deletions

View File

@ -1,72 +1,14 @@
cdir = $(shell pwd)
check-lint:
which golangci-lint || (GO111MODULE=off go get -u github.com/golangci/golangci-lint/cmd/golangci-lint)
up:
sudo -S docker-compose -f ./cicd/docker-compose.yml up -d --build
lint: check-lint
@echo "+ $@"
@golangci-lint run -v --timeout 3m \
--fast \
--issues-exit-code=0 \
--print-issued-lines=false \
--enable=gocognit \
--enable=gocritic \
--enable=prealloc \
--enable=unparam \
--enable=nakedret \
--enable=scopelint \
--disable=deadcode \
--disable=unused \
--enable=gocyclo \
--enable=golint \
--enable=varcheck \
--enable=structcheck \
--enable=maligned \
--enable=errcheck \
--enable=dupl \
--enable=ineffassign \
--enable=interfacer \
--enable=unconvert \
--enable=goconst \
--enable=gosec \
--enable=megacheck \
./...
down: shutdown clean
app-up:
docker-compose -f ./cicd/dc_app.yml up -d --build
app-down:
docker-compose -f ./cicd/dc_app.yml down
app-reload: app-down app-up
db-up:
rm -rf /opt/mysql_master/* ; \
rm -rf /opt/mysql_slave1/* ; \
rm -rf /opt/mysql_slave2/* ; \
docker-compose -f ./cicd/dc_db.yml up -d --build ; \
./cicd/init.sh
db-down:
docker-compose -f ./cicd/dc_db.yml down
client-up:
docker-compose -f ./cicd/dc_client.yml up -d --build
client-down:
docker-compose -f ./cicd/dc_client.yml down
prom-up:
docker-compose -f ./test/monitor/docker-compose.yml up -d --build
prom-down:
docker-compose -f ./test/monitor/docker-compose.yml down
up: db-up app-up prom-up
down: prom-down app-down db-down
shutdown:
sudo -S docker-compose -f ./cicd/docker-compose.yml down
clean:
docker rmi $$(sudo docker images -a | grep '<none>' | awk '{print $$3}')
sudo docker rmi $(sudo docker images | grep '<none>' | awk '{print $3}')
.PHONY: app-up app-down app-reload prom-up prom-down up down
.PHONY: up down

View File

@ -1,20 +1,41 @@
## Домашние задания по курсу [Highload Architect](https://otus.ru/lessons/highloadarchitect/)
### [01. Заготовка для социальной сети](test/dz001/README.md) (ЗАЧЕТ)
### [02. Производительность индексов](test/dz002/README.md) (ЗАЧЕТ)
### [03. Полусинхронная репликация](test/dz003/README.md) (ЗАЧЕТ)
# Заготовка для социальной сети
Цель: В результате выполнения ДЗ вы создадите базовый скелет социальной сети, который будет развиваться в дальнейших ДЗ.
-----
###В данном задании тренируются навыки:
- декомпозиции предметной области;
- построения элементарной архитектуры проекта
Требуется разработать создание и просмотр анект в социальной сети.
### [04. Масштабируемая подсистема диалогов](test/dz004/README.md) (не начата)
### [05. Лента новостей социальной сети](test/dz005/README.md) (в процессе)
###Функциональные требования:
- Авторизация по паролю.
- Страница регистрации, где указывается следующая информация:
- Имя
- Фамилия
- Возраст
- Пол
- Интересы
- Город
- Страницы с анкетой.
-----
###Нефункциональные требования:
- Любой язык программирования
- В качестве базы данных использовать MySQL
- Не использовать ORM
- Программа должна представлять из себя монолитное приложение.
- Не рекомендуется использовать следующие технологии:
- Репликация
- Шардинг
- Индексы
- Кэширование
### [06. Репликация из MySQL в tarantool](test/dz006/README.md) (не начата)
### [07. Онлайн обновление ленты новостей](test/dz007/README.md) (не начата)
### [08. Разделение монолита на сервисы](test/dz008/README.md) (не начата)
### [09. Отказоустойчивость приложений](test/dz009/README.md) (не начата)
### [10. Сервис счетчиков](test/dz010/README.md) (не начата)
### [11. Внедрение docker и consul](test/dz011/README.md) (не начата)
### [12. Мониторинг](test/dz012/README.md) (не начата)
### [13. Проектная работа](test/dz013/README.md) (не начата)
Верстка не важна. Подойдет самая примитивная.
Разместить приложение на любом хостинге. Например, heroku.
ДЗ принимается в виде исходного кода на github и демонстрации проекта на хостинге.
Критерии оценки: Оценка происходит по принципу зачет/незачет.
###Требования:
- Есть возможность регистрации, создавать персональные страницы, возможность подружиться, список друзей.
- Отсутствуют SQL-инъекции.
- Пароль хранится безопасно.

2
cicd/dbinit/init.sql Normal file
View File

@ -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';

View File

@ -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"

View File

@ -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

View File

@ -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

52
cicd/docker-compose.yml Normal file
View File

@ -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"

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -2,22 +2,20 @@ package main
import (
"fmt"
"log"
"net"
"net/http"
"os"
"time"
"github.com/codegangsta/martini"
"github.com/codegangsta/martini-contrib/binding"
"github.com/codegangsta/martini-contrib/render"
"github.com/codegangsta/martini-contrib/sessions"
_ "github.com/go-sql-driver/mysql"
"github.com/tiburon-777/OTUS_HighLoad/internal/application"
"github.com/tiburon-777/OTUS_HighLoad/internal/auth"
"github.com/tiburon-777/OTUS_HighLoad/internal/handlers"
"github.com/tiburon-777/OTUS_HighLoad/pkg/dataset"
"log"
"net"
"net/http"
"os"
"time"
)
func init() {
@ -27,11 +25,11 @@ func init() {
func main() {
log.Println("Starting...")
m := martini.Classic()
app, err := application.New("application.conf", "APP")
app, err := application.New("", "APP")
if err != nil {
log.Fatal(fmt.Errorf("can't build app: %w", err).Error())
}
go dataset.FillDB(app.DBMaster, 1000000)
go dataset.FillDB(app.DB, 1000000)
m.Map(log.New(os.Stdout, "[app]", log.Lshortfile))
m.Map(app)
@ -42,7 +40,7 @@ func main() {
Extensions: []string{".tmpl"},
}))
auth.RedirectURL = "/login"
auth.RedirectUrl = "/login"
auth.RedirectParam = "next"
m.Get("/404", func(r render.Render) {
@ -74,11 +72,6 @@ func main() {
m.Get("/search", handlers.GetUserList)
m.Post("/search", handlers.PostUserSearch)
m.Get("/addPost", auth.LoginRequired, handlers.GetAddPost)
m.Post("/addPost", auth.LoginRequired, handlers.PostAddPost)
m.Get("/feed", auth.LoginRequired, handlers.GetFeed)
m.NotFound(func(r render.Render) {
r.HTML(404, "404", nil)
})

View File

@ -3,47 +3,30 @@ package application
import (
"database/sql"
"fmt"
"log"
"github.com/tiburon-777/modules/core/config"
"github.com/tiburon-777/OTUS_HighLoad/internal/models"
"github.com/tiburon-777/modules/core/config"
"log"
)
type App struct {
Config *models.Configuration
DBMaster *sql.DB
DBSlave1 *sql.DB
DBSlave2 *sql.DB
DB *sql.DB
}
func New(configFile, envPrefix string) (app App, err error) {
app.Config, err = configure(configFile, envPrefix)
func New(configFile, envPrefix string) (App, error) {
conf, err := configure(configFile, envPrefix)
if err != nil {
return App{}, fmt.Errorf("can't apply config: %w", err)
return App{}, fmt.Errorf("can't apply config: %w\n", err)
}
app.DBMaster, err = sql.Open("mysql", app.Config.DSN.User+":"+app.Config.DSN.Pass+"@tcp("+app.Config.DSN.Master+":"+app.Config.DSN.Port+")/"+app.Config.DSN.Base+"?charset=utf8&collation=utf8_unicode_ci")
db, err := sql.Open("mysql", conf.DSN.User+":"+conf.DSN.Pass+"@tcp("+conf.DSN.Host+":"+conf.DSN.Port+")/"+conf.DSN.Base)
if err != nil {
return App{}, err
}
if err = dbInit(app.DBMaster); err != nil {
if err = dbInit(db); err != nil {
return App{}, err
}
if app.Config.DSN.Slave1 != "" {
app.DBSlave1, err = sql.Open("mysql", app.Config.DSN.User+":"+app.Config.DSN.Pass+"@tcp("+app.Config.DSN.Slave1+":"+app.Config.DSN.Port+")/"+app.Config.DSN.Base+"?charset=utf8&collation=utf8_unicode_ci")
if err != nil {
return App{}, err
}
}
if app.Config.DSN.Slave2 != "" {
app.DBSlave2, err = sql.Open("mysql", app.Config.DSN.User+":"+app.Config.DSN.Pass+"@tcp("+app.Config.DSN.Slave2+":"+app.Config.DSN.Port+")/"+app.Config.DSN.Base+"?charset=utf8&collation=utf8_unicode_ci")
if err != nil {
return App{}, err
}
}
return app, nil
return App{Config: conf, DB: db}, nil
}
func configure(fileName string, envPrefix string) (*models.Configuration, error) {
@ -77,7 +60,7 @@ func dbInit(db *sql.DB) error {
City varchar(255) DEFAULT NULL,
Interests varchar(255) DEFAULT NULL,
PRIMARY KEY (Id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_unicode_ci`); err != nil {
) ENGINE=InnoDB DEFAULT CHARSET=utf8`); err != nil {
return err
}
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS relations (
@ -86,18 +69,6 @@ func dbInit(db *sql.DB) error {
) ENGINE=InnoDB DEFAULT CHARSET=utf8`); err != nil {
return err
}
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS posts (
Id INT(11) NOT NULL AUTO_INCREMENT,
Author INT(11) NULL DEFAULT NULL,
Created TIMESTAMP NULL DEFAULT NULL,
Subject VARCHAR(50) NULL DEFAULT NULL,
Body MEDIUMTEXT NULL DEFAULT NULL,
PRIMARY KEY (Id) USING BTREE,
INDEX AuthorID (author) USING BTREE,
CONSTRAINT AuthorID FOREIGN KEY (author) REFERENCES app.users (Id) ON UPDATE RESTRICT ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_unicode_ci`); err != nil {
return err
}
log.Println("All tables exists")
return nil
}

View File

@ -2,21 +2,19 @@ package auth
import (
"fmt"
"log"
"net/http"
"github.com/codegangsta/martini"
"github.com/codegangsta/martini-contrib/render"
"github.com/codegangsta/martini-contrib/sessions"
"github.com/tiburon-777/OTUS_HighLoad/internal/application"
"log"
"net/http"
)
// These are the default configuration values for this package. They
// can be set at anytime, probably during the initial setup of Martini.
var (
// RedirectURL should be the relative URL for your login route
RedirectURL string = "/login"
// RedirectUrl should be the relative URL for your login route
RedirectUrl string = "/login"
// RedirectParam is the query string parameter that will be set
// with the page the user was trying to visit before they were
@ -41,10 +39,10 @@ type User interface {
Logout()
// Return the unique identifier of this user object
UniqueID() interface{}
UniqueId() interface{}
// Populate this user object with values
GetByID(app application.App, id interface{}) error
GetById(app application.App, id interface{}) error
}
// SessionUser will try to read a unique user ID out of the session. Then it tries
@ -55,11 +53,11 @@ type User interface {
// user type.
func SessionUser(newUser func() User) martini.Handler {
return func(s sessions.Session, c martini.Context, l *log.Logger, app application.App) {
userID := s.Get(SessionKey)
userId := s.Get(SessionKey)
user := newUser()
if userID != nil {
err := user.GetByID(app, userID)
if userId != nil {
err := user.GetById(app, userId)
if err != nil {
l.Printf("Login Error: %v\n", err)
} else {
@ -90,8 +88,8 @@ func Logout(s sessions.Session, user User) {
// authenticated, they will be redirected to /login with the "next" get parameter
// set to the attempted URL.
func LoginRequired(r render.Render, user User, req *http.Request) {
if !user.IsAuthenticated() {
path := fmt.Sprintf("%s?%s=%s", RedirectURL, RedirectParam, req.URL.Path)
if user.IsAuthenticated() == false {
path := fmt.Sprintf("%s?%s=%s", RedirectUrl, RedirectParam, req.URL.Path)
r.Redirect(path, 302)
}
}
@ -99,6 +97,6 @@ func LoginRequired(r render.Render, user User, req *http.Request) {
// UpdateUser updates the User object stored in the session. This is useful incase a change
// is made to the user model that needs to persist across requests.
func UpdateUser(s sessions.Session, user User) error {
s.Set(SessionKey, user.UniqueID())
s.Set(SessionKey, user.UniqueId())
return nil
}

View File

@ -2,13 +2,12 @@ package auth
import (
"fmt"
"time"
"github.com/tiburon-777/OTUS_HighLoad/internal/application"
"time"
)
type UserModel struct {
ID int64 `db:"id" form:"id"`
Id int64 `db:"id" form:"id"`
Username string `db:"username" form:"username"`
Password string `db:"password" form:"password"`
Name string `db:"name" form:"name"`
@ -46,14 +45,14 @@ func (u *UserModel) IsAuthenticated() bool {
return u.authenticated
}
func (u *UserModel) UniqueID() interface{} {
return u.ID
func (u *UserModel) UniqueId() interface{} {
return u.Id
}
func (u *UserModel) GetByID(app application.App, id interface{}) error {
func (u *UserModel) GetById(app application.App, id interface{}) error {
var v string
query := fmt.Sprintf("SELECT username, name, surname, birthdate, gender, city, interests FROM users WHERE id=%d", id)
err := app.DBMaster.QueryRow(query).Scan(&u.Username, &u.Name, &u.Surname, &v, &u.Gender, &u.City, &u.Interests)
err := app.DB.QueryRow(query).Scan(&u.Username, &u.Name, &u.Surname, &v, &u.Gender, &u.City, &u.Interests)
if err != nil {
return err
}
@ -61,6 +60,6 @@ func (u *UserModel) GetByID(app application.App, id interface{}) error {
if err != nil {
return err
}
u.ID = id.(int64)
u.Id = id.(int64)
return nil
}

View File

@ -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)
}

View File

@ -2,28 +2,18 @@ package handlers
import (
"fmt"
"log"
"net/http"
"time"
"github.com/codegangsta/martini-contrib/render"
"github.com/codegangsta/martini-contrib/sessions"
"golang.org/x/crypto/bcrypt"
// MySQL driver
_ "github.com/go-sql-driver/mysql"
"github.com/tiburon-777/OTUS_HighLoad/internal/application"
"github.com/tiburon-777/OTUS_HighLoad/internal/auth"
"golang.org/x/crypto/bcrypt"
"log"
"net/http"
"strconv"
"time"
)
type Post struct{
ID int `db:"Id"`
Author string `db:"Author"`
Created time.Time `db:"Created"`
Subject string `db:"Subject"`
Body string `db:"Body"`
}
func GetHome(app application.App, r render.Render, user auth.User) {
h := user.(*auth.UserModel).BirthDate
user.(*auth.UserModel).YearsOld = int(time.Since(h).Hours() / 8760)
@ -32,7 +22,7 @@ func GetHome(app application.App, r render.Render, user auth.User) {
var users []auth.UserModel
var tmp auth.UserModel
var tmpTime string
var results, err = app.DBMaster.Query(`SELECT
var results, err = app.DB.Query(`SELECT
users.id as id,
users.name as name,
users.surname as surname,
@ -45,13 +35,13 @@ func GetHome(app application.App, r render.Render, user auth.User) {
relations.friendId=users.Id
AND relations.userId=?
GROUP BY users.Id`,
user.(*auth.UserModel).ID)
user.(*auth.UserModel).Id)
if err != nil || results == nil {
err500("can't get user list from DB: ", err, r)
}
defer results.Close()
for results.Next() {
err = results.Scan(&tmp.ID, &tmp.Name, &tmp.Surname, &tmpTime, &tmp.Gender, &tmp.City)
err = results.Scan(&tmp.Id, &tmp.Name, &tmp.Surname, &tmpTime, &tmp.Gender, &tmp.City)
if err != nil {
err500("can't scan result from DB: ", err, r)
}
@ -61,23 +51,6 @@ func GetHome(app application.App, r render.Render, user auth.User) {
}
doc["table"] = users
var post Post
var posts []Post
results, err = app.DBMaster.Query(`SELECT Id, Created, Subject, Body FROM posts WHERE Author=? ORDER BY Created DESC;`, user.(*auth.UserModel).ID)
if err != nil || results == nil {
err500("can't get user list from DB: ", err, r)
}
defer results.Close()
for results.Next() {
err = results.Scan(&post.ID, &tmpTime, &post.Subject, &post.Body)
if err != nil {
err500("can't scan result from DB: ", err, r)
}
post.Created = str2Time(tmpTime, r)
posts = append(posts, post)
}
doc["posts"] = posts
r.HTML(200, "index", doc)
}
@ -106,7 +79,7 @@ func PostSignup(app application.App, postedUser auth.UserModel, r render.Render)
if err != nil {
err500("can't generate password hash: ", err, r)
}
_, err = app.DBMaster.Exec(`INSERT INTO users (username, password, name, surname, birthdate, gender, city, interests)
_, err = app.DB.Exec(`INSERT INTO users (username, password, name, surname, birthdate, gender, city, interests)
values (?, ?, ?, ?, ?, ?, ?, ?)`,
postedUser.Username,
pHash,
@ -123,26 +96,182 @@ func PostSignup(app application.App, postedUser auth.UserModel, r render.Render)
r.Redirect("/login")
}
func GetUserList(app application.App, r render.Render) {
doc := make(map[string]interface{})
doc["UsersFound"] = 0
var tmp int
if err := app.DB.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&tmp); err != nil {
err500("can't get total of user profiles from DB: ", err, r)
}
doc["UsersTotal"] = tmp
r.HTML(200, "list", doc)
}
func PostUserList(app application.App, user auth.User, r render.Render, req *http.Request) {
postName := req.FormValue("name")
postSurname := req.FormValue("surname")
doc := make(map[string]interface{})
doc["user"] = user.(*auth.UserModel)
var users []auth.UserModel
var tmp auth.UserModel
var tmpTime string
var results, err = app.DB.Query(`SELECT
users.id as id,
users.name as name,
users.surname as surname,
users.birthdate as birthdate,
users.gender as gender,
users.city as city
FROM
users
WHERE
NOT users.id=?
AND users.id NOT IN (
SELECT
relations.friendId
FROM
relations
WHERE
relations.userId=?)
AND ( users.Name LIKE concat(?, '%') AND users.Surname LIKE concat(?, '%') )`,
user.(*auth.UserModel).Id,
user.(*auth.UserModel).Id,
postName,
postSurname,
)
if err != nil || results == nil {
err500("can't get user list from DB: ", err, r)
}
defer results.Close()
for results.Next() {
err = results.Scan(&tmp.Id, &tmp.Name, &tmp.Surname, &tmpTime, &tmp.Gender, &tmp.City)
if err != nil {
err500("can't scan result from DB: ", err, r)
}
tmp.BirthDate = str2Time(tmpTime, r)
tmp.YearsOld = int(time.Since(tmp.BirthDate).Hours() / 8760)
users = append(users, tmp)
if len(users) >= 100 {
doc["msg"] = "( Too much rows in result. We will display only the first 100. )"
break
}
}
doc["table"] = users
doc["UsersFound"] = len(users)
var uTotal int
if err := app.DB.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&uTotal); err != nil {
err500("can't get total of user profiles from DB: ", err, r)
}
doc["UsersTotal"] = uTotal
r.HTML(200, "list", doc)
}
func PostUserSearch(app application.App, r render.Render, req *http.Request) {
postName := req.FormValue("name")
postSurname := req.FormValue("surname")
doc := make(map[string]interface{})
var users []auth.UserModel
var tmp auth.UserModel
var tmpTime string
var results, err = app.DB.Query(`SELECT
users.id as id,
users.name as name,
users.surname as surname,
users.birthdate as birthdate,
users.gender as gender,
users.city as city
FROM
users
WHERE
( users.Name LIKE concat(?, '%') AND users.Surname LIKE concat(?, '%') )`,
postName,
postSurname,
)
if err != nil || results == nil {
err500("can't get user list from DB: ", err, r)
}
defer results.Close()
for results.Next() {
err = results.Scan(&tmp.Id, &tmp.Name, &tmp.Surname, &tmpTime, &tmp.Gender, &tmp.City)
if err != nil {
err500("can't scan result from DB: ", err, r)
}
tmp.BirthDate = str2Time(tmpTime, r)
tmp.YearsOld = int(time.Since(tmp.BirthDate).Hours() / 8760)
users = append(users, tmp)
if len(users) >= 100 {
doc["msg"] = "( Too much rows in result. We will display only the first 100. )"
break
}
}
doc["table"] = users
doc["UsersFound"] = len(users)
var uTotal int
if err := app.DB.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&uTotal); err != nil {
err500("can't get total of user profiles from DB: ", err, r)
}
doc["UsersTotal"] = uTotal
r.HTML(200, "list", doc)
}
func PostLogin(app application.App, session sessions.Session, postedUser auth.UserModel, r render.Render, req *http.Request) {
user := auth.UserModel{}
err1 := app.DBMaster.QueryRow("SELECT id, password FROM users WHERE username=?", postedUser.Username).Scan(&user.ID, &user.Password)
err1 := app.DB.QueryRow("SELECT id, password FROM users WHERE username=?", postedUser.Username).Scan(&user.Id, &user.Password)
err2 := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(postedUser.Password))
if err1 != nil || err2 != nil {
doc := map[string]interface{}{
"msg": "Wrong user or password. You may sign in.",
}
r.HTML(200, "login", doc)
r.Redirect(auth.RedirectURL)
r.Redirect(auth.RedirectUrl)
return
} else {
err := auth.AuthenticateSession(session, &user)
if err != nil {
err500("can't auth session: ", err, r)
}
params := req.URL.Query()
redirect := params.Get(auth.RedirectParam)
r.Redirect(redirect)
return
}
err := auth.AuthenticateSession(session, &user)
if err != nil {
err500("can't auth session: ", err, r)
}
func GetSubscribe(app application.App, r render.Render, user auth.User, req *http.Request) {
sid, ok := req.URL.Query()["id"]
if !ok {
err500("can't parce URL query", nil, r)
}
params := req.URL.Query()
redirect := params.Get(auth.RedirectParam)
r.Redirect(redirect)
return
did, err := strconv.Atoi(sid[0])
if err != nil {
err500("can't convert URL query value: ", err, r)
}
_, err = app.DB.Exec(`REPLACE INTO relations (userId, friendId) values (?, ?)`, user.(*auth.UserModel).Id, did)
if err != nil {
err500("can't create relation in DB: ", err, r)
}
_, err = app.DB.Exec(`REPLACE INTO relations (userId, friendId) values (?, ?)`, did, user.(*auth.UserModel).Id)
if err != nil {
err500("can't create relation in DB: ", err, r)
}
r.Redirect("/list")
}
func GetUnSubscribe(app application.App, r render.Render, user auth.User, req *http.Request) {
sid, ok := req.URL.Query()["id"]
if !ok {
err500("can't parce URL query", nil, r)
}
did, err := strconv.Atoi(sid[0])
if err != nil {
err500("can't convert URL query value: ", err, r)
}
_, err = app.DB.Exec(`DELETE FROM relations WHERE (userId,friendId) IN ((?, ?),(?, ?))`, user.(*auth.UserModel).Id, did, did, user.(*auth.UserModel).Id)
if err != nil {
err500("can't remove relation from DB: ", err, r)
}
r.Redirect("/")
}
func str2Time(s string, r render.Render) time.Time {
@ -154,7 +283,7 @@ func str2Time(s string, r render.Render) time.Time {
}
func err500(s string, err error, r render.Render) {
e := fmt.Errorf("%s %w", s, err)
e := fmt.Errorf("s% %w", s, err)
log.Println(e)
doc := map[string]interface{}{
"Error": e,

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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("/")
}

View File

@ -1,8 +1,8 @@
package models
type Configuration struct {
Server Server
DSN DSN
Server Server
DSN DSN
}
type Server struct {
@ -11,11 +11,9 @@ type Server struct {
}
type DSN struct {
Master string
Slave1 string
Slave2 string
Port string
User string
Pass string
Base string
Host string
Port string
User string
Pass string
Base string
}

View File

@ -2,13 +2,12 @@ package dataset
import (
"database/sql"
"github.com/mdigger/translit"
"log"
"math/rand"
"strconv"
"strings"
"time"
"github.com/mdigger/translit"
)
type Person struct {
@ -31,13 +30,9 @@ func NewPerson() (p Person) {
p.FirstName = womanNames[rand.Intn(len(womanNames))]
p.SecondName = secondNames[rand.Intn(len(secondNames))] + "а"
}
charSet := "abcdedfghijklmnopqrstABCDEFGHIJKLMNOP0123456789"
var output strings.Builder
for i := 0; i < 16; i++ {
random := rand.Intn(len(charSet))
output.WriteString(string(charSet[random]))
}
p.Password = output.String()
t := make([]byte, 16)
rand.Read(t)
p.Password = string(t)
p.City = cities[rand.Intn(len(cities))]
for i := 0; i < (rand.Intn(4) + 3); i++ {
p.Interests = append(p.Interests, interests[rand.Intn(len(interests))])
@ -54,7 +49,7 @@ func FillDB(db *sql.DB, lim int) {
}
uCount = lim - uCount
if uCount <= 0 {
log.Printf("Ok. We have more users then %d.", lim)
log.Printf("Ok. We have more users then %s.", lim)
}
log.Printf("Try to generate %d rows and fill the DB...", uCount)
for i := 1; i < uCount; i++ {

View File

@ -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>

View File

@ -25,12 +25,6 @@
<p>You now live in <b>{{ .user.City }}</b></p>
<p>You interests is: <b>{{ .user.Interests }}</b></p>
<h2>Available actions:</h2>
<input type="button" onclick="location.href='/addPost';" value="Create Post" />
<input type="button" onclick="location.href='/feed';" value="Friend Feed" />
<input type="button" onclick="location.href='/list';" value="User list" />
<input type="button" onclick="location.href='/logout';" value="Logout" /><br />
<h2> You have friends:</h2>
<table>
<thead>
@ -55,11 +49,8 @@
</tbody>
</table>
<h2> Your posts:</h2>
{{ range $post:=.posts }}
<a><b>{{ $post.Subject }}</b><br/><a>{{ $post.Body }}</a>
<br/><br/>
{{ end }}
<h2>Available actions:</h2>
<input type="button" onclick="location.href='/list';" value="User list" />
<input type="button" onclick="location.href='/logout';" value="Logout" /><br />
</body>
</html>

View File

@ -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>

View File

@ -1,45 +0,0 @@
#ЗАЧТЕНА: [ЧАТ](https://otus.ru/learning/61597/#/homework-chat/12337/) / [РАБОЧИЙ СЕРВИС](http://lab.tiburon.su:8080/)
-----
# Заготовка для социальной сети
Цель: В результате выполнения ДЗ вы создадите базовый скелет социальной сети, который будет развиваться в дальнейших ДЗ.
###В данном задании тренируются навыки:
- декомпозиции предметной области;
- построения элементарной архитектуры проекта
Требуется разработать создание и просмотр анект в социальной сети.
###Функциональные требования:
- Авторизация по паролю.
- Страница регистрации, где указывается следующая информация:
- Имя
- Фамилия
- Возраст
- Пол
- Интересы
- Город
- Страницы с анкетой.
###Нефункциональные требования:
- Любой язык программирования
- В качестве базы данных использовать MySQL
- Не использовать ORM
- Программа должна представлять из себя монолитное приложение.
- Не рекомендуется использовать следующие технологии:
- Репликация
- Шардинг
- Индексы
- Кэширование
Верстка не важна. Подойдет самая примитивная.
Разместить приложение на любом хостинге. Например, heroku.
ДЗ принимается в виде исходного кода на github и демонстрации проекта на хостинге.
Критерии оценки: Оценка происходит по принципу зачет/незачет.
###Требования:
- Есть возможность регистрации, создавать персональные страницы, возможность подружиться, список друзей.
- Отсутствуют SQL-инъекции.
- Пароль хранится безопасно.

13
test/dz002.sh Normal file
View File

@ -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

View File

@ -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

View File

@ -1,44 +0,0 @@
### 1. Графики Latency и Kb/s от кол-ва одновременных запросов, до и после установки индексов.
![mountains](img/diagramms.jpg "Диаграммы нагрузки")
### 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
```
![mountains](img/explain1.jpg "Explain запроса с прямым индексом")
### 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
```
![mountains](img/explain2.jpg "Explain запроса с обратным индексом")
#### 4. Резюме
Для конечной оптимизации используется составной индекс `('Surname', 'Name')` потому, что в запросе используется объединение AND условий LIKE ?%. Mysql в этом случае ищет по составному индексу и объединяет строки без сортировки. Именно поэтому, приходится принудительно сортировать результаты. Порядок полей выбран с точки зрения селективности. В реальной обстановке поле «Фамилия» все же более селективно чем «Имя». Плюс к этому, мы видим в EXPLAIN запроса, что в случае использования индекса `('Surname', 'Name')` mysql применяет Multi-Range Read оптимизацию, позволяющую линеаризовать процедуру чтения с диска. Возможно за этот счет значительно повысилась скорость передачи данных? по сравнению с индексом `('Name', 'Surname')`.

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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
```
- #### Получаем график нагрузки:
![График нагрузки на контейнерах приложения](img/dz003_part1.jpg "График нагрузки на контейнерах приложения")
### 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 строки.

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -1,27 +0,0 @@
#НЕ НАЧАТА: [ЧАТ](https://otus.ru/learning/61597/#/homework-chat/12342/) / [ОТЧЕТ](REPORT.md)
-----
# Масштабируемая подсистема диалогов
В результате выполнения ДЗ вы создадите базовый скелет микросервиса, который будет развиваться в дальнейших ДЗ.
### В данном задании тренируются навыки:
- декомпозиции предметной области;
- построения элементарной архитектуры проекта;
### План выполнения:
1) Необходимо написать систему диалогов между пользователями.
2) Обеспечить горизонтальное масштабирование хранилищ на запись с помощью шардинга.
3) Предусмотреть:
- Возможность решардинга.
- “Эффект Леди Гаги” (один пользователь пишет сильно больше среднего).
- Наиболее эффективную схему.
ДЗ принимается в виде исходного кода на github и отчета по выполненной работе.
Требования:
- Верно выбран ключ шардирования с учетом "эффекта Леди Гаги"
- В отчете описан процесс решардинга без даунтайма
###Рекомендуем сдать до: 17.11.2021

View File

@ -1,29 +0,0 @@
#В ПРОЦЕССЕ: [ЧАТ](https://otus.ru/learning/61597/#/homework-chat/12340/) / [ОТЧЕТ](REPORT.md)
-----
#Лента новостей социальной сети
Цель: В результате выполнения ДЗ вы создадите ленту новостей социальной сети
### В данном задании тренируются навыки:
- работа с кешами;
- работа с очередями;
- проектирование масштабируемых архитектур.
### План выполнения:
1) Разработать страницу добавления поста.
2) Разработать ленту новостей, содержащую посты пользователей на которых подписан текущий пользователь.
- Создается отдельная страница, куда пишутся все обновления друзей. Для этого нужно хранить подписчиков.
- Лента формируется на уровне кешей.
- Формирование ленты производить через постановку задачи в очередь на часть подписчиков, чтобы избежать эффекта леди Гаги.
- В ленте держать последние 1000 обновлений друзей.
- Лента должна кешироваться.
ДЗ сдается в виде ссылки на github и демонстрации работающего проекта, развернутого в интернете.
Критерии оценки: Оценка происходит по принципу зачет/незачет.
### Требования:
- Верно работает инвалидация кеша.
- Обновление лент работает через очередь.
- Есть возможность перестройки кешей из СУБД.
### Рекомендуем сдать до: 17.02.2021

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,26 +0,0 @@
#НЕ НАЧАТА: [ЧАТ](https://otus.ru/learning/61597/#/homework-chat/12346/) / [ОТЧЕТ](REPORT.md)
-----
# Сервис счетчиков
Цель: В результате выполнения ДЗ вы создадите сервис счетчиков. Сервис будет хранить такие счетчики, как число непрочитанных сообщений.
### В данном задании тренируются навыки:
- разработка отказоустойчивых сервисов;
- использование кешей;
### План выполнения:
1) Разработайте сервис счетчиков.
2) Учтите то, что на этот сервис будет большая нагрузка, особенно на чтение.
3) Продумайте, как обеспечить консистентность между счетчиком и реальным числом непрочитанных сообщений. Например, используйте паттерн SAGA.
4) Внедрите сервис для отображения счетчиков.
ДЗ сдается в виде демонстрации работоспособности сервиса, ссылки на репозиторий github, отчета по архитектуре.
Критерии оценки: Оценка происходит по принципу зачет/незачет.
### Требования:
- Верно описан выбранный паттерн обеспечения консистентности.
- Выбранная архитектура сервиса подходит для решения задачи.
### Рекомендуем сдать до: 24.01.2022

View File

@ -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

View File

@ -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

View File

@ -1,21 +0,0 @@
#НЕ НАЧАТА: [ЧАТ](https://otus.ru/learning/61597/#/homework-chat/12349/) / [ОТЧЕТ](REPORT.md)
-----
# Разработать MVP по данной архитектуре
### Варианты проектов:
- Новостной ресурс
- Сайт знакомств
- Ресурс для персональных блогов
- Интернет-магазин
- Или любой другое проект, кроме социальной сети
Необходимо выбрать тему проекта и отправить её в чат с преподавателем.
### План выполнения:
1) В начале проекта необходимо с наставником согласовать требования.
2) Разработать MVP по данной архитектуре.
3) Итогом будет защита архитектуры и MVP
Критерии оценки: Готовый проект + защита проекта

View File

@ -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>'

View File

@ -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
}

View File

@ -1,3 +0,0 @@
GF_SECURITY_ADMIN_USER=admin
GF_SECURITY_ADMIN_PASSWORD=changeme
GF_USERS_ALLOW_SIGN_UP=false

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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"
}
}
]
}

View File

@ -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"
}

View File

@ -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

View File

@ -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}}."

View File

@ -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']