Các tính năng của ngôn ngữ Q và KDB+ sử dụng ví dụ về dịch vụ thời gian thực

Bạn có thể đọc về nền tảng KDB+, ngôn ngữ lập trình Q, điểm mạnh và điểm yếu của chúng trong phần trước của tôi. Bài viết và ngắn gọn trong phần giới thiệu. Trong bài viết, chúng tôi sẽ triển khai một dịch vụ trên Q để xử lý luồng dữ liệu đến và tính toán các hàm tổng hợp khác nhau mỗi phút ở chế độ “thời gian thực” (tức là sẽ có thời gian để tính toán mọi thứ trước phần dữ liệu tiếp theo). Tính năng chính của Q là nó là ngôn ngữ vectơ cho phép bạn hoạt động không phải với các đối tượng đơn lẻ mà với các mảng, mảng mảng và các đối tượng phức tạp khác của chúng. Các ngôn ngữ như Q và họ hàng của nó là K, J, APL nổi tiếng vì sự ngắn gọn. Thông thường, một chương trình chiếm nhiều màn hình mã bằng ngôn ngữ quen thuộc như Java có thể được viết trên đó bằng một vài dòng. Đây là những gì tôi muốn chứng minh trong bài viết này.

Các tính năng của ngôn ngữ Q và KDB+ sử dụng ví dụ về dịch vụ thời gian thực

Giới thiệu

KDB+ là cơ sở dữ liệu dạng cột tập trung vào lượng dữ liệu rất lớn, được sắp xếp theo một cách cụ thể (chủ yếu theo thời gian). Nó được sử dụng chủ yếu trong các tổ chức tài chính - ngân hàng, quỹ đầu tư, công ty bảo hiểm. Ngôn ngữ Q là ngôn ngữ nội bộ của KDB+ cho phép bạn làm việc hiệu quả với dữ liệu này. Hệ tư tưởng Q là sự ngắn gọn và hiệu quả nhưng lại hy sinh sự rõ ràng. Điều này được chứng minh bởi thực tế là ngôn ngữ vectơ sẽ khó hiểu trong mọi trường hợp và tính ngắn gọn và phong phú của bản ghi cho phép bạn xem phần lớn hơn nhiều của chương trình trên một màn hình, điều này cuối cùng giúp bạn dễ hiểu hơn.

Trong bài viết này, chúng tôi triển khai một chương trình chính thức trong Q và bạn có thể muốn dùng thử. Để làm điều này, bạn sẽ cần Q thực tế. Bạn có thể tải xuống phiên bản 32-bit miễn phí trên trang web của công ty kx - www.kx.com. Ở đó, nếu quan tâm, bạn sẽ tìm thấy thông tin tham khảo về Q, cuốn sách Q Dành cho phàm nhân và nhiều bài viết khác nhau về chủ đề này.

Báo cáo sự cố

Có một nguồn gửi một bảng có dữ liệu cứ sau 25 mili giây. Vì KDB+ được sử dụng chủ yếu trong tài chính nên chúng ta sẽ giả định rằng đây là bảng giao dịch (giao dịch), có các cột sau: thời gian (thời gian tính bằng mili giây), sym (tên công ty trên sàn giao dịch chứng khoán - IBM, AAPL,…), giá (giá mua cổ phiếu), quy mô (quy mô giao dịch). Khoảng thời gian 25 mili giây là tùy ý, không quá nhỏ và không quá dài. Sự hiện diện của nó có nghĩa là dữ liệu đến dịch vụ đã được lưu vào bộ đệm. Sẽ dễ dàng triển khai bộ đệm ở phía dịch vụ, bao gồm bộ đệm động tùy thuộc vào tải hiện tại, nhưng để đơn giản, chúng ta sẽ tập trung vào một khoảng thời gian cố định.

Dịch vụ phải tính mỗi phút cho mỗi ký hiệu đến từ cột sym một tập hợp các hàm tổng hợp - giá tối đa, giá trung bình, kích thước tổng, v.v. thông tin hữu ích. Để đơn giản, chúng ta sẽ giả định rằng tất cả các hàm có thể được tính tăng dần, tức là để có được một giá trị mới, chỉ cần biết hai số - giá trị cũ và giá trị mới là đủ. Ví dụ: các hàm max, Average, sum có thuộc tính này, nhưng hàm trung vị thì không.

