마이크로서비스 오케스트레이션을 위해 Temporal을 사용한 정교한 주문 처리 시스템 구현에 관한 포괄적인 블로그 시리즈의 첫 번째 부분에 오신 것을 환영합니다. 이 시리즈에서는 복잡하고 장기 실행되는 워크플로를 처리할 수 있는 강력하고 확장 가능하며 유지 관리 가능한 시스템을 구축하는 과정의 복잡함을 살펴보겠습니다.
우리의 여정은 프로젝트의 기반을 마련하는 것부터 시작됩니다. 이 게시물이 끝나면 Golang에서 구현되고 워크플로 조정을 위해 Temporal과 통합되고 Postgres 데이터베이스에서 지원되는 완전한 기능의 CRUD REST API를 갖게 됩니다. 우리는 최신 도구와 모범 사례를 사용하여 코드베이스를 깔끔하고 효율적이며 유지 관리하기 쉽게 만들 것입니다.
자세히 알아보고 주문 처리 시스템 구축을 시작해 보세요!
구현을 시작하기 전에 사용할 주요 기술과 개념을 간략하게 살펴보겠습니다.
Go는 단순성, 효율성 및 탁월한 동시 프로그래밍 지원으로 잘 알려진 정적으로 유형이 지정되고 컴파일된 언어입니다. 표준 라이브러리와 강력한 에코시스템은 마이크로서비스 구축을 위한 탁월한 선택입니다.
Temporal은 분산 애플리케이션 개발을 단순화하는 마이크로서비스 오케스트레이션 플랫폼입니다. 이를 통해 복잡하고 오래 실행되는 워크플로를 간단한 절차 코드로 작성하여 실패와 재시도를 자동으로 처리할 수 있습니다.
Gin은 Go로 작성된 고성능 HTTP 웹 프레임워크입니다. 훨씬 더 나은 성능과 낮은 메모리 사용량을 갖춘 마티니와 유사한 API를 제공합니다.
OpenAPI(이전의 Swagger)는 RESTful 웹 서비스를 설명, 생성, 소비 및 시각화하기 위한 기계 판독 가능 인터페이스 파일에 대한 사양입니다. oapi-codegen은 OpenAPI 3.0 사양에서 Go 코드를 생성하는 도구로, 이를 통해 API 계약을 먼저 정의하고 서버 스텁 및 클라이언트 코드를 생성할 수 있습니다.
sqlc는 SQL에서 유형이 안전한 Go 코드를 생성합니다. 이를 통해 일반 SQL 쿼리를 작성하고 완전히 유형이 안전한 Go 코드를 생성하여 데이터베이스와 상호 작용할 수 있으므로 런타임 오류 가능성이 줄어들고 유지 관리성이 향상됩니다.
PostgreSQL은 신뢰성, 기능 견고성 및 성능으로 잘 알려진 강력한 오픈 소스 객체 관계형 데이터베이스 시스템입니다.
Docker를 사용하면 애플리케이션과 해당 종속성을 컨테이너에 패키징하여 다양한 환경에서 일관성을 보장할 수 있습니다. docker-compose는 로컬 개발 환경을 설정하는 데 사용할 다중 컨테이너 Docker 애플리케이션을 정의하고 실행하기 위한 도구입니다.
이제 기본 사항을 알아보았으니 시스템 구현을 시작해 보겠습니다.
먼저 프로젝트 디렉터리를 만들고 기본 구조를 설정해 보겠습니다.
mkdir order-processing-system cd order-processing-system # Create directory structure mkdir -p cmd/api \ internal/api \ internal/db \ internal/models \ internal/service \ internal/workflow \ migrations \ pkg/logger \ scripts # Initialize Go module go mod init github.com/yourusername/order-processing-system # Create main.go file touch cmd/api/main.go
이 구조는 표준 Go 프로젝트 레이아웃을 따릅니다.
일반적인 작업을 단순화하기 위해 Makefile을 만들어 보겠습니다.
touch Makefile
Makefile에 다음 콘텐츠를 추가하세요.
.PHONY: generate build run test clean generate: @echo "Generating code..." go generate ./... build: @echo "Building..." go build -o bin/api cmd/api/main.go run: @echo "Running..." go run cmd/api/main.go test: @echo "Running tests..." go test -v ./... clean: @echo "Cleaning..." rm -rf bin .DEFAULT_GOAL := build
이 Makefile은 코드 생성, 애플리케이션 빌드, 실행, 테스트 실행 및 빌드 아티팩트 정리를 위한 대상을 제공합니다.
api/openapi.yaml이라는 파일을 생성하고 API 사양을 정의합니다.
openapi: 3.0.0 info: title: Order Processing API version: 1.0.0 description: API for managing orders in our processing system paths: /orders: get: summary: List all orders responses: '200': description: Successful response content: application/json: schema: type: array items: $ref: '#/components/schemas/Order' post: summary: Create a new order requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateOrderRequest' responses: '201': description: Created content: application/json: schema: $ref: '#/components/schemas/Order' /orders/{id}: get: summary: Get an order by ID parameters: - name: id in: path required: true schema: type: integer responses: '200': description: Successful response content: application/json: schema: $ref: '#/components/schemas/Order' '404': description: Order not found put: summary: Update an order parameters: - name: id in: path required: true schema: type: integer requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateOrderRequest' responses: '200': description: Successful response content: application/json: schema: $ref: '#/components/schemas/Order' '404': description: Order not found delete: summary: Delete an order parameters: - name: id in: path required: true schema: type: integer responses: '204': description: Successful response '404': description: Order not found components: schemas: Order: type: object properties: id: type: integer customer_id: type: integer status: type: string enum: [pending, processing, completed, cancelled] total_amount: type: number created_at: type: string format: date-time updated_at: type: string format: date-time CreateOrderRequest: type: object required: - customer_id - total_amount properties: customer_id: type: integer total_amount: type: number UpdateOrderRequest: type: object properties: status: type: string enum: [pending, processing, completed, cancelled] total_amount: type: number
이 사양은 주문에 대한 기본 CRUD 작업을 정의합니다.
oapi-codegen 설치:
go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@latest
Generate the server code:
oapi-codegen -package api -generate types,server,spec api/openapi.yaml > internal/api/api.gen.go
This command generates the Go code for our API, including types, server interfaces, and the OpenAPI specification.
Create a new file internal/api/handler.go:
package api import ( "net/http" "github.com/gin-gonic/gin" ) type Handler struct { // We'll add dependencies here later } func NewHandler() *Handler { return &Handler{} } func (h *Handler) RegisterRoutes(r *gin.Engine) { RegisterHandlers(r, h) } // Implement the ServerInterface methods func (h *Handler) GetOrders(c *gin.Context) { // TODO: Implement c.JSON(http.StatusOK, []Order{}) } func (h *Handler) CreateOrder(c *gin.Context) { var req CreateOrderRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // TODO: Implement order creation logic order := Order{ Id: 1, CustomerId: req.CustomerId, Status: "pending", TotalAmount: req.TotalAmount, } c.JSON(http.StatusCreated, order) } func (h *Handler) GetOrder(c *gin.Context, id int) { // TODO: Implement c.JSON(http.StatusOK, Order{Id: id}) } func (h *Handler) UpdateOrder(c *gin.Context, id int) { var req UpdateOrderRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // TODO: Implement order update logic order := Order{ Id: id, Status: *req.Status, } c.JSON(http.StatusOK, order) } func (h *Handler) DeleteOrder(c *gin.Context, id int) { // TODO: Implement c.Status(http.StatusNoContent) }
This implementation provides a basic structure for our API handlers. We’ll flesh out the actual logic when we integrate with the database and Temporal workflows.
Create a docker-compose.yml file in the project root:
version: '3.8' services: postgres: image: postgres:13 environment: POSTGRES_USER: orderuser POSTGRES_PASSWORD: orderpass POSTGRES_DB: orderdb ports: - "5432:5432" volumes: - postgres_data:/var/lib/postgresql/data volumes: postgres_data:
This sets up a Postgres container for our local development environment.
Install golang-migrate:
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
Create our first migration:
migrate create -ext sql -dir migrations -seq create_orders_table
Edit the migrations/000001_create_orders_table.up.sql file:
CREATE TABLE orders ( id SERIAL PRIMARY KEY, customer_id INTEGER NOT NULL, status VARCHAR(20) NOT NULL, total_amount DECIMAL(10, 2) NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_orders_customer_id ON orders(customer_id); CREATE INDEX idx_orders_status ON orders(status);
Edit the migrations/000001_create_orders_table.down.sql file:
DROP TABLE IF EXISTS orders;
Add a new target to our Makefile:
migrate-up: @echo "Running migrations..." migrate -path migrations -database "postgresql://orderuser:orderpass@localhost:5432/orderdb?sslmode=disable" up migrate-down: @echo "Reverting migrations..." migrate -path migrations -database "postgresql://orderuser:orderpass@localhost:5432/orderdb?sslmode=disable" down
Now we can run migrations with:
make migrate-up
go install github.com/kyleconroy/sqlc/cmd/sqlc@latest
Create a sqlc.yaml file in the project root:
version: "2" sql: - engine: "postgresql" queries: "internal/db/queries.sql" schema: "migrations" gen: go: package: "db" out: "internal/db" emit_json_tags: true emit_prepared_queries: false emit_interface: true emit_exact_table_names: false
Create a file internal/db/queries.sql:
-- name: GetOrder :one SELECT * FROM orders WHERE id = $1 LIMIT 1; -- name: ListOrders :many SELECT * FROM orders ORDER BY id; -- name: CreateOrder :one INSERT INTO orders ( customer_id, status, total_amount ) VALUES ( $1, $2, $3 ) RETURNING *; -- name: UpdateOrder :one UPDATE orders SET status = $2, total_amount = $3, updated_at = CURRENT_TIMESTAMP WHERE id = $1 RETURNING *; -- name: DeleteOrder :exec DELETE FROM orders WHERE id = $1;
Add a new target to our Makefile:
generate-sqlc: @echo "Generating sqlc code..." sqlc generate
Run the code generation:
make generate-sqlc
This will generate Go code for interacting with our database in the internal/db directory.
Add Temporal to our docker-compose.yml:
temporal: image: temporalio/auto-setup:1.13.0 ports: - "7233:7233" environment: - DB=postgresql - DB_PORT=5432 - POSTGRES_USER=orderuser - POSTGRES_PWD=orderpass - POSTGRES_SEEDS=postgres depends_on: - postgres temporal-admin-tools: image: temporalio/admin-tools:1.13.0 depends_on: - temporal
Create a file internal/workflow/order_workflow.go:
package workflow import ( "time" "go.temporal.io/sdk/workflow" "github.com/yourusername/order-processing-system/internal/db" ) func OrderWorkflow(ctx workflow.Context, order db.Order) error { logger := workflow.GetLogger(ctx) logger.Info("OrderWorkflow started", "OrderID", order.ID) // Simulate order processing err := workflow.Sleep(ctx, 5*time.Second) if err != nil { return err } // Update order status err = workflow.ExecuteActivity(ctx, UpdateOrderStatus, workflow.ActivityOptions{ StartToCloseTimeout: time.Minute, }, order.ID, "completed").Get(ctx, nil) if err != nil { return err } logger.Info("OrderWorkflow completed", "OrderID", order.ID) return nil } func UpdateOrderStatus(ctx workflow.Context, orderID int64, status string) error { // TODO: Implement database update return nil }
This basic workflow simulates order processing by waiting for 5 seconds and then updating the order status to “completed”.
Update the internal/api/handler.go file to include Temporal client and start the workflow:
package api import ( "context" "net/http" "github.com/gin-gonic/gin" "go.temporal.io/sdk/client" "github.com/yourusername/order-processing-system/internal/db" "github.com/yourusername/order-processing-system/internal/workflow" ) type Handler struct { queries *db.Queries temporalClient client.Client } func NewHandler(queries *db.Queries, temporalClient client.Client) *Handler { return &Handler{ queries: queries, temporalClient: temporalClient, } } // ... (previous handler methods) func (h *Handler) CreateOrder(c *gin.Context) { var req CreateOrderRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } order, err := h.queries.CreateOrder(c, db.CreateOrderParams{ CustomerID: req.CustomerId, Status: "pending", TotalAmount: req.TotalAmount, }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Start Temporal workflow workflowOptions := client.StartWorkflowOptions{ ID: "order-" + order.ID, TaskQueue: "order-processing", } _, err = h.temporalClient.ExecuteWorkflow(context.Background(), workflowOptions, workflow.OrderWorkflow, order) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to start workflow"}) return } c.JSON(http.StatusCreated, order) } // ... (implement other handler methods)
Create a new file internal/service/service.go:
package service import ( "database/sql" "github.com/yourusername/order-processing-system/internal/api" "github.com/yourusername/order-processing-system/internal/db" "go.temporal.io/sdk/client" ) type Service struct { DB *sql.DB Queries *db.Queries TemporalClient client.Client Handler *api.Handler } func NewService() (*Service, error) { // Initialize database connection db, err := sql.Open("postgres", "postgresql://orderuser:orderpass@localhost:5432/orderdb?sslmode=disable") if err != nil { return nil, err } // Initialize Temporal client temporalClient, err := client.NewClient(client.Options{ HostPort: "localhost:7233", }) if err != nil { return nil, err } // Initialize queries queries := db.New(db) // Initialize handler handler := api.NewHandler(queries, temporalClient) return &Service{ DB: db, Queries: queries, TemporalClient: temporalClient, Handler: handler, }, nil } func (s *Service) Close() { s.DB.Close() s.TemporalClient.Close() }
Update the cmd/api/main.go file:
package main import ( "log" "github.com/gin-gonic/gin" _ "github.com/lib/pq" "github.com/yourusername/order-processing-system/internal/service" ) func main() { svc, err := service.NewService() if err != nil { log.Fatalf("Failed to initialize service: %v", err) } defer svc.Close() r := gin.Default() svc.Handler.RegisterRoutes(r) if err := r.Run(":8080"); err != nil { log.Fatalf("Failed to run server: %v", err) } }
Create a Dockerfile in the project root:
# Build stage FROM golang:1.17-alpine AS build WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -o /order-processing-system ./cmd/api # Run stage FROM alpine:latest WORKDIR / COPY --from=build /order-processing-system /order-processing-system EXPOSE 8080 ENTRYPOINT ["/order-processing-system"]
Update the docker-compose.yml file to include our application:
version: '3.8' services: postgres: # ... (previous postgres configuration) temporal: # ... (previous temporal configuration) temporal-admin-tools: # ... (previous temporal-admin-tools configuration) app: build: . ports: - "8080:8080" depends_on: - postgres - temporal environment: - DB_HOST=postgres - DB_USER=orderuser - DB_PASSWORD=orderpass - DB_NAME=orderdb - TEMPORAL_HOST=temporal:7233
Throughout the implementation guide, we’ve provided code snippets with explanations. Here’s a more detailed look at a key part of our system: the Order Workflow.
package workflow import ( "time" "go.temporal.io/sdk/workflow" "github.com/yourusername/order-processing-system/internal/db" ) // OrderWorkflow defines the workflow for processing an order func OrderWorkflow(ctx workflow.Context, order db.Order) error { logger := workflow.GetLogger(ctx) logger.Info("OrderWorkflow started", "OrderID", order.ID) // Simulate order processing // In a real-world scenario, this could involve multiple activities such as // inventory check, payment processing, shipping arrangement, etc. err := workflow.Sleep(ctx, 5*time.Second) if err != nil { return err } // Update order status // We use ExecuteActivity to run the status update as an activity // This allows for automatic retries and error handling err = workflow.ExecuteActivity(ctx, UpdateOrderStatus, workflow.ActivityOptions{ StartToCloseTimeout: time.Minute, }, order.ID, "completed").Get(ctx, nil) if err != nil { return err } logger.Info("OrderWorkflow completed", "OrderID", order.ID) return nil } // UpdateOrderStatus is an activity that updates the status of an order func UpdateOrderStatus(ctx workflow.Context, orderID int64, status string) error { // TODO: Implement database update // In a real implementation, this would use the db.Queries to update the order status return nil }
This workflow demonstrates several key concepts:
For this initial setup, we’ll focus on manual testing to ensure our system is working as expected. In future posts, we’ll dive into unit testing, integration testing, and end-to-end testing strategies.
To manually test our system:
docker-compose up
Use a tool like cURL or Postman to send requests to our API:
Check the logs to ensure the Temporal workflow is being triggered and completed successfully.
While setting up this initial version of our order processing system, we encountered several challenges and considerations:
Database Schema Design : Designing a flexible yet efficient schema for orders is crucial. We kept it simple for now, but in a real-world scenario, we might need to consider additional tables for order items, customer information, etc.
Error Handling : Our current implementation has basic error handling. In a production system, we’d need more robust error handling and logging, especially for the Temporal workflows.
Configuration Management : We hardcoded configuration values for simplicity. In a real-world scenario, we’d use environment variables or a configuration management system.
Keselamatan : Persediaan semasa kami tidak termasuk sebarang pengesahan atau kebenaran. Dalam sistem pengeluaran, kami perlu melaksanakan langkah keselamatan yang betul.
Skalabiliti : Walaupun Temporal membantu dengan kebolehskalaan aliran kerja, kami perlu mempertimbangkan kebolehskalaan pangkalan data dan prestasi API untuk sistem trafik tinggi.
Pemantauan dan Kebolehmerhatian : Kami belum melaksanakan sebarang alat pemantauan atau kebolehmerhatian lagi. Dalam sistem pengeluaran, ini akan menjadi penting untuk menyelenggara dan menyelesaikan masalah aplikasi.
Dalam bahagian pertama siri kami ini, kami telah menyediakan asas untuk sistem pemprosesan pesanan kami. Kami mempunyai API CRUD asas, penyepaduan pangkalan data dan aliran kerja Temporal yang ringkas.
Dalam bahagian seterusnya, kita akan menyelami lebih mendalam aliran kerja dan aktiviti Temporal. Kami akan meneroka:
Kami juga akan mula menyempurnakan API kami dengan logik pemprosesan pesanan yang lebih realistik dan meneroka corak untuk mengekalkan kod yang bersih dan boleh diselenggara apabila sistem kami semakin kompleks.
Nantikan Bahagian 2, di mana kami akan membawa sistem pemprosesan pesanan kami ke peringkat seterusnya!
Adakah anda menghadapi masalah yang mencabar, atau memerlukan perspektif luaran tentang idea atau projek baharu? Saya boleh tolong! Sama ada anda ingin membina konsep bukti teknologi sebelum membuat pelaburan yang lebih besar, atau anda memerlukan panduan tentang isu yang sukar, saya sedia membantu.
Jika anda berminat untuk bekerja dengan saya, sila hubungi melalui e-mel di hungaikevin@gmail.com.
Mari jadikan cabaran anda sebagai peluang!
위 내용은 주문 처리 시스템 구현: 기반 구축 부분의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!