Trong bài viết này

Trong hệ thống phân tán, retry là một trong những cách đơn giản nhất để cải thiện khả năng chịu lỗi. Nhưng khi nhiều service trong 1 luồng gọi qua và phụ thuộc nhau, nếu có 1 service bắt đầu lỗi và bắt đầu retry, số lượng request có thể tăng lên nhanh chóng theo cấp số nhân và góp phần vào sự cố trên toàn hệ thống.
Link bài viết gốc của Uber: https://www.uber.com/us/en/blog/protecting-against-retry-storms

1. Retry Storm - khi một lỗi nhỏ tạo ra thêm hàng nghìn request

Trong hệ thống phân tán, khi service gọi service, việc xảy ra lỗi chẳng hạn như timeout, connection reset hoặc mã HTTP 5xx... là khá bình thường. Có thể do mạng chập chờn, một service bị restart, mất kết nối db, hoặc đơn thuần chỉ là xử lý chậm hơn bình thường. Và để tăng khả năng request được xử lý thành công, chúng ta thường bổ sung cơ chế Retry. Ý tưởng rất đơn giản kiểu:

A gửi request sang B:

  • Nếu SUCCESS trả về result
  • Nếu FAILED thực hiện Retry
    • Retry Success trả về result
    • Retry Error thì mới chính thức trả về lỗi

Nếu lỗi chỉ là nhất thời, việc retry có thể khiến request được xử lý thành công mà người dùng không nhận ra là có lỗi. Tuy nhiên, mọi chuyện sẽ trở nên phức tạp hơn khi hệ thống được chưa thành nhiều microservices.

1.1. Việc retry ở từng tầng sẽ khuếch đại request

Giả sử hệ thống có chuỗi gọi:

Client -> Service A -> Service B -> Service C -> Service D

Trong điều kiện bình thường, mỗi request từ Client sẽ tạo đúng 1 request tới D.

Bỗng nhiên, D trả lỗi liên tục. Và các service A, B, C đều được cấu hình gọi tối đa 2 lần (1 lần gọi mặc định và 1 lần gọi retry nếu có lỗi). Vậy C sẽ gọi D 2 lần, sau lần retry nó mới trả lỗi về B. B thấy lỗi cũng retry lại gọi tới C, C lại gọi thêm 2 lần nữa,... rồi A cũng sẽ tương tự.

Kết quả là, A chỉ xử lý 1 request (1 request = 2 lần gọi xuống service đằng sau), B xử lý 2 request, C xử lý 4 request, D xử lý 8 request.

Vậy nếu số lần retry cấu hình ở mỗi node là k, và lỗi lan qua d node, số request gọi tới service lỗi trong mô hình tuyến tính này sẽ là k^d. Chẳng hạn như trường hợp với 4 node trên, số request gọi tới service lỗi D sẽ 2^3 = 8 requests!!! Đây chính là Retry Storm.

Và vấn đề đã rõ, request tăng theo phép nhân chứ không đơn thuần là phép cộng. Tất nhiên, những con số trên là trường hợp xấu nhất theo mô hình đơn giản: downstream liên tục thất bại, và mỗi tầng retry hết số lần cho phép và không có cơ chế bảo vệ khác. Với hệ thống thực tế, lượng request còn phụ thuộc vào cơ chế fan-out, loại lỗi và việc service có tiếp tục gọi các dependency phía sau.

1.2. Retry Storm thực sự nguy hiểm.

Nếu D đang hoạt động bình thường, việc nhận thêm vài request có thể không đáng kể. Nhưng trong trường hợp đang sự cố, 1 chỗ đang chậm lại bị đổ thêm tải vào chắc chắn sẽ như thêm dầu vào lửa.

Ví dụ thằng D đang dính vấn đề với db connection pool. Traffic bình thường của nó là 1500 request/giây, và tải tối đa nó chịu được là 2000 requests/giây. Khi có sự cố hạ tầng, thời gian thực thi query tăng mạnh, D chắc chắn sẽ không đủ khả năng để xử lý kịp lượng request bình thường. Một phần request bắt đầu timeout. Các service phía trên nhận timeout và retry. Lượng request mới lại đổ vào, trong khi những request cũ vẫn đang chiếm connection và chờ xử lý. Lúc này nó thành một vòng lặp:

Backend xử lý chậm -> request timeout nhiều hơn -> upstream tăng số lần retry -> backend nhận thêm traffic -> queue dài hơn -> resource càng cạn -> backend càng chậm hơn...

Đây là một dạng postitive feedback loop. Một lỗi ban đầu tạo ra hành vi khiến chính lỗi đó trở nên nghiêm trọng hơn.

Ở góc độ tài nguyên, vấn đề này còn liên quan trực tiếp concurrency. Theo little's law, trong trạng thái ổn định, số request đồng thời trung bình có thể được tính: Concurrency = Throughput * Response time

Tức là, nếu Response Time, hay latency tăng lên bao nhiêu lần thì lượng công việc cần xử lý đồng thời cũng sẽ tăng lên bấy nhiêu lần. Nếu upstream còn tiếp tục retry, tổng request sẽ lên cao hơn nữa. Thread pool, connection pool, memory, CPU và các hàng đợi tài nguyên khác đều chịu thêm áp lực. Cuối cùng, ngay cả khi nguyên nhân ban đầu đã được khắc phục, backend vẫn có thể chưa phục hồi vì lượng request retry còn tồn đọng tiếp tục dồn tới đòi xử lý.

Đây chính là điểm nguy hiểm của Retry Storm: Retry không đơn thuần làm tăng traffic, mà còn gây duy trì trạng thái quá tải và cản trở quá trình phục hồi của hệ thống.

2. Một số pattern retry truyền thống và giới hạn của từng cái.

2.1. Exponential Backoff và Jitter

Cách đơn giản nhất là thử lại ngay khi gặp lỗi. Cách này có thể phù hợp với một số lỗi tức thời. Nhưng nếu nguyên nhân là backend đang quá tải, việc gửi lại liên tục chỉ khiến tình hình thêm tệ hơn. Exponential Backoff giải quyết bằng cách tăng khoảng thời gian chờ giữa các lần retry.

Một công thức thường gặp:

delay = min(maxDelay, baseDelay × 2^attempt)

Ví dụ với base delay = 100ms, các khoảng chờ có thể là:

100 ms -> 200 ms -> 400 ms -> 800 ms

Với cách này, thay vì gửi request liên tục, client cho backend thêm thời gian để phục hồi.

Tuy nhiên, dễ nhận ra thêm 1 vấn đề khác, nếu tất cả đều sử dụng chung cấu hình backoff, thì chúng cũng sẽ retry vào các thời điểm giống nhau. Ở 100ms cũng 10000 requests, 200ms cũng 10000 requests, 400ms cũng 10000 requests, kiểu thế,... Chúng ta đã giảm tần xuất retry, nhưng lại vô tình tạo ra những đợt traffic đẩy vào cùng lúc.

Để giải quyết vấn đề đó, có thể sử dụng Jitter, bổ sung một khoảng thời gian ngẫu nhiên vào thời gian chờ.

Một biến thể lấy số ngẫu nhiên là Full Jitter:

maxWait = min(maxDelay, baseDelay × 2^attempt)
delay = random(0, maxWait)

Nhờ yếu tố ngẫu nhiên, các client không còn đồng loạt retry vào cùng một thời điểm. Tuy nhiên, lưu ý quan trọng là backoff và jitter giải quyết vấn đề phân bổ retry diễn ra khi nào, nhưng không trực tiếp giải quyết vấn đề service nào được retry. Nên dù các lần retry đã được phân tán theo thời gian, tổng số request gửi tới D vẫn có thể bị nhân lên.

2.2. Retry budget - giới hạn tải tăng thêm

Thay vì quy định mỗi request được retry bao nhiêu lần, hệ thống tự giới hạn tổng tài nguyên được sử dụng hoặc số request được phép retry trong một khoảng thời gian.

Ví dụ giả sử giới hạn retry tương đương 10% lưu lượng ban đầu ở mỗi node. Chẳng hạn bình thường ở A có 1000 request, thì khi D lỗi liên tọi từ A đến B sẽ là 1100 request, B đến C là 1210 request, C đến D là 1331 request. So với việc việc tăng gấp 8 lần traffic, chỉ tăng tầm 33% là một con số dễ kiểm soát hơn rất nhiều.

Nhưng câu hỏi đáng đặt ra vẫn chưa được giải quyết, là tại sao D vẫn phải nhận thêm traffic tăng lên, trong lỗi thực sự bắt nguồn từ service D? Và nếu biết được D là service đang lỗi, chỉ cho phép retry trên đoạn gọi từ service C sang service D, thì tổng số request tới D sẽ chỉ tăng tối đa 10%.