Chúng tôi cũng sẽ giả định rằng luồng dữ liệu đến được sắp xếp theo thời gian. Điều này sẽ cho chúng ta cơ hội chỉ làm việc vào phút cuối. Trong thực tế, chỉ cần có thể làm việc với phút hiện tại và phút trước đó là đủ trong trường hợp một số cập nhật bị trễ. Để đơn giản, chúng ta sẽ không xét trường hợp này.

Hàm tổng hợp

Các hàm tổng hợp cần thiết được liệt kê dưới đây. Tôi đã lấy càng nhiều càng tốt để tăng tải cho dịch vụ:

  • cao – giá tối đa – giá tối đa mỗi phút.
  • thấp – giá tối thiểu – giá tối thiểu mỗi phút.
  • firstPrice – giá đầu tiên – giá đầu tiên mỗi phút.
  • giá cuối cùng – giá cuối cùng – giá cuối cùng mỗi phút.
  • firstSize – kích thước đầu tiên – kích thước giao dịch đầu tiên mỗi phút.
  • LastSize – kích thước cuối cùng – quy mô giao dịch cuối cùng trong một phút.
  • numTrades – đếm i – số lượng giao dịch mỗi phút.
  • khối lượng – tổng kích thước – tổng kích thước giao dịch mỗi phút.
  • pvolume – tổng giá – tổng giá mỗi phút, bắt buộc đối với avgPrice.
  • – tổng giá doanh thu*kích thước – tổng khối lượng giao dịch mỗi phút.
  • avgPrice – pvolume%numTrades – giá trung bình mỗi phút.
  • avgSize – khối lượng%numGiao dịch – quy mô giao dịch trung bình mỗi phút.
  • vwap – doanh thu% khối lượng – giá trung bình mỗi phút tính theo quy mô giao dịch.
  • cumVolume – tổng khối lượng – quy mô giao dịch tích lũy trong toàn bộ thời gian.

Hãy thảo luận ngay về một điểm không rõ ràng - cách khởi tạo các cột này lần đầu tiên và cho mỗi phút tiếp theo. Một số cột thuộc loại FirstPrice phải được khởi tạo thành null mỗi lần; giá trị của chúng không được xác định. Các loại khối lượng khác phải luôn được đặt thành 0. Ngoài ra còn có các cột yêu cầu cách tiếp cận kết hợp - ví dụ: cumVolume phải được sao chép từ phút trước và đối với cột đầu tiên được đặt thành 0. Hãy đặt tất cả các tham số này bằng cách sử dụng dữ liệu từ điển loại (tương tự như một bản ghi):

