Kinh nghiệm triển khai Microservices
tại Sapo.vn
Nguyễn Minh Khôi
CTO DKT Technology
dkt.com.vn
Agenda
• Giới thiệu về Sapo
• Kiến trúc Microservices
• Bài toán nghiệp vụ của Sapo
2
Giới thiệu về Sapo
3
4
5
Kiến trúc Microservices
6
Programming Languages
Frameworks & Libraries
Web ServersDatabases
Message Queues
|S3, EC2, Route53
Cloud Services
Others
Sapo Tech Stack
7
Sapo Microservices Components
• Dựa trên Spring Boot & Netflix OSS
• Service Discovery: Eureka (Server), Ribbon (Client)
• API Gateway: Zuul
• Centralized Configuration: Spring Cloud Config
• API Security: Spring Security & Spring Security OAuth
• REST API: Spring Boot
• Job Service: Kafka & Spring Boot
8
Sapo Microservices Architecture
9
Jobs Services
Nginx
Backend
(.NET MVC)
API
Gateway
(Zuul)
OAuth Server
(Spring OAuth)
Browser
3rd
Apps
Kafka
Eureka
Server
Redis
Session
Application
Services
(Spring Boot)
Config Server
Job Services
Webhooks
Service
RabbitMQ
Mobile
app
Routing & Service Discovery
10
11
/admin/orders.json
Routing Request
???
???
Request trong hệ thống Microservices
• Service chạy trên nhiều máy
chủ, ở nhiều port khác nhau
• Các service có thể bật, tắt,
bổ sung bất cứ lúc nào
→ Làm sao để biết service ở
đâu để gọi?
12
Service Discovery - Eureka
• Mỗi 1 service được định danh bằng
serviceId
• Service sử dụng Eureka Client để tương
tác với Eureka Server:
• Register: đăng ký mới (serviceId, host,
port)
• Renew: sử dụng heartbeats định kỳ đăng
ký lại để biết service còn hoạt động
• Get Registry: trả về danh sách host:port
của các service theo serviceId
13
Request vào hệ
thống Microservices
• Kết nối vào hệ thống trở nên
phức tạp do phải biết
serviceId, phải kết nối đến
Service Discovery
• Nginx không biết được sự thay
đổi của các service để gọi &
cân tải
→ Làm sao để đơn giản hóa việc
gọi vào các microservices?
14
API Gateway - Zuul
• Địa chỉ truy xuất duy nhất để gọi vào các microservices
• Zuul là edge service -> không dùng để các microservices gọi lẫn nhau
• Zuul sử dụng Ribbon để gọi tới các service
• Eureka Client
• Client Load Balancer
• Caching
• ZuulFilter: tiền xử lý request trước khi gọi sang các service
15
API Gateway & Service Discovery
16
Get Registry
order
10.0.0.1:8080
10.0.0.2:8081
10.0.0.3:8082
Centralized Configuration
17
Centralized Configuration
18
Vấn đề:
- Cấu hình phân tán, khó
kiểm soát
- Các service có 1 số cấu
hình chung, thay đổi là
phải đổi hàng loạt
- Reload config khi đang
chạy
Centralized Configuration
19
Spring Cloud Config
• Thông tin file cấu hình được lưu trong git hoặc file vật lý
• Tên file: {serviceId}.yml
• Nhiều môi trường: dev, test, stage, live...
• Kế thừa từ application.yml: mọi service đều lấy cấu hình
chung
• File bootstrap.yml: cấu hình serviceId, config server, môi
trường
• Reload Config: HTTP GET [service-host:port]/refresh
• Spring Cloud Bus – auto reload
20
Authentication & Authorization
21
Đối tượng yêu cầu truy cập
• 1st Party: Backend, quản trị hệ thống...: toàn quyền truy xuất mọi
website
• 3rd Party: ứng dụng của các nhà phát triển cung cấp thêm tính năng
cho chủ cửa hàng: chủ cửa hàng cấp quyền truy xuất từng tài nguyên
• Mobile app: đăng nhập để lấy token truy xuất, quyền theo tài khoản
đăng nhập
→Cần 1 phương thức xác thực & kiểm tra quyền linh hoạt -> OAuth2 +
Spring Security
22
23
Ghi chú:
• 3rd Apps: xác thực OAuth qua
Access Token
• Sapo Mobile App: Client Credential
• Backend: Client Credential, Session
Credential (Share session giữa
Backend và OAuth Server).
• 3rd App: có quyền trên mọi cửa
hàng , Sapo Mobile App +
Backend: Có quyền trên cửa hàng
được cấu hình
CI/CD với Docker Swarm
& Jenkins
24
1 Dockerfile cho mọi service
FROM frolvlad/alpine-oraclejdk8:slim
ADD lib lib
ADD order.jar app.jar
RUN sh -c 'touch /app.jar'
ENTRYPOINT ["java","-Xmx128m","-Xms128m","-
Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
25
CI/CD với Jenkins & Docker
• Sử dụng Spotify docker-maven-plugin:
• Tách lớp thư viện ra thư mục riêng & cache được layer này, giảm kích thước
image (~75MB)
• Giảm network traffic & thời gian deploy (~200-700KB dữ liệu mỗi lần update)
• Tự động gán tag Docker image:
{git_commit_short_code}-{branch} -> 4b4a71ef-dev
26
Kết hợp Netflix OSS với Docker Swarm
• Zuul làm API Gateway
• Eureka làm Service Discovery
• Ribbon Client để gọi giữa các microservices
• Docker Swarm:
• Quản lý các microservices
• Deploy, scale, update các microservices
27
28
1. Dev push code lên Gitlab
2. Gitlab webhook
gọi sang Jenkins
3. Jenkins pull code từ Gitlab:
- mã nguồn
- Dockerfile
- Jenkinsfile
4. Biên dịch & build Docker
image, tự động gắn tag image
5. Push image lên
Private Docker Registry
6. Bắn webhook sang Jenkins
server khác, thực hiện lệnh
docker service update
Bài toán xử lý nghiệp vụ của
SAPO
29
Yêu cầu với sản phẩm SAPO
• Đáp ứng nhiều nghiệp vụ
• Nghiệp vụ phức tạp
• Maintainable code
• Tính đúng đắn dữ liệu
• Performance
→ lựa chọn Domain Driven Design (DDD) & xử lý bất đồng bộ qua
Message Queue
30
Tại sao chọn DDD?
• Kiểm soát tốt nghiệp vụ trong code
• Code có thể maintain được
• Dễ dàng sửa chữa khi có lỗi hoặc nghiệp vụ thay đổi
• Đảm bảo tính đúng đắn dữ liệu do toàn bộ Aggregate được lưu trong
1 transaction
31
Một số khái niệm
• Entity: đối tượng được định danh
• Aggregate: một nhóm các entity có quan hệ với nhau phản ánh
nghiệp vụ của domain
• Aggregate Root: là entity đại diện cho Aggregate
• Sub Entity: các entity còn lại trong Aggregate
• Repository: lưu trữ aggregate và truy xuất dữ liệu
• Domain Event: các event xảy ra trong Aggregate Root
32
33
Identity trong DDD
• Entity cần được định danh luôn khi khởi tạo
• Kết quả của nghiệp vụ được lưu trữ qua 1 transaction (Repository
save nguyên Aggregate Root)
• Có nhiều nghiệp vụ map các entity & sub entity, cần biết Id của nhau.
Ví dụ: Order tạo Fulfillment -> event fulfillment_created, data
của event cần có FulfillmentId
→ SQL Server support cơ chế generate Id
SELECT NEXT VALUE FOR SeqOrderId
EXEC sp_sequence_get_range
34
Xử lý xung đột khi update dữ liệu
• Cùng 1 thời điểm có nhiều người dùng cập nhật 1 entity.
• Ví dụ: cùng thời điểm 1 nhân viên xác nhận đơn hàng, 1 nhân viên
khác hủy đơn hàng. Cần đảm bảo ai cập nhật trước thành công,
người cập nhật sau thất bại.
→ Optimistic Lock cho Aggregate Root
Đánh version cho bảng Order, mọi thao tác update lên Order hoặc bất
cứ Sub Entity nào trong Order Aggregate cũng sẽ thay đổi order version
35
Tối ưu update cho Repository
• 1 Aggregate chỉ được lưu bằng 1 hàm duy nhất
• Order Domain có 8 Sub Entity liên quan -> update cho ít nhất 9 bảng
→ Cơ chế xác định sự thay đổi ở entity và sub entity, chỉ update entity
và những sub entity nào thay đổi.
Bất cứ thay đổi nào ở sub entity -> thay đổi ở entity
this.modify = true;
this.modifiedOn = Util.getUTC();
36
37
Sơ đồ nghiệp vụ cơ bản
38
Sơ đồ nghiệp vụ cơ bản
Kafka Message Queue
39
Tại sao dùng Kafka?
• Distributed Streaming Platform
• Hỗ trợ Pub/Sub
• High performance
• Đảm bảo consumer xử lý message đúng thứ tự
mà producer gửi trên 1 partition
• Đảm bảo ko mất message, tất cả các message
trong queue đều đc xử lý
• Exactly one message process
40
Exactly one message process
• Mỗi message chỉ được xử lý 1 lần ứng với 1 nghiệp vụ xác định
• Có 2 cách:
• Kafka hỗ trợ - lưu offset & data đẩy vào kafka trong cùng 1 transaction
• Tự implement
• SAPO tự implement do dữ liệu nghiệp vụ lưu trong DB
• Sử dụng transaction cho việc lưu offset đồng thời với việc lưu dữ liệu
nghiệp vụ
41
Vấn đề
42
Aggregate
Save
DB
Kafka
Không đảm bảo được
transaction khi lưu
đồng thời vào DB và
Kafka
Giải pháp: Kafka Connect
43
Aggregate
save
Kafka
ConnectDB
Kafka
Transaction
• Lưu Aggregate + message vào
DB trong cùng transaction
• Kafka connect đẩy msg từ db
vào kafka
→ performance ko tốt bằng đẩy
thằng vào kafka nhưng đảm bảo
chính xác
Ví dụ phân hệ bán hàng
• 2 màn hình bán hàng
• Bán hàng online
• Đồng bộ từ kênh bán hàng
• Nhân viên chọn hàng cho Khách (gọi điện, chat)
• Cần phải giao hàng cho khách
• Bán hàng tại quầy (POS)
• Khách mang hàng ra thanh toán
• Nhận hàng luôn tại quầy
44
45
46
Quy trình online - tuần tự
Tạo đơn Duyệt Đóng gói
Giao hàng,
Thanh toán
47
created event
ETL Job,
Webhook Job
finalized event
ETL Job,
Webhook Job,
Stock Job
fulfillment_added event
ETL Job,
Webhook Job,
Notification Job
fulfillment_received,
payment_added event
ETL Job,
Webhook Job,
Notification Job,
Stock Job,
Debt Job
Quy trình POS – 1 thao tác
48
fulfillment_received
payment_added
ETL Job,
Webhook Job,
Notification Job,
Stock Job,
Debt Job
Tạo đơn
Duyệt
Đóng gói
Xuất kho
Thanh toán
Kafka
created
finalized
fulfillment_added
Lợi ích sử dụng DDD & Message Queue
• 2 quy trình khác nhau nhưng cùng chung 1 Order Domain xử lý
• Linh hoạt trong việc thay đổi quy trình hoặc tạo ra quy trình mới
49
Hướng cải tiến hệ thống
• Đẩy thẳng dữ liệu vào Kafka không phải lưu vào DB
• Tăng Performance phần Order
50
Reference
• http://martinfowler.com/
• http://microservices.io/
• https://github.com/mfornos/awesome-microservices
• http://spring.io/projects
• https://netflix.github.io/
• https://www.slideshare.net/juminchoi/bizweb-microservices-
architecture
• https://www.slideshare.net/juminchoi/building-bizweb-
microservices-with-docker
51
52
• Business Analyst
• .NET Developer
• Java Developer
• Android Developer
Contact
• Nguyễn Minh Khôi – DKT Technology
• Email: khoinm@dkt.com.vn
• Facebook: https://fb.com/khoinguyen84
53
Thank you!
Q&A

Sapo Microservices Architecture

  • 1.
    Kinh nghiệm triểnkhai Microservices tại Sapo.vn Nguyễn Minh Khôi CTO DKT Technology dkt.com.vn
  • 2.
    Agenda • Giới thiệuvề Sapo • Kiến trúc Microservices • Bài toán nghiệp vụ của Sapo 2
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
    Programming Languages Frameworks &Libraries Web ServersDatabases Message Queues |S3, EC2, Route53 Cloud Services Others Sapo Tech Stack 7
  • 8.
    Sapo Microservices Components •Dựa trên Spring Boot & Netflix OSS • Service Discovery: Eureka (Server), Ribbon (Client) • API Gateway: Zuul • Centralized Configuration: Spring Cloud Config • API Security: Spring Security & Spring Security OAuth • REST API: Spring Boot • Job Service: Kafka & Spring Boot 8
  • 9.
    Sapo Microservices Architecture 9 JobsServices Nginx Backend (.NET MVC) API Gateway (Zuul) OAuth Server (Spring OAuth) Browser 3rd Apps Kafka Eureka Server Redis Session Application Services (Spring Boot) Config Server Job Services Webhooks Service RabbitMQ Mobile app
  • 10.
    Routing & ServiceDiscovery 10
  • 11.
  • 12.
    Request trong hệthống Microservices • Service chạy trên nhiều máy chủ, ở nhiều port khác nhau • Các service có thể bật, tắt, bổ sung bất cứ lúc nào → Làm sao để biết service ở đâu để gọi? 12
  • 13.
    Service Discovery -Eureka • Mỗi 1 service được định danh bằng serviceId • Service sử dụng Eureka Client để tương tác với Eureka Server: • Register: đăng ký mới (serviceId, host, port) • Renew: sử dụng heartbeats định kỳ đăng ký lại để biết service còn hoạt động • Get Registry: trả về danh sách host:port của các service theo serviceId 13
  • 14.
    Request vào hệ thốngMicroservices • Kết nối vào hệ thống trở nên phức tạp do phải biết serviceId, phải kết nối đến Service Discovery • Nginx không biết được sự thay đổi của các service để gọi & cân tải → Làm sao để đơn giản hóa việc gọi vào các microservices? 14
  • 15.
    API Gateway -Zuul • Địa chỉ truy xuất duy nhất để gọi vào các microservices • Zuul là edge service -> không dùng để các microservices gọi lẫn nhau • Zuul sử dụng Ribbon để gọi tới các service • Eureka Client • Client Load Balancer • Caching • ZuulFilter: tiền xử lý request trước khi gọi sang các service 15
  • 16.
    API Gateway &Service Discovery 16 Get Registry order 10.0.0.1:8080 10.0.0.2:8081 10.0.0.3:8082
  • 17.
  • 18.
    Centralized Configuration 18 Vấn đề: -Cấu hình phân tán, khó kiểm soát - Các service có 1 số cấu hình chung, thay đổi là phải đổi hàng loạt - Reload config khi đang chạy
  • 19.
  • 20.
    Spring Cloud Config •Thông tin file cấu hình được lưu trong git hoặc file vật lý • Tên file: {serviceId}.yml • Nhiều môi trường: dev, test, stage, live... • Kế thừa từ application.yml: mọi service đều lấy cấu hình chung • File bootstrap.yml: cấu hình serviceId, config server, môi trường • Reload Config: HTTP GET [service-host:port]/refresh • Spring Cloud Bus – auto reload 20
  • 21.
  • 22.
    Đối tượng yêucầu truy cập • 1st Party: Backend, quản trị hệ thống...: toàn quyền truy xuất mọi website • 3rd Party: ứng dụng của các nhà phát triển cung cấp thêm tính năng cho chủ cửa hàng: chủ cửa hàng cấp quyền truy xuất từng tài nguyên • Mobile app: đăng nhập để lấy token truy xuất, quyền theo tài khoản đăng nhập →Cần 1 phương thức xác thực & kiểm tra quyền linh hoạt -> OAuth2 + Spring Security 22
  • 23.
    23 Ghi chú: • 3rdApps: xác thực OAuth qua Access Token • Sapo Mobile App: Client Credential • Backend: Client Credential, Session Credential (Share session giữa Backend và OAuth Server). • 3rd App: có quyền trên mọi cửa hàng , Sapo Mobile App + Backend: Có quyền trên cửa hàng được cấu hình
  • 24.
    CI/CD với DockerSwarm & Jenkins 24
  • 25.
    1 Dockerfile chomọi service FROM frolvlad/alpine-oraclejdk8:slim ADD lib lib ADD order.jar app.jar RUN sh -c 'touch /app.jar' ENTRYPOINT ["java","-Xmx128m","-Xms128m","- Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"] 25
  • 26.
    CI/CD với Jenkins& Docker • Sử dụng Spotify docker-maven-plugin: • Tách lớp thư viện ra thư mục riêng & cache được layer này, giảm kích thước image (~75MB) • Giảm network traffic & thời gian deploy (~200-700KB dữ liệu mỗi lần update) • Tự động gán tag Docker image: {git_commit_short_code}-{branch} -> 4b4a71ef-dev 26
  • 27.
    Kết hợp NetflixOSS với Docker Swarm • Zuul làm API Gateway • Eureka làm Service Discovery • Ribbon Client để gọi giữa các microservices • Docker Swarm: • Quản lý các microservices • Deploy, scale, update các microservices 27
  • 28.
    28 1. Dev pushcode lên Gitlab 2. Gitlab webhook gọi sang Jenkins 3. Jenkins pull code từ Gitlab: - mã nguồn - Dockerfile - Jenkinsfile 4. Biên dịch & build Docker image, tự động gắn tag image 5. Push image lên Private Docker Registry 6. Bắn webhook sang Jenkins server khác, thực hiện lệnh docker service update
  • 29.
    Bài toán xửlý nghiệp vụ của SAPO 29
  • 30.
    Yêu cầu vớisản phẩm SAPO • Đáp ứng nhiều nghiệp vụ • Nghiệp vụ phức tạp • Maintainable code • Tính đúng đắn dữ liệu • Performance → lựa chọn Domain Driven Design (DDD) & xử lý bất đồng bộ qua Message Queue 30
  • 31.
    Tại sao chọnDDD? • Kiểm soát tốt nghiệp vụ trong code • Code có thể maintain được • Dễ dàng sửa chữa khi có lỗi hoặc nghiệp vụ thay đổi • Đảm bảo tính đúng đắn dữ liệu do toàn bộ Aggregate được lưu trong 1 transaction 31
  • 32.
    Một số kháiniệm • Entity: đối tượng được định danh • Aggregate: một nhóm các entity có quan hệ với nhau phản ánh nghiệp vụ của domain • Aggregate Root: là entity đại diện cho Aggregate • Sub Entity: các entity còn lại trong Aggregate • Repository: lưu trữ aggregate và truy xuất dữ liệu • Domain Event: các event xảy ra trong Aggregate Root 32
  • 33.
  • 34.
    Identity trong DDD •Entity cần được định danh luôn khi khởi tạo • Kết quả của nghiệp vụ được lưu trữ qua 1 transaction (Repository save nguyên Aggregate Root) • Có nhiều nghiệp vụ map các entity & sub entity, cần biết Id của nhau. Ví dụ: Order tạo Fulfillment -> event fulfillment_created, data của event cần có FulfillmentId → SQL Server support cơ chế generate Id SELECT NEXT VALUE FOR SeqOrderId EXEC sp_sequence_get_range 34
  • 35.
    Xử lý xungđột khi update dữ liệu • Cùng 1 thời điểm có nhiều người dùng cập nhật 1 entity. • Ví dụ: cùng thời điểm 1 nhân viên xác nhận đơn hàng, 1 nhân viên khác hủy đơn hàng. Cần đảm bảo ai cập nhật trước thành công, người cập nhật sau thất bại. → Optimistic Lock cho Aggregate Root Đánh version cho bảng Order, mọi thao tác update lên Order hoặc bất cứ Sub Entity nào trong Order Aggregate cũng sẽ thay đổi order version 35
  • 36.
    Tối ưu updatecho Repository • 1 Aggregate chỉ được lưu bằng 1 hàm duy nhất • Order Domain có 8 Sub Entity liên quan -> update cho ít nhất 9 bảng → Cơ chế xác định sự thay đổi ở entity và sub entity, chỉ update entity và những sub entity nào thay đổi. Bất cứ thay đổi nào ở sub entity -> thay đổi ở entity this.modify = true; this.modifiedOn = Util.getUTC(); 36
  • 37.
    37 Sơ đồ nghiệpvụ cơ bản
  • 38.
    38 Sơ đồ nghiệpvụ cơ bản
  • 39.
  • 40.
    Tại sao dùngKafka? • Distributed Streaming Platform • Hỗ trợ Pub/Sub • High performance • Đảm bảo consumer xử lý message đúng thứ tự mà producer gửi trên 1 partition • Đảm bảo ko mất message, tất cả các message trong queue đều đc xử lý • Exactly one message process 40
  • 41.
    Exactly one messageprocess • Mỗi message chỉ được xử lý 1 lần ứng với 1 nghiệp vụ xác định • Có 2 cách: • Kafka hỗ trợ - lưu offset & data đẩy vào kafka trong cùng 1 transaction • Tự implement • SAPO tự implement do dữ liệu nghiệp vụ lưu trong DB • Sử dụng transaction cho việc lưu offset đồng thời với việc lưu dữ liệu nghiệp vụ 41
  • 42.
    Vấn đề 42 Aggregate Save DB Kafka Không đảmbảo được transaction khi lưu đồng thời vào DB và Kafka
  • 43.
    Giải pháp: KafkaConnect 43 Aggregate save Kafka ConnectDB Kafka Transaction • Lưu Aggregate + message vào DB trong cùng transaction • Kafka connect đẩy msg từ db vào kafka → performance ko tốt bằng đẩy thằng vào kafka nhưng đảm bảo chính xác
  • 44.
    Ví dụ phânhệ bán hàng • 2 màn hình bán hàng • Bán hàng online • Đồng bộ từ kênh bán hàng • Nhân viên chọn hàng cho Khách (gọi điện, chat) • Cần phải giao hàng cho khách • Bán hàng tại quầy (POS) • Khách mang hàng ra thanh toán • Nhận hàng luôn tại quầy 44
  • 45.
  • 46.
  • 47.
    Quy trình online- tuần tự Tạo đơn Duyệt Đóng gói Giao hàng, Thanh toán 47 created event ETL Job, Webhook Job finalized event ETL Job, Webhook Job, Stock Job fulfillment_added event ETL Job, Webhook Job, Notification Job fulfillment_received, payment_added event ETL Job, Webhook Job, Notification Job, Stock Job, Debt Job
  • 48.
    Quy trình POS– 1 thao tác 48 fulfillment_received payment_added ETL Job, Webhook Job, Notification Job, Stock Job, Debt Job Tạo đơn Duyệt Đóng gói Xuất kho Thanh toán Kafka created finalized fulfillment_added
  • 49.
    Lợi ích sửdụng DDD & Message Queue • 2 quy trình khác nhau nhưng cùng chung 1 Order Domain xử lý • Linh hoạt trong việc thay đổi quy trình hoặc tạo ra quy trình mới 49
  • 50.
    Hướng cải tiếnhệ thống • Đẩy thẳng dữ liệu vào Kafka không phải lưu vào DB • Tăng Performance phần Order 50
  • 51.
    Reference • http://martinfowler.com/ • http://microservices.io/ •https://github.com/mfornos/awesome-microservices • http://spring.io/projects • https://netflix.github.io/ • https://www.slideshare.net/juminchoi/bizweb-microservices- architecture • https://www.slideshare.net/juminchoi/building-bizweb- microservices-with-docker 51
  • 52.
    52 • Business Analyst •.NET Developer • Java Developer • Android Developer
  • 53.
    Contact • Nguyễn MinhKhôi – DKT Technology • Email: [email protected] • Facebook: https://fb.com/khoinguyen84 53
  • 54.