2.3. Circuit breaker, liệu có giải quyết được đa số vấn đề?

Khi nhắc về cơ chế bảo vệ microservice, circuit breaker thường được nói tới.

Ý tưởng là theo dõi trạng thái downstream và sẽ chủ động tạm thời từ chối những lời gọi có khả năng thất bại. Khi circuit ở trạng thái open, caller sẽ không gửi được request tới downstream trong 1 khoảng thời gian, giúp hệ thống có cơ hội phục hồi. Sau 1 khoảng thời gian, circuit có thể mở Half Open để cho một số request đi vào xem downstream đã ổn định chưa.

Văn bản
CLOSED
  |
  | Failure threshold exceeded
  v
OPEN
  |
  | Wait
  v
HALF_OPEN
  |
  +---- Success ----> CLOSED
  |
  +---- Failure ----> OPEN

Tuy nhiên, circuit breaker cũng có những đánh đổi riêng. Một trong những cái khó là ranh giới giữa việc quyết định mở hay đóng. Nếu cấu hình quá nhạy, hệ thống có thể từ chối request dù downstream đã bắt đầu hoạt động trở lại, hoặc nếu cấu hình quá lỏng, circuit có thể mở ra muộn. Ngoài ra, việc mỗi service nếu duy trì 1 circuit breaker riêng thì việc chúng cùng đóng, mở và phục hồi ở các thời điểm khác nhau có thể gây ra biến động tải khó kiểm soát. Circuit breaker còn không tự xác định được một lỗi upstream là lỗi nội bộ service đó hay chỉ là lỗi truyền lên từ một dependency sâu hơn.

2.4. Nói chung là...

Nhìn chung, các kỹ thuật truyền thống giúp kiểm soát ba câu hỏi:

  • Có nên retry không? Dựa trên mã lỗi, loại exception, hay nghiệp vụ quy định.
  • Retry khi nào? Dựa trên backoff, jitter và một số biến số khác
  • Retry bao nhiêu lần? Dựa trên retry count, retry budget

Nhưng trong 1 luồng call rất nhiều tầng, vẫn còn một câu hỏi thứ tư, và đây cũng là câu hỏi mà Uber muốn xử lý: Retry nên diễn ra ở đâu?

3. Error Ownership - Uber xác định vị trí thực sự nên retry thế nào?

Tháng 9 năm nay, Uber Engineering đã công bố bài viết mà mình có gắn link ở đầu bài, mô tả một cơ chế đã được triển khai trên hạ tần RPC của họ nhằm hạn chế retry lan truyền qua nhiều service.

Điểm cốt lõi của giải pháp là Error Ownership - xác định service nào sở hữu lỗi đang được trả về, thay vì mặc định rằng bất kỳ service lỗi nào cũng đáng được retry.

3.1. Phân biệt nguyên nhân và triệu chứng?

Lấy ví dụ với hệ thống đơn giản, có luồng gọi A -> B -> C

C gặp lỗi internal gì đấy trong service và trả về B 500. Và với góc nhìn thằng A, nếu B không có xử lý gì đặc biệt, nó sẽ coi là thằng B đang có lỗi và retry. Trong khi thực chất C là nơi phát sinh, B là nơi truyền và A chỉ là thằng nhìn thấy.

B không có vấn đề gì, nó chỉ thực hiện lỗi vì thằng C thất bại. A đi retry thằng B đồng nghĩa với việc bắt nó thực hiện lại các công việc mà nó đã làm, sau đó lại gọi C lần nữa. Thay vào đó, nếu C là nơi phát sinh lỗi, việc retry nên chỉ được xem xét tại caller trực tiếp của C, tức là thằng B -> C.

Uber gọi service phát sinh lỗi và chịu trách nhiệm là error owner. Và một service trả lỗi vì dependency bắt buộc bị thất bại nên làm sao để được coi như nó đang truyền một symptom, tức là triệu chứng để đoán thông tin được sự thất bại ấy, thay vì thực sự như thể thực sự sở hữu lỗi.

Tất nhiên, error owner ở đây chỉ là khái niệm trong cơ chế điều phối retry, không nhất thiết đồng nghĩa với nó là root cause của vấn đề. Khi điều tra sâu hơn, lỗi có thể đến từ network, database hoặc hạ tầng nằm ngoài phạm vi service.