// list ! list – создать словарь, 0n – float null, 0N – long null, `sym – тип символ, `sym1`sym2 – список символов
initWith:`sym`time`high`low`firstPrice`lastPrice`firstSize`lastSize`numTrades`volume`pvolume`turnover`avgPrice`avgSize`vwap`cumVolume!(`;00:00;0n;0n;0n;0n;0N;0N;0;0;0.0;0.0;0n;0n;0n;0);
aggCols:reverse key[initWith] except `sym`time; // список всех вычисляемых колонок, reverse объяснен ниже

Tôi đã thêm sym và thời gian vào từ điển để thuận tiện, bây giờ initWith là một dòng được tạo sẵn từ bảng tổng hợp cuối cùng, nơi vẫn còn để đặt sym và thời gian chính xác. Bạn có thể sử dụng nó để thêm hàng mới vào bảng.

Chúng ta sẽ cần aggCols khi tạo hàm tổng hợp. Danh sách phải được đảo ngược do thứ tự các biểu thức trong Q được đánh giá (từ phải sang trái). Mục đích là để đảm bảo phép tính đi từ cao đến cumVolume, vì một số cột phụ thuộc vào các cột trước đó.

Các cột cần copy sang phút mới từ phút trước, cột sym thêm vào cho tiện:

rollColumns:`sym`cumVolume;

Bây giờ hãy chia các cột thành các nhóm theo cách chúng sẽ được cập nhật. Có thể phân biệt ba loại:

  1. Tích lũy (khối lượng, doanh thu,..) – chúng ta phải cộng giá trị đầu vào vào giá trị trước đó.
  2. Với một điểm đặc biệt (cao, thấp, ..) – giá trị đầu tiên trong phút được lấy từ dữ liệu đến, phần còn lại được tính bằng hàm.
  3. Nghỉ ngơi. Luôn được tính bằng hàm.

Hãy xác định các biến cho các lớp này:

accumulatorCols:`numTrades`volume`pvolume`turnover;
specialCols:`high`low`firstPrice`firstSize;

Thứ tự tính toán

Chúng tôi sẽ cập nhật bảng tổng hợp theo hai giai đoạn. Để đạt hiệu quả, trước tiên chúng tôi thu nhỏ bảng đến sao cho chỉ có một hàng cho mỗi ký tự và phút. Thực tế là tất cả các chức năng của chúng tôi đều mang tính gia tăng và liên kết đảm bảo rằng kết quả của bước bổ sung này sẽ không thay đổi. Bạn có thể thu nhỏ bảng bằng cách chọn:

select high:max price, low:min price … by sym,time.minute from table

Phương pháp này có nhược điểm - tập hợp các cột được tính toán được xác định trước. May mắn thay, trong Q, select cũng được triển khai như một hàm trong đó bạn có thể thay thế các đối số được tạo động:

?[table;whereClause;byClause;selectClause]

Tôi sẽ không mô tả chi tiết định dạng của các đối số; trong trường hợp của chúng tôi, chỉ các biểu thức by và select sẽ không tầm thường và chúng phải là từ điển của các cột biểu mẫu!biểu thức. Do đó, hàm thu nhỏ có thể được định nghĩa như sau:

selExpression:`high`low`firstPrice`lastPrice`firstSize`lastSize`numTrades`volume`pvolume`turnover!parse each ("max price";"min price";"first price";"last price";"first size";"last size";"count i";"sum size";"sum price";"sum price*size"); // each это функция map в Q для одного списка
preprocess:?[;();`sym`time!`sym`time.minute;selExpression];

Để rõ ràng, tôi đã sử dụng hàm phân tích cú pháp, hàm này biến một chuỗi có biểu thức Q thành một giá trị có thể được chuyển đến hàm eval và là giá trị bắt buộc trong hàm chọn. Cũng lưu ý rằng tiền xử lý được định nghĩa là một phép chiếu (tức là một hàm có các đối số được xác định một phần) của hàm chọn, một đối số (bảng) bị thiếu. Nếu chúng ta áp dụng tiền xử lý cho một bảng, chúng ta sẽ nhận được một bảng nén.

Giai đoạn thứ hai là cập nhật bảng tổng hợp. Trước tiên chúng ta hãy viết thuật toán bằng mã giả:

for each sym in inputTable
  idx: row index in agg table for sym+currentTime;
  aggTable[idx;`high]: aggTable[idx;`high] | inputTable[sym;`high];
  aggTable[idx;`volume]: aggTable[idx;`volume] + inputTable[sym;`volume];
  …

Trong Q, người ta thường sử dụng các hàm ánh xạ/thu nhỏ thay vì các vòng lặp. Nhưng vì Q là ngôn ngữ vectơ và chúng ta có thể dễ dàng áp dụng tất cả các thao tác cho tất cả các ký hiệu cùng một lúc, nên với phép tính gần đúng đầu tiên, chúng ta có thể thực hiện mà không cần vòng lặp, thực hiện các thao tác trên tất cả các ký hiệu cùng một lúc:

idx:calcIdx inputTable;
row:aggTable idx;
aggTable[idx;`high]: row[`high] | inputTable`high;
aggTable[idx;`volume]: row[`volume] + inputTable`volume;
…

Nhưng chúng ta có thể đi xa hơn, Q có một toán tử độc đáo và cực kỳ mạnh mẽ - toán tử gán tổng quát. Nó cho phép bạn thay đổi một tập hợp các giá trị trong cấu trúc dữ liệu phức tạp bằng cách sử dụng danh sách các chỉ mục, hàm và đối số. Trong trường hợp của chúng tôi, nó trông như thế này:

idx:calcIdx inputTable;
rows:aggTable idx;
// .[target;(idx0;idx1;..);function;argument] ~ target[idx 0;idx 1;…]: function[target[idx 0;idx 1;…];argument], в нашем случае функция – это присваивание
.[aggTable;(idx;aggCols);:;flip (row[`high] | inputTable`high;row[`volume] + inputTable`volume;…)];

Thật không may, để gán cho một bảng, bạn cần một danh sách các hàng chứ không phải các cột và bạn phải hoán đổi ma trận (danh sách các cột thành danh sách các hàng) bằng hàm lật. Điều này rất tốn kém đối với một bảng lớn, vì vậy thay vào đó, chúng tôi áp dụng phép gán tổng quát cho từng cột riêng biệt bằng cách sử dụng hàm bản đồ (trông giống như dấu nháy đơn):

.[aggTable;;:;]'[(idx;)each aggCols; (row[`high] | inputTable`high;row[`volume] + inputTable`volume;…)];

Chúng tôi lại sử dụng phép chiếu hàm. Cũng lưu ý rằng trong Q, việc tạo danh sách cũng là một hàm và chúng ta có thể gọi nó bằng hàm each(map) để lấy danh sách các danh sách.

Để đảm bảo rằng tập hợp các cột được tính toán không cố định, chúng ta sẽ tạo biểu thức trên một cách linh hoạt. Trước tiên, hãy xác định các hàm để tính toán từng cột, sử dụng các biến row và inp để tham chiếu đến dữ liệu tổng hợp và đầu vào:

aggExpression:`high`low`firstPrice`lastPrice`firstSize`lastSize`avgPrice`avgSize`vwap`cumVolume!
 ("row[`high]|inp`high";"row[`low]&inp`low";"row`firstPrice";"inp`lastPrice";"row`firstSize";"inp`lastSize";"pvolume%numTrades";"volume%numTrades";"turnover%volume";"row[`cumVolume]+inp`volume");

Một số cột là đặc biệt; giá trị đầu tiên của chúng không được tính bằng hàm. Chúng ta có thể xác định rằng đó là giá trị đầu tiên theo cột row[`numTrades] - nếu nó chứa 0 thì giá trị sẽ là giá trị đầu tiên. Q có hàm chọn - ?[Boolean list;list1;list2] - chọn một giá trị từ danh sách 1 hoặc 2 tùy thuộc vào điều kiện trong đối số đầu tiên:

// high -> ?[isFirst;inp`high;row[`high]|inp`high]
// @ - тоже обобщенное присваивание для случая когда индекс неглубокий
@[`aggExpression;specialCols;{[x;y]"?[isFirst;inp`",y,";",x,"]"};string specialCols];

Ở đây tôi gọi một phép gán tổng quát với hàm của mình (một biểu thức trong dấu ngoặc nhọn). Nó nhận giá trị hiện tại (đối số đầu tiên) và một đối số bổ sung mà tôi chuyển vào tham số thứ 4.

Hãy thêm loa pin riêng biệt vì chức năng của chúng giống nhau:

// volume -> row[`volume]+inp`volume
aggExpression[accumulatorCols]:{"row[`",x,"]+inp`",x } each string accumulatorCols;

Đây là một phép gán bình thường theo tiêu chuẩn Q, nhưng tôi đang gán một danh sách các giá trị cùng một lúc. Cuối cùng, hãy tạo chức năng chính:

// ":",/:aggExprs ~ map[{":",x};aggExpr] => ":row[`high]|inp`high" присвоим вычисленное значение переменной, потому что некоторые колонки зависят от уже вычисленных значений
// string[cols],'exprs ~ map[,;string[cols];exprs] => "high:row[`high]|inp`high" завершим создание присваивания. ,’ расшифровывается как map[concat]
// ";" sv exprs – String from Vector (sv), соединяет список строк вставляя “;” посредине
updateAgg:value "{[aggTable;idx;inp] row:aggTable idx; isFirst_0=row`numTrades; .[aggTable;;:;]'[(idx;)each aggCols;(",(";"sv string[aggCols],'":",/:aggExpression aggCols),")]}";

Với biểu thức này, tôi tự động tạo một hàm từ một chuỗi chứa biểu thức tôi đã đưa ra ở trên. Kết quả sẽ như thế này:

{[aggTable;idx;inp] rows:aggTable idx; isFirst_0=row`numTrades; .[aggTable;;:;]'[(idx;)each aggCols ;(cumVolume:row[`cumVolume]+inp`cumVolume;… ; high:?[isFirst;inp`high;row[`high]|inp`high])]}

Thứ tự đánh giá cột bị đảo ngược vì trong Q thứ tự đánh giá là từ phải sang trái.

Bây giờ chúng ta có hai chức năng chính cần thiết cho việc tính toán, chúng ta chỉ cần thêm một chút cơ sở hạ tầng và dịch vụ đã sẵn sàng.

bước cuối cùng

Chúng tôi có các hàm tiền xử lý và updateAgg để thực hiện tất cả công việc. Nhưng vẫn cần phải đảm bảo chuyển đổi chính xác qua từng phút và tính toán các chỉ số để tổng hợp. Trước hết, hãy xác định hàm init:

init:{
  tradeAgg:: 0#enlist[initWith]; // создаем пустую типизированную таблицу, enlist превращает словарь в таблицу, а 0# означает взять 0 элементов из нее
  currTime::00:00; // начнем с 0, :: означает, что присваивание в глобальную переменную
  currSyms::`u#`symbol$(); // `u# - превращает список в дерево, для ускорения поиска элементов
  offset::0; // индекс в tradeAgg, где начинается текущая минута 
  rollCache:: `sym xkey update `u#sym from rollColumns#tradeAgg; // кэш для последних значений roll колонок, таблица с ключом sym
 }

Chúng ta cũng sẽ định nghĩa hàm roll, hàm này sẽ thay đổi phút hiện tại:

roll:{[tm]
  if[currTime>tm; :init[]]; // если перевалили за полночь, то просто вызовем init
  rollCache,::offset _ rollColumns#tradeAgg; // обновим кэш – взять roll колонки из aggTable, обрезать, вставить в rollCache
  offset::count tradeAgg;
  currSyms::`u#`$();
 }

Chúng ta sẽ cần một hàm để thêm các ký tự mới:

addSyms:{[syms]
  currSyms,::syms; // добавим в список известных
  // добавим в таблицу sym, time и rollColumns воспользовавшись обобщенным присваиванием.
  // Функция ^ подставляет значения по умолчанию для roll колонок, если символа нет в кэше. value flip table возвращает список колонок в таблице.
  `tradeAgg upsert @[count[syms]#enlist initWith;`sym`time,cols rc;:;(syms;currTime), (initWith cols rc)^value flip rc:rollCache ([] sym: syms)];
 }

Và cuối cùng, hàm cập nhật (tên truyền thống của hàm này dành cho các dịch vụ Q), được máy khách gọi để thêm dữ liệu:

upd:{[tblName;data] // tblName нам не нужно, но обычно сервис обрабатывает несколько таблиц 
  tm:exec distinct time from data:() xkey preprocess data; // preprocess & calc time
  updMinute[data] each tm; // добавим данные для каждой минуты
};
updMinute:{[data;tm]
  if[tm<>currTime; roll tm; currTime::tm]; // поменяем минуту, если необходимо
  data:select from data where time=tm; // фильтрация
  if[count msyms:syms where not (syms:data`sym)in currSyms; addSyms msyms]; // новые символы
  updateAgg[`tradeAgg;offset+currSyms?syms;data]; // обновим агрегированную таблицу. Функция ? ищет индекс элементов списка справа в списке слева.
 };

Đó là tất cả. Đây là mã hoàn chỉnh của dịch vụ của chúng tôi, như đã hứa, chỉ có vài dòng:

initWith:`sym`time`high`low`firstPrice`lastPrice`firstSize`lastSize`numTrades`volume`pvolume`turnover`avgPrice`avgSize`vwap`cumVolume!(`;00:00;0n;0n;0n;0n;0N;0N;0;0;0.0;0.0;0n;0n;0n;0);
aggCols:reverse key[initWith] except `sym`time;
rollColumns:`sym`cumVolume;

accumulatorCols:`numTrades`volume`pvolume`turnover;
specialCols:`high`low`firstPrice`firstSize;

selExpression:`high`low`firstPrice`lastPrice`firstSize`lastSize`numTrades`volume`pvolume`turnover!parse each ("max price";"min price";"first price";"last price";"first size";"last size";"count i";"sum size";"sum price";"sum price*size");
preprocess:?[;();`sym`time!`sym`time.minute;selExpression];

