# Основы микросервисной архитектуры ## Навигация - [🏠 Главная страница курса](../../../README.md) - [📚 Модуль 2](../README.md) --- ## Определение микросервисов **Микросервис** — это небольшая, независимая, слабо связанная служба, которая: - Имеет единственную ответственность - Развертывается независимо - Имеет собственные данные - Коммуницирует через четко определенные API ## Принципы микросервисной архитектуры ### 1. Single Responsibility Principle Каждый микросервис должен решать одну бизнес-задачу. **Пример:** ``` User Service → управление пользователями Payment Service → обработка платежей Order Service → управление заказами Notification Service → отправка уведомлений ``` **Технические примеры:** **Java (Spring Boot) - User Service:** ```java // User Service - только управление пользователями @SpringBootApplication public class UserServiceApplication { public static void main(String[] args) { SpringApplication.run(UserServiceApplication.class, args); } } @RestController @RequestMapping("/api/users") public class UserController { @Autowired private UserService userService; @PostMapping public ResponseEntity createUser(@RequestBody CreateUserRequest request) { User user = userService.createUser(request); return ResponseEntity.ok(user); } @GetMapping("/{id}") public ResponseEntity getUser(@PathVariable Long id) { User user = userService.getUser(id); return ResponseEntity.ok(user); } @PutMapping("/{id}") public ResponseEntity updateUser(@PathVariable Long id, @RequestBody UpdateUserRequest request) { User user = userService.updateUser(id, request); return ResponseEntity.ok(user); } @DeleteMapping("/{id}") public ResponseEntity deleteUser(@PathVariable Long id) { userService.deleteUser(id); return ResponseEntity.ok().build(); } } @Service @Transactional public class UserService { @Autowired private UserRepository userRepository; public User createUser(CreateUserRequest request) { // Валидация if (userRepository.existsByEmail(request.getEmail())) { throw new UserAlreadyExistsException("User with email already exists"); } // Создание пользователя User user = new User(); user.setName(request.getName()); user.setEmail(request.getEmail()); user.setPasswordHash(passwordEncoder.encode(request.getPassword())); user.setCreatedAt(LocalDateTime.now()); return userRepository.save(user); } public User getUser(Long id) { return userRepository.findById(id) .orElseThrow(() -> new UserNotFoundException("User not found")); } public User updateUser(Long id, UpdateUserRequest request) { User user = getUser(id); user.setName(request.getName()); user.setUpdatedAt(LocalDateTime.now()); return userRepository.save(user); } public void deleteUser(Long id) { User user = getUser(id); userRepository.delete(user); } } @Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false) private String email; @Column(nullable = false) private String name; @Column(name = "password_hash", nullable = false) private String passwordHash; @Column(name = "created_at", nullable = false) private LocalDateTime createdAt; @Column(name = "updated_at") private LocalDateTime updatedAt; // геттеры и сеттеры } ``` **Payment Service - только обработка платежей:** ```java // Payment Service - только обработка платежей @SpringBootApplication public class PaymentServiceApplication { public static void main(String[] args) { SpringApplication.run(PaymentServiceApplication.class, args); } } @RestController @RequestMapping("/api/payments") public class PaymentController { @Autowired private PaymentService paymentService; @PostMapping("/process") public ResponseEntity processPayment(@RequestBody PaymentRequest request) { PaymentResult result = paymentService.processPayment(request); return ResponseEntity.ok(result); } @GetMapping("/{id}") public ResponseEntity getPayment(@PathVariable Long id) { Payment payment = paymentService.getPayment(id); return ResponseEntity.ok(payment); } @PostMapping("/{id}/refund") public ResponseEntity refundPayment(@PathVariable Long id, @RequestBody RefundRequest request) { RefundResult result = paymentService.refundPayment(id, request); return ResponseEntity.ok(result); } } @Service @Transactional public class PaymentService { @Autowired private PaymentRepository paymentRepository; @Autowired private PaymentGatewayClient paymentGatewayClient; public PaymentResult processPayment(PaymentRequest request) { // Валидация платежа validatePaymentRequest(request); // Создание записи о платеже Payment payment = new Payment(); payment.setAmount(request.getAmount()); payment.setCurrency(request.getCurrency()); payment.setPaymentMethod(request.getPaymentMethod()); payment.setStatus(PaymentStatus.PENDING); payment.setCreatedAt(LocalDateTime.now()); payment = paymentRepository.save(payment); try { // Обработка через платежный шлюз PaymentGatewayResponse response = paymentGatewayClient.processPayment(request); if (response.isSuccess()) { payment.setStatus(PaymentStatus.COMPLETED); payment.setTransactionId(response.getTransactionId()); payment.setProcessedAt(LocalDateTime.now()); } else { payment.setStatus(PaymentStatus.FAILED); payment.setFailureReason(response.getErrorMessage()); } payment = paymentRepository.save(payment); return new PaymentResult( payment.getId(), payment.getStatus(), response.getTransactionId(), response.getErrorMessage() ); } catch (Exception e) { payment.setStatus(PaymentStatus.FAILED); payment.setFailureReason(e.getMessage()); paymentRepository.save(payment); throw new PaymentProcessingException("Payment processing failed", e); } } private void validatePaymentRequest(PaymentRequest request) { if (request.getAmount().compareTo(BigDecimal.ZERO) <= 0) { throw new InvalidPaymentAmountException("Payment amount must be positive"); } if (request.getPaymentMethod() == null) { throw new InvalidPaymentMethodException("Payment method is required"); } } public Payment getPayment(Long id) { return paymentRepository.findById(id) .orElseThrow(() -> new PaymentNotFoundException("Payment not found")); } public RefundResult refundPayment(Long id, RefundRequest request) { Payment payment = getPayment(id); if (payment.getStatus() != PaymentStatus.COMPLETED) { throw new InvalidRefundException("Only completed payments can be refunded"); } // Логика возврата средств RefundGatewayResponse response = paymentGatewayClient.refundPayment( payment.getTransactionId(), request.getAmount() ); if (response.isSuccess()) { payment.setStatus(PaymentStatus.REFUNDED); payment.setRefundedAt(LocalDateTime.now()); paymentRepository.save(payment); } return new RefundResult(response.isSuccess(), response.getRefundId()); } } @Entity @Table(name = "payments") public class Payment { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private BigDecimal amount; @Column(nullable = false) private String currency; @Enumerated(EnumType.STRING) @Column(nullable = false) private PaymentMethod paymentMethod; @Enumerated(EnumType.STRING) @Column(nullable = false) private PaymentStatus status; @Column(name = "transaction_id") private String transactionId; @Column(name = "failure_reason") private String failureReason; @Column(name = "created_at", nullable = false) private LocalDateTime createdAt; @Column(name = "processed_at") private LocalDateTime processedAt; @Column(name = "refunded_at") private LocalDateTime refundedAt; // геттеры и сеттеры } ``` **Node.js (Express + TypeScript + Prisma) - User Service:** ```typescript // User Service - только управление пользователями import express, { Request, Response, NextFunction } from 'express'; import { PrismaClient } from '@prisma/client'; import { z } from 'zod'; import bcrypt from 'bcrypt'; const app = express(); const prisma = new PrismaClient(); // Interfaces interface CreateUserRequest { name: string; email: string; password: string; } interface UpdateUserRequest { name: string; } interface User { id: number; name: string; email: string; passwordHash: string; createdAt: Date; updatedAt: Date | null; } // User Controller class UserController { constructor(private userService: UserService) {} async createUser(req: Request, res: Response, next: NextFunction) { try { const user = await this.userService.createUser(req.body); res.json(user); } catch (error) { next(error); } } async getUser(req: Request, res: Response, next: NextFunction) { try { const id = parseInt(req.params.id); const user = await this.userService.getUser(id); res.json(user); } catch (error) { next(error); } } async updateUser(req: Request, res: Response, next: NextFunction) { try { const id = parseInt(req.params.id); const user = await this.userService.updateUser(id, req.body); res.json(user); } catch (error) { next(error); } } async deleteUser(req: Request, res: Response, next: NextFunction) { try { const id = parseInt(req.params.id); await this.userService.deleteUser(id); res.status(204).send(); } catch (error) { next(error); } } } // User Service class UserService { constructor(private userRepository: UserRepository) {} async createUser(request: CreateUserRequest): Promise { // Валидация const existingUser = await this.userRepository.findByEmail(request.email); if (existingUser) { throw new Error('User with email already exists'); } // Создание пользователя const passwordHash = await bcrypt.hash(request.password, 10); const user = { name: request.name, email: request.email, passwordHash, createdAt: new Date(), updatedAt: null }; return await this.userRepository.save(user); } async getUser(id: number): Promise { const user = await this.userRepository.findById(id); if (!user) { throw new Error('User not found'); } return user; } async updateUser(id: number, request: UpdateUserRequest): Promise { const user = await this.getUser(id); user.name = request.name; user.updatedAt = new Date(); return await this.userRepository.save(user); } async deleteUser(id: number): Promise { const user = await this.getUser(id); await this.userRepository.delete(user.id); } } // User Repository interface UserRepository { save(user: Omit): Promise; findById(id: number): Promise; findByEmail(email: string): Promise; delete(id: number): Promise; } class PrismaUserRepository implements UserRepository { async save(user: Omit): Promise { const savedUser = await prisma.user.create({ data: { name: user.name, email: user.email, passwordHash: user.passwordHash, createdAt: user.createdAt, updatedAt: user.updatedAt } }); return { id: savedUser.id, name: savedUser.name, email: savedUser.email, passwordHash: savedUser.passwordHash, createdAt: savedUser.createdAt, updatedAt: savedUser.updatedAt }; } async findById(id: number): Promise { const user = await prisma.user.findUnique({ where: { id } }); if (!user) return null; return { id: user.id, name: user.name, email: user.email, passwordHash: user.passwordHash, createdAt: user.createdAt, updatedAt: user.updatedAt }; } async findByEmail(email: string): Promise { const user = await prisma.user.findUnique({ where: { email } }); if (!user) return null; return { id: user.id, name: user.name, email: user.email, passwordHash: user.passwordHash, createdAt: user.createdAt, updatedAt: user.updatedAt }; } async delete(id: number): Promise { await prisma.user.delete({ where: { id } }); } } // Validation schemas const createUserSchema = z.object({ name: z.string().min(1, 'Name is required'), email: z.string().email('Valid email is required'), password: z.string().min(6, 'Password must be at least 6 characters') }); const updateUserSchema = z.object({ name: z.string().min(1, 'Name is required') }); // Dependency injection const userRepository = new PrismaUserRepository(); const userService = new UserService(userRepository); const userController = new UserController(userService); // Routes app.post('/api/users', async (req: Request, res: Response, next: NextFunction) => { try { const validatedData = createUserSchema.parse(req.body); await userController.createUser({ ...req, body: validatedData }, res, next); } catch (error) { if (error instanceof z.ZodError) { res.status(400).json({ error: error.errors }); } else { next(error); } } }); app.get('/api/users/:id', (req: Request, res: Response, next: NextFunction) => { userController.getUser(req, res, next); }); app.put('/api/users/:id', async (req: Request, res: Response, next: NextFunction) => { try { const validatedData = updateUserSchema.parse(req.body); await userController.updateUser({ ...req, body: validatedData }, res, next); } catch (error) { if (error instanceof z.ZodError) { res.status(400).json({ error: error.errors }); } else { next(error); } } }); app.delete('/api/users/:id', (req: Request, res: Response, next: NextFunction) => { userController.deleteUser(req, res, next); }); // Error handling middleware app.use((error: Error, req: Request, res: Response, next: NextFunction) => { console.error(error); res.status(500).json({ error: 'Internal server error' }); }); app.listen(3001, () => { console.log('User Service running on port 3001'); }); ``` **Rust (Actix Web + Diesel) - User Service:** ```rust // User Service - только управление пользователями use actix_web::{web, App, HttpResponse, HttpServer, Result}; use diesel::prelude::*; use serde::{Deserialize, Serialize}; use uuid::Uuid; #[derive(Serialize, Deserialize)] struct CreateUserRequest { name: String, email: String, password: String, } #[derive(Serialize, Deserialize)] struct UpdateUserRequest { name: String, } #[derive(Serialize, Deserialize, Queryable)] struct User { id: Uuid, name: String, email: String, password_hash: String, created_at: chrono::DateTime, updated_at: Option>, } // User Service struct UserService { pool: diesel::r2d2::Pool>, } impl UserService { fn new(database_url: &str) -> Self { let manager = diesel::r2d2::ConnectionManager::::new(database_url); let pool = diesel::r2d2::Pool::builder() .build(manager) .expect("Failed to create pool"); Self { pool } } async fn create_user(&self, request: CreateUserRequest) -> Result> { use crate::schema::users::dsl::*; // Проверка существования пользователя let existing_user = users .filter(email.eq(&request.email)) .first::(&mut self.pool.get()?) .optional()?; if existing_user.is_some() { return Err("User with email already exists".into()); } // Хеширование пароля let password_hash = bcrypt::hash(&request.password, bcrypt::DEFAULT_COST)?; let new_user = NewUser { id: Uuid::new_v4(), name: request.name, email: request.email, password_hash, created_at: chrono::Utc::now(), updated_at: None, }; let user = diesel::insert_into(users) .values(&new_user) .get_result::(&mut self.pool.get()?)?; Ok(user) } async fn get_user(&self, user_id: Uuid) -> Result> { use crate::schema::users::dsl::*; let user = users .filter(id.eq(user_id)) .first::(&mut self.pool.get()?)?; Ok(user) } async fn update_user(&self, user_id: Uuid, request: UpdateUserRequest) -> Result> { use crate::schema::users::dsl::*; let user = diesel::update(users.filter(id.eq(user_id))) .set(( name.eq(request.name), updated_at.eq(Some(chrono::Utc::now())), )) .get_result::(&mut self.pool.get()?)?; Ok(user) } async fn delete_user(&self, user_id: Uuid) -> Result<(), Box> { use crate::schema::users::dsl::*; diesel::delete(users.filter(id.eq(user_id))) .execute(&mut self.pool.get()?)?; Ok(()) } } #[derive(Insertable)] #[diesel(table_name = users)] struct NewUser { id: Uuid, name: String, email: String, password_hash: String, created_at: chrono::DateTime, updated_at: Option>, } // Handlers async fn create_user( user_service: web::Data, request: web::Json, ) -> Result { match user_service.create_user(request.into_inner()).await { Ok(user) => Ok(HttpResponse::Ok().json(user)), Err(e) => Ok(HttpResponse::BadRequest().json(serde_json::json!({ "error": e.to_string() }))), } } async fn get_user( user_service: web::Data, path: web::Path, ) -> Result { let user_id = path.into_inner(); match user_service.get_user(user_id).await { Ok(user) => Ok(HttpResponse::Ok().json(user)), Err(_) => Ok(HttpResponse::NotFound().json(serde_json::json!({ "error": "User not found" }))), } } async fn update_user( user_service: web::Data, path: web::Path, request: web::Json, ) -> Result { let user_id = path.into_inner(); match user_service.update_user(user_id, request.into_inner()).await { Ok(user) => Ok(HttpResponse::Ok().json(user)), Err(e) => Ok(HttpResponse::BadRequest().json(serde_json::json!({ "error": e.to_string() }))), } } async fn delete_user( user_service: web::Data, path: web::Path, ) -> Result { let user_id = path.into_inner(); match user_service.delete_user(user_id).await { Ok(_) => Ok(HttpResponse::NoContent().finish()), Err(e) => Ok(HttpResponse::BadRequest().json(serde_json::json!({ "error": e.to_string() }))), } } #[actix_web::main] async fn main() -> std::io::Result<()> { let database_url = std::env::var("DATABASE_URL") .expect("DATABASE_URL must be set"); let user_service = UserService::new(&database_url); HttpServer::new(move || { App::new() .app_data(web::Data::new(user_service.clone())) .route("/api/users", web::post().to(create_user)) .route("/api/users/{id}", web::get().to(get_user)) .route("/api/users/{id}", web::put().to(update_user)) .route("/api/users/{id}", web::delete().to(delete_user)) }) .bind("127.0.0.1:3001")? .run() .await } ``` **Python (FastAPI + SQLAlchemy) - User Service:** ```python # User Service - только управление пользователей from fastapi import FastAPI, HTTPException, Depends from sqlalchemy import create_engine, Column, String, DateTime, Boolean from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker, Session from pydantic import BaseModel, EmailStr from passlib.context import CryptContext from datetime import datetime from typing import Optional import uuid app = FastAPI(title="User Service", version="1.0.0") # Database setup DATABASE_URL = "postgresql://user:password@localhost/userdb" engine = create_engine(DATABASE_URL) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base() # Password hashing pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") # Database Models class User(Base): __tablename__ = "users" id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) name = Column(String, nullable=False) email = Column(String, unique=True, nullable=False) password_hash = Column(String, nullable=False) created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, nullable=True) # Pydantic Models class CreateUserRequest(BaseModel): name: str email: EmailStr password: str class UpdateUserRequest(BaseModel): name: str class UserResponse(BaseModel): id: str name: str email: str created_at: datetime updated_at: Optional[datetime] class Config: from_attributes = True # Dependency def get_db(): db = SessionLocal() try: yield db finally: db.close() # User Service class UserService: def __init__(self, db: Session): self.db = db def create_user(self, request: CreateUserRequest) -> User: # Проверка существования пользователя existing_user = self.db.query(User).filter(User.email == request.email).first() if existing_user: raise HTTPException(status_code=400, detail="User with email already exists") # Хеширование пароля password_hash = pwd_context.hash(request.password) # Создание пользователя user = User( name=request.name, email=request.email, password_hash=password_hash ) self.db.add(user) self.db.commit() self.db.refresh(user) return user def get_user(self, user_id: str) -> User: user = self.db.query(User).filter(User.id == user_id).first() if not user: raise HTTPException(status_code=404, detail="User not found") return user def update_user(self, user_id: str, request: UpdateUserRequest) -> User: user = self.get_user(user_id) user.name = request.name user.updated_at = datetime.utcnow() self.db.commit() self.db.refresh(user) return user def delete_user(self, user_id: str) -> None: user = self.get_user(user_id) self.db.delete(user) self.db.commit() # API Endpoints @app.post("/api/users", response_model=UserResponse) async def create_user( request: CreateUserRequest, db: Session = Depends(get_db) ): user_service = UserService(db) user = user_service.create_user(request) return user @app.get("/api/users/{user_id}", response_model=UserResponse) async def get_user( user_id: str, db: Session = Depends(get_db) ): user_service = UserService(db) user = user_service.get_user(user_id) return user @app.put("/api/users/{user_id}", response_model=UserResponse) async def update_user( user_id: str, request: UpdateUserRequest, db: Session = Depends(get_db) ): user_service = UserService(db) user = user_service.update_user(user_id, request) return user @app.delete("/api/users/{user_id}") async def delete_user( user_id: str, db: Session = Depends(get_db) ): user_service = UserService(db) user_service.delete_user(user_id) return {"message": "User deleted successfully"} if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=3001) ``` ### 2. Decentralized Governance - Каждая команда выбирает свои технологии - Нет единого технологического стека - Самостоятельное принятие решений ### 3. Decentralized Data Management - Каждый сервис владеет своими данными - Нет единой базы данных - Разные типы БД для разных сервисов ### 4. Infrastructure Automation - Автоматизированное развертывание - CI/CD пайплайны - Мониторинг и логирование ### 5. Design for Failure - Сервисы могут отказывать - Необходима отказоустойчивость - Circuit breaker паттерны ## Преимущества микросервисов ### Технические преимущества - **Независимое развертывание** — можно обновлять сервисы отдельно - **Технологическое разнообразие** — разные технологии для разных задач - **Масштабируемость** — масштабирование только нужных сервисов - **Отказоустойчивость** — отказ одного сервиса не влияет на другие ### Организационные преимущества - **Независимые команды** — каждая команда работает со своим сервисом - **Быстрая разработка** — параллельная работа команд - **Специализация** — команды могут специализироваться на доменах ## Недостатки микросервисов ### Сложность - **Распределенная система** — сложнее отладка и мониторинг - **Сетевая задержка** — коммуникация через сеть - **Сложность данных** — распределенные транзакции ### Операционные проблемы - **Мониторинг** — нужно отслеживать множество сервисов - **Развертывание** — сложность управления множеством сервисов - **Тестирование** — интеграционное тестирование усложняется ## Когда использовать микросервисы ### Подходящие случаи: - Большие команды (8+ разработчиков) - Сложные домены - Высокие требования к масштабируемости - Необходимость независимого развертывания - Разные технологические требования ### Неподходящие случаи: - Маленькие команды - Простые приложения - Низкие требования к масштабируемости - Недостаток DevOps экспертизы