3.2. Phần thú vị nhất, Service Dependency Analysis - làm sao để biết lỗi thuộc về thằng nào?

Uber không đơn giản là yêu cầu các dev đánh dấu mọi dependency là bắt buộc hay tuỳ chọn. Với hệ thống có nhiều service, endpoint và luồng gọi nội bộ có thể thay đổi liên tục, việc duy trì thủ công các thông tin này rất khó scale nhanh.

Thay vào đó, Uber xây dựng một cơ chế SDA - Service Dependency Analysis.

Đầu tiên, cần phải phân biệt 2 loại dependency:

  • Fail-close dependency

Đây là dependency mà việc nó thất bại thường dẫn tới caller cũng thất bại. Ví dụ: Payment Service -> call Account Validate Service, nếu fail, không thể xác minh thông tin tài khoản -> không thể đi tiếp luồng nghiệp vụ.

  • Fail-open dependency

Đây là loại dependency có thể thất bại nhưng không nhất thiết sẽ khiến caller thất bại. Ví dụ Payment Service gọi Accounting Service hạch toán xong, đến bước call Noti Service thông báo về người dùng bị fail thì sẽ lưu vào đâu đấy sau outbox call gửi lại sau. Nói chung nó như 1 tính năng bổ sung, không nhất thiết làm hỏng cả luồng thanh toán.

SDA cần phân biệt cả 2 loại quan hệ này để tránh quy trách nhiệm nhầm.

Cách mà Uber thu thập thông tin dependency

Uber dùng hạ tầng RPC có middleware cho cả inbound và outbound request.

Một luồng xử lý có middleware ở mấy đầu kiểu như sau:

Văn bản
              Incoming Request
                      |
                      v
              Inbound Middleware
                      |
                      v
                 Application
                      |
                      v
              Outbound Middleware
                      |
                      v
               Downstream Service

Khi inbound request bắt đầu, middleware tạo một định danh request nội bộ và lưu thông tin vào một cấu trúc như Request Tracker. Và các lần gọi outbounds của request đó sử dụng cùng context để ghi lại dependency được gọi và kết quả tương ứng. Đến khi inbound request kết thúc, middleware có thể đối chiếu:

Văn bản
Inbound request: FAILED

Outbound C:      FAILED
Outbound D:      SUCCESS
Outbound E:      SUCCESS

Thông qua số liệu thu thập được trong quá trình vận hành, SDA xác định mối quan hệ giữa lỗi của caller và callee bằng một công thức, mà với Uber, họ dùng công thức sau:

P (fail-close) = (số lần cả caller và callee lỗi)/số lần callee lỗi

Uber công bố các ngưỡng phân loại như sau:

P >= 0,8 thì là Fail Close

P <= 0,2 thì là Fail Open

Trong khoảng còn lại thì không phân loại.

Điểm hay ở thiết kế này là hạ tầng có thể tự quan sát được hành vi thực tế thai vì chỉ dựa trên các khai báo được hard code.

3.3. Claim và Unclaim - truyền thông tin sở hữu lỗi qua RPC

Sau khi xác định được lỗi phát sinh ở đâu, Uber cần một cách để truyền thông tin đó tới các service ở phía trên. Trong bài viết, cơ chế được mô tả là sử dụng header: x-uber-error-claim: true.

Header này biểu thị callee nhận sở hữu lỗi. Khi caller trả về lỗi thuộc một fail-close stream khác, nó có thể trả thông tin unclaimed.

Quay lại với ví dụ A -> B -> C mà C gặp lỗi ở trên.

C phát sinh lỗi, C trả về phản hồi cho B HTTPCODE 500, kèm với header x-uber-error-claim = true, tức C nhận là thằng sở hữu lỗi. B nhận được claim từ C, kiểm tra chính sách và budget cho việc retry. Nếu điều kiện cho phép, B sẽ gọi C thêm một lần nữa.

Nếu retry vẫn thất bại, B vẫn phải trả lỗi về A vì C không thể xử lý, nhưng B biết nguyên nhân nằm ở dependency fail-close là C, nên B không nhận sở hữu lỗi. Và B sẽ phản hồi về cho A với header x-uber-error-claim: false.