aggExpression:`high`low`firstPrice`lastPrice`firstSize`lastSize`avgPrice`avgSize`vwap`cumVolume!("row[`high]|inp`high";"row[`low]&inp`low";"row`firstPrice";"inp`lastPrice";"row`firstSize";"inp`lastSize";"pvolume%numTrades";"volume%numTrades";"turnover%volume";"row[`cumVolume]+inp`volume");
@[`aggExpression;specialCols;{"?[isFirst;inp`",y,";",x,"]"};string specialCols];
aggExpression[accumulatorCols]:{"row[`",x,"]+inp`",x } each string accumulatorCols;
updateAgg:value "{[aggTable;idx;inp] row:aggTable idx; isFirst_0=row`numTrades; .[aggTable;;:;]'[(idx;)each aggCols;(",(";"sv string[aggCols],'":",/:aggExpression aggCols),")]}"; / '

init:{
  tradeAgg::0#enlist[initWith];
  currTime::00:00;
  currSyms::`u#`symbol$();
  offset::0;
  rollCache:: `sym xkey update `u#sym from rollColumns#tradeAgg;
 };
roll:{[tm]
  if[currTime>tm; :init[]];
  rollCache,::offset _ rollColumns#tradeAgg;
  offset::count tradeAgg;
  currSyms::`u#`$();
 };
addSyms:{[syms]
  currSyms,::syms;
  `tradeAgg upsert @[count[syms]#enlist initWith;`sym`time,cols rc;:;(syms;currTime),(initWith cols rc)^value flip rc:rollCache ([] sym: syms)];
 };

upd:{[tblName;data] updMinute[data] each exec distinct time from data:() xkey preprocess data};
updMinute:{[data;tm]
  if[tm<>currTime; roll tm; currTime::tm];
  data:select from data where time=tm;
  if[count msyms:syms where not (syms:data`sym)in currSyms; addSyms msyms];
  updateAgg[`tradeAgg;offset+currSyms?syms;data];
 };

Kiểm tra

Hãy kiểm tra hiệu suất của dịch vụ. Để thực hiện việc này, hãy chạy nó trong một quy trình riêng biệt (đặt mã vào tệp service.q) và gọi hàm init:

q service.q –p 5566

q)init[]

Trong bảng điều khiển khác, bắt đầu quá trình Q thứ hai và kết nối với bảng điều khiển đầu tiên:

h:hopen `:host:5566
h:hopen 5566 // если оба на одном хосте

Đầu tiên, chúng ta hãy tạo một danh sách các ký hiệu - 10000 mảnh và thêm chức năng tạo bảng ngẫu nhiên. Trong bảng điều khiển thứ hai:

syms:`IBM`AAPL`GOOG,-9997?`8
rnd:{[n;t] ([] sym:n?syms; time:t+asc n#til 25; price:n?10f; size:n?10)}

Tôi đã thêm ba ký hiệu thực vào danh sách để giúp tìm kiếm chúng trong bảng dễ dàng hơn. Hàm rnd tạo một bảng ngẫu nhiên có n hàng, trong đó thời gian thay đổi từ t đến t+25 mili giây.

Bây giờ bạn có thể thử gửi dữ liệu đến dịch vụ (thêm mười giờ đầu tiên):

{h (`upd;`trade;rnd[10000;x])} each `time$00:00 + til 60*10

Bạn có thể kiểm tra dịch vụ xem bảng đã được cập nhật:

c 25 200
select from tradeAgg where sym=`AAPL
-20#select from tradeAgg where sym=`AAPL

Kết quả:

sym|time|high|low|firstPrice|lastPrice|firstSize|lastSize|numTrades|volume|pvolume|turnover|avgPrice|avgSize|vwap|cumVolume
--|--|--|--|--|--------------------------------
AAPL|09:27|9.258904|9.258904|9.258904|9.258904|8|8|1|8|9.258904|74.07123|9.258904|8|9.258904|2888
AAPL|09:28|9.068162|9.068162|9.068162|9.068162|7|7|1|7|9.068162|63.47713|9.068162|7|9.068162|2895
AAPL|09:31|4.680449|0.2011121|1.620827|0.2011121|1|5|4|14|9.569556|36.84342|2.392389|3.5|2.631673|2909
AAPL|09:33|2.812535|2.812535|2.812535|2.812535|6|6|1|6|2.812535|16.87521|2.812535|6|2.812535|2915
AAPL|09:34|5.099025|5.099025|5.099025|5.099025|4|4|1|4|5.099025|20.3961|5.099025|4|5.099025|2919

Bây giờ chúng ta hãy tiến hành kiểm tra tải để tìm hiểu xem dịch vụ có thể xử lý bao nhiêu dữ liệu mỗi phút. Hãy để tôi nhắc bạn rằng chúng tôi đặt khoảng thời gian cập nhật là 25 mili giây. Theo đó, dịch vụ phải (trung bình) đạt ít nhất 20 mili giây cho mỗi lần cập nhật để người dùng có thời gian yêu cầu dữ liệu. Nhập thông tin sau vào quy trình thứ hai:

tm:10:00:00.000
stressTest:{[n] 1 string[tm]," "; times,::h ({st:.z.T; upd[`trade;x]; .z.T-st};rnd[n;tm]); tm+:25}
start:{[n] times::(); do[4800;stressTest[n]]; -1 " "; `min`avg`med`max!(min times;avg times;med times;max times)}

4800 là hai phút. Bạn có thể thử chạy trước 1000 hàng cứ sau 25 mili giây:

start 1000

Trong trường hợp của tôi, kết quả là khoảng vài mili giây cho mỗi lần cập nhật. Vì vậy tôi sẽ tăng ngay số hàng lên 10.000:

start 10000

Kết quả:

min| 00:00:00.004
avg| 9.191458
med| 9f
max| 00:00:00.030

Một lần nữa, không có gì đặc biệt, nhưng đây là 24 triệu dòng mỗi phút, 400 nghìn dòng mỗi giây. Trong hơn 25 mili giây, quá trình cập nhật chỉ chậm lại 5 lần, rõ ràng là khi phút thay đổi. Hãy tăng lên 100.000:

start 100000

Kết quả:

min| 00:00:00.013
avg| 25.11083
med| 24f
max| 00:00:00.108
q)sum times
00:02:00.532

Như bạn có thể thấy, dịch vụ này hầu như không thể đối phó được, tuy nhiên nó vẫn tồn tại được. Khối lượng dữ liệu như vậy (240 triệu hàng mỗi phút) là cực kỳ lớn; trong những trường hợp như vậy, người ta thường khởi chạy một số bản sao (hoặc thậm chí hàng chục bản sao) của dịch vụ, mỗi bản sao chỉ xử lý một phần ký tự. Tuy nhiên, kết quả vẫn rất ấn tượng đối với một ngôn ngữ thông dịch tập trung chủ yếu vào việc lưu trữ dữ liệu.

Câu hỏi có thể đặt ra là tại sao thời gian lại tăng phi tuyến tính với kích thước của mỗi lần cập nhật. Lý do là hàm thu nhỏ thực chất là hàm C, hiệu quả hơn nhiều so với updateAgg. Bắt đầu từ một kích thước cập nhật nhất định (khoảng 10.000), updateAgg đạt đến mức trần và khi đó thời gian thực hiện của nó không phụ thuộc vào kích thước cập nhật. Nhờ bước sơ bộ Q mà dịch vụ có thể xử lý khối lượng dữ liệu như vậy. Điều này nhấn mạnh tầm quan trọng của việc chọn thuật toán phù hợp khi làm việc với dữ liệu lớn. Một điểm khác là việc lưu trữ dữ liệu chính xác trong bộ nhớ. Nếu dữ liệu không được lưu trữ theo cột hoặc không được sắp xếp theo thời gian thì chúng ta sẽ quen với hiện tượng như lỗi bộ đệm TLB - thiếu địa chỉ trang bộ nhớ trong bộ đệm địa chỉ bộ xử lý. Việc tìm kiếm địa chỉ sẽ mất nhiều thời gian hơn khoảng 30 lần nếu không thành công và nếu dữ liệu bị phân tán, nó có thể làm chậm dịch vụ nhiều lần.

Kết luận

Trong bài viết này, tôi đã chỉ ra rằng cơ sở dữ liệu KDB+ và Q không chỉ phù hợp để lưu trữ dữ liệu lớn và dễ dàng truy cập nó thông qua lựa chọn mà còn để tạo các dịch vụ xử lý dữ liệu có khả năng xử lý hàng trăm triệu hàng/gigabyte dữ liệu ngay cả trong một quá trình Q duy nhất. Bản thân ngôn ngữ Q cho phép triển khai cực kỳ ngắn gọn và hiệu quả các thuật toán liên quan đến xử lý dữ liệu do tính chất vectơ của nó, trình thông dịch phương ngữ SQL tích hợp và một bộ chức năng thư viện rất thành công.

Tôi sẽ lưu ý rằng những điều trên chỉ là một phần những gì Q có thể làm, nó còn có các tính năng độc đáo khác. Ví dụ: một giao thức IPC cực kỳ đơn giản giúp xóa ranh giới giữa các quy trình Q riêng lẻ và cho phép bạn kết hợp hàng trăm quy trình này thành một mạng duy nhất, mạng này có thể được đặt trên hàng chục máy chủ ở các nơi khác nhau trên thế giới.

Nguồn: www.habr.com

Thêm một lời nhận xét