
Knight Capital 2012: Dead code, cờ bị tái sử dụng và 440 triệu USD bốc hơi trong 45 phút
- luandnh
- Devops , System design
- 12 tháng 9, 2026
Mục lục
Ngày 1 tháng 8 năm 2012, 9 giờ 30 sáng theo giờ ET. Chuông mở cửa ở NYSE.
Trong 45 phút sau đó, Knight Capital Group - công ty chiếm khoảng 17% thị phần cổ phiếu Mỹ, xử lý 3.3 tỷ lệnh và hơn 20 tỷ USD khối lượng mỗi ngày - bắn ra thị trường 4 triệu giao dịch, gom về 3.5 tỷ USD thế long ở 80 mã và 3.15 tỷ USD thế short ở 74 mã, rồi bay mất khoảng 440 triệu USD.
Knight bước vào buổi sáng hôm đó với 365 triệu USD tiền mặt. Họ có đúng 48 giờ để gọi vốn trước khi bị xóa sổ.
Thứ giết Knight không phải hacker, không phải flash crash, cũng không phải một thuật toán giao dịch cao siêu bị sai logic. Nó là ba thứ rất đời thường, và cả ba đều nằm trong tầm tay của bất kỳ team backend nào:
- Một đoạn dead code đã bị ngừng dùng từ 7 năm trước nhưng chưa từng bị xóa.
- Một cờ (flag) bị tái sử dụng, gán ý nghĩa mới lên một khe cắm cũ vẫn còn code cũ tham chiếu tới.
- Một quy trình deploy thủ công, không có bước xác minh, và một quyết định rollback biến sự cố 1 node thành thảm họa 8 node.
Phần còn lại của bài là mổ xẻ từng lớp của chuỗi thất bại đó, kèm hai đoạn Go code production-grade để bạn không lặp lại nó.
1. SMARS, RLP và 8 server
Knight vận hành SMARS (Smart Market Access Routing System), bộ định tuyến đặt lệnh tự động nằm giữa trading platform nội bộ và các sàn.
Kiến trúc ở mức cao:
- SMARS nhận parent order từ trading platform. Ví dụ: một broker cần mua 500,000 cổ phiếu.
- Nó chẻ parent order thành nhiều child order nhỏ hơn, gửi tới NYSE, NASDAQ và các venue khác để tìm đối ứng.
- Parent order càng lớn, số child order sinh ra càng nhiều.
- Một order router phân tán parent order vào 8 server SMARS chạy tại một data center ở New Jersey.
Giả định nền tảng của toàn bộ thiết kế là tính đồng nhất: cả 8 node chạy cùng một phiên bản code và phải hành xử giống hệt nhau. Đây là một invariant rất dễ vỡ, và Knight đã làm vỡ nó đúng vào ngày tệ nhất có thể.
Năm 2012, NYSE chuẩn bị ra mắt Retail Liquidity Program (RLP) - cho phép lệnh của nhà đầu tư cá nhân nhận cải thiện giá ở mức dưới một cent. Knight phải cập nhật SMARS để gắn tag và định tuyến các lệnh đủ điều kiện. Bản cập nhật này được viết để thay thế đoạn code không còn dùng trong order router, và đoạn code không còn dùng đó có tên: Power Peg.
2. Power Peg: dead code và cờ bị tái sử dụng
Power Peg ra đời khoảng năm 2003 như một routine phục vụ việc kiểm thử định tuyến. Công việc của nó khá cụ thể: đếm số cổ phiếu đã khớp so với parent order khi các child order được execute, và ra lệnh dừng định tuyến child order một khi parent order đã được khớp đủ. Nói cách khác, Power Peg giữ cuốn sổ cộng dồn.
Knight ngừng sử dụng Power Peg từ khoảng 2005. Nhưng thay vì xóa, họ để nguyên nó trong codebase. Tệ hơn: khi chuyển logic cộng dồn lên một tầng thực thi sớm hơn, phần đếm số lượng bị gỡ khỏi Power Peg - trong khi bản thân Power Peg vẫn còn nằm đó, vẫn còn khả năng được gọi.
Và đây là chi tiết chí mạng. Cờ dùng để kích hoạt Power Peg - một bit trong cấu hình lệnh - bị tái sử dụng cho tính năng RLP mới. Cách đặt vấn đề nghe rất hợp lý trong một buổi chiều deadline: Power Peg sẽ không bao giờ chạy nữa, nên tái dùng cờ của nó là tiết kiệm. Nhưng đó là gán một ý nghĩa mới lên một khe cắm cũ, trong khi code cũ vẫn còn sống và vẫn còn đọc khe đó.
Cách diễn giải chuẩn xác nhất nằm trong SEC Release No. 70694: code RLP mới được viết để thay thế đoạn code không còn dùng, nhưng tính năng Power Peg vẫn hiện diện và vẫn callable tại thời điểm deploy, và code RLP mới đã tái sử dụng cờ vốn dùng để kích hoạt Power Peg.
Warning
Cờ (flag) không phải là một biến boolean. Nó là một API contract ghi trên dây. Khi một cờ được truyền trong payload lệnh tới mọi hệ thống downstream, số hiệu của nó đã trở thành một phần của giao thức. Tái sử dụng cờ đồng nghĩa với việc thay đổi ngữ nghĩa của một giao thức đang chạy - chỉ khác là không ai bump version, không ai review, và không có gì báo lỗi khi hai bên hiểu khác nhau.
Đây là dạng lỗi mà tôi gọi là semantic collision: hai đoạn code, ở hai thời điểm lịch sử khác nhau, cùng đọc một giá trị nhưng hiểu nó theo hai nghĩa hoàn toàn khác nhau. Không có compiler nào bắt được. Chỉ có runtime bắt được - và runtime ở đây là thị trường chứng khoán Mỹ.
3. Deploy: 7 trên 8
Từ ngày 27 đến 31 tháng 7 năm 2012, một kỹ thuật viên của Knight cài đặt bản release mới lên 8 server SMARS. Thủ công. Từng server một.
Không có công cụ deploy tự động. Không có kỹ sư thứ hai review lại. Không có quy trình bằng văn bản nào yêu cầu việc review đó. Không có bước xác minh sau deploy - không ai đối chiếu trạng thái thực tế của từng host với một manifest mong đợi.
Kết quả: 7 server nhận code mới. Một server bị sót. Server thứ 8 vẫn giữ nguyên code Power Peg từ 2003 và chưa bao giờ nhận code RLP. Không ai phát hiện ra, bởi vì tín hiệu duy nhất là sự hiện diện của một file trên một host mà không ai kiểm tra.
graph TD
Parent["`Parent Orders
từ Broker-Dealers`"]
Router["`SMARS Order Router
phân tán vào 8 node`"]
subgraph "Knight Data Center (New Jersey)"
subgraph "Fleet đúng - 7 node"
S1["`Server 1
RLP code (27/07)`"]
S2["`Server 2
RLP code (27/07)`"]
S3["`Servers 3-7
RLP code (27/07)`"]
end
subgraph "Node lệch trạng thái - 1 node"
S8["`Server 8
Power Peg code (2003)
KHÔNG được redeploy`"]
end
end
Venue["`NYSE / NASDAQ
Matching Engines`"]
Parent --> Router
Router --> S1
Router --> S2
Router --> S3
Router --> S8
S1 -->|"child order có kiểm soát"| Venue
S2 -->|"child order có kiểm soát"| Venue
S3 -->|"child order có kiểm soát"| Venue
S8 -->|"child order KHÔNG có kiểm soát"| Venue
style S8 fill:#3b1d1d,stroke:#f38ba8,stroke-width:3px,color:#f5e0dc
Điểm cần nhấn: đây không phải lỗi của riêng người kỹ thuật viên. Cá nhân anh ta làm đúng những gì quy trình cho phép - và quy trình đó cho phép một lỗi nhỏ leo thang thành sự cố toàn hệ thống. Bất kỳ quy trình nào phụ thuộc vào việc con người tự đọc và làm theo hướng dẫn mà không có kiểm chứng tự động đều đang dựng sẵn một quả mìn như vậy. Sai sót có thể nằm ở bản hướng dẫn, ở cách hiểu hướng dẫn, hoặc ở bước thực thi. Cả ba đều không có lưới an toàn.
4. 45 phút
9 giờ 30 phút 00 giây, thị trường mở cửa. SMARS bắt đầu nhận parent order thật.
7 server xử lý đúng. Server thứ 8 đọc bit cờ trong cấu hình lệnh, hiểu theo nghĩa cũ - “kích hoạt Power Peg” - và gọi thẳng vào đoạn code chết từ 2003.
Và Power Peg, đúng như thiết kế cũ của nó, không còn đếm số lượng cộng dồn nữa. Nó cứ định tuyến child order, không có bất kỳ cơ chế nào để biết parent order đã khớp đủ hay chưa. Về mặt hành vi, nó là một vòng lặp vô hạn có quyền truy cập vào đường ống gửi lệnh ra sàn.
| Mốc thời gian | Sự kiện |
|---|---|
| 08:01 | SMARS xử lý các lệnh đủ điều kiện pre-market. Các email tự động bắt đầu được gửi, nội dung báo lỗi “Power Peg disabled”. |
| 08:01 - 09:30 | 97 email tự động được gửi tới nhân sự Knight. Không ai đọc. |
| 09:30:00 | Thị trường mở cửa. Server 8 kích hoạt Power Peg. |
| 09:31 | Nhiều người trên thị trường đã nhận ra có gì đó sai. Lệnh tràn vào với khối lượng bất thường ở một số mã. |
| 09:32 | Nhiều người bắt đầu tự hỏi tại sao hệ thống gây ra chuyện đó vẫn chưa bị dừng lại. |
| 09:30 - 10:15 | Knight thử nhiều biện pháp đối phó nhưng không xác định được nguyên nhân. |
| ~10:15 | Hệ thống cuối cùng được dừng lại, sau 45 phút giao dịch. |
Trong 45 phút đó, 212 parent order đi vào Power Peg. Kết quả: hàng triệu child order được bắn ra, dẫn tới 4 triệu giao dịch trên 154 mã cổ phiếu, với tổng khối lượng hơn 397 triệu cổ phiếu.
Kết quả định vị: Knight gom về khoảng 3.5 tỷ USD net long ở 80 mã và 3.15 tỷ USD net short ở 74 mã. Trong 45 phút đầu tiên, khối lượng khớp của Knight chiếm hơn 50% tổng khối lượng giao dịch ở các mã bị ảnh hưởng, đẩy một số cổ phiếu tăng hơn 10% giá trị, các mã khác giảm theo.
Về mặt kỹ thuật, đây là một bài kiểm tra ngược: bạn có một hệ thống có khả năng gửi lệnh tự động tốc độ cao, và bạn vừa gỡ bỏ đúng cái cơ chế duy nhất khiến nó biết dừng lại.
Hai lỗi vận hành song song với lỗi kỹ thuật:
Không có kill switch. Không tồn tại bất kỳ công tắc tổng nào để ngắt dòng lệnh ra thị trường. Không có runbook nào mô tả phải làm gì khi thuật toán hành xử bất thường. Nhóm kỹ thuật bị ném vào một môi trường giao dịch đang chạy thật, nơi 8 triệu cổ phiếu được giao dịch mỗi phút, và họ phải debug ngay trên production.
Alert không actionable. 97 email từ 8 giờ 01 phút tới 9 giờ 30 phút, tất cả đều liên quan tới SMARS, tất cả đều báo “Power Peg disabled”. Nhưng chúng được sinh ra như thông báo hệ thống chứ không phải cảnh báo sự cố, và không ai thiết kế để con người đọc chúng trong thời gian thực. Về mặt tín hiệu: 97 email bằng 0 tín hiệu.
Warning
Một cảnh báo gửi vào một kênh mà không ai theo dõi trong giờ giao dịch không phải là cảnh báo. Nó là log có thêm bước HTTP. Nếu một thông báo không dẫn tới ai đó phải làm gì trong vòng N phút, thì hệ thống của bạn không có alert - nó chỉ có bằng chứng hậu kiểm.
5. Quyết định rollback chí mạng
Đây là phần đau nhất của câu chuyện, và cũng là phần dạy được nhiều nhất.
Nhóm kỹ thuật Knight không xác định được node nào đang gây ra vấn đề. Họ không có công cụ để so sánh trạng thái thực tế của 8 node với trạng thái mong đợi. Họ biết có gì đó sai với code mới vừa deploy, và phản xạ tự nhiên là: rollback.
Nhưng cách họ rollback là gỡ code mới khỏi những server đã nhận nó đúng. Nói cách khác: họ xóa code đang chạy đúng và để lại code đang chạy sai.
Hiệu ứng rất nhanh và rất tệ. Sau khi gỡ, thêm nhiều parent order kích hoạt Power Peg trên tất cả các server, không chỉ node thứ 8. Một đám cháy ở 1 node biến thành đám cháy ở 8 node.
graph TD
Trigger["`9:30 - Server 8 làm ngập sổ lệnh
Nhóm kỹ thuật không xác định được nguồn`"]
subgraph "Phản ứng (bước chí mạng)"
Decision["`Quyết định: 'rollback'
gỡ code RLP mới`"]
Act["`Gỡ code RLP khỏi
Server 1-7 (các node đang ĐÚNG)`"]
end
subgraph "Hệ quả"
R1["`Servers 1-7
giờ chạy code Power Peg cũ`"]
R8["`Server 8
vẫn chạy Power Peg`"]
end
Outcome["`Cả 8 node cùng kích hoạt Power Peg
đám cháy 1 node -> đám cháy 8 node`"]
Trigger --> Decision
Decision --> Act
Act --> R1
R8 --> Outcome
R1 --> Outcome
Outcome --> Loss["`~440 triệu USD
4 triệu giao dịch / 154 mã`"]
style Outcome fill:#3b1d1d,stroke:#f38ba8,stroke-width:3px,color:#f5e0dc
Nguyên nhân gốc ở đây không phải “rollback là sai”. Rollback là phản xạ đúng. Vấn đề là rollback đã được xem như một thao tác miễn phí, trong khi nó thực chất là một lần deploy nữa theo chiều ngược lại - cũng cần version, cũng cần artifact bất biến, cũng cần xác minh, và trên hết là cần biết mình đang rollback về cái gì.
Trong trường hợp này, rollback trở về trạng thái cũ - và trạng thái cũ chính là thứ chứa Power Peg. Không ai trong nhóm nhận ra điều đó trong lúc sự cố đang diễn ra, vì không có tài liệu nào mô tả việc rollback sẽ kích hoạt lại đoạn code đã chết.
6. Code Go #1: Feature flag an toàn
Bây giờ sang phần code. Bài học đầu tiên: nếu bạn bắt buộc phải dùng feature flag (và bạn sẽ phải dùng), hãy coi nó như một giao thức có version, có chủ sở hữu, có vòng đời - không phải một boolean.
Bốn mục tiêu thiết kế:
- Danh tính bất biến. Mỗi cờ có
FlagIDổn định trên dây. Số hiệu không bao giờ được gán lại. - Chống tái sử dụng. Nếu cùng một
FlagIDđược đăng ký với ngữ nghĩa khác, quá trình khởi động phải thất bại. Fail closed, không fail silent. - Deprecation guard. Cờ deprecated phải có ngày sunset; sau ngày đó,
Resolvetừ chối, không im lặng trả về giá trị mặc định. - Boot-time validation.
ValidateBoot()chạy trongmain()trước khi nhận lệnh đầu tiên.
package flags
import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"sync"
"time"
)
// FlagID là giá trị đi kèm mọi lệnh trên dây. Schema lệnh upstream và mọi
// parser downstream đều phụ thuộc vào những con số này, nên ID là một
// contract vĩnh viễn. Không bao giờ gán lại.
type FlagID uint32
const (
// FlagPowerPeg bị ngừng dùng năm 2005. Khe cắm của nó bị khóa vĩnh viễn.
FlagPowerPeg FlagID = 0x0001
FlagRLPRetail FlagID = 0x0002
)
// FlagState là vòng đời của một cờ. Một cờ không phải là một bool.
type FlagState uint8
const (
StateActive FlagState = iota
StateDeprecated
StateRetired
)
// Spec là contract của một cờ. Hai Spec cùng ID nhưng khác Semantic chính là
// điều kiện đã giết Knight.
type Spec struct {
ID FlagID
Name string
State FlagState
Owner string
Semantic string // hash ổn định của *ý nghĩa* cờ
SunsetAt time.Time
}
func (s Spec) Digest() string {
sum := sha256.Sum256([]byte(fmt.Sprintf("%d|%s|%s", s.ID, s.Name, s.Semantic)))
return hex.EncodeToString(sum[:8])
}
var (
ErrFlagReused = errors.New("flag id already claimed by a different semantic")
ErrFlagRetired = errors.New("flag is retired and may not be activated")
ErrFlagUnknown = errors.New("flag is not registered")
)
// Registry là nguồn chân lý duy nhất về danh tính cờ trong tiến trình.
type Registry struct {
mu sync.RWMutex
specs map[FlagID]Spec
}
func NewRegistry() *Registry { return &Registry{specs: make(map[FlagID]Spec)} }
// Register fail closed. Nếu một ID đã gắn với digest khác, caller nhận lỗi,
// và ValidateBoot biến lỗi đó thành một lần deploy bị từ chối.
func (r *Registry) Register(s Spec) error {
r.mu.Lock()
defer r.mu.Unlock()
prev, ok := r.specs[s.ID]
if ok {
if prev.Digest() != s.Digest() {
return fmt.Errorf("%w: id=%#04x held by %q digest=%s, attempted %q digest=%s",
ErrFlagReused, s.ID, prev.Name, prev.Digest(), s.Name, s.Digest())
}
return nil
}
r.specs[s.ID] = s
return nil
}
// Resolve là cách duy nhất để biến một cờ trên dây thành hành vi. Nó không
// bao giờ im lặng bỏ qua một cờ không xác định hoặc đã chết.
func (r *Registry) Resolve(id FlagID) (Spec, error) {
r.mu.RLock()
defer r.mu.RUnlock()
s, ok := r.specs[id]
if !ok {
return Spec{}, fmt.Errorf("%w: id=%#04x", ErrFlagUnknown, id)
}
if s.State == StateRetired {
return Spec{}, fmt.Errorf("%w: %s", ErrFlagRetired, s.Name)
}
if s.State == StateDeprecated && !s.SunsetAt.IsZero() && time.Now().After(s.SunsetAt) {
return Spec{}, fmt.Errorf("%w: %s passed sunset %s",
ErrFlagRetired, s.Name, s.SunsetAt.Format(time.RFC3339))
}
return s, nil
}
// ValidateBoot phải được gọi trong main() trước khi engine nhận lệnh.
// Lỗi ở đây làm deploy bị hủy: đây là cái guard Knight đã không có.
func (r *Registry) ValidateBoot() error {
r.mu.RLock()
defer r.mu.RUnlock()
for id, s := range r.specs {
if s.Owner == "" {
return fmt.Errorf("flag %#04x (%s): no owner", id, s.Name)
}
if s.State == StateDeprecated && s.SunsetAt.IsZero() {
return fmt.Errorf("flag %#04x (%s): deprecated without a sunset date", id, s.Name)
}
if s.State == StateRetired && s.SunsetAt.IsZero() {
return fmt.Errorf("flag %#04x (%s): retired without a retirement date", id, s.Name)
}
}
return nil
}
Chạy thử với hai kịch bản: đăng ký đúng, và một lập trình viên tương lai cố tái sử dụng khe cắm của Power Peg cho tính năng mới. Output thật từ máy tôi:
reuse attempt : flag id already claimed by a different semantic: id=0x0001 held by "power_peg" digest=39eca3c3e7b0acbc, attempted "power_peg_v2" digest=3805ce384bd2feef
resolve retired: flag is retired and may not be activated: power_peg
boot check : <nil>
Đặt đoạn code này vào codebase của Knight năm 2012: dòng đầu tiên của bản release RLP cố đăng ký ngữ nghĩa mới lên FlagPowerPeg sẽ ném lỗi ngay lúc boot. Tiến trình không khởi động. Deploy bị hủy. Không có lệnh nào ra thị trường. Không có 440 triệu USD nào biến mất.
Chi phí của một guard như vậy là khoảng 80 dòng code và một lệnh gọi trong main().
7. Code Go #2: Circuit breaker & tripwire
Feature flag ngăn được nguyên nhân gốc. Nhưng bạn cũng cần một lớp phòng thủ cuối cùng cho trường hợp bạn không lường trước được nguyên nhân gốc - vì bạn sẽ không lường trước được hết.
Lớp đó là một circuit breaker ở tầng execution, đặt ngay trước đường ống gửi lệnh ra sàn. Yêu cầu thiết kế:
- Kiểm tra cộng dồn theo parent order - đúng cái check mà Power Peg đã đánh mất năm 2005. Vượt cap thì dừng, không thương lượng.
- Hard limits về notional trên từng lệnh, notional theo session, và số child order mỗi giây.
- Kill switch là một
atomic.Bool, idempotent, gọi được từ bất kỳ goroutine nào - kể cả khi phần còn lại của hệ thống đang bận. - Fail closed: mọi child order bắt buộc phải đi qua
Allow(). Không có đường tắt. - Watchdog độc lập theo dõi fill rate trên blotter, cho trường hợp logic phía trước đã hỏng.
package risk
import (
"context"
"errors"
"sync"
"sync/atomic"
"time"
)
var ErrTrip = errors.New("tripwire engaged: outbound order flow halted")
// Limits là các ranh giới cứng pre-trade. Chúng không mang tính khuyến nghị.
type Limits struct {
MaxChildOrdersPerSec float64
MaxNotionalPerOrder float64
MaxSessionNotional float64
MaxSharesPerParent int64 // cái check Power Peg đã đánh mất năm 2005
}
type Breaker struct {
limits Limits
mu sync.Mutex
recentOrders []time.Time
sessionNotion float64
sharesByParent sync.Map // parentID -> *atomic.Int64
armed atomic.Bool
trips atomic.Int64
}
func NewBreaker(l Limits) *Breaker {
b := &Breaker{limits: l}
b.armed.Store(true)
return b
}
func (b *Breaker) Armed() bool { return b.armed.Load() }
func (b *Breaker) Trips() int64 { return b.trips.Load() }
// Kill là công tắc tổng: idempotent, gọi được từ bất kỳ goroutine nào.
// Trong production, đây cũng là chỗ đóng outbound FIX session và page on-call.
func (b *Breaker) Kill(reason string) {
if b.armed.Swap(false) {
b.trips.Add(1)
_ = reason // log, page, đóng outbound session ở đây
}
}
// Allow là cổng duy nhất mà mọi child order phải đi qua. Fail closed.
func (b *Breaker) Allow(parentID string, qty int64, notional float64) error {
if !b.armed.Load() {
return ErrTrip
}
if notional > b.limits.MaxNotionalPerOrder {
b.Kill("single-order notional limit exceeded")
return ErrTrip
}
ctr, _ := b.sharesByParent.LoadOrStore(parentID, new(atomic.Int64))
filled := ctr.(*atomic.Int64).Add(qty)
if filled > b.limits.MaxSharesPerParent {
b.Kill("parent order overfilled: cumulative share cap exceeded")
return ErrTrip
}
b.mu.Lock()
now := time.Now()
cutoff := now.Add(-time.Second)
kept := b.recentOrders[:0]
for _, t := range b.recentOrders {
if t.After(cutoff) {
kept = append(kept, t)
}
}
kept = append(kept, now)
b.recentOrders = kept
rate := float64(len(kept))
b.sessionNotion += notional
total := b.sessionNotion
b.mu.Unlock()
if rate > b.limits.MaxChildOrdersPerSec {
b.Kill("child-order rate limit exceeded")
return ErrTrip
}
if total > b.limits.MaxSessionNotional {
b.Kill("session notional limit exceeded")
return ErrTrip
}
return nil
}
// Watch là watchdog độc lập trên blotter. Nó biến "một con người sẽ nhận ra
// sau mười phút" thành "hệ thống nhận ra trong một tick".
func (b *Breaker) Watch(ctx context.Context, fillRatePerSec func() float64, every time.Duration) {
t := time.NewTicker(every)
defer t.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.C:
if r := fillRatePerSec(); r > b.limits.MaxChildOrdersPerSec {
b.Kill("fill-rate anomaly detected by watchdog")
return
}
}
}
}
Áp vào tình huống Knight: giới hạn MaxSharesPerParent sẽ trip ngay parent order đầu tiên vượt cap. Kịch bản xấu nhất thay vì 397 triệu cổ phiếu, bạn rò rỉ vài nghìn cổ phiếu trước khi Kill() đóng đường ống. Chênh lệch giữa hai con số đó chính là chênh lệch giữa một sự cố có thể kể lại trong retro và một công ty phá sản.
Để ý một chi tiết: Kill() dùng Swap(false) chứ không phải Store(false). Điều đó khiến nó idempotent - nhiều goroutine cùng trip một lúc thì chỉ có một lần chuyển trạng thái, và số đếm trips phản ánh đúng số lần trip thật. Chi tiết nhỏ, nhưng đúng kiểu code bạn cần ở đường dẫn nóng nhất của hệ thống.
8. Bài học hệ thống
Kéo lên một tầm nhìn rộng hơn, thảm họa này là giao điểm của năm vấn đề mà team backend nào cũng gặp.
Dead code không trung tính - nó là một khoản nợ có lãi kép. Code không chạy không có nghĩa là code không hại. Nó vẫn tồn tại trong không gian tên, vẫn chiếm các khe cờ, vẫn có thể bị gọi nhầm. Một hàm chết cộng với một cờ bị tái sử dụng là một quả mìn hẹn giờ. Khi gỡ tính năng, hãy gỡ cả code, không chỉ tắt cờ.
Cờ là giao thức, không phải biến. Nếu một cờ đi qua ranh giới mạng hoặc ranh giới tiến trình, nó là một phần của wire format. Wire format cần version, cần tài liệu, cần migration. Xóa một khe cờ chết và khóa vĩnh viễn nó rẻ hơn rất nhiều so với việc tái sử dụng.
Tính đồng nhất của fleet là một invariant phải được enforce, không phải giả định. “7/8 server chạy code mới” nghe như một tiến độ deploy dở dang. Trong một hệ thống tài chính, nó là một trạng thái không hợp lệ. Hãy dùng manifest hash, drift detection, và các bài kiểm tra boot-time để phát hiện node lệch trạng thái - và coi node lệch trạng thái là sự cố, không phải chi tiết vận hành.
Rollback phải được thiết kế như deploy. Nó cần một artifact bất biến đã biết là tốt, một quy trình đã được test, và một câu trả lời rõ ràng cho câu hỏi “về phiên bản nào”. Nếu artifact trước đó có chứa code chết đang chờ được kích hoạt, thì rollback chính là kích hoạt nó. Việc bạn có cần rollback hay không phụ thuộc vào việc bạn có một điểm quay về an toàn hay không.
Kill switch và runbook là hạ tầng, không phải tùy chọn. Cả ngành đã trả giá để học điều này. Năm 2010, SEC ban hành Rule 15c3-5 (Market Access Rule), yêu cầu broker-dealer có kiểm soát rủi ro pre-trade trước khi cho phép truy cập sàn. Ngày 1 tháng 8 năm 2012, luật này đã có hiệu lực. Knight chưa tuân thủ. SEC Release No. 70694 ngày 16 tháng 10 năm 2013 là hành động thực thi đầu tiên theo rule này: Knight vi phạm vì thiếu quy trình bằng văn bản cho việc deploy phần mềm, thiếu kiểm thử đầy đủ trong môi trường giống production, thiếu quy trình tự động phát hiện lệnh sai trước khi chúng ra thị trường, và thiếu quy trình escalation cho nhân sự kỹ thuật khi thuật toán hành xử bất thường. Knight đồng ý nộp phạt 12 triệu USD.
Còn về hậu quả doanh nghiệp: Knight huy động được 400 triệu USD vốn khẩn cấp từ một nhóm nhà đầu tư (Jefferies, Blackstone, Getco, Stifel, TD Ameritrade và Stephens) trong vài ngày. Cổ đông cũ bị pha loãng nặng. Cuối năm 2012, Getco sáp nhập với Knight, tạo thành KCG Holdings. Năm 2017, Virtu Financial mua lại KCG với giá 1.4 tỷ USD. Toàn bộ vòng đời đó bắt đầu từ một lần copy file sót một server.
9. Chốt
Knight Capital không chết vì một bug logic phức tạp. Họ chết vì bốn quyết định kỹ thuật tưởng như nhỏ và hoàn toàn bình thường:
- Không xóa dead code.
- Tái sử dụng một flag.
- Deploy thủ công không xác minh.
- Rollback mà không biết mình rollback về cái gì.
Không cái nào trong số đó nghe nghiêm trọng khi bạn đang ở giữa sprint và cần ship tính năng cho kịp ngày NYSE ra mắt. Đó chính là lý do chúng nguy hiểm.
Nếu bạn đọc tới đây và thấy codebase của mình có một cờ đã được tái sử dụng, một hàm chết mà không ai dám xóa, một script deploy phải chạy tay từng bước, và không có kill switch nào cho đường ống quan trọng nhất - thì bạn không cần đợi tới lúc thị trường mở cửa để biết kết quả. Bạn đã biết rồi.
Cái bạn còn thiếu chỉ là một buổi chiều để sửa nó.
Nguồn: SEC Release No. 70694 (16/10/2013) - In the Matter of Knight Capital Americas LLC; Doug Seven, “Knightmare: A DevOps Cautionary Tale” (17/04/2014); Knight Capital Group báo cáo thường niên và các bản công bố tài chính liên quan đến sự kiện ngày 01/08/2012.