A nhận được cái này, nó sẽ hiểu là B unclaimed tức không phải nguồn lỗi gây ra lỗi này, vì thế nó sẽ không retry thêm nữa, mà trả về luôn cho client. Kết quả là retry được giới hạn lại quanh vùng phát sinh lỗi, thay vì lan ra rộng hơn ở call graph.

Trong trường hợp thiếu header, thường khá hay xảy ra vì trong quá trình triển khai thực tế, không phải service nào cũng có đầy đủ middleware hoặc khả năng lan truyền cái thông tin ngữ cảnh đấy, thì Uber vẫn cho phép caller đầu tiên nhận lỗi không có header được xem xét retry, rồi giới hạn tiếp việc retry ở các tầng trên. Điều này giúp cơ chế có thể hoạt động trong một môi trường chưa đồng nhất.

Tuy nhiên, vẫn phải nhớ, claimed không có nghĩa là retry. Việc có thực hiện retry hay không còn phụ thuộc vào việc retry middleware đánh giá chính sách, loại lỗi và giới hạn tài nguyên. Error Ownership chỉ giúp quyết định retry theo nguồn gốc lỗi.

4. Các trường hợp phức tạp và bài học từ Production của Uber

Nếu mọi luồng gọi đều tuyến tính, mọi dependency đều rõ ràng và không có chuyện mất context khi gọi nhau, Error Owner sẽ tương đối dễ dàng để triển khai. Nhưng hệ thống thực tế thì chắc chắn không đơn giản vậy. Uber cũng đã liệt kê một số case khiến việc quy thuộc lỗi trở nên không chính xác.

4.1. Hai lỗi độc lập xuất hiện cùng lúc - Coincidental Errors

Giả sử service A có 2 dependency tuỳ chọn:

Văn bản
         +----> B (fail-open)
         |
         A
         |
         +----> C (fail-open)

B và C đều có thể lỗi mà A vẫn hoàn thành request. Nhưng đồng thời, A cũng có một vấn đề nội bộ gì đấy khiến chính nó trả lỗi trong vài % request. Nếu đúng lúc A gặp lỗi nội bộ của nó mà B hoặc C cũng gặp lỗi, SDA có thể hiểu rằng lỗi đó đến từ dependency.

Làm tí bài toán xác suất, ví dụ cả B và C đều có tỷ lệ lỗi 10%, và độc lập với nhau, thì xác suất ít nhất 1 trong 2 dependency lỗi là:

1 - 0.9 (tỷ lệ thành công)* 2 = 19%.

Nếu A có 10 request lỗi nội bộ, khoảng 1,9 request có thể quan sát thấy cả lỗi từ B hoặc C. Nếu hệ thống chỉ dựa vào việc thấy có request outbound fail để unclaim, nó sẽ quy thuộc nhầm một số lỗi của A. Và hậu quả sẽ làm mất đi cơ hội retry hợp lệ.

Để xử lý vấn đề này, Uber xem xét thông tin tương quan lịch sử của SDA, chỉ unclaim khi lỗi inbound có liên quan tới một fail-close dependency phù hợp.

Như vậy, thấy rằng Error Ownership phải cần bằng giữa hai rủi ro:

  • Claim nhầm -> Retry quá nhiều, retry quá sớm
  • Claim thiếu -> bỏ xót cơ hội retry, có thể gây giảm availability

4.2. Context drop - khi thông tin request bị mất

Cơ chế SDA phụ thuộc và khả năng liên kết inbound request với các outbound request tương ứng. Giả sử luồng A -> B -> C -> D, D trả lỗi nội bộ, nhưng trong quá trình xử lý, C làm mất context, khiến SDA không liên kết được inbound B -> C với outbound C -> D. Hệ thống lúc này có thể nhìn thấy C trả error, nhưng không có outbound failure nào liên quan, và kết luận C là thằng sở hữu lỗi. Lúc này, cả C -> D và B -> C đều có thể phát sinh retry, tương đương với việc retry đã được dịch lên 1 tầng so với vị trí lý tưởng.

Đối với Java, đây là một điểm đáng chú ý khi sử dụng CompletableFuture.supplyAsync(), hoặc annotation @Async , vì nếu thông tin request chỉ được lưu trong Thread Local mà không chuyển được sang thread mới, middleware có thể không còn đủ dữ liệu để liên kết inbound và outbound calls.

Mở rộng ra về việc truyền context thì nó không chỉ hữu ích cho traceId và logging, mà với các engineer ở uber, nó còn ảnh hưởng trực tiếp tới quyết định xử lý lỗi.

4.3. Giữ lại cơ hội để retry khi service trung gian chưa cấu hình retry

Một tính huồng khác là không phải service nào cũng bật retry. Chẳng hạn A -> B -> C, C lỗi, nhưng B không có cấu hình retry việc gọi sang C. Nếu B luôn đánh dấu lỗi từ C là unclaimed, A sẽ không retry được B. Như vậy, cả chuỗi gọi không có bất kỳ cơ hội retry nào, ngay cả với một lỗi tạm thời mà việc retry request có thể khiến nó thành công.

Uber khắc phục việc này bằng cách bổ sung thêm một tín hiệu nội bộ, gọi là retry criteria satisfied, để Retry Middleware và SDA phối hợp việc claim hay unclaimed lỗi. Cái này nếu nói kỹ hơn sẽ cần đào sâu vào phức tạp hơn, và mình chưa tìm hiểu kỹ.

Luồng xử lý sử dụng retry criteria satisfied.

4.4. Error Ownership đã được kiểm chứng trong tình huống thực tế

Vào ngày 18/11/2025, Uber đã gặp một sự cố lớn liên quan đến Core Entity service nằm sâu hơn 5 tầng trong call graph. Service này bắt đầu trả về tỷ lệ lỗi cao do vấn đề hạ tầng, khiến lỗi lan truyền ngược lên nhiều service phục vụ người dùng. Theo bài viết trên của Uber, nếu chỉ sử dụng Retry Budget thông thường, lượng traffic dội vào service đang suy thoái có thể tăng từ 46% tới 135%. Nhưng nhờ cơ chế Error Ownership đã hoạt động trên production, nhiều tầng upstream bị ngăn thực hiện các retry không cần thiết. Và Uber đã công bố kết quả như sau:

  • Một số caller đã được ngăn gửi thêm tới 200000 request
  • Trên toàn bộ mesh, Uber tính toán đã có khoảng 9,5 triệu request retry không cần thiết đã được ngăn chặn
  • Độ sâu tối đa của các tầng retry giảm từ 25 tầng đến 3 tầng

5. Tổng quan lại thì

Retry vốn dĩ là một kỹ thuật quen thuộc và có vẻ đơn giản, nhưng khi đặt vào hệ thống phân tán nhiều tầng, nó liên quan trực tiếp tới availability, latency, throughput, concurrency và cả tính đúng đắn nghiệp vụ. Một cấu hình retry có thể cải thiện đáng kể khả năng thành công của request khi lỗi xảy ra ngẫu nhiên. Nhưng cùng cấu hình đó cũng có thể khiến một backend đang quá tải nhận thêm hàng nghìn request, kéo dài thời gian phục hồi và làm lỗi lan sang các service khác.

Bài học đáng chú ý từ Uber là họ không cố loại bỏ retry. Thay vào đó, họ bổ sung ngữ cảnh để hệ thống có thể quyết định chính xác hơn nơi retry nên được phép diễn ra.

Error Ownership kết hợp với Service Dependency Analysis giúp phân biệt service phát sinh lỗi và service chỉ đang truyền lỗi. Nhờ vậy, retry có thể được giới hạn gần nguồn lỗi thay vì tiếp tục khuếch đại qua toàn bộ call graph. Tuy nhiên, Error Ownership không thay thế cho Backoff, Retry Budget, Circuit Breaker, Deadline hay Idempotency. Các cơ chế này giải quyết những khía cạnh khác nhau và cần được thiết kế phối hợp.

Với những hệ thống nhỏ hơn Uber, có lẽ chúng ta không cần ngay lập tức xây dựng một nền tảng dependency analysis phức tạp. Nhưng vẫn nên bắt đầu bằng việc hiểu rõ call graph, rà soát những tầng đang retry. Đặc biệt trong lĩnh vực fintech, việc request thất bại ở mức network chưa chắc đồng nghĩa với việc nghiệp vụ thất bại. Vì vậy, tính đúng đắn của giao dịch phải luôn được đặt trước mong muốn tăng tỷ lệ request thành công.

Đ
Cảm ơn bạn đã ở lại đọc.

Mình là Đức. Vẫn đang học, đang đi và đang ghi chép.

Đang tải lượt xem…

Chia sẻ

Chia sẻ bài viết

Chia sẻ lên FacebookMở trong tab mới